From 8aab73502981938dd707a12d9122746683e01c19 Mon Sep 17 00:00:00 2001 From: Toby Date: Sat, 29 Jun 2024 13:40:52 -0700 Subject: [PATCH] feat: experimental HTTP/TLS sniffing implementation (no QUIC yet) --- app/cmd/server.go | 7 ++ extras/go.mod | 2 +- extras/sniff/sniff.go | 165 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 extras/sniff/sniff.go diff --git a/app/cmd/server.go b/app/cmd/server.go index d2c9f4c..1a1ee6d 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -15,6 +15,8 @@ import ( "strings" "time" + "github.com/apernet/hysteria/extras/v2/sniff" + "github.com/caddyserver/certmagic" "github.com/libdns/cloudflare" "github.com/libdns/duckdns" @@ -855,6 +857,11 @@ func runServer(cmd *cobra.Command, args []string) { logger.Fatal("failed to load server config", zap.Error(err)) } + hyConfig.RequestHook = &sniff.Sniffer{ + Timeout: 4 * time.Second, + RewriteDomain: false, + } + s, err := server.NewServer(hyConfig) if err != nil { logger.Fatal("failed to initialize server", zap.Error(err)) diff --git a/extras/go.mod b/extras/go.mod index 5830418..21cc989 100644 --- a/extras/go.mod +++ b/extras/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000 + github.com/apernet/quic-go v0.44.1-0.20240520215222-bb2e53664023 github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 github.com/hashicorp/golang-lru/v2 v2.0.5 github.com/miekg/dns v1.1.59 @@ -15,7 +16,6 @@ require ( ) require ( - github.com/apernet/quic-go v0.44.1-0.20240520215222-bb2e53664023 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect diff --git a/extras/sniff/sniff.go b/extras/sniff/sniff.go new file mode 100644 index 0000000..61f80c2 --- /dev/null +++ b/extras/sniff/sniff.go @@ -0,0 +1,165 @@ +package sniff + +import ( + "bufio" + "context" + "crypto/tls" + "io" + "net" + "net/http" + "strings" + "time" + + "github.com/apernet/hysteria/core/v2/server" + "github.com/apernet/quic-go" +) + +var _ server.RequestHook = (*Sniffer)(nil) + +// Sniffer is a server core RequestHook that performs packet inspection and possibly +// rewrites the request address based on what's in the protocol header. +// This is mainly for inbounds that inherently cannot get domain information (e.g. TUN), +// in which case sniffing can restore the domains and apply ACLs correctly. +// Currently supports HTTP, HTTPS (TLS) and QUIC. +type Sniffer struct { + Timeout time.Duration + RewriteDomain bool // Whether to rewrite the address even when it's already a domain +} + +func (h *Sniffer) isDomain(addr string) bool { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return false + } + return net.ParseIP(host) == nil +} + +func (h *Sniffer) isHTTP(buf []byte) bool { + if len(buf) < 3 { + return false + } + // First 3 bytes should be English letters (whatever HTTP method) + for _, b := range buf[:3] { + if (b < 'A' || b > 'Z') && (b < 'a' || b > 'z') { + return false + } + } + return true +} + +func (h *Sniffer) isTLS(buf []byte) bool { + if len(buf) < 3 { + return false + } + return buf[0] >= 0x16 && buf[0] <= 0x17 && + buf[1] == 0x03 && buf[2] <= 0x09 +} + +func (h *Sniffer) Check(isUDP bool, reqAddr string) bool { + // @ means it's internal (e.g. speed test) + return !strings.HasPrefix(reqAddr, "@") && !isUDP && (h.RewriteDomain || !h.isDomain(reqAddr)) +} + +func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { + err := stream.SetReadDeadline(time.Now().Add(h.Timeout)) + if err != nil { + return nil, err + } + // Make sure to reset the deadline after sniffing + defer stream.SetReadDeadline(time.Time{}) + // Read 3 bytes to determine the protocol + pre := make([]byte, 3) + n, err := io.ReadFull(stream, pre) + if err != nil { + // Not enough within the timeout, just return what we have + return pre[:n], nil + } + if h.isHTTP(pre) { + fConn := &fakeConn{Stream: stream, Pre: pre} + req, _ := http.ReadRequest(bufio.NewReader(fConn)) + if req != nil && req.Host != "" { + _, port, err := net.SplitHostPort(*reqAddr) + if err != nil { + return nil, err + } + *reqAddr = net.JoinHostPort(req.Host, port) + } + return fConn.Buffer, nil + } else if h.isTLS(pre) { + fConn := &fakeConn{Stream: stream, Pre: pre} + var clientHello *tls.ClientHelloInfo + _ = tls.Server(fConn, &tls.Config{ + GetConfigForClient: func(info *tls.ClientHelloInfo) (*tls.Config, error) { + clientHello = info + return nil, nil + }, + }).HandshakeContext(context.Background()) + if clientHello != nil && clientHello.ServerName != "" { + _, port, err := net.SplitHostPort(*reqAddr) + if err != nil { + return nil, err + } + *reqAddr = net.JoinHostPort(clientHello.ServerName, port) + } + return fConn.Buffer, nil + } else { + // Unrecognized protocol, just return what we have + return pre, nil + } +} + +func (h *Sniffer) UDP(data []byte, reqAddr *string) error { + return nil +} + +type fakeConn struct { + Stream quic.Stream + Pre []byte + Buffer []byte +} + +func (c *fakeConn) Read(b []byte) (n int, err error) { + if len(c.Pre) > 0 { + n = copy(b, c.Pre) + c.Pre = c.Pre[n:] + c.Buffer = append(c.Buffer, b[:n]...) + return n, nil + } + n, err = c.Stream.Read(b) + if n > 0 { + c.Buffer = append(c.Buffer, b[:n]...) + } + return n, err +} + +func (c *fakeConn) Write(b []byte) (n int, err error) { + // Do not write anything, pretend it's successful + return len(b), nil +} + +func (c *fakeConn) Close() error { + // Do not close the stream + return nil +} + +func (c *fakeConn) LocalAddr() net.Addr { + // Doesn't matter + return nil +} + +func (c *fakeConn) RemoteAddr() net.Addr { + // Doesn't matter + return nil +} + +func (c *fakeConn) SetDeadline(t time.Time) error { + return c.Stream.SetReadDeadline(t) +} + +func (c *fakeConn) SetReadDeadline(t time.Time) error { + return c.Stream.SetReadDeadline(t) +} + +func (c *fakeConn) SetWriteDeadline(t time.Time) error { + return c.Stream.SetWriteDeadline(t) +}