The API test suite asserts exact DNS query counters. Several of them
(`TOTAL`, `DNSKEY`, `DS`, `TOP_DOMAIN`) depend on how many DNSKEY/DS
lookups dnsmasq issues while validating DNSSEC. Until now those lookups
recursed to the live ICANN root, because FTL configures the real root
trust anchors whenever `dns.dnssec` is enabled and the local PowerDNS
recursor had no root zone of its own. The number of root DNSKEY queries
therefore tracked ICANN's published root key set, so an ongoing
key-signing-key rollover silently shifted the counters (9 -> 7 DNSKEY)
and broke the suite even on unrelated PRs.
We make the whole suite hermetic:
1. Serve a locally-signed root zone from PowerDNS, forward `.` to it and
trust its key, so root DNSKEY validation resolves inside the test
environment instead of reaching the internet.
2. Mark the locally-served *unsigned* zones (`icloud.com`,
`apple-dns.net`, `in-addr.arpa`, `ip6.arpa`) as local domains, so
dnsmasq no longer proves them unsigned by walking up to the real root.
3. Give the `bogus` zone a deliberately mismatched local trust anchor so
it fails validation locally rather than by failing to find a secure
delegation at the root.
4. Drop the root-key pre-warm `dig`, an internet round-trip that no
longer serves any purpose.
With no query leaving for the real root the counters are stable and
independent of ICANN key rollovers, so they are recalibrated
accordingly. Marking the extra zones as local emits the same "negative
DS reply without NS record" warning already whitelisted for `ftl`, so
the `test_final` whitelist is broadened to match it for any zone.
Signed-off-by: DL6ER <dl6er@dl6er.de>
Cover three fixes from the security pass: session cookies carry the Secure attribute only over TLS (and never over plain HTTP), an accepted TOTP code cannot be replayed, and /api/queries clamps an unbounded length parameter. The cookie and TOTP tests live in a self-contained module so they do not disturb the order-dependent workflow in test_z_auth.py; the TOTP test clears the shared secret via the session created by the accepted login so cleanup neither trips the replay guard nor emits a 2FA warning. Account for the module's four extra pihole.toml writes in test_final.bats.
Signed-off-by: DL6ER <dl6er@dl6er.de>
Issue #2818 reports FTL terminating cleanly (code 0) after ~15-16 hours of
uptime, with the only captured log lines being:
Thread webserver (7) is idle, terminating it.
All threads joined
########## FTL terminated after 15h 50m 45s (code 0)! ##########
A thorough audit of all paths that can set the global `killed` flag confirms
there is no internal code path leading to spontaneous termination - every
shutdown is either test-mode (`pihole-FTL test`), a `die()`/longjmp from
dnsmasq, or an external SIGTERM mapped to dnsmasq's EVENT_TERM via SIGUSR6.
The "Thread webserver (7) is idle" message is a red herring: the webserver
cert-renewal thread sleeps for 24 hours between checks, so it is essentially
always cancellable when shutdown begins.
The reason the reporter could not diagnose the cause is that the relevant
information was either logged at DEBUG level (and therefore invisible) or
emitted far above the final termination message (and therefore lost when the
log was truncated). This commit closes both gaps so the next occurrence will
be diagnosable from a tail of FTL.log alone:
- signals.c: persist the SIGTERM sender info ("name (PID, user UID)") into
a static buffer in SIGTERM_handler, exposed via get_term_source(). The
buffer is sized 256 bytes and only ever read after being written by the
signal handler, so no locking is needed.
- daemon.c: in cleanup(), re-log the stored SIGTERM source as
"Terminated by ..." right before the final "FTL terminated after"
banner. Even when the log is truncated to the last few lines, the
termination source will now be visible alongside the final message.
- main.c: promote the "Shutting down (exit code, jmpret)" log from
log_debug to log_info so the entry into the cleanup path is always
recorded, not just under DEBUG_ANY.
- webserver/webserver.c: split the webserver thread's 24-hour
cert-check sleep into 24 x 1-hour sleeps with intermediate `killed`
checks. This is a robustness improvement, not a bug fix: the thread
is no longer marked as cancellable for 24 hours straight, so the
misleading "is idle" log on shutdown happens at most an hour after
the last cert check rather than at any point in a 24-hour window.
- test/test_final.bats: add two regression tests that run after the
existing "FTL terminates with message" test:
* Verify "INFO: Shutting down (exit code" is present (catches
accidental regression of the log_debug -> log_info change).
* Verify "INFO: Terminated by" is present AND that the three key
messages appear in order: Shutting down -> Terminated by ->
FTL terminated after.
After this change, a SIGTERM-triggered shutdown produces:
INFO: Asked to terminate by "systemd" (PID 1, user root UID 0)
...
INFO: Shutting down (exit code 0, jmpret 0)
INFO: Finished final database update
INFO: Waiting for threads to join
INFO: Thread webserver (7) is idle, terminating it.
INFO: All threads joined
INFO: Terminated by "systemd" (PID 1, user root UID 0)
INFO: ########## FTL terminated after 15h 50m 45s (code 0)! ##########
Signed-off-by: Dominik <dl6er@dl6er.de>
Add test/api/test_s_auth_stress.py exercising the auth subsystem under
concurrent load (12 threads, kept below max_sessions=16). Sessions are
created sequentially (respecting FTL's 3-attempts/s rate limit with
retry-on-429 backoff), then concurrency targets the thread-safety
surfaces:
- Parallel session validation (concurrent GET /api/auth with SIDs)
- Concurrent logout with cross-session isolation checks (logout half,
verify other half survives)
- Mixed session check/logout operations from a shared session pool
- Concurrent wrong-password rejection (verifies no SIGSEGV; runs last
since it intentionally triggers rate-limiting)
Every test cleans up its sessions to avoid exhausting max_sessions
across test boundaries. Bump expected config rotation count in
test_final.bats from 14 to 16 for the stress test's password
set/remove cycle.
Also add test/libs/ to .gitignore per review feedback — the directory is
populated at test time by test/run.sh cloning bats-core.
See alse: https://github.com/pi-hole/FTL/pull/2835
Signed-off-by: Dominik <dl6er@dl6er.de>
Add pytest tests for all previously untested GET API endpoints:
dns/blocking, domains (all type/kind combinations and single lookup),
groups, stats/summary, stats/top_domains, stats/top_clients,
stats/upstreams, stats/query_types, stats/recent_blocked,
stats/database (error handling), dhcp/leases, endpoints, info/ftl,
info/login, info/version, info/messages, info/client, info/database,
info/system, network/devices, network/interfaces, logs (dnsmasq, ftl,
webserver), and padd.
All assertions use exact expected values derived from the deterministic
BATS DNS query seeding (137 total queries, 49 blocked, 47 forwarded,
41 cached, 11 active clients, 8 gravity domains). On failure, the
full JSON response is dumped to /tmp/ftl_test_*.json for easy
inspection.
Fix double-free bug in printFTLenv() (src/config/env.c): when
printFTLenv() was called more than once (e.g. after config reload
triggered by the CLI password test), it would free item->error a
second time because neither the pointer nor the error_allocated flag
were reset after the first free. This produced "Trying to free NULL
pointer in printFTLenv()" warnings. Fix: set item->error = NULL and
item->error_allocated = false after freeing.
Files modified:
src/config/env.c — reset error/error_allocated after free
test/api/test_api.py — add 34 new endpoint tests (22 -> 56 total)
Signed-off-by: Dominik <dl6er@dl6er.de>