pyportscan 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JinGu123123
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyportscan
3
+ Version: 1.2.0
4
+ Summary: A multi-threaded Python port scanner with async support and banner grabbing.
5
+ Author-email: JinGu123123 <gujin123123@outlook.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/JinGu123123/pyportscan
8
+ Project-URL: Issues, https://github.com/JinGu123123/pyportscan/issues
9
+ Keywords: port-scanner,network,security,asyncio
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Security
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # pyportscan
19
+
20
+ 一个用 Python 实现的多线程 TCP 端口扫描器,支持端口范围解析、并发扫描和 Banner 抓取。用于学习网络安全与网络编程。
21
+
22
+ ## 功能
23
+
24
+ - [x] 单端口 / 多端口 / 端口范围扫描
25
+ - [x] 多线程并发加速
26
+ - [x] 灵活的命令行参数(argparse)
27
+ - [x] Banner Grabbing 服务识别
28
+ - [x] 模块化设计,可作为 Python 库使用
29
+ - [x] 单元测试覆盖核心逻辑
30
+ - [x] UDP 扫描(计划中)
31
+ - [ ] 导出扫描结果到 JSON/CSV(计划中)
32
+
33
+ ## 环境要求
34
+
35
+ - Python 3.8+
36
+ - 依赖见 `requirements.txt`
37
+
38
+ ## 开发环境
39
+
40
+ ### Python 部分
41
+ - Python 3.8+
42
+ - pytest(测试)
43
+
44
+ ### C 语言 SYN 扫描(阶段 7.3 准备)
45
+ - WSL2(Windows Subsystem for Linux 2)
46
+ - Ubuntu 26.04 LTS
47
+ - gcc 15.2.0
48
+ - make 4.4.1
49
+ - libpcap-dev
50
+
51
+ #### 在 WSL2 里安装
52
+
53
+ ```bash
54
+ sudo apt update
55
+ sudo apt install -y gcc make libpcap-dev
56
+
57
+ ## 安装
58
+
59
+ ```bash
60
+ git clone git@github.com:JinGu123123/pyportscan.git
61
+ cd pyportscan
62
+ pip install -r requirements.txt
63
+ ```
64
+
65
+ ## 使用
66
+
67
+ ### 基本用法
68
+
69
+ ```bash
70
+ # 扫描本机默认端口(1-1024)
71
+ python main.py 127.0.0.1
72
+
73
+ # 扫描指定端口
74
+ python main.py 127.0.0.1 -p 22,80,443,8000
75
+
76
+ # 扫描端口范围
77
+ python main.py 127.0.0.1 -p 1-1024
78
+
79
+ # 混合格式
80
+ python main.py 127.0.0.1 -p 22,80,8000-8100,3306
81
+
82
+ # 不抓取 banner(更快)
83
+ python main.py 127.0.0.1 -p 1-1024 --no-banner
84
+
85
+ # UDP 扫描
86
+ python main.py 127.0.0.1 -p 53,123,161 --udp
87
+
88
+ # 查看帮助
89
+ python main.py -h
90
+ ```
91
+
92
+ ### 参数说明
93
+
94
+ | 参数 | 说明 | 默认值 |
95
+ |------|------|--------|
96
+ | `host` | 目标主机(IP 或域名,必填) | - |
97
+ | `-p, --ports` | 端口范围 | `1-1024` |
98
+ | `-t, --timeout` | 连接超时(秒) | `1.0` |
99
+ | `-w, --workers` | 并发线程数 | `100` |
100
+ | `--no-banner` | 不抓取服务 banner | 关闭 |
101
+ | `--udp` | 使用 UDP 扫描(默认 TCP) | 关闭 |
102
+ | `-h, --help` | 显示帮助 | - |
103
+
104
+ ### 示例输出
105
+
106
+ ```
107
+ $ python main.py 127.0.0.1 -p 1-1024
108
+ 开始扫描 127.0.0.1,端口数 1024
109
+ 开放端口:
110
+ 135/tcp (无法识别)
111
+ 445/tcp (无法识别)
112
+ 902/tcp 220 VMware Authentication Daemon Version 1.10: SSL Required...
113
+ 912/tcp 220 VMware Authentication Daemon Version 1.0, ServerDaemonProtocol:SOAP...
114
+ ```
115
+
116
+ ## Python API 用法
117
+
118
+ 除了命令行,`pyportscan` 也可以作为 Python 库使用:
119
+
120
+ ```python
121
+ from pyportscan import parse_ports, scan_ports, grab_banner
122
+
123
+ # 解析端口字符串
124
+ ports = parse_ports("22,80,8000-8100") # [22, 80, 8000, ..., 8100]
125
+
126
+ # 扫描端口(不抓 banner)
127
+ results = scan_ports("127.0.0.1", ports, grab=False)
128
+
129
+ # 扫描端口并抓取 banner
130
+ results = scan_ports("127.0.0.1", [8000], grab=True)
131
+ for port, banner in results:
132
+ print(port, banner)
133
+
134
+ # 单独抓取某个端口的 banner
135
+ banner = grab_banner("127.0.0.1", 8000)
136
+
137
+ # UDP 扫描
138
+ results = scan_ports("127.0.0.1", [53, 123], udp=True)
139
+ # [(53, 'open|filtered'), ...]
140
+ ```
141
+
142
+ ## 项目结构
143
+
144
+ ```
145
+ pyportscan/
146
+ ├── pyportscan/ # 核心包
147
+ │ ├── __init__.py # 统一暴露 API
148
+ │ ├── parse.py # 端口范围解析
149
+ │ ├── scan.py # 端口扫描
150
+ │ └── banner.py # banner 抓取
151
+ ├── tests/ # 单元测试
152
+ │ ├── test_parse.py
153
+ │ ├── test_scan.py
154
+ │ └── test_banner.py
155
+ ├── main.py # CLI 入口
156
+ ├── requirements.txt
157
+ ├── README.md
158
+ └── LICENSE
159
+ ```
160
+
161
+ ## 运行测试
162
+
163
+ ```bash
164
+ # 安装测试依赖
165
+ pip install -r requirements.txt
166
+
167
+ # 运行全部测试
168
+ pytest tests/ -v
169
+
170
+ # 只运行某个测试文件
171
+ pytest tests/test_parse.py -v
172
+ ```
173
+
174
+ ## 开发进度
175
+
176
+ - [x] 阶段 0-3:基础扫描 + 并发扫描
177
+ - [x] 阶段 4:命令行工具(argparse)
178
+ - [x] 阶段 5:Banner Grabbing 服务识别
179
+ - [x] 阶段 6:单元测试 + 项目整理
180
+ - [x] 阶段 7.1:UDP 扫描
181
+ - [x] 阶段 7.2:搭建 WSL2 开发环境
182
+ - [ ] 阶段 7.3:C 语言 SYN 半开扫描
183
+ ## 免责声明
184
+
185
+ 本工具仅供学习与授权测试使用。请勿用于未授权的目标,否则后果自负。
186
+
187
+ ## License
188
+
189
+ [MIT](LICENSE)
@@ -0,0 +1,172 @@
1
+ # pyportscan
2
+
3
+ 一个用 Python 实现的多线程 TCP 端口扫描器,支持端口范围解析、并发扫描和 Banner 抓取。用于学习网络安全与网络编程。
4
+
5
+ ## 功能
6
+
7
+ - [x] 单端口 / 多端口 / 端口范围扫描
8
+ - [x] 多线程并发加速
9
+ - [x] 灵活的命令行参数(argparse)
10
+ - [x] Banner Grabbing 服务识别
11
+ - [x] 模块化设计,可作为 Python 库使用
12
+ - [x] 单元测试覆盖核心逻辑
13
+ - [x] UDP 扫描(计划中)
14
+ - [ ] 导出扫描结果到 JSON/CSV(计划中)
15
+
16
+ ## 环境要求
17
+
18
+ - Python 3.8+
19
+ - 依赖见 `requirements.txt`
20
+
21
+ ## 开发环境
22
+
23
+ ### Python 部分
24
+ - Python 3.8+
25
+ - pytest(测试)
26
+
27
+ ### C 语言 SYN 扫描(阶段 7.3 准备)
28
+ - WSL2(Windows Subsystem for Linux 2)
29
+ - Ubuntu 26.04 LTS
30
+ - gcc 15.2.0
31
+ - make 4.4.1
32
+ - libpcap-dev
33
+
34
+ #### 在 WSL2 里安装
35
+
36
+ ```bash
37
+ sudo apt update
38
+ sudo apt install -y gcc make libpcap-dev
39
+
40
+ ## 安装
41
+
42
+ ```bash
43
+ git clone git@github.com:JinGu123123/pyportscan.git
44
+ cd pyportscan
45
+ pip install -r requirements.txt
46
+ ```
47
+
48
+ ## 使用
49
+
50
+ ### 基本用法
51
+
52
+ ```bash
53
+ # 扫描本机默认端口(1-1024)
54
+ python main.py 127.0.0.1
55
+
56
+ # 扫描指定端口
57
+ python main.py 127.0.0.1 -p 22,80,443,8000
58
+
59
+ # 扫描端口范围
60
+ python main.py 127.0.0.1 -p 1-1024
61
+
62
+ # 混合格式
63
+ python main.py 127.0.0.1 -p 22,80,8000-8100,3306
64
+
65
+ # 不抓取 banner(更快)
66
+ python main.py 127.0.0.1 -p 1-1024 --no-banner
67
+
68
+ # UDP 扫描
69
+ python main.py 127.0.0.1 -p 53,123,161 --udp
70
+
71
+ # 查看帮助
72
+ python main.py -h
73
+ ```
74
+
75
+ ### 参数说明
76
+
77
+ | 参数 | 说明 | 默认值 |
78
+ |------|------|--------|
79
+ | `host` | 目标主机(IP 或域名,必填) | - |
80
+ | `-p, --ports` | 端口范围 | `1-1024` |
81
+ | `-t, --timeout` | 连接超时(秒) | `1.0` |
82
+ | `-w, --workers` | 并发线程数 | `100` |
83
+ | `--no-banner` | 不抓取服务 banner | 关闭 |
84
+ | `--udp` | 使用 UDP 扫描(默认 TCP) | 关闭 |
85
+ | `-h, --help` | 显示帮助 | - |
86
+
87
+ ### 示例输出
88
+
89
+ ```
90
+ $ python main.py 127.0.0.1 -p 1-1024
91
+ 开始扫描 127.0.0.1,端口数 1024
92
+ 开放端口:
93
+ 135/tcp (无法识别)
94
+ 445/tcp (无法识别)
95
+ 902/tcp 220 VMware Authentication Daemon Version 1.10: SSL Required...
96
+ 912/tcp 220 VMware Authentication Daemon Version 1.0, ServerDaemonProtocol:SOAP...
97
+ ```
98
+
99
+ ## Python API 用法
100
+
101
+ 除了命令行,`pyportscan` 也可以作为 Python 库使用:
102
+
103
+ ```python
104
+ from pyportscan import parse_ports, scan_ports, grab_banner
105
+
106
+ # 解析端口字符串
107
+ ports = parse_ports("22,80,8000-8100") # [22, 80, 8000, ..., 8100]
108
+
109
+ # 扫描端口(不抓 banner)
110
+ results = scan_ports("127.0.0.1", ports, grab=False)
111
+
112
+ # 扫描端口并抓取 banner
113
+ results = scan_ports("127.0.0.1", [8000], grab=True)
114
+ for port, banner in results:
115
+ print(port, banner)
116
+
117
+ # 单独抓取某个端口的 banner
118
+ banner = grab_banner("127.0.0.1", 8000)
119
+
120
+ # UDP 扫描
121
+ results = scan_ports("127.0.0.1", [53, 123], udp=True)
122
+ # [(53, 'open|filtered'), ...]
123
+ ```
124
+
125
+ ## 项目结构
126
+
127
+ ```
128
+ pyportscan/
129
+ ├── pyportscan/ # 核心包
130
+ │ ├── __init__.py # 统一暴露 API
131
+ │ ├── parse.py # 端口范围解析
132
+ │ ├── scan.py # 端口扫描
133
+ │ └── banner.py # banner 抓取
134
+ ├── tests/ # 单元测试
135
+ │ ├── test_parse.py
136
+ │ ├── test_scan.py
137
+ │ └── test_banner.py
138
+ ├── main.py # CLI 入口
139
+ ├── requirements.txt
140
+ ├── README.md
141
+ └── LICENSE
142
+ ```
143
+
144
+ ## 运行测试
145
+
146
+ ```bash
147
+ # 安装测试依赖
148
+ pip install -r requirements.txt
149
+
150
+ # 运行全部测试
151
+ pytest tests/ -v
152
+
153
+ # 只运行某个测试文件
154
+ pytest tests/test_parse.py -v
155
+ ```
156
+
157
+ ## 开发进度
158
+
159
+ - [x] 阶段 0-3:基础扫描 + 并发扫描
160
+ - [x] 阶段 4:命令行工具(argparse)
161
+ - [x] 阶段 5:Banner Grabbing 服务识别
162
+ - [x] 阶段 6:单元测试 + 项目整理
163
+ - [x] 阶段 7.1:UDP 扫描
164
+ - [x] 阶段 7.2:搭建 WSL2 开发环境
165
+ - [ ] 阶段 7.3:C 语言 SYN 半开扫描
166
+ ## 免责声明
167
+
168
+ 本工具仅供学习与授权测试使用。请勿用于未授权的目标,否则后果自负。
169
+
170
+ ## License
171
+
172
+ [MIT](LICENSE)
@@ -0,0 +1,14 @@
1
+ """pyportscan - 一个多线程 TCP 端口扫描器"""
2
+
3
+ from .parse import parse_ports
4
+ from .scan import scan_port, scan_ports
5
+ from .banner import grab_banner
6
+
7
+ __version__ = "0.5.0"
8
+
9
+ __all__ = [
10
+ "parse_ports",
11
+ "scan_port",
12
+ "scan_ports",
13
+ "grab_banner",
14
+ ]
@@ -0,0 +1,38 @@
1
+ """服务 Banner 抓取模块"""
2
+
3
+ import socket
4
+
5
+
6
+ def grab_banner(host, port, timeout=2.0):
7
+ """连接端口,尝试获取服务 banner。返回字符串,失败返回 None。"""
8
+ sock = None
9
+ try:
10
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
11
+ sock.settimeout(timeout)
12
+ sock.connect((host, port))
13
+
14
+ # 尝试1:被动接收(SSH/FTP/SMTP 主动发)
15
+ try:
16
+ data = sock.recv(1024)
17
+ if data:
18
+ return data.decode(errors="ignore").strip()
19
+ except socket.timeout:
20
+ pass
21
+
22
+ # 尝试2:主动发 HTTP/1.1 请求
23
+ try:
24
+ req = (b"HEAD / HTTP/1.1\r\nHost: " + host.encode()
25
+ + b"\r\nConnection: close\r\n\r\n")
26
+ sock.send(req)
27
+ data = sock.recv(1024)
28
+ if data:
29
+ return data.decode(errors="ignore").strip()
30
+ except socket.timeout:
31
+ pass
32
+
33
+ return None
34
+ except (socket.error, OSError):
35
+ return None
36
+ finally:
37
+ if sock:
38
+ sock.close()
@@ -0,0 +1,20 @@
1
+ """端口范围解析模块"""
2
+
3
+
4
+ def parse_ports(s):
5
+ """解析端口字符串,返回整数列表。
6
+
7
+ 支持格式:
8
+ "80" → [80]
9
+ "22,80,443" → [22, 80, 443]
10
+ "8000-8005" → [8000, 8001, ..., 8005]
11
+ "22,8000-8002" → [22, 8000, 8001, 8002]
12
+ """
13
+ ports = []
14
+ for part in s.split(","):
15
+ if "-" in part:
16
+ start, end = part.split("-")
17
+ ports.extend(range(int(start), int(end) + 1))
18
+ else:
19
+ ports.append(int(part))
20
+ return ports
@@ -0,0 +1,12 @@
1
+ """常用端口列表(参考 Nmap Top 100)"""
2
+
3
+ TOP_PORTS = [
4
+ 7, 9, 13, 21, 22, 23, 25, 26, 37, 53, 79, 80, 81, 88, 106, 110, 111, 113,
5
+ 119, 135, 139, 143, 144, 179, 199, 389, 427, 443, 444, 445, 465, 513, 514,
6
+ 515, 543, 544, 548, 554, 587, 631, 646, 873, 990, 993, 995, 1025, 1026,
7
+ 1027, 1028, 1029, 1110, 1433, 1720, 1723, 1755, 1900, 2000, 2001, 2049,
8
+ 2121, 2717, 3000, 3128, 3306, 3389, 3986, 4899, 5000, 5009, 5051, 5060,
9
+ 5101, 5190, 5357, 5432, 5631, 5666, 5800, 5900, 6000, 6001, 6646, 7070,
10
+ 8000, 8008, 8009, 8080, 8081, 8443, 8888, 9100, 9999, 10000, 32768,
11
+ 49152, 49153, 49154, 49155, 49156, 49157,
12
+ ]
@@ -0,0 +1,106 @@
1
+ """TCP 端口扫描模块"""
2
+
3
+ import socket
4
+ from concurrent.futures import ThreadPoolExecutor
5
+
6
+
7
+ def scan_port(host, port, timeout=1.0):
8
+ """扫描单个 TCP 端口。返回 True 表示开放。"""
9
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10
+ sock.settimeout(timeout)
11
+ try:
12
+ result = sock.connect_ex((host, port))
13
+ return result == 0
14
+ finally:
15
+ sock.close()
16
+
17
+
18
+ def udp_scan_port(host,port,timeout = 2.0):
19
+ sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
20
+ sock.settimeout(timeout)
21
+ try:
22
+ sock.sendto(b"",(host,port))
23
+ try:
24
+ data, _ = sock.recvfrom(1024)
25
+ return "open"
26
+ except socket.timeout:
27
+ return "open|filtered"
28
+ except (ConnectionRefusedError,ConnectionResetError):
29
+ return "closed"
30
+ finally:
31
+ sock.close()
32
+
33
+
34
+ def scan_ports(host, ports, timeout=1.0, max_workers=200, grab=False,udp=False,on_progress=None):
35
+ """
36
+ on_prograss:可选回调,每扫完一个端口调用一次
37
+ 签名:on_progress(done,total,open_ports)
38
+ """
39
+
40
+ """并发扫描多个端口。
41
+
42
+ 参数:
43
+ host: 目标主机
44
+ ports: 端口列表
45
+ timeout: 单个端口超时
46
+ max_workers: 并发线程数
47
+ grab: 是否抓取 banner
48
+
49
+ 返回:
50
+ TCP (udp=False):
51
+ grab=False → [(port, None), ...](仅开放端口)
52
+ grab=True → [(port, banner), ...]
53
+ UDP (udp=True):
54
+ [(port, state), ...] # state ∈ {"open", "closed", "open|filtered"}
55
+ """
56
+
57
+ ports = list(ports)
58
+ total = len(ports)
59
+ done = 0
60
+
61
+ # 第一步:并发扫描端口
62
+ open_ports = []
63
+ #UDP
64
+ if udp:
65
+ results = []
66
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
67
+ futures = {executor.submit(udp_scan_port,host,port,timeout):port
68
+ for port in ports}
69
+ for future, port in futures.items():
70
+ done += 1
71
+ if on_progress:
72
+ on_progress(done,total,results)
73
+ state = future.result()
74
+ if state != "closed":
75
+ results.append((port,state))
76
+ results.sort()
77
+ return results
78
+
79
+
80
+ #TCP
81
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
82
+ futures = {executor.submit(scan_port, host, port, timeout): port
83
+ for port in ports}
84
+ for future, port in futures.items():
85
+ done += 1
86
+ if on_progress:
87
+ on_progress(done,total,open_ports)
88
+ if future.result():
89
+ open_ports.append(port)
90
+
91
+ # 第二步:不抓 banner 直接返回
92
+ if not grab:
93
+ return sorted([(p, None) for p in open_ports])
94
+
95
+ # 第三步:并发抓 banner
96
+ from .banner import grab_banner # 延迟导入,避免循环依赖
97
+ results = []
98
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
99
+ futures = {executor.submit(grab_banner, host, port, timeout): port
100
+ for port in open_ports}
101
+ for future, port in futures.items():
102
+ banner = future.result()
103
+ results.append((port, banner))
104
+
105
+ results.sort()
106
+ return results
@@ -0,0 +1,63 @@
1
+ """基于asyncio的异步端口扫描"""
2
+
3
+ import asyncio
4
+
5
+
6
+ async def scan_port_async(host,port,timeout=1.0,semaphore=None):
7
+ """异步扫描单个TCP端口
8
+ 参数:
9
+ semaphore:可选的 asyncio.Semaphore,用于限流
10
+ 返回:
11
+ True表示端口开放,False表示端口关闭/超时
12
+ """
13
+
14
+ if semaphore is not None:
15
+ async with semaphore:
16
+ return await _try_connect(host,port,timeout)
17
+ return await _try_connect(host,port,timeout)
18
+
19
+
20
+
21
+ async def _try_connect(host,port,timeout):
22
+ #内部:尝试连接
23
+
24
+ try:
25
+ reader,writer = await asyncio.wait_for(
26
+ asyncio.open_connection(host,port),
27
+ timeout=timeout,
28
+ )
29
+ writer.close()
30
+ try:
31
+ await writer.wait_closed()
32
+ except Exception:
33
+ pass
34
+ return True
35
+ except(asyncio.TimeoutError,ConnectionRefusedError,OSError):
36
+ return False
37
+
38
+
39
+
40
+ async def scan_ports_async(host,ports,timeout=1.0,max_concurrent=500):
41
+ """异步并发扫描多个端口
42
+ 参数:
43
+ host:目标主机
44
+ ports:端口列表(或range)
45
+ timeout:单个端口超时
46
+ max_concurrent:最大并发数
47
+
48
+ 返回:
49
+ [(port,None),...](暂不抓banner,格式与scan_ports一致)
50
+ """
51
+
52
+ ports = list(ports)
53
+ semaphore = asyncio.Semaphore(max_concurrent)
54
+
55
+ tasks = [
56
+ scan_port_async(host,port,timeout,semaphore)
57
+ for port in ports
58
+ ]
59
+ results = await asyncio.gather(*tasks)
60
+
61
+ open_ports = [p for p, ok in zip(ports,results)if ok]
62
+ open_ports.sort()
63
+ return [(p,None) for p in open_ports]
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyportscan
3
+ Version: 1.2.0
4
+ Summary: A multi-threaded Python port scanner with async support and banner grabbing.
5
+ Author-email: JinGu123123 <gujin123123@outlook.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/JinGu123123/pyportscan
8
+ Project-URL: Issues, https://github.com/JinGu123123/pyportscan/issues
9
+ Keywords: port-scanner,network,security,asyncio
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Security
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # pyportscan
19
+
20
+ 一个用 Python 实现的多线程 TCP 端口扫描器,支持端口范围解析、并发扫描和 Banner 抓取。用于学习网络安全与网络编程。
21
+
22
+ ## 功能
23
+
24
+ - [x] 单端口 / 多端口 / 端口范围扫描
25
+ - [x] 多线程并发加速
26
+ - [x] 灵活的命令行参数(argparse)
27
+ - [x] Banner Grabbing 服务识别
28
+ - [x] 模块化设计,可作为 Python 库使用
29
+ - [x] 单元测试覆盖核心逻辑
30
+ - [x] UDP 扫描(计划中)
31
+ - [ ] 导出扫描结果到 JSON/CSV(计划中)
32
+
33
+ ## 环境要求
34
+
35
+ - Python 3.8+
36
+ - 依赖见 `requirements.txt`
37
+
38
+ ## 开发环境
39
+
40
+ ### Python 部分
41
+ - Python 3.8+
42
+ - pytest(测试)
43
+
44
+ ### C 语言 SYN 扫描(阶段 7.3 准备)
45
+ - WSL2(Windows Subsystem for Linux 2)
46
+ - Ubuntu 26.04 LTS
47
+ - gcc 15.2.0
48
+ - make 4.4.1
49
+ - libpcap-dev
50
+
51
+ #### 在 WSL2 里安装
52
+
53
+ ```bash
54
+ sudo apt update
55
+ sudo apt install -y gcc make libpcap-dev
56
+
57
+ ## 安装
58
+
59
+ ```bash
60
+ git clone git@github.com:JinGu123123/pyportscan.git
61
+ cd pyportscan
62
+ pip install -r requirements.txt
63
+ ```
64
+
65
+ ## 使用
66
+
67
+ ### 基本用法
68
+
69
+ ```bash
70
+ # 扫描本机默认端口(1-1024)
71
+ python main.py 127.0.0.1
72
+
73
+ # 扫描指定端口
74
+ python main.py 127.0.0.1 -p 22,80,443,8000
75
+
76
+ # 扫描端口范围
77
+ python main.py 127.0.0.1 -p 1-1024
78
+
79
+ # 混合格式
80
+ python main.py 127.0.0.1 -p 22,80,8000-8100,3306
81
+
82
+ # 不抓取 banner(更快)
83
+ python main.py 127.0.0.1 -p 1-1024 --no-banner
84
+
85
+ # UDP 扫描
86
+ python main.py 127.0.0.1 -p 53,123,161 --udp
87
+
88
+ # 查看帮助
89
+ python main.py -h
90
+ ```
91
+
92
+ ### 参数说明
93
+
94
+ | 参数 | 说明 | 默认值 |
95
+ |------|------|--------|
96
+ | `host` | 目标主机(IP 或域名,必填) | - |
97
+ | `-p, --ports` | 端口范围 | `1-1024` |
98
+ | `-t, --timeout` | 连接超时(秒) | `1.0` |
99
+ | `-w, --workers` | 并发线程数 | `100` |
100
+ | `--no-banner` | 不抓取服务 banner | 关闭 |
101
+ | `--udp` | 使用 UDP 扫描(默认 TCP) | 关闭 |
102
+ | `-h, --help` | 显示帮助 | - |
103
+
104
+ ### 示例输出
105
+
106
+ ```
107
+ $ python main.py 127.0.0.1 -p 1-1024
108
+ 开始扫描 127.0.0.1,端口数 1024
109
+ 开放端口:
110
+ 135/tcp (无法识别)
111
+ 445/tcp (无法识别)
112
+ 902/tcp 220 VMware Authentication Daemon Version 1.10: SSL Required...
113
+ 912/tcp 220 VMware Authentication Daemon Version 1.0, ServerDaemonProtocol:SOAP...
114
+ ```
115
+
116
+ ## Python API 用法
117
+
118
+ 除了命令行,`pyportscan` 也可以作为 Python 库使用:
119
+
120
+ ```python
121
+ from pyportscan import parse_ports, scan_ports, grab_banner
122
+
123
+ # 解析端口字符串
124
+ ports = parse_ports("22,80,8000-8100") # [22, 80, 8000, ..., 8100]
125
+
126
+ # 扫描端口(不抓 banner)
127
+ results = scan_ports("127.0.0.1", ports, grab=False)
128
+
129
+ # 扫描端口并抓取 banner
130
+ results = scan_ports("127.0.0.1", [8000], grab=True)
131
+ for port, banner in results:
132
+ print(port, banner)
133
+
134
+ # 单独抓取某个端口的 banner
135
+ banner = grab_banner("127.0.0.1", 8000)
136
+
137
+ # UDP 扫描
138
+ results = scan_ports("127.0.0.1", [53, 123], udp=True)
139
+ # [(53, 'open|filtered'), ...]
140
+ ```
141
+
142
+ ## 项目结构
143
+
144
+ ```
145
+ pyportscan/
146
+ ├── pyportscan/ # 核心包
147
+ │ ├── __init__.py # 统一暴露 API
148
+ │ ├── parse.py # 端口范围解析
149
+ │ ├── scan.py # 端口扫描
150
+ │ └── banner.py # banner 抓取
151
+ ├── tests/ # 单元测试
152
+ │ ├── test_parse.py
153
+ │ ├── test_scan.py
154
+ │ └── test_banner.py
155
+ ├── main.py # CLI 入口
156
+ ├── requirements.txt
157
+ ├── README.md
158
+ └── LICENSE
159
+ ```
160
+
161
+ ## 运行测试
162
+
163
+ ```bash
164
+ # 安装测试依赖
165
+ pip install -r requirements.txt
166
+
167
+ # 运行全部测试
168
+ pytest tests/ -v
169
+
170
+ # 只运行某个测试文件
171
+ pytest tests/test_parse.py -v
172
+ ```
173
+
174
+ ## 开发进度
175
+
176
+ - [x] 阶段 0-3:基础扫描 + 并发扫描
177
+ - [x] 阶段 4:命令行工具(argparse)
178
+ - [x] 阶段 5:Banner Grabbing 服务识别
179
+ - [x] 阶段 6:单元测试 + 项目整理
180
+ - [x] 阶段 7.1:UDP 扫描
181
+ - [x] 阶段 7.2:搭建 WSL2 开发环境
182
+ - [ ] 阶段 7.3:C 语言 SYN 半开扫描
183
+ ## 免责声明
184
+
185
+ 本工具仅供学习与授权测试使用。请勿用于未授权的目标,否则后果自负。
186
+
187
+ ## License
188
+
189
+ [MIT](LICENSE)
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ pyportscan/__init__.py
5
+ pyportscan/banner.py
6
+ pyportscan/parse.py
7
+ pyportscan/ports.py
8
+ pyportscan/scan.py
9
+ pyportscan/scan_async.py
10
+ pyportscan.egg-info/PKG-INFO
11
+ pyportscan.egg-info/SOURCES.txt
12
+ pyportscan.egg-info/dependency_links.txt
13
+ pyportscan.egg-info/entry_points.txt
14
+ pyportscan.egg-info/top_level.txt
15
+ tests/test_banner.py
16
+ tests/test_parse.py
17
+ tests/test_scan.py
18
+ tests/test_scan_ports.py
19
+ tests/test_udp.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyportscan = main:main
@@ -0,0 +1 @@
1
+ pyportscan
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyportscan"
7
+ version = "1.2.0"
8
+ description = "A multi-threaded Python port scanner with async support and banner grabbing."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "JinGu123123", email = "gujin123123@outlook.com" }
15
+ ]
16
+ keywords = ["port-scanner", "network", "security", "asyncio"]
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Security",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/JinGu123123/pyportscan"
25
+ Issues = "https://github.com/JinGu123123/pyportscan/issues"
26
+
27
+ [project.scripts]
28
+ pyportscan = "main:main"
29
+
30
+ [tool.setuptools.packages.find]
31
+ include = ["pyportscan*"]
32
+ exclude = ["c_syn_scan*", "tests*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,50 @@
1
+ """banner 模块的单元测试(用 mock 避免真实网络)"""
2
+
3
+ import socket
4
+ from unittest.mock import patch
5
+
6
+ from pyportscan.banner import grab_banner
7
+
8
+
9
+ @patch("pyportscan.banner.socket.socket")
10
+ def test_grab_banner_receives_banner(mock_socket_cls):
11
+ """被动接收成功:SSH 主动发 banner"""
12
+ fake_sock = mock_socket_cls.return_value
13
+ fake_sock.recv.return_value = b"SSH-2.0-OpenSSH_8.0\r\n"
14
+
15
+ assert grab_banner("127.0.0.1", 22) == "SSH-2.0-OpenSSH_8.0"
16
+
17
+
18
+ @patch("pyportscan.banner.socket.socket")
19
+ def test_grab_banner_http_after_timeout(mock_socket_cls):
20
+ """被动超时 + 主动 HTTP 请求成功"""
21
+ fake_sock = mock_socket_cls.return_value
22
+ # 第一次 recv 抛 timeout,第二次 recv 返回 HTTP 响应
23
+ fake_sock.recv.side_effect = [
24
+ socket.timeout(),
25
+ b"HTTP/1.1 200 OK\r\nServer: nginx\r\n\r\n",
26
+ ]
27
+
28
+ result = grab_banner("127.0.0.1", 80)
29
+ assert result is not None
30
+ assert "200 OK" in result
31
+
32
+
33
+ @patch("pyportscan.banner.socket.socket")
34
+ def test_grab_banner_returns_none_on_timeout(mock_socket_cls):
35
+ """两次 recv 都超时 → 返回 None"""
36
+ fake_sock = mock_socket_cls.return_value
37
+ fake_sock.recv.side_effect = socket.timeout
38
+
39
+ assert grab_banner("127.0.0.1", 9999) is None
40
+
41
+
42
+ @patch("pyportscan.banner.socket.socket")
43
+ def test_grab_banner_handles_binary(mock_socket_cls):
44
+ """响应含非 UTF-8 字节:不崩溃"""
45
+ fake_sock = mock_socket_cls.return_value
46
+ fake_sock.recv.return_value = b"\xff\xfe\x00\x01binary"
47
+
48
+ result = grab_banner("127.0.0.1", 445)
49
+ # 不应该抛异常;decode(errors="ignore") 会丢掉坏字节
50
+ assert isinstance(result, str)
@@ -0,0 +1,25 @@
1
+ """parse_ports 的单元测试"""
2
+
3
+ from pyportscan.parse import parse_ports
4
+
5
+
6
+ def test_single_port():
7
+ assert parse_ports("80") == [80]
8
+
9
+
10
+ def test_multiple_ports():
11
+ assert parse_ports("22,80,443") == [22, 80, 443]
12
+
13
+
14
+ def test_range():
15
+ assert parse_ports("8000-8005") == [8000, 8001, 8002, 8003, 8004, 8005]
16
+
17
+
18
+ def test_mixed():
19
+ assert parse_ports("22,8000-8002,3306") == [22, 8000, 8001, 8002, 3306]
20
+
21
+
22
+ def test_range_boundary():
23
+ # range 的 +1 是经典坑,专门测边界
24
+ assert parse_ports("1-1") == [1]
25
+ assert parse_ports("65535-65535") == [65535]
@@ -0,0 +1,36 @@
1
+ """scan 模块的单元测试(用 mock 避免真实网络)"""
2
+
3
+ from unittest.mock import patch
4
+
5
+ from pyportscan.scan import scan_port
6
+
7
+
8
+ @patch("pyportscan.scan.socket.socket")
9
+ def test_scan_port_open(mock_socket_cls):
10
+ """端口开放:connect_ex 返回 0 → scan_port 返回 True"""
11
+ fake_sock = mock_socket_cls.return_value
12
+ fake_sock.connect_ex.return_value = 0
13
+
14
+ assert scan_port("127.0.0.1", 8000) is True
15
+
16
+
17
+ @patch("pyportscan.scan.socket.socket")
18
+ def test_scan_port_closed(mock_socket_cls):
19
+ """端口关闭:connect_ex 返回非 0 → scan_port 返回 False"""
20
+ fake_sock = mock_socket_cls.return_value
21
+ fake_sock.connect_ex.return_value = 111 # ECONNREFUSED
22
+
23
+ assert scan_port("127.0.0.1", 9999) is False
24
+
25
+
26
+ @patch("pyportscan.scan.socket.socket")
27
+ def test_scan_port_closes_socket(mock_socket_cls):
28
+ """scan_port 必须关闭 socket"""
29
+ fake_sock = mock_socket_cls.return_value
30
+ fake_sock.connect_ex.return_value = 0
31
+
32
+ scan_port("127.0.0.1", 8000)
33
+
34
+ fake_sock.close.assert_called_once() # 验证 close 被调用了一次
35
+ fake_sock.close.assert_called_once()
36
+ fake_sock.settimeout.assert_called_once() # 新增:验证 settimeout 被调用
@@ -0,0 +1,74 @@
1
+ """scan_ports 的集成测试(mock socket,真跑线程池)"""
2
+
3
+ import socket
4
+ from unittest.mock import patch
5
+
6
+ from pyportscan.scan import scan_ports
7
+
8
+
9
+ @patch("pyportscan.scan.socket.socket")
10
+ def test_scan_ports_tcp_no_banner(mock_socket_cls):
11
+ """TCP 模式,grab=False:所有端口都开放"""
12
+ fake_sock = mock_socket_cls.return_value
13
+ fake_sock.connect_ex.return_value = 0 # 所有端口开放
14
+
15
+ result = scan_ports("127.0.0.1", [80, 443], grab=False)
16
+
17
+ assert (80, None) in result
18
+ assert (443, None) in result
19
+ assert len(result) == 2
20
+
21
+
22
+ @patch("pyportscan.scan.socket.socket")
23
+ def test_scan_ports_tcp_with_banner(mock_socket_cls):
24
+ """TCP 模式,grab=True:抓 banner"""
25
+ fake_sock = mock_socket_cls.return_value
26
+ fake_sock.connect_ex.return_value = 0
27
+ fake_sock.recv.return_value = b"HTTP/1.1 200 OK\r\n"
28
+
29
+ result = scan_ports("127.0.0.1", [80], grab=True)
30
+
31
+ assert len(result) == 1
32
+ port, banner = result[0]
33
+ assert port == 80
34
+ assert "HTTP" in banner
35
+
36
+
37
+ @patch("pyportscan.scan.socket.socket")
38
+ def test_scan_ports_tcp_all_closed(mock_socket_cls):
39
+ """TCP 模式,所有端口关闭:返回空列表"""
40
+ fake_sock = mock_socket_cls.return_value
41
+ fake_sock.connect_ex.return_value = 111 # 非 0 = 关闭
42
+
43
+ result = scan_ports("127.0.0.1", [80, 443], grab=False)
44
+
45
+ assert result == []
46
+
47
+
48
+ @patch("pyportscan.scan.socket.socket")
49
+ def test_scan_ports_udp(mock_socket_cls):
50
+ """UDP 模式"""
51
+ fake_sock = mock_socket_cls.return_value
52
+ fake_sock.recvfrom.side_effect = socket.timeout # 所有端口超时
53
+
54
+ result = scan_ports("127.0.0.1", [53], udp=True)
55
+
56
+ # 超时 → open|filtered,全部保留
57
+ assert (53, "open|filtered") in result
58
+
59
+
60
+ @patch("pyportscan.scan.socket.socket")
61
+ def test_scan_ports_udp_mixed(mock_socket_cls):
62
+ """UDP 模式:混合状态(closed 被过滤)"""
63
+ fake_sock = mock_socket_cls.return_value
64
+ # recvfrom 按顺序返回:第一次超时,第二次 ConnectionResetError
65
+ fake_sock.recvfrom.side_effect = [
66
+ socket.timeout(),
67
+ ConnectionResetError(),
68
+ ]
69
+
70
+ result = scan_ports("127.0.0.1", [53, 9999], udp=True)
71
+
72
+ # 53 → open|filtered,9999 → closed(被过滤)
73
+ assert (53, "open|filtered") in result
74
+ assert all(port != 9999 for port, _ in result)
@@ -0,0 +1,42 @@
1
+ """UDP 扫描的单元测试(用 mock 避免真实网络)"""
2
+
3
+ import socket
4
+ from unittest.mock import patch
5
+
6
+ from pyportscan.scan import udp_scan_port
7
+
8
+
9
+ @patch("pyportscan.scan.socket.socket")
10
+ def test_udp_open(mock_socket_cls):
11
+ """收到 UDP 响应 → open"""
12
+ fake_sock = mock_socket_cls.return_value
13
+ fake_sock.recvfrom.return_value = (b"response", ("127.0.0.1", 53))
14
+
15
+ assert udp_scan_port("127.0.0.1", 53) == "open"
16
+
17
+
18
+ @patch("pyportscan.scan.socket.socket")
19
+ def test_udp_closed_win(mock_socket_cls):
20
+ """Windows 上收到 ICMP unreachable → ConnectionResetError → closed"""
21
+ fake_sock = mock_socket_cls.return_value
22
+ fake_sock.recvfrom.side_effect = ConnectionResetError()
23
+
24
+ assert udp_scan_port("127.0.0.1", 9999) == "closed"
25
+
26
+
27
+ @patch("pyportscan.scan.socket.socket")
28
+ def test_udp_closed_linux(mock_socket_cls):
29
+ """Linux 上收到 ICMP unreachable → ConnectionRefusedError → closed"""
30
+ fake_sock = mock_socket_cls.return_value
31
+ fake_sock.recvfrom.side_effect = ConnectionRefusedError()
32
+
33
+ assert udp_scan_port("127.0.0.1", 9999) == "closed"
34
+
35
+
36
+ @patch("pyportscan.scan.socket.socket")
37
+ def test_udp_filtered(mock_socket_cls):
38
+ """超时 → open|filtered"""
39
+ fake_sock = mock_socket_cls.return_value
40
+ fake_sock.recvfrom.side_effect = socket.timeout()
41
+
42
+ assert udp_scan_port("8.8.8.8", 53) == "open|filtered"