This is a modified V2Ray-core maintained by myself.
Updates from the upstream will be merged periodically.
- OSPFv2 support when act as an on-demand transparent proxy.
- DNS-route capability.
- Conn-track capability for routing decision.
- HTTP health-check inbounds.
If you have any questions which are not related to features described above, please submit it to upstream project.
Project V is a set of network tools that helps you to build your own computer network. It secures your network connections and thus protects your privacy.
- Documentation and Newcomer's Instructions
- Welcome to translate V2Ray documents via Transifex
先叠个甲,本方案配置较为繁琐,有硬件要求,且深度涉及计算机网络原理,仅适用于有进阶网络知识的用户使用。
本项目旨在解决:使用透明代理进行网关科学的模式下,网关直接使用软路由带来的稳定性问题,以及性能问题。
如果你不在意:默认使用软路由作为你的家庭网关,且可以接受折腾软路由时造成全部网络中断/抖动的问题,则本文方案可能不适合你。
核心理念为:仅需要科学的流量会被转发至软路由处理,其余流量由主路由直接发出。 主路由使用常规硬路由以保证性能和稳定性。
因此,对于旁路由的性能要求降到了一个非常低的水平,同时,软路由的任何故障对于网络的影响也基本消除。
类似的,以“按需转发流量”作为核心理念的方案有:FakeDNS。 我也试用过相当长一段时间,但其存在几个我无法接受的问题:
- FakeIP污染,例如:大陆白名单时,默认污染其他所有域名
- 旁路由故障/修改配置重启时,FakeIP污染会持续一段时间无法立即清除
- 需要手动在主路由上维护静态路由条目
- 无法灵活应对Telegram这种不使用系统DNS的软件
- 旁路由入侵网络拓扑,无法快速移除
相比之下,本方案具有以下优点:
- 全真IP,不存在FakeIP污染,同时解决国内环境的DNS污染
- 支持基于规则文件的IP路由规则,灵活应对Telegram类似的软件
- 分流黑白名单模式可按喜好配置,无任何副作用
- 默认支持 srcIP -> dstIP 作为pattern的Connection-track,路由决策无需Sniffing
- 旁路由可插拔,生效路由条目由旁路由自动通告,无需维护静态路由条目,网络拓扑可自动容灾
- 整体方案扩展能力强,可结合硬路由和软路由的各自特点,并充分利用各自的优势
特性 | 软路由 | 硬路由 | 本方案(按需旁路) |
---|---|---|---|
科学能力 | 强,取决于ROM | 弱,配置复杂+不灵活 | 强,包含所有V2Ray功能 |
性能 | 取决于硬件配置 | 远强于同规格软路由 | 强,直连性能等同于硬路由,科学性能取决于软路由配置 |
功耗 | 高 | 低 | 较低 |
NAT情况 | 取决于软件承诺 | 一般为FullClone | 直连流量与硬路由无异,科学流量取决于V2Ray承诺 |
容灾 | 无,全部断网 | 配置复杂 | 自动恢复拓扑,科学流量可降级为直连 |
稳定性 | 低,重启/故障影响全部网络 | 高,仅受不可抗力影响 | 高,旁路由故障/重启不影响主干网络 |
扩展能力 | 低下限高上限,取决于ROM | 高下限,支持各种电信级玩法 | 高,可充分利用软硬路由各自优势 |
下面的拓扑图表示了本项目中旁路由的工作方式。
图示过程描述了一台内网设备是如何在
- 不修改默认网关
- 不修改默认DNS
- 不安装代理软件
的情况下,无感知的通过网关透明代理,科学访问www.google.com的。
粉色的箭头表示DNS请求流程,绿色的箭头表示真正的访问流程(即实际传输数据的TCP/UDP过程)。
简单来说,主路由会把所有来自LAN的DNS请求,通过防火墙的DNAT规则转发给旁路由处理, 旁路由会使用DNS请求的域名+DNS解析结果,预先进行一次路由决策:
若域名+DNS解析后的IP
- 匹配代理出口的Tag:通过OSPFv2动态路由协议,向主路由通告目标IP的下一跳为旁路由,返回DNS解析结果,同时添加基于 srcIP -> dstIP 的conn-track规则
- 不匹配代理Tag:直接返回DNS解析结果即可
这一过程被我称作DNS Route:通过分析来自客户端的DNS请求,按需产生一条通往目标域名IP的路由规则。 同时,得益于动态添加的,基于源-目标IP的conn-track规则,后续连接出口的匹配可以直接跳过V2Ray的Sniffing,在ECH普及的未来仍可做到精准域名分流。
而且,由于科学访问的路由表由OSPF动态路由协议维护,旁路探活失败时主路由会自动恢复网络拓扑, 配合探活脚本自动回切防火墙的DNS转发规则,则可以完美的消除旁路故障对于主干网络的影响。
目前实现中,DNS Route的掩码为/32,有效时间为6个小时,6小时内没有任何DNS请求或者实际流量,则会自动废弃对应路由条目。 实际使用中,生效路由条目约为400-600条,配合fastTrack,对于主路由的性能影响可以忽略不计。
本项目中,V2Ray将被配置为旁路由透明代理使用,需要你事先掌握/具备以下条件:
- 理解什么是透明代理
- 如何配置V2Ray以透明代理模式工作
- 理解单臂路由(旁路由)的基本工作原理
- 理解路由设备的工作原理,熟悉路由决策过程,理解路由表及防火墙基本原理
- 熟悉nftables,具备基本的linux操作能力
- 一台支持OSPFv2动态路由协议的主路由,且主路由需要支持策略路由(某些文章可能称为标记路由)。
- 一台可运行V2Ray的Debian Linux作为单臂旁路由
以下的使用说明中,采用的硬件配置为
主路由(ROSv6):MikroTik hAPac2 RBD52G-5HacD2HnD (RouterOS v6.49.14)
主路由(ROSv7):MikroTik RB5009UG+S+IN (RouterOS v7.15.0)
旁路由:Debian 12 Linux with 2-core 2GiB RAM (LXC PVE v8.2.4 on N100)
推荐主路由使用ROS,旁路由使用Debian11及以上的linux系统,至少分配1c1g的资源。
请参考如下拓扑,配置好主路由与旁路由。
核心诉求只有两点:
- 主路由与旁路由需要和LAN设备隔离出一个网段,且这个网段只有主路由和旁路由两个设备,这个是必须要求。
- 主路由与旁路由IP固定
简单来说,旁路由配置主要有以下步骤:配置透明代理,配置OSPF相关参数/健康检查端口,配置IP masquerade等。
以下所有命令中 ${IFNAME}
均代表软路由和主路由连接的网卡名称,可以使用 ifconfig
ip link
等命令查看。
使用时需要替换成你自己环境中的网卡名称。
linux系统上推荐使用 fhs-install-v2ray 下载并安装V2Ray
然后下载本项目Release页
里的修改版,替换v2ray可执行文件即可,v2ray的默认安装路径为 /usr/local/bin/v2ray
透明代理的配置教程 已经很多,我就不再赘述了,请参考已有教程自行完成透明代理的nftables配置,核心要求只有以下几个:
-
只支持TPROXY模式的透明代理,请勿配置成REDIRECT模式。
-
需要拦截UDP53的DNS查询请求,并转交给V2Ray内置DNS处理
-
强烈推荐替换V2Ray的默认geoip/geosite规则文件为社区增强版本的 Loyalsoldier/v2ray-rules-dat
V2Ray的默认dat文件路径为
/usr/local/share/v2ray
,下载对应dat文件直接替换即可。 -
需要参考本节末尾,额外赋予 V2Ray
NET_RAW
权限,否则无法正常收发OSPF数据包
另外,建议按照教程要求,修改v2ray的最大文件描述符限制, 避免在处理UDP流量时出现问题。
在 /etc/systemd/system/v2ray.service.d/11-extra-capability.conf
里创建以下内容
[Service]
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
保存并退出,执行 systemctl daemon-reload
以便配置生效
此模块为本项目完全独立开发的部分,得益于V2Ray良好的模块化设计,最终以dnsCircuit
模块形式嵌入了V2Ray中,需要在配置文件中写入指定配置方可开启。
V2ray的默认配置文件路径为 /usr/local/etc/v2ray/config.json
不开启此模块时,此修改版的V2Ray与官方版本无异。
配置文件示例(节选),供参考。
以下内容核心有两块必填,其余配置请按自己实际情况填写。
dnsCircuit
部分,是本项目核心的功能模块,用于启用DNS Route,并配置要监听的inbounds/outbounds等等inbounds
中需要配置一个HTTP健康检查入口,用于主路由检查旁路软件健康状况,这块后续主路由配置部分会再提到
{
// 最重要的部分
// 用于DNS route的配置
"dnsCircuit": {
//(必填)DNS outbound 的Tag,用于分析DNS请求并预决策路由。
"dnsOutboundTag": "dns-out",
//(必填)用于conn-track的inbound,目前只支持dokodemo-door协议的inbound。
// 填写透明代理的inboundTag即可
"inboundTags": [
"transparent"
],
//(outboundTags和balancerTags不能同时为空)
// 用于conn-track的outbound,同时,DNS路由结果命中此outboundTag的流量都会被转发至旁路由。
// 填写代理服务器的outboundTag即可
"outboundTags": [
"proxy"
],
//(outboundTags和balancerTags不能同时为空)
// 用于conn-track的balancer,同时,DNS路由结果命中此balancerTag的流量都会被转发至旁路由。
"balancerTags": [
"jp-balancer"
],
//(可选)固定通告某些IP段,目标IP在此范围内的流量都会被转发至旁路由。
"persistentRoute": [
// 从规则文件中载入电报的服务器IP段,从而实现内网设备自动通过旁路由访问电报。
"geoip:telegram",
// 也可以直接以CIDR形式书写要转发至旁路的IP段,这块只是示例,请按自己实际情况填写。
"10.0.0.0/8"
],
//(可选)不活跃路由的清理时间(秒),不活跃时间超过这个数值后,对应路由条目和conn-track规则会被删除。
// 默认:21600秒(6个小时)
"inactiveClean": 21600,
//(必填)OSPF设置
// 需要填写软路由和主路由相连的网卡名称,以及软路由自己的IP和网段的掩码,以CIDR形式填写。
"ospfSetting": {
//(必填)软路由上的网卡名称
"ifName": "ens160",
//(必填)软路由自己的IP+子网掩码
"address": "192.168.87.2/24"
}
},
"inbounds": [
{
//(必填)用作代理软件健康检查
"tag": "health-check",
// 必须填写 0.0.0.0 否则无法接受来自旁路由的请求
"listen": "0.0.0.0",
// 端口可随意填写,注意和后面主路由配置的健康检查端口对应即可
"port": 54321,
//(必填)注意protocol一定要填写http-healthcheck
"protocol": "http-healthcheck",
"settings": {
"timeout": 3
}
},
{
//(必填)透明代理 inbound,本方案中必须填写
"tag": "transparent",
"listen": "127.0.0.1",
"port": 12345,
"protocol": "dokodemo-door",
"settings": {
"network": "tcp,udp",
"followRedirect": true
},
"sniffing": {
// 本方案无需开启嗅探
"enabled": false,
},
"streamSettings": {
"sockopt": {
// 透明代理必须使用 TPROXY 方式
"tproxy": "tproxy",
"mark": 255
}
}
}
// ... 省略不相干inbounds
],
"outbounds": [
{
// 直连流量
"tag": "direct",
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIPv4"
},
"streamSettings": {
"sockopt": {
"mark": 255
}
}
},
{
// 代理出口
"tag": "proxy",
"protocol": "vmess",
"settings": {
"vnext": [
{
"address": "your.proxy.server",
"port": 65535,
"users": [
{
"id": "************************",
"security": "auto"
}
]
}
]
},
"streamSettings": {
"sockopt": {
"mark": 255
}
}
},
{
//(必填)dns outbound,用于接受DNS请求
"tag": "dns-out",
"protocol": "dns",
"streamSettings": {
"sockopt": {
"mark": 255
}
}
}
// ... 省略不相干outbounds
],
// 此处示例路由为GFW黑名单模式
"routing": {
// 建议使用此规则
"domainStrategy": "IPIfNonMatch",
"domainMatcher": "mph",
"rules": [
{
// 直连 123 端口 UDP 流量(NTP 协议)
"type": "field",
"inboundTag": "transparent",
"port": 123,
"network": "udp",
"outboundTag": "direct"
},
{
// 劫持 53 端口 UDP 流量,使用 V2Ray 的 DNS
"type": "field",
"inboundTag": "transparent",
"port": 53,
"network": "udp",
"outboundTag": "dns-out"
},
{
// 直连 国内网站
"type": "field",
"domain": [
"domain:ntp.org",
"geosite:china-list",
"geosite:cn",
"geosite:tld-cn",
"geosite:apple",
"geosite:apple-cn",
"geosite:google-cn",
"geosite:icloud",
"geosite:category-games@cn",
// steam下载走国内CDN
"domain:steamserver.net",
"geosite:geolocation-cn"
],
"outboundTag": "direct"
},
{
// 直连 国内IP
"type": "field",
"ip": [
"geoip:cn"
],
"outboundTag": "direct"
},
{
// telegram IP 走代理
// 用于和dnsCircuit 的 persistentRoute 相配合
"type": "field",
"ip": [
"geoip:telegram"
],
"outboundTag": "proxy"
},
{
// 墙的域名走代理
"type": "field",
"domain": [
"geosite:gfw",
"geosite:geolocation-!cn"
],
"outboundTag": "proxy"
},
{
//(重要,必填)
// 注意顺序,建议紧跟在域名路由规则之后。
// DNS Route 动态维护的 conn-track 规则,实际使用的是V2Ray router的 srcIP - dstIP 匹配规则。
// 格式为:
// from: dynamic-ipset:dnscircuit-conntrack-src-{outboundTag}
// to: dynamic-ipset:dnscircuit-conntrack-dest-{outboundTag}
"type": "field",
"source": "dynamic-ipset:dnscircuit-conntrack-src-proxy",
"ip": "dynamic-ipset:dnscircuit-conntrack-dest-proxy",
"outboundTag": "proxy"
},
{
//(重要,必填)
// 注意顺序,建议写在所有路由规则最后。
// DNS Route 路由默认出口,当一个incoming连接没有被conn-track规则命中时,会被此规则兜底。
// 照着写即可
"type": "field",
"ip": "dynamic-ipset:dnscircuit-dest-default",
"outboundTag": "proxy"
}
// ... 其他路由规则省略
]
},
"dns": {
// DNS应该使用国内外DNS分流的配置。
// 此处暂时省略,后续完整配置示例会给出。
}
}
此配置的主要目的是,配合主路由上的策略路由规则,直接转发透明代理不处理的IP数据包(即,TCP/UDP协议以外的IP报文), 以及直接送出旁路由本身发出的流量,避免形成路由环路。
直接按要求设置即可,主路由配置时会再提到这部分。
运行命令,开启内核的IPv4包转发功能,并设置从旁路由网卡发出的IP包做masquerade,注意替换IFNAME
为你自己的网卡名称。
# 开启内核IPv4包转发
sysctl -w net.ipv4.ip_forward=1
# 添加一个名为v2ray的table
# 如果你在配置透明代理时已经添加过这个table
# 则这个命令不出意外会执行失败,也可以直接跳过
nft add table v2ray
# 在table v2ray的POSTROUTING链上挂一个nat hook
nft add chain v2ray postrouting { type nat hook postrouting priority 0 \; }
# 向v2ray POSTROUTING链中添加一条规则,从 ${IFNAME} 网卡发出的流量全部进行masquerade
nft add rule v2ray postrouting oif ${IFNAME} masquerade
主要是内核参数,透明代理策略和nftables的规则持久化。
编辑 /etc/sysctl.conf
,添加net.ipv4.ip_forward=1
,
执行命令
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
执行 systemctl enable v2ray
即可
然后用 systemctl status v2ray
确认设置,有出现enabled字样即可
root@debian:~# systemctl status v2ray
● v2ray.service - V2Ray Service
Loaded: loaded (/etc/systemd/system/v2ray.service; enabled; vendor preset: enabled)
Drop-In: /etc/systemd/system/v2ray.service.d
└─10-donot_touch_single_conf.conf, 11-extra-capability.conf, 20-ulimit.conf
Active: active (running) since Thu 2024-05-09 23:16:40 HKT; 17h ago
Docs: https://www.v2fly.org/
Main PID: 195546 (v2ray)
Tasks: 9 (limit: 2337)
Memory: 215.3M
CPU: 7min 8.496s
CGroup: /system.slice/v2ray.service
└─195546 /usr/local/bin/v2ray run -config /usr/local/etc/v2ray/config.json
May 10 17:07:05 debian v2ray[195546]: 2024/05/10 17:07:05 192.168.88.192:44383 accepted udp:192.168.87.2:53 [dns-out]
首先,检查nftables配置,运行命令 nft list ruleset
你的配置应该和下面的输出类似,注意不要照抄。按自己实际情况确认。
OUTPUT Chain
的配置,其目的是拦截旁路由本身主动向外发出的流量并代理,但在一些特殊情况下会导致一些预期外的行为,包括但不限于:
-
有公网IPv4的情况下,主路由暴露端口,并直接DNAT到旁路由的某一端口。会发现旁路由不响应任何来自公网IP的连接请求,其原因是响应报文被
OUTPUT Chain
拦截所致。解决办法是:修改
OUTPUT Chain
规则,使其只拦截本机的UDP 53的DNS查询流量即可,但也会导致旁路由本身不在透明代理范畴内,不过基本没什么影响。有需要的可按照上文所述自行调整nftables规则,此处不再赘述。
root@debian:~# nft list ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy accept;
}
chain forward {
type filter hook forward priority filter; policy accept;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
table ip v2ray {
chain prerouting {
type filter hook prerouting priority filter; policy accept;
ip daddr { 127.0.0.1, 224.0.0.0/4, 255.255.255.255 } return
meta l4proto tcp ip daddr 192.168.0.0/16 return
ip daddr 192.168.0.0/16 udp dport != 53 return
meta mark 0x000000ff return
meta l4proto { tcp, udp } meta mark set 0x00000001 tproxy to 127.0.0.1:12345 accept
}
chain output {
type route hook output priority filter; policy accept;
ip daddr { 127.0.0.1, 224.0.0.0/4, 255.255.255.255 } return
meta l4proto tcp ip daddr 192.168.0.0/16 return
ip daddr 192.168.0.0/16 udp dport != 53 return
meta mark 0x000000ff return
meta l4proto { tcp, udp } meta mark set 0x00000001 accept
}
chain postrouting {
type nat hook postrouting priority filter; policy accept;
oif "ens160" masquerade
}
}
table ip filter {
chain divert {
type filter hook prerouting priority mangle; policy accept;
meta l4proto tcp socket transparent 1 meta mark set 0x00000001 accept
}
}
确认无误后,保存规则至 /etc/nftables/rules.v4
,需要执行以下命令
nft list ruleset > /etc/nftables/rules.v4
然后,新建systemd service,在 /etc/systemd/system/tproxy.service
创建以下内容,
目的是通过systemd管理自启任务。
[Unit]
Description=Tproxy rule
After=network.target
Wants=network.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/ip rule add fwmark 1 table 100 ; /sbin/ip route add local default dev lo table 100 ; /sbin/nft -f /etc/nftables/rules.v4
ExecStop=/sbin/ip rule del fwmark 1 table 100 ; /sbin/ip route del local default dev lo table 100 ; /sbin/nft flush ruleset
[Install]
WantedBy=multi-user.target
设置开机自启动,执行 systemctl enable tproxy
即可。
设置完成后,可以用 systemctl status tproxy
确认有enabled字样即可
root@debian:~# systemctl status tproxy
● tproxy.service - Tproxy rule
Loaded: loaded (/etc/systemd/system/tproxy.service; enabled; vendor preset: enabled)
Active: active (exited) since Mon 2023-10-09 22:10:30 HKT; 7 months 0 days ago
Main PID: 714 (code=exited, status=0/SUCCESS)
Tasks: 0 (limit: 2337)
Memory: 0B
CPU: 0
CGroup: /system.slice/tproxy.service
主路由配置基本是四块:开启OSPF动态路由协议,防止路由环路,DNS转发,以及旁路由探活和探活失败时自动回切DNS的脚本。
主路由怎么配置正常上网我就不赘述了,本文默认你已经会使用ROS配置PPPoE拨号或者直接DHCP上网。
由于v6和v7的OSPF配置差别过大,下面会同时给出两种系统的配置示例。只支持OSPFv2,即IPv4协议。
进入 Routing -> OSPF
菜单,如果是v7的ROS系统,参考下面选OSPFv2,本项目目前只支持IPv4
进入 Interfaces
,选择和旁路由直接相连的接口,我这里旁路由和主路由接口都属于一个网桥,所以直接选网桥即可,如果你没用网桥,那就选接口。
验证选None,不开启验证,优先级填1,其他默认即可,务必保证HelloInterval=10
且 RouterDeadInterval=40
,否则会影响邻接。
进入 Instances
,填写主路由的RouterID,这里直接写主路由相对于旁路由网段的IP地址即可,例如在我的拓扑中,这里填写主路由IP 192.168.87.1
。
其他全默认即可,见下图
进入 Network
,填写主路由和旁路由所属的网段以及掩码,Area选择默认的backbone即可,如下图所示
至此完成OSPF配置,等待40秒后,你的主路由 Interface - State
应该和上图一样,展示为 Designated Router(即DR)状态。
进入 Routing -> OSPF
菜单,先进入Instances
,新建一个OSPFv2的实例,注意下图的红框内容。
RouterId填写你主路由和旁路由通信的IP地址即可,图中仅供参考不要照抄。
进入 Areas
,填写backbone区域,instance选择上一步创建的instance,Area ID
和其他内容照图填写。
进入 Interface Templates
,选择和旁路由相连的接口名称,选择刚刚创建的area,Networks填写你规划的主路由和旁路由的网段。
其余配置照图填写
至此完成OSPF配置,等待40秒后,你的主路由 Interfaces
中,应该会出现一个state为DR(Designated Router)动态条目。代表OSPF配置成功。
因为透明代理只能处理TCP和UDP流量,其他类型的IP数据包会由linux内核直接转发, 而主路由上的OSPF动态路由表,会无条件将所有OSPF通告目标IP的数据报文下一跳给旁路,旁路由的默认网关又是主路由。 因此,在极少数情况下,这个互相甩锅的过程,会造成路由环路的问题。
当然,V2Ray配置有误也会导致环路,这个暂且按下不表。
为了避免环路,需要识别出旁路由发出的流量,跳过OSPF的动态路由规则进行匹配。
这块就需要旁路由转发IP报文时,无条件做IP masquerade,然后,用主路由的策略路由功能进行分流,具体步骤为:
-
主路由创建一个新的路由表,记为
side-anti-loop
,此路由表中需要填写默认路由为WAN口,以及本地LAN IP段所属的网桥或接口。注意红框中的内容,如果你本地有其他网段,需要一并以静态路由形式填入,注意选择所属接口。这块照图自己写吧,就不给命令了。
⚠️ :如果使用ROSv7系统,需要到Routing -> Tables
新建路由表,才能在IP -> Routes
中使用,注意新建的路由表也要勾选FIB。新建的路由表中,其所有条目状态(最前面的字母),应该为
AS
,即active & static
,如果状态不对请自行排查。 -
主路由创建策略路由规则:来自于旁路由IP 192.168.87.2的数据包,仅查询路由表
side-anti-loop
ROSv6的对应命令如下,其中
side-router
是我旁路由所在的网桥,192.168.87.2
是我的旁路由IP,你可以视情况改成接口/你自己的旁路由IP。不要照抄。/ip route rule add src-address=192.168.87.2 interface=side-router action=lookup-only-in-table table=side-anti-loop
⚠️ :ROSv7系统的策略路由配置在Routing -> Rules
菜单中,对应命令示例如下,注意不要照抄,src-address
和interface
要视情况改成你自己的旁路由IP和接口。/routing rule add src-address=192.168.87.2 interface=side-router action=lookup-only-in-table table=side-anti-loop
至此,你应该已经完成了主路由的策略路由配置:所有来自于旁路由IP的数据包,将仅查询side-anti-loop
这个路由表,
甚至包括V2Ray配置错误时(例如:错误的将应该代理的流量直连发出)也不会环路,从根本上避免了路由环路的产生。
据我了解,包括FakeDNS以及DNSMasquerade+GFW IPset在内的一众旁路由方案,应该都没有考虑这个问题,对于TCP/UDP以外的流量,会直接产生路由环路。
这一步的作用是,主路由拦截所有内网设备发出的DNS请求,并将其转发给旁路由,由旁路由解析并返回,同时做DNS Route决策。 对于整个项目的目标来说,是至关重要的一步,其主要目的是:
- 嗅探内网设备要访问的域名,提前建立路由表转发规则,达成按需转发流量的目的
- 内网设备零配置,对于科学上网完全无感知
- 科学或者旁路故障切换时,仅网关进行切换即可
所以这里的配置就很简单了,只需要排除来源为旁路由IP的DNS查询流量, 然后将所有目的为UDP53的流量DNAT给旁路由即可。
直接上命令,添加DNAT rule,注意替换目标IP为你旁路由的IP,以及,注意一定要给这条规则,添加注释为:DnsForward, 这条注释会用作下面探活切换时,防火墙的DNAT规则匹配。
/ip firewall nat add chain=dstnat protocol=udp dst-port=53 src-address=!192.168.87.2 action=dst-nat to-addresses=192.168.87.2 to-ports=53 comment="DnsForward"
这会导致V2Ray的DnsRoute里出现比较奇怪的来源IP记录。要禁止接受WAN口DNS请求, 需要在Firewall - Filter - Forward chain,添加 DST UDP53 且in-interface-list WAN action DROP的规则即可。不再赘述。
这一步是配置旁路由故障时的自动容灾措施,目的是在旁路由故障时,自动切换DNS为ISP默认DNS,保持主干网络完全可用。
还记得之前在V2Ray的inbounds里,建立了一个protocol名为http-healthcheck
的代理入口么,那就是本项目用来探测V2Ray实例是否正常工作的探活端点。
相比于IP探活,HTTP探活直接检测了代理软件的存活情况,更加精准可靠。
以下内容仅适用于ROSv6的系统,v7的系统可以直接使用Tools -> Netwatch
,直接配置旁路由IP+探活端口,HTTP方式探活即可。
在ROS的 System -> Scripts
菜单中,创建一个名为 probeSide
的脚本,内容填写下面的代码。
注意端口号要和V2Ray配置中的探活端口号一致。
do {
:local result [/tool fetch url=("http://health-check.side.local:54321/health") mode=http duration=10s output=user as-value];
:if ($result->"status" = "finished") do={
:if ([/ip firewall nat get [/ip firewall nat find where comment="DnsForward"] disabled]) do={
/log info "Side-Router health probe OK - Turn ON DNS Forward";
/ip firewall nat enable [/ip firewall nat find where comment="DnsForward"];
/ip dns set allow-remote-requests=no;
}
}
} on-error={
:if (![/ip firewall nat get [/ip firewall nat find where comment="DnsForward"] disabled]) do={
/log info "Side-Router health probe FAILED - Turn OFF DNS Forward";
/ip firewall nat disable [/ip firewall nat find where comment="DnsForward"];
/ip dns set allow-remote-requests=yes;
}
}
然后,在 IP -> DNS -> Static
菜单中填入一个静态DNS记录,指向旁路由IP。如下图所示。注意域名不要填错,以及旁路由IP填你自己的IP。
health-check.side.local 192.168.87.2
最后,在 System -> Scheduler
中创建一个定时任务,设置间隔为一分钟,执行下面的命令即可
/execute script="probeSide"
ROSv7可直接使用Tools - Netwatch
新建探活任务,照下图设置即可,注意host和port填写旁路由的IP地址和探活端口。
然后,别忘了在UP
和DOWN
事件的脚本里填写如下内容
On Up
/log info "Side-Router health probe OK - Turn ON DNS Forward";
/ip firewall nat enable [/ip firewall nat find where comment="DnsForward"];
/ip dns set allow-remote-requests=no;
On Down
/log info "Side-Router health probe FAILED - Turn OFF DNS Forward";
/ip firewall nat disable [/ip firewall nat find where comment="DnsForward"];
/ip dns set allow-remote-requests=yes;
至此,你已经完成了主路由和旁路由的拓扑配置,下一步,是时候完成V2Ray的完整配置了。
理论上,本方案中V2Ray可按喜好配置GFW黑名单,或者大陆白名单代理模式。 但由于其运行在网关上,如果你不想梯子账单爆炸,一般还是建议使用GFW黑名单+自定义黑名单模式。
- 2024/08/02: 添加了负载均衡的配置示例,用于简化使用负载均衡作为出口时,conn-track规则书写繁琐的问题。提高配置可维护性。
- 2024/08/15: 添加了statsServer配置,可配合个人修改版的v2ray-exporter/prometheus/grafana观测V2Ray出口及客户端实时流量或趋势。
- 2024/09/10: 添加v5cfg的配置支持,但未做测试,不保证完全可用,暂不提供配置指导。
以下是一个完整的V2Ray配置示例,包含了大陆域名白名单的DNS分流+尝试使用大陆DNS解析未知域名+海外DNS兜底+GFW黑名单路由模式+特殊域名走不同代理出口。 请根据自己需求酌情修改。
{
"log": {
"loglevel": "warning"
},
// 最重要的部分
// 用于DNS route的配置
"dnsCircuit": {
//(必填)DNS outbound 的Tag,用于分析DNS请求并预决策路由。
"dnsOutboundTag": "dns-out",
//(必填)用于conn-track的inbound,目前只支持dokodemo-door协议的inbound。
// 填写透明代理的inboundTag即可
"inboundTags": [
"transparent"
],
//(outboundTags和balancerTags不能同时为空)
// 用于conn-track的outbound,同时,DNS路由决策结果命中这些outboundTag的流量都会被转发至旁路由。
// 填写代理服务器的outboundTag即可
"outboundTags": [
"proxy-default",
"proxy-jp"
],
//(outboundTags和balancerTags不能同时为空)
// 用于conn-track的balancer,同时,DNS路由决策结果命中这些balancerTag的流量都会被转发至旁路由。
// 填写出口的balancerTag即可
"balancerTags": [
"usa-balancer"
],
//(可选)固定通告某些IP段,目标IP在此范围内的流量都会被转发至旁路由。
"persistentRoute": [
// 从规则文件中载入电报的服务器IP段,从而实现内网设备自动通过旁路由访问电报。
"geoip:telegram"
],
//(可选)不活跃路由的清理时间(秒),不活跃时间超过这个数值后,对应路由条目和conn-track规则会被删除。
// 默认:21600秒(6个小时)
"inactiveClean": 21600,
//(必填)OSPF设置
// 需要填写软路由和主路由相连的网卡名称,以及软路由自己的IP和网段的掩码,以CIDR形式填写。
"ospfSetting": {
//(必填)软路由上的网卡名称,填写你自己的旁路由网卡名称,比如我自己的是ens160
"ifName": "{IFNAME}",
//(必填)软路由自己的IP,以及对应的子网掩码
"address": "192.168.87.2/24"
}
},
"inbounds": [
{
// 流量监测用,如果你不需要,删了它
"tag": "api",
"listen": "127.0.0.1",
"port": 11451,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
},
{
//(必填)用作代理软件健康检查
"tag": "health-check",
// 必须填写 0.0.0.0 否则无法接受来自旁路由的请求
"listen": "0.0.0.0",
// 端口可随意填写,注意和后面主路由配置的健康检查端口对应即可
"port": 54321,
//(必填)注意protocol一定要填写http-healthcheck
"protocol": "http-healthcheck",
"settings": {
"timeout": 3
}
},
{
//(必填)透明代理 inbound,本方案中必须填写
"tag": "transparent",
"listen": "127.0.0.1",
"port": 12345,
"protocol": "dokodemo-door",
"settings": {
"network": "tcp,udp",
"followRedirect": true
},
"sniffing": {
// 本方案无需开启流量嗅探
"enabled": false,
},
"streamSettings": {
"sockopt": {
// 透明代理必须使用 TPROXY 方式
"tproxy": "tproxy",
"mark": 255
}
}
}
// 如果你有主动连接代理软件的需求,
// 这里还可以继续添加socks或者其他类型的inbounds
],
"outbounds": [
{
// 直连流量,也是默认路由出口,
// 路由策略使用GFW黑名单模式时,请务必将直连作为第一个outbound
"tag": "direct",
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIPv4"
},
"streamSettings": {
"sockopt": {
// sock mark不能删,必须和透明代理配置相对应
"mark": 255
}
}
},
{
// 默认代理出口,这里是vmess示例,需要填写你自己的代理服务和对应协议
"tag": "proxy-default",
"protocol": "vmess",
"settings": {
"vnext": [
{
"address": "your.proxy.server",
"port": 65535,
"users": [
{
"id": "************************",
"security": "auto"
}
]
}
]
},
"streamSettings": {
"sockopt": {
// sock mark不能删,必须和透明代理配置相对应
"mark": 255
}
}
},
{
//(必填)dns outbound,用于接受DNS请求
"tag": "dns-out",
"protocol": "dns",
"streamSettings": {
"sockopt": {
// sock mark不能删,必须和透明代理配置相对应
"mark": 255
}
}
},
{
// 另一个代理出口,用于部分域名按需分流出口。
// 如果你只有一个代理服务器,可以删掉这里用默认就行。
// 这里是vmess示例,需要填写你自己的代理服务和对应协议
"tag": "proxy-jp",
"protocol": "vmess",
"settings": {
"vnext": [
{
"address": "your.proxy.server.to.jp",
"port": 65535,
"users": [
{
"id": "************************",
"security": "auto"
}
]
}
]
},
"streamSettings": {
"sockopt": {
// sock mark不能删,必须和透明代理配置相对应
"mark": 255
}
}
},
// 如果你有多个代理出口分流/负载均衡的需求,
// 可以在这里继续添加outbounds
{
// USA代理出口-1,用于部分域名按需分流出口。
// 这里是vmess示例,需要填写你自己的代理服务和对应协议
"tag": "proxy-usa-01",
"protocol": "vmess",
"settings": {
"vnext": [
{
"address": "your.proxy.server.to.usa.01",
"port": 65535,
"users": [
{
"id": "************************",
"security": "auto"
}
]
}
]
},
"streamSettings": {
"sockopt": {
// sock mark不能删,必须和透明代理配置相对应
"mark": 255
}
}
},
{
// USA代理出口-2,用于部分域名按需分流出口。
// 这里是vmess示例,需要填写你自己的代理服务和对应协议
"tag": "proxy-usa-02",
"protocol": "vmess",
"settings": {
"vnext": [
{
"address": "your.proxy.server.to.usa.02",
"port": 65535,
"users": [
{
"id": "************************",
"security": "auto"
}
]
}
]
},
"streamSettings": {
"sockopt": {
// sock mark不能删,必须和透明代理配置相对应
"mark": 255
}
}
}
],
// DNS规则,使用了国内外DNS分流+大陆DNS优先+海外DNS兜底的配置
"dns": {
"queryStrategy": "UseIPv4",
// DNS分流必须使用该策略
"fallbackStrategy": "disabled-if-any-match",
"domainMatcher": "mph",
"hosts": {
// 屏蔽广告域名
"geosite:category-ads-all": "127.0.0.1",
},
// 注意以下配置中,aaa.bbb.ccc.ddd 应该替换为你ISP分配的DNS
// 如果你使用ROS PPPoE拨号,那么在 IP -> DNS 菜单中,DynamicServers就是你的运营商DNS。
// 如果你ROS用DHCP上网,那么运营商DNS直接填写光猫网关IP即可。
"servers": [
{
// 默认国内DNS服务器,应该使用你ISP提供的DNS服务器。
// 用于解析没有命中任何规则的域名,只接受解析结果为大陆IP。
// 警告:对于未记录大陆域名白名单中的海外域名,这个配置会产生DNS泄露,请自己评估是否可以接受
"address": "aaa.bbb.ccc.ddd",
"port": 53,
"expectIPs": [
"geoip:cn"
],
"tag": "dns-china-try-resolve",
},
{
// 默认国内DNS服务器的backup,这里使用了DNSPod,你可以按自己喜好调整。
// 用于解析没有命中任何规则的域名,只接受解析结果为大陆IP。
// 警告:对于未记录在大陆域名白名单中的海外域名,这个配置会产生DNS泄露,请自己评估是否可以接受
"address": "119.29.29.29",
"port": 53,
"expectIPs": [
"geoip:cn"
],
"tag": "dns-china-try-resolve-backup",
},
{
// 默认DNS服务器,所有没有命中规则的域名,都会使用这个DNS服务器解析。
// 示例使用Cloudflare DNS,你可以根据自己喜好调整
"address": "1.1.1.1",
"port": 53,
"tag": "dns-default-abroad"
},
{
// 特殊的域名规则,例如:此配置表示,pixiv的域名,以及jp结尾的域名使用Cloudflare的DNS
// 然后标记其DNS流量为dns-jp-site,稍后在routing模块中匹配其走JP出口即可,
"address": "1.1.1.1",
"port": 53,
"domains": [
"geosite:pixiv",
"regexp:.*\\.jp$"
],
"tag": "dns-jp-site"
},
{
// Twitter / Netflix 等特殊网站DNS规则。
// 用于分流DNS流量,后续routing模块走特殊出口
"address": "1.1.1.1",
"port": 53,
"domains": [
"geosite:twitter",
"geosite:facebook",
"geosite:netflix"
],
"tag": "dns-usa-site"
},
{
// 特殊域名规则,例如:
// 群晖的两个顶级域名走114的DNS
"address": "114.114.114.114",
"port": 53,
"domains": [
"domain:synology.com",
"domain:synology.cn",
],
"tag": "dns-china-special"
},
{
// 大陆域名白名单,规则中的域名优先使用ISP DNS解析
"address": "aaa.bbb.ccc.ddd",
"port": 53,
"domains": [
"domain:ntp.org",
// 用于国内SteamCDN下载
"domain:steamserver.net",
"geosite:mihoyo-cn",
"geosite:china-list",
"geosite:cn",
"geosite:tld-cn",
"geosite:apple",
"geosite:apple-cn",
"geosite:google-cn",
"geosite:icloud",
"geosite:category-games@cn",
"geosite:geolocation-cn"
],
"tag": "dns-china-site"
},
{
// 大陆域名白名单的backup,规则中的域名优先使用DNSPod解析
// 你可以根据自己喜好修改
"address": "119.29.29.29",
"port": 53,
"domains": [
"domain:ntp.org",
// 用于国内SteamCDN下载
"domain:steamserver.net",
"geosite:mihoyo-cn",
"geosite:china-list",
"geosite:cn",
"geosite:tld-cn",
"geosite:apple",
"geosite:apple-cn",
"geosite:google-cn",
"geosite:icloud",
"geosite:category-games@cn",
"geosite:geolocation-cn"
],
"tag": "dns-china-site-backup"
},
{
// 特殊的域名规则,例如:此配置表示Google的域名使用谷歌的DNS。
// 然后标记其DNS流量为dns-jp-special,稍后在routing模块中匹配其走JP出口即可。
// 注意顺序,google-cn需要在这条规则之前,否则google-cn无法直连
"address": "8.8.8.8",
"port": 53,
"domains": [
"geosite:google"
],
"tag": "dns-jp-special"
}
]
},
// 定义多个连接观测器,用于给负载均衡器提供连接观测数据
"multiObservatory": {
"observers": [
{
// 用default或者填空,另一个burst看了下代码好像没写完
"type": "default",
// 定义此连接观测器的tag
"tag": "internet-usa-observatory",
"settings": {
"subjectSelector": [
// 观测所有proxy-usa开头的outbound
"proxy-usa"
],
// 采用赛博大善人cloudflare的status api来测试outbound的网络连接情况
"probeURL": "https://www.cloudflarestatus.com/api/v2/status.json",
// 每隔多少秒进行一次网络情况测试,不建议设太低,容易浪费流量
"probeInterval": "60s"
}
}
// 可以继续添加匹配不同出口的连接观测器,用于给多个负载均衡器提供连接观测数据
]
},
// api配置,用于统计流量
// 如果你不需要,删了它
"api": {
"tag": "api",
"services": [
"StatsService"
]
},
// 别问我为啥要配个空配置,代码里这么要求的。
// 如果你不需要,删了它
"stats": {
// I don't know why this empty config is needed.
// but without it, the stat server doesn't even boot up.
},
// policy开启system outboundlink的上下行统计
// 如果你不需要,删了它
"policy": {
"system": {
"statsOutboundUplink": true,
"statsOutboundDownlink": true
}
},
// 此处路由示例路由配置为GFW黑名单+自定义黑名单模式,
// 不匹配黑名单的域名会默认直连。
"routing": {
// 建议使用此域名规则
"domainStrategy": "IPIfNonMatch",
"domainMatcher": "mph",
"balancers": [
{
// USA 出口负载均衡,匹配proxy-usa开头的outbound
"tag": "usa-balancer",
"selector": [
"proxy-usa"
],
"strategy": {
// 可用类型 建议在leastping和random类型中选择,leastload看了下实现好像没做好
"type": "leastping",
"settings": {
// 这里单独定义此负载均衡器,使用上面定义的独立连接观测器。
// 如果你有多个不同区域的负载均衡,建议为每个负载均衡都使用独立的连接观测器。
"observerTag": "internet-usa-observatory",
// 仅random类型的负载均衡有效,leastping会自动检测节点存活情况
"aliveOnly": true
}
},
// 当负载均衡所有节点均不可用时,降级为直连
"fallbackTag": "direct"
}
// 可以继续添加更多的负载均衡出口,但请记得为每个负载均衡器配置好合适连接观测器和strategy
],
"rules": [
{
// 流量观测用,如果你不需要,删了它
"type": "field",
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
// 直连 123 端口 UDP 流量(NTP 协议)
"type": "field",
"inboundTag": "transparent",
"port": 123,
"network": "udp",
"outboundTag": "direct"
},
{
// 劫持 53 端口 UDP 流量,使用 V2Ray 的 DNS
"type": "field",
"inboundTag": "transparent",
"port": 53,
"network": "udp",
"outboundTag": "dns-out"
},
{
// 直连 本地保留 ip
"type": "field",
"ip": [
"geoip:private"
],
"outboundTag": "direct"
},
{
// 国内DNS的请求流量直连
"type": "field",
"inboundTag": [
"dns-china-try-resolve",
"dns-china-try-resolve-backup",
"dns-china-special",
"dns-china-site",
"dns-china-site-backup"
],
"outboundTag": "direct"
},
{
// 特殊规则 使用日本DNS的流量,走日本代理出口
"type": "field",
"inboundTag": [
"dns-jp-special",
"dns-jp-site"
],
"outboundTag": "proxy-jp"
},
{
// 特殊规则,USA网站的DNS流量,走USA负载均衡出口
"type": "field",
"inboundTag": [
"dns-usa-site"
],
"balancerTag": "usa-balancer"
},
{
// 海外默认DNS产生的流量 走默认代理出口
"type": "field",
"inboundTag": [
"dns-default-abroad"
],
"outboundTag": "proxy-default"
},
{
// 直连 国内网站 保持和上面DNS分流一致即可,
// 当然,在GFW黑名单路由模式下,这块也可以删掉。填写这个只是为了减少V2Ray的WARNING日志数量。
"type": "field",
"domain": [
"domain:ntp.org",
// 用于steam下载走国内CDN 如果要steam下载不走代理请务必保留这个规则
"domain:steamserver.net",
"geosite:mihoyo-cn",
"geosite:china-list",
"geosite:cn",
"geosite:tld-cn",
"geosite:apple",
"geosite:apple-cn",
"geosite:google-cn",
"geosite:icloud",
"geosite:category-games@cn",
"geosite:geolocation-cn"
],
"outboundTag": "direct"
},
{
// 直连 国内IP
// 当然,在GFW黑名单路由模式下,这块也可以删掉。
"type": "field",
"ip": [
"geoip:cn"
],
"outboundTag": "direct"
},
{
// Telegram的IP 走默认代理出口
// 用于和dnsCircuit 的 persistentRoute 相配合,达成内网设备可以直接访问电报
"type": "field",
"ip": [
"geoip:telegram"
],
"outboundTag": "proxy-default"
},
{
// 特殊规则 配合DNS分流,Google的域名+pixiv+jp结尾的域名走日本出口
"type": "field",
"domain": [
"geosite:google",
"geosite:pixiv",
"regexp:.*\\.jp$"
],
"outboundTag": "proxy-jp"
},
{
//(重要,必填)
// 注意顺序,建议紧跟在域名路由规则之后。
// DNS Route 动态维护的 conn-track 规则,实际使用的是V2Ray router的 srcIP - dstIP 匹配规则。
// 格式为:
// from: dynamic-ipset:dnscircuit-conntrack-src-{outboundTag}
// to: dynamic-ipset:dnscircuit-conntrack-dest-{outboundTag}
"type": "field",
"source": "dynamic-ipset:dnscircuit-conntrack-src-proxy-jp",
"ip": "dynamic-ipset:dnscircuit-conntrack-dest-proxy-jp",
"outboundTag": "proxy-jp"
},
{
// USA Twitter Netflix等网站,走USA负载均衡
"type": "field",
"domain": [
"geosite:twitter",
"geosite:facebook",
"geosite:netflix"
],
// 注意这里使用了负载均衡作为出口
"balancerTag": "usa-balancer",
},
{
//(重要,必填)
// 注意顺序,建议紧跟在域名路由规则之后。
// DNS Route 动态维护的 conn-track 规则,实际使用的是V2Ray router的 srcIP - dstIP 匹配规则。
// 格式为:
// from: dynamic-ipset:dnscircuit-conntrack-src-{balancerTag}
// to: dynamic-ipset:dnscircuit-conntrack-dest-{balancerTag}
"type": "field",
"source": "dynamic-ipset:dnscircuit-conntrack-src-usa-balancer",
"ip": "dynamic-ipset:dnscircuit-conntrack-dest-usa-balancer",
// 注意这里使用了负载均衡作为出口
"balancerTag": "usa-balancer"
},
{
// 被墙的域名和典型的非大陆域名,走默认代理
"type": "field",
"domain": [
"geosite:gfw",
"geosite:geolocation-!cn"
],
"outboundTag": "proxy-default"
},
{
//(重要,必填)
// 注意顺序,建议紧跟在域名路由规则之后。
// DNS Route 动态维护的 conn-track 规则,实际使用的是V2Ray router的 srcIP - dstIP 匹配规则。
// 格式为:
// from: dynamic-ipset:dnscircuit-conntrack-src-{outboundTag}
// to: dynamic-ipset:dnscircuit-conntrack-dest-{outboundTag}
"type": "field",
"source": "dynamic-ipset:dnscircuit-conntrack-src-proxy-default",
"ip": "dynamic-ipset:dnscircuit-conntrack-dest-proxy-default",
"outboundTag": "proxy-default"
},
{
//(重要,必填)
// 注意顺序,建议写在所有路由规则最后。
// DNS Route 路由默认出口,当一个incoming连接没有被任何conn-track规则命中时,会被此规则兜底。
// 照着写即可
"type": "field",
"ip": "dynamic-ipset:dnscircuit-dest-default",
"outboundTag": "proxy-default"
}
// 对于剩下未匹配任何路由规则的流量,走默认路由(即第一个outbound)
// 在GFW黑名单模式下,意即为直连
]
}
}
很快,基本在DNS请求发出后的1秒内就可以完成路由收敛,体感首次访问某个被墙站点时,有30-40%的概率会出现ConnectionRST, 随后只需要刷新一下页面即可正常访问。
同时,由于默认路由有效期是6个小时,对于常用网站,只要不是6个小时内一次没访问过,对应路由规则就会一直生效。
因为使用了OSPF协议,其标准要求,在路由下线时,必须从广播域中废止自己生成的路由条目。 所以,在收到退出信号时,旁路由广播废止路由表后,其实在等待主路由对于废止条目的确认,这个一般需要1-2秒。