用户态协议栈

497 阅读12分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

  本文简易阐述用户态如何完成内核的协议栈,实现一个udp协议的协议栈,由于TCP实现过于复杂,建议多看一下其原理即可posix API与网络协议栈的实现原理

用户态协议栈

在这里插入图片描述   为什么要实现用户态协议栈?

  应用程序在获得数据的时候,其实是进行了两次copy,先从网卡copy到协议栈,再从协议栈copy到内存,应用程序再对内存进行操作。

  实现用户态协议栈直接对内存进行操作可以达到零copy,大大提高了效率。

  原本的协议栈是在内核之中,由内核来完成其操作。也就是说网卡进行模数转换之后到达的地方是内核的协议栈,如果我们要实现用户态的协议栈就要想办法在网卡到内核协议栈中间将其拦截。

获得原始数据

获得原始数据的三种方法,分别有:

  • tcpdump和wireshark
  • dbdk
  • netmap   本文使用netmap来实现,netmap是用于用户层应用程序收发原始网络数据的高性能框架,netmap采用mmap的方式,直接将网卡的数据映射到一块内存中,应用程序可以直接通过mmap操作相应内存的数据,实现了零copy。

  零拷贝并不是没有拷贝数据,而是减少用户态、内核态的切换次数以及CPU拷贝次数。主要还是依靠DMA技术,DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与。(有一篇讲的易懂些的文章,想要再了解了解可以看看什么是零拷贝

netmap的安装

  netmap的安装可以参考手把手教你ubuntu18.04安装netmap

netmap

nm_open

函数声明:
struct nm_desc *nm_open(const char *ifname, const struct nmreq *req, uint64_t new_flags, const struct nm_desc *arg);
举例:
struct nm_desc *nmr = nm_open("netmap:ens33", NULL, 0, NULL);

  调用 nm_open 函数时,如:nmr = nm_open("netmap:ens33", NULL, 0, NULL); nm_open()会对传递的 ifname 指针里面的字符串进行分析,提取出网络接口名。   nm_open() 会申请struct nm_desc结构体大小的内存空间并返回首地址(这里用nmr接收) ,并通过 nmr->fd =open(NETMAP_DEVICE_NAME, O_RDWR);打开一个特殊的设备/dev/netmap 来创建文件描述符 nmr->fd。

  注意这个fd是/dev/netmap这个网卡设备,网卡只要来数据了,相应的这个fd就会有EPOLLIN事件,这个fd是检测网卡有没有数据的,因为是mmap,只要网卡有数据了,那么内存就有数据的。

  fd是指向网卡,操作数据是操作内存,内存和网卡数据的同步的,而我们cpu只能操作内存,不能操作外设。   一旦调用 nm_open 函数,网卡的数据就不从内核协议栈走了,这时候最好在虚拟机中建两个网卡,一个用于netmap,一个用于ssh等应用程序的正常工作。

nm_nextpkt

函数声明:
static u_char *nm_nextpkt(struct nm_desc *d, struct nm_pkthdr *hdr);
举例:
unsigned* stream = nm_nextpkt(nmr, &nmhead);

  nm_nextpkt是用来接收网卡上到来的数据包的函数。nm_nextpkt会将所有 rx 环都检查一遍,当发现有一个 rx 环有需要接收的数据包时,得到这个数据包的地址,并返回。nm_nextpkt()每次只能取一个数据包。(rx 环:想象成一个环形队列即可,每一项就是一个数据包。)

  stream即为数据在缓冲区中的首地址,struct nm_pkthdr为返回的数据包头部信息,不需要管头部的话直接从stream去取数据就行了。stream现在就是链路层的数据,包含了一些头部协议数据。

nm_inject

函数声明:
static int nm_inject(struct nm_desc *d, const void *buf, size_t size);
举例:
nm_inject(nmr,&arp_rt,sizeof(arp_rt));

  nm_inject()是用来往共享内存中写入待发送的数据包数据的。数据包经共享内存拷贝到网卡,然后发送出去。所以 nm_inject()是用来发包的。

  nm_inject()也会查找所有的发送环(tx 环),找到一个可以发送的槽,就将数据包写入并返回,所以每次函数调用也只能发送一个包。

nm_close

函数声明:
static int nm_close(struct nm_desc *d)
举例:
nm_close(nmr);

  nm_close 函数就是回收动态内存,回收共享内存,关闭文件描述符什么的了。

数据的传输与处理

  比如对方数据发送的时候先是在数据前面加一个udp的协议头,到了网络层再加一个ip头,进入数据链路层再加一个以太网头,再进入物理层那就是网卡把他从数字信号转换为模拟信号传输了。(简单来看加协议头无非也就是数据前面再多加点数据)

  数据传输过来的时候到了网卡的物理层,先模数转换成数字信号,原本的内核再依次把这个数据前面的协议一一解开,到了我们应用层手里就是对方真正传输的数据了。

  这时候我们自己来实现这个协议栈的话,就需要自己来完成这个数据的处理了。

以太网协议

在这里插入图片描述   以太网协议:两个地址皆为6字节的mac地址,后面2字节的来类型区分是什么协议。

ip协议

在这里插入图片描述

udp协议

在这里插入图片描述

  下面实现中会创建相应的结构体来与这些协议数据一一对应。

  udp的最后一个成员是柔性数组,是用来定义用户数据包的起始地址而不占用实际的结构体空间,用来指向数据的首地址,关于柔性数组,可以自行百度一下,这里不是重点。

简易实现

实现简易的获取数据,代码如下:

#include <stdio.h>
#include <sys/poll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define NETMAP_WITH_LIBS//一定要有,这是一个类似开关按钮的宏定义
#include <net/netmap_user.h>

//设置结构体以1字节对齐
#pragma pack(1)
#define ETH_ADDR_LENGTH 6

//以太网头
struct ethhdr{
    unsigned char h_dst[ETH_ADDR_LENGTH];
    unsigned char h_src[ETH_ADDR_LENGTH];
    unsigned short h_proto;
};

//ip头
struct iphdr {
    unsigned char version: 4,
    hdrlen: 4;
    unsigned char tos;
    unsigned short totlen;
    unsigned short id;
    unsigned short flag_offset;
    unsigned char ttl;
    unsigned char type;
    unsigned short check;
    unsigned int sip;
    unsigned int dip;
};

//udp头
struct udphdr {
    unsigned short sport;
    unsigned short dport;
    unsigned short length;
    unsigned short check;
};

//整体
struct udppkt {
    struct ethhdr eh; //14
    struct iphdr ip; //20
    struct udphdr udp;//8
    unsigned char data[0];//
};


int main() {

    struct nm_desc *nmr = nm_open("netmap:ens33", NULL, 0, NULL);
    if (nmr == NULL) {
        return -1;
    }
    printf("open ens33 seccess\n");

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

    while(1){
        int ret=poll(&pfd,1,-1);
        if(ret==-1){
            printf("poll error\n");
            return -1;
        }

        if (pfd.revents & POLLIN) {
            struct nm_pkthdr h;
            unsigned char *stream = nm_nextpkt(nmr, &h);

            struct udppkt *udp = (struct udppkt *) stream;
            int udplength = ntohs(udp->udp.length);
            udp->data[udplength - 8] = '\0';
            printf("udp--->%s\n",udp->data);
        }

    }


    nm_close(nmr);
    return 0;
}

  我们会发现,一开始可以正常传输,后面怎么弄都不行了,而且还一直有显示不出来的数据打印出来。 在这里插入图片描述

ARP协议

  因为宿主机不知道虚拟机的ip和mac地址了,我们查看arp表,发现没有192.168.109.100虚拟机的记录,此时宿主机会在局域网内广播arp请求。(如果对arp没有概念的可以先了解一下ARP协议

  刚开始能正常解析数据是因为,虚拟机刚开机的时候,我用xshell连虚拟机,所以宿主机发送过arp请求,而虚拟机的内核协议栈此时还没被netmap接管,回应了arp请求,宿主机就将虚拟机的信息暂时的添加到arp表中,当动态arp记录失效,udp包不知道发给谁,就会先发arp请求(如果源主机没有收到ARP响应数据包则表示查询失败,也就不会发udp包了)。一般这个记录会缓存个二十分钟左右。如果没法理解可以看看这篇数据链路层:ARP协议详解(绝对经典)

  解决这个问题的办法很简单,要么我们手动在宿主机上添加一条静态的arp记录,要么我们实现arp协议,下面我们来实现arp协议。

  我们可以先检测下是不是arp,先定义相应的arp结构体,arp协议是在物理层上面的层的,根据以太网协议中的帧类型来判断是IP还是ARP,所以我们先解析ether,再解析arp即可。

在这里插入图片描述

struct arphdr {
    unsigned short h_type;
    unsigned short h_proto;

    unsigned char h_addrlen;
    unsigned char h_protolen;

    unsigned short oper;

    unsigned char smac[ETH_ADDR_LENGTH];
    unsigned int sip;
    unsigned char dmac[ETH_ADDR_LENGTH];
    unsigned int dip;
};

  还有相应的检测与发送消息,当我们回了arp协议广播消息了之后就会发现可以一直接收到数据了。

#include <string.h>
#include <stdio.h>
#include <sys/poll.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define NETMAP_WITH_LIBS//一定要有,这是一个类似开关按钮的宏定义
#include <net/netmap_user.h>

//设置结构体以1字节对齐
#pragma pack(1)
#define ETH_ADDR_LENGTH 6

#define PROTO_IP 0x0800
#define PROTO_ARP 0x0806
#define PROTO_RARP	0x0835
#define PROTP_UPD 17

struct ethhdr{
    unsigned char h_dst[ETH_ADDR_LENGTH];
    unsigned char h_src[ETH_ADDR_LENGTH];
    unsigned short h_proto;
};

struct iphdr {
    unsigned char version: 4,
                    hdrlen: 4;
    unsigned char tos;
    unsigned short totlen;
    unsigned short id;
    unsigned short flag_offset;
    unsigned char ttl;
    unsigned char type;
    unsigned short check;
    unsigned int sip;
    unsigned int dip;
};

struct udphdr {
    unsigned short sport;
    unsigned short dport;
    unsigned short length;
    unsigned short check;
};

struct udppkt {
    struct ethhdr eh; //14
    struct iphdr ip; //20
    struct udphdr udp;//8
    unsigned char data[0];//
};

struct arphdr {
    unsigned short h_type;
    unsigned short h_proto;

    unsigned char h_addrlen;
    unsigned char h_protolen;

    unsigned short oper;

    unsigned char smac[ETH_ADDR_LENGTH];
    unsigned int sip;
    unsigned char dmac[ETH_ADDR_LENGTH];
    unsigned int dip;
};

struct arppkt {
    struct ethhdr eh;
    struct arphdr arp;
};

void echo_udp_pkt(struct udppkt *udp, struct udppkt *udp_rt) {
    memcpy(udp_rt, udp, sizeof(struct udppkt));
    memcpy(udp_rt->eh.h_dst, udp->eh.h_src, ETH_ADDR_LENGTH);
    memcpy(udp_rt->eh.h_src, udp->eh.h_dst, ETH_ADDR_LENGTH);
    udp_rt->ip.sip = udp->ip.dip;
    udp_rt->ip.dip = udp->ip.sip;
    udp_rt->udp.sport = udp->udp.dport;
    udp_rt->udp.dport = udp->udp.sport;
}

int str2mac(char *mac, char *str) {
    char *p = str;
    unsigned char value = 0x0;//16进制0
    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(struct arppkt *arp, struct arppkt *arp_rt, char *mac) {
    memcpy(arp_rt, arp, sizeof(struct arppkt));
    memcpy(arp_rt->eh.h_dst, arp->eh.h_src, ETH_ADDR_LENGTH);//以太网首部填入目的 mac
    str2mac(arp_rt->eh.h_src, mac);//以太网首部填入源mac
//    arp_rt->eh.h_proto = arp->eh.h_proto;//以太网协议还是arp协议
//    arp_rt->arp.h_addrlen = 6;//没有转是因为是一字节
//    arp_rt->arp.h_protolen = 4;//同上
    arp_rt->arp.oper = htons(2); // ARP响应,操作类型(op):1表示ARP请求,2表示ARP应答
    str2mac(arp_rt->arp.smac, mac);//arp报文填入源mac
    arp_rt->arp.sip = arp->arp.dip; // arp报文填入发送端 ip
    memcpy(arp_rt->arp.dmac, arp->arp.smac, ETH_ADDR_LENGTH);//arp报文填入目的 mac
    arp_rt->arp.dip = arp->arp.sip; // arp报文填入目的 ip
}

int main() {

    struct nm_desc *nmr = nm_open("netmap:ens33", NULL, 0, NULL);
    if (nmr == NULL) {
        return -1;
    }
    printf("open ens33 seccess\n");

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

    while(1) {
        int ret = poll(&pfd, 1, -1);
        if (ret == -1) {
            printf("poll error\n");
            return -1;
        }

        if (pfd.revents & POLLIN) {

            struct nm_pkthdr h;
            unsigned char *stream = nm_nextpkt(nmr, &h);
            struct ethhdr *eh = (struct ethhdr *) stream;
            if (ntohs(eh->h_proto) == PROTO_IP) {
                struct udppkt *udp = (struct udppkt *) stream;
                if (udp->ip.type == PROTP_UPD) {
                    int udplength = ntohs(udp->udp.length);
                    udp->data[udplength - 8] = '\0';
                    printf("udp--->%s\n", udp->data);

                    struct udppkt udp_rt;
                    echo_udp_pkt(udp, &udp_rt);
                    nm_inject(nmr, &udp_rt, sizeof(struct udppkt));

                }
            } else if (ntohs(eh->h_proto) == PROTO_ARP) {
                printf("ARP req\n");
                struct arppkt *arp = (struct arppkt *) stream;
                struct arppkt arp_rt;
                if (arp->arp.dip == inet_addr("192.168.109.100")) {
                    echo_arp_pkt(arp, &arp_rt, "00:0c:29:33:b9:90");//mac记得填入对应的mac地址,每个人网卡的mac地址不同
                    nm_inject(nmr, &arp_rt, sizeof(arp_rt));
                    printf("arp ret\n");
                }

            }


        }
    }

    nm_close(nmr);

    return 0;
}

  接下来会发现数据可以正常发送过去了,ping却ping不通,就算数据送到了也ping不通。ping命令是基于ICMP协议的。

ICMP协议

  icmp协议是ip协议的一部分。 在这里插入图片描述

在这里插入图片描述

  ICMP类型为ping时,具体协议格式如下: 在这里插入图片描述   icmp的类型有一个ICMP报文类型表,这里主要完成回显回答(对ping的回答),也就是类型填0编码(代码)填0。回显请求也就是ping的类型为8编码为0。其中增加的标识符和序号字段还有选项数据按照请求端的数据返回即可。

  检验和的数据需要注意下,它的计算方法与IP数据报中的首部校验和计算方式是一样的校验和计算原理

#include <string.h>
#include <stdio.h>
#include <sys/poll.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define NETMAP_WITH_LIBS//一定要有,这是一个类似开关按钮的宏定义
#include <net/netmap_user.h>

//设置结构体以1字节对齐
#pragma pack(1)
#define ETH_ADDR_LENGTH 6

#define PROTO_IP 0x0800
#define PROTO_ARP 0x0806
#define PROTO_RARP	0x0835
#define PROTP_UPD 17
#define PROTO_ICMP    1

struct ethhdr{
    unsigned char h_dst[ETH_ADDR_LENGTH];
    unsigned char h_src[ETH_ADDR_LENGTH];
    unsigned short h_proto;
};

struct iphdr {
    unsigned char version: 4,
                    hdrlen: 4;
    unsigned char tos;
    unsigned short totlen;
    unsigned short id;
    unsigned short flag_offset;
    unsigned char ttl;
    unsigned char type;
    unsigned short check;
    unsigned int sip;
    unsigned int dip;
};

struct udphdr {
    unsigned short sport;
    unsigned short dport;
    unsigned short length;
    unsigned short check;
};

struct udppkt {
    struct ethhdr eh; //14
    struct iphdr ip; //20
    struct udphdr udp;//8
    unsigned char data[0];//
};

struct arphdr {
    unsigned short h_type;
    unsigned short h_proto;

    unsigned char h_addrlen;
    unsigned char h_protolen;

    unsigned short oper;

    unsigned char smac[ETH_ADDR_LENGTH];
    unsigned int sip;
    unsigned char dmac[ETH_ADDR_LENGTH];
    unsigned int dip;
};

struct arppkt {
    struct ethhdr eh;
    struct arphdr arp;
};

void echo_udp_pkt(struct udppkt *udp, struct udppkt *udp_rt) {
    memcpy(udp_rt, udp, sizeof(struct udppkt));
    memcpy(udp_rt->eh.h_dst, udp->eh.h_src, ETH_ADDR_LENGTH);
    memcpy(udp_rt->eh.h_src, udp->eh.h_dst, ETH_ADDR_LENGTH);
    udp_rt->ip.sip = udp->ip.dip;
    udp_rt->ip.dip = udp->ip.sip;
    udp_rt->udp.sport = udp->udp.dport;
    udp_rt->udp.dport = udp->udp.sport;
}

int str2mac(char *mac, char *str) {
    char *p = str;
    unsigned char value = 0x0;//16进制0
    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(struct arppkt *arp, struct arppkt *arp_rt, char *mac) {
    memcpy(arp_rt, arp, sizeof(struct arppkt));
    memcpy(arp_rt->eh.h_dst, arp->eh.h_src, ETH_ADDR_LENGTH);//以太网首部填入目的 mac
    str2mac(arp_rt->eh.h_src, mac);//以太网首部填入源mac
//    arp_rt->eh.h_proto = arp->eh.h_proto;//以太网协议还是arp协议
//    arp_rt->arp.h_addrlen = 6;//没有转是因为是一字节
//    arp_rt->arp.h_protolen = 4;//同上
    arp_rt->arp.oper = htons(2); // ARP响应,操作类型(op):1表示ARP请求,2表示ARP应答
    str2mac(arp_rt->arp.smac, mac);//arp报文填入源mac
    arp_rt->arp.sip = arp->arp.dip; // arp报文填入发送端 ip
    memcpy(arp_rt->arp.dmac, arp->arp.smac, ETH_ADDR_LENGTH);//arp报文填入目的 mac
    arp_rt->arp.dip = arp->arp.sip; // arp报文填入目的 ip
}

struct icmphdr {
    unsigned char type;
    unsigned char code;
    unsigned short check;
    unsigned short identifier;
    unsigned short seq;
    unsigned char data[32];
};

struct icmppkt {
    struct ethhdr eh;
    struct iphdr ip;
    struct icmphdr icmp;
};

struct ippkt {
    struct ethhdr eh; //14
    struct iphdr ip; //20
};

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(struct icmppkt *icmp, struct icmppkt *icmp_rt) {
    memcpy(icmp_rt, icmp, sizeof(struct icmppkt));
    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;
    memcpy(icmp_rt->eh.h_dst, icmp->eh.h_src, ETH_ADDR_LENGTH);
    memcpy(icmp_rt->eh.h_src, icmp->eh.h_dst, ETH_ADDR_LENGTH);
    icmp_rt->icmp.check = in_cksum((unsigned short *) &icmp_rt->icmp, sizeof(struct icmphdr));
}

int main() {

    struct nm_desc *nmr = nm_open("netmap:ens33", NULL, 0, NULL);
    if (nmr == NULL) {
        return -1;
    }
    printf("open ens33 seccess\n");

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

    while(1) {
        int ret = poll(&pfd, 1, -1);
        if (ret == -1) {
            printf("poll error\n");
            return -1;
        }

        if (pfd.revents & POLLIN) {

            struct nm_pkthdr h;
            unsigned char *stream = nm_nextpkt(nmr, &h);
            struct ethhdr *eh = (struct ethhdr *) stream;
            if (ntohs(eh->h_proto) == PROTO_IP) {
                struct ippkt *iph = (struct ippkt *) stream;
                if (iph->ip.type == PROTP_UPD) {
                    struct udppkt *udp = (struct udppkt *) stream;
                    int udplength = ntohs(udp->udp.length);
                    udp->data[udplength - 8] = '\0';
                    printf("udp--->%s\n", udp->data);

                    struct udppkt udp_rt;
                    echo_udp_pkt(udp, &udp_rt);
                    nm_inject(nmr, &udp_rt, sizeof(struct udppkt));
                }else if(iph->ip.type == PROTO_ICMP){
                    struct icmppkt *icmp = (struct icmppkt *) stream;
                    printf("icmp ---------- --> type=[%d]code=[%d]\n", icmp->icmp.type,icmp->icmp.code);
                    if (icmp->icmp.type == 0x08) {
                        struct icmppkt icmp_rt = {0};
                        echo_icmp_pkt(icmp, &icmp_rt);
                        nm_inject(nmr, &icmp_rt, sizeof(struct icmppkt));
                    }

                }

            } else if (ntohs(eh->h_proto) == PROTO_ARP) {
                printf("ARP req\n");
                struct arppkt *arp = (struct arppkt *) stream;
                struct arppkt arp_rt;
                if (arp->arp.dip == inet_addr("192.168.109.100")) {
                    echo_arp_pkt(arp, &arp_rt, "00:0c:29:33:b9:90");//mac记得填入对应的mac地址,每个人网卡的mac地址不同
                    nm_inject(nmr, &arp_rt, sizeof(arp_rt));
                    printf("arp ret\n");
                }

            }


        }
    }

    nm_close(nmr);

    return 0;
}

  记得每次重启使用前都需要insmod netmap.ko ,然后我们查看ls /dev/netmap -l,出现下面的设备就说明开启成功了。

root@root:/netmap/LINUX# insmod netmap.ko 
root@root:/netmap/LINUX# ls /dev/netmap -l
crw------- 1 root root 10, 55 Sep 14 10:14 /dev/netmap

在这里插入图片描述