From cba7d2366bc4e637d6ead16e989b79703d8bfcf8 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sun, 25 Oct 2026 23:54:40 +0800 Subject: [PATCH] Fix truncated ClientHello parsing for some sizes 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. --- internal/server/clienthello.go | 2 +- internal/server/clienthello_test.go | 71 +++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/internal/server/clienthello.go b/internal/server/clienthello.go index 6fb6531..addce06 100644 --- a/internal/server/clienthello.go +++ b/internal/server/clienthello.go @@ -54,7 +54,7 @@ func readClientHello(conn net.Conn) (raw []byte, sni string, alpn []string, err 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]) + need = 4 + (int(hello[1])<<16 | int(hello[2])<<8 | int(hello[3])) } if len(hello) >= need { sni, alpn = parseClientHello(hello[4:need]) diff --git a/internal/server/clienthello_test.go b/internal/server/clienthello_test.go index f4e464d..2549e5f 100644 --- a/internal/server/clienthello_test.go +++ b/internal/server/clienthello_test.go @@ -42,6 +42,77 @@ func TestReadClientHello(t *testing.T) { } } +func TestReadClientHelloLengthBit2(t *testing.T) { + // Regression: a handshake length with bit 2 set used to compute `need` + // four bytes short, truncating the extensions block and losing SNI/ALPN. + sni := "demo.com" // body length 76: bit 2 is set + record := syntheticClientHello(sni, []string{"h2", "http/1.1"}) + if bodyLen := len(record) - 9; bodyLen&4 == 0 { + t.Fatalf("test hello body length %d does not exercise the bug", bodyLen) + } + + client, serverConn := net.Pipe() + go func() { + client.Write(append(record, 0xAA)) // sentinel after the hello + client.Close() + }() + serverConn.SetReadDeadline(time.Now().Add(2 * time.Second)) + raw, gotSNI, alpn, err := readClientHello(serverConn) + if err != nil { + t.Fatalf("readClientHello: %v", err) + } + if gotSNI != sni { + t.Errorf("sni = %q, want %q", gotSNI, sni) + } + if len(alpn) != 2 || alpn[0] != "h2" || alpn[1] != "http/1.1" { + t.Errorf("alpn = %v", alpn) + } + if len(raw) != len(record) { + t.Errorf("raw = %d bytes, want %d", len(raw), len(record)) + } + rest := make([]byte, 1) + if _, err := serverConn.Read(rest); err != nil || rest[0] != 0xAA { + t.Errorf("read past ClientHello: %v %x", err, rest) + } +} + +// syntheticClientHello builds one TLS record carrying a minimal ClientHello +// with the given SNI and ALPN protocols. +func syntheticClientHello(sni string, protocols []string) []byte { + body := []byte{0x03, 0x03} // legacy version + body = append(body, make([]byte, 32)...) // random + body = append(body, 0) // session id length + body = append(body, 0, 2, 0x13, 0x01) // cipher suites + body = append(body, 1, 0) // compression methods + + extension := func(typ uint16, data []byte) []byte { + out := []byte{byte(typ >> 8), byte(typ), byte(len(data) >> 8), byte(len(data))} + return append(out, data...) + } + + name := []byte(sni) + sniList := []byte{0, 0, byte(len(name))} // name type 0, name length + sniList = append(sniList, name...) + sniData := append([]byte{byte(len(sniList) >> 8), byte(len(sniList))}, sniList...) + + var alpnList []byte + for _, protocol := range protocols { + alpnList = append(alpnList, byte(len(protocol))) + alpnList = append(alpnList, protocol...) + } + alpnData := append([]byte{byte(len(alpnList) >> 8), byte(len(alpnList))}, alpnList...) + + exts := extension(0, sniData) + exts = append(exts, extension(16, alpnData)...) + body = append(body, byte(len(exts)>>8), byte(len(exts))) + body = append(body, exts...) + + handshake := []byte{1, byte(len(body) >> 16), byte(len(body) >> 8), byte(len(body))} + handshake = append(handshake, body...) + record := []byte{22, 0x03, 0x01, byte(len(handshake) >> 8), byte(len(handshake))} + return append(record, handshake...) +} + // captureConn records everything written to it. type captureConn struct { net.Conn