mirror of
https://github.com/apernet/hysteria.git
synced 2025-04-03 04:27:39 +03:00
Merge pull request #1134 from apernet/wip-sniff
feat: server-side sniffing for HTTP/TLS/QUIC
This commit is contained in:
commit
0ce3df4396
25 changed files with 1757 additions and 201 deletions
|
@ -34,7 +34,9 @@ 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"
|
||||
eUtils "github.com/apernet/hysteria/extras/v2/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -64,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"`
|
||||
|
@ -179,6 +182,14 @@ type serverConfigResolver struct {
|
|||
HTTPS serverConfigResolverHTTPS `mapstructure:"https"`
|
||||
}
|
||||
|
||||
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 {
|
||||
File string `mapstructure:"file"`
|
||||
Inline []string `mapstructure:"inline"`
|
||||
|
@ -541,6 +552,29 @@ 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 {
|
||||
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
|
||||
}
|
||||
|
||||
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:
|
||||
|
@ -821,6 +855,7 @@ func (c *serverConfig) Config() (*server.Config, error) {
|
|||
c.fillConn,
|
||||
c.fillTLSConfig,
|
||||
c.fillQUICConfig,
|
||||
c.fillRequestHook,
|
||||
c.fillOutboundConfig,
|
||||
c.fillBandwidthConfig,
|
||||
c.fillIgnoreClientBandwidth,
|
||||
|
|
|
@ -111,6 +111,13 @@ func TestServerConfig(t *testing.T) {
|
|||
Insecure: true,
|
||||
},
|
||||
},
|
||||
Sniff: serverConfigSniff{
|
||||
Enable: true,
|
||||
Timeout: 1 * time.Second,
|
||||
RewriteDomain: true,
|
||||
TCPPorts: "80,443,1000-2000",
|
||||
UDPPorts: "443",
|
||||
},
|
||||
ACL: serverConfigACL{
|
||||
File: "chnroute.txt",
|
||||
Inline: []string{
|
||||
|
|
|
@ -83,6 +83,13 @@ resolver:
|
|||
sni: real.stuff.net
|
||||
insecure: true
|
||||
|
||||
sniff:
|
||||
enable: true
|
||||
timeout: 1s
|
||||
rewriteDomain: true
|
||||
tcpPorts: 80,443,1000-2000
|
||||
udpPorts: 443
|
||||
|
||||
acl:
|
||||
file: chnroute.txt
|
||||
inline:
|
||||
|
|
12
app/go.mod
12
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
|
||||
|
|
24
app/go.sum
24
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=
|
||||
|
|
|
@ -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
|
||||
|
|
16
core/go.sum
16
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=
|
||||
|
|
|
@ -4,21 +4,25 @@ 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
|
||||
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/apernet/quic-go v0.44.1-0.20240520215222-bb2e53664023 // indirect
|
||||
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
|
||||
)
|
||||
|
||||
|
|
|
@ -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=
|
||||
|
|
12
extras/sniff/.mockery.yaml
Normal file
12
extras/sniff/.mockery.yaml
Normal file
|
@ -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
|
31
extras/sniff/internal/quic/LICENSE
Normal file
31
extras/sniff/internal/quic/LICENSE
Normal file
|
@ -0,0 +1,31 @@
|
|||
Author:: Cuong Manh Le <cuong.manhle.vn@gmail.com>
|
||||
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.
|
1
extras/sniff/internal/quic/README.md
Normal file
1
extras/sniff/internal/quic/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
The code here is from https://github.com/cuonglm/quicsni with various modifications.
|
105
extras/sniff/internal/quic/header.go
Normal file
105
extras/sniff/internal/quic/header.go
Normal file
|
@ -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
|
||||
}
|
193
extras/sniff/internal/quic/packet_protector.go
Normal file
193
extras/sniff/internal/quic/packet_protector.go
Normal file
|
@ -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
|
||||
}
|
94
extras/sniff/internal/quic/packet_protector_test.go
Normal file
94
extras/sniff/internal/quic/packet_protector_test.go
Normal file
|
@ -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)
|
||||
}
|
122
extras/sniff/internal/quic/payload.go
Normal file
122
extras/sniff/internal/quic/payload.go
Normal file
|
@ -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
|
||||
}
|
59
extras/sniff/internal/quic/quic.go
Normal file
59
extras/sniff/internal/quic/quic.go
Normal file
|
@ -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
|
||||
}
|
492
extras/sniff/mock_Stream.go
Normal file
492
extras/sniff/mock_Stream.go
Normal file
|
@ -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
|
||||
}
|
193
extras/sniff/sniff.go
Normal file
193
extras/sniff/sniff.go
Normal file
|
@ -0,0 +1,193 @@
|
|||
package sniff
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/apernet/hysteria/extras/v2/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
sniffDefaultTimeout = 4 * time.Second
|
||||
)
|
||||
|
||||
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
|
||||
TCPPorts utils.PortUnion
|
||||
UDPPorts utils.PortUnion
|
||||
}
|
||||
|
||||
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)
|
||||
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) {
|
||||
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
|
||||
}
|
||||
// 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) {
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
*reqAddr = net.JoinHostPort(req.Host, port)
|
||||
}
|
||||
return tr.Buffer(), nil
|
||||
} else if h.isTLS(pre) {
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
*reqAddr = net.JoinHostPort(clientHello.ServerName, port)
|
||||
}
|
||||
return pre, nil
|
||||
} else {
|
||||
// Unrecognized protocol, just return what we have
|
||||
return pre, nil
|
||||
}
|
||||
}
|
||||
|
||||
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 teeReader struct {
|
||||
Stream quic.Stream
|
||||
Pre []byte
|
||||
|
||||
buf []byte
|
||||
}
|
||||
|
||||
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.buf = append(c.buf, b[:n]...)
|
||||
return n, nil
|
||||
}
|
||||
n, err = c.Stream.Read(b)
|
||||
if n > 0 {
|
||||
c.buf = append(c.buf, b[:n]...)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *teeReader) Buffer() []byte {
|
||||
return append(c.Pre, c.buf...)
|
||||
}
|
135
extras/sniff/sniff_test.go
Normal file
135
extras/sniff/sniff_test.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package sniff
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"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,
|
||||
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"
|
||||
putback, err := sniffer.TCP(stream, &reqAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, *buf, putback)
|
||||
assert.Equal(t, "example.com:80", 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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
err = sniffer.UDP(pkt, &reqAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "90.90.90.90:90", reqAddr)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
107
extras/utils/portunion.go
Normal file
107
extras/utils/portunion.go
Normal file
|
@ -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
|
||||
}
|
92
extras/utils/portunion_test.go
Normal file
92
extras/utils/portunion_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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=
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue