تخطَّ إلى المحتوى

تحليل تقني معمّق

تصليب nginx ضد هجمات DDoS: حدود الاتصالات ومناطق المعدل والمهل

آخر تحديث: أغسطس 2026 · مناطق المعدل وحدود الاتصالات والمهل بحسب التوجيه · زمن القراءة ~18 دقيقة

حلقة صغيرة من العمّال الهادئين في المركز تتناقل الرموز بانتظام، بينما يتزاحم حشد عنبري كبير عند بوابة مُقنَّنة حولهم؛ ولا يصل إلى العمّال سوى خيط محكوم.

nginx يقاوم هجمات الاتصال البطيء بحكم تصميمه، لأن حلقة الأحداث لا تصرف خيطاً لكل اتصال. لكنه لا يفعل شيئاً تجاه فيضان الطلبات حتى يُضبط. التوجيهات الحاملة هي limit_req_zone وlimit_conn_zone والمهل الأربعة؛ تُفهرَس على $binary_remote_addr، وتُقاس على مقاييسك الأساسية، وتُختبَر بـ limit_req_dry_run قبل أن تُسقط مستخدماً حقيقياً واحداً.

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

كل توجيه أدناه يأتي بقيمته، وبالعدّاد الذي يُظهر عمله، وحيث يلزم بتشغيله التجريبي. القيم نقاط بداية مقيسة على حركتك أنت. والمعدل المنسوخ من دليل إمّا عديم الفائدة وإمّا يخنق مستخدميك أنت.

أشكال الهجوم وتوجيهات nginx التي تردّها
الجهازشكل الهجومتوجيه nginxالتحقّق
فيضان طلبات من مصادر قليلةlimit_req_zone + limit_req (burst, nodelay)نسبة 429 في access log؛ limit_req_dry_run أولاً
اتصالات متزامنة كثيرة من المصدرlimit_conn_zone + limit_connlimit_conn_status 429؛ ‏$connections في stub_status
ترويسة/جسم بطيء (Slowloris)client_header_timeout, client_body_timeoutreset_timedout_connection؛ error log بمستوى info
إساءة الترويسات/الأجسام الكبيرةlarge_client_header_buffers, client_max_body_sizeنسبة 413 / 400 في access log
استنفاد الواصفات/العمّالworker_connections, worker_rlimit_nofile‏active في stub_status مقابل سقف worker_connections

كل سطر يُضبط في nginx.conf ويُتحقَّق منه عبر access log أو stub_status. ولا شيء من هذا ينفع حين يملأ الهجوم القناة أمام nginx، فتلك مسألة طبقة شبكية.

0. المقاييس الأساسية أولاً: شغّل الأرقام التي ستضبط عليها

لا يمكنك قياس حدّ معدل لم تقِسه. فعّل stub_status وتأكّد أن access log يحمل زمن الطلب وحالة الحدّ:

# In an internal-only server block
location = /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}

# A log format that shows rate/conn limiting and timing
log_format ddos '$remote_addr $status $request_time '
                '$limit_req_status $limit_conn_status "$request"';
access_log /var/log/nginx/access.log ddos;
# Live connection state
curl -s http://127.0.0.1/nginx_status
# Active connections, reading/writing/waiting; compare 'Active' to worker_connections

# Requests per second per client, from the log, over a normal week
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

نسبة عدد Active إلى سقف worker_connections، ومعدل الطلبات لكل عميل من السجل، هما المقياسان الأساسيان اللذان يُختار على أساسهما كل عتبة أدناه.

1. سعة العمّال: السقف الذي يقع تحته كل شيء آخر

قبل أي قاعدة معدل، يجب أن يُسمح لـ nginx بعدد كافٍ من الاتصالات والواصفات، وإلّا استنفد نفسه قبل أن يُطلَق أي حدّ.

worker_processes auto;              # one per core
worker_rlimit_nofile 262144;        # must be >= worker_connections * 2

events {
    worker_connections 32768;       # per worker; total = this * worker_processes
    multi_accept on;
    use epoll;                      # Linux; the event loop that makes nginx scale
}

يجب أن يتجاوز worker_rlimit_nofile قيمة worker_connections بهامش لمقابس الاتصالات العليا والملفات، ويجب أن يتجاوزه بدوره حدُّ خدمة نظام التشغيل: LimitNOFILE في وحدة systemd، كما ورد في دليل تصليب Linux. فإن كان حدّ نظام التشغيل أدنى، فهو السقف الحقيقي الذي لا يرفعه أي توجيه في nginx.

# Confirm nginx actually got the descriptors
cat /proc/$(pgrep -o -x nginx)/limits | grep 'open files'

2. تحديد المعدل: limit_req، الدفاع الأول عن فيضان الطلبات

يعرّف limit_req_zone منطقة ذاكرة مشتركة مفهرَسة على عنوان العميل، ويطبّقها limit_req. وهذا أهمّ توجيه DDoS في nginx.

# http context — one zone, keyed on the binary client address
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=2r/s;

server {
    location / {
        limit_req zone=perip burst=20 nodelay;
        limit_req_status 429;
    }
    # A stricter zone on the expensive path
    location = /login {
        limit_req zone=login burst=5 nodelay;
        limit_req_status 429;
    }
}

rate هو السقف المستدام، وburst هامش الاندفاعة القصيرة، وnodelay يقدّم الاندفاعة فوراً بدل مباعدتها. منطقة بحجم 10 ميغابايت تتّسع لنحو 160 ألف مدخل IPv4، لأن $binary_remote_addr يخزّن العنوان في أربع بايتات.

أطلقه بالتشغيل التجريبي أولاً. هنا يكمن الفرق بين التصليب والانقطاع:

# Log what WOULD be limited, but let everything through
limit_req_dry_run on;
# Read what the dry-run would have rejected, per client
grep 'limiting requests' /var/log/nginx/error.log | awk '{print $NF}' | sort | uniq -c | sort -rn

اترك limit_req_dry_run on أسبوعاً تمثيلياً، وتأكّد أن العملاء الذين كان سيحدّهم ليسوا حركتك الحقيقية، ثم أطفئه فيسري القيد.

3. تحديد الاتصالات: limit_conn ضد التزامن

حيث يحدّ limit_req معدل الطلبات، يحدّ limit_conn عدد الاتصالات المتزامنة لكل مصدر. وهو الدفاع عن عميل يفتح ويُبقي اتصالات كثيرة.

limit_conn_zone $binary_remote_addr zone=connperip:10m;

server {
    location / {
        limit_conn connperip 20;    # max 20 concurrent connections per source
        limit_conn_status 429;
    }
}
# Rejections show in the access log as the status you set
awk '$2==429' /var/log/nginx/access.log | wc -l

4. المهل: تحويل شبه الحصانة تجاه Slowloris إلى حصانة

nginx لا يصرف خيطاً لكل اتصال، فالاتصال البطيء رخيص. لكنه ليس مجانياً، لأن الواصف محدود. المهل تطرد الاتصالات المعلَّقة قبل أن تتراكم:

client_header_timeout 5s;      # time allowed to send the full request header
client_body_timeout 5s;        # time allowed between body reads
send_timeout 10s;              # time allowed between successful writes to the client
keepalive_timeout 30s;         # idle keep-alive lifetime
keepalive_requests 100;        # requests per keep-alive connection
reset_timedout_connection on;  # send RST on timeout, freeing the socket immediately

مهلة الترويسة والجسم القصيرة هي ردّ Slowloris المباشر. فالاتصال الذي يقطر ترويساته يُسقَط عند الثانية الخامسة، قبل أن يشغل واصفاً لدقائق.

# Timed-out connections appear in the error log at 'info'
grep -Ei 'timed out|timeout' /var/log/nginx/error.log | tail

5. حدود المخازن والأحجام: إغلاق سطح الطلبات الكبيرة

يستطيع المهاجم استنفاد الذاكرة أيضاً بترويسات أو أجسام مفرطة الحجم. حُدّها:

client_max_body_size 10m;              # reject bodies larger than this (413)
large_client_header_buffers 4 8k;      # count and size of large header buffers
client_header_buffer_size 1k;
# 413 (body too large) and 400 (bad/oversized header) rates
awk '$2==413 || $2==400' /var/log/nginx/access.log | wc -l

6. أخذ عنوان العميل بشكل صحيح خلف وكيل

كل منطقة معدل واتصالات تُفهرَس على عنوان العميل. وخلف موازِن حمل أو CDN، يكون هذا العنوان — ما لم تصحّحه — عنوان الوكيل. عندها يتقاسم كل عميل حقيقي دلواً واحداً وتفقد الحدود معناها.

# Trust the proxy ranges and read the real client from its header
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# Confirm the log now shows real client IPs, not the proxy's single address
awk '{print $1}' /var/log/nginx/access.log | sort -u | wc -l

إن كان هذا العدد 1، فكل طلب يُنسب إلى الوكيل وحدودك مفهرَسة خطأً. وهذا أشيع سبب يجعل حدّ معدل مكتوباً بعناية إمّا يخنق الجميع وإمّا لا أحد.

7. تقديم ردّ رخيص تحت الحدّ

حين يُطلَق حدّ، قدّم شيئاً رخيصاً بدل صفحة خطأ تكلّف عملاً بذاتها:

limit_req_status 429;
limit_conn_status 429;
error_page 429 = @ratelimited;
location @ratelimited {
    default_type text/plain;
    return 429 "Too Many Requests\n";
}

تقديم 429 ثابت لا يكلّف شيئاً تقريباً، فلا يتحوّل الحدّ إلى تضخيم صغير خاص به.

الإشارات التي تُوصَل بالمراقبة

# 1. 429 rate — the limits firing
awk '$2==429' /var/log/nginx/access.log | wc -l
# 2. Active vs ceiling — worker saturation
curl -s http://127.0.0.1/nginx_status | awk '/Active/{print $3}'
# 3. Timed-out (Slowloris) connections
grep -c 'timed out' /var/log/nginx/error.log
# 4. Descriptor headroom
cat /proc/$(pgrep -o -x nginx)/limits | grep 'open files'

ارتفاع نسبة 429 هو عمل الحدود. واقتراب Active من worker_connections هو بلوغ السقف، والإشارة إلى أن معدل الطلبات تجاوز ما يستطيع المضيف امتصاصه.

الحدّ الصادق لتصليب nginx

كل ما هنا يدافع عن استنفاد معدل الطلبات وحالة الاتصال عند حافة التطبيق. وحالتان تقعان خارجه.

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

أين تتوقف الطبقة داخل المنشأة عن أن تنفع وصلة الوصول لديك: 10 غيغابت/ث الخط مُشبع أصلاً — الطبقة العليا فقط الجهاز داخل المنشأة يخفّف 2 غيغابت/ث 8 غيغابت/ث 25 غيغابت/ث 120 غيغابت/ث 1 تيرابت/ث+ حجم الهجوم (مقياس لوغاريتمي)
تحت سعة القناة لحدود nginx نصيب؛ وفوقها لا يعني أي توجيه شيئاً.

والثانية كل ما تحت nginx: طوابير SYN في النواة، وجدول conntrack، والواصفات، التي لا يصل الطلب إلى nginx قبل عبورها. وتلك موضوع دليل تصليب Linux، وتصليب nginx يفترض أنها في مكانها أصلاً. وفوق القناة، الجواب هو الطبقة الشبكية. يُبقي nginx حافة التطبيق قائمة حتى تعمل تلك الطبقة. هذا عمله، وليس أكثر من عمله.

ترتيب التطبيق

  1. فعّل stub_status والسجل الموسَّع، وسجّل أسبوعاً أساسياً.
  2. اضبط worker_connections وworker_rlimit_nofile، وتأكّد أن حدّ نظام التشغيل يتجاوزهما.
  3. إن كان هناك أي وكيل في المقدّمة، صحّح real_ip قبل كتابة أي حدّ.
  4. أضف منطقتَي limit_req وlimit_conn مع limit_req_dry_run on، وراقب أسبوعاً.
  5. أطفئ التشغيل التجريبي ليسري القيد، وراقب نسبة 429.
  6. اضبط المهل وحدود الأحجام.
  7. أوصِل الإشارات الأربع بالمراقبة.

الخطوة الثالثة تسبق الحدود عمداً: فحدّ معدل مفهرَس على عنوان خاطئ أسوأ من لا حدّ، لأنه يبدو حمايةً دون أن يحمي شيئاً.

أسئلة متكررة

أليس nginx محصَّناً أصلاً ضد Slowloris؟
محصَّن إلى حدّ بعيد، ويجدر قول ذلك بدقّة. يعمل Slowloris بشغل خيط عامل لكل اتصال بطيء. أمّا nginx فيستخدم حلقة أحداث، فالاتصال البطيء يكلّف واصف ملف وقليلاً من الذاكرة لا خيطاً، وآلاف منها تُلحق ضرراً أقلّ بكثير مما تُلحقه بخادم يخصّص خيطاً لكل اتصال. لكن «أقلّ بكثير» ليست «معدومة». الواصفات تبقى محدودة، لذا يبقى client_header_timeout وclient_body_timeout مهمَّين؛ وهما ما يحوّل الحصانة شبه الكاملة إلى حصانة فعلية بطرد الاتصالات المعلَّقة.
هل يُستخدم limit_req مع burst وnodelay أم مع delay؟
لأكثر النقاط العامة، burst مع nodelay. فـ limit_req المجرَّد يرفض فوراً كل ما يتجاوز المعدل، ويعاقب الاندفاعات المشروعة مثل تحميل صفحة لمواردها. وburst يضيف طابوراً يمتصّ اندفاعة قصيرة. وnodelay يقدّم الطلبات المطبورة فوراً بدل مباعدتها، وهو ما يتوقّعه المتصفّح. أمّا delay فلحالة أندر تريد فيها تنعيم الحركة نحو خلفية لا تحتمل الاندفاع. اجعل المعدل منخفضاً وburst سخياً، ثم اقرأ نسبة 429 قبل أن تشدّ.
ما هو $binary_remote_addr ولماذا لا $remote_addr؟
كلاهما يفهرس المنطقة على عنوان العميل. لكن $binary_remote_addr يخزّن العنوان في 4 بايتات لـ IPv4 بدل سلسلة نصّية، فيتّسع حجم منطقة ثابت لعدد أكبر بكثير من المدخلات. منطقة بحجم 10 ميغابايت تتّسع لنحو 160 ألف مدخل IPv4 بالصيغة الثنائية. والمسألة هي السعة: في هجوم من عناوين كثيرة يجب ألّا تمتلئ المنطقة، وإلّا طُرد العملاء الشرعيون الجدد مع المهاجمين. لذا افهرس مناطق المعدل والاتصالات دائماً على الصيغة الثنائية.
كيف أُطلق تحديد معدل دون قطع المستخدمين الحقيقيين؟
بـ limit_req_dry_run on. يشغّل منطق التحديد كاملاً ويكتب ما كان سيرفضه في error log، لكنه يمرّر كل طلب. تتركه مفعَّلاً أسبوعاً تمثيلياً، وتقرأ أي العملاء كان سيُحدَّد وبأي معدل، وتتأكّد أنهم ليسوا حركتك الحقيقية، وعندها فقط تطفئه فيسري القيد. أمّا إطلاق limit_req مباشرةً في وضع الإنفاذ، ومضبوطاً على رقم من دليل، فهو طريقك إلى تحديد صفحتك الرئيسية في صباح مزدحم.
ماذا ينكسر خلف موازِن حمل أو CDN؟
المفتاح ينكسر. إن جلس nginx خلف وكيل، فإن $binary_remote_addr هو عنوان الوكيل، فيتقاسم كل العملاء دلواً واحداً للمعدل ويفقد القيد معناه. عليك أولاً ضبط عنوان العميل الحقيقي: set_real_ip_from لنطاقات الوكيل، وreal_ip_header للترويسة التي يرسلها. عندها تُفهرَس المنطقة على العميل الفعلي. والخطأ هنا هو أشيع سبب يجعل قيداً مكتوباً بعناية إمّا لا يفعل شيئاً وإمّا يخنق الجميع دفعة واحدة.
هل توقف هذه التوجيهات هجوماً حجمياً؟
لا. إن ملأ الهجوم القناة أمام الخادم، فإن nginx لا يستقبل الحزم أصلاً ولا يسري أي توجيه. كل ما هنا يدافع عن استنفاد معدل الطلبات وحالة الاتصال عند حافة التطبيق. أمّا الحجم فوق سعة القناة فمسألة طبقة شبكية لا تُحلّ في nginx.conf.

النشر: أغسطس 2026

يُحدَّث هذا الدليل كلما طرح المصنّعون طرازات وشروط ترخيص جديدة. كيف نقارن بين المصنّعين