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