@@ -153,9 +153,9 @@ func openListener(cfg *config.Gateway, stateDir string) (*tsnet.Server, net.List
153153 return nil , nil , err
154154 }
155155 _ = bringUp .Close ()
156- // Advertise exit routes so whole-machine and per-process tsnet
157- // clients can use this node as a Tailscale exit node.
158- go advertiseExitRoutes ( s )
156+ // Route advertisement ( exit routes + VIP subnet routes) happens in
157+ // runGateway via advertiseExitRoutes once the dnsvip allocator's
158+ // CIDRs are known.
159159 return s , ln , nil
160160}
161161
@@ -238,7 +238,28 @@ func tsnetCertDomain(s *tsnet.Server) string {
238238// node (advertises 0.0.0.0/0 and ::/0). Whole-machine clients on the
239239// same tailnet can then route all traffic through this gateway; exit
240240// flows are intercepted via RegisterFallbackTCPHandler in runGateway.
241- func advertiseExitRoutes (s * tsnet.Server ) {
241+ //
242+ // The dnsvip CIDRs are advertised alongside as plain subnet routes.
243+ // The exit-node /0 advertisements alone do NOT make the v4 VIPs
244+ // reachable: tailscaled derives the inbound packet filter's accept
245+ // set (localNets) locally from AdvertiseRoutes, and a /0 route is
246+ // deliberately shrunk to "the internet" (guest-wifi semantics) by
247+ // subtracting removeFromDefaultRoute — which contains 10.0.0.0/8 and
248+ // therefore the v4 VIP range. Inbound exit-node flows to a v4 VIP
249+ // were dropped by the filter's "destination not allowed" check before
250+ // any clawpatrol handler ran, for every client kind (tsnet
251+ // `clawpatrol run` and whole-machine alike). The v6 list strips only
252+ // link-local/multicast/fd7a:115c:a1e0::/48, so fd78:: VIPs always
253+ // passed — which is why v6-capable clients masked the bug. See
254+ // ipn/ipnlocal updateFilterLocked + shrinkDefaultRoute.
255+ //
256+ // Advertising the VIP CIDRs as non-default routes puts them in
257+ // localNets verbatim, so VIP-bound flows reach
258+ // RegisterFallbackTCPHandler / the UDP catch-all like any other
259+ // intercepted traffic. This is purely node-local: it does not require
260+ // the routes to be approved in the tailnet (clients route VIPs via
261+ // the exit-node /0, not via subnet routes). (#653)
262+ func advertiseExitRoutes (s * tsnet.Server , vipCIDRs ... netip.Prefix ) {
242263 lc , err := s .LocalClient ()
243264 if err != nil {
244265 log .Printf ("tsnet: LocalClient for exit routes: %v" , err )
@@ -248,13 +269,18 @@ func advertiseExitRoutes(s *tsnet.Server) {
248269 netip .MustParsePrefix ("0.0.0.0/0" ),
249270 netip .MustParsePrefix ("::/0" ),
250271 }
272+ for _ , p := range vipCIDRs {
273+ if p .IsValid () {
274+ routes = append (routes , p )
275+ }
276+ }
251277 if _ , err := lc .EditPrefs (context .Background (), & ipn.MaskedPrefs {
252278 AdvertiseRoutesSet : true ,
253279 Prefs : ipn.Prefs {AdvertiseRoutes : routes },
254280 }); err != nil {
255281 log .Printf ("tsnet: advertise exit routes: %v" , err )
256282 } else {
257- log .Printf ("tsnet: advertised exit routes (0.0.0.0/0, ::/0)" )
283+ log .Printf ("tsnet: advertised exit routes (%s)" , routes )
258284 }
259285}
260286
0 commit comments