nnfewl's Blog

Raspberry Pi as transparent proxy

November 06, 2022

记录使用树莓派搭建透明代理的过程

Setup Clash service

配置 docker compose 驱动的 Clash 实例

$HOME 目录下创建项目:

mkdir -p $HOME/Services/pi-gateway
mkdir -p $HOME/Services/pi-gateway/clash
cd $HOME/Services/pi-gateway

如下为 pi-gateway 项目结构:

.
├── clash
│   ├── config.yaml # clash configuration
│   └── dashboard   # clash control plane (https://github.com/haishanh/yacd)
├── docker-compose.yaml
├── naive-1         # naiveproxy instance 1 related folder
│   └── config.json # naiveproxy instance 1 configuration
└── naive-2         # naiveproxy instance 2 related folder
    └── config.json # naiveproxy instance 2 configuration

创建 docker compose 配置文件:

touch docker-compose.yaml

使用如下配置样例:

version: "3.9"
services:

  clash:
    container_name: "pi-gateway_clash"
  # image: dreamacro/clash-premium:2021.07.03
  # image: dreamacro/clash-premium:2022.07.07
    image: dreamacro/clash-premium:latest
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun
    restart: unless-stopped
    volumes:
      - ./clash/dashboard:/root/.config/clash/dashboard
      - ./clash/config.yaml:/root/.config/clash/config.yaml:ro
    # When your system is Linux, you can use `network_mode: "host"` directly.
    network_mode: host

  naive1:
    container_name: "pi-gateway_naive_1"
    image: pocat/naiveproxy:client
    volumes:
      - ./naive-1/config.json:/etc/naiveproxy/config.json:ro
    restart: unless-stopped
    network_mode: host

  naive2:
    container_name: "pi-gateway_naive_2"
    image: pocat/naiveproxy:client
    volumes:
      - ./naive-2/config.json:/etc/naiveproxy/config.json:ro
    restart: unless-stopped
    network_mode: host
    
# we could add move container here follow the format listed above
# ...

创建 clash 配置文件:

touch clash/config.yaml

使用如下配置样例:

# Port of HTTP(S) proxy server on the local end
# port: 7890

# Port of SOCKS5 proxy server on the local end
# socks-port: 7891

# Transparent proxy server port for Linux and macOS (Redirect TCP and TProxy UDP)
redir-port: 7892

# Transparent proxy server port for Linux (TProxy TCP and TProxy UDP)
# tproxy-port: 7893

# HTTP(S) and SOCKS4(A)/SOCKS5 server on the same port
mixed-port: 7890

# authentication of local SOCKS5/HTTP(S) server
# authentication:
#  - "user1:pass1"
#  - "user2:pass2"

# Set to true to allow connections to the local-end server from
# other LAN IP addresses
allow-lan: true

# This is only applicable when `allow-lan` is `true`
# '*': bind all IP addresses
# 192.168.122.11: bind a single IPv4 address
# "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address
# bind-address: '*'

# Clash router working mode
# rule: rule-based packet routing
# global: all packets will be forwarded to a single endpoint
# direct: directly forward the packets to the Internet
mode: rule

# Clash by default prints logs to STDOUT
# info / warning / error / debug / silent
log-level: silent

# When set to false, resolver won't translate hostnames to IPv6 addresses
# ipv6: false

# RESTful web API listening address
external-controller: 0.0.0.0:9090

# A relative path to the configuration directory or an absolute path to a
# directory in which you put some static web resource. Clash core will then
# serve it at `http://{{external-controller}}/ui`.
external-ui: dashboard

# Secret for the RESTful API (optional)
# Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
# ALWAYS set a secret if RESTful API is listening on 0.0.0.0
secret: ""

# DNS server settings
# This section is optional. When not present, the DNS server will be disabled.
dns:
  enable: true
  listen: :1053

  # These nameservers are used to resolve the DNS nameserver hostnames below.
  # Specify IP addresses only
  default-nameserver:
    - 223.5.5.5
    - 223.6.6.6
    - 114.114.114.114
    - 1.1.1.1
    - 8.8.8.8
  enhanced-mode: redir-host

  # Supports UDP, TCP, DoT, DoH. You can specify the port to connect to.
  # All DNS questions are sent directly to the nameserver, without proxies
  # involved. Clash answers the DNS question with the first result gathered.
  nameserver:
    - 114.114.114.114 # default value
    - 8.8.8.8 # default value
    - tls://dns.rubyfish.cn:853 # DNS over TLS
    - https://1.1.1.1/dns-query # DNS over HTTPS
    # - dhcp://en0 # dns from dhcp

  fallback:
    - 208.67.220.220:5353
    - 208.67.222.222:5353
    - 101.6.6.6:5353

proxies:
  # local NaïveProxy 1
  - name: "🇺🇸 NaïveProxy-1"
    type: socks5
    server: localhost
    port: 1081
    udp: true

  # local NaïveProxy 2
  - name: "🇺🇸 NaïveProxy-2"
    type: socks5
    server: localhost
    port: 1082
    udp: true

proxy-groups:
  # select is used for selecting proxy or proxy group
  # you can use RESTful API to switch proxy, is recommended for use in GUI.
  - name: "🕹️ Controller"
    type: select
    proxies:
      - "🛸 Autopilot"
      - "🚀 Load-Balance_CH"
      - "🚚 Load-Balance_RR"

  # url-test select which proxy will be used by benchmarking speed to a URL.
  - name: "🛸 Autopilot"
    type: url-test
    proxies:
      - "🇺🇸 NaïveProxy-1"
      - "🇺🇸 NaïveProxy-2"

    # tolerance: 150
    url: "http://cp.cloudflare.com/generate_204"
    interval: 30

  # load-balance: The request of the same eTLD will be dial on the same proxy.
  - name: "🚀 Load-Balance_CH"
    type: load-balance
    proxies:
      - "🇺🇸 NaïveProxy-1"
      - "🇺🇸 NaïveProxy-2"

    url: "http://www.google.com/generate_204"
    interval: 30
    strategy: consistent-hashing

  - name: "🚚 Load-Balance_RR"
    type: load-balance
    proxies:
      - "🇺🇸 NaïveProxy-1"
      - "🇺🇸 NaïveProxy-2"

    url: "http://www.google.com/generate_204"
    interval: 30
    strategy: round-robin

rules:
  - GEOIP,CN,DIRECT
  - IP-CIDR,10.0.0.0/8,DIRECT
  - IP-CIDR,17.0.0.0/8,DIRECT
  - IP-CIDR,100.64.0.0/10,DIRECT
  - IP-CIDR,127.0.0.0/8,DIRECT
  - IP-CIDR,172.16.0.0/12,DIRECT
  - IP-CIDR,192.168.0.0/16,DIRECT
  - MATCH,🕹️ Controller

这里可以通过 🕹️ Controller 选择器控制三种代理组,分别是自动选择和两种不同的负载均衡策略。

三种代理组分别拥有两个代理,两个代理对应容器编排里的两个 NaïveProxy 实例,分别运行在 1081、1082 端口。

关于 NaiveProxy 实例

创建对于配置文件:

touch naive-1/config.json
touch naive-2/config.json

关于 NaïveProxy 的代理配置详见:https://github.com/klzgrad/naiveproxy

  1. 代理监听在 1081 端口 (naive-1/config.json)
{
  "listen": "socks://0.0.0.0:1081",
  "proxy": "https://<YOUR-NP-USERNAME>:<YOUR-NP-PASSWORD>@<YOUR-NP-DOMAIN>",
  "log": ""
}
  1. 代理监听在 1082 端口 (naive-2/config.json)
{
  "listen": "socks://0.0.0.0:1082",
  "proxy": "https://<YOUR-NP-USERNAME>:<YOUR-NP-PASSWORD>@<YOUR-NP-DOMAIN>",
  "log": ""
}

关于 Clash 实例

  • Clash 会监听在 7890 端口,能同时提供 HTTP/SOCKS 代理功能
  • Clash 会监听在 7892 端口,提供透明代理功能,用于接受并代理来自 iptables 转发过来的流量

启动容器编排

先使用交互式查看运行日志、确认是否能正常运行:

docker compose up

使用另一终端测试代理是否可用:

curl https://ifconfig.co -x http://localhost:7890
curl https://ifconfig.co -x socks5://localhost:7890

结果应该返回代理 IP 地址

回到原来阻塞的终端,使用 CTRL + c 结束命令,确认容器可以正常工作后:

docker compose up -d

Install PiVPN

安装 PiVPN 服务: https://www.pivpn.io/

curl -L https://install.pivpn.io | bash

安装过程中选择 OpenVPN、WireGuard 均可,需要留意 VPN 服务监听端口,后续需要在路由器上配置对应的端口转发。

  • 如果安装 OpenVPN,启用的虚拟网卡为 tun0

  • 如果安装 WireGuard,启用的虚拟网卡为 wg0

后续需要让 Pi-Hole 启动也绑定在对应的虚拟网卡上,让通过 VPN 连接的设备也能使用内网中的 Pi-Hole DNS。

Install Pi-Hole

安装 Pi-Hole 服务:https://docs.pi-hole.net/main/basic-install/

主要用于解决 DNS 污染,统计、监控网络内的 DNS 请求

为 WireGuard 客户端提供 DNS

让 Pi-Hole 监听 wg0 网卡服务通过 WireGuard 接入的设备:

sudo echo "interface=wg0" > /etc/dnsmasq.d/100-interfaces.conf

重启 Pi-Hole 服务:

sudo systemctl restart pihole-FTL.service

其他

让 Pi-Hole 监听树莓派以太网接口 eth0 网卡:

sudo echo "interface=eth0" > /etc/dnsmasq.d/90-interfaces.conf

让 Pi-Hole 监听树莓派无线网接口 wlan0 网卡:

sudo echo "interface=wlan0" > /etc/dnsmasq.d/99-interfaces.conf

重启 Pi-Hole 服务生效

Enable transparent proxy

使用 iptables 转发流量实现透明代理

启用透明代理脚本: tproxy_enable.sh

#!/bin/sh

proxy_port=7892

# -*- TCP --------------------------------------------------------------------

# create user-defined chain `clash` in `nat` table
iptables -t nat -N clash

# add rules into user-defined chain `clash` from `nat` table
# ignore Reserved IP addresses
iptables -t nat -A clash -d 0.0.0.0/8 -j RETURN
iptables -t nat -A clash -d 10.0.0.0/8 -j RETURN
iptables -t nat -A clash -d 127.0.0.0/8 -j RETURN
iptables -t nat -A clash -d 169.254.0.0/16 -j RETURN
iptables -t nat -A clash -d 172.16.0.0/12 -j RETURN
iptables -t nat -A clash -d 192.168.0.0/16 -j RETURN
iptables -t nat -A clash -d 224.0.0.0/4 -j RETURN
iptables -t nat -A clash -d 240.0.0.0/4 -j RETURN

# redirect TCP traffic to port 7892
iptables -t nat -A clash -p tcp -j REDIRECT --to-port "$proxy_port"

# add three rules into `PREROUTING` chain from nat table
iptables -t nat -I PREROUTING -p tcp -j clash
iptables -t nat -I PREROUTING -p tcp -d 8.8.8.8 -j REDIRECT --to-port "$proxy_port"
iptables -t nat -I PREROUTING -p tcp -d 8.8.4.4 -j REDIRECT --to-port "$proxy_port"

# -*- UDP --------------------------------------------------------------------

ip rule add fwmark 1 table 100
ip route add local default dev lo table 100

# create user-defined chain `clash` in `mangle` table
iptables -t mangle -N clash

# add rules into user-defined chain `clash` from `mangle` table
# ignore Reserved IP addresses
iptables -t mangle -A clash -d 0.0.0.0/8 -j RETURN
iptables -t mangle -A clash -d 10.0.0.0/8 -j RETURN
iptables -t mangle -A clash -d 127.0.0.0/8 -j RETURN
iptables -t mangle -A clash -d 169.254.0.0/16 -j RETURN
iptables -t mangle -A clash -d 172.16.0.0/12 -j RETURN
iptables -t mangle -A clash -d 192.168.0.0/16 -j RETURN
iptables -t mangle -A clash -d 224.0.0.0/4 -j RETURN
iptables -t mangle -A clash -d 240.0.0.0/4 -j RETURN

# add rules info user-defined chain `clash` from `mangle` table match protocal udp
iptables -t mangle -A clash -p udp -j TPROXY --on-port "$proxy_port" --tproxy-mark 1

# add rules into `PREROUTING` chain from `mangle` table
iptables -t mangle -A PREROUTING -p udp -j clash

禁用透明代理脚本: tproxy_disable.sh

#!/bin/sh

# clash (Tproxy TCP+UDP)
ip rule del fwmark 1 table 100
ip route del local default dev lo table 100

# delete rules into `PREROUTING` chain from `mangle` table
iptables -t mangle -D PREROUTING -p udp -j clash

# flush the rules under user-defined chain `clash`
iptables -t mangle -F clash

# delete user-defined chain `clash` from `mangel` table
iptables -t mangle -X clash

# --------------------------------------------------------------------------

# delete three rules in `PREROUTING` chain from nat table
iptables -t nat -D PREROUTING -p tcp -d 8.8.8.8 -j REDIRECT --to-port 7892
iptables -t nat -D PREROUTING -p tcp -d 8.8.4.4 -j REDIRECT --to-port 7892
iptables -t nat -D PREROUTING -p tcp -j clash

# flush the rules under user-defined chain `clash`
iptables -t nat -F clash

# delete user-defined chain `clash` from `nat` table
iptables -t nat -X clash

相关命令

chmod +x tproxy_enable.sh
chmod +x tproxy_disable.sh

启用透明代理:

sudo ./tproxy_enable.sh

禁用透明代理:

sudo ./tproxy_disable.sh

Port forwarding

在路由器上设置端口转发到树莓派 WireGuard 监听的端口

获取公网 IP

curl ifconfig.co

公网 IPv4 地址需要向 ISP 申请,将 VPN 配置里 endpoint 改为获取到的 IPv4 地址通过 VPN 客户端连接即可

公网 IPv6 将 VPN 配置里 endpoint 改为对应树莓派 IPv6 地址通过 VPN 客户端连接即可


Written by nnfewl, a noob. Follow me on Twitter