Gitea reads .gitea/workflows in preference to .github/workflows, so this
supersedes the ghcr.io workflow here without deleting it: that one stays
correct for github.com, and the two cannot race.
Registry cache rather than type=gha -- a self-hosted act_runner supplies
ACTIONS_CACHE_URL/ACTIONS_RUNTIME_TOKEN inconsistently, and without a
cache every run recompiles the openvpn3 core from scratch. Attestations
are off because they turn the push into a multi-manifest index that
Gitea's package view renders as junk entries.
The Dockerfile's builder stage already runs the unit suite, so the workflow
deliberately has no separate test job -- a red test cannot produce an image.
After pushing, the published artifact is smoke-tested by digest: `--version`
covers a runtime stage missing a shared library, and `--check` against a
throwaway credentials file covers the config baked into the image. Both were
failure modes a green build would not have caught. The `--check` invocation is
verified locally against docker/openvpngate.conf.
`latest` follows the newest v* tag rather than the branch head; master head is
published as `master`. Registry paths are lowercased explicitly rather than
relying on metadata-action, since the same value is reused for the smoke test.
GHCR_TOKEN / GHCR_USER / GHCR_IMAGE override the built-ins so this still works
from a mirror or a Gitea/Forgejo runner, where the ambient token authenticates
to the wrong registry. On github.com none of them need to be set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Distils the things that took a debugging session to learn and cannot be
read off a single file: the two-build-tree workflow, the one-way module
dependency chain, the invariants around the single lwIP stack, and the
traps (asio error categories, self-re-arming io work blocking shutdown,
slice-driven tests being blind to that class of bug).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A userspace VPN gateway: builds an OpenVPN tunnel to a VPNGate node with
the OpenVPN 3 core, terminates it in-process with lwIP, and serves SOCKS5
(RFC 1928/1929, CONNECT and UDP ASSOCIATE) over it. No root, no tun
device, no routing table changes.
Layout follows the module boundaries in docs/ARCHITECTURE.md:
vpngate/ directory fetch + CSV parse (lines run to ~13.5 KB, so the
parser streams rather than splitting on newlines)
selector/ two-phase pick: cheap prior over the whole list, then real
TCP handshake timing of the top K
ovpn/ openvpn3 driven through TunBuilder, packets over a socketpair
netstack/ lwIP: the TCP/IP stack that makes "no root" possible
egress/ the swappable way out, and make-before-break switching
socks5/ the front door
health/ per-window scoring, and the decision to move
app/ wiring, admin HTTP, signals
docs/FEASIBILITY.md is the analysis this was built from, including the
one requirement that is not physically possible -- carrying established
TCP connections across a node switch -- and what is done instead
(zero-progress redial, UDP re-homing, grace-period drain).
Tests: 155 without the tunnel egress, 172 with it. The seam is the egress
factory; selection, scoring, history and probing all run for real.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>