diff --git a/common/protocol/bittorrent/bittorrent.go b/common/protocol/bittorrent/bittorrent.go index 708307645992..4c89b2c69b31 100644 --- a/common/protocol/bittorrent/bittorrent.go +++ b/common/protocol/bittorrent/bittorrent.go @@ -3,11 +3,8 @@ package bittorrent import ( "encoding/binary" "errors" - "math" - "time" "github.com/xtls/xray-core/common" - "github.com/xtls/xray-core/common/buf" ) type SniffHeader struct{} @@ -39,50 +36,44 @@ func SniffUTP(b []byte) (*SniffHeader, error) { return nil, common.ErrNoClue } - buffer := buf.FromBytes(b) - - var typeAndVersion uint8 - - if binary.Read(buffer, binary.BigEndian, &typeAndVersion) != nil { - return nil, common.ErrNoClue - } else if b[0]>>4&0xF > 4 || b[0]&0xF != 1 { + // type 4 (ST_SYN), version 1 + if b[0] != 0x41 { return nil, errNotBittorrent } - var extension uint8 - - if binary.Read(buffer, binary.BigEndian, &extension) != nil { - return nil, common.ErrNoClue - } else if extension != 0 && extension != 1 { + // timestamp_difference is always 0 in new connections + if binary.BigEndian.Uint32(b[8:12]) != 0 { return nil, errNotBittorrent } + // Walk the extension chain. Selective ack (1) and extension bits (2) + extension, offset := b[1], 20 for extension != 0 { - if extension != 1 { + if len(b) < offset+2 { return nil, errNotBittorrent } - if binary.Read(buffer, binary.BigEndian, &extension) != nil { - return nil, common.ErrNoClue - } - - var length uint8 - if err := binary.Read(buffer, binary.BigEndian, &length); err != nil { - return nil, common.ErrNoClue + length := int(b[offset+1]) + switch extension { + case 1: // selective ack + if length < 4 || length%4 != 0 { + return nil, errNotBittorrent + } + case 2: // extension bits: fixed 8 bytes, sent in ST_SYN by µTorrent + if length != 8 { + return nil, errNotBittorrent + } + default: + return nil, errNotBittorrent } - if common.Error2(buffer.ReadBytes(int32(length))) != nil { - return nil, common.ErrNoClue + if len(b) < offset+2+length { + return nil, errNotBittorrent } + extension = b[offset] + offset += 2 + length } - if common.Error2(buffer.ReadBytes(2)) != nil { - return nil, common.ErrNoClue - } - - var timestamp uint32 - if err := binary.Read(buffer, binary.BigEndian, ×tamp); err != nil { - return nil, common.ErrNoClue - } - if math.Abs(float64(time.Now().UnixMicro()-int64(timestamp))) > float64(24*time.Hour) { + // extensions should consume all ST_SYN payload + if len(b) != offset { return nil, errNotBittorrent } diff --git a/common/protocol/bittorrent/bittorrent_test.go b/common/protocol/bittorrent/bittorrent_test.go new file mode 100644 index 000000000000..7fae197ce16c --- /dev/null +++ b/common/protocol/bittorrent/bittorrent_test.go @@ -0,0 +1,67 @@ +package bittorrent + +import ( + "encoding/binary" + "testing" + + "github.com/xtls/xray-core/common" +) + +// utpPacket builds the fixed 20-byte header defined by BEP 29. +func utpPacket(packetType, extension byte, tsDiff uint32, payload ...byte) []byte { + b := make([]byte, 20) + b[0] = packetType<<4 | 1 + b[1] = extension + binary.BigEndian.PutUint16(b[2:4], 0x4a3f) // connection_id, random + binary.BigEndian.PutUint32(b[4:8], 0x8c3a91d2) // timestamp_microseconds, sender's clock + binary.BigEndian.PutUint32(b[8:12], tsDiff) + binary.BigEndian.PutUint32(b[12:16], 0x00100000) // wnd_size + binary.BigEndian.PutUint16(b[16:18], 0x71ee) // seq_nr, random in libutp/libtorrent + binary.BigEndian.PutUint16(b[18:20], 0x0000) // ack_nr + return append(b, payload...) +} + +func TestSniffUTP(t *testing.T) { + selectiveAck := []byte{0, 4, 0xff, 0x00, 0xff, 0x00} + wrongVersion := utpPacket(4, 0, 0) + wrongVersion[0] = 4<<4 | 2 + + cases := []struct { + name string + payload []byte + err error + }{ + {"syn", utpPacket(4, 0, 0), nil}, + {"syn with selective ack", append(utpPacket(4, 1, 0), selectiveAck...), nil}, + {"syn with extension bits", append(utpPacket(4, 2, 0), 0, 8, 1, 2, 3, 4, 5, 6, 7, 8), nil}, + {"extension bits with wrong length", append(utpPacket(4, 2, 0), 0, 4, 1, 2, 3, 4), errNotBittorrent}, + {"syn with nonzero timestamp_difference", utpPacket(4, 0, 0x1234), errNotBittorrent}, + {"syn with trailing payload", utpPacket(4, 0, 0, 'x'), errNotBittorrent}, + // txid 0x4100, no EDNS0: the worst case colliding with the uTP header + {"dns query", []byte{ + 0x41, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 'a', 0x02, 'c', 'o', 0x00, 0x00, 0x01, 0x00, 0x01, + }, errNotBittorrent}, + {"established connection packets", utpPacket(0, 0, 0x5678, 'x', 'y', 'z'), errNotBittorrent}, + {"state", utpPacket(2, 0, 0x5678), errNotBittorrent}, + {"fin", utpPacket(1, 0, 0x5678), errNotBittorrent}, + {"wrong version", wrongVersion, errNotBittorrent}, + {"unknown packet type", utpPacket(5, 0, 0), errNotBittorrent}, + {"unknown extension", utpPacket(4, 2, 0), errNotBittorrent}, + {"extension chain past the datagram", utpPacket(4, 1, 0, 0, 8, 0xff), errNotBittorrent}, + {"selective ack not in multiples of 4", append(utpPacket(4, 1, 0), 0, 3, 0xff, 0x00, 0xff), errNotBittorrent}, + {"shorter than the header", utpPacket(4, 0, 0)[:19], common.ErrNoClue}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + h, err := SniffUTP(c.payload) + if err != c.err { + t.Fatalf("expected error %v, got %v", c.err, err) + } + if err == nil && h == nil { + t.Fatal("expected a sniff header, got nil") + } + }) + } +}