From 0b442c4fbfaa402f2de1f139d7d1b41fbcebfcec Mon Sep 17 00:00:00 2001 From: LyCecilion Date: Sat, 11 Jul 2026 10:42:06 +0800 Subject: [PATCH 1/8] docs: complete project documentation --- AGENTS.md | 166 ++++++++++-------------------------------------- CHANGELOG.md | 18 ++++++ CONTRIBUTING.md | 47 ++++++++++++++ LICENSE | 21 ++++++ README.md | 109 ++++++++++++++++++++++++++++++- ROADMAP.md | 155 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 380 insertions(+), 136 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 ROADMAP.md diff --git a/AGENTS.md b/AGENTS.md index 5610418..f852932 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,146 +1,46 @@ # xidio Agents Instructions -xidio(XiDian Internet Diagnostic Intelligence Operator)是一款跨平台的校园网诊断工具,由 Project Hazelita 基于 C# 和 .NET 开发,专门针对西安电子科技大学的网络环境。 +xidio(Xidian Internet Diagnostic Intelligence Operator)是 Project Hazelita 基于 C# 和 .NET 10 开发的跨平台西电校园网诊断工具。 -在为 xidio 贡献代码前,请先阅读 `README.md` 了解项目最新情况,并仔细阅读本文件、遵循下面的指引。请确保你的修改是**最小化**的,并且与项目的现有内容保持一致、互不冲突。 +## 开始工作前 -## 各部分功能 +1. 阅读 `README.md` 了解当前能力与构建方式。 +2. 阅读 `docs/architecture.md` 和修改模块的相关文档。 +3. 对照 `ROADMAP.md` 确认需求边界;路线图是开发方向,不代表功能已实现。 +4. 检查当前分支和工作区,保留用户的现有修改。 -`xidio.Core` 主要负责诊断、修复和上报。 +## 修改原则 -### 诊断 / Diagnostics +- 保持修改最小化,避免与任务无关的重构、格式化或依赖升级。 +- 优先使用 .NET 跨平台 API;平台专用能力必须放入对应的 `xidio.Platform.*` Provider。 +- 不得在 `xidio.Core` 中使用 `Console`、Spectre.Console、Avalonia 或其他 UI API。 +- `xidio.Core` 应输出结构化数据,并明确分离诊断、修复和上报逻辑。 +- 所有 I/O 与长时间操作使用异步 API,接受 `CancellationToken`,必要时报告结构化进度。 +- 探测失败也是诊断数据;保留目标、时间、耗时、错误类型与必要原始信息,不得只返回布尔值。 +- 未获得用户明确同意时,不收集 hosts、代理、VPN、防火墙、安全软件、账号业务状态等敏感信息,不执行修复。 +- MAC、BSSID、账号、在线设备与日志中的个人信息默认脱敏;任何上报都必须 opt-in 且可预览。 -xidio 需要诊断用户设备,确定网络问题的根源。诊断过程分为**信息收集**和**分析**两步。 +## 架构边界 -#### 收集信息 +- `xidio.Core`:诊断、分析、修复、上报的抽象、模型与通用逻辑。 +- `xidio.CLI`:终端交互、进度展示、结果渲染与平台 Provider 组装。 +- `xidio.Platform.Windows`:Windows 系统 API、WLAN、RAS/PPPoE 及其他 Windows 专用收集。 +- `xidio.Platform.macOS`:macOS 系统命令与 API 适配。 +- `xidio.Platform.Linux`:Linux 系统 API,以及 `ip`、`resolvectl`、`nmcli`、`iw` 等工具的可控适配。 -xidio 将从用户及其设备上收集必要信息。以下列出部分需要采集的信息类别。 +新增平台信息时,先在 Core 定义平台无关的模型和接口,再在各 Provider 中实现。单一平台不支持某项能力时,应返回明确的“不支持/不可用”结果,不得伪造数据或导致整份报告失败。 -##### 用户场景信息 +## 验证 -xidio 会主动询问: +- 根据修改范围运行最小相关构建或测试,再考虑完整解决方案构建。 +- Windows:`dotnet build xidio/xidio.slnx -c Release`。 +- Linux/macOS:`dotnet build xidio/xidio.CLI/xidio.CLI.csproj -c Release -f net10.0`。 +- 平台专用修改必须说明实际验证平台;不得把交叉编译当作真实系统行为验证。 +- 对外部命令的解析应覆盖空输出、命令不存在、非零退出码、本地化输出和权限不足。 -1. 用户所在位置。 -2. 当前连接方式。连接方式划分为: - - **直接连接**:用户直接通过 Wi-Fi 连接校内任意 AP,或通过网口以 PPPoE 方式连接。 - - **间接连接**:用户通过路由器等方式接入,数据经校园网设施传输。 - - **其他连接**:用户通过手机热点等方式连接,数据未经过校园网设施。 -3. 遇到的问题。例如:无法连接 Wi-Fi;已连接但显示“无 Internet”;未跳转到 Portal;Portal 认证成功但无法打开网页;部分应用不能使用;拨号失败;拨号成功但无 Internet 等。 -4. 问题波及范围。例如:仅本机有问题;同宿舍其他人也有问题;同楼层其他人也有问题;不清楚。 +## Git 与文档 -这些信息有助于网管会判断是否为区域性问题。 - -##### 系统与网卡基础信息 - -xidio 需要收集: - -1. 操作系统信息:操作系统类型(Windows、Linux 或 macOS)、版本号;xidio 版本号;xidio 是否以管理员/root 权限运行。 -2. 时间:本机时间、某 NTP 服务器时间,以及两者的差值。 -3. 网络接口:所有网络接口及主网络接口(物理接口)的接口名称、类型、是否启用、是否已连接、(若有)MAC 地址、链路速度、MTU、接口 metric 等,以及当前默认路由所使用的具体网卡。 - -如果用户使用 Wi-Fi 连接,还需收集:当前 Wi-Fi 的 SSID、BSSID、RSSI、频段、信道、PHY 类型、认证/加密类型、连接时长、当前连接速率,以及可见的 SSID 列表。 - -如果用户使用 PPPoE 连接,还需收集:网线是否插好;对端是否有链路;链路速度;是否存在 PPPoE/宽带连接,若存在,则获取 PPPoE 的当前状态、分配到的 IP、DNS、默认路由。在可行的情况下,可尝试进行一次 RAS 拨号并记录 RAS 错误码;(在 Windows 上)还要检查宽带连接名称是否含有非 ASCII 字符,以及用户是否安装了 Npcap 或 WinPcap。 - -##### IP、DHCP、DNS、路由、ARP 信息 - -如果用户已连接到至少一个网络,xidio 将收集:IPv4 地址、IPv6 地址、子网掩码/前缀长度、是否通过 DHCP 获取地址(若是,则收集 DHCP 服务器地址、租约开始和过期时间)、默认网关、DNS 服务器、路由表、ARP/IPv6 Neighbor 表。 - -##### 本机因素 - -在征得用户同意后,xidio 可以进一步收集: - -1. 系统代理设置:包括 Windows Internet Options 代理、WinHTTP 代理、系统代理、环境变量 `HTTP_PROXY`/`HTTPS_PROXY`、macOS 网络代理等。 -2. VPN/TUN/TAP 相关状态:如 Clash TUN、v2rayN、WireGuard、OpenVPN、ZeroTier、Tailscale、WARP、VMware/VirtualBox 虚拟网卡、WSL 虚拟网卡等。 -3. DNS-over-HTTPS 与 Secure DNS 状态。 -4. hosts 文件内容。 -5. 防火墙状态。 -6. 安全软件及过滤型驱动。 -7. 浏览器代理设置。 - -##### 认证与业务状态 - -在征得用户同意后,xidio 可检查:认证服务器是否可达;当前用户是否已通过校园网认证;当前账号是否在线;当前设备是否出现在在线列表中;套餐是否生效;用户所属运营商/套餐类型;宽带优先级;是否欠费;是否达到设备数量上限。 - -##### 主动探测信息 - -我们选定以下目标进行主动探测: - -1. 校园内目标:认证服务器域名和 IP、校内若干服务的 HTTP/HTTPS 域名。 -2. 校园外目标:xidio 官方域名、HTTP 204 端点、HTTPS 固定响应体端点、TCP Echo 或 Connect 端点、UDP Echo/Jitter 端点。 - -执行探测或收集:ARP 默认网关信息;ICMP Ping 情况;到 80、443、53(TCP)的 TCP Connect 情况;UDP DNS 查询;UDP Echo/Jitter/丢包测试;HTTP GET 情况;HTTPS 握手与证书校验;DNS 解析情况(包括使用系统解析器和直接向当前 DNS 服务器查询,分别测试 A 和 AAAA 记录)。更进一步,还可进行 Traceroute/MTR、MTU/PMTUD 测试。 - -#### 分析 - -我们按以下顺序逐层排查问题。每次检测需覆盖所有层,且每层不能仅返回“是/否”,必须附带完整的探测信息。 - -```text -本机网卡 -> 有线/Wi-Fi 情况 -> IP 获取 -> 默认网关可达 -> 认证服务可达 -> 认证服务正常 -> DNS 正常 -> 探测目标可达 -> 具体协议 -``` - -下面给出典型的判断流程,xidio 默认按照该顺序排查,后续也支持用户自定义判断规则。 - -##### Wi-Fi 场景 - -###### 无法连接 Wi-Fi - -适用于设备附近扫不到校园 SSID、信号极弱,或认证失败的情形。SSID 不可见可能是 AP 覆盖不足或无线网卡问题;SSID 可见且信号不弱但无法连接,可能是配置文件损坏、认证方式错误或 AP 故障。xidio 可建议用户忘记网络后重新连接、移动到 AP 附近等。 - -###### Wi-Fi 连接成功但未获取到 IP - -适用于已关联 SSID/BSSID,但 IPv4 地址为 `169.254.x.x` 或为空,DHCP 服务器为空或请求超时等情形。若连接其他 SSID 也失败,则可能是本机 DHCP 服务、防火墙或驱动问题;若同一 AP 下仅个别用户失败,可能是本机网络栈或网卡驱动问题;若多个用户同时失败,则可能是 AP/接入交换机/VLAN/DHCP 中继问题。 - -###### 获取到 IP 但没有默认网关 - -适用于 DHCP 成功分配 IP,但默认网关为空,或路由表中无 `0.0.0.0/0` 的情形。可能由 DHCP 配置异常,或用户自行配置了错误的网络参数导致。 - -###### 有默认网关但不可达 - -适用于有默认网关,但 ARP 表中无对应 MAC 地址,且 Ping/TCP 探测网关失败、校园认证 IP 也不可达等情况。可能是 AP/交换机/网关/VLAN 异常。 - -###### 网关可达但认证页面打不开 - -如果解析 Portal 域名失败但 IP 可达,则可能是 DNS 故障,此时可检查 DNS 和 DoH/公共 DNS 状况。 - -如果 Portal 的域名和 IP 均不可达,可能是校内认证服务存在故障。 - -###### 认证成功但无法上网 - -按层次分析:如果能访问公网 IP 但无法解析域名,则问题可能在 DNS。若 DNS 正常但 TCP 80/443 不通,可能是出口路由、防火墙、代理或 VPN 问题。若 TCP 80/443 正常但浏览器无法打开网页,可能是系统代理、浏览器代理、证书、系统时间或插件问题。若 TCP 正常但 UDP 丢包严重,可能与 UDP/QoS/运营商出口/CDN/上游拥塞有关。若 IPv4 正常但 IPv6 异常(或反之),则可能是双栈配置问题,浏览器 Happy Eyeballs 机制或地址优先级导致体验异常。 - -##### PPPoE 场景 - -###### 以太网无链路 - -适用于以太网未连接、无 carrier 或链路速度为 0 的情形。通常由网线未连接或连接错误导致。 - -###### 有链路但未拨号 - -适用于以太网处于 `up` 状态,但没有 PPP 接口和 PPPoE 默认路由的情形,一般是因为尚未拨号。 - -###### PPPoE 拨号失败 - -若有拨号错误码,可对照错误码进行诊断。如果接口有链路但 PPPoE Discovery 无任何响应,则优先考虑接入侧问题。 - -###### PPPoE 拨号成功但无法联网 - -需检查 PPP 接口是否获取到 IP、DNS,默认路由是否经过 PPP 接口,以及是否开启了系统代理等。 - -##### 其他连接场景 - -对于其他连接方式,xidio 不提供专门的分析帮助,因为这些情况通常与校园网无关。 - -#### 其他问题 - -##### Captive Portal Detection 失效 - -若未自动弹出认证页面,但可手动访问 Portal,则只是 Captive Portal 探测机制失效;若域名访问失败而 IP 访问成功,则为 DNS 问题;若两者均失败,则通常是认证服务器路径或网关问题。 - -##### Windows 侧的“无 Internet”连接 - -Windows 显示的“无 Internet”主要依赖 NCSI 探测结果。xidio 不应仅以此为依据,应手动测试默认网关、Portal、DNS、HTTP 204、HTTPS 以及主动探测目标等情况,以判断是否真正无 Internet 连接。 - -##### 仅部分应用无法访问互联网 - -适用于能上 QQ 但不能打开网页等场景。此类问题成因复杂,xidio 可进行基本探测,如基础 HTTP/HTTPS 连通性、DNS 解析、TCP 443、UDP 443/QUIC 等。 +- 遵循 Git Flow:功能与普通修复从 `develop` 分支开始,已发布版本的紧急修复从 `main` 建立 `hotfix/*`。 +- 提交信息遵循 Conventional Commits,推荐使用精确 scope,例如 `feat(linux): collect wireless details`。 +- 不要重写、压缩或删除用户现有提交,除非用户明确要求。 +- 功能范围或计划变化时更新 `ROADMAP.md`;用户可见的使用、构建或平台状态变化时同步更新 `README.md` 和 `CHANGELOG.md`。 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e7616ad --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +本项目的所有重要变更都将记录在此文件中。 + +本文档格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),项目遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。 + +## [0.1.0] - 2026-05-22 + +### Added + +- 增加交互式 CLI,用于收集用户场景并展示网络诊断报告。 +- 增加 `xidio.Core` 诊断抽象、结构化模型、进度报告和可取消的信息收集流程。 +- 增加 Windows 平台网络诊断 Provider,覆盖网络接口、地址、网关、DHCP、DNS、metric、路由和 Wi-Fi 信息。 +- 增加 macOS 平台网络诊断 Provider 的初始实现。 +- 增加项目 Logo、图标与 Figma 设计源文件。 +- 增加 CI、标签发布工作流与 Nerdbank.GitVersioning 版本管理。 + +[0.1.0]: https://github.com/LyCecilion/xidio/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a0e6d28 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing to xidio + +感谢你愿意帮助 xidio 更准确地诊断西电校园网问题。我们接受 Issue 和 Pull Request;不论是问题复现、平台适配、诊断规则还是文档改进,都欢迎参与。 + +## 提交 Issue + +在新建 Issue 前,请先搜索是否已有相同问题。报告 Bug 时建议包含: + +- xidio 版本、操作系统与系统版本; +- 连接方式与可稳定复现的步骤; +- 期望结果、实际结果与必要日志; +- 是否以管理员或 root 权限运行。 + +发布日志、截图或诊断报告前,请删除账号、密码、完整 MAC/BSSID、IP 等不必要的个人或网络标识信息。 + +## 提交 Pull Request + +1. 从最新的 `develop` 分支创建短期分支:新功能使用 `feature/`,普通修复使用 `fix/`。仅针对已发布版本的紧急修复从 `main` 创建 `hotfix/`。 +2. 保持修改最小化,不要在同一 PR 中混入无关重构。 +3. 提交信息遵循 [Conventional Commits](https://www.conventionalcommits.org/),例如 `feat(linux): collect default route` 或 `fix(core): preserve probe cancellation`。 +4. 根据修改所在平台运行构建和相关测试,并在 PR 中说明未能验证的平台。 +5. 默认将 PR 提交到 `develop`,在描述中关联 Issue,并说明用户可见变化、验证方法与隐私影响。 + +## 开发约定 + +- 先阅读 [`README.md`](./README.md)、[`AGENTS.md`](./AGENTS.md) 与 [`docs/architecture.md`](./docs/architecture.md)。 +- `xidio.Core` 不得依赖 CLI/GUI,应输出结构化数据,并为长任务提供异步、进度与取消支持。 +- 平台专用能力放入相应的 `xidio.Platform.*` 项目,通过 Core 中的抽象调用。 +- 收集敏感信息或执行修复前必须获得用户明确同意;上报始终保持 opt-in。 + +## 构建 + +Windows: + +```powershell +dotnet restore xidio/xidio.slnx +dotnet build xidio/xidio.slnx -c Release --no-restore +``` + +Linux 或 macOS: + +```bash +dotnet restore xidio/xidio.CLI/xidio.CLI.csproj +dotnet build xidio/xidio.CLI/xidio.CLI.csproj -c Release -f net10.0 --no-restore +``` + +更多开发环境信息见 [`README.md`](./README.md#-development)。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c17d067 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Project Hazelita + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 54877af..af48ab2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - +
@@ -17,6 +17,109 @@ _✨**Xidian Internet Diagnostic Intelligence Operator**✨_\
-## 简介 +## 📖 About -xidio _(XiDian Internet Diagnostic Intelligence Operator)_ 是一款基于 C# 和 .NET 架构的、由 Project Hazelita 开发的、跨平台的适用于西安电子科技大学的校园网诊断工具。xidio 以 [NanCunChild](https://github.com/NanCunChild) 的项目 [Marduk](https://github.com/NanCunChild/Marduk) 为灵感来源,同时具有 CLI 和 GUI 两种用户界面,致力于成为一款简单易用且足够强大的辅助工具。 +xidio _(Xidian Internet Diagnostic Intelligence Operator)_ 是一款由 Project Hazelita 开发的西安电子科技大学校园网诊断工具。项目基于 C# 和 .NET 10,面向 Windows、Linux 与 macOS,希望将分散的网卡、IP、DHCP、DNS、路由、Wi-Fi 及主动探测信息整理为可理解、可上报的诊断结果。 + +xidio 的灵感来自 [NanCunChild](https://github.com/NanCunChild) 的 [Marduk](https://github.com/NanCunChild/Marduk)。项目当前处于早期开发阶段:v0.1.0 已提供 CLI 与以 Windows 为主的信息收集能力,其他平台与完整分析、修复、上报功能仍在实现中。 + +## ✨ Features + +- **交互式场景问询**:通过 CLI 收集位置、连接方式、问题现象与影响范围,为后续判断补充用户上下文。 +- **分层网络信息收集**:收集操作系统、时间、网络接口、IP、DHCP、DNS、默认路由、邻居表和主动探测结果。 +- **Windows 深度适配**:利用 Windows 系统能力获取网卡、Wi-Fi、路由、代理与 PPPoE 等诊断信息。 +- **跨平台架构**:通用诊断逻辑位于 `xidio.Core`,平台差异由独立 Provider 隔离;当前已有 Windows 和 macOS Provider,Linux Provider 正在路线图中。 +- **结构化与可取消**:核心诊断输出结构化数据,长时间任务支持进度报告和 `CancellationToken`。 + +## 🚀 Quick Start + +当前版本需要 [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)。从源码运行 CLI: + +```bash +git clone https://github.com/LyCecilion/xidio.git +cd xidio +dotnet restore xidio/xidio.CLI/xidio.CLI.csproj +dotnet run --project xidio/xidio.CLI/xidio.CLI.csproj -f net10.0 +``` + +Windows 上需要调用 Windows 平台 Provider 时,请将最后的目标框架改为 `net10.0-windows`。部分系统信息需要管理员或 root 权限;仅在信任项目源码且确有需要时提权运行。 + +## 📁 Project Structure + +```text +xidio/ +├── assets/ # Logo、图标与设计源文件 +├── docs/ # 架构与核心开发准则 +├── xidio/ +│ ├── xidio.CLI/ # 命令行入口与交互式报告 +│ ├── xidio.Core/ # 诊断模型、收集、分析与抽象 +│ ├── xidio.Platform.Windows/ # Windows 平台 Provider +│ ├── xidio.Platform.macOS/ # macOS 平台 Provider +│ └── xidio.slnx # .NET 解决方案 +├── AGENTS.md # 仓库内 AI Agent 协作约定 +├── CONTRIBUTING.md # 贡献指南 +├── ROADMAP.md # 开发路线与诊断范围 +└── flake.nix # 可选的 Nix 开发环境 +``` + +## 💻 Development + +通用要求: + +- 使用 `global.json` 声明的 .NET 10 SDK;代码风格由 `Directory.Build.props` 中的 .NET Analyzer 规则约束。 +- 修改前阅读 [`AGENTS.md`](./AGENTS.md)、[`docs/architecture.md`](./docs/architecture.md) 和相关模块文档。 +- 分支遵循 Git Flow,提交信息遵循 [Conventional Commits](https://www.conventionalcommits.org/)。 + +### Windows + +安装 .NET 10 SDK,使用 PowerShell 运行: + +```powershell +dotnet restore xidio/xidio.slnx +dotnet build xidio/xidio.slnx -c Debug +dotnet run --project xidio/xidio.CLI/xidio.CLI.csproj -f net10.0-windows +``` + +### Linux + +可以从发行版安装 .NET 10 SDK,也可使用 Nix。原生 SDK 环境下: + +```bash +dotnet restore xidio/xidio.CLI/xidio.CLI.csproj +dotnet build xidio/xidio.CLI/xidio.CLI.csproj -c Debug -f net10.0 +``` + +使用 Nix 时,无需全局安装 .NET SDK: + +```bash +nix develop +dotnet build xidio/xidio.CLI/xidio.CLI.csproj -c Debug -f net10.0 +``` + +### macOS + +安装 .NET 10 SDK 后运行: + +```bash +dotnet restore xidio/xidio.CLI/xidio.CLI.csproj +dotnet build xidio/xidio.CLI/xidio.CLI.csproj -c Debug -f net10.0 +dotnet run --project xidio/xidio.CLI/xidio.CLI.csproj -f net10.0 +``` + +Apple Silicon 与 Intel Mac 也可使用已启用 `aarch64-darwin` 和 `x86_64-darwin` 的 `nix develop`。 + +## 🗺️ Roadmap + +详细的诊断范围、分析顺序与分阶段开发计划见 [`ROADMAP.md`](./ROADMAP.md)。 + +## 📰 Changelog + +版本变更记录见 [`CHANGELOG.md`](./CHANGELOG.md),文件遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 规范。 + +## 🤝 Contributing + +我们欢迎通过 Issue 报告问题或提出建议,也接受 Pull Request。提交前请阅读 [`CONTRIBUTING.md`](./CONTRIBUTING.md)。 + +## 📜 License + +xidio 以 [MIT License](./LICENSE) 开源。 diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..ab65422 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,155 @@ +# xidio Roadmap + +本文档定义 xidio 的开发路线、诊断范围与分析顺序。其中的未勾选项为计划,不代表当前版本已具备对应能力。 + +## 总体目标 + +xidio 需要在 Windows、Linux 和 macOS 上完成“信息收集 → 分层分析 → 可选修复 → 用户同意后上报”的完整流程。每层诊断必须附带完整探测信息,不得只返回“是/否”。 + +```text +本机网卡 -> 有线/Wi-Fi 情况 -> IP 获取 -> 默认网关可达 +-> 认证服务可达 -> 认证服务正常 -> DNS 正常 +-> 探测目标可达 -> 具体协议 +``` + +## 阶段计划 + +### 0.1 基础信息收集 + +- [x] 建立 `xidio.Core`、CLI 和平台 Provider 抽象。 +- [x] 增加用户场景问询与结构化诊断报告。 +- [x] 实现以 Windows 为主的系统、网卡、IP、路由、Wi-Fi 与主动探测信息收集。 +- [x] 提供 macOS Provider 的初始实现。 + +### 0.2 跨平台收集补全 + +- [ ] 建立并接入 `xidio.Platform.Linux`。 +- [ ] 对齐 Windows、Linux 和 macOS 的基础系统、网卡、路由、DNS、邻居表、代理与 Wi-Fi 收集能力。 +- [ ] 补全 PPPoE 链路、会话、错误码与路由检查。 +- [ ] 为外部命令解析与平台特性增加测试。 + +### 0.3 分析引擎与扩展探测 + +- [ ] 实现本文档定义的 Wi-Fi、PPPoE、Captive Portal、NCSI 和部分应用故障判断流程。 +- [ ] 实现可配置、可测试的判断规则,并保留每个结论的证据链。 +- [ ] 补全直接 DNS、UDP Echo/Jitter/丢包、Traceroute/MTR 与 MTU/PMTUD 探测。 +- [ ] 区分 IPv4/IPv6、TCP/UDP 和 HTTP/HTTPS/QUIC 问题。 + +### 0.4 修复与上报 + +- [ ] 按 Drip、Drift、Current、Surge 等级实现权限、确认、回滚与审计日志。 +- [ ] 实现不同平台的基础修复 Provider,优先支持可回滚操作。 +- [ ] 实现崩溃、匿名使用情况与校园网情况三类上报;所有上报保持 opt-in。 +- [ ] 允许用户在上报前预览、脱敏和删除字段。 + +### 1.0 用户体验与稳定发布 + +- [ ] 提供稳定的 CLI 命令与可导出报告格式。 +- [ ] 完成基于 Avalonia 的跨平台 GUI。 +- [ ] 完成三个目标平台的 CI、发布包、安装指南与真实校园网场景验证。 +- [ ] 完成隐私说明、敏感字段盘点与上报数据保留策略。 + +## 信息收集范围 + +### 用户场景 + +xidio 应主动询问: + +1. 用户所在校区、建筑类型,以及可选的楼栋和楼层。 +2. 当前连接方式: + - **直接连接**:直接通过 Wi-Fi 连接校内 AP,或通过网口以 PPPoE 方式连接。 + - **间接连接**:通过路由器等设备接入,数据仍经校园网设施传输。 + - **其他连接**:通过手机热点等方式连接,数据未经校园网设施。 +3. 问题现象:无法连接 Wi-Fi、已连接但显示无 Internet、未跳转 Portal、认证后无法打开网页、仅部分应用不可用、拨号失败、拨号后无 Internet 等。 +4. 影响范围:仅本机、同宿舍、同楼层或不清楚。 + +对于其他连接方式,xidio 仅提供通用信息,不应将其问题归因于校园网。 + +### 系统、时间与网卡 + +- 操作系统类型与版本、xidio 版本、是否以管理员/root 运行。 +- 本机时间、NTP 或可靠网络时间、两者差值。 +- 所有接口及主物理接口的名称、类型、启用/连接状态、脱敏 MAC、链路速率、MTU、metric、收发包、错误包和丢弃包统计。 +- 默认路由使用的具体网卡,以及访问校内/校外目标时实际命中的路由。 + +Wi-Fi 场景还需收集: + +- 关联状态、SSID、脱敏 BSSID、RSSI/信号质量、频段、信道、PHY 类型和认证/加密类型。 +- 连接时长、当前速率、可见 SSID 列表、是否为已知校园 AP,以及可获得时的最近连接失败原因码。 + +PPPoE 场景还需收集: + +- 网线/carrier 状态、对端链路和链路速率。 +- PPPoE/宽带连接是否存在,以及未拨号、连接中、已连接、已断开等状态。 +- PPP 接口获取的 IP、DNS 和默认路由。 +- 可行时进行一次受控 RAS 拨号并记录错误码,包括 691、651、678/718 和 720 等常见情况。 +- Windows 宽带连接名称是否包含非 ASCII 字符,以及是否安装 Npcap/WinPcap。 + +### IP、DHCP、DNS、路由与邻居表 + +当至少连接一个网络时,收集: + +- IPv4/IPv6 地址、子网掩码/前缀长度,以及是否存在 `169.254.x.x` 或可疑静态地址。 +- DHCP 获取状态、服务器地址、租约开始与过期时间。 +- 默认网关、DNS 服务器、路由表、ARP/IPv6 Neighbor 表。 +- 多默认路由及其 metric,以及实际出站选路。 + +### 本机干扰因素 + +以下收集必须先征得用户同意: + +- Windows Internet Options、WinHTTP、系统、环境变量 `HTTP_PROXY`/`HTTPS_PROXY` 与 macOS 网络代理。 +- Clash TUN、v2rayN、WireGuard、OpenVPN、ZeroTier、Tailscale、WARP、VMware/VirtualBox、WSL 等 VPN/TUN/TAP/虚拟网卡状态。 +- DNS-over-HTTPS/Secure DNS、hosts 文件、防火墙、常见安全软件/过滤驱动与浏览器代理设置。 + +### 认证与业务状态 + +征得用户同意后,可检查: + +- 认证服务器可达性与当前认证状态。 +- 账号在线状态、当前设备是否在在线列表、最近登录错误码与强制下线情况。 +- 套餐是否生效、运营商/套餐类型、宽带/出口优先级、欠费与设备数上限。 +- 是否存在异常在线设备。 + +### 主动探测 + +目标包含校内认证服务域名/IP、若干校内 HTTP/HTTPS 服务,以及校外 xidio 官方域名、HTTP 204、HTTPS 固定响应体、TCP Connect/Echo 和 UDP Echo/Jitter 端点。按需执行: + +- ARP 默认网关检查、ICMP Ping,但不得仅根据 ICMP 失败判定网络不可达。 +- 到 80、443、53/TCP 的 TCP Connect,以及 UDP DNS 查询。 +- UDP Echo/Jitter/丢包测试、HTTP GET(可选不跟随重定向)、HTTPS 握手与证书校验。 +- 系统解析器与直接查询当前 DNS 服务器的 A/AAAA 记录对比。 +- 深度模式下的 Traceroute/MTR 与 MTU/PMTUD 测试,尤其关注 PPPoE。 + +## 典型分析流程 + +### Wi-Fi + +1. **无法连接 Wi-Fi**:SSID 不可见可能是 AP 覆盖或网卡问题;SSID 可见且信号不弱但连接失败,检查配置文件、认证方式与 AP 状态。可建议忘记网络后重连或移近 AP。 +2. **已关联但未获取 IP**:检查空地址、`169.254.x.x`、DHCP 服务器缺失或请求超时。其他 SSID 也失败时优先本机 DHCP/防火墙/驱动;单 AP 下多人失败时优先 AP、交换机、VLAN 或 DHCP 中继。 +3. **已获取 IP 但无默认网关**:检查 DHCP 配置和用户手动配置,确认路由表是否存在 `0.0.0.0/0`。 +4. **默认网关不可达**:结合 ARP、Ping/TCP 网关探测和校园认证 IP 可达性,判断 AP/交换机/网关/VLAN 异常。 +5. **网关可达但 Portal 不可用**:Portal 域名失败而 IP 成功时检查 DNS、DoH 和公共 DNS;域名与 IP 都失败时检查校内认证服务。 +6. **认证成功但无法上网**:依次比较公网 IP、DNS、TCP 80/443、浏览器、代理/VPN、证书/系统时间、UDP 质量与 IPv4/IPv6,考虑 Happy Eyeballs 和地址优先级影响。 + +### PPPoE + +1. **以太网无链路**:检查接口连接、carrier 与链路速率,优先提示网线未接或接错。 +2. **有链路但未拨号**:以太网接口 `up` 但不存在 PPP 接口或 PPPoE 默认路由时,提示用户拨号。 +3. **拨号失败**:优先解释 RAS 错误码;有链路但 PPPoE Discovery 无响应时,优先考虑接入侧问题。 +4. **拨号成功但无法上网**:检查 PPP 接口的 IP、DNS、默认路由和系统代理。 + +### 其他典型问题 + +- **Captive Portal Detection 失效**:可手动打开 Portal 时仅判定为自动探测失效;域名失败而 IP 成功时判定 DNS 问题;两者都失败时检查认证服务器路径或网关。 +- **Windows “无 Internet”**:不得仅依赖 NCSI,应独立测试默认网关、Portal、DNS、HTTP 204、HTTPS 和主动探测目标。 +- **仅部分应用无法联网**:比较 HTTP/HTTPS、DNS、TCP 443、UDP 443/QUIC、代理、VPN 和双栈表现,不根据单一应用直接归因。 + +## 修复安全等级 + +- **Drip**:完全只读,仅收集和分析。 +- **Drift**:可回滚且风险较低的修复,如关闭已确认失效的代理、刷新 DNS 缓存、重新获取 DHCP 租约。 +- **Current**:可回滚但会改变网络配置的修复,如修改 DNS、调整 metric、临时禁用冲突虚拟网卡或在有充分证据时临时禁用 IPv6。 +- **Surge**:侵入式修复,如重置 Winsock/IP 协议栈、Windows 网络重置、驱动回滚/更新/设备删除和电源策略修改。 + +所有修复必须获得用户明确确认并留下日志;除 Surge 外的操作必须可回滚。 From afe3761ea8cde35d02e462c5faee9fa38de036a1 Mon Sep 17 00:00:00 2001 From: LyCecilion Date: Sat, 11 Jul 2026 10:43:00 +0800 Subject: [PATCH 2/8] build(nix): add dotnet development shell --- flake.nix | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 flake.nix diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..3557606 --- /dev/null +++ b/flake.nix @@ -0,0 +1,59 @@ +{ + description = "xidio .NET development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = + { nixpkgs, ... }: + let + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + in + { + devShells = forAllSystems ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + default = pkgs.mkShell { + packages = with pkgs; [ + dotnet-sdk_10 + git + pkg-config + ] + ++ lib.optionals stdenv.isLinux [ + clang + lld + ]; + + buildInputs = with pkgs; [ + icu + openssl + zlib + ]; + + DOTNET_CLI_TELEMETRY_OPTOUT = "1"; + DOTNET_NOLOGO = "1"; + + LD_LIBRARY_PATH = pkgs.lib.optionalString pkgs.stdenv.isLinux ( + pkgs.lib.makeLibraryPath [ + pkgs.icu + pkgs.openssl + pkgs.stdenv.cc.cc.lib + pkgs.zlib + ] + ); + }; + } + ); + }; +} From bfb6e45f665b445f1a13ac2fe063efba260cef36 Mon Sep 17 00:00:00 2001 From: LyCecilion Date: Sat, 11 Jul 2026 10:50:39 +0800 Subject: [PATCH 3/8] build(nix): add direnv integration --- .envrc | 1 + .gitignore | 22 +++++++++++++++++++--- flake.lock | 27 +++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 .envrc create mode 100644 flake.lock diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index eb0cf13..7279f78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,visualstudio,rider,csharp -# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,visualstudio,rider,csharp +# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,direnv,visualstudio,visualstudiocode,rider,csharp +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,direnv,visualstudio,visualstudiocode,rider,csharp ### Csharp ### ## Ignore Visual Studio temporary files, build results, and @@ -399,6 +399,9 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +### direnv ### +/.direnv/ + ### Linux ### # temporary files which can be created if a process still has a handle open of a deleted file @@ -525,6 +528,19 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser +### VisualStudioCode ### +!.vscode/*.code-snippets + +# Local History for Visual Studio Code + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + ### Windows ### # Windows thumbnail cache files Thumbs.db @@ -727,6 +743,6 @@ $RECYCLE.BIN/ ### VisualStudio Patch ### # Additional files built by Visual Studio -# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,visualstudio,rider,csharp +# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,direnv,visualstudio,visualstudiocode,rider,csharp *.lscache diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..fe90317 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1783522502, + "narHash": "sha256-iffAls3iaNTyJC2faYcUXSI+Gp02cDjYl+MygxKl2GI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0bb7ec54c8483066ec9d7720e780a5caa71f8612", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} From 19f751698cf15102b5194242536cfbec71bf7510 Mon Sep 17 00:00:00 2001 From: LyCecilion Date: Sat, 11 Jul 2026 10:50:39 +0800 Subject: [PATCH 4/8] chore(assets): add raster logo assets --- assets/xidio_icon.png | Bin 0 -> 15836 bytes assets/xidio_logo.png | Bin 0 -> 104214 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/xidio_icon.png create mode 100644 assets/xidio_logo.png diff --git a/assets/xidio_icon.png b/assets/xidio_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..162440debc8ec3635939f833e96f4d8f2ad4d028 GIT binary patch literal 15836 zcmeIZj7O@$m4^|NGs) zhZ`B2ho|D7Sd*CjsD&fTN!>t5m-~l%j%vuK zlqdPzo}b1)$-R(ragjLvyy>#Z7ude2#`RIypq-GTQ)uFsB%hnqtX$h8skX%@p`RM> zE#m#)L4G#0RjB1LC}p%?TYRf+-B%dlENvwd>+~uNh3#iA`stfVvs4X&WBRaBsI@{S zJUp7-UOc?mKeR|ZJU-P@LcCJEI4ZnY0wQ@lJTjX9JNW;pLEN6eei}1yODKFVD1R&5 zJrJ~e@{0WDyN5OUJ|)@TWao+SGL`etLHrQsi;;2+q$Z^$$98Wj-7|E%GHyo9fpeZ+9X*n3Ai?UJi0&^99hhF#%<6)YD$)NewEBi{AcDZE$u3ly;c&y`(?D zgAt$a7asg0#>S2v;Zy=`0~p)^aH2iaC0+94ed?#jR`VtMHLyB(5GY`zE!X%V!wNKw z^EIBQxXO25M5LuMH>-<)dH9fsnW|2yweuw?fK~oF?goqHC)RI9*6nRl$@d#_!E zd5`NCB9U88RLe82>1#1ms@~8UB@JG){2F-prq2n-Ux#7If$ z;@y99ELoUnYj-o*pXfts4rU1NsXeQMK%u`Z_nJx`;C)T!abZJ`DkfrHgDt~L8NzFE ze_!xWe!xxZE7a$5L~LJ`%#0jXTXYQkRy&xaHZIjJDY3cuf(l4LTJ zB}2qlc*J@xHK^nK{%ne>h~Sl4w&uy@hJ8t7Le3jQ2XtY{9q6j?y;8hbnSQ^vf4X9r zf9dF5FH*y|4BeDr&G7G9TLFMpTu#*G*}f7~JsI&OCi<}2I-af|eY<>9G*E#CuC z*|oeh8|+qn&m;7WxaX=tUyF^Nryd-rS?$W7aa$V37Ae4x1H!$R-X_46uvyi;EQ^?a2dG#UxZ zy{k!WY0MtjuNiyOll}Ix;Vz2%YP;4=U@AGo{Xjn+dckFcS1~ zciTymUM(u}ylJ97V&)M^yq4>5Ici*4avEXM0QjQfDS|~=(3fxbA6~w7$|)qh5W500 za%~bQlKRCYbiq|YSDl9h>#Qvy`0P$31=7mnCf-y*Q_Fq-{=gq*;SmFndPCW5clm2Y z<$2^6=?UR1*E@fHud*?1MC*KxFJ-uOgPNs8piXaGp1^N$5xj(pjuA)~!QanERxPEwb`RmY#5(@l>*DrB&We7{KQAx>AP*$%RII)dBM z2U~;+1m&laz{@wx)ZZEmh>VtGt}X^^1zYb$wLrhFCTdrM7MF)>$`)2B^C(KM=et6xT9VlDBm}@rF^ok1+M;x=sP;_m)P9<&t%k(bS?I6M~n}#n%z+ z$DbxqD{yXbn-W+iGOE!x<^1}(B}ZZZjeTu==bpI77l(4el8P*2k&!P%z@5R~gPR8` zyE$Ui7To$gPsWS75n?sV6;mwCXH4a#4L(7ZlEL^TQE2HQ2P$6s)1(=!FxK=DqIi@H zLl?8*pivY|X?anneWxBAV;nGa%%Eof(3`iRWu*%0zJ1Cz@p%u+q7^EU5!6K1+q>mG zb#DDC4P(Hj=p-^*<s>0Z@GCxOHq1(!WLRFu; zY~!J@MVD3e^h#{MS>Cvz)IZ7v_tL*C>V3c?g*)WiItP?)nvfzOG+mJJiUE88)H`~`kwMMi z`a~Y&;yXZI|I_W|Fb2WJD>1jj_S*bH>=ZMlY}eib==Ae-YAe@aXbh5XcL5%Y&4um% zX4?Azu~zf^Wz&Ez4x4HwueF~X(-rXduASGcm_o%g3NVMl|Dis5nA$+cqJtUVgmCng$0%h)z|~%Ck5q#1V`j&J&t3X2 zAx~AN1MW5)Muzn+oO%&wL9(iHZSFB$I~1O|?AW9jH>t-3w)wdZQWi1X5lv_Ap)E

V}_bn-C`B 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 literal 0 HcmV?d00001 diff --git a/assets/xidio_logo.png b/assets/xidio_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..21605d0c3d93e193bca22effa8036be662269875 GIT binary patch literal 104214 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 v_2)0tV0~t?>i;t4|35GmQvJU Date: Sat, 11 Jul 2026 11:01:40 +0800 Subject: [PATCH 5/8] fix(privacy): require consent for proxy collection --- xidio/xidio.CLI/ConsoleDiagnosticQuestionnaire.cs | 12 +++++++++++- xidio/xidio.CLI/ConsoleDiagnosticReporter.cs | 4 ++++ .../Diagnostics/NetworkDiagnosticsCollector.cs | 11 +++++++++-- xidio/xidio.Core/Models/SystemProxyInfo.cs | 2 ++ xidio/xidio.Core/Models/UserScenarioInfo.cs | 2 ++ 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/xidio/xidio.CLI/ConsoleDiagnosticQuestionnaire.cs b/xidio/xidio.CLI/ConsoleDiagnosticQuestionnaire.cs index 5d2fc6f..8e53dea 100644 --- a/xidio/xidio.CLI/ConsoleDiagnosticQuestionnaire.cs +++ b/xidio/xidio.CLI/ConsoleDiagnosticQuestionnaire.cs @@ -59,13 +59,23 @@ public static Task AskAsync(CancellationToken cancellationToke new("不清楚", ImpactScope.Unknown) ]); + cancellationToken.ThrowIfCancellationRequested(); + var collectSensitiveSystemInformation = AnsiConsole.Prompt( + new ConfirmationPrompt( + "是否同意 xidio 进一步收集系统代理等敏感系统信息?诊断结果不会自动上报。") + { + DefaultValue = false + }); + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(new UserScenarioInfo { Location = location, ConnectionMethod = connectionMethod, AccessMethod = accessMethod, ProblemSymptom = problemSymptom, - ImpactScope = impactScope + ImpactScope = impactScope, + CollectSensitiveSystemInformation = collectSensitiveSystemInformation }); } diff --git a/xidio/xidio.CLI/ConsoleDiagnosticReporter.cs b/xidio/xidio.CLI/ConsoleDiagnosticReporter.cs index 9a578be..b2e1d60 100644 --- a/xidio/xidio.CLI/ConsoleDiagnosticReporter.cs +++ b/xidio/xidio.CLI/ConsoleDiagnosticReporter.cs @@ -92,6 +92,7 @@ private static void PrintUserScenario(UserScenarioInfo scenario) scenarioTable.AddRow("接入方式", Escape(FormatNetworkAccessMethod(scenario.AccessMethod))); scenarioTable.AddRow("主要问题", Escape(FormatProblemSymptom(scenario.ProblemSymptom))); scenarioTable.AddRow("影响范围", Escape(FormatImpactScope(scenario.ImpactScope, scenario.ProblemSymptom))); + scenarioTable.AddRow("敏感系统信息收集", FormatBool(scenario.CollectSensitiveSystemInformation)); AnsiConsole.Write(scenarioTable); } @@ -549,6 +550,9 @@ private static string FormatAddressFamily(int addressFamily) private static string FormatProxyStatus(SystemProxyInfo systemProxy) { + if (!systemProxy.WasCollected) + return "[grey]未收集(未经授权)[/]"; + return systemProxy.IsEnabled ? $"[yellow]on[/] {Escape(systemProxy.ProxyUri?.ToString() ?? "")}" : "[green]off[/]"; diff --git a/xidio/xidio.Core/Diagnostics/NetworkDiagnosticsCollector.cs b/xidio/xidio.Core/Diagnostics/NetworkDiagnosticsCollector.cs index 52ba9d7..b1f9051 100644 --- a/xidio/xidio.Core/Diagnostics/NetworkDiagnosticsCollector.cs +++ b/xidio/xidio.Core/Diagnostics/NetworkDiagnosticsCollector.cs @@ -95,8 +95,14 @@ public async Task CollectAsync( cancellationToken); ReportProgress(progress, NetworkDiagnosticStage.DefaultRoutes, "已读取默认路由信息", 7); - var systemProxy = SafeGet(GetSystemProxyInfo, new SystemProxyInfo { IsEnabled = false }); - ReportProgress(progress, NetworkDiagnosticStage.SystemProxy, "已读取系统代理状态", 8); + var systemProxy = userScenario.CollectSensitiveSystemInformation + ? SafeGet(GetSystemProxyInfo, new SystemProxyInfo { WasCollected = true, IsEnabled = false }) + : new SystemProxyInfo { WasCollected = false, IsEnabled = false }; + ReportProgress( + progress, + NetworkDiagnosticStage.SystemProxy, + systemProxy.WasCollected ? "已读取系统代理状态" : "已跳过未授权的系统代理收集", + 8); var probeResults = await CollectProbeResultsAsync(primaryInterfaces, progress, cancellationToken); ReportProgress(progress, NetworkDiagnosticStage.ActiveProbes, "已完成主动探测", 9); @@ -819,6 +825,7 @@ private static SystemProxyInfo GetSystemProxyInfo() return new SystemProxyInfo { + WasCollected = true, IsEnabled = isEnabled, ProxyUri = isEnabled ? proxyUri : null }; diff --git a/xidio/xidio.Core/Models/SystemProxyInfo.cs b/xidio/xidio.Core/Models/SystemProxyInfo.cs index e409dea..f311f91 100644 --- a/xidio/xidio.Core/Models/SystemProxyInfo.cs +++ b/xidio/xidio.Core/Models/SystemProxyInfo.cs @@ -2,6 +2,8 @@ namespace xidio.Core.Models; public sealed class SystemProxyInfo { + public bool WasCollected { get; init; } + public required bool IsEnabled { get; init; } public Uri? ProxyUri { get; init; } diff --git a/xidio/xidio.Core/Models/UserScenarioInfo.cs b/xidio/xidio.Core/Models/UserScenarioInfo.cs index 9763bbd..fb05aa0 100644 --- a/xidio/xidio.Core/Models/UserScenarioInfo.cs +++ b/xidio/xidio.Core/Models/UserScenarioInfo.cs @@ -20,4 +20,6 @@ public sealed class UserScenarioInfo public required ProblemSymptom ProblemSymptom { get; init; } public required ImpactScope ImpactScope { get; init; } + + public bool CollectSensitiveSystemInformation { get; init; } } From a1106cf51aac4581ad003a8cf4f5f1b5d5268b5f Mon Sep 17 00:00:00 2001 From: LyCecilion Date: Sat, 11 Jul 2026 11:19:15 +0800 Subject: [PATCH 6/8] feat(linux): add network diagnostics provider --- CHANGELOG.md | 7 + README.md | 7 +- ROADMAP.md | 2 +- docs/architecture.md | 1 + flake.nix | 4 +- xidio/xidio.CLI/Program.cs | 11 +- xidio/xidio.CLI/xidio.CLI.csproj | 14 +- ...LinuxPlatformNetworkDiagnosticsProvider.cs | 813 ++++++++++++++++++ .../xidio.Platform.Linux.csproj | 13 + xidio/xidio.slnx | 1 + 10 files changed, 864 insertions(+), 9 deletions(-) create mode 100644 xidio/xidio.Platform.Linux/LinuxPlatformNetworkDiagnosticsProvider.cs create mode 100644 xidio/xidio.Platform.Linux/xidio.Platform.Linux.csproj diff --git a/CHANGELOG.md b/CHANGELOG.md index e7616ad..68fce62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ 本文档格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),项目遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。 +## [Unreleased] + +### Added + +- 增加 Linux 平台 Provider,支持系统与权限、物理网卡与驱动、Wi-Fi、DHCP、路由、metric、默认路由和邻居表信息收集。 + ## [0.1.0] - 2026-05-22 ### Added @@ -16,3 +22,4 @@ - 增加 CI、标签发布工作流与 Nerdbank.GitVersioning 版本管理。 [0.1.0]: https://github.com/LyCecilion/xidio/releases/tag/v0.1.0 +[Unreleased]: https://github.com/LyCecilion/xidio/compare/v0.1.0...HEAD diff --git a/README.md b/README.md index af48ab2..e97542c 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ xidio 的灵感来自 [NanCunChild](https://github.com/NanCunChild) 的 [Marduk] - **交互式场景问询**:通过 CLI 收集位置、连接方式、问题现象与影响范围,为后续判断补充用户上下文。 - **分层网络信息收集**:收集操作系统、时间、网络接口、IP、DHCP、DNS、默认路由、邻居表和主动探测结果。 - **Windows 深度适配**:利用 Windows 系统能力获取网卡、Wi-Fi、路由、代理与 PPPoE 等诊断信息。 -- **跨平台架构**:通用诊断逻辑位于 `xidio.Core`,平台差异由独立 Provider 隔离;当前已有 Windows 和 macOS Provider,Linux Provider 正在路线图中。 +- **跨平台架构**:通用诊断逻辑位于 `xidio.Core`,平台差异由独立 Provider 隔离;当前已有 Windows、Linux 和 macOS Provider。 - **结构化与可取消**:核心诊断输出结构化数据,长时间任务支持进度报告和 `CancellationToken`。 ## 🚀 Quick Start @@ -53,6 +53,7 @@ xidio/ ├── xidio/ │ ├── xidio.CLI/ # 命令行入口与交互式报告 │ ├── xidio.Core/ # 诊断模型、收集、分析与抽象 +│ ├── xidio.Platform.Linux/ # Linux 平台 Provider │ ├── xidio.Platform.Windows/ # Windows 平台 Provider │ ├── xidio.Platform.macOS/ # macOS 平台 Provider │ └── xidio.slnx # .NET 解决方案 @@ -84,6 +85,8 @@ dotnet run --project xidio/xidio.CLI/xidio.CLI.csproj -f net10.0-windows 可以从发行版安装 .NET 10 SDK,也可使用 Nix。原生 SDK 环境下: +Linux Provider 会按需调用 `ip`、`nmcli` 和 `iw`;建议安装 `iproute2`、NetworkManager 与 `iw`。命令不存在时,xidio 仍会返回 .NET 与 `/sys` 可获取的其他诊断信息。 + ```bash dotnet restore xidio/xidio.CLI/xidio.CLI.csproj dotnet build xidio/xidio.CLI/xidio.CLI.csproj -c Debug -f net10.0 @@ -106,7 +109,7 @@ dotnet build xidio/xidio.CLI/xidio.CLI.csproj -c Debug -f net10.0 dotnet run --project xidio/xidio.CLI/xidio.CLI.csproj -f net10.0 ``` -Apple Silicon 与 Intel Mac 也可使用已启用 `aarch64-darwin` 和 `x86_64-darwin` 的 `nix develop`。 +Apple Silicon Mac 也可使用已启用 `aarch64-darwin` 的 `nix develop`。当前 nixpkgs 已不再支持 `x86_64-darwin`,Intel Mac 请使用原生 .NET 10 SDK。 ## 🗺️ Roadmap diff --git a/ROADMAP.md b/ROADMAP.md index ab65422..24ae0b5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -23,7 +23,7 @@ xidio 需要在 Windows、Linux 和 macOS 上完成“信息收集 → 分层分 ### 0.2 跨平台收集补全 -- [ ] 建立并接入 `xidio.Platform.Linux`。 +- [x] 建立并接入 `xidio.Platform.Linux`,收集系统、物理网卡、Wi-Fi、DHCP、路由、metric 与邻居表信息。 - [ ] 对齐 Windows、Linux 和 macOS 的基础系统、网卡、路由、DNS、邻居表、代理与 Wi-Fi 收集能力。 - [ ] 补全 PPPoE 链路、会话、错误码与路由检查。 - [ ] 为外部命令解析与平台特性增加测试。 diff --git a/docs/architecture.md b/docs/architecture.md index d206507..930c773 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,6 +8,7 @@ - `xidio.Core`:xidio 的核心模块,封装了所有诊断、修复和上报逻辑,供 CLI 与 GUI 调用。 - `xidio.CLI`:xidio 的命令行版本,同时也可作为终端内的交互式工具使用。我们将使用 `System.CommandLine` 和 `Spectre.Console`。 +- `xidio.Platform.*`:Windows、Linux 与 macOS 的平台专用信息收集 Provider。 - `xidio.GUI`:xidio 的图形界面版本,基于 Avalonia 框架实现跨平台功能。 ### xidio 的调用能力和平台特性 diff --git a/flake.nix b/flake.nix index 3557606..de823ad 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,6 @@ supportedSystems = [ "x86_64-linux" "aarch64-linux" - "x86_64-darwin" "aarch64-darwin" ]; @@ -32,7 +31,10 @@ ] ++ lib.optionals stdenv.isLinux [ clang + iproute2 + iw lld + networkmanager ]; buildInputs = with pkgs; [ diff --git a/xidio/xidio.CLI/Program.cs b/xidio/xidio.CLI/Program.cs index c069b4f..e62e06e 100644 --- a/xidio/xidio.CLI/Program.cs +++ b/xidio/xidio.CLI/Program.cs @@ -5,7 +5,9 @@ using xidio.Core.Models; #if WINDOWS using xidio.Platform.Windows; -#else +#elif LINUX +using xidio.Platform.Linux; +#elif MACOS using xidio.Platform.macOS; #endif @@ -74,10 +76,11 @@ private static IPlatformNetworkDiagnosticsProvider CreatePlatformProvider() { #if WINDOWS return new WindowsPlatformNetworkDiagnosticsProvider(); +#elif LINUX + return new LinuxPlatformNetworkDiagnosticsProvider(); +#elif MACOS + return new MacPlatformNetworkDiagnosticsProvider(); #else - if (OperatingSystem.IsMacOS()) - return new MacPlatformNetworkDiagnosticsProvider(); - return NoopPlatformNetworkDiagnosticsProvider.Instance; #endif } diff --git a/xidio/xidio.CLI/xidio.CLI.csproj b/xidio/xidio.CLI/xidio.CLI.csproj index d537605..6cb658a 100644 --- a/xidio/xidio.CLI/xidio.CLI.csproj +++ b/xidio/xidio.CLI/xidio.CLI.csproj @@ -19,7 +19,19 @@ - + + $(DefineConstants);LINUX + + + + $(DefineConstants);MACOS + + + + + + + diff --git a/xidio/xidio.Platform.Linux/LinuxPlatformNetworkDiagnosticsProvider.cs b/xidio/xidio.Platform.Linux/LinuxPlatformNetworkDiagnosticsProvider.cs new file mode 100644 index 0000000..a805c56 --- /dev/null +++ b/xidio/xidio.Platform.Linux/LinuxPlatformNetworkDiagnosticsProvider.cs @@ -0,0 +1,813 @@ +using System.Diagnostics; +using System.Globalization; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text.Json; +using xidio.Core.Abstractions; +using xidio.Core.Diagnostics; +using xidio.Core.Models; + +namespace xidio.Platform.Linux; + +public sealed class LinuxPlatformNetworkDiagnosticsProvider : IPlatformNetworkDiagnosticsProvider +{ + private const string SysClassNet = "/sys/class/net"; + private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(5); + + public async ValueTask GetOperatingSystemInfoAsync( + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var osRelease = await ReadOsReleaseAsync(cancellationToken); + osRelease.TryGetValue("PRETTY_NAME", out var description); + osRelease.TryGetValue("VERSION_ID", out var version); + + return new OperatingSystemDiagnosticInfo + { + Family = "Linux", + Description = string.IsNullOrWhiteSpace(description) + ? RuntimeInformation.OSDescription + : description, + Version = string.IsNullOrWhiteSpace(version) + ? Environment.OSVersion.VersionString + : version, + Architecture = RuntimeInformation.ProcessArchitecture.ToString(), + XidioVersion = XidioVersion.InformationalVersion, + IsElevated = GetEffectiveUserId() == 0 + }; + } + + public ValueTask> GetPhysicalNetworkInterfaceIdsAsync( + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!Directory.Exists(SysClassNet)) + return ValueTask.FromResult>(Array.Empty()); + + var interfaces = Directory.EnumerateDirectories(SysClassNet) + .Where(path => Directory.Exists(Path.Combine(path, "device"))) + .Select(Path.GetFileName) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Cast() + .ToHashSet(StringComparer.Ordinal); + + return ValueTask.FromResult>(interfaces); + } + + public async ValueTask> GetNetworkAdapterDriverInfosAsync( + CancellationToken cancellationToken) + { + var physicalIds = await GetPhysicalNetworkInterfaceIdsAsync(cancellationToken); + var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces() + .ToDictionary(networkInterface => networkInterface.Name, StringComparer.Ordinal); + var result = new List(); + + foreach (var interfaceId in physicalIds.Order(StringComparer.Ordinal)) + { + cancellationToken.ThrowIfCancellationRequested(); + networkInterfaces.TryGetValue(interfaceId, out var networkInterface); + + var driverName = GetDriverName(interfaceId); + var driverVersion = driverName is null + ? null + : await ReadTrimmedTextAsync($"/sys/module/{driverName}/version", cancellationToken); + + result.Add(new NetworkAdapterDriverInfo + { + Name = interfaceId, + Description = driverName is null + ? networkInterface?.Description ?? interfaceId + : $"{networkInterface?.Description ?? interfaceId} ({driverName})", + DriverVersion = string.IsNullOrWhiteSpace(driverVersion) + ? "" + : driverVersion + }); + } + + return result; + } + + public async ValueTask GetInterfacePlatformInfoAsync( + NetworkInterface networkInterface, + CancellationToken cancellationToken) + { + var interfacePath = Path.Combine(SysClassNet, networkInterface.Name); + if (!Directory.Exists(interfacePath)) + return null; + + var flagsText = await ReadTrimmedTextAsync(Path.Combine(interfacePath, "flags"), cancellationToken); + var mtuText = await ReadTrimmedTextAsync(Path.Combine(interfacePath, "mtu"), cancellationToken); + + bool? isEnabled = null; + if (TryParseHexUInt64(flagsText, out var flags)) + isEnabled = (flags & 0x1) != 0; + + int? mtu = int.TryParse(mtuText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedMtu) + ? parsedMtu + : null; + + return new InterfacePlatformInfo + { + IsEnabled = isEnabled, + Mtu = mtu + }; + } + + public async ValueTask GetWirelessConnectionInfoAsync( + NetworkInterface networkInterface, + CancellationToken cancellationToken) + { + var iwOutput = await RunCommandAsync( + "iw", + ["dev", networkInterface.Name, "link"], + cancellationToken); + var current = ParseIwLink(iwOutput?.StandardOutput); + + var nmcliOutput = await RunCommandAsync( + "nmcli", + [ + "-t", + "--escape", "yes", + "-f", "IN-USE,SSID,BSSID,SIGNAL,FREQ,CHAN,RATE,SECURITY", + "device", "wifi", "list", + "ifname", networkInterface.Name, + "--rescan", "no" + ], + cancellationToken); + var wifiRows = ParseNmcliWifiRows(nmcliOutput?.StandardOutput); + var activeRow = wifiRows.FirstOrDefault(row => row.IsActive); + + if (current is null && activeRow is null) + return null; + + var ssid = current?.Ssid ?? activeRow?.Ssid; + var bssid = current?.Bssid ?? activeRow?.Bssid; + if (string.IsNullOrWhiteSpace(ssid) || string.IsNullOrWhiteSpace(bssid)) + return null; + + return new WirelessConnectionInfo + { + Ssid = ssid, + Bssid = bssid, + SignalQualityPercent = activeRow?.SignalQualityPercent, + RssiDbm = current?.RssiDbm, + Channel = current?.Channel ?? activeRow?.Channel, + FrequencyGhz = current?.FrequencyGhz ?? activeRow?.FrequencyGhz, + PhyType = InferPhyType(current?.RateDescription), + Authentication = activeRow?.Security, + ReceiveRateMbps = current?.ReceiveRateMbps ?? activeRow?.RateMbps, + TransmitRateMbps = current?.TransmitRateMbps ?? activeRow?.RateMbps, + VisibleNetworks = CreateVisibleNetworks(wifiRows) + }; + } + + public async ValueTask GetInterfaceDhcpInfoAsync( + NetworkInterface networkInterface, + CancellationToken cancellationToken) + { + var output = await RunCommandAsync( + "nmcli", + ["-t", "-f", "DHCP4.OPTION,DHCP6.OPTION", "device", "show", networkInterface.Name], + cancellationToken); + + if (string.IsNullOrWhiteSpace(output?.StandardOutput)) + return null; + + long? expiry = null; + int? leaseSeconds = null; + + foreach (var line in output.StandardOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries)) + { + var separator = line.IndexOf('='); + if (separator < 0) + continue; + + var name = line[..separator].Trim(); + var value = line[(separator + 1)..].Trim(); + + if (name.EndsWith("expiry", StringComparison.OrdinalIgnoreCase) && + long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedExpiry)) + { + expiry = parsedExpiry; + } + else if (name.EndsWith("dhcp_lease_time", StringComparison.OrdinalIgnoreCase) && + int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLease)) + { + leaseSeconds = parsedLease; + } + } + + var leaseExpires = expiry is > 0 + ? DateTimeOffset.FromUnixTimeSeconds(expiry.Value) + : (DateTimeOffset?)null; + + return new InterfaceDhcpInfo + { + IsEnabled = true, + LeaseExpires = leaseExpires, + LeaseObtained = leaseExpires is not null && leaseSeconds is > 0 + ? leaseExpires.Value - TimeSpan.FromSeconds(leaseSeconds.Value) + : null + }; + } + + public async ValueTask> GetInterfaceMetricsAsync( + NetworkInterface networkInterface, + CancellationToken cancellationToken) + { + var routes = await GetRoutesAsync(networkInterface, cancellationToken); + + return routes + .GroupBy(route => route.AddressFamily) + .Select(group => new InterfaceMetricInfo + { + AddressFamily = group.Key, + InterfaceMetric = group + .Where(route => route.DestinationPrefix is "0.0.0.0/0" or "::/0") + .Select(route => route.TotalMetric) + .DefaultIfEmpty(group + .Where(route => route.TotalMetric > 0) + .Select(route => route.TotalMetric) + .DefaultIfEmpty(0) + .Min()) + .Min() + }) + .OrderBy(metric => metric.AddressFamily) + .ToList(); + } + + public async ValueTask> GetRoutesAsync( + NetworkInterface networkInterface, + CancellationToken cancellationToken) + { + var routes = new List(); + routes.AddRange(await ReadRoutesAsync(AddressFamily.InterNetwork, networkInterface.Name, cancellationToken)); + routes.AddRange(await ReadRoutesAsync(AddressFamily.InterNetworkV6, networkInterface.Name, cancellationToken)); + return routes; + } + + public async ValueTask> GetNetworkNeighborsAsync( + NetworkInterface networkInterface, + CancellationToken cancellationToken) + { + var output = await RunCommandAsync("ip", ["-j", "neigh", "show"], cancellationToken); + if (string.IsNullOrWhiteSpace(output?.StandardOutput)) + return Array.Empty(); + + try + { + using var document = JsonDocument.Parse(output.StandardOutput); + var result = new List(); + + foreach (var item in document.RootElement.EnumerateArray()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!TryGetString(item, "dev", out var device) || + !string.Equals(device, networkInterface.Name, StringComparison.Ordinal) || + !TryGetString(item, "dst", out var destination) || + destination is null) + { + continue; + } + + _ = TryGetString(item, "lladdr", out var linkLayerAddress); + + result.Add(new NetworkNeighborInfo + { + IpAddress = destination, + LinkLayerAddress = linkLayerAddress ?? "", + AddressFamily = IPAddress.TryParse(destination, out var address) + ? (int)address.AddressFamily + : 0, + State = GetNeighborState(item) + }); + } + + return result; + } + catch (JsonException) + { + return Array.Empty(); + } + } + + public async ValueTask> GetDefaultRoutesAsync( + CancellationToken cancellationToken) + { + var result = new List(); + + foreach (var family in new[] { AddressFamily.InterNetwork, AddressFamily.InterNetworkV6 }) + { + foreach (var route in await ReadRouteEntriesAsync(family, cancellationToken)) + { + if (!IsDefaultDestination(route.Destination)) + continue; + + result.Add(new DefaultRouteInfo + { + DestinationPrefix = family == AddressFamily.InterNetwork ? "0.0.0.0/0" : "::/0", + NextHop = route.NextHop, + AddressFamily = (int)family, + InterfaceIndex = GetInterfaceIndex(route.Device, family), + InterfaceAlias = route.Device, + RouteMetric = route.Metric, + InterfaceMetric = 0 + }); + } + } + + return result + .OrderBy(route => route.AddressFamily) + .ThenBy(route => route.TotalMetric) + .ThenBy(route => route.InterfaceAlias, StringComparer.Ordinal) + .ToList(); + } + + private static async Task> ReadRoutesAsync( + AddressFamily addressFamily, + string interfaceName, + CancellationToken cancellationToken) + { + var entries = await ReadRouteEntriesAsync(addressFamily, cancellationToken); + + return entries + .Where(entry => string.Equals(entry.Device, interfaceName, StringComparison.Ordinal)) + .Select(entry => new InterfaceRouteInfo + { + DestinationPrefix = NormalizeDestination(entry.Destination, addressFamily), + NextHop = entry.NextHop, + AddressFamily = (int)addressFamily, + RouteMetric = entry.Metric, + InterfaceMetric = 0 + }) + .ToList(); + } + + private static async Task> ReadRouteEntriesAsync( + AddressFamily addressFamily, + CancellationToken cancellationToken) + { + var familyFlag = addressFamily == AddressFamily.InterNetwork ? "-4" : "-6"; + var output = await RunCommandAsync( + "ip", + ["-j", familyFlag, "route", "show", "table", "all"], + cancellationToken); + + if (string.IsNullOrWhiteSpace(output?.StandardOutput)) + return Array.Empty(); + + try + { + using var document = JsonDocument.Parse(output.StandardOutput); + var result = new List(); + + foreach (var item in document.RootElement.EnumerateArray()) + { + if (!TryGetString(item, "dev", out var device) || string.IsNullOrWhiteSpace(device)) + continue; + + _ = TryGetString(item, "dst", out var destination); + _ = TryGetString(item, "gateway", out var gateway); + + result.Add(new RouteEntry( + destination ?? "default", + gateway ?? "", + device, + TryGetInt32(item, "metric") ?? 0)); + } + + return result; + } + catch (JsonException) + { + return Array.Empty(); + } + } + + private static IwLinkInfo? ParseIwLink(string? output) + { + if (string.IsNullOrWhiteSpace(output) || + output.Contains("Not connected", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + string? ssid = null; + string? bssid = null; + string? rateDescription = null; + int? rssi = null; + int? frequencyMhz = null; + double? receiveRate = null; + double? transmitRate = null; + + foreach (var rawLine in output.Split('\n', StringSplitOptions.RemoveEmptyEntries)) + { + var line = rawLine.Trim(); + + if (line.StartsWith("Connected to ", StringComparison.OrdinalIgnoreCase)) + bssid = line["Connected to ".Length..].Split(' ', 2)[0]; + else if (line.StartsWith("SSID:", StringComparison.OrdinalIgnoreCase)) + ssid = line["SSID:".Length..].Trim(); + else if (line.StartsWith("freq:", StringComparison.OrdinalIgnoreCase)) + frequencyMhz = ParseFirstInt(line["freq:".Length..]); + else if (line.StartsWith("signal:", StringComparison.OrdinalIgnoreCase)) + rssi = ParseFirstInt(line["signal:".Length..]); + else if (line.StartsWith("rx bitrate:", StringComparison.OrdinalIgnoreCase)) + { + rateDescription = line; + receiveRate = ParseFirstDouble(line["rx bitrate:".Length..]); + } + else if (line.StartsWith("tx bitrate:", StringComparison.OrdinalIgnoreCase)) + { + rateDescription ??= line; + transmitRate = ParseFirstDouble(line["tx bitrate:".Length..]); + } + } + + if (string.IsNullOrWhiteSpace(ssid) || string.IsNullOrWhiteSpace(bssid)) + return null; + + return new IwLinkInfo( + ssid, + bssid, + rssi, + frequencyMhz is > 0 ? frequencyMhz.Value / 1000d : null, + FrequencyMhzToChannel(frequencyMhz), + receiveRate, + transmitRate, + rateDescription); + } + + private static IReadOnlyList ParseNmcliWifiRows(string? output) + { + if (string.IsNullOrWhiteSpace(output)) + return Array.Empty(); + + var result = new List(); + + foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries)) + { + var fields = SplitEscapedFields(line); + if (fields.Count < 8 || string.IsNullOrWhiteSpace(fields[1])) + continue; + + var frequencyMhz = ParseFirstInt(fields[4]); + result.Add(new NmcliWifiRow( + fields[0] is "*" or "yes", + fields[1], + fields[2], + ParseFirstInt(fields[3]), + frequencyMhz is > 0 ? frequencyMhz.Value / 1000d : null, + ParseFirstInt(fields[5]) ?? FrequencyMhzToChannel(frequencyMhz), + ParseFirstDouble(fields[6]), + string.IsNullOrWhiteSpace(fields[7]) ? null : fields[7])); + } + + return result; + } + + private static List CreateVisibleNetworks( + IReadOnlyList rows) + { + return rows + .GroupBy(row => row.Ssid, StringComparer.Ordinal) + .Select(group => new VisibleWirelessNetworkInfo + { + Ssid = group.Key, + SignalQualityPercent = group.Max(row => row.SignalQualityPercent), + Authentication = group.Select(row => row.Security).FirstOrDefault(value => value is not null), + BssidCount = group.Select(row => row.Bssid).Where(value => value.Length > 0).Distinct().Count(), + IsConnectable = null + }) + .OrderByDescending(network => network.SignalQualityPercent) + .ThenBy(network => network.Ssid, StringComparer.Ordinal) + .ToList(); + } + + private static List SplitEscapedFields(string line) + { + var result = new List(); + var current = new List(); + var escaped = false; + + foreach (var character in line) + { + if (escaped) + { + current.Add(character); + escaped = false; + } + else if (character == '\\') + { + escaped = true; + } + else if (character == ':') + { + result.Add(new string(current.ToArray())); + current.Clear(); + } + else + { + current.Add(character); + } + } + + if (escaped) + current.Add('\\'); + + result.Add(new string(current.ToArray())); + return result; + } + + private static string? InferPhyType(string? rateDescription) + { + if (string.IsNullOrWhiteSpace(rateDescription)) + return null; + if (rateDescription.Contains("EHT", StringComparison.OrdinalIgnoreCase)) + return "802.11be"; + if (rateDescription.Contains("HE-", StringComparison.OrdinalIgnoreCase)) + return "802.11ax"; + if (rateDescription.Contains("VHT-", StringComparison.OrdinalIgnoreCase)) + return "802.11ac"; + if (rateDescription.Contains("MCS", StringComparison.OrdinalIgnoreCase)) + return "802.11n"; + return null; + } + + private static int? FrequencyMhzToChannel(int? frequencyMhz) + { + if (frequencyMhz is null or <= 0) + return null; + if (frequencyMhz == 2484) + return 14; + if (frequencyMhz is >= 2412 and <= 2472) + return (frequencyMhz.Value - 2407) / 5; + if (frequencyMhz is >= 5000 and <= 5900) + return (frequencyMhz.Value - 5000) / 5; + if (frequencyMhz is >= 5955 and <= 7115) + return (frequencyMhz.Value - 5950) / 5; + return null; + } + + private static async Task> ReadOsReleaseAsync( + CancellationToken cancellationToken) + { + var result = new Dictionary(StringComparer.Ordinal); + + try + { + foreach (var line in await File.ReadAllLinesAsync("/etc/os-release", cancellationToken)) + { + var separator = line.IndexOf('='); + if (separator <= 0) + continue; + + result[line[..separator]] = line[(separator + 1)..].Trim().Trim('"'); + } + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + + return result; + } + + private static async Task ReadTrimmedTextAsync( + string path, + CancellationToken cancellationToken) + { + try + { + return (await File.ReadAllTextAsync(path, cancellationToken)).Trim(); + } + catch (IOException) + { + return null; + } + catch (UnauthorizedAccessException) + { + return null; + } + } + + private static string? GetDriverName(string interfaceName) + { + try + { + var driverPath = Path.Combine(SysClassNet, interfaceName, "device", "driver"); + return Directory.ResolveLinkTarget(driverPath, returnFinalTarget: true)?.Name; + } + catch (IOException) + { + return null; + } + catch (UnauthorizedAccessException) + { + return null; + } + } + + private static int GetInterfaceIndex(string interfaceName, AddressFamily addressFamily) + { + try + { + var networkInterface = NetworkInterface.GetAllNetworkInterfaces() + .FirstOrDefault(item => string.Equals(item.Name, interfaceName, StringComparison.Ordinal)); + var properties = networkInterface?.GetIPProperties(); + + return addressFamily == AddressFamily.InterNetwork + ? properties?.GetIPv4Properties()?.Index ?? 0 + : properties?.GetIPv6Properties()?.Index ?? 0; + } + catch (NetworkInformationException) + { + return 0; + } + } + + private static async Task RunCommandAsync( + string fileName, + IReadOnlyList arguments, + CancellationToken cancellationToken) + { + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(CommandTimeout); + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = fileName, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.StartInfo.Environment["LC_ALL"] = "C"; + foreach (var argument in arguments) + process.StartInfo.ArgumentList.Add(argument); + + try + { + process.Start(); + var outputTask = process.StandardOutput.ReadToEndAsync(timeout.Token); + var errorTask = process.StandardError.ReadToEndAsync(timeout.Token); + + await process.WaitForExitAsync(timeout.Token); + var standardOutput = await outputTask; + var standardError = await errorTask; + + return process.ExitCode == 0 + ? new CommandResult(standardOutput, standardError) + : null; + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + TryKill(process); + return null; + } + catch (OperationCanceledException) + { + TryKill(process); + throw; + } + catch (Exception exception) when (exception is System.ComponentModel.Win32Exception or IOException) + { + return null; + } + } + + private static void TryKill(Process process) + { + try + { + if (!process.HasExited) + process.Kill(entireProcessTree: true); + } + catch (InvalidOperationException) + { + } + } + + private static bool TryGetString(JsonElement element, string propertyName, out string? value) + { + value = null; + if (!element.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.String) + return false; + + value = property.GetString(); + return value is not null; + } + + private static int? TryGetInt32(JsonElement element, string propertyName) + { + return element.TryGetProperty(propertyName, out var property) && property.TryGetInt32(out var value) + ? value + : null; + } + + private static string GetNeighborState(JsonElement element) + { + if (!element.TryGetProperty("state", out var state)) + return ""; + if (state.ValueKind == JsonValueKind.String) + return state.GetString() ?? ""; + if (state.ValueKind == JsonValueKind.Array) + return string.Join(",", state.EnumerateArray().Select(value => value.GetString())); + return ""; + } + + private static bool IsDefaultDestination(string destination) + { + return string.IsNullOrWhiteSpace(destination) || + destination is "default" or "0.0.0.0/0" or "::/0"; + } + + private static string NormalizeDestination(string destination, AddressFamily addressFamily) + { + if (IsDefaultDestination(destination)) + return addressFamily == AddressFamily.InterNetwork ? "0.0.0.0/0" : "::/0"; + + if (IPAddress.TryParse(destination, out var address)) + return $"{destination}/{(address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128)}"; + + return destination; + } + + private static bool TryParseHexUInt64(string? value, out ulong result) + { + result = 0; + if (string.IsNullOrWhiteSpace(value)) + return false; + + return ulong.TryParse( + value.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? value[2..] : value, + NumberStyles.HexNumber, + CultureInfo.InvariantCulture, + out result); + } + + private static int? ParseFirstInt(string value) + { + var token = value.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + return int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? result + : null; + } + + private static double? ParseFirstDouble(string value) + { + var token = value.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + return double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) + ? result + : null; + } + + [DllImport("libc")] + private static extern uint geteuid(); + + private static uint GetEffectiveUserId() + { + try + { + return geteuid(); + } + catch (DllNotFoundException) + { + return uint.MaxValue; + } + } + + private sealed record CommandResult(string StandardOutput, string StandardError); + + private sealed record RouteEntry(string Destination, string NextHop, string Device, int Metric); + + private sealed record IwLinkInfo( + string Ssid, + string Bssid, + int? RssiDbm, + double? FrequencyGhz, + int? Channel, + double? ReceiveRateMbps, + double? TransmitRateMbps, + string? RateDescription); + + private sealed record NmcliWifiRow( + bool IsActive, + string Ssid, + string Bssid, + int? SignalQualityPercent, + double? FrequencyGhz, + int? Channel, + double? RateMbps, + string? Security); +} diff --git a/xidio/xidio.Platform.Linux/xidio.Platform.Linux.csproj b/xidio/xidio.Platform.Linux/xidio.Platform.Linux.csproj new file mode 100644 index 0000000..a5e4516 --- /dev/null +++ b/xidio/xidio.Platform.Linux/xidio.Platform.Linux.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/xidio/xidio.slnx b/xidio/xidio.slnx index 1df6d62..4013310 100644 --- a/xidio/xidio.slnx +++ b/xidio/xidio.slnx @@ -1,6 +1,7 @@ + From c282d93ffa6964db0c9a457b84e883acdfd4699a Mon Sep 17 00:00:00 2001 From: LyCecilion Date: Sat, 11 Jul 2026 11:25:27 +0800 Subject: [PATCH 7/8] fix(privacy): redact hardware addresses in CLI --- xidio/xidio.CLI/ConsoleDiagnosticReporter.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/xidio/xidio.CLI/ConsoleDiagnosticReporter.cs b/xidio/xidio.CLI/ConsoleDiagnosticReporter.cs index 9a578be..91d957e 100644 --- a/xidio/xidio.CLI/ConsoleDiagnosticReporter.cs +++ b/xidio/xidio.CLI/ConsoleDiagnosticReporter.cs @@ -278,7 +278,7 @@ private static void PrintPrimaryInterfaceDetails(PrimaryInterfaceInfo primaryInt detailsTable.AddRow("已连接", FormatBool(primaryInterface.IsConnected)); detailsTable.AddRow("状态", FormatOperationalStatus(primaryInterface.OperationalStatus)); detailsTable.AddRow("接口类型", Escape(primaryInterface.NetworkInterfaceType.ToString())); - detailsTable.AddRow("MAC 地址", Escape(primaryInterface.MacAddress ?? "")); + detailsTable.AddRow("MAC 地址", Escape(FormatHardwareAddress(primaryInterface.MacAddress))); detailsTable.AddRow("链路速度", Escape(FormatLinkSpeed(primaryInterface.LinkSpeedBitsPerSecond))); detailsTable.AddRow("MTU", Escape(primaryInterface.Mtu is null ? "" @@ -287,7 +287,7 @@ private static void PrintPrimaryInterfaceDetails(PrimaryInterfaceInfo primaryInt if (primaryInterface.WirelessConnection is not null) { detailsTable.AddRow("SSID", Escape(primaryInterface.WirelessConnection.Ssid)); - detailsTable.AddRow("BSSID", Escape(primaryInterface.WirelessConnection.Bssid)); + detailsTable.AddRow("BSSID", Escape(FormatHardwareAddress(primaryInterface.WirelessConnection.Bssid))); detailsTable.AddRow("RSSI", Escape(primaryInterface.WirelessConnection.RssiDbm is null ? "" : $"{primaryInterface.WirelessConnection.RssiDbm} dBm")); @@ -371,7 +371,7 @@ private static void PrintNeighborSummary(IReadOnlyList neig neighborTable.AddRow( Escape(FormatAddressFamily(neighbor.AddressFamily)), Escape(neighbor.IpAddress), - Escape(string.IsNullOrWhiteSpace(neighbor.LinkLayerAddress) ? "" : neighbor.LinkLayerAddress), + Escape(FormatHardwareAddress(neighbor.LinkLayerAddress, "")), Escape(neighbor.State)); } @@ -637,6 +637,19 @@ private static string FormatWirelessRates(WirelessConnectionInfo wireless) return $"Rx {rx}, Tx {tx}"; } + private static string FormatHardwareAddress(string? address, string unavailable = "") + { + if (string.IsNullOrWhiteSpace(address)) + return unavailable; + + var separator = address.Contains(':') ? ':' : '-'; + var parts = address.Split(separator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + return parts.Length == 6 && parts.All(part => part.Length == 2) + ? string.Join(separator, parts.Take(3).Concat(["XX", "XX", "XX"])) + : ""; + } + private static string FormatDnsResult(DnsProbeResult result) { var source = string.IsNullOrWhiteSpace(result.DnsServer) ? "system" : result.DnsServer; From 6d66474adb2889d8beccd6b0d36020c77e761f38 Mon Sep 17 00:00:00 2001 From: LyCecilion Date: Sat, 11 Jul 2026 11:34:11 +0800 Subject: [PATCH 8/8] chore(release): prepare v0.1.1 --- CHANGELOG.md | 18 +++++++++++++++++- README.md | 2 +- version.json | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68fce62..6153e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,24 @@ ## [Unreleased] +## [0.1.1] - 2026-07-11 + ### Added - 增加 Linux 平台 Provider,支持系统与权限、物理网卡与驱动、Wi-Fi、DHCP、路由、metric、默认路由和邻居表信息收集。 +- 增加 Nix flake、锁文件与 direnv 集成,为 Linux 开发环境提供 .NET 10、`iproute2`、NetworkManager 和 `iw`。 +- 增加 Roadmap、Contributing、MIT License 与完整的项目文档。 +- 增加项目图标与 Logo 的 PNG 资源。 + +### Changed + +- 重写 `AGENTS.md`,将开发路线与诊断范围迁移至 `ROADMAP.md`。 +- 根据当前 nixpkgs 支持范围,Nix devShell 支持 `x86_64-linux`、`aarch64-linux` 和 `aarch64-darwin`。 + +### Fixed + +- 修复未征得用户同意即收集系统代理信息的隐私问题。 +- 修复 CLI 原样显示 MAC、BSSID 与邻居链路层地址的问题,默认隐藏设备标识字节。 ## [0.1.0] - 2026-05-22 @@ -22,4 +37,5 @@ - 增加 CI、标签发布工作流与 Nerdbank.GitVersioning 版本管理。 [0.1.0]: https://github.com/LyCecilion/xidio/releases/tag/v0.1.0 -[Unreleased]: https://github.com/LyCecilion/xidio/compare/v0.1.0...HEAD +[0.1.1]: https://github.com/LyCecilion/xidio/compare/v0.1.0...v0.1.1 +[Unreleased]: https://github.com/LyCecilion/xidio/compare/v0.1.1...HEAD diff --git a/README.md b/README.md index e97542c..7ed0995 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ _✨**Xidian Internet Diagnostic Intelligence Operator**✨_\ xidio _(Xidian Internet Diagnostic Intelligence Operator)_ 是一款由 Project Hazelita 开发的西安电子科技大学校园网诊断工具。项目基于 C# 和 .NET 10,面向 Windows、Linux 与 macOS,希望将分散的网卡、IP、DHCP、DNS、路由、Wi-Fi 及主动探测信息整理为可理解、可上报的诊断结果。 -xidio 的灵感来自 [NanCunChild](https://github.com/NanCunChild) 的 [Marduk](https://github.com/NanCunChild/Marduk)。项目当前处于早期开发阶段:v0.1.0 已提供 CLI 与以 Windows 为主的信息收集能力,其他平台与完整分析、修复、上报功能仍在实现中。 +xidio 的灵感来自 [NanCunChild](https://github.com/NanCunChild) 的 [Marduk](https://github.com/NanCunChild/Marduk)。项目当前处于早期开发阶段:v0.1.1 已提供 CLI 与 Windows、Linux、macOS 平台 Provider,完整分析、修复与上报功能仍在实现中。 ## ✨ Features diff --git a/version.json b/version.json index cf44616..ef811c6 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.1", + "version": "0.1.1", "publicReleaseRefSpec": [ "^refs/tags/v\\d+\\.\\d+\\.\\d+$" ],