Πρωτόκολλα δικτύου - ένα ερώτημα DNS#
Στο τέλος αυτού του κεφαλαίου θα μπορείτε να κατασκευάζετε και να αναλύετε ένα header ερωτήματος DNS και ένα question section χρησιμοποιώντας τις pack και unpack - και να ακολουθείτε την ίδια προσέγγιση σε οποιοδήποτε πρωτόκολλο που ορίζεται από RFC.
Ένα «πρωτόκολλο δικτύου» είναι, εν τέλει, μια ακολουθία πεδίων ακριβών σε byte που περιγράφονται σε αγγλική πρόζα. Αν μπορείτε να διαβάσετε τον πίνακα πεδίων, μπορείτε να γράψετε το πρότυπο. Αυτό το κεφάλαιο διατρέχει ένα συγκεκριμένο παράδειγμα - το πακέτο ερωτήματος DNS - από το διάγραμμα επί της γραμμής του RFC σε ένα πλήρως λειτουργικό ζεύγος υπορουτινών κωδικοποίησης / αποκωδικοποίησης.
Το πρόβλημα#
Θέλουμε να ρωτήσουμε έναν διακομιστή DNS για την εγγραφή A του example.com. Το φορτίο UDP που στέλνουμε είναι ένα μήνυμα DNS (RFC 1035), που αποτελείται από:
Ένα header 12 byte.
Ένα question section - τον τομέα για τον οποίο ρωτάμε, και τον τύπο εγγραφής που θέλουμε.
(Ένας πραγματικός πελάτης αναλύει επίσης το answer section στην απάντηση. Σταματάμε στην κωδικοποίηση και στην παρουσίαση ενός σκαριφήματος της αποκωδικοποίησης.)
Το header#
Από την ενότητα 4.1.1 του RFC 1035, το header είναι έξι διαδοχικά πεδία 16-bit big-endian:
Offset | Πεδίο | Σημασία |
|---|---|---|
0 | ID | Αυθαίρετο 16-bit αναγνωριστικό που επιλέγουμε |
2 | Σημαίες | Opcode, bit RD, σημαίες απάντησης, rcode |
4 | QDCOUNT | Πλήθος εγγραφών στο question section |
6 | ANCOUNT | Πλήθος εγγραφών πόρων στην απάντηση |
8 | NSCOUNT | Εγγραφές authority |
10 | ARCOUNT | Πρόσθετες εγγραφές |
Έξι μη προσημασμένα big-endian shorts σημαίνει έξι οδηγίες 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 bytes, ακριβώς όπως απαιτούν οι προδιαγραφές. Το n6 είναι συντομογραφία του n n n n n n.
Το question section#
Ένα question είναι:
Ένα όνομα, κωδικοποιημένο ως ακολουθία ετικετών με πρόθεμα μήκους που τερματίζεται από μια ετικέτα μηδενικού μήκους.
Ένα 16-bit QTYPE (1 = εγγραφή A).
Ένα 16-bit QCLASS (1 = IN, Internet).
Για το example.com το όνομα κωδικοποιείται ως:
\x07 e x a m p l e \x03 c o m \x00
Κάθε ετικέτα ξεκινάει με ένα byte μήκους· ολόκληρο το όνομα τελειώνει με μια ετικέτα μηδενικού μήκους. Δύο πράγματα να προσέξετε: το μήκος είναι ένα byte (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* - ένα μήκος ενός byte ακολουθούμενο από τα bytes της ετικέτας, υπολογισμένο αυτόματα (δείτε το κεφάλαιο grouping-and-counts για τη μορφή /). Μετά τον βρόχο, ένα κυριολεκτικό "\0" τερματίζει τη λίστα.
Με την encode_name στη θέση της, ολόκληρο το question section είναι δύο συνενώσεις και μία pack:
sub dns_question {
my ($name, $qtype, $qclass) = @_;
return encode_name($name) . pack "n n", $qtype, $qclass;
}
Συναρμολόγηση του πακέτου#
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 bytes συνολικά: 12 header + 13 για το όνομα (το \x07example\x03com\x00 είναι 13) + 2 + 2 για τα QTYPE και QCLASS. Τρέξτε length $pkt για επιβεβαίωση.
Ανάλυση του header απάντησης#
Η απάντηση έχει το ίδιο σχήμα header - μόνο τα bits σημαιών αλλάζουν. Η αποκωδικοποίηση είναι το αντίστροφο πρότυπο:
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 με μία κίνηση· τα επιμέρους bits σημαιών προκύπτουν από shifts και masks πάνω στο $flags. Αυτό το μοτίβο είναι καθολικό: χρησιμοποιήστε pack / unpack για τη διάταξη σε επίπεδο byte, σκέτη Perl για αποκωδικοποίηση σε επίπεδο bit των πεδίων σημαιών.
Ανάλυση ενός ονόματος#
Τα ονόματα στο answer section χρησιμοποιούν την ίδια κωδικοποίηση προθέματος μήκους, συν έναν μηχανισμό συμπίεσης δεικτών που θα παραλείψουμε. Για ένα φρέσκο όνομα (χωρίς συμπίεση) ο αποκωδικοποιητής κατοπτρίζει τον κωδικοποιητή:
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 για να παρακαμφθεί μέχρι το σωστό byte. Το όνομα εναλλάσσεται μεταξύ μηκών ενός byte και ετικετών μεταβλητού πλάτους, και ο βρόχος συνεχίζει να διαβάζει μέχρι να συναντήσει μια ετικέτα μηδενικού μήκους. Το τέχνασμα x$offset a$len - σύνθεση προτύπου από τιμές της Perl, κλήση της unpack - είναι το πρότυπο μοτίβο όταν το πλάτος του επόμενου πεδίου εξαρτάται από την τιμή του προηγούμενου.
Τι να μεταφέρετε παρακάτω#
Ένας πίνακας διάταξης bytes ενός RFC αντιστοιχίζεται απευθείας σε συμβολοσειρά προτύπου. Big-endian shorts →
n, big-endian longs →N, bytes →C.Τα πεδία με πρόθεμα μήκους χρησιμοποιούν
C/a*/n/a*/ … - μία έκφραση, χωρίς μέτρημα off-by-one με το χέρι.Τα πεδία επιπέδου bit μέσα σε ένα byte αποκωδικοποιούνται σε σκέτη Perl αφού η
unpackσας έχει παραδώσει το byte ή το short που τα περιέχει.Τα δεδομένα μεταβλητού πλάτους απαιτούν αποπακετάρισμα σε στάδια. Διαβάστε ένα μήκος, μετά κατασκευάστε το πρότυπο για τα δεδομένα από αυτό το μήκος, μετά καλέστε ξανά την
unpack.
Το επόμενο κεφάλαιο εφαρμόζει την ίδια προσέγγιση σε μια πραγματική μορφή αρχείου - ένα header εικόνας GIF - και εισάγει το ιδίωμα του «ελέγχου μαγικού αριθμού».