| app | ||
| package | ||
| scripts | ||
| README.md | ||
DragonMoonlight
A fork of moonlight-qt with native DragonRelay integration and an embedded WireGuard client.
Instead of managing a separate VPN app, DragonMoonlight authenticates against your DragonRelay server, auto-provisions a WireGuard peer, brings up the tunnel, and presents your streaming hosts — all in one UI.
Architecture
┌─────────────────────────────────┐ ┌──────────────────────────────┐
│ DragonMoonlight (this fork) │ │ DragonRelay (homelab) │
│ │ │ │
│ RelayClient ─── HTTPS ────────►│──────►│ POST /api/vpn/peer │
│ │ │ │ (returns WireGuard .conf) │
│ ▼ │ │ │
│ TunnelManager │ │ wg0 interface │
│ ├─ boringtun (WireGuard/Rust) │◄═════►│ 10.99.0.1 │
│ ├─ utun (macOS, no root*) │ WG │ │
│ ├─ Wintun (Windows, Admin) │ │ Hosts registered by Artemis │
│ └─ /dev/net/tun (Linux, CAP) │ │ via /api/host/register │
│ │ │ │
│ DragonRelayView.qml │ └──────────────────────────────┘
│ (host list, stream button) │
└─────────────────────────────────┘
* Route setup requires one macOS admin auth dialog per session.
New files (vs upstream moonlight-qt)
app/vpn/
├── boringtun_ffi.h C ABI for the boringtun Rust library
├── wireguardconfig.h/.cpp WireGuard .conf parser
├── tunnelmanager.h Cross-platform tunnel interface
├── tunnelmanager_mac.mm macOS implementation (utun + boringtun)
├── tunnelmanager_win.cpp Windows implementation (Wintun + boringtun)
├── tunnelmanager_linux.cpp Linux implementation (/dev/net/tun + boringtun)
├── wintun.h Wintun dynamic loader (Windows)
├── relayclient.h/.cpp HTTP client for the DragonRelay API
├── dragonrelaybackend.h/.cpp QObject bridge: RelayClient + TunnelManager → QML
└── CMakeLists_vpn.cmake Build system additions
app/gui/
├── DragonRelayView.qml Connect / host-list UI page
└── DragonDisplayPicker.qml Multi-display selection modal
app/
├── DragonMoonlight.manifest Windows UAC manifest (requireAdministrator)
├── main.cpp Entry point with QML engine + dragonRelay backend
└── assets/ WildDragon logo + qrc
scripts/
├── build-boringtun.sh Compiles boringtun for Linux/macOS
├── build-boringtun-win.ps1 Compiles boringtun for Windows
├── build-installer-mac.sh macOS .dmg + .pkg installer
├── build-installer-win.ps1 Windows .exe installer (Inno Setup)
└── DragonMoonlight.iss Inno Setup script
package/
├── mac/ macOS code-signing entitlements + DMG art
└── win/ Windows pre-install elevation/version checks
deps/boringtun/ Created by build scripts (git-ignored)
Building
macOS
brew install qt@6 cmake rustup
rustup-init && rustup target add aarch64-apple-darwin x86_64-apple-darwin
git clone https://forge.wilddragon.net/zgaetano/dragonmoonlight && cd dragonmoonlight
git remote add upstream https://github.com/moonlight-stream/moonlight-qt.git
git fetch upstream && git merge upstream/master --allow-unrelated-histories
git submodule update --init --recursive
bash scripts/build-boringtun.sh --universal
mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH="$(brew --prefix qt@6)"
make -j$(sysctl -n hw.logicalcpu)
Windows
# Prerequisites: Visual Studio Build Tools 2019+, Rust, CMake, Qt 6, Inno Setup
git clone https://forge.wilddragon.net/zgaetano/dragonmoonlight
cd dragonmoonlight
git remote add upstream https://github.com/moonlight-stream/moonlight-qt.git
git fetch upstream
git merge upstream/master --allow-unrelated-histories
git submodule update --init --recursive
pwsh scripts/build-boringtun-win.ps1
cmake -B build -A x64 -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
# Optional: build the .exe installer
pwsh scripts/build-installer-win.ps1
wintun.dll (x64) must sit next to DragonMoonlight.exe at run time. The
installer script downloads it automatically; for ad-hoc builds, fetch it from
https://www.wintun.net and copy it to the build output directory.
Linux
sudo apt install qt6-base-dev qt6-declarative-dev qml6-module-qtquick \
cmake build-essential libcurl4-openssl-dev rustup
bash scripts/build-boringtun.sh
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
# Grant net-admin so the binary can open /dev/net/tun without being root:
sudo setcap cap_net_admin+ep ./build/dragonmoonlight
How it works (connection flow)
User clicks "Connect" in DragonRelayView
│
▼
RelayClient.login(url, user, pass)
│ POST /api/auth/login → JWT
▼
RelayClient.provisionVPN(deviceName)
│ POST /api/vpn/peer → {id, conf}
▼
WireGuardConfig::fromConf(conf)
│
▼
TunnelManager.start(cfg) (mac/win/linux platform impl)
├─ new_tunnel() via boringtun FFI
├─ Open utun/Wintun/tun device
├─ UDP socket → server endpoint
├─ configureInterface() → routes
└─ I/O threads (tunToUdp, udpToTun, ticker)
│
▼
WireGuard handshake completes
│
▼
udpToTunThread emits tunnelUp() on first decrypted packet
│
▼
RelayClient.fetchHosts() → GET /api/hosts
│
▼
DragonRelayView shows host list
│
User clicks "Stream"
│
▼
dragonRelay.streamHost(ip, "Desktop")
(launches Moonlight streaming session via QProcess)
Privilege model
| Operation | macOS | Windows | Linux |
|---|---|---|---|
| Open TUN device | none | Admin (Wintun driver) | CAP_NET_ADMIN |
| Configure address + routes | one osascript dialog | none (IP Helper API) | CAP_NET_ADMIN |
| Streaming (RTSP/UDP) | none | none | none |
The macOS dialog appears once when the VPN first connects; subsequent reconnects in the same session reuse the saved original-gateway and only need the dialog again if the interface was torn down.
Production path (macOS): Replace the osascript approach with a signed,
sandboxed SMJobBless privileged helper or a NetworkExtension PacketTunnelProvider
entitlement (requires Apple developer approval).
Roadmap
DragonRelayBackendC++ ↔ QML bridgeDragonRelayViewis the navigation entry point exposed to QML- Windows support (
tunnelmanager_win.cppusing Wintun) - Linux support (
tunnelmanager_linux.cppusing boringtun userspace +/dev/net/tun) tunnelUp()signal from first received packet (with 1500 ms fallback)- Multi-monitor / display selection (Sunshine API integration via DragonDisplayPicker)
- Replace
osascriptwith SMJobBless privileged helper on macOS - Replace plain-text password storage with system keychain (QKeychain)
- Auto-reconnect on network change
Notes for upstream maintainers
app/main.cppis the entry point; it instantiates aDragonRelayBackendand registers it as thedragonRelaycontext property before loadingmain.qml.- All Dragon-specific files live under
app/vpn/,app/gui/Dragon*, andscripts/. Upstreamapp/main.qmlis unmodified — DragonRelayView is loaded on demand by user action in the existing PcView navigation stack. - The boringtun FFI header is identical to the one used by Artemis; both projects link the same boringtun build artifact and the FFI surface must stay in lock-step.