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>
156 lines
6.7 KiB
Docker
156 lines
6.7 KiB
Docker
# syntax=docker/dockerfile:1
|
|
#
|
|
# openvpngate as a container.
|
|
#
|
|
# The interesting property of this image is what it does *not* need: no
|
|
# --privileged, no --cap-add NET_ADMIN, no --device /dev/net/tun, no
|
|
# --sysctl. The tunnel is terminated in userspace by lwIP, so from the kernel's
|
|
# point of view this is an ordinary unprivileged process that listens on a
|
|
# socket. docker-compose.yml drops every capability to make that testable
|
|
# rather than merely claimed.
|
|
#
|
|
# See docs/DOCKER.md for the container-specific configuration traps -- the
|
|
# defaults in etc/openvpngate.conf are written for a host, and two of them
|
|
# (loopback listen addresses, relative state paths) are actively wrong here.
|
|
|
|
# Pinned rather than :latest, and pinned to *trixie* specifically: the runtime
|
|
# package names below are release-specific (bookworm ships libssl3 and libfmt9,
|
|
# trixie ships libssl3t64 and libfmt10). Moving this tag means re-checking them.
|
|
ARG DEBIAN_TAG=trixie-slim
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 1 -- build
|
|
# ---------------------------------------------------------------------------
|
|
FROM debian:${DEBIAN_TAG} AS build
|
|
|
|
# openvpn3 and lwIP are source dependencies. Left to itself, CMake's
|
|
# FetchContent pulls openvpn3 at GIT_TAG master, which makes this image
|
|
# un-reproducible: the same Dockerfile and the same commit of this repo would
|
|
# build a different VPN client next week. So clone them here at pinned refs and
|
|
# hand the checkouts to CMake through OVG_OPENVPN3_DIR / OVG_LWIP_DIR, the knob
|
|
# it already has for a pre-existing checkout.
|
|
#
|
|
# The openvpn3 default is the commit this tree was developed and tested against.
|
|
# Bump it deliberately, and re-run the suite when you do.
|
|
ARG OPENVPN3_REF=1512c16622288f3c01da09d3278ac61a86dca26d
|
|
ARG LWIP_REF=STABLE-2_2_1_RELEASE
|
|
|
|
# OFF builds without openvpn3/lwIP: `direct` egress only, host sockets, no VPN.
|
|
# Useful for testing the SOCKS5 layer in isolation, useless as a gateway -- and
|
|
# the binary refuses to start in tunnel mode rather than proxying in the clear.
|
|
ARG OVG_WITH_TUNNEL=ON
|
|
|
|
# The unit tests are the only thing between "it compiled" and "it works", and
|
|
# they cost seconds against a build measured in minutes. Turn off with
|
|
# --build-arg OVG_RUN_TESTS=0 on a builder without loopback networking: a dozen
|
|
# tests bind and connect on 127.0.0.1.
|
|
ARG OVG_RUN_TESTS=1
|
|
|
|
ARG BUILD_JOBS=
|
|
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
build-essential \
|
|
cmake \
|
|
pkg-config \
|
|
git \
|
|
ca-certificates \
|
|
libasio-dev \
|
|
libssl-dev \
|
|
liblz4-dev \
|
|
libfmt-dev \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Fetched before the source is copied, so editing a .cpp does not re-clone the
|
|
# dependencies. --filter=blob:none keeps the clone small while still allowing a
|
|
# checkout of an arbitrary ref -- which a --depth 1 clone cannot do for a bare
|
|
# commit id, and the openvpn3 pin above is one.
|
|
WORKDIR /deps
|
|
RUN git clone --filter=blob:none --no-checkout \
|
|
https://github.com/OpenVPN/openvpn3.git openvpn3 \
|
|
&& git -C openvpn3 checkout --detach "${OPENVPN3_REF}" \
|
|
&& git clone --filter=blob:none --no-checkout \
|
|
https://github.com/lwip-tcpip/lwip.git lwip \
|
|
&& git -C lwip checkout --detach "${LWIP_REF}"
|
|
|
|
WORKDIR /src
|
|
COPY . .
|
|
|
|
RUN cmake -S . -B build \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
-DOVG_WITH_TUNNEL="${OVG_WITH_TUNNEL}" \
|
|
-DOVG_BUILD_TESTS=ON \
|
|
-DOVG_OPENVPN3_DIR=/deps/openvpn3 \
|
|
-DOVG_LWIP_DIR=/deps/lwip \
|
|
&& cmake --build build -j"${BUILD_JOBS:-$(nproc)}"
|
|
|
|
RUN if [ "${OVG_RUN_TESTS}" = "1" ]; then ./build/tests/ovg_tests; fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stage 2 -- runtime
|
|
# ---------------------------------------------------------------------------
|
|
FROM debian:${DEBIAN_TAG} AS runtime
|
|
|
|
LABEL org.opencontainers.image.title="openvpngate" \
|
|
org.opencontainers.image.description="OpenVPN client with an authenticated SOCKS5 front door, terminated in userspace" \
|
|
org.opencontainers.image.version="0.1.0" \
|
|
org.opencontainers.image.licenses="NOASSERTION"
|
|
|
|
# Release-specific names -- see the DEBIAN_TAG comment at the top.
|
|
# curl is here only for the HEALTHCHECK below; drop both together if you run
|
|
# with admin.enabled = false.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
libssl3t64 \
|
|
liblz4-1 \
|
|
libfmt10 \
|
|
ca-certificates \
|
|
curl \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# A fixed uid, not a distro-assigned one: a bind-mounted state directory has to
|
|
# be chown'd to a number the host knows in advance.
|
|
RUN groupadd --system --gid 10001 ovg \
|
|
&& useradd --system --uid 10001 --gid 10001 \
|
|
--home-dir /var/lib/openvpngate --shell /usr/sbin/nologin ovg
|
|
|
|
COPY --from=build /src/build/src/openvpngate /usr/local/bin/openvpngate
|
|
# Brings up one tunnel, pings through it, exits. The first thing worth running
|
|
# on a host with real network access, and the fastest way to tell "the image is
|
|
# broken" from "this network cannot reach VPNGate".
|
|
COPY --from=build /src/build/src/ovg_tunnel_smoke /usr/local/bin/ovg_tunnel_smoke
|
|
|
|
# Baked in so `docker run` alone works; docker-compose.yml mounts over it so the
|
|
# config can be edited without a rebuild.
|
|
COPY docker/openvpngate.conf /etc/openvpngate/openvpngate.conf
|
|
|
|
# The one writable path the service needs: the node cache and the per-node
|
|
# outcome history. An empty named volume mounted here inherits this ownership,
|
|
# which is what makes `read_only: true` on the rest of the rootfs work.
|
|
RUN install -d -o ovg -g ovg -m 0750 /var/lib/openvpngate
|
|
|
|
# So that a relative path in a user-supplied config resolves somewhere writable
|
|
# instead of failing at the first cache write.
|
|
WORKDIR /var/lib/openvpngate
|
|
|
|
# 1080 SOCKS5, 9080 admin. The admin endpoint has NO authentication; publish it
|
|
# to host loopback or not at all.
|
|
EXPOSE 1080 9080
|
|
|
|
USER ovg
|
|
|
|
# Liveness only -- /healthz answers 200 as soon as the admin server is up and
|
|
# says nothing about whether a tunnel is established. Tunnel state is
|
|
# GET /status, and the service already reacts to a degraded tunnel by switching
|
|
# nodes on its own; see docs/DOCKER.md before wiring restart-on-unhealthy.
|
|
# start-period covers a cold start: fetch the directory, probe candidates,
|
|
# negotiate with a volunteer-run server on the other side of the world.
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
|
CMD curl -fsS http://127.0.0.1:9080/healthz || exit 1
|
|
|
|
# Explicit because the second one means something different: SIGTERM starts a
|
|
# graceful shutdown, a second SIGTERM/SIGINT during it stops immediately. The
|
|
# binary runs as pid 1 (exec form, no shell wrapper), so it receives them.
|
|
STOPSIGNAL SIGTERM
|
|
|
|
ENTRYPOINT ["/usr/local/bin/openvpngate"]
|
|
CMD ["-c", "/etc/openvpngate/openvpngate.conf"]
|