macOS transparent proxy is loopback-only and can't recover original destination
#248 opened on Jun 26, 2026
Repository metrics
- Stars
- (266 stars)
- PR merge metrics
- (PR metrics pending)
Description
Background
#247 fixed two bugs that prevented meow's macOS (pf) transparent proxy from working at all (pf rule ordering → listener never started; anchor not nested under com.apple/* → rules never evaluated). After those fixes the host's own loopback-destined connections are now intercepted (verified in a macOS 26.4 VM via scripts/verify-tproxy-setup.sh + scripts/verify-tproxy-test.sh: 3/4 checks pass).
Two deeper limitations remain — meow's macOS transparent proxy is still not usable for real traffic.
1. Original destination not recovered (loopback handshake fails)
With MATCH,REJECT and the rig up, a client connecting to the test IP is diverted away from the echo server (interception works), but:
ncto the rdr'd address reportsOperation timed out— the redirected-to-127.0.0.1connection never completes its TCP handshake.- meow's listener logs nothing for the connection —
get_original_dst(DIOCNATLOOK) /accept()never yields a usable stream, so the original destination is never recovered. verify-tproxy-test.shcheckmeow_logged_original_dstfails.
pf state confirms the rdr fires (states exist for … <- 10.88.0.1:9999 <- … 127.0.0.1:7893), but the connection doesn't establish cleanly at meow. Suspected causes to investigate:
- martian-style src→loopback delivery (source is a non-loopback
lo0alias, dst rewritten to127.0.0.1); - the tproxy listener binding
127.0.0.1vs. needing a routable/0.0.0.0bind; DIOCNATLOOKstruct/ioctl ABI on recent macOS (theDIOCNATLOOKconstant encodes a struct size; macOS 26.x may differ).
2. rdr on lo0 only covers loopback
build_pf_ruleset emits rdr pass on lo0 …. Real outbound traffic (a browser to the internet) leaves via the physical interface (en0), not lo0, so it is never intercepted. As written, macOS tproxy can only ever touch loopback-destined traffic — which isn't useful for a general transparent proxy.
A proper fix likely needs:
rdron the real (and/or all) interface(s), not justlo0;- correct reverse-path / source handling so the handshake completes and
DIOCNATLOOKresolves; - revisiting UID-based loop avoidance — note
pass … user <uid>is a filter rule and does not preventrdr(translation runs first), so it doesn't actually stop meow's own traffic from being redirected.
Repro
# on a macOS host (use a throwaway VM):
./scripts/verify-tproxy-setup.sh up --meow /path/to/meow # macOS arm64 build
./scripts/verify-tproxy-test.sh # 3/4 pass; meow_logged_original_dst fails
./scripts/verify-tproxy-setup.sh down
Scope
Linux tproxy is unaffected (verified working). This is macOS-only and was masked from CI because the macOS pf code path doesn't compile on the Linux runners.
🤖 Generated with Claude Code