() {
16 | fun containsKey(key: K) = indexOfKey(key) >= 0
17 |
18 | operator fun get(key: K): E? {
19 | val index = indexOfKey(key)
20 | return if (index >= 0) get(index) else null
21 | }
22 |
23 | open fun indexOfKey(key: K): Int {
24 | val iterator = listIterator()
25 | while (iterator.hasNext()) {
26 | val index = iterator.nextIndex()
27 | if (iterator.next()!!.key == key)
28 | return index
29 | }
30 | return -1
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/log_viewer_activity.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
12 |
13 |
19 |
20 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android
6 |
7 | import android.content.BroadcastReceiver
8 | import android.content.Context
9 | import android.content.Intent
10 | import android.util.Log
11 | import com.wireguard.android.backend.WgQuickBackend
12 | import com.wireguard.android.util.applicationScope
13 | import kotlinx.coroutines.launch
14 |
15 | class BootShutdownReceiver : BroadcastReceiver() {
16 | override fun onReceive(context: Context, intent: Intent) {
17 | val action = intent.action ?: return
18 | applicationScope.launch {
19 | if (Application.getBackend() !is WgQuickBackend) return@launch
20 | val tunnelManager = Application.getTunnelManager()
21 | if (Intent.ACTION_BOOT_COMPLETED == action) {
22 | Log.i(TAG, "Broadcast receiver restoring state (boot)")
23 | tunnelManager.restoreState(false)
24 | } else if (Intent.ACTION_SHUTDOWN == action) {
25 | Log.i(TAG, "Broadcast receiver saving state (shutdown)")
26 | tunnelManager.saveState()
27 | }
28 | }
29 | }
30 |
31 | companion object {
32 | private const val TAG = "WireGuard/BootShutdownReceiver"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/crypto/KeyPair.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.crypto;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | /**
11 | * Represents a Curve25519 key pair as used by WireGuard.
12 | *
13 | * Instances of this class are immutable.
14 | */
15 | @NonNullForAll
16 | public class KeyPair {
17 | private final Key privateKey;
18 | private final Key publicKey;
19 |
20 | /**
21 | * Creates a key pair using a newly-generated private key.
22 | */
23 | public KeyPair() {
24 | this(Key.generatePrivateKey());
25 | }
26 |
27 | /**
28 | * Creates a key pair using an existing private key.
29 | *
30 | * @param privateKey a private key, used to derive the public key
31 | */
32 | public KeyPair(final Key privateKey) {
33 | this.privateKey = privateKey;
34 | publicKey = Key.generatePublicKey(privateKey);
35 | }
36 |
37 | /**
38 | * Returns the private key from the key pair.
39 | *
40 | * @return the private key
41 | */
42 | public Key getPrivateKey() {
43 | return privateKey;
44 | }
45 |
46 | /**
47 | * Returns the public key from the key pair.
48 | *
49 | * @return the public key
50 | */
51 | public Key getPublicKey() {
52 | return publicKey;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tunnel/tools/libwg-go/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
2 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
3 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
4 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
5 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
6 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
7 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
8 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
9 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
10 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
11 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
12 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
13 | golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
14 | golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
15 | gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
16 | gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
17 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/ClipboardUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.util
6 |
7 | import android.content.ClipData
8 | import android.content.ClipboardManager
9 | import android.os.Build
10 | import android.view.View
11 | import android.widget.TextView
12 | import androidx.core.content.getSystemService
13 | import com.google.android.material.snackbar.Snackbar
14 | import com.google.android.material.textfield.TextInputEditText
15 | import com.wireguard.android.R
16 |
17 | /**
18 | * Standalone utilities for interacting with the system clipboard.
19 | */
20 | object ClipboardUtils {
21 | @JvmStatic
22 | fun copyTextView(view: View) {
23 | val data = when (view) {
24 | is TextInputEditText -> Pair(view.editableText, view.hint)
25 | is TextView -> Pair(view.text, view.contentDescription)
26 | else -> return
27 | }
28 | if (data.first == null || data.first.isEmpty()) {
29 | return
30 | }
31 | val service = view.context.getSystemService() ?: return
32 | service.setPrimaryClip(ClipData.newPlainText(data.second, data.first))
33 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
34 | Snackbar.make(view, view.context.getString(R.string.copied_to_clipboard, data.second), Snackbar.LENGTH_LONG).show()
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/log_viewer_entry.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
12 |
13 |
22 |
23 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/config/ParseException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.config;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | import androidx.annotation.Nullable;
11 |
12 | /**
13 | *
14 | */
15 | @NonNullForAll
16 | public class ParseException extends Exception {
17 | private final Class> parsingClass;
18 | private final CharSequence text;
19 |
20 | public ParseException(final Class> parsingClass, final CharSequence text,
21 | @Nullable final String message, @Nullable final Throwable cause) {
22 | super(message, cause);
23 | this.parsingClass = parsingClass;
24 | this.text = text;
25 | }
26 |
27 | public ParseException(final Class> parsingClass, final CharSequence text,
28 | @Nullable final String message) {
29 | this(parsingClass, text, message, null);
30 | }
31 |
32 | public ParseException(final Class> parsingClass, final CharSequence text,
33 | @Nullable final Throwable cause) {
34 | this(parsingClass, text, null, cause);
35 | }
36 |
37 | public ParseException(final Class> parsingClass, final CharSequence text) {
38 | this(parsingClass, text, null, null);
39 | }
40 |
41 | public Class> getParsingClass() {
42 | return parsingClass;
43 | }
44 |
45 | public CharSequence getText() {
46 | return text;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/config_naming_dialog_fragment.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
22 |
23 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/widget/TvCardView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.widget
7 |
8 | import android.content.Context
9 | import android.util.AttributeSet
10 | import android.view.View
11 | import com.google.android.material.card.MaterialCardView
12 | import com.wireguard.android.R
13 |
14 | class TvCardView(context: Context?, attrs: AttributeSet?) : MaterialCardView(context, attrs) {
15 | var isUp: Boolean = false
16 | set(value) {
17 | field = value
18 | refreshDrawableState()
19 | }
20 | var isDeleting: Boolean = false
21 | set(value) {
22 | field = value
23 | refreshDrawableState()
24 | }
25 |
26 | override fun onCreateDrawableState(extraSpace: Int): IntArray {
27 | if (isUp || isDeleting) {
28 | val drawableState = super.onCreateDrawableState(extraSpace + (if (isUp) 1 else 0) + (if (isDeleting) 1 else 0))
29 | if (isUp) {
30 | View.mergeDrawableStates(drawableState, STATE_IS_UP)
31 | }
32 | if (isDeleting) {
33 | View.mergeDrawableStates(drawableState, STATE_IS_DELETING)
34 | }
35 | return drawableState
36 | }
37 | return super.onCreateDrawableState(extraSpace)
38 | }
39 |
40 | companion object {
41 | private val STATE_IS_UP = intArrayOf(R.attr.state_isUp)
42 | private val STATE_IS_DELETING = intArrayOf(R.attr.state_isDeleting)
43 | }
44 | }
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.13.0"
3 | kotlin = "2.2.20"
4 |
5 | [libraries]
6 | androidx-activity-ktx = "androidx.activity:activity-ktx:1.11.0"
7 | androidx-annotation = "androidx.annotation:annotation:1.9.1"
8 | androidx-appcompat = "androidx.appcompat:appcompat:1.7.1"
9 | androidx-biometric = "androidx.biometric:biometric:1.1.0"
10 | androidx-collection = "androidx.collection:collection:1.5.0"
11 | androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.2.1"
12 | androidx-coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.3.0"
13 | androidx-core-ktx = "androidx.core:core-ktx:1.17.0"
14 | androidx-datastore-preferences = "androidx.datastore:datastore-preferences:1.1.7"
15 | androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.8.9"
16 | androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.9.4"
17 | androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.1"
18 | desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.1.5"
19 | google-material = "com.google.android.material:material:1.13.0"
20 | jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
21 | junit = "junit:junit:4.13.2"
22 | kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
23 | zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0"
24 |
25 | [plugins]
26 | android-application = { id = "com.android.application", version.ref = "agp" }
27 | android-library = { id = "com.android.library", version.ref = "agp" }
28 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
29 | kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
30 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/widget/ToggleSwitch.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2013 The Android Open Source Project
3 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | package com.wireguard.android.widget
7 |
8 | import android.content.Context
9 | import android.os.Parcelable
10 | import android.util.AttributeSet
11 | import com.google.android.material.materialswitch.MaterialSwitch
12 |
13 | class ToggleSwitch @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : MaterialSwitch(context, attrs) {
14 | private var isRestoringState = false
15 | private var listener: OnBeforeCheckedChangeListener? = null
16 | override fun onRestoreInstanceState(state: Parcelable) {
17 | isRestoringState = true
18 | super.onRestoreInstanceState(state)
19 | isRestoringState = false
20 | }
21 |
22 | override fun setChecked(checked: Boolean) {
23 | if (checked == isChecked) return
24 | if (isRestoringState || listener == null) {
25 | super.setChecked(checked)
26 | return
27 | }
28 | isEnabled = false
29 | listener!!.onBeforeCheckedChanged(this, checked)
30 | }
31 |
32 | fun setCheckedInternal(checked: Boolean) {
33 | super.setChecked(checked)
34 | isEnabled = true
35 | }
36 |
37 | fun setOnBeforeCheckedChangeListener(listener: OnBeforeCheckedChangeListener?) {
38 | this.listener = listener
39 | }
40 |
41 | interface OnBeforeCheckedChangeListener {
42 | fun onBeforeCheckedChanged(toggleSwitch: ToggleSwitch?, checked: Boolean)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout-sw600dp/main_activity.xml:
--------------------------------------------------------------------------------
1 |
5 |
12 |
13 |
21 |
22 |
29 |
30 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.widget
6 |
7 | import android.content.Context
8 | import android.util.AttributeSet
9 | import android.view.View
10 | import android.widget.RelativeLayout
11 | import com.wireguard.android.R
12 |
13 | class MultiselectableRelativeLayout @JvmOverloads constructor(
14 | context: Context? = null,
15 | attrs: AttributeSet? = null,
16 | defStyleAttr: Int = 0,
17 | defStyleRes: Int = 0
18 | ) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
19 | private var multiselected = false
20 |
21 | override fun onCreateDrawableState(extraSpace: Int): IntArray {
22 | if (multiselected) {
23 | val drawableState = super.onCreateDrawableState(extraSpace + 1)
24 | View.mergeDrawableStates(drawableState, STATE_MULTISELECTED)
25 | return drawableState
26 | }
27 | return super.onCreateDrawableState(extraSpace)
28 | }
29 |
30 | fun setMultiSelected(on: Boolean) {
31 | if (!multiselected) {
32 | multiselected = true
33 | refreshDrawableState()
34 | }
35 | isActivated = on
36 | }
37 |
38 | fun setSingleSelected(on: Boolean) {
39 | if (multiselected) {
40 | multiselected = false
41 | refreshDrawableState()
42 | }
43 | isActivated = on
44 | }
45 |
46 | companion object {
47 | private val STATE_MULTISELECTED = intArrayOf(R.attr.state_multiselected)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.backend;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | import java.util.regex.Pattern;
11 |
12 | /**
13 | * Represents a WireGuard tunnel.
14 | */
15 |
16 | @NonNullForAll
17 | public interface Tunnel {
18 | int NAME_MAX_LENGTH = 15;
19 | Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_=+.-]{1,15}");
20 |
21 | static boolean isNameInvalid(final CharSequence name) {
22 | return !NAME_PATTERN.matcher(name).matches();
23 | }
24 |
25 | /**
26 | * Get the name of the tunnel, which should always pass the !isNameInvalid test.
27 | *
28 | * @return The name of the tunnel.
29 | */
30 | String getName();
31 |
32 | /**
33 | * React to a change in state of the tunnel. Should only be directly called by Backend.
34 | *
35 | * @param newState The new state of the tunnel.
36 | */
37 | void onStateChange(State newState);
38 |
39 | /**
40 | * Enum class to represent all possible states of a {@link Tunnel}.
41 | */
42 | enum State {
43 | DOWN,
44 | TOGGLE,
45 | UP;
46 |
47 | /**
48 | * Get the state of a {@link Tunnel}
49 | *
50 | * @param running boolean indicating if the tunnel is running.
51 | * @return State of the tunnel based on whether or not it is running.
52 | */
53 | public static State of(final boolean running) {
54 | return running ? UP : DOWN;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android GUI for [WireGuard](https://www.wireguard.com/)
2 |
3 | **[Download from the Play Store](https://play.google.com/store/apps/details?id=com.wireguard.android)**
4 |
5 | This is an Android GUI for [WireGuard](https://www.wireguard.com/). It [opportunistically uses the kernel implementation](https://git.zx2c4.com/android_kernel_wireguard/about/), and falls back to using the non-root [userspace implementation](https://git.zx2c4.com/wireguard-go/about/).
6 |
7 | ## Building
8 |
9 | ```
10 | $ git clone --recurse-submodules https://git.zx2c4.com/wireguard-android
11 | $ cd wireguard-android
12 | $ ./gradlew assembleRelease
13 | ```
14 |
15 | macOS users may need [flock(1)](https://github.com/discoteq/flock).
16 |
17 | ## Embedding
18 |
19 | The tunnel library is [on Maven Central](https://search.maven.org/artifact/com.wireguard.android/tunnel), alongside [extensive class library documentation](https://javadoc.io/doc/com.wireguard.android/tunnel).
20 |
21 | ```
22 | implementation 'com.wireguard.android:tunnel:$wireguardTunnelVersion'
23 | ```
24 |
25 | The library makes use of Java 8 features, so be sure to support those in your gradle configuration with [desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring):
26 |
27 | ```
28 | compileOptions {
29 | sourceCompatibility JavaVersion.VERSION_17
30 | targetCompatibility JavaVersion.VERSION_17
31 | coreLibraryDesugaringEnabled = true
32 | }
33 | dependencies {
34 | coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.3"
35 | }
36 | ```
37 |
38 | ## Translating
39 |
40 | Please help us translate the app into several languages on [our translation platform](https://crowdin.com/project/WireGuard).
41 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/DonatePreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.preference
7 |
8 | import android.content.Context
9 | import android.content.Intent
10 | import android.net.Uri
11 | import android.util.AttributeSet
12 | import android.widget.Toast
13 | import androidx.preference.Preference
14 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
15 | import com.wireguard.android.R
16 | import com.wireguard.android.updater.Updater
17 | import com.wireguard.android.util.ErrorMessages
18 | import androidx.core.net.toUri
19 |
20 | class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
21 | override fun getSummary() = context.getString(R.string.donate_summary)
22 |
23 | override fun getTitle() = context.getString(R.string.donate_title)
24 |
25 | override fun onClick() {
26 | /* Google Play Store forbids links to our donation page. */
27 | if (Updater.installerIsGooglePlay(context)) {
28 | MaterialAlertDialogBuilder(context)
29 | .setTitle(R.string.donate_title)
30 | .setMessage(R.string.donate_google_play_disappointment)
31 | .show()
32 | return
33 | }
34 |
35 | val intent = Intent(Intent.ACTION_VIEW)
36 | intent.data = "https://www.wireguard.com/donations/".toUri()
37 | try {
38 | context.startActivity(intent)
39 | } catch (e: Throwable) {
40 | Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tv_file_list_item.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
19 |
20 |
21 |
30 |
31 |
34 |
35 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.widget
6 |
7 | import android.text.InputFilter
8 | import android.text.SpannableStringBuilder
9 | import android.text.Spanned
10 | import com.wireguard.android.backend.Tunnel
11 |
12 | /**
13 | * InputFilter for entering WireGuard configuration names (Linux interface names).
14 | */
15 | class NameInputFilter : InputFilter {
16 | override fun filter(
17 | source: CharSequence,
18 | sStart: Int, sEnd: Int,
19 | dest: Spanned,
20 | dStart: Int, dEnd: Int
21 | ): CharSequence? {
22 | var replacement: SpannableStringBuilder? = null
23 | var rIndex = 0
24 | val dLength = dest.length
25 | for (sIndex in sStart until sEnd) {
26 | val c = source[sIndex]
27 | val dIndex = dStart + (sIndex - sStart)
28 | // Restrict characters to those valid in interfaces.
29 | // Ensure adding this character does not push the length over the limit.
30 | if (dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c) &&
31 | dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH
32 | ) {
33 | ++rIndex
34 | } else {
35 | if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
36 | replacement.delete(rIndex, rIndex + 1)
37 | }
38 | }
39 | return replacement
40 | }
41 |
42 | companion object {
43 | private fun isAllowed(c: Char) = Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0
44 |
45 | @JvmStatic
46 | fun newInstance() = NameInputFilter()
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/widget/KeyInputFilter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.widget
6 |
7 | import android.text.InputFilter
8 | import android.text.SpannableStringBuilder
9 | import android.text.Spanned
10 | import com.wireguard.crypto.Key
11 |
12 | /**
13 | * InputFilter for entering WireGuard private/public keys encoded with base64.
14 | */
15 | class KeyInputFilter : InputFilter {
16 | override fun filter(
17 | source: CharSequence,
18 | sStart: Int, sEnd: Int,
19 | dest: Spanned,
20 | dStart: Int, dEnd: Int
21 | ): CharSequence? {
22 | var replacement: SpannableStringBuilder? = null
23 | var rIndex = 0
24 | val dLength = dest.length
25 | for (sIndex in sStart until sEnd) {
26 | val c = source[sIndex]
27 | val dIndex = dStart + (sIndex - sStart)
28 | // Restrict characters to the base64 character set.
29 | // Ensure adding this character does not push the length over the limit.
30 | if ((dIndex + 1 < Key.Format.BASE64.length && isAllowed(c) ||
31 | dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
32 | dLength + (sIndex - sStart) < Key.Format.BASE64.length
33 | ) {
34 | ++rIndex
35 | } else {
36 | if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
37 | replacement.delete(rIndex, rIndex + 1)
38 | }
39 | }
40 | return replacement
41 | }
42 |
43 | companion object {
44 | private fun isAllowed(c: Char) = Character.isLetterOrDigit(c) || c == '+' || c == '/'
45 |
46 | @JvmStatic
47 | fun newInstance() = KeyInputFilter()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/config/Attribute.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.config;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | import java.util.Iterator;
11 | import java.util.Optional;
12 | import java.util.regex.Matcher;
13 | import java.util.regex.Pattern;
14 |
15 | @NonNullForAll
16 | public final class Attribute {
17 | private static final Pattern LINE_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^\\s#][^#]*)");
18 | private static final Pattern LIST_SEPARATOR = Pattern.compile("\\s*,\\s*");
19 |
20 | private final String key;
21 | private final String value;
22 |
23 | private Attribute(final String key, final String value) {
24 | this.key = key;
25 | this.value = value;
26 | }
27 |
28 | public static String join(final Iterable> values) {
29 | final Iterator> it = values.iterator();
30 | if (!it.hasNext()) {
31 | return "";
32 | }
33 | final StringBuilder sb = new StringBuilder();
34 | sb.append(it.next());
35 | while (it.hasNext()) {
36 | sb.append(", ");
37 | sb.append(it.next());
38 | }
39 | return sb.toString();
40 | }
41 |
42 | public static Optional parse(final CharSequence line) {
43 | final Matcher matcher = LINE_PATTERN.matcher(line);
44 | if (!matcher.matches())
45 | return Optional.empty();
46 | return Optional.of(new Attribute(matcher.group(1), matcher.group(2)));
47 | }
48 |
49 | public static String[] split(final CharSequence value) {
50 | return LIST_SEPARATOR.split(value);
51 | }
52 |
53 | public String getKey() {
54 | return key;
55 | }
56 |
57 | public String getValue() {
58 | return value;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/android/backend/BackendException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.backend;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | /**
11 | * A subclass of {@link Exception} that encapsulates the reasons for a failure originating in
12 | * implementations of {@link Backend}.
13 | */
14 | @NonNullForAll
15 | public final class BackendException extends Exception {
16 | private final Object[] format;
17 | private final Reason reason;
18 |
19 | /**
20 | * Public constructor for BackendException.
21 | *
22 | * @param reason The {@link Reason} which caused this exception to be thrown
23 | * @param format Format string values used when converting exceptions to user-facing strings.
24 | */
25 | public BackendException(final Reason reason, final Object... format) {
26 | this.reason = reason;
27 | this.format = format;
28 | }
29 |
30 | /**
31 | * Get the format string values associated with the instance.
32 | *
33 | * @return Array of {@link Object} for string formatting purposes
34 | */
35 | public Object[] getFormat() {
36 | return format;
37 | }
38 |
39 | /**
40 | * Get the reason for this exception.
41 | *
42 | * @return Associated {@link Reason} for this exception.
43 | */
44 | public Reason getReason() {
45 | return reason;
46 | }
47 |
48 | /**
49 | * Enum class containing all known reasons for why a {@link BackendException} might be thrown.
50 | */
51 | public enum Reason {
52 | UNKNOWN_KERNEL_MODULE_NAME,
53 | WG_QUICK_CONFIG_ERROR_CODE,
54 | TUNNEL_MISSING_CONFIG,
55 | VPN_NOT_AUTHORIZED,
56 | UNABLE_TO_START_VPN,
57 | TUN_CREATION_ERROR,
58 | GO_ACTIVATION_ERROR_CODE,
59 | DNS_RESOLUTION_FAILURE,
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | wireguardVersionCode=516
2 | wireguardVersionName=1.0.20250531
3 | wireguardPackageName=com.wireguard.android
4 |
5 | # When configured, Gradle will run in incubating parallel mode.
6 | # This option should only be used with decoupled projects. More details, visit
7 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
8 | org.gradle.parallel=true
9 | org.gradle.configureondemand=true
10 | org.gradle.caching=true
11 |
12 | # Enable Kotlin incremental compilation
13 | kotlin.incremental=true
14 |
15 | # Enable AndroidX support
16 | android.useAndroidX=true
17 |
18 | # Specifies the JVM arguments used for the daemon process.
19 | # The setting is particularly useful for tweaking memory settings.
20 | org.gradle.jvmargs=-Xmx1536m
21 |
22 | # Turn off AP discovery in compile path to enable compile avoidance
23 | kapt.include.compile.classpath=false
24 |
25 | # Experimental AGP flags
26 | # Generate compile-time only R class for app modules.
27 | android.enableAppCompileTimeRClass=true
28 | # Keep AAPT2 daemons alive between incremental builds.
29 | android.keepWorkerActionServicesBetweenBuilds=true
30 | # Generate manifest class as a .class directly rather than a Java source file.
31 | android.generateManifestClass=true
32 |
33 | # Default Android build features
34 | # Disable resource values generation by default in libraries
35 | android.defaults.buildfeatures.resvalues=false
36 | # Disable shader compilation by default
37 | android.defaults.buildfeatures.shaders=false
38 | # Disable Android resource processing by default
39 | android.library.defaults.buildfeatures.androidresources=false
40 |
41 | # Suppress warnings for some features that aren't yet stabilized
42 | android.suppressUnsupportedOptionWarnings=android.keepWorkerActionServicesBetweenBuilds,\
43 | android.enableAppCompileTimeRClass,\
44 | android.suppressUnsupportedOptionWarnings
45 |
46 | # OSSRH sometimes struggles with slow deployments, so this makes Gradle
47 | # more tolerant to those delays.
48 | systemProp.org.gradle.internal.http.connectionTimeout=500000
49 | systemProp.org.gradle.internal.http.socketTimeout=500000
50 |
--------------------------------------------------------------------------------
/tunnel/src/test/java/com/wireguard/config/ConfigTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.config;
7 |
8 | import org.junit.Test;
9 |
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 | import java.util.Arrays;
13 | import java.util.Collection;
14 | import java.util.HashSet;
15 | import java.util.Objects;
16 |
17 | import static org.junit.Assert.assertEquals;
18 | import static org.junit.Assert.assertNotNull;
19 | import static org.junit.Assert.assertTrue;
20 | import static org.junit.Assert.fail;
21 |
22 | public class ConfigTest {
23 |
24 | @Test(expected = BadConfigException.class)
25 | public void invalid_config_throws() throws IOException, BadConfigException {
26 | try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("broken.conf")) {
27 | Config.parse(is);
28 | }
29 | }
30 |
31 | @Test
32 | public void valid_config_parses_correctly() throws IOException, ParseException {
33 | Config config = null;
34 | final Collection expectedAllowedIps = new HashSet<>(Arrays.asList(InetNetwork.parse("0.0.0.0/0"), InetNetwork.parse("::0/0")));
35 | try (final InputStream is = Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("working.conf")) {
36 | config = Config.parse(is);
37 | } catch (final BadConfigException e) {
38 | fail("'working.conf' should never fail to parse");
39 | }
40 | assertNotNull("config cannot be null after parsing", config);
41 | assertTrue(
42 | "No applications should be excluded by default",
43 | config.getInterface().getExcludedApplications().isEmpty()
44 | );
45 | assertEquals("Test config has exactly one peer", 1, config.getPeers().size());
46 | assertEquals("Test config's allowed IPs are 0.0.0.0/0 and ::0/0", config.getPeers().get(0).getAllowedIps(), expectedAllowedIps);
47 | assertEquals("Test config has one DNS server", 1, config.getInterface().getDnsServers().size());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tunnel/tools/libwg-go/Makefile:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 | #
3 | # Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
4 |
5 | BUILDDIR ?= $(CURDIR)/build
6 | DESTDIR ?= $(CURDIR)/out
7 |
8 | NDK_GO_ARCH_MAP_x86 := 386
9 | NDK_GO_ARCH_MAP_x86_64 := amd64
10 | NDK_GO_ARCH_MAP_arm := arm
11 | NDK_GO_ARCH_MAP_arm64 := arm64
12 | NDK_GO_ARCH_MAP_mips := mipsx
13 | NDK_GO_ARCH_MAP_mips64 := mips64x
14 |
15 | comma := ,
16 | CLANG_FLAGS := --target=$(TARGET) --sysroot=$(SYSROOT)
17 | export CGO_CFLAGS := $(CLANG_FLAGS) $(subst -mthumb,-marm,$(CFLAGS))
18 | export CGO_LDFLAGS := $(CLANG_FLAGS) $(patsubst -Wl$(comma)--build-id=%,-Wl$(comma)--build-id=none,$(LDFLAGS)) -Wl,-soname=libwg-go.so
19 | export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME))
20 | export GOOS := android
21 | export CGO_ENABLED := 1
22 |
23 | GO_VERSION := 1.24.3
24 | GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m))
25 | GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz
26 | GO_HASH_darwin-amd64 := 13e6fe3fcf65689d77d40e633de1e31c6febbdbcb846eb05fc2434ed2213e92b
27 | GO_HASH_darwin-arm64 := 64a3fa22142f627e78fac3018ce3d4aeace68b743eff0afda8aae0411df5e4fb
28 | GO_HASH_linux-amd64 := 3333f6ea53afa971e9078895eaa4ac7204a8c6b5c68c10e6bc9a33e8e391bdd8
29 |
30 | default: $(DESTDIR)/libwg-go.so
31 |
32 | $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL):
33 | mkdir -p "$(dir $@)"
34 | flock "$@.lock" -c ' \
35 | [ -f "$@" ] && exit 0; \
36 | curl -o "$@.tmp" "https://dl.google.com/go/$(GO_TARBALL)" && \
37 | echo "$(GO_HASH_$(GO_PLATFORM)) $@.tmp" | sha256sum -c && \
38 | mv "$@.tmp" "$@"'
39 |
40 | $(BUILDDIR)/go-$(GO_VERSION)/.prepared: $(GRADLE_USER_HOME)/caches/golang/$(GO_TARBALL)
41 | mkdir -p "$(dir $@)"
42 | flock "$@.lock" -c ' \
43 | [ -f "$@" ] && exit 0; \
44 | tar -C "$(dir $@)" --strip-components=1 -xzf "$^" && \
45 | patch -p1 -f -N -r- -d "$(dir $@)" < goruntime-boottime-over-monotonic.diff && \
46 | touch "$@"'
47 |
48 | $(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)
49 | $(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod
50 | go build -tags linux -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard -buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode c-shared
51 |
52 | .DELETE_ON_ERROR:
53 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.model
7 |
8 | object TunnelComparator : Comparator {
9 | private class NaturalSortString(originalString: String) {
10 | class NaturalSortToken(val maybeString: String?, val maybeNumber: Int?) : Comparable {
11 | override fun compareTo(other: NaturalSortToken): Int {
12 | if (maybeString == null) {
13 | if (other.maybeString != null || maybeNumber!! < other.maybeNumber!!) {
14 | return -1
15 | } else if (maybeNumber > other.maybeNumber) {
16 | return 1
17 | }
18 | } else if (other.maybeString == null || maybeString > other.maybeString) {
19 | return 1
20 | } else if (maybeString < other.maybeString) {
21 | return -1
22 | }
23 | return 0
24 | }
25 | }
26 |
27 | val tokens: MutableList = ArrayList()
28 |
29 | init {
30 | for (s in NATURAL_SORT_DIGIT_FINDER.findAll(originalString.split(WHITESPACE_FINDER).joinToString(" ").lowercase())) {
31 | try {
32 | val n = s.value.toInt()
33 | tokens.add(NaturalSortToken(null, n))
34 | } catch (_: NumberFormatException) {
35 | tokens.add(NaturalSortToken(s.value, null))
36 | }
37 | }
38 | }
39 |
40 | private companion object {
41 | private val NATURAL_SORT_DIGIT_FINDER = Regex("""\d+|\D+""")
42 | private val WHITESPACE_FINDER = Regex("""\s""")
43 | }
44 | }
45 |
46 | override fun compare(a: String, b: String): Int {
47 | if (a == b)
48 | return 0
49 | val na = NaturalSortString(a)
50 | val nb = NaturalSortString(b)
51 | for (i in 0 until nb.tokens.size) {
52 | if (i == na.tokens.size) {
53 | return -1
54 | }
55 | val c = na.tokens[i].compareTo(nb.tokens[i])
56 | if (c != 0)
57 | return c
58 | }
59 | return 1
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/app_list_item.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
19 |
20 |
23 |
24 |
25 |
34 |
35 |
42 |
43 |
55 |
56 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/QuickTilePreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.preference
7 |
8 | import android.app.StatusBarManager
9 | import android.content.ComponentName
10 | import android.content.Context
11 | import android.graphics.drawable.Icon
12 | import android.os.Build
13 | import android.util.AttributeSet
14 | import android.widget.Toast
15 | import androidx.annotation.RequiresApi
16 | import androidx.preference.Preference
17 | import com.wireguard.android.QuickTileService
18 | import com.wireguard.android.R
19 |
20 | @RequiresApi(Build.VERSION_CODES.TIRAMISU)
21 | class QuickTilePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
22 | override fun getSummary() = context.getString(R.string.quick_settings_tile_add_summary)
23 |
24 | override fun getTitle() = context.getString(R.string.quick_settings_tile_add_title)
25 |
26 | override fun onClick() {
27 | val statusBarManager = context.getSystemService(StatusBarManager::class.java)
28 | statusBarManager.requestAddTileService(
29 | ComponentName(context, QuickTileService::class.java),
30 | context.getString(R.string.quick_settings_tile_action),
31 | Icon.createWithResource(context, R.drawable.ic_tile),
32 | context.mainExecutor
33 | ) {
34 | when (it) {
35 | StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED,
36 | StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED -> {
37 | parent?.removePreference(this)
38 | --preferenceManager.preferenceScreen.initialExpandedChildrenCount
39 | }
40 | StatusBarManager.TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE,
41 | StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS,
42 | StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT,
43 | StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER,
44 | StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND,
45 | StatusBarManager.TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE ->
46 | Toast.makeText(context, context.getString(R.string.quick_settings_tile_add_failure, it), Toast.LENGTH_SHORT).show()
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tunnel/tools/libwg-go/jni.c:
--------------------------------------------------------------------------------
1 | /* SPDX-License-Identifier: Apache-2.0
2 | *
3 | * Copyright © 2017-2021 Jason A. Donenfeld . All Rights Reserved.
4 | */
5 |
6 | #include
7 | #include
8 | #include
9 |
10 | struct go_string { const char *str; long n; };
11 | extern int wgTurnOn(struct go_string ifname, int tun_fd, struct go_string settings);
12 | extern void wgTurnOff(int handle);
13 | extern int wgGetSocketV4(int handle);
14 | extern int wgGetSocketV6(int handle);
15 | extern char *wgGetConfig(int handle);
16 | extern char *wgVersion();
17 |
18 | JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOn(JNIEnv *env, jclass c, jstring ifname, jint tun_fd, jstring settings)
19 | {
20 | const char *ifname_str = (*env)->GetStringUTFChars(env, ifname, 0);
21 | size_t ifname_len = (*env)->GetStringUTFLength(env, ifname);
22 | const char *settings_str = (*env)->GetStringUTFChars(env, settings, 0);
23 | size_t settings_len = (*env)->GetStringUTFLength(env, settings);
24 | int ret = wgTurnOn((struct go_string){
25 | .str = ifname_str,
26 | .n = ifname_len
27 | }, tun_fd, (struct go_string){
28 | .str = settings_str,
29 | .n = settings_len
30 | });
31 | (*env)->ReleaseStringUTFChars(env, ifname, ifname_str);
32 | (*env)->ReleaseStringUTFChars(env, settings, settings_str);
33 | return ret;
34 | }
35 |
36 | JNIEXPORT void JNICALL Java_com_wireguard_android_backend_GoBackend_wgTurnOff(JNIEnv *env, jclass c, jint handle)
37 | {
38 | wgTurnOff(handle);
39 | }
40 |
41 | JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV4(JNIEnv *env, jclass c, jint handle)
42 | {
43 | return wgGetSocketV4(handle);
44 | }
45 |
46 | JNIEXPORT jint JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetSocketV6(JNIEnv *env, jclass c, jint handle)
47 | {
48 | return wgGetSocketV6(handle);
49 | }
50 |
51 | JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgGetConfig(JNIEnv *env, jclass c, jint handle)
52 | {
53 | jstring ret;
54 | char *config = wgGetConfig(handle);
55 | if (!config)
56 | return NULL;
57 | ret = (*env)->NewStringUTF(env, config);
58 | free(config);
59 | return ret;
60 | }
61 |
62 | JNIEXPORT jstring JNICALL Java_com_wireguard_android_backend_GoBackend_wgVersion(JNIEnv *env, jclass c)
63 | {
64 | jstring ret;
65 | char *version = wgVersion();
66 | if (!version)
67 | return NULL;
68 | ret = (*env)->NewStringUTF(env, version);
69 | free(version);
70 | return ret;
71 | }
72 |
--------------------------------------------------------------------------------
/ui/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
34 |
35 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
34 |
35 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.configStore
6 |
7 | import com.wireguard.config.Config
8 |
9 | /**
10 | * Interface for persistent storage providers for WireGuard configurations.
11 | */
12 | interface ConfigStore {
13 | /**
14 | * Create a persistent tunnel, which must have a unique name within the persistent storage
15 | * medium.
16 | *
17 | * @param name The name of the tunnel to create.
18 | * @param config Configuration for the new tunnel.
19 | * @return The configuration that was actually saved to persistent storage.
20 | */
21 | @Throws(Exception::class)
22 | fun create(name: String, config: Config): Config
23 |
24 | /**
25 | * Delete a persistent tunnel.
26 | *
27 | * @param name The name of the tunnel to delete.
28 | */
29 | @Throws(Exception::class)
30 | fun delete(name: String)
31 |
32 | /**
33 | * Enumerate the names of tunnels present in persistent storage.
34 | *
35 | * @return The set of present tunnel names.
36 | */
37 | fun enumerate(): Set
38 |
39 | /**
40 | * Load the configuration for the tunnel given by `name`.
41 | *
42 | * @param name The identifier for the configuration in persistent storage (i.e. the name of the
43 | * tunnel).
44 | * @return An in-memory representation of the configuration loaded from persistent storage.
45 | */
46 | @Throws(Exception::class)
47 | fun load(name: String): Config
48 |
49 | /**
50 | * Rename the configuration for the tunnel given by `name`.
51 | *
52 | * @param name The identifier for the existing configuration in persistent storage.
53 | * @param replacement The new identifier for the configuration in persistent storage.
54 | */
55 | @Throws(Exception::class)
56 | fun rename(name: String, replacement: String)
57 |
58 | /**
59 | * Save the configuration for an existing tunnel given by `name`.
60 | *
61 | * @param name The identifier for the configuration in persistent storage (i.e. the name of
62 | * the tunnel).
63 | * @param config An updated configuration object for the tunnel.
64 | * @return The configuration that was actually saved to persistent storage.
65 | */
66 | @Throws(Exception::class)
67 | fun save(name: String, config: Config): Config
68 | }
69 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
31 |
32 |
44 |
45 |
49 |
50 |
--------------------------------------------------------------------------------
/tunnel/tools/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 | #
3 | # Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
4 |
5 | cmake_minimum_required(VERSION 3.4.1)
6 | project("WireGuard")
7 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
8 | add_link_options(LINKER:--build-id=none)
9 | add_compile_options(-Wall -Werror)
10 |
11 | add_executable(libwg-quick.so wireguard-tools/src/wg-quick/android.c ndk-compat/compat.c)
12 | target_compile_options(libwg-quick.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DWG_PACKAGE_NAME=\"${ANDROID_PACKAGE_NAME}\")
13 | target_link_libraries(libwg-quick.so -ldl)
14 |
15 | file(GLOB WG_SOURCES wireguard-tools/src/*.c ndk-compat/compat.c)
16 | add_executable(libwg.so ${WG_SOURCES})
17 | target_include_directories(libwg.so PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/uapi/linux/" "${CMAKE_CURRENT_SOURCE_DIR}/wireguard-tools/src/")
18 | target_compile_options(libwg.so PUBLIC -std=gnu11 -include ${CMAKE_CURRENT_SOURCE_DIR}/ndk-compat/compat.h -DRUNSTATEDIR=\"/data/data/${ANDROID_PACKAGE_NAME}/cache\")
19 |
20 | add_custom_target(libwg-go.so WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/libwg-go" COMMENT "Building wireguard-go" VERBATIM COMMAND "${ANDROID_HOST_PREBUILTS}/bin/make"
21 | ANDROID_ARCH_NAME=${ANDROID_ARCH_NAME}
22 | ANDROID_PACKAGE_NAME=${ANDROID_PACKAGE_NAME}
23 | GRADLE_USER_HOME=${GRADLE_USER_HOME}
24 | CC=${CMAKE_C_COMPILER}
25 | CFLAGS=${CMAKE_C_FLAGS}
26 | LDFLAGS=${CMAKE_SHARED_LINKER_FLAGS}
27 | SYSROOT=${CMAKE_SYSROOT}
28 | TARGET=${CMAKE_C_COMPILER_TARGET}
29 | DESTDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
30 | BUILDDIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/../generated-src
31 | )
32 |
33 | # Strip unwanted ELF sections to prevent DT_FLAGS_1 warnings on old Android versions
34 | file(GLOB ELF_CLEANER_SOURCES elf-cleaner/*.c elf-cleaner/*.cpp)
35 | add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND cc
36 | -O2 -DPACKAGE_NAME="elf-cleaner" -DPACKAGE_VERSION="" -DCOPYRIGHT=""
37 | -o "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner" ${ELF_CLEANER_SOURCES}
38 | )
39 | add_custom_command(TARGET libwg.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"
40 | --api-level "${ANDROID_NATIVE_API_LEVEL}" "$")
41 | add_dependencies(libwg.so elf-cleaner)
42 | add_custom_command(TARGET libwg-quick.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"
43 | --api-level "${ANDROID_NATIVE_API_LEVEL}" "$")
44 | add_dependencies(libwg-quick.so elf-cleaner)
45 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/config/InetNetwork.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.config;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | import java.net.Inet4Address;
11 | import java.net.InetAddress;
12 |
13 | /**
14 | * An Internet network, denoted by its address and netmask
15 | *
16 | * Instances of this class are immutable.
17 | */
18 | @NonNullForAll
19 | public final class InetNetwork {
20 | private final InetAddress address;
21 | private final int mask;
22 |
23 | private InetNetwork(final InetAddress address, final int mask) {
24 | this.address = address;
25 | this.mask = mask;
26 | }
27 |
28 | public static InetNetwork parse(final String network) throws ParseException {
29 | final int slash = network.lastIndexOf('/');
30 | final String maskString;
31 | final int rawMask;
32 | final String rawAddress;
33 | if (slash >= 0) {
34 | maskString = network.substring(slash + 1);
35 | try {
36 | rawMask = Integer.parseInt(maskString, 10);
37 | } catch (final NumberFormatException ignored) {
38 | throw new ParseException(Integer.class, maskString);
39 | }
40 | rawAddress = network.substring(0, slash);
41 | } else {
42 | maskString = "";
43 | rawMask = -1;
44 | rawAddress = network;
45 | }
46 | final InetAddress address = InetAddresses.parse(rawAddress);
47 | final int maxMask = (address instanceof Inet4Address) ? 32 : 128;
48 | if (rawMask > maxMask)
49 | throw new ParseException(InetNetwork.class, maskString, "Invalid network mask");
50 | final int mask = rawMask >= 0 ? rawMask : maxMask;
51 | return new InetNetwork(address, mask);
52 | }
53 |
54 | @Override
55 | public boolean equals(final Object obj) {
56 | if (!(obj instanceof InetNetwork))
57 | return false;
58 | final InetNetwork other = (InetNetwork) obj;
59 | return address.equals(other.address) && mask == other.mask;
60 | }
61 |
62 | public InetAddress getAddress() {
63 | return address;
64 | }
65 |
66 | public int getMask() {
67 | return mask;
68 | }
69 |
70 | @Override
71 | public int hashCode() {
72 | return address.hashCode() ^ mask;
73 | }
74 |
75 | @Override
76 | public String toString() {
77 | return address.getHostAddress() + '/' + mask;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/ui/src/main/res/xml/preferences.xml:
--------------------------------------------------------------------------------
1 |
5 |
7 |
10 |
17 |
18 |
19 |
24 |
31 |
38 |
39 |
40 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/VersionPreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.preference
6 |
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.net.Uri
10 | import android.util.AttributeSet
11 | import android.widget.Toast
12 | import androidx.preference.Preference
13 | import com.wireguard.android.Application
14 | import com.wireguard.android.BuildConfig
15 | import com.wireguard.android.R
16 | import com.wireguard.android.backend.Backend
17 | import com.wireguard.android.backend.GoBackend
18 | import com.wireguard.android.backend.WgQuickBackend
19 | import com.wireguard.android.util.ErrorMessages
20 | import com.wireguard.android.util.lifecycleScope
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.launch
23 | import kotlinx.coroutines.withContext
24 |
25 | class VersionPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
26 | private var versionSummary: String? = null
27 |
28 | override fun getSummary() = versionSummary
29 |
30 | override fun getTitle() = context.getString(R.string.version_title, BuildConfig.VERSION_NAME)
31 |
32 | override fun onClick() {
33 | val intent = Intent(Intent.ACTION_VIEW)
34 | intent.data = Uri.parse("https://www.wireguard.com/")
35 | try {
36 | context.startActivity(intent)
37 | } catch (e: Throwable) {
38 | Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show()
39 | }
40 | }
41 |
42 | companion object {
43 | private fun getBackendPrettyName(context: Context, backend: Backend) = when (backend) {
44 | is WgQuickBackend -> context.getString(R.string.type_name_kernel_module)
45 | is GoBackend -> context.getString(R.string.type_name_go_userspace)
46 | else -> ""
47 | }
48 | }
49 |
50 | init {
51 | lifecycleScope.launch {
52 | val backend = Application.getBackend()
53 | versionSummary = getContext().getString(R.string.version_summary_checking, getBackendPrettyName(context, backend).lowercase())
54 | notifyChanged()
55 | versionSummary = try {
56 | getContext().getString(R.string.version_summary, getBackendPrettyName(context, backend), withContext(Dispatchers.IO) { backend.version })
57 | } catch (_: Throwable) {
58 | getContext().getString(R.string.version_summary_unknown, getBackendPrettyName(context, backend).lowercase())
59 | }
60 | notifyChanged()
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tunnel_list_item.xml:
--------------------------------------------------------------------------------
1 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
22 |
23 |
26 |
27 |
30 |
31 |
32 |
42 |
43 |
54 |
55 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/app_list_dialog_fragment.xml:
--------------------------------------------------------------------------------
1 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
22 |
23 |
24 |
28 |
29 |
34 |
35 |
39 |
40 |
44 |
45 |
46 |
50 |
51 |
59 |
60 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.util
7 |
8 | import android.icu.text.ListFormatter
9 | import android.icu.text.MeasureFormat
10 | import android.icu.text.RelativeDateTimeFormatter
11 | import android.icu.util.Measure
12 | import android.icu.util.MeasureUnit
13 | import android.os.Build
14 | import com.wireguard.android.Application
15 | import com.wireguard.android.R
16 | import java.util.Locale
17 | import kotlin.time.Duration.Companion.seconds
18 |
19 | object QuantityFormatter {
20 | fun formatBytes(bytes: Long): String {
21 | val context = Application.get().applicationContext
22 | return when {
23 | bytes < 1024 -> context.getString(R.string.transfer_bytes, bytes)
24 | bytes < 1024 * 1024 -> context.getString(R.string.transfer_kibibytes, bytes / 1024.0)
25 | bytes < 1024 * 1024 * 1024 -> context.getString(R.string.transfer_mibibytes, bytes / (1024.0 * 1024.0))
26 | bytes < 1024 * 1024 * 1024 * 1024L -> context.getString(R.string.transfer_gibibytes, bytes / (1024.0 * 1024.0 * 1024.0))
27 | else -> context.getString(R.string.transfer_tibibytes, bytes / (1024.0 * 1024.0 * 1024.0) / 1024.0)
28 | }
29 | }
30 |
31 | fun formatEpochAgo(epochMillis: Long): String {
32 | var span = (System.currentTimeMillis() - epochMillis) / 1000
33 |
34 | if (span <= 0L)
35 | return RelativeDateTimeFormatter.getInstance().format(RelativeDateTimeFormatter.Direction.PLAIN, RelativeDateTimeFormatter.AbsoluteUnit.NOW)
36 | val measureFormat = MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
37 | val parts = ArrayList(4)
38 | if (span >= 24 * 60 * 60L) {
39 | val v = span / (24 * 60 * 60L)
40 | parts.add(measureFormat.format(Measure(v, MeasureUnit.DAY)))
41 | span -= v * (24 * 60 * 60L)
42 | }
43 | if (span >= 60 * 60L) {
44 | val v = span / (60 * 60L)
45 | parts.add(measureFormat.format(Measure(v, MeasureUnit.HOUR)))
46 | span -= v * (60 * 60L)
47 | }
48 | if (span >= 60L) {
49 | val v = span / 60L
50 | parts.add(measureFormat.format(Measure(v, MeasureUnit.MINUTE)))
51 | span -= v * 60L
52 | }
53 | if (span > 0L)
54 | parts.add(measureFormat.format(Measure(span, MeasureUnit.SECOND)))
55 |
56 | val joined = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
57 | parts.joinToString()
58 | else
59 | ListFormatter.getInstance(Locale.getDefault(), ListFormatter.Type.UNITS, ListFormatter.Width.SHORT).format(parts)
60 |
61 | return Application.get().applicationContext.getString(R.string.latest_handshake_ago, joined)
62 | }
63 | }
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.viewmodel
6 |
7 | import android.os.Build
8 | import android.os.Parcel
9 | import android.os.Parcelable
10 | import androidx.core.os.ParcelCompat
11 | import androidx.databinding.ObservableArrayList
12 | import androidx.databinding.ObservableList
13 | import com.wireguard.config.BadConfigException
14 | import com.wireguard.config.Config
15 | import com.wireguard.config.Peer
16 |
17 | class ConfigProxy : Parcelable {
18 | val `interface`: InterfaceProxy
19 | val peers: ObservableList = ObservableArrayList()
20 |
21 | private constructor(parcel: Parcel) {
22 | `interface` = ParcelCompat.readParcelable(parcel, InterfaceProxy::class.java.classLoader, InterfaceProxy::class.java) ?: InterfaceProxy()
23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
24 | ParcelCompat.readParcelableList(parcel, peers, PeerProxy::class.java.classLoader, PeerProxy::class.java)
25 | } else {
26 | parcel.readTypedList(peers, PeerProxy.CREATOR)
27 | }
28 | peers.forEach { it.bind(this) }
29 | }
30 |
31 | constructor(other: Config) {
32 | `interface` = InterfaceProxy(other.getInterface())
33 | other.peers.forEach {
34 | val proxy = PeerProxy(it)
35 | peers.add(proxy)
36 | proxy.bind(this)
37 | }
38 | }
39 |
40 | constructor() {
41 | `interface` = InterfaceProxy()
42 | }
43 |
44 | fun addPeer(): PeerProxy {
45 | val proxy = PeerProxy()
46 | peers.add(proxy)
47 | proxy.bind(this)
48 | return proxy
49 | }
50 |
51 | override fun describeContents() = 0
52 |
53 | @Throws(BadConfigException::class)
54 | fun resolve(): Config {
55 | val resolvedPeers: MutableCollection = ArrayList()
56 | peers.forEach { resolvedPeers.add(it.resolve()) }
57 | return Config.Builder()
58 | .setInterface(`interface`.resolve())
59 | .addPeers(resolvedPeers)
60 | .build()
61 | }
62 |
63 | override fun writeToParcel(dest: Parcel, flags: Int) {
64 | dest.writeParcelable(`interface`, flags)
65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
66 | dest.writeParcelableList(peers, flags)
67 | } else {
68 | dest.writeTypedList(peers)
69 | }
70 | }
71 |
72 | private class ConfigProxyCreator : Parcelable.Creator {
73 | override fun createFromParcel(parcel: Parcel): ConfigProxy {
74 | return ConfigProxy(parcel)
75 | }
76 |
77 | override fun newArray(size: Int): Array {
78 | return arrayOfNulls(size)
79 | }
80 | }
81 |
82 | companion object {
83 | @JvmField
84 | val CREATOR: Parcelable.Creator = ConfigProxyCreator()
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.activity
6 |
7 | import android.content.ComponentName
8 | import android.os.Build
9 | import android.os.Bundle
10 | import android.service.quicksettings.TileService
11 | import android.util.Log
12 | import android.widget.Toast
13 | import androidx.activity.result.contract.ActivityResultContracts
14 | import androidx.annotation.RequiresApi
15 | import androidx.appcompat.app.AppCompatActivity
16 | import androidx.lifecycle.lifecycleScope
17 | import com.wireguard.android.Application
18 | import com.wireguard.android.QuickTileService
19 | import com.wireguard.android.R
20 | import com.wireguard.android.backend.GoBackend
21 | import com.wireguard.android.backend.Tunnel
22 | import com.wireguard.android.util.ErrorMessages
23 | import kotlinx.coroutines.launch
24 |
25 | class TunnelToggleActivity : AppCompatActivity() {
26 | private val permissionActivityResultLauncher =
27 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() }
28 |
29 | private fun toggleTunnelWithPermissionsResult() {
30 | val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return
31 | lifecycleScope.launch {
32 | try {
33 | tunnel.setStateAsync(Tunnel.State.TOGGLE)
34 | } catch (e: Throwable) {
35 | TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
36 | val error = ErrorMessages[e]
37 | val message = getString(R.string.toggle_error, error)
38 | Log.e(TAG, message, e)
39 | Toast.makeText(this@TunnelToggleActivity, message, Toast.LENGTH_LONG).show()
40 | finishAffinity()
41 | return@launch
42 | }
43 | TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java))
44 | finishAffinity()
45 | }
46 | }
47 |
48 | override fun onCreate(savedInstanceState: Bundle?) {
49 | super.onCreate(savedInstanceState)
50 | lifecycleScope.launch {
51 | if (Application.getBackend() is GoBackend) {
52 | try {
53 | val intent = GoBackend.VpnService.prepare(this@TunnelToggleActivity)
54 | if (intent != null) {
55 | permissionActivityResultLauncher.launch(intent)
56 | return@launch
57 | }
58 | } catch (e: Exception) {
59 | Toast.makeText(this@TunnelToggleActivity, ErrorMessages[e], Toast.LENGTH_LONG).show()
60 | }
61 | }
62 | toggleTunnelWithPermissionsResult()
63 | }
64 | }
65 |
66 | companion object {
67 | private const val TAG = "WireGuard/TunnelToggleActivity"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
5 |
6 | val pkg: String = providers.gradleProperty("wireguardPackageName").get()
7 |
8 | plugins {
9 | alias(libs.plugins.android.application)
10 | alias(libs.plugins.kotlin.android)
11 | alias(libs.plugins.kotlin.kapt)
12 | }
13 |
14 | android {
15 | compileSdk = 36
16 | buildFeatures {
17 | buildConfig = true
18 | dataBinding = true
19 | viewBinding = true
20 | }
21 | namespace = pkg
22 | defaultConfig {
23 | applicationId = pkg
24 | minSdk = 24
25 | targetSdk = 36
26 | versionCode = providers.gradleProperty("wireguardVersionCode").get().toInt()
27 | versionName = providers.gradleProperty("wireguardVersionName").get()
28 | buildConfigField("int", "MIN_SDK_VERSION", minSdk.toString())
29 | }
30 | compileOptions {
31 | sourceCompatibility = JavaVersion.VERSION_17
32 | targetCompatibility = JavaVersion.VERSION_17
33 | isCoreLibraryDesugaringEnabled = true
34 | }
35 | buildTypes {
36 | release {
37 | isMinifyEnabled = true
38 | isShrinkResources = true
39 | proguardFiles("proguard-android-optimize.txt")
40 | packaging {
41 | resources {
42 | excludes += "DebugProbesKt.bin"
43 | excludes += "kotlin-tooling-metadata.json"
44 | excludes += "META-INF/*.version"
45 | }
46 | }
47 | }
48 | debug {
49 | applicationIdSuffix = ".debug"
50 | versionNameSuffix = "-debug"
51 | }
52 | create("googleplay") {
53 | initWith(getByName("release"))
54 | matchingFallbacks += "release"
55 | }
56 | }
57 | androidResources {
58 | generateLocaleConfig = true
59 | }
60 | lint {
61 | disable += "LongLogTag"
62 | warning += "MissingTranslation"
63 | warning += "ImpliedQuantity"
64 | }
65 | }
66 |
67 | dependencies {
68 | implementation(project(":tunnel"))
69 | implementation(libs.androidx.activity.ktx)
70 | implementation(libs.androidx.annotation)
71 | implementation(libs.androidx.appcompat)
72 | implementation(libs.androidx.constraintlayout)
73 | implementation(libs.androidx.coordinatorlayout)
74 | implementation(libs.androidx.biometric)
75 | implementation(libs.androidx.core.ktx)
76 | implementation(libs.androidx.fragment.ktx)
77 | implementation(libs.androidx.preference.ktx)
78 | implementation(libs.androidx.lifecycle.runtime.ktx)
79 | implementation(libs.androidx.datastore.preferences)
80 | implementation(libs.google.material)
81 | implementation(libs.zxing.android.embedded)
82 | implementation(libs.kotlinx.coroutines.android)
83 | coreLibraryDesugaring(libs.desugarJdkLibs)
84 | }
85 |
86 | tasks.withType().configureEach {
87 | options.compilerArgs.add("-Xlint:unchecked")
88 | options.isDeprecation = true
89 | }
90 |
91 | tasks.withType().configureEach {
92 | compilerOptions.jvmTarget = JvmTarget.JVM_17
93 | }
94 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_tile.xml:
--------------------------------------------------------------------------------
1 |
5 |
10 |
16 |
22 |
28 |
29 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.databinding
6 |
7 | import java.util.AbstractList
8 | import java.util.Collections
9 | import java.util.Comparator
10 | import java.util.Spliterator
11 |
12 | /**
13 | * KeyedArrayList that enforces uniqueness and sorted order across the set of keys. This class uses
14 | * binary search to improve lookup and replacement times to O(log(n)). However, due to the
15 | * array-based nature of this class, insertion and removal of elements with anything but the largest
16 | * key still require O(n) time.
17 | */
18 | class ObservableSortedKeyedArrayList>(private val comparator: Comparator) : ObservableKeyedArrayList() {
19 | @Transient
20 | private val keyList = KeyList(this)
21 |
22 | override fun add(element: E): Boolean {
23 | val insertionPoint = getInsertionPoint(element)
24 | if (insertionPoint < 0) {
25 | // Skipping insertion is non-destructive if the new and existing objects are the same.
26 | if (element === get(-insertionPoint - 1)) return false
27 | throw IllegalArgumentException("Element with same key already exists in list")
28 | }
29 | super.add(insertionPoint, element)
30 | return true
31 | }
32 |
33 | override fun add(index: Int, element: E) {
34 | val insertionPoint = getInsertionPoint(element)
35 | require(insertionPoint >= 0) { "Element with same key already exists in list" }
36 | if (insertionPoint != index) throw IndexOutOfBoundsException("Wrong index given for element")
37 | super.add(index, element)
38 | }
39 |
40 | override fun addAll(elements: Collection): Boolean {
41 | var didChange = false
42 | for (e in elements) {
43 | if (add(e))
44 | didChange = true
45 | }
46 | return didChange
47 | }
48 |
49 | override fun addAll(index: Int, elements: Collection): Boolean {
50 | var i = index
51 | for (e in elements)
52 | add(i++, e)
53 | return true
54 | }
55 |
56 | private fun getInsertionPoint(e: E) = -Collections.binarySearch(keyList, e.key, comparator) - 1
57 |
58 | override fun indexOfKey(key: K): Int {
59 | val index = Collections.binarySearch(keyList, key, comparator)
60 | return if (index >= 0) index else -1
61 | }
62 |
63 | override fun set(index: Int, element: E): E {
64 | val order = comparator.compare(element.key, get(index).key)
65 | if (order != 0) {
66 | // Allow replacement if the new key would be inserted adjacent to the replaced element.
67 | val insertionPoint = getInsertionPoint(element)
68 | if (insertionPoint < index || insertionPoint > index + 1)
69 | throw IndexOutOfBoundsException("Wrong index given for element")
70 | }
71 | return super.set(index, element)
72 | }
73 |
74 | private class KeyList>(private val list: ObservableSortedKeyedArrayList) : AbstractList(), Set {
75 | override fun get(index: Int): K = list[index].key
76 |
77 | override val size
78 | get() = list.size
79 |
80 | override fun spliterator(): Spliterator = super.spliterator()
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/configStore/FileConfigStore.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.configStore
6 |
7 | import android.content.Context
8 | import android.util.Log
9 | import com.wireguard.android.R
10 | import com.wireguard.config.BadConfigException
11 | import com.wireguard.config.Config
12 | import java.io.File
13 | import java.io.FileInputStream
14 | import java.io.FileNotFoundException
15 | import java.io.FileOutputStream
16 | import java.io.IOException
17 | import java.nio.charset.StandardCharsets
18 |
19 | /**
20 | * Configuration store that uses a `wg-quick`-style file for each configured tunnel.
21 | */
22 | class FileConfigStore(private val context: Context) : ConfigStore {
23 | @Throws(IOException::class)
24 | override fun create(name: String, config: Config): Config {
25 | Log.d(TAG, "Creating configuration for tunnel $name")
26 | val file = fileFor(name)
27 | if (!file.createNewFile())
28 | throw IOException(context.getString(R.string.config_file_exists_error, file.name))
29 | FileOutputStream(file, false).use { it.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
30 | return config
31 | }
32 |
33 | @Throws(IOException::class)
34 | override fun delete(name: String) {
35 | Log.d(TAG, "Deleting configuration for tunnel $name")
36 | val file = fileFor(name)
37 | if (!file.delete())
38 | throw IOException(context.getString(R.string.config_delete_error, file.name))
39 | }
40 |
41 | override fun enumerate(): Set {
42 | return context.fileList()
43 | .filter { it.endsWith(".conf") }
44 | .map { it.substring(0, it.length - ".conf".length) }
45 | .toSet()
46 | }
47 |
48 | private fun fileFor(name: String): File {
49 | return File(context.filesDir, "$name.conf")
50 | }
51 |
52 | @Throws(BadConfigException::class, IOException::class)
53 | override fun load(name: String): Config {
54 | FileInputStream(fileFor(name)).use { stream -> return Config.parse(stream) }
55 | }
56 |
57 | @Throws(IOException::class)
58 | override fun rename(name: String, replacement: String) {
59 | Log.d(TAG, "Renaming configuration for tunnel $name to $replacement")
60 | val file = fileFor(name)
61 | val replacementFile = fileFor(replacement)
62 | if (!replacementFile.createNewFile()) throw IOException(context.getString(R.string.config_exists_error, replacement))
63 | if (!file.renameTo(replacementFile)) {
64 | if (!replacementFile.delete()) Log.w(TAG, "Couldn't delete marker file for new name $replacement")
65 | throw IOException(context.getString(R.string.config_rename_error, file.name))
66 | }
67 | }
68 |
69 | @Throws(IOException::class)
70 | override fun save(name: String, config: Config): Config {
71 | Log.d(TAG, "Saving configuration for tunnel $name")
72 | val file = fileFor(name)
73 | if (!file.isFile)
74 | throw FileNotFoundException(context.getString(R.string.config_not_found_error, file.name))
75 | FileOutputStream(file, false).use { stream -> stream.write(config.toWgQuickString().toByteArray(StandardCharsets.UTF_8)) }
76 | return config
77 | }
78 |
79 | companion object {
80 | private const val TAG = "WireGuard/FileConfigStore"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/android/backend/Backend.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.backend;
7 |
8 | import com.wireguard.config.Config;
9 | import com.wireguard.util.NonNullForAll;
10 |
11 | import java.util.Set;
12 |
13 | import androidx.annotation.Nullable;
14 |
15 | /**
16 | * Interface for implementations of the WireGuard secure network tunnel.
17 | */
18 |
19 | @NonNullForAll
20 | public interface Backend {
21 | /**
22 | * Enumerate names of currently-running tunnels.
23 | *
24 | * @return The set of running tunnel names.
25 | */
26 | Set getRunningTunnelNames();
27 |
28 | /**
29 | * Get the state of a tunnel.
30 | *
31 | * @param tunnel The tunnel to examine the state of.
32 | * @return The state of the tunnel.
33 | * @throws Exception Exception raised when retrieving tunnel's state.
34 | */
35 | Tunnel.State getState(Tunnel tunnel) throws Exception;
36 |
37 | /**
38 | * Get statistics about traffic and errors on this tunnel. If the tunnel is not running, the
39 | * statistics object will be filled with zero values.
40 | *
41 | * @param tunnel The tunnel to retrieve statistics for.
42 | * @return The statistics for the tunnel.
43 | * @throws Exception Exception raised when retrieving statistics.
44 | */
45 | Statistics getStatistics(Tunnel tunnel) throws Exception;
46 |
47 | /**
48 | * Determine version of underlying backend.
49 | *
50 | * @return The version of the backend.
51 | * @throws Exception Exception raised while retrieving version.
52 | */
53 | String getVersion() throws Exception;
54 |
55 | /**
56 | * Determines whether the service is running in always-on VPN mode.
57 | * In this mode the system ensures that the service is always running by restarting it when necessary,
58 | * e.g. after reboot.
59 | *
60 | * @return A boolean indicating whether the service is running in always-on VPN mode.
61 | * @throws Exception Exception raised while retrieving the always-on status.
62 | */
63 |
64 | boolean isAlwaysOn() throws Exception;
65 |
66 | /**
67 | * Determines whether the service is running in always-on VPN lockdown mode.
68 | * In this mode the system ensures that the service is always running and that the apps
69 | * aren't allowed to bypass the VPN.
70 | *
71 | * @return A boolean indicating whether the service is running in always-on VPN lockdown mode.
72 | * @throws Exception Exception raised while retrieving the lockdown status.
73 | */
74 |
75 | boolean isLockdownEnabled() throws Exception;
76 |
77 | /**
78 | * Set the state of a tunnel, updating it's configuration. If the tunnel is already up, config
79 | * may update the running configuration; config may be null when setting the tunnel down.
80 | *
81 | * @param tunnel The tunnel to control the state of.
82 | * @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or
83 | * {@code TOGGLE}.
84 | * @param config The configuration for this tunnel, may be null if state is {@code DOWN}.
85 | * @return The updated state of the tunnel.
86 | * @throws Exception Exception raised while changing state.
87 | */
88 | Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception;
89 | }
90 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.util
7 |
8 | import android.content.ContentResolver
9 | import android.graphics.Bitmap
10 | import android.graphics.BitmapFactory
11 | import android.net.Uri
12 | import android.util.Log
13 | import com.google.zxing.BinaryBitmap
14 | import com.google.zxing.DecodeHintType
15 | import com.google.zxing.NotFoundException
16 | import com.google.zxing.RGBLuminanceSource
17 | import com.google.zxing.Reader
18 | import com.google.zxing.Result
19 | import com.google.zxing.common.HybridBinarizer
20 | import kotlinx.coroutines.Dispatchers
21 | import kotlinx.coroutines.withContext
22 |
23 | /**
24 | * Encapsulates the logic of scanning a barcode from a file,
25 | * @property contentResolver - Resolver to read the incoming data
26 | * @property reader - An instance of zxing's [Reader] class to parse the image
27 | */
28 | class QrCodeFromFileScanner(
29 | private val contentResolver: ContentResolver,
30 | private val reader: Reader,
31 | ) {
32 | private fun scanBitmapForResult(source: Bitmap): Result {
33 | val width = source.width
34 | val height = source.height
35 | val pixels = IntArray(width * height)
36 | source.getPixels(pixels, 0, width, 0, 0, width, height)
37 |
38 | val bBitmap = BinaryBitmap(HybridBinarizer(RGBLuminanceSource(width, height, pixels)))
39 | return reader.decode(bBitmap, mapOf(DecodeHintType.TRY_HARDER to true))
40 | }
41 |
42 | private fun doScan(data: Uri): Result {
43 | Log.d(TAG, "Starting to scan an image: $data")
44 | contentResolver.openInputStream(data).use { inputStream ->
45 | var bitmap: Bitmap? = null
46 | var firstException: Throwable? = null
47 | for (i in arrayOf(1, 2, 4, 8, 16, 32, 64, 128)) {
48 | try {
49 | val options = BitmapFactory.Options()
50 | options.inSampleSize = i
51 | bitmap = BitmapFactory.decodeStream(inputStream, null, options)
52 | ?: throw IllegalArgumentException("Can't decode stream for bitmap")
53 | return scanBitmapForResult(bitmap)
54 | } catch (e: Throwable) {
55 | bitmap?.recycle()
56 | System.gc()
57 | Log.e(TAG, "Original image scan at scale factor $i finished with error: $e")
58 | if (firstException == null)
59 | firstException = e
60 | }
61 | }
62 | throw Exception(firstException)
63 | }
64 | }
65 |
66 | /**
67 | * Attempts to parse incoming data
68 | * @return result of the decoding operation
69 | * @throws NotFoundException when parser didn't find QR code in the image
70 | */
71 | suspend fun scan(data: Uri) = withContext(Dispatchers.Default) { doScan(data) }
72 |
73 | companion object {
74 | private const val TAG = "QrCodeFromFileScanner"
75 |
76 | /**
77 | * Given a reference to a file, check if this file could be parsed by this class
78 | * @return true if the file can be parsed, false if not
79 | */
80 | fun validContentType(contentResolver: ContentResolver, data: Uri): Boolean {
81 | return contentResolver.getType(data)?.startsWith("image/") == true
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/fragment/ConfigNamingDialogFragment.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.fragment
6 |
7 | import android.app.Dialog
8 | import android.os.Bundle
9 | import android.view.WindowManager
10 | import androidx.fragment.app.DialogFragment
11 | import androidx.lifecycle.lifecycleScope
12 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
13 | import com.wireguard.android.Application
14 | import com.wireguard.android.R
15 | import com.wireguard.android.databinding.ConfigNamingDialogFragmentBinding
16 | import com.wireguard.config.BadConfigException
17 | import com.wireguard.config.Config
18 | import kotlinx.coroutines.launch
19 | import java.io.ByteArrayInputStream
20 | import java.io.IOException
21 | import java.nio.charset.StandardCharsets
22 |
23 | class ConfigNamingDialogFragment : DialogFragment() {
24 | private var binding: ConfigNamingDialogFragmentBinding? = null
25 | private var config: Config? = null
26 |
27 | private fun createTunnelAndDismiss() {
28 | val binding = binding ?: return
29 | val activity = activity ?: return
30 | val name = binding.tunnelNameText.text.toString()
31 | activity.lifecycleScope.launch {
32 | try {
33 | Application.getTunnelManager().create(name, config)
34 | dismiss()
35 | } catch (e: Throwable) {
36 | binding.tunnelNameTextLayout.error = e.message
37 | }
38 | }
39 | }
40 |
41 | override fun onCreate(savedInstanceState: Bundle?) {
42 | super.onCreate(savedInstanceState)
43 | val configText = requireArguments().getString(KEY_CONFIG_TEXT)
44 | val configBytes = configText!!.toByteArray(StandardCharsets.UTF_8)
45 | config = try {
46 | Config.parse(ByteArrayInputStream(configBytes))
47 | } catch (e: Throwable) {
48 | when (e) {
49 | is BadConfigException, is IOException -> throw IllegalArgumentException("Invalid config passed to ${javaClass.simpleName}", e)
50 | else -> throw e
51 | }
52 | }
53 | }
54 |
55 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
56 | val activity = requireActivity()
57 | val alertDialogBuilder = MaterialAlertDialogBuilder(activity)
58 | alertDialogBuilder.setTitle(R.string.import_from_qr_code)
59 | binding = ConfigNamingDialogFragmentBinding.inflate(activity.layoutInflater, null, false)
60 | binding?.apply {
61 | executePendingBindings()
62 | alertDialogBuilder.setView(root)
63 | }
64 | alertDialogBuilder.setPositiveButton(R.string.create_tunnel) { _, _ -> createTunnelAndDismiss() }
65 | alertDialogBuilder.setNegativeButton(R.string.cancel) { _, _ -> dismiss() }
66 | val dialog = alertDialogBuilder.create()
67 | dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
68 | return dialog
69 | }
70 |
71 | companion object {
72 | private const val KEY_CONFIG_TEXT = "config_text"
73 |
74 | fun newInstance(configText: String?): ConfigNamingDialogFragment {
75 | val extras = Bundle()
76 | extras.putString(KEY_CONFIG_TEXT, configText)
77 | val fragment = ConfigNamingDialogFragment()
78 | fragment.arguments = extras
79 | return fragment
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tv_tunnel_list_item.xml:
--------------------------------------------------------------------------------
1 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
24 |
25 |
28 |
29 |
32 |
33 |
34 |
46 |
47 |
50 |
51 |
60 |
61 |
70 |
71 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.util
7 |
8 | import android.os.Handler
9 | import android.os.Looper
10 | import android.util.Log
11 | import androidx.annotation.StringRes
12 | import androidx.biometric.BiometricManager
13 | import androidx.biometric.BiometricManager.Authenticators
14 | import androidx.biometric.BiometricPrompt
15 | import androidx.fragment.app.Fragment
16 | import com.wireguard.android.R
17 |
18 |
19 | object BiometricAuthenticator {
20 | private const val TAG = "WireGuard/BiometricAuthenticator"
21 |
22 | // Not all devices support strong biometric auth so we're allowing both device credentials as
23 | // well as weak biometrics.
24 | private const val allowedAuthenticators = Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
25 |
26 | sealed class Result {
27 | data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
28 | data class Failure(val code: Int?, val message: CharSequence) : Result()
29 | object HardwareUnavailableOrDisabled : Result()
30 | object Cancelled : Result()
31 | }
32 |
33 | fun authenticate(
34 | @StringRes dialogTitleRes: Int,
35 | fragment: Fragment,
36 | callback: (Result) -> Unit
37 | ) {
38 | val authCallback = object : BiometricPrompt.AuthenticationCallback() {
39 | override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
40 | super.onAuthenticationError(errorCode, errString)
41 | Log.d(TAG, "BiometricAuthentication error: errorCode=$errorCode, msg=$errString")
42 | callback(
43 | when (errorCode) {
44 | BiometricPrompt.ERROR_CANCELED, BiometricPrompt.ERROR_USER_CANCELED,
45 | BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
46 | Result.Cancelled
47 | }
48 |
49 | BiometricPrompt.ERROR_HW_NOT_PRESENT, BiometricPrompt.ERROR_HW_UNAVAILABLE,
50 | BiometricPrompt.ERROR_NO_BIOMETRICS, BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
51 | Result.HardwareUnavailableOrDisabled
52 | }
53 |
54 | else -> Result.Failure(errorCode, fragment.getString(R.string.biometric_auth_error_reason, errString))
55 | }
56 | )
57 | }
58 |
59 | override fun onAuthenticationFailed() {
60 | super.onAuthenticationFailed()
61 | callback(Result.Failure(null, fragment.getString(R.string.biometric_auth_error)))
62 | }
63 |
64 | override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
65 | super.onAuthenticationSucceeded(result)
66 | callback(Result.Success(result.cryptoObject))
67 | }
68 | }
69 | val biometricPrompt = BiometricPrompt(fragment, { Handler(Looper.getMainLooper()).post(it) }, authCallback)
70 | val promptInfo = BiometricPrompt.PromptInfo.Builder()
71 | .setTitle(fragment.getString(dialogTitleRes))
72 | .setAllowedAuthenticators(allowedAuthenticators)
73 | .build()
74 | if (BiometricManager.from(fragment.requireContext()).canAuthenticate(allowedAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS) {
75 | biometricPrompt.authenticate(promptInfo)
76 | } else {
77 | callback(Result.HardwareUnavailableOrDisabled)
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.preference
6 |
7 | import android.content.Context
8 | import android.util.AttributeSet
9 | import androidx.preference.Preference
10 | import com.wireguard.android.Application
11 | import com.wireguard.android.R
12 | import com.wireguard.android.util.ToolsInstaller
13 | import com.wireguard.android.util.lifecycleScope
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.launch
16 | import kotlinx.coroutines.withContext
17 |
18 | /**
19 | * Preference implementing a button that asynchronously runs `ToolsInstaller` and displays the
20 | * result as the preference summary.
21 | */
22 | class ToolsInstallerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
23 | private var state = State.INITIAL
24 | override fun getSummary() = context.getString(state.messageResourceId)
25 |
26 | override fun getTitle() = context.getString(R.string.tools_installer_title)
27 |
28 | override fun onAttached() {
29 | super.onAttached()
30 | lifecycleScope.launch {
31 | try {
32 | val state = withContext(Dispatchers.IO) { Application.getToolsInstaller().areInstalled() }
33 | when {
34 | state == ToolsInstaller.ERROR -> setState(State.INITIAL)
35 | state and ToolsInstaller.YES == ToolsInstaller.YES -> setState(State.ALREADY)
36 | state and (ToolsInstaller.MAGISK or ToolsInstaller.NO) == ToolsInstaller.MAGISK or ToolsInstaller.NO -> setState(State.INITIAL_MAGISK)
37 | state and (ToolsInstaller.SYSTEM or ToolsInstaller.NO) == ToolsInstaller.SYSTEM or ToolsInstaller.NO -> setState(State.INITIAL_SYSTEM)
38 | else -> setState(State.INITIAL)
39 | }
40 | } catch (_: Throwable) {
41 | setState(State.INITIAL)
42 | }
43 | }
44 | }
45 |
46 | override fun onClick() {
47 | setState(State.WORKING)
48 | lifecycleScope.launch {
49 | try {
50 | val result = withContext(Dispatchers.IO) { Application.getToolsInstaller().install() }
51 | when {
52 | result and (ToolsInstaller.YES or ToolsInstaller.MAGISK) == ToolsInstaller.YES or ToolsInstaller.MAGISK -> setState(State.SUCCESS_MAGISK)
53 | result and (ToolsInstaller.YES or ToolsInstaller.SYSTEM) == ToolsInstaller.YES or ToolsInstaller.SYSTEM -> setState(State.SUCCESS_SYSTEM)
54 | else -> setState(State.FAILURE)
55 | }
56 | } catch (_: Throwable) {
57 | setState(State.FAILURE)
58 | }
59 | }
60 | }
61 |
62 | private fun setState(state: State) {
63 | if (this.state == state) return
64 | this.state = state
65 | if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
66 | notifyChanged()
67 | }
68 |
69 | private enum class State(val messageResourceId: Int, val shouldEnableView: Boolean) {
70 | INITIAL(R.string.tools_installer_initial, true),
71 | ALREADY(R.string.tools_installer_already, false),
72 | FAILURE(R.string.tools_installer_failure, true),
73 | WORKING(R.string.tools_installer_working, false),
74 | INITIAL_SYSTEM(R.string.tools_installer_initial_system, true),
75 | SUCCESS_SYSTEM(R.string.tools_installer_success_system, false),
76 | INITIAL_MAGISK(R.string.tools_installer_initial_magisk, true),
77 | SUCCESS_MAGISK(R.string.tools_installer_success_magisk, false);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.activity
6 |
7 | import android.os.Bundle
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.databinding.CallbackRegistry
10 | import androidx.databinding.CallbackRegistry.NotifierCallback
11 | import androidx.lifecycle.lifecycleScope
12 | import com.wireguard.android.Application
13 | import com.wireguard.android.model.ObservableTunnel
14 | import kotlinx.coroutines.launch
15 |
16 | /**
17 | * Base class for activities that need to remember the currently-selected tunnel.
18 | */
19 | abstract class BaseActivity : AppCompatActivity() {
20 | private val selectionChangeRegistry = SelectionChangeRegistry()
21 | private var created = false
22 | var selectedTunnel: ObservableTunnel? = null
23 | set(value) {
24 | val oldTunnel = field
25 | if (oldTunnel == value) return
26 | field = value
27 | if (created) {
28 | if (!onSelectedTunnelChanged(oldTunnel, value)) {
29 | field = oldTunnel
30 | } else {
31 | selectionChangeRegistry.notifyCallbacks(oldTunnel, 0, value)
32 | }
33 | }
34 | }
35 |
36 | fun addOnSelectedTunnelChangedListener(listener: OnSelectedTunnelChangedListener) {
37 | selectionChangeRegistry.add(listener)
38 | }
39 |
40 | override fun onCreate(savedInstanceState: Bundle?) {
41 | super.onCreate(savedInstanceState)
42 |
43 | // Restore the saved tunnel if there is one; otherwise grab it from the arguments.
44 | val savedTunnelName = when {
45 | savedInstanceState != null -> savedInstanceState.getString(KEY_SELECTED_TUNNEL)
46 | intent != null -> intent.getStringExtra(KEY_SELECTED_TUNNEL)
47 | else -> null
48 | }
49 | if (savedTunnelName != null) {
50 | lifecycleScope.launch {
51 | val tunnel = Application.getTunnelManager().getTunnels()[savedTunnelName]
52 | if (tunnel == null)
53 | created = true
54 | selectedTunnel = tunnel
55 | created = true
56 | }
57 | } else {
58 | created = true
59 | }
60 | }
61 |
62 | override fun onSaveInstanceState(outState: Bundle) {
63 | if (selectedTunnel != null) outState.putString(KEY_SELECTED_TUNNEL, selectedTunnel!!.name)
64 | super.onSaveInstanceState(outState)
65 | }
66 |
67 | protected abstract fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean
68 |
69 | fun removeOnSelectedTunnelChangedListener(
70 | listener: OnSelectedTunnelChangedListener
71 | ) {
72 | selectionChangeRegistry.remove(listener)
73 | }
74 |
75 | interface OnSelectedTunnelChangedListener {
76 | fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?)
77 | }
78 |
79 | private class SelectionChangeNotifier : NotifierCallback() {
80 | override fun onNotifyCallback(
81 | listener: OnSelectedTunnelChangedListener,
82 | oldTunnel: ObservableTunnel?,
83 | ignored: Int,
84 | newTunnel: ObservableTunnel?
85 | ) {
86 | listener.onSelectedTunnelChanged(oldTunnel, newTunnel)
87 | }
88 | }
89 |
90 | private class SelectionChangeRegistry :
91 | CallbackRegistry(SelectionChangeNotifier())
92 |
93 | companion object {
94 | private const val KEY_SELECTED_TUNNEL = "selected_tunnel"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/config/BadConfigException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.config;
7 |
8 | import com.wireguard.crypto.KeyFormatException;
9 | import com.wireguard.util.NonNullForAll;
10 |
11 | import androidx.annotation.Nullable;
12 |
13 | @NonNullForAll
14 | public class BadConfigException extends Exception {
15 | private final Location location;
16 | private final Reason reason;
17 | private final Section section;
18 | @Nullable private final CharSequence text;
19 |
20 | private BadConfigException(final Section section, final Location location,
21 | final Reason reason, @Nullable final CharSequence text,
22 | @Nullable final Throwable cause) {
23 | super(cause);
24 | this.section = section;
25 | this.location = location;
26 | this.reason = reason;
27 | this.text = text;
28 | }
29 |
30 | public BadConfigException(final Section section, final Location location,
31 | final Reason reason, @Nullable final CharSequence text) {
32 | this(section, location, reason, text, null);
33 | }
34 |
35 | public BadConfigException(final Section section, final Location location,
36 | final KeyFormatException cause) {
37 | this(section, location, Reason.INVALID_KEY, null, cause);
38 | }
39 |
40 | public BadConfigException(final Section section, final Location location,
41 | @Nullable final CharSequence text,
42 | final NumberFormatException cause) {
43 | this(section, location, Reason.INVALID_NUMBER, text, cause);
44 | }
45 |
46 | public BadConfigException(final Section section, final Location location,
47 | final ParseException cause) {
48 | this(section, location, Reason.INVALID_VALUE, cause.getText(), cause);
49 | }
50 |
51 | public Location getLocation() {
52 | return location;
53 | }
54 |
55 | public Reason getReason() {
56 | return reason;
57 | }
58 |
59 | public Section getSection() {
60 | return section;
61 | }
62 |
63 | @Nullable
64 | public CharSequence getText() {
65 | return text;
66 | }
67 |
68 | public enum Location {
69 | TOP_LEVEL(""),
70 | ADDRESS("Address"),
71 | ALLOWED_IPS("AllowedIPs"),
72 | DNS("DNS"),
73 | ENDPOINT("Endpoint"),
74 | EXCLUDED_APPLICATIONS("ExcludedApplications"),
75 | INCLUDED_APPLICATIONS("IncludedApplications"),
76 | LISTEN_PORT("ListenPort"),
77 | MTU("MTU"),
78 | PERSISTENT_KEEPALIVE("PersistentKeepalive"),
79 | PRE_SHARED_KEY("PresharedKey"),
80 | PRIVATE_KEY("PrivateKey"),
81 | PUBLIC_KEY("PublicKey");
82 |
83 | private final String name;
84 |
85 | Location(final String name) {
86 | this.name = name;
87 | }
88 |
89 | public String getName() {
90 | return name;
91 | }
92 | }
93 |
94 | public enum Reason {
95 | INVALID_KEY,
96 | INVALID_NUMBER,
97 | INVALID_VALUE,
98 | MISSING_ATTRIBUTE,
99 | MISSING_SECTION,
100 | SYNTAX_ERROR,
101 | UNKNOWN_ATTRIBUTE,
102 | UNKNOWN_SECTION
103 | }
104 |
105 | public enum Section {
106 | CONFIG("Config"),
107 | INTERFACE("Interface"),
108 | PEER("Peer");
109 |
110 | private final String name;
111 |
112 | Section(final String name) {
113 | this.name = name;
114 | }
115 |
116 | public String getName() {
117 | return name;
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/tunnel_list_fragment.xml:
--------------------------------------------------------------------------------
1 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
20 |
21 |
24 |
25 |
26 |
32 |
33 |
47 |
48 |
55 |
56 |
64 |
65 |
73 |
74 |
75 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.util;
7 |
8 | import android.content.Context;
9 | import android.os.Build;
10 | import android.util.Log;
11 |
12 | import com.wireguard.util.NonNullForAll;
13 |
14 | import java.io.File;
15 | import java.io.FileOutputStream;
16 | import java.io.IOException;
17 | import java.io.InputStream;
18 | import java.util.Arrays;
19 | import java.util.Collection;
20 | import java.util.HashSet;
21 | import java.util.zip.ZipEntry;
22 | import java.util.zip.ZipFile;
23 |
24 | import androidx.annotation.RestrictTo;
25 | import androidx.annotation.RestrictTo.Scope;
26 |
27 | @NonNullForAll
28 | @RestrictTo(Scope.LIBRARY_GROUP)
29 | public final class SharedLibraryLoader {
30 | private static final String TAG = "WireGuard/SharedLibraryLoader";
31 |
32 | private SharedLibraryLoader() {
33 | }
34 |
35 | public static boolean extractLibrary(final Context context, final String libName, final File destination) throws IOException {
36 | final Collection apks = new HashSet<>();
37 | if (context.getApplicationInfo().sourceDir != null)
38 | apks.add(context.getApplicationInfo().sourceDir);
39 | if (context.getApplicationInfo().splitSourceDirs != null)
40 | apks.addAll(Arrays.asList(context.getApplicationInfo().splitSourceDirs));
41 |
42 | for (final String abi : Build.SUPPORTED_ABIS) {
43 | for (final String apk : apks) {
44 | try (final ZipFile zipFile = new ZipFile(new File(apk), ZipFile.OPEN_READ)) {
45 | final String mappedLibName = System.mapLibraryName(libName);
46 | final String libZipPath = "lib" + File.separatorChar + abi + File.separatorChar + mappedLibName;
47 | final ZipEntry zipEntry = zipFile.getEntry(libZipPath);
48 | if (zipEntry == null)
49 | continue;
50 | Log.d(TAG, "Extracting apk:/" + libZipPath + " to " + destination.getAbsolutePath());
51 | try (final FileOutputStream out = new FileOutputStream(destination);
52 | final InputStream in = zipFile.getInputStream(zipEntry)) {
53 | int len;
54 | final byte[] buffer = new byte[1024 * 32];
55 | while ((len = in.read(buffer)) != -1) {
56 | out.write(buffer, 0, len);
57 | }
58 | out.getFD().sync();
59 | }
60 | }
61 | return true;
62 | }
63 | }
64 | return false;
65 | }
66 |
67 | public static void loadSharedLibrary(final Context context, final String libName) {
68 | Throwable noAbiException;
69 | try {
70 | System.loadLibrary(libName);
71 | return;
72 | } catch (final UnsatisfiedLinkError e) {
73 | Log.d(TAG, "Failed to load library normally, so attempting to extract from apk", e);
74 | noAbiException = e;
75 | }
76 | File f = null;
77 | try {
78 | f = File.createTempFile("lib", ".so", context.getCodeCacheDir());
79 | if (extractLibrary(context, libName, f)) {
80 | System.load(f.getAbsolutePath());
81 | return;
82 | }
83 | } catch (final Exception e) {
84 | Log.d(TAG, "Failed to load library apk:/" + libName, e);
85 | noAbiException = e;
86 | } finally {
87 | if (f != null)
88 | // noinspection ResultOfMethodCallIgnored
89 | f.delete();
90 | }
91 | if (noAbiException instanceof RuntimeException)
92 | throw (RuntimeException) noAbiException;
93 | throw new RuntimeException(noAbiException);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/android/backend/Statistics.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.android.backend;
7 |
8 | import android.os.SystemClock;
9 |
10 | import com.wireguard.crypto.Key;
11 | import com.wireguard.util.NonNullForAll;
12 |
13 | import java.util.HashMap;
14 | import java.util.Map;
15 | import java.util.Objects;
16 |
17 | import androidx.annotation.Nullable;
18 |
19 | /**
20 | * Class representing transfer statistics for a {@link Tunnel} instance.
21 | */
22 | @NonNullForAll
23 | public class Statistics {
24 | public record PeerStats(long rxBytes, long txBytes, long latestHandshakeEpochMillis) { }
25 | private final Map stats = new HashMap<>();
26 | private long lastTouched = SystemClock.elapsedRealtime();
27 |
28 | Statistics() {
29 | }
30 |
31 | /**
32 | * Add a peer and its current stats to the internal map.
33 | *
34 | * @param key A WireGuard public key bound to a particular peer
35 | * @param rxBytes The received traffic for the {@link com.wireguard.config.Peer} referenced by
36 | * the provided {@link Key}. This value is in bytes
37 | * @param txBytes The transmitted traffic for the {@link com.wireguard.config.Peer} referenced by
38 | * the provided {@link Key}. This value is in bytes.
39 | * @param latestHandshake The timestamp of the latest handshake for the {@link com.wireguard.config.Peer}
40 | * referenced by the provided {@link Key}. The value is in epoch milliseconds.
41 | */
42 | void add(final Key key, final long rxBytes, final long txBytes, final long latestHandshake) {
43 | stats.put(key, new PeerStats(rxBytes, txBytes, latestHandshake));
44 | lastTouched = SystemClock.elapsedRealtime();
45 | }
46 |
47 | /**
48 | * Check if the statistics are stale, indicating the need for the {@link Backend} to update them.
49 | *
50 | * @return boolean indicating if the current statistics instance has stale values.
51 | */
52 | public boolean isStale() {
53 | return SystemClock.elapsedRealtime() - lastTouched > 900;
54 | }
55 |
56 | /**
57 | * Get the statistics for the {@link com.wireguard.config.Peer} referenced by the provided {@link Key}
58 | *
59 | * @param peer A {@link Key} representing a {@link com.wireguard.config.Peer}.
60 | * @return a {@link PeerStats} representing various statistics about this peer.
61 | */
62 | @Nullable
63 | public PeerStats peer(final Key peer) {
64 | return stats.get(peer);
65 | }
66 |
67 | /**
68 | * Get the list of peers being tracked by this instance.
69 | *
70 | * @return An array of {@link Key} instances representing WireGuard
71 | * {@link com.wireguard.config.Peer}s
72 | */
73 | public Key[] peers() {
74 | return stats.keySet().toArray(new Key[0]);
75 | }
76 |
77 | /**
78 | * Get the total received traffic by all the peers being tracked by this instance
79 | *
80 | * @return a long representing the number of bytes received by the peers being tracked.
81 | */
82 | public long totalRx() {
83 | long rx = 0;
84 | for (final PeerStats val : stats.values()) {
85 | rx += val.rxBytes;
86 | }
87 | return rx;
88 | }
89 |
90 | /**
91 | * Get the total transmitted traffic by all the peers being tracked by this instance
92 | *
93 | * @return a long representing the number of bytes transmitted by the peers being tracked.
94 | */
95 | public long totalTx() {
96 | long tx = 0;
97 | for (final PeerStats val : stats.values()) {
98 | tx += val.txBytes;
99 | }
100 | return tx;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.preference
6 |
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.util.AttributeSet
10 | import android.util.Log
11 | import androidx.lifecycle.lifecycleScope
12 | import androidx.preference.Preference
13 | import com.wireguard.android.Application
14 | import com.wireguard.android.R
15 | import com.wireguard.android.activity.SettingsActivity
16 | import com.wireguard.android.backend.Tunnel
17 | import com.wireguard.android.backend.WgQuickBackend
18 | import com.wireguard.android.util.UserKnobs
19 | import com.wireguard.android.util.activity
20 | import com.wireguard.android.util.lifecycleScope
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.SupervisorJob
23 | import kotlinx.coroutines.async
24 | import kotlinx.coroutines.awaitAll
25 | import kotlinx.coroutines.launch
26 | import kotlinx.coroutines.withContext
27 | import kotlin.system.exitProcess
28 |
29 | class KernelModuleEnablerPreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) {
30 | private var state = State.UNKNOWN
31 |
32 | init {
33 | isVisible = false
34 | lifecycleScope.launch {
35 | setState(if (Application.getBackend() is WgQuickBackend) State.ENABLED else State.DISABLED)
36 | }
37 | }
38 |
39 | override fun getSummary() = if (state == State.UNKNOWN) "" else context.getString(state.summaryResourceId)
40 |
41 | override fun getTitle() = if (state == State.UNKNOWN) "" else context.getString(state.titleResourceId)
42 |
43 | override fun onClick() {
44 | activity.lifecycleScope.launch {
45 | if (state == State.DISABLED) {
46 | setState(State.ENABLING)
47 | UserKnobs.setEnableKernelModule(true)
48 | } else if (state == State.ENABLED) {
49 | setState(State.DISABLING)
50 | UserKnobs.setEnableKernelModule(false)
51 | }
52 | val observableTunnels = Application.getTunnelManager().getTunnels()
53 | val downings = observableTunnels.map { async(SupervisorJob()) { it.setStateAsync(Tunnel.State.DOWN) } }
54 | try {
55 | downings.awaitAll()
56 | withContext(Dispatchers.IO) {
57 | val restartIntent = Intent(context, SettingsActivity::class.java)
58 | restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
59 | restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
60 | Application.get().startActivity(restartIntent)
61 | exitProcess(0)
62 | }
63 | } catch (e: Throwable) {
64 | Log.e(TAG, Log.getStackTraceString(e))
65 | }
66 | }
67 | }
68 |
69 | private fun setState(state: State) {
70 | if (this.state == state) return
71 | this.state = state
72 | if (isEnabled != state.shouldEnableView) isEnabled = state.shouldEnableView
73 | if (isVisible != state.visible) isVisible = state.visible
74 | notifyChanged()
75 | }
76 |
77 | private enum class State(val titleResourceId: Int, val summaryResourceId: Int, val shouldEnableView: Boolean, val visible: Boolean) {
78 | UNKNOWN(0, 0, false, false),
79 | ENABLED(R.string.module_enabler_enabled_title, R.string.module_enabler_enabled_summary, true, true),
80 | DISABLED(R.string.module_enabler_disabled_title, R.string.module_enabler_disabled_summary, true, true),
81 | ENABLING(R.string.module_enabler_disabled_title, R.string.success_application_will_restart, false, true),
82 | DISABLING(R.string.module_enabler_enabled_title, R.string.success_application_will_restart, false, true);
83 | }
84 |
85 | companion object {
86 | private const val TAG = "WireGuard/KernelModuleEnablerPreference"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/ui/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 | #1a73e8
7 | #005BC0
8 | #FFFFFF
9 | #D8E2FF
10 | #001A41
11 | #565E71
12 | #FFFFFF
13 | #DBE2F9
14 | #131B2C
15 | #715574
16 | #FFFFFF
17 | #FBD7FC
18 | #29132D
19 | #BA1A1A
20 | #FFDAD6
21 | #FFFFFF
22 | #410002
23 | #FEFBFF
24 | #1B1B1F
25 | #FEFBFF
26 | #1B1B1F
27 | #E1E2EC
28 | #44474F
29 | #74777F
30 | #F2F0F4
31 | #303033
32 | #ADC7FF
33 | #000000
34 | #005BC0
35 | #C4C6D0
36 | #000000
37 | #ADC7FF
38 | #002E68
39 | #004493
40 | #D8E2FF
41 | #BFC6DC
42 | #283041
43 | #3F4759
44 | #DBE2F9
45 | #DEBCDF
46 | #402843
47 | #583E5B
48 | #FBD7FC
49 | #FFB4AB
50 | #93000A
51 | #690005
52 | #FFDAD6
53 | #1B1B1F
54 | #E3E2E6
55 | #1B1B1F
56 | #E3E2E6
57 | #44474F
58 | #C4C6D0
59 | #8E9099
60 | #1B1B1F
61 | #E3E2E6
62 | #005BC0
63 | #000000
64 | #ADC7FF
65 | #44474F
66 | #000000
67 |
68 |
--------------------------------------------------------------------------------
/ui/src/main/res/layout/add_tunnels_bottom_sheet.xml:
--------------------------------------------------------------------------------
1 |
5 |
11 |
12 |
34 |
35 |
58 |
59 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/fragment/AddTunnelsSheet.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.fragment
6 |
7 | import android.content.pm.PackageManager
8 | import android.graphics.drawable.GradientDrawable
9 | import android.os.Bundle
10 | import android.view.LayoutInflater
11 | import android.view.View
12 | import android.view.ViewGroup
13 | import android.view.ViewTreeObserver
14 | import android.widget.FrameLayout
15 | import androidx.core.os.bundleOf
16 | import androidx.fragment.app.setFragmentResult
17 | import com.google.android.material.bottomsheet.BottomSheetBehavior
18 | import com.google.android.material.bottomsheet.BottomSheetDialog
19 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
20 | import com.wireguard.android.R
21 | import com.wireguard.android.util.resolveAttribute
22 |
23 | class AddTunnelsSheet : BottomSheetDialogFragment() {
24 |
25 | private var behavior: BottomSheetBehavior? = null
26 | private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
27 | override fun onSlide(bottomSheet: View, slideOffset: Float) {
28 | }
29 |
30 | override fun onStateChanged(bottomSheet: View, newState: Int) {
31 | if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
32 | dismiss()
33 | }
34 | }
35 | }
36 |
37 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
38 | if (savedInstanceState != null) dismiss()
39 | val view = inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
40 | if (activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) != true) {
41 | val qrcode = view.findViewById(R.id.create_from_qrcode)
42 | qrcode.isEnabled = false
43 | qrcode.visibility = View.GONE
44 | }
45 | return view
46 | }
47 |
48 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
49 | super.onViewCreated(view, savedInstanceState)
50 | view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
51 | override fun onGlobalLayout() {
52 | view.viewTreeObserver.removeOnGlobalLayoutListener(this)
53 | val dialog = dialog as BottomSheetDialog? ?: return
54 | behavior = dialog.behavior
55 | behavior?.apply {
56 | state = BottomSheetBehavior.STATE_EXPANDED
57 | peekHeight = 0
58 | addBottomSheetCallback(bottomSheetCallback)
59 | }
60 | dialog.findViewById(R.id.create_empty)?.setOnClickListener {
61 | dismiss()
62 | onRequestCreateConfig()
63 | }
64 | dialog.findViewById(R.id.create_from_file)?.setOnClickListener {
65 | dismiss()
66 | onRequestImportConfig()
67 | }
68 | dialog.findViewById(R.id.create_from_qrcode)?.setOnClickListener {
69 | dismiss()
70 | onRequestScanQRCode()
71 | }
72 | }
73 | })
74 | val gradientDrawable = GradientDrawable().apply {
75 | setColor(requireContext().resolveAttribute(com.google.android.material.R.attr.colorSurface))
76 | }
77 | view.background = gradientDrawable
78 | }
79 |
80 | override fun dismiss() {
81 | super.dismiss()
82 | behavior?.removeBottomSheetCallback(bottomSheetCallback)
83 | }
84 |
85 | private fun onRequestCreateConfig() {
86 | setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_CREATE))
87 | }
88 |
89 | private fun onRequestImportConfig() {
90 | setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_IMPORT))
91 | }
92 |
93 | private fun onRequestScanQRCode() {
94 | setFragmentResult(REQUEST_KEY_NEW_TUNNEL, bundleOf(REQUEST_METHOD to REQUEST_SCAN))
95 | }
96 |
97 | companion object {
98 | const val REQUEST_KEY_NEW_TUNNEL = "request_new_tunnel"
99 | const val REQUEST_METHOD = "request_method"
100 | const val REQUEST_CREATE = "request_create"
101 | const val REQUEST_IMPORT = "request_import"
102 | const val REQUEST_SCAN = "request_scan"
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 | package com.wireguard.android.databinding
6 |
7 | import android.content.Context
8 | import android.view.LayoutInflater
9 | import android.view.ViewGroup
10 | import androidx.databinding.DataBindingUtil
11 | import androidx.databinding.ObservableList
12 | import androidx.databinding.ViewDataBinding
13 | import androidx.recyclerview.widget.RecyclerView
14 | import com.wireguard.android.BR
15 | import java.lang.ref.WeakReference
16 |
17 | /**
18 | * A generic `RecyclerView.Adapter` backed by a `ObservableKeyedArrayList`.
19 | */
20 | class ObservableKeyedRecyclerViewAdapter> internal constructor(
21 | context: Context, private val layoutId: Int,
22 | list: ObservableKeyedArrayList?
23 | ) : RecyclerView.Adapter() {
24 | private val callback = OnListChangedCallback(this)
25 | private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
26 | private var list: ObservableKeyedArrayList? = null
27 | private var rowConfigurationHandler: RowConfigurationHandler? = null
28 |
29 | private fun getItem(position: Int): E? = if (list == null || position < 0 || position >= list!!.size) null else list?.get(position)
30 |
31 | override fun getItemCount() = list?.size ?: 0
32 |
33 | override fun getItemId(position: Int) = (getKey(position)?.hashCode() ?: -1).toLong()
34 |
35 | private fun getKey(position: Int): K? = getItem(position)?.key
36 |
37 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
38 | holder.binding.setVariable(BR.collection, list)
39 | holder.binding.setVariable(BR.key, getKey(position))
40 | holder.binding.setVariable(BR.item, getItem(position))
41 | holder.binding.executePendingBindings()
42 | if (rowConfigurationHandler != null) {
43 | val item = getItem(position)
44 | if (item != null) {
45 | rowConfigurationHandler?.onConfigureRow(holder.binding, item, position)
46 | }
47 | }
48 | }
49 |
50 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(DataBindingUtil.inflate(layoutInflater, layoutId, parent, false))
51 |
52 | fun setList(newList: ObservableKeyedArrayList?) {
53 | list?.removeOnListChangedCallback(callback)
54 | list = newList
55 | list?.addOnListChangedCallback(callback)
56 | notifyDataSetChanged()
57 | }
58 |
59 | fun setRowConfigurationHandler(rowConfigurationHandler: RowConfigurationHandler<*, *>?) {
60 | @Suppress("UNCHECKED_CAST")
61 | this.rowConfigurationHandler = rowConfigurationHandler as? RowConfigurationHandler
62 | }
63 |
64 | interface RowConfigurationHandler {
65 | fun onConfigureRow(binding: B, item: T, position: Int)
66 | }
67 |
68 | private class OnListChangedCallback> constructor(adapter: ObservableKeyedRecyclerViewAdapter<*, E>) : ObservableList.OnListChangedCallback>() {
69 | private val weakAdapter: WeakReference> = WeakReference(adapter)
70 |
71 | override fun onChanged(sender: ObservableList) {
72 | val adapter = weakAdapter.get()
73 | if (adapter != null)
74 | adapter.notifyDataSetChanged()
75 | else
76 | sender.removeOnListChangedCallback(this)
77 | }
78 |
79 | override fun onItemRangeChanged(sender: ObservableList, positionStart: Int,
80 | itemCount: Int) {
81 | onChanged(sender)
82 | }
83 |
84 | override fun onItemRangeInserted(sender: ObservableList, positionStart: Int,
85 | itemCount: Int) {
86 | onChanged(sender)
87 | }
88 |
89 | override fun onItemRangeMoved(sender: ObservableList, fromPosition: Int,
90 | toPosition: Int, itemCount: Int) {
91 | onChanged(sender)
92 | }
93 |
94 | override fun onItemRangeRemoved(sender: ObservableList, positionStart: Int,
95 | itemCount: Int) {
96 | onChanged(sender)
97 | }
98 |
99 | }
100 |
101 | class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
102 |
103 | init {
104 | setList(list)
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/ui/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
5 |
10 |
15 |
16 |
17 |
20 |
23 |
24 |
25 |
28 |
29 |
32 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/tunnel/src/main/java/com/wireguard/config/InetAddresses.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2017-2025 WireGuard LLC. All Rights Reserved.
3 | * SPDX-License-Identifier: Apache-2.0
4 | */
5 |
6 | package com.wireguard.config;
7 |
8 | import com.wireguard.util.NonNullForAll;
9 |
10 | import java.lang.reflect.Method;
11 | import java.net.Inet4Address;
12 | import java.net.Inet6Address;
13 | import java.net.InetAddress;
14 | import java.net.UnknownHostException;
15 | import java.util.regex.Pattern;
16 |
17 | import androidx.annotation.Nullable;
18 |
19 | /**
20 | * Utility methods for creating instances of {@link InetAddress}.
21 | */
22 | @NonNullForAll
23 | public final class InetAddresses {
24 | @Nullable private static final Method PARSER_METHOD;
25 | private static final Pattern WONT_TOUCH_RESOLVER = Pattern.compile("^(((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?)|((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$");
26 | private static final Pattern VALID_HOSTNAME = Pattern.compile("^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\\.?$");
27 |
28 | static {
29 | Method m = null;
30 | try {
31 | if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q)
32 | // noinspection JavaReflectionMemberAccess
33 | m = InetAddress.class.getMethod("parseNumericAddress", String.class);
34 | } catch (final Exception ignored) {
35 | }
36 | PARSER_METHOD = m;
37 | }
38 |
39 | private InetAddresses() {
40 | }
41 |
42 | /**
43 | * Determines whether input is a valid DNS hostname.
44 | *
45 | * @param maybeHostname a string that is possibly a DNS hostname
46 | * @return whether or not maybeHostname is a valid DNS hostname
47 | */
48 | public static boolean isHostname(final CharSequence maybeHostname) {
49 | return VALID_HOSTNAME.matcher(maybeHostname).matches();
50 | }
51 |
52 | /**
53 | * Parses a numeric IPv4 or IPv6 address without performing any DNS lookups.
54 | *
55 | * @param address a string representing the IP address
56 | * @return an instance of {@link Inet4Address} or {@link Inet6Address}, as appropriate
57 | */
58 | public static InetAddress parse(final String address) throws ParseException {
59 | if (address.isEmpty())
60 | throw new ParseException(InetAddress.class, address, "Empty address");
61 | try {
62 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
63 | return android.net.InetAddresses.parseNumericAddress(address);
64 | else if (PARSER_METHOD != null)
65 | return (InetAddress) PARSER_METHOD.invoke(null, address);
66 | else
67 | throw new NoSuchMethodException("parseNumericAddress");
68 | } catch (final IllegalArgumentException e) {
69 | throw new ParseException(InetAddress.class, address, e);
70 | } catch (final Exception e) {
71 | final Throwable cause = e.getCause();
72 | // Re-throw parsing exceptions with the original type, as callers might try to catch
73 | // them. On the other hand, callers cannot be expected to handle reflection failures.
74 | if (cause instanceof IllegalArgumentException)
75 | throw new ParseException(InetAddress.class, address, cause);
76 | try {
77 | if (WONT_TOUCH_RESOLVER.matcher(address).matches())
78 | return InetAddress.getByName(address);
79 | else
80 | throw new ParseException(InetAddress.class, address, "Not an IP address");
81 | } catch (final UnknownHostException f) {
82 | throw new ParseException(InetAddress.class, address, f);
83 | }
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------