Skip to content

Commit

Permalink
Implement NAT Gateway (#6)
Browse files Browse the repository at this point in the history
* add pod_watcher

Signed-off-by: walnuts1018 <[email protected]>

* add pod_watcher

Signed-off-by: walnuts1018 <[email protected]>

* add shouldHandle

Signed-off-by: walnuts1018 <[email protected]>

* [WIP] implementing pod watcher

Signed-off-by: gotti <[email protected]>

* implement pod watcher

Signed-off-by: walnuts1018 <[email protected]>

* change test dependencies

Signed-off-by: walnuts1018 <[email protected]>

* Implement pod_watcher

Signed-off-by: walnuts1018 <[email protected]>

* Implement pod_watcher_test

Signed-off-by: walnuts1018 <[email protected]>

* [WIP] implementing fou tunnel

Signed-off-by: gotti <[email protected]>

* [WIP] Addpeer

Signed-off-by: walnuts1018 <[email protected]>

* [WIP] writing addpeer

Signed-off-by: gotti <[email protected]>

* [WIP] delete unnecessary mutex

Signed-off-by: gotti <[email protected]>

* [WIP] setupDevice

Signed-off-by: walnuts1018 <[email protected]>

* [WIP] implement nat server

Signed-off-by: gotti <[email protected]>

* [WIP] implement nat.AddClient

Signed-off-by: walnuts1018 <[email protected]>

* fou.ConvNetIP -> netiputil.ConvNetIP

Signed-off-by: walnuts1018 <[email protected]>

* [WIP] writing fou tunnel

Signed-off-by: gotti <[email protected]>

* init pod_watcher

Signed-off-by: walnuts1018 <[email protected]>

* implement tests for pod_watcher

Signed-off-by: gotti <[email protected]>

* add dockerfiles and change Makefile to use them

Signed-off-by: gotti <[email protected]>

* change natpod image

Signed-off-by: walnuts1018 <[email protected]>

* [WIP] testing nat gateway

Signed-off-by: gotti <[email protected]>

* rename dummy device

Signed-off-by: walnuts1018 <[email protected]>

* rename nat pod to nat gateway

Signed-off-by: walnuts1018 <[email protected]>

* rename test image name

Signed-off-by: walnuts1018 <[email protected]>

* expect egressContainer.command == nil

Signed-off-by: walnuts1018 <[email protected]>

* change package structure

Signed-off-by: walnuts1018 <[email protected]>

* fix test failing

Signed-off-by: gotti <[email protected]>

---------

Signed-off-by: walnuts1018 <[email protected]>
Signed-off-by: gotti <[email protected]>
Co-authored-by: walnuts1018 <[email protected]>
  • Loading branch information
gotti and walnuts1018 authored Aug 27, 2024
1 parent 1b07b57 commit 90cd1ba
Show file tree
Hide file tree
Showing 24 changed files with 1,596 additions and 135 deletions.
33 changes: 17 additions & 16 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@ on:
pull_request:
push:
branches:
- 'main'
- "main"

jobs:
test:
strategy:
matrix:
go-version: [ 1.22 ]
platform: [ ubuntu-22.04 ]
go-version: [1.22]
platform: [ubuntu-22.04]
name: Small tests
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- run: make test
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- run: make check-generate
- run: make test
18 changes: 11 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Image URL to use all building/pushing image targets
IMG ?= controller:latest
IMG_TAG ?= dev
IMG_CONTROLLER ?= egress-controller:$(IMG_TAG)
IMG_GATEWAY ?= nat-gateway:$(IMG_TAG)
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.30.0

Expand Down Expand Up @@ -65,11 +67,11 @@ mod: ## Run go mod tidy against code.
go mod tidy

.PHONY: test
test: vet envtest check-generate ## Run tests.
test: envtest manifests generate fmt vet mod ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out

.PHONY: check-generate
check-generate: manifests generate fmt mod
check-generate: manifests generate fmt mod
git diff --exit-code --name-only

# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors.
Expand Down Expand Up @@ -100,11 +102,13 @@ run: manifests generate fmt vet mod ## Run a controller from your host.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
$(CONTAINER_TOOL) build -t ${IMG} .
$(CONTAINER_TOOL) build -t ${IMG_CONTROLLER} -f ./dockerfiles/Dockerfile.egress-controller .
$(CONTAINER_TOOL) build -t ${IMG_GATEWAY} -f ./dockerfiles/Dockerfile.nat-gateway .

.PHONY: docker-push
docker-push: ## Push docker image with the manager.
$(CONTAINER_TOOL) push ${IMG}
$(CONTAINER_TOOL) push ${IMG_CONTROLLER}
$(CONTAINER_TOOL) push ${IMG_GATEWAY}

# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
Expand All @@ -126,7 +130,7 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform
.PHONY: build-installer
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
mkdir -p dist
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
cd config/manager && $(KUSTOMIZE) edit set image egress-controller=${IMG_CONTROLLER}
$(KUSTOMIZE) build config/default > dist/install.yaml

##@ Deployment
Expand All @@ -145,7 +149,7 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified

.PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
cd config/manager && $(KUSTOMIZE) edit set image egress-controller=${IMG_CONTROLLER}
$(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -

.PHONY: undeploy
Expand Down
2 changes: 1 addition & 1 deletion api/v1beta1/egress_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type EgressSpec struct {
// +optional
SessionAffinityConfig *corev1.SessionAffinityConfig `json:"sessionAffinityConfig,omitempty"`

// PodDisruptionBudget is an optional PodDisruptionBudget for Egress NAT pods.
// PodDisruptionBudget is an optional PodDisruptionBudget for Egress NAT Gateways.
// +optional
PodDisruptionBudget *EgressPDBSpec `json:"podDisruptionBudget,omitempty"`
}
Expand Down
17 changes: 13 additions & 4 deletions cmd/egress-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"crypto/tls"
"flag"
"fmt"
"os"

// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
Expand Down Expand Up @@ -37,7 +38,8 @@ func init() {
}

type Config struct {
FoUPort int
FoUPort int
NatGatewayImage string
}

func main() {
Expand All @@ -61,13 +63,19 @@ func main() {
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
flag.IntVar(&config.FoUPort, "fou-port", 5555, "port number for foo-over-udp tunnels")
flag.StringVar(&config.NatGatewayImage, "natgateway-image", "", "default image name for nat-gateway pods")

opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()

if config.NatGatewayImage == "" {
setupLog.Error(fmt.Errorf("--natgateway-image is required"), "failed to parse flags")
os.Exit(1)
}

ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

// if the enable-http2 flag is false (the default), http/2 should be disabled
Expand Down Expand Up @@ -138,9 +146,10 @@ func main() {
}

if err = (&controller.EgressReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Port: int32(config.FoUPort),
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Port: int32(config.FoUPort),
DefaultImage: config.NatGatewayImage,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Egress")
os.Exit(1)
Expand Down
225 changes: 225 additions & 0 deletions cmd/nat-gateway/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package main

import (
"crypto/tls"
"errors"
"flag"
"net/netip"
"os"
"strings"

// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"

"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"

ponav1beta1 "github.com/cybozu-go/pona/api/v1beta1"
"github.com/cybozu-go/pona/internal/controller"
"github.com/cybozu-go/pona/internal/nat"
"github.com/cybozu-go/pona/internal/tunnel/fou"
// +kubebuilder:scaffold:imports
)

var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)

func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))

utilruntime.Must(ponav1beta1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}

type Config struct {
FoUPort int
}

func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)

var config Config

flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.BoolVar(&secureMetrics, "metrics-secure", true,
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
flag.IntVar(&config.FoUPort, "fou-port", 5555, "port number for foo-over-udp tunnels")

opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()

ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
// Rapid Reset CVEs. For more information see:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("disabling http/2")
c.NextProtos = []string{"http/1.1"}
}

if !enableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}

webhookServer := webhook.NewServer(webhook.Options{
TLSOpts: tlsOpts,
})

// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
// More info:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
// TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are
// not provided, self-signed certificates will be generated by default. This option is not recommended for
// production environments as self-signed certificates do not offer the same level of trust and security
// as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing
// unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName
// to provide certificates, ensuring the server communicates using trusted and secure certificates.
TLSOpts: tlsOpts,
}

if secureMetrics {
// FilterProvider is used to protect the metrics endpoint with authn/authz.
// These configurations ensure that only authorized users and service accounts
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
// https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/metrics/filters#WithAuthenticationAndAuthorization
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "7e4aaa0a.pona.cybozu.com",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}

myNS := os.Getenv(controller.EnvPodNamespace)
if myNS == "" {
setupLog.Error(errors.New(controller.EnvPodNamespace+" environment variable must be set"), "unable to get env")
os.Exit(1)
}

myName := os.Getenv(controller.EnvEgressName)
if myName == "" {
setupLog.Error(errors.New(controller.EnvEgressName+" environment variable must be set"), "unable to get env")
os.Exit(1)
}

myAddresses := strings.Split(os.Getenv(controller.EnvAddresses), ",")
if len(myAddresses) == 0 {
setupLog.Error(errors.New(controller.EnvAddresses+" environment variable must be set"), "unable to get env")
os.Exit(1)
}

var ipv4, ipv6 *netip.Addr
for _, addr := range myAddresses {
n, err := netip.ParseAddr(addr)
if err != nil {
setupLog.Error(errors.New(controller.EnvAddresses+" contains invalid address"), "unable to parse address",
"address", addr,
)
os.Exit(1)
}
if n.Is4() {
ipv4 = &n
} else {
ipv6 = &n
}
}

fc, err := fou.NewFoUTunnelController(config.FoUPort, ipv4, ipv6)
if err != nil {
setupLog.Error(err, "unable to create FouTunnelController")
os.Exit(1)
}
if err := fc.Init(); err != nil {
setupLog.Error(err, "failed to Initialize FoUTunnelController")
os.Exit(1)
}
nc, err := nat.NewController("eth0", ipv4, ipv6)
if err != nil {
setupLog.Error(err, "unable to create nat.Controller")
os.Exit(1)
}
if err := nc.Init(); err != nil {
setupLog.Error(err, "failed to Initialize nat.Controller")
os.Exit(1)
}

if err = controller.NewPodWatcher(
mgr.GetClient(),
mgr.GetScheme(),
myName,
myNS,
fc,
nc,
).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Pod")
os.Exit(1)
}
// +kubebuilder:scaffold:builder

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}

setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
2 changes: 1 addition & 1 deletion config/crd/bases/pona.cybozu.com_egresses.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ spec:
minItems: 1
type: array
podDisruptionBudget:
description: PodDisruptionBudget is an optional PodDisruptionBudget for Egress NAT pods.
description: PodDisruptionBudget is an optional PodDisruptionBudget for Egress NAT Gateways.
properties:
maxUnavailable:
anyOf:
Expand Down
Loading

0 comments on commit 90cd1ba

Please sign in to comment.