This commit is contained in:
世界 2023-04-03 07:26:16 +08:00
parent 794506c42d
commit dbdb8ef472
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
6 changed files with 264 additions and 27 deletions

View File

@ -2,7 +2,6 @@ package adapter
import (
"context"
"crypto/tls"
"net"
N "github.com/sagernet/sing/common/network"
@ -12,7 +11,3 @@ type MITMService interface {
Service
ProcessConnection(ctx context.Context, conn net.Conn, dialer N.Dialer, metadata InboundContext) (net.Conn, error)
}
type TLSOutbound interface {
NewTLSConnection(ctx context.Context, conn net.Conn, tlsConfig *tls.Config, metadata InboundContext) error
}

13
mitm/engine.go Normal file
View File

@ -0,0 +1,13 @@
package mitm
import (
"context"
"crypto/tls"
"net"
"github.com/sagernet/sing-box/adapter"
)
type Engine interface {
ProcessConnection(ctx context.Context, clientConn net.Conn, serverConn *tls.Conn, metadata adapter.InboundContext) (net.Conn, error)
}

135
mitm/http.go Normal file
View File

@ -0,0 +1,135 @@
package mitm
import (
std_bufio "bufio"
"context"
"crypto/tls"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"io"
"net"
"net/http"
"os"
)
var _ Engine = (*HTTPEngine)(nil)
type HTTPEngine struct {
logger logger.ContextLogger
urlRewriteRules []HTTPHandlerFunc
}
func NewHTTPEngine(logger logger.ContextLogger, options option.MITMHTTPOptions) (*HTTPEngine, error) {
engine := &HTTPEngine{
logger: logger,
}
for i, urlRewritePath := range options.URLRewritePath {
urlRewriteFile, err := os.Open(C.BasePath(urlRewritePath))
if err != nil {
return nil, E.Cause(err, "read url rewrite configuration[", i, "]")
}
urlRewriteRules, err := readSurgeURLRewriteRules(urlRewriteFile)
if err != nil {
return nil, E.Cause(err, "read url rewrite configuration[", i, "] at ", urlRewritePath)
}
engine.urlRewriteRules = append(engine.urlRewriteRules, urlRewriteRules...)
}
return engine, nil
}
func (e *HTTPEngine) ProcessConnection(ctx context.Context, clientConn net.Conn, serverConn *tls.Conn, metadata adapter.InboundContext) (net.Conn, error) {
buffer := buf.NewPacket()
httpRequest, err := http.ReadRequest(std_bufio.NewReader(io.TeeReader(clientConn, buffer)))
if err != nil {
return nil, err
}
e.logger.DebugContext(ctx, "HTTP ", httpRequest.Method, " ", httpRequest.URL.String(), " ", httpRequest.Proto)
var httpServer http.Server
var handled bool
httpConn := &httpMITMConn{Conn: bufio.NewCachedConn(clientConn, buffer.ToOwned()), readOnly: true}
processCtx, cancel := context.WithCancel(ctx)
httpServer.Handler = http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.Method == "PRI" && len(request.Header) == 0 && request.URL.Path == "*" && request.Proto == "HTTP/2.0" {
httpConn.readOnly = false
h2c.NewHandler(httpServer.Handler, new(http2.Server)).ServeHTTP(writer, request)
return
}
defer cancel()
httpConn.readOnly = false
url := *request.URL
url.Scheme = "https"
url.Host = request.Host
urlString := url.String()
for _, rule := range e.urlRewriteRules {
if rule(writer, request, urlString) {
handled = true
break
}
}
if !handled {
httpConn.readOnly = true
}
})
_ = httpServer.Serve(&fixedListener{conn: httpConn})
<-processCtx.Done()
if !handled {
if !httpConn.readOnly {
return nil, E.New("http2 description failed")
}
return bufio.NewCachedConn(clientConn, buffer), nil
}
serverConn.Close()
buffer.Release()
return nil, nil
}
type httpMITMConn struct {
net.Conn
readOnly bool
closed bool
}
func (c *httpMITMConn) Write(p []byte) (n int, err error) {
if c.readOnly {
return 0, os.ErrInvalid
}
return c.Conn.Write(p)
}
func (c *httpMITMConn) Close() error {
c.closed = true
return nil
}
func (c *httpMITMConn) Upstream() any {
return c.Conn
}
type fixedListener struct {
conn net.Conn
}
func (l *fixedListener) Accept() (net.Conn, error) {
conn := l.conn
l.conn = nil
if conn != nil {
return conn, nil
}
return nil, os.ErrClosed
}
func (l *fixedListener) Addr() net.Addr {
return M.Socksaddr{}
}
func (l *fixedListener) Close() error {
return nil
}

View File

@ -0,0 +1,74 @@
package mitm
import (
"bufio"
"errors"
"io"
"net/http"
"os"
"regexp"
"strings"
E "github.com/sagernet/sing/common/exceptions"
)
type HTTPHandlerFunc func(writer http.ResponseWriter, request *http.Request, urlString string) bool
func readSurgeURLRewriteRules(file *os.File) ([]HTTPHandlerFunc, error) {
defer file.Close()
reader := bufio.NewReader(file)
var handlers []HTTPHandlerFunc
for {
lineBytes, _, err := reader.ReadLine()
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, err
}
ruleLine := strings.TrimSpace(string(lineBytes))
if ruleLine == "" || ruleLine[0] == '#' {
continue
}
ruleParts := strings.Split(ruleLine, " ")
if len(ruleParts) != 3 {
return nil, E.New("invalid surge url rewrite line: ", ruleLine)
}
urlRegex, err := regexp.Compile(ruleParts[0])
if err != nil {
return nil, E.Cause(err, "invalid surge url rewrite line (bad regex): ", ruleLine)
}
switch ruleParts[2] {
case "reject":
handlers = append(handlers, surgeURLRewriteReject(urlRegex))
case "header":
// TODO: support header redirect
fallthrough
case "302":
handlers = append(handlers, surgeURLRewrite302(urlRegex, ruleParts[1]))
default:
return nil, E.Cause(err, "invalid surge url rewrite line (unknown acton): ", ruleLine)
}
}
return handlers, nil
}
func surgeURLRewriteReject(urlRegex *regexp.Regexp) HTTPHandlerFunc {
return func(writer http.ResponseWriter, request *http.Request, urlString string) bool {
if !urlRegex.MatchString(urlString) {
return false
}
writer.WriteHeader(404)
return true
}
}
func surgeURLRewrite302(urlRegex *regexp.Regexp, rewriteURL string) HTTPHandlerFunc {
return func(writer http.ResponseWriter, request *http.Request, urlString string) bool {
if !urlRegex.MatchString(urlString) {
return false
}
// use 307 to keep method
http.RedirectHandler(rewriteURL, http.StatusTemporaryRedirect).ServeHTTP(writer, request)
return true
}
}

View File

@ -32,6 +32,7 @@ type Service struct {
keyPath string
watcher *fsnotify.Watcher
insecure bool
engines []Engine
}
func NewService(router adapter.Router, logger logger.ContextLogger, options option.MITMServiceOptions) (*Service, error) {
@ -80,6 +81,14 @@ func NewService(router adapter.Router, logger logger.ContextLogger, options opti
insecure: options.Insecure,
}
if options.HTTP != nil && options.HTTP.Enabled {
engine, err := NewHTTPEngine(logger, common.PtrValueOrDefault(options.HTTP))
if err != nil {
return nil, err
}
service.engines = append(service.engines, engine)
}
return service, nil
}
@ -119,25 +128,30 @@ func (s *Service) ProcessConnection(ctx context.Context, conn net.Conn, dialer N
if err != nil {
return nil, N.HandshakeFailure(conn, err)
}
clientConn := tls.Server(bufio.NewCachedConn(conn, buffer), &tls.Config{
GetConfigForClient: func(info *tls.ClientHelloInfo) (*tls.Config, error) {
var serverConfig tls.Config
serverConfig.Time = s.router.TimeFunc()
if serverConn.ConnectionState().NegotiatedProtocol != "" {
serverConfig.NextProtos = []string{serverConn.ConnectionState().NegotiatedProtocol}
}
serverConfig.ServerName = clientHello.ServerName
serverConfig.MinVersion = tls.VersionTLS10
serverConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return sTLS.GenerateKeyPair(nil, serverConfig.ServerName, s.tlsCertificate)
}
return &serverConfig, nil
},
})
err = clientConn.HandshakeContext(ctx)
var serverConfig tls.Config
serverConfig.Time = s.router.TimeFunc()
if serverConn.ConnectionState().NegotiatedProtocol != "" {
serverConfig.NextProtos = []string{serverConn.ConnectionState().NegotiatedProtocol}
}
serverConfig.ServerName = clientHello.ServerName
serverConfig.MinVersion = tls.VersionTLS10
serverConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return sTLS.GenerateKeyPair(nil, serverConfig.ServerName, s.tlsCertificate)
}
clientTLSConn := tls.Server(bufio.NewCachedConn(conn, buffer), &serverConfig)
err = clientTLSConn.HandshakeContext(ctx)
if err != nil {
return nil, E.Cause(err, "mitm TLS handshake")
}
var clientConn net.Conn = clientTLSConn
for _, engine := range s.engines {
clientConn, err = engine.ProcessConnection(ctx, clientTLSConn, serverConn, metadata)
if conn == nil {
return nil, err
}
}
s.logger.DebugContext(ctx, "mitm TLS handshake success")
return nil, bufio.CopyConn(ctx, clientConn, serverConn)
}

View File

@ -1,10 +1,16 @@
package option
type MITMServiceOptions struct {
Enabled bool `json:"enabled,omitempty"`
Insecure bool `json:"insecure,omitempty"`
Certificate string `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"`
Key string `json:"key,omitempty"`
KeyPath string `json:"key_path,omitempty"`
Enabled bool `json:"enabled,omitempty"`
Insecure bool `json:"insecure,omitempty"`
Certificate string `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"`
Key string `json:"key,omitempty"`
KeyPath string `json:"key_path,omitempty"`
HTTP *MITMHTTPOptions `json:"http,omitempty"`
}
type MITMHTTPOptions struct {
Enabled bool `json:"enabled,omitempty"`
URLRewritePath []string `json:"url_rewrite_path,omitempty"`
}