Skip to content

Commit 99061fc

Browse files
weltekialexellis
authored andcommitted
Add proxy protocol support for tunnel servers
Add support for configuring proxy protocol on inlets tunnel servers provisioned by the operator. When enabled, the tunnel server is started with the --proxy-proto flag so that the original client IP address is preserved and forwarded to upstream services. The proxy protocol can be set per-service using the operator.inlets.dev/proxy-proto annotation. Configuration options: - Annotation: operator.inlets.dev/proxy-proto (per-service) Note: CRD has been updated with a new field. Signed-off-by: Han Verstraete (OpenFaaS Ltd) <han@openfaas.com> Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
1 parent 081e522 commit 99061fc

13 files changed

Lines changed: 230 additions & 33 deletions

File tree

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ COPY validate.go validate.go
3030
COPY validate_test.go validate_test.go
3131
COPY config.go config.go
3232
COPY config_test.go config_test.go
33+
COPY userdata.go userdata.go
34+
COPY userdata_test.go userdata_test.go
3335

3436
RUN gofmt -l -d $(find . -type f -name '*.go' -not -path "./vendor/*")
3537

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ Install the chart with `annotatedOnly: true`, then run:
6363
kubectl annotate service nginx-1 operator.inlets.dev/manage=1
6464
```
6565

66+
## Proxy Protocol support
67+
68+
Proxy protocol can be enabled on tunnel exit servers so that the original client IP address is preserved and forwarded to your services. This is controlled by an annotation.
69+
70+
Allowed values are `v1`, `v2`, or `""` (disabled).
71+
72+
```bash
73+
kubectl annotate service nginx-1 operator.inlets.dev/proxy-proto=v2
74+
```
75+
76+
> **Important**: The proxy protocol configuration is applied when the tunnel exit server VM is provisioned and **cannot be changed afterwards**. If you need to change the proxy protocol setting for an existing service, you must delete the service (which will delete the tunnel and VM), then recreate it with the new annotation.
77+
6678
## Using IPVS for your Kubernetes networking?
6779

6880
For IPVS, you need to declare a Tunnel Custom Resource instead of using the LoadBalancer field.

chart/inlets-operator/crds/operator.inlets.dev_tunnels.yaml

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ apiVersion: apiextensions.k8s.io/v1
22
kind: CustomResourceDefinition
33
metadata:
44
annotations:
5-
controller-gen.kubebuilder.io/version: v0.11.1
6-
creationTimestamp: null
5+
controller-gen.kubebuilder.io/version: v0.14.0
76
name: tunnels.operator.inlets.dev
87
spec:
98
group: operator.inlets.dev
@@ -29,7 +28,7 @@ spec:
2928
name: HostIP
3029
type: string
3130
- jsonPath: .metadata.creationTimestamp
32-
name: Created
31+
name: Age
3332
type: date
3433
- jsonPath: .status.hostId
3534
name: HostID
@@ -50,10 +49,19 @@ spec:
5049
type: object
5150
properties:
5251
apiVersion:
53-
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
52+
description: |-
53+
APIVersion defines the versioned schema of this representation of an object.
54+
Servers should convert recognized schemas to the latest internal value, and
55+
may reject unrecognized values.
56+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
5457
type: string
5558
kind:
56-
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
59+
description: |-
60+
Kind is a string value representing the REST resource this object represents.
61+
Servers may infer this from the endpoint the client submits requests to.
62+
Cannot be updated.
63+
In CamelCase.
64+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
5765
type: string
5866
metadata:
5967
type: object
@@ -71,14 +79,23 @@ spec:
7179
type: string
7280
nullable: true
7381
licenseRef:
74-
description: LicenseRef is the secret used to load the inlets-client license, and is the same for each tunnel within the cluster
82+
description: |-
83+
LicenseRef is the secret used to load the inlets-client
84+
license, and is the same for each tunnel within the cluster
7585
type: object
7686
properties:
7787
name:
7888
type: string
7989
namespace:
8090
type: string
8191
nullable: true
92+
proxyProto:
93+
description: |-
94+
ProxyProto when set, is passed onto the tunnel server
95+
in order to have it send the original source IP.
96+
Note: any upstream must be able to read the Proxy Protocol header
97+
type: string
98+
nullable: true
8299
serviceRef:
83100
description: ServiceRef is the internal service to tunnel to the remote host
84101
type: object
@@ -112,7 +129,9 @@ spec:
112129
type: string
113130
nullable: true
114131
generated:
115-
description: Generated is set to true when the tunnel is created by the operator and false when a user creates the Tunnel via YAML
132+
description: |-
133+
Generated is set to true when the tunnel is created by the operator and false
134+
when a user creates the Tunnel via YAML
116135
type: boolean
117136
hostIP:
118137
type: string

chart/inlets-operator/values.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ resources:
5959
# Set a maximum memory limit for the inlets client Deployments
6060
maxClientMemory: 128Mi
6161

62+
# Enable proxy protocol on tunnel exit servers.
63+
# Allowed values: "v1", "v2", or "" (disabled)
64+
tunnelProxyProto: ""
65+
6266
nodeSelector: {}
6367
tolerations: []
6468
affinity: {}

config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@ type InfraConfig struct {
2424
AnnotatedOnly bool
2525
MaxClientMemory string
2626
Plan string
27-
ProConfig InletsProConfig
27+
TunnelConfig TunnelConfig
2828
}
2929

30-
type InletsProConfig struct {
30+
type TunnelConfig struct {
3131
License string
3232
LicenseFile string
3333
ClientImage string
3434
InletsRelease string
3535
}
3636

37-
func (c InletsProConfig) GetLicenseKey() (string, error) {
37+
func (c TunnelConfig) GetLicenseKey() (string, error) {
3838
val := ""
3939
if len(c.License) > 0 {
4040
val = c.License

config_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
func Test_GetLicenseKey_FromLiteral(t *testing.T) {
1010
want := "static.key.text"
1111

12-
c := InletsProConfig{
12+
c := TunnelConfig{
1313
License: want,
1414
}
1515

@@ -38,7 +38,7 @@ func Test_GetLicenseKey_FromFile(t *testing.T) {
3838
f.Close()
3939
defer os.Remove(name)
4040

41-
c := InletsProConfig{
41+
c := TunnelConfig{
4242
LicenseFile: name,
4343
}
4444

@@ -70,7 +70,7 @@ func Test_GetLicenseKey_FromFileTrimsWhitespace_JWT(t *testing.T) {
7070
f.Close()
7171
defer os.Remove(name)
7272

73-
c := InletsProConfig{
73+
c := TunnelConfig{
7474
LicenseFile: name,
7575
}
7676

@@ -88,7 +88,7 @@ func Test_GetLicenseKey_FromFileTrimsWhitespace_JWT(t *testing.T) {
8888
func Test_GetLicenseKey_FromLiteral_WithDashes(t *testing.T) {
8989
want := `static-dashes-key-text`
9090

91-
c := InletsProConfig{
91+
c := TunnelConfig{
9292
License: want,
9393
}
9494

controller.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import (
4949
const controllerAgentName = "inlets-operator"
5050
const inletsPROControlPort = 8123
5151
const inletsPortsAnnotation = "inlets.dev/ports"
52+
const proxyProtoAnnotation = "operator.inlets.dev/proxy-proto"
5253
const licenseSecretName = "inlets-license"
5354

5455
const (
@@ -384,8 +385,7 @@ func (c *Controller) syncHandler(key string) error {
384385
// No pre-created secret ref, and no generated secret name either
385386
// so create one.
386387
if getSecretName(tunnel) == "" {
387-
_, err = createTunnelAuthTokenSecret(tunnel, c)
388-
if err != nil {
388+
if _, err = createTunnelAuthTokenSecret(tunnel, c); err != nil {
389389
klog.Infof("Error creating tunnel auth token: %s", err)
390390
return fmt.Errorf("error creating tunnel auth token: %s", err)
391391
}
@@ -593,6 +593,13 @@ func createTunnelResource(service *corev1.Service, c *Controller) error {
593593
return nil
594594
}
595595

596+
var proxyProto string
597+
if v, ok := service.Annotations[proxyProtoAnnotation]; ok && v != "" && v != "v1" && v != "v2" {
598+
return fmt.Errorf("%s annotation must be 'v1', 'v2', or empty string, got: %s", proxyProtoAnnotation, v)
599+
} else {
600+
proxyProto = v
601+
}
602+
596603
klog.Infof("Creating Tunnel: %s.%s\n", name, namespace)
597604

598605
tunnel := &inletsv1alpha1.Tunnel{
@@ -602,6 +609,7 @@ func createTunnelResource(service *corev1.Service, c *Controller) error {
602609
Namespace: service.Namespace,
603610
},
604611
UpdateServiceIP: true,
612+
ProxyProto: proxyProto,
605613
},
606614
ObjectMeta: metav1.ObjectMeta{
607615
Name: name,
@@ -658,7 +666,7 @@ func createClientDeployment(tunnel *inletsv1alpha1.Tunnel, c *Controller) error
658666
return err
659667
}
660668

661-
licenseKey, _ := c.infraConfig.ProConfig.GetLicenseKey()
669+
licenseKey, _ := c.infraConfig.TunnelConfig.GetLicenseKey()
662670

663671
ports := getPortsString(service)
664672

@@ -721,7 +729,7 @@ func updateClientDeploymentRef(tunnel *inletsv1alpha1.Tunnel, c *Controller) err
721729
if deployment.ObjectMeta.Annotations != nil &&
722730
deployment.ObjectMeta.Annotations[inletsPortsAnnotation] != getPortsString(service) {
723731

724-
licenseKey, _ := c.infraConfig.ProConfig.GetLicenseKey()
732+
licenseKey, _ := c.infraConfig.TunnelConfig.GetLicenseKey()
725733

726734
ports := getPortsString(service)
727735
clientDeployment := makeClientDeployment(tunnel,
@@ -749,7 +757,12 @@ func getHostConfig(c *Controller, tunnel *inletsv1alpha1.Tunnel, service *corev1
749757
return provision.BasicHost{}, err
750758
}
751759

752-
userData := provision.MakeExitServerUserdata(tokenValue, inletsVersion)
760+
proxyProto := ""
761+
if v, ok := service.Annotations[proxyProtoAnnotation]; ok {
762+
proxyProto = v
763+
}
764+
765+
userData := makeExitServerUserdata(tokenValue, inletsVersion, proxyProto)
753766

754767
var host provision.BasicHost
755768

image_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import "testing"
55
func Test_GetInletsReleaseDefault(t *testing.T) {
66

77
c := InfraConfig{
8-
ProConfig: InletsProConfig{
8+
TunnelConfig: TunnelConfig{
99
License: "non-empty",
1010
},
1111
AccessKey: "key",
@@ -22,7 +22,7 @@ func Test_GetInletsReleaseDefault(t *testing.T) {
2222
func Test_GetInletsReleaseOverride(t *testing.T) {
2323

2424
c := InfraConfig{
25-
ProConfig: InletsProConfig{
25+
TunnelConfig: TunnelConfig{
2626
License: "non-empty",
2727
InletsRelease: "0.9.40",
2828
},
@@ -41,7 +41,7 @@ func Test_GetInletsReleaseOverride(t *testing.T) {
4141
func Test_InletsClientImageDefault(t *testing.T) {
4242

4343
c := InfraConfig{
44-
ProConfig: InletsProConfig{
44+
TunnelConfig: TunnelConfig{
4545
License: "non-empty",
4646
},
4747
AccessKey: "key",
@@ -57,7 +57,7 @@ func Test_InletsClientImageDefault(t *testing.T) {
5757
func Test_InletsClientImageOverride(t *testing.T) {
5858

5959
c := InfraConfig{
60-
ProConfig: InletsProConfig{
60+
TunnelConfig: TunnelConfig{
6161
License: "non-empty",
6262
ClientImage: "alexellis2/inlets-pro:0.9.40",
6363
},

main.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const defaultRelease = "0.9.40"
4242

4343
func main() {
4444
infra := &InfraConfig{
45-
ProConfig: InletsProConfig{},
45+
TunnelConfig: TunnelConfig{},
4646
}
4747

4848
flag.StringVar(&infra.Provider, "provider", "", "Your infrastructure provider - 'equinix-metal', 'digitalocean', 'scaleway', 'gce', 'linode', 'azure', 'ec2' or 'hetzner'")
@@ -57,10 +57,10 @@ func main() {
5757
flag.StringVar(&infra.VpcID, "vpc-id", "", "The VPC ID to create the exit-server in (ec2)")
5858
flag.StringVar(&infra.SubnetID, "subnet-id", "", "The Subnet ID where the exit-server should be placed (ec2)")
5959
flag.StringVar(&infra.ProjectID, "project-id", "", "The project ID if using equinix-metal, or gce as the provider")
60-
flag.StringVar(&infra.ProConfig.License, "license", "", "Supply a license for use with inlets-pro")
61-
flag.StringVar(&infra.ProConfig.LicenseFile, "license-file", "", "Supply a file to read for the inlets-pro license")
62-
flag.StringVar(&infra.ProConfig.ClientImage, "client-image", "ghcr.io/inlets/inlets-pro:"+defaultRelease, "Container image for inlets tunnel clients run in the cluster")
63-
flag.StringVar(&infra.ProConfig.InletsRelease, "inlets-release", defaultRelease, "Inlets version to use to create tunnel servers")
60+
flag.StringVar(&infra.TunnelConfig.License, "license", "", "Supply a license for use with inlets-pro")
61+
flag.StringVar(&infra.TunnelConfig.LicenseFile, "license-file", "", "Supply a file to read for the inlets-pro license")
62+
flag.StringVar(&infra.TunnelConfig.ClientImage, "client-image", "ghcr.io/inlets/inlets-pro:"+defaultRelease, "Container image for inlets tunnel clients run in the cluster")
63+
flag.StringVar(&infra.TunnelConfig.InletsRelease, "inlets-release", defaultRelease, "Inlets version to use to create tunnel servers")
6464

6565
flag.StringVar(&infra.MaxClientMemory, "max-client-memory", "128Mi", "Maximum memory limit for the tunnel clients")
6666

@@ -89,7 +89,7 @@ func main() {
8989
infra.GetInletsClientImage(),
9090
infra.GetInletsRelease())
9191

92-
if _, err := infra.ProConfig.GetLicenseKey(); err != nil {
92+
if _, err := infra.TunnelConfig.GetLicenseKey(); err != nil {
9393
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
9494
os.Exit(1)
9595
}
@@ -133,18 +133,18 @@ func main() {
133133

134134
// GetInletsClientImage returns the image for the client-side tunnel
135135
func (i *InfraConfig) GetInletsClientImage() string {
136-
if i.ProConfig.ClientImage == "" {
136+
if i.TunnelConfig.ClientImage == "" {
137137
return fmt.Sprintf("ghcr.io/inlets/inlets-pro:%s", defaultRelease)
138138
}
139-
return strings.TrimSpace(i.ProConfig.ClientImage)
139+
return strings.TrimSpace(i.TunnelConfig.ClientImage)
140140
}
141141

142142
func (i *InfraConfig) GetInletsRelease() string {
143-
if i.ProConfig.InletsRelease == "" {
143+
if i.TunnelConfig.InletsRelease == "" {
144144
return defaultRelease
145145
}
146146

147-
return strings.TrimSpace(i.ProConfig.InletsRelease)
147+
return strings.TrimSpace(i.TunnelConfig.InletsRelease)
148148
}
149149

150150
// GetAccessKey from parameter or file trimming

pkg/apis/inletsoperator/v1alpha1/types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ type TunnelSpec struct {
6363
// +nullable
6464
// +kubebuilder:validation:Optional
6565
UpdateServiceIP bool `json:"updateServiceIP,omitempty"`
66+
67+
// +nullable
68+
// +kubebuilder:validation:Optional
69+
// ProxyProto when set, is passed onto the tunnel server
70+
// in order to have it send the original source IP.
71+
// Note: any upstream must be able to read the Proxy Protocol header
72+
ProxyProto string `json:"proxyProto,omitempty"`
6673
}
6774

6875
// TunnelStatus is the status for a Tunnel resource

0 commit comments

Comments
 (0)