Skip to content

[3/4] HTTP signaling front end with identity handling - #10

Open
SendableMetatype wants to merge 10 commits into
Kas-tle:masterfrom
SendableMetatype:nethernet-http-signaling
Open

[3/4] HTTP signaling front end with identity handling#10
SendableMetatype wants to merge 10 commits into
Kas-tle:masterfrom
SendableMetatype:nethernet-http-signaling

Conversation

@SendableMetatype

@SendableMetatypeSendableMetatype commented Aug 2, 2026

Copy link
Copy Markdown

Stacked on #9. This branch contains the 6 commits of the PRs below it (#7, #9); review the 4 commits after 1a9065c. As lower PRs merge I will rebase, so the diff collapses to this layer only.

Implements the HTTP signaling protocol from Mojang's official NetherNet partner onboarding guide, matching the go-nethernet reference: NetherNetHttpSignaling serves NetherNet's direct connection model on TCP under the Bedrock port, with GET /v1/join as the capability check and POST /v1/join/{networkId} exchanging the SDP offer for a full ICE answer in one round trip. One shot connections, a 1 MiB offer cap, uint64 network id validation, and a 502 after the negotiation timeout. The backend gains a full ICE accept mode: no trickle candidates in either direction, the answer re read from the engine after gathering completes so it carries every candidate, terminated with a=end-of-candidates. TLS is a Supplier of SslContext consulted per connection, so certificate rotation needs no rebind.

Identity handling follows the guide's validation flow (section 5): client a=identity assertions are stripped before the SDP reaches setRemoteDescription, as the guide instructs, with the raw offer available above the backend for validation. A NETHER_SERVER_ANSWER_DECORATOR channel option is the seam for the server identity assertion; when no decorator is set the channel signs with the built in self signed ServerIdentity from #12, so the 26.40 requirement stays satisfied out of the box, and consumers replace it to own the assertion's keys and domain (for a persistent identity that survives restarts). A decoration failure now fails the exchange explicitly (a 400 on HTTP) rather than sending an answer the client will only refuse after parsing.

Server backends are also created lazily: the factory pool materializes in doBind only after the signaling endpoint bound, so a taken TCP port creates no native state instead of creating factories and disposing them milliseconds later, a teardown that races engine initialization and can abort the JVM.

Live confirmed in production through Geyser for about a month: retail clients join by direct IP over pure NetherNet with TLS and identity assertions.

@SendableMetatype
SendableMetatype marked this pull request as ready for review August 2, 2026 18:35
@SendableMetatypeSendableMetatype changed the title HTTP signaling front end with identity handling[3/4] HTTP signaling front end with identity handlingAug 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.
NetherNetHttpSignaling serves NetherNet's direct connection model on
TCP under the Bedrock port: GET /v1/join as the capability check and
POST /v1/join/{networkId} exchanging the SDP offer for a full ICE
answer in one round trip, per Mojang's onboarding guide and matching
the go-nethernet reference (one shot connections, 1 MiB offer cap,
uint64 network id validation, 502 after the negotiation timeout with
the half negotiated child reaped). The listener owns a single thread
accept group because bind() runs on the server channel's event loop.
The backend gains a full ICE accept mode: no trickle candidates in
either direction, the answer reported once after the local description
applies and gathering completes, re read from the engine so it carries
every candidate, and terminated with a=end-of-candidates.
Server signaling implementations can now report the peer's transport
address (the HTTP front end knows it from the request) used as the
child's initial remote address, and request full ICE answers. A new
NETHER_SERVER_ANSWER_DECORATOR channel option lets consumers transform
answers before signaling, the seam for the server identity assertion
that HTTP signaled clients require; TLS is a Supplier<SslContext>
consulted per connection so certificate rotation needs no rebind.
Mojang's onboarding guide instructs removing a=identity before the SDP
reaches setRemoteDescription: WebRTC implementations may reject
unknown attributes, and the assertion is signaling layer metadata
(validated above the backend, not in it). Current client offers always
carry one. The engine tolerated it in live testing, but stripping
removes the dependence on that tolerance. The unstripped offer stays
available above the backend for validation.
A decorator failure previously fell back to the undecorated answer,
which a peer requiring the decoration only refuses after parsing and
negotiation. The exchange now fails explicitly instead (a 400 on
HTTP), matching the reasoning that gates the whole listener on the
decorator being available: an unasserted answer is never useful, and
an explicit error is the fastest fallback signal.
…ative state
NetherNetServerChannel gains a backend supplier constructor: the backend
(and with it the PeerConnectionFactory pool) is created in doBind only
after the signaling endpoint bound successfully. Previously consumers had
to create the pool before channel construction, so a signaling endpoint
that could not bind, such as a taken TCP port on shared hosting, meant
disposing freshly created factories milliseconds after creation. That
teardown races engine initialization inside libwebrtc and aborts the JVM
with a pure virtual call.
Offers racing a failed bind are dropped safely, doClose tolerates a
backend that never materialized, and NetherNetChannelFactory gains a
supplier overload building the LibWebRtcServerBackend on demand. The
eager constructors are unchanged.
@SendableMetatype
SendableMetatypeforce-pushed the nethernet-http-signaling branch from b48e564 to 598f1b9CompareAugust 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