UnDyingProxy redirects TCP and UDP traffic from a single external IP to internal Kubernetes services across namespaces, significantly reducing cloud load balancer costs and simplifying ingress.
Cloud load balancers are powerful but can become expensive, especially when you need to expose numerous services (like game servers, databases, or IoT endpoints) each in it's own namespace and requiring its own port. UnDyingProxy offers a cost-effective alternative by allowing you to:
- Multiplex Ports: Forward traffic from many source ports on a single external IP to different internal services and ports.
- Reduce Cloud Costs: Pay for potentially only one cloud load balancer (or even none!) instead of many.
- Simplify Ingress: Manage TCP/UDP forwarding declaratively using Kubernetes custom resources.
- Achieve High Availability: Run multiple replicas of the proxy for resilience.
Each UnDyingProxy
object specifies one listen port and one target host and port to forward to each for TCP and UDP.
This supports only paying for one Loadbalancer, or even avoiding a cloud Loadbalancer altogether by running as a DaemonSet on nodes with direct public IPs. If using a cloud Loadbalancer, this operator can automatically add/remove Ports
in a Service
object as UnDyingProxy
objects are created/destroyed, to open/close the ports in the Loadbalancer.
- TCP & UDP Forwarding: Configure forwarding rules for both protocols independently per
UnDyingProxy
resource. - Dynamic Configuration: Define forwarding rules using the
UnDyingProxy
Custom Resource Definition (CRD). Changes are automatically detected and applied. - Automatic Service Port Management (Optional): Automatically update Kubernetes
Service
ports (e.g., for LoadBalancer Services) asUnDyingProxy
resources are created/deleted. - High Availability: Designed to run with multiple replicas behind a Kubernetes Service; leader election is disabled.
- Prometheus Metrics: Exposes detailed metrics for monitoring traffic and operator health.
- Configurable Timeouts & Buffers: Fine-tune UDP connection handling.
- Forward both UDP and TCP from external port
27015
to the same port on an internal Kubernetes Service in a different namespace.
---
apiVersion: proxy.sfact.io/v1alpha1
kind: UnDyingProxy
metadata:
name: game-server-1001
namespace: undying-proxy # Must match operator's namespace
spec:
udp:
listenPort: 27015 # External port on the proxy's IP
targetPort: 27015 # Internal port of the game server pod/service
targetHost: my-game-server-1001.game-ns.svc.cluster.local
tcp:
listenPort: 27015
targetPort: 27015
targetHost: my-game-server-1001.game-ns.svc.cluster.local
- UnDyingProxy can also be run as a DaemonSet along with round robin DNS to point a domain to your node IPs, and potentially avoid a cloud Loadbalancer altogether. This requires clients to connect directly to node IPs.
UnDyingProxy listens for TCP and UDP connections on designated ports defined in UnDyingProxy
custom resources, and forwards packets to the specified destination addresses and ports. This operator works like the NGINX Ingress Controller in that it does the actual forwarding itself, rather than operating external resources. Therefore, it should be run as multiple replicas to provide high availability.
The --operator-namespace
flag must be set, as this is a namespaced operator. The operator will only watch for UnDyingProxy
objects in the namespace specified by this flag.
Run one set of replicas of this operator per IP you need to listen upon.
Use config/manager
as example configs to customize your deployment.
The operator watches for UnDyingProxy objects in the namespace specified by the --operator-namespace
flag. When an UnDyingProxy object is created, the operator will start new goroutines listening on the specified ports for TCP and UDP traffic. When an UnDyingProxy object is deleted, the operator will shutdown those goroutines, and stop listening on the specified ports, and optionally edit Kubernetes Service objects to control cloud load balancers.
graph LR
classDef k8sComponent fill:#555,color:#fff,stroke:#aaa,stroke-width:1px;
classDef proxyComponent fill:#444,color:#fff,stroke:#aaa,stroke-width:1px;
classDef external fill:#666,color:#fff,stroke:#aaa,stroke-width:1px;
subgraph "External"
ClientTCP("TCP Client"):::external
ClientUDP("UDP Client"):::external
end
Ingress("Load Balancer or other Ingress"):::external
subgraph "Kubernetes Cluster"
direction TB
K8sAPI("Kubernetes API Server"):::k8sComponent
ManagedSvc("Kubernetes Service"):::k8sComponent
subgraph "Controller Pods"
direction TB
Mgr("Controller Manager (main.go)"):::proxyComponent --> R{"UnDyingProxyReconciler"}:::proxyComponent
R -- "Creates/Updates/Deletes" --> TF("TCP Forwarder (tcp_handler.go)"):::proxyComponent
R -- "Creates/Updates/Deletes" --> UF("UDP Forwarder (udp_handler.go)"):::proxyComponent
R -- "Adds/Removes Ports" --> SM("Service Manager (service_handler.go)"):::proxyComponent
end
TargetTCP("Target TCP Service"):::k8sComponent
TargetUDP("Target UDP Service"):::k8sComponent
UDPCR("UnDyingProxy CR"):::k8sComponent
R -- Watches --> K8sAPI
K8sAPI -- Notifies --> R
K8sAPI -- CRUD --> UDPCR
R -- Reads --> UDPCR
SM -- "Gets/Patches" --> ManagedSvc
ManagedSvc -.-> TF
TF --> TargetTCP
ManagedSvc -.-> UF
UF --> TargetUDP
end
ClientTCP --> Ingress
ClientUDP --> Ingress
Ingress --> ManagedSvc
linkStyle 9 stroke:#3399ff,stroke-width:2px
linkStyle 10 stroke:#3399ff,stroke-width:2px
linkStyle 11 stroke:#3399ff,stroke-width:2px
linkStyle 12 stroke:#3399ff,stroke-width:2px
linkStyle 13 stroke:#3399ff,stroke-width:2px
linkStyle 14 stroke:#3399ff,stroke-width:2px
linkStyle 15 stroke:#3399ff,stroke-width:2px
When running multiple replicas, the operator pods sit behind the Kubernetes Service
resources defined in config/manager/service.undying-proxy-tcp.yaml
and config/manager/service.undying-proxy-udp.yaml
. These services are typically of type LoadBalancer
. The cloud provider's load balancer distributes incoming traffic across the available operator pods. All operator replicas watch for UnDyingProxy
objects in the configured --operator-namespace
and maintain their own listeners independently.
To facilitate integration with cloud load balancers, the operator can optionally manage the ports
section of designated Kubernetes Service
objects. This is controlled by the --tcp-service-to-manage
and --udp-service-to-manage
flags.
When an UnDyingProxy
object is created with a TCP or UDP listener, if the corresponding flag is set, the operator will add (or update) a ServicePort
entry in the specified Service. The entry's name
will match the UnDyingProxy
object's name, and the port
and targetPort
will match the listenPort
from the UnDyingProxy
. When the UnDyingProxy
is deleted, the corresponding ServicePort
entry is removed.
Prerequisites:
- The
Service
object specified by the flag must already exist in the same namespace as the operator. - The operator needs RBAC permissions to
get
,patch
, andupdate
Services
in its namespace (these are included in the default manifests).
GitOps Considerations:
If you enable this feature and use a GitOps tool like Argo CD or Flux to manage the Service
object, you must configure the tool to ignore differences in the .spec.ports
field of the managed Service(s). Otherwise, the GitOps tool will continuously revert the changes made by the operator.
The operator is configured primarily through the UnDyingProxy
Custom Resource Definition (CRD).
Each UnDyingProxy
object can define TCP and/or UDP forwarders via the .spec
field:
spec.tcp
: Defines configuration for a TCP forwarder.listenPort
(Required, integer): The port the operator will listen on for incoming TCP connections.targetPort
(Required, integer): The destination port on the target host to forward TCP connections to.targetHost
(Required, string): The destination hostname (DNS resolvable) or IP address to forward TCP connections to.
spec.udp
: Defines configuration for a UDP forwarder.listenPort
(Required, integer): The port the operator will listen on for incoming UDP packets.targetPort
(Required, integer): The destination port on the target host to forward UDP packets to.targetHost
(Required, string): The destination hostname (DNS resolvable) or IP address to forward UDP packets to.readTimeoutSeconds
(Optional, integer): The timeout in seconds for reading UDP packets from both the client and the target connection. Defaults to30
seconds.writeTimeoutSeconds
(Optional, integer): The timeout in seconds for writing UDP packets to both the client and the target connection. Defaults to5
seconds.
Note: Currently, changes to the spec
of an existing UnDyingProxy
object after creation are not fully supported and may require deleting and recreating the object. // TODO: Add this note or remove when mutability is supported
The operator's behavior can be further customized via command-line flags passed to the manager binary:
--operator-namespace
(Required, string): The Kubernetes namespace the operator should watch forUnDyingProxy
objects. The operator is restricted to this single namespace.--tcp-service-to-manage
(Optional, string, default:undying-proxy-tcp
): The name of a pre-existing KubernetesService
object (in the same namespace as the operator) whoseports
section will be managed for TCPlistenPort
s defined inUnDyingProxy
objects. Set to""
to disable this feature.--udp-service-to-manage
(Optional, string, default:undying-proxy-udp
): The name of a pre-existing KubernetesService
object (in the same namespace as the operator) whoseports
section will be managed for UDPlistenPort
s defined inUnDyingProxy
objects. Set to""
to disable this feature.--udp-buffer-bytes
(Optional, integer, default:1024
): The size of the buffer in bytes used for reading and writing UDP packets.--metrics-bind-address
(Optional, string, default::8080
): The address the metrics endpoint binds to. Use:8443
for HTTPS,:8080
for HTTP. See also--metrics-secure
.--metrics-secure
(Optional, boolean, default:false
): Iftrue
, serve metrics via HTTPS. Requires certificates (can be self-signed).--metrics-authed
(Optional, boolean, default:false
): Iftrue
, protect the metrics endpoint with Kubernetes token review and subject access review. Requires corresponding RBAC setup. See the kubebuilder project for more information.--health-probe-bind-address
(Optional, string, default::8081
): The address the health probe endpoints (/healthz
,/readyz
) bind to.--zap-devel
(Optional, boolean, default:false
): Enable development mode logging (more verbose, human-readable).--zap-stacktrace-level
(Optional, string, default:panic
): The minimum log level (info
,error
,panic
) at which to include stack traces.
The operator exposes Prometheus metrics for monitoring its status and the traffic being forwarded.
Metrics scraping is enabled via the manager flags:
--metrics-bind-address
: Set to a non-zero value like:8080
(HTTP) or:8443
(HTTPS).--metrics-secure
: Usetrue
for HTTPS.--metrics-authed
: Usetrue
to require authentication/authorization.
See the Manager Flags
section above for more details on these flags.
The config/prometheus
directory contains an example ServiceMonitor
resource for Prometheus Operator integration. Note that the default ServiceMonitor configuration assumes metrics are served over HTTPS (:8443
) and may require adjustments based on your chosen flags and security configuration (e.g., setting insecureSkipVerify: true
for self-signed certificates, or providing CA details).
undying_proxy_tcp_forwarders
(Gauge): Number of TCP forwarders currently running.undying_proxy_tcp_from_clients_to_target_bytes_total
(CounterVec): Total TCP bytes forwarded from clients to the target, labeled byforwarder
(UnDyingProxy name).undying_proxy_tcp_from_target_to_clients_bytes_total
(CounterVec): Total TCP bytes forwarded from the target back to clients, labeled byforwarder
.undying_proxy_tcp_forwarding_errors_total
(CounterVec): Total TCP networking-related errors, labeled bytype
(e.g.,Accept
,DialServerTarget
,CopyClientToTarget
,CopyTargetToClient
).undying_proxy_udp_forwarders
(Gauge): Number of UDP forwarders currently running.undying_proxy_udp_clients_connected
(GaugeVec): Number of UDP clients currently connected, labeled byforwarder
.undying_proxy_udp_from_clients_to_target_bytes_total
(CounterVec): Total UDP bytes forwarded from clients to the target, labeled byforwarder
.undying_proxy_udp_from_target_to_clients_bytes_total
(CounterVec): Total UDP bytes forwarded from the target back to clients, labeled byforwarder
.undying_proxy_udp_forwarding_errors_total
(CounterVec): Total UDP networking-related errors, labeled bytype
(e.g.,ResolveListenAddress
,ResolveTargetAddress
,Listen
,DialUDPTarget
,WriteToTarget
,ReadFromClient
,ReadFromTarget
,WriteToClient
).undying_proxy_operator_errors_total
(CounterVec): Total internal operator errors, labeled bytype
(e.g.,FailedSetupTCPForwarder
,FailedSetupUDPForwarder
,FailedFetch
,FailedUpdateStatus
,FailedRemoveFinalizer
,FailedAddFinalizer
,FailedServiceCleanup
,FailedManageTCPService
,FailedManageUDPService
).
- Support one operator for UnDyingProxies from many namespaces.
- Support an annotation to set which instance of the operator will handle a given UnDyingProxy, which will allow for more than one operator in the same namespace.
- Go version v1.22.0+
- Docker version 17.03+.
- kubectl version v1.11.3+.
- Access to a Kubernetes v1.11.3+ cluster.
Build and push an image somewhere:
make docker-build docker-push IMG=<some-registry>/undying-proxy:tag
Install the CRDs into the cluster:
make install
Deploy the Manager to the cluster with the image specified by IMG
:
make deploy IMG=<some-registry>/undying-proxy:tag
Create instances of your solution: You can apply the samples (examples) from the config/sample:
kubectl apply -k config/samples/
Delete the instances (CRs) from the cluster:
kubectl delete -k config/samples/
Delete the APIs (CRDs) from the cluster:
make uninstall
Undeploy the controller from the cluster:
make undeploy
Contributions are welcome! Please create an issue first to discuss the proposed changes.
When submitting a pull request, please ensure you have:
- Run
make lint
and addressed any linting errors. - Run
make manifests
to ensure generated code and manifests are up to date. - Run
make test
andmake test-e2e
to ensure tests pass.
NOTE: Run make help
for more information on all potential make
targets.
More information can be found via the Kubebuilder Documentation.
Note the following test commands:
make test
- Runs the unit tests.
make test-e2e
- Runs the end to end tests against an existing kubectl context named
kind-undying-proxy-test
- Tests are destructive. A check ensures you are connected to the correct context.
- An easy way to create the cluster and context is with kind:
kind create cluster --name undying-proxy-test
- Runs the end to end tests against an existing kubectl context named
Copyright 2024 david amick.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.