galactic-router programs interface traffic policy on each node. For every
VPCAttachment whose status.node is its node, it compiles the TrafficPolicy
each direction of the interface is bound to, publishes it into the uSID
datapath, reports what it applied in the TrafficPolicyBinding's
status.nodes, and exports the policy counters. It does the same for every
Galactic Edge attachment on its node (see Edge attachments). The cell side, which
validates policies and resolves bindings, is
galactic-controller; the design is in
the traffic policy proposal.
Set GALACTIC_ROUTER_TRAFFIC_POLICY=true on the plain galactic-router
DaemonSet, never on the route reflector, which refuses it. Install first:
- the
galactic.datumapis.comCRDs (config/crd/), since an informer for a missing CRD never syncs; - the ClusterRole in
config/galactic-router/rbac.yaml, which grants read access to policies, bindings, EdgeAttachments and EdgeServices andpatchon the bindings' status.
Whether the datapath enforces, audits or ignores what galactic-router
publishes is galactic-cni's setting, GALACTIC_CNI_TRAFFIC_POLICY. With it
off, galactic-router still publishes and reports, and nothing is enforced.
| Binding | The direction gets |
|---|---|
| No TrafficPolicyBinding | The built-in default-deny-all, which denies everything |
| A binding with no reference for a direction | The built-in default-deny-all |
| A reference to a valid policy | That policy, at its current generation |
| A reference to a missing policy | Nothing: the direction denies as policy_unavailable and its status is Pending |
| A reference to a policy that does not compile | The last snapshot of the same policy, if any, otherwise nothing; status is Failed |
A reference to a default-deny-all the controller does not own resolves to
nothing, as on the cell side.
Ingress policy is decided on the packet as it is delivered, after
decapsulation and any translation. A client of an IPv4 VIP reaches its
IPv6 backend translated to IPv6, from 64:ff9b::<client address> (RFC
6052), so an ingress rule must match that client as an IPv6 source in
64:ff9b::/96. An IPv4 CIDR or an ipFamily: IPv4 rule never matches an
IPv4 VIP's flows. See
What an IPv4 VIP needs.
Each rule keeps its counter row while its name and action stay the same within one policy and one interface lifetime, so editing a match does not reset its counts. Renaming a rule, flipping its action, binding a different policy or recreating the interface starts a new series.
galactic-router reconciles on every change and every five minutes. A new interface denies everything until its policy is published. galactic-router watches the node's interfaces, so it publishes as soon as the interface comes up, even when the attachment's status landed before the interface existed; an interface still waiting for its datapath row is retried every five seconds as a fallback. When it restarts, it adopts each snapshot still bound in the datapath that holds exactly what the current policy compiles to, for the same token, policy UID and generation, so counts continue; a snapshot an older galactic-router compiled differently is republished, starting its rules from zero. A snapshot on an interface no attachment claims within one resync is unbound, and its rows are freed.
To read a snapshot back, galactic-router pins each one it publishes under
tp_snapshots/ in the datapath's pin directory, beside the policy maps, and
galactic-cni removes the pin when it unbinds the snapshot. On its first
reconcile and at most once every five minutes after, galactic-router removes
any pin that holds no bound snapshot, such as one left by a publish that an
interface's removal or a restart interrupted. Until then, such a pin holds its
snapshot's memory. The kernel opens a map
by ID only for CAP_SYS_ADMIN, which galactic-router does not have (it runs
with NET_ADMIN and BPF), so a snapshot without a matching pin, such as
one bound by a release that did not pin, cannot be read. galactic-router
adopts it as empty and logs it: it stays bound, so traffic keeps its
current verdict, until the next reconcile republishes it with fresh counter
rows or the sweep unbinds it. Until then its rules can count into rows
another snapshot was given, so those counters can over-count for that
window; verdicts are unaffected.
A rebind that cannot be installed, such as one that runs out of counter
rows, unbinds the direction rather than keeping the old policy's grants,
and reports Failed. A failed update of the same policy keeps its last
snapshot. Status is written only when this node's entry changes.
An EdgeAttachment's host link is enforced like a workload's interface.
galactic-edge writes the link's policy row and attaches the enforcement
programs before the link comes up, and galactic-router binds the policy
the attachment's EdgeService selects in spec.trafficPolicy:
apiVersion: galactic.datumapis.com/v1alpha1
kind: EdgeService
metadata:
name: connect
namespace: tenant
spec:
# providerRef, vpcRef, capabilities, ...
trafficPolicy:
ingressPolicyRef: {name: from-vpc} # traffic delivered to the provider
egressPolicyRef: {name: to-vpc} # traffic the provider sends into the VPCBoth references resolve in the service's namespace, never in the provider's
namespace where the attachment lives, so a provider cannot choose its own
policy. The table above applies, with the service's trafficPolicy in
place of a binding. In addition, galactic-router binds nothing, and unbinds
both directions, while the attachment is not Admitted or its service is
gone, being deleted, replaced or granting another provider; the link then
denies everything as policy_unavailable. A change to the service or to a
referenced policy is republished to the attachments on the node.
galactic-router writes no status for an edge attachment, so
galactic_cni_traffic_policy_mode (see Metrics) is the only
record of whether its node enforces what it installed. galactic-controller
reports whether the references resolve in the service's
TrafficPolicyResolved condition, with reason Default while both
directions take the built-in default and its attachments carry nothing, and
the counters below show what each direction applies. Their resource_namespace and attachment labels name the
EdgeAttachment, vpc the service's VPC, and interface the host link.
Each node owns one entry in the binding's status.nodes, written with
server-side apply as field manager galactic-router/<node>:
status:
nodes:
- node: worker-1
ingress:
state: Applied
policy: {name: default-deny-all, uid: ..., generation: 1}
observedGeneration: 3
lastTransitionTime: "2026-10-15T12:00:00Z"
egress:
state: Applied
policy: {name: web, uid: ..., generation: 7}
observedGeneration: 3
lastTransitionTime: "2026-10-15T12:00:00Z"
mode: Enforce
heartbeatTime: "2026-10-15T12:05:00Z"
conditions:
- type: Installed
status: "True"
reason: Installed
- type: Enforcing
status: "True"
reason: EnforcingPending means the interface's datapath state is not ready yet, or a
reference resolves to nothing. Failed carries the reason in message. The
entry disappears when the attachment leaves the node.
Applied means a snapshot is installed, not that traffic is protected.
mode is the node's effective mode, which galactic-router reads from the
datapath galactic-cni configured on every reconcile, so a galactic-cni
restart into another mode reaches status within the 5 minute resync without
a policy edit:
mode |
Meaning |
|---|---|
Off |
Installed policies decide nothing. |
Audit |
Every packet is decided and counted, but forwarded as before. |
Enforce |
What policy denies is dropped. |
Unsupported |
Audit or enforce is configured, but the kernel failed the preflight; the node protects nothing. |
Unknown |
galactic-router could not read the mode, or the entry was written by a router that predates mode. |
heartbeatTime is refreshed at least every 10 minutes while the node holds
the interface, so the entry is rewritten at most once per heartbeat when
nothing else changes. An entry whose heartbeat is 25 minutes old or more
describes a node that stopped reporting.
galactic-controller derives two conditions from status.nodes, alongside
Resolved on each direction:
InstalledisTruewhen the node the interface is on, its VPCAttachment'sstatus.node, reports both directionsAppliedat the binding's current generation, with a current heartbeat. Otherwise it isFalsewith reasonNoNodes(the interface is on no node),Failed,StaleorPending(including no report yet), naming the node.EnforcingisTruewhen the binding isInstalledand that node reportsmode: Enforce. Only then is the interface protected. Otherwise it isFalsewithInstalled's reason, or, when installed, with reasonUnsupported,Off,AuditorUnknown.
Entries other nodes left behind, such as one from a node lost while the interface moved away, are ignored, and the controller reconciles the binding again when its attachment changes node.
Neither condition is ever True from an Applied entry alone, and a node
that stops reporting turns both False once its heartbeat goes stale: the
controller requeues the binding for that moment rather than waiting for an
event. It keeps no state of its own, so a restarted controller derives the
same conditions. kubectl get trafficpolicybindings shows Enforcing as a
column.
galactic-router serves the policy counters on its metrics port (9179),
which the galactic-router PodMonitor already scrapes, as
galactic_cni_traffic_policy_*: the prefix names the datapath that counts,
not the process that serves them. docs/metrics.md
lists every series, its labels and example queries.
Every installed rule has a series from the moment it is published, at zero,
including the built-in default's deny-all rule, whose policy_uid is
builtin:default-deny-all:v1; an interface still on the default is visible
by that label. A hit is one evaluation, not one forwarded packet. The
supported budget is 27,200 series per node, 100 interfaces at the 64-rule
limit. After a restart, series return once galactic-router reconciles each
interface and adopts its snapshot, with their counts intact. A counter row
the collector cannot read is left out, never reported as zero, and counted
in galactic_cni_traffic_policy_counter_read_errors.
Audit and enforce count identically, so the counters cannot tell them apart.
galactic_cni_traffic_policy_mode{mode} reports the node's effective mode:
one series each for off, audit, enforce, unsupported and unknown,
1 for the current mode and 0 for the rest. It reads unknown while
galactic-router has not opened the datapath's maps yet.
Run these in the galactic-router pod.
galactic-router policy explain decides one packet against a policy, from a
file or the cluster, without touching the datapath:
$ galactic-router policy explain --policy tenant/web \
--path Private --src 10.0.0.1 --dst 10.0.0.2 --proto tcp --dport 80
policy web
path Private
verdict allow
reason rule
rule http (#0)
compiled entries 1It decides with both the Go evaluator and the compiled entries the datapath
runs, and fails if they disagree. Leave --path out to see what an
unclassified packet gets.
galactic-router policy show <namespace>/<attachment> prints what this node
holds for one attachment: each direction's binding, status, bound snapshot
and every rule's counters, plus any fallback counts. Rules are named when the
policy in the cluster is still the generation the snapshot was compiled from,
and numbered otherwise.