-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsni.go
More file actions
127 lines (111 loc) · 3.23 KB
/
sni.go
File metadata and controls
127 lines (111 loc) · 3.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package main
import (
"encoding/binary"
"fmt"
)
// extractSNI parses a raw TLS ClientHello message and extracts the SNI
// (Server Name Indication) hostname. The input should start with the TLS
// record header (content type 0x16).
//
// Returns empty string if no SNI extension is found.
func extractSNI(data []byte) (string, error) {
// TLS record: type(1) + version(2) + length(2) + fragment
if len(data) < 5 {
return "", fmt.Errorf("too short for TLS record header")
}
if data[0] != 0x16 {
return "", fmt.Errorf("not a TLS handshake record (got 0x%02x)", data[0])
}
recordLen := int(binary.BigEndian.Uint16(data[3:5]))
fragment := data[5:]
if len(fragment) < recordLen {
// We may not have the full record, but try to parse what we have.
fragment = fragment[:len(fragment)]
} else {
fragment = fragment[:recordLen]
}
// Handshake: type(1) + length(3) + body
if len(fragment) < 4 {
return "", fmt.Errorf("too short for handshake header")
}
if fragment[0] != 0x01 { // ClientHello
return "", fmt.Errorf("not a ClientHello (got 0x%02x)", fragment[0])
}
// handshakeLen := int(fragment[1])<<16 | int(fragment[2])<<8 | int(fragment[3])
body := fragment[4:]
// ClientHello: version(2) + random(32) + sessionID(1+var) + cipherSuites(2+var) + compression(1+var) + extensions(2+var)
if len(body) < 34 {
return "", fmt.Errorf("too short for ClientHello fixed fields")
}
pos := 34 // skip version + random
// Session ID
if pos >= len(body) {
return "", fmt.Errorf("truncated at session ID")
}
sessionIDLen := int(body[pos])
pos += 1 + sessionIDLen
// Cipher suites
if pos+2 > len(body) {
return "", fmt.Errorf("truncated at cipher suites")
}
cipherSuitesLen := int(binary.BigEndian.Uint16(body[pos:]))
pos += 2 + cipherSuitesLen
// Compression methods
if pos >= len(body) {
return "", fmt.Errorf("truncated at compression methods")
}
compressionLen := int(body[pos])
pos += 1 + compressionLen
// Extensions
if pos+2 > len(body) {
return "", nil // No extensions — no SNI
}
extensionsLen := int(binary.BigEndian.Uint16(body[pos:]))
pos += 2
extensionsEnd := pos + extensionsLen
if extensionsEnd > len(body) {
extensionsEnd = len(body)
}
for pos+4 <= extensionsEnd {
extType := binary.BigEndian.Uint16(body[pos:])
extLen := int(binary.BigEndian.Uint16(body[pos+2:]))
pos += 4
if pos+extLen > extensionsEnd {
break
}
if extType == 0x0000 { // SNI extension
extData := body[pos : pos+extLen]
return parseSNIExtension(extData)
}
pos += extLen
}
return "", nil // No SNI extension found
}
// parseSNIExtension parses the SNI extension data and returns the hostname.
func parseSNIExtension(data []byte) (string, error) {
// ServerNameList: length(2) + list of ServerName entries
if len(data) < 2 {
return "", fmt.Errorf("SNI extension too short")
}
listLen := int(binary.BigEndian.Uint16(data))
list := data[2:]
if len(list) < listLen {
list = list[:len(list)]
} else {
list = list[:listLen]
}
pos := 0
for pos+3 <= len(list) {
nameType := list[pos]
nameLen := int(binary.BigEndian.Uint16(list[pos+1:]))
pos += 3
if pos+nameLen > len(list) {
break
}
if nameType == 0x00 { // host_name
return string(list[pos : pos+nameLen]), nil
}
pos += nameLen
}
return "", nil
}