(接上文)
二、核心代码逐段分析
2.5 收发线程(ReceivePackets & SendPackets)
2.5.1 接收线程
c
static DWORD WINAPI ReceivePackets(DWORD_PTR SessionPtr)
{
WINTUN_SESSION_HANDLE Session = (WINTUN_SESSION_HANDLE)SessionPtr;
HANDLE WaitHandles[] = { WintunGetReadWaitEvent(Session), QuitEvent };
while (!HaveQuit) {
DWORD PacketSize;
BYTE *Packet = WintunReceivePacket(Session, &PacketSize);
if (Packet) {
PrintPacket(Packet, PacketSize);
WintunReleaseReceivePacket(Session, Packet);
} else {
DWORD LastError = GetLastError();
switch (LastError) {
case ERROR_NO_MORE_ITEMS:
if (WaitForMultipleObjects(_countof(WaitHandles), WaitHandles, FALSE, INFINITE) == WAIT_OBJECT_0)
continue;
return ERROR_SUCCESS;
default:
LogError(L"Packet read failed", LastError);
return LastError;
}
}
}
return ERROR_SUCCESS;
}
关键点:
- 使用
WintunGetReadWaitEvent获取一个事件句柄,当环形缓冲区中有新数据时,该事件会被置位。 - 若
WintunReceivePacket返回ERROR_NO_MORE_ITEMS,则阻塞等待事件(或退出事件),避免忙等浪费 CPU。 - 收到包后,调用
PrintPacket打印信息,然后立即WintunReleaseReceivePacket释放缓冲区,以便驱动复用。
2.5.2 发送线程
c
static DWORD WINAPI SendPackets(DWORD_PTR SessionPtr)
{
WINTUN_SESSION_HANDLE Session = (WINTUN_SESSION_HANDLE)SessionPtr;
while (!HaveQuit) {
BYTE *Packet = WintunAllocateSendPacket(Session, 28);
if (Packet) {
MakeICMP(Packet);
WintunSendPacket(Session, Packet);
} else if (GetLastError() != ERROR_BUFFER_OVERFLOW)
return LogLastError(L"Packet write failed");
switch (WaitForSingleObject(QuitEvent, 1000 /* 1 second */)) {
case WAIT_ABANDONED:
case WAIT_OBJECT_0:
return ERROR_SUCCESS;
}
}
return ERROR_SUCCESS;
}
- 每秒构造并发送一个 28 字节的 ICMP Echo Request 包。
- 如果发送缓冲区已满(
ERROR_BUFFER_OVERFLOW),则静默丢弃该包(这是 Wintun 推荐的做法,避免阻塞)。 - 通过
WaitForSingleObject(QuitEvent, 1000)实现每秒一次发送,同时能及时响应退出信号。
2.6 主流程(main)
c
int __cdecl main(void)
{
// 1. 加载 Wintun DLL
HMODULE Wintun = InitializeWintun();
if (!Wintun) return LogError(L"Failed to initialize Wintun", GetLastError());
WintunSetLogger(ConsoleLogger);
Log(WINTUN_LOG_INFO, L"Wintun library loaded");
// 2. 创建退出事件和信号处理器
QuitEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
SetConsoleCtrlHandler(CtrlHandler, TRUE);
// 3. 创建适配器(指定固定 GUID)
GUID ExampleGuid = { 0xdeadbabe, 0xcafe, 0xbeef, { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef } };
WINTUN_ADAPTER_HANDLE Adapter = WintunCreateAdapter(L"Demo", L"Example", &ExampleGuid);
if (!Adapter) { /* 错误处理 */ }
// 4. 获取驱动版本并打印
DWORD Version = WintunGetRunningDriverVersion();
Log(WINTUN_LOG_INFO, L"Wintun v%u.%u loaded", (Version >> 16) & 0xff, (Version >> 0) & 0xff);
// 5. 配置 IP 地址(10.6.7.7/24)
MIB_UNICASTIPADDRESS_ROW AddressRow;
InitializeUnicastIpAddressEntry(&AddressRow);
WintunGetAdapterLUID(Adapter, &AddressRow.InterfaceLuid);
AddressRow.Address.Ipv4.sin_family = AF_INET;
AddressRow.Address.Ipv4.sin_addr.S_un.S_addr = htonl((10 << 24) | (6 << 16) | (7 << 8) | (7 << 0));
AddressRow.OnLinkPrefixLength = 24;
AddressRow.DadState = IpDadStatePreferred;
LastError = CreateUnicastIpAddressEntry(&AddressRow);
if (LastError != ERROR_SUCCESS && LastError != ERROR_OBJECT_ALREADY_EXISTS) { /* 错误处理 */ }
// 6. 启动 Wintun 会话(容量 4MiB)
WINTUN_SESSION_HANDLE Session = WintunStartSession(Adapter, 0x400000);
if (!Session) { /* 错误处理 */ }
// 7. 创建两个工作线程(收、发)
HANDLE Workers[2];
Workers[0] = CreateThread(NULL, 0, ReceivePackets, (LPVOID)Session, 0, NULL);
Workers[1] = CreateThread(NULL, 0, SendPackets, (LPVOID)Session, 0, NULL);
// 等待线程结束(直到收到退出信号)
WaitForMultipleObjectsEx(2, Workers, TRUE, INFINITE, TRUE);
// 8. 清理资源(逆序)
// 结束会话、关闭适配器、释放 DLL 等
}
流程总结:
- 动态加载
wintun.dll并获取所有函数指针。 - 设置日志回调,便于观察内部事件。
- 创建名为
Demo、类型为Example的 Wintun 适配器,指定 GUID(固定,便于测试)。 - 为适配器分配 IP 地址
10.6.7.7/24(使用CreateUnicastIpAddressEntry)。 - 启动会话,容量为
0x400000(4,194,304 字节,即 4 MiB),这是合理的中间值。 - 创建接收线程和发送线程,前者阻塞等待数据,后者每秒发送一个 ICMP 请求。
- 主线程等待线程结束(通过 Ctrl+C 触发),然后按顺序清理。
三、项目配置文件分析
3.1 example.vcxproj
xml
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>WindowsApplicationForDrivers10.0</PlatformToolset>
<ForcedTargetVersion>Windows10</ForcedTargetVersion>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\api</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>iphlpapi.lib;kernel32.lib;ntdll.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ProjectReference Include="..\api\api.vcxproj">
<Project>{897f02e3-3eaa-40af-a6dc-17eb2376edaf}</Project>
</ProjectReference>
</ItemGroup>
关键点:
- 使用
WindowsApplicationForDrivers10.0工具集,使得可以链接ntdll.lib并调用NtQuerySystemTime等原生 API。 - 添加
..\api到包含目录,以引用wintun.h头文件。 - 链接
iphlpapi.lib(CreateUnicastIpAddressEntry、InitializeUnicastIpAddressEntry)、ntdll.lib(NtQuerySystemTime)、ws2_32.lib(字节序转换函数)。 - 项目引用
api.vcxproj,确保在构建示例前先编译wintun.dll(虽然示例实际运行时动态加载,但项目依赖用于生成正确的输出目录)。
3.2 example.vcxproj.filters
只是一个标准的源文件筛选器,将 example.c 归入"Source Files"组,无特殊之处。
四、编译与运行
- 编译 :在 Visual Studio 中打开
wintun.sln,编译example项目(它会自动依赖api项目生成wintun.dll)。 - 运行前准备 :确保
wintun.dll与example.exe在同一目录(或系统目录)。如果驱动未安装,example.exe会通过WintunCreateAdapter自动安装(需要管理员权限)。 - 运行 :以管理员身份 打开命令提示符,执行
example.exe。控制台会显示日志,每秒发送一个 ICMP 请求,并在收到回复时打印。 - 停止:按 Ctrl+C 触发清理,程序退出。
预期输出示例:
2026-08-10 10:00:00.1234 [+] Wintun library loaded
2026-08-10 10:00:00.1256 [+] Wintun v0.14 loaded
2026-08-10 10:00:01.1278 [+] Sending IPv4 ICMP echo request to 10.6.7.8 from 10.6.7.7
2026-08-10 10:00:01.1301 [+] Received IPv4 ICMP echo reply from 10.6.7.7 to 10.6.7.8
...
五、设计评价与教学价值
5.1 优点
- 完整生命周期:从 DLL 加载、适配器创建、IP 配置、数据收发到优雅退出,覆盖了所有关键 API。
- 健壮的错误处理:每步操作都检查返回值,并通过日志输出详细错误信息。
- 多线程示范:展示了如何并发处理收发包,以及如何利用事件对象协调线程。
- 自包含测试:通过构造 ICMP 包并回环接收,无需外部设备即可验证驱动功能。
- 清晰的代码风格:函数职责单一,注释虽少但命名自解释。
5.2 教学意义
- 可作为新手入门 Wintun 的第一份代码,快速理解 API 用法。
- 展示了 Windows 网络编程中的常见模式:延迟加载 DLL、错误码格式化、控制台信号处理、IP 地址配置。
- 可扩展为实际 VPN 客户端的骨架:只需修改
MakeICMP为隧道数据封装,并添加路由表操作。
5.3 潜在改进点
- 发送线程固定每秒一个包,实际应用可能需要动态调整速率。
- 未处理 IPv6 情况(虽然接收端支持,但发送端只构造 IPv4)。
- 未展示如何设置默认网关或路由表,这对 VPN 是必需的。
六、总结
example 文件夹虽然仅包含一个源文件,但它浓缩了 Wintun 使用的所有精华。它不仅是项目的"门面示例",更是 WireGuard 团队为开发者精心准备的可工作的最小原型。通过这个示例,开发者可以快速搭建起自己的隧道应用,而无需从头研究 Windows 网络栈的复杂细节。
评价:这是一个教科书级别的示例程序,兼具实用性和教学性,值得所有 Windows 网络开发者学习和借鉴。