69天探索操作系统-第37天:使用原始套接字进行网络协议处理和报文构造

197 阅读6分钟

pro10.avif

1.介绍

原始套接字编程是一种底层网络技术,允许开发人员直接与网络协议栈交互。与标准套接字不同,标准套接字抽象了许多网络层的细节,而原始套接字则提供了对数据包头和协议行为的细粒度控制。这使得原始套接字非常适合自定义协议实现、网络监控和数据包构造任务。

原始套接字在需要操作数据包头部或实现操作系统不支持的协议的场景中特别有用。例如,可以使用原始套接字创建自定义的TCP/IP数据包、分析网络流量,甚至模拟网络攻击进行安全测试。然而,使用原始套接字需要对网络协议有深入的了解,并且要小心处理数据包结构,以免发生错误。

image.png

2. 套接字类型和协议

2.1 原始套接字创建

原始套接字是通过使用 SOCK_RAW 类型的 socket() 系统调用创建的。这允许套接字绕过传输层,直接与网络层交互。协议参数指定了套接字将处理的包类型,例如 IPPROTO_TCP 用于 TCP 包或 IPPROTO_RAW 用于自定义 IP 包。

以下代码演示了如何创建原始套接字并启用在数据包中包含IP头部:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

int create_raw_socket() {
    int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    int one = 1;
    if (setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one)) < 0) {
        perror("setsockopt failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    return sockfd;
}

运行代码:

gcc -o raw_socket raw_socket.c
./raw_socket

3. 原始套接字实现

3.1 完成TCP数据包构造

构造一个TCP数据包涉及创建IP头部、TCP头部和数据负载,并计算必要的校验和。以下代码演示了如何使用原始套接字来构造和发送TCP数据包:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

struct pseudo_header {
    uint32_t source_address;
    uint32_t dest_address;
    uint8_t placeholder;
    uint8_t protocol;
    uint16_t tcp_length;
};

uint16_t calculate_tcp_checksum(struct iphdr *iph, struct tcphdr *tcph, char *data, int data_len) {
    struct pseudo_header psh;
    char *pseudo_packet;
    uint16_t checksum;

    psh.source_address = iph->saddr;
    psh.dest_address = iph->daddr;
    psh.placeholder = 0;
    psh.protocol = IPPROTO_TCP;
    psh.tcp_length = htons(sizeof(struct tcphdr) + data_len);

    int psize = sizeof(struct pseudo_header) + sizeof(struct tcphdr) + data_len;
    pseudo_packet = malloc(psize);

    memcpy(pseudo_packet, &psh, sizeof(struct pseudo_header));
    memcpy(pseudo_packet + sizeof(struct pseudo_header), tcph, sizeof(struct tcphdr));
    memcpy(pseudo_packet + sizeof(struct pseudo_header) + sizeof(struct tcphdr), data, data_len);

    checksum = 0;
    uint16_t *ptr = (uint16_t *)pseudo_packet;
    for (int i = 0; i < psize/2; i++) {
        checksum += ptr[i];
    }
    if (psize % 2 == 1) {
        checksum += ((uint16_t)pseudo_packet[psize-1]) << 8;
    }

    while (checksum >> 16) {
        checksum = (checksum & 0xFFFF) + (checksum >> 16);
    }

    free(pseudo_packet);
    return ~checksum;
}

void send_tcp_packet(int sockfd, char *src_ip, char *dst_ip,
                    int src_port, int dst_port, char *data) {
    char packet[4096];
    struct iphdr *iph = (struct iphdr *)packet;
    struct tcphdr *tcph = (struct tcphdr *)(packet + sizeof(struct iphdr));
    char *data_ptr = packet + sizeof(struct iphdr) + sizeof(struct tcphdr);
    struct sockaddr_in sin;

    memset(packet, 0, 4096);

    iph->ihl = 5;
    iph->version = 4;
    iph->tos = 0;
    iph->tot_len = sizeof(struct iphdr) + sizeof(struct tcphdr) + strlen(data);
    iph->id = htonl(54321);
    iph->frag_off = 0;
    iph->ttl = 255;
    iph->protocol = IPPROTO_TCP;
    iph->check = 0;
    iph->saddr = inet_addr(src_ip);
    iph->daddr = inet_addr(dst_ip);

    iph->check = calculate_ip_checksum((unsigned short *)packet, iph->ihl*4);

    tcph->source = htons(src_port);
    tcph->dest = htons(dst_port);
    tcph->seq = htonl(1);
    tcph->ack_seq = 0;
    tcph->doff = 5;
    tcph->fin = 0;
    tcph->syn = 1;
    tcph->rst = 0;
    tcph->psh = 0;
    tcph->ack = 0;
    tcph->urg = 0;
    tcph->window = htons(5840);
    tcph->check = 0;
    tcph->urg_ptr = 0;

    strcpy(data_ptr, data);

    tcph->check = calculate_tcp_checksum(iph, tcph, data, strlen(data));

    sin.sin_family = AF_INET;
    sin.sin_port = htons(dst_port);
    sin.sin_addr.s_addr = iph->daddr;

    if (sendto(sockfd, packet, iph->tot_len, 0,
               (struct sockaddr *)&sin, sizeof(sin)) < 0) {
        perror("sendto failed");
    }
}

运行代码:

gcc -o tcp_packet tcp_packet.c
./tcp_packet

上面做了什么?

  1. 伪头部: 伪头部用于计算TCP校验和,包括源和目的IP地址、协议以及TCP长度。
  2. 校验和计算: calculate_tcp_checksum() 函数计算 TCP 头部和有效载荷的校验和。
  3. 数据包构造: send_tcp_packet() 函数构建 IP 和 TCP 头部,填写有效载荷,并使用 sendto() 发送数据包。

4. 网络协议处理

4.1 协议分析仪实现

协议分析仪捕获并分析网络流量。以下代码使用pcap库捕获数据包并提取IP和TCP头部:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netinet/if_ether.h>
#include <net/ethernet.h>
#include <pcap.h>

void packet_handler(u_char *args, const struct pcap_pkthdr *header,
                   const u_char *packet) {
    struct ether_header *eth_header;
    struct iphdr *ip_header;
    struct tcphdr *tcp_header;
    struct udphdr *udp_header;

    eth_header = (struct ether_header *)packet;

    if (ntohs(eth_header->ether_type) == ETHERTYPE_IP) {
        ip_header = (struct iphdr*)(packet + sizeof(struct ether_header));

        printf("\nIP Header\n");
        printf("   |-Source IP        : %s\n",
               inet_ntoa(*(struct in_addr *)&ip_header->saddr));
        printf("   |-Destination IP   : %s\n",
               inet_ntoa(*(struct in_addr *)&ip_header->daddr));

        if (ip_header->protocol == IPPROTO_TCP) {
            tcp_header = (struct tcphdr*)(packet +
                         sizeof(struct ether_header) +
                         ip_header->ihl*4);

            printf("\nTCP Header\n");
            printf("   |-Source Port      : %d\n", ntohs(tcp_header->source));
            printf("   |-Destination Port : %d\n", ntohs(tcp_header->dest));
            printf("   |-Sequence Number  : %u\n", ntohl(tcp_header->seq));
            printf("   |-Acknowledge Number: %u\n", ntohl(tcp_header->ack_seq));
        }
    }
}

运行代码:

gcc -o protocol_analyzer protocol_analyzer.c -lpcap
./protocol_analyzer

5. 高级套接字功能

5.1 非阻塞套接字操作

非阻塞套接字允许异步通信,使应用程序在等待网络操作完成时能够执行其他任务。以下代码演示了如何将套接字设置为非阻塞模式:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>

void set_nonblocking(int sockfd) {
    int flags = fcntl(sockfd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL");
        exit(EXIT_FAILURE);
    }

    if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL");
        exit(EXIT_FAILURE);
    }
}

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    set_nonblocking(sockfd);

    // Non-blocking connect
    if (connect(sockfd, ...) == -1) {
        if (errno == EINPROGRESS) {
            // Connection in progress
            fd_set write_fds;
            struct timeval tv;

            FD_ZERO(&write_fds);
            FD_SET(sockfd, &write_fds);
            tv.tv_sec = 5;
            tv.tv_usec = 0;

            if (select(sockfd + 1, NULL, &write_fds, NULL, &tv) > 0) {
                // Connection completed
                int error;
                socklen_t len = sizeof(error);
                getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
                if (error != 0) {
                    // Connection failed
                    close(sockfd);
                    return -1;
                }
            }
        }
    }

    return 0;
}

代码解释

  1. 非阻塞模式: 使用fcntl()函数将套接字设置为非阻塞模式。
  2. 异步连接: connect()函数立即返回,应用程序使用select()等待连接完成。

运行代码:

gcc -o nonblocking_socket nonblocking_socket.c
./nonblocking_socket

6. 安全考虑

使用原始套接字时,安全性至关重要,因为它们提供了对网络的底层访问。以下代码演示了如何实施基本的安全措施:

#include <sys/socket.h>
#include <linux/socket.h>
#include <linux/capability.h>

void implement_security_measures(int sockfd) {
    int yes = 1;

    if (setsockopt(sockfd, IPPROTO_IP, IP_TRANSPARENT, &yes, sizeof(yes)) < 0) {
        perror("setsockopt IP_TRANSPARENT");
    }

    if (setsockopt(sockfd, IPPROTO_TCP, TCP_SYNCNT, &yes, sizeof(yes)) < 0) {
        perror("setsockopt TCP_SYNCNT");
    }

    struct timeval tv;
    tv.tv_sec = 30;
    tv.tv_usec = 0;
    if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
        perror("setsockopt SO_RCVTIMEO");
    }
}
  1. IP欺骗预防: IP_TRANSPARENT选项可以防止IP欺骗。
  2. TCP SYN Cookies: TCP_SYNCNT选项启用TCP SYN Cookies以减轻SYN洪水攻击。
  3. 接收超时: SO_RCVTIMEO选项设置接收数据的超时时间。

运行代码:

gcc -o security_checks security_checks.c
./security_checks

7. 总结

原始套接字编程是网络开发人员的一个强大工具,它能够实现自定义协议、网络监控和报文构造。然而,它需要深入理解网络协议,并仔细处理安全考虑。通过掌握原始套接字,可以构建高级网络应用程序,并深入了解网络通信的内部工作原理。