link

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 8, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package link implements the in-cluster gateway-link daemon: the cluster end of the WireGuard tunnel to the gateway VM. It dials out to the gateway, brings up wg0, and programs nftables to DNAT configured public ports arriving on wg0 to in-cluster Service ClusterIPs.

All configuration, including the gateway's peer endpoint, comes from the operator-rendered ConfigMap mounted at the config path. The daemon watches the ConfigMap and reloads in place on any change, reconciling wg0 and the nftables ruleset non-disruptively rather than restarting; it holds no cluster credentials.

The gateway is the public anchor and masquerades client traffic to its own wg0 address, so the link needs no connmark, policy routing, or rp_filter changes: replies route natively back out wg0.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Apply

func Apply(ctx context.Context, run runner, rc RuntimeConfig, privKey, peerPubKey string, resolve func(ctx context.Context, host string) (string, error)) error

Apply brings up the WireGuard tunnel and programs nftables from rc.

It mutates host network state (wg0 and the nftables ruleset) and must not be called concurrently; it relies on the single reconcile-goroutine caller.

It assumes a kube-proxy data plane (iptables or ipvs) so that masqueraded traffic to a Service ClusterIP is load-balanced to a backend pod. Cilium's kube-proxy replacement bypasses the ClusterIP DNAT that this design relies on and is out of scope for v1.

resolve maps a Service DNS name to a single address; pass a resolver built by newResolver in production. privKey and peerPubKey are base64 WireGuard keys read from mounted Secret files. The rendered config (which embeds the private key) is written to a 0600 temp file for the duration of the call and removed before return. A best-effort delete of any pre-existing wg0 runs first so Apply is idempotent across restarts; it is used for first-time setup, not for the live reload path, which is Reconcile. If a step after the interface is created fails, wg0 is torn down best-effort before returning, so a partial Apply never leaves a half-configured interface that a later Reconcile would patch instead of rebuilding.

func Reconcile

func Reconcile(ctx context.Context, run runner, rc RuntimeConfig, privKey, peerPubKey string, resolve func(ctx context.Context, host string) (string, error)) error

Reconcile applies rc onto an already-up wg0 without tearing the interface down: wg syncconf reconciles the peer endpoint and key against the running interface, and nft -f atomically swaps the ruleset. Unlike Apply it issues no ip link del or ip link add, so an established handshake and in-flight connections survive a config change. The caller chooses Apply versus Reconcile from wg0Exists.

Concurrency, resolve, and key handling match Apply: a single reconcile goroutine, and the rendered config (which embeds the private key) lives in a 0600 temp file only for the duration of the call.

func RenderNftables

func RenderNftables(forwards []ResolvedForward) (string, error)

RenderNftables renders the inet "gateway" table that DNATs configured public ports arriving on wg0 to in-cluster ClusterIPs and masquerades the forwarded traffic toward the cluster so backends reply to the link pod.

The document is self-replacing: it opens with `add table inet gateway` then `flush table inet gateway`, so a single `nft -f` ensures the table exists, empties it, and repopulates it in one atomic transaction. Re-applying the same or a changed ruleset is therefore idempotent and never accumulates stale rules.

The forward filter chain defaults to drop and accepts forwarded packets by their post-DNAT destination (ClusterIP and target port): the prerouting nat hook rewrites the destination before the forward hook runs, so matching the public port there would never hit. The established/related rule covers the return path. The output is deterministic: forwards are sorted by public port then protocol on a copy before rendering. It returns an error only if template execution fails.

func RenderWGConf

func RenderWGConf(rc RuntimeConfig, privKey, peerPubKey string) (string, error)

RenderWGConf renders a wg(8) setconf-compatible configuration for wg0. The output is suitable for `wg setconf`, which understands only [Interface] PrivateKey/ListenPort and Peer keys: Address and MTU are interface properties applied via ip(8) and are deliberately omitted here. ListenPort is emitted only when greater than zero. PersistentKeepalive is always emitted, including the value 0, so that `wg syncconf` clears a previously-set keepalive when the config drops it. privKey and peerPubKey are base64 WireGuard keys read from mounted Secret files, not from rc. It returns an error only if template execution fails.

func Run

func Run(ctx context.Context, cfg Config, log *zap.SugaredLogger) error

Run loads the runtime config and key material, then runs two concurrent loops until ctx is cancelled: a reload loop that applies the WireGuard and nftables configuration and re-applies it in place whenever the mounted ConfigMap changes, and the readiness HTTP server. Config load and key reads are fatal and returned before the loops start. The reload loop is non-fatal on transient load or apply failures: it logs and retries, so the process does not exit merely because the peer endpoint is not yet present in the config. The peer endpoint is supplied by the operator in the ConfigMap; the link holds no cluster credentials. The health server is drained gracefully on cancel.

Types

type Config

type Config struct {
	// ConfigPath is the on-disk path to the JSON RuntimeConfig. It lives inside a
	// whole-volume ConfigMap mount (the parent dir is watched), so the operator's
	// in-place config updates are picked up via fsnotify rather than a pod restart.
	ConfigPath string `envconfig:"GATEWAY_CONFIG_PATH" default:"/etc/gateway/config/config.json"`
	// WGKeyPath is the path to the WireGuard private key, kept out of the
	// RuntimeConfig so the key Secret and the config ConfigMap can be mounted
	// and rotated independently. It defaults to the "private" key of the link
	// Secret mounted at /etc/gateway/wg/.
	WGKeyPath string `envconfig:"GATEWAY_WG_KEY_PATH" default:"/etc/gateway/wg/private"`
	// PeerPubKeyPath is the path to the gateway's WireGuard public key. The key
	// is generated by the bootstrap Job, so it is delivered via the mounted link
	// Secret (the "peerPublicKey" key) rather than the Helm-templated ConfigMap.
	PeerPubKeyPath string `envconfig:"GATEWAY_WG_PEER_PUBKEY_PATH" default:"/etc/gateway/wg/peerPublicKey"`
	// HealthAddr is the listen address for the readiness HTTP server.
	HealthAddr string `envconfig:"GATEWAY_HEALTH_ADDR" default:":8080"`
	// ReconcileInterval is the safety-net interval for the reload loop. The link
	// reloads in place on every ConfigMap change via an fsnotify watch; this
	// ticker is purely a backstop that re-reads the config periodically in case a
	// filesystem event is missed. First-time endpoint pickup is handled by the
	// reload loop's initial synchronous apply, not by this ticker.
	ReconcileInterval time.Duration `envconfig:"GATEWAY_RECONCILE_INTERVAL" default:"10s"`
}

Config is the process-level configuration for gateway-link, populated from the environment via config.Load. The empty envconfig prefix means tags are read verbatim.

type Forward

type Forward struct {
	Name       string `json:"name"`
	PublicPort int    `json:"publicPort"`
	// Protocol is tcp or udp; it is lowercased during validation.
	Protocol   string `json:"protocol"`
	Service    string `json:"service"`
	TargetPort int    `json:"targetPort"`
}

Forward maps a public port arriving on wg0 to an in-cluster Service. Service is a DNS name resolved to a ClusterIP at apply time; a ConfigMap change re-resolves it, but the link does not watch for a ClusterIP change that leaves the config untouched.

type Peer

type Peer struct {
	// Endpoint is the gateway's public host:port for the WireGuard handshake. The
	// operator renders it into the ConfigMap once it observes the gateway address;
	// it is optional on disk because that observation may trail the link's start,
	// in which case the reload loop waits for the ConfigMap to gain it.
	Endpoint string `json:"endpoint"`
	// AllowedIPs is the set of source ranges accepted from and routed to the
	// peer, typically the wg0 subnet.
	AllowedIPs []string `json:"allowedIPs"`
	// PersistentKeepalive in seconds keeps the NAT pinhole open from the
	// link's side; 0 disables it.
	PersistentKeepalive int `json:"persistentKeepalive"`
}

Peer is the gateway endpoint the link connects to. The peer's public key is generated key material; it is read from Config.PeerPubKeyPath at apply time, not carried in this config.

type ResolvedForward

type ResolvedForward struct {
	Name       string
	PublicPort int
	Protocol   string
	ClusterIP  string
	TargetPort int
}

ResolvedForward is a Forward with its Service resolved to a concrete ClusterIP, ready to be rendered into nftables DNAT rules.

type RuntimeConfig

type RuntimeConfig struct {
	WireGuard WireGuard `json:"wireguard"`
	Forwards  []Forward `json:"forwards"`
}

RuntimeConfig is the on-disk JSON config describing the WireGuard tunnel and the port forwards the link programs into nftables. The WireGuard private key is deliberately absent; it is read separately from Config.WGKeyPath.

func LoadRuntimeConfig

func LoadRuntimeConfig(path string) (RuntimeConfig, error)

LoadRuntimeConfig reads and validates the JSON RuntimeConfig at path. Unknown fields are tolerated so older daemons can run against newer config schemas.

type WireGuard

type WireGuard struct {
	// Address is the wg0 address in CIDR form (e.g. 10.99.0.2/32).
	Address string `json:"address"`
	// ListenPort is the optional local UDP listen port; 0 lets WireGuard pick
	// an ephemeral port, which is the common case since the link dials out.
	ListenPort int `json:"listenPort"`
	// MTU is the optional wg0 MTU; 0 leaves the kernel default.
	MTU  int  `json:"mtu"`
	Peer Peer `json:"peer"`
}

WireGuard describes the local wg0 interface and the single gateway peer the link dials out to.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL