Files
tunnel/internal/server/clienthello.go
T
TZGyn 682a341010 M1: single-node OpenTunnel-compatible server
HTTP API per the recorded contract, bridge WebSocket, SNI relay with
pass-through tunnel TLS, local CA issuance, JSON store. Verified
end-to-end with the official opentunnel CLI.
2026-10-25 11:52:45 +08:00

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)
}