في مقال لماذا تتمرن دون ذكاء اصطناعي؟ شرحنا لماذا تضعف المهارة التي لا تستخدمها، وكيف تتمرن بطريقة Kata وحدك أو مع فريقك، وهذا المقال يطبق الطريقة على الشبكات Networking. والسبب أن أغلب البلاغات التي تصلك بعبارة «الموقع لا يفتح» أو «يفتح عندي ولا يفتح عند العميل» هي في الحقيقة سؤال عن الشبكة، فإما أن الاسم لا يحل، أو أن المنفذ لا يجيب، أو أن الشهادة غير صحيحة، أو أن ال Reverse Proxy لا يصل إلى التطبيق، وكل واحدة من هذه لها رسالة مختلفة تعرفها إذا قرأتها بنفسك عشرات المرات، ولا تعرفها إذا كنت تلصقها في أداة وتنتظر الجواب.
وطريقة الاستخدام قصيرة: أغلق أي مساعد ذكاء اصطناعي، واقرأ التمرين، واضبط مؤقتاً على الوقت المقترح، واكتب إجابتك كاملة على ورقة أو في ملف قبل أن تشغل أي أمر. أغلب تمارين المستوى الأول تحل بالورقة والقلم، أما البقية فتحتاج المختبر Lab الذي سوف نجهزه بعد قليل، وإذا علقت فافتح التلميح Hint ثم حاول مرة أخرى، وبعدها فقط افتح الحل Answer وقارنه بإجابتك، وفي النهاية سجل الوقت في جدول المتابعة وارجع إلى التمرين بعد أسبوع.
وسوف نناقش في هذا المقال ما يلي:
- تجهيز مختبر Lab في Docker فيه سيرفر DNS محلي وNginx وتطبيق خلفه وجهاز كلاينت عليه
digوcurlوopenssl. - المستوى الأول: عدد الأجهزة في الشبكة الفرعية Subnet، وهل العنوان داخل الشبكة، والعناوين الخاصة والعامة وCGNAT، وقراءة مخرج
digوcurl -vوss. - المستوى الثاني: تقسيم شبكة /24 إلى أربع، وجهازان على /23، ولماذا يظهر العنوان القديم بعد تغيير السجل، و301 أم 302، وتوقيتات
curl -w، وحالة TIME-WAIT. - المستوى الثالث: توجيه المنافذ Port Forwarding عبر NAT، وسجل CNAME على النطاق الرئيسي، وقراءة الشهادة ب
openssl s_client، والفرق بين 502 و504، وسلم تشخيص «الموقع لا يفتح». - جدول متابعة تنسخه لتسجل تقدمك، وروابط المقالات التي تشرح كل موضوع بالتفصيل.
والصورة التالية تبين التمارين السبعة عشر موزعة على المستويات الثلاثة:

تجهيز المختبر Lab
كل التمارين التي تحتاج أوامر تعمل على شبكة Docker خاصة واحدة اسمها kata-net بالنطاق 10.77.0.0/24، وفيها خمسة Containers: سيرفر DNS يحمل سجلات النطاق التجريبي shop.lab.test عبر dnsmasq، وسيرفر DNS ثان يعمل كذاكرة مؤقتة Cache مثل الذي يعطيك إياه مزود الإنترنت، وتطبيق صغير يرد على ثلاثة عناوين بسرعات مختلفة، وNginx أمامه، وجهاز كلاينت نشغل منه الأوامر. والنطاق .test محجوز للتجارب ولا يمكن أن يتعارض مع نطاق حقيقي على الإنترنت.
ابدأ بإنشاء مجلد للمختبر وفيه ملف إعداد Nginx التالي باسم nginx/default.conf:
server {
listen 80;
server_name shop.lab.test;
root /usr/share/nginx/html;
location = /old-price-list { return 301 /price-list; }
location = /offers { return 302 /ramadan-offers; }
location = /price-list { default_type text/plain; return 200 "price list\n"; }
location /api/ {
proxy_pass http://10.77.0.20:8080/cgi-bin/;
proxy_connect_timeout 2s;
proxy_read_timeout 3s;
}
}
server {
listen 443 ssl;
server_name shop.lab.test;
ssl_certificate /etc/nginx/certs/leaf.crt;
ssl_certificate_key /etc/nginx/certs/leaf.key;
root /usr/share/nginx/html;
}
server {
listen 8443 ssl;
server_name shop.lab.test;
ssl_certificate /etc/nginx/certs/fullchain.crt;
ssl_certificate_key /etc/nginx/certs/leaf.key;
root /usr/share/nginx/html;
location /api/ {
proxy_pass http://10.77.0.20:8080/cgi-bin/;
proxy_read_timeout 3s;
}
}
ثم صفحة بسيطة وثلاثة سكربتات CGI للتطبيق، الأول يرد فوراً، والثاني بعد ثانيتين، والثالث بعد عشر ثوان:
mkdir -p html app/cgi-bin certs
echo '<!doctype html><title>shop</title><h1>shop.lab.test</h1>' > html/index.html
cat > app/cgi-bin/orders <<'EOF'
#!/bin/sh
printf 'Content-Type: application/json
'
echo '{"orders": 3}'
EOF
cat > app/cgi-bin/report <<'EOF'
#!/bin/sh
sleep 2
printf 'Content-Type: application/json
'
echo '{"report": "ready"}'
EOF
cat > app/cgi-bin/export <<'EOF'
#!/bin/sh
sleep 10
printf 'Content-Type: application/json
'
echo '{"export": "done"}'
EOF
chmod +x app/cgi-bin/*
ثم ملف certs/mk.sh الذي ينشئ جهة إصدار رئيسية Root CA وجهة وسيطة Intermediate CA وشهادة للموقع تحمل اسمين، وهي نفس خطوات مختبر مقال الشهادات مختصرة:
set -e
cd /c
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout root.key -out root.crt -days 3650 -subj "/O=Lab Kata/CN=Lab Kata Root CA" -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout int.key -out int.csr -subj "/O=Lab Kata/CN=Lab Kata Intermediate CA"
printf "basicConstraints=critical,CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign\n" > int.ext
openssl x509 -req -in int.csr -CA root.crt -CAkey root.key -CAcreateserial -out int.crt -days 1825 -extfile int.ext
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout leaf.key -out leaf.csr -subj "/CN=shop.lab.test"
printf "subjectAltName=DNS:shop.lab.test,DNS:www.shop.lab.test\nextendedKeyUsage=serverAuth\nbasicConstraints=CA:FALSE\n" > leaf.ext
openssl x509 -req -in leaf.csr -CA int.crt -CAkey int.key -CAcreateserial -out leaf.crt -days 90 -extfile leaf.ext
cat leaf.crt int.crt > fullchain.crt
chmod 644 *
الآن نشغل الشبكة وال Containers، ولاحظ أن الكلاينت يثبت أدواته أولاً من شبكة Docker العادية، ثم نجعل سيرفر ال DNS الخاص به هو الذاكرة المؤقتة 10.77.0.54 فقط، وأن Nginx منشور على 127.0.0.1 وحده حتى لا يصل إليه أحد من خارج جهازك:
docker network create --subnet 10.77.0.0/24 kata-net
docker run --rm -v "$PWD/certs:/c" alpine:3.22 sh -c "apk add -q openssl >/dev/null && sh /c/mk.sh"
docker run -d --name kata-dns --network kata-net --ip 10.77.0.53 alpine:3.22 sh -c "apk add -q dnsmasq && exec dnsmasq -k --no-resolv --no-hosts --local=/lab.test/ --log-queries --log-facility=- --local-ttl=300 --host-record=shop.lab.test,10.77.0.10 --cname=www.shop.lab.test,shop.lab.test --mx-host=shop.lab.test,mail.shop.lab.test,10 --host-record=mail.shop.lab.test,10.77.0.25"
docker run -d --name kata-cache --network kata-net --ip 10.77.0.54 alpine:3.22 sh -c "apk add -q dnsmasq && exec dnsmasq -k --no-resolv --no-hosts --server=10.77.0.53 --cache-size=1000 --log-queries --log-facility=-"
docker run -d --name kata-app --network kata-net --ip 10.77.0.20 -v "$PWD/app:/www" alpine:3.22 sh -c "apk add -q busybox-extras && httpd -p 8080 -h /www && exec sleep infinity"
docker run -d --name kata-web --network kata-net --ip 10.77.0.10 -p 127.0.0.1:21390:80 -p 127.0.0.1:21391:443 \
-v "$PWD/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro" -v "$PWD/certs:/etc/nginx/certs:ro" -v "$PWD/html:/usr/share/nginx/html:ro" nginx:1.29-alpine
docker run -d --name kata-client --network kata-net --ip 10.77.0.100 -v "$PWD/certs:/certs:ro" alpine:3.22 sleep infinity
docker exec kata-client sh -c 'apk add -q curl bind-tools openssl iproute2 bind python3 && echo "nameserver 10.77.0.54" > /etc/resolv.conf'
docker exec kata-client curl -s http://shop.lab.test/
والمخرج سوف يكون كما يلي:
<!doctype html><title>shop</title><h1>shop.lab.test</h1>
وكل أمر في التمارين يبدأ ب docker exec kata-client ما لم نذكر غير ذلك، فإذا أردت أن تكتب الأوامر كما تكتبها على سيرفر حقيقي فادخل الكلاينت مرة واحدة ب docker exec -it kata-client sh. ولاحظ أن المعرفات والتواريخ في مخرجاتك سوف تختلف عن مخرجاتنا، لأن الشهادات أنشئت في وقت آخر، أما الأرقام والرسائل فهي نفسها.
المستوى الأول: مبتدئ Beginner
تمارين هذا المستوى تحل بالورقة أو بقراءة مخرج واحد، والهدف أن تقرأ العنوان والقناع Mask ورد ال DNS ورد HTTP وقائمة المنافذ دون أن تتردد.
التمرين 1: كم جهازاً يتسع في الشبكة 192.168.10.0/26؟
أعطاك فريق البنية التحتية الشبكة الفرعية Subnet التالية لسيرفرات التطبيق:
192.168.10.0/26
اكتب بيدك: قناع الشبكة Netmask بالشكل العشري، وعنوان الشبكة Network Address، وعنوان البث Broadcast Address، وأول وآخر عنوان يمكن إعطاؤه لجهاز، وعدد الأجهزة الممكنة.
الوقت المقترح: 5 دقائق.
تلميح Hint
الرقم 26 يعني أن أول 26 بتاً من 32 هي جزء الشبكة، فيبقى 6 بتات للأجهزة، و2 أس 6 يساوي 64 عنواناً، وأول عنوان وآخر عنوان في كل شبكة محجوزان.
الحل Answer
بقي للأجهزة 32 ناقص 26 أي 6 بتات، فحجم الشبكة 26 = 64 عنواناً، والبايت الأخير في القناع فيه أول بتين فقط مضاءين، أي 128 + 64 = 192، فالقناع 255.255.255.192. والشبكة تبدأ من 192.168.10.0 وتنتهي عند 192.168.10.63، والأول هو عنوان الشبكة والأخير هو عنوان البث، فيبقى 62 عنواناً للأجهزة من .1 إلى .62. وتحققنا من ذلك بالمكتبة ipaddress في Python، وتستطيع تشغيلها على جهازك أو داخل الكلاينت ب docker exec -it kata-client python3، ونفس الجلسة تخدم التمارين 2 و3 و7 و8:
import ipaddress as ip
n = ip.ip_network('192.168.10.0/26')
print(n.netmask, n.network_address, n.broadcast_address, n.num_addresses, n.num_addresses - 2, n[1], n[-2])
والمخرج سوف يكون كما يلي:
255.255.255.192 192.168.10.0 192.168.10.63 64 62 192.168.10.1 192.168.10.62
لاحظ أن ال Router نفسه يأخذ عادة واحداً من الاثنين والستين، فإذا كان لديك 62 سيرفراً بالضبط فأنت تحتاج شبكة أكبر. وطريقة الحساب في RFC 4632 الذي يعرف كتابة CIDR.
للتفصيل: العناوين الخاصة وأقنعة الشبكات في مقال NAT.
التمرين 2: هل هذه العناوين داخل الشبكة 10.20.32.0/22؟
قاعدة في جدار الحماية Firewall تسمح فقط للشبكة 10.20.32.0/22 بالوصول إلى قاعدة البيانات، ووصلتك أربعة عناوين تشتكي أنها لا تصل. حدد بيدك أيها داخل الشبكة وأيها خارجها:
10.20.33.200
10.20.35.255
10.20.36.5
10.20.31.250
الوقت المقترح: 7 دقائق.
تلميح Hint
القناع /22 يغطي 4 قيم كاملة في البايت الثالث، فابحث عن أول قيمة تقبل القسمة على 4 وأين تنتهي الشبكة.
الحل Answer
القناع /22 يترك 10 بتات للأجهزة، أي 2 بت من البايت الثالث و8 بتات من الرابع، فكل شبكة /22 تأخذ أربع قيم متتالية في البايت الثالث، والقيمة 32 تقبل القسمة على 4، فالشبكة من 10.20.32.0 إلى 10.20.35.255 والقناع 255.255.252.0. وبالتالي 10.20.33.200 و10.20.35.255 داخلها، والعنوانان 10.20.36.5 و10.20.31.250 خارجها، وهؤلاء من يحتاج قاعدة أخرى. والتحقق:
net = ip.ip_network('10.20.32.0/22')
print(net, net.netmask, net[0], net[-1])
for a in ['10.20.33.200', '10.20.35.255', '10.20.36.5', '10.20.31.250']:
print(a, ip.ip_address(a) in net)
والمخرج سوف يكون كما يلي:
10.20.32.0/22 255.255.252.0 10.20.32.0 10.20.35.255
10.20.33.200 True
10.20.35.255 True
10.20.36.5 False
10.20.31.250 False
لاحظ أن 10.20.35.255 داخل الشبكة ولكنه عنوان البث، فهو لا يكون عنوان جهاز في الواقع، وإذا ظهر لك في سجل الجدار كمصدر فهناك إعداد خاطئ في مكان ما.
للتفصيل: العناوين الخاصة في مقال NAT، ومرجع الكتابة RFC 4632.
التمرين 3: خاص أم عام أم CGNAT؟
صنف كل عنوان من العناوين التالية: خاص Private حسب RFC 1918، أو عام Public يصل من الإنترنت، أو من مساحة CGNAT التي يستخدمها مزود الإنترنت، أو غير ذلك:
10.8.0.1
172.16.5.4
172.32.5.4
192.168.1.10
100.72.3.9
100.128.0.1
169.254.10.20
8.8.8.8
الوقت المقترح: 7 دقائق.
تلميح Hint
النطاقات الخاصة ثلاثة: 10/8 و172.16/12 و192.168/16، ونطاق CGNAT هو 100.64.0.0/10. احسب أين ينتهي 172.16/12 وأين ينتهي 100.64/10 قبل أن تحكم على 172.32 و100.128.
الحل Answer
النطاق 172.16.0.0/12 يمتد من 172.16 إلى 172.31 فقط، لذلك 172.32.5.4 عنوان عام رغم أنه «يشبه» الخاص، وهذا خطأ شائع في قواعد الجدار. والنطاق 100.64.0.0/10 يمتد من 100.64 إلى 100.127، فالعنوان 100.72.3.9 من مساحة CGNAT المشتركة Shared Address Space حسب RFC 6598، و100.128.0.1 عام. والعنوان 169.254.10.20 عنوان محلي على الرابط Link-Local حسب RFC 3927، ويظهر عادة عندما يفشل الجهاز في الحصول على عنوان من DHCP. وتحققنا بالمكتبة نفسها:
cg = ip.ip_network('100.64.0.0/10')
for a in ['10.8.0.1', '172.16.5.4', '172.32.5.4', '192.168.1.10', '100.72.3.9', '100.128.0.1', '169.254.10.20', '8.8.8.8']:
x = ip.ip_address(a)
print(f"{a:15} private={x.is_private!s:5} global={x.is_global!s:5} cgnat={x in cg!s:5} link_local={x.is_link_local}")
والمخرج سوف يكون كما يلي:
10.8.0.1 private=True global=False cgnat=False link_local=False
172.16.5.4 private=True global=False cgnat=False link_local=False
172.32.5.4 private=False global=True cgnat=False link_local=False
192.168.1.10 private=True global=False cgnat=False link_local=False
100.72.3.9 private=False global=False cgnat=True link_local=False
100.128.0.1 private=False global=True cgnat=False link_local=False
169.254.10.20 private=True global=False cgnat=False link_local=True
8.8.8.8 private=False global=True cgnat=False link_local=False
في المخرج أعلاه لاحظ التالي:
- عنوان CGNAT ليس خاصاً ولا عاماً في نظر المكتبة، فلا هو من RFC 1918 ولا يصل من الإنترنت، وإذا رأيته على واجهة WAN في ال Router الخاص بك فأنت خلف NAT ثان عند المزود، وتوجيه المنافذ عندك لن يكفي.
- المكتبة تعتبر
169.254.10.20خاصاً لأنها تتبع سجل IANA للعناوين الخاصة الاستخدام وليس RFC 1918 وحده، وهو في الواقع عنوان لا يخرج من الشبكة المحلية أصلاً.
للتفصيل: CGNAT في مقال NAT واكتشاف CGNAT في دليل الاستضافة من إنترنت المنزل.
التمرين 4: اقرأ رد dig
نفذ زميلك الأمر التالي وأرسل لك المخرج:
$ dig www.shop.lab.test
; <<>> DiG 9.20.29 <<>> www.shop.lab.test
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 60488
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.shop.lab.test. IN A
;; ANSWER SECTION:
www.shop.lab.test. 300 IN CNAME shop.lab.test.
shop.lab.test. 300 IN A 10.77.0.10
;; Query time: 0 msec
;; SERVER: 10.77.0.54#53(10.77.0.54) (UDP)
;; WHEN: Wed Oct 07 07:52:35 UTC 2026
;; MSG SIZE rcvd: 89
أجب: هل نجح الاستعلام؟ ما نوعا السجلين في الإجابة ولماذا يوجد سجلان لسؤال واحد؟ ما العنوان الذي سوف يتصل به المتصفح؟ ماذا يعني الرقم 300؟ ومن السيرفر الذي أجاب؟
الوقت المقترح: 7 دقائق.
تلميح Hint
ابدأ من السطر status، ثم القسم ANSWER SECTION سطراً بسطر: الاسم، ثم المدة بالثواني، ثم الصنف، ثم النوع، ثم القيمة. والسطر SERVER في الأسفل يقول من سألت.
الحل Answer
الحالة NOERROR تعني أن الاسم موجود وأن هناك إجابة، ولو كان الاسم غير موجود لظهرت NXDOMAIN. والسؤال كان عن سجل A للاسم www.shop.lab.test، فأجاب السيرفر بأن هذا الاسم هو سجل CNAME أي اسم بديل يشير إلى shop.lab.test، ثم أضاف من نفسه سجل A للاسم الذي يشير إليه حتى لا تحتاج سؤالاً ثانياً، لذلك يوجد في الإجابة سجلان. والعنوان الذي سوف يتصل به المتصفح هو 10.77.0.10.
والرقم 300 هو مدة البقاء TTL بالثواني، أي أن من استلم هذه الإجابة يحق له أن يحفظها خمس دقائق ويجيب منها دون أن يسأل من جديد، وهذا ما سوف تراه في التمرين 9. والسيرفر الذي أجاب هو 10.77.0.54 على المنفذ 53 عبر UDP، أي الذاكرة المؤقتة وليس السيرفر الذي يحمل السجلات. وفي المختبر أعطانا هذا المخرج الأمر:
docker exec kata-client dig www.shop.lab.test
وإذا أردت السطور المهمة فقط فأضف +noall +answer، وإذا أردت سؤال سيرفر بعينه فاكتبه بعد @، كما سوف نفعل في التمرين 9.
للتفصيل: سجل CNAME والتخزين المؤقت ومدة TTL وأدوات الفحص في مقال DNS.
التمرين 5: اقرأ مخرج curl -v
هذا مخرج curl -sv -o /dev/null http://shop.lab.test/ كما ظهر في المختبر:
* Host shop.lab.test:80 was resolved.
* IPv6: (none)
* IPv4: 10.77.0.10
* Trying 10.77.0.10:80...
* Connected to shop.lab.test (10.77.0.10) port 80
* using HTTP/1.x
> GET / HTTP/1.1
> Host: shop.lab.test
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx/1.29.8
< Date: Wed, 07 Oct 2026 07:55:09 GMT
< Content-Type: text/html
< Content-Length: 57
< Last-Modified: Wed, 07 Oct 2026 07:43:46 GMT
< Connection: keep-alive
< ETag: "6ac5f832-39"
< Accept-Ranges: bytes
<
{ [57 bytes data]
* Connection #0 to host shop.lab.test left intact
أجب: أي السطور أرسلها curl وأيها جاءت من السيرفر؟ ما رمز الحالة؟ ما برنامج السيرفر؟ كم حجم الجسم Body؟ وماذا يرجع السيرفر إذا أرسلت الطلب نفسه مع الترويسة If-None-Match: "6ac5f832-39"؟
الوقت المقترح: 8 دقائق.
تلميح Hint
في curl السطر الذي يبدأ ب > خرج منك، والذي يبدأ ب < وصلك، والذي يبدأ ب * ملاحظة من curl نفسه. والترويسة ETag بصمة للملف تستخدم في الطلبات الشرطية Conditional Requests.
الحل Answer
السطور التي تبدأ ب > هي الطلب: الطريقة GET والمسار / والترويسة Host التي يختار بها Nginx الموقع. والسطور التي تبدأ ب < هي الرد: الحالة 200 OK، والسيرفر nginx/1.29.8، والجسم 57 بايتاً من نوع text/html، وConnection: keep-alive مع السطر الأخير left intact يعنيان أن الاتصال بقي مفتوحاً ليستخدم مرة أخرى. والسطور التي تبدأ ب * تخبرك أن الاسم حل إلى 10.77.0.10 وأن اتصال TCP نجح على المنفذ 80 قبل أن يرسل أي شيء.
وإذا أرسلت البصمة نفسها في If-None-Match فأنت تقول للسيرفر «عندي هذه النسخة، أرسلها فقط إذا تغيرت»، فيرد بالحالة 304 دون جسم:
docker exec kata-client curl -s -o /dev/null -w "%{http_code}\n" -H 'If-None-Match: "6ac5f832-39"' http://shop.lab.test/
والمخرج سوف يكون كما يلي:
304
لاحظ أن البصمة عندك سوف تختلف لأنها مبنية على وقت تعديل الملف وحجمه، فخذها من مخرج curl -v عندك.
للتفصيل: مم يتكون الطلب والرد ورموز 3xx ومنها 304 في مقال HTTP.
التمرين 6: من يستطيع الوصول إلى هذه المنافذ؟
على سيرفر فيه ثلاث خدمات نفذت ss -tlnp فظهر المخرج التالي:
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 4096 127.0.0.11:43973 0.0.0.0:*
LISTEN 0 5 0.0.0.0:9090 0.0.0.0:* users:(("python3",pid=339,fd=3))
LISTEN 0 5 127.0.0.1:3000 0.0.0.0:* users:(("python3",pid=332,fd=3))
LISTEN 0 5 *:8080 *:* users:(("python3",pid=345,fd=3))
عنوان السيرفر على الشبكة 10.77.0.100. أي المنافذ يصل إليها جهاز آخر على نفس الشبكة، وأيها لا يصل إليه إلا السيرفر نفسه؟ وماذا يعني السطر الأول الذي ليس له اسم برنامج؟
الوقت المقترح: 7 دقائق.
تلميح Hint
انظر إلى العنوان قبل النقطتين في عمود Local Address: 0.0.0.0 و* تعنيان كل الواجهات، و127.0.0.1 تعني الواجهة المحلية Loopback فقط.
الحل Answer
شغلنا الخدمات الثلاث في الكلاينت بخادم HTTP الموجود في Python، كل واحدة على عنوان مختلف:
docker exec -d kata-client python3 -m http.server 3000 --bind 127.0.0.1
docker exec -d kata-client python3 -m http.server 9090 --bind 0.0.0.0
docker exec -d kata-client python3 -m http.server 8080 --bind ::
docker exec kata-client ss -tlnp
المنفذ 9090 يستمع على 0.0.0.0 أي كل عناوين IPv4، والمنفذ 8080 يستمع على * أي كل العناوين في IPv4 وIPv6، فكلاهما يصل إليه أي جهاز يرى السيرفر ما لم يمنعه جدار حماية. أما 3000 فيستمع على 127.0.0.1 وحده، فلا يصل إليه إلا برنامج على السيرفر نفسه، وهذا هو الإعداد الصحيح لتطبيق يقف خلف Reverse Proxy. وجربنا ذلك من Container آخر على الشبكة:
for p in 3000 9090 8080; do
docker exec kata-web sh -c "curl -s -o /dev/null -w 'port $p: %{http_code}\n' --connect-timeout 3 http://10.77.0.100:$p/ || echo 'port $p: exit '\$?"
done
docker exec kata-client curl -s -o /dev/null -w 'from inside, port 3000: %{http_code}\n' http://127.0.0.1:3000/
والمخرج سوف يكون كما يلي:
port 3000: 000
port 3000: exit 7
port 9090: 200
port 8080: 200
from inside, port 3000: 200
والرمز 7 في curl يعني أنه لم يستطع الاتصال. والسطر الأول 127.0.0.11:43973 هو سيرفر ال DNS الداخلي الذي يضعه Docker في كل Container على شبكة خاصة، ولا يظهر له اسم برنامج لأنه ليس عملية داخل ال Container، وتجده موثقاً في توثيق شبكات Docker. وعلى سيرفر Ubuntu عادي سوف ترى في مكانه غالباً 127.0.0.53:53 الخاص ب systemd-resolved. وأعمدة المخرج موثقة في صفحة ss.
للتفصيل: المنافذ المنشورة وغير المنشورة في دليل شبكات Docker، وحالات الوصول الجزئي في دليل التشخيص.
المستوى الثاني: متوسط Intermediate
في هذا المستوى تحتاج أن تربط معلومتين: الحساب مع القرار، أو المخرج مع ما حدث قبله، ولذلك الوقت المقترح أطول.
التمرين 7: قسم الشبكة 192.168.50.0/24 إلى أربع شبكات متساوية
لديك الشبكة 192.168.50.0/24 وتريد أن تقسمها إلى أربع شبكات متساوية للتطبيق وقواعد البيانات والإدارة والضيوف. اكتب لكل شبكة: الكتابة بصيغة CIDR، وأول عنوان للأجهزة، وآخر عنوان، وعنوان البث، وعدد الأجهزة.
الوقت المقترح: 12 دقيقة.
تلميح Hint
أربع شبكات تحتاج بتين إضافيين لجزء الشبكة، فالقناع الجديد 24 + 2. وحجم كل شبكة 256 مقسوماً على 4.
الحل Answer
أربع شبكات تساوي 22، فنأخذ بتين من جزء الأجهزة ويصبح القناع /26، وكل شبكة فيها 64 عنواناً منها 62 للأجهزة، وتبدأ الشبكات عند 0 و64 و128 و192:
for s in ip.ip_network('192.168.50.0/24').subnets(new_prefix=26):
print(s, s[1], s[-2], s.broadcast_address, s.num_addresses - 2)
والمخرج سوف يكون كما يلي:
192.168.50.0/26 192.168.50.1 192.168.50.62 192.168.50.63 62
192.168.50.64/26 192.168.50.65 192.168.50.126 192.168.50.127 62
192.168.50.128/26 192.168.50.129 192.168.50.190 192.168.50.191 62
192.168.50.192/26 192.168.50.193 192.168.50.254 192.168.50.255 62
وقد يتساءل البعض: لماذا لا نعطي كل فريق ما يكفيه بالضبط، فالضيوف مثلاً يحتاجون 120 والإدارة 10؟ والإجابة أن هذا ممكن بأقنعة مختلفة الطول Variable Length Subnet Mask، فتأخذ الضيوف /25 والبقية /26 و/27، ولكن ابدأ دائماً بالأكبر حتى لا تقع الشبكة الكبيرة في منتصف حدود لا تقبل القسمة على حجمها.
للتفصيل: العناوين الخاصة في مقال NAT وتوثيق ipaddress.
التمرين 8: جهازان على /23 لا يتحدثان
ثلاثة سيرفرات بلا Router بينها، وكلها موصولة بنفس ال Switch، وإعدادها كما يلي:
app1 10.0.1.10/23
app2 10.0.2.20/23
db1 10.0.3.30/23
السيرفر app1 لا يصل إلى db1، بينما app2 يصل إليه. لماذا؟ وما أقل تغيير يحل المشكلة؟
الوقت المقترح: 12 دقيقة.
تلميح Hint
احسب عنوان الشبكة لكل سيرفر. الجهاز يرسل مباشرة فقط إلى من يرى أنه في شبكته، وغير ذلك يرسله إلى البوابة الافتراضية Default Gateway، وهنا لا توجد بوابة.
الحل Answer
القناع /23 يأخذ قيمتين في البايت الثالث تبدآن من رقم زوجي، فالسيرفر app1 في الشبكة 10.0.0.0/23 أي من 10.0.0.0 إلى 10.0.1.255، بينما app2 وdb1 في 10.0.2.0/23 أي من 10.0.2.0 إلى 10.0.3.255:
for a in ['10.0.1.10/23', '10.0.2.20/23', '10.0.3.30/23']:
print(a, '->', ip.ip_interface(a).network)
والمخرج سوف يكون كما يلي:
10.0.1.10/23 -> 10.0.0.0/23
10.0.2.20/23 -> 10.0.2.0/23
10.0.3.30/23 -> 10.0.2.0/23
فعندما يريد app1 الوصول إلى 10.0.3.30 يرى أنه خارج شبكته فيبحث عن بوابة ولا يجدها، رغم أن الكابل نفسه يصل إليه. وأقل تغيير هو نقل app1 إلى عنوان داخل 10.0.2.0/23 مثل 10.0.2.10، أو توسيع القناع على الأجهزة الثلاثة إلى /22 فتصبح كلها في 10.0.0.0/22. ولا تقم بتغيير القناع على جهاز واحد فقط، والسبب أن الاتصال يحتاج طرفين، فالرد من db1 سوف يفشل بنفس الطريقة.
للتفصيل: ماذا يفعل ال Router فعلاً في مقال وصول الإنترنت.
التمرين 9: غيرنا السجل والعنوان القديم ما زال يظهر
نقلت الموقع من السيرفر 10.77.0.10 إلى 10.77.0.11 وغيرت سجل A عند مزود ال DNS، ثم سألت من جهازك فظهر لك ما يلي:
$ dig +noall +answer shop.lab.test
shop.lab.test. 262 IN A 10.77.0.10
$ dig +noall +answer @10.77.0.53 shop.lab.test
shop.lab.test. 300 IN A 10.77.0.11
وقبل التغيير بدقائق كان الأمر الأول قد أظهر 286 بدل 262. فسر المخرج: لماذا يختلف الجوابان؟ ماذا يعني تناقص الرقم؟ ومتى سوف يرى جهازك العنوان الجديد؟ وماذا كان يجب أن تفعل قبل النقل؟
الوقت المقترح: 12 دقيقة.
تلميح Hint
الأمر الأول لم يحدد سيرفراً، فمن أجاب؟ والأمر الثاني سأل السيرفر الذي يحمل السجل مباشرة. والرقم الثاني في السطر هو المدة المتبقية وليس المدة الأصلية.
الحل Answer
الأمر الأول سأل الذاكرة المؤقتة 10.77.0.54 التي يستخدمها جهازك، وهي حفظت الجواب القديم عندما سألتها أول مرة لمدة 300 ثانية، والرقم الذي يتناقص من 286 إلى 262 هو الوقت المتبقي قبل أن ترمي الجواب وتسأل من جديد. أما الأمر الثاني فسأل السيرفر الذي يحمل السجل نفسه، فأعطاك العنوان الجديد بمدته الكاملة 300. وبالتالي جهازك سوف يرى العنوان الجديد بعد 262 ثانية على الأكثر، وهذا ما يسميه الناس «انتشار DNS» وهو في الحقيقة انتهاء صلاحية النسخ المحفوظة واحدة بعد الأخرى.
وفي المختبر أنتجنا هذا المخرج بتغيير السجل في سيرفر kata-dns بعد أن سألنا عنه مرة:
docker exec kata-client dig +noall +answer shop.lab.test
docker rm -f kata-dns
docker run -d --name kata-dns --network kata-net --ip 10.77.0.53 alpine:3.22 sh -c "apk add -q dnsmasq && exec dnsmasq -k --no-resolv --no-hosts --local=/lab.test/ --local-ttl=300 --host-record=shop.lab.test,10.77.0.11 --cname=www.shop.lab.test,shop.lab.test"
docker exec kata-client dig +noall +answer shop.lab.test
docker exec kata-client dig +noall +answer @10.77.0.53 shop.lab.test
والمخرج سوف يكون كما يلي:
shop.lab.test. 286 IN A 10.77.0.10
shop.lab.test. 262 IN A 10.77.0.10
shop.lab.test. 300 IN A 10.77.0.11
وما كان يجب فعله هو أن تخفض TTL إلى 60 أو 300 ثانية قبل النقل بمدة لا تقل عن قيمته القديمة، فإذا كانت يوماً كاملاً فخفضها قبل يوم، ثم تنقل، ثم تبقي السيرفر القديم يعمل حتى تنتهي المدة، ثم ترجع القيمة إلى ما كانت. ولا تقم بإيقاف السيرفر القديم لحظة تغيير السجل، والسبب أن بعض المستخدمين سوف يذهبون إليه حتى تنتهي مدة النسخة المحفوظة عندهم. وأرجع السجل إلى 10.77.0.10 بنفس الأمر قبل أن تكمل بقية التمارين.
للتفصيل: «انتشار» DNS: الحقيقة أنه تخزين مؤقت في مقال DNS، وحالة الاسم الذي لا يحل أو التغيير الذي لم يصل في دليل التشخيص.
التمرين 10: 301 أم 302؟
في إعداد Nginx في المختبر رابطان يعيدان التوجيه:
location = /old-price-list { return 301 /price-list; }
location = /offers { return 302 /ramadan-offers; }
اكتب بيدك ما يتوقع أن يظهر في سطر الحالة والترويسة Location لكل واحد. ثم أجب: أيهما يصلح لصفحة نقلتها نهائياً وأيهما لعرض موسمي، وماذا يحدث في متصفحات المستخدمين إذا استخدمت 301 للعرض الموسمي ثم أردت إلغاءه بعد شهر؟
الوقت المقترح: 10 دقائق.
تلميح Hint
فكر في من يحفظ الجواب. أحد الرمزين قابل للحفظ في ذاكرة المتصفح Cache افتراضياً حتى دون ترويسة Cache-Control، والآخر لا.
الحل Answer
الطلب بالطريقة HEAD عبر curl -sI يظهر لك الحالة والترويسات فقط:
docker exec kata-client curl -sI http://shop.lab.test/old-price-list
docker exec kata-client curl -sI http://shop.lab.test/offers
docker exec kata-client curl -sL -o /dev/null -w '%{http_code} %{url_effective} redirects=%{num_redirects}\n' http://shop.lab.test/old-price-list
والمخرج سوف يكون كما يلي:
HTTP/1.1 301 Moved Permanently
Server: nginx/1.29.8
Date: Wed, 07 Oct 2026 07:54:35 GMT
Content-Type: text/html
Content-Length: 169
Location: http://shop.lab.test/price-list
Connection: keep-alive
HTTP/1.1 302 Moved Temporarily
Server: nginx/1.29.8
Date: Wed, 07 Oct 2026 07:54:38 GMT
Content-Type: text/html
Content-Length: 145
Location: http://shop.lab.test/ramadan-offers
Connection: keep-alive
200 http://shop.lab.test/price-list redirects=1
في المخرج أعلاه لاحظ التالي:
- Nginx حول المسار النسبي في
returnإلى رابط كامل فيLocation. - Nginx يكتب للرمز 302 العبارة القديمة
Moved Temporarilyبينما اسمه في RFC 9110 هوFound، والمهم هو الرقم وليس العبارة. - الخيار
-Lجعل curl يتبع التوجيه، فانتهى عند/price-listبالحالة 200 بعد توجيه واحد.
والرمز 301 يعني أن المكان تغير نهائياً، ويحق للمتصفح أن يحفظه في ذاكرته ويذهب مباشرة إلى العنوان الجديد في المرات القادمة دون أن يسألك، لأنه قابل للحفظ افتراضياً حسب RFC 9110، لذلك هو المناسب لصفحة نقلتها نهائياً، ومحركات البحث تنقل ترتيب الصفحة القديمة إلى الجديدة. أما العرض الموسمي فيأخذ 302، والسبب أنك إذا وضعت له 301 ثم ألغيته فإن المتصفحات التي زارته سوف تستمر في الذهاب إلى صفحة العرض من ذاكرتها، ولن تستطيع إصلاح ذلك من السيرفر.
للتفصيل: إعادة التوجيه 301 و302 و307 و308 في مقال HTTP.
التمرين 11: أين ذهبت الثانيتان؟ توقيتات curl -w
اشتكى مستخدم أن صفحة التقرير بطيئة، فقست الطلب بالأمر التالي:
F='namelookup: %{time_namelookup}\nconnect: %{time_connect}\nappconnect: %{time_appconnect}\nstarttransfer: %{time_starttransfer}\ntotal: %{time_total}\nhttp_code: %{http_code}\n'
docker exec kata-client curl -s -o /dev/null --cacert /certs/root.crt -w "$F" https://shop.lab.test:8443/api/report
وكان المخرج:
namelookup: 0.001194
connect: 0.001365
appconnect: 0.077486
starttransfer: 2.125221
total: 2.125309
http_code: 200
احسب كم أخذ كل جزء: حل الاسم DNS، واتصال TCP، ومصافحة TLS، وانتظار السيرفر، ونقل الجسم. ثم حدد أين تبحث عن السبب.
الوقت المقترح: 12 دقيقة.
تلميح Hint
كل رقم في -w هو وقت تراكمي من بداية الطلب وليس مدة المرحلة وحدها، فاطرح كل رقم من الذي بعده.
الحل Answer
الأرقام تراكمية بالثواني من لحظة البداية كما في دليل curl، فمدة كل مرحلة هي الفرق بين رقمين:
DNS namelookup 0.0012 s (1.2 ms)
TCP connect - namelookup 0.0002 s (0.2 ms)
TLS appconnect - connect 0.0761 s (76 ms)
server wait starttransfer - appconnect 2.0477 s (2048 ms)
body total - starttransfer 0.0001 s
فالشبكة كلها أخذت أقل من 80 ملي ثانية، وأكثر من ثانيتين ضاعت بين إرسال الطلب ووصول أول بايت من الرد، أي داخل السيرفر. والطلب يمر عبر Nginx إلى التطبيق، فتبحث في التطبيق وليس في الشبكة، وفي المختبر السبب معروف لأن السكربت report ينام ثانيتين قبل أن يرد. وقارن ذلك بالعنوان السريع:
docker exec kata-client curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' http://shop.lab.test/api/orders
والمخرج سوف يكون كما يلي:
dns=0.045843 connect=0.046078 ttfb=0.203511 total=0.207435 code=200
ولاحظ أن ال DNS هنا أخذ 45 ملي ثانية لأن الذاكرة المؤقتة لم تكن تحمل الاسم في تلك اللحظة فسألت السيرفر الآخر، وفي الطلب التالي نزل إلى أقل من 2 ملي ثانية، لذلك لا تحكم من طلب واحد وكرر القياس.
للتفصيل: ال curl -w: خمسة أرقام تقسم الطلب في دليل الطلب البطيء، وأين تذهب المللي ثانية في مقال وصول الإنترنت.
التمرين 12: عشرون طلباً وعشرون اتصالاً في TIME-WAIT
شغلت من الكلاينت عشرين أمر curl متتالياً إلى Nginx، ثم نظرت إلى حالات الاتصالات في الطرفين:
$ for i in $(seq 1 20); do curl -s -o /dev/null http://shop.lab.test/; done
$ ss -tan state time-wait | head -4
Recv-Q Send-Q Local Address:Port Peer Address:Port
0 0 10.77.0.100:38620 10.77.0.10:80
0 0 10.77.0.100:38564 10.77.0.10:80
0 0 10.77.0.100:38648 10.77.0.10:80
client TIME-WAIT: 20
server TIME-WAIT: 0
لماذا ظهرت حالة TIME-WAIT عند الكلاينت وليس عند Nginx؟ وما فائدة هذه الحالة؟ ومتى تصبح مشكلة؟
الوقت المقترح: 15 دقيقة.
تلميح Hint
حالة TIME-WAIT تبقى عند الطرف الذي أغلق الاتصال أولاً. فمن أغلق هنا: curl عندما انتهى، أم Nginx الذي يقول Connection: keep-alive؟
الحل Answer
كل أمر curl هو برنامج مستقل يفتح اتصالاً واحداً وينهيه عند الخروج، فهو الطرف الذي يغلق أولاً Active Close، بينما Nginx كان مستعداً لإبقاء الاتصال مفتوحاً، لذلك بقيت الاتصالات العشرون في TIME-WAIT عند الكلاينت ولا يوجد شيء منها عند السيرفر. والأوامر التي أنتجت المخرج:
docker exec kata-client sh -c 'for i in $(seq 1 20); do curl -s -o /dev/null http://shop.lab.test/; done; sleep 1; ss -tan state time-wait | head -4; echo "client TIME-WAIT: $(ss -Htan state time-wait | wc -l)"'
docker exec kata-web sh -c 'echo "server TIME-WAIT: $(netstat -tan | grep -c TIME_WAIT)"'
وفائدة الحالة أن الطرف الذي أغلق ينتظر مدة تساوي ضعف أطول عمر للحزمة 2MSL حسب RFC 9293، حتى لا تصل حزمة متأخرة من هذا الاتصال فتختلط باتصال جديد يستخدم نفس المنفذ، وحتى يعيد إرسال آخر ACK إذا ضاع. وهي لا تستهلك شيئاً يذكر، ولكنها تصبح مشكلة عندما يفتح تطبيقك آلاف الاتصالات القصيرة في الثانية إلى نفس السيرفر، كتطبيق ينشئ كلاينت HTTP جديداً لكل طلب، فتنفد المنافذ المحلية Ephemeral Ports وتظهر أخطاء مثل Cannot assign requested address. والحل ليس تعديل إعدادات النواة Kernel، وإنما إعادة استخدام الاتصال عبر مجمع اتصالات Connection Pool، وأعدنا التجربة بعد أن انتهت الحالات السابقة، ولكن بأمر curl واحد يطلب الرابط عشرين مرة، والمتغير num_connects يطبع عدد الاتصالات الجديدة التي فتحها لكل طلب:
docker exec kata-client sh -c 'curl -s $(for i in $(seq 1 20); do printf -- "-o /dev/null "; done) "http://shop.lab.test/?[1-20]" -w "%{num_connects} "; sleep 1; echo; echo "client TIME-WAIT: $(ss -Htan state time-wait | wc -l)"'
والمخرج سوف يكون كما يلي:
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
client TIME-WAIT: 1
فالطلب الأول فتح اتصالاً والتسعة عشر الباقية استخدمته، وبقي اتصال واحد في TIME-WAIT بدل عشرين.
للتفصيل: إعادة استخدام الاتصال في مقال HTTP، وإعادة استخدام اتصالات HTTP في مقال تجميع الاتصالات، وصفحة tcp في Linux.
المستوى الثالث: متقدم Advanced
تمارين هذا المستوى أقرب إلى حوادث صغيرة، فأنت تقرأ أكثر من مخرج وتبني منها قصة واحدة، ثم تكتب الإصلاح والسبب.
التمرين 13: توجيه المنفذ عبر NAT ومن يرى عنوان من
الصورة التالية تبين المختبر: سيرفر منزلي خلف Router، وجهاز على الإنترنت، وقاعدتان في ال Router:

iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 8443 -j DNAT --to-destination 192.168.77.20:80
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
أجب بيدك: (1) عندما يطلب الجهاز الخارجي http://203.0.113.2:8443/، ما عنوان العميل الذي يكتبه Nginx في سجله على السيرفر المنزلي؟ (2) عندما يطلب السيرفر المنزلي صفحة من الجهاز الخارجي، ما العنوان الذي يراه الجهاز الخارجي؟ (3) غير أحدهم البوابة الافتراضية للسيرفر المنزلي إلى جهاز آخر على الشبكة، فتوقف التوجيه رغم أن القاعدة لم تتغير. لماذا؟ (4) واجهة WAN في ال Router عندك عنوانها 100.72.3.9، هل يكفي توجيه المنفذ؟
الوقت المقترح: 25 دقيقة.
تلميح Hint
DNAT يغير عنوان الوجهة فقط ولا يلمس المصدر. والرد يجب أن يمر بنفس ال Router حتى يعيد الترجمة، وإلا وصل إلى الجهاز الخارجي من عنوان لم يكلمه. وارجع إلى التمرين 3 للعنوان الأخير.
الحل Answer
بنينا المختبر بشبكتين جديدتين ألغينا فيهما ترجمة Docker نفسه، حتى تكون الترجمة الوحيدة هي قاعدتنا، بنفس طريقة مختبر مقال NAT:
docker network create --subnet 192.168.77.0/24 -o com.docker.network.bridge.enable_ip_masquerade=false kata-lan
docker network create --subnet 203.0.113.0/24 -o com.docker.network.bridge.enable_ip_masquerade=false kata-wan
docker run -d --name kata-router --cap-add NET_ADMIN --sysctl net.ipv4.ip_forward=1 alpine:3.22 sleep infinity
docker run -d --name kata-homesrv --cap-add NET_ADMIN nginx:1.29-alpine
docker run -d --name kata-ext --cap-add NET_ADMIN nginx:1.29-alpine
docker exec kata-router apk add -q iptables conntrack-tools iproute2
for c in router homesrv ext; do docker network disconnect bridge kata-$c; done
docker network connect --ip 192.168.77.2 kata-lan kata-router
docker network connect --ip 203.0.113.2 kata-wan kata-router
docker network connect --ip 192.168.77.20 kata-lan kata-homesrv
docker network connect --ip 203.0.113.50 kata-wan kata-ext
docker exec kata-homesrv ip route replace default via 192.168.77.2
docker exec kata-router ip route del default
docker exec kata-ext ip route del default
docker exec kata-router sh -c 'iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 8443 -j DNAT --to-destination 192.168.77.20:80; iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE'
ثم جربنا الاتجاهين وقرأنا جدول تتبع الاتصالات Connection Tracking في ال Router وسجل Nginx في كل طرف:
docker exec kata-ext curl -s -m 4 -o /dev/null -w '%{http_code}\n' http://203.0.113.2:8443/
docker exec kata-router conntrack -L -p tcp | grep 8443
docker logs kata-homesrv 2>/dev/null | grep 'GET /' | tail -1
docker exec kata-homesrv curl -s -m 4 -o /dev/null -w '%{http_code}\n' http://203.0.113.50/
docker exec kata-router conntrack -L -p tcp | grep 'src=192.168.77.20 dst=203.0.113.50'
docker logs kata-ext 2>/dev/null | grep 'GET /' | tail -1
والمخرج سوف يكون كما يلي:
200
tcp 6 118 TIME_WAIT src=203.0.113.50 dst=203.0.113.2 sport=42598 dport=8443 src=192.168.77.20 dst=203.0.113.50 sport=80 dport=42598 [ASSURED] mark=0 use=1
203.0.113.50 - - [07/Oct/2026:08:13:41 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.17.0" "-"
200
tcp 6 118 TIME_WAIT src=192.168.77.20 dst=203.0.113.50 sport=43846 dport=80 src=203.0.113.50 dst=203.0.113.2 sport=80 dport=43846 [ASSURED] mark=0 use=1
203.0.113.2 - - [07/Oct/2026:08:13:46 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.17.0" "-"
في المخرج أعلاه لاحظ التالي:
- (1) سجل السيرفر المنزلي يكتب
203.0.113.50، أي العنوان الحقيقي للعميل، لأن DNAT غير الوجهة فقط. وفي سطر conntrack الأول ترى الاتجاهين: الطلب كما وصل إلى203.0.113.2:8443، والرد المتوقع من192.168.77.20:80. - (2) الجهاز الخارجي يرى
203.0.113.2، أي عنوان ال Router العام، لأن MASQUERADE يغير المصدر عند الخروج منeth1، والعنوان الخاص لا يظهر خارج البيت أبداً.
(3) وعندما غيرنا البوابة الافتراضية للسيرفر المنزلي إلى 192.168.77.1 فشل الطلب نفسه:
docker exec kata-homesrv ip route replace default via 192.168.77.1
docker exec kata-ext curl -sS -m 4 -o /dev/null -w '%{http_code}\n' http://203.0.113.2:8443/
docker exec kata-router conntrack -L -p tcp | grep SYN
docker exec kata-homesrv ip route replace default via 192.168.77.2
والمخرج سوف يكون كما يلي:
curl: (28) Connection timed out after 4001 milliseconds
000
tcp 6 112 SYN_SENT src=203.0.113.50 dst=203.0.113.2 sport=36704 dport=8443 [UNREPLIED] src=192.168.77.20 dst=203.0.113.50 sport=80 dport=36704 mark=0 use=1
والحالة [UNREPLIED] تقول إن الطلب ترجم ووصل، ولكن الرد لم يمر بال Router، لأن السيرفر أرسله إلى بوابة أخرى بعنوانه الخاص، فوصل إلى الجهاز الخارجي من عنوان لم يكلمه فرماه. وهذا سبب شائع لعبارة «وجهت المنفذ ولا يصل أحد».
(4) العنوان 100.72.3.9 من مساحة CGNAT كما في التمرين 3، فال Router عندك خلف NAT آخر عند المزود، والطلبات من الإنترنت لا تصل إلى ال Router أصلاً، لذلك لا يكفي توجيه المنفذ، والحل أن تطلب عنواناً عاماً من المزود أو تستخدم نفقاً Tunnel أو IPv6.
للتفصيل: توجيه المنافذ DNAT ووجهت المنفذ ولا يصل أحد في مقال NAT، وكل الطرق الممكنة في دليل الاستضافة من المنزل، وتوثيق DNAT وMASQUERADE.
التمرين 14: سجل CNAME على النطاق الرئيسي
تريد أن يعمل متجرك على shop.example نفسه وليس فقط على www، ومنصة الاستضافة تعطيك اسماً وليس عنواناً: shop-store.hosting.example. فكتبت ملف المنطقة Zone File التالي:
$TTL 300
@ IN SOA ns1.dns-provider.example. admin.shop.example. ( 2026100701 3600 600 1209600 300 )
@ IN NS ns1.dns-provider.example.
@ IN NS ns2.dns-provider.example.
@ IN MX 10 mail.shop.example.
@ IN CNAME shop-store.hosting.example.
www IN CNAME shop-store.hosting.example.
mail IN A 203.0.113.25
ما الخطأ؟ وماذا سوف يقول سيرفر DNS عند تحميل الملف؟ وما الحلول الصحيحة؟
الوقت المقترح: 15 دقيقة.
تلميح Hint
الرمز @ يعني النطاق الرئيسي نفسه. عد السجلات التي تحمل الاسم @، ثم تذكر القاعدة الأساسية لسجل CNAME: الاسم الذي له CNAME لا يجوز أن يكون له شيء آخر.
الحل Answer
الاسم @ عليه SOA وسجلا NS وسجل MX، وهذه إجبارية للنطاق الرئيسي Root Domain، وحسب RFC 1034 وRFC 2181 إذا كان للاسم سجل CNAME فلا يجوز أن يكون له أي سجل آخر، والسبب أن CNAME يقول «كل ما تسأل عنه لهذا الاسم موجود عند الاسم الآخر»، فلو سمح به هنا لضاع سجل MX والبريد معه. وتحققنا بالأداة named-checkzone من BIND بعد أن حفظنا الملف أعلاه باسم shop.example.zone ونسخناه إلى الكلاينت:
docker exec -i kata-client sh -c 'cat > /tmp/shop.example.zone' < shop.example.zone
docker exec kata-client named-checkzone shop.example /tmp/shop.example.zone; echo "exit=$?"
والمخرج سوف يكون كما يلي:
dns_master_load: /tmp/shop.example.zone:6: shop.example: CNAME and other data
dns_master_load: /tmp/shop.example.zone:6: shop.example: CNAME and other data
dns_master_load: /tmp/shop.example.zone:6: shop.example: CNAME and other data
zone shop.example/IN: loading from master file /tmp/shop.example.zone failed: CNAME and other data
zone shop.example/IN: not loaded due to errors.
exit=1
والحلول الصحيحة ثلاثة: أن تضع سجل A بالعنوان الذي تعطيك إياه المنصة للنطاق الرئيسي، أو أن تستخدم ميزة ALIAS أو CNAME Flattening إذا كان مزود ال DNS يدعمها، وهي ليست سجلاً في المعيار وإنما يحل المزود الاسم بنفسه ويجيب بسجل A، أو أن تجعل النطاق الرئيسي يعيد التوجيه إلى www وتترك CNAME على www وحده. وعندما استبدلنا السطر السادس بسجل A قبل السيرفر الملف:
@ IN A 203.0.113.80
zone shop.example/IN: loaded serial 2026100701
OK
exit=0
ولاحظ أن بعض لوحات المزودين لا تعطيك رسالة الخطأ هذه أصلاً وتمنعك من الحفظ بصمت، أو تقبل السجل ثم تتصرف بطريقتها، لذلك افهم القاعدة قبل أن تلوم اللوحة.
للتفصيل: سجل CNAME وأخطاء شائعة في مقال DNS.
التمرين 15: اقرأ الشهادة ب openssl s_client
المتصفح على جهازك يفتح الموقع دون مشكلة، ولكن تطبيق الموبايل وأداة المراقبة يفشلان مع خطأ في الشهادة. نفذت الأمر التالي، وroot.crt هي الشهادة الرئيسية لجهة الإصدار التي تثق بها الأداة:
$ echo | openssl s_client -connect shop.lab.test:443 -servername shop.lab.test -CAfile /certs/root.crt
Connecting to 10.77.0.10
CONNECTED(00000003)
depth=0 CN=shop.lab.test
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN=shop.lab.test
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 CN=shop.lab.test
verify return:1
---
Certificate chain
0 s:CN=shop.lab.test
i:O=Lab Kata, CN=Lab Kata Intermediate CA
a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
v:NotBefore: Oct 7 07:44:16 2026 GMT; NotAfter: Jan 5 07:44:16 2027 GMT
---
...
Verification error: unable to verify the first certificate
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
...
Verify return code: 21 (unable to verify the first certificate)
أجب: متى تنتهي الشهادة؟ كم شهادة أرسل السيرفر؟ لماذا فشل التحقق؟ لماذا يعمل المتصفح رغم ذلك؟ وكيف تعرف الأسماء التي تغطيها الشهادة؟
الوقت المقترح: 20 دقيقة.
تلميح Hint
في قسم Certificate chain السطر s هو صاحب الشهادة Subject وi هو من أصدرها Issuer. والأداة تملك الشهادة الرئيسية فقط، فمن أين تأتي بالشهادة الوسيطة؟
الحل Answer
الشهادة تنتهي في Jan 5 07:44:16 2027 GMT كما في السطر v، أي بعد 90 يوماً من إصدارها. والسيرفر أرسل شهادة واحدة فقط برقم 0، وهي شهادة الموقع التي أصدرتها الجهة الوسيطة Lab Kata Intermediate CA، والأداة لا تملك إلا الشهادة الرئيسية، فلا تستطيع أن تبني السلسلة من شهادة الموقع إلى الرئيسية لأن الحلقة التي في الوسط ناقصة، وهذا معنى الخطأ 20 unable to get local issuer certificate ثم 21. والمتصفح يعمل غالباً لأنه حفظ الشهادة الوسيطة من موقع آخر زاره، أو لأنه يجلبها بنفسه، بينما الأدوات وتطبيقات الموبايل لا تفعل ذلك، والحل الوحيد الصحيح أن يرسل السيرفر السلسلة كاملة، أي الملف fullchain وليس الشهادة وحدها.
والمنفذ 8443 في المختبر يستخدم fullchain.crt، فقارن:
docker exec kata-client sh -c 'echo | openssl s_client -connect shop.lab.test:8443 -servername shop.lab.test -CAfile /certs/root.crt 2>/dev/null | sed -n "/Certificate chain/,/^---/p;/Verify return code/p"'
والمخرج سوف يكون كما يلي:
Certificate chain
0 s:CN=shop.lab.test
i:O=Lab Kata, CN=Lab Kata Intermediate CA
a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
v:NotBefore: Oct 7 07:44:16 2026 GMT; NotAfter: Jan 5 07:44:16 2027 GMT
1 s:O=Lab Kata, CN=Lab Kata Intermediate CA
i:O=Lab Kata, CN=Lab Kata Root CA
a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
v:NotBefore: Oct 7 07:44:15 2026 GMT; NotAfter: Oct 6 07:44:15 2031 GMT
---
Verify return code: 0 (ok)
والأسماء التي تغطيها الشهادة في الامتداد Subject Alternative Name وليس في CN، ومعها التواريخ وسؤال «هل تنتهي خلال 30 يوماً؟» الذي تضعه في المراقبة:
docker exec kata-client sh -c 'echo | openssl s_client -connect shop.lab.test:443 -servername shop.lab.test 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName'
docker exec kata-client sh -c 'echo | openssl s_client -connect shop.lab.test:443 -servername shop.lab.test 2>/dev/null | openssl x509 -noout -checkend 2592000; echo exit=$?'
docker exec kata-client sh -c 'echo | openssl s_client -connect shop.lab.test:443 -servername shop.lab.test 2>/dev/null | openssl x509 -noout -checkend 8640000; echo exit=$?'
والمخرج سوف يكون كما يلي:
subject=CN=shop.lab.test
issuer=O=Lab Kata, CN=Lab Kata Intermediate CA
notBefore=Oct 7 07:44:16 2026 GMT
notAfter=Jan 5 07:44:16 2027 GMT
X509v3 Subject Alternative Name:
DNS:shop.lab.test, DNS:www.shop.lab.test
Certificate will not expire
exit=0
Certificate will expire
exit=1
والرقم 2592000 هو 30 يوماً بالثواني فالشهادة لن تنتهي خلالها، و8640000 هو 100 يوم فهي سوف تنتهي خلالها. وأرقام أخطاء التحقق موثقة في خيارات التحقق في OpenSSL.
للتفصيل: لماذا يجب أن يرسل السيرفر الشهادة الوسيطة ومتى تنتهي الشهادة في مقال الشهادات.
التمرين 16: 502 أم 504؟ ثلاث حالات من سجل Nginx
Nginx في المختبر يمرر /api/ إلى التطبيق على 10.77.0.20:8080 ب proxy_connect_timeout 2s وproxy_read_timeout 3s. وظهرت ثلاثة أسطر في error.log في ثلاثة أوقات مختلفة:
(A) connect() failed (111: Connection refused) while connecting to upstream, ... upstream: "http://10.77.0.20:8080/cgi-bin/orders"
(B) upstream timed out (110: Operation timed out) while reading response header from upstream, ... upstream: "http://10.77.0.20:8080/cgi-bin/export"
(C) upstream timed out (110: Operation timed out) while connecting to upstream, ... upstream: "http://10.77.0.20:8080/cgi-bin/orders"
لكل سطر: ما رمز الحالة الذي رآه المستخدم؟ كم انتظر تقريباً؟ وما الذي حدث للتطبيق؟
الوقت المقترح: 20 دقيقة.
تلميح Hint
الرمز 502 يعني أن ال Proxy تلقى جواباً سيئاً أو رفضاً، و504 يعني أنه انتظر ولم يصله شيء في الوقت المحدد. وانظر في كل سطر: هل فشل أثناء الاتصال connecting أم أثناء القراءة reading؟ وأي مهلة تنطبق؟
الحل Answer
أنتجنا الحالات الثلاث في المختبر وقسنا ما يراه المستخدم:
docker exec kata-app pkill httpd
docker exec kata-client curl -s -o /dev/null -w 'code=%{http_code} total=%{time_total}\n' http://shop.lab.test/api/orders
docker exec kata-app httpd -p 8080 -h /www
docker exec kata-client curl -s -o /dev/null -w 'code=%{http_code} total=%{time_total}\n' http://shop.lab.test/api/export
docker stop kata-app
docker exec kata-client curl -s -o /dev/null -w 'code=%{http_code} total=%{time_total}\n' http://shop.lab.test/api/orders
docker start kata-app
والمخرج سوف يكون كما يلي:
code=502 total=0.003244
code=504 total=3.004797
code=504 total=2.010858
في المخرج أعلاه لاحظ التالي:
- (A) التطبيق متوقف ولكن الجهاز موجود، فنظام التشغيل رد فوراً برفض الاتصال Connection Refused، وNginx أرجع
502 Bad Gatewayخلال 3 ملي ثانية. وهذا ما تراه عندما ينهار التطبيق أو يعاد تشغيله أو يستمع على منفذ آخر. - (B) التطبيق قبل الاتصال ولكنه لم يرسل ترويسات الرد خلال
proxy_read_timeout، فأرجع Nginx504 Gateway Time-outبعد 3 ثوان بالضبط، والتطبيق في الحقيقة ما زال يعمل على الطلب، وهذا هو الاستعلام البطيء أو الخدمة الخارجية التي لا ترد. - (C) هنا الحالة المفاجئة: أوقفنا ال Container كله فاختفى العنوان من الشبكة، فلم يرد أحد لا بالقبول ولا بالرفض، وانتهت مهلة الاتصال
proxy_connect_timeoutبعد ثانيتين فأرجع Nginx الرمز 504 وليس 502. فالقاعدة الصحيحة ليست «502 يعني التطبيق متوقف»، وإنما: 502 جواب سيئ أو رفض سريع، و504 لا جواب خلال المهلة، واقرأ السطر فيerror.logلتعرف أي مرحلة.
وتعريف الرمزين في RFC 9110، والمهلتان في توثيق ngx_http_proxy_module.
للتفصيل: من أين يأتي 502 و503 و504 في مقال HTTP، وحالة الخطأ 502 أو 504 في دليل التشخيص، ومن الذي أغلق الطلب في دليل الطلب البطيء.
التمرين 17: «الموقع لا يفتح»، رتب الرسائل على السلم
وصلتك ست رسائل من ستة مستخدمين خلال يوم واحد، وكل واحد أرسل مخرج curl عنده. لكل رسالة: حدد الطبقة التي فشلت، وأول أمر تنفذه للتحقق، والسبب الأرجح:
1) curl: (6) Could not resolve host: shpo.lab.test
2) curl: (7) Failed to connect to shop.lab.test port 8081 after 5 ms: Could not connect to server
3) curl: (28) Connection timed out after 3115 milliseconds
4) curl: (60) SSL certificate problem: unable to get local issuer certificate
5) curl: (60) SSL: no alternative certificate subject name matches target ipv4 address '10.77.0.10'
6) HTTP/1.1 504 Gateway Time-out
الوقت المقترح: 25 دقيقة.
تلميح Hint
الطلب يصعد سلماً: الاسم، ثم الاتصال، ثم التشفير، ثم HTTP، ثم التطبيق. ورقم الخطأ في curl يخبرك بالدرجة قبل أن تقرأ النص، وكلها في قائمة أخطاء libcurl.
الحل Answer
الصورة التالية تبين السلم الذي تمشي عليه، وكل رسالة في الصورة خرجت فعلاً من المختبر:

والأوامر التي أنتجت الرسائل في المختبر:
docker exec kata-client curl -sS http://shpo.lab.test/
docker exec kata-client curl -sS http://shop.lab.test:8081/
docker exec kata-client curl -sS --connect-timeout 3 http://10.77.0.99/
docker exec kata-client curl -sS --cacert /certs/root.crt https://shop.lab.test/
docker exec kata-client curl -sS --cacert /certs/root.crt https://10.77.0.10:8443/
docker exec kata-client curl -sv http://shop.lab.test:8081/ 2>&1 | grep -iE "trying|refused|failed"
والسطر الأخير يظهر السبب الحقيقي خلف الرسالة 2، لأن curl 8.14 يكتب «Could not connect to server» في الرسالة المختصرة:
* Trying 10.77.0.10:8081...
* connect to 10.77.0.10 port 8081 from 10.77.0.100 port 55696 failed: Connection refused
* Failed to connect to shop.lab.test port 8081 after 1 ms: Could not connect to server
وبالترتيب:
- الرمز 6 من درجة DNS: الاسم مكتوب خطأ
shpo، وتتحقق بdig shpo.lab.testفترىstatus: NXDOMAIN. وإذا كان الاسم صحيحاً فابحث عن سجل لم يضف أو تغيير لم يصل كما في التمرين 9. - الرمز 7 من درجة TCP مع رفض فوري خلال ملي ثوان: الجهاز موجود ولا يستمع أحد على المنفذ 8081، وتتحقق ب
ss -tlnpعلى السيرفر كما في التمرين 6، والسبب الأرجح منفذ خاطئ أو خدمة متوقفة. - الرمز 28 من درجة TCP أيضاً ولكن دون أي رد: العنوان لا يجيب أصلاً، إما لأنه لا يوجد جهاز عليه، أو لأن جدار حماية يرمي الحزم بصمت، أو لأن الرد يذهب في طريق آخر كما في التمرين 13. وتتحقق ب
nc -zv -w 3من داخل الشبكة ومن خارجها. - الرمز 60 مع
unable to get local issuer certificateمن درجة TLS: السلسلة ناقصة كما في التمرين 15، أو الشهادة من جهة لا يثق بها الكلاينت، وتتحقق بopenssl s_client. - الرمز 60 مع
no alternative certificate subject name matchesمن درجة TLS: المستخدم فتح الموقع بالعنوان10.77.0.10أو باسم لا تغطيه الشهادة، والأسماء المقبولة في SAN فقط. - 504 من درجة HTTP: كل ما تحت هذه الدرجة يعمل، وال Reverse Proxy لم يصله رد من التطبيق في المهلة، فاقرأ
error.logكما في التمرين 16.
وقد يتساءل البعض: لماذا لا نبدأ بإعادة تشغيل كل شيء ونرى؟ والإجابة أن إعادة التشغيل تمسح الدليل، فالمنفذ الذي كان مغلقاً قد يفتح، والاتصالات المعلقة تختفي، ولن تعرف أبداً لماذا حدث ذلك ولا متى سوف يتكرر.
للتفصيل: طريقة التحقيق خطوة بخطوة وحالة الشهادة المنتهية أو غير الموثوقة في دليل التشخيص، وقس الطريق بنفسك في مقال وصول الإنترنت.
جدول المتابعة
انسخ الجدول التالي إلى ملف عندك، واكتب في كل مرة تحل فيها تمريناً التاريخ والوقت الذي أخذه وهل احتجت التلميح، وارجع إلى كل تمرين احتجت فيه التلميح أو تجاوزت الوقت بعد أسبوع، وإلى التمارين التي حللتها بسرعة بعد شهر:
| # | المستوى | التمرين | التاريخ | الوقت | تلميح؟ | المراجعة القادمة |
|----|----------|------------------------------------------|---------|-------|--------|------------------|
| 1 | مبتدئ | عدد الأجهزة في /26 | | | | |
| 2 | مبتدئ | هل العنوان داخل /22 | | | | |
| 3 | مبتدئ | خاص أم عام أم CGNAT | | | | |
| 4 | مبتدئ | قراءة رد dig | | | | |
| 5 | مبتدئ | قراءة curl -v و304 | | | | |
| 6 | مبتدئ | قراءة ss -tlnp | | | | |
| 7 | متوسط | تقسيم /24 إلى أربع | | | | |
| 8 | متوسط | جهازان على /23 | | | | |
| 9 | متوسط | العنوان القديم وTTL | | | | |
| 10 | متوسط | 301 أم 302 | | | | |
| 11 | متوسط | توقيتات curl -w | | | | |
| 12 | متوسط | TIME-WAIT | | | | |
| 13 | متقدم | توجيه المنفذ عبر NAT | | | | |
| 14 | متقدم | CNAME على النطاق الرئيسي | | | | |
| 15 | متقدم | قراءة openssl s_client | | | | |
| 16 | متقدم | 502 أم 504 | | | | |
| 17 | متقدم | سلم «الموقع لا يفتح» | | | | |
وعندما تنتهي احذف المختبر بالأسماء نفسها التي أنشأته بها:
docker rm -f kata-client kata-web kata-app kata-cache kata-dns kata-router kata-homesrv kata-ext
docker network rm kata-net kata-lan kata-wan
الخلاصة
وصلنا لنهاية تمارين الشبكات، وأهم ما تخرج به منها:
- القناع يقول كم بتاً للشبكة والباقي للأجهزة، ومن هذه الجملة وحدها تحسب الحجم والحدود، وتعرف لماذا لا يتحدث جهازان على نفس الكابل، ولماذا
172.32ليس خاصاً و100.72ليس عاماً. - مخرج
digوcurl -vوssليس ضجيجاً، فكل سطر فيه يجيب عن سؤال محدد: من أجاب، وكم تبقى من مدة الحفظ، وما الذي أرسلته، وعلى أي عنوان يستمع البرنامج. - «انتشار DNS» هو انتهاء مدة TTL، فخفضها قبل النقل وليس بعده، و301 يحفظ في المتصفح فلا تستخدمه لشيء مؤقت.
- DNAT يغير الوجهة ويحفظ عنوان العميل، والرد يجب أن يمر بنفس ال Router، وعنوان CGNAT على واجهة WAN يعني أن توجيه المنافذ لن يصل.
- كل طبقة تفشل برسالة مختلفة، فاصعد السلم من الاسم إلى الاتصال إلى الشهادة إلى HTTP إلى التطبيق، و502 و504 يفرق بينهما سطر واحد في
error.log.
وهذه المقالات التي تشرح كل موضوع في التمارين بالتفصيل:
دليلما هو NAT؟ شرح ترجمة العناوين وتوجيه المنافذلماذا تخرج كل أجهزة بيتك بعنوان عام واحد، وكيف يترجم ال Router العناوين، ولماذا يفشل توجيه المنافذ خلف CGNAT.
دليلشرح DNS وسجلاته للمبتدئين بمثال حقيقيكيف يتحول اسم نطاقك إلى عنوان سيرفرك، وما وظيفة كل سجل تضيفه في لوحة النطاق، والأخطاء التي تضيع فيها الساعات عادة.
دليلHTTP وHTTPS ببساطة: ماذا يحدث بين الرابط والصفحةماذا يحدث بين كتابة الرابط وظهور الصفحة، وكيف تقرأ الطلب والرد ورموز مثل 502 و504، ولماذا يكلف الاتصال الجديد أكثر.
دليلالشهادات الرقمية TLS للمبتدئين وصيغ ملفاتهاكيف يعرف المتصفح أنه يتحدث مع الموقع الحقيقي، وماذا يوجد داخل الشهادة، ولماذا تأتي ملفاتها بأكثر من صيغة.
دليلاستكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السببعندما يبطئ الموقع أو يعطي 502، هذه طريقة منظمة توصلك من العرض إلى السبب قبل أن تخمن أو تشتري سيرفراً أكبر.
دليلتشغيل خادمك من إنترنت المنزل: كل طرق الوصول من الخارجكيف تصل إلى سيرفر في بيتك من الإنترنت حتى لو كنت خلف CGNAT، وما الطريقة المناسبة لك من بين ثماني طرق.وفي بقية سلسلة تمارين Kata سوف نطبق نفس الطريقة بإذن الله على SQL والبرمجة والأمن وDevOps وتشخيص الأعطال.
سجل التحديثات
- أكتوبر 2026: كتابة التمارين السبعة عشر، وتنفيذ كل الحلول في مختبر Docker على شبكة خاصة: dnsmasq 2.91 للسجلات والذاكرة المؤقتة، وnginx 1.29.8، وكلاينت Alpine 3.22.6 عليه curl 8.14.1 وDiG 9.20.29 وOpenSSL 3.5.9 وnamed-checkzone، ومختبر NAT ب iptables 1.8.11 (nf_tables) وconntrack 1.4.8، وحسابات الشبكات بمكتبة ipaddress في Python 3.13.3، على Docker Engine 29.8.0 (Docker Desktop).