Kojacoord is a multi-version proxy for Minecraft: Java Edition. It sits in front of one or more backend servers, handles the handshake/login/encryption with the client, and relays play traffic to the right backend — translating protocol versions on the fly when the client and backend don't match. It's written in Rust on Tokio, and it's the proxy that runs KojaCraft in production.
Status: pre-1.0, actively developed. Protocol coverage spans 1.6.4 through 1.21.x, but verification depth varies by version — see version support before depending on a specific one.
- Features
- Architecture
- Building
- Configuration
- Running
- Version support
- Metrics
- Plugins
- Security model
- Contributing
- License
- Protocol sources
- Protocol support for 1.6.x through 1.21.x, mapped onto canonical per-version buckets — see version support.
- Rule-based routing (username glob or client IP/CIDR), GeoIP region selection, failover groups with automatic failback, and per-server TCP health checks.
- Full-server connection queueing instead of hard rejection, and
weighted/least-connections/latency-aware selection across named backend
pools (
[[server_groups]]). - Limbo: a synthetic holding world for players when no backend is reachable, instead of a disconnect.
- Online-mode auth against Mojang's session servers with profile-signature verification, offline mode, Velocity (HMAC-signed) and legacy BungeeCord player-info forwarding.
- Per-IP connection throttling and a pluggable IP-reputation blocklist (static CIDRs plus an optional external provider).
- A WASM plugin system — typed guest SDK, host-side event hooks, Redis and outbound HTTP access. See Plugins.
- Redis-backed clustering for running multiple proxy nodes.
- Hot config reload, gzip log rotation, graceful shutdown with a configured disconnect reason, GameSpy4/UT3 query support, server-list favicon, and a Prometheus metrics endpoint.
- No database, no admin HTTP surface, by design — see Metrics.
The workspace is split into focused crates:
| Crate | Responsibility |
|---|---|
kojacoord-protocol |
Packet types, codecs and per-version registries |
kojacoord-netty |
Framing, compression and encryption codec layer |
kojacoord-auth |
Session authentication and login-phase encryption |
kojacoord-proxy-core |
Core proxy: sessions, relay, routing, limbo, realms, control planes |
kojacoord-config |
Configuration schema, loading and validation |
kojacoord-api |
Public API surface for plugin development |
kojacoord-plugin-abi |
Wire types shared by the plugin host and guest SDK |
kojacoord-plugin-sdk |
Guest SDK for writing WASM plugins |
kojacoord-plugin-system |
Plugin loading, lifecycle and host API |
kojacoord-cluster |
Redis-backed cluster coordination |
kojacoord-metrics |
Prometheus metrics collection and exporter |
You'll need Rust 1.92+ and protoc (the Protocol Buffers compiler — it's
needed to build the gRPC control plane).
git clone https://github.com/KojaCraft/kojacoord.git
cd kojacoord-proxy
# Release build (recommended for actually running the proxy)
cargo build --release
# Binary ends up at target/release/kojacoord-proxy
# Development build + tests
cargo build
cargo testOn first run, if there's no config file, Kojacoord writes a default
config.toml, generates strong random tokens for anything enabled that
needs one, and prompts once to accept the Minecraft EULA. Pass a different
path as the first argument if you'd rather keep it elsewhere:
kojacoord-proxy /path/to/config.toml.
Secrets can also come from environment variables instead of the file — use
a KOJA_ prefix and __ for nesting, e.g. KOJA_SERVER_MANAGEMENT__AUTH_TOKEN
or KOJA_FORWARDING__VELOCITY_SECRET.
Here's a minimal one to get a feel for the shape of it:
[proxy]
bind = "0.0.0.0:25565"
online_mode = true
compression_threshold = 256
max_players = 1000
session_timeout_secs = 5
[listeners]
motd = "KojacoordNetwork"
tab_list = "GLOBAL_PING" # GLOBAL_PING | SERVER_PING | HIDDEN
[forwarding]
mode = "none" # none | velocity | bungeecord
velocity_secret = "" # required for velocity; e.g. `openssl rand -hex 32`
[geoip]
database_path = "" # optional MaxMind GeoLite2 .mmdb path; empty = region routing off
[telemetry]
enabled = true # set false to disable usage telemetry entirely
interval_secs = 1800
[[servers]]
name = "lobby"
address = "127.0.0.1:25566"
display_name = "Lobby"
game_type = "lobby"
[[servers]]
name = "survival"
address = "127.0.0.1:25567"
backend_type = "spigot" # spigot | forge | hybrid
display_name = "Survival"
max_players = 100Most of this hot-reloads when the file changes; a few fields need a restart. The generated default config has per-field comments noting which is which, so that's the source of truth if this snippet and the code ever drift apart.
# Run the release binary
./target/release/kojacoord-proxy
# Or via Cargo
cargo run --releaseConfig changes are picked up automatically on save, or on SIGHUP (Unix).
Ctrl+C (or SIGTERM/SIGQUIT) triggers a graceful shutdown that disconnects
players with a configured reason instead of just dropping the socket.
Drop a favicon.png (64×64, standard Minecraft server-icon format) next to
the binary and it's sent in the server-list ping — no config needed. A
missing or invalid file is logged and skipped; it's re-checked on the same
reload/SIGHUP as the MOTD, so swapping it out doesn't need a restart either.
Every client version maps onto a canonical bucket — the concrete typed-packet implementation that drives limbo and protocol handling for that family. A few patch releases share a protocol number (1.19.1 and 1.19.2 are both 760, for instance), so they collapse onto one row below.
"Tested" means someone actually verified it end-to-end against a real vanilla client: reaches the limbo spawn, the chat/sound/abilities/keep-alive loop holds up, and the proxy can disconnect it gracefully with a real reason.
| Version family | Canonical bucket | Status |
|---|---|---|
| 1.6.x | V1_6_4 |
Tested |
| 1.7.x | V1_7_10 |
Tested |
| 1.8.x | V1_8 |
Tested |
| 1.9.x – 1.12.x | V1_12_2 |
1.12 tested · 1.9–1.11 tested |
| 1.13.x – 1.16.x | V1_16_5 |
Tested |
| 1.17.x – 1.19.x | V1_19_4 |
Tested |
| 1.20.x | V1_20_4 |
1.20 / 1.20.1 tested · 1.20.2+ tested |
| 1.21.x | V1_21 |
Tested |
| 1.26.x | V26 |
Tested |
One wrinkle worth knowing about: limbo's registry handling differs by
protocol era. Protocols ≤ 763 (1.16–1.20.1) get the registry codec embedded
directly in the join packet; 1.20.2–1.20.4 (764/765) fall back to whatever
registries the client already has built in; and 1.20.5+/1.21 (766+) get
explicit RegistryData captured from minecraft-data. The void-chunk /
set-center-chunk trick that clears the "Loading terrain" screen currently
covers the V1_19_4 bucket, with V1_20/V1_21 being extended.
Protocol conversion between client and backend versions happens
automatically during relay — the 1.6.x ↔ 1.12.2 converter pair covers the
gameplay packet set bidirectionally, and framing is pre-netty-aware so 1.6
clients get raw [id][body] packets while modern backends get
varint-length-framed, compressed ones.
The public roadmap — including things like Bedrock bridging and a Realms
compatibility layer that are still in progress — lives in
ROADMAP.md.
Kojacoord holds no persistent state and exposes no inbound admin HTTP
surface. The only thing it listens on over HTTP is a read-only Prometheus
endpoint (metrics.enabled = true, default bind 127.0.0.1:9090). Bans,
mutes, warnings, ranks and permissions are left to a plugin — the proxy
relays packets, it doesn't manage player data.
Plugins are WebAssembly modules. You depend on kojacoord-plugin-sdk and
kojacoord-plugin-abi, implement the Plugin trait, and call
export_plugin! once — the macro takes care of the C ABI exports the host
expects and the JSON marshalling underneath.
The host exposes logging, config access, proxy commands (register/deregister servers, transfer or kick players, broadcast messages), a Redis client family, and outbound HTTP. Plugins subscribe to a bitmask of events — joins, leaves, chat, movement, server connect/switch/kick, server-list pings, plugin messages, Redis messages, and more.
use kojacoord_plugin_sdk::*;
struct MyPlugin;
impl Plugin for MyPlugin {
fn on_enable(&mut self) {
log(LogLevel::Info, "hello from wasm");
redis_connect(&get_config("redis_url").unwrap_or_default());
redis_subscribe("kojacoord:sanctions");
}
fn handle_event(&mut self, ev: &PluginEvent) -> Option<PluginResponse> {
if let PluginEvent::RedisMessage { channel, payload } = ev {
log(LogLevel::Info, &format!("{channel}: {payload}"));
}
None
}
}
export_plugin!(MyPlugin, MyPlugin);Set plugins.hot_reload = true and the plugin directory gets polled, with
modules reloaded as their files change.
- Login encryption. Online-mode login uses RSA key exchange followed by AES-CFB8 packet encryption, matching the vanilla protocol.
- Identity verification. Profile property signatures are checked against the configured Mojang public key, so a MITM can't forge a skin/cape onto a session.
- Forwarding secrets. Velocity forwarding is HMAC-signed and validated at startup — empty, too-short, or well-known placeholder secrets are rejected outright. Legacy BungeeCord forwarding is unsigned by design; if you use it, your backends need to be firewalled to only accept connections from the proxy.
- Control-plane internode encryption. Separate from Minecraft login
encryption entirely: a pluggable cipher registry offers AES-256-GCM,
ChaCha20-Poly1305 and XChaCha20-Poly1305 for internode/control-plane
payloads, plus room to register custom algorithms. An optional
post-quantumfeature adds a real ML-KEM-768 (NIST FIPS 203) + AES-256-GCM hybrid via RustCrypto'sml-kem— off by default, and it's a KEM+DEM hybrid only (not paired with a classical KEM), so combine it with a classical cipher if you need classical/PQ hybrid guarantees. - Crash reports. Written IP-redacted and stripped of tokens/keys, so they're safe to attach to a bug report as-is.
- Fork the repo and branch off
main. - Make your change — try to keep it consistent with the code around it.
- Run
cargo fmt,cargo clippy, andcargo testbefore you push. - Add tests for new behavior and rustdoc for anything public.
- Open a PR describing what changed and why.
See CONTRIBUTING.md for the full guidelines and CODE_OF_CONDUCT.md for the code of conduct. When filing a bug, include the proxy version, client/backend versions, a redacted config, relevant logs, and steps to reproduce.
MIT — see LICENSE.
Copyright (c) 2026 KojaCraft.
Built against these references:
- Minecraft Wiki — Java Edition protocol — packet documentation and version history
- PrismarineJS minecraft-data — protocol data and mappings
- ProtocolSupport — reference for legacy protocols
