Documentation
¶
Overview ¶
Package netguard refuses outbound connections to destinations a caller picked but must not be able to reach: loopback, private, link-local, cloud-metadata, CGNAT, multicast, reserved and documentation ranges, the in-cluster and internal hostnames, and the IPv4 addresses hidden inside IPv6 transition forms (mapped, compatible, translated, NAT64, 6to4, Teredo).
Use it wherever a server dials an address a customer or end user supplied: a webhook URL, a model endpoint, a cluster API server. The refused set is one predicate, Refused, so the hostname check, the URL check and the dialer cannot disagree about a range.
What is the boundary ¶
The dialer. Policy.DialContext (and HTTPClient, built on it) checks the address the socket is about to connect to, after DNS resolution, so there is no window between "the name resolved to a public IP" and "the socket connected to a private one": DNS rebinding, wildcard-DNS names such as 10-0-0-1.nip.io, and anything else that changes what a name means lose. Behind it sits the network policy of the deployment (an egress NetworkPolicy matches the destination IP the same way) for the processes that dial through code that is not yours.
Policy.CheckAddress is NOT a security boundary. It resolves the name once, at validation time, and the answer can differ by the time anything dials. It exists to reject a bad address early with an error that names the host and the range, so a user is told at submit time rather than by a failed converge. Never rely on it alone.
Usage ¶
policy := netguard.Policy{Ports: []uint16{443, 6443}}
if err := policy.CheckAddress(ctx, req.Server, nil); err != nil {
return svcerr.Wrap(connect.NewError(connect.CodeInvalidArgument, err))
}
client := netguard.HTTPClient(policy, 10*time.Second) // follows no redirect, ignores proxy variables
Policy.Allow is the exception list for destinations the platform itself must reach inside an otherwise refused range (an in-cluster endpoint, a proxy). It admits IP addresses only; the hostname rules are never relaxed.
Index ¶
- Variables
- func HTTPClient(p Policy, timeout time.Duration) *http.Client
- func Refused(ip netip.Addr) (reason string, refused bool)
- func RefusedHost(host string) (reason string, refused bool)
- type Policy
- func (p Policy) CheckAddress(ctx context.Context, rawURL string, resolver Resolver) error
- func (p Policy) DialContext(base *net.Dialer) func(ctx context.Context, network, addr string) (net.Conn, error)
- func (p Policy) Refused(ip netip.Addr) (reason string, refused bool)
- func (p Policy) RefusedHost(host string) (reason string, refused bool)
- func (p Policy) Transport() *http.Transport
- type Resolver
Constants ¶
This section is empty.
Variables ¶
var ErrRedirectRefused = fmt.Errorf("%w: redirects are not followed", ErrRefused)
ErrRedirectRefused is returned by the client from HTTPClient when a response is a redirect. It wraps ErrRefused.
var ErrRefused = errors.New("netguard: destination refused")
ErrRefused is returned (wrapped) whenever a destination is refused. The message names the host the caller supplied and the range it fell in, never anything else about the process's network.
Functions ¶
func HTTPClient ¶
HTTPClient returns a client for user-chosen addresses: guarded dialer, no proxy, and no redirect followed. A redirect is a second destination the caller never vetted, so it fails with ErrRedirectRefused instead.
func Refused ¶
Refused reports whether ip is not a routable public unicast address, and names the refused range. An IPv6 address that carries an IPv4 address (IPv4-mapped, -compatible and -translated, NAT64, 6to4, Teredo) is judged by the IPv4 address inside it too, so wrapping 169.254.169.254 in a transition prefix does not make it reachable. The zero Addr is refused.
func RefusedHost ¶
RefusedHost applies the hostname rules to host, which is a bare host as it appears in a URL: a name, an IP literal, or a bracketed IPv6 literal. A literal is judged by Refused. A name is refused when it is localhost or under .localhost, .internal, .local, .svc or .cluster.local; when it has a single label (the system resolver would expand it through its search domains); or when it is a numeric spelling the system resolver may read as an IPv4 address (2130706433, 0x7f.0.0.1, 127.1). A name that passes still has to be resolved and every answer checked: see Policy.CheckAddress.
Types ¶
type Policy ¶
type Policy struct {
// Allow lists prefixes admitted before the refused set is consulted: the
// platform exemption. An address inside one is admitted whatever range it
// is in. It never relaxes the hostname rules, the scheme or the port.
Allow []netip.Prefix
// Ports is the set of destination ports; empty means {443}. It applies to
// [Policy.CheckAddress] and to the dialer.
Ports []uint16
}
Policy decides which destinations a caller-chosen address may reach. The zero value refuses the whole refused set and admits https on port 443.
func (Policy) CheckAddress ¶
CheckAddress validates a user-chosen URL: https, a port in p.Ports, no userinfo, a host that passes Policy.RefusedHost, and, for a name, a resolution (A and AAAA, 5s) in which EVERY answer passes Policy.Refused. A name that does not resolve, resolves to nothing, or cannot be resolved is refused: failing closed here costs one retry, failing open costs the network. A nil resolver is net.DefaultResolver.
CheckAddress is for error messages. It is not the boundary: the answer it saw can change before anything dials. The dialer (Policy.DialContext) and the network policy are the boundary.
func (Policy) DialContext ¶
func (p Policy) DialContext(base *net.Dialer) func(ctx context.Context, network, addr string) (net.Conn, error)
DialContext returns a dial function that refuses to connect to a destination the policy forbids. The check runs through the dialer's Control hook, on the address the socket is actually about to connect to, so it holds whatever name was used and however it resolved. base supplies the dialer's other settings (timeouts, resolver, local address); nil is a dialer with a 10s timeout and 30s keep-alive. base is copied, never modified, and a Control or ControlContext it carries still runs after the guard admits the address.
func (Policy) Refused ¶
Refused is Refused with the exception list applied first: an address inside a prefix of p.Allow is admitted.
func (Policy) RefusedHost ¶
RefusedHost is the package-level RefusedHost with p.Allow applied to IP literals. The hostname rules are never relaxed by Allow.
func (Policy) Transport ¶
Transport returns an http.Transport that dials through Policy.DialContext and never uses an ambient proxy: a proxy would be dialed instead of the destination, defeating the guard. Each call returns a new Transport.