كيفية توجيه حركة حاويات Docker وحدها عبر OpenVPN
أبقِ المضيف على وصلته العادية وضع شبكة فرعية واحدة للحاويات داخل النفق: ترتيب NAT وقواعد المصدر و iroute في OpenVPN وفحوص MSS.
لا تصل حزمة الحاوية أبدًا إلى tun0، ولن تغيّر قدرات إضافية على الحاوية ذلك۔ بين الضيف والنفق يوجد استعلامان للمسار: أحدهما داخل فضاء أسماء الشبكة في الضيف، والآخر على المضيف بعد وصول الإطار إلى الجسر، والثاني يستشير جدولًا مختلفًا بقواعد مطابقة مختلفة۔ مخططي المرجعي مضيف Linux يشغّل عميل OpenVPN يملك 10.8.0.10 على tun0، وجسر Docker على 172.18.0.0/16، وعنوانًا خاصًا لا يصل إليه إلا جانب النفق۔ يبقى الهدف ضيقًا: تخرج شبكة الضيف الفرعية عبر النفق بينما يحتفظ المضيف بوصلةه العادية۔
تشغيل عميل VPN داخل الحاوية تصميم مختلف، وليس اختصارًا۔ الحاوية عندئذ تملك نفقها وبيانات اعتمادها، وينتقل العمل إلى Dockerfile بدلًا من البقاء في iptables۔
إعادة التوجيه تختار المسار حسب الوجهة، ثم يعيد NAT كتابة المصدر
مع تعيين net.ipv4.ip_forward=1، تُوجَّه الحزمة المُعاد توجيهها حسب الوجهة فقط، في الجدول الرئيسي۔ لا يلعب المصدر أي دور في ذلك القرار۔ فقط بعد اختيار المسار يشغّل النواة POSTROUTING، وهنا تقع قاعدة Docker نفسها: -s 172.18.0.0/16 ! -o br-appnet -j MASQUERADE۔ كل ما يغادر الجسر إلى واجهة أخرى يُترجَم، والعنوان الذي يصبح عليه يعتمد على الواجهة التي أرسله المسار إليها۔
تفسّر هذه القاعدة معظم الأعراض التي يُسألني عنها۔ إذا لم يكن في الجدول الرئيسي مسار للوجهة الخاصة، تخرج الحزمة من الوصلة حاملةً العنوان الفيزيائي للمضيف۔ يرفض مزوّدك الوجهة، أو يرفض الموقع البعيد مصدرًا من RFC 1918 لم يتوقعه قط، فتنتهي مهلة الحاوية بينما يبقى النفق قائمًا وسليمًا۔ لا يُظهر التقاط على tun0 شيئًا، وهذا هو الدليل۔ إذا كان المسار يشير فعلًا إلى tun0، يعيد MASQUERADE كتابة المصدر إلى 10.8.0.10 وتعمل الحاوية نفسها دون أن تلمس شيئًا۔
اطلب البحث الذي يسمّي مصدر الضيف۔ بدون from و iif يجيب المضيف عن مقبسه الخاص ويبلغ عن مسار لن تسلكه الحزمة المُعاد توجيهها أبدًا:
ip route get 192.168.11.222 from 172.18.0.5 iif br-appnetإذا سمّى ذلك الرد الوصلة، فالنفق ليس في الصورة ولن تساعد أي إعدادات من جانب النفق۔
ترجمة الضيف إلى عنوان النفق نفسه
أصغر تغيير ناجح هو إعادة كتابة صريحة للمصدر عند الخروج۔ أدرجه بدلًا من إلحاقه: Docker يجلس أصلًا في رأس POSTROUTING، لذا فإن قاعدة تُضاف بـ -A تُقيَّم خلف قاعدة Docker ولا تعمل أبدًا۔
iptables -t nat -I POSTROUTING 1 -s 172.18.0.0/16 -o tun0 -j SNAT --to-source 10.8.0.10شرطان يحددان بقاء ذلك۔ يجب أن يكون العنوان عنوانًا يملكه العميل فعلًا، لذا ثبّته بمدخل لكل عميل يحتوي ifconfig-push 10.8.0.10 255.255.255.0؛ أما قاعدة تسمّي عنوانًا لم يُسنَد قط فتُنتج إسقاطات صامتة لا خطأً۔ ويجب أن يقبل الطرف البعيد ذلك المصدر ويعيد توجيه الردود، ولهذا تسمّي قائمة السماح هناك عنوان النفق لا شبكة الضيف الفرعية۔ وعندما يكون عنوان العميل ديناميكيًا، فإن -j MASQUERADE مع -o tun0 يتبع ما تحمله الواجهة، على حساب سياسة بعيدة لا يمكنك كتابتها مقابل قيمة ثابتة۔
الترشيح هو النصف الآخر۔ يضبط Docker سياسة FORWARD على drop ويفرّغ السلاسل التي يملكها عند كل إعادة تشغيل للخدمة، لذا ضع السياسة المحلية في DOCKER-USER، التي تبقى:
iptables -I DOCKER-USER -i br-appnet -o tun0 -j ACCEPT
iptables -I DOCKER-USER -i tun0 -o br-appnet -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTقاعدة العودة مهمة: الرد الذي يصل على tun0 لاتصال فتحه الجسر لا يُقبل لمجرد أن الاتجاه الصادر قُبل۔
أبقِ المضيف مباشرًا بقاعدة مصدر
عندما يجب أن يستخدم الجسر كله النفق ولا شيء آخر على المضيف، تجاوز NAT وأعطِ ذلك المصدر جدوله الخاص۔ تبقى عناوين الضيف مرئية من الطرف إلى الطرف۔
echo "200 vpnbox" >> /etc/iproute2/rt_tables
ip rule add from 172.18.0.0/16 lookup vpnbox pref 1000
ip route add default dev tun0 table vpnboxتُستشار القواعد قبل الجدول الرئيسي، و from يطابق المصدر۔ تحمل الحزم الخارجية للنفق مصدر المضيف الفيزيائي، لذا لا تطابق أبدًا from 172.18.0.0/16 ولا يستطيع النفق توجيه نفسه إلى نفسه۔ هذا الاستبعاد الذاتي هو ما يجعل التخطيط مستقرًا۔
يحتاج مسار الرد إلى مساعدة الآن۔ يرى الطرف البعيد عنوانًا غير مترجم 172.18.0.x وليس لديه سبب لمعرفة مكانه۔ على الخادم، يحتاج ملف لكل عميل إلى iroute 172.18.0.0 255.255.0.0 كي يسلّم الجدول الداخلي للخادم تلك البادئة إلى هذا العميل، وإلى route 172.18.0.0 255.255.0.0 مدفوعة كي يعرف العملاء الآخرون أنها موجودة۔
فحصان قبل الثقة به۔ شغّل ip route show default: مع redirect-gateway def1 يحمل الجدول 0.0.0.0/1 و 128.0.0.0/1 عبر tun0، والمضيف ليس مباشرًا إطلاقًا، لأن تلك الأنصاف الافتراضية تفوز دون أن تحل محل default۔ القواعد المضافة بـ ip rule add تختفي أيضًا عند إعادة التشغيل، فاحملها في سكربت up۔
حجم الحزمة هو الفشل الذي تواجهه ثانيًا
واجهة جسر بـ 1500 بايت تمنح النفق أكثر مما يستطيع حمله بمجرد إضافة رؤوس UDP وIP الخارجية۔ تمر التبادلات الصغيرة، فيحل DNS ويكتمل مصافحة TLS، ثم يتعلق رفع أكبر دون أي خطأ في أي مكان۔ يواصل المرسل إعادة المحاولة بالحجم نفسه لأن رسالة ICMP التي تُبلغه بالحاجة إلى التجزئة وتطلب منه التصغير لا تصل أبدًا۔
أصلحه عند الواجهة أو عند MSS أو كليهما۔ لا يحتاج التثبيت إلى تعاون من الضيف:
ip link set dev tun0 mtu 1400
iptables -t mangle -A FORWARD -o tun0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtuالتثبيت لا يعيد كتابة إلا TCP۔ أما UDP، بما فيه QUIC، فيحتاج إلى خفض واجهة الضيف بدلًا من ذلك، بـ --opt com.docker.network.driver.mtu=1400 عند إنشاء الشبكة أو مفتاح compose المطابق۔
DNS والفحوص التي تستحق التكرار
يتبع حل الأسماء المسار نفسه۔ يمرّر محلّل Docker المدمج عند 127.0.0.11 إلى خوادم المضيف المهيأة، فترث الحاوية ما يستخدمه المضيف۔ وعندما تُجاب المنطقة الخاصة من محلّل لا يُصل إليه إلا عبر النفق، وجّه الضيف إلى ذلك العنوان، واختبر بـ getent hosts داخل الحاوية لا على المضيف۔
- شغّل الطلب الفاشل داخل الضيف؛ فالاختبار على جانب المضيف يستخدم عنوان مصدر آخر ويخفي المشكلة۔
- اقرأ العدادات على قواعد NAT والترشيح التي أضفتها۔ القاعدة التي لا تطابق شيئًا هي خطأ توجيه لا خطأ سياسة۔
- تأكد أن عنوان النفق في قاعدة SNAT لديك عنوان يملكه العميل فعلًا۔
- اخفض MTU للضيف أو ثبّت MSS قبل أن تلوم الخادم على توقف۔
اعمل على تلك القائمة وتوقف عند أول فشل۔ تلك الطبقة هي التي يجب إصلاحها؛ وأي شيء يُضاف بعدها يخفي فقط الحد الذي ينكسر۔