From 23bd918b20115f558efff11ba699f0fb87928a3f Mon Sep 17 00:00:00 2001 From: maze Date: Fri, 10 Oct 2025 14:39:31 +0200 Subject: [PATCH] Added support for STUN protocol detection --- protocol/detect_stun.go | 101 +++++++++++++++++++++++++++++++++++ protocol/detect_stun_test.go | 85 +++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 protocol/detect_stun.go create mode 100644 protocol/detect_stun_test.go diff --git a/protocol/detect_stun.go b/protocol/detect_stun.go new file mode 100644 index 0000000..66152fa --- /dev/null +++ b/protocol/detect_stun.go @@ -0,0 +1,101 @@ +package protocol + +import ( + "log" + + "golang.org/x/crypto/cryptobyte" +) + +func init() { + registerSTUN() +} + +func registerSTUN() { + Register(Both, "????\x21\x12\xa4\x42", detectSTUN) +} + +func detectSTUN(dir Direction, data []byte, srcPort, dstPort int) (proto *Protocol, confidence float64) { + // All STUN messages comprise a 20-byte header followed by zero or more attributes. + var ( + stream = cryptobyte.String(data) + messageType uint16 + messageLength uint16 + magicCookie uint32 + transactionID []byte + ) + if !stream.ReadUint16(&messageType) || + !stream.ReadUint16(&messageLength) || + !stream.ReadUint32(&magicCookie) || + !stream.ReadBytes(&transactionID, 12) { + return + } + + if len(stream) != int(messageLength) { + confidence -= .2 + } + + // The most significant 2 bits of every STUN message MUST be zeroes. + if messageType&0b11000000 != 0 { + log.Printf("type %#08b", messageType) + return + } + + // The Magic Cookie field MUST contain the fixed value 0x2112A442 in network byte order. + if magicCookie != 0x2112A442 { + log.Printf("cookie %#04x", magicCookie) + return + } + confidence += .5 + + // Decode the message class & type. + class := (messageType & 0b00000100000000) >> 7 + class |= (messageType & 0b00000000010000) >> 4 + method := (messageType & 0b11111000000000) >> 2 + method |= (messageType & 0b00000011100000) >> 1 + method |= (messageType & 0b00000000001111) >> 0 + + // Validate most common methods. + // TODO(maze): check method arguments to improve accuracy + switch class { + case stunRequest: // request + switch method { + case stunBinding: // binding + confidence += .2 + } + + case stunSuccesResponse: // success response + switch method { + case stunBinding: // binding + confidence += .2 + } + + case stunErrorResponse: + switch method { + case stunBinding: // binding error + confidence += .2 + } + } + + if stunPort[srcPort] || stunPort[dstPort] { + confidence += .2 + } + + return &Protocol{ + Type: TypeSTUN, + }, confidence +} + +const ( + stunRequest = 0b00 + stunSuccesResponse = 0b10 + stunErrorResponse = 0b11 + stunBinding = 0x0001 +) + +// IANA has updated the reference from RFC 5389 to RFC 8489 for the +// following ports in the "Service Name and Transport Protocol Port +// Number Registry". +var stunPort = map[int]bool{ + 3478: true, + 5349: true, +} diff --git a/protocol/detect_stun_test.go b/protocol/detect_stun_test.go new file mode 100644 index 0000000..c47de6f --- /dev/null +++ b/protocol/detect_stun_test.go @@ -0,0 +1,85 @@ +package protocol + +import "testing" + +func TestDetectSTUN(t *testing.T) { + // A valid STUN binding request. + bindingRequest := []byte{ + 0x00, 0x01, // Message Type + 0x00, 0x00, // Message Length (0 bytes) + 0x21, 0x12, 0xa4, 0x42, // Magic Cookie + 0x4b, 0x49, 0x6b, 0x72, 0x7a, 0x6a, 0x56, 0x37, 0x41, 0x61, 0x6e, 0x38, // Transaction ID + } + + // A valid STUN binding success response. + bindingSuccessResponse := []byte{ + 0x01, 0x01, // Message Type + 0x00, 0x0c, // Message Length (12 bytes) + 0x21, 0x12, 0xa4, 0x42, // Magic Cookie + 0x4b, 0x49, 0x6b, 0x72, 0x7a, 0x6a, 0x56, 0x37, 0x41, 0x61, 0x6e, 0x38, // Transaction ID + // --- Attributes (12 bytes) --- + 0x00, 0x01, // Attribute Type MAPPED-ADDRESS + 0x00, 0x08, // Attribute Length (8 bytes) + 0x00, // Reserved (always 0) + 0x01, // Protocol Family: IPv4 (0) + 0x11, 0xfc, // Port Number: 4604 + 0x46, 0xc7, 0x80, 0x2e, // IP: 70.199.128.46 + } + + // A truncated STUN binding success response. + truncatedBindingSuccessResponse := []byte{ + 0x01, 0x01, // Message Type + 0x00, 0x0c, // Message Length (12 bytes) + 0x21, 0x12, 0xa4, 0x42, // Magic Cookie + 0x4b, 0x49, 0x6b, 0x72, 0x7a, 0x6a, 0x56, 0x37, 0x41, 0x61, 0x6e, 0x38, // Transaction ID + // --- Attributes (12 bytes) --- + 0x00, 0x01, // Attribute Type MAPPED-ADDRESS + 0x00, 0x08, // Attribute Length (8 bytes) + 0x00, // Reserved (always 0) + /* truncated */ + } + + // Am invalid STUN binding request (invalid magic). + invalidMagicBindingRequest := []byte{ + 0x00, 0x01, // Message Type + 0x00, 0x00, // Message Length (0 bytes) + 0x2a, 0x12, 0xa4, 0x42, // Magic Cookie + 0x4b, 0x49, 0x6b, 0x72, 0x7a, 0x6a, 0x56, 0x37, 0x41, 0x61, 0x6e, 0x38, // Transaction ID + } + + tests := []*testCase{ + { + Name: "STUN binding request", + Direction: Client, + Data: bindingRequest, + DstPort: 3478, + WantType: TypeSTUN, + WantConfidence: .9, + }, + { + Name: "STUN binding success response", + Direction: Server, + Data: bindingSuccessResponse, + SrcPort: 3478, + WantType: TypeSTUN, + WantConfidence: .9, + }, + { + Name: "Truncated STUN binding success response", + Direction: Server, + Data: truncatedBindingSuccessResponse, + SrcPort: 3478, + WantType: TypeSTUN, + WantConfidence: .7, + }, + { + Name: "Invalid magic STUN binding request", + Direction: Client, + Data: invalidMagicBindingRequest, + DstPort: 3478, + WantError: ErrUnknown, + }, + } + + testRunner(t, tests) +}