Documentation
¶
Overview ¶
Package routerule decides what a flow does: take the tunnel, go direct, or be refused.
It lives here rather than in either mobile client because both of them ask the same question at the same moment. The iOS packet tunnel and the Android debug VpnService both hand their packets to the userspace stack in mobile/core, and every flow that stack accepts arrives at one forwarder with a destination and, once DNS is intercepted, a name. One matcher there is one set of semantics to get right and one set of tests to keep it right; two would be a parity script and an argument about which is correct.
The syntax is the one the rule files in circulation already use -- Clash, mihomo, sing-box, Shadowrocket all read a list of `TYPE,VALUE,ACTION` lines -- because the point of this is that a user can bring the list they already maintain. Where those tools disagree with each other this follows the majority and says so at the rule concerned.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Parse ¶
Parse reads a rule list.
The accepted form is one rule per line, `TYPE,VALUE,ACTION`, with FINAL taking `FINAL,ACTION` because it has nothing to match on. Blank lines and lines beginning with # or ; are ignored, as is anything after a trailing `,no-resolve`, which is recorded rather than rejected. Type and action names are case-insensitive; a comma inside a value is not supported by any tool using this syntax and is not supported here.
A list with problems still returns the rules that parsed, in order. That is what lets a caller show "these 4 lines of your 900 did not load" instead of refusing the file, and it is why Problem carries the line number.
Types ¶
type Action ¶
type Action uint8
Action is what happens to a flow that matches.
const ( // Proxy sends the flow through the tunnel. It is the zero value because it // is what this transport exists to do: a flow nothing matched is a flow // nobody made a decision about, and carrying it is the choice that cannot // leak. Proxy Action = iota // Direct sends the flow out the ordinary interface. Direct // Reject refuses the flow without dialing anything. Reject )
type Countries ¶
type Countries interface {
// Contains reports whether addr is registered to the two-letter code,
// which is upper-case. An unknown code reports false rather than an error:
// a rule naming a set this build does not carry must not decide the flow.
Contains(code string, addr netip.Addr) bool
}
Countries answers whether an address belongs to a country's registered space. The bundled set that scripts/generate_cn_geoip.py packs is one implementation; a test supplies another.
type Flow ¶
type Flow struct {
// Domain is the name the flow was opened to, empty when it is not known.
// It is known when DNS was intercepted and the destination is one of the
// addresses handed out for a name; it is not known for a flow opened to a
// literal address.
Domain string
// Addr is the destination address, invalid when the flow has a name that
// has not been resolved yet.
Addr netip.Addr
// Port is the destination port.
Port uint16
}
Flow is what is known about a connection at the moment the decision is made.
type Packed ¶
type Packed struct {
// contains filtered or unexported fields
}
Packed answers GEOIP rules from the fixed-width set that scripts/generate_cn_geoip.py produces from registry delegation data. It is the same file mobile/ios/PacketTunnel/Resources/cn-direct.bin, read by the same rules as mobile/ios/Shared/CountryRoutes.swift, and scripts/ test_cn_geoip.py holds the generator's half of the contract.
The blob is kept as bytes and searched in place rather than parsed into prefixes. The China set is about seven and a half thousand v4 blocks, which is a quarter of a megabyte once it is netip.Prefix values, and this has to stay resident to answer a rule on every flow -- unlike the Swift reader, which builds the routes once at connect and drops them. docs/MOBILE-MEMORY.md is the budget that makes the difference matter: the packet-tunnel extension shares a fixed profile with the Go runtime.
The entries are fixed-width and the generator emits them sorted and collapsed, which is what makes a binary search over the raw bytes possible at all. Load verifies both properties rather than trusting them, because a set that is not sorted answers "no" for addresses it holds, and a GEOIP,CN,DIRECT rule that answers "no" sends Chinese traffic through the tunnel silently -- the exact failure this feature exists to remove.
func LoadPacked ¶
LoadPacked reads a packed set and binds it to a two-letter country code. The file itself does not name the country it holds -- it is generated per country and shipped under a name that says which -- so the caller supplies it.
func (*Packed) Blocks ¶
Blocks reports how many blocks the set carries, so a screen can say how heavy a toggle is without holding the prefixes.
func (*Packed) Contains ¶
Contains reports whether the address is in the set this file carries.
A code other than the one loaded reports false. A rule naming a set the build does not ship must not decide the flow -- it falls through to whatever the list says next, which is the same thing that happens when no set is loaded at all.
type Problem ¶
Problem is a line that did not become a rule, and why.
Parsing reports rather than drops. A rule list is a security boundary -- every line in it is somebody saying "this must not take the tunnel", or "this must" -- and a parser that skips what it does not understand turns a typo into traffic going somewhere the user did not intend, silently and for as long as the file lives. The caller decides whether to refuse the list or to run it and show what was lost; it cannot decide either without being told.
type Rule ¶
type Rule struct {
Kind Kind
Action Action
// Domain carries the name for the three name kinds, already lowered and
// stripped of a trailing dot so that matching does no work per flow.
Domain string
// Prefix carries the block for IP-CIDR.
Prefix netip.Prefix
// Country carries the two-letter code for GEOIP, upper-cased.
Country string
// Port carries the destination port for DST-PORT.
Port uint16
// NoResolve is carried because the rule files in circulation set it and a
// parser that rejected it would refuse lists that work everywhere else. It
// says an address rule must not trigger a name lookup to be evaluated,
// which is already true here: this matcher never resolves anything, it is
// given what the flow already knows. Kept so a round trip through parse and
// format does not silently rewrite a user's file.
NoResolve bool
}
Rule is one line of a rule list.
type Set ¶
type Set struct {
// contains filtered or unexported fields
}
Set is a parsed rule list, ready to match.
func (*Set) Match ¶
Match returns the action for a flow and the rule that decided it.
First match wins, which is what every tool using this syntax does, and is why a list is ordered rather than a set. A rule whose input the flow does not carry is skipped rather than treated as a miss: a name rule cannot speak about a flow with no name, and skipping it lets the same list serve the lookup that has a name and the connection that only has an address.
A flow that matches nothing takes Proxy. A list that wants otherwise ends with FINAL, and every list in circulation does.
func (*Set) WithCountries ¶
WithCountries returns the set bound to a country lookup. A set with none evaluates GEOIP rules as misses rather than refusing to build, so a build that ships without the packed set still runs every other rule in the file.