Warning The last revision of this setup was quite a while ago, so some details may no longer match current reality.
After finishing work on a new node, I also took the opportunity to rework the router at home and make it access Apricot Spring Network in a "one-way" manner. That wording is deliberate: this is not full bidirectional VPN connectivity, and the distinction matters.
One more note before getting into the configuration: this write-up contains a fair amount of code, so any word counter will make it look longer than it really is.
What this is solving
Apricot Spring Network is a private VPN-style network built along lines similar to DN42. In my home LAN, there currently are not many devices that actually need to join it directly, so the practical value is limited.
The problem is that some software simply refuses to respect system proxy settings, and others do not even provide a proxy option in the first place. OBS is a good example. If a resource only exists inside Apricot Spring Network and a program like OBS needs to reach it, then application-layer proxy settings are no longer enough. At that point, something lower-level becomes necessary.
Why "one-way" instead of normal VPN access
In principle, nodes in a VPN network should be able to talk to each other transparently over the VPN itself. In practice, cross-border connectivity is where things fall apart.
Because of filtering and poor international routing, especially on non-optimized paths such as VPS routes that detour through the US, common VPN protocols tend to run into all kinds of trouble: heavy packet loss, unstable performance, or even outright blocking. WireGuard is no exception. According to real-world testing from a friend, direct IPv4 transport can end up with the port blocked after a few minutes. IPv6 does not currently show the same behavior, but its routing detours are even worse than IPv4, so it is not especially useful either.
That is also why a domestic segment for this network has not been enabled: the trade-off is bad either way. You either run into compliance concerns, or the connection quality is too poor to be dependable. Stable connectivity is hard to achieve.
Another possible approach is to wrap VPN packets in an encrypted proxy, adding an obfuscation layer before sending them to the remote host. That can work, but there are still doubts about how long-lived TCP or UDP connections are treated. There was a test using WireGuard over Shadowsocks with JMS, and a continuous four-hour ping did not show obvious blocking, but that still leaves the transport-quality problem. Over long-distance, high-latency links, getting good TCP behavior is not easy. TCP LFN issues show up quickly.
So instead of insisting on end-to-end VPN transport, it makes more sense to step back and terminate TCP locally, then proxy only the payload. That is the role TProxy plays here.
What TProxy changes — and what it cannot do
This approach provides transparent access, but it is not a VPN.
TProxy does not carry layer-3 packets across the network, which means the remote side cannot route traffic back properly and cannot initiate connections on its own. Only the local side can actively reach resources inside the private network. In other words, this is fundamentally the same limitation ordinary proxy-based access already has, just implemented in a transparent way.
That is why this setup is best described as one-way access.
Router environment
The soft router here runs Debian 12 rather than OpenWrt, so a few steps differ from what is commonly seen in router-focused tutorials.
Encrypted proxy configuration
There was already an Xray instance running on the router for other internal purposes, so the only real change needed on the outbound side was to point it to the Hong Kong node.
The interesting part is the inbound. Because TProxy is required, Xray needs a special inbound using dokodemo-door:
{
"port": 65530,
"protocol": "dokodemo-door",
"settings": {
"network": "tcp,udp",
"followRedirect": true
},
"streamSettings": {
"sockopt": {
"tproxy": "tproxy"
}
}
}
After saving the configuration, restart Xray.
TProxy setup
For transparent proxying, TProxy is the most current and generally applicable method.
Create a script wherever convenient and put the following into it:
This code was generated with GPT-5 Thinking Mini and then manually modified. Check all settings carefully before using it.
#!/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
The basic idea behind this script is straightforward:
- create a dedicated routing table for TProxy-marked traffic,
- add policy routing rules for both IPv4 and IPv6,
- redirect matching traffic destined for the internal network prefixes into the Xray listening port,
- mark locally generated traffic as well, so programs running on the router itself can also reach those internal resources transparently,
- and clear conntrack state so old connections do not get in the way.
The target prefixes in this setup are:
- IPv4:
198.18.0.0/16 - IPv6:
fd04:17::/32
The transparent proxy port is 65530, matching the Xray inbound above.
Because this is running on Debian rather than OpenWrt, using a shell script like this is often the simplest way to keep the behavior explicit and manageable.
What you get in practice
With this in place, devices on the home network can access resources inside Apricot Spring Network without configuring per-application proxies, and software that ignores system proxy settings can still reach those destinations.
But the limitations remain exactly where they were before: this is still not full mesh VPN behavior. The remote side cannot initiate a connection back through this setup, and there is no true layer-3 transparency across the link. Access works only when the local side starts the connection.
That trade-off is the whole point. When stable, direct cross-border VPN transport is difficult to maintain, using TProxy to terminate traffic locally and proxy only the application data is often the more practical option.