fix: answer a CER with invalid header bits after the handshake
Summary
On a connection whose capabilities exchange had completed, a CER with the P or E command flag set was handled as an ordinary repeated CER. It was dropped with a Warn log, or passed to OnCER first and then dropped, and it got no answer. Before the exchange, the same CER gets 3008, as does an Open-phase CER with a non-zero Application-ID. The protocol core now validates the header before it applies the repeated-CER policy. Such a CER therefore gets the 3008 answer, the connection closes after it, and OnCER is not called.
Server.RenewAcceptedCER also stops treating reserved command-flag bits as significant (RFC 6733 §3: receivers ignore them), so the Server and the protocol core classify the same CER the same way.
Closes #150.
Behaviour
| Input after CER/CEA | Before | After |
|---|---|---|
| CER with P set | dropped (OnCER called if set) | 3008, flags 0x60, then close |
| CER with E set | dropped (OnCER called if set) | 3008, flags 0x20, then close |
| CER with P set, externally admitted StateMachine without CEA metadata | consumed silently | 3008, flags 0x60, then close |
| CER with P set and a non-fatal decoding error | dropped | 3008, then close |
CER with reserved bits, RenewAcceptedCER on | not renewed, dropped | renewed like a plain CER |
| CER with non-zero Application-ID | 3008, then close | unchanged |
| Valid repeated CER (including T or reserved bits, or an invalid body) | Warn, dropped, connection kept | unchanged |
This holds for StateMachine (accepted and externally admitted), Client and Manager (accepted and dialed). The 3008 answer carries E, P as in the request, Application-ID 0, the request's identifiers and the local Origin-Host and Origin-Realm (RFC 6733 §§3, 5.3.1, 6.2, 7.1.3, 7.2; verified erratum 4887). Framing errors (5011, 5015) keep precedence.
Standards checked today: RFC 6733 is a Proposed Standard, not obsoleted, updated by RFC 7075 and RFC 8553, and neither changes these rules. None of its 19 errata adds PXY to CER or changes P mirroring. RFC 6737, cited in the README, is current and has no errata.
Testing
- Reproducer: the issue's reproducer passes unchanged on dd84d47, which shows the defect. A variant pointed at this branch receives 3008 with flags
0x60, then EOF, and no extraOnCERcall. - Core tests:
- every front-end configuration: native and externally admitted responders, initiators, hooks on and off, and each closure owner;
- P, E, P+E and Application-ID headers, crossed with decoded, non-fatal, fatal and framing errors;
- the exact required answer, close-after-reply, the notification, no hook and no watchdog credit;
- T-bit and reserved-bit controls keep the repeated-CER disposition.
- Public TCP tests:
- the four front ends, with and without
OnCER/OnCEA, plus every external-admission metadata shape; - the answer bytes, then EOF;
- events and logs identical to the non-zero Application-ID control.
- the four front ends, with and without
- Renewal: a TCP test of plain, reserved-bit and P-set CERs with
RenewAcceptedCER. - SCTP: tests for StateMachine and Manager accepted connections, run in Linux containers.
- Mutation: removing the header guard fails the new core and public tests, and restoring the reserved-bit mask fails the renewal test.
- Packet captures: over TCP for all four front ends and over SCTP for one accepted front end. Each shows the 3008 answer with flags
0x60before FIN or SCTP SHUTDOWN. - Allocations:
BenchmarkInboundMessagesin peer and sm shows identical B/op and allocs/op to main in all 14 cases. - Local validation: see below.
Local validation on the final tree:
scripts/check.sh: 9/9 steps passed (format, test 400 s, race 392 s, vet, lint, vuln, windows, secrets, codeql), on Go 1.27.3 and 1.26.10 for every module.- Host static tools: build, vet, staticcheck and golangci-lint passed for
.,oteldiamanddnsdiam; gopls is clean on the 13 touched Go files. - Race detector: 20 repetitions of each new or touched test passed under load on Go 1.27.2 and 1.26.9.
- SCTP: all 13 SCTP wire-harness variants passed in Docker.
- API: apidiff found no API change in 48 packages.
Summary by cubic
Fixes a bug where a CER with invalid header bits received after the handshake was silently dropped instead of being answered with a 3008 result.
Header validation now runs before the repeated-CER policy, so a CER with the P or E flag set gets the RFC 6733 3008 answer (E set, P mirrored, Application-ID 0) and the connection closes after the reply, on all four front ends: sm.StateMachine (native and externally admitted), client.Client, and peer.Manager (accepted and dialed).
- Invalid-header CERs no longer reach
OnCER, and no longer get silently consumed by externally admitted StateMachines without CEA metadata. - A CER with P set and a non-fatal decoding error also gets 3008; framing errors (5011, 5015) still take precedence.
Server.RenewAcceptedCERnow ignores reserved command-flag bits per RFC 6733 §3, so such CERs renew like plain ones; P and E never renew.- Valid repeated CERs (including T bits, reserved bits, or an invalid body) keep their existing Warn-and-drop behavior.
Fixes #150.