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与目的IP
        char 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_ALL

int 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与目的IP
            char 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;
}

代码运行结果

结束

代码重在练习!

代码重在练习!

代码重在练习!

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值