Documentation
¶
Overview ¶
Package netguard is the outbound network policy the agent dials its own connections through: a default-deny gate that decides whether a given address may be reached. It is the in-process half of egress control, the layer that stops a connection the agent itself makes from going somewhere it should not, including a server-side request forgery to a private or cloud-metadata address.
One Policy type expresses the range of needs: a sandboxed step that may reach nothing (the zero value denies everything), a download that may reach any public host but never a private or loopback one, and a run granted a specific range. The gate is enforced at the point of connect, after DNS resolution on the actual address being dialed, so a name that resolves to a denied address, including a rebinding attack, is blocked rather than trusted.
This guards connections the agent's own Go code makes. Confining a child process the agent launches (an inference runtime, say) to no network is a complementary, OS-level layer that belongs with the execution sandbox.
Index ¶
- func Client(p Policy) *http.Client
- func DialControl(p Policy) func(network, address string, c syscall.RawConn) error
- func DialControlContext(p Policy) func(ctx context.Context, network, address string, c syscall.RawConn) error
- func Dialer(p Policy) *net.Dialer
- func IsPublic(addr netip.Addr) bool
- func WithObserver(ctx context.Context, obs Observer) context.Context
- type Decision
- type Observer
- type Policy
- type Proxy
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Client ¶
Client builds a hardened HTTP client that dials only where p allows, follows a bounded number of redirects (re-applying the policy on each hop through the dial control) and refuses a non-https redirect, and honors no environment proxy so the policy stays authoritative. A request's own scheme is the caller's check; this guards where connections may go.
func DialControl ¶
DialControl returns a net.Dialer Control function that enforces p. Because Control runs after DNS resolution on the address actually being dialed, it blocks a name that resolves to a denied address (a rebinding attack included), not just a literal one.
func DialControlContext ¶
func DialControlContext(p Policy) func(ctx context.Context, network, address string, c syscall.RawConn) error
DialControlContext is the context-aware DialControl: it enforces p identically and, when an Observer is seeded on ctx, reports the decision to it before returning. It is the hook Dialer and Client install, so a run that seeds an observer records every dial its own code makes; a dial with no observer on its context reports nothing and behaves exactly as DialControl.
func Dialer ¶
Dialer returns a net.Dialer that gates every connection through p, for raw socket protocols that are not HTTP (a line-delimited or JSON-RPC service, say). It is the raw-socket counterpart of Client: a caller reaching a local, operator-supplied service over a loopback port grants exactly that address in p, and the policy still blocks anything else, including an address a name rebinds to.
func IsPublic ¶
IsPublic reports whether addr is a globally-routable public address. It rejects loopback, private (RFC1918 and IPv6 unique-local), link-local (which covers the 169.254.169.254 cloud metadata endpoint), multicast, the unspecified address, and the IANA special-purpose ranges above, so only an address that can legitimately be reached on the public internet is allowed.
Types ¶
type Decision ¶
Decision is one egress verdict netguard reached at dial time: the host it was asked to reach (the literal address resolved before connecting), whether the policy allowed it, and a short reason for the verdict. It is what an Observer is handed, so a run can record its own outbound network decisions without netguard depending on the recording layer.
type Observer ¶
type Observer func(Decision)
Observer is notified of each egress decision netguard makes on a dial. It is registered on a context (see WithObserver), so netguard reports a run's outbound verdicts without importing the recording layer, and a dial made outside any run reports to no one. It runs on the dial goroutine, so an implementation must not block.
type Policy ¶
type Policy struct {
// AllowPublic permits any globally-routable public address while still denying
// private, loopback, link-local, and the cloud metadata range. This is the
// anti-SSRF mode for reaching public sources.
AllowPublic bool
// Allow is an explicit allowlist of address ranges, permitted even when not
// public (a specific host's address, or a private range a run is granted).
Allow []netip.Prefix
// AllowHosts, when non-empty, restricts egress to these destination host names,
// checked on the name the client asked to reach before it is resolved. An entry is a
// host name matched case-insensitively; an entry beginning with a dot (".openai.com")
// also matches any subdomain of it. It composes with the address rules above rather
// than replacing them: a destination must pass both the name gate here and the
// resolved-address gate (AllowPublic/Allow), so an allowlisted name that resolves to a
// private or rebinding address is still denied. It is enforced where the destination
// name is known, the egress proxy a child process is pointed at, so it governs a
// process whose own code we do not control: "deny all egress except these providers".
// An empty AllowHosts imposes no name restriction (the address rules still apply).
AllowHosts []string
}
Policy decides which outbound addresses are reachable. The zero Policy denies everything (default-deny): a connection is allowed only if its address falls in an allowed range, or it is a public address and the policy permits public addresses.
func DenyAll ¶
func DenyAll() Policy
DenyAll is the default-deny policy: no outbound connection is permitted.
func PublicOnly ¶
func PublicOnly() Policy
PublicOnly permits public addresses and denies everything private (anti-SSRF).
func (Policy) AllowsHost ¶
AllowsHost reports whether host (a destination name without a port) passes the name allowlist. An empty allowlist permits any name, so the address rules alone decide; otherwise the name must equal an entry, or be a subdomain of a dotted entry (".example.com" matches example.com and any subdomain), compared case-insensitively. A trailing dot on a fully-qualified name is ignored. It is a name gate only: a caller still applies the address rules (Allows) on the resolved address, so this never widens what an address rule denies.
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy is a forward egress proxy that authorizes every upstream connection through a Policy, so a child process pointed at it (HTTP_PROXY / HTTPS_PROXY / ALL_PROXY) gets the same default-deny egress control the agent's own dials get. It is how the one egress policy engine governs a process whose own code we do not control: the child dials the proxy, the proxy enforces the policy after DNS resolution (rebinding-safe, via the same DialControl as Client and Dialer), and a denied destination is refused before any connection is made.
It speaks the standard proxy protocol: CONNECT to tunnel TLS (and any other TCP a client opens through CONNECT), and absolute-form requests to forward plain HTTP. It holds no listener of its own; a caller serves it on a loopback listener (through bindguard), so the proxy is reachable only on the host it runs on.
func NewProxy ¶
NewProxy returns a proxy that admits only what p allows. Upstream connections are dialed through a policy-enforcing dialer, so the address rules are applied at connect time on the resolved address (the same DialControl Client and Dialer use). The name allowlist (p.AllowHosts) is applied earlier, on the requested destination name, before the connection is dialed.