<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>vpn · wleeaf field notes</title><link>https://articles.wleeaf.dev/tags/vpn/</link><description>Hands-on guides and build logs from the wleeaf studio: self-hosting, Linux, networking, Docker, the command line and reverse engineering.</description><language>en-us</language><lastBuildDate>Wed, 25 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://articles.wleeaf.dev/tags/vpn/feed.xml" rel="self" type="application/rss+xml"/><item><title>Split Tunnel WireGuard: Private Services Without Routing Everything</title><link>https://articles.wleeaf.dev/posts/wireguard-split-tunnel/</link><pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/wireguard-split-tunnel/</guid><description>How to set up WireGuard so only traffic to your private services goes through the tunnel, while everything else uses your normal connection.</description><category>wireguard</category><category>vpn</category><category>networking</category><content:encoded><![CDATA[<p>Most WireGuard tutorials show you how to route all traffic through the tunnel. That is great for privacy but terrible for latency and bandwidth. If you just want to access a few private services on your server, there is a much better approach: put only your VPN subnet in the client&rsquo;s <code>AllowedIPs</code>, so traffic to those services goes through the tunnel and everything else takes your normal route.</p>
<h2 id="the-problem">The Problem<a class="anchor" href="#the-problem" aria-label="Link to “The Problem”">#</a></h2>
<p>I run several services on my VPS that should not be publicly accessible: a git server, an admin panel, a VPN management UI, and a mail server admin interface. These need to be reachable from my laptop and phone, but nobody else should be able to see them.</p>
<p>The obvious solution is a VPN. But I do not want all my traffic going through a server in Germany when I am watching a video or browsing the web. I want my normal internet connection for everything except the few private services.</p>
<h2 id="split-tunnel-configuration">Split Tunnel Configuration<a class="anchor" href="#split-tunnel-configuration" aria-label="Link to “Split Tunnel Configuration”">#</a></h2>
<p>The key is the <code>AllowedIPs</code> setting in the WireGuard client config. Instead of <code>0.0.0.0/0</code> which routes everything, you specify only the VPN subnet:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>[Interface]
PrivateKey = &lt;client-private-key&gt;
Address = 10.0.0.2/24
DNS = 10.0.0.1

[Peer]
PublicKey = &lt;server-public-key&gt;
Endpoint = your-server-ip:51820
AllowedIPs = 10.0.0.0/24</code></pre>
</div>
<p>With this config, only traffic destined for <code>10.0.0.x</code> addresses goes through the tunnel. Everything else takes the normal route. Your browsing speed stays the same, your public IP does not change, and you still get access to your private services.</p>
<h2 id="dns-is-where-the-magic-happens">DNS Is Where the Magic Happens<a class="anchor" href="#dns-is-where-the-magic-happens" aria-label="Link to “DNS Is Where the Magic Happens”">#</a></h2>
<p>The split tunnel routes packets, but your browser needs to know that <code>admin.example.com</code> should resolve to a VPN IP instead of a public one. This is where a local DNS server comes in.</p>
<p>I run dnsmasq on the VPN server, listening only on the WireGuard interface. It has entries like:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>address=/git.example.com/10.0.0.1
address=/admin.example.com/10.0.0.1
address=/vpn.example.com/10.0.0.1</code></pre>
</div>
<p>For any domain not in this list, dnsmasq forwards the query to Cloudflare or Google DNS. So private subdomains resolve to the VPN IP, and everything else resolves normally.</p>
<p>The client config sets <code>DNS = 10.0.0.1</code> which tells the system to use the VPN DNS server. Combined with the split tunnel routing, DNS queries for private domains go through the tunnel to dnsmasq, and all other DNS queries go through the tunnel too but get forwarded to public DNS by dnsmasq.</p>
<h2 id="the-dns-over-tls-conflict">The DNS-over-TLS Conflict<a class="anchor" href="#the-dns-over-tls-conflict" aria-label="Link to “The DNS-over-TLS Conflict”">#</a></h2>
<p>This one took me a while to figure out. I use a DPI bypass tool on my local machine that forces all DNS through DNS-over-TLS via systemd-resolved. When the VPN comes up, the DNS setting should override this, but it does not always work cleanly.</p>
<p>The fix was a PostUp script in the WireGuard config:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>PostUp = resolvectl dnsovertls %i no</code></pre>
</div>
<p>This disables DNS-over-TLS specifically on the VPN interface so queries to 10.0.0.1 go through as plain DNS over the tunnel. Without this, the system tries to establish a TLS connection to the dnsmasq server, which obviously does not support TLS, and private domains fail to resolve.</p>
<h2 id="no-public-dns-leaks">No Public DNS Leaks<a class="anchor" href="#no-public-dns-leaks" aria-label="Link to “No Public DNS Leaks”">#</a></h2>
<p>One nice side effect of this setup: your private subdomains are completely invisible. There are no public DNS records for them. They do not show up in certificate transparency logs because the wildcard cert covers them. Someone scanning your domain will find the public subdomains but have no idea the private ones exist.</p>
<p>The only way to discover them is to be on the VPN, at which point dnsmasq resolves them. And even if someone knows the subdomain name and tries to access it from the public internet, Caddy returns 403 based on the source IP check.</p>
<h2 id="mobile-access">Mobile Access<a class="anchor" href="#mobile-access" aria-label="Link to “Mobile Access”">#</a></h2>
<p>WireGuard works great on phones. The official app for both iOS and Android supports split tunneling natively. I have the same config on my phone: only VPN traffic goes through the tunnel. This means I can check my git server from my phone without draining battery or slowing down everything else.</p>
<p>The one downside is that you have to manually toggle the VPN on when you need it. There is no automatic connection based on network or location. But honestly, it takes one tap and the connection establishes in under a second.</p>
<h2 id="resource-usage">Resource Usage<a class="anchor" href="#resource-usage" aria-label="Link to “Resource Usage”">#</a></h2>
<p>WireGuard is absurdly lightweight. The server side runs as a single container using about 30MB of RAM. The CPU usage is essentially zero unless you are pushing serious bandwidth. On the client side, the kernel module handles everything so there is no userspace overhead.</p>
<p>Compare this to OpenVPN which needs a full userspace daemon, TLS handshakes, and significantly more CPU per connection. For a personal VPN with one or two clients, WireGuard is comically overpowered.</p>
<h2 id="final-thoughts">Final Thoughts<a class="anchor" href="#final-thoughts" aria-label="Link to “Final Thoughts”">#</a></h2>
<p>Split tunnel WireGuard gives you the best of both worlds: fast, private access to your services without compromising your normal internet experience. The setup takes maybe 30 minutes and the result is rock solid. I have had zero downtime in months.</p>
<p>The only complexity is the DNS piece, and even that is just a few lines in a dnsmasq config file. If you are running private services on a VPS, this is the way to expose them.</p>
]]></content:encoded></item></channel></rss>