Android QNX内部Virtio net 抓包分析
工具:tcpdump + wireshark
背景:负责项目出现Android QNX内部网络丢包问题
解决思路:通过抓包进一步分析root cause,需要通过tcpdump抓两个系统的网络包,并通过wireshark分析
步骤
1. tcpdum抓包
通过ifconfig确定两个系统Virtio-Ethernet对应的网卡
得出Android的网卡是eth0, QNX的网卡是vp0
应用的服务端跑在QNX,端口号为49598,所以抓包命令为
QNX侧:
tcpdump -v -i vp0 port 49598 -c 400000 -w /blackbox/tcpdump49598.pcap
Android侧:
tcpdump -i eth0 -vvn -w tcpdump_android.pcap
后续通过tcpdump获得的pcap文件,导入wireshark继续分析
附上tcpdump常用参数:
1、指定网卡。
tcpdump -i eth0
2、指定ip
tcpdump -i eth0 ip host 172.168.1.1
3、指定端口
tcpdump -i eth0 port 80
4、指定协议
tcpdump -i eth0 udp/tcp
5、指定源和目的
tcpdump -i eth0 src/dst host 172.168.1.1
6、写入指定的文件中
tcpdump -i eth0 ip host 172.168.1.1 -w/opt/wenjian,cap
7、将抓包信息打印到前台。
tcpdump -A -i etho ip host 172.168.1.1 -vvn
8、写入1000个包到文件中
tcpdump -i eth0 ip host 172.168.1.1 -c 1000 -w /opt/wenjian,cap
9、环路抓包
tcpdump -i ol
# tcpdump -v -i vp0 port 49598 -c 400000 -w blackbox/tcpdump49598.pcap
tcpdump: listening on vp0, link-type EN10MB (Ethernet), capture size 262144 bytes
547 packets captured
9576 packets received by filter
0 packets dropped by kernel
# ls /blackbox/tcpdump49598.pcap
/blackbox/tcpdump49598.pcap
2. 用wireshark分析pcap文件
用wireshark打开,大概是这样的界面,由于tcpdump已经过滤了port 49598的数据,所以打开不需要过滤就能看到192.068.1.1(server)和192.168.1.3(client)之间在49598上通信的报文
但是报文那么多,我们怎么知道哪条报文是我们需要找的?
为此我
1. 修改了servcer端的代码,减少了我不想抓的报文
2. 通过应用的log打印,获取发送到socket的大致时间
通过log我们能够定位发送报文的时间大概是Jan 05 01:58:34.054
3. 通过wireshar过滤掉192.168.1.3 -> 192.068.1.1的报文,只保留192.068.1.1 -> 192.168.1.3的报文
通过ip.src == 192.168.1.1只显示192.068.1.1 -> 192.168.1.3的报文
附上wireshark常用过滤参数:
注: 过滤表达式可通过&&, |, !, ^^等逻辑符号进行组合
1. 针对ip的过滤
对源地址进行过滤
ip.src == 192.168.0.1
对目的地址进行过滤
ip.dst == 192.168.0.1
对源地址或者目的地址进行过滤
ip.addr == 192.168.0.1
如果想排除以上的数据包,只需要将其用括号囊括,然后使用 "!" 即可
!(ip.addr == 192.168.0.1)
2. 针对协议的过滤
获某种协议的数据包,表达式很简单仅仅需要把协议的名字输入即可
http
注意:是否区分大小写?答:区分,只能为小写
捕获多种协议的数据包
http or telnet
排除某种协议的数据包
not arp 或者 !tcp
3. 针对端口的过滤(视传输协议而定)
捕获某一端口的数据包(以tcp协议为例)
tcp.port == 80
捕获多端口的数据包,可以使用and来连接,下面是捕获高于某端口的表达式(以udp协议为例)
udp.port >= 2048
4. 针对长度和内容的过滤
针对长度的过虑(这里的长度指定的是数据段的长度)
udp.length < 20
http.content_length <=30
针对uri 内容的过滤
http.request.uri matches "user" (请求的uri中包含“user”关键字的)
注意:matches 后的关键字是不区分大小写的!
http.request.uri contains "User" (请求的uri中包含“user”关键字的)
注意:contains 后的关键字是区分大小写的!
5. 针对http请求的一些过滤实例。
过滤出请求地址中包含“user”的请求,不包括域名;
http.request.uri contains "User"
精确过滤域名
http.host==baidu.com
模糊过滤域名
http.host contains "baidu"
过滤请求的content_type类型
http.content_type =="text/html"
过滤http请求方法
http.request.method=="POST"
过滤tcp端口
tcp.port==80
http && tcp.port==80 or tcp.port==5566
过滤http响应状态码
http.response.code==302
过滤含有指定cookie的http数据包
http.cookie contains "userid"
4. 由于我只想知道业务报文,不关心握手报文,ACK报文以及心跳报文,所以我们可通过PSH找到有data的报文
在TCP层,有个FLAGS字段,这个字段有以下几个标识:SYN, FIN, ACK, PSH, RST, URG.
通过找到PSH的字段,就是我们需要的业务报文
参考: https://zhuanlan.zhihu.com/p/439614017
附上TCP层,FLAGS字段含义:
SYN表示建立连接,
FIN表示关闭连接,
ACK表示响应,
PSH表示有 DATA数据传输,
RST表示连接重置。
其中,ACK是可能与SYN,FIN等同时使用的,比如SYN和ACK可能同时为1,它表示的就是建立连接之后的响应,
如果只是单个的一个SYN,它表示的只是建立连接。
TCP的几次握手就是通过这样的ACK表现出来的。
但SYN与FIN是不会同时为1的,因为前者表示的是建立连接,而后者表示的是断开连接。
RST一般是在FIN之后才会出现为1的情况,表示的是连接重置。
一般地,当出现FIN包或RST包时,我们便认为客户端与服务器端断开了连接;而当出现SYN和SYN+ACK包时,我们认为客户端与服务器建立了一个连接。
PSH为1的情况,一般只出现在 DATA内容不为0的包中,也就是说PSH为1表示的是有真正的TCP数据包内容被传递。
TCP的连接建立和连接关闭,都是通过请求-响应的模式完成的。
5. 通过代码定义的header找到精确的报文
由于我们的报文是通过flatbuffer序列化后的,且不是string类型,故wireshark只能看到序列化后的十六进制数据,看不到内容,所以需要想点办法通过报文中的data定位我们需要找的数据
分析发送代码:
bool MessagingManager::sendMessage(nio::messaging::TxMessageIds messageId, unsigned int payloadLength, uint8_t *payload)
{
std::lock_guard<std::mutex> lock(mSendMutex); //Prevent out of order messages from being sent from multiple threads
bool send_valid = false;
uint8_t buf_header[8];
uint32_t id = static_cast<uint32_t>(messageId);
/* not good practice : network should use big-endian */
buf_header[0] = id&0xFF;
buf_header[1] = (id>>8)&0xFF;
buf_header[2] = (id>>16)&0xFF;
buf_header[3] = (id>>24)&0xFF;
buf_header[4] = payloadLength&0xFF;
buf_header[5] = (payloadLength>>8)&0xFF;
buf_header[6] = (payloadLength>>16)&0xFF;
buf_header[7] = (payloadLength>>24)&0xFF;
for (int i=0; i<MAX_CONNECTIONS; i++)
{
if (mConnections[i].isValid())
{
struct iovec iov[2];
iov[0].iov_base = (void *)buf_header;
iov[0].iov_len = sizeof(buf_header);
iov[1].iov_base = payload;
iov[1].iov_len = payloadLength;
mTcpServer->SendV(mConnections[i].getSocket(), iov, 2);
send_valid = true;
}
}
if (false == send_valid)
{
NioLog::warning("MessagingManager: {} Can't send MessageId {}", __FUNCTION__, messageId);
}
return send_valid;
}
bool CTCPServer::SendV(const Socket socket, struct iovec *iov, int iovcnt)
{
int iResult = 0;
iResult = writev(socket, iov, iovcnt);
if (iResult < 0)
{
NioLog::error("[TCPServer][Error] ret {} IOV writing to socket : {} ({})", iResult, errno, strerror(errno));
return false;
}
return true;
}
可以看出,报文是由两部分组成,第一部分是buf_header,第二部分是序列化后的payload。
buf_header是由8 bytes组成(uint8_t buf_header[8]),messageId是一个五位数的十进制整数,最大不超过2^17。
buf_header[0]是messageId二进制的0-7位(0xFF即 1111 1111)。
buf_header[1]是messageId二进制的8-15位。
buf_header[2]是messageId二进制的16-23位。
buf_header[3]是messageId二进制的24-31位。
分析到这里就比较简单了,我查了我想抓的报文的messageId是57275
转换成十六进制是0xDFBB,二进制为1101 1111 1011 1011。
(注: 一位十六进制是2^4, 即四位二进制,故四位二进制正好表达一位十六进制)
故这个messageId总共只有16位二进制,故buf_header[0]为BB,buf_header[1]为DF,故buf_header[2]和buf_header[3]均为0.
我们只要找到报文的data开头是BB DF 00 00 的报文,即是我们需要的报文。
到这一步,就很轻易的找到了
如下图所示,即为我们需要找的报文