Skip to content
This repository has been archived by the owner on Jul 11, 2024. It is now read-only.

Commit

Permalink
fix: vpn bugs
Browse files Browse the repository at this point in the history
Fixes a bug where file descriptor was not being closed properly causing a crash on reconnection.

Fixes a bug where the VPN would shutdown if ipv6 or ipv4 traffic is not being routed correctly due to a misconfigured gateway.

Fixes a bug where vpn state could become out of sync with the lib.

Fixes a bug where user could change entry/exit points while vpn was not disconnected.

Fixes a bug where users could navigate back to the login screen even after already being logged in.

Refactor of nymvpn android lib to improve usability and state synchronization.
  • Loading branch information
zaneschepke committed Apr 15, 2024
1 parent d651616 commit 224a505
Show file tree
Hide file tree
Showing 24 changed files with 327 additions and 232 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ android {
Constants.SENTRY_DSN,
"\"${(System.getenv(Constants.SENTRY_DSN) ?: getLocalProperty("sentry.dsn")) ?: ""}\"",
)
buildConfigField("Boolean", "IS_SANDBOX", "false")
buildConfigField("Boolean", Constants.OPT_IN_REPORTING, "false")
proguardFile("fdroid-rules.pro")
}
Expand Down Expand Up @@ -131,6 +132,7 @@ android {
proguardFile("proguard-rules.pro")
}
create(Constants.SANDBOX) {
buildConfigField("Boolean", "IS_SANDBOX", "true")
dimension = Constants.TYPE
}
}
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/net/nymtech/nymvpn/NymVpn.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import net.nymtech.nymvpn.service.tile.VpnQuickTile
import net.nymtech.nymvpn.util.log.DebugTree
import net.nymtech.nymvpn.util.log.ReleaseTree
import net.nymtech.nymvpn.util.navigationBarHeight
import net.nymtech.vpn.model.Environment
import timber.log.Timber

@HiltAndroidApp
Expand All @@ -31,6 +32,8 @@ class NymVpn : Application() {
lateinit var instance: NymVpn
private set

val environment = if (BuildConfig.IS_SANDBOX) Environment.SANDBOX else Environment.MAINNET

private const val BASELINE_HEIGHT = 2201
private const val BASELINE_WIDTH = 1080
private const val BASELINE_DENSITY = 2.625
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/net/nymtech/nymvpn/module/ServiceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import net.nymtech.nymvpn.NymVpn
import net.nymtech.nymvpn.service.gateway.GatewayApiService
import net.nymtech.nymvpn.util.Constants
import net.nymtech.vpn.NymVpnClient
import net.nymtech.vpn.VpnClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
Expand Down Expand Up @@ -37,4 +40,10 @@ class ServiceModule {
fun provideGatewayService(retrofit: Retrofit): GatewayApiService {
return retrofit.create(GatewayApiService::class.java)
}

@Singleton
@Provides
fun provideVpnClient(): VpnClient {
return NymVpnClient.init(environment = NymVpn.environment)
}
}
19 changes: 19 additions & 0 deletions app/src/main/java/net/nymtech/nymvpn/module/ViewModelModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package net.nymtech.nymvpn.module

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
import net.nymtech.vpn.NymApi
import net.nymtech.nymvpn.NymVpn

@Module
@InstallIn(ViewModelComponent::class)
internal object ViewModelModule {
@Provides
@ViewModelScoped
fun provideNymApi(): NymApi {
return NymApi(NymVpn.environment)
}
}
12 changes: 9 additions & 3 deletions app/src/main/java/net/nymtech/nymvpn/receiver/BootReceiver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import net.nymtech.nymvpn.NymVpn
import net.nymtech.nymvpn.data.GatewayRepository
import net.nymtech.nymvpn.data.SettingsRepository
import net.nymtech.nymvpn.util.goAsync
import net.nymtech.vpn.NymVpnClient
import net.nymtech.vpn.VpnClient
import javax.inject.Inject

@AndroidEntryPoint
Expand All @@ -19,6 +19,9 @@ class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepository: SettingsRepository

@Inject
lateinit var vpnClient: VpnClient

override fun onReceive(context: Context?, intent: Intent?) = goAsync {
if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync
if (settingsRepository.isAutoStartEnabled()) {
Expand All @@ -28,8 +31,11 @@ class BootReceiver : BroadcastReceiver() {
context?.let { context ->
val entry = entryCountry.toEntryPoint()
val exit = exitCountry.toExitPoint()
NymVpnClient.configure(entry, exit, mode)
NymVpnClient.start(context)
vpnClient.apply {
this.mode = mode
this.exitPoint = exit
this.entryPoint = entry
}.start(context, true)
NymVpn.requestTileServiceStateUpdate(context)
}
}
Expand Down
13 changes: 10 additions & 3 deletions app/src/main/java/net/nymtech/nymvpn/service/AlwaysOnVpnService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import kotlinx.coroutines.launch
import net.nymtech.nymvpn.NymVpn
import net.nymtech.nymvpn.data.GatewayRepository
import net.nymtech.nymvpn.data.SettingsRepository
import net.nymtech.vpn.NymVpnClient
import net.nymtech.vpn.VpnClient
import timber.log.Timber
import javax.inject.Inject

Expand All @@ -21,6 +21,9 @@ class AlwaysOnVpnService : LifecycleService() {
@Inject
lateinit var settingsRepository: SettingsRepository

@Inject
lateinit var vpnClient: VpnClient

override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
// We don't provide binding, so return null
Expand All @@ -36,8 +39,12 @@ class AlwaysOnVpnService : LifecycleService() {
val mode = settingsRepository.getVpnMode()
val entry = entryCountry.toEntryPoint()
val exit = exitCountry.toExitPoint()
NymVpnClient.configure(entry, exit, mode)
NymVpnClient.start(this@AlwaysOnVpnService)
vpnClient.apply {
this.mode = mode
this.entryPoint = entry
this.exitPoint = exit
}
vpnClient.start(this@AlwaysOnVpnService, true)
NymVpn.requestTileServiceStateUpdate(this@AlwaysOnVpnService)
}
START_STICKY
Expand Down
25 changes: 14 additions & 11 deletions app/src/main/java/net/nymtech/nymvpn/service/tile/VpnQuickTile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import net.nymtech.nymvpn.NymVpn
import net.nymtech.nymvpn.R
import net.nymtech.nymvpn.data.GatewayRepository
import net.nymtech.nymvpn.data.SettingsRepository
import net.nymtech.vpn.NymVpnClient
import net.nymtech.vpn.VpnClient
import net.nymtech.vpn.model.VpnMode
import net.nymtech.vpn.model.VpnState
import timber.log.Timber
Expand All @@ -23,16 +23,20 @@ class VpnQuickTile : TileService() {
@Inject
lateinit var gatewayRepository: GatewayRepository

@Inject
lateinit var settingsRepository: SettingsRepository

@Inject
lateinit var vpnClient: VpnClient

private val scope = CoroutineScope(Dispatchers.IO)

override fun onStartListening() {
super.onStartListening()
Timber.d("Quick tile listening called")
setTileText()
scope.launch {
NymVpnClient.stateFlow.collect {
vpnClient.stateFlow.collect {
when (it.vpnState) {
VpnState.Up -> {
setActive()
Expand Down Expand Up @@ -76,17 +80,16 @@ class VpnQuickTile : TileService() {
setTileText()
Timber.i("Tile clicked")
unlockAndRun {
when (NymVpnClient.getState().vpnState) {
VpnState.Up -> NymVpnClient.disconnect(this)
when (vpnClient.getState().vpnState) {
VpnState.Up -> vpnClient.stop(this, true)
VpnState.Down -> {
scope.launch {
val entryCountry = gatewayRepository.getFirstHopCountry()
val exitCountry = gatewayRepository.getLastHopCountry()
val mode = settingsRepository.getVpnMode()
val entry = entryCountry.toEntryPoint()
val exit = exitCountry.toExitPoint()
NymVpnClient.configure(entry, exit, mode)
NymVpnClient.start(this@VpnQuickTile)
vpnClient.apply {
this.mode = settingsRepository.getVpnMode()
this.exitPoint = gatewayRepository.getLastHopCountry().toExitPoint()
this.entryPoint = gatewayRepository.getFirstHopCountry().toEntryPoint()
}
vpnClient.start(this@VpnQuickTile, true)
NymVpn.requestTileServiceStateUpdate(this@VpnQuickTile)
}
}
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/net/nymtech/nymvpn/ui/AppUiState.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package net.nymtech.nymvpn.ui

import net.nymtech.nymvpn.ui.theme.Theme
import net.nymtech.vpn.model.VpnState

data class AppUiState(
val loading: Boolean = true,
val theme: Theme = Theme.AUTOMATIC,
val loggedIn: Boolean = false,
val snackbarMessage: String = "",
val snackbarMessageConsumed: Boolean = true,
val vpnState: VpnState = VpnState.Down,
)
14 changes: 9 additions & 5 deletions app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import net.nymtech.nymvpn.data.SettingsRepository
import net.nymtech.nymvpn.util.Constants
import net.nymtech.nymvpn.util.FileUtils
import net.nymtech.nymvpn.util.log.NymLibException
import net.nymtech.vpn.NymVpnClient
import net.nymtech.vpn.NymApi
import net.nymtech.vpn.VpnClient
import net.nymtech.vpn.model.Country
import nym_vpn_lib.FfiException
import timber.log.Timber
Expand All @@ -40,20 +41,23 @@ constructor(
private val settingsRepository: SettingsRepository,
private val gatewayRepository: GatewayRepository,
private val application: Application,
private val vpnClient: VpnClient,
private val nymApi: NymApi,
) : ViewModel() {
private val _uiState = MutableStateFlow(AppUiState())

val logs = mutableStateListOf<LogMessage>()
private val logsBuffer = mutableListOf<LogMessage>()

val uiState =
combine(_uiState, settingsRepository.settingsFlow) { state, settings ->
combine(_uiState, settingsRepository.settingsFlow, vpnClient.stateFlow) { state, settings, vpnState ->
AppUiState(
false,
settings.theme,
settings.loggedIn,
state.snackbarMessage,
state.snackbarMessageConsumed,
vpnState.vpnState,
)
}.stateIn(
viewModelScope,
Expand Down Expand Up @@ -129,7 +133,7 @@ constructor(

private suspend fun updateEntryCountriesCache() {
try {
val entryCountries = NymVpnClient.gateways(false)
val entryCountries = nymApi.gateways(false)
gatewayRepository.setEntryCountries(entryCountries)
} catch (e: FfiException) {
Timber.e(e)
Expand All @@ -138,7 +142,7 @@ constructor(

private suspend fun updateExitCountriesCache() {
try {
val exitCountries = NymVpnClient.gateways(true)
val exitCountries = nymApi.gateways(true)
gatewayRepository.setExitCountries(exitCountries)
} catch (e: FfiException) {
Timber.e(e)
Expand All @@ -147,7 +151,7 @@ constructor(

private suspend fun setFirstHopToLowLatency() {
runCatching {
NymVpnClient.getLowLatencyEntryCountryCode()
nymApi.getLowLatencyEntryCountry()
}.onFailure {
Timber.e(it)
}.onSuccess {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,14 @@ fun MainScreen(navController: NavController, appUiState: AppUiState, viewModel:
ListOptionSelectionButton(
label = stringResource(R.string.first_hop),
value = firstHopName,
onClick = { navController.navigate(NavItem.Hop.Entry.route) },
onClick = { if (uiState.connectionState is ConnectionState.Disconnected) navController.navigate(NavItem.Hop.Entry.route) },
leadingIcon = firstHopIcon,
)
}
ListOptionSelectionButton(
label = stringResource(R.string.last_hop),
value = lastHopName,
onClick = { navController.navigate(NavItem.Hop.Exit.route) },
onClick = { if (uiState.connectionState is ConnectionState.Disconnected) navController.navigate(NavItem.Hop.Exit.route) },
leadingIcon = lastHopIcon,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import net.nymtech.nymvpn.ui.model.StateMessage
import net.nymtech.nymvpn.util.Constants
import net.nymtech.nymvpn.util.NumberUtils
import net.nymtech.nymvpn.util.StringValue
import net.nymtech.vpn.NymVpnClient
import net.nymtech.vpn.VpnClient
import net.nymtech.vpn.model.ErrorState
import net.nymtech.vpn.model.VpnMode
import javax.inject.Inject
Expand All @@ -29,12 +29,13 @@ constructor(
private val gatewayRepository: GatewayRepository,
private val settingsRepository: SettingsRepository,
private val application: Application,
private val vpnClient: VpnClient,
) : ViewModel() {
val uiState =
combine(
gatewayRepository.gatewayFlow,
settingsRepository.settingsFlow,
NymVpnClient.stateFlow,
vpnClient.stateFlow,
) { gateways, settings, clientState ->
val connectionTime =
clientState.statistics.connectionSeconds?.let {
Expand Down Expand Up @@ -86,13 +87,16 @@ constructor(
val mode = settingsRepository.getVpnMode()
val entry = entryCountry.toEntryPoint()
val exit = exitCountry.toExitPoint()
NymVpnClient.configure(entry, exit, mode)
NymVpnClient.start(application)
vpnClient.apply {
this.exitPoint = exit
this.entryPoint = entry
this.mode = mode
}.start(application)
NymVpn.requestTileServiceStateUpdate(application)
}

fun onDisconnect() = viewModelScope.launch {
NymVpnClient.disconnect(application)
vpnClient.stop(application)
NymVpn.requestTileServiceStateUpdate(application)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import net.nymtech.nymvpn.ui.common.buttons.surface.SelectionItem
import net.nymtech.nymvpn.ui.common.buttons.surface.SurfaceSelectionGroupButton
import net.nymtech.nymvpn.util.scaledHeight
import net.nymtech.nymvpn.util.scaledWidth
import net.nymtech.vpn.model.VpnState

@Composable
fun SettingsScreen(
Expand Down Expand Up @@ -117,6 +118,7 @@ fun SettingsScreen(
Modifier
.height(32.dp.scaledHeight())
.width(52.dp.scaledWidth()),
enabled = (appUiState.vpnState is VpnState.Down),
)
},
stringResource(R.string.entry_location),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ fun LoginScreen(navController: NavController, appViewModel: AppViewModel, viewMo
viewModel.onLogin(recoveryPhrase).let {
when (it) {
is Result.Success -> {
navController.navigate(NavItem.Main.route)
navController.navigate(NavItem.Main.route) {
// clear backstack after login
popUpTo(0)
}
appViewModel.showSnackbarMessage(
context.getString(R.string.credential_successful),
)
Expand Down
12 changes: 3 additions & 9 deletions buildSrc/src/main/kotlin/Constants.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import org.gradle.api.JavaVersion

object Constants {
const val VERSION_NAME = "v0.1.2-alpha"
const val VERSION_CODE = 12
const val VERSION_NAME = "v0.1.2"
const val VERSION_CODE = 13
const val TARGET_SDK = 34
const val COMPILE_SDK = 34
const val MIN_SDK = 24
Expand All @@ -29,17 +29,11 @@ object Constants {
const val SANDBOX = "sandbox"
const val BUILD_LIB_TASK = "buildDeps"

const val SANDBOX_API_URL = "https://sandbox-nym-api1.nymtech.net/api"
const val SANDBOX_EXPLORER_URL = "https://sandbox-explorer.nymtech.net/api"

const val MAINNET_API_URL = "https://validator.nymtech.net/api/"
const val MAINNET_EXPLORER_URL = "https://explorer.nymtech.net/api/"

//licensee
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
const val ANDROID_TERMS_URL = "https://developer.android.com/studio/terms.html"

//build config
const val OPT_IN_REPORTING = "OPT_IN_REPORTING"
const val SENTRY_DSN = "SENTRY_DSN"
}
}
Loading

0 comments on commit 224a505

Please sign in to comment.