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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user