package servewaf 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 servewaf 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 // SWWAF_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{ denyNets{}, activeBan{}, } } // afterLookup are the heuristics that run once the lookup has been made. func afterLookup() []heuristic { return []heuristic{ countries{}, blocklists{}, crowdSec{}, dnsbl{}, abuseIPDB{}, rateLimits{}, trapPaths{}, ruleFiles{}, headerLimits{}, coreRuleSet{}, } } // afterAnswer are the heuristics that run on a request once servewaf 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 report_heuristic and // report_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.ReportHeuristic = name rq.line.ReportHeuristicFigures = 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 }