Add container build, compose example and deployment docs

Multi-stage Dockerfile on debian:trixie-slim. openvpn3 and lwIP are cloned
at pinned refs and handed to CMake through OVG_OPENVPN3_DIR/OVG_LWIP_DIR
rather than left to FetchContent, whose GIT_TAG master would make the same
Dockerfile build a different VPN client each week. The unit suite runs in
the builder stage.

docker/openvpngate.conf overrides only the keys whose host default is wrong
inside a container -- loopback listen addresses, which make a published port
reach nothing, and relative state paths, which put the node failure history
on a layer that gets thrown away. Everything else stays absent and takes the
compiled-in default so the file cannot drift from the code.

The compose example drops every capability, runs read-only as uid 10001 and
publishes both ports to host loopback: the admin endpoint has no auth and
includes POST /switch. That configuration is the design constraint of this
project (no root, no tun device) turned into something testable.

docs/DOCKER.md 8 records what was checked against the source and what was
not: this sandbox has no docker daemon, so neither the image build nor the
compose file has actually been run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iceBear67
2026-07-28 05:54:44 +00:00
co-authored by Claude Opus 5
parent 5782207744
commit f37cd0a125
8 changed files with 706 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
# openvpngate -- configuration baked into the container image.
#
# This file sets ONLY the keys whose host default is wrong inside a container.
# Everything else is left absent and takes the compiled-in default, so this
# file cannot drift from the code the way a copied-and-edited full config does.
#
# etc/openvpngate.conf in the source tree is the annotated reference: every key,
# its default, and why that is the default. Read that one to change behaviour,
# then add the key here.
# ---------------------------------------------------------------------------
[socks5]
# ---------------------------------------------------------------------------
# The default is 127.0.0.1, which inside a container means "reachable from this
# container only" -- a published port would connect to nothing. Bind everywhere
# and let the container runtime decide who gets to reach it: docker-compose.yml
# publishes this to host loopback, not to the LAN.
listen_address = 0.0.0.0
listen_port = 1080
require_auth = true
# Credentials stay out of the image. Mount a file here (docker-compose.yml
# does) -- SIGHUP re-reads it without dropping a live session:
#
# docker compose kill -s HUP openvpngate
#
# If this path is missing the process exits at startup with
# "config: cannot open auth file"; if it is a *directory* -- which is what
# Docker silently creates for a bind mount whose source does not exist -- the
# file parses as empty and startup warns "every login will be refused". The
# compose file uses create_host_path: false so that case fails loudly instead.
auth_file = /etc/openvpngate/socks5.auth
# ---------------------------------------------------------------------------
[vpngate]
# ---------------------------------------------------------------------------
# Absolute. The default is relative (var/vpngate_cache.csv) and would resolve
# against the working directory; inside the image that is /var/lib/openvpngate,
# which happens to be right, but only by accident. Say it explicitly, because
# this is also the one path that survives a container replacement and the one
# directory a read-only rootfs still permits writing to.
cache_path = /var/lib/openvpngate/vpngate_cache.csv
# ---------------------------------------------------------------------------
[selector]
# ---------------------------------------------------------------------------
# Same reasoning. Worth persisting for a different reason though: this is the
# record of which nodes have failed on you, and losing it on every `up` means
# walking back into the same broken node with a flattering API score.
history_path = /var/lib/openvpngate/node_history.tsv
# ---------------------------------------------------------------------------
[admin]
# ---------------------------------------------------------------------------
# Bound everywhere for the same reason as socks5 above -- but this endpoint has
# NO authentication and includes POST /switch, so whoever can reach it can force
# your gateway onto another node. It is protected here by *publishing* rules,
# not by the bind address: keep it on 127.0.0.1 on the host side, or set
# enabled = false and drop the HEALTHCHECK from the Dockerfile, which probes it.
enabled = true
listen_address = 0.0.0.0
listen_port = 9080
# ---------------------------------------------------------------------------
[log]
# ---------------------------------------------------------------------------
# stderr, so that `docker logs` and the compose log driver see it. Logging to a
# file inside a container puts the record on the layer that gets thrown away.
level = info
file = -
+32
View File
@@ -0,0 +1,32 @@
# SOCKS5 credentials, one per line: <user>:<secret>
#
# cp docker/socks5.auth.example docker/socks5.auth
# $EDITOR docker/socks5.auth
#
# docker-compose.yml mounts docker/socks5.auth read-only into the container.
# Reloaded on SIGHUP without dropping a live session:
#
# docker compose kill -s HUP openvpngate
#
# Two accepted forms.
#
# 1. Plaintext. Hashed with a random salt when the file is read, so it is never
# held in memory in the clear -- but it is sitting in the clear right here,
# which is the part that matters on a shared host.
#
# alice:changeme
#
# 2. Pre-hashed: sha256$<salt_hex>$<sha256_hex(salt_hex + password)>. Note the
# salt is concatenated as its *hex text*, not as raw bytes. Generate one:
#
# salt=$(openssl rand -hex 16)
# printf 'alice:sha256$%s$%s\n' "$salt" \
# "$(printf '%s' "${salt}${PASSWORD}" | sha256sum | cut -d' ' -f1)"
#
# Lines starting with '#' and blank lines are ignored. A malformed line is a
# hard startup error, not a skipped entry: half-loaded credentials are worse
# than none.
#
# The entry below is an example and will be rejected by anyone paying
# attention. Replace it.
alice:changeme