abuse

package
v0.8.8 Latest Latest
Warning

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

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

Documentation

Overview

Package abuse is the per-app ingress abuse defense: a policy, a sharded per-client token bucket, deny lists and a replay of past traffic.

Index

Constants

View Source
const (
	EnvMaxRPS            = "APP_ABUSE_MAX_RPS"
	EnvMaxBurst          = "APP_ABUSE_MAX_BURST"
	EnvMaxDenyEntries    = "APP_ABUSE_MAX_DENY_ENTRIES"
	EnvMaxAgentEntries   = "APP_ABUSE_MAX_AGENT_ENTRIES"
	EnvDefaultRPS        = "APP_ABUSE_DEFAULT_RPS"
	EnvDefaultBurst      = "APP_ABUSE_DEFAULT_BURST"
	EnvMaxTrackedClients = "APP_ABUSE_MAX_TRACKED_CLIENTS"
	EnvShards            = "APP_ABUSE_SHARDS"
	EnvIdleEvict         = "APP_ABUSE_IDLE_EVICT"
	EnvMaxRetryAfter     = "APP_ABUSE_MAX_RETRY_AFTER"
	EnvDryRunDefault     = "APP_ABUSE_DRYRUN_DEFAULT_WINDOW"
	EnvDryRunMax         = "APP_ABUSE_DRYRUN_MAX_WINDOW"
	EnvDryRunTop         = "APP_ABUSE_DRYRUN_TOP_CLIENTS"
	EnvSuggestShare      = "APP_ABUSE_SUGGEST_CLIENT_SHARE"
	EnvSuggestBurstSecs  = "APP_ABUSE_SUGGEST_BURST_SECONDS"
)

Environment variables that tune the abuse defense. Every one is optional.

Variables

This section is empty.

Functions

func MonotonicClock

func MonotonicClock() int64

MonotonicClock is the production clock.

func ParseDenyEntry

func ParseDenyEntry(s string) (netip.Prefix, error)

ParseDenyEntry parses an IP address or CIDR range into a prefix.

func ParseTrusted

func ParseTrusted(raw string) ([]netip.Prefix, error)

ParseTrusted parses a comma or space separated list of IPs and CIDRs into the proxy ranges forwarded headers are believed from.

func PrivateRanges

func PrivateRanges() []netip.Prefix

PrivateRanges are the loopback, link-local and RFC 1918 / ULA ranges.

func ReasonNames

func ReasonNames() []string

ReasonNames lists every reason name in counter order.

func ResolveClientIP

func ResolveClientIP(remoteAddr string, forwardedFor []string, trusted []netip.Prefix) netip.Addr

ResolveClientIP picks the client address for a request. Forwarded headers are believed only when the TCP peer is itself a trusted proxy, and then the chain is walked from the right, skipping trusted hops, so a value the client put at the front of X-Forwarded-For is never reached. Without a trusted peer the peer address is the client, whatever the headers say.

Types

type Action

type Action int

Action is what the caller should do with a request.

const (
	ActionAllow Action = iota
	ActionWouldBlock
	ActionBlock
)

Actions. WouldBlock means log_only saw a violation and let the request through.

type ClientImpact

type ClientImpact struct {
	Client     string `json:"client"`
	Requests   int    `json:"requests"`
	WouldBlock int    `json:"would_block"`
	// TopReason is the reason that blocked this client most often.
	TopReason string `json:"top_reason"`
}

ClientImpact is one client's share of a replay.

type Clock

type Clock func() int64

Clock returns monotonic nanoseconds. Tests inject a fake one.

type Counters

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

Counters tallies one app's decisions without taking a lock.

func (*Counters) Drain

func (c *Counters) Drain() Snapshot

Drain returns what was counted since the last call and resets it.

func (*Counters) NoteFailOpen

func (c *Counters) NoteFailOpen()

NoteFailOpen counts one request served because the defense failed.

type Decision

type Decision struct {
	Action Action
	Reason Reason
	// RetryAfter is whole seconds, at least 1, set only for rate limit blocks.
	RetryAfter int
}

Decision is the engine's verdict on one request.

type Engine

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

Engine evaluates one compiled policy. It is immutable after NewEngine and safe for concurrent use; the limiter and counters it points at are shared across policy reloads so a reload never hands clients a fresh bucket.

func NewEngine

func NewEngine(p Policy, st *State, l Limits) (*Engine, error)

NewEngine compiles p against the shared state st.

func (*Engine) Decide

func (e *Engine) Decide(client netip.Addr, ua string) Decision

Decide judges one request. client may be invalid (address unknown), in which case only the User-Agent checks apply. The allow path takes at most one shard lock and allocates nothing.

type FieldError

type FieldError struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

FieldError is one problem with a policy field.

type Limiter

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

Limiter is a sharded per-client token bucket. Each shard has its own lock and its own bounded map, so there is no global lock and memory is capped at the configured client count. A shard drops buckets idle for longer than idle on a timed sweep, and evicts an arbitrary bucket when it is full.

func NewLimiter

func NewLimiter(maxClients, shards int, idle time.Duration, clock Clock) *Limiter

NewLimiter tracks at most maxClients addresses across shards shards (rounded up to a power of two). A nil clock uses MonotonicClock.

func (*Limiter) Allow

func (l *Limiter) Allow(addr netip.Addr, rps float64, burst int) (ok bool, retryAfter time.Duration)

Allow takes one token for addr from a bucket refilling at rps up to burst. When it refuses, retryAfter is how long until a token exists.

func (*Limiter) Evicted

func (l *Limiter) Evicted() uint64

Evicted is how many buckets were dropped because a shard was full.

func (*Limiter) Len

func (l *Limiter) Len() int

Len is the number of clients currently tracked.

type Limits

type Limits struct {
	MaxRPS            float64
	MaxBurst          int
	MaxDenyEntries    int
	MaxAgentEntries   int
	DefaultRPS        float64
	DefaultBurst      int
	MaxTrackedClients int
	Shards            int
	IdleEvict         time.Duration
	MaxRetryAfter     time.Duration
	DryRunDefault     time.Duration
	DryRunMax         time.Duration
	DryRunTop         int
	// SuggestShare is the fraction of the app's peak request rate one client
	// may use before the suggested limit bites; SuggestBurstSeconds sizes the
	// suggested burst as that many seconds of the suggested rate.
	SuggestShare        float64
	SuggestBurstSeconds float64
}

Limits caps every policy field and sizes the limiter. Built from the environment with LimitsFromEnv.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits are the values used when no environment override is set.

func LimitsFromEnv

func LimitsFromEnv(getenv func(string) string) Limits

LimitsFromEnv reads the overrides through getenv; bad or non-positive values keep the default.

type Mode

type Mode string

Mode says what the policy does with a request it would turn away.

const (
	ModeOff     Mode = "off"
	ModeLogOnly Mode = "log_only"
	ModeEnforce Mode = "enforce"
)

The three modes. Off is the default for an app with no policy.

func (Mode) Valid

func (m Mode) Valid() bool

Valid reports whether m is one of the three modes.

type Observation

type Observation struct {
	Time      time.Time
	Client    netip.Addr
	UserAgent string
}

Observation is one past request as the replay needs it.

type Policy

type Policy struct {
	Mode Mode `json:"mode"`
	// RPS is the sustained request rate allowed per client IP; Burst is the
	// bucket size (at least 1 whenever RPS is set).
	RPS   float64 `json:"rps,omitempty"`
	Burst int     `json:"burst,omitempty"`
	// DenyCIDRs are addresses and ranges turned away outright.
	DenyCIDRs []string `json:"deny_cidrs,omitempty"`
	// DenyUserAgents are case-insensitive substrings of the User-Agent header.
	DenyUserAgents   []string `json:"deny_user_agents,omitempty"`
	RequireUserAgent bool     `json:"require_user_agent,omitempty"`
}

Policy is one app's ingress abuse defense. RPS 0 turns the rate limit off, leaving only the deny lists and the User-Agent requirement.

func (Policy) Active

func (p Policy) Active() bool

Active reports whether the policy evaluates requests at all.

func (Policy) Normalize

func (p Policy) Normalize(l Limits) Policy

Normalize defaults an empty mode to off, fills the burst when a rate is set without one, and trims list entries. It never changes what a valid policy means.

func (Policy) Validate

func (p Policy) Validate(l Limits) error

Validate checks the policy against l.

type Reason

type Reason int

Reason names why a request would be, or was, turned away.

const (
	ReasonRateLimit Reason = iota
	ReasonDenyCIDR
	ReasonDenyUserAgent
	ReasonMissingUserAgent
	ReasonCount
)

Reasons, in the order their counters are stored.

func (Reason) String

func (r Reason) String() string

String is the reason's stable wire and metric name.

type ReasonTotal

type ReasonTotal struct {
	Reason string `json:"reason"`
	Count  int    `json:"count"`
}

ReasonTotal pairs a reason name with how many requests it covers.

type Registry

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

Registry hands out one State per app. It is touched on config load and on each metrics drain, never per request.

func DefaultRegistry

func DefaultRegistry() *Registry

DefaultRegistry is the process-wide registry the ingress handler uses; Caddy modules are built from JSON and cannot be handed a live value.

func NewRegistry

func NewRegistry(clock Clock) *Registry

NewRegistry returns an empty registry. A nil clock uses MonotonicClock.

func (*Registry) DrainAll

func (r *Registry) DrainAll() map[string]Snapshot

DrainAll returns each app's counters since the last call, omitting apps with nothing to report.

func (*Registry) For

func (r *Registry) For(app string, l Limits) *State

For returns app's state, creating it sized by l on first use.

func (*Registry) Forget

func (r *Registry) Forget(app string)

Forget drops an app's state, for an app that was deleted.

func (*Registry) NoteFailOpen

func (r *Registry) NoteFailOpen(app string, l Limits)

NoteFailOpen counts one request served unchecked for app, because the policy could not be loaded or applied.

func (*Registry) Tracked

func (r *Registry) Tracked(app string) int

Tracked is how many clients app's limiter holds, 0 for an unknown app.

type ReplayResult

type ReplayResult struct {
	Requests   int            `json:"requests"`
	WouldBlock int            `json:"would_block"`
	Percent    float64        `json:"percent"`
	ByReason   []ReasonTotal  `json:"by_reason"`
	TopClients []ClientImpact `json:"top_clients"`
	From       time.Time      `json:"from"`
	To         time.Time      `json:"to"`
}

ReplayResult is what a policy would have done to a stretch of past traffic. Nothing was blocked or changed to produce it.

func Replay

func Replay(p Policy, obs []Observation, l Limits, top int) (ReplayResult, error)

Replay runs obs, oldest first, through a fresh engine for p on a clock that follows the recorded times, and reports what it would have blocked. It uses private state, so the live limiter and counters are untouched.

type Snapshot

type Snapshot struct {
	Allowed    uint64
	WouldBlock [ReasonCount]uint64
	Blocked    [ReasonCount]uint64
	FailOpen   uint64
}

Snapshot is a drained copy of the counters.

func (*Snapshot) Add

func (s *Snapshot) Add(o Snapshot)

Add merges o into s.

func (Snapshot) Zero

func (s Snapshot) Zero() bool

Zero reports whether nothing was counted.

type State

type State struct {
	Limiter  *Limiter
	Counters *Counters
}

State is what survives a policy reload for one app: the limiter buckets and the counters.

type Suggestion

type Suggestion struct {
	PeakRPS float64 `json:"peak_rps"`
	RPS     float64 `json:"rps"`
	Burst   int     `json:"burst"`
	// FromTraffic is false when there was no traffic and the instance
	// defaults were used.
	FromTraffic bool `json:"from_traffic"`
}

Suggestion is a starting policy derived from the app's recent traffic.

func Suggest

func Suggest(peakRPS float64, l Limits) Suggestion

Suggest sizes a per-client limit from the app's peak request rate: one client may use a configured share of the peak, never less than the default rate, so ordinary visitors are not caught by a quiet-hour measurement.

type ValidationError

type ValidationError struct{ Fields []FieldError }

ValidationError lists every problem found, so a form can show them at once.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Jump to

Keyboard shortcuts

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