# -*- coding: utf-8 -*- r'''在 Lightsail 上装 nginx,把 yk.xxmxxs.top 反代到中转。 == 架构 == 用户 ──HTTPS──> Cloudflare CDN ──HTTP:80──> nginx ──> 127.0.0.1:13245 (yk.xxmxxs.top) <源站IP> == 为什么要加 nginx 这一层 == 1. **Cloudflare 只能回源到特定端口** (HTTP: 80/8080/8880/2052/2082/2086/2095, HTTPS: 443/2053/2083/2087/2096/8443) 中转跑的 13245 **不在列表里** —— Cloudflare 没法直接回源。 2. **顺便把中转藏起来** 加了 nginx 之后,可以让 relay 只监听 127.0.0.1, 公网只能通过 nginx 进来。少一个暴露面。 3. **以后上 HTTPS 有地方挂证书** 现在先用 Cloudflare 的"灵活"模式(CF→源站走 HTTP), 以后要"完全(严格)"就把源站证书挂到 nginx 上。 == 关于端口 80 的安全 == nginx 监听 80 是**必须的**(Cloudflare 的开源端口)。 但要注意:如果有人知道 <源站IP>,也能直接打 80 绕过 Cloudflare。 所以配置里加了: * 只接受来自 Cloudflare 网段的请求(可选,见 allowlist 那段) * 或者更简单:不启用 allowlist,但把 relay 绑到 127.0.0.1, 反正绕不绕过都是同一个网站,没有额外信息泄露 这里选后者(简单、不容易把自己锁在外面)。 ''' from __future__ import annotations import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "_build")) from lssh import run, upload # noqa: E402 DOMAIN = "yk.xxmxxs.top" RELAY_PORT = "13245" NGINX_SITE = '''# ykxmz 中转 —— 给 Cloudflare 回源用的 # # Cloudflare 只能回源到特定端口,中转跑的 %(port)s 不在列表里, # 所以中间放一层 nginx(监听 80)。 # # 客户端拿到的大文件(安装包 15MB)会被 Cloudflare 缓存 —— # 第二次下载就不回源了,速度取决于离用户最近的 CF 边缘节点。 server { listen 80; listen [::]:80; server_name %(domain)s; # 日志:单独一份,方便排查"到底是 CF 打的还是别人直接打的" access_log /var/log/nginx/ykxmz-access.log; error_log /var/log/nginx/ykxmz-error.log; # ---- 下载:让 Cloudflare 放心缓存 ---- # # 文件名带版本号(ykxmz-setup-2.1.3.exe),同一个名字内容永不改变, # 所以可以长缓存。中转自己也发过 Cache-Control, # 这里显式再写一遍是为了让 CF 一定看到。 location /download/ { proxy_pass http://127.0.0.1:%(port)s; 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 CF-Connecting-IP $http_cf_connecting_ip; proxy_buffering on; proxy_cache_valid 200 7d; add_header Cache-Control "public, max-age=604800, immutable" always; } # ---- 长轮询 ---- # # 中转的 /v1/agent/poll 要挂 60 秒。nginx 默认 60 秒就断, # 必须放宽,否则手表那边会一直掉线重连。 location /v1/ { proxy_pass http://127.0.0.1:%(port)s; 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 CF-Connecting-IP $http_cf_connecting_ip; proxy_read_timeout 180s; proxy_send_timeout 180s; proxy_connect_timeout 30s; # 长轮询不能缓冲,否则要等整个响应完才吐给客户端 —— 那就不是长轮询了 proxy_buffering off; proxy_cache off; } # ---- 其余(官网 / 管理页 / 抢救报告 / 截图)---- location / { proxy_pass http://127.0.0.1:%(port)s; 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 CF-Connecting-IP $http_cf_connecting_ip; proxy_read_timeout 180s; proxy_connect_timeout 30s; } # 别把 nginx 版本号报出去 server_tokens off; client_max_body_size 200m; } ''' def main() -> int: print("=" * 62) print(" 装 nginx,把 %s 反代到中转" % DOMAIN) print("=" * 62) print() print(" ── 1. 装 nginx ──") out = run("which nginx >/dev/null 2>&1 && echo ALREADY || " "(sudo apt-get update -qq && " "sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nginx) " "&& echo OK; nginx -v 2>&1", timeout=600, quiet=True) for ln in out[1].strip().splitlines()[-4:]: print(" " + ln) print() print(" ── 2. 写站点配置 ──") conf = NGINX_SITE % {"domain": DOMAIN, "port": RELAY_PORT} tmp = Path(tempfile.gettempdir()) / "ykxmz.conf" tmp.write_text(conf, encoding="utf-8", newline="\n") upload(str(tmp), "/tmp/ykxmz.conf") out = run("sudo mv /tmp/ykxmz.conf /etc/nginx/sites-available/ykxmz && " "sudo ln -sf /etc/nginx/sites-available/ykxmz " "/etc/nginx/sites-enabled/ykxmz && " # 关掉默认站点,免得抢 80 "sudo rm -f /etc/nginx/sites-enabled/default && " "sudo nginx -t 2>&1 | tail -3", timeout=180, quiet=True) for ln in out[1].strip().splitlines(): print(" " + ln) print() print(" ── 3. 重启 nginx ──") out = run("sudo systemctl enable nginx >/dev/null 2>&1; " "sudo systemctl restart nginx; sleep 2; " "systemctl is-active nginx", timeout=180, quiet=True) print(" nginx: " + out[1].strip()) print() print(" ── 4. 中转改为只监听 127.0.0.1(不再直接对公网)──") # 公网只能通过 nginx 进来,少一个暴露面。 out = run("sudo sed -i 's/^Environment=RELAY_BIND=.*$/" "Environment=RELAY_BIND=127.0.0.1/' " "/etc/systemd/system/ykxmz-relay.service && " "grep RELAY_BIND /etc/systemd/system/ykxmz-relay.service && " "sudo systemctl daemon-reload && " "sudo systemctl restart ykxmz-relay && sleep 3 && " "systemctl is-active ykxmz-relay", timeout=180, quiet=True) for ln in out[1].strip().splitlines(): print(" " + ln) print() print(" ── 5. 验证 ──") script = ( 'printf " %-22s %s\\n" "nginx 直连(本机:80)" ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 ' 'http://127.0.0.1:80/v1/health)"\n' 'printf " %-22s %s\\n" "带 Host 头" ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 ' '-H \'Host: ' + DOMAIN + '\' http://127.0.0.1:80/v1/health)"\n' 'printf " %-22s %s\\n" "公网 80(IP)" ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 15 ' 'http://<源站IP>:80/v1/health)"\n' 'printf " %-22s %s\\n" "中转本机(13245)" ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 ' 'http://127.0.0.1:13245/v1/health)"\n' 'printf " %-22s %s\\n" "中转是否还暴露公网" ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 ' 'http://<源站IP>:13245/v1/health)"\n' ) out = run(script, timeout=180, quiet=True) for ln in out[1].strip().splitlines(): print(ln) print() print(" 说明:上面「中转是否还暴露公网」应该是 000(连不上)——") print(" 那正说明它已经只认 127.0.0.1,公网只能走 nginx。") return 0 if __name__ == "__main__": sys.exit(main())