sing-box 透明代理配置。本文中的配置是示例,如果要使用请根据实际情况自己修改。

环境

Debian系统:专门用来做透明代理的网关服务器,或者叫旁路网关,并且在/etc/network/interfaces将网络设置为静态:

iface ens16 inet static
    address 10.0.0.20/24
    gateway 10.0.0.1

sing-box:运行在网关服务器上做DNS服务、规则分流和代理。

配置

注意替换里面的路径。

路径/path/to/sing-box/nftables.sh

#!/bin/bash 

INTERFACE=$(ip route show default | awk '/default/ {print $5}')
TPROXY_PORT=7895  ## 和 sing-box 中定义的一致
ROUTING_MARK=666  ## 和 sing-box 中定义的一致
PROXY_FWMARK=1
PROXY_ROUTE_TABLE=100

# https://en.wikipedia.org/wiki/Reserved_IP_addresses
ReservedIP4='{ 127.0.0.0/8, 10.0.0.0/16, 192.168.0.0/16, 100.64.0.0/10, 169.254.0.0/16, 172.16.0.0/12, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32 }'
CustomBypassIP='{ 192.168.0.0/16 }' ## 添加你的代理IP地址或其他不想透明代理的地址

if [ $# != 1 ]
then
   echo "Use $(basename "$0") <set|clear>"
   exit 1;
fi

clearFirewallRules()
{
    IPRULE=$(ip rule show | grep $PROXY_ROUTE_TABLE)
    if [ -n "$IPRULE" ]
    then
        ip -f inet rule del fwmark $PROXY_FWMARK lookup $PROXY_ROUTE_TABLE
        ip -f inet route del local default dev $INTERFACE table $PROXY_ROUTE_TABLE
        echo "clear ip rule"
    fi

    nft flush ruleset
    echo "clear nftables"
}

if [ $1 = 'set' ]
then

    clearFirewallRules

    ip -f inet rule add fwmark $PROXY_FWMARK lookup $PROXY_ROUTE_TABLE
    ip -f inet route add local default dev $INTERFACE table $PROXY_ROUTE_TABLE
    sysctl -w net.ipv4.ip_forward=1 > /dev/null

nft -f - <<EOF
table inet sing-box {
    chain prerouting_tproxy {
        type filter hook prerouting priority mangle; policy accept;
        meta l4proto { tcp, udp } th dport 53 tproxy to :$TPROXY_PORT accept comment "DNS透明代理"
        ip daddr $CustomBypassIP accept comment "绕过某些地址"
        fib daddr type local meta l4proto { tcp, udp } th dport $TPROXY_PORT reject with icmpx type host-unreachable comment "直接访问tproxy端口拒绝, 防止回环"
        fib daddr type local accept comment "本机绕过"
        ip daddr $ReservedIP4 accept comment "保留地址绕过"
        meta l4proto tcp socket transparent 1 meta mark set $PROXY_FWMARK accept comment "绕过已经建立的透明代理"
        meta l4proto { tcp, udp } tproxy to :$TPROXY_PORT meta mark set $PROXY_FWMARK comment "其他流量透明代理"
    }

    chain output_tproxy {
        type route hook output priority mangle; policy accept;
        oifname != $INTERFACE accept comment "绕过本机内部通信的流量(接口lo)"
        meta mark $ROUTING_MARK accept comment "绕过本机sing-box发出的流量"
        meta l4proto { tcp, udp } th dport 53 meta mark set $PROXY_FWMARK accept comment "DNS重路由到prerouting"
        udp dport { netbios-ns, netbios-dgm, netbios-ssn } accept comment "绕过NBNS流量"
        ip daddr $CustomBypassIP accept comment "绕过某些地址"
        fib daddr type local accept comment "本机绕过"
        ip daddr $ReservedIP4 accept comment "保留地址绕过"
        meta l4proto { tcp, udp } meta mark set $PROXY_FWMARK comment "其他流量重路由到prerouting"
    }
}
EOF
    
    echo "set nftables"

elif [ $1 = 'clear' ]
then
    clearFirewallRules
fi

路径/etc/systemd/system/sing-box.service

[Unit]
Description=sing-box tproxy daemon.
After=network.target nss-lookup.target network-online.target

[Service]
Type=simple
LimitNOFILE=1000000
ExecStart=/path/to/sing-box/sing-box run -c /path/to/sing-box/sing-box-tproxy.json -D /path/to/sing-box/data
ExecStartPost=bash /path/to/sing-box/nftables.sh set
ExecStop=bash /path/to/sing-box/nftables.sh clear

[Install]
WantedBy=multi-user.target

客户端配置文件,路径/path/to/sing-box/sing-box-tproxy.json

{
    "log": {
        "disabled": false,
        "level": "warn",
        "output": "box.log",
        "timestamp": true
    },
    "dns": {
        "servers": [
            {
                "tag": "tencentDNS",
                "address": "tls://120.53.53.53",
                "detour": "DIRECT"
            },
            {
                "tag": "aliDNS",
                "address": "https://223.6.6.6/dns-query",
                "detour": "DIRECT"
            },
            {
                "tag": "cloudflareDNS",
                "address": "https://1.1.1.1/dns-query",
                "detour": "PROXY"
            },
            {
                "tag": "localDNS",
                "address": "10.0.0.1",
                "detour": "DIRECT"
            },
            {
                "tag": "refusedDNS",
                "address": "rcode://success"
            },
            {
                "tag": "fakeipDNS",
                "address": "fakeip"
            }
        ],
        "rules": [
            {
                "outbound": "any",
                "server": "tencentDNS"
            },
            {
                "query_type": [
                    "AAAA",
                ],
                "server": "refusedDNS",
                "disable_cache": true
            },
            {
                "invert": true,
                "protocol": "dns",
                "server": "refusedDNS",
                "disable_cache": true
            },
            {
                "rule_set": "geosite-private",
                "server": "localDNS"
            },
            {
                "rule_set": [
                    "geosite-geolocation-cn",
                    "geosite-win-update",
                    "geosite-apple"
                ],
                "server": "tencentDNS"
            },
            {
                "query_type": "A",
                "rule_set": [
                    "geosite-google",
                    "geosite-github",
                    "geosite-telegram",
                    "geosite-openai",
                    "geosite-steam",
                    "geosite-gfw"
                ],
                "server": "fakeipDNS",
                "rewrite_ttl": 10
            },
            {
                "query_type": [
                    "A",
                    "CNAME",
                    "SVCB",
                    "HTTPS"
                ],
                "server": "cloudflareDNS"
            }
        ],
        "final": "localDNS",
        "strategy": "ipv4_only",
        "disable_cache": false,
        "disable_expire": false,
        "independent_cache": true,
        "reverse_mapping": false,
        "fakeip": {
            "enabled": true,
            "inet4_range": "198.18.0.0/15"
        }
    },
    "inbounds": [
        {
            "tag": "TPROXY-IN",
            "type": "tproxy",
            "listen": "0.0.0.0",
            "listen_port": 7895,
            "udp_timeout": 180,
            "sniff": true
        },
        {
            "tag": "MIXED-MAIN-IN",
            "type": "mixed",
            "listen": "0.0.0.0",
            "listen_port": 7900,
            "udp_timeout": 180,
            "sniff": true
        }
    ],
    "outbounds": [
        {
            "tag": "PROXY",
            "type": "selector",
            "outbounds": [
                "PROXY_REALITY",
                "PROXY_TROJAN"
            ],
            "default": "PROXY_REALITY",
            "interrupt_exist_connections": true
        },
        {
            "tag": "FINAL",
            "type": "selector",
            "outbounds": [
                "PROXY",
                "DIRECT"
            ],
            "default": "PROXY",
            "interrupt_exist_connections": true
        },
        {
            "tag": "APPLE",
            "type": "selector",
            "outbounds": [
                "PROXY",
                "DIRECT",
                "PROXY_REALITY",
                "PROXY_TROJAN"
            ],
            "default": "DIRECT",
            "interrupt_exist_connections": true
        },
        {
            "tag": "STEAM",
            "type": "selector",
            "outbounds": [
                "PROXY",
                "DIRECT",
                "PROXY_REALITY",
                "PROXY_TROJAN"
            ],
            "default": "PROXY",
            "interrupt_exist_connections": true
        },
        {
            "tag": "OPENAI",
            "type": "selector",
            "outbounds": [
                "PROXY",
                "DIRECT",
                "PROXY_REALITY",
                "PROXY_TROJAN"
            ],
            "default": "PROXY_REALITY",
            "interrupt_exist_connections": true
        },
        {
            "tag": "TELEGRAM",
            "type": "selector",
            "outbounds": [
                "PROXY",
                "DIRECT",
                "PROXY_REALITY",
                "PROXY_TROJAN"
            ],
            "default": "PROXY_REALITY",
            "interrupt_exist_connections": true
        },
        {
            "tag": "DOWNLOADER",
            "type": "selector",
            "outbounds": [
                "PROXY",
                "DIRECT",
                "PROXY_REALITY",
                "PROXY_TROJAN"
            ],
            "default": "DIRECT",
            "interrupt_exist_connections": true
        },
        {
            "tag": "PROXY_TROJAN",
            "type": "trojan",
            "server": "exp1.com",
            "server_port": 443,
            "password": "passwd",
            "tls": {
                "enabled": true,
                "server_name": "exp.com",
                "insecure": false,
                "utls": {
                    "enabled": true,
                    "fingerprint": "chrome"
                }
            },
            "multiplex": {
                "enabled": true,
                "protocol": "h2mux",
                "max_streams": 6,
                "padding": true
            }
        },
        {
            "tag": "PROXY_REALITY",
            "type": "vless",
            "server": "1.2.3.4",
            "server_port": 443,
            "uuid": "xx",
            "tls": {
                "enabled": true,
                "server_name": "exp2.com",
                "utls": {
                    "enabled": true,
                    "fingerprint": "chrome"
                },
                "reality": {
                    "enabled": true,
                    "public_key": "xx",
                    "short_id": "xx"
                }
            },
            "multiplex": {
                "enabled": true,
                "protocol": "h2mux",
                "max_streams": 6,
                "padding": true
            }
        },
        {
            "tag": "DIRECT",
            "type": "direct"
        },
        {
            "tag": "DNS-OUT",
            "type": "dns"
        },
        {
            "tag": "BLOCK",
            "type": "block"
        }
    ],
    "route": {
        "rules": [
            {
                "protocol": "dns",
                "outbound": "DNS-OUT"
            },
            {
                "port": 53,
                "outbound": "DNS-OUT"
            },
            {
                "rule_set": "geosite-private",
                "outbound": "DIRECT"
            },
            {
                "clash_mode": "direct",
                "outbound": "DIRECT"
            },
            {
                "clash_mode": "global",
                "outbound": "PROXY"
            },
            {
                "rule_set": "geosite-apple",
                "outbound": "APPLE"
            },
            {
                "rule_set": "geosite-openai",
                "outbound": "OPENAI"
            },
            {
                "rule_set": [
                    "geosite-telegram",
                    "geoip-telegram"
                ],
                "outbound": "TELEGRAM"
            },
            {
                "rule_set": [
                    "geosite-google",
                    "geosite-github",
                    "geosite-debian",
                    "geosite-gfw"
                ],
                "outbound": "PROXY"
            },
            {
                "rule_set": [
                    "geoip-private",
                    "geoip-cn",
                    "geosite-geolocation-cn",
                    "geosite-win-update"
                ],
                "outbound": "DIRECT"
            },
            {
                "rule_set": "geosite-steam",
                "outbound": "STEAM"
            },
            {
                "source_ip_cidr": "10.0.0.10/32",
                "outbound": "DOWNLOADER"
            }
        ],
        "rule_set": [
            {
                "tag": "geosite-private",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/private.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geosite-google",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/google.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geosite-geolocation-cn",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/geolocation-cn.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geosite-win-update",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/win-update.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geosite-apple",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/apple.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geosite-github",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/github.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geoip-telegram",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/telegram.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "30d"
            },
            {
                "tag": "geosite-telegram",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/telegram.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geosite-bing",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/bing.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geosite-openai",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/openai.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geosite-steam",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/steam.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geosite-debian",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/debian.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geosite-gfw",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/gfw.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geoip-private",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/private.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            },
            {
                "tag": "geoip-cn",
                "type": "remote",
                "format": "source",
                "url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/cn.json",
                "download_detour": "PROXY_TROJAN",
                "update_interval": "999d"
            }
        ],
        "final": "FINAL",
        "auto_detect_interface": true,
        "default_mark": 666
    },
    "experimental": {
        "clash_api": {
            "external_controller": "0.0.0.0:9090",
            "external_ui": "yacd",
            "external_ui_download_url": "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip",
            "external_ui_download_detour": "PROXY_TROJAN",
            "secret": "passwd",
            "default_mode": "rule"
        }
    }
}

服务端配置文件,前置haproxy复用443端口

{
    "log": {
        "disabled": false,
        "level": "warn",
        "timestamp": true
    },
    "inbounds": [
        {
            "type": "vless",
            "tag": "vless-reality",
            "listen": "0.0.0.0",
            "listen_port": 500,
            "users": [
                {
                    "name": "xx",
                    "uuid": "uuid",
                }
            ],
            "tls": {
                "enabled": true,
                "server_name": "exp2.com",
                "reality": {
                    "enabled": true,
                    "handshake": {
                        "server": "xx.com",
                        "server_port": 443
                    },
                    "private_key": "private_key",
                    "short_id": ["short_id"]
                }
            },
            "multiplex": {
                "enabled": true,
                  "padding": true
            }
        },
        {
            "type": "trojan",
            "tag": "trojan",
            "listen": "0.0.0.0",
            "listen_port": 501,
            "users": [
                {
                    "password": "password"
                }
            ],
            "tls": {
                "enabled": true,
                "server_name": "exp1.com",
                "alpn": ["h2", "http/1.1"],
                "acme": {
                    "domain": "exp1.com",
                    "data_directory": "/path/to/sing-box/data/cert",
                    "email": "email@gmail.com",
                    "provider": "letsencrypt",
                    "dns01_challenge": {
                        "provider": "cloudflare",
                          "api_token": "api_token"
                    }
                }
            },
            "multiplex": {
                "enabled": true,
                  "padding": true
            }
        }
    ],
    "outbounds": [
        {
            "type": "direct",
            "tag": "direct"
        }
    ]
}

路由和规则匹配

在透明代理环境中,程序在发起连接前首先需要知道目标IP地址,也就是DNS查询,拿到IP后再建立连接。这在sing-box路由中体现为2次入站:

  • 第一次:nftables判断目标端口是否为53,透明代理入站sing-box,route.rules中匹配DNS规则分流到DNS模块处理DNS查询。
  • 第二次:程序拿着IP透明代理入站sing-box,路由和规则匹配,走代理或直连出站。

telegram等程序已经有目标IP的情况下,则没有DNS查询步骤,入站就是IP。

如果程序使用socks5或http代理入站,DNS查询通常是由代理服务器负责,此时入站是域名,DNS查询发生在出站的时候(本地直接出站或走代理到服务端出站)。

如果使用fakeIP呢?

如果DNS全返回fakeIP

  • 对于需要走代理的连接,本地出站时是传域名到服务器重新进行DNS查询建立连接。
  • 对于需要直连的连接,本地出站时也是域名,此时sing-box会发起DNS查询,dns.rules时匹配outound:any返回正常的IP地址再建立连接。

从以上不难看出,fakeIP对于明确需要走代理的连接很有用,相当于在本地省略了一次DNS查询(sing-box立即返回fakeIP),由服务端进行DNS查询建立连接。

但由于DNS查询都返回fakeIP,匹配IP类规则失去意义,两者在路由时都只能开启sniff匹配域名规则。

如果DNS全返回正常IP

  • 对于需要走代理的连接,出站时传IP到服务端建立连接。
  • 对于需要直连的连接,就直接IP出站建立连接。

对于此种方案,优点是可以开启sniff同时匹配域名规则和IP规则。但缺点是对于需要走代理的连接,传到服务端的可能是被污染的IP,或者干脆在本地就对污染的IP匹配到错误的规则导致分流不符合预期。如果全使用国外DNS服务器走代理查询,速度上也很慢。

sniff_override_destination

如果在服务端入站启用sniff_override_destination,无论入站是IP还是域名,通通覆盖IP为域名重新进行DNS查询是否可行?这样不就解决服务端拿到IP是污染的问题了吗?但这样有个弊端。

有些连接目标IP地址是确定的,或者说是硬编码的,这种不需要DNS查询直接IP入站。这种情况下,如果强行用sniff到的域名覆盖目标地址重新进行DNS查询建立连接,可能会导致连接出现各种问题,比如sniff到的域名解析出的IP地址和程序的预期不符,或者干脆就是无效的域名。

折中方案

那么有没有一种方案既可以保留fakeIP优点,又可以最大限度的保证DNS查询的速度和准确,并且尽可能同时匹配域名和IP类规则?折中的方案是:

  • 仅对明确需要走代理的域名返回fakeIP,出站走代理。
  • 需要直连的域名就用国内的DNS服务器查询,出站走直连。
  • 其他连接用国外DNS服务器走代理查询,拿到可信IP后出站走代理。

启动

启动systemctl start sing-box
停止systemctl stop sing-box
重启systemctl restart sing-box 日志journalctl -f -u sing-box

如果最后测试没问题将sing-box设为开机自启动systemctl enable sing-box

使用

将局域网其他设备的网关和DNS服务器手动指定到该机器上,测试看看是否正常工作。没问题后可以在主路由器的DHCP附加选项(dhcp_option)中将网关和DNS服务器自动通告给客户端,这样就不用在其他设备手动指定了,这里以OpenWrt为例:

关于nftables

由于 nftables 更现代化、更高效、更灵活,具有很多优势,是时候抛弃使用 iptables 命令了。和 iptables 的逻辑不同,nftables 默认没有表和链,都需要你自己创建。创建链时需要注意:

type filter hook prerouting priority -150; policy accept;
  • typefilterroutenat,每种类型功能不一样,能在什么hook工作也不一样,比如redirect需要在nat类型进行。在iptables体现为4个预先定义的表(raw,mangle,nat,filter),并且默认定义了优先级不能改。
  • hookpreroutinginputforwardoutputpostrouting,取决于网络数据包在路由中的位置。在iptables体现为5个预先定义的链(PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING)。
  • priority:在当前hook中,各个自定义链的优先级和执行顺序。

Docker

Docker 目前仍然操纵 iptables 规则来使 docker 网络工作,建议不要在该网关服务器上使用 docker 相关服务,以免出现各种问题。

参考