האתר רץ אצלכם על localhost:3000, ומישהו צריך לראות אותו עכשיו: לקוח, מעצב או הטלפון שלכם בחוץ. שירותים כמו ngrok פותרים את זה בפקודה אחת, אבל התנועה עוברת דרך שרת של מישהו אחר, והכתובת משתנה בכל הפעלה בתוכנית החינמית. אם יש לכם שרת קטן עם כתובת ציבורית, אפשר לבנות את אותו דבר בעצמכם, עם כלים שכבר מותקנים עליו.
המדריך בנוי על הרעיון מהפוסט Self-hosted HTTP tunnels with SSH and nginx של וינסנט ברנה. כאן אני בונה גרסה פשוטה יותר: טווח פורטים קבוע וסיסמה, במקום פורטים אקראיים וקישורים חתומים שפגים. בכל שלב אסביר למה הוא קיים, איך עושים אותו, ומה אמורים לראות כשהוא עובד. בסוף אסביר מה ההבדל מהגרסה המקורית ומתי כדאי לעבור אליה.
איך זה עובד
שלושה חלקים, וכל אחד עושה דבר אחד:
- 01מנהרת SSH הפוכה
המחשב שלכם פותח חיבור SSH לשרת ומבקש ממנו להאזין לפורט, למשל 8042. כל חיבור לפורט הזה בשרת עובר דרך המנהרה לשרת הפיתוח אצלכם.
- 02nginx על השרת
מקבל בקשות HTTPS לכתובות כמו p8042.tunnel.example.com, קורא את מספר הפורט מתוך שם המארח ומעביר את הבקשה ל־127.0.0.1:8042.
- 03DNS ותעודה
רשומת wildcard שמפנה כל תת־דומיין לשרת, ותעודת Let's Encrypt אחת שמכסה את כולם.
כך נראה המסלול של בקשה אחת, מהטלפון של הלקוח עד המחשב שלכם:
browser -> https://p8042.tunnel.example.com
-> DNS: *.tunnel.example.com points to the server
-> nginx on the server (TLS, password, port 8042 from the name)
-> 127.0.0.1:8042 on the server (the end of the tunnel)
-> SSH connection to your laptop
-> localhost:3000 (your dev server)
היתרון של המבנה הזה: אין שום תוכנה חדשה. SSH כבר מותקן בשני הצדדים, ו־nginx כבר רץ על רוב השרתים. אין גם צורך לפתוח פורטים בחומת האש מעבר ל־22 ול־443: הפורט של המנהרה מאזין רק ל־127.0.0.1 בשרת, ורק nginx מגיע אליו.
מה צריך לפני שמתחילים
- שרת עם כתובת IP ציבורית, עם גישת
sudo. הדוגמאות כתובות ל־Debian ול־Ubuntu; בהפצות אחרות שמות החבילות קצת שונים. - nginx מותקן על השרת. אם הוא עדיין לא שם:
sudo apt install nginx. - דומיין שאתם שולטים ב־DNS שלו. בדוגמאות הדומיין הוא
example.com, וכל המנהרות יהיו תחתtunnel.example.com. החליפו בדומיין שלכם בכל מקום. - פורטים 22 ו־443 פתוחים בחומת האש של השרת.
שלב 1: מנהרה הפוכה בפקודה אחת
למה מתחילים מכאן: זה הלב של כל הפתרון, ואפשר לבדוק אותו לבד, בלי DNS, בלי תעודה ובלי nginx. אם המנהרה לא עובדת, אין טעם להמשיך.
SSH יודע להעביר פורטים בשני כיוונים. -L (local) פותח פורט אצלכם ומעביר אותו לשרת. -R (remote), הכיוון שאנחנו צריכים, פותח פורט בשרת ומעביר כל חיבור אליו אליכם. זו מנהרה ״הפוכה״, כי החיבור נפתח מהמחשב שלכם החוצה, אבל התנועה זורמת פנימה. זה גם מה שמאפשר לה לעבוד מאחורי NAT או ראוטר ביתי: המחשב שלכם לא צריך כתובת ציבורית.
על המחשב שלכם, כשהאתר רץ על פורט 3000:
ssh -N -R 8042:localhost:3000 [email protected]
-R 8042:localhost:3000: השרת מאזין לפורט 8042, וכל חיבור אליו מגיע ל־localhost:3000אצלכם. הסדר תמיד ״פורט בשרת : לאן להעביר אצלכם״.-N: לא לפתוח shell. החיבור קיים רק בשביל המנהרה.
איך זה נראה: שום דבר. הפקודה לא מדפיסה כלום ולא מחזירה את שורת הפקודה, וזה בדיוק המצב התקין: המנהרה פתוחה כל עוד הפקודה רצה. Ctrl+C סוגר אותה.
עכשיו, בחלון אחר, התחברו לשרת ובדקו שהוא באמת מאזין:
sudo ss -tlnp | grep 8042
LISTEN 0 128 127.0.0.1:8042 0.0.0.0:* users:(("sshd",pid=41237,fd=9))
LISTEN 0 128 [::1]:8042 [::]:* users:(("sshd",pid=41237,fd=8))
שימו לב לכתובת: 127.0.0.1, לא 0.0.0.0. כברירת מחדל sshd קושר פורטים של מנהרה הפוכה רק לכתובת המקומית של השרת (ההגדרה GatewayPorts no), וזה בדיוק מה שאנחנו רוצים: מבחוץ אי אפשר להגיע ל־8042 ישירות, רק תהליכים על השרת עצמו, כמו nginx.
ובדיקה אחרונה, עדיין על השרת:
curl -I http://127.0.0.1:8042
HTTP/1.1 200 OK
Content-Type: text/html
...
אם חזרה תשובה מהאתר שלכם, המנהרה עובדת. אם קיבלתם Connection refused, בדקו שהפקודה במחשב שלכם עדיין רצה ושהאתר באמת מאזין על 3000.
אם המנהרה לא נפתחת בכלל, בדקו ש־sshd מרשה העברת פורטים:
sudo sshd -T | grep -E 'allowtcpforwarding|gatewayports'
allowtcpforwarding yes
gatewayports no
אלה ערכי ברירת המחדל. אם allowtcpforwarding הוא no, מישהו כיבה אותו ב־/etc/ssh/sshd_config.
שלב 2: משתמש נפרד למנהרות
למה: בשלב 1 התחברתם עם המשתמש הרגיל שלכם, שיש לו shell ואולי גם sudo. המפתח שישמש את המנהרות יישב על המחשב, ייכנס לסקריפטים, ואולי גם למחשב של עמית. עדיף שגם אם הוא ידלוף, כל מה שאפשר לעשות איתו הוא לפתוח מנהרה, לא להיכנס לשרת.
איך: על השרת, צרו משתמש בלי סיסמה ובלי גישת התחברות רגילה:
sudo adduser --disabled-password --gecos "" tunnel
sudo mkdir -p /home/tunnel/.ssh
sudo touch /home/tunnel/.ssh/authorized_keys
sudo chown -R tunnel:tunnel /home/tunnel/.ssh
sudo chmod 700 /home/tunnel/.ssh
sudo chmod 600 /home/tunnel/.ssh/authorized_keys
ההרשאות חשובות: אם התיקייה או הקובץ פתוחים לאחרים, sshd מתעלם מהם בשקט, והחיבור נכשל בלי הסבר ברור.
על המחשב שלכם, צרו מפתח נפרד למנהרות והציגו את החלק הציבורי שלו:
ssh-keygen -t ed25519 -f ~/.ssh/tunnel -C laptop-tunnel
cat ~/.ssh/tunnel.pub
העתיקו את השורה שהודפסה לקובץ /home/tunnel/.ssh/authorized_keys בשרת, והוסיפו לפניה הגבלות. השורה המלאה נראית כך:
restrict,port-forwarding,command="/bin/false" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... laptop-tunnel
restrictמבטל הכול: shell אינטראקטיבי, העברת סוכן, X11 והעברת פורטים.port-forwardingמחזיר רק את העברת הפורטים.command="/bin/false"מבטיח שגם אם מישהו ינסה לפתוח shell עם המפתח, הוא ייסגר מיד. עם-Nלא מתבקשת פקודה בכלל, כך שהמנהרה לא מושפעת.
איך בודקים שזה עובד: נסו להתחבר רגיל עם המפתח החדש:
ssh -i ~/.ssh/tunnel [email protected]
PTY allocation request failed on channel 0
Connection to server.example.com closed.
זו התוצאה הנכונה: החיבור נדחה מיד. ועכשיו המנהרה, עם אותו מפתח:
ssh -i ~/.ssh/tunnel -N -R 8042:localhost:3000 [email protected]
היא צריכה להיפתח כמו בשלב 1. כדי לא לכתוב -i בכל פעם, הוסיפו ל־~/.ssh/config במחשב שלכם:
Host tunnel-server
HostName server.example.com
User tunnel
IdentityFile ~/.ssh/tunnel
מעכשיו ssh -N -R 8042:localhost:3000 tunnel-server מספיק.
שלב 3: DNS לכל המנהרות
למה: כל מנהרה צריכה כתובת משלה, p<port>.tunnel.example.com, כדי ש־nginx ידע לאיזה פורט להעביר. אי אפשר להוסיף רשומת DNS לכל פורט מראש, ולכן משתמשים ברשומת wildcard: כוכבית שתופסת כל תת־דומיין שלא הוגדר במפורש.
איך: אצל ספק ה־DNS, הוסיפו שתי רשומות:
| רשומה | סוג | ערך |
|---|---|---|
tunnel.example.com | A | כתובת ה־IP של השרת |
*.tunnel.example.com | CNAME | tunnel.example.com |
הרשומה הראשונה מצביעה על השרת. השנייה אומרת ״כל מה שמתחת ל־tunnel, לך לאותו מקום״. היתרון ב־CNAME על פני A שני: אם השרת יחליף כתובת, משנים רק רשומה אחת.
אם הדומיין מאחורי Cloudflare, השאירו את שתי הרשומות במצב DNS only (ענן אפור, לא כתום). במצב Proxied, Cloudflare עומד באמצע, מציג לדפדפן תעודה משלו, וחיבורי WebSocket ארוכים עלולים להיסגר. כאן אנחנו רוצים שהדפדפן ידבר ישירות עם nginx.
איך זה נראה: אחרי כמה דקות, בדקו מהמחשב שלכם כתובת שלא הגדרתם במפורש:
dig +short p8042.tunnel.example.com
tunnel.example.com.
203.0.113.10
השורה הראשונה היא ה־CNAME, השנייה היא כתובת השרת. אם לא חוזר כלום, הרשומות עוד לא התפשטו, או שיש טעות בשם.
שלב 4: תעודת wildcard עם certbot
למה: בלי תעודה, הדפדפן יציג אזהרה אדומה, ושום לקוח לא ילחץ ״המשך בכל זאת״. תעודה רגילה מכסה שם אחד, וכאן יש אינסוף שמות, אז צריך תעודת wildcard: *.tunnel.example.com.
הבעיה: Let's Encrypt מנפיקה תעודת wildcard רק אחרי אימות דרך DNS. באימות הרגיל (HTTP), היא בודקת קובץ על שרת אחד, אבל זה לא מוכיח שאתם שולטים בכל התת־דומיינים. באימות DNS, היא מבקשת שתוסיפו רשומת TXT בשם _acme-challenge.tunnel.example.com עם ערך שהיא בחרה, ובודקת שהרשומה הופיעה. רק מי ששולט ב־DNS יכול לעשות את זה.
אפשר להוסיף את הרשומה ידנית, אבל אז תצטרכו לחזור על זה בכל חידוש, כל 90 יום. במקום זה, certbot משתמש בתוסף שמדבר עם ה־API של ספק ה־DNS, מוסיף את הרשומה, מחכה לאימות ומוחק אותה. לתוסף הזה צריך הרשאה, וזה בדיוק התפקיד של הקובץ cloudflare.ini.
התקנה
sudo apt install certbot python3-certbot-dns-cloudflare
לספקים אחרים יש תוספים דומים (python3-certbot-dns-digitalocean, python3-certbot-dns-route53 ועוד), וכל אחד מקבל קובץ הרשאות משלו.
טוקן API ב־Cloudflare
למה טוקן ולא המפתח הגלובלי: ב־Cloudflare יש Global API Key, שנותן שליטה מלאה בכל החשבון: כל הדומיינים, החיוב וההגדרות. הקובץ שניצור יישב על השרת לתמיד. אם השרת ייפרץ, אתם רוצים שהנזק יהיה מוגבל לעריכת רשומות DNS של דומיין אחד, וזה מה שטוקן עם הרשאה מצומצמת נותן.
איך יוצרים אותו:
- בלוח הבקרה של Cloudflare: My Profile ואז API Tokens.
- Create Token, ובחרו בתבנית Edit zone DNS.
- תחת Zone Resources בחרו
Include,Specific zoneואת הדומיין שלכם. אל תשאירו ״All zones״. - Continue to summary ואז Create Token. העתיקו את הטוקן: Cloudflare מציג אותו פעם אחת בלבד.
הקובץ cloudflare.ini
על השרת, צרו את הקובץ:
sudo nano /etc/letsencrypt/cloudflare.ini
התוכן הוא הערה ושורה אחת עם הטוקן שהעתקתם:
# Cloudflare API token for certbot: Zone.DNS edit on example.com only
dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567
ואז נעלו אותו, כך שרק root יוכל לקרוא:
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
אם תשכחו את ה־chmod, certbot יעבוד, אבל ידפיס אזהרה בסגנון ״Unsafe permissions on credentials configuration file״. כדאי להתייחס אליה: היא אומרת שכל משתמש על השרת יכול לקרוא את הטוקן ולשנות את ה־DNS שלכם.
הנפקת התעודה
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d 'tunnel.example.com' -d '*.tunnel.example.com' \
--deploy-hook 'systemctl reload nginx'
certonly: רק להנפיק תעודה, לא לגעת בהגדרות nginx. את ההגדרות נכתוב בעצמנו בשלב הבא.- שני ה־
-d: התעודה תכסה גם אתtunnel.example.comעצמו וגם כל תת־דומיין שלו. wildcard לא כולל את השם שמעליו. - המירכאות הבודדות סביב
*.tunnel.example.comחשובות: בלעדיהן ה־shell עלול לפרש את הכוכבית כשמות קבצים. --deploy-hook: אחרי כל חידוש מוצלח, nginx טוען את התעודה החדשה. בלי זה,certbotיחדש את הקובץ, אבל nginx ימשיך להגיש את הישנה עד שמישהו יטען אותו מחדש, ואחרי 90 יום הדפדפנים יתחילו להתריע.
איך זה נראה: אחרי כחצי דקה (התוסף מחכה שהרשומה תתפשט):
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/tunnel.example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/tunnel.example.com/privkey.pem
This certificate expires on 2027-01-03.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
שני הנתיבים האלה ייכנסו להגדרות nginx. שימו לב שהתיקייה נקראת tunnel.example.com, בלי הכוכבית.
כדי לוודא שהחידוש האוטומטי יעבוד, הריצו חידוש מדומה:
sudo certbot renew --dry-run
אם הוא מסתיים ב־״Congratulations, all simulated renewals succeeded״, אפשר לשכוח מהתעודה.
שלב 5: nginx קורא את הפורט מתוך שם המארח
למה: זה החלק שמחבר הכול. nginx מקבל בקשה ל־p8042.tunnel.example.com, וצריך להבין ממנה ״העבר ל־127.0.0.1:8042״. במקום לכתוב בלוק server לכל פורט, משתמשים ביכולת של nginx לקבל ביטוי רגולרי ב־server_name: קבוצה עם שם בתוך הביטוי הופכת למשתנה שאפשר להשתמש בו בהמשך.
איך: צרו את הקובץ /etc/nginx/sites-available/tunnel.conf:
# WebSocket: pass "Upgrade" through, otherwise close the connection normally
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
# p8000 ... p8099: the number becomes $port
server_name ~^p(?<port>80[0-9][0-9])\.tunnel\.example\.com$;
ssl_certificate /etc/letsencrypt/live/tunnel.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tunnel.example.com/privkey.pem;
auth_basic "Preview";
auth_basic_user_file /etc/nginx/tunnel.htpasswd;
location / {
proxy_pass http://127.0.0.1:$port;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 1h;
}
}
שורה אחרי שורה:
mapיוצר משתנה$connection_upgrade: אם הדפדפן ביקש לשדרג את החיבור ל־WebSocket, הערך הואupgrade; אחרתclose. הוא חייב לשבת מחוץ ל־server, וזה בסדר, כי הקבצים ב־sites-enabledנטענים בתוך הבלוקhttp. אם כבר יש לכםmapעם אותו שם בקובץ אחר, nginx יתלונן על כפילות; מחקו אחד מהם.listen 443 sslו־http2 on: HTTPS בלבד. אין צורך בבלוק לפורט 80, כי הקישורים שתשלחו יתחילו תמיד ב־https://.server_name ~^p(?<port>80[0-9][0-9])...: ה־~אומר ״זה ביטוי רגולרי״.(?<port>...)תופס את המספר לתוך המשתנה$port. הנקודות מסומנות\.כדי שיתאימו רק לנקודה אמיתית.- הטווח
80[0-9][0-9]מגביל את nginx לפורטים 8000-8099. זו החלטת אבטחה, לא נוחות: בלי הגבלה, כל מי שמגיע לכתובת יכול לבקשp5432.tunnel.example.comולהגיע לשירות שמאזין על השרת עצמו, למשל מסד נתונים. בדקו עםsudo ss -tlnpשאף שירות אחר בשרת לא משתמש בטווח שבחרתם. auth_basic: סיסמה על כל המנהרות. ההסבר המלא בשלב הבא.proxy_pass http://127.0.0.1:$port: העברה לקצה המנהרה בשרת.proxy_set_header Host $host: שרת הפיתוח יראה את השםp8042.tunnel.example.com, ולא127.0.0.1. זה חשוב לקישורים שהאתר בונה, ולבדיקת השם ש־Vite עושה (שלב 8).Upgradeו־Connection: מעבירים חיבורי WebSocket. בלעדיהם האתר ייטען, אבל ה־HMR (רענון המודולים בזמן אמת) של Vite או Nuxt לא יעבוד, והדף לא יתעדכן כשאתם שומרים קובץ.X-Forwarded-Proto https: מספר לשרת הפיתוח שהגולש הגיע ב־HTTPS, למקרה שהוא בונה כתובות מלאות או מחליט על cookies מאובטחים.proxy_read_timeout 1h: ברירת המחדל היא 60 שניות. חיבור ה־HMR שקט רוב הזמן, ובלי זה nginx יסגור אותו כל דקה.
הפעילו את הקובץ, בדקו את התחביר וטענו מחדש:
sudo ln -s /etc/nginx/sites-available/tunnel.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
nginx -t תמיד לפני reload: אם יש טעות, השרת ממשיך לרוץ עם ההגדרות הישנות במקום ליפול. כרגע הוא עוד ייכשל, כי קובץ הסיסמאות לא קיים. זה השלב הבא.
שלב 6: קובץ הסיסמאות
למה: שרת פיתוח לא נבנה כדי לעמוד מול האינטרנט. הוא מציג הודעות שגיאה מפורטות, לפעמים קוד מקור דרך source maps, ולפעמים משתני סביבה. בוטים סורקים כל הזמן תת־דומיינים חדשים, במיוחד כאלה שמופיעים ברשומות התעודות הציבוריות. סיסמה אחת מספיקה כדי שרק מי שקיבל אותה מכם יגיע לאתר.
מה זה Basic Auth: כשהדפדפן מבקש דף מוגן, nginx מחזיר 401 עם הכותרת WWW-Authenticate. הדפדפן מציג חלון קטן של שם משתמש וסיסמה, ושולח אותם בכל בקשה אחרי זה. הם נשלחים בקידוד Base64, שהוא לא הצפנה, ולכן Basic Auth בטוח רק מעל HTTPS. אצלנו כל החיבור מוצפן, כך שזה בסדר.
nginx לא שומר סיסמאות בעצמו. הוא קורא קובץ בפורמט שנולד ב־Apache: שורה לכל משתמש, ״שם:גיבוב״. הכלי שיוצר את הקובץ הזה, htpasswd, מגיע בחבילה apache2-utils. השם מטעה: היא לא מתקינה את Apache, רק כמה כלי שורת פקודה קטנים.
איך:
sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/tunnel.htpasswd preview
New password:
Re-type new password:
Adding password for user preview
-cיוצר את הקובץ. רק בפעם הראשונה: על קובץ קיים הוא מוחק את כל מה שהיה בו.previewהוא שם המשתמש. אפשר לבחור כל שם.
כדי להוסיף משתמש נוסף, למשל לקוח מסוים, בלי -c:
sudo htpasswd /etc/nginx/tunnel.htpasswd client-acme
וכדי להסיר משתמש: sudo htpasswd -D /etc/nginx/tunnel.htpasswd client-acme.
איך זה נראה בפנים:
sudo cat /etc/nginx/tunnel.htpasswd
preview:$apr1$Qx3kq7Lb$2Uu1k9oW0vN8yJm4Hq6cX/
client-acme:$apr1$8Hn2Wd0p$Jf5sTzQe7RkL1vB3nY9aM.
הסיסמה עצמה לא שמורה, רק גיבוב שלה. $apr1$ מציין את שיטת הגיבוב. nginx צריך לקרוא את הקובץ, אבל אף אחד אחר לא:
sudo chown root:www-data /etc/nginx/tunnel.htpasswd
sudo chmod 640 /etc/nginx/tunnel.htpasswd
אם אתם לא רוצים להתקין חבילה בשביל כלי אחד, openssl יכול לייצר את אותה שורה:
printf 'preview:%s\n' "$(openssl passwd -apr1)" | sudo tee /etc/nginx/tunnel.htpasswd
עכשיו אפשר לטעון את nginx: sudo nginx -t && sudo systemctl reload nginx.
הבדיקה המלאה: כשהמנהרה משלב 2 רצה, מהמחשב שלכם:
curl -I https://p8042.tunnel.example.com
HTTP/2 401
www-authenticate: Basic realm="Preview"
בלי סיסמה, 401. עם סיסמה:
curl -I -u preview https://p8042.tunnel.example.com
Enter host password for user 'preview':
HTTP/2 200
content-type: text/html
200 אומר שכל השרשרת עובדת: DNS, תעודה, nginx, סיסמה, מנהרה ושרת הפיתוח. בדפדפן תראו חלון התחברות, ואחריו את האתר שלכם.
שלב 7: פקודה אחת ששולחת קישור
למה: כל ההגדרות עד כאן נעשות פעם אחת. מה שתעשו כל יום הוא לפתוח מנהרה, וכדאי שזו תהיה פקודה של מילה אחת.
איך: על המחשב שלכם, צרו את הקובץ ~/bin/share:
#!/usr/bin/env bash
# share <local-port> [remote-port]: expose a local dev server at https://p<port>.tunnel.example.com
set -euo pipefail
local_port=${1:?usage: share <local-port> [remote-port]}
remote_port=${2:-$((8000 + RANDOM % 100))}
echo "https://p${remote_port}.tunnel.example.com"
exec ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \
-R "${remote_port}:localhost:${local_port}" tunnel-server
set -euo pipefail: הסקריפט עוצר בשגיאה הראשונה במקום להמשיך בשקט.${1:?usage: ...}: אם לא נתתם פורט, הסקריפט מדפיס את שורת השימוש ויוצא.${2:-$((8000 + RANDOM % 100))}: בלי פורט שני, נבחר פורט אקראי בטווח 8000-8099, אותו טווח ש־nginx מכיר.exec ssh: הסקריפט מוחלף בתהליך ה־ssh, כך ש־Ctrl+C סוגר את המנהרה ישירות.ExitOnForwardFailure=yes: אם הפורט כבר תפוס בשרת (מנהרה אחרת שלכם, או שירות אחר),sshיוצא עם שגיאה. בלי האפשרות הזו הוא רק מדפיס אזהרה ונשאר מחובר בלי מנהרה, ואתם שולחים קישור שלא עובד.ServerAliveInterval=30: סימן חיים כל 30 שניות, כדי שראוטר או חומת אש לא יסגרו חיבור שנראה להם שקט.tunnel-server: השם מ־~/.ssh/configשהגדרנו בשלב 2, עם המשתמש והמפתח הנכונים.
הפכו אותו לבר הרצה, ובדקו ש־~/bin נמצא ב־PATH:
chmod +x ~/bin/share
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc # or ~/.bashrc
איך זה נראה:
share 3000
https://p8057.tunnel.example.com
והסמן נשאר מהבהב: המנהרה פתוחה. שלחו את הכתובת יחד עם שם המשתמש והסיסמה. כשמסיימים, Ctrl+C, והכתובת מחזירה מיד 502 Bad Gateway.
אם הפורט שנבחר תפוס:
https://p8042.tunnel.example.com
Error: remote port forwarding failed for listen port 8042
מריצים שוב, או נותנים פורט מפורש: share 3000 8010.
שלב 8: Vite ושם המארח
למה: שרתי פיתוח חדשים בודקים את כותרת Host ודוחים שמות שהם לא מכירים. זו הגנה מפני DNS rebinding: מתקפה שבה אתר זר מפנה את הדומיין שלו לכתובת המקומית שלכם, וכך קוד באתר הזר קורא נתונים משרת הפיתוח שלכם. בגלל ש־nginx מעביר את השם p8042.tunnel.example.com, השרת רואה שם שהוא לא מכיר.
איך זה נראה: במקום האתר, הדפדפן מציג:
Blocked request. This host ("p8042.tunnel.example.com") is not allowed.
To allow this host, add "p8042.tunnel.example.com" to `server.allowedHosts` in vite.config.js.
איך מתקנים: מוסיפים את הדומיין לרשימה המותרת:
// vite.config.ts
export default defineConfig({
server: {
allowedHosts: ['.tunnel.example.com'],
},
})
הנקודה בתחילת הערך מתירה כל תת־דומיין, כך שלא צריך לעדכן את ההגדרה כשהפורט משתנה. ב־Nuxt אותה הגדרה נכנסת תחת vite:
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
server: {
allowedHosts: ['.tunnel.example.com'],
},
},
})
כשמשהו לא עובד
| מה רואים | מה כנראה קרה | מה בודקים |
|---|---|---|
502 Bad Gateway | אין מנהרה על הפורט הזה | ש־share עדיין רץ, ושהפורט בכתובת זהה לפורט שהודפס |
401 שוב ושוב | שם משתמש או סיסמה שגויים | השורה ב־tunnel.htpasswd, ושלא דרסתם את הקובץ עם -c |
| שגיאת תעודה בדפדפן | הרשומה ב־Cloudflare במצב Proxied, או שהתעודה לא כוללת את ה־wildcard | ענן אפור, ו־sudo certbot certificates |
| הדף של ברירת המחדל של nginx | הפורט מחוץ לטווח 8000-8099, או שהקובץ לא ב־sites-enabled | הכתובת, ו־ls /etc/nginx/sites-enabled |
| ״Blocked request״ | Vite לא מכיר את השם | allowedHosts (שלב 8) |
| האתר עולה, אבל לא מתעדכן בשמירה | WebSocket לא עובר | שורות ה־Upgrade, ה־Connection וה־proxy_read_timeout |
Connection refused ב־curl על השרת | שרת הפיתוח לא מאזין על הפורט המקומי | שהאתר רץ, ועל איזה פורט |
מתי לעבור לקישורים חתומים
הגרסה כאן מתאימה כשאתם משתפים עם מעט אנשים שאתם סומכים עליהם. יש לה שתי חולשות שכדאי להכיר:
- סיסמה אחת לכל המנהרות. מי שקיבל אותה פעם יכול להיכנס לכל מנהרה פעילה, גם אחרי שבוע. משתמש נפרד לכל לקוח (שלב 6) מקטין את הבעיה, אבל לא פותר אותה.
- פורט שחוזר לשימוש. קישור ישן ל־
p8042יוביל מחר למה שירוץ אז על 8042.
הפוסט המקורי של ברנה פותר את שתיהן: sshd בוחר פורט פנוי לבד (-R 0:...), ו־nginx בודק עם המודול secure_link חתימה שמשלבת את הפורט, זמן תפוגה וסוד ששמור בשרת. כל קישור עובד רק למנהרה אחת ורק עד מועד מסוים. אם אתם שולחים קישורים ללקוחות, זה שווה את המורכבות הנוספת, וכדאי לקרוא את הפירוט אצלו.
המדריך מבוסס על הרעיון מ־Self-hosted HTTP tunnels with SSH and nginx מאת וינסנט ברנה. ההגדרות, הסקריפט, ההסברים וההגנה בסיסמה הם גרסה פשוטה שכתבתי, והיא שונה מזו שבמקור.