destination

package
v1.0.215 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package destination is the PR-7 destination-safety engine: destination extraction, URL/host/IP canonicalization, immutable destination policy, an INJECTED DNS resolver, connect-time pinned resolution + peer verification (MCP-INSP-005 rebinding guard), and a shared reusable redirect guard (MCP-INSP-006). SSRF classification reuses the AUTHORITATIVE private/internal ranges from internal/ssrf (ssrf.PrivateIP) — there is no second, divergent private-address table.

Load-bearing properties, all asserted by tests:

  • Reuse the one SSRF table. Every private/link-local/metadata/loopback/ reserved/multicast decision goes through ssrf.PrivateIP (and the same ssrf.Control at connect). No parallel CIDR list.
  • Pinned resolve→connect. Resolution happens ONCE (Resolve) and produces an immutable PinnedDestination whose permitted IP set the connect-time VerifyPeer checks; the core never re-resolves a hostname for connect. A DNS answer that mixes public and private addresses fails closed as a whole.
  • Injected resolver. The core NEVER uses net.DefaultResolver directly; the resolver is an interface a caller injects, so unit tests use no real DNS.
  • Canonical facts. Canonicalization rejects userinfo, fragments-as-policy, malformed ports, control chars, ambiguous percent-encoding, non-canonical numeric-IP spellings, IPv6 zone identifiers, and (in V1) non-ASCII hosts; policy facts use exact canonical values.
  • Request-local redirect state. The redirect guard holds per-request state (hop count, origin); there is one shared implementation, not per-client copies, and every hop is independently re-canonicalized, re-SSRF-checked and re-pinned.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Canonicalize

func Canonicalize(raw string, pol Policy, lim limits.InspectionLimits) (Canonical, Class, error)

Canonicalize validates and canonicalizes a raw destination URL under the immutable policy and limits, BEFORE any resolution. It returns the canonical facts and the destination Class (ClassPublic/private/… for an IP literal, or ClassUnknown for a hostname that must still be resolved). It rejects — with a typed mcperr — a blocked scheme, userinfo, a fragment, a missing host, a malformed/over-bound URL, control chars, ambiguous percent-encoding, a non-canonical numeric-IP spelling, an IPv6 zone identifier, or (in V1) a non-ASCII host.

func Resolve

Resolve performs bounded, pinned resolution of a canonical destination. For an IP literal it pins the single address; for a hostname it resolves via the injected resolver, rejects an empty/oversized/malformed answer, fails the WHOLE answer closed if ANY address is private/forbidden (mixed public/private is a single private-address poison), and builds an immutable PinnedDestination. It never re-resolves for connect — VerifyPeer checks the pinned set. now + ttl bound the pin lifetime. Resolution is single-shot; there is no per-address goroutine.

func VerifyPeer

func VerifyPeer(pin PinnedDestination, peer netip.Addr, pol Policy, now time.Time) error

VerifyPeer is the connect-time rebinding guard (MCP-INSP-005). It confirms the actual peer (1) still passes the AUTHORITATIVE ssrf.Control (a private peer is refused even if it somehow reached the pin), (2) is a MEMBER of the immutable pinned set (the resolved answer did not change between resolve and connect), and (3) the pin is not stale. Any failure fails closed with a typed reason. It does NOT re-resolve the hostname.

Types

type Candidate

type Candidate struct {
	Path    string // bounded JSON-pointer-like location
	RawURL  string // the raw string (canonicalized by the caller; never trusted)
	Modeled bool   // true: from an explicit compiled rule; false: heuristic backstop
}

Candidate is one extracted destination-bearing string from a value.

type Canonical

type Canonical struct {
	Scheme string
	Host   string // canonical host (lowercased ASCII, or canonical IP literal text)
	Port   string // explicit or scheme-default port, always present
	IsIP   bool
	IP     netip.Addr // valid iff IsIP
	// HasQuery/HasUserinfo/HasFragment are booleans (facts), never the values.
	HasQuery bool
	// contains filtered or unexported fields
}

Canonical is the immutable canonicalized destination. It holds only exact canonical facts — never the raw URL, never a query string with secrets.

func (Canonical) Origin

func (c Canonical) Origin() string

Origin returns the canonical scheme://host:port origin string (safe evidence).

func (Canonical) RequestURL added in v1.0.185

func (c Canonical) RequestURL() string

RequestURL returns the full canonical request URL (origin + path + optional query) a client must POST to. Unlike Origin() this is NOT evidence — it is the validated destination the pinned transport connects to, so it preserves any path a Streamable-HTTP server was mounted under (e.g. /mcp). A path-less endpoint yields the origin with a single "/" (the pre-existing default). The path/query stay unexported for evidence; RequestURL is the ONLY sanctioned surface that reconstructs them, for the connect leg only.

type Class

type Class uint8

Class is the safe, bounded destination classification used as a policy fact and in evidence. It never carries the raw host/URL.

const (
	// ClassUnknown — not classified (fails closed for high-risk).
	ClassUnknown Class = iota
	// ClassPublic — a public, routable address (the only permitted class).
	ClassPublic
	// ClassLoopback — 127.0.0.0/8, ::1.
	ClassLoopback
	// ClassPrivate — RFC1918 / ULA / CGN internal ranges.
	ClassPrivate
	// ClassLinkLocal — 169.254/16, fe80::/10 (link-local).
	ClassLinkLocal
	// ClassMetadata — a cloud metadata endpoint (169.254.169.254 / fd00:ec2::254).
	ClassMetadata
	// ClassReserved — unspecified/reserved/benchmark ranges.
	ClassReserved
	// ClassMulticast — multicast ranges.
	ClassMulticast
	// ClassBlockedScheme — a rejected URL scheme (file/data/javascript/…).
	ClassBlockedScheme
	// ClassMalformed — a URL/host/IP that failed canonicalization.
	ClassMalformed
)

func (Class) Permitted

func (c Class) Permitted() bool

Permitted reports whether a class may be dialed (only ClassPublic).

func (Class) String

func (c Class) String() string

String returns the stable class label.

type ExtractionRules

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

ExtractionRules is an immutable, compiled set of explicit destination-extraction pointers plus an optional conservative name-heuristic backstop. Destinations are NOT discovered by scanning arbitrary strings for "http": extraction is tied to explicit schema-property paths (JSON pointers). The name heuristic is a CONSERVATIVE BACKSTOP only — a heuristic hit is reported as an unmodeled destination, never used as the sole allow mechanism.

func CompileRules

func CompileRules(pointers []string, heuristic bool, lim limits.InspectionLimits) (ExtractionRules, error)

CompileRules compiles explicit JSON-pointer extraction paths (bounded by MaxExtractionPaths) plus the optional heuristic backstop.

func (ExtractionRules) Extract

Extract returns the destination candidates in v under the compiled rules, bounded by MaxExtractedDests. Explicit-rule hits are Modeled; heuristic hits are not. Deterministic order: explicit rules first (in rule order), then heuristic hits in document order.

type PinnedDestination

type PinnedDestination struct {
	Scheme           string
	Host             string
	Port             string
	AllowedIPs       []netip.Addr // every entry already passed the SSRF check
	ResolverRevision uint64       // profile/resolver revision the pin was made under
	Expiry           time.Time    // bounded pin lifetime (resolver deadline)
}

PinnedDestination is the immutable resolve-time pin the connect leg verifies against. It is produced ONCE by Resolve and consumed by VerifyPeer; a future upstream client MUST dial only an address in AllowedIPs, never re-resolve Host.

func (PinnedDestination) Contains

func (p PinnedDestination) Contains(ip netip.Addr) bool

Contains reports whether ip is a member of the pinned permitted set.

func (PinnedDestination) Stale

func (p PinnedDestination) Stale(now time.Time) bool

Stale reports whether the pin has expired relative to now (fail closed on a zero/expired deadline).

type Policy

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

Policy is an immutable destination policy. Its scheme set is stored as a sorted slice (tiny; linear membership) so the value shares no mutable map with callers.

func DefaultGatewayPolicy

func DefaultGatewayPolicy() Policy

DefaultGatewayPolicy returns the narrow production Gateway destination policy: https only, no private, no cross-origin redirect, no scheme downgrade.

func NewPolicy

func NewPolicy(cfg PolicyConfig) (Policy, error)

NewPolicy validates cfg and returns an immutable Policy. It rejects an empty scheme allowlist (fail closed) and any forbidden scheme.

func (Policy) AllowPrivate

func (p Policy) AllowPrivate() bool

AllowPrivate reports the test/env-scoped private-destination flag.

func (Policy) ResolverRevision

func (p Policy) ResolverRevision() uint64

ResolverRevision returns the revision pins are stamped with.

type PolicyConfig

type PolicyConfig struct {
	// Schemes is the allowlist (e.g. {"https"}). Empty ⇒ fail closed (no scheme
	// permitted). A forbidden scheme in the list is a hard configuration error.
	Schemes []string
	// AllowPrivate permits private/internal destinations. It is TEST- or
	// ENVIRONMENT-scoped ONLY; a production policy must leave it false. There is no
	// broad production allow_private bypass.
	AllowPrivate bool
	// AllowCrossOriginRedirect permits a redirect to a different origin.
	AllowCrossOriginRedirect bool
	// AllowSchemeDowngrade permits an https→http redirect (default false).
	AllowSchemeDowngrade bool
	// ResolverRevision stamps pins made under this policy.
	ResolverRevision uint64
}

PolicyConfig is the mutable input to NewPolicy.

type RedirectEvidence

type RedirectEvidence struct {
	Hop        int
	FromOrigin string
	ToOrigin   string
	DestClass  Class
}

RedirectEvidence is one bounded, safe redirect-hop record (no raw URL, only the canonical origin and hop index).

type RedirectGuard

type RedirectGuard struct {
	Evidence []RedirectEvidence
	// contains filtered or unexported fields
}

RedirectGuard is the single, shared, reusable redirect guard (MCP-INSP-006). It holds REQUEST-LOCAL state (hop count, previous origin, visited set) — there is one implementation, never a per-client copy. A future upstream MCP client consumes this guard: every hop is independently re-canonicalized, re-SSRF-checked (for IP-literal targets) and must be re-pinned by the caller (Resolve on the returned Canonical), authorization is never carried across an origin (cross-origin is rejected by default), and the chain is bounded.

func NewRedirectGuard

func NewRedirectGuard(initial Canonical, pol Policy, lim limits.InspectionLimits) *RedirectGuard

NewRedirectGuard starts a guard at the initial canonical destination.

func (*RedirectGuard) ForwardAuthAllowed

func (g *RedirectGuard) ForwardAuthAllowed(next Canonical) bool

ForwardAuthAllowed reports whether an authorization/credential header may be forwarded to next given the current origin. It is false across origins — the future client must strip credentials on any cross-origin hop.

func (*RedirectGuard) Next

func (g *RedirectGuard) Next(location string) (Canonical, error)

Next validates ONE redirect hop given the Location value (absolute or relative). It returns the new canonical destination (which the caller MUST re-resolve and re-pin) or a typed rejection. It rejects hop-count overflow, loops, scheme downgrade, userinfo, cross-origin (unless allowed), a malformed relative target, and — for an IP-literal target — a public→private/metadata escape.

type Resolver

type Resolver interface {
	// LookupIP resolves host to a set of IP addresses (already deduplicated is not
	// required). It MUST honor ctx (deadline/cancel). It returns addresses only —
	// never CNAME chains — so CNAME depth is bounded inside the implementation.
	LookupIP(ctx context.Context, host string) ([]netip.Addr, error)
}

Resolver is the INJECTED DNS resolver. The core inspector never uses net.DefaultResolver directly — a caller supplies a bounded implementation, so unit tests use no real external DNS. Production wiring passes an adapter over a bounded net.Resolver.

type Status

type Status struct {
	Class     Class
	Canonical Canonical
	Reason    mcperr.Reason // ReasonNone when permitted
}

Status is the safe result of destination inspection for one candidate.

Jump to

Keyboard shortcuts

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