外观
自动对等(autopeering)
自动对等是面向其他 DN42 运营者的自助对等系统的用户面,挂在控制面的 /control/v1/autopeering/* 下。
它在控制面进程内,而不是独立服务。 门户的核心动作是编辑目标状态——创建 Peering 聚合、分配 WireGuard 端口与 link-local、触发 generation——这些全部依赖控制面内部的服务层。独立部署意味着要么外放一套特权写 API,要么在门户里重复实现分配逻辑,两条路都比进程内调用差。放在进程内还能直接复用审计、缓存与限速设施。
前端在独立仓库,控制面只出 API。运维视角见 自动对等运维。
它最重要的性质是自己不做认证——登录整体委托给 认证服务,控制面在这条流程里是标准的 OIDC 依赖方(RP)。身份策略的演进(验证方式、人机验证、令牌格式)都发生在 auth-server,门户端点不感知。
分工
- auth-server 是唯一的身份权威:ASN 归属验证、会话 JWT、OIDC 令牌签发。
- 控制面是机密 OAuth 客户端兼资源服务:持
client_secret、换令牌、维护门户会话;对等请求获批后直接落成 Peering / DesiredState。
独立门户时代为「用户令牌透传控制面」准备的 RFC 8707 resource indicator 在并入后不再需要——RP 与资源服务器是同一个进程,门户会话即控制面会话。auth-server 侧的 resource indicator 实现保留(通用 OAuth 能力),只是当前没有消费方。
信任域
门户会话与管理员账号体系完全分开:portal_sessions 表独立于 admin_sessions,鉴权依赖 require_portal_session 独立于 require_admin。门户会话只解锁 /control/v1/autopeering/* 下按会话内 ASN 划权的端点,绝不通向 Admin API。
为什么门户要有自己的会话
上游 access token 只活 1 小时,而对等申请是个慢流程(填表 → 等审批 → 回来看进度)。直接把上游令牌当门户凭据用,等于让用户每小时重登一次。
标准 RP 做法:门户签发自己的会话令牌(TTL 120 天——低频慢流程,频繁重登伤体验;令牌只解锁门户自身,长时效风险可接受。只存 sha256),上游 access/refresh token 留在服务端行里,需要时静默 refresh。浏览器只见门户自己的令牌。⚠️ 上游令牌是全站「只存哈希」纪律的例外——它们要被再次使用(userinfo 刷新),只能明文存,portal_sessions 因此是控制库里最敏感的表之一。
前后端分离下的交接
OIDC 回调必须落在后端(client_secret 不能进浏览器)。但后端换完令牌后,怎么把会话交给前端?
采用一次性交接码:后端建好会话 → 302 回 {frontend_base_url}/auth/complete 并附 handoff → 前端 POST /control/v1/autopeering/auth/exchange 换真正的会话令牌,兑换即作废并轮换会话令牌。
刻意不把会话令牌直接放进回跳 URL——URL 会进浏览器历史、Referer 头和各级访问日志。交接码即便泄露,兑换后也已失效。
端点与实现位置
| 模块 | 位置 |
|---|---|
路由包(与 admin/ / ui/ 同构;auth.py 登录闭环、session.py 登录态读取、common.py 共享工具) | apps/control-server/app/api/v1/autopeering/ |
| OIDC RP 客户端(discovery / PKCE / 换令牌 / ID Token 验签) | apps/control-server/app/services/portal_oidc.py |
| 登录事务 + 门户会话仓库 | apps/control-server/app/services/portal_sessions.py |
ORM 模型(portal_login_transactions / portal_sessions) | apps/control-server/app/db/models/portal.py |
配置在 control-server.toml 的 [portal] 段(OIDC 凭据、redirect_uri、前端基址、会话 TTL),见 配置参考。凭据未配齐时登录端点 fail-closed 503,控制面其余功能不受影响。
除 OIDC 登录闭环外,GET /control/v1/autopeering/session 还接受 auth-server 挑战流直接签发的会话 JWT(JWKS 无状态验签,require_peering_session)——面向不走浏览器的 API 调用方。
PeeringRequest 状态机
自动对等是 Peering 聚合根前的薄壳:提交通过配额与容量闸后当场落成标准 Peering + WgInterface + BgpSession,materializer / flap 检测 / 观测全部复用,不引入第二套数据面。
免审批:身份已由门户会话背书(ASN 归属验证),配额与容量全部机器可判,不设人工闸门。请求要么落成(provisioned,decided_by='auto'),要么整体回滚不留行——不存在 pending 停留态(pending / rejected 是审批闸门时期的遗留状态值,当前代码不产出)。cancelled 是拆除后的终态:用户 DELETE 自助拆除(删聚合、释放配额位与端口)、或运维经 Peering 管理 API 删聚合时同事务联动推进(reject_reason='removed by operator'),两种路径都保留请求行作审计痕迹。隧道握手 / BGP established 等活性由读侧派生。运维的控制点是供给策略(开放哪些节点、给多少额度)。
审计快照与读侧派生:请求行的连接参数列(公钥 / endpoint / link-local / 端口 / MTU / 接口名)是提交或收养时刻的审计快照,落成后不回写;门户读路径经 services.autopeering.peering_views 从 Peering 聚合的接口 / BGP spec 现查展示态——管理面调参、改名后门户与观测(活性 / 流量 / 探测)自动跟进,聚合已删的行回落快照。
手工对等的惰性收养:运维手工建立的外部对等在对应 ASN 拉取请求列表时归并为请求行(decided_by='adopted',快照列按当时聚合内容填充,可空),门户单一入口可见、可自助拆除,且解释了「每节点每 AS 一条」的 409。收养在列表读取时进行——登录身份已背书 ASN 归属,且此刻才有会话主体可填;幂等,多 WG 接口的运维特调聚合跳过。
表单纪律:只收对端公钥、endpoint(可选)与 link-local(可选,缺省按 ASN 尾四位派生)。绝不收对端私钥,也不写本端私钥——节点密钥由 materialize 无条件注入,见 fleet 供给收权。
配额(services/autopeering.py):按节点划界——同一 AS 可在多个节点各建一条对等,但每节点每 AS 最多一条(目标节点上已有外部对等——含手工建立的——或在途请求即拒)。跨节点数量不设上限,天然被节点数与各节点容量闸住。(asn, node) 的 partial unique index 兜住并发双提交;节点容量 = autopeering_node_policies.max_sessions 与端口池双重闸。已落成计数只算聚合仍存在的请求行(内连接),异常路径留下的死指针行不占配额。
落成事务(api/v1/autopeering/requests.py)与手工 provision 同一事务纪律:行锁节点 → 在 runtime.wireguard_port_range 内分配最小空闲端口(已占集合含禁用接口——禁用是暂停不是释放)→ 组 spec → 建三行 → materialize_change 同事务 → 提交后 broadcast_change。任一步失败整体回滚,不留请求行。
spec 组装约定(与手工外部 peer 逐条对齐):接口名 as<ASN>;private_key_ref 继承自节点既有 WG 接口(固定钥);本端地址不写 spec——materializer 注入 Node.link_local/64;BGP 走 link-local MP-BGP + extended next hop,policy dnpeers;MTU 默认 1420(WG over v4 满额值,特殊路径落成后经 Peering 管理 API 调低)。发给对端的本网公钥从节点私钥现场推导,不读任何缓存列。
当前进度
已完成:登录闭环全链并入控制面;对等请求提交即落成(节点供给策略 + 端口池分配器 + Peering 聚合,迁移 e0f1a2b3c4d5 / f1a2b3c4d5e6,端到端测试覆盖配额 / 分配 / 回滚 / 越权 / 守卫)。
未完成:落成后的主动验证(握手确认 → DF ping 扫 PMTU 自动调 MTU);门户正式前端。状态页 BGP 活性与用户自助拆除已实现。
测试的一个刻意选择
门户测试里的上游不是桩,是进程内跑的真 auth-server(httpx ASGI 传输直连,apps/control-server/app/tests/test_autopeering_login.py)。于是 discovery、JWKS、PKCE、授权码、ID Token 验签全程走真实实现——跨服务契约由测试真正锚住。换成桩就只能测到「我以为上游长什么样」,而契约漂移恰恰发生在这个缝里。