Chute Devs

Proxy Chains and Relay Groups — Routing Through a Chain

Hello everyone.

Today we are introducing proxy chains to the Chute engine, on every platform: a policy whose own connection is carried by another policy, a relay group that strings members into one path, and the random group that Shadowrocket users already know. All three are live in the policy editors on iOS and macOS, and all three import from the configs you already have.

Why a Chain

Some servers are only reachable from certain places. A relay or landing server may be reachable from one region but not from where you actually are; a home-rolled entry is reachable only from your own VPS; sometimes the second hop simply gives a better route to the target. The classic fix is to stack clients, or to run one client that only speaks “connect through this other server first”. Chute now does it inside the engine: one config, one rules engine, one set of policies — and any policy can sit on top of another.

underlying-proxy: One Policy Riding Another

The core is one new common parameter on a proxy policy:

1
2
3
[Proxy]
Entry = trojan, entry.example.com, 443, password=...
Tunnel = ss, exit.example.com, 8388, method=aes-256-gcm, password=..., underlying-proxy=Entry

Tunnel reaches its server through Entry: the engine builds the upstream connection first, then connects to Tunnel‘s server across it. The upstream can itself have an upstream, so arbitrary multi-hop is possible — bounded at sixteen levels of nesting, after which the connection is refused rather than unfolded forever.

Three properties of this design are worth spelling out, because they are what make a chain safe to use:

  • Fail-closed, never silently direct. If a named upstream is missing, if a member of the path cannot be chained, or if a loop makes the path unusable, the connection through it is refused and the reason is logged. Chute will not quietly fall back to a direct connection that leaks your traffic around the chain.
  • The chain resolves the server’s domain. A chained policy’s server hostname is looked up by the upstream, not by your local resolver — and Chute does not ping a chained server directly. This matters when the server’s name would resolve differently (or not at all) from where you actually sit.
  • Latency tests walk the real path. Health checks and url-test/fallback/load-balance probes reach a chained member through the chain, so the number you see is the latency a real connection would get.

Not every protocol can be chained. A policy whose server connection is not a plain TCP stream — SSH, WireGuard, AmneziaWG, Tailscale, AnyTLS, Hysteria2, TUIC, MASQUE, and XHTTP riding HTTP/3 — cannot take an upstream. If you set underlying-proxy on one of those, connections through it are refused with a log line, never sent direct. UDP is also not forwarded across a chain: traffic to such a policy follows your udp-policy-not-supported-behaviour setting (direct or reject — your call).

Loops are handled at two levels. Fixed references that form a cycle — including one that wraps around through a relay group’s first member — are rejected when the configuration loads, before any traffic flows. A cycle that only exists because of a group’s current selection is not a permanent error: pick different members and the policy becomes usable again, but while the cycle exists, connections through it are refused.

Relay Groups: A Path Written as a Group

The chain above attaches an upstream to a policy. The relay group writes the whole path as a group, in reading order:

1
2
[Proxy Group]
Relay = relay, Entry, Exit

A connection through Relay goes: device → Entry → Exit → target. The target sees Exit as its source. This is the same member order Clash and mihomo use, so a relay group in an existing config imports as a relay group with its order intact.

The details that matter in practice:

  • The first member can be any protocol. Every member after it must itself be chainable, since it has to reach its own server across the members before it. If one cannot — including if a nested group’s current pick is not chainable — the connection is refused and logged, never sent direct.
  • Members after the first ignore their own underlying-proxy: the members ahead of them are their path. A relay group nested inside another expands in place, and the whole assembled path is capped at eight hops.
  • Only the first member is reached directly; the rest are never pinged from the device. The group carries TCP only, has no health check, and no selectable members — it is a path, not a picker.

Random Groups: Shadowrocket’s random, Kept

Chute also reads Shadowrocket’s random groups now: every new connection picks a member at random, with equal probability and no memory of the last pick. UDP only picks among members that can relay UDP — if none can, the group does not carry UDP and your udp-policy-not-supported-behaviour decides. Members accept the same forms as a select group, including policy-provider: references, and a member’s own underlying-proxy is kept. There is nothing to switch: the web console and the control API list its members but refuse to change the pick, and HIDDEN=true hides the group like any other.

One Upstream for a Whole Group

A third way to express the idea is on the group itself:

1
Airport = select, policy-path=https://example.com/nodes.list, underlying-proxy=Entry

Every proxy member of the group then rides Entry, appearing as a derived policy named Member (via Entry). This replaces the members’ own upstreams; group members, DIRECT and REJECT pass through untouched, and the original policies remain usable elsewhere in the config.

Imports

If you are moving a config from another client, the chain arrives with it:

  • mihomo’s dialer-proxy and sing-box’s detour on a proxy import as underlying-proxy.
  • A proxy provider’s override: dialer-proxy imports as the provider’s underlying-proxy, so every node it supplies rides the same upstream.
  • Clash and mihomo relay groups import as relay groups, member order unchanged.

And if you would rather point a whole subscription at one upstream, the group-level underlying-proxy above is the Surge-compatible way to do it.

In the Apps

The policy editors on Chute iOS and Chute macOS can edit all of this: set or clear an upstream on a policy, build and edit relay groups, and add random groups. Chute Dashboard, which shows traffic per policy, now also keeps hidden policies out of the list. The full reference, including every parameter and the exact refusal rules, is in the Chute Manual.

Thanks.

Chute Devs