天天看點

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

繼續閱讀