windows: avoid subnet-route override of on-link routes

Set higher route metrics for advertised subnet routes so Windows prefers local on-link routes when present (tailscale/tailscale#12248).
pull/18367/head
wmd-ntex 3 days ago
parent 4c37141ab7
commit 043a1a4e0b

@ -405,7 +405,15 @@ func configureInterface(cfg *router.Config, tun *tun.NativeTun, ht *health.Track
RouteData: winipcfg.RouteData{
Destination: route,
NextHop: gateway,
Metric: 0,
// Explicit route metrics are important on Windows: if we leave the
// metric at 0 ("automatic"), Windows will often assign a very low
// effective metric to routes on the Tailscale interface (commonly 5),
// which can cause a host that is physically on a LAN to prefer the
// subnet-router path for that same LAN. When the subnet router is
// unavailable, this can blackhole local traffic.
//
// See https://github.com/tailscale/tailscale/issues/12248.
Metric: windowsRouteMetric(route),
},
}

@ -0,0 +1,45 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package osrouter
import (
"net/netip"
"tailscale.com/net/tsaddr"
)
// windowsSubnetRouteMetric is an intentionally "high" route metric used for
// non-Tailscale routes (e.g. advertised subnet routes) on Windows.
//
// Rationale: when a Windows client is physically on a LAN that's also
// advertised via a subnet router, Windows may otherwise prefer the Tailscale
// route due to a low Tailscale interface metric, resulting in local traffic
// being sent to (and dependent on) the subnet router.
//
// See https://github.com/tailscale/tailscale/issues/12248.
const windowsSubnetRouteMetric = uint32(5000)
// windowsRouteMetric returns the route metric to use when installing routes on
// Windows.
//
// For exit-node default routes and single-host Tailscale routes, we keep the
// metric low so the routes behave as expected. For advertised subnet routes, we
// set a higher metric so on-link / locally-connected routes win when present.
func windowsRouteMetric(route netip.Prefix) uint32 {
if !route.IsValid() {
return 0
}
// Default route (exit node) should stay preferred when configured.
if route.Bits() == 0 {
return 0
}
// Single-host Tailscale routes should stay preferred.
if route.IsSingleIP() && tsaddr.IsTailscaleIP(route.Addr().Unmap()) {
return 0
}
// Everything else (notably: advertised subnet routes) should not override
// on-link routes with the same prefix.
return windowsSubnetRouteMetric
}

@ -0,0 +1,58 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package osrouter
import (
"net/netip"
"testing"
"tailscale.com/net/tsaddr"
)
func TestWindowsRouteMetric(t *testing.T) {
tests := []struct {
name string
route netip.Prefix
want uint32
}{
{
name: "default_route_v4",
route: netip.MustParsePrefix("0.0.0.0/0"),
want: 0,
},
{
name: "default_route_v6",
route: netip.MustParsePrefix("::/0"),
want: 0,
},
{
name: "tailscale_service_ip_v4_single_host",
route: netip.PrefixFrom(tsaddr.TailscaleServiceIP(), 32),
want: 0,
},
{
name: "tailscale_service_ip_v6_single_host",
route: netip.PrefixFrom(tsaddr.TailscaleServiceIPv6(), 128),
want: 0,
},
{
name: "advertised_subnet_v4",
route: netip.MustParsePrefix("192.168.1.0/24"),
want: windowsSubnetRouteMetric,
},
{
name: "advertised_subnet_v6",
route: netip.MustParsePrefix("fd00::/64"),
want: windowsSubnetRouteMetric,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := windowsRouteMetric(tt.route); got != tt.want {
t.Fatalf("windowsRouteMetric(%v)=%v; want %v", tt.route, got, tt.want)
}
})
}
}
Loading…
Cancel
Save