madeye/meow-rs

macOS transparent proxy is loopback-only and can't recover original destination

Open

#248 opened on Jun 26, 2026

View on GitHub
 (5 comments) (0 reactions) (0 assignees)Rust (27 forks)github user discovery
bughelp wanted

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:

  • nc to the rdr'd address reports Operation timed out — the redirected-to-127.0.0.1 connection 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.sh check meow_logged_original_dst fails.

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 lo0 alias, dst rewritten to 127.0.0.1);
  • the tproxy listener binding 127.0.0.1 vs. needing a routable/0.0.0.0 bind;
  • DIOCNATLOOK struct/ioctl ABI on recent macOS (the DIOCNATLOOK constant 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:

  • rdr on the real (and/or all) interface(s), not just lo0;
  • correct reverse-path / source handling so the handshake completes and DIOCNATLOOK resolves;
  • revisiting UID-based loop avoidance — note pass … user <uid> is a filter rule and does not prevent rdr (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

Contributor guide