外观
Control Server —— 自动对等 API(用户面)
源码:apps/control-server/app/api/v1/autopeering/。前缀 /control/v1/autopeering,下文表格的「路径」列省略该前缀。
这是面向其他 DN42 运营者的自助对等接口,与管理面平级但信任域完全分开:鉴权凭据是门户会话令牌(按会话内 ASN 划权)或 auth-server 签发的会话 JWT,两者都打不进 Admin API。用户旅程的实现原理见 自动对等,运维侧的供给策略见 管理面 API。
生产前端在 https://peering.natlan.io,身份提供方是 https://auth.natlan.io。
会话模型
浏览器只持有门户会话令牌,上游 OIDC 的 access / refresh token 留在服务端行里。整条闭环:
前端 → GET /autopeering/auth/login-url → 跳 auth.natlan.io 授权页
→ 用户完成 ASN 归属验证并批准
→ auth-server 回调 /autopeering/auth/callback(服务端换令牌 + 验 ID Token)
→ 302 回 {frontend_base_url}/auth/complete?handoff=<一次性码>
→ 前端 POST /autopeering/auth/exchange 用交接码换门户会话令牌RP 凭据([portal] 配置段)未配齐时,登录端点 fail-closed 503,控制面其余功能不受影响。
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
GET | /auth/login | 无 | 建登录事务并 302 跳授权页。?next= 记住登录后要回的站内相对路径;绝对 URL 与协议相对 URL 一律丢弃(防开放重定向) |
GET | /auth/login-url | 无 | 同上但返回 JSON { authorize_url },供 SPA 自己控制跳转时机 |
GET | /auth/callback | 无 | OIDC 回调:换令牌 → 验 ID Token(含 nonce 绑定)→ 建门户会话 → 302 回前端;失败也回前端并带 error= |
POST | /auth/exchange | 一次性交接码 | 换门户会话令牌 + 身份摘要;兑换即轮换令牌并作废交接码 |
GET | /me | 门户会话令牌 | 当前登录身份(ASN / 维护者 / 起源前缀,取自缓存 claims)+ expires_at |
POST | /auth/refresh-claims | 门户会话令牌 | 现查上游 userinfo 刷新 dn42 claim;上游 access token 过期时先用 refresh token 静默换新 |
POST | /auth/logout | 门户会话令牌 | 吊销门户会话(auth-server 侧登录态不受影响) |
GET | /session | 会话 JWT | 直接用 auth-server 挑战流签发的会话 JWT 回显身份,面向不走浏览器的 API 调用方。返回 { asn, mntner, method, sub, expires_at } |
/me 与 /session 是两条不同凭据的回显端点,不可互换。/session 走 JWKS 本地无状态验签,验证热路径不打网络、不依赖 auth-server 在线;未配置 [auth_service] 时 fail-closed 503。
可对等节点
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
GET | /nodes | 门户会话令牌 | 开放自动对等的节点清单(站点、拨入主机、容量、展示元数据) |
容量由外部端口池派生:
| 字段 | 语义 |
|---|---|
capacity | 池总席位 |
used_ports | 已占席位,含手工建立的对等 |
free_ports | 池内空闲席位 |
remaining | 实际可接纳数 |
provisioned | 其中经自动对等落成的数量 |
max_sessions | 可选人工上限;null 表示只受端口池约束 |
容量条画 used_ports / capacity。列表页元数据 region / country_code / description / bandwidth_mbps / monthly_quota_gb / notice 由管理面同名字段配置。endpoint_host 为空的节点不接受拨入,不出现在本清单里。
对等请求
全部挂 require_portal_session,按会话内 ASN 划权。会话里没有已验证的 ASN 时(claims 缺失或异常)一律 403——门户会话必须携带经归属验证的 ASN 才能提交或查看对等。
| 方法 | 路径 | 说明 |
|---|---|---|
POST | /requests | 提交即落成(免审批),201 |
GET | /requests | 我的请求列表(按会话 ASN) |
GET | /requests/{id} | 单条详情;他人请求一律 404(防枚举) |
DELETE | /requests/{id} | 自助拆除;非 provisioned 409 |
GET | /requests/{id}/traffic?range= | 会话流量:当前速率、时 / 日 / 月用量(含月配额展示位)、5min 序列(per-interface 存档,保留 60 天) |
GET | /requests/{id}/metrics?range= | 会话路由计数时序(5min gauge:per-AF imported;agent ≥1.0.233 起含 exported) |
GET | /requests/{id}/routes?family=&limit=&offset= | 对端宣告进本网、被该节点收到的路由明细(node_route_entries 按会话协议过滤) |
POST | /requests/{id}/probe-mtu | DF ping 二分扫描对端隧道地址 |
POST | /requests/{id}/probe-latency | 按需 RTT / 丢包采样(ping 10 包) |
提交请求
RequestCreateIn:
| 字段 | 必填 | 语义 |
|---|---|---|
node_id | 是 | 目标接入节点 |
public_key | 是 | 对端 WireGuard 公钥 |
endpoint | 否 | 对端 WireGuard host:port;NAT 后可留空由本网侧被拨 |
link_local | 否 | 对端 link-local;缺省按 ASN 尾四位派生 |
mtu | 否 | 1280..1420,缺省 1420(WG over IPv4 满额值) |
mp_bgp | 否 | 默认 true(单条 MP-BGP 会话) |
session_addressing | 否 | link-local(默认)或 ipv6-ula,仅 MP 形态有意义 |
peer_ipv4 / peer_ipv6 | 视形态 | 对端隧道内地址 |
extended_next_hop | 否 | MP 形态恒为 true;显式给 false 回一条解释性 422 |
notes | 否 | ≤512 字符 |
会话形态三选一:
| 形态 | 参数组合 | 产出 |
|---|---|---|
| link-local MP-BGP(默认) | mp_bgp=true, session_addressing=link-local | 单会话,不接受 peer_ipv4 / peer_ipv6 |
| ULA 单会话 | mp_bgp=true, session_addressing=ipv6-ula, peer_ipv6 | 单会话,peer_ipv6 须为 fd00::/8 |
| v4 / v6 双纯会话 | mp_bgp=false, peer_ipv4 且 peer_ipv6 | 两条会话,命名 autopeer_<asn>_v4 / _v6 |
对端隧道内地址须落在该 ASN 在 registry 登记的 route / route6 起源前缀内(现查副本):不符 422 并指明该注册哪个前缀,副本不可用 503。
落成事务(同一事务内完成,任一步失败整体回滚、不留半成品请求行):行锁节点 → 从外部端口池分配最小空闲端口 → 组 spec(接口名 as<ASN>,继承节点固定钥,policy dnpeers)→ 建 Peering 聚合并 materialize → 提交后广播门铃。
错误:校验失败或节点落成前置不齐 422;节点未开放 404;每节点每 AS 最多一条——目标节点已有该 AS 的外部对等(含手工建立)、或容量 / 端口池满 409。同一个 AS 可以在多个不同节点各建一条。
201 响应含 provisioned 请求行、provision 连接参数与 generation。
请求列表与详情
读取列表时惰性收养:手工建立的外部对等按登录 ASN 归并为请求行(adopted: true,前端可标注「由运维建立」;多 WG 接口的特调聚合跳过)。这样门户是单一入口,用户看得到也管得了运维手工建的那条。
展示态(公钥 / endpoint / 端口 / MTU / 接口名 / 会话形态)从 Peering 聚合 spec 读侧派生——管理面调参或改名后门户自动跟进;聚合已删的行(cancelled)回落审计快照。
详情额外带两个块:
provision:本网侧连接参数——endpoint(拨入主机 + 分配端口)、node_public_key(从节点私钥现场推导)、node_link_local、node_asn、mtu。live:本会话 BGP 活性,从节点最新 runtime snapshot 派生——bgp_state、bgp_health(up/connecting/down/unknown)、since、observed_at、established_seconds、wireguard(握手时刻与累计字节)、routes(per-AF 导入导出计数,旧 agent 只有导入侧)。双会话形态另有sessions数组。节点无快照时为null。
拆除
DELETE /requests/{id} 删 Peering 聚合(接口与 BGP 会话随 cascade)并 materialize,提交后广播。配额位与端口随之释放,请求行留作 cancelled 审计痕迹,之后可重新提交。
落成后探测
| 端点 | 产出 | 限制 |
|---|---|---|
POST /requests/{id}/probe-mtu | {status: ok|mismatch|unreachable, pmtu, mtu},落详情的 mtu_probe。落成后自动跑一次 | 每会话 10min 冷却 429;agent 离线 409;超时 504 |
POST /requests/{id}/probe-latency | {rtt_ms_avg, rtt_ms_max, loss_pct},落详情的 latency | 同上 |
只读网络态势
源码:apps/control-server/app/api/v1/autopeering/observability.py。与控制台复用同一批 builder 函数,但不复用端点——/ui 与 /admin 的 require_admin 挂在路由级,信任域不跨越;这里另起一个挂 require_portal_session 的路由,直接调那些纯函数。
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /fleet/dashboard | 门户版仪表盘(概述 + 流量 + 路由),字段经白名单投影 |
GET | /fleet/traffic | 全 fleet 吞吐时间线 |
GET | /nodes/{node_id}/traffic | 单节点吞吐时间线 |
GET | /fleet/traffic-breakdown | 按节点 / 按对端的当前吞吐排行 |
GET | /fleet/prefix-flaps | 只读前缀抖动榜,与管理面同 builder、同参数,含根因定位 |
GET | /fleet/flap-stats | fleet 级抖动速率仪表与时序 |
GET | /my/flaps | 「我这家 AS 最近抖不抖」——按会话 ASN 划权的单对端 flap 详情 |
披露口径:AS 号、前缀、流量占比、抖动明细不脱敏——这些本就在 DN42 registry 与各家 explorer 上公开。收窄两处,且一律白名单投影(builder 将来新增字段默认不出,不会静默泄漏):
traffic-breakdown的 WG 对端只保留node_id/interface/ 收发速率,不出public_key与endpoint(密钥材料与攻击面,不是路由信息)。overview节点行只保留node_id/health/site/region/geo/capabilities,不出 agent 版本、心跳、drift、generation、release 遥测(内部运维信息,版本号还会给出漏洞窗口线索)。
其他差异:门户侧流量时段最长 24h(7d / 30d 档不下放);管理面的处置写路径与告警事件流不在门户暴露;/my/flaps 在无 flap 痕迹时返回 200 空态并带 has_flaps: false,而不是像管理面那样 404——对门户用户来说「没抖」是正向结果,不该长得像出错。