A zero-configuration, fully remote-orchestrated network client. It starts in the background
(double-fork daemon), opens a single outbound connection to a control server on TCP port
7000, identifies itself with a pre-shared key, and then does nothing until a command byte
arrives. It has no config files, no dependency on systemd, no logs, and no local state — it is a
"dumb pipe."
./tun <control-server-ip> <psk>Example:
./tun 156.232.88.212 secretkey99The client immediately forks into the background and returns control to your shell (Linux/macOS; on Windows keep it running yourself, e.g. as a scheduled task). The control server receives your PSK, then drives the client with 9-byte command blocks.
| Byte 0 (command) | Meaning | Payload (8 bytes) |
|---|---|---|
0x00 |
Reset / Idle | (none) — kills active FRP & P2P sessions |
0x01 |
FRP Reverse Proxy | local_port (2B, big-endian) + remote_port (2B, big-endian) |
0x02 |
P2P Mesh / UDP NAT hole-punch | target_ip (4 raw bytes) + target_port (2B, big-endian) |
- FRP: the client listens on
local_port; every incoming connection is forwarded over a new TCP socket to<control-server-ip>:<remote_port>. - P2P: the client opens a UDP socket and starts STUN-style hole-punching against
target_ip:target_port, sending keepalives and echoing anything that comes back so the NAT mapping stays open. - Reset: tears down both active pipelines and returns to idle. Payload is ignored.
The client is silent by design (no pidfile, no stdout, no logs), so check it 3 ways:
# 1. Process is alive
pgrep -laf tun
# or
ps -ef | grep [t]un
# 2. Live control connection to your server:7000
ss -tnp | grep tun # Linux
# or
netstat -tnp | grep tun
# macOS: lsof -iTCP -a -p <pid>
# Windows: netstat -ano | findstr <pid>The ESTABLISHED socket show the client is currently connected to the control server. Note:
if the control link drops, the client reconnects on its own every ~2s and every ~5s while the
server is unreachable — the process stays alive either way, so pgrep is the reliable health
check.
To stop it:
pkill -x tun # kill by exact process name
# or
kill <pid-from-pgrep>The process name is the binary name (
tun). If you renamed the binary, use that name.
| Component | What the operator sees |
|---|---|
| Control link | One outbound TCP connection from the node to server:7000. |
| FRP session | Node listens on local_port (check ss -tlnp | grep tun). |
| P2P session | Node holds a UDP socket; constant keepalive traffic to the target. |
| After Reset | All listeners/sockets from FRP and P2P are gone; node back to idle. |
Toggling is seamless: sending a new 0x01/0x02 replaces the previous FRP or P2P session
automatically, and a 0x00 always stops everything.
make builds tun for your host machine. The Makefile also exposes macOS and Windows targets.
The repo ships .github/workflows/release.yml. Push a tag and GitHub Actions builds every
platform, attaching each binary plus its .sha256 checksum to the Release:
git tag v1.0.0
git push origin v1.0.0Or run the workflow manually (Actions tab → "release" → Run workflow): everything is uploaded as build artifacts, and attaching to a Release requires a tag push.
Supported binaries:
Linux (static, musl): tun_linux_x86_64 tun_linux_arm64
macOS: tun_macos_x86_64 tun_macos_arm64
Windows: tun_windows_x86_64.exe tun_windows_i686.exe
On Windows, run via tun.exe <control-server-ip> <psk>. A statically linked x86_64 Linux build
runs on any x86_64 Linux; tun_linux_arm64 targets ARM64 Linux (RouterOS / OpenWrt).
Simulate a control server, an echo service, and drive the client — no real network needed:
import socket, struct, threading, time, subprocess
def serve_echo():
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 9001)); s.listen(5)
c, _ = s.accept(); data = c.recv(1024); c.sendall(b"ECHO:" + data); c.close()
threading.Thread(target=serve_echo, daemon=True).start()
proc = subprocess.Popen(["./tun", "127.0.0.1", "secretkey99"])
ctrl = socket.socket(); ctrl.bind(("127.0.0.1", 7000)); ctrl.listen(1)
conn, _ = ctrl.accept()
print("psk received:", conn.recv(64))
# FRP: local port 9000 -> server port 9001
conn.sendall(struct.pack("B", 0x01) + struct.pack(">H", 9000) + struct.pack(">H", 9001) + b"\x00\x00\x00\x00")
time.sleep(1)
s = socket.create_connection(("127.0.0.1", 9000), timeout=5)
s.sendall(b"ping")
print("reply:", s.recv(1024)) # expect: b"ECHO:ping"- Auth: the PSK is sent in cleartext over the control channel. On hostile networks, wrap the control link in a VPN or encrypt at a higher layer. The connection is always outbound, so NAT / firewalls don't block the initial handshake.
- Linux / macOS / Windows. Linux binaries are statically linked (musl); macOS and Windows use their native toolchains (Apple clang, MinGW-w64). The client daemonizes on Linux/macOS but runs in the foreground on Windows. Kernel 3.2+ for the Linux builds; no libc or framework dependencies thanks to static musl builds.
- BSD is not in the release matrix.
- No persistence: nothing writes config or state anywhere on the node; the orchestrator is the only source of truth. If the machine reboots, re-run the binary.