ipn/ipnlocal: warn incompatibility between no-snat-routes and exitnode (#19023)

* ipn/ipnlocal: warn incompatibility between no-snat-routes and exitnode

This commit adds a warning to health check when the --snat-subnet-routes=false flag for subnet router is
set alone side --advertise-exit-node=true. These two would conflict with each other and result internet-bound
traffic from peers using this exit node no masqueraded to the node's source IP and fail to route return
packets back. The described combination is not valid until we figure out a way to separate exitnode masquerade rule and skip it for subnet routes.

Updates #18725

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>

* use date instead of for now to clarify effectivness

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>

---------

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
This commit is contained in:
KevinLiang10
2026-03-26 12:36:31 -04:00
committed by GitHub
parent b4519e97c3
commit 45f989f52a
4 changed files with 107 additions and 0 deletions
+72
View File
@@ -7878,3 +7878,75 @@ func TestEditPrefs_InvalidAdvertiseRoutes(t *testing.T) {
})
}
}
func TestNoSNATWithAdvertisedExitNodeWarning(t *testing.T) {
exitRoutes := []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
}
warnCode := health.WarnableCode("nosnat-with-advertised-exit-node")
tests := []struct {
name string
prefs *ipn.Prefs
wantWarning bool
}{
{
name: "no-snat-without-exit-node",
prefs: &ipn.Prefs{
NoSNAT: true,
AdvertiseRoutes: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
},
wantWarning: false,
},
{
name: "snat-enabled-with-exit-node",
prefs: &ipn.Prefs{
NoSNAT: false,
AdvertiseRoutes: exitRoutes,
},
wantWarning: false,
},
{
name: "no-snat-with-exit-node",
prefs: &ipn.Prefs{
NoSNAT: true,
AdvertiseRoutes: exitRoutes,
},
wantWarning: true,
},
{
name: "no-snat-with-exit-node-and-subnet",
prefs: &ipn.Prefs{
NoSNAT: true,
AdvertiseRoutes: append([]netip.Prefix{
netip.MustParsePrefix("10.0.0.0/24"),
}, exitRoutes...),
},
wantWarning: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := newTestLocalBackend(t)
b.SetPrefsForTest(tt.prefs)
_, hasWarning := b.HealthTracker().CurrentState().Warnings[warnCode]
if hasWarning != tt.wantWarning {
t.Errorf("warning present = %v, want %v", hasWarning, tt.wantWarning)
}
})
}
// Verify that the warning clears when the conflicting combination is resolved.
t.Run("warning-clears-on-fix", func(t *testing.T) {
b := newTestLocalBackend(t)
b.SetPrefsForTest(&ipn.Prefs{NoSNAT: true, AdvertiseRoutes: exitRoutes})
if _, ok := b.HealthTracker().CurrentState().Warnings[warnCode]; !ok {
t.Fatal("expected warning to be set")
}
b.SetPrefsForTest(&ipn.Prefs{NoSNAT: false, AdvertiseRoutes: exitRoutes})
if _, ok := b.HealthTracker().CurrentState().Warnings[warnCode]; ok {
t.Fatal("expected warning to be cleared after enabling SNAT")
}
})
}