外网访问

預設情況下 雲瞰 只在區域網路可用——出門後手機 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.pemprivkey.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 -ssites-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 没传对。更多排查见 排错