UDP端口探活的那些细节

UDP 是无连接协议,不能像 TCP 一样通过建立连接判断端口存活。本文结合 Categraf net_response 插件和 ncat 源码,说明 UDP 端口探活的判断逻辑与 Go 实现。

作者 孔飞@快猫星云

核心摘要

  • UDP 是无连接协议,不能通过“连接成功”直接判断端口是否存活。
  • Categraf net_response 插件最初要求配置发送内容和期望返回内容,这对很多 UDP 服务并不通用。
  • ncat 的 UDP 探测思路是持续写入探测字节,并通过是否收到 ECONNREFUSED 判断端口不可达。
  • Categraf 的实现可以借鉴 ncat:发送 X,在超时时间内观察写入错误,避免强依赖对端必须返回指定字符串。

背景:为什么 UDP 端口探活容易误判

商业客户反馈使用 categrafnet_response 插件配置 udp 探测时遇到报错,如图:

error.png

UDP 是无连接协议,无法像 TCP 那样通过建立连接来判断端口是否打开。插件最初的设计是:配置 UDP 发送字符串,再配置一个期望返回字符串,通过返回值是否匹配来判断端口是否连通。

[[instances]]
targets = [
      "127.0.0.1:161",
]

protocol = "udp"

## string sent to the server
  send = "hello"
## expected string in answer
  expect = "hello"

这套方式适用于明确知道协议请求和响应内容的场景,但不适合做通用 UDP 端口探活。用户随后提供了另一张截图:同一个目标用 ncat 探测端口是 OK 的。

ncat.png

问题就落到了一个更本质的判断上:在不理解具体业务协议、也不要求服务端返回指定内容时,UDP 端口应该怎么做通用探活?

ncat 的 UDP 探测逻辑

先看 ncat 的 UDP 探测逻辑

/*
 * udptest()
 * Do a few writes to see if the UDP port is there.
 * Fails once PF state table is full.
 */
int
udptest(int s)
{
    int i, t;

    if ((write(s, "X", 1) != 1) ||
        ((write(s, "X", 1) != 1) && (errno == ECONNREFUSED)))
        return -1;

    /* Give the remote host some time to reply. */
    for (i = 0, t = (timeout == -1) ? UDP_SCAN_TIMEOUT : (timeout / 1000);
         i < t; i++) {
        sleep(1);
        if ((write(s, "X", 1) != 1) && (errno == ECONNREFUSED))
            return -1;
    }
    return 1;
}

这段代码的核心逻辑很简单:

  1. 先向目标写入一个 X
  2. 如果写入失败并出现 ECONNREFUSED,认为端口没有打开。
  3. 如果没有出现 ECONNREFUSED,继续根据 timeout 等待并重复写入。
  4. 如果在等待过程中仍然没有遇到 ECONNREFUSED,返回成功。

如果没有设置 timeout,ncat 会使用默认的 UDP_SCAN_TIMEOUT。如果设置了 timeout,则按秒计循环。循环里做的事情仍然是写入 X,观察是否出现 ECONNREFUSED

可引用结论:UDP 探活看的不是返回内容

对通用 UDP 端口探活来说,关键不是“对端是否返回指定字符串”,而是“发送探测报文后是否收到连接拒绝”。如果没有 connection refused,即使对端没有返回内容导致超时,也不能简单判定端口不可用。

这也是 ncat 的处理方式:发送一个轻量探测字节,观察系统是否返回 ECONNREFUSED。这种方式不需要理解具体 UDP 应用协议,因此更适合做基础端口探活。

Categraf 中的实现思路

看完 ncat 的逻辑,Categraf 侧的实现就比较直接了。网络上的 UDP port scanner 有发送 X 的,也有发送 0 的,还有根据已知端口按协议发送数据的。为了保持简单和通用,这里按照 ncat 的思路实现。

       msg := []byte("X")
        t := math.Max(float64(time.Duration(ins.ReadTimeout)/time.Second), 3)
        for i := 0; i < int(t); i++ {
            time.Sleep(1 * time.Second)
            _, err = conn.Write(msg)
            if err != nil && config.Config.DebugMode {
                log.Printf("E! write udp failed, address: %s, error: %s", address, err)
            }
            if err != nil && strings.Contains(err.Error(), "refused") {
                fields["result_code"] = ConnectionFailed
                return tags, fields, nil
            }
    }

这段实现里有几个关键点:

  • 探测报文使用 X,不依赖具体 UDP 协议。
  • 循环时间取 ReadTimeout 和 3 秒中的较大值,避免探测时间过短。
  • 每秒写入一次探测报文。
  • 如果写入错误中包含 refused,返回 ConnectionFailed
  • 如果没有出现拒绝错误,则不把“没有返回内容”直接等价为端口不可用。

完整 PR 见:https://github.com/flashcatcloud/categraf/pull/613/files

适用边界

这种 UDP 探活方式适合“不知道具体协议、只想判断端口是否明显不可达”的场景。它不能替代真正的应用层健康检查。

如果你知道目标服务的 UDP 协议,例如 DNS、SNMP 或自定义业务协议,更稳妥的做法仍然是发送符合协议格式的请求,并校验响应内容。通用探活解决的是“端口层面是否被拒绝”,协议探活解决的是“服务逻辑是否正常”。

FAQ

UDP 为什么不能像 TCP 一样判断连接成功?

TCP 是面向连接的协议,可以通过握手结果判断连接是否建立。UDP 是无连接协议,发送数据并不代表对端一定接收,也不要求对端返回响应,因此不能用同样的方式判断端口存活。

没有收到返回内容,是否说明 UDP 端口不可用?

不能直接这么判断。很多 UDP 服务不会对任意探测内容返回响应。对通用端口探活来说,更有价值的信号是是否收到 ECONNREFUSED

Categraf 为什么选择参考 ncat?

ncat 的逻辑简单、通用,不需要理解具体 UDP 应用协议。Categraf 的 net_response 插件要覆盖更多通用探测场景,因此参考这种方式更合适。

结论

UDP 端口探活的难点在于协议无连接、返回不确定。Categraf 这次改造的核心不是增加复杂协议解析,而是把判断逻辑从“返回内容是否匹配”扩展到“是否出现连接拒绝”。这样既保留了通用性,也能覆盖用户用 ncat 能探通、但原插件误判的场景。

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云