BBS:      TELESC.NET.BR
Assunto:  ip.can / .can filter matching does not support IPv6 CIDR notation (web
De:       Rob Swindell
Data:     Fri, 22 May 2026 17:40:24 -0700
-----------------------------------------------------------
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 `/<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*
n
---
  mSynchronetn  hgVertrauen n hHome of Synchronet n gh[vert/cvs/bbs].synchro.net

-----------------------------------------------------------
[Voltar]