الأدلة التقنية

ما هو NAT؟ شرح ترجمة العناوين وتوجيه المنافذ

لماذا تخرج كل أجهزة بيتك بعنوان عام واحد، وكيف يترجم ال Router العناوين، ولماذا يفشل توجيه المنافذ خلف CGNAT.

ما هو NAT؟ شرح ترجمة العناوين وتوجيه المنافذ

افتح موقعاً يعرض عنوانك العام من هاتفك ثم من حاسوبك وأنت على شبكة البيت نفسها، وسوف تجد أن الجهازين يظهران للعالم بالعنوان نفسه، مع أن لكل منهما داخل البيت عنواناً مختلفاً يبدأ غالباً ب 192.168، والأمر نفسه يحدث مع التلفاز والطابعة وكل جهاز يتصل بال Router. والذي يقوم بهذا العمل دون أن تراه هو ترجمة عناوين الشبكة Network Address Translation، واختصاراً NAT، وهي السبب في أن الإنترنت ما زال يعمل بعناوين IPv4 رغم أنها نفدت منذ سنوات، وهي أيضاً السبب في أكثر سؤالين يتكرران عند من يشغل سيرفراً في البيت: لماذا لا يصل أحد إلى خدمتي رغم أني فتحت المنفذ؟ ولماذا يعمل النطاق من الخارج ولا يعمل من داخل البيت؟

وقد مر ذكر NAT في عدد من مقالاتنا، في تشغيل خادمك من إنترنت المنزل عند الحديث عن CGNAT وتوجيه المنافذ، وفي شرح VPN عند العبور من خلف NAT، وفي شبكات Docker عندما يتجاوز Docker جدار ufw، وهذا المقال هو الشرح الكامل للفكرة التي تعود إليه تلك المقالات، وسوف نناقش في هذا المقال ما يلي:

  • المشكلة التي جاء NAT ليحلها: نفاد عناوين IPv4، والعناوين الخاصة Private Addresses التي لا يراها الإنترنت.
  • كيف يترجم ال Router عنوان المصدر Source NAT ويحفظ كل اتصال في جدول الترجمة، ولماذا لا يصح أن تعتبر NAT جدار حماية Firewall.
  • ترجمة عنوان الوجهة Destination NAT وتوجيه المنافذ Port Forwarding، ومشكلة Hairpin NAT وحلها بال Split DNS.
  • CGNAT عند مزود الخدمة، ولماذا لا يفيد فتح المنافذ خلفه.
  • أنواع NAT ولماذا تهم المكالمات والألعاب وWebRTC وشبكات Mesh، وكيف يعمل STUN وTURN وICE.
  • NAT داخل سيرفرك: ما الذي يكتبه Docker في جدول nat، ثم IPv6 وهل ما زلنا نحتاج NAT.
  • تجربة عملية نبني فيها Router بأنفسنا من Containers، ونرى جدول الترجمة والحزم قبل الترجمة وبعدها، ثم أشهر المشكلات وكيف تشخصها.

المشكلة: عناوين IPv4 نفدت وأجهزتك تزيد

كل جهاز على الإنترنت يحتاج عنواناً يصل إليه الآخرون من خلاله، وعنوان IPv4 طوله 32 بت، أي أن عدد العناوين الممكنة كلها 2 أس 32، وهو 4,294,967,296 عنواناً، أي قرابة 4.3 مليار. وقد يبدو هذا الرقم كبيراً، ولكن جزءاً منه محجوز لاستخدامات خاصة كما في سجل IANA للعناوين ذات الاستخدام الخاص، وعدد البشر وحده أكبر منه، ولكل شخص اليوم هاتف وحاسوب وربما تلفاز وساعة وكاميرا، فضلاً عن السيرفرات وأجهزة الشركات.

والنتيجة أن العناوين نفدت فعلاً، فالجهة التي توزع العناوين على مستوى العالم، وهي IANA، تعطي السجلات الإقليمية Regional Internet Registries، واختصاراً RIRs، العناوين في كتل حجم كل منها /8، أي 16,777,216 عنواناً كما في سجل توزيع IPv4، وفي 3 فبراير 2011 أعلنت منظمة NRO أن IANA وزعت آخر خمس كتل لديها، كتلة لكل سجل إقليمي، وبذلك فرغ المخزون العالمي. ثم نفدت مخزونات السجلات واحداً بعد الآخر، فقد وزعت ARIN آخر عناوينها الحرة في 24 سبتمبر 2015، وأعلنت RIPE NCC، وهي التي تخدم أوروبا والشرق الأوسط، في 25 نوفمبر 2019 أنها وزعت آخر كتلة لديها، وأن ما يعود إليها من عناوين يذهب إلى قائمة انتظار.

لنأخذ مثالاً من بيتك: مزود الخدمة ISP يعطيك عنواناً عاماً Public IP واحداً، وقد لا يعطيك حتى هذا كما سيأتي في قسم CGNAT، وفي البيت عشرة أجهزة أو أكثر. والحل الأول الذي يخطر على البال هو أن يعطي المزود كل جهاز عنواناً عاماً، وهذا الحل يفشل لسبب بسيط، وهو أن العناوين غير موجودة أصلاً، ولو وجدت فسوف يدفع كل بيت ثمن عشرة عناوين، وسوف يكون كل جهاز فيه مكشوفاً للإنترنت مباشرة. والحل الذي تعمل به البيوت والشركات اليوم هو أن تأخذ الأجهزة عناوين خاصة تتكرر في كل بيت، وأن يبقى العنوان العام الوحيد عند ال Router، وأن يترجم ال Router بينهما، والصورة التالية تبين ذلك:

مخطط لشبكة منزلية فيها حاسوب وهاتف وحاسوب مكتبي وسيرفر منزلي بعناوين خاصة من 192.168.50.0/24، متصلة بRouter له عنوان عام واحد 203.0.113.10، والعنوان العام يصل إلى الإنترنت أما العناوين الخاصة فلا توجه، مع بطاقة توضح أن عناوين IPv4 قرابة 4.3 مليار وأن مخزون IANA نفد في فبراير 2011
كل الأجهزة بعناوين خاصة، والعنوان العام واحد عند ال Router

العناوين الخاصة: شبكة كاملة لا يراها الإنترنت

حتى تتكرر العناوين في كل بيت دون أن تتعارض، خصص المعيار RFC 1918 عام 1996 ثلاثة نطاقات للشبكات الخاصة، يستخدمها أي أحد دون إذن من أي جهة بشرط ألا تظهر على الإنترنت، ثم أضاف RFC 6598 عام 2012 نطاقاً رابعاً مشتركاً Shared Address Space لمزودي الخدمة تحديداً:

النطاقمنإلىعدد العناوينأين تراه غالباً
10.0.0.0/810.0.0.010.255.255.25516,777,216شبكات الشركات الكبيرة، وشبكات VPN مثل 10.8.0.0/24
172.16.0.0/12172.16.0.0172.31.255.2551,048,576شبكات Docker مثل 172.17.0.0/16 و172.18.0.0/16
192.168.0.0/16192.168.0.0192.168.255.25565,536أغلب أجهزة ال Router المنزلية
100.64.0.0/10100.64.0.0100.127.255.2554,194,304شبكات CGNAT عند المزود، وعناوين Tailscale

والمهم هنا أن أجهزة التوجيه على الإنترنت لا تعرف طريقاً إلى هذه النطاقات، فالمعيار نفسه يطلب ألا تنشر المسارات Routes إليها خارج الشبكة الخاصة، والسبب واضح، فالعنوان 192.168.1.10 موجود في ملايين البيوت في اللحظة نفسها، فإلى أي بيت يذهب الرد؟ وهذا يعني أن جهازك يستطيع أن يرسل حزمة Packet إلى الإنترنت بعنوانه الخاص، ولكن الرد لن يعود أبداً، وسوف ترى ذلك بنفسك في التجربة العملية، حيث تصل الحزمة إلى السيرفر فلا يجد طريقاً يرد منه.

وقد يتساءل البعض: إذا كانت العناوين الخاصة لا تخرج إلى الإنترنت، فكيف أتصفح من حاسوبي الآن؟ والإجابة على ذلك أن ال Router لا يرسل حزمتك كما هي، وإنما يغير فيها عنوان المصدر قبل أن يخرجها، وهذا هو NAT.

كيف يعمل NAT خطوة خطوة؟

لنفرض أن حاسوبك 192.168.50.10 يفتح موقعاً على السيرفر 203.0.113.80، وأن عنوان ال Router العام هو 203.0.113.10، فالذي يحدث كما يلي:

  1. يرسل الحاسوب الحزمة من عنوانه ومن منفذ مصدر Source Port يختاره النظام، وليكن 192.168.50.10:40000، إلى 203.0.113.80:80، ولأن الوجهة خارج شبكته يرسلها إلى البوابة الافتراضية Default Gateway، وهي ال Router.
  2. قبل أن يخرج ال Router الحزمة من واجهة الإنترنت WAN، يغير عنوان المصدر إلى عنوانه العام فتصبح 203.0.113.10:40000، ويكتب في جدول الترجمة Translation Table صفاً يقول إن هذا الاتصال يعود إلى 192.168.50.10:40000. وهذه العملية اسمها ترجمة عنوان المصدر Source NAT، واختصاراً SNAT.
  3. يرى السيرفر طلباً قادماً من 203.0.113.10، فيرسل الرد إليه، وهو عنوان عام يعرف الإنترنت طريقه.
  4. يصل الرد إلى ال Router على 203.0.113.10:40000، فيبحث في الجدول ويجد الصف، فيغير عنوان الوجهة إلى 192.168.50.10:40000 ويرسله إلى الحاسوب، والحاسوب لا يعرف أن شيئاً قد تغير في الطريق.

وماذا لو فتح الهاتف 192.168.50.11 اتصالاً إلى السيرفر نفسه ومن المنفذ نفسه 40000؟ هنا لا يستطيع ال Router أن يستخدم 203.0.113.10:40000 مرة ثانية، لأنه لن يعرف لمن يعيد الرد، لذلك يختار منفذاً آخر حراً، وهذا ما يسمى ترجمة المنافذ Port Address Translation، واختصاراً PAT، ويسميه RFC 3022 باسم NAPT، فال Router يميز الاتصالات بالمنفذ وليس بالعنوان وحده، وبهذه الطريقة يخرج بيت كامل، بل شركة كاملة، بعنوان عام واحد. والصورة التالية تبين ذلك بالأرقام التي ظهرت في التجربة العملية:

مخطط يوضح حاسوباً وهاتفاً يرسلان من المنفذ 40000، وال Router يغير المصدر إلى 203.0.113.10 مع المنفذ 40000 للحاسوب و18606 للهاتف، ويكتب صفاً لكل اتصال في جدول الترجمة، ثم يعود الرد إلى 203.0.113.10:18606 فيعيده ال Router إلى الهاتف على 192.168.50.11:40000
ترجمة المصدر مع المنافذ: صف لكل اتصال، ومنه يعرف ال Router لمن يعيد الرد

في الصورة أعلاه لاحظ التالي:

  • الجدول هو قلب NAT، فكل صف فيه اتصال واحد، وال Router يملؤه عندما تخرج الحزمة الأولى، ويقرأ منه عندما يعود الرد.
  • الحاسوب والهاتف استخدما المنفذ نفسه 40000، فبقي الحاسوب على 40000 لأنه كان حراً، وأخذ الهاتف منفذاً آخر هو 18606، وهذا ما ظهر فعلاً في مخرج التجربة.
  • المنافذ في العنوان الواحد محدودة، وهذا لا يظهر في بيت أو شركة صغيرة، ولكنه يظهر عند مزود يضع آلاف المشتركين خلف عنوان واحد، لذلك يطلب RFC 6888 من أجهزة CGNAT أن تحدد عدد المنافذ لكل مشترك حتى لا يستهلكها مشترك واحد.

كيف يتذكر ال Router الاتصالات؟ تتبع الاتصالات Connection Tracking

في Linux، وهو النظام الذي يعمل داخل كثير من أجهزة ال Router المنزلية، ومنها كل جهاز يعمل ب OpenWrt، يقوم بهذا العمل جزء في النواة Kernel اسمه تتبع الاتصالات Connection Tracking، واختصاراً conntrack، وهو جزء من إطار netfilter. فكل اتصال جديد يمر على قواعد جدول nat مرة واحدة فقط، عند الحزمة الأولى، فتقرر القاعدة كيف يترجم، ثم تمر بقية الحزم وردودها بالترجمة نفسها من جدول conntrack دون أن تمر على القواعد مرة أخرى. وتستطيع أن ترى هذا الجدول بالأمر conntrack -L من حزمة conntrack-tools، وهذا صف حقيقي من التجربة:

tcp      6 118 TIME_WAIT src=192.168.50.10 dst=203.0.113.80 sport=38350 dport=80 src=203.0.113.80 dst=203.0.113.10 sport=80 dport=38350 [ASSURED] mark=0 use=1

والصف ينقسم إلى نصفين: الأول هو الاتصال كما خرج من الحاسوب، من 192.168.50.10 إلى 203.0.113.80، والثاني هو الرد الذي ينتظره ال Router، من 203.0.113.80 إلى 203.0.113.10، أي إلى العنوان العام، وفي الفرق بين النصفين تظهر الترجمة. والرقم 118 هو عدد الثواني الباقية قبل حذف الصف، فلكل صف مدة صلاحية Timeout، وهذه المدة هي سبب مشكلات كثيرة مع UDP كما سيأتي.

هل NAT جدار حماية؟

من الآثار الجانبية لما سبق أن أي اتصال يبدأ من الإنترنت ويصل إلى ال Router لا يجد صفاً في الجدول، فلا يعرف ال Router إلى أي جهاز داخلي يرسله، فيرفضه، ولذلك يظن كثيرون أن NAT يحميهم. وهذا الظن خاطئ، والسبب أن NAT صمم لتوفير العناوين وليس للحماية، فهو لا يمنع الاتصال بقرار، وإنما لا يعرف أين يرسله فقط. فإذا وصلت إلى ال Router حزمة موجهة مباشرة إلى عنوان خاص في شبكتك، من جهاز يشاركك شبكة المزود مثلاً، ولم يكن في ال Router جدار حماية، فسوف يمررها كأي Router آخر. وهذا ما سوف نثبته في التجربة العملية: جهاز على جهة الإنترنت يضيف مساراً إلى شبكتنا الخاصة عبر ال Router، فيصل إلى السيرفر الداخلي رغم وجود NAT، ثم يتوقف بمجرد أن نضيف قاعدة جدار حماية.

لذلك لا تعتمد على NAT كجدار حماية، وتأكد أن ال Router يرفض الاتصالات الواردة الجديدة Inbound Connections في سلسلة التمرير FORWARD، وهذا هو الإعداد الافتراضي في أجهزة ال Router المنزلية المعروفة، ولكنه ليس الإعداد الافتراضي في Linux عندما تبني Router بنفسك أو تفعل ip_forward على سيرفر، كما سوف ترى في التجربة. وسوف يتكرر هذا الدرس عندما نصل إلى IPv6.

توجيه المنافذ: ترجمة عنوان الوجهة DNAT

عرفنا أن NAT يسمح للأجهزة الداخلية أن تبدأ الاتصال، فماذا لو أردت العكس؟ لنفرض أن لديك سيرفراً في البيت على 192.168.50.20 يشغل موقعاً على المنفذ 80، وتريد أن يصل إليه الناس من الإنترنت. فالزائر لا يعرف إلا عنوانك العام 203.0.113.10، وال Router لا يعرف إلى أي جهاز يرسل الاتصال القادم. والحل أن تكتب له قاعدة ثابتة تقول: كل اتصال يصل إلى 203.0.113.10 على المنفذ 8080 أرسله إلى 192.168.50.20:80. وهذه القاعدة اسمها توجيه المنافذ Port Forwarding، وهي من الداخل ترجمة عنوان الوجهة Destination NAT، واختصاراً DNAT، أي أن ال Router يغير هذه المرة عنوان الوجهة وليس عنوان المصدر:

مخطط يوضح زائراً يتصل ب 203.0.113.10 على المنفذ 8080، وقاعدة DNAT في ال Router تغير الوجهة إلى السيرفر المنزلي 192.168.50.20 على المنفذ 80، بينما يحاول فاحص منافذ الوصول إلى المنفذ 22 فيتوقف عند ال Router لعدم وجود قاعدة، والحاسوب في البيت لا يصل إليه أحد
كل قاعدة توجيه باب مفتوح إلى جهاز واحد، والمنافذ التي لا توجد لها قاعدة لا تصل إلى شيء

في الصورة أعلاه لاحظ التالي:

  • الترجمة تحدث عند وصول الحزمة الأولى، فيكتب ال Router صفاً في الجدول، وعندما يرد السيرفر 192.168.50.20 يعيد ال Router كتابة عنوان المصدر في الرد إلى 203.0.113.10:8080، فيرى الزائر الرد قادماً من العنوان الذي اتصل به.
  • المنفذ الخارجي لا يشترط أن يكون مثل الداخلي، فقد وجهنا 8080 إلى 80.
  • الفاحص Port Scanner الذي يجرب المنافذ 22 و3389 و5432 لا يصل إلى شيء لأنه لا توجد لها قواعد، ولكن المنفذ 8080 أصبح مفتوحاً له كما هو مفتوح للزائر، فكل قاعدة توجيه هي باب مفتوح للجميع وليس للزائر الذي تقصده فقط.
  • السيرفر يجب أن يرسل الرد عبر ال Router نفسه، وهذا يتحقق عندما يكون ال Router هو البوابة الافتراضية للسيرفر، وإلا خرج الرد من طريق آخر دون ترجمة فرفضه الزائر.

أما خطوات توجيه المنافذ في لوحة ال Router المنزلي، وحجز عنوان ثابت للسيرفر عبر حجز DHCP Reservation، والمنافذ التي لا يجب توجيهها أبداً، فقد شرحناها في قسم فتح المنافذ في ال Router بالطريقة الصحيحة، لذلك لن نكررها هنا.

لماذا يعمل من الخارج ولا يعمل من داخل البيت؟ Hairpin NAT

بعد أن توجه المنفذ وتربط نطاقاً مثل home.example.com بعنوانك العام، تفتح النطاق من هاتفك على بيانات الجوال فيعمل، ثم تفتحه من حاسوبك في البيت فلا يعمل، وهذه من أكثر المشكلات التي تربك أصحاب السيرفرات المنزلية. والسبب أن الحاسوب يرسل الاتصال إلى العنوان العام، فيصل إلى ال Router من جهة الشبكة الداخلية وليس من جهة الإنترنت، وهنا يحدث أحد أمرين:

  1. قاعدة التوجيه مكتوبة لواجهة الإنترنت فقط، فلا تنطبق على الحزمة، ويرد ال Router بأن المنفذ مغلق.
  2. القاعدة تنطبق فيغير ال Router الوجهة إلى 192.168.50.20، ولكنه يترك المصدر كما هو، أي 192.168.50.10. فيرى السيرفر أن الطلب جاء من جهاز في شبكته نفسها، فيرسل الرد إليه مباشرة دون أن يمر بال Router، والحاسوب كان ينتظر رداً من 203.0.113.10 فيصله رد من 192.168.50.20 لا يعرفه، فيرفضه ويرسل حزمة RST.

والحل الأول أن يغير ال Router عنوان المصدر أيضاً في هذه الحالة، فيرى السيرفر الطلب قادماً من ال Router نفسه، فيعيد الرد إليه، وال Router يعيده إلى الحاسوب، وهذه الخاصية اسمها Hairpin NAT أو NAT Loopback أو NAT Reflection بحسب الشركة المصنعة، وRFC 4787 يطلب في المتطلب REQ-9 أن يدعمها كل جهاز NAT، ولكن كثيراً من أجهزة ال Router المنزلية لا تدعمها أو تتركها معطلة:

مخطط من جزأين: بدون Hairpin NAT يفتح الحاسوب العنوان العام فيوجه ال Router الطلب إلى السيرفر المنزلي مع بقاء المصدر 192.168.50.10، فيرد السيرفر على الحاسوب مباشرة فيرفض الحاسوب الرد. ومع Hairpin NAT يغير ال Router المصدر إلى 192.168.50.254 فيعود الرد عبره، مع خيار Split DNS الذي يعيد العنوان الخاص للنطاق
بدون Hairpin يعود الرد من طريق لا ينتظره الحاسوب، ومعه يمر الرد عبر ال Router

والحل الثاني، وهو الأنظف في رأينا، هو ال Split DNS، ويسمى أيضاً Split-horizon DNS، أي أن يجيب سيرفر DNS داخل البيت عن home.example.com بالعنوان الخاص 192.168.50.20، ويجيب DNS العام بعنوانك العام، فيصل الحاسوب إلى السيرفر مباشرة دون أن يمر بال Router أصلاً. ويكفي لذلك سجل A محلي في سيرفر DNS داخلي مثل AdGuard Home أو Pi-hole، أو في إعدادات ال Router إذا كان يسمح بذلك، وقد شرحنا سجل A وكيف يجيب سيرفر DNS في شرح DNS وسجلاته. وسوف ترى المشكلة والحل الأول بمخرج حقيقي في التجربة العملية.

CGNAT: عندما يضعك المزود خلف NAT ثان

كل ما سبق يفترض أن ال Router في بيتك يملك عنواناً عاماً، ولكن مع نفاد العناوين أصبح كثير من مزودي الخدمة، وخاصة شبكات الجوال وكثير من اشتراكات الألياف، يضعون عدداً من المشتركين خلف عنوان عام واحد، فيحصل ال Router لديك على عنوان من النطاق 100.64.0.0/10 الذي خصصه RFC 6598 لهذا الغرض، ويقوم جهاز عند المزود بترجمة ثانية إلى العنوان العام المشترك، وهذه التقنية اسمها Carrier-Grade NAT، واختصاراً CGNAT:

مخطط من ثلاث مناطق: البيت فيه Router عنوانه على جهة المزود 100.64.0.2 مع قاعدة توجيه للمنفذ 8080، وشبكة المزود فيها جهاز CGNAT عنوانه العام 203.0.113.20 يشترك فيه البيت مع الجيران، والحركة الخارجة تترجم مرتين، أما الزائر من الإنترنت فيتوقف عند جهاز CGNAT لعدم وجود قاعدة له
ترجمتان في الطريق، والعنوان العام عند المزود يشترك فيه عدد من البيوت

ولاحظ في الصورة أن توجيه المنفذ في ال Router المنزلي ما زال يعمل، ولكن من داخل شبكة المزود فقط، أما الزائر من الإنترنت فيصل إلى العنوان العام 203.0.113.20 الذي يملكه المزود، وجهاز CGNAT لا توجد فيه قاعدة لك، ولن يضعها المزود لأن العنوان نفسه مشترك مع جيرانك، فأي منهم يستحق المنفذ 443؟ لذلك فإن فتح المنافذ خلف CGNAT لا يفيد إطلاقاً.

وتعرف أنك خلف CGNAT بثلاث طرق شرحناها بالأوامر في قسم اكتشاف CGNAT: أن يكون عنوان WAN في صفحة حالة ال Router مختلفاً عن العنوان الذي تراه المواقع، أو أن يكون من النطاق 100.64.0.0/10، أو أن يظهر في traceroute بعد ال Router مباشرة عنوان خاص أو عنوان من هذا النطاق، كما في هذا المخرج من التجربة، حيث الخطوة الأولى هي ال Router المنزلي والثانية هي جهاز CGNAT عند المزود:

traceroute to 203.0.113.80 (203.0.113.80), 30 hops max, 46 byte packets
 1  192.168.50.254  0.008 ms
 2  100.64.0.1  0.005 ms
 3  203.0.113.80  0.012 ms

والحلول المتاحة خلف CGNAT هي أن تطلب عنواناً عاماً من المزود، أو أن تستخدم IPv6، أو أن تتجاوز المشكلة باتصال يبدأ من الداخل، وتفصيلها في الدليل نفسه. أما الاتصال الذي يبدأ من الداخل فهو فكرة الأنفاق العكسية Reverse Tunnels التي شرحناها في نشر خدمة من خلف NAT دون فتح منافذ، وفكرة شبكات Mesh التي سوف نصل إليها بعد قليل.

أنواع NAT: لماذا تهم المكالمات والألعاب وشبكات Mesh؟

المواقع والتطبيقات العادية لا تهتم بنوع NAT، لأن الكلاينت Client يبدأ الاتصال دائماً والسيرفر له عنوان عام. ولكن هناك تطبيقات تريد أن يتصل جهازان ببعضهما مباشرة، وكلاهما خلف NAT، مثل المكالمات الصوتية ومكالمات الفيديو عبر الإنترنت VoIP، والألعاب الجماعية، وWebRTC في المتصفح، وشبكات Mesh مثل Tailscale وHeadscale، وهنا لا يوجد طرف له عنوان عام، فيصبح السؤال: إذا فتح جهازي منفذاً على ال Router باتصال خارج، فمن يستطيع أن يرسل إليه عبر هذا المنفذ؟ والإجابة تختلف من جهاز NAT إلى آخر.

وأشهر تقسيم لهذا الاختلاف هو الأنواع الأربعة التي وضعها RFC 3489 عام 2003، وما زالت تظهر في إعدادات الألعاب وأجهزة الشبكات حتى اليوم. لنفرض أن الحاسوب 192.168.50.10:40000 أرسل حزمة UDP إلى السيرفر A على المنفذ 3478، فأصبح له على ال Router العنوان 203.0.113.10:40000:

  • Full Cone: بعد أن يفتح الحاسوب المنفذ، يستطيع أي جهاز على الإنترنت أن يرسل إلى 203.0.113.10:40000 فتصل حزمته إلى الحاسوب، وهو أسهل الأنواع للاتصال المباشر وأقلها تقييداً.
  • Restricted Cone: يقبل ال Router الحزم من العنوان A فقط، لأنه العنوان الذي راسله الحاسوب، ومن أي منفذ على A.
  • Port-Restricted Cone: يقبل الحزم من A ومن المنفذ 3478 نفسه فقط.
  • Symmetric: كل وجهة جديدة تأخذ منفذاً جديداً على ال Router، فالاتصال إلى A يخرج من 40000، والاتصال إلى B يخرج من منفذ آخر، وبالتالي لا يفيد B أن يعرف المنفذ الذي رآه A، وهذا النوع هو الأصعب.
أربعة أجزاء لأنواع NAT بعد أن يرسل الحاسوب إلى A على المنفذ 3478: في Full Cone يقبل ال Router الحزم من A وأي منفذ ومن B، وفي Restricted Cone من A فقط، وفي Port-Restricted Cone من A:3478 فقط، وفي Symmetric من A:3478 فقط مع منفذ عام مختلف لكل وجهة، ومع كل نوع مصطلحات RFC 4787 مثل EIM وEIF وADF وAPDF وAPDM
من يستطيع أن يرسل إليك بعد أن تفتح اتصالاً، في كل نوع من الأنواع الأربعة

ثم وجد المهندسون أن أجهزة كثيرة لا تنطبق عليها هذه الأنواع بدقة، فقد ذكر RFC 5389 صراحة أن خوارزمية تصنيف الأنواع في STUN القديم كانت خاطئة لأن كثيراً من الأجهزة لا تنتمي إلى نوع واحد منها، لذلك فصل RFC 4787 عام 2007 السلوك إلى سؤالين مستقلين:

  • سلوك الربط Mapping Behavior: هل يبقى المنفذ العام نفسه لكل الوجهات، وهذا هو Endpoint-Independent Mapping واختصاراً EIM، أم يتغير حسب عنوان الوجهة Address-Dependent Mapping، أم حسب العنوان والمنفذ معاً Address and Port-Dependent Mapping واختصاراً APDM؟
  • سلوك التصفية Filtering Behavior: من يسمح له بالإرسال إلى المنفذ العام: أي أحد، وهذا هو Endpoint-Independent Filtering واختصاراً EIF، أم العناوين التي راسلتها فقط Address-Dependent Filtering واختصاراً ADF، أم العنوان والمنفذ اللذان راسلتهما فقط Address and Port-Dependent Filtering واختصاراً APDF؟

وبهذه المصطلحات يصبح Full Cone هو EIM مع EIF، وRestricted Cone هو EIM مع ADF، وPort-Restricted Cone هو EIM مع APDF، وSymmetric هو APDM ومعه غالباً APDF، كما في أعلى كل جزء من الصورة. والأهم من الأسماء أن المعيار يطلب في المتطلب REQ-1 أن يكون الربط من نوع EIM، والسبب أن هذا هو الشرط الذي يجعل الاتصال المباشر ممكناً، فإذا لم يتحقق اضطرت التطبيقات إلى المرور عبر سيرفر وسيط Relay. ويطلب أيضاً في المتطلب REQ-5 ألا يحذف ربط UDP قبل دقيقتين من الخمول، ويوصي بخمس دقائق أو أكثر.

كيف يتصل جهازان خلف NAT ببعضهما؟ STUN وHole Punching وTURN

لنفرض أن الطرف الأول Peer A حاسوب في بيت خلف NAT، والطرف الثاني Peer B هاتف على شبكة جوال خلف NAT آخر، وكل منهما لا يعرف حتى عنوانه العام. والحل الذي تستخدمه أغلب التطبيقات يتكون من أربع خطوات:

  1. اكتشاف العنوان العام بسيرفر STUN: يرسل كل طرف طلباً صغيراً إلى سيرفر STUN على الإنترنت، فيرد السيرفر بالعنوان والمنفذ اللذين رأى الطلب قادماً منهما، أي العنوان بعد الترجمة، وهذا هو بروتوكول Session Traversal Utilities for NAT الذي يعرفه RFC 8489.
  2. تبادل العناوين المرشحة Candidates: يرسل كل طرف عناوينه المحتملة، الداخلية والعامة وعنوان ال Relay، إلى الطرف الآخر عبر قناة تنسيق Signaling، مثل سيرفر التطبيق أو سيرفر التنسيق في شبكات Mesh، ثم يجرب الطرفان أزواج العناوين بالترتيب، وهذا الإجراء كله اسمه Interactive Connectivity Establishment، واختصاراً ICE، في RFC 8445.
  3. Hole Punching: يرسل الطرفان في الوقت نفسه حزماً إلى العنوان العام للطرف الآخر، فالحزمة الخارجة من A تفتح صفاً في NAT A يسمح بالرد من B، والحزمة الخارجة من B تفتح صفاً مثله في NAT B، فإذا وصلت الحزم بعد أن انفتح الصفان في الجهتين أصبح بين الجهازين اتصال مباشر، وهذه الطريقة يشرحها RFC 5128 بالتفصيل.
  4. ال Relay عبر TURN عند الفشل: إذا كان أحد الطرفين خلف NAT من نوع Symmetric، فالمنفذ الذي رآه سيرفر STUN ليس هو المنفذ الذي سوف يستخدمه مع الطرف الآخر، فيفشل Hole Punching، وهنا تمر الحركة Traffic عبر سيرفر وسيط له عنوان عام بحسب بروتوكول Traversal Using Relays around NAT، أي TURN، في RFC 8656، وأشهر سيرفر مفتوح المصدر له هو coturn. وهذا الطريق ينجح في أغلب الحالات، ولكنه أبطأ ويستهلك Bandwidth السيرفر الوسيط.
مخطط لطرفين خلف NAT: كل طرف يسأل سيرفر STUN فيعرف عنوانه العام، ثم يتبادلان العناوين عبر قناة التنسيق، ثم يرسلان في الوقت نفسه فيفتح كل NAT صفاً وينشأ اتصال UDP مباشر بينهما، وإذا كان أحد NAT من نوع Symmetric تمر الحركة عبر سيرفر TURN في الأسفل
STUN يعرفك بعنوانك، وHole Punching يفتح الطريق المباشر، وTURN هو الطريق البديل

وهذه هي الآلية نفسها التي تعمل بها شبكات Mesh، فكما ذكرنا في قسم شبكات Mesh في شرح VPN، تحاول Tailscale وHeadscale فتح نفق WireGuard مباشر بين الجهازين، فإذا فشلت مرت الحركة عبر سيرفرات DERP، وهي سيرفرات Relay تبقى الحركة عبرها مشفرة بين الجهازين، وشرح Tailscale لعبور NAT من أفضل ما كتب في هذا الموضوع، ويمكنك تشغيل DERP بنفسك مع Headscale كما في توثيقه. أما مع WireGuard العادي فأحد الطرفين له عنوان عام عادة، والطرف الآخر يحتاج فقط أن يبقي صفه في جدول NAT حياً، وهو ما يفعله الإعداد PersistentKeepalive = 25 الذي شرحناه في قسم العبور من خلف NAT، والرقم 25 ثانية أقل من مدة الحذف في أغلب أجهزة NAT، ففي Linux مثلاً يحذف صف UDP الذي لم يأت له رد بعد 30 ثانية فقط، كما سوف ترى في التجربة.

NAT داخل سيرفرك: ماذا يكتب Docker في جدول nat؟

NAT ليس خاصاً بال Router في البيت، فكل سيرفر يشغل Docker هو Router صغير، فال Containers على شبكة bridge تأخذ عناوين خاصة مثل 172.18.0.2، وتخرج إلى الإنترنت بعنوان السيرفر عبر قاعدة MASQUERADE، وكل منفذ تنشره بالخيار -p أو ports: في Compose هو قاعدة DNAT من عنوان السيرفر إلى ال Container، كما يذكر توثيق نشر المنافذ وجدار الحماية في Docker:

مخطط لسيرفر عنوانه 203.0.113.10 فيه شبكة bridge عليها Container التطبيق 172.18.0.2 وقاعدة البيانات، زائر يتصل بالمنفذ 8080 فتغير قاعدة DNAT في PREROUTING الوجهة إلى 172.18.0.2:80 قبل قواعد ufw، والتطبيق يتصل بواجهة API خارجية فتغير قاعدة MASQUERADE في POSTROUTING المصدر إلى عنوان السيرفر
Docker على السيرفر: DNAT للمنافذ المنشورة، وMASQUERADE لخروج ال Containers

ولكي ترى ذلك على سيرفرك، ننشئ شبكة وContainer ينشر منفذه على 127.0.0.1 فقط، ثم نعرض ما كتبه Docker في جدول nat:

docker network create --subnet 10.250.50.0/24 lab-nat-app
docker run -d --name lab-nat-web --network lab-nat-app -p 127.0.0.1:18590:80 traefik/whoami:v1.12.0
sudo iptables -t nat -S | grep -E '18590|10.250.50'
sudo iptables -t nat -S PREROUTING

والمخرج سوف يكون كما يلي:

-A POSTROUTING -s 10.250.50.0/24 ! -o br-8a9e00521052 -j MASQUERADE
-A POSTROUTING -s 10.250.50.2/32 -d 10.250.50.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -d 127.0.0.1/32 -p tcp -m tcp --dport 18590 -j DNAT --to-destination 10.250.50.2:80
-P PREROUTING ACCEPT
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER

في الإعداد أعلاه لاحظ التالي:

  • كل حزمة تصل إلى عنوان من عناوين السيرفر نفسه تذهب إلى سلسلة DOCKER، وهناك قاعدة DNAT لكل منفذ منشور، فالحزمة إلى 127.0.0.1:18590 تصبح موجهة إلى 10.250.50.2:80.
  • القاعدة الأولى في POSTROUTING هي MASQUERADE لكل ما يخرج من شبكة 10.250.50.0/24 إلى أي واجهة غير جسر الشبكة نفسها، أي أن ال Containers تخرج بعنوان السيرفر، كما يخرج حاسوبك بعنوان ال Router.
  • القاعدة الثانية هي Hairpin NAT نفسه الذي شرحناه، ولكن لحالة ال Container الذي يتصل بنفسه عبر المنفذ المنشور.
  • ترجمة الوجهة تحدث في PREROUTING قبل أن تقرر النواة هل الحزمة للسيرفر نفسه أم للتمرير، لذلك لا تمر الحزمة على قواعد ufw التي تحمي السيرفر في سلسلة INPUT، وهذه هي قصة تجاوز Docker لجدار ufw التي شرحناها بالتفصيل في شبكات Docker وأفضل الممارسات، ومعها طريقة النشر الآمن للمنافذ.

وبعد أن تنتهي احذف ما أنشأناه:

docker rm -f lab-nat-web
docker network rm lab-nat-app

IPv6: هل ما زلنا نحتاج NAT؟

عناوين IPv6 طولها 128 بت، والمزود يعطي البيت عادة شبكة كاملة بطول /64 أو أكبر، والشبكة /64 وحدها فيها 2 أس 64 عنواناً، أي أكثر من 18 مليار مليار عنوان لبيت واحد، لذلك لا توجد مشكلة نقص تحتاج إلى NAT، ويحصل كل جهاز على عنوان عام خاص به، ويعود الإنترنت إلى فكرته الأولى: أي جهاز يستطيع أن يتصل بأي جهاز.

وهنا يعود الدرس الذي ذكرناه في قسم هل NAT جدار حماية؟، فكثيرون اعتمدوا على الأثر الجانبي ل NAT دون أن يعرفوا، فإذا فعلت IPv6 أصبح كل جهاز في البيت قابلاً للوصول من الإنترنت من حيث المبدأ، والذي يحميه عندها هو جدار الحماية في ال Router وحده. ويوصي RFC 6092 بأن ترفض أجهزة ال Router المنزلية في إعدادها الافتراضي الاتصالات الواردة التي لم يطلبها جهاز داخلي، ويشرح RFC 4864 كيف تحصل على كل ما كنت تظنه من حماية NAT دون NAT. لذلك عندما تفعل IPv6 على سيرفر أو Router، تأكد أن جدار الحماية يعمل على IPv6 كما يعمل على IPv4، وافتح المنافذ التي تريدها فقط.

وقد يتساءل البعض: هل يوجد NAT في IPv6 أصلاً؟ والإجابة نعم، فهناك NAT66 الذي يعمل مثل NAT في IPv4 ولا يوصي به أحد للاستخدام العادي، وهناك ترجمة البادئة Network Prefix Translation، أي NPTv6 في RFC 6296، وهو معيار تجريبي Experimental يبدل بادئة الشبكة كلها بأخرى، عنواناً مقابل عنوان، دون منافذ ودون جدول اتصالات، ويستخدم في حالات خاصة مثل شبكة لها مزودان وتريد أن تبقى عناوينها الداخلية ثابتة. وغالباً لن تحتاج أياً منهما في البيت أو على السيرفر.

تجربة عملية: نبني Router بأنفسنا ونرى الترجمة من الداخل

الكلام عن الجداول والحزم يصبح أوضح عندما تراه في مخرج حقيقي، لذلك سوف نبني الآن على جهازك شبكة منزلية صغيرة من Containers: «Router» له واجهتان، واحدة على الشبكة الداخلية 192.168.50.0/24 وواحدة على شبكة تمثل الإنترنت، و«حاسوب» و«هاتف» و«سيرفر منزلي» في الشبكة الداخلية، و«سيرفر على الإنترنت» في الجهة الأخرى. ولاحظ أن عناوين «الإنترنت» هنا من النطاق 203.0.113.0/24 المخصص للتوثيق في RFC 5737، فلا تخرج حزمة واحدة من جهازك إلى الإنترنت الحقيقي، وأن كل الأسماء تبدأ ب lab-nat- حتى تحذفها بأمر واحد في النهاية.

بناء الشبكة

نبدأ بإنشاء الشبكتين، ونعطل فيهما الخيار enable_ip_masquerade حتى لا يضيف Docker ترجمة من عنده، فتكون الترجمة الوحيدة في التجربة هي التي نكتبها نحن:

docker network create --subnet 192.168.50.0/24 -o com.docker.network.bridge.enable_ip_masquerade=false lab-nat-lan
docker network create --subnet 203.0.113.0/24 -o com.docker.network.bridge.enable_ip_masquerade=false lab-nat-wan

وإذا ظهر لك الخطأ Pool overlaps with other one on this address space فهناك شبكة Docker أخرى على جهازك تستخدم جزءاً من النطاق نفسه، فاختر نطاقاً آخر مثل 192.168.150.0/24 وغير العناوين في بقية الأوامر بحسبه.

ثم ننشئ ال Containers على شبكة Docker الافتراضية حتى نثبت فيها الأدوات، ثم نفصلها عنها، ولاحظ أن ال Router وحده يأخذ الإعداد ip_forward=1 الذي يسمح له بتمرير الحزم بين الشبكتين، وأن الصلاحية NET_ADMIN تسمح لكل Container بتعديل المسارات والقواعد داخله فقط:

docker run -d --name lab-nat-router --hostname router --cap-add NET_ADMIN --cap-add NET_RAW --sysctl net.ipv4.ip_forward=1 alpine:3.24 sleep 7200
for c in laptop phone homeserver internet; do
  docker run -d --name lab-nat-$c --hostname $c --cap-add NET_ADMIN --cap-add NET_RAW alpine:3.24 sleep 7200
done
for c in router laptop phone homeserver internet; do
  docker exec lab-nat-$c apk add -q iptables nftables conntrack-tools tcpdump curl iproute2 busybox-extras
  docker network disconnect bridge lab-nat-$c
done

الآن نصل كل Container بشبكته وبعنوان ثابت، فال Router على الشبكتين، والحاسوب والهاتف والسيرفر المنزلي في الداخل، وسيرفر الإنترنت في الخارج:

docker network connect --ip 192.168.50.254 lab-nat-lan lab-nat-router
docker network connect --ip 203.0.113.10 lab-nat-wan lab-nat-router
docker network connect --ip 192.168.50.10 lab-nat-lan lab-nat-laptop
docker network connect --ip 192.168.50.11 lab-nat-lan lab-nat-phone
docker network connect --ip 192.168.50.20 lab-nat-lan lab-nat-homeserver
docker network connect --ip 203.0.113.80 lab-nat-wan lab-nat-internet

وكما يعطي ال Router في البيت أجهزتك البوابة الافتراضية عبر DHCP، نجعل ال Router بوابة الأجهزة الداخلية، ونحذف البوابة الافتراضية من ال Router ومن سيرفر الإنترنت، فسيرفر الإنترنت مثل أي سيرفر حقيقي لا يعرف طريقاً إلى الشبكات الخاصة:

for c in laptop phone homeserver; do docker exec lab-nat-$c ip route replace default via 192.168.50.254; done
docker exec lab-nat-router ip route del default
docker exec lab-nat-internet ip route del default
docker exec lab-nat-router ip -br -4 addr show
docker exec lab-nat-internet ip route

والمخرج سوف يكون كما يلي:

lo               UNKNOWN        127.0.0.1/8
eth0@if2973      UP             192.168.50.254/24
eth1@if2974      UP             203.0.113.10/24
203.0.113.0/24 dev eth0 proto kernel scope link src 203.0.113.80 

لاحظ أن الواجهة eth0 في ال Router هي الداخلية LAN، وeth1 هي واجهة الإنترنت WAN، وسوف نستخدم هذين الاسمين في القواعد. وأخيراً نشغل موقعاً بسيطاً بأداة httpd من BusyBox على السيرفر المنزلي، وآخر على سيرفر الإنترنت، ومعه صفحة صغيرة /cgi-bin/ip تطبع العنوان الذي يرى الطلب قادماً منه، مثل المواقع التي تعرض لك عنوانك العام:

docker exec lab-nat-homeserver sh -c 'mkdir -p /www && echo "hello from the home server 192.168.50.20" > /www/index.html && httpd -p 80 -h /www'
docker exec lab-nat-internet sh -c 'mkdir -p /www/cgi-bin && echo "hello from 203.0.113.80" > /www/index.html'
docker exec -i lab-nat-internet sh -c 'cat > /www/cgi-bin/ip' <<'EOF'
#!/bin/sh
a=${REMOTE_ADDR#*ffff:}
echo "Content-Type: text/plain"
echo
echo "you are ${a%]}"
EOF
docker exec lab-nat-internet sh -c 'chmod +x /www/cgi-bin/ip && httpd -p 80 -h /www'

بدون NAT: الحزمة تصل ولا يعود الرد

ال Router الآن يمرر الحزم بين الشبكتين، ولكن لا توجد فيه أي قاعدة ترجمة، وهذا هو الحل البسيط: أن نوجه الحزم كما هي. لنراقب ما يصل إلى سيرفر الإنترنت في نافذة، ونفتح الموقع من الحاسوب في نافذة أخرى:

docker exec lab-nat-internet tcpdump -ni eth0 -c 2 tcp port 80
docker exec lab-nat-laptop curl -sS -m 4 http://203.0.113.80/

والمخرج سوف يكون كما يلي، أولاً من curl ثم من tcpdump:

curl: (28) Connection timed out after 4001 milliseconds
19:45:14.103846 IP 192.168.50.10.33416 > 203.0.113.80.80: Flags [S], seq 3836795149, win 64240, options [mss 1460,sackOK,TS val 3748284274 ecr 0,nop,wscale 7], length 0
19:45:15.158400 IP 192.168.50.10.33416 > 203.0.113.80.80: Flags [S], seq 3836795149, win 64240, options [mss 1460,sackOK,TS val 3748285329 ecr 0,nop,wscale 7], length 0

حزمة بدء الاتصال SYN، وهي التي عليها Flags [S]، وصلت إلى السيرفر فعلاً، ولكن مصدرها 192.168.50.10، والسيرفر لم يرد، ثم أعاد الحاسوب إرسالها بعد ثانية. والسبب يظهر عندما نسأل السيرفر عن طريقه إلى هذا العنوان:

docker exec lab-nat-internet ip route get 192.168.50.10
RTNETLINK answers: Network unreachable

وهذا بالضبط ما يحدث على الإنترنت الحقيقي، فلا أحد يعرف طريقاً إلى عنوانك الخاص.

تفعيل الترجمة بقاعدة MASQUERADE

نضيف الآن إلى ال Router قاعدة ترجمة المصدر في سلسلة POSTROUTING من جدول nat، أي بعد أن يقرر ال Router إلى أي واجهة تخرج الحزمة، والقاعدة تقول: كل ما يخرج من الشبكة الداخلية عبر واجهة الإنترنت eth1 غير مصدره إلى عنوان هذه الواجهة:

docker exec lab-nat-router iptables -t nat -A POSTROUTING -s 192.168.50.0/24 -o eth1 -j MASQUERADE
docker exec lab-nat-router iptables -t nat -S POSTROUTING

والمخرج سوف يكون كما يلي:

-P POSTROUTING ACCEPT
-A POSTROUTING -d 127.0.0.11/32 -j DOCKER_POSTROUTING
-A POSTROUTING -s 192.168.50.0/24 -o eth1 -j MASQUERADE

والسطر الذي فيه 127.0.0.11 يخص سيرفر DNS الداخلي الذي يضيفه Docker في كل Container، فلا علاقة له بتجربتنا. وقد يتساءل البعض: ما الفرق بين MASQUERADE وSNAT؟ والإجابة أن توثيق iptables يعرف SNAT بعنوان ثابت تكتبه أنت --to-source 203.0.113.10، وهو المناسب عندما يكون عنوانك العام ثابتاً، أما MASQUERADE فيأخذ عنوان الواجهة وقت خروج الحزمة، ويحذف صفوف الاتصالات إذا سقطت الواجهة، لذلك هو المناسب للعنوان المتغير الذي يعطيه المزود، وهو ما تستخدمه أجهزة ال Router المنزلية وDocker.

لنراقب الآن الواجهتين في ال Router معاً، كل واحدة في نافذة، ثم نفتح صفحة العنوان من الحاسوب:

docker exec lab-nat-router tcpdump -ni eth0 -c 2 tcp port 80
docker exec lab-nat-router tcpdump -ni eth1 -c 2 tcp port 80
docker exec lab-nat-laptop curl -sS -m 4 http://203.0.113.80/cgi-bin/ip

والمخرج سوف يكون كما يلي، ف curl يطبع العنوان الذي رآه السيرفر، ثم الحزمتان الأوليان على الواجهة الداخلية eth0، ثم على واجهة الإنترنت eth1:

you are 203.0.113.10
19:45:23.453205 IP 192.168.50.10.38350 > 203.0.113.80.80: Flags [S], seq 2438291816, win 64240, options [mss 1460,sackOK,TS val 3748293623 ecr 0,nop,wscale 7], length 0
19:45:23.453260 IP 203.0.113.80.80 > 192.168.50.10.38350: Flags [S.], seq 648471657, ack 2438291817, win 65160, options [mss 1460,sackOK,TS val 654366973 ecr 3748293623,nop,wscale 7], length 0
19:45:23.453223 IP 203.0.113.10.38350 > 203.0.113.80.80: Flags [S], seq 2438291816, win 64240, options [mss 1460,sackOK,TS val 3748293623 ecr 0,nop,wscale 7], length 0
19:45:23.453257 IP 203.0.113.80.80 > 203.0.113.10.38350: Flags [S.], seq 648471657, ack 2438291817, win 65160, options [mss 1460,sackOK,TS val 654366973 ecr 3748293623,nop,wscale 7], length 0

في المخرج أعلاه لاحظ التالي:

  • الحزمة نفسها، بالرقم التسلسلي نفسه seq 2438291816، تظهر على الواجهة الداخلية بمصدر 192.168.50.10، وعلى واجهة الإنترنت بمصدر 203.0.113.10، أي أن ال Router غير العنوان فقط وترك الباقي كما هو.
  • المنفذ 38350 بقي كما هو على الجهتين، لأنه لم يكن مستخدماً على العنوان العام.
  • الرد Flags [S.] وصل إلى 203.0.113.10، ثم خرج على الواجهة الداخلية موجهاً إلى 192.168.50.10، وهذه هي الترجمة العكسية من جدول conntrack.
  • السيرفر يقول إنك 203.0.113.10، تماماً كما تقول لك المواقع عن عنوان بيتك.

والصف الذي كتبه ال Router لهذا الاتصال هو الصف الذي رأيناه في قسم تتبع الاتصالات:

docker exec lab-nat-router conntrack -L -p tcp
tcp      6 118 TIME_WAIT src=192.168.50.10 dst=203.0.113.80 sport=38350 dport=80 src=203.0.113.80 dst=203.0.113.10 sport=80 dport=38350 [ASSURED] mark=0 use=1
conntrack v1.4.9 (conntrack-tools): 1 flow entries have been shown.

جهازان من المنفذ نفسه

الآن نجعل الحاسوب والهاتف يتصلان بالسيرفر نفسه ومن منفذ المصدر نفسه 40000، بالخيار --local-port في curl، ونرى ماذا يفعل ال Router:

docker exec lab-nat-laptop curl -sS -m 4 --local-port 40000 http://203.0.113.80/cgi-bin/ip
docker exec lab-nat-phone curl -sS -m 4 --local-port 40000 http://203.0.113.80/cgi-bin/ip
docker exec lab-nat-router conntrack -L -p tcp --dport 80

والمخرج سوف يكون كما يلي:

you are 203.0.113.10
you are 203.0.113.10
tcp      6 119 TIME_WAIT src=192.168.50.10 dst=203.0.113.80 sport=40000 dport=80 src=203.0.113.80 dst=203.0.113.10 sport=80 dport=40000 [ASSURED] mark=0 use=1
tcp      6 119 TIME_WAIT src=192.168.50.11 dst=203.0.113.80 sport=40000 dport=80 src=203.0.113.80 dst=203.0.113.10 sport=80 dport=18606 [ASSURED] mark=0 use=1
conntrack v1.4.9 (conntrack-tools): 2 flow entries have been shown.

الجهازان ظهرا للسيرفر بالعنوان نفسه، ولكن في الصف الثاني لاحظ dport=18606 في نصف الرد، أي أن ال Router أبقى الحاسوب على المنفذ 40000 وأعطى الهاتف المنفذ 18606، لأن 203.0.113.10:40000 كان محجوزاً لاتصال الحاسوب، وهذا هو PAT الذي رسمناه في الصورة الثانية.

UDP ومدة بقاء الصف

في TCP يعرف ال Router متى ينتهي الاتصال من حزم الإغلاق، أما UDP فلا توجد فيه بداية ولا نهاية، لذلك يعتمد ال Router على مدة الخمول فقط. نرسل حزمة UDP واحدة من الحاسوب إلى منفذ لا يرد عليه أحد، ثم نعرض الصف وإعدادات المدة في نواة ال Router:

docker exec lab-nat-laptop sh -c 'echo hello | nc -u -w1 203.0.113.80 5000'
docker exec lab-nat-router conntrack -L -p udp
docker exec lab-nat-router sysctl net.netfilter.nf_conntrack_udp_timeout net.netfilter.nf_conntrack_udp_timeout_stream net.netfilter.nf_conntrack_tcp_timeout_established

والمخرج سوف يكون كما يلي:

udp      17 29 src=192.168.50.10 dst=203.0.113.80 sport=48449 dport=5000 [UNREPLIED] src=203.0.113.80 dst=203.0.113.10 sport=5000 dport=48449 mark=0 use=1
conntrack v1.4.9 (conntrack-tools): 1 flow entries have been shown.
net.netfilter.nf_conntrack_udp_timeout = 30
net.netfilter.nf_conntrack_udp_timeout_stream = 120
net.netfilter.nf_conntrack_tcp_timeout_established = 432000

في المخرج أعلاه لاحظ التالي:

  • الصف عليه [UNREPLIED] لأن الرد لم يأت، وبقي له 29 ثانية من أصل 30، فإذا لم يمر فيه شيء خلالها حذفه ال Router، وأي رد يصل بعد ذلك لا يجد صفاً فيرفض.
  • عندما يمر الرد في الاتجاهين تصبح المدة udp_timeout_stream أي 120 ثانية، وهو الحد الأدنى الذي يطلبه RFC 4787، أما اتصال TCP القائم فيبقى 432000 ثانية، أي خمسة أيام، كما في توثيق النواة.
  • أجهزة ال Router المنزلية وشبكات الجوال تستخدم مدداً مختلفة، وبعضها أقصر من ذلك بكثير، وهذا سبب انقطاع اتصالات UDP الطويلة الهادئة، وسبب وجود Keepalive في WireGuard وغيره.

NAT ليس جدار حماية: الدليل

لنفرض أن جهازاً يشاركك شبكة المزود، وهو هنا سيرفر الإنترنت نفسه، يعرف أن خلف 203.0.113.10 شبكة 192.168.50.0/24، فيضيف مساراً إليها عبر ال Router ويحاول أن يفتح السيرفر المنزلي مباشرة:

docker exec lab-nat-internet ip route add 192.168.50.0/24 via 203.0.113.10
docker exec lab-nat-internet curl -sS -m 4 http://192.168.50.20/
hello from the home server 192.168.50.20

وصل إلى السيرفر المنزلي رغم وجود NAT، والسبب أن ال Router في Linux يمرر كل شيء ما دامت سلسلة FORWARD تقبل افتراضياً، وقاعدة MASQUERADE لا علاقة لها بالاتصالات القادمة من الخارج. والحل هو جدار حماية حقيقي يرفض كل تمرير، ثم يسمح بالاتصالات التي بدأت من الداخل وبردودها فقط:

docker exec lab-nat-router iptables -P FORWARD DROP
docker exec lab-nat-router iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
docker exec lab-nat-router iptables -A FORWARD -i eth0 -o eth1 -j ACCEPT
docker exec lab-nat-internet curl -sS -m 4 http://192.168.50.20/
docker exec lab-nat-laptop curl -sS -m 4 http://203.0.113.80/

والمخرج سوف يكون كما يلي:

curl: (28) Connection timed out after 4001 milliseconds
hello from 203.0.113.80

الاتصال من الخارج توقف، والحاسوب ما زال يتصفح كما كان، وهذا هو السلوك الذي تحصل عليه في ال Router المنزلي، ولكنه يأتي من قاعدة FORWARD وليس من NAT. الآن نحذف المسار من سيرفر الإنترنت حتى يعود كما كان:

docker exec lab-nat-internet ip route del 192.168.50.0/24 via 203.0.113.10

توجيه منفذ إلى السيرفر المنزلي

نجرب أولاً أن نصل من الإنترنت إلى المنفذ 8080 على العنوان العام قبل أن نضيف أي قاعدة:

docker exec lab-nat-internet curl -sS -m 4 http://203.0.113.10:8080/
curl: (7) Failed to connect to 203.0.113.10:8080 after 0 ms: Could not connect to server

رفض فوري بعد 0 ms، لأن الاتصال وصل إلى ال Router نفسه ولا يوجد عليه شيء يستمع على 8080، فرد بحزمة RST. الآن نضيف قاعدة DNAT في سلسلة PREROUTING، أي قبل أن يقرر ال Router مسار الحزمة، ومعها قاعدة في FORWARD تسمح بمرور الاتصالات التي ترجمت وجهتها، والسبب أننا جعلنا سياسة FORWARD الرفض في الخطوة السابقة:

docker exec lab-nat-router iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 8080 -j DNAT --to-destination 192.168.50.20:80
docker exec lab-nat-router iptables -A FORWARD -d 192.168.50.20 -p tcp --dport 80 -m conntrack --ctstate DNAT -j ACCEPT

ثم نراقب الواجهتين ونتصل من الإنترنت مرة أخرى:

docker exec lab-nat-router tcpdump -ni eth1 -c 2 tcp
docker exec lab-nat-router tcpdump -ni eth0 -c 2 tcp
docker exec lab-nat-internet curl -sS -m 4 http://203.0.113.10:8080/

والمخرج سوف يكون كما يلي، الرد من السيرفر المنزلي، ثم الحزم على واجهة الإنترنت، ثم على الواجهة الداخلية:

hello from the home server 192.168.50.20
19:45:37.166061 IP 203.0.113.80.34016 > 203.0.113.10.8080: Flags [S], seq 2263824646, win 64240, options [mss 1460,sackOK,TS val 654380688 ecr 0,nop,wscale 7], length 0
19:45:37.166114 IP 203.0.113.10.8080 > 203.0.113.80.34016: Flags [S.], seq 3358748686, ack 2263824647, win 65160, options [mss 1460,sackOK,TS val 1808768304 ecr 654380688,nop,wscale 7], length 0
19:45:37.166076 IP 203.0.113.80.34016 > 192.168.50.20.80: Flags [S], seq 2263824646, win 64240, options [mss 1460,sackOK,TS val 654380688 ecr 0,nop,wscale 7], length 0
19:45:37.166110 IP 192.168.50.20.80 > 203.0.113.80.34016: Flags [S.], seq 3358748686, ack 2263824647, win 65160, options [mss 1460,sackOK,TS val 1808768304 ecr 654380688,nop,wscale 7], length 0

هذه المرة تغيرت الوجهة وبقي المصدر، فالحزمة دخلت إلى 203.0.113.10.8080 وخرجت إلى الداخل نحو 192.168.50.20.80، والسيرفر المنزلي يرى عنوان الزائر الحقيقي 203.0.113.80، والرد عاد إلى الزائر من 203.0.113.10.8080 كما ينتظره. وصف conntrack يبين الترجمة في الاتجاهين:

docker exec lab-nat-router conntrack -L -p tcp
docker exec lab-nat-router iptables -t nat -L PREROUTING -n -v
conntrack v1.4.9 (conntrack-tools): 1 flow entries have been shown.
tcp      6 117 TIME_WAIT src=203.0.113.80 dst=203.0.113.10 sport=34016 dport=8080 src=192.168.50.20 dst=203.0.113.80 sport=80 dport=34016 [ASSURED] mark=0 use=1
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    1    60 DNAT       tcp  --  eth1   *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:192.168.50.20:80

ولاحظ العداد pkts بجانب القاعدة، فهو يزيد مع كل اتصال جديد ينطبق عليه التوجيه، وهو أهم أداة للتشخيص كما سوف ترى الآن.

ماذا لو تغير عنوان السيرفر الداخلي؟

هذا ما يحدث عندما يعطي DHCP السيرفر عنواناً جديداً بعد إعادة التشغيل، وقاعدة التوجيه ما زالت تشير إلى العنوان القديم. نحاكيه بتغيير القاعدة إلى عنوان لا يوجد عليه جهاز، والخيار -R يستبدل القاعدة رقم 1:

docker exec lab-nat-router iptables -t nat -R PREROUTING 1 -i eth1 -p tcp --dport 8080 -j DNAT --to-destination 192.168.50.30:80
docker exec lab-nat-internet curl -sS -m 4 http://203.0.113.10:8080/
docker exec lab-nat-router iptables -t nat -L PREROUTING -n -v

والمخرج سوف يكون كما يلي:

curl: (28) Connection timed out after 4001 milliseconds
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    3   180 DNAT       tcp  --  eth1   *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:192.168.50.30:80

لاحظ الفرق بين الخطأين: قبل القاعدة كان الرفض فورياً Could not connect، لأن ال Router رد بنفسه، أما الآن فالخطأ انتهاء المهلة timed out، لأن ال Router وجه الحزمة إلى عنوان لا يرد أحد عليه. والعداد ارتفع من 1 إلى 3، أي أن القاعدة تعمل وتلتقط كل محاولة، وهي حزمة SYN الأولى ومحاولتا إعادة الإرسال، وبالتالي فالمشكلة خلف ال Router وليست فيه. نعيد القاعدة إلى العنوان الصحيح:

docker exec lab-nat-router iptables -t nat -R PREROUTING 1 -i eth1 -p tcp --dport 8080 -j DNAT --to-destination 192.168.50.20:80

Hairpin NAT من داخل الشبكة

الآن نفتح العنوان العام من الحاسوب داخل الشبكة، كما يفعل من يفتح نطاقه من البيت:

docker exec lab-nat-laptop curl -sS -m 4 http://203.0.113.10:8080/
curl: (7) Failed to connect to 203.0.113.10:8080 after 0 ms: Could not connect to server

هذه هي الحالة الأولى التي ذكرناها، فالقاعدة مكتوبة لواجهة الإنترنت -i eth1، والحزمة دخلت من eth0، فلم تنطبق. والحل الذي يبدو منطقياً أن نكتب القاعدة بالعنوان العام -d 203.0.113.10 بدل الواجهة، حتى تنطبق من الجهتين، ثم نراقب السيرفر المنزلي:

docker exec lab-nat-router iptables -t nat -R PREROUTING 1 -d 203.0.113.10 -p tcp --dport 8080 -j DNAT --to-destination 192.168.50.20:80
docker exec lab-nat-homeserver tcpdump -ni eth0 -c 3 tcp port 80
docker exec lab-nat-laptop curl -sS -m 4 http://203.0.113.10:8080/

والمخرج سوف يكون كما يلي:

curl: (28) Connection timed out after 4001 milliseconds
19:45:46.928280 IP 192.168.50.10.50812 > 192.168.50.20.80: Flags [S], seq 2384381775, win 64240, options [mss 1460,sackOK,TS val 2544389776 ecr 0,nop,wscale 7], length 0
19:45:46.928333 IP 192.168.50.20.80 > 192.168.50.10.50812: Flags [S.], seq 2461356977, ack 2384381776, win 65160, options [mss 1460,sackOK,TS val 2973419956 ecr 2544389776,nop,wscale 7], length 0
19:45:46.928354 IP 192.168.50.10.50812 > 192.168.50.20.80: Flags [R], seq 2384381776, win 0, length 0

وهذه هي الحالة الثانية تماماً: الطلب وصل إلى السيرفر المنزلي ومصدره 192.168.50.10، فرد السيرفر على الحاسوب مباشرة، والحاسوب الذي كان ينتظر رداً من 203.0.113.10:8080 رفض الرد بحزمة Flags [R]. والحل أن يغير ال Router المصدر أيضاً للاتصالات التي تأتي من الداخل وتترجم وجهتها إلى السيرفر المنزلي:

docker exec lab-nat-router iptables -t nat -A POSTROUTING -s 192.168.50.0/24 -d 192.168.50.20 -p tcp --dport 80 -m conntrack --ctstate DNAT -j MASQUERADE
docker exec lab-nat-homeserver tcpdump -ni eth0 -c 2 tcp port 80
docker exec lab-nat-laptop curl -sS -m 4 http://203.0.113.10:8080/
docker exec lab-nat-internet curl -sS -m 4 http://203.0.113.10:8080/

والمخرج سوف يكون كما يلي:

hello from the home server 192.168.50.20
19:45:56.130040 IP 192.168.50.254.48714 > 192.168.50.20.80: Flags [S], seq 885456275, win 64240, options [mss 1460,sackOK,TS val 2544398978 ecr 0,nop,wscale 7], length 0
19:45:56.130054 IP 192.168.50.20.80 > 192.168.50.254.48714: Flags [S.], seq 1070413328, ack 885456276, win 65160, options [mss 1460,sackOK,TS val 1122125537 ecr 2544398978,nop,wscale 7], length 0
hello from the home server 192.168.50.20

السيرفر المنزلي يرى الطلب الآن من ال Router 192.168.50.254، فيعيد الرد إليه، وال Router يعيده إلى الحاسوب، والزائر من الخارج ما زال يصل كما كان. والثمن أن السيرفر لا يرى عنوان الحاسوب الحقيقي في هذه الحالة، وهذا من أسباب تفضيلنا لل Split DNS عندما يكون ممكناً. والقواعد كلها في ال Router الآن كما يلي:

docker exec lab-nat-router iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER_OUTPUT
-N DOCKER_POSTROUTING
-A PREROUTING -d 203.0.113.10/32 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 192.168.50.20:80
-A OUTPUT -d 127.0.0.11/32 -j DOCKER_OUTPUT
-A POSTROUTING -d 127.0.0.11/32 -j DOCKER_POSTROUTING
-A POSTROUTING -s 192.168.50.0/24 -o eth1 -j MASQUERADE
-A POSTROUTING -s 192.168.50.0/24 -d 192.168.50.20/32 -p tcp -m tcp --dport 80 -m conntrack --ctstate DNAT -j MASQUERADE
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:44099
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:38624
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -m tcp --sport 44099 -j SNAT --to-source :53
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 38624 -j SNAT --to-source :53

ولاحظ أن قاعدتنا الأولى والثانية والثالثة هي كل ما يحتاجه Router منزلي ليعمل: MASQUERADE للخروج، وDNAT للتوجيه، وMASQUERADE إضافية لل Hairpin، أما سطور DOCKER_ و127.0.0.11 فهي ل DNS الداخلي في Docker كما ذكرنا، وهي مثال صغير على أن Docker نفسه يستخدم DNAT وSNAT داخل كل Container.

القواعد نفسها في nftables

الأمر iptables في أغلب التوزيعات الحديثة، ومنها Alpine في تجربتنا، هو واجهة فوق nftables، وهذا ما تعنيه (nf_tables) في مخرج iptables -V، لذلك تستطيع أن ترى القواعد نفسها بلغة nftables:

docker exec lab-nat-router nft list table ip nat
# Warning: table ip nat is managed by iptables-nft, do not touch!
table ip nat {
	chain DOCKER_OUTPUT {
		ip daddr 127.0.0.11 tcp dport 53 counter packets 0 bytes 0 xt target "DNAT"
		ip daddr 127.0.0.11 udp dport 53 counter packets 0 bytes 0 xt target "DNAT"
	}

	chain OUTPUT {
		type nat hook output priority dstnat; policy accept;
		ip daddr 127.0.0.11 counter packets 0 bytes 0 jump DOCKER_OUTPUT
	}

	chain DOCKER_POSTROUTING {
		ip saddr 127.0.0.11 tcp sport 44099 counter packets 0 bytes 0 xt target "SNAT"
		ip saddr 127.0.0.11 udp sport 38624 counter packets 0 bytes 0 xt target "SNAT"
	}

	chain POSTROUTING {
		type nat hook postrouting priority srcnat; policy accept;
		ip daddr 127.0.0.11 counter packets 0 bytes 0 jump DOCKER_POSTROUTING
		ip saddr 192.168.50.0/24 oifname "eth1" counter packets 5 bytes 274 xt target "MASQUERADE"
		ip saddr 192.168.50.0/24 ip daddr 192.168.50.20 tcp dport 80 xt match "conntrack" counter packets 1 bytes 60 xt target "MASQUERADE"
	}

	chain PREROUTING {
		type nat hook prerouting priority dstnat; policy accept;
		ip daddr 203.0.113.10 tcp dport 8080 counter packets 3 bytes 180 xt target "DNAT"
	}
}

ولاحظ التحذير في السطر الأول، فهذه الجداول يديرها iptables-nft، فلا تعدلها بالأمر nft مباشرة، واختر أداة واحدة لكل سيرفر. وإذا كتبت قواعدك بلغة nftables من البداية، فالصيغة الأصلية للترجمة مشروحة في ويكي nftables، والفكرة نفسها: masquerade في سلسلة من نوع postrouting، وdnat to في سلسلة من نوع prerouting.

إضافة طبقة CGNAT

نختم بمحاكاة المزود الذي يضعك خلف CGNAT، فننشئ شبكة للمزود من النطاق 100.64.0.0/24، وجهاز CGNAT له عنوان عام 203.0.113.20 وقاعدة MASQUERADE لكل المشتركين، ونجعل البوابة التي ينشئها Docker في هذه الشبكة على 100.64.0.254 حتى يبقى العنوان الأول لجهاز المزود:

docker network create --subnet 100.64.0.0/24 --gateway 100.64.0.254 -o com.docker.network.bridge.enable_ip_masquerade=false lab-nat-isp
docker run -d --name lab-nat-cgnat --hostname cgnat --cap-add NET_ADMIN --cap-add NET_RAW --sysctl net.ipv4.ip_forward=1 alpine:3.24 sleep 7200
docker exec lab-nat-cgnat apk add -q iptables conntrack-tools tcpdump curl iproute2
docker network disconnect bridge lab-nat-cgnat
docker network connect --ip 100.64.0.1 lab-nat-isp lab-nat-cgnat
docker network connect --ip 203.0.113.20 lab-nat-wan lab-nat-cgnat
docker exec lab-nat-cgnat ip route del default
docker exec lab-nat-cgnat iptables -t nat -A POSTROUTING -s 100.64.0.0/10 -o eth1 -j MASQUERADE

ثم ننقل واجهة الإنترنت في ال Router المنزلي من الشبكة العامة إلى شبكة المزود، فيصبح عنوان WAN فيه 100.64.0.2 بدل العنوان العام، ونجعل جهاز المزود بوابته الافتراضية، ونحدث قاعدة التوجيه إلى عنوانه الجديد:

docker network disconnect lab-nat-wan lab-nat-router
docker network connect --ip 100.64.0.2 lab-nat-isp lab-nat-router
docker exec lab-nat-router ip route del default
docker exec lab-nat-router ip route add default via 100.64.0.1
docker exec lab-nat-router iptables -t nat -R PREROUTING 1 -d 100.64.0.2 -p tcp --dport 8080 -j DNAT --to-destination 192.168.50.20:80
docker exec lab-nat-router ip -br -4 addr show
lo               UNKNOWN        127.0.0.1/8
eth0@if2996      UP             192.168.50.254/24
eth1@if3006      UP             100.64.0.2/24 

والآن نسأل من الحاسوب عن عنوانه العام، ونتتبع الطريق إلى سيرفر الإنترنت:

docker exec lab-nat-laptop curl -sS -m 4 http://203.0.113.80/cgi-bin/ip
docker exec lab-nat-laptop traceroute -n -q 1 -w 1 203.0.113.80

والمخرج سوف يكون كما يلي:

you are 203.0.113.20
traceroute to 203.0.113.80 (203.0.113.80), 30 hops max, 46 byte packets
 1  192.168.50.254  0.008 ms
 2  100.64.0.1  0.005 ms
 3  203.0.113.80  0.012 ms

وهذان هما الدليلان اللذان ذكرناهما في قسم CGNAT: العنوان الذي تراه المواقع 203.0.113.20 مختلف عن عنوان WAN في ال Router 100.64.0.2، والخطوة الثانية في traceroute عنوان من 100.64.0.0/10. والآن نجرب توجيه المنفذ من الإنترنت، مرة إلى العنوان العام المشترك، ومرة إلى عنوان ال Router المنزلي مباشرة، ثم من داخل شبكة المزود:

docker exec lab-nat-internet curl -sS -m 4 http://203.0.113.20:8080/
docker exec lab-nat-internet curl -sS -m 4 http://100.64.0.2:8080/
docker exec lab-nat-cgnat curl -sS -m 4 http://100.64.0.2:8080/

والمخرج سوف يكون كما يلي:

curl: (7) Failed to connect to 203.0.113.20:8080 after 0 ms: Could not connect to server
curl: (7) Failed to connect to 100.64.0.2:8080 after 0 ms: Could not connect to server
hello from the home server 192.168.50.20

أي أن الزائر من الإنترنت لا يصل إلى الخدمة بأي عنوان، فجهاز CGNAT يرفض المنفذ 8080 على العنوان المشترك لأنه لا توجد فيه قاعدة لك، والعنوان 100.64.0.2 لا يعرف الإنترنت طريقه، أما من داخل شبكة المزود فتوجيه المنفذ في ال Router المنزلي يعمل تماماً، وبالتالي فالمشكلة ليست في إعدادك، وإنما في أن الباب الخارجي ليس لك. وجدول conntrack في جهاز المزود يبين الترجمة الثانية لاتصال الحاسوب السابق:

docker exec lab-nat-cgnat conntrack -L -p tcp --dport 80
conntrack v1.4.9 (conntrack-tools): 1 flow entries have been shown.
tcp      6 118 TIME_WAIT src=100.64.0.2 dst=203.0.113.80 sport=54832 dport=80 src=203.0.113.80 dst=203.0.113.20 sport=80 dport=54832 [ASSURED] mark=0 use=1

جهاز المزود لا يعرف شيئاً عن 192.168.50.10، فهو يرى الاتصال قادماً من ال Router المنزلي 100.64.0.2، ويترجمه مرة ثانية إلى 203.0.113.20، وهذه هي الترجمة المزدوجة التي رسمناها في الصورة الرابعة.

تنظيف التجربة

احذف كل ما أنشأناه بالأوامر التالية:

docker rm -f lab-nat-router lab-nat-laptop lab-nat-phone lab-nat-homeserver lab-nat-internet lab-nat-cgnat
docker network rm lab-nat-lan lab-nat-wan lab-nat-isp

مشكلات شائعة وكيف تشخصها

وجهت المنفذ ولا يصل أحد إلى الخدمة

هذه أكثر مشكلة تتكرر، وأسبابها كثيرة، لذلك افحصها بالترتيب التالي، من الخارج إلى الداخل:

  1. هل أنت خلف CGNAT؟ قارن عنوان WAN في ال Router بالعنوان الذي تراه المواقع كما في قسم اكتشاف CGNAT، فإذا اختلفا فلن يفيدك أي توجيه، كما رأيت في التجربة.
  2. هل تختبر من داخل البيت؟ إذا كان الاختبار من شبكة البيت نفسها فقد تكون المشكلة Hairpin NAT وليس التوجيه، فاختبر من بيانات الهاتف أو من سيرفر خارجي.
  3. هل يحجب المزود المنفذ؟ بعض المزودين يحجبون المنافذ 25 و80 و443 الواردة على الاشتراكات المنزلية، فجرب منفذاً خارجياً عالياً مثل 8443 يوجه إلى المنفذ نفسه في الداخل، فإذا عمل فالحجب عند المزود.
  4. هل تنطبق القاعدة؟ إذا كان ال Router يعرض عدادات للقواعد، أو كان Linux أو OpenWrt، فالعداد pkts في iptables -t nat -L PREROUTING -n -v يخبرك، فإذا كان صفراً فالحزم لا تصل إلى ال Router أصلاً أو القاعدة مكتوبة لمنفذ أو بروتوكول خاطئ، فهل اخترت TCP والخدمة UDP مثلاً؟ وإذا كان يزيد فالمشكلة خلف ال Router.
  5. هل تغير العنوان الداخلي للسيرفر؟ خطأ timed out مع عداد يزيد يعني غالباً أن القاعدة تشير إلى عنوان لا يرد، كما في التجربة، والحل حجز DHCP Reservation للسيرفر حتى لا يتغير عنوانه.
  6. هل الخدمة تستمع على العنوان الصحيح؟ نفذ على السيرفر ss -tlnp، فإذا كانت الخدمة تستمع على 127.0.0.1 فقط فلن تصلها الاتصالات من ال Router، وإذا كانت في Docker فتأكد من المنفذ المنشور.
  7. هل يمنعها جدار الحماية في السيرفر نفسه؟ تأكد أن ufw أو غيره يسمح بالمنفذ الداخلي، وليس الخارجي، فالحزمة تصل إلى السيرفر بعد الترجمة على المنفذ 80 وليس 8080.
  8. هل يعود الرد عبر ال Router؟ إذا كان للسيرفر بوابة افتراضية أخرى، أو كان متصلاً بنفق VPN كامل، فسوف يخرج الرد من طريق آخر دون ترجمة، فيرفضه الزائر.

وتذكر الفرق بين الخطأين اللذين رأيتهما في التجربة: الرفض الفوري Could not connect ... after 0 ms يعني أن الجواب جاء مباشرة، إما بحزمة RST من جهاز وصلته الحزمة وهو لا يستمع على هذا المنفذ ولا توجد فيه قاعدة توجيه، وإما لأن جهازك لا يعرف طريقاً إلى العنوان أصلاً، كما حدث مع 100.64.0.2 من الإنترنت. أما انتهاء المهلة timed out فيعني أن الحزمة ضاعت في الطريق، أو رفضها جدار حماية بصمت، أو ضاع الرد.

Router خلف Router: ال Double NAT

إذا ركبت Router خاصاً بك خلف جهاز المزود، فكلاهما يقوم ب NAT، فيصبح عندك ترجمتان كما في CGNAT، ولكن الفرق أنك تملك الجهازين هنا، فتعرفه عندما ترى في traceroute عنوانين خاصين متتاليين قبل الإنترنت، مثل 192.168.1.1 ثم 192.168.0.1. والحل الأفضل أن تضع جهاز المزود في وضع الجسر Bridge Mode أو تمرير العنوان IP Passthrough إذا كان يدعمه، فيأخذ ال Router الخاص بك العنوان العام مباشرة، وإذا لم يدعمه فوجه المنفذ في جهاز المزود إلى عنوان ال Router الخاص بك، ثم وجهه مرة ثانية من ال Router الخاص بك إلى السيرفر. ولاحظ أن ال Double NAT يزيد أيضاً من صعوبة الاتصال المباشر في المكالمات والألعاب وشبكات Mesh، لأن كل طبقة لها سلوكها في الربط والتصفية.

اتصالات UDP تنقطع بعد فترة هدوء

نفق VPN يعمل ثم يتوقف استقبال الرسائل على الهاتف بعد دقيقة من الهدوء، أو مكالمة تنقطع من جهة واحدة، فالسبب غالباً أن صف UDP في جدول NAT حذف بعد مدة الخمول، كما رأيت في التجربة حيث يحذف Linux الصف غير المردود عليه بعد 30 ثانية. والحل من جهة التطبيق أن يرسل حزمة صغيرة دورية أقصر من هذه المدة، مثل PersistentKeepalive = 25 في WireGuard، وإذا كان ال Router يعمل ب Linux وتديره أنت فتستطيع رفع nf_conntrack_udp_timeout_stream، ولكن حل التطبيق أفضل، والسبب أنك لا تتحكم في أجهزة NAT الأخرى في الطريق، مثل CGNAT عند المزود.

ال Router يسقط الاتصالات تحت الضغط

لكل صف في جدول conntrack ذاكرة، وللجدول حد أقصى هو nf_conntrack_max، فإذا امتلأ لم يقبل ال Router أي اتصال جديد، وظهرت في سجل النواة الرسالة nf_conntrack: table full, dropping packet، وهذا يحدث على Router صغير مع برامج تفتح آلاف الاتصالات، أو على سيرفر يتعرض لهجوم. وتعرف العدد الحالي والحد الأقصى بالأمر sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max، والإعدادات كلها مشروحة في توثيق النواة.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • عناوين IPv4 قرابة 4.3 مليار ونفدت، فمخزون IANA فرغ في فبراير 2011، لذلك تأخذ أجهزة البيت والشركة عناوين خاصة من RFC 1918 لا يوجهها الإنترنت، ويبقى العنوان العام عند ال Router.
  • ترجمة المصدر SNAT أو MASQUERADE تغير عنوان المصدر عند الخروج، وPAT يميز الأجهزة بالمنافذ، وجدول conntrack يحفظ صفاً لكل اتصال ومنه يعود الرد إلى صاحبه.
  • NAT ليس جدار حماية، والحماية تأتي من قاعدة FORWARD التي ترفض الاتصالات الواردة الجديدة، وهذا يصبح أوضح مع IPv6 الذي لا يحتاج NAT أصلاً.
  • توجيه المنافذ هو ترجمة الوجهة DNAT، وكل قاعدة باب مفتوح للجميع، وإذا عمل النطاق من الخارج ولم يعمل من الداخل فالحل Hairpin NAT أو Split DNS.
  • خلف CGNAT لا يفيد فتح أي منفذ، وتعرفه بعنوان WAN من 100.64.0.0/10 أو المختلف عن عنوانك العام، والحل عنوان عام من المزود أو IPv6 أو نفق يبدأ من الداخل.
  • الاتصال المباشر بين جهازين خلف NAT يمر بثلاث مراحل: STUN لمعرفة العنوان، ثم Hole Punching، ثم TURN أو DERP عند الفشل، وأصعب الأنواع Symmetric.
  • Docker على سيرفرك Router صغير: DNAT لكل منفذ منشور قبل قواعد ufw، وMASQUERADE لخروج ال Containers.
  • عند التشخيص راقب عداد القاعدة، وفرق بين الرفض الفوري وانتهاء المهلة، واختبر من خارج الشبكة.
دليلتشغيل خادمك من إنترنت المنزل: كل طرق الوصول من الخارجكيف تصل إلى سيرفر في بيتك من الإنترنت حتى لو كنت خلف CGNAT، وما الطريقة المناسبة لك من بين ثماني طرق.دليلما هو VPN؟ الشبكة الخاصة الافتراضية من المشكلة إلى الأنواعلماذا تحتاج شبكة خاصة افتراضية VPN، وكيف تغلف حزمك وتشفرها داخل نفق، وأي نوع منها يناسب فريقك.دليلما هو النفق Tunnel في الشبكات؟ ومتى تحتاج إليهكيف تصل إلى قاعدة بيانات لا تراها من الإنترنت، وتنشر خدمة من خلف CGNAT دون فتح منافذ، ومتى تحتاج إلى VPN بدل النفق.دليلشبكات Docker وأفضل الممارسات مع Docker Composeلماذا يبقى المنفذ الذي تنشره في Compose مفتوحاً للإنترنت رغم ufw، وكيف تصل التطبيقات إلى بعضها عبر شبكات Docker دون نشر منافذ.دليلشرح DNS وسجلاته للمبتدئين بمثال حقيقيكيف يتحول اسم نطاقك إلى عنوان سيرفرك، وما وظيفة كل سجل تضيفه في لوحة النطاق، والأخطاء التي تضيع فيها الساعات عادة.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وبناء التجربة العملية بستة Containers على Alpine 3.24 مع iptables 1.8.13 وconntrack-tools 1.4.9 وtcpdump 4.99.6، ومثال جدول nat في Docker Engine 29.8.0.
Could not load content
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.