
Knocker 是一款自托管服务,为您的 Homelab 提供基于 HTTP 的“敲门”单包授权 (SPA) 网关,支持 Web、CLI + GNOME 以及 Android 客户端。 它可以用作反向代理(如 Caddy)的身份验证,甚至可以在防火墙层面使用 FirewallD 集成。它允许您将服务完全私有化,仅在需要时向授权的 IP 地址开放。
这非常适合 Homelab 环境,您希望在不建立持久 VPN 连接的情况下将服务暴露到互联网,同时最大限度地减少对外攻击面。
sequenceDiagram
participant User as 用户
participant Caddy as 反向代理 (Caddy)
participant Knocker
participant Service as 受保护服务
User->>Caddy: 向受保护服务发送 HTTP 请求
Caddy->>Knocker: GET /verify (复制 X-Forwarded-For)
Knocker-->>Knocker: 检查 always_allowed_ips / excluded_paths / whitelist
alt IP 已加入白名单
Knocker-->>Caddy: 200 OK (空 body)
Caddy->>Service: 转发请求
Service-->>Caddy: 200 OK
Caddy-->>User: 200 OK
else IP 未加入白名单
Knocker-->>Caddy: 401 Unauthorized (空 body)
Caddy-->>User: 401 Unauthorized
end
Note over User,Knocker: 执行“敲门”操作(添加白名单条目)
User->>Knocker: POST /knock (X-Api-Key, 可选 ip_address, ttl)
Knocker->>Knocker: 验证 API 密钥,确定客户端 IP
Knocker->>Knocker: 更新 whitelist.json 并设置过期时间
Knocker-->>User: 200 OK (whitelisted_entry, expires_at, expires_in_seconds)
本项目设计为使用提供的 docker-compose.yml 文件作为 Docker 容器部署。它使用预构建的 Docker 镜像,支持 AMD64、ARMv8 和 ARMv7。
Knocker 为不同用例提供不同的镜像标签:
latest 最新稳定版(推荐用于生产环境)v1.2.3 特定版本标签(固定版本)main 开发分支(滚动更新,可能不稳定)配置:
knocker.example.yaml 重命名为 knocker.yaml。knocker.yaml 中的默认 API 密钥替换为您自己安全随机的字符串。knocker.yaml 中的 trusted_proxies 列表,它应该与反向代理网络的子网匹配(docker network inspect xxx)。whitelist.storage_path 保留在应用工作目录、/data 或 /tmp 下。firewalld.enabled: true 并调整相关设置来配置 Firewalld 集成。注意:这需要容器以 root 权限运行。运行服务:
docker compose up -d
这将拉取预构建的 knocker 镜像,并启动 knocker 和 两个服务。
Knocker 通过充当反向代理的认证网关来工作。它提供一个验证端点,用于检查请求的 IP 是否已加入白名单,如果没有,则返回 401,反向代理将拒绝连接。
Caddy 使用 forward_auth 指令通过认证端点来检查连接。
定义可复用片段:最佳实践是在 Caddyfile 中为认证检查定义一个片段。
保护您的服务:为您想要保护的任何服务导入该片段。
示例 Caddyfile:
# Caddyfile
# 为 knock-knock 检查定义可复用片段。
# 它使用 Docker 的内部 DNS 指向 knocker 服务。
(knocker_auth) {
forward_auth knocker:8000 {
uri /verify
}
}
# 用于执行敲门操作的公共端点。
# 确保此域名指向您的 Caddy 服务器 IP。
knock.your-domain.com {
reverse_proxy knocker:8000
}
# 一个受保护服务的示例。
jellyfin.your-domain.com {
import knocker_auth # 应用 forward_auth 检查
reverse_proxy jellyfin_service_name:8096
}
当用户未加入白名单时,Caddy 的 forward_auth 指令将返回一个空 body 的 401 Unauthorized 响应。
重要说明:Caddy 的 handle_errors 指令不适用于 forward_auth 响应。错误响应直接来自认证服务(knocker),而非 Caddy 本身,因此 handle_errors 无法拦截或修改这些响应。
Knocker 通过 firewalld 提供高级防火墙集成,创建基于 TTL 的动态、定时防火墙规则,这些规则会根据敲门请求中指定的 TTL 自动过期。此功能在网络层面运行,允许您将 knocker 用于非 HTTP 服务,如 SSH 或游戏服务器。
sequenceDiagram
participant Client as 用户
participant Firewall as Firewalld (knocker 区域)
participant Knocker
participant Service as 受保护服务 (端口 22)
Note over Client,Firewall: 初始状态 — 受监控端口默认被阻止
Client->>Firewall: TCP SYN 到 Service:22
Firewall-->>Client: DROP(无响应)
Note over Client,Knocker: 用户执行敲门以将 IP 加入白名单
Client->>Knocker: POST /knock (X-Api-Key, 可选 ip_address, ttl)
Knocker->>Knocker: 验证 API 密钥并确定客户端 IP
Knocker->>Firewall: 为客户端 IP 添加端口 22 的富接受规则并设置超时
Firewall-->>Knocker: 成功
Note over Firewall,Client: 新规则因优先级更高而覆盖 DROP 规则
Client->>Firewall: TCP SYN 到 Service:22
Firewall->>Service: 转发数据包
Service-->>Client: TCP SYN-ACK(连接建立)
Knocker->>Knocker: 更新 whitelist.json 并设置过期时间
Knocker 需要 FirewallD 2.0+,因为它依赖于区域优先级功能。该版本在 Debian 13、Ubuntu 24.04 LTS 及其他近期的稳定发行版中可用。
选择 FirewallD 是因为它能够将 CLI 接口与守护进程分离。这使得 Knocker 可以通过挂载系统的 D-Bus 套接字从 Docker 容器内部控制 firewalld,并且 FirewallD 还支持定时规则,因此 knocker 规则可以在 TTL 结束时自动过期。
FIREWALLD 无法与 Docker 发布的端口一起工作,更多详情请查看这个问题。
前提条件
配置
监控活跃规则:
# 检查 knocker 区域
firewall-cmd --zone=knocker --list-all
# 查看富规则
firewall-cmd --zone=knocker --list-rich-rules
# 监控规则变化
journalctl -u firewalld -f
有关详细的配置、架构和故障排除信息,请参阅完整的 FirewallD 集成指南。
如果您为 Tailscale 或其他 IP 启用敲门功能,可能会遇到由于 userland-proxy 工作方式导致的问题,您可能会得到与实际 IP 不同的请求 IP。
禁用 Userland-proxy 应该可以解决此问题,但请确保测试您的设置。您也可以使用主机网络模式。
/knock (POST)此端点验证 API 密钥并将 IP 加入白名单。
请求头:
X-Api-Key:您的秘密 API 密钥。请求体(可选):
allow_remote_whitelist: true):
{"ip_address": "您的目标IP或CIDR"}
示例(将您自己的 IP 加入白名单):
curl -i -H "X-Api-Key: YOUR_SECRET_KEY" https://knock.your-domain.com/knock
成功响应(200 OK):
{
"whitelisted_entry": "1.2.3.4",
"expires_at": 1672534800,
"expires_in_seconds": 3600
}
/verify (GET)此端点由 Caddy 的 forward_auth 使用,用于检查客户端的 IP 是否已加入白名单。成功时返回 200 OK,失败时返回 401 Unauthorized。X-Forwarded-For、X-Forwarded-Host 和 X-Forwarded-Uri 仅在请求来自 server.trusted_proxies 时才被信任。
Caddy 会自动将相关的 X-Forwarded-* 请求头转发给 Knocker,以便 /verify 做出认证决定。
该项目包含完整的测试套件
该项目使用 Astral 的 Python 工具链:
uv 用于依赖管理、环境及命令执行ruff 用于 lint 和格式化ty 用于类型检查要在本地运行测试:
安装 uv:
curl -LsSf https://astral.sh/uv/install.sh | sh
同步项目环境:
uv sync --all-groups
运行检查:
uv run pytest
uv run --group lint ruff check .
uv run --group lint ruff format --check .
uv run --group type ty check
dev 目录下有一个开发环境,包含用于与 Caddy 集成测试的 bash 脚本,以及另一个用于 Firewalld 的脚本。
标准测试栈是 dev/docker-compose.yml 和 dev/docker-compose.ci.yml;两者都在 http://localhost:18080 和 https://localhost:18443 上暴露 Caddy。
CI 运行 Caddy 测试,但 Firewalld 需要特权 runner,因此它需要在本地运行,不属于 CI 的一部分。
交互式文档端点(/docs、/redoc、/openapi.json)默认被禁用。要暴露它们,请在 knocker.yaml 中设置以下内容:
documentation:
enabled: true
openapi_output_path: "openapi.json"
当文档被禁用时(默认状态),Knocker 会移除这些端点,并删除任何先前生成的 schema 文件,以防止陈旧的工件残留。
有关正式的 API 规范以及架构选择的总结,请参阅文档。
Knocker 完全是 vibe 编码的。 初始实现使用 Gemini 2.5 pro 完成,感谢 roo code/requesty 黑客松提供的 token。
后续功能大多由 GitHub Copilot Agent(sonnet 4/后来的 4.5)完成,这需要大量的修复,主要依靠 GPT-5 mini/CODEX(在 Roo code、Opencode 和标准 Copilot 扩展中)完成。
我尽力而为,始终在每次更改后规划变更并进行测试,但如果您反对 AI,我可能无法改变您的看法。
caddy