← blog

hopsock: Getting Chrome to Use an Authenticated SOCKS5 Proxy

For a while I had a shell script called chrome-proxy.sh that did one job. It started gost as a local SOCKS5 proxy, pointed it at a residential proxy that needs a username and password, waited for it to come up, then launched Vivaldi with the right flags. It worked, but it was a bash script babysitting a big general-purpose binary, and I wanted something I actually understood end to end. So I wrote hopsock.

hopsock dashboard

Why this needs to exist at all

Chromium browsers (Chrome, Vivaldi, Brave, Edge) support SOCKS5 proxies, but not SOCKS5 authentication. There’s no flag for it and no prompt. You can hand the browser socks5://user:pass@host:port and it’ll just ignore the credentials and fail.

The usual workaround is a local proxy with no auth that forwards to the real one with auth. That’s all gost was doing for me. hopsock does the same thing, plus the two things I actually cared about: SSH hops, and launching the browser safely.

The whole thing is one interface

Every hop is a Dialer, and every dialer takes another dialer as its Forward. That’s the entire design.

type Dialer interface {
	DialContext(ctx context.Context, network, addr string) (net.Conn, error)
}

A SOCKS5 hop dials its proxy server through Forward, does the handshake, then asks for the target. An SSH hop dials the SSH server through Forward, keeps one session open, and opens a direct-tcpip channel per connection, the same thing ssh -D does. The first hop forwards through a plain net.Dialer. Stack them and you get a chain:

hopsock -F 'socks5://user@proxy:1080?passenv=PROXY_PASS' -F 'ssh://me@box'

That reaches the SSH box through the residential proxy, and traffic exits from the box. The local side is a small SOCKS5 server whose only job is to read a target address from the browser and call DialContext on the chain.

Hostnames go through unresolved the whole way, so DNS happens at the last hop and not on my machine. That matters more than it sounds, which brings me to the browser.

Launching the browser without leaks

Pointing Chrome at a SOCKS5 proxy isn’t enough on its own. Two things still go around it:

hopsock adds both every time it launches a browser, so I don’t have to remember them.

The bigger trap was the profile. If Vivaldi is already running and you start it again with proxy flags, the new process just hands off to the existing one, opens a window, and silently drops every flag. You get a normal, unproxied window, and it looks exactly like the proxied one. So hopsock checks the process list first and refuses to launch if the browser is already running on your normal profile. If you want both at once, -profile gives it a separate --user-data-dir.

That check had its own little bug. On my machine it kept saying Vivaldi was running when it wasn’t. The culprit was zombie processes left behind by an earlier crash, still sitting in ps with a Z state. Skipping those fixed it. Linux also cuts the process name to 15 characters, so the match has to allow for truncated names.

Small things that bit me

Passwords in URLs are a pain. Proxy passwords tend to have characters that need URL escaping, and putting them in a command means they end up in shell history. So hops take passenv=VAR and read the password from an environment variable instead. On the first real run I pasted the password itself as the value, which is exactly what you’d expect to happen. Now it checks that the value looks like a variable name and says so, without echoing what you typed back to the terminal.

A + that turned into a space. You can pin an SSH host key with hostkey=SHA256:... instead of using known_hosts. Those fingerprints are base64, and base64 has + in it. Go’s query parsing turns + into a space, so pinned keys with a + never matched. The fix is one line that puts the + back, and there’s a test for it now.

SSH sessions go stale. Laptops sleep and Wi-Fi drops. The SSH hop sends keepalives, and if opening a channel fails on a dead session, it reconnects once and tries again before giving up. The dashboard shows the session state, so I can tell whether a problem is the SSH box or the proxy.

The dashboard

It started as plain log output and turned into a Bubble Tea dashboard because I kept wanting to know what was going on. At startup it fetches ipinfo.io through the chain, so the first thing you see is the exit IP and location, and you know it’s working before you open anything. Below that are the SSH state, live connections with bytes in each direction, and a log. Press b and it opens a browser through the proxy.

There’s a -plain mode for scripts, and -exec runs any command with the proxy address filled in, so it isn’t only for browsers.

Where it’s at

It’s at 1.0.0, with binaries for Linux, macOS and Windows on the releases page. It’s deliberately small. If what you want is “make my browser use this authenticated proxy, maybe through an SSH box, and don’t leak”, that’s what this is for.

It’s also a Go package, so you can build the same chain in your own program and use it as an http.Transport dialer.

Source and releases: github.com/abdullahwaqar/hopsock