Help us shape the future of Go! Take the 2026 Go Developer Survey

egress

package
v0.15.1 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: AGPL-3.0 Imports: 19 Imported by: 0

Documentation

Overview

Package egress is what an egress rule means: how an entry is written, what it matches, and whether a set of rules lets a connection out (R-181 – R-187).

A leaf package. Core resolves an app's rules against host policy (policy.Document.EgressFor), a runtime adapter carries the result to where it is enforced, and the gateway (Gateway) enforces it. All three read the same definitions from here, so "does this entry match" cannot mean one thing when an app's plan is shown and another when a connection is refused.

Index

Constants

View Source
const DefaultPort = 3128

DefaultPort is where the gateway listens, and what a restricted app's HTTP_PROXY and HTTPS_PROXY name (R-187). 3128 because it is the port every forward proxy has listened on since Squid, and some clients guess it.

Variables

This section is empty.

Functions

func Carried added in v0.14.0

func Carried(a netip.Addr) netip.Addr

Carried is the address a connection to a really goes to, for deciding whether it may: without an IPv6 zone, which names an interface rather than another address, and as the IPv4 address it carries when it is one written as IPv6 — mapped (::ffff:10.0.0.1) or behind well-known NAT64 (64:ff9b::a00:1).

func OwnAddresses added in v0.14.0

func OwnAddresses() []netip.Addr

OwnAddresses lists what the gateway refuses as its own beyond loopback (Gateway.Own): the addresses of its network interfaces, and every router its routing table sends traffic through. On a Docker bridge network the router is the bridge's address on the host, so a connection to it reaches the host's own services rather than anywhere the app's rules describe.

Read from the gateway's own network namespace when it starts. What cannot be read is left out: the rules still apply to it.

func Private

func Private(a netip.Addr) bool

Private reports whether an address is one R-181's private-range switch refuses: RFC 1918, loopback, link-local, unique-local, carrier-grade NAT, "this network" (0.0.0.0/8, which Linux connects to the local host), the deprecated IPv6 site-local range, and the IANA special-purpose ranges that are never a public destination: IETF protocol assignments (192.0.0.0/24), benchmarking (198.18.0.0/15), reserved (240.0.0.0/4) and IPv6 discard-only (100::/64). Link-local covers 169.254.169.254, the cloud metadata address, which is the destination this switch most often exists to protect.

An IPv4 address carried inside IPv6 — mapped (::ffff:10.0.0.1) or behind NAT64 (64:ff9b::a00:1) — is judged by the IPv4 address it carries, or a private address could be reached by spelling it differently. An IPv6 zone (fe80::1%eth0) is ignored: it names an interface, not a different address, and netip.Prefix.Contains is false for every zoned address.

Types

type Compiled

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

Compiled is Rules with every entry parsed, ready to decide connections.

func (*Compiled) Allows

func (c *Compiled) Allows(host string, port int, addr netip.Addr) Decision

Allows decides one connection: to host (as the app asked for it, which may be an address literal), on port, at the address it resolved to.

func (*Compiled) AllowsName

func (c *Compiled) AllowsName(host string, port int) Decision

AllowsName reports whether a hostname could be let out on some address — the check made before resolving it. A denylist that names the host refuses it outright. An allowlist refuses it only when nothing on it could match: no entry names the host, and no address or range entry covers the port, so no address it could resolve to would be allowed. Otherwise the name is let through to be decided per address by Allows.

Refusing here, before the name is resolved, is not only cheaper. A gateway that resolved every name it was asked for would carry whatever an app spelled into a DNS query, to a resolver outside the app — a way out for data the allowlist exists to keep in.

type Decision

type Decision struct {
	Allowed bool
	// Reason is written for the app's owner reading a refused connection in
	// the gateway's log.
	Reason string
}

Decision is whether one connection may leave, and why not.

type Entry

type Entry struct {
	// Raw is the entry as written, which is how it is shown back.
	Raw string

	// Any is "*": every destination.
	Any bool

	// Host is an exact hostname, lower case, without a trailing dot.
	Host string

	// Wildcard is the suffix of "*.example.com" — "example.com" — and matches
	// any name strictly below it. Not the apex: "*.example.com" does not
	// match "example.com", which is how every certificate and every DNS
	// operator already reads a wildcard.
	Wildcard string

	// Prefix is an address or range. A bare address is a /32 or /128.
	Prefix netip.Prefix

	// Port restricts the entry to one port. Zero is every port.
	Port int
}

Entry is one destination in an egress list (R-185).

Exactly one of Host, Wildcard, Prefix or Any is set. A hostname entry matches the name an app asked for; an address or range entry matches where that name resolved.

func Parse

func Parse(raw string) (Entry, error)

Parse reads one entry.

The error is written for somebody who typed the entry, because they are who will see it (R-105).

func ParseAll

func ParseAll(raw []string) ([]Entry, []error)

ParseAll reads a list, reporting every entry that does not parse at once: somebody fixing a list should not learn about the second mistake only after fixing the first.

func (Entry) Key

func (e Entry) Key() string

Key is a canonical spelling, stable across ways of writing the same entry.

func (Entry) MatchesAddr

func (e Entry) MatchesAddr(a netip.Addr, port int) bool

MatchesAddr reports whether the entry matches an address a connection is going to. Hostname entries never match an address.

func (Entry) MatchesName

func (e Entry) MatchesName(host string, port int) bool

MatchesName reports whether the entry matches a hostname the app asked for. Address and range entries never match a name: they match where it resolved.

func (Entry) Same

func (e Entry) Same(o Entry) bool

Same reports whether two entries name the same destination, however each was written. Used to apply an app's removals to the install's list: removing "API.example.com." removes "api.example.com".

type Gateway

type Gateway struct {
	// Rules decide every connection. Required.
	Rules *Compiled

	// Logger receives one line per refusal. Nil logs nothing.
	Logger *zap.Logger

	// Resolver looks names up. Nil is the system resolver.
	Resolver Resolver

	// Own lists the gateway's own addresses beyond loopback: its interfaces,
	// and the address of the router its outbound network leaves through,
	// which on a bridge network is the host itself. Refused whatever the
	// rules say, as loopback is (OwnAddresses fills it).
	Own []netip.Addr

	// DialTimeout bounds one attempt to connect to one address, and
	// ResolveTimeout one name lookup. Zero is ten seconds for each.
	DialTimeout    time.Duration
	ResolveTimeout time.Duration
	// contains filtered or unexported fields
}

Gateway is the egress gateway: an HTTP forward proxy that lets a restricted app's traffic out only where its rules allow (R-185 – R-187).

It answers two kinds of request, the two that HTTP_PROXY and HTTPS_PROXY produce: CONNECT host:port, which opens a tunnel (HTTPS, and anything else a client chooses to tunnel), and a plain HTTP request in absolute form (GET http://host/path). Everything else is refused.

Each connection is decided the same way:

  1. The name the app asked for is checked on its own (Compiled.AllowsName), before anything is resolved — a denylisted name is never looked up.
  2. The name is resolved once, or taken as it is when it is an address.
  3. Only the addresses the rules allow (Compiled.Allows) are kept, and the connection is dialed to one of those addresses. Never to the name again: resolving a second time would let a name that answered with a public address when it was checked answer with 169.254.169.254 when it was dialed, which is how DNS rebinding walks around private-range blocking.

A refusal is a 403 whose body says why, in words an app owner can act on (R-105), and one structured log line.

func (*Gateway) ListenAndServe

func (g *Gateway) ListenAndServe(ctx context.Context, addr string) error

ListenAndServe runs the gateway on addr until ctx ends.

func (*Gateway) Serve

func (g *Gateway) Serve(ctx context.Context, ln net.Listener) error

Serve runs the gateway on a listener until ctx ends, then stops accepting, closes open tunnels and returns.

func (*Gateway) ServeHTTP

func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP handles one request to the gateway.

type Layer

type Layer struct {
	Mode Mode     `json:"mode"`
	List []string `json:"list"`
	// From says whose layer this is: "install" or "app". Shown in a refusal,
	// so an app owner reading "refused by the installation's allowlist" knows
	// the fix is not in their own settings.
	From string `json:"from,omitempty"`
}

Layer is one list and how to read it.

type Mode

type Mode string

Mode is how one layer of rules reads its list.

const (
	// Allowlist lets out only what the list names.
	Allowlist Mode = "allowlist"
	// Denylist lets out everything except what the list names.
	Denylist Mode = "denylist"
)

type Resolver

type Resolver interface {
	LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
}

Resolver is the part of net.Resolver the gateway uses.

type Rules

type Rules struct {
	Layers       []Layer `json:"layers,omitempty"`
	BlockPrivate bool    `json:"block_private,omitempty"`
}

Rules is everything that decides whether an app's connection may leave.

Every layer must allow a destination (R-182): the install's rules, with the app's additions and removals applied, and then the app's own list on top. A destination one layer refuses is refused. No layers and no private-range blocking is no restriction at all, and a runtime adapter puts nothing in the app's path (R-186).

func (Rules) Compile

func (r Rules) Compile() (*Compiled, error)

Compile parses every entry once. An entry that does not parse is an error rather than skipped: a gateway that silently dropped a denylist entry would let out exactly what somebody tried to stop.

func (Rules) Restricted

func (r Rules) Restricted() bool

Restricted reports whether these rules refuse anything.

func (Rules) Validate

func (r Rules) Validate() error

Validate checks every entry parses and every mode is known.

Jump to

Keyboard shortcuts

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