Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: SenseUnit/dumbproxy
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.19.0
Choose a base ref
...
head repository: SenseUnit/dumbproxy
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 17 commits
  • 9 files changed
  • 3 contributors

Commits on Dec 25, 2024

  1. instrumenting

    Snawoot committed Dec 25, 2024
    Copy the full SHA
    ff35253 View commit details
  2. Merge pull request #101 from SenseUnit/instrumenting

    Instrumenting
    Snawoot authored Dec 25, 2024
    Copy the full SHA
    7488829 View commit details
  3. upd doc

    Snawoot committed Dec 25, 2024
    Copy the full SHA
    44f73d2 View commit details
  4. in-memory certificate cache

    Snawoot committed Dec 25, 2024
    Copy the full SHA
    8ef7f36 View commit details
  5. certcache: redis cache

    Snawoot committed Dec 25, 2024
    Copy the full SHA
    1ab15e1 View commit details
  6. certcache: encryption layer

    Snawoot committed Dec 25, 2024
    Copy the full SHA
    ca2d25c View commit details
  7. Merge pull request #102 from SenseUnit/adv_cert_cache

    Advanced cert cache
    Snawoot authored Dec 25, 2024
    Copy the full SHA
    d34ac1c View commit details
  8. update snapcraft.yaml

    Snawoot committed Dec 25, 2024
    Copy the full SHA
    aba8de8 View commit details
  9. upd docs

    Snawoot committed Dec 25, 2024
    Copy the full SHA
    3514865 View commit details

Commits on Dec 27, 2024

  1. Update README.md

    Snawoot authored Dec 27, 2024
    Copy the full SHA
    fcc3cde View commit details
  2. Update README.md

    Snawoot authored Dec 27, 2024
    Copy the full SHA
    5abb368 View commit details

Commits on Jan 4, 2025

  1. fix cert blacklist reload

    Snawoot committed Jan 4, 2025
    Copy the full SHA
    eb822b1 View commit details
  2. Merge pull request #104 from SenseUnit/fix_cert_bl_reload

    fix cert blacklist reload
    Snawoot authored Jan 4, 2025
    Copy the full SHA
    cb7b4e9 View commit details
  3. bump snap version

    Snawoot committed Jan 4, 2025
    Copy the full SHA
    882078d View commit details

Commits on Jan 13, 2025

  1. proxy protocol support added

    docs updated
    zolg committed Jan 13, 2025
    Copy the full SHA
    1e82281 View commit details
  2. Merge pull request #105 from zolg/proxy-protocol-support

    Proxy protocol support on listening socket
    Snawoot authored Jan 13, 2025
    Copy the full SHA
    ba4aeb2 View commit details
  3. bump snap version

    Snawoot committed Jan 13, 2025
    Copy the full SHA
    0e7e9db View commit details
Showing with 464 additions and 58 deletions.
  1. +67 −4 README.md
  2. +7 −8 auth/cert.go
  3. +66 −0 certcache/cryptobox.go
  4. +75 −0 certcache/local.go
  5. +59 −0 certcache/redis.go
  6. +4 −0 go.mod
  7. +12 −0 go.sum
  8. +172 −44 main.go
  9. +2 −2 snapcraft.yaml
71 changes: 67 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ dumbproxy

[![dumbproxy](https://snapcraft.io//dumbproxy/badge.svg)](https://snapcraft.io/dumbproxy)

Dumbest HTTP proxy ever.
Simple, scriptable, secure forward proxy.

## Features

@@ -18,12 +18,17 @@ Dumbest HTTP proxy ever.
* Supports TLS operation mode (HTTP(S) proxy over TLS)
* Supports client authentication with client TLS certificates
* Native ACME support (can issue TLS certificates automatically using Let's Encrypt or BuyPass)
* Certificate cache in local directory
* Certificate cache in Redis/Redis Cluster
* Optional local in-memory inner cache
* Optional AEAD encryption layer for cache
* Per-user bandwidth limits
* HTTP/2 support
* Optional DNS cache
* Resilient to DPI (including active probing, see `hidden_domain` option for authentication providers)
* Connecting via upstream HTTP(S)/SOCKS5 proxies (proxy chaining)
* systemd socket activation
* [Proxy protocol](https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt) support for working behind a reverse proxy (HAProxy, Nginx)
* Scripting with JavaScript:
* Access filter by JS function
* Upstream proxy selection by JS function
@@ -39,7 +44,7 @@ Pre-built binaries available on [releases](https://github.com/SenseUnit/dumbprox
Alternatively, you may install dumbproxy from source. Run within source directory

```
go install
go install .
```

#### Docker
@@ -83,6 +88,48 @@ Run HTTPS proxy (HTTP proxy over TLS) with automatic certs from LetsEncrypt on p
dumbproxy -bind-address :443 -auth 'static://?username=admin&password=123456' -autocert
```

### Example: HTTP proxy over TLS (pre-issued cert) behind Nginx reverse proxy performing SNI routing

Run HTTPS proxy (HTTP proxy over TLS) with pre-issued cert listening proxy protocol on localhost's 10443 with `Basic` authentication (users and passwords in /etc/dumbproxy.htpasswd)):

```sh
dumbproxy \
-bind-address 127.0.0.1:10443 \
-proxyproto \
-auth basicfile://?path=/etc/dumbproxy.htpasswd \
-cert=/etc/letsencrypt/live/proxy.example.com/fullchain.pem \
-key=/etc/letsencrypt/live/proxy.example.com/privkey.pem
```

Nginx config snippet:

```
stream
{
ssl_preread on;
map $ssl_preread_server_name $backend
{
proxy.example.com dumbproxy;
...
}
upstream dumbproxy
{
server 127.0.0.1:10443;
}
server
{
listen 443;
listen [::]:443;
proxy_protocol on;
proxy_pass $backend;
}
}
```

### Example: HTTP proxy over TLS (BuyPass automatic certs)

Run HTTPS proxy (HTTP proxy over TLS) with automatic certs from BuyPass on port 443 with `Basic` authentication with username `admin` and password `123456`:
@@ -285,16 +332,32 @@ Usage of /home/user/go/bin/dumbproxy:
issue TLS certificates automatically
-autocert-acme string
custom ACME endpoint (default "https://acme-v02.api.letsencrypt.org/directory")
-autocert-dir string
path to autocert cache (default "/home/user/.dumbproxy/autocert")
-autocert-cache-enc-key value
hex-encoded encryption key for cert cache entries. Can be also set with DUMBPROXY_CACHE_ENC_KEY environment variable
-autocert-cache-redis value
use Redis URL for autocert cache
-autocert-cache-redis-cluster value
use Redis Cluster URL for autocert cache
-autocert-cache-redis-prefix string
prefix to use for keys in Redis or Redis Cluster cache
-autocert-dir value
use directory path for autocert cache
-autocert-email string
email used for ACME registration
-autocert-http string
listen address for HTTP-01 challenges handler of ACME
-autocert-local-cache-timeout duration
timeout for cert cache queries (default 10s)
-autocert-local-cache-ttl duration
enables in-memory cache for certificates
-autocert-whitelist value
restrict autocert domains to this comma-separated list
-bind-address string
HTTP proxy listen address. Set empty value to use systemd socket activation. (default ":8080")
-proxyproto
listen proxy protocol
-bind-pprof string
enables pprof debug endpoints
-bind-reuseport
allow multiple server instances on the same port
-bw-limit uint
15 changes: 7 additions & 8 deletions auth/cert.go
Original file line number Diff line number Diff line change
@@ -43,12 +43,6 @@ func NewCertAuth(param_url *url.URL, logger *clog.CondLogger) (*CertAuth, error)
}
auth.blacklist.Store(new(serialNumberSetFile))

if auth.blacklistFilename != "" {
if err := auth.reload(); err != nil {
return nil, fmt.Errorf("unable to load initial certificate blacklist: %w", err)
}
}

reloadInterval := 15 * time.Second
if reloadIntervalOption := values.Get("reload"); reloadIntervalOption != "" {
parsedInterval, err := time.ParseDuration(reloadIntervalOption)
@@ -57,8 +51,13 @@ func NewCertAuth(param_url *url.URL, logger *clog.CondLogger) (*CertAuth, error)
}
reloadInterval = parsedInterval
}
if reloadInterval > 0 {
go auth.reloadLoop(reloadInterval)
if auth.blacklistFilename != "" {
if err := auth.reload(); err != nil {
return nil, fmt.Errorf("unable to load initial certificate blacklist: %w", err)
}
if reloadInterval > 0 {
go auth.reloadLoop(reloadInterval)
}
}

return auth, nil
66 changes: 66 additions & 0 deletions certcache/cryptobox.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package certcache

import (
"context"
"crypto/cipher"
cryptorand "crypto/rand"
"errors"

"golang.org/x/crypto/acme/autocert"
"golang.org/x/crypto/chacha20poly1305"
)

type EncryptedCache struct {
aead cipher.AEAD
next autocert.Cache
}

func NewEncryptedCache(key []byte, next autocert.Cache) (*EncryptedCache, error) {
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return nil, err
}
return &EncryptedCache{
aead: aead,
next: next,
}, nil
}

func (c *EncryptedCache) Get(ctx context.Context, key string) ([]byte, error) {
encryptedData, err := c.next.Get(ctx, key)
if err != nil {
return nil, err
}

if len(encryptedData) < c.aead.NonceSize() {
return nil, errors.New("ciphertext too short")
}

// Split nonce and ciphertext.
nonce, ciphertext := encryptedData[:c.aead.NonceSize()], encryptedData[c.aead.NonceSize():]

// Decrypt the data and check it wasn't tampered with.
plaintext, err := c.aead.Open(nil, nonce, ciphertext, []byte(key))
if err != nil {
return nil, err
}

return plaintext, nil
}

func (c *EncryptedCache) Put(ctx context.Context, key string, data []byte) error {
// Select a random nonce, and leave capacity for the ciphertext.
nonce := make([]byte, c.aead.NonceSize(), c.aead.NonceSize()+len(data)+c.aead.Overhead())
if _, err := cryptorand.Read(nonce); err != nil {
return err
}

// Encrypt the message and append the ciphertext to the nonce.
encryptedData := c.aead.Seal(nonce, nonce, data, []byte(key))

return c.next.Put(ctx, key, encryptedData)
}

func (c *EncryptedCache) Delete(ctx context.Context, key string) error {
return c.next.Delete(ctx, key)
}
75 changes: 75 additions & 0 deletions certcache/local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package certcache

import (
"context"
"sync"
"time"

"github.com/jellydator/ttlcache/v3"
"golang.org/x/crypto/acme/autocert"
)

type certCacheKey = string
type certCacheValue struct {
res []byte
err error
}

type LocalCertCache struct {
cache *ttlcache.Cache[certCacheKey, certCacheValue]
next autocert.Cache
startOnce sync.Once
stopOnce sync.Once
}

func NewLocalCertCache(next autocert.Cache, ttl, timeout time.Duration) *LocalCertCache {
cache := ttlcache.New[certCacheKey, certCacheValue](
ttlcache.WithTTL[certCacheKey, certCacheValue](ttl),
ttlcache.WithLoader(
ttlcache.NewSuppressedLoader(
ttlcache.LoaderFunc[certCacheKey, certCacheValue](
func(c *ttlcache.Cache[certCacheKey, certCacheValue], key certCacheKey) *ttlcache.Item[certCacheKey, certCacheValue] {
ctx, cl := context.WithTimeout(context.Background(), timeout)
defer cl()
res, err := next.Get(ctx, key)
if err != nil {
return c.Set(key, certCacheValue{res, err}, -100)
}
return c.Set(key, certCacheValue{res, err}, 0)
},
),
nil),
),
)
return &LocalCertCache{
cache: cache,
next: next,
}
}

func (cc *LocalCertCache) Get(_ context.Context, key string) ([]byte, error) {
resItem := cc.cache.Get(key).Value()
return resItem.res, resItem.err
}

func (cc *LocalCertCache) Put(ctx context.Context, key string, data []byte) error {
cc.cache.Set(key, certCacheValue{data, nil}, 0)
return cc.next.Put(ctx, key, data)
}

func (cc *LocalCertCache) Delete(ctx context.Context, key string) error {
cc.cache.Delete(key)
return cc.next.Delete(ctx, key)
}

func (cc *LocalCertCache) Start() {
cc.startOnce.Do(func() {
go cc.cache.Start()
})
}

func (cc *LocalCertCache) Stop() {
cc.stopOnce.Do(cc.cache.Stop)
}

var _ autocert.Cache = new(LocalCertCache)
59 changes: 59 additions & 0 deletions certcache/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package certcache

import (
"context"

"github.com/redis/go-redis/v9"
"golang.org/x/crypto/acme/autocert"
)

type RedisCache struct {
r redis.Cmdable
pfx string
}

func NewRedisCache(r redis.Cmdable, prefix string) *RedisCache {
return &RedisCache{
r: r,
pfx: prefix,
}
}

func (r *RedisCache) Get(ctx context.Context, key string) ([]byte, error) {
res, err := r.r.Get(ctx, r.pfx+key).Bytes()
if err != nil {
if err == redis.Nil {
return nil, autocert.ErrCacheMiss
}
return nil, err
}
return res, nil
}

func (r *RedisCache) Put(ctx context.Context, key string, data []byte) error {
return r.r.Set(ctx, r.pfx+key, data, 0).Err()
}

func (r *RedisCache) Delete(ctx context.Context, key string) error {
return r.r.Del(ctx, r.pfx+key).Err()
}

func RedisCacheFromURL(url string, prefix string) (*RedisCache, error) {
opts, err := redis.ParseURL(url)
if err != nil {
return nil, err
}

r := redis.NewClient(opts)
return NewRedisCache(r, prefix), nil
}

func RedisClusterCacheFromURL(url string, prefix string) (*RedisCache, error) {
opts, err := redis.ParseClusterURL(url)
if err != nil {
return nil, err
}

r := redis.NewClusterClient(opts)
return NewRedisCache(r, prefix), nil
}
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/jellydator/ttlcache/v3 v3.3.0
github.com/libp2p/go-reuseport v0.4.0
github.com/redis/go-redis/v9 v9.7.0
github.com/tg123/go-htpasswd v1.2.3
github.com/zeebo/xxh3 v1.0.2
golang.org/x/crypto v0.31.0
@@ -19,11 +20,14 @@ require (

require (
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/pires/go-proxyproto v0.8.0
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
Loading