协议栈,指的是TCP/IP协议栈。linux系统中,协议栈是内核实现的。
协议,是通信双方对包格式的一种约定。
为什么是栈呢?因为对于包的组织,类似于栈的数据结构。发送端组织包的顺序是应用层->传输层->网络层->数据链路层,之后通过网卡将数字信号转换成光电信号,发送给接收端;接收端的网卡将光电信号转换成数字信号,解包的顺序是数据链路层->网络层->传输层->应用层。
如何拿到最原始的数据?
Client发送数据给server,数据首先到达网卡,经过两步到达应用程序
1)将数据从网卡的内存copy到内核协议栈,内核协议栈对数据包进行解析;
2)应用程序通过调用recv函数,将数据从内核copy进用户空间,得到应用层的数据包。
网卡的作用,接收的时候,是将光电信号转换成数字信号;发送的时候,将数字信号转换成光电信号。
就是将协议栈,做到应用程序。为什么要这么做呢?减少了一次数据copy的过程,绕过内核,数据可以直接从网卡copy到应用程序,对于性能会有很大的提升。
为什么要有用户态协议栈呢?
是为了解决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主要利用了mmap,将网卡中数据,直接映射到内存。netmap直接接管网卡数据,可以绕过内核协议栈。我们直接在应用程序中实现协议栈,对协议进行解析,就可以获取到网络数据了。
零拷贝,使用的是mmap方式,本质是DMA的方式,不需要CPU参与。普通copy,从磁盘copy数据到内存,需要CPU的move指令。sendfile使用的是mmap方式。**零拷贝主要是说CPU有没有参与,而不是说有没有copy。**是由主板上的DMA芯片将外设的数据copy到内存。
netmap可以在github上下载,按照上面的readme编译安装,使用比较方便。
https://github.com/luigirizzo/netmap
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;
UDP比TCP要简单很多,没有序列号,无法保证消息必达;也做不了重传;没有拥塞控制。
typedef struct _udphdr {
unsigned short sport;
unsigned short dport;
unsigned short length;
unsigned short check;
} udphdr;
UDP包组成:以太网头 + IP头 + UDP头 + UDP数据。
以太网头、IP头、UDP头都已经定义好了,这里有一个问题,UDP数据怎么定义?
用指针是不合适的,这里引入了零长数组,也叫柔性数组。
typedef struct _udppkt {
ethhdr eh; // 14
iphdr ip; // 20
udphdr udp; // 8
unsigned char data[0];
} udppkt;
柔性数组的好处,是不占空间,只是占了一个位置,作用相当于标签。
零长数组使用条件:
因为4字节对齐的关系,sizeof(udppkt)
的结果是44,所以需要使用单字节对齐,结果就是42。
#pragma pack(1)
nm_open
主要做了两个事情:
通过检测fd,就可以判断网卡是否有数据,有数据就可以直接操作内存。
如果来了一个数据,如果数据很多,网卡需要进行模拟信号和数字信号转换,DMA也需要不断把数据映射到内存,怎么把多个数据包给组织起来?比如一下来100个包,怎么把这100个包组织起来?
使用ringbuff。
对于大量数据,从网卡将数据取到内存中,CPU有两种做法:
对于大量数据,使用轮询方式比较好。这就是网络这一层,从网卡里面取数据的两种方法。事件的方式针对稀疏型的数据。
nm_nextpkt
是操作内存,取出来的就是一个完整的包。
之后通过去掉以太网头,IP头,UDP头,得到用户数据。
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表中没有我们这台机器的数据。
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 _arppkt {
ethhdr eh;
arphdr arp;
} arppkt;
解析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请求是向局域网内所有的机器发送请求,要获取目标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");
}
}
}
}
}