UDP 端口探测及shell重定向
需求背景
需要检测服务的某个UDP的端口是否正常。
分析
UDP是一种无状态,无连接的协议,这一知识点牢记我心,所以第一反应就是这探测没办法做了。只能从其他层面想办法,比如说服务增加一个状态检查的rest接口。通过检查rest接口的状态来判断对应的UDP端口的状态。
直到我终于了解到,原来,不仅仅是TCP,当UDP端口未开启监听时,操作系统也会发送ICMP端口不可达报文。ICMP并不是UDP的一部分, 所以这和UDP是无连接,无状态的协议这一说法并不矛盾。之前真是一知半解啊。
方案
总共两步:
- ping对应IP检测对应IP是否正常。
约束:对应节点不能关闭ping响应。防火墙不能过滤ICMP 请求和响应报文 - 发送一个内容为空的UDP报文:
1)如果收到了ICMP端口不可达的回复。则认为端口关闭。
2)如果未收到ICMP端口不可达报文。则有两种可能:
I)端口正常。不回复。
约束:UDP端口对应的程序要能够处理报文内容为空的报文。
II) 防火墙过滤掉了ICMP端口不可达报文
约束:
操作系统不能过滤ICMP端口不可达报文
本身处于同一个局域网,报文传输过程中应该是不会经过防火墙的。
备注:
由于和待检测的节点处于同一个局域网,所以以上约束都是可以实施的。
由于UDP协议本身的不可靠,所以,可以考虑探测多次,只要有一次不成功则认为不成功。
实现
python
有人提供了一个基于python的解决方案:
- 启动抓包程序。
- 发送内容为空UDP报文。
- 判断一定时间内是否抓到对应的ICMP端口不可达报文。
此方案通过抓包的方式实现,显然不够优雅。直觉告诉我,显然应该有更优雅的解决方案。
更重要的是,交付的版本层面上来说,由于涉及到python2和python3的切换,以及如果项目中使用python脚本之后,就需要增加对应的安全扫描之类的工作。总体来说,如果使用python脚本,会增大版本层面的工作量。
java
前段时间,在看这篇文章的时候,看到了这样一段话:
这个ICMP错误消息虽然不属于telnet引发的TCP流,但它跟该TCP流却是RELATED的,而RELATED的流会继承原始流的conntrack结构体表项,这就是问题的根本,如果缺失了这个细节,就会带来错误的判断。
结合现在遇到的问题,我想到,我们在使用tcp的socket连接时,接收到的connection refused信息就是由于操作系统接收到了ICMP端口不可达报文。所以说,这个TCP流收发的ICMP报文,被操作系统送到了同一个socket。
而我们在使用UDP的时候,也是使用的socket连接的,那么UDP是不是也是同样的情况呢?
查找了一番,果然如此。只不过UDP稍微特殊一点:
- 需要明确调用bind()函数
- 并且发送报文的时候不会直接抛出错误,而是在接收报文的时候返回PortUnreachableException
这个方案显然是比python脚本抓包要优雅一些了。但是:
- 还需先检测IP是否能ping通,java代码显然不太方便。
- 等待可能需要设置一定的超时时间,所以可能需要新开线程去处理。
- java代码产生的结果最终需要传递给keepalived, 比较麻烦。
shell脚本
既然java的socket能够搞定这个问题,那么shell脚本的socket能不能搞定这个问题呢?如果能够搞定的话,shell脚本应该就是一个比较优雅的解决方案了。
shell提供了一种建立TCP/UDP连接的方法:
/dev/udp/host/port
/dev/tcp/host/port
所以,直接重定向当前shell的一个文件描述符到对应的ip/端口:exec 8<>/dev/udp/10.0.2.15/12345
就相当于建立一个UDP socket。
发送报文:echo "" >&8
抓包结果:
00:48:18.291124 IP 10.0.2.15.40371 > 10.0.2.15.12345: UDP, length 1
00:48:18.291145 IP 10.0.2.15 > 10.0.2.15: ICMP 10.0.2.15 udp port 12345 unreachable, length 37
由于UDP的无连接性,命令返回的结果依然为成功。
从对应的文件描述符中读取状态:
root@debian2:~# cat <&8
cat: -: Connection refused
当然,直接往流中再次写入数据,也会得到同样的错误:
root@debian2:~# exec 8<>/dev/udp/10.0.2.15/12345
root@debian2:~# echo "" >&8
root@debian2:~# echo "" >&8
-bash: echo: write error: Connection refused
使用完毕之后,关闭对应的流:exec 8>&-
参考:
bash shell 连接socketBash One-Liners Explained, Part III: All about redirections