mirror of
https://github.com/ProtonVPN/android-app.git
synced 2026-03-13 09:02:15 +08:00
ProTUN's packet capture support in "Debug tools"
This commit is contained in:
committed by
MargeBot
parent
4547196238
commit
3c8aefc7be
@@ -48,12 +48,14 @@ fun VpnSolidButton(
|
||||
enabled: Boolean = true,
|
||||
isExternalLink: Boolean = false,
|
||||
isLoading: Boolean = false,
|
||||
colors: ButtonColors = ButtonDefaults.protonButtonColors(isLoading)
|
||||
) {
|
||||
ProtonSolidButton(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
loading = isLoading,
|
||||
colors = colors
|
||||
) {
|
||||
VpnButtonContent(
|
||||
text = text,
|
||||
|
||||
@@ -19,15 +19,11 @@
|
||||
|
||||
package com.protonvpn.android.logging
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import com.protonvpn.android.BuildConfig
|
||||
import com.protonvpn.android.utils.createSendFileIntent
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
@@ -36,18 +32,6 @@ class LogFileForSharing @Inject constructor(
|
||||
) {
|
||||
suspend operator fun invoke(): Intent? {
|
||||
val file = ProtonLogger.getLogFileForSharing()
|
||||
return file?.let { createIntent(it) }
|
||||
}
|
||||
|
||||
private fun createIntent(file: File): Intent {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "*/*"
|
||||
|
||||
val contentUri: Uri =
|
||||
FileProvider.getUriForFile(appContext, "${BuildConfig.APPLICATION_ID}.fileprovider", file)
|
||||
intent.putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
intent.clipData = ClipData.newRawUri("", contentUri)
|
||||
return Intent.createChooser(intent, "Share log")
|
||||
return file?.let { appContext.createSendFileIntent(it, "Share log") }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,30 +21,44 @@ package com.protonvpn.android.redesign.settings.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.protonvpn.android.base.ui.ProtonVpnPreview
|
||||
import com.protonvpn.android.base.ui.VpnSolidButton
|
||||
import com.protonvpn.android.base.ui.protonButtonColors
|
||||
import com.protonvpn.android.redesign.base.ui.ProtonOutlinedTextField
|
||||
import com.protonvpn.android.redesign.base.ui.SettingsItem
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.presentation.R as CoreR
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun DebugTools(
|
||||
state: DebugToolsState,
|
||||
onConnectGuestHole: () -> Unit,
|
||||
onRefreshConfig: () -> Unit,
|
||||
netzone: String,
|
||||
country: String,
|
||||
setNetzone: (String) -> Unit,
|
||||
setCountry: (String) -> Unit,
|
||||
onClose: () -> Unit,
|
||||
setPcapActive: (Boolean) -> Unit,
|
||||
onSharePcapFile: () -> Unit,
|
||||
onRemovePcapFile: () -> Unit,
|
||||
onSetMaxPcapSizeMB: (Long) -> Unit,
|
||||
) {
|
||||
SubSetting(
|
||||
title = "Debug tools",
|
||||
@@ -58,14 +72,14 @@ fun DebugTools(
|
||||
|
||||
val paddingModifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp).fillMaxWidth()
|
||||
DebugTextInputRow(
|
||||
value = netzone,
|
||||
value = state.netzone,
|
||||
onValueChange = setNetzone,
|
||||
labelText = "x-pm-netzone",
|
||||
placeholderText = "IP address",
|
||||
modifier = paddingModifier,
|
||||
)
|
||||
DebugTextInputRow(
|
||||
value = country,
|
||||
value = state.country,
|
||||
onValueChange = setCountry,
|
||||
labelText = "x-pm-country",
|
||||
placeholderText = "2-letter country code",
|
||||
@@ -76,6 +90,67 @@ fun DebugTools(
|
||||
text = "Refresh config and servers",
|
||||
modifier = paddingModifier,
|
||||
)
|
||||
|
||||
SettingsItem(
|
||||
modifier = Modifier.clickable(onClick = onConnectGuestHole),
|
||||
name = "Packet capture",
|
||||
)
|
||||
|
||||
VpnSolidButton(
|
||||
onClick = { setPcapActive(!state.isPacketCaptureActive) },
|
||||
text = if (state.isPacketCaptureActive) "Stop PCAP" else "Start PCAP",
|
||||
colors =
|
||||
if (state.isPacketCaptureActive) ButtonDefaults.protonButtonColors(
|
||||
contentColor = ProtonTheme.colors.textNorm,
|
||||
backgroundColor = ProtonTheme.colors.notificationError
|
||||
) else {
|
||||
ButtonDefaults.protonButtonColors()
|
||||
},
|
||||
modifier = paddingModifier,
|
||||
)
|
||||
|
||||
if (!state.isPacketCaptureActive) {
|
||||
DebugTextInputRow(
|
||||
value = if (state.pcapMaxMBytes == 0L) "" else state.pcapMaxMBytes.toString(),
|
||||
onValueChange = { newValue ->
|
||||
// Filter to only allow digits (positive numbers)
|
||||
val filtered = newValue.filter { it.isDigit() }
|
||||
val sizeInMB = filtered.toLongOrNull() ?: 0L
|
||||
onSetMaxPcapSizeMB(sizeInMB)
|
||||
},
|
||||
labelText = "Max PCAP size (MB)",
|
||||
placeholderText = state.pcapMaxMBytes.takeIf { it > 0 }?.toString() ?: "Unlimited",
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = paddingModifier,
|
||||
)
|
||||
}
|
||||
|
||||
state.existingPcapFileName?.let { fileName ->
|
||||
Row(
|
||||
modifier = paddingModifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = fileName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
IconButton(onClick = onSharePcapFile) {
|
||||
Icon(
|
||||
painter = painterResource(id = CoreR.drawable.ic_proton_arrow_up_from_square),
|
||||
contentDescription = "Share",
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onRemovePcapFile) {
|
||||
Icon(
|
||||
painter = painterResource(id = CoreR.drawable.ic_proton_trash),
|
||||
contentDescription = "Remove",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +161,7 @@ private fun DebugTextInputRow(
|
||||
labelText: String,
|
||||
placeholderText: String,
|
||||
modifier: Modifier = Modifier,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
@@ -98,6 +174,7 @@ private fun DebugTextInputRow(
|
||||
placeholderText = placeholderText,
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
keyboardOptions = keyboardOptions,
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = CoreR.drawable.ic_proton_close),
|
||||
@@ -110,3 +187,28 @@ private fun DebugTextInputRow(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ProtonVpnPreview
|
||||
@Composable
|
||||
fun DebugToolsPreview() {
|
||||
ProtonVpnPreview {
|
||||
DebugTools(
|
||||
state = DebugToolsState(
|
||||
netzone = "netzone",
|
||||
country = "country",
|
||||
isPacketCaptureActive = true,
|
||||
pcapMaxMBytes = 100,
|
||||
existingPcapFileName = "protonvpn.pcap",
|
||||
),
|
||||
onConnectGuestHole = {},
|
||||
onRefreshConfig = {},
|
||||
setNetzone = {},
|
||||
setCountry = {},
|
||||
onClose = {},
|
||||
setPcapActive = {},
|
||||
onSharePcapFile = {},
|
||||
onRemovePcapFile = {},
|
||||
onSetMaxPcapSizeMB = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -20,17 +20,23 @@
|
||||
package com.protonvpn.android.redesign.settings.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.protonvpn.android.api.GuestHole
|
||||
import com.protonvpn.android.api.data.DebugApiPrefs
|
||||
import com.protonvpn.android.promooffers.data.ApiNotificationManager
|
||||
import com.protonvpn.android.appconfig.AppConfig
|
||||
import com.protonvpn.android.auth.usecase.CurrentUser
|
||||
import com.protonvpn.android.promooffers.data.ApiNotificationManager
|
||||
import com.protonvpn.android.ui.home.ServerListUpdater
|
||||
import com.protonvpn.android.utils.ifOrNull
|
||||
import com.protonvpn.android.vpn.protun.PacketCapture
|
||||
import com.protonvpn.android.vpn.protun.PacketCapturePrefs
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -42,21 +48,38 @@ class DebugToolsViewModel @Inject constructor(
|
||||
private val appConfig: AppConfig,
|
||||
private val serverListUpdater: ServerListUpdater,
|
||||
private val apiNotificationManager: ApiNotificationManager,
|
||||
private val packetCapture: PacketCapture,
|
||||
debugApiPrefsNullable: DebugApiPrefs?,
|
||||
private val packetCapturePrefs: PacketCapturePrefs,
|
||||
): ViewModel() {
|
||||
|
||||
private val debugApiPrefs = requireNotNull(debugApiPrefsNullable)
|
||||
private val updateStateTrigger = MutableSharedFlow<Unit>(replay = 1).apply {
|
||||
tryEmit(Unit)
|
||||
}
|
||||
|
||||
val state = combine(
|
||||
debugApiPrefs.netzoneFlow,
|
||||
debugApiPrefs.countryFlow,
|
||||
) { netzone, country ->
|
||||
packetCapturePrefs.maxBytesFlow,
|
||||
packetCapture.isCaptureActiveFlow,
|
||||
updateStateTrigger
|
||||
) { netzone, country, pcapMaxBytes, isPacketCaptureActive, _ ->
|
||||
DebugToolsState(
|
||||
netzone = netzone,
|
||||
country = country,
|
||||
netzone = netzone.orEmpty(),
|
||||
country = country.orEmpty(),
|
||||
isPacketCaptureActive = isPacketCaptureActive,
|
||||
pcapMaxMBytes = pcapMaxBytes / (1024 * 1024),
|
||||
existingPcapFileName = ifOrNull(!isPacketCaptureActive) { packetCapture.fileIfExists()?.name }
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface Event {
|
||||
data class SendPcap(val file: File): Event
|
||||
data object ShowNoPcapFound: Event
|
||||
}
|
||||
val events = MutableSharedFlow<Event>(extraBufferCapacity = 1)
|
||||
|
||||
fun connectGuestHole() {
|
||||
mainScope.launch {
|
||||
guestHole.onAlternativesUnblock {
|
||||
@@ -80,9 +103,39 @@ class DebugToolsViewModel @Inject constructor(
|
||||
fun setCountry(country: String) {
|
||||
debugApiPrefs.country = country.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
fun setPcapActive(active: Boolean) {
|
||||
viewModelScope.launch {
|
||||
packetCapture.setActive(active)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMaxPcapSizeMB(sizeInMBytes: Long) {
|
||||
packetCapturePrefs.maxBytes = sizeInMBytes * 1024 * 1024
|
||||
}
|
||||
|
||||
fun removePcapFile() {
|
||||
viewModelScope.launch {
|
||||
packetCapture.removeFileIfInactive()
|
||||
updateStateTrigger.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun sharePcapFile() {
|
||||
viewModelScope.launch {
|
||||
val file = packetCapture.fileIfExists()
|
||||
if (file != null)
|
||||
events.emit(Event.SendPcap(file))
|
||||
else
|
||||
events.emit(Event.ShowNoPcapFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DebugToolsState(
|
||||
val netzone: String?,
|
||||
val country: String?,
|
||||
val netzone: String,
|
||||
val country: String,
|
||||
val isPacketCaptureActive: Boolean,
|
||||
val pcapMaxMBytes: Long,
|
||||
val existingPcapFileName: String?,
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ package com.protonvpn.android.redesign.settings.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -76,6 +77,7 @@ import com.protonvpn.android.ui.settings.SettingsSplitTunnelAppsActivity
|
||||
import com.protonvpn.android.ui.settings.SettingsSplitTunnelIpsActivity
|
||||
import com.protonvpn.android.utils.Constants
|
||||
import com.protonvpn.android.utils.DebugUtils
|
||||
import com.protonvpn.android.utils.createSendFileIntent
|
||||
import com.protonvpn.android.utils.openUrl
|
||||
import com.protonvpn.android.utils.openVpnSettings
|
||||
import com.protonvpn.android.vpn.ui.LocalVpnUiDelegate
|
||||
@@ -271,15 +273,28 @@ fun SubSettingsRoute(
|
||||
SubSettingsScreen.Type.DebugTools -> {
|
||||
val debugToolsViewModel = hiltViewModel<DebugToolsViewModel>()
|
||||
val state = debugToolsViewModel.state.collectAsStateWithLifecycle(initialValue = null).value
|
||||
DebugTools(
|
||||
onClose = onClose,
|
||||
onConnectGuestHole = debugToolsViewModel::connectGuestHole,
|
||||
onRefreshConfig = debugToolsViewModel::refreshConfig,
|
||||
netzone = state?.netzone ?: "",
|
||||
country = state?.country ?: "",
|
||||
setNetzone = debugToolsViewModel::setNetzone,
|
||||
setCountry = debugToolsViewModel::setCountry,
|
||||
)
|
||||
debugToolsViewModel.events.collectAsEffect { event ->
|
||||
when (event) {
|
||||
is DebugToolsViewModel.Event.SendPcap ->
|
||||
context.startActivity(context.createSendFileIntent(event.file, "Share pcap"))
|
||||
DebugToolsViewModel.Event.ShowNoPcapFound ->
|
||||
Toast.makeText(context, "No PCAP captured.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
if (state != null) {
|
||||
DebugTools(
|
||||
state = state,
|
||||
onClose = onClose,
|
||||
onConnectGuestHole = debugToolsViewModel::connectGuestHole,
|
||||
onRefreshConfig = debugToolsViewModel::refreshConfig,
|
||||
setNetzone = debugToolsViewModel::setNetzone,
|
||||
setCountry = debugToolsViewModel::setCountry,
|
||||
setPcapActive = debugToolsViewModel::setPcapActive,
|
||||
onRemovePcapFile = debugToolsViewModel::removePcapFile,
|
||||
onSetMaxPcapSizeMB = debugToolsViewModel::setMaxPcapSizeMB,
|
||||
onSharePcapFile = debugToolsViewModel::sharePcapFile,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SubSettingsScreen.Type.NatType -> {
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.app.Application
|
||||
import android.app.ApplicationExitInfo
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Context.RECEIVER_NOT_EXPORTED
|
||||
import android.content.ContextWrapper
|
||||
@@ -52,9 +53,11 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.browser.customtabs.CustomTabsClient
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.protonvpn.android.BuildConfig
|
||||
import com.protonvpn.android.R
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -376,3 +379,15 @@ fun BroadcastReceiver.launchAsyncReceive(scope: CoroutineScope, block: suspend (
|
||||
|
||||
inline fun <reified T> ifOrNull(predicate: Boolean, block: () -> T): T? =
|
||||
if (predicate) block() else null
|
||||
|
||||
fun Context.createSendFileIntent(file: File, title: String): Intent {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "*/*"
|
||||
|
||||
val contentUri: Uri =
|
||||
FileProvider.getUriForFile(this, "${BuildConfig.APPLICATION_ID}.fileprovider", file)
|
||||
intent.putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
intent.clipData = ClipData.newRawUri("", contentUri)
|
||||
return Intent.createChooser(intent, title)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import me.proton.core.network.domain.session.SessionId
|
||||
import me.proton.vpn.sdk.api.InitialConfig
|
||||
import me.proton.vpn.sdk.api.InterfaceConfig
|
||||
import me.proton.vpn.sdk.api.IpNetworkPrefix
|
||||
import me.proton.vpn.sdk.api.PacketCaptureInfo
|
||||
import me.proton.vpn.sdk.api.Peer
|
||||
import me.proton.vpn.sdk.api.SplitTunnelAppsConfig
|
||||
import me.proton.vpn.sdk.api.SplitTunnelMode
|
||||
@@ -68,6 +69,7 @@ class ConnectionParamsProTun(
|
||||
sessionId: SessionId?,
|
||||
certificateRepository: CertificateRepository,
|
||||
computeAllowedIPs: ComputeAllowedIPs,
|
||||
pcapFile: PacketCaptureInfo?,
|
||||
): InitialConfig {
|
||||
val allowedIps =
|
||||
if (connectIntent is AnyConnectIntent.GuestHole) {
|
||||
@@ -84,7 +86,7 @@ class ConnectionParamsProTun(
|
||||
)
|
||||
|
||||
val privateKey = certificateRepository.getX25519Key(sessionId)
|
||||
return InitialConfig(iface, privateKey, peers)
|
||||
return InitialConfig(iface, privateKey, peers, pcapFile)
|
||||
}
|
||||
|
||||
fun splitTunnelAppsConfig(
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2026. Proton AG
|
||||
*
|
||||
* This file is part of ProtonVPN.
|
||||
*
|
||||
* ProtonVPN is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonVPN is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.protonvpn.android.vpn.protun
|
||||
|
||||
import android.content.Context
|
||||
import com.protonvpn.android.concurrency.VpnDispatcherProvider
|
||||
import com.protonvpn.android.utils.ifOrNull
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.proton.vpn.sdk.api.PacketCaptureFile
|
||||
import me.proton.vpn.sdk.api.PacketCaptureInfo
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class PacketCapture @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val dispatcherProvider: VpnDispatcherProvider,
|
||||
private val prefs: PacketCapturePrefs,
|
||||
) {
|
||||
val isCaptureActiveFlow =
|
||||
prefs.isActiveFlow.filterNotNull()
|
||||
|
||||
val activeFileFlow: Flow<PacketCaptureInfo?> = combine(
|
||||
prefs.isActiveFlow,
|
||||
prefs.maxBytesFlow,
|
||||
) { active, maxBytes ->
|
||||
ifOrNull(active) {
|
||||
context.shareDir().mkdirs()
|
||||
PacketCaptureInfo(
|
||||
PacketCaptureFile.Path(context.packetCaptureFile(), append = false),
|
||||
maxBytes.toULong()
|
||||
)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
fun setActive(active: Boolean) {
|
||||
prefs.isActive = active
|
||||
}
|
||||
|
||||
suspend fun fileIfExists() = withContext(dispatcherProvider.Io) {
|
||||
context.packetCaptureFile().takeIf { it.exists() }
|
||||
}
|
||||
|
||||
suspend fun removeFileIfInactive() = withContext(dispatcherProvider.Io) {
|
||||
val active = isCaptureActiveFlow.first()
|
||||
if (!active) {
|
||||
context.packetCaptureFile().takeIf { it.exists() }?.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.shareDir() = File(cacheDir, "share")
|
||||
private fun Context.packetCaptureFile() = File(shareDir(), "protonvpn.pcap")
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG
|
||||
*
|
||||
* This file is part of ProtonVPN.
|
||||
*
|
||||
* ProtonVPN is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonVPN is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonVPN. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.protonvpn.android.vpn.protun
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.protonvpn.android.utils.SharedPreferencesProvider
|
||||
import dagger.Reusable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import me.proton.core.util.android.sharedpreferences.PreferencesProvider
|
||||
import me.proton.core.util.android.sharedpreferences.boolean
|
||||
import me.proton.core.util.android.sharedpreferences.long
|
||||
import me.proton.core.util.android.sharedpreferences.observe
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class PacketCapturePrefs @Inject constructor(
|
||||
private val prefsProvider: SharedPreferencesProvider
|
||||
) : PreferencesProvider {
|
||||
|
||||
override val preferences: SharedPreferences
|
||||
get() = prefsProvider.getPrefs(PREFS_NAME)
|
||||
|
||||
var isActive: Boolean by boolean(key = KEY_IS_ACTIVE, default = false)
|
||||
val isActiveFlow: Flow<Boolean> = preferences.observe<Boolean>(KEY_IS_ACTIVE).mapNotNull { it ?: isActive }
|
||||
var maxBytes: Long by long(key = KEY_MAX_BYTES, default = DEFAULT_MAX_BYTES)
|
||||
val maxBytesFlow: Flow<Long> = preferences.observe<Long>(key = KEY_MAX_BYTES).mapNotNull { it ?: maxBytes }
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "PcapPrefs"
|
||||
private const val KEY_IS_ACTIVE = "is_active"
|
||||
private const val KEY_MAX_BYTES = "max_bytes"
|
||||
|
||||
private const val DEFAULT_MAX_BYTES = 100L * 1024 * 1024 // 100 MB
|
||||
}
|
||||
}
|
||||
@@ -43,9 +43,13 @@ import com.protonvpn.android.vpn.VpnBackend
|
||||
import com.protonvpn.android.vpn.VpnState
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.core.network.data.di.SharedOkHttpClient
|
||||
import me.proton.core.network.domain.NetworkManager
|
||||
import me.proton.vpn.sdk.api.ProtonVpnSdk
|
||||
@@ -70,11 +74,14 @@ class ProTunBackend @Inject constructor(
|
||||
@SharedOkHttpClient okHttp: OkHttpClient,
|
||||
private val sdk: ProtonVpnSdk,
|
||||
private val computeAllowedIPs: ComputeAllowedIPs,
|
||||
private val preparePeers: PreparePeersForConnectionProTun
|
||||
private val preparePeers: PreparePeersForConnectionProTun,
|
||||
private val packetCapture: PacketCapture,
|
||||
) : VpnBackend(
|
||||
settingsForConnection, certificateRepository, networkManager, networkCapabilitiesFlow, VpnProtocol.ProTun, mainScope,
|
||||
dispatcherProvider, localAgentUnreachableTracker, currentUser, getNetZone, foregroundActivityTracker, okHttp, shouldWaitForTunnelVerified = false
|
||||
) {
|
||||
private var observePcapJob: Job? = null
|
||||
|
||||
init {
|
||||
sdk.connectionManager.state.onEach { state ->
|
||||
ProtonLogger.logCustom(
|
||||
@@ -124,15 +131,30 @@ class ProTunBackend @Inject constructor(
|
||||
|
||||
override suspend fun connect(connectionParams: ConnectionParams) {
|
||||
super.connect(connectionParams)
|
||||
val wireGuardParams = connectionParams as ConnectionParamsProTun
|
||||
val settings = settingsForConnection.getFor(wireGuardParams.connectIntent)
|
||||
val protunParams = connectionParams as ConnectionParamsProTun
|
||||
val settings = settingsForConnection.getFor(protunParams.connectIntent)
|
||||
val sessionId = currentUser.sessionId()
|
||||
val config = wireGuardParams.getTunnelConfig(
|
||||
context, settings, sessionId, certificateRepository, computeAllowedIPs)
|
||||
val config = protunParams.getTunnelConfig(
|
||||
context,
|
||||
settings,
|
||||
sessionId,
|
||||
certificateRepository,
|
||||
computeAllowedIPs,
|
||||
packetCapture.activeFileFlow.first()
|
||||
)
|
||||
sdk.connectionManager.connect(config)
|
||||
|
||||
observePcapJob = mainScope.launch {
|
||||
packetCapture.activeFileFlow
|
||||
.drop(1)
|
||||
.collect { pcapFile -> sdk.connectionManager.setPacketCaptureEnabled(pcapFile) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun closeVpnTunnel(withStateChange: Boolean) {
|
||||
observePcapJob?.cancel()
|
||||
observePcapJob = null
|
||||
|
||||
sdk.connectionManager.disconnect()
|
||||
if (withStateChange) {
|
||||
// Set state to disabled right away to give app some time to close notification
|
||||
|
||||
@@ -70,7 +70,7 @@ proton-core-env-config-version = "1.3.1"
|
||||
proton-core-version = "36.3.2"
|
||||
proton-test-fusion-version = "0.9.97"
|
||||
proton-vpn-govpnlib-version = "0.1.78"
|
||||
proton-vpn-internal-sdk-version = "0.1.24-1.0.28"
|
||||
proton-vpn-internal-sdk-version = "0.1.29-1.0.29"
|
||||
proton-vpn-wireguard-version = "1.0.20230512.29"
|
||||
relex-circleindicator-version = "2.1.6"
|
||||
retrofit-version = "2.11.0"
|
||||
|
||||
Reference in New Issue
Block a user