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.

Leave a Comment