Play broadcast-only LAN co-op games online, on Windows and Linux. One dedicated relay on a VPS hands every player a virtual LAN IP — any player can host, the rest just join. No TUN/TAP, no virtual NIC, no driver, no admin.
- Relay + client are plain Python 3.10+ (stdlib only), identical on both OSes
- Per-game network hook: Windows DLL (
lan_hook64/32.dll) or LinuxLD_PRELOAD(lan_hook.so) - One public port on the relay (TCP+UDP); players need no port forwarding
- Discovery broadcast, game TCP and game UDP all tunneled; UDP stays UDP
- P2P mesh: each node appears at its own virtual IP (default
10.200.0.0/24)
- Each node registers with the relay (
NODE+ token room) and gets a stable virtual IP. - Discovery broadcasts fan out with sender identity; games see distinct servers at distinct IPs.
- Joins go to explicit destinations: addressed TCP opens and UDP datagrams route by node (per-dest flows, so identical LAN triples to different hosts don't collide); implicit (unaddressed) traffic falls back to designated-host → beaconer → per-sender sticky-replier routing. A new host claim only steers NEW implicit joins — live streams keep flowing to their original targets.
- The hosting node bridges tunnel traffic to its local game at
127.0.0.1— viawclient --host, or the hook, which auto-claims when the game callslisten().
Relay (any Linux/Windows box with a public IP, forward TCP+UDP 47777):
python server.py --port 47777 --token SECRET --subnet 10.200.0.0/24Hosting player (the PC running the game server):
Windows:
injector.exe --server RELAY_IP --port 47777 --token SECRET -- lan_hook64.dll game.exeLinux, or Windows without the hook:
python wclient.py --server RELAY_IP --port 47777 --disc 4444 --host --token SECRETLinux hook instead of wclient (same thing the DLL does, via LD_PRELOAD):
LD_PRELOAD=./lan_hook.so LAN_HOOK_SERVER=RELAY_IP LAN_HOOK_PORT=47777 LAN_HOOK_TOKEN=SECRET ./gameJoining players (same binaries, no flags beyond the room):
injector.exe --server RELAY_IP --port 47777 --token SECRET -- lan_hook64.dll game.exepython wclient.py --server RELAY_IP --port 47777 --disc 4444 --tcp 27015 --udp 7777 --token SECRETThen the host starts the LAN game, everyone else opens the LAN browser and joins. Find the game's ports once with Wireshark (udp.dstport, tcp.dstport) or netstat -ano / ss -tunap: discovery UDP, game TCP/UDP. Include discovery UDP ports in --udp too if that title uses unicast discovery replies. One game per relay port is still the supported setup (run more relays on more ports for more parties); if two hosts do claim the same port, live sessions now survive and only new implicit joins follow the newest claim, while addressed (hook virtual-IP) sessions stay per-dest isolated.
injector.exe [--server HOST] [--port PORT] [--token SECRET] [--ports LIST] [-e KEY=VAL]... [--debug] [--]
<hook.dll> <game.exe> [game args...]Without --server the hook is a passthrough. Also honored as env (both OSes): LAN_HOOK_SERVER, LAN_HOOK_PORT (default 47777), LAN_HOOK_TOKEN, LAN_HOOK_PORTS (4444,27015 to limit hooked ports), LAN_HOOK_DEBUG=1. Match the DLL to the game bitness (lan_hook32.dll for 32-bit games).
server.py— dedicated relay only: membership, virtual-IP assignment, addressed + fallback routingwclient.py— symmetric client: player proxy and/or--hostbridge, node registrationcommon.py— shared framinghook/lan_hook.c— universal hook (Windows DLLs + Linux.sotest/LD_PRELOAD build)hook/injector.c— Windows launcher (injector.exe)game.example.json— config template
make -C hook all # DLLs + injector.exe (mingw) + lan_hook.soOn Windows with MinGW: hook/build.bat. Run hook tests with make -C hook test.
python3 test_e2e.py # relay + wclient host + wclient player (ALL PASS)
python3 test_relay_topology.py # host migration incl. demote-keeps-link (MIGRATE_ALL_PASS)
python3 hook/test_direct.py # symmetric hook host+join (DIRECT_ALL_PASS)
python3 hook/test_virtual_p2p.py # 3-node virtual-IP mesh (P2P_ALL_PASS)
python3 hook/test_childprop.py # child-process inheritance (CHILDPROP_ALL_PASS)
python3 hook/test_perdest.py # per-dest UDP flows, node GC (PERDEST_ALL_PASS)
SHADOWLAN_WINEPREFIX=~/.wine python3 hook/test_late.py # late-loaded plugin DLLs (LATE_ALL_PASS; Wine)- IPv4 only; IPv6 passes through untouched
- Same-machine peers talk over the real local stack (self-addressed traffic is never tunneled); the tunnel carries what would leave the NIC. Keep the relay subnet (default 10.200.0.0/24) clear of any real local interface, or pick another with --subnet.
- One game per relay port is the supported setup;
--tokenis the room key (empty = open relay). Accidental double-claims no longer kill live sessions (new implicit joins follow the newest claim; addressed hook sessions are per-dest isolated). - Same-dest UDP with byte-identical LAN triples from two sites still last-writer-wins per dest (logged); the hook sends from its virtual IP so hook nodes don't collide, and
wclientclient ports are usually random ephemerals. - No encryption — trusted peers only (or wrap in WireGuard)
- Async overlapped Winsock and
ConnectEx/WSAEventSelectwaiting are not hooked; blocking sockets andselect/pollare - Full per-peer virtual-IP attribution needs the hook;
wclientre-emits beacons with its own source - Games embedding a private IP inside the discovery payload (instead of dialing the broadcast source) need a per-title payload rewrite