Skip to content

Reference ​

Concepts ​

TermMeaning
HostThe browser machine. It runs the daemon and owns DNS, the HTTPS edge, the local CA, and the token store
NodeThe CLI's word for a host daemon address; a node profile is a named list of addresses. No cluster or mesh
LeafA dev environment that runs testd run, connects outbound, registers names, and serves the app
OperatorThe human on the host who approves requests and manages tokens
Claim or registrationA live name-to-target mapping owned by one leaf connection
TokenA bearer credential issued by the host and persisted on both sides
GrantThe exact names a token may publish
Operator secretThe bootstrap credential that can register any valid name and is not an issued token
ProfileA named configuration; node profiles choose hosts, token profiles choose credentials

A token and its grants are durable permission. A claim is an ephemeral route tied to a connection. A token is the key, its grants are the doors it may open, and a claim is a door currently held open by a leaf.

Command line ​

text
testd <command> [options]
CommandPurpose
initPrepare this machine as a host; idempotent
runRegister names, optionally wrap a command, deregister on exit
deregisterRelease names without stopping a process
hostRun the host daemon in the foreground
dashboardPrint the authenticated local dashboard URL
urlHandle a testd:// action
doctorCheck host setup; optionally repair
shareStart, stop, or list tunnels for registered names
requestsList waiting access requests
approveApprove one request and issue its token
denyDeny one request
tokensList issued tokens, or revoke one
export-caddyfileWrite the managed Caddyfile for an external front
uninstallRemove the OS resolver route and trusted root
setup-hosts-helperInstall the hosts resolver helper and sudoers rule

Global options: --help, --version.

init ​

text
testd init [--caddyfile <path>] [-y|--yes] [--resolver-now]
OptionMeaning
--caddyfile <path>Global Caddyfile to import; empty string disables the external front. Env: TESTD_CADDYFILE
-y, --yesAccept defaults and allow privileged steps without prompting. Env: TESTD_YES
--resolver-nowConfigure the OS resolver now instead of lazily on the first remote client

Creates config.toml, the root CA and intermediate, and the operator secret if missing. Re-running keeps the existing secret and unknown config keys. Without a terminal and without --yes, it does not touch files outside the config directory.

run ​

text
testd run <entry>... [--force] [--host <addr>] [--hosts <list>]
                      [--token-profile <name>] [--node-profile <name>]
                      [-- <command>...]

Entries are :port, name:port, or host:name:port. A trailing .test on a name is stripped. Without a command, the registration is held until Ctrl+C. With a command, the registration follows the child's lifetime, and the exit status is the child's.

OptionMeaning
--forceTake names owned by another connection
--host <addr>One host endpoint. Env: TESTD_HOST
--hosts <list>Comma-separated host endpoints. Env: TESTD_HOSTS
--token-profile <name>Credential profile. Env: TESTD_TOKEN_PROFILE
--node-profile <name>Node profile from leaf.toml. Env: TESTD_PROFILE

--host and --hosts entries without a port get :7737. Addresses from the environment or leaf.toml must include a port.

deregister ​

text
testd deregister <domain>... [--host <addr>] [--hosts <list>]
                             [--token-profile <name>] [--node-profile <name>]

Connects to the host and releases the named registrations. The token and its grants remain. Accepts the same host and profile options as run.

host ​

text
testd host

Runs the daemon in the foreground. A second instance fails on the instance lock. Stop it with Ctrl+C or the service manager.

dashboard ​

text
testd dashboard

Prints the loopback dashboard URL including the per-run token fragment. Requires a running daemon.

url ​

text
testd url <url>

Accepts testd://fix/resolver and testd://fix/certificate. Forwards to the running daemon when possible, otherwise performs the repair itself. Without a terminal it uses a native authorization dialog.

doctor ​

text
testd doctor [--fix] [--json]
OptionMeaning
--fixConfigure the resolver, write a missing secret, add the Caddyfile import, reload Caddy, and install or reinstall the root
--jsonPrint { ok, exit_code, checks[] } with name, ok, detail, and fix per check

Exit codes:

CodeMeaning
0Ready
1Configuration or missing state
2Resolver or name resolution
3PKI, certificate trust, or HTTPS edge

Other CLI failures exit 1.

share ​

text
testd share <name> [--provider <name>]
testd share --list
testd share --stop <name>

Providers: cloudflared (default), tailscale, ngrok. The daemon owns the tunnel. Requires a running daemon.

VariableMeaning
TESTD_TUNNEL_PROVIDERDefault provider when --provider is omitted
TESTD_TUNNELPath to a provider binary outside PATH

requests, approve, deny, tokens ​

text
testd requests
testd approve <id>
testd deny <id>
testd tokens
testd tokens --revoke <id>

All require a running daemon. requests prints the id, label, requested names, address, and age. The label is user@host:project when the requested names carry a project. Requests expire after two minutes. Token secrets are never printed.

export-caddyfile ​

text
testd export-caddyfile

Writes the managed Caddyfile to [host] caddyfile_path and prints the import line to add to the global Caddyfile.

uninstall ​

text
testd uninstall [--purge] [--keep-resolver] [--keep-certificate]
OptionMeaning
--purgeAlso delete the config directory, PKI, tokens, and local state
--keep-resolverLeave the OS resolver route in place
--keep-certificateLeave the trusted root in place

Stop the daemon first. The hosts helper and its sudoers rule are removed whenever they are present. --purge proceeds only when system cleanup succeeds.

setup-hosts-helper ​

text
testd setup-hosts-helper [--prefix <path>]

Installs {prefix}/libexec/testd-hosts-helper and /etc/sudoers.d/testd-hosts-helper for the invoking user, then validates the rule. Default prefix: /usr/local. Not supported on Windows.

Environment variables ​

VariableUsed byMeaning
TESTD_HOSTrun, deregister, trayOne host endpoint, host:port
TESTD_HOSTSrun, deregister, trayComma-separated host:port endpoints
TESTD_PROFILErun, deregisterNode profile name
TESTD_TOKENrun, deregisterCredential override; skips the credential file
TESTD_TOKEN_PROFILErun, deregisterToken profile name
TESTD_CADDYFILEinitGlobal Caddyfile path
TESTD_YESinitSame as --yes
TESTD_TUNNEL_PROVIDERhostDefault tunnel provider
TESTD_TUNNELhostProvider binary path
TESTD_REGISTER_SCHEMETray dev buildsRegister the testd:// scheme outside a release build

Host configuration ​

Path: ~/.config/testd/config.toml; on Windows, %APPDATA%\testd\config.toml. Mode 0600.

KeyDefaultMeaning
resolverdnsdns for the built-in server or hosts for a managed hosts block
listen_addr0.0.0.0:7737Leaf intake
edge_addr0.0.0.0:443HTTPS edge when edge_enabled is true
edge_enabledtrueStart the built-in TLS listener; the plain listener always runs
http_addr127.0.0.1:7740Plain HTTP routing for an external front; always on
answer_addr127.0.0.1A-record address returned for registered names
allow_remote_targetsfalsePermit a remote leaf to name a target other than its own address
trust_forwarded_forfalseTrust inbound X-Forwarded-For from a configured front
secretgeneratedOperator secret; bootstrap credential
pki_dir~/.config/testd/pkiRoot CA, intermediate, and backups
caddyfile_path~/.config/testd/CaddyfileManaged Caddyfile
caddy_config_pathdetectedGlobal Caddyfile to inspect and reload
hosts_pathplatform hosts fileFile edited by the hosts resolver
hosts_helper_path/usr/local/libexec/testd-hosts-helperPrivileged hosts helper

The DNS server always binds loopback and answers testd.test plus every registered name. It is not a LAN DNS service. From another machine, reach a name through testd share, not by pointing its resolver at the host.

The daemon reads the operator secret at startup; restart it after changing the value. Existing issued tokens are unaffected.

Leaf configuration ​

Path: ~/.config/testd/leaf.toml; on Windows, %APPDATA%\testd\leaf.toml.

toml
default_profile = 'home'

[profiles.home]
hosts = ['192.168.1.5:7737', '192.168.1.6:7737']

Contains addresses only, never credentials.

Project configuration: the nearest .testd.toml found upward from the working directory.

toml
token_profile = 'myproject'

Safe to commit. No secrets.

Files and permissions ​

Host, under the config directory:

PathContentsMode
config.tomlHost settings and operator secret0600
pki/root.crtTrust anchor0644
pki/root.keyRoot CA private keyprivate
pki/node.crt, pki/node.keyHost intermediateprivate key private
pki/backups/Previous intermediatesprivate
tokens.jsonIssued tokens and granted names, including secrets0600
control.jsonPer-run loopback control tokenuser-private
launches.jsonQuick launch definitionsuser-private
CaddyfileManaged external-front configplain

Leaf:

PathContents
leaf.tomlNode profiles
credentials/<profile>Bearer token for that profile, mode 0600

The host's token store is the source of truth. A leaf's credential file is a copy, not a second grant database.

Listeners and ports ​

ListenerDefaultConfig keyNotes
Leaf intake0.0.0.0:7737listen_addrPlain TCP, bearer token
Dashboard127.0.0.1:7737follows listen_addr portLoopback only
HTTPS edge0.0.0.0:443edge_addrOnly when edge_enabled; needs privilege on Linux
Plain HTTP routing127.0.0.1:7740http_addrAlways on; used by external fronts
DNS127.0.0.1:7753fixedLinux and macOS
DNS127.0.0.1:53fixedWindows

The control channel uses the dashboard address with a per-run token from control.json.

Names, targets, and limits ​

  • The TLD is fixed to .test. It is appended by the host; a trailing .test in an entry is stripped.
  • Names are <service>.<project>, stored and displayed without the TLD.
  • Labels allow ASCII letters, digits, and hyphens; they may not start or end with a hyphen.
  • Maximum label: 63 bytes. Maximum full name: 253 bytes.
  • testd.test is reserved for the dashboard.
  • Targets: bare port, host:port, ip:port, or [ipv6]:port. Maximum 512 bytes. Port 0 is invalid. The run entry syntax splits host:name:port on colons, so an IPv6 host cannot be used in a run entry; use a host name. Quick launches accept a full IPv6 target.
  • A bare port is completed with the leaf's connection address.

Operational limits:

LimitValue
Intake connections256 simultaneous
NDJSON frame64 KiB
HTTP request head32 KiB
HTTP request body256 KiB
HTTP request timeout10 seconds
HTTP response write timeout5 seconds
Edge connections512 total, 64 per source address
Edge certificate cache1024 entries
Pending access requests32
Access request lifetime120 seconds
Control request line4096 bytes
Quick launch log500 lines in memory
Leaf certificate24 hours
Intermediate certificate1 year
Root certificate10 years

There is no telemetry, usage reporting, or name reporting.

Dashboard and HTTP API ​

Base URL: http://127.0.0.1:7737/. The Host header must be localhost, 127.0.0.1, or [::1]. Only GET and POST are accepted.

A bare URL is read-only. Mutations and /api/local/* need the per-run token from control.json, sent as:

text
X-Testd-Token: <token>

Every POST also needs:

text
X-Testd-Request: 1

The token is normally supplied by the dashboard URL fragment (#token=...), which the page reads and sends as a header. The fragment is not sent in the HTTP request.

MethodPathAuthPurpose
GET/NoneDashboard, read-only without a token
GET/api/statusNoneNames, targets, projects, advisories, shares
GET/ca.crtNoneRoot certificate PEM
GET/ok.svgNoneTrivial liveness image
GET/api/local/stateTokenLive dashboard snapshot: names, shares, launches, requests, tokens
GET/api/local/eventsTokenServer-sent snapshot stream for live dashboard updates
GET/api/local/launchesTokenQuick launch definitions and running state
POST/api/local/launches/saveTokenCreate or update a definition
POST/api/local/launches/deleteTokenDelete a definition
POST/api/local/launches/startTokenStart a definition
POST/api/local/launches/stopTokenStop a running definition
POST/api/local/launches/rebindTokenChange one running entry's target
GET/api/local/requestsTokenPending access requests
POST/api/local/requests/approveTokenApprove a request
POST/api/local/requests/denyTokenDeny a request
GET/api/local/tokensTokenIssued tokens
POST/api/local/tokens/revokeTokenRevoke a token
GET/api/local/certificateTokenTrust state, fingerprint, expiry
POST/api/local/certificate/installTokenInstall system trust
POST/api/local/certificate/removeTokenRemove system trust
POST/stop/<name>TokenStop a name's leaf; keep its grant
POST/delete/<name>TokenDelete one name's grant and stop it
POST/unshare/<name>TokenStop a name's tunnel

All /api/local/* paths require the token even for GET. The dashboard and API are not served by the edge.

The dashboard keeps one /api/local/events request open and replaces its cards whenever a snapshot arrives. A snapshot is sent on connect, then whenever names, shares, launches, access requests, or tokens change; a comment frame is sent every 15 seconds to detect a dead connection. The browser reads the stream with fetch, so the token stays in the X-Testd-Token header instead of a URL.

Tunnels ​

ProviderExposureStart command behaviorURL shape
cloudflaredPubliccloudflared tunnel --no-autoupdate --url http://<target>https://*.trycloudflare.com
ngrokPublicngrok http <target>https://*.ngrok-free.app, *.ngrok-free.dev, *.ngrok.io, *.ngrok.app
tailscaleTailnettailscale serve --bg http://<edge plain address>https://*.ts.net

A provider has 30 seconds to report its URL. tailscale requires the plain HTTP routing listener and refuses to overwrite existing serve configuration on port 443. One share per registered name.

Platform notes ​

  • Linux: DNS on UDP 7753; port 443 needs CAP_NET_BIND_SERVICE or a high edge_addr. Use packaging/systemd/testd@.service for the capability, or packaging/systemd/testd.service with a high port.
  • macOS: DNS on UDP 7753; launchd template in packaging/launchd/. The root is installed in the system keychain through the setup helper.
  • Windows: DNS on UDP 53; config under %APPDATA%\testd. NRPT rules route .test to the local server. The hosts resolver is not supported. A stale answer may need ipconfig /flushdns.
  • Firefox: may use its own trust store; import the root separately if Chromium-based browsers and the system store accept it but Firefox does not.
  • Caddy: optional. The built-in edge needs no external front.

Troubleshooting by layer ​

CheckCommand or observation
Configuration presenttestd doctor; first check is config
Operator secret setdoctor check secret
Resolver routedoctor check resolver
Name resolves through the OSdoctor check resolution
PKI can issue a leafdoctor check pki, with the built-in edge enabled
Root trusteddoctor check certificate, with the built-in edge enabled
Edge listening and trusteddoctor check edge
Optional Caddy wiringdoctor checks caddy, caddyfile, reload
Live names and targetscurl http://127.0.0.1:7737/api/status
Pending requeststestd requests
Issued tokenstestd tokens
Shares and URLstestd share --list

ISC licensed. No account, no telemetry.