用户态协议栈的实现

协议栈

协议栈,指的是TCP/IP协议栈。linux系统中,协议栈是内核实现的。

协议,是通信双方对包格式的一种约定。

为什么是栈呢?因为对于包的组织,类似于栈的数据结构。发送端组织包的顺序是应用层->传输层->网络层->数据链路层,之后通过网卡将数字信号转换成光电信号,发送给接收端;接收端的网卡将光电信号转换成数字信号,解包的顺序是数据链路层->网络层->传输层->应用层。

用户态协议栈的实现_第1张图片

如何拿到最原始的数据?

  1. raw socket,socket的第二个参数,可以设置SOCK_STREAM,SOCK_DGRAM. SOCK_RAW就可以拿到以太网数据。tcpdump、wireshark就是利用这种方法。
  2. netmap
  3. dpdk

网卡的作用

Client发送数据给server,数据首先到达网卡,经过两步到达应用程序
1)将数据从网卡的内存copy到内核协议栈,内核协议栈对数据包进行解析;
2)应用程序通过调用recv函数,将数据从内核copy进用户空间,得到应用层的数据包。
网卡的作用,接收的时候,是将光电信号转换成数字信号;发送的时候,将数字信号转换成光电信号。

什么是用户态协议栈?

就是将协议栈,做到应用程序。为什么要这么做呢?减少了一次数据copy的过程,绕过内核,数据可以直接从网卡copy到应用程序,对于性能会有很大的提升。
用户态协议栈的实现_第2张图片

为什么要有用户态协议栈呢?

是为了解决C10M的问题。

之前说过C10K的问题,使用epoll可以解决C10K的问题。现在epoll已经可以支持两三百万的并发了。
什么是C10M问题?
实现10M(即1千万)的并发连接挑战意味着什么:(网上找的)
1)1千万的并发连接数;
2)100万个连接/秒:每个连接以这个速率持续约10秒;
3)10GB/秒的连接:快速连接到互联网;
4)1千万个数据包/秒:据估计目前的服务器每秒处理50K数据包,以后会更多;
5)10微秒的延迟:可扩展服务器也许可以处理这个规模(但延迟可能会飙升);
6)10微秒的抖动:限制最大延迟;
7)并发10核技术:软件应支持更多核的服务器(通常情况下,软件能轻松扩展到四核,服务器可以扩展到更多核,因此需要重写软件,以支持更多核的服务器).

我们来计算一下,单机承载1000万连接,需要的硬件资源:
内存:1个连接,大概需要4k recvbuffer,4k sendbuffer,一共需要10M * 8k = 80G
CPU:10M 除以 50K = 200核
只是支持这么多连接,还没有做其他事情,就需要这么多的资源,如果在加上其他的限制,加上业务的处理,资源肯定会更多。使用用户态协议栈,可以减少一次数据的copy,可以节省很大一部分资源。

要实现用户态协议栈,很关键的一个问题,是网络数据怎么才能绕过内核,直接到达用户空间?netmap、dpdk为用户态协议栈的实现,提供了可能。

这次我们使用了netmap实现用户态协议栈,后面会介绍dpdk。

netmap原理

netmap主要利用了mmap,将网卡中数据,直接映射到内存。netmap直接接管网卡数据,可以绕过内核协议栈。我们直接在应用程序中实现协议栈,对协议进行解析,就可以获取到网络数据了。

零拷贝,使用的是mmap方式,本质是DMA的方式,不需要CPU参与。普通copy,从磁盘copy数据到内存,需要CPU的move指令。sendfile使用的是mmap方式。**零拷贝主要是说CPU有没有参与,而不是说有没有copy。**是由主板上的DMA芯片将外设的数据copy到内存。
用户态协议栈的实现_第3张图片

netmap可以在github上下载,按照上面的readme编译安装,使用比较方便。
https://github.com/luigirizzo/netmap

利用netmap实现用户态协议栈

以太网头定义

用户态协议栈的实现_第4张图片

typedef struct _ethhdr {

    unsigned char h_dst[ETH_ADDR_LENGTH];
    unsigned char h_src[ETH_ADDR_LENGTH];
    unsigned short h_proto;

} ethhdr;

IP头定义

用户态协议栈的实现_第5张图片

typedef struct _iphdr {

    unsigned char hdrlen:4, // ip头长度,最大15*4=60字节
                  version:4;
    unsigned char tos;
    unsigned short length;
    unsigned short id;
    unsigned short flag_offset;
    unsigned char ttl; // time to live ping的ttl就是ip头里面的ttl
    unsigned char type;
    unsigned short check;
    unsigned int sip;
    unsigned int dip;

} iphdr;

UDP头定义

用户态协议栈的实现_第6张图片

UDP比TCP要简单很多,没有序列号,无法保证消息必达;也做不了重传;没有拥塞控制。

typedef struct _udphdr {

    unsigned short sport;
    unsigned short dport;
    unsigned short length;
    unsigned short check;

} udphdr;

UDP包定义

UDP包组成:以太网头 + IP头 + UDP头 + UDP数据。

以太网头、IP头、UDP头都已经定义好了,这里有一个问题,UDP数据怎么定义?

用指针是不合适的,这里引入了零长数组,也叫柔性数组。

typedef struct _udppkt {

    ethhdr eh; // 14
    iphdr ip; // 20
    udphdr udp; // 8

    unsigned char data[0];

} udppkt;

柔性数组的好处,是不占空间,只是占了一个位置,作用相当于标签。

零长数组使用条件:

  1. 不关心长度,可以通过某种方法计算出它的长度,比如通过udp头的length能够计算出来用户数据的长度;
  2. 它的内存是提前分配好的,不会越界。

因为4字节对齐的关系,sizeof(udppkt)的结果是44,所以需要使用单字节对齐,结果就是42。

#pragma pack(1)

使用netmap处理UDP包

nm_open主要做了两个事情:

  1. 把网卡的内存映射到内存;
  2. 把fd指向eth0对应的设备文件。

通过检测fd,就可以判断网卡是否有数据,有数据就可以直接操作内存。

用户态协议栈的实现_第7张图片

如果来了一个数据,如果数据很多,网卡需要进行模拟信号和数字信号转换,DMA也需要不断把数据映射到内存,怎么把多个数据包给组织起来?比如一下来100个包,怎么把这100个包组织起来?

使用ringbuff。

对于大量数据,从网卡将数据取到内存中,CPU有两种做法:

  1. 轮询
  2. 事件

对于大量数据,使用轮询方式比较好。这就是网络这一层,从网卡里面取数据的两种方法。事件的方式针对稀疏型的数据。

nm_nextpkt是操作内存,取出来的就是一个完整的包。

之后通过去掉以太网头,IP头,UDP头,得到用户数据。

用户态协议栈的实现_第8张图片

int main() {
    struct nm_pkthdr h; // ringbuff的指针
    struct nm_desc *nmr = nm_open("netmap:eth1", NULL, 0, NULL);

    if (nmr == NULL) return -1;

    struct pollfd pfd = {0};
    pfd.fd = nmr->fd;
    pfd.events = POLLIN;

    while (1) {

        int ret = poll(&pfd, 1, -1);
        if (ret < 0) continue;

        if (pfd.events & POLLIN) {

            unsigned char *stream = nm_nextpkt(nmr, &h); // ringbuff

            ethhdr *eh = (ethhdr*)stream;
            if (ntohs(eh->h_proto) == PROTO_IP) {

                udppkt *udp = (udppkt*)stream;

                if (udp->ip.type == PROTO_UDP) {

                    int udplen = ntohs(udp->udp.length);

                    udp->data[udplen - 8] = '\0';

                    printf("udp --> %s\n", udp->data);

                }

            }

        }

    }


}

程序编译运行后,是可以成功打印udp数据的。

运行的时候,注意要使用两块网卡,不然eth0的网卡被我们的程序接管了,ssh就无法登陆了。

但是会有一个问题,程序运行一段时间后,无法处理udp包了。

原因是机器会维护一个arp表,就是ip和mac地址的对应关系表,arp表会老化,过期了之后,如果要向目标机器发送消息,会重新发送arp请求,由于我们的程序接管了机器的网卡,但是却没有处理arp请求,导致对端的arp表中没有我们这台机器的数据。

用户态协议栈的实现_第9张图片

arp协议实现

arp头定义

用户态协议栈的实现_第10张图片

typedef struct _aprhdr {

    unsigned short h_type;
    unsigned short protocol;
    unsigned char h_addr_len;
    unsigned char p_addr_len;
    unsigned short oper;
    unsigned char smac[ETH_ADDR_LENGTH];
    unsigned int sip;
    unsigned char dmac[ETH_ADDR_LENGTH];
    unsigned int dip;
    

} arphdr;

arp包定义

typedef struct _arppkt {

    ethhdr eh;
    arphdr arp;


} arppkt;

处理arp包

解析arp包,将源ip、源mac和目的ip、目的mac互换,并且将oper设置为2(arp 请求时1,回复是2),就可以了。

int main() {
    struct nm_pkthdr h; // ringbuff的指针
    struct nm_desc *nmr = nm_open("netmap:eth1", NULL, 0, NULL);

    if (nmr == NULL) return -1;

    struct pollfd pfd = {0};
    pfd.fd = nmr->fd;
    pfd.events = POLLIN;

    while (1) {

        int ret = poll(&pfd, 1, -1);
        if (ret < 0) continue;

        if (pfd.events & POLLIN) {

            unsigned char *stream = nm_nextpkt(nmr, &h); // ringbuff

            ethhdr *eh = (ethhdr*)stream;
            if (ntohs(eh->h_proto) == PROTO_IP) {

                udppkt *udp = (udppkt*)stream;

                if (udp->ip.type == PROTO_UDP) {

                    int udplen = ntohs(udp->udp.length);

                    udp->data[udplen - 8] = '\0';

                    printf("udp --> %s\n", udp->data);

                }

            } else if (ntohs(eh->h_proto) == PROTO_ARP) {

                arppkt *arp = (arppkt *)stream;

                arppkt arp_rt;

                if (arp->arp.dip == inet_addr("10.36.121.51")) { // 去掉if判断,就可以做arp攻击

                    echo_arp_pkt(arp, &arp_rt, "fa:16:3e:72:4c:ca");

                    nm_inject(nmr, &arp_rt, sizeof(arp_rt));

                    printf("arp ret\n");
                
                }

            }

        }

    }


}

arp攻击

客户端发送请求的时候,如果发现目的地址不在arp表当中,会自动发送一个arp请求。

arp请求是向局域网内所有的机器发送请求,要获取目标ip对应的机器,也就是mac地址。如果对于任何arp请求,你会进行回复,就会造成arp欺骗,也就是arp攻击。

在上面的代码中,如果不对arp包中的目的ip进行判断,就可以实现arp攻击。

 if (arp->arp.dip == inet_addr("10.36.121.51")) { // 去掉if判断,就可以做arp攻击

完整代码

#include 

#include 
#include 

#define NETMAP_WITH_LIBS

#include 

#pragma pack(1) // 单字节对齐

#define ETH_ADDR_LENGTH 6

#define PROTO_IP 0x0800

#define PROTO_ARP 0x0806

#define PROTO_UDP 17
#define PROTO_ICMP 1


typedef struct _ethhdr {

    unsigned char h_dst[ETH_ADDR_LENGTH];
    unsigned char h_src[ETH_ADDR_LENGTH];
    unsigned short h_proto;


} ethhdr;

typedef struct _iphdr {

    unsigned char hdrlen:4, // ip头长度,最大15*4=60字节
                  version:4;
    unsigned char tos;
    unsigned short length;
    unsigned short id;
    unsigned short flag_offset;
    unsigned char ttl; // time to live ping的ttl就是ip头里面的ttl
    unsigned char type;
    unsigned short check;
    unsigned int sip;
    unsigned int dip;


} iphdr;

typedef struct _aprhdr {

    unsigned short h_type;
    unsigned short protocol;
    unsigned char h_addr_len;
    unsigned char p_addr_len;
    unsigned short oper;
    unsigned char smac[ETH_ADDR_LENGTH];
    unsigned int sip;
    unsigned char dmac[ETH_ADDR_LENGTH];
    unsigned int dip;
    

} arphdr;


typedef struct _udphdr {

    unsigned short sport;
    unsigned short dport;

    unsigned short length;
    unsigned short check;


} udphdr;

typedef struct _icmphdr {

    unsigned char type;
    unsigned char code;
    unsigned short check;
    unsigned short id;
    unsigned short seq;
    unsigned char mask[32];

} icmphdr;


typedef struct _udppkt {

    ethhdr eh; // 14
    iphdr ip; // 20
    udphdr udp; // 8

    unsigned char data[0];

} udppkt;

typedef struct _arppkt {

    ethhdr eh;
    arphdr arp;


} arppkt;

typedef struct _icmppkt {

    ethhdr eh;
    iphdr ip;
    icmphdr icmp;

} icmppkt;

int str2mac(char *mac, char *str) {

    char *p = str;
    unsigned char value = 0x0;
    int i = 0;

    while (p != '\0') {
        
        if (*p == ':') {
            mac[i++] = value;
            value = 0x0;
        } else {
            
            unsigned char temp = *p;
            if (temp <= '9' && temp >= '0') {
                temp -= '0';
            } else if (temp <= 'f' && temp >= 'a') {
                temp -= 'a';
                temp += 10;
            } else if (temp <= 'F' && temp >= 'A') {
                temp -= 'A';
                temp += 10;
            } else {	
                break;
            }
            value <<= 4;
            value |= temp;
        }
        p ++;
    }

    mac[i] = value;

    return 0;
}



void echo_arp_pkt(arppkt *arp, arppkt *arp_rt, char *mac) {

    memcpy(arp_rt, arp, sizeof(arppkt));

    memcpy(arp_rt->eh.h_dst, arp->eh.h_src, ETH_ADDR_LENGTH);
    str2mac(arp_rt->eh.h_src, mac);
    arp_rt->eh.h_proto = arp->eh.h_proto;

    arp_rt->arp.h_addr_len = 6;
    arp_rt->arp.p_addr_len = 4;
    arp_rt->arp.oper = htons(2);
    
    str2mac(arp_rt->arp.smac, mac);
    arp_rt->arp.sip = arp->arp.dip;
    
    memcpy(arp_rt->arp.dmac, arp->arp.smac, ETH_ADDR_LENGTH);
    arp_rt->arp.dip = arp->arp.sip;

}

unsigned short in_cksum(unsigned short *addr, int len)
{
	register int nleft = len;
	register unsigned short *w = addr;
	register int sum = 0;
	unsigned short answer = 0;

	while (nleft > 1)  {
		sum += *w++;
		nleft -= 2;
	}

	if (nleft == 1) {
		*(u_char *)(&answer) = *(u_char *)w ;
		sum += answer;
	}

	sum = (sum >> 16) + (sum & 0xffff);	
	sum += (sum >> 16);			
	answer = ~sum;
	
	return (answer);

}

void echo_icmp_pkt(icmppkt *icmp, icmppkt *icmp_rt) {

    memcpy(icmp_rt, icmp, sizeof(icmppkt));

    memcpy(icmp_rt->eh.h_dst, icmp->eh.h_src, ETH_ADDR_LENGTH);
    memcpy(icmp_rt->eh.h_src, icmp_rt->eh.h_dst, ETH_ADDR_LENGTH);
    icmp_rt->eh.h_proto = icmp->eh.h_proto;

	icmp_rt->icmp.type = 0x0; //
	icmp_rt->icmp.code = 0x0; //
	icmp_rt->icmp.check = 0x0;

	icmp_rt->ip.sip = icmp->ip.dip;
	icmp_rt->ip.dip = icmp->ip.sip;

    icmp_rt->icmp.check = in_cksum((unsigned short*)&icmp_rt->icmp, sizeof(icmphdr));

}

// insmod netmap.ko
// gcc -o net_family_o net_family_0.c
int main() {
    struct nm_pkthdr h; // ringbuff的指针
    struct nm_desc *nmr = nm_open("netmap:eth1", NULL, 0, NULL);

    if (nmr == NULL) return -1;

    struct pollfd pfd = {0};
    pfd.fd = nmr->fd;
    pfd.events = POLLIN;

    while (1) {

        int ret = poll(&pfd, 1, -1);
        if (ret < 0) continue;

        if (pfd.events & POLLIN) {

            unsigned char *stream = nm_nextpkt(nmr, &h); // ringbuff

            ethhdr *eh = (ethhdr*)stream;
            if (ntohs(eh->h_proto) == PROTO_IP) {

                udppkt *udp = (udppkt*)stream;

                if (udp->ip.type == PROTO_UDP) {

                    int udplen = ntohs(udp->udp.length);

                    udp->data[udplen - 8] = '\0';

                    printf("udp --> %s\n", udp->data);

                } else if (udp->ip.type == PROTO_ICMP) {

                    icmppkt *icmp = (icmppkt*)stream;

                    icmppkt icmp_rt;

                    echo_icmp_pkt(icmp, &icmp_rt);

                    nm_inject(nmr, &icmp_rt, sizeof(icmp_rt));


                }

            } else if (ntohs(eh->h_proto) == PROTO_ARP) {

                arppkt *arp = (arppkt *)stream;

                arppkt arp_rt;

                if (arp->arp.dip == inet_addr("10.36.121.51")) { // 去掉if判断,就可以做arp攻击

                    echo_arp_pkt(arp, &arp_rt, "fa:16:3e:72:4c:ca");

                    nm_inject(nmr, &arp_rt, sizeof(arp_rt));

                    printf("arp ret\n");
                
                }

            }

        }

    }


}
        icmppkt icmp_rt;

                    echo_icmp_pkt(icmp, &icmp_rt);

                    nm_inject(nmr, &icmp_rt, sizeof(icmp_rt));


                }

            } else if (ntohs(eh->h_proto) == PROTO_ARP) {

                arppkt *arp = (arppkt *)stream;

                arppkt arp_rt;

                if (arp->arp.dip == inet_addr("10.36.121.51")) { // 去掉if判断,就可以做arp攻击

                    echo_arp_pkt(arp, &arp_rt, "fa:16:3e:72:4c:ca");

                    nm_inject(nmr, &arp_rt, sizeof(arp_rt));

                    printf("arp ret\n");
                
                }

            }

        }

    }


}

你可能感兴趣的:(Linux,server,网络,linux,网络协议,协议栈,后端)