mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-30 04:55:42 +00:00
TUN inbound: autoSystemDnsToGateway falls back to an IPv6 gateway on Linux
Without an IPv4 address in gateway, the system DNS now points at the first IPv6 gateway plus one (e.g. fc00::1/64 -> fc00::2) instead of nothing, and the routing check before the takeover accepts IPv6 addresses for it. The README also says what each system does without gateway: Xray assigns no address on Linux, Windows gives the TUN link-local ones itself, and macOS and FreeBSD use 169.254.10.1/30. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
de02da553a
commit
3a6bdb19ba
+3
-2
@@ -15,7 +15,8 @@ Plainly enabling it in the config probably will result nothing, or lock your rou
|
||||
## DETAILS
|
||||
|
||||
By default, enabling the feature will only bring the tun interface up. \
|
||||
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS uses the first IPv4 prefix from `gateway` to configure the utun point-to-point address. \
|
||||
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS and FreeBSD use the first IPv4 prefix from `gateway` for the point-to-point address. \
|
||||
Without `gateway`, the systems differ: Xray assigns no address on Linux, Windows gives the interface link-local addresses itself (an IPv6 one at once, an IPv4 one from `169.254.0.0/16` after a few seconds), and macOS and FreeBSD use `169.254.10.1/30`. \
|
||||
Windows, Linux and macOS can also apply system routes from `autoSystemRoutingTable`.
|
||||
macOS does not configure system DNS from the `dns` field, and neither does Linux by default; system DNS remains managed by the OS or distribution-specific network services. \
|
||||
For more advanced routing policies or rules, OS level configuration can still manage the named interface (e.g. xray0) when it appears.
|
||||
@@ -33,7 +34,7 @@ It uses `resolvectl`, which means it applies only when all of these hold:
|
||||
- systemd-resolved is version 240 or newer, where `default-route` exists
|
||||
- no `dns` upstream resolves through the system resolver, directly or through its own bootstrap (see below)
|
||||
|
||||
The address handed over is the first IPv4 `gateway` incremented by one (e.g. `192.168.100.1/30` -> `192.168.100.2`). It is not taken from `dns`: handing `1.1.1.1` to `resolvectl dns` would make systemd-resolved query that server directly over the physical link, which is the leak this option exists to close.
|
||||
The address handed over is the first IPv4 `gateway`, or without one the first IPv6 `gateway`, incremented by one (e.g. `192.168.100.1/30` -> `192.168.100.2`, `fc00::1/64` -> `fc00::2`). Without any `gateway`, the option does nothing. It is not taken from `dns`: handing `1.1.1.1` to `resolvectl dns` would make systemd-resolved query that server directly over the physical link, which is the leak this option exists to close.
|
||||
|
||||
Because that address has to actually answer, the takeover is checked before it happens. A query from the interface address to that address is routed through the configured rules, and host-wide DNS is only changed when the result is a DNS-capable outbound. Otherwise the option does nothing and DNS is left to the OS. In practice this means you also need a routing rule sending the interface's port 53 to a `dns` outbound, for example:
|
||||
|
||||
|
||||
+18
-12
@@ -53,23 +53,29 @@ var resolvectlRunner = func(name string, args ...string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// systemDNSAddrs derives the addresses used for the system DNS takeover from the
|
||||
// first IPv4 gateway: the gateway address itself is what a query from this
|
||||
// interface appears to come from, and the next address is what the resolver is
|
||||
// pointed at. The latter belongs to the TUN and is answered inside Xray;
|
||||
// handing the configured public resolvers to resolvectl instead would leave the
|
||||
// system querying them directly over the physical link, defeating the point of
|
||||
// the TUN.
|
||||
// first IPv4 gateway, or without one, the first IPv6 gateway: the gateway
|
||||
// address itself is what a query from this interface appears to come from, and
|
||||
// the next address is what the resolver is pointed at. The latter belongs to
|
||||
// the TUN and is answered inside Xray; handing the configured public resolvers
|
||||
// to resolvectl instead would leave the system querying them directly over the
|
||||
// physical link, defeating the point of the TUN.
|
||||
func systemDNSAddrs(gateway []string) (source, dns netip.Addr, ok bool) {
|
||||
var first6 netip.Addr
|
||||
for _, address := range gateway {
|
||||
prefix, err := netip.ParsePrefix(address)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addr := prefix.Addr()
|
||||
if !addr.Is4() {
|
||||
continue
|
||||
if addr.Is4() {
|
||||
return addr, addr.Next(), true
|
||||
}
|
||||
return addr, addr.Next(), true
|
||||
if !first6.IsValid() {
|
||||
first6 = addr
|
||||
}
|
||||
}
|
||||
if first6.IsValid() {
|
||||
return first6, first6.Next(), true
|
||||
}
|
||||
return netip.Addr{}, netip.Addr{}, false
|
||||
}
|
||||
@@ -115,11 +121,11 @@ const probeSourcePort = 49152
|
||||
// Overridable for tests.
|
||||
var verifyDNSRouting = func(ctx context.Context, inboundTag, source, address string) error {
|
||||
ip, err := netip.ParseAddr(address)
|
||||
if err != nil || !ip.Is4() {
|
||||
if err != nil {
|
||||
return errors.New("invalid DNS address ", address).Base(err)
|
||||
}
|
||||
src, err := netip.ParseAddr(source)
|
||||
if err != nil || !src.Is4() {
|
||||
if err != nil || src.Is4() != ip.Is4() {
|
||||
return errors.New("invalid source address ", source).Base(err)
|
||||
}
|
||||
|
||||
@@ -202,7 +208,7 @@ func (t *LinuxTun) ConfigureSystemDNS(ctx context.Context, inboundTag string) er
|
||||
|
||||
source, address, ok := systemDNSAddrs(t.options.Gateway)
|
||||
if !ok {
|
||||
return errors.New("no IPv4 gateway, cannot derive a system DNS address")
|
||||
return errors.New("no gateway, cannot derive a system DNS address")
|
||||
}
|
||||
|
||||
iface := t.ifaceName()
|
||||
|
||||
@@ -191,3 +191,15 @@ func TestVerifyDNSRoutingDecisions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Without an IPv4 gateway, the takeover uses the first IPv6 one, and the probe
|
||||
// carries IPv6 addresses.
|
||||
func TestVerifyDNSRoutingIPv6(t *testing.T) {
|
||||
ctx := newRouteTestContext(t, true, udpNameServer([]byte{9, 9, 9, 9}), []*router.RoutingRule{port53Rule()})
|
||||
if err := verifyDNSRouting(ctx, routeTestInboundTag, "fc00::1", "fc00::2"); err != nil {
|
||||
t.Fatalf("expected the takeover to be accepted, got: %v", err)
|
||||
}
|
||||
if err := verifyDNSRouting(ctx, routeTestInboundTag, routeTestSource, "fc00::2"); err == nil {
|
||||
t.Fatal("expected mixed IPv4 and IPv6 addresses to be refused")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func TestConfigureSystemDNSNoGateway(t *testing.T) {
|
||||
t1.options.Gateway = nil
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err == nil {
|
||||
t.Fatal("expected an error when no IPv4 gateway is configured")
|
||||
t.Fatal("expected an error when no gateway is configured")
|
||||
}
|
||||
if len(*probes) != 0 {
|
||||
t.Errorf("routing probe must not run without a gateway, got %d calls", len(*probes))
|
||||
@@ -351,9 +351,18 @@ func TestSystemDNSAddrs(t *testing.T) {
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "ipv6 only",
|
||||
gateway: []string{"fc00::1/64"},
|
||||
wantOK: false,
|
||||
name: "ipv6 only",
|
||||
gateway: []string{"fc00::1/64"},
|
||||
wantSource: "fc00::1",
|
||||
wantDNS: "fc00::2",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "first ipv6 without ipv4",
|
||||
gateway: []string{"fc00::1/64", "fd00::1/64"},
|
||||
wantSource: "fc00::1",
|
||||
wantDNS: "fc00::2",
|
||||
wantOK: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user