天天看点

Linux下如何在数据链路层接收原始数据包

作者:whowin1963

大多数的网络编程都是在应用层接收数据和发送数据的,本文介绍在数据链路层的网络编程方法,介绍如何在数据链路层直接接收从物理层发过来的原始数据数据包,文章给出了一个完整的范例程序。

1. 概述

  • linux下进行网络编程通常都是使用socket在应用层接收和发送数据;
  • 本文介绍如何绕过数据链路层、网络层和传输层对数据包的处理,直接从数据链路层接收从物理层发过来的原始数据;
  • 本文所介绍的内容在实际编程中很少会用到,但希望对读者理解网络结构和协议能有帮助;
  • 本文会提供了直接从数据链路层接收数据的范例程序,源代码在ubuntu 20.04下编译运行成功;
  • 本文可能并不适合初学者,但并不妨碍初学者收藏此文,以便在今后学习。

2. socket编程

  • 在看下面的内容之前还是要简单地回顾一下TCP/IP的五层网络模型(OSI 七层架构的简化版)
  1. 应用层
  2. 传输层
  3. 网络层
  4. 数据链路层
  5. 物理层
  • 使用socket进行网络编程时,我们通常只需要关心需要发送的数据,数据发送后,要发送的数据将从应用层向下传递
  1. 在TCP/UDP(传输)层加入一个TCP头
  2. 在IP(网络)层加上一个IP头
  3. 在数据链路层加上一个以太网头
  4. 交给物理层传输
  • 当我们在应用层进行socket编程时,我们通常会这样发送数据(以UDP为例):
......
struct sockaddr_in addr;
int sock = socket(AF_INET, SOCK_DGRAM, 0);
......
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(DST_IP);       // 目的IP
addr.sin_port = htons(PORT);                    // 端口号
.....
sendto(sock, &DATA, DATA_LEN, 0, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
close(fd);           
  • 当我们把DATA给sendto(......)以后,会发生什么呢?
  1. 数据从应用层被送到传输层,传输层给这个数据加上一个UDP 头;
  2. (UDP头+DATA)从传输层被送到网络层,IP协议会给数据包再加上一个IP头;
  3. (IP头+UDP头+DATA)从网络层被送到了数据链路层,数据链路层的以太网协议会给这个数据包加上一个以太网头;
  4. (以太网头+IP头+UDP头+DATA)从数据链路层被送到了物理层,数据就被发送走了。
Linux下如何在数据链路层接收原始数据包

图1:使用socket从应用程序发送数据的过程

  • 当我们在应用层进行socket编程时,我们通常会这样接收数据(以UDP为例):
......
struct sockaddr_in addr;
int addr_len = sizeof(struct sockaddr_in);
int sock = socket(AF_INET, SOCK_DGRAM, 0);
......
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = inet_addr(SERVER_IP);
......
recvfrom(sock, buffer, sizeof(buffer), 0, &addr, &addr_len);
......           
  • 当我们调用recvfrom()函数并成功返回时,都发生了什么事情呢?
  1. 原始数据包(以太网头+IP头+UDP头+DATA)通过网卡驱动程序发送到数据链路层;
  2. 数据链路层从原始数据包中提取出以太网头,数据包的其余部分发送给网络层(IP头+UDP头+DATA);
  3. 网络层从数据中提取出IP头,其余部分交给传输层(UDP头+DATA);
  4. 传输层从数据中提取出UDP头,其余部分交给应用程序(DATA);
  5. 所以我们在应用层收到的就只有数据了,报头已经被各协议层提取出来
Linux下如何在数据链路层接收原始数据包

图2:在应用程序中用socket接收数据的过程

  • 很显然,在应用层进行网络编程,我们不需要关心各协议层的报头,各层的协议栈会为我们处理好所有报头;
  • 但这样的编程显然也是受限的,除了TCP和UDP以外,你还知道有什么其它的网络通信形式吗?这种在应用层的编程仅能收到发给这台机器的数据,而且在你收到的数据中,并没有源和目的地址的任何信息;
  • 从图1和图2可以看出,当我们需要在传输层编程时,实际上就是比在应用层编程多了一个UDP(TCP)头;同理,当我们需要在网络层编程时,也就是比在传输层编程多加一个IP头;
  • 本文介绍在数据链路层编程,与在应用层的网络编程相比,只是要多封装(提取)三个数据头:以太网头、IP头、UDP(TCP)头

3. raw socket

  • raw socket也是一种socket,常用于接收原始数据包,所谓原始数据包指的是从物理层直接传送出来的数据包;使用raw socket可以绕过通常的TCP/IP处理流程,在应用程序中直接收到原始数据包(见图3)。使用raw socket编程,并不需要对Linux内核有深入的了解。
Linux下如何在数据链路层接收原始数据包

图3:在应用程序中使用raw socket接收数据

  • 打开raw socket

和普通socket一样,打开一个raw socket,必须要知道三件事:socket family、socket type 和 protocol;

对raw socket而言,socket family为AF_PACKET,socket type为SOCK_RAW;

接收数据时,protocol请参考头文件if_ether.h;接收所有数据包,protocol使用宏ETH_P_ALL;接收IP数据包,protocol使用宏ETH_P_IP。

int sock_raw;
sock_raw = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock_raw < 0) {
    printf("error in socket\n");
    return -1;
}           

发送数据时,protocol要参考头文件in.h,通常protocol使用IPPROTO_RAW;

sock_raw = socket(AF_PACKET, SOCK_RAW, IPPROTO_RAW);
if (sock_raw == -1)
    printf("error in socket");
           

4. 数据报的报头

  • 前面提过,应用程序使用socket发送数据(以UDP为例)的时候,在经过传输层时,要增加一个UDP头,经过网络层时,要再加上一个IP头,在经过数据链路层时,还要加上一个以太网头,然后才能交给物理层发送,见图1;
  • 同样,应用程序使用socket接收数据(以UDP为例)时,数据从物理层经过数据链路层时,将去除以太网头,在经过网络层时,要去掉IP头,在经过传输层时,还要去掉UDP头,所以到达应用程序时,就只有数据了,见图2;
  • 当使用raw_socket在数据链路层编程时,收到的数据需要自行解开以太网头、IP头、UDP头;而发送数据时,需要自行在数据上封装UDP头、IP头和以太网头;
  • 网络报文的报头的通用定义

网络报文的报头分为三个部分:传输层的传输层协议头、网络层的网络层协议头和数据链路层的以太网头,见图4;

Linux下如何在数据链路层接收原始数据包

图4:网络报头的通用定义

以下仅就本文范例中用到的报头结构做一个简单说明。

  • 数据链路层的以太网头

以太网报头定义在头文件linux/if_ether.h中:

struct ethhdr {
    unsigned char  h_dest[ETH_ALEN];    /* destination eth addr  */
    unsigned char  h_source[ETH_ALEN];  /* source ether addr  */
    __be16         h_proto;             /* packet type ID field  */
} __attribute__((packed));           

h_dest字段为目的MAC地址,h_source字段为源MAC地址;

h_proto表示当前数据包在网络层使用的协议,Linux支持的协议在头文件linux/if_ether.h中定义;通常在网络层使用的IP协议,这个字段的值是0x0800(ETH_P_IP)。

  • 网络层的 IP 头

IP(Internet Protocol)协议是网络层最常用的协议;

IP报头定义在头文件linux/ip.h中;

struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u8  ihl:4,
          version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
    __u8  version:4,
          ihl:4;
#else
#error  "Please fix <asm/byteorder.h>"
#endif
    __u8    tos;
    __be16  tot_len;
    __be16  id;
    __be16  frag_off;
    __u8    ttl;
    __u8    protocol;
    __sum16 check;
    __be32  saddr;
    __be32  daddr;
    /*The options start here. */
};           
Linux下如何在数据链路层接收原始数据包

图5:IP 报头

version - IPV4时,version=4

ihl(Internet Header Length) - 报头的长度,表示报头占用多少个32 bits字(4 字节),IP报头最少要20 bytes,也就是ihl=5,最长可以是60 bytes,也就是ihl=15;ihl x 4就是IP报头占用的字节数;

tos - 这个字段通常并不使用,可以填0;

tot_len(Total Length) - 报文全长,包括IP头和IP payload,单位是字节;

id - IP报文的唯一标识,同一个IP报文分片传输时,其id是一样的,便于分片重组;

frag_off(Fragment Offest) - 其中bit 0、bit 1 和 bit 2用于控制和识别分片,bit 3 - 15这13个bit表示每个分片相对于原始报文开头的偏移量,以8字节作单位;

ttl(Time To Live) - 这个字段是为了防止报文在互联网上永远存在(比如进入路由环路),在发送报文时设置这个值,最大255,通常设置为64,每经过一个路由器,该值将减1,当为0时,该报文将被丢弃;

protocol - 该字段定义了在传输层所用的协议,协议号列表文件在/etc/protocols文件中,UDP为17,TCP为6;

check - IP头的检查和,不包括payload,关于IP头的检查和的计算方法有专门的文章介绍,开一参考这个链接(https://www.thegeekstuff.com/2012/05/ip-header-checksum/),也可以参考本文的范例源代码;

saddr - 源IP地址,此字段是一个4字节的IP地址转为二进制并拼在一起所得到的32位值;例如:10.9.8.7是00001010 00001001 00001000 00000111

daddr - 目的IP地址,表示方法与saddr一样;

当数据链路层的h_proto字段为ETH_P_IP时,表示网络层使用的是IP(Internet Protocol)协议;实际上,网络层支持一些其它的协议,比如:Ethernet Loopback、Xerox PUP等;

网络层和传输层支持的协议可以在文件/etc/protocols中查看。

  • 传输层的 UDP 头

UDP(User Datagram Protocol)是传输层最常用的协议之一;

UDP头定义在头文件linux/udp.h中;

struct udphdr {
    __be16	source;
    __be16	dest;
    __be16	len;
    __sum16	check;
};           

source - 来源连接端口号,可选项,如果不使用,填充0;

dest - 目的连接端口号;

len - 报文长度;

check - 报头的校验和,在IPv4中是可选的,IPv6中是强制的,如果不使用,应填充0;校验和的计算还涉及到UDP的伪头部,请参考我的另一篇文章《如何计算UDP头的checksum]》。

5. 使用 raw socket 接收数据

  • 把上面介绍的内容综合起来就可以编写出一个在数据链路层使用raw socket接收原始数据包的程序了;
  • 以接收一个UDP数据包为例说明接收数据的步骤:
  1. 打开一个raw socket;
  2. 在内存中分配一个buffer,并接收数据;
  3. 提取数据链路层的以太网协议头;
  4. 提取解开网络层的IP协议头;
  5. 提取解开传输层的UDP协议头;
  6. 提取收到的数据
  • 下面是一个监听UDP数据包的范例程序,文件名receive_udp_packet.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <malloc.h>

#include <sys/socket.h>
#include <sys/types.h>

#include <arpa/inet.h>           // to avoid warning at inet_ntoa

#include<linux/if_packet.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/tcp.h>

#define LOG_FILE       "udp_packets.log"    // log file name

struct ethhdr *eth_hdr;
struct iphdr *ip_hdr;
struct udphdr *udp_hdr;

/*****************************************************************************
* Function: unsigned int ethernet_header(unsigned char *buffer, int buflen)
* Description: Extracting the Ethernet header
*              struct ethhdr is defined in if_ether.h
* 
* Entry: buffer     data packet
*        buf_len    length of data packet
* Return: protocol of network layer or -1 when error
*****************************************************************************/
int ethernet_header(unsigned char *buffer, int buf_len) {
    if (buf_len < sizeof(struct ethhdr)) {
        printf("Wrong data packet.\n");
        return -1;
    }

    eth_hdr = (struct ethhdr *)(buffer);

    return ntohs(eth_hdr->h_proto);
}
/*********************************************************************************
* Function: void log_ethernet_header(FILE *log_file, struct ethhdr *eth_hdr)
* Description: write ether header into log file
* 
* Entry:   log_file    log file object
*          eth_hdr     pointer of ethernet header structure
*********************************************************************************/
void log_ethernet_header(FILE *log_file, struct ethhdr *eth_hdr) {
    fprintf(log_file, "\nEthernet Header\n");
    fprintf(log_file, "\t|-Source MAC Address     : %.2X-%.2X-%.2X-%.2X-%.2X-%.2X\n", 
            eth_hdr->h_source[0], eth_hdr->h_source[1], eth_hdr->h_source[2], 
            eth_hdr->h_source[3], eth_hdr->h_source[4], eth_hdr->h_source[5]);
    fprintf(log_file, "\t|-Destination MAC Address: %.2X-%.2X-%.2X-%.2X-%.2X-%.2X\n", 
            eth_hdr->h_dest[0], eth_hdr->h_dest[1], eth_hdr->h_dest[2], 
            eth_hdr->h_dest[3], eth_hdr->h_dest[4], eth_hdr->h_dest[5]);
    fprintf(log_file, "\t|-Protocol               : 0X%04X\n", ntohs(eth_hdr->h_proto));
    // ETH_P_IP = 0x0800, ETH_P_LOOP = 0X0060
}
/********************************************************************************
* Function: unsigned int ip_header(unsigned char *buffer, int buf_len)
* Description: Extracting the IP header
*              struct iphdr is defined in ip.h
* 
* Entry: buffer     data packet
*        buf_len    length of data packet
* return: protocol of transport layer or -1 when error
********************************************************************************/
int ip_header(unsigned char *buffer, int buf_len) {
    if (buf_len < sizeof(struct ethhdr) + 20) {
        printf("Wrong data packet.\n");
        return -1;
    }

    ip_hdr = (struct iphdr *)(buffer + sizeof(struct ethhdr));
    int tot_len = ntohs(ip_hdr->tot_len);

    if (buf_len < sizeof(struct ethhdr) + tot_len) {
        printf("Wrong data packet.\n");
        return -1;
    }
    return (int)ip_hdr->protocol;
}
/********************************************************************************
* Function: void log_ip_header(FILE *log_file, struct iphdr *ip_hdr)
* Description: write ip header into log file
* 
* Entry:   log_file        log file's handler
*          ip_hdr          the pointer of ip header structure
********************************************************************************/
void log_ip_header(FILE *log_file, struct iphdr *ip_hdr) {
    struct sockaddr_in source, dest;

    memset(&source, 0, sizeof(source));
    source.sin_addr.s_addr = ip_hdr->saddr;     
    memset(&dest, 0, sizeof(dest));
    dest.sin_addr.s_addr = ip_hdr->daddr;     

    fprintf(log_file, "\nIP Header\n");

    fprintf(log_file, "\t|-Version               : %d\n", (unsigned int)ip_hdr->version);
    fprintf(log_file, "\t|-Internet Header Length: %d DWORDS or %d Bytes\n", (unsigned int)ip_hdr->ihl, ((unsigned int)(ip_hdr->ihl)) * 4);
    fprintf(log_file, "\t|-Type Of Service       : %d\n", (unsigned int)ip_hdr->tos);
    fprintf(log_file, "\t|-Total Length          : %d  Bytes\n", ntohs(ip_hdr->tot_len));
    fprintf(log_file, "\t|-Identification        : %d\n", ntohs(ip_hdr->id));
    fprintf(log_file, "\t|-Time To Live          : %d\n", (unsigned int)ip_hdr->ttl);
    fprintf(log_file, "\t|-Protocol              : %d\n", (unsigned char)ip_hdr->protocol);
    fprintf(log_file, "\t|-Header Checksum       : %d\n", ntohs(ip_hdr->check));
    fprintf(log_file, "\t|-Source IP             : %s\n", inet_ntoa(source.sin_addr));
    fprintf(log_file, "\t|-Destination IP        : %s\n", inet_ntoa(dest.sin_addr));
}
/************************************************************************
* Function: udp_header(FILE *log_file, struct iphdr *ip_hdr)
* Description: Extracting the UDP header
* 
* Entry:   log_file    log file
*          ip_hdr      pointer of IP header
************************************************************************/
void udp_header(FILE *log_file, struct iphdr *ip_hdr) {
    fprintf(log_file, "\nUDP Header\n");

    udp_hdr = (struct udphdr *)((unsigned char *)ip_hdr + (unsigned int)ip_hdr->ihl * 4);

    fprintf(log_file, "\t|-Source Port     : %d\n", ntohs(udp_hdr->source));
    fprintf(log_file, "\t|-Destination Port: %d\n", ntohs(udp_hdr->dest));
    fprintf(log_file, "\t|-UDP Length      : %d\n", ntohs(udp_hdr->len));
    fprintf(log_file, "\t|-UDP Checksum    : %d\n", ntohs(udp_hdr->check));
}
/**************************************************************************
* Function: void udp_payload(FILE *log_file, struct udphdr *udp_hdr)
* Description: Show data
* 
* Entry: buffer     data packet
*        buf_len    length of data packet
**************************************************************************/
void udp_payload(FILE *log_file, struct udphdr *udp_hdr) {
    int i = 0;
    unsigned char *data = (unsigned char *)udp_hdr + sizeof(struct udphdr);
    fprintf(log_file, "\nData\n");
    int data_len = ntohs(udp_hdr->len) - sizeof(struct udphdr);

    for (i = 0; i < data_len; i++) {
        if (i != 0 && i % 16 == 0) 
            fprintf(log_file, "\n");
        fprintf(log_file, " %.2X ", data[i]);
    }

    fprintf(log_file, "\n");
}
/*****************************************************
* Main 
*****************************************************/
int main() {
    FILE* log_file;                             // log file
    struct sockaddr saddr;

    int sock_raw, saddr_len, buf_len;
    int ret_value = 0;

    int done = 0;               // exit loop when done=1
    int udp = 0;                // udp packet count

    // open a raw socket
    sock_raw = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); 
    if (sock_raw < 0) {
        printf("Error in socket\n");
        return -1;
    }
    // Allocate a block of memory for the receive buffer
    unsigned char *buffer = (unsigned char *)malloc(65536); 
    if (buffer == NULL) {
        printf("Unable to allocate memory.\n");
        close(sock_raw);
        return -1;
    }
    memset(buffer, 0, 65536);

    // Create a log file for storing output
    log_file = fopen(LOG_FILE, "w");
    if (!log_file) {
        printf("Unable to open %s\n", LOG_FILE);
        free(buffer);
        close(sock_raw);
        return -1;
    }

    printf("starting .... %d\n", sock_raw);
    while (!done) {
        // Receive data packet
        saddr_len = sizeof saddr;
        buf_len = recvfrom(sock_raw, buffer, 65536, 0, &saddr, (socklen_t *)&saddr_len);

        if (buf_len < 0) {
            printf("Error in reading recvfrom function\n");
            ret_value = -1;
            goto QUIT;
        }

        fflush(log_file);
        // Extracting the Ethernet header
        if (ethernet_header(buffer, buf_len) != ETH_P_IP) {
            // drop the packet if network layer protocol is not IP
            continue;
        }
        // Extracting the IP header
        if (ip_header(buffer, buf_len) != 17) {
            // drop packet if transport layer protocol is not UDP
            continue;
        }
        fprintf(log_file, "\n**** UDP packet %02d*********************************\n", udp + 1);
        // Write ethernet header into log file
        log_ethernet_header(log_file, eth_hdr);
        // Write IP header into log file
        log_ip_header(log_file, ip_hdr);
        // Extracting the UDP header and write into log file
        udp_header(log_file, ip_hdr);
        // write UDP payload into log file
        udp_payload(log_file, udp_hdr);
        // exit when the count of received udp packets is more than 10
        if (++udp >= 10) done = 1;
    }

QUIT:
    fclose(log_file);
    free(buffer);
    close(sock_raw);        // close raw socket 
    printf("DONE!!!!\n");
    return ret_value;
}
           
  • 该程序使用raw_socket在数据链路层直接接收从物理层发过来的数据,数据不会经过各个协议层的处理;
  • 在应用层进行socket进行网络编程时,端口号可以用于区分接收数据的应用程序,使用raw socket接收数据时,端口号没有用;
  • 该程序将收到的udp数据包的以太网头、IP头、UDP头提取出来,和数据一起写入到文件udp_packets.log文件中;
  • 该程序丢弃了除UDP包以外的所有其它数据包;
  • 为了避免冗长的log文件,这个程序接收10个UDP数据包后会自动退出;
  • 该程序经过扩展后可以成为一个简单的数据包嗅探器;
  • 编译程序
gcc -Wall receive_udp_packet.c -o receive_udp_packet           
  • 运行程序
sudo ./receive_udp_packet           

这个程序必须要使用root权限运行,因为使用了raw socket

  • 测试程序

最好使用局域网中的两台机器(虚拟机)进行测试,因为在下面的测试方法中,从本机发送时,以太网头中的源和目的MAC地址可能会被填0;

假定A机的IP地址为 192.168.2.114,在A机运行程序receive_udp_packet程序;

我们从B机(与A机的IP不同),发送数据:

echo -n "udp packet 01" > /dev/udp/192.168.2.114/8000
echo -n "udp packet 02" > /dev/udp/192.168.2.114/8001
......           

8000和8001是端口号,可以是任意的;

连接在网络上的A机,有可能会从网络上收到其它的UDP包,所以A机启动receive_udp_packet程序后,要尽快在B机发出数据,否则可能你还没有发出数据,A机已经收到了10条UDP包并自动退出;

查看log文件,看看有没有你发出来的数据

cat udp_packets.log           

在我的电脑上看到的是这样的:

Linux下如何在数据链路层接收原始数据包

图6:收到的 UDP 数据包

欢迎访问我的博客:https://whowin.cn

继续阅读