SSH SOCKS proxy on a Mac: route one site, not everything

· · 9 min read

An SSH SOCKS proxy sends an app’s connections through a server you can SSH into. This walkthrough uses macOS Terminal/OpenSSH and a host that allows TCP forwarding. I checked the config block and the PAC file below on macOS 26.7 with OpenSSH 10.3p1; the tunnel and proxy commands are quoted from the official manuals.

Start an SSH SOCKS proxy on macOS

To start the SSH proxy, run this in Terminal, replacing the SSH destination:

ssh -D 127.0.0.1:1080 -N -C user@host

-D opens a SOCKS4/5 listener, -N requests no remote command, and -C compresses data. The OpenBSD ssh(1) manual (2022) warns compression can slow fast connections. The command stays attached to Terminal after you authenticate.

Bind to 127.0.0.1 to keep the listener on your Mac. OpenSSH says * or an empty address listens on all interfaces. Use an unused port above 1023.

To save the port and keepalive settings for a host, add this block to ~/.ssh/config:

Host staging
    HostName host.example
    User user
    DynamicForward 127.0.0.1:1080
    ServerAliveInterval 15
    ServerAliveCountMax 3
    ExitOnForwardFailure yes

Then start it with:

ssh -N -C staging

DynamicForward is the config form of -D, which sets up SSH dynamic port forwarding. A 15-second interval and count of 3 disconnects after about 45 seconds without a reply; ExitOnForwardFailure quits if SSH cannot bind (OpenBSD’s 2026 ssh_config(5) manual). Expected: SSH stays open at 127.0.0.1:1080.

Check the route and DNS

Ask curl to fetch an IP-check page through the tunnel:

curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me

The response is the public IP seen by that site; compare it with a direct request. curl’s --socks5-hostname documentation sends the name to the proxy; --socks5 resolves it locally first.

If curl says Failed to connect to 127.0.0.1 port 1080, check SSH and the port. Curl success proves only curl’s route; confirm the browser separately.

Choose which traffic uses the tunnel

Use the narrowest route that fits the task. SOCKS works per application, not for the whole machine. RFC 1928 (1996) describes it as a shim layer that does not provide network-layer gateway services, so it carries only the connections of apps set to use it.

Option Scope DNS through the tunnel? Sleep or reconnect Setup Undo
macOS SOCKS setting Apps using that network service’s system proxy Depends on the app; verify it Plain SSH needs reconnecting Set SOCKS host to 127.0.0.1, port 1080 Turn SOCKS proxy off
Firefox manual proxy Firefox Yes when “Proxy DNS when using SOCKS v5” is checked Plain SSH needs reconnecting Manual proxy, SOCKS v5, host and port Select “No proxy”
Chrome launch flag One separate Chrome instance Chromium says URL hostnames resolve at the proxy, but other DNS requests may be local Plain SSH needs reconnecting Launch Chrome with --proxy-server and its own data directory Quit that instance
PAC file Selected hosts in a PAC-aware app or system service Client-dependent; verify in that app Plain SSH needs reconnecting Match hostnames to SOCKS; return DIRECT for others Disable/remove the PAC URL
Octoweb per-site proxy Listed sites in Octoweb Check the route with curl or the page you need Restarts a dropped tunnel Settings → Proxies → SSH tunnel Switch the proxy off or quit Octoweb

Use the macOS network service setting

Open System Settings → Network → Wi-Fi → Details → Proxies, enable SOCKS proxy, and enter 127.0.0.1 and port 1080. Apple Support’s 2026 proxy settings guide documents the path; choose your active service if it isn’t Wi-Fi.

You can also set the proxy in Terminal. First check the exact service name with networksetup -listallnetworkservices. The following assumes it is called Wi-Fi:

sudo networksetup -setsocksfirewallproxy "Wi-Fi" 127.0.0.1 1080 off
networksetup -getsocksfirewallproxy "Wi-Fi"

The setter requires admin privileges. networksetup(8) (2020) documents the arguments. The second command reports server, port, and state; change Wi-Fi to your service name.

Proxy just one browser

In Firefox, open Settings → Privacy & Security, go to Connection and software security, and click Advanced settings → Proxy settings → Configure proxy, as Mozilla’s connection settings guide describes. Select Manual proxy configuration, set SOCKS Host 127.0.0.1, Port 1080, and SOCKS v5, then check Proxy DNS when using SOCKS v5, the setting Mozilla’s enterprise policy reference calls UseProxyForDNS. Undo by selecting No proxy.

Launch Chrome with a new data directory to keep this proxy instance separate:

open -na "Google Chrome" --args --proxy-server="socks5://127.0.0.1:1080" --user-data-dir=/tmp/chrome-socks-proxy

Chromium’s SOCKS guide, checked October 2026 says HTTP and HTTPS URL hostnames are resolved by the proxy, but its network stack can still make direct DNS requests, such as prefetches. Its separate profile guide, checked October 2026 documents --user-data-dir. Expected: Chrome opens a separate profile window. Quit that window to undo the setting.

Route only chosen sites with a PAC file

Save this example as proxy.pac, changing the two sample domains:

function FindProxyForURL(url, host) {
	if (
		host === 'admin.example.com' ||
		dnsDomainIs(host, '.admin.example.com') ||
		host === 'staging.example.net' ||
		dnsDomainIs(host, '.staging.example.net')
	) {
		return 'SOCKS5 127.0.0.1:1080; SOCKS 127.0.0.1:1080';
	}
	return 'DIRECT';
}

The exact-host checks include each base domain; the leading dot in dnsDomainIs matches its subdomains without matching names such as notadmin.example.com. MDN’s PAC guide, checked October 2026 documents FindProxyForURL, dnsDomainIs, SOCKS5, and DIRECT. Expected: the sample domains and their subdomains use the tunnel; other hosts are direct.

On macOS, open Network → service → Details → Proxies, enable Automatic proxy configuration, and enter the PAC URL. Firefox takes a PAC URL in the same Connection Settings dialog (Automatic proxy configuration URL), and its connection guide says file: URLs work there. Apple and Chromium don’t document local file:// support; use an HTTP(S) URL if a client ignores it. Turn off Automatic proxy configuration to undo.

PAC routing is by requested hostname; DNS behavior varies by client. Add API or asset domains to the match list or they go direct. Test a hostname only the SSH server can resolve.

Use per-site routing inside Octoweb

As a browser with built-in proxy routing, Octoweb 0.16 has Settings → Proxies for SSH tunnels and SOCKS5 or HTTP proxies. Add the admin or staging domains, or a website you want to view from another country through a proxy there, and other tabs stay direct; a proxied tab sends the page and its requests through the proxy. I build Octoweb, so weigh that accordingly.

The 0.16 release notes describe the SSH host formats, restart behavior, and separate cookie store. This option requires macOS 14 or later; it is ignored on macOS 13. Octoweb is free and open source, and the install section lists its Mac builds.

Keep the tunnel available after sleep

ServerAliveInterval 15 and ServerAliveCountMax 3 detect an unresponsive connection in about 45 seconds, but don’t reconnect it. After sleep, restart plain SSH once the network returns.

If autossh is installed, use it with the keepalive options to restart SSH after it exits; -M 0 disables its monitor port and lets SSH keepalives trigger restart. The autossh manual, checked October 2026 describes this mode. Recovery still depends on the server being reachable:

autossh -M 0 -N -C -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -D 127.0.0.1:1080 user@host

Expected: it stays attached after authentication; if SSH exits, autossh starts it again.

For Octoweb, the 0.16 notes say it checks every 15 seconds, notices a dead connection within about 45 seconds, and retries with backoff up to 30 seconds. Tabs that failed while it was down reload when the tunnel returns. That behavior is specific to Octoweb, not plain ssh.

Fix the errors that stop the tunnel

Message Cause Fix
bind [127.0.0.1]:1080: Address already in use Another process already owns port 1080. Find the listener with lsof -ti:1080, stop that tunnel, or use another unused port everywhere.
channel 1: open failed: administratively prohibited: open failed The server may forbid TCP forwarding, or the SSH key may disallow it. Ask the server administrator to check AllowTcpForwarding and any key restrictions; sshd_config(5) documents AllowTcpForwarding no.
Privileged ports can only be forwarded by root The requested local port is below 1024. Use 1080 or another port above 1023 instead.
Sites resolve locally despite SOCKS The client is doing local DNS resolution. In curl use --socks5-hostname; in Firefox check its SOCKS5 DNS option. Chromium warns that some DNS requests can still be local.

The address-in-use message appears verbatim in a 2026 cmux bug report about an SSH relay. The forwarding error and its common server-side cause are discussed in this Unix & Linux Stack Exchange thread and documented under AllowTcpForwarding in OpenBSD’s sshd_config(5) manual. A separate 2018 Unix & Linux Stack Exchange answer shows the privileged-port message and fixes it by moving the local port above 1023.

Undo every layer

Stop SSH with Ctrl-C in its Terminal. Stop autossh if used. Then turn off SOCKS proxy in Network → Proxies, choose No proxy in Firefox, quit the separate Chrome window, or disable and clear the PAC URL. For a Wi-Fi service, sudo networksetup -setsocksfirewallproxystate "Wi-Fi" off turns off SOCKS.

Check the end state: curl fails to connect to port 1080 after the SSH tunnel proxy stops, and the selected app or system service no longer points at the proxy.

See the Octoweb 0.16 SSH tunnel and per-site proxy notes for the product-specific behavior.

Don Karter

Don Karter

CEO & Co-founder at Muvon

20+ years in software engineering, the last decade deep in AI systems. Builds Octoweb — a keyboard-first AI browser for macOS on WebKit and Rust. Writes from the trenches: native browser engineering, agents driving real pages over MCP, and what breaks when every action has to be one keystroke away.

Octoweb is free and open source, for macOS.

Install it →