wgengine/router: use quad-100 as the nexthop on Windows

Windows requires routes to have a nexthop. Routes created using the interface's local IP address or an unspecified IP address ("0.0.0.0" or "::") as the nexthop are considered on-link routes. Notably, Windows treats on-link subnet routes differently, reserving the last IP in the range as the broadcast IP and therefore prohibiting TCP connections to it, resulting in WSA error 10049: "The requested address is not valid in its context. This does not happen with single-host routes, such as routes to Tailscale IP addresses, but becomes a problem with advertised subnets when all IPs in the range should be reachable.

Before Windows 8, only routes created with an unspecified IP address were considered on-link, so our previous approach of using the interface's own IP as the nexthop likely worked on Windows 7.

This PR updates configureInterface to use the TailscaleServiceIP (100.100.100.100) and its IPv6 counterpart as the nexthop for subnet routes.

Fixes tailscale/support-escalations#57

Signed-off-by: Nick Khyl <nickk@tailscale.com>
pull/12858/head
Nick Khyl 4 months ago committed by Nick Khyl
parent d3af54444c
commit 1608831c33

@ -320,12 +320,24 @@ func configureInterface(cfg *Config, tun *tun.NativeTun, ht *health.Tracker) (re
ipif6 = nil ipif6 = nil
} }
// Windows requires routes to have a nexthop. For routes such as // Windows requires routes to have a nexthop. Routes created using
// ours where the nexthop is meaningless, you're supposed to use // the interface's local IP address or an unspecified IP address
// one of the local IP addresses of the interface. Find an IPv4 // ("0.0.0.0" or "::") as the nexthop are considered on-link routes.
// and IPv6 address we can use for this purpose. //
var firstGateway4 netip.Addr // Notably, Windows treats on-link subnet routes differently, reserving the last
var firstGateway6 netip.Addr // IP in the range as the broadcast IP and therefore prohibiting TCP connections
// to it, resulting in WSA error 10049: "The requested address is not valid in its context."
// This does not happen with single-host routes, such as routes to Tailscale IP addresses,
// but becomes a problem with advertised subnets when all IPs in the range should be reachable.
// See https://github.com/tailscale/support-escalations/issues/57 for details.
//
// For routes such as ours where the nexthop is meaningless, we can use an
// arbitrary nexthop address, such as TailscaleServiceIP, to prevent the
// routes from being marked as on-link. We can still create on-link routes
// for single-host Tailscale routes, but we shouldn't attempt to create a
// route for the interface's own IP.
var localAddr4, localAddr6 netip.Addr
var gatewayAddr4, gatewayAddr6 netip.Addr
addresses := make([]netip.Prefix, 0, len(cfg.LocalAddrs)) addresses := make([]netip.Prefix, 0, len(cfg.LocalAddrs))
for _, addr := range cfg.LocalAddrs { for _, addr := range cfg.LocalAddrs {
if (addr.Addr().Is4() && ipif4 == nil) || (addr.Addr().Is6() && ipif6 == nil) { if (addr.Addr().Is4() && ipif4 == nil) || (addr.Addr().Is6() && ipif6 == nil) {
@ -333,10 +345,12 @@ func configureInterface(cfg *Config, tun *tun.NativeTun, ht *health.Tracker) (re
continue continue
} }
addresses = append(addresses, addr) addresses = append(addresses, addr)
if addr.Addr().Is4() && !firstGateway4.IsValid() { if addr.Addr().Is4() && !gatewayAddr4.IsValid() {
firstGateway4 = addr.Addr() localAddr4 = addr.Addr()
} else if addr.Addr().Is6() && !firstGateway6.IsValid() { gatewayAddr4 = tsaddr.TailscaleServiceIP()
firstGateway6 = addr.Addr() } else if addr.Addr().Is6() && !gatewayAddr6.IsValid() {
localAddr6 = addr.Addr()
gatewayAddr6 = tsaddr.TailscaleServiceIPv6()
} }
} }
@ -349,7 +363,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun, ht *health.Tracker) (re
continue continue
} }
if route.Addr().Is6() && !firstGateway6.IsValid() { if route.Addr().Is6() && !gatewayAddr6.IsValid() {
// Windows won't let us set IPv6 routes without having an // Windows won't let us set IPv6 routes without having an
// IPv6 local address set. However, when we've configured // IPv6 local address set. However, when we've configured
// a default route, we want to forcibly grab IPv6 traffic // a default route, we want to forcibly grab IPv6 traffic
@ -358,43 +372,51 @@ func configureInterface(cfg *Config, tun *tun.NativeTun, ht *health.Tracker) (re
// route source. // route source.
ip := tsaddr.Tailscale4To6Placeholder() ip := tsaddr.Tailscale4To6Placeholder()
addresses = append(addresses, netip.PrefixFrom(ip, ip.BitLen())) addresses = append(addresses, netip.PrefixFrom(ip, ip.BitLen()))
firstGateway6 = ip gatewayAddr6 = ip
} else if route.Addr().Is4() && !firstGateway4.IsValid() { } else if route.Addr().Is4() && !gatewayAddr4.IsValid() {
// TODO: do same dummy behavior as v6? // TODO: do same dummy behavior as v6?
return errors.New("due to a Windows limitation, one cannot have interface routes without an interface address") return errors.New("due to a Windows limitation, one cannot have interface routes without an interface address")
} }
var gateway netip.Addr var gateway, localAddr netip.Addr
if route.Addr().Is4() { if route.Addr().Is4() {
gateway = firstGateway4 localAddr = localAddr4
gateway = gatewayAddr4
} else if route.Addr().Is6() { } else if route.Addr().Is6() {
gateway = firstGateway6 localAddr = localAddr6
} gateway = gatewayAddr6
r := &routeData{
RouteData: winipcfg.RouteData{
Destination: route,
NextHop: gateway,
Metric: 0,
},
} }
if r.Destination.Addr().Unmap() == gateway {
switch destAddr := route.Addr().Unmap(); {
case destAddr == localAddr:
// no need to add a route for the interface's // no need to add a route for the interface's
// own IP. The kernel does that for us. // own IP. The kernel does that for us.
// If we try to replace it, we'll fail to // If we try to replace it, we'll fail to
// add the route unless NextHop is set, but // add the route unless NextHop is set, but
// then the interface's IP won't be pingable. // then the interface's IP won't be pingable.
continue continue
case route.IsSingleIP() && (destAddr == gateway || tsaddr.IsTailscaleIP(destAddr)):
// add an on-link route if the destination
// is the nexthop itself or a single Tailscale IP.
gateway = localAddr
} }
r := &routeData{
RouteData: winipcfg.RouteData{
Destination: route,
NextHop: gateway,
Metric: 0,
},
}
if route.Addr().Is4() { if route.Addr().Is4() {
if route.Bits() == 0 { if route.Bits() == 0 {
foundDefault4 = true foundDefault4 = true
} }
r.NextHop = firstGateway4
} else if route.Addr().Is6() { } else if route.Addr().Is6() {
if route.Bits() == 0 { if route.Bits() == 0 {
foundDefault6 = true foundDefault6 = true
} }
r.NextHop = firstGateway6
} }
routes = append(routes, r) routes = append(routes, r)
} }

Loading…
Cancel
Save