Pipe Manual

Profile manual

The command line

On this page
  1. Reaching the engine
  2. Output
  3. Checking a profile
  4. Running an engine
  5. Commands
  6. Tailscale's command line
  7. Cloudflare Tunnel
  8. Temporary rules
  9. Statistics

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#

  • switch leaves 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-node is one device, or auto:any, which follows the best there is. auto: of anything else is refused.
  • exit-node suggest is the exit node online that answers a ping soonest. Mullvad's take no ping, and are suggested when nothing else is there.
  • nc is 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 no ProxyCommand, and ssh needs none here.
  • ssh runs the system's ssh at the device's address, which the app's utun carries while the account is on. It does not manage known_hosts as Tailscale's does.
  • file cp sends files by path, so stdin (-) and --name are refused. file cp and file get work where the engine runs, not over --remote.
  • netcheck --format=json names regions, where Tailscale numbers them. There is no --every: it reads the node's last report.
  • metrics prints Pipe's numbers under Pipe's names: they are not Tailscale's, and a dashboard made for those needs changing.
  • dns query gives addresses, so it takes a and aaaa alone.
  • lock has no local-disable, since one node trusting what its tailnet's keys did not sign is what a lock exists to stop, and no revoke-keys: lock remove takes a stolen key off while another trusted key can sign. init needs --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-validity and the like. --operator and --accept-risk change nothing here, and are taken. Commands Tailscale has and this does not, such as web, configure and update, 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.