Skip to content

[2/4] Rebuild the transport core behind a backend seam - #9

Open
SendableMetatype wants to merge 6 commits into
Kas-tle:masterfrom
SendableMetatype:nethernet-transport-core
Open

[2/4] Rebuild the transport core behind a backend seam#9
SendableMetatype wants to merge 6 commits into
Kas-tle:masterfrom
SendableMetatype:nethernet-transport-core

Conversation

@SendableMetatype

@SendableMetatypeSendableMetatype commented Aug 2, 2026

Copy link
Copy Markdown

Stacked on #7. This branch contains its 3 commits; review the 3 commits after f10df32. As lower PRs merge I will rebase, so the diff collapses to this layer only.

The server data plane no longer reaches into libwebrtc. A WebRtcServerBackend interface owns offer negotiation and data channel transfer, with LibWebRtcServerBackend as the single class touching engine types: factory pooling, port allocator defaults, session tracking, and a lifecycle lock that makes close idempotent and safe against in flight accepts. NetherNet's countdown framing moves out of the channels into NetherNetFramingCodec, a standalone duplex handler covered by unit tests, fragmenting to the peer's advertised a=max-message-size, reassembling with a 16MB cap, and releasing partial reassembly buffers on teardown.

The performance and correctness work, all expressed against the seam:

  • Connection setup runs off the signaling thread, whose keepalives must never starve behind blocking native calls.
  • Optional PeerConnectionFactory pooling spreads DTLS and SCTP load across several native network threads.
  • The data path caches open state instead of a JNI call per write, sends through the engine's async path, which copies before returning so buffers never escape, and delivers inbound with a single copy. Watermark backpressure pauses writes at 2MB of engine backlog, resumes at 512KB, and closes deterministically once pending writes exceed 8MB.
  • Child channels expose the peer's real transport address from the ICE selected candidate pair. Remote candidates buffer until the remote description is applied. Client handshake retries are attempt scoped so stale engine callbacks cannot mutate a replacement attempt. Channel activation is guarded and single fire, hopping to the event loop when engine callbacks race it.
  • Rebased on master after Add identity to SDP response #12: the answer path keeps the server identity assertion, now attached in the channel's session bridge, where a signing failure logs and sends the answer undecorated instead of throwing through the engine callback that delivered it (which also skipped surfacing the accepted child).

One deliberate tradeoff to flag for your judgment: child channels previously all shared new InetSocketAddress(0), which is clearly recognizable as not a real address but breaks anything keyed on the remote address (per IP connection limits treat every NetherNet player as one host). Each connection now gets a unique random 10.x.x.x placeholder, which keeps address keyed logic functional but is less obviously fictional in logs. With the candidate pair callback the placeholder only covers the window before ICE nomination and the rare case where the callback never fires, so if you prefer the recognizable 0.0.0.0 or another marker there, I am happy to adjust.

The backend uses RTCDataChannel.sendAsync and PeerConnectionObserver.onSelectedCandidatePairChanged from Kas-tle/webrtc-java#3, so this compiles once a webrtc-java release contains that change; no dependency pin or repository changes are included. Suggested review order: the three backend interfaces, the framing codec and its tests, LibWebRtcServerBackend, then the channel rewiring.

This transport has run in production through Geyser for about a month, on servers with 30+ concurrent players: signaling drop recovery without player disconnects, sustained throughput, and correct remote addresses are all live confirmed. The two smallest commits, the send teardown race and the session lifecycle hardening, are recent, from a pre submission review of that production code.

@SendableMetatype
SendableMetatype marked this pull request as ready for review August 2, 2026 18:35
@SendableMetatypeSendableMetatype changed the title Rebuild the transport core behind a backend seam[2/4] Rebuild the transport core behind a backend seamAug 2, 2026
reconnect(freshToken) replaces only the socket to the signaling service.
The signaling instance, its handlers, and everything built on it (server
channel, WebRTC factories, live peer connections) survive, so a
signaling drop no longer requires tearing the transport down.
Liveness is now detectable on idle servers: a WebSocket protocol ping
every 15 seconds guarantees inbound pongs on a healthy socket, so
isChannelAlive(maxSilence) also catches silently half open TCP.
Per channel scheduled tasks are tracked and cancelled on channel
inactive, so reconnects no longer leak ping loops. TURN credential
pushes are applied for the lifetime of the socket instead of only
during connect, and the JSON RPC endpoint refreshes credentials every
30 minutes, so late joining peers no longer receive expired relay
credentials. Pending RPC requests fail fast when the socket dies. The
frame aggregator limit is raised to 128 KB for batched RPC frames.
Each in flight JSON RPC request now records the WebSocket channel it
was written to. When a socket dies, onChannelInactive fails only the
requests that were sent on that socket: during a reconnect the old
channel's inactive event can no longer fail requests already written
to the replacement socket, which previously left those callers with a
spurious ClosedChannelException while the reply was still on its way.
The object or array check introduced with the params support tested
the message envelope, which is always a JSON object, so the array
branch could never run: array form params fell into the object branch,
where getAsJsonObject throws on an array and the delivery was lost.
The check now tests the params element itself. Batched frames are
where array form params appear, so those deliveries were being
dropped.
The server data plane no longer reaches into libwebrtc. A
WebRtcServerBackend interface owns offer negotiation and data channel
transfer, with LibWebRtcServerBackend as the single class touching
engine types: factory pooling, port allocator defaults, session
tracking, and a lifecycle lock that makes close idempotent and safe
against in flight accepts, closing every live session before disposing
the factories they run on. Child channels attach to backend sessions
and the server channel bridges session events into netty.
Connection setup runs off the signaling thread so slow negotiation
never stalls the socket carrying every other player's signals. Sends
go through the engine's async path with watermark backpressure: the
channel pauses writes at 2MB of engine backlog, resumes at 512KB, and
closes deterministically once netty's pending writes exceed 8MB.
Remote candidates buffer until the remote description is applied.
Child channels expose the peer's real transport address from the
selected ICE candidate pair instead of a placeholder.
NetherNet's countdown framing moves out of the channels into
NetherNetFramingCodec, a standalone duplex handler placed first in the
pipeline. Channels now move raw fragments; the codec fragments to the
negotiated maximum message size, honoring the peer's advertised
a=max-message-size, and reassembles with a 16MB cap on both the
accumulation and completion paths. Covered by unit tests.
The client channel keeps direct engine access but gains the same
hardening: handshake retries are attempt scoped so stale engine
callbacks cannot mutate a replacement attempt, signal ids are re
validated on the event loop, and remote candidates buffer until the
answer is applied.
The server handshake reaper is cancelled by the child's close future
rather than from engine observer callbacks, so it fires only for a
connection that is still open but never activated.
Session.send checked the closed flag and then called into the engine
unguarded, so a session closed between the two could throw into the
caller's write path despite the documented drop contract; the native
call now honors the contract under the race too.
accept registered the session before creating its peer connection, so
an exception from engine setup retained the tracked session until
backend shutdown. Setup is now guarded and a failed session is closed
on the way out, which also unregisters it.
Session teardown closed both data channels and the peer connection in
a single try block, so one throwing close skipped the resources behind
it. Each resource now closes under its own guard.
@SendableMetatype
SendableMetatypeforce-pushed the nethernet-transport-core branch from a512c30 to 1a9065cCompareAugust 5, 2026 10:08
Sign up for freeto 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

@SendableMetatype