9.2 Defensive modules — what each one actually checks¶
[!IMPORTANT] 9.1 ended on one rule: filter cheap and early, authenticate the rest. These are the modules that filter, and each checks exactly one thing —
sanityproves the message is legal SIP,secfiltermatches strings against lists,pikecounts packets per source,authestablishes who sent it. None inspects free-text header content for shell or SQL safety. Knowing which does what is the whole game.
sanity — well-formedness, not safety¶
sanity_check([msg_checks[, uri_checks]]), called first in request_route as route("sanity"), answers one question: is this legal SIP? On failure sanity_reply() emits the matching 4xx. The documented checks are all structural — R-URI version and scheme, required headers (To/From/CSeq/Call-ID/Via), CSeq method-matches-request and a valid integer, Content-Length equal to the actual body size (the one that catches truncation and request smuggling), Expires value, Proxy-Require, parseable URIs, digest-credential form, duplicated To/From tags, a present-parseable-branched first Via, and RFC 3261 magic-cookie/lr compliance.
Every check is about form, never meaning — the pivot for 9.4:
A
$(...)substring inside aUser-Agent, or a'inside aFrom, is legal SIP token content.sanity_check()passes it untouched.
sanity rejects a packet whose Content-Length lies about its body; it cannot reject a well-formed packet that smuggles a shell metacharacter or an SQL fragment, because that packet is valid SIP. It is a syntax gate — cheap triage against malformed junk, and nothing about hostile-but-well-formed content.
secfilter — the content filter¶
secfilter is the stock set's closest thing to content filtering: in-memory black/whitelists (loaded from DB), matched by a secf_check_* family.
| Function | Matches |
|---|---|
secf_check_ip() |
source IP vs blacklist |
secf_check_ua() |
User-Agent vs UA list |
secf_check_from_hdr() / secf_check_to_hdr() |
From/To user, domain, display-name |
secf_check_contact_hdr() |
Contact user / domain |
secf_check_dst(string) |
destination number |
secf_check_country(string) |
country code (needs geoip) |
secf_check_sqli_hdr(string) / secf_check_sqli_all() |
illegal chars in one value / across UA, From, To, Contact |
This catches friendly-scanner UAs, known-bad domains, toll-fraud prefixes, and obvious SQLi probes. But it is static pattern matching — strings and a fixed illegal-character set. It raises the cost of a naive attack; it has no idea what your downstream DB, shell, or script does with a value. A filter, not a guarantee — and the wrong layer for 9.4-style safety, which must happen where you use the value.
pike — flood detection¶
pike answers is one source sending too fast? It tracks per-source-IP rate in shm via a tree keyed byte-by-byte on the address (one structure spans IPv4 and IPv6). pike_check_req() — or pike_check_ip(ipaddr) for an arbitrary address — accounts each hit. Three params set policy: sampling_time_unit (window seconds, default 2), reqs_density_per_unit (allowed per window, default 30), remove_latency (idle retention, default 120).
The return is the point: 1 not-blocked (also on internal error — fail-open), -1 known flooder, -2 new flooder this instant. The -2/-1 split lets you act once on first detection (log, ban) then short-circuit every later packet from that source. That verdict normally feeds the ipban pattern of 9.3: pike detects, the htable remembers.
topoh / topos — cut the recon¶
Not filters — topology hiding, read here as reconnaissance denial. A scanner mines Via, Record-Route, Contact, and Route to map your internal addressing; topoh (encodes or strips those headers) and topos (stashes the state server-side, hands back an opaque token) shrink what leaks in the replies you do send. Mechanics are in 8.1 topology hiding; they sit here because edge hardening is as much about what you leak in replies as what you accept in requests.
auth — the only real trust boundary on UDP¶
Everything above is heuristic — none establishes who sent the packet, and on UDP source IP is not identity (9.1). auth draws the identity boundary via SIP digest. The challenge functions — www_challenge(realm, flags[, algorithms]) (401, registrar/UAS), proxy_challenge(...) (407, proxy), auth_challenge(...) (picks by context) — emit the WWW-/Proxy-Authenticate header; the verify side (pv_www_authenticate / pv_proxy_authenticate / pv_auth_check) recomputes the digest against the stored secret and accepts only on a match. Replay protection lives in the challenge: bounded nonce lifetime (nonce_expire, default 300s), and qop brings in the client nonce-count so a captured Authorization can't be replayed. This — not any blacklist — stops the REGISTER-hijack and toll-fraud cases. Filtering buys cheap rejection of garbage; only auth lets you trust what's left.
Summary¶
| Module | Checks | Where | Does not |
|---|---|---|---|
sanity |
SIP well-formedness | first in request_route |
inspect content for shell/SQL safety |
secfilter |
fields vs lists; SQLi char set | early, per request | parse intent; catch anything off-list |
pike |
request rate per source IP | per request, edge | care about content or identity |
topoh/topos |
(rewrites outbound state) | per message, edge | reject requests; authenticate |
auth |
digest identity of the sender | after cheap filters | filter floods or malformed input |
Top to bottom, that is the 9.1 doctrine made concrete: reject malformed junk for almost nothing (sanity), drop known-bad strings and floods (secfilter, pike), leak little in replies (topoh/topos), and spend a digest challenge (auth) only on traffic that survived. The gap it exposes: pike forgets a flooder after remove_latency, and secf_check_ip() only matches a list you populate. Turning one-off detection into a durable, shareable, cluster-wide ban is 9.3.
← Table of contents · ← 9.1 Attack surface · Next: 9.3 Dynamic blocklists →