杏泉网络:借助TProxy实现的『单向』接入
系列文章:杏泉网络 专题页
这两天折腾完新节点之后,顺便把家里路由器也处理了一下,实现了『单向』接入杏泉网络。至于为什么说是单向,下面还会有详细讲述。
此外提醒一句,因为代码较多,这篇文章的字数计数器比较『虚』。
1. 前言
杏泉网络是笔者搭建的一个类似DN42的VPN网络,名字来源于笔者暂未写完也暂未公布的小说主人公『乌林杏泉』。关于这个网络的具体介绍和架构,在上面的前情提要中已有提及,此处不再赘述。
目前,家里局域网并没有别的设备需要接入杏泉网络,因此意义不是很大。但是笔者碰到过很多软件是死活不认系统设定的代理的(毕竟系统代理属于是『看你是情分,不看是本分』),甚至根本没有设置的地方,比如开源直播软件OBS。有时候部分资源只在杏泉网络内提供,又需要让OBS访问,就只能考虑相对底层一些的方式了。
2. 单向?双向?
杏泉网络作为一个VPN网络,节点之间本来是可以采用VPN进行无缝互联的,但是由于大墙和非优化国际链路的原因(比如三网绕美的VPS),常见的VPN协议在跨境传输的时候都会碰到各种各样的问题,比如高丢包,甚至是直接阻断——这一点连杏泉网络所使用的WireGuard也不能避免,根据友人的说法,IPv4直接传输会碰到几分钟后端口被阻断的问题,IPv6虽然暂时没有这个问题,但是v6路由绕路比v4更为严重,所以意义不大。(这一点,也是笔者迟迟未启用杏泉网络境内段的原因:要么有合规性风险,要么连接状况不佳,总之就是无法稳定连接。)
另一方面,虽然可以使用现成的加密代理软件,先把VPN数据包一层混淆层,再发往远端主机,但笔者不太确定老大哥对于长连接的TCP/UDP的看法如何(曾经测试过WireGuard over SS(JMS),连续ping四个小时,没有发现明显的阻断现象),而且另一方面,想要在长距离高延迟链路上获得高质量TCP连接是相对困难的(很容易碰到TCP LFN问题),所以不如退一步,也就是今天的方案,使用TProxy,本地终结TCP,仅仅对里面的数据进行代理。
但很显然,TProxy方案不是VPN,无法传递三层数据包,也就是说远端无法正确路由回来,也无法主动发起连接,只能由本地端去主动访问网内资源(其实这就是目前的代理软件访问方案的实质),因此虽然可以实现透明接入,笔者依然会把这种方式归类为单向接入。
3. 路由器配置
软路由运行的是Debian 12系统而非OpenWRT,因此部分操作与一般的软路由有所不同。
3.1 加密代理
因为路由器上本来就已经有一个用于网内其他用途的Xray,因此只需要把它的出口指向香港节点即可。重点是入口,因为我们需要用到TProxy,因此需要一个特殊的inbound节点:
{
"port": 65530,
"protocol": "dokodemo-door",
"settings": {
"network": "tcp,udp",
"followRedirect": true
},
"streamSettings": {
"sockopt": {
"tproxy": "tproxy"
}
}
}
}保存,重启Xray即可。
3.2 TProxy
想要实现透明代理,现在最新也是最通用的方法,自然是使用TProxy。
在方便的地方新建一个脚本文件,写入:
此代码使用GPT-5 Thinking Mini生成,并做人工修改,使用前请务必核对你所需要的设置
#!/usr/bin/env bash
# usage: ./tproxy.sh start|stop
set -euo pipefail
TPORT=65530
FWMARK=1
RT_TABLE_ID=100
RT_TABLE_NAME="tproxy"
IPV4_NET="198.18.0.0/16"
IPV6_NET="fd04:17::/32"
ensure_rt_table() {
grep -qE "^[[:space:]]*${RT_TABLE_ID}[[:space:]]+${RT_TABLE_NAME}\$" /etc/iproute2/rt_tables 2>/dev/null || \
echo "${RT_TABLE_ID} ${RT_TABLE_NAME}" >> /etc/iproute2/rt_tables
}
remove_rt_table_entry() {
sed -i "/^[[:space:]]*${RT_TABLE_ID}[[:space:]]\+${RT_TABLE_NAME}\$/d" /etc/iproute2/rt_tables 2>/dev/null || true
}
add_ip_rule_and_route() {
# IPv4
ip rule | grep -q "fwmark ${FWMARK} lookup ${RT_TABLE_NAME}" 2>/dev/null || \
ip rule add fwmark ${FWMARK} lookup ${RT_TABLE_NAME}
ip route show table ${RT_TABLE_NAME} | grep -q "^local 0.0.0.0/0" 2>/dev/null || \
ip route add local 0.0.0.0/0 dev lo table ${RT_TABLE_NAME}
# IPv6
ip -6 rule | grep -q "fwmark ${FWMARK} lookup ${RT_TABLE_NAME}" 2>/dev/null || \
ip -6 rule add fwmark ${FWMARK} lookup ${RT_TABLE_NAME}
ip -6 route show table ${RT_TABLE_NAME} | grep -q "^local ::/0" 2>/dev/null || \
ip -6 route add local ::/0 dev lo table ${RT_TABLE_NAME}
}
del_ip_rule_and_route() {
# IPv4
ip rule del fwmark ${FWMARK} lookup ${RT_TABLE_NAME} 2>/dev/null || true
ip route del local 0.0.0.0/0 dev lo table ${RT_TABLE_NAME} 2>/dev/null || true
# IPv6
ip -6 rule del fwmark ${FWMARK} lookup ${RT_TABLE_NAME} 2>/dev/null || true
ip -6 route del local ::/0 dev lo table ${RT_TABLE_NAME} 2>/dev/null || true
}
add_iptables_rules() {
# IPv4 - PREROUTING TPROXY for traffic coming from the network
iptables -t mangle -C PREROUTING -d ${IPV4_NET} -p tcp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK} 2>/dev/null || \
iptables -t mangle -A PREROUTING -d ${IPV4_NET} -p tcp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK}
iptables -t mangle -C PREROUTING -d ${IPV4_NET} -p udp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK} 2>/dev/null || \
iptables -t mangle -A PREROUTING -d ${IPV4_NET} -p udp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK}
# IPv4 - mark OUTPUT (local processes)
iptables -t mangle -C OUTPUT -d ${IPV4_NET} -p tcp -j MARK --set-mark ${FWMARK} 2>/dev/null || \
iptables -t mangle -A OUTPUT -d ${IPV4_NET} -p tcp -j MARK --set-mark ${FWMARK}
iptables -t mangle -C OUTPUT -d ${IPV4_NET} -p udp -j MARK --set-mark ${FWMARK} 2>/dev/null || \
iptables -t mangle -A OUTPUT -d ${IPV4_NET} -p udp -j MARK --set-mark ${FWMARK}
# IPv6 - PREROUTING TPROXY for network-origin traffic (requires kernel/netfilter support for IPv6 TPROXY)
ip6tables -t mangle -C PREROUTING -d ${IPV6_NET} -p tcp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK} 2>/dev/null || \
ip6tables -t mangle -A PREROUTING -d ${IPV6_NET} -p tcp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK}
ip6tables -t mangle -C PREROUTING -d ${IPV6_NET} -p udp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK} 2>/dev/null || \
ip6tables -t mangle -A PREROUTING -d ${IPV6_NET} -p udp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK}
# IPv6 - mark OUTPUT (local processes)
ip6tables -t mangle -C OUTPUT -d ${IPV6_NET} -p tcp -j MARK --set-mark ${FWMARK} 2>/dev/null || \
ip6tables -t mangle -A OUTPUT -d ${IPV6_NET} -p tcp -j MARK --set-mark ${FWMARK}
ip6tables -t mangle -C OUTPUT -d ${IPV6_NET} -p udp -j MARK --set-mark ${FWMARK} 2>/dev/null || \
ip6tables -t mangle -A OUTPUT -d ${IPV6_NET} -p udp -j MARK --set-mark ${FWMARK}
}
del_iptables_rules() {
# IPv4
iptables -t mangle -D PREROUTING -d ${IPV4_NET} -p tcp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK} 2>/dev/null || true
iptables -t mangle -D PREROUTING -d ${IPV4_NET} -p udp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK} 2>/dev/null || true
iptables -t mangle -D OUTPUT -d ${IPV4_NET} -p tcp -j MARK --set-mark ${FWMARK} 2>/dev/null || true
iptables -t mangle -D OUTPUT -d ${IPV4_NET} -p udp -j MARK --set-mark ${FWMARK} 2>/dev/null || true
# IPv6
ip6tables -t mangle -D PREROUTING -d ${IPV6_NET} -p tcp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK} 2>/dev/null || true
ip6tables -t mangle -D PREROUTING -d ${IPV6_NET} -p udp -j TPROXY --on-port ${TPORT} --tproxy-mark 0x${FWMARK}/0x${FWMARK} 2>/dev/null || true
ip6tables -t mangle -D OUTPUT -d ${IPV6_NET} -p tcp -j MARK --set-mark ${FWMARK} 2>/dev/null || true
ip6tables -t mangle -D OUTPUT -d ${IPV6_NET} -p udp -j MARK --set-mark ${FWMARK} 2>/dev/null || true
}
cleanup_conntrack() {
which conntrack >/dev/null 2>&1 && {
conntrack -D -d ${IPV4_NET} 2>/dev/null || true
conntrack -D -d ${IPV6_NET} 2>/dev/null || true
} || true
}
run() {
ensure_rt_table
add_ip_rule_and_route
add_iptables_rules
cleanup_conntrack
}
stop() {
del_iptables_rules
del_ip_rule_and_route
remove_rt_table_entry
cleanup_conntrack
}
case "${1:-}" in
run) run;;
stop) stop;;
*)
echo "Usage: $0 run|stop" >&2
exit 2
;;
esac保存,添加可执行权限,并在Xray启动之后执行这个脚本,传入run参数,即可实现对于特定地址段的透明代理。
3.3 DNS
路由器上使用的是ADGuard Home,因为目前还在测试阶段,因此并没有折腾别的分流工具,在上面做就好。
因为目前使用hk-2接入,所以打开设置——DNS设置,在上面的DNS服务器中,填入[/xq/]198.18.5.1,使特定后缀走网内就近DNS解析,加快解析速度。

4. 实际测试
在路由器下的客户端,已经可以直接访问杏泉网络内的资源:

同时,其他不读取代理设置的软件也能正常使用。比如上面提到的OBS,在OBS中添加一个网内资源:

保存好,也可以看到程序正确加载并播放网内资源了:

5. 写在最后
正如开头所说,本文实际有效内容不算太长,字数计数器虚胖很严重。不过既然是为这个小网络添砖加瓦,记录一下具体流程,也是一件比较好的事情。
(完)