├── .gitignore ├── .gitmodules ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── copyright │ ├── Default.xml │ └── profiles_settings.xml └── inspectionProfiles │ ├── Default.xml │ └── profiles_settings.xml ├── COPYING ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── settings.gradle.kts ├── sync-crowdin.sh ├── tunnel ├── build.gradle.kts ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── wireguard │ │ │ ├── android │ │ │ ├── backend │ │ │ │ ├── Backend.java │ │ │ │ ├── BackendException.java │ │ │ │ ├── GoBackend.java │ │ │ │ ├── Statistics.java │ │ │ │ ├── Tunnel.java │ │ │ │ └── WgQuickBackend.java │ │ │ └── util │ │ │ │ ├── RootShell.java │ │ │ │ ├── SharedLibraryLoader.java │ │ │ │ └── ToolsInstaller.java │ │ │ ├── config │ │ │ ├── Attribute.java │ │ │ ├── BadConfigException.java │ │ │ ├── Config.java │ │ │ ├── InetAddresses.java │ │ │ ├── InetEndpoint.java │ │ │ ├── InetNetwork.java │ │ │ ├── Interface.java │ │ │ ├── ParseException.java │ │ │ └── Peer.java │ │ │ ├── crypto │ │ │ ├── Curve25519.java │ │ │ ├── Key.java │ │ │ ├── KeyFormatException.java │ │ │ └── KeyPair.java │ │ │ └── util │ │ │ └── NonNullForAll.java │ └── test │ │ ├── java │ │ └── com │ │ │ └── wireguard │ │ │ └── config │ │ │ ├── BadConfigExceptionTest.java │ │ │ └── ConfigTest.java │ │ └── resources │ │ ├── broken.conf │ │ ├── invalid-key.conf │ │ ├── invalid-number.conf │ │ ├── invalid-value.conf │ │ ├── missing-attribute.conf │ │ ├── missing-section.conf │ │ ├── syntax-error.conf │ │ ├── unknown-attribute.conf │ │ ├── unknown-section.conf │ │ └── working.conf └── tools │ ├── CMakeLists.txt │ ├── libwg-go │ ├── .gitignore │ ├── Makefile │ ├── api-android.go │ ├── go.mod │ ├── go.sum │ ├── goruntime-boottime-over-monotonic.diff │ └── jni.c │ └── ndk-compat │ ├── compat.c │ └── compat.h └── ui ├── build.gradle.kts ├── proguard-android-optimize.txt ├── sampledata └── interface_names.json └── src ├── debug └── res │ └── values │ └── strings.xml ├── googleplay └── AndroidManifest.xml └── main ├── AndroidManifest.xml ├── java └── com │ └── wireguard │ └── android │ ├── Application.kt │ ├── BootShutdownReceiver.kt │ ├── QuickTileService.kt │ ├── activity │ ├── BaseActivity.kt │ ├── LogViewerActivity.kt │ ├── MainActivity.kt │ ├── SettingsActivity.kt │ ├── TunnelCreatorActivity.kt │ ├── TunnelToggleActivity.kt │ └── TvMainActivity.kt │ ├── configStore │ ├── ConfigStore.kt │ └── FileConfigStore.kt │ ├── databinding │ ├── BindingAdapters.kt │ ├── ItemChangeListener.kt │ ├── Keyed.kt │ ├── ObservableKeyedArrayList.kt │ ├── ObservableKeyedRecyclerViewAdapter.kt │ └── ObservableSortedKeyedArrayList.kt │ ├── fragment │ ├── AddTunnelsSheet.kt │ ├── AppListDialogFragment.kt │ ├── BaseFragment.kt │ ├── ConfigNamingDialogFragment.kt │ ├── TunnelDetailFragment.kt │ ├── TunnelEditorFragment.kt │ └── TunnelListFragment.kt │ ├── model │ ├── ApplicationData.kt │ ├── ObservableTunnel.kt │ ├── TunnelComparator.kt │ └── TunnelManager.kt │ ├── preference │ ├── DonatePreference.kt │ ├── KernelModuleEnablerPreference.kt │ ├── PreferencesPreferenceDataStore.kt │ ├── QuickTilePreference.kt │ ├── ToolsInstallerPreference.kt │ ├── VersionPreference.kt │ └── ZipExporterPreference.kt │ ├── updater │ ├── Ed25519.java │ ├── SnackbarUpdateShower.kt │ └── Updater.kt │ ├── util │ ├── AdminKnobs.kt │ ├── BiometricAuthenticator.kt │ ├── ClipboardUtils.kt │ ├── DownloadsFileSaver.kt │ ├── ErrorMessages.kt │ ├── Extensions.kt │ ├── QrCodeFromFileScanner.kt │ ├── QuantityFormatter.kt │ ├── TunnelImporter.kt │ └── UserKnobs.kt │ ├── viewmodel │ ├── ConfigProxy.kt │ ├── InterfaceProxy.kt │ └── PeerProxy.kt │ └── widget │ ├── KeyInputFilter.kt │ ├── MultiselectableRelativeLayout.kt │ ├── NameInputFilter.kt │ ├── SlashDrawable.kt │ ├── ToggleSwitch.kt │ └── TvCardView.kt └── res ├── anim ├── scale_down.xml └── scale_up.xml ├── color └── tv_list_item_tint.xml ├── drawable ├── ic_action_add_white.xml ├── ic_action_delete.xml ├── ic_action_edit.xml ├── ic_action_generate.xml ├── ic_action_open.xml ├── ic_action_save.xml ├── ic_action_scan_qr_code.xml ├── ic_action_select_all.xml ├── ic_action_share_white.xml ├── ic_arrow_back.xml ├── ic_launcher_foreground.xml ├── ic_settings.xml ├── ic_tile.xml ├── list_item_background.xml └── tv_logo_banner.xml ├── layout-sw600dp └── main_activity.xml ├── layout ├── add_tunnels_bottom_sheet.xml ├── app_list_dialog_fragment.xml ├── app_list_item.xml ├── config_naming_dialog_fragment.xml ├── log_viewer_activity.xml ├── log_viewer_entry.xml ├── main_activity.xml ├── tunnel_detail_fragment.xml ├── tunnel_detail_peer.xml ├── tunnel_editor_fragment.xml ├── tunnel_editor_peer.xml ├── tunnel_list_fragment.xml ├── tunnel_list_item.xml ├── tv_activity.xml ├── tv_file_list_item.xml └── tv_tunnel_list_item.xml ├── menu ├── config_editor.xml ├── log_viewer.xml ├── main_activity.xml ├── tunnel_detail.xml └── tunnel_list_action_mode.xml ├── mipmap-anydpi-v26 ├── ic_launcher.xml └── ic_launcher_round.xml ├── mipmap-hdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-mdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xhdpi ├── banner.png ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxxhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── resources.properties ├── values-ar-rSA └── strings.xml ├── values-ca-rES └── strings.xml ├── values-cs-rCZ └── strings.xml ├── values-da-rDK └── strings.xml ├── values-de └── strings.xml ├── values-el-rGR └── strings.xml ├── values-es-rES └── strings.xml ├── values-et-rEE └── strings.xml ├── values-fa-rIR └── strings.xml ├── values-fi-rFI └── strings.xml ├── values-fr └── strings.xml ├── values-hi-rIN └── strings.xml ├── values-hi └── strings.xml ├── values-in └── strings.xml ├── values-it └── strings.xml ├── values-ja └── strings.xml ├── values-ko-rKR └── strings.xml ├── values-night ├── bools.xml ├── logviewer_colors.xml └── themes.xml ├── values-nl-rNL └── strings.xml ├── values-no-rNO └── strings.xml ├── values-pa-rIN └── strings.xml ├── values-pl-rPL └── strings.xml ├── values-pt-rBR └── strings.xml ├── values-pt-rPT └── strings.xml ├── values-ro-rRO └── strings.xml ├── values-ru └── strings.xml ├── values-si-rLK └── strings.xml ├── values-sk-rSK └── strings.xml ├── values-sl └── strings.xml ├── values-sv-rSE └── strings.xml ├── values-tr-rTR └── strings.xml ├── values-uk-rUA └── strings.xml ├── values-v23 └── styles.xml ├── values-v27 └── styles.xml ├── values-vi-rVN └── strings.xml ├── values-zh-rCN └── strings.xml ├── values-zh-rTW └── strings.xml ├── values ├── attrs.xml ├── bools.xml ├── colors.xml ├── dimens.xml ├── ic_launcher_background.xml ├── ids.xml ├── logviewer_colors.xml ├── strings.xml ├── styles.xml └── themes.xml └── xml ├── app_restrictions.xml └── preferences.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle/ 2 | /.idea/*.xml 3 | /.idea/caches/ 4 | /.idea/dictionaries/ 5 | /.idea/libraries/ 6 | /captures/ 7 | /local.properties 8 | .DS_Store 9 | .cxx/ 10 | Thumbs.db 11 | build/ 12 | *.apk 13 | *.class 14 | *.dex 15 | *.iml 16 | *.jks 17 | gradlew.bat 18 | maint/ 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tunnel/tools/wireguard-tools"] 2 | path = tunnel/tools/wireguard-tools 3 | url = https://git.zx2c4.com/wireguard-tools 4 | [submodule "tunnel/tools/elf-cleaner"] 5 | path = tunnel/tools/elf-cleaner 6 | url = https://github.com/termux/termux-elf-cleaner 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/copyright/Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) apply false 3 | alias(libs.plugins.android.library) apply false 4 | alias(libs.plugins.kotlin.android) apply false 5 | alias(libs.plugins.kotlin.kapt) apply false 6 | } 7 | 8 | tasks { 9 | wrapper { 10 | gradleVersion = "8.1.1" 11 | distributionSha256Sum = "e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | wireguardVersionCode=509 2 | wireguardVersionName=1.0.20230707 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 | # Enable non-transitive R class namespacing where each library only contains 26 | # references to the resources it declares instead of declarations plus all 27 | # transitive dependency references. 28 | android.nonTransitiveRClass=true 29 | 30 | # Experimental AGP flags 31 | # Generate compile-time only R class for app modules. 32 | android.enableAppCompileTimeRClass=true 33 | # Keep AAPT2 daemons alive between incremental builds. 34 | android.keepWorkerActionServicesBetweenBuilds=true 35 | # Make R fields non-final to improve build speeds. 36 | # http://tools.android.com/tips/non-constant-fields 37 | android.nonFinalResIds=true 38 | # Enable the newly refactored resource shrinker. 39 | android.experimental.enableNewResourceShrinker=true 40 | # Enable precise shrinking in the new resource shrinker. 41 | android.experimental.enableNewResourceShrinker.preciseShrinking=true 42 | # Generate manifest class as a .class directly rather than a Java source file. 43 | android.generateManifestClass=true 44 | # Generate the text map of source sets and absolute paths to allow 45 | # generating relative paths from absolute paths later in the build. 46 | android.experimental.enableSourceSetPathsMap=true 47 | # Use relative paths for better Gradle caching of library build tasks 48 | android.experimental.cacheCompileLibResources=true 49 | 50 | # Default Android build features 51 | # Disable BuildConfig generation by default 52 | android.defaults.buildfeatures.buildconfig=false 53 | # Disable AIDL stub generation by default 54 | android.defaults.buildfeatures.aidl=false 55 | # Disable RenderScript compilation by default 56 | android.defaults.buildfeatures.renderscript=false 57 | # Disable resource values generation by default in libraries 58 | android.defaults.buildfeatures.resvalues=false 59 | # Disable shader compilation by default 60 | android.defaults.buildfeatures.shaders=false 61 | # Disable Android resource processing by default 62 | android.library.defaults.buildfeatures.androidresources=false 63 | 64 | # Suppress warnings for some features that aren't yet stabilized 65 | android.suppressUnsupportedOptionWarnings=android.keepWorkerActionServicesBetweenBuilds,\ 66 | android.experimental.enableNewResourceShrinker.preciseShrinking,\ 67 | android.enableAppCompileTimeRClass,\ 68 | android.suppressUnsupportedOptionWarnings 69 | 70 | # OSSRH sometimes struggles with slow deployments, so this makes Gradle 71 | # more tolerant to those delays. 72 | systemProp.org.gradle.internal.http.connectionTimeout=500000 73 | systemProp.org.gradle.internal.http.socketTimeout=500000 74 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.2.0-alpha10" 3 | kotlin = "1.8.21" 4 | 5 | [libraries] 6 | androidx-activity-ktx = "androidx.activity:activity-ktx:1.7.1" 7 | androidx-annotation = "androidx.annotation:annotation:1.6.0" 8 | androidx-appcompat = "androidx.appcompat:appcompat:1.6.1" 9 | androidx-biometric = "androidx.biometric:biometric:1.1.0" 10 | androidx-collection = "androidx.collection:collection:1.2.0" 11 | androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" 12 | androidx-coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0" 13 | androidx-core-ktx = "androidx.core:core-ktx:1.10.1" 14 | androidx-datastore-preferences = "androidx.datastore:datastore-preferences:1.0.0" 15 | androidx-fragment-ktx = "androidx.fragment:fragment-ktx:1.5.7" 16 | androidx-lifecycle-runtime-ktx = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" 17 | androidx-preference-ktx = "androidx.preference:preference-ktx:1.2.0" 18 | desugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.3" 19 | google-material = "com.google.android.material:material:1.9.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.7.0" 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 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 5 | networkTimeout=10000 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | google() 7 | mavenCentral() 8 | } 9 | } 10 | 11 | dependencyResolutionManagement { 12 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | } 18 | 19 | rootProject.name = "wireguard-android" 20 | 21 | include(":tunnel") 22 | include(":ui") 23 | -------------------------------------------------------------------------------- /sync-crowdin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | curl -Lo - https://crowdin.com/backend/download/project/wireguard.zip | bsdtar -C ui/src/main/res -x -f - --strip-components 5 wireguard-android 4 | find ui/src/main/res -name strings.xml -exec bash -c '[[ $(xmllint --xpath "count(//resources/*)" {}) -ne 0 ]] || rm -rf "$(dirname {})"' \; 5 | -------------------------------------------------------------------------------- /tunnel/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import org.gradle.api.tasks.testing.logging.TestLogEvent 4 | 5 | val pkg: String = providers.gradleProperty("wireguardPackageName").get() 6 | 7 | plugins { 8 | alias(libs.plugins.android.library) 9 | `maven-publish` 10 | signing 11 | } 12 | 13 | android { 14 | compileSdk = 34 15 | compileOptions { 16 | sourceCompatibility = JavaVersion.VERSION_17 17 | targetCompatibility = JavaVersion.VERSION_17 18 | } 19 | namespace = "${pkg}.tunnel" 20 | defaultConfig { 21 | minSdk = 21 22 | } 23 | externalNativeBuild { 24 | cmake { 25 | path("tools/CMakeLists.txt") 26 | } 27 | } 28 | testOptions.unitTests.all { 29 | it.testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) } 30 | } 31 | buildTypes { 32 | all { 33 | externalNativeBuild { 34 | cmake { 35 | targets("libwg-go.so", "libwg.so", "libwg-quick.so") 36 | arguments("-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}") 37 | } 38 | } 39 | } 40 | release { 41 | externalNativeBuild { 42 | cmake { 43 | arguments("-DANDROID_PACKAGE_NAME=${pkg}") 44 | } 45 | } 46 | } 47 | debug { 48 | externalNativeBuild { 49 | cmake { 50 | arguments("-DANDROID_PACKAGE_NAME=${pkg}.debug") 51 | } 52 | } 53 | } 54 | } 55 | lint { 56 | disable += "LongLogTag" 57 | disable += "NewApi" 58 | } 59 | publishing { 60 | singleVariant("release") { 61 | withJavadocJar() 62 | withSourcesJar() 63 | } 64 | } 65 | } 66 | 67 | dependencies { 68 | implementation(libs.androidx.annotation) 69 | implementation(libs.androidx.collection) 70 | compileOnly(libs.jsr305) 71 | testImplementation(libs.junit) 72 | } 73 | 74 | publishing { 75 | publications { 76 | register("release") { 77 | groupId = pkg 78 | artifactId = "tunnel" 79 | version = providers.gradleProperty("wireguardVersionName").get() 80 | afterEvaluate { 81 | from(components["release"]) 82 | } 83 | pom { 84 | name.set("WireGuard Tunnel Library") 85 | description.set("Embeddable tunnel library for WireGuard for Android") 86 | url.set("https://www.wireguard.com/") 87 | 88 | licenses { 89 | license { 90 | name.set("The Apache Software License, Version 2.0") 91 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 92 | distribution.set("repo") 93 | } 94 | } 95 | scm { 96 | connection.set("scm:git:https://git.zx2c4.com/wireguard-android") 97 | developerConnection.set("scm:git:https://git.zx2c4.com/wireguard-android") 98 | url.set("https://git.zx2c4.com/wireguard-android") 99 | } 100 | developers { 101 | organization { 102 | name.set("WireGuard") 103 | url.set("https://www.wireguard.com/") 104 | } 105 | developer { 106 | name.set("WireGuard") 107 | email.set("team@wireguard.com") 108 | } 109 | } 110 | } 111 | } 112 | } 113 | repositories { 114 | maven { 115 | name = "sonatype" 116 | url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") 117 | credentials { 118 | username = providers.environmentVariable("SONATYPE_USER").orNull 119 | password = providers.environmentVariable("SONATYPE_PASSWORD").orNull 120 | } 121 | } 122 | } 123 | } 124 | 125 | signing { 126 | useGpgCmd() 127 | sign(publishing.publications) 128 | } 129 | -------------------------------------------------------------------------------- /tunnel/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tunnel/src/main/java/com/wireguard/android/backend/Backend.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | * Set the state of a tunnel, updating it's configuration. If the tunnel is already up, config 57 | * may update the running configuration; config may be null when setting the tunnel down. 58 | * 59 | * @param tunnel The tunnel to control the state of. 60 | * @param state The new state for this tunnel. Must be {@code UP}, {@code DOWN}, or 61 | * {@code TOGGLE}. 62 | * @param config The configuration for this tunnel, may be null if state is {@code DOWN}. 63 | * @return The updated state of the tunnel. 64 | * @throws Exception Exception raised while changing state. 65 | */ 66 | Tunnel.State setState(Tunnel tunnel, Tunnel.State state, @Nullable Config config) throws Exception; 67 | } 68 | -------------------------------------------------------------------------------- /tunnel/src/main/java/com/wireguard/android/backend/BackendException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /tunnel/src/main/java/com/wireguard/android/backend/Statistics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/config/Attribute.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/config/BadConfigException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /tunnel/src/main/java/com/wireguard/config/InetAddresses.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /tunnel/src/main/java/com/wireguard/config/InetNetwork.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /tunnel/src/main/java/com/wireguard/config/ParseException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | * An exception thrown when attempting to parse an invalid key (too short, too long, or byte 12 | * data inappropriate for the format). The format being parsed can be accessed with the 13 | * {@link #getFormat} method. 14 | */ 15 | @NonNullForAll 16 | public final class KeyFormatException extends Exception { 17 | private final Key.Format format; 18 | private final Type type; 19 | 20 | KeyFormatException(final Key.Format format, final Type type) { 21 | this.format = format; 22 | this.type = type; 23 | } 24 | 25 | public Key.Format getFormat() { 26 | return format; 27 | } 28 | 29 | public Type getType() { 30 | return type; 31 | } 32 | 33 | public enum Type { 34 | CONTENTS, 35 | LENGTH 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tunnel/src/main/java/com/wireguard/crypto/KeyPair.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/src/main/java/com/wireguard/util/NonNullForAll.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package com.wireguard.util; 7 | 8 | import java.lang.annotation.ElementType; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.RetentionPolicy; 11 | 12 | import javax.annotation.Nonnull; 13 | import javax.annotation.meta.TypeQualifierDefault; 14 | 15 | import androidx.annotation.RestrictTo; 16 | import androidx.annotation.RestrictTo.Scope; 17 | 18 | /** 19 | * This annotation can be applied to a package, class or method to indicate that all 20 | * class fields and method parameters and return values in that element are nonnull 21 | * by default unless overridden. 22 | */ 23 | @RestrictTo(Scope.LIBRARY_GROUP) 24 | @Nonnull 25 | @TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) 26 | @Retention(RetentionPolicy.RUNTIME) 27 | 28 | public @interface NonNullForAll { 29 | } 30 | -------------------------------------------------------------------------------- /tunnel/src/test/java/com/wireguard/config/ConfigTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/src/test/resources/broken.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | PrivateKey = l0lth1s1sd3f1n1t3lybr0k3n= 3 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128 4 | DNS = 192.0.2.0 5 | 6 | [Peer] 7 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg= 8 | AllowedIPs = 0.0.0.0/0,::0/0 9 | Endpoint = 192.0.2.1:51820 10 | -------------------------------------------------------------------------------- /tunnel/src/test/resources/invalid-key.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128 3 | DNS = 192.0.2.0 4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo= 5 | [Peer] 6 | AllowedIPs = 0.0.0.0/0, ::0/0 7 | Endpoint = 192.0.2.1:51820 8 | PersistentKeepalive = 0 9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6Og= 10 | -------------------------------------------------------------------------------- /tunnel/src/test/resources/invalid-number.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128 3 | DNS = 192.0.2.0 4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo= 5 | [Peer] 6 | AllowedIPs = 0.0.0.0/0, ::0/0 7 | Endpoint = 192.0.2.1:51820 8 | PersistentKeepalive = 0L 9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg= 10 | -------------------------------------------------------------------------------- /tunnel/src/test/resources/invalid-value.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128 3 | DNS = 192.0.2.0,invalid_value 4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo= 5 | [Peer] 6 | AllowedIPs = 0.0.0.0/0, ::0/0 7 | Endpoint = 192.0.2.1:51820 8 | PersistentKeepalive = 0 9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg= 10 | -------------------------------------------------------------------------------- /tunnel/src/test/resources/missing-attribute.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128 3 | DNS = 192.0.2.0 4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo= 5 | [Peer] 6 | AllowedIPs = 0.0.0.0/0, ::0/0 7 | Endpoint = 192.0.2.1:51820 8 | PersistentKeepalive = 0 9 | -------------------------------------------------------------------------------- /tunnel/src/test/resources/missing-section.conf: -------------------------------------------------------------------------------- 1 | [Peer] 2 | AllowedIPs = 0.0.0.0/0, ::0/0 3 | Endpoint = 192.0.2.1:51820 4 | PersistentKeepalive = 0 5 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg= 6 | -------------------------------------------------------------------------------- /tunnel/src/test/resources/syntax-error.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128 3 | DNS = 192.0.2.0 4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo= 5 | [Peer] 6 | AllowedIPs = 0.0.0.0/0, ::0/0 7 | Endpoint = 8 | PersistentKeepalive = 0 9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg= 10 | -------------------------------------------------------------------------------- /tunnel/src/test/resources/unknown-attribute.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128 3 | DNS = 192.0.2.0 4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo= 5 | [Peer] 6 | AllowedIPs = 0.0.0.0/0, ::0/0 7 | Endpoint = 192.0.2.1:51820 8 | DontLetTheFeelingFade = 1 9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg= 10 | -------------------------------------------------------------------------------- /tunnel/src/test/resources/unknown-section.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128 3 | DNS = 192.0.2.0 4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo= 5 | [Peers] 6 | AllowedIPs = 0.0.0.0/0, ::0/0 7 | Endpoint = 192.0.2.1:51820 8 | PersistentKeepalive = 0 9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg= 10 | -------------------------------------------------------------------------------- /tunnel/src/test/resources/working.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 192.0.2.2/32,2001:db8:ffff:ffff:ffff:ffff:ffff:ffff/128 3 | DNS = 192.0.2.0 4 | PrivateKey = TFlmmEUC7V7VtiDYLKsbP5rySTKLIZq1yn8lMqK83wo= 5 | [Peer] 6 | AllowedIPs = 0.0.0.0/0, ::0/0 7 | Endpoint = 192.0.2.1:51820 8 | PersistentKeepalive = 0 9 | PublicKey = vBN7qyUTb5lJtWYJ8LhbPio1Z4RcyBPGnqFBGn6O6Qg= 10 | -------------------------------------------------------------------------------- /tunnel/tools/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # 3 | # Copyright © 2017-2023 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/tools/libwg-go/.gitignore: -------------------------------------------------------------------------------- 1 | build/ -------------------------------------------------------------------------------- /tunnel/tools/libwg-go/Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # 3 | # Copyright © 2017-2023 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.20.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 := c1e1161d6d859deb576e6cfabeb40e3d042ceb1c6f444f617c3c9d76269c3565 27 | GO_HASH_darwin-arm64 := 86b0ed0f2b2df50fa8036eea875d1cf2d76cefdacf247c44639a1464b7e36b95 28 | GO_HASH_linux-amd64 := 979694c2c25c735755bf26f4f45e19e64e4811d661dd07b8c010f7a8e18adfca 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 | -------------------------------------------------------------------------------- /tunnel/tools/libwg-go/go.mod: -------------------------------------------------------------------------------- 1 | module golang.zx2c4.com/wireguard/android 2 | 3 | go 1.20 4 | 5 | require ( 6 | golang.org/x/sys v0.6.0 7 | golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 8 | ) 9 | 10 | require ( 11 | golang.org/x/crypto v0.7.0 // indirect 12 | golang.org/x/net v0.8.0 // indirect 13 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /tunnel/tools/libwg-go/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= 2 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 3 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 4 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 5 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 6 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= 8 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= 9 | golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 h1:/J/RVnr7ng4fWPRH3xa4WtBJ1Jp+Auu4YNLmGiPv5QU= 10 | golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA= 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tunnel/tools/ndk-compat/compat.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD 2 | * 3 | * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. 4 | * 5 | */ 6 | 7 | #define FILE_IS_EMPTY 8 | 9 | #if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24 10 | #undef FILE_IS_EMPTY 11 | #include 12 | 13 | char *strchrnul(const char *s, int c) 14 | { 15 | char *x = strchr(s, c); 16 | if (!x) 17 | return (char *)s + strlen(s); 18 | return x; 19 | } 20 | #endif 21 | 22 | #ifdef FILE_IS_EMPTY 23 | #undef FILE_IS_EMPTY 24 | static char ____x __attribute__((unused)); 25 | #endif 26 | -------------------------------------------------------------------------------- /tunnel/tools/ndk-compat/compat.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD 2 | * 3 | * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. 4 | * 5 | */ 6 | 7 | #if defined(__ANDROID_MIN_SDK_VERSION__) && __ANDROID_MIN_SDK_VERSION__ < 24 8 | char *strchrnul(const char *s, int c); 9 | #endif 10 | 11 | -------------------------------------------------------------------------------- /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 = 34 16 | buildFeatures { 17 | buildConfig = true 18 | dataBinding = true 19 | viewBinding = true 20 | } 21 | namespace = pkg 22 | defaultConfig { 23 | applicationId = pkg 24 | minSdk = 21 25 | targetSdk = 34 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.set(JvmTarget.JVM_17) 93 | } 94 | -------------------------------------------------------------------------------- /ui/proguard-android-optimize.txt: -------------------------------------------------------------------------------- 1 | -allowaccessmodification 2 | -dontusemixedcaseclassnames 3 | -dontobfuscate 4 | -verbose 5 | 6 | -keepattributes *Annotation* 7 | 8 | -keepclasseswithmembernames class * { 9 | native ; 10 | } 11 | 12 | -keepclassmembers enum * { 13 | public static **[] values(); 14 | public static ** valueOf(java.lang.String); 15 | } 16 | 17 | -keepclassmembers class * implements android.os.Parcelable { 18 | public static final ** CREATOR; 19 | } 20 | 21 | -keep class androidx.annotation.Keep 22 | 23 | -keep @androidx.annotation.Keep class * {*;} 24 | 25 | -keepclasseswithmembers class * { 26 | @androidx.annotation.Keep ; 27 | } 28 | 29 | -keepclasseswithmembers class * { 30 | @androidx.annotation.Keep ; 31 | } 32 | 33 | -keepclasseswithmembers class * { 34 | @androidx.annotation.Keep (...); 35 | } 36 | -------------------------------------------------------------------------------- /ui/sampledata/interface_names.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "Interface names", 3 | "names": [ 4 | { 5 | "names": [ 6 | { "name": "wg0" }, 7 | { "name": "wg1" }, 8 | { "name": "wg2" }, 9 | { "name": "wg3" }, 10 | { "name": "wg4" }, 11 | { "name": "wg5" }, 12 | { "name": "wg6" }, 13 | { "name": "wg7" }, 14 | { "name": "wg8" }, 15 | { "name": "wg9" }, 16 | { "name": "wg10" }, 17 | { "name": "wg11" } 18 | ], 19 | "checked": [ 20 | { "checked": true }, 21 | { "checked": false }, 22 | { "checked": true }, 23 | { "checked": false }, 24 | { "checked": true }, 25 | { "checked": false }, 26 | { "checked": true }, 27 | { "checked": false }, 28 | { "checked": true }, 29 | { "checked": false }, 30 | { "checked": true } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WireGuard β 4 | 5 | -------------------------------------------------------------------------------- /ui/src/googleplay/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/BootShutdownReceiver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/activity/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/activity/TunnelCreatorActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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.fragment.app.commit 9 | import com.wireguard.android.fragment.TunnelEditorFragment 10 | import com.wireguard.android.model.ObservableTunnel 11 | 12 | /** 13 | * Standalone activity for creating tunnels. 14 | */ 15 | class TunnelCreatorActivity : BaseActivity() { 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | if (supportFragmentManager.findFragmentById(android.R.id.content) == null) { 19 | supportFragmentManager.commit { 20 | add(android.R.id.content, TunnelEditorFragment()) 21 | } 22 | } 23 | } 24 | 25 | override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?, newTunnel: ObservableTunnel?): Boolean { 26 | finish() 27 | return true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/activity/TunnelToggleActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | @RequiresApi(Build.VERSION_CODES.N) 26 | class TunnelToggleActivity : AppCompatActivity() { 27 | private val permissionActivityResultLauncher = 28 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { toggleTunnelWithPermissionsResult() } 29 | 30 | private fun toggleTunnelWithPermissionsResult() { 31 | val tunnel = Application.getTunnelManager().lastUsedTunnel ?: return 32 | lifecycleScope.launch { 33 | try { 34 | tunnel.setStateAsync(Tunnel.State.TOGGLE) 35 | } catch (e: Throwable) { 36 | TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java)) 37 | val error = ErrorMessages[e] 38 | val message = getString(R.string.toggle_error, error) 39 | Log.e(TAG, message, e) 40 | Toast.makeText(this@TunnelToggleActivity, message, Toast.LENGTH_LONG).show() 41 | finishAffinity() 42 | return@launch 43 | } 44 | TileService.requestListeningState(this@TunnelToggleActivity, ComponentName(this@TunnelToggleActivity, QuickTileService::class.java)) 45 | finishAffinity() 46 | } 47 | } 48 | 49 | override fun onCreate(savedInstanceState: Bundle?) { 50 | super.onCreate(savedInstanceState) 51 | lifecycleScope.launch { 52 | if (Application.getBackend() is GoBackend) { 53 | val intent = GoBackend.VpnService.prepare(this@TunnelToggleActivity) 54 | if (intent != null) { 55 | permissionActivityResultLauncher.launch(intent) 56 | return@launch 57 | } 58 | } 59 | toggleTunnelWithPermissionsResult() 60 | } 61 | } 62 | 63 | companion object { 64 | private const val TAG = "WireGuard/TunnelToggleActivity" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/configStore/ConfigStore.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/java/com/wireguard/android/configStore/FileConfigStore.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/databinding/Keyed.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package com.wireguard.android.databinding 6 | 7 | /** 8 | * Interface for objects that have a identifying key of the given type. 9 | */ 10 | interface Keyed { 11 | val key: K 12 | } 13 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/databinding/ObservableKeyedArrayList.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package com.wireguard.android.databinding 6 | 7 | import androidx.databinding.ObservableArrayList 8 | 9 | /** 10 | * ArrayList that allows looking up elements by some key property. As the key property must always 11 | * be retrievable, this list cannot hold `null` elements. Because this class places no 12 | * restrictions on the order or duplication of keys, lookup by key, as well as all list modification 13 | * operations, require O(n) time. 14 | */ 15 | open class ObservableKeyedArrayList> : ObservableArrayList() { 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/java/com/wireguard/android/databinding/ObservableKeyedRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/java/com/wireguard/android/databinding/ObservableSortedKeyedArrayList.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/fragment/AddTunnelsSheet.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/fragment/ConfigNamingDialogFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/java/com/wireguard/android/model/ApplicationData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved. 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | package com.wireguard.android.model 6 | 7 | import android.graphics.drawable.Drawable 8 | import androidx.databinding.BaseObservable 9 | import androidx.databinding.Bindable 10 | import com.wireguard.android.BR 11 | import com.wireguard.android.databinding.Keyed 12 | 13 | class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isSelected: Boolean) : BaseObservable(), Keyed { 14 | override val key = name 15 | 16 | @get:Bindable 17 | var isSelected = isSelected 18 | set(value) { 19 | field = value 20 | notifyPropertyChanged(BR.selected) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/model/TunnelComparator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/java/com/wireguard/android/preference/DonatePreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | 19 | class DonatePreference(context: Context, attrs: AttributeSet?) : Preference(context, attrs) { 20 | override fun getSummary() = context.getString(R.string.donate_summary) 21 | 22 | override fun getTitle() = context.getString(R.string.donate_title) 23 | 24 | override fun onClick() { 25 | /* Google Play Store forbids links to our donation page. */ 26 | if (Updater.installerIsGooglePlay(context)) { 27 | MaterialAlertDialogBuilder(context) 28 | .setTitle(R.string.donate_title) 29 | .setMessage(R.string.donate_google_play_disappointment) 30 | .show() 31 | return 32 | } 33 | 34 | val intent = Intent(Intent.ACTION_VIEW) 35 | intent.data = Uri.parse("https://www.wireguard.com/donations/") 36 | try { 37 | context.startActivity(intent) 38 | } catch (e: Throwable) { 39 | Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_SHORT).show() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/preference/KernelModuleEnablerPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/java/com/wireguard/android/preference/QuickTilePreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/preference/ToolsInstallerPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/preference/VersionPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/java/com/wireguard/android/util/AdminKnobs.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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.RestrictionsManager 9 | import androidx.core.content.getSystemService 10 | import com.wireguard.android.Application 11 | 12 | object AdminKnobs { 13 | private val restrictions: RestrictionsManager? = Application.get().getSystemService() 14 | val disableConfigExport: Boolean 15 | get() = restrictions?.applicationRestrictions?.getBoolean("disable_config_export", false) 16 | ?: false 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/util/ClipboardUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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.view.View 10 | import android.widget.TextView 11 | import androidx.core.content.getSystemService 12 | import com.google.android.material.snackbar.Snackbar 13 | import com.google.android.material.textfield.TextInputEditText 14 | import com.wireguard.android.R 15 | 16 | /** 17 | * Standalone utilities for interacting with the system clipboard. 18 | */ 19 | object ClipboardUtils { 20 | @JvmStatic 21 | fun copyTextView(view: View) { 22 | val data = when (view) { 23 | is TextInputEditText -> Pair(view.editableText, view.hint) 24 | is TextView -> Pair(view.text, view.contentDescription) 25 | else -> return 26 | } 27 | if (data.first == null || data.first.isEmpty()) { 28 | return 29 | } 30 | val service = view.context.getSystemService() ?: return 31 | service.setPrimaryClip(ClipData.newPlainText(data.second, data.first)) 32 | Snackbar.make(view, view.context.getString(R.string.copied_to_clipboard, data.second), Snackbar.LENGTH_LONG).show() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/util/Extensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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.util.TypedValue 10 | import androidx.annotation.AttrRes 11 | import androidx.lifecycle.lifecycleScope 12 | import androidx.preference.Preference 13 | import com.wireguard.android.Application 14 | import com.wireguard.android.activity.SettingsActivity 15 | import kotlinx.coroutines.CoroutineScope 16 | 17 | fun Context.resolveAttribute(@AttrRes attrRes: Int): Int { 18 | val typedValue = TypedValue() 19 | theme.resolveAttribute(attrRes, typedValue, true) 20 | return typedValue.data 21 | } 22 | 23 | val Any.applicationScope: CoroutineScope 24 | get() = Application.getCoroutineScope() 25 | 26 | val Preference.activity: SettingsActivity 27 | get() = context as? SettingsActivity 28 | ?: throw IllegalStateException("Failed to resolve SettingsActivity") 29 | 30 | val Preference.lifecycleScope: CoroutineScope 31 | get() = activity.lifecycleScope 32 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/util/QrCodeFromFileScanner.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | 33 | private fun scanBitmapForResult(source: Bitmap): Result { 34 | val width = source.width 35 | val height = source.height 36 | val pixels = IntArray(width * height) 37 | source.getPixels(pixels, 0, width, 0, 0, width, height) 38 | 39 | val bBitmap = BinaryBitmap(HybridBinarizer(RGBLuminanceSource(width, height, pixels))) 40 | return reader.decode(bBitmap, mapOf(DecodeHintType.TRY_HARDER to true)) 41 | } 42 | 43 | private fun downscaleBitmap(source: Bitmap, scaledSize: Int): Bitmap { 44 | 45 | val originalWidth = source.width 46 | val originalHeight = source.height 47 | 48 | var newWidth = -1 49 | var newHeight = -1 50 | val multFactor: Float 51 | 52 | when { 53 | originalHeight > originalWidth -> { 54 | newHeight = scaledSize 55 | multFactor = originalWidth.toFloat() / originalHeight.toFloat() 56 | newWidth = (newHeight * multFactor).toInt() 57 | } 58 | 59 | originalWidth > originalHeight -> { 60 | newWidth = scaledSize 61 | multFactor = originalHeight.toFloat() / originalWidth.toFloat() 62 | newHeight = (newWidth * multFactor).toInt() 63 | } 64 | 65 | originalHeight == originalWidth -> { 66 | newHeight = scaledSize 67 | newWidth = scaledSize 68 | } 69 | } 70 | return Bitmap.createScaledBitmap(source, newWidth, newHeight, false) 71 | } 72 | 73 | private fun doScan(data: Uri): Result { 74 | Log.d(TAG, "Starting to scan an image: $data") 75 | contentResolver.openInputStream(data).use { inputStream -> 76 | val originalBitmap = BitmapFactory.decodeStream(inputStream) 77 | ?: throw IllegalArgumentException("Can't decode stream to Bitmap") 78 | 79 | return try { 80 | scanBitmapForResult(originalBitmap).also { 81 | Log.d(TAG, "Found result in original image") 82 | } 83 | } catch (e: Exception) { 84 | Log.e(TAG, "Original image scan finished with error: $e, will try downscaled image") 85 | val scaleBitmap = downscaleBitmap(originalBitmap, 500) 86 | scanBitmapForResult(originalBitmap).also { scaleBitmap.recycle() } 87 | } finally { 88 | originalBitmap.recycle() 89 | } 90 | } 91 | 92 | } 93 | 94 | /** 95 | * Attempts to parse incoming data 96 | * @return result of the decoding operation 97 | * @throws NotFoundException when parser didn't find QR code in the image 98 | */ 99 | suspend fun scan(data: Uri) = withContext(Dispatchers.Default) { doScan(data) } 100 | 101 | companion object { 102 | private const val TAG = "QrCodeFromFileScanner" 103 | 104 | /** 105 | * Given a reference to a file, check if this file could be parsed by this class 106 | * @return true if the file can be parsed, false if not 107 | */ 108 | fun validContentType(contentResolver: ContentResolver, data: Uri): Boolean { 109 | return contentResolver.getType(data)?.startsWith("image/") == true 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/util/QuantityFormatter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 35 | return Application.get().applicationContext.getString(R.string.latest_handshake_ago, span.seconds.toString()) 36 | 37 | if (span <= 0L) 38 | return RelativeDateTimeFormatter.getInstance().format(RelativeDateTimeFormatter.Direction.PLAIN, RelativeDateTimeFormatter.AbsoluteUnit.NOW) 39 | val measureFormat = MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE) 40 | val parts = ArrayList(4) 41 | if (span >= 24 * 60 * 60L) { 42 | val v = span / (24 * 60 * 60L) 43 | parts.add(measureFormat.format(Measure(v, MeasureUnit.DAY))) 44 | span -= v * (24 * 60 * 60L) 45 | } 46 | if (span >= 60 * 60L) { 47 | val v = span / (60 * 60L) 48 | parts.add(measureFormat.format(Measure(v, MeasureUnit.HOUR))) 49 | span -= v * (60 * 60L) 50 | } 51 | if (span >= 60L) { 52 | val v = span / 60L 53 | parts.add(measureFormat.format(Measure(v, MeasureUnit.MINUTE))) 54 | span -= v * 60L 55 | } 56 | if (span > 0L) 57 | parts.add(measureFormat.format(Measure(span, MeasureUnit.SECOND))) 58 | 59 | val joined = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) 60 | parts.joinToString() 61 | else 62 | ListFormatter.getInstance(Locale.getDefault(), ListFormatter.Type.UNITS, ListFormatter.Width.SHORT).format(parts) 63 | 64 | return Application.get().applicationContext.getString(R.string.latest_handshake_ago, joined) 65 | } 66 | } -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/widget/KeyInputFilter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/widget/MultiselectableRelativeLayout.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | -------------------------------------------------------------------------------- /ui/src/main/java/com/wireguard/android/widget/NameInputFilter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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/ToggleSwitch.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2013 The Android Open Source Project 3 | * Copyright © 2017-2023 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/java/com/wireguard/android/widget/TvCardView.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2023 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 | } -------------------------------------------------------------------------------- /ui/src/main/res/anim/scale_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | -------------------------------------------------------------------------------- /ui/src/main/res/anim/scale_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | -------------------------------------------------------------------------------- /ui/src/main/res/color/tv_list_item_tint.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_action_add_white.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_action_delete.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_action_edit.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_action_generate.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_action_open.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_action_save.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_action_scan_qr_code.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_action_select_all.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_action_share_white.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 13 | 14 | 17 | 20 | 21 | 22 | 25 | 26 | 29 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/ic_tile.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /ui/src/main/res/drawable/list_item_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ui/src/main/res/layout-sw600dp/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 25 | 26 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/add_tunnels_bottom_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 31 | 32 | 55 | 56 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/app_list_dialog_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 19 | 20 | 21 | 25 | 26 | 31 | 32 | 36 | 37 | 41 | 42 | 43 | 47 | 48 | 56 | 57 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/app_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 20 | 21 | 22 | 31 | 32 | 39 | 40 | 52 | 53 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/config_naming_dialog_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 19 | 20 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/log_viewer_activity.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 18 | 19 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/log_viewer_entry.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 22 | 23 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/tunnel_list_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 17 | 18 | 21 | 22 | 23 | 29 | 30 | 44 | 45 | 52 | 53 | 61 | 62 | 70 | 71 | 72 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/tunnel_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 19 | 20 | 23 | 24 | 27 | 28 | 29 | 39 | 40 | 51 | 52 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/tv_file_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | 27 | 28 | 31 | 32 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /ui/src/main/res/layout/tv_tunnel_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 21 | 22 | 25 | 26 | 29 | 30 | 31 | 43 | 44 | 47 | 48 | 57 | 58 | 67 | 68 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /ui/src/main/res/menu/config_editor.xml: -------------------------------------------------------------------------------- 1 | 2 |

4 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/menu/log_viewer.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/main/res/menu/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /ui/src/main/res/menu/tunnel_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/menu/tunnel_list_action_mode.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-xhdpi/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xhdpi/banner.png -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heiher/wireguard-android/e21fb5b7ed6dffd2dc2a5e9e3340ecf274cf92eb/ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ui/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en-US 2 | -------------------------------------------------------------------------------- /ui/src/main/res/values-night/bools.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | false 5 | 6 | -------------------------------------------------------------------------------- /ui/src/main/res/values-night/logviewer_colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #aaaaaa 3 | #ff0000 4 | #00ff00 5 | #ffff00 6 | 7 | -------------------------------------------------------------------------------- /ui/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31 | 32 | -------------------------------------------------------------------------------- /ui/src/main/res/values-v23/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /ui/src/main/res/values-v27/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ui/src/main/res/values/bools.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 6 | -------------------------------------------------------------------------------- /ui/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #1a73e8 3 | #005BC0 4 | #FFFFFF 5 | #D8E2FF 6 | #001A41 7 | #565E71 8 | #FFFFFF 9 | #DBE2F9 10 | #131B2C 11 | #715574 12 | #FFFFFF 13 | #FBD7FC 14 | #29132D 15 | #BA1A1A 16 | #FFDAD6 17 | #FFFFFF 18 | #410002 19 | #FEFBFF 20 | #1B1B1F 21 | #FEFBFF 22 | #1B1B1F 23 | #E1E2EC 24 | #44474F 25 | #74777F 26 | #F2F0F4 27 | #303033 28 | #ADC7FF 29 | #000000 30 | #005BC0 31 | #C4C6D0 32 | #000000 33 | #ADC7FF 34 | #002E68 35 | #004493 36 | #D8E2FF 37 | #BFC6DC 38 | #283041 39 | #3F4759 40 | #DBE2F9 41 | #DEBCDF 42 | #402843 43 | #583E5B 44 | #FBD7FC 45 | #FFB4AB 46 | #93000A 47 | #690005 48 | #FFDAD6 49 | #1B1B1F 50 | #E3E2E6 51 | #1B1B1F 52 | #E3E2E6 53 | #44474F 54 | #C4C6D0 55 | #8E9099 56 | #1B1B1F 57 | #E3E2E6 58 | #005BC0 59 | #000000 60 | #ADC7FF 61 | #44474F 62 | #000000 63 | 64 | -------------------------------------------------------------------------------- /ui/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 56dp 5 | 8dp 6 | 8dp 7 | 16dp 8 | 16dp 9 | 10 | -------------------------------------------------------------------------------- /ui/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #871719 4 | -------------------------------------------------------------------------------- /ui/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui/src/main/res/values/logviewer_colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #444444 3 | #aa0000 4 | #00aa00 5 | #aaaa00 6 | 7 | -------------------------------------------------------------------------------- /ui/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 14 | 15 | 16 | 22 | 23 | 27 | 28 | 40 | 41 | 45 | 46 | -------------------------------------------------------------------------------- /ui/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31 | 32 | -------------------------------------------------------------------------------- /ui/src/main/res/xml/app_restrictions.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /ui/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 14 | 15 | 16 | 21 | 28 | 35 | 36 | 37 | 44 | 45 | 46 | --------------------------------------------------------------------------------