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):
פלט לדוגמה:
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
מה קורה בקוד¶
- יוצרים raw socket עם
IPPROTO_ICMP- הקרנל יודע שאנחנו רוצים ICMP - בונים חבילת ICMP Echo Request ידנית: ממלאים את ה-header, מחשבים checksum
- שולחים עם
sendto- הקרנל מוסיף IP header אוטומטית (כי לא ביקשנוIP_HDRINCL) - מקבלים תשובה עם
recvfrom- מקבלים את כל החבילה כולל IP header - מפרסרים: קודם IP header (כדי לדעת את אורכו), ואז ICMP header
- מחשבים 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;
}
קומפילציה והרצה:
מה קורה בקוד¶
socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))- סוקט שמקבל כל חבילה שמגיעה לממשק הרשת, כולל ה-Ethernet headerrecvfromמחזיר חבילה שלמה, מ-Ethernet ומעלה- מפרסרים שכבה אחרי שכבה:
- Ethernet (14 בתים) - כתובות MAC ומזהה הפרוטוקול הבא
- IP (20+ בתים) - כתובות IP, TTL, פרוטוקול שכבת התחבורה
- 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