首先简单看一下网络的ios模型
由上图可以清晰看出,主机A往主机B发送数据的过程,应用层把数据组织好后,经过传输层时,加上TCP头,经过网络层时,加上ip头,经过数据链路层时,加上以太头,经过网卡转换为数字信号,传递到主机B,主机B在通过层层拆包,最后到达应用层。
而对于用户来说,一般只需要组织好应用层的数据,下层的数据由内核协议栈来负责封装和解析,现在,尝试着跳过内核协议栈,自己实现一个简单的udp协议的解析和封装过程。
首先,要借助netmap工具,从网卡上把源数据捞出来,这个工具可以在github上下载,安装过程很简单,安装完毕后 insmod netmap.ko 把设备安装上,可以在/dev目录下看到一个netmap设备,这样我们就可以把网卡的数据拦截下来进行操作了。
下面是一个udp的首部格式
根据上图,创建一个udp的结构体
struct udphdr
{
unsigned short sprot;
unsigned short dport;
unsigned short length;
unsigned short check;
}; //8byte
ip头的首部格式
ip头的结构体
struct iphdr {
unsigned char hdrlen:4,
version: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;
}; //20byte
以太头格式
以太头结构体
struct ethhdr {
unsigned char h_dst[ETH_ADDR_LENGTH];
unsigned char h_str[ETH_ADDR_LENGTH];
unsigned short h_proto;
}//14byte
最后组织成一个udp的pkt
struct udppkt{
struct ethhdr eh;//14
struct iphdr ip;//20
struct udphdr udp;//8
unsigned char data[0];//存储应用层数据
};//42
下面我们来接收一下服务器udp的包
#include <stdio.h>
#include <sys/poll.h>
#include <arpa/inet.h>
#include <net/netmap_user.h>
#pragma pack(1) //按一字节对齐,不然解析会失败
#define NETMAP_WITH_LIBS
#define ETH_ADDR_LENGTH 6
#define PROTO_IP 0x0800
#define PROTO_ARP 0x0806
#define PROTO_UDP 17
#define PROTO_TCP 6
#define PROTO_ICMP 1
int main()
{
struct nm_pkthdr h;
struct nm_desc *nm = nm_open("netmap:eth33",NULL,0,NULL);//拦截eth33网卡数据
if (nm == NULL) return -1;
struct pollfd pfd = {0};
pfd.fd = nm->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(nm,&h);
struct ethhdr* eh =(struct ethhdr*)stream;
if(ntohs(eh->h_proto) == PROTO_IP){
struct udppkt *udp = (struct 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);
}
}
}
}
}
编译通过之后,执行一下,发现还是无法接收到数据,把程序停掉后先ping一下服务器,在运行程序,现在就可以正常接收到数据了,然后在过一段时间,又无法接收到数据了。
这是为什么呢,我们看一下路由表
这时候是没有192.168.32.145这个路由信息的,那是因为我们还没有实现arp协议,停掉程序后,用内核协议栈的arp协议把服务器添加进路由表后,才能正常接收到数据
那为什么过一段时间之后,又无法收到数据了呢,因为这是一个动态的ip,路由器隔一段时间会广播arp包,收不到145的回包,便把他从路由表里去掉了。
接下来,我们完善一下arp协议,先看看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;
};//28
然后检测到eh->hproto类型为PROTO_ARP时,返回一个arp包
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);
str2mac(arp_rt->eh.h_src, mac);
arp_rt->eh.h_proto = arp->eh.h_proto;
arp_rt->arp.h_addrlen = 6;
arp_rt->arp.h_protolen = 4;
arp_rt->arp.oper = htons(2);
str2mac(arp_rt->arp.smac, mac);//把字符串转为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;
}
.......
else if (ntohs(eh->h_proto) == PROTO_ARP) {
struct arppkt *arp = (struct arppkt *)stream;
struct arppkt arp_rt;
if (arp->arp.dip == inet_addr("192.168.32.145")) { //确认是本机地址在回包,不然会形成arp攻击
echo_arp_pkt(arp, &arp_rt, "00:50:56:33:1c:ca");//组织arp包
nm_inject(nmr, &arp_rt, sizeof(arp_rt));
printf("arp ret\n");
}
这样就可以和服务器保持一个长时间的通信了。这时还会发现,客户端ping不通服务器了,那是因为ping用到的是icmp协议,这里我们还没实现。
-----------------------------------------------------
附上icmp实现的效果图
当然,这只是一个简单的udp包解析的过程,还有许多协议没有实现,后续可以试着实现一下tcp协议栈。
网络协议栈是由多个协议栈组成的,构成了一个复杂的网络环境,实现了一个强大的数据收发功能,这个艰巨的工作还是交给内核去完成吧。