selfdefense

package
v0.1.0-rc.1 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package selfdefense is the standard-HTTP driving adapter for ingress self-defense. It owns the request-side classification (fixed impossible-path matching and adaptive quarantine lookup) and the generic refusal responses; the provider/protocol-neutral state machine lives in internal/core/ingressdefense and the transport-auth outcome observation lives in internal/stdhttp/auth.

The gate runs outside tracing, general HTTP metrics, request ID, access logging, transport auth and the route mux, so a deterministic probe refusal and an adaptive quarantine refusal both cost one address parse, one fixed matcher pass and at most one bounded state lookup. It performs no DNS lookup, no reputation query, no feed download and no database or other request-driven network I/O.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ImpossiblePath

func ImpossiblePath(path string) bool

ImpossiblePath reports whether the decoded request-target path matches any frozen impossible-path family, ignoring the routes this generation owns.

Use ImpossiblePathExcept for the gate's decision, which additionally carves the routes the router actually owns. This function is the frozen rule set on its own.

path must be the decoded path (url.URL.Path) of the request target, which net/http has already percent-decoded exactly once; passing a raw escaped target is a caller error. Matching uses only this path structure: it never reads the request body, canonical messages or items, tool arguments, SQL or code text, and never an arbitrary query value, and it retains no part of the path.

A decoded path can itself contain a question mark, because one escaped "?" in the request target decodes into a literal "?" plus the attacker's remaining text. normalizeTarget therefore cuts at the first "?" itself, so query-independence is a structural property of the matcher rather than a caller obligation, and no rule family can reintroduce the leak. The accepted trade-off is that a probe smuggled behind an escaped "?" is treated as query text and therefore evades this deterministic classifier and accrues no adaptive probe offense; it still reaches no route, because the router sees the same decoded "?" inside the path.

The decision is deterministic and allocation-free. It is case-insensitive over ASCII and ignores repeated leading slashes, because the standard distribution is case-sensitive and never normalizes a target into any of these families.

func ImpossiblePathExcept

func ImpossiblePathExcept(path, method string, owned OwnedRoutes) bool

ImpossiblePathExcept is the gate's decision: the frozen families, minus the routes the router actually owns.

owned is the pre-compiled inventory for this generation. It is a value, not a slice, precisely so the request path performs no per-request normalization and no allocation: an attacker controls how often this branch runs. The zero value carves nothing and is therefore exactly ImpossiblePath.

func Middleware

func Middleware(in Input, next http.Handler) http.Handler

Middleware returns the early ingress self-defense gate.

Per request the order is normative:

  1. resolve the source address with the existing GeoIP resolver semantics; a resolver failure returns a generic 403 and stops;
  2. attach the normalized source address to the request context;
  3. when impossible-path matching is enabled and the path matches and is not a route this generation publishes, record one probe offense for a non-adaptively-exempt source, record finite metrics and return a generic 404;
  4. an adaptively-exempt source delegates immediately, without inspecting adaptive quarantine state;
  5. a source that is not quarantined delegates;
  6. a quarantined source is refused with a generic 429 only when the conservative credential probe proves it cannot authenticate as presented, and otherwise reaches the normal transport-auth chain.

Adaptive exemption is applied before both the quarantine lookup and the probe offense, and never overrides fixed network policy or the deterministic impossible-path refusal.

func OwnedRoutesFromMux

func OwnedRoutesFromMux(mux *http.ServeMux, candidates []httpcontract.OwnedRouteCandidate) []httpcontract.OwnedRoute

OwnedRoutesFromMux resolves which of the candidates the router actually owns, and with which match semantics, by asking the router itself.

Ownership is a property of the router, not of the configuration that produced the candidate, so this is resolved against the real *http.ServeMux rather than inferred from a path's shape. That is what makes the carve faithful:

  • a configured path whose feature is disabled is not mounted, resolves to no dedicated pattern, and therefore carves nothing;
  • a literal registration is recorded as exact, and a trailing-slash registration is recorded as a subtree, because that is what the router actually does with it;
  • the method is carried through, so a route registered for one method is not treated as owning the other methods.

The router's fallback is never ownership: every path it does not route resolves to a fallback handler, so a fallback pattern would otherwise carve the entire data plane. The bare root pattern and the empty pattern are therefore dropped.

Resolution happens once per generation, at composition, and never on the request path.

Types

type CredentialProbe

type CredentialProbe func(r *http.Request) bool

CredentialProbe is the conservative private credential-presence probe.

It reports whether the active transport-auth provider chain might still authenticate the request as presented. Only a proven false is a license to refuse a quarantined source before authentication: a credential-bearing request, a credential-free provider, an unknown/custom provider, or a provider without the private capability must all report true, so a legitimate user behind shared NAT, VPN or corporate egress is never locked out by another actor on the same public address. A nil probe fails open to the normal auth chain.

type Input

type Input struct {
	// Policy is the immutable generation policy. A nil or disabled policy is a
	// structural fast path: the wrapper is absent and no request performs any
	// self-defense work at all.
	Policy *ingressdefense.Policy
	// State is the process-owned bounded adaptive state. A nil State means the
	// process resource was not projected: deterministic impossible-path
	// rejection still runs and adaptive state is simply not consulted.
	State *ingressdefense.State
	// Resolver is the already-compiled GeoIP client-address trust configuration.
	// Self-defense reuses it verbatim, so it creates no second forwarding parser
	// and no second trust boundary; the same parser bounds and fail-closed
	// behavior apply here.
	Resolver geoipingress.ResolverConfig
	// ImpossiblePaths toggles the fixed matcher. Disabling it bypasses only the
	// matcher; adaptive quarantine behavior is retained.
	ImpossiblePaths bool
	// OwnedRoutes are the data-plane routes the RUNNING ROUTER owns, already
	// resolved with the router's own match semantics: an exact registration owns
	// one method/path pair, a trailing-slash registration owns its subtree, and
	// comparison is case-sensitive exactly as the router's is.
	//
	// A published route is authoritative, so the deterministic matcher never
	// refuses one. The configuration surface for a frontend base path and for a
	// diagnostics, metrics or operator-mount path is any normalized absolute
	// non-root path, which includes paths inside the frozen impossible families, so
	// without this inventory a default-on security layer would make a valid
	// configuration's route unreachable.
	//
	// This is the router's answer, not a path-shaped guess: a configured path whose
	// feature is disabled is not mounted and therefore carves nothing, and an exact
	// registration does not carve its own subtree. An empty or absent inventory
	// keeps the full pre-carve behavior, so a generation that projects no inventory
	// still refuses every frozen family.
	OwnedRoutes []httpcontract.OwnedRoute

	// Probe is the optional conservative credential-presence probe consulted for
	// a quarantined source.
	Probe CredentialProbe
	// Observer is the optional bounded metrics seam.
	Observer Observer
	// Now supplies the request-time instant passed to the adaptive state. A nil
	// Now uses time.Now; tests inject a fake clock.
	Now func() time.Time
}

Input is the generation-scoped, non-owning self-defense gate projection.

The gate never owns adaptive state: State is the process-lifetime ingressdefense.State capability shared by every generation, and Policy is the immutable reloadable policy of this generation.

type Observer

type Observer interface {
	// Denial records one generic refusal for a finite reason.
	Denial(reason ingressdefense.Reason)
	// QuarantineTransition records one started or escalated quarantine for a
	// finite reason. entryCount is the bounded number of tracked source entries
	// after the transition, used for the current-entry gauge.
	QuarantineTransition(reason ingressdefense.Reason, entryCount int)
}

Observer receives bounded self-defense observations. Every reason is a member of the closed ingressdefense.AllReasons vocabulary; no request path, source address, prefix text, query, header, User-Agent, credential or principal ever reaches it, so it can only ever produce finite metric labels. Observer absence or failure is not authoritative: metrics never decide or change a security outcome.

type OwnedRoutes

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

OwnedRoutes is an immutable, pre-compiled set of the routes the running generation owns. It is built once when the gate is constructed, so deciding whether a path is owned costs a slice comparison and allocates nothing: an attacker chooses how often the branch that consults it runs, so the work must not be per-request proportional to the inventory size.

func NewOwnedRoutes

func NewOwnedRoutes(routes []httpcontract.OwnedRoute) OwnedRoutes

NewOwnedRoutes compiles the inventory into its immutable form. A route that is empty, blank, relative, or the root path itself owns nothing and is dropped, so a degenerate inventory cannot widen the carve to the whole data plane.

func (OwnedRoutes) Covers

func (o OwnedRoutes) Covers(method, path string) bool

Covers reports whether the router owns this exact request target, using the router's own semantics: case-sensitive comparison, an exact registration matching only its own method and path, and a subtree registration matching everything below its path.

Matching is case-SENSITIVE on purpose. http.ServeMux matches literal patterns case-sensitively, so /WP-ADMIN/responses is not the route /wp-admin/responses and is correctly an impossible-path probe rather than a carved request.

func (OwnedRoutes) Len

func (o OwnedRoutes) Len() int

Len reports the number of owned routes, for diagnostics and tests.

Jump to

Keyboard shortcuts

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