跳转到内容
启涵的小破站
返回

端口探测笔记

手上有几百个端口要挨个确认后面到底是什么东西。第一轮我用的是最笨的办法,挨个 connect 一遍,能连上的算”在线”。跑完就发现这结论没什么用:一大堆端口连接成功,可真发请求过去,一个字节都不回。

后来把探测拆成几层,从最便宜的试到最贵的,才慢慢能给每个端口下个说得过去的判断。这篇记一下这套流程。


connect 成功说明不了什么

三次握手能完成,只说明对端有人接了这个包。中间挂一层转发的话,转发程序自己就能把握手做完,后端是死是活它根本不关心。

所以 connect() 成功是所有证据里最弱的一条。

写起来就一行:

import socket

s = socket.create_connection(("example.com", 25565), timeout=5)

timeout 一定要给。不给的话,碰上丢包的 IP 会卡在系统默认超时上,几百个端口跑一天都跑不完。

分层试探

顺序按代价排,便宜的能解决就不往下走。

什么都不发,先听听

有些协议的服务端自己会先开口。SSH、SMTP、FTP、MySQL 都是这样,连上就吐一段 banner,里面常常直接写着版本号。

timeout 5 nc -v host 22

有些 nc 版本要加 -w,拿不到东西就换 printf '' | nc host 22 试试。banner 抓到的字符串往往比后面所有探测加起来都管用。

发一个 HTTP 请求

覆盖面最大的一步。就算后面不是 HTTP 服务,回什么错误也有信息量。

curl -sS -m 8 -i http://host:8888/ | head -20

-i 把响应头带上,-m 限时。返回 400 也许是服务在,只是不喜欢这个请求;返回一串 HTML 就是明牌了。连接直接被拒或者超时,说明这层没有 HTTP。

TLS 握手

HTTPS 或者别的 TLS 服务,光握手就能拿到证书。证书里的 CN 和 SAN 经常直接写着后面是谁,不用猜。

openssl s_client -connect host:443 </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

带 -servername host 和不带各试一次。不少服务不带 SNI 时会发默认证书,两张证书不是一回事。

UDP

前面三步全在 TCP 上。如果 TCP 一路拒绝,而申报写的是 UDP 服务,探测方式得整套换掉。

UDP 没有连接这回事,没回包和”这里本来就没服务”在协议层面是同一件事。要判断,只能按目标协议的报文格式发对,再等回包。

批量跑的时候

几百个端口,一个个手敲不现实。

并发别开太大。线程池给个二三十就够,管道里塞太多连接容易在转发侧被限速,运气差还会被当成扫描处理。探测脚本本身长得就跟扫描器一模一样,节奏控制不住就是在给自己找麻烦。

超时要分开设。connect 超时和读超时不是一回事:connect 卡住说明路不通,读超时说明路通了但对面不理你,这两种要分开记,别混成一个”失败”。

结果文件里把每一层的原始返回都留下,不要只写最终判定。我第一版只记了个 ok/fail,后来想回头统计”有多少是连得上但不吭声的”,翻不出来了,只能重跑一遍。

应用层协议才给得出结论

上面几步只能告诉你”这像是个 HTTP 服务”或者”这像是个 TLS 服务”。要确认具体是什么,得按它自己的协议说话。

拿 Minecraft 举例,它有个专门用来问状态的协议,Server List Ping。

流程很短:

  1. 握手包:packet id 0x00,后面跟协议版本(VarInt)、服务器地址(字符串)、端口(无符号短整型)、下一状态(VarInt,填 1 表示只问状态)
  2. 紧接着发状态请求:id 0x00,空包
  3. 服务端回一段 JSON

那段 JSON 里有版本号、MOTD、在线人数、最大人数。版本号是硬证据,转发层编不出这个。要么它真在跟一个 MC 服务端说话,要么后面什么都没有。

# 骨架,VarInt 编码和省掉的读包逻辑自己补
handshake = b"\x00" + varint(47) + encode_str(host) + port.to_bytes(2, "big") + varint(1)
s.sendall(with_len(handshake) + with_len(b"\x00"))
resp = read_packet(s)
print(resp[1:].decode())   # {"version":{"name":"..."},"players":{...}}

Bedrock 版是另一套,走 UDP 的 RakNet unconnected ping:一个 0x01,跟 8 字节时间戳,再跟 16 字节固定 magic,最后 8 字节 client GUID。回包是 0x1c,里面是分号隔开的字符串。

那串 magic 是 00ffff00fefefefefdfdfdfd12345678,写死就行。

零响应的那批怎么判

全套跑完还是没反应,也不止一种可能,别急着下结论:

能拉开差距的是几个侧面信息。

一个是 connect 的耗时。本地转发和跨洋的真实服务,握手延迟差得很明显。

一个是连接结束的方式。RST 秒回通常意味着对端内核里压根没有监听,握完手晾着不动的更像转发层。

还有个笨办法,拿同一个节点上其他端口做对照。同一个转发服务商的一批端口如果表现完全一样(连接成功、之后完全不响应),基本能判定是同一类配置问题,而不是每个端口各自有事。

几个反直觉的地方

面板上写”在线”,看的是端口能不能连上,跟后面有没有服务是两件事。

端口开着、服务活着、后面真是什么服务,这是三件独立的事,别合并成一个判断。

UDP 端口”探测不通”这个说法本身就不成立,UDP 没有连接,你得先知道该发什么。

最后,别只做一次采样就下结论。网络抖动能让活着的服务判死,也能让挂掉的后端看着还行。我在这个问题上翻过车,拿单次结果做对比,得出的结论后来被自己推翻了。

这套流程后来我一直在用,从最便宜的试探开始一层层往下,多数端口在第二步就有答案。剩下不吭声的那些,交给应用层协议去问。


分享本文:

下一篇
Linux 安装指北

加载评论区中...