لنفرض أن قاعدة البيانات Database على سيرفرك تستمع على localhost فقط كما ينبغي، وأنك تريد اليوم أن تفتحها من جهازك ببرنامج مثل DBeaver لتصلح سجلاً واحداً، أو أن لديك تطبيقاً يعمل على جهاز في المنزل خلف CGNAT ولا يوجد لديك عنوان عام تفتح عليه منفذاً، أو أنك في شبكة عمل لا تسمح إلا باتصال واحد محدد. والحل السريع الذي يخطر على البال في كل هذه الحالات هو أن تفتح المنفذ Port للإنترنت، وهذا الحل إما أنه لا يعمل أصلاً، وإما أنه يكشف خدمة لا ينبغي أن يراها أحد. والحل الذي يستخدمه مديرو الأنظمة منذ سنوات هو النفق Tunnel، أي أن تحمل الاتصال الذي تحتاجه داخل اتصال آخر مسموح به.
وكلمة النفق تظهر في أماكن كثيرة على الموقع، في نفق SSH الذي نفتح به واجهات الإدارة في أغلب الأدلة، وفي Cloudflare Tunnel الذي ننشر به خدمات المنزل، وفي WireGuard وشبكات VPN، لذلك سوف نجمعها هنا مرة واحدة ونشرح ما الذي يجمعها وما الذي يفرق بينها. أما شبكات VPN نفسها، أي أنواعها وبروتوكولاتها ومصطلحاتها، فقد شرحناها في ما هو VPN؟ شرح للمبتدئين، ولن نكررها هنا.
وسوف نناقش في هذا المقال ما يلي:
- فكرة النفق، والمشكلات الأربع التي يحلها.
- أنفاق SSH بأنواعها الثلاثة: التوجيه المحلي
-L، والتوجيه العكسي-R، والتوجيه الديناميكي-D، بأوامر ومخرجات حقيقية، ومعها حالة عملية للوصول إلى جهاز في المنزل خلف CGNAT من شبكة عمل لا تسمح إلا ب RDP. - الأنفاق العكسية Reverse Tunnels التي تنشر خدمة من خلف NAT عبر سيرفر وسيط: Cloudflare Tunnel وfrp وrathole وPangolin، مع مثال كامل ل frp.
- أنفاق طبقة الشبكة GRE وIP-in-IP وVXLAN، ولماذا لا يوجد فيها أي تشفير.
- جدول يقارن بين الأنواع، ثم كيف تختار النفق المناسب لحالتك، وأخطاء الأمان الشائعة.
ما هو النفق، ولماذا نحتاجه؟
النفق Tunnel هو أن تأخذ اتصالاً أو بروتوكولاً Protocol وتضعه داخل اتصال آخر، فيسير الأول محمولاً داخل الثاني حتى يصل إلى الطرف الآخر من النفق، وهناك يخرج ويكمل طريقه كأنه بدأ من هناك. والتشبيه الأقرب هو الظرف داخل الظرف، فأنت تضع رسالة موجهة إلى زميل في قسم داخلي داخل ظرف موجه إلى استقبال الشركة، فساعي البريد لا يرى إلا العنوان الخارجي، والاستقبال يفتح الظرف الخارجي ويوصل الرسالة الداخلية إلى صاحبها. واسم هذه العملية في الشبكات التغليف Encapsulation.
الصورة التالية تبين الفكرة في أبسط أشكالها، فجدار الحماية Firewall لا يسمح إلا بالمنفذ 22، والاتصال بقاعدة البيانات على المنفذ 5432 يمر داخل اتصال SSH:

ونحتاج إلى النفق في أربع حالات تتكرر كثيراً:
- الوصول إلى خدمة غير منشورة: قاعدة بيانات أو واجهة إدارة Admin UI تستمع على
127.0.0.1على السيرفر، ولا تريد أن تفتح منفذها للإنترنت ولو لساعة. - المرور عبر شبكة تمنعك: شبكة عمل أو فندق لا تسمح إلا بمنافذ معينة، فتحمل ما تحتاجه داخل الاتصال المسموح.
- ربط شبكتين لا تستطيعان التوجيه Routing إلى بعضهما: فرع وفرع آخر، أو سيرفرات في مزودين مختلفين، كل منها بعناوين خاصة Private IPs لا يعرفها الإنترنت.
- نشر خدمة من خلف NAT أو CGNAT دون فتح منافذ: جهاز في المنزل ليس له عنوان عام، فيتصل هو بسيرفر وسيط ويستقبل الزوار عبر هذا الاتصال.
وقد يتساءل البعض: أليس هذا كله هو VPN؟ والإجابة أن كل VPN نفق، وليس كل نفق VPN، فالشبكة الخاصة الافتراضية VPN نفق على مستوى الشبكة يربط جهازاً أو شبكة كاملة بشبكة أخرى مع التشفير Encryption والمصادقة Authentication، أما نفق SSH مثلاً فيحمل منفذاً واحداً أو بضعة منافذ، ونفق VXLAN يحمل الحزم Packets دون أي تشفير. لذلك سوف نبدأ بأبسط الأنواع وأكثرها استخداماً، وهو نفق SSH الموجود على كل سيرفر Linux.
أنفاق SSH: النفق الموجود على كل سيرفر
برنامج OpenSSH الذي تدخل به إلى سيرفرك لا ينقل الأوامر فقط، وإنما يستطيع أن يحمل اتصالات TCP أخرى داخل الاتصال المشفر نفسه، واسم هذه الميزة توجيه المنافذ Port Forwarding، ولها ثلاثة أشكال يشرحها دليل الأمر ssh: المحلي -L والعكسي -R والديناميكي -D. وكل الأمثلة التالية تفترض أنك تدخل إلى السيرفر بمفتاح SSH Key وليس بكلمة مرور، كما في دليل تأمين خادم VPS من أول دخول.
التوجيه المحلي ssh -L: الوصول إلى قاعدة بيانات على السيرفر
لنأخذ مثالاً، سيرفر عليه PostgreSQL تستمع على localhost فقط، وأنت تريد أن تتصل بها من جهازك. الحل الأول الذي يخطر على البال هو أن تغير listen_addresses إلى * وتفتح المنفذ 5432 في جدار الحماية، أو أن تضيف السطر ports: ["5432:5432"] في ملف Compose، وهذا الحل غير مناسب إطلاقاً، والسبب أن قاعدة البيانات تصبح أمام كل من يفحص الإنترنت، وأن Docker يتجاوز ufw كما شرحنا في شبكات Docker وأفضل الممارسات. والحل الثاني أن تفتح المنفذ لعنوانك فقط، ولكن عنوان المنزل يتغير، وعنوان المقهى لا تعرفه مسبقاً. أما الحل الصحيح فهو أن تترك قاعدة البيانات كما هي، وتفتح نفقاً من جهازك عبر SSH.
وقبل ذلك لاحظ أن الاتصال المباشر من جهازك يفشل كما هو متوقع:
psql -h 203.0.113.10 -U postgres -c "select 1"psql: error: connection to server at "203.0.113.10", port 5432 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?الآن سوف نقوم بفتح النفق، والأمر التالي يجعل ssh يستمع على المنفذ 5432 على جهازك، وكل اتصال يصل إليه يحمله داخل اتصال SSH إلى السيرفر، وهناك يتصل sshd بالعنوان localhost:5432:
ssh -f -N -L 5432:localhost:5432 [email protected]والصورة التالية تبين ما يحدث في الخطوات الثلاث:

نتأكد أن ssh يستمع على جهازك، ثم نتصل بقاعدة البيانات عبر النفق:
ss -tlnp | grep 5432
psql -h 127.0.0.1 -U postgres -c "select version();"والمخرج سوف يكون كما يلي:
LISTEN 0 128 127.0.0.1:5432 0.0.0.0:* users:(("ssh",pid=303,fd=5))
LISTEN 0 128 [::1]:5432 [::]:* users:(("ssh",pid=303,fd=4))
version
-----------------------------------------------------------------------------------------
PostgreSQL 18.0 on x86_64-pc-linux-musl, compiled by gcc (Alpine 14.2.0) 14.2.0, 64-bit
(1 row)في الإعداد أعلاه لاحظ التالي:
- الصيغة هي
-L [عنوان_محلي:]منفذ_محلي:الوجهة:منفذ_الوجهة، والوجهةlocalhostتعني السيرفر وليس جهازك، والسبب أن sshd على السيرفر هو من يفتح الاتصال الأخير. - ssh يستمع على
127.0.0.1فقط افتراضياً، فلا يستطيع جهاز آخر في شبكتك أن يستخدم النفق، وهذا ما تريده في الغالب. - الخيار
-Nيعني لا تشغل أي أمر على السيرفر، والخيار-fيرسل ssh إلى الخلفية بعد الاتصال. وإذا كان المنفذ 5432 مستخدماً على جهازك فاختر منفذاً آخر مثل-L 15432:localhost:5432واتصل ب127.0.0.1:15432.
والوجهة لا يلزم أن تكون على السيرفر نفسه، فأي جهاز يصل إليه السيرفر يمكنك الوصول إليه عبر النفق. لنفرض أن لدى السيرفر شبكة داخلية فيها تطبيق اسمه app.internal لا يراه جهازك أصلاً:
curl -sS -m 5 http://app.internal/
ssh -f -N -L 8080:app.internal:80 [email protected]
curl -s http://127.0.0.1:8080/ | grep -E "^(Hostname|RemoteAddr|Host):"curl: (6) Could not resolve host: app.internal (Domain name not found)
Hostname: app
RemoteAddr: 192.168.80.2:46310
Host: 127.0.0.1:8080لاحظ أن التطبيق يرى الاتصال قادماً من 192.168.80.2، وهو عنوان السيرفر على الشبكة الداخلية وليس عنوان جهازك، لذلك فإن أي قاعدة في التطبيق تسمح بعناوين الشبكة الداخلية سوف تسمح لك أيضاً، وهذا يفيدك ويفيد من يسرق مفتاحك بالقدر نفسه.
ماذا لو كان التوجيه ممنوعاً على السيرفر؟
الإعداد AllowTcpForwarding في ملف /etc/ssh/sshd_config قيمته الافتراضية yes، وهذا حال Ubuntu وDebian، ولكن بعض التوزيعات والسيرفرات المؤمنة تغيره إلى no، ومنها Alpine التي يأتي ملفها بالسطر AllowTcpForwarding no. وفي هذه الحالة ينجح الأمر نفسه ويستمع ssh على جهازك، ولكن كل اتصال عبر النفق يفشل، والمخرج سوف يكون كما يلي من psql ثم من ssh:
psql: error: connection to server at "127.0.0.1", port 5432 failed: server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
channel 2: open failed: administratively prohibited: open failedوالعبارة administratively prohibited تعني أن السيرفر رفض فتح القناة Channel بسبب إعداده، وليس بسبب الشبكة، وفي سجل sshd سوف تجد السطر التالي:
refused local port forward: originator 127.0.0.1 port 35972, target localhost port 5432وإذا كان السيرفر سيرفرك فغير القيمة إلى yes وأعد تحميل sshd، أما إذا كان سيرفر جهة أخرى فهذا قرار أمني من مديره، فلا تحاول الالتفاف عليه.
التوجيه العكسي ssh -R: السيرفر يفتح منفذاً يعود إلى جهازك
التوجيه العكسي هو عكس السابق تماماً، فالمنفذ يفتح على السيرفر، وكل اتصال يصل إليه يعود عبر النفق إلى جهازك. والفائدة الكبرى هنا أن الاتصال يبدأ من جهازك، فيعمل حتى لو كان جهازك خلف NAT أو CGNAT ولا يوجد له عنوان عام، كما في الصورة التالية:

لنفرض أن على جهازك تطبيق ويب يعمل على 127.0.0.1:3000، والأمر التالي ينشره على السيرفر على المنفذ 9000:
ssh -f -N -R 9000:localhost:3000 [email protected]ثم على السيرفر نتأكد من المنفذ ونطلب الصفحة:
ss -tlnp | grep 9000
curl -s http://127.0.0.1:9000/LISTEN 0 128 127.0.0.1:9000 0.0.0.0:*
LISTEN 0 128 [::1]:9000 [::]:*
hello from the laptop behind NATلاحظ أن sshd فتح المنفذ على 127.0.0.1 فقط، فمن جهاز آخر على الشبكة نفسها يفشل الاتصال:
curl: (7) Failed to connect to 203.0.113.10:9000 after 3 ms: Could not connect to serverوالسبب هو الإعداد GatewayPorts وقيمته الافتراضية no، فحتى لو طلبت صراحة -R 0.0.0.0:9000:localhost:3000 فإن sshd يتجاهل العنوان ويبقي المنفذ على 127.0.0.1. وإذا غيرت القيمة إلى clientspecified فإن sshd يحترم العنوان الذي يطلبه الكلاينت، والمخرج بعد الطلب نفسه سوف يكون كما يلي:
LISTEN 0 128 0.0.0.0:9000 0.0.0.0:*
hello from the laptop behind NATأي أن المنفذ أصبح مفتوحاً على كل عناوين السيرفر، والجملة الثانية جاءت من جهاز آخر على الشبكة. وهنا يكمن الخطر، فخدمة على جهازك لم تصمم لتواجه الإنترنت أصبحت على الإنترنت بأمر واحد، لذلك لا تغير GatewayPorts إلا إذا احتجت إليه فعلاً، وعندها احصر المنفذ في جدار الحماية كما في الحالة التالية. ولا تستخدم القيمة yes، والسبب أنها تفتح كل منفذ عكسي على كل العناوين حتى لو لم يطلب الكلاينت ذلك.
حالة عملية: الوصول إلى جهاز المنزل خلف CGNAT من شبكة لا تسمح إلا ب RDP
وهذه حالة من تجربة شخصية، فاتصال منزلي خلف CGNAT، فلا يوجد عنوان عام أفتح عليه منفذاً، وشبكة العمل لا تسمح بتثبيت أي برنامج VPN، والاتصال الوحيد المسموح بالخروج منها هو سطح المكتب البعيد Remote Desktop Protocol واختصاراً RDP. والحل الذي أستخدمه هو نفق SSH عكسي عبر سيرفر آخر، فجهاز المنزل يفتح بنفسه اتصال SSH إلى سيرفر VPS ويطلب منه أن يستمع على المنفذ 3389، ومن العمل أتصل ب RDP بهذا السيرفر، فيحمل النفق الاتصال إلى جهاز المنزل:

وقد يتساءل البعض: لماذا لا يبقى المنفذ على 127.0.0.1 كما نصحنا أعلاه؟ والإجابة أن جهاز العمل لا يستطيع أن يفتح SSH إلى السيرفر أصلاً، فلا يوجد طريق ل ssh -L من هناك، وبالتالي يجب أن يكون المنفذ 3389 على عنوان السيرفر العام. ولكن هذا لا يعني أن تفتحه للعالم، فالمنفذ 3389 من أكثر المنافذ التي يفحصها المخترقون ويجربون عليها كلمات المرور، والحل أن تجمع عدة طبقات معاً كما يلي.
أولاً على ال VPS، نسمح للمستخدم الذي يملك النفق أن يطلب العنوان بنفسه، فنضيف السطر التالي إلى /etc/ssh/sshd_config ثم نعيد تحميل sshd:
GatewayPorts clientspecifiedثم نقيد مفتاح جهاز المنزل في ملف authorized_keys لمستخدم مخصص للنفق، فلا يستطيع هذا المفتاح أن يفتح Shell، ولا أن يطلب أي منفذ غير 3389، وهذه الخيارات يشرحها قسم authorized_keys في دليل sshd:
restrict,port-forwarding,permitlisten="0.0.0.0:3389",command="/sbin/nologin" ssh-ed25519 AAAAC3Nza... home-pcثم نفتح المنفذ في جدار الحماية لعنوان العمل العام وحده، وليكن 198.51.100.25، كما في إعداد ufw في دليل تأمين ال VPS:
sudo ufw allow from 198.51.100.25 to any port 3389 proto tcpثانياً على جهاز المنزل، النفق يجب أن يبقى متصلاً طوال الوقت، وأن يعود وحده إذا انقطع الإنترنت أو أعيد تشغيل ال Router، ولهذا نستخدم autossh الذي يراقب ssh ويعيد تشغيله:
AUTOSSH_GATETIME=0 autossh -M 0 -f -N \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-R 0.0.0.0:3389:localhost:3389 [email protected]في الإعداد أعلاه لاحظ التالي:
ServerAliveInterval=30وServerAliveCountMax=3يجعلان ssh يرسل رسالة كل 30 ثانية، فإذا لم يرد السيرفر على ثلاث رسائل متتالية أغلق الاتصال، وعندها يعيد autossh فتحه. وبدونهما قد يبقى ssh معلقاً على اتصال ميت لدقائق طويلة.ExitOnForwardFailure=yesيجعل ssh يخرج إذا رفض السيرفر فتح المنفذ، بدلاً من أن يبقى متصلاً دون نفق، وهذا يحدث مثلاً عندما يبقى اتصال قديم ممسكاً بالمنفذ 3389 على السيرفر.-M 0يلغي منفذ المراقبة الخاص ب autossh ويعتمد على خيارات ssh السابقة، وAUTOSSH_GATETIME=0يجعله يعيد المحاولة حتى لو فشل الاتصال الأول.
ولتتأكد، افحص المنفذ على ال VPS، ثم اتصل به من جهاز آخر. في المثال التالي وضعنا مكان RDP خدمة TCP بسيطة على المنفذ 3389 في جهاز المنزل ترد بسطر واحد:
ss -tln | grep 3389
nc -w 2 203.0.113.10 3389LISTEN 0 128 0.0.0.0:3389 0.0.0.0:*
home-pc RDP stand-in on port 3389وعندما قطعنا جلسة SSH من جهة السيرفر ظهر في سجل sshd السطر mm_reap: child terminated by signal 15، وبعد ثوان فتح autossh اتصالاً جديداً وعاد المنفذ 3389 يستمع، دون أن نلمس جهاز المنزل. أما إذا حاول المفتاح نفسه أن يطلب منفذاً آخر غير المسموح به في permitlisten فالمخرج سوف يكون كما يلي:
ssh -N -o ExitOnForwardFailure=yes -R 0.0.0.0:2222:localhost:22 [email protected]Error: remote port forwarding failed for listen port 2222ولكي يعمل النفق بعد إعادة تشغيل جهاز المنزل، اجعله خدمة System Service. فإذا كان جهاز المنزل يعمل بنظام Linux فملف systemd التالي يكفي، ولاحظ أن Restart=always هنا يقوم بعمل autossh، فنستخدم ssh مباشرة:
# /etc/systemd/system/rdp-tunnel.service
[Unit]
Description=Reverse SSH tunnel for RDP
After=network-online.target
Wants=network-online.target
[Service]
User=tunnel
ExecStart=/usr/bin/ssh -N -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes -R 0.0.0.0:3389:localhost:3389 [email protected]
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetsudo systemctl enable --now rdp-tunnel.serviceأما إذا كان جهاز المنزل يعمل بنظام Windows، وهو الغالب عندما يكون الهدف RDP، فإن برنامج OpenSSH المدمج في Windows يقبل أمر ssh نفسه بالخيارات نفسها، وتجعله يعمل دائماً عبر مهمة مجدولة Scheduled Task تبدأ عند تشغيل الجهاز At startup، وتعمل سواءً سجل المستخدم دخوله أم لا، مع خيار إعادة التشغيل عند الفشل Restart on failure.
ثالثاً جهاز المنزل نفسه، فالنفق يوصل المخترق إلى شاشة الدخول إذا عرف عنوان السيرفر واستطاع تجاوز جدار الحماية، لذلك أبق خيار المصادقة على مستوى الشبكة Network Level Authentication واختصاراً NLA مفعلاً، واستخدم كلمة مرور قوية لحساب Windows، وفعل قفل الحساب بعد عدد من المحاولات الفاشلة Account Lockout. وبهذا الشكل نكون قد جمعنا أربع طبقات: جدار حماية لا يقبل إلا عنوان العمل، ومفتاح SSH لا يفتح إلا منفذاً واحداً، وNLA، وقفل الحساب.
GatewayPorts yes، والسبب أن خدمة RDP المكشوفة من أكثر الطرق التي يدخل منها المخترقون، ويكفي أن يجد الفاحص المنفذ مفتوحاً حتى يبدأ بتجربة كلمات المرور.التوجيه الديناميكي ssh -D: بروكسي SOCKS عبر السيرفر
في التوجيه المحلي تحدد وجهة واحدة في الأمر، فإذا احتجت إلى عشر خدمات داخلية احتجت إلى عشرة خيارات -L. أما التوجيه الديناميكي فيجعل ssh بروكسي SOCKS على جهازك، فكل طلب يحدد وجهته بنفسه، ويفتحها sshd من داخل شبكة السيرفر، كما في الصورة التالية:

ssh -f -N -D 1080 [email protected]
ss -tln | grep 1080
curl -s --socks5-hostname 127.0.0.1:1080 http://app.internal/ | grep -E "^(Hostname|RemoteAddr|Host):"LISTEN 0 128 127.0.0.1:1080 0.0.0.0:*
LISTEN 0 128 [::1]:1080 [::]:*
Hostname: app
RemoteAddr: 192.168.80.2:49198
Host: app.internalلاحظ أن الخيار --socks5-hostname يجعل اسم app.internal يحل على السيرفر وليس على جهازك، ولهذا نجح الطلب رغم أن جهازك لا يعرف هذا الاسم. وفي المتصفح تضع العنوان 127.0.0.1:1080 في إعداد بروكسي SOCKS5، وتفعل خيار تمرير طلبات DNS عبر البروكسي، فتفتح الواجهات الداخلية كأنك على السيرفر. ولا تستخدم هذا النوع بديلاً دائماً لشبكة VPN للفريق، والسبب أن كل من يملك المفتاح يصل إلى كل ما يصل إليه السيرفر دون أي قائمة وصول.
كيف تحمي أنفاق SSH على سيرفرك؟
توجيه المنافذ مفيد لك ولغيرك، فمن يسرق مفتاحاً يدخل به إلى السيرفر يستطيع أن يستخدم السيرفر جسراً إلى شبكتك الداخلية، لذلك عليك بما يلي:
- المفاتيح فقط: عطل الدخول بكلمة المرور كما في دليل تأمين ال VPS، والسبب أن النفق الدائم يعني اتصالاً دائماً، وكلمة المرور على اتصال دائم دعوة مفتوحة للتخمين.
- حدد ما يسمح به كل مفتاح: الخيار
restrictفيauthorized_keysيمنع كل شيء، ثم تضيف ما تحتاجه فقط، مثلpermitopen="localhost:5432"للتوجيه المحلي أوpermitlisten="0.0.0.0:3389"للعكسي. وعندما قيدنا مفتاحاً بpermitopen="localhost:5432"نجح الاتصال بقاعدة البيانات، وفشل الوصول إلىapp.internal:80بالخطأchannel 4: open failed: administratively prohibited: open failed، أما محاولة فتح Shell بالمفتاح نفسه فردت بالعبارةThis account is not available. - عطل التوجيه لمن لا يحتاجه: إذا كان على السيرفر مستخدمون لا يحتاجون إلى الأنفاق فاجعل
AllowTcpForwarding noلهم في كتلةMatch User. ولاحظ أن الدليل نفسه ينبه إلى أن منع التوجيه لا يحمي السيرفر إذا كان المستخدم يملك Shell، والسبب أنه يستطيع تشغيل برنامج توجيه خاص به. - أبق
GatewayPortsعلىnoإلا في حالة محددة مثل حالة RDP أعلاه، ومعها جدار حماية يحصر المنفذ.
نشر خدمة من خلف NAT دون فتح منافذ
في التوجيه العكسي رأينا الفكرة التي تقوم عليها كل أدوات هذا القسم: الجهاز الداخلي يتصل خارجاً بسيرفر وسيط Relay له عنوان عام، ويبقي هذا الاتصال مفتوحاً، ثم يصل الزائر إلى السيرفر الوسيط، فيرسل الطلب عبر الاتصال المفتوح إلى الداخل. ولا يحتاج جهازك إلى عنوان عام ولا إلى منفذ مفتوح في ال Router، والفرق بين الطريقتين في الصورة التالية:

وقد يتساءل البعض: إذا كان ssh -R يفعل ذلك، فلماذا توجد أدوات أخرى؟ والإجابة أن ssh يحمل منافذ TCP فقط، ولا يعرف أسماء النطاقات، فلا يستطيع أن يوجه app.example.com إلى جهاز وwiki.example.com إلى جهاز آخر على المنفذ نفسه، ولا يحمل UDP، ولا توجد فيه لوحة تحكم، ولا يصدر شهادات TLS. وهذا ما تضيفه الأدوات التالية:
- Cloudflare Tunnel: السيرفر الوسيط هو شبكة Cloudflare نفسها، والبرنامج
cloudflaredعلى سيرفرك هو الوكيل Agent. ولن نكرر شرحه هنا، فقد شرحنا ما يقدمه وأين يقف في خدمات Cloudflare وبدائلها، وإعداده خطوة خطوة في تشغيل خادمك من إنترنت المنزل. وتذكر أن TLS ينتهي عند Cloudflare، أي أنها ترى حركة موقعك دون تشفير. - frp برخصة Apache-2.0: سيرفر
frpsعلى VPS لك ووكيلfrpcفي الداخل، وينشر TCP وUDP وHTTP وHTTPS، ويوجه حسب اسم النطاق. والمشروع نشط، فآخر إصدار عند كتابة المقال v0.71.0 في أغسطس 2026. - rathole برخصة Apache-2.0: نفق خفيف مكتوب بلغة Rust وفكرته مثل frp، ولكن آخر إصدار مستقر منه هو v0.5.0 في أكتوبر 2023، وما بعده بضعة تعديلات متفرقة وإصدار تجريبي dev-latest، فاعتبره مشروعاً قليل الصيانة ولا تبن عليه خدمة مهمة.
- Pangolin: الأقرب لتجربة Cloudflare Tunnel على سيرفرك، فهو Reverse Proxy مع لوحة تحكم وتسجيل دخول للمستخدمين، ووكيله الداخلي اسمه Newt ويتصل عبر WireGuard. وملف الرخصة يقول إن الملفات برخصة AGPL-3.0 ما لم يذكر رأس الملف رخصة Fossorial التجارية، أي أن بعض الأجزاء تجارية. والمشروع نشط جداً، فقد صدر منه 1.24.0 في 30 سبتمبر 2026.
- ngrok: خدمة تجارية مشهورة للفكرة نفسها، تفيد عندما تريد أن تعرض تطبيقاً على جهازك لزميل أو لتجربة Webhook لدقائق، والسيرفر الوسيط هنا تملكه الشركة وليس أنت.
مثال كامل: نشر تطبيق من خلف NAT مع frp
سوف نقوم بنشر تطبيق ويب على جهاز في المنزل ليس له عنوان عام، باستخدام VPS عليه frps وعنوانه relay.example.com. والتطبيق في مثالنا هو traefik/whoami على جهاز اسمه homeapp في شبكة المنزل، وهو لا يصل إلى الإنترنت ولا يصل إليه أحد، والوكيل frpc هو الوحيد الذي يرى الشبكتين. ونبدأ بتوكن مشترك بين الطرفين:
openssl rand -hex 32ملف frps.toml على ال VPS:
bindPort = 7000
vhostHTTPPort = 8080
auth.method = "token"
auth.token = "ضع-التوكن-هنا"وملف frpc.toml على جهاز المنزل:
serverAddr = "relay.example.com"
serverPort = 7000
auth.method = "token"
auth.token = "ضع-التوكن-هنا"
[[proxies]]
name = "web"
type = "http"
localIP = "homeapp"
localPort = 80
customDomains = ["app.example.com"]ثم نشغل frps -c frps.toml على ال VPS، وfrpc -c frpc.toml في المنزل. والمخرج في سجل frps سوف يكون كما يلي:
[I] [frps/root.go:115] frps uses config file: /etc/frp/frps.toml
[I] [server/service.go:253] frps tcp listen on 0.0.0.0:7000
[I] [server/service.go:325] http service listen on 0.0.0.0:8080
[I] [frps/root.go:124] frps started successfully
[I] [server/service.go:803] [f743c2f23b6548ae] client login info: ip [192.168.96.3:48540] version [0.71.0] hostname [home] os [linux] arch [amd64]
[I] [proxy/http.go:106] [f743c2f23b6548ae] [web] http proxy listen for host [app.example.com] location [] group [], routeByHTTPUser []
[I] [server/control.go:761] [f743c2f23b6548ae] new proxy [web] type [http] successوفي سجل frpc:
[I] [client/service.go:312] try to connect to server...
[I] [client/service.go:332] [f743c2f23b6548ae] login to server success, get run id [f743c2f23b6548ae]
[I] [proxy/proxy_manager.go:183] [f743c2f23b6548ae] proxy added: [web]
[I] [client/control.go:174] [f743c2f23b6548ae] [web] start proxy successالآن نطلب التطبيق من الخارج عبر ال VPS باسم النطاق، ثم باسم آخر غير معرف:
curl -s -H "Host: app.example.com" http://relay.example.com:8080/ | grep -E "^(Hostname|Host|X-Forwarded-For):"
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: other.example.com" http://relay.example.com:8080/Hostname: homeapp
Host: app.example.com
X-Forwarded-For: 192.168.96.1
404في الإعداد أعلاه لاحظ التالي:
- الاتصال بين
frpcوfrpsعلى المنفذ 7000 بدأ من المنزل، فلا يوجد أي منفذ مفتوح في Router المنزل، وعلى ال VPS تفتح المنفذ 7000 والمنفذ الذي يصل إليه الزوار فقط. frpsيوجه الطلب حسب الترويسةHost، فالاسم غير المعرف أعاد 404، ويضيف عنوان الزائر فيX-Forwarded-For، وتجد كيف يقرأ تطبيقك هذه الترويسة في تطبيقك خلف Reverse Proxy.- الاتصال بين الطرفين مشفر ب TLS افتراضياً منذ الإصدار v0.50.0، ولكن توثيق frp يذكر أن
frpcلا يتحقق من شهادةfrpsافتراضياً، فإذا كان الطريق بين الطرفين غير موثوق فأضف شهادة وتحقق منها بالخيارtransport.tls.trustedCaFile. - المنفذ
vhostHTTPPortيقدم HTTP دون تشفير للزائر، لذلك ضع أمامه على ال VPS Reverse Proxy مع شهادة، مثل Nginx Proxy Manager، أو استخدم نوعhttpsفي frp.
والآن لنكسر الإعداد عن قصد، فنشغل frpc بتوكن خاطئ، والمخرج سوف يكون كما يلي:
[W] [client/service.go:323] connect to server error: token in login doesn't match token from configuration
login to the server failed: token in login doesn't match token from configuration. With loginFailExit enabled, no additional retries will be attemptedوهذا ما تريده تماماً. أما إذا حذفت سطري auth من frps.toml فإن أي أحد يعرف عنوان سيرفرك يستطيع أن يسجل عليه نفقاً، وعندما شغلنا frps بالسطر bindPort = 7000 وحده، اتصل به frpc غريب دون أي توكن وفتح على سيرفرنا المنفذ 6000:
[I] [client/service.go:332] [11272b3a1268c9f0] login to server success, get run id [11272b3a1268c9f0]
[I] [proxy/proxy_manager.go:183] [11272b3a1268c9f0] proxy added: [stranger]
[I] [client/control.go:174] [11272b3a1268c9f0] [stranger] start proxy successأي أن سيرفرك يصبح بوابة مجانية لأي شخص ينشر عبرها ما يريد باسم عنوانك، لذلك لا تشغل frps أبداً دون المصادقة بالتوكن أو OIDC، وحدد المنافذ التي يسمح بفتحها بالإعداد allowPorts.
أنفاق طبقة الشبكة: GRE وIP-in-IP وVXLAN
كل ما سبق يحمل اتصالات TCP أو طلبات HTTP، أما هذا النوع فيحمل حزم IP كاملة، أو إطارات Ethernet كاملة، ويستخدم لربط شبكتين كأنهما شبكة واحدة. والفكرة هي التغليف نفسه الذي بدأنا به، فالحزمة الأصلية بعناوينها الخاصة توضع داخل حزمة جديدة عناوينها هي عناوين طرفي النفق على الإنترنت، واسم الشبكة التي تحمل النفق Underlay، والشبكة التي تعمل داخله Overlay، كما في الصورة التالية:

- IP-in-IP كما في RFC 2003: أبسط الأنواع، رأس IP جديد فوق الحزمة الأصلية، ويحمل IPv4 فقط، ويضيف 20 بايت.
- GRE واختصاره Generic Routing Encapsulation كما في RFC 2784: يضيف رأساً صغيراً يحدد نوع ما بداخله، فيحمل IPv4 وIPv6 وبروتوكولات أخرى، ويضيف 24 بايت، وتجده في أجهزة الشبكات وعند مزودي الحماية من DDoS.
- VXLAN واختصاره Virtual Extensible LAN كما في RFC 7348: يحمل إطارات Ethernet كاملة داخل UDP على المنفذ 4789، ومعها رقم شبكة VNI يسمح بأكثر من 16 مليون شبكة منفصلة، ويضيف 50 بايت. وهو ما تستخدمه شبكة overlay في Docker Swarm التي ذكرناها في شبكات Docker، وتستخدمه شبكات Kubernetes مثل Calico بخياري VXLAN وIP-in-IP، لتصل ال Pods على سيرفرات مختلفة إلى بعضها.
وفي Linux تنشئ هذه الأنفاق بالأمر ip link. المثال التالي ينشئ نفق IP-in-IP بين موقعين، عنوان الأول على شبكة ال Underlay هو 192.168.128.2 والثاني 192.168.128.3، ويعطي طرفي النفق العنوانين 10.200.0.1 و10.200.0.2. على الموقع الأول:
ip link add t1 type ipip local 192.168.128.2 remote 192.168.128.3
ip addr add 10.200.0.1/30 dev t1
ip link set t1 upوعلى الموقع الثاني الأوامر نفسها مع عكس العنوانين و10.200.0.2/30، ثم نرسل ping من الأول، ونراقب على الثاني ما يمر على الشبكة الحقيقية:
ping -c 2 10.200.0.2
tcpdump -n -i eth0 -c 2 ip proto 464 bytes from 10.200.0.2: seq=0 ttl=64 time=2.033 ms
64 bytes from 10.200.0.2: seq=1 ttl=64 time=0.295 ms
12:59:44.120701 IP 192.168.128.2 > 192.168.128.3: IP 10.200.0.1 > 10.200.0.2: ICMP echo request, id 75, seq 0, length 64
12:59:44.121924 IP 192.168.128.3 > 192.168.128.2: IP 10.200.0.2 > 10.200.0.1: ICMP echo reply, id 75, seq 0, length 64لاحظ أن كل سطر في مخرج tcpdump فيه حزمتان، الخارجية بعناوين ال Underlay، وداخلها الحزمة الأصلية بعناوين النفق. وأنفاق GRE تنشأ بالطريقة نفسها مع type gre، ولكنها تحتاج إلى الوحدة ip_gre في النواة Kernel، وبعض البيئات لا تحملها، فيظهر الخطأ Error: Unknown device type..
والآن نفق VXLAN بين الموقعين نفسيهما، برقم شبكة 100 على المنفذ 4789:
ip link add vxlan100 type vxlan id 100 remote 192.168.128.3 dstport 4789 dev eth0
ip addr add 10.100.0.1/24 dev vxlan100
ip link set vxlan100 up
ip -d link show vxlan1003: vxlan100: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/ether b6:d5:75:80:19:6a brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535
vxlan id 100 remote 192.168.128.3 dev eth0 srcport 0 0 dstport 4789 ttl auto ageing 300 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 لاحظ أن mtu أصبح 1450 بدلاً من 1500، وفي نفق IP-in-IP كان 1480، وهذه هي البايتات التي أخذها التغليف من كل حزمة. وإذا نسيت ذلك في شبكة أكبر فسوف ترى أغرب مشكلة في الشبكات، وهي أن ping يعمل وبعض الصفحات لا يكتمل تحميلها، وقد شرحنا أثرها في مشكلات MTU في دليل WireGuard.
والسؤال المهم هنا: هل هذا النفق آمن؟ لا، والتفصيل فيما يلي. أرسلنا عبر نفق VXLAN سطراً يشبه بيانات دخول، وراقبنا الشبكة الحقيقية من الطرف الآخر:
echo "user=admin password=S3cret" | nc -w 1 10.100.0.2 5000
tcpdump -n -i eth0 -vv -X udp port 4789والحزمة التي تحمل البيانات ظهرت في مخرج tcpdump كما يلي:
12:58:37.196183 IP (tos 0x0, ttl 64, id 47834, offset 0, flags [none], proto UDP (17), length 129)
192.168.128.2.47718 > 192.168.128.3.4789: [bad udp cksum 0xef25 -> 0xe085!] VXLAN, flags [I] (0x08), vni 100
IP (tos 0x0, ttl 64, id 65294, offset 0, flags [DF], proto TCP (6), length 79)
10.100.0.1.42349 > 10.100.0.2.5000: Flags [P.], cksum 0x150c (incorrect -> 0x066c), seq 1:28, ack 1, win 507, options [nop,nop,TS val 1157313672 ecr 2897517599], length 27
0x0060: 3488 acb4 9c1f 7573 6572 3d61 646d 696e 4.....user=admin
0x0070: 2070 6173 7377 6f72 643d 5333 6372 6574 .password=S3cret
0x0080: 0a .أي أن كل من يقف على الطريق بين الموقعين، من مزود الإنترنت إلى أي جهاز في المنتصف، يقرأ ما بداخل النفق كما هو، والسبب أن GRE وIP-in-IP وVXLAN صممت لتغليف الحزم وتوجيهها وليس لحمايتها، فلا يوجد فيها تشفير ولا تحقق من هوية الطرف الآخر. لذلك عندما تمر هذه الأنفاق عبر الإنترنت توضع فوق IPsec، أو تستبدل كلياً بنفق مشفر مثل WireGuard، ولهذا السبب أيضاً يوفر Docker الخيار --opt encrypted لشبكة overlay، والذي يضيف IPsec فوق VXLAN.
أنفاق VPN: النفق المشفر لشبكة كاملة
شبكة VPN هي النوع الذي يجمع ما سبق، فهي نفق على مستوى الشبكة مثل GRE، ولكن مع التشفير والمصادقة، فيدخل جهازك أو فرعك إلى شبكة أخرى كأنه جزء منها، وكل ما يمر بينهما مشفر. وقد شرحنا أنواعها، أي الوصول عن بعد Remote Access والربط بين موقعين Site-to-Site والشبكة المتداخلة Mesh، وبروتوكولاتها ومصطلحاتها في ما هو VPN؟ شرح للمبتدئين، وطريقة تشغيل واحدة على سيرفرك في شبكة خاصة مع WireGuard وواجهة wg-easy.
المقارنة بين أنواع الأنفاق
| النوع | ما يحمله | من يبدأ الاتصال | مشفر؟ | الاستخدام الشائع | أمثلة |
|---|---|---|---|---|---|
SSH محلي -L | منفذ TCP واحد لكل خيار | جهازك إلى السيرفر | نعم، داخل SSH | فتح قاعدة بيانات أو واجهة إدارة على السيرفر | OpenSSH |
SSH عكسي -R | منفذ TCP يعود إلى جهازك | الجهاز الداخلي إلى السيرفر | نعم، داخل SSH | الوصول إلى جهاز خلف NAT أو CGNAT | OpenSSH، autossh |
SSH ديناميكي -D | أي وجهة TCP عبر SOCKS | جهازك إلى السيرفر | نعم، داخل SSH | تصفح الواجهات الداخلية مؤقتاً | OpenSSH |
| نفق عكسي عبر وسيط | HTTP وHTTPS وTCP وUDP | الوكيل الداخلي إلى ال Relay | بين الوكيل والوسيط، وTLS ينتهي عند الوسيط | نشر خدمة من المنزل دون فتح منافذ | Cloudflare Tunnel، frp، Pangolin، rathole، ngrok |
| تغليف طبقة الشبكة | حزم IP أو إطارات Ethernet | الطرفان معاً، دون مصادقة | لا | ربط الشبكات، وشبكات Overlay في Swarm وKubernetes | GRE، IP-in-IP، VXLAN |
| VPN | حزم IP لجهاز أو شبكة كاملة | الكلاينت، أو الطرفان في Site-to-Site | نعم | وصول الفريق إلى الشبكة الخاصة وربط الفروع | WireGuard، IPsec، OpenVPN |
أي نفق تحتاج؟
- أريد أن أفتح قاعدة البيانات أو واجهة إدارة على سيرفري مرة واحدة:
ssh -L، ثم أغلقه عندما تنتهي، ولا تنشر المنفذ أبداً. - أريد أن أتصفح عدة واجهات داخلية لبعض الوقت:
ssh -Dمع بروكسي SOCKS في المتصفح. - أريد أن أنشر تطبيقاً من المختبر المنزلي Homelab للعامة دون فتح منافذ: Cloudflare Tunnel إذا قبلت أن ترى Cloudflare الحركة، أو frp أو Pangolin على VPS لك إذا أردت أن يبقى كل شيء عندك.
- أريد أن أصل إلى جهاز في المنزل خلف CGNAT من مكان واحد معروف:
ssh -Rإلى VPS مع جدار حماية يقبل عنوان ذلك المكان فقط، كما في حالة RDP أعلاه. - أريد أن أربط موقعين أو سيرفرين كأنهما شبكة واحدة: WireGuard Site-to-Site، أو GRE وVXLAN فوق IPsec إذا كانت أجهزتك تتطلب ذلك، ولا تمرر GRE أو VXLAN عبر الإنترنت دون تشفير.
- أريد أن يصل فريقي إلى الخدمات الخاصة كل يوم: هذه حالة VPN وليست حالة نفق SSH، فراجع شرح VPN ودليل WireGuard.
أخطاء الأمان الشائعة مع الأنفاق
- النفق يتجاوز سياسة جدار الحماية: جدار الحماية يرى اتصال SSH أو اتصالاً خارجاً على 443، ولا يرى ما بداخله، لذلك فإن كل نفق هو فتحة في سياستك أنت قررت وجودها. فاكتب قائمة بالأنفاق الدائمة على سيرفراتك، ومن يملك كل منها، ولماذا يوجد.
- كشف خدمة داخلية للإنترنت دون قصد:
GatewayPorts yesأوclientspecifiedدون جدار حماية، أوssh -L 0.0.0.0:5432:...على جهاز في شبكة مشتركة، أوfrpsدون توكن، وكل منها رأيناه أعلاه بمخرجه الحقيقي. - نفق دائم بكلمة مرور أو بمفتاح دون قيود: النفق العكسي الدائم يعني أن على جهاز المنزل مفتاحاً يدخل إلى ال VPS طوال الوقت، فاجعل له مستخدماً خاصاً ومفتاحاً مقيداً ب
restrictوpermitlisten، ولا تستخدم مفتاحك الشخصي أو حساب root. أما الدخول التفاعلي إلى السيرفر فأضف إليه عاملاً ثانياً للمصادقة MFA إذا كان متاحاً لك. - الثقة في السيرفر الوسيط: اسأل نفسك أين ينتهي TLS، ففي Cloudflare Tunnel ينتهي عند Cloudflare، وفي frp مع
vhostHTTPPortلا يوجد TLS للزائر أصلاً إلا إذا أضفته، ومن يتحكم في الوسيط يرى الحركة أو يستطيع تغييرها. - أنفاق لا يراقبها أحد: راقب المنافذ التي تستمع على سيرفراتك بالأمر
ss -tlnp، وسجل sshd الذي يذكر كل توجيه مرفوض، وسجلاتfrpsالتي تذكر كل كلاينت يسجل دخوله وعنوانه، والسبب أن النفق الذي نسيته هو النفق الذي سوف يستخدمه غيرك. - سياسة الجهة التي تعمل بها: في الشركات والجهات الحكومية قد تكون الأنفاق ممنوعة في سياسة أمن المعلومات حتى لو سمحت بها الشبكة تقنياً، لذلك لا تفتح نفقاً من شبكة العمل أو إليها قبل أن يوافق عليه فريق تقنية المعلومات.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- النفق يحمل اتصالاً داخل اتصال آخر، ويحل أربع مشكلات: الوصول إلى خدمة غير منشورة، والمرور عبر شبكة تمنعك، وربط شبكتين، ونشر خدمة من خلف NAT دون فتح منافذ.
ssh -Lيوصلك إلى خدمة على السيرفر أو خلفه، وssh -Rيفتح على السيرفر منفذاً يعود إلى جهازك، وssh -Dيجعل السيرفر بروكسي SOCKS لك، وكلها مشفرة داخل SSH.- أبق
GatewayPortsعلىno، وقيد كل مفتاح نفق بrestrictوpermitopenأوpermitlisten، وإذا احتجت إلى منفذ عكسي عام فاحصره في جدار الحماية على عنوان واحد. - الأنفاق العكسية عبر وسيط، مثل Cloudflare Tunnel وfrp وPangolin، تنشر خدمات المنزل دون منافذ مفتوحة، فلا تشغل
frpsدون توكن، واعرف أين ينتهي TLS. أما rathole فلم يصدر منه إصدار مستقر منذ 2023. - GRE وIP-in-IP وVXLAN تغلف الحزم دون أي تشفير، وتأخذ من MTU، فضعها فوق IPsec أو استخدم WireGuard عندما تمر عبر الإنترنت.
- وصول الفريق اليومي إلى الشبكة الخاصة مكانه VPN وليس نفق SSH.
دليلما هو VPN؟ شرح الشبكة الخاصة الافتراضية من المشكلة إلى الأنواع والبروتوكولاتلماذا تحتاج شبكة خاصة افتراضية VPN، وكيف تغلف حزمك وتشفرها داخل نفق، وما الفرق بين Remote Access وSite-to-Site وMesh وZero Trust، مع مقارنة WireGuard وOpenVPN وIPsec وتجربة عملية تريك النفق من الداخل.
دليلشبكة خاصة مع WireGuard وواجهة wg-easyلا تحتاج لوحات الإدارة إلى أن تكون مكشوفة للإنترنت. ننشئ شبكة VPN خاصة باستخدام WireGuard وواجهة wg-easy، ونضيف إليها الأجهزة برمز QR، فلا يصل إلى أدواتك إلا أجهزة فريقك.
دليلخدمات Cloudflare بكلمات بسيطة وبدائلها مفتوحة المصدرما الذي تقدمه كل خدمة من Cloudflare، من DNS والتخزين المؤقت وWAF إلى Tunnel وWorkers، وما المتاح منها مجاناً، وما البديل مفتوح المصدر الذي تشغله على سيرفرك، وأين يستبدلها فعلاً وأين لا يمكن استبدالها.
دليلتشغيل خادمك من إنترنت المنزل: CGNAT وDDNS وCloudflare Tunnelهل يمكنك تشغيل خدماتك من اتصال الإنترنت في منزلك؟ يشرح هذا الدليل كيف تعرف ما يسمح به اتصالك، ومتى يكفي توجيه المنافذ، ولماذا يكون Cloudflare Tunnel غالباً الحل الأسهل والأكثر أماناً.
دليلتأمين خادم VPS من أول دخول على Ubuntu 24.04استلمت خادمك للتو؟ قبل أن تثبت عليه أي تطبيق، اتبع هذه الخطوات لإغلاق أبرز الثغرات: الدخول بمفتاح SSH بدلاً من كلمة المرور، وجدار حماية، وتحديثات أمنية تلقائية.سجل التحديثات
- أكتوبر 2026: كتابة المقال واختبار أمثلته على OpenSSH 10.3p1 وfrp 0.71.0 وautossh 1.4g وiproute2 7.0.0.