Skip to content

Commit

Permalink
feat: add tunnel details screen and handshake monitoring
Browse files Browse the repository at this point in the history
Adds details screen which display details of tunnel configuration as well as last handshake and rx/tx of peer.

Adds last handshake monitoring with statuses and thresholds.

Adds handshake/connection notifications based on last successful handshake.

Adds status LED next to tunnel on main screen.

Fixes bug where first click on QR code could result in nothing happening if QR code module is being downloaded. Now shows message to user.

Fixes bug where changes made after editing tunnel were not propagated to settings if that tunnel was configured as the default tunnel.

Fixes bug causing crash if wrong config file selected

Update README

Closes #7, Closes #6
  • Loading branch information
zaneschepke committed Jul 18, 2023
1 parent 0c45558 commit 68b0902
Show file tree
Hide file tree
Showing 28 changed files with 612 additions and 107 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This is an alternative Android Application for [WireGuard](https://www.wireguard
## Screenshots

<p float="center">
<img label="Main" style="padding-right:25px" src="./asset/main_screen.png" width="200" />
<img label="Main" style="padding-right:25px" src="asset/main_screen.png" width="200" />
<img label="Config" style="padding-left:25px" src="./asset/config_screen.png" width="200" />
<img label="Settings" style="padding-left:25px" src="./asset/settings_screen.png" width="200" />
<img label="Support" style="padding-left:25px" src="./asset/support_screen.png" width="200" />
Expand Down
9 changes: 5 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ android {
compileSdk = 33

val versionMajor = 2
val versionMinor = 0
val versionPatch = 3
val versionMinor = 1
val versionPatch = 2
val versionBuild = 0

defaultConfig {
Expand Down Expand Up @@ -54,7 +54,7 @@ android {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.7"
kotlinCompilerExtensionVersion = "1.4.8"
}
packaging {
resources {
Expand Down Expand Up @@ -83,7 +83,7 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-test-manifest")

//wireguard tunnel
implementation("com.wireguard.android:tunnel:1.0.20230405")
implementation("com.wireguard.android:tunnel:1.0.20230427")

//logging
implementation("com.jakewharton.timber:timber:5.0.1")
Expand Down Expand Up @@ -127,6 +127,7 @@ dependencies {


}

kapt {
correctErrorTypes = true
}
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
Expand Down Expand Up @@ -59,12 +60,13 @@
android:stopWithTask="false"
android:exported="false">
</service>
<receiver android:enabled="true" android:name=".BootReceiver"
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.zaneschepke.wireguardautotunnel
package com.zaneschepke.wireguardautotunnel.receiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
Expand All @@ -11,7 +12,6 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
Expand All @@ -23,21 +23,19 @@ class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : Repository<Settings>

@OptIn(DelicateCoroutinesApi::class)
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
CoroutineScope(SupervisorJob()).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings[0]
val setting = settings.first()
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
val defaultTunnel = TunnelConfig.from(setting.defaultTunnel!!)
ServiceTracker.actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java,
mapOf(context.resources.getString(R.string.tunnel_extras_key) to
defaultTunnel.toString())
setting.defaultTunnel!!)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.zaneschepke.wireguardautotunnel.receiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.repository.Repository
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceTracker
import com.zaneschepke.wireguardautotunnel.service.foreground.WireGuardTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {

@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onReceive(context: Context, intent: Intent?) {
CoroutineScope(SupervisorJob()).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.defaultTunnel != null) {
ServiceTracker.actionOnService(
Action.STOP, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
delay(1000)
ServiceTracker.actionOnService(
Action.START, context,
WireGuardTunnelService::class.java,
mapOf(
context.resources.getString(R.string.tunnel_extras_key) to
setting.defaultTunnel!!
)
)
}
}
} finally {
cancel()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class QRScanner @Inject constructor(private val gmsBarcodeScanner: GmsBarcodeSca
gmsBarcodeScanner.startScan().addOnSuccessListener {
trySend(it.rawValue)
}.addOnFailureListener {
trySend(it.message)
Timber.e(it.message)
}
awaitClose {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -123,7 +122,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
}
}

@OptIn(DelicateCoroutinesApi::class)
private fun startWatcherJob() {
watcherJob = CoroutineScope(SupervisorJob()).launch {
val settings = settingsRepo.getAll();
Expand Down Expand Up @@ -151,13 +149,17 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
is NetworkStatus.CapabilitiesChanged -> {
isMobileDataConnected = true
Timber.d("Mobile data capabilities changed")
if(!isWifiConnected && setting.isTunnelOnMobileDataEnabled
&& vpnService.getState() == Tunnel.State.DOWN)
startVPN()
if(!disconnecting && !connecting) {
if(!isWifiConnected && setting.isTunnelOnMobileDataEnabled
&& vpnService.getState() == Tunnel.State.DOWN)
startVPN()
}
}
is NetworkStatus.Unavailable -> {
isMobileDataConnected = false
if(!isWifiConnected && vpnService.getState() == Tunnel.State.UP) stopVPN()
if(!disconnecting && !connecting) {
if(!isWifiConnected && vpnService.getState() == Tunnel.State.UP) stopVPN()
}
Timber.d("Lost mobile data connection")
}
}
Expand All @@ -178,7 +180,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
Timber.d("Not connect and not disconnecting")
val ssid = wifiService.getNetworkName(it.networkCapabilities);
Timber.d("SSID: $ssid")
if ((setting.trustedNetworkSSIDs?.contains(ssid) == false) && vpnService.getState() == Tunnel.State.DOWN) {
if (!setting.trustedNetworkSSIDs.contains(ssid) && vpnService.getState() == Tunnel.State.DOWN) {
Timber.d("Starting VPN Tunnel for untrusted network: $ssid")
startVPN()
} else if (!disconnecting && vpnService.getState() == Tunnel.State.UP && setting.trustedNetworkSSIDs.contains(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.zaneschepke.wireguardautotunnel.service.foreground

import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -28,6 +31,8 @@ class WireGuardTunnelService : ForegroundService() {

private lateinit var job : Job

private var tunnelName : String = ""

override fun startService(extras : Bundle?) {
super.startService(extras)
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
Expand All @@ -36,10 +41,8 @@ class WireGuardTunnelService : ForegroundService() {
if(tunnelConfigString != null) {
try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
val state = vpnService.startTunnel(tunnelConfig)
if (state == Tunnel.State.UP) {
launchVpnConnectedNotification(tunnelConfig.name)
}
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
} catch (e : Exception) {
Timber.e("Problem starting tunnel: ${e.message}")
stopService(extras)
Expand All @@ -48,6 +51,34 @@ class WireGuardTunnelService : ForegroundService() {
Timber.e("Tunnel config null")
}
}
CoroutineScope(job).launch {
var didShowConnected = false
var didShowFailedHandshakeNotification = false
vpnService.handshakeStatus.collect {
when(it) {
HandshakeStatus.NOT_STARTED -> {
}
HandshakeStatus.NEVER_CONNECTED -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
didShowFailedHandshakeNotification = true
}
}
HandshakeStatus.HEALTHY -> {
if(!didShowConnected) {
launchVpnConnectedNotification()
didShowConnected = true
}
}
HandshakeStatus.UNHEALTHY -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
didShowFailedHandshakeNotification = true
}
}
}
}
}
}

override fun stopService(extras : Bundle?) {
Expand All @@ -59,7 +90,7 @@ class WireGuardTunnelService : ForegroundService() {
stopSelf()
}

private fun launchVpnConnectedNotification(tunnelName : String) {
private fun launchVpnConnectedNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
Expand All @@ -70,6 +101,22 @@ class WireGuardTunnelService : ForegroundService() {
)
super.startForeground(foregroundId, notification)
}

private fun launchVpnConnectionFailedNotification(message : String) {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
action = PendingIntent.getBroadcast(this,0,Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed),
onGoing = false,
showTimestamp = true,
description = message
)
super.startForeground(foregroundId, notification)
}


private fun cancelJob() {
if(this::job.isInitialized) {
job.cancel()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.service.network

import android.app.Service
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
Expand All @@ -10,12 +9,10 @@ import android.net.wifi.SupplicantState
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import javax.inject.Inject


abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package com.zaneschepke.wireguardautotunnel.service.notification

import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent

interface NotificationService {
fun createNotification(
channelId: String,
channelName: String,
title: String = "",
action: PendingIntent? = null,
actionText: String? = null,
description: String,
showTimestamp : Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
Expand Down
Loading

0 comments on commit 68b0902

Please sign in to comment.