摘要:把我手上的几台机器连成一张虚拟局域网,Windows、Linux、Android 三端,引擎用 EasyTier,账号、调度、锚点、后台都自己搭。这里只记三件事:为什么有台设备死活走不了 P2P,在线状态为什么不能指望客户端心跳,CentOS 7 上跑不起来怎么办。仓库在文末。
先说这个东西是干什么的
需求很土:我自己名下几台机器,家里台式机、公司电脑、一台 NAS,加两部手机,想把它们连成一张虚拟局域网。能互相 ping 通,能联机开服,能共享文件和打印机就够了。
不好办的地方有三处:
- 开服那台没有公网 IP,我也不想为它做端口映射
- 虚拟 IP 必须固定,机器换个网络不能变地址
- 手机得能进
现在做出来的形态是:客户端登录,填一个 8 位适配码,客户端在本机拉起 EasyTier 引擎,建虚拟网卡和路由。账号、设备分配、锚点调度在服务端。
打洞和中继 EasyTier 自己都做了,我写的其实是它外面那一圈。按踩坑的先后顺序讲。
一、走不走 P2P,先看 NAT 类型
一开始我想的是两边都在国内、延迟又低,直连应该没问题。结果有台设备一直挂在中继上,速度只有几百 KB/s。查下来,它是对称型 NAT。
EasyTier 会把对端的 NAT 类型和当前链路一起报出来,用自带的 CLI 就能看:
easytier-cli-p127.0.0.1:15888-ojson peer输出里每条对端长这样(节选):
{"ipv4":"10.0.0.5","hostname":"pc-a","cost":"p2p","lat_ms":"5.45","loss_rate":"0.0%","tunnel_proto":"tcp,udp","nat_type":"SymmetricEasyInc"}cost是p2p就是直连,出现relay就是走中继了。nat_type常见的几种:
PortRestricted/Restricted:锥形 NAT,一般打得通Symmetric/SymmetricEasyInc:端口每次都换,打洞基本没戏
知道这个以后我改了两处。客户端界面上把每个对端标出来是直连还是中继,省得用户自己猜;另外把"能不能直连"和"节点延迟"拆成两个指标。创建组网时我们会并发探测各调度节点的 TCP 延迟,挑最近的那个当锚点,但锚点近不代表这两台客户端之间打得到洞。这两个指标我曾经混在一起看,结论是错的。
中继现在在锚点侧限了速。它是兜底,不是主路径。P2P 的数据不经过服务器,不受这个限制。
二、在线状态别指望客户端
一开始的判定方式很直接:客户端连上以后定时给服务端发心跳,服务端写last_seen,查询时看这个时间有没有超出窗口,我们定的是 90 秒。
Windows 和 Linux 都正常。安卓加进组网之后,界面上一直是"离线"。
查了半天,是它压根没发心跳。安卓用系统VpnService建 tun,再把 fd 交给 JNI 里的 EasyTier,跟桌面端是两条完全不同的路,写的时候漏了这一段。服务端记录里也看得出来,那台机器的last_seen是 NULL。
补心跳当然能解决,但我想的是另一件事:锚点本来就知道谁连着。所有客户端都得连锚点,信令至少要从那儿过,锚点上的对端列表比客户端自己报的准。让每个端各实现一遍心跳还得先定协议,不如直接问锚点:
# 锚点的 RPC 端口按房间号算,形如 16000 + (房间号 % 2000)easytier-cli-p127.0.0.1:16021-ojson peer拿到对端 IP 列表,跟数据库里该组网的成员按虚拟 IP 对一遍,命中的就刷last_seen。这段逻辑放在锚点的管理脚本里,跟着定时任务一分钟跑一次:
UPDATEnetwork_membersSETlast_seen=?WHEREnetwork_id=?ANDSUBSTRING_INDEX(v4_addr,'/',1)IN(?,?,...)这样客户端不用配合。安卓那个没心跳的版本没更新,Linux 的老版本也没更新,都自动显示在线了。设备真断了之后,对端会从锚点列表里消失,last_seen不再刷新,过窗口自然变离线。安卓那个坑我到现在也没回头去补心跳,因为用不上了。
三、固定虚拟 IP 交给服务端分
需求是"不删组网,IP 就不变"。实现不难,重点是别让客户端自己决定。
设备第一次加入某个组网时,服务端在成员表里给它分配一个虚拟地址写进库,顺便记下设备标识、网段和网关。之后这台机器的内网 IP 怎么变、重连多少次、客户端重装(只要设备标识还在),拿到的都是同一个地址。
客户端那边就简单了:登录、拉配置、从配置里读自己的虚拟地址、拿这个地址起引擎。不猜,也不看本地网段。
顺带说个容易搞错的点。虚拟机里跑游戏服务端,监听地址填0.0.0.0有时候收不到组网网卡上过来的连接,得显式写组网虚拟 IP。这个问题我查了很久,最后确认是虚拟机的网络栈把0.0.0.0的绑定限制在某张网卡上了。不是组网的问题,但确实踩过。
四、老系统上跑不起来,这块最费时间
有台 CentOS 7 的机器要当组网节点。客户端装完,命令行直接报错:
aipai: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by aipai) aipai: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.22' not foundCentOS 7 是 glibc 2.17、GLIBCXX 3.4.19,而客户端是 .NET 自包含程序,底线是 glibc 2.27 加 GLIBCXX 3.4.22。这不是装个依赖就能补的事,glibc 没法单独升级,硬换会把系统弄坏。
剩下的路只有容器。容器里的用户空间是新的,虚拟网卡还是建在宿主机上,效果跟直装一样:
docker run -d --name aipai --restart unless-stopped \ --network host --cap-add NET_ADMIN --cap-add NET_RAW \ --device /dev/net/tun \ -e AIPAI_USER=账号 -e AIPAI_PASSWORD=密码 -e AIPAI_NETWORK=适配码 \ -v /opt/aipai-docker/app:/app -v /opt/aipai-docker/data:/root/.config/aipai \ -w /app 基础镜像 /app/aipai connect真做起来又踩了三个坑。
第一个是镜像拉不动。国内访问 Docker Hub 基本没戏,配了加速器也可能一起挂。最后的办法是把一个精简过的 Ubuntu 镜像放到自己服务器上,官方镜像拉不到就docker import导进去:
dockerimportaipai-base.tar.xz aipai-base:jammy原镜像解包 684MB,删掉包索引、Python、vim、文档和本地化之后剩 211MB,压成 xz 是 50MB。容器里跑 .NET 需要的东西一个都不能少:glibc、libstdc++、libssl.so.3(HTTPS 要用)、根证书。这几个我是挨个 ls 确认过的。
第二个坑更隐蔽。我给容器加了--restart unless-stopped,它崩了会自动拉起。启动脚本检查状态那一瞬间,容器恰好是 Running,于是脚本报了"已启动",可日志里正在刷Failed to create CoreCLR。
容器健康不能只看一眼状态。后来改成看三样:日志里有没有致命错误、RestartCount是不是 0、日志里能不能看到"已接入"这类成功标记。
dockerinspect-f'{{.State.Running}} {{.RestartCount}}'aipai第三个是老版 Docker 拦住了 .NET。CentOS 7 的源里是 Docker 1.13(2017 年的版本),它默认的 seccomp 策略会拦掉 .NET 运行时需要的系统调用,报出来的还是Failed to create CoreCLR, HRESULT: 0x80070008。这个错误码字面意思是内存不足,实际是 syscall 被拦了。老内核上 .NET 的 W^X 也起不来,一起关掉:
dockerrun... --security-optseccomp=unconfined\-eDOTNET_EnableWriteXorExecute=0-eDOTNET_gcServer=0\-eDOTNET_GCHeapHardLimitHex=0x14000000...还有一点:CentOS 7 的官方源 2024 年就下线了,yum install会卡在失效的镜像上很久,得先切到 vault 归档源。这个我在安装脚本里自动处理了,不处理的话用户看到的就是"命令敲下去没反应"。
五、几个现在回看还算合理的取舍
组网内所有端口互通,这是产品要的效果,开服不用做端口映射。代价是用户协议里必须写清楚:不能拿来对外暴露 80/443,不能建站,不能转租通道。技术上能做到跟使用边界是两件事,能写在协议里就别等出事再补。
中继一直留着,但我没把它当卖点讲。对称型 NAT 打不通的时候总得有条退路,不然用户直接判定软件是坏的。它限速、不承诺可用性,因为它就是兜底,不是线路。
收费方式也从卖注册邀请码改成了按月收技术服务费。邀请码那套,用户理解不了自己到底买了什么。现在写的是账号与设备管理、打洞调度、控制台、售后,口径清楚,也避开了"卖流量"这种怎么解释都别扭的说法。
最后
三端源码都在 GitHub 上:
https://github.com/wenzaiclub/aipai-link-clientWindows 是 WPF,Linux 是命令行加一个网页面板,Android 用 VpnService 加 JNI。服务端(账号、调度、锚点、计费、后台)没开源。要自己编译的话 README 里写了怎么拉 EasyTier 引擎,Android 的原生库也提交在仓库里,不用再交叉编译。
哪块想细看,比如锚点怎么按房间拉起独立进程,或者客户端怎么按延迟选节点,评论里说一声,我另外写。