本页从纯架构视角描述 NSC(Network Site Client) 的功能划分与设计特点。改造路线见 roadmap.md。
NSC 是 NSIO 生态在用户侧的运行时二进制,担任"访问端"的角色。它把远端 NSN 站点上的服务"拉到本地",让用户在本机上看到一组环回虚拟 IP 与一串 *.n.ns 域名,可用任意标准 TCP 工具(SSH、curl、psql、浏览器等)透明访问远端服务。
NSC 与 NSN 在协议栈上对称,但流量方向相反:NSN 是入站的服务暴露端,NSC 是出站的服务消费端。
NSC 的产品差异化:
.ns 命名空间 + 环回 VIP:服务被映射到本机 127.127.x.x 段的虚拟 IP,操作系统层无需 NAT 或路由配置。*.n.ns 域名的解析就近发生在本机,既不依赖系统 DNS 也不污染上游 DNS。NSC 通过同一套控制面订阅多类配置事件,但仅消费与"用户侧出站"相关的子集:
git.company.com 也指到内部站点)。127.127.0.0/16 段(完整 /8 环回内的留白段),避免与 systemd-resolved、传统 dnsmasq、mDNS 抢端口或 IP;在 TUN 模式下使用 RFC 6598 CGNAT 共享段。NSC 自带一个手写的 UDP DNS 服务器,部署在专用环回地址(默认 127.53.53.53:53),职责边界清晰:
*.n.ns 与 NSD 显式下发的自定义域名返回对应 VIP 的 A 记录。监听点选在 127.53.53.53:53 是出于回避主流 stub resolver 占用、保留标准端口 53、不需要为绑定特权端口加 capability 三方面考量。
NSC 一旦接管本地解析(写 resolv.conf 首行、配 resolvectl、加 /etc/resolver/n.ns),就夺走了原本属于系统的默认 DNS——内核 stub、getaddrinfo、应用层全部走 NSC,而 NSC 自己的"未命中透传"上游决定了网络其余部分能否解析。当前实现把上游硬编码为 1.1.1.1:53,这条简化在以下场景下直接打破用户原有体验:
*.corp / *.local / mDNS / Active Directory 等内部记录,改走 1.1.1.1 后这些查询全部失败;1.1.1.1 被运营商 / 国家级阻断的网络下,NSC 接管后等于全网解析瘫痪;9.9.9.9 / 自建 PiHole 等)被静默替换成 1.1.1.1,违反"非破坏性集成"的产品承诺。正确做法:NSC 启动时先快照系统当前的 DNS 上游(/etc/resolv.conf 的 nameserver 行 / resolvectl status 的 per-link servers / macOS scutil --dns 输出 / Windows 网络适配器 DNS 列表),把快照作为默认上游;然后再接管本地解析。这条顺序约束很硬:
127.53.53.53 → 127.53.53.53 的自循环。resolv.conf 的 search / domain 行也是非 FQDN 解析(ssh server → ssh server.corp.example.com)的关键,NSC 自己也应当透传这些后缀给应用,否则用户脚本里的短主机名都会 NXDOMAIN。NotifyAddrChange),刷新快照后 atomic swap 内部上游表。--upstream-dns → 退到 1.1.1.1(或为空)。最末端不阻塞 NSC 自身启动,只是降级为"无未命中透传",*.n.ns 仍然可用。dns_config 覆盖——管理员可以在 NSD 端显式声明默认上游(企业上游、按域分流见 §2.4.3),覆盖系统快照。优先级:dns_config 显式 > 系统快照 > CLI fallback > 硬编码兜底。这条接续原则与 §2.4.3 的按域转发组合,使 NSC 接管 DNS 既"非破坏"(系统原有解析路径继续生效)、又"可增强"(在保留原行为的基础上新增 *.n.ns 与按域分流)。
本地 DNS 服务器对外是单一的"命中即返 VIP"语义,但命中条目实际来自两条互不耦合的输入:
| 来源 | 命名空间 | 典型场景 | 上游有无同名记录 |
|---|---|---|---|
routing_config 自动派生 | *.n.ns(协议级保留) | 自动注册的内部站点服务,如 ssh.office.n.ns | 无(.n.ns 不在公网 DNS) |
dns_config 显式下发 | 任意 FQDN(含真实公网域名) | 双栈服务:git.company.com、grafana.internal.example.com | 可能有,由管理员决定是否遮蔽 |
两类条目共用同一份 DnsRecords,在查询路径上无差异;差异只在"命中后用户感知"——*.n.ns 命中是"凭空多一个域名",真实域名命中是"在本机层面遮蔽掉公网 DNS 的解析结果"。
许多服务在企业部署中同时具备两条访问路径:
git.company.com 在公网 DNS 中解析到对外 VIP / Ingress / 反代,任何外部用户可访问。NSC 通过 dns_config 把同一域名映射到本地 VIP,在客户端层面"遮蔽"公网解析:用户与脚本无需知道双栈细节,继续使用熟悉的公网域名,流量却沿 127.127.x.x → VIP listener → WSS → NSGW → NSN 完整走隧道。这条路径与浏览器场景下 HTTP 代理 lookup_domain 命中走 WSS 的入口共享同一份 NscRouter 决策。
设计上需明确的边界:
dns_config 仅修改 NSC 持有的命中表,不改写公网 DNS、不修改系统 hosts 文件。卸载或停用 NSC,公网域名立即恢复直连解析,不留残留状态。dns_config 编排;NSC 不在本地维护静态遮蔽表,以保证"用户终端永远跟随策略中心"。仅靠"命中即返 VIP / 未命中转发单一公网上游"两档策略,不足以覆盖企业级解析需求。许多场景里,某些域名族必须由特定 DNS 服务器才能解析正确:
*.corp.company.com 由企业 AD DNS(10.0.0.53)持有,公网 DNS 既不会回也不应当回;*.k8s.internal 由集群内的 CoreDNS 解析,带集群拓扑感知;*.dev.example.local 由开发环境专属 DNS 持有,与生产命名空间隔离;NSC 的本地 DNS 服务器据此引入按域转发表,每条规则形如 (域名后缀, 上游列表, 上游可达性)。整体解析优先级:
按域转发的关键特例是"指定上游本身只在隧道侧可达":企业 AD DNS(10.0.0.53)、集群 CoreDNS 等并不暴露在公网,客户端直连等于不可达。
直观的设计冲动是在 DNS 转发器内嵌"是否走隧道"的判断,但这本质上是在 DNS 层重做了一份"路由策略"——而 NSC 的产品哲学是**"任何内网服务都通过分配 VIP 来访问"。内网 DNS 服务并不特殊,把它当成普通 service 注册即可,DNS 转发器只需要看到一个上游 IP / FQDN,不需要知道它通向哪里**。
部署方式:
services.toml 里 service = "dns", port = 53 暴露给某 site,自动通过 services_report → routing_config 派生 dns.<site>.n.ns 域名与一个 127.127.x.x:53 VIP。dns_config 里转发条目直接写:for *.corp.company.com forward to dns.office.n.ns:53/tcp(或写裸 VIP 127.127.x.x:53/tcp,但 FQDN 形式对 VIP 重分配更稳健)。NscRouter::open_stream → WSS → NSGW → NSN → 内网真实 DNS。由此 DNS 模块不需要任何新的"隧道感知"代码;转发表条目就是一个普通的 (suffix, Vec<上游地址>) 二元组,与 dnsmasq / unbound 的条件转发表语义完全一致。是否进隧道、走哪个 site,这些决策已经被压到了"VIP 是哪个 site 的"这个早就建好的索引里。
与上一节 §2.4.3 的衔接:
把"该不该进隧道"完全外推给 VIP 后,DNS 模块在三种数据面下的行为就是同一份代码;真正的差异落在"上游地址应当填什么"上,这是部署期 / dns_config 编排的职责,不是运行时分支。
| 上游地址类型 | TUN(规划) | userspace(默认) | HTTP proxy 入口 |
|---|---|---|---|
公网 IP(1.1.1.1) | OS 默认路由送出 | 同 | 同 |
内网 LAN IP(10.0.0.53) | OS 路由表选 LAN 直连或 TUN 隧道(由 metric 决定);用户在 LAN 内时走 LAN 直达,离开 LAN 时进隧道——这就是上一轮提到的"本地有路由,可以选择" | OS 没有指向 10.0.0.53 的路由,直发会失败;该模式下不应当用此类上游 | 同 userspace |
Site 服务 VIP / FQDN(dns.office.n.ns:53) | TUN 段是 100.64/16;OS 路由把这一段送进 TUN,等价于其他服务 | VIP listener 在 127.127.x.x:53 抓 TCP,经 WSS 进隧道——与所有 *.n.ns 服务同路 | 同 userspace |
编排建议:在多模式部署中,转发表条目可以并列两条上游(顺序回退),例如:
dns.office.n.ns:53/tcp(VIP 版,任何模式都工作)10.0.0.53:53/udp(原生 IP 版,在 TUN 模式 + LAN 内时由 OS 选 LAN 直达,延迟最低)NSC 按列出顺序尝试;userspace 模式下第二条会失败、自动回退到第一条;TUN 模式下两条都可工作,OS 路由表 / metric 决定细节。这种"用配置而不是代码表达模式差异"的取舍,与 NSC 整体"VIP 段 + lazy binding + 单一路由表"的精神保持一致——把分支推到数据,而不是推到逻辑。
至此 NSC 在 DNS 层提供两类能力,共同构成"客户端策略 DNS"模型,不再需要"上游可达性"这个第三类:
| 能力 | 决定的事 | 数据来源 | 当前状态 |
|---|---|---|---|
| 命中 → VIP(§2.4.1 + §2.4.2) | 哪些域名在本机直接回 VIP(虚拟域名 / 公网遮蔽) | routing_config + dns_config | ✅ 已实现 |
| 按域转发(§2.4.3 + §2.4.4) | 未命中时该走哪个上游;隧道侧上游通过指向 service VIP 的 FQDN 表达 | dns_config(规划项) | ⚠️ 仅有硬编码默认上游 |
DNS 层只负责名字解析,不关心上游可达性:可达性问题在它到达 DNS 模块之前已经被"VIP 分配 + 路由表"解决。这是 NSC 设计的一致性体现——任何要"走隧道"的需求,最后都收敛为"分配一个 VIP,通过现有路径访问"。
NSC 在 DNS 层提供的三类能力构成一个完整的"客户端策略 DNS"模型,彼此不耦合但配合使用:
| 能力 | 决定的事 | 数据来源 | 当前状态 |
|---|---|---|---|
| 命中 → VIP(§2.4.1 + §2.4.2) | 哪些域名在本机直接回 VIP,使流量进隧道 | routing_config + dns_config | ✅ 已实现 |
| 按域转发(§2.4.3) | 未命中时该走哪个上游 | dns_config(规划项) | ⚠️ 仅有硬编码默认上游 |
| 上游可达性(§2.4.4) | 该上游通过 OS 路由还是 NSC 显式打隧道 | 转发表条目 + 当前数据面模式 | ❌ 当前仅 OS 直连;模式相关 |
设计上严格保持"DNS 层只负责名字解析,真正的流量路径由路由表与 ACL 决定"——按域转发表不重新发明一份"路由策略",它仅负责把 NSC 自己发出的 DNS 查询分流到正确的解析者。
把 §2.4.1–§2.4.5 涉及的所有路径并到一张表,展开 NSC 在不同数据面模式下对四类目标的处理方式:
| 目标 | TUN 模式(规划) | userspace(默认) | HTTP proxy 入口 |
|---|---|---|---|
*.n.ns 虚拟域名 | DNS 命中返 VIP;OS 路由把 VIP 段经 TUN 送出 | DNS 命中返 VIP;TCP 在 127.127.x.x:port 被 VIP listener 抓到 | 不依赖 DNS,直接 lookup_domain 命中 → open_stream |
公网域名遮蔽(git.company.com 等被 dns_config 重定向) | 同上 | 同上 | 同上 |
内网 DNS(10.0.0.53 等仅隧道侧可达) | NSC 把查询发往 10.0.0.53:53 UDP/TCP,OS 路由表根据 nsio0 route 决定送进 TUN;若用户已在 LAN 内,优先走直连 LAN | NSC 转发器显式调 open_stream(site, "10.0.0.53", 53) 走 53/TCP;OS 不知道 10.0.0.53 存在于隧道侧 | 同 userspace |
公网通用解析(默认上游 1.1.1.1) | UDP 直发,OS 默认路由经物理网卡 | UDP 直发,OS 默认路由经物理网卡 | 同 userspace |
关键观察:只有 TUN 模式真正给了 OS 路由表"本地 vs 隧道"的选择权;userspace 与 HTTP proxy 模式下,NSC 必须自己在每条规则上声明"该上游能不能走 OS",对应 §2.4.4 中的 OsRoute / Tunneled 二选一。这让"同一份 dns_config 转发表在 TUN 部署和 userspace 部署上有不同的执行行为"成为合法的设计意图,而不是 bug。
NSC 内部维护一份小型路由表,在多个出站入口之间共享:
CONNECT(SSH / HTTPS / 任意 TCP 隧道入口)与明文 HTTP 方法。CLI 提供三种"数据面"选项,但当前实现的功能差异比命名暗示的小:
127.127.0.0/16 + WSS 中继到主网关,功能完整,无需 root。数据面之间共享 NSC 上层的所有路由 / DNS / HTTP 代理逻辑,差异只在"用户进程发出的包以何种方式进入 NSC 的处理路径"。
127.127/16 的 VIP 段是为 userspace 模式量身设计的:loopback 段使 NSC 在普通用户权限下能 bind() 监听,且不与系统 stub resolver 抢端口。这套约束在 TUN 模式下完全不存在:NSC 已经获得 root 并管控一张虚拟网卡,任何 IP 段都能由内核路由表导引到本进程,VIP 间接层从此变成"可选设计",而不是必需结构。
由此 TUN 模式有两种合法的寻址模型,可以共存:
| 寻址模型 | 服务 → IP 映射 | 路由安装 | NSC 端代价 | NSN 端代价 |
|---|---|---|---|---|
VIP 模式(100.64/16,延续 userspace 的抽象) | 每个 service 由 VipAllocator 分配一个 CGNAT IP | 100.64.0.0/16 dev nsio0 一条 | VIP 分配器 + DNS 改写 + per-service 监听语义 | NSN 用 nat::ServiceRouter 做端口 → 本地服务的查表(现状) |
| Passthrough 模式(直接路由到站点 CIDR) | 服务保留真实内部 IP / 域名,NSC 不分配 VIP | <site_cidr> dev nsio0 一条(可多条) | 几乎为零——内核路由 + ACL 即可,DNS 不改写 | NSN 直接转发已解密的 IP 包到本机网络;不需要端口映射 |
两种模型的对比:
| 维度 | VIP 模式 | Passthrough 模式 |
|---|---|---|
| 与本机网络的冲突 | 100.64/16 在 RFC 6598 CGNAT 段,极少冲突 | 取决于站点 CIDR——若站点是 10.0.0.0/24,用户家里也是同段就麻烦;需要"站点 CIDR 全局唯一"或显式 NAT 重映射 |
| service 粒度 | 天然 per-service —— 每条 (VIP, port) 一个 listener,ACL 可按 service 精细授权 | 路由是 per-CIDR —— 整段网络对客户端可见;service 粒度由 ACL 在端口层补 |
| DNS 体验 | 必须由 NSC 本地 DNS 改写 *.n.ns / 公网遮蔽到 VIP | 真实域名解析直接命中真实 IP,NSC 不做改写;DNS 只在"内网 DNS 也只在隧道侧可达"的场景需要按 §2.4 处理 |
| 多站点同 IP 冲突 | 不冲突——VIP 由 NSC 分配,两个站点的 10.0.0.5 各自得到不同 VIP | 冲突——两站点 CIDR 重叠时路由表无法表达;需 NSD 在编排层判负 |
| 与传统 VPN / WG 客户端的兼容 | 自成一套 —— *.n.ns + VIP 是 NSIO 特有 | 与 WireGuard / Tailscale subnet router / OpenVPN 同模型,迁移用户零学习成本 |
| NSC 实现复杂度 | VIP 分配 + DNS server + per-service listener | TUN + 路由 + 既有 router 表(直接按 dst IP 查 site) |
*.n.ns 命名空间在 passthrough 模式下的归宿:不是被废弃,而是变成"纯虚拟服务的命名空间"——GatewayEgress 这类没有真实 IP 后端的服务必须有一个名字,VIP 是它的物理化形式;NsnBacked 在 passthrough 模式下可以不取 *.n.ns 名,直接用站点的真实域名 (db.office.example.com) 即可。
推荐策略(规划):
services.toml 或 NSD 编排里声明 cidr_passthrough = "10.20.30.0/24") + 虚拟服务 VIP(GatewayEgress 等没有 IP 后端的形态);services.toml 增 addressing = "vip" | "passthrough" 字段(默认 "vip" 保现状),决定该 service 是分配 VIP 还是直通真实 IP;详细演进项见 roadmap.md F.1 TUN 数据面真实化(已扩展为"真实化 + 寻址模型抉择")。
启动完成后,NSC 在终端输出一份"site → VIP → 域名"映射表,作为最低门槛的状态视图。当前没有独立的运行时查询接口(规划中将提供 UNIX socket / HTTP 端点暴露 sites、VIPs、DNS 记录、隧道状态等)。
NSC 在本机以"加几行 resolv.conf"或"配 systemd-resolved 按域转发"为代价,即可让 *.n.ns 在系统层直接解析。配合环回 VIP,用户使用任意标准 TCP 工具,语义如同访问本机服务,无需切网络环境、无需走系统 VPN 接口。
默认形态下 NSC 不创建 TUN 设备、不修改路由表、不修改防火墙、不需要管理员权限。这使 NSC 能跑在普通桌面用户、CI runner、容器、Android App 沙箱等多种受限环境。
NSC 复用 NSN 的控制面、机器身份、WireGuard / WSS 协议栈、proxy / ACL 子模块,但流量方向相反、视角相反:
| 维度 | NSN(站点端) | NSC(客户端) |
|---|---|---|
| 流量方向 | 入站 | 出站 |
| 隧道入口 | NSGW → NSN | NSC → NSGW |
| NAT 类比 | DNAT(端口 → 服务) | SNAT + PAT(VIP → site+port) |
| ACL 视角 | 按入站源过滤 | 按出站目标过滤(预留) |
| 服务来源 | 本地白名单 | 控制中心自动发现 |
这种对称性使大部分组件可在两个二进制之间复用,降低代码维护成本。
VIP 监听器、HTTP 代理、本地 DNS 三个入口共享同一份路由表与 site 索引。任何一处的状态更新对其他入口立即可见,避免了配置重复维护与状态分裂。
NSC 收到的 routing_config 只是"site → service → VIP"的映射,对 backend 是 NSN 还是其他形态完全不感知。当 NSIO 引入三类 service backend(规划,见总体架构 §6.5与 nsgw architecture §2.8.1)后,NSC 的适配压力按 backend 类型分两档:
| Backend | NSC 是否需要改造 | 原因 |
|---|---|---|
NsnBacked(现有) | 不需要 | 现状即此 |
GatewayEgress(指定目标 L4 转发) | 不需要 | NSC 看到的仍是"VIP + service",open_stream 走主网关,网关侧自己 connect 到目标——NSC 无感知 |
GatewayExit(exit node 模式) | 需要 TUN 数据面 | userspace 模式只能捕获 127.127/16 VIP 段,无法承接 0.0.0.0/0 默认路由;HTTP 代理只能覆盖 HTTP/CONNECT;exit node 启用后 NSC 需在系统路由表写 0.0.0.0/0 dev nsio0 或目标 CIDR,前置依赖见 roadmap.md F.1 |
前两类是"VIP 抽象 + lazy binding"的副产品——NSC 从一开始就把"流量到底通向何处"封装在主网关的 WSS 流里,客户端永远只与"VIP 与主网关"两个抽象打交道。GatewayExit 是唯一突破这个模型的形态,它的目标不再是"某个 VIP",而是"任意公网 IP",必须由内核路由表把流量送进 TUN 设备才能成立。
这进一步强化了 roadmap.md F.1 的优先级:TUN 数据面真实化不仅是"补齐 CLI 表面承诺",也是 NSC 进入 exit-node 形态的前置条件;若选 (A) 删除 TUN 路线,则 NSC 永远不参与 GatewayExit 场景,该形态只能由"非 NSC 的 WireGuard 客户端"使用 NSGW 的 exit 能力。
NSC 的核心价值并不局限于"凭空创造 *.n.ns 这套虚拟域名空间",更重要的是允许任意域名(尤其是用户已在使用的真实公网域名)通过 dns_config 在本机被遮蔽到 VIP,使流量走隧道。这是 NSC 与传统纯 VPN 客户端的关键区别——传统方案要么"全流量走 VPN",要么"按目标 IP / CIDR 路由",难以做到"按 FQDN 在客户端层面拆分公网与隧道流量";NSC 把这一拆分推到 DNS 层,实现细粒度、低侵入、可由控制中心动态变更的双栈访问。详见 §2.4.2。
新建 TCP 连接时才解析"走哪个网关",而不是在监听器启动时绑死。这使得网关健康状态变化、控制面切主、跨区 failover 等事件对用户连接透明,新连接立即享有新拓扑,旧连接保持原路径。
NSC 在控制面建立时声明全部 8 类事件接收器,即便当前只消费其中 3 类,也保证未来引入"出站 ACL 预检"、"WireGuard TUN 模式"、"凭证轮转"等能力时不需要重新设计接入点。
启动时 NSC 立即输出 site→VIP→域名映射表,任何配置错位、连接失败、网关未就绪都通过日志立即可见;未来的运行时状态接口将以 UNIX socket / HTTP 形式暴露更细的连接、隧道、DNS、VIP 维度。
NSC 不做的事情:
.n.ns 命名空间是协议级约定,证书由网关或外部入口层负责。NSC 的演进核心围绕三条主线:
按协议层把 NSC 内部的组件做一次扁平汇总,详细职责见上文 §2。