Skip to main content

Home / lab access

Reach your home LAN, or a throwaway VM, from the outside. OpenVPN over UDP, split tunnel, a self-generated local CA - nothing else. This is the home profile and the beginner path; it builds on the README quick start.

1. Run the server​

Same as the quick start, with CULVERT_PROFILE=home added. Set CULVERT_SERVER_CN to a name clients can reach from outside - your DDNS hostname, or the VM's public IP.

docker run -d \
--cap-add=NET_ADMIN \
--device=/dev/net/tun \
-p 1194:1194/udp \
-p 127.0.0.1:9090:9090/tcp \
-e CULVERT_PROFILE=home \
-e CULVERT_SERVER_CN=vpn.yourdomain.example \
-v culvert-pki:/etc/vpn/pki \
-v culvert-clients:/etc/vpn/clients \
ghcr.io/hyperi-io/culvert:latest

The shipped profiles live at /etc/vpn/profiles/ inside the image, so the bare name home resolves. Explicit CULVERT_* env vars always override profile values.

Or use the repo's compose file, which publishes the same default set. It has no default image, so name the one to run. Pin it by digest, which docker buildx imagetools inspect ghcr.io/hyperi-io/culvert:<version> prints:

CULVERT_IMAGE=ghcr.io/hyperi-io/culvert:<version>@sha256:<digest> docker compose up -d

2. Open UDP 1194 to the host​

The server is only reachable once the port reaches it:

  • Home: forward UDP 1194 on your router to the host running the container.
  • Cloud VM: allow inbound UDP 1194 in the security group / firewall.

The observability port (9090) is published on loopback only - it is not part of what you expose.

3. Choose what the tunnel reaches​

The home profile is split tunnel: only the VPN subnet and the routes you push go down the tunnel, the rest of the client's traffic stays local. The profile pushes 192.168.1.0/24 as a starting point - set it to your actual LAN:

-e CULVERT_PUSH_ROUTES=192.168.50.0/24

Want everything routed through home instead (for example to use its public IP)? Switch to full tunnel:

-e CULVERT_FULL_TUNNEL=true

Addressing background - why the tunnel itself defaults to 10.8.0.0/22 and when a home LAN could still clash - is in addressing.md.

4. Generate a client​

docker exec -it <container> generate-client --name laptop

This writes a set of .ovpn files to the clients volume. For the home case the one you want is laptop-udp-split.ovpn (UDP, split tunnel); laptop-udp-full.ovpn is the full-tunnel variant. The TCP and HTTPS variants are for later cases and need their listeners enabled server side.

To remove access again:

docker exec -it <container> revoke-client laptop

5. Import and connect​

Copy laptop-udp-split.ovpn to the client and import it. Platform steps (OpenVPN Connect, Tunnelblick, the Linux CLI, always-on setups) are in vpn-client-setup.md.

Fetching the configs over HTTP​

If copying files off the volume is a nuisance, turn on the download server:

-e CULVERT_CLIENT_DOWNLOAD_ENABLED=true
-e CULVERT_CLIENT_DOWNLOAD_TOKEN=<a long random string>

It lists and serves every .ovpn, WireGuard .conf and per-client .zip in the clients volume on port 8443, and each request needs the header Authorization: Bearer <token>. It binds to loopback unless CULVERT_CLIENT_DOWNLOAD_BIND says otherwise, and it speaks plain HTTP unless CULVERT_CLIENT_DOWNLOAD_TLS_CERT and _TLS_KEY are set, so put a TLS-terminating proxy in front before exposing it. With CULVERT_PROTOCOL=both the WireGuard configs and laptop.zip come from here too, with no docker cp.

See also​