Networking · Qeli 0.8.1

Routing and server push

Who creates a route, how server push combines with local include/exclude and route_file, and why client_subnet, allowed_networks and gateway solve different jobs.

Mental model

Six mechanisms for six different jobs

Start with direction: do you need to push a route to a client, allow a destination, register a network behind a client, or tunnel all device traffic?

MechanismWhereDirection and meaning
route[profile:*]The server pushes a CIDR to every user in the profile
route[user:*]The server pushes a CIDR to one user; the per-user list replaces the profile list
client_subnet[user:*]The server registers an inbound path to the network behind this client
allowed_networks[user:*]Allowlist of destinations to which the user may send packets
gateway, include, excludeclient.confDevice-local choice: full tunnel plus extra routes and exclusions
route_fileclient.conf + fileLocally adds a large CIDR list to split tunnel on Windows/macOS
qeli:// does not carry routes. It contains connection data. Push arrives after successful authentication, while gateway/include/exclude/route_file remain local.
Client mode

Split tunnel and full tunnel

The default depends on the client: Linux CLI starts in split-tunnel mode, while GUI clients start in full-tunnel mode. Split mode sends the TUN pool and server-pushed routes through Qeli; full-tunnel internet egress requires NAT on the server.

/etc/qeli/client.conf · [qeli]
# split tunnel: TUN pool, server push and include only
gateway = false
include = 10.50.0.0/16
exclude = 203.0.113.0/24

# full tunnel: route all remaining traffic through Qeli too
gateway = true
kill_switch = true
/etc/qeli/server.conf · [profile:main]
# required for full-tunnel internet egress
routing.nat.enabled = true
# WAN is detected automatically; set it explicitly when needed
routing.nat.interface = ens3
The client chooses full tunnel. Enabling NAT on the server alone does not make devices send all internet traffic through the tunnel.
Server push

Profile and per-user routes

The key is repeatable. CIDR is required and comes first; gateway and metric are optional. The usual next hop is the server's tun.address.

/etc/qeli/server.conf · [profile:main]
# sent to every user of the profile
route = 192.168.50.0/24 gateway=10.9.0.1 metric=100
route = 10.50.0.0/16
/etc/qeli/users.conf
[user:contractor]
# sent to this user only
route = 192.168.50.128/25 gateway=10.9.0.1
Precedence: if a user has at least one personal route, profile route entries are not sent to that user. If both sets are needed, list them in the user section.
SituationWhat happens
route_local=falseExplicit server-pushed CIDRs are still applied; this flag controls only the broad RFC1918 ranges
route=0.0.0.0/0 or prefix /1…/7The client core filters an overly broad push: a server cannot silently turn split tunnel into full tunnel
INI contains advertised_routes or push_routesThose INI keys do not exist; use repeatable route entries
Large list over UDPAuthOK is fragmented; above roughly 28 KB the server refuses the connection and asks you to reduce the list or use TCP
UDP compatibility: clients version 0.7.14 or newer can reassemble a fragmented AuthOK. If older clients remain, reduce the pushed list or give them a TCP profile.
Local list

Loading IP addresses and subnets with route_file

route_file attaches one or more external files to local split tunnelling. The key is repeatable; entries from every file are merged with include, canonicalised and deduplicated.

client.conf · [qeli]
# Windows
gateway = false
route_file = C:\qeli\corp-routes.txt
route_file = C:\qeli\openvpn-routes.txt

# macOS: use an absolute path
# route_file = /Users/alice/.config/qeli/routes.txt
routes.txt
# one network per line
10.20.0.0/16
192.0.2.0/24      # branch office

; write one IPv4 address as /32
203.0.113.17/32
route 172.16.9.7 255.255.0.0 vpn_gateway 10
route-ipv6 2001:db8:42::/48
RuleMeaning
FormatAccepts IPv4/IPv6 CIDR, route ADDRESS [NETMASK] [gateway] [metric] and route-ipv6 CIDR. Blank lines plus full-line and inline #/; comments are ignored
RepeatabilitySpecify route_file more than once. Paths may contain spaces; every file participates in the same canonicalisation and deduplication pass.
BoundAt most 250,000 unique routes in total after merging include and every route_file.
Address familyThe file accepts IPv4 and IPv6 CIDRs. The client installs routes only for families present in the negotiated NetworkPlan; with ipv6=required, missing IPv6 fails closed.
DirectionAdds routes into the tunnel only, like include. Use exclude in client.conf to bypass the tunnel
ReloadingAll files are read into one snapshot when NetworkPlan arrives; reconnect after editing. Reading, installation and cleanup of large lists can be cancelled with Disconnect.
Read failureAn unreadable file, malformed line or mask, or a bound violation stops the connection fail-closed; Qeli does not continue with a partial routing policy.
Successful loadThe log contains Loaded N route(s) from …
This is not server push. route_file is applied locally by Windows/macOS clients only. Rust CLI recognises it as a foreign platform key but does not read it; Android and iOS preserve it during profile round trips but do not apply it.
How to choose: one list for every client — repeatable profile route; a per-user list — route in users.conf; a large local Windows/macOS list — route_file; a portable local list — include/exclude in client.conf.
Access control

Destination ACL and a network behind a client

allowed_networks limits a user's outbound destinations. client_subnet tells the server that a CIDR is reachable through this client. The keys are not interchangeable.

/etc/qeli/users.conf
[user:branch-a]
static_ip = 10.9.0.10
# network behind branch-a: inbound iroute on the server
client_subnet = 192.168.50.0/24
# branch-a itself may access only this destination
allowed_networks = 10.9.0.0/24, 192.168.60.0/24

An empty allowed_networks means no ACL restriction, not deny all. A client_subnet must not overlap pool.cidr or networks at other sites.

Two sites

Minimal site-to-site without NAT

Each gateway gets a fixed address, registers its LAN via client_subnet, and receives a personal route to the other site's LAN.

/etc/qeli/users.conf
[user:branch-a]
static_ip = 10.9.0.10
client_subnet = 192.168.50.0/24
route = 192.168.60.0/24 gateway=10.9.0.1

[user:branch-b]
static_ip = 10.9.0.11
client_subnet = 192.168.60.0/24
route = 192.168.50.0/24 gateway=10.9.0.1
server.conf + gateway client.conf
# server, [profile:main]
routing.nat.enabled = false
routing.forward_private = true
routing.client_to_client = true

# each Linux gateway, [qeli]
forward = true
Do not forget the return path. LAN hosts must send the remote subnet through their Qeli gateway, using a static route on the main router or gateway configuration.
Beyond push

Additional routing mechanisms

These settings do not replace route or route_file: they control NAT, app selection, IPv6 or interface lifecycle on a particular client.

MechanismWhere it worksDirection and meaning
gateway_nat + lan_subnetLinuxSource NAT for the LAN behind a client, turning the Linux client into a gateway for its network
exit_nodeLinuxSource NAT for tunnel traffic leaving through this client's physical WAN; the exit node itself must remain split tunnel
apps + apps_modeWindows / macOS / AndroidSelects applications rather than networks; iOS preserves the setting but cannot apply it without an MDM NEAppRule
allow_lanAndroidKeeps the home LAN, link-local traffic and local multicast outside the VPN
dev_attach + QELI_TUNIP_FILELinux CLIDelegates creation, addressing and routing of an existing TUN to an external controller
allow_ipv6_leakClientExplicitly allows IPv6 over the physical path when a full tunnel is negotiated without IPv6; by default the missing family is blocked
Local policy: these keys are not carried in qeli:// and do not arrive through server push. Configure them in a trusted client.conf or the relevant platform UI.
DNS

DNS proxy and the address a client receives

dns.enabled starts the server proxy. dns.push_servers separately determines which address is sent to clients.

dns.push_serversdns.enabledResult
seteitherThe client receives the first address and queries it directly
emptytrueThe client receives dns.listen and uses the server proxy, cache and blocklist
emptyfalseNo DNS is pushed; the device keeps its resolvers
Common mistake: setting an external dns.push_servers and expecting requests to pass through the built-in proxy. In this case the client queries the specified DNS directly.
Compatibility

How clients apply pushed routes

All current clients apply the CIDR. Optional next-hop and metric behaviour is platform-specific, so do not use those fields as the only prioritisation mechanism.

ClientCIDRgateway/metric
Linux CLIApplies both fields
AndroidReads both fields
Windows / macOSReads them, but binds the route to TUN; logs the limitation
iOS sourceUses the CIDR but does not apply the optional fields
Verification

How to find where a route was lost

Check in order: server config, actual push, client routing table, forwarding, and NAT/firewall.

sudo qeli check-config --config /etc/qeli/server.confrejects invalid CIDRs and conflicting profiles
sudo qeli show-routes <user>shows routes for a specific user
ip route showchecks the Linux client routing table
sudo sysctl net.ipv4.ip_forwardexpected to be 1 on the gateway and server
sudo iptables-save | grep -E 'qeli-nat|TCPMSS'checks NAT and MSS clamping

Look for Pushed route applied on the client. If the push appears in the log but the OS has no route, the problem is platform application, a table conflict or permissions.

Primary sources

Complete routing documentation