לדלג לתוכן

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)
{
    /* יצירת סוקט UDP */
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(1);
    }

    /* bind - קשירה לכתובת */
    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);

    /* לולאה ראשית - קבלה ושליחה */
    char buffer[BUFFER_SIZE];
    while (1) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);

        /* קבלת הודעה - recvfrom גם ממלא את כתובת השולח */
        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);

        /* שליחת תשובה - sendto שולח לכתובת ספציפית */
        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]);

    /* יצירת סוקט UDP */
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(1);
    }

    /* הגדרת כתובת השרת */
    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);

    /* שליחה וקבלה */
    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;

        /* שליחה */
        sendto(sockfd, input, strlen(input), 0,
               (struct sockaddr *)&server_addr, sizeof(server_addr));

        /* קבלת תשובה */
        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, מה שנרצה. זה מאפשר:

  • בניית חבילות מותאמות אישית
  • שליחת פרוטוקולים שאין להם ממשק 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;       /* סוג הודעה */
    uint8_t  code;       /* קוד משנה */
    uint16_t checksum;   /* סכום ביקורת */
    uint16_t id;         /* מזהה (לecho) */
    uint16_t sequence;   /* מספר סידורי (ל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;

    /* סכימה של כל 16-ביט */
    while (len > 1) {
        sum += *buf++;
        len -= 2;
    }

    /* אם יש בית בודד בסוף */
    if (len == 1)
        sum += *(uint8_t *)buf;

    /* קיפול: מוסיפים את 16 הביטים העליונים ל-16 התחתונים */
    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];

    /* יצירת raw socket ל-ICMP */
    int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sockfd == -1) {
        perror("socket (need root/CAP_NET_RAW)");
        exit(1);
    }

    /* הגדרת timeout לקבלה */
    struct timeval tv;
    tv.tv_sec = TIMEOUT_SEC;
    tv.tv_usec = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    /* כתובת יעד */
    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++) {
        /* בניית חבילת ICMP Echo Request */
        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);

        /* שליחה */
        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;
        }

        /* קבלת תשובה */
        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);

        /* פענוח התשובה - IP header ואז 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);  /* שנייה בין 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) - הזמן בין שליחה לקבלה

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

סוקטים גולמיים מאפשרים לנו להאזין לכל התעבורה שעוברת בממשק הרשת. זה בדיוק מה ש-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 = כל החבילות, כולל 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);

        /* שכבה 2 - Ethernet */
        struct ethhdr *eth = (struct ethhdr *)buffer;
        print_ethernet(eth);

        /* בדיקה שזה IP */
        if (ntohs(eth->h_proto) != ETH_P_IP)
            continue;

        /* שכבה 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;

        /* שכבה 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)) - סוקט שמקבל כל חבילה שמגיעה לממשק הרשת, כולל ה-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