IPv6 and dual stack in Qeli
Outer IPv4/IPv6 carriers, inner NetworkPlan v2, ULA/GUA, NAT66 or routing, the built-in NDP proxy for on-link prefixes, DNS, MTU and safe migration.
The outer carrier and inner TUN are independent
Since 0.8.0, Qeli separates the server-connection family from packet families inside the tunnel. bind/listen/server may use IPv4, IPv6 or a hostname with A/AAAA, while tun.ip_mode, pools and the authenticated NetworkPlan define the inner IPv4/IPv6 data plane.
server = [2001:db8::10]:443.tcpdump diagnostics.tun.ip_mode = ipv4 | dual | ipv6
| Mode | Client plan | Use |
|---|---|---|
ipv4 | IPv4 lease, routes and DNS. | Compatibility or no ready IPv6 uplink. |
dual | Independent IPv4 /32 and IPv6 /128 on L3 TUN, both route and DNS sets. | Recommended normal mode. |
ipv6 | IPv6 lease and plan only. | Specialized IPv6-only network; IPv4-only services need separate NAT64. |
NetworkPlan v2 separately carries assigned address, on-link prefix and gateway for each family and applies the plan atomically.
Minimal dual-stack example
tun.ip_mode = dual
tun.name = vpn0
tun.address = 10.19.0.1
tun.ipv6_address = fd71:e1:8000:102::1
tun.mtu = 1400
tun.device_type = tun
pool.cidr = 10.19.0.0/24
pool.ipv6.cidr = fd71:e1:8000:102::/64
routing.nat.enabled = true
routing.ipv6.mode = nat66
dns.enabled = true
dns.listen = 10.19.0.1
dns.listen_ipv6 = fd71:e1:8000:102::1
dns.push_servers = 10.19.0.1, fd71:e1:8000:102::1
Use a deployment-unique RFC 4193 ULA /48 and one /64 per profile. Do not copy the sample fd71:… across deployments or use fe80::/10 as a pool. Quick Start randomizes the 40-bit Global ID.
routing.ipv6.mode
| Value | Behavior | Requirements |
|---|---|---|
nat66 | Translates the ULA source to the server's WAN IPv6. | IPv6 default route, public GUA in 2000::/3 and working ip6tables NAT. |
route | Preserves the client's IPv6 source. | A provider-routed GUA prefix and reverse route for Internet egress; own routes are enough for LAN/site-to-site. |
off | No IPv6 egress and fail-closed isolation. | Working ip6tables to guarantee DROP. |
Use reservations or static_ipv6 for routed GUA; CLI supports qeli add-client --static-ipv6 ….
NDP proxy: when it is needed and how to configure it
NDP (Neighbor Discovery Protocol) performs, among other jobs, the IPv6 equivalent of IPv4 ARP: a neighbouring router sends a Neighbor Solicitation (NS) to discover which MAC address serves a particular IPv6 address. A VPN client address lives on the TUN rather than the server WAN. For an on-link prefix, Qeli therefore sends a Neighbor Advertisement (NA) containing the server MAC and then routes the packet into the active VPN session.
routing.ipv6.ndp_proxy = off.Select the mode from the provider topology
| Provider behaviour | Signal | Qeli setting |
|---|---|---|
| Routes a separate prefix through the server WAN address | No NS for client addresses arrives on WAN; the provider panel shows a route/next hop. | routing.ipv6.mode = routerouting.ipv6.ndp_proxy = off |
| Treats the separate prefix as on-link | NS packets for individual public client addresses arrive on WAN. | routing.ipv6.mode = routerouting.ipv6.ndp_proxy = required |
| Topology is not documented | The provider gives no precise routing description. | Inspect traffic with tcpdump. auto is useful for diagnostics, but use required for a production on-link deployment. |
Determine whether the proxy is required
Do not assign the client prefix to the WAN interface. Stop the profile or server, capture on the actual IPv6 uplink, and probe a future client address from an independent external IPv6 host:
# On the Qeli server
$ sudo systemctl stop qeli-server
$ ip -6 addr show dev ens3
$ ip -6 route show table all
$ sudo tcpdump -ni ens3 'icmp6 && ip6[40] == 135'
# On an external IPv6 host
$ ping -6 2001:db8:1200:10::100Neighbor Solicitation, who has 2001:db8:1200:10::100 means that the upstream resolves the client address through NDP and this topology needs a proxy. If no NS appears, first verify the address, WAN interface, firewall/security group, and provider conditions; absence of one packet does not by itself prove an L3-routed topology.
Complete profile fragment
The 2001:db8::/32 addresses below are documentation-only. Replace them with a separate GUA prefix issued by your provider for VPN clients.
[profile:public-v6-onlink]
tun.ip_mode = ipv6
tun.name = vpn-public
tun.ipv6_address = 2001:db8:1200:10::1
tun.mtu = 1280
pool.ipv6.cidr = 2001:db8:1200:10::/64
# Preserve the public client source address; no MASQUERADE is created.
routing.ipv6.mode = route
routing.ipv6.interface = ens3
routing.ipv6.ndp_proxy = required
routing.ipv6.ndp_proxy_interface = ens3
[user:alice]
static_ipv6 = 2001:db8:1200:10::100pool.ipv6.cidr to ens3 and do not add a connected route for it through WAN. The client-prefix route must point only to the profile TUN; the upstream performs NDP on its side. For dual stack, change tun.ip_mode = ipv6 to dual and retain the existing IPv4 settings.Modes and requirements
| Mode | Behaviour |
|---|---|
off | Responder disabled. This is the default and correct choice for a normal L3-routed prefix. |
auto | Attempts to open the responder. If the interface, Ethernet link, or CAP_NET_RAW is unavailable, it logs a warning and continues without the responder. |
required | Refuses profile startup until the responder attaches to the selected interface. Recommended for a production on-link deployment. |
An empty routing.ipv6.ndp_proxy_interface reuses the effective uplink from routing.ipv6.interface or the IPv6 default route. It requires an Ethernet-compatible Linux interface and CAP_NET_RAW; the normal Qeli system service already runs with the needed privileges. NDP proxy is valid only with source-preserving route, never with nat66 or off.
Which addresses Qeli advertises
- The exact IPv6 lease of a live session from
pool.ipv6.cidr, includingstatic_ipv6. - Addresses inside a non-default IPv6
client_subnetregistered by a connected router or site-to-site client. - Only live-session addresses: disconnect, revoke, or session replacement immediately removes ownership.
/0, link-local, multicast, invalid NS, and unrelated targets are ignored. Qeli does not turn the responder into a general multicast relay.
Verify after startup
$ sudo qeli check-config --config /etc/qeli/server.conf
$ sudo systemctl restart qeli-server
$ sudo journalctl -u qeli-server -n 100 --no-pager | grep -F 'IPv6 NDP proxy'
$ ip -6 route show 2001:db8:1200:10::/64
$ sudo tcpdump -ni ens3 'icmp6 && (ip6[40] == 135 || ip6[40] == 136)'With required, the journal must report session-aware IPv6 NDP proxy active on 'ens3', and the client /64 must route through vpn-public. While Alice is offline, a new NS for her address receives no NA. Once connected, Qeli advertises its own MAC and routes external traffic into her TUN session. New responses stop after disconnect; an old entry may remain in the upstream neighbour cache until its timer expires.
| Symptom | Check |
|---|---|
required does not start the profile | Uplink name, Ethernet link type, root/CAP_NET_RAW, and the journal message. |
tcpdump shows no NS | Address and WAN interface, provider route/security group; the prefix may already be routed and require no proxy. |
| NS arrives but no NA while the client is online | The actual static_ipv6, session state, and whether the target belongs to the pool or an active client_subnet. |
| NA appears but traffic does not arrive | The /64 route to TUN, IPv6 forwarding, ip6tables/cloud firewall, and client firewall. |
auto starts without NDP | Find NDP proxy auto mode is unavailable, correct the cause, and switch to required. |
ipv6 = auto | required | off
| Policy | Result |
|---|---|
auto | Accepts IPv4, dual or IPv6. Dual may safely downgrade to IPv4 when the platform lacks the complete IPv6 contract. |
required | Requires inner IPv6 and fails closed with an old server, IPv4-only profile, MTU below 1280 or incomplete platform support. |
off | Requests IPv4 from a dual profile and rejects IPv6-only plans. |
Full tunnel blocks a missing family by default. allow_ipv6_leak=true or allow_ipv4_leak=true is an explicit escape hatch allowing that family over the physical path. Both default to false.
Configure both families as one plan
dns.listenanddns.listen_ipv6must match active gateway addresses; do not push an unreachable listener.- IPv6 requires MTU of at least 1280.
requiredrejects a smaller plan, while PMTU probing determines a safe carrier budget. route,route_file, server push and ACL accept IPv4/IPv6 CIDRs but apply only to negotiated families.- Outer DNS may return A and AAAA; roaming can move a session across families after authenticated path validation.
An existing config does not become dual-stack automatically
- Back up and upgrade the server to 0.8.1.
- Select a unique ULA /64 or obtain a provider-routed GUA prefix.
- Add
tun.ip_mode,tun.ipv6_address,pool.ipv6.cidr, IPv6 DNS androuting.ipv6.mode. - Run
qeli check-config; for NAT66 also verify default route, public source GUA and ip6tables NAT. - Keep clients on
ipv6=auto, test IPv4 and IPv6 independently, then tighten torequiredif needed.
What Qeli does not provide
- No NAT64/DNS64: an IPv6-only client cannot reach an IPv4-only service without separate NAT64 infrastructure.
- A random WAN /64 does not become a client pool: use a separate routed/on-link prefix and a correct reverse path.
- Inner IPv6 does not prove the server uplink is ready; verify address, default route, firewall and reverse route.