diff --git a/adapter/mitm.go b/adapter/mitm.go index b0181739..d3219ca4 100644 --- a/adapter/mitm.go +++ b/adapter/mitm.go @@ -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 -} diff --git a/mitm/engine.go b/mitm/engine.go new file mode 100644 index 00000000..75c27195 --- /dev/null +++ b/mitm/engine.go @@ -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) +} diff --git a/mitm/http.go b/mitm/http.go new file mode 100644 index 00000000..400724a9 --- /dev/null +++ b/mitm/http.go @@ -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 +} diff --git a/mitm/http_surge_url_rewrite.go b/mitm/http_surge_url_rewrite.go new file mode 100644 index 00000000..32b3859c --- /dev/null +++ b/mitm/http_surge_url_rewrite.go @@ -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 + } +} diff --git a/mitm/service.go b/mitm/service.go index aff49d32..06b955a2 100644 --- a/mitm/service.go +++ b/mitm/service.go @@ -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) } diff --git a/option/mitm.go b/option/mitm.go index ff3db2db..2159da18 100644 --- a/option/mitm.go +++ b/option/mitm.go @@ -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"` }