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