From 16bfdc7720bab6f0fb11ea56e81c53b92ae5f0dd Mon Sep 17 00:00:00 2001 From: Toby Date: Sat, 29 Jun 2024 15:52:56 -0700 Subject: [PATCH] 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=