远控编程
远控大家都知道最重要的就是框架的编写,框架不好,后面加功能会很累人,很多时候,可能会因为添加一个功能点,就要对框架进行修改,修改后可能所有的功能都要测试,耗时耗力
尤其是对于我这种半路出家的人来说,很折磨人。
因为项目原因,代码不能放出来,我会尽可能的说明问题现象,以及我在处理这些问题时的方法。
基本流程:
建立连接 - 数据加密 - 数据发送 - 数据接收 - 数据解密 -发送到模块 - 不同类型的处理
1、多协议的处理:
很多项目可能不止要求一种协议tcp,udp,http,https,icmp 等等,不管是哪种协议,无非就是 建立连接,数据发送,数据接收。我之前写的时候,会存在一个比较长的switch ... case ... 结构,用来对不同的协议进行区分,不同的协议调用不同的函数,大概如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
void myrecv()
{
switch (协议类型)
{
case tcp:
.......
case udp:
..
}
}
void mysend()
{
switch()
{
case tcp:
...
case udp:
..
}
}
|
这样我每次在添加一种新的协议的时候,都需要对这些switch 进行维护。
这里可以对连接本身进行一种抽象,主框架代码中用类似这样的一种结构来进行编程,代码最开运行的时候,对整个结构体进行初始化就可以了。
1
2
3
4
5
6
7
8
|
myconnect
{
socket sockfd;
int
protype;
void (
*
pconnect)(ip,port);
void (
*
pmysend)(void
*
data,
int
len
);
void (
*
pmyrecv)(void
*
data,
int
len
);
}
|
2、tcp 的粘包 分包问题
粘包和分包问题并不是tcp协议的问题,而是程序员使用tcp协议时,需要主动了解的机制问题。tcp 是流式套接字,数据在发送的时候是以字节流的形式进行发送的,字节流是无界的。举个例子,客户端调用了两次send 函数,每次send 一个字节,控制端 调用recv 进行接收,他可能一次接收就接收到了两个字节,这个就是粘包;客户端一次发送了 100 个字节,控制端接收可能需要两次接收,第一次接收20 个字节,第二次接收到了80 个字节,这个就是分包。究其原因,和tcp本身数据发送的逻辑有关。查看send 和 recv 的api 文档,send 的返回值表示发送成功的数据字节数,其实这里的发送成功并不是真正 的发送成功,他只是表示成功写入到了内核中的发送缓冲区数据的字节数,真正的发送要由内核完成,内核在数据发送的时候,又需要考虑流量控制,拥趸控制等,涉及滑动窗口,mss/mtu,nagle算法等,详细的大家可以看其他文档,他们讲的比我专业的多 。之前在处理粘包分包问题的时候都是简单的使用sleep函数,在项目中加这种sleep 是很恶心的一件事。
常见的解决方案,一般是两个:
一个是添加数据包头,指明数据包的大小;
另一个是添加标志字符串,指明数据的结尾或者开头。
也有说可以通过禁用nagle 算法的,windows下也有相关的socket 选项TCP_NODELAY ,但是从我查阅的文档来看,大部分的windows 操作系统是不支持禁用的。
用户层缓冲区的设计,可以阅读陈硕的 linux多线程服务端编程中关于缓冲区的设计。
这里大概说下缓冲区处理粘包问题的基本思路,接收到数据之后,先将数据放到缓冲区中,再解析数据包头的长度字段,如果整个缓冲区的数据的长度小于这个值,则不进行数据处理;如果大于这个值,则进行循环解析。因为可能出现粘包的情况,缓冲区中可能有多个数据包,所以需要一个循环处理。
3、tcp多线程数据发送的问题
不要在多线程中对同一个socket 进行读写操作,即使是加锁。正确的处理思路是维护一个发送队列,使用一个发送线程从发送队列中取数据进行发送。
4、使用select 操作socket,socket 关闭,线程退出的问题
考虑这么一个场景:
在对socket进行监听的时候,经常会单独起一个线程,然后使用select 函数监听可读,可写。但是如果同时我们的心跳线程检测到了目标端的退出,需要对socket 进行关闭操作,同时希望select 线程进行退出。这个时候如果我们在心跳线程中进行close操作,处于阻塞状态的select 函数是不会做出响应的,select线程将不会退出。
这种情况有两种处理方法:
一种是改变socket 为非阻塞式socket,同时设置一个标志位,然后在心跳线程中设置该标志位为 0 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
thread1
{
while
()
{
心跳检测代码
if
(检测失败)
{
flag
=
0
;
break
;
}
}
return
;
}
thread2
{
if
(flag)
{
select(非阻塞式socket,添加超时时间)
}
return
;
}
|
另一种方法是使用shutdown函数,关闭socket 的可读可写。
shutdown( SHUT_RDWR);
5、大小端问题
如果想要适配的机器更广,都需要对大小端进行处理。大小端处理的方法一般就两种思路。
一个是使用 ntohs / htons。将所有的网络传输中的数字转换为网络序,也就是大端序,数据到达机器之后,然后根据机器不同,考虑是否需要转换成小端序;
另一种方法就是使用atoi。在数据发送的时候,数据统一转换成字符串,到达机器之后,再是使用atoi 函数,转换成数字。
6、远控支持s5反向代理的实现问题
这里还要说下调试的问题,这种s5 反向的调试真的是非常恶心,多线程,缺乏很好的定位手段,尤其是程序不崩溃,部分结果异常。
这里说下我在调试时 的排查点:
1、两边的连接数量是不是一致。
2、两边的发送数据流量 和 接收数据流量是不是一致,单个线程的去统计。
7、icmp 传输大数据量的问题
icmp本身携带的数据量比较小,遇到类似 netstat -ano 这种返回结果的时候,单次肯定不能完全返回,这里就涉及到一个拆包,多次返回的问题。
其实下面这种方法对于短链接的模型,应该都是可以用的。
模型大概就是这样的:
控制端发送命令执行请求,目标端获得结果之后,不立即返回,将数据拆包,放在待返回队列中;
控制端发送数据获取请求,目标端从队列中获取数据,发送给控制端。
涉及一个数据返回的结构体
1
2
3
4
5
6
|
mystruct
{
请求包编号
分包的总个数
当前数据包位于分包的第几个
}
|
根据请求包的编号,可以按发送顺序获得返回结果,根据分包总个数和当前数据包位于分包的第几个 可以获得完整的返回结果。
8、插件化设计问题
并不是所有的功能模块都适合做成插件,像s5,端口转发这种的做成插件就比较鸡肋,因为他们本身对同步性的要求就比较高,这种的做成静态的功能就挺好的。
像远程shell,文件管理,进程管理这种的在做插件化设计时要考虑的点,说到底,其实就是数据如何与主模块进行传输。
我知道的两种吧:
一种是通过虚函数,主模块留出一些和插件之间进行数据交互的接口,然后插件模块对虚函数进行实现,mysend 和 myrecv之类的;
第二种就是导出函数,主模块寻找模块插件中的特定函数,通过调用这些特定函数,实现与主模块的交互。
9、模块/进程数据同步问题
进程,线程间数据同步的方法很多,个人觉得在项目里面不需要使用太多的方法,一种有效的手段就足够了
共享内存,文件,socket,管道,事件对象,互斥体
10、界面同步问题
数据处理逻辑和页面展示逻辑分开处理,通过消息进行同步。
11、端口复用问题
linux 下进行端口复用的方法比较多,这里说一个我在使用rawsocket 进行端口复用时遇到的问题。
rawsoket 进行端口复用时,如果复用的端口是一个短链接的或者说存在协议校验的端口,如 22 端口,那么使用rawsocket 进行复用的时候,是会出问题的,具体表现就是发送的流量和接收的流量不一致。
猜想如下,如果发送端存在了分包发送逻辑,内核在第一个分包发送完,第二个包还没发送的时候,接收端的22协议此时进行完了协议校验,进行了close 操作,那么第二个分包将无法发送,且数据包将被发送端丢弃,而send 函数只是将数据写入了内核的缓冲区,返回的是成功,无法得知是哪一段数据被丢弃了。
这里提供一种解决方法,通过 lkm 编程,hook close 函数。close 函数的参数是fd,而我们能够进行判断的只有 client 的端口信息,这里想到了3环下 get_peername 函数。可以在__sys_getpeername 内核源码逻辑的基础上,通过转换 fd,struct socket,struct sock之间的关系,得到fd 对应的端口信息。拦截close 的同时,需要在内核中同时开启数据接收线程,将数据从内核缓冲区中取出来,不然会出现 windows zero size 的报错信息。