news 2026/9/25 3:54:13

用 nftables 集合与 timeout 实现 SSH 端口敲门:让公网扫描器无门可敲

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 nftables 集合与 timeout 实现 SSH 端口敲门:让公网扫描器无门可敲

SSH端口暴露在公网上,每天被各类扫描器来回捶打,日志里全是暴力尝试记录,这是很多运维心里的痛。“端口隐藏”这件事,本质上不是把服务藏起来,而是改变攻击面:对外表现为“端口不存在”或“拒绝连接”,只有掌握敲门序列的调用方才能临时获得访问通道。这篇文章我会用 nftables 自带的集合(set)与超时(timeout)机制,实现一套不需要额外守护进程的端口敲门方案,让 SSH 或其他管理端口在公网扫描下快速“隐身”。这套方案很适合被扫描问题困扰的运维、对安全感兴趣的开发者,以及想把手头防火墙规则升级为现代 nftables 方案的朋友。不需要太复杂的前置知识,只要能看懂基本的 nftables 规则,就能照着落地。

1. 端口隐藏到底在解决什么问题

1.1 为什么只依赖改端口不够

很多朋友第一反应是把 SSH 从 22 改成 2222,以为这样可以躲开扫描。但全端口扫描工具根本不挑口,几十秒就能把全部 65535 个端口扫一遍,2222 同样会暴露在结果里。现在不少扫描器会优先扫描高频端口段,但哪怕你的端口不在高频段里,有心人拿着常见的“常见端口清单”扫完也能很快覆盖掉。改端口的真实成本很低,对应的防护收益也有限。

相比之下,端口隐藏做得更彻底:探测者看到的不只是一个“高端口”,而是这个端口根本不存在、完全拒绝服务。敲门技术就是实现这种“不存在感”的一种手段——服务端只信任那些能够证明自己“按顺序敲过门”的地址。所以这篇博文的核心目标很直接:把 22 端口从“暴露”变成“需要验证后才暴露”。

1.2 nftables 为什么适合承载这套逻辑

从内核层面来说,nftables 是当前推荐的 netfilter 前端,继承了 iptables 的钩子能力,但把规则、集合、映射的抽象做得更清晰。尤其是集合(set)支持超时(timeout)特性,这几乎就是为敲门状态机量身定做的:我们可以把敲过第一道门的来源 IP 放进集合 A 并标记 10 秒存活,敲过第二道门的放进集合 B,依次往下,最终完成整个序列的 IP 才被放入放行集合。整个过程不需要写一堆 echo iptables 命令拼成的剧本,也不需要一个常驻进程去 tail 日志,规则的更新由 nftables 内核自身完成。

这个优势在流量大的服务器上非常明显。iptables-legacy 规则多时是线性遍历,而 nftables 的 set 是可以走哈希查找的;更关键的是,flags timeout让元素自动过期,IP 白名单不会越积越多。列个简单对比:

维度iptables-legacynftables
查找性能规则多时线性遍历明显集合可哈希查找,更快
集合超时回收需借助 ipset 和外部定时清理原生支持 timeout 自动过期
配置可读性传统写法混乱,参数难记类似脚本语言,层次清晰
规则更新原子性批量替换有加载窗口nft -f 整体加载,原子生效

我在很早以前就用过 iptables 加 ipset 的“动态放行”方案,维护成本实在不低:要定时清空 ipset 残留条目,还要写 crontab 配合。nftables 直接把这些能力做进了语法里,这才是现代防火墙该有的样子。

1.3 攻击面治理的价值观

端口隐藏属于“攻击面最小化”的一环,但我不建议把它当成银弹。它解决的是批量扫描、预探测、自动化攻击成本极低的场景,面对有人盯着你的流量来玩,普通敲门的序列可能被观察并被重放。所以我的观点很明确:端口隐藏是纵深防御的第一道伪装,不是最后一道锁。SSH 本身必须用密钥认证,账号要禁用密码登录,fail2ban 作为补充,再加上这套敲门,才是一个合理的组合。这篇博文的核心是讲清楚 nftables 敲门怎么做,但它真正值钱的地方在于你对攻击面模型的认知——每一步减少暴露,都是在给攻击者增加成本。

2. 环境准备与基础规则骨架

2.1 系统与版本要求

我测试的环境是 Debian 12 / Ubuntu 22.04,nftables 1.0+,Linux 内核 5.15+。新一点的系统基本都自带 nftables,但为了保险,可以这样装:

apt update apt install -y nftables systemctl enable nftables

先确认版本:

nft --version

如果输出类似nftables v1.0.2 (Lester Gooch #4),那就正常。还需要确认内核模块已经加载:

lsmod | grep nf_tables

没有输出就手动加载:

modprobe nf_tables

另外,做这套实验时需要理解 TCP 状态跟踪,所以nf_conntrack内核模块也建议确认存在。一般来说,发行版默认都会加载,不用太操心。> 注意不要一上来就用生产服务器做实验,先在测试机器或云主机上把流程跑通,再把规则改造成自己生产环境的样子。

2.2 一台“有底线的”服务器骨架

实验前,先给自己留一条后路。在本地物理控制台、带外管理面板,或者 screen/tmux 里保持一个 root 会话,防止规则一上就把自己锁在外面。绝不要先用policy drop然后测试到一半断线。

我自己的习惯是先写一个临时保底命令:

at now +5 minutes <<< 'nft flush ruleset'

如果 5 分钟内没出事,我再把这个计划任务删除。这个方法虽然土,但比被锁在门外干瞪眼强多了。

初始化一张干净的骨架规则集。先清空再加载:

nft flush ruleset nft -f /etc/nftables.conf

基础配置文件可以写成这样:

#!/usr/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority filter; policy drop; ct state established,related accept iif "lo" accept ct state invalid drop } chain forward { type filter hook forward priority filter; policy drop } chain output { type filter hook output priority filter; policy accept } }

这里解释几个关键点。inet表同时覆盖 IPv4 和 IPv6,所以不需要建两张表;ct state established,related accept的作用是保证已经建立的连接不会被新的默认策略掐断,这对 SSH 掉线问题有很大帮助;ct state invalid drop会把畸形包直接丢掉,减少无意义的告警。至于forward链,默认 drop 是给这台机器当路由器场景兜底,如果你只是普通服务器,也可以暂时不定义 forward 链。

2.3 检查规则是否生效

加载后,立刻开一个新的 SSH 窗口,确认新连接能连上。能连上再执行:

nft list ruleset

看到三条链和policy drop说明基本框架起来了。此时外部的新连接会被丢弃,但已经建立的 SSH 不会断,因为这条连接命中了established规则。

在排查和调整期间,我强烈建议背下来这条临时候补命令:

nft add rule inet filter input tcp dport 22 accept

万一规则写错了、自己又被挡在门外,只要还有带外通道或 tmux 里的 root 会话,就可以执行这条命令把 SSH 放行出来。

3. 敲门技术原理与方案选型

3.1 敲门流程拆解

敲门技术的模型像一个机关门:门上有三个按钮,必须按照 7000 → 8000 → 9000 的顺序各按一次,且间隔不能太久,门才会开;按错顺序或超时,一切从头再来。放到网络里,这个“按钮”就是 TCP 连接尝试。

攻击者扫到 7000 端口,他看到的只是一个拒绝服务;但服务端的 nftables 其实已经暗地里把他的 IP 标记为“已按过第一下”;如果他在很短时间内又去碰 8000,就被标记为“已按过第二下”;当第三次碰到 9000 时,SSH 端口才对他临时开放。

整个过程涉及三个关键设计:

  • 序列唯一性:门铃端口不要用连续端口,也尽量别用容易被猜到的数字;
  • 窗口期:相邻两次敲门的时间差必须落在规定窗口内,否则状态作废;
  • 放行后的过期时间:白名单不是永久生效,到期自动回收,避免长期挂着一个放行状态。

3.2 三种主流实现路线对比

先看第一梯队:knockd。这是老牌敲门工具,配置在/etc/knockd.conf里,指定sequence = 7000,8000,9000,动作可以调用nft命令添加规则。优点是简单可靠,网上资料也多;缺点是引入一个额外的守护进程,它通过 libpcap 抓取数据包,在高并发流量下有一定开销,而且它跟 nftables 之间是外部命令联动,状态不透明。

第二条路线:日志加脚本。从 sshd 日志或 nftables 日志里看到特定端口访问,再用 awk/sed 提取 IP,执行 nft 命令加入白名单。这条方案因为简单,常出现在老博客中,但它有两个天然问题:日志洪泛可以拖垮解析脚本;从日志到添加规则有几秒延迟,敲门体验很差。我自己早期试过,后来放弃了。

第三条路线:纯 nftables 集合状态机,也就是本文主推方案。本质是让 netfilter 在匹配到敲门包时,直接对指定集合做add动作,并附带一个超时时间。这个机制不需要额外进程、不依赖日志、操作是内核原子性的,性能好、可靠性高。

三种方案对照:

方案依赖维护成本性能安全性
knockd额外守护进程中高并发时一般基于源 IP,序列可被观察
日志+脚本crontab/sed/nft高低依赖日志解析,延迟明显
nftables 内置集合仅内核 nftables低高与 knockd 类似,可扩展加密

3.3 集合 timeout 机制就是临时通行证

nftables 里的flags timeout集合会在每个元素创建时绑定一个存活时间,到期后自动删除,不需要外部定时器。这个机制被我用作“按过门的临时状态”:第一次敲门产生的标记在 10 秒后消失,所以你没在窗口期内继续敲第二个端口,就重新开始。

用生活类比解释最直观:保安手里有一叠“临时通行券”,他看到你按了第一道门铃,就往你手上贴一张 10 秒有效的票;你没在 10 秒内按第二道门铃,票自动作废。你每过一道门,票会在上一个的基础上换成一张新票;最后一次换到的票让你能进 SSH 房间,而且这张票的有效期是 1 小时。这种由内核自动回收的设计,不会留下越来越多的无用 IP 条目,也大大减少白名单维护工作量。

4. 核心实现:nftables 端口敲门完整配置

4.1 定义三段式状态机规则集

直接给一份可以放到/etc/nftables.conf的配置。门铃序列我用 7000、8000、9000 演示,SSH 端口是 22,各位根据实际环境替换。窗口期 10 秒,最终放行时间为 1 小时。

#!/usr/sbin/nft -f flush ruleset table inet knock_demo { # 第一道门的临时通行证 set knock_stage1 { type ipv4_addr flags timeout } # 第二道门的临时通行证 set knock_stage2 { type ipv4_addr flags timeout } # 最终白名单 set ssh_allowlist { type ipv4_addr flags timeout } chain input { type filter hook input priority filter; policy drop; ct state established,related accept iif "lo" accept ct state invalid drop # 白名单内 IP 直接放行 SSH tcp dport 22 ip saddr @ssh_allowlist accept # 第一击:碰 7000 → 把来源 IP 放进 stage1 tcp dport 7000 add @knock_stage1 { ip saddr timeout 10s } reject with tcp reset # 第二击:仅在 stage1 里有记录的 IP 碰 8000 → 移入 stage2 tcp dport 8000 ip saddr @knock_stage1 add @knock_stage2 { ip saddr timeout 10s } reject with tcp reset # 第三击:仅在 stage2 里有记录的 IP 碰 9000 → 进入白名单,有效期 1 小时 tcp dport 9000 ip saddr @knock_stage2 add @ssh_allowlist { ip saddr timeout 3600s } reject with tcp reset # 其余一律丢弃 counter drop } }

关键点逐条解释。

首先,三个集合用type ipv4_addr。如果你的服务器有 IPv6 公网地址,不能漏掉这个维度,后面我会单独讲双栈改法。其次,放行 SSH 那条规则放在敲门规则之前没有问题,因为@ssh_allowlist默认是空集,外部没有敲过门的 IP 不会匹配它,会继续往下走,最终走到counter drop。第三,所有敲门动作最后都以reject with tcp reset结束,客户端会立刻收到 RST,甚至会觉得“端口关闭”,但实际上服务端已经偷偷记下状态。

4.2 为什么用 reject 而不是 drop 或 accept

这里值得细说。如果使用drop,对端 TCP 会一直等待连接超时,客户端看到的是“连接超时”,敲一次门可能要等两三秒,敲三次门就要十几秒,对本地体验影响不小。如果使用accept,对端会完成 TCP 握手,扫描器会认为 7000 端口“开放”,这显然和“端口隐藏”的初衷矛盾。

使用reject with tcp reset则模拟了一个真实关闭的端口:既不暴露,又能让敲门工具快速结束。这是一个很小但影响体验的细节,很多教程照抄 drop,实际操作时明显感觉到卡顿。

4.3 加载与验证

把配置写进/etc/nftables.conf后执行:

nft -f /etc/nftables.conf

然后立刻开一个新 SSH 窗口验证原来的 SSH 是不是被保护住了。如果新窗口连不进去,没关系,这正是预期效果。接着查看规则集:

nft list ruleset

再用一台客户端模拟敲门:

for port in 7000 8000 9000; do nc -z -w1 your_server_ip "$port" done ssh your_server_ip

理想情况下,前三次 nc 都返回“拒绝连接”,但第三次之后 ssh 已经能连上。为了确认集合内的状态,可以同时开一个终端执行:

nft list set inet knock_demo ssh_allowlist

你会看到 SSH 放行集合里出现了客户端的 IP,并且带有expires时间。更精确的检查命令是:

nft get element inet knock_demo ssh_allowlist { 客户端IP }

它会告诉你这个条目还剩多少秒有效。

4.4 客户端敲门脚本怎么写才顺手

上面用 nc 手动敲三次虽然直观,但日常使用不方便。我会写一个小脚本,放到本地任何可执行路径,比如/usr/local/bin/knock-ssh:

#!/usr/bin/env bash set -euo pipefail SERVER="${1:?usage: knock-ssh <server>}" SEQUENCE=(7000 8000 9000) for port in "${SEQUENCE[@]}"; do nc -z -w1 "$SERVER" "$port" || true done exec ssh "$SERVER"

这样每次连服务器前先执行knock-ssh my-server,几秒后直接进入 SSH。如果想在 Windows 上也能用,用nmap -Pn -p 7000,8000,9000或者配合/dev/tcp都能实现,原理都一样:只要确实发出 TCP SYN 即可,不要求握手成功。

4.5 如果服务器本身是 NAT/云主机怎么办

公网云主机通常直接配置就够了。但如果这台服务器还承担 NAT 网关角色,或者你在内网环境里,还需要在forward链写对应规则。比如要保护的内网机器是 192.168.1.10,那敲门规则涉及的不是input链,而是forward链,集合记录来源 IP,最终放行规则变成tcp dport 22 ip daddr 192.168.1.10 ip saddr @ssh_allowlist accept。思路完全一致,只是链换成forward。建议先拿最简单的直连场景跑通,再往 NAT 迁移。

5. 常见问题与排查技巧实录

5.1 为什么敲完门仍然连不上 SSH

最常见的几个原因按概率排:第一,客户端 IP 和集合记录的 IP 不一致——如果你是经过 NAT 网关或代理访问服务器,“你看到的源 IP”可能不是真正送达服务器的 IP,要确认nft get element inet knock_demo ssh_allowlist里存的地址是什么;第二,SSH 连接被另一层防火墙拦截,比如云平台安全组、fail2ban、SELinux;第三,规则顺序错了,比如放行规则被写到了其它 drop 规则之后。

排查时我习惯先在服务器上用tcpdump -i any port 22看报文有没有到达内核,再逐条检查nft list ruleset,定位是在哪一环断掉。记住一个大原则:先分清是“没到 Linux”还是“到了但被 nftables 拒了”,这能帮你少绕半天弯。

5.2 敲门窗口期到底设多少合适

窗口期太短,手动敲门很容易失败;太长又相当于给了攻击者更多时间利用被劫持的标记。我的经验值:第一道门 10 秒左右,第二道门可以稍长一点,比如 15 秒,最终放行 2 小时。不追求极低的窗口值,因为这套机制主要防批量扫描,而不是防定向攻击。

如果全部用脚本自动敲门,整个过程在 1 秒内完成,10 秒窗口绰绰有余。但如果偶尔需要手动敲,建议把 stage1 和 stage2 的 timeout 都调到 20 秒,把最终白名单调到 8 小时,然后定期用脚本清理。不要为了追求“看起来安全”把窗口压得太死,硬件上的体验会严重恶化。

5.3 有 IPv6 地址该怎么办

如果你只用type ipv4_addr写规则,IPv6 报文完全不会命中这些敲门规则。这样一来,白名单放行规则不响应 IPv6 地址,但默认 drop 策略依然会拦下未放行的 IPv6 流量。真正的问题在于,如果你实际想通过 IPv6 使用 SSH,那 IPv6 的敲门通道就不存在,等于自己把自己的路堵死。

一个实用的双栈改法是把三个集合类型换成ipv6_addr,规则里用meta nfproto ipv6限定:

table inet knock_demo { set knock_stage1 { type ipv6_addr; flags timeout; } set knock_stage2 { type ipv6_addr; flags timeout; } set ssh_allowlist { type ipv6_addr; flags timeout; } chain input { type filter hook input priority filter; policy drop; ct state established,related accept iif "lo" accept tcp dport 22 meta nfproto ipv6 ip6 saddr @ssh_allowlist accept tcp dport 7000 meta nfproto ipv6 ip6 saddr @knock_stage1 accept tcp dport 8000 meta nfproto ipv6 ip6 saddr @knock_stage1 add @knock_stage2 { ip6 saddr timeout 10s } reject with tcp reset tcp dport 9000 meta nfproto ipv6 ip6 saddr @knock_stage2 add @ssh_allowlist { ip6 saddr timeout 3600s } reject with tcp reset } }

如果你只需要 IPv4,那就保持原样,但要确认服务器上的 IPv6 服务已经关闭或没分配公网地址,免得留一个不设防的逻辑出口。

5.4 开机后规则丢了怎么办

规则丢了通常有两个原因:一是你在命令行加载了但没写入配置文件;二是 nftables.service 没有开启。我建议直接把规则写进/etc/nftables.conf,然后:

systemctl enable nftables systemctl restart nftables

用 systemd 管理时,加载失败会在 journal 里留下错误。排查命令:

journalctl -u nftables -n 50

另外要特别注意云平台安全组这个独立于操作系统的控制层。哪怕内核规则写错,只要安全组没放行 22,你依然会被拦住;反过来安全组放行了 22,内核里的policy drop一样会拒绝。排查时要先分清访问到底是在哪一层被掐断的,否则容易原地打转。

5.5 敲门时偶尔失败,重试就好了吗

TCP 的 SYN 重传可能导致同一个包被重复处理,但集合add是幂等的,重复添加不会产生副作用,放心重试即可。唯一值得注意的是:如果你在脚本里每次用nc -z等结果,中间间隔超过窗口期就可能失败。加上-w1这种超时限制后,三次敲门通常在 2 秒内完成,正常不会超时。

如果客户端和服务器之间的网络延迟较高,比如跨洋访问,10 秒窗口也可能不够稳定。此时可以把 timeout 提升到 20 秒,或者改用 hping3 从应用层直接发 SYN 包,可靠性更高。我实测不同网络环境下,10 秒窗口在国内常规线路够用,但从海外回连时建议放宽到 20 秒。

5.6 自检清单

上线前一定过一遍:

  • 新窗口 SSH 还能连上吗(规则刚加载完立刻验证)
  • nft list ruleset里的表名、链名与后续操作一致吗
  • 客户端敲门时,源 IP 是不是服务器上看到的那一个
  • 重启机器或重载 nftables 后配置是否还在
  • IPv6 是否留了一个不设防的口子

6. 从敲门到更安全的玩法

6.1 给规则加计数器和日志

调试和上线观察时,可以在每条敲门规则上加counter和log,这样就能清楚看到谁在敲门。例如:

tcp dport 7000 add @knock_stage1 { ip saddr timeout 10s } log prefix "knock1: " counter reject with tcp reset

但生产环境要小心日志量。有人敲错门不算攻击,可如果高频出现,可能是扫描器正在探测你的敲门端口规律。建议只在调试验证阶段开启 log,上线后保留 counter 即可。counter可以帮你统计每个敲门端口被访问的次数,对判断攻击模式很有帮助。

6.2 敲门序列的口令化

硬编码一组序列有一个明显风险:一旦序列泄露,比如出现在配置备份、shell 历史或被他人抓包看到,保护就形同虚设。因此我强烈建议:把序列放进密码管理器的自定义字段,每部署一台服务器就用不同的一组端口,不要所有机器都设置同一个门铃。

如果需要更高安全等级,可以把“无口令敲门”升级为“加密单包授权”。思路是把序列用对称密钥加密后放进一个 UDP 包,服务端用同一密钥解密、校验时间戳后放行,这样抓包也无法重放。这个方向有现成的 fwknop 实现,感兴趣可以继续深入。

6.3 我踩过的一些坑

第一,千万别在敲门配置里顺手把 SSH 端口改成非标准端口,再叠加这套规则。这样做会增加很多排查复杂度,实际收益有限。第二,reject with tcp reset的语法在高版本 nftables 上是reject with tcp reset,不要在旧版本里直接照抄,先nft --version确认。第三,如果和 fail2ban 共用,你要知道 fail2ban 默认写入的是 iptables-nft 规则,两个工具同时管理同一张表,会让 nftables 规则集变得混乱。我的建议是:新的敲门方案里,不要再用 fail2ban 去管理 SSH,或者把 fail2ban 的 ban 动作改成写 nftables 集合,避免双管理冲突。

第四,从实际运维出发,所有脚本都要考虑“不可达”的场景。敲门脚本本身失败了不要立刻 panic,检查集合里的元素状态比看 nc 输出更可靠。最后,把门铃端口和 SSH 端口分开写进文档,别把两者都猜成 22。我就是因为端口写错,愣是排查了一个晚上,真的很耽误事。这套方案跑通后,你会发现日常的扫描噪音明显减少,运维安全感也提升不少。如果你的场景比我这套更复杂,比如有多个管理端口需要隐藏,完全可以照着同样的思路,在多几个 stage 集合上继续延伸。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 3:54:13

机器学习驱动学生综合能力测评:特征工程与模型落地实践

简介&#xff1a;一套基于机器学习的学生综合能力测试系统&#xff0c;面向教育信息化、智能测评与人工智能应用开发人员。项目以学情数据为依据&#xff0c;尝试将机器学习与深度学习引入学习评估&#xff0c;适合作为理解分类预测、特征工程、模型训练及前后端联动落地的实战…

作者头像 李华
网站建设 2026/9/25 3:52:28

为何我不写政策解读?出租车行业四大替代选题方向

先说明一下&#xff0c;这篇我没有动笔的原因看到“南宁市出租汽车行业发展规划&#xff08;2024-2029&#xff09;”这个选题时&#xff0c;我没有直接按常规流程去拆解标题、搭建博文框架&#xff0c;而是先停下做了一轮内容合规自查。原因不复杂&#xff1a;这类文件属于地方…

作者头像 李华
网站建设 2026/9/25 3:52:04

PX4 MAVLink 标准模式协议:飞行模式的发现、查询与切换全解析

嵌入式物联网机器人自动驾驶智能硬件 【免费下载链接】PX4-Autopilot PX4 Autopilot Software 项目地址&#xff1a; https://gitcode.com/gh_mirrors/px/PX4-Autopilot 点击查看 免费下载 本篇基于 PX4 官方文档 standard_modes.md 整理并深入源码。自 PX4 v1.15 起&#xff…

作者头像 李华
网站建设 2026/9/25 3:50:15

AI小说生成:7分钟装好本地长篇写作工具

AI小说生成&#xff1a;7分钟装好本地长篇写作工具 【免费下载链接】AI_NovelGenerator 使用ai生成多章节的长篇小说&#xff0c;自动衔接上下文、伏笔 项目地址: https://gitcode.com/GitHub_Trending/ai/AI_NovelGenerator AI小说生成工具里&#xff0c;AI_NovelGener…

作者头像 李华