The handshake length expression combined shifts with +/|, so when bit 2 of the 24-bit length was set it computed four bytes too few. The extensions block was then truncated, SNI and ALPN parsed as empty, and the relay dropped the connection as "sni \"\" matches no tunnel hostname". It only hit certain ClientHello sizes, so some routes or ALPN choices reset while others worked. Parenthesize the length and add a regression test with a synthetic hello whose length sets bit 2.
154 lines
4.0 KiB
Go
154 lines
4.0 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
// clientHelloLimit bounds the bytes read while looking for a ClientHello
|
|
// (the reference uses 64 KiB and 10 seconds).
|
|
const (
|
|
clientHelloLimit = 64 * 1024
|
|
clientHelloTimeout = 10 * time.Second
|
|
)
|
|
|
|
// readClientHello reads TLS records from conn until the ClientHello is
|
|
// complete, without consuming anything beyond it. It returns the exact bytes
|
|
// read (for replay or forwarding), the SNI server name, and the offered ALPN
|
|
// protocols. Reads are exact (no read-ahead), so conn is handed on intact.
|
|
func readClientHello(conn net.Conn) (raw []byte, sni string, alpn []string, err error) {
|
|
conn.SetReadDeadline(time.Now().Add(clientHelloTimeout))
|
|
defer conn.SetReadDeadline(time.Time{})
|
|
|
|
var hello []byte // handshake message fragments, starting with the 4-byte header
|
|
need := -1 // total ClientHello bytes once the header is known
|
|
|
|
for len(raw) < clientHelloLimit {
|
|
header := make([]byte, 5)
|
|
if _, err = io.ReadFull(conn, header); err != nil {
|
|
return nil, "", nil, fmt.Errorf("reading record header: %w", err)
|
|
}
|
|
if header[0] != 22 { // handshake
|
|
return nil, "", nil, fmt.Errorf("expected a handshake record, got %d", header[0])
|
|
}
|
|
bodyLen := int(binary.BigEndian.Uint16(header[3:]))
|
|
if len(raw)+5+bodyLen > clientHelloLimit {
|
|
return nil, "", nil, errors.New("ClientHello exceeds 64 KiB")
|
|
}
|
|
body := make([]byte, bodyLen)
|
|
if _, err = io.ReadFull(conn, body); err != nil {
|
|
return nil, "", nil, fmt.Errorf("reading record body: %w", err)
|
|
}
|
|
raw = append(raw, header...)
|
|
raw = append(raw, body...)
|
|
hello = append(hello, body...)
|
|
|
|
if need < 0 {
|
|
if len(hello) < 4 {
|
|
continue
|
|
}
|
|
if hello[0] != 1 { // client_hello
|
|
return nil, "", nil, fmt.Errorf("expected ClientHello, got handshake type %d", hello[0])
|
|
}
|
|
need = 4 + (int(hello[1])<<16 | int(hello[2])<<8 | int(hello[3]))
|
|
}
|
|
if len(hello) >= need {
|
|
sni, alpn = parseClientHello(hello[4:need])
|
|
return raw, sni, alpn, nil
|
|
}
|
|
}
|
|
return nil, "", nil, errors.New("ClientHello exceeds 64 KiB")
|
|
}
|
|
|
|
// parseClientHello extracts SNI and ALPN from a ClientHello body (everything
|
|
// after the 4-byte handshake header). Truncation is an error; a missing
|
|
// extension simply yields an empty result, as the relay closes such
|
|
// connections anyway.
|
|
func parseClientHello(body []byte) (sni string, alpn []string) {
|
|
p := parser{data: body}
|
|
p.skip(2) // version
|
|
p.skip(32) // random
|
|
p.skip(int(p.u8())) // session id
|
|
p.skip(int(p.u16())) // cipher suites
|
|
p.skip(int(p.u8())) // compression methods
|
|
extLen := int(p.u16()) // extensions block
|
|
if p.err {
|
|
return "", nil
|
|
}
|
|
exts := parser{data: p.take(extLen)}
|
|
for !exts.empty() {
|
|
typ := exts.u16()
|
|
data := exts.take(int(exts.u16()))
|
|
if exts.err {
|
|
break
|
|
}
|
|
switch typ {
|
|
case 0: // server_name
|
|
d := parser{data: data}
|
|
list := parser{data: d.take(int(d.u16()))}
|
|
for !list.empty() {
|
|
nameType := list.u8()
|
|
name := list.take(int(list.u16()))
|
|
if list.err {
|
|
break
|
|
}
|
|
if nameType == 0 {
|
|
sni = string(name)
|
|
}
|
|
}
|
|
case 16: // application_layer_protocol_negotiation
|
|
d := parser{data: data}
|
|
list := parser{data: d.take(int(d.u16()))}
|
|
for !list.empty() {
|
|
proto := list.take(int(list.u8()))
|
|
if list.err {
|
|
break
|
|
}
|
|
alpn = append(alpn, string(proto))
|
|
}
|
|
}
|
|
}
|
|
return sni, alpn
|
|
}
|
|
|
|
// parser is a cursor over a byte slice; any out-of-bounds read sets err and
|
|
// yields zero values from then on.
|
|
type parser struct {
|
|
data []byte
|
|
err bool
|
|
}
|
|
|
|
func (p *parser) empty() bool { return p.err || len(p.data) == 0 }
|
|
|
|
func (p *parser) take(n int) []byte {
|
|
if n < 0 || len(p.data) < n {
|
|
p.err = true
|
|
return nil
|
|
}
|
|
out := p.data[:n]
|
|
p.data = p.data[n:]
|
|
return out
|
|
}
|
|
|
|
func (p *parser) skip(n int) { p.take(n) }
|
|
|
|
func (p *parser) u8() byte {
|
|
b := p.take(1)
|
|
if b == nil {
|
|
return 0
|
|
}
|
|
return b[0]
|
|
}
|
|
|
|
func (p *parser) u16() uint16 {
|
|
b := p.take(2)
|
|
if b == nil {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint16(b)
|
|
}
|