VPNGate 家宽出口实战:Oracle VPS + fanout + VLESS/REALITY 全链路
TL;DR:用 Oracle VPS 做"接线总机",fanout 拨 VPNGate 志愿者家庭宽带做真实出口,VLESS+REALITY 入站,订阅经 nginx + Cloudflare Worker 下发到 karing。零成本,真居民 ISP 出口。
适用:想低成本获得居民 ISP 出口 IP 的个人学习;不适用:需要稳定固定 IP、高纯净度、商用的场景(看完前言再决定)。
测试环境:2026-10,Oracle 圣何塞 ARM(Ubuntu),fanout(byJoey/fanout,main 分支,部署前请固定 commit 并通读 install.sh),xray-core,karing。
本文所有 IP、域名、token、端口均为示例占位符,请替换成自己的;发布后建议更换示例中的 SNI 与端口。
零、前置声明:合理使用(先看完这节)
VPNGate 是筑波大学运营的志愿者网络,节点来自全球志愿者共享的家庭宽带。这意味着:
- 志愿者在为你的流量背锅——他们的家庭 IP 会出现在你访问的服务的日志里;
- 把 VPNGate 出口做成订阅分发给他人,可能违反其使用条款,也放大了对志愿者的连带风险。本教程默认仅自用;
- 请合理使用、不要跑大流量、不要做违规的事。部署与使用后果自负。
能接受,再往下看。
一、架构与取舍
局限先行
- 节点寿命按小时计,随时会死,必须有自动重拨;
- IP 是共享的——同一出口可能几十个人在用,商业 IP 情报库普遍把 VPNGate 段标记为 VPN/proxy。别指望它帮你过严格的账号/支付风控,“真住宅"不等于"高纯净”;
- 节点质量参差,有的国家常年没节点(截至 2026-10:美国符合家宽过滤的极少,新加坡经常挂零)。
架构
数据面(你的流量真正走的路):
你(karing)→ VPS:1000x(VLESS+REALITY 入站)
→ netns 内 SOCKS5 → VPNGate 志愿者家庭宽带 → 目标网站
订阅面(只负责下发配置,不走流量):
karing → Cloudflare Worker(sub.example.com)
→ VPS:10006(nginx TLS,只代理订阅 endpoint)→ fanout 面板 127.0.0.1:8899
上线前必做 3 件事(别等"以后再加固")
- 面板进程只绑
127.0.0.1,防火墙不放行面板端口;
- 想清楚 fail-open 兜底(见第七节),别等出事;
- 搭一个最简健康检查(第七节给逻辑),15 分钟跑一次。
二、准备工作
- Oracle Cloud 账号:Always Free ARM 实例,Ubuntu。注意 Oracle 有两层防火墙:VCN 的 Security List/NSG(云控制台管)+ 实例内部的 iptables(机器里管),端口必须两层同时放行,排障时一层一层看;
- 一个域名,NS 接入 Cloudflare,例如
proxy.example.com 做回源、sub.example.com 绑 Worker;
- Cloudflare 账号(Worker 用)。
灰云/橙云是关键:Cloudflare 只代理 443 等少数端口(10006 不在其中),所以 proxy.example.com 必须设为 DNS only(灰云),直接解析到 VPS IP。设成橙云的话,Worker 回源和 VLESS 直连都会失败。sub.example.com 绑 Worker 自定义域名,走 CF 网络没问题。
三、部署 fanout + 立即加固
# 先看一眼脚本再跑,main 分支换成固定 commit 更稳
bash <(curl -fsSL https://raw.githubusercontent.com/byJoey/fanout/main/install.sh)
systemctl enable --now fanout
装完面板默认监听 0.0.0.0:8899 且是 HTTP。先加固再用,别等第八节:
- 进面板改掉默认密码(面板路径是安装时生成的一串随机字符,形如
/<secret-path>,后面所有地址都要带它);
- 把面板绑到本机:
settings.json 里 listen_addr 改为 127.0.0.1,systemctl restart fanout;
- 以后进面板走 SSH 隧道,不走公网:
ssh -L 8899:127.0.0.1:8899 [email protected]
# 本地浏览器打开 http://127.0.0.1:8899/<secret-path>
- iptables 只放行业务端口。OCI 的 Ubuntu 镜像 INPUT 链尾有 REJECT:如果链尾已有 REJECT/DROP,允许规则必须插到它前面(
iptables -I),否则等于没加。加完 netfilter-persistent save。操作前先开第二个 SSH 会话,防止把自己锁死;IPv6(ip6tables)别漏。
四、新建出口:拨 VPNGate 节点
面板"新建出口"→ 选国家 → fanout 从 VPNGate 列表挑节点拨号。务必打开"只用家宽节点"过滤,否则可能拨到机房节点。
- 优先日本、韩国:节点多、延迟低、存活相对久;
- 每个国家 1~2 条就够,多了是维护负担;
- 美国/新加坡:截至 2026-10 前者极少通过家宽过滤(fanout 会直接拒绝"没有可用的空闲节点"),后者经常挂零,看到没有就换日韩,别硬来。
出口体检(每条新出口都做)
- 面板看当前出口 IP → 查 ASN,确认是居民 ISP(So-net、LG DACOM 之类);
- 交叉验证 hosting 标记:
ipinfo.io/<ip> 看 hosting 字段(whois 只能看到 ISP 名,分不出住宅还是商业专线);
- 测速:志愿者家宽上行经常只有几 Mbps,心里有数;
- 实测你的目标场景(流媒体解锁等),别信"理论上能过"。
五、VLESS 入站与 REALITY
5.1 端口规划(示例)
fanout 建入站会随机分配高位端口(如示例里的 48650),改成安全组放行范围内的。示例规划 10001~10005(你的环境按自己的安全组定):
端口 |
绑定出口 |
说明 |
|---|
10001 |
(不绑定) |
VPS 直连 |
10002 |
日本-1 |
住宅出口 |
10003 |
韩国-1 |
住宅出口 |
10004 |
日本-2 |
住宅出口 |
10005 |
韩国-2 |
住宅出口 |
改端口走面板 API。密码别写进命令行(会进 shell history 和 ps),用环境变量或交互输入:
read -s PANEL_PW
curl -s -b cookies.txt -c cookies.txt -d "password=$PANEL_PW" \
http://127.0.0.1:8899/<secret-path>/login > /dev/null
curl -s -b cookies.txt \
"http://127.0.0.1:8899/<secret-path>/api/panel/inbound/update?id=<入站ID>&port=10002"
chmod 600 cookies.txt # 用完删掉
改完确认 xray 路由绑定还在(in-10002-tcp → fanout-vpn<id>)。
5.2 REALITY 参数(概念先分清)
dest:服务端伪装/回落目标站;
serverNames(服务端)/serverName(客户端 SNI):握手时发送的域名;
fingerprint:客户端 TLS 指纹(如 chrome),和 SNI 是两回事;
privateKey/publicKey:REALITY 密钥对(面板建入站时自动生成);
shortId:客户端与服务端匹配用的短 ID,别手改。
REALITY 不用你为每个入站申请证书,借大厂域名的 TLS 握手做伪装。flow 留空即可(xtls-rprx-vision 按需开,它是客户端属性,改动不涉及换 UUID)。
5.3 SNI 选择与分散(建议级别)
同一 IP 上的 5 个端口本来就能被聚类,"分散 SNI"只是降低关联特征的经验做法,不是协议要求。建议:
- 每个入站用不同的大厂域名做 SNI(示例:tesla / apple / amazon / bing / cloudflare——示例而已,别照抄);
- 选站标准:支持 TLS1.3、看着像正常网站、最好与 VPS 地域别差太远;
- fanout 自带的 dest 检查通过 ≠ 客户端能连,必须做端到端验证。
实测坑(截至 2026-10):www.microsoft.com 做 SNI,服务端检查能过,但从圣何塞发起 REALITY 握手连续失败,换 www.bing.com 一次通过。SNI 定下来之前先验证:
openssl s_client -connect www.bing.com:443 -tls1_3 -alpn h2 </dev/null | head -5
5.4 端到端验证(两步)
- VPS 上起 xray 客户端,走入站的 vless 链接做 SOCKS 出口,
curl -x socks5h://127.0.0.1:11801 https://api.ipify.org,返回 IP 与面板显示的出口一致;
- 本地用 karing 实连一次——VPS 上通不代表你的网络路径通。
六、订阅链:证书 → nginx → Worker
6.1 先签证书
Let’s Encrypt 的 HTTP-01 验证走的是 80 端口,和 443 无关。所以:443 没放行不影响申请,只要 80 通;80 也不通才需要 DNS-01。
# 80 已放行时最简单(示例拓扑里 80 是开的,顺便做 301 跳转)
certbot certonly --nginx -d proxy.example.com
# 续期由 systemd timer 自动处理;定期验证:
certbot renew --dry-run
注意顺序:先有证书,再写 nginx 的 ssl 配置(否则 nginx -t 直接失败)。签完把子域名记下来——证书会进 CT 公开日志,子域名是藏不住的。
6.2 nginx:只暴露订阅 endpoint
server {
listen 10006 ssl;
server_name proxy.example.com;
ssl_certificate /etc/letsencrypt/live/proxy.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/proxy.example.com/privkey.pem;
access_log off; # 订阅 URL 带 token,会被明文记进日志——关掉是降低泄露面,不是根治
server_tokens off;
# CF 回源 IP 白名单:只是挡掉直连扫描的噪音,不是认证!
# 任何人的 CF 站点/Worker 出口都在这些段里。完整列表:
# curl -s https://www.cloudflare.com/ips-v4 https://www.cloudflare.com/ips-v6
# 拼成 allow 行(v4+v6 都要),最后 deny all
allow 173.245.48.0/20;
# ...(全部填上,别学示例只写两段就上线,会把正常回源 403 掉)
deny all;
location = /<secret-path>/sub {
proxy_pass http://127.0.0.1:8899;
}
location / {
return 404;
}
}
location = 精确匹配是重点:写成 location / 全代理等于把面板登录页挂公网;
- 端口用 10006 是因为示例里 OCI 安全组没放 443;你的厂商放行了 443 就直接用 443;
- 想要真认证:上 Authenticated Origin Pulls(mTLS),或 Worker 加自定义头 + nginx 校验。
6.3 Cloudflare Worker:订阅中转
// 建议用 Module Worker(export default)+ wrangler;
// 我们当时用 API 单文件上传时 module 格式被拒(10021),
// 才用的 service-worker(addEventListener)格式——那是上传方式问题,不是"必须"
const UPSTREAM = 'https://proxy.example.com:10006'; // 别硬编码 IP:Worker subrequest 不支持直接对 IP 发起(我们实测报 1003),一律用域名
export default {
async fetch(req, env) {
const url = new URL(req.url);
// 只放行订阅路径,别做成开放转发器
if (url.pathname !== '/<secret-path>/sub') {
return new Response('Not Found', { status: 404 });
}
let resp;
try {
resp = await fetch(UPSTREAM + url.pathname + url.search,
{ headers: { 'User-Agent': 'Mozilla/5.0' } });
if (!resp.ok) throw new Error('upstream ' + resp.status);
} catch (e) {
return new Response('Bad Gateway', { status: 502 });
}
let body = await resp.text();
try {
// 最小实现:假设订阅是标准 base64;base64url/分行等情况需另行处理
let decoded = atob(body.trim());
decoded = decoded.split('198.51.100.10').join('proxy.example.com');
body = btoa(decoded);
} catch (e) {
return new Response('Bad Gateway', { status: 502 });
}
return new Response(body, {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-store', // 带 token 的订阅别被缓存
},
});
}
};
说明:这是最小实现——透传时丢了上游的 subscription-userinfo 等头(karing 会用),生产用建议补上;Worker 只是转发层,不做 token 鉴权,别误会。
最终订阅地址:https://sub.example.com/<secret-path>/sub?token=<SUB_TOKEN>。
6.4 验证订阅
curl -s -A "Mozilla/5.0" \
"https://sub.example.com/<secret-path>/sub?token=<SUB_TOKEN>" \
| base64 -d | grep -o "sni=[^&#]*" | sort | uniq -c
# 期望 5 个不同 sni 各 1 次;另检查 UUID 不重复
curl 带 -A:CF 会拦默认 UA(Python urllib 不带 UA 拿过 403,虚惊一场)。
七、fail-open:最隐蔽的风险
实测:隧道死掉后,xray 里对应入站的路由规则会消失,流量 fallback 到默认出站(direct = VPS 本机出口)。客户端毫无感知,等于静默切成了机房 IP。
我们的做法(15 分钟跑一次):
- 对每条入站做 REALITY 握手,验证出口 IP 不是 VPS 本机 IP(最好连 ASN 一起对,连续多次确认);
- 连续 2 次失败 → 调面板 API 自动禁用该入站(连不上,好过静默走直连);
- 之后每轮恢复探测:重开 → 握手验证 → 通了保持开启并告警,没通重新禁用;
- 手动禁用的永不自动碰;同一故障只告警一次。
更彻底的结构性兜底:在 xray 路由末尾给住宅入站加一条 → blackhole 的 catch-all(10001 直连显式走 direct),路由丢了就断流不断泄。但要先确认 fanout 重写配置时不会覆盖这条——没验证过,列出来供参考。
八、日常维护(每月看一眼)
- 健康检查有没有持续 ALERT 的隧道;
certbot renew --dry-run 确认续期链路;
- CF 官方 IP 段变更 → 同步 nginx 白名单;
- 备份:fanout 配置目录(面板密码、sub_token、入站配置)+ nginx 站点配置,打包加密异地存,口令单独保管;备份完最好做一次恢复演练;
- 准备一个备用订阅域名,万一主域名被封可迁移。
九、避坑速查表
症状 |
原因 |
解法 |
|---|
新端口本地通、公网不通 |
iptables 链尾 REJECT 吃了规则 / 安全组没放 |
iptables -I 插前面 + netfilter-persistent save;检查云控制台安全组 |
面板绿勾但客户端连不上 |
fanout 的 dest 检查只是服务端视角 |
xray 客户端端到端握手验证(5.4) |
karing 订阅 502 |
裸 IP+非标端口被拦截;或刷新请求走了代理自身 |
走 Worker 订阅链;订阅域名加直连规则;偶发时可临时关 VPN 排查 |
某天突然变成机房 IP |
隧道死了,fail-open |
第七节健康检查 + 自动禁用 |
能连但网页转圈 |
链路 UDP/QUIC 不通 |
karing 里禁用 QUIC/HTTP3 |
大页面/下载卡死 |
嵌套隧道 MTU 问题 |
调小 mssfix / tun-mtu |
换了 SNI 还是握手失败 |
目标站回包策略问题 |
换 SNI(microsoft→bing 实测案例),用 s_client 先验证站点 |
十、备选与进阶
- 嫌 nginx+证书麻烦:可以试试 cloudflared tunnel 或"VPS 定时把订阅推到 Workers KV",整套回源都不用开了;
- 要更强伪装再看 Hysteria2 等,但先把这套跑稳。
初稿 v2。实测 4 条住宅隧道(日本×2、韩国×2)+ 1 条直连。教程里的"血泪"都是真踩出来的,欢迎指正。