You Don’t Always Need a VPN
Someone locks down a service to their office network. You’re not at the office. Your first instinct is probably “spin up WireGuard” or “beg the IT guy to add my IP.” But here’s the thing — if you already have SSH access to a machine on that network, you’ve got everything you need.
ssh -D 1080 user@yourserver — that’s it. One flag, one port, and you’ve got a SOCKS5 proxy running on localhost:1080. No VPN client to install, no kernel module drama, no firewall rule negotiation. Point your browser (or curl, or git) at it and your traffic comes out the other side as if you were sitting at that machine.
This is selective routing. Your Spotify doesn’t go through the tunnel. Your apt update doesn’t go through the tunnel. Only the apps you tell go through the tunnel — which is exactly what you want when you’re proxying into a work network and you don’t want your entire internet connection running through your employer’s infrastructure. Everybody wins.
The Basics: ssh -D
ssh -D 1080 -C -N -f user@yourserverBreaking that down:
-D 1080— open a dynamic SOCKS proxy on local port 1080-C— compress traffic (worth it on slow links)-N— don’t execute a remote command, just forward-f— background the process after auth
You now have a SOCKS5 proxy at 127.0.0.1:1080. Anything that speaks SOCKS5 can use it. That’s a lot of things.
To verify it’s listening:
ss -tlnp | grep 1080To kill it later:
pkill -f "ssh -D 1080"The DNS Leak Gotcha: socks5h vs socks5
Before anything else — this is the gotcha that bites people. If you configure an app to use socks5://127.0.0.1:1080, the DNS lookup still happens locally before the traffic goes through the proxy. Your query for internal.corp.example leaks to your local resolver, which probably has no idea what that is.
The fix is socks5h — the h stands for “hostname forwarding.” DNS resolution happens on the remote side, through the tunnel.
# Wrong — DNS leaks locallysocks5://127.0.0.1:1080
# Right — DNS resolves on the remote serversocks5h://127.0.0.1:1080This matters any time you’re accessing hostnames that only resolve inside the remote network. Keep socks5h in your muscle memory.
Browser: FoxyProxy and SwitchyOmega
The easiest win. Both extensions let you define proxy profiles and switch between them per-URL pattern.
FoxyProxy (Firefox and Chrome) — add a new proxy:
- Type: SOCKS5
- Hostname: 127.0.0.1
- Port: 1080
- Enable “Remote DNS” (this is the socks5h equivalent in the UI — don’t skip it)
Then create a pattern like *.corp.example to only proxy matching URLs. Everything else hits your normal connection. It’s fast to toggle and you can have multiple profiles for multiple tunnels.
SwitchyOmega (Chrome/Edge) — same idea, more granular. You can set up auto-switch rules that check the URL against a PAC-style list. Useful if you’ve got a dozen internal hostnames to route.
Both extensions respect SOCKS5 DNS correctly as long as you enable the remote DNS option. Don’t forget that checkbox.
curl: —socks5-hostname
For curl, the flag to reach for is --socks5-hostname, not --socks5. Again, same DNS forwarding distinction.
curl --socks5-hostname 127.0.0.1:1080 http://internal.corp.example/api/healthOr set it in your environment for the session:
export ALL_PROXY=socks5h://127.0.0.1:1080curl http://internal.corp.example/api/healthALL_PROXY is respected by curl, wget, and a bunch of other tools. Set it, do your work, unset it when done.
git: proxy.command
Git doesn’t speak SOCKS natively, but you can tell it to use nc (netcat) as a proxy command:
[http] proxy = socks5h://127.0.0.1:1080
[core] gitProxy = nc -X 5 -x 127.0.0.1:1080The http.proxy setting covers HTTPS remotes. The gitProxy / proxy.command with nc covers native git protocol (the git:// scheme). -X 5 tells nc to use SOCKS5. -x is the proxy address.
For a single repo or a one-off:
GIT_SSH_COMMAND="ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:1080 %h %p'" \Ugly but it works. The gitconfig approach is cleaner if you’re accessing internal repos regularly.
ProxyChains: For Stubborn Apps
Some apps just don’t have proxy settings. Electron apps, random CLI tools, that one Java binary from 2011 that you can’t recompile. ProxyChains wraps any program and forces its TCP connections through your proxy.
Install it:
sudo apt install proxychains4 # Debian/Ubuntusudo pacman -S proxychains-ng # ArchConfigure it:
strict_chainproxy_dnsquiet_mode
[ProxyList]socks5 127.0.0.1 1080proxy_dns handles the DNS-through-the-proxy bit. Then just prefix your command:
proxychains4 wget http://internal.corp.example/file.tar.gzproxychains4 nmap -sT internal.corp.exampleIt’s not pretty — there’s overhead and some apps detect and reject it — but for the stubborn cases, it saves you from installing a full VPN just to run one command.
autossh + systemd: Keep the Tunnel Alive
The ssh -f background approach dies when the connection drops. For a tunnel you want always-on, combine autossh with a systemd user service.
Install autossh:
sudo apt install autosshCreate the service:
[Unit]Description=SOCKS5 SSH TunnelAfter=network-online.target
[Service]Type=simpleExecStart=/usr/bin/autossh \ -M 0 \ -N \ -o "ServerAliveInterval=30" \ -o "ServerAliveCountMax=3" \ -o "ExitOnForwardFailure=yes" \ -D 1080 \ user@yourserverRestart=alwaysRestartSec=10
[Install]WantedBy=default.target-M 0 disables autossh’s own monitoring port (we’re using SSH keepalives instead via ServerAliveInterval). ExitOnForwardFailure=yes makes sure autossh actually restarts when port binding fails rather than quietly running without the forward.
Enable and start:
systemctl --user daemon-reloadsystemctl --user enable --now ssh-tunnel-proxy.servicesystemctl --user status ssh-tunnel-proxy.serviceNow your SOCKS5 proxy survives reboots, network drops, and laptop lid-closes. Your 2 AM self will appreciate it.
Multi-hop: SSH ProxyJump
Sometimes your target isn’t directly reachable — you need to go through a bastion/jump host first. ProxyJump handles this cleanly:
Or in your ssh config, which is cleaner:
Host internal-target HostName internal-host.corp.example User user ProxyJump bastion.corp.example
Host bastion.corp.example User jumpuser IdentityFile ~/.ssh/id_ed25519_bastionThen your tunnel command becomes:
ssh -D 1080 -N -f internal-targetThe full connection flows: your machine → bastion → internal host → SOCKS proxy back to you. From the outside, you’re coming from the internal host’s IP. This ProxyJump + bastion pattern is worth understanding deeply — we’ll dig into bastion host architecture properly in an upcoming article.
SOCKS5-over-SSH vs WireGuard vs Tailscale
Here’s the honest breakdown.
Use SOCKS5-over-SSH when:
- You already have SSH access to the target machine
- You only need to proxy one or two apps, not your whole system
- You don’t want to (or can’t) install anything on the remote end
- It’s a one-off task — checking a thing, pulling a file, testing an endpoint
- You’re accessing a jump-host chain that’s already SSH-accessible
Use WireGuard when:
- You need system-wide routing — DNS, all traffic, all apps
- Performance matters (WireGuard is significantly faster than SSH tunnels for sustained throughput)
- You’re building something permanent that multiple users will share
Use Tailscale when:
- You want zero-config mesh networking across many machines
- You’re willing to route through Tailscale’s coordination server
- The WireGuard setup complexity isn’t worth it for your use case
The SOCKS5 approach shines in exactly the “I have SSH, I need to reach one thing, I don’t want to yak-shave a VPN setup” scenario. It also doesn’t require any elevated permissions on the remote host or any software installation. If you can SSH in, you can proxy. That’s the whole value proposition.
For permanent infrastructure where everyone on the team needs access, Tailscale or WireGuard are the right answer. For “I need to quickly check the internal Grafana dashboard from home,” ssh -D 1080 is done before you’ve finished installing WireGuard.
Quick Reference
# Open tunnelssh -D 1080 -C -N -f user@host
# curl through tunnel (DNS via proxy)curl --socks5-hostname 127.0.0.1:1080 http://target
# Set env for sessionexport ALL_PROXY=socks5h://127.0.0.1:1080
# Multi-hopssh -D 1080 -N -J jumphost user@target
# Kill tunnelpkill -f "ssh -D 1080"DNS leak rule: always socks5h, never socks5. Tunnel persistence: autossh + systemd. Browser switching: FoxyProxy with remote DNS enabled. The rest is just picking the right tool for the job.