Sniffing: Support QUICv2 (#6695)

Co-authored-by: 风扇滑翔翼 <Fangliding.fshxy@outlook.com>
This commit is contained in:
Kostaand风扇滑翔翼 authored and nerpa committed 2026-09-13 10:50:32 +03:00
1 parent 50ec17492b
commit c6d9c7b591
3 files changed
+98 -82

No files matched your search

-8
View File
@@ -1,18 +1,10 @@
package quic
import (
"crypto"
"crypto/cipher"
_ "crypto/tls"
_ "unsafe"
)
type CipherSuiteTLS13 struct {
ID uint16
KeyLen int
AEAD func(key, fixedNonce []byte) cipher.AEAD
Hash crypto.Hash
}
//go:linkname AEADAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
func AEADAESGCMTLS13(key, nonceMask []byte) cipher.AEAD
+89 -74
View File
@@ -3,7 +3,6 @@ package quic
import (
"crypto"
"crypto/aes"
"crypto/tls"
"encoding/binary"
"io"
@@ -28,22 +27,43 @@ func (s SniffHeader) Domain() string {
return s.domain
}
const (
versionDraft29 uint32 = 0xff00001d
version1 uint32 = 0x1
var (
errNotQUIC = errors.New("not quic")
errNotQUICInitial = errors.New("not initial packet")
)
type quicVersionSpec struct {
ver uint32
typeInitial byte
initialSalt []byte
labelPrefix string
}
var (
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
quicSalt = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
initialSuite = &CipherSuiteTLS13{
ID: tls.TLS_AES_128_GCM_SHA256,
KeyLen: 16,
AEAD: AEADAESGCMTLS13,
Hash: crypto.SHA256,
quicDraft29 = quicVersionSpec{
ver: 0xff00001d,
typeInitial: 0b00,
initialSalt: []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99},
labelPrefix: "quic",
}
quicV1 = quicVersionSpec{
ver: 0x1,
typeInitial: 0b00,
initialSalt: []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a},
labelPrefix: "quic",
}
quicV2 = quicVersionSpec{
ver: 0x6b3343cf,
typeInitial: 0b01,
initialSalt: []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9},
labelPrefix: "quicv2",
}
quicVersionSpecMap = map[uint32]*quicVersionSpec{
quicDraft29.ver: &quicDraft29,
quicV1.ver: &quicV1,
quicV2.ver: &quicV2,
}
errNotQuic = errors.New("not quic")
errNotQuicInitial = errors.New("not initial packet")
)
func SniffQUIC(b []byte) (*SniffHeader, error) {
@@ -63,60 +83,61 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
buffer := buf.FromBytes(b)
typeByte, err := buffer.ReadByte()
if err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
isLongHeader := typeByte&0x80 > 0
if !isLongHeader || typeByte&0x40 == 0 {
return nil, errNotQuicInitial
return nil, errNotQUICInitial
}
vb, err := buffer.ReadBytes(4)
if err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
versionNumber := binary.BigEndian.Uint32(vb)
if versionNumber != 0 && typeByte&0x40 == 0 {
return nil, errNotQuic
} else if versionNumber != versionDraft29 && versionNumber != version1 {
return nil, errNotQuic
var s *quicVersionSpec
if v, ok := quicVersionSpecMap[versionNumber]; ok {
s = v
} else {
return nil, errNotQUIC
}
packetType := (typeByte & 0x30) >> 4
isQuicInitial := packetType == 0x0
var destConnID []byte
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
} else if destConnID, err = buffer.ReadBytes(int32(l)); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
if l, err := buffer.ReadByte(); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
} else if common.Error2(buffer.ReadBytes(int32(l))) != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := readShortQuicVarint(buffer)
packetType := (typeByte & 0x30) >> 4
isQUICInitial := packetType == s.typeInitial
if isQUICInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
tokenLen, err := readShortQUICVarint(buffer)
if err != nil || tokenLen > int32(len(b)) {
return nil, errNotQuic
return nil, errNotQUIC
}
if _, err = buffer.ReadBytes(tokenLen); err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
}
packetLen, err := readShortQuicVarint(buffer)
packetLen, err := readShortQUICVarint(buffer)
if err != nil {
return nil, errNotQuic
return nil, errNotQUIC
}
// packetLen is impossible to be shorter than this
if packetLen < 4 {
return nil, errNotQuic
return nil, errNotQUIC
}
hdrLen := len(b) - int(buffer.Len())
@@ -125,26 +146,22 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
}
restPayload := b[hdrLen+int(packetLen):]
if !isQuicInitial { // Skip this packet if it's not initial packet
if !isQUICInitial { // Skip this packet if it's not initial packet
b = restPayload
continue
}
var salt []byte
if versionNumber == version1 {
salt = quicSalt
} else {
salt = quicSaltOld
}
salt := s.initialSalt
label := s.labelPrefix
initialSecret := hkdf.Extract(crypto.SHA256.New, destConnID, salt)
secret := hkdfExpandLabel(crypto.SHA256, initialSecret, []byte{}, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(initialSuite.Hash, secret, []byte{}, "quic hp", initialSuite.KeyLen)
secret := hkdfExpandLabel(initialSecret, "client in", crypto.SHA256.Size())
hpKey := hkdfExpandLabel(secret, label+" hp", 16)
block, err := aes.NewCipher(hpKey)
if err != nil {
return nil, err
}
if len(b) < hdrLen+4+block.BlockSize() {
return nil, errNotQuic
return nil, errNotQUIC
}
cache.Clear()
mask := cache.Extend(int32(block.BlockSize()))
@@ -155,8 +172,8 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
b[hdrLen+i] ^= mask[i+1]
}
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
key := hkdfExpandLabel(secret, label+" key", 16)
iv := hkdfExpandLabel(secret, label+" iv", 12)
cipher := AEADAESGCMTLS13(key, iv)
nonce := cache.Extend(int32(cipher.NonceSize()))
@@ -181,44 +198,44 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
case 0x00: // PADDING frame
case 0x01: // PING frame
case 0x02, 0x03: // ACK frame
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Largest Acknowledged
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Largest Acknowledged
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Delay
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Delay
return nil, io.ErrUnexpectedEOF
}
ackRangeCount, err := readShortQuicVarint(buffer) // Field: ACK Range Count
ackRangeCount, err := readShortQUICVarint(buffer) // Field: ACK Range Count
if err != nil {
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: First ACK Range
if _, err = readShortQUICVarint(buffer); err != nil { // Field: First ACK Range
return nil, io.ErrUnexpectedEOF
}
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> Gap
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> Gap
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ACK Range -> ACK Range Length
return nil, io.ErrUnexpectedEOF
}
}
if frameType == 0x03 {
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
if _, err = readShortQUICVarint(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
if _, err = readShortQUICVarint(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
return nil, io.ErrUnexpectedEOF
}
}
case 0x06: // CRYPTO frame, we will use this frame
offset, err := readShortQuicVarint(buffer) // Field: Offset
offset, err := readShortQUICVarint(buffer) // Field: Offset
if err != nil {
return nil, io.ErrUnexpectedEOF
}
length, err := readShortQuicVarint(buffer) // Field: Length
length, err := readShortQUICVarint(buffer) // Field: Length
if err != nil || length > buffer.Len() {
return nil, io.ErrUnexpectedEOF
}
@@ -234,13 +251,13 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, io.ErrUnexpectedEOF
}
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Error Code
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Error Code
return nil, io.ErrUnexpectedEOF
}
if _, err = readShortQuicVarint(buffer); err != nil { // Field: Frame Type
if _, err = readShortQUICVarint(buffer); err != nil { // Field: Frame Type
return nil, io.ErrUnexpectedEOF
}
length, err := readShortQuicVarint(buffer) // Field: Reason Phrase Length
length, err := readShortQUICVarint(buffer) // Field: Reason Phrase Length
if err != nil {
return nil, io.ErrUnexpectedEOF
}
@@ -250,7 +267,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
default:
// Only above frame types are permitted in initial packet.
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
return nil, errNotQuicInitial
return nil, errNotQUICInitial
}
}
@@ -268,35 +285,33 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, protocol.ErrProtoNeedMoreData
}
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {
b := make([]byte, 3, 3+6+len(label)+1+len(context))
binary.BigEndian.PutUint16(b, uint16(length))
b[2] = uint8(6 + len(label))
b = append(b, []byte("tls13 ")...)
b = append(b, []byte(label)...)
b = b[:3+6+len(label)+1]
b[3+6+len(label)] = uint8(len(context))
b = append(b, context...)
func hkdfExpandLabel(secret []byte, label string, length int) []byte {
b := make([]byte, 0, 2+1+6+len(label)+1)
b = binary.BigEndian.AppendUint16(b, uint16(length))
b = append(b, byte(6+len(label)))
b = append(b, "tls13 "...)
b = append(b, label...)
b = append(b, 0) // context
out := make([]byte, length)
n, err := hkdf.Expand(hash.New, secret, b).Read(out)
n, err := hkdf.Expand(crypto.SHA256.New, secret, b).Read(out)
if err != nil || n != length {
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
}
return out
}
// readShortQuicVarint wraps quicvarint.Read with a max limit for length related fields.
// readShortQUICVarint wraps quicvarint.Read with a max limit for length related fields.
// we only handle QUIC Initial so these numbers should not exceed 65535
// returns int32 to reduce type conversion
func readShortQuicVarint(reader io.ByteReader) (int32, error) {
func readShortQUICVarint(reader io.ByteReader) (int32, error) {
v, err := quicvarint.Read(reader)
if err != nil {
return 0, err
}
if v > 65535 {
// not used(
return 0, errNotQuicInitial
return 0, errNotQUICInitial
}
return int32(v), nil
}
+9
View File
@@ -268,6 +268,15 @@ func TestSniffQUICPacketNumberLength4(t *testing.T) {
}
}
func TestSniffQUICv2(t *testing.T) {
pkt, err := hex.DecodeString("d26b3343cf14a04d19d18c864e168f4e929ddf60ddd774708211000044e2ae9c783da28244f30c6af9a8aeff6db7c5ae6c595b2e7d367a5e6578bbb9f7859202ac5a5cdaaf32429e7c94e4c6b9494ba1a83e3adab4c716a08e7fe38907888196905c660db7d72f461d2835e81e02f8cf976dc2f08ab3faa18939d73a3da2faca3e6be9df6ce51da00b00dcd5c64f53c6347531e509d7678aba83d8818026f8f6b955cfa95ea8ae9ba0ad0b7f01a11f424abcf490b704663dbb7e700bcecd9725974eb07d2eb67484aa9c8f361d12bb83ff88de06854c45576ff0141461164f999828407ff7ce54abb514ad75e8743c27b23801d330a8b58f75952fe8e4ea696056fc29676351c7e1d6a2d7860ab8ab2582d952c05720a39a551d24b1ce58edb6b14039e905feaa51cac9f712ee22798c6f62d186193c4b9abe7c339d528e0d20c7106a85575fe3535703e0e3bfc33ccdc4de1372466db56519c692e021e5976c0ef841222f1cad2884a6cf78f3973ce8b795a191397cea39d72080cb18f7da7da579758b7923f29c756ac89fa253302c403222bc1888bc7bc0e3f3cfd43000129d3b670b28a2f3b938f6c6c67b27a10fa5d5f92d845a78d063cfe83aa30386e8cba9d152999e56e46ade53940f228671573ca76ea36a910c01a269cf698681b7b462a9b0f467ac8083b2b1568dbb56f657f6ea4e52954520370ffa1b5e24375bfbc53a58aa2d1883e6712cdd6c10b88d69196357f93fa3856d253e01f2ce8237fbd71f27c897ef8378d6f5823af1c69334f97900da32d64359ae6a20231a8ddb56fdc694cdc7a4d2719df0494b34831f10d46f768af082ad6acb491c937ec0e9ad9c9c2aa9f5c8fab93538b4dbeead9c1ecf274fa894377482565aa827a65e6eac57c6e313a95bbc4cb597359f29a2d7ebfb528310c01ba5048a85f3611feafa239ab3ce24a5f8876771b72467b5b2a96bdda475250da97886361db71c001d5e324633246f60a19985c682ea8027aeb5885cbe1ee4f3249d273b111227ef7ba267545feaa4981c3e40cfa82f6f22e550eae2df67678d9591df1f9ffb9cd199dba82b270c0ca42f2977e12031e0ec6a61c1c635012c3f3887e9284394695a8be3ee7249600bb6828ab2e65e1193dc42b3411de0e42bd9abb4d4df7cab5fe2d0000874c08e2e3ebb5dd42129e4ed28327cb531b66e9a9cee4be3ceae38f0a57a4b333cca1a5e6807318d7c6a4b1c07bfd6787e5e73b8dbaddb959df91f41c8afeaaa2349f5d0c6f6a953a49375f72311a45d1beb536fffb032cd8b968c069688ac95dedbd65cf3c82c412e6dc8d1a3ef07c608cd9798a88b0652fdf8f6b42488d7a2aab024746b50c1e6628704e022e1fe29d6407988c78dc572f9d422789aaa9cb6c4b3dff3f9def75e29ed37f777c8d7dcfc235562f2e27736eafbedc0d42c15a2a65711e09ed21bfe69a4e6a6aceae1ae4f1f6af94715d5174c618700fcb15d65aca5b5bc2381f95c2dc1cf684add0fd2984257ae39787077d6ae4ee7b44f938d54fba7ccb3f0bf7336c4a20480b44364fc17956372dea9b182602408371a07b014b848f44f50d18e6a596d9f2ea73b89fd182c3cca573590899dc5aa4bb439925d64fbe56da8fd44c2e785db349b41e11737b1f0fae2fbf3692815b483bb492961d426f991cf0b615b12d3185ccf110f9f2ded4f611bc87ad8d167eb2e7638b1388d0c68dbfec9b56619a10c62f1a4146a617ab0802a0940234b4a3fdb1bdd0d67416ccba9ad21dd37bfd8320efe43bef743de305740f23764ad3e208b55e7de6b3343cf14a04d19d18c864e168f4e929ddf60ddd774708211000044e2423d7e3778747532056c364b468ea574954ea0d66a8e67bd93d6896837814c76d24cccfcbece575590c5c022eb13598146aee3a164f133329c278e83daab535f8a595edfccafca9fa08bf5d729d55ff3fb810cfcd075726bd3f181466e2d0ab59e26f7fe9c2a220457c31b1cd25e98a1cf2de53232049950716c2b87fe96a3be76b34ec0d8806579260f727d209c6e3a54584ceb3a2d464c666fe12778a0a1cfe3fc2a7e52cd304031cdb900194c82d781418f518a2bacfc61fdcdd9070fb569fb60fdfd927cd40e1ce5524c485524c6a19a73126f0d1b8950ec83b6f2a5601c793440eea7ec53803411bc7462d026ca4acb6bc0889304fedaad4f74888e19b4dbc9a8d628b3a33da47dab660aa032a62d6de64de1ab25010d4623fd4a64ffacc2e5e990944384df899d5f4801371222cbc96895352d31caf1c1b6928d29df14b07d4f89ef766009dfc403139988b7d5718d0779ee9d7f08f0f6da313b73c2018f8724bd92ef9b69f56fa6d7a3158e580561eb31fe3c9ac8569bcc1cdc3b4deb3b7106f147acd1c7662dfd61c3b0836bc52dcf06d6905f36c189b5fba90c90d9e06d52fe4707e97808c4115d6f9c9aac9e11dc524b20b48a73913c00a330e028fb0a69976703515c858ce5c633fcf615531f7041c536dddb24617d3153bb9d7294bef7b1e1f4d0ae05f4aae0fb7e8f8be5884ecb12d2fd45ff58cd9031b121a521271270d6660d9d3dc358f65ea3ccf4a362f21a5f62b7d96fd5ef05a0e129f435944bbc7fcb3b40134e4842af013f526a0881216fdc1b2d64829360277d10b304f4316795ba0ce90cb7f994019b4869dfafffb8952cec74f266cac503fc59d00da5c94dd120a18097d7aebe2ef7879e27e6f31b6c73935050a64432696787af7afa69c119205c5afdfde63c0f42602ccbe85035bd33e7abd0d56acc44ead9508c8d5adc2463cf2caeb16a64c7108d99364e7781a7cabd84fa24820eb6f3910a05a351d68884cd9b73b8c0c1fb57a8cda4caa1bc8126ed8a8e351a5cd2dbea787be50ca52733bc77baa13eda498ec59850745531294a7a7b80c5fe18e860b3c85193af4c733972677a369717e1ae31ea4b660bfdf0355673643b75f23d8c5cfe8e5b6167b256eb37e3661e74f477926ad9defb0b1e48eeebc8426a3e7aa920ab85df106cabbead4117b9ad81cc40974950f97b51e299881aa52045c178febc2dca090666138677402191d1fb6541d4132c2994603b5ef90f15c0c2660b84b7211afad02eec9f956196bbb655c1754b3bb6551f76f36c448dcaff9f24cc65695b91d4449424744cd8a4b6ed55ac3cc489c94efb81b9c22fbce758be2626210484c305345baab7ca070b39d806c3546f0ba098ced2636d6c25d36fdefd0ff1d78591d223cd4d1c853acccf27578f7ad3c8bc6d124fc4e02c7c0946ce31dc442e05f3752a3ab6121fa087cd1fb276aa52d900ec7c79ea48568530ae40cf4fc7d7ed6ca520b809cfaf62189ea6a262831d598f1c373e8e8d0d5f3128bf76e0c61b82bb30b685cfa359979a38f412568642d9177be7bb2c9ae51438fa4d9610f2608266c374f5892c4802f672adfc42ee9ce83d415516b68e5ae06b3a1991628290c10369729fd2f213a0347ec13d21841766e964ffe5780fLine truncated
common.Must(err)
quicHdr, err := quic.SniffQUIC(pkt)
if err != nil || quicHdr.Domain() != "test.example.com" {
t.Error("failed")
}
}
func TestSniffFakeQUICPacketWithInvalidPacketNumberLength(t *testing.T) {
pkt, err := hex.DecodeString("cb00000001081c8c6d5aeb53d54400000090709b8600000000000000000000000000000000")
common.Must(err)