diff --git a/routes.go b/routes.go index bab35ea8..f1fbb995 100644 --- a/routes.go +++ b/routes.go @@ -106,13 +106,36 @@ func (h *Headscale) DisableRoute(id uint64) error { return err } - route.Enabled = false - route.IsPrimary = false - err = h.db.Save(route).Error + // Tailscale requires both IPv4 and IPv6 exit routes to + // be enabled at the same time, as per + // https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002 + if !route.isExitRoute() { + route.Enabled = false + route.IsPrimary = false + err = h.db.Save(route).Error + if err != nil { + return err + } + + return h.handlePrimarySubnetFailover() + } + + routes, err := h.GetMachineRoutes(&route.Machine) if err != nil { return err } + for i := range routes { + if routes[i].isExitRoute() { + routes[i].Enabled = false + routes[i].IsPrimary = false + err = h.db.Save(&routes[i]).Error + if err != nil { + return err + } + } + } + return h.handlePrimarySubnetFailover() } @@ -122,7 +145,30 @@ func (h *Headscale) DeleteRoute(id uint64) error { return err } - if err := h.db.Unscoped().Delete(&route).Error; err != nil { + // Tailscale requires both IPv4 and IPv6 exit routes to + // be enabled at the same time, as per + // https://github.com/juanfont/headscale/issues/804#issuecomment-1399314002 + if !route.isExitRoute() { + if err := h.db.Unscoped().Delete(&route).Error; err != nil { + return err + } + + return h.handlePrimarySubnetFailover() + } + + routes, err := h.GetMachineRoutes(&route.Machine) + if err != nil { + return err + } + + routesToDelete := []Route{} + for _, r := range routes { + if r.isExitRoute() { + routesToDelete = append(routesToDelete, r) + } + } + + if err := h.db.Unscoped().Delete(&routesToDelete).Error; err != nil { return err } diff --git a/routes_test.go b/routes_test.go index b67b3ee9..e2d056b7 100644 --- a/routes_test.go +++ b/routes_test.go @@ -457,6 +457,37 @@ func (s *Suite) TestAllowedIPRoutes(c *check.C) { c.Assert(foundExitNodeV4, check.Equals, true) c.Assert(foundExitNodeV6, check.Equals, true) + + // Now we disable only one of the exit routes + // and we see if both are disabled + var exitRouteV4 Route + for _, route := range routes { + if route.isExitRoute() && netip.Prefix(route.Prefix) == prefixExitNodeV4 { + exitRouteV4 = route + + break + } + } + + err = app.DisableRoute(uint64(exitRouteV4.ID)) + c.Assert(err, check.IsNil) + + enabledRoutes1, err = app.GetEnabledRoutes(&machine1) + c.Assert(err, check.IsNil) + c.Assert(len(enabledRoutes1), check.Equals, 1) + + // and now we delete only one of the exit routes + // and we check if both are deleted + routes, err = app.GetMachineRoutes(&machine1) + c.Assert(err, check.IsNil) + c.Assert(len(routes), check.Equals, 4) + + err = app.DeleteRoute(uint64(exitRouteV4.ID)) + c.Assert(err, check.IsNil) + + routes, err = app.GetMachineRoutes(&machine1) + c.Assert(err, check.IsNil) + c.Assert(len(routes), check.Equals, 2) } func (s *Suite) TestDeleteRoutes(c *check.C) {