microproxy is a lightweight non-caching forward proxy. It is both a Go
package you embed in your own program and a standalone executable.
It accepts HTTP, HTTPS (CONNECT) and SOCKS5 connections, and forwards them
directly, through an upstream HTTP proxy, or through an upstream SOCKS5 server —
which is how it is put in front of a VPN client.
- Usable as a library: everything is configured from Go, nothing is read from a file unless your program chooses to read one.
- Two frontends over one policy: HTTP/HTTPS and SOCKS5 share the same ACLs, credentials, routing and access log.
- Basic and Digest access authentication, against a credential store you supply
or an
htpasswd/htdigestfile. - IP-based black and white access lists.
- Per-domain routing to upstream HTTP or SOCKS5 proxies.
- Ability to log all requests.
- Ability to tweak the
X-Forwarded-ForandViaheaders. - Ability to specify the IP address for outgoing connections.
- A health endpoint for a container or a load balancer to probe.
- Live reload of the configuration and the credentials.
- Single executable with no external dependencies.
- Reasonable memory usage.
$ go get github.com/dobrevit/microproxy
A proxy on an ephemeral port sending everything through a SOCKS5 server:
srv, err := microproxy.New(microproxy.Config{
ForwardProxyURL: "socks5://127.0.0.1:1080",
AllowedNetworks: []string{"127.0.0.1/32"},
})
if err != nil {
return err
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
go srv.Serve(listener)
defer srv.Shutdown(context.Background())
port := listener.Addr().(*net.TCPAddr).Port // where to point the clientsServe takes a listener of your own, so binding port 0 and reading the port
back is how you avoid a fixed port. ListenAndServe uses Config.Listen
instead, and Handler returns the proxy as an http.Handler if you would
rather run the server yourself.
To accept SOCKS5 as well, serve a second listener — the same Server, the same
policy:
socks, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
go srv.ServeSOCKS(socks)Instead of a url, the traffic can be handed to a dialer your program owns, which is what to do when it holds the tunnel itself:
srv, err := microproxy.New(cfg, microproxy.WithDialer(tunnel))Users can be added and removed while the proxy is running:
users := microproxy.NewBasicUsers("proxy")
users.Set("alice", "open sesame")
srv, err := microproxy.New(cfg, microproxy.WithCredentials(users))
...
users.Remove("alice")Server.Reload replaces the configuration of a running proxy and
Server.SetCredentials its credentials. Config.Listen, Config.SOCKSListen
and Config.BindIP are the exception: they are consumed while the proxy is
being built and keep their startup values.
The options are WithLogger, WithAccessLogger, WithCredentials,
WithRouter, WithDialer, WithTLSClientConfig, WithInsecureUpstream and
WithVerbose. See the package documentation for the details.
$ go build -o microproxy ./cmd/microproxy
$ ./microproxy --config microproxy.toml
To enable debug mode, add the -v switch. To only test the configuration file
add -t, i.e. $ ./microproxy --config microproxy.toml -t.
The command uses TOML for its configuration file. Below is a list of supported options.
listen="ip:port"-- ip address and port where to listen for incoming proxy requests. Default:127.0.0.1:3128socks_listen="ip:port"-- ip address and port where to listen for incoming SOCKS5 connections. Unset means no SOCKS frontend.access_log="path"-- path to a file where to write requested through proxy urls.activity_log="path"-- path to a file where to write debug and auxiliary information.allowed_connect_ports=[port1, port2, ...]-- list of allowed ports to CONNECT to. Applies to SOCKS5 requests as well. Default:[443]auth_file="path"-- path to a file with users' passwords. If you use thedigestauth. scheme this file has to be in the format used by Apache's htdigest utility, for thebasicscheme it has to be in the format used by Apache's htpasswd utility with the -p option, i.e. created as$ htpasswd -c -p auth.txt username.auth_type="type"-- authentication scheme type. Available options are:"basic"-- use the Basic authentication scheme."digest"-- use the Digest authentication scheme.
auth_realm="realmstring"-- realm name which is to be reported to the client for the proxy authentication scheme.forwarded_for_header="action"-- specifies how to handle theX-Forwarded-ForHTTP protocol header. Available options are:"on"-- set theX-Forwarded-Forheader with the client's IP address, this is the default choice."off"-- do nothing, i.e. leave the header as is."delete"-- delete theX-Forwarded-Forheader, this turns on stealth mode."truncate"-- delete all oldX-Forwarded-Forheaders and insert a new one with the client's IP address.
via_header="action"-- specifies how to handle theViaHTTP protocol header. Available options are:"on"-- set theViaheader, this is the default choice."off"-- do nothing with theViaheader."delete"-- delete theViaheader.
via_proxy_name="name"-- this value will be used as the host name in theViaheader, by default the server's host name will be used.allowed_networks=["net1", ...]-- list of whitelisted networks in CIDR format. A bare IP address is a single host.disallowed_networks=["net1", ...]-- list of blacklisted networks in CIDR format.bind_ip="ip"-- specify which IP will be used for outgoing connections.add_headers=[["header1", "value1"], ["header2", "value2"]...]-- adds the specified headers to outgoing HTTP requests, this option will not work for HTTPS connections.read_timeout/write_timeout-- how long a single read or write on a proxied connection may take, e.g."15m". A negative value disables the deadline. Default: 15 minutes.tls_cert_file/tls_key_file-- the proxy's own certificate and key. Setting them makes the proxy listener accept TLS connections instead of plain ones. Both are needed, or neither.enable_health_check="on"-- track whether the proxy is serving requests successfully and expose the result at/health. Needshttp_listen.http_listen="ip:port"-- ip address and port the/healthendpoint is served on. This is not a proxy listener; nothing is proxied there.health_failure_limit=N-- how many consecutive failures make the proxy unhealthy. Default: 5.forward_proxy_url="http://user:password@host:port"-- specify the proxy to forward requests to. Uses the basic auth type for the forward proxy.[proxies]-- table of named upstream proxies, e.g.name="http://user:password@host:port". Used together with[rules].[rules]-- table mapping a domain to the name of the proxy from[proxies]that serves it, e.g."example.com"="name". A rule matches the domain itself and its subdomains, so"example.com"matchesexample.comandwww.example.com, but notnotexample.com. The special domain"."matches every host.
If neither allowed_networks nor authentication is configured, the proxy only
accepts connections from 127.0.0.1/32, so that an unconfigured one is never an
open relay.
An upstream proxy url may be http://, https://, socks5:// or socks5h://.
The HTTP ones are reached with CONNECT; the SOCKS ones are dialled, and the
target host name is resolved by the SOCKS server rather than locally, so
socks5 and socks5h behave the same way here.
Upstream proxy selection, from the highest to the lowest priority:
- the most specific matching
[rules]entry; - the
"."rule, if there is one; forward_proxy_url;- the
http_proxy/https_proxy/no_proxyenvironment variables.
If none of them applies, the connection is made directly. Example:
forward_proxy_url="http://fallback:3128"
[proxies]
internal="http://user:password@internal-proxy:3128"
partner="http://partner-proxy:3128"
vpn="socks5://127.0.0.1:1080"
[rules]
"example.com"="internal"
"api.partner.net"="partner"
"internal.corp"="vpn"The SOCKS5 frontend implements the CONNECT command of
RFC 1928 and the username/password
authentication of RFC 1929. BIND and
UDP ASSOCIATE are refused.
When credentials are configured, SOCKS clients have to present them: a Basic store verifies the password directly, and a Digest store verifies it by computing the digest it would have produced. When no credentials are configured, SOCKS clients are accepted without authentication and the network ACLs are the only thing limiting them.
By default a client talks to the proxy in the clear, which puts the
Proxy-Authorization credentials and the host names of every CONNECT request
on the local network. Setting tls_cert_file and tls_key_file makes the proxy
listener accept TLS, so the client speaks the proxy protocol inside a TLS
connection — what a browser calls an HTTPS proxy.
listen="0.0.0.0:3128"
tls_cert_file="/etc/microproxy/proxy.crt"
tls_key_file="/etc/microproxy/proxy.key"The certificate is the proxy's own, for the name its clients reach it by. It is
re-read on USR2 along with the configuration, so a renewal is picked up
without a restart, and a pair that can't be read leaves the one in force in
place. Connections already established keep the certificate they started with.
This does not decrypt anything: what a client sends through CONNECT stays
opaque to the proxy. It protects the hop between the client and the proxy, not
the traffic inside it.
The SOCKS frontend is unaffected — SOCKS5 has no TLS convention — and so is the
/health endpoint, which stays plain HTTP on http_listen.
Note that not every client can be configured to use an HTTPS proxy: browsers generally can, through a PAC file or a command-line flag, while system-wide proxy settings on some platforms only accept a plain one. Check before turning it on for an existing deployment.
In Go, the same thing is WithListenerTLS:
srv, err := microproxy.New(cfg, microproxy.WithListenerTLS(&tls.Config{
GetCertificate: myCertSource.GetCertificate,
}))Serve and ListenAndServe then wrap the listener themselves, so do not pass
an already wrapped one as well. TLS 1.2 is applied as a floor when the
configuration does not set one.
enable_health_check="on" makes the proxy count how its work goes: a run of
consecutive failures marks it unhealthy and a single success clears the run.
/health on http_listen answers 200 while it is healthy and 503 once it is
not, which is what the container image's healthcheck probes.
What counts differs slightly between the two frontends:
- the SOCKS frontend reports on whether it reached the target. Opening a tunnel is a success and failing to dial one is a failure; a client refused before the proxy tried to reach anything — wrong password, disallowed network, disallowed port — records nothing, because it says something about that client rather than about this proxy;
- the HTTP frontend counts every response it serves, and additionally treats a
407 as a failure. A
Proxy-Authorizationchallenge in the response stream may have come from an upstream proxy whose credentials have gone stale, which is the proxy's problem, but it may equally be this proxy challenging its own client, which is not.
A program embedding the package gets the same thing through the API, and can read the state directly rather than over HTTP:
srv, err := microproxy.New(cfg, microproxy.WithHealth(microproxy.NewHealth(5)))
...
if !srv.Health().Healthy() {
// take this proxy out of rotation
}Health.Handler returns an http.Handler to mount wherever suits.
On USR1 microproxy reopens the access and activity log files.
On USR2 microproxy re-reads its configuration file and applies it to the
requests that follow. A file that can't be parsed or validated is reported in
the activity log and the running configuration is kept, so a typo never takes
the proxy down. listen, socks_listen, bind_ip, access_log and
activity_log are used while the proxy is being set up and keep their startup
values; a reload that changes them says so in the activity log. Digest nonces
already issued stay valid across a reload, so clients are not asked to
authenticate again.
On INT or TERM microproxy stops accepting new connections and gives the ones
in flight ten seconds to finish.
All source code included in this distribution is covered by the MIT License found in the LICENSE file.