Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ipv4/ipv6 dual stack support #4375

Merged
merged 5 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions api/v1alpha1/backend_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type BackendEndpoint struct {
// +optional
FQDN *FQDNEndpoint `json:"fqdn,omitempty"`

// IP defines an IP endpoint. Currently, only IPv4 Addresses are supported.
// IP defines an IP endpoint. Supports both IPv4 and IPv6 addresses.
//
// +optional
IP *IPEndpoint `json:"ip,omitempty"`
Expand All @@ -73,10 +73,11 @@ type BackendEndpoint struct {
// https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-socketaddress
type IPEndpoint struct {
// Address defines the IP address of the backend endpoint.
// Supports both IPv4 and IPv6 addresses.
//
// +kubebuilder:validation:MinLength=7
// +kubebuilder:validation:MaxLength=15
// +kubebuilder:validation:Pattern=`^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`
// +kubebuilder:validation:MinLength=3
// +kubebuilder:validation:MaxLength=45
// +kubebuilder:validation:Pattern=`^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{1,4}|::|(([0-9a-fA-F]{1,4}:){0,5})?(:[0-9a-fA-F]{1,4}){1,2})$`
Address string `json:"address"`

// Port defines the port of the backend endpoint.
Expand Down
25 changes: 25 additions & 0 deletions api/v1alpha1/envoyproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,17 @@ type EnvoyProxySpec struct {
// These settings are applied on backends for which TLS policies are specified.
// +optional
BackendTLS *BackendTLSConfig `json:"backendTLS,omitempty"`

// IPFamily specifies the IP family for the EnvoyProxy fleet.
// This setting only affects the Gateway listener port and does not impact
// other aspects of the Envoy proxy configuration.
// If not specified, the system will operate as follows:
// - It defaults to IPv4 only.
// - IPv6 and dual-stack environments are not supported in this default configuration.
// Note: To enable IPv6 or dual-stack functionality, explicit configuration is required.
// +kubebuilder:validation:Enum=IPv4;IPv6;DualStack
// +optional
IPFamily *IPFamily `json:"ipFamily,omitempty"`
juwon8891 marked this conversation as resolved.
Show resolved Hide resolved
}

// RoutingType defines the type of routing of this Envoy proxy.
Expand Down Expand Up @@ -415,6 +426,20 @@ type EnvoyProxyList struct {
Items []EnvoyProxy `json:"items"`
}

// IPFamily defines the IP family to use for the Envoy proxy.
type IPFamily string

const (
// IPv4 defines the IPv4 family.
IPv4 IPFamily = "IPv4"
// IPv6 defines the IPv6 family.
IPv6 IPFamily = "IPv6"
// DualStack defines the dual-stack family.
// When set to DualStack, Envoy proxy will listen on both IPv4 and IPv6 addresses
// for incoming client traffic, enabling support for both IP protocol versions.
DualStack IPFamily = "DualStack"
juwon8891 marked this conversation as resolved.
Show resolved Hide resolved
)

func init() {
SchemeBuilder.Register(&EnvoyProxy{}, &EnvoyProxyList{})
}
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,16 @@ spec:
- port
type: object
ip:
description: IP defines an IP endpoint. Currently, only IPv4
Addresses are supported.
description: IP defines an IP endpoint. Supports both IPv4 and
IPv6 addresses.
properties:
address:
description: Address defines the IP address of the backend
endpoint.
maxLength: 15
minLength: 7
pattern: ^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
description: |-
Address defines the IP address of the backend endpoint.
Supports both IPv4 and IPv6 addresses.
maxLength: 45
minLength: 3
pattern: ^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{1,4}|::|(([0-9a-fA-F]{1,4}:){0,5})?(:[0-9a-fA-F]{1,4}){1,2})$
type: string
port:
description: Port defines the port of the backend endpoint.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,20 @@ spec:
rule: (has(self.before) && !has(self.after)) || (!has(self.before)
&& has(self.after))
type: array
ipFamily:
description: |-
IPFamily specifies the IP family for the EnvoyProxy fleet.
This setting only affects the Gateway listener port and does not impact
other aspects of the Envoy proxy configuration.
If not specified, the system will operate as follows:
- It defaults to IPv4 only.
- IPv6 and dual-stack environments are not supported in this default configuration.
Note: To enable IPv6 or dual-stack functionality, explicit configuration is required.
enum:
- IPv4
- IPv6
- DualStack
type: string
logging:
default:
level:
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/egctl/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ metadata:
namespace: default
spec:
...
local validation error: Backend.gateway.envoyproxy.io "backend-1" is invalid: spec.endpoints[0].ip.address: Invalid value: "a.b.c.d": spec.endpoints[0].ip.address in body should match '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
local validation error: Backend.gateway.envoyproxy.io "backend-1" is invalid: spec.endpoints[0].ip.address: Invalid value: "a.b.c.d": spec.endpoints[0].ip.address in body should match '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{1,4}|::|(([0-9a-fA-F]{1,4}:){0,5})?(:[0-9a-fA-F]{1,4}){1,2})$'

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: Backend
Expand Down
9 changes: 2 additions & 7 deletions internal/gatewayapi/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,8 @@ func validateBackend(backend *egv1a1.Backend) error {
ip, err := netip.ParseAddr(ep.IP.Address)
if err != nil {
return fmt.Errorf("IP address %s is invalid", ep.IP.Address)
} else {
if !ip.Is4() {
return fmt.Errorf("IP address %s is not IPv4", ep.IP.Address)
}
if ip.IsLoopback() {
return fmt.Errorf("IP address %s in the loopback range is not supported", ep.IP.Address)
}
} else if ip.IsLoopback() {
return fmt.Errorf("IP address %s in the loopback range is not supported", ep.IP.Address)
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions internal/gatewayapi/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,3 +608,21 @@ func setIfNil[T any](target **T, value *T) {
*target = value
}
}

func getIPFamily(envoyProxy *egv1a1.EnvoyProxy) *ir.IPFamily {
if envoyProxy == nil || envoyProxy.Spec.IPFamily == nil {
return nil
}
var result ir.IPFamily
switch *envoyProxy.Spec.IPFamily {
case egv1a1.IPv4:
result = ir.IPv4
case egv1a1.IPv6:
result = ir.IPv6
case egv1a1.DualStack:
result = ir.Dualstack
default:
return nil
}
return &result
}
11 changes: 8 additions & 3 deletions internal/gatewayapi/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,17 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource
Address: "0.0.0.0",
Port: uint32(containerPort),
Metadata: buildListenerMetadata(listener, gateway),
IPFamily: getIPFamily(gateway.envoyProxy),
},
TLS: irTLSConfigs(listener.tlsSecrets...),
Path: ir.PathSettings{
MergeSlashes: true,
EscapedSlashesAction: ir.UnescapeAndRedirect,
},
}
if ipFamily := getIPFamily(gateway.envoyProxy); ipFamily != nil {
irListener.CoreListenerDetails.IPFamily = ipFamily
}
if listener.Hostname != nil {
irListener.Hostnames = append(irListener.Hostnames, string(*listener.Hostname))
} else {
Expand All @@ -129,9 +133,10 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource
case gwapiv1.TCPProtocolType, gwapiv1.TLSProtocolType:
irListener := &ir.TCPListener{
CoreListenerDetails: ir.CoreListenerDetails{
Name: irListenerName(listener),
Address: "0.0.0.0",
Port: uint32(containerPort),
Name: irListenerName(listener),
Address: "0.0.0.0",
Port: uint32(containerPort),
IPFamily: getIPFamily(gateway.envoyProxy),
},

// Gateway is processed firstly, then ClientTrafficPolicy, then xRoute.
Expand Down
77 changes: 77 additions & 0 deletions internal/gatewayapi/resource/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
discoveryv1 "k8s.io/api/discovery/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
)
Expand Down Expand Up @@ -123,3 +125,78 @@ func TestEqualXds(t *testing.T) {
})
}
}

func TestGetEndpointSlicesForBackendDualStack(t *testing.T) {
// Test data setup
dualStackService := &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "dual-stack-service",
Namespace: "default",
Labels: map[string]string{
discoveryv1.LabelServiceName: "my-dual-stack-service",
},
},
AddressType: discoveryv1.AddressTypeIPv4,
Endpoints: []discoveryv1.Endpoint{
{
Addresses: []string{"192.0.2.1"},
},
{
Addresses: []string{"192.0.2.2"},
},
},
}

dualStackServiceIPv6 := &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "dual-stack-service-ipv6",
Namespace: "default",
Labels: map[string]string{
discoveryv1.LabelServiceName: "my-dual-stack-service",
},
},
AddressType: discoveryv1.AddressTypeIPv6,
Endpoints: []discoveryv1.Endpoint{
{
Addresses: []string{"2001:db8::1"},
},
{
Addresses: []string{"2001:db8::2"},
},
},
}

resources := &Resources{
EndpointSlices: []*discoveryv1.EndpointSlice{dualStackService, dualStackServiceIPv6},
}

t.Run("Dual Stack Service", func(t *testing.T) {
result := resources.GetEndpointSlicesForBackend("default", "my-dual-stack-service", KindService)

assert.Len(t, result, 2, "Expected 2 EndpointSlices for dual-stack service")

var ipv4Slice, ipv6Slice *discoveryv1.EndpointSlice
for _, slice := range result {
if slice.AddressType == discoveryv1.AddressTypeIPv4 {
ipv4Slice = slice
} else if slice.AddressType == discoveryv1.AddressTypeIPv6 {
ipv6Slice = slice
}
}

assert.NotNil(t, ipv4Slice, "Expected to find an IPv4 EndpointSlice")
assert.NotNil(t, ipv6Slice, "Expected to find an IPv6 EndpointSlice")

if ipv4Slice != nil {
assert.Len(t, ipv4Slice.Endpoints, 2, "Expected 2 IPv4 endpoints")
assert.Equal(t, "192.0.2.1", ipv4Slice.Endpoints[0].Addresses[0], "Unexpected IPv4 address")
assert.Equal(t, "192.0.2.2", ipv4Slice.Endpoints[1].Addresses[0], "Unexpected IPv4 address")
}

if ipv6Slice != nil {
assert.Len(t, ipv6Slice.Endpoints, 2, "Expected 2 IPv6 endpoints")
assert.Equal(t, "2001:db8::1", ipv6Slice.Endpoints[0].Addresses[0], "Unexpected IPv6 address")
assert.Equal(t, "2001:db8::2", ipv6Slice.Endpoints[1].Addresses[0], "Unexpected IPv6 address")
}
})
}
Loading