☰
Python端口扫描脚本:TCP连接检测与多线程并发实战
2026/10/14 7:05:16 网站建设 项目流程

批量探测一批主机的端口开放情况时,最蠢的办法就是一个个敲 telnet 去试。以前我排查某个内网环境,光确认一台机器上 20 多个端口通不通,就折腾了大半天。后来干脆花了一个晚上写了这个 Python 端口扫描脚本,后续再遇到类似问题,基本几分钟就能拿到结果。这篇文章就把这套脚本的完整思路、核心代码、踩过的坑和扩展做法都整理出来,希望能给需要做网络服务检测、内网端口摸底的朋友一点参考。

需要先说明一点:端口扫描技术本质是网络管理员和服务端开发人员排查问题时的常用手段,包括确认服务有没有监听、防火墙策略是否生效、部署完后端口是否按预期开放等。请务必只在你自己有权限、被明确授权的环境中使用,不要拿它去探测未经允许的目标系统。

1. 端口扫描到底在扫什么:需求与原理

1.1 为什么需要自己写一个扫描脚本

很多人第一反应是:有现成的工具,为什么还要自己写?我用这个脚本之前也一直在用现成工具,但实际工作中会遇到几个挺别扭的场景。

第一,内网批量巡检的需求很常见。部署完一套服务,可能要确认几十台机器的某些端口都正常监听,如果一行一行命令手动试,效率很低。脚本可以一次性把多台机器的多个端口全扫完,结果结构化输出,方便后续对比和分析。

第二,现成的工具通常比较重。我需要的是一个轻量、能塞进自己流程里的检查手段,最好能直接作为模块被调用,扫描完还能把结果保存成 JSON、CSV,方便其他脚本消费。

第三,自己写一遍才能真正理解端口探测的原理。我不怕说得直白一点,只有亲手把 socket 连接、超时控制、并发调度这些东西写一遍,对 TCP 协议和网络状态的理解才会真正落地,后面排查网络问题时脑子的反应速度快很多。

1.2 扫描的底层原理:TCP 握手与端口状态

要理解端口扫描,先要理解 TCP 连接建立的过程。一台主机上的某个端口是否开放,本质上是看这个端口上有没有进程在监听 TCP 连接请求。

客户端发起连接时,会经历所谓的三次握手:客户端发送 SYN 包,服务端如果端口开放且接受连接,会回 SYN-ACK;客户端再回 ACK,连接就建立了。如果端口未开放,服务端一般会直接回一个 RST,拒绝连接。如果中间有防火墙拦截,可能什么响应都不回,客户端就只能在超时之后判断为不可达。

从这个角度理解端口状态就很直观了:

  • 端口开放:握手能正常完成。
  • 端口关闭:收到 RST 拒绝,握手失败。
  • 端口被过滤:请求发出后没有任何响应,直到超时。

我们的 Python 脚本做不了底层裸协议包,但借助系统自带的 socket 模块,模仿“连接”这个动作完全可行。connect_ex 这个函数就是最实用的工具,它建立连接后返回 0 则表示成功,返回其他值则代表失败,我们用返回值就能判断端口状态。

1.3 扫描方式选型:为什么选 TCP Connect 扫描

常见的端口扫描方式有好几种,比如 SYN 半开扫描、TCP Connect 全连接扫描、UDP 扫描、ACK 扫描等。我们的脚本选 TCP Connect 扫描,原因是它最稳定、最通用,而且实现起来不需要构造裸数据包。

SYN 半开扫描的优势是速度快、隐蔽性好,但这类扫描通常需要管理员权限,并且需要自己构造 IP 头、TCP 头,Python 的 socket 默认不提供这种能力,除非用更底层的库。对于绝大多数日常检测任务来说,隐蔽性并不重要,重要的是连接能不能建立、服务是否正常应答,所以全连接扫描反而更贴近实际。

TCP Connect 扫描的流程就是直接调用 socket 去连目标端口,连接成功视为开放,连接失败或超时分别对应不同状态。这种扫描方式不会对目标系统造成额外负担,唯一缺点是要完成完整的握手,速度上不如半开扫描。不过配合多线程,速度就已经足够日常使用了。

2. 动手前的设计与选型

2.1 功能边界:目标定得越清楚,代码越简单

写脚本之前,我建议先把需求边界划清楚,否则写的过程中很容易越加越多,最后变成一个四不像。

我这个脚本的核心需求很简单:给定一个或多个目标 IP,给定一个或多个端口,脚本去尝试连接,输出哪些端口开放、哪些关闭、哪些超时。端口开放的情况顺便抓一下 banner,方便判断服务类型。再加一类需求是结果保存成文件,方便对接其他流程。

基于这个边界,我把不做的功能也列出来了:不做操作系统指纹识别,不做漏洞检测,不做 UDP 扫描,不做 SYN 半开扫描。这些功能背后的复杂度和依赖会大幅增加代码量,日常大多数场景根本用不到。

2.2 地址与端口表达方式设计

设计参数格式时,我参考了常见工具的输入方式,同时保留了一点自定义的灵活度。

IP 地址支持三种形式:

  • 单个 IP,例如192.168.1.10
  • 连续范围,例如192.168.1.10-50
  • CIDR 网段,例如192.168.1.0/24

端口同样支持三种形式:

  • 单个端口:80
  • 逗号分隔列表:22,80,443,3306
  • 范围格式:8000-9000

也可以混合使用:22,80,443,8000-9000,解析时先按逗号拆成若干段,再判断每一段是单值还是范围,展开后合并成一个列表。

这种参数设计在写起来时稍微费点功夫,但使用起来是真的方便。比如我要扫一台机器的常见服务端口,直接写-p 21,22,23,25,53,80,110,143,443,3306,3389,8080;要扫一段端口范围,就写-p 8000-9000;要扫整个 C 段,就写-H 192.168.1.0/24。少记很多别扭的索引规则。

2.3 并发模型选择与超时策略

端口扫描最核心的性能问题是串行连接太慢。一个端口扫描时如果超时设置为 2 秒,扫 1000 个端口最坏情况下要 2000 秒,这显然不能接受。

解决方案就是并发。Python 的并发方案主要有线程、进程、协程三种。这里选择线程,理由很简单:任务是网络 IO 密集型的,线程在等待网络响应时会释放 GIL,而且线程的创建和管理开销比进程小很多,代码也直观。

并发数量要设置上限,否则扫一个大网段时可能创建几千个线程,反而把系统资源耗尽。我用ThreadPoolExecutor的max_workers来控制并发数,默认设为 200,用户也可以自己调节。

超时策略同样重要。超时太短,某些响应慢的主机或者高延迟网络下会误报;超时太长,又影响整体速度。默认值我设为 2.0 秒,也允许用户用参数覆盖。局域网环境下 0.5 到 1 秒通常就够,跨网段或目标主机负载高时建议调到 3 秒以上。

3. 核心代码一步步写出来

3.1 参数解析与端口段展开

我用argparse来解析命令行参数,这比手工解析sys.argv[1:]要省心很多,而且自动生成--help帮助信息。

端口段展开是整个脚本里比较有代表性的一个函数,我直接贴出来讲。

def parse_ports(port_str: str) -> list: """ 将端口字符串解析为端口列表。 支持格式: 80 80,443,3306 8000-9000 22,80,443,8000-9000 """ ports = set() parts = port_str.split(",") for part in parts: part = part.strip() if not part: continue if "-" in part: start, end = part.split("-", 1) try: start_p, end_p = int(start), int(end) except ValueError: raise ValueError(f"无法解析端口段: {part}") if start_p < 1 or end_p > 65535 or start_p > end_p: raise ValueError(f"端口段不合法: {part}") for p in range(start_p, end_p + 1): ports.add(p) else: try: p = int(part) except ValueError: raise ValueError(f"无法解析端口: {part}") if 1 <= p <= 65535: ports.add(p) else: raise ValueError(f"端口超出范围: {p}") return sorted(ports)

用集合去重是为了避免参数里出现80,80-90这种重叠写法时产生重复端口。展开后返回排序列表,让扫描结果看起来整齐。

有人可能会问,range(8000, 9001)展开成 1001 个元素,内存会不会爆?实际上 Python 的range在迭代过程中是按需生成数的,所以 65535 个端口全展开也才几十 KB 级别,完全不用但心。

3.2 IP 目标生成与地址解析

IP 地址的解析我分三种情况处理:单个 IP、范围、CIDR。Python 标准库的ipaddress模块可以直接处理 CIDR,但范围形式需要自己拆分。

import ipaddress import sys def parse_hosts(host_str: str) -> list: """ 解析目标主机。 支持格式: 192.168.1.10 192.168.1.10-50 192.168.1.0/24 """ host_str = host_str.strip() if "-" in host_str and "/" not in host_str: # 处理 192.168.1.10-50 这类写法 prefix, last_part = host_str.rsplit(".", 1) if "-" not in last_part: raise ValueError(f"无法解析主机范围: {host_str}") start_str, end_str = last_part.split("-", 1) try: start = int(start_str) end = int(end_str) except ValueError: raise ValueError(f"主机范围数字部分不合法: {host_str}") hosts = [] for i in range(start, end + 1): if 0 <= i <= 255: hosts.append(f"{prefix}.{i}") return hosts else: try: # 单个 IP 或 CIDR network = ipaddress.ip_network(host_str, strict=False) return [str(ip) for ip in network.hosts()] except ValueError: # 尝试单 IP try: ip = ipaddress.ip_address(host_str) return [str(ip)] except ValueError: raise ValueError(f"无法解析主机: {host_str}")

这里有个细节值得说明:ipaddress.ip_network("192.168.1.10", strict=False)也能把单个 IP 当做一个只含一个地址的网段处理,但为了格式更清晰,我还是单独处理单 IP。另外,network.hosts()会排除网络地址和广播地址,如果你想扫描的是包含网络地址的 IP,需要自己拼接计算。

3.3 TCP 扫描函数与存活判定

核心扫描函数的实现并不复杂,但它决定了整个脚本结果的可靠性。

import socket def tcp_connect_scan(host: str, port: int, timeout: float): """ 对指定主机的指定端口进行 TCP 连接测试。 返回状态码:0 表示开放,1 表示关闭,2 表示超时/无响应 """ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: result = sock.connect_ex((host, port)) if result == 0: return 0 else: # 返回码不为 0,且不是超时异常,视为端口关闭 return 1 except socket.timeout: return 2 except OSError as e: # 网络不可达等系统级错误也归入超时/无响应 return 2 finally: sock.close()

注意这里用的是connect_ex而不是connect。connect在连接失败时会抛异常,而connect_ex会把错误码作为返回值返回。用connect_ex的好处是我们不需要频繁捕异常分支,代码更干净。

连接关闭端口时,connect_ex通常会返回类似 111(连接拒绝)这样的错误码;连接超时时被socket.timeout捕获;网络不可达时抛OSError。三种情况对应三种状态,处理逻辑就清晰了。

3.4 服务指纹识别与结果记录

端口开放后,如果能抓到服务的 banner,对判断服务类型和版本非常有帮助。抓 banner 的思路很简单:连接建立后主动发送一行数据或直接等待,服务端一般会回一句欢迎信息或版本信息。

def grab_banner(host: str, port: int, timeout: float) -> str: """ 抓取端口返回的 banner 信息,用于识别服务类型与版本。 抓不到时返回空字符串。 """ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: sock.connect((host, port)) # 有些服务需要先发送一条指令才会响应 try: sock.send(b"\r\n") except OSError: pass try: banner = sock.recv(1024).decode("utf-8", errors="ignore").strip() except socket.timeout: banner = "" return banner except OSError: return "" finally: sock.close()

这里有一个实际操作中的经验:不同的服务对 banner 抓取的反应差异很大。HTTP 服务会等收到完整请求才返回,SSH 服务会主动回版本号,某些 MySQL 服务会报协议错误但能看出版本信息。所以 banner 抓取只作为参考,不要过度依赖。

结果记录方面,我会对每个开放的端口保存一个字典结构,格式大概是{"host": "192.168.1.1", "port": 80, "service": "http", "banner": "..."}。这样后续转 CSV、JSON 都很方便。

3.5 多线程并发和结果收集

多线程部分我用concurrent.futures.ThreadPoolExecutor,提交任务后用as_completed收集结果。这里最关键的一点是任务提交的顺序和完成顺序不一定一致,所以收集结果时需要记录主机和端口的对应信息,不能只看返回值的排列。

from concurrent.futures import ThreadPoolExecutor, as_completed def scan_worker(host, port, timeout): status = tcp_connect_scan(host, port, timeout) banner = "" if status == 0: banner = grab_banner(host, port, timeout) return { "host": host, "port": port, "status": status, "banner": banner } def run_scan(hosts, ports, timeout, max_workers): results = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: future_map = {} for host in hosts: for port in ports: future = executor.submit(scan_worker, host, port, timeout) future_map[future] = (host, port) for future in as_completed(future_map): try: result = future.result() results.append(result) except Exception as e: host, port = future_map[future] results.append({ "host": host, "port": port, "status": 2, "banner": f"扫描异常: {e}" }) # 按主机和端口排序,方便阅读 results.sort(key=lambda x: (x["host"], x["port"])) return results

我特意在每个任务返回的字典里带上主机和端口信息,因为as_completed返回的顺序是任务完成顺序,不是提交顺序,如果不保存原始参数,后面根本没法把结果对应回正确的目标。

4. 完整脚本与运行实测

4.1 完整代码清单

前面把核心函数拆开讲了,这里把完整脚本整合在一起,加上主函数和参数解析,形成一个可以直接跑的版本。

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Python 端口扫描脚本 功能:对指定主机或网段的指定端口进行 TCP 连接测试,识别开放状态并抓取 banner。 示例: python port_scanner.py -H 192.168.1.1 -p 22,80,443 python port_scanner.py -H 192.168.1.10-50 -p 1-1000 python port_scanner.py -H 192.168.1.0/24 -p 80,8080 --timeout 1 --threads 100 -o result.json """ import argparse import ipaddress import json import socket import sys import time from concurrent.futures import ThreadPoolExecutor, as_completed def parse_ports(port_str: str) -> list: ports = set() parts = port_str.split(",") for part in parts: part = part.strip() if not part: continue if "-" in part: start_str, end_str = part.split("-", 1) start_p, end_p = int(start_str), int(end_str) if start_p < 1 or end_p > 65535 or start_p > end_p: raise ValueError(f"端口段不合法: {part}") for p in range(start_p, end_p + 1): ports.add(p) else: p = int(part) if 1 <= p <= 65535: ports.add(p) else: raise ValueError(f"端口超出范围: {p}") return sorted(ports) def parse_hosts(host_str: str) -> list: host_str = host_str.strip() if "-" in host_str and "/" not in host_str: prefix, last_part = host_str.rsplit(".", 1) if "-" not in last_part: raise ValueError(f"无法解析主机范围: {host_str}") start_str, end_str = last_part.split("-", 1) start, end = int(start_str), int(end_str) hosts = [] for i in range(start, end + 1): if 0 <= i <= 255: hosts.append(f"{prefix}.{i}") return hosts else: try: network = ipaddress.ip_network(host_str, strict=False) return [str(ip) for ip in network.hosts()] except ValueError: ip = ipaddress.ip_address(host_str) return [str(ip)] def tcp_connect_scan(host: str, port: int, timeout: float) -> int: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: result = sock.connect_ex((host, port)) return 0 if result == 0 else 1 except socket.timeout: return 2 except OSError: return 2 finally: sock.close() def grab_banner(host: str, port: int, timeout: float) -> str: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: sock.connect((host, port)) try: sock.send(b"\r\n") except OSError: pass try: return sock.recv(1024).decode("utf-8", errors="ignore").strip() except socket.timeout: return "" except OSError: return "" finally: sock.close() def scan_task(host: str, port: int, timeout: float) -> dict: status = tcp_connect_scan(host, port, timeout) banner = "" if status == 0: banner = grab_banner(host, port, timeout) return { "host": host, "port": port, "status": status, "banner": banner } def run_scan(hosts: list, ports: list, timeout: float, max_workers: int) -> list: results = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: future_map = {} for host in hosts: for port in ports: future = executor.submit(scan_task, host, port, timeout) future_map[future] = (host, port) for future in as_completed(future_map): host, port = future_map[future] try: result = future.result() results.append(result) except Exception as e: results.append({ "host": host, "port": port, "status": 2, "banner": f"扫描异常: {e}" }) results.sort(key=lambda x: (x["host"], x["port"])) return results def output_text(results: list): current_host = None for item in results: if item["host"] != current_host: print(f"\n=== 主机 {item['host']} ===") current_host = item["host"] if item["status"] == 0: print(f" [{item['port']:>5}] OPEN {item['banner']}") elif item["status"] == 1: print(f" [{item['port']:>5}] CLOSED") else: print(f" [{item['port']:>5}] TIMEOUT/FILTERED") def main(): parser = argparse.ArgumentParser(description="Python 端口扫描脚本") parser.add_argument("-H", "--hosts", required=True, help="目标主机,支持 单IP / 范围 / CIDR") parser.add_argument("-p", "--ports", required=True, help="端口,支持 单个端口 / 逗号分隔 / 范围") parser.add_argument("-t", "--timeout", type=float, default=2.0, help="连接超时秒数,默认 2.0") parser.add_argument("-T", "--threads", type=int, default=200, help="并发线程数,默认 200") parser.add_argument("-o", "--output", default="", help="结果输出 JSON 文件路径") args = parser.parse_args() try: hosts = parse_hosts(args.hosts) ports = parse_ports(args.ports) except ValueError as e: print(f"参数解析错误: {e}", file=sys.stderr) sys.exit(1) print(f"目标主机: {len(hosts)} 个") print(f"待扫端口: {len(ports)} 个") print(f"超时设置: {args.timeout}s, 并发线程: {args.threads}") print("扫描开始...") start_time = time.time() results = run_scan(hosts, ports, args.timeout, args.threads) elapsed = time.time() - start_time print(f"扫描完成,耗时: {elapsed:.2f}s") output_text(results) open_count = sum(1 for r in results if r["status"] == 0) print(f"\n共发现开放端口 {open_count} 个") if args.output: with open(args.output, "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) print(f"结果已保存到: {args.output}") if __name__ == "__main__": main()

这个脚本保存为port_scanner.py就可以直接运行了。代码全部使用 Python 标准库,不需要额外安装第三方依赖,只要是 Python 3 环境就能跑。

4.2 运行方法与输出解读

运行方式很简单,我用几个常见场景来演示。

扫描单个主机常见服务端口:

python port_scanner.py -H 192.168.1.10 -p 21,22,80,443,3306,3389

输出看起来可能是这样:

目标主机: 1 个 待扫端口: 6 个 超时设置: 2.0s, 并发线程: 200 扫描开始... 扫描完成,耗时: 2.35s === 主机 192.168.1.10 === [ 21] CLOSED [ 22] OPEN SSH-2.0-OpenSSH_9.0p1 Ubuntu-1ubuntu1.3 [ 80] OPEN HTTP/1.1 200 OK [ 443] OPEN [ 3306] CLOSED [ 3389] CLOSED 共发现开放端口 2 个

注意 443 端口虽然显示 OPEN,但 banner 是空的。这是常见的现象,TLS 服务在客户端发送握手信息之前不会主动回任何数据,所以抓到的 banner 就是空字符串。这不代表服务有问题,只要状态是 OPEN,就说明端口确实有进程监听。

扫网段例子:

python port_scanner.py -H 192.168.1.0/24 -p 22,80 -t 1 -T 100 -o result.json

这个命令会扫整个 C 段所有主机的 22 和 80 端口,超时 1 秒,并发 100,结果保存到 JSON 文件。

4.3 参数组合的真实使用场景

我举两个实际用过的场景,帮大家理解参数怎么配合。

场景一:确认某业务组新上线的三台机器端口是否都通了。

机器 IP 是192.168.100.5、192.168.100.6、192.168.100.7,需要确认 8080 和 9090 端口。直接用范围写法:

python port_scanner.py -H 192.168.100.5-7 -p 8080,9090 -t 1

0.5 秒就能出结果,比挨个去 curl 或 telnet 快得多。

场景二:排查一台机器上 8000 到 10000 范围内哪些端口被服务占用。因为要扫的端口很多,超时设置短一点,线程数适当调大:

python port_scanner.py -H 10.0.6.12 -p 8000-10000 -t 0.8 -T 300

大概运行几十秒,输出会按端口从小到大排序,扫完后一眼就能看到哪些端口是开放的,再结合实际服务配置判断有没有异常监听。

5. 常见问题与排查记录

5.1 扫描结果不准?先查超时和重试

我最早写完这个脚本时,扫一台延迟有点高的服务器,发现很多明明开放的服务端口被标成了 CLOSED。排查下来原因是目标服务器负载太高,响应延迟超过了 2 秒,连接被主动超时判定为关闭。

如果你的扫描结果经常不准,第一件事就是确认目标网络的延迟和丢包情况。可以先 ping 一下确认延迟基线,如果延迟本身超过 100ms,建议把超时时间调到 3 秒以上。还有一种情况是网络偶发丢包,某个端口第一次连超时,第二次连就通了。针对这种场景,可以在脚本里增加一个简单的重试机制:状态为 2(超时)的端口重试一次,如果第二次连接成功,则记为开放。

5.2 线程数上去反而变慢

把max_workers从 100 调到 1000 后,扫描速度不一定变快,甚至可能变慢。原因有几个方面。

第一,线程过多导致系统调度开销变大。当上千个线程同时处于等待状态时,操作系统要频繁切换上下文,Python 解释器也要花大量时间管理内部状态,这部分开销会上涨。

第二,目标系统本身有并发连接数上限。当你同时向同一台机器发起太多连接时,目标内核可能开始丢弃部分 SYN 包,结果就是大量端口超时,扫描时间反而更长。

我的建议是,对单个主机的扫描线程数控制在 100 到 300 之间比较合适;对多个主机进行扫描时,可以适当增加线程数,但要避免所有连接集中打到同一台机器。如果有条件,按主机维度做并发隔离更合理。

5.3 Windows 环境下的 socket 兼容问题

这个脚本在 Windows 上运行时,有几个细节需要注意。

第一个是socket.timeout的捕获。在 Windows 下,某些网络操作抛出的异常类型不一定是socket.timeout,可能是OSError且错误码为 10060。我的代码里已经用except OSError兜底捕获,这样兼容性会好一些。

第二个是文件编码问题。Windows 默认编码不是 UTF-8,如果你在脚本里输出中文内容,控制台可能出现乱码。建议在脚本开头声明# -*- coding: utf-8 -*-,同时设置输出的编码格式。我在代码里用ensure_ascii=False写 JSON,就是为了让结果文件里的中文可读。

第三个是防火墙弹窗。Windows 的防火墙有时会拦截 socket 绑定操作,如果脚本运行时报权限相关错误,需要确认防火墙规则放行。

5.4 服务指纹识别不准的修正思路

banner 识别有一个天然的局限:不是所有服务都会在连接后第一时间返回信息。有些服务需要客户端先发送特定的协议数据。我在代码中发送了\r\n作为通用触发,能覆盖一部分 HTTP、SSH、FTP 等常见服务,但对数据库类服务往往作用有限。

如果你想识别 MySQL、Redis 这类服务,可以针对性的构造协议探测数据,比如 MySQL 可以先发送一个协议握手包。但这涉及具体的协议细节,代码复杂度会上来。一个更简单的方案是维护端口与服务名的映射表,把常见端口对应的服务写死,比如 3306 就标 MySQL、6379 就标 Redis;再用实际抓到的 banner 作为补充信息。这样即使抓不到 banner,也能给出一个参考判断。

6. 扩展思路与个人经验

6.1 输出 JSON 对接其他工具

脚本里的 JSON 输出格式是我经过几次调优后确定的,每个对象包含host、port、status、banner四个字段,适合后续程序处理。status用 0、1、2 分别表示开放、关闭、超时,既稳定又能保留状态语义。

json 文件的保存用ensure_ascii=False,这样中文 banner 看起来直观。我实际对接过一个监控脚本,它会定期读取这个 JSON 文件,把端口开放状态变化趋势汇总成本地服务台账,相当实用。

6.2 后续可以加的功能

这个脚本结构上留了很多扩展点,如果你有兴趣,完全可以基于它继续增强。

第一个值得加的是 UDP 扫描。UDP 探测原理和 TCP 不太一样,它依赖 ICMP 端口不可达错误来判断端口状态,Python 标准库的socket也能做,用SOCK_DGRAM创建套接字,方法上要设置较短的重试逻辑。但 UDP 扫描本身就慢很多,一般场景下用得不多。

第二个值得加的是多轮扫描取交集。第一次扫描超时的端口重试一轮,最后只保留在两轮中都开放的端口作为最终结论,能有效降低误报率。

第三个是做一个简单的状态传播:把扫描结果按主机 -> 端口列表的方式聚合,对开放端口做分类统计,比如哪些主机开放了 22 端口,方便之后做统一的安全基线复核。

6.3 合规使用的边界与建议

最后必须再说一遍边界问题。端口扫描这个能力很容易被滥用,但它本身是网络运维和安全测试的常规手段。正确使用姿势是在你自己的服务器、公司分配给你的测试环境或者客户明确授权的渗透测试中执行。脚本里没有任何针对受害系统的特殊设计,代码也不具备隐蔽性或规避检测能力,但正因为如此,更要时刻提醒自己:使用范围永远限制在授权目标内。

我在实际使用中发现,真正有帮助的不是脚本本身,而是观察结果时的思考习惯。比如某次扫到一个内网数据库端口意外对外开放,顺着这个线索排查,发现是防火墙规则配置错误导致端口透传到了公网。如果没有这个脚本,这个问题可能要拖很久才会暴露。从这个角度看,端口扫描不只是探测工具,更是一件检查网络配置一致性的有效辅助手段。

另外,脚本跑完后建议把结果保存一段时间。当以后需要查“某天内网端口是否出现过变化”时,这些历史记录就派上大用场了,运行前备份好数据总没错。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询