Standard library · Network

net/proxy

Connecting through an HTTP or SOCKS5 proxy, alone or in a chain.

import net.proxy

A proxy is a middleman that makes the connection on behalf of the program. This package builds the tunnel and gives back an ordinary net/tcp connection, read and written exactly as a direct one is.

Two kinds of proxy are supported. An HTTP proxy (NewHTTPProxy, often on port 3128 or 8080) opens the tunnel with a CONNECT request; a SOCKS5 proxy (NewSOCKS5Proxy, port 1080, or 9050 for Tor) uses its own small binary handshake. Both take "host:port", and the ...Auth forms add a user name and password. Dial returns a tcp.Conn; when the proxy cannot be reached or refuses, the connection is empty, so always check it with tcp.ConnOk.

What trips people up

  • The handshake has no timeout. A proxy that accepts the connection but never answers makes Dial wait forever, and there is no way to set a deadline before the handshake. Run the dial in a thread you can give up on if that matters.
  • Nothing is read from the environment. HTTPS_PROXY, ALL_PROXY and NO_PROXY are ignored; read them yourself, as the first guide does.
  • The address must be host:port with a name or an IPv4 address. The text is split at the first colon, so an IPv6 proxy address such as [::1]:1080 is not understood, and a missing port becomes 0.
  • Names are resolved by the proxy. SOCKS5 sends the target as a host name, so the proxy does the DNS lookup. That is what you want with Tor, but it means the target must be reachable by name from the proxy's network.
  • The result is a plain TCP connection. For HTTPS, upgrade it with tcp.UpgradeTLS as the HTTPS guide shows; the package does not do TLS to the proxy itself.

Reading proxy settings from a URL

Proxy settings usually arrive as one URL, for example from an HTTPS_PROXY environment variable or a config file: socks5://user:pass@host:1080. The package takes the parts separately, so a program needs a few lines to split the scheme, the credentials and the address. Unknown schemes should be reported rather than silently ignored.

net-proxy-guide-url.salam
import net.proxy
import str

func from_url(url: str, ok &: bool): proxy.Proxy:
    ok = true
    mut rest := url
    mut socks := false
    if str.StartsWith(rest, "socks5://"):
        socks = true
        rest = rest.substr(9, rest.len() - 9)
    else str.StartsWith(rest, "http://"):
        rest = rest.substr(7, rest.len() - 7)
    else:
        ok = false
        ret proxy.Proxy {}
    end
    mut user := ""
    mut pass := ""
    at := rest.find("@")
    if at >= 0:
        cred := rest.substr(0, at)
        rest = rest.substr(at + 1, rest.len() - at - 1)
        colon := cred.find(":")
        user = cred.substr(0, colon)
        pass = cred.substr(colon + 1, cred.len() - colon - 1)
    end
    if socks: ret proxy.NewSOCKS5ProxyAuth(rest, user, pass) end
    ret proxy.NewHTTPProxyAuth(rest, user, pass)
end

func main:
    urls := ["http://10.0.0.5:3128", "socks5://127.0.0.1:9050", "socks5://ali:s3cret@gw.example.net:1080", "ftp://old.example.net:21"]
    each u in urls:
        mut ok := false
        p := from_url(u, ok)
        if not ok:
            println u, "-> not supported"
            continue
        end
        mut kind := "http"
        if p.kind == proxy.TypeSOCKS5: kind = "socks5" end
        println u, "->", kind, p.host, p.port, "user=[" + p.username + "]"
    end
end
Outputhttp://10.0.0.5:3128 -> http 10.0.0.5 3128 user=[] socks5://127.0.0.1:9050 -> socks5 127.0.0.1 9050 user=[] socks5://ali:s3cret@gw.example.net:1080 -> socks5 gw.example.net 1080 user=[ali] ftp://old.example.net:21 -> not supported

What the tunnel looks like

This program runs offline: it starts a tiny fake HTTP proxy on a free local port in a thread, dials through it and prints the request the proxy received. That request is the whole HTTP proxy protocol: a CONNECT host:port line, the Host header, and with credentials a Proxy-Authorization: Basic header holding user:password in Base64. Once the proxy answers 200, every byte is passed straight through, which is why the echo reply comes back on the same connection.

net-proxy-guide-local.salam
import net.proxy
import net.tcp
import str

mut listener := tcp.Server {}
mut seen := ""

func fake_proxy():
    c := tcp.Accept(listener)
    seen = tcp.Read(c, 4096)
    tcp.Write(c, "HTTP/1.1 200 Connection established\r\n\r\n")
    msg := tcp.Read(c, 100)
    tcp.Write(c, "echo: " + msg)
    tcp.Close(c)
end

func main:
    listener = tcp.Bind(0)
    worker := spawn(fake_proxy)
    p := proxy.NewHTTPProxyAuth("127.0.0.1:" + str.FromInt(tcp.Port(listener) as i64), "ali", "secret")
    conn := proxy.Dial(p, "example.com", 443)
    if not tcp.ConnOk(conn):
        println "the proxy refused the tunnel"
        ret 1
    end
    tcp.Write(conn, "hello")
    println tcp.Read(conn, 100)
    tcp.Close(conn)
    join(worker)
    print str.Replace(seen, "\r\n", "\n")
    tcp.CloseServer(listener)
    ret 0
end
Outputecho: hello CONNECT example.com:443 HTTP/1.1 Host: example.com:443 Proxy-Authorization: Basic YWxpOnNlY3JldA== Proxy-Connection: Keep-Alive

Basic authentication is only Base64, not encryption. Over an untrusted network, use a proxy you reach through a VPN or SSH tunnel, or send only TLS traffic through it as in the next section.

HTTPS through a proxy

A tunnel carries any bytes, including TLS. Dial the proxy for port 443 of the real host, then turn the connection into TLS with tcp.UpgradeTLS and a tls config for that host name. The proxy sees only the host name and encrypted bytes. To try it, start a local proxy such as Squid:

docker run -d -p 3128:3128 ubuntu/squid
net-proxy-guide-https.salam
import net.proxy
import net.tcp
import tls
import str

func first_line(s: str): str:
    end_at := s.find("\r\n")
    if end_at < 0: ret s end
    ret s.substr(0, end_at)
end

func main:
    host := "example.com"
    p := proxy.NewHTTPProxy("127.0.0.1:3128")
    conn := proxy.Dial(p, host, 443)
    if not tcp.ConnOk(conn):
        println "no tunnel through " + p.host + ":" + str.FromInt(p.port as i64)
        ret 1
    end
    mut secure := tcp.UpgradeTLS(conn, tls.NewConfig(host))
    if not tls.ConnOk(secure):
        println "TLS failed: " + tls.ConnError(secure)
        ret 1
    end
    tls.Write(secure, "GET / HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n")
    println first_line(tls.Read(secure, 4096))
    tls.Close(secure)
    ret 0
end

With no proxy listening, the program prints no tunnel through 127.0.0.1:3128 and exits with 1.

A chain with a direct fallback

A Chain connects through several proxies in order: the first proxy opens a tunnel to the second, and so on, and the last one reaches the target. Build it with NewChain and Add, then call DialChain. If any hop fails the result is empty, and a program that may run both inside and outside a corporate network can then try tcp.Dial directly.

net-proxy-guide-chain.salam
import net.proxy
import net.tcp

func main:
    mut chain := proxy.NewChain()
    proxy.Add(chain, proxy.NewSOCKS5Proxy("127.0.0.1:9050"))
    proxy.Add(chain, proxy.NewHTTPProxyAuth("exit.example.net:3128", "team", "s3cret"))

    mut conn := proxy.DialChain(chain, "example.com", 80)
    if not tcp.ConnOk(conn):
        println "the chain is broken, trying direct"
        conn = tcp.Dial("example.com", 80)
    end
    if not tcp.ConnOk(conn):
        println "example.com is unreachable"
        ret 1
    end
    tcp.Write(conn, "HEAD / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")
    reply := tcp.Read(conn, 4096)
    println reply.substr(0, reply.find("\r\n"))
    tcp.Close(conn)
    ret 0
end

Each hop adds a round trip before the first byte of real data, so keep chains short. To use Tor, run it locally (docker run -d -p 9050:9050 dperson/torproxy) and add NewSOCKS5Proxy("127.0.0.1:9050") as the first hop.

Types

Proxy

struct Proxy
One proxy.
NameTypeDescription
kindint = 0The kind; one of the Type constants.
hoststr = ""The host.
portint = 0The port.
usernamestr = ""The user name, when one is needed.
passwordstr = ""The password.
net-proxy-Proxy.salam
import net.proxy

func main:
    a := proxy.NewHTTPProxy("10.0.0.1:3128")
    println a.kind == proxy.TypeHTTP, a.host, a.port
    b := proxy.NewHTTPProxyAuth("proxy.local:8080", "ali", "secret")
    println b.username, b.password
    c := proxy.NewSOCKS5Proxy("127.0.0.1:1080")
    d := proxy.NewSOCKS5ProxyAuth("socks.local:1080", "u", "p")
    println c.kind == proxy.TypeSOCKS5, d.port
    mut chain := proxy.NewChain()
    proxy.Add(chain, c)
    proxy.Add(chain, a)
    println chain.items.len()
end
Outputtrue 10.0.0.1 3128 ali secret true 1080 2

Chain

struct Chain
A chain of proxies used one after another.
NameTypeDescription
itemsVectorThe proxies in order.

The example for this item is shown with the ones above: net-proxy-Proxy.salam

Constants and variables

TypeHTTP

const TypeHTTP: i32 = 0
An HTTP proxy, through the CONNECT method.

The example for this item is shown with the ones above: net-proxy-Proxy.salam

TypeSOCKS5

const TypeSOCKS5: i32 = 1
A SOCKS5 proxy.

The example for this item is shown with the ones above: net-proxy-Proxy.salam

Functions

NewHTTPProxy

func NewHTTPProxy(addr: str): Proxy
An HTTP proxy from a host:port address; the text is split at the first colon, so IPv6 addresses do not work.

The example for this item is shown with the ones above: net-proxy-Proxy.salam

NewHTTPProxyAuth

func NewHTTPProxyAuth(addr: str, username: str, password: str): Proxy
An HTTP proxy with a user name and password.

The example for this item is shown with the ones above: net-proxy-Proxy.salam

NewSOCKS5Proxy

func NewSOCKS5Proxy(addr: str): Proxy
A SOCKS5 proxy.

The example for this item is shown with the ones above: net-proxy-Proxy.salam

NewSOCKS5ProxyAuth

func NewSOCKS5ProxyAuth(addr: str, username: str, password: str): Proxy
A SOCKS5 proxy with a user name and password.

The example for this item is shown with the ones above: net-proxy-Proxy.salam

Dial

func Dial(p: Proxy, target_host: str, target_port: int): tcp.Conn
Connects to the destination through the proxy and gives back a ready TCP connection; an invalid one (check tcp.ConnOk) when the proxy cannot be reached or refuses. The handshake waits without a time limit.
net-proxy-Dial.salam
import net.proxy
import net.tcp

func main:
    p := proxy.NewSOCKS5Proxy("127.0.0.1:1080")
    conn := proxy.Dial(p, "example.com", 80)
    mut chain := proxy.NewChain()
    proxy.Add(chain, p)
    conn2 := proxy.DialChain(chain, "example.com", 80)
    println tcp.ConnOk(conn), tcp.ConnOk(conn2)
end

NewChain

func NewChain(): Chain
An empty chain.

The example for this item is shown with the ones above: net-proxy-Proxy.salam

Add

func Add(c &: Chain, p: Proxy)
Adds a proxy to the end of the chain.

The example for this item is shown with the ones above: net-proxy-Proxy.salam

DialChain

func DialChain(c: Chain, target_host: str, target_port: int): tcp.Conn
Connects to the destination through every proxy in the chain, in order; an empty chain dials the destination directly.

The example for this item is shown with the ones above: net-proxy-Dial.salam