IPsec 配置备忘 Part1 - IKEv2 基础

俗话说得好,配置 IPsec 隧道只有零次和无数次,在被 strongSwan 折磨了 N 次以后,我终于决定要把之前试过的配置都记录下来,于是就有了这个系列。我计划基本上每个 PART 会介绍一个(或几个)特定场景下的配置,配置文件样例以 strongSwan vici 为主,之后可能会介绍 iOS, Android 或者是 Mikrotik 路由器的配置方法,如果我能坚持不鸽写到那里的话(画外音:你这 FLAG 立得……)。如果各位有想看的配置场景欢迎留言告诉我,会考虑先写。

IKEv2 与 IPsec 基础

严格来说 IKEv2 不是 VPN,它的全称是 Internet Key Exchange,只是一种用于交换密钥的协议罢了。密钥在计算机里一般就表示为一串固定长度的二进制数据,密钥交换就是指在两台设备之间约定一个相同的二进制串,就像两个密友之间约定暗号一样。一旦密钥交换完毕,IKE 的使命就结束了,具体怎么用约定好的密钥加密数据不是 IKE 解决的问题。在 Linux 系统上,实际的数据包加密解密是由内核的 XFRM 框架负责的,你可以使用ip xfrm命令看到配置好的密钥以及加解密使用的算法。事实上,不使用 IKEv2 而完全手动“交换”密钥是可行的,比如朴素VPN:一个纯内核级静态隧道。你可以看到作者直接使用ip xfrm {policy,state} add指令设定密钥,然后内核就会自动用设定的密钥加密流量。

然而,手动管理内核状态是复杂的,人工分发密钥也不怎么安全,这时就轮到 strongSwan 登场啦(或者说,任何实现了 IKE 的 Daemon 服务)。两台服务器的 strongSwan 使用 IKEv2 协议交换密钥,解决了密钥分发的问题。随后 strongSwan 会把交换得来的密钥设定进内核,这样内核就会自动加密指定的流量了。

从数据包层面上看,IKE 是7层协议,密钥交换使用特殊的 UDP 包完成。而一般被加密的数据包会使用 ESP 封装,ESP 头一般紧跟在 IP 头后。ESP 也可以被封装进 UDP 用以穿越 NAT。

没有 TUN 设备

内核 XFRM 的工作方式和基于 TUN 设备的 VPN 很不一样。一般基于 TUN 的 VPN 会加密所有进入 TUN 设备的流量,因此你可以直接使用路由表来控制哪些流量走 VPN,哪些不走。而 XFRM 的匹配基于策略(i.e. 源地址+目标地址+一些别的),如果某个数据包匹配到了一个策略,这个数据包就会根据这个策略指定的方式被加密。

比方说有A [fd00::1]B [fd00::2],如果你从 A 发送一个数据包到 B,普通情况下这个数据包是明文的。如果你在 A 配置了src=fd00::1,dst=fd00::2,encrypt=<...>的策略并再发一个数据包,这个包就会自动被加密。B 收到了这个数据包,但是它并不知道该如何解密,所以你必须同时在 B 配置一条src=fd00::1,dst=fd00::2,decrypt=<...>的策略,这样 B 才能解密。对于从 B 到 A 的流量也需要类似的两条策略。使用 IKEv2 的话,这些策略 strongSwan 都会自动帮你设置好,无需操心。于是你会发现,尽管我们仍然在使用节点本身的 IP,但是流量却已经被加密了。

对于那些必须使用路由表或是策略匹配不是很有效的场景, Route-based IPsec VPN 也是存在的。我也许会在未来的某一期讲。

一些 IKEv2 的细节

IKEv2 除了交换密钥以外,还负责包括身份认证,协议协商等一系列其他工作。实际使用的时候,我们一般需要指定这些参数:

  • 对方的 IKE 服务器地址
  • 自己的身份标识符
  • 能接受的对方的身份标识符,可选。
  • 对自己身份标识符的证明,一般是 PSK (预共享密钥) 或者是证书。
  • 对对方身份标识符证明的验证方式,比方说,如果对方使用证书认证其身份,则可以通过检查 CA 证书链的方式来证明其证书的有效性。
  • 自己能接受的 Cipher suite
  • Local traffic selector
  • Remote traffic selector

以上所有这些参数需要在两端都配置。其中,cipher suite 需要至少指定两次,原因是 IKEv2 是一个两阶段协议,两阶段使用的 cipher suite 可以不同。在第一阶段会简单地进行一次 DH 密钥交换,建立 IKE_SA,然后进入第二阶段。二阶段中的身份认证,traffic selector 协商等均会被加密。在二阶段 IKE 会建立 CHILD_SA,也是用来加密实际数据的 SA,CHILD_SA 的协商结果,包括密钥,加密算法,traffic selector 等均会被设定入内核,以便内核进行实际的加密操作。

Traffic selector 决定了内核匹配数据包的策略,即,哪些数据需要被加密。比方说,一个 VPN 客户端可能会设定 local_ts=10.0.0.14/32, remote_ts=0.0.0.0/0 这意味着该客户端希望所有流量都被加密。而一个服务器可能会设定 local_ts=<所有非中国大陆IP>, remote_ts=10.0.0.14/32,这说明该服务器不希望处理去往中国大陆 IP 的流量。如果这两者进行协商,结果客户端就不会将去往中国大陆 IP 的流量发送给服务端。这也被称作 Split Tunneling

简单 Host-to-Host 配置

这是一份简单的 Host-to-Host 配置样例,场景和没有 TUN 设备中描述的一致。我在本地的两台物理机上进行配置,两台机器使用网线直连,并用ip addr add fd00::{1,2}/64 dev <name>手动配置 IP。认证方式使用 PSK。在 Archlinux 上,配置文件位于/etc/swanctl/swanctl.conf。使用sudo systemctl start strongswan来启动 strongSwan。

这是 HostA 的配置:

hosta.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
connections {
conn_hosta_hostb {
# IKEv2
version = 2
# 本地 IKEv2 服务地址
local_addrs = fd00::1
# 对方 IKEv2 服务地址
remote_addrs = fd00::2
# IKE_SA 的 cipher suite
proposals = aes256gcm128-sha512-x25519
local {
id = hosta # 己方身份标识符
auth = psk # 己方身份标识符的证明方式
}
remote {
id = %any # 对方身份标识符,这里接受任意标识符
auth = psk # 对方身份标识符证明的验证方式
}
children { # 列出需要建立的 CHILD_SA
child_sa { # 只有一个 CHILD_SA, 叫做 "child_sa"
local_ts = fd00::1/128 # Local traffic selector
remote_ts = fd00::2/128 # Remote traffic selector
mode = transport # 使用 Tansport 模式而不是 Tunnel 模式
# CHILD_SA 的 cipher suite, 其实这里用的和 IKE_SA 的是一样的
esp_proposals = aes256gcm128-sha512-x25519
}
}
}
}

secrets {
ike_hosta {
# 发送己方身份时,使用这个 PSK
id = hosta
secret = pwd_for_hosta
}
ike_hostb {
# 对方发送的身份标识符是"hostb",会匹配到这个 PSK
# 实际操作中一般不这么写,而是把两个 id 写到同一个 ike_* 块中,共用 secret。
id = hostb
secret = pwd_for_hostb
}
}

HostB 的配置,几乎一样:

hostb.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
connections {
conn_hosta_hostb {
version = 2
local_addrs = fd00::2 # IP 地址交换一下
remote_addrs = fd00::1
proposals = aes256gcm128-sha512-x25519
local {
id = hostb # 标识符改一下
auth = psk
}
remote {
id = %any
auth = psk
}
children {
child_sa {
local_ts = fd00::2/128 # IP 地址交换一下
remote_ts = fd00::1/128
mode = transport
esp_proposals = aes256gcm128-sha512-x25519
}
}
}
}

secrets {
ike_hosta {
id = hosta
secret = pwd_for_hosta
}
ike_hostb {
id = hostb
secret = pwd_for_hostb
}
}

链接测试

测试之前需要先检查一下 IP 掩码的长度,如果和我一样使用/64的话,可能会受到bypass-lan插件的干扰,导致数据不被加密。需要去/etc/strongswan.d/charon/bypass-lan.conf把它关掉。然后重启 strongSwan 即可。由于我们没有配置自动连接,所以启动 strongSwan 后数据流还是未加密的:

23:50:29.873728 IP6 (flowlabel 0xe3038, hlim 64, next-header ICMPv6 (58) payload length: 64) fd00::1 > fd00::2: [icmp6 sum ok] ICMP6, echo request, seq 1
  0x0000:  600e 3038 0040 3a40 fd00 0000 0000 0000  `.08.@:@........
  0x0010:  0000 0000 0000 0001 fd00 0000 0000 0000  ................
  0x0020:  0000 0000 0000 0002 8000 02ec 001d 0001  ................
  0x0030:  15ed 9c5f 0000 0000 0457 0d00 0000 0000  ..._.....W......
  0x0040:  1011 1213 1415 1617 1819 1a1b 1c1d 1e1f  ................
  0x0050:  2021 2223 2425 2627 2829 2a2b 2c2d 2e2f  .!"#$%&'()*+,-./
  0x0060:  3031 3233 3435 3637                      01234567

需要使用 swanctl 手动建立连接:

sudo swanctl -i -c child_sa

你应该能看到 IKEv2 的四个 UDP 包,然后再 ping,数据就是加密的了:

23:51:18.731508 IP6 (flowlabel 0xe3038, hlim 64, next-header ESP (50) payload length: 100) fd00::1 > fd00::2: ESP(spi=0xc704dbda,seq=0x1), length 100
  0x0000:  600e 3038 0064 3240 fd00 0000 0000 0000  `.08.d2@........
  0x0010:  0000 0000 0000 0001 fd00 0000 0000 0000  ................
  0x0020:  0000 0000 0000 0002 c704 dbda 0000 0001  ................
  0x0030:  dc51 b5d7 bef1 bce6 da9d 74b2 7e6c 482d  .Q........t.~lH-
  0x0040:  d9db 6e37 24d6 9fc6 10bb 525c e308 bc76  ..n7$.....R\...v
  0x0050:  9d26 74d6 64ff ef55 5a54 5f95 94c7 01cf  .&t.d..UZT_.....
  0x0060:  2ae7 51b2 db41 439b 4d37 1f1e 3075 74d1  *.Q..AC.M7..0ut.
  0x0070:  25dc 2990 8c07 b484 a37e b052 e5fc 8709  %.)......~.R....
  0x0080:  e229 c79d 0816 0ae5 5c8b 652f            .)......\.e/

strongSwan 调试技巧

在 Archlinux 上,strongSwan 的日志级别控制在/etc/strongswan.d/charon-systemd.conf。另外,strongSwan 支持 NULL 加密(即不加密)以方便调试,将proposalsesp_proposals修改为如下值即可:

proposals = null-sha-modp2048
esp_proposals = null-sha-modp2048

然后在 Wireshark 中选中 Attempt to detect/decode NULL encrypted ESP payloads 即可直接查看数据包内容: