feat(customvpn): generic VPN_TYPE=custom running a user-provided VPN client binary - #3434
feat(customvpn): generic VPN_TYPE=custom running a user-provided VPN client binary#3434poka-IT wants to merge 5 commits into
Conversation
|
(didn't review code yet) a few ideas crossing my mind :
|
|
Checked each point against the code. Reuse for OpenVPN: agreed. Trying it with openvpn: that doubles as the validation of the point above. Port forwarding: you are right, nothing in it is bound to the VPN protocol. PIA needs the gateway, the server name and the credentials, ProtonVPN needs the gateway for NAT-PMP, PrivateVPN needs the assigned IP, and all of those come from One constraint that will need documenting: PMTUD: agreed, One thing I noticed while checking that: the interface polling signals ready as soon as the interface carries an address, which can be before the binary installs its default route, and both PMTUD ( |
|
Pushed the three things I said I would, description updated.
The openvpn reuse is the one thing I have not touched, since I would rather know whether you want it in this PR or after it lands. |
Type
Please tick which one the following applies to your pull request:
Description
This adds a generic
VPN_TYPE=customwhich runs a user-provided VPN client binary inside the gluetun container, while gluetun keeps providing everything around it: the killswitch firewall, DNS over TLS, the HTTP and Shadowsocks proxies, the healthcheck and the public IP lookup.Why
Recurring requests ask gluetun to embed one more VPN client: Tailscale (#1854), OpenConnect/AnyConnect (#3139), Cloudflare WARP (#1738), and others. Each of those means building, shipping and maintaining another client and its quirks forever. A generic exec-based type turns all of them into a user-side concern: mount the binary into the container, point
CUSTOM_VPN_BINARYat it, and gluetun supervises it the same way it supervises OpenVPN today, without carrying any client-specific code.How it works
internal/customvpnrunner cloned from the OpenVPN exec pattern:exec.CommandContextwithSetpgid, stdout and stderr lines streamed to the logger, and process death forwarded to the VPN loop, which restarts it with the usual backoff.CUSTOM_VPN_READY_LINEregular expression, or, when that variable is empty, when the tunnel network interface exists with at least one address assigned, checked every 200ms.CUSTOM_VPN_ENDPOINT_IP,CUSTOM_VPN_ENDPOINT_PORTandCUSTOM_VPN_ENDPOINT_PROTOCOL(defaultudp), like the connection of the other VPN types.CUSTOM_VPN_INTERFACE, defaulttun0) and install the in-container default route through it. Gluetun creates/dev/net/tunfor it but performs no other network setup for this type. This is documented on the settings doc strings.VPN_SERVICE_PROVIDER=customis valid for this type, mirroring AmneziaWG.All new environment variables are inert unless
VPN_TYPE=custom:CUSTOM_VPN_BINARY(required),CUSTOM_VPN_ARGS(split following shell word splitting rules, reusing the existing splitter now exported ascommand.Split),CUSTOM_VPN_INTERFACE,CUSTOM_VPN_READY_LINE,CUSTOM_VPN_ENDPOINT_IP(required),CUSTOM_VPN_ENDPOINT_PORT(required) andCUSTOM_VPN_ENDPOINT_PROTOCOL. No existing behavior changes.Port forwarding and path MTU discovery
Both were left out of the first push and both are in, after @qdm12's review said they should not be:
routing.VPNLocalGatewayIP/AssignedIPon the tunnel interface. What blocked it was this PR's own wiring, which built amodels.ConnectionwithPortForwardfalse and an emptyServerName; it now mirrorscustom.getWireguardConnection(PIA panics on an empty server name).PORT_FORWARD_PROVIDERselects the implementation exactly as it does for the custom provider.MaxTheoreticalVPNMTUis the ceiling of the binary search, not a computed MTU, so it does not have to match the tunnel's real overhead. The custom case now subtracts no VPN header at all, leaving the ceiling at physical link minus IP header minus UDP or TCP header, and the search probes for the truth. The floor is alreadyMinIPv4MTU68 andMinIPv6MTU1280.One constraint that needs documenting rather than fixing:
routing.VPNLocalGatewayIPrecognises two route shapes, a default route with a gateway (OpenVPN) and the local-table single-address heuristic (WireGuard). A binary installingdefault dev tun0with no gateway resolves to neither, so it gets no port forwarding.Readiness waits for the route
When
CUSTOM_VPN_READY_LINEis empty, readiness used to fire as soon as the interface carried an address. Both of the things the tunnel-up path does next need the route as well: path MTU discovery lists the interface's routes to set their MSS, and port forwarding reads the VPN gateway out of them. A binary that adds its address and installs its route a moment later was declared ready in between. The poll now requires a route through the interface too.Changes
internal/constants/vpn: newcustomtype constant.internal/configuration/settings/customvpn.go: settings with the usual gosettings read, validate, defaults, copy, override and toLinesNode, wired intosettings/vpn.go,settings/provider.goandsettings/serverselection.go.internal/customvpn: the runner (start, line streaming, interface polling).internal/vpn: dispatch case,setupCustomVPNbuilding the firewall connection, the interface name for the down command, and IPv6 detection reusing the OpenVPN link inspection (extracted into a shared helper).internal/command:Splitexported to reuse the existing shell word splitting forCUSTOM_VPN_ARGS.Dockerfile: environment defaults block.go build ./...,go test ./...andgolangci-lint runpass on the touched packages. This has not yet been validated end to end inside a container against a real third-party VPN binary; @qdm12 offered to try it with openvpn, which is also the proof that the runner can be shared withinternal/openvpn(CUSTOM_VPN_BINARY=/usr/sbin/openvpn2.6,CUSTOM_VPN_ARGS=--config /gluetun/target.ovpn,CUSTOM_VPN_READY_LINE=Initialization Sequence Completed).Still open from the review: moving
internal/openvpnonto this runner. The two differ in three places only (argv, theprocessLogLinelevel and filtering pass, and ready signalled on everyInitialization Sequence Completedrather than once). Making those injectable turns OpenVPN into the generic runner plus its own hooks. Waiting on whether that belongs in this PR or a follow-up.This pull request was authored by an AI coding agent under human direction and review, hence the ticked checkbox above.
Issue (optional)
Would resolve the recurring "add VPN client X" class of requests, notably #1854 (Tailscale), #3139 (OpenConnect/AnyConnect) and #1738 (Cloudflare WARP), without gluetun carrying any of their code.
Gluetun wiki associated pull request (optional)
None yet. Happy to write a wiki page documenting
VPN_TYPE=customif this approach is accepted.