Skip to content

Let people keep chosen sites out of the tunnel - #76

Merged
DarkPoesidon merged 1 commit into
mainfrom
feat/direct-routing-for-iran
Sep 1, 2026
Merged

Let people keep chosen sites out of the tunnel#76
DarkPoesidon merged 1 commit into
mainfrom
feat/direct-routing-for-iran

Conversation

@DarkPoesidon

Copy link
Copy Markdown
Collaborator

Iranian sites should not travel to another country and back. People were disconnecting the VPN to reach a bank or a government form and then forgetting to turn it on again — and a connection people keep switching off is not protecting them.

Settings → Sites that skip the tunnel, with two lists the user owns: websites and address ranges.

Why not GEOIP

This is the important decision, and it goes against the obvious design.

mihomo can match a country with GEOIP,IR,DIRECT. But NewGEOIP calls InitGeoIP, which downloads a database when one is not on disk and returns an error when it cannot:

if err := geodata.InitGeoIP(); err != nil {
	log.Errorln("can't initial GeoIP: %s", err)
	return nil, err          // ← a rule that fails to parse fails the whole config
}

Nothing ships that database with this app — cores/ holds the engine and wintun.dll, and the geoip.dat in the Makefile is staged for the legacy Xray core, not mihomo.

So on a filtered network, on the first connection after an install, a GEOIP rule would not route Iranian traffic around the tunnel. It would stop the tunnel existing, for exactly the people who most need it.

Two lists cost nothing and cannot fail. ir as a domain suffix covers every .ir address with no database anywhere — most of what this was asked for — and the rest is names people add as they meet them, which also makes the feature useful to someone whose list has nothing to do with Iran.

The starting list ships off, but not empty

ir · digikala.com · aparat.com · varzesh3.com · divar.ir · snapp.ir · shaparak.ir

The switch is the decision; the list is the explanation of what the switch does. An empty one behind it teaches nothing and leaves a person guessing at the format. Off by default, because an update must not move anybody's traffic without them choosing it. Every entry is ordinary — editable, removable, no more privileged than one added later.

Turning the switch off keeps the lists, so testing whether a site is misbehaving does not cost retyping them.

Order is the whole of a rule list's meaning

Direct rules are matched before the split tunnel. Both can send traffic direct, so under bypass mode the order changes nothing — but under "only these programs are tunnelled" it decides whether a tunnelled program's Iranian traffic still goes around. It should: somebody who named their bank has named it whichever program opens it.

TestDirectRulesAreMatchedBeforeTheSplitTunnel fails if the two are swapped — confirmed by mutation, not just asserted.

Details worth reviewing

  • Address rules carry no-resolve. Without it every request resolves before routing and hands the name to whichever resolver answered — the leak this app closes elsewhere.
  • What people type is not what mihomo matches. A leading dot, a scheme, a path, a port, stray capitals: all trimmed rather than rejected, because each could have meant exactly one thing and a validation message would only teach a syntax to satisfy this field. A bare address gets its own /32 (or /128).
  • The warning sits with the lists, not in a tooltip. Anything listed leaves with the user's real address; somebody who has just sent their bank around the tunnel should know that before closing the page.

Shape

Three parameters became a Routing struct on the way through — one decision made in three parts, read together at the one place that orders them into a rule list. BuildProxiesYAML and BuildProxiesYAMLWithChain stay as thin wrappers, so no existing caller moved and no test churned.

Testing

12 new tests: input normalisation, rule shape and ordering, settings behaviour, and one that puts the generated rules through the real engine's parserValidateConfig only catches what fails to parse, which is exactly the class of fault a malformed rule would be. That one runs green against a locally built core.

Full suite green, go vet clean, tsc --noEmit clean, frontend builds.

Noted, not touched

V2RaySettingsProfile.IranRoutingEnabled exists on the legacy Xray profile and is read by nothing — another stored-and-never-read control, like the four this codebase has already found. It predates this change and removing it is its own.

🤖 Generated with Claude Code

Asked for directly: Iranian sites should not travel to another country
and back, and people were disconnecting the VPN to reach a bank or a
government form and then forgetting to turn it on again. A connection
people keep switching off is not protecting them.

It is the user's own two lists — websites and address ranges — rather
than a country switch, and that is a deliberate refusal of the obvious
design. mihomo can match a country with GEOIP, but NewGEOIP calls
InitGeoIP, which downloads a database when one is not on disk and returns
an error when it cannot, and a rule that fails to parse fails the whole
configuration. Nothing ships that database with this app. So on a
filtered network, on the first connection after an install, a GEOIP rule
would not send Iranian traffic around the tunnel — it would stop the
tunnel existing, for exactly the people who most need it.

Two lists cost nothing and cannot fail. "ir" as a domain suffix covers
every .ir address on its own with no database anywhere, which is most of
what this was asked for, and it also makes the feature useful to somebody
whose list has nothing to do with Iran.

The starting list ships off but not empty. The switch is the decision;
the list is the explanation of what the switch does, and an empty one
behind it teaches nothing and leaves a person guessing at the format. Off
by default because an update must not move anybody's traffic without them
choosing it.

Order is the whole of a rule list's meaning. These are matched before the
split tunnel, not after: both can send traffic direct, so under bypass
the order changes nothing, but under "only these programs are tunnelled"
it decides whether a tunnelled program's Iranian traffic still goes
around. It should — somebody who named their bank has named it whichever
program opens it. There is a test that fails if the two are swapped.

Address rules carry no-resolve, or every request would resolve before
routing and hand the name to whichever resolver answered, which is the
leak this app closes elsewhere.

What people type is not what mihomo matches, so a leading dot, a scheme,
a path, a port and stray capitals are all trimmed rather than rejected:
each could have meant exactly one thing, and a validation message would
only be teaching someone a syntax to satisfy this field.

The warning sits with the lists rather than in a tooltip. Anything listed
here leaves with the user's real address, and somebody who has just sent
their bank around the tunnel should know that before they close the page.

Three parameters became a Routing struct on the way through. They are one
decision made in three parts, read together at the one place that orders
them into a rule list, which is where getting it wrong is the whole
failure mode. The old two builders stay as thin wrappers, so no existing
caller moved.

Not touched: V2RaySettingsProfile.IranRoutingEnabled, which is on the
legacy Xray profile and is read by nothing. It predates this and removing
it is its own change.
@DarkPoesidon
DarkPoesidon merged commit ef05be9 into main Sep 1, 2026
7 checks passed
@DarkPoesidon
DarkPoesidon deleted the feat/direct-routing-for-iran branch September 1, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant