برنامه نویسی

شبکه پایتون: سربرگ IP – انجمن DEV

در قسمت قبلی ما در مورد اتصال شبکه اولیه صحبت کردیم. اکنون هنگام ارسال چیزی به سرور، فقط داده‌های ارسالی نیستند، بلکه ابرداده‌های مربوط به داده‌ها نیز هستند. این مورد استفاده قرار می گیرد تا روترها بدانند ترافیک شبکه را به کجا هدایت کنند (یا در برخی موارد آن را مسدود کنند). در این مقاله به بخشی از انتقال شبکه که با اطلاعات IP سر و کار دارد نگاه می کنیم.

اما ابتدا مقداری باینری

باینری دنباله ای از 1 و 0 است که سطوح مختلف توان دو را نشان می دهد. به عنوان مثال، اگر مقدار باینری را 1111 بگیریم، تبدیل می شود:

2^0 + 2^1 + 2^2 + 2^3

یا 15. 2^0 بودن 1 به مقادیر فرد اجازه می دهد. مهم است که توجه داشته باشید که یک توان تنها در صورتی محاسبه می شود که یک عدد 1 در دنباله وجود داشته باشد. بنابراین:

0011 = 3 (2^0 + 2^1)
0101 = 5 (2^0 + 2^2)

از نظر اینکه چگونه داده های باینری می توانند در سطح پایین تر کار کنند، اغلب در یک مقدار ثابت قرار می گیرند. به عنوان مثال یک مقدار 8 بیتی (1 بایت) را در نظر بگیرید. این مقدار حداکثر 255 را دارد. این مسئله چگونگی مدیریت مقدار 1 را مطرح می کند. در باینری 1 به سادگی با 1 نشان داده می شود که 2^0 است. پس با 7 بیت دیگر چه باید کرد؟ مقادیر باینری در یک طول ثابت با 0 padding در سمت چپ این کار را انجام می دهد. این به این معنی است که اکنون 1 می شود:

00000001

در پایتون، منظور از انواع داده‌ها این است که تعداد مشخصی بایت را اشغال کنند. کمترین مقدار فضای اشغال شده توسط این نوع داده ها 1 بایت یا 8 بیت است. از آنجایی که اطلاعات شبکه را می توان در کمتر از یک بایت برای حفظ فضا بسته بندی کرد، از عملگرهای بیتی برای کار با داده های سطح بیت زیرین مقادیر بایت استفاده می شود. برای مثال یک میدان 8 بیتی را در نظر بگیرید که به دو قسمت 4 بیتی تقسیم شده است. یکی دارای مقدار 6 و دیگری دارای مقدار 3:

اکنون سؤال این است که چگونه مقادیر مربوطه را بدست آوریم؟ برای مقابله با این موضوع، پایتون را باز می کنم و یک مقدار موقت با مقادیر ترکیبی 4 بیتی تنظیم می کنم:

my_value = 0b01100011
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ابتدا به بدست آوردن مقدار 6 می پردازیم که 4 بیت اول مقدار 8 بیت است. برای این کار می توان از عملگر شیفت سمت راست استفاده کرد. چند نمونه از نحوه عملکرد آن:

>>> format(my_value, '08b')
'01100011'
>>> format(my_value >> 1, '08b')
'00110001'
>>> format(my_value >> 2, '08b')
'00011000'
>>> format(my_value >> 3, '08b')
'00001100'
>>> format(my_value >> 4, '08b')
'00000110'
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

format با 08b به ما این امکان را می دهد که مقادیر باینری یک متغیر را به طول خاصی (8 بیت) چاپ کنیم و در صورت لزوم، سمت چپ را با 0s قرار دهیم. در هر تکرار یک مقدار از سمت راست حذف می شود و 0 به سمت چپ اضافه می شود. با جابجایی مقدار برای 6 روی 4 فاصله و اضافه کردن سمت چپ با 0، اساساً بخش 4 بیتی دوم حذف می شود و چون سمت چپ با 0 ها پر شده است، مقدار 6 را به دست می آوریم:

>>> 0b00000110
6
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین یک ایده منطقی می تواند این باشد که معکوس شیفت راست را امتحان کنید، که شیفت چپ است. مشکل این است که جهت مخالف است. 0های جایگزین دیگر بالشتک نیستند، بلکه بخشی از مقدار واقعی هستند. مثلا:

>>> format(my_value, '08b')
'01100011'
>>> format(my_value << 1, '08b')
'11000110'
>>> format(my_value << 2, '08b')
'110001100'
>>> format(my_value << 3, '08b')
'1100011000'
>>> format(my_value << 4, '08b')
'11000110000'
>>> my_value << 4
1584
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این به این دلیل است که تغییر سمت چپ باعث می‌شود که 1ها به مقادیر بالاتری از توان دو نسبت به آنچه که در ابتدا انجام می‌دادند ضربه بزنند و اعمال شوند. برای مقابله با این موضوع می توانیم از یک عملگر بیتی برای 4 بیت آخر استفاده کنیم. عملگر بیتی “&” دو مقدار باینری را می گیرد، در صورت لزوم، صفر را در سمت چپ قرار می دهد و بر اساس این منطق، هر مقدار را با هم مقایسه می کند:

  • 0 و 0 0 است
  • 0 و 1 0 است
  • 1 و 1 1 است

با توجه به این منطق می توانیم از مقدار استفاده کنیم 00001111 به عنوان میانبری برای پوشاندن 4 بیت اول به عنوان لایه صفر و 4 بیت آخر همانطور که هست تبدیل می شود. کوتاه نویسی 0xF همچنین کار می کند به عنوان F هگزادسیمال 1111 است که با 4 0s خالی می شود تا همین کار را انجام دهد:

>>> my_value & 0b00001111
3
>>> my_value & 0xF
3
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه داشته باشید که تلاش برای انجام این کار برای بیت های x اول به دلایل مشابه برای تغییر به چپ کار نمی کند. بنابراین قوانین عبارتند از:

  • اگر بیت های x اول یک مقدار را می خواهید: مقدار >> (طول_مقدار_در_بیت – x)
  • اگر آخرین x بیت های یک مقدار را می خواهید: value & 0b[1 repeated x times]

هر دوی اینها فرض می کنند که طول بیت ها برای مقدار بزرگتر یا مساوی تعداد بیت هایی است که می خواهید بدست آورید.

شروع یک بسته Sniff

با پایتون می توانید از آن استفاده کنید socket کتابخانه برای یک بسته sniffer داخلی ساده:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP)
s.bind(("192.168.1.81", 0))
s.ioctl(socket.SIO_RCVALL,socket.RCVALL_ON)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه داشته باشید که این عمل به مجوزهای مدیریتی مانند root/sudo در *NIX یا یک خط فرمان بالا در ویندوز نیاز دارد. بنابراین ابتدا یک سوکت خام ایجاد می شود. این نوع سوکت به ما اجازه می دهد تا بسته ها را به شکل خام در مقابل پس از تجزیه و انتزاع برای ما مشاهده کنیم. را bind مورد نیاز است تا sniffer بسته ما بتواند داده ها را دریافت کند. ارائه پورت 0 اساساً به سیستم عامل می‌گوید که یک پورت آزاد تصادفی را انتخاب کند زیرا ما واقعاً اهمیتی نمی‌دهیم از چه پورتی استفاده کنیم. سرانجام، SIO_RCVALL تنظیم شده است RCVALL_ON که به کنترلر رابط شبکه (NIC) ما می گوید که وارد حالت خاصی به نام حالت promiscuous شود. این به طور موثری sniffer بسته ما را قادر می سازد تا بسته ها را دریافت کند.

اکنون برای دریافت یک بسته باید بدانیم که با چه مقدار داده کار کنیم. معلوم شد که مشخصات پروتکل اینترنت دیکته می کند که حداکثر مقداری که یک بسته می تواند داشته باشد 65535 اکتت باشد. Octets به سادگی راهی برای تعیین 8 بیت در زمانی است که 1 بایت لزوماً برابر با 8 بیت نیست. بنابراین دریافت یک بسته:

packet = s.recvfrom(65535)
packet = packet[0]
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

را packet = packet[0] تکلیف به این دلیل است s.recvfrom چند عدد از (داده، آدرس) را برمی‌گرداند که قسمت آدرس برای ما مهم نیست و فقط اولین مقدار داده است. اکنون بسته چیزی شبیه به این به نظر می رسد (قطع شده زیرا بسیار بزرگ بود):

>>> بسته (b'E\x00\x05\x1dw\'@\x00\x80\x06\x00\x00\xc0\xa8\x01Qh\xf4*D\xccS\x01\xbb\xe5\x80\xad\ x0b\xdeU\xce\xb3P\x18\x04\x01ZA\x00\x00\x17\x03\x03\x04\xf0\xa5\xd2ns\xa4\x14\xe1\xda\x03[\x06\x11[hC\\x8\x8f\xd1\x02\x06\xb2\xa1%\x91|D\xbclt\x0f GB4\xf1h\xf2Y\xfa\xee\x05i~\xfb\x88\xe9\xbeN\x17t\x1f\xb0K#\x8b\xa9\xa1P\x107l{\x9e]\xb2\x9c\x13\x10%\x02`?\xdd\x0b\xc20\x95\xbf\x07>\xa1\xd1\xb8\xc5\xe8d\x8e\xbf{\xb5\x84ip\x9aJ\x1c\ x8e{\x1f\xae1y\xc0\x9f\x89\xda\xaaZ\xac[\x80\xb6\xa3\xd1YX\xed\xde\x8c\xcea\x84\x13w5\xd2\x8aD9Ur\xe1\xdf\x1c-\x1aWB\tl_\x921Ek\xf7\xd7\xca\xb7\x148\x18\x91\x15\\!M\xaf\xab\xc2\xbf\\F\x06\xba\x8d\xfe\t%\xc4b\xa1\xf6\xb2\x0eS\x9b@F\xb1&\x8e\x19Q\xa7\x80\x10\xe3|L~\xbbr\x8f\xb2\x06\x844\xab\xfe\x0e\xd1\x08[<\xc1;~i\xfc\x92\x89\xf9\x8f\xe6\xf7[\xee\xf9\xa4;o\xaf\xde\xcd<snip>
Enter fullscreen mode

Exit fullscreen mode

IP Header

The first part of the packet is an IP header. This is a special series of bytes which gives information about the source, destination, and some other useful metadata of the packet. This information is utilized by networking hardware such as routers to know where to direct the traffic to (or in some cases drop the traffic). Request for comments lists an IP header’s format in section 3.1. A cleaner visual format can be found on the IPv4 Wikipedia page:

Visual table of the parts of the IPv4 header

The header itself is made up of a minimum of 5 32 bit fields with each field spanning 4 bytes. This makes 20 bytes the minimum size of an IPv4 header. The rest can have up to 10 option fields making the total maximum size of an IP header out to be 60 bytes. The layout already shows some values that are sub byte level such as 3 bits for Flags and 4 bits for Version. This means we’ll need to use bitwise operators to obtain some of the values.

Understanding Unpack

As-is the stream of bytes is pretty difficult to work with. You could use something like splices to target individual byte groups. Instead though we can use the handy struct.unpack method. This reads in binary data that’s packed in sequential order which is essentially what our IP packet is. According to the RFC 791 standard the mandatory fields for a minimum size 20 byte IP header is:

1st 32 bit field

  • Version: 4 bits
  • IHL: 4 bits
  • Type of Service: 8 bits
  • Total Length: 16 bit

2nd 32 bit field

  • Identification: 16 bits
  • Flags: 3 bits
  • Fragment Offset: 13 bits

3rd 32 bit field

  • Time to Live: 8 bits
  • Protocol 8 bits
  • Header Checksum: 16 bits

4th 32 bit field

5th 32 bit field

  • Destination Address: 32bits

The way struct.unpack works is it takes binary streams and maps them to specific data types of various byte sizes. The first part of the arguments will be related to byte ordering. In the old days there was a divide between big endian and little endian. Kohei Otsuka has a nice article on the differences. Most modern PCs are little endian encoding which you can find through sys.byteorder:

>>> import sys
>>> sys.byteorder
'little'
Enter fullscreen mode

Exit fullscreen mode

Network protocols on the other hand were standardized around when big endian had a bigger share so that’s what’s used in network communications. Thankfully python has a “!” format flag to indicate network byte ordering or big endian. Now it’s time to map out the values. The format chart for unpack shows the various format identifiers, what it maps to in C, and their size in bytes. Given that headers work with positive values the unsigned version tends to be used in most unpack statements. There’s also a bytes format if you still want to keep the underlying binary data intact. Taking from the format table into account:

  • unsigned char (1 byte/8 bit values) B
  • unsigned short (2 byte/16 bit values) H
  • unsigned long (4 byte/32 bit values) L
  • Xs = X number of bytes as is

Ignoring sub byte values we’ll pull everything in as:

  • B: Version + IHL (8 bits)
  • B: Type of Service (8 bits)
  • H: Total Length (16 bits)
  • H: Identification (16 bits)
  • H: Flags + Fragment Offset (16 bits)
  • B: Time to Live (8 bits)
  • B: Protocol (8 bits)
  • H: Header Checksum (16 bits)
  • 4s: Source (IP) Address (32 bits as-is)
  • 4s: Dest (IP) Address (32 bits as-is)

So we’ll take the first 20 bytes of the packet (which we can do with list splices since each index of the packet is a byte) and then unpacking it using our final expression:

>>> ip_header_bytes = packet[0:20]
>>> import struct >>> ip_header = struct.unpack('!BBHHHBBH4s4s', ip_header_bytes) >>> ip_header (69, 0, 1309, 30503, 16384, 128, 6, 0, b'8\xc0\xa ', b'h\xf4*D')
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می توانیم روی تجزیه فیلدهای واقعی کار کنیم.

تجزیه یک فیلد هدر IP

ما 8 بایت اول را می گیریم که شامل نسخه و طول سرصفحه اینترنت (IHL) است. در فرم باینری در حال حاضر به نظر می رسد:

>>> format(ip_header[0], '08b')
'01000101'
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پس اینطور 0100 به صورت دودویی و 0101 در باینری در مجموع 8 بیت بسته بندی شده است. با استفاده از شیفت سمت راست می توانیم 4 بیت اول را بدست آوریم. همانطور که در بخش باینری ذکر شد برای به دست آوردن اولین X بیت ها، تعداد کل بیت ها را 8 می گیرید و تعداد بیت های مورد نظر خود را 4 کم می کنید تا به 4 برسید:

>>> ip_version = ip_header[0] >> 4
>>> format(ip_version, '08b')
'00000100'
>>> ip_version
4
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون از نظر فنی، از آنجایی که ما فقط بسته های IPv4 را وارد می کنیم، واقعاً نیازی به توجه به این موضوع نیست، زیرا همیشه می دانیم که نسخه 4 است. به روشی مشابه، فیلد سرصفحه اینترنت 4 بیت آخر است و می توان از طریق یک عملگر بیتی به دست آورد. :

>>> ihl = ip_header[0] & 0xf
>>> ihl
5
>>> format(ihl, '08b')
'00000101'
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با نگاه کردن به مقدار کامل، هر دو مقدار 4 بیتی نشان داده می شوند تا بدانیم کد کار می کند.

کل بسته

اکنون که اصول اولیه از کار افتاده است، می‌توانیم بر روی بقیه هدر IP کار کنیم. Type of Service مقداری است که به طور بالقوه می تواند برای اولویت بندی ترافیک استفاده شود در صورتی که تجهیزات شبکه از آن پشتیبانی کند. با نگاه کردن به خارج از میدان، 3 بیت برای پرچم های اولویت، 3 بیت دیگر که با وزن کردن یک بسته سروکار دارند، و دو بیت دیگر هستند که رزرو شده اند. اگر به کل مقدار یک بایت نگاه کنیم:

>>> format(ip_header[1], '08b')
'00000000'
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

طبق مشخصات، اولویت عادی است و بقیه خصوصیات ترافیکی نیز در حالت عادی تنظیم شده است. مثال دیگری با اولویت بندی در عمل:

10111000

این دارای اولویت CRITIC/ECP سطح بالاتری است و ویژگی هایی را برای تاخیر کم و توان عملیاتی بالا تنظیم می کند. بعدی طول کل بسته است:

>>> ip_header[2]
1309
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این به این معنی است که حجم کل داده ها 1309 بایت است که با طول لیست بایت ها مطابقت دارد:

>>> len(packet)
1309
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

شناسایی مقدار بعدی است:

>>> format(ip_header[3], '08b')
'111011100100111'
>>> ip_header[3]
30503
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این به سادگی به عنوان یک شناسه در مونتاژ بسته استفاده می شود. این مقدار دارای پرچم هایی است که 3 بیت اول یک مقدار 16 بیتی را می گیرد:

>>> format(ip_header[4] >> 13, '03b')
'010'
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با نگاه کردن به مشخصات، مقدار اول همیشه 0 است. مقدار بعدی نشان می دهد که این بسته نباید به قطعات تقسیم شود. به همین دلیل افست قطعه نیز تنظیم نمی شود:

>>> format(ip_header[4] & 0b0001111111111111, '08b')
'00000000'
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مقاله جالبی از استیون ایوسون وجود دارد که در مورد تکه تکه شدن با جزئیات بیشتر صحبت می کند. در مرحله بعد، زمان حیات (TTL) بسته است که در عرض چند ثانیه از اینترنت عبور می کند:

>>> ip_header[5]
128
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این مقدار همچنین می تواند در حین مسیر تغییر کند تا زمان بیشتری اضافه شود. بعدی پروتکل است:

>>> ip_header[6]
6
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

RFC 790 مقادیر اعشاری و اکتال مربوطه را در پروتکل های مربوطه فهرست می کند. در این مورد ما با پروتکل 6 یا TCP سر و کار داریم. بعدی جمع چک سرصفحه است:

>>> format(ip_header[7], '016b')
'0000000000000000'
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این چیزی است که معمولاً در سمت مشتری نخواهید دید. در عوض، در صورت تغییر مقادیر TTL که به نوبه خود جمع کنترلی را تغییر می‌دهد، با انتقال آن، تغییراتی در آن ایجاد می‌شود. آدرس منبع و مقصد از نظر تجزیه یکسان هستند:

>>> socket.inet_ntoa(ip_header[8])
'192.168.1.81'
>>> socket.inet_ntoa(ip_header[9])
'104.244.42.68'
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

socket.inet_ntoa به شکل دودویی یک آدرس IPv4 بسته بندی شده در بایت ها را دریافت می کند و علامت نقطه استانداردی را که ممکن است به آن عادت کرده باشید برمی گرداند. با انجام ARIN whois در سمت مقصد، متوجه می‌شویم که IP متعلق به توییتر است، بنابراین این بسته با سرورهای توییتر ارتباط برقرار می‌کند.

نتیجه

این به بررسی ساختار هدر IP و کار با آن در سطح باینری با استفاده از پایتون پایان می دهد. جالب است که در پشت پرده از آنچه در ترافیک استاندارد اینترنت می گذرد، اوج بگیرید. در قسمت بعدی این سری، دو پروتکل محبوب را بررسی خواهیم کرد: TCP و UDP.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا