diff --git a/docs/P1.md b/docs/P1.md index 4b85a90..d565a67 100644 --- a/docs/P1.md +++ b/docs/P1.md @@ -193,3 +193,31 @@ sift serve … (the service) 4. Install the skill: `skill/SKILL.md` → `~/.hermes/skills/web/sift/SKILL.md`; add the known-limitation note (§2) to it. 5. Kyle restarts talos's gateway. Then the exit-criteria evidence (§1). + +## 9. P1.1 — dynamic self-IP deny (Kyle, 2026-09-22) + +The home WAN address is residential DSL and changes. A static `/32` in `denyPrefixes` goes +stale silently (and the old address gets reassigned to a stranger). Add **self-address +discovery** to sift itself so the hairpin-NAT block follows the real address: + +- Config `self_ip` block: + `sources = ["ddns:", "trace:https://1.1.1.1/cdn-cgi/trace"]`, + `interval = "10m"`, `keep_previous = "24h"`. + - `ddns:` — resolve the router's DDNS name (preferred if one exists, e.g. MikroTik + IP Cloud `…sn.mynetname.net`); no outbound HTTP needed. + - `trace:` — GET through the **guarded** fetcher, parse the `ip=` line (Cloudflare + trace format). A result that is not a global unicast address is ignored. +- Discovered addresses (v4 /32, v6 /64) are added to the dial-time deny set alongside + `denyPrefixes`; previous addresses are kept for `keep_previous` so a change mid-request + can't open a window. +- Discovery runs **before** the listener opens (bounded wait, e.g. 10 s); on failure sift + starts with the static `denyPrefixes` plus the last persisted value + (`/var/lib/sift/self_ip.json`) and logs WARN; it never serves with an *empty* self set if a + previous value exists. +- Swap the deny set atomically (`atomic.Pointer`); `/healthz` reports `self_ip` age and + source; `/metrics` counts discovery failures. +- Tests: injected resolver/trace returning A then B → both denied until B + keep_previous; + non-global answer ignored; discovery failure keeps the persisted value. + +Until P1.1 lands, deploy with the current address as a static entry +(`denyPrefixes = [ "99.73.161.224/32" ]` in `hw/orion`).