Skip to content

v3.0.0

Choose a tag to compare

@NewbieOrange NewbieOrange released this 13 Sep 12:25
· 7 commits to master since this release

ShadowLAN 3.0.0

Range: v2.0.0 (2026-09-09) → v3.0.0 (2026-09-13).

Hook and relay must be deployed together. A 2.x peer is dropped at registration (NODE version check). Old hooks cannot parse a 3.0.0 ASSIGN.


Breaking

  • Control protocol is PVER 3 (was 2). Registration is the only versioned frame; gating it gates streams too.
  • ASSIGN no longer carries a trailing link_id byte. Payload is 11+8*n (vnode, net, bits, membership only).
  • UDP source identity is the sender socket’s bound vport — the port a real NIC would stamp. The v2 slot-mark (link_id*256+slot) is gone from the wire. Apps that fold recvfrom sources into peer state and dial them now get a real, dialable port.
  • STREQ is exactly 10 bytes (sid + gport + opener vnode). Short frames are rejected (no ovirt=0 fallback).

Protocol and relay

  • connect() completes when a dest link claims the stream (STOK at claim = SYN/ACK). STJOINED starts the raw pipe. A bridge failure after that is a post-connect reset, which real TCP can do too.
  • Half-close: T_STSHUT FINs one direction and leaves the reverse alive. Implicit (port-only) streams now record the winning joiner so a joinee STSHUT actually reaches the opener.
  • Designated-host election is gone. Implicit dials fan out like LAN ARP; every live link is asked, only the process that listens claims. NODE_F_HOST is an ordering stamp for wclient --host migration, not an election.
  • Membership lists only nodes with a live link. A fully dark node keeps its virtual IP for reconnect (up to NODE_TTL) but disappears from peer tables immediately.
  • Beacons are forwarded, never cached or replayed. Same-process control links get NIC-once delivery (no duplicate beacons that fold phantom peers).
  • UDP-over-TCP links are addressable (("tcp", writer)), so rooms behind unfriendly NAT can start a join instead of dropping P2P as no-udp-endpoint.
  • Implicit claims use a short preferential window (IMPLICIT_GRACE_S): all claims collected, freshest host_claim wins, the rest get BUSY.
  • Accepted tunnel sockets get the same TCP tuning as the hook (Nagle off, large windows on the SYN).
  • Stream verdicts match a kernel: nobody claims → framed NO_ROUTE (fast refuse); claim then vanish → transport EOF/reset, not a 10 s stall.

Hook — LAN fidelity

  • Accept door. Accepted game sockets present the opener’s vnode on accept, getpeername, and getsockname. Local port is the listen vport, not an aliased kernel ephemeral. Learn matches the bridge ephemeral — never “first live hosted row” (that stole the opener vnode on same-box hairpins).
  • Same-box hairpin. A process dialing its own vnode uses this process’s alias real port, not the vport number (which another node on the same kernel may own).
  • Recv gather. recv returns the full in-queue that FIONREAD reported, not only the first chunk. Short first-chunk pops left lobby parsers waiting on a length prefix forever.
  • Hosted UDP identity. Session ovirt is the parsed vnode, not the first four ASCII bytes of "10.200.…".
  • Replacement atomicity. A new STREQ for the same (peer, gport) retires the old hosted session before claiming (kernel: a new connection implies the old is dead).
  • Close / duplex. shutdown(SHUT_RD/WR), close() flushes the out-queue, peer FIN half-closes instead of killing both pumps, FIN vs RST distinguished (recv 0 vs ECONNRESET). SO_ERROR and select/poll connect edges match the kernel (no vacuous writable while still connecting).
  • Exit drain. Queued stream bytes and the control-frame queue flush on process exit (ExitProcess / exit / _exit), so parting frames land instead of peers waiting out an app timeout.
  • Hosted pump. Unwritten game bytes are held and retried; the pump no longer drops a remainder when the relay write would block (that silently corrupted streams and looked like app-side stalls).
  • Event-driven IO. Control link and per-stream pumps wake on kernel readiness plus a self-wake pair. Claim round-trip is sub-millisecond locally; poll slices are idle backstops only.
  • Bind ledger. Node-scoped shared registry (Windows Local\ mapping / Linux shm_open+flock). Same-node binds collide like a real kernel; different nodes on one OS alias underneath and never see each other. Dead-owner slots are reclaimed. SO_REUSEADDR UDP sharing matches the kernel (both sides must set it). bind(0) under LAN_ONLY allocates from the node ephemeral space.
  • Last-error. hk_bind has a single exit that snapshots WSAGetLastError / errno around logging, so apps no longer see err=2 for a refused bind.
  • Windows ledger crash. Startup AV from GetProcessTimes on a kernelbase out-param is gone (PEB create-time + GetExitCodeProcess liveness only). Registry init is content-based; the section create handle stays open for the process lifetime.
  • Own-vnode dials loop back correctly (compare against g_node, not the vnode number).
  • sid seeding is 64-bit on both Windows ABIs (the old unsigned long fold was a no-op on Win32/Win64).
  • Channel policy after the mark fix: same-box double-node runs behave like two machines (forward and reverse both bridge). The old co-host yield guard and any-proto bridge backstop are gone — they existed only to paper over the mark leak.

Hook — structure

  • One translation unit per hk_*.c. hk_core holds wire ops, tables, DLOCK, and policy. lan_hook.c is the version stamp only. Linux is hk_linux.c; Windows is hk_winsock / hk_wicmp / hk_winnic / hk_wininst.
  • Compile defaults to -j$(nproc); a parent jobserver or explicit -j still wins. Objects rebuild from headers, not from every sibling .c.
  • pack.sh lists every artifact and refuses a Windows zip that contains lan_hook.so (or a Linux tarball that contains the DLLs).

Client

  • wclient stream pumps are strongly referenced (WinClient._spawn) so the loop cannot GC a live pipe.
  • wclient parses STREQ with the shared 10-byte decoder.

Tests and harness

  • make -C hook test runs every Linux suite in parallel (runtests.py -j, ports from testutil.free_port()). test-all adds Wine to the same pool.
  • New / extended guards: test_fullduplex (close, half-close, gather), test_bindfidelity, test_aliasbridge (same-box hairpin, accept-door triple, forward channel), test_socket_doors, test_stfail_semantics (including implicit STSHUT), test_burst_connect, test_exitdrain, test_treeid.

Packages

  • shadowlan-v3.0.0-linux-amd64.tar.gz — lan_hook.so + Python + docs
  • shadowlan-v3.0.0-windows-amd64.zip — both DLLs + injector + Python + docs