# dumbproxy: one Go binary that speaks HTTP and SOCKS5, with the policy in JavaScript

> dumbproxy is a forward proxy whose interesting parts are the access control and the edge cases: authentication can be chained across an htpasswd file, an HMAC service and Redis, certificates can be issued by ACME and cached in Redis with an optional AEAD layer, and access filters, upstream selection, bandwidth limits and GeoIP lookups are written as JavaScript functions. It is also the rare proxy that treats deep packet inspection as a design constraint rather than an afterthought.

**SenseUnit/dumbproxy** — Simple, scriptable, secure HTTP/SOCKS5 forward proxy

- Repository: https://github.com/SenseUnit/dumbproxy
- Website: https://github.com/SenseUnit/dumbproxy/wiki
- Stars: 950 · Forks: 75
- Language: Go
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/senseunit-dumbproxy

## One listener that answers to both HTTP and SOCKS5 clients

dumbproxy describes itself as a simple, scriptable, secure HTTP/SOCKS5 forward proxy, and the two protocol halves are not separate programs. Run it with no arguments and it starts accepting connections on port 8080, and the same port serves clients that speak either protocol.

The deployment story is deliberately frictionless. Pre-built binaries are published on the releases page, installation from source is a single go install . in the source directory, there is a Docker image and a Snap package, and there is even a shell script, easy-dp, for a complete installation on a fresh Linux server. The project claims zero-configuration, a single self-contained binary, and cross-platform support across Windows, macOS, Linux, Android via shell and the BSDs.

Beyond ordinary web traffic, the README claims seamless forwarding of all kinds of TCP connections, and there is a port-forwarding mode alongside the proxy modes. A separate set of TrustTunnel extensions for UDP and healthchecks is supported, which means a dumbproxy server running with HTTPS enabled is compatible with a TrustTunnel client. For an operator, that means one binary covers the tunnel role, the proxy role and the port-forward role without a second daemon.

## Authentication is a chain of lookups rather than one password file

The authentication design is the part of the feature list that most deserves reading. The proxy supports Basic proxy authentication, and it can obtain that credential from several sources. There is an auto-reloaded NCSA httpd-style passwords file, so a password can be changed without restarting the proxy. There is a static login and password. There is HMAC signatures provisioned by a central authority such as some webservice, which means the proxy can validate a request without storing any password at all. There is Redis or Redis Cluster as a credential store.

Those sources can be chained in order, to look up multiple sources or to provide a custom rejection response. That is the interesting capability. A static credential can be a fallback behind a central service, and the rejection response can be tailored per source, so a client that fails one check gets a different answer from a client that fails another.

The static form is the simplest and appears in every example in the README:

```sh
dumbproxy -bind-address :1234 -auth 'static://?username=admin&password=123456'
```

The auth argument is a URL-like scheme, which is what makes the chaining and the different backends expressible in a single flag. The Redis and Redis Cluster options are backed by the go-redis client, visible in the dependency list, and the htpasswd file by a dedicated Go htpasswd library, so the hot-reload and the file parsing are real implementations rather than reimplementations of the format.

## ACME certificates cached in Redis, optionally encrypted

The proxy can run as an HTTP proxy over TLS, which is what makes it usable from a browser, and it can issue its own certificate. Native ACME support is built in, with Let's Encrypt named as one provider and BuyPass as the other, and the README shows both configurations. The Let's Encrypt version is a single flag:

```sh
dumbproxy -bind-address :443 -auth 'static://?username=admin&password=123456' -autocert
```

The BuyPass variant is spelled out with an explicit directory URL, an email address and an HTTP port for the challenge:

```sh
dumbproxy \
	-bind-address :443 \
	-auth 'static://?username=admin&password=123456' \
	-autocert \
	-autocert-acme 'https://api.buypass.com/acme/directory' \
	-autocert-email YOUR-EMAIL@EXAMPLE.ORG \
	-autocert-http :80
```

The certificate cache has four possible homes. It can sit in a local directory, in Redis or Redis Cluster, with an optional local in-memory inner cache in front of that, and there is an optional AEAD encryption layer for the cache. That layering is the part that matters operationally, because a shared cache across several proxy instances is the natural way to run this, and a cache file holding private key data on disk is worth protecting.

The proxy also supports client authentication with client TLS certificates, so a deployment can require a client certificate in addition to the Basic credential.

## Inspection resistance is a feature list, not a footnote

The README states that the proxy is resilient to DPI, including active probing, and points at the hidden_domain option for authentication providers. Active probing is the technique where an observer who suspects a tunnel sends traffic to the suspected endpoint and watches whether anything answers; a proxy that answers on the same port for arbitrary hostnames gives itself away. The hidden_domain option is the mechanism for making the proxy answer only for a domain that the real client actually uses.

Two further countermeasures sit alongside it. When connecting via upstream HTTP or SOCKS5 proxies, the proxy can optionally parrot the TLS fingerprints of popular software such as web browsers, which keeps a chain of proxies from being distinguishable by the TLS ClientHello it produces. There is also an optional countermeasure against TLS-in-TLS detection, available when both client and server are dumbproxy, which is the case where a TLS connection is tunnelled inside another TLS connection and the outer layer looks wrong.

The last point is worth a caveat. Parroting fingerprints and hiding probes are mitigations against a specific technique, and the README does not claim the traffic becomes indistinguishable from ordinary browsing. A project that lists these as options rather than defaults is being careful about what it promises.

## Access filters, upstream selection and bandwidth limits are JavaScript functions

The scripting layer is what separates dumbproxy from a configuration-file proxy. The README lists four uses for JavaScript: an access filter expressed as a JS function, upstream proxy selection by JS function, bandwidth limits by JS function, and GeoIP integration. Native PAC file support is also available for compatibility with existing solutions, so a browser that only speaks PAC can still use the proxy.

The mechanism is visible in the dependency list, which includes a JavaScript runtime for Go and a jsext directory in the source tree. That runtime is what makes the difference from a plain config proxy. A config file can express a list of rules; a function can compute a decision, look up a table, or consult something external, and the decision can depend on the connection in ways a static list cannot express.

The GeoIP integration deserves a note. The dependency list includes a Go library for reading MaxMind database files, and the feature list puts GeoIP under the scripting section, so the expectation is that a JS rule can ask where the connecting address is located. The README does not document where the MaxMind database file is obtained or where it is expected on disk, which is exactly the kind of detail an operator needs before deploying. Per-user bandwidth limits are part of the same feature set and pair naturally with the authentication sources, since a limit has to be attached to an identified user.

## The container runs as a non-root user with a fixed entrypoint

The Dockerfile is a two-stage build that ends in either a scratch image or Alpine, and the details are worth copying rather than rediscovering. The build stage compiles a static binary with CGO disabled:

```dockerfile
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -a -tags netgo -ldflags '-s -w -extldflags "-static"'
RUN mkdir /.dumbproxy

FROM scratch AS scratch
COPY --from=build /go/src/github.com/SenseUnit/dumbproxy/dumbproxy /
COPY --from=build --chown=9999:9999 /.dumbproxy /.dumbproxy
USER 9999:9999
EXPOSE 8080/tcp
ENTRYPOINT ["/dumbproxy", "-bind-address", ":8080"]
```

The image runs as user 9999 and exposes 8080, and the entrypoint already passes the bind address, so the container is usable with no arguments. The directory created during the build is where the certificate cache lives, which is why it is copied into the final image and why a mounted volume is what you would attach to it.

The README also gives a run recipe that shows the shape of a real deployment:

```sh
docker run -d \
    --security-opt no-new-privileges \
    -p 8080:8080 \
    --restart unless-stopped \
    --name dumbproxy \
    ghcr.io/senseunit/dumbproxy -auth 'static://?username=admin&password=123456'
```

For deployments behind another reverse proxy, the proxy protocol support lets dumbproxy sit behind HAProxy or Nginx, and the README shows an Nginx stream configuration with ssl_preread doing SNI routing to a dumbproxy upstream on 127.0.0.1:10443, and a Traefik label set for the same idea in Docker Compose.

## An SSH tunnel and a forward proxy are not the same service

The realistic alternative to running dumbproxy is to tunnel out over SSH, and the difference in approach matters even though the project itself supports both.

An SSH tunnel is a per-session encrypted channel authenticated by the SSH server, with the client configuration living in an ssh_config or a command line. dumbproxy is a shared endpoint: many clients connect to one listener, each is identified by a Basic credential that the proxy resolves through a chain of sources, and the traffic it forwards is HTTP or SOCKS5 rather than an SSH protocol. That difference changes the operational surface. The SSH path inherits the SSH server's user management, key handling and audit trail. The proxy path gives you centralised credential lookup, including an HMAC scheme where the client proves identity without the proxy storing a secret, and per-user bandwidth limits, which an SSH tunnel does not give you.

The trade runs the other way too. A tunnel carries arbitrary TCP inside SSH, with the encryption and the authentication already provided by the transport, so there is no separate TLS layer to deploy and no certificate to distribute. dumbproxy has to run a TLS listener for browsers to accept it, and the README is candid that using an HTTP proxy over TLS with browsers is a little bit tricky and that TLS must be enabled through the -cert and -key options or the -autocert option for it to work at all.

The project supports combining them, and the README gives the OpenSSH integration explicitly:

```sh
ssh -o ProxyCommand="dumbproxy -config proxy.cfg -mode stdio %h %p" root@server1
```

That form runs dumbproxy as a stdio transport under SSH, which is the pattern to reach for when the proxy is a stepping stone to a server rather than a general egress.

## Conclusion

Adopt dumbproxy when you need a forward proxy you can script, with authentication sourced from Redis or an HMAC service, per-user bandwidth limits, and a deployment shape that survives inspection by a network middlebox. Do not adopt it as a general purpose VPN replacement or as a filtering proxy for untrusted users, since the access policy lives in JavaScript functions and Basic authentication is the only scheme listed. Verify first that the Go toolchain matches the 1.26.0 line named in go.mod, that your clients can trust the certificate the proxy presents, and that the MaxMind database your GeoIP rules need is present, since the README does not say where that file comes from.

## FAQ

### How do I install dumbproxy?

Pre-built binaries are on the releases page, go install . builds it from source, there is a Docker image at ghcr.io/senseunit/dumbproxy and a Snap package, and the easy-dp script installs it on a fresh Linux server.

### How does dumbproxy authenticate users?

It supports Basic proxy authentication sourced from an auto-reloaded NCSA httpd-style passwords file, a static login and password, HMAC signatures provisioned by a central authority, or Redis or Redis Cluster. The sources can be chained in order to look up multiple sources or to give a custom rejection response.

### Can dumbproxy issue its own TLS certificate?

Yes, it has native ACME support for Let's Encrypt and BuyPass, with the certificate cache able to live in a local directory, in Redis or Redis Cluster, optionally with an in-memory inner cache and an optional AEAD encryption layer over the cache.

### Does dumbproxy support per-user bandwidth limits and GeoIP rules?

Both are listed among the scripting features, expressed as JavaScript functions, alongside the access filter and upstream proxy selection. The README does not document where the MaxMind database file used for GeoIP lookups is obtained.

## Sources

- [License: MIT](https://github.com/SenseUnit/dumbproxy/blob/master/LICENSE)
- [Project website](https://github.com/SenseUnit/dumbproxy/wiki)
- [README](https://github.com/SenseUnit/dumbproxy/blob/master/README.md)
- [Releases](https://github.com/SenseUnit/dumbproxy/releases)
- [SenseUnit/dumbproxy on GitHub](https://github.com/SenseUnit/dumbproxy)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/senseunit-dumbproxy
