# Go 1.27 TLS 1.3 ClientHello Size Enlargement (~1.5KB) with ML-KEM/ML-DSA Cause…s Middlebox Handshake Stalls
## Issue Summary
Starting in Go 1.27 (`go1.27rc1` / `go1.27rc2`), default TLS 1.3 clients (`tls.Config{}`) advertise both **ML-KEM 768** key exchange (`X25519MLKEM768` / `0x11ec`) and **ML-DSA** signature algorithms (introduced in upstream Go commit `c74ba7d265519fab5794e5b69e4e9f6c25b45582`).
The resulting `ClientHello` payload size expands to **~1,467 bytes** (exceeding standard 1,200-1,400 byte network MTU / middlebox buffer limits). When connecting through legacy network middleboxes, firewalls, or ingress proxies (such as Envoy/Istio without PQC extension support), the enlarged `ClientHello` frame is silently dropped or causes the proxy connection to stall, leading to `net/http: TLS handshake timeout` errors.
---
## Culprit Commit in Upstream Go Repository
* **Commit**: `c74ba7d265519fab5794e5b69e4e9f6c25b45582`
* **Gerrit CL**: https://go-review.googlesource.com/c/go/+/776709
* **Title**: `crypto/tls: add ML-DSA support` (Fixes #78888)
---
## Standalone Vanilla Reproduction Code
### 1. `client.go` (Vanilla Go TLS Client Diagnostic)
Zero-dependency, standard library Go TLS client to compare connection modes.
```go
package main
import (
"crypto/tls"
"flag"
"fmt"
"net"
"net/http"
"os"
"runtime"
"strings"
"time"
)
func curveName(id tls.CurveID) string {
switch id {
case tls.X25519MLKEM768:
return "X25519MLKEM768 (ML-KEM 768 / Kyber)"
case tls.X25519:
return "X25519"
case tls.CurveP256:
return "P-256"
case tls.CurveP384:
return "P-384"
default:
if id == 0 {
return "None"
}
return fmt.Sprintf("0x%04x", uint16(id))
}
}
func tlsVersionName(version uint16) string {
switch version {
case tls.VersionTLS13:
return "TLS 1.3"
case tls.VersionTLS12:
return "TLS 1.2"
default:
return fmt.Sprintf("0x%04x", version)
}
}
type testConfig struct {
name string
tlsConfig *tls.Config
}
func testTLS(target string, timeout time.Duration, tc testConfig) {
fmt.Printf("--------------------------------------------------------------------------------\n")
fmt.Printf("Mode: %s\n", tc.name)
fmt.Printf("--------------------------------------------------------------------------------\n")
host, _, err := net.SplitHostPort(target)
if err != nil {
host = target
target = net.JoinHostPort(target, "443")
}
cfg := tc.tlsConfig.Clone()
if cfg.ServerName == "" {
cfg.ServerName = host
}
cfg.InsecureSkipVerify = true
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: cfg,
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
TLSHandshakeTimeout: timeout,
},
Timeout: timeout,
}
url := fmt.Sprintf("https://%s", target)
start := time.Now()
resp, err := client.Get(url)
elapsed := time.Since(start)
if err != nil {
fmt.Printf("Status: ❌ FAILED (%.3fs)\n", elapsed.Seconds())
fmt.Printf("Error: %v\n\n", err)
return
}
defer resp.Body.Close()
state := resp.TLS
fmt.Printf("Status: ✅ SUCCESS (%.3fs)\n", elapsed.Seconds())
fmt.Printf("Version: %s\n", tlsVersionName(state.Version))
fmt.Printf("Cipher: %s\n", tls.CipherSuiteName(state.CipherSuite))
fmt.Printf("Group: %s\n\n", curveName(state.CurveID))
}
func main() {
targetFlag := flag.String("target", "", "Target host:port to test")
timeoutFlag := flag.Duration("timeout", 5*time.Second, "Timeout duration")
flag.Parse()
target := *targetFlag
if target == "" {
if flag.NArg() > 0 {
target = flag.Arg(0)
} else {
fmt.Println("Usage: client -target <host:port>")
os.Exit(1)
}
}
if !strings.Contains(target, ":") {
target = net.JoinHostPort(target, "443")
}
fmt.Printf("================================================================================\n")
fmt.Printf("TLS Client Diagnostic Tool\n")
fmt.Printf("Go Runtime Version: %s\n", runtime.Version())
fmt.Printf("Target: %s\n", target)
fmt.Printf("================================================================================\n\n")
testTLS(target, *timeoutFlag, testConfig{
name: "1. Default TLS 1.3 (Runtime Defaults)",
tlsConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
},
})
testTLS(target, *timeoutFlag, testConfig{
name: "2. TLS 1.3 with Explicit ML-KEM Curve Preference",
tlsConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519},
},
})
testTLS(target, *timeoutFlag, testConfig{
name: "3. TLS 1.3 without ML-KEM (Standard Curves: X25519, P-256, P-384)",
tlsConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384},
},
})
testTLS(target, *timeoutFlag, testConfig{
name: "4. Forced TLS 1.2 (MaxVersion = VersionTLS12)",
tlsConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
},
})
}
```
---
## Behavior Matrix
| Connection Mode | Go 1.25 / 1.26 | Go 1.27 (Default `tlsmlkem=1`) | Middlebox Impact |
| :--- | :--- | :--- | :--- |
| **Default TLS 1.3** | ✅ SUCCESS (~239B ClientHello) | ❌ TIMEOUT (~1,467B ClientHello) | Connection stall / packet drop |
| **Explicit ML-KEM** | ✅ SUCCESS | ❌ TIMEOUT (~1,457B ClientHello) | Connection stall / packet drop |
| **Standard Curves Only** | ✅ SUCCESS | ✅ SUCCESS | Bypasses middlebox drop |
| **Forced TLS 1.2** | ✅ SUCCESS | ✅ SUCCESS | Bypasses middlebox drop |