端口扫描并不神秘:对 TCP 来说,最直接的做法就是依次尝试建立连接,再把成功建立连接的目标记为“开放”。Security Toolkit 的实现正是一个适合学习的最小版本——逻辑透明、行为可解释,也明确暴露了顺序扫描的成本。
从主机名到 IPv4 地址
输入可能是 localhost、域名或 IPv4 地址。程序先通过 socket.gethostbyname() 得到一个 IPv4 地址,再创建 AF_INET 与 SOCK_STREAM 组合的 TCP socket。这意味着当前实现没有处理 IPv6,也不会枚举一个域名背后的所有地址。
解析失败和连接失败应被区分:前者表示目标名称无法转换为地址,后者则只是某个端口没有在给定时间内完成连接。
connect_ex 返回了什么
Python 的 connect_ex((host, port)) 不会在普通连接失败时直接抛出异常,而是返回系统错误码。返回 0 代表 TCP 连接建立成功;其他值可能对应拒绝连接、超时或网络不可达。
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
probe.settimeout(0.5)
is_open = probe.connect_ex((address, port)) == 0
with 保证每次尝试结束后关闭 socket。否则扫描较大范围时,文件描述符会逐渐耗尽。
超时是一种取舍
超时过长会让不可达端口拖慢整个过程;超时过短又可能把高延迟网络中的开放端口判断为未开放。Security Toolkit 对普通范围使用约半秒超时,对一组常见端口使用更短的检查窗口。这是交互速度与准确性之间的工程取舍,不是协议保证。
由于实现按端口顺序逐个等待,最坏耗时近似为:
| 条件 | 影响 |
|---|---|
| 端口数量增加 | 总耗时近似线性增加 |
| 大量端口超时 | 比立即拒绝连接更慢 |
| 网络延迟升高 | 短超时更容易漏报 |
为什么要验证端口范围
TCP 与 UDP 端口号只有 0–65535,而面向用户的扫描通常将可用目标限制为 1–65535。程序还需要保证起始端口不大于结束端口。先验证输入可以避免创建无意义连接,也让错误信息比底层 socket 异常更清楚。
结果应该怎样理解
开放结果只说明三次握手在超时内完成。常见端口名称表可以提供“可能是 HTTP、SSH 或 DNS”的提示,但端口号不是服务身份。生产级工具还会考虑并发限制、服务探测、IPv6、速率控制和更细致的错误分类。
这个最小实现的价值在于把完整链路展示出来:解析地址、创建 socket、设置超时、尝试连接、解释返回码、关闭资源。理解这些步骤后,再讨论并发优化才有意义。