Phase 4: authorised HTTPS interception (Mode B)

Toggleable transparent interception via mitmdump in a venv. Own RSA CA generated
in-app, downloadable (public cert only, key never leaves the box), with per-OS
install guide and fingerprint. nftables redirect accepts only dnat'd connections
(never an open proxy). HTTP Explorer with headers and opt-in body viewers (JSON/
text/hex/image, sandboxed). Honest status: pinned/untrusting = reported errors,
excluded hosts pass through, bodies off by default. Helper op + systemd unit +
installer venv build + boot restore.
This commit is contained in:
Andy Dixon
2026-10-10 08:05:42 +01:00
parent 4320428f41
commit 5dbf85f84e
25 changed files with 1460 additions and 13 deletions
+27 -2
View File
@@ -129,11 +129,19 @@ func DnsmasqConf(n config.Network) string {
func q(s string) string { return `"` + s + `"` } // inputs are validated interface names
// Intercept describes transparent HTTPS-interception redirection. Port 0 / nil
// means interception is off and no redirect rules are emitted.
type Intercept struct {
Enabled bool
Port int
}
// Nftables renders the complete inet pilens table. The output begins by
// (re)creating and deleting the table so `nft -f` replaces it atomically while
// leaving every other table untouched.
func Nftables(n config.Network) string {
// leaving every other table untouched. ic may be nil (interception off).
func Nftables(n config.Network, ic *Intercept) string {
ap, up := q(n.AP.Interface), q(n.Uplink)
intercept := ic != nil && ic.Enabled && ic.Port > 0
sub := n.LAN.Subnet().String()
var mgmtIf []string
for _, i := range n.Management.Interfaces {
@@ -186,6 +194,12 @@ func Nftables(n config.Network) string {
w("\t\tip daddr %s udp dport 53 accept", n.LAN.Gateway())
w("\t\tip daddr %s tcp dport 53 accept", n.LAN.Gateway())
w("\t\ticmp type { echo-request, destination-unreachable, time-exceeded } accept")
if intercept {
// Accept ONLY connections the prerouting redirect created (ct status dnat).
// A client connecting directly to the proxy port is not dnat'd, so this
// never turns the proxy into an open proxy reachable by clients.
w("\t\ttcp dport %d ct status dnat accept comment \"redirected to interception proxy\"", ic.Port)
}
w("\t\tcounter drop comment \"AP clients cannot reach the appliance\"")
w("\t}")
@@ -213,6 +227,17 @@ func Nftables(n config.Network) string {
w("\t\tcounter drop")
w("\t}")
// Transparent interception: redirect AP-client HTTP(S) to the local proxy.
// Only when enabled, and only new connections from the AP interface. The
// proxy's own upstream connections originate on the Pi (not iifname ap), so
// they are never redirected — no loop.
w("\tchain prerouting {")
w("\t\ttype nat hook prerouting priority dstnat; policy accept;")
if intercept {
w("\t\tiifname %s meta l4proto tcp tcp dport { 80, 443 } redirect to :%d", ap, ic.Port)
}
w("\t}")
w("\tchain postrouting {")
w("\t\ttype nat hook postrouting priority srcnat; policy accept;")
w("\t\tip saddr %s oifname %s masquerade", sub, up)