diff --git a/server_config/README.md b/server_config/README.md index 9f5c6c0..fadf5ed 100644 --- a/server_config/README.md +++ b/server_config/README.md @@ -1,26 +1,386 @@ -# server_config +# RRPP 服务端部署规范 + +> 一套统一的跨境代理节点部署流程,适用于 Debian/Ubuntu 系 Linux。 +> 每台节点运行相同的服务栈:**BBR 拥塞控制 + nginx CONNECT 隧道 + sing-box(VLESS+REALITY / Shadowsocks) + mtg(MTProto)**。 +> +> 本文档为**通用规范**,所有 IP / 域名 / 端口 / 密钥均以**占位符**表示,部署时替换为实际值。 +> 凡标注 `<...>` 的均为占位符,不要原样照抄。 + +--- + +## 目录 + +- [0. 架构总览](#0-架构总览) +- [1. 端口与服务规划](#1-端口与服务规划) +- [2. 系统基础优化(BBR 跨境丢包优化)](#2-系统基础优化bbr-跨境丢包优化) +- [3. 伪装站点页面](#3-伪装站点页面) +- [4. HTTPS 证书(acme.sh DNS-01)](#4-https-证书acmesh-dns-01) +- [5. nginx 源码编译 + CONNECT 隧道](#5-nginx-源码编译--connect-隧道) +- [6. sing-box(VLESS+REALITY / Shadowsocks)](#6-sing-boxvlessreality--shadowsocks) +- [7. mtg(MTProto 代理)](#7-mtgmtproto-代理) +- [8. 本机公网 IP 自动识别(防自代理回环)](#8-本机公网-ip-自动识别防自代理回环) +- [9. systemd 服务文件](#9-systemd-服务文件) +- [10. 启动顺序与验证清单](#10-启动顺序与验证清单) +- [11. 目录结构与文件索引](#11-目录结构与文件索引) + +--- + +## 0. 架构总览 + +``` +客户端 ──► nginx(443/) ──┬─► CONNECT 隧道(任意端口, 全端口放行) + │ └─► 伪装页面(直接访问/IP访问) + │ + ├─► sing-box() ──► VLESS+REALITY(vision) + ├─► sing-box() ──► Shadowsocks(aes-256-gcm) + └─► mtg() ──► MTProto(Fake TLS, Telegram 内置代理) +``` + +**四层代理各司其职:** + +| 层 | 组件 | 协议 | 适用场景 | +|---|---|---|---| +| HTTP 隧道 | nginx | CONNECT + Basic Auth | 浏览器/curl/系统代理,全端口 WebSocket 透传 | +| 通用代理 | sing-box | VLESS+REALITY | Shadowrocket/Clash/v2rayN 客户端 | +| 兼容旧客户端 | sing-box | Shadowsocks | Shadowrocket 2.x 等不支持 REALITY 的客户端 | +| Telegram 专用 | mtg | MTProto Fake TLS | Telegram 内置代理,抗封锁 | + +**核心密钥体系(全套节点共用一套):** +- VLESS+REALITY:统一 UUID、统一 REALITY 私钥/short_id/SNI(SNI 用 `<伪装SNI域名>`,选一个高流量、TLS 指纹稳定的知名站点域名) +- Shadowsocks:统一密码,方法 `aes-256-gcm` +- MTProto:每台**独立 secret**(Fake TLS 类型,fronting 到 `<伪装SNI域名>`) + +--- + +## 1. 端口与服务规划 + +端口全部落在**同一区段**便于防火墙管理(示例用 1200–1220,可按需调整): + +| 端口 | 服务 | 说明 | +|---|---|---| +| `` | nginx | HTTPS 站点 + CONNECT 隧道(示例 `1205`) | +| `` | sing-box | VLESS+REALITY(示例 `1206`) | +| `` | sing-box | Shadowsocks(示例 `1207`) | +| `` | mtg | MTProto(示例 `1210`) | + +> 节点 A 用 `80xx` 区段、节点 B 用 `120xx` 区段皆可,**只要同节点内不冲突、且运营商/防火墙放行**。 +> 防火墙放行:`iptables -A INPUT -p tcp -m multiport --dports ,,, -j ACCEPT` + +**域名规划:** 每个节点一个独立域名 `<节点域名>`(如 `node1.example.com`),A 记录指向节点公网 IP。acme.sh 用 DNS-01 签证书,无需 80 端口可达。 + +--- + +## 2. 系统基础优化(BBR 跨境丢包优化) + +BBR(Bottleneck Bandwidth and RTT)+ fq 队列,显著改善跨境 TCP 丢包/高延迟下的吞吐。 + +### 2.1 启用 BBR + +写入 `/etc/sysctl.d/99-bbr.conf`: + +```conf +net.core.default_qdisc=fq +net.ipv4.tcp_congestion_control=bbr +``` + +应用并验证: + +```bash +modprobe tcp_bbr 2>/dev/null || true # 部分内核需手动加载模块 +sysctl --system +sysctl net.ipv4.tcp_congestion_control net.core.default_qdisc +# 期望输出: bbr / fq +``` + +> **内核要求**:BBR 需 Linux 4.9+。`default_qdisc=fq` 在部分新内核(6.12+ cloud 内核)可能无此参数,仅保留 `tcp_congestion_control=bbr` 即可,不影响 BBR 生效。 + +### 2.2 文件描述符 + +nginx worker 高并发需要提高 limit,在 `/etc/security/limits.conf` 追加: + +``` +* soft nofile 65535 +* hard nofile 65535 +``` + +### 2.3 时区与 NTP + +代理证书校验、日志排障都依赖准确时间: + +```bash +timedatectl set-timezone Asia/Shanghai +apt install -y chrony && systemctl enable --now chrony +``` + +--- + +## 3. 伪装站点页面 + +直接访问节点/IP 时返回一个普通网页(而非代理错误),降低被探测的特征。 + +放置一个静态 `index.html` 到 `/var/www/html/index.html`: +- 任意抓取的合法 HTML 页面(博客/门户首页快照均可) +- **所有节点用同一份**(md5 一致),避免逐节点特征差异 + +```bash +mkdir -p /var/www/html +# index.html 为本仓库 fake-site/index.html 的拷贝 +cp fake-site/index.html /var/www/html/index.html +``` + +--- + +## 4. HTTPS 证书(acme.sh DNS-01) + +nginx 站点与 CONNECT 隧道共用一张证书。用 [acme.sh](https://github.com/acmesh-official/acme.sh) + DNS-01(以阿里云 DNS 为例,其他 DNS 商同理): + +```bash +curl https://get.acme.sh | sh -s email=<你的邮箱> +export Ali_Key="<阿里云AccessKey>" +export Ali_Secret="<阿里云Secret>" + +~/.acme.sh/acme.sh --issue --dns dns_ali -d <节点域名> --keylength ec-256 + +# 安装到 nginx 读取路径 +mkdir -p /etc/nginx/ssl/<节点域名> +~/.acme.sh/acme.sh --install-cert -d <节点域名> --ecc \ + --fullchain-file /etc/nginx/ssl/<节点域名>/fullchain.pem \ + --key-file /etc/nginx/ssl/<节点域名>/privkey.pem +``` + +acme.sh 自带 cron 自动续期。证书路径固定为 `/etc/nginx/ssl/<节点域名>/{fullchain,privkey}.pem`。 + +--- + +## 5. nginx 源码编译 + CONNECT 隧道 + +标准 nginx 不自带 CONNECT 方法支持,需打 [ngx_http_proxy_connect_module](https://github.com/chobits/ngx_http_proxy_connect_module) 补丁后**源码编译**。 + +### 5.1 编译 + +```bash +apt install -y build-essential libssl-dev libpcre3-dev zlib1g-dev curl wget + +NGINX_VER=1.26.3 +cd /usr/local/src +curl -LO http://nginx.org/download/nginx-$NGINX_VER.tar.gz +tar xzf nginx-$NGINX_VER.tar.gz + +git clone https://github.com/chobits/ngx_http_proxy_connect_module.git +cd nginx-$NGINX_VER +patch -p1 < ../ngx_http_proxy_connect_module/patch/proxy_connect_rewrite_102101.patch + +./configure \ + --prefix=/usr/local/nginx \ + --conf-path=/etc/nginx/nginx.conf \ + --pid-path=/var/run/nginx.pid \ + --error-log-path=/var/log/nginx/error.log \ + --http-log-path=/var/log/nginx/access.log \ + --with-http_ssl_module \ + --with-http_v2_module \ + --with-http_realip_module \ + --with-http_stub_status_module \ + --with-http_gzip_static_module \ + --add-module=../ngx_http_proxy_connect_module +make -j$(nproc) && make install +``` + +> 若发行版 nginx 已自带 proxy_connect 补丁(极少见),可用 `apt install nginx` 跳过编译,但源码编译是通用做法。 + +### 5.2 Basic Auth 凭据 + +CONNECT 代理需要 Basic Auth(用户名/密码),生成 htpasswd: + +```bash +apt install -y apache2-utils +htpasswd -bc /etc/nginx/.htpasswd <代理用户名> '<代理密码>' +chmod 644 /etc/nginx/.htpasswd # worker 以 nobody 运行,必须可读 +``` + +### 5.3 nginx.conf + +完整配置见 [nginx/nginx.conf](nginx/nginx.conf)。**关键设计**: + +- **两个 server 块共用 ``**: + - 主块 `server_name <节点域名>`:证书站点 + `location /` 返回伪装页;非本域名一律 rewrite 到 `/__proxy` 转发 + - fallback 块 `default_server`:其他 SNI 也接受 CONNECT 代理;本机/私网/公网 IP 直连返回伪装页,其余走 `/__proxy` +- `proxy_connect_allow all;`:**全端口** CONNECT 放行(codex/WebSocket 走任意端口均通)。若需限制,改回 `proxy_connect_allow 443 563;` +- `auth_basic`:仅 CONNECT/正向代理需要认证,`location /` 伪装页 `auth_basic off` +- **自代理回环保护**:`/__proxy` 转发时带 `X-Proxy-Loop-Guard` 头,二次进入返回 508 防死循环 +- `/__local`:本机 IP 直连时剥前缀返回伪装页 +- `map $host $is_local`:私网/回环段判定;公网 IP 由 cron 脚本动态写入 `local_ip_rules.conf` + +### 5.4 自代理回环保护说明 + +正向代理转发目标若指向**本机自己**(如域名公网 IP 变更、或代理目标就是本节点),会无限循环。两层防护: +1. `map` 判定私网/回环段 + cron 维护的本机公网 IP 规则 → 直连伪装页 +2. `/__proxy` 的 Loop-Guard 头兜底 → 二次进入返回 508 + +--- + +## 6. sing-box(VLESS+REALITY / Shadowsocks) + +### 6.1 安装 + +```bash +SINGBOX_VER=1.13.19 +curl -LO https://github.com/SagerNet/sing-box/releases/download/v$SINGBOX_VER/sing-box-$SINGBOX_VER-linux-amd64.tar.gz +tar xzf sing-box-*.tar.gz +install -m 755 sing-box-$SINGBOX_VER-linux-amd64/sing-box /usr/local/bin/sing-box +``` + +### 6.2 配置 + +完整配置见 [sing-box/config.json](sing-box/config.json)。要点: +- **VLESS+REALITY**:统一 UUID、统一私钥/short_id/SNI,flow `xtls-rprx-vision`;REALITY handshake server 用 `<伪装SNI域名>:443` +- **Shadowsocks**:`aes-256-gcm` + 统一密码,独立端口(兼容不支持 REALITY 的老客户端) +- 无 SOCKS5 inbound(已按需移除;如需可在 inbounds 追加 `type: socks`) + +### 6.3 REALITY 密钥生成 + +首次部署生成一套(**全套节点共用**): + +```bash +# 生成私钥/公钥(sing-box 1.13+ 内置 reality 密钥生成) +sing-box generate reality-keypair +# 输出 private_key / public_key,记录备用 +# short_id 用 8 字节随机 hex: +openssl rand -hex 8 +# UUID: +sing-box generate uuid +``` + +> 私钥只放服务端配置;公钥给客户端订阅。UUID/short_id 全节点一致。 + +--- + +## 7. mtg(MTProto 代理) + +Telegram 内置代理,Fake TLS 模式抗封锁。用 [mtg](https://github.com/9seconds/mtg)(Go 单二进制): + +### 7.1 安装 + +```bash +MTG_VER=2.2.8 +curl -LO https://github.com/9seconds/mtg/releases/download/v$MTG_VER/mtg-$MTG_VER-linux-amd64.tar.gz +tar xzf mtg-*.tar.gz +install -m 755 mtg-$MTG_VER-linux-amd64/mtg /usr/local/bin/mtg +``` + +### 7.2 生成 secret(每台独立) + +```bash +mtg generate-secret --hex <伪装SNI域名> +# 输出 ee<32hex><域名hex>,以 ee 开头 = Fake TLS 类型 +``` + +### 7.3 运行 + +```bash +mtg simple-run --prefer-ip=prefer-ipv4 0.0.0.0: <上面生成的secret> +``` + +> `--prefer-ip=prefer-ipv4`:强制 IPv4 出站,避免容器/双栈环境下 IPv6 不可达导致回退失败。 + +### 7.4 Telegram 客户端参数 + +- 服务器:`<节点域名>` +- 端口:`` +- Secret:上面生成的 hex(Telegram 自动识别 Fake TLS) + +--- + +## 8. 本机公网 IP 自动识别(防自代理回环) + +正向代理转发时,若目标 Host 是本机公网 IP,会被识别为"本机直连"返回伪装页(而非经代理转发本机,造成回环)。公网 IP 可能变动,用 cron 脚本每 5 分钟自动维护。 + +脚本见 [scripts/sync-nginx-local-ips.sh](scripts/sync-nginx-local-ips.sh),cron 见 [cron/sync-nginx-local-ips](cron/sync-nginx-local-ips): + +```cron +*/5 * * * * root /usr/local/sbin/sync-nginx-local-ips.sh >/dev/null 2>&1 +``` + +脚本逻辑:取 `api.ipify.org` 出站 IP + 节点域名 A 记录,比对重写 `/etc/nginx/local_ip_rules.conf`,**仅变更时** `nginx -t && systemctl reload nginx`。 + +--- + +## 9. systemd 服务文件 + +| 服务 | 文件 | +|---|---| +| nginx | [systemd/nginx.service](systemd/nginx.service) | +| sing-box | [systemd/sing-box.service](systemd/sing-box.service) | +| mtg | [systemd/mtg.service](systemd/mtg.service) | + +```bash +cp systemd/*.service /etc/systemd/system/ +systemctl daemon-reload +systemctl enable --now nginx sing-box mtg +``` + +--- + +## 10. 启动顺序与验证清单 + +### 启动顺序 + +```bash +sysctl --system # 1. BBR +systemctl start sing-box # 2. VLESS/SS +systemctl start mtg # 3. MTProto +systemctl start nginx # 4. nginx(最后,依赖证书/伪装页/规则文件) +``` + +### 验证清单 + +```bash +# BBR +sysctl net.ipv4.tcp_congestion_control # => bbr + +# sing-box +sing-box check -c /etc/sing-box/config.json +systemctl is-active sing-box + +# mtg(Fake TLS:非法 ClientHello 应返回 TLS alert 0x15) +timeout 8 python3 -c " +import socket,os,struct +s=socket.connect(('<节点域名>',)) if False else socket.create_connection(('<节点域名>',),timeout=6) +s.sendall(b'\x16\x03\x01'+struct.pack('>H',100)+os.urandom(100)) +print('resp:',s.recv(8).hex())" # 期望首字节 15 (TLS alert) + +# nginx CONNECT(带 Basic Auth,B64 = base64('<代理用户名>:<代理密码>')) +B64=$(printf '<代理用户名>:<代理密码>' | base64 -w0) +curl -sk --proxy-insecure -x https://<节点域名>: \ + --proxy-header "Authorization: Basic $B64" \ + -o /dev/null -w '%{http_code}\n' http://example.com/ # => 200 + +# 伪装页(直连 IP/域名,无认证) +curl -sk https://<节点域名>:/ -o /dev/null -w '%{http_code}\n' # => 200 +``` + +--- + +## 11. 目录结构与文件索引 ``` server_config/ +├── README.md ← 本文档 ├── nginx/ -│ ├── nginx.conf -│ ├── conf.d/ -│ │ ├── api.aeolianchi.me.conf -│ │ └── autodiscover.aeolianchi.me.conf -│ └── local_ip_rules.conf.example +│ ├── nginx.conf ← 完整 nginx 配置(占位符版) +│ ├── local_ip_rules.conf.example ← 本机 IP 规则文件示例(cron 自动生成,勿手改) +│ └── conf.d/ ← 预留:额外站点配置 ├── sing-box/ -│ └── config.json -├── traffic-monitor/ -│ ├── config.json -│ ├── traffic_monitor.py -│ └── traffic-monitor.service +│ └── config.json ← sing-box 配置(VLESS+REALITY + SS,占位符版) +├── systemd/ +│ ├── nginx.service +│ ├── sing-box.service +│ └── mtg.service ├── scripts/ -│ ├── sync-nginx-local-ips.sh -│ └── rrpp-stats -├── pac/ -│ ├── proxy.pac -│ └── proxy_wg.pac -└── cron/ - ├── crontab.txt - └── sync-nginx-local-ips +│ └── sync-nginx-local-ips.sh ← 本机公网 IP 规则自动维护 +├── cron/ +│ └── sync-nginx-local-ips ← cron 定义(每 5 分钟) +└── fake-site/ + └── index.html ← 伪装站点页面(所有节点共用同一份) ``` + +> 旧的 `traffic-monitor/`、`pac/`、`conf.d/*.conf` 已下线或迁移,不再维护,已从本目录移除。 diff --git a/server_config/cron/crontab.txt b/server_config/cron/crontab.txt deleted file mode 100644 index 907aaf8..0000000 --- a/server_config/cron/crontab.txt +++ /dev/null @@ -1,2 +0,0 @@ -* * * * * /usr/local/bin/rrpp-stats | sudo tee /root/backups/stats.json > /dev/null -27 0,6,12,18 * * * "/home/RRRosmontis/.acme.sh"/acme.sh --cron --home "/home/RRRosmontis/.acme.sh" > /dev/null diff --git a/server_config/fake-site/index.html b/server_config/fake-site/index.html new file mode 100644 index 0000000..9b02787 --- /dev/null +++ b/server_config/fake-site/index.html @@ -0,0 +1,100 @@ + + + + + + +200: Internal server OK + + + + + + + + +
+
+
+

+ Internal server OK + Error code 200 +

+
+ Visit example.com for more information. +
+
2026-06-28 05:00:19 UTC
+
+
+
+
+
+
+ + +
+ You +

Browser

+ Working +
+
+
+ + +
+ San Francisco +

Cloudflare

+ Working +
+
+
+ + +
+ Website +

Host

+ Working +
+
+
+
+ +
+
+
+

What happened?

+ There isn't an internal server error on Cloudflare's network. +
+
+

What can I do?

+ Please try again in a few minutes. +
+
+
+ + +
+
+ + + diff --git a/server_config/nginx/conf.d/api.aeolianchi.me.conf b/server_config/nginx/conf.d/api.aeolianchi.me.conf deleted file mode 100644 index 1388be5..0000000 --- a/server_config/nginx/conf.d/api.aeolianchi.me.conf +++ /dev/null @@ -1,19 +0,0 @@ -server { - listen 443 ssl; - http2 on; - server_name api.aeolianchi.me; - - ssl_certificate /etc/nginx/ssl/api.aeolianchi.me/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/api.aeolianchi.me/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - - location /vpn/status/traffic { - proxy_pass http://127.0.0.1:18080; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } -} diff --git a/server_config/nginx/conf.d/autodiscover.aeolianchi.me.conf b/server_config/nginx/conf.d/autodiscover.aeolianchi.me.conf deleted file mode 100644 index 7b48573..0000000 --- a/server_config/nginx/conf.d/autodiscover.aeolianchi.me.conf +++ /dev/null @@ -1,44 +0,0 @@ -# autodiscover.aeolianchi.me -> jp 反代 -> bj autodiscover.rrrosmontis.icu -server { - listen 80; - listen [::]:80; - server_name autodiscover.aeolianchi.me; - location /.well-known/acme-challenge/ { - root /var/www/acme; - } - - location / { - return 301 https://$host$request_uri; - } -} - -# autodiscover.aeolianchi.me 443 -> 反代 -> bj autodiscover.rrrosmontis.icu -server { - listen 443 ssl; - http2 on; - server_name autodiscover.aeolianchi.me; - - ssl_certificate /etc/nginx/ssl/aeolianchi.me/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/aeolianchi.me/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - add_header Strict-Transport-Security "max-age=31536000"; - error_page 497 https://$host$request_uri; - - location / { - proxy_pass https://autodiscover.rrrosmontis.icu; - proxy_set_header Host autodiscover.rrrosmontis.icu; - 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 $scheme; - proxy_http_version 1.1; - proxy_ssl_server_name on; - proxy_ssl_name autodiscover.rrrosmontis.icu; - } - - access_log /var/log/nginx/autodiscover.aeolianchi.me.log; - error_log /var/log/nginx/autodiscover.aeolianchi.me.error.log; -} diff --git a/server_config/nginx/local_ip_rules.conf.example b/server_config/nginx/local_ip_rules.conf.example index 2676bd2..2cfe799 100644 --- a/server_config/nginx/local_ip_rules.conf.example +++ b/server_config/nginx/local_ip_rules.conf.example @@ -1,4 +1,5 @@ # nginx 本机公网 IP 直连规则(自动生成,勿手改;由 sync-nginx-local-ips.sh 维护) # 公网 IP 变更后本文件会被重写并 reload nginx -if ($host = "") { rewrite ^(.*)$ /__local$1 last; } -if ($host = "") { rewrite ^(.*)$ /__local$1 last; } +# 每条规则对应一个本机公网 IP(出站 IP + 节点域名 A 记录) +if ($host = "") { rewrite ^(.*)$ /__local$1 last; } +if ($host = "") { rewrite ^(.*)$ /__local$1 last; } diff --git a/server_config/nginx/nginx.conf b/server_config/nginx/nginx.conf index a94ba91..65b636d 100644 --- a/server_config/nginx/nginx.conf +++ b/server_config/nginx/nginx.conf @@ -1,4 +1,3 @@ -load_module /usr/lib/nginx/modules/ngx_http_proxy_connect_module.so; worker_processes auto; worker_rlimit_nofile 65535; events { @@ -25,45 +24,15 @@ http { keepalive_timeout 65; keepalive_requests 100; - # 80 端口:iam.aeolianchi.me 强制跳转到 HTTPS(保留 ACME 验证路径) + # :主域名 HTTPS 站点 + 认证 CONNECT 代理 server { - listen 80; - server_name iam.aeolianchi.me; - - location /.well-known/acme-challenge/ { - root /var/www/acme; - } - - location / { - return 301 https://$host$request_uri; - } - } - - # 80 端口:默认站点只做静态伪装,不提供代理、不弹认证 - server { - listen 80 default_server; - server_name _; - - root /var/www/html; - index index.html; - - location /.well-known/acme-challenge/ { - root /var/www/acme; - } - - location / { - try_files $uri $uri/ =404; - } - } - - server { - listen 443 ssl; + listen ssl; http2 on; - server_name XXX; + server_name <节点域名>; root /var/www/html; - ssl_certificate /etc/nginx/ssl/XXX/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/XXX/privkey.pem; + ssl_certificate /etc/nginx/ssl/<节点域名>/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/<节点域名>/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; @@ -74,14 +43,14 @@ http { auth_basic_user_file /etc/nginx/.htpasswd; proxy_connect; - proxy_connect_allow 443 563; + proxy_connect_allow all; proxy_connect_connect_timeout 60s; proxy_connect_read_timeout 120s; proxy_connect_send_timeout 120s; location / { auth_basic off; - if ($host != "iam.aeolianchi.me") { + if ($host != "<节点域名>") { rewrite ^ /__proxy last; } index index.html; @@ -104,14 +73,14 @@ http { } } - # 443 端口 fallback:其他 SNI 也可用 CONNECT 代理;普通 GET 只返回伪装页 + # fallback:其他 SNI 也可用 CONNECT 代理;普通 GET 只返回伪装页 server { - listen 443 ssl default_server; + listen ssl default_server; http2 on; server_name _; - ssl_certificate /etc/nginx/ssl/iam.aeolianchi.me/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/iam.aeolianchi.me/privkey.pem; + ssl_certificate /etc/nginx/ssl/<节点域名>/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/<节点域名>/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; @@ -122,7 +91,7 @@ http { auth_basic_user_file /etc/nginx/.htpasswd; proxy_connect; - proxy_connect_allow 443 563; + proxy_connect_allow all; proxy_connect_connect_timeout 60s; proxy_connect_read_timeout 120s; proxy_connect_send_timeout 120s; diff --git a/server_config/pac/proxy.pac b/server_config/pac/proxy.pac deleted file mode 100644 index df26da4..0000000 --- a/server_config/pac/proxy.pac +++ /dev/null @@ -1,647 +0,0 @@ -function FindProxyForURL(url, host) { - // RRPP 规则代理列表 — 仅以下域名走加密隧道 - var proxy = "PROXY 127.0.0.1:1080"; - // Google 全系 + Gemini + CDN - if (shExpMatch(host, "google.com") || - shExpMatch(host, "*.google.com") || - shExpMatch(host, "google.com.hk") || - shExpMatch(host, "*.google.com.hk") || - shExpMatch(host, "google.co.jp") || - shExpMatch(host, "*.google.co.jp") || - shExpMatch(host, "googleapis.com") || - shExpMatch(host, "*.googleapis.com") || - shExpMatch(host, "googleusercontent.com") || - shExpMatch(host, "*.googleusercontent.com") || - shExpMatch(host, "googletagmanager.com") || - shExpMatch(host, "*.googletagmanager.com") || - shExpMatch(host, "google-analytics.com") || - shExpMatch(host, "*.google-analytics.com") || - shExpMatch(host, "gstatic.com") || - shExpMatch(host, "*.gstatic.com") || - shExpMatch(host, "ggpht.com") || - shExpMatch(host, "*.ggpht.com") || - shExpMatch(host, "googlehosted.com") || - shExpMatch(host, "*.googlehosted.com") || - shExpMatch(host, "1e100.net") || - shExpMatch(host, "*.1e100.net") || - shExpMatch(host, "youtube.com") || - shExpMatch(host, "*.youtube.com") || - shExpMatch(host, "ytimg.com") || - shExpMatch(host, "*.ytimg.com") || - shExpMatch(host, "googlevideo.com") || - shExpMatch(host, "*.googlevideo.com") || - shExpMatch(host, "gemini.google.com") || - shExpMatch(host, "*.gemini.google.com") || - shExpMatch(host, "generativeai.googleapis.com") || - shExpMatch(host, "*.generativeai.googleapis.com") || - shExpMatch(host, "aistudio.google.com") || - shExpMatch(host, "*.aistudio.google.com") || - shExpMatch(host, "deepmind.google.com") || - shExpMatch(host, "*.deepmind.google.com") || - shExpMatch(host, "deepmind.com") || - shExpMatch(host, "*.deepmind.com")) { - return proxy; - } - - // OpenAI / ChatGPT 全系 + CDN - if (shExpMatch(host, "openai.com") || - shExpMatch(host, "*.openai.com") || - shExpMatch(host, "chatgpt.com") || - shExpMatch(host, "*.chatgpt.com") || - shExpMatch(host, "oaistatic.com") || - shExpMatch(host, "*.oaistatic.com") || - shExpMatch(host, "oaiusercontent.com") || - shExpMatch(host, "*.oaiusercontent.com") || - shExpMatch(host, "openaiusercontent.com") || - shExpMatch(host, "*.openaiusercontent.com")) { - return proxy; - } - - // Grok / xAI 全系 + CDN - if (shExpMatch(host, "grok.com") || - shExpMatch(host, "*.grok.com") || - shExpMatch(host, "x.com") || - shExpMatch(host, "*.x.com") || - shExpMatch(host, "twitter.com") || - shExpMatch(host, "*.twitter.com") || - shExpMatch(host, "twimg.com") || - shExpMatch(host, "*.twimg.com") || - shExpMatch(host, "x.ai") || - shExpMatch(host, "*.x.ai") || - shExpMatch(host, "xai.com") || - shExpMatch(host, "*.xai.com")) { - return proxy; - } - - // Meta 全系 + CDN - if (shExpMatch(host, "meta.com") || - shExpMatch(host, "*.meta.com") || - shExpMatch(host, "meta.ai") || - shExpMatch(host, "*.meta.ai") || - shExpMatch(host, "facebook.com") || - shExpMatch(host, "*.facebook.com") || - shExpMatch(host, "fb.com") || - shExpMatch(host, "*.fb.com") || - shExpMatch(host, "fbcdn.net") || - shExpMatch(host, "*.fbcdn.net") || - shExpMatch(host, "fbsbx.com") || - shExpMatch(host, "*.fbsbx.com") || - shExpMatch(host, "instagram.com") || - shExpMatch(host, "*.instagram.com") || - shExpMatch(host, "cdninstagram.com") || - shExpMatch(host, "*.cdninstagram.com") || - shExpMatch(host, "threads.net") || - shExpMatch(host, "*.threads.net") || - shExpMatch(host, "whatsapp.com") || - shExpMatch(host, "*.whatsapp.com") || - shExpMatch(host, "whatsapp.net") || - shExpMatch(host, "*.whatsapp.net") || - shExpMatch(host, "messenger.com") || - shExpMatch(host, "*.messenger.com")) { - return proxy; - } - - // Claude / Anthropic 全系 + CDN - if (shExpMatch(host, "anthropic.com") || - shExpMatch(host, "*.anthropic.com") || - shExpMatch(host, "claude.ai") || - shExpMatch(host, "*.claude.ai") || - shExpMatch(host, "claudeusercontent.com") || - shExpMatch(host, "*.claudeusercontent.com")) { - return proxy; - } - - // Dify 全系 - if (shExpMatch(host, "dify.ai") || - shExpMatch(host, "*.dify.ai") || - shExpMatch(host, "udify.app") || - shExpMatch(host, "*.udify.app")) { - return proxy; - } - - // Cloudflare 全系 - if (shExpMatch(host, "cloudflare.com") || - shExpMatch(host, "*.cloudflare.com") || - shExpMatch(host, "cloudflare.net") || - shExpMatch(host, "*.cloudflare.net") || - shExpMatch(host, "cloudflareinsights.com") || - shExpMatch(host, "*.cloudflareinsights.com") || - shExpMatch(host, "cloudflarestatus.com") || - shExpMatch(host, "*.cloudflarestatus.com") || - shExpMatch(host, "cloudflarecdn.com") || - shExpMatch(host, "*.cloudflarecdn.com") || - shExpMatch(host, "cf-cdn.com") || - shExpMatch(host, "*.cf-cdn.com") || - shExpMatch(host, "workers.dev") || - shExpMatch(host, "*.workers.dev") || - shExpMatch(host, "pages.dev") || - shExpMatch(host, "*.pages.dev")) { - return proxy; - } - - // GitHub 全系 - if (shExpMatch(host, "github.com") || - shExpMatch(host, "*.github.com") || - shExpMatch(host, "githubusercontent.com") || - shExpMatch(host, "*.githubusercontent.com") || - shExpMatch(host, "githubassets.com") || - shExpMatch(host, "*.githubassets.com") || - shExpMatch(host, "github.io") || - shExpMatch(host, "*.github.io") || - shExpMatch(host, "githubcopilot.com") || - shExpMatch(host, "*.githubcopilot.com") || - shExpMatch(host, "ghcr.io") || - shExpMatch(host, "*.ghcr.io")) { - return proxy; - } - - // Hugging Face 全系 + CDN - if (shExpMatch(host, "huggingface.co") || - shExpMatch(host, "*.huggingface.co") || - shExpMatch(host, "hf.co") || - shExpMatch(host, "*.hf.co") || - shExpMatch(host, "huggingfacecdn.com") || - shExpMatch(host, "*.huggingfacecdn.com")) { - return proxy; - } - - // Microsoft Copilot 全系 + Bing + Azure CDN - if (shExpMatch(host, "microsoft.com") || - shExpMatch(host, "*.microsoft.com") || - shExpMatch(host, "bing.com") || - shExpMatch(host, "*.bing.com") || - shExpMatch(host, "bingapis.com") || - shExpMatch(host, "*.bingapis.com") || - shExpMatch(host, "bing.net") || - shExpMatch(host, "*.bing.net") || - shExpMatch(host, "live.com") || - shExpMatch(host, "*.live.com") || - shExpMatch(host, "microsoftonline.com") || - shExpMatch(host, "*.microsoftonline.com") || - shExpMatch(host, "microsoft365.com") || - shExpMatch(host, "*.microsoft365.com") || - shExpMatch(host, "office.com") || - shExpMatch(host, "*.office.com") || - shExpMatch(host, "office.net") || - shExpMatch(host, "*.office.net") || - shExpMatch(host, "azure.com") || - shExpMatch(host, "*.azure.com") || - shExpMatch(host, "azureedge.net") || - shExpMatch(host, "*.azureedge.net") || - shExpMatch(host, "azurefd.net") || - shExpMatch(host, "*.azurefd.net") || - shExpMatch(host, "msn.com") || - shExpMatch(host, "*.msn.com") || - shExpMatch(host, "msecnd.net") || - shExpMatch(host, "*.msecnd.net") || - shExpMatch(host, "msftncsi.com") || - shExpMatch(host, "*.msftncsi.com") || - shExpMatch(host, "s-microsoft.com") || - shExpMatch(host, "*.s-microsoft.com")) { - return proxy; - } - - // Steam 全系 + CDN / 社区 / 下载 - if (shExpMatch(host, "steampowered.com") || - shExpMatch(host, "*.steampowered.com") || - shExpMatch(host, "steamcommunity.com") || - shExpMatch(host, "*.steamcommunity.com") || - shExpMatch(host, "steam.com") || - shExpMatch(host, "*.steam.com") || - shExpMatch(host, "steamgames.com") || - shExpMatch(host, "*.steamgames.com") || - shExpMatch(host, "steamusercontent.com") || - shExpMatch(host, "*.steamusercontent.com") || - shExpMatch(host, "steamcontent.com") || - shExpMatch(host, "*.steamcontent.com") || - shExpMatch(host, "steamstatic.com") || - shExpMatch(host, "*.steamstatic.com") || - shExpMatch(host, "steam-chat.com") || - shExpMatch(host, "*.steam-chat.com") || - shExpMatch(host, "steam-api.com") || - shExpMatch(host, "*.steam-api.com") || - shExpMatch(host, "steamserver.net") || - shExpMatch(host, "*.steamserver.net") || - shExpMatch(host, "steamcdn-a.akamaihd.net") || - shExpMatch(host, "*.steamcdn-a.akamaihd.net") || - shExpMatch(host, "steamcommunity-a.akamaihd.net") || - shExpMatch(host, "*.steamcommunity-a.akamaihd.net") || - shExpMatch(host, "steamstore-a.akamaihd.net") || - shExpMatch(host, "*.steamstore-a.akamaihd.net") || - shExpMatch(host, "steamuserimages-a.akamaihd.net") || - shExpMatch(host, "*.steamuserimages-a.akamaihd.net") || - shExpMatch(host, "akamaihd.net") || - shExpMatch(host, "*.akamaihd.net") || - shExpMatch(host, "valvesoftware.com") || - shExpMatch(host, "*.valvesoftware.com")) { - return proxy; - } - - // Discord 全系 - if (shExpMatch(host, "discord.com") || - shExpMatch(host, "*.discord.com") || - shExpMatch(host, "discordapp.com") || - shExpMatch(host, "*.discordapp.com") || - shExpMatch(host, "discord.gg") || - shExpMatch(host, "*.discord.gg") || - shExpMatch(host, "discord.media") || - shExpMatch(host, "*.discord.media") || - shExpMatch(host, "discordapp.net") || - shExpMatch(host, "*.discordapp.net")) { - return proxy; - } - - // Telegram 全系 - if (shExpMatch(host, "telegram.org") || - shExpMatch(host, "*.telegram.org") || - shExpMatch(host, "t.me") || - shExpMatch(host, "*.t.me") || - shExpMatch(host, "telega.one") || - shExpMatch(host, "*.telega.one") || - shExpMatch(host, "telegram-cdn.org") || - shExpMatch(host, "*.telegram-cdn.org")) { - return proxy; - } - - // Reddit 全系 - if (shExpMatch(host, "reddit.com") || - shExpMatch(host, "*.reddit.com") || - shExpMatch(host, "redd.it") || - shExpMatch(host, "*.redd.it") || - shExpMatch(host, "redditmedia.com") || - shExpMatch(host, "*.redditmedia.com") || - shExpMatch(host, "redditstatic.com") || - shExpMatch(host, "*.redditstatic.com")) { - return proxy; - } - - // LINE 全系 - if (shExpMatch(host, "line.me") || - shExpMatch(host, "*.line.me") || - shExpMatch(host, "line-apps.com") || - shExpMatch(host, "*.line-apps.com") || - shExpMatch(host, "line-scdn.net") || - shExpMatch(host, "*.line-scdn.net")) { - return proxy; - } - - // Spotify 全系 - if (shExpMatch(host, "spotify.com") || - shExpMatch(host, "*.spotify.com") || - shExpMatch(host, "spotifycdn.com") || - shExpMatch(host, "*.spotifycdn.com") || - shExpMatch(host, "scdn.co") || - shExpMatch(host, "*.scdn.co")) { - return proxy; - } - - // Netflix 全系 - if (shExpMatch(host, "netflix.com") || - shExpMatch(host, "*.netflix.com") || - shExpMatch(host, "nflxvideo.net") || - shExpMatch(host, "*.nflxvideo.net") || - shExpMatch(host, "nflximg.net") || - shExpMatch(host, "*.nflximg.net") || - shExpMatch(host, "nflxext.com") || - shExpMatch(host, "*.nflxext.com")) { - return proxy; - } - - // Disney+ 全系 - if (shExpMatch(host, "disneyplus.com") || - shExpMatch(host, "*.disneyplus.com") || - shExpMatch(host, "disney-plus.net") || - shExpMatch(host, "*.disney-plus.net") || - shExpMatch(host, "dssott.com") || - shExpMatch(host, "*.dssott.com")) { - return proxy; - } - - // TikTok 全系 - if (shExpMatch(host, "tiktok.com") || - shExpMatch(host, "*.tiktok.com") || - shExpMatch(host, "tiktokcdn.com") || - shExpMatch(host, "*.tiktokcdn.com") || - shExpMatch(host, "tiktokv.com") || - shExpMatch(host, "*.tiktokv.com")) { - return proxy; - } - - // PlayStation 全系 - if (shExpMatch(host, "playstation.com") || - shExpMatch(host, "*.playstation.com") || - shExpMatch(host, "sonyentertainmentnetwork.com") || - shExpMatch(host, "*.sonyentertainmentnetwork.com")) { - return proxy; - } - - // Nintendo 全系 - if (shExpMatch(host, "nintendo.com") || - shExpMatch(host, "*.nintendo.com") || - shExpMatch(host, "nintendowifi.net") || - shExpMatch(host, "*.nintendowifi.net")) { - return proxy; - } - - // Epic Games 全系 - if (shExpMatch(host, "epicgames.com") || - shExpMatch(host, "*.epicgames.com") || - shExpMatch(host, "unrealengine.com") || - shExpMatch(host, "*.unrealengine.com")) { - return proxy; - } - - // DuckDuckGo / Brave Search - if (shExpMatch(host, "duckduckgo.com") || - shExpMatch(host, "*.duckduckgo.com") || - shExpMatch(host, "search.brave.com") || - shExpMatch(host, "*.search.brave.com")) { - return proxy; - } - - // Proton 全系 - if (shExpMatch(host, "proton.me") || - shExpMatch(host, "*.proton.me") || - shExpMatch(host, "protonmail.com") || - shExpMatch(host, "*.protonmail.com") || - shExpMatch(host, "protonvpn.com") || - shExpMatch(host, "*.protonvpn.com")) { - return proxy; - } - - // Stack Overflow / Stack Exchange 全系 - if (shExpMatch(host, "stackoverflow.com") || - shExpMatch(host, "*.stackoverflow.com") || - shExpMatch(host, "stackexchange.com") || - shExpMatch(host, "*.stackexchange.com") || - shExpMatch(host, "superuser.com") || - shExpMatch(host, "*.superuser.com") || - shExpMatch(host, "serverfault.com") || - shExpMatch(host, "*.serverfault.com")) { - return proxy; - } - - // Quora / Medium - if (shExpMatch(host, "quora.com") || - shExpMatch(host, "*.quora.com") || - shExpMatch(host, "medium.com") || - shExpMatch(host, "*.medium.com")) { - return proxy; - } - - // Pixiv 全系 - if (shExpMatch(host, "pixiv.net") || - shExpMatch(host, "*.pixiv.net") || - shExpMatch(host, "pximg.net") || - shExpMatch(host, "*.pximg.net")) { - return proxy; - } - - // ニコニコ 全系 - if (shExpMatch(host, "nicovideo.jp") || - shExpMatch(host, "*.nicovideo.jp") || - shExpMatch(host, "nico.ms") || - shExpMatch(host, "*.nico.ms")) { - return proxy; - } - - // EA / Origin / Apex 全系 - if (shExpMatch(host, "ea.com") || - shExpMatch(host, "*.ea.com") || - shExpMatch(host, "origin.com") || - shExpMatch(host, "*.origin.com") || - shExpMatch(host, "eaassets.com") || - shExpMatch(host, "*.eaassets.com") || - shExpMatch(host, "apexlegends.com") || - shExpMatch(host, "*.apexlegends.com")) { - return proxy; - } - - // Battle.net / Blizzard 全系 - if (shExpMatch(host, "battle.net") || - shExpMatch(host, "*.battle.net") || - shExpMatch(host, "blizzard.com") || - shExpMatch(host, "*.blizzard.com")) { - return proxy; - } - - // Ubisoft 全系 - if (shExpMatch(host, "ubisoft.com") || - shExpMatch(host, "*.ubisoft.com") || - shExpMatch(host, "ubi.com") || - shExpMatch(host, "*.ubi.com")) { - return proxy; - } - - // Riot Games / 英雄联盟 / Valorant 全系 - if (shExpMatch(host, "riotgames.com") || - shExpMatch(host, "*.riotgames.com") || - shExpMatch(host, "leagueoflegends.com") || - shExpMatch(host, "*.leagueoflegends.com") || - shExpMatch(host, "valorant.com") || - shExpMatch(host, "*.valorant.com")) { - return proxy; - } - - // GOG / itch.io 全系 - if (shExpMatch(host, "gog.com") || - shExpMatch(host, "*.gog.com") || - shExpMatch(host, "itch.io") || - shExpMatch(host, "*.itch.io")) { - return proxy; - } - - // Twitch 全系 - if (shExpMatch(host, "twitch.tv") || - shExpMatch(host, "*.twitch.tv") || - shExpMatch(host, "ttvnw.net") || - shExpMatch(host, "*.ttvnw.net") || - shExpMatch(host, "twitchcdn.net") || - shExpMatch(host, "*.twitchcdn.net")) { - return proxy; - } - - // Crunchyroll 全系 - if (shExpMatch(host, "crunchyroll.com") || - shExpMatch(host, "*.crunchyroll.com")) { - return proxy; - } - - // Amazon Prime Video 全系 - if (shExpMatch(host, "primevideo.com") || - shExpMatch(host, "*.primevideo.com")) { - return proxy; - } - - // Apple TV+ 全系 - if (shExpMatch(host, "tv.apple.com") || - shExpMatch(host, "tvplus.apple.com")) { - return proxy; - } - - // HBO Max / Hulu / Paramount+ 全系 - if (shExpMatch(host, "max.com") || - shExpMatch(host, "*.max.com") || - shExpMatch(host, "hulu.com") || - shExpMatch(host, "*.hulu.com") || - shExpMatch(host, "paramountplus.com") || - shExpMatch(host, "*.paramountplus.com")) { - return proxy; - } - - // Slack 全系 - if (shExpMatch(host, "slack.com") || - shExpMatch(host, "*.slack.com") || - shExpMatch(host, "slack-edge.com") || - shExpMatch(host, "*.slack-edge.com") || - shExpMatch(host, "slack-imgs.com") || - shExpMatch(host, "*.slack-imgs.com")) { - return proxy; - } - - // Zoom 全系 - if (shExpMatch(host, "zoom.us") || - shExpMatch(host, "*.zoom.us") || - shExpMatch(host, "zoom.com") || - shExpMatch(host, "*.zoom.com") || - shExpMatch(host, "zoomgov.com") || - shExpMatch(host, "*.zoomgov.com")) { - return proxy; - } - - // Signal 全系 - if (shExpMatch(host, "signal.org") || - shExpMatch(host, "*.signal.org")) { - return proxy; - } - - // Docker 全系 - if (shExpMatch(host, "docker.com") || - shExpMatch(host, "*.docker.com") || - shExpMatch(host, "docker.io") || - shExpMatch(host, "*.docker.io")) { - return proxy; - } - - // GitLab 全系 - if (shExpMatch(host, "gitlab.com") || - shExpMatch(host, "*.gitlab.com") || - shExpMatch(host, "gitlab.io") || - shExpMatch(host, "*.gitlab.io")) { - return proxy; - } - - // Vercel 全系 - if (shExpMatch(host, "vercel.com") || - shExpMatch(host, "*.vercel.com") || - shExpMatch(host, "vercel.app") || - shExpMatch(host, "*.vercel.app")) { - return proxy; - } - - // Netlify 全系 - if (shExpMatch(host, "netlify.com") || - shExpMatch(host, "*.netlify.com") || - shExpMatch(host, "netlify.app") || - shExpMatch(host, "*.netlify.app")) { - return proxy; - } - - // Perplexity / Poe / Midjourney 全系 - if (shExpMatch(host, "perplexity.ai") || - shExpMatch(host, "*.perplexity.ai") || - shExpMatch(host, "poe.com") || - shExpMatch(host, "*.poe.com") || - shExpMatch(host, "midjourney.com") || - shExpMatch(host, "*.midjourney.com")) { - return proxy; - } - - // Figma / Canva 全系 - if (shExpMatch(host, "figma.com") || - shExpMatch(host, "*.figma.com") || - shExpMatch(host, "canva.com") || - shExpMatch(host, "*.canva.com")) { - return proxy; - } - - // LinkedIn 全系 - if (shExpMatch(host, "linkedin.com") || - shExpMatch(host, "*.linkedin.com") || - shExpMatch(host, "licdn.com") || - shExpMatch(host, "*.licdn.com")) { - return proxy; - } - - // Pinterest 全系 - if (shExpMatch(host, "pinterest.com") || - shExpMatch(host, "*.pinterest.com") || - shExpMatch(host, "pinimg.com") || - shExpMatch(host, "*.pinimg.com")) { - return proxy; - } - - // Tumblr 全系 - if (shExpMatch(host, "tumblr.com") || - shExpMatch(host, "*.tumblr.com")) { - return proxy; - } - - // Snapchat 全系 - if (shExpMatch(host, "snapchat.com") || - shExpMatch(host, "*.snapchat.com") || - shExpMatch(host, "sc-cdn.net") || - shExpMatch(host, "*.sc-cdn.net")) { - return proxy; - } - - // Wikipedia / Wikimedia 全系 - if (shExpMatch(host, "wikipedia.org") || - shExpMatch(host, "*.wikipedia.org") || - shExpMatch(host, "wikimedia.org") || - shExpMatch(host, "*.wikimedia.org")) { - return proxy; - } - - // PayPal 全系 - if (shExpMatch(host, "paypal.com") || - shExpMatch(host, "*.paypal.com") || - shExpMatch(host, "paypalobjects.com") || - shExpMatch(host, "*.paypalobjects.com")) { - return proxy; - } - - // Patreon 全系 - if (shExpMatch(host, "patreon.com") || - shExpMatch(host, "*.patreon.com")) { - return proxy; - } - - // 通用 CDN - if (shExpMatch(host, "cloudfront.net") || - shExpMatch(host, "*.cloudfront.net") || - shExpMatch(host, "fastly.net") || - shExpMatch(host, "*.fastly.net") || - shExpMatch(host, "fastlylb.net") || - shExpMatch(host, "*.fastlylb.net") || - shExpMatch(host, "jsdelivr.net") || - shExpMatch(host, "*.jsdelivr.net") || - shExpMatch(host, "unpkg.com") || - shExpMatch(host, "*.unpkg.com") || - shExpMatch(host, "statically.io") || - shExpMatch(host, "*.statically.io") || - shExpMatch(host, "cloudinary.com") || - shExpMatch(host, "*.cloudinary.com") || - shExpMatch(host, "imgur.com") || - shExpMatch(host, "*.imgur.com")) { - return proxy; - } - - // linux.do - if (shExpMatch(host, "linux.do") || - shExpMatch(host, "*.linux.do")) { - return proxy; - } - - return "DIRECT"; -} diff --git a/server_config/pac/proxy_wg.pac b/server_config/pac/proxy_wg.pac deleted file mode 100644 index 70db7fa..0000000 --- a/server_config/pac/proxy_wg.pac +++ /dev/null @@ -1,648 +0,0 @@ -function FindProxyForURL(url, host) { - // RRPP 规则代理列表 — 仅以下域名走加密隧道 - var proxy = "PROXY 127.0.0.1:1080"; - // Google 全系 + Gemini + CDN - if (shExpMatch(host, "google.com") || - shExpMatch(host, "*.google.com") || - shExpMatch(host, "google.com.hk") || - shExpMatch(host, "*.google.com.hk") || - shExpMatch(host, "google.co.jp") || - shExpMatch(host, "*.google.co.jp") || - shExpMatch(host, "googleapis.com") || - shExpMatch(host, "*.googleapis.com") || - shExpMatch(host, "googleusercontent.com") || - shExpMatch(host, "*.googleusercontent.com") || - shExpMatch(host, "googletagmanager.com") || - shExpMatch(host, "*.googletagmanager.com") || - shExpMatch(host, "google-analytics.com") || - shExpMatch(host, "*.google-analytics.com") || - shExpMatch(host, "gstatic.com") || - shExpMatch(host, "*.gstatic.com") || - shExpMatch(host, "ggpht.com") || - shExpMatch(host, "*.ggpht.com") || - shExpMatch(host, "googlehosted.com") || - shExpMatch(host, "*.googlehosted.com") || - shExpMatch(host, "1e100.net") || - shExpMatch(host, "*.1e100.net") || - shExpMatch(host, "youtube.com") || - shExpMatch(host, "*.youtube.com") || - shExpMatch(host, "ytimg.com") || - shExpMatch(host, "*.ytimg.com") || - shExpMatch(host, "googlevideo.com") || - shExpMatch(host, "*.googlevideo.com") || - shExpMatch(host, "gemini.google.com") || - shExpMatch(host, "*.gemini.google.com") || - shExpMatch(host, "generativeai.googleapis.com") || - shExpMatch(host, "*.generativeai.googleapis.com") || - shExpMatch(host, "aistudio.google.com") || - shExpMatch(host, "*.aistudio.google.com") || - shExpMatch(host, "deepmind.google.com") || - shExpMatch(host, "*.deepmind.google.com") || - shExpMatch(host, "deepmind.com") || - shExpMatch(host, "*.deepmind.com")) { - return proxy; - } - - // OpenAI / ChatGPT 全系 + CDN - if (shExpMatch(host, "openai.com") || - shExpMatch(host, "*.openai.com") || - shExpMatch(host, "chatgpt.com") || - shExpMatch(host, "*.chatgpt.com") || - shExpMatch(host, "oaistatic.com") || - shExpMatch(host, "*.oaistatic.com") || - shExpMatch(host, "oaiusercontent.com") || - shExpMatch(host, "*.oaiusercontent.com") || - shExpMatch(host, "openaiusercontent.com") || - shExpMatch(host, "*.openaiusercontent.com")) { - return proxy; - } - - // Grok / xAI 全系 + CDN - if (shExpMatch(host, "grok.com") || - shExpMatch(host, "*.grok.com") || - shExpMatch(host, "x.com") || - shExpMatch(host, "*.x.com") || - shExpMatch(host, "twitter.com") || - shExpMatch(host, "*.twitter.com") || - shExpMatch(host, "twimg.com") || - shExpMatch(host, "*.twimg.com") || - shExpMatch(host, "x.ai") || - shExpMatch(host, "*.x.ai") || - shExpMatch(host, "xai.com") || - shExpMatch(host, "*.xai.com")) { - return proxy; - } - - // Meta 全系 + CDN - if (shExpMatch(host, "meta.com") || - shExpMatch(host, "*.meta.com") || - shExpMatch(host, "meta.ai") || - shExpMatch(host, "*.meta.ai") || - shExpMatch(host, "facebook.com") || - shExpMatch(host, "*.facebook.com") || - shExpMatch(host, "fb.com") || - shExpMatch(host, "*.fb.com") || - shExpMatch(host, "fbcdn.net") || - shExpMatch(host, "*.fbcdn.net") || - shExpMatch(host, "fbsbx.com") || - shExpMatch(host, "*.fbsbx.com") || - shExpMatch(host, "instagram.com") || - shExpMatch(host, "*.instagram.com") || - shExpMatch(host, "cdninstagram.com") || - shExpMatch(host, "*.cdninstagram.com") || - shExpMatch(host, "threads.net") || - shExpMatch(host, "*.threads.net") || - shExpMatch(host, "whatsapp.com") || - shExpMatch(host, "*.whatsapp.com") || - shExpMatch(host, "whatsapp.net") || - shExpMatch(host, "*.whatsapp.net") || - shExpMatch(host, "messenger.com") || - shExpMatch(host, "*.messenger.com")) { - return proxy; - } - - // Claude / Anthropic 全系 + CDN - if (shExpMatch(host, "anthropic.com") || - shExpMatch(host, "*.anthropic.com") || - shExpMatch(host, "claude.ai") || - shExpMatch(host, "*.claude.ai") || - shExpMatch(host, "claudeusercontent.com") || - shExpMatch(host, "*.claudeusercontent.com")) { - return proxy; - } - - // Dify 全系 - if (shExpMatch(host, "dify.ai") || - shExpMatch(host, "*.dify.ai") || - shExpMatch(host, "udify.app") || - shExpMatch(host, "*.udify.app")) { - return proxy; - } - - // Cloudflare 全系 - if (shExpMatch(host, "cloudflare.com") || - shExpMatch(host, "*.cloudflare.com") || - shExpMatch(host, "cloudflare.net") || - shExpMatch(host, "*.cloudflare.net") || - shExpMatch(host, "cloudflareinsights.com") || - shExpMatch(host, "*.cloudflareinsights.com") || - shExpMatch(host, "cloudflarestatus.com") || - shExpMatch(host, "*.cloudflarestatus.com") || - shExpMatch(host, "cloudflarecdn.com") || - shExpMatch(host, "*.cloudflarecdn.com") || - shExpMatch(host, "cf-cdn.com") || - shExpMatch(host, "*.cf-cdn.com") || - shExpMatch(host, "workers.dev") || - shExpMatch(host, "*.workers.dev") || - shExpMatch(host, "pages.dev") || - shExpMatch(host, "*.pages.dev")) { - return proxy; - } - - // GitHub 全系 - if (shExpMatch(host, "github.com") || - shExpMatch(host, "*.github.com") || - shExpMatch(host, "githubusercontent.com") || - shExpMatch(host, "*.githubusercontent.com") || - shExpMatch(host, "githubassets.com") || - shExpMatch(host, "*.githubassets.com") || - shExpMatch(host, "github.io") || - shExpMatch(host, "*.github.io") || - shExpMatch(host, "githubcopilot.com") || - shExpMatch(host, "*.githubcopilot.com") || - shExpMatch(host, "ghcr.io") || - shExpMatch(host, "*.ghcr.io")) { - return proxy; - } - - // Hugging Face 全系 + CDN - if (shExpMatch(host, "huggingface.co") || - shExpMatch(host, "*.huggingface.co") || - shExpMatch(host, "hf.co") || - shExpMatch(host, "*.hf.co") || - shExpMatch(host, "huggingfacecdn.com") || - shExpMatch(host, "*.huggingfacecdn.com")) { - return proxy; - } - - // Microsoft Copilot 全系 + Bing + Azure CDN - if (shExpMatch(host, "microsoft.com") || - shExpMatch(host, "*.microsoft.com") || - shExpMatch(host, "bing.com") || - shExpMatch(host, "*.bing.com") || - shExpMatch(host, "bingapis.com") || - shExpMatch(host, "*.bingapis.com") || - shExpMatch(host, "bing.net") || - shExpMatch(host, "*.bing.net") || - shExpMatch(host, "live.com") || - shExpMatch(host, "*.live.com") || - shExpMatch(host, "microsoftonline.com") || - shExpMatch(host, "*.microsoftonline.com") || - shExpMatch(host, "microsoft365.com") || - shExpMatch(host, "*.microsoft365.com") || - shExpMatch(host, "office.com") || - shExpMatch(host, "*.office.com") || - shExpMatch(host, "office.net") || - shExpMatch(host, "*.office.net") || - shExpMatch(host, "azure.com") || - shExpMatch(host, "*.azure.com") || - shExpMatch(host, "azureedge.net") || - shExpMatch(host, "*.azureedge.net") || - shExpMatch(host, "azurefd.net") || - shExpMatch(host, "*.azurefd.net") || - shExpMatch(host, "msn.com") || - shExpMatch(host, "*.msn.com") || - shExpMatch(host, "msecnd.net") || - shExpMatch(host, "*.msecnd.net") || - shExpMatch(host, "msftncsi.com") || - shExpMatch(host, "*.msftncsi.com") || - shExpMatch(host, "s-microsoft.com") || - shExpMatch(host, "*.s-microsoft.com")) { - return proxy; - } - - // Steam 全系 + CDN / 社区 / 下载 - if (shExpMatch(host, "steampowered.com") || - shExpMatch(host, "*.steampowered.com") || - shExpMatch(host, "steamcommunity.com") || - shExpMatch(host, "*.steamcommunity.com") || - shExpMatch(host, "steam.com") || - shExpMatch(host, "*.steam.com") || - shExpMatch(host, "steamgames.com") || - shExpMatch(host, "*.steamgames.com") || - shExpMatch(host, "steamusercontent.com") || - shExpMatch(host, "*.steamusercontent.com") || - shExpMatch(host, "steamcontent.com") || - shExpMatch(host, "*.steamcontent.com") || - shExpMatch(host, "steamstatic.com") || - shExpMatch(host, "*.steamstatic.com") || - shExpMatch(host, "steam-chat.com") || - shExpMatch(host, "*.steam-chat.com") || - shExpMatch(host, "steam-api.com") || - shExpMatch(host, "*.steam-api.com") || - shExpMatch(host, "steamserver.net") || - shExpMatch(host, "*.steamserver.net") || - shExpMatch(host, "steamcdn-a.akamaihd.net") || - shExpMatch(host, "*.steamcdn-a.akamaihd.net") || - shExpMatch(host, "steamcommunity-a.akamaihd.net") || - shExpMatch(host, "*.steamcommunity-a.akamaihd.net") || - shExpMatch(host, "steamstore-a.akamaihd.net") || - shExpMatch(host, "*.steamstore-a.akamaihd.net") || - shExpMatch(host, "steamuserimages-a.akamaihd.net") || - shExpMatch(host, "*.steamuserimages-a.akamaihd.net") || - shExpMatch(host, "akamaihd.net") || - shExpMatch(host, "*.akamaihd.net") || - shExpMatch(host, "valvesoftware.com") || - shExpMatch(host, "*.valvesoftware.com")) { - return proxy; - } - - // Discord 全系 - if (shExpMatch(host, "discord.com") || - shExpMatch(host, "*.discord.com") || - shExpMatch(host, "discordapp.com") || - shExpMatch(host, "*.discordapp.com") || - shExpMatch(host, "discord.gg") || - shExpMatch(host, "*.discord.gg") || - shExpMatch(host, "discord.media") || - shExpMatch(host, "*.discord.media") || - shExpMatch(host, "discordapp.net") || - shExpMatch(host, "*.discordapp.net")) { - return proxy; - } - - // Telegram 全系 - if (shExpMatch(host, "telegram.org") || - shExpMatch(host, "*.telegram.org") || - shExpMatch(host, "t.me") || - shExpMatch(host, "*.t.me") || - shExpMatch(host, "telega.one") || - shExpMatch(host, "*.telega.one") || - shExpMatch(host, "telegram-cdn.org") || - shExpMatch(host, "*.telegram-cdn.org")) { - return proxy; - } - - // Reddit 全系 - if (shExpMatch(host, "reddit.com") || - shExpMatch(host, "*.reddit.com") || - shExpMatch(host, "redd.it") || - shExpMatch(host, "*.redd.it") || - shExpMatch(host, "redditmedia.com") || - shExpMatch(host, "*.redditmedia.com") || - shExpMatch(host, "redditstatic.com") || - shExpMatch(host, "*.redditstatic.com")) { - return proxy; - } - - // LINE 全系 - if (shExpMatch(host, "line.me") || - shExpMatch(host, "*.line.me") || - shExpMatch(host, "line-apps.com") || - shExpMatch(host, "*.line-apps.com") || - shExpMatch(host, "line-scdn.net") || - shExpMatch(host, "*.line-scdn.net")) { - return proxy; - } - - // Spotify 全系 - if (shExpMatch(host, "spotify.com") || - shExpMatch(host, "*.spotify.com") || - shExpMatch(host, "spotifycdn.com") || - shExpMatch(host, "*.spotifycdn.com") || - shExpMatch(host, "scdn.co") || - shExpMatch(host, "*.scdn.co")) { - return proxy; - } - - // Netflix 全系 - if (shExpMatch(host, "netflix.com") || - shExpMatch(host, "*.netflix.com") || - shExpMatch(host, "nflxvideo.net") || - shExpMatch(host, "*.nflxvideo.net") || - shExpMatch(host, "nflximg.net") || - shExpMatch(host, "*.nflximg.net") || - shExpMatch(host, "nflxext.com") || - shExpMatch(host, "*.nflxext.com")) { - return proxy; - } - - // Disney+ 全系 - if (shExpMatch(host, "disneyplus.com") || - shExpMatch(host, "*.disneyplus.com") || - shExpMatch(host, "disney-plus.net") || - shExpMatch(host, "*.disney-plus.net") || - shExpMatch(host, "dssott.com") || - shExpMatch(host, "*.dssott.com")) { - return proxy; - } - - // TikTok 全系 - if (shExpMatch(host, "tiktok.com") || - shExpMatch(host, "*.tiktok.com") || - shExpMatch(host, "tiktokcdn.com") || - shExpMatch(host, "*.tiktokcdn.com") || - shExpMatch(host, "tiktokv.com") || - shExpMatch(host, "*.tiktokv.com")) { - return proxy; - } - - // PlayStation 全系 - if (shExpMatch(host, "playstation.com") || - shExpMatch(host, "*.playstation.com") || - shExpMatch(host, "sonyentertainmentnetwork.com") || - shExpMatch(host, "*.sonyentertainmentnetwork.com")) { - return proxy; - } - - // Nintendo 全系 - if (shExpMatch(host, "nintendo.com") || - shExpMatch(host, "*.nintendo.com") || - shExpMatch(host, "nintendowifi.net") || - shExpMatch(host, "*.nintendowifi.net")) { - return proxy; - } - - // Epic Games 全系 - if (shExpMatch(host, "epicgames.com") || - shExpMatch(host, "*.epicgames.com") || - shExpMatch(host, "unrealengine.com") || - shExpMatch(host, "*.unrealengine.com")) { - return proxy; - } - - // DuckDuckGo / Brave Search - if (shExpMatch(host, "duckduckgo.com") || - shExpMatch(host, "*.duckduckgo.com") || - shExpMatch(host, "search.brave.com") || - shExpMatch(host, "*.search.brave.com")) { - return proxy; - } - - // Proton 全系 - if (shExpMatch(host, "proton.me") || - shExpMatch(host, "*.proton.me") || - shExpMatch(host, "protonmail.com") || - shExpMatch(host, "*.protonmail.com") || - shExpMatch(host, "protonvpn.com") || - shExpMatch(host, "*.protonvpn.com")) { - return proxy; - } - - // Stack Overflow / Stack Exchange 全系 - if (shExpMatch(host, "stackoverflow.com") || - shExpMatch(host, "*.stackoverflow.com") || - shExpMatch(host, "stackexchange.com") || - shExpMatch(host, "*.stackexchange.com") || - shExpMatch(host, "superuser.com") || - shExpMatch(host, "*.superuser.com") || - shExpMatch(host, "serverfault.com") || - shExpMatch(host, "*.serverfault.com")) { - return proxy; - } - - // Quora / Medium - if (shExpMatch(host, "quora.com") || - shExpMatch(host, "*.quora.com") || - shExpMatch(host, "medium.com") || - shExpMatch(host, "*.medium.com")) { - return proxy; - } - - // Pixiv 全系 - if (shExpMatch(host, "pixiv.net") || - shExpMatch(host, "*.pixiv.net") || - shExpMatch(host, "pximg.net") || - shExpMatch(host, "*.pximg.net")) { - return proxy; - } - - // ニコニコ 全系 - if (shExpMatch(host, "nicovideo.jp") || - shExpMatch(host, "*.nicovideo.jp") || - shExpMatch(host, "nico.ms") || - shExpMatch(host, "*.nico.ms")) { - return proxy; - } - - // EA / Origin / Apex 全系 - if (shExpMatch(host, "ea.com") || - shExpMatch(host, "*.ea.com") || - shExpMatch(host, "origin.com") || - shExpMatch(host, "*.origin.com") || - shExpMatch(host, "eaassets.com") || - shExpMatch(host, "*.eaassets.com") || - shExpMatch(host, "apexlegends.com") || - shExpMatch(host, "*.apexlegends.com")) { - return proxy; - } - - // Battle.net / Blizzard 全系 - if (shExpMatch(host, "battle.net") || - shExpMatch(host, "*.battle.net") || - shExpMatch(host, "blizzard.com") || - shExpMatch(host, "*.blizzard.com")) { - return proxy; - } - - // Ubisoft 全系 - if (shExpMatch(host, "ubisoft.com") || - shExpMatch(host, "*.ubisoft.com") || - shExpMatch(host, "ubi.com") || - shExpMatch(host, "*.ubi.com")) { - return proxy; - } - - // Riot Games / 英雄联盟 / Valorant 全系 - if (shExpMatch(host, "riotgames.com") || - shExpMatch(host, "*.riotgames.com") || - shExpMatch(host, "leagueoflegends.com") || - shExpMatch(host, "*.leagueoflegends.com") || - shExpMatch(host, "valorant.com") || - shExpMatch(host, "*.valorant.com")) { - return proxy; - } - - // GOG / itch.io 全系 - if (shExpMatch(host, "gog.com") || - shExpMatch(host, "*.gog.com") || - shExpMatch(host, "itch.io") || - shExpMatch(host, "*.itch.io")) { - return proxy; - } - - // Twitch 全系 - if (shExpMatch(host, "twitch.tv") || - shExpMatch(host, "*.twitch.tv") || - shExpMatch(host, "ttvnw.net") || - shExpMatch(host, "*.ttvnw.net") || - shExpMatch(host, "twitchcdn.net") || - shExpMatch(host, "*.twitchcdn.net")) { - return proxy; - } - - // Crunchyroll 全系 - if (shExpMatch(host, "crunchyroll.com") || - shExpMatch(host, "*.crunchyroll.com")) { - return proxy; - } - - // Amazon Prime Video 全系 - if (shExpMatch(host, "primevideo.com") || - shExpMatch(host, "*.primevideo.com")) { - return proxy; - } - - // Apple TV+ 全系 - if (shExpMatch(host, "tv.apple.com") || - shExpMatch(host, "tvplus.apple.com")) { - return proxy; - } - - // HBO Max / Hulu / Paramount+ 全系 - if (shExpMatch(host, "max.com") || - shExpMatch(host, "*.max.com") || - shExpMatch(host, "hulu.com") || - shExpMatch(host, "*.hulu.com") || - shExpMatch(host, "paramountplus.com") || - shExpMatch(host, "*.paramountplus.com")) { - return proxy; - } - - // Slack 全系 - if (shExpMatch(host, "slack.com") || - shExpMatch(host, "*.slack.com") || - shExpMatch(host, "slack-edge.com") || - shExpMatch(host, "*.slack-edge.com") || - shExpMatch(host, "slack-imgs.com") || - shExpMatch(host, "*.slack-imgs.com")) { - return proxy; - } - - // Zoom 全系 - if (shExpMatch(host, "zoom.us") || - shExpMatch(host, "*.zoom.us") || - shExpMatch(host, "zoom.com") || - shExpMatch(host, "*.zoom.com") || - shExpMatch(host, "zoomgov.com") || - shExpMatch(host, "*.zoomgov.com")) { - return proxy; - } - - // Signal 全系 - if (shExpMatch(host, "signal.org") || - shExpMatch(host, "*.signal.org")) { - return proxy; - } - - // Docker 全系 - if (shExpMatch(host, "docker.com") || - shExpMatch(host, "*.docker.com") || - shExpMatch(host, "docker.io") || - shExpMatch(host, "*.docker.io")) { - return proxy; - } - - // GitLab 全系 - if (shExpMatch(host, "gitlab.com") || - shExpMatch(host, "*.gitlab.com") || - shExpMatch(host, "gitlab.io") || - shExpMatch(host, "*.gitlab.io")) { - return proxy; - } - - // Vercel 全系 - if (shExpMatch(host, "vercel.com") || - shExpMatch(host, "*.vercel.com") || - shExpMatch(host, "vercel.app") || - shExpMatch(host, "*.vercel.app")) { - return proxy; - } - - // Netlify 全系 - if (shExpMatch(host, "netlify.com") || - shExpMatch(host, "*.netlify.com") || - shExpMatch(host, "netlify.app") || - shExpMatch(host, "*.netlify.app")) { - return proxy; - } - - // Perplexity / Poe / Midjourney 全系 - if (shExpMatch(host, "perplexity.ai") || - shExpMatch(host, "*.perplexity.ai") || - shExpMatch(host, "poe.com") || - shExpMatch(host, "*.poe.com") || - shExpMatch(host, "midjourney.com") || - shExpMatch(host, "*.midjourney.com")) { - return proxy; - } - - // Figma / Canva 全系 - if (shExpMatch(host, "figma.com") || - shExpMatch(host, "*.figma.com") || - shExpMatch(host, "canva.com") || - shExpMatch(host, "*.canva.com")) { - return proxy; - } - - // LinkedIn 全系 - if (shExpMatch(host, "linkedin.com") || - shExpMatch(host, "*.linkedin.com") || - shExpMatch(host, "licdn.com") || - shExpMatch(host, "*.licdn.com")) { - return proxy; - } - - // Pinterest 全系 - if (shExpMatch(host, "pinterest.com") || - shExpMatch(host, "*.pinterest.com") || - shExpMatch(host, "pinimg.com") || - shExpMatch(host, "*.pinimg.com")) { - return proxy; - } - - // Tumblr 全系 - if (shExpMatch(host, "tumblr.com") || - shExpMatch(host, "*.tumblr.com")) { - return proxy; - } - - // Snapchat 全系 - if (shExpMatch(host, "snapchat.com") || - shExpMatch(host, "*.snapchat.com") || - shExpMatch(host, "sc-cdn.net") || - shExpMatch(host, "*.sc-cdn.net")) { - return proxy; - } - - // Wikipedia / Wikimedia 全系 - if (shExpMatch(host, "wikipedia.org") || - shExpMatch(host, "*.wikipedia.org") || - shExpMatch(host, "wikimedia.org") || - shExpMatch(host, "*.wikimedia.org")) { - return proxy; - } - - // PayPal 全系 - if (shExpMatch(host, "paypal.com") || - shExpMatch(host, "*.paypal.com") || - shExpMatch(host, "paypalobjects.com") || - shExpMatch(host, "*.paypalobjects.com")) { - return proxy; - } - - // Patreon 全系 - if (shExpMatch(host, "patreon.com") || - shExpMatch(host, "*.patreon.com")) { - return proxy; - } - - // 通用 CDN - if (shExpMatch(host, "cloudfront.net") || - shExpMatch(host, "*.cloudfront.net") || - shExpMatch(host, "fastly.net") || - shExpMatch(host, "*.fastly.net") || - shExpMatch(host, "fastlylb.net") || - shExpMatch(host, "*.fastlylb.net") || - shExpMatch(host, "jsdelivr.net") || - shExpMatch(host, "*.jsdelivr.net") || - shExpMatch(host, "unpkg.com") || - shExpMatch(host, "*.unpkg.com") || - shExpMatch(host, "statically.io") || - shExpMatch(host, "*.statically.io") || - shExpMatch(host, "cloudinary.com") || - shExpMatch(host, "*.cloudinary.com") || - shExpMatch(host, "imgur.com") || - shExpMatch(host, "*.imgur.com")) { - return proxy; - } - - // linux.do - if (shExpMatch(host, "linux.do") || - shExpMatch(host, "*.linux.do")) { - return proxy; - } - - return "DIRECT"; -} - diff --git a/server_config/scripts/rrpp-stats b/server_config/scripts/rrpp-stats deleted file mode 100644 index 9f509ff..0000000 --- a/server_config/scripts/rrpp-stats +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# /usr/local/bin/rrpp-stats — returns traffic stats as JSON - -vnstat --json 2>/dev/null || echo '{"error":"vnstat not available"}' diff --git a/server_config/scripts/sync-nginx-local-ips.sh b/server_config/scripts/sync-nginx-local-ips.sh index fab6c98..bbe8f0a 100644 --- a/server_config/scripts/sync-nginx-local-ips.sh +++ b/server_config/scripts/sync-nginx-local-ips.sh @@ -1,13 +1,15 @@ #!/bin/bash # sync-nginx-local-ips.sh # 自动维护 nginx 本机公网 IP 直连规则(/etc/nginx/local_ip_rules.conf): -# 出站公网 IP(ipify)+ iam.aeolianchi.me 的 A 记录 = 本机当前公网 IP 集合。 +# 出站公网 IP(ipify)+ <节点域名> 的 A 记录 = 本机当前公网 IP 集合。 # 公网 IP 变更时重写规则文件并 reload nginx(仅变更时 reload)。 # 由 /etc/cron.d/sync-nginx-local-ips 每 5 分钟调用一次。 +# +# 部署时把 DOMAIN 改为本节点域名。 set -u PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin RULES=/etc/nginx/local_ip_rules.conf -DOMAIN=iam.aeolianchi.me +DOMAIN=<节点域名> ips="" # 出站公网 IP diff --git a/server_config/sing-box/config.json b/server_config/sing-box/config.json index 333232a..0bdc015 100644 --- a/server_config/sing-box/config.json +++ b/server_config/sing-box/config.json @@ -1,6 +1,6 @@ { "log": { - "level": "warn", + "level": "info", "timestamp": true }, "inbounds": [ @@ -8,7 +8,7 @@ "type": "vless", "tag": "vless-reality-in", "listen": "0.0.0.0", - "listen_port": 8443, + "listen_port": "", "users": [ { "uuid": "", @@ -17,11 +17,11 @@ ], "tls": { "enabled": true, - "server_name": "www.apple.com", + "server_name": "<伪装SNI域名>", "reality": { "enabled": true, "handshake": { - "server": "www.apple.com", + "server": "<伪装SNI域名>", "server_port": 443 }, "private_key": "", @@ -30,6 +30,14 @@ ] } } + }, + { + "type": "shadowsocks", + "tag": "ss-in", + "listen": "0.0.0.0", + "listen_port": "", + "method": "aes-256-gcm", + "password": "" } ], "outbounds": [ diff --git a/server_config/systemd/mtg.service b/server_config/systemd/mtg.service new file mode 100644 index 0000000..f3121d6 --- /dev/null +++ b/server_config/systemd/mtg.service @@ -0,0 +1,12 @@ +[Unit] +Description=MTProto proxy (mtg) for Telegram +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/mtg simple-run --prefer-ip=prefer-ipv4 0.0.0.0: +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/server_config/systemd/nginx.service b/server_config/systemd/nginx.service new file mode 100644 index 0000000..9c96190 --- /dev/null +++ b/server_config/systemd/nginx.service @@ -0,0 +1,16 @@ +[Unit] +Description=nginx (RRPP proxy, compiled with ngx_http_proxy_connect_module) +After=network.target + +[Service] +Type=forking +PIDFile=/var/run/nginx.pid +ExecStartPre=/usr/local/nginx/sbin/nginx -t +ExecStart=/usr/local/nginx/sbin/nginx +ExecReload=/usr/local/nginx/sbin/nginx -s reload +ExecStop=/usr/local/nginx/sbin/nginx -s quit +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/server_config/systemd/sing-box.service b/server_config/systemd/sing-box.service new file mode 100644 index 0000000..4da1e76 --- /dev/null +++ b/server_config/systemd/sing-box.service @@ -0,0 +1,12 @@ +[Unit] +Description=sing-box (VLESS+REALITY / Shadowsocks) +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/server_config/traffic-monitor/config.json b/server_config/traffic-monitor/config.json deleted file mode 100644 index 9e944af..0000000 --- a/server_config/traffic-monitor/config.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "vnstat_bin": "vnstat", - "interface": "eth0", - "data_dir": "/home/agent/data", - "alert_state_path": "/home/agent/alert_state.json", - "poll_interval_sec": 60, - "schedule_interval_sec": 30, - "http_host": "127.0.0.1", - "http_port": 18080, - "quota_bytes": 15000000000, - "alert_thresholds_pct": [ - 50, - 80, - 90, - 95, - 99, - 100 - ], - "effective_domain": "iam.aeolianchi.me", - "log_level": "INFO", - "smtp": { - "host": "mail.rrrosmontis.icu", - "port": 465, - "user": "", - "password": "", - "from_name": "Royirin Rosmontis Private Proxy JP 流量监控", - "from_addr": "", - "to_addr": "", - "starttls": false, - "skip_cert_verify": false, - "timeout_sec": 30, - "ssl": true - }, - "current_ip": "", - "billing_cycle_start_day": 1, - "azure_usage_enabled": true, - "azure_usage_fetch_interval_sec": 21600, - "azure_usage_cache_path": "/home/agent/data/azure_usage.json", - "azure_meter_name_match": "Data Transfer Out", - "azure_free_limit_gb": 15 -} diff --git a/server_config/traffic-monitor/traffic-monitor.service b/server_config/traffic-monitor/traffic-monitor.service deleted file mode 100644 index 686be02..0000000 --- a/server_config/traffic-monitor/traffic-monitor.service +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=Royirin Rosmontis Private Proxy JP Traffic Monitor -After=network-online.target nginx.service -Wants=network-online.target - -[Service] -Type=simple -WorkingDirectory=/home/agent -ExecStart=/usr/bin/python3 /home/agent/traffic_monitor.py --config /home/agent/config.json -Restart=always -RestartSec=5 -User=root - -[Install] -WantedBy=multi-user.target diff --git a/server_config/traffic-monitor/traffic_monitor.py b/server_config/traffic-monitor/traffic_monitor.py deleted file mode 100644 index d832ab4..0000000 --- a/server_config/traffic-monitor/traffic_monitor.py +++ /dev/null @@ -1,666 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Royirin Rosmontis Private Proxy JP 流量监控(Azure 门户口径版) -- 主计量口径:Azure 门户免费服务页 "Data Transfer Out (GB)"(Standard Data Transfer Out - Free,MGN 路由) - —— 通过 VM 托管身份调用 Consumption Usage Details API 拉取,按自然月聚合,缓存至 data/azure_usage.json -- 参考口径:vnstat -i eth0(实时原始出站,仅作参考展示) -- 数据源(参考):vnstat -i --json m/d(eth0 为 bond 主接口,覆盖 nginx 443 与 sing-box 8443 全部流量) -- 按 年/月 聚合成 JSON 存入 data/YYYY/YYYY-MM.json(弃用 SQLite) -- HTTP 服务: GET /vpn/status/traffic 返回当月用量 JSON(Azure 口径为主) -- 阈值告警: 达到配额 50/80/90/95/99/100% 时发邮件(每档每月只发一次,跳档补发;基于 Azure 口径) -- 月报: 每月 1 日 00:05 起发送上个月报告(失败重试;基于 Azure 口径) -""" -import argparse -import json -import logging -import os -import socket -import smtplib -import ssl -import subprocess -import sys -import threading -import time -import urllib.parse -import urllib.request -from datetime import date, datetime, timedelta, timezone -from email.message import EmailMessage -from email.utils import formataddr -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer - -log = logging.getLogger('traffic_monitor') - -GB = 1_000_000_000 # Azure 数据转移按十进制 GB 计费 - - -def utcnow(): - return datetime.now(timezone.utc) - - -def load_json(path, default): - try: - with open(path, 'r', encoding='utf-8') as f: - return json.load(f) - except FileNotFoundError: - return default - except (json.JSONDecodeError, OSError) as e: - log.warning('read %s failed: %s; using default', path, e) - return default - - -def save_json(path, obj): - tmp = path + '.tmp' - with open(tmp, 'w', encoding='utf-8') as f: - json.dump(obj, f, ensure_ascii=False, indent=2) - os.replace(tmp, path) - - -def gb_str(n_bytes): - return f'{n_bytes / GB:.2f} GB' - - -def month_struct(year, month): - return { - 'year': int(year), - 'month': int(month), - 'rx_bytes': 0, - 'tx_bytes': 0, - 'days': [], - 'updated_at': utcnow().isoformat(), - } - - -class Monitor: - def __init__(self, config): - self.config = config - self.lock = threading.RLock() - self.months = {} - self.alert_state = None - - os.makedirs(self.config['data_dir'], exist_ok=True) - self._load_all_months() - self._load_alert_state() - # Azure 计费用量(Usage Details API,VM 托管身份鉴权) - self.azure_usage = None - self._azure_last_fetch = 0.0 - cache_path = self.config.get('azure_usage_cache_path') - if cache_path and os.path.exists(cache_path): - self.azure_usage = load_json(cache_path, None) - log.info('azure usage cache loaded: %s', self.azure_usage) - - # ---------- 基础 ---------- - @property - def _config_path(self): - return os.environ.get('TRAFFIC_CONFIG', '/home/agent/config.json') - - def _load_all_months(self): - root = self.config['data_dir'] - if not os.path.isdir(root): - return - for year_dir in os.listdir(root): - ypath = os.path.join(root, year_dir) - if not os.path.isdir(ypath): - continue - for fn in os.listdir(ypath): - if not fn.endswith('.json'): - continue - try: - with open(os.path.join(ypath, fn), 'r', encoding='utf-8') as f: - m = json.load(f) - mk = f"{m['year']:04d}-{m['month']:02d}" - self.months[mk] = m - except Exception as e: - log.warning('load %s/%s failed: %s', ypath, fn, e) - - def _load_alert_state(self): - self.alert_state = load_json( - self.config['alert_state_path'], - {'alert_thresholds_sent': {}, 'monthly_reports_sent': []}) - self.alert_state.setdefault('alert_thresholds_sent', {}) - self.alert_state.setdefault('monthly_reports_sent', []) - - def _save_alert_state(self): - save_json(self.config['alert_state_path'], self.alert_state) - - def _month_path(self, year, month): - return os.path.join(self.config['data_dir'], str(year), f'{year:04d}-{month:02d}.json') - - def _persist_month(self, mk): - m = self.months.get(mk) - if m is None: - return - path = self._month_path(m['year'], m['month']) - os.makedirs(os.path.dirname(path), exist_ok=True) - save_json(path, m) - - # ---------- vnstat 读取 ---------- - def _vnstat(self, what): - bin_path = self.config.get('vnstat_bin', 'vnstat') - iface = self.config.get('interface', 'eth0') - cmd = [bin_path, '-i', iface, '--json', what] - try: - out = subprocess.run(cmd, capture_output=True, timeout=20, check=True) - return json.loads(out.stdout.decode('utf-8')) - except FileNotFoundError: - log.error('vnstat binary not found: %s', bin_path) - return None - except subprocess.TimeoutExpired: - log.error('vnstat --json %s timed out', what) - return None - except (subprocess.CalledProcessError, json.JSONDecodeError, OSError) as e: - log.error('vnstat --json %s failed: %s', what, e) - return None - - def poll_vnstat(self): - """拉取 vnstat 月/日数据,更新并落盘。返回更新的月数。""" - monthly = self._vnstat('m') - daily = self._vnstat('d') - touched = set() - - with self.lock: - if monthly: - try: - for itf in monthly.get('interfaces', []): - for item in itf.get('traffic', {}).get('month', []): - d = item.get('date', {}) - mk = f"{int(d['year']):04d}-{int(d['month']):02d}" - m = self.months.get(mk) - if m is None: - m = month_struct(d['year'], d['month']) - self.months[mk] = m - m['rx_bytes'] = int(item.get('rx', 0)) - m['tx_bytes'] = int(item.get('tx', 0)) - m['updated_at'] = utcnow().isoformat() - touched.add(mk) - except Exception as e: - log.exception('parse vnstat monthly failed: %s', e) - - if daily: - try: - by_month = {} - for itf in daily.get('interfaces', []): - for item in itf.get('traffic', {}).get('day', []): - d = item.get('date', {}) - mk = f"{int(d['year']):04d}-{int(d['month']):02d}" - by_month.setdefault(mk, []).append({ - 'day': int(d['day']), - 'rx_bytes': int(item.get('rx', 0)), - 'tx_bytes': int(item.get('tx', 0)), - }) - for mk, days in by_month.items(): - m = self.months.get(mk) - if m is None: - y, mon = mk.split('-') - m = month_struct(int(y), int(mon)) - self.months[mk] = m - days.sort(key=lambda x: x['day']) - m['days'] = days - m['updated_at'] = utcnow().isoformat() - touched.add(mk) - except Exception as e: - log.exception('parse vnstat daily failed: %s', e) - - for mk in touched: - self._persist_month(mk) - return len(touched) - - # ---------- 用量(按 Azure 计费周期) ---------- - def current_month_key(self): - return utcnow().strftime('%Y-%m') - - def cycle_start(self, now): - """返回当前计费周期的起始 datetime(每月 billing_cycle_start_day 00:00 UTC)。""" - day = int(self.config.get('billing_cycle_start_day', 1)) - day = max(1, min(28, day)) - if now.day >= day: - return now.replace(day=day, hour=0, minute=0, second=0, microsecond=0) - prev = (now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) - - timedelta(days=1)) - return prev.replace(day=day, hour=0, minute=0, second=0, microsecond=0) - - def cycle_end(self, start): - """周期结束 = 下个月同一天的 00:00(不含)。""" - nxt = (start.replace(day=1) + timedelta(days=32)).replace(day=1) - return nxt.replace(day=start.day, hour=0, minute=0, second=0, microsecond=0) - - def _day_totals(self): - """返回 [{date, rx, tx}],date 为 UTC date。""" - out = [] - for m in self.months.values(): - for d in m.get('days', []): - out.append((date(m['year'], m['month'], d['day']), - int(d.get('rx_bytes', 0)), int(d.get('tx_bytes', 0)))) - return out - - def cycle_usage(self): - """返回当前计费周期 {rx_bytes, tx_bytes, start, end}。""" - now = utcnow() - start = self.cycle_start(now) - end = self.cycle_end(start) - rx = tx = 0 - with self.lock: - for dt, r, t in self._day_totals(): - if start.date() <= dt <= now.date(): - rx += r - tx += t - return {'rx_bytes': rx, 'tx_bytes': tx, 'start': start, 'end': end} - - def tx_bytes_for(self, mk): - with self.lock: - m = self.months.get(mk, {}) - return int(m.get('tx_bytes', 0)) - - def current_status(self): - """当前状态:以 Azure 门户口径(Standard Data Transfer Out - Free)为主计量。""" - now = utcnow() - quota_gb = float(self.config.get('azure_free_limit_gb', 15)) - cu = self.cycle_usage() - az = self.azure_usage or {} - used_gb = az.get('portal_data_transfer_out_gb') - used_pct = round(used_gb / quota_gb * 100, 4) if (used_gb is not None and quota_gb) else None - with self.lock: - cal_mk = now.strftime('%Y-%m') - m = self.months.get(cal_mk) - cal_tx = int(m.get('tx_bytes', 0)) if m else 0 - cal_rx = int(m.get('rx_bytes', 0)) if m else 0 - vnstat_updated = (m or {}).get('updated_at') - return { - 'year': now.year, - 'month': now.month, - 'meter': 'azure_portal', - 'used_gb': used_gb, - 'quota_gb': quota_gb, - 'quota_used_pct': used_pct, - 'billing_cycle_start_day': int(self.config.get('billing_cycle_start_day', 1)), - 'cycle_start': cu['start'].strftime('%Y-%m-%d'), - 'cycle_end': cu['end'].strftime('%Y-%m-%d'), - 'azure_portal_data_transfer_out_gb': az.get('portal_data_transfer_out_gb'), - 'azure_bandwidth_out_gb': az.get('bandwidth_out_gb'), - 'azure_usage_cost_cny': az.get('cost_cny'), - 'azure_usage_month': az.get('month'), - 'azure_usage_updated_at': az.get('updated_at'), - 'vnstat_calendar_month_tx_bytes': cal_tx, - 'vnstat_calendar_month_rx_bytes': cal_rx, - 'vnstat_updated_at': vnstat_updated, - 'updated_at': now.isoformat(), - } - - def _vnstat_calendar_tx_gb(self): - """参考值:vnstat 自然月出站(GB,十进制)。""" - now = utcnow() - with self.lock: - m = self.months.get(now.strftime('%Y-%m'), {}) - return int(m.get('tx_bytes', 0)) / GB - - # ---------- Azure 计费用量(Usage Details API) ---------- - def _azure_token(self): - """通过 VM 托管身份从 IMDS 获取 management.azure.com 的访问令牌。""" - url = ('http://169.254.169.254/metadata/identity/oauth2/token' - '?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F') - req = urllib.request.Request(url, headers={'Metadata': 'true'}) - with urllib.request.urlopen(req, timeout=15) as resp: - return json.loads(resp.read().decode('utf-8'))['access_token'] - - def _azure_subscription_id(self): - url = 'http://169.254.169.254/metadata/instance/compute?api-version=2021-02-01' - req = urllib.request.Request(url, headers={'Metadata': 'true'}) - with urllib.request.urlopen(req, timeout=15) as resp: - return json.loads(resp.read().decode('utf-8')).get('subscriptionId') - - def fetch_azure_usage(self, month_offset=0): - """拉取指定月份 usageDetails,聚合与门户免费服务页同源的 Data Transfer Out 用量。 - month_offset: 0=当月(写入缓存),-1=上月(用于月报,不覆盖缓存)。""" - sub = self._azure_subscription_id() - if not sub: - raise RuntimeError('cannot read subscriptionId from IMDS') - token = self._azure_token() - now = utcnow() - month_start = (now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) - + timedelta(days=month_offset * 32)).replace(day=1) - nxt = (month_start.replace(day=1) + timedelta(days=32)).replace(day=1) - flt = ("properties/usageStart ge '{0}T00:00:00Z' and " - "properties/usageStart lt '{1}T00:00:00Z'").format( - month_start.strftime('%Y-%m-%d'), nxt.strftime('%Y-%m-%d')) - base = ('https://management.azure.com/subscriptions/{sub}/providers/' - 'Microsoft.Consumption/usageDetails?api-version=2023-11-01' - '&metric=actualcost&$filter={f}') - url = base.format(sub=sub, f=urllib.parse.quote(flt, safe='')) - portal_gb = 0.0 - bw_out_gb = 0.0 - cost_cny = 0.0 - rows = 0 - pages = 0 - while url: - req = urllib.request.Request(url, headers={'Authorization': 'Bearer ' + token}) - with urllib.request.urlopen(req, timeout=30) as resp: - d = json.loads(resp.read().decode('utf-8')) - for item in d.get('value', []): - p = item.get('properties', {}) or {} - qty = float(p.get('quantity') or 0) - rows += 1 - name = str(p.get('meterName') or '') - subcat = str(p.get('meterSubCategory') or '') - if str(p.get('meterCategory') or '') == 'Bandwidth' and 'Data Transfer Out' in name: - bw_out_gb += qty - # 门户免费服务页 "Data Transfer Out (GB)" = MGN 路由的出站免费计费表 - if 'MGN' in subcat or '- Free' in name: - portal_gb += qty - cost_cny += float(p.get('costInBillingCurrency') or 0) - pages += 1 - url = d.get('nextLink') - if pages > 30: - log.warning('azure usage pagination limit reached') - break - result = { - 'month': month_start.strftime('%Y-%m'), - 'portal_data_transfer_out_gb': round(portal_gb, 4), - 'bandwidth_out_gb': round(bw_out_gb, 4), - 'cost_cny': round(cost_cny, 4), - 'rows': rows, - 'updated_at': utcnow().isoformat(), - } - self.azure_usage = result - if month_offset == 0: - path = self.config.get('azure_usage_cache_path') - if path: - save_json(path, result) - log.info('azure usage fetched (offset=%d): %s', month_offset, result) - return result - - def check_azure_usage(self): - if not self.config.get('azure_usage_enabled', False): - return - interval = float(self.config.get('azure_usage_fetch_interval_sec', 21600)) - now = time.time() - if now - self._azure_last_fetch < interval: - return - self._azure_last_fetch = now - try: - self.fetch_azure_usage() - except Exception as e: - log.warning('azure usage fetch failed: %s', e) - - # ---------- IP / 邮件 ---------- - def get_current_ip(self): - configured = self.config.get('current_ip') - if configured: - return str(configured) - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - s.connect(('8.8.8.8', 80)) - return s.getsockname()[0] - finally: - s.close() - except OSError: - return '0.0.0.0' - - def send_email(self, subject, body): - cfg = self.config['smtp'] - msg = EmailMessage() - msg['Subject'] = subject - msg['From'] = formataddr((cfg.get('from_name', ''), cfg.get('from_addr'))) - msg['To'] = cfg.get('to_addr') - msg.set_content(body) - - context = None - if cfg.get('skip_cert_verify'): - context = ssl._create_unverified_context() - host = cfg['host'] - port = int(cfg.get('port', 465)) - timeout = int(cfg.get('timeout_sec', 30)) - if cfg.get('ssl', False): - smtp_cls = smtplib.SMTP_SSL - kwargs = {'context': context} - else: - smtp_cls = smtplib.SMTP - kwargs = {} - with smtp_cls(host, port, timeout=timeout, **kwargs) as smtp: - if not cfg.get('ssl', False): - smtp.ehlo() - if cfg.get('starttls', True): - smtp.starttls(context=context) - smtp.ehlo() - smtp.login(cfg['user'], cfg['password']) - smtp.send_message(msg) - log.info('email sent: %s', subject) - - def build_alert_mail(self, mk, used_gb, quota_gb, pct, az): - ip = self.get_current_ip() - month = (az or {}).get('month', mk.replace('azure:', '')) - az_updated = (az or {}).get('updated_at', '') - vnstat_tx = self._vnstat_calendar_tx_gb() - body = ( - 'Royirin Rosmontis Private Proxy JP 流量监控\n' - '==========\n' - '计量口径:Azure 门户(Standard Data Transfer Out - Free)\n' - f'统计月份:{month}(自然月)\n' - f'当前用量:{used_gb:.2f} GB / {quota_gb:.0f} GB({pct:.2f}%)\n' - f'AZURE 数据更新:{az_updated}\n' - f'参考(vnstat 自然月出站):{vnstat_tx:.2f} GB\n' - f'当前 IP:{ip}\n' - f'生效域名:{self.config.get("effective_domain", "")}\n' - '==========\n' - f'已达阈值 {pct:.2f}% (Azure 门户口径),请注意使用情况' - ) - subject = f'[流量告警] {month} 已用 {pct:.2f}% ({used_gb:.2f}/{quota_gb:.0f} GB)' - return subject, body - - def build_monthly_mail(self, month, used_gb, quota_gb, pct, az): - ip = self.get_current_ip() - over = '是' if used_gb > quota_gb else '否' - az_updated = (az or {}).get('updated_at', '') - vnstat_tx = self._vnstat_calendar_tx_gb() - body = ( - 'Royirin Rosmontis Private Proxy JP 流量监控\n' - '月度报告\n' - '==========\n' - '计量口径:Azure 门户(Standard Data Transfer Out - Free)\n' - f'报告月份:{month}(自然月)\n' - f'当月用量:{used_gb:.2f} GB / {quota_gb:.0f} GB({pct:.2f}%)\n' - f'AZURE 数据更新:{az_updated}\n' - f'参考(vnstat 自然月出站):{vnstat_tx:.2f} GB\n' - f'当前 IP:{ip}\n' - f'生效域名:{self.config.get("effective_domain", "")}\n' - '==========\n' - f'上月使用 {over} 超限, 使用量为限额的 {pct:.2f}%' - ) - subject = f'[流量月报] {month} 用量 {used_gb:.2f} GB ({pct:.2f}%)' - return subject, body - - # ---------- 定时任务 ---------- - def check_alerts(self): - """阈值告警:以 Azure 门户口径为准(无 Azure 数据时跳过,避免误报)。""" - quota_gb = float(self.config.get('azure_free_limit_gb', 15)) - if quota_gb <= 0: - return - az = self.azure_usage or {} - used_gb = az.get('portal_data_transfer_out_gb') - if used_gb is None: - log.warning('alerts skipped: no azure usage data yet') - return - mk = 'azure:' + str(az.get('month') or utcnow().strftime('%Y-%m')) - pct = used_gb / quota_gb * 100 - sent = self.alert_state['alert_thresholds_sent'].setdefault(mk, []) - if not sent: - # 新周期/口径切换:已越过的档位直接标记,避免切换瞬间补发一堆邮件 - for t in sorted(self.config.get('alert_thresholds_pct', [])): - if pct >= t: - sent.append(t) - self._save_alert_state() - log.info('seeded thresholds for %s at %.2f%%: %s', mk, pct, sent) - for t in sorted(self.config.get('alert_thresholds_pct', [])): - if pct >= t and t not in sent: - subject, body = self.build_alert_mail(mk, used_gb, quota_gb, pct, az) - try: - self.send_email(subject, body) - sent.append(t) - self._save_alert_state() - log.info('threshold %s%% alert sent for %s (used %.2f%%)', t, mk, pct) - except Exception as e: - log.error('send threshold %s%% alert failed for %s: %s; will retry', - t, mk, e) - break - - def check_monthly_report(self): - """月度报告:以 Azure 门户口径为准(上月数据,当月 1 号后发送)。""" - now = utcnow() - cur_start = self.cycle_start(now) - prev_start = (cur_start.replace(day=1) - timedelta(days=1)).replace(day=1) - prev_mk = 'azure:' + prev_start.strftime('%Y-%m') - if prev_mk in self.alert_state['monthly_reports_sent']: - return - # 口径切换迁移:旧键(cycle:)已发过则直接标记,避免重复发信 - old_mk = 'cycle:' + prev_start.strftime('%Y-%m') - if old_mk in self.alert_state['monthly_reports_sent']: - self.alert_state['monthly_reports_sent'].append(prev_mk) - self._save_alert_state() - log.info('monthly report key migrated: %s already sent as %s', prev_mk, old_mk) - return - try: - az_prev = self.fetch_azure_usage(month_offset=-1) - except Exception as e: - log.error('fetch prev month azure usage failed: %s; will retry', e) - return - quota_gb = float(self.config.get('azure_free_limit_gb', 15)) - used_gb = az_prev.get('portal_data_transfer_out_gb') or 0.0 - pct = used_gb / quota_gb * 100 if quota_gb else 0.0 - subject, body = self.build_monthly_mail( - az_prev.get('month', prev_start.strftime('%Y-%m')), used_gb, quota_gb, pct, az_prev) - try: - self.send_email(subject, body) - self.alert_state['monthly_reports_sent'].append(prev_mk) - self._save_alert_state() - log.info('monthly report sent for %s (used %.2f GB, %.2f%%)', - prev_mk, used_gb, pct) - except Exception as e: - log.error('send monthly report for %s failed: %s; will retry', prev_mk, e) - - -def build_http_handler(monitor): - class Handler(BaseHTTPRequestHandler): - def do_GET(self): - if self.path == '/vpn/status/traffic': - try: - payload = json.dumps(monitor.current_status(), ensure_ascii=False) - body = payload.encode('utf-8') - self.send_response(200) - self.send_header('Content-Type', 'application/json; charset=utf-8') - self.send_header('Content-Length', str(len(body))) - self.end_headers() - self.wfile.write(body) - except Exception as e: - log.exception('status endpoint error') - body = b'{"error":"internal"}' - self.send_response(500) - self.send_header('Content-Type', 'application/json') - self.send_header('Content-Length', str(len(body))) - self.end_headers() - self.wfile.write(body) - elif self.path in ('/', '/healthz'): - body = b'ok\n' - self.send_response(200) - self.send_header('Content-Type', 'text/plain') - self.send_header('Content-Length', str(len(body))) - self.end_headers() - self.wfile.write(body) - else: - body = b'{"error":"not found"}\n' - self.send_response(404) - self.send_header('Content-Type', 'application/json') - self.send_header('Content-Length', str(len(body))) - self.end_headers() - self.wfile.write(body) - - def log_message(self, fmt, *args): - log.debug('http: ' + fmt, *args) - - return Handler - - -def run_service(config): - monitor = Monitor(config) - - def poll_loop(): - while True: - try: - n = monitor.poll_vnstat() - log.debug('vnstat poll updated %d month(s)', n) - except Exception: - log.exception('poll loop error') - time.sleep(int(config.get('poll_interval_sec', 60))) - - def schedule_loop(): - while True: - try: - monitor.check_azure_usage() # 先刷新 Azure 口径数据,再评估告警 - monitor.check_alerts() - monitor.check_monthly_report() - except Exception: - log.exception('schedule loop error') - time.sleep(int(config.get('schedule_interval_sec', 30))) - - threading.Thread(target=poll_loop, daemon=True, name='vnstat-poll').start() - threading.Thread(target=schedule_loop, daemon=True, name='scheduler').start() - - host = config.get('http_host', '127.0.0.1') - port = int(config.get('http_port', 18080)) - server = ThreadingHTTPServer((host, port), build_http_handler(monitor)) - log.info('HTTP server listening on http://%s:%d/vpn/status/traffic', host, port) - server.serve_forever() - - -def main(): - ap = argparse.ArgumentParser(description='vnstat traffic monitor') - ap.add_argument('--config', default=os.environ.get('TRAFFIC_CONFIG', '/home/agent/config.json')) - ap.add_argument('--once', action='store_true', help='拉取一次 vnstat 后退出') - ap.add_argument('--status', action='store_true', help='打印当前状态 JSON 后退出') - ap.add_argument('--test-email', action='store_true', help='发送一封测试邮件后退出') - args = ap.parse_args() - - os.environ['TRAFFIC_CONFIG'] = args.config - with open(args.config, 'r', encoding='utf-8') as f: - config = json.load(f) - - logging.basicConfig( - level=getattr(logging, config.get('log_level', 'INFO').upper(), logging.INFO), - format='%(asctime)s %(levelname)s %(name)s: %(message)s') - - if args.once: - m = Monitor(config) - n = m.poll_vnstat() - print(f'updated {n} month(s)') - return 0 - if args.status: - m = Monitor(config) - m.poll_vnstat() - print(json.dumps(m.current_status(), ensure_ascii=False, indent=2)) - return 0 - if args.test_email: - m = Monitor(config) - m.poll_vnstat() - quota_gb = float(config.get('azure_free_limit_gb', 15)) - az = m.azure_usage or {} - used_gb = az.get('portal_data_transfer_out_gb') - if used_gb is None: - try: - m.fetch_azure_usage() - az = m.azure_usage or {} - used_gb = az.get('portal_data_transfer_out_gb') - except Exception as e: - print('azure usage fetch failed:', e) - mk = 'azure:' + str(az.get('month') or utcnow().strftime('%Y-%m')) - pct = used_gb / quota_gb * 100 if (used_gb is not None and quota_gb) else 0.0 - subject, body = m.build_alert_mail(mk, used_gb or 0.0, quota_gb, pct, az) - print('sending test email:') - print('Subject:', subject) - print(body) - m.send_email(subject, body) - print('sent') - return 0 - run_service(config) - - -if __name__ == '__main__': - sys.exit(main())