
简介本资源是一套基于C与MFC开发的Windows平台网络扫描器完整实现项目面向网络安全初学者、高校课程设计学生及渗透测试入门者聚焦主机发现、端口探测、协议识别与基础安全检测等核心能力训练。项目包含主机扫描、端口扫描、NetBIOS/SNMP/弱密码/嗅探器/DOS攻击/SQL注入检测共八大功能模块均附详细测试用例与结果验证截图覆盖从开发环境VC6.0Notepad到实际靶机交互的全流程实践。压缩包共98个文件约5.12MB含14个CPP源码、18个H头文件、15张PNG/JPG测试截图、3个HTML测试页面及PDF论文文档等结构清晰便于逐模块研读代码逻辑与调试验证。目前已有905人学习下载读者可直接编译运行、复现全部扫描与攻击测试场景深入理解底层Winsock编程、ICMP/UDP/TCP协议交互及常见漏洞检测原理。1. 网络扫描器的设计与实现不是端口探测那么简单而是构建可落地、可审计、可嵌入CI/CD的资产测绘基座很多人第一次听说“网络扫描器”下意识就想到nmap -sS 192.168.1.0/24—— 一行命令扫完结果一贴任务就算完成。但真实产线里某高校实验室在做物联网设备安全基线评估时发现用默认 nmap 扫出的“开放端口”里有37%在5分钟内无法复现某公司部署自动化资产发现系统后因扫描策略未收敛导致核心交换机CPU持续飙高至92%被迫紧急熔断。这说明网络扫描器的设计与实现本质不是“怎么扫得快”而是“怎么扫得准、扫得稳、扫得可控、扫得可追溯”。它要能区分生产环境与测试网段的扫描强度能自动识别 WAF/IPS 的干扰指纹能将原始扫描结果结构化为 CMDB 可消费的 JSON Schema还能在 Jenkins 流水线中作为 gate 阶段校验新上线服务是否暴露了高危端口。本文面向已掌握基础网络协议TCP/IP、ICMP、HTTP和 Python/Shell 开发能力的工程师不讲教科书式原理只拆解一个从零启动、可跑通、可调参、可进阶的轻量级扫描器落地路径用 Scapy 构建协议层控制力用 asyncio 实现并发节流用 SQLite 做本地状态持久化最后用 YAML 配置驱动行为——所有代码无第三方云依赖纯本地可验证。2. 协议层可控为什么不用 subprocess 调 nmap而用 Scapy 重写核心扫描逻辑2.1 TCP SYN 扫描的底层握手细节决定结果可信度nmap 的-sS模式虽快但其内部实现对 RST 包的响应判定、超时重传策略、源端口随机化粒度等均不可见。当面对启用了 SYN Cookie 的 Linux 内核如 5.10或中间存在状态检测型防火墙时nmap 可能将“被丢弃的 SYN”误判为“端口关闭”而非“被过滤”。Scapy 则允许我们逐包构造、发送、捕获并解析每一个交互环节from scapy.all import IP, TCP, sr1, conf import time def syn_scan_one(ip: str, port: int, timeout: float 2.0) - dict: # 构造原始IPTCP包禁用Scapy自动分片、禁用路由表查询 pkt IP(dstip, flagsDF) / TCP(dportport, flagsS, seq1000) conf.verb 0 # 关闭Scapy默认输出 try: # 发送并等待响应仅等待timeout秒不重传 resp sr1(pkt, timeouttimeout, retry0, verbose0) if resp is None: return {port: port, state: filtered, reason: no_response} elif TCP in resp: if resp[TCP].flags 0x12: # SYN-ACK (0x12 0b00010010) return {port: port, state: open, reason: syn_ack} elif resp[TCP].flags 0x14: # RST-ACK (0x14 0b00010100) return {port: port, state: closed, reason: rst_ack} return {port: port, state: unknown, reason: funhandled_flags_{resp[TCP].flags}} except Exception as e: return {port: port, state: error, reason: str(e)}提示conf.verb 0是必须项否则 Scapy 在高并发下会因日志刷屏导致性能断崖式下跌retry0强制禁用重传避免扫描时间不可控flagsDF设置不分片标志防止中间设备因MTU问题静默丢包却无反馈。2.2 ICMP 探测需区分 Type 3 Code 13通信被禁止与 Code 1主机不可达很多扫描器把所有 ICMP 目标不可达Type 3一概视为“主机离线”但实际运维中Code 13Communication Administratively Prohibited明确表示“策略拒绝”即主机在线但被 ACL 拦截——这是关键的安全信号。Scapy 可精准提取 ICMP 错误码def icmp_ping(ip: str, timeout: float 1.5) - dict: pkt IP(dstip) / ICMP() resp sr1(pkt, timeouttimeout, retry0, verbose0) if resp is None: return {host: ip, status: down, icmp_code: None} elif ICMP in resp and resp[ICMP].type 3: # Destination Unreachable return {host: ip, status: filtered, icmp_code: resp[ICMP].code} elif ICMP in resp and resp[ICMP].type 0: # Echo Reply return {host: ip, status: up, icmp_code: 0} else: return {host: ip, status: unknown, icmp_code: f{resp[ICMP].type}/{resp[ICMP].code}}2.3 为什么放弃socket.connect()而坚持 Scapysocket.connect()是阻塞式全连接会触发三次握手四次挥手易被 IDS 记录为攻击行为且无法获取中间设备返回的 ICMP 错误如 Type 3 Code 13只能知道“连不上”不知“为何连不上”。Scapy 的无状态包构造能力让扫描器具备协议层“显微镜”视角——这正是设计阶段必须确立的技术选型底线。3. 并发与节流用 asyncio 信号量控住扫描洪峰避免触发网络设备告警3.1 为什么 asyncio 比 threading/multiprocessing 更适合扫描场景扫描是典型的 I/O 密集型任务大部分时间在等待网络响应而非 CPU 计算。threading在 CPython 下受 GIL 限制并发提升有限multiprocessing进程开销大且 Scapy 的底层libpcap在多进程间共享抓包句柄极不稳定。asyncio 基于事件循环单线程即可支撑数千并发连接等待内存占用低且与 Scapy 的异步抓包扩展如scapy.contrib.async天然契合。3.2 用 asyncio.Semaphore 实现每秒请求数RPS硬限流不加节流的扫描在企业网中极易触发交换机 CPU 告警如 Cisco 的%SYS-3-CPUHOG。以下代码将并发数严格锁定在max_concurrent50并确保每秒发出请求不超过rps_limit20import asyncio import time from typing import List, Dict async def scan_host_with_rate_limit( ip: str, ports: List[int], sem: asyncio.Semaphore, rps_limiter: asyncio.Semaphore, timeout: float 2.0 ) - Dict: results {host: ip, ports: []} # 每个端口扫描前先申请RPS许可1秒窗口 await rps_limiter.acquire() # 启动定时器1秒后释放许可 asyncio.create_task(release_after(rps_limiter, delay1.0)) # 再申请并发许可 async with sem: for port in ports: result await asyncio.to_thread(syn_scan_one, ip, port, timeout) results[ports].append(result) await asyncio.sleep(0.01) # 微小抖动防脉冲 return results async def release_after(sem: asyncio.Semaphore, delay: float): await asyncio.sleep(delay) sem.release() # 主调度函数 async def run_scan_batch( targets: List[str], port_list: List[int], max_concurrent: int 50, rps_limit: int 20 ): sem asyncio.Semaphore(max_concurrent) rps_sem asyncio.Semaphore(rps_limit) # 初始满额 tasks [ scan_host_with_rate_limit( ip, port_list, sem, rps_sem, timeout2.0 ) for ip in targets ] return await asyncio.gather(*tasks, return_exceptionsTrue)参数说明max_concurrent50适用于千兆局域网若扫描跨 WAN建议降至10~20rps_limit20意味着每秒最多向同一目标 IP 发送 20 个 SYN 包——该值低于主流防火墙默认阈值通常为 30~50/s可有效规避SYN Flood类误报。3.3 扫描结果的实时流式写入与中断恢复为避免扫描中途崩溃导致全量重跑我们采用 SQLite 的 WAL 模式进行原子写入并记录每个 IP 的扫描状态import sqlite3 def init_db(db_path: str): conn sqlite3.connect(db_path, isolation_levelNone) # 自动提交 conn.execute(PRAGMA journal_modeWAL) # 启用WAL支持高并发读写 conn.execute( CREATE TABLE IF NOT EXISTS scan_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, target_ip TEXT NOT NULL, port INTEGER NOT NULL, state TEXT NOT NULL, reason TEXT, scan_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(target_ip, port) ) ) conn.execute( CREATE TABLE IF NOT EXISTS scan_progress ( target_ip TEXT PRIMARY KEY, status TEXT CHECK(status IN (pending,scanning,done,error)), last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) return conn def save_result(conn: sqlite3.Connection, result: dict): for port_info in result[ports]: try: conn.execute( INSERT OR REPLACE INTO scan_results (target_ip, port, state, reason) VALUES (?, ?, ?, ?), (result[host], port_info[port], port_info[state], port_info[reason]) ) except sqlite3.IntegrityError: pass # 忽略重复键 conn.execute( INSERT OR REPLACE INTO scan_progress (target_ip, status) VALUES (?, ?), (result[host], done) )4. 配置驱动与结果治理用 YAML 定义扫描策略用 Pydantic 校验输入合法性4.1 扫描配置文件scan_config.yaml的最小可行结构硬编码参数是运维噩梦。我们将扫描行为完全外置为 YAML包含目标、端口、超时、节流、输出格式四类核心维度# scan_config.yaml targets: - 192.168.1.0/24 - 10.0.5.100-10.0.5.110 - example.com ports: top_100: true # 使用nmap-top100端口列表 custom: [22, 80, 443, 8080, 9000] # 自定义补充端口 timing: timeout: 3.0 # 单包超时秒 max_retries: 1 # 重试次数Scapy层面禁用此处为兼容预留 rate_limit: concurrent: 30 # 最大并发连接数 rps: 15 # 每秒请求数针对单IP output: format: jsonl # jsonl每行JSON、sqlite、csv file: results.jsonl db_path: scan.db advanced: skip_host_discovery: false # 是否跳过ping检测直接扫端口 randomize_ports: true # 扫描前打乱端口顺序降低模式识别风险4.2 用 Pydantic V2 定义强类型配置模型杜绝运行时类型错误手动解析 YAML 易漏字段、错类型。Pydantic 提供自动类型转换、必填校验、枚举约束from pydantic import BaseModel, Field, validator from typing import List, Optional, Union from enum import Enum class OutputFormat(str, Enum): JSONL jsonl SQLITE sqlite CSV csv class ScanConfig(BaseModel): targets: List[str] Field(..., min_items1) ports: dict Field(...) timing: dict Field(...) rate_limit: dict Field(...) output: dict Field(...) advanced: dict Field(default{}) validator(ports) def validate_ports(cls, v): if not (v.get(top_100) or v.get(custom)): raise ValueError(At least one of top_100 or custom must be True or non-empty) return v validator(output) def validate_output_format(cls, v): fmt v.get(format) if fmt not in [jsonl, sqlite, csv]: raise ValueError(fInvalid output format: {fmt}) if fmt sqlite and not v.get(db_path): raise ValueError(db_path required when format is sqlite) return v # 加载并校验 def load_config(config_path: str) - ScanConfig: import yaml with open(config_path, r, encodingutf-8) as f: raw yaml.safe_load(f) return ScanConfig(**raw)血泪经验某次线上扫描因 YAML 中rps: 15字符串未被校验导致int(15)失败整个流水线卡死。Pydantic 的Field(...)和validator是真正的后悔药。4.3 结果去重与标准化统一输出为 Asset Schema不同扫描器输出格式混乱nmap XML、masscan Grepable、自研 JSONCMDB 无法直采。我们在输出层强制转换为通用资产模型from datetime import datetime def normalize_to_asset(result: dict) - dict: 将任意扫描结果转为标准Asset Schema return { asset_id: fhost-{result[host]}, ip: result[host], hostname: , # 后续可集成DNS反查 os_guess: , # 可扩展为p0f或Nmap OS指纹 open_ports: [ { port: p[port], protocol: tcp, state: p[state], service: , # 可扩展为banner抓取 reason: p[reason] } for p in result[ports] if p[state] open ], scan_time: datetime.utcnow().isoformat() Z, scanner: light-scan-v0.3 } # 输出为JSONL每行一个Asset对象CMDB友好 def write_jsonl(results: List[dict], filepath: str): with open(filepath, w, encodingutf-8) as f: for r in results: asset normalize_to_asset(r) f.write(json.dumps(asset, ensure_asciiFalse) \n)5. 避坑指南5 条真实踩过的坑每一条都让扫描器从“能跑”变成“敢上生产”5.1 现象扫描结果中大量端口显示filtered但人工确认设备在线且端口开放原因Linux 主机默认启用rp_filter反向路径过滤当扫描包从 eth0 进、响应从 eth1 出时内核因“非对称路由”丢弃响应包。Scapy 发送的包无源地址绑定系统随机选接口回包。解决强制指定回包接口或关闭rp_filter# 临时关闭需root echo 0 | sudo tee /proc/sys/net/ipv4/conf/all/rp_filter # 或在Scapy中指定src pkt IP(dstip, src192.168.1.100) / TCP(...) # src设为本机某接口IP5.2 现象asyncio.gather报concurrent.futures._base.CancelledError扫描随机中断原因asyncio.wait_for()超时后抛出TimeoutError但gather默认不处理子任务取消导致后续任务被连锁取消。解决显式捕获并忽略取消异常在scan_host_with_rate_limit中包裹try: result await asyncio.wait_for( asyncio.to_thread(syn_scan_one, ip, port, timeout), timeouttimeout ) except (asyncio.TimeoutError, asyncio.CancelledError): result {port: port, state: timeout, reason: async_timeout}5.3 现象SQLite 写入速度骤降INSERT OR REPLACE耗时从 2ms 涨到 200ms原因未启用 WAL 模式每次写入触发完整数据库锁且未使用executemany批量插入。解决初始化时执行PRAGMA journal_modeWAL将单条INSERT改为批量conn.executemany( INSERT OR REPLACE INTO scan_results ... VALUES (?, ?, ?, ?), [(r[host], p[port], p[state], p[reason]) for p in result[ports]] )5.4 现象扫描10.0.0.0/8时内存暴涨至 8GB进程被 OOM Killer 杀死原因targets解析未做 CIDR 展开限制10.0.0.0/8展开为 1677 万个 IP全部加载进内存。解决在load_config后增加 CIDR 范围校验from ipaddress import ip_network def validate_target_size(targets: List[str], max_hosts: int 10000): total 0 for t in targets: try: if / in t: # CIDR net ip_network(t, strictFalse) total net.num_addresses elif - in t: # IP range start, end t.split(-) total int(ip_address(end)) - int(ip_address(start)) 1 else: total 1 except Exception: pass if total max_hosts: raise ValueError(fTarget list exceeds max_hosts{max_hosts} (got {total}))5.5 现象在 Docker 容器中运行扫描scapy.all.sr1()返回None所有端口判为filtered原因容器默认无CAP_NET_RAW权限无法原始套接字发包。解决启动容器时添加权限docker run --cap-addNET_RAW --cap-addNET_ADMIN your-scan-image # 或更安全的方案使用 host 网络模式仅限可信环境 docker run --networkhost your-scan-image6. 进阶技巧如何让扫描器成为 CI/CD 中的“安全守门员”而非一次性脚本6.1 在 Jenkins Pipeline 中嵌入扫描失败时阻断发布将扫描器封装为 CLI 工具light-scan在Jenkinsfile中作为质量门禁pipeline { agent any stages { stage(Security Gate) { steps { script { // 获取本次构建部署的IP来自上游job或env变量 def target env.DEPLOYED_IP ?: 127.0.0.1 // 执行扫描只检查关键端口 sh light-scan --config scan-gate.yaml --targets ${target} --ports 22,80,443,8080 // 检查输出中是否存在高危暴露 sh if grep -q state:open.*port:22 results.jsonl; then echo ERROR: SSH exposed to internet! Blocking deploy. exit 1 fi } } } } }注意scan-gate.yaml中应设置rate_limit.concurrent: 5和timing.timeout: 1.0确保门禁扫描在 10 秒内完成不影响流水线 SLA。6.2 用 SQLite 的 FTS5 实现扫描结果全文检索当积累数万次扫描记录后人工查“哪些主机开放了 Redis 端口”效率低下。SQLite 原生支持全文搜索def enable_fts(conn: sqlite3.Connection): conn.execute( CREATE VIRTUAL TABLE IF NOT EXISTS scan_results_fts USING fts5(target_ip, port, state, reason, contentscan_results) ) conn.execute( INSERT INTO scan_results_fts SELECT target_ip, port, state, reason FROM scan_results ) def search_open_redis(conn: sqlite3.Connection) - List[dict]: cursor conn.execute( SELECT target_ip, port FROM scan_results_fts WHERE scan_results_fts MATCH port:6379 AND state:open ) return [{ip: r[0], port: r[1]} for r in cursor.fetchall()]6.3 扫描器自身的可观测性暴露 Prometheus Metrics在扫描主循环中注入指标收集让 SRE 能监控扫描健康度from prometheus_client import Counter, Histogram, Gauge SCAN_TOTAL Counter(light_scan_total, Total scan tasks executed) SCAN_ERRORS Counter(light_scan_errors, Scan task errors, [error_type]) SCAN_DURATION Histogram(light_scan_duration_seconds, Scan duration per host) HOSTS_UP Gauge(light_scan_hosts_up, Number of hosts detected as up) async def scan_with_metrics(ip: str, ports: List[int]) - dict: SCAN_TOTAL.inc() start time.time() try: result await scan_host_with_rate_limit(ip, ports, ...) SCAN_DURATION.observe(time.time() - start) up_count sum(1 for p in result[ports] if p[state] open) HOSTS_UP.set(up_count) return result except Exception as e: SCAN_ERRORS.labels(error_typetype(e).__name__).inc() raise然后启动一个独立 metrics endpointfrom prometheus_client import start_http_server start_http_server(8000) # /metrics 可被Prometheus抓取6.4 我的三个硬核习惯让扫描器真正“长在”产线里永远用--dry-run模式首发在light-scanCLI 中加入--dry-run参数只打印将要扫描的目标和端口不发任何包。上线新策略前必跑一次肉眼确认范围无误。扫描日志必须带 trace_id每次扫描生成唯一trace_id所有日志、数据库记录、指标都带上它。当某次扫描异常时grep trace_id三秒定位全链路。每周自动比对 CMDB 与扫描结果写一个diff-cmdb-scan.py找出“CMDB 有但扫描无”可能下线未注销和“扫描有但 CMDB 无”疑似野鸡资产的 IP邮件告警给资产管理员。网络扫描器的设计与实现最终不是为了炫技而是为了让安全左移真正发生——当开发提交代码那一刻扫描器已在后台默默校验他暴露的端口是否符合基线。这种无声的守护才是它存在的全部意义。希望帮到你。本文还有配套的精品资源点击获取