# 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: