SOCKS5 in context
SOCKS is an older proxy protocol built to relay arbitrary TCP or UDP traffic through forward proxies. The current version, SOCKS5, was specified in RFC 1928 in 1996 to make dialing through firewalls possible. Its design predates HTTP/1.1 and the CONNECT method — worth keeping in mind when comparing it to the CONNECT tunnels discussed in part 2 of this series.
The protocol itself is short and readable in the RFC, so rather than restating it, this part moves directly to using SOCKS5 from Go.
Dialing through a SOCKS5 proxy
SOCKS5 is a forward proxy protocol. A proxy server listens on a host and port, and clients are told to route traffic through it. The conventional port is 1080.
For a working demonstration the article uses microsocks, a small C implementation from GitHub that is easy to clone and build locally. Run without authentication:
$ ./microsocks
An unauthenticated run, with the proxy listening on port 1080. With that up, curl can be pointed at it:
$ http_proxy=socks5://localhost:1080 curl -v http://example.org * Uses proxy env variable http_proxy == 'socks5://localhost:1080' * Trying 127.0.0.1:1080... * SOCKS5 connect to IPv4 93.184.216.34:80 (locally resolved) * SOCKS5 request granted. * Connected to (nil) (127.0.0.1) port 1080 (#0) > GET / HTTP/1.1 > Host: example.org > User-Agent: curl/7.81.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK // ... rest of response
The microsocks console should log a connecting client. What makes it work is the http_proxy environment variable carrying a socks5:// prefix in the proxy address; the same works by default with Go's net/http, so the simple client can be run the same way:
$ http_proxy=socks5://localhost:1080 go run http-get-basic.go http://example.org Response status: 200 OK <!doctype html> // ... rest of response
Setting https_proxy instead does the same thing over TLS:
$ https_proxy=socks5://localhost:1080 curl -v https://example.org * Uses proxy env variable https_proxy == 'socks5://localhost:1080' * Trying 127.0.0.1:1080... * SOCKS5 connect to IPv4 93.184.216.34:443 (locally resolved) * SOCKS5 request granted. * Connected to (nil) (127.0.0.1) port 1080 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * CAfile: /etc/ssl/certs/ca-certificates.crt * CApath: /etc/ssl/certs * TLSv1.0 (OUT), TLS header, Certificate Status (22): // ... rest of TLS handshake and response
As with a CONNECT tunnel from part 2, once SOCKS5 sets up the forwarding it treats the payload as opaque bytes and stays unaware of its contents. It streams any protocol cleanly, TLS included.
Username/password authentication
SOCKS5 supports authentication through several methods, and the mechanism is extensible by design; the focus here is the username/password method from the original RFC. With microsocks running behind credentials:
$ ./microsocks -u myuser -P mypass
A request with no credentials supplied fails:
$ http_proxy=socks5://localhost:1080 curl -v http://example.org * Uses proxy env variable http_proxy == 'socks5://localhost:1080' * Trying 127.0.0.1:1080... * No authentication method was acceptable. * Closing connection 0 curl: (97) No authentication method was acceptable.
Supplying matching credentials in the proxy URL — note the username and password before the address — succeeds:
$ http_proxy=socks5://myuser:mypass@localhost:1080 curl -v http://example.org * Uses proxy env variable http_proxy == 'socks5://myuser:mypass@localhost:1080' * Trying 127.0.0.1:1080... * SOCKS5 connect to IPv4 93.184.216.34:80 (locally resolved) * SOCKS5 request granted. * Connected to (nil) (127.0.0.1) port 1080 (#0) > GET / HTTP/1.1 > Host: example.org > User-Agent: curl/7.81.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK // ... rest of response
The same environment setting is honored by Go's HTTP client by default. Credentials can also be provided in code rather than through the environment:
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"golang.org/x/net/proxy"
)
func main() {
target := flag.String("target", "http://example.org", "URL to get")
proxyAddr := flag.String("proxy", "localhost:1080", "SOCKS5 proxy address to use")
username := flag.String("user", "", "username for SOCKS5 proxy")
password := flag.String("pass", "", "password for SOCKS5 proxy")
flag.Parse()
auth := proxy.Auth{
User: *username,
Password: *password,
}
dialer, err := proxy.SOCKS5("tcp", *proxyAddr, &auth, nil)
if err != nil {
log.Fatal(err)
}
client := &http.Client{
Transport: &http.Transport{
Dial: dialer.Dial,
},
}
r, err := client.Get(*target)
if err != nil {
log.Fatal(err)
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(body))
}
The golang.org/x/net/proxy package exposes explicit proxy tooling; here, its SOCKS5 dialer acts as the custom Dial on a Transport. The client is started like this:
$ go run http-get-socks-transport.go -proxy localhost:1080 \
-user myuser -pass mypass \
http://example.org
<!doctype html>
<html>
// ... rest of response
How SOCKS5 differs from CONNECT
Against the CONNECT approach from part 2:
- SOCKS predates
CONNECT. It needs a dedicated service on a dedicated port, whileCONNECTtunnel handling can be folded into an existing HTTP server and share its port. - SOCKS5 carries any TCP or UDP traffic;
CONNECTcarries only TCP. - SOCKS5 security is primitive by comparison. An HTTP proxy can use TLS underneath for client-server authentication and traffic encryption. Wrapping SOCKS5 in TLS is not difficult —
ssh -Dalready does it — and running SOCKS5 over SSH port forwarding is another option.
A SOCKS5 server in Go
Several SOCKS5 implementations exist in Go. One chosen for clarity and simplicity is go-socks5, from one of Hashicorp's co-founders. Its README has a basic setup sample; below is a slightly more advanced variant using basic authentication, matching the earlier microsocks example:
package main
import (
"flag"
"github.com/armon/go-socks5"
)
type myCredentialStore struct {
user string
password string
}
func (cs *myCredentialStore) Valid(user, password string) bool {
return user == cs.user && password == cs.password
}
func main() {
username := flag.String("u", "", "username for SOCKS5 proxy")
password := flag.String("P", "", "password for SOCKS5 proxy")
flag.Parse()
auth := socks5.UserPassAuthenticator{
Credentials: &myCredentialStore{user: *username, password: *password},
}
conf := &socks5.Config{
AuthMethods: []socks5.Authenticator{auth},
}
server, err := socks5.New(conf)
if err != nil {
panic(err)
}
if err := server.ListenAndServe("tcp", "127.0.0.1:1080"); err != nil {
panic(err)
}
}
Start it as follows:
$ go run . -u myuser -P mypass
and curl behaves as before:
$ http_proxy=socks5://myuser:mypass@localhost:1080 curl -v http://example.org * Uses proxy env variable http_proxy == 'socks5://myuser:mypass@localhost:1080' * Trying 127.0.0.1:1080... * SOCKS5 connect to IPv4 93.184.216.34:80 (locally resolved) * SOCKS5 request granted. * Connected to (nil) (127.0.0.1) port 1080 (#0) > GET / HTTP/1.1 > Host: example.org > User-Agent: curl/7.81.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK // ... rest of response



