windows 驱动实例分析系列: wireguard-nt驱动分析-api篇(二)

WireGuard-NT API 模块分析 - 第二部分:配置管理、网络接口与日志系统

1. 配置管理

配置管理是 WireGuard 控制平面的核心功能,负责将用户配置(接口密钥、对等点、允许 IP 等)转换为驱动内部数据结构,并支持从驱动查询当前配置。

1.1 数据结构映射

API 模块与内核驱动共享相同的数据结构布局,但定义在独立的头文件中(wireguard.h../driver/ioctl.h)。configuration.c 使用 static_assert 在编译时确保两边的结构完全一致,防止因对齐或填充差异导致的错误。

关键结构体对比
API 结构体 (wireguard.h) 驱动结构体 (ioctl.h) 大小检查
WIREGUARD_INTERFACE WG_IOCTL_INTERFACE 完全一致
WIREGUARD_PEER WG_IOCTL_PEER 完全一致
WIREGUARD_ALLOWED_IP WG_IOCTL_ALLOWED_IP 完全一致
WIREGUARD_ADAPTER_STATE WG_IOCTL_ADAPTER_STATE 完全一致

这些静态断言确保了在 Windows 平台上,无论编译选项如何,结构体内存布局都相同,从而安全地通过 DeviceIoControl 在用户态和内核态之间传递二进制数据。

1.2 配置设置 (WireGuardSetConfiguration)

函数
c 复制代码
BOOL WINAPI WireGuardSetConfiguration(
    WIREGUARD_ADAPTER *Adapter,
    const WIREGUARD_INTERFACE *Config,
    DWORD Bytes
);
内部流程
  1. 通过 AdapterOpenDeviceObject 获取设备对象句柄
  2. 调用 DeviceIoControl,控制码为 WG_IOCTL_SET
    • lpInBufferNULL(输入缓冲区不使用)
    • lpOutBuffer 指向 Config,长度为 Bytes
  3. 检查操作结果,关闭句柄,返回状态

特点

  • Config 是一个可变长结构,包含固定头部以及紧随其后的 PeersCountWIREGUARD_PEER 结构
  • 每个 WIREGUARD_PEER 又包含 AllowedIPsCountWIREGUARD_ALLOWED_IP 结构
  • 用户需要构造完整的扁平内存布局,并通过 Bytes 指示总长度
  • 内核驱动解析该缓冲区,执行原子配置更新
标志位语义

WIREGUARD_INTERFACE_FLAG

  • WIREGUARD_INTERFACE_REPLACE_PEERS:删除所有现有对等点,然后添加新列表
  • WIREGUARD_INTERFACE_HAS_PUBLIC_KEY / HAS_PRIVATE_KEY / HAS_LISTEN_PORT:指示哪些字段有效

WIREGUARD_PEER_FLAG

  • WIREGUARD_PEER_REPLACE_ALLOWED_IPS:对该对等点替换所有允许 IP
  • WIREGUARD_PEER_REMOVE:删除该对等点
  • WIREGUARD_PEER_UPDATE_ONLY:仅更新已存在的对等点,不新增

1.3 配置获取 (WireGuardGetConfiguration)

函数
c 复制代码
BOOL WINAPI WireGuardGetConfiguration(
    WIREGUARD_ADAPTER *Adapter,
    WIREGUARD_INTERFACE *Config,
    DWORD *Bytes
);
流程
  1. 打开设备对象句柄
  2. 调用 DeviceIoControl,控制码 WG_IOCTL_GET
    • lpInBufferNULL
    • lpOutBuffer 指向 Config,输入 *Bytes 表示缓冲区大小
  3. 返回时,Bytes 被更新为实际写入的字节数
  4. 如果缓冲区不足,返回 FALSEGetLastErrorERROR_MORE_DATABytes 包含所需大小

注意 :调用者应先以较小的缓冲区尝试,若返回 ERROR_MORE_DATA 则重新分配足够内存再调用。

1.4 适配器状态管理

设置状态 (WireGuardSetAdapterState)
c 复制代码
BOOL WINAPI WireGuardSetAdapterState(WIREGUARD_ADAPTER *Adapter, WIREGUARD_ADAPTER_STATE State)

允许的状态:

  • WIREGUARD_ADAPTER_STATE_UP:启用适配器(创建 UDP 套接字,开始加密通信)
  • WIREGUARD_ADAPTER_STATE_DOWN:禁用适配器(关闭套接字,停止通信)

内部通过 WG_IOCTL_SET_ADAPTER_STATE 控制码,传递状态值。

获取状态 (WireGuardGetAdapterState)
c 复制代码
BOOL WINAPI WireGuardGetAdapterState(WIREGUARD_ADAPTER *Adapter, WIREGUARD_ADAPTER_STATE *State)

传递 WG_IOCTL_ADAPTER_STATE_QUERY 作为输入,返回当前状态。

2. 网络接口操作

2.1 LUID 获取 (WireGuardGetAdapterLUID)

c 复制代码
VOID WINAPI WireGuardGetAdapterLUID(WIREGUARD_ADAPTER *Adapter, NET_LUID *Luid)

从适配器结构中提取 LuidIndexIfType,组合成完整的 NET_LUID。该 LUID 可用于后续的网络 API(如 ConvertInterfaceLuidToIndexGetAdapterIndex 等)。

2.2 设备对象句柄 (AdapterOpenDeviceObject)

c 复制代码
HANDLE WINAPI AdapterOpenDeviceObject(const WIREGUARD_ADAPTER *Adapter)

使用 CreateFileW 打开 Adapter->InterfaceFilename(如 \\.\GLOBALROOT\Device\WireGuard-0),返回句柄用于 DeviceIoControl 通信。该函数是配置操作的基础。

2.3 网络连接名称设置 (NciSetAdapterName)

WireGuard 适配器在网络连接面板(Network Connections)中显示的名称需要与内核配置的名称一致。由于 Windows 的网络连接名称管理(NCI,Network Connection Interface)是半文档化的,该函数实现了健壮的命名处理。

名称冲突处理策略
c 复制代码
BOOL NciSetAdapterName(GUID *Guid, LPCWSTR Name)
  1. 尝试直接调用 NciSetConnectionName 设置名称
  2. 如果返回 ERROR_DUP_NAME(名称已存在):
    a. 获取占用该名称的适配器的 GUID(ConvertInterfaceAliasToGuid
    b. 尝试为该冲突适配器分配一个新名称(在原名称后添加数字后缀)
    c. 如果成功重命名冲突适配器,则再次尝试设置当前适配器的请求名称
  3. 如果仍冲突,为当前适配器自动添加数字后缀(如 "WireGuard Tunnel 1")
  4. 最多尝试 1000 次,避免死循环
辅助函数
  • RenameByNetGUID:通过 SetupDiSetDeviceProperty 设置 DEVPKEY_WireGuard_Name 属性来重命名设备
  • ConvertInterfaceAliasToGuid:使用 ConvertInterfaceAliasToLuid + ConvertInterfaceLuidToGuid 转换别名到 GUID

3. 日志系统

3.1 日志架构

日志系统由三部分组成:

  1. 用户态回调 :应用层通过 WireGuardSetLogger 注册回调函数
  2. API 模块的日志转发logger.c 实现日志收集线程,从驱动读取日志条目
  3. 内核驱动的日志生成:驱动内部产生带时间戳的日志消息,通过控制设备传递

3.2 日志回调注册 (WireGuardSetLogger)

c 复制代码
VOID WINAPI WireGuardSetLogger(WIREGUARD_LOGGER_CALLBACK NewLogger)
  • 全局变量 Logger 指向当前回调函数

  • 如果 NewLoggerNULL,使用默认的 NopLogger(空操作)

  • 回调函数类型:

    c 复制代码
    typedef VOID (CALLBACK *WIREGUARD_LOGGER_CALLBACK)(
        WIREGUARD_LOGGER_LEVEL Level,
        DWORD64 Timestamp,
        LPCWSTR Message
    );

3.3 适配器日志控制 (WireGuardSetAdapterLogging)

c 复制代码
BOOL WINAPI WireGuardSetAdapterLogging(WIREGUARD_ADAPTER *Adapter, WIREGUARD_ADAPTER_LOG_STATE LogState)

允许的状态:

  • WIREGUARD_ADAPTER_LOG_OFF:停止日志收集,关闭读取线程
  • WIREGUARD_ADAPTER_LOG_ON:启用日志,消息不带前缀
  • WIREGUARD_ADAPTER_LOG_ON_WITH_PREFIX:启用日志,每条消息前添加接口索引(如 "0: ")
内部实现
  1. 如果当前状态与请求状态相同,直接返回
  2. 更新 Adapter->LogState(使用原子操作 WriteULongNoFence
  3. 关闭日志 :如果从开启变为关闭且存在日志线程:
    • 调用 CancelSynchronousIo 取消阻塞的 DeviceIoControl
    • 等待线程退出(最多 100ms 超时,循环取消)
    • 关闭线程句柄
  4. 开启日志 :如果从关闭变为开启且没有日志线程:
    • 创建 LogReaderThread 线程,传入适配器句柄

3.4 日志读取线程 (LogReaderThread)

该线程循环运行,负责从驱动读取日志行并转发给用户回调。

工作流程
复制代码
无限循环:
  1. 检查 LogState 是否为 OFF,若是则退出
  2. 调用 DeviceIoControl(WG_IOCTL_READ_LOG_LINE)
     - 阻塞等待,直到有日志行或设备关闭
     - 返回 WG_IOCTL_LOG_ENTRY 结构
  3. 解析日志级别(Entry.Msg[0] 为 '1'/'2'/'3')
  4. 如果需要前缀,获取 IfIndex(若未获取,通过 LUID 转换)
  5. 将 UTF-8 消息转换为宽字符(MultiByteToWideChar)
  6. 调用 Logger 回调
  7. 如果 DeviceIoControl 失败:
     - 若错误为 ERROR_OPERATION_ABORTED(被取消),等待 5 秒后重新打开
     - 否则尝试最多 10 次重新打开设备句柄(每秒一次)
     - 若仍失败,设置 LogState = OFF 并退出
日志条目结构 (WG_IOCTL_LOG_ENTRY)
c 复制代码
typedef struct _WG_IOCTL_LOG_ENTRY
{
    DWORD64 Timestamp;    // 100ns 间隔,自 1601-01-01
    CHAR Msg[512];        // 第一个字节为级别字符,后续为 UTF-8 消息
} WG_IOCTL_LOG_ENTRY;

级别映射:

  • '1'WIREGUARD_LOG_ERR
  • '2'WIREGUARD_LOG_WARN
  • '3'WIREGUARD_LOG_INFO

3.5 日志辅助函数

logger.hlogger.c 提供了丰富的日志工具函数:

函数 用途
LoggerLog 直接记录一条宽字符串日志
LoggerLogV / LoggerLogFmt 格式化日志记录
LoggerError 记录错误码和前缀,转换 SetupAPI 错误码
LoggerErrorV / LoggerErrorFmt 格式化错误日志
LoggerLastErrorV / LoggerLastErrorFmt 自动获取 GetLastError() 并记录
LOG / LOG_ERROR / LOG_LAST_ERROR 宏简化调用
特殊处理
  • LoggerError 会尝试将错误码作为 HRESULT 解析(使用 HRESULT_FROM_SETUPAPI),获取系统消息
  • 日志消息会截断到 0x400 个宽字符,溢出处添加水平省略号(\u2026
  • 所有日志函数都保持 GetLastError 不变(即记录日志不影响错误码)

3.6 内存分配器

logger.h 中定义了一套带日志的内存分配宏:

c 复制代码
#define Alloc(Size)    LoggerAlloc(__L(__FUNCTION__), 0, Size)
#define Zalloc(Size)   LoggerAlloc(__L(__FUNCTION__), HEAP_ZERO_MEMORY, Size)
#define Free(Ptr)      HeapFree(ModuleHeap, 0, Ptr)

这些分配器在失败时会自动记录错误日志,方便调试内存不足问题。所有 API 模块的内存分配都通过 ModuleHeap 进行(进程私有堆),便于隔离和泄漏检测。

4. 与其他模块的交互

4.1 与驱动交互

所有配置操作最终通过 DeviceIoControl 与内核驱动通信。控制码定义在 ../driver/ioctl.h 中:

控制码 功能
WG_IOCTL_SET 设置完整配置
WG_IOCTL_GET 获取当前配置
WG_IOCTL_SET_ADAPTER_STATE 设置/查询适配器状态
WG_IOCTL_READ_LOG_LINE 读取日志行(阻塞)

4.2 与 SetupAPI 交互

  • 设备创建通过 SwDeviceCreate(Windows 软件设备 API)
  • 设备属性操作通过 SetupDiSetDevicePropertyW / SetupDiGetDevicePropertyW
  • 设备枚举通过 SetupDiGetClassDevsExW
  • 设备移除、启用/禁用通过 SetupDiCallClassInstaller

4.3 与 NCI 交互

NciSetConnectionNameNciGetConnectionName 通过 nci.def 定义的延迟导入函数调用系统 nci.dll(Network Connection Interface)。该 DLL 是 Windows 未公开的组件,用于管理网络连接文件夹中的连接名称和图标。

5. 日志资源管理

5.1 线程安全

  • LogState 使用无栅栏原子操作(ReadULongNoFence / WriteULongNoFence)更新,因为线程间不需要严格的内存排序,只需确保值的可见性
  • 日志回调可能被多个线程并发调用(包括主线程和日志线程),应用层回调需自行处理同步

5.2 资源清理

WireGuardCloseAdapter 中:

  1. 调用 WireGuardSetAdapterLogging(Adapter, WIREGUARD_ADAPTER_LOG_OFF) 停止日志线程
  2. 线程关闭后会等待线程退出,确保没有悬空句柄

5.3 错误恢复

  • 如果设备被意外移除,日志线程尝试重新打开设备句柄;
  • 若 10 次重试失败,自动关闭日志,避免无限循环;
相关推荐
MNLoser1 小时前
AI agent开发——LangGraph接入持久化
linux·人工智能·windows·python
小桥流水---人工智能1 小时前
Windows下RTX 5070安装PyTorch GPU完整教程:Python 3.11 + PyTorch 2.8.0 + CUDA 12.9
pytorch·windows·python3.11
爱学习的小白柏13 小时前
【AI问数技术】多Agent协同架构:查询规划/SQL生成/洞察分析/报告生成
java·网络·人工智能·windows·sql·架构·llama
何以解忧,唯有..17 小时前
Python 中获取目录操作完全指南
windows·python·microsoft
YCOSA202519 小时前
雨晨 Windows 10 轻装版 64位 六合一 VIP 19045.7663
windows·物联网
DOLA_Tech1 天前
电脑共享文件夹教程(Windows、统信UOS、银河麒麟)
windows·电脑
淡海水1 天前
03-03-线性-LinkedList-T-源码不变式与选择边界
windows·链表·c#·编译·linkedlist·clr·机器码
钝挫力PROGRAMER1 天前
Windows Docker 环境搭建 GitLab CI/CD 流水线
windows·docker·gitlab·gitlab-runner
拿本唠嗑AI研究1 天前
从电脑到手机:DeepSeek Harness Windows安装与手机互动教程(避坑完全版)
人工智能·windows·智能手机·deepseek·harness