文档状态:有效
适用范围:本文与当前模板同步,描述配置契约、校验和已实现运行时边界;未定义行为不应视为已支持。
最后核对:2026-09-21(DNS 缓存
expired状态与过期保留期定向核对,基于本次工作树;其余正文沿用原核对范围)核对基线:
d7296fd;本轮核对 P5 变更与联合验收,分批历史结果按原日期和基线解释2026-09-21 增量核对(本机日期):仅核对 DNS 缓存的 group ECS、Resolved fingerprint、stale refresh、late-result 语义与条目过期状态
expired;其余正文保留原历史核对范围增量核对基线:
e50b948edb4699f58a1e57039fb56442ab228687加本次缓存修复工作树;静态编译与前期定向测试证据见DNS 管线,未部署或实测 OpenWrt2026-10-04 增量核对:基线
eb428f92cdc3acda836547107ef2f56f6d988249加本次工作树,仅更新快照读取等待和策略表单覆盖保真;其他配置契约不变2026-10-04 负缓存增量核对:基线
e811b713292467ce964534c1cb7e2ef44002cdf6加本次工作树,仅核对dns.cache.negative_ttl_max、optimistic.negative_max_age、按响应质量区分的乐观窗口与缓存活动响应类观测;单元/策略层测试已覆盖,本地运行验收仍由负缓存新鲜度计划跟踪关联文档:后端架构
config/contract.rs 提供 ConfigV2::parse/validate/resolve_paths,ConfigV2Loader 已由 app::run_command 正式使用并直接生成 ResolvedConfig,不经过 v1 DTO 或 migration。根配置模板和启动夹具均为 v2;生产不提供 v1/v2 切换开关,不接受旧配置,也不转换旧配置或数据库。
| v2 字段/边界 | 契约与默认值 |
|---|---|
version |
只接受 2;其他版本在完整解析前拒绝,无转换 |
name |
各命名空间内唯一,1-128 个 ASCII 字符 [A-Za-z0-9_.!-];上游和组共享空间;不同空间可同名,DoH endpoint 名仅在所属 listener 内唯一 |
clients[].client_id |
必填、全体客户端唯一、大小写敏感,1-128 个 URL unreserved ASCII [A-Za-z0-9._~-];普通编辑可显式修改(缺省沿用原值),name 不派生 ID;修改后历史记录仍归属旧 ID |
clients[].match.ips |
缺省空数组;最多 256 项;地址转 /32 或 /128,CIDR 清除主机位,mapped IPv6 转 IPv4;规范化重复拒绝,包含关系允许 |
dns.cache |
缺失时全局池关闭,内存 64 MiB,failure TTL 5s,optimistic 关闭/answer TTL 10s/max age 1d;显式对象沿用必填字段规则 |
dns.cache.persistence |
缺失或空对象为 enabled: false、path: ./data/dns-cache.db、snapshot_interval: 5m;周期 1s-1d;删除旧 max_size_bytes |
dns.resolve_log |
缺失为关闭;显式对象只接受必填 enable,删除旧条数/年龄配额 |
statistics.retention |
缺失字段分别采用 days: 7、grace_days: 3、reference_size_bytes: 1073741824;days 为 1-3650,grace 非负且总和不超过 3650 |
| 字节预算 | 内存预算、参考大小为 1-1099511627776 bytes(1 TiB),可由 JavaScript 安全整数无损表达;参考大小不是磁盘硬配额 |
database.records_path |
必填;与统计库、快照一起沿两级路径基准解析,不增加在线搬库 |
| 解析/集合预算 | 配置最多 4 MiB;每命名空间最多 1024 项;每份内联 Hosts/规则文本最多 256 KiB(UTF-8 字节) |
| 覆盖/类型分支 | cache/TTL/ECS 缺失仍表示继承,显式禁用不等于缺失;显式 null、未知字段、旧 match.ids 和 variant 残留字段均拒绝 |
共享 listener、上游、策略、Hosts、规则集、代理、SecretRef 和认证字段继续复用 model;共享资源引用、循环、socket 冲突和语义复用 validate,没有另造旧版本转换层。名称和 ID 的请求期索引/历史事实属于 BC-04/05,P0 仅验证配置契约。
resolve_paths 是无 I/O 的词法检查:拒绝统计/快照相互碰撞、与详情目录及受保护配置/日志路径重叠,包括逻辑文件成为另一目标父路径的反向冲突,Windows 比较不区分大小写。物理路径由对应 owner 再检查;统计库要求 fluxdns_layout(kind=statistics-v2, layout_version=1),空文件可在首次事务初始化,任何已有未标记 schema 或错误标记均拒绝。详情分片继续由每个文件的 detail_meta 标记归属。
ResolvedClient 与 ClientRule 已把配置管理 name 和请求身份 client_ids 明确分离,ClientIndex 同时构建 name/exact ID 索引并拒绝重复 name、重复 ID、重复规范化 CIDR 和空 matcher。请求匹配仍固定为大小写敏感的 ID 优先、最长 CIDR 回退;IPv4-mapped IPv6 地址在 CIDR 匹配及客户端 cache digest 前归一化为 IPv4,因此与对应 IPv4 使用同一客户端池。
v2 配置边界只允许单个唯一 clients[].client_id,生产 resolver 将其直接编译为唯一请求身份;ResolvedClient 的复数容器是运行时内部形态,不提供 match.ids 配置入口。历史记录仍只保留请求发生时已经冻结的匹配事实,不补造或重匹配旧身份。
旧 ConfigLoader、v1 顶层/客户端/缓存/详情 DTO、迁移注册表和独立 v1 fixture 已删除。run、validate、显式内部 reload、首次用户候选和配置回归测试统一使用 ConfigV2Loader;通用资源 DTO、严格字段 parser 和安全快照继续复用。无物理来源的测试输入要求绝对 work.path,实际加载仍以源文件目录解析两级路径。内容比较使用的非加密 FNV-1a 移至 hash.rs,认证和受管文件身份继续使用 SHA-256。
Windows Rust 1.98.0 执行 cargo test --manifest-path backend/Cargo.toml -- --nocapture:838 项通过、3 项显式忽略。覆盖 v2 拒绝旧配置、源路径/快照、真实双文件 journal、启动/reload 和认证候选;该次结果不代表 P5 的完整性能或浏览器验收。
ConfigStore 的 active 子模块持有唯一 v2 活动源:生产启动把 loader 实际消费的原始正文及 runtime revision 交给 with_active_source,并要求源文件和派生快照一致。Management 复用该 active store;首次用户提交在同一 transaction gate 中校验 v2 候选、原子写入两份文件,并同步更新认证用户和活动文件事实。
config/edit.rs承接原 Management 的严格变更类型,Management 重用同一类型,无新增 HTTP 契约。旧 name 相对同一活动快照定位,单候选不重复编辑同一目标;交换名称按一次映射处理,不递归串联改名。上游/组共享空间。客户端更新的ClientEdit.client_id可选:缺省保留原值,显式提供时替换请求身份,格式与唯一性由整份候选的契约校验统一拒绝,因此同一候选内互换两个客户端的 ID 也合法。- 候选依次执行变更预算、活动源定向修改、已知 variant 引用更新、完整
ConfigV2::parse/validate、两级路径检查、CST 定位编辑及整树等价复核。覆盖 listener/DoH route、bootstrap、组成员/fallback、策略、Hosts、rule-set selector、proxy 和客户端策略;不对正文、路径、SecretRef 做字符串替换。资源首载、物理 owner 路径、socket 和进程 prepare 仍属于后续运行接线,不能把这一步视为可应用运行态。 source_edit只将发生变化的 typed 字段写回原始语法区域,未变 duration/IP 表达、缺失继承、注释和相对路径不展开为默认值。新增片段经 serializer 转义;支持块/flow、CRLF 和重复编辑。已知yaml-edit原地删除会破坏相邻嵌套字段,因此使用 CST 字节区间编辑并复读等价校验;锚点/别名/merge 等无法安全维护的表达明确拒绝,没有整份重序列化回退。observation有界读取源与派生文件,各自区分可读/缺失/不可读/超限,组合 token 包含内容指纹和物理文件身份;同内容替换仍产生不同 token。拒绝 hard link、symlink/reparse 路径;读取前后核对身份和元数据。Windows 使用真实 FileId,Unix 代码未在本次执行。此观测不提供 filesystem CAS,正式替换前仍须重新核对;BC-29 的写入身份与旁文件保护见下节。- 60 秒验证票据绑定调用者、候选、双 revision 与影响确认,只缓存 SHA-256 摘要;受管内容/观测 token 同样使用 SHA-256,不复用旧迁移 hash。随机验证票据仍为 256 bit,复用已有
sha2 0.10.9,没有依赖变更。操作记录上限 1024、完成后保留 30 分钟,进行中记录不淘汰。同 ID 不同命令拒绝,相同命令返回已有阶段;不同调用者的查询不披露结果。未同步、补偿失败或 permit 中断保持 gate,未知不被视为未执行。普通变更由同一 operation owner 连接 Runtime 与持久化;P3 单模块入口只是在 URL/单变更边界复用该事务。
BC-02 批次的 Windows 验证:cargo test --manifest-path backend/Cargo.toml --bin fluxdns config:: 71 项(含摘要加固后的 SHA-256 固定向量)、management:: 18 项通过,全部 Cargo 测试目标 --all-targets --no-run 编译通过;前端 typecheck、schema 3 项、Vitest 7 文件 38 项通过。Node/Vite 子进程在沙盒内被 spawn EPERM 阻止后,经批准在沙盒外重跑通过。新测试覆盖候选/引用/源表达和真实双文件观测;文件只在 _fluxdns/p1-config-tests/ 的随机用例目录创建并清理。该批未执行真实 v2 启动、HTTP、DNS 热更新、journal crash point、文件还原/同步重试、日志切换或浏览器验收;后续批次证据单列。当前 schema 和生成类型没有 wire 变化。
store/persistence.rs 与 files.rs 为 v2 普通配置建立独立于旧 setup journal 的分阶段事务。它们已由活动源状态机消费,不建立空存储 layout,不使用 ResolvedConfig 反写:
begin_runtime_apply在完整候选和版本核对后准备受限同目录 stage、写入 PREPARED journal,再复核两个文件;没有正式文件替换。应用成功回报只推进 active/runtime,persist_applied才写 COMMIT_DECIDED 决策并分别替换源和派生文件。成功更新 persisted revision、最终观测及操作状态;失败保持新活动源和未同步 gate,重试只执行文件操作。- 源与派生目标分别持有
File::try_lock的 OS 锁,冲突有界失败;空锁旁文件保留,进程退出释放锁,不按时间删除“过期锁”。每次替换核对固定角色、父目录身份、旧/新文件身份、SHA-256 和权限摘要;journal 不携带可任意指定的目标路径。源即派生文件时只处理一个目标。 - 测试内派生子进程的用例(panic 探针、日志 bootstrap、持久化崩溃矩阵)与
try_lock断言共用backend/src/test_support.rs的进程内读写窗口:Linux 的flock锁随 open file description 被fork复制,子进程在exec前会让并发用例看到假Busy;该窗口只在cfg(test)下编译,不改变生产语义。 - 恢复入口只用于新版启动前:PREPARED 清理已知候选、不 roll-forward;COMMIT_DECIDED 在两个目标均为已知旧/新身份与内容时,重新严格校验候选及派生路径,再补齐同一候选。损坏、超限、未知内容/身份、链接或范围不符都拒绝;不在运行进程中调用恢复自动 reload。journal 读取上限 16 KiB,配置/stage 上限 4 MiB。
- Windows 创建 stage/journal 时即复制 owner/group/DACL 并保护 DACL,拒绝空 DACL;不是先默认继承再写秘密。替换沿用
MoveFileExW(REPLACE_EXISTING | WRITE_THROUGH),文件先sync_all;实际 DACL 条目、FileId、hard link、junction、ADS 与尾点别名有测试。Unix 使用限制模式、owner 核对和目录 fsync,但未在本批执行;Windows 不声称做了 Unix 目录 fsync 或硬件断电验收。
Windows Rust 1.98.0 本批 config:: 89 项通过,其中 ConfigStore 28 项。新增子进程以退出码 73 在 PREPARED、COMMIT_DECIDED、源替换后、派生替换后直接退出,验证真实文件与 OS 锁恢复;另有真实 Windows journal/派生文件占用失败、同步重试、同内容换身份、损坏/超限 journal、单文件去重、权限和别名测试。测试数据限定 _fluxdns/p1-journal-tests/ 与 _fluxdns/p1-config-tests/,不读取个人配置。
当前正式 operation owner 已把 Runtime 成功回报接到 persist_applied,启动也在 loader 前执行 v2 recovery;文件 I/O 由阻塞 owner 调度,HTTP 断开不取消已受理操作。缺失文件还原及外部新修改后的明确确认/事务重建由固定文件路由消费;未确认新双文件版本时仍不能覆盖未知外改。崩溃在 journal 建立之前或决策旁文件替换之前可能留下未登记的受限旁文件;不扫描删除未知残留,需明确人工处理。非合作编辑器的核对/替换竞态不能被普通 filesystem replace 完全消除。
ConfigStore::restore_files 绑定调用者、operation ID、active/file 双 revision 和丢弃外改确认。只从 active_source 取原文,同时还原固定源与派生路径,不创建 Runtime、不推进 active/runtime revision,也不触碰 DNS、会话或 Hosts/规则资源文件。重复 operation 只返回既有阶段,不能因文件随后再变而重放还原;未知 ID/不同调用者和不同命令不共用结果。正常完成更新 persisted 状态与组合文件观测。
还原复用 PREPARED/COMMIT_DECIDED journal:确认还原已生效的活动源后才决定文件提交。准备失败且清理已确定完成时记为 Rejected;存在未核清的 journal/旁文件时保留阻塞。决定提交后发生 I/O 失败则保留活动源和 AppliedUnpersisted,禁止叠加变更;现有内部 persist_applied 只继续这次已知文件事务,不重新应用 Runtime。
目标仅叶节点缺失时,journal 的 old 明确记录缺失;重新创建必须使用进程在先前受管状态捕获的权限及父目录身份。权限能力不来自 HTTP 或 journal,也不在其中序列化;成功提交后更新进程内能力。Windows 使用不带 REPLACE_EXISTING 的同目录发布,拒绝确认缺失后重新出现的目标;现有目标仍核对身份、内容和权限后替换。目录、hard link、不可读、超限、缺失/被替换的父目录都拒绝,不删除未知文件或自动重建目录。Unix 沿用配置副本的 hard-link no-replace 方式;link/unlink 间中断留下的双链接将被现有防护拒绝,需人工核对,此路径未实机验收。
Windows 本批 ConfigStore 36 项、完整 config:: 97 项通过。新增还原确认/幂等/双版本冲突、真实文件占用失败和显式重试、单/双文件缺失、目录/硬链接拒绝、父目录替换、DACL 保留以及 no-replace 测试。子进程矩阵从 4 个扩至 8 个 crash point,增加缺失双文件的 PREPARED、COMMIT_DECIDED、源提交后、派生提交后;PREPARED 不补建目标,持久决策才补齐。用例数据仍在 _fluxdns/p1-config-tests/ 和 _fluxdns/p1-journal-tests/,递归清理前核对真实绝对路径归属。
该子项现已接入受 Bearer/Origin 保护的文件还原路由和阻塞 owner;UI 全局接线另见前端实现。新外改后的重新确认/事务重建、配置状态投影见下文;还原仍不改变 Runtime revision,不能把文件成功等同为重新应用 DNS。
retry_persistence 只继续当前 AppliedUnpersisted operation,核对原调用者和最新 active/file 双 revision;既有操作完成后再次请求只返回既有状态,不覆盖后来出现的外改。journal 已知的逐文件部分提交可以直接继续;遇到未知目标必须显式确认丢弃该次观测到的外改。活动源原文、active/runtime revision 和原 operation 保持不变,不接收替代候选,也不重新 prepare 或应用 DNS。
Persistence::reconfirm 对同一活动源摘要重新执行完整配置/路径校验,以受限权限准备新 stage,并在提交决策前再次核对组合文件版本、目录及文件身份。新 COMMIT_DECIDED journal 原子替换旧 journal 后才能按新决策覆盖所见外改;决策未成功持久化时保持旧决策,不先删除旧 journal。缺失叶节点继续使用受管权限能力;不放行不可读、超限、链接或被替换的父目录。
新 journal 最多登记 3 个待回收旧旁文件,只包含固定源/派生/决策角色、旧随机标识和完整身份摘要,不包含任意路径。恢复拒绝重复条目、当前候选标识、非法随机标识和无派生目标的派生角色;旧旁文件被外部改写时拒绝清理,不用名称猜测归属。连续重新确认先清理已知旧项,不无限累积清单。新 journal 替换失败且旧 journal 仍可验证时,只清理本次已知新旁文件;结果不能确认时保留现场和 Unknown。未核清旁文件记为 CompensationFailed,二者均禁止普通重试或叠加变更,等待 owner 核对。
Windows Rust 1.98.0 本批 config:: 104 项通过,其中 ConfigStore 43 项。独立子进程退出矩阵扩至 12 个检查点,新增外改重新确认前、新决策持久化后、源替换后和派生替换后;新决策前保持未知外改,新决策后仅完成已确认候选。另有真实 journal 文件占用、调用者/双版本冲突、重复重试不覆盖后续外改、旁文件篡改与连续确认清单上界测试。cargo check、全部测试目标 --all-targets --no-run 和 fmt 检查通过;未执行断电、跨平台或正式 v2 HTTP 联合验收。
正式 /api/v2/config/files/retry 已只推进原 operation 的文件事务,认证、阻塞 owner 和启动恢复均已接线;HTTP 结果未知仍必须查询 operation,不能自动重放应用或还原。
ConfigStore 的操作记录保存 OperationSnapshot,在每次状态转移时冻结对应 active/persisted revision 和白名单失败类别;后续其他操作、外部文件变化不改写旧结果。应用成功回报进入 Persisting,实际文件提交失败才进入 AppliedUnpersisted。已确认运行版本的清理失败保留该版本;运行补偿结果不明则不编造 active revision。完成或已完整补偿的拒绝重新起算 30 分钟保留期,未同步/未知记录继续由当前 gate 固定。
configuration_status、operation_snapshot 与 active_snapshot 使用同一 active 锁内的缓存快照,不执行文件 I/O。2026-10-04 起,read_active 以 5 ms 间隔尝试获取锁,最多等待 1 秒;超时或锁中毒返回 Unavailable,Management 映射为 SERVICE_UNAVAILABLE。正式 async 读取通过 spawn_blocking 调度,等待任务自身有截止时间;写事务 gate 仍保持非阻塞冲突和原有版本保护。白名单映射与接线见Management 实现。
每个文件的已知状态来自上次持久化身份/摘要,或当前 journal 明确绑定的候选身份/摘要。部分提交只将精确匹配的那一个目标识别为自写,不用“忽略下一次事件”或仅内容相同放行。同步成功后以 journal 候选身份建立基线,随后读到的未知外改仍显示 changed;同内容换 FileId 也要求外改确认。synchronization: synced 表示上次受管提交完成,外部变化独立通过 files 表示,不等于磁盘当前仍与活动源一致。
本批新增保留期、进行中固定、状态查询不阻塞及冻结结果类别的定向测试;Windows 实际文件投影覆盖部分提交、重试、同内容换身份、缺失、超限和不可读。2026-09-08 又以真实 v2 进程、Bearer HTTP、UDP、SQLite 和双文件验证了 Runtime 应用及文件同步,具体联合证据见下节。
active/external.rs 的 ConfigStore::external_source 只读创建 store 时固定的源文件及派生副本。源正文与 FileId/SHA-256 来自同一次 4 MiB 有界读取;随后执行完整 ConfigV2::parse 语义/引用校验和无 I/O 的 resolve_paths。派生副本仅参与观测,不成为候选来源;不读取资源、证书或 SecretRef 实际值。
返回前再次观测双文件并核对活动 revision,期间内容或身份改变返回 FileConflict,活动发布交错返回 ActiveConflict;不把旧活动值配上新版本。缺失、不可读、超限、旧版本和无效配置只返回白名单错误类别。预览更新缓存观测,但不创建操作、候选或文件,不应用 Runtime、不改变 active/persisted revision。
该同步入口由阻塞 owner 调度,不直接占用 HTTP executor,也不构成文件系统 CAS。Windows 定向测试覆盖双文件内容/同内容换身份、活动发布交错、事务忙及无副作用;类型化 HTTP DTO 投影与输出预算见差异投影。
ConfigMutationOwner 以同一 Arc<ConfigStore> 和 ServiceControl 为权威。校验和 ConfigStore 文件阶段在 spawn_blocking 执行;已受理 apply 立即保留可查询 operation,随后独立于 HTTP 请求完成 resolve_config_v2、SecretRef 检查、远程资源/Policy prepare、PREPARED 文件阶段、service 应用回执、活动源发布及持久化。回执丢失或任务中断保留 Unknown,不会自动重放;只有 LoggingCompensation 明确标记补偿失败,其余发布前拒绝清理候选并释放 gate。
正式 app 在构造并挂接日志 owner 后把 service.control() 注入 Management,再注册 HTTP task。稳定双文件 watcher 会把已完成的 ManagedObservation 写入 ConfigStore;Store 忙时保留该事实供下一轮投递。外改仍不调用 Runtime。启动顺序继续先恢复 setup/v2 journal,再由 loader 建立活动源;PREPARED 只清理,COMMIT_DECIDED 只补齐已知候选。
Windows 真实联合验证使用 _fluxdns/p1-config-runtime-live/ 的 v2 源文件、派生文件、SQLite 和 loopback UDP/HTTP。日志父目录缺失时 operation 返回 APPLY_FAILED,Runtime revision 保持 1、双文件未变;创建目录后相同 logs 候选完成 applied_synced,Runtime revision 变为 2,源/派生一致,debug 日志写入新路径,前后 UDP localhost A 均返回 127.0.0.1,SQLite fluxdns_layout 为 statistics-v2|1。外改源文件后 watcher 报 changed 且 Runtime 不变,差异投影返回 logs,文件还原与已同步重试均不重新应用 Runtime。进程停止并从持久化源重启后继续读取 debug 和 ./logs/hot.log。配置定向测试 115 项通过,包含真实 Windows 子进程 crash matrix;未验证 Unix、磁盘满、OS I/O 强制中断或 P3 模块表单写入。
正式入口是 app::run_command -> ConfigV2Loader::load_from_path -> contract / resolve / validate。loader 限制配置为 4 MiB 并保留产生运行态的原始正文;run 在 prepare 前恢复已知 setup/v2 文件事务、创建或核对 <work.path>/config.yaml,再校验 SecretRef。validate 不写 snapshot,也不做资源、数据库或网络可用性检查。
| 能力 | 代码实现 | 正式入口接线 | 验证证据 | 已知限制 |
|---|---|---|---|---|
| 严格配置与路径 | ConfigV2Loader、ConfigV2、ResolvedConfig | run/validate/reload/首用户候选共用 v2 加载 | loader、根模板、路径与旧版本拒绝测试;2026-09-09 全量 Cargo | 无旧版本转换 |
| 资源与缓存 | async PreparedRuntime、PolicyDnsCore | run 加载后 prepare | 本轮核对生产接线 | 能解析不表示网络/文件/SQLite 已成功打开 |
| WebUI | webui model + ManagementService | enable 时创建服务 | P3 内嵌 debug binary 的真实 Bearer HTTP 与浏览器 | origin、DB 与 bind 必须可用;默认 binary 可仅提供 API,SPA 需要 embed feature,详见管理端 |
| 首用户写回 | active ConfigStore + source-preserving editor | setup,run 前恢复 journal | v2 active source 双文件提交测试 | HTTP 断连与磁盘故障组合仍需更高层验收 |
| 热重载 | ConfigMutationOwner + ServiceControl + reload_prepared |
组合及 P3 十模块 apply;watcher 只更新文件事实 | 真实 Bearer HTTP、UDP、SQLite、日志、双文件和组合采用联合验证 | database、webui enable/address/port/public_origin 仍为启动级;跨平台/磁盘满未验证 |
本表原始静态核对未执行 Cargo 或配置 validate,后续 P1 定向验证以上述分批记录为准。协议/策略字段定义不自动意味着所有 adapter 组合已验收,真实入口和未支持项见后端实现,验证专项的已知边界见验证范围与收口。既定契约与代码冲突时须保留复现证据并明确修复边界,不仅修改字段说明来掩盖实现缺口。
FluxDNS 的配置由命名资源和引用关系组成。请求入口选择策略,策略按顺序匹配规则,再决定使用本地 hosts 或远程上游:
listener
├─ udp/tcp → strategy
└─ doh → routes → strategy
├─ hosts:本地回答
└─ rule_set → upstream / group → outbound
listener定义请求入口。一个普通 listener 可以展开为多个 IPv4/IPv6 socket。- 一个
type: dohlistener 是一个逻辑 DoH 服务,可通过多个endpoints同时提供不同绑定方式。 routes在同一个 DoH listener 内共享;每条路由选择一个策略。clients按客户端标识或 IP 网段选择策略,并覆盖缓存、TTL 和 ECS 等设置。upstreams、hosts、rule_set和outbound是可复用的命名资源。webui定义独立的 WebUI Management Server、浏览器 origin 和登录用户。
顶层 version 是配置 schema 版本。当前模板和生产加载器只接受 version: 2;其他版本直接拒绝并提示使用新的开发目录,不迁移或按旧字段猜测。
配置路径采用两级基准解析,不能直接把所有相对路径都拼到进程当前工作目录:
- 先确定启动配置文件的绝对路径。命令行传入的配置文件路径若为相对路径,仅在这一步相对于进程启动时的当前工作目录解析;其父目录记为
config_dir。 - 解析
work.path:绝对值直接使用;相对值按config_dir.join(work.path)解析。结果经词法归一化后形成绝对的resolved_work_path。 - 解析项目路径字段:绝对值直接使用;相对值统一按
resolved_work_path.join(field_path)解析并做词法归一化,不能再回到config_dir或进程当前工作目录。 work.rules_path、database.path、database.records_path、logs.path、dns.cache.persistence.path、TLS 证书和私钥、SecretRef 文件、本地 hosts 与规则文件路径都遵循第 3 步。
词法归一化应消除 . 和可消解的 ..,但不依赖目标文件或目录已经存在。通过 bytes/string 加载且没有配置文件来源路径时,不存在 config_dir;此时相对 work.path 必须报错,调用方需要提供来源路径或改用绝对 work.path,不得隐式回退到进程当前工作目录。
| 启动配置文件 | 配置值 | 解析结果 |
|---|---|---|
/opt/_fluxdns/config.yaml |
work.path: ./ |
resolved_work_path = /opt/_fluxdns |
/opt/_fluxdns/config.yaml |
work.path: ./、database.path: ./data/fluxdns.sqlite3 |
database.path = /opt/_fluxdns/data/fluxdns.sqlite3 |
/etc/fluxdns/bootstrap/config.yaml |
work.path: ../runtime、logs.path: ./logs/fluxdns.log |
logs.path = /etc/fluxdns/runtime/logs/fluxdns.log |
/etc/fluxdns/config.yaml |
work.path: /var/lib/fluxdns、database.path: /srv/fluxdns.sqlite3 |
两个绝对路径均不与其他基准拼接 |
启动时若配置文件本身不是 <resolved_work_path>/config.yaml,程序将其原子复制到该固定路径;仅仅位于同一目录但文件名不同仍会创建快照。该文件是工作目录中的配置快照;启动流程负责创建工作目录和所需父目录。
配置副本按 Config 模块方案 原子创建:目标不存在时在同目录写临时文件并以 no-replace 方式发布,内容相同则不操作,目标已存在且内容不同时拒绝自动覆盖。SecretRef 解析值不会写回配置副本。
listener、upstreams、strategy、hosts、outbound 和 rule_set 中的 name 是资源标识,同一集合内必须唯一。引用字段必须指向对应集合中存在且类型正确的名称。
| 引用字段 | 目标集合或语法 |
|---|---|
listener[].strategy、clients[].strategy、listener[type=doh].routes[].strategy |
strategy[].name |
listener[].hosts、strategy[].rules[].hosts |
hosts[].name |
strategy[].rules[].rule_set |
rule_set[].name,或 geosite:cn 这类规则集子选择器 |
strategy[].rules[].upstream、strategy[].default_upstream |
upstreams[].name |
upstreams[].bootstrap |
upstreams[].name |
upstreams[type=group].upstreams[].name、fallbacks[].name |
upstreams[].name |
upstreams[].proxy、rule_set[].proxy |
outbound[].name |
应在加载阶段检查名称唯一性、引用存在性、引用类型和循环引用。
| 数据类型 | 模板示例 | 说明 |
|---|---|---|
| 时长 | 10s、24h、1d |
用于缓存、超时和定时更新;解析为 duration,而不是整数天数或秒数。 |
| 字节数 | 8388608 |
max_size_bytes 使用整数,单位为字节。 |
| IP/CIDR | 192.168.1.0/24、fe80::/10 |
用于监听绑定、客户端匹配、ECS 和可信代理。 |
| URL | https://dns.google/dns-query |
必须按字段要求使用合法 URL。 |
| SecretRef | {env: FLUXDNS_OUTBOUND_SG_URL} |
从环境变量、文件或内联 url 取得完整敏感值;内联形式会把真实凭据写入配置文件,需自行承担。 |
outbound[].proxy_url 接受三种 SecretRef 形式,三选一:对象 {env: NAME}、对象 {file: ./secrets/proxy.txt},或直接写完整 URL 字符串 / 对象 {url: "socks5://user:password@host:1080"}。内联形式读取结果即该 URL;引用形式读取结果为环境变量或文件内容。三种形式读取后都必须是完整代理 URL。
配置块未出现时继承较低优先级的设置;块中只出现的字段按字段继承。显式的禁用值用于停止继承。
当多个层级提供 edns_client_subnet 时,优先级由高到低为:
strategy.rules[].edns_client_subnet
> strategy[].edns_client_subnet
> clients[].edns_client_subnet
> upstreams[].edns_client_subnet
> dns.edns_client_subnet
mode 的取值为 disabled、client、custom:
disabled显式停止当前请求继续向上游传递 ECS;client优先使用并规范化请求携带的合法 ECS;没有时按客户端地址推导 IPv4/24或 IPv6/56前缀,避免向上游传递完整客户端地址;custom使用custom_ip,因此custom_ip必填。
当策略目标是 group 时,rule/strategy/client 的显式 ECS 继续覆盖所有成员。只有请求级结果来自 global/default 时,运行时才遍历 group 的全部可达 direct leaves(含 nested group、fallback,以及未显式配置而继承请求级 ECS 的成员),为每个 leaf 计算并规范化最终 ECS query。所有成员 query 相同时,group 使用这一统一 query 并允许 Resolved response cache;即使成员显式 ECS 不同于 global,只要所有成员最终一致也可缓存。只有最终 query 真正异构时才为成员保留独立 query,并绕过 cache lookup、single-flight 和 commit。Fast eligibility 仍保守:策略可达 target 只要存在显式成员 ECS 就不使用 Fast,后续完整决策仍可进入 Resolved。
缓存和 TTL 覆写是两个独立配置:
clients[].cache > strategy[].cache > dns.cache
clients[].ttl_override > strategy[].ttl_override > dns.ttl_override
ttl_override.enabled: false 显式关闭当前层级的 TTL 覆写;它不等价于关闭缓存。ttl_override.min 或 max 为 0s 表示该边界不设限。
缓存不是逐层查询的 fallback 链。每个请求只选择一个逻辑缓存池:客户端显式启用时选择“实际客户端身份 + 生效策略”池;否则策略显式启用时选择策略池;策略未配置 cache 时才选择全局池。策略或客户端显式 enabled: false 会终止选择,不再回退到全局池。详见 dns.cache。
生产加载不直接把 YAML DTO 当作运行时配置,也不执行旧版本迁移:
ConfigV2
→ 拒绝非 version: 2
→ normalize 路径、SecretRef、默认值和继承
→ semantic validate
→ ResolvedConfig
- v1 顶层 DTO、迁移注册表和测试加载路径已删除;测试复用正式 v2 配置。
- 缺失、显式
null、空数组和空对象的区别由严格 v2 parser 保留;cache/TTL/ECS 继承和来源信息在ResolvedConfig阶段一次性确定。 validate不写入原始配置或快照;不提供旧配置迁移命令。- 配置 schema 版本、SQLite schema 版本、资源 parser/compiler 版本和 cache key format 版本彼此独立;升级一个版本不能隐式宣称其他版本兼容。
- 运行时升级沿用
prepare candidate → preflight → atomic activate/keep old:可热更新项替换RuntimeSnapshot,需要重新绑定的项 drain 后切换,无法安全切换的项拒绝候选并保留旧运行时。
| 字段 | 类型 | 说明 |
|---|---|---|
version |
integer | 配置 schema 版本;当前必须为 1。 |
work |
object | 工作目录和规则资源落盘目录。 |
database |
object | 默认开启的聚合统计和其他主库存储能力;始终必填。P2 详情生产写入已分离到日分片。 |
logs |
object | 服务日志输出。 |
webui |
object | WebUI Management Server、浏览器 origin 和登录用户。 |
dns |
object | 全局缓存、TTL、ECS 和解析日志默认值。 |
listener |
array | UDP、TCP 和 DoH 请求入口。 |
upstreams |
array | hosts 上游、DoH 上游和上游组。 |
strategy |
array | 策略和有序路由规则。 |
hosts |
array | 可被入口或策略复用的本地 hosts 资源。 |
outbound |
array | 远程上游或规则下载使用的代理出口。 |
rule_set |
array | 可被策略引用的规则资源。 |
clients |
array | 客户端匹配和客户端级覆盖。 |
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
work.path |
string | 必填 | 工作目录;绝对路径直接使用,相对路径以启动配置文件所在目录为基准。 |
work.rules_path |
string | 必填 | 规则资源落盘目录;相对路径以 work.path 为基准。 |
work.rule_set_max_size_bytes |
integer | 可选 | 每个规则资源读取、下载和解析的大小上限,单位为字节;必须大于 0,缺失时默认为 16777216(16 MiB)。 |
work.rule_set_max_rules |
integer | 可选 | 每个规则资源允许索引的规则数量上限;必须大于 0,缺失时默认为 131072。 |
启动时,应先得到绝对的 resolved_work_path 并确保目录存在;如果配置文件不在该目录中,再将其复制为 <resolved_work_path>/config.yaml。数据库、日志、缓存和证书等其他相对路径均以 resolved_work_path 为基准。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
database.type |
string | 必填 | 当前模板仅定义 sqlite。未知类型应拒绝。聚合统计默认开启,因此不能省略。 |
database.path |
string | 必填 | 带 v2 布局标记的统计 SQLite 文件路径;相对路径以 work.path 为基准。 |
database.records_path |
string | 必填 | UTC 日分片目录;必须与统计库、缓存快照及受保护文件隔离。 |
数据库在 prepare 阶段必须完成 v2 布局、当前 schema 和基本写入检查;失败时拒绝启动。空统计文件在同一事务直接创建当前 schema 9 和布局标记,重启校验并重开;未标记库、错误标记、其他 schema 或旧单库路径均拒绝,不升级、搬迁或清空。运行中数据库暂时不可写时,DNS 继续服务,由统计 writer 保留进程内补偿计数并重试;未恢复前进程退出可能造成 persistence gap,该状态必须可观测。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
logs.enable |
boolean | 必填 | 是否启用服务日志。 |
logs.level |
string | 必填 | 日志级别;v1 接受 trace、debug、info、warn、error,大小写归一化,未知值拒绝。 |
logs.path |
string | 必填 | 日志文件路径;相对路径以 work.path 为基准。 |
正式 app 关闭日志时仍保持 telemetry 指标/health。enable/level/path 经已有 service 日志 owner 显式热切换;路径预开失败不切 Runtime 或旧输出。当前 v2 表单保存和文件持久化联合接线仍待完成,具体边界见日志热切换。
webui 控制独立于 DoH 的 Management Server。FluxDNS 在该端口只提供 HTTP,不实现 TLS 终止,也不根据 X-Forwarded-* 推断浏览器 origin;公网部署应将监听限制在 loopback 或受保护的内网地址,并由 Nginx 等可信反向代理提供外部 HTTPS。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
webui.enable |
boolean | 必填 | true 启动 Management Server;false 不创建 management listener 或认证状态。 |
webui.address |
string | 必填 | HTTP Management Server 的监听地址;与 DNS TCP/DoH endpoint 统一检查端口冲突。 |
webui.port |
integer | 必填 | HTTP Management Server 的监听端口,范围为 1..=65535。 |
webui.public_origin |
absolute URL | enable: true 时必填 |
浏览器实际访问的唯一 http 或 https origin;不得包含凭据、path、query 或 fragment,用于严格 Origin 校验和 Cookie 策略。 |
webui.users |
array | 可选 | 省略或 [] 表示需要一次性初始化;非空表示已可登录;显式 null 拒绝。 |
webui.users[].name |
string | 必填 | 用户名;列表内必须唯一。 |
webui.users[].password_hash |
string | 必填 | 初始化时由一次性明文密码生成的单向 hash。 |
配置仍拒绝未知字段、重复用户名、非法 hash 和明文 webui.users[].password。首次初始化只把 Argon2id PHC hash 写入源配置和工作目录快照;真实密码和 hash 不应提交到公开仓库。
public_origin 使用 HTTP 时,session Cookie 不设置 Secure,因此凭据与 session 的传输安全完全依赖管理网络;这只适合 loopback 或明确受信任的隔离网络。常规部署应由反向代理提供 HTTPS,并把 public_origin 配置成浏览器看到的 https://... origin,此时后端仍只监听 HTTP。
cache 描述全局池开关、共享内存容量、短期失败 TTL、乐观缓存和独立二进制快照;TTL 覆写不再嵌套在其中。
| 字段 | 类型 | 说明 |
|---|---|---|
dns.cache.enabled |
boolean | 是否启用全局缓存池;它不是策略池和客户端池的总开关。 |
dns.cache.memory.max_size_bytes |
integer | 所有逻辑缓存池共享的内存计费容量上限,单位为字节。 |
dns.cache.failure_ttl |
duration | SERVFAIL、上游截断响应等无自然可用 TTL 的短期缓存时间;必须在 1s..=5m。 |
dns.cache.negative_ttl_max |
duration | NOERROR/NODATA 与 NXDOMAIN 的缓存寿命及客户端可见 TTL 上限;可省略,默认 5m,必须在 1s..=1d。只向下压,不影响正应答和失败类。 |
dns.cache.optimistic.enabled |
boolean | 是否允许返回已过期记录并在后台刷新。 |
dns.cache.optimistic.answer_ttl |
duration | 乐观缓存应答使用的 TTL。 |
dns.cache.optimistic.max_age |
duration | 完整正应答过期后仍可乐观返回的最长时间。 |
dns.cache.optimistic.negative_max_age |
duration | NODATA/NXDOMAIN 过期后仍可乐观返回的最长时间;可省略,默认 5m,0s 表示负应答过期即回源。解析时取 min(negative_max_age, max_age),超出不报错。 |
dns.cache.persistence.enabled |
boolean | 是否启用进程级 FDCS 快照;不等同于全局内存缓存开关。 |
dns.cache.persistence.path |
string | 当前进程级 FDCS 快照文件路径;相对路径以 work.path 为基准。 |
dns.cache.persistence.snapshot_interval |
duration | 完整快照周期,范围 1s..=1d。 |
memory.max_size_bytes 是缓存条目按 key、DNS wire 和元数据计算后的容量预算,不承诺等于进程 RSS。FDCS 文件没有用户可配的磁盘配额,也不承诺文件大小等于 Moka weight;reader/writer 使用内部 1 GiB 文件上限和记录预算。旧 persistence.max_size_bytes 在 v2 中是未知字段。
persistence.enabled 开启时,正式 app 在 bind 前由唯一进程 owner 从 FDCS 分批预热活动 Moka,随后按 snapshot_interval 覆盖完整可见集合;缺失、损坏、不兼容、超时和内存预算不足只形成冷启/部分恢复状态。内存 CAS 不等待磁盘,也不再排入逐条 SQLite persistence。reload 只切换 owner generation/source,不恢复候选;shutdown 在 finalizer 排空后 best-effort 写最终快照。详见Cache 设计与后台服务。
策略级和客户端级 cache 只允许 enabled 与 optimistic 子对象,不包含 memory、failure_ttl 或 persistence。只要出现策略级或客户端级 cache 对象,enabled 就必须显式提供;整个对象缺失才表示继续向较低优先级选择。
当前有三类逻辑缓存池,并共享 dns.cache.memory 容量预算与快照 owner:
- 全局池:namespace 为
global; - 策略池:namespace 包含生效的
strategy.name; - 客户端 + 策略池:namespace 包含实际客户端身份和生效的
strategy.name。客户端身份优先使用请求的精确client_id,没有时使用规范化源 IP;不能只使用匹配规则的clients[].name。
每个请求按以下顺序选择且只使用一个池,cache miss 时不跨池继续查找:
clients[].cache.enabled == false → 不使用缓存
clients[].cache.enabled == true → 客户端 + 策略池
clients[].cache 整块缺失 → 继续检查策略
strategy[].cache.enabled == false → 不使用缓存
strategy[].cache.enabled == true → 策略池
strategy[].cache 整块缺失 → dns.cache.enabled 为 true 时使用全局池,否则不缓存
cache key format v2 显式区分逐规则匹配前的 Fast 模式和完整决策后的 Resolved 模式,二者不能 alias。除 pool namespace 与 canonical query 外,key 包含 transport compatibility,以及 opaque 的 policy/request/target/ECS fingerprint;不包含 DNS ID、原始 client 地址、整个配置 revision 或资源 generation。Fast 与 Resolved 的 policy 维度都使用 cache_semantics_fingerprint,覆盖会改变答案的已解析配置、当前 PolicyState 中全部 hosts/rule-set content hash 和请求上下文;Resolved 另加入最终 target/ECS,不再只用 strategy ID 摘要。请求 fingerprint 对有效请求 ECS 使用规范化网段;没有 ECS 时只使用脱敏的客户端 /24(IPv4)或 /56(IPv6)网段。
规则或 hosts 的变化不会扫描或清空缓存,但会改变 policy fingerprint,使后续请求不再命中旧语义 entry;旧 entry 继续占用容量并按自身 TTL/optimistic 生命周期淘汰。请求先按最新 runtime snapshot 计算客户端、生效策略、ECS、namespace 和 policy/request fingerprint,fast miss 后才执行 rules、hosts 和 upstream target 决策。策略可达 target 存在显式成员 ECS 时 Fast eligibility 保守禁用;完整决策后若全部 direct leaf 的最终 query 相同,仍可使用 Resolved key,只有真正异构时才绕过 response cache。hosts[] 的 listener/strategy 本地回答直接使用当前资源 snapshot 并绕过 response cache;upstreams[type=hosts] 属于 upstream connector,其结果按普通上游响应处理。启用 optimistic cache 时,Fast/Resolved stale 都按最新 Policy/资源重新执行 route、ECS 与上游选择,不能沿用旧 entry 的目标;失败不会延长旧 stale。后续 WebUI 的清除缓存功能属于显式 namespace/key/predicate 操作,不由普通资源刷新隐式触发。
本轮仍使用 cache key format v2 和现有 FDCS snapshot format,不需要文件格式升级。Resolved 的 policy fingerprint 已扩充;Fast/Resolved 共用的语义摘要还显式纳入完整 DoH endpoint(包括 path/query),避免 SafeUrl 的脱敏 Debug 隐去实际分流差异。URL 明文只在计算摘要时读取,不进入 cache key 或日志。升级前的相关条目仍可按原格式解码,但新请求生成不同 key,会出现一次自然冷启动,旧项随后按 TTL/容量淘汰。
policy fingerprint 只保证实现纳入语义摘要的相关变化切换 key;它不是通用配置 revision,也不替代显式清理接口。真正旧格式的 key 仍不能与 v2 恢复记录混用,持久化 adapter 将其按不兼容记录隔离。
hosts[]产生的本地回答不进入 response cache;它直接读取已编译内存 snapshot,并仍应用 TTL override 和统计。upstreams[type=hosts]不属于此例外。- 正常
NOERROR响应按 RR TTL 缓存;NOERROR/NODATA和NXDOMAIN按负缓存 TTL 缓存,优先使用 SOA TTL 与 SOA.MINIMUM 的较小值。为满足 v1“所有 NODATA/NXDOMAIN 均缓存”的产品语义,没有可用 SOA/负 TTL 时使用failure_ttl;这是对 RFC 2308 “SHOULD NOT cache”建议的有意偏离。负缓存 TTL 最终再截到negative_ttl_max,避免 SOA.MINIMUM 很大时新增记录在 FluxDNS 层长期不可见。 - 负应答的客户端可见 TTL 同样受限:上游直出时先把 SOA/负 TTL 截到
negative_ttl_max,fresh 缓存命中时截到min(条目剩余寿命, negative_ttl_max),之后才应用 TTL override;因此 override 的min仍可能把 TTL 重新放大。缓存中的 origin response 不改写。answer 段只有 CNAME 链的响应按正应答处理,不受该上限影响。 SERVFAIL和直接从上游收到的TC=1响应使用failure_ttl;REFUSED可作为上游组的终态响应返回,但 v1 不写缓存。上游 TC 条目额外包含当前入口 transport,只允许相同 transport 命中,不能把 UDP 场景的截断结果复用于 TCP/DoH。- malformed DNS、问题段不匹配、连接/TLS/HTTP 失败和超时不属于 DNS 响应,不写缓存。
- 缓存保存不含客户端 DNS ID 和传输 envelope 的 canonical response。若本地 UDP 输出因本次客户端 advertised size 而截断,应保存完整 canonical response,并在每次发送时重新编码;只有上游本身返回的
TC=1才保存截断条目。 - 写入按响应质量做 compare-and-replace:完整
NOERROR/TC=0可以提升并替换未过期的 NXDOMAIN/SERVFAIL/TC 条目,SERVFAIL/TC 不能覆盖未过期的完整回答;同质量条目在过期前不因后到竞态反复覆盖。 所以快照恢复的旧长窗口负条目也按当前规则回源,无需迁移快照格式;这类旧条目已写入的 fresh 寿命不回溯缩短,但客户端可见 TTL 仍按上限截断。 - 条目已过期且不可乐观返回时,本次解析按
expired记录并照常回源;该状态与「缓存从未存在该条目」分开保存,便于区分回源原因。 - 同一 key 的并发 miss 通过 single-flight 合并。leader 得到可缓存结果后先形成持有 lease 的
CacheCommitCandidate并返回共享响应;后台 worker 使用独立 100ms deadline 完成 admission/CAS 并唤醒 waiter,进程快照由独立周期 owner 覆盖。客户端响应不等待 commit;candidate 被队列丢弃、取消或直接 drop 时,RAII lease 必须发布失败终态,不能永久挂住 follower。optimistic stale 由一次性 refresh permit 去重并进入统一后台刷新流程,不复用 miss 的 lease。
ttl_override 与 cache 平级,用于限制返回给客户端的 DNS TTL:
| 字段 | 类型 | 说明 |
|---|---|---|
dns.ttl_override.enabled |
boolean | 是否启用当前层级的 TTL 覆写。 |
dns.ttl_override.min |
duration | 最小 TTL;0s 表示不设下限。 |
dns.ttl_override.max |
duration | 最大 TTL;0s 表示不设上限。 |
策略和客户端可使用同样的字段结构。省略字段按层级继承,显式 enabled: false 停止当前层级的 TTL 覆写。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
mode |
enum | 必填 | disabled、client 或 custom。 |
custom_ip |
CIDR | mode: custom 时必填 |
要发送给上游的 ECS 网段。 |
全局 ECS 设置可由上游、客户端、策略和规则覆盖,详见覆盖和继承。
| 字段 | 类型 | 说明 |
|---|---|---|
enable |
boolean | 是否记录每次解析请求的详情(请求、策略、规则、ECS、缓存、上游结果和耗时等)。关闭时不写详情表,但不关闭聚合统计。 |
v2 只接受 enable;旧条数/年龄配额是未知字段。请求任务只向统一 resolution ingress 附带 typed question 和共享 response,qname digest、canonical qname 与 answer JSON 在后台 detail projector 中生成,再进入唯一有界详情 channel;满批立即提交,低流量尾批最多等待 5 秒。worker 按事件 UTC 日写入 YYYY-MM-DD.sqlite3,批写只执行校验与 INSERT,不执行历史 COUNT、按条数/年龄 DELETE 或 VACUUM。projection/SQLite 队列满或分片提交失败时丢弃当前详情并计量,DNS 请求不得等待或失败。
详情中的 duration_ms 从 transport 接入计时点计到 DNS core 完成,dns_core_duration_ms 只计算 DnsCore::resolve_with_completion 主链;两者均在主链返回时冻结,不包含后台观测排队和 SQLite 写入。DoH 的 duration_ms 包含入站 TLS/HTTP 处理,但不包含响应编码和网络写回。schema v5 之前的历史记录无法回填主链耗时,Management API 返回 null。
业务 schema v6 将数据库创建/迁移、批次提交和解析发生时间统一为带 _utc_millis 单位的 INTEGER Unix UTC 毫秒列;配置没有新增单位开关,API 时间格式与两种耗时精度不变。字段、前向迁移及失败边界见业务时间存储。
聚合统计默认开启且始终依赖 database:至少按 UTC 自然日记录总请求数,并按有界的 client bucket、transport class、strategy、source/upstream、RCODE 和 cache lookup status 记录分项计数。未匹配客户端统一进入 unknown bucket;不能使用域名、完整客户端 ID 或原始 IP 作为无界维度。请求任务只做一次有界 ResolutionEnvelope::try_publish,后台 dispatcher 更新进程内 sharded counters,stats writer 周期性以带 batch_id 的 checkpoint 批量 upsert 到 SQLite,并用 batch ledger 幂等去重。detail 下游溢出不影响聚合统计;但统一 ingress 溢出会同时丢失该事件的 stats/detail/cache commit,因此必须累计 dropped 并冻结首次 gap_started_at_utc_millis。一次请求只计一次 total_requests;cache/hosts 命中归入本地 source,parallel 的多个上游尝试不重复计请求。统计不能因为详情记录被丢弃或 enable: false 而停止。数据库运行中暂时不可写时继续维护进程内计数并报告 degraded/persistence gap;启动阶段数据库不可用则拒绝启动。
“依赖数据库”表示统计依赖 database.path,详情依赖独立受管日分片;请求线程不同步等待任一 SQLite。Management overview 的 resolution_pipeline 暴露 ingress accepted/dropped/首次 gap 时间、cache commit 各终态以及 detail accepted/dropped/failed。详情仍是有界内存队列后的 best-effort 写入,不因取消条数上限变成无损审计;若需要无损语义仍须另行定义 spool、背压和磁盘配额。
一个 listener 是一个逻辑服务。addresses 中的每个地址由运行时展开为独立 socket;同一个逻辑 listener 的地址共享协议和端口。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
name |
string | 必填 | 入口唯一名称。 |
type |
enum | 必填 | udp 或 tcp。 |
addresses |
array[string] | 必填 | IPv4/IPv6 绑定地址列表,例如 [0.0.0.0, "::"]。 |
port |
integer | 必填 | 所有地址共用的监听端口。 |
strategy |
string | 必填 | 默认策略,引用 strategy[].name。 |
hosts |
string | TCP 模板示例中可选 | 入口级本地 hosts 资源;命中时本地回答,未命中再使用 strategy。 |
通过把 IPv4 和 IPv6 地址放入同一个 addresses 数组,可以用一个逻辑 listener 表达双栈入口。实现必须明确 IPv6 socket 的 v6-only 行为,避免 0.0.0.0 与 :: 展开后的绑定重叠。
DoH 不使用普通 listener 的顶层 address、port、strategy 或单个 path 字段,而是由共享路由和 endpoint 组成:
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string | 逻辑 DoH listener 名称,必须唯一。 |
type |
enum | 固定为 doh。 |
routes |
array | 所有 endpoint 共享的 HTTP 路由列表。 |
endpoints |
array | 一个或多个绑定端点;每个 endpoint 独立处理 TLS 和客户端 IP 来源。 |
| 字段 | 类型 | 说明 |
|---|---|---|
path |
string | HTTP 路径,可使用一次完整路径段形式的 {client_id} 占位符。 |
strategy |
string | 路由命中的策略,引用 strategy[].name。 |
模板示例使用 /dns/inner/{client_id} 和 /dns/outside/{client_id}。当 {client_id} 位于模板末尾时,单条模板同时匹配裸路径 /dns/inner 和带一个非空路径段的 /dns/inner/<client_id>;裸路径不产生 client ID,/dns/inner/ 和额外多段路径不匹配。占位符位于中间时仍必须提供对应的非空路径段。不同 route 不能命中同一个实际 HTTP path,例如 /dns/inner、/dns/inner/client-a 都会与 /dns/inner/{client_id} 产生语义重叠并在配置加载时拒绝。
Transport 只对实际 HTTP path 匹配一次,并把配置模板作为稳定 route ID 传给 Policy;route ID 不包含真实 client ID。/dns-quer/inner* 与 /dns-quer/outside* 是服务保留路径,不能作为自定义路由;它们与示例中的 /dns/... 前缀不是同一组路径。
每条路由固定同时支持 DoH GET 和 POST,不增加方法开关:
- GET 使用唯一的
dnsquery 参数,值为不带=padding 的 base64url DNS wire; - POST body 是原始 DNS wire,
Content-Type必须为application/dns-message; - 解码后的 GET 消息与 POST body 最大均为 65,535 字节;对应的无 padding GET
dns参数最多 87,380 个字符,HTTP request-target 限制不得小于路由路径加该参数所需长度;超限分别由 HTTP 层返回适当的413/414,不进入 DNS 核心; - 其他方法返回
405并包含Allow: GET, POST;不支持的媒体类型返回415,非法 wire 返回400; - 通过 DNS 协议校验后的
NXDOMAIN、REFUSED、SERVFAIL等响应仍使用 HTTP2xx,失败状态由 DNS RCODE 表达; - 响应使用
application/dns-message。由于答案可能随客户端身份、策略和 ECS 变化,v1 固定发送Cache-Control: no-store,只使用 FluxDNS 内部缓存。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
name |
string | 必填 | endpoint 名称,在同一 DoH listener 内唯一。 |
addresses |
array[string] | 必填 | endpoint 的 IPv4/IPv6 绑定地址列表。 |
port |
integer | 必填 | endpoint 监听端口。 |
tls |
object | 必填 | 该 endpoint 的 TLS 所有权。 |
client_ip |
object | 必填 | 请求客户端 IP 的来源和可信边界。 |
同一个逻辑 DoH listener 可以同时包含以下两类 endpoint,而不需要创建两个顶层 DoH 配置:
tls.mode: terminate:FluxDNS 自己完成 TLS 握手和解密,必须提供证书链和私钥文件;tls.mode: external:Nginx 等反向代理已经终止 TLS,FluxDNS 接收内部 HTTP,证书字段禁止出现。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
mode |
enum | 必填 | terminate 或 external。 |
certificate_file |
string | terminate 时必填 |
证书链文件路径,相对路径以 work.path 为基准。 |
private_key_file |
string | terminate 时必填 |
私钥文件路径,相对路径以 work.path 为基准。 |
external 模式不得配置 certificate_file 或 private_key_file。反向代理占用公网端口时,external endpoint 通常绑定本机或内网端口,例如模板中的 127.0.0.1:8053。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
source |
enum | 必填 | peer、forwarded_header 或 proxy_protocol。 |
header |
string | forwarded_header 时必填 |
允许 X-Forwarded-For、X-Real-IP 或 Forwarded。 |
trusted_proxies |
array[CIDR] | forwarded_header/proxy_protocol 时必填 |
可信的反代对端网段,不是最终客户端网段。 |
on_missing |
enum | forwarded_header 时可选 |
Header 缺失时 reject 或 use_peer。 |
on_invalid |
enum | forwarded_header 时可选 |
Header 非法时 reject 或 use_peer。 |
peer 直接使用 TCP 对端地址。forwarded_header 只接受来自 trusted_proxies 的请求;转发链应从右到左解析,并且反向代理必须先清理客户端自带的伪造 Header。
proxy_protocol 的自动识别只用于区分 PROXY v1 文本头和 v2 二进制头,不表示 header 可选。运行时先按 TCP peer 地址检查 trusted_proxies,再在 TLS handshake/HTTP 解析前读取前导头;不可信 peer、缺失头、非法长度、未知协议版本或不能提供 TCP4/TCP6 源地址的头均拒绝连接,不回退为 peer。v1 必须在前 107 字节内遇到 CRLF;v2 总前导长度上限固定为 536 字节。v2 中长度合法但未知的可选 TLV 可忽略,以兼容扩展;同一个 endpoint 不同时接收裸连接和 PROXY 连接,需要时应配置两个 endpoint。
listener[].addresses、listener[type=doh].endpoints[].addresses 与启用后的 webui.address/port 展开后不得产生相同协议、地址、端口的冲突;WebUI 只占用 TCP,不与同地址、同端口的 UDP listener 冲突。IPv4/IPv6 双栈是否共享 socket、以及 v6-only 行为必须在运行时明确。
当前模板没有 dot 或 doq listener 字段;DoT/DoQ 应在协议、TLS/QUIC 材料和握手校验契约确定后再加入。
upstreams 中每个资源由 type 决定允许的字段集合。未知字段和不属于该类型的字段都应拒绝。
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string | 上游唯一名称。 |
type |
enum | hosts、doh 或 group。 |
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
format |
string | 必填 | 内容格式;按该格式严格解析。模板示例为 json。 |
hosts |
string | 必填 | 内联 hosts 数据。 |
该类型可作为其他 DoH 上游的 bootstrap,用于避免首次解析依赖系统默认 DNS。内联内容格式错误应报告字段路径。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
address |
URL | 必填 | DoH 服务 URL。 |
bootstrap |
string | 可选 | 引导解析 address 主机名的上游名称。 |
connect_ip |
IP | 可选 | 实际连接目标 IP;不改变 HTTP Host 或 TLS SNI。 |
proxy |
string | 可选 | 代理出口名称,引用 outbound[].name。 |
edns_client_subnet |
object | 可选 | 上游级 ECS 覆盖。 |
bootstrap 和 connect_ip 解决的是连接建立路径:前者引用上游完成域名解析,后者直接指定连接 IP。二者互斥,均不改变远程服务的 HTTP Host 或 TLS SNI。
正式 bootstrap resolver 按 connector 缓存地址,TTL 下限为 0、上限为 3,600 秒;零 TTL 不复用,重新 prepare 不继承旧缓存。没有新增配置字段;并发、过期和失败边界见 Upstream 设计。
未配置代理时,优先使用 connect_ip,其次使用 bootstrap,两者都没有时使用系统解析器。配置 proxy 时,先解析对应 SecretRef 的 URL scheme:
socks5://:主机名在 FluxDNS 本地解析;顺序仍为connect_ip→bootstrap→ 系统解析器,再把 IP 交给代理;socks5h://:connect_ip缺失时把原始主机名交给代理解析,因此禁止同时配置bootstrap;若显式提供connect_ip,代理接收该 IP,HTTPHost/TLS SNI 仍使用 URL 主机名。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
upstreams |
array[object] | 必填 | 主成员列表;每项的 name 必填,weight 可选。 |
upstream_mode |
enum | 必填 | parallel、round-robin、load-balance 或 failover。 |
timeout |
duration | 必填 | 主组总超时时间。 |
fallbacks |
array[object] | 可选 | 主组在 timeout 内没有终态 DNS 响应时使用的回退成员,字段约束与主成员相同。 |
fallback_upstream_mode |
enum | 有 fallbacks 时必填 |
回退成员选择模式。 |
fallback_timeout |
duration | 有 fallbacks 时必填 |
回退组总超时时间。 |
upstreams[].name / fallbacks[].name |
string | 必填 | 成员上游名称。 |
upstreams[].weight / fallbacks[].weight |
positive integer | 可选,默认 1 |
正整数权重;省略后在 DTO 边界归一化为 1。 |
组成员使用对象而不是 name:weight 字符串,避免字符串解析歧义。精确算法见 Upstream 模块方案:round-robin 使用 smooth weighted round-robin,load-balance 使用按 weight 归一化的 least-in-flight,failover 严格按配置顺序且 weight 只能省略或显式设为 1。三种模式都只在 transport failure 时尝试其他成员,任意终态 DNS 响应都会结束当前组;主组完全没有终态响应时才进入 fallback。
parallel 的 v1 语义固定如下:
- 同时向主组成员发起请求。第一个通过 DNS wire 校验、响应标志校验且问题段与请求匹配的 DNS 响应立即返回客户端;
NXDOMAIN、REFUSED、SERVFAIL和TC=1都属于终态成功。超时、连接/TLS/HTTP 失败、非法 wire 和问题段不匹配不属于成功。 - 若首个终态响应是完整的
NOERROR/TC=0,按缓存准入规则写入并取消其余请求;若首个响应是NXDOMAIN、REFUSED、SERVFAIL或TC=1,已经发出的其他主组请求继续到各自完成或组timeout,但不改变已经返回给当前客户端的响应,也不触发 fallback。 - 对上一条继续运行的后台窗口,收集到全部成员完成或
timeout后再定稿缓存:先从完整的NOERROR/TC=0响应中按成员配置顺序选择;若没有,再从允许缓存的其他终态响应中按成员配置顺序选择。REFUSED不缓存。这样迟到有效答案仍能写缓存,但缓存结果不由并发完成顺序决定。 - 主组只有在
timeout内完全没有终态 DNS 响应时才进入fallbacks;fallback 组使用同样的响应和缓存语义。 parallel不使用权重;该模式下成员应省略weight或显式设为1,非1值会被拒绝,避免配置暗示不存在的优先级。
策略定义规则顺序、默认上游以及策略级覆盖。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
name |
string | 必填 | 策略唯一名称。 |
rules |
array | 必填 | 有序规则列表;从上到下匹配,命中第一条后停止。 |
default_upstream |
string | 必填 | 没有规则命中时使用的上游名称。 |
cache |
object | 可选 | 策略级缓存覆盖,高于 dns.cache。 |
ttl_override |
object | 可选 | 策略级 TTL 覆写,与 cache 平级。 |
edns_client_subnet |
object | 可选 | 策略级 ECS 覆盖。 |
每条规则必须且只能提供一个匹配目标:rule_set 与 hosts exactly-one-of。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
rule_set |
string | 与 hosts 二选一 |
规则集名称或子选择器,例如 geosite:cn。 |
hosts |
string | 与 rule_set 二选一 |
本地 hosts 资源名称。 |
upstream |
string | rule_set 规则通常必填 |
命中规则集后使用的上游;引用 upstreams[].name。 |
edns_client_subnet |
object | 可选 | 规则级 ECS 覆盖,优先级最高。 |
校验约束:
rule_set和hosts不能同时出现,也不能同时缺失;rule_set规则应提供upstream;hosts规则是本地回答,不应依赖upstream;- 所有资源引用必须存在且类型正确。
WebUI 有序规则卡片支持本级 ECS 的继承、禁用、客户端地址和自定义 IP/CIDR;继承提交时省略对象,非 custom 模式省略 custom_ip。Hosts 规则可保留 ECS 配置,但本地回答不向上游发送 ECS,提交时清除 upstream。规则 DTO 不支持逐规则 cache/TTL;这两项继续在策略或其他既有层级配置。非法规则 CIDR 的管理接口错误包含对应 rules[index].edns_client_subnet.custom_ip 字段路径,不返回提交值或底层解析错误。
策略级缓存只允许以下字段,不包含全局 memory、failure_ttl 或 persistence:
| 字段 | 类型 | 说明 |
|---|---|---|
enabled |
boolean | true 选择策略逻辑池;false 对该策略请求完全禁用缓存。 |
optimistic.enabled |
boolean | 是否允许过期记录先返回并异步刷新。 |
optimistic.answer_ttl |
duration | 乐观缓存应答 TTL。 |
optimistic.max_age |
duration | 过期记录可被乐观返回的最长时间。 |
optimistic.negative_max_age |
duration | 可选;NODATA/NXDOMAIN 的乐观窗口,语义与 dns.cache.optimistic.negative_max_age 相同。 |
整个 strategy[].cache 缺失时才允许回退到全局池;对象存在时 enabled 必填。策略级 ttl_override 使用 dns.ttl_override 的相同字段结构和继承语义。
策略表单保存前先校验已挂载字段,再从完整 Form store 取值,因此禁用 TTL 的隐藏上下限不会因只改名称或上游而丢失。未修改覆盖块保留原 DTO;ttl_override.enabled 缺失、显式 false、0s 以及 cache.optimistic 的继承/显式配置保持区别。编辑与验证证据见前端页面实现。
顶层 hosts 是可被 listener 或策略规则直接命中的本地回答资源;它不同于 upstreams[type=hosts],后者是一个解析上游资源。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
name |
string | 必填 | hosts 资源唯一名称。 |
type |
enum | 必填 | const 或 file。 |
format |
enum | 必填 | json 或 hosts;内容必须按格式严格解析。 |
hosts |
string | type: const 时必填 |
内联 hosts 内容。 |
path |
string | type: file 时必填 |
本地 hosts 文件路径,相对路径以 work.path 为基准。 |
auto_update |
boolean | type: file 时可选 |
是否自动重载本地文件。 |
update_interval |
duration | auto_update: true 时必填 |
自动重载间隔。 |
文件型 hosts 的 auto_update 只表示本地文件重载,不表示远程同步。
hosts 与 rule_set 使用相同的 per-resource snapshot 机制:本地文件变更只发布该 hosts 资源的新版本,保留其他资源版本;发布时同步更新 Policy 使用的资源内容 hash,使后续 fast cache key 切换 fingerprint。请求和 optimistic refresh 按最新资源重新进行 hosts/rule 匹配,但不因单资源变化扫描或全局清除缓存。
outbound 定义访问远程上游或远程规则集时使用的代理出口。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
name |
string | 必填 | 出口唯一名称。 |
type |
enum | 必填 | v1 固定为 socks5,表示 SOCKS5 协议族。 |
proxy_url |
SecretRef | 必填 | {env}、{file} 或内联 url/字符串,三选一,取得完整代理 URL。 |
SecretRef 解析后的 URL scheme 必须为 socks5:// 或 socks5h://:前者在 FluxDNS 本地解析目标主机名,后者把主机名交给代理解析。实际值可能包含用户名、密码或令牌;不要将环境变量内容或文件内容写入 Git 跟踪文件。内联形式会把明文 URL 写入配置文件,Management API 与 WebUI 只回显脱敏后的密码位(占位符 FLUXDNS_REDACTED_SECRET):编辑仍指向同一 scheme/host/port 时,后端按活动源恢复原密码并只写入用户实际改动的字段;改了 scheme/host/port 或重填则按新值写入;若提交的仍是占位符而无法恢复(含采用外部文件差异、活动源不是同址内联来源),该编辑以 VALIDATION_FAILED(HTTP 422)被拒绝,用户需重新提交完整 URL,占位符不会作为密码持久化。其他代理类型尚未形成配置契约。 |
规则集是策略匹配的数据源。资源可以内联、来自本地文件或从远程 URL 下载。运行时每个资源独立形成 ResourceSnapshot,拥有自己的 revision/epoch、content hash、来源 fingerprint 和 parser/compiler version;某个资源刷新不会要求其他资源同步刷新或自动清空全局缓存,但新 content hash 会进入后续请求的 policy fingerprint。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
name |
string | 必填 | 规则集唯一名称,也可作为落盘文件名前缀。 |
type |
enum | 必填 | const、file 或 remote。 |
format |
enum | 必填 | 模板示例为 json、clash、dat。 |
rule |
string | type: const 时必填 |
内联规则内容。 |
path |
string | type: file 时必填 |
本地规则文件路径,相对路径以 work.path 为基准。 |
url |
URL | type: remote 时必填 |
远程规则下载地址。 |
proxy |
string | type: remote 时可选 |
下载远程规则使用的出口,引用 outbound[].name。 |
auto_update |
boolean | type: file 或 remote 时可选 |
是否定期刷新或重载资源。 |
update_interval |
duration | auto_update: true 时必填 |
刷新/重载间隔。 |
format: clash 表示 Clash 行格式,不是标准 YAML。监听器绑定前,所有已配置的 const、file 和 remote 资源都必须形成有效的首次内存快照;解析、读取、下载、代理或格式校验失败均拒绝启动。远程资源可先尝试加载 work.rules_path 中上一轮已成功校验并原子落盘的快照;没有有效快照且本次下载失败时必须拒绝启动。
format: json 接受两种 JSON dialect:
- FluxDNS legacy JSON:顶层只允许
domain、domain_suffix、domain_keyword、domain_regex,各字段接受单个 string 或 string array; - sing-box source JSON:顶层为
version和rules,当前接受version: 1..=5。每个rules[]只投影与 legacy JSON 同名的四个域名字段;type、invert、network、ip_cidr、端口、客户端条件、嵌套 logical rule 等其他 rule 字段全部忽略,即使忽略会改变原 sing-box 匹配语义。没有上述四个字段的单条 rule 被跳过,整份文档最终没有产生任何域名 matcher 时拒绝该资源。
该行为是面向域名集合文件的部分兼容,不是完整 sing-box source 语义兼容;使用者必须确保输入文件本身适合按四类域名字段做并集投影。sing-box 二进制 .srs 不支持。legacy JSON 和 sing-box 顶层结构不能混合,未知顶层字段继续拒绝。规则 parser/compiler 版本已提升为 rule-index-v2,旧 parser version 的远程 manifest 不作为可恢复 snapshot,启动时需要重新获取并校验资源。
进程启动后的刷新只有在下载/读取、完整解析和内容校验都成功后才能原子替换当前快照;失败时保留上一份有效快照并记录带字段路径的错误。auto_update 只控制启动后的刷新,不降低首次快照要求;重试采用指数退避并封顶 5 分钟,连续三次计划刷新失败或超过 3 × update_interval 未成功时标记资源 stale,但仍可使用上一份有效快照。没有旧快照的资源引用必须 fail-closed。模板暂未提供远程版本锁定或 expected checksum 字段,当前仅记录内部 content hash/source fingerprint,生产部署的固定版本策略需后续定稿。
geosite:cn 这类写法表示引用 V2Ray GeoSiteList protobuf dat 地理规则集中的命名子集。解析时先尝试完整资源名;完整名不存在时再按第一个 : 拆分。资源名大小写敏感;selector 必须非空、不超过 128 bytes,且只包含不含空白的可打印 ASCII,保留拒绝 : 分隔符和 @ attribute 语法,并归一化为小写。因此 geosite:geolocation-!cn 可直接引用。格式不支持 selector 或 selector 不存在时在 prepare 阶段失败。dat 加载或刷新时一次性解析、校验并编译所有 selector,查询热路径只查当前 rule 引用的 canonical selector 和既有 matcher,不会重新解析 dat 或逐个校验全部 selector。dat 内容按二进制 protobuf 读取,不先转换为 UTF-8。
客户端规则按请求级选择策略,并提供客户端级缓存、TTL 和 ECS 覆盖。
| 字段 | 类型 | 条件 | 说明 |
|---|---|---|---|
name |
string | 必填 | 客户端规则名称,列表内唯一。 |
client_id |
string | 必填 | 唯一、大小写敏感的请求身份,不从 name 派生。 |
match |
object | 可选 | 客户端 IP 匹配条件;缺失时使用空 ips。 |
match.ips |
array[CIDR] | 可选 | 按客户端 IP/CIDR 匹配。 |
strategy |
string | 可选 | 命中后使用的策略;省略时继承 listener 或 DoH route 的策略。 |
cache |
object | 可选 | 客户端级缓存覆盖,优先级最高。 |
ttl_override |
object | 可选 | 客户端级 TTL 覆写,与 cache 平级。 |
edns_client_subnet |
object | 可选 | 客户端级 ECS 覆盖。 |
匹配优先级固定为:先精确 client_id,再按 IP 的最长 CIDR 前缀。在同一优先级出现多个冲突规则时在配置校验阶段报错,不依赖数组顺序。单个客户端可以仅由 client_id 标识而没有 IP 条目。
修改 client_id 立即替换运行时匹配索引:使用旧 ID 的 DoH 请求不再命中该客户端(可能回落到 IP 匹配),客户端缓存池按新身份重新建立,旧身份的条目不再被命中,按常规过期与容量淘汰回收。已写入的详情和统计保留请求时冻结的旧 ID,不迁移或重写;历史页面按当前目录把 ID 投影为名称,因此旧 ID 的记录不再显示当前名称,按名称筛选也只覆盖新 ID。
客户端级 cache.enabled: true 选择“实际客户端身份 + 生效策略”逻辑池,false 完全禁用当前请求的缓存;只有整个客户端 cache 对象缺失时才继续选择策略池或全局池。客户端级 ttl_override 和 edns_client_subnet 分别遵循覆盖和继承中的层级规则。
v2 配置加载阶段至少执行以下校验:
version存在且为2;其他版本拒绝加载且不迁移。- 拒绝未知字段;每种
type只允许自己的字段集合,条件字段满足 exactly-one-of/required-if 约束。 - 所有资源集合中的
name唯一,所有引用存在、类型正确且无循环引用。 work.path非空;相对值可基于启动配置文件目录解析为绝对的resolved_work_path,缺少来源目录时拒绝;目录不存在时创建,启动配置不在该目录时复制为<resolved_work_path>/config.yaml。webui.enable为true时创建独立 Management Server,false时不创建管理 listener;普通 listener、DoH endpoint 和 Management endpoint 地址展开后不存在 TCP/UDP bind 冲突,并明确 IPv6 v6-only 行为。mode、format、type、端口、CIDR、URL、duration、权重和内嵌内容格式合法;group 成员缺省权重为1,parallel和failover的显式权重只能为1。ttl_override与cache平级;策略/客户端 cache 对象存在时enabled必填,显式false不得回退到全局池。- ECS 块未配置时继承,显式
mode: disabled才停止继续传递。 webui.users[].password_hash必须是受支持算法生成的单向 hash,禁止明文password字段。dns.cache.memory.max_size_bytes为正数,failure_ttl在1s..=5m,negative_ttl_max在1s..=1d,persistence.snapshot_interval在1s..=1d。- DoH route 的 path 模板合法且彼此不存在语义重叠;endpoint 的
tls.mode独立校验:terminate必须有证书和私钥,external不得有证书字段;GET/POST wire、Content-Type 和固定消息上限合法。 forwarded_header/proxy_protocol必须配置trusted_proxies,且可信范围只覆盖反代对端;PROXY v1/v2 前导头缺失、未知或非法时拒绝。- DoH 上游的
bootstrap与connect_ip互斥;SecretRef 解析后的代理 scheme 合法,socks5h://不得同时使用bootstrap。 database.type/database.path/database.records_path始终存在;prepare 阶段布局标记、数据库打开、migration 或写探针失败必须阻止启动,分片目录与统计库、缓存快照物理隔离。- 聚合统计默认开启,按日和有界 client/transport/strategy/upstream/RCODE/cache 维度持久化;
dns.resolve_log.enable只控制详情记录,R/G/T 来自statistics.retention。 - 所有配置资源在 bind 前形成有效首次 snapshot;任何首次读取、下载或校验失败都阻止启动,后续单资源刷新失败才保留该资源旧版本;成功刷新更新 policy fingerprint,但不能触发全局 cache clear。
以下项目不在当前 v2 配置 schema 中,不应通过猜测扩展字段:
dot/doqlistener 的字段、TLS/QUIC 材料来源和协议特有校验。- 主动上游健康检查、熔断器和持久健康分数配置。
- 远程规则的 expected checksum、版本锁定和签名验证字段;当前只记录内部 content hash/source fingerprint。
- 旧配置/旧数据库兼容窗口或转换工具;当前业务库升级链见 Storage 模块。
- WebUI 配置写操作、缓存清理、权限分级和更长期的历史统计保留策略。