Skip to content

Add dual-stack API support for Egress - #8244

Open
XinShuYang wants to merge 1 commit into
antrea-io:mainfrom
XinShuYang:egressdsapi
Open

Add dual-stack API support for Egress#8244
XinShuYang wants to merge 1 commit into
antrea-io:mainfrom
XinShuYang:egressdsapi

Conversation

@XinShuYang

Copy link
Copy Markdown
Contributor

Extend ExternalIPPool subnet configuration to support IPv4 and IPv6 subnets with a shared VLAN. Add Egress IP family selection and explicit dual-stack Egress IPs while preserving the legacy single-stack fields.

Add CRD and admission validation for IP family consistency, subnet matching, mutually exclusive representations, and immutable pool IP families. Remove the unused externalIPPools field.

@XinShuYang
XinShuYang marked this pull request as ready for review August 4, 2026 15:11
@luolanzone
luolanzone requested review from antoninbas, jianjuns, luolanzone, tnqn and wenyingd and removed request for luolanzone August 5, 2026 02:57
@luolanzone luolanzone added this to the Antrea v2.7 release milestone Aug 5, 2026
Comment thread pkg/apis/crd/v1beta1/util_test.go Outdated
Comment thread pkg/controller/egress/validate_test.go Outdated
return raw
}

func mutateEgress(egress *crdv1beta1.Egress, mutate func(*crdv1beta1.Egress)) *crdv1beta1.Egress {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This helper just invokes the mutator and returns the object unchanged — it adds indirection without adding value, turning each test case into nested marshal(mutateEgress(newEgress(...), func(e){ ... })) calls. The repo's test style favors explicit builder functions.

Suggest defining a builder for the dual-stack cases instead, e.g.:

func newDualStackEgress(name, pool string, egressIPs []string, ipFamilies []corev1.IPFamily) *crdv1beta1.Egress

and call it directly from the test table.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion, added newDualStackEgress and refactored the implementation.

- prefixLength
- required:
- ipFamilySubnets
x-kubernetes-validations:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The x-kubernetes-validations list indentation is inconsistent within this PR: egress.yaml keeps list items at the same level as the key (matching the existing ippool.yaml style), while externalippool.yaml indents them by 2 extra spaces (see also line 76). Suggest unifying on the same-level style.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure, updated.

Comment thread pkg/controller/egress/validate.go Outdated
ip net.IP
}

func ipFamilyForIP(ip net.IP) corev1.IPFamily {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ipFamilyForIP here and ipFamilyForAddress in pkg/controller/validation/ippool.go are identical in logic. Consider consolidating into a single shared helper (e.g. in pkg/controller/validation or pkg/util/ip) so the two sites don't drift apart semantically — for example, when handling IPv4-mapped IPv6 addresses in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added IPFamilyForAddress in pkg/util/ip, PTAL.

Comment thread pkg/apis/crd/v1beta1/types.go Outdated

// SubnetInfo specifies subnet attributes for IP Range.
// SubnetInfo specifies subnet attributes for IP ranges.
type SubnetInfo struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we add a new struct, and so IPPool wont be impacted?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure, I named the new struct ExternalIPPoolSubnetInfo.

Comment thread pkg/apis/crd/v1beta1/types.go Outdated
PrefixLength int32 `json:"prefixLength,omitempty"`
// IPFamilySubnets specifies subnet attributes by IP family. At most one entry is allowed for each IP family.
// It cannot be set together with Gateway or PrefixLength.
IPFamilySubnets []IPFamilySubnetInfo `json:"ipFamilySubnets,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would just call the field Gateways and the struct something like SubnetGateway. I hope you can think of better names though :)

Comment thread pkg/apis/crd/v1beta1/types.go Outdated
AppliedTo AppliedTo `json:"appliedTo"`
// EgressIP specifies the SNAT IP address for the selected workloads.
// If ExternalIPPool is empty, it must be specified manually.
// EgressIP is the legacy field that specifies one SNAT IP address for the selected workloads.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would keep the original comment - EgressIP specifies a single SNAT IP address for the selected workloads.

If we want to start deprecating the field, we can add: "// +deprecated:warning="EgressIP is deprecated, use EgressIPs instead."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

EgressIP is still used for single-stack Egress, I have updated related comments.

Comment thread pkg/apis/crd/v1beta1/types.go Outdated
Comment thread pkg/apis/crd/v1beta1/types.go Outdated
Comment thread pkg/apis/crd/v1beta1/types.go Outdated
Comment thread pkg/apis/crd/v1beta1/types.go Outdated
ExternalIPPools []string `json:"externalIPPools,omitempty"`
// IPFamilies specifies the IP families for which Egress IPs should be allocated. At most one entry is allowed for
// each IP family. It may be omitted for a single-stack ExternalIPPool or when EgressIP or EgressIPs is specified.
IPFamilies []corev1.IPFamily `json:"ipFamilies,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use IPFamilyPolicy instead? Even for dual-stack IPPool, we should default to dual-stack allocation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Make sense to me, I have updated it to IPFamilyPolicy. The default value is PreferDualStack.

@XinShuYang
XinShuYang force-pushed the egressdsapi branch 2 times, most recently from 73dcef7 to b5cfbb2 Compare August 11, 2026 16:30
Comment thread pkg/util/ip/ip.go

// IPFamilyForAddress returns the Kubernetes IP family for the provided address.
func IPFamilyForAddress(address netip.Addr) corev1.IPFamily {
if address.Unmap().Is4() {

@XinShuYang XinShuYang Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should IPv4-mapped IPv6 addresses be treated as IPv4 here, or should we reject them during API validation? @jianjuns @luolanzone

Comment thread pkg/apis/crd/v1beta1/types.go Outdated
Comment thread pkg/apis/crd/v1beta1/types.go Outdated
Comment thread pkg/apis/crd/v1beta1/types.go Outdated
Comment thread pkg/apis/crd/v1beta1/types.go Outdated
EgressIP string `json:"egressIP,omitempty"`
// EgressIPs specifies multiple SNAT IP addresses for the selected workloads.
// Cannot be set with EgressIP.
// EgressIPs specifies the IPv4 and IPv6 SNAT IP addresses for the selected workloads. It must contain exactly two

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we support single stack allocation in the same version too, even if not in this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can we support single stack allocation in the same version too, even if not in this PR?

Maybe we can discuss the egressIP/egressIPs unification and compatibility checks after all dual-stack Egress PRs are merged? For now, I‘d prefer to make sure all legacy single-stack Egress behavior remains consistent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can discuss separately, but I am not saying you must change the existing EgressIP implementation. I am saying we should support single stack allocation with ExternalIPs too. I would avoid changing API behaviors again in the next Antrea version.

@XinShuYang XinShuYang Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jianjuns Do you mean supporting single-stack allocation with egressIPs as well? Given Antonin's point about the version bump, I'm now thinking of using spec.egressIPs and status.egressIPs for both single-stack and dual-stack Egresses and removing spec.egressIP and status.egressIP in v1beta2, since the API compatibility concerns would be much less significant. cc @antoninbas @luolanzone @tnqn

Comment thread pkg/apis/crd/v1beta1/types.go Outdated
// Cannot be set with ExternalIPPool.
ExternalIPPools []string `json:"externalIPPools,omitempty"`
// IPFamilyPolicy specifies whether the Egress is single-stack or dual-stack. It defaults to PreferDualStack.
IPFamilyPolicy *corev1.IPFamilyPolicy `json:"ipFamilyPolicy,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Humm.. seems IPFamilyPolicy cannot specify v4 or v6 for a single stack allocation. Probably let us still use ipFamilies.

Could we by default use the ExternalIPPool's IP families when the filed not set?

I assume this field should be ignored when EgressIP or EgressIPs are specified?

@XinShuYang XinShuYang Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The explicitly specified EgressIP or EgressIPs determines the actual IP family configuration. However, I don't think IPFamilyPolicy should be silently ignored: we should reject contradictory combinations, such as EgressIP with RequireDualStack or EgressIPs with SingleStack. When no explicit IP is provided, the policy controls automatic allocation from the ExternalIPPool.

For single stack allocation, I think we can still get the IP family info from the externalIPPool or egressIP? For the default value of preferDualStack, a single-stack ExternalIPPool can still determine the IP family based on its own configuration. Do you still think there are any blockers to using IPFamilyPolicy? @jianjuns

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am fine if you prefer a strict validation to make EgressIP(s) and specified IP stack(s) consistent.

For single stack allocation, I think we can still get the IP family info from the externalIPPool or egressIP? For the default value of preferDualStack, a single-stack ExternalIPPool can still determine the IP family based on its own configuration. Do you still think there are any blockers to using IPFamilyPolicy?

How to indicate I want to allocate a v6 IP from a dual-stack pool?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

How to indicate I want to allocate a v6 IP from a dual-stack pool?

For IPFamilyPolicy in this case, I would let it select the IP family based on the first entry in ipRanges of the dual-stack pool. If you think this is too tricky and we need to support this case explicitly, we probably have to revert to using the ipFamilies field. Could you share more thoughts? @jianjuns

Comment thread pkg/apis/crd/v1beta1/types.go Outdated
Comment thread pkg/apis/crd/v1beta1/types.go Outdated
Comment thread pkg/apis/crd/v1beta1/types.go Outdated
Extend ExternalIPPool subnet configuration to support IPv4 and IPv6
subnets with a shared VLAN. Add Egress IP family selection and explicit
dual-stack Egress IPs while preserving the legacy single-stack fields.

Add CRD and admission validation for IP family consistency, subnet
matching, mutually exclusive representations, and immutable pool IP
families. Remove the unused externalIPPools field.

Signed-off-by: Shuyang Xin <xin_shuyang@hotmail.com>

@antoninbas antoninbas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Even if we assume the egressIPs and externalIPPools fields were not technically used, I feel like we have to be a bit more careful with the API change, and I think we should bump up the API version to v1beta2.

Bumping the API version allows us to do the following 2 things, which otherwise we should not do:

  • remove the externalIPPools field
  • change the validation rules for the egressIPs field

And if we introduce a new API version, we have more freedom when it comes to the new ExternalIPPoolSubnetInfo struct. We can drop gateway and prefixLength for example.

It can also be argued that if we don't bump the API version, we must make sure the top-level gateway and prefixLength fields should match gateways[0]: https://github.com/kubernetes/community/blob/main/contributors/devel/sig-architecture/api_changes.md#making-a-singular-field-plural

The conversion webhook from v1beta1 to v1beta2 should be pretty straightforward to implement.

TLDR; I would strongly recommend introducing a new v1beta2 API version, unless someone can make a valid argument against it.

// If both ExternalIPPool and EgressIP are non-empty, the IP must be in the pool.
// EgressIP specifies a single SNAT IP address for the selected workloads. It is used only for single-stack Egresses.
// If ExternalIPPool is not specified, the EgressIP field must be specified manually. If ExternalIPPool is specified,
// EgressIP field is optional, and an IP will be automatically assigned by Antrea automatically when it is not specified. If both

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you wrap lines at a fixed length?

// EgressIP specifies a single SNAT IP address for the selected workloads. It is used only for single-stack Egresses.
// If ExternalIPPool is not specified, the EgressIP field must be specified manually. If ExternalIPPool is specified,
// EgressIP field is optional, and an IP will be automatically assigned by Antrea automatically when it is not specified. If both
// ExternalIPPool and EgressIP are specified, the IP must be in the pool.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably move EgressIP to be before ExternalIPPool -> "EgressIP and ExternalIPPool are.."

EgressIP string `json:"egressIP,omitempty"`
// EgressIPs specifies multiple SNAT IP addresses for the selected workloads.
// Cannot be set with EgressIP.
// EgressIPs specifies the IPv4 and IPv6 SNAT IP addresses for the selected workloads. It must contain exactly two

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can discuss separately, but I am not saying you must change the existing EgressIP implementation. I am saying we should support single stack allocation with ExternalIPs too. I would avoid changing API behaviors again in the next Antrea version.

// If it is non-empty, the EgressIP will be assigned to a Node specified by the pool automatically and will failover
// to a different Node when the Node becomes unreachable.
// ExternalIPPool specifies the IP Pool that the EgressIP(s) should be allocated from. If it is not set, the specified EgressIP(s)
// should be assigned to a Node manually. If it is specified, the EgressIP(s) will be assigned to a Node specified by the pool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If ExternalIPPool is provided

// If it is empty, the specified EgressIP must be assigned to a Node manually.
// If it is non-empty, the EgressIP will be assigned to a Node specified by the pool automatically and will failover
// to a different Node when the Node becomes unreachable.
// ExternalIPPool specifies the IP Pool that the EgressIP(s) should be allocated from. If it is not set, the specified EgressIP(s)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

set -> provided

// If both ExternalIPPool and EgressIP are non-empty, the IP must be in the pool.
// EgressIP specifies a single SNAT IP address for the selected workloads. It is used only for single-stack Egresses.
// If ExternalIPPool is not specified, the EgressIP field must be specified manually. If ExternalIPPool is specified,
// EgressIP field is optional, and an IP will be automatically assigned by Antrea automatically when it is not specified. If both

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

EgressIP field -> the EgressIP field

// Cannot be set with ExternalIPPool.
ExternalIPPools []string `json:"externalIPPools,omitempty"`
// IPFamilyPolicy specifies whether the Egress is single-stack or dual-stack. It defaults to PreferDualStack.
IPFamilyPolicy *corev1.IPFamilyPolicy `json:"ipFamilyPolicy,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am fine if you prefer a strict validation to make EgressIP(s) and specified IP stack(s) consistent.

For single stack allocation, I think we can still get the IP family info from the externalIPPool or egressIP? For the default value of preferDualStack, a single-stack ExternalIPPool can still determine the IP family based on its own configuration. Do you still think there are any blockers to using IPFamilyPolicy?

How to indicate I want to allocate a v6 IP from a dual-stack pool?

@jianjuns

Copy link
Copy Markdown
Contributor

Even if we assume the egressIPs and externalIPPools fields were not technically used, I feel like we have to be a bit more careful with the API change, and I think we should bump up the API version to v1beta2.

So, in which Antrea version egressIPs and externalIPPools were added? If it is 2.7, we still have a chance to exclude that change? If it is a released version, why we shipped unused API fields?

@antoninbas antoninbas closed this Aug 12, 2026
@antoninbas

Copy link
Copy Markdown
Contributor

Closed by mistake...

@antoninbas antoninbas reopened this Aug 12, 2026
@XinShuYang

Copy link
Copy Markdown
Contributor Author

Even if we assume the egressIPs and externalIPPools fields were not technically used, I feel like we have to be a bit more careful with the API change, and I think we should bump up the API version to v1beta2.

So, in which Antrea version egressIPs and externalIPPools were added? If it is 2.7, we still have a chance to exclude that change? If it is a released version, why we shipped unused API fields?

@jianjuns AFAIK, it was first introduced in this PR three years ago: #4603

@jianjuns

Copy link
Copy Markdown
Contributor

@jianjuns AFAIK, it was first introduced in this PR three years ago: #4603

Antonin told me the background for that change. I think we probably should not remove the field, at least not with this PR.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants