当前位置: 首页 > news >正文

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;
}

代码运行结果

结束

代码重在练习!

代码重在练习!

代码重在练习!

今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏夹关注,谢谢大家!!!

相关文章:

  • 多语言笔记系列:共享数据
  • 从零开始学Python游戏编程37-精灵4
  • C++中的next_permutation全排列函数
  • Java学习手册:TCP 协议基础
  • C语言教程(十六): C 语言字符串详解
  • 初识Redis · 主从复制(下)
  • Redis 核心应用场景
  • rsync实现内网两台服务器文件同步
  • 【含文档+PPT+源码】基于SpringBoot+Vue的移动台账管理系统
  • 卷积神经网络:视觉炼金术士的数学魔法
  • 【C++】二叉树进阶面试题
  • 【mongodb】系统保留的数据库名
  • CIFAR10图像分类学习笔记(三)---数据加载load_cifar10
  • 从代码学习深度学习 - 图像增广 PyTorch 版
  • AI工程pytorch小白TorchServe部署模型服务
  • Linux 基础命令入门指南
  • Java函数式编程深度解析:从Lambda到流式操作
  • R-CNN,Fast-R-CNN-Faster-R-CNN个人笔记
  • TiDB 深度解析与 K8S 实战指南
  • PowerBI企业运营分析——全动态帕累托分析
  • “五一”前多地市监部门告诫经营者:对预订客房不得毁约提价
  • 获公示拟任省辖市委副书记的胡军,已赴南阳履新
  • 研究|和去年相比,人们使用AI的需求发生了哪些变化?
  • 温氏股份一季度归母净利润20.01亿元,同比扭亏为盈
  • 中海宏洋集团4.17亿元竞得浙江绍兴宅地,溢价率20.87%
  • “电化长江”的宜昌成果:船舶航运停靠都能用电,助力一江清水向东流