0%

树莓派 + OpenConnect 搭建 Anyconnect VPN 策略路由

声明:

本文章仅供学习探讨相关技术之用,不保证实际环境中的可用性与合规性,请勿部署在实际环境中。同时,请遵守 VPN 提供方的相关规定(例如,学校提供的 VPN 绝对不可以分享给他人,并且只有本人可以使用)。

此外,本系统会暴露登录 vpn 的用户名、密码与 2FA 于树莓派中,尽管可以以各种形式加强安全性(例如设置文件权限、禁止密码登录树莓派),但安全性与不部署此系统相比仍有降低。

1 需求分析

只在树莓派上登陆学校的 Cisco Anyconnect VPN,让同一个子网内的其他设备通过树莓派访问学校内网资源(我的主要需求是访问内网工作站上的 PVE web portal 和使用 SPICE 远程连接工作专用的虚拟机)。同时在树莓派上实现 VPN 的自动登录和重连,保证高可用性。

近期主力科研设备转移到了一台放在实验室的 ML Workstation,由于这个 workstation 处于学校内网,所以必须要连接上学校的 VPN 再通过远程桌面使用(不喜欢 teamviewer)。考虑到 VPN 的特殊性,我并不想在个人设备上时时开着 vpn 进行科研(说得我好像经常在科研一样),所以我总是打开放在家里的一台学校的 workstation,再连接vpn、远程桌面。这台 workstation 发热量极大,导致房间温度总是很高,所以上述需求也就应运而生。如果我在树莓派上登录了 vpn,那我子网里其他所有设备就可以通过树莓派来连接学校内网了,并且只有在规则中设定好的请求才会被转发至内网,也就是说在个人设备上进行科研不再会有各方面的顾虑了。

2 实现分析

以树莓派为核心,这套系统分成了两个部分:流量转发和 VPN 的配置(主要是自动登录与重连)。

流量转发其实是一个老生常谈的话题(毕竟这种 routing 和科学上网密不可分),简单的静态路由加树莓派上开个 nat 就足够了。

Cisco Anyconnect 的 client 在树莓派上可以用 openconnect 替代。vpn 的自动登录主要难点在于学校启用了 Duo 2FA,需要在自动登录脚本里自动生成 2FA passcode,好在我在一番搜索之后找到了在命令行生成 Duo Passcode 的办法。除了一些网络连接错误之外, VPN 会有两种情况自动断线,一种是 idle 超过一定时间(据我观测大约是1小时),另一种是达到了最大连接时长(大约是24或25小时),所以需要实现自动重新连接,来保证 vpn 服务不断。这个需求用 crontab 或者 systemd 均可,权衡一番之后我最终给 openconnect 写了个 service 文件,用 systemd 作为 daemon。

总的来说,这套系统包含:

  • 流量转发(3.2)
    • 树莓派开启包转发,并添加 NAT 将内网的请求交给 vpn tunnel 处理(3.2.1-3)
    • 设置静态路由,将目的地为学校内网 ip 的请求转发到树莓派(3.2.4)
  • OpenConnect VPN 配置(3.3)
    • Duo 2FA Passcode 命令行生成(3.3.1)
    • 自动登录脚本(3.3.2)
    • 用 systemd 守护进程(3.3.3)

警告:这套系统会在树莓派上暴露登录学校网站的用户名、密码与 2FA,尽管可以以各种形式加强安全性(例如设置文件权限、禁止密码登录树莓派),但安全性与不部署这套系统相比仍有降低。

3 系统实现

3.1 准备工作

这两年芯片短缺,连树莓派都断货了,我在家找了半天只找到一个闲置的 Raspberry Pi 3B+。不过考虑到这个树莓派只负责连回学校这一个任务,并且只有我一个人用,负载不会很大,所以 3B+ 虽然有点老但还是能用。sd 卡方面,因为要长时间开机,推荐购买标有 U3 及 A2 的卡,Samsung Pro Endurance 也可(毕竟 MLC,我最近可太喜欢这张卡了),32GB 足够。不过树莓派在这里只负责转发和连接 vpn,不会存储重要数据,用菜一点的 sd 卡我觉得问题也不大。电源需要 5V2A+ 保证稳定。系统我选择了 Ubuntu Server,毕竟不需要桌面环境,也比 Raspbian 好使。此外最好使用有线连接,比无线连接稳定太多,也不用配置。

在用 Win32DiskImager 烧写 Ubuntu Server 20.04 LTS 到 SD 卡之后,在 SD 卡根目录下新建文件名为 SSH 的文件来启用 ssh。插好网线和电源,在路由器上设置静态 ip,如此等等,网上教程太多了,按下不表。用 ssh 连接到树莓派并更新到最新版(我在 Windows 上用的 XShell,家庭和个人使用是免费的)。

1
2
sudo apt update
sudo apt upgrade

3.2 流量转发

3.2.1 安装 OpenConnect 并进行测试

<VPN URL> 替换为你的 VPN 连接地址,前面不用加 http://https://。我校强制启用了 Duo 2FA,所以在输入完用户名和密码之后,会要求用户输入第二个密码,即 Duo 生成的6位 passcode。

1
2
sudo apt install openconnect
sudo openconnect <VPN URL>

连接成功之后会有如下类似输出,实测最后一行的 Error 没啥影响。

1
2
3
4
<...>
Connected as <Your IPv4 Adress>, using SSL, with DTLS in progres
Established DTLS connection (using GnuTLS). Ciphersuite (DTLS1.2)-(ECDHE-RSA)-(AES-256-GCM).
Error: any valid prefix is expected rather than "dev".

这时候 openconnect 是在前台运行的,要查看 vpn 的连接情况,可以开启一个新的 ssh 连接,并使用命令 ip addr,可以看到如下类似输出,其中的 <Your IPv4 Adress> 应该和上面输出的 ip 相同。这里的 tun0 设备就是 vpn 使用的 tunnel 了。

1
2
3
4
5
6
7
8
9
1: lo: <...>
2: eth0: <...>
3: wlan0: <...>
6: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1390 qdisc fq state UNKNOWN group default qlen 500
link/none
inet <Your IPv4 Adress>/32 scope global tun0
valid_lft forever preferred_lft forever
inet6 <Your IPv6 Adress>/64 scope link stable-privacy
valid_lft forever preferred_lft forever

为了确保 vpn 的正确连接,可以 ping 一个内网 ip 试一下,能 ping 通即说明连接成功。

不要关闭 vpn 连接,刚刚新建的 ssh 连接里进行接下来的配置。

3.2.2 允许包转发与 bbr 优化

先查看内核的包转发有没有启用,如果已启用输出应该是1,否则为0。

1
cat /proc/sys/net/ipv4/ip_forward

编辑配置文件启用包转发

1
sudo nano /etc/sysctl.conf

找到下面的配置,并取消注释 net.ipv4.ip_forward=1

1
2
# Uncomment the next line to enable packet forwarding for IPv4
#net.ipv4.ip_forward=1

保存生效

1
sudo sysctl -p /etc/sysctl.conf

这样树莓派就开启了包转发的功能。同时,还可以开启 bbr 优化,来优化 tcp 拥塞,可以提高流量的处理效率。对算法有兴趣可以搜一下,介绍的文章很多。

方便起见,我们切换为 root 修改配置文件。

1
2
3
4
5
6
7
8
9
sudo passwd root # 设置root密码
su root # 切换为root

# 写入配置文件
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf

# 保存生效
sysctl -p

返回值应该如下所示,可以看到这三行反映了我们上面对 /etc/sysctl.conf 的修改。

1
2
3
net.ipv4.ip_forward = 1
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

确认 bbr 已经开启

1
2
3
4
5
sysctl net.ipv4.tcp_available_congestion_control
# 返回值应该类似 net.ipv4.tcp_available_congestion_control = reno cubic bbr,包括了 bbr

lsmod | grep bbr
# 返回值应该类似 tcp_bbr 20480 1

最后,用 exit 命令退出 root身份。

3.2.3 配置 NAT

使用 iptables 进行配置,其中 <内网ip地址[/mask]> 可以为一个地址段,比如 172.16.0.0/24,如果是单个 ip 则比如 172.16.0.2,也可以直接并列的写很多 ip 地址。 <vpn 设备名> 就是上面 ip addr 命令中看到的 tun0

1
sudo iptables -t nat -A POSTROUTING -d <内网ip地址[/mask]> -o <vpn 设备名> -j MASQUERADE

简单的说,这条命令是在 nat 表里(-t)添加了(-A)一个 POSTROUTING (外到内)规则,该规则负责处理目标(-d) ip 为 <内网ip地址[/mask]> 的请求,将其发送(-o)到 <vpn 设备名>,处理动作(-j)是动态伪装(MASQUERADE),即自动获取 <vpn 设备名> 的 ip,并将请求伪装成是该 ip 发出的。

上述规则将会在重启后失效。要让规则永久生效,我个人比较喜欢使用一个小工具

1
2
sudo apt install iptables-persistent # 安装
sudo netfilter-persistent save # 保存添加的规则

可以使用如下命令查看已生效的 iptables 规则

1
sudo iptables -L -t nat

输出类似如下所示

1
2
3
4
5
6
7
8
9
10
11
12
Chain PREROUTING (policy ACCEPT)
target prot opt source destination

Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain OUTPUT (policy ACCEPT)
target prot opt source destination

Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- anywhere <内网ip地址[/mask]>

3.2.4 配置静态路由

这里我们假设所有设备都在同一个子网里,出口只有一个路由器,且该子网内不与他人共享(其他情况见本段末)。最简单的配置静态路由的方法就是在路由器上进行设置了。

警告:如果配置该静态路由的路由器被共享,则你的 vpn 连接也很可能被同内网的不属于你的设备共享。所以最好是自己有一个内网,只有自己的设备可以接入。

设置可能因路由器型号、固件而不同。在我的 k2p padavan 上,该设置在 Advanced Settings - LAN - Route - Enable Static Routes? - Static Route List 里。而在 Netgear R7450 上,该设置应该在 Advanced Settings - Static Route。

Destination IP 应该设置为 <内网ip地址[/mask]> 中的 ip 地址,而 Netmask 即是子网掩码,Gateway 应设置为树莓派的静态 ip 地址,Metric 设置为 15 即可。

以网段 172.16.0.0/2 为例,

  • Destination IP - 172.16.0.0
  • Netmask - 255.255.255.0

以单个 ip 172.16.0.3 为例,(mask 实际上是32)

  • Destination IP - 172.16.0.3
  • Netmask - 255.255.255.255

如果有多个 ip,添加多条规则并保存即可。

这时候就可以测试策略路由有没有生效了。在一台与树莓派同内网的设备上,尝试 ping 一下某个内网 ip,或者尝试一下 ssh 连接,应该没有问题了。如果连不上,在 windows 上使用 tracert <ip>,Mac/Linux上使用 traceroute <ip>,可以看到在哪一步出现问题,然后再排查故障。

测试成功之后,在 vpn 连接的 ssh 会话里使用 Ctrl + C 断开 vpn 连接。

如果你没有一个独享的内网环境,大概有以下四种方法:

  • (详见 4.1)在树莓派上装一个 Clash,就可以很方便的搭建起 HTTP/HTTPS/SOCKS server,然后在本机配置相应代理即可。

  • 你可以对发起请求的设备 ip 进行过滤,确保 nat 规则只对你的设备生效,具体做法是在3.2.3节添加 iptables 规则时,添加 -s 参数指定请求来源。不过这要求你的设备都有静态 ip,不然 ip 地址总变动,规则也会随之失效。

  • 在自己的设备上直接使用 route 命令配置静态路由。具体可以自行 google。

  • 你也可以将你的设备的网关设置为树莓派的静态 ip,由树莓派而不是路由器处理你的设备的网络流量(类似于 PiHole 那种做法)。

3.3 VPN 配置

所有代码与配置文件均已在 https://github.com/tangbao/Openconnect-VPN-RPi-Gateway 开源。

将代码保存在树莓派的 ~/myvpn 目录下。

1
2
3
4
cd ~
sudo apt install git # 安装 git
git clone https://github.com/tangbao/Openconnect-VPN-RPi-Gateway myvpn
cd myvpn

使用 pwd 命令查看当前目录的绝对路径(一般是 /home/ubuntu/myvpn),下文中将使用 <WorkDir> 指代。

3.3.1 Duo 2FA Passcode 命令行生成

首先确认 python 是否安装。在 ubuntu 上 python 2 或者 3 需要显式地指定(当然你也可以自己软连接 python 命令到 2 或者 3 上)。

1
python3 --version

可以看到输出的版本号即为已安装 python 3。

1
Python 3.8.10

安装 pip3 和脚本所需的库

1
2
sudo apt install python3-pip
sudo -H pip3 install pyotp # 全局安装,不然 pyotp 只有 ubuntu 用户可以调用

(图片来源 https://github.com/AvikRao/duo-extension/wiki/Setup-and-Usage

接着获取 Duo 2FA 打开 Duo 2FA 的管理界面,选择 Add a new device - Tablet - Android - I have Duo Mobile installed,这时候会跳出一个二维码,右击二维码,选择 Copy image address

(图片来源 https://github.com/AvikRao/duo-extension/wiki/Setup-and-Usage

回到 <WorkDir> 下,运行脚本激活二维码,并提取 secret token、生成passcode。

1
python3 duo.py -qr <image address>

一切顺利的话,就可以获得一个6位的 passcode 了。

这个二维码和常见的二维码(可以离线扫的)不同,它包含的是一个向服务器发起注册设备的请求,服务器收到请求之后会返回用于生成 passcode 的 secret token。另外这是一个 HOTP passcode,可以离线扫的那种一般是 TOTP,有兴趣的可以搜一下差异,这里就不展开了。

3.3.2 openconnect 自动登录

自动登录脚本中,<root password> 就是 root 用户的密码,<NetID><NetID password> 是登录 vpn 的用户名和密码,而 <VPN URL> 则是连接 vpn 的网址。

1
challange=$(python3 duo.py) && sudo -S <<< "<root password>" echo I am root now && { printf '<NetID password>\n'; sleep 1; printf "$challange\n"; } | sudo openconnect <VPN URL> --user=<NetID>

这里就是切换到 root 身份,并使用 printf 将密码和 passcode 送到了 stdin 以便 openconnect 接收。

可以直接在 clone 下来的 myvpn.sh 中进行修改,然后 sudo bash myvpn.sh 运行进行测试。

可以按照 3.2.1 中所说的方法进行测试是否成功连接。测试完成之后断开连接。

3.3.3 用 systemd 守护进程

上面提到,vpn 在 idle (大约 1 hr)或达到最大连接时长(大约 24 hrs)之后会自动断开连接,各种各样的意外也会让 vpn 断开连接,我们也希望 vpn 在树莓派重启之后自动启动。综上所述,最合适的守护进程可能就是 systemd 了。为此,我编写了如下 service 配置文件(即 clone 下来的 myvpn.service,需要按需修改):

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Manage Openconnect VPN Connection. Follow the university policy and use at your own risk.
Documentation=https://github.com/tangbao/Openconnect-VPN-RPi-Gateway

[Install]
WantedBy=multi-user.target

[Service]
User=root
ExecStart=/bin/sh -c 'challange=$(python3 <WorkDir>/duo.py) && { printf \'<NetID password>\n\'; sleep 1; printf "$challange\n"; } | sudo openconnect <VPN URL> --user=<NetID>'
Restart=always

可以看到核心部分是 ExecStart= 之后的部分,即是我们在上节中测试的自动登录脚本。不同之处在于,生成 2FA passcode 的 duo.py 之前需要加上完整的路径 <WorkDir>/;输入 root 的密码部分被去掉了,因为 systemd 本身就会以 root 身份运行启动脚本;<NetID password>\n 两边的单引号加了转义符(有些密码用双引号会报错,如果单引号不行可以试一下双引号)。按照上节所述修改 myvpn.service,并保存。

myvpn.service 挪到正确的位置,并启用。

1
2
3
sudo mv myvpn.service /etc/systemd/system/
sudo systemctl enable myvpn # 开机启动 vpn
sudo systemctl start myvpn # 启动 vpn

使用如下命令检查 vpn 服务的运行情况

1
sudo systemctl status myvpn

如果看到绿色的 active (running),那么 vpn 服务就启动成功了。systemd 会负责在 vpn 断线时重连。在与树莓派同一子网内的设备上尝试连接内网设备,如果可以连接那就大功告成。

如下两个命令可能会有用

1
2
sudo systemctl disable myvpn # 取消 vpn 开机自启
sudo systemctl stop myvpn # 停止 vpn

4 扩展

4.1 使用 vpn 访问其他资源

上面的方法仅讨论了使用部署在树莓派上的 vpn 来访问内网机器的情况,但是,如果我们需要访问一些仅内网 ip 才能访问的网站(如下载论文)时,用同样的方法进行设置会变得非常繁琐。你需要准确的找到论文网站的 ip 地址并且设置路由,而这些网站数目繁多,ip 地址也并不一定固定,所以这一方法并不适用。这时候,我们可以在树莓派上搭一个 HTTP(S) / SOCKS 代理,再通过该代理连接到树莓派上,便可由挂了 vpn 的树莓派来处理这些请求。

这一方法也适用于 3.2.4 中提到的一些特殊情况的解决。下文中会有提到。

具体做法很简单,自然是 clash 大法好。clash 本身就提供了 HTTP(S) / SOCKS 服务器的功能,所以只需要允许局域网内的设备连接即可,无需配置其他。这里的 clash 也是 go 编写的命令行版 clash,无需使用封装好的 gui 版。

1
2
3
4
5
6
7
cd ~
mkdir clash && cd clash
wget https://github.com/Dreamacro/clash/releases/download/v1.9.0/clash-linux-armv8-v1.9.0.gz
# 下载 clash 的 armv8 版,树莓派 3B+ 与 4B 均为 armv8,最新版与其他架构版本请访问 https://github.com/Dreamacro/clash/releases/
mv clash-linux-armv8-v1.9.0 clash
chmod +x clash
./clash # 启动 clash

第一次启动时会看到如下 log,可以看到程序自动创建了配置文件并下载了一些必须的数据库文件。

1
2
3
4
INFO[0000] Can't find config, create a initial config file 
INFO[0000] Can't find MMDB, start download
INFO[0007] Mixed(http+socks) proxy listening at: 127.0.0.1:7890
ERRO[0007] Start DNS server error: missing port in address

Ctrl + C 退出 clash,cd ~/.config/clash 进入 clash 存配置的文件夹,并编辑配置文件 nano config.yaml,仅需添加一行 allow-lan: true

编辑完成后的配置文件如下所示。第一行表示 clash 的 HTTP(S) / SOCKS 代理端口为 7890,第二行表示允许局域网设备连接。

1
2
mixed-port: 7890
allow-lan: true

接下来就可以测试 clash的代理是否生效了。

1
2
3
sudo systemctl start myvpn # 启动 openconnect,如果没有启动的话
cd ~/clash
./clash

接着切换到个人设备上使用浏览器进行测试。打开 chrome 并安装 Proxy SwitchyOmega 插件(https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif)。安装完成后点击插件图标 - Options - 左侧 Profiles - proxy,然后添加 Proxy servers

  • Scheme - (default)
  • Protocol - 随便
  • Server - 树莓派的静态 ip 地址
  • Port - 7890 (你在配置文件中的端口号)

再点击左侧的 Apply changes。再打开一个需要内网 ip 才能访问的网站(这里就不举例了,学校 vpn 的话直接 ACM library 搜一个要 Get Access 的文章吧),点击插件图标 - proxy,再刷新页面,即可发现论文变得可以下载。也可以开着插件访问诸如 ip.sb 之类的网站,查看自己的 ip 地址是否是内网 ip。测试完成之后,可以点击插件图标 - Direct,停止使用代理。

如果是其他应用想要使用 vpn,可以寻找有没有内置的代理设置,对其 SOCKS5 代理进行如上设置即可。

关于 3.2.4 中所说的自己没有独立子网的情况,可以为 clash 添加如下两种配置之一来解决:

  • 添加 authentication 块为 HTTP(S) / SOCKS 代理设置密码(需要注意有些应用设置代理时并不支持密码,例如 win 10 的系统代理就不可以输入密码)
  • 添加 bind-address,仅允许设定的 ip 地址可以使用此代理。这里同样要求将设备设置为静态 ip。
    更多可以参考 clone 下来的 config.yaml 或者 clash 配置文件官方文档 https://github.com/Dreamacro/clash/wiki/Configuration

接着,同样地使用 systemd 作为 clash 的守护进程。

和 openconnect 类似,将 clash 和其配置文件拷到相应位置,并启用即可

1
2
3
4
5
6
7
8
sudo cp ~/clash/clash /usr/local/bin
sudo mkdir /etc/clash
cd ~/.config/clash
sudo mv * /etc/clash
cd ~/myvpn # 这是 3.3 中从 git clone 下来的
sudo mv clash.service /etc/systemd/system
sudo systemctl enable clash # 开机启动 clash
sudo systemctl start clash # 立即启动 clash

更多关于 clash 守护进程,可以参考官方文档 https://github.com/Dreamacro/clash/wiki/clash-as-a-daemon

这样本系统就搭建完成了,vpn 的使用将不再局限于单一设备,而是扩展到局域网内所有设备了,并且不再需要每次都手动输入密码连接了。

4.2 安全措施的略微探讨

可以对 duo.py 生成的 secrets.json (保存了生成 2FA passcode 必须的密钥)的权限进行限制,这样仅有 root 可以读取。

1
2
sudo chmod 600 secrets.json
sudo chown root.root secrets.json

同时,可以禁止所有账户使用密码登录,用户账户(比如 ubuntu)使用密钥登录之类,这里就不展开了,网上教程一大把(推荐使用 ED25519 而不是 RSA)。

此外,要经常更新系统,来修复各种各样的漏洞。各种密码也要考虑经常修改。

这样做安全性有了不少提升,在一定程度上可以保证 NetID / vpn 的用户名密码和 2FA 不被泄露。

但是安全这玩意,谁敢打包票呢。

5 总结

(我觉得怪怪的,我老觉得我在写论文,先是 abstract,然后 introduction,system design,discussion,然后又 conclusion,wtf)

本文使用树莓派、openconnect vpn、iptables、duo 2FA passcode 生成脚本、systemd、Proxy SwitchyOmega和clash,构建了一个可供与树莓派同一内网设备轻松访问内网的系统,并实现了 vpn 的 24小时不间断连接。还有一个额外的 HTTP(S) / SOCKS 代理作为补充。

(不行了我要吐了)

(祝大家网上冲浪愉快!)