p2p-cit
v0.2.0
Published
去中心化、无服务器的端到端加密终端聊天(命令名 cit)
Maintainers
Readme
cit
去中心化、无服务器的端到端加密终端聊天。
没有账号,没有服务器,没有中继。房间里的成员直接互联,聊天内容在传输层由 DTLS 强制加密,在应用层再由房间密钥加密一次。
$ cit new study-group --name alice
房间已创建 cit-9f3a2b1c
邀请码已复制到剪贴板,发给对方即可
$ cit join cit1.XK3... --name bob # 扫二维码或粘贴邀请码
已接受 alice 的邀请码
把下面的应答码发回给对方,连接才会建立:
› /link citr1.QP9... # 在 alice 那个会话里输入安装
npm install -g p2p-cit安装后的命令名是 cit(cit --help)。npm 上的包名是 p2p-cit,因为 cit 已被其它项目占用。
这是一个早期版本(0.2.0)。请先读已知边界:首次握手可能需要 20-60 秒,且不做 TURN 中继。
安装发布包要求 Node 20 及以上。平台支持由 node-datachannel 的预编译二进制决定:Linux(glibc 与 musl,x64/arm64)、macOS(x64/arm64)、Windows(x64/arm64)和 Android arm64。其他平台会由原生依赖报错,不会自动退化成源码编译。本次发布验证在 Windows x64 上完成。
用法
全屏应用
在终端中直接运行 cit,进入全屏大厅,用方向键选择「创建房间」或「加入房间」,回车进入表单。Tab 切换房间名/邀请码、显示名和可选的 STUN 地址,Enter 提交,Esc 返回。
创建房间后把邀请码发给对方;对方加入后把应答码发回,邀请者在房间里按 F3 粘贴应答码。连接建立后直接输入消息回车发送。连接码也支持 @文件路径,粘贴内容需要按 Enter 才提交。
| 按键 | 作用 | | --- | --- | | F2 | 生成新的邀请码(替换未完成的邀请) | | F3 | 接受对方发回的应答码 | | F4 | 查看最近生成的连接码 | | C(连接码界面) | 重新复制连接码 | | Page Up / Page Down | 滚动聊天记录、帮助或长连接码 | | Esc | 返回;聊天界面中离开房间,需确认 | | Ctrl+C | 退出;房间内先提示未送达消息并确认 |
全屏模式支持下方的会话命令,展示在线成员、输入状态和待送达/已送达/已读回执。离开房间回到大厅,可创建或加入另一个房间,旧房间的聊天记录不会保留。最小终端尺寸为 40 列 × 18 行。
cit --name alice 可预填显示名,--stun、--passphrase、--ephemeral、--no-clipboard 继续生效。cit new / cit join 保留逐行界面;管道、--json 和 --help 不进入全屏。
开房间
cit new <房间名> [--name <显示名>] [--qr] [--json]创建房间并打印邀请码。邀请码里已经包含房间口令,对方拿到就能加入,不需要事先另行约定。邀请码自动复制到剪贴板,--qr 会额外打印二维码供手机扫。
邀请码是一次性的:一张邀请码对应一条具体的 PeerConnection,每个加入者都要单独生成一次(在会话里用 /invite)。
加入房间
cit join <邀请码> # 也支持 cit join @invite.txt 从文件读会打印一个应答码,把它发回给对方,在对方的会话里执行:
/link <应答码>两步交换是无信令服务器的固有代价:没有任何第三方帮你们牵线。
多人组网
第三个人加入时,只需要和任意一个已有成员交换一次邀请码。成员名单沿既有连接传播,节点通过这些连接转发新的握手请求和应答,为每对成员生成独立的连接信息,之后尝试补齐全连接。静态连接名片不能在不同 PeerConnection 之间复用,见 ADR-0007。
会话内命令
| 命令 | 作用 |
| --- | --- |
| /invite | 生成新的邀请码并复制到剪贴板 |
| /link <应答码> | 接受应答码完成连接 |
| /who | 列出在线成员 |
| /pending | 查看未送达的消息 |
| /id | 显示本机指纹 |
| /block <指纹> | 本地拒绝该成员 |
| /unblock <指纹> | 解除本地拒绝 |
| /quit | 退出 |
发消息直接输入回车。@某人 或 @指纹 开头表示只发给该成员,其余是广播。
脚本化
--json 会把事件以 JSON 行输出到 stdout,stdin 不是终端时自动进入管道模式:
cit new room --json | jq -r 'select(.kind=="invite").code'安全模型
请如实理解它提供什么、不提供什么:
- 传输层:WebRTC DataChannel,DTLS 强制加密,无法降级成明文
- 应用层:口令经 scrypt 派生房间密钥,消息用 ChaCha20-Poly1305 加密,header 作为 AAD 防篡改
- 身份:每个节点有 Ed25519 密钥,签名覆盖 header 与密文
- 防重放:每个发送方单调递增序号,回退或重复的帧直接丢弃
- 防反射:消息密钥按收发方排序派生,无法把对方的密文原样退回给它自己
不提供前向保密。 口令泄露等于全部历史消息泄露,这是单一房间密钥模型的代价。任何文档或输出都不会暗示相反的保证。
邀请码等同口令。 邀请码只是 base64 编码,不是加密。它能进入房间、能以任意身份加入,拿到它就等于拿到口令。传递它的渠道安全性就是房间的安全性。
签名证明的是"这把密钥签的",不是"这个人是谁"。成员准入完全依赖口令保密。
已知边界
- 不做 TURN 中继。两端都在对称型 NAT 后时无法直接打洞。这种情况会如实报错,不会假装成功或静默超时。同一局域网内正常可用;跨网场景建议开启 STUN:
cit new room --stun stun.l.google.com:19302(默认关闭,不联系任何外部服务) - 待发消息只在内存。崩溃或强杀会丢失未确认的消息,这是有意为之:落盘意味着聊天明文留在磁盘上。退出前会提示还有多少条未送达
- 同一进程内建两条连接不可靠。
libdatachannel在同进程多 ICE agent 下表现不稳定,正常使用是两个终端各跑一个cit - 不做的:文件传输、消息持久化、同时加入多个房间、成员撤回(需要所有成员协同,无中心的 mesh 里无法诚实实现)
开发
源码开发要求 Node 22.18 及以上(直接运行 TypeScript),使用 pnpm 9.15.0。
pnpm install
pnpm build
pnpm test # 单元测试 + 端到端测试
pnpm typecheck
pnpm cli # 全屏应用(须在交互式终端中运行)代码分两层:cit-core 是纯 TypeScript 的协议与加密,不依赖原生模块,可以单独测试;cit-cli 承载 WebRTC 传输与终端界面。
端到端测试会启动真实进程、走真实 WebRTC。在本机实测,握手可能需要 20-60 秒——Windows 防火墙会丢弃最初几个 ICE 连通性检查,libjuice 需要重传才能收敛。测试因此有较长的超时和建连重试,这不是掩盖问题,而是网络本身如此。
设计决策记录在 docs/adr/,领域词汇在 GLOSSARY.md。
发布
同步根目录和 packages/cit-cli/package.json 的版本,更新本 README 中的版本说明。仅发布 p2p-cit;@cit/core 由构建内联,不独立发布。
pnpm build
pnpm typecheck
pnpm test
pnpm --dir packages/cit-cli pack --pack-destination <产物目录>
npm publish <产物目录>/p2p-cit-<版本>.tgz --access public --tag latest构建会从根 README 生成 npm 包的 README,勿手工编辑生成副本。发布前检查 tarball 的文件清单并从 tarball 安装验收;提交与版本 tag 推送后创建同名 GitHub Release。发布后核对 npm latest、tarball integrity 和 Release 指向的 commit。
许可
MIT
