Refactoring

Refactored Protocol.Name -> Protocol.Type; added Encapsulation field
Refactored TLS parsing; added support for ALPN
This commit is contained in:
2025-10-10 12:41:44 +02:00
parent f86a7f7a67
commit 81a3829382
20 changed files with 366 additions and 141 deletions

View File

@@ -41,5 +41,5 @@ func main() {
os.Exit(3) os.Exit(3)
} }
fmt.Printf("Protocol at address %q is %s version %s (confidence %g%%)\n", address, protocol.Name, protocol.Version, confidence*100) fmt.Printf("Protocol at address %q is %s version %s (confidence %g%%)\n", address, protocol.Type, protocol.Version, confidence*100)
} }

View File

@@ -42,11 +42,11 @@ func main() {
if p == nil { if p == nil {
return errors.New("no protocol detected") return errors.New("no protocol detected")
} }
if !accept[p.Name] { if !accept[p.Type] {
return fmt.Errorf("protocol %s is not accepted", p.Name) return fmt.Errorf("protocol %s is not accepted", p.Type)
} }
fmt.Fprintf(os.Stderr, "Accepting protocol %s version %s initiated by %s\n", fmt.Fprintf(os.Stderr, "Accepting protocol %s version %s initiated by %s\n",
p.Name, p.Version, dir) p.Type, p.Version, dir)
return nil return nil
}) })
defer func() { _ = c.Close() }() defer func() { _ = c.Close() }()

View File

@@ -81,7 +81,7 @@ func proxy(client net.Conn, target string) {
log.Printf("protocol detection failed: %v", result.Error) log.Printf("protocol detection failed: %v", result.Error)
} else { } else {
log.Printf("detected protocol %s version %s initiated by %s", log.Printf("detected protocol %s version %s initiated by %s",
result.Protocol.Name, result.Protocol.Version, result.Direction) result.Protocol.Type, result.Protocol.Version, result.Direction)
} }
// Wait for the multiplexing to finish. // Wait for the multiplexing to finish.

View File

@@ -31,7 +31,7 @@ func detectHTTPRequest(dir Direction, data []byte, srcPort, dstPort int) (proto
) )
if request, err := http.ReadRequest(r); err == nil { if request, err := http.ReadRequest(r); err == nil {
return &Protocol{ return &Protocol{
Name: ProtocolHTTP, Type: TypeHTTP,
Version: Version{ Version: Version{
Major: request.ProtoMajor, Major: request.ProtoMajor,
Minor: request.ProtoMinor, Minor: request.ProtoMinor,
@@ -66,7 +66,7 @@ func detectHTTPRequest(dir Direction, data []byte, srcPort, dstPort int) (proto
_, _ = fmt.Sscanf(string(part[2]), "HTTP/%d.%d ", &version.Major, &version.Minor) _, _ = fmt.Sscanf(string(part[2]), "HTTP/%d.%d ", &version.Major, &version.Minor)
return &Protocol{ return &Protocol{
Name: ProtocolHTTP, Type: TypeHTTP,
Version: version, Version: version,
}, confidence + .75 }, confidence + .75
} }
@@ -95,7 +95,7 @@ func detectHTTPResponse(dir Direction, data []byte, srcPort, dstPort int) (proto
) )
if response, err := http.ReadResponse(r, nil); err == nil { if response, err := http.ReadResponse(r, nil); err == nil {
return &Protocol{ return &Protocol{
Name: ProtocolHTTP, Type: TypeHTTP,
Version: Version{ Version: Version{
Major: response.ProtoMajor, Major: response.ProtoMajor,
Minor: response.ProtoMinor, Minor: response.ProtoMinor,
@@ -110,7 +110,7 @@ func detectHTTPResponse(dir Direction, data []byte, srcPort, dstPort int) (proto
_, _ = fmt.Sscanf(string(data), "HTTP/%d.%d ", &version.Major, &version.Minor) _, _ = fmt.Sscanf(string(data), "HTTP/%d.%d ", &version.Major, &version.Minor)
return &Protocol{ return &Protocol{
Name: ProtocolHTTP, Type: TypeHTTP,
Version: version, Version: version,
}, confidence + .75 }, confidence + .75
} }

View File

@@ -22,7 +22,7 @@ func TestDetectHTTPRequest(t *testing.T) {
Direction: Client, Direction: Client,
Data: http10Request, Data: http10Request,
DstPort: 80, DstPort: 80,
WantProto: ProtocolHTTP, WantType: TypeHTTP,
WantConfidence: .95, WantConfidence: .95,
}, },
{ {
@@ -30,7 +30,7 @@ func TestDetectHTTPRequest(t *testing.T) {
Direction: Client, Direction: Client,
Data: getRequest, Data: getRequest,
DstPort: 80, DstPort: 80,
WantProto: ProtocolHTTP, WantType: TypeHTTP,
WantConfidence: .95, WantConfidence: .95,
}, },
{ {
@@ -81,21 +81,21 @@ func TestDetectHTTPResponse(t *testing.T) {
Direction: Server, Direction: Server,
Data: http10Response, Data: http10Response,
SrcPort: 80, SrcPort: 80,
WantProto: ProtocolHTTP, WantType: TypeHTTP,
}, },
{ {
Name: "HTTP/1.1 200", Name: "HTTP/1.1 200",
Direction: Server, Direction: Server,
Data: responseOK, Data: responseOK,
SrcPort: 80, SrcPort: 80,
WantProto: ProtocolHTTP, WantType: TypeHTTP,
}, },
{ {
Name: "HTTP/1.1 404", Name: "HTTP/1.1 404",
Direction: Server, Direction: Server,
Data: responseNotFound, Data: responseNotFound,
SrcPort: 80, SrcPort: 80,
WantProto: ProtocolHTTP, WantType: TypeHTTP,
}, },
{ {
Name: "Invalid HTTP/1.1 GET", Name: "Invalid HTTP/1.1 GET",

View File

@@ -32,7 +32,7 @@ func detectMQTT(dir Direction, data []byte, srcPort, dstPort int) (proto *Protoc
// We are reasonabily sure this is MQTT now. // We are reasonabily sure this is MQTT now.
proto = &Protocol{ proto = &Protocol{
Name: ProtocolMQTT, Type: TypeMQTT,
} }
confidence = 0.5 confidence = 0.5

View File

@@ -53,7 +53,7 @@ func TestDetectMQTT(t *testing.T) {
Direction: Client, Direction: Client,
Data: validSimplePacket, Data: validSimplePacket,
DstPort: 1883, DstPort: 1883,
WantProto: ProtocolMQTT, WantType: TypeMQTT,
WantConfidence: .99, WantConfidence: .99,
}, },
{ {
@@ -61,7 +61,7 @@ func TestDetectMQTT(t *testing.T) {
Direction: Client, Direction: Client,
Data: validFullPacket, Data: validFullPacket,
DstPort: 1883, DstPort: 1883,
WantProto: ProtocolMQTT, WantType: TypeMQTT,
WantConfidence: .99, WantConfidence: .99,
}, },
{ {
@@ -69,7 +69,7 @@ func TestDetectMQTT(t *testing.T) {
Direction: Client, Direction: Client,
Data: partialPacket, Data: partialPacket,
DstPort: 1883, DstPort: 1883,
WantProto: ProtocolMQTT, WantType: TypeMQTT,
WantConfidence: .5, WantConfidence: .5,
}, },
{ {
@@ -77,7 +77,7 @@ func TestDetectMQTT(t *testing.T) {
Direction: Client, Direction: Client,
Data: trailingGarbagePacket, Data: trailingGarbagePacket,
DstPort: 1883, DstPort: 1883,
WantProto: ProtocolMQTT, WantType: TypeMQTT,
WantConfidence: .75, WantConfidence: .75,
}, },
} }

View File

@@ -49,7 +49,7 @@ func detectMySQL(dir Direction, data []byte, srcPort, dstPort int) (proto *Proto
_, _ = fmt.Sscanf(string(data[1:serverVersionEndPos]), "%d.%d.%d-%s", &version.Major, &version.Minor, &version.Patch, &version.Extra) _, _ = fmt.Sscanf(string(data[1:serverVersionEndPos]), "%d.%d.%d-%s", &version.Major, &version.Minor, &version.Patch, &version.Extra)
return &Protocol{ return &Protocol{
Name: ProtocolMySQL, Type: TypeMySQL,
Version: version, Version: version,
}, confidence + .75 }, confidence + .75
} }

View File

@@ -43,7 +43,7 @@ func TestDetectMySQL(t *testing.T) {
Direction: Server, Direction: Server,
Data: mysql8Banner, Data: mysql8Banner,
SrcPort: 3306, SrcPort: 3306,
WantProto: ProtocolMySQL, WantType: TypeMySQL,
WantConfidence: .85, WantConfidence: .85,
}, },
{ {
@@ -51,7 +51,7 @@ func TestDetectMySQL(t *testing.T) {
Direction: Server, Direction: Server,
Data: mariaDBBanner, Data: mariaDBBanner,
SrcPort: 3306, SrcPort: 3306,
WantProto: ProtocolMySQL, WantType: TypeMySQL,
WantConfidence: .85, WantConfidence: .85,
}, },
{ {

View File

@@ -40,7 +40,7 @@ func detectPostgreSQLClient(dir Direction, data []byte, srcPort, dstPort int) (p
minor := int(binary.BigEndian.Uint16(data[6:])) minor := int(binary.BigEndian.Uint16(data[6:]))
if major == 2 || major == 3 { if major == 2 || major == 3 {
return &Protocol{ return &Protocol{
Name: ProtocolPostgreSQL, Type: TypePostgreSQL,
Version: Version{ Version: Version{
Major: major, Major: major,
Minor: minor, Minor: minor,
@@ -70,7 +70,7 @@ func detectPostgreSQLServer(dir Direction, data []byte, srcPort, dstPort int) (p
'Z', // ReadyForQuery 'Z', // ReadyForQuery
'E', // ErrorResponse 'E', // ErrorResponse
'N': // NoticeResponse 'N': // NoticeResponse
return &Protocol{Name: ProtocolPostgreSQL}, confidence + .65 return &Protocol{Type: TypePostgreSQL}, confidence + .65
default: default:
return nil, 0 return nil, 0

View File

@@ -36,7 +36,7 @@ func TestDetectPostgreSQLClient(t *testing.T) {
Direction: Client, Direction: Client,
Data: pgClientStartup, Data: pgClientStartup,
DstPort: 5432, DstPort: 5432,
WantProto: ProtocolPostgreSQL, WantType: TypePostgreSQL,
WantConfidence: .85, WantConfidence: .85,
}, },
{ {
@@ -77,7 +77,7 @@ func TestDetectPostgreSQLServer(t *testing.T) {
Direction: Server, Direction: Server,
Data: pgServerAuthOK, Data: pgServerAuthOK,
DstPort: 5432, DstPort: 5432,
WantProto: ProtocolPostgreSQL, WantType: TypePostgreSQL,
WantConfidence: .65, WantConfidence: .65,
}, },
{ {
@@ -95,9 +95,9 @@ func TestDetectPostgreSQLServer(t *testing.T) {
t.Fatal(err) t.Fatal(err)
return return
} }
t.Logf("detected %s version %s confidence %g%%", p.Name, p.Version, c*100) t.Logf("detected %s version %s confidence %g%%", p.Type, p.Version, c*100)
if p.Name != ProtocolPostgreSQL { if p.Type != TypePostgreSQL {
t.Fatalf("expected postgres protocol, got %s", p.Name) t.Fatalf("expected postgres protocol, got %s", p.Type)
return return
} }
}) })
@@ -108,9 +108,9 @@ func TestDetectPostgreSQLServer(t *testing.T) {
t.Fatal(err) t.Fatal(err)
return return
} }
t.Logf("detected %s version %s confidence %g%%", p.Name, p.Version, c*100) t.Logf("detected %s version %s confidence %g%%", p.Type, p.Version, c*100)
if p.Name != ProtocolPostgreSQL { if p.Type != TypePostgreSQL {
t.Fatalf("expected postgres protocol, got %s", p.Name) t.Fatalf("expected postgres protocol, got %s", p.Type)
return return
} }
}) })

View File

@@ -58,7 +58,7 @@ func detectSSH(dir Direction, data []byte, srcPort, dstPort int) (proto *Protoco
} }
} }
return &Protocol{ return &Protocol{
Name: ProtocolSSH, Type: TypeSSH,
Version: Version{ Version: Version{
Major: 2, Major: 2,
Minor: 0, Minor: 0,
@@ -78,7 +78,7 @@ func detectSSH(dir Direction, data []byte, srcPort, dstPort int) (proto *Protoco
} }
} }
return &Protocol{ return &Protocol{
Name: ProtocolSSH, Type: TypeSSH,
Version: Version{ Version: Version{
Major: 1, Major: 1,
Minor: 99, Minor: 99,

View File

@@ -36,7 +36,7 @@ func TestDetectSSH(t *testing.T) {
Direction: Client, Direction: Client,
Data: openSSHBanner, Data: openSSHBanner,
DstPort: 22, DstPort: 22,
WantProto: ProtocolSSH, WantType: TypeSSH,
WantConfidence: .95, WantConfidence: .95,
}, },
{ {
@@ -44,7 +44,7 @@ func TestDetectSSH(t *testing.T) {
Direction: Server, Direction: Server,
Data: openSSHBanner, Data: openSSHBanner,
SrcPort: 22, SrcPort: 22,
WantProto: ProtocolSSH, WantType: TypeSSH,
WantConfidence: .95, WantConfidence: .95,
}, },
{ {
@@ -52,7 +52,7 @@ func TestDetectSSH(t *testing.T) {
Direction: Server, Direction: Server,
Data: preBannerSSH, Data: preBannerSSH,
SrcPort: 22, SrcPort: 22,
WantProto: ProtocolSSH, WantType: TypeSSH,
WantConfidence: .95, WantConfidence: .95,
}, },
{ {
@@ -60,7 +60,7 @@ func TestDetectSSH(t *testing.T) {
Direction: Server, Direction: Server,
Data: dropbearBanner, Data: dropbearBanner,
SrcPort: 22, SrcPort: 22,
WantProto: ProtocolSSH, WantType: TypeSSH,
WantConfidence: .95, WantConfidence: .95,
}, },
{ {

View File

@@ -1,6 +1,9 @@
package protocol package protocol
import ( import (
"slices"
"strings"
"golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte"
"git.maze.io/go/dpi" "git.maze.io/go/dpi"
@@ -11,12 +14,18 @@ func init() {
} }
func registerTLS() { func registerTLS() {
Register(Both, "\x16\x03\x00", detectTLS) // SSLv3 Register(Both, "\x16\x03\x00", detectTLS) // SSL 3.0
Register(Both, "\x16\x03\x01", detectTLS) // TLSv1.0 Register(Both, "\x16\x03\x01", detectTLS) // TLS 1.0
Register(Both, "\x16\x03\x02", detectTLS) // TLSv1.1 Register(Both, "\x16\x03\x02", detectTLS) // TLS 1.1
Register(Both, "\x16\x03\x03", detectTLS) // TLSv1.2 Register(Both, "\x16\x03\x03", detectTLS) // TLS 1.2
} }
const (
tlsRecordTypeHandshake uint8 = 22
tlsTypeClientHello uint8 = 1
tlsTypeServerHello uint8 = 2
)
func detectTLS(dir Direction, data []byte, _, _ int) (proto *Protocol, confidence float64) { func detectTLS(dir Direction, data []byte, _, _ int) (proto *Protocol, confidence float64) {
stream := cryptobyte.String(data) stream := cryptobyte.String(data)
@@ -44,27 +53,53 @@ func detectTLS(dir Direction, data []byte, _, _ int) (proto *Protocol, confidenc
// Initial confidence // Initial confidence
confidence = 0.5 confidence = 0.5
// Detected SSL/TLS version // Detected SSL/TLS tlsVersion
var version dpi.TLSVersion var tlsVersion dpi.TLSVersion
// Attempt to decode the full TLS Client Hello handshake // Attempt to decode the full TLS Client Hello handshake
if version == 0 { if tlsVersion == 0 {
if hello, err := dpi.DecodeTLSClientHelloHandshake(data); err == nil { if record, err := dpi.DecodeTLSRecord(data); err == nil && record.Type == tlsRecordTypeHandshake && len(record.Data) > 1 {
version = hello.Version tlsVersion = record.Version
confidence += .45 confidence += .15
}
}
// Attempt to decode the full TLS Server Hello handshake switch record.Data[0] {
if version == 0 { case tlsTypeClientHello: // TLS ClientHello
if hello, err := dpi.DecodeTLSServerHello(data); err == nil { if hello, err := dpi.DecodeTLSClientHello(record.Data); err == nil {
version = hello.Version tlsVersion = hello.Version
confidence += .45 confidence += .3
slices.SortStableFunc(hello.ALPNProtocols, func(a, b string) int {
return strings.Compare(b, a)
})
for _, id := range hello.ALPNProtocols {
if proto = ALPNProtocol[id]; proto != nil {
return
}
}
}
case tlsTypeServerHello: // TLS ServerHello
if hello, err := dpi.DecodeTLSServerHello(record.Data); err == nil {
tlsVersion = hello.Version
confidence += .3
slices.SortStableFunc(hello.ALPNProtocols, func(a, b string) int {
return strings.Compare(b, a)
})
for _, id := range hello.ALPNProtocols {
if proto = ALPNProtocol[id]; proto != nil {
return
}
}
}
}
} }
} }
// Attempt to decode at least the handshake protocol and version. // Attempt to decode at least the handshake protocol and version.
if version == 0 && !Strict { if tlsVersion == 0 && !Strict {
var handshakeType uint8 var handshakeType uint8
if stream.ReadUint8(&handshakeType) && (handshakeType == 1 || handshakeType == 2) { if stream.ReadUint8(&handshakeType) && (handshakeType == 1 || handshakeType == 2) {
var ( var (
@@ -72,33 +107,68 @@ func detectTLS(dir Direction, data []byte, _, _ int) (proto *Protocol, confidenc
versionWord uint16 versionWord uint16
) )
if stream.ReadUint24(&length) && stream.ReadUint16(&versionWord) { if stream.ReadUint24(&length) && stream.ReadUint16(&versionWord) {
version = dpi.TLSVersion(versionWord) tlsVersion = dpi.TLSVersion(versionWord)
confidence += .25 confidence += .25
} }
} }
} }
// Fall back to the version in the TLS record header, this is less accurate // Fall back to the version in the TLS record header, this is less accurate
if version == 0 && !Strict { if tlsVersion == 0 && !Strict {
version = dpi.TLSVersion(header.Version) tlsVersion = dpi.TLSVersion(header.Version)
} }
// We're "multi protocol", in that SSL is its own protocol // We're "multi protocol", in that SSL is its own protocol
if version == dpi.VersionSSL30 { if tlsVersion == dpi.VersionSSL30 {
return &Protocol{ return &Protocol{
Name: ProtocolSSL, Type: TypeSSL,
Version: Version{Major: 3, Minor: 0, Patch: -1}, Version: Version{Major: 3, Minor: 0, Patch: -1},
}, confidence }, confidence
} else if version >= dpi.VersionTLS10 && version <= dpi.VersionTLS13 { } else if tlsVersion >= dpi.VersionTLS10 && tlsVersion <= dpi.VersionTLS13 {
return &Protocol{ return &Protocol{
Name: ProtocolTLS, Type: TypeTLS,
Version: Version{Major: 1, Minor: int(uint8(version) - 1), Patch: -1}, Version: Version{Major: 1, Minor: int(uint8(tlsVersion) - 1), Patch: -1},
}, confidence }, confidence
} else if version >= dpi.VersionTLS13Draft && version <= dpi.VersionTLS13Draft23 { } else if tlsVersion >= dpi.VersionTLS13Draft && tlsVersion <= dpi.VersionTLS13Draft23 {
return &Protocol{ return &Protocol{
Name: ProtocolTLS, Type: TypeTLS,
Version: Version{Major: 1, Minor: 3, Patch: -1}, Version: Version{Major: 1, Minor: 3, Patch: -1},
}, confidence }, confidence
} }
return nil, 0 return nil, 0
} }
// ALPNProtocol is a map of TLS Application-Layer Protocol Negotiation (ALPN) Protocol identifier to [Protocol].
//
// See https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids\
var ALPNProtocol = map[string]*Protocol{
"acme-tls/1": {Type: TypeACME, Encapsulation: TypeTLS, Version: Version{Major: 1}}, // ACME-TLS/1
"co": {Type: TypeCoAP, Encapsulation: TypeTLS}, // CoAP (over DTLS)
"coap": {Type: TypeCoAP, Encapsulation: TypeTLS}, // CoAP (over TLS)
"c-webrtc": {Type: TypeWebRTC, Encapsulation: TypeTLS}, // Confidential WebRTC Media and Data
"dot": {Type: TypeDNS, Encapsulation: TypeTLS}, // DNS-over-TLS
"ftp": {Type: TypeFTP, Encapsulation: TypeTLS}, // FTP
"http/0.9": {Type: TypeHTTP, Encapsulation: TypeTLS, Version: Version{Major: 0, Minor: 9, Patch: -1}}, // HTTP/0.9
"http/1.0": {Type: TypeHTTP, Encapsulation: TypeTLS, Version: Version{Major: 1, Minor: 0, Patch: -1}}, // HTTP/1.0
"http/1.1": {Type: TypeHTTP, Encapsulation: TypeTLS, Version: Version{Major: 1, Minor: 1, Patch: -1}}, // HTTP/1.1
"h2": {Type: TypeHTTP, Encapsulation: TypeTLS, Version: Version{Major: 2, Minor: -1, Patch: -1}}, // HTTP/2 (over TLS)
"h2c": {Type: TypeHTTP, Encapsulation: TypeTLS, Version: Version{Major: 2, Minor: -1, Patch: -1}}, // HTTP/2 (over TCP)
"h3": {Type: TypeHTTP, Encapsulation: TypeTLS, Version: Version{Major: 3, Minor: -1, Patch: -1}}, // HTTP/3
"irc": {Type: TypeIRC, Encapsulation: TypeTLS}, // IRC
"imap": {Type: TypeIMAP, Encapsulation: TypeTLS}, // IMAP
"managesieve": {Type: TypeManageSieve, Encapsulation: TypeTLS}, // ManageSieve
"mqtt": {Type: TypeMQTT, Encapsulation: TypeTLS}, // MQTT
"nntp": {Type: TypeNNTP, Encapsulation: TypeTLS}, // NNTP (reading)
"nnsp": {Type: TypeNNTP, Encapsulation: TypeTLS}, // NNTP (transit)
"postgresql": {Type: TypePostgreSQL, Encapsulation: TypeTLS}, // PostgreSQL
"pop3": {Type: TypePOP3, Encapsulation: TypeTLS}, // POP3
"radius/1.0": {Type: TypeRADIUS, Encapsulation: TypeTLS, Version: Version{Major: 1, Minor: 0, Patch: -1}}, // RADIUS/1.0
"radius/1.1": {Type: TypeRADIUS, Encapsulation: TypeTLS, Version: Version{Major: 1, Minor: 1, Patch: -1}}, // RADIUS/1.1
"smb": {Type: TypeSMB, Encapsulation: TypeTLS, Version: Version{Major: 2, Minor: -1, Patch: -1}}, // SMB2
"stun.nat-discovery": {Type: TypeSTUN, Encapsulation: TypeTLS}, // NAT discovery using Session Traversal Utilities for NAT (STUN)
"stun.turn": {Type: TypeSTUN, Encapsulation: TypeTLS}, // Traversal Using Relays around NAT (TURN)
"sunrpc": {Type: TypeSunRPC, Encapsulation: TypeTLS}, // SunRPC
"webrtc": {Type: TypeWebRTC, Encapsulation: TypeTLS}, // WebRTC Media and Data
"xmpp-client": {Type: TypeXMPP, Encapsulation: TypeTLS}, // XMPP jabber:client namespace
"xmpp-server": {Type: TypeXMPP, Encapsulation: TypeTLS}, // XMPP jabber:server namespace
}

View File

@@ -76,6 +76,45 @@ func TestDetectTLS(t *testing.T) {
0x03, 0x02, // Client Version: TLS 1.1 (Major=3, Minor=2) 0x03, 0x02, // Client Version: TLS 1.1 (Major=3, Minor=2)
} }
// A valid TLS 1.1 ServerHello
tls11ServerHello := []byte{
// --- Record Layer (5 bytes) ---
0x16, // Content Type: Handshake (22)
0x03, 0x02, // Version: TLS 1.1 (for compatibility in a 1.3 hello)
0x00, 0x40, // Length of the handshake message below (64 bytes)
// --- Handshake Protocol: ServerHello (64 bytes) ---
0x02, // Handshake Type: ServerHello (2)
0x00, 0x00, 0x3c, // Length of the rest of the message (60 bytes)
0x03, 0x02, // Server Version: TLS 1.1 (Major=3, Minor=2)
// Random (32 bytes)
0xb7, 0xa8, 0xdf, 0xd5, 0x17, 0xb1, 0x50, 0xb4,
0x28, 0xb7, 0xf6, 0xf3, 0xb9, 0x83, 0xcf, 0x9f,
0x31, 0x55, 0x79, 0x1f, 0x3b, 0x07, 0x6d, 0x17,
0x44, 0x4f, 0x57, 0x4e, 0x47, 0x52, 0x44, 0x00,
// Session ID
0x00, // Session ID Length: 0 (new session)
0xc0, 0x09, // Cipher Suite
0x00, // Compression Method
// --- Extensions (20 bytes) ---
0x00, 0x14, // Extensions Length: 20 bytes
0xff, 0x01, // Extension Type: renegotiation_info
0x00, 0x01, // Extension Length: 1 byte
0x00, // Renegotiation Info Length: 0 bytes
0x00, 0x10, // Extension Type: alpn
0x00, 0x05, // Extension Length: 5 bytes
0x00, 0x03, // ALPN Extension Length: 3 bytes
0x02, 'h', '2',
0x0, 0x0b, // Extension Type: ec_points_format
0x0, 0x02, // Extension Length: 2 bytes
0x01, // EC Points Format Length: 1 byte
0x00, // EC Point Format: uncompressed
}
// A synthesized TLSv1.2 ClientHello // A synthesized TLSv1.2 ClientHello
tls12ClientHello := []byte{ tls12ClientHello := []byte{
// --- Record Layer (5 bytes) --- // --- Record Layer (5 bytes) ---
@@ -129,35 +168,62 @@ func TestDetectTLS(t *testing.T) {
// A valid TLS 1.3 Client Hello (captured from a real connection) // A valid TLS 1.3 Client Hello (captured from a real connection)
tls13ClientHello := []byte{ tls13ClientHello := []byte{
// --- Record Layer (5 bytes) ---
0x16, // Content Type: Handshake (22) 0x16, // Content Type: Handshake (22)
0x03, 0x01, // Version: TLS 1.0 (for compatibility in a 1.3 hello) 0x03, 0x01, // Version: TLS 1.0 (for compatibility in a 1.3 hello)
0x01, 0x3a, // Length 0x01, 0x3a, // Length
0x01, 0x00, 0x01, 0x36, 0x03, 0x03, 0xb1,
0x40, 0xd3, 0xf1, 0x7d, 0xa3, 0xb8, 0x33, 0xac, 0xad, 0x21, 0x79, 0x9c, // --- Handshake Protocol: ClientHello ---
0xbe, 0x39, 0x96, 0x08, 0x49, 0x3b, 0x53, 0x75, 0xa0, 0x1b, 0xee, 0x6e, 0x01, // Handshake Type: ClientHello (1)
0x6a, 0xbe, 0x6c, 0x41, 0xdf, 0x6c, 0xf4, 0x20, 0xa4, 0xaa, 0x0c, 0xca, 0x00, 0x01, 0x36, // Length of the rest of the message
0xd4, 0x37, 0x76, 0x5f, 0x49, 0xc6, 0x06, 0x9b, 0xac, 0x90, 0x89, 0x76, 0x03, 0x03, // Client Version: TLS 1.2 (Major=3, Minor=3)
0x1c, 0xc7, 0xc4, 0x12, 0xb4, 0x4a, 0xe0, 0x27, 0x72, 0x89, 0x97, 0x85,
0x76, 0xf8, 0xc8, 0x83, 0x00, 0x62, 0x13, 0x03, 0x13, 0x02, 0x13, 0x01, // Random (32 bytes)
0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x30, 0xc0, 0x2c, 0xc0, 0x28, 0xb1, 0x40, 0xd3, 0xf1, 0x7d, 0xa3, 0xb8, 0x33,
0xc0, 0x24, 0xc0, 0x14, 0xc0, 0x0a, 0x00, 0x9f, 0x00, 0x6b, 0x00, 0x39, 0xac, 0xad, 0x21, 0x79, 0x9c, 0xbe, 0x39, 0x96,
0xff, 0x85, 0x00, 0xc4, 0x00, 0x88, 0x00, 0x81, 0x00, 0x9d, 0x00, 0x3d, 0x08, 0x49, 0x3b, 0x53, 0x75, 0xa0, 0x1b, 0xee,
0x00, 0x35, 0x00, 0xc0, 0x00, 0x84, 0xc0, 0x2f, 0xc0, 0x2b, 0xc0, 0x27, 0x6e, 0x6a, 0xbe, 0x6c, 0x41, 0xdf, 0x6c, 0xf4,
0xc0, 0x23, 0xc0, 0x13, 0xc0, 0x09, 0x00, 0x9e, 0x00, 0x67, 0x00, 0x33,
0x00, 0xbe, 0x00, 0x45, 0x00, 0x9c, 0x00, 0x3c, 0x00, 0x2f, 0x00, 0xba, // Session ID
0x00, 0x41, 0xc0, 0x11, 0xc0, 0x07, 0x00, 0x05, 0x00, 0x04, 0xc0, 0x12, 0x20, // Session ID Length: 32
0xc0, 0x08, 0x00, 0x16, 0x00, 0x0a, 0x00, 0xff, 0x01, 0x00, 0x00, 0x8b, 0xa4, 0xaa, 0x0c, 0xca, 0xd4, 0x37, 0x76, 0x5f, //
0x00, 0x2b, 0x00, 0x09, 0x08, 0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x49, 0xc6, 0x06, 0x9b, 0xac, 0x90, 0x89, 0x76, //
0x01, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x2c, 0x1c, 0xc7, 0xc4, 0x12, 0xb4, 0x4a, 0xe0, 0x27, //
0x4b, 0xaa, 0xb4, 0xb3, 0xc8, 0x93, 0xcd, 0x5c, 0x24, 0xb9, 0x9b, 0xd4, 0x72, 0x89, 0x97, 0x85, 0x76, 0xf8, 0xc8, 0x83, //
0x59, 0x04, 0xfe, 0x69, 0xaf, 0x68, 0xb9, 0xa6, 0x36, 0xbb, 0xab, 0x87, 0x00, 0x62, 0x13, 0x03, 0x13, 0x02, 0x13, 0x01, //
0xfa, 0x15, 0x59, 0xea, 0xdd, 0x38, 0x68, 0x00, 0x00, 0x00, 0x0e, 0x00,
0x0c, 0x00, 0x00, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, // Cipher Suites
0x74, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x30,
0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x0d, 0x00, 0xc0, 0x2c, 0xc0, 0x28, 0xc0, 0x24, 0xc0, 0x14,
0x18, 0x00, 0x16, 0x08, 0x06, 0x06, 0x01, 0x06, 0x03, 0x08, 0x05, 0x05, 0xc0, 0x0a, 0x00, 0x9f, 0x00, 0x6b, 0x00, 0x39,
0x01, 0x05, 0x03, 0x08, 0x04, 0x04, 0x01, 0x04, 0x03, 0x02, 0x01, 0x02, 0xff, 0x85, 0x00, 0xc4, 0x00, 0x88, 0x00, 0x81,
0x03, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x00, 0x9d, 0x00, 0x3d, 0x00, 0x35, 0x00, 0xc0,
0x00, 0x84, 0xc0, 0x2f, 0xc0, 0x2b, 0xc0, 0x27,
0xc0, 0x23, 0xc0, 0x13, 0xc0, 0x09, 0x00, 0x9e,
0x00, 0x67, 0x00, 0x33, 0x00, 0xbe, 0x00, 0x45,
0x00, 0x9c, 0x00, 0x3c, 0x00, 0x2f, 0x00, 0xba,
0x00, 0x41, 0xc0, 0x11, 0xc0, 0x07, 0x00, 0x05,
0x00, 0x04, 0xc0, 0x12, 0xc0, 0x08, 0x00, 0x16,
0x00, 0x0a, 0x00, 0xff, 0x01, 0x00, 0x00, 0x8b,
0x00, 0x2b, 0x00, 0x09, 0x08, 0x03, 0x04, 0x03,
0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x33, 0x00,
0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x2c,
0x4b, 0xaa, 0xb4, 0xb3, 0xc8, 0x93, 0xcd, 0x5c,
0x24, 0xb9, 0x9b, 0xd4, 0x59, 0x04, 0xfe, 0x69,
0xaf, 0x68, 0xb9, 0xa6, 0x36, 0xbb, 0xab, 0x87,
0xfa, 0x15, 0x59, 0xea, 0xdd, 0x38, 0x68, 0x00,
0x00, 0x00, 0x0e, 0x00, 0x0c, 0x00, 0x00, 0x09,
0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73,
0x74, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00,
0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00,
0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x0d, 0x00,
0x18, 0x00, 0x16, 0x08, 0x06, 0x06, 0x01, 0x06,
0x03, 0x08, 0x05, 0x05,
// Compression Methods, etc.
0x01, 0x05, 0x03, 0x08, 0x04, 0x04, 0x01, 0x04,
0x03, 0x02, 0x01, 0x02, 0x03, 0x00, 0x10, 0x00,
0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68,
0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31,
} }
@@ -172,15 +238,23 @@ func TestDetectTLS(t *testing.T) {
Direction: Client, Direction: Client,
Data: sslV3ClientHello, Data: sslV3ClientHello,
DstPort: 443, DstPort: 443,
WantProto: ProtocolSSL, WantType: TypeSSL,
WantConfidence: .95, WantConfidence: .95,
}, },
{ {
Name: "TLS 1.1", Name: "TLS 1.1 ClientHello",
Direction: Client, Direction: Client,
Data: tls11ClientHello, Data: tls11ClientHello,
DstPort: 443, DstPort: 443,
WantProto: ProtocolTLS, WantType: TypeTLS,
WantConfidence: .95,
},
{
Name: "TLS 1.1 ServerHello",
Direction: Server,
Data: tls11ServerHello,
SrcPort: 443,
WantType: TypeHTTP,
WantConfidence: .95, WantConfidence: .95,
}, },
{ {
@@ -188,7 +262,7 @@ func TestDetectTLS(t *testing.T) {
Direction: Client, Direction: Client,
Data: tls12ClientHello, Data: tls12ClientHello,
DstPort: 443, DstPort: 443,
WantProto: ProtocolTLS, WantType: TypeHTTP,
WantConfidence: .95, WantConfidence: .95,
}, },
{ {
@@ -196,7 +270,7 @@ func TestDetectTLS(t *testing.T) {
Direction: Client, Direction: Client,
Data: tls13ClientHello, Data: tls13ClientHello,
DstPort: 443, DstPort: 443,
WantProto: ProtocolTLS, WantType: TypeHTTP,
WantConfidence: .95, WantConfidence: .95,
}, },
{ {
@@ -225,7 +299,7 @@ func TestDetectTLS(t *testing.T) {
Direction: Client, Direction: Client,
Data: tls11ClientHelloPartial, Data: tls11ClientHelloPartial,
DstPort: 443, DstPort: 443,
WantProto: ProtocolTLS, WantType: TypeTLS,
WantConfidence: .50, WantConfidence: .50,
}, },
}, tests...)) }, tests...))

View File

@@ -16,7 +16,7 @@ type testCase struct {
Data []byte Data []byte
SrcPort int SrcPort int
DstPort int DstPort int
WantProto string WantType string
WantConfidence float64 WantConfidence float64
WantError error WantError error
} }
@@ -52,7 +52,7 @@ func testRunner(t *testing.T, tests []*testCase) {
} else if test.WantError != nil { } else if test.WantError != nil {
t.Fatalf("Detect(%s, %s, %d, %d) returned protocol %q version %s, expected error %q", t.Fatalf("Detect(%s, %s, %d, %d) returned protocol %q version %s, expected error %q",
test.Direction, testBytesSample(test.Data, 8), test.SrcPort, test.DstPort, test.Direction, testBytesSample(test.Data, 8), test.SrcPort, test.DstPort,
proto.Name, proto.Version, test.WantError) proto.Type, proto.Version, test.WantError)
return return
} }
@@ -60,16 +60,16 @@ func testRunner(t *testing.T, tests []*testCase) {
if proto == nil { if proto == nil {
t.Fatalf("Detect(%s, %s, %d, %d) returned nil, expected protocol %q", t.Fatalf("Detect(%s, %s, %d, %d) returned nil, expected protocol %q",
test.Direction, testBytesSample(test.Data, 8), test.SrcPort, test.DstPort, test.Direction, testBytesSample(test.Data, 8), test.SrcPort, test.DstPort,
test.WantProto) test.WantType)
return return
} }
t.Logf("Detect(%s, %s, %d, %d) returned protocol %q version %s with confidence %g%%", t.Logf("Detect(%s, %s, %d, %d) returned protocol %s with confidence %g%%",
test.Direction, testBytesSample(test.Data, 4), test.SrcPort, test.DstPort, test.Direction, testBytesSample(test.Data, 4), test.SrcPort, test.DstPort,
proto.Name, proto.Version, confidence*100) proto, confidence*100)
if proto.Name != test.WantProto { if proto.Type != test.WantType {
t.Errorf("Expected protocol %q", test.WantProto) t.Errorf("Expected protocol %q, got %q", test.WantType, proto.Type)
} }
if !testAlmostEqual(confidence, test.WantConfidence) { if !testAlmostEqual(confidence, test.WantConfidence) {
t.Errorf("Expected confidence %g%%", test.WantConfidence*100) t.Errorf("Expected confidence %g%%", test.WantConfidence*100)

View File

@@ -7,21 +7,63 @@ import (
// Protocols supported by this package. // Protocols supported by this package.
const ( const (
ProtocolDNS = "dns" TypeACME = "ACME"
ProtocolHTTP = "http" TypeCoAP = "CoAP"
ProtocolMQTT = "mqtt" TypeDNS = "DNS"
ProtocolMySQL = "mysql" TypeFTP = "FTP"
ProtocolPostgreSQL = "postgresql" TypeHTTP = "HTTP"
ProtocolSSH = "ssh" TypeIRC = "IRC"
ProtocolSSL = "ssl" TypeIMAP = "IMAP"
ProtocolTLS = "tls" TypeJabber = TypeXMPP
TypeManageSieve = "ManageSieve"
TypeMosquitto = TypeMQTT
TypeMQTT = "MQTT"
TypeMySQL = "MySQL"
TypeNNTP = "NNTP"
TypePOP3 = "POP3"
TypePgSQL = TypePostgreSQL
TypePostgreSQL = "PostgreSQL"
TypeRADIUS = "RADIUS"
TypeSamba = TypeSMB
TypeSIP = "SIP"
TypeSMB = "SMB"
TypeSSH = "SSH"
TypeSSL = "SSL"
TypeSTUN = "STUN"
TypeSunRPC = "SunRPC"
TypeTLS = "TLS"
TypeWebRTC = "WebRTC"
TypeXMPP = "XMPP"
) )
// Protocol description.
type Protocol struct { type Protocol struct {
Name string // Type of protocol, usually one of the constants defined in this package.
Type string
// Encapsulation type, usually one of the constants defined in this package.
//
// Empty if there is no encapsulation.
Encapsulation string
// Version of the protocol. Unknown versions are marked with [UnknownVersion].
Version Version Version Version
} }
func (proto Protocol) String() string {
var s string
if proto.Encapsulation != "" {
s = proto.Type + " (over " + proto.Encapsulation + ")"
} else {
s = proto.Type
}
if proto.Version == UnknownVersion {
return s
}
return s + " version " + proto.Version.String()
}
// Version of a protocol.
type Version struct { type Version struct {
Major int Major int
Minor int Minor int
@@ -29,15 +71,20 @@ type Version struct {
Extra string Extra string
} }
// UnknownVersion
var UnknownVersion Version
func (v Version) String() string { func (v Version) String() string {
if v == UnknownVersion {
return "unknown"
}
p := make([]string, 0, 3) p := make([]string, 0, 3)
if v.Major >= 0 { p = append(p, strconv.Itoa(v.Major))
p = append(p, strconv.Itoa(v.Major)) if v.Minor >= 0 {
if v.Minor >= 0 { p = append(p, strconv.Itoa(v.Minor))
p = append(p, strconv.Itoa(v.Minor)) if v.Patch >= 0 {
if v.Patch >= 0 { p = append(p, strconv.Itoa(v.Patch))
p = append(p, strconv.Itoa(v.Patch))
}
} }
} }
s := strings.Join(p, ".") s := strings.Join(p, ".")

32
tls.go
View File

@@ -19,7 +19,7 @@ type TLSExtension struct {
type TLSRecord struct { type TLSRecord struct {
Raw []byte Raw []byte
Type uint8 Type uint8
Version uint16 Version TLSVersion
Length uint16 Length uint16
Data []byte Data []byte
} }
@@ -30,26 +30,24 @@ func DecodeTLSRecord(data []byte) (*TLSRecord, error) {
record = &TLSRecord{Raw: data} record = &TLSRecord{Raw: data}
) )
var version uint16
if !stream.ReadUint8(&record.Type) || if !stream.ReadUint8(&record.Type) ||
!stream.ReadUint16(&record.Version) || !stream.ReadUint16(&version) ||
!stream.ReadUint16(&record.Length) { !stream.ReadUint16(&record.Length) {
return nil, DecodeError{ return nil, DecodeError{
Reason: "invalid TLS record header", Reason: "invalid TLS record header",
Err: io.ErrUnexpectedEOF, Err: io.ErrUnexpectedEOF,
} }
} }
record.Version = TLSVersion(version)
if !stream.ReadBytes(&record.Data, int(record.Length)) { if !stream.ReadBytes(&record.Data, int(record.Length)) {
return nil, DecodeError{ return nil, DecodeError{
Reason: "invalid TLS record data", Reason: "invalid TLS record data",
Err: io.ErrUnexpectedEOF, Err: io.ErrUnexpectedEOF,
} }
} }
if !stream.Empty() {
return nil, DecodeError{
Reason: "extraneous data after TLS record",
Err: ErrInvalid,
}
}
return record, nil return record, nil
} }
@@ -166,12 +164,6 @@ func DecodeTLSClientHello(data []byte) (*TLSClientHello, error) {
Err: io.ErrUnexpectedEOF, Err: io.ErrUnexpectedEOF,
} }
} }
if !record.Empty() {
return nil, DecodeError{
Reason: "extraneous TLS extension data",
Err: io.ErrUnexpectedEOF,
}
}
for !extensions.Empty() { for !extensions.Empty() {
var ( var (
@@ -260,6 +252,7 @@ type TLSServerHello struct {
CipherSuite uint16 CipherSuite uint16
CompressionMethod uint8 CompressionMethod uint8
Extensions []TLSExtension Extensions []TLSExtension
ALPNProtocols []string // RFC 7301, Section 3.1
} }
func DecodeTLSServerHello(data []byte) (*TLSServerHello, error) { func DecodeTLSServerHello(data []byte) (*TLSServerHello, error) {
@@ -347,12 +340,6 @@ func DecodeTLSServerHello(data []byte) (*TLSServerHello, error) {
Err: io.ErrUnexpectedEOF, Err: io.ErrUnexpectedEOF,
} }
} }
if !record.Empty() {
return nil, DecodeError{
Reason: "extraneous TLS extension data",
Err: io.ErrUnexpectedEOF,
}
}
for !extensions.Empty() { for !extensions.Empty() {
var ( var (
@@ -366,6 +353,11 @@ func DecodeTLSServerHello(data []byte) (*TLSServerHello, error) {
} }
} }
hello.Extensions = append(hello.Extensions, extension) hello.Extensions = append(hello.Extensions, extension)
switch extension.Type {
case tlsExtensionALPN:
_ = readTLSALPN(extensionData, &hello.ALPNProtocols)
}
} }
return hello, nil return hello, nil

View File

@@ -72,6 +72,39 @@ func TestDecodeTLSServerHello(t *testing.T) {
t.Fatalf("failed to decode test ServerHello: %s", err) t.Fatalf("failed to decode test ServerHello: %s", err)
} }
tls11ServerHello := []byte{
// --- Handshake Protocol: ServerHello (64 bytes) ---
0x02, // Handshake Type: ServerHello (2)
0x00, 0x00, 0x3c, // Length of the rest of the message (60 bytes)
0x03, 0x02, // Server Version: TLS 1.1 (Major=3, Minor=2)
// Random (32 bytes)
0xb7, 0xa8, 0xdf, 0xd5, 0x17, 0xb1, 0x50, 0xb4,
0x28, 0xb7, 0xf6, 0xf3, 0xb9, 0x83, 0xcf, 0x9f,
0x31, 0x55, 0x79, 0x1f, 0x3b, 0x07, 0x6d, 0x17,
0x44, 0x4f, 0x57, 0x4e, 0x47, 0x52, 0x44, 0x00,
// Session ID
0x00, // Session ID Length: 0 (new session)
0xc0, 0x09, // Cipher Suite
0x00, // Compression Method
// --- Extensions (20 bytes) ---
0x00, 0x14, // Extensions Length: 20 bytes
0xff, 0x01, // Extension Type: renegotiation_info
0x00, 0x01, // Extension Length: 1 byte
0x00, // Renegotiation Info Length: 0 bytes
0x00, 0x10, // Extension Type: alpn
0x00, 0x05, // Extension Length: 5 bytes
0x00, 0x03, // ALPN Extension Length: 3 bytes
0x02, 'h', '2',
0x0, 0x0b, // Extension Type: ec_points_format
0x0, 0x02, // Extension Length: 2 bytes
0x01, // EC Points Format Length: 1 byte
0x00, // EC Point Format: uncompressed
}
t.Run("Server Hello", func(t *testing.T) { t.Run("Server Hello", func(t *testing.T) {
hello, err := DecodeTLSServerHello(serverHelloBytes) hello, err := DecodeTLSServerHello(serverHelloBytes)
if err != nil { if err != nil {
@@ -80,6 +113,15 @@ func TestDecodeTLSServerHello(t *testing.T) {
} }
t.Logf("%#+v", hello) t.Logf("%#+v", hello)
}) })
t.Run("TLS 1.1 Server Hello", func(t *testing.T) {
hello, err := DecodeTLSServerHello(tls11ServerHello)
if err != nil {
t.Fatal(err)
return
}
t.Logf("%#+v", hello)
})
} }
func testDecodeHexString(s string) ([]byte, error) { func testDecodeHexString(s string) ([]byte, error) {