Skip to content

Commit

Permalink
Add support for IPv6 (#2190)
Browse files Browse the repository at this point in the history
Problem: User wants to NGINX Gateway Fabric to support IPv6 and IPv4

Solution: Add a new field ipFamily to NginxProxy API to specify the IP family to use with server and update listen directives in the nginx.conf.
  • Loading branch information
salonichf5 authored Jul 17, 2024
1 parent 45190d3 commit 4813408
Show file tree
Hide file tree
Showing 30 changed files with 1,636 additions and 1,002 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ MANIFEST_DIR = $(CURDIR)/deploy/manifests
CHART_DIR = $(SELF_DIR)charts/nginx-gateway-fabric
NGINX_CONF_DIR = internal/mode/static/nginx/conf
NJS_DIR = internal/mode/static/nginx/modules/src
KIND_CONFIG_FILE = $(SELF_DIR)config/cluster/kind-cluster.yaml
NGINX_DOCKER_BUILD_PLUS_ARGS = --secret id=nginx-repo.crt,src=nginx-repo.crt --secret id=nginx-repo.key,src=nginx-repo.key
BUILD_AGENT=local
PLUS_ENABLED ?= false
Expand Down Expand Up @@ -160,7 +161,7 @@ deps: ## Add missing and remove unused modules, verify deps and download them to
.PHONY: create-kind-cluster
create-kind-cluster: ## Create a kind cluster
$(eval KIND_IMAGE=$(shell grep -m1 'FROM kindest/node' <$(SELF_DIR)tests/Dockerfile | awk -F'[ ]' '{print $$2}'))
kind create cluster --image $(KIND_IMAGE)
kind create cluster --image $(KIND_IMAGE) --config $(KIND_CONFIG_FILE)

.PHONY: delete-kind-cluster
delete-kind-cluster: ## Delete kind cluster
Expand Down
20 changes: 20 additions & 0 deletions apis/v1alpha1/nginxproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,28 @@ type NginxProxyList struct {
Items []NginxProxy `json:"items"`
}

// IPFamilyType specifies the IP family to be used by NGINX.
//
// +kubebuilder:validation:Enum=dual;ipv4;ipv6
type IPFamilyType string

const (
// Dual specifies that NGINX will use both IPv4 and IPv6.
Dual IPFamilyType = "dual"
// IPv4 specifies that NGINX will use only IPv4.
IPv4 IPFamilyType = "ipv4"
// IPv6 specifies that NGINX will use only IPv6.
IPv6 IPFamilyType = "ipv6"
)

// NginxProxySpec defines the desired state of the NginxProxy.
type NginxProxySpec struct {
// IPFamily specifies the IP family to be used by the NGINX.
// Default is "dual", meaning the server will use both IPv4 and IPv6.
//
// +optional
// +kubebuilder:default:=dual
IPFamily *IPFamilyType `json:"ipFamily,omitempty"`
// Telemetry specifies the OpenTelemetry configuration.
//
// +optional
Expand Down
5 changes: 5 additions & 0 deletions apis/v1alpha1/zz_generated.deepcopy.go

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

1 change: 1 addition & 0 deletions charts/nginx-gateway-fabric/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ nginx:
config:
{}
# disableHTTP2: false
# ipFamily: dual
# telemetry:
# exporter:
# endpoint: otel-collector.default.svc:4317
Expand Down
7 changes: 7 additions & 0 deletions config/cluster/kind-cluster.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
networking:
ipFamily: dual
apiServerAddress: 127.0.0.1
10 changes: 10 additions & 0 deletions config/crd/bases/gateway.nginx.org_nginxproxies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ spec:
DisableHTTP2 defines if http2 should be disabled for all servers.
Default is false, meaning http2 will be enabled for all servers.
type: boolean
ipFamily:
default: dual
description: |-
IPFamily specifies the IP family to be used by the NGINX.
Default is "dual", meaning the server will use both IPv4 and IPv6.
enum:
- dual
- ipv4
- ipv6
type: string
telemetry:
description: Telemetry specifies the OpenTelemetry configuration.
properties:
Expand Down
10 changes: 10 additions & 0 deletions deploy/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,16 @@ spec:
DisableHTTP2 defines if http2 should be disabled for all servers.
Default is false, meaning http2 will be enabled for all servers.
type: boolean
ipFamily:
default: dual
description: |-
IPFamily specifies the IP family to be used by the NGINX.
Default is "dual", meaning the server will use both IPv4 and IPv6.
enum:
- dual
- ipv4
- ipv6
type: string
telemetry:
description: Telemetry specifies the OpenTelemetry configuration.
properties:
Expand Down
14 changes: 14 additions & 0 deletions docs/developer/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,24 @@ This will build the docker images `nginx-gateway-fabric:<your-user>` and `nginx-

1. Create a `kind` cluster:

To create a `kind` cluster with dual (IPv4 and IPv6) enabled:

```makefile
make create-kind-cluster
```

To create a `kind` cluster with IPv6 or IPv4 only, edit kind cluster config located at `nginx-gateway-fabric/config/cluster/kind-cluster.yaml`:

```yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
networking:
ipFamily: ipv6
apiServerAddress: 127.0.0.1
```
2. Load the previously built images onto your `kind` cluster:

```shell
Expand Down
12 changes: 12 additions & 0 deletions internal/mode/static/nginx/config/http/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ type Server struct {
GRPC bool
}

// IPFamily holds the IP family configuration to be used by NGINX.
type IPFamily struct {
IPv4 bool
IPv6 bool
}

// Location holds all configuration for an HTTP location.
type Location struct {
Path string
Expand Down Expand Up @@ -106,3 +112,9 @@ type ProxySSLVerify struct {
TrustedCertificate string
Name string
}

// ServerConfig holds configuration for an HTTP server and IP family to be used by NGINX.
type ServerConfig struct {
Servers []Server
IPFamily IPFamily
}
19 changes: 18 additions & 1 deletion internal/mode/static/nginx/config/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ var grpcBaseHeaders = []http.Header{
func executeServers(conf dataplane.Configuration) []executeResult {
servers, httpMatchPairs := createServers(conf.HTTPServers, conf.SSLServers)

serverConfig := http.ServerConfig{
Servers: servers,
IPFamily: getIPFamily(conf.BaseHTTPConfig),
}

serverResult := executeResult{
dest: httpConfigFile,
data: helpers.MustExecuteTemplate(serversTemplate, servers),
data: helpers.MustExecuteTemplate(serversTemplate, serverConfig),
}

// create httpMatchPair conf
Expand All @@ -86,6 +91,18 @@ func executeServers(conf dataplane.Configuration) []executeResult {
return allResults
}

// getIPFamily returns whether the server should be configured for IPv4, IPv6, or both.
func getIPFamily(baseHTTPConfig dataplane.BaseHTTPConfig) http.IPFamily {
switch baseHTTPConfig.IPFamily {
case dataplane.IPv4:
return http.IPFamily{IPv4: true}
case dataplane.IPv6:
return http.IPFamily{IPv6: true}
}

return http.IPFamily{IPv4: true, IPv6: true}
}

func createAdditionFileResults(conf dataplane.Configuration) []executeResult {
uniqueAdditions := make(map[string][]byte)

Expand Down
22 changes: 21 additions & 1 deletion internal/mode/static/nginx/config/servers_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,52 @@ package config

const serversTemplateText = `
js_preload_object matches from /etc/nginx/conf.d/matches.json;
{{- range $s := . -}}
{{- range $s := .Servers -}}
{{ if $s.IsDefaultSSL -}}
server {
{{- if $.IPFamily.IPv4 }}
listen {{ $s.Port }} ssl default_server;
{{- end }}
{{- if $.IPFamily.IPv6 }}
listen [::]:{{ $s.Port }} ssl default_server;
{{- end }}
ssl_reject_handshake on;
}
{{- else if $s.IsDefaultHTTP }}
server {
{{- if $.IPFamily.IPv4 }}
listen {{ $s.Port }} default_server;
{{- end }}
{{- if $.IPFamily.IPv6 }}
listen [::]:{{ $s.Port }} default_server;
{{- end }}
default_type text/html;
return 404;
}
{{- else }}
server {
{{- if $s.SSL }}
{{- if $.IPFamily.IPv4 }}
listen {{ $s.Port }} ssl;
{{- end }}
{{- if $.IPFamily.IPv6 }}
listen [::]:{{ $s.Port }} ssl;
{{- end }}
ssl_certificate {{ $s.SSL.Certificate }};
ssl_certificate_key {{ $s.SSL.CertificateKey }};
if ($ssl_server_name != $host) {
return 421;
}
{{- else }}
{{- if $.IPFamily.IPv4 }}
listen {{ $s.Port }};
{{- end }}
{{- if $.IPFamily.IPv6 }}
listen [::]:{{ $s.Port }};
{{- end }}
{{- end }}
server_name {{ $s.ServerName }};
Expand Down
143 changes: 143 additions & 0 deletions internal/mode/static/nginx/config/servers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,117 @@ func TestExecuteServers(t *testing.T) {
}
}

func TestExecuteServersForIPFamily(t *testing.T) {
httpServers := []dataplane.VirtualServer{
{
IsDefault: true,
Port: 8080,
},
{
Hostname: "example.com",
Port: 8080,
},
}
sslServers := []dataplane.VirtualServer{
{
IsDefault: true,
Port: 8443,
},
{
Hostname: "example.com",
SSL: &dataplane.SSL{
KeyPairID: "test-keypair",
},
Port: 8443,
},
}
tests := []struct {
msg string
expectedHTTPConfig map[string]int
config dataplane.Configuration
}{
{
msg: "http and ssl servers with IPv4 IP family",
config: dataplane.Configuration{
HTTPServers: httpServers,
SSLServers: sslServers,
BaseHTTPConfig: dataplane.BaseHTTPConfig{
IPFamily: dataplane.IPv4,
},
},
expectedHTTPConfig: map[string]int{
"listen 8080 default_server;": 1,
"listen 8080;": 1,
"listen 8443 ssl default_server;": 1,
"listen 8443 ssl;": 1,
"server_name example.com;": 2,
"ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1,
"ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1,
"ssl_reject_handshake on;": 1,
},
},
{
msg: "http and ssl servers with IPv6 IP family",
config: dataplane.Configuration{
HTTPServers: httpServers,
SSLServers: sslServers,
BaseHTTPConfig: dataplane.BaseHTTPConfig{
IPFamily: dataplane.IPv6,
},
},
expectedHTTPConfig: map[string]int{
"listen [::]:8080 default_server;": 1,
"listen [::]:8080;": 1,
"listen [::]:8443 ssl default_server;": 1,
"listen [::]:8443 ssl;": 1,
"server_name example.com;": 2,
"ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1,
"ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1,
"ssl_reject_handshake on;": 1,
},
},
{
msg: "http and ssl servers with Dual IP family",
config: dataplane.Configuration{
HTTPServers: httpServers,
SSLServers: sslServers,
BaseHTTPConfig: dataplane.BaseHTTPConfig{
IPFamily: dataplane.Dual,
},
},
expectedHTTPConfig: map[string]int{
"listen 8080 default_server;": 1,
"listen 8080;": 1,
"listen 8443 ssl default_server;": 1,
"listen 8443 ssl;": 1,
"server_name example.com;": 2,
"ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1,
"ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1,
"ssl_reject_handshake on;": 1,
"listen [::]:8080 default_server;": 1,
"listen [::]:8080;": 1,
"listen [::]:8443 ssl default_server;": 1,
"listen [::]:8443 ssl;": 1,
},
},
}

for _, test := range tests {
t.Run(test.msg, func(t *testing.T) {
g := NewWithT(t)
results := executeServers(test.config)
g.Expect(results).To(HaveLen(2))
serverConf := string(results[0].data)
httpMatchConf := string(results[1].data)
g.Expect(httpMatchConf).To(Equal("{}"))

for expSubStr, expCount := range test.expectedHTTPConfig {
g.Expect(strings.Count(serverConf, expSubStr)).To(Equal(expCount))
}
})
}
}

func TestExecuteForDefaultServers(t *testing.T) {
testcases := []struct {
msg string
Expand Down Expand Up @@ -2515,3 +2626,35 @@ func TestAdditionFilename(t *testing.T) {
name := createAdditionFileName(dataplane.Addition{Identifier: "my-addition"})
g.Expect(name).To(Equal(includesFolder + "/" + "my-addition.conf"))
}

func TestGetIPFamily(t *testing.T) {
test := []struct {
msg string
baseHTTPConfig dataplane.BaseHTTPConfig
expected http.IPFamily
}{
{
msg: "ipv4",
baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.IPv4},
expected: http.IPFamily{IPv4: true, IPv6: false},
},
{
msg: "ipv6",
baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.IPv6},
expected: http.IPFamily{IPv4: false, IPv6: true},
},
{
msg: "dual",
baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.Dual},
expected: http.IPFamily{IPv4: true, IPv6: true},
},
}

for _, tc := range test {
t.Run(tc.msg, func(t *testing.T) {
g := NewWithT(t)
result := getIPFamily(tc.baseHTTPConfig)
g.Expect(result).To(Equal(tc.expected))
})
}
}
Loading

0 comments on commit 4813408

Please sign in to comment.