# 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"]