去年我介绍过一种 Linux VPS 共享方案1,核心思路是为每个用户创建非特权账号并且创建独立的网络名称空间,然后给每个用户单独分配 IPv6 地址,从而构成纯 IPv6 的试验环境。经过一年多的实践,我发现还有一种更简单也更好玩的思路,今天分享给大家。
关于方案选择相关的内容本文不作赘述,有兴趣的朋友读读去年的文章。本文的核心优化思路为利用 IPv6 的本地链路地址+代理服务实现网络隔离和管理。
libpam-net
网络空间隔离还是用 libpam-net 项目。但在 7.0 内核加载会报如下错误:
Jul 21 05:46:45 host (systemd)[1696]: PAM unable to
dlopen(/usr/local/lib/security/pam_usernet.so):
/usr/local/lib/security/pam_usernet.so:
undefined symbol: pam_get_user
原因是编译时未链接 libpam 库。我给该项目提了一个 PR2,不知是否能合入。
编译时需要注意 CPU 指令架构。通常是 x86_64,但我的 Oracle ARM 服务器是 aarch64
cd build
cmake .. -DLIBSECURITYDIR=/lib/aarch64-linux-gnu/security编译后建议将 pam_{groupnet,newnet,usernet}.so 保存到 /usr/local/lib/security 目录。
然后在 /etc/pam.d/common-account 最后添加如下两行:
session optional /usr/local/lib/security/pam_usernet.so group=zz
session optional pam_exec.so stdout /usr/local/sbin/veth.sh
第一行是为 zz 组的每个用户新创建独立的网络空间,之后用户所有的操作都只能在自己的网络空间执行。
新创建的网络空间只包含 lo 回环网卡,无法跟外界通信。为此,我们需要创建 veth 虚拟网卡设备,将一端留在用户的网络空间,另一端挪到默认的网络空间,这样用户才能跟宿主机上的其他服务进行通信。
为此,我们通过 pam_exec 模块执行 veth.sh 脚本,来完成 veth 虚拟网卡相关操作。
#!/usr/bin/env bash
# 只在激活用户或者切换到用户的时候才执行
if [[ "$PAM_TYPE" != "open_session" ]]; then
exit 0
fi
# 提取当前登录的用户名
USER=${PAM_USER:-$1}
# 不属于 zz 组则退出
if ! groups $USER|grep -qE '\bzz\b'; then
exit 0
fi
# 对应的用户网卡已存在则退出
if ip link show dev eth0 > /dev/null; then
exit 0
fi
# 创建 veth 虚拟链路 ve-$USER <---> eth0
ip link add ve-$USER type veth peer name eth0
ip link set up dev ve-$USER
ip addr add fe80::1/64 dev ve-$USER
ip link set up dev eth0
ip addr add fe80::2/64 dev eth0
ip link set ve-$USER netns 1
ip link set up dev lo这里通过ip link add ve-$USER type veth peer name eth0创建的 veth 设备。注意,这个脚本是在 pam_usernet 之后执行。所以执行的时候网络空间已经切换到用户的专用空间了。但执行脚本的用户依然是 root。
我们希望将eth0这端留在用户网络空间,对应的ve-$USER放到根空间,这样就能通过设备名来区分流量来源。所以我们最脚本后面执行了ip link set ve-$USER netns 1。注意,根网络空间没有名字,只能根据 ID=1 来设置。
前面说过用户需要通过根空间的代理服务来访问外网,所以用户不需要分配 IP 地址,只要有链路本地地址就可以了。所以我把 veth 的主机端设置为fe80::1/64、用户端设置为 fe80::2/64。这样用户就可以通过fe80::1%eth0来跟宿主机通信了。注意,这里用户侧网卡固定为eth0,所以访问宿主机的地址fe80::1%eth0也是固定的。
用户激活之后 PAM 会依次创建并切换网络空间,然后执行 veth.sh 初始化 veth 网络。
到这里用户网络空间就准备就绪了。但这样只实现了用户侧跟宿主机的相互访问。为了让用户可以正常访问外部网络,同时还要让外部网络可以访问用户侧运行的服务,我们还要搭建两套代理程序。
正向代理
第一套是正向 HTTP 代理。我用的是 TinyProxy。这里仅展示核心配置:
Port 8888
Allow fe80::/64
TinyProxy 监听 8888 端口,然后允许本地链路地址网段访问代理服务。服务启动之后,用户网络空间的程序就可以通过http://[fe80:1%25eth0]:8888代理服务来访问外部网络了。
✏️Note注意这里的代理地址是
http://[fe80:1%25eth0]:8888,需要将%转义,不然可能会出出现兼容问题。
为了方便用户,我还创建了 /etc/profile.d/zz-proxy.sh 文件,给 zz 用户组的成员自动添加 HTTP 代理相关的环境变量。
if id -Gn|grep -qE '\bzz\b'; then
export http_proxy=http://[fe80::1%25eth0]:8888
export https_proxy=http://[fe80::1%25eth0]:8888
fi到这里,用户可以正常访问外部网络了。但要想让外部网络能访问用户的服务,还需要配置反向代理。
反向代理
反向代理的思路也很简单,就是跑一个 HTTP 反向代理,根据用户域名将求反代到用户对应的本地链路地址的 8080 端口。比如用户绑定的域名是 example.zz.ac,那么所有 Host 为 example.zz.ac 的子域名的 HTTP 请求都被转发给fe80::2%ve-example:8080。注意这里从域名到用户名再到网卡名的映射关系。用户的 veth 设备两端只有链路本坡地址,主机端是fe80::1/64,用户端是fe80::2/64,然后就是处理好设备名。
通常大家使用 Nginx 做反向代理。但我测试发现 Nginx 不支持在 upstream 中使用 IPv6 本地链路地址。于是便选用了 Caddy,核心配置如下:
https:// {
tls {
on_demand
}
@hostregex header_regexp host Host ^([^.]+\.)+(?P<zzname>[^.]+)\.zz\.ac$
handle @hostregex {
reverse_proxy [fe80::2%ve-{re.host.zzname}]:8080
}
}
{
on_demand_tls {
ask http://localhost:8000
}
}
其中@hostregex指令定义了host变量,值通过正则表达式从Host头中提取,模式为 ([^.]+\.)+(?P<zzname>[^.]+)\.zz\.ac$,也就是提取 zz.ac 的三级域名。比如foo.example.zz.ac 对应的zzname为example。最后通过handle @hostregex将请求转发给[fe80::2%ve-{re.host.zzname}]:8080。
这里约定用户需要自己监听 8080 端口。如果用户有多个服务,可以再跑一个 HTTP 代理并根据域名或者路径分流。
用户服务
我们推荐使用用户级 systemd 来管理服务。为了让用户服务开机自启我们需要开启 linger
loginctl enable-linger examplesystemd 的具体用法请参考这篇文章。
系统安全
多用户共享系统会带来安全隐患,需要对系统做加固。
最基础的措施是禁用 ssh 密码登录,具体配置不做赘述。然后是内核相关的配置。
首先要禁止普通用户查看 dmesg 信息:
sysctl -w kernel.dmesg_restrict = 1
然后禁止用户查看其他用户的进程,这需要修改 /proc 目录的挂载参数:
proc /proc proc rw,nosuid,nodev,noexec,relatime,hidepid=invisible 0 0
这里最重要的就是hidepid=invisible参数。重新挂载之后,普通用户运行 ps 命令只能看到自己的参数。
ubuntu 系统默认的 home 目录权限为 750,用户无法查看他人的家目录。那能不能通过 chroot 将用户限制在自己的家目录呢?答案是否定的,因为很多命令或者程序依赖 /etc, /var 等目录,使用 chroot 后就没法工作了。
除了目录权限,我们还可以设置磁盘配额,也可以通过 systemd 的 slice 来限制其他资源。具体请参考我的方案一文章3。
这些限制用户超过后不会对系统造成严重的问题,完全可以通过事后审计来追查。
好了,以上就是本文的主要内容,希望对大家能有启发。
Linux VPS 共享方案一./multi-tenant-linux.html↩︎
Linux VPS 共享方案一./multi-tenant-linux.html↩︎













