Multi-cloud networking for Kubernetes, simplified.
Galactic connects Kubernetes workloads across multiple clouds and regions as if they were on a single, unified network. It provides secure, isolated Virtual Private Clouds (VPCs) that span cloud boundaries—without vendor lock-in or complex configuration.
Modern organizations run workloads everywhere: AWS, Azure, GCP, on-premises, and edge locations. Each environment brings its own networking model, APIs, and constraints. The result is fragmented networks, operational complexity, and cloud provider lock-in.
Galactic provides the SRv6 data plane that makes multi-cloud VPC connectivity work at the kernel level. It runs as a DaemonSet agent on every node, managing SRv6 routes and VRF isolation, and as a CNI plugin that attaches pods to the correct virtual network. On nodes dedicated to the gateway role, a third component loads an edge XDP NAT+LB datapath to handle ingress load balancing at the VPC boundary. VPC and VPCAttachment definitions are managed by a companion operator; Galactic acts on the identifiers that operator assigns.
Under the hood, Galactic uses Segment Routing over IPv6 (SRv6) for efficient, deterministic routing and Virtual Routing and Forwarding (VRF) for true network isolation at the kernel level. BGP is used to distribute SRv6 routes between agents across nodes and clusters.
For Developers — Attach to a VPC with a single annotation. No networking code, no cloud-specific APIs.
For Platform Teams — Manage multi-cloud networking from Kubernetes using GitOps workflows and standard tooling.
For Organizations — Move workloads between providers without network redesign. One networking model instead of N cloud-specific implementations.
A ContainerLab environment is available under deploy/containerlab/:
deploy/containerlab/— Three Kind clusters wired over an SRv6 transit mesh. The full GVPC multi-cluster environment with FRR underlay and GoBGP L3VPN overlay.
See the galactic DevContainer for development environment setup. On ARM64 / OrbStack, use the containerlab DevContainer to run ContainerLab via Docker-out-of-Docker.
Manifests for a real cluster live under config/, composed with Kustomize. One command deploys the galactic-system namespace (labeled pod-security.kubernetes.io/enforce: privileged — every DaemonSet here needs it, for hostPath volumes, hostNetwork, and elevated capabilities), the galactic-cni DaemonSet, and both galactic-router roles — the default per-node role (galactic-router, runs everywhere except control-plane nodes) and rr (galactic-router-rr, the BGP route reflector, opt-in — stays at zero replicas until nodes are labeled galactic.datumapis.com/galactic-route-reflector: "true"):
kubectl apply -k config/Each component can also be applied on its own, e.g. kubectl apply -k config/galactic-router for just the router (both roles) or kubectl apply -k config/galactic-router/overlays/default for just the default per-node role.
Two components are not part of kubectl apply -k config/ and must be applied separately, each with its own per-node prerequisite:
config/fabric-router/— the FRR underlay eBGP DaemonSetgalactic-routerdepends on.config/galactic-gateway/— the edge XDP NAT+LB gateway control plane (galactic-router+galactic-gatewayrunning together on dedicatedgalactic.datumapis.com/node: edgenodes).kubectl apply -k config/galactic-gateway/only installs the shared, cluster-safe ServiceAccount/RBAC;config/galactic-gateway/base/itself is a template meant to be instantiated once per gateway node by a further overlay (seedeploy/containerlab/resources/galactic-gateway/for a worked example) — apply that overlay per node instead ofbase/directly.
kubectl apply -k config/fabric-router/
kubectl apply -k config/galactic-gateway/-
Container images.
.github/workflows/publish.yamlbuildsghcr.io/datum-cloud/galactic-cni,ghcr.io/datum-cloud/galactic-router,ghcr.io/datum-cloud/galactic-gateway, andghcr.io/datum-cloud/fabric-router(fromcontainers/galactic-cni/Dockerfile,containers/galactic-router/Dockerfile,containers/galactic-gateway/Dockerfile, andcontainers/fabric-router/Dockerfilerespectively) on every push and release — but it never publishes a:latesttag, only date-stamped tags per push/release (e.g.v0.0.0-main-20260713-170924) and, for tagged releases, semver tags. Theimage:references committed inconfig/galactic-cni/daemonset.yaml,config/galactic-router/base/daemonset.yaml,config/galactic-gateway/base/daemonset.yaml, andconfig/fabric-router/daemonset.yamlsay:latestonly as a placeholder that CI substitutes with a real published tag when it builds theghcr.io/datum-cloud/galactic-kustomizeOCI Kustomize bundle — that substitution never happens in the git checkout itself. Applyingconfig/directly from a clone will therefore fail to pull:latest. Before applying, resolve the current tag (check the package pages or the latest successful run ofpublish.yamlonmain) and pin it, e.g.:cd config/galactic-cni && kustomize edit set image ghcr.io/datum-cloud/galactic-cni=ghcr.io/datum-cloud/galactic-cni:<resolved-tag> cd config/galactic-router/base && kustomize edit set image ghcr.io/datum-cloud/galactic-router=ghcr.io/datum-cloud/galactic-router:<resolved-tag> cd config/galactic-gateway/base && kustomize edit set image ghcr.io/datum-cloud/galactic-router=ghcr.io/datum-cloud/galactic-router:<resolved-tag> && kustomize edit set image ghcr.io/datum-cloud/galactic-gateway=ghcr.io/datum-cloud/galactic-gateway:<resolved-tag> cd config/fabric-router && kustomize edit set image ghcr.io/datum-cloud/fabric-router=ghcr.io/datum-cloud/fabric-router:<resolved-tag>
-
config/fabric-router/: per-nodefrr.conf. Unlike every other component underconfig/,config/fabric-router/daemonset.yamlhas no generic default config — the underlay eBGP session (interface addresses, remote-AS, etc.) is different for every physical node, and this DaemonSet'snodeAffinity(galactic.datumapis.com/fabricExists) can legitimately match more than one node per cluster. Before applyingconfig/fabric-router/, create afabric-configConfigMap in thegalactic-systemnamespace with onefrr.conf.<nodename>key per matching node (<nodename>is the Kubernetes node name, e.g.frr.conf.worker-1) —frr-initpicks the right key at pod start via the pod'sNODE_NAMEdownward-API env var. The other two files FRR needs,daemonsandvtysh.conf, are already baked into thefabric-routerimage (seecontainers/fabric-router/Dockerfile); include them in the ConfigMap too only if you need to override the image defaults.deploy/containerlab/resources/fabric/{dfw,iad,sjc}/frr.confare worked examples from the lab, not something you can apply as-is. -
config/fabric-router/andconfig/galactic-router/: rolling out updates is manual. Bothconfig/fabric-router/daemonset.yamlandconfig/galactic-router/base/daemonset.yamluseupdateStrategy: OnDelete— akubectl apply(new image tag, or a spec change) will not restart any pod on its own. This is deliberate for both: each is a BGP speaker whose liveness/health probe only reflects "the process is up," not "the BGP session has reconverged," soRollingUpdatewould advance to the next node on exactly the wrong signal — see the comment aboveupdateStrategyin each manifest for the full reasoning. To actually roll out a change, delete pods one at a time and confirm the new pod's session(s) have reconverged before moving to the next node, e.g. forfabric-router:kubectl -n galactic-system get pods -l app.kubernetes.io/name=fabric-router -o wide kubectl -n galactic-system delete pod <fabric-router-pod-on-one-node> # wait for the new pod to be Ready, then confirm on that node: kubectl -n galactic-system exec <new-pod> -- vtysh -c "show bgp summary" # repeat for the next node only once the above shows the session Established
A ConfigMap edit to
fabric-configbehaves the same way today regardless ofupdateStrategy— there's no checksum annotation wiring pod restarts to ConfigMap changes, so a config change also requires this same manual, node-by-node pod bounce to take effect. -
config/galactic-router/: rollout order —rrlast, one compute node at a time. Forrr, every compute node's iBGP session pivots through that one pod, so bouncing it is a fleet-wide route flap, not a single-node one — roll it only after all compute nodes are already on the new version, and confirm every client has re-peered before considering the rollout done. For the default role, a bounce only withdraws that one node's own advertised prefixes, so it's safe to go node-by-node as withfabric-router. Check session state via theBGPPeerCRD'sSTATEcolumn rather thanvtysh(there's novtyshin this binary —galactic-routerreports session state itself):kubectl -n galactic-system get pods -l app.kubernetes.io/name=galactic-router -o wide kubectl -n galactic-system delete pod <galactic-router-pod-on-one-node> # wait for the new pod to be Ready, then confirm its BGPPeer CRDs are back to STATE=Established kubectl get bgppeer # repeat for the next compute node, then only last roll galactic-router-rr the same way
-
config/galactic-gateway/: per-node public interface and SRv6 address.config/galactic-gateway/base/daemonset.yaml'sgalactic-gatewaycontainer requiresGALACTIC_GATEWAY_PUBLIC_INTERFACEandGALACTIC_GATEWAY_SRV6_ADDRESS— the latter must be unique per gateway node and has no generic default (there's no in-cluster mechanism yet that derives it automatically; seepublishSelfAddress's doc comment ininternal/controller/networkgateway_controller.go). Applyingbase/as shipped, without pinning both per node, produces a crash-looping container. Instantiatebase/via a further overlay that pins it to one node (kubernetes.io/hostname) and sets that node's values — seedeploy/containerlab/resources/galactic-gateway/for a worked two-node example. -
Talos: gRPC health port.
galactic-routerrunshostNetwork: trueand defaults to gRPC health checks on port5000, which collides with Talos's built-in dashboard (/sbin/dashboardpermanently binds127.0.0.1:5000on every Talos node).config/galactic-router/base/daemonset.yamlalready ships withGALACTIC_ROUTER_GRPC_HEALTH_PORT=5179(and matching probe/containerPort) to avoid this; if you rungalactic-routeroutside these manifests on Talos, setGALACTIC_ROUTER_GRPC_HEALTH_PORTto something other than5000yourself. -
galactic-router: BGP local address. The node needs a global-unicast IPv6 address assigned tolo(typically byconfig/fabric-router/'s underlay eBGP daemon, which must start and converge beforegalactic-router), or you must setGALACTIC_ROUTER_BGP_LOCAL_ADDRESSexplicitly — this is required even whenGALACTIC_ROUTER_BGP_LISTEN_PORT=-1(no inbound listener), sincegalactic-routerstill needs a source address for outbound BGP connections. Without one of these, startup fails withGALACTIC_ROUTER_BGP_LOCAL_ADDRESS not set and no address could be detected on lo: no global-unicast IPv6 address found on lo. Seedocs/router/configuration.mdfor details.
See docs/router/configuration.md for the full galactic-router CLI flag / environment variable reference — env var names follow GALACTIC_ROUTER_<FLAG_NAME> (hyphens become underscores, uppercased); the reference table has the exact name for every flag.
This project uses Task as its build tool. Build, test, and lint operations are defined in the root Taskfile.yaml.
# macOS
brew install go-task
# Linux (official installer)
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
# Go toolchain
go install github.com/go-task/task/v3/cmd/task@latestSee taskfile.dev/installation for the full list of options.
task # list available taskstask build # produces bin/{galactic-cni,galactic-veth,galactic-tap,galactic-ipam,
# galactic-bgp,galactic-route,galactic-router,galactic-gateway}
task lint # golangci-lint + yamlfmt; lint-fix applies safe auto-fixes
task ci # full pipeline: lint → build → test:unit → test:e2eThere is no task docker-build — the shared production Dockerfile and release
workflow were removed (see Production Deployment above). containers/galactic-cni/Dockerfile
exists only to support task test:e2e below.
task test # run unit tests then e2e tests (requires Docker + Kind)
task test:unit # unit tests only — race detector, coverage output
task test:e2e # full e2e lifecycle — spins up a Kind cluster, builds and
# loads the image, then tears the cluster down on exittask test:unit is the fast path for development; it runs the same command as the CI test-unit job. task test:e2e requires Docker and Kind and mirrors the CI test-e2e job exactly, including automatic cluster cleanup via a trap on exit.
Run task ci before opening a pull request.
The lab environment has its own Taskfile.yaml; run task from deploy/containerlab/ to see available tasks.
See AGENTS.md for the contributor guide (development workflow, code standards, architecture pointers) and its Architecture Reference section for links to the full per-component architecture docs (CNI, router, gateway).
See LICENSE for details.
Galactic is developed by Datum.