diff --git a/dialer/http2/h2/dialer.go b/dialer/http2/h2/dialer.go index 63d1420c..ebaa27f0 100644 --- a/dialer/http2/h2/dialer.go +++ b/dialer/http2/h2/dialer.go @@ -93,7 +93,7 @@ func (d *h2Dialer) Dial(ctx context.Context, address string, opts ...dialer.Dial if d.h2c { client.Transport = &http2.Transport{ AllowHTTP: true, - DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { return options.Dialer.Dial(ctx, network, addr) }, } @@ -126,14 +126,11 @@ func (d *h2Dialer) Dial(ctx context.Context, address string, opts ...dialer.Dial } pr, pw := io.Pipe() req := &http.Request{ - Method: http.MethodConnect, - URL: &url.URL{Scheme: scheme, Host: host}, - Header: d.md.header, - ProtoMajor: 2, - ProtoMinor: 0, - Body: pr, - Host: host, - // ContentLength: -1, + Method: http.MethodConnect, + URL: &url.URL{Scheme: scheme, Host: host}, + Header: d.md.header, + Body: pr, + Host: host, } if req.Header == nil { req.Header = make(http.Header) diff --git a/handler/auto/handler.go b/handler/auto/handler.go index 857fc4ff..63b7d386 100644 --- a/handler/auto/handler.go +++ b/handler/auto/handler.go @@ -12,7 +12,7 @@ import ( "github.com/go-gost/gosocks4" "github.com/go-gost/gosocks5" ctxvalue "github.com/go-gost/x/ctx" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/registry" ) @@ -101,7 +101,7 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler return err } - conn = netpkg.NewBufferReaderConn(conn, br) + conn = xnet.NewReadWriteConn(br, conn, conn) switch b[0] { case gosocks4.Ver4: // socks4 if h.socks4Handler != nil { diff --git a/handler/forward/local/handler.go b/handler/forward/local/handler.go index a7c20e2f..8a8eaa40 100644 --- a/handler/forward/local/handler.go +++ b/handler/forward/local/handler.go @@ -134,7 +134,7 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand ro.Time = time.Time{} return h.handleHTTP(ctx, rw, ro2, log) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, ro, log) + return h.handleTLS(ctx, xnet.NewReadWriteConn(rw, rw, conn), ro, log) } } diff --git a/handler/forward/local/sniffing.go b/handler/forward/local/sniffing.go index 51afb60f..b0df864d 100644 --- a/handler/forward/local/sniffing.go +++ b/handler/forward/local/sniffing.go @@ -23,7 +23,7 @@ import ( xbypass "github.com/go-gost/x/bypass" "github.com/go-gost/x/config" ctxvalue "github.com/go-gost/x/ctx" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" xhttp "github.com/go-gost/x/internal/net/http" "github.com/go-gost/x/internal/util/sniffing" tls_util "github.com/go-gost/x/internal/util/tls" @@ -323,16 +323,16 @@ func (h *forwardHandler) httpRoundTrip(ctx context.Context, rw io.ReadWriter, re } if req.Header.Get("Upgrade") == "websocket" { - netpkg.Transport(rw, cc) + xnet.Transport(rw, cc) } return resp.Close, nil } -func (h *forwardHandler) handleTLS(ctx context.Context, rw io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { +func (h *forwardHandler) handleTLS(ctx context.Context, conn net.Conn, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { buf := new(bytes.Buffer) - clientHello, err := dissector.ParseClientHello(io.TeeReader(rw, buf)) + clientHello, err := dissector.ParseClientHello(io.TeeReader(conn, buf)) if err != nil { return err } @@ -429,13 +429,13 @@ func (h *forwardHandler) handleTLS(ctx context.Context, rw io.ReadWriter, ro *xr ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) } - if _, err := buf.WriteTo(rw); err != nil { + if _, err := buf.WriteTo(conn); err != nil { return err } t := time.Now() log.Infof("%s <-> %s", ro.RemoteAddr, addr) - netpkg.Transport(rw, cc) + xnet.Transport(conn, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", ro.RemoteAddr, addr) diff --git a/handler/forward/remote/handler.go b/handler/forward/remote/handler.go index 522f90a8..7647829b 100644 --- a/handler/forward/remote/handler.go +++ b/handler/forward/remote/handler.go @@ -135,7 +135,7 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand ro.Time = time.Time{} return h.handleHTTP(ctx, rw, ro2, log) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, ro, log) + return h.handleTLS(ctx, xnet.NewReadWriteConn(rw, rw, conn), ro, log) } } diff --git a/handler/forward/remote/sniffing.go b/handler/forward/remote/sniffing.go index e4fd50f0..ec264dd4 100644 --- a/handler/forward/remote/sniffing.go +++ b/handler/forward/remote/sniffing.go @@ -23,7 +23,7 @@ import ( xbypass "github.com/go-gost/x/bypass" "github.com/go-gost/x/config" ctxvalue "github.com/go-gost/x/ctx" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" xhttp "github.com/go-gost/x/internal/net/http" "github.com/go-gost/x/internal/net/proxyproto" "github.com/go-gost/x/internal/util/sniffing" @@ -326,16 +326,16 @@ func (h *forwardHandler) httpRoundTrip(ctx context.Context, rw io.ReadWriter, re } if req.Header.Get("Upgrade") == "websocket" { - netpkg.Transport(rw, cc) + xnet.Transport(rw, cc) } return resp.Close, nil } -func (h *forwardHandler) handleTLS(ctx context.Context, rw io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { +func (h *forwardHandler) handleTLS(ctx context.Context, conn net.Conn, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { buf := new(bytes.Buffer) - clientHello, err := dissector.ParseClientHello(io.TeeReader(rw, buf)) + clientHello, err := dissector.ParseClientHello(io.TeeReader(conn, buf)) if err != nil { return err } @@ -432,13 +432,13 @@ func (h *forwardHandler) handleTLS(ctx context.Context, rw io.ReadWriter, ro *xr ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) } - if _, err := buf.WriteTo(rw); err != nil { + if _, err := buf.WriteTo(conn); err != nil { return err } t := time.Now() log.Infof("%s <-> %s", ro.RemoteAddr, addr) - netpkg.Transport(rw, cc) + xnet.Transport(conn, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", ro.RemoteAddr, addr) diff --git a/handler/http/handler.go b/handler/http/handler.go index 0fb12dee..35704378 100644 --- a/handler/http/handler.go +++ b/handler/http/handler.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "crypto/tls" "encoding/base64" "encoding/binary" "errors" @@ -28,12 +29,12 @@ import ( "github.com/go-gost/core/recorder" xbypass "github.com/go-gost/x/bypass" ctxvalue "github.com/go-gost/x/ctx" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" xhttp "github.com/go-gost/x/internal/net/http" limiter_util "github.com/go-gost/x/internal/util/limiter" "github.com/go-gost/x/internal/util/sniffing" stats_util "github.com/go-gost/x/internal/util/stats" + tls_util "github.com/go-gost/x/internal/util/tls" rate_limiter "github.com/go-gost/x/limiter/rate" traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper" stats_wrapper "github.com/go-gost/x/observer/stats/wrapper" @@ -52,6 +53,7 @@ type httpHandler struct { limiter traffic.TrafficLimiter cancel context.CancelFunc recorder recorder.RecorderObject + certPool tls_util.CertPool } func NewHandler(opts ...handler.Option) handler.Handler { @@ -89,6 +91,10 @@ func (h *httpHandler) Init(md md.Metadata) error { } } + if h.md.certificate != nil && h.md.privateKey != nil { + h.certPool = tls_util.NewMemoryCertPool() + } + return nil } @@ -324,7 +330,7 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) } - br := bufio.NewReader(conn) + br := bufio.NewReader(rw) proto, _ := sniffing.Sniff(ctx, br) ro.Proto = proto @@ -332,21 +338,37 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt conn.SetReadDeadline(time.Time{}) } - rw = xio.NewReadWriter(br, conn) + sniffer := &sniffing.Sniffer{ + Recorder: h.recorder.Recorder, + RecorderOptions: h.recorder.Options, + RecorderObject: ro, + Certificate: h.md.certificate, + PrivateKey: h.md.privateKey, + NegotiatedProtocol: h.md.alpn, + CertPool: h.certPool, + MitmBypass: h.md.mitmBypass, + ReadTimeout: h.md.readTimeout, + Log: log, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + return cc, nil + }, + DialTLS: func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) { + return cc, nil + }, + } + + conn = xnet.NewReadWriteConn(br, rw, conn) switch proto { case sniffing.ProtoHTTP: - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - return h.handleHTTP(ctx, rw, cc, ro2, log) + return sniffer.HandleHTTP(ctx, conn) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, cc, ro, log) + return sniffer.HandleTLS(ctx, conn) } } start := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), addr) - netpkg.Transport(rw, cc) + xnet.Transport(rw, cc) log.WithFields(map[string]any{ "duration": time.Since(start), }).Infof("%s >-< %s", conn.RemoteAddr(), addr) @@ -438,7 +460,7 @@ func (h *httpHandler) proxyRoundTrip(ctx context.Context, rw io.ReadWriter, cc n if req.Body != nil { maxSize := opts.MaxBodySize if maxSize <= 0 { - maxSize = defaultBodySize + maxSize = sniffing.DefaultBodySize } reqBody = xhttp.NewBody(req.Body, maxSize) req.Body = reqBody @@ -495,7 +517,7 @@ func (h *httpHandler) proxyRoundTrip(ctx context.Context, rw io.ReadWriter, cc n if opts := h.recorder.Options; opts != nil && opts.HTTPBody { maxSize := opts.MaxBodySize if maxSize <= 0 { - maxSize = defaultBodySize + maxSize = sniffing.DefaultBodySize } respBody = xhttp.NewBody(resp.Body, maxSize) resp.Body = respBody @@ -591,7 +613,7 @@ func (h *httpHandler) authenticate(ctx context.Context, conn net.Conn, req *http defer cc.Close() req.Write(cc) - netpkg.Transport(conn, cc) + xnet.Transport(conn, cc) return case "file": f, _ := os.Open(pr.Value) diff --git a/handler/http/metadata.go b/handler/http/metadata.go index 73d48721..50ed5478 100644 --- a/handler/http/metadata.go +++ b/handler/http/metadata.go @@ -1,12 +1,17 @@ package http import ( + "crypto" + "crypto/tls" + "crypto/x509" "net/http" "strings" "time" + "github.com/go-gost/core/bypass" mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/core/metadata/util" + "github.com/go-gost/x/registry" ) const ( @@ -23,8 +28,14 @@ type metadata struct { authBasicRealm string observePeriod time.Duration proxyAgent string + sniffing bool sniffingTimeout time.Duration + + certificate *x509.Certificate + privateKey crypto.PrivateKey + alpn string + mitmBypass bypass.Bypass } func (h *httpHandler) parseMetadata(md mdata.Metadata) error { @@ -64,6 +75,22 @@ func (h *httpHandler) parseMetadata(md mdata.Metadata) error { h.md.sniffing = mdutil.GetBool(md, "sniffing") h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile") + keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile") + if certFile != "" && keyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + h.md.privateKey = tlsCert.PrivateKey + } + h.md.alpn = mdutil.GetString(md, "mitm.alpn") + h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass")) + return nil } diff --git a/handler/http/sniffing.go b/handler/http/sniffing.go deleted file mode 100644 index fc2861c3..00000000 --- a/handler/http/sniffing.go +++ /dev/null @@ -1,259 +0,0 @@ -package http - -import ( - "bufio" - "bytes" - "context" - "encoding/hex" - "errors" - "io" - "net" - "net/http" - "net/http/httputil" - "strings" - "time" - - "github.com/go-gost/core/bypass" - "github.com/go-gost/core/logger" - dissector "github.com/go-gost/tls-dissector" - xbypass "github.com/go-gost/x/bypass" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" - xhttp "github.com/go-gost/x/internal/net/http" - tls_util "github.com/go-gost/x/internal/util/tls" - xrecorder "github.com/go-gost/x/recorder" -) - -const ( - defaultBodySize = 1024 * 1024 // 1MB -) - -func (h *httpHandler) handleHTTP(ctx context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - br := bufio.NewReader(rw) - req, err := http.ReadRequest(br) - if err != nil { - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - ro.Host = host - - log = log.WithFields(map[string]any{ - "host": host, - }) - - if h.options.Bypass != nil && - h.options.Bypass.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { - log.Debugf("bypass: %s %s", host, req.RequestURI) - return xbypass.ErrBypass - } - - shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log) - if err != nil || shouldClose { - return err - } - - for { - req, err := http.ReadRequest(br) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { - return nil - } - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - if shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { - return err - } - } -} - -func (h *httpHandler) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { - close = true - - if req == nil { - return - } - - start := time.Now() - ro.Time = start - - log.Infof("%s <-> %s", ro.RemoteAddr, req.Host) - defer func() { - ro.Duration = time.Since(start) - if err != nil { - ro.Err = err.Error() - } - if err := ro.Record(ctx, h.recorder.Recorder); err != nil { - log.Errorf("record: %v", err) - } - - log.WithFields(map[string]any{ - "duration": time.Since(start), - }).Infof("%s >-< %s", ro.RemoteAddr, req.Host) - }() - - if clientIP := xhttp.GetClientIP(req); clientIP != nil { - ro.ClientIP = clientIP.String() - } - ro.HTTP = &xrecorder.HTTPRecorderObject{ - Host: req.Host, - Proto: req.Proto, - Scheme: req.URL.Scheme, - Method: req.Method, - URI: req.RequestURI, - Request: xrecorder.HTTPRequestRecorderObject{ - ContentLength: req.ContentLength, - Header: req.Header.Clone(), - }, - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if strings.ToLower(req.Header.Get("Connection")) == "keep-alive" { - req.Header.Del("Connection") - } else { - req.Header.Set("Connection", "close") - } - } - - var reqBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - if req.Body != nil { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - reqBody = xhttp.NewBody(req.Body, maxSize) - req.Body = reqBody - } - } - - if err = req.Write(cc); err != nil { - return - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - resp, err := http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - log.Errorf("read response: %v", err) - return - } - defer resp.Body.Close() - xio.SetReadDeadline(cc, time.Time{}) - - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - ro.HTTP.Response.ContentLength = resp.ContentLength - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(resp, false) - log.Trace(string(dump)) - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if !resp.Close { - resp.Header.Set("Connection", "keep-alive") - } - resp.ProtoMajor = req.ProtoMajor - resp.ProtoMinor = req.ProtoMinor - } - - var respBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - respBody = xhttp.NewBody(resp.Body, maxSize) - resp.Body = respBody - } - - if err = resp.Write(rw); err != nil { - log.Errorf("write response: %v", err) - return - } - - if respBody != nil { - ro.HTTP.Response.Body = respBody.Content() - ro.HTTP.Response.ContentLength = respBody.Length() - } - - if req.Header.Get("Upgrade") == "websocket" { - netpkg.Transport(rw, cc) - } - - return resp.Close, nil -} - -func (h *httpHandler) handleTLS(_ context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - buf := new(bytes.Buffer) - - clientHello, err := dissector.ParseClientHello(io.TeeReader(rw, buf)) - if err != nil { - return err - } - - ro.TLS = &xrecorder.TLSRecorderObject{ - ServerName: clientHello.ServerName, - ClientHello: hex.EncodeToString(buf.Bytes()), - } - if len(clientHello.SupportedProtos) > 0 { - ro.TLS.Proto = clientHello.SupportedProtos[0] - } - - if _, err := buf.WriteTo(cc); err != nil { - return err - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) - xio.SetReadDeadline(cc, time.Time{}) - - if serverHello != nil { - ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String() - ro.TLS.CompressionMethod = serverHello.CompressionMethod - if serverHello.Proto != "" { - ro.TLS.Proto = serverHello.Proto - } - if serverHello.Version > 0 { - ro.TLS.Version = tls_util.Version(serverHello.Version).String() - } - } - - if buf.Len() > 0 { - ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) - } - - if _, err := buf.WriteTo(rw); err != nil { - return err - } - - t := time.Now() - log.Infof("%s <-> %s", ro.RemoteAddr, ro.Host) - netpkg.Transport(rw, cc) - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", ro.RemoteAddr, ro.Host) - - return err -} diff --git a/handler/http2/handler.go b/handler/http2/handler.go index 7c57aa9a..e96ecb9b 100644 --- a/handler/http2/handler.go +++ b/handler/http2/handler.go @@ -28,7 +28,7 @@ import ( xbypass "github.com/go-gost/x/bypass" ctxvalue "github.com/go-gost/x/ctx" xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" xhttp "github.com/go-gost/x/internal/net/http" limiter_util "github.com/go-gost/x/internal/util/limiter" stats_util "github.com/go-gost/x/internal/util/stats" @@ -275,7 +275,7 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req start := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), host) - netpkg.Transport(conn, cc) + xnet.Transport(conn, cc) log.WithFields(map[string]any{ "duration": time.Since(start), }).Infof("%s >-< %s", conn.RemoteAddr(), host) @@ -304,7 +304,7 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req start := time.Now() log.Infof("%s <-> %s", req.RemoteAddr, host) - netpkg.Transport(rw, cc) + xnet.Transport(rw, cc) log.WithFields(map[string]any{ "duration": time.Since(start), }).Infof("%s >-< %s", req.RemoteAddr, host) diff --git a/handler/redirect/tcp/handler.go b/handler/redirect/tcp/handler.go index eb16b542..ecb4c11d 100644 --- a/handler/redirect/tcp/handler.go +++ b/handler/redirect/tcp/handler.go @@ -4,9 +4,10 @@ import ( "bufio" "bytes" "context" + "crypto/tls" "fmt" - "io" "net" + "strings" "time" "github.com/go-gost/core/handler" @@ -14,9 +15,9 @@ import ( "github.com/go-gost/core/recorder" xbypass "github.com/go-gost/x/bypass" ctxvalue "github.com/go-gost/x/ctx" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/util/sniffing" + tls_util "github.com/go-gost/x/internal/util/tls" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" @@ -36,6 +37,7 @@ type redirectHandler struct { md metadata options handler.Options recorder recorder.RecorderObject + certPool tls_util.CertPool } func NewHandler(opts ...handler.Option) handler.Handler { @@ -61,6 +63,10 @@ func (h *redirectHandler) Init(md md.Metadata) (err error) { } } + if h.md.certificate != nil && h.md.privateKey != nil { + h.certPool = tls_util.NewMemoryCertPool() + } + return } @@ -122,7 +128,6 @@ func (h *redirectHandler) Handle(ctx context.Context, conn net.Conn, opts ...han "dst": fmt.Sprintf("%s/%s", dstAddr, dstAddr.Network()), }) - var rw io.ReadWriter = conn if h.md.sniffing { if h.md.sniffingTimeout > 0 { conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) @@ -136,15 +141,59 @@ func (h *redirectHandler) Handle(ctx context.Context, conn net.Conn, opts ...han conn.SetReadDeadline(time.Time{}) } - rw = xio.NewReadWriter(br, conn) + dial := func(ctx context.Context, network, address string) (net.Conn, error) { + var cc net.Conn + var err error + if address != "" { + host, _, _ := net.SplitHostPort(address) + if host == "" { + host = address + } + _, port, _ := net.SplitHostPort(dstAddr.String()) + address = net.JoinHostPort(strings.Trim(host, "[]"), port) + ro.Host = address + + var buf bytes.Buffer + cc, err = h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", address) + ro.Route = buf.String() + if err != nil && !h.md.sniffingFallback { + return nil, err + } + } + + if cc == nil { + var buf bytes.Buffer + cc, err = h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", dstAddr.String()) + ro.Route = buf.String() + ro.Host = dstAddr.String() + } + + return cc, err + } + + sniffer := &sniffing.Sniffer{ + Recorder: h.recorder.Recorder, + RecorderOptions: h.recorder.Options, + RecorderObject: ro, + Certificate: h.md.certificate, + PrivateKey: h.md.privateKey, + NegotiatedProtocol: h.md.alpn, + CertPool: h.certPool, + MitmBypass: h.md.mitmBypass, + ReadTimeout: h.md.readTimeout, + Log: log, + Dial: dial, + DialTLS: func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) { + return dial(ctx, network, address) + }, + } + + conn = xnet.NewReadWriteConn(br, conn, conn) switch proto { case sniffing.ProtoHTTP: - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - return h.handleHTTP(ctx, rw, dstAddr, ro2, log) + return sniffer.HandleHTTP(ctx, conn) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, dstAddr, ro, log) + return sniffer.HandleTLS(ctx, conn) } } @@ -167,7 +216,7 @@ func (h *redirectHandler) Handle(ctx context.Context, conn net.Conn, opts ...han t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), dstAddr) - netpkg.Transport(rw, cc) + xnet.Transport(conn, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", conn.RemoteAddr(), dstAddr) diff --git a/handler/redirect/tcp/metadata.go b/handler/redirect/tcp/metadata.go index 00b6aa33..c2c85e5a 100644 --- a/handler/redirect/tcp/metadata.go +++ b/handler/redirect/tcp/metadata.go @@ -1,18 +1,29 @@ package redirect import ( + "crypto" + "crypto/tls" + "crypto/x509" "time" + "github.com/go-gost/core/bypass" mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/core/metadata/util" + "github.com/go-gost/x/registry" ) type metadata struct { - readTimeout time.Duration - tproxy bool + readTimeout time.Duration + tproxy bool + sniffing bool sniffingTimeout time.Duration sniffingFallback bool + + certificate *x509.Certificate + privateKey crypto.PrivateKey + alpn string + mitmBypass bypass.Bypass } func (h *redirectHandler) parseMetadata(md mdata.Metadata) (err error) { @@ -21,8 +32,26 @@ func (h *redirectHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.readTimeout = 15 * time.Second } h.md.tproxy = mdutil.GetBool(md, "tproxy") + h.md.sniffing = mdutil.GetBool(md, "sniffing") h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") h.md.sniffingFallback = mdutil.GetBool(md, "sniffing.fallback") + + certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile") + keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile") + if certFile != "" && keyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + h.md.privateKey = tlsCert.PrivateKey + } + h.md.alpn = mdutil.GetString(md, "mitm.alpn") + h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass")) + return } diff --git a/handler/redirect/tcp/sniffing.go b/handler/redirect/tcp/sniffing.go deleted file mode 100644 index 76a0cb7b..00000000 --- a/handler/redirect/tcp/sniffing.go +++ /dev/null @@ -1,318 +0,0 @@ -package redirect - -import ( - "bufio" - "bytes" - "context" - "encoding/hex" - "errors" - "io" - "net" - "net/http" - "net/http/httputil" - "strings" - "time" - - "github.com/go-gost/core/bypass" - "github.com/go-gost/core/logger" - dissector "github.com/go-gost/tls-dissector" - xbypass "github.com/go-gost/x/bypass" - ctxvalue "github.com/go-gost/x/ctx" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" - xhttp "github.com/go-gost/x/internal/net/http" - tls_util "github.com/go-gost/x/internal/util/tls" - xrecorder "github.com/go-gost/x/recorder" -) - -func (h *redirectHandler) handleHTTP(ctx context.Context, rw io.ReadWriter, dstAddr net.Addr, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - br := bufio.NewReader(rw) - req, err := http.ReadRequest(br) - if err != nil { - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - ro.Host = host - - log = log.WithFields(map[string]any{ - "host": host, - }) - - if h.options.Bypass != nil && - h.options.Bypass.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { - log.Debugf("bypass: %s %s", host, req.RequestURI) - return xbypass.ErrBypass - } - - var buf bytes.Buffer - cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", host) - ro.Route = buf.String() - if err != nil { - log.Error(err) - if !h.md.sniffingFallback { - return err - } - } - - if cc == nil { - var buf bytes.Buffer - cc, err = h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", dstAddr.String()) - ro.Route = buf.String() - if err != nil { - log.Error(err) - return err - } - } - defer cc.Close() - - if shoudlClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shoudlClose { - return err - } - - for { - req, err := http.ReadRequest(br) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { - return nil - } - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - if shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { - return err - } - } -} - -func (h *redirectHandler) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { - close = true - - if req == nil { - return - } - - start := time.Now() - ro.Time = start - - log.Infof("%s <-> %s", ro.RemoteAddr, req.Host) - defer func() { - ro.Duration = time.Since(start) - if err != nil { - ro.Err = err.Error() - } - if err := ro.Record(ctx, h.recorder.Recorder); err != nil { - log.Errorf("record: %v", err) - } - - log.WithFields(map[string]any{ - "duration": time.Since(start), - }).Infof("%s >-< %s", ro.RemoteAddr, req.Host) - }() - - ro.HTTP = &xrecorder.HTTPRecorderObject{ - Host: req.Host, - Proto: req.Proto, - Scheme: req.URL.Scheme, - Method: req.Method, - URI: req.RequestURI, - Request: xrecorder.HTTPRequestRecorderObject{ - ContentLength: req.ContentLength, - Header: req.Header.Clone(), - }, - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if strings.ToLower(req.Header.Get("Connection")) == "keep-alive" { - req.Header.Del("Connection") - } else { - req.Header.Set("Connection", "close") - } - } - - var reqBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - if req.Body != nil { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - reqBody = xhttp.NewBody(req.Body, maxSize) - req.Body = reqBody - } - } - - if err = req.Write(cc); err != nil { - return - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - resp, err := http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - log.Errorf("read response: %v", err) - return - } - defer resp.Body.Close() - xio.SetReadDeadline(cc, time.Time{}) - - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - ro.HTTP.Response.ContentLength = resp.ContentLength - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(resp, false) - log.Trace(string(dump)) - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if !resp.Close { - resp.Header.Set("Connection", "keep-alive") - } - resp.ProtoMajor = req.ProtoMajor - resp.ProtoMinor = req.ProtoMinor - } - - var respBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - respBody = xhttp.NewBody(resp.Body, maxSize) - resp.Body = respBody - } - - if err = resp.Write(rw); err != nil { - log.Errorf("write response: %v", err) - return - } - - if respBody != nil { - ro.HTTP.Response.Body = respBody.Content() - ro.HTTP.Response.ContentLength = respBody.Length() - } - - if req.Header.Get("Upgrade") == "websocket" { - netpkg.Transport(rw, cc) - } - - return resp.Close, nil -} - -func (h *redirectHandler) handleTLS(ctx context.Context, rw io.ReadWriter, dstAddr net.Addr, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - buf := new(bytes.Buffer) - - clientHello, err := dissector.ParseClientHello(io.TeeReader(rw, buf)) - if err != nil { - return err - } - - ro.TLS = &xrecorder.TLSRecorderObject{ - ServerName: clientHello.ServerName, - ClientHello: hex.EncodeToString(buf.Bytes()), - } - if len(clientHello.SupportedProtos) > 0 { - ro.TLS.Proto = clientHello.SupportedProtos[0] - } - - var cc net.Conn - - host := clientHello.ServerName - if host != "" { - if _, _, err := net.SplitHostPort(host); err != nil { - _, port, _ := net.SplitHostPort(dstAddr.String()) - if port == "" { - port = "443" - } - host = net.JoinHostPort(strings.Trim(host, "[]"), port) - } - log = log.WithFields(map[string]any{ - "host": host, - }) - ro.Host = host - - if h.options.Bypass != nil && - h.options.Bypass.Contains(ctx, "tcp", host) { - log.Debug("bypass: ", host) - return xbypass.ErrBypass - } - - var routeBuf bytes.Buffer - cc, err = h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &routeBuf), "tcp", host) - ro.Route = routeBuf.String() - if err != nil { - log.Error(err) - - if !h.md.sniffingFallback { - return err - } - } - } - - if cc == nil { - var routeBuf bytes.Buffer - cc, err = h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &routeBuf), "tcp", dstAddr.String()) - ro.Route = routeBuf.String() - if err != nil { - log.Error(err) - return err - } - } - defer cc.Close() - - if _, err := buf.WriteTo(cc); err != nil { - return err - } - - cc.SetReadDeadline(time.Now().Add(h.md.readTimeout)) - serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) - cc.SetReadDeadline(time.Time{}) - - if serverHello != nil { - ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String() - ro.TLS.CompressionMethod = serverHello.CompressionMethod - if serverHello.Proto != "" { - ro.TLS.Proto = serverHello.Proto - } - if serverHello.Version > 0 { - ro.TLS.Version = tls_util.Version(serverHello.Version).String() - } - } - - if buf.Len() > 0 { - ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) - } - - if _, err := buf.WriteTo(rw); err != nil { - return err - } - - t := time.Now() - log.Infof("%s <-> %s", ro.RemoteAddr, host) - netpkg.Transport(rw, cc) - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", ro.RemoteAddr, host) - - return err -} diff --git a/handler/redirect/udp/handler.go b/handler/redirect/udp/handler.go index b2c5118b..c878766b 100644 --- a/handler/redirect/udp/handler.go +++ b/handler/redirect/udp/handler.go @@ -12,7 +12,7 @@ import ( "github.com/go-gost/core/recorder" xbypass "github.com/go-gost/x/bypass" ctxvalue "github.com/go-gost/x/ctx" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" @@ -118,7 +118,7 @@ func (h *redirectHandler) Handle(ctx context.Context, conn net.Conn, opts ...han t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), dstAddr) - netpkg.Transport(conn, cc) + xnet.Transport(conn, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", conn.RemoteAddr(), dstAddr) diff --git a/handler/relay/connect.go b/handler/relay/connect.go index 42411df6..590c147e 100644 --- a/handler/relay/connect.go +++ b/handler/relay/connect.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "crypto/tls" "errors" "fmt" "io" @@ -18,7 +19,6 @@ import ( ctxvalue "github.com/go-gost/x/ctx" xnet "github.com/go-gost/x/internal/net" serial "github.com/go-gost/x/internal/util/serial" - xio "github.com/go-gost/x/internal/io" "github.com/go-gost/x/internal/util/sniffing" traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper" stats_wrapper "github.com/go-gost/x/observer/stats/wrapper" @@ -64,12 +64,19 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network ctx = ctxvalue.ContextWithHash(ctx, &ctxvalue.Hash{Source: address}) } - var cc io.ReadWriteCloser + var cc net.Conn switch network { case "unix": cc, err = (&net.Dialer{}).DialContext(ctx, "unix", address) case "serial": - cc, err = serial.OpenPort(serial.ParseConfigFromAddr(address)) + var port io.ReadWriteCloser + port, err = serial.OpenPort(serial.ParseConfigFromAddr(address)) + if err == nil { + cc = &serialConn{ + ReadWriteCloser: port, + port: address, + } + } default: var buf bytes.Buffer cc, err = h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), network, address) @@ -139,7 +146,7 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) } - br := bufio.NewReader(conn) + br := bufio.NewReader(rw) proto, _ := sniffing.Sniff(ctx, br) ro.Proto = proto @@ -147,15 +154,31 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network conn.SetReadDeadline(time.Time{}) } - rw = xio.NewReadWriter(br, conn) + sniffer := &sniffing.Sniffer{ + Recorder: h.recorder.Recorder, + RecorderOptions: h.recorder.Options, + RecorderObject: ro, + Certificate: h.md.certificate, + PrivateKey: h.md.privateKey, + NegotiatedProtocol: h.md.alpn, + CertPool: h.certPool, + MitmBypass: h.md.mitmBypass, + ReadTimeout: h.md.readTimeout, + Log: log, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + return cc, nil + }, + DialTLS: func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) { + return cc, nil + }, + } + + conn = xnet.NewReadWriteConn(br, rw, conn) switch proto { case sniffing.ProtoHTTP: - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - return h.handleHTTP(ctx, rw, cc, ro2, log) + return sniffer.HandleHTTP(ctx, conn) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, cc, ro, log) + return sniffer.HandleTLS(ctx, conn) } } @@ -168,3 +191,44 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network return nil } + +type serialConn struct { + io.ReadWriteCloser + port string +} + +func (c *serialConn) LocalAddr() net.Addr { + return &serialAddr{ + port: "@", + } +} + +func (c *serialConn) RemoteAddr() net.Addr { + return &serialAddr{ + port: c.port, + } +} + +func (c *serialConn) SetDeadline(t time.Time) error { + return nil +} + +func (c *serialConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *serialConn) SetWriteDeadline(t time.Time) error { + return nil +} + +type serialAddr struct { + port string +} + +func (a *serialAddr) Network() string { + return "serial" +} + +func (a *serialAddr) String() string { + return a.port +} diff --git a/handler/relay/forward.go b/handler/relay/forward.go index ba9824ec..b5991375 100644 --- a/handler/relay/forward.go +++ b/handler/relay/forward.go @@ -12,7 +12,7 @@ import ( "github.com/go-gost/core/observer/stats" "github.com/go-gost/relay" ctxvalue "github.com/go-gost/x/ctx" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/limiter/traffic/wrapper" stats_wrapper "github.com/go-gost/x/observer/stats/wrapper" ) @@ -111,7 +111,7 @@ func (h *relayHandler) handleForward(ctx context.Context, conn net.Conn, network t := time.Now() log.Debugf("%s <-> %s", conn.RemoteAddr(), target.Addr) - netpkg.Transport(rw, cc) + xnet.Transport(rw, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Debugf("%s >-< %s", conn.RemoteAddr(), target.Addr) diff --git a/handler/relay/handler.go b/handler/relay/handler.go index 64386050..f61c2423 100644 --- a/handler/relay/handler.go +++ b/handler/relay/handler.go @@ -16,6 +16,7 @@ import ( ctxvalue "github.com/go-gost/x/ctx" limiter_util "github.com/go-gost/x/internal/util/limiter" stats_util "github.com/go-gost/x/internal/util/stats" + tls_util "github.com/go-gost/x/internal/util/tls" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" @@ -39,6 +40,7 @@ type relayHandler struct { limiter traffic.TrafficLimiter cancel context.CancelFunc recorder recorder.RecorderObject + certPool tls_util.CertPool } func NewHandler(opts ...handler.Option) handler.Handler { @@ -76,6 +78,10 @@ func (h *relayHandler) Init(md md.Metadata) (err error) { } } + if h.md.certificate != nil && h.md.privateKey != nil { + h.certPool = tls_util.NewMemoryCertPool() + } + return nil } diff --git a/handler/relay/metadata.go b/handler/relay/metadata.go index 33af1507..0bbe820f 100644 --- a/handler/relay/metadata.go +++ b/handler/relay/metadata.go @@ -1,24 +1,35 @@ package relay import ( + "crypto" + "crypto/tls" + "crypto/x509" "math" "time" + "github.com/go-gost/core/bypass" mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/core/metadata/util" "github.com/go-gost/x/internal/util/mux" + "github.com/go-gost/x/registry" ) type metadata struct { - readTimeout time.Duration - enableBind bool - udpBufferSize int - noDelay bool - hash string - muxCfg *mux.Config - observePeriod time.Duration + readTimeout time.Duration + enableBind bool + udpBufferSize int + noDelay bool + hash string + muxCfg *mux.Config + observePeriod time.Duration + sniffing bool sniffingTimeout time.Duration + + certificate *x509.Certificate + privateKey crypto.PrivateKey + alpn string + mitmBypass bypass.Bypass } func (h *relayHandler) parseMetadata(md mdata.Metadata) (err error) { @@ -53,5 +64,21 @@ func (h *relayHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.sniffing = mdutil.GetBool(md, "sniffing") h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile") + keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile") + if certFile != "" && keyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + h.md.privateKey = tlsCert.PrivateKey + } + h.md.alpn = mdutil.GetString(md, "mitm.alpn") + h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass")) + return } diff --git a/handler/relay/sniffing.go b/handler/relay/sniffing.go deleted file mode 100644 index c7c0ccc2..00000000 --- a/handler/relay/sniffing.go +++ /dev/null @@ -1,259 +0,0 @@ -package relay - -import ( - "bufio" - "bytes" - "context" - "encoding/hex" - "errors" - "io" - "net" - "net/http" - "net/http/httputil" - "strings" - "time" - - "github.com/go-gost/core/bypass" - "github.com/go-gost/core/logger" - dissector "github.com/go-gost/tls-dissector" - xbypass "github.com/go-gost/x/bypass" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" - xhttp "github.com/go-gost/x/internal/net/http" - tls_util "github.com/go-gost/x/internal/util/tls" - xrecorder "github.com/go-gost/x/recorder" -) - -const ( - defaultBodySize = 1024 * 1024 // 1MB -) - -func (h *relayHandler) handleHTTP(ctx context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - br := bufio.NewReader(rw) - req, err := http.ReadRequest(br) - if err != nil { - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - ro.Host = host - - log = log.WithFields(map[string]any{ - "host": host, - }) - - if h.options.Bypass != nil && - h.options.Bypass.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { - log.Debugf("bypass: %s %s", host, req.RequestURI) - return xbypass.ErrBypass - } - - shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log) - if err != nil || shouldClose { - return err - } - - for { - req, err := http.ReadRequest(br) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { - return nil - } - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - if shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { - return err - } - } -} - -func (h *relayHandler) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { - close = true - - if req == nil { - return - } - - start := time.Now() - ro.Time = start - - log.Infof("%s <-> %s", ro.RemoteAddr, req.Host) - defer func() { - ro.Duration = time.Since(start) - if err != nil { - ro.Err = err.Error() - } - if err := ro.Record(ctx, h.recorder.Recorder); err != nil { - log.Errorf("record: %v", err) - } - - log.WithFields(map[string]any{ - "duration": time.Since(start), - }).Infof("%s >-< %s", ro.RemoteAddr, req.Host) - }() - - if clientIP := xhttp.GetClientIP(req); clientIP != nil { - ro.ClientIP = clientIP.String() - } - ro.HTTP = &xrecorder.HTTPRecorderObject{ - Host: req.Host, - Proto: req.Proto, - Scheme: req.URL.Scheme, - Method: req.Method, - URI: req.RequestURI, - Request: xrecorder.HTTPRequestRecorderObject{ - ContentLength: req.ContentLength, - Header: req.Header.Clone(), - }, - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if strings.ToLower(req.Header.Get("Connection")) == "keep-alive" { - req.Header.Del("Connection") - } else { - req.Header.Set("Connection", "close") - } - } - - var reqBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - if req.Body != nil { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - reqBody = xhttp.NewBody(req.Body, maxSize) - req.Body = reqBody - } - } - - if err = req.Write(cc); err != nil { - return - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - resp, err := http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - log.Errorf("read response: %v", err) - return - } - defer resp.Body.Close() - xio.SetReadDeadline(cc, time.Time{}) - - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - ro.HTTP.Response.ContentLength = resp.ContentLength - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(resp, false) - log.Trace(string(dump)) - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if !resp.Close { - resp.Header.Set("Connection", "keep-alive") - } - resp.ProtoMajor = req.ProtoMajor - resp.ProtoMinor = req.ProtoMinor - } - - var respBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - respBody = xhttp.NewBody(resp.Body, maxSize) - resp.Body = respBody - } - - if err = resp.Write(rw); err != nil { - log.Errorf("write response: %v", err) - return - } - - if respBody != nil { - ro.HTTP.Response.Body = respBody.Content() - ro.HTTP.Response.ContentLength = respBody.Length() - } - - if req.Header.Get("Upgrade") == "websocket" { - netpkg.Transport(rw, cc) - } - - return resp.Close, nil -} - -func (h *relayHandler) handleTLS(_ context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - buf := new(bytes.Buffer) - - clientHello, err := dissector.ParseClientHello(io.TeeReader(rw, buf)) - if err != nil { - return err - } - - ro.TLS = &xrecorder.TLSRecorderObject{ - ServerName: clientHello.ServerName, - ClientHello: hex.EncodeToString(buf.Bytes()), - } - if len(clientHello.SupportedProtos) > 0 { - ro.TLS.Proto = clientHello.SupportedProtos[0] - } - - if _, err := buf.WriteTo(cc); err != nil { - return err - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) - xio.SetReadDeadline(cc, time.Time{}) - - if serverHello != nil { - ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String() - ro.TLS.CompressionMethod = serverHello.CompressionMethod - if serverHello.Proto != "" { - ro.TLS.Proto = serverHello.Proto - } - if serverHello.Version > 0 { - ro.TLS.Version = tls_util.Version(serverHello.Version).String() - } - } - - if buf.Len() > 0 { - ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) - } - - if _, err := buf.WriteTo(rw); err != nil { - return err - } - - t := time.Now() - log.Infof("%s <-> %s", ro.RemoteAddr, ro.Host) - netpkg.Transport(rw, cc) - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", ro.RemoteAddr, ro.Host) - - return err -} diff --git a/handler/sni/handler.go b/handler/sni/handler.go index ab49af6b..50560128 100644 --- a/handler/sni/handler.go +++ b/handler/sni/handler.go @@ -2,7 +2,9 @@ package sni import ( "bufio" + "bytes" "context" + "crypto/tls" "errors" "net" "time" @@ -11,8 +13,9 @@ import ( md "github.com/go-gost/core/metadata" "github.com/go-gost/core/recorder" ctxvalue "github.com/go-gost/x/ctx" - xio "github.com/go-gost/x/internal/io" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/util/sniffing" + tls_util "github.com/go-gost/x/internal/util/tls" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" @@ -30,6 +33,7 @@ type sniHandler struct { md metadata options handler.Options recorder recorder.RecorderObject + certPool tls_util.CertPool } func NewHandler(opts ...handler.Option) handler.Handler { @@ -57,6 +61,10 @@ func (h *sniHandler) Init(md md.Metadata) (err error) { } } + if h.md.certificate != nil && h.md.privateKey != nil { + h.certPool = tls_util.NewMemoryCertPool() + } + return nil } @@ -83,14 +91,12 @@ func (h *sniHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler. log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) defer func() { - if !ro.Time.IsZero() { - if err != nil { - ro.Err = err.Error() - } - ro.Duration = time.Since(start) - if err := ro.Record(ctx, h.recorder.Recorder); err != nil { - log.Errorf("record: %v", err) - } + if err != nil { + ro.Err = err.Error() + } + ro.Duration = time.Since(start) + if err := ro.Record(ctx, h.recorder.Recorder); err != nil { + log.Errorf("record: %v", err) } log.WithFields(map[string]any{ @@ -106,15 +112,40 @@ func (h *sniHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler. proto, _ := sniffing.Sniff(ctx, br) ro.Proto = proto - rw := xio.NewReadWriter(br, conn) + sniffer := &sniffing.Sniffer{ + Recorder: h.recorder.Recorder, + RecorderOptions: h.recorder.Options, + RecorderObject: ro, + Certificate: h.md.certificate, + PrivateKey: h.md.privateKey, + NegotiatedProtocol: h.md.alpn, + CertPool: h.certPool, + MitmBypass: h.md.mitmBypass, + ReadTimeout: h.md.readTimeout, + Log: log, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + var buf bytes.Buffer + cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", address) + ro.Route = buf.String() + return cc, err + }, + DialTLS: func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) { + var buf bytes.Buffer + cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", address) + ro.Route = buf.String() + if err != nil { + return nil, err + } + cc = tls.Client(cc, cfg) + return cc, nil + }, + } + conn = xnet.NewReadWriteConn(br, conn, conn) switch proto { case sniffing.ProtoHTTP: - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - return h.handleHTTP(ctx, rw, ro2, log) + return sniffer.HandleHTTP(ctx, conn) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, ro, log) + return sniffer.HandleTLS(ctx, conn) default: return errors.New("unknown traffic") } diff --git a/handler/sni/metadata.go b/handler/sni/metadata.go index 7abb463d..c4bc8a53 100644 --- a/handler/sni/metadata.go +++ b/handler/sni/metadata.go @@ -1,15 +1,25 @@ package sni import ( + "crypto" + "crypto/tls" + "crypto/x509" "time" + "github.com/go-gost/core/bypass" mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/core/metadata/util" + "github.com/go-gost/x/registry" ) type metadata struct { readTimeout time.Duration hash string + + certificate *x509.Certificate + privateKey crypto.PrivateKey + alpn string + mitmBypass bypass.Bypass } func (h *sniHandler) parseMetadata(md mdata.Metadata) (err error) { @@ -18,5 +28,22 @@ func (h *sniHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.readTimeout = 15 * time.Second } h.md.hash = mdutil.GetString(md, "hash") + + certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile") + keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile") + if certFile != "" && keyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + h.md.privateKey = tlsCert.PrivateKey + } + h.md.alpn = mdutil.GetString(md, "mitm.alpn") + h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass")) + return } diff --git a/handler/sni/sniffing.go b/handler/sni/sniffing.go deleted file mode 100644 index ddb19132..00000000 --- a/handler/sni/sniffing.go +++ /dev/null @@ -1,355 +0,0 @@ -package sni - -import ( - "bufio" - "bytes" - "context" - "encoding/base64" - "encoding/binary" - "encoding/hex" - "errors" - "hash/crc32" - "io" - "net" - "net/http" - "net/http/httputil" - "strings" - "time" - - "github.com/go-gost/core/bypass" - "github.com/go-gost/core/logger" - dissector "github.com/go-gost/tls-dissector" - xbypass "github.com/go-gost/x/bypass" - ctxvalue "github.com/go-gost/x/ctx" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" - xhttp "github.com/go-gost/x/internal/net/http" - tls_util "github.com/go-gost/x/internal/util/tls" - xrecorder "github.com/go-gost/x/recorder" -) - -func (h *sniHandler) handleHTTP(ctx context.Context, rw io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - br := bufio.NewReader(rw) - req, err := http.ReadRequest(br) - if err != nil { - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - ro.Host = host - - log = log.WithFields(map[string]any{ - "host": host, - }) - - if h.options.Bypass != nil && - h.options.Bypass.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { - log.Debugf("bypass: %s %s", host, req.RequestURI) - return xbypass.ErrBypass - } - - var buf bytes.Buffer - cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", host) - ro.Route = buf.String() - if err != nil { - log.Error(err) - return err - } - defer cc.Close() - - if shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { - return err - } - - for { - req, err := http.ReadRequest(br) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { - return nil - } - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - if shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { - return err - } - } -} - -func (h *sniHandler) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { - close = true - - if req == nil { - return - } - - start := time.Now() - ro.Time = start - - log.Infof("%s <-> %s", ro.RemoteAddr, req.Host) - defer func() { - ro.Duration = time.Since(start) - if err != nil { - ro.Err = err.Error() - } - if err := ro.Record(ctx, h.recorder.Recorder); err != nil { - log.Errorf("record: %v", err) - } - - log.WithFields(map[string]any{ - "duration": time.Since(start), - }).Infof("%s >-< %s", ro.RemoteAddr, req.Host) - }() - - ro.HTTP = &xrecorder.HTTPRecorderObject{ - Host: req.Host, - Proto: req.Proto, - Scheme: req.URL.Scheme, - Method: req.Method, - URI: req.RequestURI, - Request: xrecorder.HTTPRequestRecorderObject{ - ContentLength: req.ContentLength, - Header: req.Header.Clone(), - }, - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if strings.ToLower(req.Header.Get("Connection")) == "keep-alive" { - req.Header.Del("Connection") - } else { - req.Header.Set("Connection", "close") - } - } - - var reqBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - if req.Body != nil { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - reqBody = xhttp.NewBody(req.Body, maxSize) - req.Body = reqBody - } - } - - if err = req.Write(cc); err != nil { - return - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - resp, err := http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - log.Errorf("read response: %v", err) - return - } - defer resp.Body.Close() - xio.SetReadDeadline(cc, time.Time{}) - - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - ro.HTTP.Response.ContentLength = resp.ContentLength - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(resp, false) - log.Trace(string(dump)) - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if !resp.Close { - resp.Header.Set("Connection", "keep-alive") - } - resp.ProtoMajor = req.ProtoMajor - resp.ProtoMinor = req.ProtoMinor - } - - var respBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - respBody = xhttp.NewBody(resp.Body, maxSize) - resp.Body = respBody - } - - if err = resp.Write(rw); err != nil { - log.Errorf("write response: %v", err) - return - } - - if respBody != nil { - ro.HTTP.Response.Body = respBody.Content() - ro.HTTP.Response.ContentLength = respBody.Length() - } - - if req.Header.Get("Upgrade") == "websocket" { - netpkg.Transport(rw, cc) - } - - return resp.Close, nil -} - -func (h *sniHandler) handleTLS(ctx context.Context, rw io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - buf := new(bytes.Buffer) - host, err := h.decodeHost(io.TeeReader(rw, buf)) - if err != nil { - log.Error(err) - return err - } - - clientHello, err := dissector.ParseClientHello(bytes.NewReader(buf.Bytes())) - if err != nil { - return err - } - - ro.TLS = &xrecorder.TLSRecorderObject{ - ServerName: clientHello.ServerName, - ClientHello: hex.EncodeToString(buf.Bytes()), - } - if len(clientHello.SupportedProtos) > 0 { - ro.TLS.Proto = clientHello.SupportedProtos[0] - } - - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "443") - } - - ro.Host = host - - log = log.WithFields(map[string]any{ - "dst": host, - }) - log.Debugf("%s >> %s", ro.RemoteAddr, host) - - if h.options.Bypass != nil && h.options.Bypass.Contains(ctx, "tcp", host) { - log.Debug("bypass: ", host) - return xbypass.ErrBypass - } - - switch h.md.hash { - case "host": - ctx = ctxvalue.ContextWithHash(ctx, &ctxvalue.Hash{Source: host}) - } - - var routeBuf bytes.Buffer - cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &routeBuf), "tcp", host) - ro.Route = routeBuf.String() - if err != nil { - log.Error(err) - return err - } - defer cc.Close() - - if _, err := buf.WriteTo(cc); err != nil { - return err - } - - cc.SetReadDeadline(time.Now().Add(h.md.readTimeout)) - serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) - cc.SetReadDeadline(time.Time{}) - - if serverHello != nil { - ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String() - ro.TLS.CompressionMethod = serverHello.CompressionMethod - if serverHello.Proto != "" { - ro.TLS.Proto = serverHello.Proto - } - if serverHello.Version > 0 { - ro.TLS.Version = tls_util.Version(serverHello.Version).String() - } - } - - if buf.Len() > 0 { - ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) - } - - if _, err := buf.WriteTo(rw); err != nil { - return err - } - - t := time.Now() - log.Infof("%s <-> %s", ro.RemoteAddr, host) - netpkg.Transport(rw, cc) - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", ro.RemoteAddr, host) - - return err -} - -func (h *sniHandler) decodeHost(r io.Reader) (host string, err error) { - record, err := dissector.ReadRecord(r) - if err != nil { - return - } - clientHello := dissector.ClientHelloMsg{} - if err = clientHello.Decode(record.Opaque); err != nil { - return - } - - var extensions []dissector.Extension - for _, ext := range clientHello.Extensions { - if ext.Type() == 0xFFFE { - b, _ := ext.Encode() - if v, err := h.decodeServerName(string(b)); err == nil { - host = v - } - continue - } - extensions = append(extensions, ext) - } - clientHello.Extensions = extensions - - for _, ext := range clientHello.Extensions { - if ext.Type() == dissector.ExtServerName { - snExtension := ext.(*dissector.ServerNameExtension) - if host == "" { - host = snExtension.Name - } else { - snExtension.Name = host - } - break - } - } - - return -} - -func (h *sniHandler) decodeServerName(s string) (string, error) { - b, err := base64.RawURLEncoding.DecodeString(s) - if err != nil { - return "", err - } - if len(b) < 4 { - return "", errors.New("invalid name") - } - v, err := base64.RawURLEncoding.DecodeString(string(b[4:])) - if err != nil { - return "", err - } - if crc32.ChecksumIEEE(v) != binary.BigEndian.Uint32(b[:4]) { - return "", errors.New("invalid name") - } - return string(v), nil -} diff --git a/handler/socks/v4/handler.go b/handler/socks/v4/handler.go index bb80ccff..70c8bbbc 100644 --- a/handler/socks/v4/handler.go +++ b/handler/socks/v4/handler.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "crypto/tls" "errors" "net" "time" @@ -17,11 +18,11 @@ import ( "github.com/go-gost/core/recorder" "github.com/go-gost/gosocks4" ctxvalue "github.com/go-gost/x/ctx" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" limiter_util "github.com/go-gost/x/internal/util/limiter" "github.com/go-gost/x/internal/util/sniffing" stats_util "github.com/go-gost/x/internal/util/stats" + tls_util "github.com/go-gost/x/internal/util/tls" rate_limiter "github.com/go-gost/x/limiter/rate" traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper" stats_wrapper "github.com/go-gost/x/observer/stats/wrapper" @@ -46,6 +47,7 @@ type socks4Handler struct { limiter traffic.TrafficLimiter cancel context.CancelFunc recorder recorder.RecorderObject + certPool tls_util.CertPool } func NewHandler(opts ...handler.Option) handler.Handler { @@ -83,6 +85,10 @@ func (h *socks4Handler) Init(md md.Metadata) (err error) { } } + if h.md.certificate != nil && h.md.privateKey != nil { + h.certPool = tls_util.NewMemoryCertPool() + } + return nil } @@ -232,7 +238,7 @@ func (h *socks4Handler) handleConnect(ctx context.Context, conn net.Conn, req *g conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) } - br := bufio.NewReader(conn) + br := bufio.NewReader(rw) proto, _ := sniffing.Sniff(ctx, br) ro.Proto = proto @@ -240,24 +246,40 @@ func (h *socks4Handler) handleConnect(ctx context.Context, conn net.Conn, req *g conn.SetReadDeadline(time.Time{}) } - rw = xio.NewReadWriter(br, conn) + sniffer := &sniffing.Sniffer{ + Recorder: h.recorder.Recorder, + RecorderOptions: h.recorder.Options, + RecorderObject: ro, + Certificate: h.md.certificate, + PrivateKey: h.md.privateKey, + NegotiatedProtocol: h.md.alpn, + CertPool: h.certPool, + MitmBypass: h.md.mitmBypass, + ReadTimeout: h.md.readTimeout, + Log: log, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + return cc, nil + }, + DialTLS: func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) { + return cc, nil + }, + } + + conn = xnet.NewReadWriteConn(br, rw, conn) switch proto { case sniffing.ProtoHTTP: - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - return h.handleHTTP(ctx, rw, cc, ro2, log) + return sniffer.HandleHTTP(ctx, conn) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, cc, ro, log) + return sniffer.HandleTLS(ctx, conn) } } t := time.Now() - log.Infof("%s <-> %s", conn.RemoteAddr(), addr) - netpkg.Transport(rw, cc) + log.Infof("%s <-> %s", conn.RemoteAddr(), ro.Host) + xnet.Transport(rw, cc) log.WithFields(map[string]any{ "duration": time.Since(t), - }).Infof("%s >-< %s", conn.RemoteAddr(), addr) + }).Infof("%s >-< %s", conn.RemoteAddr(), ro.Host) return nil } diff --git a/handler/socks/v4/metadata.go b/handler/socks/v4/metadata.go index 79619663..f61d970b 100644 --- a/handler/socks/v4/metadata.go +++ b/handler/socks/v4/metadata.go @@ -1,18 +1,29 @@ package v4 import ( + "crypto" + "crypto/tls" + "crypto/x509" "time" + "github.com/go-gost/core/bypass" mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/core/metadata/util" + "github.com/go-gost/x/registry" ) type metadata struct { - readTimeout time.Duration - hash string - observePeriod time.Duration + readTimeout time.Duration + hash string + observePeriod time.Duration + sniffing bool sniffingTimeout time.Duration + + certificate *x509.Certificate + privateKey crypto.PrivateKey + alpn string + mitmBypass bypass.Bypass } func (h *socks4Handler) parseMetadata(md mdata.Metadata) (err error) { @@ -27,5 +38,21 @@ func (h *socks4Handler) parseMetadata(md mdata.Metadata) (err error) { h.md.sniffing = mdutil.GetBool(md, "sniffing") h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile") + keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile") + if certFile != "" && keyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + h.md.privateKey = tlsCert.PrivateKey + } + h.md.alpn = mdutil.GetString(md, "mitm.alpn") + h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass")) + return } diff --git a/handler/socks/v4/sniffing.go b/handler/socks/v4/sniffing.go deleted file mode 100644 index 4c47f87d..00000000 --- a/handler/socks/v4/sniffing.go +++ /dev/null @@ -1,259 +0,0 @@ -package v4 - -import ( - "bufio" - "bytes" - "context" - "encoding/hex" - "errors" - "io" - "net" - "net/http" - "net/http/httputil" - "strings" - "time" - - "github.com/go-gost/core/bypass" - "github.com/go-gost/core/logger" - dissector "github.com/go-gost/tls-dissector" - xbypass "github.com/go-gost/x/bypass" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" - xhttp "github.com/go-gost/x/internal/net/http" - tls_util "github.com/go-gost/x/internal/util/tls" - xrecorder "github.com/go-gost/x/recorder" -) - -const ( - defaultBodySize = 1024 * 1024 // 1MB -) - -func (h *socks4Handler) handleHTTP(ctx context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - br := bufio.NewReader(rw) - req, err := http.ReadRequest(br) - if err != nil { - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - ro.Host = host - - log = log.WithFields(map[string]any{ - "host": host, - }) - - if h.options.Bypass != nil && - h.options.Bypass.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { - log.Debugf("bypass: %s %s", host, req.RequestURI) - return xbypass.ErrBypass - } - - shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log) - if err != nil || shouldClose { - return err - } - - for { - req, err := http.ReadRequest(br) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { - return nil - } - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - if shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { - return err - } - } -} - -func (h *socks4Handler) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { - close = true - - if req == nil { - return - } - - start := time.Now() - ro.Time = start - - log.Infof("%s <-> %s", ro.RemoteAddr, req.Host) - defer func() { - ro.Duration = time.Since(start) - if err != nil { - ro.Err = err.Error() - } - if err := ro.Record(ctx, h.recorder.Recorder); err != nil { - log.Errorf("record: %v", err) - } - - log.WithFields(map[string]any{ - "duration": time.Since(start), - }).Infof("%s >-< %s", ro.RemoteAddr, req.Host) - }() - - if clientIP := xhttp.GetClientIP(req); clientIP != nil { - ro.ClientIP = clientIP.String() - } - ro.HTTP = &xrecorder.HTTPRecorderObject{ - Host: req.Host, - Proto: req.Proto, - Scheme: req.URL.Scheme, - Method: req.Method, - URI: req.RequestURI, - Request: xrecorder.HTTPRequestRecorderObject{ - ContentLength: req.ContentLength, - Header: req.Header.Clone(), - }, - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if strings.ToLower(req.Header.Get("Connection")) == "keep-alive" { - req.Header.Del("Connection") - } else { - req.Header.Set("Connection", "close") - } - } - - var reqBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - if req.Body != nil { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - reqBody = xhttp.NewBody(req.Body, maxSize) - req.Body = reqBody - } - } - - if err = req.Write(cc); err != nil { - return - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - resp, err := http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - log.Errorf("read response: %v", err) - return - } - defer resp.Body.Close() - xio.SetReadDeadline(cc, time.Time{}) - - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - ro.HTTP.Response.ContentLength = resp.ContentLength - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(resp, false) - log.Trace(string(dump)) - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if !resp.Close { - resp.Header.Set("Connection", "keep-alive") - } - resp.ProtoMajor = req.ProtoMajor - resp.ProtoMinor = req.ProtoMinor - } - - var respBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - respBody = xhttp.NewBody(resp.Body, maxSize) - resp.Body = respBody - } - - if err = resp.Write(rw); err != nil { - log.Errorf("write response: %v", err) - return - } - - if respBody != nil { - ro.HTTP.Response.Body = respBody.Content() - ro.HTTP.Response.ContentLength = respBody.Length() - } - - if req.Header.Get("Upgrade") == "websocket" { - netpkg.Transport(rw, cc) - } - - return resp.Close, nil -} - -func (h *socks4Handler) handleTLS(_ context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - buf := new(bytes.Buffer) - - clientHello, err := dissector.ParseClientHello(io.TeeReader(rw, buf)) - if err != nil { - return err - } - - ro.TLS = &xrecorder.TLSRecorderObject{ - ServerName: clientHello.ServerName, - ClientHello: hex.EncodeToString(buf.Bytes()), - } - if len(clientHello.SupportedProtos) > 0 { - ro.TLS.Proto = clientHello.SupportedProtos[0] - } - - if _, err := buf.WriteTo(cc); err != nil { - return err - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) - xio.SetReadDeadline(cc, time.Time{}) - - if serverHello != nil { - ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String() - ro.TLS.CompressionMethod = serverHello.CompressionMethod - if serverHello.Proto != "" { - ro.TLS.Proto = serverHello.Proto - } - if serverHello.Version > 0 { - ro.TLS.Version = tls_util.Version(serverHello.Version).String() - } - } - - if buf.Len() > 0 { - ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) - } - - if _, err := buf.WriteTo(rw); err != nil { - return err - } - - t := time.Now() - log.Infof("%s <-> %s", ro.RemoteAddr, ro.Host) - netpkg.Transport(rw, cc) - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", ro.RemoteAddr, ro.Host) - - return err -} diff --git a/handler/socks/v5/bind.go b/handler/socks/v5/bind.go index 6c7a798e..b9ace68c 100644 --- a/handler/socks/v5/bind.go +++ b/handler/socks/v5/bind.go @@ -8,7 +8,6 @@ import ( "github.com/go-gost/core/logger" "github.com/go-gost/gosocks5" - netpkg "github.com/go-gost/x/internal/net" xnet "github.com/go-gost/x/internal/net" ) @@ -139,7 +138,7 @@ func (h *socks5Handler) serveBind(ctx context.Context, conn net.Conn, ln net.Lis start := time.Now() log.Debugf("%s <-> %s", rc.LocalAddr(), rc.RemoteAddr()) - netpkg.Transport(pc2, rc) + xnet.Transport(pc2, rc) log.WithFields(map[string]any{"duration": time.Since(start)}). Debugf("%s >-< %s", rc.LocalAddr(), rc.RemoteAddr()) diff --git a/handler/socks/v5/connect.go b/handler/socks/v5/connect.go index 5aa48a06..a1fd4f31 100644 --- a/handler/socks/v5/connect.go +++ b/handler/socks/v5/connect.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "crypto/tls" "fmt" "net" "time" @@ -13,8 +14,7 @@ import ( "github.com/go-gost/core/observer/stats" "github.com/go-gost/gosocks5" ctxvalue "github.com/go-gost/x/ctx" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/util/sniffing" traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper" stats_wrapper "github.com/go-gost/x/observer/stats/wrapper" @@ -83,7 +83,7 @@ func (h *socks5Handler) handleConnect(ctx context.Context, conn net.Conn, networ conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) } - br := bufio.NewReader(conn) + br := bufio.NewReader(rw) proto, _ := sniffing.Sniff(ctx, br) ro.Proto = proto @@ -91,21 +91,37 @@ func (h *socks5Handler) handleConnect(ctx context.Context, conn net.Conn, networ conn.SetReadDeadline(time.Time{}) } - rw = xio.NewReadWriter(br, conn) + sniffer := &sniffing.Sniffer{ + Recorder: h.recorder.Recorder, + RecorderOptions: h.recorder.Options, + RecorderObject: ro, + Certificate: h.md.certificate, + PrivateKey: h.md.privateKey, + NegotiatedProtocol: h.md.alpn, + CertPool: h.certPool, + MitmBypass: h.md.mitmBypass, + ReadTimeout: h.md.readTimeout, + Log: log, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + return cc, nil + }, + DialTLS: func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) { + return cc, nil + }, + } + + conn = xnet.NewReadWriteConn(br, rw, conn) switch proto { case sniffing.ProtoHTTP: - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - return h.handleHTTP(ctx, rw, cc, ro2, log) + return sniffer.HandleHTTP(ctx, conn) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, cc, ro, log) + return sniffer.HandleTLS(ctx, conn) } } t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), address) - netpkg.Transport(rw, cc) + xnet.Transport(rw, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", conn.RemoteAddr(), address) diff --git a/handler/socks/v5/handler.go b/handler/socks/v5/handler.go index a7c730f4..20613d1f 100644 --- a/handler/socks/v5/handler.go +++ b/handler/socks/v5/handler.go @@ -15,6 +15,7 @@ import ( limiter_util "github.com/go-gost/x/internal/util/limiter" "github.com/go-gost/x/internal/util/socks" stats_util "github.com/go-gost/x/internal/util/stats" + tls_util "github.com/go-gost/x/internal/util/tls" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" @@ -37,6 +38,7 @@ type socks5Handler struct { limiter traffic.TrafficLimiter cancel context.CancelFunc recorder recorder.RecorderObject + certPool tls_util.CertPool } func NewHandler(opts ...handler.Option) handler.Handler { @@ -81,6 +83,10 @@ func (h *socks5Handler) Init(md md.Metadata) (err error) { } } + if h.md.certificate != nil && h.md.privateKey != nil { + h.certPool = tls_util.NewMemoryCertPool() + } + return } diff --git a/handler/socks/v5/metadata.go b/handler/socks/v5/metadata.go index fe016ee3..f0b127a1 100644 --- a/handler/socks/v5/metadata.go +++ b/handler/socks/v5/metadata.go @@ -1,12 +1,17 @@ package v5 import ( + "crypto" + "crypto/tls" + "crypto/x509" "math" "time" + "github.com/go-gost/core/bypass" mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/core/metadata/util" "github.com/go-gost/x/internal/util/mux" + "github.com/go-gost/x/registry" ) type metadata struct { @@ -19,8 +24,14 @@ type metadata struct { hash string muxCfg *mux.Config observePeriod time.Duration - sniffing bool - sniffingTimeout time.Duration + + sniffing bool + sniffingTimeout time.Duration + + certificate *x509.Certificate + privateKey crypto.PrivateKey + alpn string + mitmBypass bypass.Bypass } func (h *socks5Handler) parseMetadata(md mdata.Metadata) (err error) { @@ -57,5 +68,21 @@ func (h *socks5Handler) parseMetadata(md mdata.Metadata) (err error) { h.md.sniffing = mdutil.GetBool(md, "sniffing") h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile") + keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile") + if certFile != "" && keyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + h.md.privateKey = tlsCert.PrivateKey + } + h.md.alpn = mdutil.GetString(md, "mitm.alpn") + h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass")) + return nil } diff --git a/handler/socks/v5/sniffing.go b/handler/socks/v5/sniffing.go deleted file mode 100644 index e6d37644..00000000 --- a/handler/socks/v5/sniffing.go +++ /dev/null @@ -1,259 +0,0 @@ -package v5 - -import ( - "bufio" - "bytes" - "context" - "encoding/hex" - "errors" - "io" - "net" - "net/http" - "net/http/httputil" - "strings" - "time" - - "github.com/go-gost/core/bypass" - "github.com/go-gost/core/logger" - dissector "github.com/go-gost/tls-dissector" - xbypass "github.com/go-gost/x/bypass" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" - xhttp "github.com/go-gost/x/internal/net/http" - tls_util "github.com/go-gost/x/internal/util/tls" - xrecorder "github.com/go-gost/x/recorder" -) - -const ( - defaultBodySize = 1024 * 1024 // 1MB -) - -func (h *socks5Handler) handleHTTP(ctx context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - br := bufio.NewReader(rw) - req, err := http.ReadRequest(br) - if err != nil { - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - ro.Host = host - - log = log.WithFields(map[string]any{ - "host": host, - }) - - if h.options.Bypass != nil && - h.options.Bypass.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { - log.Debugf("bypass: %s %s", host, req.RequestURI) - return xbypass.ErrBypass - } - - shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log) - if err != nil || shouldClose { - return err - } - - for { - req, err := http.ReadRequest(br) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { - return nil - } - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - if shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { - return err - } - } -} - -func (h *socks5Handler) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { - close = true - - if req == nil { - return - } - - start := time.Now() - ro.Time = start - - log.Infof("%s <-> %s", ro.RemoteAddr, req.Host) - defer func() { - ro.Duration = time.Since(start) - if err != nil { - ro.Err = err.Error() - } - if err := ro.Record(ctx, h.recorder.Recorder); err != nil { - log.Errorf("record: %v", err) - } - - log.WithFields(map[string]any{ - "duration": time.Since(start), - }).Infof("%s >-< %s", ro.RemoteAddr, req.Host) - }() - - if clientIP := xhttp.GetClientIP(req); clientIP != nil { - ro.ClientIP = clientIP.String() - } - ro.HTTP = &xrecorder.HTTPRecorderObject{ - Host: req.Host, - Proto: req.Proto, - Scheme: req.URL.Scheme, - Method: req.Method, - URI: req.RequestURI, - Request: xrecorder.HTTPRequestRecorderObject{ - ContentLength: req.ContentLength, - Header: req.Header.Clone(), - }, - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if strings.ToLower(req.Header.Get("Connection")) == "keep-alive" { - req.Header.Del("Connection") - } else { - req.Header.Set("Connection", "close") - } - } - - var reqBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - if req.Body != nil { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - reqBody = xhttp.NewBody(req.Body, maxSize) - req.Body = reqBody - } - } - - if err = req.Write(cc); err != nil { - return - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - resp, err := http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - log.Errorf("read response: %v", err) - return - } - defer resp.Body.Close() - xio.SetReadDeadline(cc, time.Time{}) - - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - ro.HTTP.Response.ContentLength = resp.ContentLength - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(resp, false) - log.Trace(string(dump)) - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if !resp.Close { - resp.Header.Set("Connection", "keep-alive") - } - resp.ProtoMajor = req.ProtoMajor - resp.ProtoMinor = req.ProtoMinor - } - - var respBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - respBody = xhttp.NewBody(resp.Body, maxSize) - resp.Body = respBody - } - - if err = resp.Write(rw); err != nil { - log.Errorf("write response: %v", err) - return - } - - if respBody != nil { - ro.HTTP.Response.Body = respBody.Content() - ro.HTTP.Response.ContentLength = respBody.Length() - } - - if req.Header.Get("Upgrade") == "websocket" { - netpkg.Transport(rw, cc) - } - - return resp.Close, nil -} - -func (h *socks5Handler) handleTLS(_ context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - buf := new(bytes.Buffer) - - clientHello, err := dissector.ParseClientHello(io.TeeReader(rw, buf)) - if err != nil { - return err - } - - ro.TLS = &xrecorder.TLSRecorderObject{ - ServerName: clientHello.ServerName, - ClientHello: hex.EncodeToString(buf.Bytes()), - } - if len(clientHello.SupportedProtos) > 0 { - ro.TLS.Proto = clientHello.SupportedProtos[0] - } - - if _, err := buf.WriteTo(cc); err != nil { - return err - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) - xio.SetReadDeadline(cc, time.Time{}) - - if serverHello != nil { - ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String() - ro.TLS.CompressionMethod = serverHello.CompressionMethod - if serverHello.Proto != "" { - ro.TLS.Proto = serverHello.Proto - } - if serverHello.Version > 0 { - ro.TLS.Version = tls_util.Version(serverHello.Version).String() - } - } - - if buf.Len() > 0 { - ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) - } - - if _, err := buf.WriteTo(rw); err != nil { - return err - } - - t := time.Now() - log.Infof("%s <-> %s", ro.RemoteAddr, ro.Host) - netpkg.Transport(rw, cc) - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", ro.RemoteAddr, ro.Host) - - return err -} diff --git a/handler/ss/handler.go b/handler/ss/handler.go index b077e006..54d892ab 100644 --- a/handler/ss/handler.go +++ b/handler/ss/handler.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "crypto/tls" "io" "net" "time" @@ -13,10 +14,10 @@ import ( "github.com/go-gost/core/recorder" "github.com/go-gost/gosocks5" ctxvalue "github.com/go-gost/x/ctx" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/util/sniffing" "github.com/go-gost/x/internal/util/ss" + tls_util "github.com/go-gost/x/internal/util/tls" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" @@ -32,6 +33,7 @@ type ssHandler struct { md metadata options handler.Options recorder recorder.RecorderObject + certPool tls_util.CertPool } func NewHandler(opts ...handler.Option) handler.Handler { @@ -65,6 +67,10 @@ func (h *ssHandler) Init(md md.Metadata) (err error) { } } + if h.md.certificate != nil && h.md.privateKey != nil { + h.certPool = tls_util.NewMemoryCertPool() + } + return } @@ -91,13 +97,11 @@ func (h *ssHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.H log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) defer func() { - if !ro.Time.IsZero() { - if err != nil { - ro.Err = err.Error() - } - ro.Duration = time.Since(start) - ro.Record(ctx, h.recorder.Recorder) + if err != nil { + ro.Err = err.Error() } + ro.Duration = time.Since(start) + ro.Record(ctx, h.recorder.Recorder) log.WithFields(map[string]any{ "duration": time.Since(start), @@ -148,7 +152,6 @@ func (h *ssHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.H } defer cc.Close() - var rw io.ReadWriter = conn if h.md.sniffing { if h.md.sniffingTimeout > 0 { conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) @@ -162,24 +165,40 @@ func (h *ssHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.H conn.SetReadDeadline(time.Time{}) } - rw = xio.NewReadWriter(br, conn) + sniffer := &sniffing.Sniffer{ + Recorder: h.recorder.Recorder, + RecorderOptions: h.recorder.Options, + RecorderObject: ro, + Certificate: h.md.certificate, + PrivateKey: h.md.privateKey, + NegotiatedProtocol: h.md.alpn, + CertPool: h.certPool, + MitmBypass: h.md.mitmBypass, + ReadTimeout: h.md.readTimeout, + Log: log, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + return cc, nil + }, + DialTLS: func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) { + return cc, nil + }, + } + + conn = xnet.NewReadWriteConn(br, conn, conn) switch proto { case sniffing.ProtoHTTP: - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - return h.handleHTTP(ctx, rw, cc, ro2, log) + return sniffer.HandleHTTP(ctx, conn) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, cc, ro, log) + return sniffer.HandleTLS(ctx, conn) } } t := time.Now() - log.Infof("%s <-> %s", conn.RemoteAddr(), addr) - netpkg.Transport(rw, cc) + log.Infof("%s <-> %s", conn.RemoteAddr(), ro.Host) + xnet.Transport(conn, cc) log.WithFields(map[string]any{ "duration": time.Since(t), - }).Infof("%s >-< %s", conn.RemoteAddr(), addr) + }).Infof("%s >-< %s", conn.RemoteAddr(), ro.Host) return nil } diff --git a/handler/ss/metadata.go b/handler/ss/metadata.go index f239345e..f8f74ebe 100644 --- a/handler/ss/metadata.go +++ b/handler/ss/metadata.go @@ -1,18 +1,29 @@ package ss import ( + "crypto" + "crypto/tls" + "crypto/x509" "time" + "github.com/go-gost/core/bypass" mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/core/metadata/util" + "github.com/go-gost/x/registry" ) type metadata struct { - key string - hash string - readTimeout time.Duration + key string + hash string + readTimeout time.Duration + sniffing bool sniffingTimeout time.Duration + + certificate *x509.Certificate + privateKey crypto.PrivateKey + alpn string + mitmBypass bypass.Bypass } func (h *ssHandler) parseMetadata(md mdata.Metadata) (err error) { @@ -27,5 +38,21 @@ func (h *ssHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.sniffing = mdutil.GetBool(md, "sniffing") h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile") + keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile") + if certFile != "" && keyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + h.md.privateKey = tlsCert.PrivateKey + } + h.md.alpn = mdutil.GetString(md, "mitm.alpn") + h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass")) + return } diff --git a/handler/ss/sniffing.go b/handler/ss/sniffing.go deleted file mode 100644 index 4174e675..00000000 --- a/handler/ss/sniffing.go +++ /dev/null @@ -1,259 +0,0 @@ -package ss - -import ( - "bufio" - "bytes" - "context" - "encoding/hex" - "errors" - "io" - "net" - "net/http" - "net/http/httputil" - "strings" - "time" - - "github.com/go-gost/core/bypass" - "github.com/go-gost/core/logger" - dissector "github.com/go-gost/tls-dissector" - xbypass "github.com/go-gost/x/bypass" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" - xhttp "github.com/go-gost/x/internal/net/http" - tls_util "github.com/go-gost/x/internal/util/tls" - xrecorder "github.com/go-gost/x/recorder" -) - -const ( - defaultBodySize = 1024 * 1024 // 1MB -) - -func (h *ssHandler) handleHTTP(ctx context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - br := bufio.NewReader(rw) - req, err := http.ReadRequest(br) - if err != nil { - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - ro.Host = host - - log = log.WithFields(map[string]any{ - "host": host, - }) - - if h.options.Bypass != nil && - h.options.Bypass.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { - log.Debugf("bypass: %s %s", host, req.RequestURI) - return xbypass.ErrBypass - } - - shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log) - if err != nil || shouldClose { - return err - } - - for { - req, err := http.ReadRequest(br) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { - return nil - } - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - if shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { - return err - } - } -} - -func (h *ssHandler) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { - close = true - - if req == nil { - return - } - - start := time.Now() - ro.Time = start - - log.Infof("%s <-> %s", ro.RemoteAddr, req.Host) - defer func() { - ro.Duration = time.Since(start) - if err != nil { - ro.Err = err.Error() - } - if err := ro.Record(ctx, h.recorder.Recorder); err != nil { - log.Errorf("record: %v", err) - } - - log.WithFields(map[string]any{ - "duration": time.Since(start), - }).Infof("%s >-< %s", ro.RemoteAddr, req.Host) - }() - - if clientIP := xhttp.GetClientIP(req); clientIP != nil { - ro.ClientIP = clientIP.String() - } - ro.HTTP = &xrecorder.HTTPRecorderObject{ - Host: req.Host, - Proto: req.Proto, - Scheme: req.URL.Scheme, - Method: req.Method, - URI: req.RequestURI, - Request: xrecorder.HTTPRequestRecorderObject{ - ContentLength: req.ContentLength, - Header: req.Header.Clone(), - }, - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if strings.ToLower(req.Header.Get("Connection")) == "keep-alive" { - req.Header.Del("Connection") - } else { - req.Header.Set("Connection", "close") - } - } - - var reqBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - if req.Body != nil { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - reqBody = xhttp.NewBody(req.Body, maxSize) - req.Body = reqBody - } - } - - if err = req.Write(cc); err != nil { - return - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - resp, err := http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - log.Errorf("read response: %v", err) - return - } - defer resp.Body.Close() - xio.SetReadDeadline(cc, time.Time{}) - - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - ro.HTTP.Response.ContentLength = resp.ContentLength - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(resp, false) - log.Trace(string(dump)) - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if !resp.Close { - resp.Header.Set("Connection", "keep-alive") - } - resp.ProtoMajor = req.ProtoMajor - resp.ProtoMinor = req.ProtoMinor - } - - var respBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - respBody = xhttp.NewBody(resp.Body, maxSize) - resp.Body = respBody - } - - if err = resp.Write(rw); err != nil { - log.Errorf("write response: %v", err) - return - } - - if respBody != nil { - ro.HTTP.Response.Body = respBody.Content() - ro.HTTP.Response.ContentLength = respBody.Length() - } - - if req.Header.Get("Upgrade") == "websocket" { - netpkg.Transport(rw, cc) - } - - return resp.Close, nil -} - -func (h *ssHandler) handleTLS(_ context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - buf := new(bytes.Buffer) - - clientHello, err := dissector.ParseClientHello(io.TeeReader(rw, buf)) - if err != nil { - return err - } - - ro.TLS = &xrecorder.TLSRecorderObject{ - ServerName: clientHello.ServerName, - ClientHello: hex.EncodeToString(buf.Bytes()), - } - if len(clientHello.SupportedProtos) > 0 { - ro.TLS.Proto = clientHello.SupportedProtos[0] - } - - if _, err := buf.WriteTo(cc); err != nil { - return err - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) - xio.SetReadDeadline(cc, time.Time{}) - - if serverHello != nil { - ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String() - ro.TLS.CompressionMethod = serverHello.CompressionMethod - if serverHello.Proto != "" { - ro.TLS.Proto = serverHello.Proto - } - if serverHello.Version > 0 { - ro.TLS.Version = tls_util.Version(serverHello.Version).String() - } - } - - if buf.Len() > 0 { - ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) - } - - if _, err := buf.WriteTo(rw); err != nil { - return err - } - - t := time.Now() - log.Infof("%s <-> %s", ro.RemoteAddr, ro.Host) - netpkg.Transport(rw, cc) - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", ro.RemoteAddr, ro.Host) - - return err -} diff --git a/handler/sshd/handler.go b/handler/sshd/handler.go index 10d5027a..d80b05f1 100644 --- a/handler/sshd/handler.go +++ b/handler/sshd/handler.go @@ -4,10 +4,10 @@ import ( "bufio" "bytes" "context" + "crypto/tls" "encoding/binary" "errors" "fmt" - "io" "net" "strconv" "time" @@ -18,10 +18,10 @@ import ( "github.com/go-gost/core/recorder" xbypass "github.com/go-gost/x/bypass" ctxvalue "github.com/go-gost/x/ctx" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" + xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/util/sniffing" sshd_util "github.com/go-gost/x/internal/util/sshd" + tls_util "github.com/go-gost/x/internal/util/tls" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" @@ -41,6 +41,7 @@ type forwardHandler struct { md metadata options handler.Options recorder recorder.RecorderObject + certPool tls_util.CertPool } func NewHandler(opts ...handler.Option) handler.Handler { @@ -66,6 +67,10 @@ func (h *forwardHandler) Init(md md.Metadata) (err error) { } } + if h.md.certificate != nil && h.md.privateKey != nil { + h.certPool = tls_util.NewMemoryCertPool() + } + return nil } @@ -143,7 +148,6 @@ func (h *forwardHandler) handleDirectForward(ctx context.Context, conn *sshd_uti } defer cc.Close() - var rw io.ReadWriter = conn if h.md.sniffing { if h.md.sniffingTimeout > 0 { conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) @@ -157,21 +161,36 @@ func (h *forwardHandler) handleDirectForward(ctx context.Context, conn *sshd_uti conn.SetReadDeadline(time.Time{}) } - rw = xio.NewReadWriter(br, conn) + sniffer := &sniffing.Sniffer{ + Recorder: h.recorder.Recorder, + RecorderOptions: h.recorder.Options, + RecorderObject: ro, + Certificate: h.md.certificate, + PrivateKey: h.md.privateKey, + NegotiatedProtocol: h.md.alpn, + CertPool: h.certPool, + MitmBypass: h.md.mitmBypass, + ReadTimeout: h.md.readTimeout, + Log: log, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + return cc, nil + }, + DialTLS: func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) { + return cc, nil + }, + } + switch proto { case sniffing.ProtoHTTP: - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - return h.handleHTTP(ctx, rw, cc, ro2, log) + return sniffer.HandleHTTP(ctx, xnet.NewReadWriteConn(br, conn, conn)) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, cc, ro, log) + return sniffer.HandleTLS(ctx, xnet.NewReadWriteConn(br, conn, conn)) } } t := time.Now() log.Infof("%s <-> %s", cc.LocalAddr(), targetAddr) - netpkg.Transport(rw, cc) + xnet.Transport(conn, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", cc.LocalAddr(), targetAddr) @@ -270,7 +289,7 @@ func (h *forwardHandler) handleRemoteForward(ctx context.Context, conn *sshd_uti t := time.Now() log.Debugf("%s <-> %s", conn.LocalAddr(), conn.RemoteAddr()) - netpkg.Transport(ch, conn) + xnet.Transport(ch, conn) log.WithFields(map[string]any{ "duration": time.Since(t), }).Debugf("%s >-< %s", conn.LocalAddr(), conn.RemoteAddr()) diff --git a/handler/sshd/metadata.go b/handler/sshd/metadata.go index 3cc567dc..5b24ff90 100644 --- a/handler/sshd/metadata.go +++ b/handler/sshd/metadata.go @@ -1,16 +1,27 @@ package ssh import ( + "crypto" + "crypto/tls" + "crypto/x509" "time" + "github.com/go-gost/core/bypass" mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/core/metadata/util" + "github.com/go-gost/x/registry" ) type metadata struct { - readTimeout time.Duration + readTimeout time.Duration + sniffing bool sniffingTimeout time.Duration + + certificate *x509.Certificate + privateKey crypto.PrivateKey + alpn string + mitmBypass bypass.Bypass } func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) { @@ -22,5 +33,21 @@ func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.sniffing = mdutil.GetBool(md, "sniffing") h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile") + keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile") + if certFile != "" && keyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + h.md.privateKey = tlsCert.PrivateKey + } + h.md.alpn = mdutil.GetString(md, "mitm.alpn") + h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass")) + return } diff --git a/handler/sshd/sniffing.go b/handler/sshd/sniffing.go deleted file mode 100644 index 3736c984..00000000 --- a/handler/sshd/sniffing.go +++ /dev/null @@ -1,259 +0,0 @@ -package ssh - -import ( - "bufio" - "bytes" - "context" - "encoding/hex" - "errors" - "io" - "net" - "net/http" - "net/http/httputil" - "strings" - "time" - - "github.com/go-gost/core/bypass" - "github.com/go-gost/core/logger" - dissector "github.com/go-gost/tls-dissector" - xbypass "github.com/go-gost/x/bypass" - xio "github.com/go-gost/x/internal/io" - netpkg "github.com/go-gost/x/internal/net" - xhttp "github.com/go-gost/x/internal/net/http" - tls_util "github.com/go-gost/x/internal/util/tls" - xrecorder "github.com/go-gost/x/recorder" -) - -const ( - defaultBodySize = 1024 * 1024 // 1MB -) - -func (h *forwardHandler) handleHTTP(ctx context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - br := bufio.NewReader(rw) - req, err := http.ReadRequest(br) - if err != nil { - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - ro.Host = host - - log = log.WithFields(map[string]any{ - "host": host, - }) - - if h.options.Bypass != nil && - h.options.Bypass.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { - log.Debugf("bypass: %s %s", host, req.RequestURI) - return xbypass.ErrBypass - } - - shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log) - if err != nil || shouldClose { - return err - } - - for { - req, err := http.ReadRequest(br) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { - return nil - } - return err - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - if shouldClose, err := h.httpRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { - return err - } - } -} - -func (h *forwardHandler) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { - close = true - - if req == nil { - return - } - - start := time.Now() - ro.Time = start - - log.Infof("%s <-> %s", ro.RemoteAddr, req.Host) - defer func() { - ro.Duration = time.Since(start) - if err != nil { - ro.Err = err.Error() - } - if err := ro.Record(ctx, h.recorder.Recorder); err != nil { - log.Errorf("record: %v", err) - } - - log.WithFields(map[string]any{ - "duration": time.Since(start), - }).Infof("%s >-< %s", ro.RemoteAddr, req.Host) - }() - - if clientIP := xhttp.GetClientIP(req); clientIP != nil { - ro.ClientIP = clientIP.String() - } - ro.HTTP = &xrecorder.HTTPRecorderObject{ - Host: req.Host, - Proto: req.Proto, - Scheme: req.URL.Scheme, - Method: req.Method, - URI: req.RequestURI, - Request: xrecorder.HTTPRequestRecorderObject{ - ContentLength: req.ContentLength, - Header: req.Header.Clone(), - }, - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if strings.ToLower(req.Header.Get("Connection")) == "keep-alive" { - req.Header.Del("Connection") - } else { - req.Header.Set("Connection", "close") - } - } - - var reqBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - if req.Body != nil { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - reqBody = xhttp.NewBody(req.Body, maxSize) - req.Body = reqBody - } - } - - if err = req.Write(cc); err != nil { - return - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - resp, err := http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - log.Errorf("read response: %v", err) - return - } - defer resp.Body.Close() - xio.SetReadDeadline(cc, time.Time{}) - - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - ro.HTTP.Response.ContentLength = resp.ContentLength - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(resp, false) - log.Trace(string(dump)) - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if !resp.Close { - resp.Header.Set("Connection", "keep-alive") - } - resp.ProtoMajor = req.ProtoMajor - resp.ProtoMinor = req.ProtoMinor - } - - var respBody *xhttp.Body - if opts := h.recorder.Options; opts != nil && opts.HTTPBody { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - respBody = xhttp.NewBody(resp.Body, maxSize) - resp.Body = respBody - } - - if err = resp.Write(rw); err != nil { - log.Errorf("write response: %v", err) - return - } - - if respBody != nil { - ro.HTTP.Response.Body = respBody.Content() - ro.HTTP.Response.ContentLength = respBody.Length() - } - - if req.Header.Get("Upgrade") == "websocket" { - netpkg.Transport(rw, cc) - } - - return resp.Close, nil -} - -func (h *forwardHandler) handleTLS(_ context.Context, rw, cc io.ReadWriter, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - buf := new(bytes.Buffer) - - clientHello, err := dissector.ParseClientHello(io.TeeReader(rw, buf)) - if err != nil { - return err - } - - ro.TLS = &xrecorder.TLSRecorderObject{ - ServerName: clientHello.ServerName, - ClientHello: hex.EncodeToString(buf.Bytes()), - } - if len(clientHello.SupportedProtos) > 0 { - ro.TLS.Proto = clientHello.SupportedProtos[0] - } - - if _, err := buf.WriteTo(cc); err != nil { - return err - } - - xio.SetReadDeadline(cc, time.Now().Add(h.md.readTimeout)) - serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) - xio.SetReadDeadline(cc, time.Time{}) - - if serverHello != nil { - ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String() - ro.TLS.CompressionMethod = serverHello.CompressionMethod - if serverHello.Proto != "" { - ro.TLS.Proto = serverHello.Proto - } - if serverHello.Version > 0 { - ro.TLS.Version = tls_util.Version(serverHello.Version).String() - } - } - - if buf.Len() > 0 { - ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) - } - - if _, err := buf.WriteTo(rw); err != nil { - return err - } - - t := time.Now() - log.Infof("%s <-> %s", ro.RemoteAddr, ro.Host) - netpkg.Transport(rw, cc) - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", ro.RemoteAddr, ro.Host) - - return err -} diff --git a/handler/tunnel/entrypoint.go b/handler/tunnel/entrypoint.go index dbd8d4de..d7ad1ed9 100644 --- a/handler/tunnel/entrypoint.go +++ b/handler/tunnel/entrypoint.go @@ -3,13 +3,8 @@ package tunnel import ( "bufio" "context" - "fmt" - "io" "net" - "net/http" - "net/http/httputil" "strconv" - "strings" "time" "github.com/go-gost/core/handler" @@ -23,7 +18,6 @@ import ( admission "github.com/go-gost/x/admission/wrapper" xio "github.com/go-gost/x/internal/io" xnet "github.com/go-gost/x/internal/net" - xhttp "github.com/go-gost/x/internal/net/http" "github.com/go-gost/x/internal/net/proxyproto" climiter "github.com/go-gost/x/limiter/conn/wrapper" metrics "github.com/go-gost/x/metrics/wrapper" @@ -63,7 +57,7 @@ func (ep *entrypoint) handle(ctx context.Context, conn net.Conn) error { return err } if v[0] == relay.Version1 { - return ep.handleConnect(ctx, xnet.NewBufferReaderConn(conn, br), log) + return ep.handleConnect(ctx, xnet.NewReadWriteConn(br, conn, conn), log) } ro := &xrecorder.HandlerRecorderObject{ @@ -77,276 +71,6 @@ func (ep *entrypoint) handle(ctx context.Context, conn net.Conn) error { ro.ClientIP, _, _ = net.SplitHostPort(conn.RemoteAddr().String()) return ep.handleHTTP(ctx, xio.NewReadWriter(br, conn), ro, log) - - var cc net.Conn - for { - resp := &http.Response{ - ProtoMajor: 1, - ProtoMinor: 1, - Header: http.Header{}, - StatusCode: http.StatusServiceUnavailable, - } - - err := func() error { - req, err := http.ReadRequest(br) - if err != nil { - // log.Errorf("read http request: %v", err) - return err - } - - start := time.Now() - ro := &xrecorder.HandlerRecorderObject{ - Node: ep.node, - Service: ep.service, - RemoteAddr: conn.RemoteAddr().String(), - LocalAddr: conn.LocalAddr().String(), - Network: "tcp", - Host: req.Host, - Time: start, - HTTP: &xrecorder.HTTPRecorderObject{ - Host: req.Host, - Method: req.Method, - Proto: req.Proto, - Scheme: req.URL.Scheme, - URI: req.RequestURI, - Request: xrecorder.HTTPRequestRecorderObject{ - ContentLength: req.ContentLength, - Header: req.Header, - }, - }, - } - if clientIP := xhttp.GetClientIP(req); clientIP != nil { - ro.ClientIP = clientIP.String() - } else { - ro.ClientIP, _, _ = net.SplitHostPort(conn.RemoteAddr().String()) - } - - defer func() { - if err != nil { - d := time.Since(start) - log.WithFields(map[string]any{ - "duration": d, - }).Debugf("%s >-< %s", conn.RemoteAddr(), req.Host) - - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - - ro.Duration = d - ro.Err = err.Error() - ro.Record(ctx, ep.recorder.Recorder) - } - }() - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - resp.ProtoMajor = req.ProtoMajor - resp.ProtoMinor = req.ProtoMinor - - var tunnelID relay.TunnelID - if ep.ingress != nil { - if rule := ep.ingress.GetRule(ctx, req.Host); rule != nil { - tunnelID = parseTunnelID(rule.Endpoint) - } - } - if tunnelID.IsZero() { - err = fmt.Errorf("no route to host %s", req.Host) - log.Error(err) - resp.StatusCode = http.StatusBadGateway - resp.Write(conn) - return err - } - - ro.ClientID = tunnelID.String() - - if tunnelID.IsPrivate() { - err = fmt.Errorf("access denied: tunnel %s is private for host %s", tunnelID, req.Host) - log.Error(err) - resp.StatusCode = http.StatusBadGateway - resp.Write(conn) - return err - } - - log = log.WithFields(map[string]any{ - "host": req.Host, - "tunnel": tunnelID.String(), - }) - - remoteAddr := conn.RemoteAddr() - if addr := ep.getRealClientAddr(req, remoteAddr); addr != remoteAddr { - log = log.WithFields(map[string]any{ - "src": addr.String(), - }) - remoteAddr = addr - } - ro.RemoteAddr = remoteAddr.String() - - d := &Dialer{ - node: ep.node, - pool: ep.pool, - sd: ep.sd, - retry: 3, - timeout: 15 * time.Second, - log: log, - } - c, node, cid, err := d.Dial(ctx, "tcp", tunnelID.String()) - if err != nil { - log.Error(err) - return resp.Write(conn) - } - log.Debugf("new connection to tunnel: %s, connector: %s", tunnelID, cid) - - cc = c - - host := req.Host - if h, _, _ := net.SplitHostPort(host); h == "" { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - - if node == ep.node { - var features []relay.Feature - af := &relay.AddrFeature{} - af.ParseFrom(remoteAddr.String()) - features = append(features, af) // src address - - af = &relay.AddrFeature{} - af.ParseFrom(host) - features = append(features, af) // dst address - - (&relay.Response{ - Version: relay.Version1, - Status: relay.StatusOK, - Features: features, - }).WriteTo(c) - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if strings.ToLower(req.Header.Get("Connection")) == "keep-alive" { - req.Header.Del("Connection") - } else { - req.Header.Set("Connection", "close") - } - } - - var reqBody *xhttp.Body - if opts := ep.recorder.Options; opts != nil && opts.HTTPBody { - if req.Body != nil { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - reqBody = xhttp.NewBody(req.Body, maxSize) - req.Body = reqBody - } - } - - if err = req.Write(c); err != nil { - c.Close() - log.Errorf("send request: %v", err) - resp.Write(conn) - return err - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - if req.Header.Get("Upgrade") == "websocket" { - err = xnet.Transport(c, xio.NewReadWriter(br, conn)) - if err == nil { - err = io.EOF - } - return err - } - - go func() { - defer c.Close() - - log.Debugf("%s <-> %s", remoteAddr, host) - - var err error - var res *http.Response - var respBody *xhttp.Body - - defer func() { - d := time.Since(start) - log.WithFields(map[string]any{ - "duration": d, - }).Debugf("%s >-< %s", remoteAddr, host) - - ro.Duration = d - if err != nil { - ro.Err = err.Error() - } - if res != nil { - ro.HTTP.StatusCode = res.StatusCode - ro.HTTP.Response.Header = res.Header - ro.HTTP.Response.ContentLength = res.ContentLength - if respBody != nil { - ro.HTTP.Response.Body = respBody.Content() - ro.HTTP.Response.ContentLength = respBody.Length() - } - } - ro.Record(ctx, ep.recorder.Recorder) - }() - - res, err = http.ReadResponse(bufio.NewReader(c), req) - if err != nil { - log.Errorf("read response: %v", err) - resp.Write(conn) - return - } - defer res.Body.Close() - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(res, false) - log.Trace(string(dump)) - } - - if res.Close { - defer conn.Close() - } - - // HTTP/1.0 - if req.ProtoMajor == 1 && req.ProtoMinor == 0 { - if !res.Close { - res.Header.Set("Connection", "keep-alive") - } - res.ProtoMajor = req.ProtoMajor - res.ProtoMinor = req.ProtoMinor - } - - if opts := ep.recorder.Options; opts != nil && opts.HTTPBody { - maxSize := opts.MaxBodySize - if maxSize <= 0 { - maxSize = defaultBodySize - } - respBody = xhttp.NewBody(res.Body, maxSize) - res.Body = respBody - } - - if err = res.Write(conn); err != nil { - conn.Close() - log.Errorf("write response: %v", err) - } - }() - - return nil - }() - - if err != nil { - if cc != nil { - cc.Close() - } - break - } - } - - return nil } func (ep *entrypoint) handleConnect(ctx context.Context, conn net.Conn, log logger.Logger) error { @@ -433,37 +157,6 @@ func (ep *entrypoint) handleConnect(ctx context.Context, conn net.Conn, log logg return nil } -func (ep *entrypoint) getRealClientAddr(req *http.Request, raddr net.Addr) net.Addr { - if req == nil { - return nil - } - // cloudflare CDN - sip := req.Header.Get("CF-Connecting-IP") - if sip == "" { - ss := strings.Split(req.Header.Get("X-Forwarded-For"), ",") - if len(ss) > 0 && ss[0] != "" { - sip = ss[0] - } - } - if sip == "" { - sip = req.Header.Get("X-Real-Ip") - } - - ip := net.ParseIP(sip) - if ip == nil { - return raddr - } - - _, sp, _ := net.SplitHostPort(raddr.String()) - - port, _ := strconv.Atoi(sp) - - return &net.TCPAddr{ - IP: ip, - Port: port, - } -} - type tcpListener struct { ln net.Listener options listener.Options diff --git a/handler/unix/handler.go b/handler/unix/handler.go index 0dc9fa35..ce3c9c2e 100644 --- a/handler/unix/handler.go +++ b/handler/unix/handler.go @@ -3,6 +3,7 @@ package unix import ( "bufio" "context" + "crypto/tls" "errors" "io" "net" @@ -18,6 +19,7 @@ import ( xio "github.com/go-gost/x/internal/io" xnet "github.com/go-gost/x/internal/net" "github.com/go-gost/x/internal/util/sniffing" + tls_util "github.com/go-gost/x/internal/util/tls" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" ) @@ -31,6 +33,7 @@ type unixHandler struct { md metadata options handler.Options recorder recorder.RecorderObject + certPool tls_util.CertPool } func NewHandler(opts ...handler.Option) handler.Handler { @@ -56,6 +59,10 @@ func (h *unixHandler) Init(md md.Metadata) (err error) { } } + if h.md.certificate != nil && h.md.privateKey != nil { + h.certPool = tls_util.NewMemoryCertPool() + } + return } @@ -121,7 +128,6 @@ func (h *unixHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler } defer cc.Close() - var rw io.ReadWriter = conn if h.md.sniffing { if h.md.sniffingTimeout > 0 { conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) @@ -135,21 +141,37 @@ func (h *unixHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler conn.SetReadDeadline(time.Time{}) } - rw = xio.NewReadWriter(br, conn) + sniffer := &sniffing.Sniffer{ + Recorder: h.recorder.Recorder, + RecorderOptions: h.recorder.Options, + RecorderObject: ro, + Certificate: h.md.certificate, + PrivateKey: h.md.privateKey, + NegotiatedProtocol: h.md.alpn, + CertPool: h.certPool, + MitmBypass: h.md.mitmBypass, + ReadTimeout: h.md.readTimeout, + Log: log, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + return cc, nil + }, + DialTLS: func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) { + return cc, nil + }, + } + + conn = xnet.NewReadWriteConn(br, conn, conn) switch proto { case sniffing.ProtoHTTP: - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - return h.handleHTTP(ctx, rw, cc, ro2, log) + return sniffer.HandleHTTP(ctx, conn) case sniffing.ProtoTLS: - return h.handleTLS(ctx, rw, cc, ro, log) + return sniffer.HandleTLS(ctx, conn) } } t := time.Now() log.Infof("%s <-> %s", conn.LocalAddr(), "@") - xnet.Transport(rw, cc) + xnet.Transport(conn, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", conn.LocalAddr(), "@") diff --git a/handler/unix/metadata.go b/handler/unix/metadata.go index a098ace7..dabd6372 100644 --- a/handler/unix/metadata.go +++ b/handler/unix/metadata.go @@ -1,16 +1,27 @@ package unix import ( + "crypto" + "crypto/tls" + "crypto/x509" "time" + "github.com/go-gost/core/bypass" mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/core/metadata/util" + "github.com/go-gost/x/registry" ) type metadata struct { - readTimeout time.Duration + readTimeout time.Duration + sniffing bool sniffingTimeout time.Duration + + certificate *x509.Certificate + privateKey crypto.PrivateKey + alpn string + mitmBypass bypass.Bypass } func (h *unixHandler) parseMetadata(md mdata.Metadata) (err error) { @@ -22,5 +33,20 @@ func (h *unixHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.sniffing = mdutil.GetBool(md, "sniffing") h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + certFile := mdutil.GetString(md, "mitm.certFile", "mitm.caCertFile") + keyFile := mdutil.GetString(md, "mitm.keyFile", "mitm.caKeyFile") + if certFile != "" && keyFile != "" { + tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + h.md.certificate, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + h.md.privateKey = tlsCert.PrivateKey + } + h.md.alpn = mdutil.GetString(md, "mitm.alpn") + h.md.mitmBypass = registry.BypassRegistry().Get(mdutil.GetString(md, "mitm.bypass")) return } diff --git a/internal/net/net.go b/internal/net/net.go index 866253cc..fe5106ea 100644 --- a/internal/net/net.go +++ b/internal/net/net.go @@ -3,6 +3,7 @@ package net import ( "context" "fmt" + "io" "net" "runtime" "strings" @@ -96,3 +97,25 @@ func (lc *ListenConfig) ListenPacket(ctx context.Context, network, address strin } return lc.ListenConfig.ListenPacket(ctx, network, address) } + +type readWriteConn struct { + net.Conn + r io.Reader + w io.Writer +} + +func NewReadWriteConn(r io.Reader, w io.Writer, c net.Conn) net.Conn { + return &readWriteConn{ + Conn: c, + r: r, + w: w, + } +} + +func (c *readWriteConn) Read(p []byte) (int, error) { + return c.r.Read(p) +} + +func (c *readWriteConn) Write(p []byte) (int, error) { + return c.w.Write(p) +} diff --git a/internal/net/transport.go b/internal/net/transport.go index 7ce4b10d..8e8806d4 100644 --- a/internal/net/transport.go +++ b/internal/net/transport.go @@ -1,9 +1,7 @@ package net import ( - "bufio" "io" - "net" "github.com/go-gost/core/common/bufpool" ) @@ -36,19 +34,3 @@ func CopyBuffer(dst io.Writer, src io.Reader, bufSize int) error { _, err := io.CopyBuffer(dst, src, buf) return err } - -type bufferReaderConn struct { - net.Conn - br *bufio.Reader -} - -func NewBufferReaderConn(conn net.Conn, br *bufio.Reader) net.Conn { - return &bufferReaderConn{ - Conn: conn, - br: br, - } -} - -func (c *bufferReaderConn) Read(b []byte) (int, error) { - return c.br.Read(b) -} diff --git a/internal/util/sniffing/sniff.go b/internal/util/sniffing/sniff.go index 8a11696f..087936da 100644 --- a/internal/util/sniffing/sniff.go +++ b/internal/util/sniffing/sniff.go @@ -51,5 +51,8 @@ func isHTTP(s string) bool { strings.HasPrefix(http.MethodPatch, s) || strings.HasPrefix(http.MethodHead, s[:4]) || strings.HasPrefix(http.MethodConnect, s) || - strings.HasPrefix(http.MethodTrace, s) + strings.HasPrefix(http.MethodTrace, s) || + // HTTP/2 connection preface + // PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n + strings.HasPrefix(s, "PRI *") } diff --git a/internal/util/sniffing/sniffer.go b/internal/util/sniffing/sniffer.go new file mode 100644 index 00000000..9da3a6de --- /dev/null +++ b/internal/util/sniffing/sniffer.go @@ -0,0 +1,572 @@ +package sniffing + +import ( + "bufio" + "bytes" + "context" + "crypto" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "strings" + "time" + + "github.com/go-gost/core/bypass" + "github.com/go-gost/core/logger" + "github.com/go-gost/core/recorder" + dissector "github.com/go-gost/tls-dissector" + xio "github.com/go-gost/x/internal/io" + xnet "github.com/go-gost/x/internal/net" + xhttp "github.com/go-gost/x/internal/net/http" + tls_util "github.com/go-gost/x/internal/util/tls" + xrecorder "github.com/go-gost/x/recorder" + "golang.org/x/net/http2" +) + +const ( + // Default max body size to record. + DefaultBodySize = 1024 * 1024 // 1MB +) + +var ( + DefaultCertPool = tls_util.NewMemoryCertPool() +) + +type Sniffer struct { + Dial func(ctx context.Context, network, address string) (net.Conn, error) + DialTLS func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) + + Recorder recorder.Recorder + RecorderOptions *recorder.Options + RecorderObject *xrecorder.HandlerRecorderObject + + // MITM TLS termination + Certificate *x509.Certificate + PrivateKey crypto.PrivateKey + NegotiatedProtocol string + CertPool tls_util.CertPool + MitmBypass bypass.Bypass + + ReadTimeout time.Duration + Log logger.Logger +} + +func (h *Sniffer) HandleHTTP(ctx context.Context, conn net.Conn) error { + br := bufio.NewReader(conn) + req, err := http.ReadRequest(br) + if err != nil { + return err + } + if h.Log.IsLevelEnabled(logger.TraceLevel) { + dump, _ := httputil.DumpRequest(req, false) + h.Log.Trace(string(dump)) + } + + ro := h.RecorderObject + host := req.Host + if host != "" { + if _, _, err := net.SplitHostPort(host); err != nil { + host = net.JoinHostPort(strings.Trim(host, "[]"), "80") + } + ro.Host = host + + h.Log = h.Log.WithFields(map[string]any{ + "host": host, + }) + } + + // http/2 + if req.Method == "PRI" && len(req.Header) == 0 && req.URL.Path == "*" && req.Proto == "HTTP/2.0" { + const expectedBody = "SM\r\n\r\n" + + buf := make([]byte, len(expectedBody)) + n, err := io.ReadFull(br, buf) + if err != nil { + return fmt.Errorf("h2: error reading client preface: %s", err) + } + if string(buf[:n]) != expectedBody { + return errors.New("h2: invalid client preface") + } + + ro.Time = time.Time{} + + h2s := &http2.Server{} + h2s.ServeConn(xnet.NewReadWriteConn(br, conn, conn), &http2.ServeConnOpts{ + Context: ctx, + SawClientPreface: true, + Handler: &h2Handler{ + Client: &http.Client{ + Transport: &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { + if dial := h.DialTLS; dial != nil { + return dial(ctx, network, addr, cfg) + } + + cc, err := (&net.Dialer{}).DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + + cc = tls.Client(cc, cfg) + return cc, nil + }, + }, + }, + Sniffer: h, + }, + }) + return nil + } + + dial := h.Dial + if dial == nil { + dial = (&net.Dialer{}).DialContext + } + cc, err := dial(ctx, "tcp", host) + if err != nil { + return err + } + defer cc.Close() + + ro.Time = time.Time{} + + shouldClose, err := h.httpRoundTrip(ctx, conn, cc, req) + if err != nil || shouldClose { + return err + } + + for { + req, err := http.ReadRequest(br) + if err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { + return nil + } + return err + } + + if h.Log.IsLevelEnabled(logger.TraceLevel) { + dump, _ := httputil.DumpRequest(req, false) + h.Log.Trace(string(dump)) + } + + if shouldClose, err := h.httpRoundTrip(ctx, conn, cc, req); err != nil || shouldClose { + return err + } + } +} + +func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriter, req *http.Request) (close bool, err error) { + close = true + + ro := &xrecorder.HandlerRecorderObject{} + *ro = *h.RecorderObject + + ro.Time = time.Now() + h.Log.Infof("%s <-> %s", ro.RemoteAddr, req.Host) + defer func() { + ro.Duration = time.Since(ro.Time) + if err != nil { + ro.Err = err.Error() + } + if err := ro.Record(ctx, h.Recorder); err != nil { + h.Log.Errorf("record: %v", err) + } + + h.Log.WithFields(map[string]any{ + "duration": time.Since(ro.Time), + }).Infof("%s >-< %s", ro.RemoteAddr, req.Host) + }() + + if clientIP := xhttp.GetClientIP(req); clientIP != nil { + ro.ClientIP = clientIP.String() + } + ro.HTTP = &xrecorder.HTTPRecorderObject{ + Host: req.Host, + Proto: req.Proto, + Scheme: req.URL.Scheme, + Method: req.Method, + URI: req.RequestURI, + Request: xrecorder.HTTPRequestRecorderObject{ + ContentLength: req.ContentLength, + Header: req.Header.Clone(), + }, + } + + // HTTP/1.0 + if req.ProtoMajor == 1 && req.ProtoMinor == 0 { + if strings.ToLower(req.Header.Get("Connection")) == "keep-alive" { + req.Header.Del("Connection") + } else { + req.Header.Set("Connection", "close") + } + } + + var reqBody *xhttp.Body + if opts := h.RecorderOptions; opts != nil && opts.HTTPBody { + if req.Body != nil { + maxSize := opts.MaxBodySize + if maxSize <= 0 { + maxSize = DefaultBodySize + } + reqBody = xhttp.NewBody(req.Body, maxSize) + req.Body = reqBody + } + } + + if err = req.Write(cc); err != nil { + return + } + + if reqBody != nil { + ro.HTTP.Request.Body = reqBody.Content() + ro.HTTP.Request.ContentLength = reqBody.Length() + } + + xio.SetReadDeadline(cc, time.Now().Add(h.ReadTimeout)) + resp, err := http.ReadResponse(bufio.NewReader(cc), req) + if err != nil { + h.Log.Errorf("read response: %v", err) + return + } + defer resp.Body.Close() + xio.SetReadDeadline(cc, time.Time{}) + + ro.HTTP.StatusCode = resp.StatusCode + ro.HTTP.Response.Header = resp.Header + ro.HTTP.Response.ContentLength = resp.ContentLength + + if h.Log.IsLevelEnabled(logger.TraceLevel) { + dump, _ := httputil.DumpResponse(resp, false) + h.Log.Trace(string(dump)) + } + + // HTTP/1.0 + if req.ProtoMajor == 1 && req.ProtoMinor == 0 { + if !resp.Close { + resp.Header.Set("Connection", "keep-alive") + } + resp.ProtoMajor = req.ProtoMajor + resp.ProtoMinor = req.ProtoMinor + } + + var respBody *xhttp.Body + if opts := h.RecorderOptions; opts != nil && opts.HTTPBody { + maxSize := opts.MaxBodySize + if maxSize <= 0 { + maxSize = DefaultBodySize + } + respBody = xhttp.NewBody(resp.Body, maxSize) + resp.Body = respBody + } + + if err = resp.Write(rw); err != nil { + h.Log.Errorf("write response: %v", err) + return + } + + if respBody != nil { + ro.HTTP.Response.Body = respBody.Content() + ro.HTTP.Response.ContentLength = respBody.Length() + } + + if req.Header.Get("Upgrade") == "websocket" { + xnet.Transport(rw, cc) + } + + return resp.Close, nil +} + +func (h *Sniffer) HandleTLS(ctx context.Context, conn net.Conn) error { + buf := new(bytes.Buffer) + clientHello, err := dissector.ParseClientHello(io.TeeReader(conn, buf)) + if err != nil { + return err + } + + ro := h.RecorderObject + ro.TLS = &xrecorder.TLSRecorderObject{ + ServerName: clientHello.ServerName, + ClientHello: hex.EncodeToString(buf.Bytes()), + } + if len(clientHello.SupportedProtos) > 0 { + ro.TLS.Proto = clientHello.SupportedProtos[0] + } + + host := clientHello.ServerName + if host != "" { + if _, _, err := net.SplitHostPort(host); err != nil { + host = net.JoinHostPort(strings.Trim(host, "[]"), "443") + } + ro.Host = host + } + + dial := h.Dial + if dial == nil { + dial = (&net.Dialer{}).DialContext + } + cc, err := dial(ctx, "tcp", host) + if err != nil { + return err + } + defer cc.Close() + + if h.Certificate != nil && h.PrivateKey != nil && + len(clientHello.SupportedProtos) > 0 && (clientHello.SupportedProtos[0] == "h2" || clientHello.SupportedProtos[0] == "http/1.1") { + if host == "" { + host = ro.Host + } + if h.MitmBypass == nil || !h.MitmBypass.Contains(ctx, "tcp", host) { + return h.terminateTLS(ctx, xnet.NewReadWriteConn(io.MultiReader(buf, conn), conn, conn), cc, clientHello) + } + } + + if _, err := buf.WriteTo(cc); err != nil { + return err + } + + xio.SetReadDeadline(cc, time.Now().Add(h.ReadTimeout)) + serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) + xio.SetReadDeadline(cc, time.Time{}) + + if serverHello != nil { + ro.TLS.CipherSuite = tls_util.CipherSuite(serverHello.CipherSuite).String() + ro.TLS.CompressionMethod = serverHello.CompressionMethod + if serverHello.Proto != "" { + ro.TLS.Proto = serverHello.Proto + } + if serverHello.Version > 0 { + ro.TLS.Version = tls_util.Version(serverHello.Version).String() + } + } + + if buf.Len() > 0 { + ro.TLS.ServerHello = hex.EncodeToString(buf.Bytes()) + } + + if _, err := buf.WriteTo(conn); err != nil { + return err + } + + h.Log.Infof("%s <-> %s", ro.RemoteAddr, ro.Host) + xnet.Transport(conn, cc) + h.Log.WithFields(map[string]any{ + "duration": time.Since(ro.Time), + }).Infof("%s >-< %s", ro.RemoteAddr, ro.Host) + + return err +} + +func (h *Sniffer) terminateTLS(ctx context.Context, conn, cc net.Conn, clientHello *dissector.ClientHelloInfo) error { + ro := h.RecorderObject + + nextProtos := clientHello.SupportedProtos + if h.NegotiatedProtocol != "" { + nextProtos = []string{h.NegotiatedProtocol} + } + + cfg := &tls.Config{ + ServerName: clientHello.ServerName, + NextProtos: nextProtos, + CipherSuites: clientHello.CipherSuites, + } + if cfg.ServerName == "" { + cfg.InsecureSkipVerify = true + } + clientConn := tls.Client(cc, cfg) + if err := clientConn.HandshakeContext(ctx); err != nil { + return err + } + + cs := clientConn.ConnectionState() + ro.TLS.CipherSuite = tls_util.CipherSuite(cs.CipherSuite).String() + ro.TLS.Proto = cs.NegotiatedProtocol + ro.TLS.Version = tls_util.Version(cs.Version).String() + + host := cfg.ServerName + if host == "" { + if host = cs.PeerCertificates[0].Subject.CommonName; host == "" { + host = ro.Host + } + } + if h, _, _ := net.SplitHostPort(host); h != "" { + host = h + } + + negotiatedProtocol := cs.NegotiatedProtocol + if h.NegotiatedProtocol != "" { + negotiatedProtocol = h.NegotiatedProtocol + } + nextProtos = nil + if negotiatedProtocol != "" { + nextProtos = []string{negotiatedProtocol} + } + + serverConn := tls.Server(conn, &tls.Config{ + NextProtos: nextProtos, + GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { + certPool := h.CertPool + if certPool == nil { + certPool = DefaultCertPool + } + serverName := chi.ServerName + if serverName == "" { + serverName = host + } + cert, err := certPool.Get(serverName) + if cert == nil { + cert, err = tls_util.GenerateCertificate(serverName, 7*24*time.Hour, h.Certificate, h.PrivateKey) + } + if err != nil { + return nil, err + } + + return &tls.Certificate{ + Certificate: [][]byte{cert.Raw}, + PrivateKey: h.PrivateKey, + }, nil + }, + }) + if err := serverConn.HandshakeContext(ctx); err != nil { + return err + } + + sniffer := &Sniffer{} + *sniffer = *h + + sniffer.Dial = func(ctx context.Context, network, address string) (net.Conn, error) { + return clientConn, nil + } + sniffer.DialTLS = func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) { + return clientConn, nil + } + return sniffer.HandleHTTP(ctx, serverConn) +} + +type h2Handler struct { + Client *http.Client + Sniffer *Sniffer +} + +func (h *h2Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log := h.Sniffer.Log + + ro := &xrecorder.HandlerRecorderObject{} + *ro = *h.Sniffer.RecorderObject + ro.Time = time.Now() + + var err error + log.Infof("%s <-> %s", ro.RemoteAddr, r.Host) + defer func() { + ro.Duration = time.Since(ro.Time) + if err != nil { + ro.Err = err.Error() + } + if err := ro.Record(r.Context(), h.Sniffer.Recorder); err != nil { + log.Errorf("record: %v", err) + } + + log.WithFields(map[string]any{ + "duration": time.Since(ro.Time), + }).Infof("%s >-< %s", ro.RemoteAddr, r.Host) + }() + + if clientIP := xhttp.GetClientIP(r); clientIP != nil { + ro.ClientIP = clientIP.String() + } + ro.HTTP = &xrecorder.HTTPRecorderObject{ + Host: r.Host, + Proto: r.Proto, + Scheme: "https", + Method: r.Method, + URI: r.RequestURI, + Request: xrecorder.HTTPRequestRecorderObject{ + ContentLength: r.ContentLength, + Header: r.Header.Clone(), + }, + } + + if log.IsLevelEnabled(logger.TraceLevel) { + dump, _ := httputil.DumpRequest(r, false) + log.Trace(string(dump)) + } + + url := r.URL + url.Scheme = "https" + url.Host = r.Host + req := &http.Request{ + Method: r.Method, + URL: url, + Host: r.Host, + Header: r.Header, + Body: r.Body, + ContentLength: r.ContentLength, + Trailer: r.Trailer, + } + + var reqBody *xhttp.Body + if opts := h.Sniffer.RecorderOptions; opts != nil && opts.HTTPBody { + if req.Body != nil { + maxSize := opts.MaxBodySize + if maxSize <= 0 { + maxSize = DefaultBodySize + } + reqBody = xhttp.NewBody(req.Body, maxSize) + req.Body = reqBody + } + } + + resp, err := h.Client.Do(req.WithContext(r.Context())) + if reqBody != nil { + ro.HTTP.Request.Body = reqBody.Content() + ro.HTTP.Request.ContentLength = reqBody.Length() + } + if err != nil { + log.Error(err) + w.WriteHeader(http.StatusServiceUnavailable) + return + } + defer resp.Body.Close() + + ro.HTTP.StatusCode = resp.StatusCode + ro.HTTP.Response.Header = resp.Header + ro.HTTP.Response.ContentLength = resp.ContentLength + + if log.IsLevelEnabled(logger.TraceLevel) { + dump, _ := httputil.DumpResponse(resp, false) + log.Trace(string(dump)) + } + + h.setHeader(w, resp.Header) + w.WriteHeader(resp.StatusCode) + + var respBody *xhttp.Body + if opts := h.Sniffer.RecorderOptions; opts != nil && opts.HTTPBody { + maxSize := opts.MaxBodySize + if maxSize <= 0 { + maxSize = DefaultBodySize + } + respBody = xhttp.NewBody(resp.Body, maxSize) + resp.Body = respBody + } + + io.Copy(w, resp.Body) + + if respBody != nil { + ro.HTTP.Response.Body = respBody.Content() + ro.HTTP.Response.ContentLength = respBody.Length() + } +} + +func (h *h2Handler) setHeader(w http.ResponseWriter, header http.Header) { + for k, v := range header { + for i := range v { + w.Header().Add(k, v[i]) + } + } +} diff --git a/internal/util/tls/tls.go b/internal/util/tls/tls.go index 64b48ec6..621e91cc 100644 --- a/internal/util/tls/tls.go +++ b/internal/util/tls/tls.go @@ -1,9 +1,14 @@ package tls import ( + "crypto" + "crypto/rand" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" + "errors" "fmt" + "math/big" "net" "os" "strconv" @@ -12,6 +17,7 @@ import ( "github.com/go-gost/core/logger" "github.com/go-gost/x/config" + "github.com/patrickmn/go-cache" ) const ( @@ -380,3 +386,76 @@ func WrapTLSClient(conn net.Conn, tlsConfig *tls.Config, timeout time.Duration) return tlsConn, err } + +var ( + ErrCertNotFound = errors.New("certificate not found") +) + +type CertPool interface { + Get(serverName string) (*x509.Certificate, error) + Put(serverName string, cert *x509.Certificate) +} + +type memoryCertPool struct { + cache *cache.Cache +} + +func NewMemoryCertPool() CertPool { + return &memoryCertPool{ + cache: cache.New(24*7*time.Hour, 1*time.Hour), + } +} + +func (p *memoryCertPool) Get(serverName string) (*x509.Certificate, error) { + v, ok := p.cache.Get(serverName) + if !ok { + return nil, ErrCertNotFound + } + return v.(*x509.Certificate), nil +} + +func (p *memoryCertPool) Put(serverName string, cert *x509.Certificate) { + p.cache.Set(serverName, cert, cache.DefaultExpiration) +} + +func GenerateCertificate(serverName string, validity time.Duration, caCert *x509.Certificate, caKey crypto.PrivateKey) (*x509.Certificate, error) { + if host, _, _ := net.SplitHostPort(serverName); host != "" { + serverName = host + } + + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(time.Now().UnixNano() / 100000), + Subject: pkix.Name{ + Organization: []string{"GOST"}, + }, + NotBefore: time.Now().Add(-validity), + NotAfter: time.Now().Add(validity), + SignatureAlgorithm: x509.SHA256WithRSA, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + } + + if ip := net.ParseIP(serverName); ip != nil { + tmpl.IPAddresses = []net.IP{ip} + } else { + tmpl.Subject.CommonName = serverName + tmpl.DNSNames = []string{serverName} + } + + pk, ok := caKey.(privateKey) + if !ok { + return nil, errors.New("invalid private key type") + } + + raw, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, pk.Public(), caKey) + if err != nil { + return nil, err + } + + return x509.ParseCertificate(raw) +} + +// https://pkg.go.dev/crypto#PrivateKey +type privateKey interface { + Public() crypto.PublicKey + Equal(x crypto.PrivateKey) bool +} diff --git a/recorder/recorder.go b/recorder/recorder.go index 9bc84cf3..ba8b0d8a 100644 --- a/recorder/recorder.go +++ b/recorder/recorder.go @@ -65,21 +65,21 @@ type HandlerRecorderObject struct { RemoteAddr string `json:"remote"` LocalAddr string `json:"local"` Host string `json:"host"` + Proto string `json:"proto,omitempty"` ClientIP string `json:"clientIP"` ClientID string `json:"clientID,omitempty"` - Proto string `json:"proto,omitempty"` HTTP *HTTPRecorderObject `json:"http,omitempty"` TLS *TLSRecorderObject `json:"tls,omitempty"` DNS *DNSRecorderObject `json:"dns,omitempty"` Route string `json:"route,omitempty"` Err string `json:"err,omitempty"` + SID string `json:"sid"` Duration time.Duration `json:"duration"` Time time.Time `json:"time"` - SID string `json:"sid"` } func (p *HandlerRecorderObject) Record(ctx context.Context, r recorder.Recorder) error { - if p == nil || r == nil || p.Time.IsZero(){ + if p == nil || r == nil || p.Time.IsZero() { return nil } diff --git a/service/service.go b/service/service.go index 2c93b787..daf00b3d 100644 --- a/service/service.go +++ b/service/service.go @@ -193,14 +193,19 @@ func (s *defaultService) Serve() error { clientIP = h } - ctx := ctxvalue.ContextWithSid(ctx, ctxvalue.Sid(xid.New().String())) + sid := xid.New().String() + ctx := ctxvalue.ContextWithSid(ctx, ctxvalue.Sid(sid)) ctx = ctxvalue.ContextWithClientAddr(ctx, ctxvalue.ClientAddr(clientAddr)) ctx = ctxvalue.ContextWithHash(ctx, &ctxvalue.Hash{Source: clientIP}) + log := s.options.logger.WithFields(map[string]any{ + "sid": sid, + }) + for _, rec := range s.options.recorders { if rec.Record == recorder.RecorderServiceClientAddress { if err := rec.Recorder.Record(ctx, []byte(clientIP)); err != nil { - s.options.logger.Errorf("record %s: %v", rec.Record, err) + log.Errorf("record %s: %v", rec.Record, err) } break } @@ -208,7 +213,7 @@ func (s *defaultService) Serve() error { if s.options.admission != nil && !s.options.admission.Admit(ctx, clientAddr) { conn.Close() - s.options.logger.Debugf("admission: %s is denied", clientAddr) + log.Debugf("admission: %s is denied", clientAddr) continue } @@ -233,7 +238,7 @@ func (s *defaultService) Serve() error { } if err := s.handler.Handle(ctx, conn); err != nil { - s.options.logger.Error(err) + log.Error(err) if v := xmetrics.GetCounter(xmetrics.MetricServiceHandlerErrorsCounter, metrics.Labels{"service": s.name, "client": clientIP}); v != nil { v.Inc()