לדלג לתוכן

6.9 מחסנית הרשת פתרון

תרגיל 1 - מעקב אחרי חבילה בcurl

  1. syscall: לcurl יש socket פתוח (TCP) לשרת. הוא קורא send() (או write()) עם תוכן הHTTP GET request (למשל GET / HTTP/1.1\r\nHost: example.com\r\n\r\n).

  2. שכבת הsocket: הsyscall send מגיע לקוד הsocket בקרנל. הנתונים מועתקים מuser space לsocket buffer בקרנל באמצעות copy_from_user. נוצר sk_buff עם הנתונים.

  3. שכבת TCP: הקרנל מוסיף TCP header באמצעות skb_push. ה-header מכיל:

  4. פורט מקור (הפורט האקראי שנבחר בחיבור, למשל 54321)
  5. פורט יעד (80 - HTTP)
  6. מספר סידורי - Sequence Number (מציין את המיקום של הבתים בזרם)
  7. מספר אישור - ACK Number (מאשר בתים שהתקבלו מהצד השני)
  8. דגלים (PSH, ACK)
  9. גודל חלון - Window Size (כמה מקום יש בבאפר הקבלה)
  10. סיכום ביקורת - Checksum

בנוסף, TCP שומר עותק של הsk_buff כדי שיוכל לשלוח שוב (retransmit) אם לא מגיע ACK.

  1. שכבת IP: הקרנל מוסיף IP header באמצעות skb_push:
  2. כתובת מקור: הIP שלנו (למשל 192.168.1.100)
  3. כתובת יעד: הIP של example.com (למשל 93.184.216.34, מתוך DNS)
  4. TTL: בדרך כלל 64
  5. Protocol: 6 (TCP)

החלטת ניתוב: הקרנל מחפש בטבלת הניתוב (ip route) את הנתיב לכתובת היעד. הוא מוצא את הdefault gateway (למשל 192.168.1.1) ואת ממשק הרשת (למשל eth0).

  1. שכבת Ethernet: הקרנל מוסיף Ethernet header:
  2. כתובת MAC מקור: הMAC של eth0
  3. כתובת MAC יעד: הMAC של הdefault gateway (הראוטר)
  4. הערה: כתובת MAC יעד היא לא של example.com! היא של הhop הבא (הראוטר). הראוטר יחליף את הMAC headers ויעביר הלאה.

מאיפה כתובת הMAC? הקרנל מחפש בטבלת ARP cache. אם הכתובת לא שם, הוא שולח בקשת ARP (broadcast) ומחכה לתשובה. אפשר לראות את הcache עם ip neigh show.

  1. הדרייבר: הsk_buff נכנס לTX queue של ממשק eth0. הדרייבר לוקח אותו מהתור ושם אותו בTX ring buffer של החומרה - מבנה נתונים בזיכרון שהNIC קורא ממנו.

  2. DMA: הNIC משתמש בDMA כדי לקרוא את החבילה מהTX ring buffer בזיכרון ישירות, בלי מעורבות המעבד. אחרי ששלח את החבילה, הNIC יכול לשלוח פסיקה לציין שהTX הושלם (כדי שהקרנל ישחרר את הsk_buff).


תרגיל 2 - בדיקת proc/net/tcp/

1-2. פענוח /proc/net/tcp:

שורה לדוגמה:

sl  local_address  rem_address   st tx_queue rx_queue ... uid
0:  0100007F:0016  00000000:0000 0A 00000000:00000000 ... 0

פענוח הכתובת 0100007F:0016:
- הכתובת בhex little-endian: 0100007F = 7F.00.00.01 = 127.0.0.1
- הפורט בhex: 0016 = 22 בעשרוני
- זה socket שמאזין על 127.0.0.1:22

ערכי state (st):
- 01 = ESTABLISHED - חיבור פעיל
- 02 = SYN_SENT - שלחנו SYN, מחכים לSYN-ACK
- 06 = TIME_WAIT - החיבור נסגר, מחכים שזמן יעבור
- 0A = LISTEN - מאזינים לחיבורים נכנסים

uid: 0 = root. sockets של שירותי מערכת (SSH, web server) בדרך כלל שייכים ל-root.

  1. הקשר בין proc/net/tcp ל-ss:
    הפקודה ss קוראת את המידע מאותם מבני נתונים בקרנל (דרך Netlink socket, שהוא יעיל יותר מקריאת /proc/net/tcp). שני הכלים מציגים את אותו מידע, רק ש-ss מציג אותו בפורמט קריא יותר (כתובות עשרוניות, שמות states, שמות תהליכים).

תרגיל 3 - מצבי socket עם ss

  1. משמעות הדגלים:
  2. t - הצג sockets של TCP
  3. u - הצג sockets של UDP
  4. l - הצג רק sockets במצב LISTEN
  5. n - הצג מספרי פורטים (לא שמות שירותים) - numeric

שירות SSH (פורט 22): הsocket שלו במצב LISTEN.

הבדל 0.0.0.0 מול 127.0.0.1:
- 0.0.0.0:22 - SSH מאזין על כל הממשקים. אפשר להתחבר מכל כתובת IP.
- 127.0.0.1:22 - SSH מאזין רק על loopback. אפשר להתחבר רק מהמכונה עצמה.

מבחינת אבטחה, שירות שמאזין על 127.0.0.1 לא נגיש מבחוץ.

  1. חיבור ESTABLISHED: נראה שורה כמו:

    ESTAB  0  0  192.168.1.100:54321  10.0.0.5:22
    

    כתובת מקור 192.168.1.100 פורט 54321 (אקראי שנבחר על ידי הקרנל), כתובת יעד 10.0.0.5 פורט 22.

  2. ss -s מציג סיכום:

  3. Total TCP sockets: המספר הכולל של sockets (כולל TIME_WAIT ומצבים אחרים)
  4. כמה ב-ESTABLISHED
  5. כמה sockets UDP
  6. המספרים משתנים בין מערכות. שרת עמוס יכול להראות אלפי ESTABLISHED connections.

תרגיל 4 - הבנת sk_buff ומניפולציית headers

  1. מצב התחלתי (אחרי socket layer):
head                              data    tail                    end
 |                                 |       |                       |
 v                                 v       v                       v
 [........headroom (54+ bytes)....][H][i]...[.....tailroom..........]

הheadroom חייב להיות לפחות 54 בתים: 14 (Ethernet) + 20 (IP) + 20 (TCP).

  1. אחרי TCP (skb_push 20 בתים):
head                  data                  tail                   end
 |                     |                     |                      |
 v                     v                     v                      v
 [...headroom (34)...][TCP header (20B)][H][i]...[...tailroom......]

הdata הוזז 20 בתים שמאלה. עכשיו הdata מתחיל בTCP header ונמשך עד tail.

  1. אחרי IP (skb_push 20 בתים):
head       data                                    tail            end
 |          |                                       |               |
 v          v                                       v               v
 [room(14)][IP header (20B)][TCP header (20B)][H][i][..tailroom....]
  1. אחרי Ethernet (skb_push 14 בתים):
head/data                                                tail      end
 |                                                        |         |
 v                                                        v         v
 [Eth (14B)][IP header (20B)][TCP header (20B)][H][i].....[tailroom]

עכשיו data = head (אין יותר headroom). החבילה מוכנה לשליחה.

  1. למה headroom חייב להיות מספיק גדול:
    אם אין מספיק headroom ל-skb_push, הקרנל צריך להקצות sk_buff חדש ויותר גדול, להעתיק אליו את כל הנתונים, ולשחרר את הישן. זו פעולה יקרה! לכן הקרנל מקצה מראש headroom בגודל מספיק לכל הheaders (הפונקציה alloc_skb מקבלת פרמטר headroom).

אם בכל זאת אין מספיק, הפונקציה skb_cow_head() מטפלת בזה - אבל זה overhead שרוצים להימנע ממנו.


תרגיל 5 - מרחבי שמות של רשת

  1. מה יש בnamespace חדש:
  2. ip addr show - רואים רק ממשק lo (loopback), והוא במצב DOWN
  3. ip route show - ריק לגמרי, אין טבלת ניתוב
  4. ping 8.8.8.8 - נכשל עם "Network is unreachable" כי:

    • אין ממשק רשת פעיל (lo הוא DOWN)
    • אין טבלת ניתוב
    • הnamespace מבודד לחלוטין מהstack של המארח
  5. איך Docker containers מתקשרים עם האינטרנט:

Docker משתמש בשלושה מנגנונים:

veth pair - זוג ממשקים וירטואליים שמחוברים זה לזה. כמו צינור - מה שנכנס בצד אחד יוצא בצד השני. קצה אחד נמצא בnamespace של הcontainer, והקצה השני בnamespace של המארח.

bridge - Docker יוצר bridge וירטואלי (בדרך כלל docker0) בnamespace של המארח. כל צדדי הveth של הcontainers מחוברים לbridge הזה. הbridge עובד כמו switch - מעביר חבילות בין הcontainers ובין המארח.

NAT - הקרנל של המארח משתמש ב-iptables/nftables (Netfilter מההרצאה) כדי לעשות NAT (Network Address Translation) לחבילות שיוצאות מcontainers. חוק MASQUERADE ב-POSTROUTING משנה את כתובת המקור של חבילות מcontainers לכתובת IP של המארח. ככה העולם החיצון רואה חבילות מהמארח, לא מהcontainer.

הזרימה: container -> veth -> bridge (docker0) -> iptables NAT -> eth0 -> אינטרנט

  1. (ניקוי - אין תשובה נדרשת)

  2. סוגי namespaces נוספים בלינוקס:

  3. PID namespace - בידוד מזהי תהליכים. תהליך 1 בcontainer הוא לא תהליך 1 במארח.
  4. Mount namespace - בידוד מערכת הקבצים. כל container רואה עץ קבצים שונה.
  5. UTS namespace - בידוד hostname. לכל container יכול להיות hostname שונה.
  6. IPC namespace - בידוד מנגנוני IPC (shared memory, semaphores, message queues).
  7. User namespace - בידוד UIDs. root בcontainer הוא לא root במארח.
  8. Cgroup namespace - בידוד cgroups (הגבלת משאבים).

כל הnamespaces האלה ביחד הם הבסיס לcontainers (Docker, LXC, Podman ועוד).