netguard

package
v2.2.2 Latest Latest
Warning

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

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

Documentation

Overview

Package netguard provides SSRF safeguards for outbound requests whose destination is derived from stored configuration (LLM upstream endpoints, OCI registries). Enforcement is opt-in via environment variables so that deployments proxying to internal LLMs (a first-class use case) keep working unchanged.

Internal-range blocking is enforced at dial time, on the exact IP address being connected (via net.Dialer.Control). This closes the DNS-rebinding TOCTOU window: there is no separate validate-then-resolve step for an attacker-controlled DNS server to exploit — the address that is checked is the address that is dialed.

Index

Constants

View Source
const (
	// EnvBlockInternal ("LLM_UPSTREAM_BLOCK_INTERNAL") enables blocking of
	// connections to internal/reserved IP ranges at dial time.
	EnvBlockInternal = "LLM_UPSTREAM_BLOCK_INTERNAL"

	// EnvAllowedHosts ("LLM_UPSTREAM_ALLOWED_HOSTS") is a comma-separated
	// allowlist of upstream hostnames. Entries starting with '.' match the
	// apex domain and any subdomain on a label boundary (".anthropic.com"
	// matches "anthropic.com" and "api.anthropic.com", never
	// "evil-anthropic.com"). When set, upstream hosts not on the list are
	// refused.
	EnvAllowedHosts = "LLM_UPSTREAM_ALLOWED_HOSTS"

	// EnvAllowInternal is the existing development flag that disables
	// internal-network restrictions platform-wide.
	EnvAllowInternal = "ALLOW_INTERNAL_NETWORK_ACCESS"

	// EnvAllowedInternalHosts ("LLM_UPSTREAM_ALLOWED_INTERNAL_HOSTS") names
	// upstream hosts that are exempt from internal-range blocking, using the
	// same matching as EnvAllowedHosts (".svc.cluster.local" matches every
	// in-cluster Service). It exists because the two existing controls cannot
	// express "reach this in-cluster model server, keep blocking everything
	// else internal": EnvAllowInternal disables the policy platform-wide, and
	// EnvAllowedHosts is a global allowlist, so using it to permit a cluster
	// host silently restricts every external provider not also listed.
	//
	// This is an additive exemption. It is consulted before EnvAllowedHosts and
	// widens the policy only for the hosts named, so an operator can run an
	// InferencePool-backed upstream alongside SaaS providers with egress
	// blocking left on.
	EnvAllowedInternalHosts = "LLM_UPSTREAM_ALLOWED_INTERNAL_HOSTS"
)

Variables

View Source
var ErrPolicyDial = errors.New("destination resolved to a blocked internal address")

ErrPolicyDial is returned from a policy-built HTTP client when the address a hostname resolved to is blocked. It is permanent: retrying will not change the answer.

View Source
var ErrURLPolicy = errors.New("url is not allowed by the outbound URL policy")

ErrURLPolicy is the default sentinel wrapped by every URLPolicy error. Features that need their own sentinel set PolicyOptions.Err.

Functions

func DialControl

func DialControl(network, address string, _ syscall.RawConn) error

DialControl is a net.Dialer Control hook that rejects connections to internal IP ranges when blocking is enabled. It runs after DNS resolution, once per connection attempt, on the exact address being dialed — so a DNS answer that changes between validation and connection cannot bypass it.

func HTTPTransport

func HTTPTransport() *http.Transport

HTTPTransport returns an *http.Transport (based on http.DefaultTransport) whose dials are guarded by DialControl. Use it for any HTTP client whose destination comes from stored configuration.

The exemption is applied here rather than in DialControl because Control runs after DNS resolution and sees only an IP, whereas the transport hands DialContext the hostname it set out to reach — the same name ValidateUpstreamURL checked. Keying the exemption on that name is what makes a host allowlist expressible at all, and it does not reopen the DNS-rebinding window the Control hook closes: an exempted name is one the operator has declared may resolve internally, not one an attacker chose.

func HostMatches

func HostMatches(host, pattern string) bool

HostMatches compares a lower-cased host with a pattern: ".example.com" matches example.com and any subdomain; anything else is an exact match.

func IsInternalHostname

func IsInternalHostname(host string) bool

IsInternalHostname reports whether a hostname names a local or cluster-internal destination by convention.

func IsInternalIP

func IsInternalIP(ip net.IP) bool

IsInternalIP reports whether ip belongs to a private, loopback, link-local, unique-local, or unspecified range.

func NewDialer

func NewDialer() *net.Dialer

NewDialer returns a net.Dialer whose connections are guarded by DialControl.

func ValidateUpstreamURL

func ValidateUpstreamURL(u *url.URL) error

ValidateUpstreamURL validates an outbound upstream URL against the configured policy:

  1. Scheme must be http or https.
  2. If LLM_UPSTREAM_ALLOWED_INTERNAL_HOSTS names the host, it is permitted outright — this is the in-cluster exemption and it short-circuits the rules below.
  3. If LLM_UPSTREAM_ALLOWED_HOSTS is set, the hostname must match an entry (exact, case-insensitive; ".suffix" entries match the apex and subdomains on a label boundary).
  4. If internal blocking is enabled and the host is an IP literal, it must not be an internal address (an early, clear error; hostnames are enforced at dial time by DialControl/HTTPTransport, which also covers DNS-rebinding).

This function performs no DNS resolution, so it is cheap on the request path and cannot be raced against a later lookup.

func ValidatingHTTPTransport

func ValidatingHTTPTransport() http.RoundTripper

ValidatingHTTPTransport returns a RoundTripper that applies the full URL policy (scheme + host allowlist) to every request — including redirects and any request issued by wrapping clients — on top of HTTPTransport's dial-time internal-IP guard. Use it for clients where per-request URL validation cannot be guaranteed at the call sites.

Types

type PolicyOptions

type PolicyOptions struct {
	// AllowInternal permits loopback, RFC1918, link-local and other internal
	// destinations for every URL the policy checks.
	AllowInternal bool
	// AllowedHosts, when non-empty, restricts destinations to these hosts
	// (exact, or ".suffix" for a domain and its subdomains). A host named
	// exactly here may also be internal.
	AllowedHosts []string
	// DeniedHosts always rejects and wins over AllowedHosts.
	DeniedHosts []string
	// ExemptHosts are exact hosts permitted to be internal without opening
	// the policy for anything else (a per-object exemption, e.g. one
	// administrator-approved Dashboard on a private address).
	ExemptHosts []string
	// Err is the sentinel wrapped in every validation error. Defaults to
	// ErrURLPolicy.
	Err error
	// InternalHint is appended to "internal address" rejections so the
	// message names the switch that would allow it.
	InternalHint string
}

PolicyOptions configures a URLPolicy.

type URLPolicy

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

URLPolicy decides where an outbound request derived from stored configuration may go. It is always on, independent of the LLM_UPSTREAM_* environment gating in this package, because the URLs it guards are typed in by users and are an exfiltration vector.

Two layers: Validate checks the URL as written (scheme, userinfo, host, IP literals, allow/deny lists) whenever a URL is stored and again before every use; the client built by NewHTTPClient re-checks the resolved IP at connect time so a hostname that resolves to an internal address (or is re-pointed there later) is still blocked.

func NewURLPolicy

func NewURLPolicy(o PolicyOptions) *URLPolicy

NewURLPolicy builds a policy from options.

func (*URLPolicy) ExplicitlyAllowed

func (p *URLPolicy) ExplicitlyAllowed(host string) bool

ExplicitlyAllowed reports whether the operator named this exact host in the allow list or the exemptions; such hosts may be internal.

func (*URLPolicy) HostAllowed

func (p *URLPolicy) HostAllowed(host string) error

HostAllowed applies the deny list, then the allow list.

func (*URLPolicy) NewHTTPClient

func (p *URLPolicy) NewHTTPClient(timeout time.Duration) *http.Client

NewHTTPClient returns a client whose dialer rejects internal addresses at connect time (unless permitted), which never uses a proxy (a proxy would bypass the dial guard) and never follows redirects: a 3xx would send the request somewhere the administrator did not approve.

func (*URLPolicy) Validate

func (p *URLPolicy) Validate(raw string) (*url.URL, error)

Validate parses and checks a URL. The returned URL is normalised.

func (*URLPolicy) WithExemptHosts

func (p *URLPolicy) WithExemptHosts(hosts ...string) *URLPolicy

WithExemptHosts returns a copy of the policy that additionally permits the named hosts to be internal.

Jump to

Keyboard shortcuts

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