首页
/ croc 网页客户端深度指南:croc-web 统一服务器、WASM 安全协议与 WebSocket 中继桥接

croc 网页客户端深度指南:croc-web 统一服务器、WASM 安全协议与 WebSocket 中继桥接

2026-09-05 17:44:45作者:董灵辛Dennis

croc 的网页客户端(web client)是一个 React/Vite 应用,可与普通 croc CLI 对端完成文件收发和短文本消息直传;其生产构建与 WebAssembly 运行时被嵌入独立的 croc-web 二进制,由这一个 HTTP 地址同时承载网站、运行时配置、健康检查和 WebSocket-to-TCP 中继桥。本文基于 web/README.md 与对应源码展开,帮你掌握 croc-web 的本地开发、自托管中继池配置、生产拓扑与存储模式(stored mode)的完整运维细节,以及安全敏感协议操作如何被编译进 WASM 供浏览器执行。

一、croc web 客户端定位:浏览器与 CLI 对端之间保持加密

croc-web 的客户端能力包括:

  • 与普通 croc CLI 对端发送和接收文件,以及短的直接文本消息;
  • 文件元数据与内容在浏览器和另一 croc 客户端之间保持加密;
  • 生产构建与 WebAssembly 运行时嵌入 croc-web 二进制,部署后的二进制不需要任何外部静态文件。

启动时如果带上 --store-dir,UI 会额外提供一个显式的 stored 模式,由发送方选择一个有限的生存期。该模式在浏览器内加密文件名、元数据和 4 MiB 分块,只上传密文,并同时产出一个浏览器链接和一个 CLI 令牌;传输会在“已验证下载次数额度”或“所选生存期”中先到者触发时被删除。常规直传(direct)模式仍是默认模式。

两种发送模式都可以为浏览器接收地址显示 QR 码:直传模式的二维码打开接收页并自动填好 croc 码开始连接;stored 模式的二维码包含完整的加密分享链接。直传模式还提供与 croc send --text 兼容的次级文本编辑器:文本在显示前被审查、在内存中校验,从不落盘到临时存储,且文本载荷限制为 1 MiB 的 UTF-8 内容。

进度方面,活跃的发送与接收会显示总量与单文件进度、实测字节/秒速率,以及用 arrival-time 库计算的 ETA(见 web/package.json 中的 arrival-time 依赖)。

二、安全敏感操作编译进 WASM:同一份 Go 协议代码跑在浏览器里

这是 croc web 最核心的设计:安全敏感的协议操作不是用 JavaScript 重写,而是直接由本仓库的 Go 包编译为 WebAssembly,从而与原生客户端共享同一份密码学实现。文档列出的 WASM 能力包括:

  • croc PAKE,用于中继(relay)握手与对端(peer)握手;
  • 与身份和会话绑定的 HKDF,加对端通道的相互确认(mutual confirmation);
  • 用于兼容旧版本的中继单跳(legacy-compatible relay hop)的 PBKDF2,以及 AES-GCM 加密;
  • 原始 DEFLATE 压缩;
  • xxhash 校验;
  • EFF 三词码生成与兼容性解析;
  • 与原生客户端共享的 SHA-256 码到中继路由。

从源码看,WASM 入口在 web/wasm/main.go,它通过 syscall/js 把一组函数挂到全局 crocWasm 对象上,与上述文档条目一一对应:

  • pakeInit / pakeInitWithIdentities / pakeUpdate:分别基于 pake 库和仓库内 src/pakekey/pakekey.go 的带身份 PAKE;pakeUpdate 完成握手并返回会话密钥;
  • derivePeerKeys / confirmPeerKey:通过 pakekey.Derive 用目的、房间、曲线、发起方/响应方身份与盐派生加密密钥和两个确认标签,再校验对端确认,实现通道密钥与身份、会话的绑定;
  • deriveKey:调用 src/crypt/crypt.gocrypt.New(PBKDF2)派生中继跳密钥;
  • encrypt / decrypt:AES-GCM 加解密;cipherInit / cipherRelease / encodeChunk / decodeChunk 维护 AEAD 句柄并按分块加密(可选先压缩);
  • compress / decompress:调用 src/compress/compress.go,解压时要求显式字节上限;
  • hashInit / hashUpdate / hashFinal:流式 xxhash;
  • codeComponents / relayIndex:基于 src/codephrase/codephrase.go 解析 croc 码并计算码对应的中继索引(SHA-256 路由),与原生客户端共享逻辑;
  • sha256Init 等:SHA-256 摘要;
  • storeGenerateKey / storeRedeemCapability / storeSealManifest / storeOpenManifest / storeSealChunk / storeOpenChunk:stored 模式在浏览器内完成密钥生成、能力赎回、清单与 4 MiB 分块的密封/开启,底层是 src/storecrypto/storecrypto.go

每个暴露函数都走 safeCall 包装(recover panic 后返回 {ok:false,error}),字节数据通过 js.CopyBytesToGo / js.CopyBytesToJSUint8Array 传递。构建脚本为 web/scripts/build-wasm.mjsnpm run wasm),这解释了“浏览器端只上传密文”的可信边界:加密路径不经过 JavaScript 生态的第三方依赖。

三、本地开发:一条命令跑起完整客户端 + croc-web

web 目录下:

npm install
npm run dev:stack

该流程会先构建并嵌入完整客户端,然后运行:

croc-web localhost:5173

这个本地快捷方式会直接把服务绑定到 localhost:5173,网站和 WebSocket 中继都在同一地址可用。这一行为由 src/webcli/webcli.goresolveServeAddress 实现:当显式给出带端口的回环地址(如 localhost:5173)且未显式提供 --bind 时,bind 直接沿用该公网地址。

需要前端热重载时运行:

npm run dev:hot

dev:hot 会先执行 embed,再用 concurrently 并行跑 Vite dev server 与 croc-web --bind 127.0.0.1:9014 localhost:5173(见 web/package.json 的 scripts 定义)。

常用检查命令:

npm test              # vitest 单元测试
npm run test:e2e      # Playwright 端到端(先 embed)
npm run typecheck     # tsc -b
npm run build
npm run embed
make build-web
go test ./...

npm run embed 构建 WASM 与 Vite 客户端,并把产物复制到被 git 忽略的 src/webassets/dist 目录,Go 的 embed 包只在 croc-web 中打包它;生成文件不提交仓库。部署后的 croc-web 二进制既不需要该目录,也不需要任何外部静态文件。从源码看:

Playwright 套件(npm run test:e2e)会构建真实的 croccroc-web 二进制,在临时端口上启动隔离的本地 croc 中继与统一嵌入式服务器,然后逐字节验证 CLI → Web、Web → CLI、Web → Web、CLI stored → Web、Web stored → CLI 五类传输(见 web/e2e/transfers.spec.ts)。浏览器安装一次即可:npx playwright install chromium。测试进程使用隔离的 CROC_CONFIG_DIR 与存储目录,不读取也不改动你已记住的 croc 配置。

四、自托管中继池:croc-web 作为受约束的 WebSocket 到 TCP 桥

服务器端固定一个有序的上游主机列表并放行所有 TCP 端口,使 /ws 不可能被当成任意网络代理使用。单主机自托管池示例:

croc-web --pass YOUR_RELAY_PASSWORD \
  --bind 127.0.0.1:9014 \
  --relays relay.example.com \
  --ports 9009,9010,9011,9012,9013,9014,9015,9016,9017 \
  files.example.com

files.example.com 是作为位置参数传入的公网网站地址(UsageText 为 croc-web [OPTIONS] [public-host[:port]])。服务器通过 /config.js 把权威有序中继地址与密码作为浏览器默认值注入:

window.__CROC_RUNTIME_CONFIG__ = {"gatewayURL":"/ws","relayAddresses":[...],"relayPassword":"...","store":{...}};

对应实现在 src/webrelay/webrelay.goconfig handler(带 Cache-Control: no-storeX-Content-Type-Options: nosniff)。公共客户端使用 1.getcroc.com,2.getcroc.com,3.getcroc.com,4.getcroc.com(即 src/publicrelay/publicrelay.go 中固定的四台 9009 端口池,也是 --relays 的默认值);运维可以用 --relays 提供另一个逗号分隔的顺序。

关于“最佳中继”记忆:直传生成的发送会把胜出中继地址存进功能性 cookie croc-best-relay,有效期 30 天;之后的发送直接复用该精确地址,不做探测也不延长 cookie 有效期;无效池条目会被自动替换;中继连接失败会清除 cookie,让下一次发送重新竞速整个配置的池;手动清除站点 cookie 等价于强制同样的刷新。

/ws 端点的约束在源码中非常明确:src/webrelay/webrelay.gowebsocket handler 只接受 relay=<零基索引>&port=<放行端口> 两个参数,索引必须落在配置的主机列表内、端口必须落在放行集合内,否则返回 403;连接建立后只是用 64 KiB 缓冲做双向 io.CopyBuffer 的透明字节流转发,不参与 croc 协议本身。

croc-web 的完整命令行参数(默认值取自 src/webcli/webcli.go):

参数 默认值 说明
--bind 127.0.0.1:9014 本地 HTTP 绑定地址
--relays / --relay 1.getcroc.com:9009,...(公共池) 有序逗号分隔的上游 croc 中继主机
--ports 9009,...,9017 放行的上游中继端口
--pass models.DEFAULT_PASSPHRASE 中继密码,支持环境变量 CROC_PASS;若值是可读文件路径则读取文件内容
--debug false 开启 debug 日志
--store-dir 空(不启用) 在此目录启用加密临时存储
--store-max-transfer 1GiB 单个 stored 传输的最大明文字节
--store-quota 5GiB 所有受管 stored 传输的总字节上限
--store-min-free 512MiB 磁盘需保留的空闲空间
--store-max-files 100 单个 stored 传输的最大文件数
--store-downloads 1 每个 stored 传输的最大已验证下载次数(CROC_STORE_DOWNLOADS
--store-max-expiration 0(不限) 最长 stored 生存期,单位 m/h/d/w(CROC_STORE_MAX_EXPIRATION
--store-create-rate 5 每客户端 IP 每小时最多创建的 stored 传输数
--store-active-uploads 2 每客户端 IP 的并发上传数
--store-trusted-proxy 可重复;信任的反向代理 CIDR,用于客户端 IP 转发

注意 --passdeterminePass 逻辑:如果该值恰好是一个可读文件的路径,会读取文件内容作为实际密码,适合把密码放在磁盘文件里而非命令行。

五、统一服务器暴露的端点

Handler 的路由注册(src/webrelay/webrelay.go)看,croc-web 一个进程提供:

  • GET / 与内嵌静态客户端资源;User-Agent 为 curl/wget/ 时返回安装脚本(default.txt),浏览器则收到 Web 客户端(响应带 Vary: User-Agent);
  • GET /config.js:运行时配置注入,见上一节;
  • GET /healthz:返回 ok
  • GET /ws?relay=<零基索引>&port=<放行端口>:升级为二进制 WebSocket,两个值都必须命中配置的白名单,单条 WebSocket 消息上限 65 MiB;
  • 当设置 --store-dir 时,额外注册 /api/v1/store/transfers 及其子路径,由 store.Service 处理。

静态文件缓存策略也值得注意:assets/ 前缀资源是 public, max-age=31536000, immutable,HTML 页面 no-cache,Service Worker 文件 no-cache

六、生产拓扑:HTTPS 反向代理 + 存储模式

生产环境把 croc-web 放在 HTTPS 反向代理之后:

croc-web --bind 127.0.0.1:9014 getcroc.com

完整回源(包括 WebSocket 升级)都指向 127.0.0.1:9014,并保留原始 Host 头。服务器在 / 返回站点、在 /ws 返回 WebSocket 桥,因此无需拆分路由或外部静态文件部署;TLS 证书留在反向代理。

可选的 Umami 分析只有在两个运行时变量都设置时才启用:

UMAMI_URL=https://umami.schollz.com \
UMAMI_WEBSITE_ID=website-uuid \
croc-web --bind 127.0.0.1:9014 getcroc.com

成功的浏览器传输会发出 send-directsend-with-storagereceive 自定义事件。未配置 Umami 时事件追踪关闭、传输行为完全一致;已配置时,服务器把 Umami 的 defer 脚本直接注入每个页面的 <head>(见 injectUmamiScript,要求 URL 必须为 HTTPS)。Umami 记录常规页面浏览,自定义传输事件上报的 URL 会去掉查询串与 fragment。服务器在成功向 curl GET / 交付安装脚本后还会发出 installer-curl 事件(wget 下载与 HEAD 请求不计入),该服务端事件只上报站点主机名、/ 路径、事件名和 croc 版本。

临时存储在未显式配置目录前保持关闭:

croc-web \
  --bind 127.0.0.1:9014 \
  --store-dir /var/lib/croc/store \
  --store-max-transfer 1GiB \
  --store-quota 5GiB \
  --store-min-free 512MiB \
  --store-max-expiration 2w \
  getcroc.com

运维要求:存储目录被锁给单个服务器进程,应是持久、私有、仅 croc 服务账号可写的目录,并排除出备份。若可信反向代理提供 X-Forwarded-For,用可重复的 --store-trusted-proxy CIDR 标识其网络;不受信任的转发头会被忽略。全部限制与运维细节见 src/docs/STORED_TRANSFERS.md

--bind 默认为 127.0.0.1:9014;当显式使用带端口的回环网站地址(如 localhost:5173)时,本地快捷方式会绑定到该地址,除非显式提供 --bind

七、当前边界与限制

文档明确列出的当前能力边界(适用于本仓库版本):

  • 一次一个对端、一次一个传输;
  • 文本消息仅限直传模式;stored 模式只接受常规文件;
  • 可以一次发送多个选中的文件;发送文件夹与 ZIP 打包未实现;
  • CLI 发送的嵌套文件夹与空文件夹,在浏览器支持目录访问时可以接收;逐文件下载的兜底方案会把文件名压平并在冲突时追加数字后缀;
  • 本地组播(multicast)、浏览器到浏览器直传、非 xxhash 的实时传输未实现;
  • stored 上传仅接受常规文件。CLI 端 stored 下载可续传已完成分块;浏览器 stored 下载在受支持时以流式方式工作,标签页关闭后不续传;
  • 目标是当前主流常青(evergreen)桌面浏览器。

参考入口

登录后查看全文
热门项目推荐
相关项目推荐