• ip.can / .can filter matching does not support IPv6 CIDR notation (web

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Fri May 22 17:40:24 2026
    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)
  • From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Fri May 22 18:27:58 2026
    close https://gitlab.synchro.net/main/sbbs/-/issues/1145
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Fri May 22 18:27:58 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1145#note_9011

    Fixed in commit eee438b0b on `master`.

    **Code** (`src/sbbs3/findstr.c`):

    - Added IPv6 parallel functions: `parse_ipv6_address` (via `inet_pton(AF_INET6, …)`), `parse_ipv6_cidr`, and `is_ipv6_cidr_match` (byte-then-bit prefix compare).
    - `findstr_compare()` now dispatches to the right family by which one the input parsed as; `find2strs_in_list()` and `find2strs()` pre-parse both forms up-front into a small internal `findstr_ip_t`. Public API in `findstr.h` unchanged.
    - Two latent IPv4 bugs surfaced by the new test cases were fixed alongside:
    - `is_cidr_match()`'s `>> (32 - subnet)` is undefined behavior when `subnet == 0` (a 32-bit shift by 32); on x86 it modulo-32s and `/0` never matched anything except via exact host hit. Now guarded.
    - `parse_cidr()` returned `uint32_t` with `0` overloaded as both "parse failed" and "0.0.0.0", so the literal pattern `0.0.0.0/0` (the canonical "match-any IPv4") fell through to string-only matching. Changed to `bool` + out-param, matching the new IPv6 convention.

    **Tests** (`exec/tests/system/findstr.js`, new):

    50+ assertions covering string-pattern features (exact, case-insensitive, `~`/`^`/`*` wildcards, `;` comment, `!` reverse-match), IPv4 CIDR (including `/0` and boundary stress), IPv6 exact + CIDR (`/32` through `/128`, with bit-boundary stress at `/33`), `!CIDR` reverse-match for both families, cross-family no-false-match, and malformed-pattern rejection. Silent pass under `jsexec exec/tests/system/findstr.js`; throws on failure.

    **Docs**: Updated [`config:filter_files`](https://wiki.synchro.net/config:filter_files) on the wiki — split the former `IPv4 CIDR Notation` section into parallel `IPv4 CIDR Notation` and `IPv6 CIDR Notation` H4s, removed the "IPv6 CIDR notation is not supported at this time" line, added the `/0` correctness note to the IPv4 section, and added an "upgrading from v3.21" note about `ip.can` files carrying inert IPv6 entries written by the rate-limit auto-filter (now enforced correctly).

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)