تصویر مقاله
Photo by Miguel A Amutio on Unsplash

آدرس‌های IPv4-mapped IPv6

آدرس‌های IPv4-mapped IPv6 به برنامه‌های IPv6 در یک میزبان Dual Stack امکان می‌دهند تا با میزبان‌های مبتنی بر IPv4 ارتباط برقرار کنند. زمانی که یک برنامه IPv6 برای میزبانی که صرفاً از IPv4 پشتیبانی می‌کند درخواست آدرس IPv6 می‌دهد، این آدرس به‌صورت خودکار توسط DNS Resolver تولید می‌شود.

تصویر مقاله
آدرس IPv4-mapped IPv6
  • در این ساختار، ۳۲ بیت پایینی به ذخیره آدرس IPv4 اختصاص داده می‌شود.
  • از آنجا که در IPv6 رشته‌های صفر متوالی با : خلاصه می‌شوند و آدرس‌های IPv4 نیز به‌صورت ده‌دهی با نقطه نمایش داده می‌شوند، یک آدرس IPv4-mapped IPv6 را می‌توان به این شکل نوشت: ::FFFF:12.106.32.254

حالت اول: کلاینت IPv4 — سرور IPv6

تصویر زیر نشان می‌دهد که چگونه یک سرور TCP از نوع IPv6 در میزبانی با قابلیت Dual Stack، می‌تواند هم‌زمان به کلاینت‌های TCP از هر دو نوع IPv4 و IPv6 سرویس‌دهی کند.

تصویر مقاله
سرویس‌دهی سرور IPv6 در میزبان Dual Stack به هر دو کلاینت IPv4 و IPv6

وقتی سرور IPv6 راه‌اندازی می‌شود، یک سوکت شنود از نوع IPv6 متصل به آدرس Wildcard ایجاد می‌کند. در این مرحله، کلاینت‌های IPv4 یا IPv6 برای برقراری ارتباط با سرور، باید یک سگمنت SYN ارسال کنند.

در این حین، کلاینت IPv4 با فراخوانی تابع gethostbyname() به دنبال رکوردهای مربوط به سرور می‌گردد. با وجود اینکه میزبان سرور دارای هر دو رکورد A و AAAA است، کلاینت IPv4 صرفاً رکورد A را درخواست می‌کند.

سپس کلاینت با فراخوانی تابع connect()، بسته SYN را ارسال می‌کند.

  • کلاینت IPv4 بسته را در قالب یک دیتاگرام IPv4 (شامل هدر IPv4 + هدر TCP + داده‌های TCP) ارسال می‌کند. مقدار فیلد Type در هدر اترنت 0x0800 است که نشان می‌دهد این فریم از نوع IPv4 است؛ همچنین شماره پورت نیز درون هدر TCP قرار دارد.
  • از سوی دیگر، کلاینت IPv6 داده‌ها را در قالب دیتاگرام IPv6 (شامل هدر IPv6 + هدر TCP + داده‌های TCP) می‌فرستد. در اینجا مقدار فیلد Type در هدر اترنت برابر با 0x86dd است که نشان‌دهنده فریم IPv6 می‌باشد.

میزبانی که این سگمنت را دریافت می‌کند، با بررسی نوع هدر اترنت، آن را به ماژول IP مناسب تحویل می‌دهد.

  • ماژول IPv4 با تشخیص اینکه سوکت مقصد از نوع IPv6 است، آدرس را به فرمت IPv4-mapped IPv6 تبدیل می‌کند. این آدرس سپس به‌عنوان مقدار خروجی تابع accept() به پردازه سرور تحویل داده می‌شود و پس از آن، سرور برای برقراری اتصال یک بسته SYN/ACK از نوع IPv4 برمی‌گرداند.
  • ماژول IPv6 نیز دیتاگرام دریافتی را بدون هیچ تغییری مستقیماً به مقصد منتقل می‌کند.

پس از برقراری ارتباط، سرور می‌تواند دیتاگرام‌ها را به مقصد آدرس کلاینت ارسال کند. در این شرایط، اگر آدرس مقصد از نوع IPv4-mapped IPv6 باشد، پشته IP متوجه می‌شود که مقصد یک میزبان IPv4 است و خودبه‌خود دیتاگرامی با آدرس IPv4 می‌فرستد.

جالب اینجاست که در طول این فرآیند، سرور اصلاً متوجه نمی‌شود که در حال تبادل داده با یک کلاینت IPv4 است؛ چرا که تمام این جزئیات در لایه پشته IP مدیریت می‌شود. کلاینت IPv4 نیز به همین ترتیب هیچ اطلاعی از ارتباط خود با یک سرور IPv6 ندارد.

البته این سناریو بر این فرض استوار است که میزبان سرور هر دو آدرس IPv4 و IPv6 را در اختیار داشته باشد؛ فرضی که تا پیش از پر شدن کامل آدرس‌های IPv4 کاملاً معتبر است.

این سناریو برای سرورهای UDP نیز به شیوه‌ای کاملاً مشابه کار می‌کند.

  • دیتاگرامی که کلاینت IPv4 ارسال می‌کند، در قالب مقدار خروجی تابع recvfrom() به یک آدرس IPv4-mapped IPv6 تبدیل شده و به سرور تحویل داده می‌شود.
  • هنگامی که سرور بخواهد به کلاینت پاسخ دهد، تابع sendto() را با مقصد همان آدرس IPv4-mapped IPv6 فراخوانی می‌کند و پشته IP نیز متناسب با آدرس مقصد، دیتاگرام IPv4 یا IPv6 مناسب را ایجاد می‌نماید.

نمودار زیر نحوه مدیریت و پردازش دیتاگرام‌های IPv4/v6 را بر اساس نوع سوکت دریافت‌کننده نشان می‌دهد.

تصویر مقاله
فرآیند پردازش دیتاگرام‌های IPv4/v6 بر اساس نوع سوکت شنود
  • وقتی پروتکل دیتاگرام با نوع سوکت کاملاً همخوانی داشته باشد، بسته‌ها بدون تغییر پردازش می‌شوند؛ این مسیر در تصویر بالا با خطوط ممتد نمایش داده شده است.
  • اگر یک دیتاگرام IPv4 برای یک سوکت IPv6 دریافت شود، پشته IP آن را به یک آدرس IPv4-mapped IPv6 تبدیل کرده و از طریق توابع accept() (در پروتکل TCP) یا recvfrom() (در پروتکل UDP) بازمی‌گرداند.
  • اما برعکس این حالت امکان‌پذیر نیست؛ یعنی نمی‌توان دیتاگرام IPv6 را به مقصد یک سوکت IPv4 پردازش کرد، زیرا آدرس‌های طولانی‌تر IPv6 را نمی‌توان در قالب آدرس‌های IPv4 بازنمایی کرد.

بیشتر میزبان‌های Dual Stack هنگام مدیریت سوکت‌های شنود، از قواعد زیر پیروی می‌کنند:

  1. سوکت شنود IPv4 صرفاً درخواست‌های اتصال دریافتی از سوی کلاینت‌های IPv4 را می‌پذیرد.
  2. سوکت شنود IPv6، در صورتی که به آدرس Wildcard متصل باشد و گزینه سوکت IPV6_V6ONLY فعال نشده باشد، درخواست‌های اتصال هر دو کلاینت IPv4 و IPv6 را تأیید می‌کند.
  3. اما اگر سوکت شنود IPv6 فقط به آدرس‌های IPv6 متصل شده باشد یا گزینه IPV6_V6ONLY فعال باشد، تنها اتصالات کلاینت‌های IPv6 را می‌پذیرد.

حالت دوم: کلاینت IPv6 — سرور IPv4

حال بیایید سناریو را معکوس کنیم و عملکرد سیستم را با یک کلاینت IPv6 و یک سرور IPv4 بسنجیم. عملکرد یک کلاینت TCP از نوع IPv6 در میزبانی با قابلیت Dual Stack به این صورت است:

  1. ابتدا سرور IPv4 یک سوکت شنود IPv4 ایجاد می‌کند.
  2. کلاینت IPv6 با فراخوانی تابع getaddrinfo() آدرس IPv6 سرور را جستجو می‌کند. در این مرحله با انتخاب آرگومان‌های AF_INET6 و AI_V4MAPPED، برای سرور IPv4 که صرفاً دارای رکورد A است، یک آدرس IPv4-mapped IPv6 تحویل داده می‌شود.
  3. سپس کلاینت IPv6 تابع connect() را با استفاده از آدرس دریافت شده فراخوانی می‌کند.
  4. کرنل سیستم با تشخیص این آدرس مپ‌شده، یک سگمنت SYN از نوع IPv4 تولید کرده و به سرور تحویل می‌دهد.
  5. سرور IPv4 نیز با یک بسته SYN/ACK از نوع IPv4 پاسخ می‌دهد و اتصال با موفقیت برقرار می‌شود.

این روند در تصویر زیر به تصویر کشیده شده است:

تصویر مقاله
فرآیند مدیریت درخواست کلاینت بر اساس نوع آدرس و سوکت
  • اگر نوع دیتاگرام با نوع آدرس مقصد یکسان باشد، درخواست بدون تغییر پردازش می‌شود که در تصویر بالا با خطوط ممتد نمایش داده شده است.
  • چنانچه کلاینت IPv6 توابع connect() یا sendto() را به مقصد یک آدرس IPv4-mapped IPv6 فراخوانی کند، هسته سیستم متوجه می‌شود که سرور مقصد از آدرس IPv4 استفاده می‌کند و در نتیجه یک دیتاگرام IPv4 ارسال خواهد کرد.
  • اما کلاینت IPv4 تحت هیچ شرایطی نمی‌تواند توابع connect() یا sendto() را به مقصد یک آدرس IPv6 فراخوانی کند؛ زیرا یک آدرس ۱۶ بایتی IPv6 را نمی‌توان درون ساختار ۴ بایتی in_addr در ساختار sockaddr_in مربوط به IPv4 جا داد.

جمع‌بندی تعامل‌پذیری IPv4 و IPv6

تصویر زیر خلاصه‌ای جامع از تمامی مباحث مطرح‌شده را نشان می‌دهد:

تصویر مقاله
خلاصه تعامل‌پذیری و سازگاری میان کلاینت‌ها و سرورهای IPv4 و IPv6
  • خانه‌هایی که با IPv4 یا IPv6 مشخص شده‌اند، نشان‌دهنده ترکیب‌های معتبر بوده و پروتکلی را نشان می‌دهند که ارتباط نهایی بر بستر آن صورت می‌گیرد.
  • خانه‌هایی که با (no) مشخص شده‌اند، نشان‌دهنده نامعتبر بودن آن ترکیب هستند. با این حال، علامت (no*) به این معنی است که نحوه عملکرد سیستم به آدرسی بستگی دارد که کلاینت قصد اتصال به آن را دارد. در واقع اگر با تعیین رکورد AAAA یک دیتاگرام IPv6 ارسال شود با خطا مواجه خواهد شد، در حالی که در صورت ارسال دیتاگرام IPv4 سیستم به درستی کار خواهد کرد.
  • (در زمان نگارش این کتاب) پیش‌بینی می‌شود که در آینده بیشتر هاست‌ها از Dual Stack و هر دو پروتکل IPv4/IPv6 پشتیبانی کنند؛ بنابراین اکثر ترکیب‌های (no) چالش‌برانگیز نخواهند بود.

ماکروهای بررسی آدرس IPv6

برخی از برنامه‌های کاربردی مبتنی بر IPv6 باید قادر به تشخیص این باشند که آیا طرف مقابل از IPv4 استفاده می‌کند یا خیر؛ به بیان دیگر، باید بتوانند از روی مقدار آدرس، معمولی بودن آدرس IPv6 یا IPv4-mapped IPv6 بودن آن را شناسایی کنند. برای این منظور، ۱۲ ماکرو در اختیار ما قرار گرفته است.

C++
  • از آنجا که آدرس‌های IPv4 دیگر در اولویت قرار ندارند، نیازی به بررسی و تست آن‌ها نیست.
  • یک کلاینت IPv6 می‌تواند با فراخوانی ماکروی IN6_IS_ADDR_V4MAPPED، آدرس IPv6 دریافتی از Resolver را بررسی کند.
  • یک سرور IPv6 نیز می‌تواند با فراخوانی ماکروی IN6_IS_ADDR_V4MAPPED، آدرس IPv6 برگشتی از توابع accept() یا recvfrom() را تست و ارزیابی نماید.

به عنوان نمونه‌ای از برنامه‌هایی که به چنین ماکروهایی نیاز دارند، می‌توان به سرویس FTP اشاره کرد. در پروتکل FTP دستوری به نام PORT وجود دارد که کلاینت IPv4 از طریق آن، آدرس IP و پورت خود را به سرور منتقل می‌کند. سپس سرور با استفاده از این اطلاعات، اتصال مربوط به انتقال داده را برقرار می‌سازد.

اما در خصوص کلاینت FTP مبتنی بر IPv6، چنانچه سرور از نوع IPv4 باشد باید از دستور PORT و اگر از نوع IPv6 باشد باید از دستور EPRT استفاده شود. در چنین شرایطی، ماکروهای فوق به ما کمک می‌کنند تا نوع پروتکل سرور (IPv4 یا IPv6) را به درستی تشخیص دهیم.

قابلیت حمل کد منبع (Source Code Portability)

(در زمان نگارش این کتاب) بیشتر برنامه‌های موجود با این فرض توسعه یافته‌اند که از آدرس‌های IPv4 استفاده خواهند کرد. تبدیل یک برنامه IPv4 به IPv6 معمولاً بدون دردسر و تلاش زیاد امکان‌پذیر است؛ با این وجود، برنامه‌هایی که از ویژگی‌های وابسته به IPv4 مثل Multicasting، تنظیمات IP Options یا سوکت‌های Raw استفاده می‌کنند، به کار و بازبینی بیشتری نیاز خواهند داشت.

هنگام مهاجرت به IPv6 باید بررسی کرد که آیا سیستم مقصد نیز از IPv6 پشتیبانی می‌کند یا خیر. یک روش رایج این است که در کد از دستور #ifdef استفاده کنیم و فقط در صورت امکان به سراغ IPv6 برویم؛ اما این روش نگهداری کد را بسیار دشوار می‌کند. راهکار ایده‌آل این است که ساختار برنامه را طوری تغییر دهیم که به هیچ پروتکل خاصی وابسته نباشد.

توصیه می‌شود به جای توابع gethostbyname و gethostbyaddr، از توابع مدرن‌تر getaddrinfo و getnameinfo استفاده شود. این کار به ما اجازه می‌دهد ساختار آدرس سوکت را به عنوان یک شیء مات (Opaque Object) در نظر بگیریم که تنها از طریق اشاره‌گر و اندازه به آن ارجاع داده می‌شود.

با به‌کارگیری توابع مرتبط با sock_ که در فصل ۳ معرفی شدند، می‌توان فارغ از اینکه اتصال مبتنی بر IPv4 است یا IPv6 با آن کار کرد. گرچه این توابع در ساختار داخلی خود از #ifdef استفاده می‌کنند، اما وابستگی به پروتکل را درون کتابخانه پنهان کرده و در نتیجه کد نهایی ما را بسیار ساده و روان نگه می‌دارند.

خلاصه

  1. یک سرور IPv6 روی هاستی با پشتیبانی از Dual Stack می‌تواند به طور هم‌زمان پاسخگوی درخواست‌های کلاینت‌های IPv4 و IPv6 باشد.
  2. کلاینت‌های IPv6 روی هاست‌های Dual Stack نیز امکان برقراری ارتباط با سرورهای IPv4 را خواهند داشت.
  3. در شرایط خاصی که دانستن پروتکل طرف مقابل ضروری است، می‌توان با استفاده از ماکروهای از پیش تعریف‌شده نوع آن را بررسی کرد.

تمرین‌ها

۱۲.۱ دستور PORT ایجاد شده در هنگام اجرای دستور dir توسط کلاینت IPv6 را هنگام اتصال به سرورهای FTP مبتنی بر IPv4 و IPv6 با یکدیگر مقایسه کنید.
  • هنگام اتصال به سرور FTP با پروتکل IPv4، انتقال اطلاعات IP و پورت با دستور PORT انجام می‌شود، در حالی که در اتصال به سرور IPv6، از دستور EPRT استفاده می‌گردد.
Terminal / Bash
۱۲.۲ دو برنامه مجزا بنویسید: (۱) برنامه سرور IPv4 و (۲) برنامه سرور IPv6. اگر ابتدا سرور IPv4 را اجرا کرده و سپس در ترمینالی دیگر برنامه IPv6 را اجرا کنید، چه نتیجه‌ای حاصل می‌شود؟ در صورت استفاده از آپشن سوکت SO_REUSEADDR این رفتار چه تغییری می‌کند؟ اگر ابتدا سرور IPv6 را اجرا کنید چطور؟
Python
Python
  • اگر ابتدا سرور IPv4 اجرا شود، تلاش برای اجرای سرور IPv6 رفتاری وابسته به سیستم‌عامل خواهد داشت. در برخی سیستم‌ها (مانند توزیع‌های خاصی از لینوکس)، سرور IPv6 امکان Bind شدن به همان پورت را ندارد و خطایی رخ می‌دهد؛ اما در سیستم‌های دیگری که از «سوکت‌های Dual Stack» پشتیبانی می‌کنند، ممکن است این اتصال همچنان بدون مانع انجام شود.
  • اگر ابتدا سرور IPv6 اجرا شود و حالت Dual Stack فعال باشد، سرور IPv6 تمام اتصالات IPv4 و IPv6 را مدیریت کرده و مانع از Bind شدن سرور IPv4 به آدرس 0.0.0.0:9999 می‌شود. در مقابل، اگر حالت Dual Stack غیرفعال باشد، سرور IPv4 کماکان می‌تواند به 0.0.0.0:9999 متصل (Bind) شود.
  • حتی با فعال‌سازی آپشن سوکت SO_REUSEADDR نیز دو سوکت فعال قادر نخواهند بود به طور هم‌زمان روی یک ترکیب IP و پورت یکسان در هر دو پروتکل IPv4 و IPv6 عمل Bind را انجام دهند.

منابع و مراجع