aboutsummaryrefslogtreecommitdiff
path: root/docs/design/2026-06-30-captive-portal-login.org
blob: 17396894226ecd7ddaaba6d64889b30d0d9065a4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#+TITLE: Captive-portal login — learnings + baking it into the net panel
#+DATE: 2026-06-30
#+SOURCE: the 2026-06-30 Hyatt wifi saga (velox)

* Why this exists

On a locked-down-DNS laptop, captive portals never show their login page, even
though phones get on fine. We spent hours on a Hyatt portal before finding the
mechanism; this captures it so the fix becomes a panel feature instead of a
one-off script.

* The mechanism (what actually blocks the login)

A redirect portal works by *DNS hijack*: you query a name, the hotel's resolver
hands back the portal, you get the login page. Two things on velox stop that:

- *System resolver forces DNS-over-TLS.* =/etc/systemd/resolved.conf.d/dns-over-tls.conf=
  hardcodes =DNS=1.1.1.1#... 9.9.9.9#...= with =DNSOverTLS=yes=. The system never
  queries the hotel's resolver at all. The hotel blocks 853 (DoT) and external
  53, so system DNS is simply dead on the portal — only 443 (DoH) gets out.
- *Browser DoH.* Chrome "secure DNS" on bypasses the hotel DNS too, so the
  browser never gets redirected either.

A phone works because it uses *plain DNS* from the hotel plus a built-in
captive-portal popper. The laptop has neither.

Confirmed facts from the saga:
- Front desk: it's a normal redirect-to-login portal. Phone: connects fine.
- No DHCP option 114 (RFC 8910) — the portal doesn't advertise its URL. But the
  URL is recoverable from the HTTP 302 once you're on plain DNS.
- The walled garden whitelists OS captive-detection endpoints
  (=captive.apple.com= returns "Success") — a *misleading* signal, not real
  internet. Don't trust it.
- 443/DoH egress works broadly on the portal; only port-53 DNS is held. So
  "system DNS fails" never means "no internet" here.

* The working fix (=~/.local/bin/hotel-wifi=, to be folded in)

Temporarily disable DoT → plain hotel DNS → discover the portal URL from the
redirect → open it in a clean browser profile (no DoH, no stale HSTS/cookies) →
click the button → restore DoT. Reversible; tested to restore cleanly.

#+begin_src sh
#!/bin/sh
# hotel-wifi        disable DoT -> find the portal login URL -> open it
# hotel-wifi off    restore normal encrypted DNS (run once online)
conf=/etc/systemd/resolved.conf.d/dns-over-tls.conf
if [ "${1:-on}" = "off" ]; then
    [ -f "$conf.captive-disabled" ] && sudo mv "$conf.captive-disabled" "$conf"
    sudo systemctl restart systemd-resolved
    echo "Encrypted DNS (DoT) restored."; exit 0
fi
[ -f "$conf" ] && sudo mv "$conf" "$conf.captive-disabled"
sudo systemctl restart systemd-resolved; sleep 1
resolvectl flush-caches 2>/dev/null || true
portal=""
for t in http://captive.apple.com/hotspot-detect.html http://neverssl.com \
         http://detectportal.firefox.com/canonical.html; do
    loc=$(curl -sS -m 6 -o /dev/null -w '%{redirect_url}' "$t" 2>/dev/null)
    [ -n "$loc" ] && { portal="$loc"; break; }
    url=$(curl -sS -m 6 "$t" 2>/dev/null | grep -ioE 'https?://[^"'"'"' >]+' \
          | grep -ivE 'apple\.com|neverssl|firefox|w3\.org|gstatic' | head -1)
    [ -n "$url" ] && { portal="$url"; break; }
done
prof=$(mktemp -d)
setsid -f google-chrome-stable --user-data-dir="$prof" "${portal:-http://neverssl.com}" >/dev/null 2>&1
echo "Click the login button. When online: hotel-wifi off"
#+end_src

* Baking it into the net panel (the task)

- The net engine already diagnoses captive / no-internet. When it sees a held
  portal, the panel should offer a first-class *"Log in to this network"*
  action that runs the plain-DNS + clean-browser flow above, reversibly, and
  auto-restores DoT when connectivity returns (or on a timeout).
- Reconcile with the existing =net portal= command and the =captive= helper —
  they assumed a DNS-hijack-to-gateway model that did NOT match this portal
  (gateway served no web; DNS was held, not hijacked-to-portal). The plain-DNS
  approach is the one that worked; make it the engine's portal path.
- The DoT toggle must be safe and reversible (the =off= step). Consider a
  per-connection or time-boxed DoT-off that can't strand encrypted DNS.
- Surface the misleading-"Success" lesson: a whitelisted captive-check passing
  is not "online" — gate on a real, non-whitelisted fetch.

* Related fix that unblocked the panel (already shipped)

The panel could never switch networks because =net up= placed =--wait= after the
nmcli subcommand (it's a global option). Fixed in dotfiles 2432311; fake-nmcli
now rejects the misplaced flag so it can't regress.