Skip to content

BuildNot yet confirmed elsewhere1 publisher3 min readPublished

iOS has nowhere to put a proxy setting, so the proxy moves into the network

A Tailscale App Connector fronting a sing-box TUN turns plain TCP into SOCKS5 for any tailnet device. The steering is longest-prefix match, and it fails without complaining.

The Engineer · Build desk

How we use AISend a correction

What happened

  • iOS offers no per-app proxy configuration and most apps ignore the system proxy, so the PAC file that solves this on a laptop has nowhere to live.
  • The published design instead puts a Tailscale App Connector in front of a sing-box TUN that converts plain TCP into SOCKS5, with nothing configured on the client.
  • On the connector node, a rule at priority 100 matching traffic arriving on tailscale0 sends nominated prefixes to table 100, whose routes point at singtun0.
  • sing-box carries no route rules at all: final sends everything entering the TUN to a SOCKS5 outbound at 10.0.0.10:1080, bound to tailscale0.

Why it matters

  • capability Devices with no place to configure a proxy get one anyway, because the translation sits on a node the operator controls rather than on the handset.
  • constraint Access is bounded by an enumerated prefix list held in two files, so adding an internal service is a change on the connector rather than a setting a user can flip.
  • exposure The bad state looks like the good state: flows keep completing after the steering is lost, so the usual signal of breakage never fires.
  • decision With no readiness signal from a Type=simple unit, correctness has to be bought with polling inside the helper script instead of an ordering guarantee from systemd.

Longest-prefix match is the whole steering language in this design, and it does more work than the config files suggest. A /32 in the connector's route list beats the same address learned from another subnet router advertising 10.20.0.0/16, so one host gets carved out of an existing network without renumbering anything [10]. The author is straight about the other side of that: delete the /32 and traffic reverts to the old path with no error anywhere [11]. Connections still complete. They just stop going through the corporate proxy, and no client sees a difference.

The destination list also exists twice. Once in the tailnet policy file, where 10.10.0.0/24, 10.20.0.50/32 and 192.0.2.7/32 are nominated for the connector [9], and once in /usr/local/etc/singbox-routes.conf on the node, carrying the same three entries under a comment telling you to keep them in sync [13]. One copy decides what arrives at the connector, the other decides what the connector does with it, and neither validates the other [17].

The rule that does the steering matches on input interface tailscale0 [14], which means packets the connector node originates itself never match it [19]. That is load-bearing in two directions. The SOCKS5 session sing-box opens to 10.0.0.10:1080 is pinned to tailscale0 by explicit bind_interface [2] and cannot be recaptured by the node's own rules, and in any case 10.0.0.10 falls outside all three steered prefixes [18]. It also means a curl from a shell on the connector exercises none of the path you just built.

The ordering bug is the part of the writeup worth keeping. sing-box's unit is Type=simple, so systemd fires ExecStartPost the moment the process forks, before singtun0 exists, and every route add fails with Cannot find device [3]. The ip rule entries install regardless, because rules are not device-bound, so the node ends up with rules present and table 100 empty [4]. Half the configuration lands, which is worse than none of it landing, and the shape of that failure comes directly from Type=simple giving systemd nothing to wait for. The mitigation is a poll loop in the script, checking for the interface in 100ms steps for up to ten seconds and logging how late it showed up [16].

What this buys is a proxy for devices that have no place to configure one, since iOS offers no per-app proxy setting and most apps ignore the system proxy anyway [5]. What it costs is that the translation now lives in a routing table on one Linux box, with auto_route deliberately switched off so sing-box does not manage that table either [12]. Every question a client might ask about whether its traffic went through the proxy has to be answered somewhere else.

What to watch

  • Whether sing-box ships or documents a unit that signals readiness, which would let the route install be a dependency instead of a ten-second poll loop.
  • Whether Tailscale exposes the connector's effective route set on the node, so the policy-file list and the local list can be diffed rather than trusted.
  • What the path does when a competing subnet router's broader advertisement is withdrawn or re-added while the more specific /32 is in place.

Clarity's read

What the record supports and how the coverage leans. The claims behind it follow.

Reality

Evidence58
Adoption10
Hype gap−8
Incentives22
Confidence45
Why these scores

Claim ledger

Ranked by verification strength, evidence, and original report placement.

  1. [1]

    The sing-box inbound is a TUN tagged tun-in with interface_name singtun0, address 172.19.0.1/30, mtu 1500, auto_route false and strict_route false.

  2. [2]

    The sing-box outbound is type socks, version 5, server 10.0.0.10 port 1080, with bind_interface tailscale0; the route block has empty rules and final set to that outbound, so anything routed into the TUN is forwarded without needing a rule of its own.

  3. [3]

    sing-box's unit is Type=simple, so systemd runs ExecStartPost the moment the process is forked, before sing-box has created singtun0, and every `ip route add ... dev singtun0` fails with Cannot find device.

Sources

1 independent publisher whose own reporting we read for this story.

  1. dev.to

    1 article · August 25, 2026

    Transparent SOCKS5 access from iOS with a Tailscale App Connector and sing-box

Share your take

Let Clarity write the post for you.

Signed-in readers get a short post drafted on this story in the register they choose — narrative, analytical, or a direct position — editable to the last word before it goes anywhere. The share buttons at the top of this story work without an account.

Topics and entities

Follow any of these and your For You feed starts watching them — no settings page required.

Topics

Entities

Loading related stories