无需 OpenWRT,用树莓派搭建旁路由网关

陪她去流浪 桃子 编辑 阅读次数:532

标题中特别注明“无需 OpenWRT”,是的,通常来说,大家都是习惯性用 OpenWRT 来作为主路由/旁路由/软路由。 但是我一直觉得,#OpenWRT 家族(原版➕改版)实在太大了,以至于我总是不知道应该选哪一款。 并且它们总是做得功能非常复杂,比较吃资源。然后绝大多数人应该只是需要其中的那个 Passwall 面板,仅此而已。

它们还需要各种适配硬件,应该大都是以 ISO 或者 IMG 的方式提供的,需要完整安装或者导入,并且提供的安装包一般也比较大(超过 100MB),还很可能找不到自己的硬件对应的资源。总之就是,操作复杂,成本太高,大材小用了。

起因

一些朋友应该知道我最近离开了深圳的家,去北方旅居了几个月。 北方的家里没有宽带,靠一个几十块钱的便携 Wi-Fi 支撑着1。 然后,莫名其妙地,中国联通的 IPv6 好像连不上中国电信的 IPv6,所以我暂时也没法通过 IPv6 回深圳的家里的网络走 OpenWRT 了2。“赖以生存”多年的网络架构突然有点儿“绷不住了”。 然后我就意识到,我应该重新搭一个 #旁路由 了。

为什么是旁路由?

稳定性大于一切。

我一直相信那句话很有名的话:“如果你家里有位网络工程师,那你的网会经常出问题。” 我不想把自己家里的网络搞得经常出问题(我自己没关系,不应该影响其他人/设备)。 所以我总是推荐把软路由作为旁路由而不是主路由

旁路由和其它设备一样处于同一个网段,就是一个普通的设备,完全不影响未把旁路由作为网关的设备的网络。此时的旁路由它其实并没有路由功能,更只是一个网关程序——拦截流量,重新发出去。所以我们好像没有任何理由用 OpenWRT,我们平等地处于同一个网络,我们需要的根本就不是一个路由器。

如何拦截流量?

正常情况下,设备的网关地址是指向路由器的,路由器简单处理一下(通常是 NAT)后直接发出去:

normal

如果我们把设备的网关(Gateway)地址设置成旁路由(此时还是一个普通设备/Linux 主机),路由器就会把来自设备的、发给服务器的流量发给旁路由设备(本文的树莓派):

gateway

那我们能不能在旁路由上把它还原成一个 TCP 连接?如果可以的话,是不是就接管了此连接?

可以! 过程其实很简单:旁路由假定自己就是设备要连接的服务器,然后把所有来自设备的网络包当作是发给自己的。

简单回顾一下 TCP/IP 模型中应用层的两个程序是如何一层一层地通过物理网络通信的:

  • 发送端: 应用层 → 传输层 → 网络层 → 链路层 → 物理层;
  • 接收端: 物理层 → 链路层 → 网络层 → 传输层 → 应用层。

如果要在内核中 HACK 一下,把接收端的网络层的“目标 IP”改一下,是不是就可以欺骗内核中的网络子系统说这些包就是发给本系统的? 然后,网络子系统就会把它还原成一条像是到本系统的连接一样,listen 后 accept 到此连接。

实际上,Linux 中早在 2008 年就有此技术了。其名字叫“TProxy”,即“Transparent Proxy”/“透明代理”。其实现更加细节:比如它还支持通过特定 socket 选项操作获取到原始设备的地址、原始服务器地址、模拟原始设备重新把数据发出去。无敌透明🫥!

拦截实现

按照上面的 tproxy 模块的文档,完成流量拦截需要两个主要步骤:

  1. 监听连接请求,socket 选项要支持 IP_TRANSPARENT 以允许作为透明代理的传入:

    1
    
    setsockopt(fd, SOL_IP, IP_TRANSPARENT, &value, sizeof(value));
    
  2. 用 iptables 把流量转发到名为 TPROXY 的 Target 上:

    1
    
    iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY --tproxy-mark 0x1/0x1 --on-port 50080
    

    此 target 支持名为 --on-port 的端口参数。tproxy 会将透明代理来的连接转发到监听此端口的 socket 上。

经过上面两个步骤以后,所有来自“设备”的连接,都被 tproxy 接管并转发到树莓派上的监听程序上了,并且还可以拿到原始的服务器地址。

实现 tproxy 的几个例子:

第一个是很好的学习、研究例子。 第二个则可以把连接再次转发到 SOCKS5 服务器上,这是目前被所有主流代理软件提供的接入协议,所以使用更友好。

搭建旁路由网关

前面所讲的内容全部是为本节作铺垫。

  1. 常规代理软件

    在设备上运行任何一个众所周知的代理软件即可。此软件会监听“localhost:1080”的 SOCKS5 连接。

  2. tproxy

    上述第二个程序(ipt2socks)将来自 tproxy 的流量转发到代理软件。

    无任何参数运行后,会将 tproxy 的 “localhost:60080” 转发到 SOCKS5 的 “localhost:1080” 端口上。完成第一步操作。

  3. iptables

    同样来自此作者的 ss-tproxy,主要用来操作 iptables,将来自设备的流量转发到 tproxy 上。同时还提供了“特供版” DNS 解析器。完成了第二步操作。

    作者的帮助文档写得非常详细,我不多说了。

完全上述3️⃣个步骤(运行3️⃣个程序)后,基本上,在树莓派本机上已经可以自由访问网络了。

同样,以“稳定”为前提,由于主路由上几乎都有开启 DHCP 功能,所以我不会在旁路由上面开启。 所以需要把需要代理的设备的网络设置改一下:

  1. 把 网关 改成树莓派的 IP 地址;
  2. 把 DNS 改成树莓派的 IP 地址。

此修改一直有效,断开 Wi-Fi 后重连也如此。

当然,建议把树莓派的 IP 地址在主路由器把和 MAC 地址绑定一下,使主路由分配固定的 IP 地址给旁路由。比如我的主路由是 192.168.0.1,我的旁路由是 192.168.0.2。其它设备的地址从 192.168.0.10 开始。

基本上,“无需 OpenWRT,用树莓派搭建旁路由网关”就成功结束了。

随机附一张图:

结束语

我觉得这是我目前体验最好、搭建起来最方便的方式了。

本文虽以树莓派为例,实际上完全不限硬件平台,任意能跑 Linux 的小主机都行。 甚至是虚拟机也没问题,只不过我不希望在 MacBook 上跑虚拟机(要一直开着,不然手机咋用?)。

我对旁路由的要求中“稳定性” > “网速”。 树莓派被很多人瞧不起,因为它的网络确实不算很好。不过,我很容易满足:慢一点可以,不要老是断网!虽然你们看不上树莓派,但是我用它刷网页、看油管很流畅,不开玩笑[狗头]

树莓派便宜啊!我买的 #树莓派 Zero 2 W 才 💯 刚出头…… 能用如此性价比的硬件带来全家设备自由上网,还要什么自行车吗?等等,我在旅游哎!树莓派这么小巧的东西谁见不爱呢?

参考配置文件

以下是 ss-tproxy.conf 我的修改部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
diff --git a/old.conf b/new.conf
index d179ab1..5582102 100644
--- a/old.conf
+++ b/new.conf
@@ -5,10 +5,10 @@ mode='chnroute' # 大陆白名单:{gfwlist}走代理,{ignlist,chnlist,chnrou
 
 ## ipv4/6
 ipv4='true'     # 是否对ipv4启用'透明代理': true启用 false不启用
-ipv6='false'    # 是否对ipv6启用'透明代理': true启用 false不启用
+ipv6='true'     # 是否对ipv6启用'透明代理': true启用 false不启用
 
 ## tproxy
-tproxy='false'  # true:  TPROXY(tcp)   + TPROXY(udp) ## 纯 tproxy 模式 ##
+tproxy='true'   # true:  TPROXY(tcp)   + TPROXY(udp) ## 纯 tproxy 模式 ##
                 # false: REDIRECT(tcp) + TPROXY(udp) ## redirect 模式  ##
                 #
                 # 具体取决于'本机代理进程'的透明代理传入'协议'
@@ -107,12 +107,12 @@ dns_direct6_white='true'              # 将dns_direct6的ip加入白名单(globa
 
 # 远程DNS,用于解析“黑名单”(要走代理的)域名
 # 对远程DNS的最终网络访问(TCP/UDP)必须走“代理”
-dns_remote_tcp='tcponly'              # 对“无协议限定”的上游进行修改,以满足当前的代理配置
+dns_remote_tcp='always'               # 对“无协议限定”的上游进行修改,以满足当前的代理配置
                                       # - tcponly: tcponly模式时自动使用tcp上游(避免污染)
                                       # - always: 总是自动使用tcp上游(例如代理的udp不太行)
                                       # - 留空: 不处理,尊重dns_remote/dns_remote6的配置值
 dns_remote='8.8.8.8'                  # 远程DNS(用于v4透明代理),允许配置多个DNS,用空格隔开
-dns_remote6='2001:4860:4860::8888'    # 远程DNS(用于v6透明代理),允许配置多个DNS,用空格隔开
+dns_remote6=''                        # 远程DNS(用于v6透明代理),允许配置多个DNS,用空格隔开
 dns_remote_black='true'               # 将dns_remote的ip加入黑名单(gfwlist/chnroute),使其走代理
 dns_remote6_black='true'              # 将dns_remote6的ip加入黑名单(gfwlist/chnroute),使其走代理
 
@@ -128,7 +128,7 @@ chinadns_verdict_cache='4096'            # tag:none 域名的判决缓存容量(
 chinadns_verdict_db='verdict-cache.db'   # 若非空,则启用 verdict 缓存持久化,重启 chinadns-ng 将会保留缓存数据
 chinadns_chnlist_first='false'           # 优先加载 chnlist 域名列表,默认是 gfwlist 优先,用于 chnroute 模式
 chinadns_config_files=''                 # 加载 chinadns 配置文件,多个文件请用空格隔开,具体配置见 chinadns-ng
-chinadns_extra_options=''                # 追加 chinadns 命令行参数,请勿覆盖已有的参数,具体参数见 chinadns-ng
+chinadns_extra_options='-N'              # 追加 chinadns 命令行参数,请勿覆盖已有的参数,具体参数见 chinadns-ng
 chinadns_verbose='false'                 # 记录详细运行日志,除非进行调试,否则不建议启用
 chinadns_logfile='/var/log/chinadns.log' # 日志文件,如果不想保存日志可以改为 /dev/null
 
@@ -136,12 +136,12 @@ chinadns_logfile='/var/log/chinadns.log' # 日志文件,如果不想保存日
 ipts_if_lo='lo'                     # 环回接口的名称,在标准发行版中,通常为 lo,如果不是请修改
 ipts_rt_tab='233'                   # iproute2 路由表名或表 ID,除非产生冲突,否则不建议改动该选项
 ipts_rt_mark='0x2333'               # iproute2 策略路由的防火墙标记,除非产生冲突,否则不建议改动该选项
-ipts_set_snat='false'               # 设置 ipv4 MASQUERADE(SNAT) 规则,selfonly=false 时有效,详见 README
-ipts_set_snat6='false'              # 设置 ipv6 MASQUERADE(SNAT) 规则,selfonly=false 时有效,详见 README
+ipts_set_snat='true'               # 设置 ipv4 MASQUERADE(SNAT) 规则,selfonly=false 时有效,详见 README
+ipts_set_snat6='true'              # 设置 ipv6 MASQUERADE(SNAT) 规则,selfonly=false 时有效,详见 README
 ipts_reddns_onstop='223.5.5.5#53'   # stop后重定向内网主机发来的dns至指定dns,selfonly=false 时有效,详见 README
 ipts_reddns6_onstop='240C::6666#53' # stop后重定向内网主机发来的dns至指定dns,selfonly=false 时有效,详见 README
 ipts_proxy_dst_port=''              # 要代理哪些端口,留空表示全部,多个逗号隔开,冒号表示范围(含边界),详见 README
-ipts_drop_quic='tcponly'            # 丢弃发往"黑名单"的QUIC: 留空:不丢弃 | tcponly:tcponly时丢弃 | always:总是丢弃
+ipts_drop_quic='always'             # 丢弃发往"黑名单"的QUIC: 留空:不丢弃 | tcponly:tcponly时丢弃 | always:总是丢弃
 
 ## opts
 opts_ss_netstat='auto'      # auto/ss/netstat,用哪个端口检测工具: auto(自动选择,优先考虑ss) | ss | netstat
@@ -282,4 +282,3 @@ custom_dns_pid() {
 #
 # ss-tproxy.conf是一个shell脚本,可以使用source来加载其他shell脚本
 # ss-tproxy.conf被执行时,可以访问ss-tproxy传来的命令行参数(位置参数)

另外,还写了个 tmux 脚本用于开机自启动(启动上面几个程序的方式,不用完全参考我的做法):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
#
# 会被 /etc/systemd/system/gfw.service 执行。

set -eux

tmux=/usr/bin/tmux
session=gfw

"$tmux" has-session -t "$session" && exit 0

"$tmux" new-session -d -s "$session"

"$tmux" split-window  -t "$session"
"$tmux" split-window  -t "$session"
"$tmux" split-window  -t "$session"
"$tmux" select-layout -t "$session" tiled

"$tmux" send-keys -t "$session:.0" 'sudo -i' C-m 'sudo -g proxy ipt2socks' C-m
"$tmux" send-keys -t "$session:.1" 'while :; do socks-proxy client -s https://example.com/some-path/ -t token; sleep 1; done' C-m
"$tmux" send-keys -t "$session:.2" 'sudo ss-tproxy restart' C-m
"$tmux" send-keys -t "$session:.3" 'curl google.com'
"$tmux" select-pane -t "$session:.3"

树莓派装系统?

用官方的 Imager 装个 Lite(无桌面)版就可以了,足够。 安装的时候在 Imager 上面设置好 Wi-Fi 和 SSH 的密码/公钥🔐。

然后再建议安装上我特意为树莓派编写的一款走蓝牙通信连接 SSH 的一款极其简单的软件,并设置开机自启动,方便在树莓派网络设置失败以至于失联的时候用 SSH 连上去补救一下[狗头],见《SSH 通过蓝牙连接树莓派》。