Skip to content

Conversation

@antoninbas
Copy link
Contributor

@antoninbas antoninbas commented Nov 25, 2025

Add IPv6 and dual-stack support to NodePortLocal on Linux Nodes using iptables/ip6tables. NPL previously only supported IPv4.

Key changes:

  1. Separate NPL mappings per IP family: For dual-stack Services, NPL now creates independent mappings for IPv4 and IPv6, each with its own Node IP and Node port. The required IP families are determined from the Service's .spec.ipFamilies field.

  2. IPv4 and IPv6 port space: Node ports for IPv4 and IPv6 are allocated independently from the same port range, as iptables and ip6tables rules operate independently and do not interfere.

  3. Node IP address selection: The NPL controller now watches the Node object to obtain Node IP addresses for both IP families, prioritizing external IPs over internal IPs. When Node IPs change, all local Pods are automatically reconciled with updated NPL annotations. Note that prior to this change, internal Node IPs had precedence, but external IPs make more sense for a feature such as this one.

  4. IP family tracking: Introduced an efficient ipFamilies bitmask type to track sets of IP families with no memory allocations.

  5. Annotation format: NPL annotations now include an ipFamily field to distinguish between IPv4 and IPv6 mappings. For example:

[
  {"podPort":8080,"nodeIP":"10.10.10.10","nodePort":61002, "protocol":"tcp","ipFamily":"IPv4"},
  {"podPort":8080,"nodeIP":"fd12:3456:789a:1::1", "nodePort":61003,"protocol":"tcp","ipFamily":"IPv6"}
]
  1. PortTable and iptables rule management: The NPL controller gets one PortTable per IP family and all rules are managed independently. The AddAllRules function now performs separate restore operations for each IP family.

  2. Port allocation: Modified LocalPortOpener.OpenLocalPort to accept an isIPv6 parameter and bind to the appropriate network type (tcp4/tcp6, udp4/udp6) based on the Pod IP family.

Implementation notes:

  • Assumes Kubernetes version >= 1.23, which guarantees the availability of .spec.ipFamilies on Services and .status.podIPs on Pods.
  • Windows support remains IPv4-only and is not affected by this change.
  • The implementation maintains backward compatibility by treating existing annotations without the ipFamily field as IPv4.
  • All existing unit tests have been updated, and new tests have been added to verify Node IP updates and dual-stack functionality.
  • E2E tests have been updated to support IPv6 and dual-stack testing. All NPL Services are created with the PreferDualStack policy.

Fixes #7513

@antoninbas
Copy link
Contributor Author

/test-ipv6-e2e
/test-ipv6-only-e2e

@antoninbas antoninbas added area/proxy/nodeportlocal Issues or PRs related to the NodePortLocal feature area/transit/ipv6 Issues or PRs related to IPv6. labels Nov 25, 2025
@antoninbas
Copy link
Contributor Author

/test-kind-ipv6-e2e
/test-kind-ipv6-only-e2e

@antoninbas
Copy link
Contributor Author

/test-kind-ipv6-e2e
/test-kind-ipv6-only-e2e

1 similar comment
@antoninbas
Copy link
Contributor Author

/test-kind-ipv6-e2e
/test-kind-ipv6-only-e2e

@antoninbas
Copy link
Contributor Author

/test-kind-ipv6-e2e
/test-kind-ipv6-only-e2e

1 similar comment
@antoninbas
Copy link
Contributor Author

/test-kind-ipv6-e2e
/test-kind-ipv6-only-e2e

Add IPv6 and dual-stack support to NodePortLocal on Linux Nodes using
iptables/ip6tables. NPL previously only supported IPv4.

Key changes:

1. **Separate NPL mappings per IP family**: For dual-stack Services, NPL
   now creates independent mappings for IPv4 and IPv6, each with its own
   Node IP and Node port. The required IP families are determined from
   the Service's `.spec.ipFamilies` field.

2. **IPv4 and IPv6 port space**: Node ports for IPv4 and IPv6 are
   allocated independently from the same port range, as iptables and
   ip6tables rules operate independently and do not interfere.

3. **Node IP address selection**: The NPL controller now watches the
   Node object to obtain Node IP addresses for both IP families,
   prioritizing external IPs over internal IPs. When Node IPs change,
   all local Pods are automatically reconciled with updated NPL
   annotations. Note that prior to this change, internal Node IPs had
   precedence, but external IPs make more sense for a feature such as
   this one.

4. **IP family tracking**: Introduced an efficient `ipFamilies` bitmask
   type to track sets of IP families with no memory allocations.

5. **Annotation format**: NPL annotations now include an `ipFamily`
   field to distinguish between IPv4 and IPv6 mappings. For example:
   ```json
   [
     {"podPort":8080,"nodeIP":"10.10.10.10","nodePort":61002,
      "protocol":"tcp","ipFamily":"IPv4"},
     {"podPort":8080,"nodeIP":"fd12:3456:789a:1::1",
      "nodePort":61003,"protocol":"tcp","ipFamily":"IPv6"}
   ]
   ```

6. **PortTable and iptables rule management**: The NPL controller gets
   one PortTable per IP family and all rules are managed
   independently. The `AddAllRules` function now performs separate
   restore operations for each IP family.

7. **Port allocation**: Modified `LocalPortOpener.OpenLocalPort` to
   accept an `isIPv6` parameter and bind to the appropriate network type
   (tcp4/tcp6, udp4/udp6) based on the Pod IP family.

Implementation notes:

- Assumes Kubernetes version >= 1.23, which guarantees the availability
  of `.spec.ipFamilies` on Services and `.status.podIPs` on Pods.
- Windows support remains IPv4-only and is not affected by this change.
- The implementation maintains backward compatibility by treating
  existing annotations without the `ipFamily` field as IPv4.
- All existing unit tests have been updated, and new tests have been
  added to verify Node IP updates and dual-stack functionality.
- E2E tests have been updated to support IPv6 and dual-stack
  testing. All NPL Services are created with the PreferDualStack policy.

Fixes antrea-io#7513

Signed-off-by: Antonin Bas <[email protected]>
@antoninbas
Copy link
Contributor Author

/test-kind-ipv6-e2e
/test-kind-ipv6-only-e2e

@antoninbas
Copy link
Contributor Author

/test-kind-ipv6-e2e
/test-kind-ipv6-only-e2e

@antoninbas antoninbas marked this pull request as ready for review December 1, 2025 22:35
@antoninbas antoninbas requested review from luolanzone and tnqn December 1, 2025 22:35
@antoninbas
Copy link
Contributor Author

/test-kind-ipv6-e2e
/test-kind-ipv6-only-e2e

@antoninbas
Copy link
Contributor Author

/test-kind-ipv6-e2e

@luolanzone luolanzone added the action/release-note Indicates a PR that should be included in release notes. label Dec 9, 2025
// This will be handled gracefully by the NPL controller: if there is an
// annotation using this port, it will be removed and replaced with a new
// one with a valid port mapping.
klog.ErrorS(err, "Cannot bind to local port, skipping it", "port", nplPort.NodePort)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
klog.ErrorS(err, "Cannot bind to local port, skipping it", "port", nplPort.NodePort)
klog.ErrorS(err, "Cannot bind to local port, skipping it", "port", nplPort.NodePort, "ipv6", pt.IsIPv6)

)

// Bubble time will automatically advance when goroutines are blocked.
portTable.RestoreRules(t.Context(), allNPLPorts)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we can have some ipv6 NPL ports in allNPLPorts to verify the ipv6?

expectedAnnotation := a.find(nplAnnotation.PodPort, nplAnnotation.Protocol)
if !assert.NotNilf(t, expectedAnnotation, "Unexpected annotation with PodPort %d", nplAnnotation.PodPort) {
expectedAnnotation := a.find(nplAnnotation.PodPort, nplAnnotation.Protocol, nplAnnotation.IPFamily)
if !assert.NotNilf(t, expectedAnnotation, "Unexpected annotation with PodPort %d, Protocol %s, IPFamily %s", nplAnnotation.PodPort, nplAnnotation.Protocol, nplAnnotation.IPFamily) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if !assert.NotNilf(t, expectedAnnotation, "Unexpected annotation with PodPort %d, Protocol %s, IPFamily %s", nplAnnotation.PodPort, nplAnnotation.Protocol, nplAnnotation.IPFamily) {
if !assert.NotNilf(t, expectedAnnotation, "Unexpected annotation with Pod port %d, Protocol %s, IPFamily %s", nplAnnotation.PodPort, nplAnnotation.Protocol, nplAnnotation.IPFamily) {

if portData != nil && portData.Defunct() {
klog.InfoS("Deleting defunct NodePortLocal rule for Pod to prevent re-use", "pod", klog.KObj(pod), "podIP", podIP, "port", port, "protocol", protocol)
if err := portTable.DeleteRule(key, port, protocol); err != nil {
return fmt.Errorf("failed to delete defunct rule for Pod %s, Pod Port %d, Protocol %s: %w", key, port, protocol, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fmt.Errorf("failed to delete defunct rule for Pod %s, Pod Port %d, Protocol %s: %w", key, port, protocol, err)
return fmt.Errorf("failed to delete defunct rule for Pod %s, Pod port %d, Protocol %s: %w", key, port, protocol, err)

if portData != nil && portData.PodIP != podIP {
klog.InfoS("Deleting NodePortLocal rule for Pod because of IP change", "pod", klog.KObj(pod), "podIP", podIP, "prevPodIP", portData.PodIP)
if err := portTable.DeleteRule(key, port, protocol); err != nil {
return fmt.Errorf("failed to delete rule for Pod %s, Pod Port %d, Protocol %s: %w", key, port, protocol, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

const (
IPFamilyIPv4 IPFamilyType = "IPv4"
IPFamilyIPv6 IPFamilyType = "IPv6"
IPFamilyUnknown IPFamilyType = ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not seeing it's used anywhere.

}

return nplk8s.NewNPLController(kubeClient, podInformer, serviceInformer.Informer(), portTable, nodeName), nil
if ipv6Enabled {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ipv6 is not supported on Windows, I suppose we should skip this on Windows platform?

@luolanzone luolanzone requested a review from XinShuYang December 9, 2025 07:45
NodeIP string `json:"nodeIP"`
NodePort int `json:"nodePort"`
Protocol string `json:"protocol"`
IPFamily IPFamilyType `json:"ipFamily"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we update annotation examples in the document?

return fmt.Errorf("failed to add rule for Pod %s: %v", key, err)

portTable := c.getPortTableForFamily(ipFamily)
portData := portTable.GetEntry(key, port, protocol)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to call portTable.GetEntry without portTable nil check?

}

if c.updateNodeIPs(newNode) {
klog.InfoS("Node IPs changed, reconciling all Pods", "node", klog.KObj(newNode), "IPv4", c.nodeIPv4, "IPv6", c.nodeIPv6)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected to print unprotected c.nodeIPv4 and c.nodeIPv6 here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action/release-note Indicates a PR that should be included in release notes. area/proxy/nodeportlocal Issues or PRs related to the NodePortLocal feature area/transit/ipv6 Issues or PRs related to IPv6.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NodePortLocal: Support IPv6 and dual-stack

3 participants