要讓外面連到你的服務,先問誰要用,再決定用 Cloudflare Tunnel 還是 SSH 通道

開發網站時常常遇到同一個問題:服務跑在自己的電腦或內網機器上,要怎麼讓外面的人或服務連得到?

可能是要給客戶看開發中的畫面、讓 Stripe 的 webhook 打到本機、用手機測試 PWA,也可能是把家裡 NAS 上的小工具公開出去,或者反過來,從自己的電腦連到正式環境的資料庫。

能解決這些問題的工具不少:Cloudflare Tunnel、SSH 通道、Tailscale、ngrok、frp……它們功能有重疊,但擅長的情境不同。這篇先說明最常用的兩種(Cloudflare Tunnel 與 SSH 通道)的原理,再從使用情境出發,一個一個說明該選哪一種、怎麼設定。

文中的網域一律用 example.com,請換成自己託管在 Cloudflare 上的網域。


一、Cloudflare Tunnel 是什麼

Cloudflare Tunnel 是 Cloudflare 提供的反向通道服務。在自己的機器上執行一個叫 cloudflared 的小程式,它會主動往外連到 Cloudflare 的邊緣節點,建立幾條常駐的連線。之後有人連到你的網域,請求先到 Cloudflare,再沿著這幾條連線送回你的機器。

flowchart LR
    U["使用者瀏覽器"] -- "HTTPS<br>app.example.com" --> E["Cloudflare 邊緣節點<br>(CDN、WAF、DDoS 防護)"]
    C["cloudflared<br>(你的機器)"] == "主動連出的通道" ==> E
    E -. "請求沿通道送回" .-> C
    C --> S["本機服務<br>localhost:8080"]

連線方向是從內部往外,所以防火牆不用開任何 port。

這個設計帶來幾個好處:

  • 不需要公開 IP,也不用設定 port forwarding。在 NAT、公司內網、電信業者共用 IP(CGNAT)後面都能用。
  • 真實 IP 不會曝光,外部看到的只有 Cloudflare 的 IP。
  • HTTPS 憑證由 Cloudflare 處理,不用自己跑 Let’s Encrypt。
  • 流量會經過 Cloudflare 的 CDN、WAF 和 DDoS 防護,也可以加上 Cloudflare Access 做登入驗證。
  • 免費。

代價是:網域的 DNS 要託管在 Cloudflare,而且 TLS 在 Cloudflare 端解密,Cloudflare 看得到明文流量。

二、SSH 通道的三種模式

SSH 通道是 OpenSSH 內建的連接埠轉發功能,任何有 SSH 的機器都能用,不依賴任何服務商。有三種模式:

模式 指令 用途
本地轉發 -L ssh -L 5432:localhost:5432 user@server 把遠端的服務(例如資料庫)拉到本機使用
遠端轉發 -R ssh -R 8080:localhost:3000 user@vps 把本機的服務送到遠端主機上公開
動態轉發 -D ssh -D 1080 user@server 建立 SOCKS proxy,流量經由遠端主機出去
flowchart LR
    subgraph L["-L 本地轉發:把遠端拉進來"]
        direction LR
        A1["本機程式<br>連 localhost:5432"] --> A2["ssh 用戶端"] == "SSH 加密" ==> A3["遠端 sshd"] --> A4["遠端資料庫<br>:5432"]
    end
    subgraph R["-R 遠端轉發:把本機推出去"]
        direction LR
        B1["外部使用者<br>連 vps:8080"] --> B2["VPS 上的 sshd"] == "SSH 加密" ==> B3["本機 ssh 用戶端"] --> B4["本機服務<br>:3000"]
    end

-L 和 -R 的差別在於「誰在聽」:-L 由本機監聽,-R 由遠端監聽。

和 Cloudflare Tunnel 功能重疊的是 -R:兩者都是從內部連出去,把本機服務公開出去。但 -R 需要一台有公開 IP 的跳板機或 VPS,sshd 要開 GatewayPorts yes,要網域和 HTTPS 還得在 VPS 上自己架 nginx 和 certbot,斷線要靠 autossh 重連。

三、兩者比較

項目 SSH 通道 Cloudflare Tunnel
需要的基礎設施 一台有公開 IP 的 VPS 或跳板機 不用,只要網域在 Cloudflare
支援的協定 任何 TCP 以 HTTP/HTTPS 為主;TCP 需要用戶端也裝 cloudflared
方向 雙向(-L 拉進來、-R 推出去) 主要是推出去
加密與隱私 點對點加密,不經過第三方 在 Cloudflare 解密
HTTPS 憑證 自己架 nginx 和 certbot 自動處理
對外 IP VPS 的 IP 會曝光 只看得到 Cloudflare 的 IP
DDoS/WAF 沒有 內建
存取控制 SSH 金鑰(只管誰能建通道) Cloudflare Access(SSO、email 驗證)
穩定性 靠 autossh 重連,VPS 是單點故障 自動重連,同時連到多個節點
上傳限制 只受 VPS 頻寬限制 免費方案單一請求上限 100MB
費用 VPS 月費 免費
廠商綁定 沒有 綁定 Cloudflare

簡單來說,SSH 通道比較底層、通用,但機器、憑證、防護都要自己準備;Cloudflare Tunnel 專門處理「把 HTTP 服務公開到網路上」這件事,附帶 HTTPS、防護和存取控制,代價是流量要經過 Cloudflare。


四、依使用情境選擇

以下每個情境依序說明:要解決什麼問題、建議的方案、為什麼選它,以及關鍵的設定。

情境 1:臨時給別人看開發中的畫面

本機跑著 localhost:3000,想讓客戶或同事看一下,看完就關掉。

建議方案:Cloudflare Quick Tunnel

不用 Cloudflare 帳號,也不用網域,只要一行指令:

# 安裝(Debian/Ubuntu)
curl -L -o cloudflared.deb \
  https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb

# 分享本機的 3000 port
cloudflared tunnel --url http://localhost:3000

終端機會印出一個像 https://xxx-yyy-zzz.trycloudflare.com 的網址,把它傳給對方就行了。按 Ctrl+C 結束後網址就失效。

限制:每次啟動網址都不一樣、有併發請求的上限、不支援 Server-Sent Events。只適合臨時展示,不要用在正式環境。

如果不想安裝任何東西,也可以用 SSH 型的服務,概念一樣:

ssh -R 80:localhost:3000 localhost.run

情境 2:Webhook 開發、手機實機測試

開發 LINE Bot、Stripe 付款、GitHub App,對方要一個固定的 HTTPS 網址回呼;或是要在手機上測試相機、定位、PWA 這些需要 HTTPS(secure context)的功能。

建議方案:Cloudflare Named Tunnel,給一個固定的子網域,例如 dev.example.com

Quick Tunnel 的網址每次都會變,每次重開都要去 webhook 設定頁改網址,很麻煩。Named Tunnel 的網址是固定的,設定一次之後,每天開發時只要啟動 cloudflared 就能接收 webhook。設定方式見下一個情境。

如果需要檢視每個 request 的內容、重送 request 來除錯,ngrok 內建的 inspector 介面比較方便,可以考慮搭配使用。

情境 3:把 listen 某個 port 的程式部署到子網域

機器上有個程式聽 8080 port,想讓它變成 https://app.example.com,而且要長期穩定運作。

建議方案:Cloudflare Named Tunnel + systemd 常駐

這是 Cloudflare Tunnel 最典型的用法。網域已經託管在 Cloudflare 的話,第一次設定大約 5 分鐘:

# 1. 登入授權(會開瀏覽器,選擇 example.com)
cloudflared tunnel login

# 2. 建立一條 tunnel(一台機器建一條就夠,可以掛很多服務)
cloudflared tunnel create my-server

# 3. 建立 DNS:app.example.com → 這條 tunnel(自動建立 CNAME)
cloudflared tunnel route dns my-server app.example.com

# 4. 啟動
cloudflared tunnel run --url http://localhost:8080 my-server

打開 https://app.example.com 就能看到服務,HTTPS 自動生效。

一台機器掛多個服務

一條 tunnel 可以依 hostname 把請求分到不同的 port:

flowchart LR
    U1["app.example.com"] --> E["Cloudflare"]
    U2["api.example.com"] --> E
    U3["admin.example.com"] --> E
    E == "同一條 tunnel" ==> C["cloudflared<br>ingress 規則"]
    C --> S1["localhost:8080<br>前端"]
    C --> S2["localhost:3000<br>API"]
    C --> S3["localhost:9000<br>管理後台"]

一條 tunnel、一個 cloudflared 程序,依 hostname 分流到三個服務。

把規則寫在 ~/.cloudflared/config.yml:

tunnel: my-server
credentials-file: /home/me/.cloudflared/<TUNNEL-ID>.json   # 第 2 步產生的檔案

ingress:
  - hostname: app.example.com
    service: http://localhost:8080
  - hostname: api.example.com
    service: http://localhost:3000
  - hostname: admin.example.com
    service: http://localhost:9000
  - service: http_status:404          # 最後一條必須是 catch-all
# 每個新的子網域都要建一次 DNS
cloudflared tunnel route dns my-server api.example.com
cloudflared tunnel route dns my-server admin.example.com

# 檢查設定
cloudflared tunnel ingress validate

# 註冊成系統服務,開機自動啟動
sudo cloudflared --config ~/.cloudflared/config.yml service install
sudo systemctl restart cloudflared

之後要新增服務只要三步:在 config.yml 加一條規則、執行一次 route dns、重新啟動 cloudflared。

也可以不用 CLI,改到 Cloudflare Dashboard 的 Zero Trust → Networks → Tunnels 建立 tunnel,在伺服器上執行 sudo cloudflared service install <TOKEN>,之後子網域的增減都在網頁上操作。用 Docker 的話,cloudflare/cloudflared 映像檔可以直接放進 docker-compose 當作一個服務。

常見問題

  1. 子網域只能一層。免費的 Universal SSL 憑證涵蓋 example.com 和 *.example.com,所以 app.example.com 沒問題,dev.app.example.com 會出現憑證錯誤,要付費的 Advanced Certificate 才能用。
  2. DNS 記錄衝突。子網域如果已經有 A 或 CNAME 記錄,route dns 會失敗,要先刪掉舊記錄,或加 -f 強制覆蓋。
  3. 框架擋掉 Host header。很多開發伺服器預設只接受 localhost,要把網域加進白名單:
    • Vite:server.allowedHosts: ['app.example.com']
    • Django:ALLOWED_HOSTS = ['app.example.com']
    • Rails:config.hosts << "app.example.com"
    • webpack dev server:allowedHosts: 'all'
  4. 程式本身是 HTTPS(自簽憑證):service 改成 https://localhost:8443,並加上
    originRequest:
      noTLSVerify: true
  5. 出現 502 Bad Gateway。通常是本機服務沒有啟動或 port 寫錯。先在機器上執行 curl http://localhost:8080 確認服務有回應。
  6. 程式讀到的使用者 IP 都是 Cloudflare 的。真實 IP 在 CF-Connecting-IP header 裡。

情境 4:內部工具、staging 環境只給特定的人看

Grafana、Jenkins、管理後台要給同事用;staging 環境要給客戶驗收,但不能讓所有人都看得到。

建議方案:Cloudflare Tunnel + Cloudflare Access

照情境 3 把服務接上子網域後,到 Zero Trust → Access → Applications 為這個子網域建立一個應用程式,設定允許的對象,例如 @example.com 結尾的信箱、指定的幾個 email,或是 Google、GitHub 帳號登入。

sequenceDiagram
    participant U as 使用者
    participant A as Cloudflare Access
    participant T as Tunnel
    participant S as 內部工具
    U->>A: 開啟 grafana.example.com
    A-->>U: 尚未登入,導向登入頁
    U->>A: 用 email 驗證碼或 Google 登入
    A->>A: 檢查是否在允許名單內
    A-->>U: 發給 session cookie
    U->>A: 帶著 cookie 再次請求
    A->>T: 驗證通過,轉送請求
    T->>S: localhost:3000
    S-->>U: 回應頁面

驗證發生在 Cloudflare,沒通過的請求根本不會進到你的機器。

好處是程式完全不用修改,也不用架 VPN 或發 SSH 金鑰給不熟技術的人。免費方案可以給 50 個使用者用。

如果是團隊成員之間要長期互連多台內網機器,而且不只用到 HTTP(例如 SSH、資料庫、檔案分享),Tailscale 這類 mesh VPN 會更直接:每台機器裝上 Tailscale 後,彼此就像在同一個區網裡。

情境 5:從本機連到遠端的資料庫

正式環境的 PostgreSQL 只聽 127.0.0.1,想用本機的 GUI 工具連進去查資料。

建議方案:SSH -L 本地轉發

這是 SSH 通道的主場。Cloudflare Tunnel 擅長「推出去」,而這個情境是「拉進來」:

ssh -N -L 15432:localhost:5432 user@db-server

然後本機的 GUI 工具連 localhost:15432 就可以了。-N 表示不開 shell,只做轉發。

常用的話可以寫進 ~/.ssh/config:

Host prod-db
    HostName db-server.example.com
    User deploy
    LocalForward 15432 localhost:5432

之後只要執行 ssh -N prod-db。

同類的用法還有:臨時偵錯時順手加上 -L 看遠端某個只聽 localhost 的管理介面;或用 ssh -D 1080 建立 SOCKS proxy,讓瀏覽器經由遠端主機上網。

情境 6:伺服器不想開 22 port,但還是要能 SSH 進去

想把伺服器的防火牆全部關掉,連 SSH 也不對外開放。

建議方案:讓 SSH 走 Cloudflare Tunnel,再用 Access 控管

在 tunnel 的 ingress 加一條把 SSH 送進來的規則:

ingress:
  - hostname: ssh.example.com
    service: ssh://localhost:22
  # ...其他規則
  - service: http_status:404

用戶端也要安裝 cloudflared,在 ~/.ssh/config 加上:

Host ssh.example.com
    ProxyCommand cloudflared access ssh --hostname %h

之後 ssh [email protected] 就會經由 Cloudflare 連進去。再為 ssh.example.com 設定 Access,連線前會先開瀏覽器做 SSO 驗證。伺服器不用對外開任何 port,而且可以從 Access 集中管理誰能登入。

這也說明兩種工具不是二選一:外層用 Cloudflare 保護入口,進去之後照樣用 SSH -L 連資料庫,兩者可以疊在一起用。

情境 7:Cloudflare Tunnel 不適合的情況

有些需求不適合讓流量經過 Cloudflare:

需求 建議方案 原因
流量不能讓第三方解密(金融、醫療、合約限制) SSH 通道,或自架 frp、WireGuard 點對點加密,中間沒有人看得到
大檔案上傳(超過 100MB)、影音串流 SSH -R + VPS,或直接架在 VPS 上 避開大小上限和 Cloudflare 服務條款的限制
非 HTTP 協定要給一般使用者連(遊戲伺服器、MQTT、自訂 TCP) 自架 frp,或 VPS 直接開 port Cloudflare Tunnel 的 TCP 需要用戶端安裝 cloudflared
對延遲極度敏感 VPS 直接架設,或 Tailscale 直連 少繞一段中繼
不想被任何服務商綁定 SSH 通道、frp 標準開源工具,到哪裡都能用

五、總整理

依情境選工具

分類 情境 建議方案
公開 Web 服務 臨時給別人看 Cloudflare Quick Tunnel
Webhook 開發、手機實機測試 Cloudflare Named Tunnel(固定子網域)
小型網站、side project 正式上線 Named Tunnel + systemd
家用伺服器、NAS、沒有固定 IP Cloudflare Tunnel
一台機器掛多個服務 Tunnel + config.yml ingress
限定特定的人 內部工具、管理後台 Tunnel + Access
staging 給客戶驗收 Tunnel + Access(指定 email)
團隊多台機器長期互連 Tailscale
工程師存取 連遠端資料庫、Redis SSH -L
臨時偵錯 SSH -L/-R
SOCKS proxy SSH -D
伺服器不開 22 port Tunnel + Access 保護 SSH
特殊需求 不能讓第三方解密 SSH 通道、frp、WireGuard
大檔案、影音串流 VPS 直接架設
非 HTTP 協定要公開 frp、VPS 開 port
要檢視、重送 request ngrok

決策流程

flowchart TD
    Q1{"要給「瀏覽器使用者」<br>存取嗎?"}
    Q1 -- 是 --> Q2{"能接受流量<br>經過 Cloudflare 嗎?"}
    Q1 -- "否,給工程師或程式" --> Q5{"要做什麼?"}
    Q2 -- 否 --> X1["SSH -R + VPS<br>或自架 frp"]
    Q2 -- 是 --> Q3{"只是臨時用?"}
    Q3 -- 是 --> T1["Quick Tunnel"]
    Q3 -- "否,要長期或固定網址" --> T2["Named Tunnel<br>app.example.com"]
    T2 --> Q4{"要限制誰能看?"}
    Q4 -- 是 --> T3["再加上 Cloudflare Access"]
    Q4 -- 否 --> T4["直接公開"]
    Q5 -- "把遠端服務拉到本機" --> S1["SSH -L"]
    Q5 -- "多台機器長期互連" --> S2["Tailscale"]
    Q5 -- "非 HTTP 協定要公開" --> S3["frp 或 VPS 開 port"]

先問「誰要用」,再問「能不能經過 Cloudflare」,大多數情況兩三個問題就能決定。

結語

如果網域已經託管在 Cloudflare,日常的 Web 開發和小型部署,大部分情境用 Cloudflare Named Tunnel 就能解決:一條 tunnel 掛多個子網域,需要時加上 Access 控管存取,伺服器一個 port 都不用開。剩下「拉進來」的需求,例如連遠端資料庫,交給 SSH -L。

只有在流量不能經過第三方、要傳大檔案,或要公開非 HTTP 協定時,才需要動用 VPS、frp 或 Tailscale。判斷的原則很簡單:給「人」用瀏覽器存取的服務,選 Cloudflare Tunnel;給工程師或程式存取任意協定,或流量不能經過第三方的,選 SSH 通道或自架方案。