@@ -338,6 +338,80 @@ func TestGatewayHostNetRoutesStalenessReconcileV6(t *testing.T) {
338338 assertRoutesApplied (t )
339339}
340340
341+ // ---- R6: offline enable proceeds and self-heals when a default appears ----
342+ //
343+ // Enabling the gateway client with NO IPv4 default route must succeed (warn +
344+ // empty exemption table) with the TUN default and the v6 fence installed, and
345+ // the monitor must copy the host default into tableID once one appears — the
346+ // §8.4 offline-boot self-heal. We simulate "offline" by removing the host's
347+ // real IPv4 defaults and restoring them afterwards; while they are gone the
348+ // host's v4 egress is the TUN black hole, which is within this suite's risk
349+ // profile (every routes test black-holes egress mid-flight anyway).
350+ func TestGatewayHostNetRoutesOfflineEnableSelfHeals (t * testing.T ) {
351+ verifyNoLeaks (t )
352+ requireRoot (t )
353+ // A default is required not by the enable under test (that is the point)
354+ // but as material: we snapshot the real defaults to remove and re-add.
355+ requireDefaultRoute (t )
356+ setupDummyTun (t )
357+
358+ origDefaults , err := getDefaultRoutes ()
359+ require .NoError (t , err )
360+ require .NotEmpty (t , origDefaults )
361+ for i := range origDefaults {
362+ r := origDefaults [i ]
363+ require .NoError (t , netlink .RouteDel (& r ), "remove host default %v" , r )
364+ }
365+ t .Cleanup (func () {
366+ // RouteReplace, not RouteAdd: the mid-test re-add below leaves one of
367+ // them already present on the success path.
368+ for i := range origDefaults {
369+ r := origDefaults [i ]
370+ _ = netlink .RouteReplace (& r )
371+ }
372+ })
373+
374+ before := snapshotNet (t )
375+
376+ // See R4 for why Start + cancel are needed here.
377+ mgr := NewManager ()
378+ ctx , cancel := context .WithCancel (context .Background ())
379+ t .Cleanup (cancel )
380+ require .NoError (t , mgr .Start (ctx ))
381+ require .NoError (t , mgr .EnableClientRoutes (testTunIf ),
382+ "offline enable must proceed with a warning, not fail" )
383+ t .Cleanup (func () { _ = mgr .DisableClientRoutes () })
384+ require .True (t , mgr .ClientRoutesActive ())
385+
386+ // Offline shape: TUN default captured, exemption table empty (nothing to
387+ // exempt yet). assertRoutesApplied is not usable here — it requires a
388+ // non-empty exemption table.
389+ main := cmdOut (t , "ip" , "-4" , "route" , "show" )
390+ require .Contains (t , main , "dev " + testTunIf , "TUN default must be installed even offline" )
391+ require .Empty (t , strings .TrimSpace (routeTableDump (t , tableID )),
392+ "the v4 exemption table must start empty when enabled offline" )
393+
394+ // The network comes back: restore one real default in the main table.
395+ restored := origDefaults [0 ]
396+ require .NoError (t , netlink .RouteReplace (& restored ))
397+
398+ require .Eventually (t , func () bool {
399+ return strings .Contains (routeTableDump (t , tableID ), "default" )
400+ }, 5 * time .Second , 100 * time .Millisecond ,
401+ "the monitor must copy the appeared host default into the awl exemption table" )
402+
403+ // The reconcile must have left the TUN default and the IPv6 fence alone.
404+ assertRoutesApplied (t )
405+
406+ require .NoError (t , mgr .DisableClientRoutes ())
407+ require .False (t , mgr .ClientRoutesActive ())
408+
409+ // Back out the mid-test re-add so the state is comparable with the
410+ // "no defaults" snapshot; teardown must have removed everything else.
411+ require .NoError (t , netlink .RouteDel (& restored ))
412+ require .Equal (t , before , snapshotNet (t ), "teardown must restore the exact pre-setup routing state" )
413+ }
414+
341415// ---------------------------------------------------------------------------
342416// assertions
343417// ---------------------------------------------------------------------------
0 commit comments