feat(stage): nginx 加內網 IP 直連 server block(demo 用)

agent 本機 demo 需直連內網 192.168.0.130:9527 建 tunnel(繞過公網邊界
proxy — 那台剝 WS upgrade header、碰不到)。但直連 IP 時 Host=192.168.0.130
不命中 stage-9527 白名單被 444 擋。

加一個 server_name 192.168.0.130 的 server block(複製 stage-9527 block、
XFP 改 http),8 個 location 全對齊含 /tunnel/connect WS upgrade。現有兩
block(default 白名單 + stage-9527)一字未動。

安全:公網經 proxy 進來 Host 固定 stage-9527、偽造 Host 打不進;IP block
只內網可達;default 444 白名單未削弱。⚠️ demo 專用、正式上線前應移除或
加 allow/deny(註解已標)。

搭配 .env.stage VISIONA_RELAY_PUBLIC_URL 改內網 ws(部署時設、缺一不可)。
Reviewer 通過 0C/0M/0Mi。stage 純 demo 無正式使用者。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-07-01 20:59:13 +08:00
parent ab50691f8a
commit 58313169c2

View File

@ -346,3 +346,175 @@ server {
proxy_cache off; proxy_cache off;
} }
} }
# ============================================================
# 內網 IP 直連 server blockdemo 用)
# ------------------------------------------------------------
# 目的:讓 visionA Agent / 內網瀏覽器以 http://192.168.0.130:9527
# 直連 container繞過公網邊界 proxy那層剝 WS upgrade header、
# 碰不到 /tunnel/connect
#
# 為什麼安全(見 spec §4 完整評估):
# - 公網邊界 proxy 進來的 Host 一律被改寫成 stage-9527.innovedus.com
# → 永遠走上面那個 block外部偽造 Host:192.168.0.130 也打不進這條。
# - 這條只有「能直接連到 130:9527 的內網來源」可命中。
# - default_server 仍是 server_name _ 那條,本 block 不加 default_server。
#
# DRY 取捨:務實複製 stage-9527 block 的 location。WS upgrade map
# $connection_upgrade 在 http context 定義(檔頭 38-41 行),本 block 直接繼承。
#
# X-Forwarded-Proto實際是 http 進來,但 backend 不讀 XFP已 code 確認),
# 設 http 或 https 都不影響。這裡設 http 以符合實際(避免 backend 未來
# 萬一改讀 XFP 時組出錯誤的 https absolute URL
#
# ⚠️ demo 專用,正式上線前應移除,或改用 allow <內網網段>; deny all; 限制來源。
# ============================================================
server {
listen 80;
listen [::]:80;
server_name 192.168.0.130;
# 與 stage-9527 block 對齊的全域行為
client_max_body_size 100M;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
gzip on;
gzip_min_length 1024;
gzip_types
text/plain text/css text/xml text/javascript
application/json application/javascript application/xml
image/svg+xml;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# ── 淺層健康檢查 ──
location = /healthz {
access_log off;
return 200 "ok\n";
add_header Content-Type text/plain;
}
# ── /api/* → api-server :3721含 /api/pairing/exchange、models、devices──
location /api/ {
proxy_pass http://visiona_api;
proxy_http_version 1.1;
proxy_buffering off;
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-Proto http;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Connection "";
proxy_no_cache 1;
proxy_cache_bypass 1;
add_header Cache-Control "no-store, no-cache, must-revalidate, private" always;
add_header Pragma "no-cache" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
# ── /storage/* → api-server :3721presigned 下載)──
location /storage/ {
proxy_pass http://visiona_api;
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Connection "";
proxy_no_cache 1;
proxy_cache_bypass 1;
add_header Cache-Control "private, no-store" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
# ── /preset-models/* → api-server :3721B8 預設模型 .nef──
location /preset-models/ {
proxy_pass http://visiona_api;
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Connection "";
expires 1d;
add_header Cache-Control "public, max-age=86400" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
# ── /tunnel/connect — WebSocket upgrade最關鍵──
# 完整複製 stage-9527 block 的 WS 設定http/1.1 + Upgrade + Connection
# + 86400s timeout + buffering off。$connection_upgrade 繼承自 http-context map。
location /tunnel/connect {
proxy_pass http://visiona_tunnel;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_buffering off;
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-Proto http;
}
# ── Next.js hashed static內網瀏覽器用──
location /_next/static/ {
proxy_pass http://visiona_frontend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
# ── 其他靜態資源 ──
location ~* ^/(favicon\.ico|.*\.(?:png|jpg|jpeg|gif|svg|webp|ico|woff|woff2|ttf|eot))$ {
proxy_pass http://visiona_frontend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
expires 1d;
add_header Cache-Control "public, max-age=86400" always;
}
# ── catch-all → Next.js frontend :3000內網瀏覽器用──
location / {
proxy_pass http://visiona_frontend;
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-Proto http;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
}
}