diff --git a/common/sniff/bittorrent.go b/common/sniff/bittorrent.go index ca5ec1c7..491daa2e 100644 --- a/common/sniff/bittorrent.go +++ b/common/sniff/bittorrent.go @@ -11,6 +11,18 @@ import ( "github.com/sagernet/sing-box/constant" ) +const ( + trackerConnectFlag = iota + trackerAnnounceFlag + trackerScrapeFlag + + trackerProtocolID = 0x41727101980 + + trackerConnectMinSize = 16 + trackerAnnounceMinSize = 20 + trackerScrapeMinSize = 8 +) + // BitTorrent detects if the stream is a BitTorrent connection. // For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html func BitTorrent(_ context.Context, reader io.Reader) (*adapter.InboundContext, error) { @@ -78,3 +90,24 @@ func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) { Protocol: constant.ProtocolUTP, }, nil } + +// UDPTracker detects if the packet is a UDP Tracker Protocol packet. +// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html +func UDPTracker(_ context.Context, packet []byte) (*adapter.InboundContext, error) { + switch { + case len(packet) >= trackerConnectMinSize && + binary.BigEndian.Uint64(packet[:8]) == trackerProtocolID && + binary.BigEndian.Uint32(packet[8:12]) == trackerConnectFlag: + fallthrough + case len(packet) >= trackerAnnounceMinSize && + binary.BigEndian.Uint32(packet[8:12]) == trackerAnnounceFlag: + fallthrough + case len(packet) >= trackerScrapeMinSize && + binary.BigEndian.Uint32(packet[8:12]) == trackerScrapeFlag: + return &adapter.InboundContext{ + Protocol: constant.ProtocolUDPTracker, + }, nil + default: + return nil, os.ErrInvalid + } +} diff --git a/common/sniff/bittorrent_test.go b/common/sniff/bittorrent_test.go index 535a7f2e..6cb1854d 100644 --- a/common/sniff/bittorrent_test.go +++ b/common/sniff/bittorrent_test.go @@ -49,3 +49,33 @@ func TestSniffUTP(t *testing.T) { require.Equal(t, constant.ProtocolUTP, metadata.Protocol) } } + +func TestSniffUDPTracker(t *testing.T) { + t.Parallel() + + connectPackets := []string{ + // connect packets + "00000417271019800000000078e90560", + "00000417271019800000000022c5d64d", + "000004172710198000000000b3863541", + + // announce packets + "3d7592ead4b8c9e300000001b871a3820000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365", + "3d7592ead4b8c9e30000000188deed1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365", + "3d7592ead4b8c9e300000001ceb948ad0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a3362cdb7020ff920e5aa642c3d4066950dd1f01f4d00000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365", + + // scrape packets + "3d7592ead4b8c9e300000002d2f4bba5a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + "3d7592ead4b8c9e300000002441243292aae6c35c94fcfb415dbe95f408b9ce91ee846ed", + "3d7592ead4b8c9e300000002b2aa461b1ad1fa9661cf3fe45fb2504ad52ec6c67758e294", + } + + for _, pkt := range connectPackets { + pkt, err := hex.DecodeString(pkt) + require.NoError(t, err) + + metadata, err := sniff.UDPTracker(context.TODO(), pkt) + require.NoError(t, err) + require.Equal(t, constant.ProtocolUDPTracker, metadata.Protocol) + } +} diff --git a/constant/protocol.go b/constant/protocol.go index 69707480..f1d753c6 100644 --- a/constant/protocol.go +++ b/constant/protocol.go @@ -8,4 +8,5 @@ const ( ProtocolSTUN = "stun" ProtocolBitTorrent = "bittorrent" ProtocolUTP = "utp" + ProtocolUDPTracker = "udp-tracker" ) diff --git a/route/router.go b/route/router.go index d8004576..1631b4b8 100644 --- a/route/router.go +++ b/route/router.go @@ -924,6 +924,7 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m sniff.QUICClientHello, sniff.STUNMessage, sniff.UTP, + sniff.UDPTracker, ) if sniffMetadata != nil { metadata.Protocol = sniffMetadata.Protocol