กฎ ingress ของ Cloudflare Tunnel คืออะไร และวิธีเขียน path ให้จับเฉพาะ URL ที่ตั้งใจ

Cloudflare Tunnel ใช้กฎ ingress บอกว่า request ของ hostname และ path ไหนส่งต่อไปที่ service ใดบน server กฎเทียบจากบนลงล่าง ใช้ข้อแรกที่ตรง และ path เป็น regex ที่หาเจอตรงไหนของ URL ก็นับว่าตรง ถ้าไม่ใส่ ^ กฎหนึ่งอาจไปจับ URL ที่ไม่ได้ตั้งใจ

เผยแพร่
เวลาอ่าน
4 นาที
แท็ก
#Laravel#Linux
ในโพสต์นี้ 5 หัวข้อ
  1. กฎ ingress บอกว่า request ไหนส่งไป service ไหน
  2. กฎเทียบจากบนลงล่าง และต้องมีกฎรับทุกอย่างปิดท้าย
  3. path เป็น regex ที่ไม่ยึดต้น path
  4. ตรวจกฎด้วย cloudflared ก่อนใช้จริง
  5. กรณีที่เจอได้จริงกับแอป Laravel

ปกติเว็บที่อยู่หลัง Cloudflare ต้องมี IP ของ server ที่เปิด port 80 และ 443 ให้ Cloudflare ต่อเข้ามา Cloudflare Tunnel กลับทิศ ให้โปรแกรม cloudflared บน server เปิด connection ออกไปหา Cloudflare เอง request ของผู้ใช้วิ่งกลับมาตาม connection นั้น server จึงไม่ต้องเปิด port ขาเข้าเลย เหมาะกับเครื่องที่ไม่มี IP สาธารณะ อยู่หลัง NAT หรืออยากปิด port ทั้งหมดไว้

หลักคิดคล้าย SSH tunnel ที่ส่ง traffic ผ่าน connection ที่เปิดไว้แล้ว ต่างกันที่ปลายทางเป็น Cloudflare และรับ request จากทั้งอินเทอร์เน็ต ส่วนที่ต้องเข้าใจให้ดีคือกฎที่บอกว่า request ไหนไปที่ไหน

กฎ ingress บอกว่า request ไหนส่งไป service ไหน

tunnel ที่ตั้งด้วยไฟล์ config มีกฎอยู่ในรายการ ingress แต่ละข้อมี service ปลายทาง กับเงื่อนไข hostname และ path ที่จะใส่หรือไม่ใส่ก็ได้

tunnel: <tunnel-id>
credentials-file: /etc/cloudflared/<tunnel-id>.json

ingress:
  - hostname: app.example.com
    path: ^/app/
    service: http://127.0.0.1:8080
  - hostname: app.example.com
    service: http://127.0.0.1:80
  - hostname: "*.example.com"
    service: http://127.0.0.1:80
  - service: http_status:404
  • hostname ชื่อโดเมนที่ request ขอมา ใส่ * ได้เฉพาะหน้าสุด อย่าง *.example.com ใส่กลางชื่อแบบ test.*.example.com ไม่ได้
  • path regex ที่เทียบกับ path ของ URL ไม่ใส่ก็คือรับทุก path
  • service ปลายทาง ส่วนใหญ่เป็น http://127.0.0.1:<port> ของ nginx หรือแอปบนเครื่อง และยังรองรับ SSH, RDP, TCP, Unix socket หรือคำตอบสำเร็จรูปอย่าง http_status:404

ถ้าสร้าง tunnel จาก dashboard ของ Cloudflare กฎชุดเดียวกันจะอยู่ในหน้าตั้งค่าของ tunnel แทนไฟล์ แต่ช่อง hostname, path และ service จับคู่แบบเดียวกัน

กฎเทียบจากบนลงล่าง และต้องมีกฎรับทุกอย่างปิดท้าย

cloudflared ไล่เทียบ request กับกฎทีละข้อจากบนลงล่าง แล้วใช้ข้อแรกที่ตรง ข้อที่อยู่ล่างกว่าจะไม่ได้ดูเลย ลำดับจึงสำคัญ กฎที่เจาะจงกว่าต้องอยู่บน กฎกว้าง ๆ อยู่ล่าง ในตัวอย่างข้างบน ถ้าสลับให้ hostname: app.example.com ที่ไม่มี path ขึ้นก่อน request ของ /app/... ก็จะไปที่ port 80 หมด กฎของ port 8080 ไม่มีวันได้ใช้

เอกสารของ Cloudflare กำหนดว่าไฟล์ที่มีกฎ ingress ต้องปิดท้ายด้วยกฎรับทุกอย่าง (catch-all) ที่มีแค่ service อย่าง http_status:404 request ที่ไม่ตรงกฎไหนเลยจะได้คำตอบที่เราเลือก แทนที่จะหลุดไปที่ใดที่หนึ่งโดยไม่ตั้งใจ

path เป็น regex ที่ไม่ยึดต้น path

นี่คือจุดที่พลาดง่ายที่สุด เอกสารบอกว่า path เป็น regex ตามไวยากรณ์ของ Go และในโค้ดของ cloudflared เทียบด้วย regexp.MatchString ซึ่งหาเจอตรงไหนของ path ก็นับว่าตรง ไม่ได้บังคับให้ตรงตั้งแต่ตัวแรก

กฎ path /app/key123 /build/assets/app-3f9a.js /apple
/app.* ตรง ตรง ตรง
/app/ ตรง ไม่ตรง ไม่ตรง (แต่ /docs/app/x ตรง)
^/app/ ตรง ไม่ตรง ไม่ตรง

ถ้าคุ้นกับ nginx จะยิ่งพลาดง่าย เพราะ location /app { ... } ของ nginx เทียบต้น path อยู่แล้ว ส่วน path: /app.* ของ cloudflared เทียบทั้งเส้น จึงใส่ ^ นำหน้าเสมอเมื่อหมายถึงต้น path และใส่ $ ท้ายเมื่อต้องการให้ตรงทั้งเส้น จุด . ใน regex คือตัวอักษรอะไรก็ได้ ถ้าหมายถึงจุดจริง ๆ อย่างนามสกุลไฟล์ให้เขียนเป็น \. เหมือนตัวอย่างในเอกสาร \.(jpg|png|css|js)$

ตรวจกฎด้วย cloudflared ก่อนใช้จริง

อย่าตรวจกฎด้วยตา cloudflared มีคำสั่งให้สองตัว

cloudflared tunnel ingress validate
cloudflared tunnel ingress rule https://app.example.com/build/assets/app-3f9a.js

ตัวแรกเช็กว่าไฟล์ถูกรูปแบบ มีกฎปิดท้ายครบ ตัวที่สองบอกว่า URL ที่ให้ไปตรงกับกฎข้อไหน ลองกับ URL ที่ตั้งใจให้ตรง และ URL ที่ไม่ควรตรงด้วย โดยเฉพาะไฟล์ static ที่ชื่อบังเอิญมีคำเดียวกับ path ในกฎ

แก้ config แล้วต้องให้ cloudflared โหลดใหม่ ซึ่งจะตัด connection ที่ค้างอยู่อย่าง WebSocket เอกสารแนะนำให้เปิด cloudflared ตัวใหม่ด้วย config ใหม่ขึ้นมาก่อน รอจนพร้อม แล้วค่อยปิดตัวเดิม ผู้ใช้จะได้ไม่สะดุด

กรณีที่เจอได้จริงกับแอป Laravel

WebSocket ของ Reverb ต้องมีกฎของตัวเอง Laravel Reverb รับ WebSocket ที่ path /app/{key} บน port 8080 ส่วนหน้าเว็บอยู่ที่ nginx port 80 จึงต้องมีกฎแยก path นี้ไว้บนสุดของ hostname นั้น แอปหนึ่งของเราเคยตั้งไว้เป็น path: /app.* พอ deploy แล้วแอป React ขาวทั้งหน้า เพราะ Vite ตั้งชื่อไฟล์ JavaScript หลักตามชื่อ entry app.jsx เป็น /build/assets/app-<hash>.js ซึ่งมี /app อยู่ข้างใน กฎจึงส่งไฟล์นี้ไปหา Reverb ที่ตอบ 404 แล้ว Cloudflare ก็ cache 404 นั้นไว้ แก้เป็น ^/app/ แล้ว purge cache ของไฟล์นั้นจึงหาย แก้ config อย่างเดียวไม่พอ เพราะ 404 ยังค้างใน cache ของ Cloudflare ถ้าใช้ Laravel อยู่แล้ว laravel-cloudflare มีคำสั่ง purge ให้

แอปต้องรู้ว่าอยู่หลัง proxy request ที่มาทาง tunnel ถึง nginx จาก 127.0.0.1 เสมอ Laravel จะเห็น IP ของผู้ใช้เป็น localhost และอาจสร้างลิงก์เป็น http ถ้าไม่ได้ตั้งให้เชื่อ header ของ Cloudflare วิธีตั้งให้ถูกโดยไม่เชื่อทุก IP อยู่ในโพสต์ trustProxies หลัง Cloudflare

หลายเว็บบน server เดียว ใส่ hostname ของแต่ละเว็บเป็นกฎแยก ส่งไป nginx port เดียวกันได้ เพราะ nginx เลือก vhost จาก header Host ที่ cloudflared ส่งต่อมาให้ หรือใช้ *.example.com รับทุก subdomain แล้วให้ nginx แยกเอง

กฎ ingress มีแค่สามช่อง แต่ลำดับของกฎกับ regex ที่ไม่ยึดต้น path ทำให้ผลต่างจากที่เราคิดได้มาก ถ้าเขียน ^ นำหน้าทุก path และตรวจด้วย ingress rule ทุกครั้งที่แก้ ก็กันเรื่องส่วนใหญ่ได้ครับ

อ่านต่อ

ล่าสุด

ดูทั้งหมด →