check / check (push) Waiting to run
Every setting of discriminator and rproxy is read through viper, in place of the os.LookupEnv function Main passed down. NewViper turns AutomaticEnv and AllowEmptyEnv on, so a setting that is set but empty reads as set. A setting may also be given as a file, the _FILE form, which rproxy's settings now have too; rproxy still writes nothing to disk. The settings are renamed. Discriminator's take the prefix SWW_WAF_, rproxy's take SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core Rule Set's settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_. Model: deepseek-v4-pro (first version); opus-5-5 (rework)
140 lines
4.2 KiB
Go
140 lines
4.2 KiB
Go
package discriminator
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// To add a heuristic, write one file with a type that implements heuristic,
|
|
// and add one line for it to one of the lists below. Its settings go in
|
|
// config.Config, and the comment at the top of its file names them.
|
|
|
|
// heuristic is one of the checks that decide what discriminator does with a
|
|
// request.
|
|
type heuristic interface {
|
|
// name is the heuristic's name, which the log line gives.
|
|
name() string
|
|
// check looks at the request at now, and returns nil to let it through.
|
|
// ctx is the request's own context.
|
|
check(ctx context.Context, rq *request, now time.Time) *verdict
|
|
}
|
|
|
|
// verdict is a heuristic refusing a request, or banning its client: the action
|
|
// the log line names, one of requestlog's Action constants, and the figures the
|
|
// heuristic acted on, by name, such as the count and the limit.
|
|
type verdict struct {
|
|
action string
|
|
figures map[string]any
|
|
}
|
|
|
|
// The names of the figures that more than one heuristic gives.
|
|
const (
|
|
figureWindow = "window"
|
|
figureCount = "count"
|
|
figureLimit = "limit"
|
|
figureScore = "score"
|
|
figureNetblock = "netblock"
|
|
)
|
|
|
|
// The lists of heuristics, each in the order its heuristics run. A client in
|
|
// SWW_WAF_ALLOW_NETS skips them all.
|
|
|
|
// beforeLookup are the heuristics that run before the client's AS number and
|
|
// country are looked up, so that a client they refuse is not looked up.
|
|
func beforeLookup() []heuristic {
|
|
return []heuristic{
|
|
tagLists{},
|
|
denyNets{},
|
|
activeBan{},
|
|
}
|
|
}
|
|
|
|
// afterLookup are the heuristics that run once the lookup has been made.
|
|
// tag_deny runs twice: before the rate limits, on the tags known by then, and
|
|
// last, on the tag rule_files and core_rule_set can add.
|
|
func afterLookup() []heuristic {
|
|
return []heuristic{
|
|
countries{},
|
|
blocklists{},
|
|
crowdSec{},
|
|
dnsbl{},
|
|
abuseIPDB{},
|
|
tagDeny{},
|
|
rateLimits{},
|
|
trapPaths{},
|
|
ruleFiles{},
|
|
headerLimits{},
|
|
coreRuleSet{},
|
|
tagDeny{},
|
|
}
|
|
}
|
|
|
|
// afterAnswer are the heuristics that run on a request once discriminator has
|
|
// answered it, before its decision line is written. They ban the client, and do
|
|
// not change what the request got: their verdict's action is not used.
|
|
func afterAnswer() []heuristic {
|
|
return []heuristic{
|
|
refusalLimit{},
|
|
}
|
|
}
|
|
|
|
// onReport are the heuristics that run on an allowed request when rproxy's
|
|
// report of it comes, before its report line is written. Like afterAnswer, they
|
|
// only ban.
|
|
func onReport() []heuristic {
|
|
return []heuristic{
|
|
byteLimits{},
|
|
}
|
|
}
|
|
|
|
// runAfterAnswer runs the heuristics of afterAnswer on the request, at the time
|
|
// it is called.
|
|
func (rq *request) runAfterAnswer(ctx context.Context) {
|
|
rq.runBanningHeuristics(ctx, afterAnswer())
|
|
}
|
|
|
|
// runHeuristics runs list, beforeLookup or afterLookup, on the request at now,
|
|
// and returns the first verdict, or nil when none gives one. The log line's
|
|
// heuristic and heuristic_figures name the heuristic that gave it, and give its
|
|
// figures. ctx is the request's own context.
|
|
func (rq *request) runHeuristics(
|
|
ctx context.Context, list []heuristic, now time.Time,
|
|
) *verdict {
|
|
name, v := rq.firstVerdict(ctx, list, now)
|
|
if v != nil {
|
|
rq.line.Heuristic = name
|
|
rq.line.HeuristicFigures = v.figures
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
// runBanningHeuristics runs list, afterAnswer or onReport, on the request at
|
|
// the time it is called. The log line's after_answer_heuristic and
|
|
// after_answer_heuristic_figures name the heuristic that gave a verdict, and
|
|
// give its figures, so that heuristic and heuristic_figures still name the one
|
|
// that refused the request. ctx is the request's own context.
|
|
func (rq *request) runBanningHeuristics(ctx context.Context, list []heuristic) {
|
|
name, v := rq.firstVerdict(ctx, list, rq.h.now())
|
|
if v != nil {
|
|
rq.line.AfterAnswerHeuristic = name
|
|
rq.line.AfterAnswerHeuristicFigures = v.figures
|
|
}
|
|
}
|
|
|
|
// firstVerdict runs list on the request at now, in order, until a heuristic
|
|
// gives a verdict, and returns that heuristic's name and its verdict, or "" and
|
|
// nil when none gives one. ctx is the request's own context.
|
|
func (rq *request) firstVerdict(
|
|
ctx context.Context, list []heuristic, now time.Time,
|
|
) (string, *verdict) {
|
|
for _, each := range list {
|
|
v := each.check(ctx, rq, now)
|
|
if v != nil {
|
|
return each.name(), v
|
|
}
|
|
}
|
|
|
|
return "", nil
|
|
}
|