| 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
—
规定每个 NCP 原生模式 客户端在 TCP / QUIC 握手完成后、首个
HelloFrame 发出之前,必须发送一次 8 字节常量前导 b"NPS/1.0\n"。
服务端若在这 8 字节里读到任何其它字节序列,必须关闭连接且 不 发
ErrorFrame。HTTP 模式 不受 影响。新增一个错误码
(NCP-PREAMBLE-INVALID)和一个状态码(NPS-PROTO-PREAMBLE-INVALID)。
NCP 原生模式目前把 HelloFrame (0x06) 当作握手后首字节的信号。对守规矩的
对端可用,但有两个运维上的弱点:
错路由流量不好便宜地拒掉。 非 NPS 客户端(HTTP/1.x 探测器、Redis
客户端、端口扫描器、配错的反向代理)打到原生端口时,服务端会把第一个
字节当帧类型解析,读一个假长度,最后在下游解析失败。加一个常量前导
后,服务端第一次 read(8) 就能拒掉,帧解析器完全不暴露给陌生输入。
大版本升级在线上没有明确的闸门。 NPS 2.0 客户端连到 1.x 服务端
时,会走到 CapsFrame 协商中间才失败,而不是在任何帧解析之前被干净
地以”协议版本错”拒掉。前导里嵌版本号可以在字节层面给大版本兼容性
一个便宜的显式检查点。
此 RFC 同时回应 2026-04-20 的一条 review 评论,评论者认为 NCP “需要 magic code 来处理 TCP 粘包”。该评论把两件事混在一起(长度前缀解决帧 定界 vs. 连接级流量识别)——NCP 的长度前缀帧头已经解决了前者(见 §5.2),而连接级流量识别是另一件合理的事,由本 RFC 解决。
本 RFC 不:
Content-Type: application/nwp-frame
消歧。HelloFrame / CapsFrame 的协商语义。ALPN 字符串。ALPN 是正交的发现机制;后续可能有单独的
RFC 要求原生模式在 QUIC 上用 nps/1 ALPN,但不属于本 RFC。前导字节序列(每条连接发一次,所有帧之前):
偏移 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:
tcpdump/Wireshark 抓包能直接读出 NPS/1.0\n,零歧义。N(0x4E)不与任何 NCP 帧类型冲突——0x40–0x4F 段当前
只有 NWP 帧 0x41–0x43,0x4E 是 Reserved;§4.3 正式把这个位
预留下来。\n 给按行解析的协议探测工具一个干净的停点。HTTP/1.1 首行是 HEAD/GET /POST 等——任何合规的 HTTP 解析器
都解析不出以 NPS/ 开头的请求行。HTTP/2 前导是 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n(24 字节,以
PRI 开头),跟我们完全区分开。握手流程(原生模式):
Client Server
│ │
│──── TCP/QUIC handshake ───────────────────── │
│ │
│──── 8 字节 "NPS/1.0\n" (前导) ────────→ │
│ │ 校验
│ │ (不匹配就关连接,
│ │ 不发 ErrorFrame)
│──── HelloFrame (0x06) ────────────────────→ │
│ │
│ ←─── CapsFrame (0x04) ────────────────────── │
│ │
│──── 应用帧 ←→ ───────────────────────────── │
前导 不是 帧——没有帧头、没有 Flags、没有长度字段,就是常量字节。
服务端校验规则:
b"NPS/1.0\n",进入帧解析。ErrorFrame(对端未知是否讲 NCP;写
ErrorFrame 会把帧结构泄给扫描器)。客户端行为:
HelloFrame(无需等 RTT),因为
服务端只是按常量 memcmp 校验。大版本未来语义:
b"NPS/2.0\n" 等保留给未来大版本。NPS/2.0\n 时必须关连接(未知大版本),关之前
可以 写一次 32 字节诊断 NPS-PREAMBLE-UNSUPPORTED-VERSION\n。
这种情况允许写诊断,因为对端已经自报 NPS 身份。1.0 里那个 0)是 Reserved;v1 客户端必须发 0,v1 服务端
只能接受 0。次版本协商另开 RFC 定义。无。前导在传输层面下方,不触碰任何 NWM 表面。
新增错误码(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 帧。
原生模式服务端连接状态:
[LISTEN] ──accept──→ [PREAMBLE-WAIT] ──8 字节匹配──→ [FRAMING]
│ │
│──不匹配──→ [CLOSING] │
│──超时(10s)──→ [CLOSING] │
│
[FRAMING] ──────→ [HANDSHAKE]
HelloFrame
│
[HANDSHAKE] ────→ [ESTABLISHED]
发出 CapsFrame
超时:
PREAMBLE-WAIT → CLOSING:10 秒(覆盖慢链路 + TLS 握手尾部;
同时足够严防御 slowloris)。CLOSING:决定后 500 ms 内必须关完。NCP-FRAME-UNKNOWN-TYPE。NCP-PREAMBLE-INVALID。min_agent_version 吗?是。升到实现本 RFC 的首个版本;
走 21 天窗口。破坏性变更理由:原生模式按 spec/NPS-Roadmap.md 还在 Phase 2+,尚无
GA 用户依赖。现在破比 1.0 GA 后再补要便宜得多。
每个帧加 2 字节 magic(如 0x4E 0x50)。
保持现状:首帧就是 HelloFrame (0x06);服务端直接解析;非 NPS 流量
在帧解析阶段失败。
比如 0x89 4E 50 53(PNG 风格高位 + “NPS”)。
tcpdump 不如 ASCII 直读;丢掉”HTTP 解析器立刻拒”的
性质。靠 TLS ALPN(nps/1)做协议识别,不加带内前导。
nps/1 ALPN。防御
深度。memcmp 等价操作、无动态分配、
无长度字段解析、无 unicode。审计难度严格低于现有帧头解析器。PREAMBLE-WAIT 超时缓解。| 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 合入;文档更新 |
| SDK | 负责人 | 状态 | 备注 |
|---|---|---|---|
| .NET | Ori Lynn | pending | 主参考;最先落地 |
| Python | 待定 | pending | |
| TypeScript | 待定 | pending | 浏览器:原生模式不适用;仅 Node.js |
| Java | 待定 | pending | |
| Rust | 待定 | pending | |
| Go | 待定 | pending |
本 RFC 要带进来的新测试:
NPS/1.0\n + HelloFrame;服务端接受、
回 CapsFrame。"GET / HTT"、全零、
NPS/2.0\n);服务端 500 ms 内关连接,无帧响应。NPS/2.0\n;服务端可在关之前写
NPS-PREAMBLE-UNSUPPORTED-VERSION\n 诊断(测试断言连接被关,诊断
字符串是可选的)。现有测试改动:
跨 SDK 互通:
不需要新基准。每条连接线上多 8 字节开销,在任何交换 >1 帧的连接上都 被摊薄到近零。延迟影响为零,因为客户端可以把前导 + HelloFrame 放一 次 write。
如果将来实测发现 10 秒 PREAMBLE-WAIT 超时挡住了超过 0.1% 的合法连
接,重开本 RFC 的 OQ-2。
暂无。进入 Accepted 之前会挂一个实验分支。目标测量:
| 指标 | 基线 | 建议 | 差值 | 方法 |
|---|---|---|---|---|
| 每连接线上开销 | 0 字节 | 8 字节 | +8 B | 平凡 |
| 握手 RTT(localhost loopback) | TBD | TBD | 目标:无回归 | .NET BenchmarkDotNet 在原生模式 loopback |
| 服务端拒掉非 NPS 扫描的成本 | 完整帧解析尝试 | 8 字节 memcmp |
目标:至少便宜 10× | 扫描模拟工具 |
HelloFrame 之前就能选择连接级特性(如”客户端支持 E2E 加密”)?需
shepherd 定调。默认立场:不加——HelloFrame 已经有完整能力声明,
前导保持最小。 目标:Accepted 之前解决。PREAMBLE-WAIT 超时 10 秒——是否应可配?Owner:
Ori Lynn。目标:默认留 10 秒,SDK 可以暴露开关;Accepted
之前在本 RFC 加一条说明。nps/1 ALPN。
与带内前导互补(§5.4)。0 的保留
字节)。| 日期 | 作者 | 变更 |
|---|---|---|
| 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-INVALID、frame-registry.yaml 中保留 0x4E。Phase 1 .NET 参考 helper(NPS.Core.Ncp.NcpPreamble)同时落地;Phase 2(其余 5 个 SDK)和 Phase 3(默认开启)按 RFC §8.1 推迟。 |