利用 cgroup 在一台服务器方便地使用多公网 IP
在有些案例下(多数是类似爬虫的需求),用户会需要使用一台被分配多个公网 IP 的设备。本文提供一种利用 systemd 和 cgroups v2 方便地同时使用多个公网 IP 作为出口的方法。
本文使用 AWS 的 EC2 和 Ubuntu 18.04 作为样例。
原理
EC2 中,每个公网 IP 对应一个内网 IP。从不同内网 IP 发出的数据包将会通过不同的公网 IP 发出。系统的默认路由通常会使所有数据包的源 IP 都是首先获得的内网 IP,这带来的结果就是第二个 IP 不会被使用。为了使用不同的 IP 地址,我们只需要修改出站数据包的源 IP,这可以用 iptables 方便地实现。至于如何让 iptables 判断需要修改哪些出站数据包,就看 iptables 的筛选规则配置如何了。这里我们使用用起来比较方便的 cgroups 模块进行筛选。
方法
在 AWS 给一台 EC2 服务器分配第二个公网 IP
我们知道,新配置的 EC2 服务器默认有一个内网 IP 和一个公网 IP:
为了获取并配置新的公网 IP,我们需要:
- 给这台服务器配置第二个内网 IP (对于 IPv4,即上图中的 "Secondary private IPv4 addresses")
- 获取一个弹性公网 IP
- 将第二个内网 IP 和这个弹性公网 IP 绑定
- 在服务器配置新的内网 IP
那么我们开始:
配置第二个内网 IP
我们先需要知道这台服务器的网卡 ID(不过,如果只开个一台服务器就不必特意记下来了):
在左侧的 "Network & Security" > "Network Interfaces" 找到这个 ID 的网卡,点击右上方 "Actions" > "Manage IP addresses",展开 eth0
,点击 "Assign new IP address" 来分配一个新内网 IP,点击右下角的 "Save" 保存。保存后,新的内网 IP 会出现在之前写着 "Auto-assign" 的空位。记下这个内网 IP 地址。
获取一个弹性公网 IP
在左侧的 "Network & Security" > "Elastic IPs",点击右上方的 "Allocate Elastic IP address",获取一个新的弹性 IP。
注意,弹性 IP 通常会产生费用。
绑定内网 IP 和公网 IP
选择新拿到的弹性 IP,点击 "Actions" > "Associate Elastic IP address",在新界面选择你要分配 IP 地址的实例和新获取的内网 IP,点击下方的 "Associate" 即可。
为了方便演示起见,我们创建两个 slice,使在它们之中运行的进程分别使用两个公网 IP 地址。我们假设:
- 第一组:
- AWS 分配的第一个内网 IP:172.20.0.1
- AWS 分配的第一个公网 IP:3.4.5.6
- 设置的 cgroup 名字:a1.slice
- 第二组:
- 我们获取的第二个内网 IP:172.21.0.1
- 我们获取的弹性公网 IP:223.4.5.6
- 设置的 cgroup 名字:a2.slice
在服务器配置新的内网 IP
我们还需要让服务器知道新的内网 IP。给它的 eth0
接口(其它服务商可能不同)添加 IP:
sudo ip addr add 172.21.0.1/20 dev eth0
这里填你的新内网 IP (经常以 172 开头),后面的 /x
建议按照服务器已有 IP 的相应配置设定。
如何知道 /x
该填多少?
在服务器运行 ip addr
,可以看到类似这样的内容:
2: eth0: mtu 9001 qdisc fq_codel state UP group default qlen 1000
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
inet 172.x.x.x/20 brd 172.x.x.x scope global dynamic eth0
(...)
这里就可以看到服务器自动获取的 IP 在这里的设置是 /20
。新的 IP 一般也按这个设置就好。
创建 cgroup
临时使用的话,用 systemd-run
通过在 cgroup 中运行程序,动态创建 cgroup 即可:
# 以 uid=1000 启动 shell,顺便创建 slice:
sudo systemd-run --slice a1.slice --scope --uid 1000 -S
# 如果是直接运行程序的话:
# sudo systemd-run --slice a1.slice --scope --uid 1000 /bin/bash
# 还有 a2.slice!
sudo systemd-run --slice a2.slice --scope --uid 1000 -S
我们有两个公网 IP 可用。在实际应用中,可以为它们各创建一个 cgroup,也可以只为第二个创建一个(因为第一个默认就是使用的)。
在 cgroup 创建之后,通过 systemd-cgtop
或 systemd-cgls
可以看到新创建的 cgroup。
如果希望通过系统配置来在开机时自动创建 cgroup 的话,可以在 /etc/systemd/system
下创建 <name>.slice
文件。具体格式可以参考 system.slice 和 systemd.resource-control 的文档。
在下一步开始之前,记得确认 cgroup 已经创建,否则 iptables 会报错。
配置 iptables
我们要做的事情是修改从不同 slice 发出的数据包的源地址。看过上次透明代理那篇文的读者大概对下面的 iptables 命令感到有一点熟悉:
# 发往本地地址的不修改源地址
sudo iptables -t nat -A POSTROUTING -m addrtype --dst-type LOCAL -j RETURN
sudo iptables -t nat -A POSTROUTING -m cgroup --path "a1.slice" -j SNAT --to-source 172.20.0.1
sudo iptables -t nat -A POSTROUTING -m cgroup --path "a2.slice" -j SNAT --to-source 172.21.0.1
完成!之后在两个 slice 中运行的程序就会使用各自设置的 IP 作为出口了。