microproxy

package module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 27 Imported by: 0

README

About

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.

Main features

  • 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/htdigest file.
  • 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-For and Via headers.
  • 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.

Using it as a library

$ 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 clients

Serve 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.

Using it as a command

$ 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.

Configuration file options

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:3128
  • socks_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 the digest auth. scheme this file has to be in the format used by Apache's htdigest utility, for the basic scheme 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 the X-Forwarded-For HTTP protocol header. Available options are:
    • "on" -- set the X-Forwarded-For header with the client's IP address, this is the default choice.
    • "off" -- do nothing, i.e. leave the header as is.
    • "delete" -- delete the X-Forwarded-For header, this turns on stealth mode.
    • "truncate" -- delete all old X-Forwarded-For headers and insert a new one with the client's IP address.
  • via_header="action" -- specifies how to handle the Via HTTP protocol header. Available options are:
    • "on" -- set the Via header, this is the default choice.
    • "off" -- do nothing with the Via header.
    • "delete" -- delete the Via header.
  • via_proxy_name="name" -- this value will be used as the host name in the Via header, 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. Needs http_listen.
  • http_listen="ip:port" -- ip address and port the /health endpoint 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" matches example.com and www.example.com, but not notexample.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.

Upstream proxies

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:

  1. the most specific matching [rules] entry;
  2. the "." rule, if there is one;
  3. forward_proxy_url;
  4. the http_proxy/https_proxy/no_proxy environment 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"

SOCKS5 support

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.

Encrypting the connection to the proxy

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.

Health

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-Authorization challenge 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.

Signal handling

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.

Licensing

All source code included in this distribution is covered by the MIT License found in the LICENSE file.

Documentation

Overview

Package microproxy is an embeddable HTTP and HTTPS forward proxy.

It offers Basic and Digest authentication, client network ACLs, CONNECT port restrictions, header rewriting, an access log, and per-domain routing to upstream HTTP or SOCKS5 proxies. Everything is configured from Go; nothing is read from a file unless the program chooses to read one.

The simplest useful proxy listens on an ephemeral port and sends everything through a SOCKS5 server, which is what fronting a VPN client looks like:

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())

// the port the clients have to be pointed at
port := listener.Addr().(*net.TCPAddr).Port

The same can be done with a dialer instead of a url, which is what a program that owns the tunnel rather than a SOCKS port in front of it wants:

srv, err := microproxy.New(cfg, microproxy.WithDialer(tunnel))

Authentication is a credential store the program supplies, so users can be added and removed at runtime without writing an htpasswd file:

users := microproxy.NewBasicUsers("proxy")
users.Set("alice", "open sesame")

srv, err := microproxy.New(cfg, microproxy.WithCredentials(users))

A Config can be replaced while the proxy is serving with Server.Reload, and the credentials with Server.SetCredentials. Config.Listen and Config.BindIP are the exception: they are consumed while the proxy is being built.

The cmd/microproxy command is a standalone proxy built on this package, and is what keeps the TOML configuration file, the signal handling and the log files that microproxy has always had.

Index

Constants

View Source
const (
	// DefaultListenAddress is used when Config.Listen is empty.
	DefaultListenAddress = "127.0.0.1:3128"
	// DefaultAllowedNetwork is the only network allowed when neither
	// Config.AllowedNetworks nor authentication is configured.
	DefaultAllowedNetwork = "127.0.0.1/32"
	// DefaultAllowedConnectPort is the only port CONNECT may reach when
	// Config.AllowedConnectPorts is empty.
	DefaultAllowedConnectPort = 443

	// DefaultReadTimeout bounds how long a single read on a proxied connection
	// may take.
	DefaultReadTimeout = 15 * time.Minute
	// DefaultWriteTimeout bounds how long a single write on a proxied
	// connection may take.
	DefaultWriteTimeout = 15 * time.Minute

	// GenericProxyRule is the Rules key standing for "any host". It is more
	// specific than ForwardProxyURL and less specific than any domain rule.
	GenericProxyRule = "."
)
View Source
const DefaultHealthFailureLimit = 5

DefaultHealthFailureLimit is how many consecutive failures put a proxy in the unhealthy state when Config.HealthFailureLimit is not set.

Variables

View Source
var ErrServerRunning = errors.New("microproxy: the server is already serving")

ErrServerRunning is returned when a server that is already serving is asked to serve again.

View Source
var SupportedProxySchemes = []string{"http", "https", "socks5", "socks5h"}

SupportedProxySchemes are the upstream proxy url schemes this package knows how to reach.

http and https are spoken by the proxy itself; socks5 and socks5h are handed to a SOCKS5 dialer, which always resolves the target name at the SOCKS server, so the two behave identically here.

Functions

func DigestHA1

func DigestHA1(user, realm, password string) string

DigestHA1 computes MD5(user:realm:password), the digest a Digest exchange is verified against.

func ParseProxyURL

func ParseProxyURL(rawURL string) (*url.URL, error)

ParseProxyURL parses an upstream proxy url, rejecting the ones that carry no host to connect to or a scheme this package can't reach.

Types

type AccessEntry

type AccessEntry struct {
	Time          time.Time
	RemoteAddr    string
	Method        string
	URL           string
	StatusCode    int
	ContentLength int64
	// User is the authenticated user, empty when the request was not
	// authenticated.
	User string
	Err  error
}

AccessEntry is one line of the access log: a request that was served, or a tunnel that was opened.

func (*AccessEntry) String

func (e *AccessEntry) String() string

String formats the entry the way microproxy has always written its access log: fields separated by spaces, "-" for the ones a request did not carry.

type AccessLogger

type AccessLogger interface {
	LogAccess(*AccessEntry)
}

AccessLogger records the requests the proxy serves. It is called from the goroutine serving the request, so an implementation that can block should hand the entry to a writer of its own, the way FileAccessLogger does.

type AccessLoggerFunc

type AccessLoggerFunc func(*AccessEntry)

AccessLoggerFunc adapts a function to AccessLogger.

func (AccessLoggerFunc) LogAccess

func (f AccessLoggerFunc) LogAccess(entry *AccessEntry)

LogAccess implements AccessLogger.

type BasicCredentials

type BasicCredentials interface {
	Credentials

	VerifyBasic(user, password string) bool
}

BasicCredentials verifies a user and password sent with the Basic scheme. The password reaches it in the clear, so the store is the right place to apply a constant-time comparison against whatever it keeps.

func BasicCredentialsFunc

func BasicCredentialsFunc(realm string, verify func(user, password string) bool) BasicCredentials

BasicCredentialsFunc turns a verification function into credentials for the Basic scheme. verify is called from the goroutine serving the request and has to be safe for concurrent use.

type BasicUsers

type BasicUsers struct {
	// contains filtered or unexported fields
}

BasicUsers is an in-memory set of users for the Basic scheme. It is safe for concurrent use, so a program can add and remove users while the proxy it serves is running.

func LoadBasicUsers

func LoadBasicUsers(realm string, r io.Reader) (*BasicUsers, error)

LoadBasicUsers reads the "user:password" lines that htpasswd -p writes.

func LoadBasicUsersFile

func LoadBasicUsersFile(realm, path string) (*BasicUsers, error)

LoadBasicUsersFile reads a file in the format LoadBasicUsers accepts.

func NewBasicUsers

func NewBasicUsers(realm string) *BasicUsers

NewBasicUsers returns an empty user set announcing realm.

func (*BasicUsers) Realm

func (u *BasicUsers) Realm() string

Realm implements Credentials.

func (*BasicUsers) Remove

func (u *BasicUsers) Remove(user string)

Remove drops a user. Connections it has already opened are not affected.

func (*BasicUsers) Set

func (u *BasicUsers) Set(user, password string)

Set adds a user or replaces its password.

func (*BasicUsers) Users

func (u *BasicUsers) Users() []string

Users returns the user names currently known.

func (*BasicUsers) VerifyBasic

func (u *BasicUsers) VerifyBasic(user, password string) bool

VerifyBasic implements BasicCredentials. The comparison takes the same time whether the user exists or not.

type Config

type Config struct {
	// Listen is the "host:port" the HTTP frontend binds to in ListenAndServe.
	// Serve ignores it and takes the listener from the caller. Applied at
	// startup only.
	Listen string `toml:"listen"`

	// SOCKSListen is the "host:port" the SOCKS5 frontend binds to in
	// ListenAndServeSOCKS. Empty means no SOCKS frontend unless ServeSOCKS is
	// called with a listener of the caller's own. Applied at startup only.
	SOCKSListen string `toml:"socks_listen"`

	// BindIP is the local address outgoing connections are made from. Applied
	// at startup only.
	BindIP string `toml:"bind_ip"`

	// AllowedNetworks, when non-empty, is the whitelist of client networks;
	// every other client is refused. Entries are CIDR blocks or bare IP
	// addresses. When it is empty and no credentials are configured, New
	// installs DefaultAllowedNetwork.
	AllowedNetworks []string `toml:"allowed_networks"`

	// DisallowedNetworks is the blacklist of client networks, applied after
	// AllowedNetworks.
	DisallowedNetworks []string `toml:"disallowed_networks"`

	// AllowedConnectPorts limits the ports CONNECT may reach. Empty means
	// DefaultAllowedConnectPort only.
	AllowedConnectPorts []int `toml:"allowed_connect_ports"`

	// ForwardedForHeader is what to do with X-Forwarded-For: "on", "off",
	// "delete" or "truncate". Empty means "on".
	ForwardedForHeader string `toml:"forwarded_for_header"`

	// ViaHeader is what to do with Via: "on", "off" or "delete". Empty means
	// "on".
	ViaHeader string `toml:"via_header"`

	// ViaProxyName is the name this proxy announces in Via. Empty means the
	// host name.
	ViaProxyName string `toml:"via_proxy_name"`

	// AddHeaders are [name, value] pairs added to a request that does not
	// already carry the header.
	AddHeaders [][]string `toml:"add_headers"`

	// Proxies maps an alias to an upstream proxy url and Rules maps a domain
	// to one of those aliases. The longest matching domain wins. See
	// GenericProxyRule and ForwardProxyURL for the fallbacks.
	Proxies map[string]string `toml:"proxies"`
	Rules   map[string]string `toml:"rules"`

	// ForwardProxyURL is the upstream proxy for the hosts no rule matches.
	// Empty falls back to the environment (HTTP_PROXY and friends).
	ForwardProxyURL string `toml:"forward_proxy_url"`

	// ReadTimeout and WriteTimeout bound a single read or write on a proxied
	// connection. Zero means the Default values; a negative value disables
	// the deadline.
	ReadTimeout  time.Duration `toml:"read_timeout"`
	WriteTimeout time.Duration `toml:"write_timeout"`

	// HealthCheckEnabled turns health tracking on when it is "on". The state
	// is then readable through Server.Health, which also serves it over HTTP.
	// Applied at startup only.
	HealthCheckEnabled string `toml:"enable_health_check"`

	// HealthFailureLimit is how many consecutive failures make the proxy
	// unhealthy. Zero means DefaultHealthFailureLimit.
	HealthFailureLimit int `toml:"health_failure_limit"`
}

Config is the data-only description of a proxy. Every field can be set from Go; the toml tags exist so that a program that does keep a configuration file can decode one into this struct.

Callers construct a Config, hand it to New, and are free to reuse or modify it afterwards: New takes its own copy of the slices and maps.

func (Config) Validate

func (c Config) Validate() error

Validate reports whether the configuration can be used, without building a server from it. It is what a "test the configuration and exit" flag wants.

type ContextDialer

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

ContextDialer establishes the connections the proxy makes on behalf of a client. It is what a program embedding this package implements to send the traffic somewhere of its own: a VPN tunnel, a SOCKS port, a test double.

func SOCKS5Dialer

func SOCKS5Dialer(addr, user, password string, forward ContextDialer) (ContextDialer, error)

SOCKS5Dialer reaches targets through the SOCKS5 server at addr, which is how a proxy is pointed at a VPN client or any other SOCKS port. user and password may be empty when the server wants no authentication.

The target host name is resolved by the SOCKS server rather than locally, so the names a client asks for are looked up on the far side of the tunnel.

forward is how the SOCKS server itself is reached; nil dials it directly.

type Credentials

type Credentials interface {
	// Realm is the protection space announced in the 407 challenge.
	Realm() string
}

Credentials is what the two credential store interfaces have in common. A store is consulted concurrently, so its methods have to be safe for use from several goroutines.

type DialerFunc

type DialerFunc func(ctx context.Context, network, addr string) (net.Conn, error)

DialerFunc adapts a function to ContextDialer. It also satisfies the Dial method that golang.org/x/net/proxy expects, so that it can be handed to a SOCKS5 dialer as the way to reach the SOCKS server itself.

func (DialerFunc) Dial

func (f DialerFunc) Dial(network, addr string) (net.Conn, error)

Dial implements the dialer that golang.org/x/net/proxy expects, with the background context.

func (DialerFunc) DialContext

func (f DialerFunc) DialContext(ctx context.Context, network, addr string) (net.Conn, error)

DialContext implements ContextDialer.

type DigestCredentials

type DigestCredentials interface {
	Credentials

	HA1(user, realm string) (string, bool)
}

DigestCredentials supplies the HA1 digest, MD5(user:realm:password), that the Digest scheme is verified against. The password itself never travels, so a store can only ever hold or derive HA1.

realm is the one the client echoed back, which a store is free to reject in favour of its own.

func DigestCredentialsFunc

func DigestCredentialsFunc(realm string, ha1 func(user, realm string) (string, bool)) DigestCredentials

DigestCredentialsFunc turns an HA1 lookup into credentials for the Digest scheme. ha1 is called from the goroutine serving the request and has to be safe for concurrent use.

type DigestUsers

type DigestUsers struct {
	// contains filtered or unexported fields
}

DigestUsers is an in-memory set of users for the Digest scheme, keyed by user and realm. It is safe for concurrent use.

Digest never sees the password, only HA1 = MD5(user:realm:password), which is what this store keeps.

func LoadDigestUsers

func LoadDigestUsers(realm string, r io.Reader) (*DigestUsers, error)

LoadDigestUsers reads the "user:realm:ha1" lines that htdigest writes.

func LoadDigestUsersFile

func LoadDigestUsersFile(realm, path string) (*DigestUsers, error)

LoadDigestUsersFile reads a file in the format LoadDigestUsers accepts.

func NewDigestUsers

func NewDigestUsers(realm string) *DigestUsers

NewDigestUsers returns an empty user set announcing realm.

func (*DigestUsers) HA1

func (u *DigestUsers) HA1(user, realm string) (string, bool)

HA1 implements DigestCredentials.

func (*DigestUsers) Realm

func (u *DigestUsers) Realm() string

Realm implements Credentials.

func (*DigestUsers) Remove

func (u *DigestUsers) Remove(user string)

Remove drops a user from this store's realm.

func (*DigestUsers) Set

func (u *DigestUsers) Set(user, password string)

Set adds a user, deriving HA1 from the password for this store's realm.

func (*DigestUsers) SetHA1

func (u *DigestUsers) SetHA1(user, realm, ha1 string)

SetHA1 adds a user whose HA1 was computed elsewhere, which is how an htdigest file and any store that never held the password are loaded.

type FileAccessLogger

type FileAccessLogger struct {
	// contains filtered or unexported fields
}

FileAccessLogger writes the access log to a file from a goroutine of its own, so that a slow disk delays the writer rather than the requests.

func NewFileAccessLogger

func NewFileAccessLogger(path string) (*FileAccessLogger, error)

NewFileAccessLogger appends the access log to path.

func (*FileAccessLogger) Close

func (l *FileAccessLogger) Close() error

Close flushes the entries already handed over and releases the file. Entries submitted afterwards are discarded rather than panicking on a closed channel.

func (*FileAccessLogger) LogAccess

func (l *FileAccessLogger) LogAccess(entry *AccessEntry)

LogAccess implements AccessLogger. It hands the entry to the writing goroutine, and discards it once the logger has been closed.

func (*FileAccessLogger) Reopen

func (l *FileAccessLogger) Reopen() error

Reopen makes the logger write to a freshly opened file, for logrotate.

type FileLogger

type FileLogger struct {
	// contains filtered or unexported fields
}

FileLogger writes to a file, or to stderr when its path is empty, and can reopen it in place for logrotate. Reopening in place means nobody has to swap the logger of a running proxy while requests are being served.

func NewFileLogger

func NewFileLogger(path string) (*FileLogger, error)

NewFileLogger logs to path, or to stderr when path is empty.

func (*FileLogger) Close

func (l *FileLogger) Close() error

Close releases the file, after which the logger writes to stderr again.

func (*FileLogger) Printf

func (l *FileLogger) Printf(format string, v ...interface{})

Printf implements Logger.

func (*FileLogger) Reopen

func (l *FileLogger) Reopen() error

Reopen points the logger at a freshly opened file. log.Logger serializes writes with the change of destination, so concurrent Printf calls either reach the old file before it is closed or the new one.

type Health

type Health struct {
	// contains filtered or unexported fields
}

Health tracks whether the proxy is answering requests successfully. A run of consecutive failures marks it unhealthy; a single success clears the run.

It is safe for concurrent use, and a program embedding this package can read it directly or serve it over HTTP with Handler.

func NewHealth

func NewHealth(failureLimit int) *Health

NewHealth returns a tracker that turns unhealthy after failureLimit consecutive failures. A limit of zero or less means DefaultHealthFailureLimit. A new tracker starts out healthy.

func (*Health) Failures

func (h *Health) Failures() int

Failures is the number of consecutive failures recorded since the last success.

func (*Health) Handler

func (h *Health) Handler() http.Handler

Handler serves the state as a health endpoint: 200 while the proxy is healthy, 503 once it is not, which is what a container or a load balancer probes.

func (*Health) Healthy

func (h *Health) Healthy() bool

Healthy reports the state the proxy is in.

func (*Health) RecordFailure

func (h *Health) RecordFailure()

RecordFailure counts a failed request, and marks the proxy unhealthy once enough of them have happened in a row.

func (*Health) RecordSuccess

func (h *Health) RecordSuccess()

RecordSuccess counts a served request, which clears the run of failures.

type Logger

type Logger interface {
	Printf(format string, v ...interface{})
}

Logger is where the proxy reports what it is doing and what went wrong. It is the shape goproxy expects, so a *log.Logger satisfies it as is.

var DiscardLogger Logger = discardLogger{}

DiscardLogger throws away everything the proxy reports.

func NewSlogLogger

func NewSlogLogger(logger *slog.Logger, level slog.Level) Logger

NewSlogLogger adapts a *slog.Logger, which is what a program embedding this package is most likely to already have. Every line is recorded at level.

type LoggerFunc

type LoggerFunc func(format string, v ...interface{})

LoggerFunc adapts a function to Logger.

func (LoggerFunc) Printf

func (f LoggerFunc) Printf(format string, v ...interface{})

Printf implements Logger.

type Option

type Option func(*Server) error

Option is a part of a proxy that a Config can't describe, because it is a piece of the embedding program rather than data: where the logs go, what the credentials are, how the connections are made.

func WithAccessLogger

func WithAccessLogger(access AccessLogger) Option

WithAccessLogger records every request the proxy serves. The default records nothing.

func WithCredentials

func WithCredentials(creds Credentials) Option

WithCredentials authenticates the clients against creds. A BasicCredentials makes the proxy challenge with the Basic scheme and a DigestCredentials with Digest. The default is to not authenticate at all, which is also what limits an otherwise unconfigured proxy to the loopback.

The credentials can be replaced afterwards with Server.SetCredentials.

func WithDialer

func WithDialer(dialer ContextDialer) Option

WithDialer makes every connection the proxy opens on behalf of a client go through dialer, instead of through the network directly. This is how the traffic is handed to a tunnel the embedding program owns.

It replaces the dialing, not the routing: a request that a rule sends to an upstream proxy still goes there, with dialer opening the connection to the proxy. Config.BindIP no longer applies, since the dialer decides where the connection comes from.

func WithHealth

func WithHealth(health *Health) Option

WithHealth tracks whether the proxy is answering requests successfully, overriding Config.HealthCheckEnabled. Passing nil turns tracking off.

The state is readable through Server.Health, and Health.Handler serves it as an endpoint for a container or a load balancer to probe.

func WithInsecureUpstream

func WithInsecureUpstream(insecure bool) Option

WithInsecureUpstream disables certificate verification towards an https:// upstream proxy. It is the -i flag of the command, and is a bad idea anywhere else.

func WithListenerTLS added in v0.0.3

func WithListenerTLS(config *tls.Config) Option

WithListenerTLS makes the HTTP frontend accept TLS connections: clients connect to the proxy over TLS and speak the proxy protocol inside it, which is what a browser calls an "HTTPS proxy".

This keeps the Proxy-Authorization credentials and the host names of CONNECT requests off the local network, which a plain proxy sends in the clear. It does not decrypt anything a client tunnels: what goes through CONNECT is still opaque to the proxy.

The certificate is the proxy's own, for the name its clients reach it by. Use tls.Config.GetCertificate to serve one that can be replaced without a restart. A MinVersion of TLS 1.2 is applied when config does not set one, and config is copied, so later changes to it are not picked up.

This is the server side. WithTLSClientConfig is the unrelated client side, used when this proxy talks to an https:// upstream.

The SOCKS frontend is unaffected: SOCKS5 has no TLS convention and no client speaks one.

func WithLogger

func WithLogger(logger Logger) Option

WithLogger sends what the proxy reports to logger. The default discards it; NewFileLogger and NewSlogLogger are the ready-made ones.

func WithRouter

func WithRouter(router Router) Option

WithRouter decides the upstream per target host, replacing the routing that Config.Proxies, Config.Rules and Config.ForwardProxyURL describe.

func WithTLSClientConfig

func WithTLSClientConfig(config *tls.Config) Option

WithTLSClientConfig is the TLS configuration used when the proxy itself speaks TLS as a client, which is to an https:// upstream proxy. See WithListenerTLS for the server side.

func WithVerbose

func WithVerbose(verbose bool) Option

WithVerbose makes the proxy report every request it serves through its logger.

type Route

type Route struct {
	// Proxy is an HTTP or HTTPS upstream proxy. Tunnelled traffic reaches it
	// with CONNECT and plain HTTP with an absolute-form request line, which is
	// why it can't be reduced to a Dialer.
	Proxy *url.URL

	// Dialer establishes the connection to the target itself. A SOCKS5
	// upstream is expressed this way, as is any custom transport.
	Dialer ContextDialer
}

Route is how the proxy reaches one target host.

The zero Route is a direct connection. At most one field is set: Proxy for the upstreams that speak HTTP, Dialer for everything else.

func (Route) Direct

func (r Route) Direct() bool

Direct reports whether the route connects to the target without an upstream.

type Router

type Router interface {
	Route(host string) (Route, error)
}

Router decides how a target host is reached. host is the "host:port" of the target, never of an upstream proxy.

A Router has to be safe for concurrent use, and its answer has to depend on nothing but the host: connections are pooled per host, so a route that varies between two requests to the same target would not be honoured for the second.

type RouterFunc

type RouterFunc func(host string) (Route, error)

RouterFunc adapts a function to Router.

func (RouterFunc) Route

func (f RouterFunc) Route(host string) (Route, error)

Route implements Router.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is a proxy. It is created by New, and serves requests through Serve or ListenAndServe, or as the http.Handler that Handler returns.

A Server is safe for concurrent use, and its configuration and credentials can be replaced while it is serving.

func New

func New(cfg Config, opts ...Option) (*Server, error)

New builds a proxy from cfg. The options replace the parts a configuration can't describe: where the logs go, what the credentials are, and how the connections are made.

cfg is copied, so the caller may reuse or modify it afterwards.

func (*Server) Addr

func (s *Server) Addr() net.Addr

Addr is the address the proxy is listening on, or nil when it is not serving. It is how a caller that listened on port 0 learns the port it got.

func (*Server) Close

func (s *Server) Close() error

Close stops the proxy at once, dropping the requests and the SOCKS connections in flight.

func (*Server) Config

func (s *Server) Config() Config

Config returns a copy of the configuration in force.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the proxy as an http.Handler, for a caller that would rather run a server of its own.

func (*Server) Health

func (s *Server) Health() *Health

Health returns the proxy's health tracker, or nil when health tracking was not enabled with WithHealth.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe() error

ListenAndServe listens on Config.Listen and serves until Shutdown is called.

func (*Server) ListenAndServeSOCKS

func (s *Server) ListenAndServeSOCKS() error

ListenAndServeSOCKS listens on Config.SOCKSListen and serves SOCKS5 there.

func (*Server) Reload

func (s *Server) Reload(cfg Config) error

Reload installs cfg for the requests that follow. The settings that are consumed while the proxy is being built — Listen and BindIP — keep their startup values, and a change to them is reported through the logger.

The configuration in force is left untouched when cfg can't be used.

func (*Server) SOCKSAddr

func (s *Server) SOCKSAddr() net.Addr

SOCKSAddr is the address the SOCKS frontend is listening on, or nil when it is not serving.

func (*Server) Serve

func (s *Server) Serve(l net.Listener) error

Serve accepts connections on l until it is closed or Shutdown is called.

When WithListenerTLS is in force, l is wrapped so that clients speak the proxy protocol inside a TLS connection. Do not pass an already wrapped listener as well.

func (*Server) ServeSOCKS

func (s *Server) ServeSOCKS(l net.Listener) error

ServeSOCKS accepts SOCKS5 connections on l until it is closed or the server is shut down.

The clients reaching it are subject to the same configuration as the ones speaking HTTP: the network ACLs, the CONNECT port restrictions, the credentials and the upstream routing all apply, and the connections are written to the same access log.

Only the CONNECT command is implemented; BIND and UDP ASSOCIATE are refused.

func (*Server) SetCredentials

func (s *Server) SetCredentials(creds Credentials) error

SetCredentials installs the credentials the clients are authenticated against. A nil argument turns authentication off.

The digest nonces already issued stay valid, so replacing the credentials does not interrupt the exchanges in flight.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown stops both frontends, waiting for the requests and the SOCKS connections in flight to finish or for ctx to be done.

The connections a CONNECT request tunnelled are hijacked from the http server and are not waited for; closing them is up to whatever holds them.

Directories

Path Synopsis
cmd
microproxy command
Command microproxy is a standalone HTTP and HTTPS forward proxy built on the github.com/dobrevit/microproxy package.
Command microproxy is a standalone HTTP and HTTPS forward proxy built on the github.com/dobrevit/microproxy package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL