forked from cloud/ovgate
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>
158 lines
6.0 KiB
YAML
158 lines
6.0 KiB
YAML
# Example deployment. See docs/DOCKER.md for what each block is protecting
|
|
# against; the short version is that a userspace VPN client should be able to
|
|
# run with every capability dropped, and this file is where that gets proven
|
|
# rather than asserted.
|
|
#
|
|
# cp docker/socks5.auth.example docker/socks5.auth # then edit it
|
|
# docker compose up -d --build
|
|
# curl -x socks5h://alice:...@127.0.0.1:1080 https://ifconfig.me
|
|
#
|
|
name: openvpngate
|
|
|
|
services:
|
|
openvpngate:
|
|
build:
|
|
context: .
|
|
args:
|
|
# OFF drops openvpn3 and lwIP: `direct` egress only, no VPN. Builds in
|
|
# seconds, useful for exercising the SOCKS5 layer, useless as a gateway.
|
|
OVG_WITH_TUNNEL: "ON"
|
|
image: openvpngate:0.1.0
|
|
restart: unless-stopped
|
|
|
|
# Both published to host loopback, and that is doing real work in each case:
|
|
# 1080 the proxy is only as private as who can reach it;
|
|
# 9080 the admin endpoint has NO authentication and includes POST /switch.
|
|
# Change these to 0.0.0.0 only after reading docs/DOCKER.md 4.
|
|
ports:
|
|
- "127.0.0.1:1080:1080"
|
|
- "127.0.0.1:9080:9080"
|
|
|
|
volumes:
|
|
# Long syntax with create_host_path: false on purpose. Docker's default is
|
|
# to silently create a *directory* when a bind mount's source is missing,
|
|
# which for the auth file means the proxy starts and refuses every login,
|
|
# and for the config means it starts on defaults that listen on loopback
|
|
# inside the container and are reachable by nobody. Fail at `up` instead.
|
|
- type: bind
|
|
source: ./docker/openvpngate.conf
|
|
target: /etc/openvpngate/openvpngate.conf
|
|
read_only: true
|
|
bind:
|
|
create_host_path: false
|
|
|
|
- type: bind
|
|
source: ./docker/socks5.auth
|
|
target: /etc/openvpngate/socks5.auth
|
|
read_only: true
|
|
bind:
|
|
create_host_path: false
|
|
|
|
# The node cache and the per-node failure history. Worth persisting for
|
|
# more than startup speed: the history is the record of which volunteer
|
|
# nodes have already failed on you, and throwing it away on every `up`
|
|
# means walking back into the same one.
|
|
- ovg-state:/var/lib/openvpngate
|
|
|
|
# ---- the point of the exercise ----------------------------------------
|
|
# No NET_ADMIN, no /dev/net/tun, no --privileged, no sysctls, uid 10001.
|
|
# The tunnel is terminated by lwIP inside the process, so nothing here
|
|
# needs kernel networking privileges. If a change to this project ever
|
|
# makes one of these lines necessary, the change is wrong.
|
|
cap_drop:
|
|
- ALL
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
read_only: true
|
|
tmpfs:
|
|
# Nothing is known to need it; it is here so that a library that decides
|
|
# to spool to /tmp fails loudly at write time rather than at connect time.
|
|
- /tmp:size=16m,mode=1777
|
|
|
|
# SIGTERM starts a graceful shutdown (stop accepting, drain, tear the tunnel
|
|
# down); it normally completes in well under a second. The window is wide
|
|
# because the alternative when it does not is SIGKILL in the middle of
|
|
# writing the node history.
|
|
stop_grace_period: 30s
|
|
|
|
# socks5.max_sessions defaults to 1200 and each session costs a client-side
|
|
# fd (plus an egress-side one in direct mode). The Docker default is usually
|
|
# far higher than this, but it is not guaranteed to be.
|
|
ulimits:
|
|
nofile:
|
|
soft: 8192
|
|
hard: 8192
|
|
|
|
# A gateway logs one line per session at info. Unrotated json-file logging
|
|
# is how a container quietly fills a host disk.
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "5"
|
|
|
|
# The image's HEALTHCHECK probes admin /healthz. It is liveness only -- it
|
|
# says the process is answering, not that a tunnel is up. Do NOT wire an
|
|
# unhealthy-triggers-restart supervisor on top: the service already responds
|
|
# to a degrading tunnel by switching nodes, and a restart throws away the
|
|
# drain, the session, and the freshly-learned reason the node was bad.
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Proxy without a VPN, for splitting "is the SOCKS5 implementation correct"
|
|
# from "is the tunnel up". Egress is a plain host socket -- traffic through
|
|
# this one is NOT tunnelled, which is why it is behind a profile and on its
|
|
# own port.
|
|
#
|
|
# docker compose --profile test up openvpngate-direct
|
|
# -------------------------------------------------------------------------
|
|
openvpngate-direct:
|
|
profiles: [test]
|
|
# Same image and same build inputs as above, so this resolves to the one
|
|
# already built rather than trying to pull it from a registry.
|
|
build:
|
|
context: .
|
|
args:
|
|
OVG_WITH_TUNNEL: "ON"
|
|
image: openvpngate:0.1.0
|
|
command:
|
|
- "-c"
|
|
- "/etc/openvpngate/openvpngate.conf"
|
|
- "--egress"
|
|
- "direct"
|
|
- "--no-admin"
|
|
ports:
|
|
- "127.0.0.1:1081:1080"
|
|
volumes:
|
|
- type: bind
|
|
source: ./docker/openvpngate.conf
|
|
target: /etc/openvpngate/openvpngate.conf
|
|
read_only: true
|
|
bind:
|
|
create_host_path: false
|
|
- type: bind
|
|
source: ./docker/socks5.auth
|
|
target: /etc/openvpngate/socks5.auth
|
|
read_only: true
|
|
bind:
|
|
create_host_path: false
|
|
# Inherited from the image, but --no-admin means there is nothing listening
|
|
# on 9080 to answer it; left armed it would report this container unhealthy
|
|
# forever.
|
|
healthcheck:
|
|
disable: true
|
|
cap_drop:
|
|
- ALL
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp:size=16m,mode=1777
|
|
# This one still fetches and caches the node directory even though it
|
|
# never dials a node, so it needs its state directory writable. Throwaway
|
|
# rather than a volume: nothing produced by a no-VPN test run is worth
|
|
# keeping, least of all a failure history for nodes it never contacted.
|
|
- /var/lib/openvpngate:size=64m,mode=1777
|
|
|
|
volumes:
|
|
ovg-state:
|