ecs

package
v1.8.2 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package ecs holds the policy and helpers for EDNS Client Subnet handling (RFC 7871). The package is deliberately tiny and free of DNS handlers, goroutines, or other side effects so middleware/edns (forwarding side) and middleware/cache (key-shape side) can both import it without bringing each other into their dependency graph.

RFC 7871 is opt-in by design — SDNS strips ECS by default to honour §11's privacy guidance, and the operator must enable forwarding explicitly via the ecs config block.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ReadResponseScope

func ReadResponseScope(resp *dns.Msg) (netip.Prefix, bool)

ReadResponseScope extracts the SCOPE prefix from an authoritative server's response OPT, expressed as a netip.Prefix the cache can key on. Returns (invalid, false) when:

  • the response has no OPT, or
  • the OPT has no EDNS0_SUBNET, or
  • the option's Address is missing / unparseable, or
  • SourceScope == 0 (RFC 7871 §6: "the answer is suitable for the entire address space", i.e. cache shared-key, not scoped).

The returned prefix is built from the option's Address truncated to SourceScope bits — that's the slice of IP space the authority is asserting the answer covers. The caller (cache insert path) can hand that prefix to Policy.ClampScope before keying.

Used by middleware/cache in Stage 2; lives here so middleware/edns and middleware/cache share one definition.

Types

type Policy

type Policy struct {
	// Enabled toggles ECS forwarding upstream. When false every
	// other field is ignored. Default false (RFC 7871 §11).
	Enabled bool

	// ForwardV4Max / ForwardV6Max are the ceilings on the
	// source-prefix-length we'll forward upstream. Clients that
	// send a narrower (more specific) prefix get clamped to these
	// values — narrower prefixes leak more information about the
	// client than the operator probably intends. Sensible defaults:
	// /24 for IPv4 and /56 for IPv6, matching common practice.
	ForwardV4Max uint8
	ForwardV6Max uint8

	// ClientNetworks restricts which clients are eligible for ECS
	// forwarding. Empty == every client is eligible. A non-empty
	// list lets an operator forward ECS only for known internal
	// load balancers or CDN edges while stripping for the open
	// internet.
	ClientNetworks []netip.Prefix

	// MinScopeV4 / MinScopeV6 are the Stage-2 cache safety knobs:
	// when caching a scoped answer, refuse to key on a prefix
	// narrower than this. Caps cache cardinality so a busy
	// resolver with diverse clients can't blow up the cache
	// budget on per-client scopes. Defaults match the forwarding
	// ceilings.
	MinScopeV4 uint8
	MinScopeV6 uint8
}

Policy captures the operator's decisions about how SDNS handles EDNS Client Subnet on inbound and outbound traffic. A nil *Policy represents the pre-7871 default: strip ECS on the way out, ignore SCOPE on the way back, do not key the cache on subnet. Methods on Policy tolerate a nil receiver, so the strip-only default doesn't need a sentinel.

func Build

func Build(
	enabled bool,
	forwardV4, forwardV6 uint8,
	minScopeV4, minScopeV6 uint8,
	clientNetworks []string,
) (*Policy, error)

Build constructs a Policy from raw config primitives, returning (nil, nil) when the feature is disabled and (nil, error) on any malformed input. Callers (middleware/edns, middleware/cache) log the error context themselves. Keeping the constructor here keeps internal/ecs at the bottom of the dependency graph — no config import — while preventing two middleware from drifting on what "enabled" means.

Bad input is fail-closed: a single typo'd CIDR or out-of-range source ceiling disables the entire policy. Forwarding off is always safer than forwarding too much.

func (*Policy) Allows

func (p *Policy) Allows(client netip.Addr) bool

Allows reports whether `client` is eligible for ECS forwarding under this policy. A nil or disabled policy never allows; otherwise an empty ClientNetworks means everyone, and a populated list means the client's address must fall inside one of the configured prefixes.

func (*Policy) Clamp

func (p *Policy) Clamp(in *dns.EDNS0_SUBNET) *dns.EDNS0_SUBNET

Clamp returns a normalised EDNS0_SUBNET safe to forward upstream under this policy, or nil when the input is unusable (no address, unsupported family). The returned option is always a fresh value — the caller can attach it to an outgoing OPT without aliasing the inbound request's storage.

Source-prefix is capped to ForwardV4Max / ForwardV6Max, then the address is truncated to that many bits so we don't accidentally leak the host portion if a client sent a /32 address with a /16 netmask (some implementations do that).

func (*Policy) ClampScope

func (p *Policy) ClampScope(scope, source netip.Prefix) netip.Prefix

ClampScope normalises an authoritative server's response SCOPE for cache-key purposes. Two RFC 7871 rules apply:

  • §7.1.2: a server MUST NOT return SCOPE > SOURCE. If it does (privacy violation), clamp down to SOURCE so the misbehaving authority can't widen our cache key past what we forwarded.

  • Stage-2 cardinality cap: scopes narrower than MinScopeV4 / MinScopeV6 are widened to the minimum, capping the worst-case entry count per name.

Used by middleware/cache when storing a scoped answer in Stage 2. A nil *Policy returns the input unchanged.

Jump to

Keyboard shortcuts

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