{"record":{"id":"fd415556df5bca32","repo":"golang/go","slug":"tls-server-sent-two-helloretryrequest-messages","errorCode":null,"errorMessage":"tls: server sent two HelloRetryRequest messages","messagePattern":"tls: server sent two HelloRetryRequest messages","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/crypto/tls/handshake_client_tls13.go","lineNumber":417,"sourceCode":"\t\tc.sendAlert(alertUnexpectedMessage)\n\t\treturn unexpectedMessageError(serverHello, msg)\n\t}\n\ths.serverHello = serverHello\n\n\tif err := hs.checkServerHelloOrHRR(); err != nil {\n\t\treturn err\n\t}\n\n\tc.didHRR = true\n\treturn nil\n}\n\nfunc (hs *clientHandshakeStateTLS13) processServerHello() error {\n\tc := hs.c\n\n\tif bytes.Equal(hs.serverHello.random, helloRetryRequestRandom) {\n\t\tc.sendAlert(alertUnexpectedMessage)\n\t\treturn errors.New(\"tls: server sent two HelloRetryRequest messages\")\n\t}\n\n\tif len(hs.serverHello.cookie) != 0 {\n\t\tc.sendAlert(alertUnsupportedExtension)\n\t\treturn errors.New(\"tls: server sent a cookie in a normal ServerHello\")\n\t}\n\n\tif hs.serverHello.selectedGroup != 0 {\n\t\tc.sendAlert(alertDecodeError)\n\t\treturn errors.New(\"tls: malformed key_share extension\")\n\t}\n\n\tif hs.serverHello.serverShare.group == 0 {\n\t\tc.sendAlert(alertIllegalParameter)\n\t\treturn errors.New(\"tls: server did not send a key share\")\n\t}\n\tif !slices.ContainsFunc(hs.hello.keyShares, func(ks keyShare) bool {\n\t\treturn ks.group == hs.serverHello.serverShare.group","sourceCodeStart":399,"sourceCodeEnd":435,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/crypto/tls/handshake_client_tls13.go#L399-L435","documentation":"processServerHello compares the server_random against the canonical helloRetryRequestRandom sentinel. RFC 8446 §4.1.3 permits at most one HelloRetryRequest per handshake; a second random equal to the HRR sentinel means the server sent a second HRR. Go sends `unexpected_message` and aborts. This is a hard protocol violation.","triggerScenarios":"After already processing one HelloRetryRequest (hs.didHRR == true via the earlier flow), the subsequent ServerHello's random again equals helloRetryRequestRandom. Produced by a server that loops on HRR or replays it.","commonSituations":"Faulty server implementations of TLS 1.3 retry logic, adversarial fuzzers, or stateful MITM proxies that re-issue HRR. No legitimate mainstream server does this.","solutions":["Treat the peer as broken/malicious; report to the server operator.","Log and fail the connection — there is no client-side workaround that preserves security.","Verify against a different server hostname/IP to rule out a route-specific middlebox.","If you operate the server, ensure HelloRetryRequest is sent at most once per handshake."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Second HelloRetryRequest is a protocol violation; abort permanently for this peer.\nconn, err := tls.Dial(\"tcp\", addr, cfg)\nif err != nil {\n    if strings.Contains(err.Error(), \"two HelloRetryRequest\") {\n        blocklist.Add(addr) // server is broken or malicious\n    }\n    return err\n}","preventionTips":["Maintain a blocklist of peers that emit non-conformant handshakes.","Do not auto-retry on protocol-violation errors; they indicate a broken or hostile peer.","Capture handshakes for forensic review when these errors cluster."],"tags":["tls","tls13","handshake","helloretryrequest","protocol-violation","network","go"],"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T08:17:17.861Z"}