@@ -198,6 +198,66 @@ func TestCleanName(t *testing.T) {
198198 assert .Equal (t , "service" , nr .cleanName (& s , "127.0.0.1" , "service.k8snamespace.svc.cluster.local." ))
199199}
200200
201+ func TestParseK8sFQDN (t * testing.T ) {
202+ tests := []struct {
203+ name string
204+ fqdn string
205+ expectedName string
206+ expectedNS string
207+ }{
208+ {
209+ name : "standard K8s FQDN" ,
210+ fqdn : "bar-server.bar-ns.svc.cluster.local" ,
211+ expectedName : "bar-server" ,
212+ expectedNS : "bar-ns" ,
213+ },
214+ {
215+ name : "with trailing dot" ,
216+ fqdn : "myservice.mynamespace.svc.cluster.local." ,
217+ expectedName : "myservice" ,
218+ expectedNS : "mynamespace" ,
219+ },
220+ {
221+ name : "case insensitive suffix" ,
222+ fqdn : "svc.ns.SVC.CLUSTER.LOCAL" ,
223+ expectedName : "svc" ,
224+ expectedNS : "ns" ,
225+ },
226+ {
227+ name : "just service name (no namespace in FQDN)" ,
228+ fqdn : "myservice.svc.cluster.local" ,
229+ expectedName : "myservice" ,
230+ expectedNS : "" ,
231+ },
232+ {
233+ name : "not a K8s FQDN - plain hostname" ,
234+ fqdn : "example.com" ,
235+ expectedName : "example.com" ,
236+ expectedNS : "" ,
237+ },
238+ {
239+ name : "not a K8s FQDN - IP address" ,
240+ fqdn : "10.0.0.1" ,
241+ expectedName : "10.0.0.1" ,
242+ expectedNS : "" ,
243+ },
244+ {
245+ name : "empty string" ,
246+ fqdn : "" ,
247+ expectedName : "" ,
248+ expectedNS : "" ,
249+ },
250+ }
251+
252+ for _ , tt := range tests {
253+ t .Run (tt .name , func (t * testing.T ) {
254+ name , ns := parseK8sFQDN (tt .fqdn )
255+ assert .Equal (t , tt .expectedName , name )
256+ assert .Equal (t , tt .expectedNS , ns )
257+ })
258+ }
259+ }
260+
201261func TestResolveNodesFromK8s (t * testing.T ) {
202262 inf := & fakeInformer {}
203263 db := kube .NewStore (inf , kube.ResourceLabels {}, nil , imetrics.NoopReporter {})
@@ -341,3 +401,57 @@ func TestResolveClientFromHost(t *testing.T) {
341401 assert .Equal (t , "pod2" , serverSpan .HostName ) // we don't match the IP in k8s, but we have a service name
342402 assert .Equal (t , "something" , serverSpan .Service .UID .Namespace )
343403}
404+
405+ // TestResolveClientFromHost_K8sFQDN demonstrates the following scenario:
406+ // - A client (foo-client) makes an HTTP request to a K8s Service (bar-server)
407+ // - The destination IP seen by eBPF is the Pod IP (after kube-proxy NAT)
408+ // - The Pod IP is not in the K8s informer cache
409+ // - The HTTP Host header contains the K8s Service FQDN
410+ func TestResolveClientFromHost_K8sFQDN (t * testing.T ) {
411+ // Create a K8s store with NO matching entries for the destination Pod IP
412+ // This simulates the case where the Pod IP can't be resolved via K8s metadata
413+ inf := & fakeInformer {}
414+ db := kube .NewStore (inf , kube.ResourceLabels {}, nil , imetrics.NoopReporter {})
415+
416+ // Add only the source pod to the store, not the destination
417+ sourcePod := & informer.ObjectMeta {
418+ Name : "foo-client-abc123" ,
419+ Namespace : "foo-ns" ,
420+ Kind : "Pod" ,
421+ Ips : []string {"10.0.1.1" },
422+ Pod : & informer.PodInfo {
423+ Owners : []* informer.Owner {{Kind : "Deployment" , Name : "foo-client" }},
424+ },
425+ }
426+ inf .Notify (& informer.Event {Type : informer .EventType_CREATED , Resource : sourcePod })
427+
428+ // The destination Pod IP (10.0.2.5) is NOT in the store
429+ // This simulates the NAT scenario where we see the Pod IP but can't resolve it
430+ nr := NameResolver {
431+ db : db ,
432+ cache : expirable .NewLRU [string , string ](10 , nil , 5 * time .Hour ),
433+ sources : resolverSources ([]string {"k8s" }),
434+ }
435+
436+ // Create a client span representing an HTTP call to a K8s Service
437+ // The HTTP Host header contains the K8s Service FQDN
438+ clientSpan := request.Span {
439+ Type : request .EventTypeHTTPClient ,
440+ Peer : "10.0.1.1" ,
441+ // Destination: Pod IP after NAT (NOT in K8s store)
442+ Host : "10.0.2.5" ,
443+ // HTTP Host header captured by eBPF, stored as "scheme;host"
444+ Statement : "http;bar-server.bar-ns.svc.cluster.local" ,
445+ Service : svc.Attrs {
446+ UID : svc.UID {
447+ Name : "foo-client" ,
448+ Namespace : "foo-ns" ,
449+ },
450+ },
451+ }
452+
453+ nr .resolveNames (& clientSpan )
454+
455+ assert .Equal (t , "bar-server" , clientSpan .HostName )
456+ assert .Equal (t , "bar-ns" , clientSpan .OtherNamespace )
457+ }
0 commit comments