Pipe Manual

Profile manual

Proxies

On this page
  1. The line
  2. Built-in policies
  3. Proxy types
  4. TLS parameters
  5. WebSocket
  6. HTTPUpgrade and gRPC
  7. XHTTP
  8. Multiplexing
  9. Proxy chains
  10. Leaving the device
  11. ip-version
  12. Tests
  13. UDP through a proxy
  14. QUIC through a proxy
  15. Parameters that are read past
  16. Not supported yet
  17. [Keystore]
  18. [WireGuard <name>]
  19. The proxy pool

[Proxy] holds the proxies a profile can send traffic through, one line each.

[Proxy]
Server 1 = trojan, server1.example.net, 443, password=secret
Server 2 = hysteria2, server2.example.net, 443, password=secret, download-bandwidth=200
JP 01 = vless, jp.example.net, 443, username=0233d11c-15a4-47d3-ade3-48ffca0ce119, tls=true, ws=true, ws-path=/ray
SG 01 = ss, sg.example.net, 8388, encrypt-method=2022-blake3-aes-128-gcm, password=MDEyMzQ1Njc4OWFiY2RlZg==
Office = socks5, 10.0.0.5, 1080, alice, secret, underlying-proxy=Server 1
Home = wireguard, section-name=Home

The line#

Name = type, server, port, parameter=value, …

  • The name is what rules and groups call the proxy. It may hold spaces. A proxy and a group cannot share a name, and two proxies with one name are an error.
  • The type says what the proxy speaks, in any case: trojan, hysteria2, …
  • The server and port are where it is: a name or an address, and a port from 1 to 65535. Every type has them but wireguard, tailnet and the other names for built-in policies.
  • Parameters follow as key=value. A parameter given twice counts once: the last one given. A parameter that is not described here is read past, with a note. Only a component without an unquoted = is taken as a username or password given by position.

A line that cannot be read — no server, a port that is not one, a component that is not key=value — is an error. A line that can be read but asks for what Pipe cannot do is a warning: the proxy cannot be used, groups leave it out, and a rule that names it refuses what it matches, so that traffic never goes through a proxy other than the one the line describes.

DIRECT, REJECT and the other built-in names cannot be given to a proxy: such a line is an error, except a line naming a proxy DIRECT, which is read past with a note.

Built-in policies#

Every profile has these policies without declaring them. Rules and groups name them as they name proxies.

Policy What it does
DIRECT Connects to the destination itself, through no proxy.
REJECT Refuses the connection: a TCP connection is reset, a proxy client is told it is refused, and a plain HTTP request is answered 403 Forbidden.
REJECT-DROP Refuses it without an answer: nothing comes back, and the client waits until it gives up.
REJECT-NO-DROP Refuses it as REJECT does.
REJECT-TINYGIF Answers a plain HTTP request with a transparent 1×1 GIF, and refuses anything else as REJECT does.
CELLULAR Connects directly, out of the cellular interface when there is one, the default way otherwise.
CELLULAR-ONLY Connects directly, out of the cellular interface alone.
HYBRID Connects directly, over Wi-Fi and cellular at once; the first to connect carries the connection.
NO-HYBRID Connects directly, and never tries cellular while on Wi-Fi.

Leaving the device describes the last four.

A line can give a built-in policy another name, with parameters of its own:

Local = direct, interface=en0
Blocked = reject-drop

The types are direct, reject, reject-drop, reject-no-drop and reject-tinygif.

Proxy types#

Type Protocol Required
ss Shadowsocks encrypt-method, password
shadowtls ShadowTLS v1, v2 or v3 TCP carrier password for v2/v3, the camouflage server's sni when different
vmess VMess username, vmess-aead=true
trojan Trojan password
vless VLESS username
anytls AnyTLS password
snell Snell, versions 1 to 6 psk
hysteria2 Hysteria 2 password
tuic-v5, tuic TUIC, versions 5 and 4 uuid and password; token
masque MASQUE: HTTP/3 CONNECT and CONNECT-UDP, or a CONNECT-IP tunnel (Cloudflare WARP's among them) —
http, https HTTP proxy, in the clear or inside TLS —
h2-connect HTTP/2 CONNECT over TLS —
naive NaiveProxy over native Chromium HTTP/2 or QUIC —
socks5, socks5-tls SOCKS5, in the clear or inside TLS —
ssh SSH port forwarding, TCP alone username, and password or private-key (with private-key-passphrase for a key under one)
wireguard WireGuard section-name
tailnet A tailnet you are signed in to —
external A program Pipe runs on a Mac or Linux host, and SOCKS5 to its port exec, local-port

Network proxy types also take the parameters for chains, leaving the device, ip-version, tests and QUIC.

Shadowsocks#

SG 01 = ss, sg.example.net, 8388, encrypt-method=chacha20-ietf-poly1305, password=secret
SG 02 = ss, sg.example.net, 8389, encrypt-method=aes-128-gcm, password=secret, obfs=http, obfs-host=bing.com
Parameter Values Default What it does
encrypt-method 2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm, 2022-blake3-chacha20-poly1305, aes-128-gcm, aes-192-gcm, aes-256-gcm, chacha20-ietf-poly1305, xchacha20-ietf-poly1305 required The cipher. Any other makes the proxy unusable.
password text required The password. For the 2022-blake3 ciphers, a key in base64: 16 bytes for aes-128-gcm, 32 for the others.
obfs http, tls none simple-obfs around the connection, as an obfs-server expects. Any other value makes the proxy unusable.
obfs-host a host name bing.com The host the obfuscation shows.
obfs-uri a path / The path the http obfuscation asks for.
udp-port a port — Not supported yet: noted, and read past.

UDP goes to the server as Shadowsocks datagrams, outside the obfuscation, which carries TCP alone.

ShadowTLS#

ShadowTLS v3 authenticates a TLS 1.3 camouflage handshake and carries another proxy protocol in authenticated TLS-shaped records. Use it as the underlying proxy of an encrypted protocol such as Shadowsocks:

[Proxy]
Cover = shadowtls, cover.example.net, 443, version=3, password=outer-secret, sni=www.example.net
Private = ss, 127.0.0.1, 8388, encrypt-method=chacha20-ietf-poly1305, password=inner-secret, underlying-proxy=Cover

Native version defaults to 3; importing a sing-box definition with no version preserves sing-box's default of 1. The camouflage certificate is checked using the TLS parameters. REALITY cannot be combined with ShadowTLS, and ECH is available only for v2. Version 3's authenticated records do not encrypt application data; the inner proxy supplies that encryption and the destination address. The carrier supports TCP only. With v3, closing its write side ends the carrier, so applications needing a response after sending EOF should use another transport.

Legacy versions are available for servers that require them. Version 1 uses TLS 1.2 for camouflage, then sends raw data. It has no password authentication; a configured password has no effect. Version 2 accepts TLS 1.2 or TLS 1.3, authenticates the client's first record with the password and frames later data as TLS application records. It does not authenticate the proxy server or later data records. Both versions still validate the camouflage certificate and require an encrypted inner protocol for application confidentiality and integrity. Only v3 authenticates the proxy server and every data record.

Shadowsocks and Snell also accept the inline form:

Private = snell, cover.example.net, 443, version=4, psk=inner-secret, shadow-tls-password=outer-secret, shadow-tls-version=3, shadow-tls-sni=www.example.net

Inline shadow-tls-version defaults to 2 and accepts 2 or 3. shadow-tls-sni is required for v3; without it v2 sends no SNI and still checks the certificate against the server or server-cert-verify-name. The compiler creates an internal carrier, preserving the selected underlying proxy, DNS strategy, trust settings and egress. Group-specific proxy chains keep that carrier too. Imports represent it as two linked entries. ShadowTLS carriers do not enter automatic proxy groups; a group may name one explicitly.

Inline Shadowsocks with udp-relay=true, and inline wrappers with a separate udp-port, are refused because a TCP carrier cannot preserve that datagram path. Use separate TCP and UDP policies for those configurations. Snell's UDP over its own stream can travel through the carrier.

VMess#

Use VMess in TLS (tls=true, with or without ws=true). In TLS a connection carries as much as it is given, as other VMess clients' connections do. Without TLS its 16-bit body nonce would repeat after 65,536 encrypted chunks each way and show an observer two messages under one key, so the connection stops there instead — after some hundreds of megabytes, or sooner for many small writes — and says to turn TLS on.

JP 02 = vmess, jp2.example.net, 443, username=0233d11c-15a4-47d3-ade3-48ffca0ce119, vmess-aead=true, tls=true, ws=true, ws-path=/ray
Parameter Values Default What it does
username a UUID, or any text required The user's ID. Text that is not a UUID is the UUID its name makes, as sing-box and Xray take it.
vmess-aead true required The AEAD handshake. The older handshake is not supported: a line without vmess-aead=true cannot be used.
encrypt-method auto, aes-128-gcm, chacha20-poly1305 auto The cipher; auto is aes-128-gcm.
tls true, false false Inside TLS, with the TLS parameters.
ws and its kin A WebSocket transport.

Trojan#

Server 1 = trojan, server1.example.net, 443, password=secret, sni=cdn.example.com
Parameter Values Default What it does
password text required The password.
TLS parameters Trojan always rides TLS (TLS parameters).
ws and its kin A WebSocket transport inside the TLS.

VLESS#

JP 01 = vless, jp.example.net, 443, username=0233d11c-15a4-47d3-ade3-48ffca0ce119, tls=true, sni=jp.example.net
Parameter Values Default What it does
username a UUID, or any text required The user's ID. Text that is not a UUID is the UUID its name makes, as sing-box and Xray take it.
tls true, false false Inside TLS, with the TLS parameters. Without it, plain TCP.
ws and its kin A WebSocket transport.
flow xtls-rprx-vision none Vision padding and directional TLS bypass for inner TLS 1.3. Without VLESS encryption, requires TLS without WebSocket, HTTPUpgrade, gRPC, XHTTP or obfuscation. UDP remains available through XUDP.
encryption none, or Xray's mlkem768x25519plus.… none VLESS encryption.
xhttp and its kin An XHTTP transport.
Vision = vless, vision.example.net, 443, username=0233d11c-15a4-47d3-ade3-48ffca0ce119, tls=true, sni=vision.example.net, flow=xtls-rprx-vision

Vision starts inside authenticated outer TLS. Eligible inner TLS 1.3 traffic can then move to the underlying connection independently in each direction; other traffic stays inside outer TLS. It preserves proxy-chain socket protection and routing. It does not require a browser TLS fingerprint.

REALITY uses tls=true, a DNS server name in sni, the server's reality-public-key (32 bytes in unpadded URL-safe base64), and optional reality-short-id (up to 16 hexadecimal digits in whole bytes). The server's public key authenticates the handshake; certificate pins, custom CAs, client certificates and ECH cannot be combined with it. skip-cert-verify does not disable REALITY authentication. TLS uses native rustls by default. Set client-fingerprint=chrome for the Chrome 133 TLS profile used by sing-box's chrome preset; importing fp=chrome preserves this choice, and an import naming another browser (fp=firefox, random, …) gets it too, with a note (Importing). This requires the verified browser-capable native library for the target.

VLESS encryption

Xray's VLESS encryption seals everything VLESS sends with keys a post-quantum handshake makes (ML-KEM-768 and X25519), with or without TLS around it. encryption is what the server's xray vlessenc gives as the client's, as it is:

Sealed = vless, sealed.example.net, 443, username=0233d11c-15a4-47d3-ade3-48ffca0ce119, encryption=mlkem768x25519plus.native.0rtt.-zL8lfQSFE7pBWmN6CrPTasbxuzpkxOjnAVVm8I9dFQ

It reads mlkem768x25519plus, then native, xorpub (the handshake's keys look random) or random (every record's header does too), then 1rtt or 0rtt (later connections reuse the ticket the first was given and send at once), then the padding, if any, and the server's keys: X25519 or ML-KEM-768, one or more relayed. It goes over any transport. flow=xtls-rprx-vision goes with it, with or without TLS: Xray's recommended pairing, Vision over the encrypted stream.

AnyTLS#

US 01 = anytls, us.example.net, 443, password=secret
Parameter Values Default What it does
password text required The password.
TLS parameters AnyTLS always rides TLS (TLS parameters).
reuse true, false true reuse=false is not supported yet: noted, and read past.

Snell#

KR 01 = snell, kr.example.net, 44046, psk=secret, version=4, obfs=tls
KR 02 = snell, kr.example.net, 44047, psk=secret, version=6, mode=unshaped, reuse=true
Parameter Values Default What it does
psk text required The pre-shared key.
version 1 to 6 1 The protocol version the server speaks. 5 is spoken as 4, which a version 5 server takes. Any other value makes the proxy unusable.
mode default, unshaped, unsafe-raw default Version 6 alone, as the server has it: default shapes records as the PSK says; unshaped does not; unsafe-raw sends in the clear. A mode on an earlier version makes the line unusable.
obfs http, tls, none, off none Obfuscation, versions 1 to 5. A version 6 line with obfs=http or obfs=tls cannot be used.
obfs-host a host name bing.com for http, cloudfront.net for tls The host the obfuscation shows.
obfs-uri a path / The path obfs=http asks for; with tls it is noted as changing nothing.
reuse true, false false Versions 4 to 6: a connection done with one request carries the next, waiting 30 seconds for it. Version 2 always does this and versions 1 and 3 never do; a line that asks otherwise gets a note.
udp-port a port the line's port Where the UDP tunnel's connection goes. Versions 1 and 2 carry no UDP, and a note says so.
userkey text none Who this client is, to a server with several users.

From version 3 on, Snell carries UDP, each session on a connection of its own.

Hysteria 2#

Server 2 = hysteria2, server2.example.net, 443, password=secret, download-bandwidth=200, upload-bandwidth=50
Server 3 = hysteria2, server3.example.net, 443, password=secret, salamander-password=noise, port-hopping="20000-30000;443", port-hopping-interval=20
Server 4 = hysteria2, server4.example.net, 443, password=secret, gecko-password=noise, gecko-min-packet-size=600
Parameter Values Default What it does
password text required The password.
download-bandwidth Mbit/s none The rate the server is asked to send at.
upload-bandwidth Mbit/s none The rate Pipe sends at: the lower of this and what the server will take. Hysteria's own congestion control, Brutal, keeps to that rate; without a rate agreed, BBR is used. A sing-box server agrees on one only with a client that gives download-bandwidth too.
TLS parameters Hysteria 2 rides QUIC, which is TLS (TLS parameters); alpn is h3 unless it says.
salamander-password text none Salamander obfuscation of every packet.
gecko-password text none Gecko obfuscation: Salamander, and the handshake's packets cut into 2 to 8 fragments padded to random sizes. A line cannot have both.
gecko-min-packet-size, gecko-max-packet-size bytes, at most 2048 512, 1200 The sizes Gecko pads fragments to on the wire. Pipe's additions (mihomo's obfs-min-packet-size, sing-box's min_packet_size).
port-hopping ports and ranges, separated by ; or ,: "20000-30000;443" none The ports to hop between; the line's own port is then not used. A port that is not one, or a list with none, makes the line unusable.
port-hopping-interval seconds, 5 at least 30 How often to hop to another port.
ecn auto, on, off (true, false) auto Whether packets are marked ECN-capable. auto and on mark them until the path is found not to carry the marks; off never does.

Port hopping cannot go through another policy: a line with both port-hopping and underlying-proxy cannot be used.

TUIC#

tuic-v5 is TUIC version 5; tuic is version 4.

SG 03 = tuic-v5, sg3.example.net, 443, uuid=0233d11c-15a4-47d3-ade3-48ffca0ce119, password=secret, congestion-control=bbr
SG 04 = tuic, sg4.example.net, 443, token=secret, udp-relay-mode=quic
Parameter Values Default What it does
uuid, password a UUID, text required (v5) Who the client is.
token text required (v4) The token, in place of uuid and password.
TLS parameters TUIC rides QUIC (TLS parameters); alpn is h3 unless it says.
congestion-control cubic, new_reno, bbr cubic The congestion control. Any other makes the line unusable.
udp-relay-mode native, quic native How UDP is carried. native: as QUIC datagrams. quic: each datagram on a stream of its own.
udp-over-stream true, false false Version 5: UDP carried as UoT version 2 on a stream. Not with udp-relay-mode=quic, and not on version 4: either makes the line unusable.
heartbeat seconds 10 How often a connection in use says it is still there, so that the server and the paths between keep it.
port-hopping, port-hopping-interval As for Hysteria 2.
ecn auto, on, off auto As for Hysteria 2.

On version 4, a datagram goes whole: in the native relay mode, one larger than the path carries cannot go.

MASQUE#

Office = masque, proxy.example.net, 443, username=alice, password=secret
WARP = masque, 162.159.198.1, 443, mode=cloudflare, private-key=MHcCAQEEIKbmk1X1riPhNjwA2hrp/nRb90qhtIr4Q/jzz6VpArbQoAoGCCqGSM49AwEHoUQDQgAEBRmZdz0z2WaCjD/gdbGsaiVun4OdtI8Cu4O+SFz/5d+HIKgbgcakp8zfy+VIUC9NB2/F9voVok6hT3+n8EYJbA==, public-key=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFkanyq60YX7ymGMjH39nzSfADy5pd/SGgJX80WLB23wULLFfHGruy8m7AMAP5TeQIBVK3ndGAhTUnoCo5LfpuQ==, self-ip=172.16.0.2, self-ip-v6=2606:4700:110:8a36::2

MASQUE proxies over HTTP/3, on one QUIC connection that every TCP connection and UDP session of the policy shares. A plain line is Surge's: each TCP connection is an HTTP/3 CONNECT stream, and each UDP session a CONNECT-UDP request (RFC 9298) whose datagrams ride HTTP Datagrams (RFC 9297). UDP needs nothing on the line, but the server has to take extended CONNECT and HTTP Datagrams, which its HTTP/3 settings say when the connection is made; where they do not, the policy carries TCP alone and its UDP goes as UDP a proxy cannot carry does.

mode=connect-ip is a CONNECT-IP tunnel (RFC 9484) instead: the connection carries IP packets, and Pipe's own network stack makes TCP connections and UDP sessions of them, from the addresses the tunnel has. mode=cloudflare is Cloudflare WARP's MASQUE, as mihomo speaks it: a CONNECT-IP tunnel in WARP's dialect, the account's key shown in a certificate Pipe makes of it, the server known by its public key alone. The keys and addresses come from the account's registration (WARP's tools and mihomo configs have them); Pipe does not register accounts.

Parameter Values Default What it does
username, password text none Sent as HTTP Basic credentials (Proxy-Authorization) with each request. Both or neither.
TLS parameters MASQUE rides QUIC (TLS parameters); alpn is h3 unless it says, and client-cert shows a [Keystore] item.
mode connect, connect-ip, cloudflare connect How the connection carries traffic, as above.
uri a URI template, or a path on the server the RFCs' /.well-known/masque/udp/{target_host}/{target_port}/ and /.well-known/masque/ip/{target}/{ipproto}/; WARP's https://cloudflareaccess.com Where the extended CONNECT asks: {target_host} and {target_port} for UDP (both needed), {target} and {ipproto} for a tunnel (* each), or a query of them ({?target_host,target_port}).
private-key base64 DER (SEC1 or PKCS#8), P-256 or P-384 none An EC key shown in a certificate of its own: WARP's enrolled key.
public-key base64 DER (SubjectPublicKeyInfo) none; required by cloudflare unless skip-cert-verify The server's key: the one check of its certificate, which skip-cert-verify does not lift.
self-ip, self-ip-v6 an address the server's assignment A tunnel's own addresses. Without either, the tunnel waits for the server to assign them.
mtu 1280 to 65535 1280 The largest IP packet in a tunnel.
dns-server addresses, system the engine's DNS Where a tunnel looks up the names it dials, asked through it, as for a WireGuard section.
prefer-ipv6 true, false false A name with addresses of both families is dialled over IPv6 in a tunnel.
port-hopping, port-hopping-interval As for Hysteria 2.
ecn auto, on, off auto As for Hysteria 2.

self-ip, self-ip-v6, mtu, dns-server and prefer-ipv6 are a tunnel's: on a connect line they make it unusable. A datagram or a tunnel's packet too large for the path's QUIC datagrams goes in a DATAGRAM capsule on its request stream instead, reliably and in order; a server that reads no capsules loses it, as the path would. A server's GOAWAY sends what comes next to a new connection. Port hopping cannot go through another policy.

HTTP and SOCKS5#

Office = http, proxy.example.com, 8080, alice, secret
Secure = https, proxy.example.com, 443, username=alice, password=secret, sni=proxy.example.com
Lab = socks5, 10.0.0.5, 1080
Relay = socks5-tls, relay.example.com, 443, alice, secret

https and socks5-tls are the same proxies as http and socks5, inside TLS, and take the TLS parameters.

Parameter Values Default What it does
username, password text none The credentials, written after the port in that order, or named.
headers Name:value;Name:value none http and https: headers sent to the proxy, each in place of the request's own header of that name, Host too. <random-string(16)> or <random-string(16-32)> in a value is a random string of that length, new for each connection. On socks5, noted and read past.

With socks5-tls, UDP goes to the proxy's relay in the clear, as SOCKS5 carries it.

SSH#

Box = ssh, box.example.net, 22, username=me, password=secret, server-fingerprint="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0bqjWWRpPmLJQ4eA9X4nOszyfZ56ya0QNfDZpK6OJ1"

With a key, the key is a [Keystore] item, and a key under a passphrase has it beside it:

[Proxy]
Lab = ssh, lab.example.net, 2222, username=me, private-key=lab-key, server-fingerprint=SHA256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU, idle-timeout=600
Vault = ssh, vault.example.net, 22, username=me, private-key=vault-key, private-key-passphrase="correct horse", server-fingerprint=SHA256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU

[Keystore]
lab-key = type=openssh-private-key, base64=LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K…
vault-key = type=openssh-private-key, base64=LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K…

Every connection is a channel (direct-tcpip, what ssh -W asks for) of one SSH session to the server, made for the first and kept while it carries any. The server must let the user forward connections (AllowTcpForwarding, OpenSSH's default).

Parameter Values Default What it does
username text required Who logs in.
password text none The user's password, tried after the key, by password and then keyboard-interactive authentication.
private-key a [Keystore] item none The user's key: an openssh-private-key item, the key file as ssh-keygen writes it (OpenSSH's format or PEM), in base64. Ed25519, ECDSA (P-256, P-384, P-521) and RSA keys.
private-key-passphrase text none The passphrase the key is encrypted under (mihomo's name; sing-box's private_key_passphrase). Read for a key in OpenSSH's own format, as ssh-keygen encrypts one: bcrypt with AES-CTR, its default, or AES-GCM (-Z aes256-gcm@openssh.com). A wrong one is said to be, on the line.
server-fingerprint host keys as known_hosts writes them (ssh-ed25519 AAAA…), or SHA256:… fingerprints as ssh-keygen -l prints them, separated by commas required, unless skip-host-key-verify The keys the server's must be one of. ssh-keyscan box.example.net prints a server's; check them against the server itself.
skip-host-key-verify true, false false With no server-fingerprint, any server key is taken: a server in the middle gets the password and reads what goes through. Without either, the line is unusable.
idle-timeout seconds 180 How long a session with no connection on it is kept.
client-version SSH-2.0-… SSH-2.0-Pipe What Pipe says it is to the server. Pipe's addition (sing-box's client_version).
  • Pipe speaks what OpenSSH 6.5 and later do: curve25519-sha256 with strict key exchange, AES-GCM or ChaCha20-Poly1305, and host keys Ed25519, ECDSA or RSA (SHA-2). A server with none of them cannot be used, and the reason says what it offers.
  • A session that carries connections asks the server whether it is there after 30 quiet seconds, and is closed when it does not answer in 15; its keys change after each gigabyte. A session made before the network changed takes no new connections, and closes when its last is done.
  • SSH forwards no UDP: the proxy carries TCP alone, and its UDP goes where udp-policy-not-supported-behaviour sends it.
  • A key under a passphrase is opened each time the profile is checked and the tunnel starts, at the cost its file asks for: ssh-keygen's 24 rounds unless -a said more. A key asking for more than 128 rounds is not read (ssh-keygen -p -a 100 -f key, run on a copy, rewrites it with fewer), and a profile's keys together may ask for the second's work of key derivation [Keystore] allows: some six keys of 24 rounds. An encrypted PEM key is not read: ssh-keygen -p -f key, run on a copy, rewrites it in OpenSSH's format.
  • Imported from Clash/mihomo (type: ssh) and sing-box, an SSH proxy comes into the proxy pool with its password, or its key and the key's passphrase, held as the pool holds a password. A key must be in the config itself (mihomo's private-key: | text, sing-box's private_key): one named by its file's path is not imported, as that file is on the machine the config was written for. A converted profile, and the pool's list, write each key as a [Keystore] item of its own, named ssh-key- and its digest.

NaiveProxy#

Browser proxy = naive, proxy.example.net, 443, username=alice, password=secret, sni=proxy.example.net, udp-over-tcp=true
Browser QUIC = naive, proxy.example.net, 443, username=alice, password=secret, quic=true

naive uses the pinned native Chromium network stack, including Naive's request and stream padding. quic=true selects HTTP/3; the default is HTTP/2. udp-over-tcp=true enables sing-box UDP-over-TCP version 2 over the selected transport. headers=Name:value;Name:value adds request headers. The proxy's authentication, padding and transport control headers are reserved.

TLS certificate verification is required. sni, tls-ca-base64, ech, ech-config and ech-query-server-name are supported. Naive does not accept skip-cert-verify, custom ALPN or fingerprint profiles, certificate pins, REALITY or client certificates. ECH requires a DNS name in sni. underlying-proxy and the profile's server resolver also apply to native connections. Resource limits are fixed for mobile use; sing-box's concurrency, receive-window and congestion-control tuning fields are refused on import.

A build links a verified source archive and receipt from tools/build-cronet-native.py; the iOS app requires one, for the browser and source-trust APIs. Cargo verifies native inputs before linking and never downloads them. Builds without a matching archive report Naive as unavailable.

Protocol interoperability tests run on the engine host. Physical-device VPN memory and energy performance require separate qualification.

HTTP/2 CONNECT#

Office H2 = h2-connect, proxy.example.net, 443, username=alice, password=secret, max-streams=3

h2-connect carries TCP connections as independent HTTP/2 streams over TLS. The proxy must negotiate h2 with ALPN. Authentication, custom headers (including random padding), TLS parameters, and underlying-proxy apply. Connection-specific HTTP headers and a custom Host header are refused; CONNECT identifies its destination in the HTTP/2 authority field.

max-streams limits simultaneous tunnels on each connection to 1–32, default 3. Pipe opens up to eight connections for a policy and refuses further concurrent tunnels when those are full. Each connection reserves its bounded buffers from the engine's memory budget and closes when an idle check finds no tunnels. Cancelling a tunnel resets its stream; other streams continue. A network change opens a fresh connection for new tunnels while existing ones can finish.

udp-relay=true enables RFC 9298 CONNECT-UDP. It is off by default. The server must advertise extended CONNECT support in its HTTP/2 settings; Pipe confirms this before sending a UDP request. It uses the standard /.well-known/masque/udp/{target_host}/{target_port}/ endpoint, with authentication and TLS settings shared with TCP. Each UDP association occupies one stream in the same pool as TCP tunnels.

UDP payloads travel in RFC 9297 capsules, so packet boundaries and empty datagrams are preserved. The engine sends up to 65,507 bytes per packet; the reader accepts the RFC's 65,527-byte maximum. Unknown capsule types and context IDs are skipped incrementally. Malformed capsules end their stream. Buffers and bounded packet queues use the engine's memory budget. Because capsules travel over TCP, delivery is reliable and ordered; this is not native UDP loss behavior, and block-quic=auto still applies.

WebSocket transport (ws=true) is not supported for this policy type.

WireGuard#

Home = wireguard, section-name=Home

section-name names the [WireGuard <name>] section that holds the tunnel's settings. A line naming a section that is not there cannot be used.

Tailnet#

Home exit = tailnet, exit-node=mac-mini
Work exit = tailnet, tailnet=example.com, exit-node=auto

A tailnet is not something a profile holds: you sign in to it in the app (Tailnets), and a tailnet policy names one for rules and groups.

Parameter Values Default What it does
tailnet a tailnet's name the one signed in The tailnet, by its name as its admin console shows it (example.com), or its MagicDNS suffix. It may be left out while one tailnet is signed in.
exit-node auto, or a node none Where what the policy is given for the internet goes. auto: the nearest exit node online, kept while it stays online. A node is named by its MagicDNS name (short or full), its stable ID or its address. Without it, the policy reaches only the tailnet.
  • A policy whose tailnet is not signed in, is off or is not running fails as an unreachable proxy does: groups pass over it, and a rule that names it refuses what it matches.
  • It cannot go through an underlying-proxy: the tailnet is reached by its account's own node.
  • It needs active Pipe Pro. Without it, the policy is left out as a proxy Pipe cannot use ("requires Pipe Pro: tailscale"), and the rest of the profile runs. Nothing else a profile says needs Pro.
  • A Surge tailscale line and its [Tailscale <name>] section are not read: converting a Surge profile makes such a line a tailnet policy of the same name, keeping its exit-node, and offers the tailnet for sign-in with the section's auth key and control server, which never go into the profile. sing-box's Tailscale endpoints come across the same way.

External proxy programs#

A program that serves SOCKS5 on 127.0.0.1, such as ssh -D, used as a policy. Pipe runs it, on a Mac or a Linux host:

[Proxy]
Bastion = external, exec = "/usr/bin/ssh", args = "user@1.2.3.4", args = "-N", args = "-D", args = "127.0.0.1:1080", local-port = 1080, addresses = 1.2.3.4
Parameter What it is
exec The program, by its whole path. Nothing is looked up in PATH, and no shell reads the line.
args One argument. Given once for each, in order; at most 64.
local-port The port it serves SOCKS5 on, on 127.0.0.1.
addresses An address the program itself connects to, given once for each: it is left out of the tunnel. At most 32.
udp-relay true: UDP goes through the program too, if it does SOCKS5 UDP. Off unless said.
  • It is started when its policy is first used, and started again when it stops: after a second, then twice as long each time it stopped within a minute of starting, up to 30 seconds.
  • A connection to its port that is refused, while it is coming up, is tried again every 500 ms, six times more.
  • It stops when Pipe stops, with what it started. It runs in a process group of its own, with no terminal, and with HOME, USER, LOGNAME, PATH, LANG, LC_ALL, TMPDIR and SSH_AUTH_SOCK and no other variable. What it says goes to a log of at most 512 KiB, and one older part of the same size.
  • What the program and its child processes connect to goes DIRECT, whatever the mode and the rules say: it is what the policy rides on. A process that left the program's group (a daemon) is not known to be the program's: name what it connects to in addresses.
  • On iOS the line is noted, and refuses what it is given, as REJECT does.

Approving a program

A profile can come from anyone, and this line runs a program as you. So a profile never gets to say a program may run:

  • Compiling says what the line would run, in a warning on the line and in the profile's summary, whether or not it is approved.
  • Nothing runs until you approve that exact command in the app: the path, every argument in order, and the port. Another argument is another command, which waits for a decision of its own. Until then, and after a refusal, the policy fails its connections.
  • The decision is kept with Pipe's data, and the app lists every decision with the command it was about. Taking one back stops the program.
  • What the app shows is what runs. A path with . or .. in it, a relative path, and a word holding a control or text-direction character make the proxy unusable.
  • Modules cannot add proxies, and no policy-path list, managed profile or file included from a URL can bring this type: such a line is left out or unusable, with a warning, whatever was approved. Those change without anyone reading them. A linked profile of your own can hold it.
  • The decision is the app's alone. The HTTP API over the network and scripts cannot read or make one: a profile chooses http-api's key.
  • An engine that runs with more rights than the user who wrote its profile (--confine) runs no program at all.

TLS parameters#

Where a protocol rides TLS — trojan, anytls, shadowtls, https, h2-connect, naive, socks5-tls, and vmess and vless with tls=true — or QUIC — hysteria2, tuic, tuic-v5 — these parameters say how the server is checked.

Parameter Values Default What it does
sni a host name, or off the server's host The name the handshake asks for. sni=off sends no name; the certificate is still checked against the server's host, or against server-cert-verify-name.
skip-cert-verify true, false false Any certificate is taken. The handshake's signatures are still checked — whoever shows a certificate holds its key — but the certificate is not.
server-cert-verify-name a host name the name asked for The certificate is checked against this name, while the name asked for stays the sni's.
server-cert-fingerprint-sha256 64 hexadecimal digits, or their base64, with or without colons between pairs none The server's certificate must be this one, by the SHA-256 of its DER encoding. It is the only check, in place of the usual validation, so a self-signed certificate can be trusted; skip-cert-verify does not lift it. A value that is neither makes the line unusable.
client-cert a [Keystore] item none The certificate, with the chain behind it, and the key shown to a server that asks for one. An item that is not there, or that cannot be read, makes the line unusable.
client-fingerprint chrome, chrome-133 native rustls Use the Chrome 133 TLS profile, including its real cipher suites, key shares, shuffled extensions, ECH GREASE and Brotli certificate decompression. Requires a verified source-built native library. TCP TLS only; Naive uses its own native profile and QUIC does not accept this parameter.
tls-ca-base64 base64 of a PEM certificate bundle none Replace the default trust roots with these certificates, at most 64 anchors and 256 KiB of PEM.
ech true, false false Encrypt the inner ClientHello and server name with ECH. Requires TLS 1.3 and SNI. A failed ECH handshake does not retry with a visible inner name.
ech-config base64 of an ECHConfigList HTTPS DNS lookup Static ECH configuration, used with ech=true. In sing-box JSON, tls.ech.config holds an ECH CONFIGS PEM block.
ech-query-server-name a DNS name sni Name whose HTTPS DNS record supplies ECH configuration when ech-config is absent. Uses the profile's server bootstrap resolver and DNS cache expiry.
alpn protocols, separated by commas, quoted when there are several: alpn="h2, http/1.1" none over TCP; h3 over QUIC The protocols the handshake offers. A WebSocket asks for http/1.1.
tls13 true, false — Changes nothing, and a note says so: TLS 1.3 is always offered, and used when the server has it; TLS 1.2 otherwise.
tls-min-version, tls-max-version 1.2, 1.3 both The lowest and highest version the handshake may settle on.

The Chrome option matches the TLS profile selected by sing-box 1.14.3, which pins uTLS's Chrome 133 preset. It does not turn proxy framing or HTTP headers into a browser. ALPN defaults to h2,http/1.1; a transport's required ALPN still applies. Custom ALPN or TLS-version bounds deliberately alter the profile. Other browser profiles and targets without the required native library are reported as unavailable; they never fall back silently.

tools/build-cronet-native.py produces the native library and its checked receipt. Cargo verifies that receipt, including the pinned source, TLS patches and trust policy, before enabling Naive and the Chrome option. Apple's tools/build-apple.sh builds missing or stale archives before packaging, in CARGO_TARGET_DIR and the user's cache directory.

On a proxy without TLS, ECH and custom trust roots make the proxy unusable; the other parameters are noted as changing nothing.

Certificates are checked as the system checks them, unless tls-ca-base64 replaces its roots: on iPhone and iPad, a root you installed with a configuration profile and turned on in Settings › General › About › Certificate Trust Settings is trusted too, by proxies and by everything Pipe downloads itself (rule sets, subscriptions, scripts' requests). ECH preserves the configured certificate checks. Remote certificate_path and ECH config_path files are not imported; provide their inline contents instead.

WebSocket#

vmess, trojan and vless can carry their protocol inside a WebSocket. With TLS, the WebSocket is inside it, and the TLS asks for HTTP/1.1.

CDN = trojan, cdn.example.com, 443, password=secret, ws=true, ws-path=/t?ed=2048, ws-headers=Host:origin.example.com, ws-early-data=2048
Parameter Values Default What it does
ws true, false false Use a WebSocket. Without it, the other ws- parameters are noted and read past.
ws-path a path / The path of the upgrade request, sent as written, query and all.
ws-headers Name:value|Name:value Host is the server and its port Headers of the upgrade request.
ws-early-data bytes 0 Up to that many bytes of the protocol's first request ride in the upgrade itself, saving a round trip.
ws-early-data-header a header name, or "" Sec-WebSocket-Protocol The header the early data goes in. With "", it goes after the path instead. Without ws-early-data, noted and read past.

HTTPUpgrade and gRPC#

vmess, trojan and vless also support HTTPUpgrade and gRPC transports. Choose one transport per proxy: WebSocket, HTTPUpgrade, gRPC or XHTTP.

Upgrade = vless, cdn.example.com, 443, username=b831381d-6324-4d53-ad4f-8cda48b30811, tls=true, http-upgrade=true, http-upgrade-path=/tunnel, http-upgrade-host=origin.example.com
RPC = trojan, cdn.example.com, 443, password=secret, grpc=true, grpc-service-name=pipe
Parameter Default What it does
http-upgrade false Start an HTTP/1.1 upgrade, then carry the protocol as an unframed stream.
http-upgrade-path / Request path, including any query.
http-upgrade-host TLS server name, else server and port Host header sent to the front end.
http-upgrade-headers none Additional headers, written Name:value|Name:value.
grpc false Carry the protocol through an HTTP/2 gRPC stream.
grpc-service-name empty The server's service name, encoded in /<service>/Tun.

TLS uses HTTP/1.1 for HTTPUpgrade and HTTP/2 for gRPC. These transports also carry the proxy protocol's UDP relay when it supports one. Proxy chains still select how the transport reaches its server. gRPC idle/ping tuning is not exposed; imports with unsupported active settings report it. These transports use Pipe's default rustls TLS or the explicitly selected client-fingerprint=chrome native TLS profile, including REALITY when configured. Vision cannot be combined with their additional framing.

XHTTP#

vmess, trojan and vless can also go as Xray's XHTTP: the stream as HTTP requests, through the CDNs and front ends that carry HTTP.

Edge = vless, edge.example.net, 443, username=0233d11c-15a4-47d3-ade3-48ffca0ce119, tls=true, sni=edge.example.net, xhttp=true, xhttp-path=/x, xhttp-mode=auto
Packets = trojan, cdn.example.com, 443, password=secret, xhttp=true, xhttp-path=/up, xhttp-host=origin.example.com, xhttp-mode=packet-up, xhttp-max-connections=4

HTTP/2 carries the requests in TLS, HTTP/1.1 with alpn=http/1.1 (packet-up only) and HTTP/3 with alpn=h3. HTTP/3 goes in QUIC's own TLS, so without REALITY or a client-fingerprint.

Parameter Values Default What it does
xhttp true, false false Go as XHTTP. Not with ws, http-upgrade or grpc.
xhttp-path a path / The path of every request, query and all.
xhttp-host a host the sni, else the server The host the requests ask for.
xhttp-mode auto, packet-up, stream-up, stream-one auto One request each way (stream-one), a download and a streaming upload (stream-up), or a download and uploads in pieces (packet-up). auto is stream-one under REALITY and packet-up otherwise.
xhttp-headers Name:value|Name:value a browser's Headers of every request; with a User-Agent of its own, no browser's.
xhttp-padding bytes, or from-to 100-1000 Padding each request carries, which the server checks.
xhttp-no-grpc-header true, false false No Content-Type: application/grpc on streaming uploads.
xhttp-max-post-bytes bytes, or from-to 1000000 packet-up: the most an upload carries (256 KiB at most on iPhone and iPad).
xhttp-min-post-interval milliseconds, or from-to 30 packet-up: the least time between uploads.
xhttp-max-connections a count, or from-to 3 Connections the requests are spread over. Not with xhttp-max-concurrency.
xhttp-max-concurrency a count, or from-to none Connections on one before another is opened.
xhttp-max-reuse-times a count, or from-to none Connections one carries in its life.
xhttp-max-request-times a count, or from-to 600-900 Requests one carries in its life.
xhttp-max-reusable-secs seconds, or from-to 1800-3000 How long one takes new requests.
xhttp-download server[:port] Download from another server (Xray's downloadSettings): the GET goes there, uploads stay here. Not with stream-one or HTTP/1.1.
xhttp-download-path, xhttp-download-host the uploads' path; its address The same for that server.
xhttp-download-tls true, false as the uploads Its TLS. Set by any parameter below.
xhttp-download-sni, xhttp-download-alpn, xhttp-download-skip-cert-verify, xhttp-download-fingerprint, xhttp-download-public-key, xhttp-download-short-id Its TLS and REALITY. xhttp-download-alpn=h3 downloads over HTTP/3.

The last five are Xray's XMUX; the defaults are Xray's when none is given. With TLS the requests go over HTTP/2, or over HTTP/1.1 when alpn=http/1.1 — which carries packet-up alone; without TLS, over HTTP/2 straight away, as the server takes it. In packet-up the server hears that a connection is done only when it closes. Pipe keeps at most 16 connections (6 on iPhone and iPad) of 128 requests each. HTTP/3, padding in Xray's obfuscation mode, a session or sequence number elsewhere than the path, an upload method other than POST, and downloads from another server (downloadSettings) are not supported yet; an import that asks for them says so.

Multiplexing#

ss, vmess, vless and trojan can carry many connections on a few to the server: sing-box's multiplex and mihomo's smux, which a sing-box or mihomo server takes when its inbound enables multiplexing (and still takes connections that are not multiplexed).

Many = vless, mux.example.net, 443, username=0233d11c-15a4-47d3-ade3-48ffca0ce119, tls=true, mux=h2mux, mux-padding=true
Few = trojan, mux.example.net, 443, password=secret, mux=smux, mux-max-connections=4, mux-min-streams=4
Parameter Values Default What it does
mux h2mux, smux, yamux; true is h2mux off How connections share one: HTTP/2, smux or yamux.
mux-max-connections a count none Connections to spread over: another is opened while each has mux-min-streams and there are fewer than this.
mux-min-streams a count none With mux-max-connections, what each connection carries before another is opened.
mux-max-streams a count none What a connection carries before another is opened. Not with the two above.
mux-padding true, false false Pad the first frames each way, as a server with padding requires.
mux-only-tcp true, false false UDP goes the protocol's own way instead of on the shared connections.

Without a count, a connection is shared once it is idle: each connection in use carries one at a time. Pipe keeps at most 32 shared connections (8 on iPhone and iPad) of 128 connections each; past that, the least busy one takes more. A shared connection idle for 30 seconds closes, and all do when the network changes. VLESS with Vision is not multiplexed. TCP Brutal, which a Linux client sets on its own socket, is not asked for.

Proxy chains#

underlying-proxy names a policy — a proxy or a group — that the connection to the proxy's server goes through.

[Proxy]
Relay = trojan, relay.example.net, 443, password=secret
Exit = ss, exit.example.net, 8388, encrypt-method=aes-256-gcm, password=secret, underlying-proxy=Relay
  • The server's name is handed to the underlying policy as it is, to be looked up at its end. ip-version therefore changes nothing on such a line, and a note says so.
  • UDP goes through it too. Shadowsocks' and a SOCKS5 relay's datagrams go as the underlying policy's datagrams; protocols that carry datagrams on a stream carry that stream through it; Hysteria 2 and TUIC send their QUIC packets as its datagrams, at the smallest packet size every path carries.
  • DIRECT — and CELLULAR, CELLULAR-ONLY, HYBRID and NO-HYBRID, which go directly — is no chain.
  • A proxy whose underlying policy is not there, refuses everything, cannot be used, or leads back to it (A → B → A) cannot be used either.
  • A chain that comes back through a group — a group that picks the proxy going through it — fails that connection, and says so.
  • A proxy's way out of the device is its underlying policy's, with its own line's interface and hybrid besides — except a tunnel's (wireguard), which reaches its peers its own way whatever its underlying policy says.

A whole group can take its proxies through a policy: underlying-proxy on a group.

Leaving the device#

How a policy's connections leave the device — every proxy line, and every line that renames direct:

Parameter Values Default What it does
interface an interface name: en0, pdp_ip0 the default Connections go out of that interface. Where it is not there, they fail.
allow-other-interface true, false false With interface: where that interface is not there, connections go the default way instead of failing.
hybrid on, off, auto (true, false) auto on: connect over Wi-Fi and cellular at once, the first to connect carrying the connection. off: never try cellular while on Wi-Fi. auto: as all-hybrid says.
tfo true, false false Open TCP with Fast Open: the first bytes ride the SYN once the server has given the device a cookie. Used for a server with one address, where nothing is raced; whether the server was reached is known at the first write.
tos 0 to 255, or hexadecimal after 0x 0 The TOS byte the packets are marked with. A value that is neither is warned about and read past.

The built-in policies CELLULAR, CELLULAR-ONLY, HYBRID and NO-HYBRID are DIRECT with one of these set:

Policy Is
CELLULAR out of the cellular interface when there is one, the default way otherwise
CELLULAR-ONLY out of the cellular interface alone
HYBRID hybrid=on
NO-HYBRID hybrid=off

In [General], all-hybrid = true connects every policy over Wi-Fi and cellular at once, but those that say hybrid=off. wifi-assist = true tries cellular only when Wi-Fi has not connected within a second, or has failed. Both work on Wi-Fi with cellular there, and for connections: UDP goes the policy's one way. [SSID Setting] can change this on some networks.

  • A proxy's way out is that of its connection to its server: Hysteria 2's and TUIC's QUIC sockets too, each port they hop to included. On those, QUIC's own ECN marks take the place of tos's.
  • Through an underlying-proxy, it is the underlying proxy's.
  • WireGuard and tailnet policies reach their peers their own way: on their lines these parameters are noted and read past.
  • On a network without IPv4 — an IPv6-only one, as some mobile carriers' are, which reaches IPv4 hosts through NAT64 — a server or destination given as an IPv4 address, or a name with IPv4 addresses alone, is dialled at the address the network's NAT64 gives it, the prefix asked of the network's own resolver. So are WireGuard's peers and DNS servers given by address; a tailnet's direct paths are not.
  • dns-follow-interface is not supported yet.

ip-version#

How a proxy's server name is looked up and dialled.

Value What it does
dual (the default) Both families, raced a quarter of a second apart, the first to connect winning.
v4-only, v6-only That family alone.
prefer-v4, prefer-v6 That family first, and the other once three seconds have passed without a connection, or the first has failed.

Any other value makes the proxy unusable. A server given as an address is dialled as it is. On a wireguard or tailnet line, and on a line with an underlying-proxy, ip-version changes nothing, and a note says so. The lookup goes through the profile's DNS.

Tests#

A proxy can say how it is tested, in place of what the profile says.

Parameter Values Default What it does
test-url a URL proxy-test-url What the proxy is tested with.
test-timeout seconds the group's How long its test may take.
test-udp hostname@server proxy-test-udp Its UDP test: a lookup of hostname at the DNS server server, through the proxy.

A value Pipe cannot read is warned about, and the profile's is used. Groups says how a test is made and timed.

UDP through a proxy#

Every protocol that can carry UDP carries it, unless its line says udp-relay=false: then the proxy carries TCP alone — on ss, vmess, vless, trojan, anytls, socks5, socks5-tls and snell — and its UDP goes where udp-policy-not-supported-behaviour sends UDP a policy cannot carry: refused (REJECT, the default) or out directly. A group that picks it for UDP does the same. Hysteria 2, TUIC and WireGuard carry UDP as they are made to, and say so with a note; h2-connect and naive carry it only when their line asks (udp-relay=true, udp-over-tcp=true).

QUIC through a proxy#

block-quic on a proxy says whether QUIC goes through it, where block-quic in [General] is per-policy (its default).

Value QUIC through this proxy
on (true) is refused, so that apps fall back to TCP
off (false) goes through
auto (the default) goes through DIRECT and its kin, Hysteria 2, TUIC in its native relay mode without udp-over-stream, WireGuard and tailnet policies — when neither goes through an underlying-proxy; is refused through every other proxy

Any other value is warned about, and auto is used.

Parameters that are read past#

Parameter Said as
mptcp, dns-follow-interface, no-error-alert, always-use-connect; idle-timeout except on ssh; max-streams except on h2-connect; ecn except on hysteria2, tuic, tuic-v5 and masque a note: not supported yet, and read past
tls13 a note: changes nothing
udp-relay=false on hysteria2, tuic, tuic-v5, wireguard a note: the protocol carries UDP as it is made to

A parameter many proxies share is said once for the whole profile: line 3: `mptcp` (on 40 proxies) is not supported yet and is ignored.

Not supported yet#

A line asking for one of these cannot be used, and says why:

  • the type trust-tunnel;
  • VLESS flows other than xtls-rprx-vision;
  • inline ShadowTLS on types other than Shadowsocks and Snell, or with a separate UDP path; use explicit carrier and UDP policies instead;
  • VMess without vmess-aead=true;
  • a type Pipe does not know.

[Keystore]#

Certificates and keys, kept in one place and used by name: a proxy's client-cert, and the HTTPS decryption CA.

[Keystore]
office-cert = type=p12, base64=MIIKJwIBAzCCCe0GCSqGSIb3DQEHAaCC…, password=secret
Parameter Values Default What it does
type p12, openssh-private-key p12 when there is a password, openssh-private-key otherwise What the item is.
base64 base64 required The item's file, in base64. Line breaks and spaces in it are ignored.
password text none The PKCS#12 file's password.
  • A p12 item is a PKCS#12 file. Pipe reads it as OpenSSL 3 encrypts one (PBKDF2 and AES), as OpenSSL 1 and the macOS keychain do (3DES and RC2), and unencrypted, and checks its MAC — PKCS#12's own, or PBMAC1 (openssl pkcs12 -pbmac1_pbkdf2) — so a wrong password is said to be one, on the line of the proxy that uses it. A password that is not ASCII opens the files OpenSSL 1.0 and LibreSSL (macOS's openssl) write with it too.
  • A file larger than 256 KiB, or asking for more than 600,000 rounds of key derivation, is not read. A profile's keys together may ask for a second's work of key derivation, hundreds of files as OpenSSL writes them; a key past that is not read.
  • An openssh-private-key item is an ssh proxy's private-key: the key file, base64 once more over the base64 already in it, as Surge has it (base64 -i id_ed25519). A key under a passphrase is the file as it is, encrypted; the proxy line has the passphrase (private-key-passphrase).
  • A line that is not name = key=value, …, has no base64, or has a parameter not in the table, is warned about.

[WireGuard <name>]#

The settings of a wireguard proxy.

[Proxy]
WARP = wireguard, section-name=WARP

[WireGuard WARP]
private-key = YNXtAzepDqRv9H52osJVDQnznT5AL11eVlnvHeYk+Xs=
self-ip = 172.16.0.2
self-ip-v6 = 2606:4700:110::2
dns-server = 1.1.1.1, 2606:4700:4700::1111
mtu = 1280
peer = (public-key = bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=, allowed-ips = "0.0.0.0/0, ::/0", endpoint = engage.cloudflareclient.com:2408, keepalive = 25)
Key Values Default What it does
private-key base64 required This device's private key.
self-ip an IPv4 address — The tunnel's own IPv4 address. self-ip or self-ip-v6 is required.
self-ip-v6 an IPv6 address — The tunnel's own IPv6 address.
dns-server a list of addresses the profile's DNS Servers for the names this proxy dials, asked through the tunnel. system in the list adds the profile's DNS behind them.
prefer-ipv6 true, false false A name with addresses of both families is dialled over IPv6.
claim-routes true, false false A WireGuard network: the peers' private allowed IPs go through this tunnel ahead of the rules.
dns-domains a list of domains — Names under these domains are asked of dns-server through the tunnel, by Pipe's DNS too. Needs dns-server.
mtu 576 to 9000 1408 The largest packet the tunnel carries.
peer (…), see below required A peer. Several peers are separated by commas, or written on several peer lines.
jc, jmin, jmax, s1–s4, h1–h4, i1–i5 AmneziaWG's — The server speaks AmneziaWG.

A peer's fields:

Field Values What it does
public-key base64 The peer's public key. Required.
allowed-ips address ranges, quoted when there are several What goes to this peer. Required.
endpoint host:port, [v6-address]:port Where the peer is. Required.
preshared-key base64 A pre-shared key.
keepalive seconds A keepalive packet every so many seconds; 0 sends none.
client-id three bytes: 83/12/235, 530ceb (hexadecimal), or four base64 characters The reserved bytes some servers identify a client by.

A section with a key not in the table has it noted and read past. A section without a private key, an address or a peer, or with a field Pipe cannot read, makes its proxy unusable, and says why.

AmneziaWG#

AmneziaWG is WireGuard whose packets do not look like WireGuard's, for networks that block it. Its parameters go in the [WireGuard] section by AmneziaWG's own names, as its app's config files write them, and must be the server's:

[Proxy]
Hidden = wireguard, section-name=Hidden

[WireGuard Hidden]
private-key = YNXtAzepDqRv9H52osJVDQnznT5AL11eVlnvHeYk+Xs=
self-ip = 10.8.1.2
jc = 4
jmin = 40
jmax = 70
s1 = 15
s2 = 68
h1 = 1020325451-1020325500
h2 = 3288052141
h3 = 1766607858
h4 = 2235336242
i1 = <b 0xc700000001><rc 8><t><r 32>
peer = (public-key = bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=, allowed-ips = "0.0.0.0/0, ::/0", endpoint = awg.example.com:51820)
Key Values What it does
jc 0 to 128 Junk packets sent before each handshake.
jmin, jmax 0 to 1280 bytes The least and most bytes of a junk packet.
s1, s2, s3, s4 0 to 1024 bytes Random bytes before the handshake's first message, its answer, a cookie reply and every data packet.
h1, h2, h3, h4 a number, or a range 100-200 Each message's header, picked from its range; the four must not overlap. Without them, WireGuard's 1, 2, 3 and 4.
i1 to i5 tags: <b 0x…> bytes, <r n> random bytes, <rc n> letters, <rd n> digits, <t> the time Packets sent before each handshake, after the junk's, as they say.

These are AmneziaWG 2.0's parameters, which take 1.0's too. AmneziaWG 1.5's j1–j3 and itime, and 3.0's header protection, are not supported: a section with them makes its proxy unusable, and says why. A WireGuard config from the AmneziaVPN app, mihomo's amnezia-wg-option and Amnezia's sing-box (awg endpoints) import with their parameters.

WireGuard networks#

A WireGuard tunnel to an office or a home network is a network to reach, not a way to the internet. With claim-routes, what its peers' allowed IPs hold of the private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, fc00::/7) goes through it ahead of the rules, in every mode — never a default route, nor a public range, which stay the rules' to choose. With dns-domains, names under them are asked of its DNS servers through it, and answered with their real addresses.

[Proxy]
Office = wireguard, section-name=Office

[WireGuard Office]
private-key = YNXtAzepDqRv9H52osJVDQnznT5AL11eVlnvHeYk+Xs=
self-ip = 10.8.0.2
dns-server = 10.8.0.1
claim-routes = true
dns-domains = corp.example.com
peer = (public-key = bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=, allowed-ips = "10.8.0.0/24, 192.168.50.0/24", endpoint = vpn.example.com:51820)
  • A tailnet comes first where both claim an address.
  • A server another proxy names on the network — a proxy reached at 10.8.0.5 — is dialled through it, with nothing to set.
  • Importing a WireGuard file whose allowed IPs are all private sets claim-routes, and dns-domains to the search domains of its DNS line. A full-tunnel file (0.0.0.0/0) stays a proxy that rules choose.

Tailnets are accounts in the app, beside every profile: up to three, in an order you set, each on or off.

  • You sign in in the app — with Tailscale, with an auth key, or at another control server such as Headscale — before the VPN is ever on. The keys are kept in the device's keychain, never in a profile, a sync or an export.
  • While an account is on, its devices' addresses, its MagicDNS names, its split-DNS domains and, with Use subnet routes, the subnets its devices advertise go to it ahead of the profile's rules.
  • Its settings — Use Tailscale DNS, Use subnet routes, Allow local network access, Block incoming connections, Use relays only (no direct connections: everything through Tailscale's relays, which your rules can send through a proxy), this device's name — are the account's, the same whatever profile runs.
  • The exit node for the whole internet is chosen in the app; a tailnet policy's exit-node sends only what rules give it there. Mullvad's exit nodes, where the tailnet has them, are exit nodes like any other, listed by country and city.
  • On a tailnet with Tailnet Lock, only the devices its signing keys signed are reached; the others are listed as not trusted. This device, until a signing device signs it, reaches nothing of the tailnet: the app shows the command that signs it in.

Tunnels through another policy#

On a wireguard line, underlying-proxy names a policy — a proxy or a group — that the tunnel's own datagrams go through. DIRECT is none. A chain that leads back to where it started fails the connection, and says so.

The proxy pool#

The proxies you import go into the pool, not into a profile: from a QR code, a subscription link, share links, or another app's configuration (Surge, Clash, mihomo, sing-box, a WireGuard file).

A profile takes the pool in as a policy-path list named pool.list. The default profile's group does:

[Proxy Group]
Proxy = select, policy-path=pool.list
  • The pool is written out as [Proxy] lines — WireGuard proxies with their sections, SSH proxies with their keys' [Keystore] items — each time a profile is compiled. A subscription refreshed in the pool reaches every profile that uses it.
  • A Tailscale node an import holds is not a proxy of the pool: the tailnet is offered for sign-in instead (Tailnets).
  • A proxy's name is its identity: rules, groups and the pick a select group remembers refer to it. Names are unique across the whole pool. A name that is taken already gets a number — Server 1 2 — and so does a proxy named after a built-in policy (REJECT 2).
  • A subscription is known by its URL. Importing it again, or refreshing it, replaces what it brought last time, keeps its place in the pool, and gives its proxies back their names.
  • A proxy added on its own that is the same as one in the pool is not added again.
  • A proxy that goes through another of the same import follows it to the name it was given.
  • A proxy that cannot be written as a profile line is left out of pool.list, and the app says which, and why.
  • A Surge profile's [Proxy] lines are read exactly as a profile reads them: a line the profile would refuse is not imported, with its reason. A line written for some systems only (#!MACOS-ONLY, #!REQUIREMENT …) and a proxy with a TLS client certificate (client-cert, which lives in the profile's [Keystore]) are not imported either. An SSH proxy's key comes from the profile's [Keystore] into the pool with the proxy.
  • An SSH key in the pool is kept in the proxy, with its passphrase, as a password is; pool.list writes it as a [Keystore] item, once however many proxies use it. A list's [Keystore] items are its own: one whose name the profile has for another key is taken in under a name of its own.