MASQUE / Connect-IP
Uses Cloudflare's WARP endpoints over HTTP/3 (QUIC) with Connect-IP (RFC 9484) for the tunnel's addresses — no WireGuard required.
usque · MASQUE / Connect-IP client
usque is a small Rust client for Cloudflare WARP built on MASQUE / Connect-IP
(HTTP/3). This project packages it for OpenWrt 25.12 with a LuCI app,
a procd service and a signed apk feed. It connects in TUN mode and — unlike the
first release — leaves your routing alone until you opt in: each connection
decides whether IPv4 traffic should use the tunnel, and existing IPv6 routing is respected.
Stable release 0.2.1-r1 (usque) / 1.1.1-r1 (LuCI app): the v2 feature matrix — opt-in routing, IPv6, multiple connections, custom endpoints and LAN IPv6/NAT66 — is verified on a live OpenWrt 25.12.6 router.
Cloudflare WARP is Cloudflare's consumer VPN-ish service. usque speaks its MASQUE
transport directly and bridges it to a Linux TUN interface. This port wires the client into
OpenWrt's service manager, firewall and web UI — and separates two things that used to be
bundled together: connecting the tunnel and routing traffic
into it.
Safe by default: no IPv4 route takeover. Installing and starting usque
creates the tunnel but leaves your IPv4 routing, ip rules, DNS and firewall
forwarding alone. IPv4 traffic uses the tunnel only when you enable Default route for
a connection or add your own routes. This is a deliberate change from v0.1, where every
connection installed a default route immediately.
The one default routing change is IPv6: with
ipv6_route=auto (default) a tunnel IPv6 default route is installed only when the
router has no native IPv6 default route — that is how IPv4-only/CGNAT WANs get IPv6.
Existing IPv6 routing is never replaced; set ipv6_route='off' (or
ipv6='0') if you want no IPv6 routing changes at all.
Uses Cloudflare's WARP endpoints over HTTP/3 (QUIC) with Connect-IP (RFC 9484) for the tunnel's addresses — no WireGuard required.
The LuCI app generates a WARP device identity per connection: accept the terms, click
once, and a config file such as /etc/usque/warp0.json is created.
Each connection has its own Default route switch, off by default. Enable it for a
classic full tunnel, or leave it off and route selected prefixes yourself. IPv6 is handled
separately by ipv6_route.
/etc/config/usque holds one config usque section per tunnel, each
with its own interface, config file, metrics and routing switches.
The tunnel gets IPv6 by default; ipv6_route=auto adds an IPv6 default route
only when the router does not have a native one. A separate LAN option can hand a ULA prefix
to your devices via NAT66.
mode=cloudflare registers a WARP device for you. mode=custom
imports a JSON config for another compatible MASQUE/Connect-IP endpoint.
Cross-compiled for musl with LTO and size optimisations, stripped and packaged as an apk: 1.40 MB download, about 2.9 MB installed. The LuCI app adds a 21 KB package (~55 KB installed).
SOCKS5, HTTP proxy, port-forwarding and the HTTP/2 fallback transport were removed to keep the binary small. The router routes packets; nothing else to configure.
The tunnel interface exists and is up, but no IPv4 default route, IPv4
policy rule or DNS change is installed — nothing is IPv4-routed through it. Exception: on a
WAN without native IPv6 the tunnel may already provide an IPv6 default route
(ipv6_route=auto).
warp0 TUN default route, chosen metricThe hook also installs an endpoint bypass route via the WAN and a high-priority policy rule; both are removed when the tunnel stops.
| Requirement | Details |
|---|---|
| OpenWrt 25.12.x | apk-based (apk-tools 3) with fw4. The packages are built
against the 25.12.6 SDK; install on 25.12.x only. |
| Architecture | arm_cortex-a7_neon-vfpv4 (target ipq40xx/generic). This is
the only architecture in the feed right now — apk will refuse to install it
on other targets. |
| TUN support | The kmod-tun package (installed automatically as a dependency) and a
kernel built with TUN support. Standard OpenWrt images have both. |
| Free space | About 3 MB free in /overlay: the package build
reports 2,950 KiB installed for usque +
luci-app-usque. Check with df -h /overlay first — on a
3.3 MB-overlay router this is tight but fits. |
| Network | Outbound UDP/443 to the WARP endpoints (MASQUE over QUIC/HTTP 3). This build is QUIC-only: it has no TCP/HTTP 2 fallback, so if your ISP or upstream network blocks UDP/443 the tunnel cannot connect. In that case use the Go implementation of usque, which supports HTTP/2, instead. |
| LuCI | luci-base and a web browser for the guided setup. The daemon and the
CLI work without LuCI if you prefer SSH. |
| LAN IPv6 (optional) | Only if you enable lan_ipv6: the default OpenWrt image's
odhcpd (RA/DHCPv6) and firewall/NAT66 support are used. Hands out a
local ULA prefix to LAN devices and NATs it through the tunnel — not a globally
routed prefix. Requires an enabled connection with IPv6 on and
ipv6_route=auto or always. |
| Shell access | SSH or LuCI terminal as root for the install commands. |
| Correct clock | TLS needs a roughly correct system time. Routers usually sync via NTP after boot;
check date if the tunnel refuses to start. |
| Measured footprint | Static binary: 2,905,040 B (2.77 MiB), downloaded as a 1.40 MB apk. On the test router the daemon used ~6.7 MB resident memory while connected and roughly 1 % of one CPU core when idle. The tunnel runs in user space, so throughput is bounded by the router's CPU. |
Three quick checks tell you whether the packages will install and run:
# release + architecture (expect 25.12.x / arm_cortex-a7_neon-vfpv4)
grep -E '^(DISTRIB_RELEASE|DISTRIB_ARCH)=' /etc/openwrt_release
# free space in the overlay (want ~3 MB or more)
df -h /overlay
# TUN support (kmod-tun is installed as a dependency, but check the kernel module)
lsmod | grep tun
# optional: existing IPv6 default route (ipv6_route=auto leaves it alone)
ip -6 route show default
If lsmod prints nothing, the module is not loaded yet —
installing the package pulls in kmod-tun. If your image has no TUN support at all,
the tunnel cannot start; use a standard OpenWrt image.
Run these on the router over SSH. The feed is a signed apk repository served from GitHub Pages. Installing the packages does not start a tunnel and does not change any routing — you configure and opt in afterwards.
Back up first. Once you enable routing for a connection, the tunnel adds
routes and firewall state while it is up. A quick backup of /etc/config/network,
/etc/config/firewall, /etc/config/usque and any
/etc/usque/*.json identity is always worthwhile.
Trust the feed key, add the repository, refresh the index and install the LuCI app
(which pulls in the usque daemon and kmod-tun):
wget -O /etc/apk/keys/usque.pem https://fmu.github.io/openwrt-usque/feed/usque.pem
echo "https://fmu.github.io/openwrt-usque/feed/packages.adb" >> /etc/apk/repositories.d/customfeeds.list
apk update
apk add luci-app-usque
apk verifies every repository index against the keys installed in
/etc/apk/keys/. In apk v3 the key is matched by its public-key identity, so
the filename is not significant; we use usque.pem so it stays recognisable
and matches the commands on this page.
wget -O /etc/apk/keys/usque.pem https://fmu.github.io/openwrt-usque/feed/usque.pem
If wget is missing or fails with a TLS error, use
uclient-fetch -O /etc/apk/keys/usque.pem <same URL> instead.
OpenWrt's apk keeps extra repositories in /etc/apk/repositories.d/*.list.
Append the feed's index URL:
echo "https://fmu.github.io/openwrt-usque/feed/packages.adb" >> /etc/apk/repositories.d/customfeeds.list
Running this twice adds the line twice — harmless, but remove
duplicates if you want a tidy file. The URL points directly at the
packages.adb index (ndx repository type in apk v3); package files
are then fetched from the same /feed/ directory, which is why the feed is
flat.
apk update
apk add luci-app-usque
The luci-app-usque package depends on usque and
kmod-tun, so one command installs the daemon, the init script, the UCI
configuration and the LuCI view. For a CLI-only setup use apk add usque.
Already installed? apk add luci-app-usque
only installs missing packages. With apk-tools 3 it does not upgrade an existing
installation — apk just reports the package as “upgradable from …” and keeps the old
version. Use the upgrade steps below instead.
Check what you got — the two packages are versioned independently:
apk info -v usque # usque-0.2.1-r1
apk info -v luci-app-usque # luci-app-usque-1.1.1-r1
sha256sum /usr/bin/usque
# 413515df54fe2376dd2948f31ed548477604d6bb351f4909e70b8a4658ae5c9a
Go to Services → usque and continue with
First run. The direct URL is
http://192.168.1.1/cgi-bin/luci/admin/services/usque (adjust the router IP).
The feed index (packages.adb) is signed with an ECDSA P-256 key. The matching
public key is what you downloaded to /etc/apk/keys/usque.pem and is also committed
in the repository as keys/usque.pem.
Verify the download if you want to be sure:
sha256sum /etc/apk/keys/usque.pem
The SHA-256 of the published key file (as served at
https://fmu.github.io/openwrt-usque/feed/usque.pem and committed as
keys/usque.pem in the repository) is:
f47c63b55e2b28f49d2ff1f751ea6401043cb30821620ad2ac9ad1c7561abe4f
If the two hashes match, the key you installed is the one the feed is signed with. If the
feed is ever re-keyed, this page is updated with the new fingerprint. You can also verify an
individual package file that you downloaded yourself with
apk verify <file.apk>.
If the key is missing or does not match the index signature, apk refuses with an
UNTRUSTED signature error. Fix the setup rather than bypassing it — but if you are
stuck (for example, the key download was intercepted or the feed was rebuilt), you can install
without verification as a last resort:
apk add --allow-untrusted luci-app-usque
--allow-untrusted disables signature checking for that
transaction. Only use it with artifacts you obtained over a trusted connection, and remove the
repository afterwards if you cannot restore proper verification.
To stop using the feed without touching the installed packages:
sed -i '\#fmu.github.io/openwrt-usque#d' /etc/apk/repositories.d/customfeeds.list
rm -f /etc/apk/keys/usque.pem
apk update
Then uninstall the packages if you no longer need them.
apk-tools 3 does not upgrade installed packages through apk add: with an older
version already installed, apk update && apk add luci-app-usque leaves it in
place and only mentions that it is “upgradable from …”. Upgrade in one of these ways:
# 1) upgrade just the two usque packages (recommended)
apk update
apk upgrade usque luci-app-usque
# 2) or upgrade every installed package
apk update
apk upgrade
# 3) or pin exact versions from the feed
apk policy usque luci-app-usque # show versions available in the feed
apk add usque=<version> luci-app-usque=<version>
# e.g. apk add usque=0.2.1-r1 luci-app-usque=1.1.1-r1
The current feed provides usque 0.2.1-r1 and
luci-app-usque 1.1.1-r1. A plain apk add luci-app-usque will only
install it on a clean system; use one of the upgrade commands above on an existing
install.
apk upgrade without package names upgrades all
installed packages — convenient, but it can also update unrelated packages (and pull a new kernel
on some systems). Naming the packages (apk upgrade usque luci-app-usque) or pinning
versions with apk add <pkg>=<version> keeps the change targeted. Use
apk policy to see the versions in the feed and apk info -v to check
what is installed.
Coming from v0.1.x? The automatic route takeover is gone: after upgrading, check the Default route switch of each connection — it defaults to off — and enable it if you want the full-tunnel behaviour you had before. See Routing.
The LuCI app lists your connections, creates WARP identities, starts the service and shows live status. Routing stays off until you enable it.
In the LuCI menu under Services, pick usque (Cloudflare
WARP). The page shows a Connections table — initially one
connection named warp0 — with its enabled checkbox, interface, mode, IPv4/IPv6
routing status and per-connection actions (Start / Stop / Restart / Test / Logs / Remove).
Below it are the selected connection's settings, the WARP account / configuration file
section, Global settings and diagnostics. Each connection is an independent tunnel
with its own interface and config file.
For the default connection, the Account / configuration file section asks you to create an identity. Enter an optional device name (it shows up in your Cloudflare WARP device list), tick the box accepting Cloudflare's terms, and click Generate WARP configuration.
Under the hood this runs, for that connection's config file:
usque --config /etc/usque/warp0.json register --accept-tos --force -n "my-openwrt"
The result is /etc/usque/warp0.json: an anonymous WARP device identity
containing a private key and an access token. It is created with owner-only permissions —
treat it like a password and never publish it. Ticking the box is what accepts Cloudflare's
WARP terms; if you skip the UI and just start the service with
auto_register=1, the init script registers a device for you and accepts the
terms implicitly.
Registration needs working internet. It happens over HTTPS to Cloudflare through your normal WAN before the tunnel exists, so the router must already be online (and its clock roughly correct) when you press the button.
Tick Enabled for the connection (stored as its enabled
option), click Save & Apply, then press the connection's
Start action. The state should switch to running within a few
seconds, and the connection's interface (warp0) appears with its tunnel
address.
The init script only starts connections whose enabled
option is set: pressing Start on a disabled connection tells you to enable it and Save &
Apply first. The same applies to /etc/init.d/usque start. If no connection is
enabled at all, Save & Apply stops and disables the service.
With the service running, the connection shows its state, addresses and the last connection error (if any). The app's connection test runs a probe from the router:
wget -qO- https://www.cloudflare.com/cdn-cgi/trace | grep -E '^(ip|loc|colo|warp)='
With IPv4 routing still off, this reports warp=off — that is
expected: the tunnel is up but no IPv4 traffic is routed into it yet. (On a WAN
without native IPv6 the tunnel may already provide IPv6, because of
ipv6_route=auto; see Routing.) The connection table
labels this state IPv4: not routed; IPv4 route pending means the option was
saved but the connection has not been restarted yet.
Open the connection's settings. The routing switches are off/unset by default:
auto): install an IPv6 default route only if
the router has no native/WAN IPv6 default route yet. always installs it
regardless; off never does.Save & Apply (which restarts the connection), then re-run the check above. With
Default route on it should now report warp=on and a Cloudflare egress
IP. Details and manual alternatives are in Routing.
Registration is per connection and per device. Each generated identity counts as one device in your Cloudflare WARP account. Keep the JSON file if you want to reinstall or move the router without registering again.
usque has two independent layers: establishing the tunnel (always) and sending traffic into it (opt-in). This section describes exactly what each switch changes on the network, so there are no surprises.
Defaults change no IPv4 routing. A fresh install installs no IPv4 default
route, no ip rule for IPv4 and no DNS change. The tunnel interface is created, the
firewall zone and forwardings are prepared and the daemon waits — IPv4 keeps routing exactly
as before. The only default routing change is IPv6: when the router has no native IPv6 default
route, ipv6_route=auto installs a tunnel IPv6 default route (plus its IPv6 policy
rule) so IPv6 works on IPv4-only WANs. Stop or disable removes everything usque added.
Verified on the live test router (usque 0.2.1-r1 /
luci-app-usque 1.1.1-r1): with the defaults, IPv4 egress stayed
warp=off (no IPv4 default route touched) while IPv6 egress through the tunnel was
warp=on, on an IPv4-only uplink.
| Setting | Default | What it changes while connected |
|---|---|---|
default_route (per connection) |
0 (off) |
When on: installs ip route replace default dev <iface> metric <metric>
for IPv4. The existing WAN default route is kept, so a failing tunnel
falls back to the WAN instead of taking the router offline. Removed when the connection
stops. |
metric (per connection) |
0 |
Metric used for the tunnel default route(s). Lower wins; with two connections routing by default, give them different metrics to control priority. |
ipv6 (per connection) |
1 (on) |
Gives the tunnel its IPv6 address (with 0 the daemon runs with
--no-tunnel-ipv6 and IPv6 routing is skipped for this connection). This is
the tunnel's IPv6; reaching the endpoint over IPv6 is the separate
endpoint_ipv6 option. |
endpoint_ipv6 (per connection) |
0 (off) |
Connect to the MASQUE endpoint over IPv6 (--ipv6) — only for native-v6 or
v6-only networks. Not exposed in the LuCI form; set it with uci. |
ipv6_route (per connection) |
auto |
auto: install an IPv6 default route via the tunnel only if the router
has no native/WAN IPv6 default route (so IPv4-only/CGNAT WANs get
IPv6). A default route installed by another usque connection does not count as native.
always: install it regardless — this can override a native IPv6 uplink.
off: never install one. |
lan_ipv6 (global) |
0 (off) |
Gives LAN clients IPv6 through the tunnel (ULA + router advertisements + NAT66), so
they work on an IPv4-only WAN. It only takes effect when a connection with IPv6 enabled
provides the IPv6 default route (ipv6_route=auto or always).
The helper fills in missing settings only (network.globals.ula_prefix,
LAN ip6assign, dhcp.lan.ra) and never replaces existing
configuration. Local ULA only — devices are not globally reachable. Off by default; see
LAN IPv6. |
lan_ipv6_prefix (global) |
empty | ULA prefix to use (/48 or /64) when lan_ipv6
is on. Empty = derive one from the LAN MAC address; set it if you already use a ULA
range. |
| Firewall zone | on service start | When the service starts, a dedicated usque zone is created/updated for
all enabled tunnel interfaces (input REJECT, output ACCEPT, forward
REJECT) with masquerading (IPv4 + IPv6) and MSS/MTU fixing, plus forwarding into it
from every internal zone that forwards to wan (always lan, and
e.g. guest). Preparing the zone does not redirect traffic; the
wan zone is never modified. The zone stays configured when the service is
stopped and is removed on uninstall. |
| Endpoint bypass | when routing is on | A host route to the endpoint via the current WAN for the address family whose default route is being installed, so tunnel packets can reach the endpoint. Only needed — and only installed — when usque actually installs a route for that family. |
| Policy rule | when routing is on | ip rule priority 1000 from all lookup main, per address family whose
default route is installed, so mwan3-style fwmark rules cannot divert traffic away from
the main table. With defaults no rule is installed and mwan3 is untouched. Ownership is
ref-counted across connections: the rule is removed when the last routing connection
stops (and cleaned up from kernel state if the run-state directory was lost). A
priority-1000 rule that already existed before usque started is left in place. |
| DNS | follows routing | No DNS setting is changed. LAN clients keep using the router's dnsmasq; its upstream queries follow whatever default route you chose. With defaults, DNS behaves exactly as before; with Default route on, resolution goes through the tunnel. |
| Stop / disable | — | Removes the tunnel route, endpoint bypass and policy rules for that connection. The WAN default route was saved once, shared by all routing connections, and is restored when the last of them stops. Your WAN routing and LAN access to the router keep working throughout. |
Turn on Default route in the connection's settings (or set
default_route='1' in UCI) and save. That is the classic full-tunnel mode:
forwarded LAN traffic and router-originated traffic use the tunnel, with the WAN kept as
fallback.
uci set usque.warp0.default_route='1'
uci commit usque
/etc/init.d/usque restart
The UCI example assumes the default connection is called
warp0; adjust the section name if you renamed it.
With Default route off, the tunnel device is a normal interface — add your own routes for the destinations you want tunneled. usque does not manage these, so re-add them after a restart (or script them in a hotplug file):
ip route add 203.0.113.0/24 dev warp0
ip route add 198.51.100.0/24 dev warp0
ip route show | grep warp0
This also makes the tunnel interface usable by other routing software (mwan3, VPN policy routing, custom nftables marks) without usque touching the main table.
For per-client or per-protocol tunnelling, combine the tunnel device with your own
ip rule/nftables marks (or mwan3). Keep in mind that if you later enable
Default route, usque installs the priority-1000 rule that makes the main table
authoritative while the tunnel is up — stop the service if you need your own policy rules to
win.
IPv6 needs no route to connect, but traffic needs one to be routed. With
ipv6_route=auto (default) a tunnel IPv6 default route is installed only when
the router has none. Use always to force it, or off to leave IPv6
routing untouched. To give LAN devices IPv6 on an IPv4-only WAN, see
LAN IPv6.
On an IPv4-only WAN (common with CGNAT, tethering or DS-Lite), LAN devices have no IPv6. With
lan_ipv6=1 (global setting) usque gives the LAN a unique local address
prefix, announces it via router advertisements and NAT66s it through the tunnel, so
devices get working IPv6 to the internet. It only takes effect when a connection with IPv6
enabled provides the IPv6 default route (ipv6_route=auto or
always).
Verified end-to-end. On the live test router (usque 0.2.1-r1 /
luci-app-usque 1.1.1-r1) a Mac on the LAN received a ULA address from the
announced prefix (fd12:…) and curl -6 through the router reached
Cloudflare WARP (ip=<WARP_EGRESS_IP>, warp=on,
colo=VIE); the revert was clean.
| Aspect | Details |
|---|---|
| Requires | odhcpd (default OpenWrt image) and an enabled connection with IPv6 on and ipv6_route=auto or always, so the tunnel provides the IPv6 default route. The prefix is local/ULA — devices are not globally reachable. |
| What it sets | Only missing settings are filled in: network.globals.ula_prefix (derived from the LAN MAC unless lan_ipv6_prefix is set), an IPv6 prefix on the LAN (ip6assign=60 if neither ip6assign nor ip6addr exists), and the RA/DHCPv6 server (dhcp.lan.ra=server, dhcpv6=server). Existing configuration is never replaced. |
| NAT66 | The usque firewall zone already has masq6=1, so LAN IPv6 is NATed through the tunnel. Inbound IPv6 connections do not work. |
| Disable | Set lan_ipv6='0' and restart the service. This is intentionally non-destructive: the generated network/dhcp settings stay in place. Remove network.globals.ula_prefix, network.lan.ip6assign / ip6addr and dhcp.lan.ra manually to fully revert. |
By default, usque does not install any policy rule, so mwan3 keeps working normally. Only when
Default route is enabled — or an IPv6 route is installed — does the hook add
ip rule priority 1000 from all lookup main (and its IPv6 counterpart), because
otherwise mwan3's fwmark rules can silently send traffic to the WAN while the tunnel looks
healthy. The rule is removed when the tunnel stops. If you need mwan3's per-WAN policies while
the tunnel is up, stop the service or leave Default route off and route only selected
prefixes yourself.
What WARP is not. WARP changes your apparent egress IP to Cloudflare and encrypts traffic between your router and Cloudflare's edge. It is not an anonymity tool, it does not hide your traffic from Cloudflare, and it is not a substitute for a commercial VPN. Cloudflare's own terms apply to the WARP service.
Every tunnel is a separate config usque '<name>'
section in /etc/config/usque, with its own interface, config file, mode and routing
switches. The default connection is warp0.
The default setup: one WARP identity, one tunnel. Enable Default route for a full tunnel, or leave it off and route selected prefixes.
Separate config files (and separate Cloudflare devices). Give them different interface
names, and if both route by default, different metric values.
Keep a Cloudflare connection for general traffic and add a mode=custom tunnel
to another MASQUE/Connect-IP provider, routing only some prefixes through it.
The connection table lists every connection with its mode, interface, IPv4/IPv6 routing state and per-connection actions (Start / Stop / Restart / Test / Logs / Remove). Selecting a connection shows its settings: mode, config file, interface name, MTU, endpoint port, the routing switches and (Cloudflare mode) the account card with generate / re-register / delete configuration. Each enabled connection runs as its own procd instance; starting the service starts all of them.
Removing a connection stops it and deletes its UCI section but keeps its config file on disk — delete the file separately if you also want to drop the WARP registration.
A minimal /etc/config/usque with the default Cloudflare connection and a second
tunnel. The second section shows the shape of an additional connection (here a second Cloudflare
identity; for non-Cloudflare endpoints see custom mode):
config usque 'warp0'
option enabled '1'
option mode 'cloudflare'
option config_file '/etc/usque/warp0.json'
option interface 'warp0'
option mtu '1400'
option endpoint_port '443'
option default_route '0'
option metric '0'
option ipv6 '1'
option endpoint_ipv6 '0'
option ipv6_route 'auto'
option always_reconnect '1'
option auto_register '1'
option device_name ''
option log_level 'info'
option sni ''
option authority ''
config usque 'backup'
option enabled '0'
option mode 'cloudflare'
option config_file '/etc/usque/backup.json'
option interface 'warp1'
option default_route '1'
option metric '10'
The second connection is disabled in this example (enabled
'0') — enable it when you need it. The default config_file is
/etc/usque/<section>.json; each connection needs a unique
interface. Option names, defaults and the global LAN IPv6 settings are listed in
Advanced.
Set mode='custom' to skip Cloudflare registration and use credentials you
already have — in LuCI choose Custom endpoint as the connection's mode, then paste the
JSON into the Configuration JSON box and press Import, or write the file
yourself. There is no registration in this mode. Required fields:
private_key, endpoint_v4, endpoint_pub_key and
ipv4; optional: ipv6, endpoint_v6.
| JSON field | Meaning |
|---|---|
private_key | Your EC P-256 private key, base64 DER — the same encoding usque uses for WARP configs. Required. |
endpoint_v4, endpoint_v6 | IP address(es) of the MASQUE endpoint (UDP/443 by default). endpoint_v4 is required; endpoint_v6 is optional. |
endpoint_pub_key | Endpoint's public key as a PEM block (the same format as a WARP config). Required. |
ipv4, ipv6 | Addresses assigned to the tunnel interface by your provider. ipv4 is required; ipv6 is optional. |
{
"endpoint_v4": "203.0.113.10",
"endpoint_v6": "",
"endpoint_pub_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
"private_key": "<base64 DER EC P-256 private key>",
"ipv4": "172.16.0.2",
"ipv6": "fd00::2"
}
Shape only — get the real values from your provider. Importing writes the
file atomically with mode 600. Provider-specific TLS settings are separate
connection options (not part of the JSON): sni (-s; LuCI TLS SNI
(custom mode)), authority (--authority; LuCI HTTP authority
(custom mode)) and protocol (--protocol; LuCI CONNECT
protocol (custom mode) — pick connect-ip for an RFC 9484 server, leave
empty for Cloudflare's cf-connect-ip). logread -e usque reports the
handshake result.
The endpoint must speak a compatible MASQUE/Connect-IP service. Custom connections are not
registered or re-registered by the init script, and auto_register does nothing for
them.
Everything the LuCI app sets is plain UCI in /etc/config/usque,
so it can be scripted, versioned or set over SSH.
Each config usque '<name>' section is one connection. The section name is
the connection name used by LuCI, the procd instance name and the state directory
(/var/run/usque/<name>/); the packaged default is warp0. Options
are read as usque.<name>.option.
| Option | Default | Meaning |
|---|---|---|
enabled | 0 | Start this connection with the service/boot (each enabled connection runs as its own procd instance). |
mode | cloudflare | cloudflare registers/manages a WARP device; custom uses your own JSON credentials for another MASQUE/Connect-IP endpoint. |
config_file | /etc/usque/<section>.json | Identity/credentials file for this connection (mode 600). The default connection therefore uses /etc/usque/warp0.json. |
interface | warp0 | TUN interface name; must be unique across connections. |
mtu | 1400 | TUN MTU. Lower it (e.g. 1380) for problematic mobile uplinks. |
endpoint_port | 443 | UDP port used to reach the endpoint. |
default_route | 0 | Install an IPv4 default route via the tunnel while connected (removed on stop). Off by default — see Routing. |
metric | 0 | Metric for the tunnel default route(s); lower wins when several connections route by default. |
ipv6 | 1 | Tunnel IPv6 address (with 0 the daemon runs with --no-tunnel-ipv6). Does not select the endpoint address family — that is endpoint_ipv6. |
endpoint_ipv6 | 0 | Connect to the MASQUE endpoint over IPv6 (--ipv6); only for native-v6 or v6-only networks. |
ipv6_route | auto | auto installs an IPv6 default route only if the router has none; always installs it regardless; off never installs one. |
always_reconnect | 1 | Reconnect automatically when the tunnel drops. |
auto_register | 1 | Cloudflare mode only: register a WARP device if the config file is missing. Registration is non-interactive, so it accepts Cloudflare's WARP terms on your behalf — set it to 0 if you want to be asked. |
device_name | empty | Name used when registering a new device. |
sni | empty | TLS SNI for custom endpoints (passed as -s <sni>). Not needed for Cloudflare WARP. |
authority | empty | CONNECT :authority presented to a custom MASQUE server (--authority; a bare host becomes https://<host>). LuCI: HTTP authority (custom mode). |
protocol | empty | CONNECT protocol token for custom endpoints (--protocol): empty keeps Cloudflare's cf-connect-ip, use connect-ip for RFC 9484 servers. LuCI: CONNECT protocol (custom mode). |
log_level | info | Daemon log level: error, warn, info, debug. |
Two options live in the config settings 'settings' section and apply to the whole
package rather than a single connection (UCI path usque.settings.*). In LuCI they
are in Global settings.
| Option | Default | Meaning |
|---|---|---|
lan_ipv6 | 0 | Give LAN clients IPv6 through the tunnel (ULA + router advertisements + NAT66; see LAN IPv6). |
lan_ipv6_prefix | empty | ULA prefix to use (/48 or /64) when lan_ipv6 is on; empty = derived from the LAN MAC. |
Example: turn on full-tunnel routing for the default connection, force IPv6 through the tunnel and give LAN clients IPv6:
uci set usque.warp0.default_route='1'
uci set usque.warp0.ipv6_route='always'
uci set usque.settings.lan_ipv6='1'
uci commit usque
/etc/init.d/usque restart
Each connection points at its own JSON file (for the default connection:
/etc/usque/warp0.json). In Cloudflare mode it holds the device's
private_key and access_token; in custom mode it holds the fields listed
under custom mode. Files are written with mode 600 (root
only).
The default path is /etc/usque/<section>.json (so the
default connection uses warp0.json). If a file ever ends up with the wrong mode,
LuCI offers Fix permissions (chmod 600) for the selected connection.
cp /etc/usque/warp0.json /tmp/ and copy it away.rm -f /etc/usque/warp0.json then restart (with auto_register=1) or
press Re-register in LuCI.The daemon is also fully usable without LuCI. Registration is always per connection config file:
# register a new Cloudflare device for a specific connection
usque --config /etc/usque/warp0.json register --accept-tos --force -n "my-openwrt"
# run a tunnel in the foreground (what the init script does for each connection)
usque --config /etc/usque/warp0.json native-tun -n warp0 -m 1400 -P 443 --always-reconnect
# show help and version
usque --help
usque --version
Manual native-tun runs do not install routes by themselves — the
packaged service adds the routing hook and applies the switches described above. Use the service
for routed setups. The init script maps the UCI options to CLI flags: ipv6='0' →
--no-tunnel-ipv6, endpoint_ipv6='1' → --ipv6,
sni → -s, authority → --authority,
protocol → --protocol.
/etc/init.d/usque start # every enabled connection
/etc/init.d/usque start warp0 # one connection (section name)
/etc/init.d/usque stop warp0
/etc/init.d/usque restart warp0
/etc/init.d/usque stop
/etc/init.d/usque reload # re-read UCI, restart the instances
/etc/init.d/usque status
/etc/init.d/usque enable # start at boot
/etc/init.d/usque disable
logread -e usque # all connections (lines are prefixed with the section name)
logread -e usque -f # follow live
If you edit UCI directly and only need the new options applied, run
uci commit usque && /etc/init.d/usque restart. procd restarts a connection at
most 5 times within an hour (10 s apart) — after that it stays stopped until you restart
the service, so fix the underlying error first.
The LuCI app is backed by an rpcd script, which is also handy for scripts and monitoring.
ubus call luci.usque status
ubus call luci.usque getConfig '{"section":"warp0"}' # public fields only
ubus call luci.usque test '{"section":"warp0"}'
ubus call luci.usque logs '{"section":"warp0"}'
ubus call luci.usque setInitAction '{"action":"restart","section":"warp0"}'
ubus call luci.usque register '{"section":"warp0","name":"my-openwrt","accept_tos":true}'
ubus call luci.usque registerStatus '{"section":"warp0"}'
ubus call luci.usque importConfig '{"section":"backup","json":"{...}"}'
ubus call luci.usque deleteConfig '{"section":"backup"}'
ubus call luci.usque fixConfigPermissions '{"section":"warp0"}'
The private key and access token are never returned by
getConfig; importConfig only works for connections in custom mode.
Start with logread -e usque | tail -n 40 — the daemon logs
connection and routing decisions. The cases below cover most installs.
No — that is the default for IPv4. With default_route='0' the tunnel is
connected but no IPv4 traffic is sent into it (IPv6 may already be routed on WANs without a
native IPv6 default). To route everything, enable Default route for the connection
(or uci set usque.warp0.default_route='1'; uci commit usque; /etc/init.d/usque
restart), or add your own routes for selected prefixes. Check with:
ip route show | grep -E 'warp|default'
wget -qO- https://www.cloudflare.com/cdn-cgi/trace | grep -E '^(ip|warp)='
warp=off)This is the policy-routing trap: mwan3 (or any fwmark/IP-rule based policy routing) can be
evaluated before the main table and silently send traffic to the WAN, even though
ip route get shows the tunnel. When routing is enabled, usque installs a
priority-1000 rule to prevent it. Check that it is present:
ip rule show | grep '^1000:'
ip -6 rule show | grep '^1000:'
# if it is missing, add it for the current session:
ip rule add priority 1000 from all lookup main
ip -6 rule add priority 1000 from all lookup main # only with IPv6 routing
logread -e usque should contain installed IPv4 policy rule: priority 1000
from all lookup main. On stop the rule is removed again — mwan3 keeps working normally
when the tunnel is off.
With ipv6_route='auto' (default) usque only installs an IPv6 default route when
the router has no native/WAN IPv6 default route. A leftover or
provider-assigned default route therefore wins and is left untouched by design (a route
installed by another usque connection does not count as native). The connection table shows
this as “IPv6: auto (no route needed)”. Inspect and choose:
ip -6 route show default
uci set usque.warp0.ipv6_route='always' # force the tunnel
uci commit usque && /etc/init.d/usque restart
# or disable IPv6 for this connection entirely:
uci set usque.warp0.ipv6='0'
For LAN devices on an IPv4-only WAN you additionally need lan_ipv6
(next entry) — a tunnel IPv6 route alone does not give clients addresses.
lan_ipv6 is off by default. It needs an enabled connection with IPv6 on and
ipv6_route set to auto or always, so the tunnel provides
the IPv6 default route; the helper then fills in missing LAN settings. Check all of it:
uci show usque.settings
uci show network.globals.ula_prefix
uci show network.lan | grep -E 'ip6assign|ip6addr'
uci show dhcp.lan | grep -E 'ra|dhcpv6'
ip -6 addr show br-lan
logread -e usque | tail -n 20
If you already run native IPv6 on the LAN, leave lan_ipv6 off — the helper
should not compete with your existing RA/DHCPv6 setup. Disabling lan_ipv6 is
non-destructive: the generated network/dhcp settings stay, remove
them manually if you want to revert.
Only one IPv4 default route can win, and one IPv6 default route can win. If two connections
both have Default route enabled, give them different metric values
(lower wins), or enable routing on just one of them and route other prefixes manually. The
hook ref-counts routing ownership across connections: the policy rule and the saved WAN
default route are only removed when the last routing connection stops. Check the interfaces
and routes:
uci show usque | grep -E 'default_route|metric|interface'
ip route show default
ip -6 route show default
Custom mode skips registration and uses what is in the JSON file (import it in the
connection's Configuration JSON box, or write it yourself). Common causes: wrong or
expired keys, the endpoint unreachable over UDP/443, or a missing sni when the
provider requires TLS SNI (uci set usque.<section>.sni='example.com', passed
as -s), an HTTP authority (uci set
usque.<section>.authority='example.com', passed as --authority) or a
different CONNECT protocol token (uci set usque.<section>.protocol='connect-ip'
for an RFC 9484 server; empty means Cloudflare's cf-connect-ip). Check logread -e usque for handshake errors, verify the
required fields against custom mode, and confirm the same credentials
work with another client. Remember: auto_register has no effect in custom
mode.
The package is installed but LuCI's cached menu index still needs a refresh:
rm -f /tmp/luci-indexcache*
/etc/init.d/rpcd restart
/etc/init.d/uhttpd restart
Then hard-refresh the browser (Ctrl/Cmd+Shift+R), log out and back in. Confirm the package
is really present with apk info -e luci-app-usque and that
/usr/share/luci/menu.d/luci-app-usque.json exists.
Work through these in order:
logread -e usque | tail -n 40 — read the last error.uci show usque | grep -E 'enabled|interface'. The
init script only starts connections with enabled=1, and if none is enabled the
service is stopped and disabled.ls -l /etc/usque/warp0.json. If it
is missing, either enable auto_register or register from LuCI / the CLI.date. A router whose time is far off cannot complete
TLS. Let NTP sync (/etc/init.d/sysntpd restart) or set the time in LuCI.lsmod | grep tun and
ls -l /dev/net/tun. Install the module with apk add kmod-tun and
reboot.162.159.198.2). Some tethering, hotel, corporate or ISP
setups block it. This build is trimmed to QUIC and has no TCP fallback — if UDP is blocked it
cannot connect, and you need a WARP client with HTTP/2 support such as the
Go implementation of
usque.The index signature does not match any key in /etc/apk/keys/. Re-download the
key, check the fingerprint, and run apk update again. With
apk v3 the key is identified by its public-key identity (the filename does not have to be
usque.pem, but the key material must match the one the feed was signed with). If
the feed was re-keyed, remove the stale key file first. The
--allow-untrusted fallback is a last resort only.
With apk-tools 3, apk add only installs missing packages; it does not upgrade an
installed one (apk prints “upgradable from …” instead). Upgrade with apk upgrade
usque luci-app-usque, apk upgrade for everything, or pin versions:
apk add usque=<version> luci-app-usque=<version>. Then check:
apk info -v usque
apk info -v luci-app-usque
apk policy usque luci-app-usque
What usque added is removed on stop. If a crash left something behind, restore normal routing:
ip rule show
ip route
# remove leftovers if any:
ip rule del priority 1000
ip route del default dev warp0
fw4 reload
/etc/init.d/network restart
Also check ip -6 route if IPv6 routing was enabled. Rebooting always brings you
back to a clean state.
The binary is a few megabytes and some routers have a tiny overlay. Check the space and free some up:
df -h /overlay
du -sh /usr/bin/usque
apk cache clean
Remove packages you do not use (apk del <name>), clear the LuCI cache
(rm -f /tmp/luci-indexcache*), or move the overlay to a USB stick with extroot if
your device supports it. A 16 MB-flash router will usually struggle to fit a
multi-megabyte binary; a device with more flash is a better fit.
Every Generate WARP configuration registers a new device in your Cloudflare WARP account, and the free plan only allows a limited number of linked devices. This applies per connection: each Cloudflare connection with its own identity is another device. Reuse an existing JSON file instead of re-registering, or remove unused devices in the Cloudflare WARP dashboard / Zero Trust device list. If registration fails with a device-limit error, delete the old device first and try again.
The tunnel only affects traffic that follows the router's default route or your own routes.
Check that the connection has Default route enabled (or that your static policy routes
apply), that the usque firewall zone and forwardings exist
(fw4 print | grep -A4 usque; lan and any other internal zone that
forwards to wan are wired up automatically), and that clients use the router as
their gateway and DNS server. A client with a static public DNS or its own VPN bypasses the
router's tunnel. Also check the priority-1000 ip rule if you run mwan3.
Try, one change at a time: lower the MTU to 1380 (mobile uplinks), check CPU
load — encryption at a few hundred Mbit/s is a lot for a small router — and make sure UDP/443
is not throttled or blocked by your ISP. logread -e usque shows reconnect
attempts.
No. Routing changes only affect outbound/forwarded traffic; inbound SSH and LuCI on the LAN continue to work. With defaults, routing is not touched at all. If you manage the router from outside the LAN, keep a reliable out-of-band path before enabling Default route.
No IPv4 routing is changed: installing and starting usque creates the tunnel but leaves your
IPv4 default route, ip rules, DNS and firewall forwarding unchanged. IPv4 traffic
only enters the tunnel after you enable Default route for a connection or add your own
routes. IPv6 is the exception: with ipv6_route=auto a tunnel IPv6 default route is
installed only if the router has no native IPv6 default route, so IPv6 works on IPv4-only
WANs. Set ipv6_route='off' if you want no IPv6 routing changes either.
v0.1.x always installed default dev <iface> metric 0 on connect. That was
convenient for a full tunnel but surprising if you did not expect it. v2 makes routing an
explicit per-connection choice, keeps the WAN default route as a fallback, and removes
everything it added on stop.
No. Registration creates an anonymous free WARP device identity; usque talks to
the public WARP service. No Cloudflare login is involved.
Leave Default route off and add your own routes for the prefixes you want tunneled
(ip route add <prefix> dev warp0), or use policy routing (mwan3, nftables
marks, ip rule). See How do I get traffic through the
tunnel?. This replaces the old "split tunnelling is not supported" limitation. For IPv6,
ipv6_route='off' keeps usque out of the v6 routing table entirely.
Yes. Add another config usque section (or use the LuCI connection list) with its
own interface name and config file. If you want both to route by default, give them different
metric values — only one default route can win per address family. See
Multiple connections.
Yes, with mode='custom': provide the endpoint addresses, endpoint public key,
your private key and the assigned tunnel addresses in the connection's JSON config. There is no
registration in this mode. See custom endpoints.
The LuCI app and the Cloudflare mode focus on the free consumer WARP service. No license-key or organisation/Zero Trust fields are exposed. Custom mode is for other compatible MASQUE/Connect-IP endpoints.
Yes. Connections with enabled=1 start at boot, and identity files persist. Feed
updates show up as normal package upgrades (apk upgrade) and keep your
configuration.
Right now the feed only contains arm_cortex-a7_neon-vfpv4 packages
(ipq40xx/generic) built against OpenWrt 25.12.6. That is also the architecture
the full v2 feature matrix was verified on (0.2.1-r1 / 1.1.1-r1, live OpenWrt 25.12.6
router). Other targets would need their own build — the workflow is in the repository if you
want to add one.
Yes — LAN clients can query whichever DNS server you advertise via DHCP. Queries from the router itself, and upstream queries from dnsmasq, follow the routing you chose. A separate LAN DNS appliance keeps answering local clients directly, but its own upstream traffic is only tunneled if it is routed through the router.
For this packaging (LuCI app, init script, apk feed) use the GitHub issue tracker. Bugs in the tunnel client itself belong upstream at FireMasterK/usque-rs.
Stopping the service restores normal routing. Removing the packages cleans up everything except your saved configuration and identities.
/etc/init.d/usque stop
/etc/init.d/usque disable
apk del luci-app-usque usque
| What | After uninstall |
|---|---|
| Binary, init script, hook, helpers | Removed. |
| LuCI views, menu, ACL, rpcd backend | Removed. |
| Routes, policy rules, run state | Removed on stop (and by a fallback in the init script). Transient state lives in /var/run/usque/ and disappears on reboot. |
| Firewall zone / forwarding | Removed by the package's prerm cleanup (which calls /usr/libexec/usque/firewall teardown). If you removed the package files by hand or a stale zone remains, check uci show firewall | grep -i usque, delete the matching sections with uci delete firewall.<section>, uci commit firewall and fw4 reload. |
/etc/config/usque | Kept (conffile). Delete it manually for a completely clean system: rm -f /etc/config/usque. |
/etc/usque/*.json | Kept, because they are your device identities (one per connection). Delete the ones you will not reuse: rm -f /etc/usque/warp0.json. |
| Feed entry & key | Still configured. Remove them with the repository removal commands. |
Remove old identities before re-registering elsewhere. If you move to a new router, keep the JSON files and reuse them — that avoids burning another device slot in your WARP account.
Everything on this page is produced by GitHub Actions from the public repository — binary, apk packages, signed feed and this site.
usque 0.2.1-r1 · luci-app-usque 1.1.1-r1 for OpenWrt 25.12
(arm_cortex-a7_neon-vfpv4) — the two packages are versioned independently.
Binary: 2,905,040 B (2.77 MiB), sha256
413515df…58ae5c9a. Installed size 2,950 KiB. Built by CI run
40373166952 from commit 5139af6. Confirm with apk info -v usque
and apk info -v luci-app-usque.
packages.adb (signed index),
usque.pem (public key),
usque-0.2.1-r1.apk (1,466,405 B) and
luci-app-usque-1.1.1-r1.apk
(21,561 B).
MIT, same as upstream. The full text is in
LICENSE.md
(upstream copy: src/LICENSE.md).
| Path | Contents |
|---|---|
src/ | Vendored usque-rs at commit
f2a073c
(provenance in UPSTREAM.md), patched for OpenWrt: TUN only,
ring crypto, size optimisations. |
openwrt/usque/ | OpenWrt package: binary, procd init script, UCI defaults, routing/firewall connect + disconnect hooks, LAN IPv6 helpers. |
openwrt/luci-app-usque/ | LuCI JS views (connection list, registration, routing switches, diagnostics), menu entry, ACL and the rpcd backend. |
build/ | Reproducible armv7 cross-compile script used by CI, plus
the patch notes (build/PATCHES.md). |
scripts/ | Feed assembly and signing. |
.github/workflows/ | CI: static armv7 build
(build.yml), SDK package build + feed signing (package.yml),
site + feed deployment (publish.yml). |
keys/usque.pem | Public key used to verify the apk feed. |
tests/ | Live-router verification scripts and results —
see tests/RESULTS.md
for the router model, test matrix and measured values. |
docs/ | This static site (plain HTML/CSS, no build step). |
LICENSE.md, README.md | MIT licence text and the project overview. |
armv7-unknown-linux-musleabihf with a static musl toolchain, LTO and
opt-level="z", then stripped. See the
workflows
and build/build-usque.sh.usque and luci-app-usque; scripts/ assembles the feed and
signs packages.adb with an ECDSA P-256 key (package.yml).publish.yml copies docs/ to the site
root, puts the public key and the signed feed into /feed/, and deploys the
artifact to GitHub Pages.Verification: the 0.2.1-r1 / 1.1.1-r1 packages were exercised on a live
OpenWrt 25.12.6 ipq40xx router — signed-feed install, service lifecycle, opt-in IPv4 routing,
IPv6 (auto and forced), multiple connections, custom MASQUE/Connect-IP mode and LAN
IPv6/NAT66. Scripts, evidence and the check matrix are in
tests/RESULTS.md.
The vendored client is trimmed and adapted to keep the binary small and make it manageable by
procd/LuCI: TUN mode only (SOCKS5, HTTP proxy and port-forwarding code removed), QUIC-only
transport (the HTTP/2 fallback is removed to save space), no BoringSSL (QUIC via
ring, which supports 32-bit ARM), non-interactive
register --accept-tos --force, and a size-optimised release profile.
Multi-connection management, opt-in routing, IPv6 route selection and LAN IPv6/NAT66 are
implemented in this repository's OpenWrt init script, routing hook and LuCI app rather than in
the tunnel client. Details are recorded in
build/PATCHES.md
and UPSTREAM.md.
src/LICENSE.md).LICENSE.md.Not affiliated with Cloudflare or OpenWrt. This is an independent, community-style packaging of an open-source client. Cloudflare's WARP terms of service apply to your use of the WARP service — accept them knowingly and use the project at your own risk. No warranty is provided.
This site has no analytics, cookies or external resources — it is plain HTML, CSS and a tiny inline copy-to-clipboard script. GitHub Pages serves it and may log request metadata as described in GitHub's privacy statement. The packages themselves contain no telemetry; the tunnel's traffic goes to Cloudflare by design, and custom endpoints receive their traffic instead.