أغلب المنصات تبدأ بسيرفر افتراضي VPS واحد كما شرحنا في دليل اختيار ال VPS، له عنوان IP عام واحد، يدخل منه المستخدمون إلى التطبيق، ويخرج منه التطبيق نفسه عندما يرسل رسالة SMS أو بريداً أو طلباً إلى بوابة الدفع. وهذا الإعداد يعمل لشهور دون أي مشكلة، حتى يأتي يوم يطلب فيه شريك أن تعطيه عنوانك ليضعه في قائمة السماح Allowlist عنده، ثم تضيف سيرفراً ثانياً فيرفضه الشريك، أو يجد فريق أمن العميل في الفحص منفذ قاعدة بيانات مفتوحاً على الإنترنت، ويسألك: لماذا يرى الإنترنت سيرفر التطبيق أصلاً؟
والحل الذي تصل إليه كل المنصات الكبيرة في النهاية بسيط في فكرته: السيرفرات في شبكة خاصة لا يصل إليها أحد من الإنترنت، ومدخل واحد Ingress بعنوان ثابت تدخل منه الطلبات، ومخرج واحد Egress بعنوان ثابت آخر تخرج منه كل الاتصالات. وهذا المقال عام لا يرتبط بمزود سحابي معين، فالفكرة نفسها تطبقها على OCI أو AWS أو Azure أو على سيرفراتك بأدوات مفتوحة المصدر، وسوف نذكر المقابل عند كل منها.
دليلما هو NAT؟ شرح ترجمة العناوين وتوجيه المنافذلماذا تخرج كل أجهزة بيتك بعنوان عام واحد، وكيف يترجم ال Router العناوين، ولماذا يفشل توجيه المنافذ خلف CGNAT.
دليلتقسيم الشبكة Network Segmentation بال VLAN وال VPCلماذا تصل كاميرا مخترقة إلى قاعدة بياناتك، وكيف تمنع ذلك بتقسيم الشبكة إلى مناطق لا يمر بينها إلا ما تسمح به.
دليلالتكاليف الخفية في السحابة وكيف تخفضهاالبنود التي لا تظهر في حساب يوم التصميم ثم تملأ فاتورة السحابة بعد أشهر، وكيف تحسب كل بند وتخفضه.
دليلتكامل منصة SaaS مع أنظمة العميل دون خلط المستأجرينكيف تربط منصتك متعددة المستأجرين بنطاق العميل وبريده وأنظمته دون أن تفتح باباً على بقية العملاء.وسوف نناقش في هذا المقال ما يلي:
- الإعداد المعتاد بسيرفر واحد وعنوان واحد، ولماذا يعمل في البداية، وأين يتوقف عن العمل.
- الشكل الذي ننصح به: شبكة خاصة، ومدخل واحد، ومخرج واحد، ودور كل جزء.
- لماذا هذا الفصل: سطح هجوم أصغر، وعنوان ثابت لقوائم السماح عند الشركاء، والتحكم في الخروج، والتوسع، والمراجعة.
- عنوانان مختلفان: أيهما تعطي لمن.
- التكلفة: سعر NAT Gateway عند OCI وAWS وAzure، والتعقيد، ونقطة الفشل الواحدة، ومتى يكفيك سيرفر واحد.
- جدول المقابلات عند كل مزود وبالأدوات مفتوحة المصدر، ثم مخرج خاص لكل عميل ووكيل للخروج Egress Proxy.
- تجربة عملية في Docker: شبكة عامة وشبكة خاصة وموزع حمل وموجه NAT بأداة nftables، ثم نكسرها.
البداية المعتادة: سيرفر واحد بعنوان واحد
لنفرض أن لديك تطبيقاً على VPS عنوانه 203.0.113.5، فيه Nginx أمام التطبيق، وقاعدة البيانات على السيرفر نفسه، وقد أغلقت المنافذ بجدار الحماية Firewall كما شرحنا في دليل تأمين ال VPS. وهذا الإعداد مناسب للبداية لأسباب واضحة: تكلفة سيرفر واحد، ومكان واحد تبحث فيه عن المشكلة، وعنوان واحد تعطيه لأي شخص يسألك عنه، فالشريك الذي يريد عنوانك ليضعه في قائمة السماح يأخذ العنوان نفسه الذي يكتبه المستخدم في المتصفح. والصورة التالية تبين هذا الإعداد، وما يحدث عندما يكبر:

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

السيرفرات في شبكة فرعية خاصة Private Subnet
سيرفرات التطبيق وقاعدة البيانات توضع في شبكة فرعية خاصة، أي شبكة عناوينها من النطاقات الخاصة مثل 10.0.1.0/24 وليس لأي سيرفر فيها عنوان عام، وقد شرحنا الشبكات الفرعية في مقال Subnets وCIDR، وتقسيم الشبكة إلى مناطق في قسم VPC في مقال تقسيم الشبكة. والنتيجة أن السيرفر لا يمكن الوصول إليه من الإنترنت أصلاً، ليس لأن جدار الحماية يمنع ذلك، وإنما لأنه لا يوجد عنوان يتصل به أحد، فالعناوين الخاصة لا تمر في الإنترنت كما شرحنا في مقال NAT. وإدارة هذه السيرفرات تتم عبر سيرفر وسيط Bastion أو VPN كما شرحنا في مقال Bastion Host، وليس بمنفذ SSH مفتوح على كل سيرفر.
مدخل واحد Ingress بعنوان ثابت
الطلبات الداخلة كلها تصل إلى جهاز واحد له قدم في الشبكة العامة وقدم في الشبكة الخاصة، وهو موزع حمل Load Balancer مدار عند مزودك، أو Reverse Proxy تديره أنت مثل HAProxy أو Nginx كما شرحنا في دليل HAProxy. وهذا الجهاز يستمع على المنافذ التي تريدها فقط، عادة 443 و80، ويمرر الطلب إلى سيرفرات التطبيق في الشبكة الخاصة، وعنوانه العام محجوز Reserved لا يتغير إذا أعدت بناءه. وتستطيع أن تضع أمامه طبقة حماية اختيارية، جدار حماية تطبيقات الويب Web Application Firewall واختصاراً WAF، أو حماية من هجمات حجب الخدمة DDoS مثل Cloudflare كما شرحنا في دليل إعداد Cloudflare، أو قواعد الحماية في مقال حماية التطبيقات على مستوى HTTP.
مخرج واحد Egress بعنوان ثابت
والاتصالات الخارجة من كل السيرفرات تمر عبر بوابة NAT Gateway، وهي جهاز يترجم عنوان المصدر الخاص لكل اتصال خارج إلى عنوانه العام الواحد، وهي الفكرة نفسها التي شرحناها في قسم كيف يعمل NAT، ولكن لسيرفرات المنصة بدلاً من أجهزة البيت. والنتيجة أن الطرف الآخر يرى كل سيرفراتك بعنوان واحد، سواء كانت سيرفراً واحداً أو مئة، وسواء أعدت بناءها أو استبدلتها. ولاحظ أن ال NAT Gateway يسمح بالاتصالات التي تبدأ من الداخل وبالردود عليها فقط، ولا يقبل اتصالاً جديداً يبدأ من الإنترنت، فهو مخرج وليس مدخلاً، وتقول وثائق Azure NAT Gateway ذلك صراحة: الخدمات خارج الشبكة لا تستطيع أن تبدأ اتصالاً داخلاً عبره.
ويجدر الإشارة هنا إلى أن Azure جعل هذا الشكل هو الافتراضي، فبحسب الوثائق نفسها، الشبكات الافتراضية الجديدة منذ 31 مارس 2026 تستخدم الشبكات الفرعية الخاصة افتراضياً، ولا تعطي السيرفرات طريقاً افتراضياً إلى الإنترنت، وعليك أن تختار طريقة خروج صريحة مثل NAT Gateway.
لماذا هذا الفصل؟
سطح هجوم أصغر
سطح الهجوم Attack Surface هو مجموع الأماكن التي يستطيع المهاجم أن يحاول منها، وفي الإعداد الجديد لا يستمع على الإنترنت إلا المدخل، وعلى منفذين فقط. فإذا نسي أحد الفريق لوحة إدارة على المنفذ 8081 في سيرفر التطبيق، أو شغل قاعدة بيانات تجريبية بكلمة مرور افتراضية، فلن يصل إليها أحد من الإنترنت لأن السيرفر نفسه لا يملك عنواناً عاماً، أي أن الخطأ الصغير لا يتحول إلى اختراق. وهذا ما سوف نراه في التجربة العملية، ثم نكسره بإعطاء سيرفر واحد واجهة عامة لنرى لوحة الإدارة تظهر للإنترنت مباشرة.
عنوان ثابت لقوائم السماح عند الشركاء
مزود الرسائل القصيرة SMS، وبوابة بريد العميل، وجدار الحماية أمام أنظمة العميل، وواجهات الدفع، كثير منها يقبل الاتصال من عناوين محددة فقط، وهذا منطقي لأن مفتاح API المسروق وحده لا يكفي إذا جاء من عنوان آخر. وبالمخرج الواحد تعطي كل شريك عنواناً واحداً مرة واحدة، ثم تضيف السيرفرات وتحذفها وتعيد بناءها دون أن تراسل أحداً، لأن الشريك لا يرى إلا عنوان المخرج. وقد شرحنا في مقال تكامل منصة SaaS مع أنظمة العميل ما يحدث عملياً عندما يتغير هذا العنوان: بوابة البريد ترد 554 Access denied ومزود الرسائل يرد 403، وتتوقف رسائل كل العملاء في اللحظة نفسها.
التحكم في الخروج ومراقبته
عندما يمر كل اتصال خارج من نقطة واحدة، تستطيع أن تضع فيها قائمة بالوجهات المسموحة فقط، وتمنع كل ما سواها، كما تبين الصورة التالية:

وهذا يغلق طريقين يعتمد عليهما أي مهاجم بعد أن يدخل إلى سيرفر: الأول تسريب البيانات Data Exfiltration، أي رفع نسخة من قاعدة البيانات إلى سيرفر يملكه، والثاني الاتصال بسيرفر التحكم Command and Control واختصاراً C2، وهو السيرفر الذي يأخذ منه البرنامج الخبيث أوامره وينزل منه أدواته. وكلاهما اتصال خارج إلى عنوان لا تعرفه، فإذا كانت قائمة السماح لا تعرفه فلن يخرج، وسوف يظهر في السجل محاولة مرفوضة تستحق التحقيق. ولا توجد حماية كاملة هنا بكل تأكيد، فالمهاجم الذكي قد يسرب البيانات عبر وجهة مسموحة، ولكن الهدف الواقعي أن تجعل ذلك أصعب بكثير، وأن ترى المحاولة بدلاً من أن تكتشفها بعد شهور.
والقائمة بالعناوين تعمل جيداً مع شركاء عناوينهم ثابتة، ولكن كثيراً من الخدمات اليوم خلف شبكات توزيع المحتوى CDN وعناوينها تتغير، لذلك تحتاج أحياناً قائمة بأسماء النطاقات بدلاً من العناوين، وهذا ما يقدمه وكيل الخروج Egress Proxy كما سيأتي في قسم وكيل الخروج، أو الجدار المدار عند مزودك، فوثائق AWS Network Firewall مثلاً تذكر قوائم أسماء النطاقات للحد من الوجهات التي تصل إليها تطبيقاتك.
كل جزء يتوسع وحده
المدخل يتوسع بعدد الطلبات، والسيرفرات تتوسع بالعمل الذي تقوم به، والمخرج يتوسع بعدد الاتصالات الخارجة، وعندما تكون الثلاثة في جهاز واحد فأي ضغط على واحد منها يضغط على الباقي. وبعد الفصل تضيف سيرفرات التطبيق خلف المدخل دون أن يتغير شيء يراه المستخدم أو الشريك، وتغير حجم موزع الحمل دون أن تلمس السيرفرات، وإذا وصل المخرج إلى حده فتضيف له عنواناً آخر، فوثائق NAT Gateway في AWS مثلاً تقول إن كل عنوان IPv4 يدعم حتى 55,000 اتصال متزامن لكل وجهة فريدة، وإنك تستطيع أن تربط حتى 8 عناوين بالبوابة لترفع هذا الحد.
دليل أسهل عند المراجعة
عندما يراجع فريق أمن العميل بنيتك، أو تراجعها جهة تدقيق بحسب ضوابط مثل ECC، فالأسئلة المعتادة: ما الذي يواجه الإنترنت؟ وما الذي يسمح له بالخروج؟ وأين السجلات؟ وفي الإعداد الجديد الإجابة رسم واحد وقاعدتان: قواعد المدخل، وقائمة المخرج، وسجلات الجهازين. أما في الإعداد بسيرفر واحد، فالإجابة قائمة المنافذ المفتوحة على كل سيرفر، وقواعد جدار الحماية على كل سيرفر، ولا يوجد سجل واحد للاتصالات الخارجة.
عنوانان مختلفان: أيهما تعطي لمن؟
بعد الفصل أصبح لمنصتك عنوانان، ولاحظ أن عنوان المدخل لا يظهر أبداً في الاتصالات الخارجة، وعنوان المخرج لا يستقبل أي اتصال، لذلك فالقاعدة بسيطة: من يتصل بك يحتاج عنوان المدخل، ومن تتصل به أنت يحتاج عنوان المخرج. والصورة التالية تبين ذلك، ومعها الخلط المعتاد بين العنوانين:

| من يسأل | العنوان الذي تعطيه | والسبب |
|---|---|---|
| العميل الذي يضيف سجل DNS لنطاقه | عنوان المدخل، أو اسم موزع الحمل في سجل CNAME | المستخدم يتصل بك |
| فريق شبكة العميل الذي يسمح لموظفيه بالخروج إلى عناوين محددة | عنوان المدخل | الموظف يتصل بك من شبكة العميل |
| شريك يرسل إليك Webhook ويريد أن يعرف أين يرسل | عنوان المدخل أو اسم النطاق | الشريك يتصل بك |
| مزود الرسائل القصيرة، وواجهة الدفع، وأي API يقبل عناوين محددة | عنوان المخرج | أنت تتصل بهم |
| فريق بريد العميل الذي يسمح لك بالإرسال عبر بوابته | عنوان المخرج | أنت تتصل ببوابته |
| جدار حماية العميل أمام SIEM أو أي نظام ترسل إليه | عنوان المخرج | أنت تتصل بنظامه |
وانتبه إلى أن «العنوان» قد يكون أكثر من عنوان واحد. فموزع الحمل من الطبقة الرابعة في AWS، أي Network Load Balancer، تقول وثائقه إنك تستطيع أن تختار عنوان Elastic IP لكل منطقة توفر Availability Zone، أي عنوان لكل منطقة. وبوابة NAT الإقليمية Regional NAT Gateway الجديدة في AWS تقول وثائقها إنها تدعم حتى 32 عنواناً لكل منطقة توفر، وإن تحويل البوابات القديمة إليها يعني عناوين جديدة إلا إذا حررت العناوين القديمة وأعدت استخدامها. وفي Azure تستطيع أن تحجز مجموعة عناوين متتالية بنطاق عناوين عامة Public IP Prefix، وتقول وثائقه إن الشريك يستطيع أن يضيف النطاق كله بقاعدة واحدة في جدار الحماية. لذلك أعط الشريك القائمة الكاملة لعناوين المخرج من البداية، حتى العناوين التي لا تستخدمها اليوم، ولا تكتشف عنواناً ثانياً يوم تتعطل منطقة.
التكلفة والتنازلات
كم يكلف المخرج المدار؟
بوابة NAT المدارة ليست مجانية عند كل المزودين، والسعر له في العادة جزءان: سعر ثابت لكل ساعة تعمل فيها البوابة حتى لو لم يمر بها شيء، وسعر لكل GB تعالجه البوابة، وقد شرحنا في قسم NAT Gateway في مقال التكاليف الخفية كيف يكبر هذا البند مع تنزيل ال Docker Images عبرها، وكيف تتجنبه للوصول إلى S3. والجدول التالي يبين الأسعار كما وجدناها في 11 أكتوبر 2026، وهي تتغير، فارجع إلى صفحة مزودك قبل أن تحسب:
| المزود | سعر الساعة | سعر كل GB تعالجه البوابة | ملاحظات |
|---|---|---|---|
| AWS | 0.045 دولار | 0.045 دولار | بحسب مثال US East (Ohio) في صفحة أسعار VPC، والبوابة الإقليمية تحسب الساعة لكل منطقة توفر تعمل فيها، ونقل البيانات إلى الإنترنت يحسب فوق ذلك |
| Azure | 0.045 دولار | 0.045 دولار | من واجهة أسعار Azure للخدمة NAT Gateway، وصفحة الأسعار تقول إن Standard وStandardV2 بالسعر نفسه، وإن رسوم المعالجة تشمل البيانات الخارجة والراجعة |
| OCI | لا يوجد بند لها | لا يوجد بند لها | لم نجد في صفحة أسعار الشبكات أي بند لل NAT Gateway، وفيها أن أول 10 TB من البيانات الخارجة في الشهر مجانية، وأن Flexible Network Load Balancer مجاني |
ولنحسبها بمثال: الشهر فيه تقريباً 730 ساعة، فالبوابة الواحدة في AWS أو Azure تكلف حوالي 32.85 دولار شهرياً قبل أن يمر بها أي بايت، وإذا مر بها 500 GB في الشهر فهذه 22.50 دولار أخرى، أي حوالي 55 دولار لبوابة واحدة. وإذا وضعت بوابة في كل منطقة توفر من ثلاث مناطق كما ينصح التوثيق، فالجزء الثابت وحده حوالي 98.55 دولار شهرياً، وهذا مبلغ أكبر من سعر سيرفرات التطبيق نفسها في كثير من المشاريع الصغيرة. وفي الحالتين أضف سعر العناوين العامة المحجوزة، وسعر موزع الحمل، وقد ذكرناهما في قسم عناوين IPv4 في المقال نفسه.
التعقيد
بعد الفصل أصبحت عندك جداول توجيه Route Tables، وشبكتان فرعيتان على الأقل، وطريقة للدخول إلى السيرفرات الخاصة عبر Bastion أو VPN، وأماكن أكثر يمكن أن تخطئ فيها. وأشهر خطأ عند البداية أن تنسى طريق السيرفرات إلى ال NAT، فلا تستطيع تنزيل التحديثات ولا الاتصال بأي شيء خارجي، ويبدو التطبيق معطلاً دون سبب واضح، وهذا ما رأيناه في التجربة العملية حين كانت الشبكة الخاصة تمنع الحركة المارة عبر جهاز ال NAT. والحل أن تكتب البنية كلها بأداة بنية ككود Infrastructure as Code مثل Terraform أو OpenTofu، فالقواعد مكتوبة ومراجعة، وتعيد بناءها بالشكل نفسه كل مرة.
نقطة فشل واحدة Single Point of Failure
المخرج الواحد يعني أن كل الاتصالات الخارجة تعتمد على جهاز واحد، فإذا توقف توقفت رسائل البريد والرسائل القصيرة وكل اتصال بالشركاء. والمزودون يختلفون هنا، فتقول وثائق AWS إن كل NAT Gateway عادي ينشأ في منطقة توفر واحدة، وإن السيرفرات في المناطق الأخرى تفقد الإنترنت إذا توقفت هذه المنطقة، لذلك تنصح ببوابة في كل منطقة، أو بالبوابة الإقليمية التي تتوسع بين المناطق وحدها. وفي Azure تقول وثائق NAT Gateway إن Standard يعمل في منطقة واحدة، وإن StandardV2 موزع على كل مناطق التوفر Zone Redundant. وفي OCI تصف وثائق NAT Gateway البوابة بأنها عالية التوفر Highly Available. أما إذا بنيت المخرج بنفسك على سيرفر Linux، فهذا السيرفر نقطة فشل واحدة حتى تضع بجانبه سيرفراً ثانياً يأخذ العنوان عند توقفه، بأداة مثل keepalived، أو بميزة CARP في OPNsense وpfSense.
ولاحظ أن البوابة في كل منطقة توفر قد تعني عنوان خروج مختلفاً لكل منطقة، فإذا أعطيت الشريك عنواناً واحداً وتوقفت المنطقة، فستخرج السيرفرات الباقية من عنوان لا يعرفه، وهذا سبب إضافي لتعطيه القائمة الكاملة من البداية.
متى يكفيك سيرفر واحد؟
لا تبن هذا كله لمشروع لا يحتاجه، فالسيرفر الواحد بجدار حماية مضبوط ما زال الحل الأنسب في حالات كثيرة:
- لا يوجد شريك يضع عنوانك في قائمة سماح: فلا تحتاج عنوان خروج ثابتاً منفصلاً، والعنوان المحجوز للسيرفر نفسه يكفي.
- سيرفر واحد يكفي الحمل: مدونة، أو أداة داخلية، أو تطبيق في أوله، والسيرفر الثاني ليس قريباً.
- الميزانية لا تحتمل البوابة المدارة: بوابتان بثلاثين دولاراً لكل منهما أغلى من سيرفرك، وهنا تستطيع أن تأخذ نصف الفكرة: سيرفر صغير بعنوان محجوز يعمل Reverse Proxy ومخرجاً معاً، والتطبيق خلفه على الشبكة الخاصة عند مزود ال VPS، فتحصل على سطح هجوم أصغر وعنوان ثابت للشركاء بتكلفة سيرفر صغير.
والقاعدة: ابدأ بسيرفر واحد مؤمن جيداً، وانتقل إلى الفصل عندما يظهر أول شريك يطلب عنوانك، أو عندما تضيف السيرفر الثاني، أيهما أقرب.
المقابل عند كل مزود وبالأدوات المفتوحة
الأسماء تختلف والفكرة واحدة، والجدول التالي يجمع الأجزاء الثلاثة عند كل مزود كما وجدناها في وثائقه:
| المدخل Ingress | المخرج Egress | العنوان الثابت | |
|---|---|---|---|
| OCI | Load Balancer للطبقة السابعة، أو Network Load Balancer للطبقة الرابعة | NAT Gateway | Reserved Public IP |
| AWS | Application Load Balancer للطبقة السابعة، أو Network Load Balancer للطبقة الرابعة بعنوان Elastic IP لكل منطقة | NAT Gateway في منطقة واحدة، أو إقليمي | Elastic IP |
| Azure | Load Balancer للطبقة الرابعة، أو Application Gateway للطبقة السابعة | NAT Gateway بنوعيه Standard وStandardV2 | Standard Public IP، أو Public IP Prefix |
| على سيرفراتك | HAProxy أو Nginx، ومعهما keepalived لسيرفر احتياطي | سيرفر Linux يترجم بقاعدة masquerade أو snat في nftables، أو Outbound NAT في pfSense وOPNsense | عنوان ثابت من مزود الإنترنت أو من مركز البيانات، أو عنوان عائم Floating IP عند مزود ال VPS |
وفي الطبقة الرابعة يمرر موزع الحمل الاتصال كما هو ويرى عنوان المصدر والمنفذ فقط، وفي الطبقة السابعة يفهم HTTP ويستطيع أن يوجه بحسب النطاق أو المسار وأن ينهي TLS، وقد رأينا الطبقة الرابعة عملياً في قسم وضع TCP في دليل HAProxy، والاختيار بين أدوات ال Reverse Proxy في مقارنة Nginx Proxy Manager وTraefik وHAProxy. أما الفرق بين masquerade وsnat في nftables، فتقول وثائق nftables إن masquerade حالة خاصة من snat تأخذ عنوان المصدر من عنوان الواجهة الخارجة تلقائياً، أما snat فتكتب فيه العنوان بنفسك، ولذلك نستخدم snat في التجربة لأن العنوان الثابت هو بيت القصيد.
إضافات عند الحاجة
مخرج خاص لكل عميل
المخرج الواحد يعني أن كل عملاء منصتك يخرجون من العنوان نفسه، فإذا وضع عميل هذا العنوان في قائمة السماح عند بوابة بريده، فهو يثق بكل ما يخرج من منصتك، بما في ذلك كود يعمل لعميل آخر. وعندما يرفض العميل ذلك، أو تشترط خدمته عنواناً غير مشترك، فالحل شبكة فرعية خاصة بخدمات هذا العميل، توجه حركتها إلى بوابة NAT أخرى بعنوان آخر، والشبكة الفرعية الواحدة توجه حركتها إلى بوابة واحدة عند OCI وAzure كما في وثائقهما. وقد شرحنا هذا الخيار وثمنه بالتفصيل في قسم قائمة العناوين في مقال تكامل منصة SaaS.
وكيل الخروج Egress Proxy
قائمة السماح في ال NAT تعمل بالعناوين والمنافذ، ولكنها لا تعرف أسماء النطاقات، فإذا كان الشريك خلف CDN وتتغير عناوينه، أو أردت أن تسمح بالنطاق api.provider.example وحده دون بقية الخدمات على العنوان نفسه، فأنت تحتاج وكيلاً يفهم الطلب. والفكرة أن السيرفرات لا تخرج إلى الإنترنت مباشرة أبداً، وإنما ترسل طلباتها إلى وكيل، والوكيل يقارن اسم الوجهة بقائمة، ثم يخرج هو عبر ال NAT. ومن الأدوات المفتوحة المصدر Squid، وفيه نوع القائمة dstdomain للمطابقة باسم الوجهة كما في وثائق acl، وEnvoy بمرشح Dynamic Forward Proxy. والثمن أن كل تطبيق يجب أن يعرف الوكيل، عادة بمتغيرات البيئة HTTPS_PROXY وNO_PROXY، وأن الوكيل نقطة أخرى يجب أن تراقبها وتجعل لها بديلاً، فابدأ بقائمة العناوين في ال NAT، وأضف الوكيل عندما تحتاج المطابقة بالأسماء فعلاً.
تجربة عملية: مدخل ومخرج في Docker
الآن نبني الشكل كله على جهاز واحد، بشبكتين في Docker: شبكة عامة lab-edge-public تقوم مقام الإنترنت، وفيها زائر وواجهة الشريك التي نتصل بها وموقع آخر، وشبكة خاصة lab-edge-private فيها سيرفرات التطبيق وحدها. وبين الشبكتين جهازان فقط: موزع حمل Nginx هو المدخل، وموجه NAT بأداة nftables هو المخرج. والصورة التالية تبين شبكة التجربة:

والعناوين العامة في التجربة من نطاقات التوثيق في RFC 5737، وكلها داخل Docker، والمنفذ الوحيد المنشور على الجهاز هو 127.0.0.1:18480. والإصدارات المستخدمة: Docker Engine 29.8.0، وAlpine 3.24.2 مع BusyBox 1.37.0 وnftables 1.1.6 وcurl 8.22.0، وnginx 1.31.6.
ملفات التجربة
أنشئ مجلداً فيه الملفات التالية، وأولها سكربت التجهيز setup.sh الذي ينشئ الشبكتين والأجهزة:
#!/bin/sh
# Lab: one public network (plays the internet), one private network (our servers), one ingress, one egress.
# Run from this folder with Docker. Everything is named lab-edge-*; remove it with cleanup.sh.
set -e
IMG=alpine:3.24.2
# 1) The networks, with Docker's own NAT disabled on both, so the only translation is the one we write.
# (An --internal network would also drop the routed traffic of our NAT router, so it is not used here.)
docker network create --subnet 198.51.100.0/24 -o com.docker.network.bridge.enable_ip_masquerade=false lab-edge-public
docker network create --subnet 10.88.0.0/24 -o com.docker.network.bridge.enable_ip_masquerade=false lab-edge-private
# a container gets its tools on Docker's default bridge, then leaves it: name, then the network and address to join
start() {
docker run -d --name lab-edge-$1 --hostname $1 --cap-add NET_ADMIN $4 $IMG sleep 7200 >/dev/null
for try in 1 2 3; do docker exec lab-edge-$1 apk add -q busybox-extras $5 && break; sleep 5; done
docker network disconnect bridge lab-edge-$1
docker network connect --ip $3 $2 lab-edge-$1
}
# 2) The "internet": a partner API we must reach (the SMS provider), another site, and a visitor
start partner lab-edge-public 198.51.100.80
start other lab-edge-public 198.51.100.90
start visitor lab-edge-public 198.51.100.66 "" curl
# like real internet hosts, they know no route to private addresses
for c in partner other visitor; do docker exec lab-edge-$c ip route del default 2>/dev/null || true; done
for c in partner other; do
docker exec lab-edge-$c mkdir -p /www/cgi-bin
docker cp ip.cgi lab-edge-$c:/www/cgi-bin/ip
docker exec lab-edge-$c sh -c 'chmod +x /www/cgi-bin/ip && httpd -p 80 -h /www'
done
# 3) The egress: a NAT router with nftables, one leg in each network. Tools are installed before it leaves Docker's bridge.
start nat lab-edge-public 198.51.100.20 "--sysctl net.ipv4.ip_forward=1" "nftables tcpdump"
docker network connect --ip 10.88.0.254 lab-edge-private lab-edge-nat
docker cp nat-open.nft lab-edge-nat:/etc/nat-open.nft
docker cp nat-allowlist.nft lab-edge-nat:/etc/nat-allowlist.nft
docker exec lab-edge-nat nft -f /etc/nat-open.nft
# 4) The application servers: private network only, default route through the NAT router
for n in 1 2; do
start app$n lab-edge-private 10.88.0.1$n
docker exec lab-edge-app$n mkdir -p /www/cgi-bin /admin/cgi-bin
docker cp app.cgi lab-edge-app$n:/www/cgi-bin/app
docker cp admin.cgi lab-edge-app$n:/admin/cgi-bin/admin
docker exec lab-edge-app$n sh -c 'chmod +x /www/cgi-bin/app /admin/cgi-bin/admin && httpd -p 80 -h /www && httpd -p 8081 -h /admin'
docker exec lab-edge-app$n ip route replace default via 10.88.0.254
done
# 5) The ingress: a reverse proxy / load balancer with the stable public address 198.51.100.10,
# published on the host only at 127.0.0.1:18480. It is a proxy, not a router: ip_forward=0
docker run -d --name lab-edge-lb --hostname lb --sysctl net.ipv4.ip_forward=0 --network lab-edge-public --ip 198.51.100.10 -p 127.0.0.1:18480:80 nginx:1.31.6-alpine
docker network connect --ip 10.88.0.253 lab-edge-private lab-edge-lb
docker cp lb.conf lab-edge-lb:/etc/nginx/conf.d/default.conf
docker exec lab-edge-lb nginx -s reload
echo "lab is up"في الإعداد أعلاه لاحظ التالي:
- الخيار
enable_ip_masquerade=falseيمنع Docker من إضافة ترجمة من عنده على الشبكتين، فالترجمة الوحيدة في التجربة هي التي نكتبها في nftables، كما فعلنا في تجربة مقال NAT. - كل جهاز ينزل أدواته وهو على شبكة Docker الافتراضية، ثم يخرج منها ويدخل شبكته، ثم نجعل الطريق الافتراضي لسيرفرات التطبيق
10.88.0.254، أي المخرج. - أجهزة «الإنترنت» نحذف منها الطريق الافتراضي، لأن أي سيرفر حقيقي على الإنترنت لا يعرف طريقاً إلى عناوينك الخاصة.
- موزع الحمل يعمل بالخيار
ip_forward=0، فهو Proxy يفتح اتصالاً جديداً إلى السيرفرات، وليس موجهاً Router يمرر الحزم بين الشبكتين.
وصفحة «الإنترنت» التي تطبع العنوان الذي تراه، مثل المواقع التي تعرض لك عنوانك العام، في الملف ip.cgi:
#!/bin/sh
# The "internet" echo service: prints the source IP address it sees, like the sites that show your public IP.
a=${REMOTE_ADDR#*ffff:}
echo "Content-Type: text/plain"
echo
echo "$(hostname) sees you as ${a%]}"وصفحة التطبيق app.cgi التي تقول أي سيرفر أجاب، ولوحة الإدارة admin.cgi على المنفذ 8081، وهي لوحة داخلية لا يجوز أن يراها الإنترنت، ووضعنا فيها كلمة مرور وهمية لنرى أثر انكشافها:
#!/bin/sh
# An application server page: which server answered, and from which address the request came.
a=${REMOTE_ADDR#*ffff:}
echo "Content-Type: text/plain"
echo
echo "answered by $(hostname), request came from ${a%]}"#!/bin/sh
# An internal admin/debug page on port 8081: fine on a private network, dangerous on the internet.
echo "Content-Type: text/plain"
echo
echo "admin panel of $(hostname): DB_PASSWORD=lab-only-secret"وإعداد المدخل lb.conf، وهو موزع حمل بسيط يوزع الطلبات على السيرفرين بالتناوب:
# The only public entry: listens on the public network and forwards to the private app servers.
upstream apps {
zone apps 64k; # one shared round-robin state for all nginx workers
server 10.88.0.11:80;
server 10.88.0.12:80;
}
server {
listen 80;
location / {
proxy_pass http://apps/cgi-bin/app;
proxy_set_header X-Forwarded-For $remote_addr;
}
}ولاحظ السطر zone apps 64k، فبدونه يحتفظ كل Worker في Nginx بحالة التناوب الخاصة به، وفي أول تشغيل للتجربة أجاب app1 الطلبات الأربعة كلها لأن كل طلب وصل إلى Worker مختلف بدأ من السيرفر الأول، والمنطقة المشتركة كما في وثائق upstream تجعل الإعداد والحالة مشتركين بين كل ال Workers. وأخيراً قواعد المخرج، الأولى دون أي قيود على الوجهات:
# Step 1: a NAT router with no egress policy. Every private server goes out, always as 198.51.100.20.
flush ruleset
table ip edge {
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
ip saddr 10.88.0.0/24 accept
}
chain postrouting {
type nat hook postrouting priority 100;
ip saddr 10.88.0.0/24 ip daddr 198.51.100.0/24 snat to 198.51.100.20
}
}والثانية بقائمة سماح فيها وجهة واحدة، عنوان الشريك والمنفذ 80، وكل ما سواها يمنع ويعد:
# Step 2: the same router with an egress allowlist. Only approved destinations and ports may be reached.
flush ruleset
table ip edge {
set approved {
type ipv4_addr . inet_service
elements = { 198.51.100.80 . 80 } # the SMS provider (our "partner")
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
ip saddr 10.88.0.0/24 ip daddr . tcp dport @approved accept
ip saddr 10.88.0.0/24 counter log prefix "egress-denied " drop
}
chain postrouting {
type nat hook postrouting priority 100;
ip saddr 10.88.0.0/24 ip daddr 198.51.100.0/24 snat to 198.51.100.20
}
}في الإعداد أعلاه لاحظ التالي:
- السياسة الافتراضية للسلسلة
forwardهيdrop، والسطرct state established,related acceptيسمح بالردود على الاتصالات التي بدأت من الداخل فقط، فلا يستطيع أحد من الشبكة العامة أن يبدأ اتصالاً إلى الداخل عبر هذا الجهاز، وقد شرحنا تتبع الاتصالات في قسم Connection Tracking. - القاعدة
snat to 198.51.100.20تكتب العنوان الثابت صراحة، وهو عنوان المخرج الذي نعطيه للشركاء. - المجموعة
approvedمن النوعipv4_addr . inet_service، أي أن كل عنصر فيها عنوان ومنفذ معاً، فالسماح لعنوان الشريك على المنفذ 80 لا يسمح بالمنفذ 22 على العنوان نفسه.
وسكربت الإزالة cleanup.sh يحذف ما أنشأناه بالاسم فقط، ولا يستخدم أي أمر prune:
#!/bin/sh
# Removes only what setup.sh created, by exact name. Never prune.
docker rm -f lab-edge-partner lab-edge-other lab-edge-visitor lab-edge-nat lab-edge-lb lab-edge-app1 lab-edge-app2 lab-edge-app3
docker network rm lab-edge-public lab-edge-privateالتشغيل
شغل السكربت من المجلد نفسه، وإذا كنت تعمل على Windows في Git Bash فاكتب أولاً export MSYS_NO_PATHCONV=1، لأن Git Bash يحول المسارات التي تبدأ بـ / إلى مسارات Windows. ثم انظر إلى عناوين الأجهزة وطريق سيرفر التطبيق:
sh setup.sh
for c in lb nat app1 app2; do printf "%-5s" $c; docker exec lab-edge-$c ip -4 -o addr show | awk '$2!="lo"{printf " %s %s", $2, $4}'; echo; done
docker exec lab-edge-app1 ip routeوالمخرج سوف يكون كما يلي:
lb eth0 198.51.100.10/24 eth1 10.88.0.253/24
nat eth0 198.51.100.20/24 eth1 10.88.0.254/24
app1 eth0 10.88.0.11/24
app2 eth0 10.88.0.12/24
default via 10.88.0.254 dev eth0
10.88.0.0/24 dev eth0 scope link src 10.88.0.11المدخل والمخرج لكل منهما واجهتان، وسيرفر التطبيق له واجهة واحدة في الشبكة الخاصة، وطريقه الوحيد إلى خارجها عبر 10.88.0.254، أي عبر المخرج.
الدخول من المدخل فقط
نطلب الصفحة من الزائر أربع مرات عبر عنوان المدخل، ثم من الجهاز المضيف عبر المنفذ المنشور، ثم نفحص المنافذ المفتوحة على المدخل:
for i in 1 2 3 4; do docker exec lab-edge-visitor curl -sS -m 4 http://198.51.100.10/; done
curl -sS -m 4 http://127.0.0.1:18480/
for p in 22 80 443 8081; do docker exec lab-edge-visitor sh -c "nc -z -w 2 198.51.100.10 $p && echo $p open || echo $p closed"; doneanswered by app1, request came from 10.88.0.253
answered by app2, request came from 10.88.0.253
answered by app1, request came from 10.88.0.253
answered by app2, request came from 10.88.0.253
answered by app1, request came from 10.88.0.253
22 closed
80 open
443 closed
8081 closedفي المخرج أعلاه لاحظ التالي:
- الطلبات توزعت بالتناوب على السيرفرين، والزائر لا يعرف أن هناك سيرفرين أصلاً.
- سيرفر التطبيق يرى الطلب قادماً من
10.88.0.253، أي من المدخل وليس من الزائر، لذلك يرسل المدخل عنوان الزائر في الترويسةX-Forwarded-Forكما شرحنا في مقال تطبيقك خلف ال Reverse Proxy. - على عنوان المدخل منفذ واحد مفتوح هو 80، ولوحة الإدارة على 8081 لا يراها الزائر، لأنها على سيرفر التطبيق وليست على المدخل.
هل يصل الزائر إلى سيرفر التطبيق مباشرة؟
نحاول أولاً كما يحاول أي شخص على الإنترنت، ثم نفترض أن المهاجم على الشبكة نفسها عند المزود، فيضيف طريقاً إلى الشبكة الخاصة عبر المخرج، ثم عبر المدخل:
docker exec lab-edge-visitor curl -sS -m 4 http://10.88.0.11/cgi-bin/app
docker exec lab-edge-visitor ip route get 10.88.0.11
docker exec lab-edge-visitor ip route add 10.88.0.0/24 via 198.51.100.20
docker exec lab-edge-visitor curl -sS -m 4 http://10.88.0.11:8081/cgi-bin/admin
docker exec lab-edge-visitor ip route replace 10.88.0.0/24 via 198.51.100.10
docker exec lab-edge-visitor curl -sS -m 4 http://10.88.0.11:8081/cgi-bin/admin
docker exec lab-edge-visitor ip route del 10.88.0.0/24curl: (7) Failed to connect to 10.88.0.11:80 after 0 ms: Could not connect to server
ip: RTNETLINK answers: Network unreachable
curl: (28) Connection timed out after 4055 milliseconds
curl: (28) Connection timed out after 4053 millisecondsفي المرة الأولى لا يوجد طريق أصلاً، وهذا حال الإنترنت الحقيقي مع العناوين الخاصة. وفي الثانية وصلت الحزمة إلى المخرج، ولكن السلسلة forward سياستها drop ولا تسمح إلا بالاتصالات التي بدأت من الداخل، فانتهت المحاولة بمهلة. وفي الثالثة وصلت الحزمة إلى المدخل، والمدخل لا يمرر الحزم لأنه ليس موجهاً، فانتهت كذلك. أي أن الجهازين الوحيدين في الشبكتين لا يفتح أي منهما طريقاً إلى الداخل إلا ما نريده: المنفذ 80 على المدخل.
كل السيرفرات تخرج من عنوان واحد
نطلب صفحة الشريك من السيرفرين، ثم نضيف سيرفراً ثالثاً كما يحدث عندما يكبر التطبيق، ونطلبها منه:
for n in 1 2; do docker exec lab-edge-app$n wget -qO- http://198.51.100.80/cgi-bin/ip; done
docker run -d --name lab-edge-app3 --hostname app3 --cap-add NET_ADMIN alpine:3.24.2 sleep 7200
docker exec lab-edge-app3 apk add -q busybox-extras
docker network disconnect bridge lab-edge-app3
docker network connect --ip 10.88.0.13 lab-edge-private lab-edge-app3
docker exec lab-edge-app3 ip route replace default via 10.88.0.254
docker exec lab-edge-app3 wget -qO- http://198.51.100.80/cgi-bin/ippartner sees you as 198.51.100.20
partner sees you as 198.51.100.20
partner sees you as 198.51.100.20ثلاثة سيرفرات بثلاثة عناوين خاصة، والشريك يرى عنواناً واحداً هو عنوان المخرج، والسيرفر الثالث عمل من أول طلب دون أن نراسل الشريك. ولاحظ أن الشريك يرى .20 وليس .10، فعنوان المدخل لا يظهر أبداً في الاتصالات الخارجة. والترجمة نفسها نراها على المخرج إذا راقبنا حزمة بدء الاتصال على الواجهتين أثناء طلب من app2، فشغل الأمر الأول ثم الثاني في نافذة أخرى:
docker exec lab-edge-nat tcpdump -lnni any -c 2 'tcp dst port 80 and tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn'
docker exec lab-edge-app2 wget -qO- http://198.51.100.80/cgi-bin/ip17:26:14.085244 eth1 In IP 10.88.0.12.45214 > 198.51.100.80.80: Flags [S], seq 4110754507, win 64240, options [mss 1460,sackOK,TS val 1286751531 ecr 0,nop,wscale 7], length 0
17:26:14.085262 eth0 Out IP 198.51.100.20.45214 > 198.51.100.80.80: Flags [S], seq 4110754507, win 64240, options [mss 1460,sackOK,TS val 1286751531 ecr 0,nop,wscale 7], length 0الحزمة دخلت من الواجهة الخاصة eth1 بعنوان المصدر 10.88.0.12، وخرجت بعد 18 ميكروثانية من الواجهة العامة eth0 بعنوان المصدر 198.51.100.20، والمنفذ ورقم التسلسل كما هما.
قائمة السماح للخروج
بالقواعد الأولى يستطيع أي سيرفر أن يصل إلى أي وجهة، وهذا ما يحتاجه المهاجم ليرفع البيانات أو يتصل بسيرفر التحكم:
docker exec lab-edge-app1 wget -qO- -T 4 http://198.51.100.90/cgi-bin/ipother sees you as 198.51.100.20الآن نحمل القواعد الثانية بقائمة السماح، ونجرب الشريك، ثم الموقع الآخر، ثم منفذاً آخر على عنوان الشريك نفسه، ثم نقرأ عداد المرفوض:
docker exec lab-edge-nat nft -f /etc/nat-allowlist.nft
docker exec lab-edge-app1 wget -qO- -T 4 http://198.51.100.80/cgi-bin/ip
docker exec lab-edge-app1 wget -qO- -T 4 http://198.51.100.90/cgi-bin/ip
docker exec lab-edge-app3 sh -c 'nc -z -w 3 198.51.100.80 22 && echo "port 22 open" || echo "port 22 blocked"'
docker exec lab-edge-nat nft list chain ip edge forwardpartner sees you as 198.51.100.20
wget: download timed out
port 22 blocked
table ip edge {
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
ip saddr 10.88.0.0/24 ip daddr . tcp dport @approved accept
ip saddr 10.88.0.0/24 counter packets 5 bytes 300 log prefix "egress-denied " drop
}
}الشريك يعمل كما كان، والموقع الآخر انتهى بمهلة، والمنفذ 22 على عنوان الشريك نفسه منع لأن القائمة تسمح بالعنوان والمنفذ معاً. والعداد يقول إن 5 حزم منعت، وهي محاولات بدء الاتصال وإعادتها، وفي الإنتاج ترسل هذه السجلات إلى نظام المراقبة، لأن سيرفر تطبيق يحاول الاتصال بوجهة لا يعرفها حدث يستحق التحقيق. ولاحظ أن المنع بالقاعدة drop يظهر للتطبيق مهلة وليس رفضاً فورياً، فإذا أردت أن يفشل الطلب بسرعة ويظهر السبب في سجل التطبيق فاستخدم reject بدلاً منها.
نكسرها: واجهة عامة على سيرفر التطبيق
هذا ما يحدث كثيراً في الواقع: سيرفر لا يعمل كما يجب، فيضعه أحد الفريق في الشبكة العامة «مؤقتاً» ليختبره مباشرة، أو يعطيه عنواناً عاماً. نفعل الشيء نفسه بإضافة واجهة في الشبكة العامة لسيرفر app1، ثم نفحص منافذه من الزائر، ونطلب لوحة الإدارة، ثم نرى كيف يخرج:
docker network connect --ip 198.51.100.11 lab-edge-public lab-edge-app1
docker exec lab-edge-app1 ip route
for p in 22 80 443 8081; do docker exec lab-edge-visitor sh -c "nc -z -w 2 198.51.100.11 $p && echo $p open || echo $p closed"; done
docker exec lab-edge-visitor curl -sS -m 4 http://198.51.100.11:8081/cgi-bin/admin
docker exec lab-edge-app1 wget -qO- -T 4 http://198.51.100.90/cgi-bin/ip
docker exec lab-edge-app1 wget -qO- -T 4 http://198.51.100.80/cgi-bin/ipdefault via 10.88.0.254 dev eth0
10.88.0.0/24 dev eth0 scope link src 10.88.0.11
198.51.100.0/24 dev eth1 scope link src 198.51.100.11
22 closed
80 open
443 closed
8081 open
admin panel of app1: DB_PASSWORD=lab-only-secret
other sees you as 198.51.100.11
partner sees you as 198.51.100.11في المخرج أعلاه لاحظ التالي:
- لوحة الإدارة على المنفذ 8081 أصبحت على الإنترنت مباشرة، ومعها كلمة المرور الوهمية، مع أننا لم نغير أي إعداد في التطبيق، والسبب الوحيد أن السيرفر أصبح له عنوان يتصل به أي أحد. وكذلك المنفذ 80 أصبح مفتوحاً دون المرور بالمدخل، أي دون WAF ودون حدود المعدل ودون سجلات المدخل.
- الخروج تغير كذلك: الموقع الآخر الذي منعته قائمة السماح قبل دقيقة رأى الطلب، لأن الطلب لم يمر بالمخرج أصلاً، فقائمة السماح لم تعد تحمي شيئاً.
- والشريك يرى الآن العنوان
198.51.100.11وليس عنوان المخرج، أي أن هذا السيرفر سوف يرفض عند أي شريك يقبل عنوان المخرج وحده، وهذا هو الخطأ الذي يظهر في الإنتاج على شكل «الرسائل لا تصل من سيرفر واحد فقط».
والسبب في السلوك الأخير أن الشبكة العامة في التجربة متصلة مباشرة بالواجهة الجديدة، فأخذ السيرفر الطريق الأقرب، وفي السحابة النتيجة في الغالب مشابهة، فالسيرفر الذي له عنوان عام قد يخرج به مباشرة بدلاً من المخرج المشترك بحسب جدول التوجيه. ثم نصلح الخطأ بإزالة الواجهة:
docker network disconnect lab-edge-public lab-edge-app1
docker exec lab-edge-app1 ip route
docker exec lab-edge-app1 wget -qO- -T 4 http://198.51.100.80/cgi-bin/ip
docker exec lab-edge-visitor sh -c "nc -z -w 2 198.51.100.11 8081 && echo 8081 open || echo 8081 closed"default via 10.88.0.254 dev eth0
10.88.0.0/24 dev eth0 scope link src 10.88.0.11
partner sees you as 198.51.100.20
8081 closedوالدرس أن تراقب هذا الخطأ آلياً، فاجعل فحصاً دورياً يسأل مزودك عن كل سيرفر له عنوان عام، ويقارنها بقائمة الأجهزة المسموح لها، أي المدخل والمخرج وال Bastion فقط.
ما انكسر أثناء البناء
في أول نسخة من التجربة أنشأنا الشبكة الخاصة بالخيار --internal في Docker، لأنه يبدو الأنسب لشبكة لا تخرج إلى الإنترنت، فعملت التجربة كلها إلا الخروج، وانتهت كل طلبات السيرفرات إلى الشريك بالخطأ التالي:
wget: can't connect to remote host (198.51.100.80): Operation timed outوالسبب أن الشبكة الداخلية في Docker لا تمرر إلا الحركة التي مصدرها ووجهتها داخل مداها، وحزمة app1 إلى الشريك وجهتها 198.51.100.80، فمنعت قبل أن تصل إلى موجه ال NAT مع أن الموجه على الشبكة نفسها، وعندما غيرنا الشبكة إلى شبكة عادية مع enable_ip_masquerade=false عمل الخروج من أول طلب. وفي السحابة يظهر الخطأ نفسه بشكل آخر: جدول التوجيه في الشبكة الفرعية الخاصة يجب أن يشير صراحة إلى ال NAT Gateway، وإلا فالسيرفرات معزولة تماماً ولا تستطيع حتى تنزيل التحديثات.
إزالة التجربة
sh cleanup.shlab-edge-partner
lab-edge-other
lab-edge-visitor
lab-edge-nat
lab-edge-lb
lab-edge-app1
lab-edge-app2
lab-edge-app3
lab-edge-public
lab-edge-privateأخطاء شائعة
- إعطاء عنوان المدخل لشريك يحتاج عنوان المخرج: مزود الرسائل يضع
.10في قائمته، ثم يرد بالرمز 403 لأن الطلبات تصله من.7، فاسأل دائماً: من يتصل بمن؟ - عنوان مؤقت للمخرج: العنوان الذي يأتي مع البوابة ويذهب معها يتغير يوم تعيد بناءها، فاحجز العنوان أولاً ثم اربطه بها.
- بوابة في منطقة توفر واحدة وعنوان واحد عند الشريك: عندما تضيف بوابة ثانية في منطقة أخرى، أو تتعطل الأولى، يظهر عنوان لا يعرفه الشريك، فأعطه كل العناوين من البداية.
- سيرفر «مؤقت» بعنوان عام: كما رأينا في التجربة، يكشف منافذه ويتجاوز قائمة الخروج ويغير عنوانه عند الشركاء، والمؤقت في الإنتاج يبقى في الغالب.
- الاعتماد على العنوان وحده للتحقق: عنوان المخرج يثبت أن الطلب من منصتك، ولكنه لا يثبت من أي عميل أو أي خدمة فيها، فأكمله دائماً بمفتاح API أو حساب أو شهادة خاصة بكل استخدام.
- قائمة خروج دون سجلات: المنع وحده يحمي، ولكن المحاولة المرفوضة هي أول علامة على اختراق، فأرسل سجلات المرفوض إلى نظام المراقبة ونبه عليها.
تدريب عملي
- أضف إلى قائمة السماح في التجربة الموقع الآخر على المنفذ 80 وحده، ثم تأكد أن
app1يصل إليه وأن المنفذ 22 عليه ما زال ممنوعاً. - غير
dropفي القاعدة الأخيرة إلىreject، وقارن رسالةwgetبالرسالة التي رأيناها.
الخلاصة
- السيرفر الواحد بعنوان واحد مناسب للبداية، ولكن العنوان فيه باب للجميع وهوية عند الشركاء في الوقت نفسه، فكل منفذ مفتوح مكشوف، وكل سيرفر جديد عنوان جديد.
- الشكل الذي ننصح به: السيرفرات في شبكة خاصة دون عناوين عامة، ومدخل واحد بعنوان محجوز وأمامه WAF أو حماية DDoS إن احتجت، ومخرج واحد بعنوان محجوز تخرج منه كل السيرفرات.
- المدخل لمن يتصل بك، والمخرج لمن تتصل به، وقد يكون كل منهما أكثر من عنوان، فأعط القائمة كاملة.
- المخرج الواحد نقطة تحكم: قائمة سماح بالعناوين في ال NAT، أو بالأسماء في وكيل مثل Squid أو Envoy، تمنع التسريب والاتصال بسيرفرات التحكم وتسجل المحاولة.
- الثمن: بوابة NAT مدارة بحوالي 0.045 دولار للساعة ومثلها لكل GB في AWS وAzure في أكتوبر 2026، ولا يوجد لها بند في أسعار OCI، وتعقيد أكثر، ونقطة فشل واحدة إذا كانت البوابة في منطقة توفر واحدة.
- في التجربة رأى الشريك ثلاثة سيرفرات بعنوان واحد، ومنعت قائمة السماح وجهة غير معروفة، ثم كشفت واجهة عامة واحدة لوحة الإدارة وتجاوزت المخرج وغيرت العنوان عند الشريك.
سجل التحديثات
- أكتوبر 2026: كتابة المقال، والتحقق من أسعار NAT Gateway ووثائق OCI وAWS وAzure في 11 أكتوبر 2026، وتنفيذ التجربة على Docker Engine 29.8.0 مع Alpine 3.24.2 وnftables 1.1.6 وnginx 1.31.6.