open
https://gitlab.synchro.net/main/sbbs/-/issues/1145
## Summary
The `.can` filter matching logic in `src/sbbs3/findstr.c` only supports **IPv4** dotted-quad CIDR notation. IPv6 CIDR entries in `ip.can` / `ip-silent.can` / `host.can` / etc. are silently treated as literal string patterns and never match a real connecting IPv6 client, because:
1. `parse_ipv4_address()` at `src/sbbs3/findstr.c:101` uses `sscanf("%u.%u.%u.%u", …)` — returns 0 for any IPv6 input string.
2. `findstr_compare()` at `src/sbbs3/findstr.c:138` guards the structured-CIDR branch with `if (ip_addr != 0 && (cidr = parse_cidr(pattern, &subnet)) != 0)` (line 155). IPv6 inputs therefore skip the CIDR path entirely and fall through to plain `findstr_in_string()` string-pattern matching.
3. `parse_cidr()` at line 112 also only accepts `%u.%u.%u.%u/%u` with `subnet ≤ 32`, and `is_cidr_match()` at line 125 does a 32-bit XOR shift (`((ip_addr ^ cidr) >> (32 - subnet))`) — structurally IPv4.
4. `find2strs_in_list()` at line 171 calls only `parse_ipv4_address()` on inputs (lines 180–181); `find2strs()` and `trash_in_list()` follow the same pattern. No caller in `src/sbbs3/trash.c` preprocesses IPv6 input either.
A search for `AF_INET6`, `inet_pton`, or IPv6 notation across `findstr.c` and `trash.c` returns zero hits.
## Why this matters beyond manual `.can` entries — the web rate-limit auto-filter
The web server's rate-limit auto-filter has IPv6 awareness on the **write** side: `rate_limit_key()` in `src/sbbs3/websrvr.cpp:1979` correctly bucketizes incoming IPv6 client addresses by `RateLimitSubnetPrefix6` using `inet_pton(AF_INET6, …)` (line 1994). When the violation threshold is hit, the auto-filter writes an entry like `2001:db8:1234:5678::/64` to `ip.can`.
But the **read-back** at the next `accept()` (via `trashcan()` → `findstr_in_list()`) doesn't understand IPv6 CIDR — so the entry the auto-filter just wrote is effectively inert. Subsequent connections from the abusive `/64` see only literal-string matching, which never matches a real client address. The IPv6 subnet auto-filter therefore appears to function (entries appear in `ip.can`, log lines say `!BLOCKING SUBNET`) but does not actually block the traffic it was meant to.
This is an asymmetry between the IPv6-capable rate-limit *writer* and the IPv4-only matching *reader*.
## Reproduction
1. With `[Web] RateLimitSubnetPrefix6 = 64`, `RateLimitFilterThreshold = N`, `RateLimitFilterDuration > 0` configured.
2. Have an IPv6 client (or sequence of clients sharing a `/64`) exceed the threshold on `
https://`.
3. Confirm `ip.can` gets an entry like `2001:db8:1234:5678::/64 t=… p=HTTPS r=N rate-limit violations …`.
4. Have any address from inside that `/64` (other than the literal `2001:db8:1234:5678::/64` string) connect again.
5. Observe: the connection is **not** dropped at `accept()` via the `.can` matcher (the auto-filter rate-limit logic still re-counts denials in-memory, but the persistent `.can` block does not trigger).
A manual `2001:db8::/32` entry placed by hand into `ip.can` exhibits the same: it does not match real IPv6 clients from inside that prefix.
## Suggested direction
Add an IPv6 parallel to `parse_ipv4_address` / `parse_cidr` / `is_cidr_match`. Concretely:
- `parse_ipv6_address(str, uint8_t addr[16])` using `inet_pton(AF_INET6, …)`. - `parse_ipv6_cidr(p, uint8_t addr[16], unsigned* subnet)` accepting `<v6-addr>/<0-128>`.
- `is_ipv6_cidr_match(uint8_t ip[16], uint8_t cidr[16], unsigned subnet)` — byte-and-bit prefix comparison (cap `subnet ≤ 128`).
- In `findstr_compare()`, detect input family by presence of `:` (mirroring `rate_limit_key`'s heuristic at `websrvr.cpp:1984`) and dispatch to the matching family's CIDR machinery.
- In `find2strs_in_list()` / `find2strs()`, parse both families up-front into a small `union { uint32_t v4; uint8_t v6[16]; }` so the per-pattern loop doesn't re-parse.
The existing IPv4 path stays unchanged and remains the hot path; the v6 path activates only when the input contains `:`.
Pattern lines without CIDR (bare IPv6 addresses, prefix-less) continue to use `findstr_in_string()` as today — no regression there.
## Related notes / context
- The defect doesn't affect bare-IPv6-address entries in `.can` files — those still match by exact case-insensitive string comparison via `findstr_in_string()`, which works fine for a single host address. Only the **CIDR / subnet** form is broken.
- The `~` / `^` / `*` wildcard forms documented in `findstr_in_string()` (line 36 comments) operate on characters, not address bits, and don't substitute for proper CIDR matching.
- I haven't checked whether the SCFG UI or `scfg/scfgfltr.c` previews/validates `.can` entries — if it does, a friendly "IPv6 CIDR not yet supported" hint there would help sysops avoid creating non-functional entries.
- The same matcher is used for `host.can`, `subject.can`, `name.can`, etc., but only `ip.can` / `ip-silent.can` semantically deal with address-family CIDR; the others are unaffected.
— *Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.37-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)