# 外网访问

默认情况下 云瞰 只在局域网可用——出门后手机 App 连不上家里的服务器。本章给出三种把 云瞰 安全地暴露到外网的方式:**组网(推荐)**、反向代理、端口转发,并说明各自要开哪些端口。

> **🛑 先看这一条**
>
> 云瞰 默认监听 `0.0.0.0`、不自带防火墙规则。把端口直接裸暴露到公网 = 全世界的扫描器都能摸到你的摄像头。任何外网方案都必须配合 **HTTPS + 强密码**,而且能不裸暴露公网就不裸暴露。

## 三种方式怎么选

| 方式 | 安全性 | 难度 | 暴露公网 | 适合谁 |
| --- | --- | --- | --- | --- |
| **组网(VPN)** | 最高 | 低 | 否 | 绝大多数家庭用户(**推荐**) |
| 反向代理 + 域名 | 中 | 中 | 是(仅 443) | 想用域名、给不方便装 App 的家人发链接 |
| 端口转发 | 低 | 低 | 是(裸暴露) | 不推荐,仅临时 / 已充分理解风险 |

拿不准就用**组网**:不在路由器开任何端口、不用买域名、不用配证书,安全性最高且几乎零运维。下面三节分别展开。

## 方式一:组网(推荐)

组网工具(Tailscale / WireGuard / ZeroTier 等)在你的手机和家里的服务器之间建一条加密隧道,手机像在家一样用内网地址访问 云瞰——**不需要在路由器上开任何端口,服务器完全不暴露到公网**。

1. **服务器装组网客户端**

   在跑 云瞰 的那台机器(或同 LAN 的软路由)装 Tailscale,然后 `tailscale up` 登录。WireGuard / ZeroTier 同理。

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

2. **手机装同款 App 并登录同一账号**

   手机端装对应的 Tailscale / ZeroTier App,登录与服务器相同的账号,两端就进了同一个虚拟局域网。

3. **App 里填组网地址**

   云瞰 App 的服务器地址填组网分配的 IP——Tailscale 是 `100.x.x.x`,ZeroTier 是 `10.x.x.x`,端口仍然是 `:23406`,例如 `http://100.x.x.x:23406`。

4. **完成**

   出门后手机连 4G/5G 也能访问,体验和在家一样。无需公网 IP、无需域名、无需 HTTPS 证书。

> **💡 为什么首推组网**
>
> 不在路由器开端口 = 对公网的攻击面为零,扫描器根本扫不到你。也不依赖公网 IP——很多宽带是 CGNAT 大内网、压根没有公网 IP,组网照样能穿透。还有一个容易被忽略的好处:**组网下手机视同在局域网,直播默认走 WebRTC 低延迟通道(亚秒级延迟、秒开),而反代 / 端口转发只能用 HLS(延迟 3 秒左右)**——所以组网不光最安全,看实时画面也最跟手。唯一代价是每台要用的设备都装一次组网 App。

## 方式二:反向代理 + 域名

如果你有公网 IP + 一个域名,想用 `https://cam.example.com` 这种地址访问(方便发给不方便装组网 App 的家人),可以在 云瞰 前面架一层 nginx / Caddy 做 HTTPS 终止。云瞰 容器内部已自带一层 nginx,外层反代只需把流量整体转发给 `:23406`。

> **ℹ️ 只需转发一个端口**
>
> 容器把 Web Admin、API、直播流(HLS + WebRTC 信令)全部收敛到 `:23406` 一个端口。**外层反代只转发这一个端口即可**,不要单独暴露 mediamtx 的 `24214` / `24215`。WebRTC 实时视频走的 UDP `:23515` 不经反代,但外网环境下这条 UDP 链路通常建不起来——外网看直播会自动走 HLS,`23515` 不转发也行,原因见下方「方式二」末尾的说明。

### 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*

> **ℹ️ 外网直播走 HLS,不用纠结 WebRTC**
>
> 反代方案下,路由器只需把公网 `443/tcp` 转发到反代所在机器的 `443`。**`23515/udp` 不必转发**:WebRTC 低延迟直播依赖 mediamtx 对外通告「可达的」ICE 候选地址,而 云瞰 默认只通告局域网地址(公网穿透所需的 `webrtcAdditionalHosts`、STUN 均未配置),家庭 NAT 下外网客户端拿不到能用的 WebRTC 通道,`23515` 转了也是白转。外网看直播会自动降级成 HLS(延迟 3 秒左右,功能完全正常);想要 WebRTC 那种秒开低延迟,请用**方式一组网**——组网下手机视同局域网,WebRTC 照常生效。

## 方式三:端口转发(不推荐)

> **🛑 理解风险再用**
>
> 端口转发把服务器端口直接映射到公网,任何人都能访问。直接裸转 `23406` 是**明文 HTTP**——密码、画面全程不加密,而且浏览器和 Android 9+ 会拒绝明文 HTTP 申请摄像头权限。仅建议在完全理解风险、且只是临时使用时考虑;长期使用请改用组网或反向代理。

如果坚持走端口转发,**强烈建议在 云瞰 主机上同时架一层反向代理做 HTTPS**(见方式二),把公网 `443` 转发到反代,而不是把 `23406` 裸转出去。需要转发的端口如下:

| 外网端口 | → 内网 | 协议 | 说明 |
| --- | --- | --- | --- |
| 443 | 反代主机 443 | TCP | 经反代做 HTTPS 后再转 `23406`(推荐做法) |
| 23406 | 云瞰主机 23406 | TCP | 不架反代时的裸转端口,**明文 HTTP,不推荐** |

> **⚠️ 绝不要转发 24214 / 24215**
>
> 直播流已经走 `23406` 上带 live-grant token 的反代。把 mediamtx 的 `24214`(HLS)/ `24215`(WebRTC 信令)裸暴露到公网会绕过 token 鉴权,是安全漏洞。容器的 `mediamtx.yml` 已把这两个端口限制为仅 `127.0.0.1` 可访问。

## 端口速查表

| 端口 | 协议 | 用途 | 外网是否要开 |
| --- | --- | --- | --- |
| 23406 | TCP | Web Admin + API + 直播流 token 反代(唯一 HTTP 入口) | **必须**(反代场景下转发的是 443) |
| 23515 | UDP | WebRTC 实时画面媒体流(局域网 / 组网下生效) | 不用开(外网直播自动走 HLS) |
| 23880 | TCP | mediamtx RTSP,供摄像头 / 外部播放器直连 | 不用开 |
| 24214 | TCP | mediamtx HLS(仅 `127.0.0.1`) | 禁止开 |
| 24215 | TCP | mediamtx WebRTC 信令(仅 `127.0.0.1`) | 禁止开 |

## 配完怎么验证

```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` 没传对。更多排查见 [排错](/docs/troubleshooting)。

---

来源:https://yun-kan.com/ja/docs/remote-access
