教程 MCBE下基于UDP的RakNet通讯协议原理略谈及利用Python.Socket实现状态查询

---引言---

各基岩服服主在运维自己的服务器时往往想要不用打开客户端就可以查看自己服务器的状态。这种想法已经成功实现了,并创造出了api供各位服主直接调用。
一般情况下,服主们需要查询的基本信息有:服务器名字,服务器在线人数,服务器最大人数,服务器的在线状态,服务器内游戏模式。
这些信息足以让服主判断服务器是否运作正常,这些数据甚至可以公开用于网站供各位网站访客判断该服务器游玩的人数多不多。
但您是否想过,在未来的某一天,这些api作者因没饭恰将api关闭了?或因api管理不当将您的服务器地址泄露出去,成为某站的百D名单?您可想想,那些api可以轻易的通过您服务器地址的端口获取到服务器信息,一旦因正义出击却遭遇报复攻击时,您当初想的开着玩玩,并未购买超10T的超高防抗D防御集群的服务器将会原地暴毙。
那我们是否能在自己已联网的设备上查询服务器信息,这样即使api关闭,查询依然能正常使用。笔者在各大论坛尝试寻找相关的文章,却只有成品,却一篇教程,甚至连简单的介绍都没有。因此这篇文章诞生了。


1.MCBE服务器使用什么协议与客户端通信

MCBE服务器与客户端通信使用的是RakNet协议。

2.什么是RakNet

1623569047417.png

RakNet(官网)是一个基于UDP网络传输协议的C++网络库,允许程序员在他们自己的程序中实现高效的网络传输服务。通常情况下用于游戏,但也可以用于其它项目。


RakNet有以下特点:
l 高性能 在同一台计算机上,RakNet可以实现在两个程序之间每秒传输25,000条信息;
l 容易使用 RakNet有在线用户手册,视频教程。每一个函数和类都有详细的讲解,每一个功能都有自己的例程;
l 跨平台,当前RakNet支持Windows, Linux, Macs,可以建立在Visual Studio, GCC, Code,Blocks, DevCPP 和其它平台上。
l 在线技术支持 RakNet有一个活跃的论坛,邮件列表,你只要给他们发信,他们可以在几小时之内回复你。
l 安全的传输 RakNet在你的代码中自动使用SHA1, AES128, SYN,用RSA避免传输受到攻击
l 音频传输 用Speex编码解码,8位的音频只需要每秒500字节传输。
l 远程终端 用RakNet,你能远程管理你的程序,包括程序的设置,密码的管理和日志的管理。
l 目录服务器 目录服务器允许服务器列举他们自己需要的客户端,并与他们连接。
l Autopatcher Autopatcher系统将限制客户端传输到服务端的文件,这样是为了避免一些不合法的用户将一些不合法的文件传输到服务端。
l 对象重载系统
l 网络数据压缩 BitStream类允许压缩矢量,矩阵,四元数和在-1到1之间的实数。
l 远程功能调用
l 强健的通信层 可以保障信息按照不同的信道传输
(来自
百度百科

总的来说RakNet就是基于UDP的高效、安全、低量的通信协议。
基岩版Minecraft开发所使用的语言用的是C++,RakNet也是使用C++开发的,正好RakNet为游戏传输而生,于是MOJANG直接就拿来用了(应该是吧:tieba-25:


3.协议分析

由协议可知,RakNet是基于传输层的UDP。那么什么是传输层,除了传输层还有其他层吗?
1623569496090.png
上图所示的层次结构就是现在常说的OSI参考模型。它是国际标准化组织(ISO)提出的一个标准框架,定义了不同计算机互连的标准,目前是使世界范围内的各种计算机互连起来,构成一个网络。
OSI框架是基于1984连国际标准化组织(ISO)发布的ISO/IEC 7498标准。该标准定义了网络互联的7层框架。
这7层框架自下而上依次为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
而在网络传输中常见的TCP/UDP协议就在OSI框架的传输层。下图所示的协议层次结构是ARPANET和其后续的因特网使用的参考模型。



05225723-2ffa89aad91f46099afa530ef8660b20.jpg
其中,夹在应用层和运输层中间的Socket抽象层,就是常说的套接字,其作用对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。
用个易懂的表达方式就是:如果IP地址是127.0.0.1,而端口号是19132,那么得到套接字就是(127.0.0.1:19132)。下图是最常用的socket连接方式。


05232335-fb19fc7527e944d4845ef40831da4ec2.png

4.解析数据包

BDS服务器的开服门槛很低,基本上有台运行Win/Linux的联网计算机设备就可以直接开服,玩家可通过局域网进入服务器。接入公网的设备甚至可以开放给全球任何地方的玩家进入游玩。所以我们在本地就可以进行抓取服务端到客户端的数据包进行分析。这里使用WireShark对本地虚拟网卡进行抓包。使用虚拟网卡在过滤清洗我们需要的数据时不会太痛苦。WireShark主界面.png

在此之前,您需要:解除win10MC的本地环回限制;预先填写本地服务端的地址及端口。

我们先开启服务端,之后客户端,最后再开启抓包。等待客户端的服务器界面显示出服务器状态信息即可停止抓包。

服务端启动界面.png

过滤器输入“raknet”即可挑出我们所需要的包
抓包界面及客户端.png
BDS在运行的过程中会不断向配置文件中设定的端口发送查询数据包(类似于websocket的心跳),配置文件中设定的端口一共有四个,一组IP4,一组IP6,其中一主一备。在这里,客户端连接的是BDS下IP4的主端口19132,因此在筛选时一定要看清楚客户端请求的端口。
这里我们挑出3个包来看看相同及不同的地方。

包4

包4.png

包8​

包8.png
包11
包11.png


依三个包来看,最明显的区别就是在RakNet Time since start(ms)项发生了变化,且按包顺序逐渐增加。
在后续的测试中,我们也发现RakNet Client GUID项在每次启动时都会改变,但其值作用仅用于区分连接设备。因此该值可固定,但必须唯一。
我们再来看看数据报架构:

Ping数据包解析:
  MCBE服务器使用以UDP通信协议为基的RakNet进行通信

            # 01(1字节):     RakNet offline message ID              :01(固定)

            # 02-09(8字节):  RakNet Time since start(ms)         :00 00 00 00 00 09 70 2f(会变,此处举例)

            # 10-24(14字节): RakNet Offline message data ID : 00 ff ff 00 fe fe fe fe fd fd fd 12 34 56 78(固定)

            # 25-33(8字节): RakNet Client GUID                    : 81 0a 79 74 26 4c 03 f7(会变,此处举例)

该数据报核心内容共33字节,内容为16进制文本,其他内容在您创建socket并连接时,socket会自动帮您创建,其中包含目标地址及端口。
在RakNet文档中对此类数据包定义为“Unconnected_Ping”,定义就在数据包的头,"RakNet offline message ID"(0X01)。
若成功连接,RakNet服务端必将返回一个回应数据包,其中就包含整篇文章一直在探讨的服务器状态信息。此类数据包被定义为“Unconnected_Pong”
所以我们在使用socket发送数据包时,我们只需修改"RakNet Time since start(ms)"和"RakNet Client GUID",其他原样发送即可。


5.使用Python.Socket实现​

现在知道了数据包的构造,那手动构建socket数据包根本不成问题。
我们再来看一样socket的发送方式:

05232335-fb19fc7527e944d4845ef40831da4ec2.png
可由此图写出20行的状态信息获取代码:
获取MCBE服务器信息:
from socket import *
import uuid
import time # 导入包
addr = ("*.*.*.*", 19132) #定义目标地址及端口
uuid = str(uuid.uuid1()).upper().split("-") # 生成GUID,在C++中此值称为GUID,在Python中却叫UUID
time = int((time.perf_counter())*10000000) # 获取程序已启动时间,精准至小数后5位
tickcounttime = '%016d' % time  # 将启动时间填充零至16字节
str = "01"+str(tickcounttime) + "00FFFF00FEFEFEFEFDFDFDFD12345678" + uuid[2]+uuid[4] # 缝合数据包
str1 = bytes.fromhex(str) # 将数据包转换为16进制
try: # 尝试连接
    UDPC = socket(AF_INET, SOCK_DGRAM) #定义socket为UDP 内部大写字符为socket的特征码,详情请查阅相关资料
    socket.settimeout(UDPC, 5) # 设置超时时间为5秒
    socket.connect(UDPC, addr) # 连接远程地址
    UDPC.sendto(str1, addr) # 发送数据包
    rec = socket.recvfrom(UDPC, 1024) # 设置接收缓冲区
    print(rec) #输出返回内容 内容仍为字节编码
    #---数据清洗---#
except: # 尝试失败后执行
    print("连接超时")
UDPC.close() # 关闭socket

其中:
1.Python中UUID有4种不同的生成类型(
有什么区别?),此处使用的是UUID.UUID1(),此类UUID以“-”分割的第三组及第五组数据是依照使用主机MAC地址,序列号和当前时间生成的UUID。此版本使用IEEE 802 ,具有唯一性。
2.在第16行“socket.connect()”中,在不同网络下使用的函数不同。
面向互联网使用:"socket.connect()"(连接);
面向局域网使用:“socket.bind()”(绑定);

此处是个大坑,笔者曾经在此处头疼了很久...


数据清洗阶段懒得写了,目前还用不上,有需要时再补全!(理直气壮)。您可以使用split()函数对返回内容的“;”进行分割。
我会在评论中附上传回结果,因为这里附件满了..

服务器传送门>>>>>EFC Club<<<<<:evil:

转载此文章请附上原链接
 
最后编辑:

doupoa

【 Lv: 2 】
新闻组
2020/04/18
47
43
47
4
东莞
黄金
金粒16,547粒
1623580920299.png

依文本顺序来看:​

"MCPE"前的内容有:

数据报头(0x1c);
Ping Id;
Server_Id;

"MCPE"后的内容有:

游戏 Id: MCPE;
服务器名:Enjoy Freedom Creative;
通信协议:431;
游戏版本:1.16.220;
已连接玩家:1;
可连接最多玩家:20;
Hash(哈希):11789239690462598426
M.O.T.D(存档名):Bedrock level
游戏模式:Survival
是否在线:1(真)
 

新主题 新资源 新回帖