Skip to content

Commit ef98275

Browse files
madeyeclaude
andcommitted
Listen directly on interface IP:53 for DNS, add debug logging
- DNS forwarder now binds to the gateway interface IP on port 53 instead of requiring pf to redirect DNS traffic - Add --dns flag for easy enablement with auto-detection of interface IP - Add --interface flag to specify network interface (default: en0) - Add info-level logging for proxy connections and DNS responses - Add debug-level logging for DNS queries, SNI, and tunnel details - Default log level is debug in debug builds, info in release builds - Compile out debug logs in release builds via release_max_level_info - Print log level at startup - Remove DNS redirect from pf setup script - Add DNS forwarder integration test - Bump version to 0.3.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ab977e1 commit ef98275

9 files changed

Lines changed: 281 additions & 99 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "trans_proxy"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
edition = "2021"
55
description = "Transparent proxy for macOS pf redirection with upstream HTTP CONNECT proxy support"
66
license = "MIT"
@@ -11,7 +11,7 @@ authors = ["Max Lv <max.c.lv@gmail.com>"]
1111
tokio = { version = "1", features = ["full"] }
1212
socket2 = "0.5"
1313
clap = { version = "4", features = ["derive"] }
14-
tracing = "0.1"
14+
tracing = { version = "0.1", features = ["max_level_debug", "release_max_level_info"] }
1515
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
1616
anyhow = "1"
1717
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }

README.md

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Designed to run on a Mac acting as a side router (gateway) for other devices on
1818

1919
- **pf integration** — Uses `DIOCNATLOOK` ioctl on `/dev/pf` to recover original destinations from pf's NAT state table
2020
- **SNI extraction** — Peeks at TLS ClientHello to extract hostnames, sending proper `CONNECT host:port` instead of raw IPs
21-
- **DNS interception**Optional local DNS forwarder that builds an IP→domain lookup table as a fallback for hostname resolution (supports both traditional UDP and DNS-over-HTTPS)
21+
- **DNS forwarder**Listens directly on the gateway interface (port 53) for LAN client DNS queries, building an IP→domain lookup table. Supports DNS-over-HTTPS (DoH) and traditional UDP upstream.
2222
- **Anchor-based pf rules** — Won't clobber your existing firewall config
2323
- **Daemon mode** — Run as a background process with PID file and log file support
2424
- **Async I/O** — Built on tokio with per-connection task spawning
@@ -27,7 +27,7 @@ Designed to run on a Mac acting as a side router (gateway) for other devices on
2727

2828
- macOS 12+ (uses pf and `DIOCNATLOOK` ioctl)
2929
- Rust 1.70+ and Cargo
30-
- Root privileges (for `/dev/pf` access and pf rule management)
30+
- Root privileges (for `/dev/pf` access and port 53 binding)
3131
- An upstream HTTP CONNECT proxy (e.g., Squid, mitmproxy, or any CONNECT-capable proxy)
3232

3333
## Build
@@ -57,19 +57,18 @@ cargo test
5757
This example assumes your upstream HTTP proxy runs on `127.0.0.1:1082` and your LAN interface is `en0`.
5858

5959
```bash
60-
# Step 1: Start the transparent proxy with DNS interception (DoH by default)
60+
# Step 1: Start the transparent proxy with DNS on the gateway interface
6161
sudo ./target/release/trans_proxy \
6262
--upstream-proxy 127.0.0.1:1082 \
63-
--dns-listen 0.0.0.0:5353
63+
--dns
6464

6565
# Or run as a daemon
6666
sudo ./target/release/trans_proxy \
6767
--upstream-proxy 127.0.0.1:1082 \
68-
--dns-listen 0.0.0.0:5353 \
69-
-d
68+
--dns -d
7069

7170
# Step 2: Set up pf redirection (in another terminal, or same if using -d)
72-
sudo scripts/pf_setup.sh en0 8443 5353
71+
sudo scripts/pf_setup.sh en0 8443
7372

7473
# Step 3: Configure client devices (see "Client Setup" below)
7574

@@ -86,45 +85,44 @@ sudo kill $(cat /var/run/trans_proxy.pid)
8685
The proxy requires root to open `/dev/pf` for NAT lookups:
8786

8887
```bash
89-
# Minimal — proxy only, no DNS interception
88+
# Minimal — proxy only, no DNS
9089
sudo ./target/release/trans_proxy \
9190
--upstream-proxy <proxy_host>:<proxy_port>
9291

93-
# Full — with DNS interception (uses Cloudflare DoH by default)
92+
# With DNS on the gateway interface (auto-detects en0 IP, listens on port 53)
9493
sudo ./target/release/trans_proxy \
9594
--upstream-proxy <proxy_host>:<proxy_port> \
96-
--dns-listen 0.0.0.0:5353
95+
--dns
9796

98-
# Use a specific DoH provider
97+
# Specify a different interface
9998
sudo ./target/release/trans_proxy \
10099
--upstream-proxy <proxy_host>:<proxy_port> \
101-
--dns-listen 0.0.0.0:5353 \
102-
--dns-upstream https://dns.google/dns-query
100+
--dns --interface en1
103101

104-
# Use traditional UDP DNS instead of DoH
102+
# Override DNS listen address manually
105103
sudo ./target/release/trans_proxy \
106104
--upstream-proxy <proxy_host>:<proxy_port> \
107-
--dns-listen 0.0.0.0:5353 \
108-
--dns-upstream 8.8.8.8:53
105+
--dns-listen 192.168.1.42:53
109106

110-
# Custom listen address and debug logging
107+
# Use a specific DoH provider
111108
sudo ./target/release/trans_proxy \
112-
--listen-addr 0.0.0.0:9999 \
113-
--upstream-proxy 127.0.0.1:1082 \
114-
--dns-listen 0.0.0.0:5353 \
115-
--log-level debug
109+
--upstream-proxy <proxy_host>:<proxy_port> \
110+
--dns --dns-upstream https://dns.google/dns-query
111+
112+
# Use traditional UDP DNS instead of DoH
113+
sudo ./target/release/trans_proxy \
114+
--upstream-proxy <proxy_host>:<proxy_port> \
115+
--dns --dns-upstream 8.8.8.8:53
116116

117117
# Run as a background daemon
118118
sudo ./target/release/trans_proxy \
119119
--upstream-proxy 127.0.0.1:1082 \
120-
--dns-listen 0.0.0.0:5353 \
121-
-d
120+
--dns -d
122121

123122
# Daemon with custom PID and log file
124123
sudo ./target/release/trans_proxy \
125124
--upstream-proxy 127.0.0.1:1082 \
126-
--dns-listen 0.0.0.0:5353 \
127-
-d --pid-file /tmp/trans_proxy.pid \
125+
--dns -d --pid-file /tmp/trans_proxy.pid \
128126
--log-file /tmp/trans_proxy.log
129127
```
130128

@@ -135,24 +133,23 @@ sudo ./target/release/trans_proxy \
135133
| `--listen-addr` | `0.0.0.0:8443` | Address and port the proxy listens on |
136134
| `--upstream-proxy` | *(required)* | Upstream HTTP CONNECT proxy address (`host:port`) |
137135
| `--log-level` | `info` | Log verbosity: `trace`, `debug`, `info`, `warn`, `error` |
138-
| `--dns-listen` | *(disabled)* | Enable DNS forwarder on this address (e.g., `0.0.0.0:5353`) |
136+
| `--dns` | off | Enable DNS forwarder on the gateway interface (port 53) |
137+
| `--interface` | `en0` | Network interface for DNS auto-detection (used with `--dns`) |
138+
| `--dns-listen` | *(auto)* | Override DNS listen address (e.g., `192.168.1.42:53`) |
139139
| `--dns-upstream` | `https://cloudflare-dns.com/dns-query` | Upstream DNS: `host:port` for UDP, or `https://` URL for DoH |
140140
| `-d` / `--daemon` | off | Run as a background daemon |
141141
| `--pid-file` | `/var/run/trans_proxy.pid` | PID file path (used with `--daemon`) |
142142
| `--log-file` | `/var/log/trans_proxy.log` (daemon) / stderr | Log file path |
143143

144144
### Setting up pf redirection
145145

146-
The included scripts manage pf rules via an anchor (won't interfere with existing firewall rules):
146+
The included scripts manage pf rules via an anchor (won't interfere with existing firewall rules).
147+
DNS no longer needs pf redirection — trans_proxy listens directly on port 53.
147148

148149
```bash
149-
# Redirect HTTP/HTTPS only
150+
# Set up HTTP/HTTPS redirection
150151
sudo scripts/pf_setup.sh <interface> [proxy_port]
151152
sudo scripts/pf_setup.sh en0 8443
152-
153-
# Redirect HTTP/HTTPS + DNS
154-
sudo scripts/pf_setup.sh <interface> [proxy_port] [dns_port]
155-
sudo scripts/pf_setup.sh en0 8443 5353
156153
```
157154

158155
The setup script prints the gateway IP and configuration summary:
@@ -166,7 +163,7 @@ The setup script prints the gateway IP and configuration summary:
166163
Done.
167164
Gateway IP: 192.168.1.42 (en0)
168165
HTTP/HTTPS: ports 80,443 -> 127.0.0.1:8443
169-
DNS: port 53 -> 127.0.0.1:5353
166+
DNS: use --dns flag to listen on 192.168.1.42:53 directly
170167
171168
Configure client devices to use 192.168.1.42 as their gateway.
172169
Set DNS server to 192.168.1.42 on client devices.
@@ -189,8 +186,7 @@ Run trans_proxy as a background process:
189186
# Start as daemon
190187
sudo ./target/release/trans_proxy \
191188
--upstream-proxy 127.0.0.1:1082 \
192-
--dns-listen 0.0.0.0:5353 \
193-
-d
189+
--dns -d
194190

195191
# Check status
196192
cat /var/run/trans_proxy.pid
@@ -211,7 +207,7 @@ In daemon mode:
211207
On each device you want to route through the proxy:
212208

213209
1. **Set the default gateway** to the Mac's IP address (shown by the setup script)
214-
2. **Set the DNS server** to the Mac's IP address (if using `--dns-listen`)
210+
2. **Set the DNS server** to the Mac's IP address (if using `--dns`)
215211

216212
#### macOS / iOS
217213
Settings → Wi-Fi → (i) → Configure IP → Manual → Router: `<gateway_ip>`, DNS: `<gateway_ip>`
@@ -246,7 +242,7 @@ Settings → Wi-Fi → Long press network → Modify → Advanced → IP setting
246242
The proxy resolves hostnames for CONNECT requests using a fallback chain:
247243

248244
1. **SNI extraction** — Parses the TLS ClientHello to read the Server Name Indication extension (port 443 only). No TLS termination or certificate generation required.
249-
2. **DNS table lookup** — If `--dns-listen` is enabled, the built-in DNS forwarder records IP→domain mappings from A record responses. Works for both HTTP (port 80) and HTTPS (port 443).
245+
2. **DNS table lookup** — If `--dns` is enabled, the built-in DNS forwarder records IP→domain mappings from A record responses. Works for both HTTP (port 80) and HTTPS (port 443).
250246
3. **Raw IP** — Falls back to the IP address if no hostname can be determined.
251247

252248
### Why DIOCNATLOOK?
@@ -272,9 +268,9 @@ This is a harmless warning from `pfctl`. macOS doesn't include ALTQ — pf redir
272268
- Ensure IP forwarding is enabled: `sysctl net.inet.ip.forwarding` (should be `1`)
273269

274270
### DNS not resolving on client devices
275-
- Ensure `--dns-listen` is set and the DNS forwarder is running
276-
- Ensure pf is redirecting port 53: `sudo pfctl -a trans_proxy -s rules`
277-
- Test: `dig @<gateway_ip> -p 5353 example.com`
271+
- Ensure `--dns` is set and the DNS forwarder is running
272+
- Check that trans_proxy logs show `DNS forwarder listening on <ip>:53`
273+
- Test: `dig @<gateway_ip> example.com`
278274

279275
## License
280276

docs/index.html

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ <h1><code>trans_proxy</code></h1>
506506
<g>
507507
<rect x="30" y="180" width="170" height="46" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1.5"/>
508508
<text x="115" y="203" text-anchor="middle" fill="#8b949e" font-family="'SF Mono', monospace" font-size="12">DNS Forwarder</text>
509-
<text x="115" y="218" text-anchor="middle" fill="#8b949e" font-family="'SF Mono', monospace" font-size="10">:5353</text>
509+
<text x="115" y="218" text-anchor="middle" fill="#8b949e" font-family="'SF Mono', monospace" font-size="10">:53</text>
510510
</g>
511511

512512
<!-- ============ STATIC LINES & LABELS ============ -->
@@ -831,8 +831,8 @@ <h3>SNI Extraction</h3>
831831
<p>Peeks at the TLS ClientHello to extract hostnames, sending proper <code>CONNECT host:port</code> instead of raw IPs. No TLS termination or certificate generation needed.</p>
832832
</div>
833833
<div class="feature">
834-
<h3>DNS Interception</h3>
835-
<p>Optional local DNS forwarder that captures A record responses and builds an IP&rarr;domain lookup table. Supports DNS-over-HTTPS (DoH) and traditional UDP upstream.</p>
834+
<h3>DNS Forwarder</h3>
835+
<p>Listens directly on the gateway interface (port 53) for LAN client DNS queries. Builds an IP&rarr;domain lookup table. Supports DoH and UDP upstream.</p>
836836
</div>
837837
<div class="feature">
838838
<h3>Safe pf Anchors</h3>
@@ -891,26 +891,22 @@ <h2>Quick Start</h2>
891891
<div class="steps">
892892
<div class="step">
893893
<h3>Start the transparent proxy</h3>
894-
<p>Run with DNS interception enabled (uses Cloudflare DoH by default):</p>
895-
<pre class="code"><span class="comment"># Foreground</span>
894+
<p>Run with DNS on the gateway interface (uses Cloudflare DoH by default):</p>
895+
<pre class="code"><span class="comment"># Foreground (auto-detects en0 IP, listens on port 53)</span>
896896
<span class="cmd">sudo ./target/release/trans_proxy</span> \
897897
<span class="flag">--upstream-proxy</span> <span class="value">127.0.0.1:1082</span> \
898-
<span class="flag">--dns-listen</span> <span class="value">0.0.0.0:5353</span>
898+
<span class="flag">--dns</span>
899899

900900
<span class="comment"># Or as a daemon</span>
901901
<span class="cmd">sudo ./target/release/trans_proxy</span> \
902902
<span class="flag">--upstream-proxy</span> <span class="value">127.0.0.1:1082</span> \
903-
<span class="flag">--dns-listen</span> <span class="value">0.0.0.0:5353</span> <span class="flag">-d</span></pre>
903+
<span class="flag">--dns</span> <span class="flag">-d</span></pre>
904904
</div>
905905

906906
<div class="step">
907907
<h3>Set up pf redirection</h3>
908-
<p>In another terminal, install the pf rules:</p>
909-
<pre class="code"><span class="comment"># With DNS redirect (recommended)</span>
910-
<span class="cmd">sudo scripts/pf_setup.sh</span> <span class="value">en0 8443 5353</span>
911-
912-
<span class="comment"># Without DNS redirect</span>
913-
<span class="cmd">sudo scripts/pf_setup.sh</span> <span class="value">en0 8443</span></pre>
908+
<p>In another terminal, install the pf rules (HTTP/HTTPS only &mdash; DNS is handled directly):</p>
909+
<pre class="code"><span class="cmd">sudo scripts/pf_setup.sh</span> <span class="value">en0 8443</span></pre>
914910
<p>The script prints your gateway IP and setup summary.</p>
915911
</div>
916912

@@ -957,10 +953,20 @@ <h2>CLI Reference</h2>
957953
<td>info</td>
958954
<td>Log verbosity: trace, debug, info, warn, error</td>
959955
</tr>
956+
<tr>
957+
<td><code>--dns</code></td>
958+
<td>off</td>
959+
<td>Enable DNS forwarder on the gateway interface (port 53)</td>
960+
</tr>
961+
<tr>
962+
<td><code>--interface</code></td>
963+
<td>en0</td>
964+
<td>Network interface for DNS auto-detection</td>
965+
</tr>
960966
<tr>
961967
<td><code>--dns-listen</code></td>
962-
<td><em>disabled</em></td>
963-
<td>Enable DNS forwarder on this address</td>
968+
<td><em>auto</em></td>
969+
<td>Override DNS listen address (e.g., <code>192.168.1.42:53</code>)</td>
964970
</tr>
965971
<tr>
966972
<td><code>--dns-upstream</code></td>
@@ -986,8 +992,9 @@ <h2>CLI Reference</h2>
986992
</table>
987993

988994
<h3 style="margin-top: 32px;">pf Scripts</h3>
989-
<pre class="code"><span class="comment"># Setup: pf_setup.sh &lt;interface&gt; [proxy_port] [dns_port]</span>
990-
<span class="cmd">sudo scripts/pf_setup.sh</span> <span class="value">en0 8443 5353</span>
995+
<pre class="code"><span class="comment"># Setup: pf_setup.sh &lt;interface&gt; [proxy_port]</span>
996+
<span class="comment"># DNS no longer needs pf redirection &mdash; listens on port 53 directly</span>
997+
<span class="cmd">sudo scripts/pf_setup.sh</span> <span class="value">en0 8443</span>
991998

992999
<span class="comment"># Teardown: flushes anchor rules, disables IP forwarding</span>
9931000
<span class="cmd">sudo scripts/pf_teardown.sh</span></pre>
@@ -1019,7 +1026,7 @@ <h3>1. SNI</h3>
10191026
</div>
10201027
<div class="feature">
10211028
<h3>2. DNS Table</h3>
1022-
<p>IP&rarr;domain mapping from intercepted DNS responses (via DoH or UDP). Works for HTTP and HTTPS. Requires <code>--dns-listen</code>.</p>
1029+
<p>IP&rarr;domain mapping from DNS responses (via DoH or UDP). Works for HTTP and HTTPS. Requires <code>--dns</code>.</p>
10231030
</div>
10241031
<div class="feature">
10251032
<h3>3. Raw IP</h3>
@@ -1098,9 +1105,9 @@ <h2>Troubleshooting</h2>
10981105
<details class="faq-item">
10991106
<summary>DNS not resolving on client devices</summary>
11001107
<div class="faq-body">
1101-
Ensure <code>--dns-listen</code> is set and the DNS forwarder is running.<br>
1102-
Ensure pf is redirecting port 53: <code>sudo pfctl -a trans_proxy -s rules</code><br>
1103-
Test directly: <code>dig @&lt;gateway_ip&gt; -p 5353 example.com</code>
1108+
Ensure <code>--dns</code> is set and the DNS forwarder is running.<br>
1109+
Check that trans_proxy logs show <code>DNS forwarder listening on &lt;ip&gt;:53</code>.<br>
1110+
Test directly: <code>dig @&lt;gateway_ip&gt; example.com</code>
11041111
</div>
11051112
</details>
11061113
</div>

scripts/pf_setup.sh

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
#!/bin/bash
22
set -euo pipefail
33

4-
# Usage: pf_setup.sh <interface> [proxy_port] [dns_port]
5-
# Example: pf_setup.sh en0 8443 5353
4+
# Usage: pf_setup.sh <interface> [proxy_port]
5+
# Example: pf_setup.sh en0 8443
66

7-
IFACE="${1:?Usage: $0 <interface> [proxy_port] [dns_port]}"
7+
IFACE="${1:?Usage: $0 <interface> [proxy_port]}"
88
PROXY_PORT="${2:-8443}"
9-
DNS_PORT="${3:-}"
109
ANCHOR="trans_proxy"
1110

1211
echo "==> Enabling IP forwarding"
@@ -29,13 +28,11 @@ EOF
2928
sudo pfctl -a "${ANCHOR}" -f /dev/stdin <<EOF
3029
# Redirect HTTP and HTTPS traffic arriving on ${IFACE} to transparent proxy
3130
rdr on ${IFACE} proto tcp from any to any port {80, 443} -> 127.0.0.1 port ${PROXY_PORT}
32-
$([ -n "${DNS_PORT}" ] && echo "rdr on ${IFACE} proto {tcp, udp} from any to any port 53 -> 127.0.0.1 port ${DNS_PORT}")
3331
EOF
3432
else
3533
echo " Anchor already exists, updating rules"
3634
sudo pfctl -a "${ANCHOR}" -f /dev/stdin <<EOF
3735
rdr on ${IFACE} proto tcp from any to any port {80, 443} -> 127.0.0.1 port ${PROXY_PORT}
38-
$([ -n "${DNS_PORT}" ] && echo "rdr on ${IFACE} proto {tcp, udp} from any to any port 53 -> 127.0.0.1 port ${DNS_PORT}")
3936
EOF
4037
fi
4138

@@ -51,8 +48,8 @@ echo ""
5148
echo "Done."
5249
echo " Gateway IP: ${GATEWAY_IP:-<unknown>} (${IFACE})"
5350
echo " HTTP/HTTPS: ports 80,443 -> 127.0.0.1:${PROXY_PORT}"
54-
[ -n "${DNS_PORT}" ] && echo " DNS: port 53 -> 127.0.0.1:${DNS_PORT}"
51+
echo " DNS: use --dns flag to listen on ${GATEWAY_IP:-<interface-ip>}:53 directly"
5552
echo ""
5653
echo "Configure client devices to use ${GATEWAY_IP:-this machine} as their gateway."
57-
[ -n "${DNS_PORT}" ] && echo "Set DNS server to ${GATEWAY_IP:-this machine} on client devices."
54+
echo "Set DNS server to ${GATEWAY_IP:-this machine} on client devices."
5855
echo "Run scripts/pf_teardown.sh to undo."

0 commit comments

Comments
 (0)