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)
Go
cfg := qtineon.DefaultConfig() // relay port 7777
relay, _ := qtineon.NewRelay("0.0.0.0", cfg)
go relay.StartAndRun()
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)
Go
host, _ := qtineon.NewHost(42, "relay.example.com:7777", cfg)
host.SetClientConnectCallback(func(id uint8, name string, sid int32) {
fmt.Println(name, "joined as", id)
})
host.SetUnhandledPacketCallback(func(ptype, from uint8, payload []byte) {
handleGamePacket(ptype, from)
})
go host.StartAndRun()
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)
Go
client, _ := qtineon.NewClient("player1", cfg)
client.SetSessionConfigCallback(func(sc qtineon.SessionConfig) {
fmt.Println("Tick rate:", sc.TickRate)
})
client.SetUnhandledPacketCallback(func(ptype, from uint8, payload []byte) {
handleGamePacket(ptype, from)
})
if client.Connect(42, "relay.example.com:7777") {
go 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
Go
data := encodePosition(x, y, z)
client.SendPacket(data, PacketPosition, 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
Go
client.Stop() // or socket dies ungracefully
// ...
ok := client.Reconnect(5) // uses stored session token, up to 5 attempts
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)
Go
// Relay — load a PEM certificate and private key
relayCfg := qtineon.DefaultConfig()
relayCfg.DTLS, _ = qtineon.FromKeyStore("relay.pem", "relay-key.pem")
relay, _ := qtineon.NewRelay("0.0.0.0", relayCfg)
// Host / Client — trust the relay certificate (or supply a proper trust store)
clientCfg := qtineon.DefaultConfig()
clientCfg.DTLS = qtineon.InsecureTrustAll() // dev only
client, _ := qtineon.NewClient("player1", clientCfg)
insecure_trust_all() / insecureTrustAll() / 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.
Go (go get)
go get github.com/Quiet-Terminal-Interactive/QTINeon/go
DTLS support (github.com/pion/dtls/v2) is a direct dependency and requires no extra installation step.
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
Go
cd go
go build ./...
go vet ./...
Tests open real UDP sockets on loopback:
go test ./...
Generate docs:
gomarkdoc ./... --output ../docs/go/README.md
# output: ../docs/go/README.md
Client IDs
| ID | Role |
|---|---|
0 |
Broadcast / unassigned |
1 |
Host |
2–254 |
Connected clients |
255 |
Reserved |
License
MIT — see LICENSE.
Metadata
Release files for qti-neon 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| qti_neon-1.1.0.tar.gz | 41.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qti_neon-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 79.4 kB
Release files / qti_neon-1.1.0.tar.gz
| Download URL | qti_neon-1.1.0.tar.gz |
|---|---|
| Size | 41.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ec051be025fe9268bea9e8ba06133dd16659261793ca9926d5669fef16f91f8c
|
|
BLAKE2b-256 checksum How to use checksums |
044e19c43729a7a3e4bbdfdc125e7d0b419f0c9c2379b2e837c34f39245f683f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / qti_neon-1.1.0-py3-none-any.whl
| Download URL | qti_neon-1.1.0-py3-none-any.whl |
|---|---|
| Size | 38.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
43dae0f08646146a74a5fe7996d3da03a51b29f9f9e850fa8b24f110e0a7c6a3
|
|
BLAKE2b-256 checksum How to use checksums |
b9e98228ddacf722cece432e5ed4d6e8f9210cfc690476b669a9aaecae8eb06c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for qti-neon 1.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.