Headscale 仓库开发者与 AI Agent 协作指南:从构建测试到核心架构导航
【免费下载链接】headscaleAn open source, self-hosted implementation of the Tailscale control server项目地址: https://gitcode.com/GitHub_Trending/he/headscale
AGENTS.md 是 Headscale 仓库面向"在代码库中协作的 AI Agent"以及人工维护者编写的行为与架构导航手册。它并不重复逐个命令的用法,而是把"如何问问题、先读哪份文档、改什么文件、哪些规则绝对不能违反"这类决策准则,与数据库迁移、Tags 归属模型、策略引擎等承重架构约束沉淀在一起。阅读本文后,你将掌握在该仓库中快速进入开发状态的标准工作流(nix develop+make目标 +prek预提交钩子)、hscontrol/各子系统的划分与核心热路径,以及"为什么 tags XOR user 所有权、为什么迁移顺序不可变"等底层规则——这些同样适用于理解 Headscale 的自托管控制面实现原理。
一、这份文档的角色:行为准则与程序文档分离
Headscale 是一个用 Go 编写的、开源自托管的 Tailscale 控制服务器(control server),负责管理自托管 tailnet 中的节点注册、IP 分配、策略执行与 DERP 路由。
AGENTS.md 开篇就明确了文档分工:行为准则(behavioural guidance)放在本文件;复杂流程的操作手册放在代码旁。例如集成测试的运行方法见 cmd/hi/README.md,编写测试的规范见 integration/README.md。因此,仓库维护者给任何 Agent 的第一条指令是:运行测试或编写新测试之前,先把上述两份 README 完整读完,绝不猜测命令行参数。
这一"行为准则 + 就近手册"的组织原则贯穿全文:.claude/agents/目录已被废弃,新的行为指导写入本文件,程序性指导就近放入各目录的 README。
二、与 AI Agent 协作的交互规则
文档把"交互规则"放在最前,因为它们是所有其他决策的约束前提,具体包括六条:
- 用多选项提问代替开放式提问。需要澄清意图、范围或方案时,用
AskUserQuestion(或编号列表)给出覆盖各分支的选项,并提供"其他——请描述"的兜底项。例如询问"过期节点如何处理"时给出:(a) 保持对端可见但标记为过期(当前行为);(b) 对端完全不可见;(c) 对端不可见但管理 API 可见;(d) 其他。原因是开放式提问浪费一次往返,且往往得到答非所问的结果。 - 执行复杂命令前先读文档。任何
hi命令、集成测试、代码生成器或迁移工具,先完整阅读对应 README,从不臆测 flag;若未文档化则向用户询问而非自创。 - 先映射再行动(Map once, then act)。用 Glob/Grep 理解文件结构后立即执行,不在已规划区域重复"复查";同会话内不重读已编辑的文件,由工具跟踪状态。
- 快速失败、及时上报(Fail fast, report up)。同一错误出现两次就停止,向用户报告带上下文的准确错误,而不是循环尝试变体。
- 多文件改动先确认范围。改动超过三个文件时,先向用户展示将要变更的文件及原因;非平凡工作使用计划模式(
ExitPlanMode)。 - 优先编辑既有文件。非必要不新建文件,不生成辅助抽象、包装工具或"以防万一"的配置——三行相似代码好过过早的抽象。
三、快速开始:开发环境与常用命令
文档给出两条进入路径:一是用 Nix 开发环境锁定工具链,二是直接用 Makefile 目标。
3.1 Nix 开发环境
# 进入 nix dev shell(锁定 Go 工具链、buf、golangci-lint、prek) nix developflake.nix固定了 CI 中使用的精确工具链;仓库 go.mod 声明go 1.26.5(go 指令同时表达最低 Go 版本要求),配合 flake.lock 与 flakehashes.json 保证环境可复现。flakehashes.json由 cmd/vendorhash 保持与go.mod/go.sum同步(见 .pre-commit-config.yaml 中vendor-hash钩子)。
3.2 Makefile 目标矩阵
对照 Makefile 源码,各目标的实际行为如下:
| 目标 | 实际执行内容(依据 Makefile) |
|---|---|
make dev | 完整开发工作流:fmt + lint + test + build |
make build | go build -buildmode=pie -ldflags "-X main.version=$(VERSION)" -o headscale ./cmd/headscale |
make test | go test -race ./...(注意:Makefile 中带-race) |
make fmt | gofumpt -l -w .+golangci-lint run --fix+mdformat docs/+prettier --write |
make lint | golangci-lint run --timeout 10m |
make generate | go generate ./...+make client(重新生成客户端代码) |
make clean | 移除headscale二进制与gen/client |
make dev-server | go run ./cmd/dev启动本地开发服务器 |
make openapi | go run ./cmd/gen-openapi从代码导出 OpenAPI 规范 |
直接调用 Go 测试同样可行:
go test ./... go test -race ./...3.3 集成测试入口
go run ./cmd/hi doctor # 环境自检 go run ./cmd/hi run "TestName" # 运行指定集成测试(先读 cmd/hi/README.md)cmd/hi是集成测试运行器,其文件构成(main.go、run.go、doctor.go、docker.go、cleanup.go、stats.go、README.md)在 cmd/hi 目录下,运行任何hi命令前都必须完整阅读其 README。
四、pre-commit 门禁:用 prek 复现 CI 检查
prek安装的 git 钩子与 CI 运行同一批检查:
nix develop prek install # 一次性安装 prek run # 在暂存文件上运行钩子 prek run --all-files # 在整个代码树上运行钩子对照 .pre-commit-config.yaml,钩子覆盖范围包括:文件卫生(trailing whitespace、行尾、BOM)、语法校验(JSON/YAML/TOML/XML)、merge-conflict 标记、私钥检测,以及nixpkgs-fmt、prettier(排除docs/,docs 用mdformat)、通过--new-from-rev=HEAD~1运行的golangci-lint。全局 exclude 规则忽略生成代码^(gen|openapi)/与 golden 测试夹具^hscontrol/testdata/apiv1_golden/。
当存在upstream/main远程时,手动执行一次等价命令即可:
golangci-lint run --new-from-rev=upstream/main --timeout=5m --fix文档强调:git commit --no-verify仅可接受用于特性分支上的 WIP 提交,绝不可用于main分支。
五、项目布局与 hscontrol 子系统
AGENTS.md 给出的顶层布局为cmd/(主二进制与测试运行器)、hscontrol/(核心控制面)、integration/(基于 Docker 的端到端测试)、proto/(protobuf 定义)、gen/(buf 生成的代码,禁止手改)、docs/与packaging/。
5.1hscontrol/各包职责
- 顶层服务器文件:
app.go、handlers.go、noise.go、auth.go、oidc.go、poll.go、metrics.go、debug.go、tailsql.go、platform_config.go等。 - hscontrol/state — 中央协调器(
state.go)与写时复制(copy-on-write)的NodeStore(node_store.go)。所有跨子系统操作都经由State,而不是直接访问数据库。 - hscontrol/db — GORM 层、迁移与 schema(
node.go、users.go、api_key.go、preauth_keys.go、ip.go、policy.go等)。 - hscontrol/mapper — 流式批处理器(
batcher.go、node_conn.go、builder.go、mapper.go),负责向客户端分发 MapResponses,属于性能敏感路径。 - hscontrol/policy —
policy/v2/是唯一的策略实现,顶层policy.go只是薄封装,不存在 v1 目录。 dns/、derp/、types/、util/、templates/、capver/— MagicDNS、中继、核心类型、辅助函数、客户端模板与能力版本号。- hscontrol/servertest — 不需要 Docker 的服务器级测试内存 harness,优先于
integration/使用。 - hscontrol/assets — 内嵌 UI 资源。
5.2 从源码验证的架构要点
hscontrol/state/state.go是中央协调器:跨切面操作(节点更新、策略评估、IP 分配)都经过State类型而非直接操作数据库。map 请求的同步点State.UpdateNodeFromMapRequest()位于 state.go(当前实现约在hscontrol/state/state.go:3025),Hostinfo 变更、endpoint 更新与路由通告在此落入 NodeStore。NodeStore是写时复制缓存:在 node_store.go 中实现,内部为atomic.Pointer[Snapshot]。每次读都是一次指针加载;写操作重建整个新快照后原子替换。它是MapRequest处理与对端可见性的热路径,因此文档告诫:改动热路径代码前先做测量。- Mapper 子系统:经
batcher.go与node_conn.go流式分发 MapResponses,任何改动都会影响所有已连接客户端。 - 节点注册链路:噪声握手(
noise.go)→ 认证(auth.go)→ 状态/数据库持久化(state/、db/)→ 初始 map(mapper/)。
六、数据库迁移铁律
AGENTS.md 明确警告:这些规则是承重墙,违反即可能损坏生产数据库。在 hscontrol/db/db.go 中,约db.go:1166处有注释明确冻结了迁移策略("As of 2025-07-02, all...",禁止再在禁用外键的情况下运行新迁移)。所有新迁移必须遵守:
- 绝不重排已有迁移。迁移顺序一旦提交即不可变。
- 只能在迁移数组末尾追加新迁移。
- 绝不禁用外键。
- 使用迁移 ID 格式
YYYYMMDDHHMM-short-description(时间戳 + 描述后缀),例如202602201200-clear-tagged-node-user-id——该迁移实际存在于 db.go 的迁移数组中,并在 hscontrol/db/testdata/sqlite/clear_tagged_node_expiry_migration_test.sql 等测试夹具中被覆盖验证。 - 绝不重命名被后续迁移引用的列;如需新列,让
AutoMigrate创建。
迁移测试的 SQL 夹具(位于 hscontrol/db/testdata/sqlite)如null_tags_user_id_migration_test.sql、recover_null_tags_user_id_migration_test.sql,均针对clear-tagged-node-user-id迁移对既有脏数据的处理做了回放验证,是迁移规则"不可变、只追加"的实测佐证。
七、Tags-as-Identity:tags 与用户所有权的互斥模型
这是文档反复强调的一条承重架构规则:Headscale 强制执行tags XOR user ownership——每个节点要么归 tags 所有(被标记),要么归用户命名空间所有,二者只能取其一。
7.1 判定归属要用IsTagged()
- 用
node.IsTagged()判定所有权,不要用node.UserID().Valid()——被标记节点仍可能带有UserID(用于"由谁创建"的追踪),因此IsTagged()才是权威判断。源码佐证位于 hscontrol/types/node.go:IsTagged()约在node.go:261,IsUserOwned()在node.go:267处定义为!IsTagged()。 - 被标记节点在 Tailscale 侧以特殊用户
TaggedDevices呈现,其用户 ID 为2147455555,定义见 hscontrol/types/users.go(TaggedDevicesUserID)。 SetTags的校验由validateNodeOwnership()执行,实现在 hscontrol/state/tags.go。- 边界用例与正反例见 hscontrol/types/node_tags_test.go。
7.2 反例警示
if node.UserID().Valid() { /* assume user-owned */ } // WRONG if node.UserID().Valid() && !node.IsTagged() { /* ok */ } // correct第一种写法把"有 UserID"当作"用户所有",会误判 tagged 节点;必须先过IsTagged()这道闸。
八、策略引擎:policy/v2 是唯一实现
策略实现位于 hscontrol/policy/v2,顶层 hscontrol/policy/policy.go 仅包含对 v2 的包装函数,没有 v1 目录。开发中会遇到的核心概念包括:
- Autogroups:
autogroup:self、autogroup:member、autogroup:internet(在 v2 的compiled.go、filter.go等源码中均有对应处理)。 - Tag owners:基于 IP 的授权,决定谁能认领某个 tag。
- Route approvals:通过策略自动批准子网路由。
- SSH policies:通过 grants 实现 SSH 访问控制。
- HuJSON:策略文件解析格式(在 v2 的
types.go、policy.go及测试中体现)。
使用示例可读 hscontrol/policy/v2/policy_test.go,ACL 参考文档位于 docs/ref/policy.md 与 docs。
九、集成测试规范
文档开宗明义:运行任何hi命令前完整阅读 cmd/hi/README.md,猜测hi的 flag 会导致运行失败并残留过期容器。测试编写模式(EventuallyWithT、IntegrationSkip、helper 变体、场景搭建)记录在 integration/README.md(其中第 54 行起要求每个集成测试函数必须以IntegrationSkip(t)开头,第 111 行起说明EventuallyWithT模式)。
关键提醒:
- 集成测试函数必须以
IntegrationSkip(t)开头。 - 外部调用(
client.Status、headscale.ListNodes等)应放入EventuallyWithT内;状态变更命令(如tailscale set)则不可放入其中。 - 每次运行会在
control_logs/{runID}/下产生约100 MB 日志;磁盘紧张时需清理旧运行。 - 测试不稳定几乎总是代码问题而非基础设施问题——责备 Docker 之前先读
hs-*.stderr.log。
集成测试基础设施位于 integration,包含基于 Docker 的端到端场景(如hsic、tsic、k3sic等控制面/客户端容器辅助)。
十、代码约定
10.1 提交信息
遵循 Go 风格package: imperative description(如db: scope DestroyUser to only delete the target user's pre-auth keys、state: fix policy change race in UpdateNodeFromMapRequest)。不是 Conventional Commits,不使用feat:/chore:/docs:前缀。
10.2 Protobuf 与代码生成
proto/下的改动需要make generate(内部运行buf generate),并应放入与使用重新生成类型的调用方不同的独立提交。- 不要编辑
gen/——它由make generate从 proto 重新生成(gen/client/v1/client.gen.go、gen/client/v2/client.gen.go 即 buf/oapi-codegen 输出)。 - proto 改动与代码改动应为两个提交,而不是一个。
10.3 格式化
格式由golangci-lint配合golines(宽度 88)与gofumpt强制(配置见 .golangci.yaml)。运行make fmt或依赖 pre-commit 钩子即可。
10.4 日志
使用zerolog,倾向单行链式写法log.Info().Str(...).Msg(...);当字段数达到 4+ 或存在条件字段时,应增量构建并重新赋值事件变量:e = e.Str("k", v)——忘记重新赋值会静默丢失字段。
10.5 测试分层
服务器级、无需 Docker 的测试优先用 hscontrol/servertest,比完整集成测试更快。
10.6 读路径使用 View 类型
响应序列化器必须通过NodeView/UserView/PreAuthKeyView访问器读取数据。AsStruct()会在每次读取时克隆整条记录——它只用于数据库写/合并克隆与可变工作副本,绝不可用于构造 API 响应(约定:grep AsStruct hscontrol/api必须为空)。
十一、常见坑(Gotchas)
- 数据库差异:本地开发用 SQLite;集成密集测试用 PostgreSQL(
go run ./cmd/hi run "..." --postgres)。部分竞态只在某一种后端上暴露。 - NodeStore 写成本:写操作重建完整快照,改动热路径前务必测量。
- Agent 文件:
.claude/agents/已废弃,不要再创建新的 agent 文件。 - 生成代码:勿编辑
gen/。 - 提交拆分:proto 改动与代码改动是两个提交。
十二、小结:给"开发 Headscale"的速查坐标
AGENTS.md 的价值在于把查证路径固化下来:行为规则看本文件,运行集成测试看 cmd/hi/README.md,编写测试看 integration/README.md,节点归属判定看IsTagged()(hscontrol/types/node.go),策略逻辑只认policy/v2(hscontrol/policy/v2),迁移只许在末尾追加且 ID 形如YYYYMMDDHHMM-描述。对希望在 Headscale 上做贡献或深度理解其控制面实现的读者,上述每一个引用文件都值得顺着展开阅读——它们共同构成了这座 Go 控制服务器既可测试、又可安全演进的地基。
【免费下载链接】headscaleAn open source, self-hosted implementation of the Tailscale control server项目地址: https://gitcode.com/GitHub_Trending/he/headscale
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考