Linux网络编程【12】(原始套接字)

581 阅读7分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

原始套接字

1 基本概念

  1. 一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心;

  2. 可以接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用(直接从网卡上接收数据或发送);

  3. 开发人员可发送自己组装的数据包到网络上;

  4. 广泛应用于高级网络编程

  5. 网络专家、黑客通常会用此来编写奇特的网络程序;

  6. 与TCP,UDP传输层协议的应用层编程不同,是==基于链路层==的编程(整个TCP/IP协议的最底层)在这个层次进行编程的话,所有的协议都需要自己去指定(每一位怎们传);

2 创建原始套接字

  • ==AF_PACKET 底层协议接口==
  • SOCK_RAW 原始套接字
  • 附加协议以前可有可无,现在必须得有man 7 packet
  • sockfd理解成标识的网卡的设备文件,调用read write 就可以直接对网卡进行读取操作

vi /usr/include/linux/if_ether.h

#include <sys/types.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
    domain:通信域,协议族
        AF_PACKET  底层协议接口
    type:套接字类型
        SOCK_RAW  原始套接字
    protocol:附加协议
        ETH_P_IP   IPV4数据包
        ETH_P_ARP  ARP数据包
        ETH_P_ALL  任何协议类型的数据包
返回值:
    成功:文件描述符
    失败:-1
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>

#define ERRLOG(errmsg) do{\
                            perror(errmsg);\
                            printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
                            exit(1);\
                            }while(0)

int main(int argc, char const *argv[])
{
    int sockfd;
    //创建一个原始套接字
    if((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1)
    {
        ERRLOG("socket error");
    }

    printf("sockfd = %d\n", sockfd);
    
    return 0;
}

注意:记得执行命令时加 sudo

3 从网卡上直接读取数据包并解析

(类似与wireshark读取数据包,然后我们自己来解析)

不是所有的数据包都是四个层次,要根据协议来,但是最多有四个层次

read recv recvfrom 直接读

图片.png 先解析链路层,以太网头

MAC地址:00:0c:29:ff:de:fa ,解析的时候认为==这个字符串是18字节大小==,0是一个字节 00:是三个字节;而一共是6个字节的意思是000c29ffdefa00为一个字节。

用16进制来表示一个字节,每个字节(认为6个字节的字节)用两位16进制来表示,就是两个字节,一个是一个字节。

为什么不是6个字节加上五个:再加上'\0'的12个字节

因为你输出的是十二个字节,但是定义的是18个字节

int a = 5;//a是四个字节
printf("%d",a);//输出一个字节
//int printf(const char *format, ...);
//printf是以字符串形式输出,所以说输出的字符串占几个字节与a的类型是没有关系的。和宽度有关系,宽度就是按照多少个字节输出
//eg:如果是printf("%-4d",a);,就是按照宽度为4输出,就意味着这个字符串是按照5字节输出的,因为有一个'\0'
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>

#define ERRLOG(errmsg) do{\
                            perror(errmsg);\
                            printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
                            exit(1);\
                            }while(0)

int main(int argc, char const *argv[])
{
    int sockfd;
    //创建一个原始套接字
    if((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1)
    {
        ERRLOG("socket error");
    }

    //直接从网卡上读取数据并解析
    unsigned char buf[1600] = {0};// MTU:最大传输单元,linux是1500个字节,不能小于1500,这里定义1600
    unsigned char mac_dest[18] = {0};
    unsigned char mac_src[18] = {0};
    unsigned short mac_type;
    unsigned short ip_len;
    unsigned char ip_ttl;
    unsigned char ip_type;
    unsigned char ip_src[16] = {0};
    unsigned char ip_dest[16] = {0};
    unsigned short port_src;
    unsigned short port_dest;

    //从网卡上读取的是完整的数据包
    //使用结构体也可以,但是每种协议对应的结构体是不一样的,定义一个结构体不太方便
    //可以类似于TFTP定义一个无符号数组,一个一个字符进行解析
    while(1)
    {
        if(recvfrom(sockfd, buf, 1600, 0, NULL, NULL) == -1)
        {
            ERRLOG("recvfrom error");
        }

        //--------------解析以太网头----------------
        //目的mac地址(6个字节)
        sprintf(mac_dest, "%x:%x:%x:%x:%x:%x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);
        //源mac地址(6个字节)
        sprintf(mac_src, "%x:%x:%x:%x:%x:%x", buf[6], buf[7], buf[8], buf[9], buf[10], buf[11]);
        //协议类型(两个字节)
        mac_type = ntohs(*(unsigned short *)(buf + 12));

        printf("***********************************\n");
        printf("------ MAC头 ------\n");
        printf("源mac:%s --> 目的mac: %s\n", mac_src, mac_dest);
        printf("协议类型:%#x\n", mac_type);
        
        if(mac_type == 0x0800)
        {
            printf("------ IP数据报 ------\n");

            //--------------解析IP头----------------
            //IP总长度
            ip_len = ntohs(*(unsigned short *)(buf+16));
            //生存时间TTL
            ip_ttl = buf[22]; //buf[22] <==> *(buf+22)
            //协议类型
            ip_type = buf[23];
            //源ip地址
            sprintf(ip_src, "%d.%d.%d.%d", buf[26], buf[27], buf[28], buf[29]);
            //目的ip地址
            sprintf(ip_dest, "%d.%d.%d.%d", buf[30], buf[31], buf[32], buf[33]);

            printf("源ip: %s --> 目的ip: %s\n", ip_src, ip_dest);
            printf("IP总长度:%d, TTL:%d, 协议类型:%d\n", ip_len, ip_ttl, ip_type);

            if(ip_type == 6)
            {
                printf("------ TCP数据报 ------\n");
                //源端口号
                port_src = ntohs(*(unsigned short *)(buf+34));
                //目的端口号
                port_dest = ntohs(*(unsigned short *)(buf+36));
                printf("源端口号:%d --> 目的端口号: %d\n", port_src, port_dest);

            }
            else if(ip_type == 17) 
            {
                printf("------ UDP数据报 ------\n");
                //源端口号
                port_src = ntohs(*(unsigned short *)(buf+34));
                //目的端口号
                port_dest = ntohs(*(unsigned short *)(buf+36));
                printf("源端口号:%d --> 目的端口号: %d\n", port_src, port_dest);
            }
            else if(ip_type == 1) 
            {
                printf("------ ICMP数据报 ------\n");
            }
            else if(ip_type == 2) 
            {
                printf("------ IGMP数据报 ------\n");
            }
        }
        else if(mac_type == 0x0806)
        {
            printf("------ ARP数据报 ------\n");
        }
        else if(mac_type == 0x8035)
        {
            printf("------ RARP数据报 ------\n");
        }
        putchar(10);
    }
    return 0;
}

4 arp协议的使用

arp协议:地址解析协议,通过对方的ip地址,获取对方Mac地址

运行机制:

源主机进行组包,指定自己的ip地址和mac地址,指定对方的ip地址,通过广播的信息获取,所以mac地址是广播的mac地址(全是f),设置arp请求将数据包发送给对方,交换机接收到这个数据包之后,一看是目的mac地址是广播的,所以会将这个数据包发送给当前网段下所有的主机,主机接收到这个数据包之后对比目的ip地址,如果不是自己的则丢弃,如果是自己的,将组包之后以arp应答的方式将数据包发送给获取者。

arp数据报:

图片.png

接下来我要让ubuntu获取windows的mac地址:

以太网头:

Dest Mac:目的mac地址,广播的mac地址,全是f

Src Mac:源mac地址,ubuntu的mac地址,00:0c:29:a0:a6:f0

帧类型:后面跟的协议类型,如果是arp,则指定为0x0806

arp头:

硬件类型:以太网,1

协议类型:IP地址,0x0800

硬件地址长度:6

协议地址长度:4

OP:请求还是应答,1

​ 1(ARP请求),2(ARP应答),3(RARP请求),4(RARP应答)

发送端以太网地址:源mac地址,ubuntu的mac地址,00:0c:29:a0:a6:f0

发送端ip地址:源ip地址:ubuntu的ip地址,192.168.70.72

目的以太网地址:目的mac地址,未知,可以传0

目的ip地址:目的ip地址,windows的ip地址,192.168.70.71

图片.png

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <string.h>

#define ERRLOG(errmsg) do{\
                            perror(errmsg);\
                            printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
                            exit(1);\
                            }while(0)

int main(int argc, char const *argv[])
{
    int sockfd;
    //创建一个原始套接字
    if((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1)
    {
        ERRLOG("socket error");
    }

    //使用arp协议通过对方ip地址获取对方mac地址
    //组包
    unsigned char buf[42] = {
        //以太网头
        //目的mac地址,广播的
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        //源mac地址,自己的mac地址
        0x00, 0x0c, 0x29, 0xa0, 0xa6, 0xf0,
        //协议类型
        0x08, 0x06,

        //arp头
        //硬件类型
        0, 1,
        //协议类型
        0x08, 0x00,
        //硬件地址长度
        6,
        //协议地址长度
        4,
        //op选项,设置为1表示arp请求
        0, 1,
        //源mac地址
        0x00, 0x0c, 0x29, 0xa0, 0xa6, 0xf0,
        //源ip地址
        192, 168, 70, 72,
        //目的mac地址
        0, 0, 0, 0, 0, 0,
        //目的ip地址
        192, 168, 70, 71
    };

    //将arp请求报文发送出去,通过eth0发送出去       
    //使用ioctl函数获取本机网络接口               
    struct ifreq ethreq;      
    strncpy(ethreq.ifr_name, "ens33", IFNAMSIZ); 
    if(ioctl(sockfd, SIOCGIFINDEX, &ethreq) == -1) 
    {         
        perror("fail to ioctl");             
        exit(1);             
    }                                         
    //设置本机网络接口               
    struct sockaddr_ll sll;                   
    bzero(&sll, sizeof(sll));  
    sll.sll_ifindex = ethreq.ifr_ifindex;

    //将数据包发送给网卡
    if(sendto(sockfd, buf, 42, 0, (struct sockaddr *)&sll, sizeof(sll)) == -1)
    {
        ERRLOG("sendto error");
    }

    //接收对方的数据包并解析出mac地址
    unsigned char mac[18] = {0};
    memset(buf, 0, 42);
    while(1)
    {
        if(recvfrom(sockfd, buf, 42, 0, NULL, NULL) == -1)
        {
            ERRLOG("recvfrom error");
        }

        //先判断数据包是否是arp数据包
        if(ntohs(*(unsigned short *)(buf+12)) == 0x0806)
        {
            //再判断是否是arp应答
            if(ntohs(*(unsigned short *)(buf+20)) == 2)
            {
                sprintf(mac, "%x:%x:%x:%x:%x:%x", buf[22], buf[23], buf[24], buf[25], buf[26], buf[27]);

                printf("%d.%d.%d.%d -- %s\n", buf[28], buf[29], buf[30], buf[31], mac);
                break;
            }
        }
    }
    
    return 0;
}