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

netguard

package
v1.0.14 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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

func HTTPClient(p Policy, timeout time.Duration) *http.Client

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

func Refused(ip netip.Addr) (reason string, refused bool)

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

func RefusedHost(host string) (reason string, refused bool)

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

func (p Policy) CheckAddress(ctx context.Context, rawURL string, resolver Resolver) error

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

func (p Policy) Refused(ip netip.Addr) (reason string, refused bool)

Refused is Refused with the exception list applied first: an address inside a prefix of p.Allow is admitted.

func (Policy) RefusedHost

func (p Policy) RefusedHost(host string) (reason string, refused bool)

RefusedHost is the package-level RefusedHost with p.Allow applied to IP literals. The hostname rules are never relaxed by Allow.

func (Policy) Transport

func (p Policy) Transport() *http.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.

type Resolver

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

Resolver looks up the A and AAAA records of a host. *net.Resolver satisfies it; tests inject a fake.

Jump to

Keyboard shortcuts

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