0%

引言

Across the Great Wall we can reach every corner in the world

网络拓扑

在我的家庭网络环境中,主要设备均接入 2.5G 交换机。主路由负责拨号并作为 DHCP 服务器,旁路由(运行 OpenWRT 的斐讯 N1)与交换机一同接入主路由的 LAN 口。

核心思路:将需要代理的网络设备的 DNS 指向旁路由;同时在主路由上配置静态路由表,将特定 IP 段(如 Fake-IP 段)的流量统统转发给旁路由处理。

homeinfra

主路由配置

参考文章:AX6000 静态路由配置

如果你的路由器官方固件支持(例如 H3C NX54),直接在后台管理页面中添加静态路由表即可,无需通过 SSH 进行以下修改。

对于后台页面不支持设置静态路由、但能解锁 SSH 的路由器,可参考以下配置进行手动修改:
注:如果路由器既不支持静态路由也无法解锁 SSH,可采用手动指定客户端 DHCP 网关为旁路由 IP 的方式。但需注意,这种方式在访问国内网络时也会受限于旁路由的转发性能。

1. 增加 Fake-IP 段路由表

通过 SSH 登录主路由并编辑网络配置文件:

1
vim /etc/config/network

在网络配置中添加以下路由信息,将 198.18.0.0/16(任意 FakeIP 网段)的流量指向旁路由:

1
2
3
4
5
config route
option interface 'lan'
option target '198.18.0.0'
option netmask '255.255.0.0'
option gateway '192.168.x.x' # 此处替换为旁路由的实际 IP 地址

2. 修改防火墙转发策略

为了允许 Fake-IP 流量的正常转发,需要修改防火墙配置:

1
vim /etc/config/firewall

找到 defaults 和对应的 zone(lan)配置块,修改相应的策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
config defaults
option syn_flood '0'
option input 'ACCEPT'
option output 'ACCEPT'
option forward 'ACCEPT' # 将 REJECT 修改为 ACCEPT
option drop_invalid '0' # 将 1 修改为 0

config zone
option name 'lan'
option network 'lan'
option input 'ACCEPT'
option output 'ACCEPT'
option forward 'ACCEPT' # 将 REJECT 修改为 ACCEPT

3. 应用配置

重启网络与防火墙服务使得配置生效:

1
2
/etc/init.d/network restart
/etc/init.d/firewall restart

旁路由配置

在 OpenWRT 旁路由上安装并配置 OpenClash(或其他代理核心)。需要将本机 53 端口的请求劫持并转发到代理核心。核心运行在 Fake-IP + Tun 模式下,且 Fake-IP 的网段需要与上文中静态路由设置的 FakeIP 网段保持一致(通常为 198.18.0.0/16)。

采用 Fake-IP 的主要优势如下:

  1. 无感分流:配合主路由的静态路由表,客户端只需修改 DNS 服务器即可实现高速上网冲浪;国内流量直接不经过旁路由,即使旁路由断网,也不影响国内网站的正常访问。
  2. 连接加速:与 Real-IP 模式相比,域名解析在远端完成,省去了等待 DNS 返回结果再建立连接的过程,减少了一次 RTT。

在 Tun 模式下,OpenClash 会自动向系统注入如下路由规则:

1
198.18.0.0/16 dev utun scope link

这使得来自主路由转发的 Fake-IP 流量,能够顺利进入旁路由的 Tun 网卡,并被前端代理内核接管,完成后续的流量代理过程。

注意:如果不使用 OpenClash,可能需要手动配置防火墙(iptables/nftables 等)以允许 Fake-IP 流量进入核心,并开启 IP 转发(IP Forwarding)。

内网服务与外网访问

出于安全考虑,建议内网的所有服务(如软路由后台、NAS 等)仅监听 LAN 地址,避免将服务直接暴露到公网。

如果有从外网远程访问家庭局域网的需求,建议搭建虚拟局域网(VPN)或安全的代理隧道。以下以 Mihomo 为例分别介绍 IPv6 和 IPv4 环境下的解决方案:

建议:在复杂的跨 ISP 访问环境下,使用 TCP 连接以增强稳定性。

配置一:IPv6 直连

在具备 IPv6 的网络环境下,通过主路由直接获取 IPv6 地址,并在旁路由上配置具备鉴权能力的代理协议:

1
2
3
4
5
listeners:
- name: AAA-in-ipv6 # 公网 IPv6 直连入站
type: aaaaaa # 选择带鉴权的安全协议(此处以 trojan 为例)
port: bbbbb # 选择不易被封锁的高位端口
listen: "::" # 监听所有 IPv6。主路由需关闭对应端口防火墙,旁路由仅允许该端口流量入站

随后配合 DDNS-GO 服务,将旁路由获取到的公网 IPv6 地址动态解析至个人域名。在外部具备 IPv6 的环境下,即可通过域名和特定端口安全地访问内网。

配置二:IPv4 NAT打洞

在没有 IPv6 或公网 IPv4 的环境下,主要有以下两种内网穿透方案:

  1. Cloudflare Tunnel (cloudflared):借助 Cloudflare 节点网络进行反向代理,无需公网 IP 和端口映射,适合作为备用兜底方案,但国内访问延迟较高且速率一般。
  2. NATMap (P2P 打洞):利用运营商 NAT 策略进行直连打洞。打通后延迟极低,体验近乎直连公网。

前置要求:NATMap 打洞要求家宽的 NAT 类型为 NAT1 (Full Cone),且通常需要在主路由中将旁路由设置为 DMZ 主机。若网络架构不支持,请考虑其它中转方案(如 cloudflared)。

以下分享基于 NATMap 配合 Cloudflare Worker 与客户端 Sub-Store,实现外网节点随 NATMap 端口变化动态更新的自动打洞方案(此思路利用 Gemini 协助,实测可行)。

1. 配置 Cloudflare Worker

在 Cloudflare 面板创建 Worker,并绑定一个 KV 命名空间(如 NATMAP_KV),在 Settings -> Variables 中添加 SECRET_TOKEN 环境变量,设置一个强密码。

将以下代码发布至 Worker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const userAgent = request.headers.get("User-Agent") || "";

// 基础防爬:拦截空 UA 或明显脚本特征(放行 Sub-Store 和正常的手机客户端)
if (!userAgent || (userAgent.includes("curl") && request.method === "GET")) {
return new Response("Access Denied", { status: 403 });
}

// [GET] 供 Sub-Store 获取最新地址,路径: /sub?token=你的密码
if (request.method === "GET" && url.pathname === "/sub") {
const token = url.searchParams.get("token");
if (token !== env.SECRET_TOKEN) {
return new Response("Unauthorized", { status: 401 });
}

// 从 KV 数据库读取最新打洞地址
const address = await env.NATMAP_KV.get("natmap_address");
if (!address) {
return new Response("No node available yet.", { status: 404 });
}

return new Response(address, {
headers: { "Content-Type": "text/plain; charset=utf-8" }
});
}

// [POST] 供路由器端的 NATMap 脚本推送地址更新。路径: /update
if (request.method === "POST" && url.pathname === "/update") {
const authHeader = request.headers.get("Authorization");

// 验证 Bearer Token
if (authHeader !== `Bearer ${env.SECRET_TOKEN}`) {
return new Response("Unauthorized", { status: 401 });
}

const newAddress = await request.text();
// 简单参数校验(格式必须包含冒号 IP:端口)
if (!newAddress || !newAddress.includes(":")) {
return new Response("Bad Request: Invalid format", { status: 400 });
}

// 写入 KV 数据库
await env.NATMAP_KV.put("natmap_address", newAddress);
return new Response("Update Success: " + newAddress, { status: 200 });
}

return new Response("Not Found", { status: 404 });
},
};

2. 旁路由 NATMap 触发脚本

在旁路由配置 NATMap 任务。当 NAT 端口发生变化时,触发执行该脚本并将最新的 IP:Port 自动推送给 CF Worker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/sh

# NATMap 传入的公网 IP 和 端口参数
PUBLIC_IP=$1
PUBLIC_PORT=$2

if [ -z "$PUBLIC_IP" ] || [ -z "$PUBLIC_PORT" ]; then
echo "Error: Missing IP or Port"
exit 1
fi

# ================= 你的配置区 =================
# 替换为你的 Worker 域名和刚刚设定的 Token 密码
WORKER_URL="https://你的worker前缀.你的用户名.workers.dev"
SECRET_TOKEN="你的自定义密码"
# ==============================================

# 拼接为 IP:端口 格式
PAYLOAD="${PUBLIC_IP}:${PUBLIC_PORT}"

# 发送 POST 请求至 Cloudflare Worker
curl -s -X POST "${WORKER_URL}/update" \
-H "Content-Type: text/plain" \
-H "Authorization: Bearer ${SECRET_TOKEN}" \
-d "${PAYLOAD}"

# 记录本地日志
echo "$(date): Updated natmap address to ${PAYLOAD}" >> /var/log/natmap_update.log

3. 客户端 Sub-Store 节点动态更新

在客户端的 Sub-Store 中编写脚本操作,每次更新订阅时自动拉取最新的打洞端口并覆写至指定的家宽入口节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
async function operator(proxies) {
// ================= 你的配置区 =================
// 替换为你的 CF Worker 订阅地址及 Token 参数
const apiUrl = "https://你的worker前缀.你的用户名.workers.dev/sub?token=你的自定义密码";

// 你在 Sub-Store 里建立的基础代理节点名称
const targetNodeName = "Home_Intranet";
// ==============================================

try {
// 请求 Cloudflare API 获取最新的 IP 和 端口
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}

const data = await response.text();
const parts = data.trim().split(":");

if (parts.length === 2) {
const newIp = parts[0];
const newPort = parseInt(parts[1], 10);

// 遍历代理节点数组,匹配目标节点并修改目标地址
proxies.forEach(proxy => {
if (proxy.name === targetNodeName) {
proxy.server = newIp;
proxy.port = newPort;
// (可选)动态修改节点名称,以便在客户端列表直观展示当前连接的端口
proxy.name = `${targetNodeName} [${newPort}]`;
}
});
}
} catch (e) {
// 请求抛出异常时静默捕获,Sub-Store 将平滑回退使用原节点配置(或最近一次成功获取的本地缓存)
console.log("Fetch natmap port failed: " + e.message);
}

// 必须返回原数组,以保证后续 Sub-Store 处理流继续执行
return proxies;
}