egress

package
v0.8.21 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package egress is the island outbound-network gate — the containment thesis applied to the one channel Dejima didn't yet observe or control: an island's traffic to the internet (the LLM API, git, package installs, and anything else the agent decides to reach). The daemon's existing gates (Port, capability, MCP, link) all cover an island reaching back INTO the host or other islands; this covers it reaching OUT.

The mechanism is a forward proxy the daemon runs and that islands are pointed at via HTTPS_PROXY. It attributes every connection to an island, applies a Policy, records the decision, and forwards allowed traffic. This is the hostname-level chokepoint — a firewall can later force all egress through it (so the policy can't be bypassed), but even alone it gives full visibility and cooperative control.

Phase 1 (this package) is observe-first: the default Policy is AllowAll, so nothing is blocked — the value is that the operator can finally SEE where each island connects. Phase 2 adds real allow/deny policy; the Policy seam is here so that lands without reworking the proxy.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ProxyEnv

func ProxyEnv(island, dial string) map[string]string

ProxyEnv returns the environment variables to inject into an island so its HTTP(S) clients route outbound through the egress proxy reachable at dial (e.g. "host.docker.internal:7280"). The island name is the proxy username, so attribution travels with every request — no source-IP registry or per-island listener. Phase 1 is observe-only, so the password is a fixed placeholder the proxy doesn't validate (token-checked attribution arrives with enforcement).

Both upper- and lower-case forms are set: curl/git/wget read the lowercase names, many language HTTP libs read the uppercase. NO_PROXY exempts loopback and the daemon host so the in-island autonomy/telemetry path (DEJIMA_HOST = host.docker.internal) and localhost don't route through — or loop back into — the proxy.

Types

type AllowAll

type AllowAll struct{}

AllowAll is the observe-first Phase-1 policy: log everything, block nothing.

func (AllowAll) Allow

func (AllowAll) Allow(_, _ string) bool

Allow always permits — Phase 1 never blocks.

type Decision

type Decision string

Decision is the gate's verdict for one outbound connection.

const (
	DecisionAllow Decision = "allow"
	DecisionDeny  Decision = "deny"
)

type Event

type Event struct {
	Island   string    `json:"island"`
	Host     string    `json:"host"`           // destination hostname
	Port     string    `json:"port,omitempty"` // destination port ("443", "80", …)
	Method   string    `json:"method"`         // "CONNECT" (HTTPS tunnel) or the HTTP verb
	Decision Decision  `json:"decision"`
	Time     time.Time `json:"time"`
}

Event is one recorded outbound connection attempt from an island. It carries the destination by HOST (not full URL/path) and never any payload — egress observability is "who did it talk to," not wiretapping its content. For HTTPS (a CONNECT tunnel) the host is all the proxy sees anyway.

type IslandPolicy

type IslandPolicy struct {
	Mode  Mode     `json:"mode"`            // "" == observe
	Allow []string `json:"allow,omitempty"` // permitted hosts (used in enforce mode)
	Deny  []string `json:"deny,omitempty"`  // always-blocked hosts (even in observe mode)
}

IslandPolicy is one island's egress rules. Host matching is by exact hostname OR domain suffix — a rule "github.com" also covers "api.github.com" — so an operator allows a service with one entry, not one per subdomain/CDN node.

type Log

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

Log is an in-memory, per-island ring of recent egress events. It bounds memory (a chatty island can't grow it unbounded) and is the source for the read API. It implements Recorder.

func NewLog

func NewLog(maxPerIsland int) *Log

NewLog returns a log retaining up to maxPerIsland events per island.

func (*Log) List

func (l *Log) List(island string) []Event

List returns a copy of the retained events for one island, oldest first.

func (*Log) Record

func (l *Log) Record(e Event)

Record appends an event, trimming the island's ring to the cap. Events with an empty Island are dropped (unattributed traffic isn't useful and shouldn't accumulate under "").

type Mode

type Mode string

Mode is an island's egress posture.

const (
	// ModeObserve (the default, incl. the zero value) allows all egress and just
	// records it — the Phase 1 behavior. A Deny list still blocks specific hosts.
	ModeObserve Mode = "observe"
	// ModeEnforce is deny-all: only hosts on the Allow list are permitted. This is
	// the containment posture (mirrors Port/MCP deny-all + operator grants), but
	// opt-in per island because an island legitimately needs some egress.
	ModeEnforce Mode = "enforce"
)

type Policy

type Policy interface {
	// Allow reports whether island may open a connection to host. Implementations
	// must be safe for concurrent use (the proxy calls it per connection).
	Allow(island, host string) bool
}

Policy decides whether an island may reach a destination host. Phase 1 wires AllowAll; Phase 2 replaces it with an operator-grant-backed policy. Keeping it an interface is the whole point of shipping the proxy before the policy.

type PolicyError

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

PolicyError is a validation failure from Apply.

func (*PolicyError) Error

func (e *PolicyError) Error() string

type PolicyPatch

type PolicyPatch struct {
	Mode        Mode     `json:"mode,omitempty"`
	AddAllow    []string `json:"add_allow,omitempty"`
	RemoveAllow []string `json:"remove_allow,omitempty"`
	AddDeny     []string `json:"add_deny,omitempty"`
	RemoveDeny  []string `json:"remove_deny,omitempty"`
}

PolicyPatch is an incremental change applied to an island's policy (the body of PATCH .../egress/policy). Empty Mode leaves the mode unchanged; the add/ remove lists are applied as sets (idempotent — re-adding is a no-op).

type PolicyStore

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

PolicyStore is a persistent, per-island egress policy. It implements Policy, so the proxy consults it per connection; the daemon's API mutates it. With no rules for an island it reports observe/allow-all, so enabling Phase 2 changes nothing until an operator sets a policy.

func OpenPolicy

func OpenPolicy(path string) *PolicyStore

OpenPolicy loads the persisted egress policy from path (missing/corrupt → empty, i.e. observe-all for every island). Mutations persist back to path so policy survives a daemon restart.

func (*PolicyStore) Allow

func (s *PolicyStore) Allow(island, host string) bool

Allow implements Policy: deny-list wins always; in enforce mode only allow- listed hosts pass; observe (and the zero value) permits everything.

func (*PolicyStore) Apply

func (s *PolicyStore) Apply(island string, patch PolicyPatch) (IslandPolicy, error)

Apply mutates an island's policy with the patch and persists, returning the resulting policy. An unknown Mode (not observe/enforce/"") is rejected.

func (*PolicyStore) Get

func (s *PolicyStore) Get(island string) IslandPolicy

Get returns a copy of an island's policy (zero value = observe, no rules).

type Proxy

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

Proxy is the island egress forward proxy: an http.Handler that serves both plain-HTTP proxy requests (absolute-URI form) and HTTPS CONNECT tunnels. For each connection it attributes the island (from Proxy-Authorization), asks the Policy, records an Event, and — if allowed — forwards the traffic. It is the daemon-run chokepoint islands are pointed at via HTTPS_PROXY.

It deliberately does NOT terminate TLS: HTTPS is a blind CONNECT tunnel, so the proxy sees the destination host but never the encrypted payload. Egress control here is "who can it talk to," not content inspection.

func NewProxy

func NewProxy(rec Recorder, pol Policy) *Proxy

NewProxy builds a proxy recording to rec and gated by pol. Nil rec/pol default to a discard recorder and AllowAll (the observe-first posture), so a zero-config proxy is safe and never nil-panics.

func (*Proxy) ServeHTTP

func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request)

type Recorder

type Recorder interface{ Record(Event) }

Recorder receives egress events. The daemon plugs in a Log (for the read API) and may also fan out to the ledger. A nil Recorder is a valid no-op via (*Log)(nil)-style guards at the call sites, but the proxy always holds a non-nil one (it defaults to a discard recorder).

Jump to

Keyboard shortcuts

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