Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go, with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS framework), and a REST API under /api/v1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
189 lines
7.8 KiB
Markdown
189 lines
7.8 KiB
Markdown
# vibeproxy
|
|
|
|
A small, self-contained reverse proxy manager, written in Go. Think
|
|
"Nginx Proxy Manager, but simpler and with no external dependencies" —
|
|
`vibeproxy` handles HTTP/HTTPS reverse proxying, TCP/UDP streams, TLS
|
|
termination, and Let's Encrypt certificates itself, using Go's standard
|
|
networking stack (`net/http`, `httputil.ReverseProxy`, `crypto/tls`,
|
|
`net`). It never shells out to nginx, HAProxy, or Caddy.
|
|
|
|
The admin UI is server-rendered Go templates + Bootstrap 5 + a little
|
|
vanilla JavaScript — no React/Vue/Node build step. Everything (templates,
|
|
CSS, JS, SQL migrations) is embedded into a single binary via `go:embed`,
|
|
so deployment is just:
|
|
|
|
```
|
|
./vibeproxy serve
|
|
```
|
|
|
|
## Features
|
|
|
|
- HTTP and HTTPS reverse proxying, routed by Host header / SNI
|
|
- Redirect hosts (301/302/307/308, with path & query preservation)
|
|
- TCP and UDP stream proxying, with optional PROXY protocol v1 to backends
|
|
- Multiple backends per proxy host: round robin, least-connections, or
|
|
weighted load balancing, with TCP/HTTP health checks and configurable
|
|
failure/recovery thresholds — unhealthy backends never receive traffic
|
|
- TLS termination, HTTP/2, WebSockets, keep-alive, streaming responses
|
|
- Let's Encrypt (HTTP-01 and DNS-01, via the [lego](https://github.com/go-acme/lego)
|
|
ACME client library) plus custom/uploaded certificates, with automatic
|
|
renewal
|
|
- Reusable Access Lists (CIDR allow/deny + optional HTTP Basic Auth)
|
|
- Per-host custom request/response headers
|
|
- Configurable trusted proxy networks — `X-Forwarded-For` is only trusted
|
|
from networks you explicitly configure
|
|
- SQLite storage (WAL mode, foreign keys, migrations) with the active
|
|
configuration cached in memory — the hot request path never touches the
|
|
database, and configuration changes apply live without a restart
|
|
- REST API under `/api/v1`, authenticated with revocable API tokens
|
|
- Single administrator account (HTTP Basic Auth, Argon2id password
|
|
hashing, CSRF-protected forms)
|
|
- Request logs (with configurable retention) and an audit log of every
|
|
configuration change
|
|
- Prometheus metrics, `/healthz`, `/readyz`, graceful shutdown, automatic
|
|
and on-demand SQLite backups
|
|
|
|
## Quick start
|
|
|
|
```
|
|
go build -o vibeproxy ./cmd/vibeproxy
|
|
sudo setcap 'cap_net_bind_service=+ep' ./vibeproxy # to bind :80/:443 without root
|
|
./vibeproxy serve
|
|
```
|
|
|
|
On first run, `vibeproxy` creates the `admin` account and prints a random
|
|
temporary password to stdout — save it, it's not shown again. Change it
|
|
under **Settings → Administrator Account** after logging in.
|
|
|
|
By default:
|
|
|
|
- `:80` and `:443` — proxy traffic (HTTP/HTTPS reverse proxy + redirect
|
|
hosts + Let's Encrypt HTTP-01 challenges)
|
|
- `:8081` — admin web UI, REST API, `/healthz`, `/readyz`, `/metrics`.
|
|
**Put this behind a firewall or VPN** — Basic Auth protects it, but it
|
|
shouldn't be exposed to the internet.
|
|
|
|
All of this is configurable via flags or environment variables:
|
|
|
|
| Flag | Env var | Default | Purpose |
|
|
|---|---|---|---|
|
|
| `--data-dir` | `VIBEPROXY_DATA_DIR` | `./data` | database, certs, backups |
|
|
| `--db-path` | `VIBEPROXY_DB_PATH` | `./data/vibeproxy.db` | SQLite file |
|
|
| `--http-addr` | `VIBEPROXY_HTTP_ADDR` | `:80` | HTTP proxy listener |
|
|
| `--https-addr` | `VIBEPROXY_HTTPS_ADDR` | `:443` | HTTPS proxy listener |
|
|
| `--admin-addr` | `VIBEPROXY_ADMIN_ADDR` | `:8081` | admin UI + API |
|
|
|
|
Everything else (ACME email, renewal window, log retention, trusted proxy
|
|
CIDRs, backup schedule, ...) is runtime configuration stored in SQLite and
|
|
editable from **Settings**, so it takes effect without a restart.
|
|
|
|
## CLI
|
|
|
|
```
|
|
vibeproxy serve # run the proxy, admin UI, and API
|
|
vibeproxy version # print the version
|
|
vibeproxy config check # validate config + run migrations, then exit
|
|
vibeproxy admin reset-password # generate a new admin password
|
|
vibeproxy database backup # write an on-demand SQLite backup
|
|
vibeproxy database migrate # apply pending migrations
|
|
vibeproxy certificates renew # renew any due Let's Encrypt certs
|
|
```
|
|
|
|
## systemd
|
|
|
|
```
|
|
sudo useradd --system --home /var/lib/vibeproxy --shell /usr/sbin/nologin vibeproxy
|
|
sudo cp vibeproxy /usr/local/bin/vibeproxy
|
|
sudo cp systemd/vibeproxy.service /etc/systemd/system/
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now vibeproxy
|
|
sudo journalctl -u vibeproxy -f # first run prints the admin password here
|
|
```
|
|
|
|
The unit grants `CAP_NET_BIND_SERVICE` so `vibeproxy` can bind `:80`/`:443`
|
|
without running as root, and sandboxes the process with `ProtectSystem`,
|
|
`ProtectHome`, `PrivateDevices`, etc. It stores everything under
|
|
`/var/lib/vibeproxy` (mode `0700`).
|
|
|
|
## Docker
|
|
|
|
```
|
|
docker compose up -d
|
|
```
|
|
|
|
See `docker-compose.yml` / `Dockerfile`. The container runs as a
|
|
non-root user, so it listens on `8080`/`8443` internally — `docker-compose.yml`
|
|
maps host `80`/`443` to those.
|
|
|
|
## REST API
|
|
|
|
All endpoints are under `/api/v1` on the admin listener, authenticated
|
|
with a bearer token (create one from **API** in the UI — it's shown once):
|
|
|
|
```
|
|
curl -H "Authorization: Bearer vp_xxxxxxxx" \
|
|
http://localhost:8081/api/v1/proxy-hosts
|
|
```
|
|
|
|
Resources: `proxy-hosts`, `streams`, `certificates`, `access-lists`,
|
|
`settings`, `logs/requests`, `logs/audit`, `stats`, `tokens`. See
|
|
`internal/api` for the full route list.
|
|
|
|
## Project layout
|
|
|
|
```
|
|
cmd/vibeproxy/ CLI entry point (cobra commands)
|
|
internal/
|
|
api/ REST API (/api/v1)
|
|
app/ wires every subsystem together for `serve`
|
|
auth/ password hashing, Basic Auth, API tokens, CSRF
|
|
access/ CIDR access lists + Basic Auth evaluation
|
|
backup/ SQLite backup/restore
|
|
certificates/ ACME (via lego) + custom cert storage & renewal
|
|
config/ process bootstrap flags/env
|
|
database/ SQLite connection, migrations, repositories
|
|
health/ backend health checking
|
|
loadbalancer/ backend pools: round robin / least-conn / weighted
|
|
logging/ buffered request logging + audit log + retention
|
|
metrics/ Prometheus collectors
|
|
models/ domain types shared across packages
|
|
proxy/ the HTTP/HTTPS reverse proxy engine
|
|
runtime/ in-memory active-configuration snapshot
|
|
stats/ dashboard/API statistics
|
|
streams/ TCP/UDP proxying
|
|
web/ server-rendered admin UI (Go templates + embedded assets)
|
|
templates/, static/
|
|
systemd/ vibeproxy.service
|
|
```
|
|
|
|
## Design notes
|
|
|
|
- **Hot path stays out of SQLite.** `internal/runtime` holds an
|
|
immutable, atomically-swapped snapshot of proxy hosts, backends, access
|
|
lists, and settings. Every proxied request/connection reads that
|
|
snapshot; database writes only happen when an admin changes
|
|
configuration, at which point `Store.Reload` rebuilds the snapshot.
|
|
- **`X-Forwarded-For` is not trusted by default.** The real client IP is
|
|
taken from the TCP peer address unless the peer is inside a configured
|
|
"trusted proxy" CIDR (Settings → Trusted Proxy Networks), in which case
|
|
the right-most untrusted entry in `X-Forwarded-For` is used instead.
|
|
- **Certificates never touch logs.** Private keys are written to disk
|
|
with `0600` permissions (directories `0700`) and are never logged or
|
|
returned by the API.
|
|
- **Reloads don't drop connections.** Stream (TCP/UDP) reconciliation
|
|
diffs the desired vs. running listener set and only restarts streams
|
|
whose configuration actually changed; unaffected streams, and all
|
|
in-flight HTTP requests on the previous configuration snapshot, are
|
|
left alone.
|
|
|
|
## Testing
|
|
|
|
```
|
|
go test ./...
|
|
```
|
|
|
|
Covers load-balancing strategies and health-check thresholds, access-list
|
|
CIDR evaluation, password/API-token hashing, SQLite migrations and
|
|
proxy-host CRUD round-trips, an end-to-end TCP stream proxy test, and a
|
|
render-smoke-test for every admin UI page.
|