外網存取

預設情況下 雲瞰 只在區域網路可用——出門後手機 App 連不上家裡的伺服器。本章給出三種把 雲瞰 安全地暴露到外網的方式:公網直連(推薦)、組網、反向代理,並說明各自要開哪些連接埠。公網直連只需要一個網域和路由器上的一條規則,憑證在網頁裡自動申請。

先看這一條

把 雲瞰 暴露到外網只有一條底線:必須 HTTPS + 強密碼。雲瞰 的公網直連走的是「一個 TCP 連接埠 + TLS 加密 + 登入驗證」,不是把畫面裸露在公網上。但攝影機自己的 RTSP / ONVIF 連接埠,以及 雲瞰 的內部串流服務連接埠(24214 / 23880)絕對不要轉發到公網——它們沒有登入這一層,轉出去等於把攝影機直接交給全網的掃描器。

三種方式怎麼選

方式需要準備路由器上要做什麼即時畫面延遲適合誰
公網直連一個網域(可用免費子網域),憑證網頁裡自動申請IPv4:轉發 1 條 TCP 規則;IPv6:放行入站即可WebRTC 秒開,低於 0.5 秒寬頻有公網 IP 或 IPv6(推薦)
組網(VPN)每台要用的裝置裝同一款組網 App什麼都不用做WebRTC 秒開電信業者大內網且沒有 IPv6,或不想開任何連接埠
反向代理 + 網域已經在跑 nginx / Caddy / Lucky轉發反向代理用的連接埠預設 HLS 約 2.5 秒(可透傳後走 WebRTC)已有反向代理、想統一用 443 的使用者

三者不衝突,可以同時開。拿不準就按這個順序試:先看寬頻有沒有 IPv6——有的話公網直連連連接埠都不用轉;沒有 IPv6 但有公網 IP,同樣用公網直連;兩樣都沒有(電信業者大內網)就用組網。

方式一:公網直連(推薦)

雲瞰 把網頁、App 介面和即時畫面全部收斂到一個 TCP 連接埠上:同一個連接埠既走 HTTPS,也承載 WebRTC 的即時視訊通道,由伺服器端自動分流。所以路由器上只有一條規則,不需要再為 WebRTC 單獨轉發 UDP 連接埠。

這條路解決了什麼

以前在外面看直播只能用 HLS(延遲 2.5 秒左右):WebRTC 既要額外轉發連接埠,又要伺服器對外通告一個真正可達的位址,家庭寬頻很難同時滿足,交握逾時後就自動降級了。現在一個連接埠全部搞定,外網也能秒開——對「看一眼門口是誰」這類場景,差別是決定性的。

第 1 步:網域與憑證(網頁裡自動申請)

公網直連必須用 HTTPS:手機 App 會拒絕不受信任的自簽憑證,所以需要一個你自己的網域和一張真憑證。在 設定 → 一般 → HTTPS 憑證 裡填網域和 DNS 服務商的 API key,儲存後 雲瞰 會自動向 Let's Encrypt 申請憑證,並在到期前自動續期。

  • 不需要開放 80 連接埠:走的是 DNS-01 驗證(在你的網域下暫時寫一條 TXT 記錄),即使 80 入站被電信業者封鎖也照樣能簽。
  • 支援的 DNS 服務商:阿里雲、騰訊雲 DNSPod、華為雲、Cloudflare、AWS Route 53、Porkbun、deSEC、DuckDNS。
  • 沒有自己的網域也能用:deSEC 和 DuckDNS 免費送子網域,註冊後就能當自己的網域使用。
  • 已經有憑證的話,把 fullchain.pem 和 privkey.pem 放進 data/certs/ 即可,雲瞰 只會用它、不會覆蓋你的檔案。
  • 填完可以先點自我檢查:它會真的寫一條測試記錄再刪掉,確認 API key 和權限沒問題,而且不消耗 Let's Encrypt 的申請次數。

網域要你自己有

雲瞰 不提供代管網域(不自建 DDNS、也不發官方子網域):網域和解析都在你自己名下,我們只提供自動申請憑證的工具,定位與 certbot / acme.sh 一致。沒有網域的話,上面提到的 deSEC / DuckDNS 是免費的。

第 2 步:選一個公網連接埠

家用寬頻建議避開 443

中國大陸的家庭寬頻,80 / 443 入站普遍被電信業者封鎖(為了防止家用寬頻跑未備案的網站)。照搬國外教學用 443,會出現「路由器規則完全正確、外網就是連不上、而且沒有任何錯誤訊息」這種最難查的情況。這類環境一律用高位連接埠,預設的 23443 就很合適;雲端主機或其它地區的寬頻沒有這個限制,可以用 443——好處是存取網址不用帶連接埠號。

在同一個面板裡填公網連接埠,儲存即生效:後端會當場重新設定並確認連接埠真的進入監聽狀態,不需要重啟容器,也不用改 compose 檔案。連接埠如果被別的程式占用、或者和內部連接埠衝突,儲存時就會當場擋下來告訴你,而不是存下去再悄悄失敗。

第 3 步:路由器

  • IPv4(有公網 IP):加一條連接埠轉發,把公網的 23443/tcp 轉到 雲瞰 主機的 23443。只要這一條——不需要轉發 23515 或其它連接埠。
  • IPv6:IPv6 沒有 NAT,不用轉發,只要在路由器的 IPv6 防火牆裡放行到本機 23443/tcp 的入站連線。
  • 網域解析:公網 IPv4 填 A 記錄、IPv6 填 AAAA 記錄。家用寬頻的 IP 會變,設定一個 DDNS 用戶端自動更新即可。

第 4 步:驗證

用 https://<你的網域>:23443/ 打開網頁,登入後進即時監控。畫面應該在 1 秒內出來,並且走的是 WebRTC 通道。

同一個面板裡還有公網可達性自我檢查:它會檢查網域解析到的位址、連接埠通不通、憑證對不對,並直接告訴你卡在哪一步——不用自己抓封包猜。

電信業者沒給公網 IP 怎麼辦

如果自我檢查提示你的公網位址落在電信業者大內網(CGNAT)網段,那麼連接埠轉發無論怎麼設定都不會通,這不是設定問題。兩條出路:用 IPv6(普及率已經很高,而且連連接埠都不用轉發),或者改用下面的組網。也可以打電話給電信業者申請公網 IP,多數地區可以免費開通。

方式二:組網(不開任何連接埠)

組網工具(Tailscale / WireGuard / ZeroTier 等)會在你的手機與家裡的伺服器之間建立一條加密隧道,手機就能像在家一樣用內網位址存取 雲瞰——不需要在路由器上開放任何連接埠,伺服器完全不暴露在公網上。

  1. 1

    伺服器安裝組網用戶端

    在執行 雲瞰 的那台機器(或同一個 LAN 的軟路由)上安裝 Tailscale,然後執行 tailscale up 登入。WireGuard / ZeroTier 做法相同。

    bash
    curl -fsSL https://tailscale.com/install.sh | sh
    sudo tailscale up
  2. 2

    手機安裝同款 App 並登入同一帳號

    手機安裝對應的 Tailscale / ZeroTier App,登入與伺服器相同的帳號,兩端就會進入同一個虛擬區域網路。

  3. 3

    App 填入組網位址

    雲瞰 App 的伺服器位址填組網分配的 IP——Tailscale 是 100.x.x.x,ZeroTier 是 10.x.x.x,連接埠仍然是 :23406,例如 http://100.x.x.x:23406。

  4. 4

    完成

    出門後手機連 4G/5G 也能存取,體驗和在家一樣。不需要公網 IP、不需要網域、不需要 HTTPS 憑證。

組網的取捨

不在路由器開連接埠 = 對公網的攻擊面為零,掃描器根本掃不到你;也不依賴公網 IP,電信業者大內網照樣能穿透。組網下手機視同在區域網路,即時畫面同樣走 WebRTC 低延遲通道。代價是每台要用的裝置都要裝一次組網 App 並登入,臨時分享給家人不如一個網址方便。

方式三:反向代理 + 網域

如果你有公網 IP + 一個網域,想用 https://cam.example.com 這種位址存取(方便發給不方便安裝組網 App 的家人),可以在 雲瞰 前面架一層 nginx / Caddy 做 HTTPS 終止。雲瞰 容器內部已經自帶一層 nginx,外層反向代理只需要把流量整體轉發給 :23406 即可。

反向代理場景下即時畫面走什麼

一般的 HTTP 層反向代理只轉發網頁和介面,即時畫面會走 HLS(延遲 2.5 秒左右),功能完全正常。想要 WebRTC 那種秒開,讓反向代理在 TCP 層(nginx 的 stream 區塊、Caddy 的 layer4 外掛)把某個連接埠整體透傳給 雲瞰 的公網單一連接埠即可——兩者可以共存:網頁走 443 的反向代理,即時畫面走透傳連接埠。

nginx 配置

把以下內容整份存成 /etc/nginx/sites-available/skyview.conf,替換掉 <你的網域> 後 ln -s 到 sites-enabled/ —— 一個檔案就搞定,不用再另外建 snippet。兩個 location / 裡的 proxy_set_header 區塊完全一樣:nginx 的 proxy_set_header 是覆蓋不是繼承,每個 location 都必須各帶一份,照抄即可。

nginx
# WebSocket Upgrade 透傳 —— 必須在 http {} context
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name <你的網域>;
    # certbot --nginx 會自動把此區塊改成 301 跳轉 https
    location / {
        proxy_pass http://127.0.0.1:23406;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  $server_port;
        proxy_set_header X-Forwarded-Proto $scheme;        # ★ 漏了它 HTTPS 部署會登入死循環
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade;
        proxy_buffering       off;
        proxy_connect_timeout 60s;
        proxy_send_timeout    1d;
        proxy_read_timeout    1d;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name <你的網域>;

    ssl_certificate     /etc/letsencrypt/live/<你的網域>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<你的網域>/privkey.pem;

    # 錄影匯出、人臉庫批次匯入可能上百 MB;長 JWT cookie 較大防 414
    client_max_body_size        200m;
    client_header_buffer_size   4k;
    large_client_header_buffers 8 16k;

    location / {
        proxy_pass http://127.0.0.1:23406;
        proxy_http_version 1.1;
        # ↓ 與上面 :80 location 完全相同,逐行照抄(proxy_set_header 不繼承)
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  $server_port;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade;
        proxy_buffering       off;
        proxy_connect_timeout 60s;
        proxy_send_timeout    1d;
        proxy_read_timeout    1d;
    }
}
/etc/nginx/sites-available/skyview.conf

X-Forwarded-Proto / X-Forwarded-Host 必須傳遞 + 每個 location 都要 include

容器內 nginx 只監聽 HTTP,靠 X-Forwarded-Proto 判斷用戶端真實的 scheme、靠 X-Forwarded-Host 知道對外網域。漏了 X-Forwarded-Proto → HTTPS 部署下登入後立刻被踢回登入頁、即時畫面被瀏覽器當成 Mixed Content 攔下;漏了 X-Forwarded-Host → 錄影回放 / 下載 / 匯出的連結會拼成錯誤位址,用戶端打不開。上面的 nginx 範本和 Caddy(預設就會傳這兩個標頭)都已經涵蓋,照抄即可——連接埠不需要手動設定 X-Forwarded-Port,容器內 nginx 會自動依「外層反向代理 / 裸 IP 直連」兩種情境處理好。另外 nginx 的 proxy_set_header 是覆蓋而非疊加:某個 location 只要寫了任何一條,上層的 set_header 就會全部失效——所以每個 location 都要 include 完整的那份 snippet。

申請憑證(機器需要能從公網存取 :80):

bash
certbot --nginx -d <你的網域> -m <你的電子郵件> --agree-tos --no-eff-email --redirect

Caddy 配置(更簡單)

Caddy 會自動簽發 / 續期 Let's Encrypt 憑證,不需要 certbot,配置也精簡許多。/etc/caddy/Caddyfile:

caddyfile
<你的網域> {
    reverse_proxy 127.0.0.1:23406 {
        # Caddy 預設就會傳 X-Forwarded-{For,Proto,Host}
        # 長連線(對講 WS / 事件 SSE / 直播串流)調大 flush + 逾時
        flush_interval -1
        transport http {
            read_timeout  24h
            write_timeout 24h
        }
    }
    request_body {
        max_size 200MB
    }
}
/etc/caddy/Caddyfile

連接埠速查表

連接埠協定用途外網是否要開
23443TCP公網直連:網頁 + 介面 + 即時畫面(HTTPS,連接埠可改)要開(公網直連方案下唯一要開的連接埠)
23406TCP區域網路存取入口(明文 HTTP)不要開(要外網存取請用上面的 23443)
23515UDP + TCP即時畫面媒體串流(區域網路 / 組網 / IPv6 直連)不用開
23880TCPRTSP,供外部播放器在區域網路直連不用開
24214TCP內部串流服務禁止開

別把內部連接埠轉出去

24214(內部串流服務)和 23880(RTSP)沒有登入這一層——直播串流的驗證做在 23406 / 23443 的入口上。把它們裸轉到公網會直接繞過驗證,等於公開你的攝影機畫面。同理也不要把 23406 裸轉出去:那是明文 HTTP,密碼在公網上是明文傳輸的,瀏覽器和 Android 9+ 還會拒絕在明文頁面裡申請攝影機權限。要外網存取,用上面的公網直連(23443,HTTPS)。

設定完成後怎麼驗證

bash
# 1. HTTP→HTTPS 跳轉(反向代理情境)
curl -sSI http://<你的網域>/healthz | head -1      # 預期 301

# 2. 健康檢查
curl -sS https://<你的網域>/healthz                 # 預期 {"code":0,...}

# 3. 未登入時存取受保護的介面
curl -sS -o /dev/null -w '%{http_code}\n' \
     https://<你的網域>/api/cameras                 # 預期 401

最後用瀏覽器走一遍完整流程:登入 → 即時監控看到畫面 → 瀏覽器主控台沒有 Mixed Content 錯誤。若登入後立刻被踢回登入頁、或即時畫面報 Mixed Content,99% 是 X-Forwarded-Proto 沒傳對。更多疑難排解見 疑難排解。

這篇文件有幫助嗎?