Pipe Manual

Profile manual

Modules

On this page
  1. Metadata
  2. Arguments
  3. What a module changes
  4. Modules from other apps
  5. In the app

A module is a file that patches the profile in use. It is how a profile you do not edit — a managed one, whose text is replaced at each update — is changed, and how a behaviour is switched on and off with one tap. Modules are .sgmodule files, written in the profile format.

#!name=Quiet banners
#!desc=Refuses an ad server, and answers the banner API with nothing.
#!system=ios
#!arguments=host:ads.example.com,status:200
#!arguments-desc=host: the ad server to refuse. status: what the banner API answers with.

[General]
always-real-ip = %APPEND% *.game.example

[Rule]
DOMAIN-SUFFIX,{{{host}}},REJECT

[Map Local]
^https://api\.example\.com/banner data-type=text data="{}" status-code={{{status}}}

[MITM]
hostname = %APPEND% api.example.com

A module changes what the profile puts in effect, not the profile's text: switched off, it leaves nothing behind.

Metadata#

Lines starting with #! describe the module.

Line What it says
#!name= The module's name, as the app shows it.
#!desc= What it does.
#!system= The systems it is for, separated by commas: ios for the app, mac (or macos) and linux for CLI hosts. Other system names are retained for import compatibility and never match. Without it, it is for all supported targets.
#!requirement= A line requirement's expression: #!requirement=CORE_VERSION>=20. Where it does not hold, the module is not applied; one that cannot be read keeps it from applying, and says why.
#!arguments= Its arguments: name:default, separated by commas.
#!arguments-desc= What the arguments are for, as the app shows it.

Arguments#

#!arguments=host:ads.example.com,status:200 declares two arguments, with their defaults. Before the module is read, {{{host}}} and {{{status}}} are replaced everywhere in it — in any section, in any line — by the values given in the app, or by their defaults. An argument given no value and declared without a default is replaced by nothing. Only declared arguments are replaced: any other {{{…}}} stays as it is.

An expanded module may contain at most 4 MiB of text. One compile has a 64 MiB allowance for prepared modules, including their metadata and source positions. Expansion also has a work limit. Exceeding an allowance refuses the compile with an error; retrying without another module does not reset it.

What a module changes#

A module comes from somewhere else — a URL, a file someone shared — and is applied to whatever profile is in use. It may change how traffic is routed and how names are answered; and it may have more hosts decrypted ([MITM]'s hostname) and run scripts and rewrites on what the hosts decrypted carry — requests and responses, bodies included — or answer requests itself ([Map Local]). That is a great deal to let a file from elsewhere do: what a module decrypts, runs, rewrites and answers is part of what the app shows of it, beside what it was refused.

Everything that decides who can reach Pipe (listeners, the HTTP API, sharing the proxy, forwarded ports), whom it trusts (certificate checks, the decryption CA, [Keystore]), what the tunnel takes in, a tunnel's keys and peers ([WireGuard]), and the profile's own proxies and groups is refused from a module: each such line is left out, said with why, and the rest of the module applies. What a module was refused is part of what the app shows of it, so nothing a module wanted goes unnoticed.

Modules are applied after the profile's own #!include lines and line requirements, in the order they are switched on. A module is one file: its #!include lines are warned about, and left out.

Section What the module does
[General] The keys about routing and names (dns-server, always-real-ip, skip-proxy, …) — each key = value line replaces the profile's value for that key, where the profile's line stands, or is added when the profile has none; key = %APPEND% value adds to the end of the profile's value, key = %INSERT% value to its start, separated by a comma. A key about exposure, trust or the tunnel is refused, and said; a key Pipe does not know is refused too — a module is not a way to add settings.
[MITM] The same, for hostname, hostname-disabled, h2, auto-quic-block and tcp-connection alone. skip-server-cert-verify and the CA keys are refused: they decide which certificates are trusted.
[Ruleset <name>] Its lines join the set of that name — a new set, when the profile has none.
[Rule] Its lines go in front of the profile's own, so the module's rules are tried first. They may only name built-in policies — DIRECT, REJECT, REJECT-DROP and the others — since a module cannot know what the profile's proxies and groups are called, and none may be FINAL, which would end the profile's own rules. A rule naming another policy is warned about, and left out.
[Host], [URL Rewrite], [Header Rewrite], [Body Rewrite], [Map Local], [Script], [Panel], [IP Rewrite] Its lines go in front of the profile's own.
[DNS Server], [DNS Rule] Adds scoped resolvers and prepends ordered DNS rules. A module may reference its own or the profile's resolvers; it cannot replace a profile-owned definition or access another module's private names. Evaluated response tags follow the same scope. Disabling DNS certificate verification is an error, and an invalid module rolls back its DNS definitions and rules together. Its DNS reach is reported in the module summary.
Any other section Refused: [Proxy], [Proxy Group], [WireGuard …], [Keystore], [Port Forwarding], [SSID Setting], [Snell Server], [MTProto] and the rest.

With several modules switched on:

  • lines that go in front come in module order: the first module's lines first, and the profile's after all of them;
  • a setting two modules change is the last one's, since each patches what the ones before it left.
# Profile
[General]
always-real-ip = *.mine.example

# Module 1
[General]
always-real-ip = %APPEND% *.game.example

# Module 2
[General]
always-real-ip = %INSERT% *.first.example

# In effect
always-real-ip = *.first.example, *.mine.example, *.game.example

What compiling says about a module's lines names the module, as it names an included file: ads.sgmodule:7: …. A module that is not applied — for another system, or with a requirement that does not hold — is noted, with why. And a module whose own lines have errors — a rule of a type the format does not have, a pattern Pipe cannot run, a script whose name the profile's own has — is left out whole, said why, and the profile is compiled without it: a module's mistake is not the profile's. What a compile may spend is the one exception: a module whose rule sets, with the profile's, name rule sets past what one compile takes ([Ruleset] sections) fails the compile, as the profile's own lines would. Shared native DNS limits and its parsing allowance follow the same rule; ordinary DNS semantic errors still roll back the module.

Modules from other apps#

A Surge or Shadowrocket module, a Loon plugin (.plugin, .lpx) or a Quantumult X rewrite (.snippet, .conf) is installed as it is: it is converted into a Pipe module, line by line, and what does not come across is listed with why, and kept in the module as a # not converted: comment.

From Becomes
Surge, Shadowrocket The module as it is written, Surge's older spellings in Pipe's; Shadowrocket's reject-200, reject-img, reject-dict and reject-array in [URL Rewrite] become [Map Local] answers.
Loon [Argument] The module's arguments: an input's default, a select's first option, a switch's true or false. A line a switch turns on and off (enable=${switch}, if ${switch} == true) is kept by a line requirement on its value.
Loon [Rewrite], older and newer (request if … then …) reject, reject-200, reject-img, reject-dict, reject-array, reject-video, reject(status): [URL Rewrite]'s reject or a [Map Local] answer. 302, 307, header, redirect(…), url.replace(…): [URL Rewrite]. Header actions: [Header Rewrite], a set being a header-del and a header-add. Body replacements: [Body Rewrite]. JSON actions (json-add, json-replace, json-del, json-jq, json.jq_file): jq lines. mock-response-body, response.body.mock(…): [Map Local], with the response headers set beside it.
Loon [Script], older and newer [Script] lines: http-request, http-response, cron (a cron expression from an argument too), network-changed (an event script) and generic. An argument object ({${a}, ${b}}, [{a},{b}]) is given to the script as JSON text, as Surge modules give one. An older line's pattern matches in any case, as in Loon.
Quantumult X url reject…, url 302, url 307: as Loon's. url script-request-header, script-request-body, script-response-header, script-response-body, script-echo-response, script-analyze-echo-response: [Script] lines. url request-body, url response-body: [Body Rewrite]. url jsonjq-response-body: a jq line. url echo-response: a [Map Local] answer of the file it names. [filter_local] rules, and [task_local] jobs, as [Rule] and cron scripts.
[MitM], [mitm], hostname = … [MITM]'s hostname = %APPEND% ….

What has no counterpart is left out, and said:

  • A rule whose policy is PROXY — the proxy chosen for the plugin in Loon — or any other but a built-in one: a module names built-in policies alone.
  • A condition on what a line cannot choose by: a request's method or header, an answer's status, alternatives (||).
  • Quantumult X's request-header and response-header, which rewrite the whole head with one regular expression, and mocks of a request's body.
  • What decides trust or the tunnel: certificates, skip-server-cert-verify, bypass-tun; servers, policies and remote resources of a whole configuration.

A converted module is checked as Pipe applies it: a line that would keep it from applying — a rule of a type Pipe does not have — is left out, and said, and the rest applies. Scripts written for Quantumult X find its API too ($task.fetch, $prefs, $notify; Scripts).

In the app#

  • A module is installed from a URL or from a file, up to 4 MiB: a Pipe or Surge module, a Shadowrocket module, a Loon plugin or a Quantumult X rewrite (above). What did not come across is shown with it. An add link can offer one, for you to review.
  • What a module names by URL is fetched, kept and fetched again, as the profile's own: its scripts by the engine (Where a script comes from), its rule sets at their update-interval (rules), and the files its lines answer or rewrite with — [Map Local]'s data=, a jq line's jq-path= — by the app, over HTTPS, as soon as the module is on and once a day after; a line whose file has not come yet is left out until it has.
  • It is switched on and off, and the modules switched on are put in the order they apply.
  • Its arguments are given values there; one left empty takes its default.
  • The app says whether each module applied to the profile in use, and why not; what it reaches into — the hosts it decrypts, the scripts it runs, what it rewrites and answers itself; and each line it was refused, with why.
  • The modules switched on patch whichever profile is in use: whether one is on is not part of any profile.
  • One installed from a URL is updated from it when you ask. A new version is shown first by what it would newly do — the hosts it would decrypt, the scripts it would run, what it would rewrite and answer, its rules, the names and settings it would change — and what of it would be refused; how its text changed is there too. It takes the place of the one in use only when you update it.