用 Go 编写的现代认证式端口敲击工具集:一个守护进程(knockd)负责监听敲击端口并执行动作,一个客户端(knock)负责执行序列。
cmd/knockd 服务端:敲击端口 → AEAD 认证 → 规则动作 → 延迟清扫
cmd/knock 客户端:发送序列,打印密封响应
与经典 knockd 不同,端口序列本身不携带任何秘密——它只是路由用的装饰:每个数据包仍必须通过基于逐包 HKDF 派生密钥的 XChaCha20-Poly1305 认证。攻击者即便知道端口顺序也一无所获。
English: README.md
| 经典 knockd | knockkit | |
|---|---|---|
| 传输 | TCP 或 UDP | 两者皆可,且可在同一序列内自由混用 |
| 秘密 | 序列本身 | 每客户端 32 字节 PSK,逐包 AEAD |
| 重放 | 未覆盖 | nonce 缓存,失败关闭(fail-closed) |
| 响应 | 无 | 密封状态 + trace id(对外部观察者无 oracle) |
| 动作 | 防火墙开/关 | allow、allow-close、exec、webhook、db-update、log |
| 延迟关门 | 依赖外部 cron | 内置调度器:现在敲门,N 分钟后自动关闭 |
make build
# 1. 密钥材料
./bin/knockd -gen-psk > psk.key # 32 字节随机数,base64
# 2. 服务端配置(参见 examples/knockd.yaml),然后:
./bin/knockd -check -config knockd.yaml # 仅校验
./bin/knockd -config knockd.yaml
# 3. 客户端配置(参见 examples/knock.yaml),然后:
./bin/knock -config knock.yaml -rule ssh -v
ok rule=ssh trace=2b6c6061knock 的退出码:0 接受,1 拒绝/无应答,2 用法错误。
一条规则就是一个有序步骤列表;每个步骤由端口和传输协议组成。步骤之间可以混用传输协议:
rules:
- name: ssh
action: allow
params: {ports: "22", proto: tcp}
steps:
- {port: 7001, proto: udp}
- {port: 7002, proto: udp}
- {port: 7003, proto: tcp} # 末步:密封应答在此返回若某条序列是另一条的前缀(同一数据包会产生两种含义),规则会在启动时被拒绝。敲击的每一步都是一完整、重新封装的数据包——离路(off-path)攻击者无法通过注入单个伪造报文来污染服务端状态,因为其载荷无法通过认证。
动作是服务端白名单;客户端只能选择某条规则,永远无法指定命令、URL 或 SQL 语句。
| 动作 | 用途 | 幂等 |
|---|---|---|
allow |
在防火墙中开放端口(带 TTL) | 是 |
allow-close |
再次关闭它们——最自然的清扫动作 | 是 |
log |
仅写审计日志 | 是 |
exec |
以 argv 数组(无 shell)运行白名单内的可执行文件 | 仅当 allow_as_cleanup: true |
webhook |
POST 到预先声明的 URL,不跟随重定向 | 否 |
db-update |
执行预注册的 SQL 语句并绑定占位符 | 否 |
参数处理规则:
- 配置参数是权威的——可使用
$CLIENT_ID、$SOURCE_IP、$RULE、$TRACE,由服务端展开; - 客户端只有在键被列于该规则的
allow_params中、且动作 schema 接受该键时,才能追加参数(启动时即校验); - 每个参数——无论来自客户端还是配置——都会对照动作 schema 校验;未知键一律拒绝。
cleanup:
after: 3m
action: allow-close
params: {ports: "22", proto: tcp}
policy: refresh # refresh(默认) | ignore
retry: {max_attempts: 5, base_delay: 5s, max_delay: 1m}refresh(默认):重新敲击会重置计时器——只要客户端持续敲门端口就保持开放,并在最后一次敲门 3 分钟后关闭。ignore:首个计时器优先生效;重复敲击不会推迟它。- 任务在计时器启动之前就已持久化(
scheduler.store: file),因此守护进程崩溃不会丢失任何任务;逾期任务会在下次启动时执行。 - 只有幂等动作可用作清扫动作——启动时强制校验。
allow规则上的ttl: 10m正是上述配置块的语法糖。
- AEAD 是安全边界。 对整个数据包使用 XChaCha20-Poly1305;头部(端口、时间戳、客户端 id、标志位)作为附加认证数据(AAD)。
- 逐包密钥: HKDF-SHA256(psk, salt=
knockkit-v1‖nonce, info=aead/knock)——nonce 泄露绝不会暴露 PSK。 - 统一拒绝: 任何认证失败都产生完全相同的
auth: rejected;机器可读的失败原因仅存在于本地指标中。线上不存在任何 oracle。 - 重放防护: nonce 只有在解密成功后才进入缓存,因此没有 PSK 的攻击者无法填满它;缓存有容量上限且失败关闭。
- 先准入控制: 封禁表 → 每 IP 令牌桶 → 全局 PPS → 解析 → AEAD。只有通过限流的流量才会进入开销较高的处理环节。
- 关键路径失败关闭: nonce 缓存已满或敲击执行器饱和时直接丢包,而不是放行未验证的工作。
knockd -check—— 校验配置并退出。- 指标:
metrics.listen暴露/metrics(Prometheus 文本格式)与/healthz。关键序列:knockd_auth_fail_by_reason_total、knockd_knock_ok_total、knockd_cleanup_err_total、knockd_rate_drop_total。 - 每次敲击都会记录一个
traceid,该 id 会回显给客户端并被清扫任务复用,因此一个 id 即可关联动作 A、动作 B 与客户端重试。 - 日志绝不包含密钥材料或密文。
nftables(Linux 默认):表knockkit,带 timeout 的 concat set,优先级-10的链——即使守护进程挂掉,TTL 仍由内核强制执行。iptables:注入到INPUT 1并带knockkit注释;依赖应用层清扫来关门。noop:测试、非 Linux 环境,或防火墙由别处管理。
flush_on_exit: true(默认)会在退出时撤回守护进程添加的全部规则。
cmd/knockd, cmd/knock 二进制文件
pkg/protocol 线格式、params TLV、响应、成帧(已 fuzz)
pkg/auth HKDF、AEAD、时效性、重放缓存、密钥环(已 fuzz)
pkg/seq 每 IP 敲击状态机 + 歧义性校验
pkg/action 动作注册表、schema、内置动作
pkg/firewall nftables / iptables / noop 后端
pkg/scheduler 延迟任务:epoch、持久化、重试
pkg/ratelimit 令牌桶、全局 PPS、封禁表
internal/config 严格 YAML(KnownFields)、默认值、校验
internal/server 管线:准入 → 认证 → 序列 → 执行 → 应答
make test # 单元测试 + 端到端
make race
make vet
make fuzz # 每个解析器 30 秒Go ≥ 1.27(见 go.mod),无 CGO。服务端面向 Linux;客户端跨平台。