Skip to content

Merge dev into master: #78 whitelist arc + single-instance lock (#433) + port-in-use fix (#434)#435

Merged
Aybook merged 11 commits into
masterfrom
dev
Jul 14, 2026
Merged

Merge dev into master: #78 whitelist arc + single-instance lock (#433) + port-in-use fix (#434)#435
Aybook merged 11 commits into
masterfrom
dev

Conversation

@Aybook

@Aybook Aybook commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Promote the current dev to master. Testhub-validated (whitelist verified live), CI-green on dev (9d676cd), each feature already through its own §1a.6 review.

Contents (30 files, +3475/-17)

#78 Allowlist / whitelist arc (Phases A-D) - PRs #427/#431/#429/#430

  • core/whitelist.lua global IP/CIDR allowlist engine (Phase A)
  • +whitelist admin plugin + bundled hublist-pinger seed (Phase B)
  • IP-blocking plugins (etc_geoip / etc_proxydetect / usr_hubs) consult the whitelist first (Phase C)
  • HTTP API GET/POST/DELETE /v1/whitelist (Phase D)
  • Model-A precedence: whitelist beats the AUTOMATED blockers, a manual +ban/+blocklist still wins.

#433 single-instance lock - refuse a second hub from the same install dir (hub/hub.c).

#434 clear "port already in use" message + Windows port-hijack fix (core/server.lua drops SO_REUSEADDR on Windows; core/hub.lua logs actionable guidance).

Docs currency - CLAUDE.md / DEVELOPMENT.md (#78 arc status + old-Windows support).

Merge as a merge commit (not squash) per CLAUDE.md §8. No release tag.

Aybook and others added 11 commits July 13, 2026 17:22
 Phase A)

The allowlist deferred from the unified-blocklist arc. A new core module (a
stripped-down sibling of core/blocklist.lua: same bucketed radix cache, hex
.tbl store, v4-mapped-v6 lookup; no source-priority / stealth / rollup)
exposes whitelist.is_whitelisted(ip) as a sandbox global so every IP-blocking
path can consult it.

blocklist.check_ip applies Model-A precedence: a whitelisted IP overrides an
AUTOMATED block (geoip / proxycheck / external feed) but NOT a manual pin
(source="manual" wins) - an operator can still block a specific IP inside a
whitelisted range. The whitelist call is pcall-guarded on the accept path
(fail closed - an error keeps the block).

- core/whitelist.lua (new): store + buckets + matcher + add/remove/list/
  count/reload/init; IPv4/IPv6/CIDR, expires_at TTL, cfg-reload listener.
- core/blocklist.lua: precedence hook + local pcall = use "pcall".
- core/init.lua: register whitelist in _core (after ipmatch, before blocklist).
- core/scripts.lua: expose the whitelist sandbox global.
- core/cfg_defaults.lua: whitelist_enabled (default true) + whitelist_store_path.
- tests/unit/whitelist_test.lua (63 checks incl. a blocklist-integration block
  proving Model-A precedence; provably fails pre-fix, §1a.7); registered on
  both smoke.yml legs. blocklist_test stub gains pcall.

Phase A of a 4-PR feature (B: +whitelist plugin + hublist-pinger seed; C:
per-plugin guards; D: HTTP /v1/whitelist). 3.2.x only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…llowlist, Phase B)

Operator-facing chat command over the Phase A engine (core/whitelist.lua):
+whitelist show/add/del/count/export/import. Mirrors etc_blocklist's
structure (verb dispatch, by_level hierarchy guard, JSONL export/import
with control-byte stripping + master-only import gate, opchat report +
audit fire-sites) minus stealth and HTTP.

Bundled seed: on the first run (store .tbl missing) the plugin seeds a
small set of known hublist-pinger IPs (source="pinger", v6 /64, v4 /32)
so pingers are exempt from the automated blockers out of the box.
Seed-on-missing only; operator edits are never re-seeded. Disable with
etc_whitelist_seed=false.

- scripts/etc_whitelist.lua (new) + lang en/de.
- core/cfg_defaults.lua: etc_whitelist_* keys.
- examples/cfg/cfg.tbl: settings block + ships enabled.
- tests/unit/etc_whitelist_test.lua (53 checks) + a +whitelist roundtrip
  smoke that validates the seed on first boot; both smoke.yml legs.

§1a.6 review: no blockers; addressed a dedicated msg_save_failed (vs the
doubled "failed:" message), the /64 co-tenant breadth caveat in the seed
comment, and a de i18n capitalisation. Phase B of the 4-PR feature. 3.2.x.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…allowlist, Phase C)

etc_geoip / etc_proxydetect / usr_hubs each gain a one-line
`if whitelist.is_whitelisted(ip) then return end` guard at the top of
their per-connection decision, so a whitelisted IP (hublist pinger /
trusted infra) is exempt. For etc_proxydetect the guard sits before the
cache/quota/HTTP so a trusted IP never costs a paid provider query. This
silences the recurring PROXYDETECT/GEOIP/USER-HUBS log lines for
whitelisted IPs.

- scripts/etc_geoip.lua: guard before classify().
- scripts/etc_proxydetect.lua: guard before cache/quota/query.
- scripts/usr_hubs.lua: guard before both the invalid-hubcount kick and
  the hub-limit ban.
- etc_geoip_test + etc_proxydetect_test: whitelist-exemption regressions
  that provably fail without the guard (§1a.7). usr_hubs has no harness;
  its guard is the identical one-line mirror (diff-is-self-evidence).
- docs/DEVELOPMENT.md: the "auto-kick plugins consult the whitelist"
  convention for plugin authors.

Scope: covers the IP-reputation blockers + hub-limit ban; the
share/slots/nick-policy plugins are not yet whitelist-aware (follow-up).
Phase C of the 4-PR feature. 3.2.x.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t, Phase D)

etc_whitelist v0.02 gains four HTTP endpoints mirroring the blocklist API:
GET /v1/whitelist (read, filter/sort/paginate), GET /v1/whitelist/counts
(read), POST /v1/whitelist (admin), DELETE /v1/whitelist/{id} (admin).
Source enum {manual, pinger}, no stealth. HTTP admin tokens bypass the ADC
hierarchy guard (token = trust surface; by_level=100).

- scripts/etc_whitelist.lua: HTTP handlers + onStart registration (guarded
  by hub.http_register).
- tests/unit/etc_whitelist_test.lua: +32 HTTP checks (registration, scope,
  schema, all four handlers, happy + error).
- tests/smoke/run.py: test_http_phase_d_whitelist - real token-authed
  POST -> GET -> DELETE roundtrip, seed-aware baseline capture.
- docs: HTTP_API.md (4 endpoints + footnotes), SCRIPTS.md (etc_whitelist
  section), BLOCKLIST.md (allowlist layer), CLAUDE.md §5.

§1a.6 review: no blockers; addressed the HTTP_API.md docs-currency gap.
Completes the 4-PR #78 allowlist arc. 3.2.x.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(whitelist): global IP/CIDR allowlist engine (#78 allowlist, Phase A)
feat(whitelist): +whitelist admin plugin + bundled pinger seed (#78 allowlist, Phase B)
feat(whitelist): IP-blocking plugins consult the whitelist first (#78 allowlist, Phase C)
feat(whitelist): HTTP API /v1/whitelist (#78 allowlist, Phase D)
- CLAUDE.md §5: whitelist arc A-D merged to dev (PRs #427/#431/#429/#430);
  the Sopor sysinfo CIM->WMI old-Windows fix (#432) + KB2999226/PS-2.0 note.
- DEVELOPMENT.md §4: old-Windows hubowner traps (KB2999226 UCRT +
  Get-WmiObject fallback for PS 2.0; nil-probe-must-degrade rule).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…433)

Two hubs run from one install tree share cfg/user.tbl, master.key, the
plugin scripts/data/*.tbl stores and the logs; interleaved
saveusers()/savetable() writes can corrupt them (data loss). Nothing
stopped this - on Windows core/server.lua's pre-bind SO_REUSEADDR (the
#128 fast-restart fix) even lets the second process re-bind the same
port, so both hubs run and race silently.

Add a single-instance lock in the C launcher, before Lua/socket/user.tbl
init: a per-install lock file (luadch.lock in the install dir) held for
the whole process lifetime. unix flock(LOCK_EX|LOCK_NB) + O_CLOEXEC;
Windows exclusive CreateFile (deny-share, non-inheritable,
delete-on-close). Released by the OS on crash (no stale-lock cleanup),
retained by the daemon child across the -d fork and across +reload. Scope
is per directory, so a test hub and a prod hub in separate dirs still run
in parallel; only a second copy of the SAME install is refused (non-zero
exit + "another instance is already running"). Fail-open: only the
definitive already-held signal (unix EWOULDBLOCK, Windows
ERROR_SHARING_VIOLATION) blocks boot.

Smoke: a new stage starts a second hub against the live staging tree and
asserts it exits non-zero with the refusal while the first stays up;
provably fails pre-fix (Windows unpatched binary boots the second
instance and runs it concurrently).

Two-pass review (per the working agreement) caught the missing O_CLOEXEC
(a spawned io.popen child could otherwise inherit the fd and brick the
next restart) and the ERROR_ACCESS_DENIED-is-not-a-live-instance
misclassification.

Reported by Sopor. Docs: CHANGELOG + docs/BUILDING.md "Single instance
per install". 3.2.x only, not backported.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n Windows (#434)

When a listener could not bind its port the hub was unhelpful: on Linux
it logged only the cryptic luasocket "...bind: address already in use"
line then silently retried every 30s; on Windows the pre-bind
SO_REUSEADDR (added for the Linux #128 TIME_WAIT fast-restart) has
SO_REUSEPORT semantics, so a second process re-bound the SAME port with
no error at all - two hubs silently shared it and any local process could
hijack a luadch port.

- core/server.lua: set SO_REUSEADDR only on Linux (detected once at
  module load via util.path_sep()); on Windows it is skipped both pre-
  and post-bind, so a same-port second bind fails visibly with
  WSAEADDRINUSE and the port can no longer be hijacked. Linux keeps the
  #128 fast-restart unchanged. Also fixes a latent post-bind return: a
  keepalive-only setoption failure now returns the real error (err or
  err2) instead of nil.
- core/hub.lua add_server_handler: log a clear operator line on a port
  clash ("... change the port in cfg/cfg.tbl ...") and a generic line for
  any other previously-swallowed listener-start failure.

Verified dropping SO_REUSEADDR on Windows does NOT regress the fast
restart: the full Windows smoke restart battery (plaintext / tier /
+reload / dual-stack same-port #107) stays green (Windows does not hold a
listening socket in TIME_WAIT). Smoke: a new stage starts a second hub
(separate dir, same ports as the live first hub) and asserts the clear
guidance appears; provably fails pre-fix (the assertion is on the new
"change the port in cfg/cfg.tbl" phrase the pre-fix cryptic line lacks;
pre-fix Windows silently re-binds and logs nothing).

Follow-up to the single-instance lock (same Sopor thread): the lock stops
a second copy in the SAME dir; this makes a second hub in a DIFFERENT dir
with clashing ports fail loudly instead of silently. 3.2.x only.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Aybook
Aybook merged commit 6ece33c into master Jul 14, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant