Selective VPN Routing Using Pi-hole, iptables, and Sing-box

In my home lab setup, I use Pi-hole as a network-level DNS-based filter. It blocks ads and trackers at the DNS level for all devices without requiring client-side software. This centralised approach simplifies control over DNS traffic.
Problem: Domain Access via VPN Only
I need to access a domain that restricts access based on IP, requiring VPN connectivity. Installing a VPN client on every device is inconvenient, especially when most VPN setups route all traffic, or require complex per-device routing configurations.
Goal
Redirect only specific domain traffic through a VPN transparently, without client setup on each device.
Solution Overview
We’ll use:
- Pi-hole for DNS-level interception
- iptables for traffic redirection
- Sing-box as a local VPN proxy using TPROXY
Step 1: Override DNS Resolution
My Pi-hole is hosted at 192.168.8.10, and configured as the primary DNS server via the router.
Inside Pi-hole, I created a Local DNS Record to override the target domain (e.g. httpbin.org) to resolve to the Pi-hole’s IP:
httpbin.org → 192.168.8.10
Step 2: Intercept HTTP/HTTPS with iptables
We now intercept incoming traffic to the overridden IP (Pi-hole), and transparently proxy it to a local port (8888) using TPROXY.
#!/bin/bash
# 1. Enable IP forwarding and set rp_filter to loose
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.conf.eth0.rp_filter=0
sysctl -w net.ipv4.conf.all.rp_filter=0
# 2. Create/flush custom chain
iptables -t mangle -N TPROXY_IN 2>/dev/null
iptables -t mangle -F TPROXY_IN
# 3. Redirect TCP 80/443 to TPROXY on port 8888
iptables -t mangle -A TPROXY_IN -p tcp -m multiport --dports 80,443 \
-j TPROXY --on-port 8888 --tproxy-mark 0x1/0x1
# 4. Apply to traffic destined for Pi-hole IP
iptables -t mangle -D PREROUTING -d 192.168.8.10 -j TPROXY_IN 2>/dev/null
iptables -t mangle -A PREROUTING -d 192.168.8.10 -j TPROXY_IN
# 5. Route marked packets to local interface
ip rule add fwmark 0x1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
What This Does:
- Enables packet forwarding.
- Creates a custom mangle chain.
- Captures incoming traffic for ports 80/443.
- Redirects that traffic to a local port (8888).
- Applies only to traffic sent to the Pi-hole IP.
- Sets up routing so these connections are handled locally.
Step 3: Setup Sing-box as Transparent Proxy
We’ll now run Sing-box in Docker to handle the intercepted traffic and forward it over VPN using the VLESS protocol.
# docker-compose.yml
services:
sing-box:
image: ghcr.io/sagernet/sing-box:latest
container_name: sing-box
restart: always
volumes:
- ./etc/sing-box:/etc/sing-box/
network_mode: "host"
cap_add:
- NET_ADMIN
- NET_BIND_SERVICE
command: -D /var/lib/sing-box -C /etc/sing-box/ run
This runs Sing-box in host mode (no rootless setup for now, for simplicity).
# /etc/sing-box/config.json
{
"log": { "level": "info" },
"inbounds": [
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "0.0.0.0",
"listen_port": 8888,
"network": "tcp",
"sniff": true,
"sniff_override_destination": true
}
],
"outbounds": [
{
"type": "vless",
"tag": "proxy",
"server": "remote.vpn.server.ip.here",
"server_port": 1234,
"uuid": "uuid",
"tls": { "enabled": false }
},
{ "type": "direct", "tag": "direct" },
{ "type": "dns", "tag": "dns" }
],
"route": {
"auto_detect_interface": true,
"rules": [
{
"domain": ["httpbin.org", "domain.com"],
"outbound": "proxy"
},
{
"inbound": ["tproxy-in", "socks-in"],
"outbound": "direct"
}
]
},
"dns": {
"servers": [{ "address": "8.8.8.8" }],
"rules": [
{ "domain": "httpbin.org", "server": "8.8.8.8" },
{ "domain": "domain.com", "server": "8.8.8.8" }
]
}
}
Sing-box supports multiple protocols beyond VLESS. For a full list, refer to the official supported protocols page.
Once configured, bring up the container:
docker compose up -d
Done! Now all devices on the LAN that try to access httpbin.org will transparently get routed through the VPN tunnel.
$ curl https://httpbin.org/ip
{
"origin": "remote.vpn.server.ip.here"
}
Debugging Tips
Check DNS resolution:
dig httpbin.org
dig @192.168.8.10 httpbin.org
Bypass DNS:
curl -v https://httpbin.org/ip --resolve httpbin.org:443:192.168.8.10
Capture traffic on Pi-hole:
sudo tcpdump -i any port 443 and dst 192.168.8.10
Check Sing-box logs:
docker compose logs -f sing-box
Make sure DNS caching isn’t interfering. Restart browsers or devices as needed.