大多數的網絡程式設計都是在應用層接收資料和發送資料的,本文介紹在資料鍊路層的網絡程式設計方法,介紹如何在資料鍊路層直接接收從實體層發過來的原始資料資料包,文章給出了一個完整的範例程式。
1. 概述
- linux下進行網絡程式設計通常都是使用socket在應用層接收和發送資料;
- 本文介紹如何繞過資料鍊路層、網絡層和傳輸層對資料包的處理,直接從資料鍊路層接收從實體層發過來的原始資料;
- 本文所介紹的内容在實際程式設計中很少會用到,但希望對讀者了解網絡結構和協定能有幫助;
- 本文會提供了直接從資料鍊路層接收資料的範例程式,源代碼在ubuntu 20.04下編譯運作成功;
- 本文可能并不适合初學者,但并不妨礙初學者收藏此文,以便在今後學習。
2. socket程式設計
- 在看下面的内容之前還是要簡單地回顧一下TCP/IP的五層網絡模型(OSI 七層架構的簡化版)
- 應用層
- 傳輸層
- 網絡層
- 資料鍊路層
- 實體層
- 使用socket進行網絡程式設計時,我們通常隻需要關心需要發送的資料,資料發送後,要發送的資料将從應用層向下傳遞
- 在TCP/UDP(傳輸)層加入一個TCP頭
- 在IP(網絡)層加上一個IP頭
- 在資料鍊路層加上一個以太網頭
- 交給實體層傳輸
- 當我們在應用層進行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(......)以後,會發生什麼呢?
- 資料從應用層被送到傳輸層,傳輸層給這個資料加上一個UDP 頭;
- (UDP頭+DATA)從傳輸層被送到網絡層,IP協定會給資料包再加上一個IP頭;
- (IP頭+UDP頭+DATA)從網絡層被送到了資料鍊路層,資料鍊路層的以太網協定會給這個資料包加上一個以太網頭;
- (以太網頭+IP頭+UDP頭+DATA)從資料鍊路層被送到了實體層,資料就被發送走了。
圖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()函數并成功傳回時,都發生了什麼事情呢?
- 原始資料包(以太網頭+IP頭+UDP頭+DATA)通過網卡驅動程式發送到資料鍊路層;
- 資料鍊路層從原始資料包中提取出以太網頭,資料包的其餘部分發送給網絡層(IP頭+UDP頭+DATA);
- 網絡層從資料中提取出IP頭,其餘部分交給傳輸層(UDP頭+DATA);
- 傳輸層從資料中提取出UDP頭,其餘部分交給應用程式(DATA);
- 是以我們在應用層收到的就隻有資料了,報頭已經被各協定層提取出來
圖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核心有深入的了解。
圖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;
圖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. */
};
圖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資料包為例說明接收資料的步驟:
- 打開一個raw socket;
- 在記憶體中配置設定一個buffer,并接收資料;
- 提取資料鍊路層的以太網協定頭;
- 提取解開網絡層的IP協定頭;
- 提取解開傳輸層的UDP協定頭;
- 提取收到的資料
- 下面是一個監聽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
在我的電腦上看到的是這樣的:
圖6:收到的 UDP 資料包
歡迎通路我的部落格:https://whowin.cn