制作一个自己的对战平台

来源:互联网 发布:剑灵人族剑士捏脸数据 编辑:程序博客网 时间:2024/04/29 17:09

缘起

因为在浙大,物理网卡的地址被分配为222.205.XX.XX,但是子网掩码是255.255.255.0,这样的话虽然大家都在一个局域网里面,但是却不一定在同一个子网。
局域网联机游戏为了发现局域网中的主机,会发送广播包,有些局域网联机游戏,会发送到255.255.255.255这个广播地址(典型代表War3),但是这个广播地址
是只能广播到子网的,路由器默认不转发,这样就造成了我们同在校园网却无法联机的问题。

虚拟局域网

想了想解决方案,可能使用虚拟网卡做一个虚拟局域网是一个解决方案,于是我安装了OpenVPN,然后使用其tap0901网卡驱动,可以读取注册表
获取tap0901设备的实例UUID以及显示在网络和共享中心的那个网络名称:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}
这个里面有很多项,代表了多个网卡接口,其中会有一个是tuntap设备,通过类似这样的地址HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{4d36e972-e325-11ce-bfc1-08002be10318}\0002下的ComponentIdtap0901来确定tap网卡,然后读取该项下面的netCfgInstanceId,就是tap网卡的UUID,得到这个UUID之后,
可以使用CreateFile函数来打开一个tuntap设备:

HANDLE f = CreateFile(L"\\\\.\\Global\\{...}", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM | FILE_FLAG_OVERLAPPED, NULL);

打开这个设备后,就可以对其发送指令了,参照tuntap的驱动源代码tap-windows6,可以获得一些宏,以及一些指令的参数信息,然后使用
DeviceIoControl发送到设备:

DeviceIoControl(f, TAP_IOCTL_SET_MEDIA_STATUS, config, 4, config, 4, &returnLen, NULL);

启动tuntap设备之前,要考虑到单机游戏发送广播包,所以应该修改路由表,让255.255.255.255路由经过我们的tuntap设备,以及作为虚拟局域网,每个tuntap设备应该有一个虚拟ip,这里
假设为192.168.1.10。设置IP的windowsAPI在win7之后的系统就没有了,所以我也只能使用调用netsh命令行的方式来设置ip,然后使用GetIpForwardTable2来得到路由表信息,使用
DeleteIpTableEntry2来删除其他路由表,以及通过NotifyRouteChange获得路由表改变的通知,收到后去再次修改路由表,另外在退出时恢复之前的路由表。做好这一步之后,
我们要做的就是转发了。

读取设备上的帧,使用异步读取文件的ReadFile即可:

if (ReadFile(hFile, buff, 1500, &read, &ol) != FALSE) {    if (!readHandler(std::string((char*)buff, read))) break;}else if ((errNo = GetLastError()) == ERROR_IO_PENDING) {    switch (WaitForMultipleObjects(2, handles, false, INFINITE)) {    case WAIT_OBJECT_0:        if (!readHandler(std::string((char*)buff, ol.InternalHigh))) break;        break;    case WAIT_OBJECT_0 + 1:        return;    }}else {    if (errorHandler != nullptr)        errorHandler(errNo);    break;}

这段代码因为处于子线程中,所以还有等待线程退出信号的部分,用了WaitForMultiObjects。我使用的是tap模式,是二层设备,收到的是以太网帧,所以要有以太网帧的解析:

bool EthernetIIPacket::Parse(const char* raw, int length) {    if (length < 14) return false;    memcpy(this->srcMac, raw + 6, 6);    memcpy(this->dstMac, raw, 6);    this->protocol = Protocol(ntohs(*(unsigned short*)(&raw[12])));    this->userDataLen = length < 1514 ? length - 14 : 1500;    memcpy(this->userData, raw + 14, this->userDataLen);    return true;}

然后解析出上层协议类型,如果是IPv4,那么就要对IPv4进行解析:

bool IPv4Packet::Parse(const char* raw, int length) {    if (length < 20) return false;    this->protocol = Protocol((BYTE)(raw[9]));    memcpy(this->srcIp, raw + 12, 4);    memcpy(this->dstIp, raw + 16, 4);    this->userDataLen = length < 1500 ? length - 20 : 1480;    memcpy(this->userData, raw + 20, this->userDataLen);    return true;}

这里只需要解析出我们感兴趣的部分,然后判断,如果是广播地址,那么对所有加入虚拟局域网的设备,使用能够通讯的网卡(这里是校园网VPN)进行通讯,
我需要找到校园网的网卡,使用GetIpForwardTable2找到跃点数最低的默认路由,一定是现在活跃的网络连接,然后再用GetIpAddrTable获得其IP地址,
用来绑定socket到指定网卡。之后就是转发读取到的包了,我这里使用UDP直接将IP包传出去。

之后,为了能够收到其他端从UDP传入的数据,我们需要侦听校园网的IP地址,然后收到包之后将UDP的内容(原始的IP报文)封装到以太网帧中,发送回设备,
但是以太网帧需要知道自己和发送方的MAC地址,这就得使用GetIfTable2来获取网络接口信息了,将tuntap的nac地址填充进去,然后使用WriteFile发送
回设备。

但是这时,我发现主机虽然给后来加入的计算机发送了地图信息,但是之后就没有任何通信了,于是检查了下发包。发现有大量寻找192.168.1.8192.168.1.1
的ARP包(因为测试使用的两台计算机的虚拟IP分别为192.168.1.10,192.168.1.8。原来socket在发送前,会先检索目的IP地址对应的mac是不是在自己mac
表里面,如果不在,那么就通过ARP协议去询问,如果还询问不到,那就去询问网关的,如果搞不定,那么只能无动于衷,也就是不发包了。所以,我又额外实现了一套
ARP协议:

bool IPv4ARPPacket::Parse(const char* data, int length) {    if (length < 28) return false;    this->opCode = data[7];    memcpy(this->senderMac, data + 8, 6);    memcpy(this->senderIpAddress, data + 14, 4);    memcpy(this->targetMac, data + 18, 6);    memcpy(this->targetIpAddress, data + 24, 4);    return true;}

完善ARP协议之后,就可以顺利联机啦!另外注意360可能会报病毒,程序要以管理员身份运行(可以修改链接属性,出现UAC标识),以及关闭windows防火墙
(windows防火墙会拦截不明UDP包,即不请自来的UDP包)。完整代码参见github

![WAR3]

0 0
原创粉丝点击