> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# قضية البريد الذي لا يصل لأبعد من 500 ميل
- URL: https://arabroot.io/articles/قضية-البريد-الذي-لا-يصل-لأبعد-من-500-ميل/
- Published: 2026-10-08T22:15:00.000Z
- Updated: 2026-10-09T12:56:48.000Z
- Description: كيف تتبع مدير أنظمة بلاغاً يبدو مستحيلاً حتى وجد مهلة اتصال تساوي صفراً، وكيف تحسب أثر المسافة على مهلات الاتصال في أنظمتك.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, قصص وتحقيقات, البريد الإلكتروني, الشبكات والوصول, الاستضافة الذاتية

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

وهذه القصة من أشهر قصص مديري الأنظمة، وقد كتبها صاحبها بنفسه في 2002، ونحن هنا لن نعيد نصها وإنما سوف نرويها بكلماتنا كتحقيق تقني، كما فعلنا في [تحقيق الباب الخلفي في XZ](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%A7%D9%83%D8%AA%D8%B4%D9%81-andres-freund-%D8%A7%D9%84%D8%A8%D8%A7%D8%A8-%D8%A7%D9%84%D8%AE%D9%84%D9%81%D9%8A-%D9%81%D9%8A-xz/): لكل خطوة الملاحظة والفرضية والأمر الذي تستخدمه اليوم في الموقف نفسه، ولماذا كانت الخطوة تضيق مكان البحث، ثم نصنع على جهازك «مسافة» حقيقية بين Containers ونرى بأعيننا كيف تفشل المهلة القصيرة مع البعيد وتنجح مع القريب.

وسوف نناقش في هذا المقال ما يلي:

- مصدر القصة ومن كتبها، وما الذي نعرفه عنها بشكل مؤكد.
- البلاغ الغريب، ثم خطوات التحقيق الثماني بالترتيب: ما الذي تغير، وجمع البيانات، وعزل العامل الحقيقي، وملف الإعداد، ورسالة الترحيب Banner، والسبب، والحساب الفيزيائي، والإصلاح.
- الحساب بالأرقام: كم يقطع الضوء في الفراغ وفي الألياف خلال 3 مللي ثانية، وما الذي قاله الكاتب بنفسه عن الأرقام التقريبية في قصته.
- أين تجد مهلة الاتصال اليوم: Postfix وSendmail ونواة Linux وcurl ومكتبات HTTP في تطبيقك، وكم تكلف المسافة فعلاً.
- تجربة عملية Lab آمنة على Containers نضيف فيها تأخيراً بالمللي ثانية، ونقيس بالأوامر curl وswaks وPython وPostfix، ونحسب أقصى مسافة لكل مهلة.
- الدروس التي تستحق أن تنقلها إلى فريقك.

الصورة التالية تبين خطوات التحقيق التي سوف نمر عليها بالترتيب:

![ثماني بطاقات مرقمة من اليمين إلى اليسار: 1 البلاغ، البريد لا يصل لأبعد من 500 ميل ومعه خريطة من قسم الإحصاء، 2 ماذا تغير، تحديث وإعادة تشغيل قبل أيام، 3 بيانات، رسائل اختبار إلى مدن معروفة، نيويورك 420 ميلاً تصل وProvidence على بعد 580 ميلاً تفشل، 4 عزل، صديق في North Carolina بريده عند مزود في Seattle والرسالة تفشل، 5 الإعداد، ملف sendmail.cf لم يتغير بحسب diff، 6 البرنامج، رسالة الترحيب تكشف Sendmail قديماً من SunOS، 7 السبب، مهلة الاتصال صارت صفراً وconnect يتوقف بعد 3 مللي ثانية تقريباً، 8 الفيزياء، الضوء يقطع نحو 559 ميلاً في 3 مللي ثانية، وتحتها حلقة: بلاغ غريب ثم بيانات قبل الفرضيات ثم ما الذي تغير ثم السبب والحساب](https://arabroot.io/content/images/2026/10/500-mile-email-01-investigation.webp)

خطوات تحقيق Trey Harris كما رواها، وكل خطوة تضيق مكان البحث

## من كتب القصة، وما الذي نعرفه عنها؟

كتب Trey Harris القصة في 24 نوفمبر 2002 كرسالة إلى القائمة البريدية sage-members، وهي قائمة أعضاء SAGE أي نقابة مديري الأنظمة System Administrators Guild، وكانت الرسالة رداً على نقاش خفيف عنوانه «المهمة المستحيلة المفضلة لديك»، ثم انتشرت بعد ذلك انتشاراً كبيراً، وهو يحتفظ بها اليوم بنصها الأصلي في [صفحته على ibiblio](https://www.ibiblio.org/harris/500milemail.html?ref=arabroot.io). ولاحظ أنه كتب في أول الرسالة أنه غير بعض التفاصيل وحذف ما كان مملاً ليجعل القصة أمتع، وهذه نقطة مهمة سوف نعود إليها.

وبعد أن كثرت الأسئلة والاعتراضات على التفاصيل التقنية، كتب [صفحة أسئلة شائعة FAQ عن القصة](https://www.ibiblio.org/harris/500milemail-faq.html?ref=arabroot.io) أجاب فيها بصراحة، فذكر أن القصة حدثت فعلاً وأنه كان يدير نظام البريد المركزي في جامعة North Carolina at Chapel Hill، وأنه كتب ملفات إعداد Sendmail التي تستخدمها أغلب سيرفرات البريد في الأقسام، وأنه لا يعرف التاريخ بالضبط ولكنه يقدر أنها حدثت بين 1994 و1997\. وذكر أيضاً أنه لم يحتفظ بالسجلات ولا بالملاحظات التي كتبها وقت التحقيق، لذلك فبعض الأرقام في القصة أعيد بناؤها من الذاكرة، وسوف نفصل في [قسم الدقة](#accuracy) ما هو مؤكد وما هو تقريبي.

## البلاغ: بريد لا يصل إلى أبعد من 500 ميل

بدأت القصة بمكالمة من رئيس قسم الإحصاء، وجملته التي اشتهرت بها القصة كانت: "We can't send mail more than 500 miles"، أي أنهم لا يستطيعون إرسال البريد لأبعد من 500 ميل، ثم أضاف أن المسافة أكثر بقليل، نحو 520 ميلاً، ولكن ليس أبعد من ذلك.

وكان رد فعل Trey الطبيعي هو عدم التصديق، فالبريد لا يعمل بهذه الطريقة، والرسالة تمر بنفس البروتوكول SMTP مهما كان بعد المستلم، كما شرحنا في [مقال كيف يعمل البريد الإلكتروني](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%A8%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A7%D9%84%D9%83%D8%AA%D8%B1%D9%88%D9%86%D9%8A-smtp-%D9%88-imap/#message-path). والطريف أن رئيس القسم لم يتصل فور ظهور المشكلة، وإنما انتظر عدة أيام حتى يجمع بيانات كافية، فهو في النهاية رئيس قسم الإحصاء، وطلب من متخصصة في الإحصاء الجغرافي Geostatistics أن تدرس الأمر، فرسمت خريطة تبين أن البريد يصل داخل دائرة نصف قطرها أكثر بقليل من 500 ميل، وأن بعض الوجهات داخل الدائرة تفشل أيضاً أو تنجح أحياناً فقط، ولكن لا توجد أي وجهة خارج الدائرة نجح الإرسال إليها.

وقد يتساءل البعض: لماذا نعطي بلاغاً كهذا أي وقت؟ والإجابة أن البلاغ يصف عرضاً Symptom وليس سبباً، والمستخدم يصف ما يراه بلغته، وهو هنا يراه بلغة الخرائط لأنه إحصائي، والعرض نفسه حقيقي حتى لو كان التفسير الذي في رأسه غير صحيح. وهذه هي [الخطوة الأولى في أي تحقيق](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#define-symptom): صف العرض بدقة كما هو، قبل أن تقرر أنه مستحيل.

## التحقيق خطوة بخطوة

سوف نعرض الخطوات بالترتيب الذي رواه Trey، ونذكر لكل خطوة ما لاحظه، والفرضية التي كانت في ذهنه، ثم الأمر الذي يستخدمه أي مدير أنظمة اليوم في الموقف نفسه. ولاحظ أن الأوامر هنا من أدوات اليوم وليست من القصة، فالقصة حدثت في التسعينات على SunOS وSendmail، أما ما يهمنا فهو طريقة التفكير.

### الخطوة الأولى: ما الذي تغير؟

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

وعلى سيرفر Linux اليوم تجيب عن هذا السؤال بأوامر تقرأ ما حدث فعلاً بدلاً من ذاكرة الأشخاص:

```bash
# متى أعيد تشغيل السيرفر آخر مرة؟
uptime -s
last -x reboot | head -5

# ما الحزم التي حدثت أو ثبتت مؤخراً؟ (Debian وUbuntu)
grep -E " (upgrade|install) " /var/log/dpkg.log | tail -20

# هل تغير إصدار برنامج البريد نفسه؟
postconf mail_version
```

وقد شرحنا هذه القاعدة في [دليل استكشاف الأخطاء: ما الذي تغير؟](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#what-changed)، فالسؤال عن التغيير يسبق السؤال عن السبب، وفي هذه القصة كان الجواب الصحيح موجوداً من أول دقيقة، ولكن لم يعرف أحد بعد أنه الجواب.

### الخطوة الثانية: اجمع البيانات بنفسك

الحل البسيط هنا هو أن تقرر أن البلاغ خطأ وتغلق التذكرة، أو أن تبدأ بتغيير الإعدادات عشوائياً. ولكن Trey دخل إلى سيرفر القسم وأرسل رسائل اختبار بنفسه، والسبب أن البيانات التي يجمعها المحقق بيده أوثق من أي وصف يسمعه. وكانت النتائج كما رواها: الرسالة إلى حسابه في الجامعة نفسها وصلت، وكذلك إلى Richmond وAtlanta وWashington، ثم Princeton على بعد 400 ميل وصلت، ونيويورك على بعد 420 ميلاً وصلت، أما Memphis على بعد 600 ميل فقد فشلت، وكذلك Boston وDetroit، وProvidence على بعد 580 ميلاً فشلت أيضاً.

والملاحظة المهمة هنا أن النتيجة قابلة للتكرار، وأن الحد الفاصل بين النجاح والفشل يقع فعلاً بين 420 و580 ميلاً، أي أن البلاغ الذي بدا مستحيلاً صار حقيقة يمكن قياسها. واليوم تجمع هذه البيانات بأداة [swaks](https://jetmore.org/john/code/swaks/?ref=arabroot.io) التي تفتح اتصال SMTP وتعرض كل خطوة فيه، والخيار `--quit-after RCPT` يوقف المحادثة قبل إرسال الرسالة فعلاً، فلا تزعج أحداً برسائل الاختبار:

```bash
for d in example.org example.net example.com; do
  echo "== $d"
  time swaks --to postmaster@$d --quit-after RCPT --hide-informational
done

# وفي سجل Postfix: هل تتكرر أخطاء الاتصال نفسها؟
grep -E "connect to .*(timed out|refused)" /var/log/mail.log | tail -20
```

ولاحظ أن الأداة `time` مع كل وجهة تعطيك الرقم الذي سوف يكون مفتاح القصة كلها، وهو زمن فتح الاتصال، وسوف نرى في [التجربة العملية](#lab-smtp) كيف يظهر هذا الزمن عند كل أمر في محادثة SMTP.

### الخطوة الثالثة: افصل بين المستلم والسيرفر

بعد أن تأكد العرض ظهرت فرضية مقلقة، فهل تعتمد المشكلة على مكان الإنسان الذي يستلم الرسالة؟ وهذا مستحيل فيزيائياً، لأن السيرفر المرسل لا يعرف أين يجلس المستلم، وإنما يعرف أين سيرفر البريد الذي يستقبل عنه. وللتأكد من ذلك أرسل Trey رسالة إلى صديق يسكن في North Carolina نفسها، ولكن مزود بريده في Seattle على الطرف الآخر من الولايات المتحدة، فإذا وصلت الرسالة فالمشكلة في مكان الإنسان، وإذا فشلت فالمشكلة في المسافة إلى السيرفر. وقد فشلت، وكتب أنه شعر بالارتياح لذلك.

وهذه تجربة ضابطة Control Experiment بالمعنى الحرفي، فهي تغير عاملاً واحداً فقط، المسافة إلى السيرفر، وتثبت الباقي، كما شرحنا في [قاعدة فرضية واحدة وقياس واحد وتغيير واحد](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#hypothesis). واليوم تعرف أين يستقبل أي نطاق بريده من سجل MX كما شرحنا في [مقال DNS وسجلاته](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-dns-%D9%88%D8%B3%D8%AC%D9%84%D8%A7%D8%AA%D9%87-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#mx)، ثم تقيس الطريق إلى ذلك السيرفر نفسه:

```bash
# أين يستقبل النطاق بريده؟
dig +short MX example.org

# كم يستغرق الذهاب والعودة إلى ذلك السيرفر، وما الطريق إليه؟
ping -c 5 mx.example.org
mtr -rwc 10 mx.example.org

# كم يستغرق فتح اتصال TCP على المنفذ 25 فقط؟
time nc -z -w 5 mx.example.org 25
```

والأمر الأخير يفتح اتصال TCP فقط ثم يغلقه، فالزمن الذي يطبعه `time` هو تقريباً زمن اكتمال الاتصال، وهو بالضبط الرقم الذي كانت تقطعه المهلة في هذه القصة، وسوف نقيسه بدقة أكبر في التجربة بالمتغير `time_connect` في `curl -w` الذي شرحناه في [تمرين توقيتات curl](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D9%81%D9%8A-%D8%A7%D9%84%D8%B4%D8%A8%D9%83%D8%A7%D8%AA/#kata-network-11).

### الخطوة الرابعة: هل تغير ملف الإعداد؟

الآن صار السؤال: ما الذي يمنع الاتصال بالسيرفرات البعيدة؟ وأول مكان طبيعي للبحث هو ملف إعداد Sendmail واسمه `sendmail.cf`، ففتحه Trey ووجده طبيعياً بل ومألوفاً، ثم قارنه بالأمر `diff` مع النسخة التي يحتفظ بها في مجلده، فلم يجد أي فرق، لأنه هو من كتب هذا الملف أصلاً. وعلق ساخراً بأنه متأكد أنه لم يفعل خياراً اسمه «افشل في إرسال البريد لأبعد من 500 ميل».

وهذه الخطوة أغلقت فرضية كاملة: الإعداد لم يتغير. ولكن لاحظ أنها لم تجب عن السؤال الأهم، وهو هل البرنامج الذي يقرأ الإعداد هو نفسه؟ واليوم تقارن إعداد Postfix بطريقة أدق من `diff`، لأن الأمر `postconf -n` يعرض فقط القيم التي غيرتها عن الافتراضي، و`postconf -d` يعرض القيم الافتراضية التي يعرفها البرنامج المثبت فعلاً:

```bash
# الإعداد الذي كتبته أنت، مقارنة مع نسخة محفوظة
postconf -n > /tmp/postconf-now.txt
diff -u ~/postconf-known-good.txt /tmp/postconf-now.txt

# القيمة الافتراضية لمهلة الاتصال في هذا الإصدار
postconf -d smtp_connect_timeout
```

والمخرج للأمر الأخير على Postfix 3.10 في تجربتنا سوف يكون كما يلي:

```text
smtp_connect_timeout = 30s
```

### الخطوة الخامسة: اسأل السيرفر من هو

بعد أن عجز عن إيجاد السبب في الإعداد، اتصل Trey بالمنفذ 25 مباشرة عبر `telnet` ليرى كيف يرد السيرفر، فرد عليه السيرفر برسالة الترحيب Banner الخاصة بنسخة Sendmail التي تأتي مع SunOS. وهنا توقف، لأن Sun كانت ما زالت توزع Sendmail 5 مع نظامها رغم أن Sendmail 8 كان ناضجاً، وهو كمدير أنظمة جيد كان قد وحد كل السيرفرات على Sendmail 8، وكتب ملفات الإعداد بأسماء الخيارات الطويلة الواضحة التي جاءت مع الإصدار 8، بدلاً من الرموز المختصرة التي كان يستخدمها الإصدار 5.

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

```bash
swaks --server 10.50.2.10 --quit-after CONNECT
```

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

```text
=== Trying 10.50.2.10:25...
=== Connected to 10.50.2.10.
<-  220 mx.far.lab ESMTP Postfix (Debian)
 -> QUIT
<-  221 2.0.0 Bye
=== Connection closed with remote host.
```

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

- السطر الذي يبدأ بالرقم `220` هو رسالة الترحيب، وفيها اسم السيرفر واسم البرنامج، وأحياناً الإصدار، وكثير من المسؤولين يخفون الإصدار لأسباب أمنية، ولكن اسم البرنامج يكفي غالباً لتعرف هل هو ما تتوقعه أم لا.
- على السيرفر نفسه لا تعتمد على رسالة الترحيب، فاسأل البرنامج مباشرة عن إصداره بالأمر `postconf mail_version` أو `dpkg -l postfix`، أو بالأمر `sendmail -d0.1 -bv root` إذا كان Sendmail هو البرنامج.

### الخطوة السادسة: السبب، إصدار قديم يقرأ إعداداً جديداً

بعدها اتضحت الصورة كاملة، والصورة التالية تبين السلسلة:

![أربع بطاقات من اليمين إلى اليسار: 1 قبل التحديث Sendmail 8 وملف sendmail.cf كتبه Trey بأسماء خيارات طويلة مثل O Timeout.connect، 2 التحديث إلى SunOS أحدث، البرنامج sendmail صار V5 وملف الإعداد بقي V8، 3 Sendmail 5 يقرأ ملف V8، القواعد مفهومة والأسماء الطويلة يتجاهلها ولا توجد قيم افتراضية فتصبح صفراً، 4 النتيجة على الشبكة timeout = 0، والاتصال يتوقف بعد 3 مللي ثانية تقريباً، القريب يصل والبعيد يفشل](https://arabroot.io/content/images/2026/10/500-mile-email-03-cause.webp)

كيف صارت مهلة الاتصال صفراً رغم أن ملف الإعداد لم يتغير

وبالتفصيل كما رواها Trey:

1. المستشار قام بتحديث نظام التشغيل SunOS إلى إصدار أحدث، وهذا التحديث استبدل برنامج Sendmail 8 الذي ثبته Trey بنسخة Sendmail 5 التي توزعها Sun، أي أن التحديث أرجع برنامج البريد إلى إصدار أقدم Downgrade.
2. التحديث ترك ملف `sendmail.cf` كما هو، رغم أنه مكتوب لإصدار آخر.
3. نسخة Sendmail 5 من Sun، وكانت فيها تعديلات خاصة، استطاعت أن تقرأ الملف، لأن أغلب القواعد Rules لم تتغير بين الإصدارين، أما خيارات الإعداد بأسمائها الطويلة فاعتبرتها كلاماً غير مفهوم وتجاوزتها بدون أي خطأ.
4. البرنامج لم تكن فيه قيم افتراضية مكتوبة داخله Compiled Defaults لأغلب هذه الخيارات، لذلك عندما لم يجد قيمة صالحة في الملف جعلها صفراً.
5. أحد هذه الخيارات كان مهلة الاتصال بسيرفر SMTP البعيد، وبالتجربة وجد Trey أن المهلة الصفرية على ذلك الجهاز وبحمله المعتاد كانت تقطع استدعاء `connect()` بعد أكثر من 3 مللي ثانية بقليل.

والمفارقة أن المعنى الحديث للصفر في هذا الخيار عكس ما حدث تماماً، ففي [دليل تشغيل Sendmail 8](https://www.sendmail.org/~ca/email/doc8.12/op-sh-4.html?ref=arabroot.io#sh-4.1.2) الخيار اسمه `Timeout.connect`، والصفر فيه يعني «استخدم المهلة الافتراضية في النواة»، وهي مهلة طويلة، والدليل يذكر أن هذا الخيار يستطيع أن يقصر مهلة النواة ولكن لا يستطيع أن يطيلها. وكذلك في [توثيق Postfix](https://www.postfix.org/postconf.5.html?ref=arabroot.io#smtp%5Fconnect%5Ftimeout) الصفر في `smtp_connect_timeout` يعني إلغاء المهلة وترك القرار لنظام التشغيل، أي أن نفس القيمة كانت تعني «انتظر طويلاً» في برنامج و«لا تنتظر تقريباً» في برنامج آخر.

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

```bash
echo "smtp_connect_timout = 1s" >> /etc/postfix/main.cf
postconf -n > /dev/null
```

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

```text
postconf: warning: /etc/postfix/main.cf: unused parameter: smtp_connect_timout=1s
```

لاحظ أن Postfix لم يتوقف ولم يرفض الإعداد، وإنما طبع تحذيراً، وهذا التحذير يظهر أيضاً عند `postfix reload` وفي السجل، لذلك اجعل فحص هذه التحذيرات جزءاً من أي تحديث، والسبب أن الخيار المكتوب خطأ أو غير المدعوم في إصدار أقدم يعمل بالقيمة الافتراضية بدون أن تعرف.

### الخطوة السابعة: كم يقطع الضوء في 3 مللي ثانية؟

بقي سؤال واحد: لماذا 500 ميل بالذات؟ وهنا جاءت النقطة الثانية في القصة، فشبكة الجامعة في ذلك الوقت كانت كلها تعمل بأجهزة Switch، فالحزمة الخارجة لا تمر بأي Router حتى تصل إلى نقطة اتصال الجامعة بالإنترنت، وبالتالي فإن زمن الاتصال بسيرفر قليل الحمل في شبكة قريبة يحكمه أساساً بعد الوجهة بسرعة الضوء، وليس التأخير في أجهزة الطريق. فكتب Trey في الأداة `units` سؤالاً بسيطاً: كم ميلاً يقطع الضوء في 3 مللي ثانية؟ ونفس السؤال على Debian 13 في تجربتنا:

```bash
units "3 millilightseconds" miles
```

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

```text
	* 558.84719
	/ 0.0017893979
```

أي نحو 559 ميلاً، وهو الرقم الذي ذكره رئيس القسم تقريباً: «500 ميل أو أكثر بقليل». والحساب نفسه سهل باليد، فسرعة الضوء في الفراغ حسب [قيمة NIST الرسمية](https://physics.nist.gov/cgi-bin/cuu/Value?c&ref=arabroot.io) هي 299,792,458 متراً في الثانية، أي نحو 300 كيلومتر في كل مللي ثانية، وفي 3 مللي ثانية يقطع نحو 899 كيلومتراً، وكل ميل يساوي 1.609344 كيلومتر، فيكون الناتج 559 ميلاً.

والصورة التالية تضع مدن القصة على هذا الحساب، وتبين أيضاً لماذا لا يكفي حساب الضوء في الفراغ وحده، كما سوف نشرح في القسم التالي:

![مخطط خطي، المحور الأفقي المسافة بالميل من 0 إلى 800 وتحته الكيلومترات، والمحور الرأسي زمن الذهاب والعودة RTT بالمللي ثانية من 0 إلى 14، خط بنفسجي لسرعة الضوء في الفراغ يقطع حد المهلة 6 مللي ثانية عند 559 ميلاً، وخط أزرق متقطع للألياف الضوئية بنحو ثلثي سرعة الضوء يقطع الحد عند 373 ميلاً، ونقاط خضراء لـ Princeton على 400 ميل ونيويورك على 420 ميلاً تحت الحد، ونقاط حمراء لـ Providence على 580 ميلاً وMemphis على 600 ميل فوق الحد](https://arabroot.io/content/images/2026/10/500-mile-email-02-distance.webp)

أقل زمن ذهاب وعودة حسب المسافة، ومدن القصة على خط سرعة الضوء في الفراغ

### الخطوة الثامنة: الإصلاح أولاً، ثم الفهم

في صفحة الأسئلة الشائعة ذكر Trey تفصيلاً لم يكن في القصة، وهو أنه بمجرد أن عرف أن استبدال برنامج Sendmail 5 بنسخة Sendmail 8 سوف يحل المشكلة قام بذلك فوراً، لأن تشغيل Sendmail 5 مع ملف إعداد مكتوب للإصدار 8 إعداد غير صحيح في كل الأحوال، ثم احتفظ بالبرنامج القديم ليجرب عليه في وقته ويفهم قصة المهلة كاملة. وكتب أيضاً أن التحقيق لم يأخذ عشر دقائق كما توحي القصة، وإنما عدة ساعات من العمل.

وهذا ترتيب صحيح في أي حادثة: أوقف الضرر عن المستخدمين بأسرع إصلاح آمن، ثم ارجع إلى فهم السبب بهدوء، كما شرحنا في [متى توقف الضرر أولاً ثم تبحث عن السبب](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#mitigate-first)، فقسم الإحصاء يحتاج بريده اليوم، أما الحساب الفيزيائي فيمكن أن ينتظر.

## ما الدقيق وما التقريبي في القصة؟

القصة مكتوبة كحكاية طريفة لزملاء في قائمة بريدية وليست تقرير حادثة، وقد قال Trey ذلك بنفسه في صفحة الأسئلة الشائعة، وأجاب عن أغلب الاعتراضات التقنية بصراحة، ومن المفيد أن تعرفها لأنها تعلمك كيف تقرأ أي رقم في أي تقرير:

| النقطة                                                        | ما قاله الكاتب في صفحة الأسئلة الشائعة                                                                                                                                                                                     |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| المهلة الصفرية تقطع الاتصال بعد 3 مللي ثانية تقريباً          | قال إن الرقم 3 مللي ثانية لم يكن المهلة نفسها، وإنما رقم وصل إليه في النهاية بعد ساعات من التحقيق                                                                                                                          |
| الضوء يقطع 559 ميلاً في 3 مللي ثانية                          | الاتصال يحتاج أن تذهب الحزمة ويعود الرد، فالزمن الحقيقي ذهاباً وإياباً يجب أن يكون نحو 6 مللي ثانية، وقال إن هذا تفصيل حذفه لأنه ممل                                                                                       |
| هل يحتاج الاتصال المصافحة الثلاثية Three-way Handshake كاملة؟ | يظن أن connect() ينتهي عند وصول SYN-ACK، فلا يحتاج المصافحة كاملة، وهذا صحيح من جهة الكلاينت                                                                                                                               |
| الإشارة تسير بسرعة الضوء                                      | اعترف أنها في النحاس نحو 3c/4، وفي الألياف بين 2c/3 وأقل قليلاً من c، وأنه قاس ping إلى عدة وجهات وكتب المسافة مقابل الزمن ووصل إلى «زمن فعلي» Effective Time تجريبي                                                       |
| شبكة الجامعة Switched بالكامل                                 | قال إنه غير متأكد لماذا أضاف هذه الفقرة، وإن تأخير كل Router إضافي في حدود بضع مئات من الميكروثانية، وإنه كان يستطيع قياسه وطرحه                                                                                           |
| الحزم تسير في خط مستقيم                                       | الدائرة كانت حداً أقصى، وبعض الوجهات داخلها فشلت لأن طريقها أطول من الخط المستقيم أو بسبب تأخير آخر مثل جدار ناري Firewall                                                                                                 |
| Sendmail 5 قرأ ملف Sendmail 8                                 | قيل له إن Sendmail 5 الأصلي لا يقبل هذا الملف، فاستنتج أن نسخة Sun كانت معدلة، وهذا يوافق ما ذكره في القصة، ولاحظ أنه كتب في هذه الإجابة Solaris بينما كتب في القصة SunOS، وهذا مثال آخر على التفاصيل التي تروى من الذاكرة |
| البرنامج جعل الخيارات الناقصة صفراً                           | أكد أن Sendmail في ذلك الوقت لم تكن فيه قيم افتراضية لتلك الخيارات، وأن Eric Allman مؤلف Sendmail قال ذلك في درس حضره، وعندما سمع منه القصة سقط على الأرض متألماً كما كتب مازحاً                                           |

والآن لنحسب بأنفسنا لماذا كان الكاتب محقاً عندما قال إن الأرقام لا تتطابق حرفياً، فالجدول التالي حسبناه بالأداة `units` في تجربتنا، والألياف بسرعة 2c/3 أي نحو 199,862 كيلومتراً في الثانية:

| الافتراض                                                      | الأمر                                       | النتيجة         |
| ------------------------------------------------------------- | ------------------------------------------- | --------------- |
| 3 مللي ثانية في اتجاه واحد بسرعة الضوء في الفراغ (حساب القصة) | units "3 millilightseconds" miles           | 558.8 ميل       |
| 3 مللي ثانية في اتجاه واحد داخل الألياف                       | units -t "3 ms \* c \* 2 / 3" miles         | 372.6 ميل       |
| 3 مللي ثانية ذهاباً وإياباً بسرعة الضوء في الفراغ             | units -t "1.5 ms \* c" miles                | 279.4 ميل       |
| كم يحتاج الذهاب والعودة إلى 520 ميلاً داخل الألياف؟           | units -t "2 \* 520 miles / (c \* 2 / 3)" ms | 8.37 مللي ثانية |

والخلاصة من الجدول أن المهلة الحقيقية التي تفسر حداً قريباً من 520 ميلاً تقع بين 6 و9 مللي ثانية تقريباً حسب الوسط وطول الطريق، وأن الرقم 3 مللي ثانية في القصة هو «الزمن الفعلي» في اتجاه واحد الذي قاسه الكاتب على شبكته، وليس المهلة كما تقيسها ساعة الجهاز. ولكن الفكرة الأساسية صحيحة تماماً: مهلة اتصال بضع مللي ثوان تكفي لتقسم العالم إلى قريب يرد في الوقت وبعيد لا يرد، وهذا بالضبط ما سوف نعيد إنتاجه في التجربة. وكما قال الكاتب في نهاية صفحته، السؤال الحقيقي هو: هل يمكن لإعداد خاطئ أن يجعل البريد القريب يصل والبعيد لا يصل؟ والإجابة نعم.

## أين تجد مهلة الاتصال اليوم؟

مهلة الاتصال Connect Timeout هي أقصى وقت ينتظره البرنامج حتى يكتمل اتصال TCP، أي حتى يصل رد SYN-ACK من السيرفر، وهي غير مهلة القراءة Read Timeout التي تبدأ بعد الاتصال وتنتظر البيانات. وكل برنامج يتصل بالشبكة عنده هذه المهلة، سواءً كتبتها أنت أو تركتها لقيمتها الافتراضية، والفرق كبير بين البرامج، لذلك لنمر على أهمها.

### في Postfix

الخيار هو [smtp\_connect\_timeout](https://www.postfix.org/postconf.5.html?ref=arabroot.io#smtp%5Fconnect%5Ftimeout)، وقيمته الافتراضية 30 ثانية كما رأينا في مخرج `postconf -d`، وعندما تنتهي المهلة يجرب Postfix العنوان التالي في قائمة سجلات MX، فإذا فشلت كلها بقيت الرسالة في الطابور Queue وأعاد المحاولة لاحقاً كما شرحنا في [قسم الطابور وإعادة المحاولة](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%A8%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A7%D9%84%D9%83%D8%AA%D8%B1%D9%88%D9%86%D9%8A-smtp-%D9%88-imap/#queue-retry). والقيمة صفر تعني ترك المهلة لنظام التشغيل، والوحدات المسموحة هي الثواني والدقائق والساعات والأيام والأسابيع، فلا توجد أجزاء من الثانية، وهذا وحده يجعل قصة 500 ميل مستحيلة في Postfix. وإذا رأيت في السجل السطر `connect to …:25: Connection timed out` فقد شرحنا أسبابه الشائعة في [دليل Postfix وDovecot](https://arabroot.io/articles/%D8%AE%D8%A7%D8%AF%D9%85-%D8%A8%D8%B1%D9%8A%D8%AF-postfix-%D9%88-dovecot-%D9%8A%D8%AF%D9%88%D9%8A%D8%A7/#log-timeout)، وأغلبها أن المنفذ 25 محجوب عند مزود السيرفر وليس المسافة.

### في نواة Linux نفسها

عندما لا يضع البرنامج مهلة، تقرر النواة متى تتوقف، والقاعدة في [صفحة tcp في دليل Linux](https://man7.org/linux/man-pages/man7/tcp.7.html?ref=arabroot.io) أن النواة تعيد إرسال حزمة SYN حتى عدد مرات يحدده الإعداد `tcp_syn_retries`، وقيمته الافتراضية 6، وهذا يعني محاولة تستمر نحو 127 ثانية قبل أن يفشل الاتصال. وأول إعادة إرسال تكون بعد ثانية واحدة، وهي القيمة الأولية لمهلة إعادة الإرسال RTO التي يوصي بها [المعيار RFC 6298](https://www.rfc-editor.org/rfc/rfc6298?ref=arabroot.io) قبل أن يعرف الطرفان زمن الذهاب والعودة الحقيقي. وهذه الثانية مهمة جداً عندما تختار أي مهلة قصيرة، كما سوف نرى في [التجربة](#lab-loss).

```bash
cat /proc/sys/net/ipv4/tcp_syn_retries
```

والمخرج داخل Container من `debian:13.1` في تجربتنا سوف يكون كما يلي:

```text
6
```

### في curl

الخيار [\--connect-timeout](https://curl.se/docs/manpage.html?ref=arabroot.io#--connect-timeout) يقبل أجزاء الثانية مثل `0.010`، ويغطي مرحلة الاتصال كاملة: البحث في DNS واتصال TCP والمصافحة TLS أو QUIC إذا كانت موجودة. وإذا لم تضعه فإن مكتبة libcurl تستخدم مهلة افتراضية للاتصال قدرها 300 ثانية كما يذكر [توثيق CURLOPT\_CONNECTTIMEOUT](https://curl.se/libcurl/c/CURLOPT%5FCONNECTTIMEOUT.html?ref=arabroot.io)، وهذه المهلة مستقلة عن `--max-time` التي تحدد زمن العملية كلها.

```bash
curl --connect-timeout 3 --max-time 20 -s -o /dev/null \
  -w "connect=%{time_connect}s tls=%{time_appconnect}s total=%{time_total}s\n" https://example.com/
```

### في مكتبات HTTP داخل تطبيقك

هنا يقع أغلب المطورين في الخطأ المعاكس لقصتنا، فبدلاً من مهلة قصيرة جداً لا توجد مهلة أصلاً. فمكتبة requests في Python مثلاً لا تضع أي مهلة إلا إذا طلبتها، وتوثيقها في [قسم المهلات Timeouts](https://requests.readthedocs.io/en/latest/user/advanced/?ref=arabroot.io#timeouts) يقول إن الكود بدون مهلة قد يتوقف دقائق أو أكثر، وينصح بأن تكون مهلة الاتصال أكبر بقليل من مضاعفات 3 ثوان، وهي نافذة إعادة الإرسال التقليدية في TCP، ويسمح بمهلتين منفصلتين للاتصال والقراءة:

```python
import requests

# 3.05 ثانية للاتصال، و27 ثانية لانتظار الرد بعد الاتصال
r = requests.get("https://api.example.com/orders", timeout=(3.05, 27))
```

والقاعدة العملية التي نخرج بها من القصة ومن هذه التوثيقات: ضع دائماً مهلة اتصال ومهلة قراءة منفصلتين، واجعل مهلة الاتصال أكبر من أسوأ زمن ذهاب وعودة متوقع إلى أبعد سيرفر تتصل به، مضافاً إليها على الأقل إعادة إرسال SYN واحدة، أي أكثر من ثانية، والسبب أن فقد حزمة واحدة في الطريق أمر طبيعي، ومهلة أقل من ثانية تحول كل فقد عابر إلى خطأ يراه المستخدم. وقد شرحنا تكلفة الاتصال الجديد وإعادة استخدامه في [مقال HTTP وHTTPS](https://arabroot.io/articles/http-%D9%88-https-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#connection-cost).

### كم تكلف المسافة فعلاً؟

كما شرحنا في [مقال كيف يصل الإنترنت إلى بيتك](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B5%D9%84-%D8%A7%D9%84%D8%A5%D9%86%D8%AA%D8%B1%D9%86%D8%AA-%D8%A5%D9%84%D9%89-%D8%A8%D9%8A%D8%AA%D9%83/#latency)، الضوء داخل الألياف يسير بنحو 200,000 كيلومتر في الثانية، فكل 100 كيلومتر تضيف قرابة مللي ثانية واحدة إلى زمن الذهاب والعودة RTT، وهذا حد أدنى فيزيائي، والطريق الحقيقي أطول من الخط المستقيم ويضيف كل Router وكل طابور وقته. والجدول التالي يبين هذا الحد الأدنى لمسافات مختلفة، أما القياسات الحقيقية إلى مراكز بيانات في أوروبا والولايات المتحدة فتجدها في [قسم ping في المقال نفسه](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B5%D9%84-%D8%A7%D9%84%D8%A5%D9%86%D8%AA%D8%B1%D9%86%D8%AA-%D8%A5%D9%84%D9%89-%D8%A8%D9%8A%D8%AA%D9%83/#ping):

| المسافة في خط مستقيم                         | أقل RTT ممكن داخل الألياف | هل تكفي مهلة 10ms؟        |
| -------------------------------------------- | ------------------------- | ------------------------- |
| 50 كم، داخل المدينة                          | 0.5 مللي ثانية            | نعم                       |
| 837 كم، أي 520 ميلاً مثل القصة               | 8.4 مللي ثانية            | بالكاد، بدون أي تأخير آخر |
| 4,000 كم، بين قارتين متجاورتين               | 40 مللي ثانية             | لا                        |
| 12,000 كم، إلى الطرف الآخر من العالم تقريباً | 120 مللي ثانية            | لا                        |
| قمر صناعي في المدار الثابت GEO               | 480 مللي ثانية على الأقل  | لا                        |

## تجربة عملية: نصنع «المسافة» على جهازك

في هذه التجربة سوف نبني على جهازك سيرفرين بينهما Router يؤخر كل حزمة عدداً من المللي ثواني نختاره، فيصبح السيرفر «بعيداً» بالقدر الذي نريد، ثم نتصل به بمهلات مختلفة ونقيس. وكل شيء يعمل داخل شبكات Docker خاصة، ولا يوجد أي منفذ مفتوح على جهازك ولا أي اتصال بسيرفر حقيقي خارجي، وكل المخرجات أدناه حقيقية من تجربتنا على Docker Desktop.

### لماذا لا نستخدم tc أو Proxy بسيطاً؟

الطريقة المعتادة لإضافة تأخير على Linux هي `tc qdisc add dev eth0 root netem delay 20ms`، ولكن نواة WSL2 التي يعمل عليها Docker Desktop لا تحتوي على وحدة netem، فيفشل الأمر بالخطأ التالي:

```text
Error: Specified qdisc kind is unknown.
```

والفكرة الثانية التي قد تخطر لك هي Proxy صغير بلغة Python يستقبل الاتصال وينتظر ثم يمرره، ولكن هذا لا يؤخر الاتصال نفسه، والسبب أن النواة عند الـ Proxy ترد على SYN بـ SYN-ACK فوراً وتكمل المصافحة قبل أن يستدعي البرنامج `accept()` أصلاً، وبالتالي سوف يرى الكلاينت اتصالاً سريعاً دائماً، وهو بالضبط الرقم الذي نريد أن نؤخره.

لذلك سوف نستخدم جهاز TUN، وهو جهاز شبكة افتراضي تستقبل منه الحزم الكاملة في برنامج عادي وتعيدها إليه، فالـ Router يوجه كل حزمة عابرة إلى الجهاز `wan-in`، وبرنامج Python صغير يقرؤها وينتظر N مللي ثانية ثم يكتبها في الجهاز `wan-out`، فتكمل النواة توجيهها إلى وجهتها. والصورة التالية تبين المختبر:

![ثلاثة صناديق: على اليمين lab-mail500-client على العنوان 10.50.1.10 باسم mail.near.lab وفيه Postfix للإرسال مع smtp_connect_timeout وcurl مع connect-timeout وsmtplib وswaks، متصل بالشبكة near 10.50.1.0/24 بالـ Router lab-mail500-wan على العنوانين 10.50.1.254 و10.50.2.254 وفيه wan-in ثم wan-delay.py ثم wan-out، ومتصل بالشبكة far 10.50.2.0/24 بالسيرفر lab-mail500-server على العنوان 10.50.2.10 باسم mx.far.lab وفيه Postfix على المنفذ 25 وHTTP على 8080، وتحتها أربع خطوات: الكلاينت يرسل SYN، الحزمة تنتظر N في wan-in ثم تخرج من wan-out، الرد ينتظر N أخرى، الاتصال يكتمل بعد 2N على الأقل](https://arabroot.io/content/images/2026/10/500-mile-email-04-lab.webp)

المختبر: ثلاث Containers وRouter يؤخر كل حزمة N مللي ثانية في كل اتجاه

### ملفات التجربة

أنشئ مجلداً فارغاً وضع فيه الملفات التالية، وأولها ملف Dockerfile لـ Docker Image واحدة تستخدمها ال Containers الثلاث، مبنية على `debian:13.1` وفيها Postfix وswaks وPython وcurl والأداة units:

```dockerfile
FROM debian:13.1
RUN echo "postfix postfix/main_mailer_type select No configuration" | debconf-set-selections \
 && apt-get update \
 && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      postfix swaks python3 iproute2 iputils-ping curl units procps \
 && cp /usr/share/postfix/main.cf.debian /etc/postfix/main.cf \
 && echo "postmaster: root" > /etc/aliases \
 && rm -rf /var/lib/apt/lists/*
```

ثم برنامج الـ Router الذي يصنع «المسافة»، واسمه `wan-delay.py`، وهو يقرأ التأخير من الملف `wan.conf` ويعيد قراءته كلما تغير، فلا نحتاج إعادة تشغيل أي شيء عند تغيير المسافة:

```python
# Userspace "distance": every packet that crosses this router waits DELAY ms.
# Reads /lab/wan.conf (delay_ms=N, drop_first_syn=0|1) and reloads it when it changes.
import fcntl, os, struct, subprocess, threading, time, collections

TUNSETIFF, IFF_TUN, IFF_NO_PI = 0x400454CA, 0x0001, 0x1000
CONF = "/lab/wan.conf"

def tun(name):
    fd = os.open("/dev/net/tun", os.O_RDWR)
    fcntl.ioctl(fd, TUNSETIFF, struct.pack("16sH", name.encode(), IFF_TUN | IFF_NO_PI))
    return fd

def sh(cmd):
    subprocess.run(cmd, shell=True, check=True)

fd_in, fd_out = tun("wan-in"), tun("wan-out")
sh("ip link set wan-in up && ip link set wan-out up")
sh("ip route add default dev wan-in table 100")
sh("ip rule add iif lo lookup main pref 50")          # the router's own packets
sh("ip rule add iif wan-out lookup main pref 100")    # packets that already waited
sh("ip rule add from 10.50.0.0/16 lookup 100 pref 200")  # everything else waits first

state = {"delay": 0.0, "drop": False, "mtime": None}
seen = set()

def reload():
    while True:
        try:
            m = os.stat(CONF).st_mtime
            if m != state["mtime"]:
                kv = dict(l.strip().split("=", 1) for l in open(CONF) if "=" in l)
                state["delay"] = float(kv.get("delay_ms", 0)) / 1000
                state["drop"] = kv.get("drop_first_syn", "0") == "1"
                state["mtime"] = m
                seen.clear()
                print(f"wan: delay={state['delay']*1000:g}ms each way, drop_first_syn={int(state['drop'])}", flush=True)
        except FileNotFoundError:
            pass
        time.sleep(0.2)

queue, cv = collections.deque(), threading.Condition()

def writer():
    while True:
        with cv:
            while not queue:
                cv.wait()
            due, pkt = queue[0]
            wait = due - time.monotonic()
            if wait > 0:
                cv.wait(wait)
                continue
            queue.popleft()
        os.write(fd_out, pkt)

def is_first_syn(p):
    if len(p) < 40 or p[0] >> 4 != 4 or p[9] != 6:
        return False
    ihl = (p[0] & 0x0F) * 4
    flags = p[ihl + 13]
    if not (flags & 0x02) or (flags & 0x10):        # SYN without ACK
        return False
    key = (p[12:16], p[ihl:ihl + 2], p[16:20], p[ihl + 2:ihl + 4])
    if key in seen:
        return False
    seen.add(key)
    return True

threading.Thread(target=reload, daemon=True).start()
threading.Thread(target=writer, daemon=True).start()
while True:
    pkt = os.read(fd_in, 65535)
    if state["drop"] and is_first_syn(pkt):
        continue
    with cv:
        queue.append((time.monotonic() + state["delay"], pkt))
        cv.notify()
```

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

- القواعد `ip rule` هي قلب الفكرة: كل حزمة من الشبكتين تذهب أولاً إلى الجدول 100 الذي يرسلها إلى `wan-in`، أما الحزمة التي تخرج من `wan-out` فقد انتظرت بالفعل، لذلك توجه بالجدول الرئيسي إلى وجهتها الحقيقية، وبدون هذه القاعدة سوف تدور الحزمة في حلقة لا تنتهي.
- التأخير يطبق في كل اتجاه، فإذا كان N يساوي 5 فإن SYN ينتظر 5 مللي ثوان، وSYN-ACK ينتظر 5 أخرى، فيصبح الحد الأدنى لزمن الاتصال 10 مللي ثوان، تماماً كما يحدث على مسافة حقيقية.
- الخيار `drop_first_syn` يحذف أول حزمة SYN في كل اتصال جديد، وسوف نستخدمه في [الخطوة الأخيرة](#lab-loss) لنرى ما يحدث عندما تفقد حزمة واحدة.

ثم سكربت السيرفر البعيد `server.sh`، وهو يضيف الطريق إلى الشبكة القريبة عبر الـ Router، ويجهز Postfix ليستقبل بريد النطاق `far.lab` في صندوق المستخدم `trey`، ويشغل صفحة HTTP صغيرة على المنفذ 8080:

```bash
set -e
ip route add 10.50.1.0/24 via 10.50.2.254
useradd -m trey
postconf -e myhostname=mx.far.lab mydestination='far.lab, localhost' \
  inet_interfaces=all inet_protocols=ipv4 mynetworks=127.0.0.0/8 \
  maillog_file=/var/log/postfix.log home_mailbox=Maildir/
newaliases
cd /tmp && python3 -m http.server 8080 >/dev/null 2>&1 &
exec postfix start-fg
```

ثم سكربت الكلاينت `client.sh`، وهو سيرفر بريد القسم الذي يرسل، ويوجه كل بريد `far.lab` إلى السيرفر البعيد مباشرة بدون DNS عبر ملف `transport`:

```bash
set -e
ip route add 10.50.2.0/24 via 10.50.1.254
echo "far.lab smtp:[10.50.2.10]:25" > /etc/postfix/transport
postmap /etc/postfix/transport
postconf -e myhostname=mail.near.lab mydestination= inet_interfaces=loopback-only \
  inet_protocols=ipv4 transport_maps=hash:/etc/postfix/transport \
  maillog_file=/var/log/postfix.log
newaliases
exec postfix start-fg
```

ثم ملف Docker Compose الذي يربط كل ذلك:

```yaml
name: lab-mail500
x-common: &common
  image: lab-mail500-img:1
  cap_add: [NET_ADMIN]
  volumes: ["./:/lab"]
services:
  wan:
    <<: *common
    build: .
    container_name: lab-mail500-wan
    devices: ["/dev/net/tun:/dev/net/tun"]
    sysctls:
      net.ipv4.ip_forward: "1"
      net.ipv4.conf.all.rp_filter: "0"
      net.ipv4.conf.default.rp_filter: "0"
      net.ipv4.conf.all.send_redirects: "0"
      net.ipv4.conf.default.send_redirects: "0"
    command: python3 -u /lab/wan-delay.py
    networks:
      near: {ipv4_address: 10.50.1.254}
      far: {ipv4_address: 10.50.2.254}
  server:
    <<: *common
    container_name: lab-mail500-server
    hostname: mx.far.lab
    command: sh /lab/server.sh
    networks:
      far: {ipv4_address: 10.50.2.10}
  client:
    <<: *common
    container_name: lab-mail500-client
    hostname: mail.near.lab
    command: sh /lab/client.sh
    networks:
      near: {ipv4_address: 10.50.1.10}
networks:
  near:
    ipam: {config: [{subnet: 10.50.1.0/24}]}
  far:
    ipam: {config: [{subnet: 10.50.2.0/24}]}
```

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

- الـ Router وحده يحصل على الجهاز `/dev/net/tun` وعلى إعدادات `sysctls` التي تفعل التوجيه IP Forwarding وتوقف فحص المصدر `rp_filter`، والسبب أن الحزمة تخرج من `wan-out` بعنوان مصدر لا يتوقعه هذا الجهاز، فيرفضها فحص المصدر لو بقي فعالاً.
- الصلاحية `NET_ADMIN` في كل Container لأن السكربتات تضيف طرقاً Routes، وهي صلاحية داخل شبكة الـ Container فقط ولا تمس شبكة جهازك.
- لا يوجد أي `ports` في الملف، فلا يوجد منفذ مفتوح على جهازك، وكل الاختبارات تتم من داخل الـ Container الكلاينت.

وأخيراً برنامج صغير بلغة Python يرسل رسالة بمهلة اتصال بالمللي ثانية، مثل خيار `Timeout.connect` في Sendmail، ثم يرجع إلى مهلة عادية بعد الاتصال، واسمه `smtp-try.py`:

```python
# Sends one message, with a separate (very short) connect timeout like Sendmail's Timeout.connect
import smtplib, socket, sys, time
from email.message import EmailMessage

HOST, CONNECT_TIMEOUT = sys.argv[1], float(sys.argv[2]) / 1000   # milliseconds

class SMTP(smtplib.SMTP):
    def _get_socket(self, host, port, timeout):
        t = time.perf_counter()
        try:
            sock = socket.create_connection((host, port), CONNECT_TIMEOUT)
        finally:
            self.connect_ms = (time.perf_counter() - t) * 1000
        sock.settimeout(30)          # after connect, normal timeouts for the conversation
        return sock

msg = EmailMessage()
msg["From"], msg["To"], msg["Subject"] = "stats@near.lab", "trey@far.lab", "distance test"
msg.set_content("hello from the statistics department\n")

s = SMTP()
try:
    s.connect(HOST, 25)
    s.send_message(msg)
    s.quit()
    print(f"sent    connect={s.connect_ms:.1f}ms")
except TimeoutError as e:
    print(f"FAILED  connect gave up after {s.connect_ms:.1f}ms: {e}")
```

ولاحظ أن الدالة `_get_socket` جزء داخلي في [مكتبة smtplib](https://docs.python.org/3/library/smtplib.html?ref=arabroot.io) وليست واجهة رسمية، واستخدمناها هنا فقط لنفصل مهلة الاتصال عن مهلة المحادثة في تجربة، أما في كود الإنتاج فاستخدم المعامل `timeout` الرسمي.

### التشغيل

من داخل المجلد نجهز ملف التأخير بقيمة صفر ثم نشغل المختبر:

```bash
printf 'delay_ms=0\ndrop_first_syn=0\n' > wan.conf
docker compose up -d --build
docker compose ps --format 'table {{.Name}}\t{{.Status}}'
docker logs lab-mail500-wan
```

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

```text
NAME                 STATUS
lab-mail500-client   Up 7 seconds
lab-mail500-server   Up 7 seconds
lab-mail500-wan      Up 6 seconds
wan: delay=0ms each way, drop_first_syn=0
```

ولتغيير «المسافة» بسهولة نعرف في الـ Shell دالة صغيرة تكتب الملف من داخل الـ Router ثم تنتظر ثانية حتى يقرأه البرنامج:

```bash
setwan() { docker exec lab-mail500-wan sh -c "printf 'delay_ms=$1\ndrop_first_syn=${2:-0}\n' > /lab/wan.conf"; sleep 1; }
```

وإذا كنت تستخدم Git Bash على Windows فاكتب قبل الأوامر `export MSYS_NO_PATHCONV=1`، حتى لا يحول Git Bash مسارات مثل `/lab/wan.conf` إلى مسارات Windows.

### خط الأساس، ثم 10 مللي ثوان في كل اتجاه

أولاً نقيس بدون أي تأخير، والأمر `curl -w` يطبع توقيت كل مرحلة في الطلب:

```bash
docker exec lab-mail500-client ping -c 5 -q 10.50.2.10
docker exec lab-mail500-client curl -s -o /dev/null \
  -w "dns=%{time_namelookup}s connect=%{time_connect}s first_byte=%{time_starttransfer}s total=%{time_total}s\n" \
  http://10.50.2.10:8080/
```

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

```text
PING 10.50.2.10 (10.50.2.10) 56(84) bytes of data.

--- 10.50.2.10 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4150ms
rtt min/avg/max/mdev = 0.385/0.475/0.607/0.072 ms
dns=0.000016s connect=0.000643s first_byte=0.002226s total=0.002279s
```

ثم نجعل التأخير 10 مللي ثوان في كل اتجاه ونكرر الأمرين نفسيهما:

```bash
setwan 10
docker logs --tail 1 lab-mail500-wan
```

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

```text
wan: delay=10ms each way, drop_first_syn=0
PING 10.50.2.10 (10.50.2.10) 56(84) bytes of data.

--- 10.50.2.10 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4007ms
rtt min/avg/max/mdev = 20.628/20.713/20.810/0.058 ms
dns=0.000012s connect=0.020909s first_byte=0.042463s total=0.042532s
```

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

- زمن `ping` صار 20.7 مللي ثانية، أي 10 ذهاباً و10 إياباً، وهذا ما يحدث مع سيرفر على بعد نحو 1,000 كيلومتر داخل الألياف بدون أي تأخير آخر.
- القيمة `connect` صارت 0.0209 ثانية، أي ذهاباً وعودة مرة واحدة، لأن الاتصال يكتمل عند وصول SYN-ACK.
- القيمة `first_byte` صارت 0.042، أي ضعف زمن الاتصال، لأن الطلب يحتاج ذهاباً وعودة أخرى بعد الاتصال، وهذه هي تكلفة المسافة التي تتكرر مع كل اتصال جديد.

### مهلة 10 مللي ثوان في curl: أين ينقطع البريد؟

الآن نعيد قصة الـ 500 ميل بأرقامنا، فنضع مهلة اتصال 10 مللي ثوان، ونزيد «المسافة» تدريجياً:

```bash
for d in 1 2 3 4 5 6 8; do
  setwan $d
  docker exec lab-mail500-client curl -sS -o /dev/null --connect-timeout 0.010 \
    -w "one-way=${d}ms connect=%{time_connect}s code=%{http_code}\n" http://10.50.2.10:8080/
done
```

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

```text
one-way=1ms connect=0.002981s code=200
one-way=2ms connect=0.005159s code=200
one-way=3ms connect=0.006888s code=200
one-way=4ms connect=0.008938s code=200
curl: (28) Connection timed out after 10 milliseconds
one-way=5ms connect=0.000000s code=000
curl: (28) Connection timed out after 10 milliseconds
one-way=6ms connect=0.000000s code=000
curl: (28) Connection timed out after 10 milliseconds
one-way=8ms connect=0.000000s code=000
```

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

- زمن الاتصال يساوي تقريباً ضعف التأخير في اتجاه واحد مع نحو مللي ثانية زائدة من البرنامج والـ Router نفسه، فـ 4 مللي ثوان أعطت 8.9.
- عند 5 مللي ثوان في كل اتجاه يحتاج الاتصال 10 مللي ثوان على الأقل، فتنتهي المهلة قبل وصول الرد، ويخرج curl بالرمز 28 وهو رمز انتهاء المهلة.
- الحد هنا حاد تماماً مثل الدائرة في خريطة قسم الإحصاء: كل ما هو أقرب من الحد ينجح دائماً، وكل ما هو أبعد يفشل دائماً، ولا توجد حالة بينهما.

وبحساب الألياف، أي 200 كيلومتر لكل مللي ثانية في اتجاه واحد، فإن مهلة 10 مللي ثوان تعني أن أقصى مسافة ممكنة نظرياً نحو 1,000 كيلومتر، وفي تجربتنا كان آخر نجاح على 4 مللي ثوان في كل اتجاه، أي نحو 800 كيلومتر، لأن البرنامج والـ Router يضيفان نحو مللي ثانية إلى كل اتصال.

### مهلة 6 مللي ثوان في SMTP، مثل القصة

الآن نرسل بريداً حقيقياً من الكلاينت إلى Postfix البعيد بالبرنامج `smtp-try.py` ومهلة اتصال 6 مللي ثوان، وهي المهلة ذهاباً وإياباً التي تقابل 3 مللي ثوان في اتجاه واحد كما في القصة:

```bash
for d in 1 2 3 4; do
  setwan $d
  printf "one-way=%sms  " $d
  docker exec lab-mail500-client python3 /lab/smtp-try.py 10.50.2.10 6
done
```

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

```text
one-way=1ms  sent    connect=2.7ms
one-way=2ms  sent    connect=4.7ms
one-way=3ms  FAILED  connect gave up after 6.3ms: timed out
one-way=4ms  FAILED  connect gave up after 6.3ms: timed out
```

أي أن هذا «القسم» يستطيع أن يرسل البريد إلى سيرفر على بعد 2 مللي ثانية، نحو 400 كيلومتر في الألياف، ولا يستطيع الإرسال إلى سيرفر على بعد 3 مللي ثوان، نحو 600 كيلومتر، وهذا هو الحد الذي حسبناه في [جدول الدقة](#accuracy): 373 ميلاً تقريباً داخل الألياف. ولاحظ أن الرسالة التي تفشل لا تصل إلى السيرفر أصلاً، فلا يوجد أي أثر لها في سجله، وهذا ما يجعل هذا النوع من الأعطال صعباً، فالطرف الآخر لا يرى شيئاً.

وقد يتساءل البعض: وماذا بعد الاتصال؟ هل يكلف كل أمر في SMTP ذهاباً وعودة أخرى؟ والإجابة نعم، والأداة swaks تعرض زمن كل رد إذا أضفت الخيار `--show-time-lapse`، والمثال التالي مع تأخير 4 مللي ثوان في كل اتجاه:

```bash
setwan 4
docker exec lab-mail500-client swaks --to trey@far.lab --from stats@near.lab \
  --server 10.50.2.10 --helo mail.near.lab --show-time-lapse --suppress-data
```

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

```text
=== Trying 10.50.2.10:25...
=== Connected to 10.50.2.10.
=== response in 0.927s
<-  220 mx.far.lab ESMTP Postfix (Debian)
 -> EHLO mail.near.lab
=== response in 0.009s
<-  250-mx.far.lab
<-  250-PIPELINING
<-  250-SIZE 10240000
<-  250-VRFY
<-  250-ETRN
<-  250-ENHANCEDSTATUSCODES
<-  250-8BITMIME
<-  250-DSN
<-  250-SMTPUTF8
<-  250 CHUNKING
 -> MAIL FROM:<stats@near.lab>
=== response in 0.009s
<-  250 2.1.0 Ok
 -> RCPT TO:<trey@far.lab>
=== response in 0.010s
<-  250 2.1.5 Ok
 -> DATA
=== response in 0.009s
<-  354 End data with <CR><LF>.<CR><LF>
 -> 11 lines sent
=== response in 0.012s
<-  250 2.0.0 Ok: queued as 66E7B21291
 -> QUIT
=== response in 0.009s
<-  221 2.0.0 Bye
=== Connection closed with remote host.
```

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

- كل أمر بعد الاتصال أخذ نحو 9 مللي ثوان، أي زمن ذهاب وعودة كاملاً، فالرسالة الواحدة تكلف ستة أضعاف هذا الزمن على الأقل، ولهذا يدعم Postfix الإرسال المتتابع PIPELINING وإعادة استخدام الاتصال كما شرحنا في [قسم إعادة استخدام اتصال SMTP](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%A8%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A7%D9%84%D9%83%D8%AA%D8%B1%D9%88%D9%86%D9%8A-smtp-%D9%88-imap/#connection-reuse).
- رسالة الترحيب `220` وحدها أخذت 0.927 ثانية، وهذا ليس من المسافة، والسبب أن Postfix في السيرفر يبحث عن اسم الكلاينت في DNS قبل أن يرحب به، وهذا سلوك الخيار [smtpd\_peername\_lookup](https://www.postfix.org/postconf.5.html?ref=arabroot.io#smtpd%5Fpeername%5Flookup)، وفي تشغيل آخر لنفس الأمر كان الزمن 0.098 ثانية، وهذا مثال حي على ما قاله Trey عن الوجهات التي فشلت داخل الدائرة: تأخير آخر غير المسافة يكفي لتتجاوز المهلة.

### Postfix بمهلة ثانية واحدة

الآن نجرب سيرفر بريد حقيقياً يرسل من الطابور، فنضع في Postfix الكلاينت المهلة `smtp_connect_timeout = 1s`، وهي أقل قيمة ممكنة فيه، ثم نرسل رسالة إلى سيرفر على بعد 300 مللي ثانية في كل اتجاه، وأخرى إلى سيرفر على بعد 600:

```bash
docker exec lab-mail500-client sh -c 'postconf -e smtp_connect_timeout=1s && postfix reload'

setwan 300
docker exec lab-mail500-client sh -c 'printf "Subject: 300ms away\n\nnear enough\n" | sendmail -f stats@near.lab trey@far.lab'
sleep 5
setwan 600
docker exec lab-mail500-client sh -c 'printf "Subject: 600ms away\n\ntoo far\n" | sendmail -f stats@near.lab trey@far.lab'
sleep 5
docker exec lab-mail500-client sh -c 'grep "postfix/smtp\[" /var/log/postfix.log | tail -3'
docker exec lab-mail500-client postqueue -p
```

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

```text
Oct 08 15:44:52 mail postfix/smtp[297]: 2A8732129B: to=<trey@far.lab>, relay=10.50.2.10[10.50.2.10]:25, delay=3.3, delays=0.07/0.01/2/1.3, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 7EEB62129C)
Oct 08 15:44:56 mail postfix/smtp[297]: connect to 10.50.2.10[10.50.2.10]:25: Connection timed out
Oct 08 15:44:56 mail postfix/smtp[297]: E94D22129C: to=<trey@far.lab>, relay=none, delay=1, delays=0.01/0/1/0, dsn=4.4.1, status=deferred (connect to 10.50.2.10[10.50.2.10]:25: Connection timed out)
-Queue ID-  --Size-- ----Arrival Time---- -Sender/Recipient-------
E94D22129C      269 Thu Oct  8 15:44:55  stats@near.lab
                  (connect to 10.50.2.10[10.50.2.10]:25: Connection timed out)
                                         trey@far.lab

-- 0 Kbytes in 1 Request.
```

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

- الرسالة الأولى وصلت `status=sent`، والحقل `delays` فيه أربعة أرقام: الانتظار قبل مدير الطابور، ثم داخل الطابور، ثم زمن إعداد الاتصال مع الترحيب وEHLO، ثم زمن إرسال الرسالة، والرقمان الأخيران 2 و1.3 ثانية كلها من المسافة التي صنعناها.
- الرسالة الثانية لم تفشل نهائياً، وإنما تأجلت `status=deferred` بالرمز `4.4.1`، وهو رمز خطأ مؤقت، وبقيت في الطابور ليعيد Postfix المحاولة لاحقاً، وهذا أفضل بكثير مما حدث في القصة.
- الحقل `delays=0.01/0/1/0` يبين أن Postfix انتظر ثانية واحدة بالضبط في مرحلة الاتصال ثم توقف، لأن الرد يحتاج 1.2 ثانية.

والآن نقرب السيرفر إلى 300 مللي ثانية ونطلب من Postfix تفريغ الطابور فوراً بدلاً من انتظار المحاولة التالية:

```bash
setwan 300
docker exec lab-mail500-client postqueue -f
sleep 5
docker exec lab-mail500-client sh -c 'grep "postfix/smtp\[" /var/log/postfix.log | tail -1; postqueue -p'
```

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

```text
Oct 08 15:45:06 mail postfix/smtp[297]: E94D22129C: to=<trey@far.lab>, relay=10.50.2.10[10.50.2.10]:25, delay=11, delays=7.5/0/1.9/1.2, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as A53B72129F)
Mail queue is empty
```

وأخيراً نعيد المسافة إلى 600 مللي ثانية ونضع المهلة صفراً، وهي في Postfix تعني ترك القرار للنواة، أي عكس معناها في قصة Sendmail 5:

```bash
setwan 600
docker exec lab-mail500-client sh -c 'postconf -e smtp_connect_timeout=0 && postfix reload'
docker exec lab-mail500-client sh -c 'printf "Subject: zero\n\nfar but fine\n" | sendmail -f stats@near.lab trey@far.lab'
sleep 8
docker exec lab-mail500-client sh -c 'grep "postfix/smtp\[" /var/log/postfix.log | tail -1'
```

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

```text
Oct 08 15:45:19 mail postfix/smtp[370]: D9B2B2129F: to=<trey@far.lab>, relay=10.50.2.10[10.50.2.10]:25, delay=6.3, delays=0.07/0.01/3.7/2.5, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 3D1D421291)
```

والرسالة وصلت رغم أن الرد يحتاج 1.2 ثانية، وتستطيع التأكد من الطرف الآخر بالأمر `docker exec lab-mail500-server grep "postfix/local\[" /var/log/postfix.log`، وستجد سطراً لكل رسالة وصلت ينتهي بالعبارة `delivered to maildir`.

### حزمة SYN واحدة مفقودة تساوي ثانية كاملة

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

```bash
setwan 0 1
docker logs --tail 1 lab-mail500-wan
for i in 1 2 3; do
  docker exec lab-mail500-client curl -sS -o /dev/null -w "connect=%{time_connect}s code=%{http_code}\n" http://10.50.2.10:8080/
done
docker exec lab-mail500-client curl -sS -o /dev/null --connect-timeout 0.5 \
  -w "connect=%{time_connect}s code=%{http_code}\n" http://10.50.2.10:8080/
```

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

```text
wan: delay=0ms each way, drop_first_syn=1
connect=1.080385s code=200
connect=1.034258s code=200
connect=1.078998s code=200
curl: (28) Connection timed out after 502 milliseconds
connect=0.000000s code=000
```

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

- السيرفر على بعد أقل من مللي ثانية، ومع ذلك أخذ الاتصال أكثر من ثانية، والسبب أن النواة انتظرت مهلة إعادة الإرسال الأولى، وهي ثانية واحدة حسب RFC 6298، ثم أرسلت SYN مرة أخرى فنجح الاتصال.
- بمهلة نصف ثانية فشل الاتصال بسيرفر قريب جداً، فالمهلة التي تقل عن ثانية لا تقسم العالم حسب المسافة فقط، وإنما تحول كل حزمة SYN مفقودة إلى خطأ، حتى في الشبكة نفسها.

ثم نعيد الـ Router إلى وضعه الطبيعي:

```bash
setwan 0 0
```

### أقصى مسافة لكل مهلة

بجمع أرقام التجربة مع الحساب، نحصل على الجدول التالي، والمسافة فيه هي الحد الأقصى النظري داخل الألياف بدون أي تأخير آخر، أي نصف المهلة مضروباً في 200 كيلومتر لكل مللي ثانية:

| المهلة       | أين رأيناها                  | أقصى مسافة نظرياً              | ما حدث في التجربة                            |
| ------------ | ---------------------------- | ------------------------------ | -------------------------------------------- |
| 6 مللي ثوان  | smtp-try.py                  | 600 كم، نحو 373 ميلاً          | نجح على 2ms وفشل على 3ms في كل اتجاه         |
| 10 مللي ثوان | curl --connect-timeout 0.010 | 1,000 كم                       | نجح على 4ms وفشل على 5ms في كل اتجاه         |
| نصف ثانية    | curl --connect-timeout 0.5   | 50,000 كم                      | فشل على مسافة صفر بسبب حزمة SYN مفقودة واحدة |
| ثانية واحدة  | smtp\_connect\_timeout = 1s  | 100,000 كم، أكثر من محيط الأرض | نجح على 300ms وتأجل على 600ms في كل اتجاه    |
| 30 ثانية     | القيمة الافتراضية في Postfix | لا تحدها المسافة على الأرض     | تتحمل عدة إعادات إرسال للحزمة SYN            |

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

### حذف التجربة

نحذف ال Containers والشبكتين بالأمر `down` للمشروع نفسه، ثم نحذف الـ Docker Image التي بنيناها باسمها:

```bash
docker compose down
docker image rm lab-mail500-img:1
```

والمخرج سوف يكون كما يلي، مختصراً:

```text
 Container lab-mail500-client Removed
 Container lab-mail500-server Removed
 Container lab-mail500-wan Removed
 Network lab-mail500_near Removed
 Network lab-mail500_far Removed
Untagged: lab-mail500-img:1
```

## ماذا نتعلم كمهندسين؟

- **خذ البلاغ الغريب بجدية:** المستخدم يصف العرض بلغته، وقد يكون تفسيره خطأ، ولكن العرض نفسه غالباً حقيقي، وفي هذه القصة كان البلاغ دقيقاً لدرجة أنه جاء مع خريطة، لذلك لا تقم بإغلاق بلاغ لأنه يبدو مستحيلاً، والسبب أن «مستحيل» تعني فقط أن النموذج الذي في رأسك عن النظام ناقص.
- **البيانات قبل الفرضيات:** لم يبدأ Trey بتعديل أي إعداد، وإنما أرسل رسائل إلى مدن معروفة المسافة، ثم صمم تجربة ضابطة بصديق بريده في Seattle، وكل نظرية جاءت بعد ذلك كانت تشرح أرقاماً حقيقية، كما شرحنا في [دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#method).
- **ابحث عما تغير، وليس فقط عمن غيره:** «لم ألمس نظام البريد» كانت صحيحة حرفياً، ولكن تحديث نظام التشغيل غير البرنامج الذي يقرأ الإعداد، لذلك اقرأ سجل الحزم بعد كل تحديث، وتأكد من إصدار كل خدمة مهمة، ولا تكتف بمقارنة ملفات الإعداد.
- **الإعداد والبرنامج زوج واحد:** ملف مكتوب لإصدار 8 ويقرؤه إصدار 5 هو إعداد غير صحيح حتى لو لم يتغير فيه حرف، لذلك اجعل تحذيرات مثل `unused parameter` في Postfix جزءاً من فحص ما بعد التحديث، وفضل البرامج التي ترفض الإعداد الذي لا تفهمه أو تحذر منه على البرامج التي تتجاهله بصمت.
- **أصلح أولاً ثم افهم:** أعاد Trey البرنامج الصحيح فوراً، واحتفظ بالقديم ليفهم المشكلة على مهل، والمستخدم يحتاج الخدمة الآن، والفهم يمكن أن ينتظر ساعة.
- **المهلات فيزياء:** كل 100 كيلومتر في الألياف تكلف مللي ثانية تقريباً ذهاباً وإياباً، وكل حزمة SYN مفقودة تكلف ثانية، لذلك احسب مهلة الاتصال من أبعد وجهة ومن ثانية إعادة الإرسال، وضع دائماً مهلة اتصال ومهلة قراءة منفصلتين، ولا تترك أي اتصال بدون مهلة.
- **اقرأ الأرقام في القصص بحذر:** الكاتب نفسه قال إن رقم 3 مللي ثوان تقريبي وإن قصته حكاية وليست تقرير حادثة، وهذا لا ينقص من قيمتها، ولكنه يذكرك أن تفرق دائماً بين ما قيس وما يروى من الذاكرة، وأن تكتب ملاحظاتك وقت التحقيق، فالكاتب تمنى لاحقاً لو احتفظ بها.

## الخلاصة

- في 24 نوفمبر 2002 روى Trey Harris في قائمة sage-members كيف وصله بلاغ من قسم الإحصاء في جامعة North Carolina at Chapel Hill بأن البريد لا يصل لأبعد من 500 ميل تقريباً، ويقدر أن القصة حدثت بين 1994 و1997.
- التحقيق: سؤال ما الذي تغير، ثم رسائل اختبار إلى مدن معروفة المسافة، ثم تجربة ضابطة تثبت أن المسافة إلى السيرفر هي العامل، ثم `diff` لملف الإعداد، ثم رسالة الترحيب على المنفذ 25 التي كشفت Sendmail 5.
- السبب: تحديث SunOS أرجع Sendmail إلى الإصدار 5 وترك ملف إعداد الإصدار 8، فتجاهل البرنامج الخيارات الطويلة وجعلها صفراً لعدم وجود قيم افتراضية، ومهلة الاتصال الصفرية كانت تقطع `connect()` بعد نحو 3 مللي ثوان.
- الحساب: الضوء يقطع 558.8 ميلاً في 3 مللي ثوان في الفراغ، والكاتب نفسه أوضح أن الرقم «زمن فعلي» قاسه، وأن الحساب الدقيق يحتاج الذهاب والعودة وسرعة الإشارة في الألياف والطريق الحقيقي.
- اليوم: الصفر في Postfix وSendmail 8 يعني ترك المهلة للنواة، وأقل وحدة في Postfix ثانية، والنواة تعيد إرسال SYN بعد ثانية وتستمر نحو 127 ثانية، وcurl يقبل أجزاء الثانية، وrequests في Python بدون مهلة إلا إذا طلبتها.
- التجربة: بتأخير نصنعه بجهاز TUN رأينا مهلة 10 مللي ثوان تنجح حتى 4 مللي ثوان في كل اتجاه وتفشل بعدها، ومهلة 6 مللي ثوان تعيد القصة بحد نحو 600 كيلومتر، وحزمة SYN مفقودة واحدة تجعل الاتصال القريب يأخذ أكثر من ثانية.

وإذا أردت أن تكمل من هنا، فهذه المقالات تشرح البريد والشبكات وطريقة التحقيق التي مر ذكرها:

[![](https://arabroot.io/content/images/2026/10/how-email-works-cover-cover.webp)دليلكيف يعمل البريد الإلكتروني؟ SMTP وIMAP للمبتدئينماذا يحدث بين ضغطك على «إرسال» ووصول الرسالة، ولماذا ترتد بعض الرسائل، وأي منفذ تفتح لكل غرض.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%A8%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A7%D9%84%D9%83%D8%AA%D8%B1%D9%88%D9%86%D9%8A-smtp-%D9%88-imap/)[![](https://arabroot.io/content/images/2026/10/postfix-dovecot-cover-cover.webp)دليلبناء خادم بريد بيدك مع Postfix وDovecot على Ubuntuنبني خادم بريد كاملاً من حزم Ubuntu دون Docker، فتعرف دور كل مكون وأين تبحث عندما تتوقف رسالة.](https://arabroot.io/articles/%D8%AE%D8%A7%D8%AF%D9%85-%D8%A8%D8%B1%D9%8A%D8%AF-postfix-%D9%88-dovecot-%D9%8A%D8%AF%D9%88%D9%8A%D8%A7/)[![](https://arabroot.io/content/images/2026/10/dns-records-cover-cover.webp)دليلشرح DNS وسجلاته للمبتدئين بمثال حقيقيكيف يتحول اسم نطاقك إلى عنوان سيرفرك، وما وظيفة كل سجل تضيفه في لوحة النطاق، والأخطاء التي تضيع فيها الساعات عادة.](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-dns-%D9%88%D8%B3%D8%AC%D9%84%D8%A7%D8%AA%D9%87-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/internet-connection-cover-cover.webp)دليلكيف يصل الإنترنت إلى بيتك؟ من ال Router إلى الكابلات البحريةنتتبع طلباً واحداً من حاسوبك إلى سيرفر في بلد آخر ثم عودته، فتعرف من يحمله في كل محطة ولماذا يتأخر الرد.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B5%D9%84-%D8%A7%D9%84%D8%A5%D9%86%D8%AA%D8%B1%D9%86%D8%AA-%D8%A5%D9%84%D9%89-%D8%A8%D9%8A%D8%AA%D9%83/)[![](https://arabroot.io/content/images/2026/10/http-basics-cover-cover.webp)دليلHTTP وHTTPS ببساطة: ماذا يحدث بين الرابط والصفحةماذا يحدث بين كتابة الرابط وظهور الصفحة، وكيف تقرأ الطلب والرد ورموز مثل 502 و504، ولماذا يكلف الاتصال الجديد أكثر.](https://arabroot.io/articles/http-%D9%88-https-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/troubleshooting-guide-cover-cover.webp)دليلاستكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السببعندما يبطئ الموقع أو يعطي 502، هذه طريقة منظمة توصلك من العرض إلى السبب قبل أن تخمن أو تشتري سيرفراً أكبر.](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/katas-network-cover-cover.webp)دليلتمارين Kata في الشبكات بثلاثة مستويات مع الحلولتمارين في الشبكات تحلها بيدك دون ذكاء اصطناعي، من حساب الشبكة الفرعية إلى الفرق بين 502 و504، وكل حل مجرب في مختبر Docker.](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D9%81%D9%8A-%D8%A7%D9%84%D8%B4%D8%A8%D9%83%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/xz-investigation-cover-cover.webp)دليلكيف اكتشف Andres Freund الباب الخلفي في XZخطوات التحقيق والأوامر التي قادت مطوراً لاحظ نصف ثانية زائدة في دخول SSH إلى باب خلفي في مكتبة XZ، وكيف تكررها على سيرفرك.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%A7%D9%83%D8%AA%D8%B4%D9%81-andres-freund-%D8%A7%D9%84%D8%A8%D8%A7%D8%A8-%D8%A7%D9%84%D8%AE%D9%84%D9%81%D9%8A-%D9%81%D9%8A-xz/)

## سجل التحديثات (Changelog)

- أكتوبر 2026: كتابة المقال اعتماداً على منشور Trey Harris الأصلي في 2002 وصفحة الأسئلة الشائعة التي كتبها عن القصة، ودليل تشغيل Sendmail 8 وتوثيق Postfix وcurl وrequests وصفحة tcp في دليل Linux وRFC 6298، مع تجربة عملية على Debian 13.1 وPostfix 3.10.13 وcurl 8.14.1 وPython 3.13 ورسوم توضيحية من عملنا.