อุโมงค์เส้นเดียวคือทางเข้าเดียว และเป็นจุดตายเดียวด้วย
ตอนเน็ตยังดีอยู่ อุโมงค์เส้นเดียวดูสวยมาก ไม่ต้องเปิดพอร์ตที่เราเตอร์ ไม่ต้องขอ IP จริง ไม่ต้องตั้ง VPN ให้ใครเลย แต่วันที่มันหลุด ทุกอย่างดับพร้อมกันหมดในวินาทีเดียว และสิ่งที่ต้องซ้อมไว้ล่วงหน้าคือการ "อ่าน" ให้ออกว่ามันดับเพราะอะไร
พอดีต้องเอาเครื่องทำงานตัวหนึ่งไปวางไว้ที่ออฟฟิศเล็ก ๆ ที่มีแค่เน็ตบ้าน ไม่มี IP จริง เราเตอร์เป็นของ ISP ที่เปิดพอร์ตให้ไม่ได้ และผมก็ไม่อยากเปิดอยู่ดี งานที่ต้องเข้าถึงจากข้างนอกมีสามอย่างพร้อมกัน — เว็บแอปที่ทีมใช้ทุกวัน, SSH ตอนผมต้องเข้าไปแก้อะไร, และรีโมตเดสก์ท็อปตอนต้องขึ้นหน้าจอจริงเพื่อกดโปรแกรมที่คุมด้วยบรรทัดคำสั่งไม่ได้
ผมเลยเลือกวางทั้งหมดไว้บน Cloudflare Tunnel เส้นเดียว แล้วแยกกันด้วย ingress ตามชื่อโฮสต์ บทความนี้คือของที่ผมใช้จริงทั้งชุด รวมถึงบทเรียนวันที่เน็ตออฟฟิศหลุดยาว ๆ แล้วทุกอย่างดับพร้อมกัน
ชื่อโดเมน ชื่ออุโมงค์ และหมายเลขพอร์ตในบทความเป็นค่าตัวอย่างทั้งหมด โครงและคำสั่งเป็นของจริง
เครื่องอยู่หลัง NAT ไม่มีพอร์ตเปิดสักพอร์ต
หลักของ cloudflared คือเครื่องเราเป็นฝ่ายเปิดการเชื่อมต่อออกไปหา edge เอง แล้วค้างไว้ ทราฟฟิกขาเข้าทั้งหมดวิ่งย้อนกลับมาในท่อที่เปิดค้างนั้น แปลว่าจากมุมของเราเตอร์ ไม่มีอะไรวิ่งเข้ามาเลยสักนิดเดียว ไม่ต้อง forward ไม่ต้อง DMZ ไม่ต้องขอ IP จริงจาก ISP
ตรวจว่าเครื่องไม่มีอะไรโผล่ออกเน็ตจริง ๆ ด้วยคำสั่งเดียว
ss -tlnp | awk '{print $1, $4, $6}'
# LISTEN 0.0.0.0:80 apache2
# LISTEN 0.0.0.0:443 apache2
# LISTEN 0.0.0.0:2202 sshd
# LISTEN 127.0.0.1:3306 mariadbd
# LISTEN 127.0.0.1:6379 redis-server
# LISTEN 0.0.0.0:21118 rustdesk
สิ่งที่ฟังอยู่ 0.0.0.0 พวกนี้เข้าถึงได้จาก "ในวง" เท่านั้น เพราะจากนอกวงไม่มีเส้นทางเดินเข้ามาถึงตั้งแต่แรก ตัวที่ทำให้เข้าถึงจากข้างนอกได้คือ ingress ในอุโมงค์เท่านั้น
ingress สี่เส้นในอุโมงค์เดียว
อุโมงค์ตัวเดียวรับได้หลายโฮสต์ แยกกันด้วยกฎ ingress เรียงจากบนลงล่าง เจอตัวแรกที่ตรงแล้วหยุด ของผมตั้งไว้สี่เส้นบวกท้ายสุดหนึ่งอัน
# ingress ของอุโมงค์ (remote-managed แก้ผ่าน dashboard/API ได้ ไม่ต้องแตะเครื่อง)
ingress:
- hostname: hook.example.com # รับ webhook จากภายนอก
service: http://localhost:8787
- hostname: app.example.com # เว็บแอปตัวจริงที่ทีมใช้
service: https://localhost:443
originRequest:
noTLSVerify: true # ปลาย TLS จริงอยู่ที่ Cloudflare ในเครื่องเป็น self-signed
- hostname: ssh.example.com
service: ssh://localhost:2202
- hostname: desk.example.com
service: tcp://localhost:21118
- service: http_status:404 # catch-all ปิดท้าย ห้ามขาด
บรรทัดสุดท้ายที่เป็น http_status:404 ไม่ใช่ของประดับครับ ถ้าไม่มี cloudflared จะไม่ยอมสตาร์ตเลย และถ้าใครเผลอตั้ง catch-all เป็น service จริงแทนที่จะเป็น 404 เท่ากับเปิดทุกโฮสต์ที่ยิงเข้ามาให้ไปถึงบริการนั้นหมด
อุโมงค์แบบ remote-managed เก็บ ingress ไว้ที่ Cloudflare แล้ว cloudflared ดึงลงมาเอง แก้ผ่าน dashboard หรือ API ได้โดยไม่ต้องมี shell บนเครื่อง และไม่ต้อง restart ข้อดีคือวันที่ SSH เข้าไม่ได้แต่เว็บยังอยู่ ผมยังถอดหรือเพิ่ม ingress ได้ ข้อเสียคือไฟล์ในเครื่องไม่ใช่แหล่งความจริงอีกต่อไป — คนที่ไปนั่งแก้ `config.yml` ในเครื่องแล้วงงว่าทำไมไม่มีผล เจอเพราะเรื่องนี้
SSH ผ่าน ProxyCommand ไม่ต้องเปิดพอร์ตที่เราเตอร์
ฝั่ง ingress ตั้ง ssh://localhost:2202 ไว้แล้ว ฝั่งเครื่องที่เราจะต่อออกไปใช้ cloudflared access ssh เป็นตัวคั่นกลาง แปลง stdio ของ ssh ให้วิ่งผ่าน edge
# ต่อครั้งเดียวแบบไม่ตั้งค่าอะไร
ssh -i ~/.ssh/ops_ed25519 \
-o ProxyCommand='cloudflared access ssh --hostname %h' \
[email protected]
ถ้าใช้บ่อยเขียนลง ~/.ssh/config ไปเลย จะได้ไม่ต้องพิมพ์ยาว
Host ssh.example.com
User ops
IdentityFile ~/.ssh/ops_ed25519
ProxyCommand cloudflared access ssh --hostname %h
ServerAliveInterval 30
ServerAliveCountMax 3
ฝั่ง sshd ผมย้ายออกจาก 22 และปิดรหัสผ่านทิ้งทั้งหมด เพราะถึงจะอยู่หลังอุโมงค์ ผมก็ไม่เอาด่านเดียว
# /etc/ssh/sshd_config.d/hardening.conf
Port 2202
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
ส่วนตัวผมยืนยันว่าต้องยิงทดสอบจริงหลังแก้ทุกครั้ง อย่าเชื่อไฟล์ config ลอย ๆ ลองล็อกอินด้วยรหัสผ่านดูว่ามันถูกปฏิเสธจริงไหม และลอง root ดูด้วย ถ้าอันไหนยังผ่านแปลว่ามีไฟล์อื่นทับอยู่
รีโมตเดสก์ท็อปผ่าน access tcp
เส้นที่สนุกที่สุดคือเส้นรีโมตเดสก์ท็อป เพราะมันไม่ใช่ HTTP และไม่ใช่ SSH — เป็น TCP ดิบ ๆ cloudflared access tcp จะเปิดพอร์ตรอไว้ที่เครื่องเรา แล้วส่งทุกอย่างที่เข้าพอร์ตนั้นข้ามไปโผล่ที่ปลายทาง
# รันค้างไว้ที่เครื่องเรา (ฝั่งที่จะรีโมตเข้าไป)
cloudflared access tcp --hostname desk.example.com --url localhost:21118
# แล้วเปิดโปรแกรมรีโมต ใส่ปลายทางเป็น 127.0.0.1:21118
ฝั่งเครื่องปลายทางต้องเปิดโหมดต่อตรงไว้ ไม่งั้นมันจะดั้นด้นไปหา relay สาธารณะของผู้ผลิตแทน ซึ่งเราไม่ได้อยากใช้
# ค่าที่ต้องเปิดฝั่งเครื่องปลายทาง
direct-server = 'Y'
direct-access-port = 21118
ผลที่ได้คือรีโมตเดสก์ท็อปที่ไม่ต้องพึ่งเซิร์ฟเวอร์กลางของผู้ผลิต ไม่ต้องล็อกอินบัญชีนอก และไม่ต้องจำรหัสเครื่องที่เปลี่ยนได้ ผมทำสคริปต์ครอบให้มันวนสตาร์ตใหม่เองเวลาท่อหลุด แล้ววางไว้ให้รันตอนเข้าเครื่อง
#!/bin/bash
# วนเปิดท่อใหม่เองถ้าหลุด — ตั้งให้รันตอนล็อกอินเครื่องฝั่งเรา
while true; do
cloudflared access tcp --hostname desk.example.com --url localhost:21118
echo "[WARN] ท่อหลุด รอ 5 วิแล้วต่อใหม่"
sleep 5
done
สัญญาณชี้ขาด 530 กับ bad handshake มาพร้อมกัน
นี่คือหัวใจของบทความครับ วันที่เน็ตปลายทางหลุด อาการที่เห็นจะดูเหมือนของสามอย่างพังพร้อมกัน ทั้งที่มันคือของชิ้นเดียวพัง คนที่ไม่เคยเจอจะเริ่มไล่ผิดจุดทันที — เปิดเว็บไม่ได้ก็ไปหาว่าเว็บเซิร์ฟเวอร์ล่มไหม SSH เข้าไม่ได้ก็ไปหาว่า sshd ตายไหม เสียเวลาไปเป็นชั่วโมง
เว็บตอบ 530 และ SSH ขึ้น websocket bad handshake
สองอย่างนี้โผล่พร้อมกันในนาทีเดียวกัน ถ้าลองเปิดโฮสต์อื่นในอุโมงค์เดียวกันก็เจออาการเดียวกันหมดทุกเส้น
ไม่มี cloudflared ตัวไหนมารายงานตัวที่ edge
แปลว่าเครื่องหลุดจากเน็ต ไม่ใช่ "เว็บพัง" และไม่ใช่ "SSH พัง" — ไล่ต่อที่ปลายสาย ไม่ใช่ที่ตัวบริการ
คำสั่งเช็กสถานะที่ผมใช้จนติดมือมีบรรทัดเดียว
curl -s -o /dev/null -w '%{http_code}\n' https://app.example.com/
# 302 = ปกติ (เว็บเด้งไปหน้า login)
# 530 = ไม่มี origin มารายงานตัว (Cloudflare error 1033) = เครื่องหลุด
# 502 = อุโมงค์ยังอยู่ แต่บริการปลายทางตาย -> อันนี้ค่อยไปดูที่ตัวบริการ
ความต่างระหว่าง 530 กับ 502 คือสิ่งที่ผมอยากให้จำมากที่สุด — 530 คือ "อุโมงค์ไม่มีใครอยู่" ส่วน 502 คือ "อุโมงค์มีคนอยู่ แต่ของข้างในตาย" สองอันนี้พาไปคนละทางเลย
เวลาเข้าไม่ได้ คนส่วนใหญ่จะกด SSH ซ้ำ ๆ แล้วตีความว่า "SSH พัง" ทั้งที่ SSH เป็นแค่ผู้โดยสารคนหนึ่งในอุโมงค์ ให้ยิง curl ดูโค้ดเว็บก่อนเสมอ เพราะมันบอกสถานะของอุโมงค์ได้ในหนึ่งวินาทีโดยไม่ต้องรอ timeout ถ้าได้ 530 ก็จบ ไม่ต้องไปนั่งงมกับ key หรือ known_hosts ให้เสียเวลา
อ่านเวลาที่หลุดจริงจาก conns_inactive_at
พอรู้ว่าเครื่องหลุด คำถามถัดมาคือหลุดตั้งแต่เมื่อไหร่ อันนี้ถามเครื่องไม่ได้เพราะเข้าไม่ถึง แต่ฝั่ง Cloudflare จำไว้ให้ ในข้อมูลของอุโมงค์มีฟิลด์ conns_inactive_at บอกเวลาที่การเชื่อมต่อเส้นสุดท้ายหายไป
curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT/cfd_tunnel/$CF_TUNNEL" \
| python3 -c 'import sys,json; r=json.load(sys.stdin)["result"]; print(r["name"], r["status"], r["conns_active_at"], r["conns_inactive_at"])'
# office-gateway down 2026-10-09T09:12:44Z 2026-10-09T16:54:03Z
ตัวเลขนี้มีค่ามากตอนต้องตอบทีมว่า "ระบบล่มตั้งแต่กี่โมง" และตอนเทียบกับ log ฝั่งอื่นว่าอะไรเกิดก่อนอะไร ของผมเคยเจอรอบที่หลุดยาวสุดคือ 11 ชั่วโมงครึ่ง สาเหตุคือไดรเวอร์ไวไฟค้าง ไม่ใช่ ISP ล่ม แต่ผลลัพธ์ที่ทีมเห็นคือเว็บดับทั้งวันเหมือนกัน
ลืม enable-linger แล้วอุโมงค์ไม่ขึ้นตอนบูต
กับดักที่ผมอยากเตือนที่สุดอยู่ตรงนี้ ถ้ารัน cloudflared เป็น user service (systemctl --user) มันจะขึ้นก็ต่อเมื่อ user นั้น "มี session อยู่" เท่านั้น แปลว่าตอนเครื่องบูตแล้วไม่มีใครล็อกอิน service จะไม่ขึ้นเลย ผลคือเครื่องบูตเสร็จ เน็ตติด ทุกอย่างพร้อม แต่ไม่มีอุโมงค์ ใครก็เข้าไม่ได้ทั้งนั้น รวมทั้งเราที่จะเข้าไปแก้
loginctl enable-linger ops # <- บรรทัดที่ขาดไม่ได้
loginctl show-user ops | grep Linger
# Linger=yes
systemctl --user enable --now cloudflared-gw
systemctl --user status cloudflared-gw --no-pager
หน้าตา unit ที่ผมใช้ ตั้งให้ปลุกใหม่เสมอเพราะท่อหลุดได้เป็นเรื่องปกติ
# ~/.config/systemd/user/cloudflared-gw.service
[Unit]
Description=Cloudflare tunnel (ทางเข้าเดียวของเครื่องนี้)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
EnvironmentFile=%h/.private/cf-tunnel.env
ExecStart=%h/.local/bin/cloudflared --no-autoupdate tunnel run
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
ในไฟล์ env มีแค่โทเคนของอุโมงค์ อ่านจากไฟล์ที่ตั้ง chmod 600 ไว้ ไม่เขียนลง unit และไม่เขียนลงรีโป
# ~/.private/cf-tunnel.env (chmod 600)
TUNNEL_TOKEN=<TOKEN>
ตอนทำเครื่องสำรองแล้วก๊อปโทเคนไปใช้ต่อ อุโมงค์จะมี connection จากสองเครื่องพร้อมกัน แล้ว edge จะกระจายทราฟฟิกไปทั้งคู่แบบสุ่ม ผลคือบางคำขอไปโผล่เครื่องเก่า บางคำขอไปเครื่องใหม่ อาการที่เห็นคือ "บางทีก็ได้ข้อมูลเก่า บางทีก็ได้ใหม่" ซึ่งไล่ยากมาก ถ้าจะย้ายเครื่อง ให้ปิดของเก่าก่อนเปิดของใหม่เสมอ
bypass ไม่เท่ากับ allow
ตอนตั้ง Cloudflare Access ครอบโฮสต์ ssh กับ desk ผมเคยตั้ง policy เป็น bypass แล้วเลือกประเทศไทยไว้ คิดว่าเท่ากับล็อกให้เหลือแต่คนในประเทศ ซึ่งก็จริง แต่คำว่า bypass แปลว่าข้ามการยืนยันตัวตน ไม่ใช่อนุญาตหลังยืนยันตัวตน ผลคือใครก็ตามที่ออกเน็ตจากในประเทศ เคาะถึง sshd และถึงพอร์ตรีโมตเดสก์ท็อปได้หมด
กรองอย่างเดียว ไม่มีการยืนยันตัวตน
ทุกคนที่เข้าเกณฑ์ผ่านเข้ามาได้โดยไม่ต้องล็อกอินอะไรเลย ด่านจริงที่เหลือคือ key ของ SSH กับรหัสของโปรแกรมรีโมตเท่านั้น
ต้องยืนยันตัวตนก่อนถึงจะเห็นบริการ
คนที่ไม่อยู่ในรายชื่อจะไม่ถึงตัว service เลย ด่านหน้าถูกปิดตั้งแต่ก่อนถึง sshd ปลอดภัยกว่าชัดเจน แลกกับความยุ่งยากตอนต่อของทีม
ส่วนตัวผมยอมรับว่าใช้ bypass ต่ออยู่พักหนึ่งเพราะสะดวก แต่ถ้าใครจะทำตาม ต้องรู้ตัวว่าด่านที่เหลือมีแค่ key กับรหัสเครื่องเท่านั้น ห้ามคิดว่ามีสองชั้นทั้งที่มีชั้นเดียว ใครรับความเสี่ยงนี้ไม่ไหวก็เปลี่ยนเป็น allow ตามสะดวกครับ มันไม่ได้ยากอะไร แค่ทีมต้องล็อกอินเพิ่มอีกที
จุดตายเดียว และสิ่งที่แก้ได้จริง
สรุปสิ่งที่ผมได้จากการวางแบบนี้มาปีกว่า ข้อดีคือชัดเจน — ไม่มีพอร์ตเปิด ไม่ต้องขอ IP จริง ย้ายเครื่องแล้วไม่ต้องแก้ DNS ทีมไม่ต้องลง VPN ข้อเสียก็ชัดเจนไม่แพ้กัน คือทุกอย่างแขวนอยู่บนสายเส้นเดียว เน็ตหลุดเมื่อไหร่ดับหมดทั้งกระดาน ทั้งเว็บ ทั้งช่องที่จะเข้าไปแก้
- ยิง curl ดูโค้ด — ได้ 530 คือเครื่องหลุด ได้ 502 คือบริการข้างในตาย อย่าเดา
- ถ้า 530 ให้ดึง conns_inactive_at มาดูว่าหลุดตั้งแต่เมื่อไหร่ แล้วแจ้งทีมด้วยเวลาจริง
- ติดต่อคนหน้าเครื่องให้ดูไฟเราเตอร์ก่อนอย่างอื่น เพราะจากตรงนี้เราทำอะไรไม่ได้แล้ว
- พอกลับมาให้เช็ก Linger กับ RestartSec ว่าอุโมงค์ขึ้นเองจริงไหม หรือมีคนไปกดให้
- บันทึกช่วงเวลาที่หลุดไว้ทุกครั้ง สะสมไปสามเดือนจะเห็นว่าปัญหาอยู่ที่ไดรเวอร์ ที่ ISP หรือที่ไฟดับ
มีคนแนะนำให้ลง overlay VPN ซ้อนเข้าไปอีกชั้นเป็นทางสำรอง ผมลองคิดตามแล้วไม่ช่วยครับ เพราะมันวิ่งบนอัปลิงก์เส้นเดียวกัน อัปลิงก์ตายก็ตายตามกันอยู่ดี ของที่แก้ปัญหานี้ได้จริงมีทางเดียวคืออัปลิงก์เส้นที่สอง เช่นตัวสำรอง 4G ที่สลับอัตโนมัติ หรือย้ายของที่ทีมต้องใช้ตลอดเวลาไปไว้บนโฮสต์ที่ไม่ได้อยู่หลังเน็ตบ้าน แล้วเหลือไว้บนเครื่องนี้เฉพาะงานที่รอได้ — ซึ่งเป็นเรื่องที่ผมจะเล่าแยกอีกตอน
ใครจะเอาโครงนี้ไปใช้ก็ตามสะดวกครับ แต่ขอให้ตั้งใจรู้ตัวตั้งแต่วันแรกว่ากำลังแลกอะไรกับอะไร ไม่ใช่ไปรู้ตัวเอาวันที่ทีมโทรมาบอกว่าเปิดเว็บไม่ได้ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย