פרוטוקולי רשת - שאילתת DNS#
בסוף הפרק הזה תוכלו לבנות ולפענח כותרת שאילתת DNS וקטע שאלה באמצעות pack ו־unpack - ולנקוט באותה גישה לכל פרוטוקול מוגדר־RFC.
״פרוטוקול רשת״ הוא, בסופו של דבר, רצף של שדות מדויקים־בבתים המתוארים בפרוזה אנגלית. אם אתם יכולים לקרוא את טבלת השדות, אתם יכולים לכתוב את התבנית. הפרק הזה עובר על דוגמה קונקרטית אחת - packet שאילתת ה־DNS - מדיאגרמת החוט של ה־RFC לזוג תת־שגרות encode / decode מתפקדות לחלוטין.
הבעיה#
אנו רוצים לבקש משרת DNS את רשומת ה־A של example.com. ה־payload של UDP שאנו שולחים הוא DNS message (RFC 1035), המורכב מ:
כותרת של 12 בתים.
קטע שאלה - הדומיין שאנו שואלים עליו, וסוג הרשומה שאנו רוצים.
(לקוח אמיתי גם מפענח את קטע התשובה בתגובה. אנו עוצרים בקידוד ובהצגת סקיצה של ה־decode.)
הכותרת#
מתוך RFC 1035 קטע 4.1.1, הכותרת היא שישה שדות 16-ביט big-endian רצופים:
offset | שדה | משמעות |
|---|---|---|
0 | ID | מזהה 16-ביט שרירותי שאנו בוחרים |
2 | דגלים | Opcode, ביט RD, דגלי תגובה, rcode |
4 | QDCOUNT | מספר הרשומות בקטע השאלה |
6 | ANCOUNT | מספר רשומות המשאבים בתשובה |
8 | NSCOUNT | רשומות סמכות |
10 | ARCOUNT | רשומות נוספות |
שישה shorts big-endian ללא־סימן פירושו שש הוראות n:
sub dns_header {
my (%opts) = @_;
pack "n6",
$opts{id} // 0,
$opts{flags} // 0,
$opts{qd} // 0,
$opts{an} // 0,
$opts{ns} // 0,
$opts{ar} // 0;
}
אלה 12 בתים, בדיוק כפי שהמפרט דורש. n6 הוא קיצור עבור n n n n n n.
קטע השאלה#
שאלה היא:
שם, המקודד כרצף של תוויות עם קידומת אורך המסתיים בתווית באורך אפס.
QTYPE של 16-ביט (1 = רשומת A).
QCLASS של 16-ביט (1 = IN, אינטרנט).
עבור example.com השם מקודד כ:
\x07 e x a m p l e \x03 c o m \x00
כל תווית מתחילה בבית אורך; השם השלם מסתיים בתווית באורך אפס. שני דברים שכדאי לשים לב אליהם: האורך הוא בית אחד (C), לא שניים; ויש NUL גורר אך הוא אינו בדיוק מה ש־Z מייצר - הוא מסיים את רשימת התוויות, לא מחרוזת בודדת.
אנו יכולים לבנות זאת ידנית משם דומיין:
sub encode_name {
my ($name) = @_;
my $out = "";
for my $label (split /\./, $name) {
die "label too long" if length($label) > 63;
$out .= pack "C/a*", $label;
}
$out .= "\0"; # zero-length terminator
return $out;
}
שתי הוראות בגוף הלולאה: C/a* - אורך של בית בודד ולאחריו בתי התווית, המחושבים אוטומטית (ראו את פרק הקיבוץ והמונים עבור צורת ה־/). אחרי הלולאה, "\0" מילולי מסיים את הרשימה.
עם encode_name במקומה, קטע השאלה השלם הוא שתי שרשורים ו־pack אחד:
sub dns_question {
my ($name, $qtype, $qclass) = @_;
return encode_name($name) . pack "n n", $qtype, $qclass;
}
הרכבת ה־packet#
use constant {
QR_QUERY => 0,
OPCODE_QUERY=> 0,
RD => 1 << 8, # recursion desired
TYPE_A => 1,
CLASS_IN => 1,
};
sub dns_query_for_A {
my ($name) = @_;
my $id = int rand 65536;
my $flags = RD; # standard query, recursion desired
my $pkt = dns_header(
id => $id,
flags => $flags,
qd => 1, # one question
);
$pkt .= dns_question($name, TYPE_A, CLASS_IN);
return ($id, $pkt);
}
my ($id, $pkt) = dns_query_for_A("example.com");
# send $pkt over a UDP socket to port 53
29 בתים בסך הכל: 12 כותרת + 13 לשם (\x07example\x03com\x00 הוא 13) + 2 + 2 ל־QTYPE ול־QCLASS. הריצו length $pkt כדי לאשר.
פענוח כותרת התגובה#
לתגובה יש אותה צורת כותרת - רק ביטי הדגלים משתנים. ה־decode הוא התבנית ההפוכה:
sub parse_dns_header {
my ($buf) = @_;
my ($id, $flags, $qd, $an, $ns, $ar) = unpack "n6", $buf;
my %hdr = (
id => $id,
qr => ($flags >> 15) & 1,
op => ($flags >> 11) & 0x0f,
aa => ($flags >> 10) & 1,
tc => ($flags >> 9) & 1,
rd => ($flags >> 8) & 1,
ra => ($flags >> 7) & 1,
rcode => $flags & 0x0f,
qd => $qd,
an => $an,
ns => $ns,
ar => $ar,
);
return \%hdr;
}
n6 מושך את ששת ה־shorts בבת אחת; ביטי הדגלים הבודדים יוצאים מהיסטים ומסכות על $flags. תבנית זו אוניברסלית: השתמשו ב־pack / unpack עבור פריסה ברמת הבתים, Perl רגיל לפענוח ברמת הביטים של שדות הדגלים.
פענוח שם#
שמות בקטע התשובה משתמשים באותו קידוד קידומת־אורך, בתוספת מנגנון דחיסת מצביעים שנדלג עליו. עבור שם רענן (ללא דחיסה) המפענח משקף את המקודד:
sub decode_name {
my ($buf, $offset) = @_;
my @labels;
while (1) {
my $len = unpack "x$offset C", $buf;
last if $len == 0;
die "compression pointer" if $len >= 0xc0;
my $label = unpack "x${\ ($offset + 1)} a$len", $buf;
push @labels, $label;
$offset += 1 + $len;
}
return (join(".", @labels), $offset + 1);
}
שלוש קריאות unpack, כל אחת עם קידומת x$offset כדי לדלג לבית הנכון. השם מתחלף בין אורכי בית בודד לבין תוויות ברוחב משתנה, והלולאה ממשיכה לקרוא עד שהיא פוגעת בתווית באורך אפס. הטריק x$offset a$len - להרכיב תבנית מערכי Perl, לקרוא ל־unpack - הוא התבנית הסטנדרטית כאשר רוחב השדה הבא תלוי בערך השדה הקודם.
מה לקחת הלאה#
טבלת פריסת בתים של RFC ממופה ישירות למחרוזת תבנית. shorts big-endian →
n, longs big-endian →N, בתים →C.שדות עם קידומת אורך משתמשים ב־
C/a*/n/a*/ … - ביטוי אחד, ללא ספירת off-by-one ידנית.שדות ברמת ביטים בתוך בית מפוענחים ב־Perl רגיל לאחר ש־
unpackמסר לכם את הבית או ה־short המכילים אותם.נתונים ברוחב משתנה דורשים unpack שלבי. קראו אורך, ואז בנו את התבנית עבור הנתונים מאותו אורך, ואז קראו שוב ל־
unpack.
הפרק הבא מיישם את אותה גישה על פורמט קובץ אמיתי - כותרת תמונת GIF - ומציג את האידיום ״בדיקת מספר קסם״.