In a previous article, I introduced a fairly straightforward chained VPN setup: all devices first connect to a self-hosted WireGuard server, and the server then forwards traffic to Gluetun, which finally sends it to the Internet through an upstream VPN provider.
The core value of that setup was clear:
- Each device only needs to be configured with WireGuard once;
- The upstream VPN only needs to be maintained on the server side;
- Phones, computers, tablets, and home devices can all be connected to the same virtual LAN;
- The exit IP is provided by a commercial VPN, corporate VPN, or another upstream VPN, rather than directly exposing the VPS IP;
- The whole system can be deployed with Docker, minimizing pollution of the host environment;
- Compared with connecting directly to a VPN provider, the provider only sees the exit of your VPN gateway, namely your VPS server, instead of the real IP address of each individual device;
- In addition, your local ISP and similar parties cannot easily tell whether you are using a mainstream VPN service, because they can only see that you are connecting to a VPN gateway, namely your VPS server. Where the traffic eventually goes remains unknown to them.
However, as my use cases expanded, the original structure gradually started to reveal some limitations. The most typical issue was that traffic control was not fine-grained enough. By default, all traffic had to enter the upstream VPN tunnel, which could trigger risk-control mechanisms or cause access problems for certain services. It was also not very convenient to perform traffic engineering and apply different exit strategies to different services. Although split routing could be achieved with iptables and routing tables, the rules became difficult to maintain once they grew in number.
Therefore, I reorganized the original setup into a more complete personal VPN
gateway architecture. The new version is no longer just a simple WG-Easy + Gluetun setup. Instead, it has become:
WG-Easy + sing-box + Gluetun + AdGuard Home + unified config.yaml generation
To me, this setup is no longer merely “a VPN over another VPN”. It is a personal network exit that can be maintained over the long term, expanded, and observed. It can serve daily Internet access, remote work, home intranet access, development and testing, and unified multi-device management. If all you need is the simplest form of global VPN forwarding, the old approach is still sufficient. But if you want a better balance among privacy, controllability, split-routing rules, and DNS management, the new architecture is more suitable.
This article introduces the design ideas, traffic paths, suitable use cases, and major improvements of the new architecture compared with the old setup. If you are interested in the new version, or want to deploy it directly, you can check out my project repository on GitHub.
Why a New Architecture Is Needed
The earliest chained VPN setup could already solve many problems. For example, whether I was at home, in the office, or on an outside network, I could use the same WireGuard configuration to access the VPS, and then enter the upstream VPN through Gluetun running on that VPS. In this way, all devices shared the same upstream VPN exit, and I did not have to repeatedly install, log in to, and configure different VPN clients on every device.
In practice, this setup had been very stable for me, and its performance was good enough for my needs. There were basically no major problems. But after using it for a long time, I still ran into several pain points.
Not All Traffic Is Suitable for the Upstream VPN
In actual use, I found that some services may trigger risk-control mechanisms because of the VPN exit. This can happen during account login, payment, AI service usage, and similar scenarios. For example, some streaming platforms I commonly use place strict restrictions on VPN traffic, and sometimes such traffic is better sent directly. In addition, I sometimes need to access course servers inside the campus network, and occasionally this involves remote desktop access, which requires relatively low latency. In such cases, using a VPN gives a very poor experience even if the exit is located in the same country.
Therefore, I wanted a more fine-grained split-routing capability. For example, traffic to certain services that I trust could bypass the upstream VPN through rule sets and access the Internet directly from the server side. This would preserve the privacy protection offered by the upstream VPN while avoiding unnecessary access problems. The previous approach relied more on iptables, routing tables, and container networking. It worked, but became difficult to maintain once the number of rules increased.
After some research and testing, I found that sing-box’s TUN mode is very suitable for this kind of rule-based traffic splitting. It supports rich rule types and flexible routing strategies, and can decide the next hop of traffic based on multiple dimensions such as domain names and IP addresses. By placing sing-box between WG-Easy and Gluetun, I can implement smarter traffic splitting instead of a simple global VPN. sing-box can also reference remote rule sets, such as geosite-openai and geosite-paypal, so I do not need to maintain those rules myself. Another advantage is that, compared with deploying an OpenWrt virtual machine, sing-box is much lighter to deploy and maintain, and is more Docker-friendly.
In this way, I can perform traffic engineering much more flexibly. For example, default traffic should go through the VPN for privacy protection. Some services in certain regions may need to be accessed through another VPN exit. Some services should be accessed directly. Some services should go through a specific VPN exit. With sing-box rule-based routing, these strategies become much easier to implement.
Configuration Needs to Be Centrally Managed
In the original repository, configuration files were scattered under the conf
directory. Changing settings such as network segments was already somewhat
inconvenient. After adding sing-box, maintaining Docker Compose, WG-Easy hooks,
sing-box JSON, AdGuard Home configuration, and iptables rules together became
painful. Therefore, in the new version, I use config.yaml as the single
configuration entry point, and then generate runtime files from templates.
After each configuration change, running the generated up.sh script updates
all related configuration files, avoiding the problem of changing one place
while forgetting another.
More Complete Network Monitoring
The original setup mainly relied on logs to observe the network state. Sometimes I wanted to see which devices were currently connected to WireGuard, what services those devices were accessing, whether there was abnormal traffic, and so on. In the old setup, this information had to be obtained by checking logs inside containers or by installing additional monitoring tools. In the new version, the API provided by sing-box can be used together with tools such as MetaCubeXD to view traffic distribution, rule hits, upstream access, and other information in real time, improving observability.
Therefore, the goal of the new architecture is not simply to “add another VPN layer”. It is to build a controllable network exit that is more suitable for home or personal use.
Components of the New Setup
The new setup mainly consists of four core containers.
WG-Easy: The WireGuard Access Layer
WG-Easy is still responsible for the most basic and most important part: providing the WireGuard server and the web management interface. Phones, computers, tablets, and servers all connect to WG-Easy as WireGuard clients. Users can create peers, download configuration files, or scan QR codes to import configurations into mobile clients through the WG-Easy Web UI.
In the new architecture, WG-Easy no longer simply hands traffic directly to Gluetun. Instead, through hooks and policy routing, it sends client traffic into a more complex gateway path.
AdGuard Home: Unified DNS and Filtering Layer
AdGuard Home is responsible for DNS resolution, ad filtering, tracking-domain blocking, and DNS query logs.
In the new architecture, ordinary DNS requests from WireGuard clients are
restricted to AdGuard Home. In other words, clients cannot freely send DNS
queries to 8.8.8.8, 1.1.1.1, or other external DNS servers.
This brings several benefits:
- Upstream DNS can be configured centrally;
- Filter lists can be managed uniformly;
- Client DNS queries can be inspected;
- The risk of DNS leaks can be reduced;
- There is no need to configure filtering software separately on every device.
Of course, this does not mean that all forms of DNS bypass can be completely prevented. For example, DoH, DoT, or certain applications with built-in resolution logic still require additional strategies. But for ordinary UDP/TCP port 53 DNS requests, the gateway can enforce centralized handling.
sing-box: The Rule-Based Routing Layer
sing-box is the most important change in the new architecture.
It receives traffic forwarded from WG-Easy through a TUN inbound, and then decides the next hop according to rules:
- Traffic matching
bypass_rule_setsgoes out directly; - Private IP traffic goes out directly, avoiding accidental forwarding into the upstream VPN;
- Other default traffic is marked with a routing mark, handed over to Linux policy routing, and then sent into Gluetun;
- Rule sets can use remote
.srsfiles, such as geosite-openai, geosite-paypal, and so on.
If you look at the corresponding sing-box configuration file, you will notice a
somewhat unusual point: the gluetun outbound in sing-box is also configured as
type direct, but it carries a routing_mark. In practice, this mark is
captured by Linux policy routing, which directs the traffic to Gluetun.
Therefore, it is not “direct” in the ordinary sense, but rather “sent to Gluetun
through system routing”.
In other words, sing-box acts more like an intelligent traffic splitter here, while Gluetun is the final upstream VPN exit. Besides Gluetun, sing-box can also be configured with other outbound types in a similar way, such as WireGuard, VLESS, Trojan, and others, to support more diverse upstream services. If you do not need the various conveniences provided by Gluetun, you could even replace it here with another VPN client or a direct Internet exit.
One thing to note is that sing-box cannot obtain domain information here, so we need to enable the sniff function here to identify domain names. This allows us to perform traffic splitting based on domain-based rule sets. For some protocols or encrypted traffic that do not support sniffing, we may only be able to perform traffic splitting based on IP rules.
Gluetun: The Upstream VPN Exit Layer
Gluetun is responsible for connecting to a commercial VPN, corporate VPN, or another supported upstream VPN service.
Its advantage is that it supports a large number of VPN providers, while encapsulating OpenVPN / WireGuard clients, authentication parameters, firewall rules, health checks, and related logic inside a container.
In the new architecture, most default traffic eventually enters Gluetun, and Gluetun then accesses the Internet through the upstream VPN tunnel. As a result, clients see their own WireGuard network, while the Internet sees the exit IP of the upstream VPN.
Overall Network Topology
The new architecture can be understood as the following path:
flowchart TD
subgraph devices["Your Devices"]
phone["Phone"]
pc["PC"]
laptop["Laptop"]
end
subgraph server["Your Server"]
wgeasy["WG-Easy (WireGuard Server)"]
adguard["AdGuard Home (DNS Filtering)"]
singbox["sing-box (TUN Router / Rule Engine)"]
gluetun["Gluetun (VPN Provider Client)"]
end
phone -- WireGuard --> wgeasy
pc -- WireGuard --> wgeasy
laptop -- WireGuard --> wgeasy
wgeasy -- DNS only --> adguard
wgeasy -- regular traffic --> singbox
singbox -- bypass_rule_sets --> direct["Direct Internet"]
singbox -- default route --> gluetun
gluetun -- VPN tunnel --> provider["VPN Provider"]
provider --> internet["Internet"]
direct --> internet
From a traffic perspective, it can be divided into three categories:
- DNS traffic: DNS requests from WireGuard clients are fixed to AdGuard Home.
- Traffic matching bypass rules: For example, OpenAI, PayPal, or other rule
sets placed in
bypass_rule_setscan usedirect. - Default traffic: Ordinary Internet traffic that does not match a bypass rule enters Gluetun and accesses the Internet through the upstream VPN exit.
In addition, direct_subnets can be configured to let specific subnets bypass
sing-box. This is suitable for clear intranet access scenarios, such as
accessing a home LAN, a specific Docker network, or certain private subnets.
Configuration Management: From Manual Configuration to Generated Configuration
Another important change in the new setup is the way configuration is managed.
Previously, you might have needed to modify all of the following at the same time:
docker-compose.yml- WireGuard hooks
- iptables rules
- Gluetun environment variables
- DNS configuration
- sing-box JSON
- AdGuard Home configuration
This was flexible, but the maintenance cost was high. Especially after the
network structure changed even slightly, it was easy to end up with the classic
problem of changing one place but forgetting another. The new setup centralizes
the main configuration in
config.yaml,
and then uses a generation script to render it into the actual configuration
files under runtime/.
Therefore, ordinary users only need to modify config.yaml. Advanced users who
want to customize more details can modify the Jinja2 templates under
templates/. This keeps configuration centrally managed while still preserving
enough flexibility.
One thing to note is that I recommend not directly modifying files under the
runtime/ directory. They are generated artifacts and will be overwritten every
time the generator is run again.
How sing-box Split-Routing Rules Work
In the new setup, the sing-box rules can be roughly understood as follows:
{
"route": {
"rules": [
{ "inbound": "tun-in", "action": "sniff" },
{ "ip_is_private": true, "outbound": "direct" },
{
"rule_set": ["geosite-openai", "geosite-paypal"],
"outbound": "direct"
},
{ "outbound": "gluetun" }
]
}
}
This means:
- First sniff the traffic to identify information such as domain names;
- Private IP addresses go through
direct; - Traffic matching the rule sets goes through
direct; - All other traffic goes through the
gluetunoutbound.
The meaning of bypass_rule_sets is “bypass the upstream VPN”, not “bypass your
server”.
For example, if you configure:
singbox:
bypass_rule_sets:
- tag: "geosite-openai"
url: "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-openai.srs"
- tag: "geosite-paypal"
url: "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-paypal.srs"
Then traffic to these services still passes through your WireGuard server and sing-box. It simply no longer enters Gluetun’s upstream VPN tunnel, and is instead accessed directly from the server side.
This is very helpful for services that are sensitive to VPN exits. For example, some services may require additional verification or even reject access outright when they see a commercial VPN IP. Bypassing the upstream VPN through rules can reduce these problems.
What Is extra_rule_sets?
The new configuration also includes extra_rule_sets:
singbox:
extra_rule_sets:
- tag: "geosite-google"
url: "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-google.srs"
- tag: "geosite-apple"
url: "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-apple.srs"
Its purpose is to download and cache these rule sets without participating in
the current route matching. In other words, extra_rule_sets does not change
the traffic path. A rule only takes effect when it is placed in
bypass_rule_sets, or when it is explicitly referenced in the sing-box
template.
I keep this configuration item for easier future expansion. For example, I may
want to prepare rule sets for Google, Apple, Amazon, and others in advance,
without enabling them immediately. When they are needed, I only need to move the
corresponding rule into bypass_rule_sets, or add a new route rule.
If you prefer the simplest possible configuration, you can also set
extra_rule_sets to an empty array:
singbox:
extra_rule_sets: []
Note that in YAML, it is better to write an explicit empty array instead of leaving an empty key:
# Recommended
extra_rule_sets: []
# Not recommended; this may be parsed as null
extra_rule_sets:
DNS: Why AdGuard Home Is Included by Default
In a VPN gateway, DNS is an issue that is easy to overlook. If ordinary traffic is handled but DNS is not, the following may happen:
- Browser traffic goes through your VPN gateway;
- DNS queries are sent to the local network, the ISP’s DNS, or some public DNS service;
- This eventually causes DNS leaks or inaccurate split-routing decisions.
In the new architecture, ordinary DNS requests from WireGuard clients are restricted to AdGuard Home. This allows you to manage DNS centrally on the gateway side.
AdGuard Home brings several benefits:
- Filtering of ads and tracking domains;
- Custom upstream DNS;
- Query logs;
- Local domain rewrites;
- Different policies for different clients.
However, note that if you want to completely block all DNS bypasses, you still need to consider DoH, DoT, QUIC, and similar cases. The DNS restriction discussed in this article mainly targets traditional UDP/TCP port 53 queries.
Suitable Use Cases
The new architecture covers more scenarios than the old version. Overall, it is mainly suitable for the following cases.
1. Sharing One Upstream VPN Across Multiple Devices
This is the most basic use case.
You only need to configure the upstream VPN once on the server, and all devices connect through WireGuard. Phones, computers, tablets, and spare devices no longer need to install the upstream VPN client separately.
This is especially useful when you have many devices, a mixture of platforms, or an upstream VPN client with a poor user experience.
2. Remote Access to a Home or Office Network
After all devices join the same WireGuard network, they can form a unified virtual LAN.
You can use it to access:
- A home NAS;
- Office intranet services;
- Private Git services;
- A home media server;
- SSH management entry points.
Together with direct_subnets, some intranet access can also bypass the more
complex sing-box routing logic.
3. Applying Different Exit Strategies to Different Services
Some services are suitable for a commercial VPN, while others are not.
For example:
- Default traffic goes through the Gluetun upstream VPN;
- Sensitive services such as OpenAI and PayPal go through
direct; - Torrent and P2P traffic goes through Gluetun or another dedicated VPN exit;
- Private IPs and intranet addresses do not enter the upstream VPN;
- Specific rule sets are downloaded in advance but not enabled yet.
This structure is more flexible than the old “global proxy” model, and easier to maintain than hand-written iptables rules.
5. Low-Intrusion Deployment
The entire setup is based on Docker Compose. The host only needs Docker and Docker Compose installed, while the main networking logic is handled by containers and generated configuration files. This makes it friendly to VPS instances, home servers, soft routers, and Raspberry Pi devices.
Deployment Overview
In actual deployment, the process is roughly:
git clone https://github.com/saturneric/wg-easy-gluetun.git
cd wg-easy-gluetun
Then edit:
vim config.yaml
At minimum, you need to configure:
- The WireGuard / OpenVPN parameters for the upstream VPN;
- The WG-Easy admin password;
- The server’s public address or domain name;
- AdGuard Home upstream DNS;
- sing-box bypass rules;
- Required service ports and fixed container IPs.
Start the stack with:
./up.sh -d
Do not directly run a normal docker compose up, because the runtime Compose
file and related configuration need to be created by the generator first.
up.sh first runs the configuration generation logic, and then starts the full
stack.
Security Notes
Although this type of gateway setup is convenient, its security boundaries still require attention.
1. Do Not Use Default Passwords
The WG-Easy password, Clash API secret, and AdGuard Home credentials in
config.yaml should all be changed. This is especially important if any
management interface is exposed to the public Internet, because default
passwords are very dangerous.
2. Be Careful When Exposing Management Ports
Usually, only the WireGuard UDP port should be publicly exposed. The management interfaces of WG-Easy, MetaCubeXD, and AdGuard Home should preferably not be exposed directly to the public Internet.
3. Check the Terms of Your Upstream VPN Service
Whether multi-device sharing, gateway forwarding, and chained VPN usage are allowed depends on the terms of your upstream VPN service. You should confirm this yourself before deploying the setup.
4. Watch Out for DNS Bypass
This setup restricts ordinary DNS queries, but it cannot automatically block all application-layer DoH / DoT behavior. If you need a stricter DNS policy, you will need additional domain, IP, SNI, or application-layer policy controls.
Summary: Main Changes Compared with the Old Setup
Compared with the old WG-Easy + Gluetun setup, the new architecture mainly
introduces the following changes:
| Aspect | Old Version | New Version |
|---|---|---|
| Access layer | WG-Easy | WG-Easy |
| Upstream VPN | Gluetun | Gluetun |
| Split-routing capability | Mainly relies on routing and iptables | sing-box rule-set based routing |
| DNS | Requires additional configuration | AdGuard Home integrated by default |
| Configuration method | Manually maintained across multiple files | config.yaml + template generation |
| Rule management | Coarse-grained | geosite / rule-set level |
| Observability | Mainly log-based | Can use MetaCubeXD to inspect sing-box |
| Extensibility | Relatively limited | Better suited for adding more rules and services in the future |
In short, the old version is simpler and suitable for users who only want to send all traffic into an upstream VPN. The new version is better suited for long-term use, especially for users who need split routing, DNS filtering, centralized management, and adaptation to multiple scenarios.