Operating system
Linux
System version
NixOS 26.11.20260816.e5bdc4a, kernel 6.18
Installation type
package manager
Version
1.2.2
Description
Throne generates the TUN inbound with a route_exclude_address that holds a fixed set of private ranges and nothing else:
"route_exclude_address": ["127.0.0.0/8","10.0.0.0/8","172.16.0.0/12",
"192.168.0.0/16","169.254.0.0/16","224.0.0.0/4","255.255.255.255/32"]
IP CIDRs entered in the routing profile's Direct column do not go there. They are emitted as a routing rule instead:
{"action":"route","ip_cidr":["100.64.0.0/10"],"outbound":"direct"}
For ordinary destinations the two are equivalent. For another TUN interface on the same host they are not, and Tailscale is the case where it breaks:
- the packet is captured by
throne-tun before routing runs
- the rule sends it to the
direct outbound, so sing-box opens its own socket and the reply comes back fine
- sing-box then writes that reply back into
throne-tun
- the kernel now sees a packet with a
100.64.0.0/10 source address on an interface that is not tailscale0, and Tailscale's own anti-spoofing rule drops it:
-A ts-input -s 100.64.0.0/10 ! -i tailscale0 -j DROP
The result is that every TCP connection to a tailnet peer fails while Throne is connected, no matter what the user puts in Direct. route_exclude_address is the only thing that helps, because it keeps the traffic out of the TUN in the first place — this is the documented way to run sing-box alongside Tailscale, and mihomo exposes the same option as route-exclude-address.
Observed with strict_route: false and auto_redirect: true, so neither of those is involved.
Reproduction
- Install Tailscale and join a tailnet; note a peer address, e.g.
100.108.115.80
- In the routing profile, add
ip:100.64.0.0/10 to Direct
- Connect any profile in TUN mode
curl -v http://100.108.115.80:22 — TCP connect completes in a few ms (the local proxy accepts it) and then stalls until it is reset. tailscale ping 100.108.115.80 still works, because the daemon's own packets carry fwmark 0x80000 and bypass the tunnel
- Inspect the generated config: the rule from step 2 is present in
route.rules, and inbounds[].route_exclude_address still holds only the private ranges
There is also no other place to express this. The TUN settings dialog offers Stack, MTU, Tun Enable IPv6, Strict Route, Enable Tun Routing, Auto Redirect, the TUN addresses, and a Disable Private Range Bypass checkbox — that last one only switches the built-in list off, it cannot take an address. The Route tab lists routing profiles and nothing else, and the settings file carries no key for TUN address exclusions either.
Expected
IP CIDRs from Direct are appended to the TUN's route_exclude_address as well as to route.rules — or a dedicated field for TUN exclusions is exposed.
Related
Logs
Nothing is logged for this. The core starts normally and the connection is accepted locally, so the failure is silent — it only shows as a stalled TCP session that is eventually reset. The evidence is in the generated configuration quoted above.
Operating system
Linux
System version
NixOS 26.11.20260816.e5bdc4a, kernel 6.18
Installation type
package manager
Version
1.2.2
Description
Throne generates the TUN inbound with a
route_exclude_addressthat holds a fixed set of private ranges and nothing else:IP CIDRs entered in the routing profile's Direct column do not go there. They are emitted as a routing rule instead:
{"action":"route","ip_cidr":["100.64.0.0/10"],"outbound":"direct"}For ordinary destinations the two are equivalent. For another TUN interface on the same host they are not, and Tailscale is the case where it breaks:
throne-tunbefore routing runsdirectoutbound, so sing-box opens its own socket and the reply comes back finethrone-tun100.64.0.0/10source address on an interface that is nottailscale0, and Tailscale's own anti-spoofing rule drops it:The result is that every TCP connection to a tailnet peer fails while Throne is connected, no matter what the user puts in Direct.
route_exclude_addressis the only thing that helps, because it keeps the traffic out of the TUN in the first place — this is the documented way to run sing-box alongside Tailscale, and mihomo exposes the same option asroute-exclude-address.Observed with
strict_route: falseandauto_redirect: true, so neither of those is involved.Reproduction
100.108.115.80ip:100.64.0.0/10to Directcurl -v http://100.108.115.80:22— TCP connect completes in a few ms (the local proxy accepts it) and then stalls until it is reset.tailscale ping 100.108.115.80still works, because the daemon's own packets carryfwmark 0x80000and bypass the tunnelroute.rules, andinbounds[].route_exclude_addressstill holds only the private rangesThere is also no other place to express this. The TUN settings dialog offers Stack, MTU, Tun Enable IPv6, Strict Route, Enable Tun Routing, Auto Redirect, the TUN addresses, and a Disable Private Range Bypass checkbox — that last one only switches the built-in list off, it cannot take an address. The Route tab lists routing profiles and nothing else, and the settings file carries no key for TUN address exclusions either.
Expected
IP CIDRs from Direct are appended to the TUN's
route_exclude_addressas well as toroute.rules— or a dedicated field for TUN exclusions is exposed.Related
100.64.0.0/10entry in the routing profile that did not helpLogs
Nothing is logged for this. The core starts normally and the connection is accepted locally, so the failure is silent — it only shows as a stalled TCP session that is eventually reset. The evidence is in the generated configuration quoted above.