SSH Dynamic Forwarding as a SOCKS Proxy Over Your VPN
An app that only speaks SOCKS has no proxy endpoint on a VPN. Two hops, one loopback listener, and a name lookup the SSH server has to do itself.
An application that speaks only SOCKS has nothing to talk to on a plain VPN, because the tunnel carries packets and the app wants a proxy endpoint. OpenSSH can supply that endpoint when the access owner permits it, since dynamic forwarding turns one SSH session into a local proxy. Then there are two hops to keep straight: the tunnel that reaches the SSH host, and the connection that host makes onward. Separating them explains both why the arrangement works and what it will never do.
What the SSH listener does and does not become
Dynamic forwarding makes the SSH client listen locally and speak SOCKS4 or SOCKS5 to whatever connects. Each request names a destination and the SSH server opens the onward connection, which is why the destination lookup can happen on the far side. That is application-level TCP forwarding, not a whole-device VPN. It carries no UDP associate, no ICMP, and nothing for applications that ignore proxy settings entirely. The hop from the SSH server to the application is also not inside your tunnel, which matters when the destination accepts traffic only from known networks.
The prerequisites are real: a route to the SSH host, an authorized account, a verified server host key, and permission to forward. None of them is created by the proxy, and the last one is usually the first to be missing. Confirm all four before debugging anything else, because the failure looks identical from the client whichever one is absent.
Start it on loopback with an explicit port
Pick an unused local port and bind it explicitly to loopback rather than leaving the address off. The three options below exist for one reason each.
ssh -D 127.0.0.1:1080 -NT -o ControlPath=none -o ExitOnForwardFailure=yes \
[email protected]
-N skips the remote command and -T skips the pseudo-terminal, so the session exists only to forward. ExitOnForwardFailure makes the client exit when the listener cannot be created, instead of running without the forwarding you asked for. ControlPath=none keeps the session out of an existing multiplexed connection that might add its own forwardings. Loopback keeps the listener off the LAN, but it is not an authorization boundary on a shared machine, because other local processes can reach it too. The OpenSSH client reference documents all of these flags, and the loopback warning follows from what a listening socket is.
Send names through the proxy, not around it
A SOCKS5 client can resolve the destination itself or ask the proxy to do it, and that choice decides which resolver answers. With socks5h, curl sends the hostname through the proxy, so the lookup happens on the SSH server. With plain socks5, the laptop resolves first and only the connection travels, which quietly defeats the point when the name is internal. The request below also clears the no-proxy list, so an inherited NO_PROXY value cannot route it around the proxy.
curl -sS --noproxy '' --proxy socks5h://127.0.0.1:1080 https://app.corp.example/health
Certificate validation and the application's own login still apply, because the proxy replaces neither. Two lookups remain: the laptop must reach the SSH host through the permitted VPN and DNS path, and the SSH server must resolve the application name itself. The second one is testable on its own before any traffic is proxied.
ssh [email protected] getent hosts app.corp.example
Prove one allowed destination, then shut the session down
Test a destination the policy allows and one it should refuse, because a working forward proves that the listener works and nothing more. The application sees the source address the SSH server chooses, which is not your laptop's tunnel address. Where the far end logs connections by source, that difference is the first thing to check when a request is rejected.
Stop the session through the process that owns it rather than by killing an unrelated shell. Then confirm the listener is gone, because a port that still answers is a session you have not actually closed. Remove only the proxy setting this exercise introduced, and leave the rest of the application configuration alone. An unattended terminal holding a forwarding rule is not a service, and a permanent proxy needs an approved lifecycle rather than a forgotten window.
ss -ltn | grep 1080
Failures this arrangement cannot fix
Some symptoms point straight at the limits above, and recognising them early saves an afternoon of proxy debugging. The table maps what you observe to what it usually means. Anything in the last column is a design question rather than a configuration mistake.
| What you see | What it usually means | Where to look |
|---|---|---|
| The app connects and nothing follows | It ignores proxy settings entirely | Its own network settings, not the SSH session |
| A name resolution error for the destination | The SSH server cannot resolve it | Server resolver and search domains |
| A UDP-only application fails | Dynamic forwarding carries TCP only | This arrangement cannot serve it |
| curl works, the browser does not | The browser is not using the socks5h endpoint | Browser proxy configuration |
| The listener answers after the session ended | Another process owns port 1080 | The ss output, then the owning process |
The first three rows are the ones people misdiagnose, because each produces a connection attempt rather than an error about the proxy itself. A name that resolves on the laptop says nothing about the server's resolver, and the failure reads as a certificate or timeout problem instead. UDP-only applications sit outside the design rather than broken inside it, and no extra ssh option changes that. The browser row is the ordinary reminder that proxy settings are per application. The last row is why the shutdown check exists: a port that still listens means something else owns it now.