Skip to content

datasets: add cidr dataset type - #16085

Closed
jlucovsky wants to merge 2 commits into
OISF:mainfrom
jlucovsky:8124/1
Closed

datasets: add cidr dataset type#16085
jlucovsky wants to merge 2 commits into
OISF:mainfrom
jlucovsky:8124/1

Conversation

@jlucovsky

Copy link
Copy Markdown
Contributor

Adds a type cidr dataset backed by radix trees for IPv4 and IPv6 network ranges, with longest-prefix-match lookups.

alert ip any any -> any any (msg:"in RFC1918"; ip.src; dataset:isset,rfc1918,type cidr,load rfc1918.lst; sid:1;)

with rfc1918.lst containing 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and any packet whose ip.src falls under one of those prefixes matches. Same shape for ip.dst. All four dataset commands are supported: isset, isnotset, set, unset. set and unset accept an optional mask in decimal (mask 24), hex prefix (mask 0x18), or IPv4 bitmask (mask 0xffffff00) form so rules can accumulate /N netblocks instead of individual hosts.

Link to ticket: https://redmine.openinfosecfoundation.org/issues/8124

Describe changes:

  • Storage is one SCRadix4Tree and one SCRadix6Tree, each behind an SCRWLock. isset and isnotset take the read lock only. set probes under the read lock first and only escalates to the write lock when the entry is new. A steady-state flood of already-seen sources never blocks on writes. A microbenchmark across 16 workers measured 13-14M aggregate lookups/s.

  • Memcap is per family and approximate. Each entry counts as ~128 bytes for IPv4 or ~192 bytes for IPv6; internal split nodes are not counted, so memory can run over. datasets.rst documents the gap. When memcap rejects an add, a per family counter increments and a warning re-logs on the first rejection and every millionth after, so operators see sustained pressure rather than a single lost line.

  • The new unix-socket command dataset-add-batch takes the write lock once per family for the whole batch. A 10k-entry admin import drops from 10k wrlock acquisitions to at most 2. Hash-based types benefit too, though the win is smaller since they already lock per bucket.

SV_REPO=
SV_BRANCH=OISF/suricata-verify#3304
SU_REPO=
SU_BRANCH=

Backed by a pair of radix trees (SCRadix4Tree for IPv4,
SCRadix6Tree for IPv6). Address data comes from the ip.src or ip.dst
sticky buffer -- 4 bytes for IPv4, 16 for IPv6.

All four commands are supported:

  isset/isnotset: best-match lookup, so a /16 in the dataset matches
  any host in that block.

  set: adds the matched address as a /32 (IPv4) or /128 (IPv6).  With
  mask, the address is masked to the specified prefix length first, so
  the dataset accumulates /N netblocks rather than individual hosts.

  unset: removes the exact host entry, or with mask, removes the /N
  netblock that a corresponding set would have added.

mask accepts a decimal prefix length (24), a hex prefix length (0x18),
or an IPv4 bitmask (0xffffff00).  It is only valid on set and unset.
Prefix length 0 (match-everything) is rejected at rule load.

DetectDatasetBufferMatch dispatches CIDR sets directly to
CIDRLookupIPv4, CIDRAddIPv4Netblock, and CIDRRemoveIPv4Netblock (and
the IPv6 counterparts) so the per-packet path does not build a
temporary 5/17-byte encoding.  The generic SCDatasetAdd/DatasetLookup
/DatasetRemove entry points still accept the encoding for callers
outside the hot path.

Concurrency: CIDRLookupIPv4 and CIDRLookupIPv6 take only the read
lock, so a rule using dataset:isset on ip.src or ip.dst scales across
workers. Add and remove use a rdlock-first pattern -- probe under the
read lock, and take the write lock only if the entry needs to change.
Under a spoofed-source flood driving dataset:set on ip.src, addresses
already present short-circuit on the rdlock without ever contending
the writer. The per-family byte counter is stored atomically so a
future stats consumer can sample it without contending the lock
either.

memcap is enforced per family and is approximate. Each entry counts
as ~128 bytes (IPv4) or ~192 bytes (IPv6); the tree's internal split
nodes are not counted, so real memory use will be somewhat higher
than the tracked total. Once the tracked total for a family exceeds
memcap, further set operations for that family are rejected and one
warning is logged per family.

Restrictions: only load is supported; save and state are rejected at
rule-load time.  datarep is rejected at rule load in detect-datarep,
in SCDatasetAddwRep, and in DatasetLookupwRep.  dataset-dump is a
no-op for CIDR.  JSON/NDJSON format is rejected at DatajsonGet.

Unix socket dataset-add, dataset-remove, dataset-lookup, and
dataset-clear are wired through the CIDR helpers. DatasetClear()
takes both family write locks for the whole operation and
reinitializes the trees in place, so the operation is atomic from a
reader's point of view and the CIDRType struct and embedded locks
are never freed while workers hold a reference.

Lua: switch from DatasetFind(name, DATASET_TYPE_STRING) to
DatasetSearchByName(name) so Lua scripts can retrieve a CIDR set via
dataset.get(), and route dataset:add() through DatasetAddCIDRString
when the set type is CIDR so the Lua string is parsed as a CIDR
value rather than pushed as raw bytes.

Rust: add DSCIDR to the dataset type enum and process_cidr_set() so
the Rust dataset loader can parse CIDR entries from a load file. The
"reputation not supported for CIDR" warning is emitted once per file.

Also fixes a pre-existing bug in DetectDatasetSetup where the parse
return-code check only caught the value 0, not negative error codes,
so any parse-time -1 return (the new mask validation, and older
paths for duplicate save, bad type, oversize keys) silently loaded
rules with zeroed defaults.

Docs updated: type description, file format, example rules, mask
option, memcap semantics.

Issue: 8124
Takes one write lock per address family for the whole batch, so an
admin import of N CIDR entries drops from N wrlock acquisitions to
at most 2 (one per family). The existing dataset-add is one wrlock
acquisition per entry; 10k prefixes means 10k wrlock cycles, each
blocking the packet-path rdlock for one tree walk.

The new command takes a JSON array of values instead of a single
data argument:

  {"command": "dataset-add-batch",
   "arguments": {"setname": "blocklist", "settype": "cidr",
                 "values": ["10.0.0.0/8", "192.168.0.0/16", ...]}}

The reply reports how each value was handled:

  {"return": "OK",
   "message": {"added": N, "existed": N, "failed": N,
               "rejected_memcap": N}}

rejected_memcap is only present for CIDR since it's the only type
whose memcap can be exhausted mid-batch on this path. The batch is
not atomic: a malformed value or a memcap rejection is counted in its
own bucket while the rest of the batch proceeds.

DatasetAddCIDRBatch parses each input into per-family buckets without
any lock held, then takes the wrlock on each non-empty family and
walks the bucket in one hold. The rdlock-first optimization in the
single-add path is dropped inside the batch: the writer already holds
the lock, so a read-side probe would be wasted work.

For non-CIDR dataset types the handler falls back to looping through
DatasetAddSerialized. Hash-based sets already use per-bucket locks,
so batching them doesn't consolidate locks; the win is one
unix-socket round trip instead of N.

Trade-off called out in the docs: for CIDR the single continuous
wrlock hold is worse tail latency for the packet path during a bulk
import than the current per-entry pattern would be, but the total
blocked-reader time is smaller. Schedule bulk imports during quiet
windows.

Issue: 8124
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 43.66412% with 369 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.92%. Comparing base (acd9d3a) to head (2afabc5).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #16085      +/-   ##
==========================================
- Coverage   83.05%   82.92%   -0.14%     
==========================================
  Files        1004     1006       +2     
  Lines      277359   278007     +648     
==========================================
+ Hits       230370   230535     +165     
- Misses      46989    47472     +483     
Flag Coverage Δ
fuzzcorpus 61.26% <3.05%> (-0.27%) ⬇️
livemode 18.35% <3.35%> (-0.24%) ⬇️
netns 22.81% <0.30%> (-0.10%) ⬇️
pcap 45.17% <0.30%> (-0.25%) ⬇️
suricata-verify 67.06% <41.98%> (-0.09%) ⬇️
unittests 58.39% <0.00%> (-0.14%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@suricata-qa

Copy link
Copy Markdown

Information: QA ran without warnings.

Pipeline = 33247

@jlucovsky

Copy link
Copy Markdown
Contributor Author

Continued in #16090

@jlucovsky jlucovsky closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants