Skip to content

connector, mux: close four network-management gaps - #28

Merged
canaan5 merged 2 commits into
mainfrom
feat/network-management-gaps
Sep 11, 2026
Merged

canaan5 merged 2 commits into
mainfrom
feat/network-management-gaps

Conversation

@canaan5

@canaan5 canaan5 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Found while tracing ISW/ZONE link instability in a downstream acquirer. Each gap forced a workaround that could not fully fix the problem from outside the library. All four changes are additive — an existing configuration behaves exactly as before.

1. No way to reply to an unsolicited frame

Unsolicited handed over the frame and nothing else, and neither Mux nor Connector exposed a raw Send, so a peer-initiated message could not be answered at all. Request is the wrong tool — the frame we would send is the response, so there is nothing to wait for.

This matters because Postilion-family hosts do not only answer echoes, they send them: a host that echo-tests us and never receives an 0810 concludes the link is dead and closes it, which is indistinguishable from a flapping network.

Adds mux.Reply, mux.WithUnsolicitedReplier, (*Mux).Send, (*Connector).Send, connector.Config.OnUnsolicited.

2. Keyer is direction-blind

One keyer served both directions. With a single function no keyer can match a response to its request and separate a peer's request from our own — the two are structurally identical. Network management correlates on the trace alone (an echo carries no terminal id), so a peer 0800 on a colliding trace was delivered as our 0810 while the real answer arrived unmatched and was dropped.

Adds mux.WithResponseKeyer / connector.Config.ResponseKeyer: the request keyer maps an outgoing frame to the key of the response it expects, the response keyer maps an inbound frame by its own MTI. Our 0800 registers under 0810:<trace>, the real 0810 matches, and a peer's 0800 keys to 0800:<trace> — no match, so it reaches the unsolicited handler where it belongs.

3. A keepalive inherited the request timeout

Worth stating precisely, because the obvious diagnosis is wrong: a peer that dies cleanly (EOF/RST) fails the mux from the read loop, which unblocks the pending keepalive immediately — the synchronous serve loop handled that case fine.

The gap is the half-open peer — TCP established, application silent, no FIN (a NAT idle-out, a wedged host thread). Nothing closes the mux, so the echo waits out Timeout, a value sized for a financial round-trip. Until it gives up the connector goes on reporting Connected() and accepting requests onto a socket that will never answer: a window of KeepaliveInterval + Timeout.

Adds connector.Config.KeepaliveTimeout, bounding one keepalive independently (0 falls back to Timeout). serve also runs the keepalive off the supervise loop — that does not shrink the half-open window, but it stops an outstanding keepalive holding Stop hostage.

4. No hook after the mux is published

OnConnect runs before c.mux.Store(m). That ordering is right for sign-on — the session must exist before the link is usable — but it leaves nowhere for a ceremony that must go through the connector's own request path (a working-key exchange, a parameter download): from OnConnect it fails ErrNotConnected.

Adds connector.Config.OnReady, invoked in its own goroutine right after the mux is published. Unlike OnConnect it cannot drop the link — its failure is the caller's to handle.

Tests

Six, in connector/netmgmt_test.go, over a small line-framed peer:

  • a peer-originated echo is answered, and the answer reaches the peer
  • Send on a down link returns ErrNotConnected
  • with ResponseKeyer, a peer request on our trace reaches the unsolicited handler and our own request still times out
  • a half-open peer is detected without waiting out the request timeout — this one fails without KeepaliveTimeout
  • an outstanding keepalive does not block Stop
  • OnReady can use the connector; OnConnect cannot (documents why OnReady exists)

go vet, gofmt and the full suite are clean.

Found while tracing ISW/ZONE link instability in a downstream acquirer. Each gap
forced a workaround that could not fully fix the problem from outside the library.

Reply to an unsolicited frame. Unsolicited handed over the frame and nothing else,
and neither Mux nor Connector exposed a raw Send — so a peer-initiated message
could not be answered at all. Postilion-family hosts do not only answer echoes,
they send them, and a host that echo-tests us and never receives an 0810 concludes
the link is dead and tears it down. Adds mux.Reply, WithUnsolicitedReplier,
(*Mux).Send, (*Connector).Send and Config.OnUnsolicited.

Direction-blind Keyer. One keyer served both directions, and with a single function
no keyer can match a response to its request and also separate a peer's request from
our own — the two are structurally identical. Network management correlates on the
trace alone, so a peer 0800 on a colliding trace was delivered as our 0810 while the
real answer arrived unmatched. Adds WithResponseKeyer / Config.ResponseKeyer: the
request keyer maps an outgoing frame to the key of the response it expects, the
response keyer maps an inbound frame by its own MTI.

Keepalive inheriting the request timeout. A peer that dies cleanly unblocks the
pending keepalive through the mux, so that case was always handled. The gap is the
half-open peer — TCP established, application silent, no FIN — where nothing closes
the mux and the echo waits out a timeout sized for a financial round-trip, while the
connector goes on reporting Connected() and accepting traffic onto a dead socket.
Adds Config.KeepaliveTimeout, bounding one keepalive independently. serve also runs
the keepalive off the supervise loop, which does not shrink that window but stops an
outstanding keepalive holding Stop hostage.

No hook after the mux is published. OnConnect runs before the mux is stored, which
is right for sign-on but leaves nowhere for a ceremony that must go through the
connector's own request path — it fails ErrNotConnected. Adds Config.OnReady.

All additive: existing configurations behave exactly as before. Six tests, including
one that fails without KeepaliveTimeout and one documenting why OnReady exists.
@canaan5
canaan5 merged commit bfbf0fa into main Sep 11, 2026
6 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant