自托管节点聚合订阅服务 SOP
公开免费节点聚合 → TCP 探活 → CN 出口协议级体检 → v2rayN / sing-box 订阅端点。 解决公开节点池"端口通但实际不可用"的核心痛点。
适用人群:自建订阅服务运维、个人节点池聚合、公开节点可用性研究。
0. TL;DR(30 秒概览)
公开订阅源(5+) ── 6h 定时拉取 ──▶ raw URI 池 (~400)
│
TCP 探活 (1.5s, 并发 200)
│
alive (~340)
│
┌──────────────────────────┴──────────────────────────┐
│ │
/sub 端点 (旁路) CN-Exit 体检
(base64, v2rayN/Clash 兼容) │
│ mihomo /proxies/{}/delay
│ 三轮 5s 协议级 healthcheck
│ 全 0 → dead
│ │
│ host:port → cn_blacklist.json
└────────── /sub 出口处过滤 ◀─────────────────┘
│
~190 真正可用节点
核心依赖:
- 一台海外 VPS(SG / JP 延迟低)
- Python 3.10+ / aiohttp / PyYAML
- mihomo(原 Clash.Meta)用于协议级体检
- 可选:sing-box 做多端口分流出口
实测数据(5 源 / 周 1 体检):
- raw 节点: ~423
- TCP 探活后: ~341
- CN 体检剔除: 152 (45%)
- 最终可用: 189 节点
1. 为什么需要这个方案
1.1 公开节点池的真实困境
| 问题 | 表现 | 传统方案能否解决 |
|---|---|---|
| 短寿命 | 平均 12-48h 失效,日均 30-60% 死亡 | ✅ 定时 TCP 探活 |
| 协议假活 | TCP 端口开但 TLS / 协议层失效 | ⚠️ 需协议级握手 |
| 出口不可用 | VPS 可达但节点出口被反向封锁 | ❌ 无解 |
VPS TCP 探活验证的是「VPS → 节点 IP:Port 可达」,但用户实际使用的链路是「CN → VPS → 节点 → 目标站」。后者失败原因 90% 在「节点 → 目标站」段:节点 ISP 屏蔽国内站、协议密钥已被吊销但 TCP 端口仍监听、出口被反向封锁。只有让位于真实使用环境的客户端做体检才能复现这种失败。
1.2 设计原则
- 单一职责:聚合 / 探活 / 订阅 / 体检四层完全解耦,可独立替换
- 失败安全:体检失败时 abort 不动主服务;daemon 重启即热加载缓存避免空窗
- 零供应商锁定:纯开源工具 + 公开订阅源
- 最小运维:VPS 端全自动;体检层每周一次(cron / 任务计划)
2. 整体架构
┌─────────────────── VPS (海外) ──────────────────────┐
│ │
│ AGG_SOURCES ──6h抓取──▶ aggregator_fetcher.py │
│ (5个公开订阅源) │ │
│ ├──▶ pool_data/ │
│ │ agg_pool.json │
│ │ (持久化, ts+uris)│
│ ▼ │
│ TCP 探活 (1.5s, 并发200) │
│ │ │
│ ▼ │
│ cn_blacklist.json 过滤 │
│ │ │
│ ▼ │
│ aiohttp daemon ── /sub ──▶ base64(URIs) │
│ (端口 9588) ── /api/pool ──▶ JSON status │
│ │
│ (可选) sing-box 多端口分流出口 │
│ │
└──────────────────────────▲──────────────────────────┘
│ scp cn_blacklist.json
│
┌────── 本机 (CN 出口, 周一 09:30) ─────────┐
│ │
│ cn_health_check.py │
│ ├─ ssh 拉 agg_pool.json │
│ ├─ 自启 mihomo (端口 19090, file provider)│
│ ├─ 三轮 /proxies/{}/delay 5s │
│ ├─ 全 3 轮 delay==0 → dead │
│ ├─ name 反查 URI → host:port │
│ └─ scp cn_blacklist.json → VPS │
└───────────────────────────────────────────┘
为什么体检放本机不放 VPS?
VPS 出口在海外,访问国内站的网络路径与 CN 用户完全相反。把体检客户端放在 CN 出口,才能真实代表用户视角。
3. 聚合抓取层
3.1 推荐公开订阅源(2026-05 验证可用)
AGG_SOURCES = [
"https://raw.githubusercontent.com/peasoft/NoMoreWalls/master/list.txt",
"https://raw.githubusercontent.com/Barabama/FreeNodes/master/nodes/nodefree.txt",
"https://raw.githubusercontent.com/Pawdroid/Free-servers/main/sub",
"https://raw.githubusercontent.com/mfuu/v2ray/master/v2ray",
"https://raw.githubusercontent.com/ripaojiedian/freenode/main/sub",
]
5 源去重后 raw 总量 400-500。不建议超过 8 源 —— 重复率急剧上升、超时风险增加。 候选扩展源见 wzdnzd/aggregator README。
3.2 多格式源兼容解码
import base64
import urllib.request
PROTO_PREFIX = ("vless://", "vmess://", "ss://", "trojan://", "hysteria2://", "hy2://")
def fetch_source(url: str, timeout: int = 15) -> str:
"""自动识别纯文本 vs base64 整体编码"""
raw = urllib.request.urlopen(url, timeout=timeout).read().decode("utf-8", "ignore")
if any(p in raw for p in PROTO_PREFIX):
return raw
try:
decoded = base64.b64decode(raw + "==" * (-len(raw) % 4)).decode("utf-8", "ignore")
if any(p in decoded for p in PROTO_PREFIX):
return decoded
except Exception:
pass
return raw
坑:纯文本源强行 base64 解码会产生乱码 URI 污染池,必须先检测协议前缀。
3.3 URI 端点解析(4 协议覆盖)
import json, re
from urllib.parse import urlparse
def parse_endpoint(uri: str):
"""从 URI 提取 (host, port),失败返回 None"""
try:
if uri.startswith("vmess://"):
payload = base64.b64decode(uri[8:].split("#")[0] + "==").decode()
d = json.loads(payload)
return d["add"], int(d["port"])
u = urlparse(uri.split("#")[0])
if u.hostname and u.port:
return u.hostname, u.port
if uri.startswith("ss://"):
body = uri[5:].split("#")[0].split("?")[0]
decoded = base64.b64decode(body + "==").decode()
m = re.search(r"@([^:]+):(\d+)", decoded)
if m:
return m.group(1), int(m.group(2))
except Exception:
pass
return None
3.4 节点重命名(只动 fragment)
import urllib.parse as up
def rename_with_prefix(uri: str, prefix: str = "[AGG]") -> str:
body, _, frag = uri.partition("#")
orig = up.unquote(frag) if frag else "node"
return f"{body}#{up.quote(f'{prefix} {orig}')}"
关键坑:vmess URI 主体是 base64({json}),绝对不能 urlquote,只能改
#fragment。 URL fragment 中[AGG]会被 percent-encoded 为%5BAGG%5D,grep 时注意。---
4. TCP 探活层
4.1 异步端口探测
import asyncio
async def tcp_alive(host: str, port: int, timeout: float = 1.5) -> bool:
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(host, port), timeout=timeout
)
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
return True
except Exception:
return False
async def probe_all(uris: list, concurrency: int = 200) -> list:
sem = asyncio.Semaphore(concurrency)
async def _one(uri):
ep = parse_endpoint(uri)
if not ep:
return None
async with sem:
return uri if await tcp_alive(*ep) else None
results = await asyncio.gather(*(_one(u) for u in uris))
return [r for r in results if r]
并发调优:200 并发 × 1.5s → 400 节点约 6 秒。 过大并发(>500)会触发 Linux
nf_conntrack限流,表现为后半段大量 false negative。 调大上限:echo 262144 > /proc/sys/net/netfilter/nf_conntrack_max
4.2 持久化缓存 + 后台刷新
import time, json, logging
from pathlib import Path
CACHE_PATH = Path("pool_data/agg_pool.json")
REFRESH_INTERVAL = 6 * 3600
log = logging.getLogger("aggregator")
async def refresh_loop():
"""daemon 启动即首次刷新 + 每 6h 循环"""
while True:
try:
raw = []
for src in AGG_SOURCES:
try:
text = fetch_source(src)
raw.extend(
l.strip() for l in text.splitlines()
if l.strip().startswith(PROTO_PREFIX)
)
except Exception as e:
log.warning("source fail %s: %s", src, e)
# host:port 去重
seen, uniq = set(), []
for u in raw:
ep = parse_endpoint(u)
if not ep or ep in seen:
continue
seen.add(ep)
uniq.append(rename_with_prefix(u))
log.info("combined %d unique (pre-probe)", len(uniq))
alive = await probe_all(uniq)
log.info("alive after TCP probe: %d", len(alive))
CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
CACHE_PATH.write_text(json.dumps({
"ts": int(time.time()), "uris": alive
}))
except Exception as e:
log.exception("refresh error: %s", e)
await asyncio.sleep(REFRESH_INTERVAL)
设计要点:
- 源失败只 warn 不中断整体
- daemon 重启时读 cache 热加载,避免空窗期
- 后台 task 与 HTTP handler 完全解耦
5. 订阅输出层
5.1 /sub 端点(v2rayN / Clash 通用)
from aiohttp import web
BLACKLIST_PATH = Path("pool_data/cn_blacklist.json")
def get_filtered_lines() -> list:
"""读 cache + 应用 CN 黑名单过滤(每次请求热读)"""
if not CACHE_PATH.exists():
return []
data = json.loads(CACHE_PATH.read_text())
uris = data.get("uris", [])
bl = set()
if BLACKLIST_PATH.exists():
bl = set(json.loads(BLACKLIST_PATH.read_text()).get("blacklist", []))
if not bl:
return uris
result = []
for u in uris:
ep = parse_endpoint(u)
if ep and f"{ep[0]}:{ep[1]}" in bl:
continue
result.append(u)
return result
async def handle_sub(request):
lines = get_filtered_lines()
body = base64.b64encode("\n".join(lines).encode()).decode()
return web.Response(text=body, content_type="text/plain")
async def handle_status(request):
lines = get_filtered_lines()
cache_ts = 0
if CACHE_PATH.exists():
cache_ts = json.loads(CACHE_PATH.read_text()).get("ts", 0)
bl_size = 0
if BLACKLIST_PATH.exists():
bl_size = len(json.loads(BLACKLIST_PATH.read_text()).get("blacklist", []))
return web.json_response({
"alive_count": len(lines),
"last_refresh": cache_ts,
"blacklist_size": bl_size,
})
客户端添加订阅地址 http://<YOUR_VPS>:9588/sub 即自动导入。
5.2 sing-box 多端口分流(可选进阶)
每地区独立入站端口 + urltest outbound:
{
"inbounds": [
{"tag": "auto-in", "type": "vless", "listen_port": 9599,
"users": [{"uuid": "<YOUR_UUID>"}]},
{"tag": "US-in", "type": "vless", "listen_port": 9601,
"users": [{"uuid": "<YOUR_UUID>"}]}
],
"outbounds": [
{"tag": "auto", "type": "urltest",
"outbounds": ["node-1", "node-2"],
"url": "https://www.gstatic.com/generate_204",
"interval": "5m"},
{"tag": "node-1", "type": "vless",
"server": "1.2.3.4", "server_port": 443}
],
"route": {
"rules": [{"inbound": "auto-in", "outbound": "auto"}]
}
}
由 daemon 的 refresh_loop 落盘后调用 systemctl reload sing-box。
5.3 daemon 启动入口
async def main():
app = web.Application()
app.router.add_get("/sub", handle_sub)
app.router.add_get("/api/pool", handle_status)
asyncio.create_task(refresh_loop())
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "0.0.0.0", 9588)
await site.start()
log.info("daemon listening on :9588")
await asyncio.Event().wait()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
```---
## 6. CN-Exit 出口黑名单(核心方法论)⭐
> **本节是 SOP 最大增量。** 几乎所有公开订阅项目都缺少这一层。
### 6.1 设计思路:旁路体检 + 覆写黑名单
本机(CN 出口) ├─ ssh 拉 URI 列表 ├─ 自启 mihomo(独立端口 19090) ├─ /proxies/{name}/delay (gstatic 204) ├─ 三轮 × 5s timeout ├─ 全 3 轮 delay==0 → dead ├─ name 反查 URI → parse_endpoint → host:port ├─ 写 cn_blacklist.json └─ scp → VPS:pool_data/cn_blacklist.json │ └─▶ /sub 热读,立即过滤生效(无需重启 daemon)
**优势**:
1. **不动主服务** — daemon 完全不知道 cn_blacklist 来自哪里,只在 `/sub` 出口处读一次
2. **无需重启** — daemon 每次 HTTP 请求都热读,scp 完即生效
3. **失败安全** — 本机网络抖动时立即 abort,绝不污染 VPS
### 6.2 mihomo file-provider 协议级 healthcheck
mihomo(原 Clash.Meta)的 `/proxies/{name}/delay` API 是**真正的协议级体检**:会建立完整代理链路并请求 `http://www.gstatic.com/generate_204`,`delay==0` 表示**任何一层失败**(DNS / TLS / 代理握手 / 出口访问)。
mihomo 配置(核心):
```yaml
external-controller: 127.0.0.1:19090
mode: rule
log-level: warning
proxy-providers:
agg-pool:
type: file
path: ./proxies.yaml
health-check:
enable: false # 手动触发,不用自动
proxy-groups:
- name: GLOBAL
type: select
use:
- agg-pool
rules:
- MATCH,GLOBAL
端口选择:用 19090 避开本机已有代理客户端(通常占 9090)。
6.3 三轮独立体检代码
import asyncio, aiohttp
from urllib.parse import quote as urlquote
MIHOMO_PORT = 19090
CONCURRENCY = 30
TIMEOUT_MS = 5000
ROUNDS = 3
async def delay_check(session, name: str) -> int:
"""单次协议级体检,返回 delay(ms),0=失败"""
url = f"http://127.0.0.1:{MIHOMO_PORT}/proxies/{urlquote(name)}/delay"
params = {
"timeout": str(TIMEOUT_MS),
"url": "http://www.gstatic.com/generate_204",
}
try:
async with session.get(
url, params=params, timeout=TIMEOUT_MS / 1000 + 3
) as r:
d = await r.json()
return d.get("delay", 0)
except Exception:
return 0
async def three_round_check(names: list) -> dict:
"""三轮体检,返回 {name: [d1, d2, d3]}"""
sem = asyncio.Semaphore(CONCURRENCY)
results = {}
async with aiohttp.ClientSession() as session:
async def _check_one(name):
delays = []
for _ in range(ROUNDS):
async with sem:
delays.append(await delay_check(session, name))
await asyncio.sleep(0.3)
results[name] = delays
await asyncio.gather(*(_check_one(n) for n in names))
return results
为什么三轮:单轮可能因临时拥塞假阴。三轮独立判定容忍偶发抖动;全 0 才判 dead。
6.4 完整体检流程
import subprocess, time, json, yaml, re, requests
from pathlib import Path
from urllib.parse import unquote
BUILTIN_PROXIES = {"GLOBAL", "REJECT", "DIRECT", "COMPATIBLE", "PASS"}
GROUP_TYPES = ("Selector", "URLTest", "Fallback", "LoadBalance", "Relay")
def run_cn_health_check(
vps_host: str,
vps_user: str,
remote_cache: str,
remote_blacklist: str,
mihomo_bin: str,
work_dir: Path = Path("./cn_check_workdir"),
min_alive_threshold: int = 30,
):
work_dir.mkdir(exist_ok=True)
# 1) SSH 拉 URI 列表
subprocess.run(
["scp", f"{vps_user}@{vps_host}:{remote_cache}",
str(work_dir / "agg_pool.json")],
check=True,
)
pool = json.loads((work_dir / "agg_pool.json").read_text())
uris = pool["uris"]
print(f"[1/6] pulled {len(uris)} URIs from VPS")
# 2) 写 mihomo 输入
proxies_file = work_dir / "proxies.yaml"
proxies_file.write_text("\n".join(uris))
config = {
"external-controller": f"127.0.0.1:{MIHOMO_PORT}",
"mode": "rule",
"log-level": "warning",
"proxy-providers": {
"agg-pool": {
"type": "file",
"path": str(proxies_file.resolve()),
"health-check": {"enable": False},
}
},
"proxy-groups": [
{"name": "GLOBAL", "type": "select", "use": ["agg-pool"]}
],
"rules": ["MATCH,GLOBAL"],
}
config_path = work_dir / "config.yaml"
config_path.write_text(yaml.safe_dump(config))
# 3) 启动 mihomo
proc = subprocess.Popen(
[mihomo_bin, "-d", str(work_dir), "-f", str(config_path)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
for _ in range(30):
try:
requests.get(f"http://127.0.0.1:{MIHOMO_PORT}/version", timeout=1)
break
except Exception:
time.sleep(0.5)
else:
proc.terminate()
raise RuntimeError(f"mihomo failed to start on :{MIHOMO_PORT}")
print("[2/6] mihomo started")
try:
# 4) 列出真实节点(排除内置 + group 类型)
r = requests.get(f"http://127.0.0.1:{MIHOMO_PORT}/proxies").json()
all_proxies = r.get("proxies", {})
names = [
n for n, info in all_proxies.items()
if n not in BUILTIN_PROXIES
and info.get("type") not in GROUP_TYPES
]
print(f"[3/6] found {len(names)} proxies to check")
# 5) 三轮体检
results = asyncio.run(three_round_check(names))
dead_names = [
n for n, delays in results.items()
if all(d == 0 for d in delays)
]
alive_count = len(names) - len(dead_names)
print(f"[4/6] dead: {len(dead_names)} / {len(names)}, alive: {alive_count}")
# 6) 安全阈值:本机断网保护
if alive_count < min_alive_threshold:
raise RuntimeError(
f"ABORT: only {alive_count} alive (< {min_alive_threshold}), "
"likely local network issue. NOT pushing blacklist."
)
# 7) name → host:port(关键:mihomo 同名后缀处理)
name_to_uri = {}
for uri in uris:
body, _, frag = uri.partition("#")
node_name = unquote(frag) if frag else ""
# 后定义的覆盖前定义的(mihomo 行为一致)
name_to_uri[node_name] = uri
blacklist = set()
for dead_name in dead_names:
uri = name_to_uri.get(dead_name)
if not uri:
# mihomo 对重名节点加 " - 01", " - 02" 等后缀
base = re.sub(r"\s*-\s*\d+$", "", dead_name)
uri = name_to_uri.get(base)
if uri:
ep = parse_endpoint(uri)
if ep:
blacklist.add(f"{ep[0]}:{ep[1]}")
print(f"[5/6] blacklist: {len(blacklist)} entries")
# 8) 落盘 + scp 推送
bl_data = {
"ts": int(time.time()),
"blacklist": sorted(blacklist),
"stats": {
"total": len(names),
"dead": len(dead_names),
"alive": alive_count,
},
}
bl_path = work_dir / "cn_blacklist.json"
bl_path.write_text(json.dumps(bl_data, indent=2))
subprocess.run(
["scp", str(bl_path), f"{vps_user}@{vps_host}:{remote_blacklist}"],
check=True,
)
print("[6/6] DONE. /sub will filter immediately (hot-read)")
finally:
proc.terminate()
6.5 关键坑全集(实战血泪)
| # | 坑 | 表现 | 解法 |
|---|---|---|---|
| 1 | mihomo 同名后缀 | 多节点同 fragment → mihomo 自动加 - 01/ - 02 |
反查时 regex strip 后缀 |
| 2 | 三轮全 0 才判 dead | 单轮可能因临时拥塞假阴 | 3 轮独立判定,容忍偶发 |
| 3 | alive < 30 abort | 本机断网导致全部判 dead 然后覆写 → 全部节点被禁 | 硬阈值保护,绝不推送 |
| 4 | 全量覆写策略 | 只记"现在 dead 的",不增量累积 | 下次体检若节点复活自动解禁 |
| 5 | fragment URL 编码 | [AGG] → %5BAGG%5D |
grep 时用解码后文本 |
| 6 | 端口冲突 | 本机可能已有 Clash 占 9090 | 用 19090 独立端口 |
| 7 | mihomo 进程残留 | 异常退出后端口被占 | try/finally proc.terminate() |
| 8 | provider 类型节点排除 | 把 GLOBAL/Selector 当节点测会拿到聚合 delay | 过滤 type in GROUP_TYPES |
7. 部署指南
7.1 VPS 端文件结构
~/proxy-pool/
├── daemon.py # 主服务(聚合+订阅,§3-§5 整合)
├── pool_data/
│ ├── agg_pool.json # 缓存(daemon 写)
│ └── cn_blacklist.json # 黑名单(本机 scp 推送)
├── venv/ # Python 虚拟环境
└── logs/
7.2 systemd 服务(推荐)
# /etc/systemd/system/node-aggregator.service
[Unit]
Description=Node Aggregator Subscription Daemon
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/proxy-pool
ExecStart=/home/deploy/proxy-pool/venv/bin/python daemon.py
Restart=always
RestartSec=10
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now node-aggregator
sudo systemctl status node-aggregator # 确认 active (running)
7.3 本机端 — 周任务配置
Linux (cron):
# crontab -e
30 9 * * 1 cd ~/cn-health-check && /usr/bin/python3 cn_health_check.py >> logs/check.log 2>&1
Windows (Task Scheduler):
触发器: 每周一 09:30
操作: pythonw.exe cn_health_check.py
工作目录: C:\Tools\cn-health-check
7.4 环境变量
# VPS 端 daemon.py
export POOL_PORT=9588
export REFRESH_HOURS=6
export TCP_CONCURRENCY=200
export TCP_TIMEOUT=1.5
# 本机端 cn_health_check.py
export VPS_HOST=<your-vps-ip-or-host>
export VPS_USER=<your-deploy-user>
export REMOTE_CACHE_PATH=~/proxy-pool/pool_data/agg_pool.json
export REMOTE_BLACKLIST_PATH=~/proxy-pool/pool_data/cn_blacklist.json
export MIHOMO_BIN=/usr/local/bin/mihomo # Win: C:\Tools\mihomo.exe
7.5 SSH 免密配置(必须)
# 本机
ssh-keygen -t ed25519 -f ~/.ssh/cn_check_key
ssh-copy-id -i ~/.ssh/cn_check_key.pub <user>@<vps>
# ~/.ssh/config 单独 alias 提高复用
Host pool-vps
HostName <vps-ip>
User deploy
IdentityFile ~/.ssh/cn_check_key
StrictHostKeyChecking accept-new
体检脚本中
vps_host直接用pool-vps即可,不再硬编码 IP。
8. 抓取频率与维护
8.1 频率选择依据
| 组件 | 频率 | 原因 |
|---|---|---|
| 聚合抓取 | 6h | 公开源通常每天更新 1 次,6h 平衡时效与带宽 |
| TCP 探活 | 随聚合 (6h) | 绑定刷新周期,无需独立调度 |
| CN 体检 | 每周 1 次 | 成本约 2 min;节点出口变化在周级时间尺度 |
| 黑名单清理 | 全量覆写 | 非增量,节点复活自动解禁 |
8.2 告警阈值建议
/sub返回节点数 < 60 持续 2 次刷新 → 增加订阅源- CN 体检
alive < 30→ 自动 abort,绝不推送(本机网络问题) - daemon cache 超过 24h 未更新 → 检查 VPS 网络 / systemd 状态
8.3 订阅源维护
公开 GitHub 仓库会删除 / 迁移:
- 每月检查
AGG_SOURCES是否 404,替换失活源 - 观察 awesome-vpn / aggregator 等索引发现新源
- 节点总量 < 200 时增补,> 600 时精简(重复率过高得不偿失)
9. 验证 Checklist
部署完成后逐项验证:
- [ ]
curl http://<VPS>:9588/sub | base64 -d | head -5→ 看到 vless / vmess / ss URI - [ ]
curl http://<VPS>:9588/api/pool→alive_count > 100 - [ ] v2rayN 添加订阅
http://<VPS>:9588/sub→ 节点列表导入成功 - [ ] 本机执行
cn_health_check.py→ 输出 dead 数量 + 推送成功日志 - [ ] 推送后再 curl
/api/pool→blacklist_size > 0且alive_count减少 - [ ] 手动选几个 alive 节点连接,能正常访问境外站
10. 局限性与边界
| 局限 | 说明 |
|---|---|
| 公开节点质量有限 | 即使双重体检,速度仍远不如付费服务;适合临时 / 备用 |
| 短寿命不变 | 每次刷新 30-60% 节点更替,不适合长期固定连接 |
| 不替代住宅代理 | 持续性任务(注册、爬虫、登录态)建议用 WARP IP 轮转或购买住宅代理 |
| 体检覆盖面 | 只测 gstatic 204,不代表所有目标站可达;可扩展多目标轮询 |
| 协议范围 | 主要覆盖 vless / vmess / ss / trojan / hy2,对 wireguard / naive 等未覆盖 |
| 无并发会话隔离 | 多用户共用 /sub 会争抢同一批节点,企业场景需分用户分组 |
11. 扩展方向
- 多地域体检:在 HK / JP / SG 分别部署体检客户端,输出 region-specific 黑名单
- 延迟分级:基于 mihomo delay 数值打 tag,客户端按延迟过滤
- 协议级 fallback:vless 失败时自动回退 ss,需在 sing-box 路由层配置
- 流量统计:daemon 增加
/api/usage端点,配合 sing-box stats API 输出每节点流量 - 配额管理:基于客户端 token 限速 / 限连接数,防止公开节点被滥用
致谢
本 SOP 基于以下开源项目最佳实践提炼:
- mihomo — 协议级体检的核心引擎
- sing-box — 多端口分流出口实现
- aggregator (wzdnzd) — 公开订阅源索引
- NoMoreWalls / FreeNodes 等公开节点维护者