33
44package ipcache
55
6- // TestHostIPWorldFallbackDuringRestartWindow and TestHostIdentityRestorationGap
7- // reproduce a bug observed in production (us1.fed.dog, 2026-03-20) where node/host
8- // IPs in a local-DC CIDR were misclassified as "world" identity during rolling
9- // Cilium agent restarts, causing policy_denied drops against cluster-dns.
6+ // TestHostIPWorldFallbackDuringRestartWindow and related tests reproduce a bug
7+ // where node/host IPs that fall within a CiliumCIDRGroup CIDR are transiently
8+ // misclassified as "world" identity during rolling Cilium agent restarts.
109//
1110// Root cause (two code paths, both required):
1211//
1312// 1. pkg/ipcache/restore/local_identity_restorer.go:128
14- // dumpOldIPCache() filters restored identities to IdentityScopeLocal and
15- // ReservedIdentityIngress only. ReservedIdentityHost (scope=global, id=1) is
16- // explicitly excluded. After ipcachemap.Recreate(), the new BPF map has no
17- // entry for host IPs.
13+ // dumpOldIPCache() only restores IdentityScopeLocal and ReservedIdentityIngress
14+ // identities. ReservedIdentityHost (scope=global, id=1) is explicitly excluded.
15+ // After ipcachemap.Recreate(), the new BPF ipcache map has no entry for host IPs.
1816//
1917// 2. daemon/cmd/daemon.go startup ordering
2018// K8sWatcher.InitK8sSubsystem() starts at line 202 (begins processing
21- // CiliumCIDRGroups managed by fabric-k8s-controller ). syncHostIPs.StartAndWaitFirst()
22- // is not called until line 249. During this window, a host IP in the local-DC
23- // CiliumCIDRGroup (e.g. 10.160.0.0/14) receives only a cidrgroup label.
19+ // CiliumCIDRGroups). syncHostIPs.StartAndWaitFirst() is not called until
20+ // line 249. During this window, a host IP covered by a CiliumCIDRGroup
21+ // receives only a cidrgroup label — no reserved:host .
2422//
2523// 3. pkg/ipcache/metadata.go:798 (resolveLabels)
2624// Any IP without reserved:host, reserved:remote-node, reserved:health, or
27- // reserved:ingress label has AddWorldLabel() called on it. A host IP with only
28- // a cidrgroup label therefore becomes world — which is not covered by the
29- // cluster-dns CNP's "fromEntities: cluster" ingress rule, causing drops.
25+ // reserved:ingress has AddWorldLabel() called on it. A host IP with only
26+ // a cidrgroup label is therefore assigned world identity.
27+ //
28+ // Impact: CNPs using "fromEntities: cluster" do not cover world (id=2). Traffic
29+ // from the misclassified host IP is denied with policy_denied.
3030
3131import (
3232 "net/netip"
@@ -42,22 +42,23 @@ import (
4242 "github.com/cilium/cilium/pkg/source"
4343)
4444
45- // cidrGroupLabels returns a Labels set simulating what the CiliumCIDRGroup reconciler
46- // (fabric-k8s-controller) injects via UpsertMetadata for an IP that matches a
47- // CiliumCIDRGroup (e.g. the "local-dc" group covering 10.160.0.0/14 ).
45+ // cidrGroupLabels returns a Labels set simulating what a CiliumCIDRGroup
46+ // reconciler injects via UpsertMetadata for an IP that matches a
47+ // CiliumCIDRGroup (e.g. a group covering a node-local subnet ).
4848func cidrGroupLabels (groupName string ) labels.Labels {
4949 return labels.Labels {
5050 groupName : labels .NewLabel (groupName , "" , labels .LabelSourceCIDRGroup ),
5151 }
5252}
5353
5454// TestHostIPWorldFallbackDuringRestartWindow reproduces the bug where a host IP
55- // is assigned world identity because resolveLabels() runs with only cidrgroup labels
56- // — before syncHostIPs has inserted the reserved:host label .
55+ // covered by a CiliumCIDRGroup is assigned world identity because resolveLabels()
56+ // runs with only cidrgroup labels — before syncHostIPs has inserted reserved:host.
5757//
58- // This test asserts the CURRENT BUGGY BEHAVIOR. It is expected to fail once the
59- // bug is fixed (e.g. by ensuring host IPs are seeded into ipcache metadata before
60- // CiliumCIDRGroup processing can trigger resolveLabels for those prefixes).
58+ // This test asserts the CURRENT BUGGY BEHAVIOUR. It is expected to fail once
59+ // the root cause is fixed (e.g. by ensuring host IPs are seeded into ipcache
60+ // metadata before CiliumCIDRGroup processing can trigger resolveLabels for
61+ // those prefixes, or by restoring host identity entries in dumpOldIPCache).
6162func TestHostIPWorldFallbackDuringRestartWindow (t * testing.T ) {
6263 s := setupIPCacheTestSuite (t )
6364 ctx := t .Context ()
@@ -67,54 +68,53 @@ func TestHostIPWorldFallbackDuringRestartWindow(t *testing.T) {
6768 t .Cleanup (func () { option .Config .PolicyCIDRMatchMode = oldVal })
6869 option .Config .PolicyCIDRMatchMode = []string {}
6970
70- // The host IP observed in production: 10.161.39.126 (in 10.160.0.0/14, localDc CIDR).
71- // 8,258 drops were recorded against cluster-dns over 48h.
71+ // A host IP that falls within a CiliumCIDRGroup subnet.
7272 hostIPPrefix := cmtypes .NewLocalPrefixCluster (netip .MustParsePrefix ("10.161.39.126/32" ))
7373
7474 // ── Stage 1: Restart window ──────────────────────────────────────────────
75- // K8sWatcher has processed the "local-dc" CiliumCIDRGroup. The ipcache BPF
76- // map has been recreated empty (RestoreLocalIdentities skipped this IP since
77- // ReservedIdentityHost is not locally-scoped). syncHostIPs has NOT run yet.
78- //
79- // Only the cidrgroup label is present — no reserved:host.
75+ // The K8s watcher has processed a CiliumCIDRGroup covering this IP's subnet.
76+ // The ipcache BPF map has been recreated empty (dumpOldIPCache skipped this
77+ // IP since ReservedIdentityHost is not locally-scoped). syncHostIPs has NOT
78+ // run yet — only the cidrgroup label is present.
8079 s .IPIdentityCache .metadata .upsertLocked (
8180 hostIPPrefix ,
8281 source .Generated ,
8382 "cidrgroup-resource-uid" ,
84- cidrGroupLabels ("local-dc " ),
83+ cidrGroupLabels ("example- local-subnet " ),
8584 )
8685
8786 _ , err := s .IPIdentityCache .doInjectLabels (ctx , []cmtypes.PrefixCluster {hostIPPrefix })
8887 require .NoError (t , err )
8988
9089 entry , ok := s .IPIdentityCache .ipToIdentityCache ["10.161.39.126/32" ]
91- require .True (t , ok , "expected an identity entry for 10.161.39.126/32 " )
90+ require .True (t , ok , "expected an identity entry for the host IP " )
9291
9392 assignedID := entry .ID
9493
9594 // Verify the assigned identity is NOT reserved:host (id=1).
9695 // This demonstrates the bug: the IP should be host but is not.
9796 assert .NotEqual (t , identity .ReservedIdentityHost , assignedID ,
98- "BUG REPRODUCED: host IP 10.161.39.126 was not assigned ReservedIdentityHost (id=1). " +
99- "Got id=%d. This occurs because resolveLabels() ran with only cidrgroup labels " +
100- "(no reserved:host) during the restart window before syncHostIPs executed." ,
97+ "BUG REPRODUCED: host IP was not assigned ReservedIdentityHost (id=1). " +
98+ "Got id=%d. This occurs because resolveLabels() ran with only cidrgroup " +
99+ "labels (no reserved:host) during the restart window before syncHostIPs " +
100+ "executed." ,
101101 assignedID )
102102
103- // Verify the assigned identity has a world label — the world fallback fired.
103+ // Verify the assigned identity carries a world label — the world fallback fired.
104104 resolvedIdentity := s .Allocator .LookupIdentityByID (ctx , assignedID )
105105 require .NotNil (t , resolvedIdentity , "identity %d should be resolvable" , assignedID )
106106 assert .True (t ,
107107 resolvedIdentity .Labels .HasWorldLabel () || resolvedIdentity .Labels .HasWorldIPv4Label (),
108- "BUG: host IP 10.161.39.126/32 was assigned world identity (id=%d, labels=%v). " +
108+ "BUG: host IP was assigned world identity (id=%d, labels=%v). " +
109109 "resolveLabels() called AddWorldLabel() because HasHostLabel()=false. " +
110- "This causes policy_denied drops: the cluster-dns CNP allows 'fromEntities: cluster' " +
111- "but world (id=2) is not in the cluster entity." ,
110+ "Traffic from this IP will be denied by CNPs that use 'fromEntities: cluster' " +
111+ "because world (id=2) is not in the cluster entity." ,
112112 assignedID , resolvedIdentity .Labels )
113113
114114 // ── Stage 2: syncHostIPs runs ────────────────────────────────────────────
115- // After daemon initialization completes (daemon.go:249), syncHostIPs inserts
116- // the reserved:host label for this IP . resolveLabels() now sees HasHostLabel()=true,
117- // sets isInCluster=true, removes the cidrgroup label , and does NOT add world.
115+ // After daemon initialisation completes (daemon.go:249), syncHostIPs inserts
116+ // the reserved:host label. resolveLabels() now sees HasHostLabel()=true,
117+ // sets isInCluster=true, removes cidrgroup labels , and does NOT add world.
118118 s .IPIdentityCache .metadata .upsertLocked (
119119 hostIPPrefix ,
120120 source .Local ,
@@ -135,7 +135,7 @@ func TestHostIPWorldFallbackDuringRestartWindow(t *testing.T) {
135135}
136136
137137// TestWorldFallbackDoesNotOccurWhenHostLabelPresentFirst verifies the CORRECT
138- // behaviour: when reserved:host is present before CIDRGroup labels are processed ,
138+ // behaviour: when reserved:host is already present before CIDRGroup labels arrive ,
139139// resolveLabels() correctly identifies the IP as in-cluster and does not add
140140// the world label.
141141//
@@ -164,7 +164,7 @@ func TestWorldFallbackDoesNotOccurWhenHostLabelPresentFirst(t *testing.T) {
164164 hostIPPrefix ,
165165 source .Generated ,
166166 "cidrgroup-resource-uid" ,
167- cidrGroupLabels ("local-dc " ),
167+ cidrGroupLabels ("example- local-subnet " ),
168168 )
169169
170170 _ , err := s .IPIdentityCache .doInjectLabels (ctx , []cmtypes.PrefixCluster {hostIPPrefix })
@@ -173,10 +173,10 @@ func TestWorldFallbackDoesNotOccurWhenHostLabelPresentFirst(t *testing.T) {
173173 entry , ok := s .IPIdentityCache .ipToIdentityCache ["10.161.39.126/32" ]
174174 require .True (t , ok )
175175
176- // When reserved:host is present, the identity must be ReservedIdentityHost.
176+ // When reserved:host is present first, identity must be ReservedIdentityHost.
177177 assert .Equal (t , identity .ReservedIdentityHost , entry .ID ,
178- "When reserved:host is already in ipcache metadata before CIDRGroup labels " +
179- "arrive, the identity must be ReservedIdentityHost (id=1). Got id=%d." , entry .ID )
178+ "When reserved:host is in ipcache metadata before CIDRGroup labels arrive, " +
179+ "the identity must be ReservedIdentityHost (id=1). Got id=%d." , entry .ID )
180180
181181 resolvedIdentity := s .Allocator .LookupIdentityByID (ctx , entry .ID )
182182 require .NotNil (t , resolvedIdentity )
0 commit comments