One private subnet through the tunnel and everything else along your normal path is a small request with a large number of ways to get it wrong. What decides the outcome is not the checkbox labelled split tunneling but where the decision is enforced: the client's own route table, a route the server pushes, or a policy the two ends negotiate. Pick the wrong layer and you get either a full tunnel you never wanted or a route pointing into a tunnel the far side refuses to accept. I use a three-condition test before opening any config, then pick one of the mechanisms below.

Three conditions that settle the design

First, you need the exact prefix and mask from the network owner. The 10.x network is not a prefix; 10.20.0.0/16 is. Confirm it does not overlap your home LAN, a Docker bridge or another VPN you run, because an overlap makes the route ambiguous and the kernel picks the entry you did not intend.

Second, who answers the return traffic. Routing a packet into the tunnel only gets it to the VPN server. If the subnet sits behind another router that has no route back to your client pool, the server must masquerade the traffic, or that router must learn your pool. No client-side route fixes a missing return path.

Third, the enforcement point. Some clients install a route and the operating system does the rest. Others negotiate a policy, and a policy the gateway does not accept produces no tunnel at all: packets are dropped with no obvious error. Work out which of the two you own before writing anything.

WireGuard: AllowedIPs is the routing table

On the client, AllowedIPs lists the destinations that may enter the tunnel, and wg-quick turns each entry into an ordinary route. So the client half of this task is one line.

[Peer]
PublicKey = ...
Endpoint = vpn.example.net:51820
AllowedIPs = 10.20.0.0/16

Now compare the server's entry for that same client. It describes what the server will send to the client, which is normally the client's tunnel address, not the private subnet.

[Peer]
PublicKey = ...
AllowedIPs = 10.99.0.2/32

Swapping those two is the mistake I see most often. Putting 10.20.0.0/16 under the server's peer section states that the whole subnet lives behind that client, which is a site-to-site claim rather than a permission. Packets that match no peer's AllowedIPs are dropped silently, so a wrong entry looks exactly like a firewall problem.

If you manage routes yourself, set Table = off under [Interface] and add the route you want. Nothing is added automatically then, so the route belongs in whatever brings the interface up.

OpenVPN: push the route, then make sure it lands

On a normal server the change is one directive in the server config, next to the client pool.

push "route 10.20.0.0 255.255.0.0"

The client adds it when it connects. The failure mode shows up when the same profile also pushes redirect-gateway, or when someone has already added route-nopull: the first turns the session into a full tunnel, and the second discards every pushed route including yours.

A client that needs narrow behaviour against a full-tunnel server should ignore only the parts it does not want. On OpenVPN 2.4 and later, pull-filter exists for exactly that.

pull-filter ignore "redirect-gateway"
route 10.20.0.0 255.255.0.0

Keep address families separate: a pushed IPv4 route does nothing for an internal service that resolves to IPv6, and that service fails over the tunnel while everything else looks fine.

When the private subnet is not on the VPN server but behind another client, the server has to be told. Add a per-client file through client-config-dir containing iroute 10.20.0.0 255.255.0.0, plus the matching route so other clients learn the path. Without iroute, the server has no reason to send that prefix down that particular tunnel.

IPsec: the traffic selectors have to agree

With IPsec there is no route to push. The two ends negotiate which addresses live inside the tunnel, and traffic outside the agreed selectors is neither encrypted nor sent through it. On a strongSwan client the selectors are explicit.

leftsubnet=192.168.1.0/24
rightsubnet=10.20.0.0/16

If the gateway is configured with 0.0.0.0/0 on both sides, IKEv2 narrowing usually reduces the child SA to the intersection of the two proposals, which is the outcome you want. Some gateways instead reject a proposal that does not match their configured selectors, and the log then shows a failed child SA rather than a routing complaint. When that happens the change belongs on the gateway, because a client route cannot create a tunnel for a selector the other side will not accept.

On Windows the built-in client hides the same decision behind two settings. Split tunneling must be on, and the prefix must be registered against the profile.

Set-VpnConnection -Name Office -SplitTunneling $true
Add-VpnConnectionRoute -ConnectionName Office -DestinationPrefix 10.20.0.0/16

Registering the prefix is what makes it survive a reconnect, which a hand-added route does not.

Keep the endpoint and DNS outside the decision

The VPN endpoint must stay reachable without the tunnel, or the tunnel collapses into itself. When only 10.20.0.0/16 is routed that is automatic, since the endpoint is a public address. On a full-tunnel profile it is not, and the client needs an explicit host route to the endpoint before the tunnel comes up.

DNS is a second decision that a route push does not make for you. Internal names usually resolve through a resolver only the private network can reach. If the server pushes a DNS server, every name goes through it; if it does not, internal names either fail or resolve publicly to something you did not intend. Split DNS keeps one search domain on the internal resolver, but it depends on client support, so treat it as its own task rather than a side effect of routing.

Verify, then reboot and verify again

Three probes cover the routing question. Ask the kernel where a remote address would go, fetch something from inside the subnet, then confirm that an ordinary public request still leaves through your normal connection.

ip route get 10.20.5.7
curl -sS http://10.20.5.7/health
curl -sS https://ifconfig.me

The first line names an interface. If it names your physical interface, the route never landed. On Windows, Find-NetRoute -RemoteIPAddress 10.20.5.7 answers the same question, and the last probe should still show your ordinary address rather than the VPN server's.

Then reboot. Routes added by hand disappear, while routes from the profile, from a pushed config or from AllowedIPs come back. If narrow access works before the restart and fails after it, the fault is in how the tunnel starts, not in the route itself.

Before calling it done, confirm four things: the prefix came from the network owner and overlaps nothing local, the return path exists or a masquerade rule replaces it, the mechanism you configured is the one that enforces routing on your platform, and the same probes pass after a clean restart. The strongSwan forwarding notes are worth reading if the return path is the part that keeps failing.

ບົດຄວາມທີ່ກ່ຽວຂ້ອງ