Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qti_neon-1.0.1.tar.gz (40.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

qti_neon-1.0.1-py3-none-any.whl (37.7 kB view details)

Uploaded Python 3

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

Hashes for qti_neon-1.0.1.tar.gz
Algorithm Hash digest
SHA256 9f78e14baf8f702b3ac1e656253de044134c824af8cc0718d503949bfc2b67ba
MD5 f6fa537b701d81e88e7f31a26697b346
BLAKE2b-256 b61c97aac7c9ddc78a37d264af27e720ead754bd80ef4e7f0bc53159b903cea8

See more details on using hashes here.

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

Hashes for qti_neon-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 35f6eb217c920e11211416bb2cb8328ffc6cb96fe9989b61c9bad3f43e5c097a
MD5 3c5af956ecee5d896c31478629804766
BLAKE2b-256 b04a965f8d85ab11a477a9a37d10564a1194e721a871d2f32ab7f3e90b1ec4f2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page