Minimal, game-agnostic, relay-based UDP multiplayer protocol library
Project description
QTI Neon
Minimal, game-agnostic, relay-based UDP multiplayer protocol library.
Client A ←──── UDP ────→ Relay ←──── UDP ────→ Host
Client B ←──── UDP ────→ Relay
Clients never communicate directly. The relay routes packets by destination ID in the packet header, keeping NAT traversal trivial and host addresses private. The host is just another participant — it has no special network position, only a special protocol role.
Features
- Relay-mediated UDP with automatic NAT traversal
- Connection handshake with host-assigned client IDs and session tokens
- Token-based reconnection (5-minute window by default)
- Opt-in reliable delivery with retransmit and duplicate detection
- Per-source rate limiting (100 pps default)
- Auto-ping keepalive
- Optional DTLS 1.2/1.3 encryption — relay-terminated, transparent to game code
- Zero game-specific logic in the relay or library
Implementations
Each language implementation lives in its own subdirectory at the repository root, alongside its generated documentation under docs/<language>/.
All implementations conform to the same protocol spec — a client or host written in any language is fully interoperable with a relay or peer written in any other.
See PROTOCOL.md for the wire format specification that all implementations share.
Documentation
See ARCHITECTURE.md for a full description of the relay topology, session lifecycle, and reconnect flow.
See PROTOCOL.md for the wire format specification.
See CONFIGURATION.md for the complete configuration reference.
API documentation for each implementation is generated from source and lives under docs/<language>/.
Quick Start
1. Run a relay
Java
NeonConfig cfg = NeonConfig.defaults(); // relay port 7777
NeonRelay relay = new NeonRelay("0.0.0.0", cfg);
Thread.ofVirtual().start(relay::startAndRun);
Python
import threading
from qti_neon import NeonRelay, NeonConfig
relay = NeonRelay("0.0.0.0", NeonConfig())
threading.Thread(target=relay.start_and_run, daemon=True).start()
TypeScript
import { NeonRelay } from 'qti-neon';
const relay = new NeonRelay('0.0.0.0'); // relay port 7777
await relay.start();
Godot
var relay := NeonRelay.new("0.0.0.0") # relay port 7777
var thread := Thread.new()
thread.start(relay.start_and_run)
2. Start a host
Java
NeonHost host = new NeonHost(42, "relay.example.com:7777", cfg);
host.setClientConnectCallback((id, name, sid) ->
System.out.println(name + " joined as " + (id & 0xFF)));
host.setUnhandledPacketCallback((type, from, payload) ->
handleGamePacket(type, from));
Thread.ofVirtual().start(() -> host.startAndRun());
Python
import threading
from qti_neon import NeonHost
host = NeonHost(session_id=42, relay_address="relay.example.com:7777")
host.set_client_connect_callback(lambda cid, name, sid: on_client_join(cid, name))
host.set_unhandled_packet_callback(lambda ptype, sender, payload: handle_game_packet(ptype, sender))
threading.Thread(target=host.start_and_run, daemon=True).start()
TypeScript
import { NeonHost } from 'qti-neon';
const host = new NeonHost(42, 'relay.example.com:7777');
host.setClientConnectCallback((id, name, sid) => onClientJoin(id, name));
host.setUnhandledPacketCallback((type, from, payload) => handleGamePacket(type, from));
await host.start();
Godot
var host := NeonHost.new(42, "relay.example.com:7777")
host.set_client_connect_callback(func(id, name, sid): on_client_join(id, name))
host.set_unhandled_packet_callback(func(type, from, payload): handle_game_packet(type, from))
var thread := Thread.new()
thread.start(host.start_and_run)
3. Connect a client
Java
NeonClient client = new NeonClient("player1", cfg);
client.setSessionConfigCallback(sc ->
System.out.println("Tick rate: " + sc.tickRate()));
client.setUnhandledPacketCallback((type, from, payload) ->
handleGamePacket(type, from));
if (client.connect(42, "relay.example.com:7777")) {
Thread.ofVirtual().start(client::run);
}
Python
import threading
from qti_neon import NeonClient
client = NeonClient("player1")
client.set_session_config_callback(lambda sc: on_session_config(sc))
client.set_unhandled_packet_callback(lambda ptype, sender, payload: handle_game_packet(ptype, sender))
if client.connect(session_id=42, relay_address="relay.example.com:7777"):
threading.Thread(target=client.run, daemon=True).start()
TypeScript
import { NeonClient } from 'qti-neon';
const client = new NeonClient('player1');
client.setSessionConfigCallback(sc => onSessionConfig(sc));
client.setUnhandledPacketCallback((type, from, payload) => handleGamePacket(type, from));
await client.connect(42, 'relay.example.com:7777');
Godot
var client := NeonClient.new("player1")
client.set_unhandled_packet_callback(func(type, from, payload): handle_game_packet(type, from))
if client.connect(42, "relay.example.com:7777"):
var thread := Thread.new()
thread.start(client.run)
4. Send a packet
Java
byte[] data = encodePosition(x, y, z);
client.sendPacket(data, PACKET_POSITION, (byte) 0); // 0 = broadcast
Python
data = encode_position(x, y, z)
client.send_packet(data, PACKET_POSITION, dest_id=0) # 0 = broadcast
TypeScript
const data = encodePosition(x, y, z);
client.sendPacket(data, PACKET_POSITION, 0); // 0 = broadcast
Godot
var data := encode_position(x, y, z)
client.send_packet(data, PACKET_POSITION, 0) # 0 = broadcast
5. Reconnect after drop
Java
client.stop(); // or socket dies ungracefully
// ...
boolean ok = client.reconnect(); // uses stored session token
Python
client.stop() # or socket dies ungracefully
# ...
ok = client.reconnect() # uses stored session token
TypeScript
client.stop(); // or socket dies ungracefully
// ...
const ok = await client.reconnect(); // uses stored session token
Godot
client.stop() # or socket dies ungracefully
# ...
var ok := client.reconnect() # uses stored session token
DTLS Encryption
DTLS is opt-in — the relay, host, and client handle the handshake automatically before any Neon packets are exchanged.
Java
// Relay — load a PKCS12 keystore with the relay's certificate
KeyStore ks = KeyStore.getInstance("PKCS12");
try (var is = new FileInputStream("relay.p12")) {
ks.load(is, "password".toCharArray());
}
SSLContext relayCtx = DtlsConfig.fromKeyStore(ks, "password".toCharArray());
NeonConfig relayCfg = NeonConfig.builder()
.sslContext(relayCtx)
.build();
// Host / Client — trust the relay certificate (or use a proper TrustManager)
SSLContext clientCtx = DtlsConfig.insecureTrustAll(); // dev only
NeonConfig clientCfg = NeonConfig.builder()
.sslContext(clientCtx)
.build();
Python
from qti_neon import DtlsConfig, NeonConfig, NeonRelay, NeonClient
# Relay — load certificate and private key
relay_cfg = NeonConfig(dtls_config=DtlsConfig.from_key_store("relay.crt", "relay.key"))
relay = NeonRelay("0.0.0.0", relay_cfg)
# Host / Client — trust the relay certificate (or supply a proper trust store)
client_cfg = NeonConfig(dtls_config=DtlsConfig.insecure_trust_all()) # dev only
client = NeonClient("player1", client_cfg)
TypeScript
import { DtlsConfig, NeonConfig, NeonRelay, NeonClient } from 'qti-neon';
// Relay — load certificate and private key
const relayCfg = new NeonConfig({ dtlsConfig: DtlsConfig.fromKeyStore('relay.crt', 'relay.key') });
const relay = new NeonRelay('0.0.0.0', relayCfg);
// Host / Client — trust the relay certificate (or supply a proper trust store)
const clientCfg = new NeonConfig({ dtlsConfig: DtlsConfig.insecureTrustAll() }); // dev only
const client = new NeonClient('player1', clientCfg);
Godot
# Relay — load certificate and private key (PEM files)
var relay_cfg := NeonConfig.new({dtls_config = DtlsConfig.from_key_store("relay.crt", "relay.key")})
var relay := NeonRelay.new("0.0.0.0", relay_cfg)
# Host / Client — trust the relay certificate (or use insecure for dev)
var client_cfg := NeonConfig.new({dtls_config = DtlsConfig.insecure_trust_all()}) # dev only
var client := NeonClient.new("player1", client_cfg)
insecure_trust_all() / insecureTrustAll() is for development and testing only — it accepts any certificate.
For production, supply a trust manager that pins the relay's certificate.
When DTLS is not configured (the default), packets are sent in plaintext.
Installation
Java (Maven)
<dependency>
<groupId>com.quietterminal</groupId>
<artifactId>qti-neon</artifactId>
<version>1.0.0</version>
</dependency>
Python (pip)
pip install qti-neon
With DTLS support:
pip install "qti-neon[dtls]"
TypeScript (npm)
npm install qti-neon
With DTLS support:
npm install koffi
Godot (Asset Library / manual)
Copy (or symlink) godot/addons/qti_neon/ into your project's addons/ directory, then enable the plugin in Project → Project Settings → Plugins.
No package manager step is required — the implementation is pure GDScript with no external dependencies.
Building
Java
mvn verify
Tests that open UDP sockets (most of them) must run outside a sandbox:
mvn test
Generate docs:
mvn javadoc:javadoc
# output: target/reports/apidocs/index.html
Python
cd python
pip install -e ".[dev]"
pytest
Generate docs:
pdoc src/qti_neon --output-dir ../docs/python
# output: ../docs/python/qti_neon.html
TypeScript
cd js-ts
npm install
npm test
Generate docs:
npm run docs
# output: ../docs/ts/index.html
Godot
No build step — the implementation is pure GDScript. To verify the compliance scripts parse correctly:
cd godot
godot --headless --check-only --script compliance/neon_host_runner.gd
To run the cross-language compliance tests (requires Java and Node.js):
python3 test_compliance.py
Client IDs
| ID | Role |
|---|---|
0 |
Broadcast / unassigned |
1 |
Host |
2–254 |
Connected clients |
255 |
Reserved |
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qti_neon-1.0.1.tar.gz.
File metadata
- Download URL: qti_neon-1.0.1.tar.gz
- Upload date:
- Size: 40.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f78e14baf8f702b3ac1e656253de044134c824af8cc0718d503949bfc2b67ba
|
|
| MD5 |
f6fa537b701d81e88e7f31a26697b346
|
|
| BLAKE2b-256 |
b61c97aac7c9ddc78a37d264af27e720ead754bd80ef4e7f0bc53159b903cea8
|
File details
Details for the file qti_neon-1.0.1-py3-none-any.whl.
File metadata
- Download URL: qti_neon-1.0.1-py3-none-any.whl
- Upload date:
- Size: 37.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
35f6eb217c920e11211416bb2cb8328ffc6cb96fe9989b61c9bad3f43e5c097a
|
|
| MD5 |
3c5af956ecee5d896c31478629804766
|
|
| BLAKE2b-256 |
b04a965f8d85ab11a477a9a37d10564a1194e721a871d2f32ab7f3e90b1ec4f2
|