<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michael Landin</title>
    <description>The latest articles on DEV Community by Michael Landin (@michbsd).</description>
    <link>https://dev.to/michbsd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4093615%2F438ea7b9-05c0-4bc9-b330-3f58eca61cd3.jpg</url>
      <title>DEV Community: Michael Landin</title>
      <link>https://dev.to/michbsd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/michbsd"/>
    <language>en</language>
    <item>
      <title>Transparent SOCKS5 access from iOS with a Tailscale App Connector and sing-box</title>
      <dc:creator>Michael Landin</dc:creator>
      <pubDate>Tue, 25 Aug 2026 07:29:56 +0000</pubDate>
      <link>https://dev.to/michbsd/transparent-socks5-access-from-ios-with-a-tailscale-app-connector-and-sing-box-2fhd</link>
      <guid>https://dev.to/michbsd/transparent-socks5-access-from-ios-with-a-tailscale-app-connector-and-sing-box-2fhd</guid>
      <description>&lt;p&gt;Some internal services are only reachable through a corporate SOCKS5 proxy. On a laptop, you handle that with a PAC file. On iOS you can't: there's no per-app proxy configuration, and most apps ignore the system proxy anyway.&lt;/p&gt;

&lt;p&gt;This is how to make those destinations work transparently on any tailnet device — no PAC file, no proxy settings, no per-app configuration — by putting a Tailscale App Connector in front of a sing-box TUN that translates plain TCP into SOCKS5.&lt;/p&gt;

&lt;p&gt;Everything below is anonymized. Substitute your own addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The path
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client (Mac / iOS)
    ↓  Tailscale decides this destination uses the connector
App Connector node (gw)
    ↓  tailscale0
    ↓  policy routing, table 100
singtun0
    ↓  sing-box
    ↓  SOCKS5 to 10.0.0.10:1080
Target service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client sends normal traffic to a normal address. Tailscale routes the destinations you nominate to the connector node. On that node, policy routing pushes exactly those destinations into a TUN interface, where sing-box picks them up and forwards them through the corporate proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting it up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The connector node
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale up &lt;span class="nt"&gt;--advertise-connector&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the tailnet policy file, the destinations you want steered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"target": ["*"], "app": {"tailscale.com/app-connectors": [{
    "name":       "corp-proxy",
    "connectors": ["tag:proxy-connector"],
    "routes": [
        "10.10.0.0/24",     // internal network
        "10.20.0.50/32",    // multi-vhost host, see below
        "192.0.2.7/32",     // single host
    ],
}]}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that a &lt;code&gt;/32&lt;/code&gt; here beats a broader route learned elsewhere. If &lt;code&gt;10.20.0.50&lt;/code&gt; is already reachable through some other subnet router advertising &lt;code&gt;10.20.0.0/16&lt;/code&gt;, the more specific &lt;code&gt;/32&lt;/code&gt; wins and steers just that one address to the connector. That's the mechanism, and it's also the fragility: drop the &lt;code&gt;/32&lt;/code&gt; and traffic silently reverts to the old path with no error anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. sing-box
&lt;/h3&gt;

&lt;p&gt;TUN inbound, SOCKS5 outbound, and — the key simplification — &lt;code&gt;final&lt;/code&gt; pointing at the proxy, so anything routed into the TUN is forwarded without needing a rule of its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inbounds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tun"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tun-in"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"interface_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"singtun0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"172.19.0.1/30"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mtu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"auto_route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"strict_route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outbounds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"socks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"corp-socks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.0.0.10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"server_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bind_interface"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tailscale0"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"auto_detect_interface"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"final"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"corp-socks"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;auto_route: false&lt;/code&gt; is deliberate — the routing is ours, in table 100, not sing-box's.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Policy routing
&lt;/h3&gt;

&lt;p&gt;A list file, so adding a destination doesn't mean editing a script. &lt;code&gt;/usr/local/etc/singbox-routes.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Destinations steered into sing-box → corporate SOCKS5 proxy.
# Keep in sync with the Tailscale App Connector route list.
&lt;/span&gt;
&lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;/&lt;span class="m"&gt;24&lt;/span&gt;     &lt;span class="c"&gt;# internal network
&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;20&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;50&lt;/span&gt;/&lt;span class="m"&gt;32&lt;/span&gt;    &lt;span class="c"&gt;# multi-vhost host
&lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;2&lt;/span&gt;.&lt;span class="m"&gt;7&lt;/span&gt;/&lt;span class="m"&gt;32&lt;/span&gt;     &lt;span class="c"&gt;# single host
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/usr/local/sbin/singbox-routes&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;

&lt;span class="nv"&gt;TABLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
&lt;span class="nv"&gt;PRIORITY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
&lt;span class="nv"&gt;TUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;singtun0
&lt;span class="nv"&gt;TS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tailscale0
&lt;span class="nv"&gt;WAIT_TENTHS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100                       &lt;span class="c"&gt;# 10s, in 100ms steps&lt;/span&gt;
&lt;span class="nv"&gt;ROUTES_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/etc/singbox-routes.conf

log&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"singbox-routes: &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

routes&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROUTES_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; log &lt;span class="s2"&gt;"cannot read &lt;/span&gt;&lt;span class="nv"&gt;$ROUTES_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/#.*//'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROUTES_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

wait_for_tun&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WAIT_TENTHS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
        if &lt;/span&gt;ip &lt;span class="nb"&gt;link &lt;/span&gt;show &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; log &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TUN&lt;/span&gt;&lt;span class="s2"&gt; appeared after &lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;i &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;ms"&lt;/span&gt;
            &lt;span class="k"&gt;return &lt;/span&gt;0
        &lt;span class="k"&gt;fi
        &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;0.1
        &lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;i &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;done
    &lt;/span&gt;log &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TUN&lt;/span&gt;&lt;span class="s2"&gt; did not appear within &lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;WAIT_TENTHS &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;s"&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;

start&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    wait_for_tun
    &lt;span class="nv"&gt;NETS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;routes&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NETS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; log &lt;span class="s2"&gt;"no destinations in &lt;/span&gt;&lt;span class="nv"&gt;$ROUTES_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;NET &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$NETS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
        &lt;/span&gt;ip route replace &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; dev &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; table &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TABLE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;err&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ip rule add priority &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PRIORITY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; iif &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
                       to &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; lookup &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TABLE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
            case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$err&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"File exists"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;   &lt;span class="c"&gt;# already present&lt;/span&gt;
                &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; log &lt;span class="s2"&gt;"rule add &lt;/span&gt;&lt;span class="nv"&gt;$NET&lt;/span&gt;&lt;span class="s2"&gt; failed: &lt;/span&gt;&lt;span class="nv"&gt;$err&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
            &lt;span class="k"&gt;esac&lt;/span&gt;
        &lt;span class="k"&gt;fi
    done
    &lt;/span&gt;log &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NETS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; destinations routed via &lt;/span&gt;&lt;span class="nv"&gt;$TUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

stop&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;ip rule del priority &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PRIORITY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; :&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
    &lt;/span&gt;ip route flush table &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TABLE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;start&lt;span class="p"&gt;)&lt;/span&gt; start &lt;span class="p"&gt;;;&lt;/span&gt;
    stop&lt;span class="p"&gt;)&lt;/span&gt;  stop &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Usage: &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt; {start|stop}"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tied to the service lifecycle via a drop-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;ExecStartPost&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/sbin/singbox-routes start&lt;/span&gt;
&lt;span class="py"&gt;ExecStopPost&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/sbin/singbox-routes stop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Four things that cost me time
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ExecStartPost&lt;/code&gt; fires before the TUN exists
&lt;/h3&gt;

&lt;p&gt;sing-box's unit is &lt;code&gt;Type=simple&lt;/code&gt;, so systemd runs &lt;code&gt;ExecStartPost&lt;/code&gt; the moment the process is forked — before sing-box has created &lt;code&gt;singtun0&lt;/code&gt;. Every &lt;code&gt;ip route add ... dev singtun0&lt;/code&gt; fails with &lt;em&gt;Cannot find device&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ip rule&lt;/code&gt; entries still install, because rules aren't device-bound. So you get rules present, table empty — and a rule pointing at an empty table doesn't terminate the lookup. The kernel falls through to the next rule and out to &lt;code&gt;main&lt;/code&gt;, where the traffic takes its old path. Silent, with a running, apparently healthy service.&lt;/p&gt;

&lt;p&gt;sing-box doesn't implement sd_notify, so &lt;code&gt;Type=notify&lt;/code&gt; isn't available; the script has to poll for the interface itself. In my case the race window was &lt;strong&gt;100ms&lt;/strong&gt; — one poll interval. Small enough to look fine on an idle box and fail on a loaded one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sniffing does not rewrite the destination
&lt;/h3&gt;

&lt;p&gt;If one IP serves several TLS vhosts, an IP-matched rule can't tell them apart, and rewriting with a fixed &lt;code&gt;override_address&lt;/code&gt; sends the wrong hostname upstream. The fix is to read the SNI — but not the way the obvious reading of the docs suggests.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{"action": "sniff"}&lt;/code&gt; populates the sniffed domain for &lt;em&gt;rule matching&lt;/em&gt; only. It does &lt;strong&gt;not&lt;/strong&gt; replace the connection's destination. In &lt;code&gt;route/rule/rule_action.go&lt;/code&gt;, &lt;code&gt;NewRuleAction()&lt;/code&gt; builds the sniff action with only the sniffer names and timeout; the struct's &lt;code&gt;OverrideDestination&lt;/code&gt; field is marked deprecated and is never set from configuration. The rewrite in &lt;code&gt;route/route.go&lt;/code&gt; is guarded by &lt;code&gt;if action.OverrideDestination &amp;amp;&amp;amp; M.IsDomainName(metadata.Domain)&lt;/code&gt;, so it never fires. Identical in v1.11.0, v1.12.0, v1.13.0 and current &lt;code&gt;testing&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;does&lt;/em&gt; work is that &lt;code&gt;DomainItem.Match()&lt;/code&gt; uses the sniffed domain, so &lt;code&gt;domain&lt;/code&gt; rules match even though the destination is still an IP. So: sniff, match on domain, and set &lt;code&gt;override_address&lt;/code&gt; explicitly per hostname.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inbound"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tun-in"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ip_cidr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.20.0.50/32"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sniff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sniffer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tls"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"500ms"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inbound"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tun-in"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"app-a.example.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"route"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outbound"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"corp-socks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"override_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app-a.example.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inbound"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tun-in"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"app-b.example.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"route"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"outbound"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"corp-socks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"override_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app-b.example.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule order matters: &lt;code&gt;sniff&lt;/code&gt; is non-final and evaluation continues to the next rule, so it must come first. Scope it to the one ambiguous IP rather than applying it globally.&lt;/p&gt;

&lt;p&gt;Don't add &lt;code&gt;override_port&lt;/code&gt;. &lt;code&gt;applyRouteOptionsOverride&lt;/code&gt; preserves the original port when only &lt;code&gt;override_address&lt;/code&gt; is set, so adding it silently rewrites every port to whatever you specified — I had connections to &lt;code&gt;:8443&lt;/code&gt; arriving at the proxy as &lt;code&gt;:443&lt;/code&gt; for days before noticing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ip rule add&lt;/code&gt; is idempotent; &lt;code&gt;ip route add&lt;/code&gt; isn't
&lt;/h3&gt;

&lt;p&gt;iproute2 sets &lt;code&gt;NLM_F_CREATE|NLM_F_EXCL&lt;/code&gt; on &lt;code&gt;RTM_NEWRULE&lt;/code&gt;, and the kernel's &lt;code&gt;fib_rules.c&lt;/code&gt; calls &lt;code&gt;rule_exists()&lt;/code&gt; under that flag, returning &lt;code&gt;-EEXIST&lt;/code&gt; for a rule matching on action, table, preference and selectors. Repeated starts can't accumulate duplicate rules. Routes have no such protection, hence &lt;code&gt;ip route replace&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Worth matching on the &lt;code&gt;File exists&lt;/code&gt; string rather than blanket-suppressing errors with &lt;code&gt;2&amp;gt;/dev/null || true&lt;/code&gt; — otherwise a genuine failure looks exactly like a no-op.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;iif tailscale0&lt;/code&gt; is load-bearing
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;final&lt;/code&gt; pointing at the proxy, the &lt;code&gt;iif tailscale0&lt;/code&gt; condition is what stops sing-box's own outbound connection to the SOCKS server from being routed back into the TUN. That traffic is locally generated, so it doesn't match &lt;code&gt;iif&lt;/code&gt;, and there's no loop. Anyone "simplifying" those rules by dropping the &lt;code&gt;iif&lt;/code&gt; creates one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it without touching production
&lt;/h2&gt;

&lt;p&gt;The routing logic can be verified offline: run sing-box with a SOCKS inbound instead of a TUN (route rules behave identically), point its outbound at a throwaway SOCKS5 server that logs what it's asked to connect to, and send a real TLS ClientHello with a chosen SNI to a chosen IP and port.&lt;/p&gt;

&lt;p&gt;That gives you a table like this, which is what you actually want to know:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Probe&lt;/th&gt;
&lt;th&gt;Upstream received&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;10.20.0.50:8443&lt;/code&gt;, SNI &lt;code&gt;app-b.example.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DOMAIN app-b.example.com:8443&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;10.20.0.50:443&lt;/code&gt;, SNI &lt;code&gt;app-a.example.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DOMAIN app-a.example.com:443&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;10.20.0.50:8443&lt;/code&gt;, no TLS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IPv4 10.20.0.50:8443&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;192.0.2.7:8078&lt;/code&gt;, no rule at all&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IPv4 192.0.2.7:8078&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Forty lines of Python for the fake proxy and the probe, and it settles questions that reading the documentation does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this can't do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wildcard domains.&lt;/strong&gt; &lt;code&gt;*.example.com&lt;/code&gt; has no fixed prefix for an &lt;code&gt;ip rule&lt;/code&gt;, and sing-box can't use a sniffed SNI as an &lt;code&gt;override_address&lt;/code&gt; (that value is a static string). Tailscale App Connectors do support a &lt;code&gt;domains&lt;/code&gt; key and learn addresses via DNS — but those dynamically-advertised routes reach the TUN only if they happen to fall inside a prefix you've already listed. For a genuinely open-ended wildcard, keep the PAC file for laptops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-443 ports.&lt;/strong&gt; Many corporate proxies restrict &lt;code&gt;CONNECT&lt;/code&gt; to 443. If a destination on an unusual port fails, test the proxy directly before suspecting the routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UDP.&lt;/strong&gt; Not covered here. SOCKS5 UDP associate exists but corporate proxies rarely permit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;Adding a destination is now two edits: one line in the connector's route list, one line in a conf file. Clients need no configuration at all — which is the entire point, because on iOS "configure the client" was never available.&lt;/p&gt;

</description>
      <category>tailscale</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
