利用网络命名空间实现强制代理

我为这篇文章想过几个不同的标题,包括“Linux 免 Root 代理 Steam 流量”,但我打算提及多种不同的方案,同时,适用的应用也不止 Steam。

正如标题所述,强制应用走代理利用了 Linux 内核的“网络命名空间”功能。下面贴上 manpage 的翻译版:

Linux 网络命名空间提供与网络相关的系统资源的隔离,这些资源包括:网络设备、IPv4 和 IPv6 协议栈、IP 路由表、防火墙规则、/proc/net 目录( /proc/PID/net 的符号链接)、 /sys/class/net 目录,/proc/sys/net 下的各种文件,端口号(sockets)等等。 此外,网络命名空间也隔离了 UNIX 域抽象套接字命名空间(参见 unix(7))。

使用 unshare 命令可以很方便的创建一个命名空间,与 ip netns 相比,unshare 不需要 root。

同时,我期望代理功能不需要 root 权限即可使用,这里就要用到 slirp4netns,即 slirp for network namespace ,slirp 是一个用户态的 TCP/IP 实现。 slirp4netns 的原理就是把网络命名空间内的各种链接,通过 TAP 设备,交给 slirp ,变成用户态的普通套接字。如果你使用过 rootless Podman,你的电脑上多半已经安装了 slirp4netns

另外,本教程涉及命名空间(host 与 namespace)和多个终端( pts/x ),我会用不同的命令提示符标明。

创建一个命名空间

打开一个新终端:

(host@pts/1)$ unshare --net --map-current-user --keep-caps --mount bash
(namespace@pts/1)$ echo $$ > /tmp/nsproxy_$USER.pid

注意,这里 unshare 不仅仅创建了网络命名空间,也创建了用户命名空间与挂载命名空间。

--map-current-user 选项创建用户命名空间,将用户的 capabilities 与外界隔离开,同时也将目前用户的 UID 与 GID 放进去。

--keep-caps 则把命名空间的 capabilities 交给子进程(也就是后面的 bash ),这样的话能以非 root 权限执行网络相关的修改。

--mount 选项创建挂载命名空间,因为有些时候需要覆盖一些主机上的文件,可以直接 mount --bind

后面的 echo 把进程的 PID 写入文件,以便 slirp4netns 使用(不然 slirp4netns 怎么知道要给哪个命名空间开 TAP?)

实现代理

ProxyChains

假定你已经配置好 proxychains4 ,打开另一个终端:

(host@pts/2)$ proxychains4 slirp4netns --configure --mtu=65520 $(cat /tmp/nsproxy_$USER.pid) tap-ns

测试一下(如果没成功就试试 curl http://1.1.1.1 ):

(namespace@pts/1)$ curl http://google.com
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

这种方法简单,但缺陷也明显。

ProxyChains 不处理 UDP 套接字。不仅仅是一般的 UDP 连接不会通过代理,DNS 请求不会被转发到代理上面,所以 DNS 污染依旧。这样的话,网络就会表现的玄学(部分大陆 DNS 不会返回正确的 IP)。如果有自建本地 DNS (比如 smartdns)或者在本地用 V2Ray 做个端口映射的倒可以试试。

OpenVPN

假如你有一个可用的 OpenVPN 服务,也可以在命名空间内使用 OpenVPN 代理所有流量。搭建 OpenVPN 的过程在此不赘述,相关的教程网络上不少,也有 SoftEther 这种带第OpenVPN 实现且方便管理的 VPN 服务器。

当然,OpenVPN 直连是不行的,所以假设你在主机上有个 Socks 代理,位于 1080 端口,通过该端口连接到 OpenVPN 服务器。下面给出 OpenVPN 客户端的配置示例:

# 省略无关部分
proto tcp-client
socks-proxy-retry
socks-proxy 10.0.2.2 1080

proto tcp-client 考虑到大多数代理都是有可靠性层实现,而 OpenVPN 自己在 UDP 上又弄了一个可靠性层(虽然大多数载荷是没有的),用 UDP 显然是画蛇添足。

socks-proxy-retry 与 socks-proxy 10.0.2.2 1080 指定代理的地址端口,与断线重试。 slirp4netns 在命名空间内以 10.0.2.2 为 IP 提供对主机的访问,所以代理的 IP 写的是 10.0.2.2 。

打开终端2,输入:

(host@pts/2)$ slirp4netns --configure --mtu=65520 $(cat /tmp/nsproxy_$USER.pid) tap-ns

在终端1输入:

(namespace@pts/1)$ /usr/sbin/openvpn --config '~/openvpn.conf' &

应该能看到 OpenVPN 的连接信息:

XXXX-XX-XX 14:30:30 Attempting to establish TCP connection with [AF_INET]10.0.2.2:1080 [nonblock]
XXXX-XX-XX 14:30:30 TCP connection established with [AF_INET]10.0.2.2:1080
XXXX-XX-XX 14:30:30 TCP_CLIENT link local: (not bound)
XXXX-XX-XX 14:30:30 TCP_CLIENT link remote: [AF_INET]10.0.2.2:31080
XXXX-XX-XX 14:30:31 Peer Connection Initiated with [AF_INET]10.0.2.2:1080
XXXX-XX-XX 14:30:32 Initialization Sequence Completed

应用问题

DNS

覆盖 resolv.conf。因为 glibc 解析域名事会向 nscd 请求缓存,而缓存又是在主机的环境下进行的,所以这里屏蔽了 nscd 的 DNS 缓存。

(namespace@pts/1)$ echo "nameserver 1.1.1.1" > /tmp/nsproxy_$USER\_resolv.conf
(namespace@pts/1)$ mount --bind /tmp/nsproxy_$USER\_resolv.conf /etc/resolv.conf
(namespace@pts/1)$ mount --bind /tmp/nsproxy_$USER\_resolv.conf /var/run/netconfig/resolv.conf
(namespace@pts/1)$ mount --bind /tmp/nsproxy_$USER\_resolv.conf /var/run/NetworkManager/resolv.conf
(namespace@pts/1)$ mount --bind /dev/null /var/run/nscd/socket

用 iptables 强制使用指定 DNS。这里需要与主机环境隔离的 /run/xtables.lock 。

(namespace@pts/1)$ touch /tmp/nsproxy_$USER\_xtables.lock
(namespace@pts/1)$ mount --bind /tmp/nsproxy_$USER\_xtables.lock /run/xtables.lock
/usr/sbin/iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to 1.1.1.1:53

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注