NPS-Release

English Version 中文版

RFC 编号: NPS-RFC-0001 标题: 为 NCP 原生模式加入连接前导,用于流量识别 状态: Accepted(Phase 1 —— spec + .NET 参考实现已落地) 作者: Ori Lynn iamzerolin@gmail.com(LabAcacia) Shepherd: Ori Lynn(1.0 之前快速通道,见 spec/cr/README.cn.md创建时间: 2026-04-21 最近更新: 2026-04-25 通过时间: 2026-04-25(1.0 之前快速通道;见 spec/cr/README.cn.md激活时间: (首个参考 SDK 发布时填入,目标 v1.0-alpha.3) 替代: 被替代: 影响规范: NPS-1 NCP、spec/error-codes.md、spec/status-codes.md 影响 SDK: .NET、Python、TypeScript、Java、Rust、Go —

NPS-RFC-0001: 为 NCP 原生模式加入连接前导,用于流量识别

1. 概要

规定每个 NCP 原生模式 客户端在 TCP / QUIC 握手完成后、首个 HelloFrame 发出之前,必须发送一次 8 字节常量前导 b"NPS/1.0\n"。 服务端若在这 8 字节里读到任何其它字节序列,必须关闭连接且 ErrorFrame。HTTP 模式 不受 影响。新增一个错误码 (NCP-PREAMBLE-INVALID)和一个状态码(NPS-PROTO-PREAMBLE-INVALID)。

2. 动机

NCP 原生模式目前把 HelloFrame (0x06) 当作握手后首字节的信号。对守规矩的 对端可用,但有两个运维上的弱点:

  1. 错路由流量不好便宜地拒掉。 非 NPS 客户端(HTTP/1.x 探测器、Redis 客户端、端口扫描器、配错的反向代理)打到原生端口时,服务端会把第一个 字节当帧类型解析,读一个假长度,最后在下游解析失败。加一个常量前导 后,服务端第一次 read(8) 就能拒掉,帧解析器完全不暴露给陌生输入。

  2. 大版本升级在线上没有明确的闸门。 NPS 2.0 客户端连到 1.x 服务端 时,会走到 CapsFrame 协商中间才失败,而不是在任何帧解析之前被干净 地以”协议版本错”拒掉。前导里嵌版本号可以在字节层面给大版本兼容性 一个便宜的显式检查点。

此 RFC 同时回应 2026-04-20 的一条 review 评论,评论者认为 NCP “需要 magic code 来处理 TCP 粘包”。该评论把两件事混在一起(长度前缀解决帧 定界 vs. 连接级流量识别)——NCP 的长度前缀帧头已经解决了前者(见 §5.2),而连接级流量识别是另一件合理的事,由本 RFC 解决。

3. 非目标

本 RFC

4. 详细设计

4.1 线格式 / 帧变更

前导字节序列(每条连接发一次,所有帧之前):

偏移    0   1   2   3   4   5   6   7
       ┌───┬───┬───┬───┬───┬───┬───┬───┐
       │ N │ P │ S │ / │ 1 │ . │ 0 │\n │
       └───┴───┴───┴───┴───┴───┴───┴───┘
       0x4E 50  53  2F  31  2E  30  0A

8 字节 ASCII,以 0x0A(LF)结束。Hex:4E 50 53 2F 31 2E 30 0A

为什么选 ASCII + LF:

握手流程(原生模式):

Client                                        Server
  │                                              │
  │──── TCP/QUIC handshake ───────────────────── │
  │                                              │
  │──── 8 字节 "NPS/1.0\n" (前导) ────────→ │
  │                                              │  校验
  │                                              │  (不匹配就关连接,
  │                                              │    不发 ErrorFrame)
  │──── HelloFrame (0x06) ────────────────────→ │
  │                                              │
  │ ←─── CapsFrame (0x04) ────────────────────── │
  │                                              │
  │──── 应用帧 ←→ ───────────────────────────── │

前导 不是 帧——没有帧头、没有 Flags、没有长度字段,就是常量字节。

服务端校验规则:

  1. 接受原生模式连接后,服务端恰好读 8 字节。
  2. 8 字节若等于 b"NPS/1.0\n",进入帧解析。
  3. 不匹配时:
    • 服务端必须在 500 ms 内关连接。
    • 服务端不得ErrorFrame(对端未知是否讲 NCP;写 ErrorFrame 会把帧结构泄给扫描器)。
    • 服务端可以记录收到的前 32 字节供运维诊断。
  4. 10 秒内收不满 8 字节,当超时处理,静默关连接。

客户端行为:

大版本未来语义:

4.2 Manifest / NWM 变更

无。前导在传输层面下方,不触碰任何 NWM 表面。

4.3 错误码

新增错误码spec/error-codes.md):

错误码 NPS 状态码 描述
NCP-PREAMBLE-INVALID NPS-PROTO-PREAMBLE-INVALID 客户端前导非法/畸形;连接被关,不返回帧级响应

新增状态码spec/status-codes.md):

NPS 状态码 HTTP 映射 描述
NPS-PROTO-PREAMBLE-INVALID 400 Bad Request(不在线上发出,仅原生模式) 原生模式前导不匹配

注:此状态码永不上线传输——服务端是静默关连接。存在它的目的是 SDK 内部遥测(日志、指标)可以一致地给这种关闭原因分类。

帧类型命名空间预留spec/frame-registry.yaml):

加一条备注:原生模式连接的首字节若是 0x4E(ASCII N),应解释为 前导起点,而不是帧类型。0x4E 不得分配给任何 NCP 帧。

4.4 状态机 / 流程

原生模式服务端连接状态:

[LISTEN] ──accept──→ [PREAMBLE-WAIT] ──8 字节匹配──→ [FRAMING]
                           │                              │
                           │──不匹配──→ [CLOSING]         │
                           │──超时(10s)──→ [CLOSING]     │
                                                          │
                                        [FRAMING] ──────→ [HANDSHAKE]
                                                    HelloFrame
                                                          │
                                        [HANDSHAKE] ────→ [ESTABLISHED]
                                                    发出 CapsFrame

超时:

4.5 向后兼容

破坏性变更理由:原生模式按 spec/NPS-Roadmap.md 还在 Phase 2+,尚无 GA 用户依赖。现在破比 1.0 GA 后再补要便宜得多。


5. 考虑过的替代方案

5.1 每帧 magic 字节

每个帧加 2 字节 magic(如 0x4E 0x50)。

5.2 什么都不做(把 HelloFrame 当隐式前导)

保持现状:首帧就是 HelloFrame (0x06);服务端直接解析;非 NPS 流量 在帧解析阶段失败。

5.3 二进制 magic(4 字节非 ASCII)

比如 0x89 4E 50 53(PNG 风格高位 + “NPS”)。

5.4 只用 TLS ALPN token

靠 TLS ALPN(nps/1)做协议识别,不加带内前导。


6. 缺点与风险


7. 安全考虑


8. 实施计划

8.1 分阶段

Phase 范围 出口标准
1 规范合并 + .NET 参考实现(NpsNativeOptions.RequirePreamble 开关,默认 false) 单元测试绿;跨版本互通测试(前导客户端 ↔ 前导服务端)
2 6 个 SDK 全部实现,开关默认 false 跨 SDK 互通矩阵绿;至少一个 SDK 的原生模式 sample 在双方都启用前导的情况下端到端跑通
3 各 SDK 默认开关翻到 true;release notes 明确破坏性 一个发布周期无开放回归;min_agent_version 升级,走 21 天废弃窗口
4 移除开关——前导强制 每个 SDK 的开关移除 PR 合入;文档更新

8.2 SDK 覆盖矩阵

SDK 负责人 状态 备注
.NET Ori Lynn pending 主参考;最先落地
Python 待定 pending  
TypeScript 待定 pending 浏览器:原生模式不适用;仅 Node.js
Java 待定 pending  
Rust 待定 pending  
Go 待定 pending  

8.3 测试计划

本 RFC 要带进来的新测试:

  1. 前导合法往返:客户端发 NPS/1.0\n + HelloFrame;服务端接受、 回 CapsFrame。
  2. 前导非法拒绝:客户端发任意 8 字节("GET / HTT"、全零、 NPS/2.0\n);服务端 500 ms 内关连接,无帧响应。
  3. 前导截断:客户端发 3 字节后挂起;服务端 10 秒超时后关连接。
  4. HTTP 模式不受影响:HTTP 模式请求继续成功,无前导预期。
  5. 未来版本拒绝:客户端发 NPS/2.0\n;服务端在关之前写 NPS-PREAMBLE-UNSUPPORTED-VERSION\n 诊断(测试断言连接被关,诊断 字符串是可选的)。

现有测试改动:

跨 SDK 互通:

8.4 基准

不需要新基准。每条连接线上多 8 字节开销,在任何交换 >1 帧的连接上都 被摊薄到近零。延迟影响为零,因为客户端可以把前导 + HelloFrame 放一 次 write。

如果将来实测发现 10 秒 PREAMBLE-WAIT 超时挡住了超过 0.1% 的合法连 接,重开本 RFC 的 OQ-2。


9. 实证数据

暂无。进入 Accepted 之前会挂一个实验分支。目标测量:

指标 基线 建议 差值 方法
每连接线上开销 0 字节 8 字节 +8 B 平凡
握手 RTT(localhost loopback) TBD TBD 目标:无回归 .NET BenchmarkDotNet 在原生模式 loopback
服务端拒掉非 NPS 扫描的成本 完整帧解析尝试 8 字节 memcmp 目标:至少便宜 10× 扫描模拟工具

10. 开放问题


11. 后续工作


12. 参考


附录 A. 修订记录

日期 作者 变更
2026-04-21 Ori Lynn 初稿
2026-04-25 Ori Lynn 走 1.0 之前快速通道 Accept。已落地 spec 改动:NPS-1-NCP §2.6.1、错误码 NCP-PREAMBLE-INVALID、状态码 NPS-PROTO-PREAMBLE-INVALIDframe-registry.yaml 中保留 0x4E。Phase 1 .NET 参考 helper(NPS.Core.Ncp.NcpPreamble)同时落地;Phase 2(其余 5 个 SDK)和 Phase 3(默认开启)按 RFC §8.1 推迟。