背景分析
之前基于是端口转发(UDP/TCP 6112端口)的联机方案存在明显局限性:
- 仅适用于特定游戏(魔兽争霸3)
- 无法适配其他局域网游戏的端口需求
- 网络扩展性差
解决方案:采用虚拟网络适配器技术,在数据链路层实现全流量拦截与转发,突破端口限制,支持任意局域网游戏。
技术架构
核心组件
技术 | 作用 | 优势 |
---|---|---|
Wintun | 创建虚拟网络适配器 | 高性能Windows驱动级网络接口 |
JNA | 原生调用wintun.dll | 避免JNI开发复杂度 |
Pcap4j | 原始网络数据包处理 | 跨平台数据包解析能力 |
实现原理
- 虚拟网络层:通过Wintun创建虚拟网卡
- 流量劫持:捕获原始局域网广播包
- 隧道封装:通过TCP实现跨网络传输
环境准备
- Wintun 下载地址(需管理员权限运行)
- 项目依赖如下:
我们使用 JNA 调用本地 DLL 接口,使用 Pcap4j 处理 IP、UDP 和 TCP 层的数据包,确保可以以字节流方式分析和重构游戏通信数据。
<!-- JNA 用于调用本地 DLL -->
<dependencies>
<!-- JNA 用于调用本地 DLL -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.10.0</version>
</dependency>
<!-- Pcap4j 核心库,用于抓包和构造数据包 -->
<dependency>
<groupId>org.pcap4j</groupId>
<artifactId>pcap4j-core</artifactId>
<version>1.8.2</version>
</dependency>
<dependency>
<groupId>org.pcap4j</groupId>
<artifactId>pcap4j-packetfactory-static</artifactId>
<version>1.8.2</version>
</dependency>
</dependencies>
<!-- 打包 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version> <!-- 请检查是否有更新版本 -->
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<!-- 添加 Main-Class 属性到清单文件 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.example.ClientMain</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
以下是使用 JNA 对 wintun.dll 的函数声明,封装为 WintunLibrary 接口。它定义了创建适配器、读取数据包、发送数据包、启动/关闭会话等常用操作。
public interface WintunLibrary extends Library {
WintunLibrary INSTANCE = (WintunLibrary) Native.load("wintun", WintunLibrary.class);
Pointer WintunCreateAdapter(WString name, WString tunnelType, Pointer requestedGUID);
Pointer WintunStartSession(Pointer sessionHandle, int capacity);
Pointer WintunReceivePacket(Pointer sessionHandle, IntByReference packetSize);
void WintunSendPacket(Pointer sessionHandle, Pointer packet);
void WintunReleaseReceivePacket(Pointer sessionHandle, Pointer packet);
Pointer WintunGetReadWaitEvent(Pointer sessionHandle);
void WintunGetAdapterLuid(Pointer sessionHandle, Pointer luid);
Pointer WintunAllocateSendPacket(Pointer sessionHandle, int packetSize);
void WintunCloseAdapter(Pointer sessionHandle);
}
基础运行代码。
public static void main(String[] args) throws IOException {
String name = "Test";
String tunnelType = "Wintun";
//创建网络适配器
Pointer adapterHandle = WintunLibrary.INSTANCE.WintunCreateAdapter(new WString(name), new WString(tunnelType), null);
if (adapterHandle == null) {
throw new RuntimeException("网络适配器创建失败!(可能是没用管理员启动)");
}
//启动适配器
Pointer sessionHandle = WintunLibrary.INSTANCE.WintunStartSession(adapterHandle, 0x400000);
setIP("Test", "172.29.1.1", "255.255.255.0");
while (true){
//读取适配器数据包
IntByReference incomingPacketSize = new IntByReference();
Pointer incomingPacket = WintunLibrary.INSTANCE.WintunReceivePacket(sessionHandle, incomingPacketSize);
if (incomingPacket != null) {
try {
int packetSize = incomingPacketSize.getValue();
byte[] packetBytes = incomingPacket.getByteArray(0, packetSize);
//解析数据包
IpPacket packet = (IpPacket) IpSelector.newPacket(packetBytes, 0, packetBytes.length);
System.out.println(packet);
if(packet.getPayload() instanceof TcpPacket || packet.getPayload() instanceof UdpPacket){
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
WintunLibrary.INSTANCE.WintunReleaseReceivePacket(sessionHandle, incomingPacket);
}
} else {
int lastError = Native.getLastError();
if (lastError == 0x103) {
//没数据等待数据
Pointer readWaitEvent = WintunLibrary.INSTANCE.WintunGetReadWaitEvent(sessionHandle);
} else {
WintunLibrary.INSTANCE.WintunCloseAdapter(sessionHandle);
throw new RuntimeException("数据包读取失败,错误码:"+lastError);
}
}
}
}
public static void setIP(String interfaceName, String ipAddress, String subnetMask) throws IOException {
String command = String.format("netsh interface ip set address name=\"%s\" static %s %s",
interfaceName, ipAddress, subnetMask);
Process process = Runtime.getRuntime().exec(command);
try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static byte[] hexStringToByteArray(String s) {
s = s.replaceAll("\\s+", "");
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
// 将每两个字符转换成一个字节
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
简要流程说明
本项目整体思路如下:
- 客户端连接服务端,获取分配的 IP 地址;
- 客户端使用 Wintun 创建虚拟网络适配器,并通过
netsh
设置 IP 和子网掩码; - 由于虚拟网卡的跃点数最小,当游戏进行广播时,广播数据将优先发往该适配器;
- Java 程序监听该适配器的数据包,捕获游戏广播信息并发送至服务器;
- 服务器接收后将广播重新分发到其他客户端;
- 客户端将收到的广播写入本地适配器;
- 游戏监听
0.0.0.0
地址,因此可以接收到适配器发出的广播包; - 游戏之间完成握手,后续 TCP 和 UDP 数据也通过该流程完成传输。
⚠️ 注意:Wintun 创建的适配器默认不分配 IP,需要手动配置。
流程图展示
下图展示了 UDP 的通信流程,TCP 流程与之类似:
该方法不仅适用于《魔兽争霸3》,也可以推广至其他所有使用局域网联机的游戏。只要确保虚拟适配器跃点最小,并正确处理流量的转发,即可实现真正的跨网络“虚拟局域网”体验。