Skip to content

Commit

Permalink
feat: dualstack support
Browse files Browse the repository at this point in the history
Signed-off-by: Juwon Hwang (Kevin) <[email protected]>
  • Loading branch information
juwon8891 committed Oct 23, 2024
1 parent 7188dad commit 8a39a13
Show file tree
Hide file tree
Showing 326 changed files with 1,427 additions and 37 deletions.
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
26 changes: 26 additions & 0 deletions api/v1alpha1/envoyproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ 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:default=IPv4
// +kubebuilder:validation:Enum=IPv4;IPv6;DualStack
// +optional
IPFamily *IPFamily `json:"ipFamily,omitempty"`
}

// RoutingType defines the type of routing of this Envoy proxy.
Expand Down Expand Up @@ -415,6 +427,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"
)

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,21 @@ spec:
rule: (has(self.before) && !has(self.after)) || (!has(self.before)
&& has(self.after))
type: array
ipFamily:
default: IPv4
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
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ xdsIR:
- address: 0.0.0.0
hostnames:
- '*'
ipFamily: IPv4
isHTTP2: false
metadata:
kind: Gateway
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
14 changes: 14 additions & 0 deletions internal/gatewayapi/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,3 +592,17 @@ 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 ir.IPv4
}
switch *envoyProxy.Spec.IPFamily {
case egv1a1.IPv6:
return ir.IPv6
case egv1a1.DualStack:
return ir.Dualstack
default:
return ir.IPv4
}
}
8 changes: 5 additions & 3 deletions internal/gatewayapi/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ 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{
Expand All @@ -129,9 +130,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

0 comments on commit 8a39a13

Please sign in to comment.