Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for automatically disconnecting from a tunnel when connected to a specified Wi-Fi network. #58

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jsr305 = "com.google.code.findbugs:jsr305:3.0.2"
junit = "junit:junit:4.13.2"
kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0"
zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0"
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version = "2.8.1" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Expand Down
10 changes: 10 additions & 0 deletions tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,14 @@ public static State of(final boolean running) {
return running ? UP : DOWN;
}
}

/**
* Enum class to represent the reason for why a tunnel's state has changed
*/
enum StateChangeReason {
USER_INTERACTION,
EXTERNAL_INTENT,
AUTO_START,
WIFI_WORKER;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ public enum Reason {
MISSING_SECTION,
SYNTAX_ERROR,
UNKNOWN_ATTRIBUTE,
UNKNOWN_SECTION
UNKNOWN_SECTION,
INVALID_ADDITIONAL_CONFIG
}

public enum Section {
Expand Down
62 changes: 60 additions & 2 deletions tunnel/src/main/java/com/wireguard/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@ public final class Config {
private final Interface interfaze;
private final List<Peer> peers;

private final boolean enableAutoDisconnect;
private final String autoDisconnectNetworks;

private Config(final Builder builder) {
interfaze = Objects.requireNonNull(builder.interfaze, "An [Interface] section is required");
// Defensively copy to ensure immutability even if the Builder is reused.
peers = Collections.unmodifiableList(new ArrayList<>(builder.peers));
enableAutoDisconnect = builder.enableAutoDisconnect;
autoDisconnectNetworks = builder.autoDisconnectNetworks;
}

/**
Expand Down Expand Up @@ -71,8 +76,25 @@ public static Config parse(final BufferedReader reader)
@Nullable String line;
while ((line = reader.readLine()) != null) {
final int commentIndex = line.indexOf('#');
if (commentIndex != -1)
if (commentIndex != -1) {
final String commentData = line.substring(commentIndex);
if(commentData.startsWith("#ADD;")){
final String[] tokens = commentData.split(";", 3);
if(tokens.length != 3){
throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL, Reason.INVALID_ADDITIONAL_CONFIG, line);
}
final String tokenName = tokens[1];
final String tokenValue = tokens[2];
switch (tokenName) {
case "wifi_auto_disconnect" ->
builder.setAutoDisconnect("1".equalsIgnoreCase(tokenValue));
case "wifi_auto_disconnect_networks" ->
builder.setAutoDisconnectNetworks(tokenValue);
}
continue;
}
line = line.substring(0, commentIndex);
}
line = line.trim();
if (line.isEmpty())
continue;
Expand Down Expand Up @@ -120,6 +142,27 @@ public boolean equals(final Object obj) {
return interfaze.equals(other.interfaze) && peers.equals(other.peers);
}


/**
* Returns whether or not the connection should be disconnected, once the user connects to a
* specified network.
*
* @return whether or not the connection should be disconnected, once the user connects to a
* specified network
*/
public boolean isAutoDisconnectEnabled() {
return enableAutoDisconnect;
}

/**
* Returns the list of Wi-Fi networks that should cause this connection to disconnect.
*
* @return the list of Wi-Fi networks that should cause this connection to disconnect
*/
public String getAutoDisconnectNetworks(){
return autoDisconnectNetworks;
}

/**
* Returns the interface section of the configuration.
*
Expand All @@ -140,7 +183,7 @@ public List<Peer> getPeers() {

@Override
public int hashCode() {
return 31 * interfaze.hashCode() + peers.hashCode();
return 33 * (enableAutoDisconnect ? 1 : 0) + 32 * autoDisconnectNetworks.hashCode() + 31 * interfaze.hashCode() + peers.hashCode();
}

/**
Expand All @@ -165,6 +208,8 @@ public String toWgQuickString() {
sb.append("[Interface]\n").append(interfaze.toWgQuickString());
for (final Peer peer : peers)
sb.append("\n[Peer]\n").append(peer.toWgQuickString());
sb.append("\n#ADD;wifi_auto_disconnect;").append(enableAutoDisconnect ? '1' : '0');
sb.append("\n#ADD;wifi_auto_disconnect_networks;").append(autoDisconnectNetworks);
return sb.toString();
}

Expand All @@ -189,11 +234,24 @@ public static final class Builder {
// No default; must be provided before building.
@Nullable private Interface interfaze;

private boolean enableAutoDisconnect = false;
private String autoDisconnectNetworks = "";

public Builder addPeer(final Peer peer) {
peers.add(peer);
return this;
}

public Builder setAutoDisconnect(final boolean enable){
enableAutoDisconnect = enable;
return this;
}

public Builder setAutoDisconnectNetworks(final String networks){
autoDisconnectNetworks = networks;
return this;
}

public Builder addPeers(final Collection<Peer> peers) {
this.peers.addAll(peers);
return this;
Expand Down
1 change: 1 addition & 0 deletions ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ dependencies {
implementation(libs.google.material)
implementation(libs.zxing.android.embedded)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.work.runtime.ktx)
coreLibraryDesugaring(libs.desugarJdkLibs)
}

Expand Down
5 changes: 5 additions & 0 deletions ui/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
*/
package com.wireguard.android.fragment

import android.Manifest
import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
Expand All @@ -18,6 +23,9 @@ import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.os.BundleCompat
import androidx.core.view.MenuProvider
import androidx.lifecycle.Lifecycle
Expand All @@ -34,6 +42,9 @@ import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.viewmodel.ConfigProxy
import com.wireguard.config.Config
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

/**
* Fragment for editing a WireGuard configuration.
Expand Down Expand Up @@ -112,10 +123,29 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
selectedTunnel = tunnel
}

private lateinit var requestPermissionLauncher: ActivityResultLauncher<Array<String>>
private var requestPermissionCallback: ((permissions: Map<String, Boolean>) -> Unit)? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
requestPermissionCallback?.invoke(it);
}
}

private suspend fun suspendRequestPermissions(permissions: Array<String>): Map<String, Boolean> {
return suspendCoroutine { cont ->
requestPermissionCallback = {
cont.resume(it)
requestPermissionCallback = null
}
requestPermissionLauncher?.launch(permissions)
}
}

override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
if (menuItem.itemId == R.id.menu_action_save) {
binding ?: return false
val newConfig = try {
var newConfig = try {
binding!!.config!!.resolve()
} catch (e: Throwable) {
val error = ErrorMessages[e]
Expand All @@ -127,6 +157,73 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider {
}
val activity = requireActivity()
activity.lifecycleScope.launch {
if (newConfig.isAutoDisconnectEnabled) run {
val disableFeature = {
// The user has changed their mind - disable the feature, but leave the network list in place
newConfig = Config.Builder()
.setInterface(newConfig.`interface`)
.addPeers(newConfig.peers)
.setAutoDisconnect(false)
.setAutoDisconnectNetworks(newConfig.autoDisconnectNetworks)
.build()
}

val requiredPermissions = arrayListOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
requiredPermissions.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
}
val allRequiredPermissionsGranted =
requiredPermissions.all { ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED }
if (!allRequiredPermissionsGranted) {
val shouldAsk = suspendCancellableCoroutine { cont ->
AlertDialog.Builder(activity)
.setMessage(R.string.wifi_location_needed_message)
.setTitle(R.string.wifi_location_needed_title)
.create()
.run {
val listener = DialogInterface.OnClickListener { _, button ->
when (button) {
AlertDialog.BUTTON_POSITIVE -> cont.resume(true)
else -> cont.resume(false)
}
}
cont.invokeOnCancellation { this.dismiss() }
setButton(AlertDialog.BUTTON_POSITIVE, context.getText(R.string.wifi_location_ok), listener)
setButton(AlertDialog.BUTTON_NEGATIVE, context.getText(R.string.wifi_location_cancel), listener)
show()
}
}
if (!shouldAsk) {
disableFeature()
return@run
}

// Ask for the permissions
val basePermissionsGranted = suspendRequestPermissions(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
).values.all { it }
if (!basePermissionsGranted) {
disableFeature()
return@run
}

// If we're not on android Q, we're done - all required permissions granted
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return@run
}

// If we're on android Q or more, the background location needs its separate permission - it can't be
// requested together with the 2 base location ones.
val backgroundPermissionGranted = suspendRequestPermissions(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).values.all { it }
if (!backgroundPermissionGranted) {
disableFeature()
}
}
}

when {
tunnel == null -> {
Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class ObservableTunnel internal constructor(

suspend fun setStateAsync(state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) {
if (state != [email protected])
manager.setTunnelState(this@ObservableTunnel, state)
manager.setTunnelState(this@ObservableTunnel, state, Tunnel.StateChangeReason.USER_INTERACTION)
else
[email protected]
}
Expand Down
Loading