Godot Freelay
Serverless multiplayer for Godot 4 on top of plain MQTT brokers, implementing
the Freelay protocol (see freelay-spec.md): signed public channels,
end-to-end encrypted private sessions, presence with tamper resistance, and an
optional WebRTC upgrade for direct peer connections.
Dependencies
| Addon | Version | Provides |
|---|---|---|
| mqtt-node | ≥ 0.2.0 | MqttNode (WebSocket MQTT 3.1.1, LWT, binary payloads, auto-reconnect) |
| gd-ed25519 | ≥ 1.0.0 | Ed25519, Ed25519Keypair, X25519, X25519Keypair, Monocypher |
| webrtc-native (optional) | - | WebRTCPeerConnection on desktop/mobile; built into web exports |
Copy addons/mqtt-node, addons/ed25519 and addons/freelay into your
project. Without webrtc-native everything works relay-only; upgrade_to_rtc()
then logs an error and the session stays on MQTT.
Quick start
See example.gd. In short:
var client := RelayClient.new()
client.app_id = "my-cool-game"
client.broker_urls = ["wss://broker.hivemq.com:8884/mqtt"]
add_child(client)
client.open()
await client.opened
var lobby := client.join("lobby") # public, signed, plaintext
await lobby.joined
lobby.send({"hello": "world"})
var conn := client.connect_peer(peer_id) # private, E2E encrypted
await conn.ready
conn.send({"secret": 42})
conn.upgrade_to_rtc() # optional direct connection
API surface
- RelayClient (Node) -
open(),close(),join(name) -> RelayChannel,connect_peer(peer_id) -> RelayPeerConnection,mute_peer(id). Signals:opened,reconnected,closed(reason),peer_connection_requested(conn),outbound_throttled(count). Config:app_id,broker_urls,profile,auto_accept_peers,outbound_limit,inbound_limit_per_peer(bothRelayRateLimit, disabled by default). - RelayProfile -
generate(),from_seed(seed),save(path, password),load_from(path, password),peer_id,public_key. The seed never leaves the class except viasave(). - RelayChannel -
send(data, reliable=true),leave(),peers. Signals:joined,left(reason),message(peer, data),peer_joined(peer),peer_left(peer). - RelayPeerConnection -
send(data, reliable=true),accept(),reject(),close(),upgrade_to_rtc(ice_servers). Signals:ready,message(data),transport_changed("relay"|"rtc"),closed(reason). - RelayPeer -
peer_id,public_key,mute()/unmute(),meta.
Internal (protocol-level) classes: Freelay, FreelayCrypto,
FreelayEnvelope, FreelayPresenceManager.
Addon API status
All gd-ed25519 calls are isolated in freelay_crypto.gd and were verified
against the v1.0.0 doc-classes:
Ed25519.sign(message, private_key, public_key)- public key passed empty and derived internally (mismatch would fail fast).Ed25519Keypair.generate()/from_seed()/get_seed()/get_public_key().X25519.generate_keypair()andX25519Keypair.get_private_key()/get_public_key().X25519.shared_secret(own_private, their_public)- returns null on low-order points; treated as fatal handshake failure.Monocypher.aead_encrypt(key, nonce, plaintext, ad)/aead_decrypt(key, nonce, ciphertext_with_tag, ad)- note the key-and-nonce-first parameter order; both return null on failure, which the adapter maps to an empty array (encrypt) or passes through (decrypt).Monocypher.blake2b(data, out_len)- null on out-of-range output length.
Still assumed (not covered by the uploaded doc-classes): MqttNode.close() for
clean disconnect (guarded with has_method).
Design notes / limitations
- No gameplay trust: signatures prove sender identity, not honesty. Anti-cheat (lockstep, host authority, replays) is an application concern.
- Channel traffic is signed plaintext by design; only peer sessions are encrypted.
- The profile KDF is iterated BLAKE2b (not memory-hard). Fine against casual attackers; switch to Argon2 if/when the crypto addon exposes it.
- If the broker refuses the connection with a non-retryable CONNACK code,
mqtt-node disables auto-reconnect;
RelayClientcurrently does not surface this asclosed- watchMqttNode.disconnectedif you need it. bytes_to_varis used without object decoding (safe), so payloads are Godot Variants. For cross-language interop sendDictionary/Arrayof primitives, or extendsend()with a raw-bytes mode.- Public brokers can still be flooded topic-wide; inbound limits and muting are local defenses only (spec §11).
Changelog for version 0.0.1
No changelog provided for this version.