For reading and verification only. You may not copy, use, modify or redistribute this code. Full notice on the main page.
tunnel/sni.go
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "net" |
| 6 | "strings" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | // This file reads the CLEARTEXT part of a TLS ClientHello, only to learn the |
| 11 | // server name (SNI) the client asked for. Nothing in this program ever |
| 12 | // decrypts, terminates or alters a client's TLS session: the bytes read here |
| 13 | // are replayed verbatim to the device, which is the real TLS server. |
| 14 | |
| 15 | const ( |
| 16 | recordTypeHandshake = 22 |
| 17 | handshakeClientHello = 1 |
| 18 | |
| 19 | maxRecordPayload = 1 << 14 // TLS plaintext record limit |
| 20 | maxClientHelloBytes = 64 << 10 |
| 21 | maxHelloRecords = 32 |
| 22 | // Upper bound on the bytes buffered while looking for the SNI. |
| 23 | maxPeek = maxClientHelloBytes + maxHelloRecords*5 + 4 |
| 24 | ) |
| 25 | |
| 26 | var ( |
| 27 | errNotTLS = errors.New("not a TLS handshake record") |
| 28 | errNotClientHello = errors.New("handshake is not a ClientHello") |
| 29 | errHelloMalformed = errors.New("malformed ClientHello") |
| 30 | errHelloTooLarge = errors.New("ClientHello too large") |
| 31 | errBadSNI = errors.New("invalid server name") |
| 32 | ) |
| 33 | |
| 34 | // helloScan is the outcome of looking at a prefix of the client's bytes. |
| 35 | type helloScan struct { |
| 36 | complete bool // the whole ClientHello message is present |
| 37 | sni string // normalized server name; "" when the extension is absent |
| 38 | need int // when !complete: total buffer length required before rescanning is useful |
| 39 | err error |
| 40 | hello []byte // when complete: the ClientHello's body (the observer reads the groups it offers from it) |
| 41 | } |
| 42 | |
| 43 | // scanClientHello inspects buf, the bytes received so far from a client. It |
| 44 | // never panics on any input and never reads outside buf. The handshake message |
| 45 | // may be split across several TLS records (rare, but legal) and across any |
| 46 | // number of TCP reads. |
| 47 | func scanClientHello(buf []byte) helloScan { |
| 48 | if len(buf) == 0 { |
| 49 | return helloScan{need: 1} |
| 50 | } |
| 51 | var hs []byte |
| 52 | off := 0 |
| 53 | for rec := 0; ; rec++ { |
| 54 | if rec >= maxHelloRecords { |
| 55 | return helloScan{err: errHelloMalformed} |
| 56 | } |
| 57 | if len(buf)-off >= 1 && buf[off] != recordTypeHandshake { |
| 58 | return helloScan{err: errNotTLS} |
| 59 | } |
| 60 | if len(buf)-off >= 2 && buf[off+1] != 3 { |
| 61 | return helloScan{err: errNotTLS} |
| 62 | } |
| 63 | if len(buf)-off < 5 { |
| 64 | return helloScan{need: off + 5} |
| 65 | } |
| 66 | n := int(buf[off+3])<<8 | int(buf[off+4]) |
| 67 | if n == 0 || n > maxRecordPayload { |
| 68 | return helloScan{err: errHelloMalformed} |
| 69 | } |
| 70 | if len(buf)-off-5 < n { |
| 71 | return helloScan{need: off + 5 + n} |
| 72 | } |
| 73 | hs = append(hs, buf[off+5:off+5+n]...) |
| 74 | off += 5 + n |
| 75 | |
| 76 | if len(hs) >= 1 && hs[0] != handshakeClientHello { |
| 77 | return helloScan{err: errNotClientHello} |
| 78 | } |
| 79 | if len(hs) >= 4 { |
| 80 | l := int(hs[1])<<16 | int(hs[2])<<8 | int(hs[3]) |
| 81 | if l > maxClientHelloBytes { |
| 82 | return helloScan{err: errHelloTooLarge} |
| 83 | } |
| 84 | if len(hs) >= 4+l { |
| 85 | sni, err := parseClientHelloBody(hs[4 : 4+l]) |
| 86 | return helloScan{complete: true, sni: sni, err: err, hello: hs[4 : 4+l]} |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | // cursor is a bounds-checked reader over a byte slice. |
| 93 | type cursor struct { |
| 94 | b []byte |
| 95 | bad bool |
| 96 | } |
| 97 | |
| 98 | func (c *cursor) take(n int) []byte { |
| 99 | if c.bad || n < 0 || n > len(c.b) { |
| 100 | c.bad = true |
| 101 | return nil |
| 102 | } |
| 103 | out := c.b[:n] |
| 104 | c.b = c.b[n:] |
| 105 | return out |
| 106 | } |
| 107 | |
| 108 | func (c *cursor) u8() int { |
| 109 | b := c.take(1) |
| 110 | if b == nil { |
| 111 | return 0 |
| 112 | } |
| 113 | return int(b[0]) |
| 114 | } |
| 115 | |
| 116 | func (c *cursor) u16() int { |
| 117 | b := c.take(2) |
| 118 | if b == nil { |
| 119 | return 0 |
| 120 | } |
| 121 | return int(b[0])<<8 | int(b[1]) |
| 122 | } |
| 123 | |
| 124 | // parseClientHelloBody walks a ClientHello body (the bytes after the 4-byte |
| 125 | // handshake header) and returns the normalized host_name from the |
| 126 | // server_name extension: "" when there is none, errBadSNI when it is present |
| 127 | // but unusable. |
| 128 | func parseClientHelloBody(b []byte) (string, error) { |
| 129 | c := &cursor{b: b} |
| 130 | c.take(2) // legacy_version |
| 131 | c.take(32) // random |
| 132 | c.take(c.u8()) |
| 133 | if cs := c.u16(); cs < 2 || cs%2 != 0 { |
| 134 | return "", errHelloMalformed |
| 135 | } else { |
| 136 | c.take(cs) |
| 137 | } |
| 138 | c.take(c.u8()) // compression methods |
| 139 | if c.bad { |
| 140 | return "", errHelloMalformed |
| 141 | } |
| 142 | if len(c.b) == 0 { |
| 143 | return "", nil // no extensions at all |
| 144 | } |
| 145 | extLen := c.u16() |
| 146 | ext := &cursor{b: c.take(extLen)} |
| 147 | if c.bad || len(c.b) != 0 { // the extensions end the message: bytes after them are a hello that lies about its own length |
| 148 | return "", errHelloMalformed |
| 149 | } |
| 150 | found := false |
| 151 | name := "" |
| 152 | for len(ext.b) > 0 { |
| 153 | typ := ext.u16() |
| 154 | data := &cursor{b: ext.take(ext.u16())} |
| 155 | if ext.bad { |
| 156 | return "", errHelloMalformed |
| 157 | } |
| 158 | if typ != 0 { // server_name |
| 159 | continue |
| 160 | } |
| 161 | if found { |
| 162 | return "", errHelloMalformed // duplicate extension |
| 163 | } |
| 164 | found = true |
| 165 | listLen := data.u16() |
| 166 | list := &cursor{b: data.take(listLen)} |
| 167 | if data.bad || len(data.b) != 0 { |
| 168 | return "", errHelloMalformed |
| 169 | } |
| 170 | hostNames := 0 |
| 171 | for len(list.b) > 0 { |
| 172 | nameType := list.u8() |
| 173 | raw := list.take(list.u16()) |
| 174 | if list.bad { |
| 175 | return "", errHelloMalformed |
| 176 | } |
| 177 | if nameType != 0 { |
| 178 | continue |
| 179 | } |
| 180 | hostNames++ |
| 181 | if hostNames > 1 { |
| 182 | return "", errHelloMalformed |
| 183 | } |
| 184 | n, ok := normalizeName(string(raw)) |
| 185 | if !ok { |
| 186 | return "", errBadSNI |
| 187 | } |
| 188 | name = n |
| 189 | } |
| 190 | } |
| 191 | return name, nil |
| 192 | } |
| 193 | |
| 194 | // readClientHello reads from c until the ClientHello is complete (or invalid, |
| 195 | // too large, or the deadline passes). It returns EVERY byte it read from the |
| 196 | // connection, which the caller must forward to the device first. |
| 197 | func readClientHello(c net.Conn, timeout time.Duration) (peeked []byte, sni string, err error) { |
| 198 | _ = c.SetReadDeadline(time.Now().Add(timeout)) |
| 199 | defer func() { _ = c.SetReadDeadline(time.Time{}) }() |
| 200 | |
| 201 | buf := make([]byte, 0, 2048) |
| 202 | need := 1 |
| 203 | for { |
| 204 | for len(buf) >= need { |
| 205 | res := scanClientHello(buf) |
| 206 | if res.err != nil { |
| 207 | return buf, "", res.err |
| 208 | } |
| 209 | if res.complete { |
| 210 | return buf, res.sni, nil |
| 211 | } |
| 212 | need = res.need |
| 213 | if need > maxPeek { |
| 214 | return buf, "", errHelloTooLarge |
| 215 | } |
| 216 | } |
| 217 | if len(buf) == cap(buf) { |
| 218 | next := 2 * cap(buf) |
| 219 | if next > maxPeek { |
| 220 | next = maxPeek |
| 221 | } |
| 222 | if next <= len(buf) { |
| 223 | return buf, "", errHelloTooLarge |
| 224 | } |
| 225 | grown := make([]byte, len(buf), next) |
| 226 | copy(grown, buf) |
| 227 | buf = grown |
| 228 | } |
| 229 | n, rerr := c.Read(buf[len(buf):cap(buf)]) |
| 230 | buf = buf[:len(buf)+n] |
| 231 | if rerr != nil { |
| 232 | if n > 0 && len(buf) >= need { |
| 233 | continue // decide on what we have before reporting the error |
| 234 | } |
| 235 | return buf, "", rerr |
| 236 | } |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | // normalizeName is the one way a server name becomes a routing key, for an SNI and for a Host header alike: lower case, at most one trailing dot |
| 241 | // removed, then strict LDH. A name the table could hold under two spellings, or that a resolver would read differently from the node, is refused: |
| 242 | // letters, digits and hyphens only (no underscore), labels of 1..63 bytes that neither start nor end with a hyphen, at most 253 bytes in all, a last |
| 243 | // label that is not all digits (so no dotted-decimal address in disguise) and not an IP literal. The device's own name always passes: it is one |
| 244 | // label under a zone. |
| 245 | func normalizeName(s string) (string, bool) { |
| 246 | b := []byte(s) |
| 247 | for i, ch := range b { |
| 248 | if ch >= 'A' && ch <= 'Z' { |
| 249 | b[i] = ch + ('a' - 'A') |
| 250 | } |
| 251 | } |
| 252 | s = string(b) |
| 253 | if len(s) > 1 && s[len(s)-1] == '.' { |
| 254 | s = s[:len(s)-1] |
| 255 | } |
| 256 | if s == "" || len(s) > 253 || net.ParseIP(s) != nil { |
| 257 | return "", false |
| 258 | } |
| 259 | start := 0 |
| 260 | for i := 0; i <= len(s); i++ { |
| 261 | if i < len(s) && s[i] != '.' { |
| 262 | ch := s[i] |
| 263 | if !(ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '-') { |
| 264 | return "", false |
| 265 | } |
| 266 | continue |
| 267 | } |
| 268 | label := s[start:i] |
| 269 | if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' { |
| 270 | return "", false |
| 271 | } |
| 272 | if i == len(s) && strings.Trim(label, "0123456789") == "" { |
| 273 | return "", false |
| 274 | } |
| 275 | start = i + 1 |
| 276 | } |
| 277 | return s, true |
| 278 | } |