Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dc6b1a5
Add features, settings and ui bits for Anti-Censorship features
encrypt94 May 12, 2026
3191a8c
Introduce a new Rust crate at `obfuscators/` exposing a C ABI via cbi…
encrypt94 May 13, 2026
62f190b
update lockfile
encrypt94 May 13, 2026
785169b
bump cargo to 1.89 in base image to user version 4 lockfile
encrypt94 May 13, 2026
211a452
update obfuscators crate to provide also a standalone executable and …
encrypt94 May 29, 2026
42da0fa
add helper to build rust binries in rustlang.cmake, refactor the rust…
encrypt94 May 29, 2026
0e52aaf
add obfuscators support for android using JNA, VPNService call the ob…
encrypt94 Jun 1, 2026
2fe705c
update cargo lockfile
encrypt94 Jun 1, 2026
bdbeea2
install cargo 1.91 instead of 1.8* due to 'invalid version 0 on git_p…
encrypt94 Jun 1, 2026
a26d560
bump minimum rust version to 1.89. udp over tcp requires 1.87, but ub…
encrypt94 Jun 1, 2026
9d37f45
bump minimum rust version to 1.89. udp over tcp requires 1.87, time r…
encrypt94 Jun 1, 2026
dbfe054
correctly link /usr/bin/cargo and /usr/bin/rustc
encrypt94 Jun 1, 2026
34ec956
keep linter happy
encrypt94 Jun 1, 2026
d3d8898
rustc is a symlink
encrypt94 Jun 1, 2026
4dcdada
step back from add_rust_binary creating an IMPORTED executable as it …
encrypt94 Jun 1, 2026
c513cb7
bump rust to 1.91
encrypt94 Jun 1, 2026
50666a3
update flatpak crates
encrypt94 Jun 2, 2026
f1c518d
simpler rust target install, avoid toolchain mismatch on minor versions
encrypt94 Jun 2, 2026
ef8ab9b
replace the single-file obfuscator with an abstract one and add a no-…
encrypt94 Jun 2, 2026
30fca1b
cleanup, remove cbindgen stuff, we don't use cffi anymore
encrypt94 Jun 2, 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
529 changes: 475 additions & 54 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"signature",
"obfuscators",
]
resolver = "2"

1 change: 1 addition & 0 deletions android/daemon/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,5 @@ dependencies {

implementation project(path: ':common')
implementation project(path: ':tunnel')
implementation project(path: ':obfuscator')
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.wireguard.crypto.Key
import org.json.JSONObject
import org.mozilla.firefox.qt.common.CoreBinder
import org.mozilla.firefox.qt.common.Prefs
import org.mozilla.firefox.vpn.obfuscator.Obfuscator
import org.mozilla.guardian.tunnel.WireGuardGo
import java.util.*

Expand All @@ -30,6 +31,7 @@ class VPNService : android.net.VpnService() {
private var mAlreadyInitialised = false
val mConnectionHealth = ConnectionHealth(this)
private var mCityname = ""
private var mObfuscator: Obfuscator? = null
private val mBackgroundPingTimerMSec: Long = 3 * 60 * 60 * 1000 // 3 hours, in milliseconds
private val mShortTimerBackgroundPingMSec: Long = 3 * 60 * 1000 // 3 minutes, in milliseconds

Expand Down Expand Up @@ -223,9 +225,31 @@ class VPNService : android.net.VpnService() {
}
Log.sensitive(tag, json.toString())
val jServer: JSONObject = getNextServerConfig(json, useFallbackServer)
val wireguard_conf = buildWireguardConfig(jServer, json)

// If an obfuscator is configured, start its proxy now and rewrite the
// WireGuard endpoint to its loopback port. The relay's outbound socket
// is protected below (after the tunnel is up) so its packets bypass
// the VPN.
val obfuscationMethodRaw = json.optInt("obfuscationMethod", 0)
val obfuscationMethod = Obfuscator.Method.fromValue(obfuscationMethodRaw)
Log.i(tag, "Attempt to start obfuscator '$obfuscationMethodRaw'")
val obfuscator: Obfuscator? = if (obfuscationMethod != null && obfuscationMethod != Obfuscator.Method.NoObfuscation) {
val r = Obfuscator.start(
method = obfuscationMethod,
serverIpv4 = jServer.optString("ipv4AddrIn").ifEmpty { null },
serverIpv6 = jServer.optString("ipv6AddrIn").ifEmpty { null },
serverPort = jServer.getInt("port"),
serverPublicKey = jServer.optString("publicKey").ifEmpty { null },
publicKey = json.optJSONObject("device")?.optString("publicKey")?.ifEmpty { null },
) ?: throw Error("Failed to start obfuscator method: $obfuscationMethod")
Log.i(tag, "Obfuscator '$obfuscationMethod' listening on 127.0.0.1:${r.localPort}")
r
} else { null }

val wireguard_conf = buildWireguardConfig(jServer, json, obfuscator)
val wgConfig: String = wireguard_conf.toWgUserspaceString()
if (wgConfig.isEmpty()) {
obfuscator?.stop()
throw Error("WG_Userspace config is empty, can't continue")
}
mCityname = json.getString("city")
Expand All @@ -247,24 +271,35 @@ class VPNService : android.net.VpnService() {
builder.establish().use { tun ->
if (tun == null) {
Log.e(tag, "Activation Error: did not get a TUN handle")
obfuscator?.stop()
return
}
// We should have everything to establish a new connection, turn down the old tunnel
// now.
if (currentTunnelHandle != -1) {
Log.i(tag, "Currently have a connection, close old handle")
WireGuardGo.wgTurnOff(currentTunnelHandle)
mObfuscator?.stop()
mObfuscator = null
}
currentTunnelHandle = WireGuardGo.wgTurnOn("mvpn0", tun.detachFd(), wgConfig)
}
if (currentTunnelHandle < 0) {
obfuscator?.stop()
throw Error("Activation Error Wireguard-Error -> $currentTunnelHandle")
} else {
Log.i(tag, "Updated tunnel handle to: " + currentTunnelHandle)
}
protect(WireGuardGo.wgGetSocketV4(currentTunnelHandle))
protect(WireGuardGo.wgGetSocketV6(currentTunnelHandle))

// Mark the obfuscator's outbound socket so its packets bypass the VPN.
obfuscator?.let {
if (it.socketV4 >= 0) protect(it.socketV4)
if (it.socketV6 >= 0) protect(it.socketV6)
mObfuscator = it
}

mConfig = json
// We don't want to update connection health in several situations:
// - If this is an app-caused server switch.
Expand Down Expand Up @@ -355,6 +390,8 @@ class VPNService : android.net.VpnService() {

WireGuardGo.wgTurnOff(currentTunnelHandle)
currentTunnelHandle = -1
mObfuscator?.stop()
mObfuscator = null
// If the client is "dead", on a disconnect the
// message won't be updated to 'you disconnected from X'
// so we should get rid of it. :)
Expand Down Expand Up @@ -418,14 +455,23 @@ class VPNService : android.net.VpnService() {
* Create a Wireguard [Config] from a [json] string - The [json] will be created in
* AndroidController.cpp
*/
private fun buildWireguardConfig(jServer: JSONObject, obj: JSONObject): Config {
private fun buildWireguardConfig(
jServer: JSONObject,
obj: JSONObject,
obfuscator: Obfuscator? = null,
): Config {
val confBuilder = Config.Builder()

val peerBuilder = Peer.Builder()
val ep =
val ep = if (obfuscator != null) {
// Send WireGuard traffic to the local obfuscator instead of the
// real server
InetEndpoint.parse("127.0.0.1:${obfuscator.localPort}")
} else {
InetEndpoint.parse(
jServer.getString("ipv4AddrIn") + ":" + jServer.getString("port"),
)
}
peerBuilder.setEndpoint(ep)
peerBuilder.setPublicKey(Key.fromBase64(jServer.getString("publicKey")))

Expand Down
45 changes: 45 additions & 0 deletions android/obfuscator/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

plugins {
id("com.android.library")
id("kotlin-android")
}

android {
namespace = "org.mozilla.firefox.vpn.obfuscator"
compileSdk = Config.compileSdkVersion
ndkVersion = Config.ndkVersion

defaultConfig {
minSdk = Config.minSdkVersion
targetSdk = Config.targetSdkVersion
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}

buildFeatures {
buildConfig = false
}

buildTypes {
debug {
isMinifyEnabled = false
}
release {
isMinifyEnabled = false
}
}
}

dependencies {
implementation("net.java.dev.jna:jna:5.18.1@aar")
}
3 changes: 3 additions & 0 deletions android/obfuscator/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.firefox.vpn.obfuscator

import com.sun.jna.Callback
import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.Structure

/**
* Kotlin wrapper around the `obfuscators` Rust library.
*
* Usage from VPNService:
* val obfuscator = Obfuscator.start(
* method = Obfuscator.Method.UdpOverTcp,
* serverIpv4 = "1.2.3.4",
* serverPort = 51820,
* ...
* )
* protect(obfuscator.socketV4)
* protect(obfuscator.socketV6)
* // rewrite WG endpoint -> 127.0.0.1:relay.localPort
* obfuscator.stop()
*/
class Obfuscator private constructor(
private val handle: Pointer,
val localPort: Int,
val socketV4: Int,
val socketV6: Int,
) {
@Volatile
private var stopped = false

fun stop() {
synchronized(this) {
if (stopped) return
stopped = true
LIB.obfuscator_stop(handle)
}
}

/** Mirrors `ObfuscationMethod` in obfuscators/src/obfuscator.rs. */
enum class Method(val value: Int) {
NoObfuscation(0),
Lwo(1),
Masque(2),
UdpOverTcp(3),
Shadowsocks(4),
;

companion object {
fun fromValue(v: Int): Method? = values().firstOrNull { it.value == v }
}
}

companion object {
/** Mirrors `ObfuscatorConfig` in obfuscators/src/obfuscator.rs. */
@Structure.FieldOrder(
"obfuscation_method",
"server_ipv4_addr_in",
"server_ipv6_addr_in",
"server_port",
"listen_port",
"server_public_key",
"public_key",
)
open class Config : Structure() {
@JvmField var obfuscation_method: Int = 0
@JvmField var server_ipv4_addr_in: String? = null
@JvmField var server_ipv6_addr_in: String? = null
@JvmField var server_port: Short = 0
@JvmField var listen_port: Short = 0
@JvmField var server_public_key: String? = null
@JvmField var public_key: String? = null

class ByReference : Config(), Structure.ByReference
}

interface LogCallback : Callback {
fun invoke(level: Int, message: Pointer)
}

fun interface LogHandler {
fun onLog(level: Int, message: String)
}

private interface Lib : Library {
fun obfuscator_start(cfg: Config.ByReference): Pointer?
fun obfuscator_local_port(handle: Pointer): Short
fun obfuscator_socket_v4(handle: Pointer): Int
fun obfuscator_socket_v6(handle: Pointer): Int
fun obfuscator_stop(handle: Pointer)
fun obfuscators_set_log_handler(handler: LogCallback)
}

private val LIB: Lib by lazy {
Native.load("obfuscators", Lib::class.java)
}

// Avoid log callback being collected by GC
@Volatile
private var logCallback: LogCallback? = null

// Install the log handler
fun setLogHandler(handler: LogHandler) {
val cb = object : LogCallback {
override fun invoke(level: Int, message: Pointer) {
handler.onLog(level, message.getString(0))
}
}
logCallback = cb
LIB.obfuscators_set_log_handler(cb)
}

// Run the obfuscator, returns null on failure
fun start(
method: Method,
serverIpv4: String?,
serverIpv6: String?,
serverPort: Int,
serverPublicKey: String? = null,
publicKey: String? = null,
): Obfuscator? {
val cfg = Config.ByReference().apply {
this.obfuscation_method = method.value
this.server_ipv4_addr_in = serverIpv4
this.server_ipv6_addr_in = serverIpv6
this.server_port = serverPort.toShort()
this.server_public_key = serverPublicKey
this.public_key = publicKey
}
val handle = LIB.obfuscator_start(cfg) ?: return null
val localPort = LIB.obfuscator_local_port(handle).toInt() and 0xFFFF
val socketV4 = LIB.obfuscator_socket_v4(handle)
val socketV6 = LIB.obfuscator_socket_v6(handle)
return Obfuscator(handle, localPort, socketV4, socketV6)
}
}
}
1 change: 1 addition & 0 deletions android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ buildCache {

include ':tunnel'
include ':daemon'
include ':obfuscator'
include ':ClientCommon'
include ':vpnClient'
include ':common'
10 changes: 5 additions & 5 deletions env-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ dependencies:
- ccache=4.10.1
- python=3.12
- pip=25.0
- rust=1.85
- rust=1.91
- go=1.24.5
- rust-std-armv7-linux-androideabi=1.85
- rust-std-x86_64-linux-android=1.85
- rust-std-i686-linux-android=1.85
- rust-std-aarch64-linux-android=1.85
- rust-std-armv7-linux-androideabi=1.91
- rust-std-x86_64-linux-android=1.91
- rust-std-i686-linux-android=1.91
- rust-std-aarch64-linux-android=1.91
- cmake=4.2.1
- ninja=1.11.0
- conda-forge::openjdk=17
Expand Down
10 changes: 5 additions & 5 deletions env-apple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ dependencies:
- python=3.12
- nodejs=20.*
- pip=25.0
- rust=1.85
- rust-std-aarch64-apple-darwin=1.85
- rust-std-x86_64-apple-darwin=1.85
- rust-std-aarch64-apple-ios=1.85
- rust-std-x86_64-apple-ios=1.85
- rust=1.91
- rust-std-aarch64-apple-darwin=1.91
- rust-std-x86_64-apple-darwin=1.91
- rust-std-aarch64-apple-ios=1.91
- rust-std-x86_64-apple-ios=1.91
- go=1.23
- compiler-rt=20.1.8
- compiler-rt_osx-64
Expand Down
4 changes: 2 additions & 2 deletions env-wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ dependencies:
- python=3.12
- nodejs=20.*
- pip=25.0
- rust=1.85
- rust-std-wasm32-unknown-emscripten=1.85
- rust=1.91
- rust-std-wasm32-unknown-emscripten=1.91
- cmake=3.26.3
- ninja=1.11.0
- pip:
Expand Down
6 changes: 3 additions & 3 deletions env-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ dependencies:
- python=3.12
- nodejs=20.*
- pip=25.0
- rust=1.85.0
- rust-std-aarch64-pc-windows-msvc=1.85.0
- rust-std-x86_64-pc-windows-msvc=1.85.0
- rust=1.91.0
- rust-std-aarch64-pc-windows-msvc=1.91.0
- rust-std-x86_64-pc-windows-msvc=1.91.0
- compiler-rt=20.1.8
- compiler-rt_win-64
- cmake=4.2.1
Expand Down
Loading
Loading