SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-23

لماذا يتجاوز Docker جدار UFW وكيف تمنعه؟

إذا كان UFW يرفض المنفذ 8080 لكنه يظل متاحاً، فهذه ليست مصادفة: تعرّف إلى قاعدة DNAT التي يتجاوز بها Docker UFW والحلين الفعّالين لإصلاحها.

لماذا يتجاوز Docker جدار UFW الناري

يتجاوز Docker جدار UFW الناري لأن منافذ الحاويات المنشورة لا تمر أبداً عبر قواعد الجدار الناري التي يديرها UFW. عند تشغيل docker run -p 8080:80، يكتب Docker قاعدة DNAT (ترجمة عنوان الشبكة الوجهة) في سلسلة PREROUTING ضمن جدول nat في نواة النظام. تعيد هذه القاعدة كتابة وجهة كل حزمة إلى العنوان الخاص بالحاوية قبل أن تقرر النواة إلى أين ستتجه الحزمة. ثم تُمرَّر الحزمة بعد إعادة كتابة وجهتها إلى الحاوية عبر سلسلة FORWARD التي يتحكم بها Docker. توجد قواعد UFW في سلسلة INPUT، ولا تدخل الحزمة إليها أبداً. لذلك يعرض ufw status سياسة الرفض الافتراضية، ويبلغ sudo ufw deny 8080 عن نجاح العملية، بينما يظل المنفذ 8080 يستجيب للإنترنت بالكامل.

هذه ليست مشكلة في Docker، ولا يعني ذلك أن UFW معطّل. فكلا الأداتين يضبطان جدار النواة الناري نفسه. لكن قواعد Docker تعمل في مرحلة أسبق من مسار الحزمة، ولذلك لا يُطلب من UFW فحصها. يوضّح هذا الدليل هذا التجاوز، ويشرح آليته، ثم يتناول الحلين الفعّالين: نشر المنافذ على 127.0.0.1، وتصفية الحزم في سلسلة DOCKER-USER. إذا كان UFW جديداً عليك، فأعد إعداده أولاً باتباع دليل أساسيات جدار UFW الناري، لأن جداراً نارياً بسياسة رفض افتراضية يظل الأساس الصحيح لكل ما عدا ذلك على الخادم.

شاهد التجاوز على خادمك بنفسك

ابدأ من VPS يكون فيه UFW مفعّلاً مع سياسة افتراضية لرفض حركة المرور الواردة. شغّل حاوية ويب مع نشر منفذ:

sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpine

يعرض ufw status verbose كلّاً من Default: deny (incoming), allow (outgoing)، ولا يعرض أي قاعدة للمنفذ 8080. ووفقاً لتقرير الجدار الناري نفسه، المنفذ مغلق. اختبر الآن من جهاز مختلف، وليس من الخادم نفسه:

curl -I http://your-vps-ip:8080/
HTTP/1.1 200 OK

تستجيب الحاوية. أضف قاعدة رفض صريحة ثم اختبر مرة أخرى:

sudo ufw deny 8080/tcp

يظل المنفذ مستجيباً، لأن قاعدة الرفض موجودة في سلسلة لا تمر بها الحزمة. لم يفشل UFW. لم يُستشر أصلاً. وهذا يفسر أيضاً سبب صعوبة اكتشاف المشكلة: لا تظهر أي رسالة خطأ في أي مكان، وتنجح عملية النشر، ويبدو ناتج حالة الجدار الناري مطابقاً تماماً لخادم محمي بإحكام ويعمل بصورة سليمة.

الآلية: تعمل PREROUTING قبل INPUT

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

  1. PREROUTING تعمل أولاً. يمكن للقواعد هنا إعادة كتابة وجهة الحزمة، وهذا ما تفعله قاعدة Docker الخاصة بمنفذ منشور.
  2. يأتي قرار التوجيه بعد ذلك. تنتقل الحزمة الموجّهة إلى المضيف نفسه إلى سلسلة INPUT. وتنتقل الحزمة الموجّهة إلى أي جهاز آخر إلى سلسلة FORWARD.
  3. توجد قواعد UFW في INPUT. وتوجد قواعد Docker في FORWARD.

اعرض قاعدة Docker للحاوية التي شغّلتها للتو:

sudo iptables -t nat -L DOCKER -n
Chain DOCKER (2 references)
target   prot opt source       destination
RETURN   0    --  0.0.0.0/0    0.0.0.0/0
DNAT     6    --  0.0.0.0/0    0.0.0.0/0    tcp dpt:8080 to:172.17.0.2:80

يمثل السطر DNAT القصة كاملة. تُعاد كتابة وجهة أي حزمة تصل إلى المنفذ 8080 إلى 172.17.0.2:80، وهو عنوان الحاوية على شبكة الجسر الخاصة بـDocker. بعد إعادة الكتابة، لا تعود الحزمة موجّهة إلى المضيف. لذلك يرسلها قرار التوجيه عبر مسار FORWARD، حيث أضاف Docker مسبقاً قواعد تسمح بحركة الشبكة إلى شبكاته الخاصة. تنتظر قاعدة deny 8080/tcp في INPUT حزمة لا تصل أبداً.

في Ubuntu 24.04، يُعد الأمر iptables واجهة أمامية لـnftables، لكن ترتيب السلاسل والنتيجة متماثلان. يكتب كل من UFW وDocker قواعده في مسار حزم النواة نفسه، وتأتي نقطة دخول Docker أولاً. ولا يقتصر ذلك على UFW: تعمل firewalld على VPS يعمل بـRocky أو AlmaLinux على التصفية عند النقطة نفسها في ذلك المسار، وتتجاوزها قاعدة DNAT نفسها. لذلك تنطبق الإصلاحات أدناه هناك أيضاً.

الإصلاح اليومي: نشر المنافذ على 127.0.0.1

لم تكن معظم الحاويات بحاجة إلى أن تكون عامة من الأساس. قاعدة بيانات، أو خادم تطبيق خلف Reverse Proxy، أو لوحة إدارة، أو نقطة نهاية للمقاييس: لا ينبغي لأيٍّ منها أن يستجيب للإنترنت مباشرة. انشرها على عنوان loopback:

docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpine

أو في ملف Compose:

services:
  web:
    image: nginx:1.29-alpine
    ports:
      - "127.0.0.1:8080:80"

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

sudo ss -tlnp | grep 8080

يجب أن يظهر 127.0.0.1:8080 في الخرج، وليس 0.0.0.0:8080 أو [::]:8080. ثم تحقّق من جهاز آخر من أن curl http://your-vps-ip:8080/ مرفوض.

بالنسبة إلى الخدمات التي يجب أن تكون متاحة عبر الإنترنت، شغّل Reverse Proxy واحداً يتولى المنفذين 80 و443 ويوجّه الطلبات حسب اسم المضيف، ولا تنشر أي منافذ أخرى. هذا هو النمط الذي يشرحه دليل Reverse Proxy باستخدام Traefik، وبه يظل تطبيق مستضاف ذاتياً مثل Nextcloud على VPS غير قابل للوصول إلا عبر الـproxy الخاص به. يشرح دليل أساسيات Docker Compose كيفية تعريف إدخالات ports:، إلى جانب بقية سير عمل Compose.

عندما تكون كل حاوية داخلية مرتبطة بـloopback، تعود UFW إلى مهمتها المعتادة: حماية المنافذ التي يوفّرها الخادم نفسه. أنشئ مجموعة القواعد هنا، ثم نفّذ الأوامر بالترتيب:

ToolUFW rule generator

التصفية الفعلية: سلسلة DOCKER-USER

أحياناً يجب أن يظل منفذ حاوية منشوراً على الشبكة، لكن مع تقييده. مثال ذلك منفذ نسخة قاعدة بيانات لا يُسمح بالوصول إليه إلا من عنوان مكتب واحد. لهذا الغرض، يوفّر Docker السلسلة DOCKER-USER. تمر كل حزمة متجهة إلى أي حاوية عبر DOCKER-USER قبل قواعد القبول الخاصة بـDocker، ولا يكتب Docker قواعد فيها مطلقاً. هذه السلسلة مخصّصة لك، ويترك Docker محتواها دون تغيير عند إعادة تشغيل daemon.

انتبه إلى نقطة مهمة قبل تنفيذ الأمر: عند وصول الحزمة إلى DOCKER-USER، تكون إعادة كتابة DNAT قد حدثت بالفعل. يصبح منفذ وجهة الحزمة هو منفذ الحاوية، وهو 80 في مثالنا، وليس المنفذ المنشور، وهو 8080. لذلك لا تطابق القاعدة التي تبحث عن --dport 8080 أي شيء. الطريقة الموثوقة هي مطابقة المنفذ الذي اتصل به العميل أصلاً، إذ يتذكره متعقّب الاتصالات في kernel:

sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP

اقرأ القاعدة كما يلي: بالنسبة إلى الحزم التي دخلت عبر eth0 وتنتمي إلى اتصال كان منفذ وجهته الأصلي 8080، احذف كل الحزم التي لم تُرسل من 10.0.0.10. يقيّد شرط المطابقة --ctdir ORIGINAL القاعدة باتجاه العميل إلى الحاوية، لذلك لا تُلتقط حزم الرد بالخطأ. استبدل eth0 بواجهة الشبكة العامة لديك؛ ويعرض ip route | grep default اسمها. اختبر القاعدة بالطريقة نفسها السابقة: ينجح curl من العنوان المسموح به، بينما تنتهي مهلة الاتصال من أي مكان آخر. هذا التعليق هو العلامة المميزة على أن قاعدة DROP تعمل كما ينبغي، وليس على عدم وجود خدمة خلف المنفذ. وتُعد الفروق بين الاتصال المرفوض والاتصال الذي تنتهي مهلته أسرع طريقة لتمييز منفذ تتم تصفيته عن خدمة لا تستمع إلى أي منفذ.

تختفي القواعد التي تضيفها باستخدام الأمر iptables عند إعادة التشغيل. وبما أن UFW يدير جدار الحماية هذا بالفعل، فإن المكان المناسب لحفظها هو /etc/ufw/after.rules. أضف كتلة في نهاية الملف:

*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP
COMMIT

ثم شغّل sudo ufw reload. يعيد UFW تطبيق هذا الملف عند كل إعادة تحميل وعند كل إقلاع، لذلك تصبح تصفية الحاويات في المكان نفسه الذي توجد فيه بقية قواعد جدار الحماية، وتبقى فعالة بعد إعادة التشغيل وبعد ترقية Docker.

لماذا يجب ألا تعطّل تكامل Docker مع iptables

تقترح إجابات أقدم لهذه المشكلة ضبط { "iptables": false } في /etc/docker/daemon.json. لا تفعل ذلك. تؤدي قواعد جدار Docker وظائف تتجاوز كثيراً نشر المنافذ. قاعدة masquerade هي التي تمنح الحاويات إمكانية الوصول إلى الإنترنت خارجياً عبر عنوان الخادم، ولذلك لا تستطيع الحاويات، عند تعطيل التكامل، سحب الصور أو الوصول إلى مستودعات الحزم أو استدعاء أي واجهة برمجة تطبيقات خارجية. وقواعد DNAT هي التي تجعل -p يعمل أساساً، ولذلك تتوقف المنافذ المنشورة عن العمل بالكامل. كما تختفي قواعد العزل التي تفصل بين شبكات Compose المنفصلة. ستعالج التجاوز عن طريق تعطيل شبكة الحاويات، وستصبح كل قاعدة من هذه القواعد مسؤوليتك لكتابتها وصيانتها يدوياً. توضّح وثائق Docker الخاصة بها أن هذا الإعداد مخصص لمن ينوون تنفيذ ذلك تحديداً. توجد سلسلة DOCKER-USER لهذا الغرض بالضبط، حتى لا يحتاج أحد إلى استخدام هذا المفتاح.

الجانب الخاص بـIPv6 من المشكلة نفسها

تحقق أولاً من شكل المنفذ المنشور عبر IPv6:

sudo ss -tlnp | grep 8080

بدءاً من Docker Engine 27، يدير Docker جداول ip6tables افتراضياً. عند تفعيل IPv6 على شبكة Docker، يخضع المنفذ المنشور لمعالجة DNAT نفسها في جداول IPv6، لذلك يوجد التجاوز نفسه وينطبق الإصلاح نفسه: توجد السلسلة DOCKER-USER أيضاً في ip6tables، لذا كرر القاعدة باستخدام sudo ip6tables -I DOCKER-USER ...، واختبر من خارج الخادم باستخدام curl مع عنوان IPv6 العام للخادم، مثلاً curl -6 http://[2001:db8:2a::1]:8080/.

على شبكة لا تستخدم IPv6، يتولى docker-proxy معالجة عملاء IPv6 بدلاً من ذلك. وهو عملية عادية في مساحة المستخدم تستمع على [::]:8080 وتعيد توجيه حركة المرور إلى الحاوية عبر IPv4. تمر حركة المرور المتجهة إلى عملية على المضيف عبر INPUT، لذلك يستطيع UFW تصفية هذا المسار، ولكن فقط عندما يدير UFW IPv6 فعلياً. يوضح دليل UFW وIPv6 ما إذا كان ذلك مفعّلاً، والطرق الأخرى التي قد تنشأ بها ثغرة في IPv6 على VPS.

يؤدي النشر على loopback إلى تجاوز المسألة بالكامل: يربط -p 127.0.0.1:8080:80 بعنوان loopback الخاص بـIPv4 فقط، لذلك لا يوجد مستمع IPv6 ولا شيء يمكن الوصول إليه من الخارج عبر أي من مكدسي البروتوكول.

النمط الذي يصمد عملياً

  • انشر كل منفذ داخلي على 127.0.0.1، حتى لا يصبح مكشوفاً من الأساس.
  • أسند الجانب العام إلى Reverse Proxy واحد يملك المنفذين 80 و443.
  • أبقِ سياسة UFW الافتراضية للمضيف على الرفض، مع السماح بـSSH ومنفذي الـproxy.
  • صفِّ منافذ الحاويات العامة فعلياً في DOCKER-USER، وطابقها مع منفذ الوجهة الأصلي، واحفظها في /etc/ufw/after.rules.
  • اترك تكامل Docker مع iptables مفعّلاً.

بعد إعداد ذلك مرة واحدة، يختفي الالتباس: يصف ufw status المضيف، ويصف DOCKER-USER الحاويات. لا يُنشر أي شيء بالخطأ، ويكشف الأمر التالي docker run -p الذي تكتبه بالضبط ما قصدت نشره.

FAQ

لماذا يمكنني الوصول إلى حاوية Docker عندما يحظر UFW المنفذ؟

لأن Docker ينشر المنفذ باستخدام قاعدة DNAT في السلسلة PREROUTING، التي تعيد كتابة وجهة الحزمة إلى عنوان الحاوية قبل إجراء أي تصفية. تنتقل الحزمة بعد ذلك عبر المسار FORWARD، بينما توجد قواعد UFW في INPUT، وهي سلسلة لا تدخلها الحزمة. لذلك لا تتم استشارة الجدار الناري، ولا يكون لقواعد الرفض فيه أي تأثير على منافذ الحاويات المنشورة.

كيف أجعل UFW يحظر المنافذ التي ينشرها Docker؟

لا يستطيع UFW فعل ذلك بمفرده، لأن قواعده موجودة في السلسلة الخطأ. أوقف أولاً تعريض المنفذ، وذلك بنشره على 127.0.0.1:8080:80 بحيث لا يتمكن من الوصول إليه إلا المضيف. أو طبّق التصفية في السلسلة DOCKER-USER باستخدام قاعدة iptables تطابق منفذ الوجهة الأصلي عبر conntrack. احفظ هذه القاعدة في /etc/ufw/after.rules حتى تبقى بعد إعادة التشغيل وufw reload.

هل ينبغي أن أضبط "iptables": false في daemon.json الخاص بـ Docker؟

لا. يزيل هذا الإعداد جميع قواعد الجدار الناري وNAT الخاصة بـ Docker، ما يؤدي إلى تعطيل وظائف تتجاوز مشكلة التجاوز بكثير. تفقد الحاويات الوصول إلى الإنترنت الصادر لأن قاعدة masquerade تختفي، وتتوقف المنافذ المنشورة عن العمل لأن قواعد DNAT تختفي. استخدم النشر على loopback والسلسلة DOCKER-USER بدلاً من ذلك؛ فهما يعالجان التعريض دون تعطيل شبكة الحاويات.

هل يتجاوز Docker قواعد UFW على IPv6 أيضاً؟

في Docker Engine 27 والإصدارات الأحدث، تكون إدارة ip6tables مفعّلة افتراضياً. لذلك يُعاد توجيه المنفذ المنشور على شبكة Docker المفعّل فيها IPv6 حول UFW بالطريقة نفسها المستخدمة مع IPv4، ويحتاج إلى قاعدة DOCKER-USER نفسها مع نظيرها باستخدام ip6tables. في الشبكات التي لا تستخدم IPv6، تستمع عملية docker-proxy على [::]، وتمر هذه الحركة عبر INPUT، حيث يستطيع UFW تصفيتها إذا كان UFW يدير IPv6. يؤدي النشر على 127.0.0.1 إلى تجنب الحالتين، لأن أياً من الخدمات لا يستمع على IPv6 أصلاً.