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)
+19 -2
View File
@@ -66,7 +66,7 @@ func TestDnsmasq(t *testing.T) {
}
func TestNftables(t *testing.T) {
s := Nftables(testNet())
s := Nftables(testNet(), nil)
for _, want := range []string{
"delete table inet pilens",
`iifname "wlan0" jump ap_forward`,
@@ -84,7 +84,24 @@ func TestNftables(t *testing.T) {
}
n := testNet()
n.Forwarding.Mode = "deny"
if !strings.Contains(Nftables(n), `"forwarding disabled"`) {
if !strings.Contains(Nftables(n, nil), `"forwarding disabled"`) {
t.Error("deny mode must drop forwarded traffic")
}
}
func TestNftablesInterception(t *testing.T) {
off := Nftables(testNet(), nil)
if strings.Contains(off, "redirect to") || strings.Contains(off, "ct status dnat") {
t.Fatal("interception rules present when disabled")
}
on := Nftables(testNet(), &Intercept{Enabled: true, Port: 8081})
for _, want := range []string{
"chain prerouting {",
`iifname "wlan0" meta l4proto tcp tcp dport { 80, 443 } redirect to :8081`,
"tcp dport 8081 ct status dnat accept",
} {
if !strings.Contains(on, want) {
t.Errorf("interception ruleset missing %q", want)
}
}
}