LLM instructions for tnl.voxent.io
Hand this URL (https://tnl.voxent.io/llm) to any LLM you're asking to install or operate the tunnel client. The text below is the same as /llm.txt — feed either to the model. The raw .txt is deterministic and parseable; this page is the human-readable mirror.
# 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=<code> \
REMOTETUNNEL_CLIENT_NAME=<hostname> \
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://<machine>.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 <N> $(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=<uuid>, 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: <your-subdomain>.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-<os>-<arch>[.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=<unique> 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-<name> -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.
- 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)
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