Conic Nexus is the cross-LAN multiplayer runtime component of the Conic launcher. One player hosts a Minecraft LAN world; remote friends join it as if they were on the same LAN.
The launcher does not link against Conic Nexus. It loads the library at
runtime (typically through libloader) and drives the whole multiplayer flow
exclusively through the C ABI declared in include/conic_nexus.h. This guide is
the integration contract: everything an embedder needs to know is in this
document and in include/conic_nexus.h — no source reading required.
- License: MIT OR Apache-2.0
- Crate type:
cdylib+rlib - Edition 2024, rust-version 1.85
┌──────────┐ loads at runtime ┌─────────────────────────────────────┐
│ Conic │ ────────────────────▶ │ libconic_nexus │
│ Launcher │ │ host / guest flows, mesh, events │
└──────────┘ ◀──────────────────── │ driven only through the C ABI │
│ UI / state └─────────────────────────────────────┘
│ driven by
│ poll_event() + get_state()
Conic Nexus is a component, not a framework. It never calls into the embedder; the embedder drives it with commands and observes it by polling. The launcher decides when to start or stop a session and what to show the user; Conic Nexus decides how a room is created, joined, and kept alive.
What the component provides:
- Hosting — scans the LAN for a real Minecraft server, opens a room, and publishes a virtual LAN server that remote guests can reach.
- Joining — a guest dials in with a room code, gets a local
127.0.0.1:<port>to point Minecraft at, and keeps its player roster in sync. - Mesh transport — an embedded EasyTier node connects both sides over the
Internet (or a private relay) so the guest's
127.0.0.1port really reaches the host's Minecraft server. - Observability — an event queue plus a point-in-time state snapshot, so the launcher can render progress and failures with minimal effort.
cargo build # produces target/debug/libconic_nexus.dylib (+ .rlib)
cargo test --lib # room-code parse/mint testsConic Nexus embeds a full EasyTier runtime (easytier-core and easytier-cli,
v2.6.4) so the delivered library is self-contained: the launcher does not need
to install or manage anything. During the first build, build.rs downloads the
prebuilt EasyTier release for the target platform from the EasyTier GitHub
releases, repacks it with LZMA2 + BCJ into
.easytier/v2.6.4/<platform>/easytier.7z, and embeds that archive into the
library. The build therefore needs network access once per EasyTier version
per checkout; afterwards the cached archive is reused.
EasyTier is LGPL-3.0 licensed, and its binaries are redistributed inside
the library. The license text and copyright notice are preserved in
THIRD_PARTY_LICENSE.
The archive is not kept in memory. On the first session (and on every
conic_nexus_create), the binaries are extracted to
<data_dir>/embedded-easytier and spawned as normal child processes on demand.
Extraction happens lazily and only if the target directory is missing or stale.
data_dir (a field of ConicNexusConfig) is the runtime working directory. It
holds:
- the extracted EasyTier binaries (
<data_dir>/embedded-easytier/); - the machine identity used for player profiles.
It is also where per-process state is separated: two instances with different
data_dir values get independent EasyTier installations and machine identities.
The default is <system-temp>/conic-nexus. The embedder normally picks a
persistent, writable location (e.g. the launcher's data directory) and sets it
once via conic_nexus_configure.
The whole API surface is small. A complete session is: create → configure → start → poll → stop. The sections below follow that order.
conic_nexus_handle h = conic_nexus_create(); // NULL on failureconic_nexus_create() starts a session engine: a worker runtime, the event
queue, and the scaffolding server (see §13). Creating an instance does not
touch the network; it is cheap and can happen once at launcher startup.
Configuration is applied with conic_nexus_configure():
ConicNexusConfig cfg = {0};
cfg.data_dir = cn_string(my_dir); // runtime directory (see §2)
cfg.motd = cn_string("§a§lMy Server"); // host banner; NULL = default
const char *peers[] = {"tcp://relay.example.net:11010"};
ConicNexusString public_nodes[1] = {{peers[0], strlen(peers[0])}};
cfg.public_nodes = public_nodes;
cfg.public_nodes_count = 1;
conic_nexus_configure(h, &cfg);The config fields:
| field | purpose | default |
|---|---|---|
public_nodes |
EasyTier rendezvous servers the mesh uses to find peers. Each entry is a ConicNexusString. Overrides the built-in public list. |
built-in public list (see §14) |
public_nodes_count |
length of public_nodes |
0 |
data_dir |
runtime directory (EasyTier extraction, machine identity) | <temp>/conic-nexus |
motd |
banner shown on the guest's virtual LAN server; the host also uses it to ignore LAN servers it should not host | "§a§lConic Nexus" |
Lifecycle rules:
create()→configure()→ start a session →destroy().configure()may be called again between sessions (while Waiting), but a session is always started after the finalconfigure().configure()while a session is running returnsCONIC_NEXUS_ERR_BAD_STATE.destroy()tears everything down. Afterdestroy()the handle is permanently invalid (any further call returnsCONIC_NEXUS_ERR_INVALID_HANDLE).
create()
│
▼
configure()
│
▼
create_room(player_name, NULL) ── room code is minted for you
│
▼
┌─────────────────────── poll loop ───────────────────────┐
│ poll_event() │ get_state() │ (re)draw UI │
└───────────────────────────────────────────────────────────┘
│
▼ StateChanged → HOST_SCANNING → HOST_STARTING → HOST_OK
▼ HostReady event fires: the room is open
▼ ... guests join / leave (PlayerJoined / PlayerLeft) ...
│
▼
reset_to_waiting() ── or destroy()
// Mint the room code for you. The code is returned to you in the HostReady
// event payload AND in the state snapshot's room_code field.
int rc = conic_nexus_create_room(h, "MyServerName", NULL);
if (rc != CONIC_NEXUS_OK) { /* see §10 */ }
for (;;) {
conic_nexus_event ev;
if (conic_nexus_poll_event(h, &ev) == CONIC_NEXUS_OK) {
if (ev.type == CONIC_NEXUS_EVENT_HOST_READY) {
// ev.payload == {"room":"U/XXXX-XXXX-XXXX-XXXX","port":25565}
show_room_code_to_user(payload_room(&ev.payload));
}
if (ev.type == CONIC_NEXUS_EVENT_FAULT) { /* command failed, §10 */ }
conic_nexus_free_event(&ev);
}
update_ui(get_state(h)); // §7
sleep_ms(100);
}What happens on the host side, in order:
create_roomreturnsCONIC_NEXUS_OKimmediately; the session entersHOST_SCANNING.- The host scans the LAN for a real Minecraft server on port 25565 (deadline
60 s). If none is found the session lands in
EXCEPTION(error.code = 9, §8). - On success the session moves to
HOST_STARTING: the EasyTier node and the scaffolding API come up. HOST_OK— the room is live. TheHostReadyevent carries the room code and the mirrored Minecraft port. From this point the host's Minecraft server is reachable by any guest who enters the code.get_state()reportsstate = CONIC_NEXUS_STATE_HOST_OK,room_code, and the current roster.- Guests join and leave; you receive
PlayerJoined/PlayerLeftevents and see the roster inget_state().detail.profiles. - The room stays open until you call
reset_to_waiting()(or the host process exits).
The host player is always present in the roster as the first entry
(kind = "HOST").
create()
│
▼
configure()
│
▼
join_room("U/XXXX-XXXX-XXXX-XXXX", player_name)
│
▼
┌─────────────────────── poll loop ───────────────────────┐
│ poll_event() │ get_state() │ (re)draw UI │
└───────────────────────────────────────────────────────────┘
│
▼ StateChanged → GUEST_CONNECTING → GUEST_STARTING → GUEST_OK
▼ GuestReady event fires: the tunnel is up
│
▼ connect Minecraft to the URL from the event / snapshot
│
▼
reset_to_waiting() ── or destroy()
int rc = conic_nexus_join_room(h, "U/XXXX-XXXX-XXXX-XXXX", "MyName");
if (rc != CONIC_NEXUS_OK) { /* see §10 */ }
for (;;) {
conic_nexus_event ev;
if (conic_nexus_poll_event(h, &ev) == CONIC_NEXUS_OK) {
if (ev.type == CONIC_NEXUS_EVENT_GUEST_READY) {
// ev.payload == {"url":"127.0.0.1:25565"} (or 127.0.0.1:<port>)
point_minecraft_at(payload_url(&ev.payload));
}
conic_nexus_free_event(&ev);
}
update_ui(get_state(h));
sleep_ms(100);
}What happens on the guest side, in order:
-
join_roomvalidates the code (CONIC_NEXUS_ERR_INVALID_ROOM_CODEif it is malformed) and returns immediately; the session entersGUEST_CONNECTING. -
The guest's EasyTier node starts; the session moves to
GUEST_STARTING. -
The guest locates the host inside the mesh by its advertised hostname, verifies it with the scaffolding fingerprint, and installs local port-forwards to the host's scaffolding and Minecraft services.
-
GUEST_OK— the tunnel is up. TheGuestReadyevent carries the local listening address:"127.0.0.1"when the port is 25565, otherwise"127.0.0.1:<port>".
The guest connects Minecraft to exactly this address. The local port mirrors the host's Minecraft port; if that port is already taken locally the engine falls back to a free port.
-
The guest keeps the roster in sync with the host's authoritative list and emits
PlayerJoined/PlayerLeftas players come and go. It also health-checks the tunnel; if the host disappears the session moves toEXCEPTION.
Validate the code before the join.
conic_nexus_room_code_is_valid()is a pure, never-failing query — run it on every input box to reject obviously bad codes before starting a session.
States are reported in two places: the state field of ConicNexusState
(get_state()) and the payload of StateChanged events. They use the same
integer values.
create()
│
▼
┌───────────┐
┌────────▶│ WAITING │◀─────────────────────────┐
│ └───────────┘ │
│ create_room join_room │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ HOST_ │ │ GUEST_ │ │
│ │ SCANNING │ │ CONNECTING │ │
│ └─────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ HOST_ │ │ GUEST_ │ │
│ │ STARTING │ │ STARTING │ │
│ └─────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ HOST_OK │ │ GUEST_OK │ │
│ └─────────────┘ └──────────────┘ │
│ ╲ ╱ │
│ ╲ (any state, any failure) │
│ ╲ ╱ │
│ ▼ ▼ │
│ ┌───────────┐ │
│ │ EXCEPTION │──────────────────────┘
│ └───────────┘
│ │
│ reset_to_waiting()
└────────────────────┘
| state | value | entered | left when | embedder should | allowed calls |
|---|---|---|---|---|---|
WAITING |
0 | create / reset | create_room / join_room |
idle; safe to configure or start |
all |
HOST_SCANNING |
1 | create_room |
MC server found → 2; timeout → 7 | show "scanning for LAN server" | get_state, poll_event, reset_to_waiting |
HOST_STARTING |
2 | server found | EasyTier + scaffolding up → 3; spawn fail → 7 | show "starting" | get_state, poll_event, reset_to_waiting |
HOST_OK |
3 | startup done | reset_to_waiting; host MC lost / EasyTier exit → 7 |
show code, render roster | all |
GUEST_CONNECTING |
4 | join_room |
EasyTier up → 5; spawn fail → 7 | show "connecting" | get_state, poll_event, reset_to_waiting |
GUEST_STARTING |
5 | EasyTier up | locate + fingerprint + forwards done → 6; any failure → 7 | show "joining / verifying host" | get_state, poll_event, reset_to_waiting |
GUEST_OK |
6 | tunnel up | reset_to_waiting; host gone / EasyTier exit → 7 |
give user the url, render roster |
all |
EXCEPTION |
7 | any failure | only reset_to_waiting |
read detail.error, prompt user, then reset |
get_state, poll_event, reset_to_waiting |
Two properties are worth highlighting:
- Progress is only ever forward on the happy path: there is no "back" from
STARTINGtoSCANNING, or fromGUEST_STARTINGback toGUEST_CONNECTING. EXCEPTIONdoes not recover by itself. Nothing retries for you. The embedder readsdetail.error, tells the user what went wrong, and callsreset_to_waiting()to start over. This keeps failure handling simple and predictable (see §10).
- Events are a stream: things that happen over time — a guest joined, a guest left, the room became ready. They drive UI updates and sounds.
- State is a snapshot: everything true right now. It drives rendering and recovery.
A single StateChanged event tells you the session moved somewhere, but not
what the full picture is; get_state() gives you the full picture, but not the
history. Neither alone is sufficient — events without a snapshot force you to
track history yourself, and a snapshot without events hides roster churn. Use
them together (§11).
Events are delivered through a bounded queue (capacity 256) drained with
conic_nexus_poll_event. When the queue is full, the newest event is dropped
(older ones are kept). This is safe because the latest authoritative picture is
always available from get_state() — an embedder that polls frequently enough
never loses information it cares about.
sequence is a monotonically increasing counter the engine assigns as events
are produced; it is an ordering hint, not a gap-free ledger.
| type | value | payload (JSON) | fired when | embedder usually |
|---|---|---|---|---|
STATE_CHANGED |
0 | {"state":<int>,"version":<uint64>} |
the session moved to another state | sync UI to the new state; optionally read get_state() |
PLAYER_JOINED |
1 | {"profile":{...}} — profile shape in §8 |
a player appeared in the roster | show "X joined" |
PLAYER_LEFT |
2 | {"machine_id":"..."} |
a player vanished (guest left, or host-side staleness pruning after 30 s) | show "X left" |
HOST_READY |
3 | {"room":"U/XXXX-XXXX-XXXX-XXXX","port":25565} |
host reached HOST_OK |
reveal the room code to the user |
GUEST_READY |
4 | {"url":"127.0.0.1"} or {"url":"127.0.0.1:<port>"} |
guest reached GUEST_OK |
point Minecraft at url |
FAULT |
5 | {"code":<int>,"message":"..."} — see §10 |
a command failed (invalid code, session already active) | handle as a recoverable command error; the session may still be usable |
StateChanged does not replace the others, and vice versa. A roster change
sends PlayerJoined/PlayerLeft (and bumps the snapshot version) but does not
change state; reaching HOST_OK sends both StateChanged and HostReady.
Free every event you poll:
conic_nexus_free_event(&ev). The payload string is owned by the library.
Conic Nexus uses a command + polling model. There are no callbacks and the component never calls into the embedder.
- Commands —
create_room,join_room,reset_to_waiting— are asynchronous. They validate input synchronously (so you get an immediate error for a bad code or a busy session) and returnCONIC_NEXUS_OKonce the command is accepted. The actual work happens on the engine's worker thread. - Observation —
poll_event()drains the event queue;get_state()reads the current snapshot. The embedder calls both repeatedly.
This asymmetry is deliberate: the launcher owns its UI thread and event loop, and never blocks on network work. The library never surprises the embedder by running code on its thread.
Recommended poll cadence: 100–200 ms. Poll faster only if you need
near-instant PlayerJoined feedback; poll slower if you want to minimize wakeups.
poll_event never blocks — it returns CONIC_NEXUS_ERR_NO_EVENT when the queue
is empty.
embedder conic-nexus
│ create_room(...) │
│ ─────────────────────────────────▶│ validates, returns OK
│ OK │
│ ... │ worker does the work
│ poll_event() │
│ ─────────────────────────────────▶│
│ StateChanged(HOST_SCANNING) ◀────│
│ ... │
│ poll_event() │
│ ─────────────────────────────────▶│
│ HostReady(...) ◀─────────────────│
│ get_state() │
│ ─────────────────────────────────▶│
│ {state:HOST_OK, room_code,...} ◀─│
conic_nexus_get_state(h, &out) fills a ConicNexusState snapshot. Call
conic_nexus_free_state(&out) when done.
typedef struct ConicNexusState {
uint64_t version;
int32_t state; // enum ConicNexusState (§4)
ConicNexusString room_code;
ConicNexusString detail; // JSON (§8)
} ConicNexusState;| field | meaning | changes |
|---|---|---|
version |
monotonically increasing revision of the session | every committed mutation (state changes and roster changes) |
state |
current session state (§4) | on every StateChanged |
room_code |
the active room code | set when a room is created/joined, cleared on reset_to_waiting |
detail |
JSON detail for the current state (§8) | with the state, the roster, and the overlay status |
Why call get_state() even though events exist?
- It cannot miss anything. Dropped or missed events are irrelevant: the snapshot always reflects the latest truth. This is your recovery source of truth.
- It is the only source for some data. The roster
(
detail.profiles), the overlay status (detail.overlay), and the guest connection URL (detail.url) are authoritative here. - It makes rendering trivial. Draw your screen from one snapshot instead of folding dozens of events.
Recommended pairing (§11): use poll_event() for the delta (toasts,
sounds, one-off actions like revealing the room code) and get_state() for the
full frame (roster list, connection status, error panel).
The detail field of ConicNexusState is a JSON object whose shape depends on
the current state. All fields below are stable ABI: consumers may rely on
their names and types.
{}{
"port": 25565,
"profiles": [
{"machine_id": "0123456789abcdef...", "name": "MyServerName",
"vendor": "Conic Nexus 0.1.0, EasyTier v2.6.4", "kind": "HOST"},
{"machine_id": "fedcba...", "name": "Alice", "vendor": "...", "kind": "GUEST"}
],
"overlay": {"pid": 1234, "alive": true, "rpc_port": 23556}
}{
"url": "127.0.0.1",
"profiles": [
{"machine_id": "...", "name": "MyName", "vendor": "...", "kind": "LOCAL"},
{"machine_id": "...", "name": "MyServerName", "vendor": "...", "kind": "HOST"}
],
"overlay": {"pid": 1234, "alive": true, "rpc_port": 23556}
}url— the local address to point Minecraft at (127.0.0.1, or127.0.0.1:<port>when the port is not 25565).profiles— the authoritative roster.kindis one ofHOST,LOCAL(your own profile),GUEST.vendoris the self-reported client identification. The list is sorted and deduplicated bymachine_id.
{
"error": {
"code": 4,
"message": "Minecraft server connection lost"
}
}code uses the session-error codes below; message is the same information in
human-readable form and is intended for direct display.
| code | macro | meaning |
|---|---|---|
| 0 | CONIC_NEXUS_SESSION_ERR_HOST_UNREACHABLE |
cannot reach the host (locate/fingerprint timeout) |
| 1 | CONIC_NEXUS_SESSION_ERR_FINGERPRINT_MISMATCH |
a peer answered TCP but failed the scaffolding fingerprint — definitively not a genuine host |
| 2 | CONIC_NEXUS_SESSION_ERR_SCAFFOLDING_REJECTED |
the host's scaffolding server rejected a request |
| 3 | CONIC_NEXUS_SESSION_ERR_SCAFFOLDING_INVALID |
a scaffolding response was malformed |
| 4 | CONIC_NEXUS_SESSION_ERR_SERVER_LOST |
the host's Minecraft server connection was lost |
| 5 | CONIC_NEXUS_SESSION_ERR_HOST_OVERLAY_EXIT |
the host's EasyTier process exited |
| 6 | CONIC_NEXUS_SESSION_ERR_GUEST_OVERLAY_EXIT |
the guest's EasyTier process exited |
| 7 | CONIC_NEXUS_SESSION_ERR_OVERLAY_SPAWN_FAILED |
failed to spawn an EasyTier process |
| 8 | CONIC_NEXUS_SESSION_ERR_PORT_FORWARD_FAILED |
port-forward rules could not be installed |
| 9 | CONIC_NEXUS_SESSION_ERR_HOST_SCAN_TIMEOUT |
the host found no LAN Minecraft server in time |
conic_nexus_reset_to_waiting(h) aborts the active session (host or guest)
and returns to WAITING. It is much more than "close the room":
- stop and reap the EasyTier node;
- shut down the virtual LAN server and release the local port;
- clear the room code, the roster, and any error;
- emit a final
StateChanged(WAITING).
After the reset completes the same handle can be reused for a new
create_room() or join_room() — no need to destroy() and recreate.
Use it for all of these:
| scenario | why reset, not destroy |
|---|---|
| host closes the room | reuse the handle for the next session |
| guest leaves the room | same |
| user cancels a connection in progress | aborts SCANNING/CONNECTING/STARTING |
session in EXCEPTION |
the only way out (see §4) |
reset_to_waiting is asynchronous like the other commands: it returns
CONIC_NEXUS_OK immediately and the session moves to WAITING shortly after
(visible via a StateChanged event / get_state()). If the session is already
WAITING, the call is a no-op.
destroy()also stops any running session, but it permanently invalidates the handle. Preferreset_to_waiting()when you expect to host/join again.
There are two independent error surfaces. Do not confuse them.
Returned synchronously by every command. They mean the call itself was
rejected; the session is untouched (still in WAITING unless otherwise noted).
| code | returned when | embedder should |
|---|---|---|
CONIC_NEXUS_OK = 0 |
success | — |
CONIC_NEXUS_ERR_INVALID_HANDLE = -1 |
null/stale/destroyed handle | drop the handle, recreate the instance |
CONIC_NEXUS_ERR_INVALID_ARGUMENT = -2 |
null out-pointer, non-UTF-8 string | fix the caller |
CONIC_NEXUS_ERR_BAD_STATE = -3 |
configure while a session is running |
call it while WAITING |
CONIC_NEXUS_ERR_INVALID_ROOM_CODE = -4 |
malformed room code in create_room/join_room |
validate earlier with room_code_is_valid |
CONIC_NEXUS_ERR_ALREADY_ACTIVE = -5 |
create_room/join_room while a session is running |
reset_to_waiting first |
CONIC_NEXUS_ERR_INTERNAL = -6 |
unexpected internal failure | treat as a bug; retry once, then report |
CONIC_NEXUS_ERR_OUT_OF_MEMORY = -7 |
allocation failure | back off |
CONIC_NEXUS_ERR_NO_EVENT = -8 |
poll_event queue empty (not an error) |
loop again after a short sleep |
CONIC_NEXUS_ERR_SHUTTING_DOWN = -9 |
reserved; the engine is shutting down | stop calling |
A runtime failure inside an active session. It is not returned by any
command; it appears as the EXCEPTION state with detail.error
(code/message, see §8). Example: the host's Minecraft server dies, or the
guest's EasyTier node exits.
Recommended flow on EXCEPTION:
StateChanged/EXCEPTION seen
│
▼
read detail.error.code + message (get_state())
│
▼
present the message to the user
│
▼
reset_to_waiting()
│
▼
optionally start a new session
A third, smaller surface: the FAULT event (type 5) fires when an accepted
command fails asynchronously, e.g. an invalid room code slipped through or a
session was already active. Its payload is {"code","message"} with these
codes: 1 invalid room code, 2 a session is already active, 3 internal,
4 not implemented. The session may still be usable — treat it as a prompt to
tell the user, not as a fatal state.
The recommended embedder loop combines the event stream with the state snapshot:
for (;;) {
/* 1. drain events — the delta */
conic_nexus_event ev;
while (conic_nexus_poll_event(h, &ev) == CONIC_NEXUS_OK) {
handle_event(&ev); // toasts, sounds, one-off actions
conic_nexus_free_event(&ev);
}
/* 2. read the snapshot — the full frame */
conic_nexus_state st;
if (conic_nexus_get_state(h, &st) == CONIC_NEXUS_OK) {
render(&st); // roster, status, room code, error
conic_nexus_free_state(&st);
}
/* 3. let the UI breathe */
sleep_ms(100); // poll cadence: 100–200 ms
}Why both?
- Events tell you what happened — "Alice joined", "the room is ready". You act on them once.
- State tells you where you are — "guest connecting", "error code 4". You render from it every frame, and it is always correct even if you missed events.
get_state() on a frame where nothing changed is cheap; do not try to optimize
it away. This loop is also how you make the UI respond instantly to
reset_to_waiting and to cancellations.
A room code looks like U/XXXX-XXXX-XXXX-XXXX and is the only secret a guest
needs to join.
U/2K4Q-0H7N-9ZP2-6RDE
└┘ └────────────┘└─────┘
│ first 8 digits last 8 digits
│ ───────────────── ─────────────
│ network name network secret
│ scaffolding-mc-2K4Q-0H7N 9ZP2-6RDE
-
The 16 digits after
U/encode a 128-bit value in base-34, least significant digit first. To keep codes human-friendly,Iis read as1,Oas0, andLis omitted from the alphabet. -
A code is valid only if its value is divisible by 7. This is a checksum: most typos produce a value that is not, so
room_code_is_valid()catches them early. -
The engine derives all EasyTier mesh credentials from the code:
- network name:
scaffolding-mc-<first 8 digits> - network secret:
<last 8 digits>
There is nothing else to exchange — the code alone pins the mesh, which is why both sides can join the same virtual network with no other setup.
- network name:
-
Parsing tolerates case and leading garbage (it scans for the
U/marker), so pasting codes from chat works even when the surrounding text leaks in.
Use conic_nexus_room_code_is_valid() to validate user input before
join_room().
This section documents the on-the-wire protocol Conic Nexus speaks inside the mesh. It is the interoperability contract between a host and a guest and is fixed for cross-implementation compatibility.
The host runs a small TCP scaffolding server inside the mesh (on port 13448
when available, otherwise an OS-assigned port) and advertises it in its mesh
hostname (scaffolding-mc-server-<port>), which is how guests locate it.
Before trusting anything, the guest verifies the server by fingerprinting
c:ping with a fixed 16-byte challenge.
Framing — requests and responses share one shape:
- request:
[kind-len:u8][kind "ns:path"][body-len:u32 BE][body] - response:
[status:u8][body-len:u32 BE][body]
Handlers (namespace c):
| handler | body | returns |
|---|---|---|
c:ping |
echo payload | same payload (guest verifies a fixed 16-byte fingerprint) |
c:protocols |
— | five NUL-separated protocol names |
c:server_port |
— | host's Minecraft port, 2-byte big-endian |
c:player_ping |
JSON {machine_id,name,vendor} |
updates the host roster |
c:player_profiles_list |
— | JSON array of player profiles (kind: HOST/LOCAL/GUEST) |
Conic Nexus uses EasyTier as its mesh transport. A room is simply an
EasyTier network whose name and secret are derived from the room code (§12);
host and guest each run an embedded easytier-core node and join it. This
section describes how the integration behaves — it is not an EasyTier tutorial.
Both sides start their node with:
--network-name <scaffolding-mc-...> (from the room code)
--network-secret <...> (from the room code)
-p <rendezvous server>... (see below)
--no-tun --compression zstd --multi-thread --latency-first --enable-kcp-proxy
-l udp://0.0.0.0:0 -l tcp://0.0.0.0:0
--p2p-only
plus role-specific flags:
- host:
--hostname scaffolding-mc-server-<scaffolding_port>,--ipv4 10.144.144.1,--tcp-whitelist <scaffolding_port>,--tcp-whitelist <mc_port>,--udp-whitelist <mc_port> - guest:
-d(DHCP address), plus runtime port-forwards to the host's scaffolding and Minecraft services.
Two flags deserve explanation because they directly affect whether a mesh can form:
- Listeners bind ephemeral ports (
-l ...:0), so a host and guest that happen to run on the same machine never collide on the well-known 11010 listener port. --p2p-onlyis used: it keeps traffic on the direct peer path once reachable. (Do not use--no-listener— a dial-out-only node cannot accept the inbound connection a P2P tunnel requires.)
The node is started with the public_nodes from ConicNexusConfig, falling
back to the built-in public EasyTier rendezvous list:
tcp://public.easytier.top:11010
tcp://public2.easytier.cn:54321
https://etnode.zkitefly.eu.org/node1
https://etnode.zkitefly.eu.org/node2
Setting public_nodes in the config replaces this list, which is how an
embedder routes traffic through its own relay nodes instead of the public
servers.
The node is spawned when a session starts, reaped on reset_to_waiting() or
destroy() (the process is killed). Node failures surface as session errors
5/6/7 (§8). Diagnostics are available through conic_nexus_recent_logs()
and peer/NAT status through conic_nexus_query_peers() (§15).
All symbols use the conic_nexus_ prefix; types use the ConicNexus prefix.
The authoritative declaration is include/conic_nexus.h. Result codes, enum
values, and struct layouts there are the ABI contract — see §10 for the return
codes.
Functions are grouped by lifecycle role.
| function | description |
|---|---|
conic_nexus_create() |
create a session engine; returns a handle or NULL |
conic_nexus_destroy(handle) |
tear everything down; the handle is permanently invalid afterwards |
| function | description |
|---|---|
conic_nexus_configure(handle, cfg) |
apply ConicNexusConfig; only while WAITING (CONIC_NEXUS_ERR_BAD_STATE otherwise) |
| function | description |
|---|---|
conic_nexus_create_room(handle, player_name, room_code) |
open a room; pass room_code = NULL to mint one, player_name = NULL for the default |
| function | description |
|---|---|
conic_nexus_join_room(handle, room_code, player_name) |
join a room by code; room_code is required, player_name optional |
| function | description |
|---|---|
conic_nexus_reset_to_waiting(handle) |
abort the current host/guest session and return to WAITING (§9) |
| function | description |
|---|---|
conic_nexus_poll_event(handle, &event) |
pop the next event; CONIC_NEXUS_ERR_NO_EVENT when empty (§5) |
conic_nexus_get_state(handle, &state) |
current snapshot: version, state, room code, detail JSON (§7) |
| function | description |
|---|---|
conic_nexus_room_code_is_valid(code) |
pure query; NULL/bad code → false; never fails |
conic_nexus_version() |
static, NUL-terminated version string |
conic_nexus_recent_logs(limit, &out) |
the limit most recent log lines as a JSON array of strings (most recent last); not per-handle — it is a process-wide ring buffer. Free with conic_nexus_free_string |
| function | description |
|---|---|
conic_nexus_query_peers(handle, &out, &count) |
current mesh peers + NAT types (EasyTier codes 0–9; see the header). CONIC_NEXUS_ERR_BAD_STATE when no node is running, CONIC_NEXUS_ERR_INTERNAL on query failure; empty list → out = NULL, count = 0. Free with conic_nexus_free_peers |
conic_nexus_free_peers(peers, count) |
free a peer array from query_peers |
Strings returned by the library are owned by the library and reference-counted by the caller. Every aggregate you poll or query must be freed with its matching free function:
| returned by | free with |
|---|---|
poll_event |
conic_nexus_free_event(&event) |
get_state |
conic_nexus_free_state(&state) |
query_peers |
conic_nexus_free_peers(peers, count) |
recent_logs (payload) |
conic_nexus_free_string(&out) |
ConicNexusString fields are not NUL-terminated; always honor len.
Two small, self-contained binaries demonstrate the C ABI against a real
session. Build the library first (cargo build at the crate root), then run
each from its own directory. Both load libconic_nexus.dylib at runtime
through libloader — point them at a local build with CONIC_NEXUS_LIB.
The host example needs a real LAN Minecraft server on port 25565 to mirror; without one it exits after the scan deadline (60 s).
# terminal 1 — the host: opens a room and prints the code
cd examples/host
cargo run [player_name]
# terminal 2 — the guest: joins with the code the host printed
cd examples/guest
cargo run -- U/XXXX-XXXX-XXXX-XXXX [player_name]examples/host/—cargo run [player_name]. Mints a room code, creates a room, prints the code, and streams session events (StateChanged,HostReady,PlayerJoined,PlayerLeft, …) until Ctrl-C. Share the printed code with a guest.examples/guest/—cargo run -- <room_code> [player_name]. Takes the room code as a command-line argument, validates it, joins the room, and once ready prints the local address to connect Minecraft to. Streams events until Ctrl-C.
Environment overrides used by both examples:
| variable | purpose |
|---|---|
CONIC_NEXUS_LIB |
path to libconic_nexus.dylib (default ../../target/debug/libconic_nexus.dylib) |
CONIC_NEXUS_MESH_PEERS |
whitespace/comma-separated EasyTier peer URLs handed to the library as public_nodes |
build.rs downloads/repacks/embeds EasyTier binaries (7z)
include/ conic_nexus.h (C ABI declaration — the integration contract)
LICENSE-MIT / LICENSE-APACHE project licenses
THIRD_PARTY_LICENSE EasyTier LGPL-3.0 text and copyright notice
src/ffi.rs C ABI, handle table, result codes
src/engine.rs session state machine, event queue
src/room.rs room-code minting/parsing
src/wire/ scaffolding client/server + framing
src/lan/ LAN server scanner + fake server
src/overlay/ easytier-core spawn, RPC peer queries, port-forwards
src/flows/ host and guest orchestration
src/ports.rs dynamic port allocation
Conic Nexus is built on top of EasyTier as its underlying virtual network. We thank the EasyTier authors and contributors for providing a stable peer-to-peer networking layer that makes cross-network Minecraft LAN multiplayer possible.
Conic Nexus is dual-licensed under the MIT License and the Apache License Version 2.0, at your option:
The scaffolding wire protocol and room-code scheme are required for interoperability with the Terracotta ecosystem and are documented in this README (§12, §13) as the protocol contract.
Conic Nexus embeds and redistributes the prebuilt EasyTier binaries
(easytier-core, easytier-cli, v2.6.4), which are licensed under the
GNU Lesser General Public License version 3.0. EasyTier is developed by the
EasyTier project (github.com/EasyTier/EasyTier).
The full LGPL-3.0 text and the EasyTier copyright notice are preserved in
THIRD_PARTY_LICENSE.