The laptop connects, the tunnel is up, and requests to an internal service hang at connect. On the service itself the request arrives from an address like 10.90.0.25, the reply leaves toward the default gateway, and nothing comes back, because that gateway has no idea where 10.90.0.25 lives. From the other end the same fault looks like an internal host that cannot ping a connected client. Both are one missing object: a return route to the VPN client pool, which only the private network can add.

Read the source address the service sees

That single field decides everything else. If the internal service sees the pool address, the Access Server is routing rather than hiding client traffic, and every reply has to be routed back to the pool. If it sees the server's own private address instead, the server is masquerading and the reply path already works, at the cost of losing any per-client identity in the service's logs and access rules.

Compare both ends before changing anything. On the server, list the tunnel interfaces and their addresses; on the client, confirm which of those addresses it actually holds; on the internal host, look at a live connection.

ip -br addr show
ss -tnp state established '( dport = :443 )'
ip route get 10.90.0.25

The last command is the one that fails in the broken case: it either reports the tunnel prefix as unreachable or, worse, returns your default gateway, which is how replies end up on the internet.

Do not assume one pool. A server can have a dynamic range plus per-user static mappings, and the static address may sit outside the dynamic prefix entirely. If you route only the range you remember, the users with fixed addresses stay broken and everyone else reports the problem as fixed.

Add the route where the reply is decided

The route belongs on the gateway that the internal service already uses, not on the service unless it routes its own traffic. Point it at the Access Server's reachable private address, which is a different value from the tunnel address a client holds.

ip route add 10.90.0.0/24 via 10.0.1.5
ip route get 10.90.0.25

In a cloud network the same statement becomes a route table entry whose target is the instance or network interface running the server, with source and destination checking disabled on that interface. Note what that entry does not do: a route is not an access grant, so security groups, network ACLs and the host firewall still have to permit the traffic.

Make it persistent, and keep the pool sizes honest. Two non-contiguous pools need two routes, and a pool that grows past its configured prefix silently produces clients whose replies have nowhere to go. If you control the pool ranges, choose a summary you can route once and leave alone.

Forwarding and filters must agree with the route

Routing is only half of the path. The server must be allowed to forward between the tunnel and the private interface, which on Linux means net.ipv4.ip_forward plus a forward chain that permits the pair of interfaces, and on a hardened host usually an explicit rule for each direction.

sysctl net.ipv4.ip_forward
nft list ruleset | grep -A5 forward

Stateful filtering explains a symptom that sends people down the wrong path. A reply inside an established flow does not need a new inbound rule, so a connection that hangs after the SYN-ACK is not being blocked by a missing accept rule; it is being dropped or misrouted. Keep that distinction, because it separates a routing change from a firewall change.

Asymmetric return paths are the quieter version of the same problem. If two routes reach the pool and the reply takes the one that leaves from a different interface than the request arrived on, strict reverse-path filtering drops it and the flow fails only for some clients.

Routed or masqueraded: pick deliberately

Routed mode preserves the client's pool address at the service, which makes per-client logging and per-client policy possible, and in exchange every internal network that must answer a client needs the pool route. Masqueraded mode removes that requirement because replies return to the server, but the service can no longer tell one client from another and cannot initiate anything toward a specific client.

Changing that behaviour on a live server drops existing sessions, so test with one client and one flow first. If the switch is the right answer, it is a configuration decision on the server, not something to work around by adding routes that will become unnecessary.

A subnet behind the client is a separate task

Reaching a device on the laptop's own LAN is not the same as reaching the laptop's tunnel address. It requires the client to act as a subnet gateway: a declared client-side network, forwarding enabled on the client's operating system, and a route on that LAN back toward the client. Adding a private-network route for the laptop's home range does not do this, and it creates conflicts when two users have the same home range.

Direction is a policy question too. Permission for a client to open a connection to an internal service does not automatically let an internal host open a connection to that client. Check the server's access policy and the client's host firewall before concluding that the path is broken.

Verify with one flow and one capture

Prove the path with a single connection instead of a general reachability test. Watch the same flow at both ends: the request arriving at the service and the reply leaving it.

tcpdump -ni any host 10.90.0.25 and port 443

Before the fix the capture shows the reply leaving toward a gateway that will not route it. After the fix the reply leaves toward the server and the flow completes. Then confirm the route the gateway would use for a second client address, so a pool you have not covered shows up now rather than in a week.

Finish with the four checks that close this out: the source address at the service matches the mode you intended, the gateway has a route to every pool in use, forwarding and filters permit both directions, and one real flow completes end to end. The Access Server routing FAQ states the same principle for the installed release, whose supported configuration interface may differ from older guides.

Barruulee wal qabatan