Qeli 0.8.2 · dual stack and routed IPv6

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.

Two layers

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.

An IPv4 carrier can carry inner IPv6, and an IPv6 carrier can carry inner IPv4. Write an IPv6 literal with brackets: server = [2001:db8::10]:443.
Did the provider issue a separate public on-link prefix? Jump to “NDP proxy: setup and checks” for the packet flow, complete configuration, and tcpdump diagnostics.
Profile mode

tun.ip_mode = ipv4 | dual | ipv6

ModeClient planUse
ipv4IPv4 lease, routes and DNS.Compatibility or no ready IPv6 uplink.
dualIndependent IPv4 /32 and IPv6 /128 on L3 TUN, both route and DNS sets.Recommended normal mode.
ipv6IPv6 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.

Base profile

Minimal dual-stack example

server.conf · [profile:main]
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.

Egress and routes

routing.ipv6.mode

ValueBehaviorRequirementsNDP proxy
offBlocks IPv6 transit between the profile TUN and other interfaces in both directions.Working ip6tables to guarantee DROP.off only
manual · 0.8.2Administrator-managed IPv6 firewall and forwarding; NDP can be enabled independently.Manual routing, DNS access and isolation. No ip6tables requirement.off, auto, required
routeConfigures forwarding while preserving 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, auto, required
nat66Configures forwarding and NAT66 through the server's IPv6 WAN.IPv6 default route, public GUA in 2000::/3 and working ip6tables NAT.off only
Do not use a WAN /64 already assigned to the uplink as the client pool. Use a separate routed or separate on-link prefix; 0.8.1 provides a session-aware NDP proxy for the latter.

Use reservations or static_ipv6 for routed GUA; CLI supports qeli add-client --static-ipv6 ….

Manual IPv6 management · 0.8.2

manual requires a 0.8.2 build that supports this mode; 0.8.1 binaries do not recognize it. The example below is a fragment of an existing INI profile with tun.ip_mode = dual or ipv6 and its IPv6 gateway/pool already configured.

When firewalld or native nftables owns routing and firewall policy, use routing.ipv6.mode = manual. Qeli still creates the tunnel, configures addresses and serves authenticated client routes and NDP. It installs no IPv6 transit, MSS, DNS INPUT/REDIRECT rules and does not enable forwarding/accept_ra. This mode needs no ip6tables; IPv4 remains independently configured.

INI · manual IPv6 with required NDP
routing.ipv6.mode = manual
routing.ipv6.interface = ens3
routing.ipv6.ndp_proxy = required
routing.ipv6.ndp_proxy_interface = ens3

The administrator provides forwarding, return routes, profile isolation, ICMPv6 and UDP/TCP access to the built-in DNS resolver. With dns.port != 53, the administrator also provides IPv6 port-53 redirection. On a mode change Qeli removes its old owned rules and releases sysctl leases; host settings must exist independently. required still refuses startup on responder failure. off continues to block IPv6 transit.

required checks that the responder binds to its interface; successful startup does not guarantee correct manual routing or firewall policy. For an ordinary routed prefix without NDP, use ndp_proxy = off.

  1. Prepare persistent forwarding, return routes, firewall, ICMPv6, RA and DNS settings.
  2. In the panel, select Config → profile → Routing → IPv6 forwarding / NAT66 → manual. Configure Upstream IPv6 NDP proxy and its interface for your topology.
  3. For file edits, run qeli check-config --config /etc/qeli/server.conf, then restart the service; in the panel, use Apply & Restart.
  4. After restart, check forwarding/RA, IPv6 access and DNS over UDP/TCP; for NDP, verify an uplink response for an active client.

Switching route ↔ manual in the form preserves NDP. Selecting off or nat66 disables NDP and clears its interface. In INI, set routing.ipv6.ndp_proxy = off yourself: incompatible combinations are rejected.

The commands below use qeli.service, installed by the standard installer/DEB. If you created a differently named service, replace qeli with your unit name.

Public IPv6 without NAT66 · 0.8.1

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.

This is not NAT66. The client's public source IPv6 is preserved. NDP proxy is needed only when the provider treats a separate client prefix as directly connected to its L2 segment. If the provider installs a normal L3 route through the server WAN address, keep routing.ipv6.ndp_proxy = off.

Select the mode from the provider topology

Provider behaviourSignalQeli setting
Routes a separate prefix through the server WAN addressNo NS for client addresses arrives on WAN; the provider panel shows a route/next hop.routing.ipv6.mode = route
routing.ipv6.ndp_proxy = off
Treats the separate prefix as on-linkNS packets for individual public client addresses arrive on WAN.routing.ipv6.mode = route
routing.ipv6.ndp_proxy = required
Topology is not documentedThe 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:

Qeli server and external IPv6 host
# On the Qeli server
$ sudo systemctl stop qeli
$ 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::100

Neighbor 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.

server.conf · [profile:public-v6-onlink]
[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

Pin the address to an existing user

Separately open the users.conf selected by [auth] users_file and add or update static_ipv6 in the existing [user:alice] section. Preserve password_hash, enabled, groups and limits; do not create a duplicate section with the same name.

users.conf · [user:alice]
[user:alice]
# Keep the existing user settings.
static_ipv6 = 2001:db8:1200:10::100

Inline [user:*] sections in server.conf are supported, but a matching entry in users.conf takes precedence as a whole. Changing only the inline address therefore does not update an existing account from the users file.

Do not assign pool.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

ModeBehaviour
offResponder disabled. This is the default and correct choice for a normal L3-routed prefix.
autoAttempts to open the responder. If the interface, Ethernet link, or CAP_NET_RAW is unavailable, it logs a warning and continues without the responder.
requiredRefuses 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 supports source-preserving route and, in 0.8.2, manual. It remains incompatible with nat66 and off.

Which addresses Qeli advertises

  • The exact IPv6 lease of a live session from pool.ipv6.cidr, including static_ipv6.
  • Addresses inside a non-default IPv6 client_subnet registered 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

configuration, journal, route and NDP
$ sudo qeli check-config --config /etc/qeli/server.conf
$ sudo systemctl restart qeli
$ sudo journalctl -u qeli -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.

SymptomCheck
required does not start the profileUplink name, Ethernet link type, root/CAP_NET_RAW, and the journal message.
tcpdump shows no NSAddress 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 onlineThe actual static_ipv6, session state, and whether the target belongs to the pool or an active client_subnet.
NA appears but traffic does not arriveThe /64 route to TUN, IPv6 forwarding, ip6tables/cloud firewall, and client firewall.
auto starts without NDPFind NDP proxy auto mode is unavailable, correct the cause, and switch to required.
Client policy

ipv6 = auto | required | off

PolicyResult
autoAccepts IPv4, dual or IPv6. Dual may safely downgrade to IPv4 when the platform lacks the complete IPv6 contract.
requiredRequires inner IPv6 and fails closed with an old server, IPv4-only profile, MTU below 1280 or incomplete platform support.
offRequests 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.

DNS and MTU

Configure both families as one plan

  • dns.listen and dns.listen_ipv6 must match active gateway addresses; do not push an unreachable listener.
  • IPv6 requires MTU of at least 1280. required rejects 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.
Migration

An existing config does not become dual-stack automatically

  1. Back up and upgrade the server to 0.8.2.
  2. Select a unique ULA /64 or obtain a provider-routed GUA prefix.
  3. Add tun.ip_mode, tun.ipv6_address, pool.ipv6.cidr, IPv6 DNS and routing.ipv6.mode.
  4. Run qeli check-config; for NAT66 also verify default route, public source GUA and ip6tables NAT.
  5. Keep clients on ipv6=auto, test IPv4 and IPv6 independently, then tighten to required if needed.
The installer keeps active dual stack only with a verified IPv6 uplink and NAT; otherwise it creates a safe active IPv4-only profile and leaves IPv6 values ready for manual enablement.
Boundaries

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.
Primary sources

Complete 0.8.2 IPv6 documentation