Profile manual
The file
On this page
A profile is a text file in UTF-8. It is read line by line, and every line is kept exactly as it was written — comments, blank lines, order, line endings (LF or CRLF), a byte-order mark, sections Pipe does not know — so that a profile edited in the app comes back byte for byte, except where it was changed.
#!MANAGED-CONFIG https://example.com/pipe.conf interval=43200
[General]
loglevel = notify
dns-server = system, 9.9.9.9 // the network's resolvers, and one more
[Proxy]
Server 1 = trojan, server1.example.net, 443, password=secret
#!REQUIREMENT SYSTEM=='iOS' Phone = socks5, 192.168.1.20, 1080
[Rule]
#!include rules/streaming.conf
FINAL,Server 1
Sections#
A line [Name] opens a section, and the lines after it, up to the next
such line, are its lines.
- The names of sections are matched in any case, with spaces around them
ignored:
[proxy group]is[Proxy Group]. - Two kinds of section carry a name of their own after a space:
[WireGuard <name>]and[Ruleset <name>]. The name keeps its case, and may hold spaces:[Ruleset My List]. - A section may be written more than once. Its parts are read as one section, in the order of the file.
- A line that says something but stands before the first section is left out, with a warning. Comments and directives may stand there.
The sections Pipe reads:
| Section | What it holds | Described in |
|---|---|---|
[General] |
Settings for the whole profile | General |
[Proxy] |
Proxies | Proxies |
[Proxy Group] |
Groups | Groups |
[Rule] |
Rules | Rules |
[Ruleset <name>] |
A list of rules, used as RULE-SET,<name>,… |
Rules |
[Host] |
Local answers and servers for names | DNS |
[URL Rewrite] |
URLs rewritten, redirected or refused | The HTTP engine |
[Header Rewrite] |
Request and answer headers changed | The HTTP engine |
[Body Rewrite] |
Request and answer bodies changed | The HTTP engine |
[Map Local] |
Requests answered locally | The HTTP engine |
[MITM] |
HTTPS decryption | The HTTP engine |
[Script] |
Scripts | Scripts |
[Panel] |
Panels in the app | Scripts |
[SSID Setting] |
What changes on some networks | Networks |
[IP Rewrite] |
Packets the tunnel answers itself | Networks |
[Port Forwarding] |
Ports forwarded through a policy | Networks |
[Keystore] |
Certificates and keys | Proxies |
[WireGuard <name>] |
A WireGuard proxy's settings | Proxies |
Sections that are kept in the file but not used:
| Section | Said as | Why |
|---|---|---|
[Snell Server], [MTProto], on iOS |
nothing | Built-in servers run on a computer; a phone passes them over, as another platform's settings. |
[DHCP], on a phone |
nothing | It belongs to a computer that serves a network as its gateway, where it is read. |
[Ponte] |
warning | Surge Ponte configuration is not supported. |
[Testing] |
note | Throughput testing is not supported yet. |
[Replica], any other name |
note | It is not a section of Pipe's format. |
The message names the section and how many lines it holds:
[Testing] (2 lines): throughput testing is not supported yet; kept in the file, not used.
Lines#
What a line says depends on its section. There are four shapes.
| Shape | Sections | Example |
|---|---|---|
key = value |
[General], [MITM], [Host], [IP Rewrite], [WireGuard <name>], [SSID Setting] (its settings, comma-separated) |
loglevel = notify |
Name = type, …, key=value, … |
[Proxy], [Proxy Group], [Script], [Panel], [Keystore] |
Server 1 = trojan, server1.example.net, 443, password=secret |
TYPE,VALUE,POLICY,options |
[Rule], [Ruleset <name>], and rule-set files |
DOMAIN-SUFFIX,example.com,Proxy |
| Words separated by spaces | [URL Rewrite], [Header Rewrite], [Body Rewrite], [Map Local], [Port Forwarding] |
^http://example\.com https://example.com 302 |
- A
key = valueorName = …line is split at its first=that is not inside quotes. Spaces around it are taken off. A name keeps its case and may hold spaces (Server 1). - After the
=, components are separated by commas that are not inside quotes or parentheses. Spaces around each component are taken off. - A component holding
=is akey=valueparameter. The keys of parameters are read in any case. A parameter given twice on one line counts once: the last one given. - In
[General]and[MITM], a key given on two lines counts once: the later line. - An empty component is skipped:
a, , bisa, b.
Comments#
- A line whose first characters, after any spaces, are
#,;or//is a comment. - On a line that says something,
#,;or//after a space or a tab, and outside quotes, starts a comment that runs to the end of the line:dns-server = 8.8.8.8 // a public server. - Without a space before it, it belongs to the value:
test-url = http://example.com/#topkeeps its#top, andport-hopping=1234;5000-6000its;. - A line starting with
#!is not a comment but a directive:#!include, line requirements,#!MANAGED-CONFIG, or a module's metadata.
Quoting#
A value is quoted when it holds what would otherwise end it.
"…"holds its text; inside it,\"is a quote and\\a backslash.'…'holds its text exactly as written.- A quote opens a quoted value only where a value begins: at the start of
a component, after
=, after(, or after the spaces that follow one of them; in a line of words, at the start of a word or after=. An apostrophe inside a name is a letter:Yu's Server = …needs no quotes. - Inside quotes, commas,
=, parentheses and comment marks are part of the value. - A parameter's value in double quotes may hold quotes of its own, as
modules write a JSON argument:
argument="{"lang":"en","debug":true}". It ends at the first quote after which the line ends or the next parameter begins (, key=, orkey=in a line of words).
Quote a value that holds a separator (a comma; an =; in a rule's
value, a parenthesis), a quote, text that would read as a comment
( #, ; or //), one that begins or ends with a space, or one that
would begin a comment, a directive or a section (#, ;, //, [).
What the app writes — an edit, an export, the pool as a list — quotes
what needs it, the same way for every writer, and reads it back before
keeping it; a value with a line break or another control character
cannot be written on a line at all, and is refused.
[Proxy Group]
Streaming = select, "Server 1, premium", JP 01, policy-regex-filter="(Server|JP) #1"
[Rule]
URL-REGEX,"^https?://ads\.example/(a,b)",REJECT
In the sections written as words — the rewrite sections and
[Map Local] — a word that holds spaces is quoted, and so may be the
value of a parameter: data="{ \"ok\": true }".
Kinds of values#
Lists#
A list is written with commas: dns-server = system, 9.9.9.9. An item
that holds a comma is quoted. Empty items are skipped.
True and false#
A setting that is on or off takes true or false, and also 1 or
0, yes or no, on or off, in any case. A value that is none of
these is warned about, and the setting keeps its default.
Numbers#
Durations are seconds unless a setting says otherwise, and may have a
fraction where the setting says so (test-timeout = 2.5). Sizes are
bytes. A number that cannot be read is warned about, and the setting
keeps its default.
Host lists#
Several settings take a list of hosts — always-real-ip,
always-raw-tcp-hosts, force-http-engine-hosts, and [MITM]'s
hostname among them. Its
items are tried in order, and the first that matches decides.
| Item | Matches |
|---|---|
example.com |
That name, in any case. |
*.example.com, api-?.example.com |
Names by pattern: * is any run of characters, dots included; ? is one character. |
192.168.1.10 |
That address, as written. |
<ip-address> |
A destination given as an address rather than a name; <ipv4-address> and <ipv6-address> one of each family. |
<simple-hostname> |
A name without a dot. |
…:8080 |
After any of the above: that port only. |
…:0 |
After any of the above: every port. |
-… |
In front of any of the above: what matches is left out. |
An item without a port means the ports the setting is about — port 80
for the HTTP engine, 443 for HTTPS decryption — as each setting says.
Since the first item that matches decides, exclusions go first:
hostname = -pinned.example.com, *.example.com. An item that cannot be
read is left out, with a warning, and the rest of the list stands.
Regular expressions#
Rules (DOMAIN-REGEX, URL-REGEX), the rewrite sections, scripts'
patterns and groups' filters take regular expressions.
- A pattern is found anywhere in the text it is tried on, unless it
anchors itself with
^and$. - The usual syntax is there: character classes (
[a-z],\d,\w,\s), repetition (*,+,?,{2,5}), alternation, groups and named groups ((?P<name>…)), and flags such as(?i). - The patterns that choose requests by their URL — the rewrite sections',
[Map Local]'s and scripts' — may look around ((?=…),(?!…),(?<=…),(?<!…)) and refer back (\1), as other apps' modules are written; a match that would take more than 100,000 steps of going back is no match. Rules, groups' filters and what a header or body rewrite finds have neither. A rewrite or a script whose pattern Pipe cannot use is left out, with a warning that says why; a rule, or a group's filter, with such a pattern is an error. - In a replacement,
$1is what the first group captured ($1ais group 1 followed bya),\$is a dollar sign and\\a backslash.
Ports#
Rules on ports take one port (443), a range (8000-9000), or a bound:
>=1024, >1024, <=1024, <1024.
Networks#
Subnet groups, SUBNET rules and [SSID Setting]
name networks by subnet expressions: SSID:Home*, TYPE:CELLULAR,
ROUTER:192.168.1.1. Networks
describes them.
Files and URLs a profile names#
A profile may name other files: by a path relative to the folder the
profile is in (rules/streaming.conf), or by a URL.
| Where | A file | A URL |
|---|---|---|
#!include |
read when the profile is compiled | downloaded by the app, and kept current (linked profiles); not read by the CLI |
policy-path |
read when the profile is compiled | downloaded by the app, and kept current |
RULE-SET, DOMAIN-SET, [Host] lists |
read when the profile is compiled | downloaded by the engine, and kept current |
script-path |
read when the profile is compiled | downloaded by the engine, and kept current |
[Map Local] data, jq-path |
read when the profile is compiled | not downloaded yet: the line is left out |
On iPhone, the app keeps each profile as one file and keeps no other files beside it yet, so a profile there takes what it names from URLs. A file or a list that is not there when the profile is compiled is said, with a warning, on the line that names it.
What compiling reads is limited: the files a profile names may come to
64 MiB in all, each counted once however often it is named. What is
copied out of them is counted too, every copy, so that one file named
from many lines costs what as many files would: what #!include brings
in may come to 64 MiB, and so may the scripts, [Map Local] answers and
jq programs written into the compiled configuration. A file past a limit
is not read, or not copied, and the line that names it is left out, with
a warning.
What the profile compiles to has to fit where Pipe runs. On iOS, whose VPN
extension the system gives little memory, that is 16 MiB held in the tunnel
and a file of 64 MiB; on CLI hosts it is 256 MiB. A profile past it is
refused, saying how much it came to. About 25,000 proxies with 150,000 rules
written out fit on a phone. The lists in files, the scripts and the
[Map Local] answers it names are written into it; a large list is better
named by its URL, which Pipe fetches itself and keeps in a file, at almost
no cost to the tunnel.
#!include#
#!include brings lines of other files into a profile, where it is
written:
[Rule]
#!include rules/streaming.conf, "rules/work lists.conf"
FINAL,Proxy
- It is written inside a section, and brings in that section of each
file it names — the
[Rule]section ofrules/streaming.confabove — in place of the#!includeline. The other sections of the file are not read. - Several files are separated by commas; a name with a space in it is quoted.
- In
[Ruleset *]and[WireGuard *], it brings in every section of that kind the file has, each under its own name. Such a section holds only#!includelines; any other line in it is left out, with a warning. - An included file cannot include others: an
#!includein it is left out, with a warning. - A file is included into a section once: a second
#!includeof it in the same section — on the same line or another — is left out, with a warning. - A file that is not there, or that has no section of the kind asked for, includes nothing, and says so.
- What is said about an included line names the file and its line:
rules/streaming.conf:4: ….
The app's settings edit the profile's own lines; included lines are edited in their own files.
Linked profiles#
A managed profile is replaced whenever it is kept current, so what you
add to it is lost. Linked to it instead: a profile of your own whose
sections #!include the managed profile's by its URL, with your lines
beside them.
[Proxy]
#!include https://example.com/managed.conf
Mine = http, proxy.example, 8080
[Proxy Group]
#!include https://example.com/managed.conf
- The app downloads the file and keeps it current, as it does a
policy-pathlist, and the profile is compiled again when it changes. - Only the sections that say
#!includetake anything from it. Write your own[Rule]lines before its#!include: itsFINALends the rules. - Its
#!MANAGED-CONFIGline stays with it: your profile is not itself managed. - An
externalproxy cannot be used from a managed profile or a file at a URL, whatever was approved: those change without anyone reading them. Write the line in your own profile. - Until it has been downloaded, nothing is included from it, and that is said.
Line requirements#
A line requirement keeps a line only where it holds. It is written at the start of a line, followed by the line:
[Proxy Group]
#!REQUIREMENT CORE_VERSION>=22 Auto = smart, Server 1, JP 01
Auto = url-test, Server 1, JP 01 //!REQUIREMENT CORE_VERSION<22
Desk = select, Server 1, DIRECT #!MACOS-ONLY
or at the end of a line, after a space, as #!REQUIREMENT … or
//!REQUIREMENT …. #!IOS-ONLY keeps a line in the app;
#!MACOS-ONLY and #!LINUX-ONLY select the corresponding CLI host.
Foreign guards (#!TVOS-ONLY, #!ANDROID-ONLY, #!HARMONYOS-ONLY,
#!WINDOWS-ONLY) are recognized for imported profiles and never apply.
A line whose requirement does not hold is read as if it were not there;
one whose requirement cannot be read is left out, with a warning.
A requirement with no line after it is a comment.
The expression is one word, or a phrase in double quotes:
#!REQUIREMENT "SYSTEM=='iOS' AND CORE_VERSION>=22" …. It is made of:
| Part | Written |
|---|---|
| Variables | CORE_VERSION, SYSTEM, SYSTEM_VERSION, DEVICE_MODEL, LANGUAGE, DEVICE_NAME, in any case |
| Values | text in quotes ('iOS'), numbers, true, false |
| Comparisons | == or =, != or <>, >, >= or =>, <, <= or =<: SYSTEM_VERSION as a version, number by number (18.6.2 is later than 9 and earlier than 18.10); otherwise as numbers when both sides are numbers, as text when not |
| Text tests | BEGINSWITH, ENDSWITH, CONTAINS; LIKE (a pattern with * and ? over the whole value); MATCHES (a regular expression that matches the whole value) |
| Logic | AND or &&, OR or ||, NOT or !, and parentheses |
What the variables are on Pipe:
| Variable | Value on iPhone |
|---|---|
CORE_VERSION |
6010000: the version of the format Pipe reads, as one number |
SYSTEM |
iOS in the app; macOS or Linux for the compiler on a CLI host |
SYSTEM_VERSION |
the system's version: 26.0, 18.6.2 |
DEVICE_MODEL |
the model's identifier: iPhone17,1 |
LANGUAGE |
the first of your preferred languages: en-US, pt-BR |
DEVICE_NAME |
iPhone or iPad: iOS does not tell apps the name you gave the device |
[Proxy Group]
#!REQUIREMENT "DEVICE_MODEL BEGINSWITH 'iPad'" Tablet = select, Server 1, JP 01
[Rule]
#!REQUIREMENT "LANGUAGE LIKE 'fr*'" DOMAIN-SUFFIX,fr,DIRECT
#!MANAGED-CONFIG#
A profile that a provider keeps up to date says where it comes from:
#!MANAGED-CONFIG https://example.com/pipe.conf interval=86400 strict=false
| Part | Meaning | Default |
|---|---|---|
| The URL | Where the profile is fetched again from: http:// or https://. |
— |
interval |
Seconds between fetches. | 86400 (a day) |
strict |
true: switching Pipe on fetches an overdue profile first. |
false |
The line may stand anywhere in the file; the first one counts.
How the app keeps a managed profile current:
- It fetches the profile in use again once its interval has passed: when the app comes to the front, when iOS gives it time in the background, when the tunnel comes up, and — while the app stays in front — at the moment it falls due. Nothing is fetched on a timer of its own, and nothing waits while the app is in the background.
- While the app stays in front, it waits at least five minutes between checks, whatever the interval says. A fetch that failed is tried again after ten minutes, or after the interval when that is shorter.
- It asks with what the server said last time (
ETag,Last-Modified), so an unchanged profile costs one short request. - What comes back replaces the profile's text, whole. A change made to a managed profile is therefore lost at its next update: change it with a module, which patches whatever profile is in use.
- What comes back must be a profile: in Pipe's format, or another app's, which is converted again as it was when the profile was added (Importing profiles). If it is not a profile, the profile is kept as it was, and the app says why.
- Only the profile in use is kept current. Another one is brought up to date when it is put in use.
- With
strict=true, switching Pipe on fetches an overdue profile first, for eight seconds at most. The tunnel starts either way, and a failed fetch is said.
The app shows where each thing it keeps current stands — when it was last updated, how often it is, why the last attempt failed — and can update it on request.
What a profile fetches#
| What | Fetched by | Again every | Set with |
|---|---|---|---|
| The profile itself | the app | a day | #!MANAGED-CONFIG … interval= |
policy-path lists |
the app | a day | the group's update-interval |
RULE-SET and DOMAIN-SET lists, [Host] lists |
the engine, through the rules | a day | update-interval= on the line |
| Scripts | the engine, through the rules | a day | script-update-interval |
| The country database | the engine, through the rules | a week | disable-geoip-db-auto-update turns it off |
| The ASN database | the engine, through the rules | a week | — |
| Modules installed from a URL | the app | when you ask | — |
"Through the rules" means the download goes where the rules send its host, like any other connection.