Skip to content

Releases: NewbieOrange/ShadowLAN

v3.1.0

Choose a tag to compare

@NewbieOrange NewbieOrange released this 02 Oct 08:05

ShadowLAN 3.1.0

A latency and bufferbloat release. Games now hear from each other as fast as the network allows, and bulk transfers still run at
full link speed, matching a plain TCP connection over the same path.

The wire protocol is unchanged (PVER 3 / UVER 3): 3.0.0 and 3.1.0 hooks and relays work together. Deploy the new server.py on
your relay to get the relay-side improvements.

Highlights

  • Low latency on every way a game waits for data. Before, the hook checked for tunneled data on a timer, which added delay
    depending on how the game read its sockets:

    • TCP with a blocking recv: about 10 ms
    • TCP or UDP with select, poll or WSAPoll: about 25 ms (51 ms for blocking UDP receives on Windows)

    Now a game wakes up the moment data arrives: about 0.1 ms round trip on Linux and about 0.5 ms on Windows (measured under Wine),
    not counting the network.

  • No more busy-spinning. Games waiting on a tunneled TCP connection with poll/select used to spin a CPU core at 60–99%.
    They now sleep until data arrives.

  • Much less bufferbloat during bulk transfers. On a 20 Mbit/s link with 40 ms round trip, a message sent on the same connection
    as a running download used to wait about 4 seconds. Now it waits about 240 ms, which is less than a plain kernel TCP connection on
    the same path (about 555 ms). Throughput is unchanged.

  • UDP-over-TCP stays responsive under load. When game traffic exceeds the link, stale datagrams are dropped instead of queuing
    without limit. Latency stays around 200 ms instead of growing past several seconds.

Fixes

  • Linux: blocking UDP receives no longer hang. A plain blocking recvfrom on a tunneled UDP socket could block forever.
  • Startup deadlock when several threads bind at once. Two threads binding sockets at the same time could leave every later
    bind() in the process hanging.
  • Data at process exit. A game that sent data and exited immediately could lose the tail of it. Two causes are fixed: the
    hook's exit flush could silently skip a stream, and the relay closed a departing machine's open connections before their last bytes
    arrived.
  • End of stream is reported to poll/select. When the other side closes a TCP connection, the game's poll/select now
    reports it as readable, as a real kernel does.
  • New hosted UDP sessions get their first reply immediately instead of after up to 100 ms.
  • wclient relay connections now have Nagle's algorithm disabled, avoiding delays on small writes.

Bandwidth

Bulk throughput matches a direct kernel TCP connection over the same links:

Link 3.0.0 3.1.0 Plain kernel TCP
20 Mbit/s, 40 ms RTT 19.1 Mbit/s 19.1 Mbit/s 19.0 Mbit/s
200 Mbit/s, 100 ms RTT 187 Mbit/s 186 Mbit/s 181 Mbit/s

Tunnel connections now let the operating system size their buffers automatically. Before, 3.0.0 requested fixed 4 MB buffers, which
turned that off. On a typical VPS the kernel cuts such a request down to about 425 KB, which limited throughput on long, fast
paths.

Under the hood

  • Every waiting call (recv, recvfrom, connect, send, poll, ppoll, select, pselect, WSAPoll) is woken by the hook
    as soon as data arrives, instead of rechecking on a timer.
  • Unsent data is kept small (TCP_NOTSENT_LOWAT on Linux and the relay). The app-side send queue per connection is 256 KB, down
    from 4 MB. A single larger send is still accepted in parts, as a real socket does.
  • Each control message goes out in one TCP segment instead of three.

Upgrading

  • Relay: replace server.py and restart. Existing 3.0.0 hooks keep working.
  • Players: replace the hook files from the package for your platform. A 3.1.0 hook works with a 3.0.0 relay too, but you only
    get the full improvement with both upgraded.

Assets

  • shadowlan-v3.1.0-windows-amd64.zip: lan_hook64.dll, lan_hook32.dll, injector.exe, relay and client scripts, docs
  • shadowlan-v3.1.0-linux-amd64.tar.gz: lan_hook.so, relay and client scripts, docs

v3.0.0

Choose a tag to compare

@NewbieOrange NewbieOrange released this 13 Sep 12:25

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

v2.0.0

Choose a tag to compare

@NewbieOrange NewbieOrange released this 12 Sep 10:21

Full Changelog: v0.1.0...v2.0.0

v1.0.0

Choose a tag to compare

@NewbieOrange NewbieOrange released this 06 Sep 17:33

Full Changelog: v0.1.0...v1.0.0

v0.1.0

v0.1.0 Pre-release
Pre-release

Choose a tag to compare

@NewbieOrange NewbieOrange released this 05 Sep 17:42
chore: ignore build binaries