Skip to content

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 25 Oct 10:41
· 14 commits to main since this release
9fd19b9

Specus v2.0.0 是 v1.2.6 之后的第一个正式版本,包含此后合入 main 的全部改动。原计划的 v1.2.7 没有发布,它的出口分流一期内容并入本版,版本号从 1.2 升到 2.0。本版的主体是 Peer 出口分流(IPv4/IPv6 地址与网段、域名规则与系统 DNS 接管)、四个界面统一改版、服务工作台与 HTTP 发布相关的新能力,以及一批 Peer Mesh、账号授权与数据库修复。控制协议仍为 v2,帧格式不变;需要两端都升级的能力见「升级说明」。

界面全新设计

  • 四个界面改用同一套设计:浅色毛玻璃材质、唯一的系统蓝强调色、大圆角、胶囊式标签页与状态标签、系统字体,支持浅色与深色。
  • 管理后台及其公开页面(互传、客户端下载、NAT 检测、登录落地页):悬浮玻璃侧栏,每个面板以 30px 页头开始,表单标签放在输入框上方,提示改为深色玻璃浮层;菜单里的 emoji 换成线条图标。
  • CLI 本地管理页(Go、Java、.NET 共用同一份页面):新的玻璃侧栏、卡片与分段控件,跟随系统深色模式;768px 以下改为顶部标签栏。页面行为与 CSP 不变。
  • Windows 桌面客户端(WPF)与 Android 客户端按同一套设计令牌改换样式与布局,功能与绑定不变:WPF 改为悬浮侧栏、胶囊页签与卡片,保留原有的深浅主题切换(玻璃只是半透明色,没有背景模糊);Android 改为卡片、胶囊分段切换与状态标签,跟随系统深色模式。
  • 去掉内置的 Inter 字体,改用系统字体栈。各端令牌的位置与改界面时的规则见 界面设计系统。

Peer 出口分流

  • 消费端按规则把指定目标的流量经已授权的对端设备(出口)访问,应用无需逐个配置代理;未命中规则的流量照常从本机出去。Java、Go、.NET 客户端都可作为消费端与出口;消费端支持 Linux、Windows、macOS,Android 不提供出口分流。关联 #42。
  • 规则写在客户端配置的 peerEgressRules,三端共用;动作分 egress、direct、block,前缀更长者优先,单条规则可停用。保存规则与启用接管分开:总开关 peerEgressEnabled 默认 false,关闭时规则只保存、不接管,config validate、启动日志与状态都会明确提示。
  • 出口由服务端租户总开关与每台设备的出口策略共同控制,目的规则默认全部拒绝,并与基础 Peer ACL 取交集;回环、链路本地与云元数据、组播广播、组网网段与本部署的控制端点永远拒绝。服务端保存策略时按出口的读法校验目的规则(#99),策略变更立即推送给在线设备。
  • 命中规则而出口不可用时阻断,不回退到本机直连:TCP 立即复位,UDP 收到主机不可达,已建立的连接在出口离线或规则改动时被复位。出口未被提供给本机、对端运行的客户端不支持出口时同样阻断并说明原因;服务端不下发出口目录时提示服务端可能过旧(#121)。
  • 路由接管只写运行期路由表(Linux ip route,Windows ActiveStore,macOS route,重启后不存在),不改防火墙与内核转发参数;只装精确前缀,前缀已被占用时拒绝并报告占用者,不抢占。虚拟网卡就绪后才应用,每 5 秒按对端、中继与控制连接重算,并对照真实路由表补回消失或指错的条目;控制连接断开只挂起,不撤路由、不关网卡,真正退出时才撤回;崩溃留下的路由在下次启动时按安装记录收回。
  • 出口用用户态连接栈终结连接、用普通 socket 连接目标,不做内核转发;Windows 与 macOS 上出站 socket 绑定到隧道之外的接口,Linux 上打 SO_MARK 0x5350。
  • 出口到消费端方向遵守消费端通告的接收窗口,每流有发送预算,并对目标 socket 施加背压,大响应完整送达(#74);租户开关关闭等撤权后,已建立的流立即被切断(#75);RTT 取样按 Karn 规则,丢包时不会长时间停滞(#108);消费端立即复用刚关闭连接的源端口时,新连接不必等 SYN 重传(#256)。
  • 出口按消费设备限制新建流速率(可突发 128 条,之后每秒 64 条,#119),并向服务端上报计数(#125);消费端流表有界,结束或空闲的流会被回收(#102)。
  • 管理入口:管理后台「私有组网 → 出口分流」页(总开关、出口策略与活动,#97);Windows 桌面端「出口分流」页(规则、接管开关、按配置预演与连通测试、运行状态,#96);CLI egress、egress rules|rule …、egress enable|disable、egress test,以及本地管理页的「出口规则」页与出口状态面板(#98)。
  • 使用与排查见 出口分流使用说明,线协议见 peer-egress.md。

出口分流:域名规则与系统 DNS 接管

  • 打开 peerEgressDnsTakeover(默认关闭,需同时打开 peerEgressEnabled)后,规则可写精确域名或 *.example.com。命中的查询由消费端用 fake-IP 应答(默认池 198.18.0.0/15),流量到达时把域名交给出口,由出口在自己的网络里解析、授权并连接。不打开时域名规则照旧被拒绝,不会在启动时解析成 IP。
  • 出口策略新增按名字授权的 domainRules(#126)。出口解析出的地址仍要过永远拒绝清单与目标范围,解析到元数据或内网地址的名字照样被拒。
  • 这是整个功能里唯一修改系统设置的部分:Linux 经 systemd-resolved 或改写 /etc/resolv.conf,macOS 用 networksetup,Windows 添加一条 NRPT 规则。改动前写事务日志 ~/.specus/egress-dns-journal.json,退出时改回,被强杀后在下次启动或执行 egress dns restore 时回滚。开关与状态用 egress dns enable|disable|status|restore。
  • 可识别边界:应用自带 DoH/DoT、使用系统缓存或直接硬编码 IP 时,域名规则不会命中,这类流量只能靠 IP/CIDR 规则覆盖。规范见 peer-egress-dns.md。

出口分流:IPv6

  • 消费端规则与出口策略的目的规则都可写 IPv6 地址与前缀,服务端按 RFC 5952 规范形式保存(#243);名字没有 A 记录时出口按 AAAA 记录拨号(#244)。
  • 三个客户端都实现了消费端 IPv6 数据面(#245):设备的 IPv6 地址由组网 IPv4 推出(fd53:7065:6375:7300::/96),IPv6 包原样经 SPEG1 交给声明了 ipv6PacketCapable 的出口;路由接管、旁路、漂移修复与明确失败按地址族分开进行,egress test 接受 IPv6 地址。
  • peerMeshMtu 低于 1280 或设备地址没能配到虚拟网卡上时没有 IPv6 数据面,IPv6 规则以 EGRESS_RULE_IPV6_UNSUPPORTED 不生效,IPv4 不受影响。fake-IP 的 DNS 应答对 AAAA 仍回 NODATA;组网设备之间仍只走 IPv4。

Peer 组网修复

  • Go 与 .NET 服务端此前转发信令时丢掉 sourceKeyEpoch:经这两个服务端的任意两台设备能建立会话、探通直连路径,但数据一个字节都过不去。Java 与 C 服务端不受影响(#72)。
  • Go 客户端在 Linux 上此前读不到虚拟网卡的任何数据包(网卡显示 UP 但不交付);.NET 客户端在 Linux 上释放后立即重建同名网卡偶发 EBUSY(#72)。
  • 四个服务端在设备离线后向同租户对端推送花名册;Go 与 .NET 服务端不再因信令投递不到而断开发送方的控制连接(#72、#169)。
  • Go、Java、.NET、Android:对端重启后忘掉旧路径,改用新进程的地址;直连健康时不再因中继探测在两条路径之间来回切换。Go 客户端在组网未运行时收到会话信令不再崩溃(#136)。

服务工作台与 HTTP 发布

以下各项来自 #38,各有独立契约,四个服务端(Java、Go、.NET、C)与管理后台都已实现:

  • 服务工作台(#143):常用服务、最近打开与由现有列表推导的待处理问题,按登录身份保存在服务端;管理后台登录后默认进入工作台。
  • 服务连通性检查(#135、#140):对单条 HTTP route 显式发起一次有界检查,按「已配置 → 设备在线 → 目标可达 → 访问成功」逐段给出结果码;不带凭据、不读响应体、不写流量明细,并有限流。见 service-connectivity-check.md。
  • 临时 HTTP 分享(#144):为受 Basic 保护的 route 签发有效期必填、可撤销、带审计的访问链接,走独立的 /http-share/{shareId}/ 路径;默认只读,令牌只存哈希,撤销后下一次请求返回 410,在途流约 2 秒内被切断。
  • 产品指标(#146):默认关闭,由租户管理员确认说明后开启;只记录接入里程碑与互传结果的按日汇总计数,不采集凭证、文件名、文本内容或内网地址,按日计数保留 180 天,可随时清除;运营方可用部署级开关整体禁用。

浏览器互传

  • 两个浏览器页面之间的直连文件传输支持分块续传(#142):1 MiB 分块逐块 SHA-256 校验,断线后在会话内续传,重复块幂等。接收方选择写入本机存储时,刷新页面后也能续传,单文件上限 2 GiB;只在内存中接收时仍为 128 MiB。发送方刷新后需要重新选择同一文件,内容变化时停止。
  • 四个服务端为 /transfer、/download、/nat-detect、/diagram、/diagram-embed 提供直接入口,刷新页面或打开带参数的邀请链接不再失效(#288)。

账号、授权与安全修复

  • HTTP route 公网入口在没有服务端记录、route 已停用或客户端已停用时一律返回 404(四端,#137);删除或停用 route、停用/改名/删除客户端之后,客户端可靠地停止转发,服务端断开被停用、改名或删除的客户端的连接(#165)。规则见 http-route.md。
  • 客户端 token 只登录签发它的账户:Go 与 C 服务端此前按名字解析,客户端被删除后新建同名客户端,旧机器会以新账户身份登录(#178)。
  • 管理账号(management-accounts.md):Go、.NET、C 的登录名改为按租户唯一,与 Java 一致(#198);token 带账号键 uid,删除后重建同名账号不会继承旧 token,删除账号时释放其邮箱(#223)并处理其名下数据(#235);账号停用、角色变化或删除时撤销其管理 WebSocket(#249)。
  • 公网 502 不再回显客户端 RST 的 reason,其中可能含内网地址(#130);C 服务端管理用户的修改与删除限定在调用者租户内(#139);并发创建分享不再突破每条 route 的数量上限(#150)。
  • 依赖:Spring Boot 4.1.1(Spring Framework 7.0.9,#221)、jackson-databind 2.21.7 / 3.1.7(#106)、.NET 服务端固定 System.Security.Cryptography.Pkcs 10.0.13(CVE-2026-84008,#284)、Go 模块强制使用已修补的工具链(#271),以及前端 npm 补丁(#288)。

服务端对齐与稳定性

  • 控制协议:修正 Go、.NET、C 服务端登录前后的顺序竞态(#164、#170、#179、#188);一帧写失败或中途被取消后关闭连接,不再在半帧之后继续写(#247);会让单条 NAT_CONTROL 超过 1 MiB 的映射或 route 在创建、启用时以 400 拒绝,写失败的手动下发按离线返回 409(#208、#231、#239);四个服务端与 Java、Go、.NET 客户端按 RFC 8259 严格读取 NAT 元数据(#350)。
  • Direct HTTP / WebSocket:Java 服务端 route WebSocket 的浏览器消息上限从 8 KiB 提到 16 MiB;浏览器不发 CLOSE 就断开时,Java 与 Go 服务端复位设备侧的流(#337、#338、#349);.NET 服务端队列满或收到迟到帧时只重置该 HTTP 流(#175);连通性检查不再断开设备的数据连接(#149)。
  • 客户端数据连接:Java 客户端遇到慢上游、或一条流连不上本地目标时,不再影响整条数据连接(#82、#129);.NET 客户端 NAT 流从等待转为活动时不再丢流或乱序(#78);Android 修正 Direct HTTP 响应尾部与 FIN 丢失、流关闭竞态与桥接 I/O 问题(#230、#252、#269、#291)。
  • 数据库:Java 服务端修正 SQLite 约束异常与 BLOB 读取、PostgreSQL 迁移与 HTTP 明细列类型、MySQL/MariaDB 行宽与 LOB 列类型;.NET 服务端修正 MySQL、MariaDB、PostgreSQL 的初始化与迁移(#278、#280、#282),CI 用真实的 MySQL、MariaDB、PostgreSQL 测试;Go 服务端按 Java 的方式保存并展示 HTTP body(#210);.NET 与 C 服务端给已存在的 Elasticsearch 索引补上 binary body 映射(#204、#215)。
  • CLI:状态文件写入失败后每秒重试,不再永久停止发布(#109);Go 客户端在 Windows 上改用 ACL API 检查文件私有性,不再每秒启动 PowerShell(#95)。

C 服务端(实验性)

  • 继续按 Java 全量对齐(#35):OIDC 用 JWKS 校验 ID Token 并签发本地 token(#145),客户端登录 nonce 防重放与登录响应 nettyTls(#133),Direct WebSocket 完整 SWS2 状态机(#122),以及 NAT 流语义、媒体采集、互传、Peer Mesh 与 TURN、管理 API 清单的逐项对齐。
  • 新增 Redis 经 TLS(rediss://)与 Sentinel 的协调(#341)、/actuator/metrics(#342)、控制连接频率限制(#348)。Java、Go、.NET、Android 客户端经 C 服务端的端到端测试已进入 CI。
  • #35 仍未完成,C 服务端不作为 Java 服务端的完整替代。

升级说明

  • 出口分流需要服务端与消费端、出口两侧的客户端都升级到本版。旧客户端登录时不声明出口能力,服务端不会给它下发出口策略,不影响其既有组网与服务共享。
  • 经 Go 或 .NET 服务端的 Peer Mesh 数据面需要升级服务端才能连通(见「Peer 组网修复」)。
  • 服务连通性检查需要设备运行本版客户端,旧客户端只能得到「无法判定」。分块续传需要两端都是新页面;与升级前打开的旧页面互传时,128 MiB 以内走旧流程,更大的文件被取消并提示对方刷新。
  • 默认关闭:出口分流(租户开关与消费端 peerEgressEnabled)、DNS 接管、产品指标。
  • 行为变化:没有服务端记录的 HTTP route 不再按公开处理,一律 404,只保留 C 服务端的 SPECUS_HTTP_ROUTES;删除仍拥有客户端或接入凭证的管理账号返回 409,需先删除或转移,目前没有转移接口(#237);非默认租户中新建的管理账号必须在登录框填写租户 ID,升级前已有的账号不填租户仍可登录。
  • 数据库结构在启动时自动迁移。Go、.NET、C 服务端上,同一租户内若有两个管理账号的登录名在去首尾空白、转小写后相同,迁移拒绝启动,需先处理。升级前签发的管理 token 在过期前仍然有效,续期后带上 uid。
  • 出口消费端需要创建虚拟网卡(peerMeshDevice 不能是默认的 noop),并以管理员或 root 运行(Linux 也可用 CAP_NET_ADMIN)。Java 客户端用 java -jar 启动时无需改动;自己拼 classpath 或用服务包装器启动的,需要加 --add-exports java.base/sun.nio.ch=ALL-UNNAMED,否则 Windows 与 macOS 上作为出口时所有连接都会被拒绝。
  • 使用 deploy/openresty/specus.conf 的反向代理需要加入新的 location ^~ /http-share/,否则临时 HTTP 分享会落到前端页面上。
  • Go 服务端的 SPECUS_NETTY_MAX_FRAME_SIZE 不能小于 1048587,否则启动失败;默认值 32 MiB 不受影响(#239)。
  • C 服务端:Redis URI 按 Lettuce 的规则解析,带下划线的主机名加端口、密码中未转义的 @/#/? 现在会导致启动失败(#341);客户端配置的 connectionRateLimitPerMinute 开始在控制登录时生效(#348)。
  • 多实例部署滚动升级期间,旧实例不处理新增的管理身份撤销事件,其上已打开的管理 WebSocket 要到断开时才结束。
  • 从源码构建 Go 客户端与服务端需要 Go 1.27.3 或更新的已修补版本,go.mod 强制这一下限。

分发

  • 沿用全端发布流程,提供客户端、服务端、STUN 与签名 Android 包;发布不代表已部署到运行中的服务器。
  • Homebrew cask 在单独的 devShuai/homebrew-specus 仓库中另行更新;cask 更新后,已有安装执行 brew update 和 brew upgrade --cask specus-client。

验证与已知限制

  • CI:四个服务端的完整测试在相关 PR 上运行;Java、Go、.NET、Android 客户端经 C 服务端跑 NAT、Direct HTTP/WebSocket、TLS 与 Peer Mesh 端到端测试;CLI 本地管理页与命令矩阵在 Windows 与 Ubuntu 上覆盖三端。出口分流的规则、授权、帧、路由读写、出站 socket 选择与 DNS 接管命令由共享测试向量钉住,期望值来自独立的 Python 参考实现。
  • Linux 出口实验室(真机实验室)在网络命名空间里用真实虚拟网卡跑三种消费端 × 三种出口共 9 种组合:TCP/UDP、8 MiB 上下行与并发流、2% 丢包、撤权与故障注入后的防泄漏检查、DNS 接管与强杀后恢复,并按 #50 事先约定的阈值设性能门槛。实验室只用 Go 服务端,不覆盖 IPv6。
  • 跨平台真机验收(#50)尚未完成:Windows 的路由安装、撤销与回滚(IPv4 与 IPv6)没有在真机上执行过;macOS 的安装路径在 CI 的 macOS 真机上验证过,但指向回环接口而不是 utun,IPv6 路由与地址安装未验证;Windows NRPT 与 macOS networksetup 的 DNS 接管、出口经 IPv6 出网都还没有真机验收。#42 与 #245 保持打开。
  • 出口分流的其他限制:下行是固定窗口加背压,不是自适应拥塞控制;出口没有字节带宽限制;不转发 ICMP;不支持多跳出口链,拒绝 0.0.0.0/0 与 ::/0;目标看到的通常是出口所在网络的公网出口地址,不承诺等于出口设备自身地址;路由表被改后最多 5 秒内补回,这期间命中规则的目标会从本机直连出去,Linux 只对照主表,策略路由导走的流量发现不了。
  • #38 的工作台、连通性检查、临时分享、分块续传与产品指标已实现,尚未完成部署验收;分块续传的跨浏览器、跨 NAT 与移动端验收未做。
  • Windows 桌面端与 Android 的连接可靠性与首次接入问题(#40)仍待真机验收。删除客户端不会清理其 Peer Mesh 设备记录与 ACL(#237)。

What's Changed

  • feat(peer-egress): authorise a peer as a network egress across all four servers by @devShuai in #53
  • feat(peer-egress): P2 Go egress user-space connection stack and authorization enforcement by @devShuai in #54
  • feat(peer-egress): P3 消费端规则引擎、路由接管与数据面 by @devShuai in #55
  • feat(peer-egress): P4 Java 与 .NET 纯逻辑层对齐 by @devShuai in #57
  • feat(peer-egress): P4 路由接管移植到 Java 与 .NET by @devShuai in #58
  • feat(peer-egress): P4 消费端数据面移植到 Java 与 .NET by @devShuai in #59
  • feat(peer-egress): P4 出口端数据面移植到 Java 与 .NET by @devShuai in #60
  • feat(peer-egress): P4 出口数据面接进 Peer Mesh by @devShuai in #61
  • feat(peer-egress): P5 macOS 路由接管 by @devShuai in #63
  • feat(peer-egress): 把 P5 的 Windows 与 macOS 接管带进 main by @devShuai in #64
  • feat(peer-egress): P6 让功能能说出哪些规则在生效 by @devShuai in #65
  • docs(peer-egress): P8 规范对齐、使用说明与发布说明,及审计发现的两个阻断缺陷 by @devShuai in #66
  • feat(peer-egress): P5 出口 socket 绑定到隧道之外的接口 by @devShuai in #67
  • fix(peer-egress): P5 规则覆盖地址后旁路下一跳不再解析回隧道 by @devShuai in #68
  • feat(peer-egress): P5 路由计划随运行期状态保持正确 by @devShuai in #69
  • feat(peer-egress): P5 对照真实路由表补回消失或指错的路由 by @devShuai in #70
  • feat(peer-egress): P5 出口不可用时让应用立即失败 by @devShuai in #71
  • fix(peer-egress): 出口离开时对端立即得知,.NET 网卡重建不再撞 EBUSY by @devShuai in #79
  • fix(peer-egress): 控制连接重连不再撤掉路由,修复静默直连泄漏(#73) by @devShuai in #76
  • fix(peer-mesh): 两个让组网数据面完全不通的 bug,以及 Linux 真机验收实验室 by @devShuai in #72
  • fix(dotnet-client): NAT 流从 pending 转 live 时不再丢流或乱序(#77) by @devShuai in #78
  • fix(go-server): 规则匹配与共享向量对齐,并在 CI 跑 Go 服务端测试 by @devShuai in #80
  • fix(java-client): 慢上游不再卡住整条数据连接,激活中的流不再被误 RST by @devShuai in #82
  • fix(schemas): 出口定义写回源 schema,并防止再次只改生成副本 by @devShuai in #81
  • feat(peer-egress): resolve bound names on the egress side (#52, part 2) by @devShuai in #94
  • docs(peer-egress): 二期规范——域名规则与 DNS 接管,附共享向量(#52 第一部分) by @devShuai in #93
  • fix(peer-egress): TIME_WAIT 中的流不再占用并发名额(#88) by @devShuai in #92
  • feat(peer-egress): 状态显示出口路径与流数,并对每个问题给出处理办法(#49) by @devShuai in #91
  • fix(peer-egress): 出口日志不再记录目标地址与规则内容(#49 日志最小化) by @devShuai in #90
  • feat(local-ui): 本地管理页编辑出口规则与接管开关(#49 第三部分) by @devShuai in #89
  • feat(egress-cli): 命令行编辑出口规则与接管开关(#49 第二部分) by @devShuai in #87
  • feat(peer-egress): 规则可单条停用,消费端加总开关,保存与启用接管分开(#49 第一部分) by @devShuai in #86
  • fix(dotnet-client): 本地服务探测改为阻塞式连接,线程池饥饿时不再漏判 by @devShuai in #85
  • test(peer-egress): 九种语言组合进 CI,并按 #50 约定阈值设性能门槛 by @devShuai in #83
  • Java 客户端:Peer 服务撤销测试改为断言行为而非端口拒绝 by @devShuai in #84
  • feat(desktop): 桌面端出口分流页——规则、接管开关、规则测试与运行状态(#49) by @devShuai in #96
  • feat(admin-web): 管理后台出口分流页——总开关、出口策略与活动(#49) by @devShuai in #97
  • fix(go-client): Windows 上用 ACL API 检查私有状态,不再每秒拉起 PowerShell by @devShuai in #95
  • feat(local-ui): 出口预演旁加实际连通测试(#49) by @devShuai in #98
  • fix(servers): 出口管理接口按出口的读法校验目的规则,并给出正确的 4xx 状态码 by @devShuai in #99
  • test(peer-egress): 实验室首字节与丢包门槛记录卡住的位置,加 Java 出口丢包模拟 by @devShuai in #100
  • fix(servers): 升级已有数据库时补上出口会话列(否则 v1.2.6 升级后客户端无法登录) by @devShuai in #101
  • fix(peer-egress): 消费端流表有界——结束或空闲的流会被忘记(三个客户端) by @devShuai in #102
  • feat(servers): egress-catalog 带上出口声明的 domainTargetCapable(#52 第三步服务端) by @devShuai in #103
  • feat(peer-egress): 二期第三步——消费端 fake-IP 数据面、读 egress-catalog、name-bind 发送(三端) by @devShuai in #105
  • fix(peer-egress): 补洞的 ACK 不再当作 RTT 样本(Karn),修复出口在丢包下的长时间停滞(三端) by @devShuai in #108
  • fix(deps): jackson-databind 升到 2.21.7 / 3.1.7,修复三个 HIGH 级通告 by @devShuai in #106
  • fix(cli): 状态文件写失败后继续发布,不再永久停止(三端) by @devShuai in #109
  • feat(peer-egress): 二期第四步——消费端 DNS 应答者(读包路径上的 UDP/TCP、逐字节报文、转发) by @devShuai in #107
  • build(admin-web): 迁移到 Tailwind CSS 4(HeroUI 2.8 支持的组合),界面保持不变 by @devShuai in #104
  • feat(peer-egress): 二期第五步——系统 DNS 接管、事务日志与回滚、egress dns 命令(三端三平台) by @devShuai in #110
  • fix(peer-egress): 遗留 DNS 接管日志按状态文件判断主人是否在运行,不回滚正在运行的客户端的接管 by @devShuai in #113
  • fix(go-server): 关停时先排空控制连接再关库,修复 TempDir 清理偶发失败 by @devShuai in #111
  • test(lab): 二期第六步——Linux 命名空间里端到端跑通域名分流与 DNS 接管 by @devShuai in #112
  • feat(peer-egress): egress test 预演域名——命中的域名规则、DNS 处理与去向(三端 + 本地页面 + 桌面端) by @devShuai in #114
  • docs(alignment): 回填 C 服务端 CI 门禁的实际运行结果 by @devShuai in #115
  • ci(c-server): C 服务端冒烟测试补上 Go 与 .NET 客户端 by @devShuai in #116
  • docs(alignment): 记录 Go/.NET 客户端已进入 C 服务端 CI 冒烟 by @devShuai in #117
  • test(c-server): 证明路由改动会自动推送 NAT_CONTROL,并让 Go/.NET 客户端也跑运行时 E2E by @devShuai in #118
  • feat(peer-egress): 出口按消费设备限制新建流速率 by @devShuai in #119
  • feat(c-server): 只读的传输额度快照接口(#38) by @devShuai in #120
  • feat(peer-egress): 出口未提供或客户端不支持时明确提示(能力不支持) by @devShuai in #121
  • feat(c-server): Direct WebSocket 的完整 SWS2 状态机(#35) by @devShuai in #122
  • feat(peer-egress): 出口策略按域名授权(domainRules) by @devShuai in #126
  • test(transfer): 额度快照四端共享向量验收(#38) by @devShuai in #124
  • feat(peer-egress): 出口向服务端上报 egress-report by @devShuai in #125
  • fix(c-server): 会话生命周期对齐 Java/Go,并补心跳与 Direct HTTP/WebSocket 端到端证据(#35) by @devShuai in #127
  • feat(peer-egress/java): Linux 出站 socket 打 SO_MARK 0x5350 by @devShuai in #123
  • fix(java-client): 一条流连不上本地目标时只重置这一条流 by @devShuai in #129
  • fix(peer-egress): 出口拒绝能到达应用、Java 出口重启后应用新策略;实验室验证出口侧拒绝(#42) by @devShuai in #128
  • fix(server): 公网 502 不再回显客户端 RST 的 reason(四端) by @devShuai in #130
  • test(c-server): 补 TURN、对象存储、/ws/connections 与管理端点测试,修复测出的 9 处偏差(#35) by @devShuai in #132
  • docs(c-server): 文档只声明有证据的能力,补 Java→C 测试对照表与 CI 日志留存(#35) by @devShuai in #131
  • fix(c-server): 登录 nonce 防重放、nettyTls、管理 JWT 回查数据库,补 TLS 证据(#35) by @devShuai in #133
  • test(c-server): 双客户端经 C server 的 Peer Mesh 信令与 TURN 中继端到端,修复 relay 不通与过期时间格式(#35) by @devShuai in #134
  • feat(connectivity): 服务连通性检查契约与三端客户端;Java 本地 TCP 异步连接(#38) by @devShuai in #135
  • fix(go-client): mesh 未运行时收到会话信令不再崩溃 by @devShuai in #136
  • fix(server): HTTP route 入口在没有服务端记录时 fail closed(四端,安全) by @devShuai in #137
  • fix(c-server): NAT 数据连接的流级语义对齐规范与 Java/Go(#35) by @devShuai in #138
  • fix(c-server): 管理用户的修改与删除限定在调用者租户内(安全) by @devShuai in #139
  • fix(c-server): 端口已能接受连接时收到 SIGTERM 也走优雅关闭 by @devShuai in #141
  • feat(transfer): 浏览器直连文件的分块续传(契约与实现,#38) by @devShuai in #142
  • feat(connectivity): 四个服务端与管理页的服务连通性检查(#38) by @devShuai in #140
  • feat: service workbench — favourites, recent opens and derived problems (#38) by @devShuai in #143
  • feat: temporary HTTP share — expiring, revocable, audited access to a protected route (#38) by @devShuai in #144
  • feat: opt-in product metrics — onboarding milestones and transfer outcomes as daily aggregates (#38) by @devShuai in #146
  • fix(c-server): verify OIDC ID tokens and sign in with a local token, as Java does (#35) by @devShuai in #145
  • fix(java-server): store transfer expiries as fixed-width instants by @devShuai in #147
  • fix: a connectivity check no longer drops the device's data connection (C tombstones, Go client RST) (#149) by @devShuai in #151
  • fix: serialize share creation per route; private refusals on private admin endpoints (#150) by @devShuai in #152
  • fix(c-server): TURN peer and identity checks, browser header rewrite, and Java test parity for capture, Turnstile and SemVer (#35) by @devShuai in #153
  • fix(java-server): workbench upserts new rows; administrators see orphaned Peer services (#156) by @devShuai in #158
  • fix(c-server): fetch the JWK Set without holding its lock (#157) by @devShuai in #159
  • fix(c-server): align HTTP media capture, manifests and playback with Java (#35) by @devShuai in #160
  • fix: 删除或停用 route、停用/改名/删除客户端后,客户端可靠停止转发(四端服务端 + 四客户端,安全) by @devShuai in #165
  • fix(admin-web): a resume never takes a busy channel; cap unfinished memory-mode receives (#155) by @devShuai in #161
  • fix(product-metrics): switching off without a purge keeps the counts (#154) by @devShuai in #162
  • fix(c-server): Java parity for client packages, downloads, archive and diagrams; JSON keeps UTF-8 (#35, #166) by @devShuai in #167
  • fix(java-server): announce a departed client to its peers again by @devShuai in #169
  • fix(c-server): Java parity for the HTTP entry, streams, trailers, routes and client accounts (#35) by @devShuai in #168
  • fix(product-metrics): the sweep re-checks the switch when it deletes (#163) by @devShuai in #171
  • fix(go,c): token logins resolve the account by id, not by name by @devShuai in #178
  • fix(csharp-server): reset only the HTTP stream on a full queue or a late frame (#172) by @devShuai in #175
  • fix(c-server): public transfer parity — STCE decoding, discovery rules, cluster rate limits, attachments (#35) by @devShuai in #181
  • fix(java-server): translate SQLite constraint violations to DataIntegrityViolationException by @devShuai in #186
  • fix(java-server): create on SQLite the unique keys Hibernate leaves out by @devShuai in #182
  • fix(java-server): SQLite 上按表读取 schema 元数据,修复二次启动失败 by @devShuai in #187
  • fix(c-server): 登录响应先于登录发布后的任何帧写出 by @devShuai in #179
  • fix(c-server): 登录后读取 session config 的数据竞争 by @devShuai in #185
  • fix(java-server): read the schema from metadata so migrations run on PostgreSQL by @devShuai in #189
  • fix(java-server): MySQL 上建不出 HTTP 明细表,LOB 列被建成 tinyblob by @devShuai in #190
  • fix(c-server): Java parity for peer mesh sessions, TURN relay, STUN config, egress reports and peer services (#35) by @devShuai in #176
  • fix(c-server): persistent login nonces, case-safe accounts, peer mesh stats, stored HTTP bodies (#35) by @devShuai in #184
  • fix(c-server): management API inventory against Java; Elasticsearch detail, client messages, unbounded lists, overview (#35) by @devShuai in #194
  • test(c-server): tls_transport_tests ignores SIGPIPE as the server does (#192) by @devShuai in #195
  • feat(server): tenant-scoped management login names, as Java (#183) by @devShuai in #198
  • test(admin-web): cut the resumed transfer on a sender frame so a chunk always repeats (#200) by @devShuai in #201
  • fix(c-server): put the binary body fields on an Elasticsearch HTTP index that exists already by @devShuai in #204
  • fix(java-server): read SQLite BLOB columns with getBytes by @devShuai in #209
  • fix(c-server): hold any number of TCP mappings and HTTP routes per client (#191) by @devShuai in #196
  • test(c-server): keep the room limit set until the server has registered the peer (#203) by @devShuai in #205
  • fix(server): refuse a mapping or route that would push NAT_CONTROL past 1 MiB (#197) by @devShuai in #208
  • fix(go-server): keep captured HTTP bodies and show them in the detail as Java does (#202) by @devShuai in #210
  • test(java): read the client's route target from its HttpSpecusConfig in EndToEndSpecusIT (#212) by @devShuai in #213
  • fix(java-server): MySQL 老库的窄 LOB 列在 Hibernate 改表前按表一次加宽 by @devShuai in #218
  • fix(csharp-server): put the binary body fields on an Elasticsearch HTTP index that exists already by @devShuai in #215
  • test(android): take distinct ports in PeerServiceRuntimeTest and fail a read that only timed out (#206) by @devShuai in #207
  • ci: run the whole Java and .NET server suites on pull requests (#216) by @devShuai in #217
  • fix(java-server): PostgreSQL 上 HTTP 明细的 body/preview 改为 bytea/text,搜索与详情不再依赖事务 by @devShuai in #222
  • fix(c-server): Peer Mesh lists without bounds, SQLite traffic search as JPA, edge cases as Java (#35) by @devShuai in #224
  • fix(android): a peer service bridge closed right after an accept forwards nothing (#226) by @devShuai in #227
  • fix(server): account lifecycle — release emails, pin tokens to the account key, scope the built-in admin by @devShuai in #223
  • fix(android): run the Gradle daemon on any JDK 21 instead of a downloaded JetBrains one (#219) by @devShuai in #220
  • test(android): Android client x C server Direct HTTP and WebSocket E2E by @devShuai in #230
  • fix(deps): Spring Boot 4.1.1 for Spring Framework 7.0.9 (#219) by @devShuai in #221
  • fix(server): a NAT_CONTROL past 1 MiB is not sent, logged, and keeps the connection by @devShuai in #231
  • fix(c-server): the remaining management CRUD endpoints and Peer service import as Java by @devShuai in #234
  • test(android): Android client x C server NAT, runtime config, TLS and Peer Mesh E2E by @devShuai in #238
  • fix(server): 删除管理账号时处理其登录名名下的数据(#225) by @devShuai in #235
  • feat(peer-egress): IPv6 规则的写法、校验与匹配语义,服务端出口策略接受 IPv6 by @devShuai in #243
  • feat(peer-egress): 出口侧授权与拨号 IPv6 目标(AAAA) by @devShuai in #244
  • fix(server): a NAT_CONTROL write that fails answers as offline, and login keeps the connection by @devShuai in #239
  • test(c-server): match E2E server logs without grep -q under pipefail (#241) by @devShuai in #242
  • fix(c-server): the last 27 Peer Mesh / Egress endpoints, mapping create, view times, Spring binding and br as Java by @devShuai in #246
  • fix(server): a frame write that fails or is cut short closes the connection by @devShuai in #247
  • fix(csharp-server): 登录提交与登录响应合并为一次原子写 by @devShuai in #170
  • fix(go-server): attach the data plane before a data login reports success by @devShuai in #164
  • fix(c-server): 连接发布前错过的运行时 NAT_CONTROL 推送由登录补推 by @devShuai in #188
  • fix(c-server): ignore SIGPIPE in the C unit tests that write to sockets by @devShuai in #193
  • fix(c-server): stop st_storage_init from writing to a current database (nat_stream_tests timeout) by @devShuai in #228
  • fix(java-server): give SQLite IDENTITY ids the integer type under create by @devShuai in #173
  • test(java-server): save media captures through JPA in the timestamp ordering test by @devShuai in #174
  • fix(java-server): store the remaining compared timestamps as fixed-width instants by @devShuai in #148
  • fix(server): revoke management WebSocket access across all four implementations by @devShuai in #249
  • fix(c-server): serialize TLS ownership without blocking full-duplex I/O by @devShuai in #250
  • fix(android): preserve shared connections through stream close races by @devShuai in #252
  • fix(ci): capture native crashes and fix .NET FIN publication race by @devShuai in #253
  • fix(peer-egress): admit initial SYN-ACK before TCP accounting by @devShuai in #258
  • fix(lab): monotonic timing and bounded new-flow evidence by @devShuai in #260
  • test(java-client): confine backpressure fixture to a real data event loop by @devShuai in #264
  • feat(c-server): coordinate management events and access revocation by @devShuai in #262
  • fix(dotnet-server): serialize traffic counter flushes and final drain by @devShuai in #267
  • fix(go): enforce patched toolchain and binary compiler evidence by @devShuai in #271
  • test(c): isolate public transfer runtime configuration and handler lifetimes by @devShuai in #272
  • test(java-client): separate overflow and rejected-open reset observations by @devShuai in #274
  • fix(java-cli): bound TLS preparation and retain timeout evidence by @devShuai in #279
  • fix(dotnet-server): repair MySQL traffic schema and preserve upgrades by @devShuai in #278
  • fix(dotnet-server): real MariaDB initialization, Unicode and migration lock safety by @devShuai in #280
  • fix(dotnet-server): PostgreSQL native bootstrap and legacy catalogue compatibility by @devShuai in #282
  • fix(dotnet-server): pin patched Pkcs dependency for CVE-2026-84008 by @devShuai in #284
  • fix(android): drain bridge I/O and handle watchdog rejection safely by @devShuai in #269
  • fix(c-server): align registration domain punctuation with Java by @devShuai in #285
  • fix(android): close failed streams before publishing write errors by @devShuai in #291
  • test(peer-egress): 等待已发布的出口路径状态,保留原验收门禁 by @devShuai in #293
  • fix(server): 修复四端公开页面刷新与互传续传入口 by @devShuai in #288
  • test(java-client): prepare TLS failure fixtures before dialing by @devShuai in #295
  • fix(server): 对齐出口 IPv6 声明、会话排序与作用域 by @devShuai in #289
  • fix(c-server): validate complete management WebSocket ticket values by @devShuai in #297
  • fix(c-server): safely retire replaced connectivity checkers by @devShuai in #299
  • fix(c-server): align registration, runtime contracts and persistent tickets by @devShuai in #290
  • fix(c-server): align persisted expiry and conditional authorization by @devShuai in #300
  • fix(c-server): bind path variables, query parameters and the user body as the Java server does by @devShuai in #302
  • test(c-server): TCP backpressure while the data connection is not read by @devShuai in #303
  • spec(peer-egress): consumer IPv6 data plane, with vectors (#245 step 1) by @devShuai in #304
  • feat(server): store and forward ipv6PacketCapable in all four servers (#245 step 2) by @devShuai in #305
  • feat(go-client): IPv6 egress side of the peer-egress data plane (#245 step 3, Go) by @devShuai in #306
  • feat(java-client): IPv6 egress side of the peer-egress data plane (#245 step 3, Java) by @devShuai in #307
  • feat(dotnet-client): IPv6 egress side of the peer-egress data plane (#245 step 3, .NET) by @devShuai in #308
  • docs(alignment): registration email forms no longer differ in C by @devShuai in #312
  • feat(go-client): IPv6 consumer data plane (#245 step 4, Go) by @devShuai in #309
  • feat(java-client): IPv6 consumer data plane (#245 step 4, Java) by @devShuai in #310
  • feat(dotnet-client): IPv6 consumer data plane (#245 step 4, .NET) by @devShuai in #311
  • diag(peer-egress): log a new SYN that meets a flow the egress still holds (#256 evidence) by @devShuai in #313
  • diag(dotnet-client): log a new SYN that meets a flow the egress still holds (#256 evidence) by @devShuai in #314
  • fix(c-server): request firewall, path decoding and repeated query parameters as Java by @devShuai in #315
  • fix(c-server): client download link validation as Java, with a shared vector by @devShuai in #316
  • test(c-server): verify temporary HTTP shares against the Java share tests by @devShuai in #317
  • fix(c-server): enforce Java's mapping port and peer device IP unique keys by @devShuai in #320
  • test(c-server): workbench checks as Java's workbench tests, identity index by @devShuai in #318
  • test(c-server): match Java product metrics, media capture, accounts and registration checks by @devShuai in #319
  • fix(c-server): answer management user errors exactly as Java by @devShuai in #321
  • test(c-server): baseline unit checks from the Java reference tests by @devShuai in #322
  • test(c-server): baseline peer mesh checks; replay empty catalogues per publisher session by @devShuai in #323
  • fix(c-server): client download catalogue keys and attachment defaults as Java by @devShuai in #324
  • fix(c-server): route, egress and entry answers as Java; baseline admin checks by @devShuai in #325
  • docs(alignment): recount the 79-class baseline after the per-method re-check by @devShuai in #326
  • fix(c-server): one transaction per batch for the timestamp migration by @devShuai in #327
  • fix(c-server): count traffic usage as Java does (#35 item 45) by @devShuai in #328
  • fix(c-server): keep long request targets whole, honour SPECUS_HTTP_* limits (#35) by @devShuai in #329
  • fix(c-server): management WebSockets no longer stall on a slow reader (#35 item 45) by @devShuai in #330
  • fix(c-server): recover Redis coordination and isolate discovery sockets (#35 item 45) by @devShuai in #331
  • fix(c-server): time the Direct HTTP waits on the monotonic clock (#35) by @devShuai in #332
  • fix(c-server): STUN/TURN relay bounds, accounting and wire details as Java (#35 item 45) by @devShuai in #333
  • fix(c-server): keep a stopped TCP stream's socket until its workers are done (#35) by @devShuai in #334
  • fix(c-server): route WebSockets no longer stall the data connection on a slow browser (#35 item 45) by @devShuai in #335
  • fix(c-server): public HTTP connections and route failures as Tomcat/Java (#35 item 45) by @devShuai in #336
  • fix(go-server): reset the stream when a route WebSocket browser drops without a CLOSE (#35) by @devShuai in #338
  • fix(c-server): refuse a login whose connectionRole is null instead of crashing by @devShuai in #339
  • fix(server): route WebSockets reset on a dropped browser and take 16 MiB messages (#35) by @devShuai in #337
  • fix(java-server): count codec rejections in specus.control.protocol.rejected (#35) by @devShuai in #340
  • feat(c-server): Redis coordination over TLS (rediss://) and through Sentinel, as Java/Lettuce (#35) by @devShuai in #341
  • ci(c-server): give the serial Makefile tests 900 s by @devShuai in #344
  • feat(c-server): Java's Micrometer meters on /actuator/metrics (#35) by @devShuai in #342
  • fix(c-server): reclaim NAT streams and listeners, survive accept errors, honour SPECUS_NETTY_* (#35 item 45) by @devShuai in #343
  • fix(c-server): read heartbeat and logout bodies as Java's codec does (#35) by @devShuai in #345
  • fix(c-server): read a null messageType as Java's codec does by @devShuai in #346
  • fix(client): reuse a TIME_WAIT egress four-tuple for a new SYN (#256) by @devShuai in #347
  • fix(c-server): enforce the control connection rate limit as Java (#301 rebased) by @devShuai in #348
  • fix(server): route WebSocket protocol errors and close credit timeouts as the other servers (#35) by @devShuai in #349
  • fix(protocol): read NAT metadata strictly by RFC 8259 in every implementation (#35) by @devShuai in #350
  • Redesign all UIs to the Liquid Glass design system by @devShuai in #351
  • Release v3.0.0 by @devShuai in #352
  • Release v2.0.0 (correct the version from 3.0.0) by @devShuai in #353

Full Changelog: v1.2.6...v2.0.0