Multipath QUIC VPN using MASQUE CONNECT-IP (RFC 9484) over HTTP Datagrams (RFC 9297) / QUIC DATAGRAMs (RFC 9221), built on a fork of XQUIC with Multipath QUIC.
- Multipath — Bind multiple interfaces (WiFi + LTE, dual ISP). Seamless failover and bandwidth aggregation via WLB scheduler.
- Standards-based — MASQUE CONNECT-IP (RFC 9484), no proprietary tunnel format.
- Dual-stack — IPv4 + IPv6 inside the tunnel.
- Multi-Platform — Available on Linux (server/client), Windows (client only) and Android (client only) support.
- PSK auth — Pre-shared key over TLS 1.3.
- DNS override — Prevents DNS leaks. Uses
resolvectlon systemd-resolved systems, falls back to resolv.conf.
curl -fsSL https://github.com/mp0rta/mqvpn/releases/latest/download/install.sh | sudo bashThis downloads the latest release, installs the binary, and generates a self-signed TLS certificate, auth key, and server config at /etc/mqvpn/server.conf. Add --start to start the server and register it for automatic startup on boot:
curl -fsSL https://github.com/mp0rta/mqvpn/releases/latest/download/install.sh \
| sudo bash -s -- --startNote: The self-signed certificate requires
--insecureon the client. For production, replace with a trusted certificate (e.g. Let's Encrypt) and omit--insecure.
Options can be combined:
curl -fsSL https://github.com/mp0rta/mqvpn/releases/latest/download/install.sh \
| sudo bash -s -- --start --port 10020 --subnet 10.8.0.0/24Uninstall: re-run the install script with --uninstall.
curl -fsSL https://github.com/mp0rta/mqvpn/releases/latest/download/install.sh \
| sudo bash -s -- --uninstallDownload the latest .deb from Releases:
# Replace VERSION and ARCH as needed (e.g., 0.6.0, amd64)
curl -LO https://github.com/mp0rta/mqvpn/releases/latest/download/mqvpn_VERSION_ARCH.deb
sudo dpkg -i mqvpn_*.debPre-built binaries are shipped for Windows amd64 and arm64. Download mqvpn_<VERSION>_windows_<ARCH>.zip from Releases, extract, and follow the bundled README.txt (admin PowerShell required).
After installing the server and client (see Installation):
# Client (single path)
sudo mqvpn --mode client --server YOUR_SERVER:443 \
--auth-key YOUR_AUTH_KEY --insecure
# Client (multipath)
sudo mqvpn --mode client --server YOUR_SERVER:443 \
--auth-key YOUR_AUTH_KEY --path eth0 --path wlan0 --insecure
# Client (with DNS override)
sudo mqvpn --mode client --server YOUR_SERVER:443 \
--auth-key YOUR_AUTH_KEY --dns 1.1.1.1 --dns 8.8.8.8 --insecureNotes:
- On Linux, without
--path, the client uses the default interface (single path); multipath requires two or more--pathflags. On Windows,--pathis always required (one or more); seedocs/windows_build.md.- The server needs its listen port open for UDP (default: 443). All client traffic is routed through the tunnel.
Config files support both INI and JSON. CLI arguments override config values.
# /etc/mqvpn/server.conf
[Interface]
Listen = 0.0.0.0:443
Subnet = 10.0.0.0/24
Subnet6 = 2001:db8:1::/112
# MTU = 1280 # TUN MTU (1280–9000, default: auto = ~1382)
[TLS]
Cert = /etc/mqvpn/server.crt
Key = /etc/mqvpn/server.key # TLS private key (PEM file)
[Auth]
Key = mPyVpoQWcp/5gr404xvS19aRC03o0XS2mrb2tZJ1Ii4= # PSK example (mqvpn --genkey)
User = alice:alice-secret
User = bob:bob-secret
[Multipath]
Scheduler = wlb
# CC = bbr2 # Congestion control (bbr2|bbr|cubic|none, default: bbr2)# /etc/mqvpn/client.conf
[Server]
Address = 203.0.113.1:443
[Auth]
Key = mPyVpoQWcp/5gr404xvS19aRC03o0XS2mrb2tZJ1Ii4=
[Interface]
DNS = 1.1.1.1, 8.8.8.8
# MTU = 1280 # TUN MTU (1280–9000, default: auto = ~1382)
[Multipath]
Scheduler = wlb
# CC = bbr2 # Congestion control (bbr2|bbr|cubic|none, default: bbr2)
Path = eth0
Path = wlan0The loader auto-detects JSON files (first non-space char is {).
Server example:
{
"mode": "server",
"listen": "0.0.0.0:443",
"subnet": "10.0.0.0/24",
"subnet6": "fd00:abcd::/112",
"cert_file": "/etc/mqvpn/server.crt",
"key_file": "/etc/mqvpn/server.key",
"auth_key": "legacy-fallback-key",
"users": [
{ "name": "alice", "key": "alice-secret" },
"bob:bob-secret"
],
"max_clients": 64,
"scheduler": "wlb",
"cc": "bbr2"
}Client example:
{
"mode": "client",
"server_addr": "203.0.113.1:443",
"auth_key": "client-key",
"insecure": true,
"dns": ["1.1.1.1", "8.8.8.8"],
"paths": ["eth0", "wlan0"],
"reconnect": true,
"reconnect_interval": 5,
"kill_switch": false,
"manage_routes": false,
"scheduler": "wlb",
"cc": "bbr2"
}Notes:
usersis server-side auth and accepts either objects ({"name","key"}) or"name:key"strings.auth_keyremains supported as a single legacy/global key.modeis optional if it can be inferred (listenimplies server).manage_routesdefaults totrue; set it tofalseon router/embedded integrations where an external orchestrator owns the host routing table and mqvpn should only bring up the TUN.- mqvpn-prometheus-exporter requires per-user keys. Using mqvpn-prometheus-exporter, you can correct and visualize mqvpn metrics. If you use it, sharing a single
auth_keyacross multiple clients works for the VPN data plane, but the control API surfaces those sessions asuser="(global)"and the Prometheus exporter cannot distinguish them — series labels collide and the scrape is dropped. For multi-client deployments register each client underusers(or viaadd_userover the control API) so each gets a distinctuserlabel.
sudo mqvpn --config /etc/mqvpn/server.conf
sudo mqvpn --config /etc/mqvpn/client.conf| Scheduler | TCP | UDP | Typical use |
|---|---|---|---|
minrtt |
min RTT | min RTT | latency-first |
wlb (default) |
flow pin | unpinned | general use; UDP packets distributed per-packet |
wlb_udp_pin |
flow pin | flow pin | each UDP connection kept on a single path |
backup_fec |
redundant | redundant | resilience-first (requires XQC_ENABLE_FEC) |
Choosing wlb vs wlb_udp_pin: Plain wlb distributes UDP packets across
paths per-packet, which gives better aggregate throughput when the inner
protocol tolerates reorder. Some inner UDP protocols, however, maintain their
own packet ordering and may treat reorder as packet loss — under asymmetric-RTT
paths this can slow them down and throughput drops. wlb_udp_pin keeps each
UDP connection on a single path to avoid that case. If you observe degraded UDP
throughput under wlb, try wlb_udp_pin; otherwise wlb is the better
default.
Trade-off note (wlb_udp_pin): the xquic WLB flow table is a fixed 4096-entry
open-addressed structure with 60s idle eviction. Workloads with very high
short-flow UDP churn (e.g. high-rate DNS, mDNS bursts) may evict longer-lived
inner flows under probe-region pressure. wlb_udp_pin is intended for tunnels
carrying a small-to-moderate set of long-lived inner UDP flows; high-churn UDP
profiles are better served by wlb.
# Server
sudo cp /etc/mqvpn/server.conf.example /etc/mqvpn/server.conf
sudo vi /etc/mqvpn/server.conf # edit cert/key paths, auth key, etc.
sudo systemctl enable --now mqvpn-server
# Client (template — instance name maps to config file)
sudo cp /etc/mqvpn/client.conf.example /etc/mqvpn/client-home.conf
sudo vi /etc/mqvpn/client-home.conf # edit server address, auth key, etc.
sudo systemctl enable --now mqvpn-client@homeA running server can be managed at runtime over a TCP port using newline-delimited JSON.
Control API: see docs/control-api.md for the full wire-protocol reference (all 8 commands, request/response schemas, error strings).
The control API is disabled by default. Enable it via any of the following:
sudo bash install.sh --enable-control # port 9090
sudo bash install.sh --enable-control 9091[Control]
Listen = 127.0.0.1:9090{
"control_listen": "127.0.0.1:9090"
}sudo mqvpn --mode server ... --control-port 9090
# Bind to a specific address (default: 127.0.0.1)
sudo mqvpn --mode server ... --control-port 9090 --control-addr 127.0.0.1Security: bind only to
127.0.0.1(the default) unless the port is protected by a firewall or network policy. The control API has no authentication.
echo '{"cmd":"add_user","name":"carol","key":"carol-secret"}' | nc 127.0.0.1 9090{"ok":true}Calling add_user with an existing name updates the key in place.
echo '{"cmd":"remove_user","name":"carol"}' | nc 127.0.0.1 9090{"ok":true}echo '{"cmd":"list_users"}' | nc 127.0.0.1 9090{"ok":true,"users":["alice","bob"]}echo '{"cmd":"get_stats"}' | nc 127.0.0.1 9090{"ok":true,"n_clients":2,"bytes_tx":983040,"bytes_rx":458752}{"ok":false,"error":"user not found"}import socket, json
def ctrl(port, cmd):
with socket.create_connection(("127.0.0.1", port)) as s:
s.sendall((json.dumps(cmd) + "\n").encode())
return json.loads(s.makefile().readline())
ctrl(9090, {"cmd": "add_user", "name": "dave", "key": "dave-secret"})
ctrl(9090, {"cmd": "remove_user", "name": "dave"})
print(ctrl(9090, {"cmd": "list_users"})) # {'ok': True, 'users': ['alice', 'bob']}
print(ctrl(9090, {"cmd": "get_stats"})) # {'ok': True, 'n_clients': 1, ...}Asymmetric dual-path (300M/10ms + 80M/30ms) via network namespaces. Full report: docs/benchmarks_netns.md
| Test | Result |
|---|---|
| Failover | 0 downtime |
| Bandwidth aggregation (WLB, 16 streams) | 319 Mbps (84% of 380 Mbps theoretical) |
| WLB vs MinRTT | WLB +21% |
┌─────────────────┐ ┌─────────────────┐
│ Application │ │ Internet │
├─────────────────┤ ├─────────────────┤
│ TUN (mqvpn0) │ │ TUN (mqvpn0) │
├─────────────────┤ ├─────────────────┤
│ MASQUE │ HTTP Datagrams │ MASQUE │
│ CONNECT-IP │◄──(Context ID = 0)──────►│ CONNECT-IP │
├─────────────────┤ ├─────────────────┤
│ Multipath QUIC │◄── Path A ──────────────►│ Multipath QUIC │
│ │◄── Path B ──────────────►│ │
├─────────────────┤ ├─────────────────┤
│ UDP (eth0/wlan)│ │ UDP (eth0) │
└─────────────────┘ └─────────────────┘
Client Server
Requirements: Linux, CMake 3.10+, GCC/Clang (C11), libevent 2.x
git clone --recurse-submodules https://github.com/mp0rta/mqvpn.git
cd mqvpn
./build.sh # builds BoringSSL, xquic, and mqvpn
./build.sh --clean # full rebuildManual build steps
# 1. Build BoringSSL
cd third_party/xquic/third_party/boringssl
mkdir -p build && cd build
cmake -DBUILD_SHARED_LIBS=0 -DCMAKE_C_FLAGS="-fPIC" -DCMAKE_CXX_FLAGS="-fPIC" ..
make -j$(nproc) ssl crypto
cd ../../../../..
# 2. Build xquic
cd third_party/xquic
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DSSL_TYPE=boringssl \
-DSSL_PATH=../third_party/boringssl \
-DXQC_ENABLE_BBR2=ON \
-DXQC_ENABLE_FEC=ON \
-DXQC_ENABLE_XOR=ON ..
make -j$(nproc)
cd ../../..
# 3. Build mqvpn
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release \
-DXQUIC_BUILD_DIR=../third_party/xquic/build ..
make -j$(nproc)scripts/build_android.sh --abi arm64-v8a # cross-compile C libs
cd android && ./gradlew assembleDebug # build SDK + demo appModule structure
android/
├── sdk-native/ # JNI bridge → libmqvpn_jni.so
├── sdk-runtime/ # MqvpnPoller (tick-loop)
├── sdk-network/ # NetworkMonitor, PathBinder
├── sdk-core/ # MqvpnVpnService, MqvpnManager, TunnelBridge
└── app/ # Demo app (Jetpack Compose)
cd build && ctest --output-on-failure # C library unit tests
sudo scripts/ci_e2e/run_test.sh # E2E (netns, requires root)
sudo scripts/run_multipath_test.sh # multipath failover
cd android && ./gradlew test # Android SDK unit testsmqvpn [--config PATH] --mode client|server [options]
--server IP:PORT Server address (client)
--path IFACE Multipath interface (repeatable)
--auth-key KEY PSK authentication
--user NAME:KEY Add server user credential (repeatable)
--dns ADDR DNS server (repeatable)
--insecure Accept untrusted certs (testing only)
--listen BIND:PORT Listen address (server, default: 0.0.0.0:443)
--subnet CIDR Client IPv4 pool (server)
--subnet6 CIDR Client IPv6 pool (server)
--scheduler minrtt|wlb|wlb_udp_pin|backup_fec
Multipath scheduler (default: wlb)
--cc bbr2|bbr|cubic|none
Congestion control algorithm (default: bbr2)
--control-port PORT TCP port for JSON control API (server)
--control-addr ADDR Bind address for control API (default: 127.0.0.1)
--genkey Generate PSK and exit
--help Show all options
- v0.1.0 — TLS verification, WLB scheduler, multi-client, PSK auth, DNS, config file
- v0.2.0 — Reconnection, kill switch, IPv6, ICMP PTB, systemd service
- v0.3.0 — libmqvpn (sans-I/O), Android Kotlin SDK, network detection
- Per-client token auth
- resolvectl DNS support (with resolv.conf fallback)
- v0.4.0 — Experimental backup_fec scheduler, Windows client, server control API support
- netlink API for routing (replace fork+exec of
ipcommand) - Performance: GSO/GRO, sendmmsg, native Android I/O
- Interop testing (masque-go, QUICHE)
| Protocol | Spec |
|---|---|
| MASQUE CONNECT-IP | RFC 9484 |
| HTTP Datagrams | RFC 9297 |
| QUIC Datagrams | RFC 9221 |
| Multipath QUIC | draft-ietf-quic-multipath |
| HTTP/3 | RFC 9114 |
mqvpn is licensed under the Apache License 2.0 and is provided "AS IS", without warranties or conditions of any kind.
Use of mqvpn is at your own risk. Users are solely responsible for validating its suitability, security, and operational safety, especially in production or commercial environments.
Apache-2.0
Copyright (c) 2026 mp0rta
- XQUIC by Alibaba
- IETF QUIC and MASQUE working groups