Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9008f4c
masterdnsvpn: add enum entries + protocol constants
May 10, 2026
813d891
masterdnsvpn: add protocol-config model + variant wiring
May 10, 2026
6501c60
masterdnsvpn: QML page + ConfigModel + controller wiring
May 10, 2026
dd962da
masterdnsvpn: unit tests for protocol-config model
May 10, 2026
2dd5491
masterdnsvpn: switch model to pure structured JSON
May 10, 2026
0cc95e2
masterdnsvpn: engine sibling dir + spec doc + crypto + SOCKS5
May 10, 2026
b92e416
masterdnsvpn: wire framing + DNS framing layers
May 10, 2026
5b5816f
masterdnsvpn: per-stream ARQ reliability layer
May 10, 2026
f51fb87
masterdnsvpn: resolver pool + 8 balancing strategies
May 10, 2026
5d9011d
masterdnsvpn: session/dispatcher — full stack assembly
May 11, 2026
b428796
masterdnsvpn: Engine facade + service-side host + desktop protocol wi…
May 11, 2026
00fcb8f
masterdnsvpn: Android JNI bridge to native engine
May 11, 2026
1e3179e
masterdnsvpn: per-layer engine unit tests
May 11, 2026
146b017
masterdnsvpn: resolve §13 open questions against upstream Go
May 11, 2026
78281ec
masterdnsvpn: tiered ping pacing per §12
May 11, 2026
2a9c5ef
masterdnsvpn: MTU probe state machine (upload + download binary search)
May 11, 2026
a810a35
masterdnsvpn: wire §9 MTU probe sweep into Session + ResolverPool
May 11, 2026
6ce33aa
masterdnsvpn: implement §8 compression codecs (ZSTD, LZ4, ZLIB-raw)
May 11, 2026
371c032
masterdnsvpn: translate internal/arq/arq_test.go to QTest (parity suite)
May 11, 2026
ead7a49
masterdnsvpn: translate vpnproto + dnsparser tests to QTest (parity s…
May 11, 2026
1c12a44
masterdnsvpn: inventory balancer + socksproto + mtu/ping tests (QSKIP)
May 11, 2026
058d9fe
masterdnsvpn: translate basecodec + security + compression + enum tests
May 11, 2026
b71ae70
masterdnsvpn: close ARQ port gaps (bounded NACK, NACK cooldown, adapt…
May 11, 2026
1160406
masterdnsvpn: cap m_rcvBuf at 2x windowSize (receive-window enforcement)
May 11, 2026
30a50a3
masterdnsvpn: port SessionAcceptClientPolicy (13-byte policy tail)
May 11, 2026
51e6dea
masterdnsvpn: balancer Report* API + DNS constants export
May 11, 2026
86ea2cc
masterdnsvpn: extract ParseTargetPayload as standalone helper
May 11, 2026
de158d2
masterdnsvpn: SOCKS5 UDP datagram + target-payload codecs
May 11, 2026
a35b9ab
masterdnsvpn: add m_controlSndBuf + control-plane adaptive RTO
May 11, 2026
b239bbf
masterdnsvpn: apply ServerPolicy + seed controlSndBuf send-side
May 11, 2026
41d0d22
masterdnsvpn: control-plane RTO retransmit (closes reliability loop)
May 11, 2026
bb554d5
masterdnsvpn: expose ARQ + ping tunables to operator JSON config
May 11, 2026
07cdc6a
masterdnsvpn: DNS query layer + SOCKS5 UDP ASSOCIATE listener
May 11, 2026
2249527
masterdnsvpn: address Codacy review findings
May 11, 2026
ea3c092
masterdnsvpn: drop redundant TCP probe in waitForSocksListener
May 11, 2026
5b2e524
Merge remote-tracking branch 'upstream/dev' into feature/masterdnsvpn…
May 18, 2026
af11ed3
masterdnsvpn: validate length in readExact (CWE-120/CWE-20)
May 18, 2026
836ba53
fix(masterdnsvpn): include QMetaType for Q_DECLARE_METATYPE
May 18, 2026
7750037
fix(masterdnsvpn): make code build on Qt 6.10 / GCC 13 / MSVC 2022
May 18, 2026
7327eb1
test(masterdnsvpn): include resolverpool.h in engine test
May 18, 2026
79ab361
fix(service): link missing masterdnsvpn TUs + zlib/zstd/lz4
May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions client/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,20 @@ endif()

find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})

# MasterDnsVPN engine compression layer (§8) — zlib raw deflate, zstd, lz4.
# Pulled in via conan (see conanfile.py).
find_package(ZLIB REQUIRED)
find_package(zstd REQUIRED)
find_package(lz4 REQUIRED)

set(LIBS ${LIBS}
Qt6::Core Qt6::Gui
Qt6::Network Qt6::Xml Qt6::RemoteObjects
Qt6::Quick Qt6::Svg Qt6::QuickControls2
Qt6::Core5Compat Qt6::Concurrent
ZLIB::ZLIB
zstd::libzstd_static
lz4::lz4
)

if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
Expand Down Expand Up @@ -119,6 +128,7 @@ endif()
include_directories(mozilla)
include_directories(mozilla/shared)
include_directories(mozilla/models)
include_directories(masterdnsvpn)

configure_file(${CMAKE_CURRENT_LIST_DIR}/../version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)

Expand Down
13 changes: 13 additions & 0 deletions client/android/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,19 @@
</intent-filter>
</service>

<service
android:name=".MasterDnsVpnService"
android:process=":amneziaMasterDnsVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="systemExempted"
android:exported="false"
tools:ignore="ForegroundServicePermission">

<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>

<service
android:name=".AmneziaTileService"
android:process=":amneziaTileService"
Expand Down
1 change: 1 addition & 0 deletions client/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dependencies {
implementation(project(":awg"))
implementation(project(":openvpn"))
implementation(project(":xray"))
implementation(project(":master_dns_vpn"))
implementation(libs.androidx.core)
implementation(libs.androidx.activity)
implementation(libs.androidx.fragment)
Expand Down
22 changes: 22 additions & 0 deletions client/android/master_dns_vpn/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
id(libs.plugins.kotlin.android.get().pluginId)
}

kotlin {
jvmToolchain(17)
}

android {
namespace = "org.amnezia.vpn.protocol.masterdnsvpn"
}

dependencies {
compileOnly(project(":utils"))
compileOnly(project(":protocolApi"))
// tun2socks reused from libxray.aar — same binary, no second copy
// bundled. The Kotlin Protocol class calls LibXray.startTun2Socks()
// pointing at the SOCKS5 the native engine exposes.
implementation(project(":xray:libXray"))
implementation(libs.kotlinx.coroutines)
}
209 changes: 209 additions & 0 deletions client/android/master_dns_vpn/src/main/kotlin/MasterDnsVpn.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package org.amnezia.vpn.protocol.masterdnsvpn

import android.net.VpnService.Builder
import org.amnezia.vpn.protocol.BadConfigException
import org.amnezia.vpn.protocol.Protocol
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
import org.amnezia.vpn.protocol.Statistics
import org.amnezia.vpn.protocol.VpnStartException
import org.amnezia.vpn.protocol.xray.libXray.LibXray
import org.amnezia.vpn.protocol.xray.libXray.Tun2SocksConfig
import org.amnezia.vpn.util.Log
import org.amnezia.vpn.util.net.InetNetwork
import org.amnezia.vpn.util.net.parseInetAddress
import org.json.JSONObject

private const val TAG = "MasterDnsVpn"

/**
* Android-side glue that bridges the Amnezia VpnService lifecycle to the
* native MasterDnsVPN engine.
*
* Architecture:
*
* AmneziaVpnService → MasterDnsVpn (this class)
* → MasterDnsVpnNative.nativeStart(configJson) // C++ engine spins up,
* // binds 127.0.0.1:<port>
* → poll MasterDnsVpnNative.nativeSocksPort() // wait for listener
* → buildVpnInterface(...) // VpnService.Builder TUN
* → LibXray.startTun2Socks(...) // tun2socks dials SOCKS5
*
* The native engine and tun2socks both live in the same process — no IPC,
* no subprocess, no bundled binary. tun2socks comes from libxray.aar (a
* dependency Amnezia already ships for the Xray protocol); we reuse its
* tun2socks engine rather than vendoring a second copy.
*/
class MasterDnsVpn : Protocol() {

private var isRunning: Boolean = false
override val statistics: Statistics = Statistics.EMPTY_STATISTICS

override fun internalInit() {
// No persistent native init: the engine instance is created fresh
// on every startVpn() and discarded on stopVpn().
}

override suspend fun startVpn(
config: JSONObject,
vpnBuilder: Builder,
protect: (Int) -> Boolean
) {
if (isRunning) {
Log.w(TAG, "MasterDnsVpn already running")
return
}

val configData = config.optJSONObject("masterdnsvpn_config_data")
?: throw BadConfigException("masterdnsvpn_config_data not found")

if (!MasterDnsVpnNative.nativeStart(configData.toString())) {
val err = MasterDnsVpnNative.nativeLastError()
throw VpnStartException("Failed to start MasterDnsVpn engine: $err")
}

// Engine binds the SOCKS5 listener asynchronously after start();
// tun2socks must not race the listener. Poll until non-zero or
// we hit the deadline.
val socksPort = waitForSocksListener()
if (socksPort == 0) {
MasterDnsVpnNative.nativeStop()
throw VpnStartException("MasterDnsVpn engine did not bind a SOCKS5 listener")
}
Log.d(TAG, "MasterDnsVpn engine listening on 127.0.0.1:$socksPort")

val mdnsvpnConfig = parseConfig(config, configData, socksPort)

var startupSucceeded = false
try {
buildVpnInterface(mdnsvpnConfig, vpnBuilder)
vpnBuilder.establish().use { tunFd ->
if (tunFd == null) {
throw VpnStartException(
"Create VPN interface: permission not granted or revoked"
)
}
Log.d(TAG, "Run tun2Socks (SOCKS5 backend = native MasterDnsVpn engine)")
runTun2Socks(mdnsvpnConfig, tunFd.detachFd())
}
startupSucceeded = true
} finally {
if (!startupSucceeded) {
MasterDnsVpnNative.nativeStop()
}
}

state.value = CONNECTED
isRunning = true
}

override fun stopVpn() {
LibXray.stopTun2Socks().isNotNullOrBlank { err ->
Log.e(TAG, "Failed to stop tun2Socks: $err")
}
MasterDnsVpnNative.nativeStop()
isRunning = false
state.value = DISCONNECTED
}

override fun reconnectVpn(vpnBuilder: Builder, protect: (Int) -> Boolean) {
// Engine handles its own resilience (ARQ retries, per-resolver
// failover). From Amnezia's perspective the tunnel is up as long
// as tun2socks + the engine are alive.
state.value = CONNECTED
}

private fun parseConfig(
config: JSONObject,
configData: JSONObject,
socksPort: Int
): MasterDnsVpnConfig {
return MasterDnsVpnConfig.build {
addAddress(MasterDnsVpnConfig.DEFAULT_IPV4_ADDRESS)

config.optString("dns1").let {
if (it.isNotBlank()) addDnsServer(parseInetAddress(it))
}
config.optString("dns2").let {
if (it.isNotBlank()) addDnsServer(parseInetAddress(it))
}

// Default-route both v4 and v6 — MasterDnsVPN is intended as a
// full-tunnel transport for environments where only DNS leaves
// the network.
addRoute(InetNetwork("0.0.0.0", 0))
addRoute(InetNetwork("2000::0", 3))

// Carve the operator's server hostname out of the tunnel so the
// underlying DNS queries can still reach it.
config.optString("hostName").let {
if (it.isNotBlank()) excludeRoute(InetNetwork(it, 32))
}

config.optString("mtu").let {
if (it.isNotBlank()) setMtu(it.toInt())
}

setSocksPort(socksPort)

configSplitTunneling(config)
configAppSplitTunneling(config)
}
}

private fun runTun2Socks(config: MasterDnsVpnConfig, fd: Int) {
// Engine binds 127.0.0.1 only; no auth needed (the device-to-engine
// hop never leaves the loopback interface).
val proxyUrl = "socks5://127.0.0.1:${config.socksPort}"
val tun2SocksConfig = Tun2SocksConfig().apply {
mtu = config.mtu.toLong()
proxy = proxyUrl
device = "fd://$fd"
logLevel = "warn"
}
LibXray.startTun2Socks(tun2SocksConfig, fd.toLong()).isNotNullOrBlank { err ->
throw VpnStartException("Failed to start tun2socks for MasterDnsVpn: $err")
}
}

private fun waitForSocksListener(): Int {
// Up to 60 s — the engine's MTU discovery + handshake can take a
// bit on a fresh connect. The native engine publishes a non-zero
// SOCKS5 port only after its underlying QTcpServer.listen() has
// succeeded, so any positive value already means "accepting
// connections" (QTcpServer.serverPort() returns 0 until listen
// returns true). No follow-up TCP probe required.
val deadline = System.currentTimeMillis() + SOCKS_WAIT_TIMEOUT_MS
while (System.currentTimeMillis() < deadline) {
val port = MasterDnsVpnNative.nativeSocksPort()
if (port > 0) {
return port
}
try {
Thread.sleep(SOCKS_POLL_INTERVAL_MS.toLong())
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
return 0
}
}
return 0
}

/**
* Process-wide singleton plus the timing constants for the SOCKS5
* listener handshake. Mirrors the pattern used by the Xray protocol
* module in the same package.
*/
companion object {
private const val SOCKS_WAIT_TIMEOUT_MS = 60_000
private const val SOCKS_POLL_INTERVAL_MS = 250

val instance: MasterDnsVpn by lazy { MasterDnsVpn() }
}
}

private fun String?.isNotNullOrBlank(block: (String) -> Unit) {
if (!this.isNullOrBlank()) {
block(this)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.amnezia.vpn.protocol.masterdnsvpn

import org.amnezia.vpn.protocol.ProtocolConfig
import org.amnezia.vpn.util.net.InetNetwork

private const val MASTER_DNS_VPN_DEFAULT_MTU = 1500

/**
* Per-connection configuration for the MasterDnsVPN protocol on Android.
*
* Built from the JSON received from the desktop/IPC layer via the
* [Builder]. The `socksPort` is filled in at runtime after the native
* engine binds its listener (it cannot be predicted at config time
* because the engine asks the OS for an ephemeral port).
*/
class MasterDnsVpnConfig protected constructor(
protocolConfigBuilder: ProtocolConfig.Builder,
val socksPort: Int,
) : ProtocolConfig(protocolConfigBuilder) {

protected constructor(builder: Builder) : this(
builder,
builder.socksPort,
)

/**
* Mutable accumulator for [MasterDnsVpnConfig] fields. Inherits the
* generic protocol-config fields (addresses, routes, DNS, MTU,
* split-tunnel rules) from [ProtocolConfig.Builder] and adds the one
* MasterDnsVPN-specific field, [socksPort].
*/
class Builder : ProtocolConfig.Builder(false) {
internal var socksPort: Int = 0
private set

override var mtu: Int = MASTER_DNS_VPN_DEFAULT_MTU

/** Set the loopback SOCKS5 listener port published by the engine. */
fun setSocksPort(port: Int) = apply { socksPort = port }

override fun build(): MasterDnsVpnConfig =
configBuild().run { MasterDnsVpnConfig(this@Builder) }
}

/**
* Static defaults and the DSL-style [build] entry point.
*/
companion object {
// /30 private subnet for the TUN interface; matches the /30 trick
// XrayConfig uses to give us a gateway/.1 + TUN local/.2 pair that
// doesn't collide with typical LAN ranges.
internal val DEFAULT_IPV4_ADDRESS: InetNetwork = InetNetwork("10.0.43.2", 30)

/** Apply [block] to a fresh [Builder] and produce the config. */
inline fun build(block: Builder.() -> Unit): MasterDnsVpnConfig =
Builder().apply(block).build()
}
}
Loading