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.

View /llm.txt (raw) For humans → quickstart Direct downloads
# 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