使用Java实现魔兽争霸3冰封王座联网-进阶

514 阅读9分钟

背景

之前写了一篇实现非局域网下玩魔兽争霸3冰封王座,这个是通过特定端口转发流量的,只是针对这个游戏有一定的局限性。现在想实现的是打通所有的局域网游戏,也是通过魔兽争霸3冰封王座分析,本来想用其他游戏,其他的局域网游戏不知道从哪里下载。

技术

Wintun的使用可以看看我之前写的文章,这里用比较简单不用完全解析TCP和UDP数据包,就对数据包上传和下发。
Java 就用 AIO 和 虚拟线程。
感觉AIO+虚拟线程相对NIO并没有什么优势,AIO会占内存更多,每个连接的都要分配byte[1024]读写。
而NIO就可以只用一个byte[1024]用一个线程管理。

分析

魔兽争霸3冰封王座局域网连接大致流程:本地打开魔兽争霸3,点击局域网这时候,就会发一个查找房间的UDP广播,同一个局域网的魔兽争霸3创好了房间,收到查房的UDP广播,就会发一个带房间信息的UDP连接,回应这个UDP广播。本地魔兽争霸3收到房间信息,并且得到了目标的IP,显示到房间列表中。当我们点击这个房间,就会通过TCP连接目标IP然后数据的交换。

QQ20241219-013939.png

大致是通过UDP广播找到可以加入的游戏房间和目标IP,再通过TCP去连接两个游戏端。

要怎么抓这个UDP广播呢,之前是通过Wireshark抓包可以抓到是6112端口发广播,再通过Java的UDP监听这个6112,这样有一个局限性,如果是别的游戏呢,可能是发别的端口呢,Java中又不能监听所有端口。

我们可以通过wintun创建一个网络适配器抓到这个广播,优先级会高些,我之前认为他会给每个网络适配器都发送UDP广播,实际上他是选择中优先级高的,通过cmd 的 route print 可以查看优先级如下:

  IPv4 路由表
===========================================================================
活动路由:
网络目标        网络掩码          网关       接口   跃点数
          0.0.0.0          0.0.0.0      192.168.1.1    192.168.1.100    311
        127.0.0.0        255.0.0.0            在链路上         127.0.0.1    331
        127.0.0.1  255.255.255.255            在链路上         127.0.0.1    331
  127.255.255.255  255.255.255.255            在链路上         127.0.0.1    331
     172.18.192.0    255.255.240.0            在链路上      172.18.192.1    271
     172.18.192.1  255.255.255.255            在链路上      172.18.192.1    271
   172.18.207.255  255.255.255.255            在链路上      172.18.192.1    271
       172.29.1.0    255.255.255.0            在链路上        172.29.1.1    261
       172.29.1.1  255.255.255.255            在链路上        172.29.1.1    261
     172.29.1.255  255.255.255.255            在链路上        172.29.1.1    261
      192.168.1.0    255.255.255.0            在链路上     192.168.1.100    311
      192.168.1.0    255.255.255.0            在链路上      172.18.192.1     16
    192.168.1.100  255.255.255.255            在链路上     192.168.1.100    311
    192.168.1.255  255.255.255.255            在链路上     192.168.1.100    311
    192.168.1.255  255.255.255.255            在链路上      172.18.192.1    271
        224.0.0.0        240.0.0.0            在链路上         127.0.0.1    331
        224.0.0.0        240.0.0.0            在链路上     192.168.1.100    311
        224.0.0.0        240.0.0.0            在链路上      172.18.192.1    271
        224.0.0.0        240.0.0.0            在链路上        172.29.1.1    261
  255.255.255.255  255.255.255.255            在链路上         127.0.0.1    331
  255.255.255.255  255.255.255.255            在链路上     192.168.1.100    311
  255.255.255.255  255.255.255.255            在链路上      172.18.192.1    271
  255.255.255.255  255.255.255.255            在链路上        172.29.1.1    261
===========================================================================

上面可以看到目标255.255.255.255的跃点数最低是261优先级高,目标是172.29.1.1,再通过cmd的ipconfig查看到是wintun的test网络适配器如下:

C:\Users\22787>ipconfig

Windows IP 配置


未知适配器 Test:

   连接特定的 DNS 后缀 . . . . . . . :
   本地链接 IPv6 地址. . . . . . . . : fe80::e0b2:fc62:c72b:c9a8%36
   IPv4 地址 . . . . . . . . . . . . : 172.29.1.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . :

通过控制面板\网络和 Internet\网络连接也可看到Test网络适配器如下:

QQ20241219-014641.png

我们再通过Wireshark抓Test网络适配器的包如下,可以看到我们要的6112广播包:

QQ20241219-014344.png

我们抓一个进入房间的完整的数据包,如下:

QQ20241219-022511.png

看到完整的数据包,实际上我们Test网络适配器也会收到像上面进来的数据包,只是少出去的数据包,这个需要我们自己封装装写入到网路适配器中。先记录一下房间信息的数据包,如下:

f7309a00505833571b000000030000008b1cfa32e5bd93e59cb0e5b180e59f9fe7bd91e58685e79a84e6b8b8e6888f2028e8bf0000010349070101a101b94901138fe76f4d8b6171735d293329ad436f6f757943613b792f77336d01e99fbfbde9a38fe5b90f8be5bf870101778be33b25fd815d43397739539341d1731dadcb3f7721000200000009000000010000000200000063020000e017

可以发现当你在应用监听了0.0.0.0:6112端口后,等于监听了其他的网络设配器(本地连接,Wintun的)的6112端口。例如: Wintun的IP是172.29.1.1,你往172.29.1.1:6112发数据,上面0.0.0.0也是能监听到。

简单的代码实现

实现:通过Java简单给魔兽争霸添加虚拟的房间。

下面代码比较简单有4个方法:

  • mian:循环接收Test网络适配器的数据包,解析数据包判断是UDP且端口是6112,认为是查询房间信息,然后写入3个房间信息到Test网络适配器
  • buildUdpPacket:构建UDP数据包。
  • writeData:将byte[]写入网络适配器。
  • hexStringToByteArray:将16进制字符串转成byte[],主要用于将上面记录的房间信息转成byte[]。

public class TestMain {

    public static void main(String[] args) throws Exception {

        String name = "Test";
        String tunnelType = "Wintun";
        //创建网络适配器
        Pointer adapterHandle = WintunLibrary.INSTANCE.WintunCreateAdapter(new WString(name), new WString(tunnelType), null);

        if (adapterHandle == null) {
            throw new RuntimeException("网络适配器创建失败!(可能没用管理员启动)");
        }

        //设置网络设配器ip 和 子网掩码
        WintunExtensionLibrary.INSTANCE.CreateAddressRow(adapterHandle,"172.29.1.1",24);

        //启动适配器
        Pointer sessionHandle = WintunLibrary.INSTANCE.WintunStartSession(adapterHandle, 0x400000);

        var t = System.currentTimeMillis();
        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 UdpPacket){
                        var udpPacket = (UdpPacket) packet.getPayload();
                        var dstPort = udpPacket.getHeader().getDstPort().valueAsInt();
                        //认为查找房间
                        if(dstPort == 6112){
                            //写入房间信息
                            var data = hexStringToByteArray("f7309a00505833571b000000030000008b1cfa32e5bd93e59cb0e5b180e59f9fe7bd91e58685e79a84e6b8b8e6888f2028e8bf0000010349070101a101b94901138fe76f4d8b6171735d293329ad436f6f757943613b792f77336d01e99fbfbde9a38fe5b90f8be5bf870101778be33b25fd815d43397739539341d1731dadcb3f7721000200000009000000010000000200000063020000e017");
                            //房间1
                            var packetData1 = buildUdpPacket(packet,InetAddress.getByName("172.29.1.10"),data);
                            writeData(sessionHandle,packetData1);
                            //房间2
                            var packetData2 = buildUdpPacket(packet,InetAddress.getByName("172.29.1.11"),data);
                            writeData(sessionHandle,packetData2);
                            //房间3
                            var packetData3  = buildUdpPacket(packet,InetAddress.getByName("172.29.1.12"),data);
                            writeData(sessionHandle,packetData3);

                        }
                    }

                } 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);
                }
            }

        }
    }

    private static byte[] buildUdpPacket(IpPacket ipPacket,InetAddress dstAddrNew, byte[] data) throws UnknownHostException {
        InetAddress srcAddr = ipPacket.getHeader().getSrcAddr();
        InetAddress dstAddr = dstAddrNew;

        UdpPacket udpPacket = (UdpPacket)ipPacket.getPayload();
        UnknownPacket.Builder unknownBuilder = new UnknownPacket.Builder();
        unknownBuilder.rawData(data);
        //创建一个udp包
        UdpPacket.Builder udpBuilder = new UdpPacket.Builder()
                .srcPort(udpPacket.getHeader().getDstPort())
                .dstPort(udpPacket.getHeader().getSrcPort())
                .payloadBuilder(unknownBuilder)
                .correctLengthAtBuild(true);



        //创建一个ip包
        IpPacket newIpPacket = new IpV4Packet.Builder()
                .version(IpVersion.IPV4)
                .ttl((byte) 45)
                .tos(IpV4Rfc1349Tos.newInstance((byte) 0))
                .protocol(IpNumber.UDP)
                .srcAddr((Inet4Address) dstAddr)
                .dstAddr((Inet4Address) srcAddr)
                .correctLengthAtBuild(true)
                .correctChecksumAtBuild(true)
                .payloadBuilder(udpBuilder)
                .identification((byte) 1000)
                .build();

        return newIpPacket.getRawData();
    }

    private static void writeData(Pointer pointer,byte[] data) {

        Pointer packetPointer = WintunLibrary.INSTANCE.WintunAllocateSendPacket(pointer, data.length);

        if (packetPointer != null) {

            packetPointer.write(0, data, 0, data.length);

            // 发送数据包
            WintunLibrary.INSTANCE.WintunSendPacket(pointer, packetPointer);

        } else {
            System.err.println("Failed to allocate send packet.");
        }
    }

    public static byte[] hexStringToByteArray(String s) {
        s = s.replaceAll(" ", "");
        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;
    }
}

运行实现效果

可以看见多出了三个房间,效果如下:

QQ20241219-034702.png

然后随便点一个房间进入,发现是加不进的,因为那几房间都是伪造的。

QQ20241219-034902.png

看看抓包情况,和我们之前想的一样,房间信息有目标IP,点击房间时会去TCP进行连接。

QQ20241219-035557.png

代码:test · 断续/net-connect - 码云 - 开源中国

实现思路流程

通过上面的分析和代码,我们可以知道魔兽争霸3操作都会发送UDP和TCP数据包,然而这些数据包我们可以通Wintun将他们拦截下,我们可以给每台电脑都装Wintun应用将拦截的数据包通过TCP上传到服务端,根据目标IP分发到其他的Wintun应用中,如果是255.255.255.255广播就发给所有Wintun应用。每个Wintun应用启动时,就得连接服务器获取IP地址。这样一来我们只用帮忙转发数据包就可以了,如图下UDP查房流程图:

QQ20241219-213445.png

TCP也是和上面一样,我们只需要把客户端A的网络适配器的数据包写到把客户端B的网络适配器里,然后再从B端写到A端,反反复复。数据的交换通过客户端连接服务端,服务端在分发下去。

代码比我之前写的简单多了,我就不粘贴出来了,感兴趣的可以看下,就Wintun拦截数据包,通过Java的AIO上传和下发数据包,主要一个客户端和一个服务端。

客户端是要通过管理员运行的,服务端是部署到服务器上的,我已经部署了一个在45.207.49.157:9999上,不知道能运行多久。

完整代码: net-connect: 局域网连接 - Gitee.com

总结

之前还觉得非局域网下游戏连接很麻烦,要去分析数据包,实现三次握手四次挥手等。通过上面的分析代码还比较简单就能实现的。