ss/ssr/v2ray/socks5 透明代理

先说 ss/ssr 透明代理吧,ss-redir 是 ss-libevssr-libev 中的一个工具,配合 iptables 可以在 Linux 上实现 ss、ssr 透明代理,ss-redir 的 TCP 透明代理是通过 REDIRECT 方式实现的,而 UDP 透明代理是通过 TPROXY 方式实现的。强调一点,利用 ss-redir 实现透明代理必须使用 ss-libev 或 ssr-libev,python、go 等版本没有 ss-redir、ss-tunnel 程序。
当然,ss、ssr 透明代理并不是只能用 ss-redir 来实现,使用 ss-local + redsocks2/tun2socks 同样可以实现 socks5(ss-local 是 socks5 服务器)全局透明代理;ss-local + redsocks2 实际上是 ss-redir 的分体实现,TCP 使用 REDIRECT 方式,UDP 使用 TPROXY 方式;ss-local + tun2socks 则相当于 Android 版 SS/SSR 的 VPN 模式,因为它实际上是通过一张虚拟的 tun 网卡来进行代理的。
最后说一下 v2ray 的透明代理,其实原理和 ss/ssr-libev 一样,v2ray 可以看作是 ss-local、ss-redir、ss-tunnel、ss-server 四者的合体,因为同一个 v2ray 程序既可以作为 server 端,也可以作为 client 端。所以 v2ray 的透明代理也有两种实现方式,一是利用对应的 ss-redir + iptables,二是利用对应的 ss-local + redsocks2/tun2socks(redsocks2/tun2socks 可以与任意 socks5 代理组合,实现透明代理)。

组件区别

ss-server

shadowsocks 服务端程序,核心部件之一,各大版本均提供 ss-server 程序。

ss-local

shadowsocks 客户端程序,核心部件之一,各大版本均提供 ss-local 程序。
ss-local 是运行在本地的 socks5 代理服务器,根据 OSI 模型,socks5 是会话层协议,支持 TCP 和 UDP 的代理。

但是现在只有少数软件直接支持 socks5 代理协议,绝大多数都只支持 http 代理协议。好在我们可以利用 privoxy 将 socks5 代理转换为 http 代理,使用 privoxy 还有一个好处,那就是可以实现 gfwlist 分流模式(不过现在的 ss-tproxy 脚本也可以了),如果你对它感兴趣,可以看看 ss-local 终端代理

ss-redir

shadowsocks-libev 提供的socks5 透明代理工具,也就是今天这篇文章的主题 - 实现透明代理!

正向代理
正向代理,即平常我们所说的代理,比如 http 代理、socks5 代理等,都属于正向代理。
正向代理的特点就是:如果需要使用正向代理访问互联网,就必须在客户端进行相应的代理设置

透明代理
透明代理和正向代理的作用是一样的,都是为了突破某些网络限制,访问网络资源。
但是透明代理对于客户端是透明的,客户端不需要进行相应的代理设置,就能使用透明代理访问互联网

反向代理
当然,这个不在本文的讨论范畴之内,不过既然提到了前两种代理,就顺便说说反向代理。
反向代理是针对服务端来说的,它的目的不是为了让我们突破互联网限制,而是为了实现负载均衡。

举个栗子:
ss-local 提供 socks5 正向代理,要让软件使用该代理,必须对软件进行相应的代理配置,否则不会走代理;
ss-redir 提供 socks5 透明代理,配置合适网络规则后,软件会在不知情的情况下走代理,不需要额外配置。

ss-tunnel

shadowsocks-libev 提供的本地端口转发工具,通常用于解决 dns 污染问题。

假设 ss-tunnel 监听本地端口 53,转发的远程目的地为 8.8.8.8:53;系统 dns 为 127.0.0.1。
去程:上层应用请求 dns 解析 -> ss-tunnel 接收 -> ss 隧道 -> ss-server 接收 -> 8.8.8.8:53;
回程:8.8.8.8:53 响应 dns 请求 -> ss-server 接收 -> ss 隧道 -> ss-tunnel 接收 -> 上层应用。

方案说明

用过 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、绕过大陆地址段这些分流模式可选择),这样只要设备连到 WiFi 就能直接无缝上网,完全感觉不到“墙”的存在。如果忽略分流模式(即全部流量都走代理出去),那么实现是很简单的(几条 iptables 就可以搞定,但是这太简单粗暴了,很多国内网站走代理会非常慢,体验很不好);但是如果要自己实现 gfwlist、绕过大陆地址段这些模式,恐怕很多人都会望而却步,因为确实复杂了一些。但这种透明代理的模式的确很诱人,毕竟只要设置一次就可以让所有内网设备上 Internet,于是我开始摸索如何在 Linux 中实现类似 OpenWrt 的代理模式,而我摸索出来的成果就是 ss-tproxy 透明代理脚本。值得说明一下,ss-tproxy 可以部署在 Linux 软路由(网关)、Linux 物理机、Linux 虚拟机等环境中;对于 Linux 软路由中的 ss-tproxy,可以用来代理网关本身以及内网主机的 TCP/UDP 流量;对于 Linux 物理机/虚拟机 中的 ss-tproxy,可以用来代理主机本身以及所有网关指向该主机的其它主机的 TCP/UDP 流量(其实就是文末的 代理网关)。

安装依赖

ss-tproxy v3 的依赖列表

  • global 模式:iproute2、TPROXY、dnsmasq
  • gfwlist 模式:iproute2、TPROXY、dnsmasq、perl、ipset
  • chnroute 模式:iproute2、TPROXY、dnsmasq、chinadns、ipset

ss-tproxy v2 的依赖列表

  • v2ray_global: v2ray-core, xt_TPROXY, iproute2, dnsmasq
  • v2ray_gfwlist: v2ray-core, xt_TPROXY, iproute2, ipset, perl, dnsmasq
  • v2ray_chnroute: v2ray-core, xt_TPROXY, iproute2, ipset, chinadns, dnsmasq
  • tlspxy_global: 代理软件, xt_TPROXY, iproute2, dnsmasq
  • tlspxy_gfwlist: 代理软件, xt_TPROXY, iproute2, ipset, perl, dnsmasq
  • tlspxy_chnroute: 代理软件, xt_TPROXY, iproute2, ipset, chinadns, dnsmasq
  • tproxy_global: ss/ssr-libev, haveged, xt_TPROXY, iproute2, dnsmasq
  • tproxy_gfwlist: ss/ssr-libev, haveged, xt_TPROXY, iproute2, ipset, perl, dnsmasq
  • tproxy_chnroute: ss/ssr-libev, haveged, xt_TPROXY, iproute2, ipset, chinadns, dnsmasq
  • tproxy_global_tcp: ss/ssr-libev, haveged, dnsforwarder
  • tproxy_gfwlist_tcp: ss/ssr-libev, haveged, ipset, perl, dnsmasq, dnsforwarder
  • tproxy_chnroute_tcp: ss/ssr-libev, haveged, ipset, chinadns, dnsforwarder
  • tun2socks_global: socks5 代理, tun2socks, iproute2, dnsmasq
  • tun2socks_gfwlist: socks5 代理, tun2socks, iproute2, ipset, perl, dnsmasq
  • tun2socks_chnroute: socks5 代理, tun2socks, iproute2, ipset, chinadns, dnsmasq
  • tun2socks_global_tcp: socks5 代理, tun2socks, iproute2, dnsforwarder
  • tun2socks_gfwlist_tcp: socks5 代理, tun2socks, iproute2, ipset, perl, dnsmasq, dnsforwarder
  • tun2socks_chnroute_tcp: socks5 代理, tun2socks, iproute2, ipset, chinadns, dnsforwarder
  • haveged 依赖项是可选的,主要用于防止系统的熵过低,从而导致 ss-redir、ss-tunnel、ss-local 启动失败等问题
  • *gfwlist* 模式更新列表时依赖 curl、base64;*chnroute* 模式更新列表时依赖 curl;建议安装,以备不时之需
  • *gfwlist* 模式中的 perl 其实可以使用 sed 替代,但由于更新 gfwlist 列表依赖 perl5 v5.10.0+,所以直接用了 perl

curl

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

perl5

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

ipset

TPROXY

TPROXY 是一个 Linux 内核模块,在 Linux 2.6.28 后进入官方内核。一般正常的发行版都没有裁剪 TPROXY 模块,TPROXY 模块缺失问题主要出现在无线路由固件上。使用以下方法可以检测当前内核是否包含 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 为例,毕竟另一个源我没试过,但是这个源我自己用了大半年,没有任何问题,很稳定

tun2socks

请选择与 CPU 相对应的二进制程序,下载后请去掉后缀名,放到 /usr/local/bin,添加可执行权限。

代理脚本

这里只介绍 ss-tproxy v3 版本,如果需要使用 v2 版本,请转到 ss-tproxy v2 分支

脚本简介

ss-tproxy 目前存在 3 个版本,分别为 v1、v2、v3。最初编写 ss-tproxy 脚本的目的很简单,就是为了透明代理 ss/ssr,这也是脚本名称的由来。在 v1 版本中,ss-tproxy 只实现了 chnroute 大陆地址分流模式,因为我这边的网络即使访问普通的国外网站也很慢,所以干脆将国外网站都走代理。随着 ss-tproxy 被 star 的数量越来越多,促使我编写了 v2 版本,v2 版本先后实现了 global、gfwlist、chnonly、chnroute 四种分流模式,并且合并了 ss-tun2socks,支持 ss/ssr/v2ray/socks5 的透明代理。但因脚本结构问题,导致 v2 版本的代码行数达到了 1300+,使得脚本过于臃肿且难以维护,最终催生了现在的 v3 版本。

ss-tproxy v3 基本上可以认为是 ss-tproxy v2 的精简优化版,v3 版本去掉了很多不是那么常用的代理模式,如 tun2socks、tcponly,并提取出了 ss/ssr/v2ray 等代理软件的相同规则,所以 v3 版本目前只有两大代理模式:REDIRECT + TPROXY、TPROXY + TPROXY(纯 TPROXY 方式)。REDIRECT + TPROXY 是指 TCP 使用 REDIRECT 方式代理而 UDP 使用 TPROXY 方式代理;纯 TPROXY 方式则是指 TCP 和 UDP 均使用 TPROXY 方式代理。目前来说,ss-libev、ssr-libev、v2ray-core、redsocks2 均为 REDIRECT + TPROXY 组合方式,而最新版 v2ray-core 则支持纯 TPROXY 方式的代理。在 v3 中,究竟使用哪种组合是由 proxy_tproxy='boolean_value' 决定的,如果为 true 则为纯 TPROXY 模式,否则为 REDIRECT + TPROXY 模式(默认)。

v3 版本仍然实现了 global、gfwlist、chnonly、chnroute 四种分流模式;global 是指全部流量都走代理;gfwlist 是指 gfwlist.txt 与 gfwlist.ext 列表中的地址走代理,其余走直连;chnonly 本质与 gfwlist 没区别,只是 gfwlist.txt 与 gfwlist.ext 列表中的域名为大陆域名,所以 chnonly 是国外翻回国内的专用模式;chnroute 则是从 v1 版本开始就有的模式,chnroute 模式会放行特殊地址、国内地址的流量,然后其它的流量(发往国外的流量)都会走代理出去(默认)。

如果你需要使用 tun2socks 模式(socks5 透明代理)、tcponly 模式(仅代理 TCP 流量),请转到 ss-tproxy v2 版本。关于 tcponly 模式,可能以后会在 v3 版本中加上,但目前暂时不考虑。而对于 socks5 透明代理,我不是很建议使用 tun2socks,因为 tun2socks 是 golang 写的一个程序,在树莓派上性能堪忧(v2ray 也是如此),即使你认为性能可以接受,我还是建议你使用 redsocks2 来配合 v3 脚本的 REDIRECT + TPROXY 模式(当然如果你的 socks5 代理仅支持 TCP,那么目前还是只能用 v2 的 tun2socks 模式,直到 v3 的 tcponly 模式上线)。使用 redsocks2 配合 REDIRECT + TPROXY 模式很简单,配置好 redsocks2 之后,在 ss-tproxy.conf 的 runcmd 中填写 redsocks2 和 socks5 代理的启动命令就行。

ss-tproxy 可以运行在 Linux 软路由/网关、Linux 物理机、Linux 虚拟机等环境中,可以透明代理 ss-tproxy 主机本身以及所有网关指向 ss-tproxy 主机的其它主机的 TCP 与 UDP 流量。透明代理主机本身的 TCP 和 UDP 没什么好讲的,我主要说一下透明代理”其它主机”的 TCP 和 UDP 的流量。即使 ss-tproxy 不是运行在 Linux 软路由/网关上,但通过某些”技巧”,ss-tproxy 依旧能够透明代理其它主机的 TCP 与 UDP 流量。比如你在某台内网主机(假设 IP 地址为 192.168.0.100)中运行 ss-tproxy,那么你只要将该内网中的其它主机的网关以及 DNS 服务器设为 192.168.0.100,那么这些内网主机的 TCP 和 UDP 就会被透明代理。当然这台内网主机可以是一个 Linux 虚拟机(网络要设为桥接模式,通常只需一张网卡),假设这台虚拟机的 IP 为 192.168.0.200,虚拟机能够与内网中的其它主机正常通信,也能够正常上外网,那么你只需将内网中的其它主机的网关和 DNS 设为 192.168.0.200 就可以透明代理它们的 TCP 与 UDP 流量。

如果你不是在 Linux 软路由/网关上运行 ss-tproxy(这里说的“软路由/网关”就是普通意义上的“路由器”,也就是说 ss-tproxy 主机至少有两张网卡,一张网卡连接外网,一张网卡连接内网),那么请将 ss-tproxy.conf 里面的 ipts_non_snat 改为 true,否则路由上设置的端口映射规则将无法正常工作(当然,这个选项对于 global 分流模式没有效果,这个以后再说吧,毕竟也很少人使用 global 分流模式)。

脚本依赖

端口占用

  • global 模式:dnsmasq:53@tcp+udp
  • gfwlist 模式:dnsmasq:53@tcp+udp
  • chnroute 模式:dnsmasq:53@tcp+udp、chinadns:65353@udp

脚本用法

安装

删除

简介

  • ss-tproxy:脚本文件
  • ss-tproxy.conf:配置文件
  • ss-tproxy.service:服务文件
  • gfwlist.txt:gfwlist 域名文件,不可配置
  • gfwlist.ext:gfwlsit 黑名单文件,可配置
  • chnroute.set:chnroute for ipset,不可配置
  • chnroute.txt:chnroute for chinadns,不可配置

配置

  • 脚本配置文件为 /etc/ss-tproxy/ss-tproxy.conf,修改后重启脚本才能生效
  • 默认分流模式为 chnroute,这也是 v1 版本中的分流模式,根据你的需要修改
  • 根据实际情况,修改 proxy 配置段中的代理软件的信息,详细内容见下面的说明
  • dns_remote 为远程 DNS 服务器(走代理),默认为 Google DNS,根据需要修改
  • dns_direct 为直连 DNS 服务器(走直连),默认为 114 公共DNS,根据需要修改
  • iptables_intranet 为要代理的内网的网段,默认为 192.168.0.0/16,根据需要修改
  • 如需配置 gfwlist 扩展列表,请编辑 /etc/ss-tproxy/gfwlist.ext,然后重启脚本生效

proxy 配置段中的 proxy_server 是代理服务器的地址,可以填域名也可以填 IP,不作要求(但并不是说这个选项就能随便写,你必须保证 proxy_server 的地址与 proxy_runcmd 里面的服务器地址保持一致,否则 iptables 规则将会出现死循环);proxy_runcmd 是用来启动代理软件的命令,此命令不可以占用前台(意思是说这个命令必须能够立即返回),否则 ss-tproxy start 将被阻塞;proxy_kilcmd 是用来停止代理软件的命令。常见的写法有:

如果还是没看懂,我再举几个具体的例子:

对于 ss-libev、ssr-libev,也可以将相关配置信息写入 json 文件,然后使用选项 -c /path/to/config.json 来运行。
特别注意,ss-redir、ssr-redir 的监听地址必须要设置为 0.0.0.0(即 -b 0.0.0.0),不能为 127.0.0.1,也不能省略。

如果使用 v2ray(只介绍 REDIRECT + TPROXY 方式),你必须配置 v2ray 客户端的 dokodemo-door 传入协议,如:

如果使用 chnonly 模式(国外翻进国内),请选择 gfwlist mode,chnonly 模式下,你必须修改 ss-tproxy.conf 中的 dns_remote 为国内的 DNS,如 dns_remote='114.114.114.114:53',并将 dns_direct 改为本地 DNS(国外的),如 dns_direct='8.8.8.8';因为 chnonly 模式与 gfwlist 模式共享 gfwlist.txt、gfwlist.ext 文件,所以在第一次使用时你必须先运行 ss-tproxy update-chnonly 将默认的 gfwlist.txt 内容替换为大陆域名(更新列表时,也应使用 ss-tproxy update-chnonly),并且注释掉 gfwlist.ext 中的 Telegram IP 段,因为这是为正常翻墙设置的。要恢复 gfwlist 模式的话,请进行相反的步骤。

dns_modify='boolean_value':如果值为 false(默认),则 ss-tproxy 在修改 /etc/resolv.conf 文件时,会采用 mount -o bind 方式(不直接修改原文件,而是“覆盖”它,在 stop 之后会自动恢复为原文件);如果值为 true,则直接使用 I/O 重定向来修改 /etc/resolv.conf 文件。一般情况下保持默认就行,但某些时候将其设为 true 可能会好一些(具体什么时候,我也不太好讲,需要具体情况具体分析)。

端口映射
前面提到,如果 ss-tproxy 运行在“代理网关”,最好将 ipts_non_snat 设为 true,否则端口映射必定失败(好吧,即使将其设为 true,在某些情况下端口映射依旧会失败)。我们先来简要分析一下,为什么设为 false 会导致端口映射失败。假设拨号网关为 192.168.1.1,代理网关为 192.168.1.2,内网主机为 192.168.1.100;在拨号网关上设置端口映射规则,将外网端口 8443 映射到内网主机 192.168.1.100 的 8443 端口;在代理网关上运行 ss-tproxy(假定分流模式为 gfwlist),然后将内网主机 192.168.1.100 的网关和 DNS 设为 192.168.1.2;此时代理网关以及内网主机均可透过代理来上网。

在内网主机 192.168.1.100 上运行端口为 8443 的服务进程,然后我们从其它外网主机(假设 IP 为 2.2.2.2)连接此端口上的服务。首先,外网主机向拨号网关的 8443 端口发起连接(假设 IP 为 1.1.1.1),即 2.2.2.2:2333 -> 1.1.1.1:8443,然后拨号网关查询到对应的端口映射规则,于是做 DNAT 转换,变为 2.2.2.2:2333 -> 192.168.1.100:8443,然后通过内网网卡送到了 192.168.1.100 主机的 8443 端口(SYN 握手请求成功到达);然后服务进程会发送 SYN+ACK 握手响应包,即 192.168.1.100:8443 -> 2.2.2.2:2333,因为内网主机的网关为 192.168.1.2,所以 SYN+ACK 包将被送到代理网关上,因为目的地址 2.2.2.2 并没有在 gfwlist 列表中,所以放行,经过 FORWARD 链,到达 POSTROUTING 链,问题来了,ss-tproxy 已经在 POSTROUTING 链的 nat 表上设置了 SNAT 规则(ipts_non_snat 为 false),所以将被转换为 192.168.1.2:6666 -> 2.2.2.2:2333,而当这个数据包到达拨号网关时,拨号网关检查发现这个源地址并不是 192.168.1.100:8443,所以并不会按照端口映射规则将其转换为 1.1.1.1:8443 -> 2.2.2.2:2333,而是将其映射为一个随机端口,如 62333,所以外网主机接收到的 SYN+ACK 包的源地址是 1.1.1.1:62333,这显然是无法成功建立 TCP 连接的。

所以,对于 gfwlist 模式,只需要将 ipts_non_snat 设为 true,端口映射基本上就能正常工作。而对于 chnroute 模式,即使将 ipts_non_snat 设为了 true,在某些情况下依旧会失败,怎么说呢?比如你在 IP 为非 chnroute list 的外网主机上连接拨号网关上的映射端口,SYN 包没问题,会成功到达内网主机,但是 SYN+ACK 包在经过代理网关时,因为这个目的 IP 并不位于 chnroute list,所以会被送到代理网关上的代理进程(比如 ss-redir),也就是说这个 SYN+ACK 包会走代理出去,这显然会握手失败。如果你要让它握手成功,就必须将对应的目的 IP 放行,或者改用 gfwlist 模式。而 global 模式就不用说了,无论目的 IP 是国内还是国外,通通走代理,所以全都会握手失败,解决方法和 chnroute 模式一样,要么放行,要么用 gfwlist 模式。

桥接模式
桥接模式 - 网络拓扑
上图由 @myjsqmail 提供,他的想法是,在不改变原网络的情况下,让 ss-tproxy 透明代理内网中的所有 TCP、UDP 流量。为了达到这个目的,他在“拨号路由”下面接了一个“桥接主机”,桥接主机有两个网口,一个连接出口路由(假设为 wan),一个连接内网总线(假设为 lan),然后将这两张网卡进行桥接,得到一个逻辑网卡(假设为 br0),在桥接主机上开启“软路由功能”,即执行 sysctl -w net.ipv4.ip_forward=1,然后通过 DHCP 方式,获取出口路由上分配的 IP 信息,此时,桥接主机和其它内网主机已经能够正常上网了。

然后,在桥接主机上运行 ss-tproxy,此时,桥接主机自己能够被正常代理,但是其它内网主机仍然走的直连,没有走透明代理。为什么呢?因为默认情况下,经过网桥的流量不会被 iptables 处理。所以我们必须让网桥上的流量经过 iptables 处理,首先,执行命令 modprobe br_netfilter 以加载 br_netfilter 内核模块,然后修改 /etc/sysctl.conf,添加:

保存退出,然后执行 sysctl -p 来让这些内核参数生效。

但这还不够,我们还需要设置 ebtables 规则,首先,安装 ebtables,如 yum -y install ebtables,然后执行:

如果 proxy_tproxy 为 false,那么你还需要修改 ss-tproxy 里面的 iptables 规则,将 REDIRECT 改为 DNAT,如:

没出什么意外的话,现在桥接主机和其它内网主机的 TCP 和 UDP 流量应该都是能够被 ss-tproxy 给透明代理的。
差点忘了,请将 /etc/ss-tproxy/ss-tproxy.conf 里面的 ipts_non_snat 选项改为 true,因为不需要 SNAT 规则。

钩子函数
ss-tproxy 支持 4 个钩子函数,分别是 pre_start(启动前执行)、post_start(启动后执行)、pre_stop(停止前执行)、post_stop(停止后执行)。举个例子,在不修改 ss-tproxy 脚本的前提下,设置一些额外的 iptables 规则,假设我需要在 ss-tproxy 启动后添加某些规则,然后在 ss-tproxy 停止后删除这些规则,则修改 ss-tproxy.conf,添加以下内容:

自启(Systemd)

  • mv -f ss-tproxy.service /etc/systemd/system
  • systemctl daemon-reload
  • systemctl enable ss-tproxy.service

自启(SysVinit)

  • touch /etc/rc.d/rc.local
  • chmod +x /etc/rc.d/rc.local
  • echo '/usr/local/bin/ss-tproxy start' >>/etc/rc.d/rc.local

注意,上述自启方式并不完美,ss-tproxy 可能会自启失败,主要是因为 ss-tproxy 可能会在网络还未完全准备好的情况下先运行,如果 ss-tproxy.conf 中的 proxy_server 为域名(即使是 IP 形式,也可能会失败,因为某些代理软件需要在有网的情况下才能启动成功),那么就会出现域名解析失败的错误,然后导致代理软件启动失败、iptables 规则配置失败等等。缓解方法有:将 proxy_server 改为 IP 形式(如果允许的话);或者将 proxy_server 中的域名添加到主机的 /etc/hosts 文件;或者使用各种方式让 ss-tproxy 在网络完全启动后再启动。如果你使用的是 ArchLinux,那么最好的自启方式是利用 netctl 的 hook 脚本来启动 ss-tproxy(如拨号成功后再启动 ss-tproxy),具体配置可参考 Arch 官方文档

用法

  • ss-tproxy help:查看帮助
  • ss-tproxy start:启动代理
  • ss-tproxy stop:关闭代理
  • ss-tproxy restart:重启代理
  • ss-tproxy status:代理状态
  • ss-tproxy check-command:检查命令是否存在
  • ss-tproxy flush-dnscache:清空 DNS 查询缓存
  • ss-tproxy flush-gfwlist:清空 ipset-gfwlist IP 列表
  • ss-tproxy update-gfwlist:更新 gfwlist(restart 生效)
  • ss-tproxy update-chnonly:更新 chnonly(restart 生效)
  • ss-tproxy update-chnroute:更新 chnroute(restart 生效)
  • ss-tproxy show-iptables:查看 iptables 的 mangle、nat 表
  • ss-tproxy flush-iptables:清空 raw、mangle、nat、filter 表

ss-tproxy flush-gfwlist 的作用:因为 gfwlist 模式下 ss-tproxy restartss-tproxy stop; ss-tproxy start 并不会清空 ipset-gfwlist 列表,所以如果你进行了 ss-tproxy update-gfwlistss-tproxy update-chnonly 操作,或者修改了 /etc/tproxy/gfwlist.ext 文件,建议在 start 前执行一下此步骤,防止因为之前遗留的 ipset-gfwlist 列表导致奇怪的问题。注意,如果执行了 ss-tproxy flush-gfwlist 那么你可能还需要清空内网主机的 dns 缓存,并重启浏览器等被代理的应用。

如果需要修改 proxy_kilcmd(比如将 ss 改为 ssr),请先执行 ss-tproxy stop 后再修改 /etc/ss-tproxy/ss-tproxy.conf 配置文件,否则之前的代理进程不会被 kill(因为 ss-tproxy 不可能再知道之前的 kill 命令是什么,毕竟 ss-tproxy 只是一个 shell 脚本,无法维持状态),这可能会造成端口冲突。

日志

脚本默认关闭了详细日志,如果需要,请修改 ss-tproxy.conf,打开相应的 log/verbose 选项

  • dnsmasq:/var/log/dnsmasq.log
  • chinadns:/var/log/chinadns.log

代理测试

本节的部分内容与 ss-tproxy v3 版本不符,重在相关流程的理解。

简单测试
所谓的简单测试就是看能不能上网(这里使用 curl 进行测试,你可以用浏览器直观的测试),如果 ss-tproxy 运行在网关上,那么建议在网关及内网中都测试一遍,如果只是在普通主机上使用,只要在这台主机上进行测试就行了。如果测试成功,足以说明 DNS 解析和 TCP 代理正常工作,如果测试失败,请参考后面的 调试方法,进行调试。

调试方法
开始调试前,请先打开 ss-tproxy.conf 中相应的 log/verbose 选项。这里只说一下简要步骤,不会详细介绍每个细节,因为模式太多了,懒得码字。重在理解,如果不理解,我写再多也没用。我建议你开启多个终端,每个终端都 tail -f 跟踪一个日志文件(为了好分辨每次调试,可以人为的敲几个空行),日志文件的路径在上一节中列出来了,当然还有 tun2socks 模式的 socks5 代理日志以及 v2ray 模式的 v2ray 日志,日志在哪你应该清楚。

调试之前,先弄清楚每个模式中的流量走向以及每个组件的作用(chnonly 模式与 gfwlist 模式原理相同,只不过 gfwlist.txt 列表不同):

无论什么模式,首先要关注的都是 DNS,因为 iptables、iproute 这些东西只认 IP,只有获得了对应的 IP,才有下一步的可能。比如 curl https://www.baidu.com,curl 首先进行的是 DNS 解析,将 www.baidu.com 转换为 IP 地址,然后再向这个 IP 地址发起 TCP 连接。如果 curl、wget 或浏览器访问网页失败,记得先排除 DNS 问题。以 tproxy_chnroute 模式为例,开 3 个窗口,分别 tail -f dnsmasq、chinadns、ss-tunnel 的 log,然后使用 dig 进行测试,先测试 ss-tunnel/60053,再测试 chinadns/65353,最后测试 dnsmasq/53。

  • dig @127.0.0.1 -p60053 www.google.com:测试 ss-tunnel,并观察 ss-tunnel 的日志。正常情况下,dig 是能够解析的,且日志有成功的 INFO。如果没有,那就不用下一步了,先解决 ss-tunnel 的问题吧。
  • dig @127.0.0.1 -p65353 www.baidu.com:测试 chinadns(国内),并观察 chinadns、ss-tunnel 的日志。正常情况下,dig 是能够解析的,从日志中可以看到:filter 来自 127.0.0.1:60053 的响应,pass 来自 114.114.114.114:53 的响应。
  • dig @127.0.0.1 -p65353 www.google.com:测试 chinadns(国外),并观察 chinadns、ss-tunnel 的日志。正常情况下,dig 是能够解析的,从日志中可以看到:filter 来自 114.114.114.114:53 的响应,pass 来自 127.0.0.1:60053 的响应。
  • dig @127.0.0.1 -p53 www.baidu.com:测试 dnsmasq(国内),并观察 dnsmasq、chinadns、ss-tunnel 的日志。正常情况下,除了第一次解析,其它的 DNS 查询时间都是 0ms(DNS 缓存的缘故)。
  • dig @127.0.0.1 -p53 www.google.com:测试 dnsmasq(国外),并观察 dnsmasq、chinadns、ss-tunnel 的日志。正常情况下,除了第一次解析,其它的 DNS 查询时间都是 0ms(DNS 缓存的缘故)。

进行 UDP 的测试
如果是 v2ray/tproxy 模式,那么请在内网主机中测试 UDP,因为本机的 UDP 没有被代理;如果是 tlspxy/tun2socks 模式,本机或内网都可以,无所谓。仍以 tproxy_chnroute 模式为例,在某台内网主机中,使用 dig @208.67.222.222 -p443 www.google.com,如果正常解析,则观察 ss-redir 的日志(verbose 模式),应该能看到对应 udp relay 成功的信息(有 udp relay 只能说明 UDP 代理规则没问题,能不能用是另一回事,因为有些 ISP 会针对 UDP 进行恶意丢包)。

反馈代理脚本的问题
如果某个环节总是无法成功,或者你认为应该是 ss-tproxy 脚本的问题,请附带对应的 log(详细日志),以及 bash -x /usr/local/bin/ss-tproxy start 的启动调试日志(敏感信息请自行替换),并且说明你的网络环境,如果还是不懂,我强烈建议您先看一遍 提问的智慧,然后提问也不迟。

常见问题

ss-tproxy 开机自启失败
将 ss-tproxy.conf 中的 v2ray_server、tlspxy_server、socks5_remote、server_addr 改为 IP,或者将相关域名加到 ss-tproxy 主机的 hosts 文件。

start 时部分组件 stopped
请先检查 /var/log 下面的日志,看看是不是地址被占用了(具体有哪些端口,可以看前面的端口列表),如果是,请自行解决。分析日志是个好习惯。

切换模式后不能正常代理了
从其它模式切换到 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/SSR 服务,那么在除 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 配置细节
基本配置不用变动,不过还是得注意一下 DNS 问题,分配给内网主机的 DNS 建议设为网关 IP(即运行 ss-tproxy 的主机)。假设整个内网的网段是 192.168.1.0/24,网关(即运行 ss-tproxy 的主机)的 IP 为 192.168.1.1,则将 DHCP 分配的 DNS 设为 192.168.1.1。不过这只是个建议,没有强制要求,你设为 114DNS、GoogleDNS、OpenDNS 也没问题。但千万别设置为 127.0.0.1 这样的地址,或者是内网中的其它主机,这种情况下 ss-tproxy 的 DNS 强制重定向就失效了,会导致内网主机的 DNS 解析失败,无法正常联网。另外,网关的 DHCP 服务器建议使用 dhcpd。

ss-tproxy 脚本的运行环境
1、ss-tproxy 支持什么 linux 系统?
一般都支持,没有限制,因为没有使用与特定发行版相关的命令、功能(我自己用的是 ArchLinux)。

2、ss-tproxy 只能运行在网关上吗?
显然不是,你可以在一台普通的 linux 主机上运行(此时你不用管 ss-tproxy.conf 中的内网网段配置)。

3、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 呢?假设这里的副路由是一个树莓派,那么我不管在什么网络下(公司、酒店),只要将树莓派插上,然后我的设备(手机、笔记本)只需要连接树莓派就能无缝上网了,同时又不会影响内网中的其它用户,一举两得。

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

切换模式、切换节点不方便?
这确实是个问题,切换节点需要修改配置文件,切换模式需要修改配置文件,有没有更加简便一些的方式?抱歉,没有。不过因为 ss-tproxy.conf 是一个 shell 脚本,所以我们可以在这里面做些文章。如果你有很多个节点(付费号一般都是),可以这样做(以 v2 版本为例,v3 版本请自行修改):