OK
This commit is contained in:
@@ -24,6 +24,13 @@ dist/
|
|||||||
*.spec
|
*.spec
|
||||||
*.manifest
|
*.manifest
|
||||||
|
|
||||||
|
# Packaged binaries and release archives
|
||||||
|
*.exe
|
||||||
|
*.msi
|
||||||
|
*.7z
|
||||||
|
*.rar
|
||||||
|
*.zip
|
||||||
|
|
||||||
# Local runtime data
|
# Local runtime data
|
||||||
network_profiles.json
|
network_profiles.json
|
||||||
*.db
|
*.db
|
||||||
|
|||||||
@@ -6,11 +6,13 @@
|
|||||||
|
|
||||||
- 网卡配置:读取网卡、IPv4/IPv6、网关、DNS、DHCP、链路速率、接口索引、DHCP 服务器与租约时间。
|
- 网卡配置:读取网卡、IPv4/IPv6、网关、DNS、DHCP、链路速率、接口索引、DHCP 服务器与租约时间。
|
||||||
- 配置模板:保存常用网卡配置、套用模板到表单、删除模板。
|
- 配置模板:保存常用网卡配置、套用模板到表单、删除模板。
|
||||||
|
- DNS 诊断:读取本机 DNS 并补充常用 DNS 服务器做对比,支持多域名、多记录类型解析、耗时/失败原因统计、诊断摘要复制与 CSV 导出。
|
||||||
- Ping 探测:支持持续/指定次数、间隔、超时、包大小、TTL、禁止分片、本地源 IP 下拉、CIDR/范围/列表批量探活、目标导入、CSV 导出、实时丢包率/平均延迟/抖动/质量统计。
|
- Ping 探测:支持持续/指定次数、间隔、超时、包大小、TTL、禁止分片、本地源 IP 下拉、CIDR/范围/列表批量探活、目标导入、CSV 导出、实时丢包率/平均延迟/抖动/质量统计。
|
||||||
- 端口扫描:支持单端口测试、端口列表/范围、常用端口预设、批量主机/CIDR/IP 段扫描、并发/超时控制、只显示开放端口、服务名识别、可选 Banner 探测、开放端口复制与 CSV 导出。
|
- 端口扫描:支持单端口测试、端口列表/范围、常用端口预设、批量主机/CIDR/IP 段扫描、并发/超时控制、只显示开放端口、服务名识别、可选 Banner 探测、开放端口复制与 CSV 导出。
|
||||||
- 路由追踪:支持 IPv4/IPv6、主机名解析开关、单次/指定次数/持续追踪、最大跳数、单跳超时、高延迟阈值、逐跳 RTT 表格、超时/高延迟/波动诊断、原始输出保留、诊断摘要复制与 CSV 导出。
|
- 路由追踪:支持 IPv4/IPv6、主机名解析开关、单次/指定次数/持续追踪、最大跳数、单跳超时、高延迟阈值、逐跳 RTT 表格、超时/高延迟/波动诊断、原始输出保留、诊断摘要复制与 CSV 导出。
|
||||||
- 环网检测:基于本机网卡统计、广播/组播流量、ARP/邻居表变化和网关 Ping 稳定性,判断疑似二层环路/广播风暴风险,支持单网卡/全部活动网卡、诊断摘要复制与 CSV 导出。
|
- 环网检测:基于本机网卡统计、广播/组播流量、ARP/邻居表变化和网关 Ping 稳定性,判断疑似二层环路/广播风暴风险,支持单网卡/全部活动网卡、诊断摘要复制与 CSV 导出。
|
||||||
- IP 冲突:支持本机 IP 冲突检测与安全限速网段扫描,结合地址状态、Tcpip 系统事件、ARP/邻居表和 IP/MAC 异常判断冲突风险,支持摘要复制与 CSV 导出。
|
- IP 冲突:支持本机 IP 冲突检测与安全限速网段扫描,结合地址状态、Tcpip 系统事件、ARP/邻居表和 IP/MAC 异常判断冲突风险,支持摘要复制与 CSV 导出。
|
||||||
|
- 设备发现:支持当前网卡安全限速局域网扫描,发现在线/ARP 可见设备,展示 IP、MAC、主机名、厂商、来源网卡、延迟与发现方式,支持设备清单复制与 CSV 导出。
|
||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,618 @@
|
|||||||
|
import concurrent.futures
|
||||||
|
import csv
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from core.Function.common import run_hidden
|
||||||
|
from core.Function.loop_fun import normalize_mac
|
||||||
|
from core.Function.network_fun import NetworkManager
|
||||||
|
|
||||||
|
|
||||||
|
OutputCallback = Callable[[str, Optional[str]], None]
|
||||||
|
DoneCallback = Callable[[], None]
|
||||||
|
StatusCallback = Callable[[dict], None]
|
||||||
|
ResultCallback = Callable[[dict], None]
|
||||||
|
|
||||||
|
|
||||||
|
ALL_ADAPTERS = "全部活动网卡"
|
||||||
|
|
||||||
|
|
||||||
|
OUI_VENDOR_MAP = {
|
||||||
|
"00-05-69": "VMware",
|
||||||
|
"00-0C-29": "VMware",
|
||||||
|
"00-1C-14": "VMware",
|
||||||
|
"00-50-56": "VMware",
|
||||||
|
"00-15-5D": "Microsoft Hyper-V",
|
||||||
|
"00-03-FF": "Microsoft",
|
||||||
|
"00-1A-A0": "Dell",
|
||||||
|
"00-1B-21": "Intel",
|
||||||
|
"00-1E-67": "Intel",
|
||||||
|
"00-21-5C": "Intel",
|
||||||
|
"00-24-D7": "Intel",
|
||||||
|
"3C-A8-2A": "Intel",
|
||||||
|
"48-7D-2E": "TP-Link",
|
||||||
|
"50-C7-BF": "TP-Link",
|
||||||
|
"60-E3-27": "TP-Link",
|
||||||
|
"A0-F3-C1": "TP-Link",
|
||||||
|
"B0-A7-B9": "TP-Link",
|
||||||
|
"D8-07-B6": "TP-Link",
|
||||||
|
"00-1D-0F": "Cisco",
|
||||||
|
"00-22-BD": "Cisco",
|
||||||
|
"00-25-9C": "Cisco",
|
||||||
|
"A4-18-75": "Cisco",
|
||||||
|
"F4-4E-05": "Cisco",
|
||||||
|
"00-16-EA": "HPE",
|
||||||
|
"00-1F-29": "HPE",
|
||||||
|
"3C-A8-2A": "HPE/Intel",
|
||||||
|
"00-1E-C2": "Apple",
|
||||||
|
"00-25-00": "Apple",
|
||||||
|
"3C-15-C2": "Apple",
|
||||||
|
"7C-D1-C3": "Apple",
|
||||||
|
"A4-C3-F0": "Apple",
|
||||||
|
"F0-18-98": "Apple",
|
||||||
|
"18-C0-4D": "Realtek",
|
||||||
|
"52-54-00": "QEMU/KVM",
|
||||||
|
"08-00-27": "VirtualBox",
|
||||||
|
"BC-24-11": "Proxmox/QEMU",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceDiscoveryOptions:
|
||||||
|
scan_range: str = ""
|
||||||
|
workers: int = 64
|
||||||
|
timeout_ms: int = 500
|
||||||
|
max_hosts: int = 254
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceInfo:
|
||||||
|
ip: str
|
||||||
|
mac: str
|
||||||
|
hostname: str
|
||||||
|
vendor: str
|
||||||
|
adapter: str
|
||||||
|
interface_index: str
|
||||||
|
latency_ms: float = 0.0
|
||||||
|
method: str = "ARP 可见"
|
||||||
|
note: str = ""
|
||||||
|
checked_at: str = field(default_factory=lambda: time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
data = asdict(self)
|
||||||
|
data["latency_ms"] = round(self.latency_ms, 1)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceDiscovery:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
output: OutputCallback,
|
||||||
|
done: Optional[DoneCallback] = None,
|
||||||
|
status: Optional[StatusCallback] = None,
|
||||||
|
result: Optional[ResultCallback] = None,
|
||||||
|
):
|
||||||
|
self.output = output
|
||||||
|
self.done = done or (lambda: None)
|
||||||
|
self.status = status or (lambda _stats: None)
|
||||||
|
self.result = result or (lambda _row: None)
|
||||||
|
self.network = NetworkManager(lambda _text, _tag=None: None)
|
||||||
|
self.stop_event = threading.Event()
|
||||||
|
self.worker = None
|
||||||
|
self.last_results: list[dict] = []
|
||||||
|
self.last_summary = ""
|
||||||
|
|
||||||
|
def get_adapter_choices(self) -> list[str]:
|
||||||
|
adapters = self._active_adapters(self.network.get_network_info())
|
||||||
|
return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
|
||||||
|
|
||||||
|
def default_scan_range(self, adapter_name: str = ALL_ADAPTERS) -> str:
|
||||||
|
adapters = self._select_adapters(adapter_name)
|
||||||
|
if not adapters:
|
||||||
|
return ""
|
||||||
|
return adapter_to_safe_range(adapters[0])
|
||||||
|
|
||||||
|
def start_discovery(self, adapter_name: str = ALL_ADAPTERS, options: Optional[dict] = None) -> None:
|
||||||
|
if self.is_running():
|
||||||
|
raise RuntimeError("设备发现正在运行,请先停止当前任务")
|
||||||
|
|
||||||
|
discovery_options = self.normalize_options(options)
|
||||||
|
adapters = self._select_adapters(adapter_name)
|
||||||
|
if not adapters:
|
||||||
|
raise ValueError("没有找到可扫描的活动网卡")
|
||||||
|
|
||||||
|
targets = self.build_scan_targets(adapters, discovery_options)
|
||||||
|
if not targets:
|
||||||
|
raise ValueError("没有可扫描的目标地址")
|
||||||
|
|
||||||
|
self.stop_event.clear()
|
||||||
|
self.last_results = []
|
||||||
|
self.last_summary = ""
|
||||||
|
self.output(
|
||||||
|
f"开始局域网设备发现: {adapter_name or ALL_ADAPTERS},"
|
||||||
|
f"目标 {len(targets)} 个,并发 {discovery_options.workers},超时 {discovery_options.timeout_ms}ms\n",
|
||||||
|
"muted",
|
||||||
|
)
|
||||||
|
self.status(self._status("扫描中", adapter_name or ALL_ADAPTERS, describe_targets(targets), len(targets), 0, 0, 0))
|
||||||
|
self.worker = threading.Thread(target=self._run_discovery, args=(adapters, targets, discovery_options), daemon=True)
|
||||||
|
self.worker.start()
|
||||||
|
|
||||||
|
def _run_discovery(self, adapters: list[dict], targets: list[str], options: DeviceDiscoveryOptions) -> None:
|
||||||
|
started = time.perf_counter()
|
||||||
|
ping_results: dict[str, dict] = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
ping_results = self.scan_targets(targets, options, started)
|
||||||
|
neighbors = self.get_neighbors()
|
||||||
|
devices = self.build_devices(adapters, targets, ping_results, neighbors)
|
||||||
|
|
||||||
|
for index, device in enumerate(sorted(devices, key=lambda item: ip_sort_key(item.ip)), start=1):
|
||||||
|
if self.stop_event.is_set():
|
||||||
|
break
|
||||||
|
row = device.to_dict()
|
||||||
|
self.last_results.append(row)
|
||||||
|
self.result(row)
|
||||||
|
self.status(
|
||||||
|
self._status(
|
||||||
|
"汇总中",
|
||||||
|
device.adapter,
|
||||||
|
describe_targets(targets),
|
||||||
|
len(targets),
|
||||||
|
len(targets),
|
||||||
|
index,
|
||||||
|
time.perf_counter() - started,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.last_summary = build_summary(self.last_results, stopped=self.stop_event.is_set())
|
||||||
|
if self.stop_event.is_set():
|
||||||
|
self.output("\n设备发现已停止\n", "warning")
|
||||||
|
else:
|
||||||
|
self.output("\n设备发现完成\n", "success")
|
||||||
|
self.output(self.last_summary + "\n", "success")
|
||||||
|
self.status(
|
||||||
|
self._status(
|
||||||
|
"已停止" if self.stop_event.is_set() else "已完成",
|
||||||
|
"",
|
||||||
|
describe_targets(targets),
|
||||||
|
len(targets),
|
||||||
|
len(targets),
|
||||||
|
len(self.last_results),
|
||||||
|
time.perf_counter() - started,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self.output(f"\n设备发现失败: {exc}\n", "error")
|
||||||
|
self.status(self._status("失败", "", describe_targets(targets), len(targets), len(ping_results), 0, time.perf_counter() - started))
|
||||||
|
finally:
|
||||||
|
self.done()
|
||||||
|
|
||||||
|
def scan_targets(self, targets: list[str], options: DeviceDiscoveryOptions, started: float) -> dict[str, dict]:
|
||||||
|
results = {}
|
||||||
|
completed = 0
|
||||||
|
|
||||||
|
def task(ip: str) -> dict:
|
||||||
|
if self.stop_event.is_set():
|
||||||
|
return {"ip": ip, "ok": False, "rtt": 0.0}
|
||||||
|
return ping_once(ip, options.timeout_ms)
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=options.workers) as executor:
|
||||||
|
futures = {executor.submit(task, ip): ip for ip in targets}
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
completed += 1
|
||||||
|
ip = futures[future]
|
||||||
|
if self.stop_event.is_set():
|
||||||
|
for item in futures:
|
||||||
|
item.cancel()
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
except Exception:
|
||||||
|
result = {"ip": ip, "ok": False, "rtt": 0.0}
|
||||||
|
results[ip] = result
|
||||||
|
if completed % 20 == 0 or completed == len(targets):
|
||||||
|
self.status(self._status("扫描中", ip, describe_targets(targets), len(targets), completed, 0, time.perf_counter() - started))
|
||||||
|
return results
|
||||||
|
|
||||||
|
def build_devices(
|
||||||
|
self,
|
||||||
|
adapters: list[dict],
|
||||||
|
targets: list[str],
|
||||||
|
ping_results: dict[str, dict],
|
||||||
|
neighbors: list[dict],
|
||||||
|
) -> list[DeviceInfo]:
|
||||||
|
adapter_by_index = {str(adapter.get("interface_index", "")): adapter for adapter in adapters}
|
||||||
|
adapter_by_ip = {adapter.get("ipv4", ""): adapter for adapter in adapters}
|
||||||
|
target_set = set(targets)
|
||||||
|
local_ips = {adapter.get("ipv4", "") for adapter in adapters}
|
||||||
|
gateways = {adapter.get("gateway", "") for adapter in adapters if adapter.get("gateway")}
|
||||||
|
devices: dict[str, DeviceInfo] = {}
|
||||||
|
|
||||||
|
for adapter in adapters:
|
||||||
|
local_ip = adapter.get("ipv4", "")
|
||||||
|
if not local_ip:
|
||||||
|
continue
|
||||||
|
devices[local_ip] = DeviceInfo(
|
||||||
|
ip=local_ip,
|
||||||
|
mac=normalize_mac(adapter.get("mac", "")),
|
||||||
|
hostname=resolve_hostname(local_ip),
|
||||||
|
vendor=vendor_name(adapter.get("mac", "")),
|
||||||
|
adapter=adapter.get("name", ""),
|
||||||
|
interface_index=str(adapter.get("interface_index", "")),
|
||||||
|
latency_ms=ping_results.get(local_ip, {}).get("rtt", 0.0),
|
||||||
|
method="在线" if ping_results.get(local_ip, {}).get("ok") else "本机",
|
||||||
|
note="本机",
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in neighbors:
|
||||||
|
ip = item.get("ip", "")
|
||||||
|
mac = normalize_mac(item.get("mac", ""))
|
||||||
|
if ip not in target_set and ip not in gateways and ip not in local_ips:
|
||||||
|
continue
|
||||||
|
if not is_valid_device_mac(mac) or is_multicast_or_broadcast_ip(ip):
|
||||||
|
continue
|
||||||
|
adapter = adapter_by_index.get(str(item.get("ifIndex", ""))) or adapter_by_ip.get(item.get("interface_ip", "")) or adapter_for_ip(ip, adapters)
|
||||||
|
if not adapter:
|
||||||
|
continue
|
||||||
|
ping = ping_results.get(ip, {})
|
||||||
|
method = "在线" if ping.get("ok") else "ARP 可见"
|
||||||
|
note = []
|
||||||
|
if ip in local_ips:
|
||||||
|
note.append("本机")
|
||||||
|
if ip in gateways:
|
||||||
|
note.append("网关")
|
||||||
|
devices[ip] = DeviceInfo(
|
||||||
|
ip=ip,
|
||||||
|
mac=mac,
|
||||||
|
hostname=resolve_hostname(ip),
|
||||||
|
vendor=vendor_name(mac),
|
||||||
|
adapter=adapter.get("name", ""),
|
||||||
|
interface_index=str(adapter.get("interface_index", "")),
|
||||||
|
latency_ms=ping.get("rtt", 0.0),
|
||||||
|
method=method,
|
||||||
|
note="; ".join(note),
|
||||||
|
)
|
||||||
|
|
||||||
|
for ip, ping in ping_results.items():
|
||||||
|
if not ping.get("ok") or ip in devices:
|
||||||
|
continue
|
||||||
|
adapter = adapter_for_ip(ip, adapters)
|
||||||
|
if not adapter:
|
||||||
|
continue
|
||||||
|
# Ping succeeded but no valid ARP MAC was visible; keep it out of
|
||||||
|
# the asset list unless it is a known local/gateway address.
|
||||||
|
if ip not in gateways and ip not in local_ips:
|
||||||
|
continue
|
||||||
|
devices[ip] = DeviceInfo(
|
||||||
|
ip=ip,
|
||||||
|
mac="",
|
||||||
|
hostname=resolve_hostname(ip),
|
||||||
|
vendor="未知",
|
||||||
|
adapter=adapter.get("name", ""),
|
||||||
|
interface_index=str(adapter.get("interface_index", "")),
|
||||||
|
latency_ms=ping.get("rtt", 0.0),
|
||||||
|
method="在线",
|
||||||
|
note="网关" if ip in gateways else "本机",
|
||||||
|
)
|
||||||
|
|
||||||
|
return list(devices.values())
|
||||||
|
|
||||||
|
def get_neighbors(self) -> list[dict]:
|
||||||
|
script = r"""
|
||||||
|
$ErrorActionPreference = "SilentlyContinue"
|
||||||
|
Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerAddress,State | ConvertTo-Json -Depth 4 -Compress
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = run_hidden(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], timeout=10)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(result.stdout.strip())
|
||||||
|
data = extract_json(result.stdout)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = [data]
|
||||||
|
return [normalize_neighbor(item) for item in data]
|
||||||
|
except Exception as exc:
|
||||||
|
self.output(f"PowerShell 邻居表读取失败,尝试 arp -a: {exc}\n", "warning")
|
||||||
|
return self.get_neighbors_from_arp()
|
||||||
|
|
||||||
|
def get_neighbors_from_arp(self) -> list[dict]:
|
||||||
|
result = run_hidden(["arp", "-a"], timeout=10)
|
||||||
|
if result.returncode != 0:
|
||||||
|
return []
|
||||||
|
neighbors = []
|
||||||
|
current_interface = ""
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
header = re.search(r"Interface:\s+([^\s]+)", line, re.IGNORECASE)
|
||||||
|
if header:
|
||||||
|
current_interface = header.group(1)
|
||||||
|
continue
|
||||||
|
match = re.match(r"\s*(\d+\.\d+\.\d+\.\d+)\s+([0-9a-fA-F-]+)\s+(\w+)", line)
|
||||||
|
if match:
|
||||||
|
neighbors.append(
|
||||||
|
{
|
||||||
|
"ifIndex": "",
|
||||||
|
"interface_ip": current_interface,
|
||||||
|
"ip": match.group(1),
|
||||||
|
"mac": normalize_mac(match.group(2)),
|
||||||
|
"state": match.group(3),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
def build_scan_targets(self, adapters: list[dict], options: DeviceDiscoveryOptions) -> list[str]:
|
||||||
|
if options.scan_range:
|
||||||
|
return parse_target_range(options.scan_range, options.max_hosts)
|
||||||
|
targets = []
|
||||||
|
for adapter in adapters:
|
||||||
|
targets.extend(parse_target_range(adapter_to_safe_range(adapter), options.max_hosts))
|
||||||
|
return list(dict.fromkeys(targets))[: options.max_hosts]
|
||||||
|
|
||||||
|
def stop_discovery(self) -> None:
|
||||||
|
if not self.is_running():
|
||||||
|
raise RuntimeError("当前没有正在运行的设备发现")
|
||||||
|
self.stop_event.set()
|
||||||
|
self.output("\n正在停止设备发现...\n", "warning")
|
||||||
|
|
||||||
|
def export_results(self, path: str) -> None:
|
||||||
|
if not self.last_results:
|
||||||
|
raise RuntimeError("还没有可导出的设备发现结果")
|
||||||
|
fields = ["ip", "mac", "hostname", "vendor", "adapter", "interface_index", "latency_ms", "method", "note", "checked_at"]
|
||||||
|
with open(path, "w", newline="", encoding="utf-8-sig") as file:
|
||||||
|
writer = csv.DictWriter(file, fieldnames=fields)
|
||||||
|
writer.writeheader()
|
||||||
|
for row in self.last_results:
|
||||||
|
writer.writerow({field_name: row.get(field_name, "") for field_name in fields})
|
||||||
|
|
||||||
|
def copy_summary(self) -> str:
|
||||||
|
if not self.last_results:
|
||||||
|
return self.last_summary
|
||||||
|
return "\n".join(
|
||||||
|
f"{row.get('ip', '')}\t{row.get('mac', '')}\t{row.get('hostname', '')}\t{row.get('vendor', '')}\t{row.get('method', '')}"
|
||||||
|
for row in sorted(self.last_results, key=lambda item: ip_sort_key(item.get("ip", "")))
|
||||||
|
)
|
||||||
|
|
||||||
|
def normalize_options(self, options: Optional[dict]) -> DeviceDiscoveryOptions:
|
||||||
|
options = options or {}
|
||||||
|
return DeviceDiscoveryOptions(
|
||||||
|
scan_range=str(options.get("scan_range", "")).strip(),
|
||||||
|
workers=clamp_int(options.get("workers", 64), 1, 256, "并发数"),
|
||||||
|
timeout_ms=clamp_int(options.get("timeout_ms", 500), 100, 10000, "超时"),
|
||||||
|
max_hosts=clamp_int(options.get("max_hosts", 254), 1, 254, "最大扫描地址数"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _select_adapters(self, adapter_name: str) -> list[dict]:
|
||||||
|
adapters = self._active_adapters(self.network.get_network_info())
|
||||||
|
if not adapter_name or adapter_name == ALL_ADAPTERS:
|
||||||
|
return adapters
|
||||||
|
return [adapter for adapter in adapters if adapter.get("name") == adapter_name]
|
||||||
|
|
||||||
|
def _active_adapters(self, adapters: list[dict]) -> list[dict]:
|
||||||
|
active = []
|
||||||
|
for adapter in adapters:
|
||||||
|
if not adapter.get("ipv4"):
|
||||||
|
continue
|
||||||
|
status = str(adapter.get("status", "")).lower()
|
||||||
|
if "disconnect" in status or "断开" in status:
|
||||||
|
continue
|
||||||
|
active.append(adapter)
|
||||||
|
return active
|
||||||
|
|
||||||
|
def _status(self, state: str, current: str, scan_range: str, total: int, scanned: int, found: int, elapsed: float) -> dict:
|
||||||
|
return {
|
||||||
|
"state": state,
|
||||||
|
"current": current,
|
||||||
|
"scan_range": scan_range,
|
||||||
|
"total": total,
|
||||||
|
"scanned": scanned,
|
||||||
|
"found": found,
|
||||||
|
"elapsed": elapsed,
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
return bool(self.worker and self.worker.is_alive())
|
||||||
|
|
||||||
|
|
||||||
|
def ping_once(ip: str, timeout_ms: int) -> dict:
|
||||||
|
try:
|
||||||
|
result = run_hidden(["ping", ip, "-n", "1", "-w", str(timeout_ms)], timeout=max(2, timeout_ms / 1000 + 2))
|
||||||
|
output = result.stdout
|
||||||
|
if re.search(r"\bTTL=", output, re.IGNORECASE):
|
||||||
|
match = re.search(r"(?:time|时间)[=<]?\s*(\d+(?:\.\d+)?)\s*(?:ms|毫秒)", output, re.IGNORECASE)
|
||||||
|
return {"ip": ip, "ok": True, "rtt": float(match.group(1)) if match else 0.0}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"ip": ip, "ok": False, "rtt": 0.0}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_target_range(text: str, max_hosts: int = 254) -> list[str]:
|
||||||
|
raw = text.strip()
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
targets = []
|
||||||
|
for part in re.split(r"[,,;\s]+", raw):
|
||||||
|
item = part.strip()
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
if "/" in item:
|
||||||
|
network = ipaddress.ip_network(item, strict=False)
|
||||||
|
targets.extend(str(ip) for ip in network.hosts())
|
||||||
|
elif re.match(r"^\d{1,3}(?:\.\d{1,3}){3}-\d{1,3}$", item):
|
||||||
|
prefix, tail = item.rsplit(".", 1)
|
||||||
|
start_text, end_text = tail.split("-", 1)
|
||||||
|
start = int(start_text)
|
||||||
|
end = int(end_text)
|
||||||
|
if start > end:
|
||||||
|
raise ValueError("IP 范围起始值不能大于结束值")
|
||||||
|
targets.extend(str(ipaddress.ip_address(f"{prefix}.{value}")) for value in range(start, end + 1))
|
||||||
|
elif re.match(r"^\d{1,3}(?:\.\d{1,3}){3}-\d{1,3}(?:\.\d{1,3}){3}$", item):
|
||||||
|
start_text, end_text = item.split("-", 1)
|
||||||
|
start_ip = ipaddress.ip_address(start_text)
|
||||||
|
end_ip = ipaddress.ip_address(end_text)
|
||||||
|
if start_ip.version != end_ip.version:
|
||||||
|
raise ValueError("IP 范围两端必须是同一 IP 版本")
|
||||||
|
if int(start_ip) > int(end_ip):
|
||||||
|
raise ValueError("IP 范围起始值不能大于结束值")
|
||||||
|
targets.extend(str(ipaddress.ip_address(value)) for value in range(int(start_ip), int(end_ip) + 1))
|
||||||
|
else:
|
||||||
|
targets.append(str(ipaddress.ip_address(item)))
|
||||||
|
return list(dict.fromkeys(targets))[:max_hosts]
|
||||||
|
|
||||||
|
|
||||||
|
def adapter_to_safe_range(adapter: dict) -> str:
|
||||||
|
ip = adapter.get("ipv4", "")
|
||||||
|
if not ip:
|
||||||
|
return ""
|
||||||
|
prefix = adapter.get("prefix_length")
|
||||||
|
if prefix in ("", None):
|
||||||
|
prefix = netmask_to_prefix(adapter.get("netmask", "")) or 24
|
||||||
|
try:
|
||||||
|
prefix = int(prefix)
|
||||||
|
if prefix < 24:
|
||||||
|
prefix = 24
|
||||||
|
return str(ipaddress.ip_network(f"{ip}/{prefix}", strict=False))
|
||||||
|
except Exception:
|
||||||
|
parts = ip.split(".")
|
||||||
|
return ".".join(parts[:3]) + ".0/24" if len(parts) == 4 else ""
|
||||||
|
|
||||||
|
|
||||||
|
def netmask_to_prefix(netmask: str) -> Optional[int]:
|
||||||
|
if not netmask:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return ipaddress.IPv4Network(f"0.0.0.0/{netmask}").prefixlen
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_neighbor(item: dict) -> dict:
|
||||||
|
return {
|
||||||
|
"ifIndex": str(item.get("ifIndex", "")),
|
||||||
|
"interface_ip": "",
|
||||||
|
"ip": str(item.get("IPAddress", "")),
|
||||||
|
"mac": normalize_mac(item.get("LinkLayerAddress", "")),
|
||||||
|
"state": str(item.get("State", "")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_hostname(ip: str) -> str:
|
||||||
|
try:
|
||||||
|
old_timeout = socket.getdefaulttimeout()
|
||||||
|
socket.setdefaulttimeout(0.8)
|
||||||
|
try:
|
||||||
|
return socket.gethostbyaddr(ip)[0]
|
||||||
|
finally:
|
||||||
|
socket.setdefaulttimeout(old_timeout)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
result = run_hidden(["nbtstat", "-A", ip], timeout=2)
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
match = re.match(r"\s*([^\s<]+)\s+<00>\s+UNIQUE", line, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def vendor_name(mac: str) -> str:
|
||||||
|
normalized = normalize_mac(mac)
|
||||||
|
if len(normalized) < 8:
|
||||||
|
return "未知"
|
||||||
|
return OUI_VENDOR_MAP.get(normalized[:8], "未知")
|
||||||
|
|
||||||
|
|
||||||
|
def build_summary(rows: list[dict], stopped: bool = False) -> str:
|
||||||
|
if stopped:
|
||||||
|
return "设备发现已停止,当前结果仅代表已完成扫描。"
|
||||||
|
if not rows:
|
||||||
|
return "未发现有效局域网设备。"
|
||||||
|
online = len([row for row in rows if row.get("method") == "在线"])
|
||||||
|
arp = len([row for row in rows if row.get("method") == "ARP 可见"])
|
||||||
|
gateway = len([row for row in rows if "网关" in row.get("note", "")])
|
||||||
|
local = len([row for row in rows if "本机" in row.get("note", "")])
|
||||||
|
vendors = sorted({row.get("vendor", "未知") for row in rows if row.get("vendor") and row.get("vendor") != "未知"})
|
||||||
|
lines = [
|
||||||
|
"==== 局域网设备发现摘要 ====",
|
||||||
|
f"发现设备: {len(rows)} 在线: {online} ARP 可见: {arp} 本机: {local} 网关: {gateway}",
|
||||||
|
]
|
||||||
|
if vendors:
|
||||||
|
lines.append("识别厂商: " + ", ".join(vendors[:8]))
|
||||||
|
else:
|
||||||
|
lines.append("未识别到已知厂商,可能需要后续扩展 OUI 数据。")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def describe_targets(targets: list[str]) -> str:
|
||||||
|
if not targets:
|
||||||
|
return ""
|
||||||
|
if len(targets) == 1:
|
||||||
|
return targets[0]
|
||||||
|
return f"{targets[0]} - {targets[-1]}"
|
||||||
|
|
||||||
|
|
||||||
|
def adapter_for_ip(ip: str, adapters: list[dict]) -> Optional[dict]:
|
||||||
|
try:
|
||||||
|
address = ipaddress.ip_address(ip)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
for adapter in adapters:
|
||||||
|
local_ip = adapter.get("ipv4", "")
|
||||||
|
if not local_ip:
|
||||||
|
continue
|
||||||
|
prefix = adapter.get("prefix_length") or netmask_to_prefix(adapter.get("netmask", "")) or 24
|
||||||
|
try:
|
||||||
|
network = ipaddress.ip_network(f"{local_ip}/{prefix}", strict=False)
|
||||||
|
if address in network:
|
||||||
|
return adapter
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return adapters[0] if adapters else None
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_device_mac(mac: str) -> bool:
|
||||||
|
normalized = normalize_mac(mac)
|
||||||
|
if not normalized or normalized == "00-00-00-00-00-00":
|
||||||
|
return False
|
||||||
|
if normalized.startswith("FF-FF-FF") or normalized.startswith("01-00-5E"):
|
||||||
|
return False
|
||||||
|
return bool(re.match(r"^[0-9A-F]{2}(?:-[0-9A-F]{2}){5}$", normalized))
|
||||||
|
|
||||||
|
|
||||||
|
def is_multicast_or_broadcast_ip(ip: str) -> bool:
|
||||||
|
try:
|
||||||
|
parsed = ipaddress.ip_address(ip)
|
||||||
|
return parsed.is_multicast or str(parsed).endswith(".255") or str(parsed) == "255.255.255.255"
|
||||||
|
except ValueError:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ip_sort_key(value: str) -> tuple:
|
||||||
|
try:
|
||||||
|
return (0, int(ipaddress.ip_address(value)))
|
||||||
|
except ValueError:
|
||||||
|
return (1, value)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_json(text: str):
|
||||||
|
output = text.strip()
|
||||||
|
json_start = min([idx for idx in (output.find("["), output.find("{")) if idx >= 0], default=-1)
|
||||||
|
if json_start < 0:
|
||||||
|
raise RuntimeError("未获取到 JSON 输出")
|
||||||
|
data = json.loads(output[json_start:])
|
||||||
|
return [] if data is None else data
|
||||||
|
|
||||||
|
|
||||||
|
def clamp_int(value, min_value: int, max_value: int, label: str) -> int:
|
||||||
|
try:
|
||||||
|
number = int(str(value).strip())
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(f"{label}必须是整数") from exc
|
||||||
|
if number < min_value or number > max_value:
|
||||||
|
raise ValueError(f"{label}必须在 {min_value}-{max_value} 之间")
|
||||||
|
return number
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
import concurrent.futures
|
||||||
|
import csv
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from core.Function.common import run_hidden
|
||||||
|
from core.Function.network_fun import NetworkManager
|
||||||
|
|
||||||
|
|
||||||
|
OutputCallback = Callable[[str, Optional[str]], None]
|
||||||
|
DoneCallback = Callable[[], None]
|
||||||
|
StatusCallback = Callable[[dict], None]
|
||||||
|
ResultCallback = Callable[[dict], None]
|
||||||
|
|
||||||
|
|
||||||
|
ALL_ADAPTERS = "全部活动网卡"
|
||||||
|
DEFAULT_DOMAINS = "www.baidu.com,www.qq.com"
|
||||||
|
DEFAULT_RECORD_TYPES = "A,AAAA,CNAME,MX,TXT"
|
||||||
|
PUBLIC_DNS_SERVERS = ["223.5.5.5", "114.114.114.114", "8.8.8.8"]
|
||||||
|
SUPPORTED_RECORD_TYPES = {"A", "AAAA", "CNAME", "MX", "TXT", "NS"}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DnsDiagOptions:
|
||||||
|
domains: list[str] = field(default_factory=lambda: parse_list(DEFAULT_DOMAINS))
|
||||||
|
record_types: list[str] = field(default_factory=lambda: parse_record_types(DEFAULT_RECORD_TYPES))
|
||||||
|
dns_servers: list[str] = field(default_factory=list)
|
||||||
|
timeout_ms: int = 2000
|
||||||
|
repeat_count: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DnsQueryResult:
|
||||||
|
domain: str
|
||||||
|
record_type: str
|
||||||
|
dns_server: str
|
||||||
|
adapter: str
|
||||||
|
status: str
|
||||||
|
elapsed_ms: float
|
||||||
|
values: str = ""
|
||||||
|
error: str = ""
|
||||||
|
verdict: str = ""
|
||||||
|
attempt: int = 1
|
||||||
|
resolver: str = "Resolve-DnsName"
|
||||||
|
checked_at: str = field(default_factory=lambda: time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
data = asdict(self)
|
||||||
|
data["elapsed_ms"] = round(self.elapsed_ms, 1)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class DnsDiagnostic:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
output: OutputCallback,
|
||||||
|
done: Optional[DoneCallback] = None,
|
||||||
|
status: Optional[StatusCallback] = None,
|
||||||
|
result: Optional[ResultCallback] = None,
|
||||||
|
):
|
||||||
|
self.output = output
|
||||||
|
self.done = done or (lambda: None)
|
||||||
|
self.status = status or (lambda _stats: None)
|
||||||
|
self.result = result or (lambda _row: None)
|
||||||
|
self.network = NetworkManager(lambda _text, _tag=None: None)
|
||||||
|
self.stop_event = threading.Event()
|
||||||
|
self.worker = None
|
||||||
|
self.last_results: list[dict] = []
|
||||||
|
self.last_summary = ""
|
||||||
|
self.local_dns_servers: set[str] = set()
|
||||||
|
|
||||||
|
def get_adapter_choices(self) -> list[str]:
|
||||||
|
adapters = self._active_adapters(self.network.get_network_info())
|
||||||
|
return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
|
||||||
|
|
||||||
|
def default_dns_servers(self, adapter_name: str = ALL_ADAPTERS) -> str:
|
||||||
|
adapters = self._select_adapters(adapter_name)
|
||||||
|
servers = collect_adapter_dns(adapters)
|
||||||
|
return ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS)))
|
||||||
|
|
||||||
|
def start_diagnosis(self, adapter_name: str = ALL_ADAPTERS, options: Optional[dict] = None) -> None:
|
||||||
|
if self.is_running():
|
||||||
|
raise RuntimeError("DNS 诊断正在运行,请先停止当前任务")
|
||||||
|
|
||||||
|
adapters = self._select_adapters(adapter_name)
|
||||||
|
if not adapters:
|
||||||
|
raise ValueError("没有找到可用于 DNS 诊断的活动网卡")
|
||||||
|
|
||||||
|
diag_options = self.normalize_options(options, adapters)
|
||||||
|
tasks = build_tasks(diag_options)
|
||||||
|
if not tasks:
|
||||||
|
raise ValueError("没有可执行的 DNS 查询任务")
|
||||||
|
|
||||||
|
self.stop_event.clear()
|
||||||
|
self.last_results = []
|
||||||
|
self.last_summary = ""
|
||||||
|
self.local_dns_servers = set(collect_adapter_dns(adapters))
|
||||||
|
current_dns = ",".join(sorted(self.local_dns_servers)) or "未读取到本机 DNS"
|
||||||
|
self.output(
|
||||||
|
f"开始 DNS 诊断: {adapter_name or ALL_ADAPTERS},"
|
||||||
|
f"域名 {len(diag_options.domains)} 个,类型 {len(diag_options.record_types)} 个,"
|
||||||
|
f"DNS {len(diag_options.dns_servers)} 个,重复 {diag_options.repeat_count} 次\n",
|
||||||
|
"muted",
|
||||||
|
)
|
||||||
|
self.status(self._status("查询中", current_dns, len(tasks), 0, 0, 0))
|
||||||
|
self.worker = threading.Thread(
|
||||||
|
target=self._run_diagnosis,
|
||||||
|
args=(adapter_name or ALL_ADAPTERS, current_dns, tasks, diag_options),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self.worker.start()
|
||||||
|
|
||||||
|
def _run_diagnosis(self, adapter_name: str, current_dns: str, tasks: list[dict], options: DnsDiagOptions) -> None:
|
||||||
|
started = time.perf_counter()
|
||||||
|
completed = 0
|
||||||
|
abnormal = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
workers = min(8, max(1, len(tasks)))
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
|
futures = {
|
||||||
|
executor.submit(self.query_one, adapter_name, task, options.timeout_ms): task
|
||||||
|
for task in tasks
|
||||||
|
}
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
if self.stop_event.is_set():
|
||||||
|
for item in futures:
|
||||||
|
item.cancel()
|
||||||
|
break
|
||||||
|
completed += 1
|
||||||
|
try:
|
||||||
|
row = future.result().to_dict()
|
||||||
|
except Exception as exc:
|
||||||
|
task = futures[future]
|
||||||
|
row = DnsQueryResult(
|
||||||
|
domain=task["domain"],
|
||||||
|
record_type=task["record_type"],
|
||||||
|
dns_server=task["dns_server"],
|
||||||
|
adapter=adapter_name,
|
||||||
|
status="失败",
|
||||||
|
elapsed_ms=0,
|
||||||
|
error=str(exc),
|
||||||
|
verdict="查询失败,建议与其它 DNS 对比",
|
||||||
|
attempt=task["attempt"],
|
||||||
|
).to_dict()
|
||||||
|
self.last_results.append(row)
|
||||||
|
if row.get("status") != "正常":
|
||||||
|
abnormal += 1
|
||||||
|
self.result(row)
|
||||||
|
self.status(self._status("查询中", current_dns, len(tasks), completed, abnormal, time.perf_counter() - started))
|
||||||
|
|
||||||
|
self.last_summary = build_summary(self.last_results, self.local_dns_servers, stopped=self.stop_event.is_set())
|
||||||
|
if self.stop_event.is_set():
|
||||||
|
self.output("\nDNS 诊断已停止\n", "warning")
|
||||||
|
else:
|
||||||
|
self.output("\nDNS 诊断完成\n", "success")
|
||||||
|
self.output(self.last_summary + "\n", "success")
|
||||||
|
self.status(
|
||||||
|
self._status(
|
||||||
|
"已停止" if self.stop_event.is_set() else "已完成",
|
||||||
|
current_dns,
|
||||||
|
len(tasks),
|
||||||
|
completed,
|
||||||
|
abnormal,
|
||||||
|
time.perf_counter() - started,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self.output(f"\nDNS 诊断失败: {exc}\n", "error")
|
||||||
|
self.status(self._status("失败", current_dns, len(tasks), completed, abnormal, time.perf_counter() - started))
|
||||||
|
finally:
|
||||||
|
self.done()
|
||||||
|
|
||||||
|
def query_one(self, adapter_name: str, task: dict, timeout_ms: int) -> DnsQueryResult:
|
||||||
|
if self.stop_event.is_set():
|
||||||
|
return DnsQueryResult(
|
||||||
|
domain=task["domain"],
|
||||||
|
record_type=task["record_type"],
|
||||||
|
dns_server=task["dns_server"],
|
||||||
|
adapter=adapter_name,
|
||||||
|
status="失败",
|
||||||
|
elapsed_ms=0,
|
||||||
|
error="任务已停止",
|
||||||
|
verdict="任务停止,结果不完整",
|
||||||
|
attempt=task["attempt"],
|
||||||
|
)
|
||||||
|
|
||||||
|
started = time.perf_counter()
|
||||||
|
domain = task["domain"]
|
||||||
|
record_type = task["record_type"]
|
||||||
|
dns_server = task["dns_server"]
|
||||||
|
try:
|
||||||
|
values = resolve_with_powershell(domain, record_type, dns_server, timeout_ms)
|
||||||
|
resolver = "Resolve-DnsName"
|
||||||
|
except Exception as ps_error:
|
||||||
|
try:
|
||||||
|
values = resolve_with_nslookup(domain, record_type, dns_server, timeout_ms)
|
||||||
|
resolver = "nslookup"
|
||||||
|
except Exception as ns_error:
|
||||||
|
elapsed = (time.perf_counter() - started) * 1000
|
||||||
|
error = classify_error(f"{ps_error}; {ns_error}")
|
||||||
|
return DnsQueryResult(
|
||||||
|
domain=domain,
|
||||||
|
record_type=record_type,
|
||||||
|
dns_server=dns_server,
|
||||||
|
adapter=adapter_name,
|
||||||
|
status="失败",
|
||||||
|
elapsed_ms=elapsed,
|
||||||
|
error=error,
|
||||||
|
verdict=verdict_for_error(error),
|
||||||
|
attempt=task["attempt"],
|
||||||
|
resolver="fallback",
|
||||||
|
)
|
||||||
|
|
||||||
|
elapsed = (time.perf_counter() - started) * 1000
|
||||||
|
if values:
|
||||||
|
return DnsQueryResult(
|
||||||
|
domain=domain,
|
||||||
|
record_type=record_type,
|
||||||
|
dns_server=dns_server,
|
||||||
|
adapter=adapter_name,
|
||||||
|
status="正常",
|
||||||
|
elapsed_ms=elapsed,
|
||||||
|
values="; ".join(values),
|
||||||
|
verdict="解析成功",
|
||||||
|
attempt=task["attempt"],
|
||||||
|
resolver=resolver,
|
||||||
|
)
|
||||||
|
return DnsQueryResult(
|
||||||
|
domain=domain,
|
||||||
|
record_type=record_type,
|
||||||
|
dns_server=dns_server,
|
||||||
|
adapter=adapter_name,
|
||||||
|
status="无记录",
|
||||||
|
elapsed_ms=elapsed,
|
||||||
|
verdict="未返回该类型记录,建议结合其它记录类型判断",
|
||||||
|
attempt=task["attempt"],
|
||||||
|
resolver=resolver,
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop_diagnosis(self) -> None:
|
||||||
|
if not self.is_running():
|
||||||
|
raise RuntimeError("当前没有正在运行的 DNS 诊断")
|
||||||
|
self.stop_event.set()
|
||||||
|
self.output("\n正在停止 DNS 诊断...\n", "warning")
|
||||||
|
|
||||||
|
def export_results(self, path: str) -> None:
|
||||||
|
if not self.last_results:
|
||||||
|
raise RuntimeError("还没有可导出的 DNS 诊断结果")
|
||||||
|
fields = [
|
||||||
|
"domain",
|
||||||
|
"record_type",
|
||||||
|
"dns_server",
|
||||||
|
"adapter",
|
||||||
|
"status",
|
||||||
|
"elapsed_ms",
|
||||||
|
"values",
|
||||||
|
"error",
|
||||||
|
"verdict",
|
||||||
|
"attempt",
|
||||||
|
"resolver",
|
||||||
|
"checked_at",
|
||||||
|
]
|
||||||
|
with open(path, "w", newline="", encoding="utf-8-sig") as file:
|
||||||
|
writer = csv.DictWriter(file, fieldnames=fields)
|
||||||
|
writer.writeheader()
|
||||||
|
for row in self.last_results:
|
||||||
|
writer.writerow({field_name: row.get(field_name, "") for field_name in fields})
|
||||||
|
|
||||||
|
def copy_summary(self) -> str:
|
||||||
|
return self.last_summary or "\n".join(
|
||||||
|
f"{row.get('domain', '')}\t{row.get('record_type', '')}\t{row.get('dns_server', '')}\t"
|
||||||
|
f"{row.get('status', '')}\t{row.get('elapsed_ms', '')}ms\t{row.get('values', '') or row.get('error', '')}"
|
||||||
|
for row in self.last_results
|
||||||
|
)
|
||||||
|
|
||||||
|
def normalize_options(self, options: Optional[dict], adapters: list[dict]) -> DnsDiagOptions:
|
||||||
|
options = options or {}
|
||||||
|
domains = parse_list(str(options.get("domains", "")).strip() or DEFAULT_DOMAINS)
|
||||||
|
record_types = parse_record_types(str(options.get("record_types", "")).strip() or DEFAULT_RECORD_TYPES)
|
||||||
|
dns_servers = parse_dns_servers(str(options.get("dns_servers", "")).strip())
|
||||||
|
if not dns_servers:
|
||||||
|
dns_servers = collect_adapter_dns(adapters) + PUBLIC_DNS_SERVERS
|
||||||
|
dns_servers = list(dict.fromkeys(dns_servers))
|
||||||
|
return DnsDiagOptions(
|
||||||
|
domains=domains,
|
||||||
|
record_types=record_types,
|
||||||
|
dns_servers=dns_servers,
|
||||||
|
timeout_ms=clamp_int(options.get("timeout_ms", 2000), 300, 10000, "超时"),
|
||||||
|
repeat_count=clamp_int(options.get("repeat_count", 1), 1, 5, "重复次数"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _select_adapters(self, adapter_name: str) -> list[dict]:
|
||||||
|
adapters = self._active_adapters(self.network.get_network_info())
|
||||||
|
if not adapter_name or adapter_name == ALL_ADAPTERS:
|
||||||
|
return adapters
|
||||||
|
return [adapter for adapter in adapters if adapter.get("name") == adapter_name]
|
||||||
|
|
||||||
|
def _active_adapters(self, adapters: list[dict]) -> list[dict]:
|
||||||
|
active = []
|
||||||
|
for adapter in adapters:
|
||||||
|
if not adapter.get("ipv4"):
|
||||||
|
continue
|
||||||
|
status = str(adapter.get("status", "")).lower()
|
||||||
|
if "disconnect" in status or "断开" in status:
|
||||||
|
continue
|
||||||
|
active.append(adapter)
|
||||||
|
return active
|
||||||
|
|
||||||
|
def _status(self, state: str, current_dns: str, total: int, completed: int, abnormal: int, elapsed: float) -> dict:
|
||||||
|
return {
|
||||||
|
"state": state,
|
||||||
|
"current_dns": current_dns,
|
||||||
|
"total": total,
|
||||||
|
"completed": completed,
|
||||||
|
"abnormal": abnormal,
|
||||||
|
"elapsed": elapsed,
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
return bool(self.worker and self.worker.is_alive())
|
||||||
|
|
||||||
|
|
||||||
|
def build_tasks(options: DnsDiagOptions) -> list[dict]:
|
||||||
|
tasks = []
|
||||||
|
for attempt in range(1, options.repeat_count + 1):
|
||||||
|
for domain in options.domains:
|
||||||
|
for record_type in options.record_types:
|
||||||
|
for dns_server in options.dns_servers:
|
||||||
|
tasks.append(
|
||||||
|
{
|
||||||
|
"domain": domain,
|
||||||
|
"record_type": record_type,
|
||||||
|
"dns_server": dns_server,
|
||||||
|
"attempt": attempt,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_with_powershell(domain: str, record_type: str, dns_server: str, timeout_ms: int) -> list[str]:
|
||||||
|
script = f"""
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$items = Resolve-DnsName -Name {ps_quote(domain)} -Type {ps_quote(record_type)} -Server {ps_quote(dns_server)} -DnsOnly -ErrorAction Stop
|
||||||
|
$items | Select-Object Name,Type,QueryType,IPAddress,NameHost,NameExchange,Preference,Strings,CharacterStrings | ConvertTo-Json -Depth 5 -Compress
|
||||||
|
"""
|
||||||
|
result = run_hidden(
|
||||||
|
["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script],
|
||||||
|
timeout=max(3, timeout_ms / 1000 + 2),
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(result.stdout.strip() or "Resolve-DnsName 查询失败")
|
||||||
|
data = extract_json(result.stdout)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = [data]
|
||||||
|
return normalize_record_values(data)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_with_nslookup(domain: str, record_type: str, dns_server: str, timeout_ms: int) -> list[str]:
|
||||||
|
seconds = max(1, int(round(timeout_ms / 1000)))
|
||||||
|
try:
|
||||||
|
result = run_hidden(["nslookup", f"-timeout={seconds}", f"-type={record_type}", domain, dns_server], timeout=seconds + 3)
|
||||||
|
except subprocess.TimeoutExpired as exc:
|
||||||
|
raise RuntimeError("nslookup 查询超时") from exc
|
||||||
|
output = result.stdout.strip()
|
||||||
|
if result.returncode != 0 and not output:
|
||||||
|
raise RuntimeError("nslookup 查询失败")
|
||||||
|
if is_nslookup_error(output):
|
||||||
|
raise RuntimeError(output)
|
||||||
|
return parse_nslookup_output(output, record_type)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_record_values(items) -> list[str]:
|
||||||
|
values = []
|
||||||
|
for item in items or []:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
if item.get("IPAddress"):
|
||||||
|
values.append(str(item["IPAddress"]))
|
||||||
|
if item.get("NameHost"):
|
||||||
|
values.append(str(item["NameHost"]).rstrip("."))
|
||||||
|
if item.get("NameExchange"):
|
||||||
|
exchange = str(item["NameExchange"]).rstrip(".")
|
||||||
|
preference = item.get("Preference")
|
||||||
|
values.append(f"{preference} {exchange}" if preference not in ("", None) else exchange)
|
||||||
|
for key in ("Strings", "CharacterStrings"):
|
||||||
|
text = item.get(key)
|
||||||
|
if isinstance(text, list):
|
||||||
|
values.append(" ".join(str(part) for part in text))
|
||||||
|
elif text:
|
||||||
|
values.append(str(text))
|
||||||
|
return sorted(dict.fromkeys(value.strip() for value in values if value and value.strip()))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_nslookup_output(output: str, record_type: str) -> list[str]:
|
||||||
|
values = []
|
||||||
|
answer_started = False
|
||||||
|
for raw_line in output.splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
lower = line.lower()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if lower.startswith("name:") or line.startswith("名称:") or "canonical name =" in lower or "mail exchanger =" in lower:
|
||||||
|
answer_started = True
|
||||||
|
if record_type in {"A", "AAAA"}:
|
||||||
|
match = re.search(r"(?:address|addresses|地址)\s*:\s*([0-9a-fA-F:.]+)", line, re.IGNORECASE)
|
||||||
|
if match and answer_started:
|
||||||
|
value = match.group(1)
|
||||||
|
if ":" in value or "." in value:
|
||||||
|
values.append(value)
|
||||||
|
elif record_type == "CNAME":
|
||||||
|
match = re.search(r"canonical name\s*=\s*(.+)$", line, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
values.append(match.group(1).strip().rstrip("."))
|
||||||
|
elif record_type == "MX":
|
||||||
|
match = re.search(r"mail exchanger\s*=\s*(.+)$", line, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
values.append(match.group(1).strip().rstrip("."))
|
||||||
|
elif record_type == "TXT":
|
||||||
|
match = re.search(r'text\s*=\s*"?(.*?)"?$', line, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
values.append(match.group(1).strip())
|
||||||
|
elif record_type == "NS":
|
||||||
|
match = re.search(r"nameserver\s*=\s*(.+)$", line, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
values.append(match.group(1).strip().rstrip("."))
|
||||||
|
if answer_started and record_type in {"A", "AAAA"}:
|
||||||
|
bare_ip = re.match(r"^([0-9a-fA-F:.]+)$", line)
|
||||||
|
if bare_ip:
|
||||||
|
values.append(bare_ip.group(1))
|
||||||
|
return sorted(dict.fromkeys(value for value in values if value))
|
||||||
|
|
||||||
|
|
||||||
|
def build_summary(rows: list[dict], local_dns_servers: set[str], stopped: bool = False) -> str:
|
||||||
|
if stopped:
|
||||||
|
return "DNS 诊断已停止,当前结果仅代表已完成查询。"
|
||||||
|
if not rows:
|
||||||
|
return "DNS 诊断未产生结果。"
|
||||||
|
|
||||||
|
total = len(rows)
|
||||||
|
ok_rows = [row for row in rows if row.get("status") == "正常"]
|
||||||
|
failed_rows = [row for row in rows if row.get("status") != "正常"]
|
||||||
|
avg_elapsed = sum(float(row.get("elapsed_ms") or 0) for row in ok_rows) / len(ok_rows) if ok_rows else 0
|
||||||
|
lines = [
|
||||||
|
"==== DNS 诊断摘要 ====",
|
||||||
|
f"查询总数: {total} 成功: {len(ok_rows)} 异常: {len(failed_rows)} 成功平均耗时: {avg_elapsed:.1f} ms",
|
||||||
|
]
|
||||||
|
|
||||||
|
for (domain, record_type), group in group_by_domain_type(rows).items():
|
||||||
|
local = [row for row in group if row.get("dns_server") in local_dns_servers]
|
||||||
|
public = [row for row in group if row.get("dns_server") not in local_dns_servers]
|
||||||
|
local_ok = any(row.get("status") == "正常" for row in local)
|
||||||
|
public_ok = any(row.get("status") == "正常" for row in public)
|
||||||
|
any_ok = any(row.get("status") == "正常" for row in group)
|
||||||
|
if local and not local_ok and public_ok:
|
||||||
|
lines.append(f"{domain} {record_type}: 本机 DNS 失败但对比 DNS 成功,本机 DNS 服务器疑似异常。")
|
||||||
|
elif not any_ok:
|
||||||
|
lines.append(f"{domain} {record_type}: 所有 DNS 均未成功,可能域名不存在、网络不可达或上游 DNS 异常。")
|
||||||
|
elif values_are_inconsistent(group):
|
||||||
|
lines.append(f"{domain} {record_type}: 不同 DNS 返回结果不一致,可能存在 CDN、缓存、污染或策略差异。")
|
||||||
|
|
||||||
|
if len(lines) == 2:
|
||||||
|
lines.append("未发现明显 DNS 异常;如故障偶发,建议增加重复次数复测。")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def group_by_domain_type(rows: list[dict]) -> dict[tuple[str, str], list[dict]]:
|
||||||
|
grouped: dict[tuple[str, str], list[dict]] = {}
|
||||||
|
for row in rows:
|
||||||
|
grouped.setdefault((row.get("domain", ""), row.get("record_type", "")), []).append(row)
|
||||||
|
return grouped
|
||||||
|
|
||||||
|
|
||||||
|
def values_are_inconsistent(rows: list[dict]) -> bool:
|
||||||
|
value_sets = {
|
||||||
|
tuple(sorted(value.strip() for value in str(row.get("values", "")).split(";") if value.strip()))
|
||||||
|
for row in rows
|
||||||
|
if row.get("status") == "正常" and row.get("values")
|
||||||
|
}
|
||||||
|
return len(value_sets) > 1
|
||||||
|
|
||||||
|
|
||||||
|
def collect_adapter_dns(adapters: list[dict]) -> list[str]:
|
||||||
|
servers = []
|
||||||
|
for adapter in adapters:
|
||||||
|
for key in ("dns1", "dns2"):
|
||||||
|
value = str(adapter.get(key, "")).strip()
|
||||||
|
if value and is_ip_address(value):
|
||||||
|
servers.append(value)
|
||||||
|
return list(dict.fromkeys(servers))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_list(text: str, max_items: int = 20) -> list[str]:
|
||||||
|
items = [item.strip() for item in re.split(r"[,,;\s]+", text.strip()) if item.strip()]
|
||||||
|
return list(dict.fromkeys(items))[:max_items]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_record_types(text: str) -> list[str]:
|
||||||
|
values = [item.upper() for item in parse_list(text, 10)]
|
||||||
|
invalid = [item for item in values if item not in SUPPORTED_RECORD_TYPES]
|
||||||
|
if invalid:
|
||||||
|
raise ValueError("不支持的 DNS 记录类型: " + ", ".join(invalid))
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dns_servers(text: str) -> list[str]:
|
||||||
|
servers = []
|
||||||
|
for item in parse_list(text, 12):
|
||||||
|
if not is_ip_address(item):
|
||||||
|
raise ValueError(f"DNS 服务器必须是 IP 地址: {item}")
|
||||||
|
servers.append(item)
|
||||||
|
return list(dict.fromkeys(servers))
|
||||||
|
|
||||||
|
|
||||||
|
def classify_error(text: str) -> str:
|
||||||
|
clean = compact_text(text)
|
||||||
|
lower = clean.lower()
|
||||||
|
if "timed out" in lower or "timeout" in lower or "超时" in clean:
|
||||||
|
return "查询超时"
|
||||||
|
if "non-existent" in lower or "nxdomain" in lower or "不存在" in clean:
|
||||||
|
return "域名或记录不存在"
|
||||||
|
if "refused" in lower or "拒绝" in clean:
|
||||||
|
return "DNS 服务器拒绝查询"
|
||||||
|
if "server failed" in lower or "servfail" in lower:
|
||||||
|
return "上游 DNS 返回失败"
|
||||||
|
return clean[:240] if clean else "解析失败"
|
||||||
|
|
||||||
|
|
||||||
|
def verdict_for_error(error: str) -> str:
|
||||||
|
if "超时" in error:
|
||||||
|
return "查询超时,可能 DNS 服务器不可达或网络阻塞"
|
||||||
|
if "不存在" in error:
|
||||||
|
return "未查询到该记录,建议与其它 DNS 或记录类型对比"
|
||||||
|
if "拒绝" in error:
|
||||||
|
return "DNS 服务器拒绝查询,可能存在策略限制"
|
||||||
|
return "解析失败,建议与其它 DNS 对比"
|
||||||
|
|
||||||
|
|
||||||
|
def is_nslookup_error(output: str) -> bool:
|
||||||
|
lower = output.lower()
|
||||||
|
return any(token in lower for token in ("timed out", "can't find", "non-existent", "servfail", "refused"))
|
||||||
|
|
||||||
|
|
||||||
|
def is_ip_address(value: str) -> bool:
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(value)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def extract_json(text: str):
|
||||||
|
output = text.strip()
|
||||||
|
if not output:
|
||||||
|
return []
|
||||||
|
json_start = min([idx for idx in (output.find("["), output.find("{")) if idx >= 0], default=-1)
|
||||||
|
if json_start < 0:
|
||||||
|
return []
|
||||||
|
data = json.loads(output[json_start:])
|
||||||
|
return [] if data is None else data
|
||||||
|
|
||||||
|
|
||||||
|
def ps_quote(value: str) -> str:
|
||||||
|
return "'" + str(value).replace("'", "''") + "'"
|
||||||
|
|
||||||
|
|
||||||
|
def compact_text(text: str) -> str:
|
||||||
|
return re.sub(r"\s+", " ", text or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def clamp_int(value, min_value: int, max_value: int, label: str) -> int:
|
||||||
|
try:
|
||||||
|
number = int(str(value).strip())
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(f"{label}必须是整数") from exc
|
||||||
|
if number < min_value or number > max_value:
|
||||||
|
raise ValueError(f"{label}必须在 {min_value}-{max_value} 之间")
|
||||||
|
return number
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
|
|
||||||
|
|
||||||
class BasicUI:
|
|
||||||
"""旧版页面的兼容辅助类。
|
|
||||||
|
|
||||||
新界面主要使用 core.ui.components;保留这个类是为了兼容后续可能仍引用
|
|
||||||
add_input/add_combobox/add_button 的小工具页。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def add_input(
|
|
||||||
self,
|
|
||||||
parent,
|
|
||||||
label_text,
|
|
||||||
row,
|
|
||||||
col=0,
|
|
||||||
inivar="",
|
|
||||||
label_width=8,
|
|
||||||
entry_width=20,
|
|
||||||
colspan=1,
|
|
||||||
sticky="w",
|
|
||||||
):
|
|
||||||
group_frame = ttk.Frame(parent)
|
|
||||||
group_frame.grid(row=row, column=col, columnspan=colspan, sticky=sticky, padx=5, pady=3)
|
|
||||||
|
|
||||||
label = ttk.Label(group_frame, text=f"{label_text}:", width=label_width, anchor="w")
|
|
||||||
label.grid(row=0, column=0, sticky="w", padx=(0, 5))
|
|
||||||
|
|
||||||
var = tk.StringVar(value=inivar)
|
|
||||||
entry = ttk.Entry(group_frame, textvariable=var, width=entry_width)
|
|
||||||
entry.grid(row=0, column=1, sticky="w")
|
|
||||||
|
|
||||||
return {"frame": group_frame, "label": label, "entry": entry, "var": var}
|
|
||||||
|
|
||||||
def add_combobox(
|
|
||||||
self,
|
|
||||||
parent,
|
|
||||||
label_text,
|
|
||||||
row,
|
|
||||||
col=0,
|
|
||||||
listbox=None,
|
|
||||||
inivar=0,
|
|
||||||
width=17,
|
|
||||||
colspan=1,
|
|
||||||
sticky="w",
|
|
||||||
label_width=8,
|
|
||||||
state="readonly",
|
|
||||||
):
|
|
||||||
values = listbox or []
|
|
||||||
frame = ttk.Frame(parent)
|
|
||||||
frame.grid(row=row, column=col, columnspan=colspan, sticky=sticky, padx=5, pady=3)
|
|
||||||
|
|
||||||
label = ttk.Label(frame, text=f"{label_text}:", width=label_width, anchor="w")
|
|
||||||
label.grid(row=0, column=0, sticky="w", padx=(0, 5))
|
|
||||||
|
|
||||||
var = tk.StringVar()
|
|
||||||
combobox = ttk.Combobox(frame, textvariable=var, values=values, width=width, state=state)
|
|
||||||
combobox.grid(row=0, column=1, sticky="w")
|
|
||||||
if values and inivar >= 0:
|
|
||||||
var.set(values[inivar])
|
|
||||||
|
|
||||||
return {"frame": frame, "label": label, "combobox": combobox, "var": var}
|
|
||||||
|
|
||||||
def add_button(self, parent, button_text, row, col=0, command=None, width=5, colspan=1, sticky="w"):
|
|
||||||
group_frame = ttk.Frame(parent)
|
|
||||||
group_frame.grid(row=row, column=col, columnspan=colspan, sticky=sticky, padx=5, pady=3)
|
|
||||||
|
|
||||||
btn = ttk.Button(group_frame, text=button_text, command=command, width=width)
|
|
||||||
btn.grid(row=0, column=0, sticky="w")
|
|
||||||
return {"frame": group_frame, "btn": btn}
|
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
import threading
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog, messagebox, ttk
|
||||||
|
|
||||||
|
from core.Function.device_discovery_fun import ALL_ADAPTERS, DeviceDiscovery
|
||||||
|
from core.ui.components import Console, Page, action_bar, button, combo, field
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceDiscoveryTab(Page):
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent, "设备发现", "发现局域网在线与 ARP 可见设备,整理 IP、MAC、主机名、厂商与来源网卡。")
|
||||||
|
self.body.rowconfigure(3, weight=1)
|
||||||
|
|
||||||
|
status = self.section("实时状态", 0, columns=6)
|
||||||
|
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||||
|
self.current = field(status, "当前网卡", 1, 1, "-", 18)
|
||||||
|
self.scan_scope = field(status, "扫描范围", 1, 2, "-", 20)
|
||||||
|
self.progress = field(status, "进度", 1, 3, "0/0", 10)
|
||||||
|
self.found = field(status, "发现设备", 1, 4, "0", 8)
|
||||||
|
self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10)
|
||||||
|
for item in (self.state, self.current, self.scan_scope, self.progress, self.found, self.elapsed):
|
||||||
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
|
params = self.section("发现参数", 1, columns=6)
|
||||||
|
self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22)
|
||||||
|
self.scan_range = field(params, "扫描范围", 1, 1, "", 34, colspan=2)
|
||||||
|
self.workers = field(params, "并发数", 1, 3, "64", 10)
|
||||||
|
self.timeout = field(params, "超时 ms", 1, 4, "500", 10)
|
||||||
|
self.max_hosts = field(params, "最大地址数", 1, 5, "254", 10)
|
||||||
|
actions = action_bar(params, 2, 6)
|
||||||
|
self.start_btn = button(actions, "开始发现", self.start_discovery, "Primary.TButton")
|
||||||
|
self.stop_btn = button(actions, "停止", self.stop_discovery, "Danger.TButton")
|
||||||
|
self.refresh_btn = button(actions, "刷新网卡", self.load_adapters, "Secondary.TButton")
|
||||||
|
self.auto_range_btn = button(actions, "自动范围", self.fill_default_range, "Secondary.TButton")
|
||||||
|
self.copy_btn = button(actions, "复制清单", self.copy_inventory, "Secondary.TButton")
|
||||||
|
self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
||||||
|
|
||||||
|
results = self.section("发现结果", 2, columns=1)
|
||||||
|
results.rowconfigure(1, weight=1)
|
||||||
|
results.columnconfigure(0, weight=1)
|
||||||
|
self.results_tree = ttk.Treeview(
|
||||||
|
results,
|
||||||
|
columns=("ip", "mac", "hostname", "vendor", "adapter", "latency", "method", "note"),
|
||||||
|
show="headings",
|
||||||
|
height=10,
|
||||||
|
)
|
||||||
|
headings = {
|
||||||
|
"ip": "IP",
|
||||||
|
"mac": "MAC",
|
||||||
|
"hostname": "主机名",
|
||||||
|
"vendor": "厂商",
|
||||||
|
"adapter": "来源网卡",
|
||||||
|
"latency": "延迟",
|
||||||
|
"method": "发现方式",
|
||||||
|
"note": "备注",
|
||||||
|
}
|
||||||
|
widths = {
|
||||||
|
"ip": 130,
|
||||||
|
"mac": 150,
|
||||||
|
"hostname": 180,
|
||||||
|
"vendor": 130,
|
||||||
|
"adapter": 150,
|
||||||
|
"latency": 85,
|
||||||
|
"method": 90,
|
||||||
|
"note": 130,
|
||||||
|
}
|
||||||
|
for column, title in headings.items():
|
||||||
|
self.results_tree.heading(column, text=title)
|
||||||
|
self.results_tree.column(column, width=widths[column], anchor="w")
|
||||||
|
self.results_tree.tag_configure("在线", foreground="#15803d")
|
||||||
|
self.results_tree.tag_configure("ARP 可见", foreground="#b7791f")
|
||||||
|
self.results_tree.tag_configure("本机", foreground="#246bfe")
|
||||||
|
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.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
||||||
|
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
||||||
|
|
||||||
|
output = self.section("发现控制台", 3, columns=1)
|
||||||
|
output.rowconfigure(1, weight=1)
|
||||||
|
output.columnconfigure(0, weight=1)
|
||||||
|
self.console = Console(output, height=12)
|
||||||
|
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
||||||
|
|
||||||
|
self.discovery = DeviceDiscovery(self.write, self.on_task_done, self.update_status, self.add_result)
|
||||||
|
self.after(350, self.load_adapters)
|
||||||
|
|
||||||
|
def write(self, text, tag=None):
|
||||||
|
self.console.write(text, tag)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.console.clear()
|
||||||
|
for item in self.results_tree.get_children():
|
||||||
|
self.results_tree.delete(item)
|
||||||
|
self.update_status(
|
||||||
|
{
|
||||||
|
"state": "等待",
|
||||||
|
"current": "-",
|
||||||
|
"scan_range": "-",
|
||||||
|
"total": 0,
|
||||||
|
"scanned": 0,
|
||||||
|
"found": 0,
|
||||||
|
"elapsed": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_adapters(self):
|
||||||
|
def worker():
|
||||||
|
try:
|
||||||
|
values = self.discovery.get_adapter_choices()
|
||||||
|
default_range = self.discovery.default_scan_range(ALL_ADAPTERS)
|
||||||
|
except Exception as exc:
|
||||||
|
values = [ALL_ADAPTERS]
|
||||||
|
default_range = ""
|
||||||
|
self.write(f"读取网卡失败: {exc}\n", "warning")
|
||||||
|
self.after(0, lambda: self.apply_adapters(values, default_range))
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
def apply_adapters(self, values, default_range):
|
||||||
|
values = values or [ALL_ADAPTERS]
|
||||||
|
values = list(dict.fromkeys(values))
|
||||||
|
self.adapter["combobox"]["values"] = values
|
||||||
|
if self.adapter["var"].get() not in values:
|
||||||
|
self.adapter["var"].set(values[1] if len(values) > 1 else values[0])
|
||||||
|
if not self.scan_range["var"].get() and default_range:
|
||||||
|
self.scan_range["var"].set(default_range)
|
||||||
|
|
||||||
|
def fill_default_range(self):
|
||||||
|
try:
|
||||||
|
value = self.discovery.default_scan_range(self.adapter["var"].get())
|
||||||
|
if not value:
|
||||||
|
messagebox.showinfo("提示", "未能根据当前网卡生成扫描范围")
|
||||||
|
return
|
||||||
|
self.scan_range["var"].set(value)
|
||||||
|
self.write(f"已生成安全扫描范围: {value}\n", "success")
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showwarning("生成失败", str(exc))
|
||||||
|
|
||||||
|
def start_discovery(self):
|
||||||
|
try:
|
||||||
|
self.clear()
|
||||||
|
self.start_btn.configure(state="disabled")
|
||||||
|
self.discovery.start_discovery(self.adapter["var"].get(), self.options())
|
||||||
|
except Exception as exc:
|
||||||
|
self.start_btn.configure(state="normal")
|
||||||
|
messagebox.showwarning("无法开始设备发现", str(exc))
|
||||||
|
|
||||||
|
def stop_discovery(self):
|
||||||
|
try:
|
||||||
|
self.discovery.stop_discovery()
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showinfo("提示", str(exc))
|
||||||
|
|
||||||
|
def options(self):
|
||||||
|
return {
|
||||||
|
"scan_range": self.scan_range["var"].get(),
|
||||||
|
"workers": self.workers["var"].get(),
|
||||||
|
"timeout_ms": self.timeout["var"].get(),
|
||||||
|
"max_hosts": self.max_hosts["var"].get(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def export_results(self):
|
||||||
|
path = filedialog.asksaveasfilename(
|
||||||
|
title="导出设备发现结果",
|
||||||
|
defaultextension=".csv",
|
||||||
|
filetypes=[("CSV 文件", "*.csv")],
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.discovery.export_results(path)
|
||||||
|
self.write(f"已导出结果: {path}\n", "success")
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showwarning("导出失败", str(exc))
|
||||||
|
|
||||||
|
def copy_inventory(self):
|
||||||
|
text = self.discovery.copy_summary()
|
||||||
|
if not text:
|
||||||
|
messagebox.showinfo("提示", "还没有可复制的设备清单")
|
||||||
|
return
|
||||||
|
self.clipboard_clear()
|
||||||
|
self.clipboard_append(text)
|
||||||
|
self.write("已复制设备清单到剪贴板\n", "success")
|
||||||
|
|
||||||
|
def update_status(self, stats):
|
||||||
|
def apply():
|
||||||
|
total = stats.get("total", 0)
|
||||||
|
scanned = stats.get("scanned", 0)
|
||||||
|
values = [
|
||||||
|
(self.state, stats.get("state", "等待")),
|
||||||
|
(self.current, stats.get("current") or "-"),
|
||||||
|
(self.scan_scope, stats.get("scan_range") or "-"),
|
||||||
|
(self.progress, f"{scanned}/{total}" if total else "0/0"),
|
||||||
|
(self.found, str(stats.get("found", 0))),
|
||||||
|
(self.elapsed, f"{stats.get('elapsed', 0):.1f}s"),
|
||||||
|
]
|
||||||
|
for item, value in values:
|
||||||
|
item["entry"].configure(state="normal")
|
||||||
|
item["var"].set(value)
|
||||||
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
|
self.after(0, apply)
|
||||||
|
|
||||||
|
def add_result(self, row):
|
||||||
|
def apply():
|
||||||
|
method = row.get("method", "")
|
||||||
|
note = row.get("note", "")
|
||||||
|
tag = "本机" if "本机" in note else method
|
||||||
|
latency = row.get("latency_ms", 0)
|
||||||
|
try:
|
||||||
|
latency_text = f"{float(latency):.1f} ms" if float(latency) > 0 else ""
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
latency_text = ""
|
||||||
|
self.results_tree.insert(
|
||||||
|
"",
|
||||||
|
"end",
|
||||||
|
values=(
|
||||||
|
row.get("ip", ""),
|
||||||
|
row.get("mac", ""),
|
||||||
|
row.get("hostname", ""),
|
||||||
|
row.get("vendor", ""),
|
||||||
|
row.get("adapter", ""),
|
||||||
|
latency_text,
|
||||||
|
method,
|
||||||
|
note,
|
||||||
|
),
|
||||||
|
tags=(tag,),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.after(0, apply)
|
||||||
|
|
||||||
|
def on_task_done(self):
|
||||||
|
self.after(0, lambda: self.start_btn.configure(state="normal"))
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import threading
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog, messagebox, ttk
|
||||||
|
|
||||||
|
from core.Function.dns_diag_fun import ALL_ADAPTERS, DEFAULT_DOMAINS, DEFAULT_RECORD_TYPES, DnsDiagnostic
|
||||||
|
from core.ui.components import Console, Page, action_bar, button, combo, field
|
||||||
|
|
||||||
|
|
||||||
|
class DnsTab(Page):
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent, "DNS 诊断", "对比本机 DNS 与常用 DNS 的解析结果、耗时和失败原因。")
|
||||||
|
self.body.rowconfigure(3, weight=1)
|
||||||
|
|
||||||
|
status = self.section("实时状态", 0, columns=6)
|
||||||
|
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||||
|
self.current_dns = field(status, "当前 DNS", 1, 1, "-", 24, colspan=2)
|
||||||
|
self.target_total = field(status, "目标数", 1, 3, "0", 8)
|
||||||
|
self.progress = field(status, "进度", 1, 4, "0/0", 10)
|
||||||
|
self.abnormal = field(status, "异常数", 1, 5, "0", 8)
|
||||||
|
self.elapsed = field(status, "耗时", 2, 0, "0.0s", 10)
|
||||||
|
for item in (self.state, self.current_dns, self.target_total, self.progress, self.abnormal, self.elapsed):
|
||||||
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
|
params = self.section("诊断参数", 1, columns=6)
|
||||||
|
self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22)
|
||||||
|
self.domains = field(params, "目标域名", 1, 1, DEFAULT_DOMAINS, 34, colspan=2)
|
||||||
|
self.record_types = field(params, "记录类型", 1, 3, DEFAULT_RECORD_TYPES, 18)
|
||||||
|
self.timeout = field(params, "超时 ms", 1, 4, "2000", 10)
|
||||||
|
self.repeat_count = field(params, "重复次数", 1, 5, "1", 10)
|
||||||
|
self.dns_servers = field(params, "DNS 服务器", 2, 0, "", 60, colspan=6)
|
||||||
|
actions = action_bar(params, 3, 6)
|
||||||
|
self.start_btn = button(actions, "开始诊断", self.start_diagnosis, "Primary.TButton")
|
||||||
|
self.stop_btn = button(actions, "停止", self.stop_diagnosis, "Danger.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.copy_btn = button(actions, "复制摘要", self.copy_summary, "Secondary.TButton")
|
||||||
|
self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
||||||
|
|
||||||
|
results = self.section("诊断结果", 2, columns=1)
|
||||||
|
results.rowconfigure(1, weight=1)
|
||||||
|
results.columnconfigure(0, weight=1)
|
||||||
|
self.results_tree = ttk.Treeview(
|
||||||
|
results,
|
||||||
|
columns=("domain", "type", "server", "status", "elapsed", "values", "verdict"),
|
||||||
|
show="headings",
|
||||||
|
height=10,
|
||||||
|
)
|
||||||
|
headings = {
|
||||||
|
"domain": "域名",
|
||||||
|
"type": "类型",
|
||||||
|
"server": "DNS 服务器",
|
||||||
|
"status": "状态",
|
||||||
|
"elapsed": "耗时",
|
||||||
|
"values": "解析结果",
|
||||||
|
"verdict": "错误 / 判断",
|
||||||
|
}
|
||||||
|
widths = {
|
||||||
|
"domain": 160,
|
||||||
|
"type": 70,
|
||||||
|
"server": 130,
|
||||||
|
"status": 80,
|
||||||
|
"elapsed": 85,
|
||||||
|
"values": 300,
|
||||||
|
"verdict": 360,
|
||||||
|
}
|
||||||
|
for column, title in headings.items():
|
||||||
|
self.results_tree.heading(column, text=title)
|
||||||
|
self.results_tree.column(column, width=widths[column], anchor="w")
|
||||||
|
self.results_tree.tag_configure("正常", foreground="#15803d")
|
||||||
|
self.results_tree.tag_configure("无记录", foreground="#b7791f")
|
||||||
|
self.results_tree.tag_configure("失败", foreground="#dc2626")
|
||||||
|
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.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
||||||
|
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
||||||
|
|
||||||
|
output = self.section("诊断控制台", 3, columns=1)
|
||||||
|
output.rowconfigure(1, weight=1)
|
||||||
|
output.columnconfigure(0, weight=1)
|
||||||
|
self.console = Console(output, height=12)
|
||||||
|
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
||||||
|
|
||||||
|
self.diagnostic = DnsDiagnostic(self.write, self.on_task_done, self.update_status, self.add_result)
|
||||||
|
self.after(350, self.load_adapters)
|
||||||
|
|
||||||
|
def write(self, text, tag=None):
|
||||||
|
self.console.write(text, tag)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.console.clear()
|
||||||
|
for item in self.results_tree.get_children():
|
||||||
|
self.results_tree.delete(item)
|
||||||
|
self.update_status(
|
||||||
|
{
|
||||||
|
"state": "等待",
|
||||||
|
"current_dns": "-",
|
||||||
|
"total": 0,
|
||||||
|
"completed": 0,
|
||||||
|
"abnormal": 0,
|
||||||
|
"elapsed": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_adapters(self):
|
||||||
|
def worker():
|
||||||
|
try:
|
||||||
|
values = self.diagnostic.get_adapter_choices()
|
||||||
|
default_dns = self.diagnostic.default_dns_servers(values[1] if len(values) > 1 else ALL_ADAPTERS)
|
||||||
|
except Exception as exc:
|
||||||
|
values = [ALL_ADAPTERS]
|
||||||
|
default_dns = ""
|
||||||
|
self.write(f"读取网卡 DNS 失败: {exc}\n", "warning")
|
||||||
|
self.after(0, lambda: self.apply_adapters(values, default_dns))
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
def apply_adapters(self, values, default_dns):
|
||||||
|
values = values or [ALL_ADAPTERS]
|
||||||
|
values = list(dict.fromkeys(values))
|
||||||
|
self.adapter["combobox"]["values"] = values
|
||||||
|
if self.adapter["var"].get() not in values:
|
||||||
|
self.adapter["var"].set(values[1] if len(values) > 1 else values[0])
|
||||||
|
if not self.dns_servers["var"].get() and default_dns:
|
||||||
|
self.dns_servers["var"].set(default_dns)
|
||||||
|
|
||||||
|
def fill_default_dns(self):
|
||||||
|
try:
|
||||||
|
value = self.diagnostic.default_dns_servers(self.adapter["var"].get())
|
||||||
|
if not value:
|
||||||
|
messagebox.showinfo("提示", "未读取到本机 DNS,已保留常用 DNS 对比服务器")
|
||||||
|
value = "223.5.5.5,114.114.114.114,8.8.8.8"
|
||||||
|
self.dns_servers["var"].set(value)
|
||||||
|
self.write(f"已生成 DNS 对比列表: {value}\n", "success")
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showwarning("生成失败", str(exc))
|
||||||
|
|
||||||
|
def start_diagnosis(self):
|
||||||
|
try:
|
||||||
|
self.clear()
|
||||||
|
self.start_btn.configure(state="disabled")
|
||||||
|
self.diagnostic.start_diagnosis(self.adapter["var"].get(), self.options())
|
||||||
|
except Exception as exc:
|
||||||
|
self.start_btn.configure(state="normal")
|
||||||
|
messagebox.showwarning("无法开始 DNS 诊断", str(exc))
|
||||||
|
|
||||||
|
def stop_diagnosis(self):
|
||||||
|
try:
|
||||||
|
self.diagnostic.stop_diagnosis()
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showinfo("提示", str(exc))
|
||||||
|
|
||||||
|
def options(self):
|
||||||
|
return {
|
||||||
|
"domains": self.domains["var"].get(),
|
||||||
|
"record_types": self.record_types["var"].get(),
|
||||||
|
"dns_servers": self.dns_servers["var"].get(),
|
||||||
|
"timeout_ms": self.timeout["var"].get(),
|
||||||
|
"repeat_count": self.repeat_count["var"].get(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def export_results(self):
|
||||||
|
path = filedialog.asksaveasfilename(
|
||||||
|
title="导出 DNS 诊断结果",
|
||||||
|
defaultextension=".csv",
|
||||||
|
filetypes=[("CSV 文件", "*.csv")],
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.diagnostic.export_results(path)
|
||||||
|
self.write(f"已导出结果: {path}\n", "success")
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showwarning("导出失败", str(exc))
|
||||||
|
|
||||||
|
def copy_summary(self):
|
||||||
|
text = self.diagnostic.copy_summary()
|
||||||
|
if not text:
|
||||||
|
messagebox.showinfo("提示", "还没有可复制的 DNS 诊断摘要")
|
||||||
|
return
|
||||||
|
self.clipboard_clear()
|
||||||
|
self.clipboard_append(text)
|
||||||
|
self.write("已复制 DNS 诊断摘要到剪贴板\n", "success")
|
||||||
|
|
||||||
|
def update_status(self, stats):
|
||||||
|
def apply():
|
||||||
|
total = stats.get("total", 0)
|
||||||
|
completed = stats.get("completed", 0)
|
||||||
|
values = [
|
||||||
|
(self.state, stats.get("state", "等待")),
|
||||||
|
(self.current_dns, stats.get("current_dns") or "-"),
|
||||||
|
(self.target_total, str(total)),
|
||||||
|
(self.progress, f"{completed}/{total}" if total else "0/0"),
|
||||||
|
(self.abnormal, str(stats.get("abnormal", 0))),
|
||||||
|
(self.elapsed, f"{stats.get('elapsed', 0):.1f}s"),
|
||||||
|
]
|
||||||
|
for item, value in values:
|
||||||
|
item["entry"].configure(state="normal")
|
||||||
|
item["var"].set(value)
|
||||||
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
|
self.after(0, apply)
|
||||||
|
|
||||||
|
def add_result(self, row):
|
||||||
|
def apply():
|
||||||
|
elapsed = row.get("elapsed_ms", 0)
|
||||||
|
try:
|
||||||
|
elapsed_text = f"{float(elapsed):.1f} ms" if float(elapsed) > 0 else ""
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
elapsed_text = ""
|
||||||
|
detail = row.get("error") or row.get("verdict", "")
|
||||||
|
self.results_tree.insert(
|
||||||
|
"",
|
||||||
|
"end",
|
||||||
|
values=(
|
||||||
|
row.get("domain", ""),
|
||||||
|
row.get("record_type", ""),
|
||||||
|
row.get("dns_server", ""),
|
||||||
|
row.get("status", ""),
|
||||||
|
elapsed_text,
|
||||||
|
row.get("values", ""),
|
||||||
|
detail,
|
||||||
|
),
|
||||||
|
tags=(row.get("status", ""),),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.after(0, apply)
|
||||||
|
|
||||||
|
def on_task_done(self):
|
||||||
|
self.after(0, lambda: self.start_btn.configure(state="normal"))
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
|
||||||
|
from core.ui.tab_device_discovery import DeviceDiscoveryTab
|
||||||
|
from core.ui.tab_dns import DnsTab
|
||||||
from core.ui.tab_ip_conflict import IpConflictTab
|
from core.ui.tab_ip_conflict import IpConflictTab
|
||||||
from core.ui.tab_loop import LoopTab
|
from core.ui.tab_loop import LoopTab
|
||||||
from core.ui.tab_network import NetworkTab
|
from core.ui.tab_network import NetworkTab
|
||||||
@@ -61,6 +63,8 @@ class MainUI:
|
|||||||
|
|
||||||
items = [
|
items = [
|
||||||
("network", "◎", "网卡配置", "IP / DNS / DHCP"),
|
("network", "◎", "网卡配置", "IP / DNS / DHCP"),
|
||||||
|
("dns", "◇", "DNS 诊断", "解析与服务器对比"),
|
||||||
|
("devices", "▦", "设备发现", "局域网资产"),
|
||||||
("ping", "⌁", "Ping 探测", "单点与批量探活"),
|
("ping", "⌁", "Ping 探测", "单点与批量探活"),
|
||||||
("ports", "⌕", "端口扫描", "TCP 连通性检测"),
|
("ports", "⌕", "端口扫描", "TCP 连通性检测"),
|
||||||
("trace", "↗", "路由追踪", "跳点路径分析"),
|
("trace", "↗", "路由追踪", "跳点路径分析"),
|
||||||
@@ -104,11 +108,13 @@ class MainUI:
|
|||||||
def _build_pages(self) -> None:
|
def _build_pages(self) -> None:
|
||||||
self.pages = {
|
self.pages = {
|
||||||
"network": NetworkTab(self.content),
|
"network": NetworkTab(self.content),
|
||||||
|
"dns": DnsTab(self.content),
|
||||||
"ping": PingTab(self.content),
|
"ping": PingTab(self.content),
|
||||||
"ports": TelnetTab(self.content),
|
"ports": TelnetTab(self.content),
|
||||||
"trace": TracertTab(self.content),
|
"trace": TracertTab(self.content),
|
||||||
"loop": LoopTab(self.content),
|
"loop": LoopTab(self.content),
|
||||||
"ip_conflict": IpConflictTab(self.content),
|
"ip_conflict": IpConflictTab(self.content),
|
||||||
|
"devices": DeviceDiscoveryTab(self.content),
|
||||||
}
|
}
|
||||||
for page in self.pages.values():
|
for page in self.pages.values():
|
||||||
page.grid(row=0, column=0, sticky="nsew")
|
page.grid(row=0, column=0, sticky="nsew")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ setlocal
|
|||||||
|
|
||||||
set "PYTHON_EXE=C:\Users\Administrator\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe"
|
set "PYTHON_EXE=C:\Users\Administrator\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe"
|
||||||
set "PYINSTALLER_PATH=%CD%\.packaging\pyinstaller"
|
set "PYINSTALLER_PATH=%CD%\.packaging\pyinstaller"
|
||||||
|
set "APP_ICON=%CD%\assets\app.ico"
|
||||||
|
|
||||||
if exist "%PYTHON_EXE%" (
|
if exist "%PYTHON_EXE%" (
|
||||||
if not exist "%PYINSTALLER_PATH%\PyInstaller\__main__.py" (
|
if not exist "%PYINSTALLER_PATH%\PyInstaller\__main__.py" (
|
||||||
@@ -10,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
|
"%PYTHON_EXE%" -m PyInstaller -F -w main.py -n NetworkTool --icon "%APP_ICON%"
|
||||||
) else (
|
) else (
|
||||||
pyinstaller -F -w main.py -n NetworkTool
|
pyinstaller -F -w main.py -n NetworkTool --icon "%APP_ICON%"
|
||||||
)
|
)
|
||||||
|
|
||||||
endlocal
|
endlocal
|
||||||
|
|||||||
Reference in New Issue
Block a user