Skip to content

Commit 58c4866

Browse files
j2rong4cnRPRXVigilansxiaokangwangdyhkwong
authored
QUIC sniffer: Full support for handling multiple initial packets (#4642)
Co-authored-by: RPRX <[email protected]> Co-authored-by: Vigilans <[email protected]> Co-authored-by: Shelikhoo <[email protected]> Co-authored-by: dyhkwong <[email protected]>
1 parent a608c5a commit 58c4866

File tree

7 files changed

+371
-95
lines changed

7 files changed

+371
-95
lines changed

app/dispatcher/default.go

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,21 @@ type cachedReader struct {
3333
cache buf.MultiBuffer
3434
}
3535

36-
func (r *cachedReader) Cache(b *buf.Buffer) {
37-
mb, _ := r.reader.ReadMultiBufferTimeout(time.Millisecond * 100)
36+
func (r *cachedReader) Cache(b *buf.Buffer, deadline time.Duration) error {
37+
mb, err := r.reader.ReadMultiBufferTimeout(deadline)
38+
if err != nil {
39+
return err
40+
}
3841
r.Lock()
3942
if !mb.IsEmpty() {
4043
r.cache, _ = buf.MergeMulti(r.cache, mb)
4144
}
42-
cacheLen := r.cache.Len()
43-
if cacheLen <= b.Cap() {
44-
b.Clear()
45-
} else {
46-
b.Release()
47-
*b = *buf.NewWithSize(cacheLen)
48-
}
49-
rawBytes := b.Extend(cacheLen)
45+
b.Clear()
46+
rawBytes := b.Extend(b.Cap())
5047
n := r.cache.Copy(rawBytes)
5148
b.Resize(0, int32(n))
5249
r.Unlock()
50+
return nil
5351
}
5452

5553
func (r *cachedReader) readInternal() buf.MultiBuffer {
@@ -355,7 +353,7 @@ func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.De
355353
}
356354

357355
func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, network net.Network) (SniffResult, error) {
358-
payload := buf.New()
356+
payload := buf.NewWithSize(32767)
359357
defer payload.Release()
360358

361359
sniffer := NewSniffer(ctx)
@@ -367,26 +365,33 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, netw
367365
}
368366

369367
contentResult, contentErr := func() (SniffResult, error) {
368+
cacheDeadline := 200 * time.Millisecond
370369
totalAttempt := 0
371370
for {
372371
select {
373372
case <-ctx.Done():
374373
return nil, ctx.Err()
375374
default:
376-
totalAttempt++
377-
if totalAttempt > 2 {
378-
return nil, errSniffingTimeout
379-
}
375+
cachingStartingTimeStamp := time.Now()
376+
cacheErr := cReader.Cache(payload, cacheDeadline)
377+
cachingTimeElapsed := time.Since(cachingStartingTimeStamp)
378+
cacheDeadline -= cachingTimeElapsed
380379

381-
cReader.Cache(payload)
382380
if !payload.IsEmpty() {
383381
result, err := sniffer.Sniff(ctx, payload.Bytes(), network)
384-
if err != common.ErrNoClue {
382+
switch err {
383+
case common.ErrNoClue: // No Clue: protocol not matches, and sniffer cannot determine whether there will be a match or not
384+
totalAttempt++
385+
case protocol.ErrProtoNeedMoreData: // Protocol Need More Data: protocol matches, but need more data to complete sniffing
386+
if cacheErr != nil { // Cache error (e.g. timeout) counts for failed attempt
387+
totalAttempt++
388+
}
389+
default:
385390
return result, err
386391
}
387392
}
388-
if payload.IsFull() {
389-
return nil, errUnknownContent
393+
if totalAttempt >= 2 || cacheDeadline <= 0 {
394+
return nil, errSniffingTimeout
390395
}
391396
}
392397
}

app/dispatcher/sniffer.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/xtls/xray-core/common"
77
"github.com/xtls/xray-core/common/errors"
88
"github.com/xtls/xray-core/common/net"
9+
"github.com/xtls/xray-core/common/protocol"
910
"github.com/xtls/xray-core/common/protocol/bittorrent"
1011
"github.com/xtls/xray-core/common/protocol/http"
1112
"github.com/xtls/xray-core/common/protocol/quic"
@@ -58,14 +59,17 @@ var errUnknownContent = errors.New("unknown content")
5859
func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) {
5960
var pendingSniffer []protocolSnifferWithMetadata
6061
for _, si := range s.sniffer {
61-
s := si.protocolSniffer
62+
protocolSniffer := si.protocolSniffer
6263
if si.metadataSniffer || si.network != network {
6364
continue
6465
}
65-
result, err := s(c, payload)
66+
result, err := protocolSniffer(c, payload)
6667
if err == common.ErrNoClue {
6768
pendingSniffer = append(pendingSniffer, si)
6869
continue
70+
} else if err == protocol.ErrProtoNeedMoreData { // Sniffer protocol matched, but need more data to complete sniffing
71+
s.sniffer = []protocolSnifferWithMetadata{si}
72+
return nil, err
6973
}
7074

7175
if err == nil && result != nil {

common/buf/buffer.go

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,27 @@ const (
1515

1616
var pool = bytespool.GetPool(Size)
1717

18+
// ownership represents the data owner of the buffer.
19+
type ownership uint8
20+
21+
const (
22+
managed ownership = iota
23+
unmanaged
24+
bytespools
25+
)
26+
1827
// Buffer is a recyclable allocation of a byte array. Buffer.Release() recycles
1928
// the buffer into an internal buffer pool, in order to recreate a buffer more
2029
// quickly.
2130
type Buffer struct {
2231
v []byte
2332
start int32
2433
end int32
25-
unmanaged bool
34+
ownership ownership
2635
UDP *net.Destination
2736
}
2837

29-
// New creates a Buffer with 0 length and 8K capacity.
38+
// New creates a Buffer with 0 length and 8K capacity, managed.
3039
func New() *Buffer {
3140
buf := pool.Get().([]byte)
3241
if cap(buf) >= Size {
@@ -40,7 +49,7 @@ func New() *Buffer {
4049
}
4150
}
4251

43-
// NewExisted creates a managed, standard size Buffer with an existed bytearray
52+
// NewExisted creates a standard size Buffer with an existed bytearray, managed.
4453
func NewExisted(b []byte) *Buffer {
4554
if cap(b) < Size {
4655
panic("Invalid buffer")
@@ -57,16 +66,16 @@ func NewExisted(b []byte) *Buffer {
5766
}
5867
}
5968

60-
// FromBytes creates a Buffer with an existed bytearray
69+
// FromBytes creates a Buffer with an existed bytearray, unmanaged.
6170
func FromBytes(b []byte) *Buffer {
6271
return &Buffer{
6372
v: b,
6473
end: int32(len(b)),
65-
unmanaged: true,
74+
ownership: unmanaged,
6675
}
6776
}
6877

69-
// StackNew creates a new Buffer object on stack.
78+
// StackNew creates a new Buffer object on stack, managed.
7079
// This method is for buffers that is released in the same function.
7180
func StackNew() Buffer {
7281
buf := pool.Get().([]byte)
@@ -81,18 +90,31 @@ func StackNew() Buffer {
8190
}
8291
}
8392

93+
// NewWithSize creates a Buffer with 0 length and capacity with at least the given size, bytespool's.
94+
func NewWithSize(size int32) *Buffer {
95+
return &Buffer{
96+
v: bytespool.Alloc(size),
97+
ownership: bytespools,
98+
}
99+
}
100+
84101
// Release recycles the buffer into an internal buffer pool.
85102
func (b *Buffer) Release() {
86-
if b == nil || b.v == nil || b.unmanaged {
103+
if b == nil || b.v == nil || b.ownership == unmanaged {
87104
return
88105
}
89106

90107
p := b.v
91108
b.v = nil
92109
b.Clear()
93110

94-
if cap(p) == Size {
95-
pool.Put(p)
111+
switch b.ownership {
112+
case managed:
113+
if cap(p) == Size {
114+
pool.Put(p)
115+
}
116+
case bytespools:
117+
bytespool.Free(p)
96118
}
97119
b.UDP = nil
98120
}
@@ -215,13 +237,6 @@ func (b *Buffer) Cap() int32 {
215237
return int32(len(b.v))
216238
}
217239

218-
// NewWithSize creates a Buffer with 0 length and capacity with at least the given size.
219-
func NewWithSize(size int32) *Buffer {
220-
return &Buffer{
221-
v: bytespool.Alloc(size),
222-
}
223-
}
224-
225240
// IsEmpty returns true if the buffer is empty.
226241
func (b *Buffer) IsEmpty() bool {
227242
return b.Len() == 0

common/protocol/protocol.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
package protocol // import "github.com/xtls/xray-core/common/protocol"
2+
3+
import (
4+
"errors"
5+
)
6+
7+
var ErrProtoNeedMoreData = errors.New("protocol matches, but need more data to complete sniffing")

common/protocol/quic/sniff.go

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package quic
22

33
import (
4-
"context"
54
"crypto"
65
"crypto/aes"
76
"crypto/tls"
@@ -13,6 +12,7 @@ import (
1312
"github.com/xtls/xray-core/common/buf"
1413
"github.com/xtls/xray-core/common/bytespool"
1514
"github.com/xtls/xray-core/common/errors"
15+
"github.com/xtls/xray-core/common/protocol"
1616
ptls "github.com/xtls/xray-core/common/protocol/tls"
1717
"golang.org/x/crypto/hkdf"
1818
)
@@ -47,22 +47,17 @@ var (
4747
errNotQuicInitial = errors.New("not initial packet")
4848
)
4949

50-
func SniffQUIC(b []byte) (resultReturn *SniffHeader, errorReturn error) {
51-
// In extremely rare cases, this sniffer may cause slice error
52-
// and we set recover() here to prevent crash.
53-
// TODO: Thoroughly fix this panic
54-
defer func() {
55-
if r := recover(); r != nil {
56-
errors.LogError(context.Background(), "Failed to sniff QUIC: ", r)
57-
resultReturn = nil
58-
errorReturn = common.ErrNoClue
59-
}
60-
}()
50+
func SniffQUIC(b []byte) (*SniffHeader, error) {
51+
if len(b) == 0 {
52+
return nil, common.ErrNoClue
53+
}
6154

6255
// Crypto data separated across packets
6356
cryptoLen := 0
64-
cryptoData := bytespool.Alloc(int32(len(b)))
57+
cryptoData := bytespool.Alloc(32767)
6558
defer bytespool.Free(cryptoData)
59+
cache := buf.New()
60+
defer cache.Release()
6661

6762
// Parse QUIC packets
6863
for len(b) > 0 {
@@ -105,13 +100,15 @@ func SniffQUIC(b []byte) (resultReturn *SniffHeader, errorReturn error) {
105100
return nil, errNotQuic
106101
}
107102

108-
tokenLen, err := quicvarint.Read(buffer)
109-
if err != nil || tokenLen > uint64(len(b)) {
110-
return nil, errNotQuic
111-
}
103+
if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2
104+
tokenLen, err := quicvarint.Read(buffer)
105+
if err != nil || tokenLen > uint64(len(b)) {
106+
return nil, errNotQuic
107+
}
112108

113-
if _, err = buffer.ReadBytes(int32(tokenLen)); err != nil {
114-
return nil, errNotQuic
109+
if _, err = buffer.ReadBytes(int32(tokenLen)); err != nil {
110+
return nil, errNotQuic
111+
}
115112
}
116113

117114
packetLen, err := quicvarint.Read(buffer)
@@ -130,9 +127,6 @@ func SniffQUIC(b []byte) (resultReturn *SniffHeader, errorReturn error) {
130127
continue
131128
}
132129

133-
origPNBytes := make([]byte, 4)
134-
copy(origPNBytes, b[hdrLen:hdrLen+4])
135-
136130
var salt []byte
137131
if versionNumber == version1 {
138132
salt = quicSalt
@@ -147,44 +141,34 @@ func SniffQUIC(b []byte) (resultReturn *SniffHeader, errorReturn error) {
147141
return nil, err
148142
}
149143

150-
cache := buf.New()
151-
defer cache.Release()
152-
144+
cache.Clear()
153145
mask := cache.Extend(int32(block.BlockSize()))
154146
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+16])
155147
b[0] ^= mask[0] & 0xf
156-
for i := range b[hdrLen : hdrLen+4] {
148+
packetNumberLength := int(b[0]&0x3 + 1)
149+
for i := range packetNumberLength {
157150
b[hdrLen+i] ^= mask[i+1]
158151
}
159-
packetNumberLength := b[0]&0x3 + 1
160-
if packetNumberLength != 1 {
161-
return nil, errNotQuicInitial
162-
}
163-
var packetNumber uint32
164-
{
165-
n, err := buffer.ReadByte()
166-
if err != nil {
167-
return nil, err
168-
}
169-
packetNumber = uint32(n)
170-
}
171-
172-
extHdrLen := hdrLen + int(packetNumberLength)
173-
copy(b[extHdrLen:hdrLen+4], origPNBytes[packetNumberLength:])
174-
data := b[extHdrLen : int(packetLen)+hdrLen]
175152

176153
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
177154
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
178155
cipher := AEADAESGCMTLS13(key, iv)
156+
179157
nonce := cache.Extend(int32(cipher.NonceSize()))
180-
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber))
158+
_, err = buffer.Read(nonce[len(nonce)-packetNumberLength:])
159+
if err != nil {
160+
return nil, err
161+
}
162+
163+
extHdrLen := hdrLen + packetNumberLength
164+
data := b[extHdrLen : int(packetLen)+hdrLen]
181165
decrypted, err := cipher.Open(b[extHdrLen:extHdrLen], nonce, data, b[:extHdrLen])
182166
if err != nil {
183167
return nil, err
184168
}
185169
buffer = buf.FromBytes(decrypted)
186-
for i := 0; !buffer.IsEmpty(); i++ {
187-
frameType := byte(0x0) // Default to PADDING frame
170+
for !buffer.IsEmpty() {
171+
frameType, _ := buffer.ReadByte()
188172
for frameType == 0x0 && !buffer.IsEmpty() {
189173
frameType, _ = buffer.ReadByte()
190174
}
@@ -234,13 +218,12 @@ func SniffQUIC(b []byte) (resultReturn *SniffHeader, errorReturn error) {
234218
return nil, io.ErrUnexpectedEOF
235219
}
236220
if cryptoLen < int(offset+length) {
237-
cryptoLen = int(offset + length)
238-
if len(cryptoData) < cryptoLen {
239-
newCryptoData := bytespool.Alloc(int32(cryptoLen))
240-
copy(newCryptoData, cryptoData)
241-
bytespool.Free(cryptoData)
242-
cryptoData = newCryptoData
221+
newCryptoLen := int(offset + length)
222+
if len(cryptoData) < newCryptoLen {
223+
return nil, io.ErrShortBuffer
243224
}
225+
wipeBytes(cryptoData[cryptoLen:newCryptoLen])
226+
cryptoLen = newCryptoLen
244227
}
245228
if _, err := buffer.Read(cryptoData[offset : offset+length]); err != nil { // Field: Crypto Data
246229
return nil, io.ErrUnexpectedEOF
@@ -276,7 +259,14 @@ func SniffQUIC(b []byte) (resultReturn *SniffHeader, errorReturn error) {
276259
}
277260
return &SniffHeader{domain: tlsHdr.Domain()}, nil
278261
}
279-
return nil, common.ErrNoClue
262+
// All payload is parsed as valid QUIC packets, but we need more packets for crypto data to read client hello.
263+
return nil, protocol.ErrProtoNeedMoreData
264+
}
265+
266+
func wipeBytes(b []byte) {
267+
for i := range len(b) {
268+
b[i] = 0x0
269+
}
280270
}
281271

282272
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {

0 commit comments

Comments
 (0)