利用tcpprobe思想和tracepoint利器可以做一个检测丢包工具

来源:互联网 发布:西安理工大学知行 编辑:程序博客网 时间:2024/05/17 08:11

       如果从app的应用的角度来说,如果固定了端口和ip的话,就确定一条链路,那么可以通过tcpprobe分析数据的流向,也可以根据修改成自己的代码。当然看了tracepoint之后,我们知道可以做更多的事情了,完全可以定制自己的丢包检查工具,
        假设我们实战在传屏中,那么可以做到,搞一个工具来给自己使用。这里需要了解网络基本知识,skb,struct udphdr之间的转换关系,同时需要了解在C/C++写网络程序的时候,往往会遇到字节的网络顺序和主机顺序的问题。接下里直接给源码,稍微分析下,这里考虑四种情况,丢包的情况,缓冲区满的情况,以及从udp入栈到qdsc失败的情况,以及打印缓冲满的时候的rx,tx mem的情况。


第一步:需要注册和释放的入口函数,如下,

 

static int __init trace_init(void){    int ret = 0;    printk("\nMAJOR:%s port = %d\n",major,port);    WARN_ON(register_trace_netif_rx(my_netif_rx,NULL));    ret = register_trace_sock_rcvqueue_full(my_sock_rcvqueue_full,NULL);    WARN_ON(ret);    ret = register_trace_skb_copy_datagram_iovec(my_skb_copy_datagram_iovec,NULL);    WARN_ON(ret);    ret = register_trace_udp_fail_queue_rcv_skb(my_udp_fail_queue_rcv_skb,NULL);    WARN_ON(ret);    ret = register_trace_sock_exceed_buf_limit(my_sock_exceed_buf_limit,NULL);    WARN_ON(ret);    return 0;}static void __exit trace_exit(void){    unregister_trace_netif_rx(my_netif_rx,NULL);    unregister_trace_sock_rcvqueue_full(my_sock_rcvqueue_full,NULL);    unregister_trace_skb_copy_datagram_iovec(my_skb_copy_datagram_iovec,NULL);    unregister_trace_udp_fail_queue_rcv_skb(my_udp_fail_queue_rcv_skb,NULL);    unregister_trace_sock_exceed_buf_limit(my_sock_exceed_buf_limit,NULL);}

接下来,需要结合kernel的代码来操作,因此

首先最简单的!从kernel的代码可以看出,在net/ipv4/udp.c找到trace_udp_fail_queue_rcv_skb,然后可以看出,

rc = ip_queue_rcv_skb(sk, skb);继续跟rc的返回值意义即可,

于是,也可以直接看kernel出错信息!

static void my_udp_fail_queue_rcv_skb(int rc,struct sock *sk){    if (rc == -ENOMEM) {        printk("func[%s] rc = %d port = %d ENOMEM\n",__func__,rc,ntohs(inet_sk(sk)->inet_num));    } else if (rc == -ENOBUFS) {        printk("func[%s] rc = %d port = %d ENOBUFS\n",__func__,rc,ntohs(inet_sk(sk)->inet_num));    }else        printk("func[%s] rc = %d port = %d unkown reason\n",__func__,rc,ntohs(inet_sk(sk)->inet_num));}

第二:这里需要看下struct proto*  prot;定义了很多tcp数据的参数。后面慢慢来分析!

static void my_sock_exceed_buf_limit(struct sock *sk,struct proto* prot,long allocated){    printk("func[%s] proto:%s sysctl_mem=%ld,%ld,%ld allocated=%ld sysctl_rmem=%d rmem_alloc=%d",        __func__,        prot->name,        prot->sysctl_mem[0],        prot->sysctl_mem[1],        prot->sysctl_mem[2],        allocated,        prot->sysctl_rmem[0],        atomic_read(&sk->sk_rmem_alloc));}

第三:和第二是一样的!

static void my_sock_rcvqueue_full(void struct sock *sk,struct sk_buff *skb){    printk("func[%s]rmem_alloc=%d size=%u rcvbuf=%d\n",__func__,atomic_read(&sk->sk_rmem_alloc),skb->truesize, sk->sk_rcvbuf);}

最后来分析,发包的核心处理函数,丢包是怎么样抓取和分析的!首先从驱动buffer里面拿数据,然后通过tcp协议栈给到用户空间。这里先看netif_rx吧,另外一个可以仿照写出来的。


根据上图rtp header的分布图,可以得到具体的信息,

static void my_netif_rx(void * ignore,struct sk_buff *skb){    unsigned char *sockBuf;    unsigned short int port;    static unsigned int rx_seqNo = 0,rx_oldSeqNo = 0;    sockBuf = skb->data;//指向保存数据内容的首地址    //0x16 = 22 = ipheader (20) + udpheader->source_port len ===>udpheader->dest_port    port = htons(*((unsigned short int *)(sockBuf + 0x16)));//偏移地址或者也可以直接先拿udp头部,然后拿dest_port也是一样的。    if (port == rtp_port) {        //0x1e= 30 = ipheader(20) + udphead (8) + RTP header(2) ==>the RTP seqnum is 2BYTE offset        //这里的偏移是根据头指针        rx_seqNo = ntohs(*((unsigned short int *)(sockBuf + 0x1E)));        if (rx_seqNo != (rx_oldSeqNo + 1) && rx_seqNo != 0)        {            printk("func[%s] pre seqNo: %u current seqNo: %u lost num of seq: %u\n", __func__,rx_oldSeqNo, rx_seqNo, rx_seqNo-rx_oldSeqNo-1);        }        rx_oldSeqNo = rx_seqNo;    }}

接下来,那就看看从协议栈到用户空间,就是一个拷贝而已!

如下所示:

static void my_skb_copy_datagram_iovec(const struct sk_buff *skb ,int len) {    struct udphdr *uh;    char *sockBuf;    static unsigned int seqNo = 0,oldSeqNo = 0;    uh = udp_hdr(skb);//这里直接用函数来拿udp header    if (ntohs(uh->dest) == rtp_port) {        sockBuf = skb->data + sizeof (struct udphdr);//拿rtp header的指针        seqNo = (unsigned int)(sockBuf[2]<<8|sockBuf[3]);//这里有大小端的问题,需要看清楚        if (seqNo != (oldSeqNo + 1) && seqNo != 0)        {            printk("func[%s] pre seqNo: %u   current seqNo: %u lost num of seq: %u\n", __func__,oldSeqNo, seqNo, seqNo-oldSeqNo-1);        }        oldSeqNo = seqNo;    }}

这样就整体的架构完成了,

至于怎么样编译,还有如何串起来就不多说了!

最后一个有意思的问题就是,sockBuf是怎么样算出seqNo的,

稍微下了一个demo,如下:

#include<stdio.h>#include <malloc.h>#include <netinet/in.h>void main(){     struct rtp_hdr {/**//* byte 0 */    unsigned char csrc_len : 4;        /**//* expect 0 */    unsigned char extension : 1;        /**//* expect 1, see RTP_OP below */    unsigned char padding : 1;        /**//* expect 0 */    unsigned char version : 2;        /**//* expect 2 */    /**//* byte 1 */    unsigned char payload : 7;        /**//* RTP_PAYLOAD_RTSP */     unsigned char marker : 1;        /**//* expect 1 */    /**//* bytes 2, 3 */    unsigned short seq_no;    /**//* bytes 4-7 */    unsigned  long timestamp;    /**//* bytes 8-11 */    unsigned long ssrc;            /**//* stream number is used here. */     } *rtp_hdr_t;    rtp_hdr_t = (struct rtp_hdr*)malloc(sizeof(struct rtp_hdr));    rtp_hdr_t->seq_no=htons(3000);    //rtp_hdr_t->version = 2;    printf("p->seq = %u\n",rtp_hdr_t->seq_no);    char *data = rtp_hdr_t;    printf("p->seq = %u\n",ntohs(rtp_hdr_t->seq_no));    printf("data[2]<<8|data[3]) = %u\n",ntohs(data[2]<<8|data[3]));    printf("data[2]<<8) = %u\n",data[2]<<8);    printf("data[3]) = %u\n",data[3]);    printf("data address = %u\n",&data);    printf("rtp_hdr_t address = %u\n",&rtp_hdr_t);    printf("unsigned int = %u\n",sizeof(unsigned int));    printf("char = %u\n",sizeof(char));    int i = 1;    i = i<<2;    printf("i=%d\n",i);}
发现一个问题,当seq当一定值的时候,会出现打印不对的情况

这是在用户空间的结果,进一步验证需要到内核空间实施!


靠,终于抓包看懂了网上的这个分析手法,差点绕进去了,我晕!


像上面的udp  rtp 包所示,根据rtp header的分析

可以知道,18 9f是SequenceNum,

肯定按照平常的算法不就是18是高位,9f是低位,然后18左移8位加上9f不就是

序列号吗?

因此写成了:SockData[2]<<8|SockData[3]

so easy!

原创粉丝点击