לדלג לתוכן

UDP ו-raw sockets

הקדמה

בפרקים 11.1 ו-11.2 עבדנו עם TCP - פרוטוקול אמין ומסודר. אבל TCP לא תמיד מתאים. לפעמים צריך משהו מהיר יותר (UDP), ולפעמים צריך שליטה מלאה על חבילות הרשת (raw sockets).

בהרצאה הזו נלמד:
- תכנות UDP - פשוט יותר מTCP, מהיר יותר, אבל לא אמין
- סוקטים גולמיים (raw sockets) - בניית חבילות רשת מאפס, כולל headers
- כתיבת כלי ping בC
- האזנה לכל התעבורה ברשת (packet sniffing)

יסודות UDP

UDP (User Datagram Protocol) הוא פרוטוקול פשוט יותר מTCP. ההבדל העיקרי: אין חיבור. אין connect, אין accept, אין לחיצת יד (handshake). פשוט שולחים חבילה (datagram) לכתובת יעד, ומקווים שהיא תגיע.

ההבדלים מTCP:

TCP UDP
אמין - מובטחת הגעה לא אמין - חבילות יכולות ללכת לאיבוד
מסודר - הסדר נשמר לא מסודר - חבילות יכולות להגיע בכל סדר
חיבור - צריך connect/accept ללא חיבור - שולחים ישירות
זרם בתים - אין גבולות הודעות דאטאגרמים - כל שליחה היא הודעה נפרדת
איטי יותר (overhead של אמינות) מהיר יותר (מינימום overhead)

מתי להשתמש בUDP?

  • DNS - שאילתת שם דומיין. שאילתה אחת, תשובה אחת. אם נאבד - שולחים שוב.
  • משחקי רשת - מיקום שחקנים משתנה כל הזמן. חבילה שאבדה לא רלוונטית - כבר יש חבילה חדשה.
  • סטרימינג של וידאו/אודיו - עדיף לדלג על פריים מאשר לחכות לו. עיכוב גרוע יותר מאובדן.
  • IoT - מכשירים פשוטים ששולחים נתוני חיישן. אם מדידה אבדה, הבאה בדרך.

לקוח ושרת UDP

שרת UDP

שרת UDP פשוט יותר מTCP - אין listen ואין accept:

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

#define PORT 9999
#define BUFFER_SIZE 1024

int main(void)
{
    /* create UDP socket */
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(1);
    }

    /* bind - bind to an address */
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        close(sockfd);
        exit(1);
    }
    printf("UDP server listening on port %d...\n", PORT);

    /* main loop - receive and send */
    char buffer[BUFFER_SIZE];
    while (1) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);

        /* receive message - recvfrom also fills in the sender's address */
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                             (struct sockaddr *)&client_addr, &client_len);
        if (n == -1) {
            perror("recvfrom");
            continue;
        }
        buffer[n] = '\0';

        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
        printf("received from %s:%d: %s\n", client_ip,
               ntohs(client_addr.sin_port), buffer);

        /* send response - sendto sends to a specific address */
        sendto(sockfd, buffer, n, 0,
               (struct sockaddr *)&client_addr, client_len);
    }

    close(sockfd);
    return 0;
}

שימו לב להבדלים מTCP:
- SOCK_DGRAM במקום SOCK_STREAM
- אין listen ואין accept
- משתמשים ב-recvfrom במקום read - כי צריך לדעת מאיפה ההודעה הגיעה
- משתמשים ב-sendto במקום write - כי צריך לציין לאן לשלוח

יתרון גדול: השרת הזה מטפל בכל הלקוחות בthread אחד, בלי שום multiplexing. כל הודעה היא עצמאית - אין מצב חיבור לנהל.

לקוח UDP

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

#define BUFFER_SIZE 1024

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "usage: %s <ip> <port>\n", argv[0]);
        exit(1);
    }

    const char *ip = argv[1];
    int port = atoi(argv[2]);

    /* create UDP socket */
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(1);
    }

    /* set up server address */
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    inet_pton(AF_INET, ip, &server_addr.sin_addr);

    /* send and receive */
    char input[BUFFER_SIZE];
    char buffer[BUFFER_SIZE];

    printf("UDP client. type messages to send:\n");
    while (fgets(input, sizeof(input), stdin)) {
        input[strcspn(input, "\n")] = '\0';

        if (strcmp(input, "quit") == 0)
            break;

        /* send */
        sendto(sockfd, input, strlen(input), 0,
               (struct sockaddr *)&server_addr, sizeof(server_addr));

        /* receive response */
        struct sockaddr_in from_addr;
        socklen_t from_len = sizeof(from_addr);
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                             (struct sockaddr *)&from_addr, &from_len);
        if (n > 0) {
            buffer[n] = '\0';
            printf("server replied: %s\n", buffer);
        }
    }

    close(sockfd);
    return 0;
}

שימו לב: הלקוח לא צריך bind ולא צריך connect. פשוט יוצר סוקט ושולח. מערכת ההפעלה מקצה לו פורט אוטומטית בשליחה הראשונה.

סוקטים גולמיים - raw sockets

סוקטים רגילים (TCP/UDP) מאפשרים לשלוח ולקבל נתונים, אבל הקרנל בונה את הheaders עבורנו - IP header, TCP/UDP header. אנחנו שולטים רק בתוכן (payload).

סוקטים גולמיים (SOCK_RAW) נותנים לנו שליטה מלאה. אנחנו בונים את כל הheaders בעצמנו - כולל IP, TCP, UDP, ICMP, מה שנרצה. זה מאפשר:

  • בניית חבילות מותאמות אישית
  • שליחת פרוטוקולים שאין להם interface socket רגיל (כמו ICMP)
  • סריקת רשת (כלים כמו nmap)
  • sniffing - האזנה לתעבורת רשת

אזהרה: סוקטים גולמיים דורשים הרשאות root (או CAP_NET_RAW). זה הגיוני - בניית חבילות מותאמות יכולה להיות כלי תקיפה.

בניית ping - ICMP echo

ping הוא הכלי הבסיסי ביותר לבדיקת קישוריות רשת. הוא שולח חבילת ICMP Echo Request ומחכה ל-ICMP Echo Reply. בואו נבנה אותו מאפס.

פרוטוקול ICMP

ICMP (Internet Control Message Protocol) הוא פרוטוקול שיושב מעל IP. הוא משמש להודעות בקרה ושגיאה. מבנה ה-header:

struct icmp_header {
    uint8_t  type;       /* message type */
    uint8_t  code;       /* sub-code */
    uint16_t checksum;   /* checksum */
    uint16_t id;         /* identifier (for echo) */
    uint16_t sequence;   /* sequence number (for echo) */
};

ל-ping אנחנו צריכים:
- Echo Request: type=8, code=0
- Echo Reply: type=0, code=0

חישוב checksum

ICMP דורש checksum - סכום ביקורת שמוודא שהחבילה לא נפגמה בדרך. האלגוריתם:

uint16_t checksum(void *data, int len)
{
    uint16_t *buf = data;
    uint32_t sum = 0;

    /* sum every 16 bits */
    while (len > 1) {
        sum += *buf++;
        len -= 2;
    }

    /* if there's a leftover byte at the end */
    if (len == 1)
        sum += *(uint8_t *)buf;

    /* fold: add the upper 16 bits to the lower 16 bits */
    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);

    return (uint16_t)~sum;
}

מימוש ping

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <errno.h>

#define PACKET_SIZE 64
#define TIMEOUT_SEC 1

uint16_t calc_checksum(void *data, int len)
{
    uint16_t *buf = data;
    uint32_t sum = 0;

    while (len > 1) {
        sum += *buf++;
        len -= 2;
    }
    if (len == 1)
        sum += *(uint8_t *)buf;

    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    return (uint16_t)~sum;
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "usage: %s <ip>\n", argv[0]);
        exit(1);
    }

    const char *dest_ip = argv[1];

    /* create a raw socket for ICMP */
    int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sockfd == -1) {
        perror("socket (need root/CAP_NET_RAW)");
        exit(1);
    }

    /* set a receive timeout */
    struct timeval tv;
    tv.tv_sec = TIMEOUT_SEC;
    tv.tv_usec = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    /* destination address */
    struct sockaddr_in dest_addr;
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.sin_family = AF_INET;
    inet_pton(AF_INET, dest_ip, &dest_addr.sin_addr);

    printf("PING %s\n", dest_ip);

    int seq = 0;
    pid_t pid = getpid();

    for (int i = 0; i < 4; i++) {
        /* build the ICMP Echo Request packet */
        char packet[PACKET_SIZE];
        memset(packet, 0, sizeof(packet));

        struct icmphdr *icmp = (struct icmphdr *)packet;
        icmp->type = ICMP_ECHO;        /* type 8 = echo request */
        icmp->code = 0;
        icmp->un.echo.id = htons(pid & 0xFFFF);
        icmp->un.echo.sequence = htons(seq++);
        icmp->checksum = 0;
        icmp->checksum = calc_checksum(packet, PACKET_SIZE);

        /* send */
        struct timespec send_time, recv_time;
        clock_gettime(CLOCK_MONOTONIC, &send_time);

        ssize_t sent = sendto(sockfd, packet, PACKET_SIZE, 0,
                              (struct sockaddr *)&dest_addr, sizeof(dest_addr));
        if (sent == -1) {
            perror("sendto");
            continue;
        }

        /* receive response */
        char recv_buf[1024];
        struct sockaddr_in from_addr;
        socklen_t from_len = sizeof(from_addr);

        ssize_t received = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0,
                                    (struct sockaddr *)&from_addr, &from_len);
        if (received == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                printf("request timeout\n");
            else
                perror("recvfrom");
            continue;
        }

        clock_gettime(CLOCK_MONOTONIC, &recv_time);

        /* parse the response - IP header then ICMP */
        struct iphdr *ip_hdr = (struct iphdr *)recv_buf;
        int ip_hdr_len = ip_hdr->ihl * 4;
        struct icmphdr *reply = (struct icmphdr *)(recv_buf + ip_hdr_len);

        if (reply->type == ICMP_ECHOREPLY) {
            double rtt = (recv_time.tv_sec - send_time.tv_sec) * 1000.0 +
                         (recv_time.tv_nsec - send_time.tv_nsec) / 1e6;

            char from_ip[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &from_addr.sin_addr, from_ip, sizeof(from_ip));

            printf("%ld bytes from %s: icmp_seq=%d ttl=%d time=%.1f ms\n",
                   received - ip_hdr_len, from_ip,
                   ntohs(reply->un.echo.sequence),
                   ip_hdr->ttl, rtt);
        }

        sleep(1);  /* one second between pings */
    }

    close(sockfd);
    return 0;
}

קומפילציה והרצה (צריך root):

gcc -o myping myping.c
sudo ./myping 8.8.8.8

פלט לדוגמה:

PING 8.8.8.8
64 bytes from 8.8.8.8: icmp_seq=0 ttl=117 time=12.3 ms
64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=11.8 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=117 time=12.1 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=117 time=11.9 ms

מה קורה בקוד

  1. יוצרים raw socket עם IPPROTO_ICMP - הקרנל יודע שאנחנו רוצים ICMP
  2. בונים חבילת ICMP Echo Request ידנית: ממלאים את ה-header, מחשבים checksum
  3. שולחים עם sendto - הקרנל מוסיף IP header אוטומטית (כי לא ביקשנו IP_HDRINCL)
  4. מקבלים תשובה עם recvfrom - מקבלים את כל החבילה כולל IP header
  5. מפרסרים: קודם IP header (כדי לדעת את אורכו), ואז ICMP header
  6. מחשבים RTT (Round Trip Time) - הזמן בין שליחה לקבלה

איור מונפש של מחזור בקשת ping, שבו נשלחת חבילת ICMP Echo Request עם מספר סידורי וחוזרת תשובת Echo Reply שממנה מחושב זמן הלוך ושוב

האזנה לתעבורה - packet sniffing

סוקטים גולמיים מאפשרים לנו להאזין לכל התעבורה שעוברת בinterface הרשת. זה בדיוק מה ש-tcpdump ו-Wireshark עושים ברמה הבסיסית.

בלינוקס, כדי לקבל את כל החבילות (כולל Ethernet header) משתמשים ב-AF_PACKET:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <linux/if_packet.h>

#define BUFFER_SIZE 65536

void print_ethernet(struct ethhdr *eth)
{
    printf("ethernet: %02x:%02x:%02x:%02x:%02x:%02x -> %02x:%02x:%02x:%02x:%02x:%02x",
           eth->h_source[0], eth->h_source[1], eth->h_source[2],
           eth->h_source[3], eth->h_source[4], eth->h_source[5],
           eth->h_dest[0], eth->h_dest[1], eth->h_dest[2],
           eth->h_dest[3], eth->h_dest[4], eth->h_dest[5]);
    printf(" (type: 0x%04x)\n", ntohs(eth->h_proto));
}

void print_ip(struct iphdr *ip)
{
    struct in_addr src, dst;
    src.s_addr = ip->saddr;
    dst.s_addr = ip->daddr;
    printf("  IP: %s -> %s", inet_ntoa(src), inet_ntoa(dst));
    printf(" (proto: %d, ttl: %d, len: %d)\n",
           ip->protocol, ip->ttl, ntohs(ip->tot_len));
}

void print_tcp(struct tcphdr *tcp)
{
    printf("  TCP: port %d -> %d", ntohs(tcp->source), ntohs(tcp->dest));
    printf(" [%s%s%s%s]\n",
           tcp->syn ? "SYN " : "",
           tcp->ack ? "ACK " : "",
           tcp->fin ? "FIN " : "",
           tcp->rst ? "RST " : "");
}

void print_udp(struct udphdr *udp)
{
    printf("  UDP: port %d -> %d (len: %d)\n",
           ntohs(udp->source), ntohs(udp->dest), ntohs(udp->len));
}

int main(void)
{
    /* AF_PACKET + SOCK_RAW + ETH_P_ALL = all packets, including ethernet */
    int sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sockfd == -1) {
        perror("socket (need root)");
        exit(1);
    }

    printf("sniffing packets... (press Ctrl+C to stop)\n\n");

    char buffer[BUFFER_SIZE];
    int count = 0;

    while (1) {
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL);
        if (n == -1) {
            perror("recvfrom");
            continue;
        }

        count++;
        printf("--- packet #%d (%zd bytes) ---\n", count, n);

        /* layer 2 - Ethernet */
        struct ethhdr *eth = (struct ethhdr *)buffer;
        print_ethernet(eth);

        /* check that it's IP */
        if (ntohs(eth->h_proto) != ETH_P_IP)
            continue;

        /* layer 3 - IP */
        struct iphdr *ip = (struct iphdr *)(buffer + sizeof(struct ethhdr));
        print_ip(ip);

        int ip_hdr_len = ip->ihl * 4;
        void *transport = buffer + sizeof(struct ethhdr) + ip_hdr_len;

        /* layer 4 - TCP/UDP */
        if (ip->protocol == IPPROTO_TCP) {
            print_tcp((struct tcphdr *)transport);
        } else if (ip->protocol == IPPROTO_UDP) {
            print_udp((struct udphdr *)transport);
        }

        printf("\n");
    }

    close(sockfd);
    return 0;
}

קומפילציה והרצה:

gcc -o sniffer sniffer.c
sudo ./sniffer

מה קורה בקוד

  1. socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) - סוקט שמקבל כל חבילה שמגיעה לinterface הרשת, כולל ה-Ethernet header
  2. recvfrom מחזיר חבילה שלמה, מ-Ethernet ומעלה
  3. מפרסרים שכבה אחרי שכבה:
  4. Ethernet (14 בתים) - כתובות MAC ומזהה הפרוטוקול הבא
  5. IP (20+ בתים) - כתובות IP, TTL, פרוטוקול שכבת התחבורה
  6. TCP/UDP - פורטים ודגלים

איור מונפש של פירוק חבילה מסוקט גולמי לשכבות, שבו מחשבים את כתובת כל שכבה לפי אורך השכבה שלפניה

זה בדיוק מה שראינו בפרק 6.9 כשלמדנו על מחסנית הרשת של הקרנל - רק שעכשיו אנחנו רואים את זה מצד התוכנה.

הערת אבטחה

סוקטים גולמיים הם כלי רב-עוצמה לאנשי אבטחת מידע ורשתות. אפשר להשתמש בהם לסריקת רשת, בניית כלי אבחון, ובדיקת חדירות.

אבל - השתמשו בהם רק על רשתות שאתם מורשים לבדוק. האזנה לתעבורת רשת ושליחת חבילות מזויפות ללא הרשאה היא עבירה פלילית ברוב המדינות.

סיכום

  • UDP הוא פרוטוקול ללא חיבור - פשוט, מהיר, אבל לא אמין
  • שרת UDP: socket -> bind -> recvfrom/sendto בלולאה. אין listen/accept
  • לקוח UDP: socket -> sendto/recvfrom. אין connect
  • סוקטים גולמיים (SOCK_RAW) נותנים שליטה מלאה על חבילות הרשת
  • ping עובד עם ICMP Echo Request/Reply דרך raw socket
  • האזנה לתעבורה עם AF_PACKET + SOCK_RAW + ETH_P_ALL
  • סוקטים גולמיים דורשים root או CAP_NET_RAW