mirror of
https://github.com/2dust/v2rayN.git
synced 2026-08-14 19:22:05 +03:00
Narrow the TUN self-address drop rule to single addresses (#9935)
The rule added in #9897 takes the TUN inbound's `address` verbatim as `ip_cidr`,
so a /30 or /126 interface prefix becomes the match range.
sing-tun derives the TUN's DNS entry from the address right after the interface's
own and hands it to the system resolver: Windows through luid.SetDNS in
tun_windows.go, Linux through systemd-resolved in tun_linux.go, both guarded only
by AutoRoute && !EXP_DisableDNSHijack. HasNextAddress keeps that address inside
the interface prefix, every preset in Global.TunIPv4Address is a /30 and every
IPv6 preset a /126, and the sing-box system stack rejects single-address
prefixes, so there is no configuration where it falls outside.
Queries from the system resolver then hit the drop rule and time out with no
response and no ICMP. Name resolution fails for the whole system while the proxy
path itself stays healthy, which makes it read as a DNS outage rather than a
routing rule. Reported in #9934 and #9926.
Matching each address on its own keeps what #9897 set out to block - the loop it
diagnosed was addressed to the interface address itself - and leaves the DNS
entry to sing-box.
Also restores the two regression tests #9897 came with, removed by eff58459
(#9817) while its implementation and template fix stayed in place.
ShouldRejectTrafficToTunOwnAddresses now asserts the single-address form and
additionally pins the prefix length, so it covers both the loop it was written
for and the resolver address it must not cover.
Verified on Linux by running sing-box directly from a generated config, changing
only this rule's prefix length between runs:
ip_cidr ["172.18.0.1/30"] getent hosts www.google.com -> empty, 3/3
ip_cidr ["172.18.0.1/32"] getent hosts www.google.com -> resolved, 3/3
dig against a public resolver, naked-IP HTTPS and the local mixed port were
unaffected in both runs. End to end, a build of this branch emits
drop ip_cidr ["172.18.0.1/32"] and system resolution works while its TUN is up.
Co-authored-by: liuclare <177657698+liuclare@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,85 @@ public class CoreConfigSingboxServiceTests
|
||||
cfg.inbounds.Should().Contain(i => i.type == "tun");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_TunEnabled_ShouldKeepEmbeddedTunRules()
|
||||
{
|
||||
// The embedded tun rules reject local-network noise (NetBIOS/mDNS, multicast).
|
||||
// They are deserialized into List<Rule4Sbox>, so a schema mismatch in the
|
||||
// embedded template makes JsonUtils.Deserialize return null and silently
|
||||
// drops every one of them.
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
config.TunModeItem.EnableTun = true;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||
{
|
||||
IsTunEnabled = true,
|
||||
};
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
|
||||
cfg.route.rules.Should().Contain(
|
||||
r => r.action == "reject"
|
||||
&& r.network != null && r.network.Contains("udp")
|
||||
&& r.port != null && r.port.Contains(5353),
|
||||
"the embedded tun rules must reject mDNS/NetBIOS noise");
|
||||
cfg.route.rules.Should().Contain(
|
||||
r => r.action == "reject"
|
||||
&& r.ip_cidr != null && r.ip_cidr.Contains("224.0.0.0/3"),
|
||||
"the embedded tun rules must reject multicast traffic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_TunEnabled_ShouldRejectTrafficToTunOwnAddresses()
|
||||
{
|
||||
// Regression test: traffic addressed to the TUN interface's own addresses must
|
||||
// never reach an outbound. auto_route hijacks the default route, so `direct`
|
||||
// writes such a packet straight back into the TUN, which routes it to the
|
||||
// outbound again - an infinite loop that pins a CPU core. Observed in the wild
|
||||
// with WebRTC ICE connectivity checks against the TUN's own fc00::/7 ULA
|
||||
// address, sustaining ~8k packets/s out of the TUN interface.
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
|
||||
config.TunModeItem.EnableTun = true;
|
||||
config.TunModeItem.EnableIPv6Address = true;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
|
||||
{
|
||||
IsTunEnabled = true,
|
||||
};
|
||||
|
||||
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
|
||||
|
||||
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
|
||||
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
|
||||
var tun = cfg.inbounds.First(i => i.type == "tun");
|
||||
tun.address.Should().NotBeNullOrEmpty();
|
||||
|
||||
foreach (var address in tun.address!)
|
||||
{
|
||||
var self = IPAddress.Parse(address.Split('/').First());
|
||||
var hostBits = self.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
|
||||
var expected = $"{self}/{hostBits}";
|
||||
cfg.route.rules.Should().Contain(
|
||||
r => r.action == "reject" && r.ip_cidr != null && r.ip_cidr.Contains(expected),
|
||||
$"traffic to the TUN's own address '{address}' must be rejected, not routed");
|
||||
}
|
||||
|
||||
// The match has to stay on the addresses themselves. sing-tun derives the TUN's DNS
|
||||
// entry from the address right after the interface's own, and every prefix offered
|
||||
// here leaves room for it, so a prefix match would drop system name lookups too.
|
||||
var dropRule = cfg.route.rules.First(r =>
|
||||
r.action == "reject" && r.method == "drop" && r.ip_cidr?.Count > 0);
|
||||
dropRule.ip_cidr!.Should().OnlyContain(c =>
|
||||
c.EndsWith("/32", StringComparison.Ordinal) || c.EndsWith("/128", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateClientConfigContent_BindInterface_ShouldUseDialBindInterface()
|
||||
{
|
||||
|
||||
@@ -48,12 +48,18 @@ public partial class CoreConfigSingboxService
|
||||
// packet straight back into the TUN, which hands it to the outbound again -
|
||||
// an infinite loop that pins a CPU core. Drop instead of rejecting so no
|
||||
// ICMP unreachable is generated back towards the same addresses.
|
||||
//
|
||||
// Match each address on its own, not the prefix it carries. On Linux sing-tun
|
||||
// registers Inet4Address[0].Addr().Next() with systemd-resolved as a "~." DNS
|
||||
// upstream, and every prefix offered here is a /30 or /126, so carrying the
|
||||
// prefix through would cover that resolver address too and drop every system
|
||||
// name lookup along with the loop.
|
||||
var tunAddresses = _coreConfig.inbounds.FirstOrDefault(i => i.type == "tun")?.address;
|
||||
if (tunAddresses?.Count > 0)
|
||||
{
|
||||
_coreConfig.route.rules.Add(new()
|
||||
{
|
||||
ip_cidr = [.. tunAddresses],
|
||||
ip_cidr = [.. tunAddresses.Select(ToSingleAddressPrefix)],
|
||||
action = "reject",
|
||||
method = "drop",
|
||||
});
|
||||
@@ -284,6 +290,14 @@ public partial class CoreConfigSingboxService
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToSingleAddressPrefix(string address)
|
||||
{
|
||||
var addr = address.Split('/').First();
|
||||
return IPAddress.TryParse(addr, out var ip)
|
||||
? $"{addr}/{(ip.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32)}"
|
||||
: address;
|
||||
}
|
||||
|
||||
private List<string> BuildRoutingDirectExe()
|
||||
{
|
||||
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Reference in New Issue
Block a user