外网访问

默认情况下 云瞰 只在局域网可用——出门后手机 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 没传对。更多排查见 排错