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()

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) ->
    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: handle_game_packet(ptype, sender))
threading.Thread(target=host.start_and_run, daemon=True).start()

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) ->
    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: 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()

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

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

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)

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]"

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

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.0.tar.gz (39.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.0-py3-none-any.whl (36.8 kB view details)

Uploaded Python 3

File details

Details for the file qti_neon-1.0.0.tar.gz.

File metadata

  • Download URL: qti_neon-1.0.0.tar.gz
  • Upload date:
  • Size: 39.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.13

File hashes

Hashes for qti_neon-1.0.0.tar.gz
Algorithm Hash digest
SHA256 9c3dae36d1a301da027ff660595f6b04d80c5925d5aa11775c135bc8482cbf24
MD5 e89357635ed57801a6e342864ad660d3
BLAKE2b-256 d083210f9332004fc679e33732cc170aea3e60992643c5c9106e4ff1604345ce

See more details on using hashes here.

File details

Details for the file qti_neon-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: qti_neon-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 36.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.13

File hashes

Hashes for qti_neon-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fc52948a009c4a978e7a1f329df8bb65e6e1cc77483430b68d80f63d1c3d2f43
MD5 5ab0ddca556521abb86974a9346212c7
BLAKE2b-256 ee44b86094481ef9f9f7a3e21792711c4a2a2929ccddc0ee42f044b75a0dec30

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