完整教程:用 systemd socket activation 给 Snell v5 提供监听套接字(留在主命名空间以不受隔离影响),而把 Snell 进程本身放进自定义网络命名空间,从而把所有出口流量严格走你指定的接口/路由。
适用场景:Linux(systemd ≥ 248 建议),已安装 iproute2,具备 root 权限。以下以端口 6180 举例、Snell 安装在 /opt/snell
举例。
方案总览
- 入口(监听):由
snell-server.socket
在主网络命名空间创建监听(如 0.0.0.0:6180
与 [::]:6180
)。当有连接到达时,systemd 通过 Socket Activation 启动 snell-server.service
并将已打开的监听 FD传给 Snell。
- 进程网络环境(出口):
snell-server.service
被设置为进入 snell-egress 网络命名空间运行。这样 Snell 的所有主动连接(DNS、上游/远端)均使用该命名空间内的路由和接口。
- 关键点:文件描述符可跨命名空间使用——监听 FD 在主命名空间创建,但交给位于 snell-egress 命名空间内的进程来
accept()
没问题;而 Snell 的出口连接将按它当前命名空间的默认路由/策略走你指定的出口接口。
第 0 步:准备用户/目录与 Snell
# 推荐创建独立用户
useradd --system --no-create-home --shell /usr/sbin/nologin snell
# 放置可执行文件和配置
mkdir -p /opt/snell /etc/snell
cp /PATH/TO/snell-server /opt/snell/
cp /PATH/TO/snell-server.conf /etc/snell/
chown -R root:snell /opt/snell /etc/snell
chmod 0750 /opt/snell /etc/snell
提示(关于配置中的 listen 项):
Snell 通过 sd_listen_fds_with_names()
检测到 systemd 传入的监听套接字后,会优先使用它们。通常无需在 snell-server.conf
再去绑定监听;若该项是必填,保持与 socket 单元一致的地址/端口也不会冲突(Snell 应该不会重复绑定已由 systemd 打开的 FD)。
第 1 步:创建出口命名空间(两种做法)
你可以二选一:
A. 推荐:veth + NAT 强制走指定出口接口(稳妥、无侵入)
不移动物理网卡,通过 veth 把 snell-egress
命名空间与主命名空间桥接,再在主命名空间做到指定出口接口(例如 eth1
)的 NAT,从而硬性规定 Snell 的所有对外连接都经由 eth1
。
一次性初始化命令(也会写成 systemd oneshot 单元见下一步):
ip netns add snell-egress
# 创建 veth 两端:veth-host <-> veth-snell
ip link add veth-host type veth peer name veth-snell
ip link set veth-snell netns snell-egress
# 主命名空间侧
ip addr add 172.31.0.1/30 dev veth-host
ip link set veth-host up
# 命名空间内侧
ip netns exec snell-egress ip addr add 172.31.0.2/30 dev veth-snell
ip netns exec snell-egress ip link set lo up
ip netns exec snell-egress ip link set veth-snell up
ip netns exec snell-egress ip route add default via 172.31.0.1
# 开启转发
sysctl -w net.ipv4.ip_forward=1
# 使用 nftables 做 IPv4 NAT(假设指定出口接口为 eth1)
nft -f - <<'EOF'
table ip snell_nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oifname "eth1" ip saddr 172.31.0.0/30 masquerade
}
}
EOF
(可选)IPv6:若需要,也可在 table ip6
做 NAT66 masquerade
,并为 veth 配 ULA 前缀,但 IPv6 NAT 生态差异较大,建议优先保证 IPv4 通路稳定后再做 IPv6。
B. 把物理出口接口移动到命名空间(强绑定,但侵入性强)
把 eth1
直接 ip link set eth1 netns snell-egress
,然后在命名空间内给 eth1
配置地址/默认路由/或运行 DHCP 客户端。这会让 eth1
脱离主命名空间,影响系统其他服务,请谨慎。由于复杂度和风险更高,本教程主流程以 A 方案为准;B 方案仅在你完全确定影响时使用。
第 2 步(推荐):把命名空间与 NAT 初始化写成 systemd oneshot
/etc/systemd/system/snell-netns.service
[Unit]
Description=Prepare netns and NAT for Snell egress
DefaultDependencies=no
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/bash -eux -c '\
ip netns add snell-egress 2>/dev/null || true; \
ip link show veth-host >/dev/null 2>&1 || ip link add veth-host type veth peer name veth-snell; \
ip link set veth-snell netns snell-egress 2>/dev/null || true; \
ip addr replace 172.31.0.1/30 dev veth-host; ip link set veth-host up; \
ip netns exec snell-egress ip addr replace 172.31.0.2/30 dev veth-snell; \
ip netns exec snell-egress ip link set lo up; \
ip netns exec snell-egress ip link set veth-snell up; \
ip netns exec snell-egress ip route replace default via 172.31.0.1; \
sysctl -w net.ipv4.ip_forward=1; \
nft list tables | grep -q "table ip snell_nat" || nft -f - <<EOF
table ip snell_nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oifname "eth1" ip saddr 172.31.0.0/30 masquerade
}
}
EOF'
ExecStop=/bin/true
[Install]
WantedBy=multi-user.target
第 3 步:创建 systemd Socket(入口仍在主命名空间)
/etc/systemd/system/snell-server.socket
[Unit]
Description=Snell v5 (socket-activated)
[Socket]
ListenStream=0.0.0.0:6180
ListenStream=[::]:6180
FileDescriptorName=snell_inet
ReusePort=no
NoDelay=true
[Install]
WantedBy=sockets.target
第 5 步:创建 systemd Service(进程进入 snell-egress 命名空间)
/etc/systemd/system/snell-server.service
[Unit]
Description=Snell v5 server (runs in snell-egress netns)
Requires=snell-netns.service
After=snell-netns.service
[Service]
Type=simple
NetworkNamespacePath=/run/netns/snell-egress
BindReadOnlyPaths=/etc/netns/snell-egress/resolv.conf:/etc/resolv.conf
User=snell
Group=snell
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
ProtectKernelModules=yes
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
WorkingDirectory=/opt/snell
ExecStart=/opt/snell/snell-server -c /etc/snell/snell-server.conf
Restart=on-failure
RestartSec=2s
[Install]
WantedBy=multi-user.target
第 6 步:重载与启用
systemctl daemon-reload
systemctl enable --now snell-netns.service
systemctl enable --now snell-server.socket
第 7 步:验证与排障
- 监听是否在主命名空间:
ss -ltnp | grep 6180
systemctl status snell-server.socket
- 服务是否进入了命名空间:
systemctl status snell-server.service
nsenter --net=/run/netns/snell-egress -- ip route
- 出口是否走指定接口:
ip netns exec snell-egress curl -I http://1.1.1.1
nft list ruleset | grep snell_nat -A4
tcpdump -i eth1
- 连不通/超时排查:检查 NAT、防火墙、转发、路由等。
进阶:多监听/多端口与命名 FD
[Socket]
ListenStream=0.0.0.0:6180
FileDescriptorName=snell_tcp4
ListenStream=[::]:6180
FileDescriptorName=snell_tcp6
常见坑位与建议
- 不要同时启用 Accept=yes 的 socket
- 低端口绑定权限可移除
- IPv6 出口需额外配置 NAT66 或路由
- 使用 journalctl 查看日志
最简检查清单
1) 创建 netns + veth + NAT(A 方案)
2) 写 resolv.conf
3) 写 snell-netns.service 并启用
4) 写 snell-server.socket 并启用
5) 写 snell-server.service
6) curl / tcpdump 验证出口接口