标签归档:network

iOS Surge 加载 wireguard 配置打通 tailscale

Wireguard 是新一代的轻量级、易于配置、简洁优雅的 VPN 协议。 tailscale 是底层利用了 wireguard 协议和 UDP 打洞等方式,把天南地北的主机去中心化地连为虚拟局域网的商业产品。 tailscale 有很大的免费使用额度(3 用户 100 终端,2024.2),在各大操作系统平台上都有配置简单的客户端。但在 iOS 上启动会占用独占性的 VPN 接口,和 Surge 产生了互斥。最贴身的 iPhone 设备不能接入自己的 tailscale 网络是很可惜的,于是 2024 年的春节档就着手准备解决这个问题。

这个问题有很多解决途径,iPhone 开启 tailscale app 之后借助 tailscale 的 exit node 功能把所有流量交给别的设备分流处理算一种(落选原因:5G 速度大于家宽上传),Surge 等 app 除了 wireguard 外还支持的 socks 、 ssh 等协议也算一种(落选原因:不想暴露任何 TCP 端口)。家里有一台常开的 N100 低功耗处理器小主机,装好了 Linux 操作系统,当然要充分利用这个资源,顺便还能把该主机上挂载的移动硬盘以 smb 协议共享为 “网盘” 来使用。

由于 Surge 支持 “分离配置” 特性,就算是遇到不能修改的订阅的配置也可以通过 “自定义主配置文件”加载 “订阅配置的某些段落”来实现自由定制某些规则。正是在这种情况下,只要再写一个 wireguard 专用的配置文件,变成 “自定义主配置文件”+“订阅配置”+“wireguard 专用配置” 即可,关键段落如下:

[Proxy]
#!include subscription.conf, wireguard.conf
[WireGuard name]
#!include wireguard.conf
[Rule]
IP-CIDR,10.0.0.1/32,wireguard,no-resolve #内网主机 wireguard 的对端地址
IP-CIDR,192.168.1.0/24,wireguard,no-resolve #内网主机所在的家庭内网 LAN 段,可访问家庭局域网
IP-CIDR,100.64.0.0/10,wireguard,no-resolve #tailscale 段,如果嫌大可以精确到主机/32

Surge 中 wireguard 配置和官方配置没什么差别,section-name 所指定的名字要和主配置、 wireguard 配置中 [WireGuard name]里的 name 相对应;peerallow-ips 的值如需填多个地址段要用双引号包裹,如 allow-ips = "0.0.0.0/0, ::0/0"

由于该内网小主机一开始经考虑被设置为内网设备而非软路由,所以在(有公网 IPv4 地址的)主路由上通过转发规则把公网来的 wireguard 的 UDP 包转发到内网小主机。主机上通过 wg 自带的命令生成公私密钥对,填入 Surge 的 wireguard 配置中,wg-quick up wg0 启动服务后 iPhone 即可访问主机上的服务,连接成功。

2024.3.8 更新方案:通过主路由器端口转发的方式把公网来的 UDP 转发到小主机,更新为直连小主机的 ipv6 地址(在主路由中用 ip6tables 放行对应端口)。使用 ddns 工具同时更新主机的 ipv6 、 ipv4 地址,ipv4 则是主路由器的公网 ipv4 地址并 fallback 到上述端口转发方案。

到这里只实现了 wireguard 的两个对端互连,要让 iPhone 访问内网主机所在的家庭局域网 192.168.1.0/24,除了上文中提到的在 Surge 主配置文件 [Rule]中将指定网段流量导向 wireguard,还要在内网主机上通过 iptables 让内网主机把 wireguard 流量转发到家庭局域网。在网上的很多 wireguard 教程里,内网主机端的 PostUp(也就 wireguard 服务启动后执行的命令)是这样写的:

iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT;
iptables -A FORWARD -i eth0 -o wg0 -j ACCEPT;
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;

这三句话可能以不同的形式(网卡名称,地址段等)出现,但实质相同,实现了内网主机 wireguard 网卡 wg0 、物理网卡 eth0 互联互通和 NAT 的规则。如无特殊,PostDown 则是写入相对应的 iptables 删除命令(把-A 换成-D)。重要的是开启 Linux 内核包转发,编辑 /etc/sysctl.conf, 将 net.ipv4.ip_forward 的值从 0  改为 1 。然后 sysctl -p  生效。

在解决了访问内网主机物理局域网段的访问问题后,我想进一步访问内网主机所在的 tailscale 虚拟局域网,实现方法也很简单并可以按照以上步骤如法炮制:把上述 iptables 命令中的物理网卡 eth0 改为 tailscale 的虚拟网卡名 tailscale0 ,再写三句命令追加在 PostUp 的后面,PostDown 也同样追加,重启 wg0 即可生效。最后 systemctl enable wg-quick@wg0wg0 加入开机自启动服务。

最后,幸亏路由器支持 NAT Loopback(又称 NAT hair pinning),也就是 IPv4 的网关会自动识别并修正 “目标写着路由器公网 IP,实际上得往内网去” 的请求,使得以上所有基于 “iPhone 在公网” 的设定到了内网也能正常运作。(如不支持 Loopback 可能需要内网自定义 DNS,使得目标服务器在公网解析出公网 IP,在内网解析出内网 IP)最终结构如下:

NAT 二三事

本篇着实算不上科普或教程,只能算是心得或随笔吧。

二〇〇几年家里刚通宽带的时候,内网两台电脑(台式机、笔记本)要同时上网。没买路由器之前,台式机在用两块网卡进行连接共享,把入户宽带共享给笔记本。后来购置了路由器,很快就学会了端口映射,想把笔记本的文件通过 FTP 共享到公网,只需在路由器中设置端口映射,将公网 IP 地址的 21 端口映射到内网笔记本 IP 地址的对应端口即可。那时候也不太懂转发的概念,只知道此时 “公网 IP 的某端口等价于内网 IP 某端口” 。

eMule 也可以通过以上方法来把内网 IP 的两个端口暴露到公网来获取 HighID 。但是另一些 p2p 软件可能会通过 UPnP 功能来自动完成以上步骤。 PnP(即插即用)在当时对我来说就是 windows 插上 USB 设备无需装驱动即可正常使用的概念,那么我就联想到 UPnP(通用即插即用)就是内网设备自动向路由器申请开通一些上述端口的映射规则,直到今天(2022 年)都在大量使用,几乎是家用路由器的标配。

就这样过了很长时间,在近几年家用游戏机流行后,我突然在网上了解到 “NAT1” 的概念,也就是家用主机在内网运行时和其他玩家直接建立连接的困难程度。 ipv4 年代大家基本上都在 NAT 后面上网,加之绝大多数游戏都是基于 UDP,那么在自家 NAT 后面和同样在 NAT 后面的其他人建立连接竟然变成了一件有点复杂的事,涉及到 “UDP 打洞” 。下载 NatTypeTester 工具一试,家里的 Merlin 固件路由器带来的 NAT type 竟然是最无趣 “Symmetric”,而 padavan 固件就提供选项可以把 NAT type 设最宽松的 type “full cone”(RFC 3489 标准,最新的 RFC 5780 标准搞得愈发复杂也没什么人用)

我认为这篇博文把 NAT type 讲得很清楚,还说明了利用生日攻击去碰撞端口的方法,很有意思。我对 STUN NAT type 的理解是这样:

假设内网的 src_lan_ip:src_lan_port 欲向公网 dest_ip:dest_port 发送 UDP 包,发到 NAT 网关处,NAT 执行 SNAT 改写,挑一个可用端口 src_wan_port,在公网以 src_wan_ip:src_wan_port 发向 dest_ip:dest_port 。

  • full cone:在 NAT 处存在 src_lan_ip:src_lan_port 到 src_wan_ip:src_wan_port 的映射之后,src_lan_ip:src_lan_port 发往任意别处 dest 的数据都会从原始 src_wan_ip:src_wan_port 出去。这样由点(原始映射)到面(任意公网目标),用几何图形来形容就像一个圆锥,因此称为全锥形 NAT 。此后,在这个映射关系的生命周期内公网上的任何 ip:port 都可以反过来向 src_wan_ip:src_wan_port 发包,会被转回 src_lan_ip:src_lan_port 。
  • Restricted cone:原始 src_lan_ip:src_lan_port 发往任意别处 dest 的数据都会从原始 src_wan_ip:src_wan_port 出去。但是,该映射的生命周期内,只有 dest_ip(端口不限)才能向 src_wan_ip:src_wan_port 发包,会被转回 src_lan_ip:src_lan_port 。(与 full cone 相比,限制了外部 IP)
  • Port Restricted cone:原始 src_lan_ip:src_lan_port 发往任意别处 dest 的数据都会从原始 src_wan_ip:src_wan_port 出去。但是,该映射的生命周期内,只有 dest_ip:dest_port 才能向 src_wan_ip:src_wan_port 发包,会被转回 src_lan_ip:src_lan_port 。(与 Restricted cone 相比限制了 port,与 full cone 相比限制了 IP+port)
  • Symmetric: 原始 src_lan_ip:src_lan_port 发往 dest1 的包会被映射为 src_wan_ip:src_wan_port1,发往 dest2 的包会被映射为 src_wan_ip:src_wan_port2,以此类推(此处以单 WAN 口为例,如果是多个 WAN 口,一定不会出现同 ip:port 的情况)。每一个不同的目的地,都会在 wan 上出现一个新的端口连出去。这样看 WAN 内外部的 UDP 连接像是以 NAT 网关为轴 “对称” 的,所以称为 “对称型 Symmetric”NAT 。且对返回包策略与 Port Restricted 一致,严格限制 ip+port 。

从上到下,规则越来越严格,NAT 打洞也越来越困难。因为 STUN 打洞需要一个第三方服务器,为同在 NAT 后面的通信双方打洞建立连接,STUN 服务器和其中一方一定不会是同一 ip 或 port,从而被严格 NAT 的另一方以 “来源不同” 拒绝连接,所以严格的 NAT 策略会给打洞带来很大的困难。

最近在使用 tailscale 的时候,发现他们的博客 How NAT traversal works 写得非常详尽,连双方都是 CGNAT 这样令人头秃的情况也考虑到了,非常推荐一读。中文译文

tailscale 的思路比较明确,先把 UPnP 列为最优先的打洞方式,直接向 NAT 网关申请端口,省去了后面所有的麻烦。顺便,除了 UPnP,我还在命令行 tailscale netcheck 里学到了,UPnP 在发现阶段之后,通过 xml 文档向 NAT 网关申请端口映射。与 UPnP 类似的技术还有:NAT-PMP 和 PCP,NAT-PMP 由 Apple 开发,PCP 则是 NAT-PMP 的高级演化版,以基于 UDP 的通信来向 NAT 网关申请端口映射规则。