把路由器变成局域网协作小站:Nook 的开发笔记

一个跑在 OpenWrt 路由器上的零依赖局域网协作服务——共享剪贴板、文件公告板、WebRTC 点对点传文件,以及配套的 LuCI 管理界面。

为什么做这个

跨设备传文件、同步一段文字或链接,是家庭、办公室、宿舍里最高频也最碎片化的需求。但现有的方案总有让人不舒服的地方:

  • 要登录第三方账号,隐私和依赖都交出去了;
  • 大文件走公网云中转,速度受限、流量有成本,还可能在途中被看;
  • 没公网或弱公网环境直接不可用;
  • 每台设备都得装客户端,手机、电脑、平板、同事的机器……装一遍就劝退一半人。

而每个家庭/办公室几乎都有一台 7×24 小时开机的东西——路由器。它天然就在局域网的中心,天然常驻,天然被所有设备信任。那为什么不能把路由器本身变成一个"随时可用的本地共享空间"?

这就是 Nook 的出发点:

  • 零客户端——浏览器打开即用,不用装任何 App;
  • 零公网依赖——不需要外部服务器、云账号、公网链路,断网也能用;
  • 本地优先——文件内容不落路由器磁盘,尽量在设备之间点对点直传;
  • 路由器即入口——开机常驻,永远在同一个地址等你。

项目形态

Nook 包含两个仓库:

  1. nook——Go 编写的核心服务,跑在 OpenWrt/ImmortalWrt 上,提供 Web 页面 + WebSocket 信令 + 内存态协调(Hub)。
  2. luci-app-nook——配套的 LuCI 应用,在 OpenWrt 管理界面 Services → Nook 里查看服务状态、启停服务、配置监听地址。

两个包都是标准 OpenWrt 打包(nFPM 生成 ipk / apk),覆盖 aarch64_cortex-a53 和 x86_64 两种架构,CI 在打 v 开头 tag 时自动构建发布。

能做什么

打开 http://路由器地址:8088 之后:

  • 局域网设备自动发现——所有打开页面的设备实时出现在在线列表里,离线自动移除;
  • 共享剪贴板——发布一段文本/链接,局域网内所有设备实时同步;自动识别 URL 可点击;每条支持复制,还带系统剪贴板 Paste;
  • 文件公告板——把文件"发布"到公告板,其他设备点一下就能领取;支持多文件、单条撤回、一键清空自己的公告;
  • 直接发送——在设备列表里点某个设备,直接给它发文本或文件,对方弹窗接受/拒绝;
  • 设备命名——自定义设备名,可记忆(刷新后保留),可一键重置回自动生成的默认名(按 UA 生成 + 随机后缀)。

关键设计:文件内容不经过路由器

这是 Nook 最核心的设计决策:默认不把文件存到路由器上。

文件公告板只在路由器内存里保存文件的元信息(文件名、大小、MIME、发布者、发布时间)。真正的文件内容始终留在发布者的浏览器里;其他设备点击领取时,通过 WebRTC DataChannel 在设备之间点对点传输,路由器只做信令中转。

对应的取舍:

  • 发布者的页面需要保持打开、设备保持在线;
  • 发布者刷新页面或关浏览器,相关公告自动失效;
  • 路由器不持久保存文件内容——低端路由器的存储和 CPU 都不会被大文件拖垮。

架构

路由器 (OpenWrt)
┌──────────────────────────────────────┐
│  /usr/bin/nook  (单二进制, 前端已嵌入)  │
│   - HTTP: /  /ws  /config             │
│   - Hub: 内存态 (peers/clipboard/offers)│
└──────────┬───────────────┬────────────┘
   WebSocket (信令/元信息)    HTTP (页面)
           │                │
     ┌─────▼─────┐    ┌─────▼─────┐
     │ 设备 A     │    │ 设备 B     │
     │ 浏览器     │◄──►│ 浏览器     │
     └───────────┘WebRTC└───────────┘
        文件内容 P2P 直传, 不经过路由器

技术栈刻意保持极简:

  • 后端:Go 标准库 + gorilla/websocket + google/uuid,全部代码约 600 行(不含前端);
  • 前端:纯 HTML/CSS/JS,零第三方依赖,通过 embed.FS 打进单二进制——部署就是一个文件 + init.d 脚本 + UCI 配置;
  • 宿主集成:procd 管理、respawn 自动重启、/etc/config/nook 标准 UCI 配置。

WebSocket 协议

设备通过 /ws 注册,消息是 JSON,核心类型:

  • hello / welcome——注册与全量状态下发(自身 ID、在线设备、剪贴板、文件公告);
  • peer-joined / peer-left——上下线广播;
  • clipboard-add / clipboard-added——剪贴板发布与广播;
  • file-board-add / file-board-added / file-board-remove / file-board-removed——公告板增删;
  • file-board-request / file-board-unavailable——领取请求与失败;
  • signal——WebRTC offer/answer/ICE candidate 的定向转发。

文件传输协议(DataChannel 内)

发送方先发 file-offer(文件名/大小/MIME),接收方回 file-accept 或 file-reject,然后按 64 KB 分块流式发送二进制,最后 file-done 收尾。状态机:waiting → accepted → sending → done,或 rejected / error。公告板领取和直接发送共用这套机制。

一些工程细节

  • WebSocket 升级时做 Origin 校验,防跨站 WebSocket 劫持;
  • 心跳 Ping 30 秒,读超时 60 秒,写超时 10 秒,空闲连接不被误杀;
  • 单条消息上限 64 KB;剪贴板最多 50 条、单条 64 KB;公告板最多 100 条;
  • 优雅关闭:收到 SIGINT/SIGTERM 后 5 秒内收尾;
  • 设备 ID 用 UUID 前 8 位,不可预测;
  • 前端断线 3 秒自动重连,重连后重新注册拉全量状态。

配套的 LuCI 界面

luci-app-nook 让 Nook 融入 OpenWrt 原生管理体验:

  • 后端是 ucode 写的 rpcd 插件(luci.nook),暴露 get_status 和 service_action 两个方法;
  • 状态页显示 running(运行中)和 enabled(开机自启)两个状态,一键 start/stop/restart/enable/disable;
  • 配置页读写 /etc/config/nook 的启用开关和监听地址,改配置自动触发服务重载;
  • 同时兼容 LuCI 23.05 和 24.10(通知 API 做了降级);
  • 权限走标准 ACL,最小授权:读状态 + 写 nook 配置 + 执行 init.d 脚本。

安装与使用

# aarch64 / x86_64 按架构选包
opkg install ./nook_0.1.6-1_aarch64_cortex-a53.ipk
# 或 apk 包管理器(未签名本地包)
apk add --allow-untrusted ./nook_0.1.6-r1_aarch64_cortex-a53.apk

/etc/init.d/nook enable
/etc/init.d/nook start

然后同一局域网内访问 http://192.168.1.1:8088 即可。升级行为也考虑过:首次安装不自动启动;升级时若服务正在运行则自动重启,未运行则不打扰。

设计取舍与边界

Nook 明确不做的事,和做了一样重要:

  • 不做公网/跨网传输——只面向同一局域网;
  • 不做账号体系——没有登录、没有云同步;
  • 不做持久化——剪贴板、在线设备、公告都是内存态,服务重启即清空(已接受的取舍);
  • 不做认证加密——当前版本基于"可信局域网"假设;没有端到端加密,局域网内流量可被嗅探。这一点在文档里写得很清楚,后续版本会评估设备配对与传输加密。

小结

Nook 解决的是一个小而真实的问题:在局域网里,设备之间应该能像在同一个房间一样自然地交换东西。它把"路由器"这个每家都有的基础设施变成了协作入口,用浏览器消灭客户端,用 WebRTC 让文件内容绕开路由器,用最克制的技术栈(Go + 原生前端 + 标准 OpenWrt 打包)换来低端路由器上也能跑的轻量体验。

代码在 GitHub,欢迎围观和提 issue:


发表评论