首页
隐私政策
iYoRoy DN42 Network
关于
更多
友情链接
Language
简体中文
English
Search
1
Docker下中心化部署EasyTier
4,389 阅读
2
给Android 4.9内核添加KernelSU支持
3,521 阅读
3
记一次为Android 4.9内核的ROM启用erofs支持
1,511 阅读
4
DN42探究日记 - Ep.1 加入DN42网络
1,188 阅读
5
在TrueNAS上使用Docker安装1Panel
983 阅读
Android
运维
NAS
开发
网络技术
专题向研究
DN42
个人ISP
CTF
Kubernetes
网络安全
奇思妙想
物联网
登录
Search
标签搜索
BGP
网络技术
BIRD
Linux
DN42
iBGP
Android
C&C++
OSPF
网络安全
Clearnet
CTF
MSVC
AOSP
Web
Docker
IGP
Windows
Kernel
TrueNAS
神楽悠笙
累计撰写
36
篇文章
累计收到
23
条评论
首页
栏目
Android
运维
NAS
开发
网络技术
专题向研究
DN42
个人ISP
CTF
Kubernetes
网络安全
奇思妙想
物联网
页面
隐私政策
iYoRoy DN42 Network
关于
友情链接
Language
简体中文
English
搜索到
36
篇与
的结果
DN42&OneManISP - 联邦, 自动化, IPAM 与 Overlay Network
本文不详细展开代码实现, 仅记录内网架构设计过程和自动化实现思路. 若需要参考代码/配置文件, 可参阅 iYoRoy-Network/bird2-config: BIRD2 Configuration for iYoRoy Network (AS4242422024, AS205369). 仓库包含较多的 AI 生成代码. 本文仅借用 Underlay/Overlay 的逻辑分层概念(基础层/业务层),特指地址角色的分离,不涉及 VXLAN/GRE 等隧道封装技术。 TL;DR 对整个内网架构进行了一次重构, 原本按“DN42 / IANA / WireGuard / BIRD 配置文件”水平切开的网络, 改造成一套按网络意图组织的基础设施. 最终设计可以概括为: 用 WireGuard 承载节点间 underlay transport; 用 BGP Confederation 替代 OSPF / full-mesh iBGP, 作为内部路由骨干; 用 Underlay / Overlay 分离节点身份和服务地址; 用 Tier / Region / Node ID / Token 自动派生 AS、loopback 和 link-local; 用 BGP Large Community 表达路由来源、传播范围和导出策略; 用 Ansible 把高层 peer intent 编译成 WireGuard + BIRD + interface 配置并自动下发. 背景 在这两个系列之前的一些文章中, 我们已经成功运营起了 DN42 AS 和 IANA AS, 并且同一套基础设施同时承载着两张网的流量. 在先前的 DN42&OneManISP - 共存环境下的OSPF源地址故障排除 - 悠笙の开发日记 中, 对于这两张网已经有了一个初步的拆分和隔离, 但是随着节点的增多, 手动维护这一坨东西的 WireGuard 和 BIRD 配置文件已经变得越来越复杂, 而且公网节点之间也需要互联与互相 IP Transit, 因此最终狠下心来打算对这一整套内网进行一次彻底的重构. 分析 旧架构的问题 重构之前, 各个节点之间的 DN42 路由是相互导通的, 使用 OSPF over WireGuard 作为 IGP, 在此基础上使用 Full-Mesh iBGP 来在各个边界路由器之间传递完整路由信息; 对于IANA 流量, 各个节点之间完全隔离, 每个节点单独广播自己的 /48 IANA IPv6 Prefix. 总结出来就是: WireGuard 负责节点间隧道; OSPF over WireGuard 负责 IGP; DN42 边界路由器之间再跑 full-mesh iBGP; IANA 节点基本各自独立广播自己的公网前缀. 这个架构在节点少的时候很好用, 但节点多起来之后问题开始明显: WireGuard、OSPF、BIRD 配置需要分别维护; 新增节点需要同时改多个地方, 容易漏; DN42 和 IANA 的路由策略互相影响, 但配置上又是分散的; IANA prefix 的跨 PoP 调度不够自然; 有些节点只是转发节点, 却仍然被迫拥有 overlay 身份; 源地址选择、路由泄漏、内部地址暴露都变得越来越难控制. 设计目标与 BGP Confederation 这次重构主要希望达成的几个目标: 节点加入要简单: 最好只需要描述节点身份和 peer 关系; DN42 / IANA / 内网骨干要能共享基础设施, 但策略上互相隔离; IP 地址应该是可以调度的资源, 而不是节点的固定身份; 内部拓扑不应该泄漏到外部 BGP peer; 配置应该由声明式数据生成, 而不是手写大量重复 WireGuard/BIRD session; 部署流程自动化 同时还有其他的一些比较细碎的需求: 部分 IANA 节点希望能承载其他节点的 IP Transit, 用 IANA PoP 来为其他节点广播公网 IP 当前 IANA AS 是たのしい和我共同在维护, 我们之间需要达成统一的协调, 部分基础设施需要互相 Transit 以及调控路由 我们只有一个 44Net IPv4 /24 (Sponsored by たのしい), 要播的话所有节点都需要 iBGP 互联和 IGP, 否则只能一个 PoP 使用 IANA AS 需要承接下游 IANA PoP 之间需要路由调优 先前的 IANA IPv6 Prefix 分配规则遵循的是 大洲 3 bits + 地区 3 bits + 同地区多节点 2 bits, 由一个完整的 /40 拆出来若干 /48, 但是因为某些上游限制播出前缀数量, 再加上这样碎片化的前缀不利于做 IPAM, 因此我们打算最终合并对外广播只详细到大洲层面, 其次走内部 iBGP 路由来处理更加详细的部分. たのしい那边的内网结构是 BGP Confederation, 用的是手动模拟的方案. 其实这样分析下来内网的架构就可以很轻松的定下来了, 最符合需求的就是把内网也切换成 BGP Confederation, 然后利用 BGP as IGP 的理念, 在联邦内广播 /32 和 /128 来处理内部路由. 同时, 因为联邦避免了传统 iBGP 水平分割的特性, 所以联邦不需要严格 Full-Mesh, 也不需要单独起 iBGP, 非常的去中心化啊xD 同时, BGP Confederation 可以很轻松的和たのしい那边的基础设施合并, 只需要我们两边都将对方的内部 AS 视作联邦 AS 处理即可. 这个方案还有一个好处, 因为实际上我们之间的基础设施包含关系是这样的: graph subgraph 4242423377 Infrastructure 3377_DN42_PoP[DN42 PoP] 3377_IANA_PoP[IANA PoP] end subgraph 4242422024 Infrastructure 2024_DN42_PoP[DN42 PoP] 2024_IANA_PoP[IANA PoP] end 3377_IANA_PoP <==跨基础设施联邦==> 2024_IANA_PoP 3377_DN42_PoP <--> DN42 <--> 2024_DN42_PoP 3377_IANA_PoP <--> IANA <--> 2024_IANA_PoP 我们之间的 DN42 PoP 和 Transit 是隔离的, 但是 IANA PoP 和 Transit 又是互通的, 在 BGP Confederation 场景下, 可以利用过滤器 + BGP (Large) Community 来对路由来源进行隔离的同时互通 IANA 之间的 Traffic Engineering 意图, 实现同一套标准两份基础设施通用. Overlay 与 IPAM 在旧设计里, 一个节点通常同时承担两种身份: 它是网络中的一个路由器; 它也是 DN42 / IANA 中可以被访问的服务地址持有者. 重构之后, 我决定把这两种身份拆开: Underlay: 节点作为路由器的身份, 用于内部互联、下一跳、隧道和转发; Overlay: 节点对外提供服务或承接流量时使用的 DN42 / IANA 地址. 这样, 一个节点可以只参与转发而没有 DN42 IPv4; 也可以在需要时临时宣告某个 overlay /32 或 /128. 实际应用场景下, 我的 DN42 段地址不算很充裕, 当初注册的是 /28, 只有 16 个可用地址. 有些节点比如 IEPL 之类的隧道节点或者 IX 节点, 它们只作转发作用, 不需要承载服务, 不需要被外部连接, 因此理论上并不需要为其分配独立可访问的 DN42/IANA 地址. 再加上, 前面我们已经打算将内网切换成 BGP Confederation, 基于此套配置, 我们可以将原本 DN42 + IANA 这样水平分割的网络结构重新规划, 改成 Underlay + Overlay 的垂直划分, DN42 和 IANA 的地址/流量作为上层 Overlay Network 的负载, 下层 Underlay Network 用作节点间底层通讯和转发的基础设施. 在这种情形下, IP 地址变成了一种可以被在内网里轻松调度的资源而非和节点绑定的唯一 ID. 这样的优势很明显: 节省稀缺地址资源: 转发节点可以只拥有 underlay 身份, 不必分配 DN42 IPv4 或公网地址; Anycast 与地址迁移: Overlay 地址变成可以通过 BGP 调度的资源, 不再强绑定某台机器; 隐藏转发节点: 纯转发节点不需要暴露全局可达地址, 外部更难直接探测内部拓扑; 新节点入网简单: 新节点先加入 underlay, 需要承载业务时再分配 overlay 地址; 优雅 Transit 交付: IANA PoP 可以通过内部 confederation 把客户前缀或服务地址投递到其他节点. 设计 综上所述, 最终设计了这样一套 IPAM 机制和自动化流程. 每个节点拥有一些基本的元数据: Tier: 指代当前节点用途, 如用作骨干网, 接入 IX, 或是作为网内节点 Region: 节点所在大洲区域 ID: 同区域下该节点ID Token: 给该节点分配的一个随机 16bit 字符串, 在 IPv6 下给节点作为唯一 ID 使用 对于 Tier 和 Region, 编写了几张表格作为数据来源: Tier Desc 1 Backbone 2 IX 3 Backbone + IX Mixed 9 Node Region Desc 0 Reserved 1 Asia 2 Europe 3 Africa 4 North America 5 South America 6 Oceania 7 Antarctica 联邦 AS: 4220240000 - 4220249999 分配规则: 422024{tier}{region}{node_id:02d} 其中 tier 指代节点用途, region 指代节点所在区域, 最后两位 node_id 用于区分同区域内的不同节点. Underlay IPAM IPv4 Prefix: 100.64.0.0/16 分配规则: 100.64.{tier*10 + region}.{node_id}/32 IPv6 Prefix: fd18:3e15:61d0:ffff::/64 分配规则: fd18:3e15:61d0:ffff:{tier}:{region}::{node_token}/128 为了简化自动化流程, 我打算将 WireGuard 隧道之间的 Link-Local 地址也接入自动化管理, 利用上述 Tier, Region, Token 生成, 由自动化脚本生成 WireGuard 配置并自动配置隧道和联邦 BGP Session. Link-Local 分配规则也大体上沿用普通 IPv6 的规则: fe80::2024:{tier}:{region}:{node_token}/64 这样, 每个节点只需要为其指派元数据便可以按照上述规则自动生成内网联邦 AS 和地址. Overlay IPAM DN42 IPv4 部分就手动指派了, 这部分存在一些历史遗留架构, 修改需要合并到官方 Registry, 因此就打算手动指派. Underlay 的 IPv6 本身是 DN42 可达的, 如果还要分配其他 DN42 IPv6 直接按需广播即可. 对于 IANA IPv6, 目前的设计格式是先按照最初提出的规则计算大洲 /43, 比如亚洲为: 2a14:7583:f220::/43 再按照地区计算出 /46, 比如香港地区 HKG: 2a14:7583:f224::/46 接着将从 /46 到 /112 全部置零, 最后的 16bits 填入节点 Token 作为节点的 IANA IPv6 地址. 比如我的某台香港地区骨干网节点: 2a14:7583:f224::7d89/128 Community 隔离机制 在这套架构里, BGP Large Community 实际上就是一条路由在不同模块和基础设施之间传递的元数据信息和控制信息. 基于这些元数据, 我们在向不同外部对等体导出路由的时候就能判断: 它属于 DN42 还是 IANA? 它是本地生成、下游带来、peer 学到, 还是 upstream 学到? 它能不能导出到别的 AS? 它是不是 underlay-only? 这条路由需不需要 prepend as? 举个具体的例子, 比如用第一位标识路由来源/目标是 DN42 还是 IANA, 允许发往 DN42 的路由全部会被打上 (4242422024, 1, 1) 同理, 发往 IANA 的路由全部会被打上 (205369, 1, 1) 在各个 Protocol 中, 这一项作为总开关, 控制是否应当导出. IANA 的上游导出过滤器: template bgp iana_upstream_v6 { ... ipv6 { ... import filter { if !iana_filter_default_check() then reject; # 基础检查 remove_confederation_as(); # 移除 confederation as, 防止外部 peer 恶意携带内部 as remove_private_community(); # 移除私有 community, 防止外部 peer 恶意操纵内部路由 iana_upstream_add_community(); # 为所有来自 IANA 的路由打上允许在 IANA 基础设施广播的 community accept; }; export filter { if !iana_filter_default_check() then reject; # 基础检查 if !iana_upstream_check_community() then reject; # 检查 community, 是否是发往 IANA 的路由/是否携带了 no-advertise/no-export 等 remove_confederation_as(); # 移除 confederation as remove_private_community(); # 移除私有 community accept; }; ... }; ... } 其中 iana_filter_default_check() 用于检查前缀长度, ROA, 是否是默认路由等比较琐碎的内容: function iana_filter_default_check(){ if net ~ [::/0] then return false; if net.len > 48 then return false; if bgp_large_community ~ [(IANA_OWNAS,1,1)] && is_self_iana_v6() then return true; if roa_check(iana_roa_v6, net, bgp_path.last) = ROA_INVALID then return false; return true; } remove_confederation_as(), 顾名思义, 移除联邦内部 AS: function remove_confederation_as() { bgp_path.delete([4220240000..4220249999]); # 4242422024 Infrastructure bgp_path.delete([4233770000..4233779999]); # 4242423377 Infrastructure } remove_private_community(), 顾名思义, 移除内部 Community. 这部分目前实施的很粗糙, 后续需要详细细化, 因为部分 Community 应当对下游开放, 使其可以通过 Community 来传递路由意图, 做一些优化, 比如 prepend AS path, 可用于路由调优或者流量工程: function remove_private_community(){ bgp_large_community.delete([(4242422024, *, *)]); bgp_large_community.delete([(205369, *, *)]); } iana_upstream_add_community(), 为来自上游的路由全都打上允许在 IANA 基础设施流通的 Community 以及路由来源标识: function iana_upstream_add_community(){ bgp_large_community.add((205369,1,1)); bgp_large_community.add((205369,2,102)); } 其中, (205369,2,102) 标识该路由来自于上游. 路由来源分为三种, 上游, 对等体, 下游/客户/自己(看作客户). 对这三种不同类型的 BGP Session, 导出的路由通常不相同: 对上游, 我们需要让上游帮我们广播我们自己的和我们下游的段, 因此需要向上游导出所有来自下游的前缀 对下游, 我们需要为其提供网络服务, 需要向其导出我们知道的所有路由, 即来自上游, 对等体, 下游的路由 对对等体, 对等体之间的连接仅仅是为了双方互相访问彼此的网络, 因此不能互相导出互相的上游或者对等体, 不然就变成免费 Transit/Tunnel 了, 因此只导出来自下游的路由 在当前内网中与之对应的, 来自对等体的会被打上(205369,2,101), 来自下游的会被打上(205369,2,100). iana_upstream_check_community() 会检查路由的 Community, 基于上述原则判断其是否应该被广播给上游: function iana_upstream_check_community(){ if !(bgp_large_community ~ [(205369,1,1)]) then return false; # 没被允许在 IANA 基础设施广播, 则拒绝 if bgp_large_community ~ [(205369,65535,65282)] then return false; # no-advertise if bgp_large_community ~ [(205369,65535,65281)] then return false; # no-export if bgp_community ~ [(65535,65281)] then reject; # no-advertise if bgp_community ~ [(65535,65282)] then reject; # no-export if bgp_large_community ~ [(205369,2,0)] then return false; # 来自内网, 该 community 被用于标识路由来自内网, 不应该往外广播, 此处拒绝广播 if bgp_large_community ~ [(205369,2,101)] then return false; # 来自对等体, 拒绝广播 if bgp_large_community ~ [(205369,2,102)] then return false; # 来自上游, 拒绝广播 return true; } 同理, 对于 DN42 也是类似的检查机制. 基于这套机制, 可以很轻松的隔离 DN42 和 IANA 两张网. 上述只是一个大致的解释, 更细的 community 设计和各类出口策略其实还可以继续展开, 但那就会变成另一篇路由策略详解了. 这部分其实大量参考来源于たのしい的BGP Communities, 毕竟两者基础设施需要互通, 很多 Community 规范基本上都是照搬过来的. 在此感谢たのしい大佬提供的思路~ 路由生命周期 第一阶段: Ingress / Import 所有外部路由进入系统时, 先经过对应 domain 的 import filter. IANA 有三类来源: upstream; peer / IX; downstream. DN42 也有两类主要来源: 普通 transit / eBGP peer; IX / route server. 除此之外, 本机也会产生一些 route: underlay loopback; DN42 overlay address; IANA own / anycast address. 这些路由在进入联邦时会被打上 Large Community. 后面的所有 filter 都需要据此调控. 第二阶段: Core / Intra Confederation 进入 BIRD RIB 之后, 会通过内部的 Intra BGP Confederation 在节点之间传播. 此处 Intra Confederation 不止单纯为了处理连通性, 更是为了让内部路由也能携带策略信息. 先前的 OSPF 很适合解决: 这个 loopback 在哪里? 这个 next-hop 怎么到? 但是它无法传递 AS 信息和策略意图, 比如: 这条路由是 IANA peer 学来的, 不能再导给另一个 peer; 这条路由是 DN42 underlay-only, 绝不能泄漏给 eBGP; 这条路由是 downstream customer prefix, 可以导给 upstream; 这些都是 BGP policy 的强项. 所以内部骨干从 OSPF / full-mesh iBGP 转向 confederation-style BGP, 本质上是把内部控制平面升级成了“可携带策略的控制平面”. 第三阶段: Egress / Export 一条路由要离开基础设施时, 会再次经过 export filter. 此处根据 community 可以系统性的防止路由泄露并进行宏观调控如 AS prepend. 比如: DN42 underlay route 带有 underlay-only community, 所以不会被导出到 DN42 eBGP; IANA upstream 学来的路由不会再导给另一个 upstream; IANA peer 学来的路由不会再导给另一个 peer; DN42 IX 学来的路由不会再导回 IX; NO_EXPORT / NO_ADVERTISE 会被尊重; 对外导出前会删除内部 confederation AS 和 private community. 其实总结下来就是这样一张图: flowchart LR classDef ext fill:#eef7ff,stroke:#5b8def,stroke-width:1px; classDef local fill:#f5f5f5,stroke:#888,stroke-width:1px; classDef filter fill:#fff3d6,stroke:#d19a00,stroke-width:1px; classDef core fill:#eaf8ea,stroke:#3c9b43,stroke-width:1px; classDef export fill:#fdecec,stroke:#d45a5a,stroke-width:1px; subgraph SRC["路由来源"] direction TB IU["IANA上游<br/>传输/全量路由"] IP["IANA对等 / IX"] ID["IANA下游<br/>客户前缀"] DT["DN42传输 / eBGP对等"] DX["DN42 IX / 路由服务器"] LU["本地底层环回<br/>100.64.x.y / fd18:...:ffff"] LO["本地覆盖地址<br/>DN42自有 / IANA自有 / 任播"] end subgraph INFRA["Bird2配置基础设施"] direction LR subgraph IMPORT["入口 / 导入过滤器"] direction TB IUF["IANA上游导入<br/>标记: 205369:1:1<br/>标记: 205369:2:102"] IPF["IANA对等导入<br/>标记: 205369:1:1<br/>标记: 205369:2:101"] IDF["IANA下游导入<br/>AS-SET / 路径检查<br/>标记: 205369:2:100"] DTF["DN42 eBGP导入<br/>前缀 / ROA / 自身检查<br/>标记: 4242422024:2:101/102"] DXF["DN42 IX导入<br/>前缀 / ROA检查<br/>标记: 4242422024:2:101"] STF["静态起源<br/>底层/覆盖路由标记"] end META["Large Community 元数据层<br/>域 + 来源 + 作用域<br/>内部路由API"] subgraph CORE["内部控制平面"] direction TB RIB["BIRD路由信息库"] CONFED["BGP联邦内部<br/>成员AS: 422024xxxx<br/>策略: full / default / iana_full / no_iana"] WG["WireGuard底层传输<br/>链路本地下一跳<br/>fwmark策略路由"] UNDERLAY["net_underlay<br/>环回 + 表1142"] OVERLAY["net_overlay<br/>DN42 / IANA服务地址"] end subgraph EGRESS["出口 / 导出过滤器"] direction TB DEXP["DN42 eBGP导出<br/>拒绝 Underlay 路由<br/>遵守no-export/no-advertise"] DXEXP["DN42 IX导出<br/>拒绝从对等/传输学习的路由"] IEXP["IANA上游/对等导出<br/>仅导出本地/下游<br/>拒绝从对等/上游学习的"] DOWNEXP["IANA下游导出<br/>策略: default / own_only / reject"] KEXP["内核导出<br/>按 Community 设置krt_prefsrc"] CLEAN["外部导出前清理<br/>移除联邦 AS<br/>移除私有 Community"] end end subgraph DST["路由去向"] direction TB OD["DN42对等 / 传输"] OX["DN42 IX"] OI["IANA上游 / 对等"] OC["IANA下游"] KF["Linux内核FIB<br/>实际报文转发"] end IU --> IUF IP --> IPF ID --> IDF DT --> DTF DX --> DXF LU --> STF LO --> STF IUF --> META IPF --> META IDF --> META DTF --> META DXF --> META STF --> META META --> RIB RIB <--> CONFED CONFED --- WG WG --- UNDERLAY RIB --- OVERLAY RIB --> DEXP RIB --> DXEXP RIB --> IEXP RIB --> DOWNEXP RIB --> KEXP DEXP --> CLEAN --> OD DXEXP --> CLEAN --> OX IEXP --> CLEAN --> OI DOWNEXP --> CLEAN --> OC KEXP --> KF class IU,IP,ID,DT,DX ext; class LU,LO local; class IUF,IPF,IDF,DTF,DXF,STF filter; class META,RIB,CONFED,WG,UNDERLAY,OVERLAY core; class DEXP,DXEXP,IEXP,DOWNEXP,KEXP,CLEAN export; class OD,OX,OI,OC,KF ext; 自动化: 把网络意图编译成配置 前面讲了这么多 IPAM, Confederation AS, Underlay / Overlay 和 BGP Community, 这些设计如果最后还是靠手写配置维护, 那其实并没有真正解决问题, 只是把复杂度从一种形式搬到了另一种形式. 在这次重构之前, 新增一个节点或者 peer 往往需要同时修改很多地方: WireGuard 配置: interface 名称, listen port, peer public key, endpoint, allowed IPs BIRD 配置: BGP protocol 名称, neighbor 地址, neighbor interface, ASN, import/export filter IPAM 相关配置: loopback, link-local, router id, overlay 地址 部署相关配置: 哪些节点需要渲染, 哪些服务需要重启 一些特殊情况: NAT 后面的节点, passive peer, 特殊 local AS, 特殊 filter 这些东西在逻辑上其实描述的是同一件事: 两个节点之间存在一条属于某个路由域的连接. 但是在手工配置里, 它们会散落在 WireGuard, BIRD, network interface, systemd/openrc 等多个地方. 只要其中一个字段漏改或者不一致, 就可能出现很诡异的问题, 比如 WireGuard 已经起来了, 但是 BIRD neighbor 指向了错误的 interface; 或者 link-local 改了, 但是对端 session 里还写着旧地址. 所以这次重构里, 我不希望 Ansible 只是一个"把 YAML 填进 Jinja2 模板"的工具. 如果只是把原来的手写配置搬进模板, 本质上还是在维护一堆低层配置, 只是换了个文件格式而已. 既然都上自动化了, 那输入不应该是"我要生成一个长什么样的配置文件", 而应该是: 这个节点是谁 它属于哪个 tier / region 它和谁互联 这条连接属于 DN42, IANA 还是 Intra 这条连接应该使用什么 import/export policy 它是否有 NAT, passive, endpoint override 之类的特殊约束 至于具体的 WireGuard interface 怎么写, BIRD session 怎么写, link-local 地址是什么, 这些都应该尽量由自动化系统根据规则生成. Source of Truth: 节点状态而不是配置文件 这套自动化的输入主要放在 Ansible 的 host_vars 里, 每个节点的变量不再只是模板渲染需要的参数, 而是这个节点在网络里的身份描述. 例如一个节点至少会有: tier: 节点层级, 比如 backbone, IX, 普通 node region: 节点所在区域 node_id: 同区域内的节点编号 node_token: 一个稳定的 16-bit token, 用来生成 IPv6 地址尾部 这些字段会被用于 IPAM 的地址管理和 Confederation 内节点的识别信息 (就像前面提到的, AS 计算和 IP 地址派生). 这样做的好处是, 新节点加入时, 我们只需要给这个节点配置好基本的元数据, 它在 underlay 里的所有基础身份就都是确定的. 这可以大量减少后续维护工作. 如果 link-local 地址是手写在每个 BIRD session 里的, 那么一旦节点 token 或地址规则发生变化, 所有对端都要一起改; 但如果 BIRD neighbor 地址是根据 peer node 的 metadata 自动派生出来的, 那么它永远和 source of truth 保持一致. Domain Intent: peer 声明式配置 这次自动化里最重要的抽象是 Domain Intent, 也就是将 Session 和与之对应的 WireGuard 隧道绑定视作一个整体, 再按照所属网络类型 (DN42/IANA/Intra) 进行区域划分. 以 Intra peer 为例, 一条内部连接本质上同时需要两类配置: WireGuard interface, 用来提供传输层隧道 BIRD BGP session, 用来在这条隧道上交换路由 如果手动维护, 这两部分很容易重复描述同样的信息: interface name peer node listen port endpoint neighbor interface BGP protocol name import/export policy 所以我们将常规 Intra peer 抽象成一条 peer intent. 简化之后大概像这样: - node: tyo03-jp interface: intra_tyo03 wireguard: listen_port: 10234 passive: true bgp: protocol: intra_ibgp_tyo03 ipv4: import_policy: full export_policy: full ipv6: import_policy: full export_policy: full 这条声明并不是直接对应某一个配置文件, 而是对 Peer 意图进行描述: 当前节点和 tyo03-jp 之间有一条名为 intra_tyo03 的内部连接, 这条连接使用 WireGuard 承载, 并在其上建立一条 Intra BGP session, BGP policy 使用 full. 之后 Ansible 基于此自动渲染对应 WireGuard 和 BIRD 配置. 这样, 一个 peer 关系只需要描述一次, 后面的低层配置都由自动化生成. 例如 BIRD neighbor 的 link-local 地址不需要手写, 可以通过对端节点的 tier / region / node_token 自动得到; WireGuard endpoint 也可以根据对端 inventory 里的 ansible_host 和对端回程 peer 的 listen_port 自动推导. 这样做最大的优点是可以避免多个配置层之间出现状态不一致. 自动派生 endpoint 与 passive peer 在我的内网架构中, 并不是所有节点都有完全对称的公网可达性, 比如有的节点 / DN42 Peer 在 NAT 后面, 只能主动连出去, 有些隧道需要固定某一端作为监听, 由另一端主动发起连接, 因此 peer intent 里需要支持被动监听这样的特殊情况. 如果一个 peer 不是 passive, 自动化可以尝试从对端 inventory 中读取 ansible_host, 再结合对端声明中的 listen_port, 自动生成 WireGuard endpoint: endpoint = peer_ansible_host + ":" + peer_return_listen_port 如果 peer 标记为 passive, 则不渲染 endpoint, 让对端主动发起连接. 本质上 endpoint 是由"对端如何被访问"和"对端监听哪个端口"共同决定的, 这两个信息本来就已经存在于 inventory 和 peer intent 里, 没必要再复制一遍. 当然, 对于特别奇怪的链路, 保留最基础的手动覆盖 endpoint. 自动化的目标不是消灭所有特殊情况, 而是让常规情况不需要特殊处理. 通用场景 & 例外 其实就是为了特殊情况考虑的 Fallback: 历史遗留 tunnel DN42 上承载的特殊内部 peer NAT 后面的节点 临时 workaround 对端要求奇怪的 BGP 参数 某些 session 需要特殊 local AS 某些路由只想进入 IANA domain, 不应该进入 DN42 domain 最后保留的设计是: 大部分普通 Intra peer 可以通过 domain intent 生成 WireGuard + BIRD, DN42 peer 的 WireGuard 也可以从 DN42 peer intent 里生成, 但是一些特殊 BIRD session, 比如需要单独指定 local_as, 或者需要使用特殊导出策略的 session, 仍然可以继续用低层配置显式声明. 最终渲染前, Ansible 会把生成的配置和 legacy/manual override 合并成最终配置. 这样做的好处是, 自动化覆盖了 90% 重复且容易出错的部分, 同时给剩下 10% 特殊情况也预留了位置. 配置渲染 整个渲染过程大概可以理解成这样: flowchart TD A[host_vars: node metadata + domain intent] --> B[materialize domain intent] B --> C[effective_wireguard_interfaces] B --> D[effective_intra_ibgp_sessions] C --> E[render wg-quick configs] D --> F[render BIRD intra sessions] A --> G[render BIRD root / DN42 / IANA modules] A --> H[render underlay / overlay interfaces] E --> I[.rendered-wireguard] F --> J[.rendered] G --> J H --> K[.rendered-network] 这里有三类最终产物: .rendered/: BIRD 配置 .rendered-wireguard/: WireGuard 配置 .rendered-network/: underlay / overlay dummy interface 配置 BIRD 配置里又分成几个模块, 这部分是通过 Jinja2 Templates 编写的, 实际拆分 Overlay Network 也是通过这里: root config: 全局 define, include, kernel protocol DN42 module: DN42 filter, RPKI, eBGP peer, IX peer IANA module: upstream, downstream, peer, RPKI, static route Intra module: confederation-style BGP session, static route, internal filter 这些模块是否 include, 也尽量由数据自动决定, 比如一个节点没有 DN42 peer, 就不需要渲染和 include ebgp.conf; 没有 IX session, 就不需要 include ix.conf. 这可以减少空配置和无意义 include, 也避免某些节点因为缺少相关变量导致模板渲染失败. 验证 & 部署 最终部署流程还添加了一些检查和验证阶段: render BIRD validate BIRD render WireGuard validate WireGuard deploy dummy interfaces deploy WireGuard deploy BIRD BIRD 配置先在本地渲染和验证, 尽量提前发现语法错误; WireGuard 配置也先渲染和验证, 避免缺 key 或者生成出明显非法的 wg-quick 配置; 最后验证都通过了再部署底层 interfaces + WireGuard, 最后才上传并 reload BIRD. 总结 我不行了这文章给我写力竭了, 这玩意的架构用线性的语言叙述怎么这么难 理顺之后代码基本上都交给 AI 写的, 效果还行, 如需参考可查阅 bird2-config/ansible at dev · iYoRoy-Network/bird2-config. 这套方案的适用情形其实挺窄的, DN42 + IANA 这种 BGP Player 场景还是太小众了 xD. 而且这套方案能正常运行, 很大一部分原因是 IANA 的前缀和 DN42 以及我自己的内网选择的网段完全不冲突, 若 Transit 网段存在冲突的话, 可能还是得考虑 VRF/MPLS L3VPN 这类正儿八经 ISP 会用的隔离方案. 参考文章: Bird 配置 BGP Confederation, 及模拟 Confederation(2020-06-07 更新) - Lan Tian @ Blog 浅谈 BGP 中的 Transit(中转)与 Peering(对等互联) | 网络蝙蝠侠部落
2026年07月24日
57 阅读
0 评论
3 点赞
家庭网络更新 - 猫棒 + 802.11k/v/r 漫游
文章开头部分会解释一下配置猫棒和 BE10000 PPPoE 的流程, 如果您只需要查阅 802.11k/v/r 相关的内容, 可跳转至 「 配置 802.11 k/v/r」 章节 起因是五一的时候在哈尔滨本地自提了一台 Xiaomi BE10000, 因为想玩玩猫棒, 看看能不能将家里的千兆网突破 1 Gbps (cc: 浙江移动烽火光猫获取超密 + G-010S-A 猫棒上网 – 米露小窝). 而且因为我在用 EasyTier 这样的组网工具, 并且需要 WakeOnLan 这样的功能, 再加上家里已有的 Xiaomi AX3600 刷了 ImmortalWRT, 所以综合考虑我还希望能刷 WRT 系列系统, 然后也许能够利用 802.11 k/v/r 来实现自动切换, 达到手动 Mesh 的效果. 找来找去发现 好像只有 BE10000 满足我的需求, 既有 SFP+ 笼, 又可以刷入 QWRT. 暑假带回家了, 是时候折腾一下了() 而且在学校的时候也已经把唯一美中不足的 NFC 碰一碰自动连接修好了 (ref: 为小米BE10000路由器的QWRT适配NFC功能 - 悠笙の开发日记 ), 这台机器已经是完全体啦(点头) 专业名词解释 1. 宽带与光通信 (Fiber & PON) 缩写 全称 中文解释 / 备注 FTTH Fiber To The Home 光纤到户. PON Passive Optical Network 无源光网络. 家庭宽带接入的主流技术. GPON Gigabit-Capable PON 千兆无源光网络. 我们使用的猫棒 (GPON Stick) 即基于此标准. OLT Optical Line Terminal 光线路终端. 运营商局端设备, 负责向下分配光信号. ONU Optical Network Unit 光网络单元. 用户端设备, 如光猫或猫棒. UPC Ultra Physical Contact 超微球面研磨. 常见的光纤接头类型(通常为蓝色端面). APC Angled Physical Contact 斜面物理接触. 常见的光纤接头类型(通常为绿色端面, 带8度斜角). LOID Logical ONU ID 逻辑光网络单元标识符. 运营商用于认证光猫身份的一串字符. PLOAM Physical Layer OAM 物理层操作、管理和维护. 也是一种光猫认证用的密码体制. SN Serial Number 序列号. 硬件设备的唯一出厂编号, 常用于光猫注册. 2. 无线局域网与漫游 (Wi-Fi & Roaming) 缩写 全称 中文解释 / 备注 AP Access Point 无线接入点. 主路由或从路由发射 Wi-Fi 信号的角色. SSID Service Set Identifier 服务集标识符. 即用户看到的 Wi-Fi 名称. BSS Basic Service Set 基础服务集. 单个 AP 及其覆盖下连接设备的集合. BSSID Basic Service Set Identifier 基础服务集标识符. 通常是 AP 无线网卡的 MAC 地址. ESS Extended Service Set 扩展服务集. 多个 BSS 组成的统一网络(同 SSID), 即漫游环境. RRM Radio Resource Management 无线电资源管理 (802.11k). 用于获取周围的 AP 邻居报告. WNM Wireless Network Management 无线网络管理 (802.11v). 允许 AP 给客户端发送漫游引导建议. FT Fast Transition 快速漫游转换 (802.11r). 减少客户端切换 AP 时的握手和认证时间. DS Distribution System 分布式系统. 有线网络主干, ft_over_ds 即通过有线主干交换漫游信息. NAS ID Network Access Server ID 网络接入服务器标识符. 用于在漫游域中唯一标识某个 BSS 节点. SAE Simultaneous Authentication of Equals 对等同时认证. WPA3 的密钥交换协议, 比 WPA2 更安全. PSK Pre-Shared Key 预共享密钥. 家庭 Wi-Fi 最常用的输入密码认证方式. 3. 网络协议与系统设置 (Network & System) 缩写 全称 中文解释 / 备注 PPPoE Point-to-Point Protocol over Ethernet 以太网上的点对点协议. 即我们常用的宽带拨号协议. VLAN / PVID Virtual Local Area Network / Port VLAN ID 虚拟局域网 / 端口默认 VLAN ID. 用于隔离网络流量, 光猫拨号必需. SFP+ Enhanced Small Form-factor Pluggable 增强型小型可插拔光模块接口. 支持最高 10 Gbps 的传输速率. UCI Unified Configuration Interface 统一配置接口. OpenWrt/QWRT 的底层命令行配置系统. LuCI Lua Configuration Interface OpenWrt/QWRT 的 Web 图形化配置界面. DHCP Dynamic Host Configuration Protocol 动态主机配置协议. 用于自动给局域网内的设备分配 IP. L2 Layer 2 数据链路层. 文中的“L2 网段”指同一广播域下的局域网. 写成## 准备材料 Xiaomi BE10000 Router Xiaomi AX3600 Router G-010S-A NOKIA 猫棒 散热片 SC/APC 转 SC/UPC 光纤 SC/UPC 光纤适配器 刷机资料 (这部分...自己找吧, 搬运别人的资料不太好. 可以参考下面刷机教程中的固件.) 关于那根 SC/APC 转 SC/UPC 光纤, 它们描述的是光纤接头的规格. 正常的家庭 FTTH 一般用的是 UPC, 是蓝色的接头, 而我们买的猫棒基本上都是 APC 的接头, 是绿色的. 它们的主要差别是末端接头的形状, UPC 的末端是向外微微凸起的, APC 的末端是斜着的切面. 如果运营商 OLT 下行光功率足够强的话直接用 UPC 接入猫棒也是能用的, 但是会有大概 3dB 左右的光衰. 因此, 我还是决定稳一点. 图片来源: 光纤连接器 PC、UPC 、APC主要区别 - 知乎 猫棒是出了名的发热量大, 所以记得贴散热片: 贴完之后我在光猫后台看到的温度大概是 60 ℃ 左右. 给 BE10000 刷机 这里就不多赘述了, 直接参考 【小米万兆路由器刷OpenWrt】小米BE10000|SSH解锁固化|UBoot|iStore商店|多拨|-小米无线路由器及小米网络设备-恩山无线论坛 - Powered by Discuz! 即可. 固件有一些已知的 bug, 比如前端配置的 WiFi 信息无法被正常写入, 会导致整个网络部分无法启动, 随后失联, 最终触发自动 Fallback. 所以下面的一些修改主路由的操作是利用 UCI 指令实现的. 获取运营商 ONU 配置 这一步没有通用教程...上闲鱼或者淘宝买个超密, 记录下 LOID, SN, LOID CheckCode(Password), PLOAM Password, 还有 Internet 部分配置的 VLAN ID. 有的地区可能还需要记录下上行的 Mac 地址. LOID和LOID CheckCode: PLOAM Password: VLAN ID: 一般而言, 运营商会选择用LOID, 配合上可能的 LOID CheckCode(Password), 或者可能使用 PLOAM Password. 请根据你的情况自行判断. 我这边是安徽联通, 只记录了 SN, VLAN ID, LOID 就可以正常工作了. (因为联通的那个猫登不进去所以拿了个移动的猫来截图xD) 有的时候不一定需要超密才能获得这些信息, 某些猫 (比如黑龙江联通, 创维的光猫) 的普通用户就能拿到上述信息. 同时, 记录下 PPPoE 登录用户名和密码: 一般而言, 用户名是公开的, 有些猫的密码经过 F12 大法去除 password 属性也会直接显示, 但是有的猫会回传一些无意义占位符, 这个时候可以打电话给运营商重置密码. 配置 GPON Stick (猫棒) 将 GPON Stick 接入 SFP+ 接口, 如果网口灯一直没有亮起来的话可以尝试在 QWRT -> 网络 -> ECM硬件加速设置 里将 SFP1 和 SFP2 接口的速率设置强制改为 Force 2.5Gbps. (我不能确定哪个对应当前SFP接口, 所以两个都改了), 随后保存并应用, 也许需要重启路由器. 默认的 br-lan IP Prefix 应该是 192.168.1.0/24, 此时保持不变, 访问 192.168.1.10 即可打开光猫的控制台: 找到 GPON ONU 设置, 填入我们刚刚记录的信息: LOID 和 SN 并且启用 VLAN 配置: 勾选互操作兼容模式, 并在 PVID 中填写 VLAN ID. 保存后可能需要重启猫棒. 然后进入状态页, 如果看到 PON认证状态/PON信号状态 为 O5, 则说明猫棒已经完成注册并且正常工作了. 配置拨号上网 BE10000 上默认 WAN 口为 eth4, 而 SFP 端口为 eth5, 因此需要将 WAN 口从 eth4 切换成 eth5. 进入 网络 -> 接口 -> 设备, 找到 br-lan 并将 eth4 加入, 将 eth5 移除; 再进入 接口 页面, 将 WAN 网卡改为 eth5, 并填入刚刚的 PPPoE 账号密码: 保存之后应该能看到 PPPoE 已经成功拨号并且能够正常上网了: (我将整个 br-lan 的网段改成了192.168.3.0/24, 这一步不是必须的) 修改 WiFi 基本信息 实测发现...主路由通过 LuCI 直接修改 WiFi 信息会修改不进去(上文已经提到了), 因此在 SSH 通过 UCI 修改信息: uci set wireless.ath0.ssid='[CENSORED]' uci set wireless.ath0.encryption='psk2+ccmp' uci set wireless.ath0.key='[CENSORED]' uci set wireless.ath1.ssid='[CENSORED]' uci set wireless.ath1.encryption='psk2+ccmp' uci set wireless.ath1.key='[CENSORED]' uci commit wireless wifi reload 此处 SSID 为 WiFi 名称, encryption 的值 psk2+ccmp 表示 WPA2-PSK/AES,也就是常见的 WPA2-AES 配置。 虽然 WPA3/SAE 更安全,但在多 AP + 802.11r 漫游场景下,WPA2-PSK + FT-PSK 的兼容性通常更好。部分手机或 IoT 设备在 WPA2/WPA3 Mixed、FT-SAE、PMF 等组合下可能会出现连接失败、漫游失败或切换变慢的问题。 如果需要追求最高兼容性,建议用于全屋漫游的主 SSID 先使用 WPA2-PSK/AES,并确保主路由和从路由的加密方式保持一致。 对于从路由, 因为要配置漫游, 需要保证两个 WiFi 接入的是同一个 L2 网段, 因此需要关闭从路由的 DHCP, 并且将从路由配置为主路由下的一个设备. 因此对于从路由, 做的配置如下: 将 网络 -> 接口 -> 接口 下的其他所有设备删除, 仅保留 br-lan: 在 网络 -> 接口 -> 设备 下的 br-lan 设备中添加 wan 网口: 给 网络 -> 接口 -> 设备 下的 br-lan 添加 IP 地址: IPv4 地址一定记得填掩码, IPv4 网关填写主路由的 IP. 在 网络 -> 防火墙 -> 常规设置 下, 放行对应的防火墙规则. (btw. 我因为懒得配置详细的规则再加上内网设备应该也不会有太大风险因此全部放行了, 如果有特殊需求的话请不要参考xD) 因为此时主路由和从路由处在同一个 L2 广播域内,局域网中应该只保留一个 DHCP Server,也就是主路由。如果从路由 DHCP 没有关闭,客户端在连接或漫游时可能会同时收到来自主路由和从路由的 DHCP 响应,导致获取到错误的网关 / DNS,或者出现 DHCPNAK wrong server-ID 之类的问题。 这种问题的表现可能是: Wi-Fi 已经切换到另一个 AP,但网络恢复很慢; 漫游后短暂断网; 客户端重新获取 IP; 网关 / DNS 偶发异常; 看起来像是 802.11r 没有生效,但实际是 DHCP 层面在打架。 在从路由 SSH 中执行: uci set dhcp.lan.ignore='1' uci commit dhcp /etc/init.d/dnsmasq restart 如果从路由还启用了 IPv6 RA / DHCPv6,而我们希望完全由主路由负责 IPv6,也可以一并关闭: uci set dhcp.lan.ra='disabled' uci set dhcp.lan.dhcpv6='disabled' uci set dhcp.lan.ndp='disabled' uci commit dhcp /etc/init.d/odhcpd restart 关闭后可以检查: uci show dhcp.lan 看到类似: dhcp.lan.ignore='1' 即可。 需要注意,dnsmasq 进程仍然运行不代表从路由还在发 DHCP。只要生成配置中存在类似: no-dhcp-interface=br-lan 就说明它不会再在 LAN / br-lan 上提供 DHCP 服务。保存并应用后应该就可以连接主路由并通过设置的子路由 IP 连接了. (这段我在LuCI上并没有找到很明确的关闭 DHCP 服务的配置选项, 是让 GPT 通过 SSH 直接编辑 UCI 得出的) 在从路由的 网络 -> 无线, 配置各个 SSID 接口的信息和主路由同步: 此时两个 WiFi 应该都能成功连上 (虽然 SSID 相同). 配置 802.11 k/v/r 主路由 uci set wireless.ath0.ieee80211k='1' uci set wireless.ath0.rrm_neighbor_report='1' uci set wireless.ath0.rrm_beacon_report='1' uci set wireless.ath0.ieee80211v='1' uci set wireless.ath0.time_advertisement='0' uci set wireless.ath0.wnm_sleep_mode='0' uci set wireless.ath0.bss_transition='1' uci set wireless.ath0.ieee80211r='1' uci set wireless.ath0.nasid='Master_2_4G' uci set wireless.ath0.mobility_domain='cafe' uci set wireless.ath0.reassociation_deadline='1000' uci set wireless.ath0.ft_over_ds='0' uci set wireless.ath0.ft_psk_generate_local='1' uci set wireless.ath1.ieee80211k='1' uci set wireless.ath1.rrm_neighbor_report='1' uci set wireless.ath1.rrm_beacon_report='1' uci set wireless.ath1.ieee80211v='1' uci set wireless.ath1.time_advertisement='0' uci set wireless.ath1.wnm_sleep_mode='0' uci set wireless.ath1.bss_transition='1' uci set wireless.ath1.ieee80211r='1' uci set wireless.ath1.nasid='Master_5G' uci set wireless.ath1.mobility_domain='cafe' uci set wireless.ath1.reassociation_deadline='1000' uci set wireless.ath1.ft_over_ds='0' uci set wireless.ath1.ft_psk_generate_local='1' uci commit wireless wifi reload 需要注意的是, 802.11k/v/r 不是 AP 主动强制客户端漫游, 真正决定是否漫游的仍然是客户端. AP 只是通过这些协议给客户端提供邻居信息、漫游建议和快速重关联能力, 不同手机、电脑、IoT 设备对这些协议的支持程度不同. 上面的配置主要配置了三个无线接口: ath0: 主路由 2.4G ath1: 主路由 5G 三个接口配置逻辑相同, 只是 nasid 不同. 1. 802.11k: 无线资源测量 / 邻居报告 相关配置: uci set wireless.ath0.ieee80211k='1' uci set wireless.ath0.rrm_neighbor_report='1' uci set wireless.ath0.rrm_beacon_report='1' 1.1 ieee80211k='1' 开启 802.11k Radio Resource Management. 802.11k 的主要作用是让 AP 能够向客户端提供周围 AP 的信息. 客户端不需要盲目扫描所有信道, 而是可以根据 AP 提供的邻居列表, 更快找到适合漫游的目标 AP. 简单理解: 802.11k 让客户端知道“附近还有哪些同 SSID 的 AP 可以切过去”. 没有 802.11k 时, 客户端通常需要自己扫描信道. 扫描过程会消耗时间, 也可能导致短暂卡顿. 开启后, 支持 802.11k 的客户端可以更快获得候选 AP 信息. 1.2 rrm_neighbor_report='1' 开启 邻居报告. 这是 802.11k 中最常用、最关键的能力. AP 会向客户端提供邻近 BSS 的信息, 例如: 邻居 AP 的 BSSID 所在信道 PHY 类型 是否同属于当前 ESS 是否支持相关漫游能力 简单理解: 这个参数让 AP 可以告诉客户端: “你旁边还有这些 AP, 它们在这些信道上. ” 这对多 AP 漫游很重要, 因为客户端不需要从 1 信道扫到 165 信道, 而是可以优先扫描 AP 告诉它的候选信道. 1.3 rrm_beacon_report='1' 开启 Beacon Report 支持. Beacon Report 允许 AP 请求客户端报告它扫描到的 Beacon 信息. 也就是说, 客户端可以告诉 AP: 我看到了哪些 AP 它们的信号强度如何 它们在哪些信道 当前无线环境大概是什么样 简单理解: Neighbor Report 是 AP 告诉客户端附近有什么;Beacon Report 是客户端反过来告诉 AP 它看到了什么. 实际家庭网络里, rrm_neighbor_report 的作用通常更直观;rrm_beacon_report 属于辅助能力, 打开即可. 2. 802.11v: BSS Transition / 漫游引导 相关配置: uci set wireless.ath0.ieee80211v='1' uci set wireless.ath0.time_advertisement='0' uci set wireless.ath0.wnm_sleep_mode='0' uci set wireless.ath0.bss_transition='1' 2.1 ieee80211v='1' 开启 802.11v Wireless Network Management. 802.11v 的作用比较广, 其中和家庭 Wi-Fi 漫游最相关的是 BSS Transition Management. 简单理解: 802.11v 让 AP 可以向客户端提出“建议你切到另一个 AP”的请求. 注意, 这只是建议, 不是强制. 客户端可以接受, 也可以拒绝. 比如客户端还连在主路由上, 但它已经走到从路由附近了. 此时 AP 可以通过 802.11v 通知它: 你现在连这个 AP 信号已经一般了, 可以考虑切到旁边那个 AP. 启用 802.11v 可以改善“黏 AP”的情况, 但不能保证所有设备都会听. 2.2 bss_transition='1' 开启 BSS Transition Management. 这是 802.11v 中最关键的漫游相关功能. 启用后, AP 可以向客户端发送 BSS Transition Management Request, 里面通常会带上推荐的目标 AP 列表. 简单理解: ieee80211v 是打开 802.11v 总开关, bss_transition 是打开真正用于漫游建议的功能. 如果只开 ieee80211v, 但不开 bss_transition, 漫游引导效果可能不完整. 2.3 time_advertisement='0' 关闭 时间通告. 802.11v 里包含 Time Advertisement 功能, AP 可以向客户端广播时间信息. 家庭 Wi-Fi 漫游场景通常不需要 AP 给客户端提供时间同步, 所以这里设置为 0. 2.4 wnm_sleep_mode='0' 关闭 WNM Sleep Mode. WNM Sleep Mode 是 802.11v 的一部分, 主要用于客户端省电. 客户端可以进入一种特殊睡眠状态, AP 帮它保留一些上下文. 家庭路由、多 AP 漫游场景一般不依赖这个功能, 而且部分设备兼容性可能一般, 所以这里关闭. 3. 802.11r: Fast Transition / 快速漫游 相关配置: uci set wireless.ath0.ieee80211r='1' uci set wireless.ath0.mobility_domain='cafe' uci set wireless.ath0.reassociation_deadline='1000' uci set wireless.ath0.ft_over_ds='0' uci set wireless.ath0.ft_psk_generate_local='1' 3.1 ieee80211r='1' 开启 802.11r Fast BSS Transition. 802.11r 的作用是缩短客户端从一个 AP 切换到另一个 AP 时的认证和重关联时间. 普通漫游时, 客户端切换 AP 可能需要重新完成一套认证流程. 802.11r 会提前准备好部分密钥派生流程, 让客户端切换时更快. 它不会决定客户端什么时候漫游, 但当客户端决定要漫游时, 它可以让切换过程更快. 适合场景: 手机在房间之间移动 语音通话 视频会议 游戏 多 AP 同 SSID 环境 需要注意的是, 大多数新设备支持 802.11r 少数老旧或兼容性差的 IoT 设备可能不喜欢 802.11r 如果遇到某些设备无法连接, 可以先怀疑 802.11r 兼容性 3.2 mobility_domain='cafe' 设置 Mobility Domain. Mobility Domain 是 802.11r 的漫游域标识. 只有处在同一个 Mobility Domain 里的 AP, 客户端才会把它们视为同一个快速漫游域. 同一套 SSID 下, 所有要参与 802.11r 快速漫游的 AP, 都应该使用相同的 mobility_domain. 这里使用: uci set wireless.ath0.mobility_domain='cafe' cafe 是一个 16-bit 十六进制值, 刚好是 4 个十六进制字符, 类似于 DEADBEAF 这种 Magic Number. 这个值可以自定义, 例如: mobility_domain='1234' mobility_domain='abcd' mobility_domain='beef' 但需要注意, 同一个漫游网络内必须一致 不同独立网络可以不同 必须是 4 位十六进制字符 3.3 reassociation_deadline='1000' 设置 重关联截止时间. 这个参数表示客户端执行 Fast Transition 重关联时, 允许的最大时间窗口. 单位通常按 TU 理解, 1 TU ≈ 1.024 ms. 1000 大约就是 1 秒左右. 简单理解: 客户端发起快速漫游后, 需要在这个时间窗口内完成重关联. 家庭网络里设置 1000 是比较常见、宽松、稳妥的选择. 太短可能导致部分设备来不及完成切换;太长一般也没有明显收益. 3.4 ft_over_ds='0' 设置 802.11r 的 Fast Transition 方式. 802.11r 有两种常见方式: FT over the Air FT over DS 这里: 使用的是 FT over the Air: uci set wireless.ath0.ft_over_ds='0' (关闭了 ft_over_ds, 也就是启用 FT over the Air). FT over the Air 客户端直接和目标 AP 进行快速切换流程. 这是非专业环境里更常见、更直观的方式. FT over DS 客户端通过当前连接的 AP, 经由分布式系统和目标 AP 交换信息. 联网设备先通过旧 AP 联系新 AP, 再完成切换. 实际家庭 OpenWRT/QWRT 多 AP 环境中, 它并不能提供明显的便利, 反而可能会出现设备兼容性问题. 3.5 ft_psk_generate_local='1' 让本机根据 PSK 本地生成 802.11r 所需密钥. 在 WPA-PSK / SAE Mixed 这类家庭网络场景中, AP 可以根据 Wi-Fi 密码本地生成 Fast Transition 需要的密钥材料. 家庭网络通常没有企业级认证服务器, 所以这个参数直接开启就行. 适用场景: WPA2-PSK WPA2/WPA3 Mixed SAE mixed 视固件支持情况而定 无 RADIUS 的普通家庭网络 4. NAS ID: 每个 BSS 的唯一身份 uci set wireless.ath0.nasid='Master_2_4G' uci set wireless.ath1.nasid='Master_5G' 4.1 nasid='Master_2_4G' nasid 是 NAS Identifier, 也就是当前 BSS 的身份标识. 在 802.11r 场景里, 它用于区分不同 AP / 不同 BSS. 每个参与漫游的无线接口都应该有唯一的 nasid. 例如主路由: ath0 -> Master_2_4G ath1 -> Master_5G 从路由对应设置为: 2.4G -> Slave_2_4G 5G -> Slave_5G 5. 为什么三个 ath 都要配置同一套参数? 因为 ath0、ath1、ath2 分别是三个不同的无线 BSS, 即使它们广播同一个 SSID, 它们在系统里仍然是三个独立的无线接口, 所以 802.11k/v/r 参数需要分别写到每个接口上. 这部分参数可以概括为: 参数 协议 作用 ieee80211k 802.11k 开启无线资源测量 rrm_neighbor_report 802.11k 允许 AP 提供邻居 AP 列表 rrm_beacon_report 802.11k 允许客户端上报扫描到的 Beacon 信息 ieee80211v 802.11v 开启无线网络管理能力 bss_transition 802.11v 允许 AP 给客户端发送漫游建议 time_advertisement 802.11v 时间通告, 家庭漫游场景通常关闭 wnm_sleep_mode 802.11v WNM 省电模式, 家庭漫游场景通常关闭 ieee80211r 802.11r 开启快速漫游 nasid 802.11r / hostapd 标识当前 BSS, 建议唯一 mobility_domain 802.11r 设置快速漫游域, 所有 AP 保持一致 reassociation_deadline 802.11r 设置快速重关联时间窗口 ft_over_ds 802.11r 选择 FT 方式, 0 表示 FT over the Air ft_psk_generate_local 802.11r 根据 PSK 本地生成 FT 密钥 从路由 直接在 LuCI 启用对应配置即可, 可以参考我的配置: 三个无线接口都得做同样的设置, 同时保证 NAS ID 不同. 记得保证两边的加密方式完全相同, 802.11r 只负责减少漫游时重新认证的时间,但它要求参与同一个漫游域的 AP 在 SSID、密码、加密方式、Mobility Domain 等关键参数上保持一致。 如果主路由和从路由一个使用 WPA2-PSK,另一个使用 WPA2/WPA3 Mixed,或者一边启用了 SAE/FT-SAE,另一边只启用了 FT-PSK,客户端在漫游时可能无法使用同一套 Fast Transition 流程,表现为: 漫游时 Wi-Fi 图标短暂断开; 切换后重新进行 4-Way Handshake; 语音 / 视频通话卡顿; 某些 IoT 设备无法连接。 因此,如果目标是优先保证全屋漫游稳定,建议参与同一个 SSID 漫游的 AP 统一使用: WPA2-PSK / AES-CCMP / FT-PSK 也就是 OpenWrt/QWRT 中常见的: encryption='psk2+ccmp' ieee80211r='1' ft_psk_generate_local='1' ft_over_ds='0' mobility_domain='cafe' WPA3/SAE 可以单独拆一个 SSID 给新设备使用,而不要和 IoT / 全屋漫游主 SSID 混在一起。 频段分析 我家里的网络环境比较复杂, 其中主路由位于客厅正中央, 客厅与厨房和阳台直接连通, 因此主路由的信号可以直接覆盖到这部分; 从路由位于书房, 左右和两个卧室相邻. 书房和客厅由一个很短的走廊连接, 房间 A 与客厅之间间隔卫生间: 其中两个星号分别是 主路由(客厅) 和 从路由(书房). 在各个房间的频段采集结果如下: 主卧 书房 (这里的红色高峰是由从路由发出的) 次卧 可以发现, 主卧的信道还算干净, 书房因为从路由所在所以信号足够强, 而次卧由于干扰严重, 加上信号没那么强, 并且目前我们的两个 AP (两个信号最强的红色的 WiFi) 信道全都挤在一起, 导致实际测速只能达到 80Mbps 左右. 因此, 接下来需要对信道和功率做一些小调整, 并且要配置 802.11 k/v/r. 调优功率和信道 其实主要就是为了将信道区分开, 同时让客户端不要粘在某个特定接入点. 其实这部分我也不能完全说清楚为啥, 也参考了很多 AI 给的配置, 在这里也让 AI 解释一下吧 (逃 1. 频段策略 (Channel): 完全错开, 避免同频干扰 主路由和从路由在所有频段上都选择了完全不重叠的信道, 这是多路由组网中最关键的一步. 2.4G 频段 (主 1 / 从 11): 2.4G 只有 1、6、11 这三个信道是完全互不干扰的. 主从路由分别占用 1 和 11, 确保了两台设备在发射 2.4G 信号时不会“打架”(同频干扰), 保证了智能家居等依赖 2.4G 设备的稳定性. 5G 频段 (主 36 / 从 52): 主路由使用了低信道 36, 从路由使用了 DFS 信道 52. 这两个信道在 80MHz 频宽下是完全独立的. 总结: 这种空间上的信道隔离, 意味着当设备在主路由和从路由之间移动时, 背景底噪最低, 网络吞吐量能达到最大化. 2. 频宽策略 (Bandwidth): 兼顾稳定性与极致速度 2.4G 采用 20MHz: 这是非常明智的做法. 虽然 40MHz 理论速度更快, 但在 2.4G 这个极度拥挤的频段(微波炉、蓝牙都在用), 开启 40MHz 会成倍增加干扰, 导致频繁断流. 锁定 20MHz 牺牲了极速, 但换来了最大的穿墙稳定性和覆盖范围, 非常适合对速度要求不高的 IoT 智能设备. 5G 采用 80MHz: 80MHz 是目前绝大多数手机、电脑的主流甜点频宽, 能够提供极高的内网传输和外网下载速度, 是主力上网频段. 3. 功率策略 (Tx Power): “弱 2.4G, 强 5G” 这套功率配置是这组参数中最亮眼的地方, 它完美解决了多路由环境下的“设备粘连”(Sticky Client)问题. 2.4G 功率调低 (18 dBm / 20 dBm): 2.4G 信号波长长, 穿墙能力极强. 如果不降低功率, 手机在家里走动时, 会一直死死“咬”住远处的 2.4G 信号不放, 导致网速极慢. 调低主从路由的 2.4G 功率, 可以人为缩小 2.4G 的覆盖圈, 促使设备在信号变弱时尽早断开并寻找更好的信号. 5G 功率拉满 (23 dBm / 24 dBm): 5G 信号穿墙能力弱, 衰减快. 保持高功率可以尽量弥补它的穿墙劣势, 扩大 5G 的高速覆盖范围. 总结: 这种功率差配置, 在物理层面实现了一种天然的“频段引导 (Band Steering)”. 当手机同时收到 2.4G 和 5G 信号时, 高功率的 5G 信号强度很容易超过低功率的 2.4G 信号, 从而让手机心甘情愿地优先连接到速度更快的 5G 网络. 4. 漫游配合 (Roaming) 在上述物理层的频段、频宽和功率都调优到位的基础上, 配合 802.11k/v/r 协议, 形成了一个完美的漫游闭环: 11k (邻居报告) + 11v (BSS 转换管理): 路由器会主动告诉手机“附近还有哪个节点信号更好”, 并建议手机切换. 由于 2.4G 功率被压制, 手机在移动时很容易触发 11v 的切换阈值. 11r (快速转换): 结合统一的 SSID (PINer) 和相同的加密方式, 省去了手机切换路由器时重新输入密码认证的几百毫秒时间, 实现了真正的“无缝”体验(比如微信语音不断线). 相同的 mobility_domain 和唯一的 nasid 也是标准的 11r 配置要求. Ref: Gemini 最终的频段, 频宽, 功率的配置是: 主路由 192.168.3.1 2.4G: channel 1 / 20 MHz / 18 dBm 5G: channel 36 / 80 MHz / 24 dBm 从路由 192.168.3.2 2.4G: channel 11 / 20 MHz / 20 dBm 5G: channel 52 / 80 MHz / 23 dBm 效果 & 后续 最终经过测试, 在客厅, 阳台, 厨房, 卫生间中都是默认连接到主路由, 在次卧中会自动切换到从路由连接, 在书房中关上门也会从主路由切换到从路由. 从信号强度下降到实际切换大概需要 5s 左右. 主卧不能确定, 有的时候会切换有的时候不确定, 但是在 5 次测试中有 3 次都切换了. 插 5Gbps 网口之后测速: 实际测速下行速率能跑到 1336.31 Mbps, 上行速率能跑到 158.16 Mbps. 应该是成功了 (? 但是实际使用体感不算太大, 因为当时测试了很多平台, 很少有平台能够跑满的, 加上现在的一些运营商策略比如 QoS 什么的, 速度很难跑满. 不过如果有 IDM 之类的多线程下载工具应该能发挥更大的作用?
2026年07月11日
102 阅读
0 评论
2 点赞
充分利用systemd的特性构建多租户CodeServer
分析 起因是大学Python课,不想每次课都带笔记本过去太重了,尝试能不能一个平板就搞定。找来找去发现coder/code-server: VS Code in the browser很符合要求,同时还能兼顾跨平台。当时自己部署了一个玩玩,后来朋友他们也想要,就在思考怎么弄能够兼顾多租户和安全性。最开始想到的是容器化,但是因为涉及到pip安装新的包,容器化不太好做,可能得把python的site-package什么的一堆都给持久化,而且性能也不好,所以还是打算使用二进制部署。 需求很简单,大致概括如下: 每个用户可以用自己的域名登录进入CodeServer 每个用户可以自己修改登录密码 每个用户的Python环境完全隔离,可独立安装pip包 用户间不应该能互相访问到对方的文件 防止用户误操作删掉系统关键组件 防止用户误操作写出死循环之类的耗尽服务器资源 防止用户利用服务器挖矿/做跳板攻击别人 经过分析和与ai的讨论,最终定下来的方案大致是这样: 利用Linux自己的多用户机制,每个用户在自己的家目录下运行一个CodeServer做最基础的隔离。这样需求145可以很轻松的实现; 监听端口根据uid计算。每个用户下手动创建一个venv,并通过.bashrc让用户启动shell的时候自动启用,解决需求3; 利用systemd的限制来限制进程的行为,实现一定程度上实现需求6-7 利用systemd的Path机制监听$HOME/.config/code-server/config.yaml的修改并自动重启daemon,使得用户可以通过修改配置文件来改登录密码,较为优雅地实现需求2。 对于7,我觉得根据大一同学的计算机水平倒是不用太担心(应该吧?) 没有直接断网是因为考虑到有些需求需要联网下软件包,或者可能涉及到爬虫操作什么的。systemd的限制应该已经能拦截绝大部分恶意操作了。 本文讨论不涉及SELinux,因为个人认为当前需求使用SELinux有点过度设计了,并且我也对SELinux/SEPolicy不太熟(这才是主要原因吧233),同时也想尽量让用户在合理使用过程中不受限制,因此并未使用SELinux来限制。 部署 配置systemd 下载下来codeserver二进制文件传到服务器上,比如我丢在/usr/bin/code-server。然后编写systemd配置,创建/etc/systemd/system/code-server@.service: # /etc/systemd/system/code-server@.service [Unit] Description=Code-Server for %i After=network.target [Service] Type=simple User=%i Group=%i WorkingDirectory=/home/%i MemoryMax=1G CPUQuota=150% TasksMax=200 IOWeight=50 ProtectSystem=strict PrivateTmp=yes PrivateDevices=yes NoNewPrivileges=yes RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX # ExecStart=/bin/bash -c "PASSWORD=$(echo -n "%i" | md5sum | cut -d' ' -f1) /usr/bin/code-server --bind-addr 0.0.0.0:$((7000 + $(id -u %i))) --auth password" ExecStart=/bin/bash -c ' \ PORT=$((7000 + $(id -u %i) %% 10000)); \ /usr/bin/code-server --bind-addr 0.0.0.0:$PORT; \ ' Restart=always RestartSec=5 [Install] WantedBy=multi-user.target 这里创建的文件名是code-server@.service,@之后的内容会被当作%i替换。设计上是利用%i传递用户名称,计算端口并自启动。端口计算逻辑是7000+uid%10000,一般创建的uid是1000开始,因此正好按照创建顺序从8000开始监听端口。 值得注意的是这部分配置: MemoryMax=1G CPUQuota=150% TasksMax=200 IOWeight=50 ProtectSystem=strict PrivateTmp=yes PrivateDevices=yes NoNewPrivileges=yes RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX (我懒得解释了,让ai解释一下吧) 它主要分为两个方面:资源限制(Resource Control) 和 安全沙盒隔离(Security and Sandboxing)。 一、 资源限制 (Resource Control) 这部分利用 Linux 的 cgroups 机制,防止该服务消耗过多系统资源,影响其他程序运行。 MemoryMax=1G 作用: 限制该服务最多只能使用 1GB 的内存。 结果: 如果服务尝试使用的内存超过 1GB,系统(OOM Killer)会强制杀死该进程以保护系统。 CPUQuota=150% 作用: 限制该服务的 CPU 使用率最高为 150%。 结果: 100% 代表一个完整的 CPU 核心。150% 意味着该服务最多可以占满一个核心,并使用第二个核心的一半(即最多使用 1.5 个 CPU 核心的算力)。 TasksMax=200 作用: 限制该服务可以创建的最大任务(进程或线程)数量为 200 个。 结果: 防止服务因为遭遇漏洞或死循环(如 Fork Bomb)而无限创建子进程,从而耗尽系统 PID 资源使系统崩溃。 IOWeight=50 作用: 设置磁盘 I/O(输入/输出)的权重。默认值通常是 100。 结果: 设置为 50 意味着当系统 I/O 繁忙时,该服务获得磁盘读写资源的优先级低于默认服务,防止它在大量读写文件时卡死整个系统。 二、 安全与隔离 (Security & Isolation) 这部分通过 Linux 命名空间(Namespaces)和其他内核安全机制,将服务“关在笼子里”,即使服务被黑客攻破,也能将破坏降到最低。 ProtectSystem=strict 作用: 严格保护系统文件。 结果: 将整个操作系统的文件系统(/ 根目录及其下的所有内容,除了 /dev, /proc 和 /sys 等特殊 API 目录)对该服务设为只读。该服务无法修改、覆盖或删除任何系统核心文件。 PrivateTmp=yes 作用: 为该服务提供独立的临时文件目录。 结果: 服务会看到一个自己专属的 /tmp 和 /var/tmp 目录。它无法看到或修改其他用户/服务放在全局 /tmp 中的文件,反之亦然。这能有效防止基于临时文件的符号链接攻击或数据泄露。 PrivateDevices=yes 作用: 隔离物理设备。 结果: 为该服务挂载一个私有的 /dev 目录,里面只包含伪设备(如 /dev/null, /dev/zero, /dev/urandom 等)。该服务将完全看不到真实的物理硬件设备(如硬盘 /dev/sda、USB 设备等),彻底杜绝了直接读写底层块设备的可能。 NoNewPrivileges=yes 作用: 禁止权限提升。 结果: 确保该服务及其所有子进程,无论如何都无法获得新的系统权限。即使服务调用了一个带有 SUID 位的程序(比如 sudo 或 su),也无法借此提升为 root 权限。 RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX 作用: 限制该服务可以使用的网络协议族。 结果: 该服务只能使用: AF_INET:IPv4 网络通信 AF_INET6:IPv6 网络通信 AF_UNIX:本地 UNIX 域套接字(用于本地进程间通信) 除此之外的网络协议(例如底层的数据包抓取 AF_PACKET、蓝牙 AF_BLUETOOTH 等)全部被内核拦截。这大大减少了网络层面的攻击面。 创建新用户时直接: adduser <username> systemctl enable --now code-server@<username> 即可同时创建对应的CodeServer服务 配置修改密码自动重启 创建文件/etc/systemd/system/code-server-restart@.path: # /etc/systemd/system/code-server-restart@.path [Unit] Description=Monitor code-server config change for %i After=network.target [Path] PathChanged=/home/%i/.config/code-server/config.yaml [Install] WantedBy=multi-user.target 创建文件/etc/systemd/system/code-server-restart@.service: [Unit] Description=Triggered safe restart for %i [Service] Type=oneshot User=root ExecStart=/usr/local/bin/safe-restart-codeserver.sh %i [Install] WantedBy=multi-user.target 这里的运行逻辑是这样的: 第一步:启动监控 (由 .path 文件负责) 系统或管理员启动了code-server-restart@<username>.path。 systemd会解析这个.path文件,将%i替换为用户名。 PathChanged=/home/<username>/.config/code-server/config.yaml:systemd 会在内核层面(利用 inotify 机制)开始静默监听这个特定路径的 config.yaml 文件。 第二步:检测变更并触发 在 code-server 网页端修改了设置,或者通过命令行直接编辑了 config.yaml 并保存。 PathChanged 检测到文件被修改并且被关闭(这意味着写入已完成,避免读到写了一半的脏数据)。 隐式绑定: 因为 .path 文件中没有使用 Unit= 显式指定要触发哪个服务,systemd 的默认行为是触发与它同名去掉后缀的 .service 文件。 于是,systemd 自动拉起 code-server-restart@<username>.service。 第三步:执行动作 (由 .service 文件负责) systemd 开始执行 code-server-restart@<username>.service。同样,这里的 %i 也会被替换为用户名。 Type=oneshot:告诉系统这不是一个常驻后台的服务,而是一次性的任务。执行完就结束。 User=root:这个重启操作需要较高的权限,因此强制以 root 身份执行。 ExecStart=/usr/local/bin/safe-restart-codeserver.sh <user>:这是整个逻辑的最终落脚点。系统以 root 身份运行了这个自定义的 shell 脚本,并将用户名作为参数传递给它。 没有直接在restart.service里重启主daemon,因为考虑到CodeServer编辑文件是每修改一点就会自动保存,因此当用户修改密码过程中,可能会存在修改了一半就被触发重启,导致后面的部分没有成功写入的情况,因此编写了一个脚本,判断等文件没有修改之后才触发重启: /usr/local/bin/safe-restart-codeserver.sh #!/bin/bash USER_NAME=$1 CONFIG_FILE="/home/$USER_NAME/.config/code-server/config.yaml" STAMP_FILE="/tmp/code-server-restart-${USER_NAME}.stamp" # 1. 前置拦截:如果记录文件存在,且配置文件比记录文件旧(或时间相同) # 说明这一次触发是 systemd 排队遗留下来的无意义事件,直接退出 if [ -f "$STAMP_FILE" ] && [ "$CONFIG_FILE" -ot "$STAMP_FILE" ]; then echo "Config hasn't changed since last restart, exiting." exit 0 fi # 2. 原有的防抖等待逻辑(保持不变) while true; do last_md5=$(md5sum "$CONFIG_FILE") sleep 5 current_md5=$(md5sum "$CONFIG_FILE") if [ "$last_md5" == "$current_md5" ]; then # 文件稳定了,执行重启 systemctl restart code-server@$USER_NAME # 3. 关键动作:重启成功后,更新时间戳文件的修改时间 touch "$STAMP_FILE" break else echo "Config file for $USER_NAME is still changing, waiting..." fi done 那个时间戳机制是为了防止多次触发导致无效重启,若检测到配置文件编辑时间早于上次重启时间则拒绝重启。 完成编辑后启用触发器:systemd enable --now code-server-restart@<username>.path(注意是.path不是.service),接着在web界面中尝试缓慢编辑config文件中的密码,查看日志就可以发现类似于如下记录: ○ code-server-restart@lyr.service - Triggered safe restart for lyr Loaded: loaded (/etc/systemd/system/code-server-restart@.service; disabled; preset: enabled) Active: inactive (dead) since Sat 2026-06-06 06:43:55 UTC; 29s ago Invocation: 0bd37fcb6ea44a58870c06b4fde5300c TriggeredBy: ● code-server-restart@lyr.path Process: 6997 ExecStart=/usr/local/bin/safe-restart-codeserver.sh lyr (code=exited, status=0/SUCCESS) Main PID: 6997 (code=exited, status=0/SUCCESS) Mem peak: 2.5M CPU: 43ms Jun 06 06:43:40 CodeServer systemd[1]: Starting code-server-restart@lyr.service - Triggered safe restart for lyr... Jun 06 06:43:45 CodeServer safe-restart-codeserver.sh[6997]: Config file for lyr is still changing, waiting... Jun 06 06:43:50 CodeServer safe-restart-codeserver.sh[6997]: Config file for lyr is still changing, waiting... Jun 06 06:43:55 CodeServer systemd[1]: code-server-restart@lyr.service: Deactivated successfully. Jun 06 06:43:55 CodeServer systemd[1]: Finished code-server-restart@lyr.service - Triggered safe restart for lyr. 当停止编辑5秒之后CodeServer才会重启,后续可以使用新密码登录。 配置Python venv 首先系统里需要安装Python3,这就不多赘述了,包管理器直接装即可。同时安装一个python3-venv: apt install python3-venv 接着,进入用户的家目录,我选择创建了一个名为.venv的文件夹用于存放虚拟环境。在.venv中执行python3 -m venv myvenv创建一个名叫myvenv的虚拟环境,并在用户的.bashrc后加上一行: fi fi +source ~/.venv/myvenv/bin/activate 之后,启动shell的时候就会自动启用venv。 为CodeServer安装Python插件并指派使用venv 这一步就不过多赘述了,在网页上给CodeServer安装Python和调试器插件,并指定Python解释器为venv下的python即可。 后记 其实这个配置很早就做了但是一直没写,当时还没爆出来CopyFail和DirtyFrag这样的漏洞。爆出来的时候第一时间做了测试,发现这套systemd配置阴差阳错的全都防住了,大致分析如下: 因为CopyFail使用了一个特殊的套接字类型:AF_ALG,而systemd配置只允许AF_INET AF_INET6 AF_UNIX这三种,因此CopyFail无法提权。DirtyFrag也是同理,其利用的AF_NETLINK、AF_RXRPC、AF_ALG都被禁止了。 同时,NoNewPrivileges=yes也是最后的防线,代码的最终目标是篡改/usr/bin/su植入恶意ELF shellcode或篡改/etc/passwd清空root密码,随后调用具有setuid权限的su命令来获取 root 权限。而NoNewPrivileges一旦设置,该进程及其派生的所有子进程,无论执行什么程序,都绝对无法通过文件的setuid或setgid标志获得更高的权限。退一万步讲,即使后续有漏洞找到了某种手段绕过了网络限制,并成功将/usr/bin/su替换成了恶意Shell,当它执行su时,弹出来的依然是一个低权限用户的Shell,提权彻底沦为空谈。 本文没有讨论SELinux,因为当前方案在“朋友共用、非生产核心”的场景下是可接受的简化,避免了配置错误导致功能不可用的风险和运维复杂度,但实际上systemd的这些沙盒选项主要基于namespace和cgroup隔离,并不能完全替代强制访问控制(也就是SELinux和AppArmor这类ACL),对于需要更高安全等级的租户隔离(如不可信用户之间需防止内核漏洞逃逸),SELinux/AppArmor仍然是主流。 当然,这个方案也存在其他一些局限,比如当用户非常多,或者uid分配有特殊需求,超过10000时原先计算端口的方式可能会失效;对于科学计算等其他正常需求,目前的限制可能过于严格等。本文仅作为一种思路探讨和参考。 碎碎念:在容器化和云原生的时代,传统systemd还有很多值得我们去探求的特性可以做到很多事情呢…… 参考文章: systemd.git - A fork of systemd to make components more independant
2026年06月06日
84 阅读
0 评论
1 点赞
为小米BE10000路由器的QWRT适配NFC功能
分析 将小米BE10000刷入QWRT后,设备的网络潜能确实得到了极大的释放,2.5G 猫棒、SFP+接口等高级特性均能完美工作,唯一美中不足的是原厂自带的NFC碰一碰连Wi-Fi功能失效了。查询资料知道,NFC标签本质上是一块挂载在主板上的EEPROM芯片。使用i2cdetect进行扫描: root@QWRT:~# i2cdetect -l i2c-1 i2c QUP I2C adapter I2C adapter i2c-2 i2c QUP I2C adapter I2C adapter i2c-0 i2c QUP I2C adapter I2C adapter root@QWRT:~# i2cdetect -y -r 0 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- 54 -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- -- 扫描结果很快锁定,设备挂载在I2C总线0上,物理地址为0x54。 NFC碰一碰连WiFi则用的是标准的NDEF格式的数据(Ref: Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation),因此只需要按照标准将数据写入EEPROM即可复现NFC碰一碰连接功能。 实现 本文内容仅探讨 OpenWrt/QWRT 系统的底层硬件驱动适配与 NDEF 标准协议的封装。文中涉及的硬件参数系基于公开规范文档与通用 I2C 调试工具探测得出。本项目为个人研究兴趣,未包含、也未分发任何厂商专有二进制代码。仅供技术交流学习,请勿用于商业用途,因尝试本文操作导致的设备损坏风险需由读者自行承担。 提取数据构造NDEF数据 要实现自动根据Wi-Fi账号密码更新NFC数据,首先得拿到当前的Wi-Fi账号密码。在OpenWrt上,这部分配置全部由UCI (Unified Configuration Interface) 管理,因此我们只需要读取 UCI 中的 wireless 配置文件即可。 为应对现代手机的兼容性问题。早期的设备通常使用Device Password Token触发WPS协商,但现代Android/iOS系统已经限制了这种行为。为了兼顾兼容性,我们需要遵循Wi-Fi Simple Configuration (WSC)规范,将配置打包成 WLAN Configuration Token (凭证配置令牌)。(Ref: Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation) 将OpenWrt的无线加密模式(如WPA2、WPA3-SAE)精准映射为 WSC 规范定义的 Hex 代码: 0x1003: 认证类型 (Authentication Type) 0x100F: 加密类型 (Encryption Type) 0x1045: SSID 0x1027: 网络密钥 (密码) 通过脚本自动遍历并选举出桥接到lan的首选AP(wifi0),将其属性转换为Hex字符串,我们就得到了一串标准的NDEF注入载荷。 写入NFC EEPROM NFC的EEPROM在接收长串的NDEF数据时,如果连续写入太快,或者单次写入块过大,容易导致芯片的I2C状态机死锁。 经过测试,最终选择使用i2ctransfer工具来进行分片原子写入。两个关键的时序细节: 由于通信限制,每次循环只切片取 4 个字节,带有寄存器地址递增的方式进行片段写入。 在每次 4 字节的 block 写入之间,强制加上10毫秒的时序延迟,给芯片留下足够的内部擦写时间。最后对不足 4 字节的数据用 0x00 补零。 实现更改Wi-Fi信息自动触发写入 为了尽量贴合OpenWRT的架构,原先通过下hook的方式尝试了,但是发现经常会hook不上,最后加了三层fallback: LuCI前端触发: 通过在/etc/uci-defaults/下注册钩子,将NFC同步脚本与系统的ucitrack机制绑定。当用户在LuCI中修改了Wi-Fi密码并点击“保存并应用”,系统就会在后台自动更新NFC数据。 Hotplug层: 在/etc/hotplug.d/iface/70-nfc中添加热插拔监听事件。当路由器的lan或wifi接口发生ifup状态改变时,系统能够自动触发调度。 定时任务: 若所有方式都没能触发,每隔15秒会强制检查一次并判断是否需要更新NFC数据。 同时,考虑到NFC芯片的EEPROM擦写次数是有限的。如果网络接口重启一次就全量重写一次,芯片很快就会报废。因此,在底层的nfc-sync调度脚本中,引入了简单的哈希校验机制: 脚本被唤醒后,首先提取当前的 wireless 配置并计算 MD5 值。 将其与缓存在/var/run/nfc-wireless.md5中的旧哈希进行对比。 只有当 MD5 值发生实际变化时,才会真正下发 I2C 写入指令。 否则直接终止流程。 结合/var/lock/nfc-sync.lock的并发文件锁,这套逻辑确保了在任何网络抖动、多重事件并发的情况下,NFC硬件的寿命都能得到绝对的保障。 经过测试,发现是能够自动更新的: 代码仓库:KaguraiYoRoy/be10000-qwrt-nfc: NFC Userland Implementation of QWRT for Xiaomi BE10000 (RC01) Router 参考文章: Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation
2026年05月25日
98 阅读
0 评论
1 点赞
从零构建跨地域 K3s 集群 - Calico 无封装 CNI
前言 其实老早就想玩玩 K8s 集群了,一直觉得没有足够的知识支撑,玩起来比较的费劲就没尝试。 前段时间好好研究了一下 DN42 和 BGP, OSPF 之类的组网协议,发现现在理解起来不那么费劲了,于是果断上手 K3s( 选择 K3s 而不是 K8s 主要原因还是其轻量化:资源要求低,部署不需要拉一大堆镜像,有国内镜像……总之就是,觉得 K3s 比较符合我的需求。 咱是刚开始研究 K3s 的小白,若有错误还请各位大佬手下留情~ 分析 CNI 组件的选择 我目前的网络架构是这样的: graph TD subgraph ZeroTier Domestic subgraph WDS Gateway <--> VM1 Gateway <--> VM2 end NGB <--> Gateway HFE-NAS <--> Gateway NGB <--> HFE-NAS end subgraph IEPL Global-NIC <==OSPF==> CN-NIC end subgraph ZeroTier Global HKG02 <--> HKG04 TYO <--> HKG04 TYO <--> HKG02 end CN-NIC <--> NGB CN-NIC <--> HFE-NAS CN-NIC <--OSPF--> Gateway Global-NIC <--OSPF--> TYO Global-NIC <--OSPF--> HKG02 Global-NIC <--OSPF--> HKG04 %% 样式定义:设置为橘色背景、加粗边框以代表路由器 classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold; class Global-NIC,CN-NIC,Gateway router; 其中, WDS 节点是个 ProxmoxVE,下挂多个 VM ,通过 OSPF 广播其 VM 的 IPv4 Prefix 地址,香港节点需要访问到 WDS 节点下挂 VM 时便可以通过加入 OSPF 内网实现多跳可达。这样封装层数也只有1层,不需要担心 MTU 消消乐。 我打算在 WDS 下新开两个 VM 分别用作主控和一个节点(暂且称其为 KubeMaster 、KubeNode-WDS1),然后 HKG04 (暂且称为KubeNode-HKG04) 也当作一个节点接入 K3s。 最简单的方式其实是直接通过 K3s 默认的 Flannel 作为 CNI,但是 Flannel 是基于 VXLAN 的,再套一层我现有的内网的话就会产生如下 MTU 消消乐的情况: 数据包 -> Flannel VXLAN封装 -> ZeroTier封装 -> 物理链路 实际容器间通信可用 MTU 大概得压缩到 1350 甚至更低。因此,我尝试寻找一个能直接基于这套内网工作的 CNI 方案,然后就找到了 Calico。了解下来知道 Calico 是以 BGP 作为底层寻路协议,支持通过 No-Encapsulated 即无封装模式启动,数据包直接交由上层路由器处理路由,因此选择 Calico 作为 CNI 组件。 路由设计 为了保证中间节点的路由器可以知道如何路由 Pod 的 IP,而 KubeMaster 和 KubeNode-WDS1 在 ProxmoxVE 主机下,他们需要跨越整个内网与 HKG04 建立 BGP, 因此这就意味着中间每一级路由都需要学习到完整的 BGP 路由,这样才能打通这样的路由路径: graph LR subgraph WDS KubeMaster KubeNode-WDS1 Gateway end subgraph IEPL CN-Namespace Global-Namespace end KubeNode-WDS1 <--> Gateway KubeMaster <--> Gateway <--> CN-Namespace <--> Global-Namespace <--> HKG04 %% 样式定义:突出显示具备路由功能的节点 classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold; class Gateway,CN-Namespace,Global-Namespace router; 否则,中间的任何一跳都会因为不认识来源/目标 IP 导致丢包。同时,由于 iBGP 从邻居学到的路由,不能继续传递给下一个 iBGP 邻居的特性,Gateway、CN-Namespace、Global-Namespace 与节点间的 BGP Session 都需要启用 Route Reflector, 否则节点无法正确互相学习到路由。 虽然但是,其实这种架构更适合做 BGP Confederation ( BGP 联邦),但是我现有的网络已经很复杂,再加 BGP 联邦会让后期维护起来比较麻烦,而且我的节点数量也不多,iBGP Full Mesh 的开销还能接受。 绝对不是因为我懒( 所以最终网络路由结构是这样的: graph TB subgraph WDS VM1 VM2 Gateway end subgraph IEPL CN-Namespace Global-Namespace end VM1 <-.Calico iBGP Full Mesh.-> VM2 VM1 <--iBGP Route Reflector--> Gateway VM2 <--iBGP Route Reflector--> Gateway <--iBGP--> CN-Namespace <--iBGP--> Global-Namespace Gateway <--iBGP--> Global-Namespace HKG04 <-.Calico iBGP Full Mesh.-> VM1 Global-Namespace <--iBGP Route Reflector--> HKG04 VM2 <-.Calico iBGP Full Mesh.-> HKG04 classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold; class Gateway,CN-Namespace,Global-Namespace router; 虚线部分的 BGP Session 是 Calico 自动创建的,实现部分是需要我们手动指派创建的 保留 Calico 自己的 iBGP Full Mesh 是为了后续可扩展性考虑,使得各个节点之间可以尽量通过 ZeroTier P2P 优先建立直连网络,而不是从 Route Reflector 汇聚路由器转发绕一圈。 部署 理清了结构之后部署就很简单了。 开启内核转发并关闭 rp_filter 老生常谈。 echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf echo "net.ipv6.conf.default.forwarding=1" >> /etc/sysctl.conf echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf echo "net.ipv4.conf.default.rp_filter=0" >> /etc/sysctl.conf echo "net.ipv4.conf.all.rp_filter=0" >> /etc/sysctl.conf sysctl -p 安装 K3s Master 因为 KubeMaster 主控节点在境内,所以最好配置一下镜像加速: mkdir -p /etc/rancher/k3s cat <<EOF > /etc/rancher/k3s/registries.yaml mirrors: docker.io: endpoint: - "https://docker.m.daocloud.io" quay.io: endpoint: - "https://quay.m.daocloud.io" EOF 使用镜像源安装: curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \ INSTALL_K3S_MIRROR=cn INSTALL_K3S_EXEC=" \ --flannel-backend=none \ --disable-network-policy \ --cluster-cidr=10.42.0.0/16" sh - 需要注意的是要指定 --flannel-backend=none 和 --disable-network-policy 来禁用默认 CNI 组件。 使用 cat /var/lib/rancher/k3s/server/node-token 查看 Token ,并记录下来。 WorkerNode 境内节点配置镜像加速: mkdir -p /etc/rancher/k3s cat <<EOF > /etc/rancher/k3s/registries.yaml mirrors: docker.io: endpoint: - "https://docker.m.daocloud.io" quay.io: endpoint: - "https://quay.m.daocloud.io" EOF 然后使用镜像源安装 K3s 并加入集群: export INSTALL_K3S_MIRROR=cn export K3S_URL=https://<主控节点 IP>:6443 # 换成你的主节点实际IP export K3S_TOKEN=K10...你的TOKEN...::server:xxx # 换成第一步获取的完整TOKEN curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | sh - 这个时候各个节点的状态应该是 NotReady 的,因为缺少 CNI 组件。 安装 Calico 并配置 No-Encap 模式 在主控上手动下载下来 https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/tigera-operator.yaml ,安装 Calico 算子: kubectl create -f tigera-operator.yaml 配置自定义资源,创建一个 custom-resource.yaml 文件: apiVersion: operator.tigera.io/v1 kind: Installation metadata: name: default spec: # 添加镜像注册表配置 registry: quay.m.daocloud.io calicoNetwork: ipPools: - blockSize: 26 cidr: 10.42.0.0/16 encapsulation: None natOutgoing: Enabled nodeSelector: all() 此处通过指定 encapsulation: None 来设置 No-Encap 模式。想要修改 IPv4 CIDR 也可以在这里改。随后 kubectl apply -f custom-resource.yaml 执行安装。使用: kubectl get pods -A -o wide 查看 Pod 状态,等待各个节点拉取完成即可。 配置 BGP 拓扑 节点打标 通过给节点打标来指定 WDS 下的节点全都连接到 WDS 节点的 Gateway 的 BGP,境外节点全部连接 Global Namespace 的 BGP: kubectl label nodes kubemaster region=WDS kubectl label nodes kubenode-wds-1 region=WDS kubectl label nodes kubenode-hkg04 region=Global Calico 配置 编写 yaml 配置文件: apiVersion: crd.projectcalico.org/v1 kind: BGPPeer metadata: name: route-reflector-domestic spec: nodeSelector: region == 'Domestic' # 这部分其实没用上,我原来设计的是 Domestic 区域有个总体的汇聚路由 peerIP: 100.64.0.108 asNumber: 64512 --- apiVersion: crd.projectcalico.org/v1 kind: BGPPeer metadata: name: route-reflector-wds spec: nodeSelector: region == 'WDS' peerIP: 192.168.100.1 asNumber: 64512 --- apiVersion: crd.projectcalico.org/v1 kind: BGPPeer metadata: name: route-reflector-global spec: nodeSelector: region == 'Global' peerIP: 100.64.1.106 asNumber: 64512 这部分的意思是: 所有 region 标签为 Domestic 的节点都添加一个连接到 100.64.0.108 (即境内汇聚路由)的 BGP Session,使用 AS 64512 所有 region 标签为 WDS 的节点都添加一个连接到 192.168.100.1 (即 WDS 节点所有 VM 的 Gateway)的 BGP Session,使用 AS 64512 所有 region 标签为 Global 的节点都添加一个连接到 100.64.1.106 (即境外汇聚路由)的 BGP Session,使用 AS 64512 借此实现上文图示的,所有 WDS 节点下的 VM,包括主控和 KubeNode-WDS1 都接入到 WDS 节点的 Gateway 汇聚路由,境外区域的所有节点都接入到境外部分的汇聚路由。 配置汇聚路由 iBGP 这部分直接写 Bird 配置文件就行了,简单( 这里举几个例子: k3s/ibgp.conf: function is_insider_as(){ if bgp_path.len > 0 && !(bgp_path ~ [= 64512 =]) then { return false; } if net ~ [ 10.42.0.0/16{16,32} ] then { return true; } return false; } template bgp k3sbackbone{ local as K3S_AS; router id INTRA_ROUTER_ID; neighbor as K3S_AS; ipv4{ table intra_table_v4; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; extended next hop; }; ipv6{ table intra_table_v6; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; }; }; template bgp k3speers{ local as K3S_AS; neighbor as K3S_AS; router id INTRA_ROUTER_ID; rr client; rr cluster id INTRA_ROUTER_ID; ipv4{ table intra_table_v4; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; }; ipv6{ table intra_table_v6; import filter{ if is_insider_as() then accept; reject; }; export filter{ if is_insider_as() then accept; reject; }; next hop self; }; }; include "ibgpeers/*"; ibgpeers/backbone-cn.conf: protocol bgp 'k3s_backbone_cn_v4' from k3sbackbone{ neighbor fd18:3e15:61d0:cafe:f001::1; }; ibgpeers/master.conf: protocol bgp 'k3s_master_v4' from k3speers{ neighbor 192.168.100.251; }; 主要是几个汇聚路由之间最好不要开 Route Reflector,以及记得开 next hop self。 全部完成之后使用 kubectl get nodes 应该能看到节点状态都 Ready 了: NAME STATUS ROLES AGE VERSION kubemaster Ready control-plane 2d23h v1.34.5+k3s1 kubenode-hkg04 Ready <none> 11h v1.34.6+k3s1 kubenode-wds-1 Ready <none> 2d7h v1.34.5+k3s1 使用 kubectl get pods -A -o wide 查看 Pods: NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES calico-system calico-kube-controllers-64fc874957-6bdlz 1/1 Running 0 5h38m 10.42.253.136 kubenode-hkg04 <none> <none> calico-system calico-node-2qz82 1/1 Running 0 4h24m 10.2.5.7 kubenode-hkg04 <none> <none> calico-system calico-node-dhl2c 1/1 Running 0 4h24m 192.168.100.251 kubemaster <none> <none> calico-system calico-node-nbpkj 1/1 Running 0 4h23m 192.168.100.252 kubenode-wds-1 <none> <none> calico-system calico-typha-7bb5db4bdc-rfpwg 1/1 Running 0 5h38m 10.2.5.7 kubenode-hkg04 <none> <none> calico-system calico-typha-7bb5db4bdc-rwwr5 1/1 Running 0 5h38m 192.168.100.251 kubemaster <none> <none> calico-system csi-node-driver-jglwp 2/2 Running 0 5h38m 10.42.64.68 kubenode-wds-1 <none> <none> calico-system csi-node-driver-jqjsc 2/2 Running 0 5h38m 10.42.253.137 kubenode-hkg04 <none> <none> calico-system csi-node-driver-vk26s 2/2 Running 0 5h38m 10.42.141.16 kubemaster <none> <none> kube-system coredns-695cbbfcb9-8fx4p 1/1 Running 1 (7h27m ago) 2d23h 10.42.141.14 kubemaster <none> <none> kube-system helm-install-traefik-crd-5bkwx 0/1 Completed 0 2d23h <none> kubemaster <none> <none> kube-system helm-install-traefik-m9fgj 0/1 Completed 1 2d23h <none> kubemaster <none> <none> kube-system local-path-provisioner-546dfc6456-dmn4g 1/1 Running 1 (7h27m ago) 2d23h 10.42.141.15 kubemaster <none> <none> kube-system metrics-server-c8774f4f4-2wkwh 1/1 Running 1 (7h27m ago) 2d23h 10.42.141.12 kubemaster <none> <none> kube-system svclb-traefik-999cddce-hpmcm 2/2 Running 6 (7h26m ago) 11h 10.42.253.134 kubenode-hkg04 <none> <none> kube-system svclb-traefik-999cddce-q4225 2/2 Running 2 (7h27m ago) 2d22h 10.42.141.9 kubemaster <none> <none> kube-system svclb-traefik-999cddce-xmd64 2/2 Running 2 (7h26m ago) 2d6h 10.42.64.66 kubenode-wds-1 <none> <none> kube-system traefik-788bc4688c-vbbhj 1/1 Running 1 (7h27m ago) 2d22h 10.42.141.13 kubemaster <none> <none> tigera-operator tigera-operator-6b95bbf4db-vl46l 1/1 Running 1 (7h27m ago) 2d23h 192.168.100.251 kubemaster <none> <none> 使用 kubectl exec -it -n calico-system <calico-node-xxxx> -- birdcl s p 可查看 Bird 的状态: root@KubeMaster:~/kube/calico# kubectl exec -it -n calico-system calico-node-2qz82 -- birdcl s p Defaulted container "calico-node" out of: calico-node, flexvol-driver (init), install-cni (init) BIRD v0.3.3+birdv1.6.8 ready. name proto table state since info static1 Static master up 08:58:17 kernel1 Kernel master up 08:58:17 device1 Device master up 08:58:17 direct1 Direct master up 08:58:17 Mesh_192_168_100_251 BGP master up 08:58:33 Established Mesh_192_168_100_252 BGP master up 08:59:00 Established Node_100_64_1_106 BGP master up 12:57:44 Established ip r 可查看系统路由表: root@KubeMaster:~/kube/calico# ip r default via 192.168.100.1 dev eth0 proto static 10.42.64.64/26 proto bird nexthop via 192.168.100.1 dev eth0 weight 1 nexthop via 192.168.100.252 dev eth0 weight 1 blackhole 10.42.141.0/26 proto bird 10.42.141.9 dev caliac6501d3794 scope link 10.42.141.12 dev calib07c23291bb scope link 10.42.141.13 dev caliab16e60bd19 scope link 10.42.141.14 dev calid5959219080 scope link 10.42.141.15 dev cali026d8f1ddb7 scope link 10.42.141.16 dev califa657ba417a scope link 10.42.253.128/26 via 192.168.100.1 dev eth0 proto bird 192.168.100.0/24 dev eth0 proto kernel scope link src 192.168.100.251 找一个Pod 的地址 Ping 一下,如果没啥问题的话应该就能直接通了: root@KubeMaster:~/kube/calico# ping 10.42.253.137 PING 10.42.253.137 (10.42.253.137) 56(84) bytes of data. 64 bytes from 10.42.253.137: icmp_seq=1 ttl=60 time=33.7 ms 64 bytes from 10.42.253.137: icmp_seq=2 ttl=60 time=33.5 ms ^C --- 10.42.253.137 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1002ms rtt min/avg/max/mdev = 33.546/33.632/33.718/0.086 ms 调优 MTU 这一步其实是为了稳定性……? 测试下来发现虽然我的 ZeroTier MTU 是 1420,但是实际上包大小到达 1392 左右就会开始触发分片(可用 ping -M do -s <包大小> <Pod_IP> 测试),因此强制指定 Pod MTU 为 1370: root@KubeMaster:~/kube/calico# cat patch-mtu.yaml apiVersion: operator.tigera.io/v1 kind: Installation metadata: name: default spec: calicoNetwork: mtu: 1370 nodeAddressAutodetectionV4: firstFound: true root@KubeMaster:~/kube/calico# kubectl apply -f patch-mtu.yaml installation.operator.tigera.io/default configured
2026年04月05日
146 阅读
0 评论
6 点赞
1
2
...
8