ss/ssr/v2ray/socks5 透明代理

先说 ss/ssr 透明代理吧,ss-redir 是 ss-libevssr-libev 中的一个工具,配合 iptables 可以在 Linux 上实现 ss、ssr 透明代理,ss-redir 的 TCP 透明代理是通过 REDIRECT 方式实现的,而 UDP 透明代理是通过 TPROXY 方式实现的。注意:ss-redir 只存在于 libev 版本。
当然,ss/ssr 透明代理并不是只能用 ss-redir 来实现,使用 ss-local + redsocks2 同样可以实现 socks5(ss-local 是 socks5 服务器)全局透明代理;ss-local + redsocks2 实际上是 ss-redir 的分体实现,TCP 使用 REDIRECT 方式实现,UDP 使用 TPROXY 方式实现。
最后说一下 v2ray 的透明代理,其实原理和 ss/ssr-libev 一样,v2ray 可以看作是 ss-local、ss-redir、ss-server 的合体,同一个 v2ray 程序既可以作为 server 端,也可以作为 client 端。所以 v2ray 的透明代理也有两种实现方式,一是利用对应的 “ss-redir”(dokodemo-door入站协议),二是利用对应的 “ss-local”(socks入站协议) + redsocks2(redsocks2 可与任意 socks5 代理组合以实现透明代理)。

方案说明

用过 Linux SS/SSR 客户端(尤其指命令行界面)的都知道,它们比 Windows/Android 中的 SS/SSR 客户端难用多了,安装好就只有一个 ss-local(libev 版还有 ss-redir、ss-tunnel,但我相信大部分人装的都是 python 版的),启动 ss-local 后并不会像 Windows/Android 那样自动配置系统代理,此时它仅仅是一个本地 socks5 代理服务器,默认监听 127.0.0.1:1080,如果需要利用该 socks5 代理上外网,必须在命令中指定对应的代理,如 curl -4sSkL -x socks5h://127.0.0.1:1080 https://www.google.com

但我想大部分人要的代理效果都不是这样的,太原始了。那能不能配置所谓的“系统代理”呢,可以是可以,但是好像只支持 http 类型的代理,即在当前 shell 中设置 http_proxyhttps_proxy 环境变量,假设存在一个 http 代理(支持 CONNECT 请求方法),监听地址是 127.0.0.1:8118,可以这样做:export http_proxy=http://127.0.0.1:8118; export https_proxy=$http_proxy。执行完后,git、curl、wget 等命令会自动从环境变量中读取 http 代理信息,然后通过 http 代理连接目的服务器。

那问题来了,ss-local 提供的是 socks5 代理,不能直接使用怎么办?也简单,Linux 中有很多将 socks5 包装为 http 代理的工具,比如 privoxy。只需要在 /etc/privoxy/config 里面添加一行 forward-socks5 / 127.0.0.1:1080 .,启动 privoxy,默认监听 127.0.0.1:8118 端口,注意别搞混了,8118 是 privoxy 提供的 http 代理地址,而 1080 是 ss-local 提供的 socks5 代理地址,发往 8118 端口的数据会被 privoxy 处理并转发给 ss-local。所以我们现在可以执行 export http_proxy=http://127.0.0.1:8118; export https_proxy=$http_proxy 来配置当前终端的 http 代理,这样 git、curl、wget 这些就会自动走 ss-local 出去了。

当然我们还可以利用 privoxy 灵活的配置,实现 Windows/Android 中的 gfwlist 分流模式。gfwlist.txt 其实是对应的 Adblock Plus 规则的 base64 编码文件,显然不能直接照搬到 privoxy 上。这个问题其实已经有人解决了,利用 snachx/gfwlist2privoxy python 脚本就可轻松搞定。但其实我也重复的造了一个轮子:zfl9/gfwlist2privoxy,至于为什么要造这个轮子,是因为我当时运行不了他的脚本(也不知道什么原因),所以花了点时间用 shell 脚本实现了一个 gfwlist2privoxy(但其实我是用 perl 转换的,只不过用 shell 包装了一下)。脚本转换出来的是一个 gfwlist.action 文件,我们只需将该 gfwlist.action 文件放到 /etc/privoxy 目录,然后在 config 中添加一行 actionsfile gfwlist.action(当然之前 forward-socks5 那行要注释掉),重启 privoxy 就可以实现 gfwlist 分流了。

但仅仅依靠 http_proxyhttps_proxy 环境变量实现的终端代理效果不是很好,因为有些命令根本不理会你的 http_proxyhttps_proxy 变量,它们依旧走的直连。但又有大神想出了一个巧妙的方法,即 rofl0r/proxychains-ng,其原理是通过 LD_PRELOAD 特殊环境变量提前加载指定的动态库,来替换 glibc 中的同名库函数。这个 LD_PRELOAD 指向的其实就是 proxychains-ng 实现的 socket 包装库,这个包装库会读取 proxychains-ng 的配置文件(这里面配置代理信息),之后执行的所有命令调用的 socket 函数其实都是 proxychains-ng 动态库中的同名函数,于是就实现了全局代理,而命令对此一无所知。将 proxychains-ng 与 privoxy 结合起来基本上可以完美实现 ss/ssr 的本地全局 gfwlist 代理(小技巧,在 shell 中执行 exec proxychains -q bash 可以实现当前终端的全局代理,如果需要每个终端都自动全局代理,可以在 bashrc 文件中加入这行)。

但很多人对此依旧不满意,因为他们想实现 OpenWrt 这种路由级别的全局透明代理(并且还有 gfwlist、绕过大陆地址段这些分流模式可选择),这样只要设备连到该路由就能直接无缝上网,完全感觉不到“墙”的存在。如果忽略分流模式(即全部流量都走代理出去),那么实现是很简单的(几条 iptables 就可以搞定,但是这太简单粗暴了,很多国内网站走代理会非常慢,体验很不好);但是如果要自己实现 gfwlist、绕过大陆地址段这些模式,就稍微有点点复杂了,尤其是 gfwlist 模式。但这种透明代理的模式的确很诱人,毕竟只要设置一次就可以让所有内网设备上 Internet,于是我开始摸索如何在 Linux 中实现类似 OpenWrt 的代理模式,而我摸索出来的成果就是 ss-tproxy 透明代理脚本,ss-tproxy 可以运行在 Linux 软路由(网关)、Linux 物理机、Linux 虚拟机等环境中;可以用来透明代理 ss-tproxy 主机本身以及所有网关指向 ss-tproxy 主机的内网主机的 TCP、UDP 流量。

安装依赖

必要的依赖

  • global 模式:TPROXY 模块、ip 命令、dnsmasq 命令
  • gfwlist 模式:TPROXY 模块、ip 命令、dnsmasq 命令、perl 命令、ipset 命令
  • chnroute 模式:TPROXY 模块、ip 命令、dnsmasq 命令、chinadns 命令、ipset 命令

可选的依赖

  • 如果要使用 ss-tproxy 的 update-gfwlistupdate-chnonlyupdate-chnroute 功能,则还需 curl 命令

有必要声明一点,不是说下面列出的所有依赖都需要安装,你只需要装对应模式需要的依赖即可,比如你如果只用 gfwlist 模式,那么就是确保 TPROXY 模块、ip 命令、dnsmasq 命令、perl 命令、ipset 命令都存在,如果没有那么就安装一下,仅此而已;在安装依赖前请先自己检查是否已有对应的模块或命令,不要盲目照搬照抄。很多命令其实发行版都自带了,所以实际需要的依赖项不是很多。

curl

请检查 curl 是否支持 HTTPS 协议,使用 curl --version 可查看(Protocols)

perl5

Perl5 的版本最好 v5.10.0+ 以上(使用 perl -v 命令可查看)

ipset

TPROXY

TPROXY 是一个 Linux 内核模块,在 Linux 2.6.28 后进入官方内核。一般正常的发行版都没有裁剪 TPROXY 模块,TPROXY 模块缺失问题主要出现在无线路由固件上(某些精简型发行版也会去掉 TPROXY 模块,比如 Alpine)。使用以下方法可以检测当前内核是否包含 TPROXY 模块。

iproute2

haveged

如果有时候启动 ss-redir、ss-tunnel 会失败,且错误提示如下,则需要安装 haveged 或 rng-utils/rng-tools。

这里以 haveged 为例,当然,你也可以选择安装 rng-utils/rng-tools,都是一样的:

dnsmasq

chinadns

dnsforwarder

如果 make 报错,提示 undefined reference to rpl_malloc,请编辑 config.h.in 文件,把里面的 #undef malloc#undef realloc 删掉,然后再编译,即:./configure --enable-downloader=nomake && make install

v2ray

安装很简单,直接使用 v2ray 官方提供的 shell 脚本即可,默认配置开机自启

ss-libev

ArchLinux 建议使用 pacman -S shadowsocks-libev 安装,方便快捷,更新也及时。
CentOS/RHEL 或其它发行版,强烈建议 编译安装,仓库安装的可能会有问题(版本太老或者根本用不了)。
下面的代码完全摘自 ss-libev 官方 README.md,随着时间的推移可能有变化,最好照着最新 README.md 来做。

ssr-libev

shadowsocksr-backup/shadowsocksr-libev(貌似已停止更新,但目前使用没问题,只是一些新特性不支持,如有更好的源请告诉我~)
https://github.com/shadowsocksrr/shadowsocksr-libev/tree/Akkariiin/master,另一个 ssr-libev 源,Akkariiin-* 分支目前仍在更新
本文仍以 shadowsocksr-backup/shadowsocks-libev 为例,毕竟另一个源我没试过,但是这个源我自己用过大半年,没有任何问题,很稳定

如果编译失败,可以看下这个 issue:https://github.com/shadowsocksrr/shadowsocksr-libev/issues/40(gcc 版本过高导致的)
解决方法就是使用此分支:git clone -b Akkariiin/develop https://github.com/shadowsocksrr/shadowsocksr-libev.git

代理脚本

https://github.com/zfl9/ss-tproxy

代理测试

这里就简单的使用 curl 进行测试,如果有网页源码输出,基本就没什么问题。

原理解析

脚本浅析

开头部分主要是检查脚本相关的一些文件是否存在

# 默认的 ss-tproxy.conf 配置文件路径
ss_tproxy_config='/etc/ss-tproxy/ss-tproxy.conf'

# 设置 PATH,确保不会在运行过程中出现命令找不到的情况
PATH=$PATH:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin

# 检查 ss-tproxy 脚本相关的文件是否存在,以及加载配置文件
[ ! -f "$ss_tproxy_config"  ] && { echo "[ERR] No such file or directory: '$ss_tproxy_config'"  1>&2; exit 1; } || source "$ss_tproxy_config"
[ ! -f "$file_gfwlist_txt"  ] && { echo "[ERR] No such file or directory: '$file_gfwlist_txt'"  1>&2; exit 1; }
[ ! -f "$file_gfwlist_ext"  ] && { echo "[ERR] No such file or directory: '$file_gfwlist_ext'"  1>&2; exit 1; }
[ ! -f "$file_chnroute_txt" ] && { echo "[ERR] No such file or directory: '$file_chnroute_txt'" 1>&2; exit 1; }
[ ! -f "$file_chnroute_set" ] && { echo "[ERR] No such file or directory: '$file_chnroute_set'" 1>&2; exit 1; }

中间部分都是一些 function 函数,目前定义的函数有

function check_command { ... }      # 检查相关命令是否存在
function update_chnonly { ... }     # 为 chnonly 模式更新列表
function update_gfwlist { ... }     # 为 gfwlist 模式更新列表
function update_chnroute { ... }    # 为 chnroute 模式更新列表
function flush_dnscache { ... }     # 清空 dnsmasq 上的 DNS 缓存
function start_proxy { ... }        # 运行 proxy_runcmd 启动命令
function start_resolver { ... }     # 开启无污染的 DNS 解析及分流
function start_iptables { ... }     # 设置相关模式的 iptables 规则
function start_dnsproxy { ... }     # 更新 /etc/resolv.conf 解析文件
function start { ... }              # 脚本启动的主逻辑,调用上述函数
function stop { ... }               # 脚本关闭的主逻辑,清理相关规则
function status { ... }             # 脚本状态的主逻辑,ss/netstat 检测
function show_iptables { ... }      # 查看 mangle 表和 nat 表的相关规则
function flush_iptables { ... }     # 清空 raw、mangle、nat、filter 表
function help { ... }               # 打印 ss-tproxy 脚本的使用&帮助信息

结尾部分就是处理命令行参数,然后调用上面写好的函数

case $1 in
    start)              check_command; start; status;;
    stop)               check_command; stop; status;;
    status)             check_command; status;;
    r*)                 check_command; stop; status; echo; start; status;;
    show-iptables)      show_iptables;;
    flush-iptables)     flush_iptables;;
    flush-gfwlist)      ipset -F gfwlist &>/dev/null;;
    flush-dnscache)     check_command; flush_dnscache;;
    update-chnonly)     check_command; update_chnonly;;
    update-gfwlist)     check_command; update_gfwlist;;
    update-chnroute)    check_command; update_chnroute;;
    c*)                 check_command;;
    h*)                 help;;
    *)                  help; exit 1;;
esac

dns 解析模块

开头部分
遍历 proxy_server 数组,如果是域名,则解析为对应 IP,稍后会加到 dnsmasq 的静态解析列表中(相当于 hosts 文件),之所以要这么做,是因为 v2ray 的一个特性造成的,如果给 v2ray 配置的代理服务器为域名,那么 v2ray 每次连接到代理服务器之前,都会去解析这个域名(而不是像 ss-libev、ssr-libev 那样,启动的时候就先解析出对应的服务器 IP),如果没有把这个域名加入静态解析列表,那么此刻代理就会陷入死循环,因为 v2ray 自己发出去的 dns 解析请求被导向代理隧道了(global 模式、chnroute 模式),而代理本身就是要先解析出这个域名对应的 IP 才能连接到我们的代理服务器;而如果我们在启动代理之前就先解析出对应的 IP,并加入 dnsmasq 的静态解析列表,就不会有问题了,因为当 v2ray 发出的 dns 解析请求到达 dnsmasq 时,dnsmasq 会从静态解析列表中返回这个 IP,然后就能正常连接 v2ray 代理服务器了。

for server in "${proxy_server[@]}"; do
    if [ $(grep -Ec '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' <<<"$server") -eq 0 ]; then
        server_addrs+=("/$server/$(ping -nq -c1 -t1 -W1 $server | head -n1 | awk -F'[()]' '{print $2}')")
    fi
done

global 模式
比较简单,因为不需要分流,全部解析都交给远程 dns 去解析(8.8.8.8,走代理),之所以加个 dnsmasq,而不是直接用 iptables 代理到 8.8.8.8,是为了利用 dnsmasq 的缓存功能来加速 DNS 解析,不然每次解析都要请求远程服务器,多慢啊,有了 DNS 缓存,我第二次解析同一个域名就直接可以返回给请求客户端了,因此,所有 mode 我都加入了 dnsmasq 来作缓存。

dnsmasq -C <(cat <<EOF
$([ "$dnsmasq_log_enable" = 'true' ] && echo 'log-queries')
log-facility = $dnsmasq_log_file
log-async = 20
domain-needed
cache-size = $dnsmasq_cache_size
$([ $(dnsmasq --help | grep -c min-cache-ttl) -ne 0 ] && echo "min-cache-ttl = $dnsmasq_cache_time")
no-negcache
no-resolv
port = 60053
server = ${dns_remote/:/#}
$(for server_addr in "${server_addrs[@]}"; do echo "address = $server_addr"; done)
EOF
)

gfwlist 模式
和 global 模式的配置差不多,只不过默认使用国内 dns 进行解析(114.114.114.114,走直连),然后对位于 gfwlist.txt、gfwlist.ext 中的域名使用远程 dns 进行解析(8.8.8.8,走代理),也就是所谓的黑名单模式,当然这里还使用了 dnsmasq 的 ipset 功能,黑名单中的这些域名解析出的 IP 地址会自动加入到我们指定的 ipset 列表中,这样我们在 iptables 规则中就可以动态的让这些 IP 走代理,从而完成分流(域名到 IP 的映射)。

dnsmasq -C <(cat <<EOF
$([ "$dnsmasq_log_enable" = 'true' ] && echo 'log-queries')
log-facility = $dnsmasq_log_file
log-async = 20
domain-needed
cache-size = $dnsmasq_cache_size
$([ $(dnsmasq --help | grep -c min-cache-ttl) -ne 0 ] && echo "min-cache-ttl = $dnsmasq_cache_time")
no-negcache
no-resolv
port = 60053
server = $dns_direct
$(for server_addr in "${server_addrs[@]}"; do echo "address = $server_addr"; done)
$(perl -pe "s@^.*+\$@server=/$&/${dns_remote/:/#}\nipset=/$&/gfwlist@" $file_gfwlist_txt <(
perl -ne 'print unless m@^\s|\s\n$|^#|^\d++\.\d++\.\d++\.\d++(?:/\d++)?$@' $file_gfwlist_ext
))
EOF
)

chnroute 模式
这里的 dnsmasq 就是起到一个缓存的作用,因为 chinadns 不支持 dns 缓存,为了提高解析性能(抗住大并发的 dns 解析请求),所以很有必要加上 dnsmasq;这里解释一下第一段代码的作用,为什么要将直连 dns 加入 chnroute 列表,要理解这个,首先要理解 chinadns 的分流原理(个人理解,有错误还请指出);chinadns 要运行,首先要给它配置两组 dns 服务器,一组是 国内 dns,一组是 可信 dns(国外 dns),所以至少需要给它配置两个上游 dns,就假设为 114.114.114.114(国内 dns)、8.8.8.8(可信 dns,走代理),然后还要给它指定一个 chnroute 列表(大陆地址段);当 chinadns 收到一个解析请求时,它会同时向这两组 dns 发出请求,然后:

  • 如果国内 dns 先返回(正常情况,因为直连肯定比代理快),那么 chinadns 会检查国内 dns 返回的 ip 是否为大陆地址(chnroute 列表),如果是,则 chinadns 会直接将这个解析出来的 IP 返回给请求客户端,然后完成解析(忽略可信 dns 的结果);如果国内 dns 返回的 ip 不是大陆地址(chnroute 列表),那么 chinadns 会过滤掉国内 dns 的解析结果,然后等待可信 dns 的解析结果并将其返回给请求客户端,完成解析。
  • 如果可信 dns 先返回(非正常情况,比如给可信 dns 加了本地缓存,导致它最先返回),那么 chinadns 会直接返回可信 dns 的解析结果,而不会考虑国内 dns 解析结果,如果是这样的话,那么其实 chinadns 的分流就完全出问题了,根本就没有分流,全都是返回远程 dns 解析出来的结果,所以一定不能给 chinadns 的可信 dns 设置 dns 缓存(如给它套一层 dnsmasq/pdnsd/dnsforwarder)。

那这和第一段代码有什么关联?当然有关联,你可曾想过,chinadns 如何知道你传递给它的这些 dns 哪个是国内 dns,哪个是国外 dns?那肯定得通过 chnroute.txt 列表来判断啊,如果这个 dns 是大陆地址,那么它就是 国内 dns,否则它就是 可信 dns,而为了保险,我干脆就将 dns_direct 加入这个列表,这样就可以避免 chinadns 将我们的国内 dns 认为是国外 dns 了(我印象中出现过这种情况,而且 issues 里面也有一个人遇到过这个问题)。

# 将直连 dns 加入列表
temp_chnroute_txt=$(mktemp)
cat $file_chnroute_txt >$temp_chnroute_txt
echo "$dns_direct/32" >>$temp_chnroute_txt
# 将内网地址段加入列表
for intranet in "${ipts_intranet[@]}"; do echo "$intranet" >>$temp_chnroute_txt; done

# 运行 chinadns(分流)
chinadns_params="-b 0.0.0.0 -p 65353 -s $dns_direct,$dns_remote -c $temp_chnroute_txt"
[ "$chinadns_mutation" = 'true' ] && chinadns_params+=' -m'
[ "$chinadns_verbose"  = 'true' ] && chinadns_params+=' -v'
(chinadns $chinadns_params </dev/null &>>$chinadns_logfile &)

# 运行 dnsmasq(缓存)
dnsmasq -C <(cat <<EOF
$([ "$dnsmasq_log_enable" = 'true' ] && echo 'log-queries')
log-facility = $dnsmasq_log_file
log-async = 20
domain-needed
cache-size = $dnsmasq_cache_size
$([ $(dnsmasq --help | grep -c min-cache-ttl) -ne 0 ] && echo "min-cache-ttl = $dnsmasq_cache_time")
no-negcache
no-resolv
port = 60053
server = 127.0.0.1#65353
$(for server_addr in "${server_addrs[@]}"; do echo "address = $server_addr"; done)
EOF
)

iptables 规则

start_iptables() 这个函数基本上是最复杂的函数了,但只要理清楚了这里面的关系和执行逻辑,就很容易理解。

设置相关内核参数

# 启用软路由功能
sysctl -w net.ipv4.ip_forward=1 &>/dev/null

# 禁止发送 icmp 重定向
for dir in $(ls /proc/sys/net/ipv4/conf); do
    sysctl -w net.ipv4.conf.$dir.send_redirects=0 &>/dev/null
done

检查 ipts_non_snat 选项的函数

function check_snat_rule {
    if [ "$ipts_non_snat" != 'true' ]; then
        if ! iptables -t nat -C POSTROUTING -s $intranet ! -d $intranet -j MASQUERADE &>/dev/null; then
            iptables -t nat -A POSTROUTING -s $intranet ! -d $intranet -j MASQUERADE
        fi
    fi
}

遍历 ipts_intranet 内网地址数组,找出非标网段

function dec2bin {
    for ((n = $1; n > 0; n >>= 1)); do bit="$((n & 1))$bit"; done
    printf "%08d\n" "$bit" # 这有个显示问题,其实是 % 08d
}

function net2bin {
    net=$(awk -F/ '{print $1}' <<<$1)
    len=$(awk -F/ '{print $2}' <<<$1)
    IFS='.' read -ra bytes <<<"$net"
    for byte in "${bytes[@]}"; do result+="$(dec2bin $byte)"; done
    echo "${result:0:len}"
}

for cidr in "${ipts_intranet[@]}"; do
    curnet=$(net2bin $cidr)
    for stdnet in 00001010 101011000001 1010100111111110 1100000010101000; do
        [[ "$curnet" == "$stdnet"* ]] && continue 2
    done
    ipts_intranet_nonstd+=($cidr)
done

设置外层 iptables 自定义规则链、设置 ip rule 策略路由

iptables -t mangle -N SSTP_OUT
iptables -t mangle -N SSTP_PRE
iptables -t nat    -N SSTP_OUT
iptables -t nat    -N SSTP_PRE

iptables -t mangle -A OUTPUT     -j SSTP_OUT
iptables -t mangle -A PREROUTING -j SSTP_PRE
iptables -t nat    -A OUTPUT     -j SSTP_OUT
iptables -t nat    -A PREROUTING -j SSTP_PRE

# 之所以要设置 ip rule 规则是因为 TPROXY 代理方式需要
ip route add local 0/0 dev lo     table $ipts_rt_tab
ip rule  add fwmark $ipts_rt_mark table $ipts_rt_tab

纯 tproxy 方式 - gfwlist 分流

# 将黑名单中的 ip 和网段加入 ipset
ipset -N gfwlist hash:net &>/dev/null
perl -ne 'print if m@^\d++\.\d++\.\d++\.\d++(?:/\d++)?$@' $file_gfwlist_ext | xargs -n1 ipset -A gfwlist &>/dev/null

# 打上 iptables 标记,mark 了的会走代理
iptables -t mangle -N SETMARK
iptables -t mangle -A SETMARK -p udp -d ${dns_remote%:*}     -j MARK --set-mark $ipts_rt_mark
iptables -t mangle -A SETMARK -m set --match-set gfwlist dst -j MARK --set-mark $ipts_rt_mark

# 本机发出去的 TCP 和 UDP 走一下 SETMARK 链
iptables -t mangle -A SSTP_OUT -p tcp -j SETMARK
iptables -t mangle -A SSTP_OUT -p udp -j SETMARK

# 将发往 127.0.0.1:53/udp 的包重定向给 dnsmasq
iptables -t nat    -A SSTP_OUT -p udp -d 127.0.0.1 --dport 53 -j REDIRECT --to-ports 60053

# 遍历内网地址段,处理内网主机发过来的 TCP 和 UDP
for intranet in "${ipts_intranet[@]}"; do
    # 将内网主机发出的 dns 请求包重定向给 dnsmasq
    iptables -t mangle -A SSTP_PRE -p udp -s $intranet --dport 53 -m mark ! --mark $ipts_rt_mark -j ACCEPT
    iptables -t nat    -A SSTP_PRE -p udp -s $intranet --dport 53 -m mark ! --mark $ipts_rt_mark -j REDIRECT --to-ports 60053

    # 让内网主机发出的 TCP 和 UDP 走一下 SETMARK 链
    iptables -t mangle -A SSTP_PRE -p tcp -s $intranet            -m mark ! --mark $ipts_rt_mark -j SETMARK
    iptables -t mangle -A SSTP_PRE -p udp -s $intranet            -m mark ! --mark $ipts_rt_mark -j SETMARK

    # 检查是否需要设置 SNAT/MASQUERADE 规则
    check_snat_rule
done

# 将所有打了标记的 TCP 和 UDP 包透明地转发到代理的监听端口
iptables -t mangle -A SSTP_PRE -m mark --mark $ipts_rt_mark -p tcp -j TPROXY --on-ip 127.0.0.1 --on-port $proxy_tcport
iptables -t mangle -A SSTP_PRE -m mark --mark $ipts_rt_mark -p udp -j TPROXY --on-ip 127.0.0.1 --on-port $proxy_udport

纯 tproxy 方式 - global/chnroute 分流

# 打上 iptables 标记
iptables -t mangle -N SETMARK
iptables -t mangle -A SETMARK -d 0/8        -j RETURN
iptables -t mangle -A SETMARK -d 10/8       -j RETURN
iptables -t mangle -A SETMARK -d 127/8      -j RETURN
iptables -t mangle -A SETMARK -d 169.254/16 -j RETURN
iptables -t mangle -A SETMARK -d 172.16/12  -j RETURN
iptables -t mangle -A SETMARK -d 192.168/16 -j RETURN
iptables -t mangle -A SETMARK -d 224/4 -j RETURN
iptables -t mangle -A SETMARK -d 240/4 -j RETURN
for cidr in "${ipts_intranet_nonstd[@]}"; do
    iptables -t mangle -A SETMARK -d $cidr -j RETURN
done
if ! [ "$proxy_dports" ]; then
    for server in "${proxy_server[@]}"; do
        iptables -t mangle -A SETMARK -d $server -j RETURN
    done
else
    for server in "${proxy_server[@]}"; do
        iptables -t mangle -A SETMARK -d $server -p tcp -m multiport --dports $proxy_dports -j RETURN
        iptables -t mangle -A SETMARK -d $server -p udp -m multiport --dports $proxy_dports -j RETURN
    done
fi
if [ "$mode" = chnroute ]; then
    ipset -X chnroute &>/dev/null
    ipset -R <$file_chnroute_set
    iptables -t mangle -A SETMARK -m set --match-set chnroute dst -j RETURN
fi
iptables -t mangle -A SETMARK -j MARK --set-mark $ipts_rt_mark

# 本机 TCP 和 UDP 走标记链
iptables -t mangle -A SSTP_OUT -p tcp -j SETMARK
iptables -t mangle -A SSTP_OUT -p udp -j SETMARK

# 本机 DNS 重定向给 dnsmasq
iptables -t nat    -A SSTP_OUT -p udp -d 127.0.0.1 --dport 53 -j REDIRECT --to-ports 60053

# 遍历内网地址段,处理 TCP 和 UDP
for intranet in "${ipts_intranet[@]}"; do
    # 将内网主机发出的 dns 请求重定向给 dnsmasq
    iptables -t mangle -A SSTP_PRE -p udp -s $intranet --dport 53 -m mark ! --mark $ipts_rt_mark -j ACCEPT
    iptables -t nat    -A SSTP_PRE -p udp -s $intranet --dport 53 -m mark ! --mark $ipts_rt_mark -j REDIRECT --to-ports 60053

    # 让内网主机发出的 TCP 和 UDP 走一下标记链
    iptables -t mangle -A SSTP_PRE -p tcp -s $intranet            -m mark ! --mark $ipts_rt_mark -j SETMARK
    iptables -t mangle -A SSTP_PRE -p udp -s $intranet            -m mark ! --mark $ipts_rt_mark -j SETMARK

    # 检查是否需要设置 SNAT/MASQUERADE 规则
    check_snat_rule
done

# 将所有打了标记的 TCP 和 UDP 包透明地转发到代理的监听端口
iptables -t mangle -A SSTP_PRE -m mark --mark $ipts_rt_mark -p tcp -j TPROXY --on-ip 127.0.0.1 --on-port $proxy_tcport
iptables -t mangle -A SSTP_PRE -m mark --mark $ipts_rt_mark -p udp -j TPROXY --on-ip 127.0.0.1 --on-port $proxy_udport

传统模式 - gfwlist 分流

# 导入黑名单 ip 和网段
ipset -N gfwlist hash:net &>/dev/null
perl -ne 'print if m@^\d++\.\d++\.\d++\.\d++(?:/\d++)?$@' $file_gfwlist_ext | xargs -n1 ipset -A gfwlist &>/dev/null

# TCP 使用重定向方式进行代理
iptables -t nat -N TCPCHAIN
iptables -t nat -A TCPCHAIN -p tcp -m set --match-set gfwlist dst -j REDIRECT --to-ports $proxy_tcport

# UDP 使用 TPROXY 方式进行代理
iptables -t mangle -N UDPCHAIN
iptables -t mangle -A UDPCHAIN -d ${dns_remote%:*}            -j MARK --set-mark $ipts_rt_mark
iptables -t mangle -A UDPCHAIN -m set --match-set gfwlist dst -j MARK --set-mark $ipts_rt_mark

# 让本机 TCP 和 UDP 走一下代理链
iptables -t nat    -A SSTP_OUT -p tcp -j TCPCHAIN
iptables -t mangle -A SSTP_OUT -p udp -j UDPCHAIN

# 将本机 dns 请求重定向到 dnsmasq
iptables -t nat    -A SSTP_OUT -p udp -d 127.0.0.1 --dport 53 -j REDIRECT --to-ports 60053

# 遍历内网地址段,处理 TCP 和 UDP
for intranet in "${ipts_intranet[@]}"; do
    # 处理 dns,重定向到 dnsmasq
    iptables -t mangle -A SSTP_PRE -p udp -s $intranet --dport 53 -m mark ! --mark $ipts_rt_mark -j ACCEPT
    iptables -t nat    -A SSTP_PRE -p udp -s $intranet --dport 53 -m mark ! --mark $ipts_rt_mark -j REDIRECT --to-ports 60053

    # 处理 TCP、UDP,走代理链
    iptables -t nat    -A SSTP_PRE -p tcp -s $intranet                                -j TCPCHAIN
    iptables -t mangle -A SSTP_PRE -p udp -s $intranet -m mark ! --mark $ipts_rt_mark -j UDPCHAIN

    # 检查 SNAT 规则
    check_snat_rule
done

# 将打了标记的 UDP 包透明地转发到代理进程的 UDP 监听端口
iptables -t mangle -A SSTP_PRE -m mark --mark $ipts_rt_mark -p udp -j TPROXY --on-ip 127.0.0.1 --on-port $proxy_udport

传统模式 - global/chnroute 分流

# TCP 使用重定向方式进行代理
iptables -t nat -N TCPCHAIN
iptables -t nat -A TCPCHAIN -d 0/8        -j RETURN
iptables -t nat -A TCPCHAIN -d 10/8       -j RETURN
iptables -t nat -A TCPCHAIN -d 127/8      -j RETURN
iptables -t nat -A TCPCHAIN -d 169.254/16 -j RETURN
iptables -t nat -A TCPCHAIN -d 172.16/12  -j RETURN
iptables -t nat -A TCPCHAIN -d 192.168/16 -j RETURN
iptables -t nat -A TCPCHAIN -d 224/4 -j RETURN
iptables -t nat -A TCPCHAIN -d 240/4 -j RETURN
for cidr in "${ipts_intranet_nonstd[@]}"; do
    iptables -t nat -A TCPCHAIN -d $cidr -j RETURN
done
for server in "${proxy_server[@]}"; do
    iptables -t nat -A TCPCHAIN -d $server $([ "$proxy_dports" ] && echo "-p tcp -m multiport --dports $proxy_dports") -j RETURN
done
if [ "$mode" = chnroute ]; then
    ipset -X chnroute &>/dev/null
    ipset -R <$file_chnroute_set
    iptables -t nat -A TCPCHAIN -m set --match-set chnroute dst -j RETURN
fi
iptables -t nat -A TCPCHAIN -p tcp -j REDIRECT --to-ports $proxy_tcport

# UDP 使用 TPROXY 方式进行代理
iptables -t mangle -N UDPCHAIN
iptables -t mangle -A UDPCHAIN -d 0/8        -j RETURN
iptables -t mangle -A UDPCHAIN -d 10/8       -j RETURN
iptables -t mangle -A UDPCHAIN -d 127/8      -j RETURN
iptables -t mangle -A UDPCHAIN -d 169.254/16 -j RETURN
iptables -t mangle -A UDPCHAIN -d 172.16/12  -j RETURN
iptables -t mangle -A UDPCHAIN -d 192.168/16 -j RETURN
iptables -t mangle -A UDPCHAIN -d 224/4 -j RETURN
iptables -t mangle -A UDPCHAIN -d 240/4 -j RETURN
for cidr in "${ipts_intranet_nonstd[@]}"; do
    iptables -t mangle -A UDPCHAIN -d $cidr -j RETURN
done
for server in "${proxy_server[@]}"; do
    iptables -t mangle -A UDPCHAIN -d $server $([ "$proxy_dports" ] && echo "-p udp -m multiport --dports $proxy_dports") -j RETURN
done
if [ "$mode" = chnroute ]; then
    iptables -t mangle -A UDPCHAIN -m set --match-set chnroute dst -j RETURN
fi
iptables -t mangle -A UDPCHAIN -j MARK --set-mark $ipts_rt_mark

# 让本机 TCP 和 UDP 走一下代理链
iptables -t nat    -A SSTP_OUT -p tcp -j TCPCHAIN
iptables -t mangle -A SSTP_OUT -p udp -j UDPCHAIN

# 将本机 dns 请求重定向到 dnsmasq
iptables -t nat    -A SSTP_OUT -p udp -d 127.0.0.1 --dport 53 -j REDIRECT --to-ports 60053

# 遍历内网地址段,处理 TCP 和 UDP
for intranet in "${ipts_intranet[@]}"; do
    # 处理 dns,重定向到 dnsmasq
    iptables -t mangle -A SSTP_PRE -p udp -s $intranet --dport 53 -m mark ! --mark $ipts_rt_mark -j ACCEPT
    iptables -t nat    -A SSTP_PRE -p udp -s $intranet --dport 53 -m mark ! --mark $ipts_rt_mark -j REDIRECT --to-ports 60053

    # 处理 TCP、UDP,走代理链
    iptables -t nat    -A SSTP_PRE -p tcp -s $intranet                                -j TCPCHAIN
    iptables -t mangle -A SSTP_PRE -p udp -s $intranet -m mark ! --mark $ipts_rt_mark -j UDPCHAIN

    # 检查 SNAT 规则
    check_snat_rule
done

# 将打了标记的 UDP 包透明地转发到代理进程的 UDP 监听端口
iptables -t mangle -A SSTP_PRE -m mark --mark $ipts_rt_mark -p udp -j TPROXY --on-ip 127.0.0.1 --on-port $proxy_udport

常见问题

做 代理网关/单臂路由/旁路网关 时,ss-tproxy 主机代理正常,其它主机代理异常
首先排查是不是 ipts_non_snat 选项的问题,默认我设置的是 false,双重否定其实就是肯定,也就是说设置为 false 的话就表示会设置 SNAT 规则,通常情况下,只有你真的需要 SNAT 规则时你才需要将其设置为 false,比如你在网络出口位置使用 ss-tproxy(一头连接内网,一头连接公网),那么就需要设置 SNAT 规则,但其实这种情况下你的系统上早就已经设置好了 SNAT 规则,不然你是怎么上网的,对吧。因此,无论什么情况,都先尝试将 ipts_non_snat 选项设为 true,除非你在设置为 true 的情况下代理有问题,那么你才需要将其设置为 false。另外,这个选项如果你设置过 false,并启动了 ss-tproxy,那么除非你手动清除这个 SNAT 规则或者重启系统(nat 表,POSTROUTING 链),否则系统中还是会存在这个 SNAT 规则的,保险起见,在切换 ipts_non_snat 选项后,先执行一遍 ss-tproxy flush-iptables 命令。

某些系统自带一些 iptables 规则,可能会影响 ss-tproxy 的正常使用
如果你对 iptables 规则不是很了解,请在启动 ss-tproxy 之前将已有的 iptables 规则清空,通常情况下执行命令 ss-tproxy flush-iptables 即可清空,但最好自己再检查一下,因为据反馈有些系统的规则还是无法彻底清除,那么怎么检查呢?执行 flush-iptables 之后,手动执行下面几个命令,查看 raw、mangle、nat、filter 表的规则,如果都是空的那说明就是彻底清空了:

iptables -nL -t raw
iptables -nL -t mangle
iptables -nL -t nat
iptables -nL -t filter

比如 raw 表,默认是这样的,其它的表也类似,只不过预定义规则链不一样而已:

$ iptables -nL -t raw
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

那如果执行了 flush-iptables 后还是有残留规则,该怎么办呢?请自行谷歌。如果系统每次开机都会自动设置一些规则,而且你还不知道如何关闭它,那么也可以在 ss-tproxy.conf 的 pre_start 钩子函数中加入 ss-tproxy flush-iptables 清理命令,如下,这样每次启动 ss-tproxy 都会自动清空规则:

function pre_start {
    ss-tproxy flush-iptables
}

ss-tproxy status 显示不准确(已运行却显示 stopped)
首先检查 ss-tproxy.conf 中的 opts_ss_netstat 选项值是否设置正确,默认是 auto 自选选择模式,即如果有 ss 就优先使用 ss 命令,如果没有才会去使用 netstat 命令;如果自动选择模式对你不适用,那么可以改为 ss 或 netstat 来明确告诉 ss-tproxy 你要使用哪个端口检测命令(有些系统没有 ss,而有些系统却只有 ss);如果确定不是这个选项的问题,那么我估计你遇到了这个问题:执行 ss-tproxy start|restart 后,发现 pxy/tcp 和 pxy/udp 的状态是 stopped 的,然后再次执行 ss-tproxy status 查看状态,却又发现是 running 的,且代理进程也正常启动了,代理也是正常的;这个其实不是 ss-tproxy 的问题,应该是 ss、netstat 的检测延迟问题,因为第一次状态检测(start|restart)是在 proxy_runcmd 运行之后立即执行的,因为某些原因(具体什么原因我也没详细了解),导致 ss/netstat 没检测到对应的监听端口(或者还有一种情况,就是代理进程此时可能还没监听端口,还在处理其它事,如还在处理命令行选项等等,反正都有可能),然后你再次执行 status 命令查看状态时,因为存在一个时间间隔,所以基本上就能正常检测到了。

ss-tproxy 现在是否支持 ipv6 的透明代理(暂时不支持)
ipv6 的支持已经在计划之中,但可能没有这么快,毕竟没有那么多业余时间来写,挺忙的。

特别注意,ss-tproxy v3 要求代理服务器支持 udp 转发
就拿 ss/ssr 来说,ss-redir/ssr-redir 需要开启 udp relay 功能,然后 ss-server/ssr-server 也需要开启 udp relay 功能,如果服务器还有防火墙规则,请注意放行对应的 udp 端口,此外还请确认你当地的 ISP 没有对 udp 流量进行恶意丢包。对于 v2ray(vmess),因为它的 udp 是通过 tcp 转发出去的(即 udp over tcp),所以不需要放行服务器的防火墙端口,也不需要担心 ISP 对 udp 流量的恶意干扰,因为根本没有走 udp,而是走的 tcp。如果你因为各种原因无法使用 udp relay,请考虑使用 v2ray,而且现在网络环境越来越差,ss/ssr 貌似有些顶不住了。

如何只对指定的 host/addr 进行代理,其它的通通走直连
其实非常简单,使用 gfwlist 模式即可;gfwlist 模式会读取 gfwlist.txt、gfwlist.ext 两个黑名单文件,如果你只想代理某些域名、IP、网段,其它的都不想代理,可以直接将 gfwlist.txt 文件清空(执行命令 true >/etc/ss-tproxy/gfwlist.txt),然后编辑 gfwlist.ext 文件,填写要代理的域名、IP、网段即可(文件中有格式说明)。注意,在这种模式下就不要执行 update-chnonlyupdate-gfwlist 命令了,因为它们会操作 gfwlist.txt 文件。

如何将 socks5 代理转换为 ss-tproxy v3 可用的透明代理
如果你因为各种原因无法编译 ss-libev、ssr-libev,但又想使用 ss-tproxy 的透明代理功能,也可以使用 redsocks2 来将任意支持 udp 的 socks5 代理转换为透明代理,这样即使你没有 ss-libev、ssr-libev,也可以使用 ss-tproxy 的透明代理功能(即使用 python 版 ss/ssr + redsocks2 来做透明代理)。当然因为我的系统只安装了 libev 版本,没有安装 python 版本,所以这里依旧使用 ssr-local 作为例子讲解(如果使用 python 版的 ss/ssr,那么你只需要将对应的 ssr-local 替换为 sslocal 命令,但记得开启 udp relay),怎么编译 redsocks2(注意是 redsocks2,不是原版 redsocks)这里就不多说了,官方 readme 有详细说明;我们只需要关注 redsocks2 的配置文件(假设文件路径为 /etc/redsocks2.conf):

base 是 redsocks2 的配置,redsocks 是 tcp 透明代理的配置(REDIRECT),redudp 是 udp 透明代理的配置(TPROXY),ip 和 port 是 socks5 服务器的监听地址,在这里就是 ssr-local 的监听地址,而 local_ip、local_port 则是 redsocks2 进程的监听地址,注意是 0.0.0.0:60080/tcp+udp,端口需要和 ss-tproxy.conf 里面的 proxy_tcport、proxy_udport 相同,监听地址也必须为 0.0.0.0。然后就是配置 ss-tproxy.conf,假设 vps 地址为 1.2.3.4:

然后在 ss-tproxy.conf 的任意位置定义我们的 start_ssrlocal_redsocks2 函数,比如在文件末尾添加:

iptables: No chain/target/match by that name
如果是 iptables -j TPROXY 这条命令报的错(使用 bash -x /usr/local/bin/ss-tproxy start 查看调试信息),那就是没有 TPROXY 模块。

Syntax error: redirection unexpected
如果运行脚本时报了这个错误,你应该检查一下你的 bash 是不是正常版本,或者使用 ls -l /bin/bash 看下这个文件是否软连接到了其它 shell。

ss-tproxy.conf 中的函数不可重复定义
特别注意,因为 ss-tproxy 和 ss-tproxy.conf 都是一个 bash 脚本,所以这两个文件的内容也必须符合 bash 的语法规则,比如你不能在里面重复定义一个函数,虽然这不会报错,但是只有最后一个函数才会生效,这可能会坑死你,如果你定义了多个同名的 bash 函数,请将它们合并为一个!

BT/PT 问题(global/chnroute 模式)
如果你经常使用 BT/PT 下载,请务必使用 gfwlist 模式,如果使用 global/chnroute 模式,可能会消耗大量的 VPS 流量,甚至可能导致 VPS 被封(因为很多主机商都不允许 BT/PT 下载)。因为使用 iptables 来识别 BT/PT 流量的效率太低(基本都是使用 string 模块来匹配,我个人是无法接受的),所以最好的办法还是使用 gfwlist 模式,因为 gfwlist 模式下只有被墙了的网站才会走代理,其它的都是走直连(绝大多数 BT/PT 流量)。

默认的 8.8.8.8:53 DNS 有问题
请关闭 chinadns 的压缩指针选项,即将 ss-tproxy.conf 中的 chinadns_mutation 改为 false(新版已默认关闭该选项),启用压缩指针时,有时候使用 1.1.1.1、8.8.8.8、8.8.4.4 这些国外 DNS 会有问题,无法正常解析 DNS,导致的现象就是国内网站可以上,但是国外网站不能上。

start 时部分组件 stopped
如果是 dnsmasq 或 chinadns 启动失败,请先检查 /var/log 下面的日志,看看是不是监听地址被占用了(按道理来说,v3 最新版本已经将它们两个的监听端口调的很高了,基本没有与之冲突的监听地址);如果是 pxy/tcp 或 pxy/udp 启动失败,请检查 ss-tproxy.conf 里面的 proxy_tcport 和 proxy_udport 端口是否与 proxy_runcmd 启动的进程的监听端口一致,因为默认情况下,ss-redir 或 ssr-redir 的监听端口是 1080,而 ss-tproxy 设置的是 60080,当然这个端口是可以随便改的,但是我觉得还是使用高位端口好一些,省得那么多端口冲突。

切换模式后不能正常代理了
从其它模式切换到 gfwlist 模式时可能出现这个问题,原因还是因为内网主机的 DNS 缓存。在访问被墙网站时,比如 www.google.com,客户机首先会进行 DNS 解析,由于存在 DNS 缓存,这个 DNS 解析请求并不会被 ss-tproxy 主机的 dnsmasq 处理(因为根本没从客户机发出来),所以对应 IP 不会添加到 ipset-gfwlist 列表中,导致客户机发给该 IP 的数据包不会被 ss-tproxy 处理,也就是走直连出去了,GFW 当然不会让它通过了,也就出现了连接被重置等问题。解决方法也很简单,对于 Windows,请先关闭浏览器,然后打开 cmd.exe,执行 ipconfig /flushdns 来清空 DNS 缓存,然后重新打开浏览器,应该正常了;对于 Android,可以先打开飞行模式,然后再关闭飞行模式,或许可以清空 DNS 缓存。

有时会无法访问代理服务器
如果你在 ss-tproxy 中使用的是自己的 VPS 的代理服务,那么在除 ss-tproxy 主机外的其他主机上会可能会出现无法访问这台 VPS 的情况(比如 SSH 连不上,但是 ping 没问题),具体表现为连接超时。起初怀疑是 ss-tproxy 主机的 iptables 规则设置不正确,然而使用 TRACE 追踪后却什么都没发现,一切正常;在 VPS 上使用 tcpdump 抓包后,发现一个很奇怪的问题:VPS 在收到来自客户端的 SYN 请求后并没有进行 SYN+ACK 回复,客户端在尝试了几次后就会显示连接超时。于是怀疑是不是 VPS 的问题,谷歌之后才知道,原来是因为两个内核参数设置不正确导致的,这两个内核参数是:net.ipv4.tcp_tw_reusenet.ipv4.tcp_tw_recycle,将它们都设为 0(也就是禁用)即可解决此问题。其实这两个内核参数默认都是为 0 的,也就是说,只要你没动过 VPS 的内核参数,那么基本不会出现这种诡异的问题。

关于网关的 DHCP 配置细节
这里说的 DHCP 配置仅针对“代理网关”模式,其实如何配置 DHCP 完全取决于你自己的需求,第一种:将路由器 DHCP 分配的 Gateway 和 DNS 改为 ss-tproxy 主机的地址,然后给 ss-tproxy 主机配置静态 IP,注意不能使用 DHCP 来获取,因为获取到的 Gateway 是指向自己的,显然有问题,这种方式下,所有内网主机默认通过 ss-tproxy 代理上网,如果某些主机不想走代理,可以手动将这些主机的 Gateway 和 DNS 改为原来的,就可以恢复直连了,不会走代理。第二种:不改变任何 DHCP 配置,默认情况下除 ss-tproxy 主机本身外,其它主机都是走直连的,如果你想让某些主机走代理上网,可以手动将这些主机的 Gateway 和 DNS 改为 ss-tproxy 主机的 IP,这样就可以走代理上网了。当然如果你的路由器支持为不同的主机设置不同的 Gateway 和 DNS,那么也可以不修改任何内网主机的配置,直接在路由上配置对应的 DHCP 规则就行了。

ss-tproxy 支持什么发行版
一般都支持,没有限制,因为没有使用与特定发行版相关的命令和特性(我自己用的是 ArchLinux),当然我说的是普通 x86、arm 发行版,如果是路由器的那种系统,应该会有点问题,但是移植起来应该不难。我测试过的系统有:ArchLinux、RHEL、CentOS、Debian、Alpine;经过几个网友测试,OpenWrt 貌似也可以,当然需要自己改一些东西,比如 OpenWrt 自带的 bash 是有问题的,阉割版,运行起来会报错。

ss-tproxy 只能运行在网关上吗
显然不是,你可以在一台普通的 linux 主机(甚至是桥接模式下的虚拟机)上运行,并且这种方式也是能够透明代理其它主机的 TCP 和 UDP 的哦。

ss-tproxy 可以运行在副路由上吗
可以。假设你有两个路由器,一主一副,主路由通过 PPPOE 拨号上网,其它设备连接到主路由可以上外网(无科学上网),副路由的 WAN 口连接到主路由的 LAN 口,副路由的 WAN 网卡 IP 可以动态获取,也可以静态分配,此时,副路由自己也是能够上外网的。然后,在副路由上运行 ss-tproxy,此时,副路由已经能够科学上网了,然后,我们在副路由上配置一个 LAN 网段(不要与主路由的 LAN 网段一样),假设主路由的 LAN 网段是 192.168.1.0/24,副路由的 LAN 网段是 10.10.10.0/24。然后指定一个网关 IP 给副路由的 LAN 口,假设为 10.10.10.1,开启副路由上的 DHCP,分配的地址范围为 10.10.10.100-200,分配的网关地址为 10.10.10.1,分配的 DNS 服务器为 10.10.10.1。现在,修改 ss-tproxy.conf 的内网网段为 10.10.10.0/24,重启 ss-tproxy,然后连接到副路由的设备应该是能够科学上网的。你可能会问,为什么不直接在主路由上安装 ss-tproxy 呢?假设这里的副路由是一个树莓派,那么我不管在什么网络下(公司、酒店),只要将树莓派插上,然后我的设备(手机、笔记本)只需要连接树莓派就能无缝上网了,同时又不会影响内网中的其它用户,一举两得(当然我只是举个栗子,实际上可能没想的这么方便)。

ss-tproxy 可以运行在内网主机上吗(代理网关)
可以。先解释一下这里的“代理网关”(不知道叫什么好),由网友 @feiyu 启发。他的意思是,将 ss-tproxy 部署在一台普通的内网主机上(该主机的网络配置不变),然后将其他内网主机的网关和 DNS 指向这台部署了 ss-tproxy 的主机,进行代理。方案是可行的,我在 VMware 环境中测试通过。注意,这个“代理网关”可以是一台真机,也可以是一台虚拟机(桥接模式),比如在 Windows 系统中运行一个 VMware 或 VirtualBox 的 Linux 虚拟机,在这个虚拟机上跑 ss-tproxy 当然也是可以的(你还别说,真有不少人是这样用的,我在公司也是这么用的)。

切换模式、切换节点不方便,能否避免直接修改文件
这确实是个问题,切换节点需要修改配置文件,切换模式需要修改配置文件,有没有更加简便一些的方式?抱歉,没有。不过因为 ss-tproxy.conf 是一个 shell 脚本,所以我们可以在这里面做些文章。如果你有很多个节点(付费机场一般都是,当然 ss-tproxy 可能更适合自建代理的用户),可以这样做:

配置文件以及其它文件的路径是否可更改
当然是可以的,比如你想将 /etc/ss-tproxy 默认目录改为 /opt/ss-tproxy,只需要修改两个地方,一个是 ss-tproxy 脚本的第 3 行,将 main_conf 变量改为 /opt/ss-tproxy/ss-tproxy.conf;另一个是 /opt/ss-tproxy/ss-tproxy.conf 里面的 file_* 变量,改为 /opt/ss-tproxy 目录下的就行了。顺便说一句,ss-tproxy 脚本本身也并不是说一定要放到 /usr/local/bin 目录下,只是我个人喜欢将第三方命令放到这个目录而已。

CentOS 7.x 必须关闭 firewalld 防火墙
当然 RHEL 7.x 也一样,因为 iptables 和 firewalld 都是 netfilter 的用户空间配置工具,而默认情况下,RHEL/CentOS 7.x 会将 firewalld 服务设置为开机自启动,而且还会设置一些防火墙规则,如果你不关闭这个服务,可能在做代理网关时,会遇到无法上网的情况,所以请务必执行 systemctl disable firewalld 来关闭它(然后重启系统)。当然也不是说一定要关闭 firewalld,如果你对 iptables 和 firewalld 比较熟悉,完全可以自由支配它们。

为什么必须使用 bash,而不能使用 sh、zsh 这些 shell
因为 ss-tproxy 脚本里面用到了非常多的 bash 高级重定向,而且有些 built-in 命令和 sh、zsh 的用法不同,无法兼容。

为什么使用 ipip.net 的 chnroute 源,而不是 APNIC 的源
这个怎么说呢,ipip.net 的源我觉得挺好的,用了很久也没啥问题,当然如果需要,你也可以自己改为其它的 chnroute 源,比如改为 APNIC 的大陆地址段列表。但是 ss-tproxy.conf 中并未提供这个选项,该怎么改呢?其实不难,你不需要去修改脚本,只需要在 ss-tproxy.conf 文件末尾添加以下内容(基本框架可以照抄,具体的更新命令可以自定义):

其实你可以完全模仿这个例子,来重写 ss-tproxy 中定义的全部函数,并且还可以添加额外的 ss-tproxy COMMAND,就看你怎么写了。

如何提高代理的性能(当然仅针对 ss-redir、ssr-redir 这些)
首先,优先选择 C 语言版的 SS/SSR,当然 Python 版也没问题,然后服务器的监听端口我个人觉得 80 和 443 比较好一点,8080 也可以,貌似这些常用端口可以减少 QoS 的影响,当然这只是我个人的一些意见,然后就是修改内核参数,ss-tproxy 主机的 sysctl.conf 以及 vps 的 sysctl.conf 都建议修改一下,最好同步设置。另外我还有一个提速小技巧,那就是在 ss-tproxy 中启动多个 ss-redir、ssr-redir,它们都监听同一个地址和端口(是的你没听错),对于 ss-redir,需要添加一个 --reuse-port 选项来启用端口重用,而对于 ssr-redir,默认情况下就启用了端口重用,而且也没有这个选项。为了简单,这里就以 ss-redir 为例,老规矩,修改 ss-tproxy.conf,添加一个函数,用来启动 N 个 ss-redir 进程:

然后修改我们的启动命令,即 proxy_runcmd,将它改为 proxy_runcmd='start_multiple_ssredir 4',其中 4 可以改为任意正整数,这个数值的意思是启动多少个 ss-redir 进程,一般建议最多启动 CPU 核心数个 ss-redir 进程,太多了性能反而会下降,当然你也可以将它改为 1,此时只会启动一个进程,也就没有所谓的加速了(多个 ss-redir 进程的加速仅针对多线程下载、多终端并发访问的情况,话虽如此,但是效果还是很明显的)。

ss-libev、ssr-libev 如何进行多节点负载均衡(SO_REUSEPORT)
其实非常简单,和上面的多进程 ss-libev、ssr-libev 加速差不多,只不过 ss_addr 不同而已,反正只要这些 ss-redir、ssr-redir 进程监听同一个 addr:port(127.0.0.1:60080),就可以正常使用,实际上 ss-tproxy 并不关心你使用的是哪个 proxy_server,也不关心你使用的是 ss-libev 还是 ssr-libev 还是 v2ray。有必要声明是,通过 SO_REUSEPORT 端口重用实现的负载均衡是“平均分配”的,假设有 4 个进程同时监听 127.0.0.1:60080,那么内核会将客户端连接平均分配给这 4 个进程(每个进程分配到的概率为 25%)。例子:假设我有 2 个 ss 服务器(1.1.1.1、2.2.2.2),2 个 ssr 服务器(3.3.3.3、4.4.4.4),我想同时使用这 4 个服务器(负载均衡),该如何做?

使用“代理网关”模式时,ss-tproxy stop 后,内网主机无法上网
如果 ss-tproxy 主机上没有运行 DNS 服务器(注意,如果代理网关上已经有一个运行在 53 端口上的 dns 服务器,就不要再执行这里的操作了,有些人看都不没看清就直接照抄过去),那就会出现这个问题,因为你的 DNS 设为了 ss-tproxy 主机的 IP,而你执行 stop 操作后,ss-tproxy 上的 dnsmasq 就会被 kill(注意 kill 的是 ss-tproxy 运行的那个 dnsmasq 进程,系统运行的或者你自己运行的 dnsmasq 进程不会被 kill),使得内网主机无法解析 DNS,从而无法上网。解决方法也很简单,修改 ss-tproxy.conf,添加两个钩子函数,post_stop 钩子函数的作用是在 stop 之后启动一个 dnsmasq,监听 0.0.0.0:53 地址,给内网主机提供普通的 DNS 服务,pre_start 钩子函数的作用是在 start 之前 kill 这个 53 端口的 dnsmasq,因为 ss-tproxy 代理启动后,这个 dnsmasq 进程就没有存在的必要了。

为什么使用 ping 命令来解析域名?而不是 dig、nslookup、getent
起初我是使用 getent hosts $domain_name 来解析域名的,但是后来我发现在某些系统上没有 getent 命令,所以我就改为了 ping 来解析。

为什么不支持自定义 dnsmasq 和 chinadns 的端口,只能使用默认的
因为没有这个必要,默认情况下,dnsmasq 监听 60053 端口,chinadns 监听 65353 端口,基本上没哪个进程会监听这两个高位端口,我之所以设置为高位端口也是为了尽可能避免端口冲突问题,在早期版本中,dnsmasq 是监听在 53 端口的,但是我收到了很多关于 dnsmasq 监听端口冲突的反馈,虽然端口冲突问题不难解决,但是我为了一劳永逸,直接将这个端口改为了 60053,从根本上避免了这个问题。

为什么 gfwlist 模式需要用到 perl?而不是使用 sed、awk、grep 这些
因为 Perl 的正则表达式真的很强大,而且没有所谓的兼容性问题,sed、awk、grep 基本上都有兼容性问题,太烦了。

支持 ss-libev 或 ssr-libev 与 kcptun 一起使用吗
可以,当然需要进行一些特殊改造。ss-libev + kcptun 和 ssr-libev + kcptun 操作起来都差不多,为了简单,这里就以 ss-libev 为例。首先我假设你已经在 vps 上运行了 ss-server 和 kcptun-server,并且 ss-server 监听 0.0.0.0:8053/tcp+udp(监听 0.0.0.0 是为了处理 ss-redir 的 udp relay,因为 kcptun 只支持 tcp 协议的加速),kcptun-server 监听 0.0.0.0:8080/udp(用来加速 ss-redir 的 tcp relay,会被封装为 kcp 协议,使用 udp 传输);当然这些端口都是可以自定义的,我并没有规定一定要使用这两个端口!然后编辑 ss-tproxy.conf,修改这几条配置(假设 vps 地址为 1.2.3.4):

注意,kcptun 的 client 和 server 程序默认并不是叫做 kcptun-clientkcptun-server,而是一个很难听的名字,如 client_linux_amd64server_linux_amd64,我为了好记,将它改为了 kcptun-client、kcptun-server;如果你没有改这个名字,那么你就需要修改一下上面的 kcptun-client 为对应的 client 二进制文件名,但是我建议你改一下,可以省去不少麻烦。然后你可能注意到了 start_sslibev_kcptun 这个命令,这实际上是我们待会要定义的一个函数,方便启动 ss-redir 和 kcptun-client,而不用在 proxy_runcmd 中写很长的启动命令;然后,在 ss-tproxy.conf 的任意位置添加以下配置(我个人喜欢在文件末尾添加),假设 method 为 aes-128-gcm,password 为 passwd.for.test:

延伸:除了 ss、ssr、v2ray,ss-tproxy 还支持其它的吗
首先你要清楚一点:ss-tproxy 并不关心你使用什么代理,它关心的仅仅是 proxy_tcportproxy_udportproxy_serverproxy_dports

  • proxy_tcport:用来接收 TCP 透明代理数据包的端口(REDIRECT/TPROXY),随便填,只要这个端口能处理 TCP 透明代理数据包
  • proxy_udport:用来接收 UDP 透明代理数据包的端口(TPROXY),随便填,只要这个端口能处理 UDP 透明代理数据包
  • proxy_server:告诉 ss-tproxy,哪些目标地址需要放行,显然,与代理有关的目标 IP 必须填上,如 VPS 的 IP/域名
  • proxy_dports:告诉 ss-tproxy,proxy_server 上的哪些端口需要放行,默认是 proxy_server 上的全部端口都放行

如果还不明白,我用通俗的语言来解释这 4 个核心选项的作用:我们将 ss-tproxy 主机看作一根水管,一头是入口(数据包都从这里进去),一头是出口(数据包都从这里出去),里面流的当前就是水了(数据包),那么 proxy_tcport 和 proxy_udport 就是告诉 ss-tproxy,你要将数据包送到哪里去(当然这里的数据包是指需要代理的数据包,放行的流量是不会被送到这个入口的),而 proxy_server 和 proxy_dports 就是告诉 ss-tproxy,这些数据包经过处理后会从哪里出来(也即目标地址、目标端口);管子内部随便你怎么接,你只要能保证数据包能流通,ss-tproxy 就会正确的配置好代理和分流。

这里的管子接法其实就是指底层代理的组合方式,管你是 ss-redir + kcptun,还是 ssr-redir + udp2raw,你只要能保证两头没问题,就可以使用。

为什么 REDIRECT + TPROXY 模式的代理需要监听 0.0.0.0
我在 README 里面强调过,ss-redir 和 ssr-redir 的监听地址需要指定为 0.0.0.0(当然其它代理软件也一样,如果是 REDIRECT + TPROXY 组合方式的话),如果你不指定这个监听地址,或者指定为监听 127.0.0.1,那么你会发现内网主机是无法正常代理上网的,为什么呢?据我个人猜测,应该是 iptables 的 REDIRECT 的问题(听说改为 DNAT 可以避免这个问题,但经过验证貌似也是有问题的),具体什么原因我也不想深究了,没多大意义。

如何在 ss-tproxy 中集成 koolproxy 等广告过滤软件
adbyby 和 koolproxy 是路由固件中常见的两个广告过滤插件,因为 adbyby 不支持 https 过滤,所以这里就以 koolproxy 作为例子讲解。首先打开 https://firmware.koolshare.cn/binary/KoolProxy/,下载对应平台的 koolproxy 二进制文件,然后将它放到 /opt/koolproxy 目录(当然放哪里没要求),并命名为 koolproxy,然后加上可执行权限;进入 /opt/koolproxy 目录,执行 ./koolproxy --cert 生成 koolproxy HTTPS 证书等文件;因为待会我们需要用一个非 root 用户来运行 koolproxy 进程(可以从 /etc/passwd 中随便找一个已存在的用户,比如 daemon),所以我们要先将 /opt/koolproxy 目录的所有者改为 daemon,不然 koolproxy 自动更新广告过滤规则时会遇到权限问题,即执行命令 chown -R daemon:daemon /opt/koolproxy;最后编辑 ss-tproxy.conf,在文件末尾添加这两个钩子函数,就可以实现 koolproxy + ss-tproxy 组合方式的 广告过滤 + 透明代理(广告过滤或多或少都会影响性能,如果设备性能不太好,不建议集成 koolproxy 等广告过滤软件,这种情况下在客户端进行广告过滤会好一些):