This commit is contained in:
2026-06-09 14:01:51 +08:00
parent 3acb26d9e2
commit 448f2f18cc
16 changed files with 699 additions and 289 deletions
+254 -83
View File
@@ -1,104 +1,218 @@
# NetPilot 网络调试工具 # NetPilot 网络调试工具
NetPilot 是一个基于 Python Tkinter 的 Windows 网络调试工具,面向日常桌面网络排障、网卡配置、局域网资产发现和连通性测场景。 NetPilot 是一个基于 Python Tkinter 的 Windows 网络调试工具,面向桌面网络排障、网卡配置、局域网设备发现、DNS 对比诊断和连通性测试等场景。
应用采用左侧功能导航、右侧公共输出控制台的布局。所有功能页的执行日志、诊断过程错误提示都会统一输出到右侧控制台,便于在不同工具之间切换时保留排障上下文。 应用采用左侧功能导航、中间功能工作区、右侧公共输出控制台的布局。所有功能页的执行日志、诊断过程错误提示和摘要都会统一输出到右侧控制台,便于在不同工具之间切换时保留排障上下文。
## 功能概览 ## 当前功能
当前界面保留以下功能页:
- 网卡配置
- DNS 诊断
- 设备发现
- Ping 探测
- 端口扫描
- 路由追踪
`IP 冲突检测``环网检测` 功能页已从主界面移除。
## 功能说明
### 网卡配置 ### 网卡配置
- 读取本机所有网卡,包括已启用和已禁用的网卡 用于查看本机网卡信息,并对常见 IPv4 / DNS 配置进行调整
主要能力:
- 读取本机启用和禁用的网卡。
- 显示网卡描述、MAC 地址、连接状态、IPv4 地址、子网掩码、默认网关、DNS 等信息。 - 显示网卡描述、MAC 地址、连接状态、IPv4 地址、子网掩码、默认网关、DNS 等信息。
- 支持启用网卡、禁用网卡。 - 支持启用网卡、禁用网卡。
- 支持 DHCP 自动获取和静态 IPv4 / DNS 配置切换 - 支持 DHCP 自动获取。
- 支持重新读取网卡信息,便于配置变更后快速刷新 - 支持写入静态 IPv4、子网掩码、默认网关、首选 DNS、备用 DNS
- 支持重新读取网卡信息。
- 支持网卡配置模板保存、套用和删除。 - 支持网卡配置模板保存、套用和删除。
说明:修改网卡状态、IP、网关、DNS 等配置通常需要以管理员权限运行程序。 权限说明:
修改网卡状态、IP、网关、DNS、DHCP 等配置通常需要以管理员权限运行程序。
### DNS 诊断 ### DNS 诊断
- 读取本机网卡 DNS 配置,并支持选择不同网卡进行诊断 用于比较本机 DNS 与常用公共 DNS 的解析结果、耗时和异常原因。它能判断“DNS 是否能解析成功”,但不等同于判断目标主机一定能 Ping 通
- 支持手动填写或自动补全 DNS 服务器。
- 支持多个域名、多个记录类型的解析测试。 主要能力:
- 只读取活动网卡用于 DNS 诊断。
- 自动读取当前网卡 DNS。
- 支持手动填写 DNS 服务器。
- 支持自动补全常用 DNS。
- 支持多个域名批量测试。
- 支持 A、AAAA、CNAME、MX、TXT、NS 等记录类型。
- 支持设置重复次数和超时时间。
- 对不同 DNS 服务器的解析结果、耗时、失败原因进行对比。 - 对不同 DNS 服务器的解析结果、耗时、失败原因进行对比。
- 诊断结果以表格展示,支持复制摘要和导出 CSV - 诊断结果以表格展示。
- 提供“修复异常”功能,用于处理常见 DNS 配置异常 - 支持复制诊断摘要
- 支持导出 CSV。
- 支持“修复异常”,可将指定网卡 DNS 设置为常用 DNS 并刷新 DNS 缓存。
注意事项:
- 域名 A 记录能解析成功,只代表 DNS 返回了 IP。
- Ping 不通可能是目标服务器禁 Ping、防火墙拦截、路由不可达、运营商策略、目标只开放 TCP/HTTPS、不响应 ICMP 等原因。
- DNS 诊断负责解析层排查,连通性需要结合 Ping 探测、端口扫描、路由追踪继续判断。
### 设备发现 ### 设备发现
- 按所选网卡的配置网段进行局域网设备发现 用于按所选网卡的配置网段发现局域网内在线或 ARP 可见设备。
- 通过 Ping 探活和 ARP 信息补充发现设备。
- 展示 IP、MAC、厂商、来源网卡、延迟、发现方式和备注。
- 支持自动范围、刷新网卡、开始发现、停止发现。
- 支持复制设备清单和导出 CSV。
当前设备发现按配置网段扫描,不再做跨网段邻居表扩展,也不做主机名解析。 主要能力:
- 按当前网卡 IPv4 和子网掩码自动生成扫描范围。
- 通过 Ping 探活发现在线设备。
- 结合 ARP 信息补充 MAC 地址和 ARP 可见设备。
- 展示 IP、MAC、厂商、来源网卡、延迟、发现方式和备注。
- 支持刷新网卡、自动范围、开始发现、停止发现。
- 支持复制设备清单。
- 支持导出 CSV。
当前扫描边界:
- 只按所选网卡的配置网段扫描。
- 不跨网段扫描。
- 不使用邻居表扩展跨网段目标。
- 不解析主机名。
- 目标设备如果禁用 ICMP Echo,可能无法通过 Ping 发现,但如果本机 ARP 表中可见,仍可能以 ARP 可见方式显示。
#### 厂商识别和 OUI 数据库
设备发现里的“厂商”列基于 MAC 地址前缀匹配 OUI 厂商库。
加载优先级:
1. `%LOCALAPPDATA%\NetPilot\oui_vendors.json`
2. 程序内置的 `assets/oui_vendors_seed.json`
3. 代码内的小型兜底表
更新策略:
- 程序启动后会在后台检查 OUI 缓存是否过期。
- 默认缓存超过 7 天后尝试联网更新。
- 更新成功后写入 `%LOCALAPPDATA%\NetPilot\oui_vendors.json`
- 更新失败不会阻塞扫描,只会在右侧输出控制台提示。
识别规则:
- 优先匹配 36 bit 前缀。
- 其次匹配 28 bit 前缀。
- 最后匹配 24 bit 前缀。
- 本地管理地址或随机 MAC 会显示为“随机/本地管理地址”。
- 没有命中的 MAC 显示为“未知”。
### Ping 探测 ### Ping 探测
- 支持单目标 Ping 和批量 Ping 用于检查一个或多个目标的 ICMP 连通性和延迟质量
主要能力:
- 支持单目标 Ping。
- 支持批量 Ping。
- 批量目标支持 CIDR、IP 范围和多行列表。
- 支持指定次数、持续探测、间隔、超时、包大小、TTL、禁止分片。 - 支持指定次数、持续探测、间隔、超时、包大小、TTL、禁止分片。
- 支持选择本地源 IP。 - 支持选择本地源 IP。
- 批量目标支持 CIDR、IP 范围和列表输入 - 实时输出探测过程
- 输出实时探测结果,并统计丢包率、平均延迟、抖动和质量状态。 - 统计丢包率、平均延迟、抖动和质量状态。
- 支持导入目标和导出 CSV - 支持导入目标。
- 支持导出 CSV。
注意事项:
Ping 依赖 ICMP。目标设备禁 Ping 时,Ping 失败不一定代表 TCP 服务不可用。
### 端口扫描 ### 端口扫描
- 支持单主机单端口连通性测试 用于检查 TCP 端口连通性,适合判断服务是否开放
主要能力:
- 支持单主机单端口测试。
- 支持端口列表、端口范围和常用端口预设。 - 支持端口列表、端口范围和常用端口预设。
- 支持批量主机扫描,目标可使用 CIDR、IP 范围或列表 - 支持批量主机扫描。
- 支持并发数、超时、仅显示开放端口等控制项 - 批量目标支持 CIDR、IP 范围和多行列表
- 支持服务名识别和可选 Banner 探测 - 支持并发数、超时、仅显示开放端口
- 支持复制开放端口和导出 CSV - 支持服务名识别
- 支持可选 Banner 探测。
- 结果表格展示主机、解析 IP、端口、服务、状态、耗时、错误等信息。
- 支持复制开放端口。
- 支持导出 CSV。
注意事项:
端口扫描只判断 TCP 连接层结果。服务协议是否正常,还需要结合业务客户端或协议工具进一步确认。
### 路由追踪 ### 路由追踪
- 支持 IPv4 / IPv6 路由追踪 用于查看到目标地址的逐跳路径和延迟情况
- 支持主机名解析开关。
- 支持单次、指定次数和持续追踪。 主要能力:
- 支持 IPv4 / IPv6。
- 支持单次追踪、指定次数追踪和持续追踪。
- 支持最大跳数、单跳超时、高延迟阈值等参数。 - 支持最大跳数、单跳超时、高延迟阈值等参数。
- 展示逐跳 RTT 表格,并保留原始命令输出 - 展示每一跳 IP、探测耗时、平均延迟、抖动和状态
- 对超时、高延迟、波动等情况给出诊断摘要 - 保留原始命令输出
- 支持复制摘要和导出 CSV - 对超时、高延迟、波动等情况生成诊断摘要
- 支持复制摘要。
- 支持导出 CSV。
### 环网检测 注意事项:
- 基于本机网卡统计、广播 / 组播流量、ARP / 邻居表变化和网关 Ping 稳定性进行综合判断 部分路由节点会屏蔽 ICMP 或限制 TTL Exceeded 响应,因此某些跳点超时不一定代表链路中断。应重点关注最后目标是否可达,以及连续多跳异常的位置
- 用于辅助发现疑似二层环路、广播风暴或局域网异常波动。
- 支持检测单个网卡或全部活动网卡。
- 输出诊断摘要,支持复制和导出 CSV。
### IP 冲突 ## 界面布局
- 支持检测本机 IP 冲突风险 - 左侧:功能导航区
- 支持按网段进行安全限速扫描 - 中间:当前功能页的参数、操作按钮和结果列表
- 结合地址状态、系统事件、ARP / 邻居信息、IP / MAC 异常关系判断冲突风险 - 右侧:公共输出控制台,所有功能页共用
- 支持自动范围、刷新网卡、复制摘要和导出 CSV - 底部:状态栏,显示当前状态、本机名、操作系统和权限状态
## 界面说明 公共输出控制台用于查看执行过程、错误提示、更新信息和诊断摘要。点击“清空”可以清除当前控制台内容。
- 左侧为功能导航区。
- 中间为当前功能页的参数和结果区域。
- 右侧为公共输出控制台,所有页面共用。
- 底部状态栏显示当前状态、本机名、操作系统和权限状态。
公共控制台可用于查看执行过程、错误提示和诊断摘要;点击“清空”可以清除当前控制台内容。
## 运行环境 ## 运行环境
建议环境:
- 操作系统:Windows 10 / Windows 11 - 操作系统:Windows 10 / Windows 11
- Python建议 Python 3.10 或更高版本 - PythonPython 3.10 或更高版本
- UI 框架:Tkinter - UI 框架:Tkinter
- 系统依赖:PowerShell、Windows 网络相关命令 - 系统依赖:PowerShell、Windows 网络相关命令
大部分只读诊断功能可以普通权限运行。涉及以下操作时建议使用管理员权限 项目主要调用 Windows 系统命令获取网络信息,例如
- 启用或禁用网卡 - `Get-NetAdapter`
- 修改 IP、网关、DNS、DHCP - `Get-NetIPConfiguration`
- 执行部分需要系统网络信息的诊断或修复动作 - `netsh`
- `ipconfig`
- `arp`
- `ping`
- `tracert`
## 权限说明
普通权限可以运行大部分只读诊断功能。
以下操作建议使用管理员权限:
- 启用网卡
- 禁用网卡
- 修改 IP 地址
- 修改子网掩码
- 修改默认网关
- 修改 DNS
- 切换 DHCP
- 执行 DNS 修复
如果按钮不可用或操作失败,优先确认程序是否以管理员身份运行。
## 源码运行 ## 源码运行
@@ -108,7 +222,7 @@ NetPilot 是一个基于 Python Tkinter 的 Windows 网络调试工具,面向
python main.py python main.py
``` ```
如果需要管理员权限,可以使用管理员 PowerShell 进入项目目录后再执行: 如果需要管理员权限,可以打开管理员 PowerShell 执行:
```powershell ```powershell
cd E:\Git\Network-tools cd E:\Git\Network-tools
@@ -117,7 +231,7 @@ python main.py
## 打包 ## 打包
项目提供 Windows 打包脚本: 项目提供 Windows 打包脚本:
```powershell ```powershell
.\打包.bat .\打包.bat
@@ -129,27 +243,38 @@ python main.py
dist\NetworkTool.exe dist\NetworkTool.exe
``` ```
打包脚本会优先使用本机指定的 Python 运行时,并在 `.packaging\pyinstaller` 下安装 PyInstaller;如果指定运行时不存在,则尝试直接调用系统中的 `pyinstaller` 打包脚本会将 OUI 离线厂商库一并打入程序:
```text
assets\oui_vendors_seed.json
```
这样即使目标电脑没有互联网,设备发现功能也可以使用内置 OUI 种子库识别常见厂商。
## 目录结构 ## 目录结构
```text ```text
Network-tools Network-tools
├─ assets # 图标等静态资源 ├─ assets
│ └─ oui_vendors_seed.json # 内置 OUI 厂商种子库
├─ core ├─ core
│ ├─ Function # 网络检测和配置相关业务逻辑 │ ├─ Function # 网络检测和配置业务逻辑
│ └─ ui # Tkinter 界面、主题和功能页 │ └─ ui # Tkinter 界面、主题和功能页
├─ main.py # 程序入口 ├─ main.py # 程序入口
├─ 打包.bat # PyInstaller 打包脚本 ├─ 打包.bat # PyInstaller 打包脚本
├─ NetworkTool.spec # PyInstaller 配置 ├─ NetworkTool.spec # PyInstaller 配置
└─ README.md └─ README.md
``` ```
## 配置和导出文件 ## 本地文件
- 网卡配置模板会保存到程序同级目录的 `network_profiles.json` 程序运行过程中可能产生以下本地文件:
- 各功能页的 CSV 导出文件由用户在导出时选择保存路径。
- 程序不会主动上传网络检测结果,所有诊断和导出均在本机完成 - `%LOCALAPPDATA%\NetPilot\oui_vendors.json`:联网更新后的 OUI 厂商缓存
- `network_profiles.json`:网卡配置模板,保存位置为程序运行目录。
- CSV 导出文件:由用户在导出时选择保存路径。
程序不会主动上传网络检测结果。诊断、缓存和导出均在本机完成。
## 常见问题 ## 常见问题
@@ -157,25 +282,53 @@ Network-tools
请确认程序以管理员权限运行。Windows 对网卡启用/禁用、IP 配置、DNS 配置等操作有权限限制。 请确认程序以管理员权限运行。Windows 对网卡启用/禁用、IP 配置、DNS 配置等操作有权限限制。
### 为什么 DNS 能解析成功,但 Ping 不通?
DNS 解析成功只说明域名能转换成 IP。Ping 依赖 ICMP,目标服务器可能禁用了 ICMP,或者链路中有防火墙、运营商策略、路由策略阻止 ICMP。
排查建议:
- 用 DNS 诊断确认解析结果是否正常。
- 用 Ping 探测确认 ICMP 是否可达。
- 用端口扫描测试目标业务端口,例如 80、443、3389、502 等。
- 用路由追踪查看路径中是否存在连续超时或高延迟跳点。
### 为什么设备发现扫不到某些设备? ### 为什么设备发现扫不到某些设备?
常见原因包括: 常见原因包括:
- 目标设备禁用了 ICMP Echo 响应,无法被 Ping 探活 - 目标设备禁用了 ICMP Echo 响应。
- 目标设备当前网卡不在同一配置网段内。 - 目标设备不在当前网卡配置网段内。
- 本机防火墙或安全软件限制了探测行为。 - 本机防火墙或安全软件限制了探测行为。
- 交换机、VLAN、无线隔离或终端防火墙阻止了局域网发现。 - 交换机、VLAN、无线隔离或终端防火墙阻止了局域网发现。
- 扫描期间目标设备未产生可见 ARP 信息。 - 目标设备未产生可见 ARP 信息。
- 当前选择的网卡不是实际通信所用网卡。
当前设备发现按所选网卡的配置网段扫描,不跨网段扫描,也不解析主机名。 当前设备发现按所选网卡的配置网段扫描,不跨网段,也不解析主机名。
### 为什么厂商显示“未知”?
可能原因:
- MAC 地址为空。
- MAC 地址是随机地址或本地管理地址。
- OUI 厂商库中没有该前缀。
- 本地缓存损坏且离线种子库未正确打包。
- 设备使用了虚拟网卡、代理网关或桥接设备暴露的 MAC。
可以查看右侧输出控制台,确认是否出现 OUI 厂商库加载或更新提示。
### 为什么部分手机显示“随机/本地管理地址”?
很多手机系统会默认开启随机 MAC 或私有 Wi-Fi 地址。此时 MAC 前缀不是厂商真实注册地址,无法通过 OUI 准确识别手机品牌。
### 为什么 DNS 诊断的网卡数量和系统里看到的不一样? ### 为什么 DNS 诊断的网卡数量和系统里看到的不一样?
程序会尽量读取启用和禁用网卡,但部分虚拟网卡、隧道网卡、隐藏适配器或驱动异常的网卡,可能无法提供完整 DNS/IP 信息。可以先在网卡配置页刷新网卡,确认目标网卡是否能被系统命令读取 DNS 诊断只使用活动网卡。禁用网卡、隐藏适配器、隧道网卡、部分虚拟网卡即使能在系统里看到,也可能不会进入 DNS 诊断列表
### 为什么部分按钮是灰色不可用? ### 为什么部分按钮是灰色不可用?
按钮状态会根据当前任务状态网卡状态变化。例如扫描正在运行会禁用开始按钮;没有选择有效网卡或当前网卡状态不支持某项操作时,也会禁用对应按钮。 按钮状态会根据当前任务状态网卡状态和输入内容变化。例如任务运行会禁用开始按钮;选择有效网卡、缺少目标、当前状态不支持某项操作时,也会禁用对应按钮。
### 导出的 CSV 用 Excel 打开乱码怎么办? ### 导出的 CSV 用 Excel 打开乱码怎么办?
@@ -188,20 +341,24 @@ Network-tools
- `ui_main.py`:主窗口布局、侧边栏、公共输出控制台、页面切换。 - `ui_main.py`:主窗口布局、侧边栏、公共输出控制台、页面切换。
- `theme.py`:全局颜色、字体、ttk 样式。 - `theme.py`:全局颜色、字体、ttk 样式。
- `components.py`:公共 Page、Console、字段、按钮等组件。 - `components.py`:公共 Page、Console、字段、按钮等组件。
- `tab_*.py`:各功能页 UI 和事件逻辑 - `tab_network.py`:网卡配置页
- `tab_dns.py`DNS 诊断页。
- `tab_device_discovery.py`:设备发现页。
- `tab_ping.py`Ping 探测页。
- `tab_telnet.py`:端口扫描页。
- `tab_tracert.py`:路由追踪页。
业务逻辑主要位于 `core/Function` 业务逻辑主要位于 `core/Function`
- `network_fun.py`:网卡读取和配置。 - `network_fun.py`:网卡读取、启用、禁用和配置写入
- `dns_diag_fun.py`DNS 诊断 - `dns_diag_fun.py`DNS 解析诊断、摘要和导出
- `device_discovery_fun.py`:设备发现。 - `device_discovery_fun.py`:设备发现、ARP 补充、结果整理和导出
- `oui_lookup.py`:OUI 厂商库加载、查询和联网更新。
- `ping_fun.py`Ping 探测。 - `ping_fun.py`Ping 探测。
- `telnet_fun.py`端口连通性和扫描。 - `telnet_fun.py`TCP 连通性和端口扫描。
- `tracert_fun.py`:路由追踪。 - `tracert_fun.py`:路由追踪。
- `loop_fun.py`:环网检测。
- `ip_conflict_fun.py`IP 冲突检测。
开发时建议先运行 开发时建议先执行编译检查
```powershell ```powershell
python -m compileall main.py core python -m compileall main.py core
@@ -212,3 +369,17 @@ python -m compileall main.py core
```powershell ```powershell
python main.py python main.py
``` ```
## 验证重点
改动功能后建议重点验证:
- 主界面是否能正常启动。
- 左侧导航是否只显示当前保留的功能页。
- 公共输出控制台是否能接收各页面日志。
- 网卡配置是否能正确读取启用和禁用网卡。
- DNS 诊断是否只列出活动网卡。
- 设备发现是否能根据所选网卡生成正确扫描范围。
- OUI 厂商库是否能加载缓存或离线种子库。
- 各结果列表是否无横向滚动条,且关键列内容可见。
- 打包后的 `dist\NetworkTool.exe` 是否能在无网络环境下启动并读取内置 OUI 种子库。
File diff suppressed because one or more lines are too long
+7 -4
View File
@@ -12,6 +12,7 @@ from typing import Callable, Optional
from core.Function.common import run_hidden from core.Function.common import run_hidden
from core.Function.loop_fun import normalize_mac from core.Function.loop_fun import normalize_mac
from core.Function.network_fun import NetworkManager from core.Function.network_fun import NetworkManager
from core.Function.oui_lookup import OuiVendorLookup
OutputCallback = Callable[[str, Optional[str]], None] OutputCallback = Callable[[str, Optional[str]], None]
@@ -63,6 +64,9 @@ OUI_VENDOR_MAP = {
} }
OUI_LOOKUP = OuiVendorLookup(OUI_VENDOR_MAP)
@dataclass @dataclass
class DeviceDiscoveryOptions: class DeviceDiscoveryOptions:
scan_range: str = "" scan_range: str = ""
@@ -106,6 +110,8 @@ class DeviceDiscovery:
self.worker = None self.worker = None
self.last_results: list[dict] = [] self.last_results: list[dict] = []
self.last_summary = "" self.last_summary = ""
OUI_LOOKUP.load(self.output)
OUI_LOOKUP.update_if_stale_async(self.output)
def get_adapter_choices(self) -> list[str]: def get_adapter_choices(self) -> list[str]:
adapters = self._active_adapters(self.network.get_network_info()) adapters = self._active_adapters(self.network.get_network_info())
@@ -543,10 +549,7 @@ def normalize_neighbor(item: dict) -> dict:
def vendor_name(mac: str) -> str: def vendor_name(mac: str) -> str:
normalized = normalize_mac(mac) return OUI_LOOKUP.lookup(mac)
if len(normalized) < 8:
return "未知"
return OUI_VENDOR_MAP.get(normalized[:8], "未知")
def build_summary(rows: list[dict], stopped: bool = False) -> str: def build_summary(rows: list[dict], stopped: bool = False) -> str:
+5 -5
View File
@@ -76,13 +76,13 @@ class DnsDiagnostic:
self.local_dns_servers: set[str] = set() self.local_dns_servers: set[str] = set()
def get_adapter_choices(self) -> list[str]: def get_adapter_choices(self) -> list[str]:
adapters = self.network.get_network_info() adapters = self._active_adapters(self.network.get_network_info())
return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters] return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
def get_adapter_choices_and_default_dns(self) -> tuple[list[str], str]: def get_adapter_choices_and_default_dns(self) -> tuple[list[str], str]:
adapters = self.network.get_network_info() adapters = self._active_adapters(self.network.get_network_info())
choices = [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters] choices = [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
servers = collect_adapter_dns(self._active_adapters(adapters)) servers = collect_adapter_dns(adapters)
default_dns = ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS))) default_dns = ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS)))
return choices, default_dns return choices, default_dns
@@ -324,9 +324,9 @@ class DnsDiagnostic:
return [adapter for adapter in adapters if adapter.get("name") == adapter_name] return [adapter for adapter in adapters if adapter.get("name") == adapter_name]
def _select_dns_source_adapters(self, adapter_name: str) -> list[dict]: def _select_dns_source_adapters(self, adapter_name: str) -> list[dict]:
adapters = self.network.get_network_info() adapters = self._active_adapters(self.network.get_network_info())
if not adapter_name or adapter_name == ALL_ADAPTERS: if not adapter_name or adapter_name == ALL_ADAPTERS:
return self._active_adapters(adapters) return adapters
return [adapter for adapter in adapters if adapter.get("name") == adapter_name] return [adapter for adapter in adapters if adapter.get("name") == adapter_name]
def _select_repair_adapters(self, adapter_name: str) -> list[dict]: def _select_repair_adapters(self, adapter_name: str) -> list[dict]:
+223
View File
@@ -0,0 +1,223 @@
import csv
import json
import os
import re
import sys
import tempfile
import threading
import time
import urllib.request
from pathlib import Path
from typing import Callable, Optional
OutputCallback = Callable[[str, Optional[str]], None]
IEEE_OUI_SOURCES = [
("MA-L", "https://standards-oui.ieee.org/oui/oui.csv"),
("MA-M", "https://standards-oui.ieee.org/oui28/mam.csv"),
("MA-S", "https://standards-oui.ieee.org/oui36/oui36.csv"),
]
UPDATE_INTERVAL_SECONDS = 7 * 24 * 60 * 60
UNKNOWN_VENDOR = "未知"
LOCAL_ADMIN_VENDOR = "随机/本地管理地址"
class OuiVendorLookup:
def __init__(self, fallback_map: Optional[dict[str, str]] = None):
self.fallback_map = {normalize_prefix(key): value for key, value in (fallback_map or {}).items()}
self.vendors: dict[str, str] = {}
self.source = "未加载"
self.updated_at = 0.0
self._loaded = False
self._update_started = False
self._lock = threading.RLock()
def load(self, output: Optional[OutputCallback] = None) -> None:
with self._lock:
if self._loaded:
return
loaded = self._load_json_file(cache_file())
if loaded:
self.vendors, self.updated_at = loaded
self.source = "本地缓存"
self._loaded = True
write(output, f"已加载 OUI 厂商缓存:{len(self.vendors)}\n", "muted")
return
loaded = self._load_json_file(seed_file())
if loaded:
self.vendors, self.updated_at = loaded
self.source = "离线内置"
self._loaded = True
write(output, f"已加载离线 OUI 厂商库:{len(self.vendors)}\n", "muted")
return
self.vendors = dict(self.fallback_map)
self.updated_at = 0.0
self.source = "内置兜底"
self._loaded = True
write(output, f"已加载内置 OUI 兜底表:{len(self.vendors)}\n", "muted")
def lookup(self, mac: str) -> str:
self.load()
normalized = normalize_mac_hex(mac)
if len(normalized) < 6:
return UNKNOWN_VENDOR
with self._lock:
for prefix_length in (9, 7, 6):
if len(normalized) >= prefix_length:
vendor = self.vendors.get(normalized[:prefix_length])
if vendor:
return vendor
fallback = self.fallback_map.get(normalized[:prefix_length])
if fallback:
return fallback
if is_locally_administered(normalized):
return LOCAL_ADMIN_VENDOR
return UNKNOWN_VENDOR
def update_if_stale_async(self, output: Optional[OutputCallback] = None) -> None:
self.load(output)
with self._lock:
if self._update_started or not self.is_stale():
return
self._update_started = True
thread = threading.Thread(target=self._update_worker, args=(output,), daemon=True)
thread.start()
def update_now(self, output: Optional[OutputCallback] = None) -> bool:
self.load(output)
vendors = download_ieee_oui()
if not vendors:
raise RuntimeError("未下载到有效 OUI 数据")
payload = {"updated_at": time.time(), "vendors": vendors}
save_json_atomic(cache_file(), payload)
with self._lock:
self.vendors = vendors
self.updated_at = float(payload["updated_at"])
self.source = "联网更新"
write(output, f"OUI 厂商库已更新:{len(vendors)}\n", "success")
return True
def is_stale(self) -> bool:
if not self.updated_at:
return True
return time.time() - self.updated_at > UPDATE_INTERVAL_SECONDS
def _update_worker(self, output: Optional[OutputCallback]) -> None:
try:
self.update_now(output)
except Exception as exc:
write(output, f"OUI 厂商库更新失败,继续使用离线库:{exc}\n", "warning")
def _load_json_file(self, path: Path) -> Optional[tuple[dict[str, str], float]]:
try:
if not path.exists():
return None
with path.open("r", encoding="utf-8") as file:
payload = json.load(file)
raw_vendors = payload.get("vendors", {})
vendors = {
normalize_prefix(prefix): compact_vendor_name(vendor)
for prefix, vendor in raw_vendors.items()
if normalize_prefix(prefix) and compact_vendor_name(vendor)
}
if not vendors:
return None
return vendors, float(payload.get("updated_at") or 0)
except Exception:
return None
def download_ieee_oui() -> dict[str, str]:
vendors: dict[str, str] = {}
for _registry, url in IEEE_OUI_SOURCES:
text = download_text(url)
vendors.update(parse_ieee_csv(text))
return vendors
def download_text(url: str) -> str:
request = urllib.request.Request(
url,
headers={
"User-Agent": "NetPilot/1.0 (+https://standards.ieee.org/develop/regauth/)",
"Accept": "text/csv,text/plain,*/*",
},
)
with urllib.request.urlopen(request, timeout=25) as response:
return response.read().decode("utf-8-sig", errors="replace")
def parse_ieee_csv(text: str) -> dict[str, str]:
rows = csv.DictReader(text.splitlines())
vendors: dict[str, str] = {}
for row in rows:
prefix = normalize_prefix(row.get("Assignment", ""))
vendor = compact_vendor_name(row.get("Organization Name", ""))
if prefix and vendor:
vendors[prefix] = vendor
return vendors
def normalize_prefix(value: str) -> str:
cleaned = re.sub(r"[^0-9A-Fa-f]", "", str(value or "")).upper()
if len(cleaned) in (6, 7, 9):
return cleaned
return ""
def normalize_mac_hex(value: str) -> str:
return re.sub(r"[^0-9A-Fa-f]", "", str(value or "")).upper()
def compact_vendor_name(value: str) -> str:
return re.sub(r"\s+", " ", str(value or "")).strip()
def is_locally_administered(mac_hex: str) -> bool:
try:
return bool(int(mac_hex[:2], 16) & 0x02)
except Exception:
return False
def cache_file() -> Path:
base = os.environ.get("LOCALAPPDATA") or os.path.join(Path.home(), "AppData", "Local")
return Path(base) / "NetPilot" / "oui_vendors.json"
def seed_file() -> Path:
candidates = []
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
candidates.append(Path(sys._MEIPASS) / "assets" / "oui_vendors_seed.json")
candidates.append(Path(__file__).resolve().parents[2] / "assets" / "oui_vendors_seed.json")
if getattr(sys, "frozen", False):
candidates.append(Path(sys.executable).resolve().parent / "assets" / "oui_vendors_seed.json")
for path in candidates:
if path.exists():
return path
return candidates[0]
def save_json_atomic(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, temp_name = tempfile.mkstemp(prefix=path.name, suffix=".tmp", dir=str(path.parent))
try:
with os.fdopen(fd, "w", encoding="utf-8") as file:
json.dump(payload, file, ensure_ascii=False, separators=(",", ":"))
os.replace(temp_name, path)
finally:
if os.path.exists(temp_name):
os.remove(temp_name)
def write(output: Optional[OutputCallback], text: str, tag: Optional[str] = None) -> None:
if output:
output(text, tag)
+6 -6
View File
@@ -86,14 +86,14 @@ class Page(ttk.Frame):
self.rowconfigure(1, weight=1) self.rowconfigure(1, weight=1)
header = ttk.Frame(self, style="Page.TFrame") header = ttk.Frame(self, style="Page.TFrame")
header.grid(row=0, column=0, sticky="ew", padx=28, pady=(28, 12)) header.grid(row=0, column=0, sticky="ew", padx=32, pady=(28, 12))
header.columnconfigure(0, weight=1) header.columnconfigure(0, weight=1)
ttk.Label(header, text=title, style="Title.TLabel").grid(row=0, column=0, sticky="w") ttk.Label(header, text=title, style="Title.TLabel").grid(row=0, column=0, sticky="w")
ttk.Label(header, text=subtitle, style="PageMuted.TLabel").grid(row=1, column=0, sticky="w", pady=(8, 0)) ttk.Label(header, text=subtitle, style="PageMuted.TLabel").grid(row=1, column=0, sticky="w", pady=(8, 0))
body_shell = ttk.Frame(self, style="Page.TFrame") body_shell = ttk.Frame(self, style="Page.TFrame")
body_shell.grid(row=1, column=0, sticky="nsew", padx=28, pady=(0, 22)) body_shell.grid(row=1, column=0, sticky="nsew", padx=32, pady=(0, 22))
body_shell.columnconfigure(0, weight=1) body_shell.columnconfigure(0, weight=1)
body_shell.rowconfigure(0, weight=1) body_shell.rowconfigure(0, weight=1)
@@ -131,7 +131,7 @@ class Page(ttk.Frame):
frame.columnconfigure(col, weight=1) frame.columnconfigure(col, weight=1)
title_bar = ttk.Frame(frame, style="Panel.TFrame") title_bar = ttk.Frame(frame, style="Panel.TFrame")
title_bar.grid(row=0, column=0, columnspan=columns, sticky="ew", padx=22, pady=(18, 10)) title_bar.grid(row=0, column=0, columnspan=columns, sticky="ew", padx=20, pady=(18, 10))
ttk.Label(title_bar, text="", style="Section.TLabel", foreground=COLORS["primary"]).pack(side="left", padx=(0, 10)) ttk.Label(title_bar, text="", style="Section.TLabel", foreground=COLORS["primary"]).pack(side="left", padx=(0, 10))
ttk.Label(title_bar, text=title, style="Section.TLabel").pack(side="left") ttk.Label(title_bar, text=title, style="Section.TLabel").pack(side="left")
return frame return frame
@@ -161,7 +161,7 @@ class Page(ttk.Frame):
def field(parent, label: str, row: int, column: int, value: str = "", width: int = 24, colspan: int = 1): def field(parent, label: str, row: int, column: int, value: str = "", width: int = 24, colspan: int = 1):
frame = ttk.Frame(parent, style="Panel.TFrame") frame = ttk.Frame(parent, style="Panel.TFrame")
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=22, pady=(4, 16)) frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=16, pady=(4, 16))
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 7)) ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 7))
@@ -174,7 +174,7 @@ def field(parent, label: str, row: int, column: int, value: str = "", width: int
def combo(parent, label: str, row: int, column: int, values=None, value: str = "", width: int = 24, colspan: int = 1): def combo(parent, label: str, row: int, column: int, values=None, value: str = "", width: int = 24, colspan: int = 1):
values = values or [] values = values or []
frame = ttk.Frame(parent, style="Panel.TFrame") frame = ttk.Frame(parent, style="Panel.TFrame")
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=22, pady=(4, 16)) frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=16, pady=(4, 16))
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 7)) ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 7))
@@ -186,7 +186,7 @@ def combo(parent, label: str, row: int, column: int, values=None, value: str = "
def action_bar(parent, row: int, columnspan: int = 4): def action_bar(parent, row: int, columnspan: int = 4):
frame = ttk.Frame(parent, style="Panel.TFrame") frame = ttk.Frame(parent, style="Panel.TFrame")
frame.grid(row=row, column=0, columnspan=columnspan, sticky="ew", padx=22, pady=(0, 18)) frame.grid(row=row, column=0, columnspan=columnspan, sticky="ew", padx=16, pady=(0, 18))
return frame return frame
+26 -23
View File
@@ -12,30 +12,30 @@ class DeviceDiscoveryTab(Page):
self.console = console self.console = console
self.body.rowconfigure(2, weight=1) self.body.rowconfigure(2, weight=1)
status = self.section("实时状态", 0, columns=6) status = self.section("实时状态", 0, columns=7)
self.state = field(status, "状态", 1, 0, "等待", 10) self.state = field(status, "状态", 1, 0, "等待", 10)
self.current = field(status, "当前网卡", 1, 1, "-", 18) self.current = field(status, "当前网卡", 1, 1, "-", 18)
self.scan_scope = field(status, "扫描范围", 1, 2, "-", 20) self.scan_scope = field(status, "扫描范围", 1, 2, "-", 22, colspan=2)
self.progress = field(status, "进度", 1, 3, "0/0", 10) self.progress = field(status, "进度", 1, 4, "0/0", 10)
self.found = field(status, "发现设备", 1, 4, "0", 8) self.found = field(status, "发现设备", 1, 5, "0", 8)
self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10) self.elapsed = field(status, "耗时", 1, 6, "0.0s", 10)
for item in (self.state, self.current, self.scan_scope, self.progress, self.found, self.elapsed): for item in (self.state, self.current, self.scan_scope, self.progress, self.found, self.elapsed):
item["entry"].configure(state="disabled") item["entry"].configure(state="disabled")
params = self.section("发现参数", 1, columns=6) params = self.section("发现参数", 1, columns=6)
self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22) self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22)
self.adapter["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.fill_default_range(silent=True)) self.adapter["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.fill_default_range(silent=True))
self.scan_range = field(params, "扫描范围", 1, 1, "", 34, colspan=2) self.scan_range = field(params, "扫描范围", 1, 1, "", 42, colspan=3)
self.workers = field(params, "并发数", 1, 3, "24", 10) self.workers = field(params, "并发数", 1, 4, "24", 10)
self.timeout = field(params, "超时 ms", 1, 4, "500", 10) self.timeout = field(params, "超时 ms", 1, 5, "500", 10)
self.max_hosts = field(params, "最大地址数", 1, 5, "254", 10) self.max_hosts = field(params, "最大地址数", 2, 0, "254", 10)
primary_actions = action_bar(params, 2, 6) primary_actions = action_bar(params, 3, 6)
self.start_btn = button(primary_actions, "开始发现", self.start_discovery, "Primary.TButton") self.start_btn = button(primary_actions, "开始发现", self.start_discovery, "Primary.TButton")
self.stop_btn = button(primary_actions, "停止", self.stop_discovery, "Danger.TButton") self.stop_btn = button(primary_actions, "停止", self.stop_discovery, "Danger.TButton")
self.refresh_btn = button(primary_actions, "刷新网卡", self.load_adapters, "Secondary.TButton") self.refresh_btn = button(primary_actions, "刷新网卡", self.load_adapters, "Secondary.TButton")
self.auto_range_btn = button(primary_actions, "自动范围", self.fill_default_range, "Secondary.TButton") self.auto_range_btn = button(primary_actions, "自动范围", self.fill_default_range, "Secondary.TButton")
self.stop_btn.configure(state="disabled") self.stop_btn.configure(state="disabled")
secondary_actions = action_bar(params, 3, 6) secondary_actions = action_bar(params, 4, 6)
self.copy_btn = button(secondary_actions, "复制清单", self.copy_inventory, "Secondary.TButton") self.copy_btn = button(secondary_actions, "复制清单", self.copy_inventory, "Secondary.TButton")
self.export_btn = button(secondary_actions, "导出CSV", self.export_results, "Secondary.TButton") self.export_btn = button(secondary_actions, "导出CSV", self.export_results, "Secondary.TButton")
@@ -58,26 +58,29 @@ class DeviceDiscoveryTab(Page):
"note": "备注", "note": "备注",
} }
widths = { widths = {
"ip": 112, "ip": 105,
"mac": 132, "mac": 170,
"vendor": 120, "vendor": 270,
"adapter": 150, "adapter": 80,
"latency": 72, "latency": 65,
"method": 80, "method": 80,
"note": 140, "note": 100,
} }
for column, title in headings.items(): for column, title in headings.items():
self.results_tree.heading(column, text=title) self.results_tree.heading(column, text=title)
self.results_tree.column(column, width=widths[column], anchor="w") self.results_tree.column(column, width=widths[column], minwidth=widths[column], anchor="w", stretch=False)
self.results_tree.tag_configure("在线", foreground="#15803d") self.results_tree.tag_configure("在线", foreground="#15803d")
self.results_tree.tag_configure("ARP 可见", foreground="#b7791f") self.results_tree.tag_configure("ARP 可见", foreground="#b7791f")
self.results_tree.tag_configure("本机", foreground="#246bfe") self.results_tree.tag_configure("本机", foreground="#246bfe")
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview) result_scroll = ttk.Scrollbar(
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) results,
x_scroll = ttk.Scrollbar(results, orient="horizontal", command=self.results_tree.xview) orient="vertical",
x_scroll.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 18)) command=self.results_tree.yview,
self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set) style="Modern.Vertical.TScrollbar",
)
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18), padx=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set)
self.discovery = DeviceDiscovery(self.write, self.on_task_done, self.update_status, self.add_result) self.discovery = DeviceDiscovery(self.write, self.on_task_done, self.update_status, self.add_result)
self.after(350, self.load_adapters) self.after(350, self.load_adapters)
+22 -18
View File
@@ -12,13 +12,13 @@ class DnsTab(Page):
self.console = console self.console = console
self.body.rowconfigure(2, weight=1) self.body.rowconfigure(2, weight=1)
status = self.section("实时状态", 0, columns=6) status = self.section("实时状态", 0, columns=7)
self.state = field(status, "状态", 1, 0, "等待", 10) self.state = field(status, "状态", 1, 0, "等待", 10)
self.current_dns = field(status, "当前 DNS", 1, 1, "-", 24, colspan=2) self.current_dns = field(status, "当前 DNS", 1, 1, "-", 18, colspan=2)
self.target_total = field(status, "目标数", 1, 3, "0", 8) self.target_total = field(status, "目标数", 1, 3, "0", 8)
self.progress = field(status, "进度", 1, 4, "0/0", 10) self.progress = field(status, "进度", 1, 4, "0/0", 10)
self.abnormal = field(status, "异常数", 1, 5, "0", 8) self.abnormal = field(status, "异常数", 1, 5, "0", 8)
self.elapsed = field(status, "耗时", 2, 0, "0.0s", 10) self.elapsed = field(status, "耗时", 1, 6, "0.0s", 10)
for item in (self.state, self.current_dns, self.target_total, self.progress, self.abnormal, self.elapsed): for item in (self.state, self.current_dns, self.target_total, self.progress, self.abnormal, self.elapsed):
item["entry"].configure(state="disabled") item["entry"].configure(state="disabled")
@@ -35,8 +35,9 @@ class DnsTab(Page):
self.refresh_btn = button(actions, "刷新网卡", self.load_adapters, "Secondary.TButton") self.refresh_btn = button(actions, "刷新网卡", self.load_adapters, "Secondary.TButton")
self.auto_dns_btn = button(actions, "自动 DNS", self.fill_default_dns, "Secondary.TButton") self.auto_dns_btn = button(actions, "自动 DNS", self.fill_default_dns, "Secondary.TButton")
self.repair_btn = button(actions, "修复异常", self.repair_dns, "Secondary.TButton") self.repair_btn = button(actions, "修复异常", self.repair_dns, "Secondary.TButton")
self.copy_btn = button(actions, "复制摘要", self.copy_summary, "Secondary.TButton") secondary_actions = action_bar(params, 4, 6)
self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton") self.copy_btn = button(secondary_actions, "复制摘要", self.copy_summary, "Secondary.TButton")
self.export_btn = button(secondary_actions, "导出 CSV", self.export_results, "Secondary.TButton")
results = self.section("诊断结果", 2, columns=1) results = self.section("诊断结果", 2, columns=1)
results.rowconfigure(1, weight=1) results.rowconfigure(1, weight=1)
@@ -57,26 +58,29 @@ class DnsTab(Page):
"verdict": "错误 / 判断", "verdict": "错误 / 判断",
} }
widths = { widths = {
"domain": 130, "domain": 135,
"type": 56, "type": 55,
"server": 116, "server": 115,
"status": 64, "status": 65,
"elapsed": 76, "elapsed": 78,
"values": 220, "values": 250,
"verdict": 260, "verdict": 130,
} }
for column, title in headings.items(): for column, title in headings.items():
self.results_tree.heading(column, text=title) self.results_tree.heading(column, text=title)
self.results_tree.column(column, width=widths[column], anchor="w") self.results_tree.column(column, width=widths[column], minwidth=widths[column], anchor="w", stretch=False)
self.results_tree.tag_configure("正常", foreground="#15803d") self.results_tree.tag_configure("正常", foreground="#15803d")
self.results_tree.tag_configure("无记录", foreground="#b7791f") self.results_tree.tag_configure("无记录", foreground="#b7791f")
self.results_tree.tag_configure("失败", foreground="#dc2626") self.results_tree.tag_configure("失败", foreground="#dc2626")
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview) result_scroll = ttk.Scrollbar(
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) results,
x_scroll = ttk.Scrollbar(results, orient="horizontal", command=self.results_tree.xview) orient="vertical",
x_scroll.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 18)) command=self.results_tree.yview,
self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set) style="Modern.Vertical.TScrollbar",
)
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18), padx=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set)
self.diagnostic = DnsDiagnostic(self.write, self.on_task_done, self.update_status, self.add_result) self.diagnostic = DnsDiagnostic(self.write, self.on_task_done, self.update_status, self.add_result)
self.after(350, self.load_adapters) self.after(350, self.load_adapters)
+12 -10
View File
@@ -15,7 +15,7 @@ class IpConflictTab(Page):
status = self.section("实时状态", 0, columns=6) status = self.section("实时状态", 0, columns=6)
self.state = field(status, "状态", 1, 0, "等待", 10) self.state = field(status, "状态", 1, 0, "等待", 10)
self.adapter_total = field(status, "网卡数", 1, 1, "0", 8) self.adapter_total = field(status, "网卡数", 1, 1, "0", 8)
self.current = field(status, "当前对象", 1, 2, "-", 18) self.current = field(status, "当前对象", 1, 2, "-", 24)
self.max_risk = field(status, "最高风险", 1, 3, "正常", 10) self.max_risk = field(status, "最高风险", 1, 3, "正常", 10)
self.progress = field(status, "进度", 1, 4, "0/0", 10) self.progress = field(status, "进度", 1, 4, "0/0", 10)
self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10) self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10)
@@ -25,7 +25,7 @@ class IpConflictTab(Page):
params = self.section("检测参数", 1, columns=6) params = self.section("检测参数", 1, columns=6)
self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22) self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22)
self.mode = combo(params, "检测模式", 1, 1, [MODE_BOTH, MODE_LOCAL, MODE_SUBNET], MODE_BOTH, 14) self.mode = combo(params, "检测模式", 1, 1, [MODE_BOTH, MODE_LOCAL, MODE_SUBNET], MODE_BOTH, 14)
self.scan_range = field(params, "扫描范围", 1, 2, "", 30, colspan=2) self.scan_range = field(params, "扫描范围", 1, 2, "", 42, colspan=2)
self.workers = field(params, "并发数", 1, 4, "64", 10) self.workers = field(params, "并发数", 1, 4, "64", 10)
self.timeout = field(params, "超时 ms", 1, 5, "500", 10) self.timeout = field(params, "超时 ms", 1, 5, "500", 10)
self.max_hosts = field(params, "最大地址数", 2, 0, "254", 10) self.max_hosts = field(params, "最大地址数", 2, 0, "254", 10)
@@ -56,13 +56,13 @@ class IpConflictTab(Page):
"verdict": "判断", "verdict": "判断",
} }
widths = { widths = {
"adapter": 130, "adapter": 120,
"ip": 150, "ip": 110,
"mac": 150, "mac": 135,
"conflict": 180, "conflict": 145,
"type": 110, "type": 90,
"level": 90, "level": 70,
"verdict": 420, "verdict": 200,
} }
for column, title in headings.items(): for column, title in headings.items():
self.results_tree.heading(column, text=title) self.results_tree.heading(column, text=title)
@@ -73,7 +73,9 @@ class IpConflictTab(Page):
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview) result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview)
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set) x_scroll = ttk.Scrollbar(results, orient="horizontal", command=self.results_tree.xview)
x_scroll.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set)
self.detector = IpConflictDetector(self.write, self.on_task_done, self.update_status, self.add_result) self.detector = IpConflictDetector(self.write, self.on_task_done, self.update_status, self.add_result)
self.after(350, self.load_adapters) self.after(350, self.load_adapters)
+19 -17
View File
@@ -15,7 +15,7 @@ class LoopTab(Page):
status = self.section("实时状态", 0, columns=6) status = self.section("实时状态", 0, columns=6)
self.state = field(status, "状态", 1, 0, "等待", 10) self.state = field(status, "状态", 1, 0, "等待", 10)
self.adapter_total = field(status, "网卡数", 1, 1, "0", 8) self.adapter_total = field(status, "网卡数", 1, 1, "0", 8)
self.current_adapter = field(status, "当前网卡", 1, 2, "-", 18) self.current_adapter = field(status, "当前网卡", 1, 2, "-", 24)
self.risk_level = field(status, "风险等级", 1, 3, "正常", 10) self.risk_level = field(status, "风险等级", 1, 3, "正常", 10)
self.max_score = field(status, "最高分", 1, 4, "0", 8) self.max_score = field(status, "最高分", 1, 4, "0", 8)
self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10) self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10)
@@ -23,10 +23,10 @@ class LoopTab(Page):
item["entry"].configure(state="disabled") item["entry"].configure(state="disabled")
params = self.section("检测参数", 1, columns=5) params = self.section("检测参数", 1, columns=5)
self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 24) self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 34, colspan=2)
self.duration = field(params, "检测时长 s", 1, 1, "15", 10) self.duration = field(params, "检测时长 s", 1, 2, "15", 10)
self.interval = field(params, "采样间隔 s", 1, 2, "1", 10) self.interval = field(params, "采样间隔 s", 1, 3, "1", 10)
self.ping_timeout = field(params, "网关 Ping 超时 ms", 1, 3, "800", 12) self.ping_timeout = field(params, "网关 Ping 超时 ms", 1, 4, "800", 12)
actions = action_bar(params, 2, 5) actions = action_bar(params, 2, 5)
self.start_btn = button(actions, "开始检测", self.start_detection, "Primary.TButton") self.start_btn = button(actions, "开始检测", self.start_detection, "Primary.TButton")
self.stop_btn = button(actions, "停止", self.stop_detection, "Danger.TButton") self.stop_btn = button(actions, "停止", self.stop_detection, "Danger.TButton")
@@ -57,17 +57,17 @@ class LoopTab(Page):
"verdict": "判断", "verdict": "判断",
} }
widths = { widths = {
"adapter": 140, "adapter": 110,
"ipv4": 120, "ipv4": 96,
"gateway": 120, "gateway": 96,
"non_unicast": 90, "non_unicast": 72,
"ratio": 90, "ratio": 78,
"loss": 80, "loss": 70,
"jitter": 80, "jitter": 72,
"arp": 80, "arp": 58,
"score": 60, "score": 50,
"level": 80, "level": 62,
"verdict": 360, "verdict": 150,
} }
for column, title in headings.items(): for column, title in headings.items():
self.results_tree.heading(column, text=title) self.results_tree.heading(column, text=title)
@@ -78,7 +78,9 @@ class LoopTab(Page):
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview) result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview)
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set) x_scroll = ttk.Scrollbar(results, orient="horizontal", command=self.results_tree.xview)
x_scroll.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set)
self.detector = LoopDetector(self.write, self.on_task_done, self.update_status, self.add_result) self.detector = LoopDetector(self.write, self.on_task_done, self.update_status, self.add_result)
self.after(350, self.load_adapters) self.after(350, self.load_adapters)
+16 -16
View File
@@ -13,39 +13,39 @@ class NetworkTab(Page):
self.adapters = [] self.adapters = []
self.profiles = {} self.profiles = {}
selector = self.section("网卡选择", 0, columns=4) selector = self.section("网卡选择", 0, columns=6)
self.iface = combo(selector, "选择网卡", 1, 0, [], "", 44, colspan=2) self.iface = combo(selector, "选择网卡", 1, 0, [], "", 56, colspan=3)
self.iface["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.load_selected_adapter()) self.iface["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.load_selected_adapter())
self.status = field(selector, "连接状态", 1, 2, "", 14) self.status = field(selector, "连接状态", 1, 3, "", 18)
self.status["entry"].configure(state="disabled") self.status["entry"].configure(state="disabled")
actions = action_bar(selector, 2, 4) actions = action_bar(selector, 2, 6)
self.refresh_btn = button(actions, "刷新网卡", self.refresh_adapters, "Primary.TButton") self.refresh_btn = button(actions, "刷新网卡", self.refresh_adapters, "Primary.TButton")
identity = self.section("网卡信息", 1, columns=4) identity = self.section("网卡信息", 1, columns=6)
self.description = field(identity, "设备描述", 1, 0, "", 46, colspan=2) self.description = field(identity, "设备描述", 1, 0, "", 62, colspan=3)
self.mac = field(identity, "MAC 地址", 1, 2, "", 24) self.mac = field(identity, "MAC 地址", 1, 3, "", 26)
adapter_actions = action_bar(identity, 2, 4) adapter_actions = action_bar(identity, 2, 6)
self.enable_adapter_btn = button(adapter_actions, "启用网卡", self.enable_adapter, "Primary.TButton") self.enable_adapter_btn = button(adapter_actions, "启用网卡", self.enable_adapter, "Primary.TButton")
self.disable_adapter_btn = button(adapter_actions, "禁用网卡", self.disable_adapter, "Danger.TButton") self.disable_adapter_btn = button(adapter_actions, "禁用网卡", self.disable_adapter, "Danger.TButton")
for item in (self.description, self.mac): for item in (self.description, self.mac):
item["entry"].configure(state="disabled") item["entry"].configure(state="disabled")
config = self.section("IPv4 配置", 2, columns=4) config = self.section("IPv4 配置", 2, columns=6)
self.dhcp = combo(config, "地址模式", 1, 0, ["DHCP 自动获取", "静态手动配置"], "DHCP 自动获取", 18) self.dhcp = combo(config, "地址模式", 1, 0, ["DHCP 自动获取", "静态手动配置"], "DHCP 自动获取", 18)
self.dhcp["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.update_entry_state()) self.dhcp["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.update_entry_state())
self.ipv4 = field(config, "IPv4 地址", 1, 1, "", 18) self.ipv4 = field(config, "IPv4 地址", 1, 1, "", 18)
self.netmask = field(config, "子网掩码", 1, 2, "", 18) self.netmask = field(config, "子网掩码", 1, 2, "", 18)
self.gateway = field(config, "默认网关", 1, 3, "", 18) self.gateway = field(config, "默认网关", 1, 3, "", 18)
self.dns1 = field(config, "首选 DNS", 2, 1, "", 18) self.dns1 = field(config, "首选 DNS", 1, 4, "", 18)
self.dns2 = field(config, "备用 DNS", 2, 2, "", 18) self.dns2 = field(config, "备用 DNS", 1, 5, "", 18)
config_actions = action_bar(config, 3, 4) config_actions = action_bar(config, 2, 6)
self.apply_btn = button(config_actions, "应用配置", self.apply_settings, "Primary.TButton") self.apply_btn = button(config_actions, "应用配置", self.apply_settings, "Primary.TButton")
self.reload_btn = button(config_actions, "重新读取", self.refresh_adapters, "Secondary.TButton") self.reload_btn = button(config_actions, "重新读取", self.refresh_adapters, "Secondary.TButton")
profiles = self.section("配置模板", 3, columns=4) profiles = self.section("配置模板", 3, columns=6)
self.profile_name = field(profiles, "模板名称", 1, 0, "", 20) self.profile_name = field(profiles, "模板名称", 1, 0, "", 28, colspan=2)
self.profile_select = combo(profiles, "选择模板", 1, 1, [], "", 24) self.profile_select = combo(profiles, "选择模板", 1, 2, [], "", 34, colspan=2)
profile_actions = action_bar(profiles, 2, 4) profile_actions = action_bar(profiles, 2, 6)
self.save_profile_btn = button(profile_actions, "保存当前为模板", self.save_profile, "Primary.TButton") self.save_profile_btn = button(profile_actions, "保存当前为模板", self.save_profile, "Primary.TButton")
self.apply_profile_btn = button(profile_actions, "套用模板到表单", self.apply_profile_to_form, "Secondary.TButton") self.apply_profile_btn = button(profile_actions, "套用模板到表单", self.apply_profile_to_form, "Secondary.TButton")
self.delete_profile_btn = button(profile_actions, "删除模板", self.delete_profile, "Danger.TButton") self.delete_profile_btn = button(profile_actions, "删除模板", self.delete_profile, "Danger.TButton")
+19 -17
View File
@@ -30,8 +30,10 @@ class PingTab(Page):
item["entry"].configure(state="disabled") item["entry"].configure(state="disabled")
modes = self.section("探测模式", 1, columns=1) modes = self.section("探测模式", 1, columns=1)
modes.rowconfigure(1, weight=1)
modes.columnconfigure(0, weight=1)
self.tabs = ttk.Notebook(modes) self.tabs = ttk.Notebook(modes)
self.tabs.grid(row=1, column=0, sticky="ew", padx=18, pady=(0, 18)) self.tabs.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.single_tab = ttk.Frame(self.tabs, style="Panel.TFrame") self.single_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
self.batch_tab = ttk.Frame(self.tabs, style="Panel.TFrame") self.batch_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
self.tabs.add(self.single_tab, text="单点探测") self.tabs.add(self.single_tab, text="单点探测")
@@ -47,18 +49,18 @@ class PingTab(Page):
for col in range(6): for col in range(6):
self.single_tab.columnconfigure(col, weight=1) self.single_tab.columnconfigure(col, weight=1)
self.local_ip = combo(self.single_tab, "本地源 IP", 0, 0, [], "", 22) self.local_ip = combo(self.single_tab, "本地源 IP", 0, 0, [], "", 28)
self.target = field(self.single_tab, "目标 IP / 域名", 0, 1, "127.0.0.1", 28) self.target = field(self.single_tab, "目标 IP / 域名", 0, 1, "127.0.0.1", 36, colspan=2)
self.single_mode = combo(self.single_tab, "模式", 0, 2, ["持续", "指定次数"], "持续", 12) self.single_mode = combo(self.single_tab, "模式", 0, 3, ["持续", "指定次数"], "持续", 12)
self.count = field(self.single_tab, "次数", 0, 3, "4", 8) self.count = field(self.single_tab, "次数", 0, 4, "4", 8)
self.interval = field(self.single_tab, "间隔 ms", 0, 4, "1000", 10) self.interval = field(self.single_tab, "间隔 ms", 0, 5, "1000", 10)
self.timeout = field(self.single_tab, "超时 ms", 0, 5, "1200", 10) self.timeout = field(self.single_tab, "超时 ms", 1, 0, "1200", 10)
self.size = field(self.single_tab, "包大小 bytes", 1, 0, "32", 12) self.size = field(self.single_tab, "包大小 bytes", 1, 1, "32", 12)
self.ttl = field(self.single_tab, "TTL0=默认)", 1, 1, "0", 12) self.ttl = field(self.single_tab, "TTL0=默认)", 1, 2, "0", 12)
self.df_var = tk.BooleanVar(value=False) self.df_var = tk.BooleanVar(value=False)
df_frame = ttk.Frame(self.single_tab, style="Panel.TFrame") df_frame = ttk.Frame(self.single_tab, style="Panel.TFrame")
df_frame.grid(row=1, column=2, sticky="ew", padx=18, pady=(22, 14)) df_frame.grid(row=1, column=3, sticky="ew", padx=18, pady=(22, 14))
ttk.Checkbutton(df_frame, text="禁止分片", variable=self.df_var).pack(anchor="w") ttk.Checkbutton(df_frame, text="禁止分片", variable=self.df_var).pack(anchor="w")
actions = action_bar(self.single_tab, 2, 6) actions = action_bar(self.single_tab, 2, 6)
@@ -70,16 +72,16 @@ class PingTab(Page):
for col in range(6): for col in range(6):
self.batch_tab.columnconfigure(col, weight=1) self.batch_tab.columnconfigure(col, weight=1)
self.batch_local_ip = combo(self.batch_tab, "本地源 IP", 0, 0, [], "", 22) self.batch_local_ip = combo(self.batch_tab, "本地源 IP", 0, 0, [], "", 28)
self.targets = field(self.batch_tab, "目标范围 / 列表", 0, 1, "192.168.1.0/24", 42, colspan=2) self.targets = field(self.batch_tab, "目标范围 / 列表", 0, 1, "192.168.1.0/24", 52, colspan=3)
self.batch_timeout = field(self.batch_tab, "超时 ms", 0, 3, "1200", 10) self.batch_timeout = field(self.batch_tab, "超时 ms", 0, 4, "1200", 10)
self.batch_size = field(self.batch_tab, "包大小 bytes", 0, 4, "32", 12) self.batch_size = field(self.batch_tab, "包大小 bytes", 0, 5, "32", 12)
self.workers = field(self.batch_tab, "并发数", 0, 5, "64", 10) self.workers = field(self.batch_tab, "并发数", 1, 0, "64", 10)
self.batch_ttl = field(self.batch_tab, "TTL0=默认)", 1, 0, "0", 12) self.batch_ttl = field(self.batch_tab, "TTL0=默认)", 1, 1, "0", 12)
self.batch_df_var = tk.BooleanVar(value=False) self.batch_df_var = tk.BooleanVar(value=False)
df_frame = ttk.Frame(self.batch_tab, style="Panel.TFrame") df_frame = ttk.Frame(self.batch_tab, style="Panel.TFrame")
df_frame.grid(row=1, column=1, sticky="ew", padx=18, pady=(22, 14)) df_frame.grid(row=1, column=2, sticky="ew", padx=18, pady=(22, 14))
ttk.Checkbutton(df_frame, text="禁止分片", variable=self.batch_df_var).pack(anchor="w") ttk.Checkbutton(df_frame, text="禁止分片", variable=self.batch_df_var).pack(anchor="w")
actions = action_bar(self.batch_tab, 2, 6) actions = action_bar(self.batch_tab, 2, 6)
+29 -22
View File
@@ -33,8 +33,10 @@ class TelnetTab(Page):
item["entry"].configure(state="disabled") item["entry"].configure(state="disabled")
modes = self.section("扫描模式", 1, columns=1) modes = self.section("扫描模式", 1, columns=1)
modes.rowconfigure(1, weight=1)
modes.columnconfigure(0, weight=1)
self.tabs = ttk.Notebook(modes) self.tabs = ttk.Notebook(modes)
self.tabs.grid(row=1, column=0, sticky="ew", padx=18, pady=(0, 18)) self.tabs.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.single_tab = ttk.Frame(self.tabs, style="Panel.TFrame") self.single_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
self.scan_tab = ttk.Frame(self.tabs, style="Panel.TFrame") self.scan_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
self.batch_tab = ttk.Frame(self.tabs, style="Panel.TFrame") self.batch_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
@@ -64,25 +66,30 @@ class TelnetTab(Page):
"banner": "Banner / 错误", "banner": "Banner / 错误",
} }
widths = { widths = {
"host": 150, "host": 185,
"resolved_ip": 130, "resolved_ip": 145,
"port": 70, "port": 70,
"service": 100, "service": 125,
"status": 80, "status": 80,
"latency": 80, "latency": 85,
"banner": 320, "banner": 120,
} }
for column, title in headings.items(): for column, title in headings.items():
self.results_tree.heading(column, text=title) self.results_tree.heading(column, text=title)
self.results_tree.column(column, width=widths[column], anchor="w") self.results_tree.column(column, width=widths[column], minwidth=widths[column], anchor="w", stretch=False)
self.results_tree.tag_configure("open", foreground="#15803d") self.results_tree.tag_configure("open", foreground="#15803d")
self.results_tree.tag_configure("timeout", foreground="#b7791f") self.results_tree.tag_configure("timeout", foreground="#b7791f")
self.results_tree.tag_configure("unreachable", foreground="#b7791f") self.results_tree.tag_configure("unreachable", foreground="#b7791f")
self.results_tree.tag_configure("dns_error", foreground="#dc2626") self.results_tree.tag_configure("dns_error", foreground="#dc2626")
self.results_tree.tag_configure("error", foreground="#dc2626") self.results_tree.tag_configure("error", foreground="#dc2626")
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview) result_scroll = ttk.Scrollbar(
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) results,
orient="vertical",
command=self.results_tree.yview,
style="Modern.Vertical.TScrollbar",
)
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18), padx=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set) self.results_tree.configure(yscrollcommand=result_scroll.set)
self.scanner = PortScanner(self.write, self.on_task_done, self.update_status, self.add_result) self.scanner = PortScanner(self.write, self.on_task_done, self.update_status, self.add_result)
@@ -91,11 +98,11 @@ class TelnetTab(Page):
for col in range(5): for col in range(5):
self.single_tab.columnconfigure(col, weight=1) self.single_tab.columnconfigure(col, weight=1)
self.single_host = field(self.single_tab, "目标 IP / 域名", 0, 0, "127.0.0.1", 26) self.single_host = field(self.single_tab, "目标 IP / 域名", 0, 0, "127.0.0.1", 36, colspan=2)
self.single_port = field(self.single_tab, "目标端口", 0, 1, "443", 12) self.single_port = field(self.single_tab, "目标端口", 0, 2, "443", 12)
self.single_timeout = field(self.single_tab, "超时 ms", 0, 2, "1000", 10) self.single_timeout = field(self.single_tab, "超时 ms", 0, 3, "1000", 10)
self.single_banner_var = tk.BooleanVar(value=False) self.single_banner_var = tk.BooleanVar(value=False)
self._check(self.single_tab, "Banner 探测", self.single_banner_var, 0, 3) self._check(self.single_tab, "Banner 探测", self.single_banner_var, 0, 4)
actions = action_bar(self.single_tab, 1, 5) actions = action_bar(self.single_tab, 1, 5)
self.single_btn = button(actions, "测试连接", self.test_single, "Primary.TButton") self.single_btn = button(actions, "测试连接", self.test_single, "Primary.TButton")
self.single_export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton") self.single_export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
@@ -104,15 +111,15 @@ class TelnetTab(Page):
for col in range(6): for col in range(6):
self.scan_tab.columnconfigure(col, weight=1) self.scan_tab.columnconfigure(col, weight=1)
self.scan_host = field(self.scan_tab, "目标 IP / 域名", 0, 0, "127.0.0.1", 26) self.scan_host = field(self.scan_tab, "目标 IP / 域名", 0, 0, "127.0.0.1", 30)
self.scan_ports = field(self.scan_tab, "端口列表 / 范围", 0, 1, PORT_PRESETS["常用端口"], 44, colspan=2) self.scan_ports = field(self.scan_tab, "端口列表 / 范围", 0, 1, PORT_PRESETS["常用端口"], 58, colspan=3)
self.scan_preset = combo(self.scan_tab, "预设", 0, 3, list(PORT_PRESETS), "常用端口", 16) self.scan_preset = combo(self.scan_tab, "预设", 0, 4, list(PORT_PRESETS), "常用端口", 16)
self.scan_timeout = field(self.scan_tab, "超时 ms", 0, 4, "800", 10) self.scan_timeout = field(self.scan_tab, "超时 ms", 0, 5, "800", 10)
self.scan_workers = field(self.scan_tab, "并发数", 0, 5, "128", 10) self.scan_workers = field(self.scan_tab, "并发数", 1, 0, "128", 10)
self.scan_show_closed_var = tk.BooleanVar(value=False) self.scan_show_closed_var = tk.BooleanVar(value=False)
self.scan_banner_var = tk.BooleanVar(value=False) self.scan_banner_var = tk.BooleanVar(value=False)
self._check(self.scan_tab, "显示关闭端口", self.scan_show_closed_var, 1, 0) self._check(self.scan_tab, "显示关闭端口", self.scan_show_closed_var, 1, 1)
self._check(self.scan_tab, "Banner 探测", self.scan_banner_var, 1, 1) self._check(self.scan_tab, "Banner 探测", self.scan_banner_var, 1, 2)
self.scan_preset["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.apply_preset(self.scan_preset, self.scan_ports)) self.scan_preset["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.apply_preset(self.scan_preset, self.scan_ports))
actions = action_bar(self.scan_tab, 2, 6) actions = action_bar(self.scan_tab, 2, 6)
@@ -125,8 +132,8 @@ class TelnetTab(Page):
for col in range(6): for col in range(6):
self.batch_tab.columnconfigure(col, weight=1) self.batch_tab.columnconfigure(col, weight=1)
self.batch_hosts = field(self.batch_tab, "目标列表 / CIDR / IP 段", 0, 0, "192.168.1.1-254", 38, colspan=2) self.batch_hosts = field(self.batch_tab, "目标列表 / CIDR / IP 段", 0, 0, "192.168.1.1-254", 46, colspan=2)
self.batch_ports = field(self.batch_tab, "端口列表 / 范围", 0, 2, "22,80,443,3389", 32, colspan=2) self.batch_ports = field(self.batch_tab, "端口列表 / 范围", 0, 2, "22,80,443,3389", 42, colspan=2)
self.batch_preset = combo(self.batch_tab, "预设", 0, 4, list(PORT_PRESETS), "远程管理", 16) self.batch_preset = combo(self.batch_tab, "预设", 0, 4, list(PORT_PRESETS), "远程管理", 16)
self.batch_timeout = field(self.batch_tab, "超时 ms", 0, 5, "800", 10) self.batch_timeout = field(self.batch_tab, "超时 ms", 0, 5, "800", 10)
self.batch_workers = field(self.batch_tab, "并发数", 1, 0, "128", 10) self.batch_workers = field(self.batch_tab, "并发数", 1, 0, "128", 10)
+25 -23
View File
@@ -31,16 +31,16 @@ class TracertTab(Page):
item["entry"].configure(state="disabled") item["entry"].configure(state="disabled")
params = self.section("追踪参数", 1, columns=7) params = self.section("追踪参数", 1, columns=7)
self.target = field(params, "目标 IP / 域名", 1, 0, "8.8.8.8", 26) self.target = field(params, "目标 IP / 域名", 1, 0, "8.8.8.8", 36, colspan=2)
self.address_family = combo(params, "地址族", 1, 1, ["自动", "IPv4", "IPv6"], "自动", 10) self.address_family = combo(params, "地址族", 1, 2, ["自动", "IPv4", "IPv6"], "自动", 10)
self.mode = combo(params, "模式", 1, 2, ["单次", "指定次数", "持续"], "单次", 12) self.mode = combo(params, "模式", 1, 3, ["单次", "指定次数", "持续"], "单次", 12)
self.repeat_count = field(params, "次数", 1, 3, "3", 8) self.repeat_count = field(params, "次数", 1, 4, "3", 8)
self.interval_ms = field(params, "间隔 ms", 1, 4, "1000", 10) self.interval_ms = field(params, "间隔 ms", 1, 5, "1000", 10)
self.max_hops = field(params, "最大跳数", 1, 5, "20", 10) self.max_hops = field(params, "最大跳数", 1, 6, "20", 10)
self.timeout_ms = field(params, "单跳超时 ms", 1, 6, "800", 12) self.timeout_ms = field(params, "单跳超时 ms", 2, 0, "800", 12)
self.high_latency_ms = field(params, "高延迟阈值 ms", 2, 0, "100", 12) self.high_latency_ms = field(params, "高延迟阈值 ms", 2, 1, "100", 12)
self.resolve_names_var = tk.BooleanVar(value=False) self.resolve_names_var = tk.BooleanVar(value=False)
self._check(params, "解析主机名", self.resolve_names_var, 2, 1) self._check(params, "解析主机名", self.resolve_names_var, 2, 2)
actions = action_bar(params, 3, 7) actions = action_bar(params, 3, 7)
self.start_btn = button(actions, "开始追踪", self.start_trace, "Primary.TButton") self.start_btn = button(actions, "开始追踪", self.start_trace, "Primary.TButton")
self.stop_btn = button(actions, "停止", self.stop_trace, "Danger.TButton") self.stop_btn = button(actions, "停止", self.stop_trace, "Danger.TButton")
@@ -52,7 +52,7 @@ class TracertTab(Page):
results.columnconfigure(0, weight=1) results.columnconfigure(0, weight=1)
self.results_tree = ttk.Treeview( self.results_tree = ttk.Treeview(
results, results,
columns=("run", "hop", "probe1", "probe2", "probe3", "avg", "jitter", "host", "ip", "status"), columns=("run", "hop", "probe1", "probe2", "probe3", "avg", "jitter", "ip", "status"),
show="headings", show="headings",
height=9, height=9,
) )
@@ -64,32 +64,35 @@ class TracertTab(Page):
"probe3": "RTT 3", "probe3": "RTT 3",
"avg": "平均", "avg": "平均",
"jitter": "波动", "jitter": "波动",
"host": "主机名",
"ip": "IP", "ip": "IP",
"status": "判断", "status": "判断",
} }
widths = { widths = {
"run": 60, "run": 60,
"hop": 60, "hop": 60,
"probe1": 70, "probe1": 78,
"probe2": 70, "probe2": 78,
"probe3": 70, "probe3": 78,
"avg": 80, "avg": 88,
"jitter": 80, "jitter": 88,
"host": 200, "ip": 190,
"ip": 150, "status": 190,
"status": 90,
} }
for column, title in headings.items(): for column, title in headings.items():
self.results_tree.heading(column, text=title) self.results_tree.heading(column, text=title)
self.results_tree.column(column, width=widths[column], anchor="w") self.results_tree.column(column, width=widths[column], minwidth=widths[column], anchor="w", stretch=False)
self.results_tree.tag_configure("ok", foreground="#15803d") self.results_tree.tag_configure("ok", foreground="#15803d")
self.results_tree.tag_configure("timeout", foreground="#b7791f") self.results_tree.tag_configure("timeout", foreground="#b7791f")
self.results_tree.tag_configure("high_latency", foreground="#dc2626") self.results_tree.tag_configure("high_latency", foreground="#dc2626")
self.results_tree.tag_configure("jitter", foreground="#b7791f") self.results_tree.tag_configure("jitter", foreground="#b7791f")
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview) result_scroll = ttk.Scrollbar(
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) results,
orient="vertical",
command=self.results_tree.yview,
style="Modern.Vertical.TScrollbar",
)
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18), padx=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set) self.results_tree.configure(yscrollcommand=result_scroll.set)
self.tracert_fun = TracertFun(self.write, self.on_task_done, self.update_status, self.add_result) self.tracert_fun = TracertFun(self.write, self.on_task_done, self.update_status, self.add_result)
@@ -207,7 +210,6 @@ class TracertTab(Page):
row.get("probe3", ""), row.get("probe3", ""),
avg, avg,
jitter, jitter,
row.get("host", ""),
row.get("ip", ""), row.get("ip", ""),
row.get("status_text", ""), row.get("status_text", ""),
), ),
+33 -43
View File
@@ -7,8 +7,6 @@ from tkinter import ttk
from core.ui.components import Console from core.ui.components import Console
from core.ui.tab_device_discovery import DeviceDiscoveryTab from core.ui.tab_device_discovery import DeviceDiscoveryTab
from core.ui.tab_dns import DnsTab from core.ui.tab_dns import DnsTab
from core.ui.tab_ip_conflict import IpConflictTab
from core.ui.tab_loop import LoopTab
from core.ui.tab_network import NetworkTab from core.ui.tab_network import NetworkTab
from core.ui.tab_ping import PingTab from core.ui.tab_ping import PingTab
from core.ui.tab_telnet import TelnetTab from core.ui.tab_telnet import TelnetTab
@@ -21,29 +19,29 @@ class MainUI:
self.root = root self.root = root
self.base_dir = base_dir self.base_dir = base_dir
self.root.title("NetPilot 网络调试工具") self.root.title("NetPilot 网络调试工具")
self.root.geometry("1440x820") self.root.geometry("1600x820")
self.root.minsize(1280, 760) self.root.minsize(1440, 780)
apply_theme(root) apply_theme(root)
self._apply_windows_titlebar_color()
self.root.after(100, self._apply_windows_titlebar_color)
self.icon_image = build_app_icon() self.icon_image = build_app_icon()
self.root.iconphoto(True, self.icon_image) self.root.iconphoto(True, self.icon_image)
self.root.columnconfigure(1, weight=1) self.root.columnconfigure(1, weight=1)
self.root.rowconfigure(1, weight=1) self.root.rowconfigure(0, weight=1)
self._build_topbar() self.sidebar = tk.Frame(root, width=250, bg=COLORS["sidebar"])
self.sidebar.grid(row=0, column=0, rowspan=2, sticky="ns")
self.sidebar = tk.Frame(root, width=260, bg=COLORS["sidebar"])
self.sidebar.grid(row=1, column=0, rowspan=2, sticky="ns")
self.sidebar.grid_propagate(False) self.sidebar.grid_propagate(False)
self.content = ttk.Frame(root, style="Page.TFrame") self.content = ttk.Frame(root, style="Page.TFrame")
self.content.grid(row=1, column=1, sticky="nsew") self.content.grid(row=0, column=1, sticky="nsew")
self.content.rowconfigure(0, weight=1) self.content.rowconfigure(0, weight=1)
self.content.columnconfigure(0, weight=1) self.content.columnconfigure(0, weight=1)
self.console_shell = tk.Frame(root, width=410, bg=COLORS["bg"]) self.console_shell = tk.Frame(root, width=360, bg=COLORS["bg"])
self.console_shell.grid(row=1, column=2, sticky="nsew", padx=(0, 16), pady=(16, 0)) self.console_shell.grid(row=0, column=2, sticky="nsew", padx=(0, 16), pady=(16, 0))
self.console_shell.grid_propagate(False) self.console_shell.grid_propagate(False)
self.console_shell.rowconfigure(0, weight=1) self.console_shell.rowconfigure(0, weight=1)
self.console_shell.columnconfigure(0, weight=1) self.console_shell.columnconfigure(0, weight=1)
@@ -69,37 +67,14 @@ class MainUI:
self._build_pages() self._build_pages()
self.show_page("network") self.show_page("network")
def _build_topbar(self) -> None:
bar = tk.Frame(self.root, height=44, bg=COLORS["topbar"])
bar.grid(row=0, column=0, columnspan=3, sticky="ew")
bar.grid_propagate(False)
icon = tk.Label(
bar,
text="NP",
bg=COLORS["primary"],
fg="#ffffff",
font=("Microsoft YaHei UI", 9, "bold"),
width=3,
height=1,
)
icon.pack(side="left", padx=(24, 12), pady=8)
tk.Label(
bar,
text="NetPilot 网络调试工具",
bg=COLORS["topbar"],
fg="#ffffff",
font=("Microsoft YaHei UI", 11, "bold"),
).pack(side="left")
def _build_sidebar(self) -> None: def _build_sidebar(self) -> None:
brand = tk.Frame(self.sidebar, bg=COLORS["sidebar"]) brand = tk.Frame(self.sidebar, bg=COLORS["sidebar"])
brand.pack(fill="x", padx=28, pady=(28, 28)) brand.pack(fill="x", padx=24, pady=(28, 28))
logo = tk.Label( logo = tk.Label(
brand, brand,
text="NP", text="NP",
width=4, width=3,
height=2, height=2,
bg=COLORS["primary"], bg=COLORS["primary"],
fg="#ffffff", fg="#ffffff",
@@ -119,8 +94,6 @@ class MainUI:
("ping", "", "Ping 探测", "单点与批量探活"), ("ping", "", "Ping 探测", "单点与批量探活"),
("ports", "", "端口扫描", "TCP 连通性检测"), ("ports", "", "端口扫描", "TCP 连通性检测"),
("trace", "", "路由追踪", "跳点路径分析"), ("trace", "", "路由追踪", "跳点路径分析"),
("loop", "", "环网检测", "二层环路风险"),
("ip_conflict", "", "IP 冲突", "地址占用排查"),
] ]
for key, icon, title, subtitle in items: for key, icon, title, subtitle in items:
self.nav_buttons[key] = self._nav_button(key, icon, title, subtitle) self.nav_buttons[key] = self._nav_button(key, icon, title, subtitle)
@@ -136,7 +109,7 @@ class MainUI:
def _nav_button(self, key: str, icon: str, title: str, subtitle: str) -> tk.Frame: def _nav_button(self, key: str, icon: str, title: str, subtitle: str) -> tk.Frame:
frame = tk.Frame(self.sidebar, bg=COLORS["sidebar"], cursor="hand2") frame = tk.Frame(self.sidebar, bg=COLORS["sidebar"], cursor="hand2")
frame.pack(fill="x", padx=16, pady=5) frame.pack(fill="x", padx=14, pady=5)
icon_label = tk.Label(frame, text=icon, width=3, bg=COLORS["sidebar"], fg="#d9e6f7", font=("Microsoft YaHei UI", 18)) icon_label = tk.Label(frame, text=icon, width=3, bg=COLORS["sidebar"], fg="#d9e6f7", font=("Microsoft YaHei UI", 18))
icon_label.pack(side="left", padx=(12, 8), pady=12) icon_label.pack(side="left", padx=(12, 8), pady=12)
@@ -163,8 +136,6 @@ class MainUI:
"ping": PingTab(self.content, self.console), "ping": PingTab(self.content, self.console),
"ports": TelnetTab(self.content, self.console), "ports": TelnetTab(self.content, self.console),
"trace": TracertTab(self.content, self.console), "trace": TracertTab(self.content, self.console),
"loop": LoopTab(self.content, self.console),
"ip_conflict": IpConflictTab(self.content, self.console),
"devices": DeviceDiscoveryTab(self.content, self.console), "devices": DeviceDiscoveryTab(self.content, self.console),
} }
for page in self.pages.values(): for page in self.pages.values():
@@ -213,7 +184,7 @@ class MainUI:
highlightthickness=1, highlightthickness=1,
bd=0, bd=0,
) )
status.grid(row=2, column=1, columnspan=2, sticky="ew") status.grid(row=1, column=1, columnspan=2, sticky="ew")
status.grid_propagate(False) status.grid_propagate(False)
status.columnconfigure(1, weight=1) status.columnconfigure(1, weight=1)
@@ -246,3 +217,22 @@ class MainUI:
return bool(ctypes.windll.shell32.IsUserAnAdmin()) return bool(ctypes.windll.shell32.IsUserAnAdmin())
except Exception: except Exception:
return False return False
def _apply_windows_titlebar_color(self) -> None:
try:
hwnd = self.root.winfo_id()
caption_color = ctypes.c_int(self._colorref(COLORS["topbar"]))
text_color = ctypes.c_int(self._colorref("#ffffff"))
border_color = ctypes.c_int(self._colorref(COLORS["topbar"]))
ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, 35, ctypes.byref(caption_color), ctypes.sizeof(caption_color))
ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, 36, ctypes.byref(text_color), ctypes.sizeof(text_color))
ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, 34, ctypes.byref(border_color), ctypes.sizeof(border_color))
except Exception:
pass
def _colorref(self, color: str) -> int:
color = color.lstrip("#")
red = int(color[0:2], 16)
green = int(color[2:4], 16)
blue = int(color[4:6], 16)
return red | (green << 8) | (blue << 16)
+2 -2
View File
@@ -11,9 +11,9 @@ if exist "%PYTHON_EXE%" (
if errorlevel 1 exit /b 1 if errorlevel 1 exit /b 1
) )
set "PYTHONPATH=%PYINSTALLER_PATH%" set "PYTHONPATH=%PYINSTALLER_PATH%"
"%PYTHON_EXE%" -m PyInstaller -F -w main.py -n NetworkTool --icon "%APP_ICON%" "%PYTHON_EXE%" -m PyInstaller -F -w main.py -n NetworkTool --icon "%APP_ICON%" --add-data "%CD%\assets\oui_vendors_seed.json;assets"
) else ( ) else (
pyinstaller -F -w main.py -n NetworkTool --icon "%APP_ICON%" pyinstaller -F -w main.py -n NetworkTool --icon "%APP_ICON%" --add-data "%CD%\assets\oui_vendors_seed.json;assets"
) )
endlocal endlocal