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
);
内部流程
- 通过
AdapterOpenDeviceObject获取设备对象句柄 - 调用
DeviceIoControl,控制码为WG_IOCTL_SETlpInBuffer为NULL(输入缓冲区不使用)lpOutBuffer指向Config,长度为Bytes
- 检查操作结果,关闭句柄,返回状态
特点:
Config是一个可变长结构,包含固定头部以及紧随其后的PeersCount个WIREGUARD_PEER结构- 每个
WIREGUARD_PEER又包含AllowedIPsCount个WIREGUARD_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:对该对等点替换所有允许 IPWIREGUARD_PEER_REMOVE:删除该对等点WIREGUARD_PEER_UPDATE_ONLY:仅更新已存在的对等点,不新增
1.3 配置获取 (WireGuardGetConfiguration)
函数
c
BOOL WINAPI WireGuardGetConfiguration(
WIREGUARD_ADAPTER *Adapter,
WIREGUARD_INTERFACE *Config,
DWORD *Bytes
);
流程
- 打开设备对象句柄
- 调用
DeviceIoControl,控制码WG_IOCTL_GETlpInBuffer为NULLlpOutBuffer指向Config,输入*Bytes表示缓冲区大小
- 返回时,
Bytes被更新为实际写入的字节数 - 如果缓冲区不足,返回
FALSE,GetLastError为ERROR_MORE_DATA,Bytes包含所需大小
注意 :调用者应先以较小的缓冲区尝试,若返回 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)
从适配器结构中提取 LuidIndex 和 IfType,组合成完整的 NET_LUID。该 LUID 可用于后续的网络 API(如 ConvertInterfaceLuidToIndex、GetAdapterIndex 等)。
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)
- 尝试直接调用
NciSetConnectionName设置名称 - 如果返回
ERROR_DUP_NAME(名称已存在):
a. 获取占用该名称的适配器的 GUID(ConvertInterfaceAliasToGuid)
b. 尝试为该冲突适配器分配一个新名称(在原名称后添加数字后缀)
c. 如果成功重命名冲突适配器,则再次尝试设置当前适配器的请求名称 - 如果仍冲突,为当前适配器自动添加数字后缀(如 "WireGuard Tunnel 1")
- 最多尝试 1000 次,避免死循环
辅助函数
RenameByNetGUID:通过SetupDiSetDeviceProperty设置DEVPKEY_WireGuard_Name属性来重命名设备ConvertInterfaceAliasToGuid:使用ConvertInterfaceAliasToLuid+ConvertInterfaceLuidToGuid转换别名到 GUID
3. 日志系统
3.1 日志架构
日志系统由三部分组成:
- 用户态回调 :应用层通过
WireGuardSetLogger注册回调函数 - API 模块的日志转发 :
logger.c实现日志收集线程,从驱动读取日志条目 - 内核驱动的日志生成:驱动内部产生带时间戳的日志消息,通过控制设备传递
3.2 日志回调注册 (WireGuardSetLogger)
c
VOID WINAPI WireGuardSetLogger(WIREGUARD_LOGGER_CALLBACK NewLogger)
-
全局变量
Logger指向当前回调函数 -
如果
NewLogger为NULL,使用默认的NopLogger(空操作) -
回调函数类型:
ctypedef 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: ")
内部实现
- 如果当前状态与请求状态相同,直接返回
- 更新
Adapter->LogState(使用原子操作WriteULongNoFence) - 关闭日志 :如果从开启变为关闭且存在日志线程:
- 调用
CancelSynchronousIo取消阻塞的DeviceIoControl - 等待线程退出(最多 100ms 超时,循环取消)
- 关闭线程句柄
- 调用
- 开启日志 :如果从关闭变为开启且没有日志线程:
- 创建
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.h 和 logger.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 交互
NciSetConnectionName 和 NciGetConnectionName 通过 nci.def 定义的延迟导入函数调用系统 nci.dll(Network Connection Interface)。该 DLL 是 Windows 未公开的组件,用于管理网络连接文件夹中的连接名称和图标。
5. 日志资源管理
5.1 线程安全
LogState使用无栅栏原子操作(ReadULongNoFence/WriteULongNoFence)更新,因为线程间不需要严格的内存排序,只需确保值的可见性- 日志回调可能被多个线程并发调用(包括主线程和日志线程),应用层回调需自行处理同步
5.2 资源清理
在 WireGuardCloseAdapter 中:
- 调用
WireGuardSetAdapterLogging(Adapter, WIREGUARD_ADAPTER_LOG_OFF)停止日志线程 - 线程关闭后会等待线程退出,确保没有悬空句柄
5.3 错误恢复
- 如果设备被意外移除,日志线程尝试重新打开设备句柄;
- 若 10 次重试失败,自动关闭日志,避免无限循环;