添加 server_config:jp 服务器端配置(nginx/sing-box/流量监控/脚本/存档),已脱敏
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# server_config — 服务器端配置(已脱敏)
|
||||
|
||||
来源:**jp 服务器**(`iam.aeolianchi.me`,Azure Japan East 学生订阅,Ubuntu 24.04)
|
||||
采集时间:2026-08-23
|
||||
|
||||
> 本目录只收录**服务器端**配置;客户端(转发器 / PAC / WG)见仓库根目录的 `lite/` 与 `common/`。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
server_config/
|
||||
├── nginx/ # jp nginx(HTTPS CONNECT 代理 + 明文 HTTP 转发 + 伪装站)
|
||||
│ ├── nginx.conf # 主配置(proxy_connect 模块 + /__proxy 转发 + 自代理环保护)
|
||||
│ ├── conf.d/ # 附加站点
|
||||
│ │ ├── api.aeolianchi.me.conf # 流量监控状态反代
|
||||
│ │ └── autodiscover.aeolianchi.me.conf # autodiscover 反代(→ bj)
|
||||
│ └── local_ip_rules.conf.example # 本机公网 IP 直连规则(自动生成,示例)
|
||||
├── sing-box/
|
||||
│ └── config.json # VLESS + REALITY 入站(8443),密钥已脱敏
|
||||
├── traffic-monitor/ # Azure 门户口径流量监控(/home/agent)
|
||||
│ ├── config.json # SMTP 凭据已脱敏
|
||||
│ ├── traffic_monitor.py # 监控脚本(vnstat + Azure Usage Details API)
|
||||
│ └── traffic-monitor.service # systemd 单元
|
||||
├── scripts/
|
||||
│ ├── sync-nginx-local-ips.sh # 每 5 分钟自动同步公网 IP 直连规则(cron)
|
||||
│ └── rrpp-stats # vnstat 统计输出(每分钟写入 stats.json)
|
||||
├── pac/ # 旧版 PAC(现客户端改为内存生成,仅存档)
|
||||
│ ├── proxy.pac
|
||||
│ └── proxy_wg.pac
|
||||
└── cron/
|
||||
├── crontab.txt # RRRosmontis 用户 crontab
|
||||
└── sync-nginx-local-ips # /etc/cron.d 条目
|
||||
```
|
||||
|
||||
## 脱敏说明
|
||||
|
||||
| 原文 | 替换为 | 说明 |
|
||||
|---|---|---|
|
||||
| 公网 IP(`20.48.32.251` / `20.89.52.88`) | `<PUBLIC_IP>` | 服务器公网地址(DNS 可解析,但按惯例遮蔽) |
|
||||
| VLESS UUID | `<VLESS_UUID>` | sing-box 入站用户 UUID |
|
||||
| REALITY 私钥 | `<REALITY_PRIVATE_KEY>` | TLS Reality 私钥(机密) |
|
||||
| REALITY short_id | `<REALITY_SHORT_ID>` | TLS Reality short id |
|
||||
| SMTP 账号 / 密码 / 收发地址 | `<SMTP_USER>` `<SMTP_PASSWORD>` `<SMTP_FROM>` `<SMTP_TO>` | 告警邮件凭据 |
|
||||
|
||||
未收录:TLS 私钥/证书(仅保留 nginx 中的路径引用)、`/etc/nginx/.htpasswd`(认证哈希,用户名为 `user`)、Azure 订阅 ID 等。
|
||||
|
||||
## 安全提示
|
||||
|
||||
1. **`common/jp_client.conf`(WireGuard 客户端配置)此前已入库且含真实密钥**(客户端私钥、对端公钥、Endpoint)。若本仓库非私有,建议**轮换 WG 密钥与 sing-box REALITY 密钥**。
|
||||
2. `local_ip_rules.conf` 由 `sync-nginx-local-ips.sh` 每 5 分钟自动重写(cron),仓库内为脱敏示例;公网 IP 变更后无需手动改 nginx。
|
||||
3. 明文 HTTP 代理路径要求 Basic 认证(`user`);无认证返回 401,不会成为开放代理。
|
||||
@@ -0,0 +1,2 @@
|
||||
* * * * * /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
|
||||
@@ -0,0 +1 @@
|
||||
*/5 * * * * root /usr/local/sbin/sync-nginx-local-ips.sh >/dev/null 2>&1
|
||||
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# 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;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# nginx 本机公网 IP 直连规则(自动生成,勿手改;由 sync-nginx-local-ips.sh 维护)
|
||||
# 公网 IP 变更后本文件会被重写并 reload nginx
|
||||
if ($host = "<PUBLIC_IP>") { rewrite ^(.*)$ /__local$1 last; }
|
||||
if ($host = "<PUBLIC_IP>") { rewrite ^(.*)$ /__local$1 last; }
|
||||
@@ -0,0 +1,179 @@
|
||||
load_module /usr/lib/nginx/modules/ngx_http_proxy_connect_module.so;
|
||||
worker_processes auto;
|
||||
worker_rlimit_nofile 65535;
|
||||
events {
|
||||
worker_connections 8192;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
server_tokens off;
|
||||
resolver 8.8.8.8 ipv6=off valid=300s;
|
||||
resolver_timeout 10s;
|
||||
# 默认站点分流:本机/私网直连返回伪装页;其余 Host(正向代理目标)走 /__proxy 转发
|
||||
# 公网 IP 无需手改:cron(sync-nginx-local-ips.sh)自动写入 local_ip_rules.conf;
|
||||
# 另有 /__proxy 自代理环保护兜底,即使公网 IP 变更也不会死循环。
|
||||
map $host $is_local {
|
||||
default 0;
|
||||
"~^(10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)" 1;
|
||||
}
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
keepalive_requests 100;
|
||||
|
||||
# 80 端口:iam.aeolianchi.me 强制跳转到 HTTPS(保留 ACME 验证路径)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
# 443 端口:iam.aeolianchi.me HTTPS 站点 + 认证 CONNECT 代理
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name iam.aeolianchi.me;
|
||||
root /var/www/html;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/iam.aeolianchi.me/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/iam.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;
|
||||
|
||||
# 仅 CONNECT/正向代理需要认证,普通页面不需要
|
||||
auth_basic "Restricted";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
|
||||
proxy_connect;
|
||||
proxy_connect_allow 443 563;
|
||||
proxy_connect_connect_timeout 60s;
|
||||
proxy_connect_read_timeout 120s;
|
||||
proxy_connect_send_timeout 120s;
|
||||
|
||||
location / {
|
||||
auth_basic off;
|
||||
if ($host != "iam.aeolianchi.me") {
|
||||
rewrite ^ /__proxy last;
|
||||
}
|
||||
index index.html;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location /__proxy {
|
||||
internal;
|
||||
proxy_intercept_errors on;
|
||||
error_page 502 503 504 =444 @drop;
|
||||
resolver 8.8.8.8 ipv6=off valid=300s;
|
||||
proxy_pass $scheme://$host$request_uri;
|
||||
proxy_set_header Host $host;
|
||||
proxy_ssl_name $host;
|
||||
proxy_ssl_server_name on;
|
||||
}
|
||||
|
||||
location @drop {
|
||||
return 444;
|
||||
}
|
||||
}
|
||||
|
||||
# 443 端口 fallback:其他 SNI 也可用 CONNECT 代理;普通 GET 只返回伪装页
|
||||
server {
|
||||
listen 443 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_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# 仅 CONNECT/正向代理需要认证,普通页面不需要
|
||||
auth_basic "Restricted";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
|
||||
proxy_connect;
|
||||
proxy_connect_allow 443 563;
|
||||
proxy_connect_connect_timeout 60s;
|
||||
proxy_connect_read_timeout 120s;
|
||||
proxy_connect_send_timeout 120s;
|
||||
|
||||
location / {
|
||||
auth_basic off;
|
||||
# 自动判定本机:连接本地地址 / 无 Host / 私网回环 / cron 维护的公网 IP 规则
|
||||
# 命中则 rewrite 到 /__local<原路径>(保留 $uri),其余走 /__proxy 转发
|
||||
if ($host = $server_addr) { rewrite ^(.*)$ /__local$1 last; }
|
||||
if ($host = "_") { rewrite ^(.*)$ /__local$1 last; }
|
||||
if ($is_local) { rewrite ^(.*)$ /__local$1 last; }
|
||||
include /etc/nginx/local_ip_rules.conf;
|
||||
rewrite ^ /__proxy last;
|
||||
}
|
||||
|
||||
# 本机 IP/域名直连:剥掉前缀后按原路径服务伪装页(保持原有行为)
|
||||
location /__local {
|
||||
internal;
|
||||
auth_basic off;
|
||||
rewrite ^/__local(.*)$ $1 break;
|
||||
root /var/www/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# 明文 HTTP 正向代理:绝对形式请求(Host=目标域名)在此转发
|
||||
location /__proxy {
|
||||
internal;
|
||||
# 自代理环保护:转发请求带标记头,二次进入说明代理目标指向本机(未知/新公网 IP),
|
||||
# 返回 508 Loop Detected 而非死循环
|
||||
if ($http_x_proxy_loop_guard = "1") {
|
||||
return 508;
|
||||
}
|
||||
proxy_set_header X-Proxy-Loop-Guard "1";
|
||||
resolver 8.8.8.8 ipv6=off valid=300s;
|
||||
proxy_pass http://$http_host$request_uri;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_read_timeout 120s;
|
||||
proxy_intercept_errors on;
|
||||
# 目标不可达/无法解析:直接断开连接(444),不返回任何页面
|
||||
error_page 502 503 504 =444 @drop;
|
||||
}
|
||||
|
||||
location @drop {
|
||||
return 444;
|
||||
}
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
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";
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
# /usr/local/bin/rrpp-stats — returns traffic stats as JSON
|
||||
|
||||
vnstat --json 2>/dev/null || echo '{"error":"vnstat not available"}'
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# sync-nginx-local-ips.sh
|
||||
# 自动维护 nginx 本机公网 IP 直连规则(/etc/nginx/local_ip_rules.conf):
|
||||
# 出站公网 IP(ipify)+ iam.aeolianchi.me 的 A 记录 = 本机当前公网 IP 集合。
|
||||
# 公网 IP 变更时重写规则文件并 reload nginx(仅变更时 reload)。
|
||||
# 由 /etc/cron.d/sync-nginx-local-ips 每 5 分钟调用一次。
|
||||
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
|
||||
|
||||
ips=""
|
||||
# 出站公网 IP
|
||||
out=$(curl -s --max-time 8 https://api.ipify.org 2>/dev/null | tr -dc '0-9.')
|
||||
# 域名 A 记录(入站公网 IP)
|
||||
dns=$(getent ahostsv4 "$DOMAIN" 2>/dev/null | awk 'NR==1{print $1}')
|
||||
|
||||
for ip in "$out" "$dns"; do
|
||||
case "$ip" in
|
||||
''|*[!0-9.]*) continue ;;
|
||||
esac
|
||||
case "|$ips|" in
|
||||
*"|$ip|"*) ;;
|
||||
*) ips="${ips}|${ip}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
tmp="$RULES.tmp"
|
||||
{
|
||||
echo "# nginx 本机公网 IP 直连规则(自动生成,勿手改;由 sync-nginx-local-ips.sh 维护)"
|
||||
echo "# 公网 IP 变更后本文件会被重写并 reload nginx"
|
||||
IFS='|'
|
||||
for ip in $ips; do
|
||||
[ -n "$ip" ] && echo "if (\$host = \"$ip\") { rewrite ^(.*)\$ /__local\$1 last; }"
|
||||
done
|
||||
unset IFS
|
||||
} > "$tmp"
|
||||
|
||||
if ! cmp -s "$RULES" "$tmp"; then
|
||||
mv "$tmp" "$RULES"
|
||||
if nginx -t >/dev/null 2>&1; then
|
||||
systemctl reload nginx
|
||||
logger -t sync-nginx-local-ips "nginx local-IP rules updated: [${ips#|}]"
|
||||
else
|
||||
logger -t sync-nginx-local-ips "nginx -t FAILED, rules NOT applied; keeping previous rules"
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "warn",
|
||||
"timestamp": true
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-reality-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8443,
|
||||
"users": [
|
||||
{
|
||||
"uuid": "<VLESS_UUID>",
|
||||
"flow": "xtls-rprx-vision"
|
||||
}
|
||||
],
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": "www.apple.com",
|
||||
"reality": {
|
||||
"enabled": true,
|
||||
"handshake": {
|
||||
"server": "www.apple.com",
|
||||
"server_port": 443
|
||||
},
|
||||
"private_key": "<REALITY_PRIVATE_KEY>",
|
||||
"short_id": [
|
||||
"<REALITY_SHORT_ID>"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[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
|
||||
@@ -0,0 +1,666 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user