GitHub - mp0rta/mqvpn: Multipath VPN using MASQUE and Multipath QUIC

7 min read Original article ↗

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.

Features

  • 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.
  • Android SDK — Kotlin SDK via JNI. Apps implement onCreateTun() and onVpnStateChanged().
  • PSK auth — Pre-shared key over TLS 1.3.
  • DNS override — Prevents DNS leaks. Uses resolvectl on systemd-resolved systems, falls back to resolv.conf.

Installation

Server

curl -fsSL https://github.com/mp0rta/mqvpn/releases/latest/download/install.sh | sudo bash

This 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 -- --start

Note: The self-signed certificate requires --insecure on 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/24

Uninstall: re-run the install script with --uninstall.

Client (deb package)

Download the latest .deb from Releases:

# Replace VERSION and ARCH as needed (e.g., 0.5.0, amd64)
curl -LO https://github.com/mp0rta/mqvpn/releases/latest/download/mqvpn_VERSION_ARCH.deb
sudo dpkg -i mqvpn_*.deb

Quick Start

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 --insecure

Notes:

  • Without --path, the client uses the default interface (single path). Multipath requires two or more --path flags.
  • The server needs its listen port open for UDP (default: 443). All client traffic is routed through the tunnel.

Configuration

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

[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
# /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

[Multipath]
Scheduler = wlb
Path = eth0
Path = wlan0

JSON config

The 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"
}

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,
    "scheduler": "wlb"
}

Notes:

  • users is server-side auth and accepts either objects ({"name","key"}) or "name:key" strings.
  • auth_key remains supported as a single legacy/global key.
  • mode is optional if it can be inferred (listen implies server).
sudo mqvpn --config /etc/mqvpn/server.conf
sudo mqvpn --config /etc/mqvpn/client.conf

systemd

# 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@home

Control API

A running server can be managed at runtime over a TCP port using newline-delimited JSON.

Enable

# CLI
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.1

Security: 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.

Commands

Add a user

echo '{"cmd":"add_user","name":"carol","key":"carol-secret"}' | nc 127.0.0.1 9090

Calling add_user with an existing name updates the key in place.

Remove a user

echo '{"cmd":"remove_user","name":"carol"}' | nc 127.0.0.1 9090

List users

echo '{"cmd":"list_users"}' | nc 127.0.0.1 9090
{"ok":true,"users":["alice","bob"]}

Get stats

echo '{"cmd":"get_stats"}' | nc 127.0.0.1 9090
{"ok":true,"n_clients":2,"bytes_tx":983040,"bytes_rx":458752}

Error response

{"ok":false,"error":"user not found"}

From code (Python example)

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, ...}

Benchmarks

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%

Architecture

┌─────────────────┐                          ┌─────────────────┐
│   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

Building

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 rebuild
Manual 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)

Android SDK

scripts/build_android.sh --abi arm64-v8a    # cross-compile C libs
cd android && ./gradlew assembleDebug       # build SDK + demo app
Module 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)

Testing

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 tests

Usage

mqvpn [--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|backup_fec
                         Multipath scheduler (default: wlb)
  --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

Roadmap

  • 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 ip command)
  • Performance: GSO/GRO, sendmmsg, native Android I/O
  • Interop testing (masque-go, QUICHE)

Protocol Standards

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

Disclaimer

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.

License

Apache-2.0

Copyright (c) 2026 mp0rta

Acknowledgments

  • XQUIC by Alibaba
  • IETF QUIC and MASQUE working groups