
https://docs.fnknock.cn/origin/why-knock
为什么做敲门 knock
买 NAS 的人,初衷往往是把照片、文档和私有服务掌握在自己手里。可一旦将设备裸露在公网,它的性质就变了。对你而言那是家庭数据中心;但在自动化扫描器眼中,那只是一个处于活跃状态的 IP、一组开放的端口,以及一扇没关严的门。
敲门 knock 的初心从来不是“再造一个反向代理面板”或堆砌花哨的配置项。我们只为解决一个基础但长期被忽视的问题:在飞牛 OS 面向公网之前,先把防盗门装上,再谈访问体验。
在直连模式下,我们仍然支持将公网访问收敛至独立的 7999 端口,在系统底层阻断外部对其他业务端口的直连尝试。用户完成身份验证后,系统才会通过动态白名单机制,放行当前受信任客户端的 IP。但它更适合少量必须保留原始端口访问的场景,不推荐作为多数新部署的首选。
我们不再去猜测某个 HTTP 请求“像不像攻击”,而是默认拒绝所有未经授权的连接。这种 Deny-by-default(默认拒绝)机制,从网络架构层面直接熔断了诸如 /app-center-static 越权访问这类漏洞的触发链。
这样的零信任机制的设计思维,使我们默认准备暴露出去的服务本身是存在漏洞的,基于这样的前提而设计的knock,我们能肯定及确信的说,即便你继续安装旧版本存在漏洞的飞牛OS,也仍然是安全的。
https://github.com/kci-lnk/fn-knock-turborepo
据对代码的详细分析,这个项目确实用到了 iptables,但不是直接在 Node.js 中调用 iptables 命令,而是通过一个 Go 后端服务 作为中间层来操作 iptables。下面是完整的架构梳理:

iptables 防火墙层(通过 Go 后端)
项目在 iptables 中创建了两个自定义链:

Go 后端提供的 iptables API(见 go-backend.ts):
initIptables() → 初始化自定义链和规则
allowIP(ip) → 放行 IP(白名单)
blockIP(ip) → 阻断 IP
removeIP(ip) → 移除 IP 规则
blockTCPPortForIP(ip, port) → 阻断特定端口的 IP 访问
syncSSHFirewall() → 批量同步 SSH 防火墙策略
cleanIptables() → 清除所有 fn-knock 相关规则
三大攻击检测机制
SSH 安全服务 (ssh-security/service.ts)
检测 SSH 暴力破解和非法访问,直接写入 iptables 阻断
实时监控 SSH 登录日志(支持 journalctl 和 auth.log)
封禁原因:
failed_login_threshold — 失败登录次数超限(如5分钟内失败N次)cidr_not_allowed — 即使登录成功,但 IP 不在允许的 CIDR 地域范围内
封禁后调用 goBackend.syncSSHFirewall() 将 blocked IPs 写入 FN-KNOCK-SSH 链
支持自动过期(定时器到期后自动解封)
HTTP 扫描检测 (scan-detector.ts)
检测恶意扫描行为,应用层阻断
分析请求路径,判断是否为"非常见路径"
利用 Redis 有序集合记录每个 IP 的可疑访问记录
在配置的时间窗口内(默认5分钟),如果非正常路径访问次数超过阈值(默认5次),则加入黑名单
黑名单存在 Redis 中,通过 isBlacklisted() 方法在请求进入时判断是否阻断
这是应用层阻断,不走 iptables,而是在 Node.js 请求处理层面拒绝
登录退避保护 (login-backoff.ts)
防止登录接口被暴力破解
基于 Redis 记录每个 IP 的登录失败次数
使用指数退避算法(2^attempts × baseDelay + 随机抖动)
基础延迟 2 秒,最大延迟 1 小时,最大尝试次数 8 次
超过 8 次失败后触发"硬阻断"(shouldHardBlock)
这也是应用层阻断,在认证逻辑中检查 ensureNotBlocked()
白名单机制 (whitelist-manager.ts)
白名单 IP 通过 iptables 放行:
支持三种目标类型:IP、CIDR(如 192.168.1.0/24)、CNAME(域名,自动解析为 IP)
添加白名单时调用 goBackend.allowIP(target) 写入 iptables FN-KNOCK-FW 链
移除时调用 goBackend.removeIP(target) 从 iptables 删除
支持手动添加和登录成功后自动添加(source: "auto")
自动过期机制(Redis 有序集合跟踪过期时间)
原理:

项目用到了 iptables。 SSH 安全和主防火墙的 IP 阻断最终是通过 iptables 的自定义链实现的(间接通过 Go 后端调用),而 HTTP 扫描检测和登录退避则是在 Node.js 应用层通过 Redis 状态管理实现的。
目的 IP 阻断方式是:
iptables 层:通过 Go 后端直接操作 iptables 自定义链(FN-KNOCK-FW 和 FN-KNOCK-SSH),每个 IP 直接作为独立的 iptables 规则添加
应用层:通过 Redis 维护黑名单状态
这意味着当前每个被封禁的 IP 都是一条独立的 iptables 规则。如果被封禁的 IP 数量较大(比如几百上千),这种逐条规则的方式会比 ipset 效率低——ipset 的优势在于将大量 IP 放入一个集合,然后 iptables 只需一条规则匹配该集合即可,内核层面的查找复杂度是 O(1) 哈希查找,而逐条规则则是线性匹配。
Go 后端的核心实现在另外一个项目中:
https://github.com/kci-lnk/Go-Reauth-Proxy
Go 后端是通过直接调用 iptables / ip6tables 命令行来操作防火墙的,完全没用 ipset。具体方式:
逐条规则操作(FN-KNOCK-FW 链)
// 允许 IP — 每个IP一条规则m.runTable(table, "-I", m.Chain, insertPos, "-s", ip, "-j", "ACCEPT")// 阻断 IP — 每个IP一条规则m.runTable(table, "-I", m.Chain, insertPos, "-s", ip, "-j", "DROP")// 移除规则 — 删除ACCEPT和DROP两条m.runTable(table, "-D", m.Chain, "-s", ip, "-j", "ACCEPT")m.runTable(table, "-D", m.Chain, "-s", ip, "-j", "DROP")
批量重建(FN-KNOCK-SSH 链)
SSH 安全链用了 iptables-restore 做批量写入,这比逐条高效一些,但仍然不是 ipset:
func(m *Manager) applyTCPPortAccessPolicy(table string, policy TCPPortAccessPolicy) error {var builder strings.Builderbuilder.WriteString("*filter\n")builder.WriteString("-F " + chain + "\n") // 先清空// 逐条写入 localCIDRs → blockRules → allowRules → defaultActionfor _, cidr := range localCIDRs { ... }for _, source := range blockRules { ... }for _, source := range allowRules { ... }builder.WriteString("COMMIT\n")m.runTableRestore(table, builder.String()) // 一次性 commit
当前架构的潜在问题

FN-KNOCK-SSH 链因为用了 iptables-restore 全量重建,暂时还能应对。但 FN-KNOCK-FW 链的白名单管理是逐条 -I / -D,当白名单 IP 数量较多时,每条规则都是独立的 iptables entry,匹配效率会下降。如果后续要优化,引入 ipset 是一个方向。
大部分人对 iptables 都比较抗拒,特别是混合了Docker的情况。这个项目的前后端都比较复杂,如果要部署到内网,理解起来可能会比较费劲。
我两年前也写了一个基于 iptables 的放行应用,非常简单,可以对照着看一下。
https://github.com/hyang0/ip_allow

"Knock 敲门"提供了一个非常漂亮的前端页面,感兴趣的可以去搜一下它的官网。

全文完。
夜雨聆风