端口扫描工具的实现

# 端口扫描工具的实现:从零构建一个并发 TCP 端口扫描器

> 端口扫描是网络探测与安全审计的基石。本文不依赖现成的 Nmap,而是用 Python 从零实现一个并发 TCP 端口扫描工具,覆盖原理、架构、并发调度、服务指纹识别与可运行的完整代码,并配有原理与架构图。

## 目录

1. [端口扫描是什么](#1-端口扫描是什么)
2. [原理基础:端口与 TCP 三次握手](#2-原理基础端口与-tcp-三次握手)
3. [常见的扫描方式对比](#3-常见的扫描方式对比)
4. [工具架构设计](#4-工具架构设计)
5. [核心实现(Python 完整代码)](#5-核心实现python-完整代码)
6. [扫描主流程](#6-扫描主流程)
7. [性能与并发优化](#7-性能与并发优化)
8. [进阶方向](#8-进阶方向)
9. [总结](#9-总结)

---

## 1. 端口扫描是什么

**端口(Port)** 是传输层用来区分不同网络服务的逻辑编号,范围 0–65535。一台主机上可能同时运行 Web、SSH、数据库等多个服务,它们各自绑定一个端口。端口扫描的目的,就是**探测目标主机上哪些端口处于开放状态**,从而推断其上运行了哪些服务,常用于:

- **安全审计**:检查自身网络对外暴露了哪些不必要的服务,缩小攻击面。
- **渗透测试**:作为信息收集的第一步,为后续漏洞探测定位目标。
- **运维巡检**:确认服务是否正常监听、端口是否被防火墙误拦。
- **网络资产盘点**:摸清网段内主机与开放端口分布。

一个基本的端口扫描器,本质上是**对目标每个端口发起一次连接尝试,并根据响应判定其状态**。

---

## 2. 原理基础:端口与 TCP 三次握手

TCP 是面向连接的协议,建立连接需要经过 **三次握手(Three-way Handshake)**。端口扫描正是利用握手过程来判断端口状态:

1. 客户端发送 `SYN` 报文,请求建立连接;
2. 服务端若在该端口监听,会回复 `SYN + ACK`,表示"同意连接";
3. 客户端再回复 `ACK`,连接正式建立。

借助这一过程,我们可以在应用层通过一次 `connect()` 调用的结果来判定端口状态——这就是 **TCP Connect 扫描** 的核心原理:

![TCP 三次握手与端口开放判定](https://multipost-s3.2some.ren/filehosting/pm1sasto5w5ofuh8jzxn91jo!style=imagePreview)

> **判定的核心**:`connect()` 成功返回 → 端口**开放**;连接被拒绝或超时 → 端口**关闭**(Closed)或被防火墙**过滤**(Filtered)。

---

## 3. 常见的扫描方式对比

不同扫描方式在"是否完成完整握手、是否留下连接痕迹、是否需要系统权限"上各有取舍:

| 扫描方式 | 原理 | 完成握手 | 隐蔽性 | 权限要求 | 适用场景 |
| --------------------- | ------------------------------ | ---- | ----------- | -------------------- | --------------------- |
| **TCP Connect** | 完整三次握手 | 是 | 低(留下完整连接日志) | 普通用户 | 通用、实现最简单,本文采用 |
| **SYN 半开扫描** | 只发 SYN,收到 SYN+ACK 即判定开放,不发 ACK | 否 | 较高 | 需要 root / RAW Socket | 快速探测、减少连接痕迹 |
| **FIN / NULL / Xmas** | 发送异常标志位报文,根据响应差异判定 | 否 | 较高 | 需要 root | 穿透部分简单防火墙(对多数系统不可靠) |
| **ACK 扫描** | 只发 ACK,探测防火墙过滤规则 | 否 | 中 | 需要 root | 探测防火墙状态,无法判定端口是否开放 |
| **UDP 扫描** | 发送 UDP 报文,依据 ICMP 端口不可达响应 | 否 | 中 | 部分需 root | 探测 UDP 服务(DNS、SNMP 等) |

**本文采用 TCP Connect 扫描**——它不需要特殊系统权限,实现清晰,足以说明端口扫描工具的完整工程结构;半开扫描等进阶思路放在第 8 节。

---

## 4. 工具架构设计

把端口扫描器拆成清晰的模块,便于维护与扩展。整体分为四层:

![端口扫描工具模块架构](https://multipost-s3.2some.ren/filehosting/cyq6wrikq87oe7ux1po09fvf!style=imagePreview)

- **配置 / CLI 层**:解析命令行参数,确定目标主机、端口范围、超时与并发数。
- **扫描引擎**:核心调度逻辑,负责并发分配端口探测任务,控制并发度与超时。
- **网络交互层**:真正执行 `TCP Connect` 探测、读取服务 Banner 的底层操作。
- **结果处理**:对探测结果做服务指纹识别与格式化输出(控制台 / JSON 文件)。

分层的好处是:换一种探测方式(如 SYN 半开)只需替换"网络交互层",调度与输出逻辑完全复用。

---

## 5. 核心实现(Python 完整代码)

### 5.1 技术选型

- **语言**:Python 3,标准库即可,零第三方依赖。
- **并发**:`concurrent.futures.ThreadPoolExecutor` 线程池,简单可靠。
- **目标解析**:`ipaddress` 库处理 IP / 网段,`socket` 解析主机名。
- **超时**:`socket.settimeout()` 防止单端口探测卡死。

### 5.2 完整代码

```python
#!/usr/bin/env python3
"""
port_scanner.py —— 一个并发 TCP 端口扫描器

用法:
python port_scanner.py 192.168.1.1
python port_scanner.py 192.168.1.0/24 --ports 1-1000
python port_scanner.py example.com --ports 22,80,443,3306 --timeout 2 --json result.json
"""
import argparse
import concurrent.futures
import ipaddress
import json
import socket

# 常见服务端口 -> 服务名映射,用于快速标注已知端口
COMMON_PORTS = {
21: "FTP", 22: "SSH", 23: "Telnet", 25: "SMTP", 53: "DNS",
80: "HTTP", 110: "POP3", 135: "RPC", 139: "NetBIOS", 143: "IMAP",
443: "HTTPS", 445: "SMB", 3306: "MySQL", 3389: "RDP",
5432: "PostgreSQL", 6379: "Redis", 8080: "HTTP-Alt", 9200: "Elasticsearch",
}


def parse_targets(targets):
"""解析目标:支持 IP、IP/掩码网段、主机名。返回 IP 字符串列表。"""
result = []
for t in targets:
if "/" in t: # 网段,如 192.168.1.0/24
result.extend([str(h) for h in ipaddress.ip_network(t, strict=False).hosts()])
else:
try:
result.append(str(ipaddress.ip_address(t))) # 单个 IP
except ValueError:
result.append(socket.gethostbyname(t)) # 主机名 -> IP
return result


def parse_ports(ports_spec):
"""解析端口范围:支持 '1-1000'、'22,80,443' 或两者混合。"""
ports = set()
for part in ports_spec.split(","):
part = part.strip()
if "-" in part:
lo, hi = part.split("-")
ports.update(range(int(lo), int(hi) + 1))
else:
ports.add(int(part))
return sorted(ports)


def grab_banner(sock, timeout):
"""读取服务 Banner,用于指纹识别。"""
sock.settimeout(timeout)
try:
sock.sendall(b"\r\n")
return sock.recv(256).decode("utf-8", errors="ignore").strip()
except Exception:
return None


def probe(host, port, timeout):
"""对单个端口执行 TCP Connect 探测,返回结构化结果。"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = {"port": port, "state": "closed", "service": None, "banner": None}
try:
if sock.connect_ex((host, port)) == 0:
result["state"] = "open"
result["service"] = COMMON_PORTS.get(port, "unknown")
result["banner"] = grab_banner(sock, timeout)
sock.close()
except socket.timeout:
result["state"] = "filtered" # 超时,可能被防火墙丢弃
sock.close()
except Exception:
sock.close()
return result


def scan_host(host, ports, timeout, workers):
"""并发扫描单个主机的一组端口,返回该主机的开放端口列表。"""
open_ports = []
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
futures = {ex.submit(probe, host, p, timeout): p for p in ports}
for fut in concurrent.futures.as_completed(futures):
r = fut.result()
if r["state"] == "open":
open_ports.append(r)
return host, sorted(open_ports, key=lambda x: x["port"])


def main():
parser = argparse.ArgumentParser(description="并发 TCP 端口扫描器")
parser.add_argument("targets", nargs="+", help="目标:IP / 网段 / 主机名")
parser.add_argument("--ports", default="1-1024", help="端口范围,如 1-1000、22,80,443")
parser.add_argument("--timeout", type=float, default=1.0, help="单端口连接超时(秒)")
parser.add_argument("--workers", type=int, default=100, help="并发线程数")
parser.add_argument("--json", help="将结果输出到 JSON 文件")
args = parser.parse_args()

ips = parse_targets(args.targets)
ports = parse_ports(args.ports)
print(f"目标主机数: {len(ips)},端口数: {len(ports)},并发线程数: {args.workers}")

all_results = {}
for ip in ips:
host, opens = scan_host(ip, ports, args.timeout, args.workers)
all_results[host] = opens
for r in opens:
line = f" [OPEN] {host}:{r['port']:<5} {r['service']}"
if r["banner"]:
line += f" banner={r['banner'][:60]}"
print(line)

if args.json:
with open(args.json, "w", encoding="utf-8") as f:
json.dump(all_results, f, ensure_ascii=False, indent=2)
print(f"结果已写入 {args.json}")


if __name__ == "__main__":
main()
```

### 5.3 关键实现要点

- **`connect_ex()` vs `connect()`**:`connect_ex()` 用返回码而非抛异常表示结果,`0` 即连接成功,免去异常捕获的繁琐,适合高频探测。
- **并发模型**:`ThreadPoolExecutor` 把每个端口探测任务丢进线程池,`as_completed` 一边收结果一边处理,主线程不阻塞。
- **超时兜底**:`settimeout()` 保证单端口最多等待 `timeout` 秒,避免因丢包导致整轮扫描卡死;超时统一归为 `filtered`。
- **Banner 识别**:连接成功后发送一个空行或协议触发指令,读取服务返回的第一段文本,作为指纹线索(如 SSH 会回显 `SSH-2.0-OpenSSH_...`)。
- **结果结构化**:每条记录统一为 `{port, state, service, banner}`,便于后续序列化成 JSON 或接入其他系统。

---

## 6. 扫描主流程

工具的完整执行流程自顶向下推进:

![端口扫描主流程](https://multipost-s3.2some.ren/filehosting/bn300opxhwpre56xi03uim6b!style=imagePreview)

1. **输入目标**:用户给定 IP、网段或主机名及端口范围;
2. **目标解析**:把目标拆成 IP 列表 + 端口列表;
3. **并发调度**:由线程池按并发度分发探测任务;
4. **端口探测**:对每个端口执行 TCP Connect;
5. **结果判定**:根据响应归为开放 / 关闭 / 过滤;
6. **服务指纹识别**:对开放端口抓 Banner;
7. **结果输出**:控制台打印或写入 JSON 文件。

每一步职责单一、边界清晰,新增功能(如存活主机探测、速率控制)可在对应步骤扩展。

---

## 7. 性能与并发优化

端口扫描是典型的 I/O 密集任务——大多数时间都花在等待网络响应上,CPU 几乎不参与。因此**并发是提升扫描速度的关键**。以扫描 1000 个端口为例,不同并发方式的耗时差异巨大:

![不同并发方式扫描耗时对比](https://multipost-s3.2some.ren/filehosting/dvxt25588qi026134mvkol54!style=imagePreview)

- **单线程串行**:逐个等待,1000 个端口即使全部秒回也要耗时数十秒,慢在网络往返的串行等待上;
- **多线程(ThreadPoolExecutor)**:用线程池同时发起数百个连接,速度提升一个数量级;
- **asyncio 协程**:单线程内用事件循环管理海量 socket 连接,开销更低,在大规模扫描时进一步缩短耗时。

### 并发数如何取舍

- **并发过低**:浪费带宽,扫描慢;
- **并发过高**:触发目标防火墙限速、占用本地文件描述符(`socket` 资源)耗尽,甚至造成网络拥塞。生产工具通常支持**可配置并发数 + 速率限制(rate limiting)**,并加入随机延迟降低被检测概率。

---

## 8. 进阶方向

| 方向 | 说明 |
| ------------- | ----------------------------------------------------------------------------------------------------- |
| **SYN 半开扫描** | 用 RAW Socket 构造并发送 `SYN` 报文,收到 `SYN+ACK` 判定开放、`RST` 判定关闭,不完成握手,更隐蔽高效;需要 root 权限,跨平台需用 `scapy` 或底层套接字。 |
| **服务指纹库** | 将 Banner 与已知服务特征库比对,甚至主动发送多种探测载荷,识别更精确的服务与版本。 |
| **全端口 / 分布式** | 扫描 1–65535 全端口时,配合多主机/多线程分布式调度,缩短整体时长。 |
| **异步 I/O** | 用 `asyncio` 替换线程池,管理上万并发连接,降低线程切换与内存开销。 |
| **防御对抗** | 增加探测随机化、低慢速、报文伪造等,降低被入侵检测系统(IDS)标记的概率——需在授权范围内使用。 |

> ⚠️ **合规提醒**:对未经授权的目标进行端口扫描属于探测行为,在多数地区可能违反法律法规。请务必只在**自己拥有或有明确书面授权**的网络范围内使用,或用于自身资产的合规巡检。

---

## 9. 总结

本文从端口扫描的原理出发,梳理了常见扫描方式,并用 Python 从零实现了一个结构清晰、可运行的并发 TCP 端口扫描器。核心要点:

- **原理**:利用 TCP 三次握手的 `connect()` 结果判定端口开放/关闭/过滤;
- **架构**:配置层、扫描引擎、网络交互层、结果处理四层分离,便于替换探测方式;
- **并发**:I/O 密集任务用线程池/协程显著提速,需平衡并发度与稳定性;
- **指纹识别**:抓取 Banner 推断服务,为安全审计提供更丰富的信息。

在此基础上,可以继续向 SYN 半开扫描、服务指纹库、异步 I/O 等方向演进,把它打磨成功能完整的网络探测工具。

posted @ 2026-10-22 00:06  源动力cn  阅读(5)  评论(0)    收藏  举报