Skip to content

Commit 4813408

Browse files
authored
Add support for IPv6 (#2190)
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.
1 parent 45190d3 commit 4813408

30 files changed

+1636
-1002
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ MANIFEST_DIR = $(CURDIR)/deploy/manifests
77
CHART_DIR = $(SELF_DIR)charts/nginx-gateway-fabric
88
NGINX_CONF_DIR = internal/mode/static/nginx/conf
99
NJS_DIR = internal/mode/static/nginx/modules/src
10+
KIND_CONFIG_FILE = $(SELF_DIR)config/cluster/kind-cluster.yaml
1011
NGINX_DOCKER_BUILD_PLUS_ARGS = --secret id=nginx-repo.crt,src=nginx-repo.crt --secret id=nginx-repo.key,src=nginx-repo.key
1112
BUILD_AGENT=local
1213
PLUS_ENABLED ?= false
@@ -160,7 +161,7 @@ deps: ## Add missing and remove unused modules, verify deps and download them to
160161
.PHONY: create-kind-cluster
161162
create-kind-cluster: ## Create a kind cluster
162163
$(eval KIND_IMAGE=$(shell grep -m1 'FROM kindest/node' <$(SELF_DIR)tests/Dockerfile | awk -F'[ ]' '{print $$2}'))
163-
kind create cluster --image $(KIND_IMAGE)
164+
kind create cluster --image $(KIND_IMAGE) --config $(KIND_CONFIG_FILE)
164165

165166
.PHONY: delete-kind-cluster
166167
delete-kind-cluster: ## Delete kind cluster

apis/v1alpha1/nginxproxy_types.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,28 @@ type NginxProxyList struct {
2727
Items []NginxProxy `json:"items"`
2828
}
2929

30+
// IPFamilyType specifies the IP family to be used by NGINX.
31+
//
32+
// +kubebuilder:validation:Enum=dual;ipv4;ipv6
33+
type IPFamilyType string
34+
35+
const (
36+
// Dual specifies that NGINX will use both IPv4 and IPv6.
37+
Dual IPFamilyType = "dual"
38+
// IPv4 specifies that NGINX will use only IPv4.
39+
IPv4 IPFamilyType = "ipv4"
40+
// IPv6 specifies that NGINX will use only IPv6.
41+
IPv6 IPFamilyType = "ipv6"
42+
)
43+
3044
// NginxProxySpec defines the desired state of the NginxProxy.
3145
type NginxProxySpec struct {
46+
// IPFamily specifies the IP family to be used by the NGINX.
47+
// Default is "dual", meaning the server will use both IPv4 and IPv6.
48+
//
49+
// +optional
50+
// +kubebuilder:default:=dual
51+
IPFamily *IPFamilyType `json:"ipFamily,omitempty"`
3252
// Telemetry specifies the OpenTelemetry configuration.
3353
//
3454
// +optional

apis/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/nginx-gateway-fabric/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ nginx:
9292
config:
9393
{}
9494
# disableHTTP2: false
95+
# ipFamily: dual
9596
# telemetry:
9697
# exporter:
9798
# endpoint: otel-collector.default.svc:4317

config/cluster/kind-cluster.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kind: Cluster
2+
apiVersion: kind.x-k8s.io/v1alpha4
3+
nodes:
4+
- role: control-plane
5+
networking:
6+
ipFamily: dual
7+
apiServerAddress: 127.0.0.1

config/crd/bases/gateway.nginx.org_nginxproxies.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ spec:
5252
DisableHTTP2 defines if http2 should be disabled for all servers.
5353
Default is false, meaning http2 will be enabled for all servers.
5454
type: boolean
55+
ipFamily:
56+
default: dual
57+
description: |-
58+
IPFamily specifies the IP family to be used by the NGINX.
59+
Default is "dual", meaning the server will use both IPv4 and IPv6.
60+
enum:
61+
- dual
62+
- ipv4
63+
- ipv6
64+
type: string
5565
telemetry:
5666
description: Telemetry specifies the OpenTelemetry configuration.
5767
properties:

deploy/crds.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,16 @@ spec:
697697
DisableHTTP2 defines if http2 should be disabled for all servers.
698698
Default is false, meaning http2 will be enabled for all servers.
699699
type: boolean
700+
ipFamily:
701+
default: dual
702+
description: |-
703+
IPFamily specifies the IP family to be used by the NGINX.
704+
Default is "dual", meaning the server will use both IPv4 and IPv6.
705+
enum:
706+
- dual
707+
- ipv4
708+
- ipv6
709+
type: string
700710
telemetry:
701711
description: Telemetry specifies the OpenTelemetry configuration.
702712
properties:

docs/developer/quickstart.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,24 @@ This will build the docker images `nginx-gateway-fabric:<your-user>` and `nginx-
134134

135135
1. Create a `kind` cluster:
136136

137+
To create a `kind` cluster with dual (IPv4 and IPv6) enabled:
138+
137139
```makefile
138140
make create-kind-cluster
139141
```
140142

143+
To create a `kind` cluster with IPv6 or IPv4 only, edit kind cluster config located at `nginx-gateway-fabric/config/cluster/kind-cluster.yaml`:
144+
145+
```yaml
146+
kind: Cluster
147+
apiVersion: kind.x-k8s.io/v1alpha4
148+
nodes:
149+
- role: control-plane
150+
networking:
151+
ipFamily: ipv6
152+
apiServerAddress: 127.0.0.1
153+
```
154+
141155
2. Load the previously built images onto your `kind` cluster:
142156

143157
```shell

internal/mode/static/nginx/config/http/config.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ type Server struct {
1212
GRPC bool
1313
}
1414

15+
// IPFamily holds the IP family configuration to be used by NGINX.
16+
type IPFamily struct {
17+
IPv4 bool
18+
IPv6 bool
19+
}
20+
1521
// Location holds all configuration for an HTTP location.
1622
type Location struct {
1723
Path string
@@ -106,3 +112,9 @@ type ProxySSLVerify struct {
106112
TrustedCertificate string
107113
Name string
108114
}
115+
116+
// ServerConfig holds configuration for an HTTP server and IP family to be used by NGINX.
117+
type ServerConfig struct {
118+
Servers []Server
119+
IPFamily IPFamily
120+
}

internal/mode/static/nginx/config/servers.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,14 @@ var grpcBaseHeaders = []http.Header{
6060
func executeServers(conf dataplane.Configuration) []executeResult {
6161
servers, httpMatchPairs := createServers(conf.HTTPServers, conf.SSLServers)
6262

63+
serverConfig := http.ServerConfig{
64+
Servers: servers,
65+
IPFamily: getIPFamily(conf.BaseHTTPConfig),
66+
}
67+
6368
serverResult := executeResult{
6469
dest: httpConfigFile,
65-
data: helpers.MustExecuteTemplate(serversTemplate, servers),
70+
data: helpers.MustExecuteTemplate(serversTemplate, serverConfig),
6671
}
6772

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

94+
// getIPFamily returns whether the server should be configured for IPv4, IPv6, or both.
95+
func getIPFamily(baseHTTPConfig dataplane.BaseHTTPConfig) http.IPFamily {
96+
switch baseHTTPConfig.IPFamily {
97+
case dataplane.IPv4:
98+
return http.IPFamily{IPv4: true}
99+
case dataplane.IPv6:
100+
return http.IPFamily{IPv6: true}
101+
}
102+
103+
return http.IPFamily{IPv4: true, IPv6: true}
104+
}
105+
89106
func createAdditionFileResults(conf dataplane.Configuration) []executeResult {
90107
uniqueAdditions := make(map[string][]byte)
91108

internal/mode/static/nginx/config/servers_template.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,52 @@ package config
22

33
const serversTemplateText = `
44
js_preload_object matches from /etc/nginx/conf.d/matches.json;
5-
{{- range $s := . -}}
5+
{{- range $s := .Servers -}}
66
{{ if $s.IsDefaultSSL -}}
77
server {
8+
{{- if $.IPFamily.IPv4 }}
89
listen {{ $s.Port }} ssl default_server;
10+
{{- end }}
11+
{{- if $.IPFamily.IPv6 }}
12+
listen [::]:{{ $s.Port }} ssl default_server;
13+
{{- end }}
914
1015
ssl_reject_handshake on;
1116
}
1217
{{- else if $s.IsDefaultHTTP }}
1318
server {
19+
{{- if $.IPFamily.IPv4 }}
1420
listen {{ $s.Port }} default_server;
21+
{{- end }}
22+
{{- if $.IPFamily.IPv6 }}
23+
listen [::]:{{ $s.Port }} default_server;
24+
{{- end }}
1525
1626
default_type text/html;
1727
return 404;
1828
}
1929
{{- else }}
2030
server {
2131
{{- if $s.SSL }}
32+
{{- if $.IPFamily.IPv4 }}
2233
listen {{ $s.Port }} ssl;
34+
{{- end }}
35+
{{- if $.IPFamily.IPv6 }}
36+
listen [::]:{{ $s.Port }} ssl;
37+
{{- end }}
2338
ssl_certificate {{ $s.SSL.Certificate }};
2439
ssl_certificate_key {{ $s.SSL.CertificateKey }};
2540
2641
if ($ssl_server_name != $host) {
2742
return 421;
2843
}
2944
{{- else }}
45+
{{- if $.IPFamily.IPv4 }}
3046
listen {{ $s.Port }};
47+
{{- end }}
48+
{{- if $.IPFamily.IPv6 }}
49+
listen [::]:{{ $s.Port }};
50+
{{- end }}
3151
{{- end }}
3252
3353
server_name {{ $s.ServerName }};

internal/mode/static/nginx/config/servers_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,117 @@ func TestExecuteServers(t *testing.T) {
137137
}
138138
}
139139

140+
func TestExecuteServersForIPFamily(t *testing.T) {
141+
httpServers := []dataplane.VirtualServer{
142+
{
143+
IsDefault: true,
144+
Port: 8080,
145+
},
146+
{
147+
Hostname: "example.com",
148+
Port: 8080,
149+
},
150+
}
151+
sslServers := []dataplane.VirtualServer{
152+
{
153+
IsDefault: true,
154+
Port: 8443,
155+
},
156+
{
157+
Hostname: "example.com",
158+
SSL: &dataplane.SSL{
159+
KeyPairID: "test-keypair",
160+
},
161+
Port: 8443,
162+
},
163+
}
164+
tests := []struct {
165+
msg string
166+
expectedHTTPConfig map[string]int
167+
config dataplane.Configuration
168+
}{
169+
{
170+
msg: "http and ssl servers with IPv4 IP family",
171+
config: dataplane.Configuration{
172+
HTTPServers: httpServers,
173+
SSLServers: sslServers,
174+
BaseHTTPConfig: dataplane.BaseHTTPConfig{
175+
IPFamily: dataplane.IPv4,
176+
},
177+
},
178+
expectedHTTPConfig: map[string]int{
179+
"listen 8080 default_server;": 1,
180+
"listen 8080;": 1,
181+
"listen 8443 ssl default_server;": 1,
182+
"listen 8443 ssl;": 1,
183+
"server_name example.com;": 2,
184+
"ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1,
185+
"ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1,
186+
"ssl_reject_handshake on;": 1,
187+
},
188+
},
189+
{
190+
msg: "http and ssl servers with IPv6 IP family",
191+
config: dataplane.Configuration{
192+
HTTPServers: httpServers,
193+
SSLServers: sslServers,
194+
BaseHTTPConfig: dataplane.BaseHTTPConfig{
195+
IPFamily: dataplane.IPv6,
196+
},
197+
},
198+
expectedHTTPConfig: map[string]int{
199+
"listen [::]:8080 default_server;": 1,
200+
"listen [::]:8080;": 1,
201+
"listen [::]:8443 ssl default_server;": 1,
202+
"listen [::]:8443 ssl;": 1,
203+
"server_name example.com;": 2,
204+
"ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1,
205+
"ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1,
206+
"ssl_reject_handshake on;": 1,
207+
},
208+
},
209+
{
210+
msg: "http and ssl servers with Dual IP family",
211+
config: dataplane.Configuration{
212+
HTTPServers: httpServers,
213+
SSLServers: sslServers,
214+
BaseHTTPConfig: dataplane.BaseHTTPConfig{
215+
IPFamily: dataplane.Dual,
216+
},
217+
},
218+
expectedHTTPConfig: map[string]int{
219+
"listen 8080 default_server;": 1,
220+
"listen 8080;": 1,
221+
"listen 8443 ssl default_server;": 1,
222+
"listen 8443 ssl;": 1,
223+
"server_name example.com;": 2,
224+
"ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1,
225+
"ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1,
226+
"ssl_reject_handshake on;": 1,
227+
"listen [::]:8080 default_server;": 1,
228+
"listen [::]:8080;": 1,
229+
"listen [::]:8443 ssl default_server;": 1,
230+
"listen [::]:8443 ssl;": 1,
231+
},
232+
},
233+
}
234+
235+
for _, test := range tests {
236+
t.Run(test.msg, func(t *testing.T) {
237+
g := NewWithT(t)
238+
results := executeServers(test.config)
239+
g.Expect(results).To(HaveLen(2))
240+
serverConf := string(results[0].data)
241+
httpMatchConf := string(results[1].data)
242+
g.Expect(httpMatchConf).To(Equal("{}"))
243+
244+
for expSubStr, expCount := range test.expectedHTTPConfig {
245+
g.Expect(strings.Count(serverConf, expSubStr)).To(Equal(expCount))
246+
}
247+
})
248+
}
249+
}
250+
140251
func TestExecuteForDefaultServers(t *testing.T) {
141252
testcases := []struct {
142253
msg string
@@ -2515,3 +2626,35 @@ func TestAdditionFilename(t *testing.T) {
25152626
name := createAdditionFileName(dataplane.Addition{Identifier: "my-addition"})
25162627
g.Expect(name).To(Equal(includesFolder + "/" + "my-addition.conf"))
25172628
}
2629+
2630+
func TestGetIPFamily(t *testing.T) {
2631+
test := []struct {
2632+
msg string
2633+
baseHTTPConfig dataplane.BaseHTTPConfig
2634+
expected http.IPFamily
2635+
}{
2636+
{
2637+
msg: "ipv4",
2638+
baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.IPv4},
2639+
expected: http.IPFamily{IPv4: true, IPv6: false},
2640+
},
2641+
{
2642+
msg: "ipv6",
2643+
baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.IPv6},
2644+
expected: http.IPFamily{IPv4: false, IPv6: true},
2645+
},
2646+
{
2647+
msg: "dual",
2648+
baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.Dual},
2649+
expected: http.IPFamily{IPv4: true, IPv6: true},
2650+
},
2651+
}
2652+
2653+
for _, tc := range test {
2654+
t.Run(tc.msg, func(t *testing.T) {
2655+
g := NewWithT(t)
2656+
result := getIPFamily(tc.baseHTTPConfig)
2657+
g.Expect(result).To(Equal(tc.expected))
2658+
})
2659+
}
2660+
}

0 commit comments

Comments
 (0)