IPsec 配置备忘 Part9 - RouterOS

这篇来填一个在 Part1 的时候挖的坑,简单介绍一下怎么在 RouterOS 和 strongSwan 之间配置一个 Site-to-Site 的 IKEv2 VPN。如果你还没有动手实际配置过 strongSwan,我强烈建议你先读完至少 Part1~5 和 Part7,并且自己动手配置一个能用的 strongSwan 服务端。这样至少能保证你会配置 strongSwan,否则同时学习 RouterOS 和 strongSwan 两种配置方法会让人云里雾里。

网络结构

RouterOS 做路由器,用 DHCP 给内网设备分配192.168.50.0/24段中的 IP 地址。并且用 IPsec 的 Tunnel 模式将所有来自此 IP 段的数据转发至服务器22.22.22.22。路由器本身的 IP 并不重要。

Network topology

阅读基本 RouterOS 命令

在本文中我主要使用 RouterOS 命令来表示具体的配置,但是实际情况下调试 WebUI 会更方便。这里提供一张简图解释怎么阅读 RouterOS 的命令:
Command explanation

Flexget+systemd.timer 配置

之前一直都是手动检查每周新番并添加到 Transmission 中的,最近尝试把这一流程自动化一下。简单搜索了一下发现了 FlexGet 这个工具。支持 Transmission 也能在 Linux 下运行,就决定是它了。

安装到用户目录

ArchLinux 源里并没有这个包,又考虑到这是个 Python 程序,所以就决定直接用venv了:

1
2
3
4
5
6
mkdir ~/.config/flexget
cd ~/.config/flexget
python -m venv virtualenv
source virtualenv/bin/activate
pip install flexget
pip install transmission-rpc

配置文件

配置方法可以看教程也可以看官方文档。我就只简单贴一下。

~/.config/flexget/config.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
templates:
transmission:
transmission:
host: 'https://domain-name.com/transmission/rpc'
port: 443
username: 'your-username'
password: 'your-password'

tasks:
这里随便写:
rss: 'https://mikanani.me/RSS/...'
accept_all: yes
template: transmission
transmission:
path: '/folder/path/on/server'

Systemd 配置

~/.config/systemd/user/flexget.service
1
2
3
4
5
6
[Unit]
Description=Refresh RSS with flexget

[Service]
Type=oneshot
ExecStart=/home/recursiveg/.config/flexget/virtualenv/bin/flexget execute

用户登录后 5 分钟进行同步,然后每两小时检查一次。

~/.config/systemd/user/flexget.timer
1
2
3
4
5
6
7
8
9
[Unit]
Description=Run flexget on login then every 2hrs

[Timer]
OnStartupSec=5min
OnUnitActiveSec=2h

[Install]
WantedBy=timers.target

一些可能会用到的命令:

1
2
3
4
5
6
7
8
# 启用并运行定时器
systemctl --user enable --now flexget.timer
# 立即检查
systemctl --user start flexget.service
# 检查定时器情况
systemctl --user list-timers
# 检查日志
journalctl --user-unit flexget.service -f

在 GNS3 中使用 ArchLinux

之前在折腾 strongSwan 配置的时候需要多台电脑互相连接做测试,用实机各种不方便。比方说,我不想暴露真实 IP,就只能在发布前手工编辑配置文件,也不知道编辑过后的文件到底能不能工作。于是尝试折腾使用 GNS3 来搭建一个虚拟的网络环境。

安装 GNS3

你可以尝试从 AUR 安装 GNS3,但是我之前试了几次都不是太成功,于是还是把 GNS3 装进了 virtualenv 里:

1
2
3
4
5
6
7
8
9
# 创建并进入 virtualenv
virtualenv -p python3.9 virtualenv
source virtualenv/bin/activate
# 安装 GNS3 及 PyQt5
pip install gns3-server gns3-gui pyqt5
# 安装一些其他的依赖(可能有漏的,请根据 gns3 的出错消息自己安装)
yay -S vpcs dynamips
# 启动 GNS3
gns3

GNS3 支持好几种方式来运行虚拟网络中的“节点”,我这里使用 Docker 来运行虚拟的 Archlinux 系统。因此也需要先安装:

1
2
3
sudo pacman -S docker
sudo usermod -aG docker `whoami`
sudo systemctl start docker

同时建议安装 Wireshark 方便抓包调试。

连接因特网

进入 GNS3 创建 Project 以后就可以尝试把左侧列表里的设备往中间画布上拖了。但是你会发现不仅设备类型少的可怜而且也基本拖不上去。这时我们需要自己创建设备节点的模板。进 Preferences,在最下面的 Docker containers 里点 New,Image name 输archlinux:base-devel,其他的都默认,Adapters 可以按自己需要指定数量。

点 OK 后,左侧的设备列表里就多了一个设备。拖到画布上,右键 Start,再右键 Console 就可以看到 ArchLinux 的命令行界面了。

GNS3 默认使用 xterm 作为终端模拟器,如果你像我一样使用 Gnome Terminal,需要先去 General - Console applications 把启动终端的命令改成

gnome-terminal -t "%d" -- telnet "%h" "%p"

这个新创建的 Archlinux 节点还是空空如也的,除了基本的系统什么也没有,我们可以把它连接到实际的网络上,这样就能用 pacman 安装软件包了。先从设备列表里拖一个“Cloud”设备出来,然后点左侧边栏最下面的“Add a link”切换到连线模式,点 Cloud 节点,选择物理机上用于联网的接口,再点 Archlinux 节点,选择要连接的端口,这样一条连线就接好了。Archlinux 节点被直接桥接到了外部接口上,和宿主机位于同一网段。

由于 Archlinux 的镜像不带 dhcpcd 无法自动获取 IP,只能手动设置一下咯:

ip link set eth0 up
ip addr add 192.168.1.222/24 dev eth0
ip route add default via 192.168.1.1 dev eth0
echo nameserver 1.1.1.1 > /etc/resolv.conf

不过实际测试以后发现这个速度实在是很残念,可能还是要写 Dockerfile 把镜像配置好再用才行。

IPsec 配置备忘 Part8 - iOS 客户端

⚠ 天坑预警 ⚠

我在 iOS 12/13/14 各一台设备上测试过,但是测试的时候不是次次都能工作。由于 iOS 的坑实在太多,以及不同版本的 iOS 的行为都不太一样,如果你碰到了我没碰到的坑我只能祝你好运 (<ゝω・)☆

接 Part6。折腾完 Android 以后我们来折腾一下 iOS。基本原理都是一样的,只是需要把苹果那一套 Vendor-specific 的配置选项翻译成 strongSwan 的,这样才能和服务器上的 strongSwan 互相通信嘛。由于 iOS 上的 IKEv2 客户端不是 strongSwan 而是苹果自己魔改的不知道什么版本,所以坑比起 Android 客户端更多。幸好 strongSwan 项目已经帮我们都踩了不少坑了:见 iOS (Apple iPhone, iPad…) and macOSIKEv2 Configuration Profile for Apple iOS 8 and newer

坑坑坑

由于本 Part 是基于 Part6 的,所以我们先来列举一下会需要我们做调整的 iOS 的坑(大部分在 strongSwan 的文档里已经提到过了):

  1. iOS 的 local_id 和 remote_id 全部都是 FQDN 类型,意味着我们需要按照 Part7 的说明给证书加上dns_name的 SAN。
  2. iOS 不允许 p12 证书使用空密码。
  3. iOS 12 还不支持 ed25519,所以我们需要改用其他算法。
  4. iOS 的 mobileconfig 全是坑。
  5. iOS 需要在配置描述文件显式指定 CA 的 CN 才会发送 CERTREQ。
  6. 你需要在服务端配置文件的 pool 里指定一个 DNS,否则 iOS 连上后无法上网。

基本流程是:生成密钥和证书;生成 iOS 的mobileconfig配置描述文件;想办法把这个文件安装到 iOS 设备上;最后尝试连接。由于 iOS 的配置文件比 Android 的复杂许多,所以我写了个 Python 脚本来负责生成,如果你用 macOS 也可以从苹果下载官方的配置工具。

IPsec 配置备忘 Part7 - 证书 II

接 Part2 与 Part6。尝试折腾一下除了“以证书 DN 作为 id”以外的证书使用姿势,如果有错误或者与本文解释相冲突的情况欢迎在评论指出。

IKEv2 证书认证

在 IKEv2 里,一方需要认证另一方基本只需要两样信息IDAUTH。ID 就是之前配置文件里反复出现的那个,AUTH 一般是使用证书私钥对某些数据的签名,具体细节请参考 RFC。证书则是把两者关联了起来:如果对方给出的 AUTH 能通过证书验证,那么对方就是这份证书所表示的那个人,同时这份证书又是颁发给 ID 的,那么对方就是 ID。

验证者要取得对方的证书有几种方式:

  • 管理员可以直接把证书塞进验证者的/etc目录里
  • 被验证者可以自己把证书发送给验证者(send_cert = always参数)
  • 验证者可以向被验证者请求证书(send_certreq = yes参数)。当然被验证者也可以选择不发送证书(send_cert = never参数),只是验证会失败就是了。

另外如果验证者既不询问证书(send_certreq = no),被验证者也不主动发送证书(send_cert = ifasked),验证一样会失败。

strongSwan 在尝试匹配 ID 和证书的时候会检查 Subject DN 和 SubjectAltName (SAN)。我们之前一直在使用 Subject DN,而 SAN 则允许我们使用域名甚至 IP 作为 ID。另外,虽然我一直称呼“域名”或是“IP”,但是实际上只要 SAN 和 ID 匹配即可,这个“域名”到底是不是我们的并没有关系。(当然只有自签才能签出这种证书)

IPsec 配置备忘 Part6 - Android 客户端

接 Part4,终于要开始准备搭一个能用的 VPN 服务器了。另外,如果你要拿这个配置去翻墙请自便,但是不对实际效果做任何保证,本文只考虑传统的 VPN 使用场景。

基本上,服务端的配置和 Part4 中的一致,这里着重介绍如何配置客户端。配置使用 Play Store 上的 strongSwan客户端,版本为 2.3.1。

服务器配置

首先,这个客户端似乎还不支持 ed25519 证书,因此我们需要改为使用 ecdsa 证书。完整的证书生成操作请看 Part2,在生成私钥时指定--key-type ecdsa即可。现在假设你已经生成好了所有 6 个文件:ca-key.pem,ca-cert.pem,client-key.pem,client-cert.pem,server-key.pem,server-cert.pem

另外,这次我们会在服务器上配置 NAT,所以不再需要手动在服务端配置一个 IP 了。假设服务端eth0接口的公网 IP 是2000::1。首先将ca-cert.pem,server-key.pem,server-cert.pem三个文件移动到服务器上正确的地方。然后编写配置文件,同样的,这里只注释和 Part4 中不同的地方。

server.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
connections {
android-connection {
version = 2
# 服务器的地址。根据官方文档,服务器内核版本需要大于等于 5.8
# 才能支持 Android 客户端使用 IPv6 地址连接
local_addrs = 2000::1
# 接受客户端来自任何地方
remote_addrs = %any
proposals = aes256gcm128-sha512-x25519
# 只分配 IPv4 的虚拟 IP
pools = ip4pool
local {
# 请按照实际情况修改 id
id = CN=server common name
auth = pubkey
}
remote {
id = %any
auth = pubkey
}
children {
child_sa {
# 允许客户端将去往所有目的地的流量发送给服务端
local_ts = 0.0.0.0/0
remote_ts = dynamic
mode = tunnel
esp_proposals = aes256gcm128-sha512-x25519
}
}
}
}

pools {
# 只保留 IPv4 池
ip4pool {
addrs = 10.10.10.100-10.10.10.150
}
}

配置完毕后systemctl restart strongswan

客户端配置

在客户端上,我决定创建一个可以直接导入的 Profile 文件,避免手工输入一大堆地址(要记得我在用 IPv6)。首先需要把 client-key.pem 和 client-cert.pem 合并成一个文件,注意 bash 本身是不支持多行命令和注释穿插写的,在执行的时候需要手动移除注释:

1
2
3
4
5
6
7
8
9
10
11
certtool \
--load-privkey client-key.pem \
--load-certificate client-cert.pem \
# 名称随意
--p12-name "Key Cert Bundle" \
# 空密码
--empty-password \
# 合并成 PKCS12 格式
--to-p12 \
# 需要指定 3des-pkcs12,否则 Android 系统无法正确导入
--pkcs-cipher 3des-pkcs12 > client-p12bundle.pem

新生成的文件是client-p12bundle.pem。然后我们参照 strongSwan 的文档创建可以直接导入应用的 Profile 文件。当然你也可以手动导入证书再手动配置 Profile。

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
jq -n \
# 生成一个随机的 UUID
--arg uuid `uuidgen -r` \
# Profile 的名称,可以随意写
--arg name "My server"\
# 认证类型
--arg type "ikev2-cert"\
# 需要手动指定 remote id,否则服务器的地址将被当作 id,导致认证失败
# 当然你也可以在创建证书的时候就把服务器地址写在证书里,避免手工指定 DN
# 注意:id 和服务器地址和证书有复杂的检验关系,填写不当容易造成各种奇怪的
# 身份验证错误。
--arg remoteid "CN=server common name"\
# 服务器的地址,我这里使用 2000::1 代替了实际地址,使用 IPv4 和域名也是可以的。
--arg addr "2000::1"\
# 嵌入服务器证书,注意这里我直接嵌了服务器本身的证书。嵌 CA 证书应该也是
# 可行的,但是我没试。
--arg servercert "`sed '/-----/d' server-cert.pem`"\
# 嵌入客户端私钥和证书
--arg localbundle "`sed '/-----/d' client-p12bundle.pem`"\
'{
uuid:$uuid,
name:$name,
type:$type,
remote:{
id:$remoteid,
addr:$addr,
cert:$servercert
},
local: {
p12:$localbundle
},
"ipv6-transport": true
}' > profile.sswan
# 根据官方文档,服务器内核版本需要大于等于 5.8 才能支持客户端使用 IPv6 地址
# 连接。同时需要设定 ipv6-transport 为 true。如果你使用 IPv4 则不需要此设置。
# 另外这里没有指定 local.id,根据文档,在没有指定 local.id 的情况下,客户端程序
# 会自动使用客户端证书的 DN 作为 id

最后将生成的profile.sswan文件拷贝到手机上并导入即可。然后试一下能否成功建立连接。

NAT 配置

然后这样配置完了,虽然能连上服务器,但是依然不能访问网络,我们还需要在服务器上配置一下 NAT。一般是一条防火墙的masquerade规则。这里只简单把配置放出来给自己做个备忘,不做细讲:

nftables.conf
1
2
3
4
5
6
7
8
9
10
table ip nat {
chain prerouting {
type nat hook prerouting priority 0;
}

chain postrouting {
type nat hook postrouting priority 100;
ip saddr 10.10.10.0/24 meta oifname eth0 masquerade
}
}