Python socket端口扫描连接耗时极长问题排查与解决
问题复现
我编写了一个基础端口扫描脚本,代码与GeeksforGeeks站点提供的示例高度相似,代码如下:
#!/bin/python3 import socket import sys from datetime import datetime # 定义扫描目标 if len(sys.argv) == 2: target = socket.gethostbyname(sys.argv[1]) # 将主机名解析为IPv4地址 else: print("Invalid amount of arguments") print("Syntax: python3 scanner.py <ip>") sys.exit() # 打印扫描横幅 print("-" * 50) print("Scanning target " + target) print("Time started: " + str(datetime.now())) print("-" * 50) try: for port in range(50,85): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) socket.setdefaulttimeout(1) print("Time starting port {}: ".format(port) + str(datetime.now())) result = s.connect_ex((target, port)) # 返回错误标识 if (result == 0): print("Port {} is open".format(port)) s.close() except KeyboardInterrupt: print("Exiting program") sys.exit() except socket.gaierror: print("Hostname could not be resolved") sys.exit() except socket.error: print("Couldn't connect to server.") sys.exit()
脚本运行输出如下:
-------------------------------------------------- Scanning target 142.251.32.110 Time started: 2022-06-11 12:33:43.256562 -------------------------------------------------- Time starting port 50: 2022-06-11 12:33:43.256649 Time starting port 51: 2022-06-11 12:35:53.061330 Time starting port 52: 2022-06-11 12:35:54.063653 Time starting port 53: 2022-06-11 12:35:55.065565 Time starting port 54: 2022-06-11 12:35:56.067881 Time starting port 55: 2022-06-11 12:35:57.084706 Time starting port 56: 2022-06-11 12:35:58.089606 Time starting port 57: 2022-06-11 12:35:59.090590 Time starting port 58: 2022-06-11 12:36:00.485674 Time starting port 59: 2022-06-11 12:36:02.077196 Time starting port 60: 2022-06-11 12:36:03.078435 Time starting port 61: 2022-06-11 12:36:04.147746 Time starting port 62: 2022-06-11 12:36:05.148526 Time starting port 63: 2022-06-11 12:36:06.159334 Time starting port 64: 2022-06-11 12:36:07.161831 Time starting port 65: 2022-06-11 12:36:08.229387 Time starting port 66: 2022-06-11 12:36:09.340348 Time starting port 67: 2022-06-11 12:36:10.427094 Time starting port 68: 2022-06-11 12:36:11.434983 Time starting port 69: 2022-06-11 12:36:12.466602 Time starting port 70: 2022-06-11 12:36:13.879363 Time starting port 71: 2022-06-11 12:36:14.948457 Time starting port 72: 2022-06-11 12:36:16.176371 Time starting port 73: 2022-06-11 12:36:17.211928 Time starting port 74: 2022-06-11 12:36:18.319003 Time starting port 75: 2022-06-11 12:36:19.321525 Time starting port 76: 2022-06-11 12:36:20.323844 Time starting port 77: 2022-06-11 12:36:21.325633 Time starting port 78: 2022-06-11 12:36:22.338753 Time starting port 79: 2022-06-11 12:36:23.340196 Time starting port 80: 2022-06-11 12:36:24.343861 Port 80 is open Time starting port 81: 2022-06-11 12:36:24.365744 Time starting port 82: 2022-06-11 12:36:25.372192 Time starting port 83: 2022-06-11 12:36:26.382037 Time starting port 84: 2022-06-11 12:36:27.387056
注:通常我会扫描自身局域网内的主机,为避免额外变量影响测试结果,本次选择扫描谷歌的服务器,在自有局域网内测试也存在完全相同的问题。
从输出可见,第一个端口的连接耗时长达2分钟,其余每个端口连接耗时约1秒,但根据教程视频中的演示,扫描50-85端口范围几乎可以瞬时完成。
运行环境为VirtualBox配置的NAT网络模式下的Kali虚拟机,跟随伦理黑客教学视频操作时遇到了这个问题。
已排查信息
- 已排除操作系统问题:在局域网内其他多台计算机上测试该脚本均存在相同问题,若代码逻辑无错则大概率为路由器相关问题,当前网络设备大多保持默认配置,使用的宽带运营商为FibrOp。
- 临时规避方案(效果仍不理想):将原代码中创建socket、设置超时的两行调换顺序,同时缩短超时时长,修改后代码片段如下:
socket.setdefaulttimeout(0.01) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
调换顺序后超时规则可对首个连接生效,缩短超时时长可避免每个端口等待1秒,用户可根据连接目标的网络情况进一步调小超时值。但该方案下的扫描速度仍远慢于教程演示效果,因此不作为最终解决方案。
- 速度差异原因推测:测试发现扫描127.0.0.1/localhost时的扫描速度与教程演示完全一致。因此推测教程演示端口扫描时,是通过有线以太网直连被扫描的路由器设备,WiFi环境下同步端口扫描无法达到该速度。
问题根因与修复方案
根因1:代码逻辑错误导致首个端口超时不生效
原代码中先创建socket对象,再调用socket.setdefaulttimeout(1),而默认超时配置仅对配置创建后新生成的socket对象生效,首个socket创建时未加载超时规则,因此会使用系统默认的TCP连接超时(通常为120-240秒),这就是第一个端口卡顿2分钟的直接原因。
根因2:串行扫描模式本身效率极低
原代码采用单线程串行扫描逻辑,每个端口必须等待上一个端口连接完成(要么连接成功、要么触发超时)才会开始下一个端口的探测,哪怕把超时设为1秒,扫描35个端口最少也需要35秒,自然达不到瞬时完成的效果。教程里的快速扫描本质是用了并发扫描逻辑,同时发起多个端口的连接请求。
最终修复代码
将超时设置移到循环外、socket创建逻辑之前,同时引入线程池实现并发扫描,调整后扫描同网段端口可达到毫秒级完成的效果:
#!/bin/python3 import socket import sys from datetime import datetime from concurrent.futures import ThreadPoolExecutor, as_completed # 定义单端口扫描逻辑 def scan_port(target, port): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(0.5) result = s.connect_ex((target, port)) s.close() if result == 0: return port return None except: return None if len(sys.argv) == 2: target = socket.gethostbyname(sys.argv[1]) else: print("Invalid amount of arguments") print("Syntax: python3 scanner.py <ip>") sys.exit() print("-" * 50) print("Scanning target " + target) print("Time started: " + str(datetime.now())) print("-" * 50) open_ports = [] try: # 用30个线程并发扫描,可根据网络情况调整线程数 with ThreadPoolExecutor(max_workers=30) as executor: futures = [executor.submit(scan_port, target, port) for port in range(50,85)] for future in as_completed(futures): res = future.result() if res: open_ports.append(res) print(f"Port {res} is open") except KeyboardInterrupt: print("\nExiting program") sys.exit() except socket.gaierror: print("Hostname could not be resolved") sys.exit() except socket.error: print("Couldn't connect to server.") sys.exit() print(f"Scan finished. Open ports: {sorted(open_ports)}")
额外网络层面优化
- 虚拟机网络优先选择桥接模式,NAT模式会额外经过一层虚拟网络地址转换,会增加连接延迟;
- 扫描公网目标时速度受运营商网络QoS、目标主机防火墙策略影响,不可能达到本地回环、局域网有线直连的速度,属于正常现象;
- 部分运营商默认会对高频端口探测做流量限制,公网扫描时可适当降低并发线程数、调高超时阈值,避免被限流。
内容的提问来源于stack exchange,提问作者Logan MacDougall

