server_config: 重写为完整部署规范(BBR/nginx CONNECT/sing-box/mtg)

- 新增完整 README.md 部署规范:架构、端口规划、BBR 跨境优化、
  nginx 源码编译+CONNECT 隧道、sing-box(VLESS+REALITY/SS)、mtg(MTProto)、
  本机 IP 自动识别、systemd、启动顺序与验证清单
- nginx/nginx.conf:占位符版,proxy_connect_allow all + 回环保护
- sing-box/config.json:占位符版,VLESS+REALITY + SS
- systemd/:nginx/sing-box/mtg 服务文件
- scripts/sync-nginx-local-ips.sh:去除真实域名,占位符版
- fake-site/index.html:伪装页(清除真实品牌域名)
- 移除过时:traffic-monitor/、pac/、conf.d 具体站点配置
- 所有敏感值(IP/域名/端口/密钥)均以 <占位符> 表示
This commit is contained in:
RRRosmontis
2026-09-10 00:22:03 +08:00
parent 17cdb276ea
commit 20d2532e8f
18 changed files with 550 additions and 2156 deletions
+379 -19
View File
@@ -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/<nginx端口>) ──┬─► CONNECT 隧道(任意端口, 全端口放行)
│ └─► 伪装页面(直接访问/IP访问)
│
├─► sing-box(<vless端口>) ──► VLESS+REALITY(vision)
├─► sing-box(<ss端口>) ──► Shadowsocks(aes-256-gcm)
└─► mtg(<mtproto端口>) ──► 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端口>` | nginx | HTTPS 站点 + CONNECT 隧道(示例 `1205`) |
| `<vless端口>` | sing-box | VLESS+REALITY(示例 `1206`) |
| `<ss端口>` | sing-box | Shadowsocks(示例 `1207`) |
| `<mtproto端口>` | mtg | MTProto(示例 `1210`) |
> 节点 A 用 `80xx` 区段、节点 B 用 `120xx` 区段皆可,**只要同节点内不冲突、且运营商/防火墙放行**。
> 防火墙放行:`iptables -A INPUT -p tcp -m multiport --dports <nginx端口>,<vless端口>,<ss端口>,<mtproto端口> -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 块共用 `<nginx端口>`**:
- 主块 `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:<mtproto端口> <上面生成的secret>
```
> `--prefer-ip=prefer-ipv4`:强制 IPv4 出站,避免容器/双栈环境下 IPv6 不可达导致回退失败。
### 7.4 Telegram 客户端参数
- 服务器:`<节点域名>`
- 端口:`<mtproto端口>`
- 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(('<节点域名>',<mtproto端口>)) if False else socket.create_connection(('<节点域名>',<mtproto端口>),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://<节点域名>:<nginx端口> \
--proxy-header "Authorization: Basic $B64" \
-o /dev/null -w '%{http_code}\n' http://example.com/ # => 200
# 伪装页(直连 IP/域名,无认证)
curl -sk https://<节点域名>:<nginx端口>/ -o /dev/null -w '%{http_code}\n' # => 200
```
---
## 11. 目录结构与文件索引
``` ```
server_config/ server_config/
├── README.md ← 本文档
├── nginx/ ├── nginx/
│ ├── nginx.conf │ ├── nginx.conf ← 完整 nginx 配置(占位符版)
│ ├── conf.d/ │ ├── local_ip_rules.conf.example ← 本机 IP 规则文件示例(cron 自动生成,勿手改)
│ │ ├── api.aeolianchi.me.conf │ └── conf.d/ ← 预留:额外站点配置
│ │ └── autodiscover.aeolianchi.me.conf
│ └── local_ip_rules.conf.example
├── sing-box/ ├── sing-box/
│ └── config.json │ └── config.json ← sing-box 配置(VLESS+REALITY + SS,占位符版)
├── traffic-monitor/ ├── systemd/
│ ├── config.json │ ├── nginx.service
│ ├── traffic_monitor.py │ ├── sing-box.service
│ └── traffic-monitor.service │ └── mtg.service
├── scripts/ ├── scripts/
│ ├── sync-nginx-local-ips.sh │ └── sync-nginx-local-ips.sh ← 本机公网 IP 规则自动维护
│ └── rrpp-stats ├── cron/
├── pac/ │ └── sync-nginx-local-ips ← cron 定义(每 5 分钟)
│ ├── proxy.pac └── fake-site/
│ └── proxy_wg.pac └── index.html ← 伪装站点页面(所有节点共用同一份)
└── cron/
├── crontab.txt
└── sync-nginx-local-ips
``` ```
> 旧的 `traffic-monitor/`、`pac/`、`conf.d/*.conf` 已下线或迁移,不再维护,已从本目录移除。
-2
View File
@@ -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
File diff suppressed because one or more lines are too long
@@ -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;
}
}
@@ -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;
}
@@ -1,4 +1,5 @@
# nginx 本机公网 IP 直连规则(自动生成,勿手改;由 sync-nginx-local-ips.sh 维护) # nginx 本机公网 IP 直连规则(自动生成,勿手改;由 sync-nginx-local-ips.sh 维护)
# 公网 IP 变更后本文件会被重写并 reload nginx # 公网 IP 变更后本文件会被重写并 reload nginx
if ($host = "<PUBLIC_IP>") { rewrite ^(.*)$ /__local$1 last; } # 每条规则对应一个本机公网 IP(出站 IP + 节点域名 A 记录)
if ($host = "<PUBLIC_IP>") { rewrite ^(.*)$ /__local$1 last; } if ($host = "<PUBLIC_IP_1>") { rewrite ^(.*)$ /__local$1 last; }
if ($host = "<PUBLIC_IP_2>") { rewrite ^(.*)$ /__local$1 last; }
+12 -43
View File
@@ -1,4 +1,3 @@
load_module /usr/lib/nginx/modules/ngx_http_proxy_connect_module.so;
worker_processes auto; worker_processes auto;
worker_rlimit_nofile 65535; worker_rlimit_nofile 65535;
events { events {
@@ -25,45 +24,15 @@ http {
keepalive_timeout 65; keepalive_timeout 65;
keepalive_requests 100; keepalive_requests 100;
# 80 端口:iam.aeolianchi.me 强制跳转到 HTTPS(保留 ACME 验证路径) # <nginx端口>:主域名 HTTPS 站点 + 认证 CONNECT 代理
server { server {
listen 80; listen <nginx端口> ssl;
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;
http2 on; http2 on;
server_name XXX; server_name <节点域名>;
root /var/www/html; root /var/www/html;
ssl_certificate /etc/nginx/ssl/XXX/fullchain.pem; ssl_certificate /etc/nginx/ssl/<节点域名>/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/XXX/privkey.pem; ssl_certificate_key /etc/nginx/ssl/<节点域名>/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5; ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
@@ -74,14 +43,14 @@ http {
auth_basic_user_file /etc/nginx/.htpasswd; auth_basic_user_file /etc/nginx/.htpasswd;
proxy_connect; proxy_connect;
proxy_connect_allow 443 563; proxy_connect_allow all;
proxy_connect_connect_timeout 60s; proxy_connect_connect_timeout 60s;
proxy_connect_read_timeout 120s; proxy_connect_read_timeout 120s;
proxy_connect_send_timeout 120s; proxy_connect_send_timeout 120s;
location / { location / {
auth_basic off; auth_basic off;
if ($host != "iam.aeolianchi.me") { if ($host != "<节点域名>") {
rewrite ^ /__proxy last; rewrite ^ /__proxy last;
} }
index index.html; index index.html;
@@ -104,14 +73,14 @@ http {
} }
} }
# 443 端口 fallback:其他 SNI 也可用 CONNECT 代理;普通 GET 只返回伪装页 # <nginx端口> fallback:其他 SNI 也可用 CONNECT 代理;普通 GET 只返回伪装页
server { server {
listen 443 ssl default_server; listen <nginx端口> ssl default_server;
http2 on; http2 on;
server_name _; server_name _;
ssl_certificate /etc/nginx/ssl/iam.aeolianchi.me/fullchain.pem; ssl_certificate /etc/nginx/ssl/<节点域名>/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/iam.aeolianchi.me/privkey.pem; ssl_certificate_key /etc/nginx/ssl/<节点域名>/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5; ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
@@ -122,7 +91,7 @@ http {
auth_basic_user_file /etc/nginx/.htpasswd; auth_basic_user_file /etc/nginx/.htpasswd;
proxy_connect; proxy_connect;
proxy_connect_allow 443 563; proxy_connect_allow all;
proxy_connect_connect_timeout 60s; proxy_connect_connect_timeout 60s;
proxy_connect_read_timeout 120s; proxy_connect_read_timeout 120s;
proxy_connect_send_timeout 120s; proxy_connect_send_timeout 120s;
-647
View File
@@ -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";
}
-648
View File
@@ -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";
}
-4
View File
@@ -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"}'
@@ -1,13 +1,15 @@
#!/bin/bash #!/bin/bash
# sync-nginx-local-ips.sh # sync-nginx-local-ips.sh
# 自动维护 nginx 本机公网 IP 直连规则(/etc/nginx/local_ip_rules.conf): # 自动维护 nginx 本机公网 IP 直连规则(/etc/nginx/local_ip_rules.conf):
# 出站公网 IP(ipify)+ iam.aeolianchi.me 的 A 记录 = 本机当前公网 IP 集合。 # 出站公网 IP(ipify)+ <节点域名> 的 A 记录 = 本机当前公网 IP 集合。
# 公网 IP 变更时重写规则文件并 reload nginx(仅变更时 reload)。 # 公网 IP 变更时重写规则文件并 reload nginx(仅变更时 reload)。
# 由 /etc/cron.d/sync-nginx-local-ips 每 5 分钟调用一次。 # 由 /etc/cron.d/sync-nginx-local-ips 每 5 分钟调用一次。
#
# 部署时把 DOMAIN 改为本节点域名。
set -u set -u
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RULES=/etc/nginx/local_ip_rules.conf RULES=/etc/nginx/local_ip_rules.conf
DOMAIN=iam.aeolianchi.me DOMAIN=<节点域名>
ips="" ips=""
# 出站公网 IP # 出站公网 IP
+12 -4
View File
@@ -1,6 +1,6 @@
{ {
"log": { "log": {
"level": "warn", "level": "info",
"timestamp": true "timestamp": true
}, },
"inbounds": [ "inbounds": [
@@ -8,7 +8,7 @@
"type": "vless", "type": "vless",
"tag": "vless-reality-in", "tag": "vless-reality-in",
"listen": "0.0.0.0", "listen": "0.0.0.0",
"listen_port": 8443, "listen_port": "<vless端口>",
"users": [ "users": [
{ {
"uuid": "<VLESS_UUID>", "uuid": "<VLESS_UUID>",
@@ -17,11 +17,11 @@
], ],
"tls": { "tls": {
"enabled": true, "enabled": true,
"server_name": "www.apple.com", "server_name": "<伪装SNI域名>",
"reality": { "reality": {
"enabled": true, "enabled": true,
"handshake": { "handshake": {
"server": "www.apple.com", "server": "<伪装SNI域名>",
"server_port": 443 "server_port": 443
}, },
"private_key": "<REALITY_PRIVATE_KEY>", "private_key": "<REALITY_PRIVATE_KEY>",
@@ -30,6 +30,14 @@
] ]
} }
} }
},
{
"type": "shadowsocks",
"tag": "ss-in",
"listen": "0.0.0.0",
"listen_port": "<ss端口>",
"method": "aes-256-gcm",
"password": "<SS_PASSWORD>"
} }
], ],
"outbounds": [ "outbounds": [
+12
View File
@@ -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:<mtproto端口> <MTPROTO_SECRET>
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
+16
View File
@@ -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
+12
View File
@@ -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
-41
View File
@@ -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": "<SMTP_USER>",
"password": "<SMTP_PASSWORD>",
"from_name": "Royirin Rosmontis Private Proxy JP 流量监控",
"from_addr": "<SMTP_FROM>",
"to_addr": "<SMTP_TO>",
"starttls": false,
"skip_cert_verify": false,
"timeout_sec": 30,
"ssl": true
},
"current_ip": "<PUBLIC_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
}
@@ -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
@@ -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 <interface> --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())