Skip to content

Latest commit

 

History

History
771 lines (542 loc) · 81.4 KB

configuration.md

File metadata and controls

771 lines (542 loc) · 81.4 KB

FluxDNS 配置字段参考

文档状态:有效

适用范围:本文与当前模板同步,描述配置契约、校验和已实现运行时边界;未定义行为不应视为已支持。

最后核对:2026-09-21(DNS 缓存 expired 状态与过期保留期定向核对,基于本次工作树;其余正文沿用原核对范围)

核对基线:d7296fd;本轮核对 P5 变更与联合验收,分批历史结果按原日期和基线解释

2026-09-21 增量核对(本机日期):仅核对 DNS 缓存的 group ECS、Resolved fingerprint、stale refresh、late-result 语义与条目过期状态 expired;其余正文保留原历史核对范围

增量核对基线:e50b948edb4699f58a1e57039fb56442ab228687 加本次缓存修复工作树;静态编译与前期定向测试证据见DNS 管线,未部署或实测 OpenWrt

2026-10-04 增量核对:基线 eb428f92cdc3acda836547107ef2f56f6d988249 加本次工作树,仅更新快照读取等待和策略表单覆盖保真;其他配置契约不变

2026-10-04 负缓存增量核对:基线 e811b713292467ce964534c1cb7e2ef44002cdf6 加本次工作树,仅核对 dns.cache.negative_ttl_max、optimistic.negative_max_age、按响应质量区分的乐观窗口与缓存活动响应类观测;单元/策略层测试已覆盖,本地运行验收仍由负缓存新鲜度计划跟踪

依据:config-example.yaml

关联文档:后端架构

加载与支持边界

v2 契约与生产基线(2026-09-08)

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 标记归属。

P1 客户端匹配索引内部能力(2026-09-08)

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 配置入口。历史记录仍只保留请求发生时已经冻结的匹配事实,不补造或重匹配旧身份。

配置旧路径退出(2026-09-09)

旧 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 的完整性能或浏览器验收。

P1 活动源与候选内部底座(2026-09-07)

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 变化。

P1 应用后持久化内部底座(2026-09-07)

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 完全消除。

P1 受管文件还原内部能力(2026-09-07)

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。

P1 外改确认重试内部能力(2026-09-07)

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,不能自动重放应用或还原。

P1 配置状态与冻结操作结果(2026-09-07)

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 应用及文件同步,具体联合证据见下节。

P1 外部差异输入内部能力(2026-09-08)

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 投影与输出预算见差异投影。

P1 配置事务生产接线(2026-09-08)

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 组合已验收,真实入口和未支持项见后端实现,验证专项的已知边界见验证范围与收口。既定契约与代码冲突时须保留复现证据并明确修复边界,不仅修改字段说明来掩盖实现缺口。

1. 配置模型概览

FluxDNS 的配置由命名资源和引用关系组成。请求入口选择策略,策略按顺序匹配规则,再决定使用本地 hosts 或远程上游:

listener
  ├─ udp/tcp → strategy
  └─ doh → routes → strategy
                         ├─ hosts:本地回答
                         └─ rule_set → upstream / group → outbound
  • listener 定义请求入口。一个普通 listener 可以展开为多个 IPv4/IPv6 socket。
  • 一个 type: doh listener 是一个逻辑 DoH 服务,可通过多个 endpoints 同时提供不同绑定方式。
  • routes 在同一个 DoH listener 内共享;每条路由选择一个策略。
  • clients 按客户端标识或 IP 网段选择策略,并覆盖缓存、TTL 和 ECS 等设置。
  • upstreams、hosts、rule_set 和 outbound 是可复用的命名资源。
  • webui 定义独立的 WebUI Management Server、浏览器 origin 和登录用户。

2. 通用约定

2.1 版本

顶层 version 是配置 schema 版本。当前模板和生产加载器只接受 version: 2;其他版本直接拒绝并提示使用新的开发目录,不迁移或按旧字段猜测。

2.2 路径和工作目录

配置路径采用两级基准解析,不能直接把所有相对路径都拼到进程当前工作目录:

  1. 先确定启动配置文件的绝对路径。命令行传入的配置文件路径若为相对路径,仅在这一步相对于进程启动时的当前工作目录解析;其父目录记为 config_dir。
  2. 解析 work.path:绝对值直接使用;相对值按 config_dir.join(work.path) 解析。结果经词法归一化后形成绝对的 resolved_work_path。
  3. 解析项目路径字段:绝对值直接使用;相对值统一按 resolved_work_path.join(field_path) 解析并做词法归一化,不能再回到 config_dir 或进程当前工作目录。
  4. 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 解析值不会写回配置副本。

2.3 命名与引用

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

应在加载阶段检查名称唯一性、引用存在性、引用类型和循环引用。

2.4 值类型

数据类型 模板示例 说明
时长 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。

2.5 覆盖和继承

配置块未出现时继承较低优先级的设置;块中只出现的字段按字段继承。显式的禁用值用于停止继承。

ECS 优先级

当多个层级提供 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

缓存和 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。

2.6 配置版本与归一化

生产加载不直接把 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 后切换,无法安全切换的项拒绝候选并保留旧运行时。

3. 顶层字段

字段 类型 说明
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 客户端匹配和客户端级覆盖。

4. work

字段 类型 条件 说明
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 为基准。

5. database

字段 类型 条件 说明
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,该状态必须可观测。

6. logs

字段 类型 条件 说明
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 表单保存和文件持久化联合接线仍待完成,具体边界见日志热切换。

7. webui

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。

8. dns

8.1 dns.cache

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:

  1. 全局池:namespace 为 global;
  2. 策略池:namespace 包含生效的 strategy.name;
  3. 客户端 + 策略池: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。

8.2 dns.ttl_override

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 覆写。

8.3 dns.edns_client_subnet

字段 类型 条件 说明
mode enum 必填 disabled、client 或 custom。
custom_ip CIDR mode: custom 时必填 要发送给上游的 ECS 网段。

全局 ECS 设置可由上游、客户端、策略和规则覆盖,详见覆盖和继承。

8.4 dns.resolve_log

字段 类型 说明
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、背压和磁盘配额。

9. listener[]

一个 listener 是一个逻辑服务。addresses 中的每个地址由运行时展开为独立 socket;同一个逻辑 listener 的地址共享协议和端口。

9.1 UDP/TCP 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 与 :: 展开后的绑定重叠。

9.2 DoH listener

DoH 不使用普通 listener 的顶层 address、port、strategy 或单个 path 字段,而是由共享路由和 endpoint 组成:

字段 类型 说明
name string 逻辑 DoH listener 名称,必须唯一。
type enum 固定为 doh。
routes array 所有 endpoint 共享的 HTTP 路由列表。
endpoints array 一个或多个绑定端点;每个 endpoint 独立处理 TLS 和客户端 IP 来源。

listener[type=doh].routes[]

字段 类型 说明
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 使用唯一的 dns query 参数,值为不带 = 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 等响应仍使用 HTTP 2xx,失败状态由 DNS RCODE 表达;
  • 响应使用 application/dns-message。由于答案可能随客户端身份、策略和 ECS 变化,v1 固定发送 Cache-Control: no-store,只使用 FluxDNS 内部缓存。

listener[type=doh].endpoints[]

字段 类型 条件 说明
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,证书字段禁止出现。

endpoints[].tls

字段 类型 条件 说明
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。

endpoints[].client_ip

字段 类型 条件 说明
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。

9.3 绑定校验

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 材料和握手校验契约确定后再加入。

10. upstreams[]

upstreams 中每个资源由 type 决定允许的字段集合。未知字段和不属于该类型的字段都应拒绝。

10.1 公共字段

字段 类型 说明
name string 上游唯一名称。
type enum hosts、doh 或 group。

10.2 type: hosts

字段 类型 条件 说明
format string 必填 内容格式;按该格式严格解析。模板示例为 json。
hosts string 必填 内联 hosts 数据。

该类型可作为其他 DoH 上游的 bootstrap,用于避免首次解析依赖系统默认 DNS。内联内容格式错误应报告字段路径。

10.3 type: doh

字段 类型 条件 说明
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,HTTP Host/TLS SNI 仍使用 URL 主机名。

10.4 type: group

字段 类型 条件 说明
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 语义固定如下:

  1. 同时向主组成员发起请求。第一个通过 DNS wire 校验、响应标志校验且问题段与请求匹配的 DNS 响应立即返回客户端;NXDOMAIN、REFUSED、SERVFAIL 和 TC=1 都属于终态成功。超时、连接/TLS/HTTP 失败、非法 wire 和问题段不匹配不属于成功。
  2. 若首个终态响应是完整的 NOERROR/TC=0,按缓存准入规则写入并取消其余请求;若首个响应是 NXDOMAIN、REFUSED、SERVFAIL 或 TC=1,已经发出的其他主组请求继续到各自完成或组 timeout,但不改变已经返回给当前客户端的响应,也不触发 fallback。
  3. 对上一条继续运行的后台窗口,收集到全部成员完成或 timeout 后再定稿缓存:先从完整的 NOERROR/TC=0 响应中按成员配置顺序选择;若没有,再从允许缓存的其他终态响应中按成员配置顺序选择。REFUSED 不缓存。这样迟到有效答案仍能写缓存,但缓存结果不由并发完成顺序决定。
  4. 主组只有在 timeout 内完全没有终态 DNS 响应时才进入 fallbacks;fallback 组使用同样的响应和缓存语义。
  5. parallel 不使用权重;该模式下成员应省略 weight 或显式设为 1,非 1 值会被拒绝,避免配置暗示不存在的优先级。

11. strategy[]

策略定义规则顺序、默认上游以及策略级覆盖。

字段 类型 条件 说明
name string 必填 策略唯一名称。
rules array 必填 有序规则列表;从上到下匹配,命中第一条后停止。
default_upstream string 必填 没有规则命中时使用的上游名称。
cache object 可选 策略级缓存覆盖,高于 dns.cache。
ttl_override object 可选 策略级 TTL 覆写,与 cache 平级。
edns_client_subnet object 可选 策略级 ECS 覆盖。

11.1 strategy[].rules[]

每条规则必须且只能提供一个匹配目标: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 字段路径,不返回提交值或底层解析错误。

11.2 策略级 cache

策略级缓存只允许以下字段,不包含全局 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 的继承/显式配置保持区别。编辑与验证证据见前端页面实现。

12. hosts[]

顶层 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 匹配,但不因单资源变化扫描或全局清除缓存。

13. outbound[]

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,占位符不会作为密码持久化。其他代理类型尚未形成配置契约。

14. rule_set[]

规则集是策略匹配的数据源。资源可以内联、来自本地文件或从远程 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。

15. clients[]

客户端规则按请求级选择策略,并提供客户端级缓存、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 分别遵循覆盖和继承中的层级规则。

16. 配置校验清单

v2 配置加载阶段至少执行以下校验:

  1. version 存在且为 2;其他版本拒绝加载且不迁移。
  2. 拒绝未知字段;每种 type 只允许自己的字段集合,条件字段满足 exactly-one-of/required-if 约束。
  3. 所有资源集合中的 name 唯一,所有引用存在、类型正确且无循环引用。
  4. work.path 非空;相对值可基于启动配置文件目录解析为绝对的 resolved_work_path,缺少来源目录时拒绝;目录不存在时创建,启动配置不在该目录时复制为 <resolved_work_path>/config.yaml。
  5. webui.enable 为 true 时创建独立 Management Server,false 时不创建管理 listener;普通 listener、DoH endpoint 和 Management endpoint 地址展开后不存在 TCP/UDP bind 冲突,并明确 IPv6 v6-only 行为。
  6. mode、format、type、端口、CIDR、URL、duration、权重和内嵌内容格式合法;group 成员缺省权重为 1,parallel 和 failover 的显式权重只能为 1。
  7. ttl_override 与 cache 平级;策略/客户端 cache 对象存在时 enabled 必填,显式 false 不得回退到全局池。
  8. ECS 块未配置时继承,显式 mode: disabled 才停止继续传递。
  9. webui.users[].password_hash 必须是受支持算法生成的单向 hash,禁止明文 password 字段。
  10. dns.cache.memory.max_size_bytes 为正数,failure_ttl 在 1s..=5m,negative_ttl_max 在 1s..=1d,persistence.snapshot_interval 在 1s..=1d。
  11. DoH route 的 path 模板合法且彼此不存在语义重叠;endpoint 的 tls.mode 独立校验:terminate 必须有证书和私钥,external 不得有证书字段;GET/POST wire、Content-Type 和固定消息上限合法。
  12. forwarded_header/proxy_protocol 必须配置 trusted_proxies,且可信范围只覆盖反代对端;PROXY v1/v2 前导头缺失、未知或非法时拒绝。
  13. DoH 上游的 bootstrap 与 connect_ip 互斥;SecretRef 解析后的代理 scheme 合法,socks5h:// 不得同时使用 bootstrap。
  14. database.type/database.path/database.records_path 始终存在;prepare 阶段布局标记、数据库打开、migration 或写探针失败必须阻止启动,分片目录与统计库、缓存快照物理隔离。
  15. 聚合统计默认开启,按日和有界 client/transport/strategy/upstream/RCODE/cache 维度持久化;dns.resolve_log.enable 只控制详情记录,R/G/T 来自 statistics.retention。
  16. 所有配置资源在 bind 前形成有效首次 snapshot;任何首次读取、下载或校验失败都阻止启动,后续单资源刷新失败才保留该资源旧版本;成功刷新更新 policy fingerprint,但不能触发全局 cache clear。

17. v2 范围外与版本化边界

以下项目不在当前 v2 配置 schema 中,不应通过猜测扩展字段:

  1. dot/doq listener 的字段、TLS/QUIC 材料来源和协议特有校验。
  2. 主动上游健康检查、熔断器和持久健康分数配置。
  3. 远程规则的 expected checksum、版本锁定和签名验证字段;当前只记录内部 content hash/source fingerprint。
  4. 旧配置/旧数据库兼容窗口或转换工具;当前业务库升级链见 Storage 模块。
  5. WebUI 配置写操作、缓存清理、权限分级和更长期的历史统计保留策略。

18. 协议依据