From 8aab73502981938dd707a12d9122746683e01c19 Mon Sep 17 00:00:00 2001 From: Toby Date: Sat, 29 Jun 2024 13:40:52 -0700 Subject: [PATCH 1/6] 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) +} From 16bfdc7720bab6f0fb11ea56e81c53b92ae5f0dd Mon Sep 17 00:00:00 2001 From: Toby Date: Sat, 29 Jun 2024 15:52:56 -0700 Subject: [PATCH 2/6] feat: QUIC sniffing --- app/go.mod | 12 +- app/go.sum | 24 ++- core/go.mod | 8 +- core/go.sum | 16 +- extras/go.mod | 12 +- extras/go.sum | 24 ++- extras/sniff/internal/quic/LICENSE | 31 +++ extras/sniff/internal/quic/README.md | 1 + extras/sniff/internal/quic/header.go | 105 ++++++++++ .../sniff/internal/quic/packet_protector.go | 193 ++++++++++++++++++ .../internal/quic/packet_protector_test.go | 94 +++++++++ extras/sniff/internal/quic/payload.go | 122 +++++++++++ extras/sniff/internal/quic/quic.go | 59 ++++++ extras/sniff/sniff.go | 98 +++++---- go.work.sum | 4 + 15 files changed, 716 insertions(+), 87 deletions(-) create mode 100644 extras/sniff/internal/quic/LICENSE create mode 100644 extras/sniff/internal/quic/README.md create mode 100644 extras/sniff/internal/quic/header.go create mode 100644 extras/sniff/internal/quic/packet_protector.go create mode 100644 extras/sniff/internal/quic/packet_protector_test.go create mode 100644 extras/sniff/internal/quic/payload.go create mode 100644 extras/sniff/internal/quic/quic.go diff --git a/app/go.mod b/app/go.mod index bfc20b0..dfbea42 100644 --- a/app/go.mod +++ b/app/go.mod @@ -23,12 +23,14 @@ require ( github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 - golang.org/x/sys v0.20.0 + golang.org/x/sys v0.21.0 ) require ( + github.com/andybalholm/brotli v1.1.0 // indirect github.com/apernet/quic-go v0.44.1-0.20240520215222-bb2e53664023 // indirect github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect + github.com/cloudflare/circl v1.3.9 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -40,6 +42,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.1.1 // indirect github.com/libdns/libdns v0.2.2 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -51,6 +54,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect + github.com/refraction-networking/utls v1.6.6 // indirect github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/spf13/afero v1.9.3 // indirect @@ -66,13 +70,13 @@ require ( go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/crypto v0.23.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/app/go.sum b/app/go.sum index 550394f..e1b80a5 100644 --- a/app/go.sum +++ b/app/go.sum @@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f h1:uVh0qpEslrWjgzx9vOcyCqsOY3c9kofDZ1n+qaw35ZY= github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f/go.mod h1:xkkq9D4ygcldQQhKS/w9CadiCKwCngU7K9E3DaKahpM= github.com/apernet/quic-go v0.44.1-0.20240520215222-bb2e53664023 h1:UTrvVPt+GfeOeli9/3gvpCDz2Jd5UEn3YotfP0u/pok= @@ -55,6 +57,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= +github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -164,6 +168,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0= github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -222,6 +228,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig= +github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= @@ -306,8 +314,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -455,8 +463,8 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -468,8 +476,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -526,8 +534,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/core/go.mod b/core/go.mod index 66a8938..f35cc54 100644 --- a/core/go.mod +++ b/core/go.mod @@ -22,12 +22,12 @@ require ( github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/stretchr/objx v0.5.2 // indirect go.uber.org/mock v0.4.0 // indirect - golang.org/x/crypto v0.23.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/core/go.sum b/core/go.sum index a8e10c4..1a133cb 100644 --- a/core/go.sum +++ b/core/go.sum @@ -47,8 +47,8 @@ go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= @@ -58,14 +58,14 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/extras/go.mod b/extras/go.mod index 21cc989..2d9fae7 100644 --- a/extras/go.mod +++ b/extras/go.mod @@ -8,17 +8,21 @@ require ( 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 + github.com/refraction-networking/utls v1.6.6 github.com/stretchr/testify v1.9.0 github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 - golang.org/x/crypto v0.23.0 + golang.org/x/crypto v0.24.0 golang.org/x/net v0.25.0 google.golang.org/protobuf v1.34.1 ) require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/cloudflare/circl v1.3.9 // 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 + github.com/klauspost/compress v1.17.9 // indirect github.com/kr/text v0.2.0 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect @@ -30,9 +34,9 @@ require ( golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/extras/go.sum b/extras/go.sum index 5d931d7..74dceb5 100644 --- a/extras/go.sum +++ b/extras/go.sum @@ -1,3 +1,5 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apernet/quic-go v0.44.1-0.20240520215222-bb2e53664023 h1:UTrvVPt+GfeOeli9/3gvpCDz2Jd5UEn3YotfP0u/pok= github.com/apernet/quic-go v0.44.1-0.20240520215222-bb2e53664023/go.mod h1:UkcG7+34BM+bbH2RFVKtHQp3mR7h8yJHx4z95lZ7sx4= github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= @@ -5,6 +7,8 @@ github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= +github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -22,6 +26,8 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -39,6 +45,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig= +github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -58,8 +66,8 @@ go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -84,8 +92,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -93,16 +101,16 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/extras/sniff/internal/quic/LICENSE b/extras/sniff/internal/quic/LICENSE new file mode 100644 index 0000000..43970c4 --- /dev/null +++ b/extras/sniff/internal/quic/LICENSE @@ -0,0 +1,31 @@ +Author:: Cuong Manh Le +Copyright:: Copyright (c) 2023, Cuong Manh Le +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the @organization@ nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/extras/sniff/internal/quic/README.md b/extras/sniff/internal/quic/README.md new file mode 100644 index 0000000..8f3a5e2 --- /dev/null +++ b/extras/sniff/internal/quic/README.md @@ -0,0 +1 @@ +The code here is from https://github.com/cuonglm/quicsni with various modifications. \ No newline at end of file diff --git a/extras/sniff/internal/quic/header.go b/extras/sniff/internal/quic/header.go new file mode 100644 index 0000000..c1a5e7c --- /dev/null +++ b/extras/sniff/internal/quic/header.go @@ -0,0 +1,105 @@ +package quic + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + + "github.com/apernet/quic-go/quicvarint" +) + +// The Header represents a QUIC header. +type Header struct { + Type uint8 + Version uint32 + SrcConnectionID []byte + DestConnectionID []byte + Length int64 + Token []byte +} + +// ParseInitialHeader parses the initial packet of a QUIC connection, +// return the initial header and number of bytes read so far. +func ParseInitialHeader(data []byte) (*Header, int64, error) { + br := bytes.NewReader(data) + hdr, err := parseLongHeader(br) + if err != nil { + return nil, 0, err + } + n := int64(len(data) - br.Len()) + return hdr, n, nil +} + +func parseLongHeader(b *bytes.Reader) (*Header, error) { + typeByte, err := b.ReadByte() + if err != nil { + return nil, err + } + h := &Header{} + ver, err := beUint32(b) + if err != nil { + return nil, err + } + h.Version = ver + if h.Version != 0 && typeByte&0x40 == 0 { + return nil, errors.New("not a QUIC packet") + } + destConnIDLen, err := b.ReadByte() + if err != nil { + return nil, err + } + h.DestConnectionID = make([]byte, int(destConnIDLen)) + if err := readConnectionID(b, h.DestConnectionID); err != nil { + return nil, err + } + srcConnIDLen, err := b.ReadByte() + if err != nil { + return nil, err + } + h.SrcConnectionID = make([]byte, int(srcConnIDLen)) + if err := readConnectionID(b, h.SrcConnectionID); err != nil { + return nil, err + } + + initialPacketType := byte(0b00) + if h.Version == V2 { + initialPacketType = 0b01 + } + if (typeByte >> 4 & 0b11) == initialPacketType { + tokenLen, err := quicvarint.Read(b) + if err != nil { + return nil, err + } + if tokenLen > uint64(b.Len()) { + return nil, io.EOF + } + h.Token = make([]byte, tokenLen) + if _, err := io.ReadFull(b, h.Token); err != nil { + return nil, err + } + } + + pl, err := quicvarint.Read(b) + if err != nil { + return nil, err + } + h.Length = int64(pl) + return h, err +} + +func readConnectionID(r io.Reader, cid []byte) error { + _, err := io.ReadFull(r, cid) + if err == io.ErrUnexpectedEOF { + return io.EOF + } + return nil +} + +func beUint32(r io.Reader) (uint32, error) { + b := make([]byte, 4) + if _, err := io.ReadFull(r, b); err != nil { + return 0, err + } + return binary.BigEndian.Uint32(b), nil +} diff --git a/extras/sniff/internal/quic/packet_protector.go b/extras/sniff/internal/quic/packet_protector.go new file mode 100644 index 0000000..42de841 --- /dev/null +++ b/extras/sniff/internal/quic/packet_protector.go @@ -0,0 +1,193 @@ +package quic + +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "crypto/tls" + "encoding/binary" + "errors" + "fmt" + "hash" + + "golang.org/x/crypto/chacha20" + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/cryptobyte" + "golang.org/x/crypto/hkdf" +) + +// NewProtectionKey creates a new ProtectionKey. +func NewProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) { + return newProtectionKey(suite, secret, v) +} + +// NewInitialProtectionKey is like NewProtectionKey, but the returned protection key +// is used for encrypt/decrypt Initial Packet only. +// +// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-initial-secrets +func NewInitialProtectionKey(secret []byte, v uint32) (*ProtectionKey, error) { + return NewProtectionKey(tls.TLS_AES_128_GCM_SHA256, secret, v) +} + +// NewPacketProtector creates a new PacketProtector. +func NewPacketProtector(key *ProtectionKey) *PacketProtector { + return &PacketProtector{key: key} +} + +// PacketProtector is used for protecting a QUIC packet. +// +// See: https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection +type PacketProtector struct { + key *ProtectionKey +} + +// UnProtect decrypts a QUIC packet. +func (pp *PacketProtector) UnProtect(packet []byte, pnOffset, pnMax int64) ([]byte, error) { + if isLongHeader(packet[0]) && int64(len(packet)) < pnOffset+4+16 { + return nil, errors.New("packet with long header is too small") + } + + // https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-sample + sampleOffset := pnOffset + 4 + sample := packet[sampleOffset : sampleOffset+16] + + // https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati + mask := pp.key.headerProtection(sample) + if isLongHeader(packet[0]) { + // Long header: 4 bits masked + packet[0] ^= mask[0] & 0x0f + } else { + // Short header: 5 bits masked + packet[0] ^= mask[0] & 0x1f + } + + pnLen := packet[0]&0x3 + 1 + pn := int64(0) + for i := uint8(0); i < pnLen; i++ { + packet[pnOffset:][i] ^= mask[1+i] + pn = (pn << 8) | int64(packet[pnOffset:][i]) + } + pn = decodePacketNumber(pnMax, pn, pnLen) + hdr := packet[:pnOffset+int64(pnLen)] + payload := packet[pnOffset:][pnLen:] + dec, err := pp.key.aead.Open(payload[:0], pp.key.nonce(pn), payload, hdr) + if err != nil { + return nil, fmt.Errorf("decryption failed: %w", err) + } + return dec, nil +} + +// ProtectionKey is the key used to protect a QUIC packet. +type ProtectionKey struct { + aead cipher.AEAD + headerProtection func(sample []byte) (mask []byte) + iv []byte +} + +// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aead-usage +// +// "The 62 bits of the reconstructed QUIC packet number in network byte order are +// left-padded with zeros to the size of the IV. The exclusive OR of the padded +// packet number and the IV forms the AEAD nonce." +func (pk *ProtectionKey) nonce(pn int64) []byte { + nonce := make([]byte, len(pk.iv)) + binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(pn)) + for i := range pk.iv { + nonce[i] ^= pk.iv[i] + } + return nonce +} + +func newProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) { + switch suite { + case tls.TLS_AES_128_GCM_SHA256: + key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, 16) + c, err := aes.NewCipher(key) + if err != nil { + panic(err) + } + aead, err := cipher.NewGCM(c) + if err != nil { + panic(err) + } + iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize()) + hpKey := hkdfExpandLabel(crypto.SHA256.New, secret, headerProtectionLabel(v), nil, 16) + hp, err := aes.NewCipher(hpKey) + if err != nil { + panic(err) + } + k := &ProtectionKey{} + k.aead = aead + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aes-based-header-protection + k.headerProtection = func(sample []byte) []byte { + mask := make([]byte, hp.BlockSize()) + hp.Encrypt(mask, sample) + return mask + } + k.iv = iv + return k, nil + case tls.TLS_CHACHA20_POLY1305_SHA256: + key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, chacha20poly1305.KeySize) + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize()) + hpKey := hkdfExpandLabel(sha256.New, secret, headerProtectionLabel(v), nil, chacha20.KeySize) + k := &ProtectionKey{} + k.aead = aead + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-based-header-prote + k.headerProtection = func(sample []byte) []byte { + nonce := sample[4:16] + c, err := chacha20.NewUnauthenticatedCipher(hpKey, nonce) + if err != nil { + panic(err) + } + c.SetCounter(binary.LittleEndian.Uint32(sample[:4])) + mask := make([]byte, 5) + c.XORKeyStream(mask, mask) + return mask + } + k.iv = iv + return k, nil + } + return nil, errors.New("not supported cipher suite") +} + +// decodePacketNumber decode the packet number after header protection removed. +// +// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-32#section-appendix.a +func decodePacketNumber(largest, truncated int64, nbits uint8) int64 { + expected := largest + 1 + win := int64(1 << (nbits * 8)) + hwin := win / 2 + mask := win - 1 + candidate := (expected &^ mask) | truncated + switch { + case candidate <= expected-hwin && candidate < (1<<62)-win: + return candidate + win + case candidate > expected+hwin && candidate >= win: + return candidate - win + } + return candidate +} + +// Copied from crypto/tls/key_schedule.go. +func hkdfExpandLabel(hash func() hash.Hash, secret []byte, label string, context []byte, length int) []byte { + var hkdfLabel cryptobyte.Builder + hkdfLabel.AddUint16(uint16(length)) + hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes([]byte("tls13 ")) + b.AddBytes([]byte(label)) + }) + hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(context) + }) + out := make([]byte, length) + n, err := hkdf.Expand(hash, secret, hkdfLabel.BytesOrPanic()).Read(out) + if err != nil || n != length { + panic("quic: HKDF-Expand-Label invocation failed unexpectedly") + } + return out +} diff --git a/extras/sniff/internal/quic/packet_protector_test.go b/extras/sniff/internal/quic/packet_protector_test.go new file mode 100644 index 0000000..bc355d2 --- /dev/null +++ b/extras/sniff/internal/quic/packet_protector_test.go @@ -0,0 +1,94 @@ +package quic + +import ( + "bytes" + "crypto" + "crypto/tls" + "encoding/hex" + "strings" + "testing" + "unicode" + + "golang.org/x/crypto/hkdf" +) + +func TestInitialPacketProtector_UnProtect(t *testing.T) { + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-server-initial + protect := mustHexDecodeString(` + c7ff0000200008f067a5502a4262b500 4075fb12ff07823a5d24534d906ce4c7 + 6782a2167e3479c0f7f6395dc2c91676 302fe6d70bb7cbeb117b4ddb7d173498 + 44fd61dae200b8338e1b932976b61d91 e64a02e9e0ee72e3a6f63aba4ceeeec5 + be2f24f2d86027572943533846caa13e 6f163fb257473d0eda5047360fd4a47e + fd8142fafc0f76 + `) + unProtect := mustHexDecodeString(` + 02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739 + 88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94 + 0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00 + 020304 + `) + + connID := mustHexDecodeString(`8394c8f03e515708`) + + packet := append([]byte{}, protect...) + hdr, offset, err := ParseInitialHeader(packet) + if err != nil { + t.Fatal(err) + } + + initialSecret := hkdf.Extract(crypto.SHA256.New, connID, getSalt(hdr.Version)) + serverSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "server in", []byte{}, crypto.SHA256.Size()) + key, err := NewInitialProtectionKey(serverSecret, hdr.Version) + if err != nil { + t.Fatal(err) + } + pp := NewPacketProtector(key) + got, err := pp.UnProtect(protect, offset, 1) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, unProtect) { + t.Error("UnProtect returns wrong result") + } +} + +func TestPacketProtectorShortHeader_UnProtect(t *testing.T) { + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-poly1305-short-hea + protect := mustHexDecodeString(`4cfe4189655e5cd55c41f69080575d7999c25a5bfb`) + unProtect := mustHexDecodeString(`01`) + hdr := mustHexDecodeString(`4200bff4`) + + secret := mustHexDecodeString(`9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b`) + k, err := NewProtectionKey(tls.TLS_CHACHA20_POLY1305_SHA256, secret, V1) + if err != nil { + t.Fatal(err) + } + + pnLen := int(hdr[0]&0x03) + 1 + offset := len(hdr) - pnLen + pp := NewPacketProtector(k) + got, err := pp.UnProtect(protect, int64(offset), 654360564) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, unProtect) { + t.Error("UnProtect returns wrong result") + } +} + +func mustHexDecodeString(s string) []byte { + b, err := hex.DecodeString(normalizeHex(s)) + if err != nil { + panic(err) + } + return b +} + +func normalizeHex(s string) string { + return strings.Map(func(c rune) rune { + if unicode.IsSpace(c) { + return -1 + } + return c + }, s) +} diff --git a/extras/sniff/internal/quic/payload.go b/extras/sniff/internal/quic/payload.go new file mode 100644 index 0000000..453b714 --- /dev/null +++ b/extras/sniff/internal/quic/payload.go @@ -0,0 +1,122 @@ +package quic + +import ( + "bytes" + "crypto" + "errors" + "fmt" + "io" + "sort" + + "github.com/apernet/quic-go/quicvarint" + "golang.org/x/crypto/hkdf" +) + +func ReadCryptoPayload(packet []byte) ([]byte, error) { + hdr, offset, err := ParseInitialHeader(packet) + if err != nil { + return nil, err + } + // Some sanity checks + if hdr.Version != V1 && hdr.Version != V2 { + return nil, fmt.Errorf("unsupported version: %x", hdr.Version) + } + if offset == 0 || hdr.Length == 0 { + return nil, errors.New("invalid packet") + } + + initialSecret := hkdf.Extract(crypto.SHA256.New, hdr.DestConnectionID, getSalt(hdr.Version)) + clientSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "client in", []byte{}, crypto.SHA256.Size()) + key, err := NewInitialProtectionKey(clientSecret, hdr.Version) + if err != nil { + return nil, fmt.Errorf("NewInitialProtectionKey: %w", err) + } + pp := NewPacketProtector(key) + // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-client-initial + // + // "The unprotected header includes the connection ID and a 4-byte packet number encoding for a packet number of 2" + if int64(len(packet)) < offset+hdr.Length { + return nil, fmt.Errorf("packet is too short: %d < %d", len(packet), offset+hdr.Length) + } + unProtectedPayload, err := pp.UnProtect(packet[:offset+hdr.Length], offset, 2) + if err != nil { + return nil, err + } + frs, err := extractCryptoFrames(bytes.NewReader(unProtectedPayload)) + if err != nil { + return nil, err + } + data := assembleCryptoFrames(frs) + if data == nil { + return nil, errors.New("unable to assemble crypto frames") + } + return data, nil +} + +const ( + paddingFrameType = 0x00 + pingFrameType = 0x01 + cryptoFrameType = 0x06 +) + +type cryptoFrame struct { + Offset int64 + Data []byte +} + +func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) { + var frames []cryptoFrame + for r.Len() > 0 { + typ, err := quicvarint.Read(r) + if err != nil { + return nil, err + } + if typ == paddingFrameType || typ == pingFrameType { + continue + } + if typ != cryptoFrameType { + return nil, fmt.Errorf("encountered unexpected frame type: %d", typ) + } + var frame cryptoFrame + offset, err := quicvarint.Read(r) + if err != nil { + return nil, err + } + frame.Offset = int64(offset) + dataLen, err := quicvarint.Read(r) + if err != nil { + return nil, err + } + frame.Data = make([]byte, dataLen) + if _, err := io.ReadFull(r, frame.Data); err != nil { + return nil, err + } + frames = append(frames, frame) + } + return frames, nil +} + +// assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible). +// It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous. +func assembleCryptoFrames(frames []cryptoFrame) []byte { + if len(frames) == 0 { + return nil + } + if len(frames) == 1 { + return frames[0].Data + } + // sort the frames by offset + sort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset }) + // check if the frames are contiguous + for i := 1; i < len(frames); i++ { + if frames[i].Offset != frames[i-1].Offset+int64(len(frames[i-1].Data)) { + return nil + } + } + // concatenate the frames + data := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data))) + for _, frame := range frames { + copy(data[frame.Offset:], frame.Data) + } + return data +} diff --git a/extras/sniff/internal/quic/quic.go b/extras/sniff/internal/quic/quic.go new file mode 100644 index 0000000..1cfa103 --- /dev/null +++ b/extras/sniff/internal/quic/quic.go @@ -0,0 +1,59 @@ +package quic + +const ( + V1 uint32 = 0x1 + V2 uint32 = 0x6b3343cf + + hkdfLabelKeyV1 = "quic key" + hkdfLabelKeyV2 = "quicv2 key" + hkdfLabelIVV1 = "quic iv" + hkdfLabelIVV2 = "quicv2 iv" + hkdfLabelHPV1 = "quic hp" + hkdfLabelHPV2 = "quicv2 hp" +) + +var ( + quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} + // https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets + quicSaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} + // https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-initial-salt-2 + quicSaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9} +) + +// isLongHeader reports whether b is the first byte of a long header packet. +func isLongHeader(b byte) bool { + return b&0x80 > 0 +} + +func getSalt(v uint32) []byte { + switch v { + case V1: + return quicSaltV1 + case V2: + return quicSaltV2 + } + return quicSaltOld +} + +func keyLabel(v uint32) string { + kl := hkdfLabelKeyV1 + if v == V2 { + kl = hkdfLabelKeyV2 + } + return kl +} + +func ivLabel(v uint32) string { + ivl := hkdfLabelIVV1 + if v == V2 { + ivl = hkdfLabelIVV2 + } + return ivl +} + +func headerProtectionLabel(v uint32) string { + if v == V2 { + return hkdfLabelHPV2 + } + return hkdfLabelHPV1 +} diff --git a/extras/sniff/sniff.go b/extras/sniff/sniff.go index 61f80c2..79e35f6 100644 --- a/extras/sniff/sniff.go +++ b/extras/sniff/sniff.go @@ -2,16 +2,17 @@ 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" + utls "github.com/refraction-networking/utls" + + "github.com/apernet/hysteria/core/v2/server" + quicInternal "github.com/apernet/hysteria/extras/v2/sniff/internal/quic" ) var _ server.RequestHook = (*Sniffer)(nil) @@ -57,7 +58,7 @@ func (h *Sniffer) isTLS(buf []byte) bool { 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)) + return !strings.HasPrefix(reqAddr, "@") && (h.RewriteDomain || !h.isDomain(reqAddr)) } func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { @@ -75,8 +76,9 @@ func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { return pre[:n], nil } if h.isHTTP(pre) { - fConn := &fakeConn{Stream: stream, Pre: pre} - req, _ := http.ReadRequest(bufio.NewReader(fConn)) + // HTTP + tr := &teeReader{Stream: stream, Pre: pre} + req, _ := http.ReadRequest(bufio.NewReader(tr)) if req != nil && req.Host != "" { _, port, err := net.SplitHostPort(*reqAddr) if err != nil { @@ -84,16 +86,24 @@ func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { } *reqAddr = net.JoinHostPort(req.Host, port) } - return fConn.Buffer, nil + return tr.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()) + // TLS + // Need to read 2 more bytes (content length) + pre = append(pre, make([]byte, 2)...) + n, err = io.ReadFull(stream, pre[3:]) + if err != nil { + // Not enough within the timeout, just return what we have + return pre[:3+n], nil + } + contentLength := int(pre[3])<<8 | int(pre[4]) + pre = append(pre, make([]byte, contentLength)...) + n, err = io.ReadFull(stream, pre[5:]) + if err != nil { + // Not enough within the timeout, just return what we have + return pre[:5+n], nil + } + clientHello := utls.UnmarshalClientHello(pre[5:]) if clientHello != nil && clientHello.ServerName != "" { _, port, err := net.SplitHostPort(*reqAddr) if err != nil { @@ -101,7 +111,7 @@ func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { } *reqAddr = net.JoinHostPort(clientHello.ServerName, port) } - return fConn.Buffer, nil + return pre, nil } else { // Unrecognized protocol, just return what we have return pre, nil @@ -109,57 +119,43 @@ func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { } func (h *Sniffer) UDP(data []byte, reqAddr *string) error { + pl, err := quicInternal.ReadCryptoPayload(data) + if err != nil || len(pl) < 4 || pl[0] != 0x01 { + // Unrecognized protocol, incomplete payload or not a client hello + return nil + } + clientHello := utls.UnmarshalClientHello(pl) + if clientHello != nil && clientHello.ServerName != "" { + _, port, err := net.SplitHostPort(*reqAddr) + if err != nil { + return err + } + *reqAddr = net.JoinHostPort(clientHello.ServerName, port) + } return nil } -type fakeConn struct { +type teeReader struct { Stream quic.Stream Pre []byte - Buffer []byte + + buf []byte } -func (c *fakeConn) Read(b []byte) (n int, err error) { +func (c *teeReader) 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]...) + c.buf = append(c.buf, b[:n]...) return n, nil } n, err = c.Stream.Read(b) if n > 0 { - c.Buffer = append(c.Buffer, b[:n]...) + c.buf = append(c.buf, 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) +func (c *teeReader) Buffer() []byte { + return append(c.Pre, c.buf...) } diff --git a/go.work.sum b/go.work.sum index 4370b0e..f0551e7 100644 --- a/go.work.sum +++ b/go.work.sum @@ -41,6 +41,8 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 h1:ckJgFhFWywOx+ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= @@ -309,6 +311,8 @@ golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= From 3412368d206dc93ed5130c2a7ede334d352ddd4e Mon Sep 17 00:00:00 2001 From: Toby Date: Sat, 29 Jun 2024 16:27:57 -0700 Subject: [PATCH 3/6] feat: app sniff options --- app/cmd/server.go | 23 ++++++++++++++++++----- app/cmd/server_test.go | 5 +++++ app/cmd/server_test.yaml | 5 +++++ extras/sniff/sniff.go | 11 ++++++++++- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/app/cmd/server.go b/app/cmd/server.go index 1a1ee6d..bca4c80 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -66,6 +66,7 @@ type serverConfig struct { UDPIdleTimeout time.Duration `mapstructure:"udpIdleTimeout"` Auth serverConfigAuth `mapstructure:"auth"` Resolver serverConfigResolver `mapstructure:"resolver"` + Sniff serverConfigSniff `mapstructure:"sniff"` ACL serverConfigACL `mapstructure:"acl"` Outbounds []serverConfigOutboundEntry `mapstructure:"outbounds"` TrafficStats serverConfigTrafficStats `mapstructure:"trafficStats"` @@ -181,6 +182,12 @@ type serverConfigResolver struct { HTTPS serverConfigResolverHTTPS `mapstructure:"https"` } +type serverConfigSniff struct { + Enable bool `mapstructure:"enable"` + Timeout time.Duration `mapstructure:"timeout"` + RewriteDomain bool `mapstructure:"rewriteDomain"` +} + type serverConfigACL struct { File string `mapstructure:"file"` Inline []string `mapstructure:"inline"` @@ -543,6 +550,16 @@ func serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.P return outbounds.NewHTTPOutbound(c.URL, c.Insecure) } +func (c *serverConfig) fillRequestHook(hyConfig *server.Config) error { + if c.Sniff.Enable { + hyConfig.RequestHook = &sniff.Sniffer{ + Timeout: c.Sniff.Timeout, + RewriteDomain: c.Sniff.RewriteDomain, + } + } + return nil +} + func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error { // Resolver, ACL, actual outbound are all implemented through the Outbound interface. // Depending on the config, we build a chain like this: @@ -823,6 +840,7 @@ func (c *serverConfig) Config() (*server.Config, error) { c.fillConn, c.fillTLSConfig, c.fillQUICConfig, + c.fillRequestHook, c.fillOutboundConfig, c.fillBandwidthConfig, c.fillIgnoreClientBandwidth, @@ -857,11 +875,6 @@ 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/app/cmd/server_test.go b/app/cmd/server_test.go index d81a61a..bd46681 100644 --- a/app/cmd/server_test.go +++ b/app/cmd/server_test.go @@ -111,6 +111,11 @@ func TestServerConfig(t *testing.T) { Insecure: true, }, }, + Sniff: serverConfigSniff{ + Enable: true, + Timeout: 1 * time.Second, + RewriteDomain: true, + }, ACL: serverConfigACL{ File: "chnroute.txt", Inline: []string{ diff --git a/app/cmd/server_test.yaml b/app/cmd/server_test.yaml index 86a2dcf..343b0a9 100644 --- a/app/cmd/server_test.yaml +++ b/app/cmd/server_test.yaml @@ -83,6 +83,11 @@ resolver: sni: real.stuff.net insecure: true +sniff: + enable: true + timeout: 1s + rewriteDomain: true + acl: file: chnroute.txt inline: diff --git a/extras/sniff/sniff.go b/extras/sniff/sniff.go index 79e35f6..68b3fbc 100644 --- a/extras/sniff/sniff.go +++ b/extras/sniff/sniff.go @@ -15,6 +15,10 @@ import ( quicInternal "github.com/apernet/hysteria/extras/v2/sniff/internal/quic" ) +const ( + sniffDefaultTimeout = 4 * time.Second +) + var _ server.RequestHook = (*Sniffer)(nil) // Sniffer is a server core RequestHook that performs packet inspection and possibly @@ -62,7 +66,12 @@ func (h *Sniffer) Check(isUDP bool, reqAddr string) bool { } func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { - err := stream.SetReadDeadline(time.Now().Add(h.Timeout)) + var err error + if h.Timeout == 0 { + err = stream.SetReadDeadline(time.Now().Add(sniffDefaultTimeout)) + } else { + err = stream.SetReadDeadline(time.Now().Add(h.Timeout)) + } if err != nil { return nil, err } From 7b4def4c35b549555cfb05b141c97dca7574dfec Mon Sep 17 00:00:00 2001 From: Toby Date: Sat, 29 Jun 2024 17:42:30 -0700 Subject: [PATCH 4/6] chore: add sniff test cases --- extras/sniff/.mockery.yaml | 12 + extras/sniff/mock_Stream.go | 492 ++++++++++++++++++++++++++++++++++++ extras/sniff/sniff_test.go | 126 +++++++++ 3 files changed, 630 insertions(+) create mode 100644 extras/sniff/.mockery.yaml create mode 100644 extras/sniff/mock_Stream.go create mode 100644 extras/sniff/sniff_test.go diff --git a/extras/sniff/.mockery.yaml b/extras/sniff/.mockery.yaml new file mode 100644 index 0000000..c866d1d --- /dev/null +++ b/extras/sniff/.mockery.yaml @@ -0,0 +1,12 @@ +with-expecter: true +dir: . +outpkg: sniff +packages: + github.com/apernet/quic-go: + interfaces: + Stream: + config: + mockname: mockStream + replace-type: # internal package alias dirty fix + - github.com/apernet/quic-go/internal/protocol=github.com/apernet/quic-go + - github.com/apernet/quic-go/internal/qerr=github.com/apernet/quic-go diff --git a/extras/sniff/mock_Stream.go b/extras/sniff/mock_Stream.go new file mode 100644 index 0000000..8b21e95 --- /dev/null +++ b/extras/sniff/mock_Stream.go @@ -0,0 +1,492 @@ +// Code generated by mockery v2.43.0. DO NOT EDIT. + +package sniff + +import ( + context "context" + + qerr "github.com/apernet/quic-go" + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// mockStream is an autogenerated mock type for the Stream type +type mockStream struct { + mock.Mock +} + +type mockStream_Expecter struct { + mock *mock.Mock +} + +func (_m *mockStream) EXPECT() *mockStream_Expecter { + return &mockStream_Expecter{mock: &_m.Mock} +} + +// CancelRead provides a mock function with given fields: _a0 +func (_m *mockStream) CancelRead(_a0 qerr.StreamErrorCode) { + _m.Called(_a0) +} + +// mockStream_CancelRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelRead' +type mockStream_CancelRead_Call struct { + *mock.Call +} + +// CancelRead is a helper method to define mock.On call +// - _a0 qerr.StreamErrorCode +func (_e *mockStream_Expecter) CancelRead(_a0 interface{}) *mockStream_CancelRead_Call { + return &mockStream_CancelRead_Call{Call: _e.mock.On("CancelRead", _a0)} +} + +func (_c *mockStream_CancelRead_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelRead_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(qerr.StreamErrorCode)) + }) + return _c +} + +func (_c *mockStream_CancelRead_Call) Return() *mockStream_CancelRead_Call { + _c.Call.Return() + return _c +} + +func (_c *mockStream_CancelRead_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelRead_Call { + _c.Call.Return(run) + return _c +} + +// CancelWrite provides a mock function with given fields: _a0 +func (_m *mockStream) CancelWrite(_a0 qerr.StreamErrorCode) { + _m.Called(_a0) +} + +// mockStream_CancelWrite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelWrite' +type mockStream_CancelWrite_Call struct { + *mock.Call +} + +// CancelWrite is a helper method to define mock.On call +// - _a0 qerr.StreamErrorCode +func (_e *mockStream_Expecter) CancelWrite(_a0 interface{}) *mockStream_CancelWrite_Call { + return &mockStream_CancelWrite_Call{Call: _e.mock.On("CancelWrite", _a0)} +} + +func (_c *mockStream_CancelWrite_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelWrite_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(qerr.StreamErrorCode)) + }) + return _c +} + +func (_c *mockStream_CancelWrite_Call) Return() *mockStream_CancelWrite_Call { + _c.Call.Return() + return _c +} + +func (_c *mockStream_CancelWrite_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelWrite_Call { + _c.Call.Return(run) + return _c +} + +// Close provides a mock function with given fields: +func (_m *mockStream) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockStream_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type mockStream_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *mockStream_Expecter) Close() *mockStream_Close_Call { + return &mockStream_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *mockStream_Close_Call) Run(run func()) *mockStream_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockStream_Close_Call) Return(_a0 error) *mockStream_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_Close_Call) RunAndReturn(run func() error) *mockStream_Close_Call { + _c.Call.Return(run) + return _c +} + +// Context provides a mock function with given fields: +func (_m *mockStream) Context() context.Context { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Context") + } + + var r0 context.Context + if rf, ok := ret.Get(0).(func() context.Context); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(context.Context) + } + } + + return r0 +} + +// mockStream_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' +type mockStream_Context_Call struct { + *mock.Call +} + +// Context is a helper method to define mock.On call +func (_e *mockStream_Expecter) Context() *mockStream_Context_Call { + return &mockStream_Context_Call{Call: _e.mock.On("Context")} +} + +func (_c *mockStream_Context_Call) Run(run func()) *mockStream_Context_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockStream_Context_Call) Return(_a0 context.Context) *mockStream_Context_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_Context_Call) RunAndReturn(run func() context.Context) *mockStream_Context_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function with given fields: p +func (_m *mockStream) Read(p []byte) (int, error) { + ret := _m.Called(p) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return rf(p) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(p) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockStream_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type mockStream_Read_Call struct { + *mock.Call +} + +// Read is a helper method to define mock.On call +// - p []byte +func (_e *mockStream_Expecter) Read(p interface{}) *mockStream_Read_Call { + return &mockStream_Read_Call{Call: _e.mock.On("Read", p)} +} + +func (_c *mockStream_Read_Call) Run(run func(p []byte)) *mockStream_Read_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *mockStream_Read_Call) Return(n int, err error) *mockStream_Read_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *mockStream_Read_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Read_Call { + _c.Call.Return(run) + return _c +} + +// SetDeadline provides a mock function with given fields: t +func (_m *mockStream) SetDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockStream_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline' +type mockStream_SetDeadline_Call struct { + *mock.Call +} + +// SetDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *mockStream_Expecter) SetDeadline(t interface{}) *mockStream_SetDeadline_Call { + return &mockStream_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)} +} + +func (_c *mockStream_SetDeadline_Call) Run(run func(t time.Time)) *mockStream_SetDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *mockStream_SetDeadline_Call) Return(_a0 error) *mockStream_SetDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetDeadline_Call { + _c.Call.Return(run) + return _c +} + +// SetReadDeadline provides a mock function with given fields: t +func (_m *mockStream) SetReadDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetReadDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockStream_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline' +type mockStream_SetReadDeadline_Call struct { + *mock.Call +} + +// SetReadDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *mockStream_Expecter) SetReadDeadline(t interface{}) *mockStream_SetReadDeadline_Call { + return &mockStream_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)} +} + +func (_c *mockStream_SetReadDeadline_Call) Run(run func(t time.Time)) *mockStream_SetReadDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *mockStream_SetReadDeadline_Call) Return(_a0 error) *mockStream_SetReadDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetReadDeadline_Call { + _c.Call.Return(run) + return _c +} + +// SetWriteDeadline provides a mock function with given fields: t +func (_m *mockStream) SetWriteDeadline(t time.Time) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetWriteDeadline") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockStream_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline' +type mockStream_SetWriteDeadline_Call struct { + *mock.Call +} + +// SetWriteDeadline is a helper method to define mock.On call +// - t time.Time +func (_e *mockStream_Expecter) SetWriteDeadline(t interface{}) *mockStream_SetWriteDeadline_Call { + return &mockStream_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)} +} + +func (_c *mockStream_SetWriteDeadline_Call) Run(run func(t time.Time)) *mockStream_SetWriteDeadline_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *mockStream_SetWriteDeadline_Call) Return(_a0 error) *mockStream_SetWriteDeadline_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetWriteDeadline_Call { + _c.Call.Return(run) + return _c +} + +// StreamID provides a mock function with given fields: +func (_m *mockStream) StreamID() qerr.StreamID { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for StreamID") + } + + var r0 qerr.StreamID + if rf, ok := ret.Get(0).(func() qerr.StreamID); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(qerr.StreamID) + } + + return r0 +} + +// mockStream_StreamID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StreamID' +type mockStream_StreamID_Call struct { + *mock.Call +} + +// StreamID is a helper method to define mock.On call +func (_e *mockStream_Expecter) StreamID() *mockStream_StreamID_Call { + return &mockStream_StreamID_Call{Call: _e.mock.On("StreamID")} +} + +func (_c *mockStream_StreamID_Call) Run(run func()) *mockStream_StreamID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockStream_StreamID_Call) Return(_a0 qerr.StreamID) *mockStream_StreamID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockStream_StreamID_Call) RunAndReturn(run func() qerr.StreamID) *mockStream_StreamID_Call { + _c.Call.Return(run) + return _c +} + +// Write provides a mock function with given fields: p +func (_m *mockStream) Write(p []byte) (int, error) { + ret := _m.Called(p) + + if len(ret) == 0 { + panic("no return value specified for Write") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return rf(p) + } + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(p) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockStream_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' +type mockStream_Write_Call struct { + *mock.Call +} + +// Write is a helper method to define mock.On call +// - p []byte +func (_e *mockStream_Expecter) Write(p interface{}) *mockStream_Write_Call { + return &mockStream_Write_Call{Call: _e.mock.On("Write", p)} +} + +func (_c *mockStream_Write_Call) Run(run func(p []byte)) *mockStream_Write_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]byte)) + }) + return _c +} + +func (_c *mockStream_Write_Call) Return(n int, err error) *mockStream_Write_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *mockStream_Write_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Write_Call { + _c.Call.Return(run) + return _c +} + +// newMockStream creates a new instance of mockStream. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockStream(t interface { + mock.TestingT + Cleanup(func()) +}) *mockStream { + mock := &mockStream{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/extras/sniff/sniff_test.go b/extras/sniff/sniff_test.go new file mode 100644 index 0000000..fb86c3b --- /dev/null +++ b/extras/sniff/sniff_test.go @@ -0,0 +1,126 @@ +package sniff + +import ( + "encoding/base64" + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestSnifferTCP(t *testing.T) { + sniffer := &Sniffer{ + Timeout: 1 * time.Second, + RewriteDomain: false, + } + + buf := &[]byte{} + + // Test HTTP + *buf = []byte("POST /hello HTTP/1.1\r\n" + + "Host: example.com\r\n" + + "User-Agent: mamamiya\r\n" + + "Content-Length: 27\r\n" + + "Connection: keep-alive\r\n\r\n" + + "param1=value1¶m2=value2") + index := 0 + stream := &mockStream{} + stream.EXPECT().SetReadDeadline(mock.Anything).Return(nil) + stream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { + if index < len(*buf) { + n := copy(bs, (*buf)[index:]) + index += n + return n, nil + } else { + return 0, io.EOF + } + }) + + // Rewrite IP to domain + reqAddr := "111.111.111.111:80" + assert.True(t, sniffer.Check(false, reqAddr)) + putback, err := sniffer.TCP(stream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, *buf, putback) + assert.Equal(t, "example.com:80", reqAddr) + + // Do not rewrite if it's already a domain + index = 0 + reqAddr = "gulag.cc:443" + assert.False(t, sniffer.Check(false, reqAddr)) + + // Turn on rewrite and now it should rewrite + sniffer.RewriteDomain = true + assert.True(t, sniffer.Check(false, reqAddr)) + + // Test TLS + *buf, err = base64.StdEncoding.DecodeString("FgMBARcBAAETAwPJL2jlt1OAo+Rslkjv/aqKiTthKMaCKg2Gvd+uALDbDCDdY+UIk8ouadEB9fC3j52Y1i7SJZqGIgBRIS6kKieYrAAoEwITAcAswCvAMMAvwCTAI8AowCfACsAJwBTAEwCdAJwAPQA8ADUALwEAAKIAAAAOAAwAAAlpcGluZm8uaW8ABQAFAQAAAAAAKwAJCAMEAwMDAgMBAA0AGgAYCAQIBQgGBAEFAQIBBAMFAwIDAgIGAQYDACMAAAAKAAgABgAdABcAGAAQAAsACQhodHRwLzEuMQAzACYAJAAdACBguQbqNJNyamYxYcrBFpBP7pWv5TgZsP9gwGtMYNKVBQAxAAAAFwAA/wEAAQAALQACAQE=") + assert.NoError(t, err) + index = 0 + reqAddr = "222.222.222.222:443" + assert.True(t, sniffer.Check(false, reqAddr)) + putback, err = sniffer.TCP(stream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, *buf, putback) + assert.Equal(t, "ipinfo.io:443", reqAddr) + + // Test unrecognized 1 + *buf = []byte("Wait It's All Ohio? Always Has Been.") + index = 0 + reqAddr = "123.123.123.123:123" + assert.True(t, sniffer.Check(false, reqAddr)) + putback, err = sniffer.TCP(stream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, *buf, putback) + assert.Equal(t, "123.123.123.123:123", reqAddr) + + // Test unrecognized 2 + *buf = []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a") + index = 0 + reqAddr = "45.45.45.45:45" + assert.True(t, sniffer.Check(false, reqAddr)) + putback, err = sniffer.TCP(stream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, []byte("\x01\x02\x03"), putback) + assert.Equal(t, "45.45.45.45:45", reqAddr) + + // Test timeout + blockStream := &mockStream{} + blockStream.EXPECT().SetReadDeadline(mock.Anything).Return(nil) + blockStream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { + time.Sleep(2 * time.Second) + return 0, io.EOF + }) + reqAddr = "66.66.66.66:66" + assert.True(t, sniffer.Check(false, reqAddr)) + putback, err = sniffer.TCP(blockStream, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, []byte{}, putback) + assert.Equal(t, "66.66.66.66:66", reqAddr) +} + +func TestSnifferUDP(t *testing.T) { + sniffer := &Sniffer{ + Timeout: 1 * time.Second, + RewriteDomain: false, + } + + // Test QUIC + reqAddr := "2.3.4.5:443" + assert.True(t, sniffer.Check(true, reqAddr)) + pkt, err := base64.StdEncoding.DecodeString("ygAAAAEIwugWgPS7ulYAAES8hY891uwgGE9GG4CPOLd+nsDe28raso24lCSFmlFwYQG1uF39ikbL13/R9ZTghYmTl+jEbr6F9TxxRiOgpTmKRmh6aKZiIiVfy5pVRckovaI8lq0WRoW9xoFNTyYtQP8TVJ3bLCK+zUqpquEQSyWf7CE43ywayyMpE9UlIoPXFWCoopXLM1SvzdQ+17P51N9KR7m4emti4DWWTBLMQOvrwd2HEEkbiZdRO1wf6ZXJlIat5dN0R/6uod60OFPO+u+awvq67MoMReC7+5I/xWI+xx6o4JpnZNn6YPG8Gqi8hS6doNcAAdtD8h5eMLuHCCgkpX3QVjjfWtcOhtw9xKjU43HhUPwzUTv+JDLgwuTQCTmlfYlb3B+pk4b2I9si0tJ0SBuYaZ2VQPtZbj2hpGXw3gn11pbN8xsbKkQL50+Scd4dGJxWQlGaJHeaU5WOCkxLXc635z8m5XO/CBHVYPGp4pfwfwNUgbe5WF+3MaUIlDB8dMfsnrO0BmZPo379jVx0SFLTAiS8wAdHib1WNEY8qKYnTWuiyxYg1GZEhJt0nXmI+8f0eJq42DgHBWC+Rf5rRBr/Sf25o3mFAmTUaul0Woo9/CIrpT73B63N91xd9A77i4ru995YG8l9Hen+eLtpDU9Q9376nwMDYBzeYG9U/Rn0Urbm6q4hmAgV/xlNJ2rAyDS+yLnwqD6I0PRy8bZJEttcidb/SkOyrpgMiAzWeT+SO+c/k+Y8H0UTRa05faZUrhuUaym9wAcaIVRA6nFI+fejfjVp+7afFv+kWn3vCqQEij+CRHuxkltrixZMD2rfYj6NUW7TTYBtPRtuV/V0ZIDjRR26vr4K+0D84+l3c0mA/l6nmpP5kkco3nmpdjtQN6sGXL7+5o0nnsftX5d6/n5mLyEpP+AEDl1zk3iqkS62RsITwql6DMMoGbSDdUpMclCIeM0vlo3CkxGMO7QA9ruVeNddkL3EWMivl+uxO43sXEEqYQHVl4N75y63t05GOf7/gm9Kb/BJ8MpG9ViEkVYaskQCzi3D8bVpzo8FfTj8te8B6c3ikc/cm7r8k0ZcZpr+YiLGDYq+0ilHxpqJfmq8dPkSvxdzLcUSvy7+LMQ/TTobRSF7L4JhtDKck0+00vl9H35Tkh9N+MsVtpKdWyoqZ4XaK2Nx1M6AieczXpdFc0y7lYPoUfF4IeW8WzeVUclol5ElYjkyFz/lDOGAe1bF2g5AYaGWCPiGleVZknNdD5ihB8W8Mfkt1pEwq2S97AHrppqkf/VoIfZzeqH8wUFw8fDDrZIpnoa0rW7HfwIQaqJhPCyB9Z6TVbV4x9UWmaHfVAcinCK/7o10dtaj3rvEqcUC/iPceGq3Tqv/p9GGNJ+Ci2JBjXqNxYr893Llk75VdPD9pM6y1SM0P80oXNy32VMtafkFFST8GpvvqWcxUJ93kzaY8RmU1g3XFOImSU2utU6+FUQ2Pn5uLwcfT2cTYfTpPGh+WXjSbZ6trqdEMEsLHybuPo2UN4WpVLXVQma3kSaHQggcLlEip8GhEUAy/xCb2eKqhI4HkDpDjwDnDVKufWlnRaOHf58cc8Woi+WT8JTOkHC+nBEG6fKRPHDG08U5yayIQIjI") + assert.NoError(t, err) + err = sniffer.UDP(pkt, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, "www.notion.so:443", reqAddr) + + // Test unrecognized + pkt = []byte("oh my sweet summer child") + reqAddr = "90.90.90.90:90" + assert.True(t, sniffer.Check(true, reqAddr)) + err = sniffer.UDP(pkt, &reqAddr) + assert.NoError(t, err) + assert.Equal(t, "90.90.90.90:90", reqAddr) +} From b481b49a28f69cc17dd9c0857567fd80d5a567fb Mon Sep 17 00:00:00 2001 From: Toby Date: Sat, 29 Jun 2024 17:46:04 -0700 Subject: [PATCH 5/6] chore: import format fix --- app/cmd/server.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/cmd/server.go b/app/cmd/server.go index bca4c80..fdd9a54 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -15,8 +15,6 @@ import ( "strings" "time" - "github.com/apernet/hysteria/extras/v2/sniff" - "github.com/caddyserver/certmagic" "github.com/libdns/cloudflare" "github.com/libdns/duckdns" @@ -36,6 +34,7 @@ import ( "github.com/apernet/hysteria/extras/v2/masq" "github.com/apernet/hysteria/extras/v2/obfs" "github.com/apernet/hysteria/extras/v2/outbounds" + "github.com/apernet/hysteria/extras/v2/sniff" "github.com/apernet/hysteria/extras/v2/trafficlogger" ) From deeeafd8d7726760c66390b48e70dff9805b18e9 Mon Sep 17 00:00:00 2001 From: Toby Date: Sun, 30 Jun 2024 12:04:59 -0700 Subject: [PATCH 6/6] feat: allow specifying port ranges for sniffing --- app/cmd/server.go | 18 +++- app/cmd/server_test.go | 2 + app/cmd/server_test.yaml | 2 + extras/sniff/sniff.go | 25 ++++- extras/sniff/sniff_test.go | 41 +++++---- extras/transport/udphop/addr.go | 39 ++------ extras/transport/udphop/addr_test.go | 132 --------------------------- extras/utils/portunion.go | 107 ++++++++++++++++++++++ extras/utils/portunion_test.go | 92 +++++++++++++++++++ 9 files changed, 276 insertions(+), 182 deletions(-) delete mode 100644 extras/transport/udphop/addr_test.go create mode 100644 extras/utils/portunion.go create mode 100644 extras/utils/portunion_test.go diff --git a/app/cmd/server.go b/app/cmd/server.go index fdd9a54..b45fb15 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -36,6 +36,7 @@ import ( "github.com/apernet/hysteria/extras/v2/outbounds" "github.com/apernet/hysteria/extras/v2/sniff" "github.com/apernet/hysteria/extras/v2/trafficlogger" + eUtils "github.com/apernet/hysteria/extras/v2/utils" ) const ( @@ -185,6 +186,8 @@ type serverConfigSniff struct { Enable bool `mapstructure:"enable"` Timeout time.Duration `mapstructure:"timeout"` RewriteDomain bool `mapstructure:"rewriteDomain"` + TCPPorts string `mapstructure:"tcpPorts"` + UDPPorts string `mapstructure:"udpPorts"` } type serverConfigACL struct { @@ -551,10 +554,23 @@ func serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.P func (c *serverConfig) fillRequestHook(hyConfig *server.Config) error { if c.Sniff.Enable { - hyConfig.RequestHook = &sniff.Sniffer{ + s := &sniff.Sniffer{ Timeout: c.Sniff.Timeout, RewriteDomain: c.Sniff.RewriteDomain, } + if c.Sniff.TCPPorts != "" { + s.TCPPorts = eUtils.ParsePortUnion(c.Sniff.TCPPorts) + if s.TCPPorts == nil { + return configError{Field: "sniff.tcpPorts", Err: errors.New("invalid port union")} + } + } + if c.Sniff.UDPPorts != "" { + s.UDPPorts = eUtils.ParsePortUnion(c.Sniff.UDPPorts) + if s.UDPPorts == nil { + return configError{Field: "sniff.udpPorts", Err: errors.New("invalid port union")} + } + } + hyConfig.RequestHook = s } return nil } diff --git a/app/cmd/server_test.go b/app/cmd/server_test.go index bd46681..bb2d12a 100644 --- a/app/cmd/server_test.go +++ b/app/cmd/server_test.go @@ -115,6 +115,8 @@ func TestServerConfig(t *testing.T) { Enable: true, Timeout: 1 * time.Second, RewriteDomain: true, + TCPPorts: "80,443,1000-2000", + UDPPorts: "443", }, ACL: serverConfigACL{ File: "chnroute.txt", diff --git a/app/cmd/server_test.yaml b/app/cmd/server_test.yaml index 343b0a9..ff0bf52 100644 --- a/app/cmd/server_test.yaml +++ b/app/cmd/server_test.yaml @@ -87,6 +87,8 @@ sniff: enable: true timeout: 1s rewriteDomain: true + tcpPorts: 80,443,1000-2000 + udpPorts: 443 acl: file: chnroute.txt diff --git a/extras/sniff/sniff.go b/extras/sniff/sniff.go index 68b3fbc..e0c94d4 100644 --- a/extras/sniff/sniff.go +++ b/extras/sniff/sniff.go @@ -5,6 +5,7 @@ import ( "io" "net" "net/http" + "strconv" "strings" "time" @@ -13,6 +14,7 @@ import ( "github.com/apernet/hysteria/core/v2/server" quicInternal "github.com/apernet/hysteria/extras/v2/sniff/internal/quic" + "github.com/apernet/hysteria/extras/v2/utils" ) const ( @@ -29,6 +31,8 @@ var _ server.RequestHook = (*Sniffer)(nil) type Sniffer struct { Timeout time.Duration RewriteDomain bool // Whether to rewrite the address even when it's already a domain + TCPPorts utils.PortUnion + UDPPorts utils.PortUnion } func (h *Sniffer) isDomain(addr string) bool { @@ -62,7 +66,26 @@ func (h *Sniffer) isTLS(buf []byte) bool { func (h *Sniffer) Check(isUDP bool, reqAddr string) bool { // @ means it's internal (e.g. speed test) - return !strings.HasPrefix(reqAddr, "@") && (h.RewriteDomain || !h.isDomain(reqAddr)) + if strings.HasPrefix(reqAddr, "@") { + return false + } + host, port, err := net.SplitHostPort(reqAddr) + if err != nil { + return false + } + if !h.RewriteDomain && net.ParseIP(host) == nil { + // Is a domain and domain rewriting is disabled + return false + } + portNum, err := strconv.Atoi(port) + if err != nil { + return false + } + if isUDP { + return h.UDPPorts == nil || h.UDPPorts.Contains(uint16(portNum)) + } else { + return h.TCPPorts == nil || h.TCPPorts.Contains(uint16(portNum)) + } } func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { diff --git a/extras/sniff/sniff_test.go b/extras/sniff/sniff_test.go index fb86c3b..a22784e 100644 --- a/extras/sniff/sniff_test.go +++ b/extras/sniff/sniff_test.go @@ -6,10 +6,35 @@ import ( "testing" "time" + "github.com/apernet/hysteria/extras/v2/utils" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) +func TestSnifferCheck(t *testing.T) { + sniffer := &Sniffer{ + Timeout: 1 * time.Second, + RewriteDomain: false, + TCPPorts: nil, // nil = all + UDPPorts: nil, // nil = all + } + + assert.True(t, sniffer.Check(false, "1.1.1.1:80")) + assert.False(t, sniffer.Check(false, "example.com:443")) + + sniffer.RewriteDomain = true + assert.True(t, sniffer.Check(false, "example.com:443")) + + sniffer.TCPPorts = []utils.PortRange{{80, 80}} + assert.True(t, sniffer.Check(false, "google.com:80")) + assert.False(t, sniffer.Check(false, "google.com:443")) + + sniffer.UDPPorts = []utils.PortRange{{443, 443}} + assert.True(t, sniffer.Check(true, "google.com:443")) + assert.False(t, sniffer.Check(true, "google.com:80")) +} + func TestSnifferTCP(t *testing.T) { sniffer := &Sniffer{ Timeout: 1 * time.Second, @@ -40,27 +65,16 @@ func TestSnifferTCP(t *testing.T) { // Rewrite IP to domain reqAddr := "111.111.111.111:80" - assert.True(t, sniffer.Check(false, reqAddr)) putback, err := sniffer.TCP(stream, &reqAddr) assert.NoError(t, err) assert.Equal(t, *buf, putback) assert.Equal(t, "example.com:80", reqAddr) - // Do not rewrite if it's already a domain - index = 0 - reqAddr = "gulag.cc:443" - assert.False(t, sniffer.Check(false, reqAddr)) - - // Turn on rewrite and now it should rewrite - sniffer.RewriteDomain = true - assert.True(t, sniffer.Check(false, reqAddr)) - // Test TLS *buf, err = base64.StdEncoding.DecodeString("FgMBARcBAAETAwPJL2jlt1OAo+Rslkjv/aqKiTthKMaCKg2Gvd+uALDbDCDdY+UIk8ouadEB9fC3j52Y1i7SJZqGIgBRIS6kKieYrAAoEwITAcAswCvAMMAvwCTAI8AowCfACsAJwBTAEwCdAJwAPQA8ADUALwEAAKIAAAAOAAwAAAlpcGluZm8uaW8ABQAFAQAAAAAAKwAJCAMEAwMDAgMBAA0AGgAYCAQIBQgGBAEFAQIBBAMFAwIDAgIGAQYDACMAAAAKAAgABgAdABcAGAAQAAsACQhodHRwLzEuMQAzACYAJAAdACBguQbqNJNyamYxYcrBFpBP7pWv5TgZsP9gwGtMYNKVBQAxAAAAFwAA/wEAAQAALQACAQE=") assert.NoError(t, err) index = 0 reqAddr = "222.222.222.222:443" - assert.True(t, sniffer.Check(false, reqAddr)) putback, err = sniffer.TCP(stream, &reqAddr) assert.NoError(t, err) assert.Equal(t, *buf, putback) @@ -70,7 +84,6 @@ func TestSnifferTCP(t *testing.T) { *buf = []byte("Wait It's All Ohio? Always Has Been.") index = 0 reqAddr = "123.123.123.123:123" - assert.True(t, sniffer.Check(false, reqAddr)) putback, err = sniffer.TCP(stream, &reqAddr) assert.NoError(t, err) assert.Equal(t, *buf, putback) @@ -80,7 +93,6 @@ func TestSnifferTCP(t *testing.T) { *buf = []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a") index = 0 reqAddr = "45.45.45.45:45" - assert.True(t, sniffer.Check(false, reqAddr)) putback, err = sniffer.TCP(stream, &reqAddr) assert.NoError(t, err) assert.Equal(t, []byte("\x01\x02\x03"), putback) @@ -94,7 +106,6 @@ func TestSnifferTCP(t *testing.T) { return 0, io.EOF }) reqAddr = "66.66.66.66:66" - assert.True(t, sniffer.Check(false, reqAddr)) putback, err = sniffer.TCP(blockStream, &reqAddr) assert.NoError(t, err) assert.Equal(t, []byte{}, putback) @@ -109,7 +120,6 @@ func TestSnifferUDP(t *testing.T) { // Test QUIC reqAddr := "2.3.4.5:443" - assert.True(t, sniffer.Check(true, reqAddr)) pkt, err := base64.StdEncoding.DecodeString("ygAAAAEIwugWgPS7ulYAAES8hY891uwgGE9GG4CPOLd+nsDe28raso24lCSFmlFwYQG1uF39ikbL13/R9ZTghYmTl+jEbr6F9TxxRiOgpTmKRmh6aKZiIiVfy5pVRckovaI8lq0WRoW9xoFNTyYtQP8TVJ3bLCK+zUqpquEQSyWf7CE43ywayyMpE9UlIoPXFWCoopXLM1SvzdQ+17P51N9KR7m4emti4DWWTBLMQOvrwd2HEEkbiZdRO1wf6ZXJlIat5dN0R/6uod60OFPO+u+awvq67MoMReC7+5I/xWI+xx6o4JpnZNn6YPG8Gqi8hS6doNcAAdtD8h5eMLuHCCgkpX3QVjjfWtcOhtw9xKjU43HhUPwzUTv+JDLgwuTQCTmlfYlb3B+pk4b2I9si0tJ0SBuYaZ2VQPtZbj2hpGXw3gn11pbN8xsbKkQL50+Scd4dGJxWQlGaJHeaU5WOCkxLXc635z8m5XO/CBHVYPGp4pfwfwNUgbe5WF+3MaUIlDB8dMfsnrO0BmZPo379jVx0SFLTAiS8wAdHib1WNEY8qKYnTWuiyxYg1GZEhJt0nXmI+8f0eJq42DgHBWC+Rf5rRBr/Sf25o3mFAmTUaul0Woo9/CIrpT73B63N91xd9A77i4ru995YG8l9Hen+eLtpDU9Q9376nwMDYBzeYG9U/Rn0Urbm6q4hmAgV/xlNJ2rAyDS+yLnwqD6I0PRy8bZJEttcidb/SkOyrpgMiAzWeT+SO+c/k+Y8H0UTRa05faZUrhuUaym9wAcaIVRA6nFI+fejfjVp+7afFv+kWn3vCqQEij+CRHuxkltrixZMD2rfYj6NUW7TTYBtPRtuV/V0ZIDjRR26vr4K+0D84+l3c0mA/l6nmpP5kkco3nmpdjtQN6sGXL7+5o0nnsftX5d6/n5mLyEpP+AEDl1zk3iqkS62RsITwql6DMMoGbSDdUpMclCIeM0vlo3CkxGMO7QA9ruVeNddkL3EWMivl+uxO43sXEEqYQHVl4N75y63t05GOf7/gm9Kb/BJ8MpG9ViEkVYaskQCzi3D8bVpzo8FfTj8te8B6c3ikc/cm7r8k0ZcZpr+YiLGDYq+0ilHxpqJfmq8dPkSvxdzLcUSvy7+LMQ/TTobRSF7L4JhtDKck0+00vl9H35Tkh9N+MsVtpKdWyoqZ4XaK2Nx1M6AieczXpdFc0y7lYPoUfF4IeW8WzeVUclol5ElYjkyFz/lDOGAe1bF2g5AYaGWCPiGleVZknNdD5ihB8W8Mfkt1pEwq2S97AHrppqkf/VoIfZzeqH8wUFw8fDDrZIpnoa0rW7HfwIQaqJhPCyB9Z6TVbV4x9UWmaHfVAcinCK/7o10dtaj3rvEqcUC/iPceGq3Tqv/p9GGNJ+Ci2JBjXqNxYr893Llk75VdPD9pM6y1SM0P80oXNy32VMtafkFFST8GpvvqWcxUJ93kzaY8RmU1g3XFOImSU2utU6+FUQ2Pn5uLwcfT2cTYfTpPGh+WXjSbZ6trqdEMEsLHybuPo2UN4WpVLXVQma3kSaHQggcLlEip8GhEUAy/xCb2eKqhI4HkDpDjwDnDVKufWlnRaOHf58cc8Woi+WT8JTOkHC+nBEG6fKRPHDG08U5yayIQIjI") assert.NoError(t, err) err = sniffer.UDP(pkt, &reqAddr) @@ -119,7 +129,6 @@ func TestSnifferUDP(t *testing.T) { // Test unrecognized pkt = []byte("oh my sweet summer child") reqAddr = "90.90.90.90:90" - assert.True(t, sniffer.Check(true, reqAddr)) err = sniffer.UDP(pkt, &reqAddr) assert.NoError(t, err) assert.Equal(t, "90.90.90.90:90", reqAddr) diff --git a/extras/transport/udphop/addr.go b/extras/transport/udphop/addr.go index 3c70472..afde26a 100644 --- a/extras/transport/udphop/addr.go +++ b/extras/transport/udphop/addr.go @@ -3,8 +3,8 @@ package udphop import ( "fmt" "net" - "strconv" - "strings" + + "github.com/apernet/hysteria/extras/v2/utils" ) type InvalidPortError struct { @@ -57,36 +57,11 @@ func ResolveUDPHopAddr(addr string) (*UDPHopAddr, error) { PortStr: portStr, } - portStrs := strings.Split(portStr, ",") - for _, portStr := range portStrs { - if strings.Contains(portStr, "-") { - // Port range - portRange := strings.Split(portStr, "-") - if len(portRange) != 2 { - return nil, InvalidPortError{portStr} - } - start, err := strconv.ParseUint(portRange[0], 10, 16) - if err != nil { - return nil, InvalidPortError{portStr} - } - end, err := strconv.ParseUint(portRange[1], 10, 16) - if err != nil { - return nil, InvalidPortError{portStr} - } - if start > end { - start, end = end, start - } - for i := start; i <= end; i++ { - result.Ports = append(result.Ports, uint16(i)) - } - } else { - // Single port - port, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { - return nil, InvalidPortError{portStr} - } - result.Ports = append(result.Ports, uint16(port)) - } + pu := utils.ParsePortUnion(portStr) + if pu == nil { + return nil, InvalidPortError{portStr} } + result.Ports = pu.Ports() + return result, nil } diff --git a/extras/transport/udphop/addr_test.go b/extras/transport/udphop/addr_test.go deleted file mode 100644 index 94a1016..0000000 --- a/extras/transport/udphop/addr_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package udphop - -import ( - "net" - "reflect" - "testing" -) - -func TestResolveUDPHopAddr(t *testing.T) { - type args struct { - addr string - } - tests := []struct { - name string - args args - want *UDPHopAddr - wantErr bool - }{ - { - name: "empty", - args: args{ - addr: "", - }, - want: nil, - wantErr: true, - }, - { - name: "no port", - args: args{ - addr: "8.8.8.8", - }, - want: nil, - wantErr: true, - }, - { - name: "single port", - args: args{ - addr: "8.8.4.4:1234", - }, - want: &UDPHopAddr{ - IP: net.ParseIP("8.8.4.4"), - Ports: []uint16{1234}, - PortStr: "1234", - }, - wantErr: false, - }, - { - name: "multiple ports", - args: args{ - addr: "8.8.3.3:1234,5678,9012", - }, - want: &UDPHopAddr{ - IP: net.ParseIP("8.8.3.3"), - Ports: []uint16{1234, 5678, 9012}, - PortStr: "1234,5678,9012", - }, - wantErr: false, - }, - { - name: "port range", - args: args{ - addr: "1.2.3.4:1234-1240", - }, - want: &UDPHopAddr{ - IP: net.ParseIP("1.2.3.4"), - Ports: []uint16{1234, 1235, 1236, 1237, 1238, 1239, 1240}, - PortStr: "1234-1240", - }, - wantErr: false, - }, - { - name: "port range reversed", - args: args{ - addr: "123.123.123.123:9990-9980", - }, - want: &UDPHopAddr{ - IP: net.ParseIP("123.123.123.123"), - Ports: []uint16{9980, 9981, 9982, 9983, 9984, 9985, 9986, 9987, 9988, 9989, 9990}, - PortStr: "9990-9980", - }, - wantErr: false, - }, - { - name: "port range & port list", - args: args{ - addr: "9.9.9.9:1234-1236,5678,9012", - }, - want: &UDPHopAddr{ - IP: net.ParseIP("9.9.9.9"), - Ports: []uint16{1234, 1235, 1236, 5678, 9012}, - PortStr: "1234-1236,5678,9012", - }, - wantErr: false, - }, - { - name: "invalid port", - args: args{ - addr: "5.5.5.5:1234,bs", - }, - want: nil, - wantErr: true, - }, - { - name: "invalid port range 1", - args: args{ - addr: "6.6.6.6:7788-bbss", - }, - want: nil, - wantErr: true, - }, - { - name: "invalid port range 2", - args: args{ - addr: "1.0.0.1:8899-9002-9005", - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ResolveUDPHopAddr(tt.args.addr) - if (err != nil) != tt.wantErr { - t.Errorf("ParseUDPHopAddr() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ParseUDPHopAddr() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/extras/utils/portunion.go b/extras/utils/portunion.go new file mode 100644 index 0000000..20a31d0 --- /dev/null +++ b/extras/utils/portunion.go @@ -0,0 +1,107 @@ +package utils + +import ( + "sort" + "strconv" + "strings" +) + +// PortUnion is a collection of multiple port ranges. +type PortUnion []PortRange + +// PortRange represents a range of ports. +// Start and End are inclusive. [Start, End] +type PortRange struct { + Start, End uint16 +} + +// ParsePortUnion parses a string of comma-separated port ranges (or single ports) into a PortUnion. +// Returns nil if the input is invalid. +// The returned PortUnion is guaranteed to be normalized. +func ParsePortUnion(s string) PortUnion { + if s == "all" || s == "*" { + // Wildcard special case + return PortUnion{PortRange{0, 65535}} + } + var result PortUnion + portStrs := strings.Split(s, ",") + for _, portStr := range portStrs { + if strings.Contains(portStr, "-") { + // Port range + portRange := strings.Split(portStr, "-") + if len(portRange) != 2 { + return nil + } + start, err := strconv.ParseUint(portRange[0], 10, 16) + if err != nil { + return nil + } + end, err := strconv.ParseUint(portRange[1], 10, 16) + if err != nil { + return nil + } + if start > end { + start, end = end, start + } + result = append(result, PortRange{uint16(start), uint16(end)}) + } else { + // Single port + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return nil + } + result = append(result, PortRange{uint16(port), uint16(port)}) + } + } + if result == nil { + return nil + } + return result.Normalize() +} + +// Normalize normalizes a PortUnion. +// No overlapping ranges, ranges are sorted from low to high. +func (u PortUnion) Normalize() PortUnion { + if len(u) == 0 { + return u + } + sort.Slice(u, func(i, j int) bool { + if u[i].Start == u[j].Start { + return u[i].End < u[j].End + } + return u[i].Start < u[j].Start + }) + normalized := PortUnion{u[0]} + for _, current := range u[1:] { + last := &normalized[len(normalized)-1] + if current.Start <= last.End+1 { + if current.End > last.End { + last.End = current.End + } + } else { + normalized = append(normalized, current) + } + } + return normalized +} + +// Ports returns all ports in the PortUnion as a slice. +func (u PortUnion) Ports() []uint16 { + var ports []uint16 + for _, r := range u { + for i := r.Start; i <= r.End; i++ { + ports = append(ports, i) + } + } + return ports +} + +// Contains returns true if the PortUnion contains the given port. +func (u PortUnion) Contains(port uint16) bool { + for _, r := range u { + if port >= r.Start && port <= r.End { + return true + } + } + return false +} diff --git a/extras/utils/portunion_test.go b/extras/utils/portunion_test.go new file mode 100644 index 0000000..551bae1 --- /dev/null +++ b/extras/utils/portunion_test.go @@ -0,0 +1,92 @@ +package utils + +import ( + "reflect" + "testing" +) + +func TestParsePortUnion(t *testing.T) { + tests := []struct { + name string + s string + want PortUnion + }{ + { + name: "empty", + s: "", + want: nil, + }, + { + name: "all 1", + s: "all", + want: PortUnion{{0, 65535}}, + }, + { + name: "all 2", + s: "*", + want: PortUnion{{0, 65535}}, + }, + { + name: "single port", + s: "1234", + want: PortUnion{{1234, 1234}}, + }, + { + name: "multiple ports (unsorted)", + s: "5678,1234,9012", + want: PortUnion{{1234, 1234}, {5678, 5678}, {9012, 9012}}, + }, + { + name: "one range", + s: "1234-1240", + want: PortUnion{{1234, 1240}}, + }, + { + name: "one range (reversed)", + s: "1240-1234", + want: PortUnion{{1234, 1240}}, + }, + { + name: "multiple ports and ranges (reversed, unsorted, overlapping)", + s: "5678,1200-1236,9100-9012,1234-1240", + want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}}, + }, + { + name: "invalid 1", + s: "1234-", + want: nil, + }, + { + name: "invalid 2", + s: "1234-ggez", + want: nil, + }, + { + name: "invalid 3", + s: "233,", + want: nil, + }, + { + name: "invalid 4", + s: "1234-1240-1250", + want: nil, + }, + { + name: "invalid 5", + s: "-,,", + want: nil, + }, + { + name: "invalid 6", + s: "http", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParsePortUnion(tt.s); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParsePortUnion() = %v, want %v", got, tt.want) + } + }) + } +}