P2P Multiplayer
RWX P2P multiplayer is designed to establish multiplayer sessions without routing game traffic through a traditional game relay server. The current implementation uses WebRTC DataChannel as the primary game transport, with libp2p GossipSub used for room discovery and WebRTC signaling.
Current Status
P2P multiplayer is still experimental. It improves the chance of connecting over the public Internet, but it cannot guarantee connectivity on every network.
Main features:
- Game traffic uses WebRTC DataChannel by default.
- WebRTC uses ICE/STUN/TURN for NAT traversal.
- libp2p handles room announcements, refreshes, and WebRTC offer/answer/ICE candidate signaling.
- LAN peers can be discovered with mDNS.
- The old libp2p stream tunnel remains as a fallback when direct libp2p addresses are available and WebRTC signaling is unavailable.
Connection Flow
When hosting a P2P room:
- The game starts a normal multiplayer host session.
- The P2P service starts a libp2p host and joins the lobby gossip topic.
- The host periodically publishes a room announcement.
- The host starts the WebRTC host side and waits for joiners to send offers over gossip.
- The room announcement includes the WebRTC signaling mode and ICE server list.
When joining a P2P room:
- The client selects a room from the gossip room list.
- The client starts a temporary local TCP listener.
- The game client connects to
127.0.0.1:<temporary-port>. - The P2P layer creates a WebRTC DataChannel offer and sends it to the host over gossip.
- The host replies with an answer, and both sides exchange ICE candidates.
- Once the DataChannel opens, the P2P layer forwards the game byte stream between the local TCP socket and the DataChannel.
Simplified structure:
Local P2P Configuration
All P2P network settings are read from the local p2p.toml file in the game working directory.
If the file does not exist, RWX creates it with defaults on startup.
Example:
[libp2p]
peers = [
"/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
"/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa"
]
peerSources = [
"https://p2p-lobby-services.shuangx339.workers.dev"
]
[libp2p.proxy]
bufferSize = 65536
readTimeoutMs = 30000
streamOpenTimeoutMs = 10000
connectTimeoutMs = 5000
idleTimeoutMs = 120000
tcpKeepAlive = true
tcpNoDelay = true
[webrtc]
iceServers = [
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302",
"stun:openrelay.metered.ca:80",
"turn:openrelayproject:openrelayproject@openrelay.metered.ca:443"
]
[webrtc.proxy]
bufferSize = 65536
openTimeoutMs = 20000
socketConnectTimeoutMs = 5000
socketReadTimeoutMs = 30000
executorThreads = 4
[nat]
enableUpnp = true
enablePcp = true
pcpLifetimeSeconds = 3600
pcpTimeoutMs = 3000
mappingDescription = "RWX P2P"
[relay]
enableCircuitRelayV2 = true
enableDcutr = false
relayReservationCount = 1
maxRelayReservations = 16
maxRelayReservationSeconds = 3600
relayPeers = []
bootstrapRelays = []
[discovery]
enableGossipSub = true
enableMdns = true
[discovery.service]
# HTTP lobby service endpoints for room discovery. Each URL can be a base URL or /rooms.
enable = true
urls = [
"https://p2p-lobby-services.shuangx339.workers.dev"
]
refreshIntervalMs = 15000
timeoutMs = 5000
maxBytes = 262144
maxRoomsPerUrl = 200
[discovery.service.publish]
# Optional: publish this client's hosted room to the lobby service.
enable = true
timeoutMs = 5000
updateIntervalMs = 15000
roomTtlMs = 600000
deleteOnClose = true
[discovery.dht]
enable = false
namespace = "/rwx/lobby/v1"
provideIntervalMs = 30000
queryIntervalMs = 15000
queryTimeoutMs = 10000
maxProviders = 50
[lobby]
roomAnnounceIntervalMs = 3000
roomTtlMs = 15000
bootstrapConnectTimeoutMs = 10000
mdnsPeerAddressTtlMs = 30000libp2p Peers
[libp2p].peers controls the peers used for public lobby discovery and WebRTC signaling over GossipSub. These peers are not TURN relays and do not carry game traffic.
[libp2p].peerSources refers to HTTP URLs of services that provide peer querying., usually GitHub Gists. These lists can publish unstable or testing rendezvous/bootstrap nodes without shipping a new client. A node list uses this shape:
These nodes help peers meet and exchange gossip/signaling messages. They are not TURN servers and do not relay WebRTC game traffic.
Room Discovery Service
[discovery.service].urls points to the HTTP URLs that provide room discovery services.
[discovery.service.publish] configures the room publishing functionality.
DHT Discovery
DHT (Distributed Hash Table) is a decentralized system that allows peer-to-peer data discovery and exchange without a centralized server. RWX plans to support room discovery and signaling based on libp2p Kademlia DHT in future versions, but it is not available in the current release.
[discovery.dht] is currently a placeholder. The bundled jvm-libp2p 1.2.0 does not expose a usable Kad-DHT provider API, so enabling it only logs a warning. It remains in the config for future library upgrades.
WebRTC ICE Servers
[webrtc].iceServers controls the ICE server list used by WebRTC DataChannel.
Supported formats:
stun:stun.l.google.com:19302
turn:user:pass@example.com:3478
turns:user:pass@example.com:5349If the list is empty or invalid, RWX uses the default free STUN servers:
stun:stun.l.google.com:19302
stun:stun1.l.google.com:19302STUN only helps peers discover their public-facing addresses and can work for many common NATs. It does not guarantee connectivity. TURN is the real relay fallback and usually requires your own account or a third-party service.
Proxy and Lobby Timeouts
[libp2p.proxy], [webrtc.proxy], [nat], [relay], and [lobby] tune local proxy buffers, socket timeouts, NAT mapping, relay behavior, room announcement interval, room TTL, and bootstrap connection timeout. Increase timeouts only if logs show slow network setup; larger values can make failed joins take longer to report.
NAT Mapping
[nat].enablePcp enables PCP TCP port mapping before UPnP. PCP currently uses the local default gateway. If PCP fails, RWX falls back to UPnP when [nat].enableUpnp is true.
Circuit Relay and DCUtR
[relay].enableCircuitRelayV2 enables the libp2p circuit relay transport and /libp2p/circuit/relay/0.2.0 hop/stop protocols provided by the bundled jvm-libp2p version.
relayPeers and bootstrapRelays can contain relay multiaddrs. If empty, RWX can try already connected peers as relay candidates.
enableDcutr is present for forward compatibility. The current bundled jvm-libp2p 1.2.0 does not include a DCUtR protocol implementation, so enabling it only logs a warning.
Why libp2p Is Still Used
WebRTC provides the data transport, but it does not define how peers find each other or exchange offer/answer/ICE messages.
RWX currently uses libp2p for:
- Publishing and receiving P2P room announcements.
- Sending WebRTC offers, answers, and ICE candidates between host and client.
- Discovering nearby LAN peers with mDNS.
- Attempting to join the public libp2p network through default bootstrap peers.
In short, WebRTC is the main transport and libp2p is the discovery/signaling layer.
Difference From Classic Multiplayer
Classic multiplayer usually requires manually entering an IP address or using a server list. P2P multiplayer discovers P2P rooms and attempts NAT traversal automatically through WebRTC.
Once NAT traversal succeeds, game data is transmitted directly between players without relaying through servers, which in theory can reduce latency and server costs.
P2P does not mean 100% connectivity:
- If both peers are behind common home NATs, STUN may be enough.
- If one or both peers are behind strict NAT, symmetric NAT, or carrier-grade NAT, STUN-only connections may fail.
- Without TURN, failed hole punching has no reliable relay fallback.
Integration of EasyTier
EasyTier is a simple, secure, and decentralized solution for cross-region networking. By establishing a virtual network, it enables Layer 2 / Layer 3 direct connectivity between players, significantly improving the success rate of P2P connections (which can exceed 95% when both parties are connected).
Given that the configuration process is relatively complex, capable users are encouraged to explore it on their own, and it will not be elaborated further here.
Troubleshooting
If no rooms appear:
- Make sure both players use the same RWX version.
- Wait a few seconds and click refresh.
- Check whether the firewall blocks Java/RWX network access.
- On the public Internet, room discovery depends on libp2p gossip. If the bootstrap network is unreachable, rooms may not appear.
If rooms appear but joining fails:
- Configure a TURN server.
- Check whether the system firewall blocks UDP.
- If only STUN is configured, strict NAT or CGNAT may still fail.
- Check the logs for
WebRTC tunnel failed,Failed to publish WebRTC signal, or native WebRTC initialization errors.
If TURN does not work:
- Use
turn:user:pass@host:portorturns:user:pass@host:port. - Verify that the username and password are valid.
- Verify that the TURN service allows UDP/TCP relay.
Limitations
- RWX does not include a free built-in TURN relay service.
- Default STUN servers do not relay traffic; they only help with address discovery.
- The WebRTC native library must support the target platform.
- libp2p gossip discovery over the public Internet depends on bootstrap connectivity and peer graph conditions.
- P2P is still evolving. Keep logs when reporting connection issues.