Repository: gardener/network-problem-detector
Pull request #93: Add additional jobs for IPv6 to support dual-stack cluster.
Author: login: axel7born
Labels: name: reviewed/ok-to-test
description: Has approval for testing (check PR in detail before setting this label because PR is run on CI/CD)
name: reviewed/lgtm
description: Has approval for merging
name: needs/ok-to-test
description: Needs approval for testing (check PR in detail before setting this label because PR is run on CI/CD)
name: size/L
description: Denotes a PR that changes 100-499 lines, ignoring generated files.
name: status/closed
description: Issue is closed (either delivered or triaged)
Created: 2025-04-17T09:04:11Z | Merged: 2025-05-06T10:11:41Z

**What this PR does / why we need it**:

**Which issue(s) this PR fixes**:
Fixes #

**Special notes for your reviewer**:

**Release note**:
<!--  Write your release note:
1. Enter your release note in the below block.
2. If no release note is required, just write "NONE" within the block.

Format of block header: <category> <target_group>
Possible values:
- category:       breaking|feature|bugfix|doc|other
- target_group:   user|operator|developer|dependency
-->
```feature user
Support dual-stack clusters.
```



Diff:
diff --git a/README.md b/README.md
index a56e7677..86b09bc6 100644
--- a/README.md
+++ b/README.md
@@ -199,28 +199,31 @@ To examine the current default configuration, run the command
 
 ### Default jobs for the daemon set on the **host network**
 
-| Job ID            | Job Type        | Description                                                                                                                                                           |
-|-------------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `https-n2api-ext` | `checkHTTPSGet` | HTTPS Get check from all pods of the daemon set of the host network to the external address of the Kube API server.                                                   |
-| `nslookup-n`      | `nslookup`      | DNS Lookup of IP addresses for the domain name `europe-docker.pkg.dev`, and external name of Kube API server.                                                                     |
-| `tcp-n2api-ext`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the external address of the Kube API server.                                              |
-| `tcp-n2api-int`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the internal address of the Kube API server.                                              |
-| `tcp-n2n`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the node port used by the NWPD agent on the host network.                                 |
-| `tcp-n2p`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to pod endpoints (pod IP, port of GRPC server) of the daemon set running in the pod network. |
-
-The job IDs of the default configuration on the host (=node) network are using the naming convention `<jobtype-shortcut>-n[2<destination>][-(int|ext)]`.
+| Job ID            | Job Type        | Description                                                                                                                                                                                 |
+|-------------------|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `https-n2api-ext` | `checkHTTPSGet` | HTTPS Get check from all pods of the daemon set of the host network to the external address of the Kube API server.                                                                         |
+| `nslookup-n`      | `nslookup`      | DNS Lookup of IP addresses for the domain name `europe-docker.pkg.dev`, and external name of Kube API server.                                                                               |
+| `tcp-n2api-ext`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the external address of the Kube API server.                                                                    |
+| `tcp-n2api-int`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the internal address of the Kube API server.                                                                    |
+| `tcp-n2n`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the node port used by the NWPD agent on the host network.                                                       |
+| `tcp-n2p`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to pod endpoints (pod IP, port of GRPC server) of the daemon set running in the pod network.                       |
+| `tcp-n2n-ipv6`    | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the node port used by the NWPD agent on the host network using the IPv6 address of the node.                    |
+| `tcp-n2p-ipv6`    | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to pod IPv6 endpoints (IPv6 address of the pod, port of GRPC server) of the daemon set running in the pod network. |
+The job IDs of the default configuration on the host (=node) network are using the naming convention `<jobtype-shortcut>-n[2<destination>][-(int|ext|ipv6)]`.
 
 ### Default jobs for the daemon set on the **cluster network**
 
 
-| Job ID            | Job Type        | Description                                                                                                                                                           |
-|-------------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `https-p2api-ext` | `checkHTTPSGet` | HTTPS Get check from all pods of the daemon set on the cluster network to the external address of the Kube API server.                                                |
-| `https-p2api-int` | `checkHTTPSGet` | HTTPS Get check from all pods of the daemon set on the cluster network to the internal address of the Kube API server (`kubernetes.default.svc.cluster.local.:443`).      |
-| `nslookup-p`      | `nslookup`      | Lookup of IP addresses for external DNS name `europe-docker.pkg.dev`, and internal and external names of Kube API server.                                                         |
-| `tcp-p2api-ext`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set on the cluster network to the external address of the Kube API server.                                               |
-| `tcp-p2api-int`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the cluster network to the internal address of the Kube API server.                                              |
-| `tcp-p2n`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the cluster network to the node port used by the NWPD agent on the host network.                                 |
-| `tcp-p2p`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the cluster network to pod endpoints (pod IP, port of GRPC server) of the daemon set running in the pod network. |
+| Job ID            | Job Type        | Description                                                                                                                                                                                    |
+|-------------------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `https-p2api-ext` | `checkHTTPSGet` | HTTPS Get check from all pods of the daemon set on the cluster network to the external address of the Kube API server.                                                                         |
+| `https-p2api-int` | `checkHTTPSGet` | HTTPS Get check from all pods of the daemon set on the cluster network to the internal address of the Kube API server (`kubernetes.default.svc.cluster.local.:443`).                           |
+| `nslookup-p`      | `nslookup`      | Lookup of IP addresses for external DNS name `europe-docker.pkg.dev`, and internal and external names of Kube API server.                                                                      |
+| `tcp-p2api-ext`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set on the cluster network to the external address of the Kube API server.                                                                    |
+| `tcp-p2api-int`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the cluster network to the internal address of the Kube API server.                                                                    |
+| `tcp-p2n`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the cluster network to the node port used by the NWPD agent on the host network.                                                       |
+| `tcp-p2p`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the cluster network to pod endpoints (pod IP, port of GRPC server) of the daemon set running in the pod network.                       |
+| `tcp-p2n-ipv6`    | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the cluster network to the node port used by the NWPD agent on the host network using the IPv6 address of the node.                    |
+| `tcp-p2p-ipv6`    | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the cluster network to pod IPv6 endpoints (IPv6 address of the pod, port of GRPC server) of the daemon set running in the pod network. |
 
-The job IDs of the default configuration on the cluster (=pod) network are using the naming convention `<jobtype-shortcut>-p[2<destination>][-(int|ext)]`.
+The job IDs of the default configuration on the cluster (=pod) network are using the naming convention `<jobtype-shortcut>-p[2<destination>][-(int|ext|ipv6)]`.
diff --git a/pkg/agent/runners/checktcpport.go b/pkg/agent/runners/checktcpport.go
index 4ff163a8..5e9769c9 100644
--- a/pkg/agent/runners/checktcpport.go
+++ b/pkg/agent/runners/checktcpport.go
@@ -14,13 +14,14 @@ import (
 	"github.com/gardener/network-problem-detector/pkg/common/config"
 
 	"github.com/spf13/cobra"
-	k8snetutils "k8s.io/utils/net"
 )
 
 type checkTCPPortArgs struct {
 	runnerArgs   *runnerArgs
 	nodePort     int
+	nodePortIPv6 int
 	podDS        bool
+	podDSIPv6    bool
 	internalKAPI bool
 	externalKAPI bool
 	endpoints    []string
@@ -57,6 +58,17 @@ func (a *checkTCPPortArgs) createRunner(_ *cobra.Command, _ []string) error {
 				})
 			}
 		}
+	case a.nodePortIPv6 != 0:
+		allowEmpty = true
+		for _, n := range a.runnerArgs.clusterCfg.Nodes {
+			for _, ip := range n.InternalIPsV6 {
+				endpoints = append(endpoints, config.Endpoint{
+					Hostname: n.Hostname,
+					IP:       ip,
+					Port:     a.nodePortIPv6,
+				})
+			}
+		}
 	case a.podDS:
 		allowEmpty = true
 		for _, pe := range a.runnerArgs.clusterCfg.PodEndpoints {
@@ -66,6 +78,15 @@ func (a *checkTCPPortArgs) createRunner(_ *cobra.Command, _ []string) error {
 				Port:     int(pe.Port),
 			})
 		}
+	case a.podDSIPv6:
+		allowEmpty = true
+		for _, pe := range a.runnerArgs.clusterCfg.PodEndpointsV6 {
+			endpoints = append(endpoints, config.Endpoint{
+				Hostname: pe.Nodename,
+				IP:       pe.PodIP,
+				Port:     int(pe.Port),
+			})
+		}
 	case a.internalKAPI:
 		allowEmpty = true
 		if pe := a.runnerArgs.clusterCfg.InternalKubeAPIServer; pe != nil {
@@ -98,7 +119,9 @@ func createCheckTCPPortCmd(ra *runnerArgs) *cobra.Command {
 	}
 	cmd.Flags().StringSliceVar(&a.endpoints, "endpoints", nil, "endpoints in format <hostname>:<ip>:<port>.")
 	cmd.Flags().IntVar(&a.nodePort, "node-port", 0, "port on nodes as alternative to specifying endpoints.")
+	cmd.Flags().IntVar(&a.nodePortIPv6, "node-port-ipv6", 0, "port on nodes via ipv6 address as alternative to specifying endpoints.")
 	cmd.Flags().BoolVar(&a.podDS, "endpoints-of-pod-ds", false, "uses known pod endpoints of the 'nwpd-agent-pod-net' service.")
+	cmd.Flags().BoolVar(&a.podDSIPv6, "endpoints-of-pod-ds-ipv6", false, "uses known pod ipv6 endpoints of the 'nwpd-agent-pod-net' service.")
 	cmd.Flags().BoolVar(&a.internalKAPI, "endpoint-internal-kube-apiserver", false, "uses known internal endpoint of kube-apiserver.")
 	cmd.Flags().BoolVar(&a.externalKAPI, "endpoint-external-kube-apiserver", false, "uses known external endpoint of kube-apiserver.")
 	return cmd
@@ -125,10 +148,7 @@ type checkTCPPort struct {
 var _ Runner = &checkTCPPort{}
 
 func checkTCPPortFunc(endpoint config.Endpoint) (string, error) {
-	addr := fmt.Sprintf("%s:%d", endpoint.IP, endpoint.Port)
-	if k8snetutils.IsIPv6(net.ParseIP(endpoint.IP)) {
-		addr = fmt.Sprintf("[%s]:%d", endpoint.IP, endpoint.Port)
-	}
+	addr := net.JoinHostPort(endpoint.IP, strconv.Itoa(endpoint.Port))
 	conn, err := net.DialTimeout("tcp", addr, 30*time.Second)
 	if err != nil {
 		return "", err
diff --git a/pkg/common/config/endpoint.go b/pkg/common/config/endpoint.go
index 268d0f65..a790ea2c 100644
--- a/pkg/common/config/endpoint.go
+++ b/pkg/common/config/endpoint.go
@@ -9,8 +9,9 @@ type WithDestHost interface {
 }
 
 type Node struct {
-	Hostname    string   `json:"hostname"`
-	InternalIPs []string `json:"internalIPs"`
+	Hostname      string   `json:"hostname"`
+	InternalIPs   []string `json:"internalIPs"`
+	InternalIPsV6 []string `json:"internalIPsV6"`
 }
 
 func (n Node) DestHost() string {
@@ -45,6 +46,8 @@ type ClusterConfig struct {
 	Nodes []Node `json:"nodes,omitempty"`
 	// PodEndpoints is the subset of the known pods of the 'nwpd-agent-pod-net' daemon set.
 	PodEndpoints []PodEndpoint `json:"podEndpoints,omitempty"`
+	// PodEndpoints is the subset of the IPv6 addresses of known pods of the 'nwpd-agent-pod-net' daemon set.
+	PodEndpointsV6 []PodEndpoint `json:"podEndpointsV6,omitempty"`
 	// InternalKubeAPIServer is the discovered internal address of the kube-apiserver
 	InternalKubeAPIServer *Endpoint `json:"internalKubeAPIServer,omitempty"`
 	// KubeAPIServer is the discovered external address of the kube-apiserver (relies on Gardener shoot-info)
diff --git a/pkg/common/config/sample.go b/pkg/common/config/sample.go
index 1d96531d..35022aee 100644
--- a/pkg/common/config/sample.go
+++ b/pkg/common/config/sample.go
@@ -81,6 +81,7 @@ func (sc *SampleConfig) ShuffledSample(cc ClusterConfig) ClusterConfig {
 		NodeCount:             len(cc.Nodes),
 		Nodes:                 CloneAndShuffle(selectSample(sc, cc.Nodes)),
 		PodEndpoints:          CloneAndShuffle(selectSample(sc, cc.PodEndpoints)),
+		PodEndpointsV6:        CloneAndShuffle(selectSample(sc, cc.PodEndpointsV6)),
 		InternalKubeAPIServer: cc.InternalKubeAPIServer,
 		KubeAPIServer:         cc.KubeAPIServer,
 	}
diff --git a/pkg/common/constant.go b/pkg/common/constant.go
index 8ae8441c..6ba4d822 100644
--- a/pkg/common/constant.go
+++ b/pkg/common/constant.go
@@ -13,6 +13,8 @@ const (
 	NameKubernetesService = "kubernetes"
 	// DomainNameKubernetesService is the Kubernetes service domain name.
 	DomainNameKubernetesService = "kubernetes.default.svc.cluster.local."
+	// NameKubeDNSService is the name of the kube-dns service.
+	NameKubeDNSService = "kube-dns"
 	// NameGardenerShootInfo is the name of the shoot info config map from Gardener.
 	NameGardenerShootInfo = "shoot-info"
 	// AgentConfigFilename is the name of the config file.
diff --git a/pkg/deploy/agent.go b/pkg/deploy/agent.go
index 2ce43463..4f116429 100644
--- a/pkg/deploy/agent.go
+++ b/pkg/deploy/agent.go
@@ -60,6 +60,8 @@ type AgentDeployConfig struct {
 	DisableAutomountServiceAccountTokenForAgents bool
 	// MaxPeerNodes if != 0 restricts number of peer nodes used as destinations for checks (nodes are selected randomly, but stable in this case).
 	MaxPeerNodes int
+
+	IPFamilies string
 }
 
 // NetworkProblemDetectorAgent returns K8s resources to be created.
@@ -78,7 +80,7 @@ func NetworkProblemDetectorAgent(config *AgentDeployConfig) ([]Object, error) {
 			return nil, err
 		}
 		objects = append(objects, svc)
-		ds, err := config.buildDaemonSet(serviceAccountName, hostnetwork)
+		ds, err := config.buildDaemonSet(serviceAccountName, hostnetwork, config.IPFamilies)
 		if err != nil {
 			return nil, err
 		}
@@ -149,7 +151,7 @@ func (ac *AgentDeployConfig) getNetworkConfig(hostnetwork bool) (name string, po
 	return
 }
 
-func (ac *AgentDeployConfig) buildDaemonSet(serviceAccountName string, hostNetwork bool) (*appsv1.DaemonSet, error) {
+func (ac *AgentDeployConfig) buildDaemonSet(serviceAccountName string, hostNetwork bool, ipFamilies string) (*appsv1.DaemonSet, error) {
 	var (
 		requestCPU, _          = resource.ParseQuantity("10m")
 		requestMemory, _       = resource.ParseQuantity("32Mi")
@@ -160,8 +162,7 @@ func (ac *AgentDeployConfig) buildDaemonSet(serviceAccountName string, hostNetwo
 
 	labels := ac.getLabels(name)
 	labelsPlusAdditionalLabels := common.MergeMaps(ac.AdditionalLabels, labels)
-	annotations := common.MergeMaps(ac.AdditionalAnnotations, map[string]string{"check-sum/k8s-exporter": strconv.FormatBool(ac.K8sExporterEnabled)})
-
+	annotations := common.MergeMaps(ac.AdditionalAnnotations, map[string]string{"check-sum/k8s-exporter": strconv.FormatBool(ac.K8sExporterEnabled), "check-sum/ipfamily": ipFamilies})
 	var capabilities *corev1.Capabilities
 	if ac.PingEnabled {
 		capabilities = &corev1.Capabilities{
@@ -616,10 +617,18 @@ func (ac *AgentDeployConfig) BuildAgentConfig() (*config.AgentConfig, error) {
 					JobID: "tcp-n2n",
 					Args:  []string{"checkTCPPort", "--node-port", fmt.Sprintf("%d", common.HostNetPodHTTPPort)},
 				},
+				{
+					JobID: "tcp-n2n-ipv6",
+					Args:  []string{"checkTCPPort", "--node-port-ipv6", fmt.Sprintf("%d", common.HostNetPodHTTPPort)},
+				},
 				{
 					JobID: "tcp-n2p",
 					Args:  []string{"checkTCPPort", "--endpoints-of-pod-ds"},
 				},
+				{
+					JobID: "tcp-n2p-ipv6",
+					Args:  []string{"checkTCPPort", "--endpoints-of-pod-ds-ipv6"},
+				},
 				{
 					JobID: "nslookup-n",
 					Args:  []string{"nslookup", "--names", "europe-docker.pkg.dev.", "--scale-period"},
@@ -643,10 +652,18 @@ func (ac *AgentDeployConfig) BuildAgentConfig() (*config.AgentConfig, error) {
 					JobID: "tcp-p2n",
 					Args:  []string{"checkTCPPort", "--node-port", fmt.Sprintf("%d", common.HostNetPodHTTPPort)},
 				},
+				{
+					JobID: "tcp-p2n-ipv6",
+					Args:  []string{"checkTCPPort", "--node-port-ipv6", fmt.Sprintf("%d", common.HostNetPodHTTPPort)},
+				},
 				{
 					JobID: "tcp-p2p",
 					Args:  []string{"checkTCPPort", "--endpoints-of-pod-ds"},
 				},
+				{
+					JobID: "tcp-p2p-ipv6",
+					Args:  []string{"checkTCPPort", "--endpoints-of-pod-ds-ipv6"},
+				},
 				{
 					JobID: "nslookup-p",
 					Args:  []string{"nslookup", "--names", "europe-docker.pkg.dev.", "--name-internal-kube-apiserver", "--scale-period"},
diff --git a/pkg/deploy/clusterconfig.go b/pkg/deploy/clusterconfig.go
index cfc2e840..789f202e 100644
--- a/pkg/deploy/clusterconfig.go
+++ b/pkg/deploy/clusterconfig.go
@@ -17,6 +17,30 @@ import (
 	corev1 "k8s.io/api/core/v1"
 )
 
+func arePodsOfIPFamily(agentPods []*corev1.Pod, ipFamily string) bool {
+	for _, p := range agentPods {
+		found := false
+		for _, podIPEntry := range p.Status.PodIPs {
+			podIP := net.ParseIP(podIPEntry.IP)
+			if podIP == nil {
+				continue
+			}
+			if ipFamily == "IPv4" && podIP.To4() != nil {
+				found = true
+				break
+			}
+			if ipFamily == "IPv6" && podIP.To4() == nil {
+				found = true
+				break
+			}
+		}
+		if !found {
+			return false
+		}
+	}
+	return true
+}
+
 func BuildClusterConfig(
 	log logrus.FieldLogger,
 	nodes []*corev1.Node,
@@ -29,19 +53,32 @@ func BuildClusterConfig(
 		KubeAPIServer:         kubeAPIServer,
 	}
 
+	// Determine the IP family of the pods once
+	arePodsIPv4 := arePodsOfIPFamily(agentPods, "IPv4")
+	arePodsIPv6 := arePodsOfIPFamily(agentPods, "IPv6")
+
 	nodeNames := common.StringSet{}
 	for _, n := range nodes {
 		hostname := ""
 		ips := []string{}
+		ipsV6 := []string{}
 		for _, addr := range n.Status.Addresses {
 			switch addr.Type {
 			case "Hostname":
 				hostname = addr.Address
 			case "InternalIP":
-				ips = append(ips, addr.Address)
+				ip := net.ParseIP(addr.Address)
+				if ip == nil {
+					continue
+				}
+				if ip.To4() != nil && arePodsIPv4 {
+					ips = append(ips, addr.Address)
+				} else if ip.To4() == nil && arePodsIPv6 {
+					ipsV6 = append(ipsV6, addr.Address)
+				}
 			}
 		}
-		if len(ips) == 0 {
+		if len(ips) == 0 && len(ipsV6) == 0 {
 			log.Infof("ignore node %s without internalIP", n.Name)
 			continue
 		}
@@ -49,8 +86,9 @@ func BuildClusterConfig(
 			hostname = n.Name
 		}
 		clusterConfig.Nodes = append(clusterConfig.Nodes, config.Node{
-			Hostname:    hostname,
-			InternalIPs: ips,
+			Hostname:      hostname,
+			InternalIPs:   ips,
+			InternalIPsV6: ipsV6,
 		})
 		nodeNames.Add(hostname)
 	}
@@ -60,12 +98,26 @@ func BuildClusterConfig(
 			continue
 		}
 		for _, podIP := range p.Status.PodIPs {
-			clusterConfig.PodEndpoints = append(clusterConfig.PodEndpoints, config.PodEndpoint{
-				Nodename: p.Spec.NodeName,
-				Podname:  p.Name,
-				PodIP:    podIP.IP,
-				Port:     common.PodNetPodHTTPPort,
-			})
+			ip := net.ParseIP(podIP.IP)
+			if ip == nil {
+				log.Infof("ignore pod %s/%s with invalid podIP %s", p.Namespace, p.Name, podIP.IP)
+				continue
+			}
+			if ip.To4() != nil {
+				clusterConfig.PodEndpoints = append(clusterConfig.PodEndpoints, config.PodEndpoint{
+					Nodename: p.Spec.NodeName,
+					Podname:  p.Name,
+					PodIP:    podIP.IP,
+					Port:     common.PodNetPodHTTPPort,
+				})
+			} else {
+				clusterConfig.PodEndpointsV6 = append(clusterConfig.PodEndpointsV6, config.PodEndpoint{
+					Nodename: p.Spec.NodeName,
+					Podname:  p.Name,
+					PodIP:    podIP.IP,
+					Port:     common.PodNetPodHTTPPort,
+				})
+			}
 		}
 	}
 
@@ -79,6 +131,13 @@ func BuildClusterConfig(
 		}
 		return cmp < 0
 	})
+	sort.Slice(clusterConfig.PodEndpointsV6, func(i, j int) bool {
+		cmp := strings.Compare(clusterConfig.PodEndpointsV6[i].Nodename, clusterConfig.PodEndpointsV6[j].Nodename)
+		if cmp == 0 {
+			cmp = strings.Compare(clusterConfig.PodEndpointsV6[i].Podname, clusterConfig.PodEndpointsV6[j].Podname)
+		}
+		return cmp < 0
+	})
 
 	clusterConfig.NodeCount = len(clusterConfig.Nodes)
 	return clusterConfig, nil
diff --git a/pkg/deploy/clusterconfig_test.go b/pkg/deploy/clusterconfig_test.go
new file mode 100644
index 00000000..6120626d
--- /dev/null
+++ b/pkg/deploy/clusterconfig_test.go
@@ -0,0 +1,273 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package deploy_test
+
+import (
+	"slices"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+
+	"github.com/gardener/network-problem-detector/pkg/common/config"
+	"github.com/gardener/network-problem-detector/pkg/deploy"
+)
+
+var _ = Describe("BuildClusterConfig", func() {
+	It("should build cluster config correctly", func() {
+		log := logrus.New()
+		nodes := []*corev1.Node{
+			{
+				Status: corev1.NodeStatus{
+					Addresses: []corev1.NodeAddress{
+						{Type: corev1.NodeInternalIP, Address: "192.168.1.1"},
+						{Type: corev1.NodeHostName, Address: "node1"},
+					},
+				},
+			},
+		}
+		agentPods := []*corev1.Pod{
+			{
+				Status: corev1.PodStatus{
+					Phase: corev1.PodRunning,
+					PodIPs: []corev1.PodIP{
+						{IP: "10.0.0.1"},
+					},
+				},
+				Spec: corev1.PodSpec{
+					NodeName: "node1",
+				},
+			},
+		}
+		internalKubeAPIServer := &config.Endpoint{
+			Hostname: "internal-api-server",
+			IP:       "192.168.1.2",
+			Port:     443,
+		}
+		kubeAPIServer := &config.Endpoint{
+			Hostname: "api-server",
+			IP:       "192.168.1.3",
+			Port:     443,
+		}
+
+		clusterConfig, err := deploy.BuildClusterConfig(log, nodes, agentPods, internalKubeAPIServer, kubeAPIServer)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(clusterConfig).NotTo(BeNil())
+		Expect(clusterConfig.NodeCount).To(Equal(1))
+		Expect(clusterConfig.Nodes[0].Hostname).To(Equal("node1"))
+		Expect(len(clusterConfig.Nodes[0].InternalIPs)).To(Equal(1))
+		Expect(slices.Contains(clusterConfig.Nodes[0].InternalIPs, ("192.168.1.1"))).To(BeTrue())
+		Expect(clusterConfig.PodEndpoints[0].Nodename).To(Equal("node1"))
+		Expect(clusterConfig.PodEndpoints[0].PodIP).To(Equal("10.0.0.1"))
+	})
+
+	It("should build cluster config correctly with IPv6 addresses", func() {
+		log := logrus.New()
+		nodes := []*corev1.Node{
+			{
+				Status: corev1.NodeStatus{
+					Addresses: []corev1.NodeAddress{
+						{Type: corev1.NodeInternalIP, Address: "2001:db8::1"},
+						{Type: corev1.NodeHostName, Address: "node1"},
+					},
+				},
+			},
+		}
+		agentPods := []*corev1.Pod{
+			{
+				Status: corev1.PodStatus{
+					Phase: corev1.PodRunning,
+					PodIPs: []corev1.PodIP{
+						{IP: "2001:db8::2"},
+					},
+				},
+				Spec: corev1.PodSpec{
+					NodeName: "node1",
+				},
+			},
+		}
+		internalKubeAPIServer := &config.Endpoint{
+			Hostname: "internal-api-server",
+			IP:       "2001:db8::3",
+			Port:     443,
+		}
+		kubeAPIServer := &config.Endpoint{
+			Hostname: "api-server",
+			IP:       "2001:db8::4",
+			Port:     443,
+		}
+
+		clusterConfig, err := deploy.BuildClusterConfig(log, nodes, agentPods, internalKubeAPIServer, kubeAPIServer)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(clusterConfig).NotTo(BeNil())
+		Expect(clusterConfig.NodeCount).To(Equal(1))
+		Expect(clusterConfig.Nodes[0].Hostname).To(Equal("node1"))
+		Expect(len(clusterConfig.Nodes[0].InternalIPsV6)).To(Equal(1))
+		Expect(slices.Contains(clusterConfig.Nodes[0].InternalIPsV6, ("2001:db8::1"))).To(BeTrue())
+		Expect(clusterConfig.PodEndpointsV6[0].Nodename).To(Equal("node1"))
+		Expect(clusterConfig.PodEndpointsV6[0].PodIP).To(Equal("2001:db8::2"))
+	})
+
+	It("should build cluster config correctly with IPv6 and IPv4 addresses in nodes but only IPv4 for pods.", func() {
+		log := logrus.New()
+		nodes := []*corev1.Node{
+			{
+				Status: corev1.NodeStatus{
+					Addresses: []corev1.NodeAddress{
+						{Type: corev1.NodeInternalIP, Address: "2001:db8::1"},
+						{Type: corev1.NodeInternalIP, Address: "192.168.1.1"},
+						{Type: corev1.NodeHostName, Address: "node1"},
+					},
+				},
+			},
+		}
+		agentPods := []*corev1.Pod{
+			{
+				Status: corev1.PodStatus{
+					Phase: corev1.PodRunning,
+					PodIPs: []corev1.PodIP{
+						{IP: "10.0.0.1"},
+					},
+				},
+				Spec: corev1.PodSpec{
+					NodeName: "node1",
+				},
+			},
+		}
+		internalKubeAPIServer := &config.Endpoint{
+			Hostname: "internal-api-server",
+			IP:       "192.168.1.2",
+			Port:     443,
+		}
+		kubeAPIServer := &config.Endpoint{
+			Hostname: "api-server",
+			IP:       "192.168.1.3",
+			Port:     443,
+		}
+
+		clusterConfig, err := deploy.BuildClusterConfig(log, nodes, agentPods, internalKubeAPIServer, kubeAPIServer)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(clusterConfig).NotTo(BeNil())
+		Expect(clusterConfig.NodeCount).To(Equal(1))
+		Expect(clusterConfig.Nodes[0].Hostname).To(Equal("node1"))
+		Expect(len(clusterConfig.Nodes[0].InternalIPs)).To(Equal(1))
+		Expect(slices.Contains(clusterConfig.Nodes[0].InternalIPs, ("192.168.1.1"))).To(BeTrue())
+		Expect(clusterConfig.PodEndpoints[0].Nodename).To(Equal("node1"))
+		Expect(clusterConfig.PodEndpoints[0].PodIP).To(Equal("10.0.0.1"))
+	})
+
+	It("should build cluster config correctly with IPv6 and IPv4 addresses in nodes but only IPv4 for pods.", func() {
+		log := logrus.New()
+		nodes := []*corev1.Node{
+			{
+				Status: corev1.NodeStatus{
+					Addresses: []corev1.NodeAddress{
+						{Type: corev1.NodeInternalIP, Address: "2001:db8::1"},
+						{Type: corev1.NodeInternalIP, Address: "192.168.1.1"},
+						{Type: corev1.NodeHostName, Address: "node1"},
+					},
+				},
+			},
+		}
+		agentPods := []*corev1.Pod{
+			{
+				Status: corev1.PodStatus{
+					Phase: corev1.PodRunning,
+					PodIPs: []corev1.PodIP{
+						{IP: "10.0.0.1"},
+						{IP: "10.0.0.2"},
+					},
+				},
+				Spec: corev1.PodSpec{
+					NodeName: "node1",
+				},
+			},
+			{
+				Status: corev1.PodStatus{
+					Phase: corev1.PodRunning,
+					PodIPs: []corev1.PodIP{
+						{IP: "10.0.0.1"},
+						{IP: "2001:db8::2"},
+					},
+				},
+				Spec: corev1.PodSpec{
+					NodeName: "node1",
+				},
+			},
+		}
+		internalKubeAPIServer := &config.Endpoint{
+			Hostname: "internal-api-server",
+			IP:       "192.168.1.2",
+			Port:     443,
+		}
+		kubeAPIServer := &config.Endpoint{
+			Hostname: "api-server",
+			IP:       "192.168.1.3",
+			Port:     443,
+		}
+
+		clusterConfig, err := deploy.BuildClusterConfig(log, nodes, agentPods, internalKubeAPIServer, kubeAPIServer)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(clusterConfig).NotTo(BeNil())
+		Expect(clusterConfig.NodeCount).To(Equal(1))
+		Expect(clusterConfig.Nodes[0].Hostname).To(Equal("node1"))
+		Expect(len(clusterConfig.Nodes[0].InternalIPs)).To(Equal(1))
+		Expect(slices.Contains(clusterConfig.Nodes[0].InternalIPs, ("192.168.1.1"))).To(BeTrue())
+		Expect(clusterConfig.PodEndpoints[0].Nodename).To(Equal("node1"))
+		Expect(clusterConfig.PodEndpoints[0].PodIP).To(Equal("10.0.0.1"))
+	})
+
+	It("should build cluster config correctly with IPv6 and IPv4 addresses for nodes and for pods.", func() {
+		log := logrus.New()
+		nodes := []*corev1.Node{
+			{
+				Status: corev1.NodeStatus{
+					Addresses: []corev1.NodeAddress{
+						{Type: corev1.NodeInternalIP, Address: "2001:db8::1"},
+						{Type: corev1.NodeInternalIP, Address: "192.168.1.1"},
+						{Type: corev1.NodeHostName, Address: "node1"},
+					},
+				},
+			},
+		}
+		agentPods := []*corev1.Pod{
+			{
+				Status: corev1.PodStatus{
+					Phase: corev1.PodRunning,
+					PodIPs: []corev1.PodIP{
+						{IP: "10.0.0.1"},
+						{IP: "2001:db8::2"},
+					},
+				},
+				Spec: corev1.PodSpec{
+					NodeName: "node1",
+				},
+			},
+		}
+		internalKubeAPIServer := &config.Endpoint{
+			Hostname: "internal-api-server",
+			IP:       "192.168.1.2",
+			Port:     443,
+		}
+		kubeAPIServer := &config.Endpoint{
+			Hostname: "api-server",
+			IP:       "192.168.1.3",
+			Port:     443,
+		}
+
+		clusterConfig, err := deploy.BuildClusterConfig(log, nodes, agentPods, internalKubeAPIServer, kubeAPIServer)
+		Expect(err).NotTo(HaveOccurred())
+		Expect(clusterConfig).NotTo(BeNil())
+		Expect(clusterConfig.NodeCount).To(Equal(1))
+		Expect(clusterConfig.Nodes[0].Hostname).To(Equal("node1"))
+		Expect(len(clusterConfig.Nodes[0].InternalIPs)).To(Equal(1))
+		Expect(len(clusterConfig.Nodes[0].InternalIPsV6)).To(Equal(1))
+		Expect(slices.Contains(clusterConfig.Nodes[0].InternalIPs, ("192.168.1.1"))).To(BeTrue())
+		Expect(slices.Contains(clusterConfig.Nodes[0].InternalIPsV6, ("2001:db8::1"))).To(BeTrue())
+		Expect(clusterConfig.PodEndpoints[0].Nodename).To(Equal("node1"))
+		Expect(clusterConfig.PodEndpoints[0].PodIP).To(Equal("10.0.0.1"))
+	})
+})
diff --git a/pkg/deploy/command.go b/pkg/deploy/command.go
index 79a96cfa..99c12268 100644
--- a/pkg/deploy/command.go
+++ b/pkg/deploy/command.go
@@ -171,12 +171,22 @@ func (dc *deployCommand) deployAgent(log logrus.FieldLogger, hostnetwork bool,
 		return err
 	}
 
-	ds, err := ac.buildDaemonSet(serviceAccountName, hostnetwork)
+	ctx := context.Background()
+	dnssvc, err := dc.Clientset.CoreV1().Services(common.NamespaceKubeSystem).Get(ctx, common.NameKubeDNSService, metav1.GetOptions{})
+	if err != nil {
+		return err
+	}
+	ipFamilies := make([]string, len(dnssvc.Spec.IPFamilies))
+	for i, ipFamily := range dnssvc.Spec.IPFamilies {
+		ipFamilies[i] = string(ipFamily)
+	}
+	ipFamiliesStr := strings.Join(ipFamilies, ",")
+	log.Infof("IP families: %s", ipFamiliesStr)
+	ds, err := ac.buildDaemonSet(serviceAccountName, hostnetwork, ipFamiliesStr)
 	if err != nil {
 		return fmt.Errorf("error building daemon set: %s", err)
 	}
 	objects = append(objects, svc, acm, ccm, ds)
-	ctx := context.Background()
 	for _, obj := range objects {
 		_, err = genericCreateOrUpdate(ctx, dc.Clientset, obj)
 		if err != nil {


Reviews:
user: login: MartinWeindel
body: Code looks good to me.
Can you please reflect the changes in the [job types and default jobs in the README.md](https://github.com/gardener/network-problem-detector/blob/main/README.md#job-types)
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-17T12:02:41Z
user: login: rickardsjp
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T14:54:31Z
user: login: axel7born
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T11:14:04Z
user: login: DerRockWolf
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T19:38:20Z
user: login: axel7born
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:36:51Z
user: login: MartinWeindel
body: /lgtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-06T09:25:34Z

Review comments:
diff_hunk: @@ -199,28 +199,31 @@ To examine the current default configuration, run the command
 
 ### Default jobs for the daemon set on the **host network**
 
-| Job ID            | Job Type        | Description                                                                                                                                                           |
-|-------------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `https-n2api-ext` | `checkHTTPSGet` | HTTPS Get check from all pods of the daemon set of the host network to the external address of the Kube API server.                                                   |
-| `nslookup-n`      | `nslookup`      | DNS Lookup of IP addresses for the domain name `europe-docker.pkg.dev`, and external name of Kube API server.                                                                     |
-| `tcp-n2api-ext`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the external address of the Kube API server.                                              |
-| `tcp-n2api-int`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the internal address of the Kube API server.                                              |
-| `tcp-n2n`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the node port used by the NWPD agent on the host network.                                 |
-| `tcp-n2p`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to pod endpoints (pod IP, port of GRPC server) of the daemon set running in the pod network. |
-
-The job IDs of the default configuration on the host (=node) network are using the naming convention `<jobtype-shortcut>-n[2<destination>][-(int|ext)]`.
+| Job ID            | Job Type        | Description                                                                                                                                                                                 |
+|-------------------|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `https-n2api-ext` | `checkHTTPSGet` | HTTPS Get check from all pods of the daemon set of the host network to the external address of the Kube API server.                                                                         |
+| `nslookup-n`      | `nslookup`      | DNS Lookup of IP addresses for the domain name `europe-docker.pkg.dev`, and external name of Kube API server.                                                                               |
+| `tcp-n2api-ext`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the external address of the Kube API server.                                                                    |
+| `tcp-n2api-int`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the internal address of the Kube API server.                                                                    |
+| `tcp-n2n`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the node port used by the NWPD agent on the host network.                                                       |
+| `tcp-n2p`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to pod endpoints (pod IP, port of GRPC server) of the daemon set running in the pod network.                       |
+| `tcp-n2n-ipv6`    | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the node port used by the NWPD agent on the host network using the IPv6 address of the node.                    |
+| `tcp-n2p-ipv6`    | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to pod IPv6 endpoints (IPv6 address of the pod, port of GRPC server) of the daemon set running in the pod network. |
path: README.md
user: login: rickardsjp
body: From a monitoring perspective this looks good to me. I don't see a massive problem with cardinality (factor of 2 at most) and the number of values that the `jobid` label can take are still limited. I have a question about the naming of the job IDs though: Is there a specific reason not to rename the `tcp-n2n` and `tcp-n2p` job IDs to `tcp-n2n-ipv4` and `tcp-n2p-ipv4`?
created_at: 2025-04-29T14:54:31Z
author_association: MEMBER
diff_hunk: @@ -199,28 +199,31 @@ To examine the current default configuration, run the command
 
 ### Default jobs for the daemon set on the **host network**
 
-| Job ID            | Job Type        | Description                                                                                                                                                           |
-|-------------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `https-n2api-ext` | `checkHTTPSGet` | HTTPS Get check from all pods of the daemon set of the host network to the external address of the Kube API server.                                                   |
-| `nslookup-n`      | `nslookup`      | DNS Lookup of IP addresses for the domain name `europe-docker.pkg.dev`, and external name of Kube API server.                                                                     |
-| `tcp-n2api-ext`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the external address of the Kube API server.                                              |
-| `tcp-n2api-int`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the internal address of the Kube API server.                                              |
-| `tcp-n2n`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the node port used by the NWPD agent on the host network.                                 |
-| `tcp-n2p`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to pod endpoints (pod IP, port of GRPC server) of the daemon set running in the pod network. |
-
-The job IDs of the default configuration on the host (=node) network are using the naming convention `<jobtype-shortcut>-n[2<destination>][-(int|ext)]`.
+| Job ID            | Job Type        | Description                                                                                                                                                                                 |
+|-------------------|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `https-n2api-ext` | `checkHTTPSGet` | HTTPS Get check from all pods of the daemon set of the host network to the external address of the Kube API server.                                                                         |
+| `nslookup-n`      | `nslookup`      | DNS Lookup of IP addresses for the domain name `europe-docker.pkg.dev`, and external name of Kube API server.                                                                               |
+| `tcp-n2api-ext`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the external address of the Kube API server.                                                                    |
+| `tcp-n2api-int`   | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the internal address of the Kube API server.                                                                    |
+| `tcp-n2n`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the node port used by the NWPD agent on the host network.                                                       |
+| `tcp-n2p`         | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to pod endpoints (pod IP, port of GRPC server) of the daemon set running in the pod network.                       |
+| `tcp-n2n-ipv6`    | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to the node port used by the NWPD agent on the host network using the IPv6 address of the node.                    |
+| `tcp-n2p-ipv6`    | `checkTCPPort`  | TCP connection check from all pods of the daemon set of the host network to pod IPv6 endpoints (IPv6 address of the pod, port of GRPC server) of the daemon set running in the pod network. |
path: README.md
user: login: axel7born
body: There is actually not a technical reason. I didn't want to change the name for the majority of single-stack IPv4 clusters. 
created_at: 2025-04-30T11:14:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +17,24 @@ import (
 	corev1 "k8s.io/api/core/v1"
 )
 
+func arePodsOfIPFamily(agentPods []*corev1.Pod, ipFamily string) bool {
+	for _, p := range agentPods {
+		if len(p.Status.PodIPs) == 1 {
+			podIP := net.ParseIP(p.Status.PodIPs[0].IP)
+			if podIP == nil {
+				return false
+			}
+			if ipFamily == "IPv4" && podIP.To4() == nil {
+				return false
+			}
+			if ipFamily == "IPv6" && podIP.To4() != nil {
+				return false
+			}
+		}
+	}
+	return true
+}
path: pkg/deploy/clusterconfig.go
user: login: DerRockWolf
body: I'm currently playing around with this PR and was a bit confused about this function.

Does it assume that if a pod has more than one IP that it must be a dual-stack pod? What about, arguably a very strange edge case, pods with multiple IPv4 addresses?
created_at: 2025-05-04T19:38:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +17,24 @@ import (
 	corev1 "k8s.io/api/core/v1"
 )
 
+func arePodsOfIPFamily(agentPods []*corev1.Pod, ipFamily string) bool {
+	for _, p := range agentPods {
+		if len(p.Status.PodIPs) == 1 {
+			podIP := net.ParseIP(p.Status.PodIPs[0].IP)
+			if podIP == nil {
+				return false
+			}
+			if ipFamily == "IPv4" && podIP.To4() == nil {
+				return false
+			}
+			if ipFamily == "IPv6" && podIP.To4() != nil {
+				return false
+			}
+		}
+	}
+	return true
+}
path: pkg/deploy/clusterconfig.go
user: login: axel7born
body: Good point! I adapted the function to check all IPs.
created_at: 2025-05-05T14:36:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: axel7born
created_at: 2025-04-29T14:38:07Z
body: /cc @rickardsjp 
author_association: CONTRIBUTOR
user: login: DerRockWolf
created_at: 2025-05-01T18:35:57Z
body: The [check](https://github.com/gardener/network-problem-detector/pull/93/files#diff-4d01490ab919019f8eb212baa374d2bf7be4985ffc827a548d117c14521a84fbR175) that determines what `ipFamilies` the cluster has is probably not sufficient, as kubernetes creates this service with ipFamilyPolicy `SingleStack`: 

[pkg/controlplane/controller/kubernetesservice/controller.go](https://github.com/kubernetes/kubernetes/blob/6c05c5e16e1fe280779abaa7ada489a90bf87837/pkg/controlplane/controller/kubernetesservice/controller.go#L236)
author_association: CONTRIBUTOR
user: login: axel7born
created_at: 2025-05-05T14:50:12Z
body: > The [check](https://github.com/gardener/network-problem-detector/pull/93/files#diff-4d01490ab919019f8eb212baa374d2bf7be4985ffc827a548d117c14521a84fbR175) that determines what `ipFamilies` the cluster has is probably not sufficient, as kubernetes creates this service with ipFamilyPolicy `SingleStack`:
> 
> [pkg/controlplane/controller/kubernetesservice/controller.go](https://github.com/kubernetes/kubernetes/blob/6c05c5e16e1fe280779abaa7ada489a90bf87837/pkg/controlplane/controller/kubernetesservice/controller.go#L236)

The `kube-dns` service is set to `PreferDualStack` for Gardener dual-stack clusters. 
However, the result is only used for an annotation that triggers a restart of the nwpd pods. The check here is only used for the deploy via nwpdcli. 

When the daemonset is deployed by the [gardener-extension-shoot-networking-problemdetector](https://github.com/gardener/gardener-extension-shoot-networking-problemdetector) ip families will be determined [here](https://github.com/axel7born/gardener-extension-shoot-networking-problemdetector/blob/020b87c88d194c9ea897648424f814d4432bd8a7/pkg/controller/lifecycle/actuator.go#L143-L152) .


author_association: CONTRIBUTOR