You already have a client connecting over IPv4 and reaching a private IPv4 workload behind the server. The goal is to reach a private IPv6 workload through the same tunnel without touching the outer transport. That order matters: the outer path stays IPv4, and only the inner address plan, the client route and the server-to-LAN return path change. Five checkpoints, each with something concrete to look at before moving on.

Checkpoint one: the connection that already works

Connect the client and confirm the IPv4 path before adding anything. A working tunnel gives you a baseline that separates a new IPv6 mistake from an old connectivity problem. It also tells you which server instance you are actually editing.

Note the pool, the topology and the pushed routes you already rely on. The IPv6 work extends that configuration; it does not replace it.

Checkpoint two: choose the inner prefixes

Pick a ULA /48 for the site and carve two /64s from it: one for tunnel clients, one for the LAN segment that holds the IPv6 workload. Generate the 40-bit global ID rather than copying an example prefix, and check both subnets against every network the site already routes:

Outer transport: IPv4 OpenVPN UDP
IPv4 pool:       10.90.0.0/24
IPv6 VPN pool:   fd73:93bc:b6e9:77::/64
IPv6 LAN:        fd73:93bc:b6e9:60::/64
Server LAN IPv6: fd73:93bc:b6e9:60::1

Two independent /64s keep the client pool from being carved out of the LAN segment, which matters later when you write the return route. The values above are one worked example, not a prefix to reuse.

Checkpoint three: add the helper and the route push

Read the current server configuration before appending anything. A second copy of an existing directive does not merge with the first, and the resulting error names the directive rather than IPv6. With the existing IPv4 helper in place, two lines extend it. Each one answers a different question:

server-ipv6 fd73:93bc:b6e9:77::/64
push "route-ipv6 fd73:93bc:b6e9:60::/64"

The first is a convenience helper that configures the interface and the client pool. The second names the LAN destination the client should reach. Assigning a client an IPv6 address does not tell it which networks exist behind the server, so both lines are needed.

The helper does not coexist with a static IPv6 pool or with nopool. If the deployment deliberately allocates addresses statically, keep that policy and design the IPv6 equivalent explicitly instead of deleting the policy to fit an example.

Checkpoint four: forwarding and the reply route

Turn on IPv6 forwarding on the server through the system's own configuration owner, not with a one-off runtime write you will forget. The runtime form is useful for a quick test. Persistent configuration belongs in a drop-in file that the distribution's network owner reads:

sysctl -w net.ipv6.conf.all.forwarding=1

Forwarding changes the host's IPv6 behaviour beyond routing packets. A host that accepted Router Advertisements as a client stops doing so in the same way once it forwards. A machine whose IPv6 uplink depends on RAs therefore needs an explicit plan for that uplink.

Then give the LAN a route back to the tunnel pool. The workload's IPv6 gateway is the LAN router, not the VPN server, so the route belongs on that router and points at the server's LAN address:

ip -6 route add fd73:93bc:b6e9:77::/64 via fd73:93bc:b6e9:60::1

Keep the ICMPv6 types the link needs, including path-MTU feedback. An IPv4 firewall rule does not imply an IPv6 one, and a blanket IPv6 drop breaks neighbour discovery in ways that look like a routing fault.

Checkpoint five: read the client, then test

Reconnect the client and read the addresses it actually received rather than inferring them from the server's helper address. The interface listing shows both families on the same device:

ip -6 addr show tun0
ip -6 route get fd73:93bc:b6e9:60::20

A route that selects the tunnel proves the client knows where to send the packet. It does not prove the server forwards it, that the LAN router has the return route, or that the application is permitted to answer.

Test the application itself over each family in turn, then repeat after a reconnect and after a host restart. A route or sysctl that survives only until reboot is a test, not a design.

Only publish an AAAA record once the IPv6 path answers end to end. DNS queried over IPv4 can still return an IPv6 address, so a resolvable name is not evidence that anything listens. The RFC 4193 ULA guidance covers generating the global ID that makes these prefixes yours rather than a copy.

Makomi ya ndenge moko