Filter AAAA at Pi-hole on hosts without IPv6 egress (v0.1.94) #14

Merged
backmeupplz merged 1 commit from myg-fix-exit-node-ipv6-aaaa into master 2026-06-01 23:20:37 +00:00
backmeupplz commented 2026-06-01 23:19:28 +00:00 (Migrated from github.com)

Problem

The Tailscale exit node advertises a ::/0 route. On a host whose upstream provides no IPv6 (e.g. behind an ISP gateway that doesn't delegate a v6 prefix — confirmed on the production box, which only has a WireGuard ULA fd08:: and no v6 default route), that route is a black hole:

  1. An exit-node client resolves a dual-stack site through Pi-hole and gets an AAAA record.
  2. Because ::/0 is advertised, the client tunnels the IPv6 connection to the exit node.
  3. The host has nowhere to forward it → the packet dies.
  4. The client's Happy-Eyeballs algorithm waits for the IPv6 attempt to time out before falling back to IPv4 — stalling every connection.

Fix

Break the chain at DNS. When the host has no IPv6 egress, set Pi-hole's filter-AAAA so clients only ever receive IPv4 and never attempt the dead path. Validated live against the production Pi-hole (filter-AAAA → AAAA returns empty, A still resolves).

  • stats::host_has_ipv6_egress() — zero-packet UDP route probe (same technique as get_server_ip). Treats link-local (fe80::/10) / ULA (fc00::/7) only hosts as having no egress.
  • apps/pihole.yml — new FTLCONF_misc_dnsmasq_lines: "${PIHOLE_DNSMASQ_LINES}".
  • apps.rs::build_merged_env — fills that var with filter-AAAA when there's no egress, empty otherwise.

Conditional & self-healing: re-evaluated on every compose render, so dual-stack installs are unaffected and the filter clears automatically if real IPv6 egress later appears.

Tests

  • stats::global_unicast_v6_classification covers global / loopback / unspecified / link-local / ULA.
  • Registry test locks the template↔code wiring.
  • Full lib suite green (the one failing paperless test fails identically on master — environmental /tmp storage guard, unrelated).

🤖 Generated with Claude Code

## Problem The Tailscale exit node advertises a `::/0` route. On a host whose upstream provides **no IPv6** (e.g. behind an ISP gateway that doesn't delegate a v6 prefix — confirmed on the production box, which only has a WireGuard ULA `fd08::` and no v6 default route), that route is a black hole: 1. An exit-node client resolves a dual-stack site through Pi-hole and gets an **AAAA** record. 2. Because `::/0` is advertised, the client tunnels the IPv6 connection to the exit node. 3. The host has nowhere to forward it → the packet dies. 4. The client's Happy-Eyeballs algorithm waits for the IPv6 attempt to **time out** before falling back to IPv4 — stalling every connection. ## Fix Break the chain at DNS. When the host has no IPv6 egress, set Pi-hole's `filter-AAAA` so clients only ever receive IPv4 and never attempt the dead path. Validated live against the production Pi-hole (`filter-AAAA` → AAAA returns empty, A still resolves). - `stats::host_has_ipv6_egress()` — zero-packet UDP route probe (same technique as `get_server_ip`). Treats link-local (`fe80::/10`) / ULA (`fc00::/7`) only hosts as having **no** egress. - `apps/pihole.yml` — new `FTLCONF_misc_dnsmasq_lines: "${PIHOLE_DNSMASQ_LINES}"`. - `apps.rs::build_merged_env` — fills that var with `filter-AAAA` when there's no egress, empty otherwise. **Conditional & self-healing:** re-evaluated on every compose render, so dual-stack installs are unaffected and the filter clears automatically if real IPv6 egress later appears. ## Tests - `stats::global_unicast_v6_classification` covers global / loopback / unspecified / link-local / ULA. - Registry test locks the template↔code wiring. - Full lib suite green (the one failing `paperless` test fails identically on master — environmental `/tmp` storage guard, unrelated). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No description provided.