Documentation
¶
Index ¶
- Constants
- Variables
- func ComputeStaticIP(gateway netip.Addr, lastOctet byte) (netip.Addr, error)
- func ConfigRulesToProto(in []config.EgressRule) []*adminv1.EgressRule
- func DetectCgroupDriver(ctx context.Context, dc *docker.Client) (string, error)
- func EBPFCgroupPath(cgroupDriver, containerID string) string
- func EnsureCA(certDir string) (*x509.Certificate, *ecdsa.PrivateKey, error)
- func GenerateCorefile(rules []config.EgressRule, healthPort int) ([]byte, error)
- func GenerateDomainCert(caCert *x509.Certificate, caKey *ecdsa.PrivateKey, domain string) (certPEM, keyPEM []byte, err error)
- func GenerateEnvoyConfig(rules []config.EgressRule, ports EnvoyPorts) ([]byte, []string, error)
- func IsCanonicalContainerID(s string) bool
- func NewRulesStore(cfg config.Config) (*storage.Store[EgressRulesFile], error)
- func NormalizeAndDedup(rules []config.EgressRule) ([]config.EgressRule, []string)
- func NormalizeRule(r config.EgressRule) config.EgressRule
- func ProtoRulesToConfig(in []*adminv1.EgressRule) []config.EgressRule
- func RegenerateDomainCerts(rules []config.EgressRule, certDir string, caCert *x509.Certificate, ...) error
- func ResolveContainerID(ctx context.Context, dc *docker.Client, ref string) (string, error)
- func RotateCA(certDir string, rules []config.EgressRule) error
- func RoutesFromRules(rules []config.EgressRule, ports EnvoyPorts) []ebpf.Route
- func RuleKey(r config.EgressRule) string
- func ValidateDst(dst string) error
- type ActionFunc
- type ActionKind
- type ActionQueue
- type ActionResult
- type BypassResult
- type ContainerResolver
- type DisableResult
- type EgressRulesFile
- type EnableResult
- type EnvoyPorts
- type Handler
- func (h *Handler) CancelAllBypassTimers() int
- func (h *Handler) FirewallAddRules(ctx context.Context, req *adminv1.FirewallAddRulesRequest) (*adminv1.FirewallAddRulesResult, error)
- func (h *Handler) FirewallBypass(ctx context.Context, req *adminv1.FirewallBypassRequest) (*adminv1.FirewallBypassResult, error)
- func (h *Handler) FirewallDisable(ctx context.Context, req *adminv1.FirewallDisableRequest) (*adminv1.FirewallDisableResult, error)
- func (h *Handler) FirewallEnable(ctx context.Context, req *adminv1.FirewallEnableRequest) (*adminv1.FirewallEnableResult, error)
- func (h *Handler) FirewallInit(ctx context.Context, _ *adminv1.FirewallInitRequest) (*adminv1.FirewallInitResult, error)
- func (h *Handler) FirewallListRules(ctx context.Context, _ *adminv1.FirewallListRulesRequest) (*adminv1.FirewallListRulesResult, error)
- func (h *Handler) FirewallReload(ctx context.Context, _ *adminv1.FirewallReloadRequest) (*adminv1.FirewallReloadResult, error)
- func (h *Handler) FirewallRemove(ctx context.Context, _ *adminv1.FirewallRemoveRequest) (*adminv1.FirewallRemoveResult, error)
- func (h *Handler) FirewallRemoveRule(ctx context.Context, req *adminv1.FirewallRemoveRuleRequest) (*adminv1.FirewallRemoveRuleResult, error)
- func (h *Handler) FirewallResolveHostname(ctx context.Context, req *adminv1.FirewallResolveHostnameRequest) (*adminv1.FirewallResolveHostnameResult, error)
- func (h *Handler) FirewallRotateCA(ctx context.Context, _ *adminv1.FirewallRotateCARequest) (*adminv1.FirewallRotateCAResult, error)
- func (h *Handler) FirewallStatus(ctx context.Context, _ *adminv1.FirewallStatusRequest) (*adminv1.FirewallStatusResult, error)
- func (h *Handler) FirewallSyncRoutes(ctx context.Context, _ *adminv1.FirewallSyncRoutesRequest) (*adminv1.FirewallSyncRoutesResult, error)
- type HandlerDeps
- type HealthTimeoutError
- type InitResult
- type ListRulesResult
- type NetworkInfo
- type ResolveResult
- type Stack
- func (s *Stack) CIDR() string
- func (s *Stack) CoreDNSIP() string
- func (s *Stack) EnsureRunning(ctx context.Context) error
- func (s *Stack) EnvoyIP() string
- func (s *Stack) NetworkID() string
- func (s *Stack) NetworkInfo(ctx context.Context) (*NetworkInfo, error)
- func (s *Stack) Reload(ctx context.Context) error
- func (s *Stack) Status(ctx context.Context) (*Status, error)
- func (s *Stack) Stop(ctx context.Context) error
- func (s *Stack) WaitForHealthy(ctx context.Context) error
- type StackLifecycle
- type StackReloadResult
- type Status
- type StatusResult
- type TCPMapping
- type TeardownResult
Constants ¶
const ( ReasonCPNotRunning = "CP_NOT_RUNNING" ReasonQueueClosed = "QUEUE_CLOSED" ReasonFirewallNotInitialized = "FIREWALL_NOT_INITIALIZED" ReasonContainerGone = "CONTAINER_GONE" ReasonRuleInvalid = "RULE_INVALID" ReasonRuleNotFound = "RULE_NOT_FOUND" ReasonRuleStoreWrite = "RULE_STORE_WRITE" ReasonCertRegen = "CERT_REGEN" ReasonStackProbe = "STACK_PROBE" ReasonConfigRegen = "CONFIG_REGEN" ReasonEnvoyRestart = "ENVOY_RESTART" ReasonCoreDNSRestart = "COREDNS_RESTART" ReasonStackUnhealthy = "STACK_UNHEALTHY" ReasonRouteSync = "ROUTE_SYNC" )
Reason* constants are the stable wire strings carried in errdetails.ErrorInfo.Reason across the gRPC boundary. CLI code matches on these instead of Go-side sentinel identity — status.Error wrapping drops errors.Is fidelity across the wire.
const ErrorInfoDomain = "firewall.clawker.dev"
ErrorInfoDomain is the errdetails.ErrorInfo.Domain for every firewall error detail. Keeps Reason lookups scoped — future domain error catalogs pick their own domain string.
Variables ¶
var ( ErrEnvoyUnhealthy = errors.New("envoy not healthy") ErrCoreDNSUnhealthy = errors.New("coredns not healthy") ErrCPUnhealthy = errors.New("clawker-controlplane not healthy") )
Sentinel errors for firewall stack health check failures.
var ( // CLI-dial layer. ErrCPNotRunning mostly surfaces client-side from // the AdminClient dial helper; the server rarely emits it. ErrCPNotRunning = errors.New("control plane not running") ErrQueueClosed = errors.New("action queue closed or CP shutting down") // Pre-Submit layer (store unchanged on failure). ErrFirewallNotInitialized = errors.New("firewall not initialized") ErrContainerGone = errors.New("container no longer exists") ErrRuleInvalid = errors.New("invalid rule") ErrRuleNotFound = errors.New("rule not found") ErrRuleStoreWrite = errors.New("rule store write failed") ErrCertRegen = errors.New("ca / per-domain cert regeneration failed") // Queued-closure layer (store already committed — partial success). ErrStackProbe = errors.New("cannot probe firewall stack state") ErrConfigRegen = errors.New("stack config regeneration failed") ErrEnvoyRestart = errors.New("envoy restart failed") ErrCoreDNSRestart = errors.New("coredns restart failed") ErrStackUnhealthy = errors.New("stack containers are not healthy") ErrRouteSync = errors.New("bpf route map sync failed") )
Sentinels surfaced through the Handler and queue. Each has a companion Reason constant for gRPC errdetails.ErrorInfo so the CLI can dispatch remediation without string-matching status messages.
Layering (see initiative memory `firewall-queue-initiative` → "Three- layer failure model"): CLI-dial sentinels fire before the queue is touched; pre-Submit sentinels fire inside the RPC handler before the store write lands; queued-closure sentinels fire on the worker after the store write has already committed — partial success by design.
var ( ErrClosed = errors.New("action queue closed") ErrNilClosure = errors.New("action queue: nil closure") ErrClosurePanic = errors.New("action closure panicked") )
Sentinels returned on an ActionResult when the queue itself rejects or cannot complete a submission. Closure-level failures use sentinels defined alongside the closure.
var CoreDNSClawkerBinary []byte
CoreDNSClawkerBinary is the pre-compiled static Linux binary for custom CoreDNS with the dnsbpf plugin (real-time BPF dns_cache population). Built by: make coredns-binary Target: GOOS=linux CGO_ENABLED=0 go build ./cmd/coredns-clawker
This binary is embedded into every clawker release binary so the custom CoreDNS container image can be built on-demand without a registry or source tree. The binary must match the Docker host's architecture (arm64 or amd64).
Functions ¶
func ComputeStaticIP ¶
ComputeStaticIP replaces the last octet of an IPv4 address with the given value. For example, gateway 172.20.0.1 with lastOctet 2 produces 172.20.0.2.
func ConfigRulesToProto ¶
func ConfigRulesToProto(in []config.EgressRule) []*adminv1.EgressRule
ConfigRulesToProto copies []config.EgressRule → []*adminv1.EgressRule. Exported because CLI command code needs the reverse mapping when displaying rules returned from FirewallListRules.
func DetectCgroupDriver ¶
DetectCgroupDriver returns the Docker daemon's cgroup driver (typically "systemd" on native Linux, "cgroupfs" on Docker Desktop). The value is stable for the daemon's lifetime; callers cache it at init. Errors propagate rather than defaulting — a silent default would produce ENOENT at eBPF attach time.
func EBPFCgroupPath ¶
EBPFCgroupPath returns the BPF-attachable cgroup v2 path for a Docker container. Any driver other than "systemd" uses the cgroupfs layout.
func EnsureCA ¶
func EnsureCA(certDir string) (*x509.Certificate, *ecdsa.PrivateKey, error)
EnsureCA creates a self-signed CA keypair if none exists under certDir, or loads the existing one.
func GenerateCorefile ¶
func GenerateCorefile(rules []config.EgressRule, healthPort int) ([]byte, error)
GenerateCorefile produces a CoreDNS Corefile from the given egress rules. healthPort is the port the CoreDNS health plugin listens on (inside the container).
Only "allow" rules with domain destinations (not IPs/CIDRs) get forward zones. Each allowed domain gets its own zone forwarding to Cloudflare malware-blocking DNS. The catch-all "." zone returns NXDOMAIN for everything else.
func GenerateDomainCert ¶
func GenerateDomainCert(caCert *x509.Certificate, caKey *ecdsa.PrivateKey, domain string) (certPEM, keyPEM []byte, err error)
GenerateDomainCert signs a per-domain certificate for TLS inspection. The certificate is signed by the given CA and has the domain as a SAN. For wildcard domains (leading-dot convention), the SAN includes both the apex (e.g., "datadoghq.com") and the wildcard ("*.datadoghq.com") so TLS inspection works for any subdomain. Returns PEM-encoded cert and key bytes.
func GenerateEnvoyConfig ¶
func GenerateEnvoyConfig(rules []config.EgressRule, ports EnvoyPorts) ([]byte, []string, error)
GenerateEnvoyConfig produces an Envoy static bootstrap YAML from egress rules. Returns the YAML bytes and a list of warnings (non-fatal issues).
func IsCanonicalContainerID ¶
IsCanonicalContainerID reports whether s matches Docker's on-the-wire container ID format: exactly 64 lowercase hex characters. Exported so the host-side resolver factory in cmd/clawker-cp can apply the same validation without re-implementing the predicate.
func NewRulesStore ¶
NewRulesStore creates a storage.Store[EgressRulesFile] for egress-rules.yaml. The store uses the firewall data subdirectory for file discovery.
func NormalizeAndDedup ¶
func NormalizeAndDedup(rules []config.EgressRule) ([]config.EgressRule, []string)
NormalizeAndDedup normalizes all rules and removes duplicates. This handles legacy store files that contain port:0 rules written before NormalizeRule defaulted TLS to 443 — after normalization those become duplicates of the correctly-ported entries.
Wildcard (.claude.ai) and exact (claude.ai) rules are NOT deduped against each other — they are semantically distinct. A user may want unrestricted subdomain access while restricting paths on the apex, or vice versa.
func NormalizeRule ¶
func NormalizeRule(r config.EgressRule) config.EgressRule
NormalizeRule fills in missing fields before storage so rules are explicit and unambiguous. Empty proto defaults to "tls", empty action to "allow", and TLS rules with no port default to 443. Existing non-zero values are never overridden. Users should set full rules — this is a storage safety net, not a feature.
func ProtoRulesToConfig ¶
func ProtoRulesToConfig(in []*adminv1.EgressRule) []config.EgressRule
ProtoRulesToConfig copies []*adminv1.EgressRule → []config.EgressRule. The two types track identical field sets; the dedicated mapper keeps the handler free of gRPC types when calling into the rules store.
func RegenerateDomainCerts ¶
func RegenerateDomainCerts(rules []config.EgressRule, certDir string, caCert *x509.Certificate, caKey *ecdsa.PrivateKey) error
RegenerateDomainCerts generates certificates for all TLS egress rules, storing them in certDir/<domain>-cert.pem and <domain>-key.pem. Every TLS rule gets a certificate — Envoy terminates TLS for all domains to enable HTTP-level inspection (paths, methods, response codes).
Rules are deduplicated by normalized domain. If any rule for a domain uses the wildcard convention (leading dot), the cert includes both apex and wildcard SANs. This prevents a later exact-domain rule from overwriting a cert that also needs wildcard SANs.
Cert generation runs before stale cleanup so that a partial failure leaves previously-working certs intact rather than an empty directory.
func ResolveContainerID ¶
ResolveContainerID normalizes a container reference (name, short ID, or canonical long ID) to the 64-char lowercase hex long ID that EBPFCgroupPath expects. Canonical inputs skip the Docker round-trip.
func RotateCA ¶
func RotateCA(certDir string, rules []config.EgressRule) error
RotateCA regenerates the CA keypair and all domain certificates. The old CA files are overwritten. Any running containers will need the new CA injected to trust the regenerated domain certs.
func RoutesFromRules ¶
func RoutesFromRules(rules []config.EgressRule, ports EnvoyPorts) []ebpf.Route
RoutesFromRules projects a rule set into the BPF route_map entry form. Destinations are normalized before hashing so the resulting DomainHash matches whatever CoreDNS writes into dns_cache at resolve time (INV: normalizeDomain + ebpf.DomainHash form the shared hashing contract across firewall / dnsbpf / ebpf).
TLS/HTTP rules route to the main egress listener (ports.EgressPort). SSH/TCP rules route to their dedicated per-rule TCP listener port (ports.TCPPortBase + index). The TCP/SSH branch drives routes directly from TCPMappings so eBPF routes and Envoy listeners stay in lockstep: matching allow semantics (empty Action == allow), matching IP/CIDR filtering, and matching tcpDefaultPort defaulting (ssh→22, tcp→443) for rules with Port==0. Any divergence here silently misroutes traffic (e.g. SSH landing on the main TLS listener — tls_inspector sees raw TCP, no SNI match, deny chain resets).
func RuleKey ¶
func RuleKey(r config.EgressRule) string
RuleKey returns the dedup key for an egress rule: dst:proto:port. The Dst is used verbatim so that ".claude.ai" and "claude.ai" are distinct rules — a wildcard and its apex carry independent semantics (e.g., different PathRules) and must not be collapsed.
func ValidateDst ¶
ValidateDst checks that a destination is a valid lowercase domain name, wildcard domain, IP address, or CIDR block. Exported so CLI commands can pre-validate before attempting store mutations.
Domain validation is based on Go's net.isDomainName (RFC 1035 / RFC 1123 label constraints) with two deliberate deviations: uppercase is rejected to enforce normalized storage, and the root domain "." is not accepted. Underscores are allowed for SRV/DMARC compatibility.
Types ¶
type ActionFunc ¶
ActionFunc is the shape of every queued closure. It receives the queue's long-lived context — cancelled by Close — and returns a Result value on success or a wrapped sentinel error on failure. Closures that run for more than a few milliseconds MUST honor ctx.Done() so Close can drain in bounded time.
type ActionKind ¶
type ActionKind int
ActionKind classifies a queued action. Its Coalesces method decides whether consecutive same-kind items collapse into a single execution.
const ( ActionUnknown ActionKind = iota ActionBringup ActionTeardown ActionReconcile ActionRead ActionEnable ActionDisable ActionBypass )
func (ActionKind) Coalesces ¶
func (k ActionKind) Coalesces() bool
Coalesces reports whether consecutive submissions of this kind collapse into a single execution. Only ActionReconcile coalesces: every RPC mapped to it regenerates stack state from the current rules store, so drained submitters observe identical output. Per-container kinds (Enable, Disable, Bypass) carry distinct arguments per call and must run individually; Bringup, Teardown, and Read likewise execute one-by-one.
func (ActionKind) String ¶
func (k ActionKind) String() string
type ActionQueue ¶
type ActionQueue struct {
// contains filtered or unexported fields
}
ActionQueue serializes firewall actions behind a single worker goroutine. Items execute in strict FIFO order across kinds; consecutive items whose kind returns true from Coalesces collapse into one execution and every drained submitter receives the same result. Submit is close-safe: submissions accepted before Close was called run to completion; once Close has been called (even before it returns), further submissions receive ErrClosed. Close then blocks until the worker has drained every accepted item.
func NewActionQueue ¶
func NewActionQueue(log *logger.Logger) *ActionQueue
NewActionQueue constructs an ActionQueue and starts its worker goroutine. A nil logger is treated as logger.Nop().
func (*ActionQueue) Close ¶
func (q *ActionQueue) Close() error
Close stops accepting new submissions, waits for every submission accepted before Close returned to run to completion, and cancels the worker context so in-flight and drained closures can observe shutdown. Cancellation is published BEFORE the broadcast that wakes the worker so any item the worker subsequently pops enters execute() with an already-cancelled ctx — ctx-aware closures that short-circuit on cancellation will deliver ctx.Err() to their submitters instead of mutating stack/eBPF state mid-shutdown. Idempotent. Always returns nil — the error return matches io.Closer so callers can treat the queue as a Closer.
func (*ActionQueue) Submit ¶
func (q *ActionQueue) Submit(kind ActionKind, fn ActionFunc) <-chan ActionResult
Submit enqueues fn for execution under kind and returns a channel that will receive exactly one ActionResult. Once Close has been called, further submissions receive a pre-closed channel carrying ErrClosed; a nil fn likewise receives ErrNilClosure. Submit never panics and never waits on the worker to drain (the reply channel is buffered).
type ActionResult ¶
ActionResult is produced by a queued closure and delivered to every submitter whose action contributed to the execution. Exactly one of Value or Err is meaningful: on clean success Value carries the closure Result and Err is nil; on failure Err carries one or more wrapped sentinels and Value is nil.
type BypassResult ¶
type BypassResult struct{}
type ContainerResolver ¶
type ContainerResolver func(ctx context.Context, ref string) (id, cgroupPath string, exists bool, err error)
ContainerResolver looks up a container reference (name, short ID, or canonical long ID) against Docker and returns its canonical ID, the BPF-attachable cgroup path, and whether Docker still knows the container.
A NotFound result MUST come back as (_, _, false, nil) — a nil error with exists=false — so callers can distinguish "container is gone" from "we couldn't talk to Docker". A real Docker API failure surfaces as err.
type DisableResult ¶
type DisableResult struct{}
type EgressRulesFile ¶
type EgressRulesFile struct {
Rules []config.EgressRule `yaml:"rules" label:"Rules" desc:"Active egress firewall rules"`
}
EgressRulesFile is the top-level document type for storage.Store[T]. It persists the active set of project-level egress rules to disk.
func (EgressRulesFile) Fields ¶
func (f EgressRulesFile) Fields() storage.FieldSet
Fields implements storage.Schema for EgressRulesFile.
type EnableResult ¶
type EnableResult struct{}
type EnvoyPorts ¶
type EnvoyPorts struct {
EgressPort int // Main egress listener — handles TLS (per-domain filter chains) and HTTP (raw_buffer filter chain).
TCPPortBase int // Starting port for TCP/SSH listeners.
HealthPort int // Dedicated health check listener port for external probes.
}
EnvoyPorts holds the port configuration for the Envoy proxy, sourced from config.Config.
func (EnvoyPorts) Validate ¶
func (p EnvoyPorts) Validate() error
Validate checks that all ports are in valid range and no two ports collide.
type Handler ¶
type Handler struct {
adminv1.UnimplementedAdminServiceServer
// contains filtered or unexported fields
}
Handler implements adminv1.AdminServiceServer for the firewall domain. Every Firewall* RPC submits its body as a closure to a single shared ActionQueue so rapid-fire calls don't collide mid-restart (see initiative memory `firewall-queue-initiative` for the full design). Rule-CRUD and rotate-CA RPCs split store-side work (pre-Submit, synchronous) from stack reconcile work (queued), so a durable rule mutation is never lost even when the subsequent reload fails.
func NewHandler ¶
func NewHandler(deps HandlerDeps) *Handler
NewHandler wires a firewall Handler. Panics on missing EBPF, Resolver, or Queue — every RPC routes through the queue and hits eBPF/Resolver, so nil there would surface as a confusing nil-deref deep inside the gRPC interceptor chain. Stack / Store / Cfg are optional at construction so tests that only exercise the ebpf-backed RPCs can skip them; calls that need them fall through to a panic at the first nil access with a clear line number.
func (*Handler) CancelAllBypassTimers ¶
CancelAllBypassTimers stops every pending dead-man timer and clears the bypass-timers map. Part of INV-B2-007 drain-to-zero: cancelling before eBPF FlushAll stops scheduled Enables from firing against maps that are about to be emptied. A fire goroutine past timer.Stop's check will submit to the queue; after queue.Close that submission returns ErrClosed and is a harmless retry-exhausted log. Returns the count cancelled.
func (*Handler) FirewallAddRules ¶
func (h *Handler) FirewallAddRules(ctx context.Context, req *adminv1.FirewallAddRulesRequest) (*adminv1.FirewallAddRulesResult, error)
FirewallAddRules persists new egress rules then reconciles the stack. Store mutation is synchronous and pre-Submit so the rule is durable the moment the RPC accepts it; the queued closure then regenerates Envoy/CoreDNS config and restarts both. When the stack is down at queue-time the closure short-circuits to StackReloadResult{Restarted: false} — the rule is still saved, next FirewallInit picks it up.
func (*Handler) FirewallBypass ¶
func (h *Handler) FirewallBypass(ctx context.Context, req *adminv1.FirewallBypassRequest) (*adminv1.FirewallBypassResult, error)
FirewallBypass = Disable (queued) + CP dead-man timer that submits a queued Enable on expiry. The restore path reuses the same drift guard as direct FirewallEnable so enforcement returns on the container's current cgroup_id, not the one it had at bypass-time.
func (*Handler) FirewallDisable ¶
func (h *Handler) FirewallDisable(ctx context.Context, req *adminv1.FirewallDisableRequest) (*adminv1.FirewallDisableResult, error)
FirewallDisable sets the per-container BPF bypass flag. Pre-Submit resolves the target cgroup_id (or falls back to the last-known value when Docker says the container is gone); the queued closure performs the ebpf.Disable.
func (*Handler) FirewallEnable ¶
func (h *Handler) FirewallEnable(ctx context.Context, req *adminv1.FirewallEnableRequest) (*adminv1.FirewallEnableResult, error)
FirewallEnable enrolls a container. Pre-Submit resolves container_id → cgroup_path and builds the BPF container_config from CP-side state (network discovery, host-proxy resolution, egress port); the queued closure installs the resulting config via ebpf.Install and cancels any pending bypass timer.
func (*Handler) FirewallInit ¶
func (h *Handler) FirewallInit(ctx context.Context, _ *adminv1.FirewallInitRequest) (*adminv1.FirewallInitResult, error)
FirewallInit brings the firewall stack (Envoy + CoreDNS) up via a queued bringup action. BPF programs are loaded once at CP startup via ebpf.Manager.Load; this RPC is the idempotent "stack up" signal.
After the stack is healthy, Init re-enrolls every running managed agent it can find. On a cold CP start that follows a previous CP's FlushAll, container_map is empty — without re-enrollment, long-lived agents that outlived the previous CP would egress unenforced (fail-open by BPF design). Re-enrollment is in-closure so it serializes with concurrent Enable/Disable/Bypass through the same ActionBringup work unit.
func (*Handler) FirewallListRules ¶
func (h *Handler) FirewallListRules(ctx context.Context, _ *adminv1.FirewallListRulesRequest) (*adminv1.FirewallListRulesResult, error)
FirewallListRules returns the current normalized+deduped rule set. Routed through the queue under ActionRead so a read never races ahead of pending writes (read-after-write consistency).
func (*Handler) FirewallReload ¶
func (h *Handler) FirewallReload(ctx context.Context, _ *adminv1.FirewallReloadRequest) (*adminv1.FirewallReloadResult, error)
FirewallReload regenerates configs and restarts Envoy+CoreDNS without mutating the rule set. No pre-Submit work — it is a pure reconcile signal against the current store contents.
func (*Handler) FirewallRemove ¶
func (h *Handler) FirewallRemove(ctx context.Context, _ *adminv1.FirewallRemoveRequest) (*adminv1.FirewallRemoveResult, error)
FirewallRemove is global teardown. Pre-Submit: cancel pending bypass timers so they can't fire against maps that are about to be flushed. Queued closure: stop stack, flush eBPF state, delete generated config files on disk, clear storedCgroupID. Unlike the pre-queue implementation, the egress-rules file is preserved — the store is authoritative across teardown so a user's removals after `firewall down` apply to the next `firewall up`.
func (*Handler) FirewallRemoveRule ¶
func (h *Handler) FirewallRemoveRule(ctx context.Context, req *adminv1.FirewallRemoveRuleRequest) (*adminv1.FirewallRemoveRuleResult, error)
FirewallRemoveRule deletes a single rule matched by (dst, proto, port) pre-Submit then reconciles the stack. A miss on the store lookup returns ErrRuleNotFound → codes.NotFound so a typo or wrong-proto/port never masquerades as success. No ValidateDst here: anything that fails to match an existing key — typo, malformed hostname, or legitimate absence — collapses into the same NotFound outcome, which is exactly the behavior the CLI needs to render.
func (*Handler) FirewallResolveHostname ¶
func (h *Handler) FirewallResolveHostname(ctx context.Context, req *adminv1.FirewallResolveHostnameRequest) (*adminv1.FirewallResolveHostnameResult, error)
FirewallResolveHostname performs a DNS lookup from the CP's network namespace — used to resolve host.docker.internal during per-container enroll so the BPF container_config holds a routable address.
func (*Handler) FirewallRotateCA ¶
func (h *Handler) FirewallRotateCA(ctx context.Context, _ *adminv1.FirewallRotateCARequest) (*adminv1.FirewallRotateCAResult, error)
FirewallRotateCA regenerates the MITM CA + per-domain certs pre-Submit, then reconciles the stack so Envoy picks up the new chain. Cert regen is synchronous so the CLI sees a clean ErrCertRegen if the disk write fails, leaving the running stack on the prior certificates.
func (*Handler) FirewallStatus ¶
func (h *Handler) FirewallStatus(ctx context.Context, _ *adminv1.FirewallStatusRequest) (*adminv1.FirewallStatusResult, error)
FirewallStatus returns a health snapshot.
func (*Handler) FirewallSyncRoutes ¶
func (h *Handler) FirewallSyncRoutes(ctx context.Context, _ *adminv1.FirewallSyncRoutesRequest) (*adminv1.FirewallSyncRoutesResult, error)
FirewallSyncRoutes is a break-glass re-sync of the BPF route_map. After the queue landed, it regenerates routes from the current store (rather than trusting caller-supplied routes) — coalescing with concurrent AddRules/Reload calls would otherwise silently discard a caller's stale route set. reconcileStackClosure already syncs routes, so routing through ActionReconcile gives SyncRoutes the stronger "stack and route_map are consistent with the store" guarantee at a cost of one extra container restart.
type HandlerDeps ¶
type HandlerDeps struct {
EBPF ebpf.EBPFManager
Stack StackLifecycle
Store *storage.Store[EgressRulesFile]
Cfg config.Config
Resolver ContainerResolver
Log *logger.Logger
Queue *ActionQueue
// CertDirFn optionally overrides FirewallCertSubdir resolution —
// tests pass a temp dir so RotateCA does not touch the real data path.
// nil defaults to cfg.FirewallCertSubdir.
CertDirFn func() (string, error)
// ListAgents returns canonical container IDs of every running
// managed agent the CP knows about. FirewallInit uses it to rebuild
// per-container enforcement after a CP restart: FlushAll wipes
// container_map on shutdown, so agents that outlived the previous
// CP instance would otherwise egress unenforced until they were
// restarted. Nil means "no re-enrollment" (test wiring and flows
// that never want Init to touch agent state).
ListAgents func(ctx context.Context) ([]string, error)
}
HandlerDeps bundles the collaborators Handler needs. Using a deps struct keeps the constructor stable as future domain handlers grow.
type HealthTimeoutError ¶
HealthTimeoutError is returned when a firewall stack health wait exceeds its deadline. Err wraps one or more of the stack health sentinel errors.
func (*HealthTimeoutError) Error ¶
func (e *HealthTimeoutError) Error() string
func (*HealthTimeoutError) Unwrap ¶
func (e *HealthTimeoutError) Unwrap() error
type InitResult ¶
type InitResult struct {
EnvoyIP, CoreDNSIP, NetworkID string
}
InitResult captures the network topology EnsureRunning settled on.
type ListRulesResult ¶
type ListRulesResult struct {
Rules []config.EgressRule
}
ListRulesResult carries the normalized rule snapshot.
type NetworkInfo ¶
type NetworkInfo struct {
NetworkID string
Gateway netip.Addr
Subnet netip.Prefix
EnvoyIP string
CoreDNSIP string
CIDR string
}
NetworkInfo holds discovered state about the firewall Docker network.
func DiscoverNetwork ¶
func DiscoverNetwork(ctx context.Context, dc *docker.Client, cfg config.Config) (*NetworkInfo, error)
DiscoverNetwork inspects the firewall network and computes static IPs for Envoy and CoreDNS from the gateway address using config-defined octets.
The network must already exist — CLI `container start` ensures it via the whail EnsureNetwork container option before any firewall operation runs.
type ResolveResult ¶
type ResolveResult struct {
Addresses []string
}
ResolveResult carries the answer set of a DNS lookup run inside the CP's netns.
type Stack ¶
type Stack struct {
// contains filtered or unexported fields
}
Stack manages the Envoy + CoreDNS container pair via Docker-outside-of- Docker from inside the control plane container. The CP container itself is created host-side by CLI bootstrap, not by Stack.
Stack is not safe for concurrent EnsureRunning + Stop calls; callers serialize via their own mutex.
func NewStack ¶
func NewStack(dc *docker.Client, cfg config.Config, log *logger.Logger, store *storage.Store[EgressRulesFile]) *Stack
NewStack returns an initialized Stack. log may be nil (a Nop logger is substituted); the other dependencies are required — nil docker or cfg produces a nil Stack that panics at first use, which is preferable to silent no-ops.
func (*Stack) EnsureRunning ¶
EnsureRunning starts Envoy + CoreDNS if they are not already running. Idempotent: the method short-circuits per container when it finds the expected container already in the "running" state.
Ordering is fixed: ensure network → discover → write configs/certs → ensure images → ensure Envoy → ensure CoreDNS → wait healthy. CoreDNS's dnsbpf plugin opens the pinned dns_cache map at startup; the map is created by the CP's eBPF manager before Stack starts, so Stack does not touch BPF state here.
func (*Stack) EnvoyIP ¶
EnvoyIP, CoreDNSIP, NetworkID, and CIDR return the current network topology. They re-discover on every call; they are intended for display paths where the cost is negligible. On failure they return "" rather than an error — accessors never panic or block callers.
func (*Stack) NetworkInfo ¶
func (s *Stack) NetworkInfo(ctx context.Context) (*NetworkInfo, error)
NetworkInfo returns the current firewall network topology (Envoy/CoreDNS IPs, CIDR, gateway, network ID). Unlike the string accessors above it surfaces discovery errors — callers on the enforcement path (FirewallEnable) must fail loudly when topology is unknown rather than silently enrolling with zero values.
func (*Stack) Reload ¶
Reload regenerates configs and restarts Envoy + CoreDNS. Callers invoke this after rule mutations land in the store. If the stack is not currently running, Reload does nothing — the next EnsureRunning call will pick up the fresh configs.
Step-level failures are wrapped with their typed sentinel (ErrConfigRegen, ErrStackProbe, ErrEnvoyRestart, ErrCoreDNSRestart, ErrStackUnhealthy) and combined via errors.Join so the Handler's RPC wrapper (toStatus) can attach one errdetails.ErrorInfo per failed step. ErrConfigRegen short-circuits the rest — restarting against stale configs would just thrash. Envoy/CoreDNS restart failures are collected independently; WaitForHealthy runs only when both restarts succeeded (otherwise the primary signal is the restart failure, not the health probe's timeout).
func (*Stack) Status ¶
Status reports the current health of the stack plus rule count and network topology. Docker API errors propagate — callers distinguish "stack down" from "Docker unreachable".
func (*Stack) Stop ¶
Stop removes Envoy + CoreDNS. The clawker-net network and eBPF state are intentionally left intact: agent containers may still be attached to the network, and BPF links are owned by the CP's ebpf.Manager. The control plane container is owned by host-side bootstrap.
func (*Stack) WaitForHealthy ¶
WaitForHealthy polls Envoy and CoreDNS health endpoints until both return HTTP 200 or the context deadline expires. On deadline expiry the error wraps one or both of ErrEnvoyUnhealthy/ErrCoreDNSUnhealthy.
Probes hit clawker-net via internal container IPs — the CP shares the network, so host port forwarding is not required.
type StackLifecycle ¶
type StackLifecycle interface {
EnsureRunning(ctx context.Context) error
Stop(ctx context.Context) error
Reload(ctx context.Context) error
Status(ctx context.Context) (*Status, error)
NetworkInfo(ctx context.Context) (*NetworkInfo, error)
}
StackLifecycle is the subset of *Stack operations the Handler drives. Declared here rather than as a method set on *Stack alone so tests can swap in a lightweight fake — Handler orchestrates Envoy+CoreDNS lifecycle as part of FirewallInit/Remove/Reload/Status/AddRules/ RemoveRules, and unit tests should not have to spin up real containers to cover the RPC surface.
type StackReloadResult ¶
type StackReloadResult struct {
Restarted bool
}
StackReloadResult is produced by reconcileStackClosure. Restarted is true when the live Envoy+CoreDNS pair was reloaded; false when the stack was down at queue-time and only the on-disk state changed. Callers map this onto the wire field `stack_restarted`.
type Status ¶
type Status struct {
// Running is true only when every managed subsystem is up (Envoy,
// CoreDNS, control plane). Per-component booleans below disambiguate
// "partially up" from "fully down".
Running bool
// EnvoyHealth / CoreDNSHealth / CPHealth: container exists and is
// running. Deeper readiness (HTTP /healthz) is probed in WaitForHealthy.
EnvoyHealth bool
CoreDNSHealth bool
CPHealth bool
// RuleCount is the number of normalized/deduplicated egress rules
// currently loaded in the rules store.
RuleCount int
// EnvoyIP / CoreDNSIP / NetworkID: discovered network topology. Empty
// strings mean the firewall network is not yet created — not an error.
EnvoyIP string
CoreDNSIP string
NetworkID string
}
Status is the firewall-domain health snapshot returned by Stack.Status.
The field set is intentionally minimal — any new attribute must be motivated by a concrete CLI or operator-facing need so this struct does not grow into a catch-all.
type StatusResult ¶
type StatusResult struct {
Status
}
StatusResult mirrors the firewall.Status summary.
type TCPMapping ¶
type TCPMapping struct {
Dst string // Destination domain or IP.
DstPort int // Original destination port (e.g. 22, 8080).
EnvoyPort int // Envoy listener port (TCPPortBase + index).
}
TCPMapping describes a per-destination eBPF DNAT entry for non-TLS traffic. Each TCP/SSH rule gets a dedicated Envoy listener port.
func TCPMappings ¶
func TCPMappings(rules []config.EgressRule, ports EnvoyPorts) []TCPMapping
TCPMappings computes TCP port mappings from egress rules. The result is deterministic for a given rule set — same rules produce same mappings. Used by both GenerateEnvoyConfig (to build listeners) and Enable (to build eBPF args).
type TeardownResult ¶
type TeardownResult struct{}
TeardownResult, EnableResult, DisableResult, BypassResult are empty markers — their RPCs report success/failure only.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package ebpf provides eBPF-based traffic routing for clawker containers.
|
Package ebpf provides eBPF-based traffic routing for clawker containers. |
|
cmd
command
ebpf-manager is the entrypoint binary for the clawker eBPF manager container.
|
ebpf-manager is the entrypoint binary for the clawker eBPF manager container. |