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

Add support for TLS Passthrough using TLSRoutes #2356

Merged
merged 7 commits into from
Aug 9, 2024
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
@@ -75,6 +75,7 @@ jobs:
run: |
ngf_prefix=ghcr.io/nginxinc/nginx-gateway-fabric
ngf_tag=${{ steps.ngf-meta.outputs.version }}
if [ ${{ inputs.enable-experimental }} == "true" ]; then export ENABLE_EXPERIMENTAL=true; fi
make generate-static-deployment PLUS_ENABLED=${{ inputs.image == 'plus' && 'true' || 'false' }} PREFIX=${ngf_prefix} TAG=${ngf_tag}
working-directory: ./tests

@@ -146,6 +147,7 @@ jobs:

- name: Run conformance tests
run: |
if [ ${{ inputs.enable-experimental }} == "true" ]; then export ENABLE_EXPERIMENTAL=true; fi
make run-conformance-tests CONFORMANCE_TAG=${{ github.sha }} NGF_VERSION=${{ github.ref_name }} CLUSTER_NAME=${{ github.run_id }}
core_result=$(cat conformance-profile.yaml | yq '.profiles[0].core.result')
extended_result=$(cat conformance-profile.yaml | yq '.profiles[0].extended.result')
2 changes: 2 additions & 0 deletions charts/nginx-gateway-fabric/templates/clusterrole.yaml
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ rules:
- grpcroutes
{{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }}
- backendtlspolicies
- tlsroutes
{{- end }}
verbs:
- list
@@ -85,6 +86,7 @@ rules:
- grpcroutes/status
{{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }}
- backendtlspolicies/status
- tlsroutes/status
{{- end }}
verbs:
- update
6 changes: 6 additions & 0 deletions charts/nginx-gateway-fabric/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -129,6 +129,8 @@ spec:
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
- name: nginx-stream-conf
kate-osborn marked this conversation as resolved.
Show resolved Hide resolved
mountPath: /etc/nginx/stream-conf.d
- name: module-includes
mountPath: /etc/nginx/module-includes
- name: nginx-secrets
@@ -166,6 +168,8 @@ spec:
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
- name: nginx-stream-conf
mountPath: /etc/nginx/stream-conf.d
- name: module-includes
mountPath: /etc/nginx/module-includes
- name: nginx-secrets
@@ -200,6 +204,8 @@ spec:
volumes:
- name: nginx-conf
emptyDir: {}
- name: nginx-stream-conf
emptyDir: {}
- name: module-includes
emptyDir: {}
- name: nginx-secrets
6 changes: 6 additions & 0 deletions config/tests/static-deployment.yaml
Original file line number Diff line number Diff line change
@@ -72,6 +72,8 @@ spec:
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
- name: nginx-stream-conf
mountPath: /etc/nginx/stream-conf.d
- name: module-includes
mountPath: /etc/nginx/module-includes
- name: nginx-secrets
@@ -102,6 +104,8 @@ spec:
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
- name: nginx-stream-conf
mountPath: /etc/nginx/stream-conf.d
- name: module-includes
mountPath: /etc/nginx/module-includes
- name: nginx-secrets
@@ -121,6 +125,8 @@ spec:
volumes:
- name: nginx-conf
emptyDir: {}
- name: nginx-stream-conf
emptyDir: {}
- name: module-includes
emptyDir: {}
- name: nginx-secrets
6 changes: 6 additions & 0 deletions deploy/aws-nlb/deploy.yaml
Original file line number Diff line number Diff line change
@@ -246,6 +246,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -276,6 +278,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -295,6 +299,8 @@ spec:
volumes:
- emptyDir: {}
name: nginx-conf
- emptyDir: {}
name: nginx-stream-conf
- emptyDir: {}
name: module-includes
- emptyDir: {}
6 changes: 6 additions & 0 deletions deploy/azure/deploy.yaml
Original file line number Diff line number Diff line change
@@ -243,6 +243,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -273,6 +275,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -294,6 +298,8 @@ spec:
volumes:
- emptyDir: {}
name: nginx-conf
- emptyDir: {}
name: nginx-stream-conf
- emptyDir: {}
name: module-includes
- emptyDir: {}
6 changes: 6 additions & 0 deletions deploy/default/deploy.yaml
Original file line number Diff line number Diff line change
@@ -243,6 +243,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -273,6 +275,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -292,6 +296,8 @@ spec:
volumes:
- emptyDir: {}
name: nginx-conf
- emptyDir: {}
name: nginx-stream-conf
- emptyDir: {}
name: module-includes
- emptyDir: {}
8 changes: 8 additions & 0 deletions deploy/experimental-nginx-plus/deploy.yaml
Original file line number Diff line number Diff line change
@@ -82,6 +82,7 @@ rules:
- referencegrants
- grpcroutes
- backendtlspolicies
- tlsroutes
verbs:
- list
- watch
@@ -93,6 +94,7 @@ rules:
- gatewayclasses/status
- grpcroutes/status
- backendtlspolicies/status
- tlsroutes/status
verbs:
- update
- apiGroups:
@@ -256,6 +258,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -286,6 +290,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -305,6 +311,8 @@ spec:
volumes:
- emptyDir: {}
name: nginx-conf
- emptyDir: {}
name: nginx-stream-conf
- emptyDir: {}
name: module-includes
- emptyDir: {}
8 changes: 8 additions & 0 deletions deploy/experimental/deploy.yaml
Original file line number Diff line number Diff line change
@@ -74,6 +74,7 @@ rules:
- referencegrants
- grpcroutes
- backendtlspolicies
- tlsroutes
verbs:
- list
- watch
@@ -85,6 +86,7 @@ rules:
- gatewayclasses/status
- grpcroutes/status
- backendtlspolicies/status
- tlsroutes/status
verbs:
- update
- apiGroups:
@@ -247,6 +249,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -277,6 +281,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -296,6 +302,8 @@ spec:
volumes:
- emptyDir: {}
name: nginx-conf
- emptyDir: {}
name: nginx-stream-conf
- emptyDir: {}
name: module-includes
- emptyDir: {}
6 changes: 6 additions & 0 deletions deploy/nginx-plus/deploy.yaml
Original file line number Diff line number Diff line change
@@ -254,6 +254,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -284,6 +286,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -303,6 +307,8 @@ spec:
volumes:
- emptyDir: {}
name: nginx-conf
- emptyDir: {}
name: nginx-stream-conf
- emptyDir: {}
name: module-includes
- emptyDir: {}
6 changes: 6 additions & 0 deletions deploy/nodeport/deploy.yaml
Original file line number Diff line number Diff line change
@@ -243,6 +243,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -273,6 +275,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -292,6 +296,8 @@ spec:
volumes:
- emptyDir: {}
name: nginx-conf
- emptyDir: {}
name: nginx-stream-conf
- emptyDir: {}
name: module-includes
- emptyDir: {}
6 changes: 6 additions & 0 deletions deploy/openshift/deploy.yaml
Original file line number Diff line number Diff line change
@@ -251,6 +251,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -281,6 +283,8 @@ spec:
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-conf
- mountPath: /etc/nginx/stream-conf.d
name: nginx-stream-conf
- mountPath: /etc/nginx/module-includes
name: module-includes
- mountPath: /etc/nginx/secrets
@@ -300,6 +304,8 @@ spec:
volumes:
- emptyDir: {}
name: nginx-conf
- emptyDir: {}
name: nginx-stream-conf
- emptyDir: {}
name: module-includes
- emptyDir: {}
1 change: 1 addition & 0 deletions internal/framework/gatewayclass/validate.go
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ var gatewayCRDs = map[string]apiVersion{
"referencegrants.gateway.networking.k8s.io": {},
"backendtlspolicies.gateway.networking.k8s.io": {},
"grpcroutes.gateway.networking.k8s.io": {},
"tlsroutes.gateway.networking.k8s.io": {},
}

type apiVersion struct {
2 changes: 2 additions & 0 deletions internal/framework/kinds/kinds.go
Original file line number Diff line number Diff line change
@@ -19,6 +19,8 @@ const (
HTTPRoute = "HTTPRoute"
// GRPCRoute is the GRPCRoute kind.
GRPCRoute = "GRPCRoute"
// TLSRoute is the TLSRoute kind.
TLSRoute = "TLSRoute"
)

// NGINX Gateway Fabric kinds.
1 change: 1 addition & 0 deletions internal/mode/static/handler.go
Original file line number Diff line number Diff line change
@@ -246,6 +246,7 @@ func (h *eventHandlerImpl) updateStatuses(ctx context.Context, logger logr.Logge
gcReqs = status.PrepareGatewayClassRequests(graph.GatewayClass, graph.IgnoredGatewayClasses, transitionTime)
}
routeReqs := status.PrepareRouteRequests(
graph.L4Routes,
graph.Routes,
transitionTime,
h.latestReloadResult,
9 changes: 9 additions & 0 deletions internal/mode/static/manager.go
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
k8spredicate "sigs.k8s.io/controller-runtime/pkg/predicate"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
gatewayv1alpha3 "sigs.k8s.io/gateway-api/apis/v1alpha3"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

@@ -73,6 +74,7 @@
utilruntime.Must(gatewayv1beta1.Install(scheme))
utilruntime.Must(gatewayv1.Install(scheme))
utilruntime.Must(gatewayv1alpha3.Install(scheme))
utilruntime.Must(gatewayv1alpha2.Install(scheme))
utilruntime.Must(apiv1.AddToScheme(scheme))
utilruntime.Must(discoveryV1.AddToScheme(scheme))
utilruntime.Must(ngfAPI.AddToScheme(scheme))
@@ -489,6 +491,12 @@
// https://github.com/nginxinc/nginx-gateway-fabric/issues/1545
objectType: &apiv1.ConfigMap{},
},
{
objectType: &gatewayv1alpha2.TLSRoute{},
options: []controller.Option{
controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}),
},
},

Check warning on line 499 in internal/mode/static/manager.go

Codecov / codecov/patch

internal/mode/static/manager.go#L495-L499

Added lines #L495 - L499 were not covered by tests
}
controllerRegCfgs = append(controllerRegCfgs, gwExpFeatures...)
}
@@ -663,6 +671,7 @@
objectLists,
&gatewayv1alpha3.BackendTLSPolicyList{},
&apiv1.ConfigMapList{},
&gatewayv1alpha2.TLSRouteList{},
)
}

2 changes: 2 additions & 0 deletions internal/mode/static/manager_test.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
gatewayv1alpha3 "sigs.k8s.io/gateway-api/apis/v1alpha3"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

@@ -105,6 +106,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
&ngfAPI.NginxProxyList{},
partialObjectMetadataList,
&gatewayv1alpha3.BackendTLSPolicyList{},
&gatewayv1alpha2.TLSRouteList{},
&gatewayv1.GRPCRouteList{},
&ngfAPI.ClientSettingsPolicyList{},
&ngfAPI.ObservabilityPolicyList{},
14 changes: 14 additions & 0 deletions internal/mode/static/nginx/conf/nginx-plus.conf
Original file line number Diff line number Diff line change
@@ -54,6 +54,20 @@ http {
}
}

stream {
kate-osborn marked this conversation as resolved.
Show resolved Hide resolved
variables_hash_bucket_size 512;
variables_hash_max_size 1024;

map_hash_max_size 2048;
map_hash_bucket_size 256;

log_format stream-main '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time "$ssl_preread_server_name"';
access_log /dev/stdout stream-main;
include /etc/nginx/stream-conf.d/*.conf;
}

mgmt {
usage_report interval=0s;
}
14 changes: 14 additions & 0 deletions internal/mode/static/nginx/conf/nginx.conf
Original file line number Diff line number Diff line change
@@ -38,3 +38,17 @@ http {
}
}
}

stream {
variables_hash_bucket_size 512;
variables_hash_max_size 1024;

map_hash_max_size 2048;
map_hash_bucket_size 256;

log_format stream-main '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time "$ssl_preread_server_name"';
access_log /dev/stdout stream-main;
include /etc/nginx/stream-conf.d/*.conf;
}
21 changes: 21 additions & 0 deletions internal/mode/static/nginx/config/base_http_config_template.go
Original file line number Diff line number Diff line change
@@ -2,4 +2,25 @@ package config

const baseHTTPTemplateText = `
{{- if .HTTP2 }}http2 on;{{ end }}
# Set $gw_api_compliant_host variable to the value of $http_host unless $http_host is empty, then set it to the value
# of $host. We prefer $http_host because it contains the original value of the host header, which is required by the
# Gateway API. However, in an HTTP/1.0 request, it's possible that $http_host can be empty. In this case, we will use
# the value of $host. See http://nginx.org/en/docs/http/ngx_http_core_module.html#var_host.
map $http_host $gw_api_compliant_host {
'' $host;
default $http_host;
}
# Set $connection_header variable to upgrade when the $http_upgrade header is set, otherwise, set it to close. This
# allows support for websocket connections. See https://nginx.org/en/docs/http/websocket.html.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
## Returns just the path from the original request URI.
map $request_uri $request_uri_path {
"~^(?P<path>[^?]*)(\?.*)?$" $path;
}
`
3 changes: 3 additions & 0 deletions internal/mode/static/nginx/config/base_http_config_test.go
Original file line number Diff line number Diff line change
@@ -47,5 +47,8 @@ func TestExecuteBaseHttp(t *testing.T) {
res := executeBaseHTTPConfig(test.conf)
g.Expect(res).To(HaveLen(1))
g.Expect(test.expCount).To(Equal(strings.Count(string(res[0].data), expSubStr)))
g.Expect(strings.Count(string(res[0].data), "map $http_host $gw_api_compliant_host {")).To(Equal(1))
g.Expect(strings.Count(string(res[0].data), "map $http_upgrade $connection_upgrade {")).To(Equal(1))
g.Expect(strings.Count(string(res[0].data), "map $request_uri $request_uri_path {")).To(Equal(1))
}
}
11 changes: 10 additions & 1 deletion internal/mode/static/nginx/config/generator.go
Original file line number Diff line number Diff line change
@@ -20,6 +20,9 @@ const (
// httpFolder is the folder where NGINX HTTP configuration files are stored.
httpFolder = configFolder + "/conf.d"

// streamFolder is the folder where NGINX Stream configuration files are stored.
streamFolder = configFolder + "/stream-conf.d"

// modulesIncludesFolder is the folder where the included "load_module" file is stored.
modulesIncludesFolder = configFolder + "/module-includes"

@@ -32,6 +35,9 @@ const (
// httpConfigFile is the path to the configuration file with HTTP configuration.
httpConfigFile = httpFolder + "/http.conf"

// streamConfigFile is the path to the configuration file with Stream configuration.
streamConfigFile = streamFolder + "/stream.conf"

// configVersionFile is the path to the config version configuration file.
configVersionFile = httpFolder + "/config-version.conf"

@@ -43,7 +49,7 @@ const (
)

// ConfigFolders is a list of folders where NGINX configuration files are stored.
var ConfigFolders = []string{httpFolder, secretsFolder, includesFolder, modulesIncludesFolder}
var ConfigFolders = []string{httpFolder, secretsFolder, includesFolder, modulesIncludesFolder, streamFolder}

// Generator generates NGINX configuration files.
// This interface is used for testing purposes only.
@@ -168,6 +174,9 @@ func (g GeneratorImpl) getExecuteFuncs(generator policies.Generator) []executeFu
executeSplitClients,
executeMaps,
executeTelemetry,
executeStreamServers,
g.executeStreamUpstreams,
executeStreamMaps,
}
}

31 changes: 29 additions & 2 deletions internal/mode/static/nginx/config/generator_test.go
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import (
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/file"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver"
)

func TestGenerate(t *testing.T) {
@@ -47,12 +48,30 @@ func TestGenerate(t *testing.T) {
Port: 443,
},
},
TLSPassthroughServers: []dataplane.Layer4VirtualServer{
{
Hostname: "app.example.com",
Port: 443,
UpstreamName: "stream_up",
},
},
Upstreams: []dataplane.Upstream{
{
Name: "up",
Endpoints: nil,
},
},
StreamUpstreams: []dataplane.Upstream{
{
Name: "stream_up",
Endpoints: []resolver.Endpoint{
{
Address: "1.1.1.1",
Port: 80,
},
},
},
},
BackendGroups: []dataplane.BackendGroup{bg},
SSLKeyPairs: map[dataplane.SSLKeyPairID]dataplane.SSLKeyPair{
"test-keypair": {
@@ -81,7 +100,7 @@ func TestGenerate(t *testing.T) {

files := generator.Generate(conf)

g.Expect(files).To(HaveLen(6))
g.Expect(files).To(HaveLen(7))
arrange := func(i, j int) bool {
return files[i].Path < files[j].Path
}
@@ -98,7 +117,7 @@ func TestGenerate(t *testing.T) {
// Note: this only verifies that Generate() returns a byte array with upstream, server, and split_client blocks.
// It does not test the correctness of those blocks. That functionality is covered by other tests in this package.
g.Expect(httpCfg).To(ContainSubstring("listen 80"))
g.Expect(httpCfg).To(ContainSubstring("listen 443"))
g.Expect(httpCfg).To(ContainSubstring("listen unix:/var/run/nginx/https443.sock"))
g.Expect(httpCfg).To(ContainSubstring("upstream"))
g.Expect(httpCfg).To(ContainSubstring("split_clients"))

@@ -127,4 +146,12 @@ func TestGenerate(t *testing.T) {
Path: "/etc/nginx/secrets/test-keypair.pem",
Content: []byte("test-cert\ntest-key"),
}))

g.Expect(files[6].Path).To(Equal("/etc/nginx/stream-conf.d/stream.conf"))
g.Expect(files[6].Type).To(Equal(file.TypeRegular))
streamCfg := string(files[6].Content)
g.Expect(streamCfg).To(ContainSubstring("listen unix:/var/run/nginx/app.example.com-443.sock"))
g.Expect(streamCfg).To(ContainSubstring("listen 443"))
g.Expect(streamCfg).To(ContainSubstring("app.example.com unix:/var/run/nginx/app.example.com-443.sock"))
g.Expect(streamCfg).To(ContainSubstring("example.com unix:/var/run/nginx/https443.sock"))
}
26 changes: 5 additions & 21 deletions internal/mode/static/nginx/config/http/config.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
package http

import "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/shared"

const InternalRoutePathPrefix = "/_ngf-internal"

// Server holds all configuration for an HTTP server.
type Server struct {
SSL *SSL
ServerName string
Listen string
Locations []Location
Includes []Include
Port int32
IsDefaultHTTP bool
IsDefaultSSL bool
GRPC bool
}

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

type LocationType string
@@ -104,19 +101,6 @@ type SplitClientDistribution struct {
Value string
}

// Map defines an NGINX map.
type Map struct {
Source string
Variable string
Parameters []MapParameter
}

// MapParameter defines a Value and Result pair in a Map.
type MapParameter struct {
Value string
Result string
}

// ProxySSLVerify holds the proxied HTTPS server verification configuration.
type ProxySSLVerify struct {
TrustedCertificate string
@@ -126,7 +110,7 @@ type ProxySSLVerify struct {
// ServerConfig holds configuration for an HTTP server and IP family to be used by NGINX.
type ServerConfig struct {
Servers []Server
IPFamily IPFamily
IPFamily shared.IPFamily
}

// Include defines a file that's included via the include directive.
117 changes: 111 additions & 6 deletions internal/mode/static/nginx/config/maps.go
Original file line number Diff line number Diff line change
@@ -5,12 +5,25 @@ import (
gotemplate "text/template"

"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/shared"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
)

var mapsTemplate = gotemplate.Must(gotemplate.New("maps").Parse(mapsTemplateText))

const (
// emptyStringSocket is used when the stream server has an invalid upstream. In this case, we pass the connection
// to the empty socket so that NGINX will close the connection with an error in the error log --
// no host in pass "" -- and set $status variable to 500 (logged by stream access log),
// which will indicate the problem to the user.
// https://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_status
emptyStringSocket = `""`

// connectionClosedStreamServerSocket is used when we want to listen on a port but have no service configured,
// so we pass to this server that just returns an empty string to tell users that we are listening.
connectionClosedStreamServerSocket = "unix:/var/run/nginx/connection-closed-server.sock"
)

func executeMaps(conf dataplane.Configuration) []executeResult {
maps := buildAddHeaderMaps(append(conf.HTTPServers, conf.SSLServers...))
result := executeResult{
@@ -21,7 +34,99 @@ func executeMaps(conf dataplane.Configuration) []executeResult {
return []executeResult{result}
}

func buildAddHeaderMaps(servers []dataplane.VirtualServer) []http.Map {
func executeStreamMaps(conf dataplane.Configuration) []executeResult {
maps := createStreamMaps(conf)

result := executeResult{
dest: streamConfigFile,
data: helpers.MustExecuteTemplate(mapsTemplate, maps),
}

return []executeResult{result}
}

func createStreamMaps(conf dataplane.Configuration) []shared.Map {
if len(conf.TLSPassthroughServers) == 0 {
return nil
}
portsToMap := make(map[int32]shared.Map)
portHasDefault := make(map[int32]struct{})
upstreams := make(map[string]dataplane.Upstream)

for _, u := range conf.StreamUpstreams {
upstreams[u.Name] = u
}

for _, server := range conf.TLSPassthroughServers {
streamMap, portInUse := portsToMap[server.Port]

socket := emptyStringSocket

if u, ok := upstreams[server.UpstreamName]; ok && server.UpstreamName != "" && len(u.Endpoints) > 0 {
socket = getSocketNameTLS(server.Port, server.Hostname)
}

if server.IsDefault {
socket = connectionClosedStreamServerSocket
}

if !portInUse {
streamMap = shared.Map{
Source: "$ssl_preread_server_name",
Variable: getTLSPassthroughVarName(server.Port),
Parameters: make([]shared.MapParameter, 0),
UseHostnames: true,
}
portsToMap[server.Port] = streamMap
}

// If the hostname is empty, we don't want to add an entry to the map. This case occurs when
// the gateway listener hostname is not specified
if server.Hostname != "" {
mapParam := shared.MapParameter{
Value: server.Hostname,
Result: socket,
}
streamMap.Parameters = append(streamMap.Parameters, mapParam)
portsToMap[server.Port] = streamMap
}
}

for _, server := range conf.SSLServers {
streamMap, portInUse := portsToMap[server.Port]

hostname := server.Hostname

if server.IsDefault {
hostname = "default"
portHasDefault[server.Port] = struct{}{}
}

if portInUse {
streamMap.Parameters = append(streamMap.Parameters, shared.MapParameter{
Value: hostname,
Result: getSocketNameHTTPS(server.Port),
})
portsToMap[server.Port] = streamMap
}
}

maps := make([]shared.Map, 0, len(portsToMap))

for p, m := range portsToMap {
if _, ok := portHasDefault[p]; !ok {
m.Parameters = append(m.Parameters, shared.MapParameter{
Value: "default",
Result: connectionClosedStreamServerSocket,
})
}
maps = append(maps, m)
}

return maps
}

func buildAddHeaderMaps(servers []dataplane.VirtualServer) []shared.Map {
addHeaderNames := make(map[string]struct{})

for _, s := range servers {
@@ -39,7 +144,7 @@ func buildAddHeaderMaps(servers []dataplane.VirtualServer) []http.Map {
}
}

maps := make([]http.Map, 0, len(addHeaderNames))
maps := make([]shared.Map, 0, len(addHeaderNames))
for m := range addHeaderNames {
maps = append(maps, createAddHeadersMap(m))
}
@@ -52,11 +157,11 @@ const (
anyStringFmt = `~.*`
)

func createAddHeadersMap(name string) http.Map {
func createAddHeadersMap(name string) shared.Map {
underscoreName := convertStringToSafeVariableName(name)
httpVarSource := "${http_" + underscoreName + "}"
mapVarName := generateAddHeaderMapVariableName(name)
params := []http.MapParameter{
params := []shared.MapParameter{
{
Value: "default",
Result: "''",
@@ -66,7 +171,7 @@ func createAddHeadersMap(name string) http.Map {
Result: httpVarSource + ",",
},
}
return http.Map{
return shared.Map{
Source: httpVarSource,
Variable: "$" + mapVarName,
Parameters: params,
30 changes: 6 additions & 24 deletions internal/mode/static/nginx/config/maps_template.go
Original file line number Diff line number Diff line change
@@ -3,30 +3,12 @@ package config
const mapsTemplateText = `
{{ range $m := . }}
map {{ $m.Source }} {{ $m.Variable }} {
{{ range $p := $m.Parameters }}
{{ $p.Value }} {{ $p.Result }};
{{ end }}
{{- if $m.UseHostnames }}
hostnames;
{{ end }}
{{ range $p := $m.Parameters }}
{{ $p.Value }} {{ $p.Result }};
{{ end }}
}
{{- end }}
# Set $gw_api_compliant_host variable to the value of $http_host unless $http_host is empty, then set it to the value
# of $host. We prefer $http_host because it contains the original value of the host header, which is required by the
# Gateway API. However, in an HTTP/1.0 request, it's possible that $http_host can be empty. In this case, we will use
# the value of $host. See http://nginx.org/en/docs/http/ngx_http_core_module.html#var_host.
map $http_host $gw_api_compliant_host {
'' $host;
default $http_host;
}
# Set $connection_header variable to upgrade when the $http_upgrade header is set, otherwise, set it to close. This
# allows support for websocket connections. See https://nginx.org/en/docs/http/websocket.html.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
## Returns just the path from the original request URI.
map $request_uri $request_uri_path {
"~^(?P<path>[^?]*)(\?.*)?$" $path;
}
`
204 changes: 197 additions & 7 deletions internal/mode/static/nginx/config/maps_test.go
Original file line number Diff line number Diff line change
@@ -6,8 +6,9 @@ import (

. "github.com/onsi/gomega"

"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/shared"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver"
)

func TestExecuteMaps(t *testing.T) {
@@ -84,9 +85,6 @@ func TestExecuteMaps(t *testing.T) {
"map ${http_my_second_add_header} $my_second_add_header_header_var {": 1,
"~.* ${http_my_second_add_header},;": 1,
"map ${http_my_set_header} $my_set_header_header_var {": 0,
"map $http_host $gw_api_compliant_host {": 1,
"map $http_upgrade $connection_upgrade {": 1,
"map $request_uri $request_uri_path {": 1,
}

mapResult := executeMaps(conf)
@@ -162,11 +160,11 @@ func TestBuildAddHeaderMaps(t *testing.T) {
IsDefault: true,
},
}
expectedMap := []http.Map{
expectedMap := []shared.Map{
{
Source: "${http_my_add_header}",
Variable: "$my_add_header_header_var",
Parameters: []http.MapParameter{
Parameters: []shared.MapParameter{
{Value: "default", Result: "''"},
{
Value: "~.*",
@@ -177,7 +175,7 @@ func TestBuildAddHeaderMaps(t *testing.T) {
{
Source: "${http_my_second_add_header}",
Variable: "$my_second_add_header_header_var",
Parameters: []http.MapParameter{
Parameters: []shared.MapParameter{
{Value: "default", Result: "''"},
{
Value: "~.*",
@@ -190,3 +188,195 @@ func TestBuildAddHeaderMaps(t *testing.T) {

g.Expect(maps).To(ConsistOf(expectedMap))
}

func TestExecuteStreamMaps(t *testing.T) {
g := NewWithT(t)
conf := dataplane.Configuration{
TLSPassthroughServers: []dataplane.Layer4VirtualServer{
{
Hostname: "example.com",
Port: 8081,
UpstreamName: "backend1",
},
{
Hostname: "example.com",
Port: 8080,
UpstreamName: "backend1",
},
{
Hostname: "cafe.example.com",
Port: 8080,
UpstreamName: "backend2",
},
},
SSLServers: []dataplane.VirtualServer{
{
Hostname: "app.example.com",
Port: 8080,
},
},
StreamUpstreams: []dataplane.Upstream{
{
Name: "backend1",
Endpoints: []resolver.Endpoint{
{
Address: "1.1.1.1",
Port: 80,
},
},
},
{
Name: "backend2",
Endpoints: []resolver.Endpoint{
{
Address: "1.1.1.1",
Port: 80,
},
},
},
},
}

expSubStrings := map[string]int{
"example.com unix:/var/run/nginx/example.com-8081.sock;": 1,
"example.com unix:/var/run/nginx/example.com-8080.sock;": 1,
"cafe.example.com unix:/var/run/nginx/cafe.example.com-8080.sock;": 1,
"app.example.com unix:/var/run/nginx/https8080.sock;": 1,
"hostnames": 2,
"default": 2,
}

results := executeStreamMaps(conf)
g.Expect(results).To(HaveLen(1))
result := results[0]

g.Expect(result.dest).To(Equal(streamConfigFile))
for expSubStr, expCount := range expSubStrings {
g.Expect(strings.Count(string(result.data), expSubStr)).To(Equal(expCount))
}
}

func TestCreateStreamMaps(t *testing.T) {
g := NewWithT(t)
conf := dataplane.Configuration{
TLSPassthroughServers: []dataplane.Layer4VirtualServer{
{
Hostname: "example.com",
Port: 8081,
UpstreamName: "backend1",
},
{
Hostname: "example.com",
Port: 8080,
UpstreamName: "backend1",
},
{
Hostname: "cafe.example.com",
Port: 8080,
UpstreamName: "backend2",
},
{
Hostname: "dne.example.com",
Port: 8080,
UpstreamName: "backend-dne",
},
{
Port: 8082,
Hostname: "",
},
{
Hostname: "*.example.com",
Port: 8080,
IsDefault: true,
},
{
Hostname: "no-endpoints.example.com",
Port: 8080,
UpstreamName: "backend3",
},
},
SSLServers: []dataplane.VirtualServer{
{
Hostname: "app.example.com",
Port: 8080,
},
{
Port: 8080,
IsDefault: true,
},
},
StreamUpstreams: []dataplane.Upstream{
{
Name: "backend1",
Endpoints: []resolver.Endpoint{
{
Address: "1.1.1.1",
Port: 80,
},
},
},
{
Name: "backend2",
Endpoints: []resolver.Endpoint{
{
Address: "1.1.1.1",
Port: 80,
},
},
},
{
Name: "backend3",
Endpoints: nil,
},
},
}

maps := createStreamMaps(conf)

expectedMaps := []shared.Map{
{
Source: "$ssl_preread_server_name",
Variable: getTLSPassthroughVarName(8082),
Parameters: []shared.MapParameter{
{Value: "default", Result: connectionClosedStreamServerSocket},
},
UseHostnames: true,
},
{
Source: "$ssl_preread_server_name",
Variable: getTLSPassthroughVarName(8081),
Parameters: []shared.MapParameter{
{Value: "example.com", Result: getSocketNameTLS(8081, "example.com")},
{Value: "default", Result: connectionClosedStreamServerSocket},
},
UseHostnames: true,
},
{
Source: "$ssl_preread_server_name",
Variable: getTLSPassthroughVarName(8080),
Parameters: []shared.MapParameter{
{Value: "example.com", Result: getSocketNameTLS(8080, "example.com")},
{Value: "cafe.example.com", Result: getSocketNameTLS(8080, "cafe.example.com")},
{Value: "dne.example.com", Result: emptyStringSocket},
{Value: "*.example.com", Result: connectionClosedStreamServerSocket},
{Value: "no-endpoints.example.com", Result: emptyStringSocket},
{Value: "app.example.com", Result: getSocketNameHTTPS(8080)},
{Value: "default", Result: getSocketNameHTTPS(8080)},
},
UseHostnames: true,
},
}

g.Expect(maps).To(ConsistOf(expectedMaps))
}

func TestCreateStreamMapsWithEmpty(t *testing.T) {
g := NewWithT(t)
conf := dataplane.Configuration{
TLSPassthroughServers: nil,
}

maps := createStreamMaps(conf)

g.Expect(maps).To(BeNil())
}
36 changes: 26 additions & 10 deletions internal/mode/static/nginx/config/servers.go
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import (
"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/policies"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/shared"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
)

@@ -65,7 +66,7 @@ func newExecuteServersFunc(generator policies.Generator) executeFunc {
}

func executeServers(conf dataplane.Configuration, generator policies.Generator) []executeResult {
servers, httpMatchPairs := createServers(conf.HTTPServers, conf.SSLServers, generator)
servers, httpMatchPairs := createServers(conf.HTTPServers, conf.SSLServers, conf.TLSPassthroughServers, generator)

serverConfig := http.ServerConfig{
Servers: servers,
@@ -99,15 +100,15 @@ func executeServers(conf dataplane.Configuration, generator policies.Generator)
}

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

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

func createIncludeFileResults(servers []http.Server) []executeResult {
@@ -138,11 +139,18 @@ func createIncludeFileResults(servers []http.Server) []executeResult {
}

func createServers(
httpServers, sslServers []dataplane.VirtualServer,
httpServers,
sslServers []dataplane.VirtualServer,
tlsPassthroughServers []dataplane.Layer4VirtualServer,
generator policies.Generator,
) ([]http.Server, httpMatchPairs) {
servers := make([]http.Server, 0, len(httpServers)+len(sslServers))
finalMatchPairs := make(httpMatchPairs)
sharedTLSPorts := make(map[int32]struct{})

for _, passthroughServer := range tlsPassthroughServers {
sharedTLSPorts[passthroughServer.Port] = struct{}{}
}

for idx, s := range httpServers {
serverID := fmt.Sprintf("%d", idx)
@@ -153,7 +161,12 @@ func createServers(

for idx, s := range sslServers {
serverID := fmt.Sprintf("SSL_%d", idx)

sslServer, matchPairs := createSSLServer(s, serverID, generator)
if _, portInUse := sharedTLSPorts[s.Port]; portInUse {
sslServer.Listen = getSocketNameHTTPS(s.Port)
sslServer.IsSocket = true
}
servers = append(servers, sslServer)
maps.Copy(finalMatchPairs, matchPairs)
}
@@ -166,10 +179,11 @@ func createSSLServer(
serverID string,
generator policies.Generator,
) (http.Server, httpMatchPairs) {
listen := fmt.Sprint(virtualServer.Port)
if virtualServer.IsDefault {
return http.Server{
IsDefaultSSL: true,
Port: virtualServer.Port,
Listen: listen,
}, nil
}

@@ -182,8 +196,8 @@ func createSSLServer(
CertificateKey: generatePEMFileName(virtualServer.SSL.KeyPairID),
},
Locations: locs,
Port: virtualServer.Port,
GRPC: grpc,
Listen: listen,
}

server.Includes = createIncludesFromPolicyGenerateResult(
@@ -197,10 +211,12 @@ func createServer(
serverID string,
generator policies.Generator,
) (http.Server, httpMatchPairs) {
listen := fmt.Sprint(virtualServer.Port)

if virtualServer.IsDefault {
return http.Server{
IsDefaultHTTP: true,
Port: virtualServer.Port,
Listen: listen,
}, nil
}

@@ -209,7 +225,7 @@ func createServer(
server := http.Server{
ServerName: virtualServer.Hostname,
Locations: locs,
Port: virtualServer.Port,
Listen: listen,
GRPC: grpc,
}

24 changes: 12 additions & 12 deletions internal/mode/static/nginx/config/servers_template.go
Original file line number Diff line number Diff line change
@@ -5,22 +5,22 @@ js_preload_object matches from /etc/nginx/conf.d/matches.json;
{{- range $s := .Servers -}}
{{ if $s.IsDefaultSSL -}}
server {
{{- if $.IPFamily.IPv4 }}
listen {{ $s.Port }} ssl default_server;
{{- if or ($.IPFamily.IPv4) ($s.IsSocket) }}
listen {{ $s.Listen }} ssl default_server;
{{- end }}
{{- if $.IPFamily.IPv6 }}
listen [::]:{{ $s.Port }} ssl default_server;
{{- if and ($.IPFamily.IPv6) (not $s.IsSocket) }}
listen [::]:{{ $s.Listen }} ssl default_server;
{{- end }}
ssl_reject_handshake on;
}
{{- else if $s.IsDefaultHTTP }}
server {
{{- if $.IPFamily.IPv4 }}
listen {{ $s.Port }} default_server;
listen {{ $s.Listen }} default_server;
{{- end }}
{{- if $.IPFamily.IPv6 }}
listen [::]:{{ $s.Port }} default_server;
listen [::]:{{ $s.Listen }} default_server;
{{- end }}
default_type text/html;
@@ -29,11 +29,11 @@ server {
{{- else }}
server {
{{- if $s.SSL }}
{{- if $.IPFamily.IPv4 }}
listen {{ $s.Port }} ssl;
{{- if or ($.IPFamily.IPv4) ($s.IsSocket) }}
listen {{ $s.Listen }} ssl;
{{- end }}
{{- if $.IPFamily.IPv6 }}
listen [::]:{{ $s.Port }} ssl;
{{- if and ($.IPFamily.IPv6) (not $s.IsSocket) }}
listen [::]:{{ $s.Listen }} ssl;
{{- end }}
ssl_certificate {{ $s.SSL.Certificate }};
ssl_certificate_key {{ $s.SSL.CertificateKey }};
@@ -43,10 +43,10 @@ server {
}
{{- else }}
{{- if $.IPFamily.IPv4 }}
listen {{ $s.Port }};
listen {{ $s.Listen }};
{{- end }}
{{- if $.IPFamily.IPv6 }}
listen [::]:{{ $s.Port }};
listen [::]:{{ $s.Listen }};
{{- end }}
{{- end }}
83 changes: 61 additions & 22 deletions internal/mode/static/nginx/config/servers_test.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import (
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/policies"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/policies/policiesfakes"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/shared"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
)

@@ -165,6 +166,26 @@ func TestExecuteServersForIPFamily(t *testing.T) {
Port: 8443,
},
}
sslServers443 := []dataplane.VirtualServer{
{
IsDefault: true,
Port: 443,
},
{
Hostname: "example.com",
SSL: &dataplane.SSL{
KeyPairID: "test-keypair",
},
Port: 443,
},
}
passThroughServers := []dataplane.Layer4VirtualServer{
{
IsDefault: true,
Hostname: "*.example.com",
Port: 8443,
},
}
tests := []struct {
msg string
expectedHTTPConfig map[string]int
@@ -191,23 +212,26 @@ func TestExecuteServersForIPFamily(t *testing.T) {
},
},
{
msg: "http and ssl servers with IPv6 IP family",
msg: "http, ssl servers, and tls servers with IPv6 IP family",
config: dataplane.Configuration{
HTTPServers: httpServers,
SSLServers: sslServers,
SSLServers: append(sslServers, sslServers443...),
BaseHTTPConfig: dataplane.BaseHTTPConfig{
IPFamily: dataplane.IPv6,
},
TLSPassthroughServers: passThroughServers,
},
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 [::]:443 ssl default_server;": 1,
"listen [::]:443 ssl;": 1,
"listen unix:/var/run/nginx/https8443.sock ssl;": 1,
"listen unix:/var/run/nginx/https8443.sock ssl default_server;": 1,
"server_name example.com;": 3,
"ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 2,
"ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 2,
"ssl_reject_handshake on;": 2,
},
},
{
@@ -761,6 +785,14 @@ func TestCreateServers(t *testing.T) {
},
}

tlsPassthroughServers := []dataplane.Layer4VirtualServer{
{
Hostname: "app.example.com",
Port: 8443,
UpstreamName: "sup",
},
}

expMatchPairs := httpMatchPairs{
"1_0": {
{Method: "POST", RedirectPath: "/_ngf-internal-rule0-route0"},
@@ -1197,17 +1229,18 @@ func TestCreateServers(t *testing.T) {
expectedServers := []http.Server{
{
IsDefaultHTTP: true,
Port: 8080,
Listen: "8080",
},
{
ServerName: "cafe.example.com",
Locations: getExpectedLocations(false),
Port: 8080,
Listen: "8080",
GRPC: true,
},
{
IsDefaultSSL: true,
Port: 8443,
Listen: getSocketNameHTTPS(8443),
IsSocket: true,
},
{
ServerName: "cafe.example.com",
@@ -1216,7 +1249,8 @@ func TestCreateServers(t *testing.T) {
CertificateKey: expectedPEMPath,
},
Locations: getExpectedLocations(true),
Port: 8443,
Listen: getSocketNameHTTPS(8443),
IsSocket: true,
GRPC: true,
},
}
@@ -1237,7 +1271,7 @@ func TestCreateServers(t *testing.T) {
},
})

result, httpMatchPair := createServers(httpServers, sslServers, fakeGenerator)
result, httpMatchPair := createServers(httpServers, sslServers, tlsPassthroughServers, fakeGenerator)

g.Expect(httpMatchPair).To(Equal(allExpMatchPair))
g.Expect(helpers.Diff(expectedServers, result)).To(BeEmpty())
@@ -1441,18 +1475,23 @@ func TestCreateServersConflicts(t *testing.T) {
expectedServers := []http.Server{
{
IsDefaultHTTP: true,
Port: 8080,
Listen: "8080",
},
{
ServerName: "cafe.example.com",
Locations: test.expLocs,
Port: 8080,
Listen: "8080",
},
}

g := NewWithT(t)

result, _ := createServers(httpServers, []dataplane.VirtualServer{}, &policiesfakes.FakeGenerator{})
result, _ := createServers(
httpServers,
[]dataplane.VirtualServer{},
[]dataplane.Layer4VirtualServer{},
&policiesfakes.FakeGenerator{},
)
g.Expect(helpers.Diff(expectedServers, result)).To(BeEmpty())
})
}
@@ -2706,22 +2745,22 @@ func TestGetIPFamily(t *testing.T) {
test := []struct {
msg string
baseHTTPConfig dataplane.BaseHTTPConfig
expected http.IPFamily
expected shared.IPFamily
}{
{
msg: "ipv4",
baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.IPv4},
expected: http.IPFamily{IPv4: true, IPv6: false},
expected: shared.IPFamily{IPv4: true, IPv6: false},
},
{
msg: "ipv6",
baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.IPv6},
expected: http.IPFamily{IPv4: false, IPv6: true},
expected: shared.IPFamily{IPv4: false, IPv6: true},
},
{
msg: "dual",
baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.Dual},
expected: http.IPFamily{IPv4: true, IPv6: true},
expected: shared.IPFamily{IPv4: true, IPv6: true},
},
}

21 changes: 21 additions & 0 deletions internal/mode/static/nginx/config/shared/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package shared

// Map defines an NGINX map.
type Map struct {
Source string
Variable string
Parameters []MapParameter
UseHostnames bool
}

// MapParameter defines a Value and Result pair in a Map.
type MapParameter struct {
Value string
Result string
}

// IPFamily holds the IP family configuration to be used by NGINX.
type IPFamily struct {
IPv4 bool
IPv6 bool
}
17 changes: 17 additions & 0 deletions internal/mode/static/nginx/config/sockets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package config

import (
"fmt"
)

func getSocketNameTLS(port int32, hostname string) string {
return fmt.Sprintf("unix:/var/run/nginx/%s-%d.sock", hostname, port)
}

func getSocketNameHTTPS(port int32) string {
return fmt.Sprintf("unix:/var/run/nginx/https%d.sock", port)
}

func getTLSPassthroughVarName(port int32) string {
return fmt.Sprintf("$dest%d", port)
}
28 changes: 28 additions & 0 deletions internal/mode/static/nginx/config/sockets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package config

import (
"testing"

. "github.com/onsi/gomega"
)

func TestGetSocketNameTLS(t *testing.T) {
res := getSocketNameTLS(800, "*.cafe.example.com")

g := NewGomegaWithT(t)
g.Expect(res).To(Equal("unix:/var/run/nginx/*.cafe.example.com-800.sock"))
}

func TestGetSocketNameHTTPS(t *testing.T) {
res := getSocketNameHTTPS(800)

g := NewGomegaWithT(t)
g.Expect(res).To(Equal("unix:/var/run/nginx/https800.sock"))
}

func TestGetTLSPassthroughVarName(t *testing.T) {
res := getTLSPassthroughVarName(800)

g := NewGomegaWithT(t)
g.Expect(res).To(Equal("$dest800"))
}
30 changes: 30 additions & 0 deletions internal/mode/static/nginx/config/stream/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package stream

import "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/shared"

// Server holds all configuration for a stream server.
type Server struct {
Listen string
ProxyPass string
Pass string
SSLPreread bool
IsSocket bool
}

// Upstream holds all configuration for a stream upstream.
type Upstream struct {
Name string
ZoneSize string // format: 512k, 1m
Servers []UpstreamServer
}

// UpstreamServer holds all configuration for a stream upstream server.
type UpstreamServer struct {
Address string
}

// ServerConfig holds configuration for a stream server and IP family to be used by NGINX.
type ServerConfig struct {
Servers []Server
IPFamily shared.IPFamily
}
69 changes: 69 additions & 0 deletions internal/mode/static/nginx/config/stream_servers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package config

import (
"fmt"
gotemplate "text/template"

"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/stream"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
)

var streamServersTemplate = gotemplate.Must(gotemplate.New("streamServers").Parse(streamServersTemplateText))

func executeStreamServers(conf dataplane.Configuration) []executeResult {
streamServers := createStreamServers(conf)

streamServerConfig := stream.ServerConfig{
Servers: streamServers,
IPFamily: getIPFamily(conf.BaseHTTPConfig),
}

streamServerResult := executeResult{
dest: streamConfigFile,
data: helpers.MustExecuteTemplate(streamServersTemplate, streamServerConfig),
}

return []executeResult{
streamServerResult,
}
}

func createStreamServers(conf dataplane.Configuration) []stream.Server {
if len(conf.TLSPassthroughServers) == 0 {
return nil
}

streamServers := make([]stream.Server, 0, len(conf.TLSPassthroughServers)*2)
portSet := make(map[int32]struct{})
upstreams := make(map[string]dataplane.Upstream)

for _, u := range conf.StreamUpstreams {
upstreams[u.Name] = u
}

for _, server := range conf.TLSPassthroughServers {
if u, ok := upstreams[server.UpstreamName]; ok && server.UpstreamName != "" {
if server.Hostname != "" && len(u.Endpoints) > 0 {
streamServers = append(streamServers, stream.Server{
Listen: getSocketNameTLS(server.Port, server.Hostname),
ProxyPass: server.UpstreamName,
IsSocket: true,
})
}
}

if _, inPortSet := portSet[server.Port]; inPortSet {
continue
}

portSet[server.Port] = struct{}{}
streamServers = append(streamServers, stream.Server{
Listen: fmt.Sprint(server.Port),
Pass: getTLSPassthroughVarName(server.Port),
SSLPreread: true,
})
}

return streamServers
}
28 changes: 28 additions & 0 deletions internal/mode/static/nginx/config/stream_servers_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package config

const streamServersTemplateText = `
{{- range $s := .Servers }}
server {
{{- if or ($.IPFamily.IPv4) ($s.IsSocket) }}
listen {{ $s.Listen }};
{{- end }}
{{- if and ($.IPFamily.IPv6) (not $s.IsSocket) }}
listen [::]:{{ $s.Listen }};
{{- end }}
{{- if $s.ProxyPass }}
proxy_pass {{ $s.ProxyPass }};
{{- end }}
{{- if $s.Pass }}
pass {{ $s.Pass }};
{{- end }}
{{- if $s.SSLPreread }}
ssl_preread on;
{{- end }}
}
{{- end }}
server {
listen unix:/var/run/nginx/connection-closed-server.sock;
return "";
}
`
263 changes: 263 additions & 0 deletions internal/mode/static/nginx/config/stream_servers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
package config

import (
"fmt"
"strings"
"testing"

. "github.com/onsi/gomega"

"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/stream"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver"
)

func TestExecuteStreamServers(t *testing.T) {
conf := dataplane.Configuration{
TLSPassthroughServers: []dataplane.Layer4VirtualServer{
{
Hostname: "example.com",
Port: 8081,
UpstreamName: "backend1",
},
{
Hostname: "example.com",
Port: 8080,
UpstreamName: "backend1",
},
{
Hostname: "cafe.example.com",
Port: 8080,
UpstreamName: "backend2",
},
},
StreamUpstreams: []dataplane.Upstream{
{
Name: "backend1",
Endpoints: []resolver.Endpoint{
{
Address: "1.1.1.1",
Port: 80,
},
},
},
{
Name: "backend2",
Endpoints: []resolver.Endpoint{
{
Address: "1.1.1.1",
Port: 80,
},
},
},
},
}

expSubStrings := map[string]int{
"pass $dest8081;": 1,
"pass $dest8080;": 1,
"ssl_preread on;": 2,
"proxy_pass": 3,
}
g := NewWithT(t)

results := executeStreamServers(conf)
g.Expect(results).To(HaveLen(1))
result := results[0]

g.Expect(result.dest).To(Equal(streamConfigFile))
for expSubStr, expCount := range expSubStrings {
g.Expect(strings.Count(string(result.data), expSubStr)).To(Equal(expCount))
}
}

func TestCreateStreamServers(t *testing.T) {
conf := dataplane.Configuration{
TLSPassthroughServers: []dataplane.Layer4VirtualServer{
{
Hostname: "example.com",
Port: 8081,
UpstreamName: "backend1",
},
{
Hostname: "example.com",
Port: 8080,
UpstreamName: "backend1",
},
{
Hostname: "cafe.example.com",
Port: 8080,
UpstreamName: "backend2",
},
{
Hostname: "blank-upstream.example.com",
Port: 8081,
UpstreamName: "",
},
{
Hostname: "dne-upstream.example.com",
Port: 8081,
UpstreamName: "dne",
},
{
Hostname: "no-endpoints.example.com",
Port: 8081,
UpstreamName: "no-endpoints",
},
},
StreamUpstreams: []dataplane.Upstream{
{
Name: "backend1",
Endpoints: []resolver.Endpoint{
{
Address: "1.1.1.1",
Port: 80,
},
},
},
{
Name: "backend2",
Endpoints: []resolver.Endpoint{
{
Address: "1.1.1.1",
Port: 80,
},
},
},
{
Name: "no-endpoints",
Endpoints: nil,
},
},
}

streamServers := createStreamServers(conf)

g := NewWithT(t)

expectedStreamServers := []stream.Server{
{
Listen: getSocketNameTLS(conf.TLSPassthroughServers[0].Port, conf.TLSPassthroughServers[0].Hostname),
ProxyPass: conf.TLSPassthroughServers[0].UpstreamName,
SSLPreread: false,
IsSocket: true,
},
{
Listen: getSocketNameTLS(conf.TLSPassthroughServers[1].Port, conf.TLSPassthroughServers[1].Hostname),
ProxyPass: conf.TLSPassthroughServers[1].UpstreamName,
SSLPreread: false,
IsSocket: true,
},
{
Listen: getSocketNameTLS(conf.TLSPassthroughServers[2].Port, conf.TLSPassthroughServers[2].Hostname),
ProxyPass: conf.TLSPassthroughServers[2].UpstreamName,
SSLPreread: false,
IsSocket: true,
},
{
Listen: fmt.Sprint(8081),
Pass: getTLSPassthroughVarName(8081),
SSLPreread: true,
},
{
Listen: fmt.Sprint(8080),
Pass: getTLSPassthroughVarName(8080),
SSLPreread: true,
},
}
g.Expect(streamServers).To(ConsistOf(expectedStreamServers))
}

func TestExecuteStreamServersForIPFamily(t *testing.T) {
passThroughServers := []dataplane.Layer4VirtualServer{
{
UpstreamName: "backend1",
Hostname: "cafe.example.com",
Port: 8443,
},
}
streamUpstreams := []dataplane.Upstream{
{
Name: "backend1",
Endpoints: []resolver.Endpoint{
{
Address: "1.1.1.1",
},
},
},
}
tests := []struct {
msg string
expectedServerConfig map[string]int
config dataplane.Configuration
}{
{
msg: "tls servers with IPv4 IP family",
config: dataplane.Configuration{
BaseHTTPConfig: dataplane.BaseHTTPConfig{
IPFamily: dataplane.IPv4,
},
TLSPassthroughServers: passThroughServers,
StreamUpstreams: streamUpstreams,
},
expectedServerConfig: map[string]int{
"listen 8443;": 1,
"listen unix:/var/run/nginx/cafe.example.com-8443.sock;": 1,
},
},
{
msg: "tls servers with IPv6 IP family",
config: dataplane.Configuration{
BaseHTTPConfig: dataplane.BaseHTTPConfig{
IPFamily: dataplane.IPv6,
},
TLSPassthroughServers: passThroughServers,
StreamUpstreams: streamUpstreams,
},
expectedServerConfig: map[string]int{
"listen [::]:8443;": 1,
"listen unix:/var/run/nginx/cafe.example.com-8443.sock;": 1,
},
},
{
msg: "tls servers with dual IP family",
config: dataplane.Configuration{
BaseHTTPConfig: dataplane.BaseHTTPConfig{
IPFamily: dataplane.Dual,
},
TLSPassthroughServers: passThroughServers,
StreamUpstreams: streamUpstreams,
},
expectedServerConfig: map[string]int{
"listen 8443;": 1,
"listen [::]:8443;": 1,
"listen unix:/var/run/nginx/cafe.example.com-8443.sock;": 1,
},
},
}

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

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

func TestCreateStreamServersWithNone(t *testing.T) {
conf := dataplane.Configuration{
TLSPassthroughServers: nil,
}

streamServers := createStreamServers(conf)

g := NewWithT(t)

g.Expect(streamServers).To(BeNil())
}
52 changes: 52 additions & 0 deletions internal/mode/static/nginx/config/upstreams.go
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import (

"github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/stream"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
)

@@ -22,6 +23,10 @@ const (
ossZoneSize = "512k"
// plusZoneSize is the upstream zone size for nginx plus.
plusZoneSize = "1m"
// ossZoneSize is the upstream zone size for nginx open source.
ossZoneSizeStream = "512k"
// plusZoneSize is the upstream zone size for nginx plus.
plusZoneSizeStream = "1m"
)

func (g GeneratorImpl) executeUpstreams(conf dataplane.Configuration) []executeResult {
@@ -35,6 +40,53 @@ func (g GeneratorImpl) executeUpstreams(conf dataplane.Configuration) []executeR
return []executeResult{result}
}

func (g GeneratorImpl) executeStreamUpstreams(conf dataplane.Configuration) []executeResult {
upstreams := g.createStreamUpstreams(conf.StreamUpstreams)

result := executeResult{
dest: streamConfigFile,
data: helpers.MustExecuteTemplate(upstreamsTemplate, upstreams),
}

return []executeResult{result}
}

func (g GeneratorImpl) createStreamUpstreams(upstreams []dataplane.Upstream) []stream.Upstream {
ups := make([]stream.Upstream, 0, len(upstreams))

for _, u := range upstreams {
if len(u.Endpoints) != 0 {
ups = append(ups, g.createStreamUpstream(u))
}
}

return ups
}

func (g GeneratorImpl) createStreamUpstream(up dataplane.Upstream) stream.Upstream {
zoneSize := ossZoneSizeStream
if g.plus {
zoneSize = plusZoneSizeStream
}

upstreamServers := make([]stream.UpstreamServer, len(up.Endpoints))
for idx, ep := range up.Endpoints {
format := "%s:%d"
if ep.IPv6 {
format = "[%s]:%d"
}
upstreamServers[idx] = stream.UpstreamServer{
Address: fmt.Sprintf(format, ep.Address, ep.Port),
}
}

return stream.Upstream{
Name: up.Name,
ZoneSize: zoneSize,
Servers: upstreamServers,
}
}

func (g GeneratorImpl) createUpstreams(upstreams []dataplane.Upstream) []http.Upstream {
// capacity is the number of upstreams + 1 for the invalid backend ref upstream
ups := make([]http.Upstream, 0, len(upstreams)+1)
5 changes: 3 additions & 2 deletions internal/mode/static/nginx/config/upstreams_template.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package config

// FIXME(kate-osborn): Dynamically calculate upstream zone size based on the number of upstreams.
// 512k will support up to 648 upstream servers for OSS.
// NGINX Plus needs 1m to support roughly the same amount of servers (556 upstream servers).
// 512k will support up to 648 http upstream servers for OSS.
// NGINX Plus needs 1m to support roughly the same amount of http servers (556 upstream servers).
// For stream upstream servers, 512k will support 576 in OSS and 1m will support 991 in NGINX Plus
// https://github.com/nginxinc/nginx-gateway-fabric/issues/483
const upstreamsTemplateText = `
{{ range $u := . }}
189 changes: 189 additions & 0 deletions internal/mode/static/nginx/config/upstreams_test.go
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import (
. "github.com/onsi/gomega"

"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/stream"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver"
)
@@ -307,3 +308,191 @@ func TestCreateUpstreamPlus(t *testing.T) {
g := NewWithT(t)
g.Expect(result).To(Equal(expectedUpstream))
}

func TestExecuteStreamUpstreams(t *testing.T) {
gen := GeneratorImpl{}
stateUpstreams := []dataplane.Upstream{
{
Name: "up1",
Endpoints: []resolver.Endpoint{
{
Address: "10.0.0.0",
Port: 80,
},
},
},
{
Name: "up2",
Endpoints: []resolver.Endpoint{
{
Address: "11.0.0.0",
Port: 80,
},
},
},
{
Name: "up3",
Endpoints: []resolver.Endpoint{},
},
}

expectedSubStrings := []string{
"upstream up1",
"upstream up2",
"server 10.0.0.0:80;",
"server 11.0.0.0:80;",
}

upstreamResults := gen.executeStreamUpstreams(dataplane.Configuration{StreamUpstreams: stateUpstreams})
g := NewWithT(t)
g.Expect(upstreamResults).To(HaveLen(1))
upstreams := string(upstreamResults[0].data)

g.Expect(upstreamResults[0].dest).To(Equal(streamConfigFile))
for _, expSubString := range expectedSubStrings {
g.Expect(upstreams).To(ContainSubstring(expSubString))
}
}

func TestCreateStreamUpstreams(t *testing.T) {
gen := GeneratorImpl{}
stateUpstreams := []dataplane.Upstream{
{
Name: "up1",
Endpoints: []resolver.Endpoint{
{
Address: "10.0.0.0",
Port: 80,
},
{
Address: "10.0.0.1",
Port: 80,
},
{
Address: "10.0.0.2",
Port: 80,
},
{
Address: "2001:db8::1",
IPv6: true,
},
},
},
{
Name: "up2",
Endpoints: []resolver.Endpoint{
{
Address: "11.0.0.0",
Port: 80,
},
},
},
{
Name: "up3",
Endpoints: []resolver.Endpoint{},
},
}

expUpstreams := []stream.Upstream{
{
Name: "up1",
ZoneSize: ossZoneSize,
Servers: []stream.UpstreamServer{
{
Address: "10.0.0.0:80",
},
{
Address: "10.0.0.1:80",
},
{
Address: "10.0.0.2:80",
},
{
Address: "[2001:db8::1]:0",
},
},
},
{
Name: "up2",
ZoneSize: ossZoneSize,
Servers: []stream.UpstreamServer{
{
Address: "11.0.0.0:80",
},
},
},
}

g := NewWithT(t)
result := gen.createStreamUpstreams(stateUpstreams)
g.Expect(result).To(Equal(expUpstreams))
}

func TestCreateStreamUpstream(t *testing.T) {
gen := GeneratorImpl{}
up := dataplane.Upstream{
Name: "multiple-endpoints",
Endpoints: []resolver.Endpoint{
{
Address: "10.0.0.1",
Port: 80,
},
{
Address: "10.0.0.2",
Port: 80,
},
{
Address: "10.0.0.3",
Port: 80,
},
},
}

expectedUpstream := stream.Upstream{
Name: "multiple-endpoints",
ZoneSize: ossZoneSize,
Servers: []stream.UpstreamServer{
{
Address: "10.0.0.1:80",
},
{
Address: "10.0.0.2:80",
},
{
Address: "10.0.0.3:80",
},
},
}

g := NewWithT(t)
result := gen.createStreamUpstream(up)
g.Expect(result).To(Equal(expectedUpstream))
}

func TestCreateStreamUpstreamPlus(t *testing.T) {
gen := GeneratorImpl{plus: true}

stateUpstream := dataplane.Upstream{
Name: "multiple-endpoints",
Endpoints: []resolver.Endpoint{
{
Address: "10.0.0.1",
Port: 80,
},
},
}
expectedUpstream := stream.Upstream{
Name: "multiple-endpoints",
ZoneSize: plusZoneSize,
Servers: []stream.UpstreamServer{
{
Address: "10.0.0.1:80",
},
},
}

result := gen.createStreamUpstream(stateUpstream)

g := NewWithT(t)
g.Expect(result).To(Equal(expectedUpstream))
}
7 changes: 7 additions & 0 deletions internal/mode/static/state/change_processor.go
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import (
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
v1 "sigs.k8s.io/gateway-api/apis/v1"
"sigs.k8s.io/gateway-api/apis/v1alpha2"
"sigs.k8s.io/gateway-api/apis/v1alpha3"
"sigs.k8s.io/gateway-api/apis/v1beta1"

@@ -107,6 +108,7 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl {
ConfigMaps: make(map[types.NamespacedName]*apiv1.ConfigMap),
NginxProxies: make(map[types.NamespacedName]*ngfAPI.NginxProxy),
GRPCRoutes: make(map[types.NamespacedName]*v1.GRPCRoute),
TLSRoutes: make(map[types.NamespacedName]*v1alpha2.TLSRoute),
NGFPolicies: make(map[graph.PolicyKey]policies.Policy),
}

@@ -211,6 +213,11 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl {
store: commonPolicyObjectStore,
predicate: funcPredicate{stateChanged: isNGFPolicyRelevant},
},
{
gvk: cfg.MustExtractGVK(&v1alpha2.TLSRoute{}),
store: newObjectStoreMapAdapter(clusterStore.TLSRoutes),
predicate: nil,
},
},
)

4 changes: 4 additions & 0 deletions internal/mode/static/state/change_processor_test.go
Original file line number Diff line number Diff line change
@@ -199,6 +199,7 @@ func createScheme() *runtime.Scheme {

utilruntime.Must(v1.Install(scheme))
utilruntime.Must(v1beta1.Install(scheme))
utilruntime.Must(v1alpha2.Install(scheme))
utilruntime.Must(v1alpha3.Install(scheme))
utilruntime.Must(apiv1.AddToScheme(scheme))
utilruntime.Must(discoveryV1.AddToScheme(scheme))
@@ -528,6 +529,7 @@ var _ = Describe("ChangeProcessor", func() {
Valid: true,
Attachable: true,
Routes: map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1},
L4Routes: map[graph.L4RouteKey]*graph.L4Route{},
SupportedKinds: []v1.RouteGroupKind{
{Kind: v1.Kind(kinds.HTTPRoute), Group: helpers.GetPointer[v1.Group](v1.GroupName)},
{Kind: v1.Kind(kinds.GRPCRoute), Group: helpers.GetPointer[v1.Group](v1.GroupName)},
@@ -539,6 +541,7 @@ var _ = Describe("ChangeProcessor", func() {
Valid: true,
Attachable: true,
Routes: map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1},
L4Routes: map[graph.L4RouteKey]*graph.L4Route{},
ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(diffNsTLSSecret)),
SupportedKinds: []v1.RouteGroupKind{
{Kind: v1.Kind(kinds.HTTPRoute), Group: helpers.GetPointer[v1.Group](v1.GroupName)},
@@ -549,6 +552,7 @@ var _ = Describe("ChangeProcessor", func() {
Valid: true,
},
IgnoredGateways: map[types.NamespacedName]*v1.Gateway{},
L4Routes: map[graph.L4RouteKey]*graph.L4Route{},
Routes: map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1},
ReferencedSecrets: map[types.NamespacedName]*graph.Secret{},
ReferencedServices: map[types.NamespacedName]struct{}{
35 changes: 35 additions & 0 deletions internal/mode/static/state/conditions/conditions.go
Original file line number Diff line number Diff line change
@@ -32,6 +32,10 @@ const (
// RouteReasonInvalidListener is used with the "Accepted" condition when the Route references an invalid listener.
RouteReasonInvalidListener v1.RouteConditionReason = "InvalidListener"

// RouteReasonHostnameConflict is used with the "Accepted" condition when a route has the exact same hostname
// as another route.
RouteReasonHostnameConflict v1.RouteConditionReason = "HostnameConflict"

// RouteReasonGatewayNotProgrammed is used when the associated Gateway is not programmed.
// Used with Accepted (false).
RouteReasonGatewayNotProgrammed v1.RouteConditionReason = "GatewayNotProgrammed"
@@ -187,6 +191,17 @@ func NewRouteInvalidListener() conditions.Condition {
}
}

// NewRouteHostnameConflict returns a Condition that indicates that the Route is not accepted because of a
// conflicting hostname on the same port.
func NewRouteHostnameConflict() conditions.Condition {
return conditions.Condition{
Type: string(v1.RouteConditionAccepted),
Status: metav1.ConditionFalse,
Reason: string(RouteReasonHostnameConflict),
Message: "Hostname(s) conflict with another route of the same kind on the same port",
}
}

// NewRouteResolvedRefs returns a Condition that indicates that all the references on the Route are resolved.
func NewRouteResolvedRefs() conditions.Condition {
return conditions.Condition{
@@ -424,6 +439,26 @@ func NewListenerProtocolConflict(msg string) []conditions.Condition {
}
}

// NewListenerHostnameConflict returns Conditions that indicate multiple Listeners are specified with the same
// Listener port, but are HTTPS and TLS and have overlapping hostnames.
func NewListenerHostnameConflict(msg string) []conditions.Condition {
return []conditions.Condition{
{
Type: string(v1.ListenerConditionAccepted),
Status: metav1.ConditionFalse,
Reason: string(v1.ListenerReasonHostnameConflict),
Message: msg,
},
{
Type: string(v1.ListenerConditionConflicted),
Status: metav1.ConditionTrue,
Reason: string(v1.ListenerReasonHostnameConflict),
Message: msg,
},
NewListenerNotProgrammedInvalid(msg),
}
}

// NewListenerUnsupportedProtocol returns Conditions that indicate that the protocol of a Listener is unsupported.
func NewListenerUnsupportedProtocol(msg string) []conditions.Condition {
return []conditions.Condition{
162 changes: 151 additions & 11 deletions internal/mode/static/state/dataplane/configuration.go
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ const (
func BuildConfiguration(
ctx context.Context,
g *graph.Graph,
resolver resolver.ServiceResolver,
serviceResolver resolver.ServiceResolver,
configVersion int,
) Configuration {
if g.GatewayClass == nil || !g.GatewayClass.Valid {
@@ -41,28 +41,164 @@ func BuildConfiguration(
}

baseHTTPConfig := buildBaseHTTPConfig(g)
upstreams := buildUpstreams(ctx, g.Gateway.Listeners, resolver, baseHTTPConfig.IPFamily)

upstreams := buildUpstreams(ctx, g.Gateway.Listeners, serviceResolver, baseHTTPConfig.IPFamily)
httpServers, sslServers := buildServers(g)
passthroughServers := buildPassthroughServers(g)
streamUpstreams := buildStreamUpstreams(ctx, g.Gateway.Listeners, serviceResolver, baseHTTPConfig.IPFamily)
backendGroups := buildBackendGroups(append(httpServers, sslServers...))
keyPairs := buildSSLKeyPairs(g.ReferencedSecrets, g.Gateway.Listeners)
certBundles := buildCertBundles(g.ReferencedCaCertConfigMaps, backendGroups)
telemetry := buildTelemetry(g)

config := Configuration{
HTTPServers: httpServers,
SSLServers: sslServers,
Upstreams: upstreams,
BackendGroups: backendGroups,
SSLKeyPairs: keyPairs,
Version: configVersion,
CertBundles: certBundles,
Telemetry: telemetry,
BaseHTTPConfig: baseHTTPConfig,
HTTPServers: httpServers,
SSLServers: sslServers,
TLSPassthroughServers: passthroughServers,
Upstreams: upstreams,
StreamUpstreams: streamUpstreams,
BackendGroups: backendGroups,
SSLKeyPairs: keyPairs,
Version: configVersion,
CertBundles: certBundles,
Telemetry: telemetry,
BaseHTTPConfig: baseHTTPConfig,
}

return config
}

// buildPassthroughServers builds TLSPassthroughServers from TLSRoutes attaches to listeners.
func buildPassthroughServers(g *graph.Graph) []Layer4VirtualServer {
passthroughServersMap := make(map[graph.L4RouteKey][]Layer4VirtualServer)
listenerPassthroughServers := make([]Layer4VirtualServer, 0)

passthroughServerCount := 0

for _, l := range g.Gateway.Listeners {
if !l.Valid || l.Source.Protocol != v1.TLSProtocolType {
continue
}
foundRouteMatchingListenerHostname := false
for key, r := range l.L4Routes {
if !r.Valid {
continue
}

var hostnames []string

for _, p := range r.ParentRefs {
if val, exist := p.Attachment.AcceptedHostnames[l.Name]; exist {
hostnames = val
break
}
}

if _, ok := passthroughServersMap[key]; !ok {
passthroughServersMap[key] = make([]Layer4VirtualServer, 0)
}

passthroughServerCount += len(hostnames)

for _, h := range hostnames {
if l.Source.Hostname != nil && h == string(*l.Source.Hostname) {
foundRouteMatchingListenerHostname = true
}
passthroughServersMap[key] = append(passthroughServersMap[key], Layer4VirtualServer{
Hostname: h,
UpstreamName: r.Spec.BackendRef.ServicePortReference(),
Port: int32(l.Source.Port),
})
}
}
if !foundRouteMatchingListenerHostname {
if l.Source.Hostname != nil {
listenerPassthroughServers = append(listenerPassthroughServers, Layer4VirtualServer{
Hostname: string(*l.Source.Hostname),
IsDefault: true,
Port: int32(l.Source.Port),
})
} else {
listenerPassthroughServers = append(listenerPassthroughServers, Layer4VirtualServer{
Hostname: "",
Port: int32(l.Source.Port),
})
}
}
}
passthroughServers := make([]Layer4VirtualServer, 0, passthroughServerCount+len(listenerPassthroughServers))

for _, r := range passthroughServersMap {
passthroughServers = append(passthroughServers, r...)
}

passthroughServers = append(passthroughServers, listenerPassthroughServers...)

return passthroughServers
}

// buildStreamUpstreams builds all stream upstreams.
func buildStreamUpstreams(
ctx context.Context,
listeners []*graph.Listener,
serviceResolver resolver.ServiceResolver,
ipFamily IPFamilyType,
) []Upstream {
// There can be duplicate upstreams if multiple routes reference the same upstream.
// We use a map to deduplicate them.
uniqueUpstreams := make(map[string]Upstream)

for _, l := range listeners {
if !l.Valid || l.Source.Protocol != v1.TLSProtocolType {
continue
}

for _, route := range l.L4Routes {
if !route.Valid {
continue
}

br := route.Spec.BackendRef

if !br.Valid {
continue
}

upstreamName := br.ServicePortReference()

if _, exist := uniqueUpstreams[upstreamName]; exist {
continue
}

var errMsg string

allowedAddressType := getAllowedAddressType(ipFamily)

eps, err := serviceResolver.Resolve(ctx, br.SvcNsName, br.ServicePort, allowedAddressType)
if err != nil {
errMsg = err.Error()
}

uniqueUpstreams[upstreamName] = Upstream{
Name: upstreamName,
Endpoints: eps,
ErrorMsg: errMsg,
}
}
}

if len(uniqueUpstreams) == 0 {
return nil
}

upstreams := make([]Upstream, 0, len(uniqueUpstreams))

for _, up := range uniqueUpstreams {
upstreams = append(upstreams, up)
}
return upstreams
}

// buildSSLKeyPairs builds the SSLKeyPairs from the Secrets. It will only include Secrets that are referenced by
// valid listeners, so that we don't include unused Secrets in the configuration of the data plane.
func buildSSLKeyPairs(
@@ -210,6 +346,9 @@ func buildServers(g *graph.Graph) (http, ssl []VirtualServer) {
}

for _, l := range g.Gateway.Listeners {
if l.Source.Protocol == v1.TLSProtocolType {
continue
}
if l.Valid {
rules := rulesForProtocol[l.Source.Protocol][l.Source.Port]
if rules == nil {
@@ -313,6 +452,7 @@ func (hpr *hostPathRules) upsertRoute(
for _, p := range route.ParentRefs {
if val, exist := p.Attachment.AcceptedHostnames[string(listener.Source.Name)]; exist {
hostnames = val
break
}
}

407 changes: 406 additions & 1 deletion internal/mode/static/state/dataplane/configuration_test.go

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion internal/mode/static/state/dataplane/types.go
Original file line number Diff line number Diff line change
@@ -30,8 +30,12 @@ type Configuration struct {
HTTPServers []VirtualServer
// SSLServers holds all SSLServers.
SSLServers []VirtualServer
// Upstreams holds all unique Upstreams.
// TLSPassthroughServers hold all TLSPassthroughServers
TLSPassthroughServers []Layer4VirtualServer
// Upstreams holds all unique http Upstreams.
Upstreams []Upstream
// StreamUpstreams holds all unique stream Upstreams
StreamUpstreams []Upstream
// BackendGroups holds all unique BackendGroups.
BackendGroups []BackendGroup
// BaseHTTPConfig holds the configuration options at the http context.
@@ -77,6 +81,18 @@ type VirtualServer struct {
IsDefault bool
}

// Layer4VirtualServer is a virtual server for Layer 4 traffic.
type Layer4VirtualServer struct {
// Hostname is the hostname of the server.
Hostname string
// UpstreamName refers to the name of the upstream that is used.
UpstreamName string
// Port is the port of the server.
Port int32
// IsDefault refers to whether this server is created for the default listener hostname.
IsDefault bool
}

// Upstream is a pool of endpoints to be load balanced.
type Upstream struct {
// Name is the name of the Upstream. Will be unique for each service/port combination.
2 changes: 1 addition & 1 deletion internal/mode/static/state/graph/backend_refs.go
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ import (
staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
)

// BackendRef is an internal representation of a backendRef in an HTTP/GRPCRoute.
// BackendRef is an internal representation of a backendRef in an HTTP/GRPC/TLSRoute.
type BackendRef struct {
// BackendTLSPolicy is the BackendTLSPolicy of the Service which is referenced by the backendRef.
BackendTLSPolicy *BackendTLSPolicy
160 changes: 133 additions & 27 deletions internal/mode/static/state/graph/gateway_listener.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package graph
import (
"errors"
"fmt"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
@@ -25,6 +26,8 @@ type Listener struct {
// Routes holds the GRPC/HTTPRoutes attached to the Listener.
// Only valid routes are attached.
Routes map[RouteKey]*L7Route
// L4Routes holds the TLSRoutes attached to the Listener.
L4Routes map[L4RouteKey]*L4Route
// AllowedRouteLabelSelector is the label selector for this Listener's allowed routes, if defined.
AllowedRouteLabelSelector labels.Selector
// ResolvedSecret is the namespaced name of the Secret resolved for this listener.
@@ -61,7 +64,7 @@ func buildListeners(
}

type listenerConfiguratorFactory struct {
http, https, unsupportedProtocol *listenerConfigurator
http, https, tls, unsupportedProtocol *listenerConfigurator
}

func (f *listenerConfiguratorFactory) getConfiguratorForListener(l v1.Listener) *listenerConfigurator {
@@ -70,6 +73,8 @@ func (f *listenerConfiguratorFactory) getConfiguratorForListener(l v1.Listener)
return f.http
case v1.HTTPSProtocolType:
return f.https
case v1.TLSProtocolType:
return f.tls
default:
return f.unsupportedProtocol
}
@@ -90,7 +95,7 @@ func newListenerConfiguratorFactory(
valErr := field.NotSupported(
field.NewPath("protocol"),
listener.Protocol,
[]string{string(v1.HTTPProtocolType), string(v1.HTTPSProtocolType)},
[]string{string(v1.HTTPProtocolType), string(v1.HTTPSProtocolType), string(v1.TLSProtocolType)},
)
return staticConds.NewListenerUnsupportedProtocol(valErr.Error()), false /* not attachable */
},
@@ -121,6 +126,18 @@ func newListenerConfiguratorFactory(
createExternalReferencesForTLSSecretsResolver(gw.Namespace, secretResolver, refGrantResolver),
},
},
tls: &listenerConfigurator{
validators: []listenerValidator{
validateListenerAllowedRouteKind,
validateListenerLabelSelector,
validateListenerHostname,
validateTLSFieldOnTLSListener,
},
conflictResolvers: []listenerConflictResolver{
sharedPortConflictResolver,
},
externalReferenceResolvers: []listenerExternalReferenceResolver{},
},
}
}

@@ -184,6 +201,7 @@ func (c *listenerConfigurator) configure(listener v1.Listener) *Listener {
Conditions: conds,
AllowedRouteLabelSelector: allowedRouteSelector,
Routes: make(map[RouteKey]*L7Route),
L4Routes: make(map[L4RouteKey]*L4Route),
Valid: valid,
Attachable: attachable,
SupportedKinds: supportedKinds,
@@ -235,37 +253,51 @@ func getAndValidateListenerSupportedKinds(listener v1.Listener) (
var conds []conditions.Condition
var supportedKinds []v1.RouteGroupKind

validRouteKind := func(kind v1.RouteGroupKind) bool {
if kind.Kind != v1.Kind(kinds.HTTPRoute) && kind.Kind != v1.Kind(kinds.GRPCRoute) {
return false
var validKinds []v1.RouteGroupKind

switch listener.Protocol {
case v1.HTTPProtocolType, v1.HTTPSProtocolType:
validKinds = []v1.RouteGroupKind{
{Kind: v1.Kind(kinds.HTTPRoute), Group: helpers.GetPointer[v1.Group](v1.GroupName)},
{Kind: v1.Kind(kinds.GRPCRoute), Group: helpers.GetPointer[v1.Group](v1.GroupName)},
}
if kind.Group == nil || *kind.Group != v1.GroupName {
case v1.TLSProtocolType:
validKinds = []v1.RouteGroupKind{
{Kind: v1.Kind(kinds.TLSRoute), Group: helpers.GetPointer[v1.Group](v1.GroupName)},
}
}

validProtocolRouteKind := func(kind v1.RouteGroupKind) bool {
if kind.Group != nil && *kind.Group != v1.GroupName {
return false
}
return true
for _, k := range validKinds {
if k.Kind == kind.Kind {
return true
}
}

return false
}

if listener.AllowedRoutes != nil && listener.AllowedRoutes.Kinds != nil {
supportedKinds = make([]v1.RouteGroupKind, 0, len(listener.AllowedRoutes.Kinds))
for _, kind := range listener.AllowedRoutes.Kinds {
if !validRouteKind(kind) {
msg := fmt.Sprintf("Unsupported route kind \"%s/%s\"", *kind.Group, kind.Kind)
if !validProtocolRouteKind(kind) {
group := v1.GroupName
if kind.Group != nil {
group = string(*kind.Group)
}
msg := fmt.Sprintf("Unsupported route kind for protocol %s \"%s/%s\"", listener.Protocol, group, kind.Kind)
conds = append(conds, staticConds.NewListenerInvalidRouteKinds(msg)...)
continue
}
supportedKinds = append(supportedKinds, kind)
}
} else {
switch listener.Protocol {
case v1.HTTPProtocolType, v1.HTTPSProtocolType:
supportedKinds = []v1.RouteGroupKind{
{Kind: v1.Kind(kinds.HTTPRoute), Group: helpers.GetPointer[v1.Group](v1.GroupName)},
{Kind: v1.Kind(kinds.GRPCRoute), Group: helpers.GetPointer[v1.Group](v1.GroupName)},
}
}
return conds, supportedKinds
}

return conds, supportedKinds
return conds, validKinds
}

func validateListenerAllowedRouteKind(listener v1.Listener) (conds []conditions.Condition, attachable bool) {
@@ -321,6 +353,19 @@ func validateListenerPort(port v1.PortNumber, protectedPorts ProtectedPorts) err
return nil
}

func validateTLSFieldOnTLSListener(listener v1.Listener) (conds []conditions.Condition, attachable bool) {
tlspath := field.NewPath("TLS")
if listener.TLS == nil {
valErr := field.Required(tlspath, "tls must be defined for TLS listener")
return staticConds.NewListenerUnsupportedValue(valErr.Error()), false
}
if listener.TLS.Mode == nil || *listener.TLS.Mode != v1.TLSModePassthrough {
valErr := field.Required(tlspath.Child("Mode"), "Mode must be passthrough for TLS listener")
return staticConds.NewListenerUnsupportedValue(valErr.Error()), false
}
return nil, true
}

func createHTTPSListenerValidator(protectedPorts ProtectedPorts) listenerValidator {
return func(listener v1.Listener) (conds []conditions.Condition, attachable bool) {
if err := validateListenerPort(listener.Port, protectedPorts); err != nil {
@@ -387,13 +432,25 @@ func createHTTPSListenerValidator(protectedPorts ProtectedPorts) listenerValidat
}

func createPortConflictResolver() listenerConflictResolver {
const (
secureProtocolGroup int = 0
insecureProtocolGroup int = 1
)
protocolGroups := map[v1.ProtocolType]int{
v1.TLSProtocolType: secureProtocolGroup,
v1.HTTPProtocolType: insecureProtocolGroup,
v1.HTTPSProtocolType: secureProtocolGroup,
}
conflictedPorts := make(map[v1.PortNumber]bool)
portProtocolOwner := make(map[v1.PortNumber]v1.ProtocolType)
portProtocolOwner := make(map[v1.PortNumber]int)
listenersByPort := make(map[v1.PortNumber][]*Listener)

format := "Multiple listeners for the same port %d specify incompatible protocols; " +
"ensure only one protocol per port"

formatHostname := "HTTPS and TLS listeners for the same port %d specify overlapping hostnames; " +
"ensure no overlapping hostnames for HTTPS and TLS listeners for the same port"

return func(l *Listener) {
port := l.Source.Port

@@ -409,24 +466,45 @@ func createPortConflictResolver() listenerConflictResolver {
// otherwise, we add the listener to the list of listeners for this port
// and then check if the protocol owner for the port is different from the current listener's protocol.

listenersByPort[port] = append(listenersByPort[port], l)

protocol, ok := portProtocolOwner[port]
protocolGroup, ok := portProtocolOwner[port]
if !ok {
portProtocolOwner[port] = l.Source.Protocol
portProtocolOwner[port] = protocolGroups[l.Source.Protocol]
listenersByPort[port] = append(listenersByPort[port], l)
return
}

// if protocol owner doesn't match the listener's protocol we mark the port as conflicted,
// if protocol group owner doesn't match the listener's protocol group we mark the port as conflicted,
// and invalidate all listeners we've seen for this port.
if protocol != l.Source.Protocol {
if protocolGroup != protocolGroups[l.Source.Protocol] {
conflictedPorts[port] = true
for _, l := range listenersByPort[port] {
l.Valid = false
for _, listener := range listenersByPort[port] {
listener.Valid = false
conflictedConds := staticConds.NewListenerProtocolConflict(fmt.Sprintf(format, port))
listener.Conditions = append(listener.Conditions, conflictedConds...)
}
l.Valid = false
conflictedConds := staticConds.NewListenerProtocolConflict(fmt.Sprintf(format, port))
l.Conditions = append(l.Conditions, conflictedConds...)
} else {
foundConflict := false
for _, listener := range listenersByPort[port] {
if listener.Source.Protocol != l.Source.Protocol &&
haveOverlap(l.Source.Hostname, listener.Source.Hostname) {
listener.Valid = false
conflictedConds := staticConds.NewListenerHostnameConflict(fmt.Sprintf(formatHostname, port))
listener.Conditions = append(listener.Conditions, conflictedConds...)
foundConflict = true
}
}

if foundConflict {
l.Valid = false
conflictedConds := staticConds.NewListenerHostnameConflict(fmt.Sprintf(formatHostname, port))
l.Conditions = append(l.Conditions, conflictedConds...)
}
}

listenersByPort[port] = append(listenersByPort[port], l)
}
}

@@ -482,3 +560,31 @@ func GetAllowedRouteLabelSelector(l v1.Listener) *metav1.LabelSelector {

return nil
}

// matchesWildcard checks if hostname2 matches the wildcard pattern of hostname1.
func matchesWildcard(hostname1, hostname2 string) bool {
matchesWildcard := func(h1, h2 string) bool {
if strings.HasPrefix(h1, "*.") {
// Remove the "*." from h1
h1 = h1[2:]
// Check if h2 ends with h1
return strings.HasSuffix(h2, h1)
}
return false
}
return matchesWildcard(hostname1, hostname2) || matchesWildcard(hostname2, hostname1)
}

// haveOverlap checks for overlap between two hostnames.
func haveOverlap(hostname1, hostname2 *v1.Hostname) bool {
// Check if hostname1 matches wildcard pattern of hostname2 or vice versa
if hostname1 == nil || hostname2 == nil {
return true
}
h1, h2 := string(*hostname1), string(*hostname2)

if h1 == h2 {
return true
}
return matchesWildcard(h1, h2)
}
150 changes: 150 additions & 0 deletions internal/mode/static/state/graph/gateway_listener_test.go
Original file line number Diff line number Diff line change
@@ -303,6 +303,10 @@ func TestGetAndValidateListenerSupportedKinds(t *testing.T) {
Group: helpers.GetPointer[v1.Group](v1.GroupName),
},
}
TLSRouteGroupKind := v1.RouteGroupKind{
Kind: kinds.TLSRoute,
Group: helpers.GetPointer[v1.Group](v1.GroupName),
}
tests := []struct {
protocol v1.ProtocolType
name string
@@ -357,6 +361,7 @@ func TestGetAndValidateListenerSupportedKinds(t *testing.T) {
HTTPRouteGroupKind, GRPCRouteGroupKind,
},
},

{
protocol: v1.HTTPProtocolType,
kind: []v1.RouteGroupKind{
@@ -365,11 +370,49 @@ func TestGetAndValidateListenerSupportedKinds(t *testing.T) {
Kind: "bad-kind",
Group: helpers.GetPointer[v1.Group](v1.GroupName),
},
TLSRouteGroupKind,
},
expectErr: true,
name: "valid and invalid kinds",
expected: []v1.RouteGroupKind{HTTPRouteGroupKind},
},
{
protocol: v1.TLSProtocolType,
kind: []v1.RouteGroupKind{
HTTPRouteGroupKind,
{
Kind: "bad-kind",
Group: helpers.GetPointer[v1.Group](v1.GroupName),
},
TLSRouteGroupKind,
GRPCRouteGroupKind,
},
expectErr: true,
name: "valid and invalid kinds for TLS protocol",
expected: []v1.RouteGroupKind{TLSRouteGroupKind},
},
{
protocol: v1.TLSProtocolType,
kind: []v1.RouteGroupKind{
HTTPRouteGroupKind,
{
Kind: "bad-kind",
Group: helpers.GetPointer[v1.Group](v1.GroupName),
},
GRPCRouteGroupKind,
},
expectErr: true,
name: "invalid kinds for TLS protocol",
expected: []v1.RouteGroupKind{},
},
{
protocol: v1.TLSProtocolType,
kind: []v1.RouteGroupKind{
TLSRouteGroupKind,
},
name: "valid kinds for TLS protocol",
expected: []v1.RouteGroupKind{TLSRouteGroupKind},
},
}

for _, test := range tests {
@@ -471,3 +514,110 @@ func TestValidateListenerPort(t *testing.T) {
})
}
}

func TestListenerNamesHaveOverlap(t *testing.T) {
tests := []struct {
hostname1 *v1.Hostname
hostname2 *v1.Hostname
msg string
expectResult bool
}{
{
hostname1: (*v1.Hostname)(helpers.GetPointer("*.example.com")),
hostname2: (*v1.Hostname)(helpers.GetPointer("*.example.com")),
expectResult: true,
msg: "same hostnames with wildcard",
},
{
hostname1: nil,
hostname2: nil,
expectResult: true,
msg: "two nil hostnames",
},
{
hostname1: (*v1.Hostname)(helpers.GetPointer("cafe.example.com")),
hostname2: (*v1.Hostname)(helpers.GetPointer("app.example.com")),
expectResult: false,
msg: "two different hostnames no wildcard",
},
{
hostname1: (*v1.Hostname)(helpers.GetPointer("cafe.example.com")),
hostname2: nil,
expectResult: true,
msg: "hostname1 is nil",
},
{
hostname1: nil,
hostname2: (*v1.Hostname)(helpers.GetPointer("cafe.example.com")),
expectResult: true,
msg: "hostname2 is nil",
},
{
hostname1: (*v1.Hostname)(helpers.GetPointer("*.example.com")),
hostname2: (*v1.Hostname)(helpers.GetPointer("*.example.org")),
expectResult: false,
msg: "wildcard hostnames that do not overlap",
},
{
hostname1: (*v1.Hostname)(helpers.GetPointer("*.example.com")),
hostname2: (*v1.Hostname)(helpers.GetPointer("cafe.example.com")),
expectResult: true,
msg: "one wildcard hostname and one hostname that overlap",
},
}

for _, test := range tests {
t.Run(test.msg, func(t *testing.T) {
g := NewWithT(t)
g.Expect(haveOverlap(test.hostname1, test.hostname2)).To(Equal(test.expectResult))
})
}
}

func TestValidateTLSFieldOnTLSListener(t *testing.T) {
tests := []struct {
listener v1.Listener
msg string
expectedCond []conditions.Condition
expectValid bool
}{
{
listener: v1.Listener{},
expectedCond: staticConds.NewListenerUnsupportedValue(
"TLS: Required value: tls must be defined for TLS listener",
),
expectValid: false,
msg: "TLS listener without tls field",
},
{
listener: v1.Listener{TLS: nil},
expectedCond: staticConds.NewListenerUnsupportedValue(
"TLS: Required value: tls must be defined for TLS listener",
),
expectValid: false,
msg: "TLS listener with TLS field nil",
},
{
listener: v1.Listener{TLS: &v1.GatewayTLSConfig{Mode: helpers.GetPointer(v1.TLSModeTerminate)}},
expectedCond: staticConds.NewListenerUnsupportedValue(
"TLS.Mode: Required value: Mode must be passthrough for TLS listener",
),
expectValid: false,
msg: "TLS listener with TLS mode terminate",
},
{
listener: v1.Listener{TLS: &v1.GatewayTLSConfig{Mode: helpers.GetPointer(v1.TLSModePassthrough)}},
expectValid: true,
msg: "TLS listener with TLS mode passthrough",
},
}
for _, test := range tests {
t.Run(test.msg, func(t *testing.T) {
g := NewWithT(t)
cond, valid := validateTLSFieldOnTLSListener(test.listener)

g.Expect(cond).To(BeEquivalentTo(test.expectedCond))
g.Expect(valid).To(BeEquivalentTo(test.expectValid))
})
}
}
Loading