用 Go 和 Wails 实现离线文件传输:offline-lan-transfer 的设计与实践
用 Go 和 Wails 实现离线文件传输:offline-lan-transfer 的设计与实践
两台电脑在同一个局域网里,想传一个文件,听起来应该很简单:
1 | 选择文件 → 发给另一台电脑 → 保存 |
但如果加上几个条件,事情就没有那么简单了:
- 不登录账号,不上传云盘;
- 互联网断开时仍然可以使用;
- 不自动连接公共中转服务;
- 接收前能够看到文件清单,并由用户决定是否接受;
- 传输过程中能查看进度,也能真正取消;
- 不能因为同名文件、异常中断或路径问题破坏已有数据。
围绕这些要求,我实现了一个局域网文件传输桌面项目:offline-lan-transfer。
项目使用 Go、Wails v2 和 Vue 3 构建桌面界面,底层复用 croc 的传输能力,通过一个范围受控的维护分支补齐桌面集成需要的接口。
先说明项目定位:
offline-lan-transfer 是基于 croc 的独立衍生项目,不是 croc 官方版本。当前完成的是本地 MVP 实现与开发环境验证,不代表已经通过 Windows 真机、安装包和发布验收。
本文既记录实现过程中的关键取舍,也给出构建和实际使用步骤。
一、先明确“离线传输”要解决什么
1.1 不只是传输时没有打开浏览器
一个工具看起来在局域网传文件,并不代表它不会访问互联网。
外部请求可能发生在用户点击发送之前,也可能出现在失败之后:
1 | 进程启动 |
因此,这个项目把离线约束放在多个层次实现:
- 前端资源本地构建,不使用 CDN、远程字体或远程图标;
- 传输必须使用用户明确配置的局域网 Relay;
- 禁止官方公共 Relay 和公网地址;
- 不做自动发现、遥测或在线更新;
- hostname 只通过本机 hosts 文件解析,不调用 DNS 或 mDNS;
- WebView2 缺失时提示用户准备离线运行时,不自动下载。
这里的“离线”指的是运行时不依赖互联网,不是不需要网络。两台电脑仍然需要保持局域网互通。
首次准备编译工具、Go 模块、npm 包和 WebView2,也可以在联网准备环境完成,再带到离线环境使用。这与应用运行时的网络边界是两件事。
1.2 第一版刻意控制范围
当前桌面目标是 Windows 10/11 x64。第一版只允许一个活动传输任务,先把下面这条流程做完整:
1 | 配置 Relay |
扫码导入、并行多任务、自动更新和 Linux 桌面版不属于当前已实现功能。
二、为什么选择 croc,而不是重新实现传输协议
文件传输界面看起来不复杂,但底层涉及连接、握手、加密、文件信息交换、数据流和异常退出。
本项目选择复用 croc 已有的传输机制,把主要精力放在桌面产品需要的控制能力上。
2.1 没有通过命令行子进程包装
一个直观方案是从 GUI 启动 croc CLI,再解析终端输出:
1 | GUI → 启动命令行 → 读取 stderr → 解析进度条 |
这种方式容易遇到几个问题:
- 进度文本不是稳定的结构化接口;
- 接收确认依赖终端输入,GUI 很难安全接管;
- 取消子进程与正确释放传输资源不是同一个问题;
- 传输短语或密码可能出现在命令参数、说明文本和日志中。
因此,正式集成使用 Go API,不启动 croc CLI,也不把解析 stderr 当作进度方案。
2.2 维护分支只处理集成边界
当前 croc 上游基线固定为:
1 | tag: v11.0.2 |
项目使用的维护分支 revision 为:
1 | dcc584b6e01a07f0ead0d61393732d0ab792da41 |
补丁集中在以下方面:
| 集成问题 | 维护分支的处理 |
|---|---|
| 默认 Relay 解析发生得过早 | 改为惰性解析 |
| 发送说明输出短语、密码和链接 | 增加 SuppressSendInstructions |
| debug 日志包含认证或协议敏感数据 | 清理敏感日志 |
| 接收确认依赖 stdin | 增加 ManifestApprover |
| 进度不适合直接供 GUI 使用 | 增加结构化 EventSink |
| 取消后仍可能阻塞 | 补齐 context 与资源生命周期处理 |
PAKE、加密算法、wire message format 和 Relay protocol 保持原有行为。staging、冲突策略、数据库和 GUI 则留在主项目中。
三、整体架构:让界面与传输引擎分开
项目的依赖方向如下:
1 | Vue 3 UI |
产品能力由主项目中的模块承接:
1 | internal/ |
例如,项目自己的传输接口是:
1 | type Engine interface { |
界面处理的是项目 DTO,不需要知道 croc 内部如何表示文件、连接或协议消息。将来升级底层依赖时,适配层负责处理接口差异,而不必让整个前端跟随修改。
四、接收流程:先确认,再写入最终位置
4.1 “收到文件清单”不等于“已经同意接收”
GUI 接收需要一个明确的暂停点:拿到完整清单后,等待用户选择。
1 | 收到并验证 manifest |
manifest 快照不能把正在修改的内部 slice 暴露给界面,等待确认也不能阻塞取消。
取消与接受同时到达时,也不能仅依赖一个普通 select 来碰运气。仲裁逻辑需要在等待前检查 context,并在取到接受结果后再次检查,只有最后检查仍未取消时才允许接受生效。这个检查点定义了接受与取消竞争时的判定边界。
4.2 为什么引入 staging
直接把网络数据写到最终文件名,有一个明显问题:
网络断开后,一个只写了一部分的文件,可能看起来已经是用户要的最终文件。
本项目让 croc 先写入保存目录内的独立临时目录:
1 | 用户保存目录/ |
成功路径为:
1 | 传输结束 |
失败或取消默认清理临时目录;清理失败会返回错误。临时目录放在最终目录所在位置,也方便保持同一文件系统内的提升流程。
这里要区分两类验证:产品层当前独立检查路径、类型和大小;传输内容校验由固定的 croc 引擎负责。集成测试另外比较传输前后 SHA-256,但不能因此声称产品新增了一套独立 SHA-256 传输协议。
4.3 同名文件不能静默覆盖
| 策略 | 当前行为 |
|---|---|
| AutoRename | 默认保留已有文件,为传入文件选择新名称 |
| Ask | 发现冲突时停止,需要重新选择目录或策略 |
| Skip | 保留冲突目标,不用传入文件替换 |
| Overwrite | 需要用户明确勾选确认,仍受路径安全检查约束 |
第一版的 Ask 并不是逐文件弹窗决策。把这一点写清楚,比让用户根据名称猜测行为更重要。
路径校验还包括目录穿越、绝对路径逃逸、Windows 保留名称、非法字符和符号链接等情况。
五、进度、取消与页面切换
5.1 用事件驱动界面,而不是解析进度条
底层事件经过适配和应用层处理后传递给 Vue,覆盖阶段、清单、文件开始、进度、文件完成、重连和终态。
界面不轮询 croc 旧公开字段,也不解析终端进度条。
这里不仅要关注“有没有进度事件”,还要处理:
- 事件是否乱序或重复;
- 迟到的 progress 是否会覆盖 completed / canceled;
- 任务启动方法返回前,manifest 是否已经先到达;
- 慢 UI 是否会阻塞传输;
- terminal 事件是否已经交付,任务就被销毁了。
项目用事件序号过滤过期更新,保留提前到达的 manifest,并在销毁任务前处理事件 drain。
5.2 Cancel 必须作用到实际资源
仅仅将界面文字改成“已取消”,并不能证明传输已经停止。
取消需要贯穿整个调用链:
1 | 用户点击 Cancel |
任务取消时,界面先进入 cancelling,等待最后结果。第一版的发送和接收共享一个活动任务名额,避免同时操作多个传输带来的额外生命周期问题。
5.3 切换页面不应该丢失任务
发送和接收页面通过 Vue KeepAlive 保留实例,隐藏时继续处理事件。因此,切到设置页再回来,任务仍然可见;即使终态在页面隐藏期间到达,也能清理短语和二维码展示。
需要注意,清理应用中的引用不等于物理擦除 Go/JavaScript 内存,也不等于自动清空用户已经复制到系统剪贴板的内容。
六、严格离线与秘密保护中容易忽略的细节
6.1 自定义 Relay 不代表已经消除公网请求
除了默认 Relay 与 fallback,还必须检查 hostname 解析。
如果将 relay.lan 直接交给系统解析器,最终采用哪个 DNS 服务器仍由操作系统配置决定。域名看起来属于局域网,并不保证解析过程不会出网。
当前实现只接受局域网地址,并使用本机 hosts 映射解析名称:
1 | 明确的 LAN IP → 校验后使用 |
这是一个明确的易用性取舍:没有 DNS/mDNS 自动发现,用户需要输入局域网 IP,或者事先配置 hosts。
6.2 Relay 密码与传输短语不同
| 信息 | 用途 | 生命周期 |
|---|---|---|
| Relay 密码 | 连接指定 Relay | 客户端配置通过 SecretStore 保存;内嵌服务密码用于本次运行 |
| 传输短语 | 匹配并保护本次传输 | 必要的任务生命周期内使用,不写入历史和数据库 |
Windows 密码存储使用当前用户 DPAPI。SQLite 保存的是密码引用和非敏感元数据,而不是 Relay 明文密码。
传输 QR 携带本次短语,是敏感信息;Relay QR 默认只携带地址和端口,密码另行提供。当前界面提供二维码生成与展示,没有应用内扫码入口。
6.3 不把原始错误直接显示给用户
底层错误可能包含协议细节、路径或其他动态信息,因此应用层转换为稳定类别,例如连接不可达、认证失败、空间不足、路径不安全、冲突、校验失败和取消。
日志采用字段白名单,诊断 ZIP 只收集允许的版本、系统、schema、安全状态与日志信息。原始协议 payload、room、密钥、短语和密码不能作为“方便调试”的字段直接写进去。
七、如何构建可以运行的 Windows EXE
7.1 所有命令从仓库根目录开始
项目地址:
github.com/jianingdai/offline-lan-transfer
可以先在联网准备环境获取代码:
1 | git clone https://github.com/jianingdai/offline-lan-transfer.git |
这里的目录应当能看到 go.mod、wails.json 和 frontend,而不是进入 docs 后执行构建。
当前依赖要求包括:
- go.mod 声明的 Go 工具链,目前为 Go 1.25.0;
- 与锁定前端工具链兼容的 Node.js/npm,开发验证使用 Node.js 24;
- Wails CLI v2.15.0;
- 需要安装包时另行准备 NSIS。
7.2 准备依赖
以下步骤可能联网,应在准备环境执行:
1 | go install github.com/wailsapp/wails/v2/cmd/wails@v2.15.0 |
确认 wails 已在 PATH 中:
1 | wails version |
项目固定使用 v2.15.0,不要把不同版本的构建结果混为一谈。依赖缓存和工具需要为实际构建平台准备,Linux 上安装的二进制依赖不应直接当作 Windows 安装结果。
7.3 构建 EXE
在仓库根目录执行:
1 | wails build -platform windows/amd64 -webview2 error |
生成位置:
1 | offline-lan-transfer/ |
该文件在 Windows 上运行,需要预先安装兼容的 WebView2 Runtime。应用不会在启动后自动下载运行时。
仓库也提供便携包脚本。在 Windows PowerShell、仓库根目录执行:
1 | # 只验证打包配置 |
默认输出:
1 | build/releases/offline-lan-transfer-0.1.0-windows-x64.zip |
脚本要求依赖事先缓存,使用本地工具链、GOPROXY=off 和 npm 离线模式。已有输出不会被覆盖,需要先自行归档。完整 Wails/NSIS 打包尚未在当前开发环境验收,不能把配置检查成功当作安装包验证通过。
7.4 Linux 文件不等于 Linux 桌面客户端
当前没有实现 Linux 桌面版。Linux 可以编译无界面的 Relay 服务:
1 | # 在 Linux 上,从仓库根目录执行 |
这是中转服务,不会打开文件传输窗口,也不能在没有配置时直接双击使用。Relay 运行需要绑定地址、端口和密码文件等配置;容器配置与离线部署步骤见仓库的 build/docker/README.md。
八、两台电脑实际如何使用
假设电脑 A 的局域网 IP 是 192.168.1.20,电脑 B 是接收端。下面的地址只用于示例,应替换成实际网卡地址。
8.1 在电脑 A 启动 Relay
进入“局域网中心”,在“启动本机局域网中心”中填写:
| 配置项 | 示例 |
|---|---|
| 绑定地址 | 192.168.1.20 |
| 控制端口 | 9009 |
| 数据端口 | 9010, 9011, 9012, 9013 |
| 临时 Relay 密码 | 自行生成并妥善传递的强密码 |
点击“启动 Relay”,确认处于运行中。两机传输不要绑定 127.0.0.1,它只能服务本机连接。
控制端口和所有数据端口都需要允许必要的 TCP 入站访问。应用提供防火墙检查和规则规划,不会替用户关闭防火墙或自动创建管理员规则。
8.2 两端都添加 Relay Profile
启动内嵌服务与保存客户端配置是两个操作。电脑 A 和电脑 B 都需要在“新增 Relay”中填写同一个服务地址、端口和密码,然后启用并测试连接。
即使 Relay 与发送端在同一个应用中,发送端也需要一个可选择的 Relay Profile。
8.3 发送端生成短语
- 进入“发送”;
- 添加单文件、多文件或文件夹;
- 选择启用的 Relay;
- 点击“生成短语并开始发送”;
- 复制短语,通过可信渠道交给接收方。
8.4 接收端查看清单并接受
- 进入“接收”,粘贴短语;
- 选择保存目录和同一个 Relay;
- 选择冲突策略,默认自动重命名;
- 点击“连接并查看清单”;
- 检查数量、大小、保存位置和空间;
- 点击“Accept 并接收”,或者点击“Reject”。
传输过程中可以取消。任务成功后查看最终目录,也可以从“传输记录”查看非敏感结果;清空历史不会删除收到的文件。
九、如何验证它,而不只验证“按钮能点”
9.1 功能测试与并发测试
仓库根目录:
1 | go test ./... |
前端目录:
1 | cd frontend |
实际覆盖包括 loopback Relay、发送接收、Accept/Reject、取消、目录与多文件、AutoRename、staging 清理和文件 SHA-256 一致性。
前端测试还覆盖乱序事件、终态保持、提前到达的 manifest,以及切换页面后的任务实例与订阅生命周期。
已知 croc 全局 logger race 仍然保留。项目自有并发测试通过,不代表整个依赖树的 race 检查全部通过。
9.2 网络审计要隔离 Go 工具链
直接审计 go test,可能把模块下载等构建行为也统计进去。这里采用先构建测试程序,再单独跟踪它的方式。
下面是 Linux/WSL 上可执行的一个审计入口,需要安装 strace:
1 | # 在仓库根目录,依赖已缓存的前提下执行 |
已记录的一次 localhost 传输审计中,12 次 connect 全部指向 loopback,未观察到 DNS 或公网连接。
这证明的是该测试路径的行为,不能直接扩展为“已经完成所有 Windows/WebView2 运行场景的流量审计”。
9.3 秘密扫描需要区分证据类型
项目测试分别检查:
- 真实传输子进程输出中的测试短语、密码、派生 room 和 getcroc 链接;
- argv 和环境变量中是否存在测试凭据;
- SQLite、WAL/SHM、历史 DTO、日志和诊断 ZIP 中的敏感标记;
- 固定 croc 分支中的握手与协议日志回归。
模拟错误里的 key sentinel 能验证应用过滤逻辑,但不等于抓到了真实 PAKE key 再完成精确扫描。区分这些证据,才能避免把测试范围写得比实际更大。
十、常见问题与排查顺序
| 现象 | 优先检查 |
|---|---|
| 启动提示缺少 WebView2 | 提前离线安装兼容运行时,或检查 EXE 旁固定版运行时目录 |
| connection refused | Relay 是否运行、控制端口和绑定地址是否正确 |
| timeout | 网卡、防火墙、设备互访与 Wi-Fi 客户端隔离 |
| authentication failure | 是否把传输短语误当成 Relay 密码,或保存了旧密码 |
| hostname 无法连接 | 本机 hosts 是否有映射;可先改用 LAN IP |
| 测试连接成功但文件传不了 | 数据端口是否全部可达,不要只放行控制端口 |
| 新任务无法启动 | 是否已有发送或接收任务尚未退出 |
| 接收因冲突停止 | 修改冲突策略或保存目录后重新发起 |
首次使用时,可以按下面的顺序检查:
- 两端都在同一可互访局域网;
- Relay 绑定的是实际 LAN 地址,不是 loopback;
- 两端 Profile 都指向同一个 Relay;
- Relay 密码、控制端口和数据端口配置正确;
- 连接测试成功,数据端口也已放行;
- 接收方输入的是本次传输短语;
- 保存目录可写,剩余空间足够;
- 已确认完整清单并点击 Accept;
- 最终状态确实为完成,而不只是进度条接近 100%。
需要反馈问题时,可以在“设置”导出安全诊断包,同时提供版本、错误类别和复现步骤,不要把真实短语或密码贴进问题描述。
十一、当前完成了什么,还有什么没有验证
截至本文整理时,项目完成了本地 MVP 实现、自动化测试、前端构建和 Windows Go 交叉编译。
以下项目仍需要实际环境验收:
- Windows 双机局域网传输;
- DPAPI 实际读写和 Windows 文件权限;
- 系统、缺失和固定版 WebView2 的启动行为;
- 原生文件选择器、剪贴板、窗口缩放和视觉效果;
- 防火墙与网络 profile 的实际检查;
- 便携包、NSIS 安装升级卸载、签名与 SmartScreen;
- Docker Compose 与镜像部署。
因此,当前更准确的定位是:
可以继续进行 Windows 真机验收的桌面 MVP,而不是已经完成全部安全与发行验证的成品。
工程占位图标、已知 logger race、没有应用内扫码和 Linux 桌面支持,也都是需要如实保留的限制。
十二、总结
这个项目最有价值的部分,并不是给命令行工具增加几个按钮,而是把传输能力放进一个边界明确的桌面流程:
1 | 显式局域网连接 |
复用成熟传输引擎可以减少协议层的工作,但它不会自动解决 GUI 确认、文件冲突、日志秘密、任务销毁和离线依赖这些产品问题。
对我而言,这次实践形成的一个重要原则是:
能完成一次文件传输,只说明传输路径跑通了;要让它成为一个可以交给别人使用的工具,还需要把失败、取消、安全落盘和验证边界一并设计清楚。
项目与参考资料
- offline-lan-transfer 项目仓库
- 项目使用说明
- croc 最小维护分支
- 集成策略 ADR-001
- MVP 交付与验证记录
- Windows 构建说明
- Docker Relay 部署说明
项目自身代码和文档采用 MIT License,croc 及其他依赖遵循各自许可证。本文对应的是本地已实现版本,仓库链接中的具体内容以作者同步后的提交为准。