Skip to content

feat(cable): caller-driven teardown and linger for linking data - #276

Draft
AlfioEmanueleFresta wants to merge 13 commits into
masterfrom
fix/cable-shutdown-and-linger
Draft

AlfioEmanueleFresta wants to merge 13 commits into
masterfrom
fix/cable-shutdown-and-linger

Conversation

@AlfioEmanueleFresta

@AlfioEmanueleFresta AlfioEmanueleFresta commented Jun 15, 2026

Copy link
Copy Markdown
Member

On hybrid connections the client never sent a Shutdown frame and tore the connection down as soon as the response arrived, so a linking update the phone sends a little later was always lost.

The caller now decides how a hybrid connection ends. Closing the channel sends Shutdown and waits for the connection to terminate. Cancelling drops it without a goodbye, and so does dropping the channel. Closing with the linger mode sends Shutdown and keeps receiving in the background, for a configurable window under a hard cap, to capture the linking update for a later state-assisted reconnect. Callers opt in by carrying a linger config on the channel settings. Its registry evicts a lingering connection when a new one is opened, and can be drained on suspend or shutdown. Only a state-assisted QR connection with a store can linger, everything else just closes.

Fixed along the way: a close during connecting no longer hangs, an inbound Shutdown decodes as a clean peer close instead of a framing error, every outbound send is bounded, and tunnel messages are capped at the Noise ceiling.

Source breaking and wants a 0.11.0 bump: ChannelSettings gained a field, so struct literals need ..Default::default(), and the cable channel's close used to be a no-op.

Part of #257.

On hybrid connections the client never sent a Shutdown frame and tore
the connection down immediately after the response. This sends a
Shutdown control frame when the channel is closed and adds the
encrypted test harness for the tunnel protocol.

Part of #257.
A Shutdown control frame carries no payload, so the empty-payload check
rejected it as invalid framing before the message type was inspected and
a peer shutdown surfaced as a UX error instead of a clean close.
Nothing publishes it yet. A lingering connection admits no further
operations and reports as closed.
…tics

Replace the one-shot close signal with a teardown watch shared between
the channel and the connection task. close() sends Shutdown and waits
for termination, cancel() drops the connection without a goodbye, and an
unattended drop is a hard cancel. The connect and handshake stages and
the initial receive are interruptible, so a close during connecting no
longer hangs, and every outbound send is bounded.
A state-assisted QR connection can keep receiving after the ceremony so
the authenticator's linking update is stored for a later state-assisted
reconnect. The caller opts in with a CableLingerConfig on
ChannelSettings and calls linger() on the channel. The linger is
detached from the channel, bounded by a configurable window under a
hard cap, and tracked in a caller-owned CableLingerRegistry so a new
connection evicts it and the caller can drain it on suspend.
The tunnel accepted messages up to the tungstenite default of 64 MiB
before the protocol layer could reject them. Cap them at the CBOR bound
plus framing overhead.
Dropping the last registry clone cancelled every tracked connection,
including one still connecting or mid-ceremony, so a registry built
inline in the settings was gone by the time channel() returned and the
connection died silently. Registry drop now cancels lingerers only, and
a connection whose registry is gone by the time it would linger closes
instead.

Also bound the linking update store write in the active phase so a
wedged store cannot hold off a close, and count only undecryptable
frames toward the linger budget so unknown frame types are ignored as
intended.
A close that does not complete within the flush timeout now cancels the
connection so close means closed. A close after linger() no longer
waits out the flush timeout for a termination that will not come.
A Noise transport message is at most 65535 bytes, so anything larger
could never be decrypted. Bound WebSocket messages there instead of at
the CBOR size.
CableChannel::close(CableClose::Immediate) sends Shutdown and waits for
termination, CableClose::Linger keeps receiving for a late linking
update. The trait close() maps to Immediate.
@AlfioEmanueleFresta
AlfioEmanueleFresta force-pushed the fix/cable-shutdown-and-linger branch from 155ff1c to f746e4d Compare September 19, 2026 15:48
@AlfioEmanueleFresta AlfioEmanueleFresta changed the title fix(cable): send shutdown and linger for linking data feat(cable): caller-driven teardown and linger for linking data Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant