Profile manual
The command line
On this page
pipe runs an engine and controls one that runs: what surge-cli is to
Surge. Each command asks the engine's control API,
so it works the same against the Mac app and against pipe run.
pipe status
pipe mode global
pipe rule match example.com
pipe profile
pipe watch speed
pipe <command> --help says what a command takes. Under the name tailscale
it is Tailscale's command line.
Reaching the engine#
On the same machine no password is needed. An engine serves its API on
a Unix socket that only your user can reach, and pipe looks for it in the
usual place:
| Host | Socket |
|---|---|
| Mac | ~/Library/Application Support/Pipe/control.sock |
| Linux | $XDG_RUNTIME_DIR/pipe/control.sock, or ~/.local/state/pipe/control.sock |
--socket PATH, or PIPE_CONTROL_SOCKET in the environment, says
otherwise. A home directory too long for a socket's path moves it to
/tmp/pipe-<uid>/control.sock. The socket's directory is yours alone, its
mode is 0600, and both ends check with the system who the other is: the
engine answers its own user, and pipe speaks to no one else's socket.
On another machine, give the address of its
external-controller-access or http-api:
pipe --remote 192.168.1.5:6170 status
pipe --remote '[2001:db8::5]:6170' status
The password comes from the first line of stdin with --password-stdin,
from PIPE_CLI_PASSWORD, or else from a prompt on the terminal that does
not echo it. It is never an argument, where other users' process lists
would show it. Wrong passwords are answered ever more slowly, and ten in a
row refuse your address for 15 minutes.
Without TLS the password and what comes back cross the network as they are,
and pipe says so. With http-api-tls on, add --tls; --ca FILE trusts
that engine's certificate authority instead of what the system trusts.
pipe dump ca, on the engine's machine, prints it.
pipe treats what an engine says as another program's words, and on
--remote possibly an impostor's. Answers are read up to 32 MiB and two
minutes, and control characters are taken out before anything reaches your
terminal.
Output#
Commands print for a person. --raw prints the engine's JSON for scripts,
one document for each reading of a command that goes on:
pipe --raw summary | jq .traffic.active
A command that fails says why on stderr and exits with 1.
Checking a profile#
pipe check profile.conf
says what is wrong with a profile, and exits with 1 when it cannot be run. It needs no engine.
Running an engine#
pipe run profile.conf --pool pool.json --data-dir data
pipe run serves the control socket beside its listeners.
External proxy programs run once approved:
pipe check shows each one's identity, and --allow-program IDENTITY on
pipe run approves that exact command. There is no other way to do it from
the command line: neither the control socket nor the HTTP API can.
--control-socket PATH puts it elsewhere and fails if it cannot;
--no-control-socket serves none. With --data-dir,
what is carried is kept there. A running engine has no
system proxy and no Enhanced Mode: those are the app's.
Commands#
The engine#
| Command | What it does |
|---|---|
status |
Whether it runs, its mode, what it carried, its features |
summary |
What it carried, how connections ended, the hosts that carried most |
version |
This program's version and the engine's |
environment |
The machine, the network, the region, how long it has run |
reload |
The profile again, from where it is kept |
switch-profile NAME |
Another profile |
stop |
Stop the engine |
set KEY VALUE |
mode, global-policy, log-level, or a feature: set mitm off |
set-log-level LEVEL |
verbose, info, notify, warning or error |
Routing#
| Command | What it does |
|---|---|
mode [rule|global|direct] |
Show the outbound mode, or change it |
global-policy [POLICY] |
Show what Global mode goes through, or change it |
policy-group [GROUP [MEMBER]] |
List the groups, show what one chose, or choose for it; --test tests it now |
rule list |
The rules in the order they are tried |
rule match HOST |
The rule that decides for a destination |
rule explain HOST |
How every rule read it |
rule temp KEY=VALUE… |
Add a temporary rule; alone, list them |
kill ID · kill --all |
End a connection, or all of them |
The profile#
| Command | What it does |
|---|---|
profile |
The profile in effect, with passwords, keys and tokens taken out; --sensitive keeps them |
profile list |
The profiles there are |
profile diff FILE |
How it differs from a file, secrets taken out of both |
module · module enable|disable NAME |
List the modules, or turn one on or off |
feature [NAME [on|off]] |
List the features, or switch one: mitm, capture, rewrite, scripting, system_proxy, enhanced_mode |
managed-profile [update] |
Where a managed profile comes from; update fetches it again |
external-resource [update KEY|all] |
The rule lists and other downloads; update fetches them again |
A feature switched here lasts until the engine stops, whatever a reload brings.
Testing#
| Command | What it does |
|---|---|
test [POLICY…] |
Each policy's latency, or all of them; --url tests with that URL, --udp whether they carry UDP |
test-policy-bandwidth POLICY |
A download through a policy; --bytes says how much |
dns lookup NAME |
Look a name up as the engine would; --type, --server |
dns trace NAME |
Ask the engine's DNS and the system's side by side: who answered, how, how fast |
http probe URL |
Each phase of a request; --policy or --rules say how it goes |
geoip ADDRESS |
The country and network of an address |
flush dns |
Forget what the DNS cached |
benchmark encryption |
How fast the engine's machine seals with each cipher, on one core |
benchmark rule-matching |
How many decisions a second the rules in effect make, on one core |
diagnostics |
Everything worth sending when something is wrong, without secrets |
dns, http probe and test-policy-bandwidth are network tools, which
the Mac app and iPhone have with Pro.
Watching#
| Command | What it does |
|---|---|
dump summary |
As summary |
dump performance |
Memory, the budget, what the tables hold |
dump rule-usage |
How often each rule decided |
dump virtual-ip |
The fake-IP table |
dump ca |
The engine's certificate, in PEM, for --ca |
watch speed |
Bytes a second, until stopped; --interval, --count |
log watch |
The engine's log as it comes; --kind, --count |
logbook |
What happened to the engine over the last week |
proxy-runtime-status |
Every policy: its kind, its last test, what it says of itself |
script list · script run NAME |
The profile's scripts, and running one by hand |
script-log |
What scripts logged |
device |
The devices of the local network: what each carried, and how many connections it has open |
security ban |
Who is refused; add ADDRESS [--seconds N], lift ADDRESS|--all |
Tailscale's command line#
pipe tailscale is Tailscale's own command line, over the
tailnets signed in to in the app. So that someone who
leaves Tailscale's Mac client keeps their habits and their scripts, the same
program answers to the name tailscale. On a Mac, Settings › Command Line
installs both, and leaves another program of that name where it is.
tailscale status --json | jq -r .Self.DNSName
tailscale ip -4
tailscale up --exit-node=mac-mini
Flags read as Tailscale's do: -json is --json, and a switch takes
--accept-dns=false. What reaches the engine is what Reaching the
engine says, --remote and --socket included. An
engine has to be running, on the Mac the app's: only version does without
one. Tailscale itself is Pro, so these commands are too.
Which account. Commands act on the one account there is, or the only one
that is on. With several on, which Tailscale's own client cannot do, say which
with --account, by its tailnet, its user or its ID:
tailscale status --account work.example
Output. What scripts parse is Tailscale's: the lines of ip, the table of
status, and --json on status, whois, serve status and lock status.
--raw prints the engine's own JSON instead. Two things in status --json
differ from Tailscale's for want of what the engine says of a peer: Peer is
keyed by stable ID rather than by node key, and PublicKey, Tags, TxBytes
and RxBytes are left out rather than said to be nothing. UserID is given
for the user's own devices alone, and Version is Pipe's.
| Command | What it does |
|---|---|
status |
The tailnet's devices; --json, --self=false, --peers=false, --active |
ip |
This device's addresses, or a device's: -1, -4, -6 |
ping HOST |
How a device is reached; -c, --timeout, --until-direct=false, --icmp for an ICMP echo through the tailnet |
up |
Turn the account on, signing in when it takes that; --auth-key, --login-server, --force-reauth, --reset, --timeout, --json, and the flags of set |
down |
Turn the account off; it stays signed in |
set |
--exit-node, --exit-node-allow-lan-access, --accept-dns, --accept-routes, --advertise-exit-node, --advertise-routes, --ssh, --shields-up, --hostname, --advertise-connector |
login |
Sign in to another account; --auth-key, --login-server, --hostname, --timeout |
logout |
Sign out: the node leaves the tailnet and its keys are deleted |
switch ACCOUNT · switch --list |
Leave that account alone on, or list them |
exit-node list · exit-node suggest |
The exit nodes, or the nearest online; --filter=COUNTRY |
service list |
The Tailscale Services this device may reach; --json |
whoami |
Who this device is signed in as; --json |
wait |
Wait until the tailnet is up; --timeout |
appc-routes |
The routes this device found as an app connector |
netcheck |
The network, as a node sees it; --format=json |
whois ADDRESS |
Which device and user are at an address; --json |
nc HOST PORT |
One conversation with a port; --wait |
dns status · dns query NAME [a|aaaa] |
The tailnet's DNS, and a name looked up |
cert [DOMAIN] |
A certificate and its key; --cert-file, --key-file, - for stdout |
serve TARGET · serve status · serve reset |
Serve something local to the tailnet |
funnel TARGET · funnel PORT on|off · funnel status · funnel reset |
The same, to the internet |
file cp FILE… HOST: · file cp --targets |
Send files |
file get FOLDER |
Move what was received there; --wait, --loop, --conflict=skip|overwrite|rename |
lock · lock status · lock log · lock sign · lock add · lock remove · lock init --confirm · lock disable |
Tailnet Lock, on a node whose key it trusts |
drive share NAME PATH · drive unshare NAME · drive rename NAME NEW · drive list |
Folders shared with the tailnet |
metrics · metrics print · metrics write FILE |
The node's numbers |
version |
This program's version first; --json, --daemon |
ssh [USER@]HOST [ARGS] |
The system's ssh, at the device's tailnet address |
Serving. A target is a port (3000), an address on this machine
(http://127.0.0.1:3000, localhost:3000), an absolute path, or text:Hello;
off takes one away. --https=PORT, --http=PORT, --tcp=PORT and
--tls-terminated-tcp=PORT say where, HTTPS on 443 unless told, and
--set-path=/api under what. As in Tailscale's, a command stays until
stopped and then puts things back, and --bg leaves them on. A command killed
outright, which has no chance to, leaves them on too: serve reset takes
everything off.
What is different here#
switchleaves one account on, as Tailscale has it, and lets go of an exit node of an account it turns off. The app can have several on at once.--exit-nodeis one device, orauto:any, which follows the best there is.auto:of anything else is refused.exit-node suggestis the exit node online that answers a ping soonest. Mullvad's take no ping, and are suggested when nothing else is there.ncis one conversation, not a stream: stdin is sent once, up to 64 KiB, and what comes back in up to 10 seconds is written out. It is noProxyCommand, andsshneeds none here.sshruns the system'ssshat the device's address, which the app's utun carries while the account is on. It does not manageknown_hostsas Tailscale's does.file cpsends files by path, so stdin (-) and--nameare refused.file cpandfile getwork where the engine runs, not over--remote.netcheck --format=jsonnames regions, where Tailscale numbers them. There is no--every: it reads the node's last report.metricsprints Pipe's numbers under Pipe's names: they are not Tailscale's, and a dashboard made for those needs changing.dns querygives addresses, so it takesaandaaaaalone.lockhas nolocal-disable, since one node trusting what its tailnet's keys did not sign is what a lock exists to stop, and norevoke-keys:lock removetakes a stolen key off while another trusted key can sign.initneeds--confirm, and makes one disablement secret.- Flags that cannot be done are refused in words:
--advertise-tags(tags come from the auth key),--snat-subnet-routes=false,--qr,--tsmp,--peerapi,--min-validityand the like.--operatorand--accept-riskchange nothing here, and are taken. Commands Tailscale has and this does not, such asweb,configureandupdate, are unknown to it.
An auth key on a command line shows in other users' process lists:
--auth-key=file:/a/path reads it from a file. A login URL goes to stderr,
and only to whoever asked, since whoever opens it can sign the node in to
their own tailnet.
Cloudflare Tunnel#
pipe cloudflared runs a Cloudflare tunnel in the foreground, Pipe being its
connector, as cloudflared runs one: a local service answers at a hostname on
Cloudflare with no port opened. A quick tunnel, on a name of its own under
trycloudflare.com and with no account:
pipe cloudflared tunnel --url http://localhost:8080
A tunnel made in Cloudflare's dashboard, by its token, which serves what the
dashboard says. Given as TUNNEL_TOKEN, other users' process lists do not
show it:
TUNNEL_TOKEN=eyJhIjoi… pipe cloudflared tunnel run
It says each connection as it registers or goes down, and each configuration
the dashboard pushes; Ctrl-C unregisters it from Cloudflare and ends it.
--protocol quic or --protocol http2 holds it to one; by default it is QUIC,
and HTTP/2 where UDP does not get through. It needs no engine, and goes to
Cloudflare directly whatever a profile says. A quick tunnel has no uptime
guarantee, and Cloudflare's terms of use apply.
Temporary rules#
A temporary rule decides ahead of the profile's rules, in Rule mode, and is gone when its time is up, when you remove it, or when the engine stops.
pipe rule temp domain-suffix=example.com policy=DIRECT ttl=600
pipe rule temp ip-cidr=192.0.2.0/24 dest-port=443 policy=REJECT
pipe rule temp
pipe rule temp --remove 1
pipe rule temp --clear
| Attribute | Matches |
|---|---|
domain |
Exactly this name |
domain-suffix |
This name, and those under it |
domain-keyword |
A name that holds this |
ip-cidr |
An address in this range; a name is not looked up for it |
dest-port |
This destination port |
process |
This process's name |
A connection has to match every attribute given. policy is a policy of
the profile, or REJECT. ttl is in seconds: an hour unless it says, a
week at most. A reload keeps the rules whose policy the new profile has. At
most 256 are in effect.
Statistics#
Where the app or pipe run --data-dir keeps them, the engine counts what
it carries per day: by app, by host, by device and by policy, the current
month and the one before. They outlive the engine. A day keeps the hosts,
apps and devices that carried most, and what is let go of stays in the
day's sums.
Helpers and agents inside an app's bundle count as the app. A process that
runs from elsewhere counts as itself, whoever started it: curl in Terminal
is curl. A connection from another device counts under that
device's name, or else its hardware address or its
address.
GET /traffic/stats?by=app&month=2026-11 gives them as JSON, and the
web dashboard charts them.