CopySync

CopySync 的技术方案

局域网直连:不用服务器也能配对和同步

状态:已实现,随 1.4.0 发布;「搜索附近设备」随 1.5.0 发布

代码:client-core/internal/transport/lan/(发现、信令、配对、搜索附近设备、地址簿)、transport/signed/(签名与校验)、peers/manager.go(两条通道合并)

验证:transport/lan/lan_test.go、peers/manager_test.go、tools/natlab/(局域网场景)、Mac 与 Windows 虚拟机实机测试

相关:多出口网络的 NAT 打洞

一、问题

1.3 之前,两台电脑必须先连上同一台信令服务器,才能配对、知道对方在线、交换连接信息。即使两台就在同一个路由器下面,用户也得先找一台常开的电脑部署服务器。

服务器在这里只做三件事:

服务器的职责局域网里用什么代替
发现:谁在线、在哪个地址mDNS(DNS-SD)广播
转发签名的信令设备之间直接用 UDP 收发
配对码:码 → 公钥在局域网里直接交换
STUN / TURN同一局域网里用不上,host candidate 就能直连

信任模型本来就把服务器当成不可信的转发者:信令逐条签名,配对靠人工核对指纹,DTLS 指纹随签名一起传。把转发者从服务器换成局域网,安全性不变。

二、原型实测

动手之前,先用一个临时程序(mDNS 注册与浏览,加固定 UDP 端口互发 hello)在 Mac 和 Windows 虚拟机之间测了几种情况。虚拟机在 Parallels 的共享网络里,与 Mac 同一网段,网络类型是「公用」。

Windows 防火墙

情况结果
程序没有任何防火墙规则Windows 能发现 Mac(自己发出的查询,回答作为回包放行);Mac 发现不了 Windows(Windows 回答不了查询)。Windows 主动打招呼后,双方都能收发
有显式的入站阻止规则(相当于弹窗时点了取消)同上。有状态防火墙对回包一律放行,阻止规则只挡主动连进来的包
只有 Mac 知道 Windows 的地址,Windows 不知道 Mac 的不通:Mac 的包被挡,Windows 不会主动发
只有 Windows 知道 Mac 的地址通
两边都知道对方地址通
删掉程序的入站规则,再加一条 UDP 允许规则只有 Mac 知道地址时也通了

结论:

macOS 15 的本地网络权限

从终端运行的程序不受限制,但同一个程序打包成 App、由 launchd 启动(与后台服务一样)时,往局域网地址发包直接返回 sendto: no route to host。对方先发来的包仍然能收到,也能回复。

这说明 1.3 的后台服务在 macOS 15 上其实也连不到局域网地址。它和 Windows 虚拟机之间显示「直连」,是因为 Parallels 的 NAT 让虚拟机的包看起来来自 Mac 自己的 IP,恰好绕开了限制。两台 Mac 之间如果都没拿到权限,谁也连不上谁。

处理办法:

后来用正式打包方式(同样的 Info.plist,由 launchd 带着 AssociatedBundleIdentifiers 启动)在解锁的屏幕上复测:

三、方案

1. 两条通道并存

peers.Manager 同时持有信令服务器客户端和局域网通道:

签名与校验从信令客户端里抽到 transport/signed,并补上一条检查:to_device_id 必须是本机。服务器按收件人转发,这条检查以前可有可无;局域网里谁都能把截到的消息转发给第三台设备,必须有。

2. 发现

3. 局域网信令

4. 地址簿与跨网段

5. 配对

6. 搜索附近设备

配对码之外的另一种配对方式,1.5.0 加入:两台都在「设备」页打开「搜索附近设备」,互相出现在雷达上,点一下对方就发起配对。原来的「添加设备」(配对码)不变,雷达是单独的入口。

7. 重启

hello 里带一个每次启动随机生成的启动标识。收到已配对设备的 hello、发现启动标识变了,就知道它重启过:立即丢掉旧的 P2P 连接重建,不必等连接超时。用服务器时,这件事由服务器的上下线通知完成;没有服务器时,没有这一步的话,应答方重启后要等发起方自己发现连接失效(实测约 30 秒)。

另外,mDNS 里出现已配对设备的新实例名或新地址时,即使还当它可达,也马上打招呼:重启的那台若是没放行防火墙的 Windows,它主动发来的包进不来,要靠本机先发。

8. 其他改动

代码位置

位置内容
transport/lan/lan.gosocket、hello、可达状态、地址簿、诊断
transport/lan/pairing.go局域网配对
transport/lan/nearby.go搜索附近设备:广播设备名、ping、不带配对码的配对请求
transport/lan/discovery.go、mdns*.go发现接口、pion/mdns 实现、Windows 的系统 DNS-SD 注册、测试用的内存网络
transport/lan/addrs.go本机地址、地址解析、令牌
transport/lan/permission_*.gomacOS 打开本地网络设置与探测地址、Windows 用 UAC 放行防火墙
transport/signed/签名、校验、防重放、收件人检查
peers/manager.go两条通道合并、配对流程
store/store.godevice_addrs 表
config/config.go默认不用服务器、lan_disabled、一次性迁移

四、能覆盖哪些场景

两台电脑结果
同一网段mDNS 发现,直连
不同网段,网段之间互通配对时输入地址,直连;之后靠地址簿
同一 tailnet配对时输入 100.x 地址,直连
一台是没放行防火墙的 Windows同一网段里能配对(它先「敲门」)、能重连;跨网段时要它知道对方的地址
两台都是没放行防火墙的 Windows,跨网段,只有一边知道地址连不上,需要在被连的一方点「放行」
两个网段之间被隔离(访客网络等)连不上,只能经两边都能访问的服务器中转
macOS 15 没允许本地网络对方先发来时能连;两台都是这样的 Mac 时连不上,界面提示去允许

五、验证

单元测试与集成测试

transport/signed/signed_test.go:签名与校验;同一条信令从两条通道到达只处理一次;收件人不是本机时拒收;未配对、冒用 ID、时间戳过期都拒收。

transport/lan/nearby_test.go:只在窗口打开期间广播设备名、两台都打开才互相看到、关掉即消失;认出已配对的设备;点了对方后交换身份、对方关掉窗口时很快返回找不到;不再收到 ping 的设备消失、收到 ping 又出现;实例被过期丢掉后只靠对方的 ping 也能完整显示;设备名编码与截断。

transport/lan/lan_test.go:两个实例绑在 127.0.0.1 的不同端口上,用内存网络代替 mDNS。已配对设备互相找到并收发信令、补发的那份被去重、地址记进地址簿;只靠记得的地址互相找到;未配对设备的 hello 不算数;局域网配对、按地址配对、码只能用一次、错 10 次后作废;可达状态过期;地址解析与令牌换天;显示码的一方向在找人配对的实例敲门、配对期间广播里带 s=1;对方重启后察觉。permission_darwin_test.go:探测地址在本网段里且不是本机,本机地址不算直连网段里的对端;对方不在线(EHOSTUNREACH 后接 EHOSTDOWN)不算被拦,也不重启 mDNS。

peers/manager_test.go:两个完整的 peers.Manager。搜索附近设备:互相看到、点了对方后双方核对确认、直连并收发消息,重复的请求只弹一次确认,配对后显示为已配对;对方没打开窗口时看不到、配不上。没有服务器时配对、P2P 直连、双向收消息,解除配对后断开;不同网段时不输地址找不到、输了地址能配对并直连;关掉局域网只用服务器时原来的配对路径照常工作,错码立即被拒;服务器和局域网同时开着时,同一个码只能用一次。

config/config_test.go:旧默认地址清空、自己的服务器保留、只迁移一次。store/store_test.go:地址簿的排序、上限、随设备删除。

界面:pairing_flow_test.dart 覆盖原来的添加设备不变、雷达是单独入口;雷达显示附近的设备、点了之后弹出核对指纹并关掉雷达(随之撤下本机)、对方没回应时的提示、局域网发现关闭时的说明;显示本机地址、找不到时展开地址框并带地址重试、错码提示、侧栏的局域网状态与 macOS 拦截提示。

NAT 打洞实验室

新增 natlab lanpeer:不起任何服务器,用真实的局域网通道连接,两端身份由种子派生,相当于已经配对。新增两种拓扑:同一网段(网桥)与两个网段(路由器,不做 NAT)。

场景预期结果
同一网段,靠 mDNS 发现直连通过
两个网段,一边记得对方地址直连通过
两个网段,两台都挡入站新连接,都记得地址直连通过
两个网段,两台都挡入站新连接,只有一边记得连不上通过
两个网段之间被隔离连不上通过

「两台都挡入站」用每台主机上的 iptables -A INPUT -p udp -m conntrack --ctstate NEW -j DROP 模拟没有放行规则的 Windows 防火墙,证明两边同时发包确实能穿过有状态防火墙。

实机验收

Mac 与 Windows 11 虚拟机,各跑一个用 -tags fakeclip 编译的后台服务(独立数据目录,不碰真实剪贴板),不配服务器。Windows 上的程序是新的路径,没有任何防火墙规则。

第二轮把 Mac 一侧换成正式打包的后台服务,由 launchd 启动,用户在授权窗里点了允许:

第三轮用正式安装的 App:Mac 装进 /Applications,Windows 用 CI 构建的安装包覆盖安装,两边都配着服务器,Windows 没放行防火墙:

第四轮(1.5.0,搜索附近设备):Mac 与 Windows 11 虚拟机各跑一个 -tags fakeclip 的后台服务(独立数据目录,与两边正式安装的 1.4.0 同时运行,局域网端口因此都退回随机端口),Windows 上的程序是新的路径,没有任何防火墙规则,用 copysync-cli nearby 代替界面:

第一轮里「两边重启后约 1 秒重新直连」的结果不可靠:清理测试进程时漏掉了几个,「重启」时旧实例还以同一个身份在运行。第二轮在干净的环境里重测,结果如上。

六、踩过的坑

七、局限与后续