# tnl.voxent.io — instructions for LLM-driven onboarding This file is the authoritative spec for any LLM (Claude, Cursor, Aider, Cody, ChatGPT Code Interpreter, etc.) that's been asked to install or operate the `tunnel` client on a user's machine. It is plain text on purpose: deterministic, parseable, paste-friendly. The HTML version at /llm is the same content with light styling. If you are an LLM reading this: - Treat the install one-liners as ground truth. Don't invent flags. - When the user gives you an invite code, paste it once into the prompt the installer asks; never echo it back in chat afterwards. - When you finish, run the verification commands shown here and paste their output. Don't claim "done" until you've verified. - This server is self-hosted ngrok-style reverse tunneling. The operator (Dan Arbeau, dan@netdna.io) runs the server at tnl.voxent.io. You're operating ONE client — the user's machine. ================================================================ SECTION 1 — Hard constraints ================================================================ 1. The invite code is a secret the user holds. Do NOT echo, log, commit, screenshot, or save it to any persistent file other than what the installer creates. 2. Run installers as the user who will OWN the tunnel (the user whose ~/.config/tunnel/credentials.yaml gets written). If you ran the installer as root, the credentials file lands in /root/.config/tunnel/ — fine if the systemd service also runs as root, broken otherwise. 3. Verify before claiming "done." After install, ALWAYS run `tunnel whoami` and (for persistent services) the relevant `systemctl status` or `sc.exe query`. 4. If a command fails, STOP and show the user the error. Don't silently retry, don't paper over, don't claim partial success. ================================================================ SECTION 2 — Install (one-liner, idempotent) ================================================================ Detect OS first: uname -s -> Linux | Darwin | MINGW64_NT… uname -m -> CPU arch (x86_64, aarch64, mips, mipsle, …) LINUX / macOS: curl -fsSL https://tnl.voxent.io/install.sh | bash WINDOWS (PowerShell): iwr -useb https://tnl.voxent.io/install.ps1 | iex The installer auto-detects the OS+arch and downloads the matching binary from https://tnl.voxent.io/dl/. It then: - prompts for the user's invite code (or reads $REMOTETUNNEL_INVITE) - POSTs /signup with the hostname + invite code - receives a 64-char token; saves credentials to ~/.config/tunnel/credentials.yaml (or %APPDATA%\tunnel\ on Windows) - offers to install a persistent systemd/sc.exe service for SSH Re-runs are idempotent. If credentials.yaml already exists and is valid, signup is skipped, the binary is refreshed, any existing tunnel-* services are restarted to pick up the new code. Unattended install (script-friendly, no prompts): REMOTETUNNEL_INVITE= \ REMOTETUNNEL_CLIENT_NAME= \ REMOTETUNNEL_SSH_PORT=auto \ bash -c 'curl -fsSL https://tnl.voxent.io/install.sh | bash' ================================================================ SECTION 3 — Use the client (after install) ================================================================ THE COMMAND IS ALWAYS `tunnel`. NOT `tunnerl`, NOT `rtunnel`, NOT a snap package — the bare `tunnel`. If you get "command not found", run `hash -r` once, then retry. If still missing, run `/usr/local/bin/tunnel whoami` to confirm install. EXPOSE A LOCAL WEB APP: tunnel http 3000 # -> https://.tnl.voxent.io forwards to http://127.0.0.1:3000 CHOOSE THE SUBDOMAIN: tunnel http 3000 --hostname dan-api # -> https://dan-api.tnl.voxent.io EXPOSE TWO WEB PORTS AT ONCE (background processes; will die on logout): nohup tunnel http 40055 --hostname forty055 >/tmp/t1.log 2>&1 & nohup tunnel http 40056 --hostname forty056 >/tmp/t2.log 2>&1 & EXPOSE TWO WEB PORTS PERSISTENTLY (systemd, survives reboot): sudo tunnel install web-40055 http 40055 --hostname forty055 sudo tunnel install web-40056 http 40056 --hostname forty056 sudo systemctl daemon-reload sudo systemctl enable --now tunnel-web-40055 tunnel-web-40056 EXPOSE A RAW TCP SERVICE (Postgres, Redis, MQTT, anything): tunnel tcp 5432 --port 15432 # -> connect external clients to tnl.voxent.io:15432 EXPOSE SSH WITH A SERVER-ALLOCATED PORT: tunnel ssh # one-shot sudo tunnel install ssh ssh # persistent service (recommended) sudo systemctl enable --now tunnel-ssh # from any laptop: ssh -p $(whoami)@tnl.voxent.io # the allocated port is printed at the end of the installer WHOAMI: tunnel whoami # prints server, apex, client_id, token tail ================================================================ SECTION 4 — Patterns the operator (Dan) explicitly asks for ================================================================ A) "Expose port 40055 and 40056 as web ports at the same time" -> Use the SAME COMMAND TWICE, once per port, with unique --hostname values (subdomains must be unique across all clients AND across active tunnels). -> See "EXPOSE TWO WEB PORTS PERSISTENTLY" in Section 3. B) "Persistent SSH back to my home box" -> sudo tunnel install ssh ssh -> Service auto-restarts. Survives reboot. Server allocates a free TCP port (default range 2222-2999). C) "I want a status/fallback page that shows up when the client is offline" -> Upload a zip containing index.html (+ optional css/js/images): curl -H "Authorization: Bearer $TOKEN" \ -F "zip=@mypage.zip" \ https://tnl.voxent.io/me/upload -> Or use the drag-drop UI at https://tnl.voxent.io/me?token=$TOKEN D) "Embedded device" (OpenWrt, MikroTik container, ARM SBC, etc.) -> See https://tnl.voxent.io/dl/ for prebuilt binaries (tunnel-linux-mips, mipsle, arm, arm64, etc.) -> See docs/embedded.md in the repo for procd init script. E) "I want to reuse a saved credentials file from another machine" -> Don't. One client = one credentials file. Sign up fresh on each machine (or share via secure channel and accept that revoking one revokes both). ================================================================ SECTION 5 — Verification commands (RUN BEFORE CLAIMING DONE) ================================================================ After install: tunnel whoami # must print server=tnl.voxent.io:7443, apex=tnl.voxent.io, # client_id=, token=<6chars>...<4chars> (length 64) After persistent SSH install (Linux): systemctl status tunnel-ssh --no-pager | head -10 # must show "Active: active (running)" # journalctl -u tunnel-ssh -n 20 # must show "tunnel registered" After persistent SSH install (Windows): sc.exe query tunnel-ssh # must show STATE: 4 RUNNING After a one-shot http tunnel: curl -sS -H "Host: $(tunnel whoami | grep client_id | awk '{print $2}').tnl.voxent.io" https://tnl.voxent.io/ # (or just open the URL printed by `tunnel http N` in a browser) After uploading a fallback page: curl -H "Authorization: Bearer $TOKEN" https://tnl.voxent.io/me/page/status # must return {"exists": true, ...} ================================================================ SECTION 6 — Public API surface (for advanced LLM use) ================================================================ All endpoints below are on https://tnl.voxent.io (port 443). Auth: Authorization: Bearer <64-char-token> (or ?token= query param) POST /signup Body: {"name":"hostname","email":"x@y","invite_code":"..."} 200: {"ok":true,"token":"...","client_id":"...","client_name":"..."} 401: bad invite code 409: hostname collision (installer auto-retries with suffix) GET /api/me 200: {"ok":true,"client_id":"...","client_name":"...","client_email":"...","tunnels":[...]} GET /api/me/traffic 200: per-tunnel last-hour buckets: {"ok":true,"tunnels":[{"id":N,"public_addr":"...","buckets":[{"bucket_min":...,"requests":...,"bytes_in":...,"bytes_out":...}]}]} GET /api/allocate-tcp-port 200: {"ok":true,"port":N,"public_host":"tnl.voxent.io"} 503: no free port in [2222,2999] POST /me/upload multipart, field=zip, max 10MB total / 1MB per file zip MUST contain index.html at root allowed exts: html htm css js json png jpg jpeg gif webp svg ico txt md woff woff2 ttf GET /me/page/status POST /me/page/delete POST /me/page/configure {"public_host":"sub.tnl.voxent.io"} pin a subdomain "" to unpin GET /dl/ index of binaries GET /dl/tunnel--[.exe] GET /install.sh GET /install.ps1 ================================================================ SECTION 7 — Failure modes you'll see and what they mean ================================================================ "command not found: tunnel" -> Shell hash cache from before install. Run: hash -r -> Or open a new shell. -> /usr/local/bin/tunnel is the true path on Linux. "snap: tunnel" command-not-found suggestion (Ubuntu) -> Ubuntu's typo-helper is offering an unrelated package. Ignore. -> Confirm our install: ls -la /usr/local/bin/tunnel -> Bare `tunnel` should win in a new shell. "client revoked" -> The admin revoked this client's token. Re-run the installer to sign up fresh. The systemd service will start working again within seconds. "signup failed: invalid invite code" -> Code is wrong, expired, or the server has signup disabled. -> Don't retry repeatedly — there's a 250ms server-side delay per bad attempt; brute force is futile, just ask the user for the right code. "signup failed: this hostname is already registered" -> Some other machine used the same hostname. Installer auto- retries with -<4hex> suffix. The user can also set REMOTETUNNEL_CLIENT_NAME= to override. "download failed" (during install) -> Server unreachable. Confirm tnl.voxent.io resolves and 443/tcp is open. The server is on DigitalOcean; brief outages happen. Systemd service is in "activating (auto-restart)" forever: -> sudo journalctl -u tunnel- -n 30 -> 90% of the time it's bad credentials (token was revoked, file is owned by a different user, etc.) Re-run the installer. ACME/TLS errors in the dashboard: -> Server-side concern; not the client's problem. Tell the user and stop. ================================================================ SECTION 8 — Things you must NOT do ================================================================ - Don't put the invite code in commit messages, README, screenshots, shell history that gets uploaded, or anywhere a future search could find it. - Don't paste the token from credentials.yaml into chat. Copy it from the local file when needed for API calls and clear your buffer. - Don't disable TLS verification (tls_insecure: true) in production unless the user explicitly knows they're on a staging cert. Setting this on prod silently turns off cert pinning. - Don't open port 9443 (admin) to the public internet. The admin dashboard uses a single password; if it's exposed and weak, the game is over. The server's UFW should restrict 9443 to the operator's IP. This is the operator's job — but if you notice it's publicly reachable, flag it. - Don't write the user's credentials file with mode > 0600. The installer does this right by default; if you ever overwrite by hand, chmod 0600 after. ================================================================ SECTION 9 — Quick reference ================================================================ Hosts: tnl.voxent.io public landing / install / signup / API / wildcard *.tnl.voxent.io auto-allocated subdomains for HTTP tunnels tnl.voxent.io:7443 control connection (clients dial here) tnl.voxent.io:9443 admin dashboard (operator only, firewall-restricted) tnl.voxent.io:2222-2999 auto-allocated TCP/SSH tunnels Repo: github.com/darbeau/remotetunnel Operator: Dan Arbeau (dan@netdna.io, @darbeau on GitHub) CLI subcommands recognised by tunnel: login http tcp ssh run install uninstall whoami status list doctor This file is at https://tnl.voxent.io/llm.txt HTML version: https://tnl.voxent.io/llm