edgefw

package
v0.7.0 Latest Latest
Warning

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

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

Documentation

Overview

Package edgefw is the ingress firewall: ordered rules, automatic bans, bot modes, a proof-of-work challenge and bounded in-memory visibility.

Index

Constants

View Source
const (
	EnvMaxRules       = "APP_FIREWALL_MAX_RULES"
	EnvMaxRateEntries = "APP_FIREWALL_MAX_RATE_ENTRIES"
	EnvBanStrikes     = "APP_FIREWALL_BAN_STRIKES"
	EnvBanWindow      = "APP_FIREWALL_BAN_WINDOW"
	EnvBanDuration    = "APP_FIREWALL_BAN_DURATION"
	EnvMaxBans        = "APP_FIREWALL_MAX_BANS"
	EnvChallengeBits  = "APP_FIREWALL_CHALLENGE_BITS"
	EnvChallengeTTL   = "APP_FIREWALL_CHALLENGE_TTL"
	EnvPassTTL        = "APP_FIREWALL_PASS_TTL" //nolint:gosec // env var name
	EnvTopCapacity    = "APP_FIREWALL_TOP_CAPACITY"
	EnvMaxStatScopes  = "APP_FIREWALL_MAX_STAT_HOSTS"
	EnvTopSample      = "APP_FIREWALL_TOP_SAMPLE"
	EnvSuggestRate    = "APP_FIREWALL_SUGGEST_ATTACK_PER_MINUTE"
	EnvSuggestMinutes = "APP_FIREWALL_SUGGEST_ATTACK_MINUTES"
	EnvMaxBotAgents   = "APP_FIREWALL_MAX_AI_BOT_AGENTS"
)

Environment variables that tune the firewall. All are optional.

View Source
const ChallengePath = "/.well-known/lr-challenge"

ChallengePath is the reserved path the challenge page posts its proof to.

View Source
const PassCookie = "__lr_pass"

PassCookie is the name of the short-lived cookie a solved challenge earns.

Variables

FieldOps lists the operators each field accepts; it is the closed catalog the dashboard builder and the validator share.

Functions

func ChallengePage

func ChallengePage(title, token string, difficulty int) (page []byte, csp string)

ChallengePage renders the self-contained proof-of-work page and the Content-Security-Policy header value that allows only its own script.

func DefaultAIBotAgents

func DefaultAIBotAgents() []string

DefaultAIBotAgents returns the shipped AI crawler list.

func LeadingZeroBits

func LeadingZeroBits(sum [32]byte) int

LeadingZeroBits counts the zero bits at the start of sum.

func Normalize

func Normalize(r *Rule)

Normalize trims and canonicalizes a rule in place before validation.

func ValidateRule

func ValidateRule(r *Rule) error

ValidateRule checks a normalized rule against the limits.

Types

type Action

type Action string

Action is what a matching rule does.

const (
	ActionAllow     Action = "allow"
	ActionDeny      Action = "deny"
	ActionChallenge Action = "challenge"
	ActionLog       Action = "log"
	ActionRateLimit Action = "rate_limit"
)

Rule actions. Allow ends evaluation and skips every later check, deny and challenge end it, log and rate_limit let the request continue unless the rate limit trips.

type Ban

type Ban struct {
	IP      netip.Addr `json:"ip"`
	Reason  string     `json:"reason"`
	Started time.Time  `json:"started"`
	Ends    time.Time  `json:"ends"`
	Hits    uint64     `json:"hits"`
}

Ban is one automatic temporary block ("persistent action"). Bans live in memory only and are gone after a restart.

type Bans

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

Bans tracks denials per client and bans a client that crosses the threshold. Both tables are capped.

func NewBans

func NewBans(l Limits) *Bans

NewBans builds an empty tracker.

func (*Bans) Active

func (b *Bans) Active(ip netip.Addr, now time.Time) bool

Active reports whether ip is banned at now, counting the hit.

func (*Bans) Add

func (b *Bans) Add(ip netip.Addr, reason string, d time.Duration, now time.Time)

Add bans ip for d.

func (*Bans) Lift

func (b *Bans) Lift(ip netip.Addr) bool

Lift removes the ban on ip and reports whether one existed.

func (*Bans) List

func (b *Bans) List(now time.Time) []Ban

List returns the unexpired bans, newest first.

func (*Bans) SetLimits

func (b *Bans) SetLimits(l Limits)

SetLimits replaces the thresholds; existing bans keep their end times.

func (*Bans) Strike

func (b *Bans) Strike(ip netip.Addr, reason string, now time.Time) bool

Strike counts one denial for ip and reports whether it just became banned.

type Condition

type Condition struct {
	Field Field  `json:"field"`
	Op    Op     `json:"op"`
	Name  string `json:"name,omitempty"`
	Value string `json:"value,omitempty"`
}

Condition is one comparison; a rule matches when all its conditions do.

type Dimension

type Dimension string

Dimension names a top table.

const (
	DimIP       Dimension = "ip"
	DimDeniedIP Dimension = "denied_ip"
	DimUA       Dimension = "user_agent"
	DimPath     Dimension = "path"
	DimHost     Dimension = "host"
)

Top table dimensions.

type Engine

type Engine struct {
	Stats  *Stats
	Bans   *Bans
	Signer *Signer
	// contains filtered or unexported fields
}

Engine evaluates requests against the current rules and settings. All state it keeps is bounded; see Stats and Bans.

func Default

func Default() *Engine

Default is the process-wide engine the ingress handler reads; Caddy modules are built from JSON and cannot receive a live value any other way.

func New

func New(l Limits, shield trafficpolicy.Limits) *Engine

New builds an engine with empty rules and default settings.

func (*Engine) CurrentSettings

func (e *Engine) CurrentSettings() Settings

CurrentSettings returns the installed settings.

func (*Engine) Evaluate

func (e *Engine) Evaluate(req *Request, now time.Time) Verdict

Evaluate runs the ordered rules, then automatic bans, attack-mode challenge and the bot modes. It never allocates per rule.

func (*Engine) Issue

func (e *Engine) Issue(req *Request, now time.Time) string

Issue creates a challenge token for req's client.

func (*Engine) Limits

func (e *Engine) Limits() Limits

Limits returns the limits the engine was built with.

func (*Engine) NeedsCountry

func (e *Engine) NeedsCountry() bool

NeedsCountry reports whether any active rule reads the country, so the ingress only pays for a geo lookup when one does.

func (*Engine) Record

func (e *Engine) Record(req *Request, o Outcome, r Reason, now time.Time)

Record classifies a finished decision: it feeds the stats and, for denials and rate limits, counts a strike toward an automatic ban.

func (*Engine) SetRules

func (e *Engine) SetRules(rules []Rule)

SetRules compiles and installs the rules; disabled rules are skipped.

func (*Engine) SetSettings

func (e *Engine) SetSettings(s Settings)

SetSettings installs the settings and rebuilds the bot matchers.

func (*Engine) SetTitle

func (e *Engine) SetTitle(name string)

SetTitle sets the brand name shown on the challenge page.

func (*Engine) Solve

func (e *Engine) Solve(ip netip.Addr, token, counter string, now time.Time) (string, bool)

Solve verifies a posted proof and returns the pass cookie value.

func (*Engine) Title

func (e *Engine) Title() string

Title is the challenge page title.

type Field

type Field string

Field is the request attribute a condition reads.

const (
	FieldPath      Field = "path"
	FieldHost      Field = "host"
	FieldMethod    Field = "method"
	FieldHeader    Field = "header"
	FieldQuery     Field = "query"
	FieldUserAgent Field = "user_agent"
	FieldIP        Field = "ip"
	FieldCountry   Field = "country"
)

Condition fields.

type FieldError

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

FieldError names one invalid field with plain text.

type Limits

type Limits struct {
	MaxRules       int
	MaxRateEntries int
	BanStrikes     int
	BanWindow      time.Duration
	BanDuration    time.Duration
	MaxBans        int
	ChallengeBits  int
	ChallengeTTL   time.Duration
	PassTTL        time.Duration
	TopCapacity    int
	MaxStatScopes  int
	TopSample      int
	SuggestRate    int
	SuggestMinutes int
	MaxBotAgents   int
}

Limits bounds every table the firewall keeps and sets its defaults.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits are the values used when no variable is set.

func LimitsFromEnv

func LimitsFromEnv(getenv func(string) string) Limits

LimitsFromEnv overlays set variables on DefaultLimits; unparseable or non-positive values keep the default.

type Mode

type Mode string

Mode is how a bot list is enforced.

const (
	ModeOff       Mode = "off"
	ModeLog       Mode = "log"
	ModeChallenge Mode = "challenge"
	ModeBlock     Mode = "block"
)

Bot modes.

type Observation

type Observation struct {
	At      time.Time
	Outcome Outcome
	Reason  Reason
	IP      netip.Addr
	UA      string
	Path    string
	Host    string
}

Observation is one request's final classification.

type Op

type Op string

Op is how a condition compares its field.

const (
	OpEquals     Op = "equals"
	OpStartsWith Op = "starts_with"
	OpContains   Op = "contains"
	OpGlob       Op = "glob"
	OpInCIDR     Op = "in_cidr"
	OpExists     Op = "exists"
)

Condition operators.

type Outcome

type Outcome int

Outcome classifies what the firewall did with one request.

const (
	OutcomeAllowed Outcome = iota
	OutcomeDenied
	OutcomeChallenged
	OutcomeLogged
	OutcomeRateLimited
	OutcomeCount
)

Outcomes, in the order their counters are stored.

func (Outcome) String

func (o Outcome) String() string

String is the outcome's stable wire name.

type Reason

type Reason int

Reason says which mechanism produced an outcome.

const (
	ReasonNone Reason = iota
	ReasonRule
	ReasonRateRule
	ReasonBan
	ReasonBot
	ReasonAIBot
	ReasonAttack
	ReasonShieldRate
	ReasonShieldConcurrency
	ReasonShieldBot
	ReasonShieldBody
	ReasonCount
)

Reasons, in the order their counters are stored.

func (Reason) String

func (r Reason) String() string

String is the reason's stable wire name.

type ReasonCountRow

type ReasonCountRow struct {
	Reason string `json:"reason"`
	Count  uint64 `json:"count"`
}

ReasonCountRow is the number of requests a reason produced.

type Request

type Request struct {
	Method     string
	Host       string
	Path       string
	RawQuery   string
	UserAgent  string
	Header     http.Header
	IP         netip.Addr
	Country    string
	PassCookie string
}

Request is the slice of an HTTP request the firewall reads. Host carries no port; Country is empty when no geo source is configured.

type Rule

type Rule struct {
	ID                string      `json:"id"`
	Name              string      `json:"name"`
	Note              string      `json:"note,omitempty"`
	Scope             string      `json:"scope,omitempty"`
	Enabled           bool        `json:"enabled"`
	Position          int         `json:"position"`
	Conditions        []Condition `json:"conditions"`
	Action            Action      `json:"action"`
	RateCount         int         `json:"rate_count,omitempty"`
	RateWindowSeconds int         `json:"rate_window_seconds,omitempty"`
	CreatedAt         string      `json:"created_at,omitempty"`
	UpdatedAt         string      `json:"updated_at,omitempty"`
}

Rule is one ordered firewall rule. Scope is a host or empty for every host; Position orders rules, lowest first.

func IPRule

func IPRule(action Action, ip, scope, note string) (Rule, error)

IPRule builds the canonical rule for an IP list entry.

func (Rule) IPList

func (r Rule) IPList() (Action, bool)

IPList reports whether the rule is a plain IP list entry (one ip condition) and which action list it belongs to.

type RuleHit

type RuleHit struct {
	Total uint64
	Last  time.Time
	Spark [ruleMinutes / 5]uint32
}

RuleHit is one rule's counters.

type SeriesPoint

type SeriesPoint struct {
	Time   time.Time            `json:"time"`
	Counts [OutcomeCount]uint64 `json:"counts"`
}

SeriesPoint is one chart bucket.

type Settings

type Settings struct {
	BotMode         Mode       `json:"bot_mode"`
	AIBotMode       Mode       `json:"ai_bot_mode"`
	AIBotAgents     []string   `json:"ai_bot_agents"`
	BansEnabled     bool       `json:"bans_enabled"`
	AttackUntil     *time.Time `json:"attack_until,omitempty"`
	AttackChallenge bool       `json:"attack_challenge"`
}

Settings is the instance-wide firewall configuration beside the rules.

func DefaultSettings

func DefaultSettings() Settings

DefaultSettings leaves both bot lists off and automatic bans on.

func (Settings) AttackActive

func (s Settings) AttackActive(now time.Time) bool

AttackActive reports whether attack mode is on at now.

func (*Settings) Validate

func (s *Settings) Validate(l Limits, attackMax time.Duration, now time.Time) error

Validate checks the settings against the limits.

type Signer

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

Signer issues and checks the challenge token and the pass cookie. Both are HMAC-SHA256 signed, bound to the client's network prefix and short lived.

func NewSigner

func NewSigner() *Signer

NewSigner starts with a random per-process key; SetKey replaces it with one derived from the master key so cookies survive a restart.

func (*Signer) NewChallenge

func (s *Signer) NewChallenge(ip netip.Addr, difficulty int, ttl time.Duration, now time.Time) string

NewChallenge returns a signed token for ip that the page must solve with the given difficulty.

func (*Signer) NewPass

func (s *Signer) NewPass(ip netip.Addr, ttl time.Duration, now time.Time) string

NewPass returns a pass cookie value for ip valid for ttl.

func (*Signer) PassValid

func (s *Signer) PassValid(cookie string, ip netip.Addr, now time.Time) bool

PassValid reports whether cookie is an unexpired pass for ip's network.

func (*Signer) SetKey

func (s *Signer) SetKey(key []byte)

SetKey installs key; a nil or short key is ignored.

func (*Signer) VerifyProof

func (s *Signer) VerifyProof(token, counter string, ip netip.Addr, now time.Time) bool

VerifyProof checks the token's signature, expiry and binding, then that sha256(token + ":" + counter) has the required leading zero bits.

type Stats

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

Stats keeps the bounded, in-memory firewall visibility: per-minute outcome counters for 24 hours, per-hour reason counters, per-rule hit rings and a rolling hour of approximate top-N tables. No URL, query string or client address is kept outside the capped top tables.

func NewStats

func NewStats(l Limits) *Stats

NewStats builds empty stats.

func (*Stats) Observe

func (s *Stats) Observe(o Observation)

Observe records one request under the global scope and, within the host cap, under its own host.

func (*Stats) PruneRules

func (s *Stats) PruneRules(keep map[string]bool)

PruneRules drops counters of rules that no longer exist.

func (*Stats) Reasons

func (s *Stats) Reasons(host string, from, to time.Time) []ReasonCountRow

Reasons sums reason counters over whole hours overlapping from..to.

func (*Stats) RuleHits

func (s *Stats) RuleHits(id string, now time.Time) RuleHit

RuleHits returns the counters for id: total since start, last hit and a sparkline of the last hour in 5 minute steps (oldest first).

func (*Stats) RuleMatched

func (s *Stats) RuleMatched(id string, at time.Time)

RuleMatched counts one match of rule id.

func (*Stats) Series

func (s *Stats) Series(host string, from, to time.Time, points int) []SeriesPoint

Series returns outcome counts between from and to in at most points buckets, each a whole number of minutes.

func (*Stats) SetLimits

func (s *Stats) SetLimits(l Limits)

SetLimits replaces the capacities used for new tables.

func (*Stats) Top

func (s *Stats) Top(host string, dim Dimension, now time.Time, n int) []TopRow

Top returns the busiest keys of dim over the last hour (the retained window), most first.

func (*Stats) Totals

func (s *Stats) Totals(host string, from, to time.Time) [OutcomeCount]uint64

Totals sums the outcome counts between from and to.

type TopRow

type TopRow struct {
	Key   string             `json:"key"`
	Count uint64             `json:"count"`
	Spark [sparkSlots]uint32 `json:"spark"`
}

TopRow is one approximate top-N entry with a six point sparkline.

type ValidationError

type ValidationError struct{ Fields []FieldError }

ValidationError carries every problem found in a rule or setting.

func (*ValidationError) Error

func (e *ValidationError) Error() string

type Verdict

type Verdict struct {
	Action     Action
	Reason     Reason
	Outcome    Outcome
	RuleID     string
	RuleName   string
	Status     int
	RetryAfter int
	Logged     bool
}

Verdict is what Evaluate decided. Action is empty when the request goes on to the remaining checks.

Jump to

Keyboard shortcuts

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