Linux网络编程 深入Linux网络栈:原始套接字链路层实战解析
之前我们编程都是在应用层,只需在地址结构体中传 地址与端口号。然后协议栈在传输层,与网络层帮我们进行数据的封装。但这里我们要学的是在链路层进行编程
这里我想说一下,当数据到达链路层,有三个分支:ARP,IP,RARP 当数据凑够IP,到达网络层又有四个分支:ICMP,IGMP,TCP,UDP
当数据到达传输层,只看端口,不看分支
步入正题:
知识点1【原始套接字】
1、概述
原始套接字,是实现与系统核心的套接字,可以接受本机网卡上所有的数据帧(只要到达网卡的数据帧都可以收到)。
当我们利用标准套接字(SOCK_DGRAM,SOCK_STREAM),都需要借助传输层协议,然后借助网络层协议,再到达网络核心。
而原始套接字,可以直接到达网络层,或者系统核心,而我们这里要学习的,就是直接到达系统核心,在系统核心上进行编程的。
补充
2、创建原始套接字
int socket(PF_PACKET,SOCK_RAW,protocol)
//第三个参数没有固定
-
函数介绍
功能
创建链路层的原始套接字
参数
protocol:指定可以接受或发送的数据包类型
ETH_P_IP:IPV4数据包
ETH_P_ARP:ARP数据包
ETH_P_ALL:任何协议类型的数据包
返回值
成功:>0 链路层套接字
失败:<0 出错
代码演示
代码运行结果
这里我想说一下,因为我们实操偏底层的代码,因此
所有的原始套接字都需要加sudo权限来执行可执行文件./a.out
这里补充一下,如果要使用宏ETH_P_…宏需要包含头文件
#include <netinet/ether.h>
知识点2【数据包】
大家可以看到每个分支都有编号,我们下面将介绍
上图,是头部的添加以及解析流程。
1、UDP报文
UDP是传输层协议,因此它的数据是它的上一层:应用层
UDP的头部是8个字节
0-15 源端口
16-31 目的端口
32-47 UDP数据长度
48-63 UDP检验和
一定要对报文有印象,这是我们组包和解包的前提
2、TCP报文
我们可以看到头部长度4位,最大也只能表示15.
但是TCP就算不算选项,也需要20个字节,该如何存储呢?
此时最大是15,只需要让15中的每一个1,代表4B即可。这样最多可以表示64个字节。
数据是源自应用层
0-15 源端口号
16-31 目的端口号
96-99 头部长度
3、IP报文
数据包是来自传输层 的数据
0-3 版本:区分IPv4与IPv6 4→IPv4 6→IPv6
4-7 首部长度:数值0-15,单位是4B
16-31 总长度:头部长度+来自传输层的数据 总长度
72-79 协议类型
1:ICMP
2:IGMP
6:TCP
17:UDP
96-127 源IP地址
128-159 目的IP地址
4、mac报文
数据包是来自网络层的数据
0-47 目的mac地址
48-95 源mac地址
96-111 类型
0x0806 ARP数据包
0x0800 IP数据包
0x8035 RARP数据包
5、ICMP报文
我们的ping命令
不同的类型值和代码值的组合代表不同的功能
8 0代表请求
0 8代表应答
知识点3【利用原始套接字捕获网络数据】
原始套接字使用 recvfrom函数 接收
这里我补充一下,原始套接字实在链路层,我们在链路层收数据,recvfrom的参数有地址结构体指针,这里就无需传参了→NULL,因为传参也没有用,不经过网络层,传输层,无法利用其协议,因此需要用户自行解包。
代码演示
代码运行结果
这里运行之后 由于recvfrom带阻塞仍能 源源不断的收到数据,为什么呢?
这是因为我们xshell使用windows终端控制Linux终端,需要反复通信,因此会不断发送数据
1、分析mac报文头部
xx:xx:xx:xx:xx:xx\0
我们知道mac地址存储时冒分法,16进制,并且高位补零。如上,因此如果打印成为字符串的形式总计18个字节
下面我们展示分析过程(提取mac报文头部)
代码演示
// 接收数据while (1){// recvfrom 收到的是一个完整的帧数据unsigned char buf[1500] = "";int len = recvfrom(fd_sock, buf, sizeof(buf), 0, NULL, NULL);if (len < 0){perror("recvfrom");_exit(-1);}printf("len == %d\\n", len);// 分析mac报文头部char mac_src_addr[18] = "";char mac_dst_addr[18] = "";//提取源mac地址sprintf(mac_src_addr, "%02x:%02x:%02x:%02x:%02x:%02x",buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);//提取目的mac地址sprintf(mac_dst_addr, "%02x:%02x:%02x:%02x:%02x:%02x",buf[0 + 6], buf[1 + 6], buf[2 + 6], buf[3 + 6], buf[4 + 6], buf[5 + 6]);//这里补充说明:网络字节序大端存储,buf[0]存储高位数据,因此需要按照上面方法提取//提取类型unsigned short mac_type = ntohs(*((unsigned short *)(buf + 12)));//遍历printf("%s---->%s, ",mac_src_addr,mac_dst_addr);switch (mac_type){case 0x0800:printf("type:IP\\n");break;case 0x0806:printf("type:ARP\\n");break;case 0x8035:printf("type:RARP\\n");break;default:break;}}
代码运行结果
我们查看一下5a和ef分别是谁?
这里我们验证了,我们一直收到的数据 就是虚拟机和主机进行的通信
2、分析IP报文头部
要分析ip头部,需要先跳过mac头
这里看一下IP的格式
10进制点分发,我们用字符串提取,16个字节(按照最多的算)
代码演示
//分析IP报文头部//跳过mac地址报文头部unsigned char *ip = buf + 14;//这里一定要是无符号的//提取源IP与目的IPchar src_ip_addr[16] = "";char dst_ip_addr[16] = "";//提取IP的方法1//sprintf(src_ip_addr,"%d.%d.%d.%d",ip[12],ip[13],ip[14],ip[15]);//sprintf(dst_ip_addr,"%d.%d.%d.%d",ip[12 + 4],ip[13 + 4],ip[14 + 4],ip[15 + 4]);//提取IP的方法二inet_ntop(AF_INET,ip + 12,src_ip_addr,sizeof(src_ip_addr));inet_ntop(AF_INET,ip + 12,dst_ip_addr,sizeof(dst_ip_addr));printf("\\t%s---->%s, ",src_ip_addr,dst_ip_addr);//提取类型unsigned char ip_type = ip[9];switch (ip_type){case 1:printf("type:ICMP, ");break;case 2:printf("type:IGMP, ");break;case 6:printf("type:TCP, ");break;case 17:printf("type:UCP, ");break;default:break;}//提取一下版本与首部长度unsigned char version = ip[0];unsigned char len_head = ip[0];version >>= 4;len_head &= 0x0F;printf("version :%d, len_head = %d\\n",version,len_head * 4); //注意这里一定不要用%c遍历,因为%c默认会遍历其ASCII码值,而并非数值}
代码运行结果
代码中的注意事项:
1、当遍历char 类型数据的时候,要显示数值使用%d,如果要显示ascll码才使用%c
2、ip无符号字符数组类型,buf也是无符号字符数组类型
3、ip的提取有两种方式一种是组包法(sprintf),另一种是inet_ntop()法
3、分析TCP和UDP报文头部
代码演示(含 数据遍历)
//分析TCP报文//从IP报文位置跳转到TCP报文位置char *tcp = ip + ip_len_head;//提取目的端口号和源端口号unsigned short src_port_id_tcp = ntohs(*((unsigned short *)tcp));unsigned short dst_port_id_tcp = ntohs(*((unsigned short *)(tcp + 2)));printf("\\t\\t%hu---->%hu\\n",src_port_id_tcp,dst_port_id_tcp);//提取数据内容//跳转到数据报文位置char tcp_len_head = (tcp[12]>>4) * 4;char *data_udp = tcp + tcp_len_head;printf("%s\\n",data_udp);
代码运行结果
4、整体代码
由于 数据内容遍历影响 结果的查看,我们这里不遍历数据
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h> //socket()
#include <unistd.h>
#include <netinet/ether.h> //ETH_P_ALLint main(int argc, char const *argv[])
{// 创建原始套接字int fd_sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));if (fd_sock < 0){perror("sock");_exit(-1);}printf("fd_sock == %d\\n", fd_sock);// 接收数据while (1){// recvfrom 收到的是一个完整的帧数据unsigned char buf[1500] = "";int len = recvfrom(fd_sock, buf, sizeof(buf), 0, NULL, NULL);if (len < 0){perror("recvfrom");_exit(-1);}printf("len == %d\\n", len);// 分析mac报文头部char mac_src_addr[18] = "";char mac_dst_addr[18] = "";// 提取源mac地址sprintf(mac_src_addr, "%02x:%02x:%02x:%02x:%02x:%02x",buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);// 提取目的mac地址sprintf(mac_dst_addr, "%02x:%02x:%02x:%02x:%02x:%02x",buf[0 + 6], buf[1 + 6], buf[2 + 6], buf[3 + 6], buf[4 + 6], buf[5 + 6]);// 这里补充说明:网络字节序大端存储,buf[0]存储高位数据,因此需要按照上面方法提取// 提取类型unsigned short mac_type = ntohs(*((unsigned short *)(buf + 12)));// 遍历printf("%s---->%s, ", mac_src_addr, mac_dst_addr);switch (mac_type){case 0x0800:printf("type:IP\\n");// 分析IP报文头部// 跳过mac地址报文头部unsigned char *ip = buf + 14; // 这里一定要是无符号的// 提取源IP与目的IPchar src_ip_addr[16] = "";char dst_ip_addr[16] = "";// 提取IP的方法1// sprintf(src_ip_addr,"%d.%d.%d.%d",ip[12],ip[13],ip[14],ip[15]);// sprintf(dst_ip_addr,"%d.%d.%d.%d",ip[12 + 4],ip[13 + 4],ip[14 + 4],ip[15 + 4]);// 提取IP的方法二inet_ntop(AF_INET, ip + 12, src_ip_addr, sizeof(src_ip_addr));inet_ntop(AF_INET, ip + 12, dst_ip_addr, sizeof(dst_ip_addr));printf("\\t%s---->%s, ", src_ip_addr, dst_ip_addr);// 提取一下版本与首部长度unsigned char version = ip[0];unsigned char ip_len_head = ip[0];version >>= 4;ip_len_head &= 0x0F;ip_len_head *= 4;printf("IP_version :%d, IP_len_head = %d, ", version, ip_len_head);// 注意这里一定不要用%c遍历,因为%c默认会遍历其ASCII码值,而并非数值// 提取类型unsigned char ip_type = ip[9];switch (ip_type){case 1:printf("type:ICMP\\n");break;case 2:printf("type:IGMP\\n");break;case 6:printf("type:TCP\\n");//分析TCP报文//从IP报文位置跳转到TCP报文位置char *tcp = ip + ip_len_head;//提取目的端口号和源端口号unsigned short src_port_id_tcp = ntohs(*((unsigned short *)tcp));unsigned short dst_port_id_tcp = ntohs(*((unsigned short *)(tcp + 2)));printf("\\t\\t%hu---->%hu\\n",src_port_id_tcp,dst_port_id_tcp);//提取数据内容//跳转到数据报文位置char tcp_len_head = (tcp[12]>>4) * 4;char *data_tcp = tcp + tcp_len_head;//printf("%s\\n",data_udp);break;case 17:printf("type:UCP\\n");//分析UDP报文//从IP报文位置跳转到UDP报文位置char *udp = ip + ip_len_head;//提取目的端口号和源端口号unsigned short src_port_id_udp = ntohs(*((unsigned short *)udp));unsigned short dst_port_id_udp = ntohs(*((unsigned short *)(udp + 2)));printf("\\t\\t%hu---->%hu\\n",src_port_id_udp,dst_port_id_udp);//提取数据内容//跳转到数据报文位置char *data_udp = udp + 8;//printf("%s\\n",data_udp);break;default:break;}break;case 0x0806:printf("type:ARP\\n");break;case 0x8035:printf("type:RARP\\n");break;default:break;}}// 关闭套接字close(fd_sock);return 0;
}
代码运行结果
结束
代码重在练习!
代码重在练习!
代码重在练习!
今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏夹关注,谢谢大家!!!