egressproxy

package
v0.0.8 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Package egressproxy implements the transparent sandbox egress filter: a userspace proxy that all sandbox TCP is iptables-REDIRECT'd through, plus the redirect installer and the on-disk policy contract the control plane writes.

The enforcement model mirrors e2b-dev/infra's tcpfirewall, adapted for a Kubernetes Pod (init container installs the redirect, a sidecar runs the proxy) and hardened for the eval anti-cheat use case: the default action is deny (allowlist), not allow.

Index

Constants

View Source
const (
	// DefaultProxyUID is the uid the sidecar runs as; the redirect exempts it so
	// the proxy's own upstream connections are not looped back.
	DefaultProxyUID = 1337

	DefaultHTTPPort  = 15001
	DefaultTLSPort   = 15002
	DefaultOtherPort = 15003

	// DefaultPolicyPath is the emptyDir file the control plane writes (exec) and
	// the proxy reads (fsnotify). Mounted only in the sidecar.
	DefaultPolicyPath = "/var/run/egress/policy.json"

	// DefaultSecretsPath is the injection config, written 0600 next to the
	// policy. It holds live credential material and the sandbox's CA key, so it
	// lives on the same sidecar-only tmpfs and is removed on release.
	DefaultSecretsPath = "/var/run/egress/secrets.json"
)

Fixed wiring shared by the redirect installer, the proxy, and the control plane. These are container-internal (the sidecar owns the whole netns view), so constants rather than config keep init and sidecar in lockstep.

View Source
const (
	ModeOverride = "Override"
	ModeIfAbsent = "IfAbsent"
)

Injection mode strings, mirroring the CRD enum.

Variables

This section is empty.

Functions

func GenerateCA added in v0.0.8

func GenerateCA(commonName string, ttl time.Duration) (certPEM, keyPEM string, err error)

GenerateCA mints a self-signed CA for one sandbox and returns the PEM pair. The control plane calls this at claim time: it holds both delivery channels (exec to the sidecar for the key, envd /init for the certificate), so generating here avoids a round-trip to read the certificate back out of the sidecar.

func InstallRedirect

func InstallRedirect(cfg RedirectConfig) error

InstallRedirect programs nat OUTPUT so all sandbox TCP is transparently redirected to the local proxy, while the proxy's own traffic, DNS, and loopback are exempted. Idempotent: the chain is flushed and rebuilt, and the OUTPUT jump is added only if absent.

Runs in the Pod netns (shared by all containers); requires CAP_NET_ADMIN.

func RemoveSecrets added in v0.0.8

func RemoveSecrets(path string) error

RemoveSecrets deletes the secrets file. Called on release so a recycled pod cannot carry the previous sandbox's credentials, CA key, or placeholder map into the next claim. A missing file is success.

func WritePolicy

func WritePolicy(path string, p Policy) error

WritePolicy atomically writes the policy to path (write temp + rename) so a reader (fsnotify reload) never observes a half-written file.

func WriteSecrets added in v0.0.8

func WriteSecrets(path string, s Secrets) error

WriteSecrets atomically writes the injection config with owner-only permissions. Same temp+rename discipline as WritePolicy so a concurrent reader never sees a half-written file.

Types

type ApplyOutcome added in v0.0.8

type ApplyOutcome struct {
	Skipped      bool // rule matched the host but not this request's path/method
	HeadersSet   int
	Substituted  int
	SubstitutedK []string // placeholder-bearing header names, for debug logging
}

ApplyOutcome reports what a single Apply did, for metrics. It deliberately carries no header values or credential material.

type Decision

type Decision struct {
	Allow bool
	Match MatchType
}

Decision is the outcome of an egress check.

type InjectHeader added in v0.0.8

type InjectHeader struct {
	Name string `json:"name"`
	// Value is the final header value; templates were expanded by the operator.
	Value string `json:"value"`
	// Mode is "Override" (default) or "IfAbsent".
	Mode string `json:"mode,omitempty"`
}

InjectHeader is one header to write onto a matching request.

type InjectRule added in v0.0.8

type InjectRule struct {
	// Host is an exact hostname, lowercase. Wildcards are rejected upstream.
	Host string `json:"host"`

	// Ports the rule covers. Empty means the defaults, 80 and 443.
	Ports []int `json:"ports,omitempty"`

	// Headers to inject.
	Headers []InjectHeader `json:"headers,omitempty"`

	// SubstitutePlaceholders lists the placeholders that may be swapped for
	// their real value on this host.
	SubstitutePlaceholders []string `json:"substitutePlaceholders,omitempty"`

	// PathPrefixes / Methods narrow the rule. Empty means no narrowing.
	PathPrefixes []string `json:"pathPrefixes,omitempty"`
	Methods      []string `json:"methods,omitempty"`
}

InjectRule is the per-host injection action.

type MatchType

type MatchType string

MatchType records why a connection was allowed/denied, for metrics and logs.

const (
	MatchNone   MatchType = "none"
	MatchDomain MatchType = "domain"
	MatchCIDR   MatchType = "cidr"
	MatchSSRF   MatchType = "ssrf"
)

type Policy

type Policy struct {
	// SandboxID records which sandbox this policy was pushed for. Informational
	// (surfaced in logs/metrics); the proxy enforces whatever is present.
	SandboxID string `json:"sandboxId,omitempty"`

	// Enforce gates the whole filter. When false the policy is treated as
	// fail-closed (deny all). The control plane sets it true only after a
	// concrete ruleset has been resolved for a claimed sandbox.
	Enforce bool `json:"enforce"`

	// DisableEgress blocks all outbound traffic regardless of the allow lists
	// (DNS is still permitted so name resolution does not hang). Equivalent to
	// an empty allowlist.
	DisableEgress bool `json:"disableEgress,omitempty"`

	// AllowedDomains permits egress to matching hostnames (exact, "*", or
	// "*.example.com"). Domains only ever allow; there is no domain deny.
	AllowedDomains []string `json:"allowedDomains,omitempty"`

	// AllowedCIDRs / DeniedCIDRs are IP/CIDR allow/deny for traffic without a
	// usable hostname (non-80/443 TCP, or bare-IP connections).
	AllowedCIDRs []string `json:"allowedCIDRs,omitempty"`
	DeniedCIDRs  []string `json:"deniedCIDRs,omitempty"`

	// AllowPrivateNetworks disables the built-in anti-SSRF deny of private /
	// link-local / cloud-metadata ranges. Default false (baseline stays on).
	AllowPrivateNetworks bool `json:"allowPrivateNetworks,omitempty"`
}

Policy is the on-disk egress ruleset the control plane pushes to the proxy. It is the single source of truth read by the sidecar; an absent, empty, or unparseable file means fail-closed (deny all egress except DNS).

func FailClosed

func FailClosed() Policy

FailClosed is the policy applied when no valid policy file is present: deny everything. DNS egress is permitted separately by the iptables rules, not by this policy, so resolution keeps working even while fail-closed.

func LoadPolicy

func LoadPolicy(path string) (Policy, error)

LoadPolicy reads and parses the policy file. A missing file yields FailClosed with no error (that is the expected steady state before the first push). A present-but-corrupt file yields FailClosed WITH an error so the caller can log it — but enforcement still fails closed.

func (Policy) Evaluate

func (p Policy) Evaluate(hostname string, ip net.IP) Decision

Evaluate applies the policy to a connection. hostname is the peeked HTTP Host or TLS SNI ("" when unavailable); ip is the original destination IP.

Semantics (allowlist / default-deny, hardened vs e2b's default-allow):

  1. Not enforcing -> allow (feature effectively off).
  2. Anti-SSRF baseline -> deny private ranges unless AllowPrivateNetworks.
  3. DisableEgress -> deny (allowlist empty by construction).
  4. Allowed domain match -> allow.
  5. Allowed CIDR match -> allow.
  6. Denied CIDR match -> deny.
  7. Default -> DENY (this is the key hardening).

type Proxy

type Proxy struct {
	// contains filtered or unexported fields
}

Proxy is the running egress filter.

func NewProxy

func NewProxy(cfg ServeConfig) *Proxy

NewProxy builds a Proxy, loading the initial policy (fail-closed if absent) and injection config (empty if absent).

func (*Proxy) Serve

func (p *Proxy) Serve(ctx context.Context) error

Serve starts the three listeners and the policy hot-reload watcher, blocking until ctx is cancelled.

type RedirectConfig

type RedirectConfig struct {
	ProxyUID  int // sidecar runs as this uid; its own egress is exempted (no loop)
	HTTPPort  int
	TLSPort   int
	OtherPort int
}

RedirectConfig parameterizes the iptables nat rules the init container installs in the Pod network namespace.

type Secrets added in v0.0.8

type Secrets struct {
	// SandboxID records which sandbox this config was pushed for. Informational.
	SandboxID string `json:"sandboxId,omitempty"`

	// CACertPEM / CAKeyPEM are the per-sandbox CA used to mint leaf
	// certificates for the hosts being intercepted. Absent means TLS
	// interception is impossible and only plaintext :80 rules can apply.
	CACertPEM string `json:"caCertPem,omitempty"`
	CAKeyPEM  string `json:"caKeyPem,omitempty"`

	// Rules is the per-host injection table.
	Rules []InjectRule `json:"rules,omitempty"`

	// Substitutions maps a placeholder handed to the sandbox to the real
	// credential. A placeholder is only ever swapped on a host whose rule
	// lists it, so the real value cannot be steered to another destination.
	Substitutions map[string]string `json:"substitutions,omitempty"`
}

Secrets is the injection config the control plane pushes over the exec channel. Unlike Policy it carries live credential material, so it is written only to the sidecar's own tmpfs (mode 0600) and is never persisted in a Pod annotation, an env var, or a log line.

Header values arrive already resolved: the operator expands the CRD's "{{ cred }}" templates before pushing, so credential *names* never reach the sidecar and the data plane needs no template engine.

func LoadSecrets added in v0.0.8

func LoadSecrets(path string) (Secrets, error)

LoadSecrets reads and parses the secrets file. A missing or empty file yields an empty config with no error: that is the steady state before the first push and simply means "inject nothing", which is the safe default — the sandbox's request then goes out without a credential and the upstream rejects it.

func (*Secrets) Apply added in v0.0.8

func (s *Secrets) Apply(req *http.Request, rules []*InjectRule) ApplyOutcome

Apply rewrites req's headers according to the rule: placeholders are substituted first, then declared headers are injected. The order matters and is deliberate — an Override header wins over whatever substitution produced, while IfAbsent leaves it alone, which is what makes "placeholder first, header as fallback" expressible.

Substitution only touches header *values*, never the body or the query string: scanning a body would mean buffering the whole request, and a credential in a query string ends up in the upstream's access log.

func (*Secrets) Enabled added in v0.0.8

func (s *Secrets) Enabled() bool

Enabled reports whether any rule is configured.

func (*Secrets) InterceptHosts added in v0.0.8

func (s *Secrets) InterceptHosts() []string

InterceptHosts returns the lowercase hosts that require TLS termination — used to decide, from the SNI alone, whether a connection takes the MITM path or stays a byte-for-byte splice.

func (*Secrets) Intercepts added in v0.0.8

func (s *Secrets) Intercepts(host string, port int) bool

Intercepts reports whether host:port has any injection rule, i.e. whether the connection must take the TLS-terminating path instead of the plain splice.

func (*Secrets) MatchAll added in v0.0.8

func (s *Secrets) MatchAll(host string, port int) []*InjectRule

MatchAll returns every rule covering host:port, in declaration order.

A host may carry several rules (mirroring E2B's host -> rule-array wire shape), and all of them apply: stopping at the first match would silently drop the rest. Host comparison is exact and case-insensitive — a wildcard would hand the credential to whoever controls a matching subdomain, so the API layer refuses to write one and the data plane never interprets one.

type ServeConfig

type ServeConfig struct {
	PolicyPath  string
	SecretsPath string
	HTTPPort    int
	TLSPort     int
	OtherPort   int
	Logger      *slog.Logger
}

ServeConfig configures the proxy listeners and policy source.

Jump to

Keyboard shortcuts

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