核心摘要
- UDP 是无连接协议,不能通过“连接成功”直接判断端口是否存活。
- Categraf
net_response插件最初要求配置发送内容和期望返回内容,这对很多 UDP 服务并不通用。 - ncat 的 UDP 探测思路是持续写入探测字节,并通过是否收到
ECONNREFUSED判断端口不可达。 - Categraf 的实现可以借鉴 ncat:发送
X,在超时时间内观察写入错误,避免强依赖对端必须返回指定字符串。
背景:为什么 UDP 端口探活容易误判
商业客户反馈使用 categraf 的 net_response 插件配置 udp 探测时遇到报错,如图:

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 的。

问题就落到了一个更本质的判断上:在不理解具体业务协议、也不要求服务端返回指定内容时,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;
}
这段代码的核心逻辑很简单:
- 先向目标写入一个
X。 - 如果写入失败并出现
ECONNREFUSED,认为端口没有打开。 - 如果没有出现
ECONNREFUSED,继续根据timeout等待并重复写入。 - 如果在等待过程中仍然没有遇到
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 能探通、但原插件误判的场景。