From 3c8add4b8258a4e2be7daa7c7e748beef16589c2 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Fri, 27 Sep 2024 15:55:28 +0800 Subject: [PATCH] add traffic sniffing for handler --- handler/dns/handler.go | 1 + handler/file/handler.go | 94 +++++- handler/forward/local/handler.go | 421 ++---------------------- handler/forward/local/metadata.go | 12 +- handler/forward/local/sniffing.go | 492 ++++++++++++++++++++++++++++ handler/forward/remote/handler.go | 420 ++---------------------- handler/forward/remote/sniffing.go | 495 +++++++++++++++++++++++++++++ handler/http/handler.go | 326 ++++++++++--------- handler/http/metadata.go | 11 + handler/http/sniffing.go | 259 +++++++++++++++ handler/http/udp.go | 9 +- handler/http2/handler.go | 32 +- handler/redirect/tcp/handler.go | 426 ++----------------------- handler/redirect/tcp/metadata.go | 5 + handler/redirect/tcp/sniffing.go | 318 ++++++++++++++++++ handler/relay/connect.go | 29 +- handler/relay/metadata.go | 23 +- handler/relay/sniffing.go | 259 +++++++++++++++ handler/sni/handler.go | 326 ++----------------- handler/sni/metadata.go | 12 +- handler/sni/sniffing.go | 355 +++++++++++++++++++++ handler/socks/v4/handler.go | 29 +- handler/socks/v4/metadata.go | 16 +- handler/socks/v4/sniffing.go | 259 +++++++++++++++ handler/socks/v5/connect.go | 29 +- handler/socks/v5/handler.go | 4 +- handler/socks/v5/metadata.go | 9 + handler/socks/v5/sniffing.go | 259 +++++++++++++++ handler/ss/handler.go | 37 ++- handler/ss/metadata.go | 25 +- handler/ss/sniffing.go | 259 +++++++++++++++ handler/sshd/handler.go | 42 ++- handler/sshd/metadata.go | 14 + handler/sshd/sniffing.go | 259 +++++++++++++++ handler/tunnel/entrypoint.go | 16 +- handler/tunnel/sniffing.go | 275 ++++++++++++++++ handler/unix/handler.go | 63 +++- handler/unix/metadata.go | 14 + handler/unix/sniffing.go | 259 +++++++++++++++ internal/io/io.go | 16 +- internal/util/forward/forward.go | 90 ------ internal/util/sniffing/sniff.go | 55 ++++ recorder/recorder.go | 3 +- 43 files changed, 4518 insertions(+), 1839 deletions(-) create mode 100644 handler/forward/local/sniffing.go create mode 100644 handler/forward/remote/sniffing.go create mode 100644 handler/http/sniffing.go create mode 100644 handler/redirect/tcp/sniffing.go create mode 100644 handler/relay/sniffing.go create mode 100644 handler/sni/sniffing.go create mode 100644 handler/socks/v4/sniffing.go create mode 100644 handler/socks/v5/sniffing.go create mode 100644 handler/ss/sniffing.go create mode 100644 handler/sshd/sniffing.go create mode 100644 handler/tunnel/sniffing.go create mode 100644 handler/unix/sniffing.go delete mode 100644 internal/util/forward/forward.go create mode 100644 internal/util/sniffing/sniff.go diff --git a/handler/dns/handler.go b/handler/dns/handler.go index 2f3ddc28..4b61e3c9 100644 --- a/handler/dns/handler.go +++ b/handler/dns/handler.go @@ -139,6 +139,7 @@ func (h *dnsHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler. Network: conn.LocalAddr().Network(), RemoteAddr: conn.RemoteAddr().String(), LocalAddr: conn.LocalAddr().String(), + Proto: "dns", Time: start, SID: string(ctxvalue.SidFromContext(ctx)), } diff --git a/handler/file/handler.go b/handler/file/handler.go index d28e19e6..3d27739f 100644 --- a/handler/file/handler.go +++ b/handler/file/handler.go @@ -9,6 +9,8 @@ import ( "github.com/go-gost/core/handler" md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/recorder" + xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" ) @@ -17,11 +19,12 @@ func init() { } type fileHandler struct { - handler http.Handler - server *http.Server - ln *singleConnListener - md metadata - options handler.Options + handler http.Handler + server *http.Server + ln *singleConnListener + md metadata + options handler.Options + recorder recorder.RecorderObject } func NewHandler(opts ...handler.Option) handler.Handler { @@ -45,6 +48,13 @@ func (h *fileHandler) Init(md md.Metadata) (err error) { Handler: http.HandlerFunc(h.handleFunc), } + for _, ro := range h.options.Recorders { + if ro.Record == xrecorder.RecorderServiceHandler { + h.recorder = ro + break + } + } + h.ln = &singleConnListener{ conn: make(chan net.Conn), done: make(chan struct{}), @@ -70,8 +80,54 @@ func (h *fileHandler) Close() error { } func (h *fileHandler) handleFunc(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + ro := &xrecorder.HandlerRecorderObject{ + Service: h.options.Service, + RemoteAddr: r.RemoteAddr, + Network: "tcp", + Host: r.Host, + Proto: "http", + HTTP: &xrecorder.HTTPRecorderObject{ + Host: r.Host, + Method: r.Method, + Proto: r.Proto, + Scheme: r.URL.Scheme, + URI: r.RequestURI, + Request: xrecorder.HTTPRequestRecorderObject{ + ContentLength: r.ContentLength, + Header: r.Header, + }, + }, + Time: start, + } + ro.ClientIP, _, _ = net.SplitHostPort(r.RemoteAddr) + + log := h.options.Logger.WithFields(map[string]any{ + "remote": r.RemoteAddr, + }) + + rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} + + defer func() { + ro.Duration = time.Since(start) + ro.HTTP.StatusCode = rw.statusCode + ro.HTTP.Response = xrecorder.HTTPResponseRecorderObject{ + ContentLength: rw.contentLength, + Header: rw.Header(), + } + if err := ro.Record(context.Background(), h.recorder.Recorder); err != nil { + log.Errorf("record: %v", err) + } + + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s %s %d %d", r.Method, r.RequestURI, rw.statusCode, rw.contentLength) + }() + if auther := h.options.Auther; auther != nil { u, p, _ := r.BasicAuth() + ro.ClientID = u if _, ok := auther.Authenticate(r.Context(), u, p); !ok { w.Header().Set("WWW-Authenticate", "Basic") w.WriteHeader(http.StatusUnauthorized) @@ -79,16 +135,7 @@ func (h *fileHandler) handleFunc(w http.ResponseWriter, r *http.Request) { } } - log := h.options.Logger - start := time.Now() - - h.handler.ServeHTTP(w, r) - - log = log.WithFields(map[string]any{ - "remote": r.RemoteAddr, - "duration": time.Since(start), - }) - log.Infof("%s %s", r.Method, r.RequestURI) + h.handler.ServeHTTP(rw, r) } type singleConnListener struct { @@ -132,3 +179,20 @@ func (l *singleConnListener) send(conn net.Conn) { return } } + +type responseWriter struct { + http.ResponseWriter + statusCode int + contentLength int64 +} + +func (w *responseWriter) Write(p []byte) (int, error) { + n, err := w.ResponseWriter.Write(p) + w.contentLength += int64(n) + return n, err +} + +func (w *responseWriter) WriteHeader(statusCode int) { + w.statusCode = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} diff --git a/handler/forward/local/handler.go b/handler/forward/local/handler.go index a2fb6db3..a7c20e2f 100644 --- a/handler/forward/local/handler.go +++ b/handler/forward/local/handler.go @@ -4,41 +4,26 @@ import ( "bufio" "bytes" "context" - "crypto/tls" "errors" "fmt" "io" "net" - "net/http" - "net/http/httputil" - "strconv" - "strings" "time" - "github.com/go-gost/core/bypass" "github.com/go-gost/core/chain" "github.com/go-gost/core/handler" "github.com/go-gost/core/hop" - "github.com/go-gost/core/logger" md "github.com/go-gost/core/metadata" "github.com/go-gost/core/recorder" - xbypass "github.com/go-gost/x/bypass" - "github.com/go-gost/x/config" ctxvalue "github.com/go-gost/x/ctx" 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/util/forward" - tls_util "github.com/go-gost/x/internal/util/tls" + "github.com/go-gost/x/internal/util/sniffing" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" ) -const ( - defaultBodySize = 1024 * 1024 // 1MB -) - func init() { registry.HandlerRegistry().Register("tcp", NewHandler) registry.HandlerRegistry().Register("udp", NewHandler) @@ -105,13 +90,11 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand 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), @@ -129,46 +112,40 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand ro.Network = network var rw io.ReadWriter = conn - var host string - var protocol string + var proto string if network == "tcp" && h.md.sniffing { if h.md.sniffingTimeout > 0 { conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) } - rw, host, protocol, _ = forward.Sniffing(ctx, conn) - log.Debugf("sniffing: host=%s, protocol=%s", host, protocol) + + br := bufio.NewReader(conn) + proto, _ = sniffing.Sniff(ctx, br) + ro.Proto = proto + if h.md.sniffingTimeout > 0 { conn.SetReadDeadline(time.Time{}) } - } - if protocol == forward.ProtoHTTP { - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - - h.handleHTTP(ctx, xio.NewReadWriteCloser(rw, rw, conn), conn.RemoteAddr(), ro2, log) - return nil - } - - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "0") + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, ro, log) + } } var target *chain.Node - if host != "" { - target = &chain.Node{ - Addr: host, - } - } if h.hop != nil { target = h.hop.Select(ctx, - hop.HostSelectOption(host), - hop.ProtocolSelectOption(protocol), + hop.ProtocolSelectOption(proto), ) } if target == nil { - err := errors.New("target not available") + err := errors.New("node not available") log.Error(err) return err } @@ -189,7 +166,6 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand ro.Host = addr log = log.WithFields(map[string]any{ - "host": host, "node": target.Name, "dst": fmt.Sprintf("%s/%s", addr, network), }) @@ -208,10 +184,10 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand } return err } - defer cc.Close() if marker := target.Marker(); marker != nil { marker.Reset() } + defer cc.Close() t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr) @@ -223,324 +199,6 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand return nil } -func (h *forwardHandler) handleHTTP(ctx context.Context, rw io.ReadWriteCloser, remoteAddr net.Addr, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) { - br := bufio.NewReader(rw) - - for { - var cc net.Conn - - 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 - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - start := time.Now() - ro.Time = start - 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(), - }, - } - if clientIP := xhttp.GetClientIP(req); clientIP != nil { - ro.ClientIP = clientIP.String() - } - - defer func() { - if err != nil { - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - - ro.Duration = time.Since(start) - ro.Err = err.Error() - ro.Record(ctx, h.recorder.Recorder) - } - }() - - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - if bp := h.options.Bypass; bp != nil && bp.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { - log.Debugf("bypass: %s %s", host, req.RequestURI) - resp.StatusCode = http.StatusForbidden - resp.Write(rw) - return xbypass.ErrBypass - } - - if addr := getRealClientAddr(req, remoteAddr); addr != remoteAddr { - log = log.WithFields(map[string]any{ - "src": addr.String(), - }) - remoteAddr = addr - ctx = ctxvalue.ContextWithClientAddr(ctx, ctxvalue.ClientAddr(remoteAddr.String())) - } - - target := &chain.Node{ - Addr: req.Host, - } - if h.hop != nil { - target = h.hop.Select(ctx, - hop.HostSelectOption(req.Host), - hop.ProtocolSelectOption(forward.ProtoHTTP), - hop.PathSelectOption(req.URL.Path), - ) - } - if target == nil { - log.Warnf("node for %s not found", req.Host) - resp.StatusCode = http.StatusBadGateway - return resp.Write(rw) - } - - ro.Host = target.Addr - - log = log.WithFields(map[string]any{ - "host": req.Host, - "node": target.Name, - "dst": target.Addr, - }) - log.Debugf("find node for host %s -> %s(%s)", req.Host, target.Name, target.Addr) - - var bodyRewrites []chain.HTTPBodyRewriteSettings - if httpSettings := target.Options().HTTP; httpSettings != nil { - if auther := httpSettings.Auther; auther != nil { - username, password, _ := req.BasicAuth() - id, ok := auther.Authenticate(ctx, username, password) - if !ok { - resp.StatusCode = http.StatusUnauthorized - resp.Header.Set("WWW-Authenticate", "Basic") - log.Warnf("node %s(%s) 401 unauthorized", target.Name, target.Addr) - return resp.Write(rw) - } - ctx = ctxvalue.ContextWithClientID(ctx, ctxvalue.ClientID(id)) - } - - if httpSettings.Host != "" { - req.Host = httpSettings.Host - } - for k, v := range httpSettings.Header { - req.Header.Set(k, v) - } - - for _, re := range httpSettings.RewriteURL { - if re.Pattern.MatchString(req.URL.Path) { - if s := re.Pattern.ReplaceAllString(req.URL.Path, re.Replacement); s != "" { - req.URL.Path = s - break - } - } - } - - bodyRewrites = httpSettings.RewriteBody - } - - var buf bytes.Buffer - cc, err = h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", target.Addr) - ro.Route = buf.String() - if err != nil { - // TODO: the router itself may be failed due to the failed node in the router, - // the dead marker may be a wrong operation. - if marker := target.Marker(); marker != nil { - marker.Mark() - } - log.Warnf("connect to node %s(%s) failed: %v", target.Name, target.Addr, err) - return resp.Write(rw) - } - if marker := target.Marker(); marker != nil { - marker.Reset() - } - - log.Debugf("connection to node %s(%s)", target.Name, target.Addr) - - if tlsSettings := target.Options().TLS; tlsSettings != nil { - cfg := &tls.Config{ - ServerName: tlsSettings.ServerName, - InsecureSkipVerify: !tlsSettings.Secure, - } - tls_util.SetTLSOptions(cfg, &config.TLSOptions{ - MinVersion: tlsSettings.Options.MinVersion, - MaxVersion: tlsSettings.Options.MaxVersion, - CipherSuites: tlsSettings.Options.CipherSuites, - ALPN: tlsSettings.Options.ALPN, - }) - cc = tls.Client(cc, cfg) - } - - 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 { - cc.Close() - log.Warnf("send request to node %s(%s): %v", target.Name, target.Addr, err) - resp.Write(rw) - 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(cc, xio.NewReadWriter(br, rw)) - if err == nil { - err = io.EOF - } - return err - } - - go func() { - defer cc.Close() - - var err error - var res *http.Response - var respBody *xhttp.Body - - defer func() { - ro.Duration = time.Since(start) - 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, h.recorder.Recorder) - }() - - res, err = http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - log.Warnf("read response from node %s(%s): %v", target.Name, target.Addr, err) - resp.Write(rw) - return - } - defer res.Body.Close() - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(res, false) - log.Trace(string(dump)) - } - - if res.Close { - defer rw.Close() - } - - if err = h.rewriteBody(res, bodyRewrites...); err != nil { - rw.Close() - log.Errorf("rewrite body: %v", err) - return - } - - if opts := h.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(rw); err != nil { - rw.Close() - log.Errorf("write response from node %s(%s): %v", target.Name, target.Addr, err) - } - }() - - return nil - }() - - if err != nil { - if cc != nil { - cc.Close() - } - break - } - } - - return -} - -func (h *forwardHandler) rewriteBody(resp *http.Response, rewrites ...chain.HTTPBodyRewriteSettings) error { - if resp == nil || len(rewrites) == 0 || resp.ContentLength <= 0 { - return nil - } - - if encoding := resp.Header.Get("Content-Encoding"); encoding != "" { - return nil - } - - body, err := drainBody(resp.Body) - if err != nil || body == nil { - return err - } - - contentType, _, _ := strings.Cut(resp.Header.Get("Content-Type"), ";") - for _, rewrite := range rewrites { - rewriteType := rewrite.Type - if rewriteType == "" { - rewriteType = "text/html" - } - if rewriteType != "*" && !strings.Contains(rewriteType, contentType) { - continue - } - - body = rewrite.Pattern.ReplaceAll(body, rewrite.Replacement) - } - - resp.Body = io.NopCloser(bytes.NewReader(body)) - resp.ContentLength = int64(len(body)) - - return nil -} - -func drainBody(b io.ReadCloser) (body []byte, err error) { - if b == nil || b == http.NoBody { - // No copying needed. Preserve the magic sentinel meaning of NoBody. - return nil, nil - } - var buf bytes.Buffer - if _, err = buf.ReadFrom(b); err != nil { - return nil, err - } - if err = b.Close(); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - func (h *forwardHandler) checkRateLimit(addr net.Addr) bool { if h.options.RateLimiter == nil { return true @@ -552,34 +210,3 @@ func (h *forwardHandler) checkRateLimit(addr net.Addr) bool { return true } - -func 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, - } -} diff --git a/handler/forward/local/metadata.go b/handler/forward/local/metadata.go index 7b24fab0..ef9e675b 100644 --- a/handler/forward/local/metadata.go +++ b/handler/forward/local/metadata.go @@ -14,13 +14,11 @@ type metadata struct { } func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) { - const ( - readTimeout = "readTimeout" - sniffing = "sniffing" - ) - - h.md.readTimeout = mdutil.GetDuration(md, readTimeout) - h.md.sniffing = mdutil.GetBool(md, sniffing) + h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + if h.md.readTimeout <= 0 { + h.md.readTimeout = 15 * time.Second + } + h.md.sniffing = mdutil.GetBool(md, "sniffing") h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") return } diff --git a/handler/forward/local/sniffing.go b/handler/forward/local/sniffing.go new file mode 100644 index 00000000..51afb60f --- /dev/null +++ b/handler/forward/local/sniffing.go @@ -0,0 +1,492 @@ +package local + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "encoding/hex" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "strings" + "time" + + "github.com/go-gost/core/bypass" + "github.com/go-gost/core/chain" + "github.com/go-gost/core/hop" + "github.com/go-gost/core/logger" + dissector "github.com/go-gost/tls-dissector" + 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" + 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" + xrecorder "github.com/go-gost/x/recorder" +) + +const ( + defaultBodySize = 1024 * 1024 // 1MB +) + +func (h *forwardHandler) 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)) + } + + shouldClose, err := h.httpRoundTrip(ctx, rw, 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, req, ro, log); err != nil || shouldClose { + return err + } + } +} + +func (h *forwardHandler) httpRoundTrip(ctx context.Context, rw io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { + close = true + + 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(), + }, + } + + if clientIP := xhttp.GetClientIP(req); clientIP != nil { + ro.ClientIP = clientIP.String() + } + { + clientAddr := ro.RemoteAddr + if ro.ClientIP != "" { + if _, port, _ := net.SplitHostPort(ro.RemoteAddr); port != "" { + clientAddr = net.JoinHostPort(ro.ClientIP, port) + } + ctx = ctxvalue.ContextWithClientAddr(ctx, ctxvalue.ClientAddr(clientAddr)) + } + } + + // 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") + } + } + + res := &http.Response{ + ProtoMajor: req.ProtoMajor, + ProtoMinor: req.ProtoMinor, + Header: http.Header{}, + StatusCode: http.StatusServiceUnavailable, + } + + host := req.Host + if _, _, err := net.SplitHostPort(host); err != nil { + host = net.JoinHostPort(strings.Trim(host, "[]"), "80") + } + + if bp := h.options.Bypass; bp != nil && + bp.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { + log.Debugf("bypass: %s %s", host, req.RequestURI) + res.StatusCode = http.StatusForbidden + ro.HTTP.StatusCode = res.StatusCode + res.Write(rw) + err = xbypass.ErrBypass + return + } + + target := &chain.Node{ + Addr: host, + } + if h.hop != nil { + target = h.hop.Select(ctx, + hop.HostSelectOption(host), + hop.ProtocolSelectOption(sniffing.ProtoHTTP), + hop.PathSelectOption(req.URL.Path), + ) + } + if target == nil { + log.Warnf("node for %s not found", host) + res.StatusCode = http.StatusBadGateway + ro.HTTP.StatusCode = res.StatusCode + res.Write(rw) + err = errors.New("node not available") + return + } + + ro.Host = target.Addr + + log = log.WithFields(map[string]any{ + "host": req.Host, + "node": target.Name, + "dst": target.Addr, + }) + log.Debugf("find node for host %s -> %s(%s)", host, target.Name, target.Addr) + + var bodyRewrites []chain.HTTPBodyRewriteSettings + if httpSettings := target.Options().HTTP; httpSettings != nil { + if auther := httpSettings.Auther; auther != nil { + username, password, _ := req.BasicAuth() + id, ok := auther.Authenticate(ctx, username, password) + if !ok { + res.StatusCode = http.StatusUnauthorized + ro.HTTP.StatusCode = res.StatusCode + res.Header.Set("WWW-Authenticate", "Basic") + log.Warnf("node %s(%s) 401 unauthorized", target.Name, target.Addr) + res.Write(rw) + err = errors.New("unauthorized") + return + } + ctx = ctxvalue.ContextWithClientID(ctx, ctxvalue.ClientID(id)) + } + + if httpSettings.Host != "" { + req.Host = httpSettings.Host + } + for k, v := range httpSettings.Header { + req.Header.Set(k, v) + } + + for _, re := range httpSettings.RewriteURL { + if re.Pattern.MatchString(req.URL.Path) { + if s := re.Pattern.ReplaceAllString(req.URL.Path, re.Replacement); s != "" { + req.URL.Path = s + break + } + } + } + + bodyRewrites = httpSettings.RewriteBody + } + + var buf bytes.Buffer + cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", target.Addr) + ro.Route = buf.String() + if err != nil { + // TODO: the router itself may be failed due to the failed node in the router, + // the dead marker may be a wrong operation. + if marker := target.Marker(); marker != nil { + marker.Mark() + } + log.Warnf("connect to node %s(%s) failed: %v", target.Name, target.Addr, err) + res.Write(rw) + return + } + if marker := target.Marker(); marker != nil { + marker.Reset() + } + // TODO: re-use the connection + defer cc.Close() + + log.Debugf("connect to node %s(%s)", target.Name, target.Addr) + + if tlsSettings := target.Options().TLS; tlsSettings != nil { + cfg := &tls.Config{ + ServerName: tlsSettings.ServerName, + InsecureSkipVerify: !tlsSettings.Secure, + } + tls_util.SetTLSOptions(cfg, &config.TLSOptions{ + MinVersion: tlsSettings.Options.MinVersion, + MaxVersion: tlsSettings.Options.MaxVersion, + CipherSuites: tlsSettings.Options.CipherSuites, + ALPN: tlsSettings.Options.ALPN, + }) + cc = tls.Client(cc, cfg) + } + + 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 { + res.Write(rw) + return + } + + if reqBody != nil { + ro.HTTP.Request.Body = reqBody.Content() + ro.HTTP.Request.ContentLength = reqBody.Length() + } + + cc.SetReadDeadline(time.Now().Add(h.md.readTimeout)) + resp, err := http.ReadResponse(bufio.NewReader(cc), req) + if err != nil { + log.Errorf("read response: %v", err) + res.Write(rw) + return + } + defer resp.Body.Close() + cc.SetReadDeadline(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 + } + + if err = h.rewriteBody(resp, bodyRewrites...); err != nil { + log.Errorf("rewrite body: %v", err) + return + } + + 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(ctx context.Context, rw 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] + } + + host := clientHello.ServerName + if _, _, err := net.SplitHostPort(host); err != nil { + host = net.JoinHostPort(strings.Trim(host, "[]"), "0") + } + + var target *chain.Node + if host != "" { + target = &chain.Node{ + Addr: host, + } + } + if h.hop != nil { + target = h.hop.Select(ctx, + hop.HostSelectOption(host), + hop.ProtocolSelectOption(sniffing.ProtoTLS), + ) + } + if target == nil { + err := errors.New("target not available") + log.Error(err) + return err + } + + addr := target.Addr + if opts := target.Options(); opts != nil { + switch opts.Network { + case "unix": + ro.Network = opts.Network + default: + if _, _, err := net.SplitHostPort(addr); err != nil { + addr += ":0" + } + } + } + ro.Host = addr + + log = log.WithFields(map[string]any{ + "host": host, + "node": target.Name, + "dst": fmt.Sprintf("%s/%s", addr, ro.Network), + }) + + log.Debugf("%s >> %s", ro.RemoteAddr, addr) + + var routeBuf bytes.Buffer + cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &routeBuf), ro.Network, addr) + ro.Route = routeBuf.String() + if err != nil { + log.Error(err) + // TODO: the router itself may be failed due to the failed node in the router, + // the dead marker may be a wrong operation. + if marker := target.Marker(); marker != nil { + marker.Mark() + } + return err + } + defer cc.Close() + if marker := target.Marker(); marker != nil { + marker.Reset() + } + + 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, addr) + netpkg.Transport(rw, cc) + log.WithFields(map[string]any{ + "duration": time.Since(t), + }).Infof("%s >-< %s", ro.RemoteAddr, addr) + + return err +} + +func (h *forwardHandler) rewriteBody(resp *http.Response, rewrites ...chain.HTTPBodyRewriteSettings) error { + if resp == nil || len(rewrites) == 0 || resp.ContentLength <= 0 { + return nil + } + + if encoding := resp.Header.Get("Content-Encoding"); encoding != "" { + return nil + } + + body, err := drainBody(resp.Body) + if err != nil || body == nil { + return err + } + + contentType, _, _ := strings.Cut(resp.Header.Get("Content-Type"), ";") + for _, rewrite := range rewrites { + rewriteType := rewrite.Type + if rewriteType == "" { + rewriteType = "text/html" + } + if rewriteType != "*" && !strings.Contains(rewriteType, contentType) { + continue + } + + body = rewrite.Pattern.ReplaceAll(body, rewrite.Replacement) + } + + resp.Body = io.NopCloser(bytes.NewReader(body)) + resp.ContentLength = int64(len(body)) + + return nil +} + +func drainBody(b io.ReadCloser) (body []byte, err error) { + if b == nil || b == http.NoBody { + // No copying needed. Preserve the magic sentinel meaning of NoBody. + return nil, nil + } + var buf bytes.Buffer + if _, err = buf.ReadFrom(b); err != nil { + return nil, err + } + if err = b.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/handler/forward/remote/handler.go b/handler/forward/remote/handler.go index 4f39e388..522f90a8 100644 --- a/handler/forward/remote/handler.go +++ b/handler/forward/remote/handler.go @@ -4,43 +4,28 @@ import ( "bufio" "bytes" "context" - "crypto/tls" "errors" "fmt" "io" "net" - "net/http" - "net/http/httputil" "strconv" - "strings" "time" - "github.com/go-gost/core/bypass" "github.com/go-gost/core/chain" "github.com/go-gost/core/handler" "github.com/go-gost/core/hop" - "github.com/go-gost/core/logger" mdata "github.com/go-gost/core/metadata" - mdutil "github.com/go-gost/core/metadata/util" "github.com/go-gost/core/recorder" - xbypass "github.com/go-gost/x/bypass" - "github.com/go-gost/x/config" ctxvalue "github.com/go-gost/x/ctx" 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" - "github.com/go-gost/x/internal/util/forward" - tls_util "github.com/go-gost/x/internal/util/tls" + "github.com/go-gost/x/internal/util/sniffing" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" ) -const ( - defaultBodySize = 1024 * 1024 // 1MB -) - func init() { registry.HandlerRegistry().Register("rtcp", NewHandler) registry.HandlerRegistry().Register("rudp", NewHandler) @@ -106,13 +91,11 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand 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), @@ -129,49 +112,41 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand } ro.Network = network - localAddr := convertAddr(conn.LocalAddr()) - var rw io.ReadWriter = conn - var host string - var protocol string + var proto string if network == "tcp" && h.md.sniffing { if h.md.sniffingTimeout > 0 { conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) } - rw, host, protocol, _ = forward.Sniffing(ctx, conn) - log.Debugf("sniffing: host=%s, protocol=%s", host, protocol) + + br := bufio.NewReader(conn) + proto, _ = sniffing.Sniff(ctx, br) + ro.Proto = proto + if h.md.sniffingTimeout > 0 { conn.SetReadDeadline(time.Time{}) } - } - if protocol == forward.ProtoHTTP { - ro2 := &xrecorder.HandlerRecorderObject{} - *ro2 = *ro - ro.Time = time.Time{} - h.handleHTTP(ctx, xio.NewReadWriteCloser(rw, rw, conn), conn.RemoteAddr(), localAddr, ro2, log) - return nil - } - - if md, ok := conn.(mdata.Metadatable); ok { - if v := mdutil.GetString(md.Metadata(), "host"); v != "" { - host = v + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, ro, log) } } + var target *chain.Node - if host != "" { - target = &chain.Node{ - Addr: host, - } - } if h.hop != nil { target = h.hop.Select(ctx, - hop.HostSelectOption(host), - hop.ProtocolSelectOption(protocol), + hop.ProtocolSelectOption(proto), ) } if target == nil { - err := errors.New("target not available") + err := errors.New("node not available") log.Error(err) return err } @@ -188,7 +163,6 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand ro.Host = target.Addr log = log.WithFields(map[string]any{ - "host": host, "node": target.Name, "dst": fmt.Sprintf("%s/%s", target.Addr, network), }) @@ -212,7 +186,7 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand marker.Reset() } - cc = proxyproto.WrapClientConn(h.md.proxyProtocol, conn.RemoteAddr(), localAddr, cc) + cc = proxyproto.WrapClientConn(h.md.proxyProtocol, conn.RemoteAddr(), convertAddr(conn.LocalAddr()), cc) t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr) @@ -224,321 +198,6 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand return nil } -func (h *forwardHandler) handleHTTP(ctx context.Context, rw io.ReadWriteCloser, remoteAddr net.Addr, localAddr net.Addr, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) { - br := bufio.NewReader(rw) - - for { - var cc net.Conn - 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 - } - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpRequest(req, false) - log.Trace(string(dump)) - } - - start := time.Now() - ro.Time = start - 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(), - }, - } - if clientIP := xhttp.GetClientIP(req); clientIP != nil { - ro.ClientIP = clientIP.String() - } - - defer func() { - if err != nil { - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.Header = resp.Header - - ro.Duration = time.Since(start) - ro.Err = err.Error() - ro.Record(ctx, h.recorder.Recorder) - } - }() - - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - if bp := h.options.Bypass; bp != nil && - bp.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { - log.Debugf("bypass: %s %s", host, req.RequestURI) - resp.StatusCode = http.StatusForbidden - resp.Write(rw) - return xbypass.ErrBypass - } - - if addr := getRealClientAddr(req, remoteAddr); addr != remoteAddr { - log = log.WithFields(map[string]any{ - "src": addr.String(), - }) - remoteAddr = addr - ctx = ctxvalue.ContextWithClientAddr(ctx, ctxvalue.ClientAddr(remoteAddr.String())) - } - - target := &chain.Node{ - Addr: req.Host, - } - if h.hop != nil { - target = h.hop.Select(ctx, - hop.HostSelectOption(req.Host), - hop.ProtocolSelectOption(forward.ProtoHTTP), - hop.PathSelectOption(req.URL.Path), - ) - } - if target == nil { - log.Warnf("node for %s not found", req.Host) - resp.StatusCode = http.StatusBadGateway - return resp.Write(rw) - } - - ro.Host = target.Addr - - log = log.WithFields(map[string]any{ - "host": req.Host, - "node": target.Name, - "dst": target.Addr, - }) - log.Debugf("find node for host %s -> %s(%s)", req.Host, target.Name, target.Addr) - - var bodyRewrites []chain.HTTPBodyRewriteSettings - if httpSettings := target.Options().HTTP; httpSettings != nil { - if auther := httpSettings.Auther; auther != nil { - username, password, _ := req.BasicAuth() - id, ok := auther.Authenticate(ctx, username, password) - if !ok { - resp.StatusCode = http.StatusUnauthorized - resp.Header.Set("WWW-Authenticate", "Basic") - log.Warnf("node %s(%s) 401 unauthorized", target.Name, target.Addr) - return resp.Write(rw) - } - ctx = ctxvalue.ContextWithClientID(ctx, ctxvalue.ClientID(id)) - } - if httpSettings.Host != "" { - req.Host = httpSettings.Host - } - for k, v := range httpSettings.Header { - req.Header.Set(k, v) - } - - for _, re := range httpSettings.RewriteURL { - if re.Pattern.MatchString(req.URL.Path) { - if s := re.Pattern.ReplaceAllString(req.URL.Path, re.Replacement); s != "" { - req.URL.Path = s - break - } - } - } - - bodyRewrites = httpSettings.RewriteBody - } - - var buf bytes.Buffer - cc, err = h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", target.Addr) - ro.Route = buf.String() - if err != nil { - // TODO: the router itself may be failed due to the failed node in the router, - // the dead marker may be a wrong operation. - if marker := target.Marker(); marker != nil { - marker.Mark() - } - log.Warnf("connect to node %s(%s) failed: %v", target.Name, target.Addr, err) - return resp.Write(rw) - } - if marker := target.Marker(); marker != nil { - marker.Reset() - } - - log.Debugf("new connection to node %s(%s)", target.Name, target.Addr) - - if tlsSettings := target.Options().TLS; tlsSettings != nil { - cfg := &tls.Config{ - ServerName: tlsSettings.ServerName, - InsecureSkipVerify: !tlsSettings.Secure, - } - tls_util.SetTLSOptions(cfg, &config.TLSOptions{ - MinVersion: tlsSettings.Options.MinVersion, - MaxVersion: tlsSettings.Options.MaxVersion, - CipherSuites: tlsSettings.Options.CipherSuites, - ALPN: tlsSettings.Options.ALPN, - }) - cc = tls.Client(cc, cfg) - } - - cc = proxyproto.WrapClientConn(h.md.proxyProtocol, remoteAddr, localAddr, cc) - - 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 { - cc.Close() - log.Warnf("send request to node %s(%s): %v", target.Name, target.Addr, err) - resp.Write(rw) - 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(cc, xio.NewReadWriter(br, rw)) - if err == nil { - err = io.EOF - } - return err - } - - go func() { - defer cc.Close() - - var err error - var res *http.Response - var respBody *xhttp.Body - - defer func() { - ro.Duration = time.Since(start) - 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, h.recorder.Recorder) - }() - - res, err = http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - log.Warnf("read response from node %s(%s): %v", target.Name, target.Addr, err) - resp.Write(rw) - return - } - defer res.Body.Close() - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(res, false) - log.Trace(string(dump)) - } - - if res.Close { - defer rw.Close() - } - - if err = h.rewriteBody(res, bodyRewrites...); err != nil { - rw.Close() - log.Errorf("rewrite body: %v", err) - return - } - - if opts := h.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(rw); err != nil { - rw.Close() - log.Errorf("write response from node %s(%s): %v", target.Name, target.Addr, err) - } - }() - - return nil - }() - - if err != nil { - if cc != nil { - cc.Close() - } - break - } - } - - return -} - -func (h *forwardHandler) rewriteBody(resp *http.Response, rewrites ...chain.HTTPBodyRewriteSettings) error { - if resp == nil || len(rewrites) == 0 || resp.ContentLength <= 0 { - return nil - } - - body, err := drainBody(resp.Body) - if err != nil || body == nil { - return err - } - - contentType, _, _ := strings.Cut(resp.Header.Get("Content-Type"), ";") - for _, rewrite := range rewrites { - rewriteType := rewrite.Type - if rewriteType == "" { - rewriteType = "text/html" - } - if rewriteType != "*" && !strings.Contains(rewriteType, contentType) { - continue - } - - body = rewrite.Pattern.ReplaceAll(body, rewrite.Replacement) - } - - resp.Body = io.NopCloser(bytes.NewReader(body)) - resp.ContentLength = int64(len(body)) - - return nil -} - -func drainBody(b io.ReadCloser) (body []byte, err error) { - if b == nil || b == http.NoBody { - // No copying needed. Preserve the magic sentinel meaning of NoBody. - return nil, nil - } - var buf bytes.Buffer - if _, err = buf.ReadFrom(b); err != nil { - return nil, err - } - if err = b.Close(); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - func (h *forwardHandler) checkRateLimit(addr net.Addr) bool { if h.options.RateLimiter == nil { return true @@ -574,34 +233,3 @@ func convertAddr(addr net.Addr) net.Addr { } } } - -func 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, - } -} diff --git a/handler/forward/remote/sniffing.go b/handler/forward/remote/sniffing.go new file mode 100644 index 00000000..e4fd50f0 --- /dev/null +++ b/handler/forward/remote/sniffing.go @@ -0,0 +1,495 @@ +package remote + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "encoding/hex" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "strings" + "time" + + "github.com/go-gost/core/bypass" + "github.com/go-gost/core/chain" + "github.com/go-gost/core/hop" + "github.com/go-gost/core/logger" + dissector "github.com/go-gost/tls-dissector" + 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" + 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" + 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 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)) + } + + shouldClose, err := h.httpRoundTrip(ctx, rw, 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, req, ro, log); err != nil || shouldClose { + return err + } + } +} + +func (h *forwardHandler) httpRoundTrip(ctx context.Context, rw io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { + close = true + + 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(), + }, + } + + if clientIP := xhttp.GetClientIP(req); clientIP != nil { + ro.ClientIP = clientIP.String() + } + clientAddr := ro.RemoteAddr + if ro.ClientIP != "" { + if _, port, _ := net.SplitHostPort(ro.RemoteAddr); port != "" { + clientAddr = net.JoinHostPort(ro.ClientIP, port) + } + ctx = ctxvalue.ContextWithClientAddr(ctx, ctxvalue.ClientAddr(clientAddr)) + } + + // 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") + } + } + + res := &http.Response{ + ProtoMajor: req.ProtoMajor, + ProtoMinor: req.ProtoMinor, + Header: http.Header{}, + StatusCode: http.StatusServiceUnavailable, + } + + host := req.Host + if _, _, err := net.SplitHostPort(host); err != nil { + host = net.JoinHostPort(strings.Trim(host, "[]"), "80") + } + + if bp := h.options.Bypass; bp != nil && + bp.Contains(ctx, "tcp", host, bypass.WithPathOption(req.RequestURI)) { + log.Debugf("bypass: %s %s", host, req.RequestURI) + res.StatusCode = http.StatusForbidden + ro.HTTP.StatusCode = res.StatusCode + res.Write(rw) + err = xbypass.ErrBypass + return + } + + target := &chain.Node{ + Addr: host, + } + if h.hop != nil { + target = h.hop.Select(ctx, + hop.HostSelectOption(host), + hop.ProtocolSelectOption(sniffing.ProtoHTTP), + hop.PathSelectOption(req.URL.Path), + ) + } + if target == nil { + log.Warnf("node for %s not found", host) + res.StatusCode = http.StatusBadGateway + ro.HTTP.StatusCode = res.StatusCode + res.Write(rw) + err = errors.New("node not available") + return + } + + ro.Host = target.Addr + + log = log.WithFields(map[string]any{ + "host": req.Host, + "node": target.Name, + "dst": target.Addr, + }) + log.Debugf("find node for host %s -> %s(%s)", host, target.Name, target.Addr) + + var bodyRewrites []chain.HTTPBodyRewriteSettings + if httpSettings := target.Options().HTTP; httpSettings != nil { + if auther := httpSettings.Auther; auther != nil { + username, password, _ := req.BasicAuth() + id, ok := auther.Authenticate(ctx, username, password) + if !ok { + res.StatusCode = http.StatusUnauthorized + ro.HTTP.StatusCode = res.StatusCode + res.Header.Set("WWW-Authenticate", "Basic") + log.Warnf("node %s(%s) 401 unauthorized", target.Name, target.Addr) + res.Write(rw) + err = errors.New("unauthorized") + return + } + ctx = ctxvalue.ContextWithClientID(ctx, ctxvalue.ClientID(id)) + } + + if httpSettings.Host != "" { + req.Host = httpSettings.Host + } + for k, v := range httpSettings.Header { + req.Header.Set(k, v) + } + + for _, re := range httpSettings.RewriteURL { + if re.Pattern.MatchString(req.URL.Path) { + if s := re.Pattern.ReplaceAllString(req.URL.Path, re.Replacement); s != "" { + req.URL.Path = s + break + } + } + } + + bodyRewrites = httpSettings.RewriteBody + } + + var buf bytes.Buffer + cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", target.Addr) + ro.Route = buf.String() + if err != nil { + // TODO: the router itself may be failed due to the failed node in the router, + // the dead marker may be a wrong operation. + if marker := target.Marker(); marker != nil { + marker.Mark() + } + log.Warnf("connect to node %s(%s) failed: %v", target.Name, target.Addr, err) + res.Write(rw) + return + } + if marker := target.Marker(); marker != nil { + marker.Reset() + } + // TODO: re-use the connection + defer cc.Close() + + log.Debugf("connect to node %s(%s)", target.Name, target.Addr) + + if tlsSettings := target.Options().TLS; tlsSettings != nil { + cfg := &tls.Config{ + ServerName: tlsSettings.ServerName, + InsecureSkipVerify: !tlsSettings.Secure, + } + tls_util.SetTLSOptions(cfg, &config.TLSOptions{ + MinVersion: tlsSettings.Options.MinVersion, + MaxVersion: tlsSettings.Options.MaxVersion, + CipherSuites: tlsSettings.Options.CipherSuites, + ALPN: tlsSettings.Options.ALPN, + }) + cc = tls.Client(cc, cfg) + } + + remoteAddr, _ := net.ResolveTCPAddr("tcp", clientAddr) + localAddr, _ := net.ResolveTCPAddr("tcp", ro.LocalAddr) + cc = proxyproto.WrapClientConn(h.md.proxyProtocol, remoteAddr, localAddr, cc) + + 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 { + res.Write(rw) + return + } + + if reqBody != nil { + ro.HTTP.Request.Body = reqBody.Content() + ro.HTTP.Request.ContentLength = reqBody.Length() + } + + cc.SetReadDeadline(time.Now().Add(30 * time.Second)) + resp, err := http.ReadResponse(bufio.NewReader(cc), req) + if err != nil { + log.Errorf("read response: %v", err) + res.Write(rw) + return + } + defer resp.Body.Close() + cc.SetReadDeadline(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 + } + + if err = h.rewriteBody(resp, bodyRewrites...); err != nil { + log.Errorf("rewrite body: %v", err) + return + } + + 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(ctx context.Context, rw 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] + } + + host := clientHello.ServerName + if _, _, err := net.SplitHostPort(host); err != nil { + host = net.JoinHostPort(strings.Trim(host, "[]"), "0") + } + + var target *chain.Node + if host != "" { + target = &chain.Node{ + Addr: host, + } + } + if h.hop != nil { + target = h.hop.Select(ctx, + hop.HostSelectOption(host), + hop.ProtocolSelectOption(sniffing.ProtoTLS), + ) + } + if target == nil { + err := errors.New("target not available") + log.Error(err) + return err + } + + addr := target.Addr + if opts := target.Options(); opts != nil { + switch opts.Network { + case "unix": + ro.Network = opts.Network + default: + if _, _, err := net.SplitHostPort(addr); err != nil { + addr += ":0" + } + } + } + ro.Host = addr + + log = log.WithFields(map[string]any{ + "host": host, + "node": target.Name, + "dst": fmt.Sprintf("%s/%s", addr, ro.Network), + }) + + log.Debugf("%s >> %s", ro.RemoteAddr, addr) + + var routeBuf bytes.Buffer + cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &routeBuf), ro.Network, addr) + ro.Route = routeBuf.String() + if err != nil { + log.Error(err) + // TODO: the router itself may be failed due to the failed node in the router, + // the dead marker may be a wrong operation. + if marker := target.Marker(); marker != nil { + marker.Mark() + } + return err + } + defer cc.Close() + if marker := target.Marker(); marker != nil { + marker.Reset() + } + + if _, err := buf.WriteTo(cc); err != nil { + return err + } + + cc.SetReadDeadline(time.Now().Add(30 * time.Second)) + 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, addr) + netpkg.Transport(rw, cc) + log.WithFields(map[string]any{ + "duration": time.Since(t), + }).Infof("%s >-< %s", ro.RemoteAddr, addr) + + return err +} + +func (h *forwardHandler) rewriteBody(resp *http.Response, rewrites ...chain.HTTPBodyRewriteSettings) error { + if resp == nil || len(rewrites) == 0 || resp.ContentLength <= 0 { + return nil + } + + if encoding := resp.Header.Get("Content-Encoding"); encoding != "" { + return nil + } + + body, err := drainBody(resp.Body) + if err != nil || body == nil { + return err + } + + contentType, _, _ := strings.Cut(resp.Header.Get("Content-Type"), ";") + for _, rewrite := range rewrites { + rewriteType := rewrite.Type + if rewriteType == "" { + rewriteType = "text/html" + } + if rewriteType != "*" && !strings.Contains(rewriteType, contentType) { + continue + } + + body = rewrite.Pattern.ReplaceAll(body, rewrite.Replacement) + } + + resp.Body = io.NopCloser(bytes.NewReader(body)) + resp.ContentLength = int64(len(body)) + + return nil +} + +func drainBody(b io.ReadCloser) (body []byte, err error) { + if b == nil || b == http.NoBody { + // No copying needed. Preserve the magic sentinel meaning of NoBody. + return nil, nil + } + var buf bytes.Buffer + if _, err = buf.ReadFrom(b); err != nil { + return nil, err + } + if err = b.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/handler/http/handler.go b/handler/http/handler.go index f840f848..0fb12dee 100644 --- a/handler/http/handler.go +++ b/handler/http/handler.go @@ -32,6 +32,7 @@ import ( netpkg "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" rate_limiter "github.com/go-gost/x/limiter/rate" traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper" @@ -40,10 +41,6 @@ import ( "github.com/go-gost/x/registry" ) -const ( - defaultBodySize = 1024 * 1024 // 1MB -) - func init() { registry.HandlerRegistry().Register("http", NewHandler) } @@ -104,6 +101,7 @@ func (h *httpHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler Service: h.options.Service, RemoteAddr: conn.RemoteAddr().String(), LocalAddr: conn.LocalAddr().String(), + Proto: "http", Time: start, SID: string(ctxvalue.SidFromContext(ctx)), } @@ -116,12 +114,12 @@ func (h *httpHandler) 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) - ro.Record(ctx, h.recorder.Recorder) + if err != nil { + ro.Err = err.Error() + } + ro.Duration = time.Since(start) + if err := ro.Record(ctx, h.recorder.Recorder); err != nil { + log.Error("record: %v", err) } log.WithFields(map[string]any{ @@ -178,11 +176,11 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt } } - ro.Host = req.Host addr := req.Host if _, port, _ := net.SplitHostPort(addr); port == "" { addr = net.JoinHostPort(strings.Trim(addr, "[]"), "80") } + ro.Host = addr fields := map[string]any{ "dst": addr, @@ -248,7 +246,7 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt } if network == "udp" { - return h.handleUDP(ctx, conn, log) + return h.handleUDP(ctx, conn, ro, log) } if req.Method == "PRI" || @@ -306,8 +304,7 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt ro2 := &xrecorder.HandlerRecorderObject{} *ro2 = *ro ro.Time = time.Time{} - - return h.handleProxy(ctx, xio.NewReadWriteCloser(rw, rw, conn), cc, req, ro2, log) + return h.handleProxy(ctx, rw, cc, req, ro2, log) } resp.StatusCode = http.StatusOK @@ -322,6 +319,31 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt return err } + if h.md.sniffing { + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) + } + + br := bufio.NewReader(conn) + proto, _ := sniffing.Sniff(ctx, br) + ro.Proto = proto + + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Time{}) + } + + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, cc, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, cc, ro, log) + } + } + start := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), addr) netpkg.Transport(rw, cc) @@ -332,143 +354,8 @@ func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *htt return nil } -func (h *httpHandler) handleProxy(ctx context.Context, rw io.ReadWriteCloser, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) { - roundTrip := func(req *http.Request) error { - if req == nil { - return nil - } - - resp := &http.Response{ - ProtoMajor: req.ProtoMajor, - ProtoMinor: req.ProtoMinor, - Header: http.Header{}, - StatusCode: http.StatusServiceUnavailable, - } - - start := time.Now() - ro.Time = start - - ro.HTTP = &xrecorder.HTTPRecorderObject{ - Host: req.Host, - Proto: req.Proto, - Scheme: req.URL.Scheme, - Method: req.Method, - URI: req.RequestURI, - StatusCode: resp.StatusCode, - 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") - } - } - - req.Header.Del("Proxy-Authorization") - req.Header.Del("Proxy-Connection") - req.Header.Del("Gost-Target") - req.Header.Del("X-Gost-Target") - - 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 { - resp.Write(rw) - - ro.Duration = time.Since(start) - ro.Err = err.Error() - ro.Record(ctx, h.recorder.Recorder) - - return err - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - go func() { - var err error - var res *http.Response - var respBody *xhttp.Body - - defer func() { - ro.Duration = time.Since(start) - 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, h.recorder.Recorder) - }() - - res, err = http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - h.options.Logger.Errorf("read response: %v", err) - resp.Write(rw) - return - } - defer res.Body.Close() - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(res, false) - log.Trace(string(dump)) - } - - if res.Close { - defer rw.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 := h.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(rw); err != nil { - rw.Close() - log.Errorf("write response: %v", err) - } - }() - - return nil - } - - if err = roundTrip(req); err != nil { +func (h *httpHandler) handleProxy(ctx context.Context, rw io.ReadWriter, cc net.Conn, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { + if shouldClose, err := h.proxyRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { return err } @@ -486,12 +373,147 @@ func (h *httpHandler) handleProxy(ctx context.Context, rw io.ReadWriteCloser, cc log.Trace(string(dump)) } - if err = roundTrip(req); err != nil { + if shouldClose, err := h.proxyRoundTrip(ctx, rw, cc, req, ro, log); err != nil || shouldClose { return err } } } +func (h *httpHandler) proxyRoundTrip(ctx context.Context, rw io.ReadWriter, cc net.Conn, 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") + } + } + + req.Header.Del("Proxy-Authorization") + req.Header.Del("Proxy-Connection") + req.Header.Del("Gost-Target") + req.Header.Del("X-Gost-Target") + + 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 + } + } + + res := &http.Response{ + ProtoMajor: req.ProtoMajor, + ProtoMinor: req.ProtoMinor, + Header: http.Header{}, + StatusCode: http.StatusServiceUnavailable, + } + ro.HTTP.StatusCode = res.StatusCode + + if err = req.Write(cc); err != nil { + res.Write(rw) + return + } + + if reqBody != nil { + ro.HTTP.Request.Body = reqBody.Content() + ro.HTTP.Request.ContentLength = reqBody.Length() + } + + cc.SetReadDeadline(time.Now().Add(h.md.readTimeout)) + resp, err := http.ReadResponse(bufio.NewReader(cc), req) + if err != nil { + log.Errorf("read response: %v", err) + res.Write(rw) + return + } + defer resp.Body.Close() + cc.SetReadDeadline(time.Time{}) + + ro.HTTP.StatusCode = resp.StatusCode + ro.HTTP.Response.Header = resp.Header.Clone() + 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() + } + + return resp.Close, nil +} + func (h *httpHandler) decodeServerName(s string) (string, error) { b, err := base64.RawURLEncoding.DecodeString(s) if err != nil { diff --git a/handler/http/metadata.go b/handler/http/metadata.go index 2ea951f0..73d48721 100644 --- a/handler/http/metadata.go +++ b/handler/http/metadata.go @@ -15,6 +15,7 @@ const ( ) type metadata struct { + readTimeout time.Duration probeResistance *probeResistance enableUDP bool header http.Header @@ -22,9 +23,16 @@ type metadata struct { authBasicRealm string observePeriod time.Duration proxyAgent string + sniffing bool + sniffingTimeout time.Duration } func (h *httpHandler) parseMetadata(md mdata.Metadata) error { + h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + if h.md.readTimeout <= 0 { + h.md.readTimeout = 15 * time.Second + } + if m := mdutil.GetStringMapString(md, "http.header", "header"); len(m) > 0 { hd := http.Header{} for k, v := range m { @@ -53,6 +61,9 @@ func (h *httpHandler) parseMetadata(md mdata.Metadata) error { h.md.proxyAgent = defaultProxyAgent } + h.md.sniffing = mdutil.GetBool(md, "sniffing") + h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + return nil } diff --git a/handler/http/sniffing.go b/handler/http/sniffing.go new file mode 100644 index 00000000..fc2861c3 --- /dev/null +++ b/handler/http/sniffing.go @@ -0,0 +1,259 @@ +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/http/udp.go b/handler/http/udp.go index 5e6edf6c..8337da77 100644 --- a/handler/http/udp.go +++ b/handler/http/udp.go @@ -1,6 +1,7 @@ package http import ( + "bytes" "context" "errors" "net" @@ -9,11 +10,13 @@ import ( "time" "github.com/go-gost/core/logger" + ctxvalue "github.com/go-gost/x/ctx" "github.com/go-gost/x/internal/net/udp" "github.com/go-gost/x/internal/util/socks" + xrecorder "github.com/go-gost/x/recorder" ) -func (h *httpHandler) handleUDP(ctx context.Context, conn net.Conn, log logger.Logger) error { +func (h *httpHandler) handleUDP(ctx context.Context, conn net.Conn, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { log = log.WithFields(map[string]any{ "cmd": "udp", }) @@ -51,7 +54,9 @@ func (h *httpHandler) handleUDP(ctx context.Context, conn net.Conn, log logger.L } // obtain a udp connection - c, err := h.options.Router.Dial(ctx, "udp", "") // UDP association + var buf bytes.Buffer + c, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "udp", "") // UDP association + ro.Route = buf.String() if err != nil { log.Error(err) return err diff --git a/handler/http2/handler.go b/handler/http2/handler.go index 42002bff..7c57aa9a 100644 --- a/handler/http2/handler.go +++ b/handler/http2/handler.go @@ -171,14 +171,14 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req ro.ClientIP = clientIP.String() } - ro.Host = req.Host - addr := req.Host - if _, port, _ := net.SplitHostPort(addr); port == "" { - addr = net.JoinHostPort(strings.Trim(addr, "[]"), "80") + host := req.Host + if _, port, _ := net.SplitHostPort(host); port == "" { + host = net.JoinHostPort(strings.Trim(host, "[]"), "80") } + ro.Host = host fields := map[string]any{ - "dst": addr, + "dst": host, } if u, _, _ := h.basicProxyAuth(req.Header.Get("Proxy-Authorization")); u != "" { fields["user"] = u @@ -190,7 +190,7 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req dump, _ := httputil.DumpRequest(req, false) log.Trace(string(dump)) } - log.Debugf("%s >> %s", req.RemoteAddr, addr) + log.Debugf("%s >> %s", req.RemoteAddr, host) for k := range h.md.header { w.Header().Set(k, h.md.header.Get(k)) @@ -221,14 +221,14 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req clientID, ok := h.authenticate(ctx, w, req, resp, log) if !ok { - return errors.New("authenication failed") + return errors.New("authentication failed") } ctx = ctxvalue.ContextWithClientID(ctx, ctxvalue.ClientID(clientID)) - if h.options.Bypass != nil && h.options.Bypass.Contains(ctx, "tcp", addr) { + if h.options.Bypass != nil && h.options.Bypass.Contains(ctx, "tcp", host) { resp.StatusCode = http.StatusForbidden w.WriteHeader(resp.StatusCode) - log.Debug("bypass: ", addr) + log.Debug("bypass: ", host) return xbypass.ErrBypass } @@ -240,11 +240,11 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req switch h.md.hash { case "host": - ctx = ctxvalue.ContextWithHash(ctx, &ctxvalue.Hash{Source: addr}) + ctx = ctxvalue.ContextWithHash(ctx, &ctxvalue.Hash{Source: host}) } var buf bytes.Buffer - cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", addr) + cc, err := h.options.Router.Dial(ctxvalue.ContextWithBuffer(ctx, &buf), "tcp", host) ro.Route = buf.String() if err != nil { log.Error(err) @@ -274,11 +274,11 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req defer conn.Close() start := time.Now() - log.Infof("%s <-> %s", conn.RemoteAddr(), addr) + log.Infof("%s <-> %s", conn.RemoteAddr(), host) netpkg.Transport(conn, cc) log.WithFields(map[string]any{ "duration": time.Since(start), - }).Infof("%s >-< %s", conn.RemoteAddr(), addr) + }).Infof("%s >-< %s", conn.RemoteAddr(), host) return nil } @@ -290,7 +290,7 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req limiter.ScopeOption(limiter.ScopeClient), limiter.ServiceOption(h.options.Service), limiter.NetworkOption("tcp"), - limiter.AddrOption(addr), + limiter.AddrOption(host), limiter.ClientOption(clientID), limiter.SrcOption(req.RemoteAddr), ) @@ -303,11 +303,11 @@ func (h *http2Handler) roundTrip(ctx context.Context, w http.ResponseWriter, req } start := time.Now() - log.Infof("%s <-> %s", req.RemoteAddr, addr) + log.Infof("%s <-> %s", req.RemoteAddr, host) netpkg.Transport(rw, cc) log.WithFields(map[string]any{ "duration": time.Since(start), - }).Infof("%s >-< %s", req.RemoteAddr, addr) + }).Infof("%s >-< %s", req.RemoteAddr, host) return nil } diff --git a/handler/redirect/tcp/handler.go b/handler/redirect/tcp/handler.go index 7904b0d1..eb16b542 100644 --- a/handler/redirect/tcp/handler.go +++ b/handler/redirect/tcp/handler.go @@ -4,30 +4,19 @@ import ( "bufio" "bytes" "context" - "crypto/tls" - "encoding/binary" - "encoding/hex" - "errors" "fmt" "io" "net" - "net/http" - "net/http/httputil" - "strings" "time" - "github.com/go-gost/core/bypass" "github.com/go-gost/core/handler" - "github.com/go-gost/core/logger" md "github.com/go-gost/core/metadata" "github.com/go-gost/core/recorder" - 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" + "github.com/go-gost/x/internal/util/sniffing" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" @@ -98,14 +87,12 @@ func (h *redirectHandler) Handle(ctx context.Context, conn net.Conn, opts ...han 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{ @@ -135,31 +122,29 @@ func (h *redirectHandler) Handle(ctx context.Context, conn net.Conn, opts ...han "dst": fmt.Sprintf("%s/%s", dstAddr, dstAddr.Network()), }) - var rw io.ReadWriteCloser = conn + var rw io.ReadWriter = conn if h.md.sniffing { if h.md.sniffingTimeout > 0 { conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) } - // try to sniff TLS traffic - var hdr [dissector.RecordHeaderLen]byte - n, err := io.ReadFull(rw, hdr[:]) + + br := bufio.NewReader(conn) + proto, _ := sniffing.Sniff(ctx, br) + ro.Proto = proto + if h.md.sniffingTimeout > 0 { conn.SetReadDeadline(time.Time{}) } - rw = xio.NewReadWriteCloser(io.MultiReader(bytes.NewReader(hdr[:n]), rw), rw, rw) - tlsVersion := binary.BigEndian.Uint16(hdr[1:3]) - if err == nil && - hdr[0] == dissector.Handshake && - (tlsVersion >= tls.VersionTLS10 && tlsVersion <= tls.VersionTLS13) { - return h.handleHTTPS(ctx, rw, conn.RemoteAddr(), dstAddr, ro, log) - } - // try to sniff HTTP traffic - if isHTTP(string(hdr[:])) { + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: ro2 := &xrecorder.HandlerRecorderObject{} *ro2 = *ro ro.Time = time.Time{} - return h.handleHTTP(ctx, rw, conn.RemoteAddr(), dstAddr, ro2, log) + return h.handleHTTP(ctx, rw, dstAddr, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, dstAddr, ro, log) } } @@ -190,367 +175,6 @@ func (h *redirectHandler) Handle(ctx context.Context, conn net.Conn, opts ...han return nil } -func (h *redirectHandler) handleHTTP(ctx context.Context, rw io.ReadWriteCloser, raddr, 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)) - } - - ro.Host = req.Host - host := req.Host - if _, _, err := net.SplitHostPort(host); err != nil { - host = net.JoinHostPort(strings.Trim(host, "[]"), "80") - } - 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 req.Header.Get("Upgrade") == "websocket" { - return h.handleWebsocket(ctx, raddr, rw, cc, req, ro, log) - } - - if err := h.httpRoundTrip(ctx, raddr, rw, cc, req, ro, log); err != nil { - 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 err = h.httpRoundTrip(ctx, raddr, rw, cc, req, ro, log); err != nil { - return err - } - } -} - -func (h *redirectHandler) handleWebsocket(ctx context.Context, raddr net.Addr, rw io.ReadWriteCloser, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) { - start := time.Now() - ro.Time = start - - log.Infof("%s <-> %s", raddr, 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", raddr, 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(), - }, - } - - if err = req.Write(cc); err != nil { - return - } - - res, err := http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - return - } - defer res.Body.Close() - - ro.HTTP.StatusCode = res.StatusCode - ro.HTTP.Response.Header = res.Header - ro.HTTP.Response.ContentLength = res.ContentLength - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(res, false) - log.Trace(string(dump)) - } - - if err = res.Write(rw); err != nil { - return - } - - netpkg.Transport(rw, cc) - - return -} - -func (h *redirectHandler) httpRoundTrip(ctx context.Context, raddr net.Addr, rw io.ReadWriteCloser, cc io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) { - if req == nil { - return nil - } - - start := time.Now() - ro.Time = start - - log.Infof("%s <-> %s", raddr, req.Host) - defer func() { - if err != nil { - ro.Duration = time.Since(start) - 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", raddr, 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 err - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - go func() { - var err error - var res *http.Response - var respBody *xhttp.Body - - defer func() { - ro.Duration = time.Since(start) - 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, h.recorder.Recorder) - }() - - res, err = http.ReadResponse(bufio.NewReader(cc), req) - if err != nil { - rw.Close() - log.Errorf("read response: %v", err) - return - } - defer res.Body.Close() - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(res, false) - log.Trace(string(dump)) - } - - if res.Close { - defer rw.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 := h.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(rw); err != nil { - rw.Close() - log.Errorf("write response: %v", err) - } - }() - - return nil -} - -func (h *redirectHandler) handleHTTPS(ctx context.Context, rw io.ReadWriter, raddr, 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 io.ReadWriteCloser - - 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 - } - - serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) - 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", raddr, host) - netpkg.Transport(rw, cc) - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", raddr, host) - - return err -} - func (h *redirectHandler) checkRateLimit(addr net.Addr) bool { if h.options.RateLimiter == nil { return true @@ -562,15 +186,3 @@ func (h *redirectHandler) checkRateLimit(addr net.Addr) bool { return true } - -func isHTTP(s string) bool { - return strings.HasPrefix(http.MethodGet, s[:3]) || - strings.HasPrefix(http.MethodPost, s[:4]) || - strings.HasPrefix(http.MethodPut, s[:3]) || - strings.HasPrefix(http.MethodDelete, s) || - strings.HasPrefix(http.MethodOptions, s) || - strings.HasPrefix(http.MethodPatch, s) || - strings.HasPrefix(http.MethodHead, s[:4]) || - strings.HasPrefix(http.MethodConnect, s) || - strings.HasPrefix(http.MethodTrace, s) -} diff --git a/handler/redirect/tcp/metadata.go b/handler/redirect/tcp/metadata.go index 3f251462..00b6aa33 100644 --- a/handler/redirect/tcp/metadata.go +++ b/handler/redirect/tcp/metadata.go @@ -8,6 +8,7 @@ import ( ) type metadata struct { + readTimeout time.Duration tproxy bool sniffing bool sniffingTimeout time.Duration @@ -15,6 +16,10 @@ type metadata struct { } func (h *redirectHandler) parseMetadata(md mdata.Metadata) (err error) { + h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + if h.md.readTimeout <= 0 { + 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") diff --git a/handler/redirect/tcp/sniffing.go b/handler/redirect/tcp/sniffing.go new file mode 100644 index 00000000..76a0cb7b --- /dev/null +++ b/handler/redirect/tcp/sniffing.go @@ -0,0 +1,318 @@ +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/relay/connect.go b/handler/relay/connect.go index 643e709a..42411df6 100644 --- a/handler/relay/connect.go +++ b/handler/relay/connect.go @@ -1,6 +1,7 @@ package relay import ( + "bufio" "bytes" "context" "errors" @@ -17,6 +18,8 @@ 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" xrecorder "github.com/go-gost/x/recorder" @@ -62,7 +65,6 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network } var cc io.ReadWriteCloser - switch network { case "unix": cc, err = (&net.Dialer{}).DialContext(ctx, "unix", address) @@ -132,6 +134,31 @@ func (h *relayHandler) handleConnect(ctx context.Context, conn net.Conn, network rw = stats_wrapper.WrapReadWriter(rw, pstats) } + if h.md.sniffing { + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) + } + + br := bufio.NewReader(conn) + proto, _ := sniffing.Sniff(ctx, br) + ro.Proto = proto + + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Time{}) + } + + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, cc, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, cc, ro, log) + } + } + t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), address) xnet.Transport(rw, cc) diff --git a/handler/relay/metadata.go b/handler/relay/metadata.go index bc827126..33af1507 100644 --- a/handler/relay/metadata.go +++ b/handler/relay/metadata.go @@ -10,17 +10,23 @@ import ( ) 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 } func (h *relayHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + if h.md.readTimeout <= 0 { + h.md.readTimeout = 15 * time.Second + } + h.md.enableBind = mdutil.GetBool(md, "bind") h.md.noDelay = mdutil.GetBool(md, "nodelay") @@ -44,5 +50,8 @@ func (h *relayHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.observePeriod = mdutil.GetDuration(md, "observePeriod") + h.md.sniffing = mdutil.GetBool(md, "sniffing") + h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + return } diff --git a/handler/relay/sniffing.go b/handler/relay/sniffing.go new file mode 100644 index 00000000..c7c0ccc2 --- /dev/null +++ b/handler/relay/sniffing.go @@ -0,0 +1,259 @@ +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 ae3e728a..ab49af6b 100644 --- a/handler/sni/handler.go +++ b/handler/sni/handler.go @@ -2,33 +2,17 @@ package sni import ( "bufio" - "bytes" "context" - "crypto/tls" - "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/handler" - "github.com/go-gost/core/logger" md "github.com/go-gost/core/metadata" "github.com/go-gost/core/recorder" - 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" + "github.com/go-gost/x/internal/util/sniffing" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" @@ -99,11 +83,15 @@ func (h *sniHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler. log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) defer func() { - if err != nil { - ro.Err = err.Error() + 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) + } } - ro.Duration = time.Since(start) - ro.Record(ctx, h.recorder.Recorder) log.WithFields(map[string]any{ "duration": time.Since(start), @@ -114,288 +102,22 @@ func (h *sniHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler. return rate_limiter.ErrRateLimit } - var hdr [dissector.RecordHeaderLen]byte - if _, err := io.ReadFull(conn, hdr[:]); err != nil { - log.Error(err) - return err + br := bufio.NewReader(conn) + proto, _ := sniffing.Sniff(ctx, br) + ro.Proto = proto + + rw := xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, ro, log) + default: + return errors.New("unknown traffic") } - - rw := xio.NewReadWriter(io.MultiReader(bytes.NewReader(hdr[:]), conn), conn) - - tlsVersion := binary.BigEndian.Uint16(hdr[1:3]) - if hdr[0] == dissector.Handshake && - (tlsVersion >= tls.VersionTLS10 && tlsVersion <= tls.VersionTLS13) { - return h.handleHTTPS(ctx, rw, conn.RemoteAddr(), ro, log) - } - return h.handleHTTP(ctx, rw, conn.RemoteAddr(), ro, log) -} - -func (h *sniHandler) handleHTTP(ctx context.Context, rw io.ReadWriter, raddr net.Addr, ro *xrecorder.HandlerRecorderObject, log logger.Logger) error { - req, err := http.ReadRequest(bufio.NewReader(rw)) - if err != nil { - return err - } - - 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, - }, - } - if clientIP := xhttp.GetClientIP(req); clientIP != nil { - ro.ClientIP = clientIP.String() - } - - 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 = req.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 - } - - switch h.md.hash { - case "host": - ctx = ctxvalue.ContextWithHash(ctx, &ctxvalue.Hash{Source: host}) - } - - 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() - - t := time.Now() - log.Infof("%s <-> %s", raddr, host) - defer func() { - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", raddr, host) - }() - - 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 { - log.Error(err) - return err - } - - if reqBody != nil { - ro.HTTP.Request.Body = reqBody.Content() - ro.HTTP.Request.ContentLength = reqBody.Length() - } - - br := bufio.NewReader(cc) - resp, err := http.ReadResponse(br, req) - if err != nil { - log.Error(err) - return err - } - defer resp.Body.Close() - - ro.HTTP.StatusCode = resp.StatusCode - ro.HTTP.Response.ContentLength = resp.ContentLength - ro.HTTP.Response.Header = resp.Header - - if log.IsLevelEnabled(logger.TraceLevel) { - dump, _ := httputil.DumpResponse(resp, false) - log.Trace(string(dump)) - } - - 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.Error(err) - return err - } - - if respBody != nil { - ro.HTTP.Response.Body = respBody.Content() - ro.HTTP.Response.ContentLength = respBody.Length() - } - - netpkg.Transport(rw, xio.NewReadWriter(br, cc)) - - return nil -} - -func (h *sniHandler) handleHTTPS(ctx context.Context, rw io.ReadWriter, raddr net.Addr, 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", raddr, 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 - } - - serverHello, err := dissector.ParseServerHello(io.TeeReader(cc, buf)) - 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", raddr, host) - netpkg.Transport(rw, cc) - log.WithFields(map[string]any{ - "duration": time.Since(t), - }).Infof("%s >-< %s", raddr, 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 } func (h *sniHandler) checkRateLimit(addr net.Addr) bool { diff --git a/handler/sni/metadata.go b/handler/sni/metadata.go index 56262630..7abb463d 100644 --- a/handler/sni/metadata.go +++ b/handler/sni/metadata.go @@ -13,12 +13,10 @@ type metadata struct { } func (h *sniHandler) parseMetadata(md mdata.Metadata) (err error) { - const ( - readTimeout = "readTimeout" - hash = "hash" - ) - - h.md.readTimeout = mdutil.GetDuration(md, readTimeout) - h.md.hash = mdutil.GetString(md, hash) + h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + if h.md.readTimeout <= 0 { + h.md.readTimeout = 15 * time.Second + } + h.md.hash = mdutil.GetString(md, "hash") return } diff --git a/handler/sni/sniffing.go b/handler/sni/sniffing.go new file mode 100644 index 00000000..ddb19132 --- /dev/null +++ b/handler/sni/sniffing.go @@ -0,0 +1,355 @@ +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 88e81427..bb80ccff 100644 --- a/handler/socks/v4/handler.go +++ b/handler/socks/v4/handler.go @@ -1,6 +1,7 @@ package v4 import ( + "bufio" "bytes" "context" "errors" @@ -16,8 +17,10 @@ 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" 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" rate_limiter "github.com/go-gost/x/limiter/rate" traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper" @@ -195,7 +198,6 @@ func (h *socks4Handler) handleConnect(ctx context.Context, conn net.Conn, req *g resp.Write(conn) return err } - defer cc.Close() resp := gosocks4.NewReply(gosocks4.Granted, nil) @@ -225,6 +227,31 @@ func (h *socks4Handler) handleConnect(ctx context.Context, conn net.Conn, req *g rw = stats_wrapper.WrapReadWriter(rw, pstats) } + if h.md.sniffing { + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) + } + + br := bufio.NewReader(conn) + proto, _ := sniffing.Sniff(ctx, br) + ro.Proto = proto + + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Time{}) + } + + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, cc, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, cc, ro, log) + } + } + t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), addr) netpkg.Transport(rw, cc) diff --git a/handler/socks/v4/metadata.go b/handler/socks/v4/metadata.go index 318221ac..79619663 100644 --- a/handler/socks/v4/metadata.go +++ b/handler/socks/v4/metadata.go @@ -8,14 +8,24 @@ import ( ) type metadata struct { - readTimeout time.Duration - hash string - observePeriod time.Duration + readTimeout time.Duration + hash string + observePeriod time.Duration + sniffing bool + sniffingTimeout time.Duration } func (h *socks4Handler) parseMetadata(md mdata.Metadata) (err error) { h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + if h.md.readTimeout <= 0 { + h.md.readTimeout = 15 * time.Second + } + h.md.hash = mdutil.GetString(md, "hash") h.md.observePeriod = mdutil.GetDuration(md, "observePeriod") + + h.md.sniffing = mdutil.GetBool(md, "sniffing") + h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + return } diff --git a/handler/socks/v4/sniffing.go b/handler/socks/v4/sniffing.go new file mode 100644 index 00000000..4c47f87d --- /dev/null +++ b/handler/socks/v4/sniffing.go @@ -0,0 +1,259 @@ +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/connect.go b/handler/socks/v5/connect.go index c40fe282..5aa48a06 100644 --- a/handler/socks/v5/connect.go +++ b/handler/socks/v5/connect.go @@ -1,6 +1,7 @@ package v5 import ( + "bufio" "bytes" "context" "fmt" @@ -12,7 +13,9 @@ 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" + "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" xrecorder "github.com/go-gost/x/recorder" @@ -46,7 +49,6 @@ func (h *socks5Handler) handleConnect(ctx context.Context, conn net.Conn, networ resp.Write(conn) return err } - defer cc.Close() resp := gosocks5.NewReply(gosocks5.Succeeded, nil) @@ -76,6 +78,31 @@ func (h *socks5Handler) handleConnect(ctx context.Context, conn net.Conn, networ rw = stats_wrapper.WrapReadWriter(rw, pstats) } + if h.md.sniffing { + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout)) + } + + br := bufio.NewReader(conn) + proto, _ := sniffing.Sniff(ctx, br) + ro.Proto = proto + + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Time{}) + } + + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, cc, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, cc, ro, log) + } + } + t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), address) netpkg.Transport(rw, cc) diff --git a/handler/socks/v5/handler.go b/handler/socks/v5/handler.go index fe82d4b3..a7c730f4 100644 --- a/handler/socks/v5/handler.go +++ b/handler/socks/v5/handler.go @@ -122,9 +122,7 @@ func (h *socks5Handler) Handle(ctx context.Context, conn net.Conn, opts ...handl return rate_limiter.ErrRateLimit } - if h.md.readTimeout > 0 { - conn.SetReadDeadline(time.Now().Add(h.md.readTimeout)) - } + conn.SetReadDeadline(time.Now().Add(h.md.readTimeout)) sc := gosocks5.ServerConn(conn, h.selector) req, err := gosocks5.ReadRequest(sc) diff --git a/handler/socks/v5/metadata.go b/handler/socks/v5/metadata.go index 0b23c0da..fe016ee3 100644 --- a/handler/socks/v5/metadata.go +++ b/handler/socks/v5/metadata.go @@ -19,10 +19,16 @@ type metadata struct { hash string muxCfg *mux.Config observePeriod time.Duration + sniffing bool + sniffingTimeout time.Duration } func (h *socks5Handler) parseMetadata(md mdata.Metadata) (err error) { h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + if h.md.readTimeout <= 0 { + h.md.readTimeout = 15 * time.Second + } + h.md.noTLS = mdutil.GetBool(md, "notls") h.md.enableBind = mdutil.GetBool(md, "bind") h.md.enableUDP = mdutil.GetBool(md, "udp") @@ -48,5 +54,8 @@ func (h *socks5Handler) parseMetadata(md mdata.Metadata) (err error) { h.md.observePeriod = mdutil.GetDuration(md, "observePeriod") + h.md.sniffing = mdutil.GetBool(md, "sniffing") + h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + return nil } diff --git a/handler/socks/v5/sniffing.go b/handler/socks/v5/sniffing.go new file mode 100644 index 00000000..e6d37644 --- /dev/null +++ b/handler/socks/v5/sniffing.go @@ -0,0 +1,259 @@ +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 e4b190f8..b077e006 100644 --- a/handler/ss/handler.go +++ b/handler/ss/handler.go @@ -1,6 +1,7 @@ package ss import ( + "bufio" "bytes" "context" "io" @@ -12,7 +13,9 @@ 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" + "github.com/go-gost/x/internal/util/sniffing" "github.com/go-gost/x/internal/util/ss" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" @@ -109,9 +112,7 @@ func (h *ssHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.H conn = ss.ShadowConn(h.cipher.StreamConn(conn), nil) } - if h.md.readTimeout > 0 { - conn.SetReadDeadline(time.Now().Add(h.md.readTimeout)) - } + conn.SetReadDeadline(time.Now().Add(h.md.readTimeout)) addr := &gosocks5.Addr{} if _, err := addr.ReadFrom(conn); err != nil { @@ -121,6 +122,8 @@ func (h *ssHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.H } ro.Host = addr.String() + conn.SetReadDeadline(time.Time{}) + log = log.WithFields(map[string]any{ "dst": addr.String(), }) @@ -145,9 +148,35 @@ 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)) + } + + br := bufio.NewReader(conn) + proto, _ := sniffing.Sniff(ctx, br) + ro.Proto = proto + + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Time{}) + } + + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, cc, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, cc, ro, log) + } + } + t := time.Now() log.Infof("%s <-> %s", conn.RemoteAddr(), addr) - netpkg.Transport(conn, cc) + netpkg.Transport(rw, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", conn.RemoteAddr(), addr) diff --git a/handler/ss/metadata.go b/handler/ss/metadata.go index 06bda199..f239345e 100644 --- a/handler/ss/metadata.go +++ b/handler/ss/metadata.go @@ -8,21 +8,24 @@ import ( ) type metadata struct { - key string - readTimeout time.Duration - hash string + key string + hash string + readTimeout time.Duration + sniffing bool + sniffingTimeout time.Duration } func (h *ssHandler) parseMetadata(md mdata.Metadata) (err error) { - const ( - key = "key" - readTimeout = "readTimeout" - hash = "hash" - ) + h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + if h.md.readTimeout <= 0 { + h.md.readTimeout = 15 * time.Second + } - h.md.key = mdutil.GetString(md, key) - h.md.readTimeout = mdutil.GetDuration(md, readTimeout) - h.md.hash = mdutil.GetString(md, hash) + h.md.key = mdutil.GetString(md, "key") + h.md.hash = mdutil.GetString(md, "hash") + + h.md.sniffing = mdutil.GetBool(md, "sniffing") + h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") return } diff --git a/handler/ss/sniffing.go b/handler/ss/sniffing.go new file mode 100644 index 00000000..4174e675 --- /dev/null +++ b/handler/ss/sniffing.go @@ -0,0 +1,259 @@ +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 98c18f04..10d5027a 100644 --- a/handler/sshd/handler.go +++ b/handler/sshd/handler.go @@ -1,11 +1,13 @@ package ssh import ( + "bufio" "bytes" "context" "encoding/binary" "errors" "fmt" + "io" "net" "strconv" "time" @@ -16,7 +18,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" + "github.com/go-gost/x/internal/util/sniffing" sshd_util "github.com/go-gost/x/internal/util/sshd" rate_limiter "github.com/go-gost/x/limiter/rate" xrecorder "github.com/go-gost/x/recorder" @@ -88,13 +92,11 @@ func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...hand 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), @@ -141,9 +143,35 @@ 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)) + } + + br := bufio.NewReader(conn) + proto, _ := sniffing.Sniff(ctx, br) + ro.Proto = proto + + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Time{}) + } + + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, cc, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, cc, ro, log) + } + } + t := time.Now() log.Infof("%s <-> %s", cc.LocalAddr(), targetAddr) - netpkg.Transport(conn, cc) + netpkg.Transport(rw, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", cc.LocalAddr(), targetAddr) diff --git a/handler/sshd/metadata.go b/handler/sshd/metadata.go index 5042352f..3cc567dc 100644 --- a/handler/sshd/metadata.go +++ b/handler/sshd/metadata.go @@ -1,12 +1,26 @@ package ssh import ( + "time" + mdata "github.com/go-gost/core/metadata" + mdutil "github.com/go-gost/core/metadata/util" ) type metadata struct { + readTimeout time.Duration + sniffing bool + sniffingTimeout time.Duration } func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) { + h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + if h.md.readTimeout <= 0 { + h.md.readTimeout = 15 * time.Second + } + + h.md.sniffing = mdutil.GetBool(md, "sniffing") + h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + return } diff --git a/handler/sshd/sniffing.go b/handler/sshd/sniffing.go new file mode 100644 index 00000000..3736c984 --- /dev/null +++ b/handler/sshd/sniffing.go @@ -0,0 +1,259 @@ +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 ead436de..dbd8d4de 100644 --- a/handler/tunnel/entrypoint.go +++ b/handler/tunnel/entrypoint.go @@ -30,10 +30,6 @@ import ( xrecorder "github.com/go-gost/x/recorder" ) -const ( - defaultBodySize = 1024 * 1024 // 1MB -) - type entrypoint struct { node string service string @@ -70,6 +66,18 @@ func (ep *entrypoint) handle(ctx context.Context, conn net.Conn) error { return ep.handleConnect(ctx, xnet.NewBufferReaderConn(conn, br), log) } + ro := &xrecorder.HandlerRecorderObject{ + Node: ep.node, + Service: ep.service, + RemoteAddr: conn.RemoteAddr().String(), + LocalAddr: conn.LocalAddr().String(), + Network: "tcp", + Time: start, + } + 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{ diff --git a/handler/tunnel/sniffing.go b/handler/tunnel/sniffing.go new file mode 100644 index 00000000..dc9dced7 --- /dev/null +++ b/handler/tunnel/sniffing.go @@ -0,0 +1,275 @@ +package tunnel + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "strings" + "time" + + "github.com/go-gost/core/logger" + "github.com/go-gost/relay" + ctxvalue "github.com/go-gost/x/ctx" + xnet "github.com/go-gost/x/internal/net" + xhttp "github.com/go-gost/x/internal/net/http" + xrecorder "github.com/go-gost/x/recorder" +) + +const ( + defaultBodySize = 1024 * 1024 // 1MB +) + +func (h *entrypoint) 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)) + } + + shouldClose, err := h.httpRoundTrip(ctx, rw, 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, req, ro, log); err != nil || shouldClose { + return err + } + } +} + +func (h *entrypoint) httpRoundTrip(ctx context.Context, rw io.ReadWriter, req *http.Request, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (close bool, err error) { + close = true + + 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(), + }, + } + if clientIP := xhttp.GetClientIP(req); clientIP != nil { + ro.ClientIP = clientIP.String() + } + clientAddr := ro.RemoteAddr + if ro.ClientIP != "" { + if _, port, _ := net.SplitHostPort(ro.RemoteAddr); port != "" { + clientAddr = net.JoinHostPort(ro.ClientIP, port) + } + ctx = ctxvalue.ContextWithClientAddr(ctx, ctxvalue.ClientAddr(clientAddr)) + } + + // 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") + } + } + + res := &http.Response{ + ProtoMajor: req.ProtoMajor, + ProtoMinor: req.ProtoMinor, + Header: http.Header{}, + StatusCode: http.StatusServiceUnavailable, + } + + host := req.Host + if _, _, err := net.SplitHostPort(host); err != nil { + host = net.JoinHostPort(strings.Trim(host, "[]"), "80") + } + + var tunnelID relay.TunnelID + if h.ingress != nil { + if rule := h.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) + res.StatusCode = http.StatusBadGateway + ro.HTTP.StatusCode = res.StatusCode + res.Write(rw) + return + } + + 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) + res.StatusCode = http.StatusBadGateway + ro.HTTP.StatusCode = res.StatusCode + res.Write(rw) + return + } + + log = log.WithFields(map[string]any{ + "host": req.Host, + "tunnel": tunnelID.String(), + "clientIP": ro.ClientIP, + }) + + d := &Dialer{ + node: h.node, + pool: h.pool, + sd: h.sd, + retry: 3, + timeout: 15 * time.Second, + log: log, + } + cc, node, cid, err := d.Dial(ctx, "tcp", tunnelID.String()) + if err != nil { + log.Error(err) + res.Write(rw) + return + } + // TODO: re-use connection + defer cc.Close() + log.Debugf("new connection to tunnel: %s, connector: %s", tunnelID, cid) + + if node == h.node { + var features []relay.Feature + af := &relay.AddrFeature{} + af.ParseFrom(clientAddr) + features = append(features, af) // src address + + af = &relay.AddrFeature{} + af.ParseFrom(host) + features = append(features, af) // dst address + + if _, err = (&relay.Response{ + Version: relay.Version1, + Status: relay.StatusOK, + Features: features, + }).WriteTo(cc); err != nil { + log.Error(err) + res.Write(rw) + return + } + } + + 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 { + log.Errorf("send request: %v", err) + res.Write(rw) + return + } + + if reqBody != nil { + ro.HTTP.Request.Body = reqBody.Content() + ro.HTTP.Request.ContentLength = reqBody.Length() + } + + cc.SetReadDeadline(time.Now().Add(15 * time.Second)) + resp, err := http.ReadResponse(bufio.NewReader(cc), req) + if err != nil { + log.Errorf("read response: %v", err) + res.Write(rw) + return + } + defer resp.Body.Close() + cc.SetReadDeadline(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" { + xnet.Transport(rw, cc) + } + + return resp.Close, nil +} diff --git a/handler/unix/handler.go b/handler/unix/handler.go index 7a165cba..0dc9fa35 100644 --- a/handler/unix/handler.go +++ b/handler/unix/handler.go @@ -1,6 +1,7 @@ package unix import ( + "bufio" "context" "errors" "io" @@ -14,7 +15,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" xrecorder "github.com/go-gost/x/recorder" "github.com/go-gost/x/registry" ) @@ -108,7 +111,7 @@ func (h *unixHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler }) ro.Host = target.Addr - return h.forwardUnix(ctx, conn, target, log) + return h.forwardUnix(ctx, conn, target, ro, log) } cc, err := h.options.Router.Dial(ctx, "tcp", "@") @@ -118,9 +121,35 @@ 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)) + } + + br := bufio.NewReader(conn) + proto, _ := sniffing.Sniff(ctx, br) + ro.Proto = proto + + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Time{}) + } + + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, cc, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, cc, ro, log) + } + } + t := time.Now() log.Infof("%s <-> %s", conn.LocalAddr(), "@") - xnet.Transport(conn, cc) + xnet.Transport(rw, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", conn.LocalAddr(), "@") @@ -128,7 +157,7 @@ func (h *unixHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler return nil } -func (h *unixHandler) forwardUnix(ctx context.Context, conn net.Conn, target *chain.Node, log logger.Logger) (err error) { +func (h *unixHandler) forwardUnix(ctx context.Context, conn net.Conn, target *chain.Node, ro *xrecorder.HandlerRecorderObject, log logger.Logger) (err error) { log.Debugf("%s >> %s", conn.LocalAddr(), target.Addr) var cc io.ReadWriteCloser @@ -143,9 +172,35 @@ func (h *unixHandler) forwardUnix(ctx context.Context, conn net.Conn, target *ch } 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)) + } + + br := bufio.NewReader(conn) + proto, _ := sniffing.Sniff(ctx, br) + ro.Proto = proto + + if h.md.sniffingTimeout > 0 { + conn.SetReadDeadline(time.Time{}) + } + + rw = xio.NewReadWriter(br, conn) + switch proto { + case sniffing.ProtoHTTP: + ro2 := &xrecorder.HandlerRecorderObject{} + *ro2 = *ro + ro.Time = time.Time{} + return h.handleHTTP(ctx, rw, cc, ro2, log) + case sniffing.ProtoTLS: + return h.handleTLS(ctx, rw, cc, ro, log) + } + } + t := time.Now() log.Infof("%s <-> %s", conn.LocalAddr(), target.Addr) - xnet.Transport(conn, cc) + xnet.Transport(rw, cc) log.WithFields(map[string]any{ "duration": time.Since(t), }).Infof("%s >-< %s", conn.LocalAddr(), target.Addr) diff --git a/handler/unix/metadata.go b/handler/unix/metadata.go index 776c8021..a098ace7 100644 --- a/handler/unix/metadata.go +++ b/handler/unix/metadata.go @@ -1,12 +1,26 @@ package unix import ( + "time" + mdata "github.com/go-gost/core/metadata" + mdutil "github.com/go-gost/core/metadata/util" ) type metadata struct { + readTimeout time.Duration + sniffing bool + sniffingTimeout time.Duration } func (h *unixHandler) parseMetadata(md mdata.Metadata) (err error) { + h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + if h.md.readTimeout <= 0 { + h.md.readTimeout = 15 * time.Second + } + + h.md.sniffing = mdutil.GetBool(md, "sniffing") + h.md.sniffingTimeout = mdutil.GetDuration(md, "sniffing.timeout") + return } diff --git a/handler/unix/sniffing.go b/handler/unix/sniffing.go new file mode 100644 index 00000000..6833fc02 --- /dev/null +++ b/handler/unix/sniffing.go @@ -0,0 +1,259 @@ +package unix + +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 *unixHandler) 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 *unixHandler) 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 *unixHandler) 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/internal/io/io.go b/internal/io/io.go index 87c72f83..9c253006 100644 --- a/internal/io/io.go +++ b/internal/io/io.go @@ -1,6 +1,9 @@ package io -import "io" +import ( + "io" + "time" +) type readWriter struct { io.Reader @@ -27,3 +30,14 @@ func NewReadWriteCloser(r io.Reader, w io.Writer, c io.Closer) io.ReadWriteClose Closer: c, } } + +type setReadDeadline interface { + SetReadDeadline(t time.Time) error +} + +func SetReadDeadline(rw io.ReadWriter, t time.Time) error { + if v, _ := rw.(setReadDeadline); v != nil { + return v.SetReadDeadline(t) + } + return nil +} diff --git a/internal/util/forward/forward.go b/internal/util/forward/forward.go deleted file mode 100644 index bad50adf..00000000 --- a/internal/util/forward/forward.go +++ /dev/null @@ -1,90 +0,0 @@ -package forward - -import ( - "bufio" - "bytes" - "context" - "crypto/tls" - "encoding/binary" - "io" - "net/http" - "strings" - - dissector "github.com/go-gost/tls-dissector" - xio "github.com/go-gost/x/internal/io" -) - -func Sniffing(ctx context.Context, rdw io.ReadWriter) (rw io.ReadWriter, host string, protocol string, err error) { - rw = rdw - - // try to sniff TLS traffic - var hdr [dissector.RecordHeaderLen]byte - n, err := io.ReadFull(rw, hdr[:]) - rw = xio.NewReadWriter(io.MultiReader(bytes.NewReader(hdr[:n]), rw), rw) - tlsVersion := binary.BigEndian.Uint16(hdr[1:3]) - if err == nil && - hdr[0] == dissector.Handshake && - (tlsVersion >= tls.VersionTLS10 && tlsVersion <= tls.VersionTLS13) { - rw, host, err = sniffSNI(rw) - protocol = ProtoTLS - return - } - - // try to sniff HTTP traffic - if isHTTP(string(hdr[:])) { - buf := new(bytes.Buffer) - var r *http.Request - r, err = http.ReadRequest(bufio.NewReader(io.TeeReader(rw, buf))) - rw = xio.NewReadWriter(io.MultiReader(buf, rw), rw) - if err == nil { - host = r.Host - protocol = ProtoHTTP - return - } - } - - protocol = sniffProtocol(hdr[:]) - - return -} - -func sniffSNI(rw io.ReadWriter) (io.ReadWriter, string, error) { - buf := new(bytes.Buffer) - host, err := getServerName(io.TeeReader(rw, buf)) - rw = xio.NewReadWriter(io.MultiReader(buf, rw), rw) - return rw, host, err -} - -func getServerName(r io.Reader) (host string, err error) { - clientHello, err := dissector.ParseClientHello(r) - if err != nil { - return - } - host = clientHello.ServerName - return -} - -func isHTTP(s string) bool { - return strings.HasPrefix(http.MethodGet, s[:3]) || - strings.HasPrefix(http.MethodPost, s[:4]) || - strings.HasPrefix(http.MethodPut, s[:3]) || - strings.HasPrefix(http.MethodDelete, s) || - strings.HasPrefix(http.MethodOptions, s) || - strings.HasPrefix(http.MethodPatch, s) || - strings.HasPrefix(http.MethodHead, s[:4]) || - strings.HasPrefix(http.MethodConnect, s) || - strings.HasPrefix(http.MethodTrace, s) -} - -const ( - ProtoHTTP = "http" - ProtoTLS = "tls" - ProtoSSHv2 = "SSH-2" -) - -func sniffProtocol(hdr []byte) string { - if string(hdr) == ProtoSSHv2 { - return "ssh" - } - return "" -} diff --git a/internal/util/sniffing/sniff.go b/internal/util/sniffing/sniff.go new file mode 100644 index 00000000..8a11696f --- /dev/null +++ b/internal/util/sniffing/sniff.go @@ -0,0 +1,55 @@ +package sniffing + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/binary" + "net/http" + "strings" + + dissector "github.com/go-gost/tls-dissector" +) + +const ( + ProtoHTTP = "http" + ProtoTLS = "tls" + ProtoSSH = "ssh" +) + +func Sniff(ctx context.Context, r *bufio.Reader) (proto string, err error) { + hdr, err := r.Peek(dissector.RecordHeaderLen) + if err != nil { + return + } + + // try to sniff TLS traffic + tlsVersion := binary.BigEndian.Uint16(hdr[1:3]) + if hdr[0] == dissector.Handshake && + (tlsVersion >= tls.VersionTLS10 && tlsVersion <= tls.VersionTLS13) { + return ProtoTLS, nil + } + + // try to sniff HTTP traffic + if isHTTP(string(hdr[:])) { + return ProtoHTTP, nil + } + + if string(hdr) == "SSH-2" { + return ProtoSSH, nil + } + + return +} + +func isHTTP(s string) bool { + return strings.HasPrefix(http.MethodGet, s[:3]) || + strings.HasPrefix(http.MethodPost, s[:4]) || + strings.HasPrefix(http.MethodPut, s[:3]) || + strings.HasPrefix(http.MethodDelete, s) || + strings.HasPrefix(http.MethodOptions, s) || + strings.HasPrefix(http.MethodPatch, s) || + strings.HasPrefix(http.MethodHead, s[:4]) || + strings.HasPrefix(http.MethodConnect, s) || + strings.HasPrefix(http.MethodTrace, s) +} diff --git a/recorder/recorder.go b/recorder/recorder.go index 4fb25f91..9bc84cf3 100644 --- a/recorder/recorder.go +++ b/recorder/recorder.go @@ -67,6 +67,7 @@ type HandlerRecorderObject struct { Host string `json:"host"` 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"` @@ -78,7 +79,7 @@ type HandlerRecorderObject struct { } func (p *HandlerRecorderObject) Record(ctx context.Context, r recorder.Recorder) error { - if p == nil || r == nil { + if p == nil || r == nil || p.Time.IsZero(){ return nil }