WebRTC-based NetherNet transport for Minecraft: Bedrock Edition, written in Java. Implements the
HTTP signaling host side (answerer): accepts SDP offers over POST /v1/join/{networkId}, negotiates
a WebRTC peer connection using Kas-tle/webrtc-java, and
exposes the two NetherNet data channels as a simple send/receive API.
| Transport | Class | Signaling | ICE |
|---|---|---|---|
| HTTP (partner/Realms) | NetherNetServer |
POST /v1/join/{id} |
full ICE, single POST |
| LAN | NetherNetLanServer + DiscoverySignaling |
encrypted UDP broadcast on 7551 | trickle (CANDIDATEADD) |
| Xbox / Franchise | NetherNetLanServer + WebSocketSignalTransport |
authenticated wss:// |
trickle (CANDIDATEADD) |
NetherNetLanServer runs the trickle-ICE negotiation over any SignalTransport. The
WebSocketSignalTransport connects to a signaling service over wss:// with an Authorization
token, relays offer/answer/candidate signals as JSON ({"Type","From","To","Message"}), and applies
TURN credentials pushed by the service to the ICE configuration automatically.
The LAN transport implements the NetherNet discovery protocol: AES-ECB + HMAC-SHA256 encrypted
packets (key SHA-256(LE u64 0xdeadbeef)) carrying RequestPacket/ResponsePacket (world
advertisement via ServerData) and MessagePacket (offer/answer/candidate signals). ServerData
is byte-compatible with vanilla (verified against golden vectors).
- Signaling: full ICE, single round-trip HTTP.
GET /v1/joinis the capability check;POST /v1/join/{networkId}carries the SDP offer and returns the SDP answer (application/sdp). - Data channels:
ReliableDataChannelandUnreliableDataChannel. - Framing: every message is prefixed with a 1-byte remaining-segment counter. Messages larger than 262143 bytes are split into descending-counter fragments on the reliable channel; the unreliable channel never fragments.
- Identity: the answer always carries an
a=identityassertion - a base64 JSON envelope holding a self-signed ES384 JWT (with acpkpublic-key claim) and a detached JWS over the answer'sa=fingerprintlines. Client offer assertions are verified for possession of thecpkkey.
ServerIdentity identity = ServerIdentity.generate("self");
NetherNetServer server = NetherNetServer.builder(identity)
.bind(new InetSocketAddress("0.0.0.0", 7551))
.listener(new ServerListener() {
@Override
public void onConnection(NetherNetConnection connection) {
connection.setListener((conn, data) -> conn.send(data));
}
})
.build();
server.start();ServerData data = new ServerData();
data.serverName = "Erila NetherNet";
data.levelName = "Lobby";
NetherNetLanServer server = NetherNetLanServer.builder(identity)
.lanDiscovery(networkId) // long, advertised to clients on the network
.serverData(data)
.listener(connection -> connection.setListener((conn, msg) -> conn.send(msg)))
.build();
server.start();NetherNetLanServer server = NetherNetLanServer.builder(identity)
.webSocket(URI.create("wss://signaling.example/ws/v1.0/signaling/" + networkId),
Long.toString(networkId),
() -> "MCToken " + xboxAuthToken) // Authorization header, refreshed per connect
.listener(connection -> connection.setListener((conn, msg) -> conn.send(msg)))
.build();
server.start();The Type values in the JSON envelope (SignalMessage.TYPE_SIGNAL etc.) and the wss:// URL shape
follow the Franchise signaling service; verify them against your target endpoint. Auth tokens (Xbox
Live / PlayFab / MCToken) are supplied by the caller - this library does not perform the Xbox Live
auth flow.
signaling(Signaling)- replace the built-in JDK HTTP server with your own transport (e.g. front it with an existing Netty stack or a reverse proxy terminating TLS).clientVerifier(ClientIdentityVerifier)- custom offer-assertion validation. The default provescpkkey possession only.verifyClientTokens(URI jwksUri)- fullGameServerTokenvalidation: verifies the token signature (RS256/ES384) against a cached JWKS from the Minecraft auth service, checksexp, then proves fingerprint possession. Also requires the assertion to be present.requireClientIdentity(true)- reject offers with noa=identity.configuration(Consumer<RTCConfiguration>)- add STUN servers or tune ICE.advertiseAddress(String publicIp[, int publicPort])- inject a server-reflexive candidate into the answer for hosts behind 1:1 NAT / dedicated servers with a public IP.answerTransformer(UnaryOperator<String>)- arbitrary answer-SDP rewrite.
The operator keypair is the unit of trust. Persist it (see ServerIdentity.of(KeyPair, domain))
and share it across a fleet so clients on plaintext HTTP see a single first-use prompt.
Requires JDK 17+. The Gradle wrapper is included.
./gradlew build # compile + run tests
./gradlew :example:run # run the echo server on :7551
./gradlew publishToMavenLocal # publish com.erilanetwork:nethernet:1.0.0