Skip to content

Commit a9792a2

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 dab3ae1 commit a9792a2

12 files changed

Lines changed: 226 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

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 (
@@ -396,8 +397,7 @@ func (c *Controller) syncHandler(key string) error {
396397
// No pre-created secret ref, and no generated secret name either
397398
// so create one.
398399
if getSecretName(tunnel) == "" {
399-
_, err = createTunnelAuthTokenSecret(tunnel, c)
400-
if err != nil {
400+
if _, err = createTunnelAuthTokenSecret(tunnel, c); err != nil {
401401
klog.Infof("Error creating tunnel auth token: %s", err)
402402
return fmt.Errorf("error creating tunnel auth token: %s", err)
403403
}
@@ -605,6 +605,13 @@ func createTunnelResource(service *corev1.Service, c *Controller) error {
605605
return nil
606606
}
607607

608+
var proxyProto string
609+
if v, ok := service.Annotations[proxyProtoAnnotation]; ok && v != "" && v != "v1" && v != "v2" {
610+
return fmt.Errorf("%s annotation must be 'v1', 'v2', or empty string, got: %s", proxyProtoAnnotation, v)
611+
} else {
612+
proxyProto = v
613+
}
614+
608615
klog.Infof("Creating Tunnel: %s.%s\n", name, namespace)
609616

610617
tunnel := &inletsv1alpha1.Tunnel{
@@ -614,6 +621,7 @@ func createTunnelResource(service *corev1.Service, c *Controller) error {
614621
Namespace: service.Namespace,
615622
},
616623
UpdateServiceIP: true,
624+
ProxyProto: proxyProto,
617625
},
618626
ObjectMeta: metav1.ObjectMeta{
619627
Name: name,
@@ -670,7 +678,7 @@ func createClientDeployment(tunnel *inletsv1alpha1.Tunnel, c *Controller) error
670678
return err
671679
}
672680

673-
licenseKey, _ := c.infraConfig.ProConfig.GetLicenseKey()
681+
licenseKey, _ := c.infraConfig.TunnelConfig.GetLicenseKey()
674682

675683
ports := getPortsString(service)
676684

@@ -733,7 +741,7 @@ func updateClientDeploymentRef(tunnel *inletsv1alpha1.Tunnel, c *Controller) err
733741
if deployment.ObjectMeta.Annotations != nil &&
734742
deployment.ObjectMeta.Annotations[inletsPortsAnnotation] != getPortsString(service) {
735743

736-
licenseKey, _ := c.infraConfig.ProConfig.GetLicenseKey()
744+
licenseKey, _ := c.infraConfig.TunnelConfig.GetLicenseKey()
737745

738746
ports := getPortsString(service)
739747
clientDeployment := makeClientDeployment(tunnel,
@@ -761,7 +769,12 @@ func getHostConfig(c *Controller, tunnel *inletsv1alpha1.Tunnel, service *corev1
761769
return provision.BasicHost{}, err
762770
}
763771

764-
userData := provision.MakeExitServerUserdata(tokenValue, inletsVersion)
772+
proxyProto := ""
773+
if v, ok := service.Annotations[proxyProtoAnnotation]; ok {
774+
proxyProto = v
775+
}
776+
777+
userData := makeExitServerUserdata(tokenValue, inletsVersion, proxyProto)
765778

766779
var host provision.BasicHost
767780

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

userdata.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) inlets Author(s) 2019. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
package main
5+
6+
import "fmt"
7+
8+
// makeExitServerUserdata makes a user-data script in bash to setup inlets
9+
//
10+
// with a systemd service and the given version. If proxyProto is non-empty,
11+
//
12+
// the PROXY_PROTO environment variable is set in the service configuration.
13+
func makeExitServerUserdata(authToken, version, proxyProto string) string {
14+
return fmt.Sprintf(`#!/bin/bash
15+
export AUTHTOKEN="%s"
16+
export IP=$(curl -sfSL https://checkip.amazonaws.com)
17+
export PROXY_PROTO="%s"
18+
19+
curl -SLsf https://github.com/inlets/inlets-pro/releases/download/%s/inlets-pro -o /tmp/inlets-pro && \
20+
chmod +x /tmp/inlets-pro && \
21+
mv /tmp/inlets-pro /usr/local/bin/inlets-pro
22+
23+
cat > /etc/systemd/system/inlets-pro.service <<EOF
24+
[Unit]
25+
Description=inlets TCP server
26+
After=network.target
27+
28+
[Service]
29+
Type=simple
30+
Restart=always
31+
RestartSec=2
32+
StartLimitInterval=0
33+
EnvironmentFile=/etc/default/inlets-pro
34+
ExecStart=/usr/local/bin/inlets-pro tcp server --auto-tls --auto-tls-san="\${IP}" --token="\${AUTHTOKEN}" --proxy-protocol="\${PROXY_PROTO}"
35+
36+
[Install]
37+
WantedBy=multi-user.target
38+
EOF
39+
40+
echo "AUTHTOKEN=$AUTHTOKEN" >> /etc/default/inlets-pro && \
41+
echo "IP=$IP" >> /etc/default/inlets-pro && \
42+
echo "PROXY_PROTO=$PROXY_PROTO" >> /etc/default/inlets-pro && \
43+
systemctl daemon-reload && \
44+
systemctl start inlets-pro && \
45+
systemctl enable inlets-pro
46+
`, authToken, proxyProto, version)
47+
}

0 commit comments

Comments
 (0)