initial commit

This commit is contained in:
zaneschepke
2026-02-03 06:40:04 -05:00
commit 478aef6952
185 changed files with 11241 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
*.iml
.kotlin
.gradle
**/build/
xcuserdata
!src/**/build/
local.properties
output
.idea
.DS_Store
captures
.externalNativeBuild
.cxx
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
composeApp/generated.conveyor.conf
+6
View File
@@ -0,0 +1,6 @@
[submodule "daemon/winsw"]
path = daemon/winsw
url = https://github.com/wgtunnel/winsw
[submodule "tunnel/tools/amneziawg-tools"]
path = tunnel/tools/amneziawg-tools
url = https://github.com/amnezia-vpn/amneziawg-tools
+8
View File
@@ -0,0 +1,8 @@
# WG Tunnel - Desktop
A WIP project for WG Tunnel desktop.
## Supported Platforms
- macOS (Future)
- Windows
- Linux
+52
View File
@@ -0,0 +1,52 @@
// build.gradle.kts
plugins {
alias(libs.plugins.composeHotReload) apply false
alias(libs.plugins.jetbrainsCompose) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.conveyor) apply false
alias(libs.plugins.moko) apply false
alias(libs.plugins.buildconfig) apply false
}
val jvmVersion = libs.versions.jvm.get().toInt()
version = libs.versions.app.get()
allprojects {
group = "com.zaneschepke.wireguardautotunnel"
version = version
plugins.withId("org.jetbrains.kotlin.jvm") {
extensions.configure<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension> {
jvmToolchain(jvmVersion)
}
}
plugins.withId("org.jetbrains.kotlin.multiplatform") {
extensions.configure<org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension> {
jvmToolchain(jvmVersion)
}
}
}
registerConveyorTask(
taskName = "buildLinuxDeb",
packageType = "debian-package",
subDir = "deb",
)
registerConveyorTask(
taskName = "buildWindowsMsix",
packageType = "windows-msix",
subDir = "windows",
)
registerConveyorTask(
taskName = "buildConveyorSite",
packageType = "site",
subDir = "site"
)
tasks.register<Delete>("clean") {
delete(layout.buildDirectory)
}
+13
View File
@@ -0,0 +1,13 @@
plugins {
`kotlin-dsl` // enable the Kotlin-DSL
}
repositories {
gradlePluginPortal()
mavenCentral()
google()
}
dependencies {
implementation("org.apache.commons:commons-lang3:3.20.0")
}
+1
View File
@@ -0,0 +1 @@
rootProject.name = "buildSrc"
@@ -0,0 +1,19 @@
import java.io.File
import java.io.FileInputStream
import java.util.*
object LocalProperties {
private val properties by lazy {
val props = Properties()
val file = File("local.properties")
if (file.exists()) {
FileInputStream(file).use { props.load(it) }
}
props
}
fun get(key: String): String? = properties.getProperty(key)
fun getOrDefault(key: String, default: String): String = properties.getProperty(key, default)
}
+5
View File
@@ -0,0 +1,5 @@
object SystemVar {
fun fromEnvironment(envVar : String) : String? {
return System.getenv(envVar)
}
}
+38
View File
@@ -0,0 +1,38 @@
import org.gradle.api.Project
import org.gradle.api.tasks.Exec
import org.gradle.kotlin.dsl.register
fun Project.registerConveyorTask(
taskName: String,
packageType: String,
subDir: String,
) {
tasks.register<Exec>(taskName) {
group = "distribution"
val outputDir = layout.buildDirectory.dir("conveyor/$subDir")
outputs.dir(outputDir)
environment(
"CONVEYOR_PASSPHRASE",
SystemVar.fromEnvironment("CONVEYOR_PASSPHRASE") ?: LocalProperties.get("conveyor.passphrase") ?: ""
)
val args = mutableListOf(
"conveyor",
"--passphrase=env:CONVEYOR_PASSPHRASE",
"make",
"--output-dir", outputDir.get().asFile.absolutePath,
packageType
)
commandLine(args)
dependsOn(
":composeApp:createDistributable",
":cli:installDist",
":daemon:installDist",
":composeApp:writeConveyorConfig"
)
}
}
+1
View File
@@ -0,0 +1 @@
/build
+49
View File
@@ -0,0 +1,49 @@
plugins {
application
alias(libs.plugins.serialization)
kotlin("jvm")
kotlin("kapt")
}
dependencies {
implementation(project(":client"))
// CLI
implementation(libs.picocli)
kapt(libs.picocli.codegen)
// DI
implementation(libs.koin.core)
// Logging
implementation(libs.kermit)
implementation(libs.logback.classic)
implementation(libs.kotlinx.serialization)
implementation(libs.kotlinx.coroutines.core)
}
kapt {
arguments {
arg("project", "${project.group}/${project.name}")
}
}
tasks.named<Sync>("installDist") {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
application {
mainClass.set("com.zaneschepke.wireguardautotunnel.cli.MainKt")
}
tasks.withType<Jar> {
manifest {
attributes(
"Implementation-Title" to project.name,
"Implementation-Version" to libs.versions.app.get(),
"Main-Class" to application.mainClass.get()
)
}
}
@@ -0,0 +1,32 @@
package com.zaneschepke.wireguardautotunnel.cli
import com.zaneschepke.wireguardautotunnel.cli.CliRoot.Companion.BANNER
import com.zaneschepke.wireguardautotunnel.cli.commands.tunnel.TunnelCommand
import com.zaneschepke.wireguardautotunnel.cli.provider.ManifestVersionProvider
import picocli.CommandLine.Command
@Command(
name = "wgtunnel",
description = ["CLI client for WG Tunnel."],
mixinStandardHelpOptions = true,
versionProvider = ManifestVersionProvider::class,
header = [BANNER],
subcommands = [
TunnelCommand::class
]
)
class CliRoot : Runnable {
override fun run() {
}
companion object {
const val BANNER: String = (""
+ "██╗ ██╗ ██████╗ ████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗ \n"
+ "██║ ██║██╔════╝ ╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║ \n"
+ "██║ █╗ ██║██║ ███╗ ██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██║ \n"
+ "██║███╗██║██║ ██║ ██║ ██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██║ \n"
+ "╚███╔███╔╝╚██████╔╝ ██║ ╚██████╔╝██║ ╚████║██║ ╚████║███████╗███████╗\n"
+ " ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚══════╝\n")
}
}
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.cli
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import co.touchlab.kermit.platformLogWriter
import com.zaneschepke.wireguardautotunnel.cli.commands.handler.CliExceptionHandler
import com.zaneschepke.wireguardautotunnel.cli.strategy.CliExecutionStrategy
import com.zaneschepke.wireguardautotunnel.client.di.databaseModule
import com.zaneschepke.wireguardautotunnel.client.di.serviceModule
import org.koin.core.context.startKoin
import picocli.CommandLine
fun main(args: Array<String>) {
Logger.setLogWriters(platformLogWriter())
Logger.setMinSeverity(Severity.Debug)
Logger.setTag("CLI")
startKoin {
modules(databaseModule, serviceModule)
}
val commandLine = CommandLine(CliRoot::class.java)
commandLine.executionStrategy = CliExecutionStrategy(commandLine.executionStrategy)
commandLine.executionExceptionHandler = CliExceptionHandler()
commandLine.execute(*args)
}
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.cli.commands.handler
import picocli.CommandLine
import picocli.CommandLine.IExecutionExceptionHandler
import picocli.CommandLine.ParseResult
class CliExceptionHandler : IExecutionExceptionHandler {
override fun handleExecutionException(
ex: Exception,
commandLine: CommandLine,
parseResult: ParseResult
): Int {
commandLine.err.println(
commandLine.colorScheme.errorText("Error completing command: ${ex.message}")
)
return CommandLine.ExitCode.SOFTWARE
}
}
@@ -0,0 +1,20 @@
package com.zaneschepke.wireguardautotunnel.cli.commands.tunnel
import picocli.CommandLine.Command
@Command(
name = "tunnel",
mixinStandardHelpOptions = true,
subcommands = [
TunnelUpCommand::class,
TunnelDownCommand::class,
TunnelImportCommand::class,
TunnelListCommand::class,
TunnelDeleteCommand::class,
]
)
class TunnelCommand : Runnable {
override fun run() {
println("Please specify a subcommand: start, stop, list, etc..")
}
}
@@ -0,0 +1,38 @@
package com.zaneschepke.wireguardautotunnel.cli.commands.tunnel
import com.zaneschepke.wireguardautotunnel.client.domain.repository.TunnelRepository
import kotlinx.coroutines.runBlocking
import org.koin.java.KoinJavaComponent.inject
import picocli.CommandLine.*
import java.util.concurrent.Callable
@Command(
name = "delete",
description = ["Delete a tunnel."],
)
class TunnelDeleteCommand : Callable<Int> {
private val tunnelRepository: TunnelRepository by inject(TunnelRepository::class.java)
@Option(names = ["-y", "--yes"], description = ["Delete without additional prompts."])
var yes: Boolean? = null
@Parameters(index = "0", paramLabel = "<tunnel-name>", description = ["The name of the tunnel to bring up."])
lateinit var tunnelName: String
override fun call(): Int = runBlocking {
if(yes == null) {
print("Are you sure you want to delete $tunnelName? [y/N]: ")
val userInput = readlnOrNull()?.trim()?.lowercase()
if (userInput != "y" && userInput != "yes") return@runBlocking 0
}
try {
tunnelRepository.deleteByName(tunnelName)
} catch (_: Exception) {
System.err.println("Failed to delete $tunnelName! Check that the service is running.")
return@runBlocking 1
}
return@runBlocking 0
}
}
@@ -0,0 +1,30 @@
package com.zaneschepke.wireguardautotunnel.cli.commands.tunnel
import com.zaneschepke.wireguardautotunnel.client.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.client.service.TunnelCommandService
import kotlinx.coroutines.runBlocking
import org.koin.java.KoinJavaComponent.inject
import picocli.CommandLine.Command
import picocli.CommandLine.Parameters
@Command(name = "down", description = ["Bring a tunnel down."])
class TunnelDownCommand : Runnable {
private val tunnelService: TunnelCommandService by inject(TunnelCommandService::class.java)
private val tunnelRepository: TunnelRepository by inject(TunnelRepository::class.java)
@Parameters(index = "0", paramLabel = "<tunnel-name>", description = ["The name of the tunnel to bring down."])
lateinit var tunnelName: String
override fun run() {
runBlocking {
val tunnel = tunnelRepository.getTunnelByName(tunnelName) ?: return@runBlocking println("Tunnel $tunnelName not found")
val result = tunnelService.stopTunnel(tunnel.id)
if (result.isSuccess) {
println("Tunnel stopped successfully.")
} else {
println("Failed to stop tunnel: ${result.exceptionOrNull()?.message ?: "Unknown error"}")
}
}
}
}
@@ -0,0 +1,67 @@
package com.zaneschepke.wireguardautotunnel.cli.commands.tunnel
import com.zaneschepke.wireguardautotunnel.client.service.TunnelImportService
import kotlinx.coroutines.runBlocking
import org.koin.java.KoinJavaComponent.inject
import picocli.CommandLine.*
import java.io.File
import java.util.concurrent.Callable
@Command(
name = "import",
description = ["Import configuration from a file, string, or stdin."]
)
class TunnelImportCommand : Callable<Int> {
private val tunnelImportService: TunnelImportService by inject(TunnelImportService::class.java)
@ArgGroup(exclusive = true, multiplicity = "1")
lateinit var input: Input
class Input {
@Option(names = ["--file"], description = ["Import config from file"])
var file: File? = null
@Option(names = ["--string"], description = ["Import config from string literal"])
var string: String? = null
}
@Option(names = ["--name"], description = ["Specify a tunnel name"])
var name: String? = null
override fun call(): Int = runBlocking {
val config : String = try {
when {
input.file != null -> {
val f = input.file!!
if (!f.exists()) {
System.err.println("Error: File does not exist: ${f.absolutePath}")
return@runBlocking 1
}
if (!f.isFile) {
System.err.println("Error: Not a file: ${f.absolutePath}")
return@runBlocking 1
}
f.readText()
}
input.string != null -> input.string!!
else -> {
System.err.println("Error: No input source provided. Use --file, --string, or - for stdin.")
return@runBlocking 1
}
}
} catch (e: Exception) {
System.err.println("Error reading input: ${e.message}")
return@runBlocking 1
}
val name = name ?: input.file?.nameWithoutExtension
tunnelImportService.import(config , name)
return@runBlocking 0
}
}
@@ -0,0 +1,48 @@
package com.zaneschepke.wireguardautotunnel.cli.commands.tunnel
import co.touchlab.kermit.Logger
import com.zaneschepke.wireguardautotunnel.client.domain.repository.TunnelRepository
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import org.koin.java.KoinJavaComponent.inject
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import java.util.concurrent.Callable
@Command(
name = "list",
description = ["List configured WG Tunnel tunnels."]
)
class TunnelListCommand : Callable<Int> {
private val tunnelRepository: TunnelRepository by inject(TunnelRepository::class.java)
@Option(names = ["--json"], description = ["Output in JSON format for scripting."])
var json: Boolean = false
override fun call(): Int = runBlocking {
val tunnels = try {
tunnelRepository.getAll().sortedBy { it.position }
} catch (e: Exception) {
Logger.e("failed to load tunnels", e)
System.err.println("Error: Failed to retrieve tunnels. ${e.message}")
return@runBlocking 1
}
if (tunnels.isEmpty()) {
println("No tunnels found")
return@runBlocking 0
}
if (json) {
val names = tunnels.map { it.name }
println(Json.encodeToString(names))
} else {
// TODO better strategy for large number of tunnels
println("Configured Tunnels:")
tunnels.forEach { println(it.name) }
}
return@runBlocking 0
}
}
@@ -0,0 +1,29 @@
package com.zaneschepke.wireguardautotunnel.cli.commands.tunnel
import com.zaneschepke.wireguardautotunnel.client.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.client.service.TunnelCommandService
import kotlinx.coroutines.runBlocking
import org.koin.java.KoinJavaComponent.inject
import picocli.CommandLine.Command
import picocli.CommandLine.Parameters
@Command(name = "up", description = ["Bring a tunnel up."])
class TunnelUpCommand : Runnable {
private val tunnelService: TunnelCommandService by inject(TunnelCommandService::class.java)
private val tunnelRepository: TunnelRepository by inject(TunnelRepository::class.java)
@Parameters(index = "0", paramLabel = "<tunnel-name>", description = ["The name of the tunnel to bring up."])
lateinit var tunnelName: String
override fun run() {
runBlocking {
val tunnel = tunnelRepository.getTunnelByName(tunnelName) ?: return@runBlocking println("Failed to find the $tunnelName")
val result = tunnelService.startTunnel(tunnel.id)
if (result.isSuccess) {
println("Tunnel start triggered successfully.")
} else {
println("Failed to start tunnel: ${result.exceptionOrNull()?.message ?: "Unknown error"}")
}
}
}
}
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.cli.provider
import picocli.CommandLine
class ManifestVersionProvider : CommandLine.IVersionProvider {
override fun getVersion(): Array<String> {
val version = ManifestVersionProvider::class.java.getPackage().implementationVersion
return if (version != null) arrayOf(version) else arrayOf("Unknown version")
}
}
@@ -0,0 +1,33 @@
package com.zaneschepke.wireguardautotunnel.cli.strategy
import com.zaneschepke.wireguardautotunnel.client.service.DaemonHealthService
import kotlinx.coroutines.runBlocking
import org.koin.java.KoinJavaComponent.inject
import picocli.CommandLine.*
class CliExecutionStrategy(private val defaultStrategy: IExecutionStrategy) : IExecutionStrategy {
val daemonHealthService : DaemonHealthService by inject(DaemonHealthService::class.java)
override fun execute(parseResult: ParseResult): Int = runBlocking {
// Drill down to the deepest subcommand
var current = parseResult
while (current.hasSubcommand()) {
current = current.subcommand()
}
val commandSpec = current.commandSpec()
val skipCheck = parseResult.isUsageHelpRequested || parseResult.isVersionHelpRequested
// if (!skipCheck && !daemonHealthService.alive()) {
// throw ExecutionException(
// commandSpec.commandLine(),
// "The WG Tunnel service must be installed and started to execute this command. " +
// "Install and start it with 'wgtunnel service install -y' or, if already installed, " +
// "start the service with 'wgtunnel service start'."
// )
// }
return@runBlocking defaultStrategy.execute(parseResult)
}
}
+1
View File
@@ -0,0 +1 @@
/build
+56
View File
@@ -0,0 +1,56 @@
import dev.icerock.gradle.MRVisibility
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.ksp)
alias(libs.plugins.room)
alias(libs.plugins.serialization)
alias(libs.plugins.moko)
}
kotlin {
jvm()
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":parser"))
implementation(project(":keyring"))
implementation(project(":core"))
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
implementation(libs.kermit)
implementation(libs.logback.classic)
implementation(libs.kotlinx.serialization)
api(libs.moko.core)
api(libs.moko.compose)
// DI
implementation(libs.koin.core)
implementation(libs.bundles.ktor.client.jvm)
// Util
implementation(libs.apache.commons.lang3)
}
}
}
}
dependencies {
"kspJvm"(libs.androidx.room.compiler)
}
room { schemaDirectory("$projectDir/schemas") }
multiplatformResources {
resourcesPackage.set("com.zaneschepke.wireguardautotunnel")
resourcesClassName.set("SharedRes")
resourcesVisibility.set(MRVisibility.Public)
}
@@ -0,0 +1,341 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "d66aaaa9eeab5a2e84406838017246b1",
"entities": [
{
"tableName": "tunnel_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `quick_config` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `active` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "quickConfig",
"columnName": "quick_config",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `already_donated` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "theme",
"columnName": "theme",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'AUTOMATIC'"
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT"
},
{
"fieldPath": "alreadyDonated",
"columnName": "already_donated",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "dns_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isGlobalTunnelDnsEnabled",
"columnName": "global_tunnel_dns_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "startOnBoot",
"columnName": "start_on_boot",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd66aaaa9eeab5a2e84406838017246b1')"
]
}
}
@@ -0,0 +1,341 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "d66aaaa9eeab5a2e84406838017246b1",
"entities": [
{
"tableName": "tunnel_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `quick_config` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `active` INTEGER NOT NULL DEFAULT false, `ping_target` TEXT DEFAULT null, `is_ethernet_tunnel` INTEGER NOT NULL DEFAULT false, `is_ipv4_preferred` INTEGER NOT NULL DEFAULT true, `position` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "quickConfig",
"columnName": "quick_config",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tunnelNetworks",
"columnName": "tunnel_networks",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isPrimaryTunnel",
"columnName": "is_primary_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "pingTarget",
"columnName": "ping_target",
"affinity": "TEXT",
"defaultValue": "null"
},
{
"fieldPath": "isEthernetTunnel",
"columnName": "is_ethernet_tunnel",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "isIpv4Preferred",
"columnName": "is_ipv4_preferred",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_tunnel_config_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tunnel_config_name` ON `${TABLE_NAME}` (`name`)"
}
]
},
{
"tableName": "proxy_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `socks5_proxy_enabled` INTEGER NOT NULL DEFAULT 0, `socks5_proxy_bind_address` TEXT, `http_proxy_enable` INTEGER NOT NULL DEFAULT 0, `http_proxy_bind_address` TEXT, `proxy_username` TEXT, `proxy_password` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "socks5ProxyEnabled",
"columnName": "socks5_proxy_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "socks5ProxyBindAddress",
"columnName": "socks5_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "httpProxyEnabled",
"columnName": "http_proxy_enable",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "httpProxyBindAddress",
"columnName": "http_proxy_bind_address",
"affinity": "TEXT"
},
{
"fieldPath": "proxyUsername",
"columnName": "proxy_username",
"affinity": "TEXT"
},
{
"fieldPath": "proxyPassword",
"columnName": "proxy_password",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "lockdown_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bypass_lan` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypassLan",
"columnName": "bypass_lan",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "general_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT 0, `app_mode` INTEGER NOT NULL DEFAULT 0, `theme` TEXT NOT NULL DEFAULT 'AUTOMATIC', `locale` TEXT, `already_donated` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isRestoreOnBootEnabled",
"columnName": "is_restore_on_boot_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "appMode",
"columnName": "app_mode",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "theme",
"columnName": "theme",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'AUTOMATIC'"
},
{
"fieldPath": "locale",
"columnName": "locale",
"affinity": "TEXT"
},
{
"fieldPath": "alreadyDonated",
"columnName": "already_donated",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "dns_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dns_protocol` INTEGER NOT NULL DEFAULT 0, `dns_endpoint` TEXT, `global_tunnel_dns_enabled` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dnsProtocol",
"columnName": "dns_protocol",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "dnsEndpoint",
"columnName": "dns_endpoint",
"affinity": "TEXT"
},
{
"fieldPath": "isGlobalTunnelDnsEnabled",
"columnName": "global_tunnel_dns_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "auto_tunnel_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL DEFAULT 0, `trusted_network_ssids` TEXT NOT NULL DEFAULT '', `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT 0, `is_wildcards_enabled` INTEGER NOT NULL DEFAULT 0, `is_stop_on_no_internet_enabled` INTEGER NOT NULL DEFAULT 0, `is_tunnel_on_unsecure_enabled` INTEGER NOT NULL DEFAULT 0, `start_on_boot` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isAutoTunnelEnabled",
"columnName": "is_tunnel_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "trustedNetworkSSIDs",
"columnName": "trusted_network_ssids",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "isTunnelOnEthernetEnabled",
"columnName": "is_tunnel_on_ethernet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnWifiEnabled",
"columnName": "is_tunnel_on_wifi_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isWildcardsEnabled",
"columnName": "is_wildcards_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isStopOnNoInternetEnabled",
"columnName": "is_stop_on_no_internet_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "isTunnelOnUnsecureEnabled",
"columnName": "is_tunnel_on_unsecure_enabled",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "startOnBoot",
"columnName": "start_on_boot",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd66aaaa9eeab5a2e84406838017246b1')"
]
}
}
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.client.data
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import com.zaneschepke.wireguardautotunnel.client.data.model.EncryptedField
import com.zaneschepke.wireguardautotunnel.core.crypto.Crypto
import org.koin.java.KoinJavaComponent.inject
import javax.crypto.SecretKey
@ProvidedTypeConverter
class AppKeyringConverter {
private val secretKey: SecretKey by inject(SecretKey::class.java)
@TypeConverter
fun decryptQuick(encryptedQuick: String): EncryptedField {
return EncryptedField(Crypto.decryptWithMasterKey(encryptedQuick, secretKey))
}
@TypeConverter
fun encryptQuick(quick: EncryptedField): String {
return Crypto.encryptWithMasterKey(quick.value, secretKey)
}
}
@@ -0,0 +1,69 @@
package com.zaneschepke.wireguardautotunnel.client.data
import androidx.room.ConstructedBy
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.RoomDatabaseConstructor
import androidx.room.TypeConverters
import com.zaneschepke.wireguardautotunnel.client.data.dao.AutoTunnelSettingsDao
import com.zaneschepke.wireguardautotunnel.client.data.dao.DnsSettingsDao
import com.zaneschepke.wireguardautotunnel.client.data.dao.GeneralSettingsDao
import com.zaneschepke.wireguardautotunnel.client.data.dao.LockdownSettingsDao
import com.zaneschepke.wireguardautotunnel.client.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.client.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.client.data.entity.AutoTunnelSettings
import com.zaneschepke.wireguardautotunnel.client.data.entity.DnsSettings
import com.zaneschepke.wireguardautotunnel.client.data.entity.GeneralSettings
import com.zaneschepke.wireguardautotunnel.client.data.entity.LockdownSettings
import com.zaneschepke.wireguardautotunnel.client.data.entity.ProxySettings
import com.zaneschepke.wireguardautotunnel.client.data.entity.TunnelConfig
import com.zaneschepke.wireguardautotunnel.keyring.Keyring
import org.apache.commons.lang3.SystemUtils
import java.io.File
@Database(entities = [TunnelConfig::class, ProxySettings::class, LockdownSettings::class,
GeneralSettings::class, DnsSettings::class, AutoTunnelSettings::class], version = 1, exportSchema = true)
@TypeConverters(DatabaseConverters::class, AppKeyringConverter::class)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun tunnelConfigDao(): TunnelConfigDao
abstract fun proxySettingsDao(): ProxySettingsDao
abstract fun generalSettingsDao(): GeneralSettingsDao
abstract fun autoTunnelSettingsDao(): AutoTunnelSettingsDao
abstract fun lockdownSettingsDao(): LockdownSettingsDao
abstract fun dnsSettingsDao(): DnsSettingsDao
companion object {
const val DB_SECRET_KEY = "db_secret"
const val DB_KEYRING = "wg_tunnel"
const val DB_FILE_NAME = "wg_tunnel.db"
const val APP_NAME = "WGTunnel" // macos convention
fun getDatabaseDir() : File {
val home = System.getProperty("user.home")
return when {
SystemUtils.IS_OS_WINDOWS -> {
val appData = System.getenv("APPDATA") ?: "${System.getProperty("user.home")}\\AppData\\Roaming"
File("$appData\\$APP_NAME")
}
SystemUtils.IS_OS_MAC -> {
File("$home/Library/Application Support/$APP_NAME")
}
else -> {
val xdgDataHome = System.getenv("XDG_DATA_HOME") ?: "$home/.local/share"
File("$xdgDataHome/${APP_NAME.lowercase()}") // linux lowercase convention
}
}
}
}
}
@Suppress("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
override fun initialize(): AppDatabase
}
@@ -0,0 +1,15 @@
package com.zaneschepke.wireguardautotunnel.client.data
import androidx.room.RoomDatabase
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.execSQL
class DatabaseCallback(private val databaseProvider: Lazy<AppDatabase>) : RoomDatabase.Callback() {
override fun onCreate(connection: SQLiteConnection) {
super.onCreate(connection)
connection.execSQL("INSERT INTO proxy_settings DEFAULT VALUES")
connection.execSQL("INSERT INTO general_settings DEFAULT VALUES")
connection.execSQL("INSERT INTO auto_tunnel_settings DEFAULT VALUES")
connection.execSQL("INSERT INTO dns_settings DEFAULT VALUES")
}
}
@@ -0,0 +1,45 @@
package com.zaneschepke.wireguardautotunnel.client.data
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import com.zaneschepke.wireguardautotunnel.client.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.client.data.model.DnsProtocol
import kotlinx.serialization.json.Json
@ProvidedTypeConverter
class DatabaseConverters {
@TypeConverter
fun listToString(value: List<String>): String {
return Json.encodeToString(value)
}
@TypeConverter
fun stringToList(value: String): List<String> {
if (value.isBlank() || value.isEmpty()) return mutableListOf()
return try {
Json.decodeFromString<List<String>>(value)
} catch (e: Exception) {
val list = value.split(",").toMutableList()
val json = listToString(list)
Json.decodeFromString<List<String>>(json)
}
}
@TypeConverter
fun setToString(value: Set<String>): String {
return listToString(value.toList())
}
@TypeConverter
fun stringToSet(value: String): Set<String> {
return stringToList(value).toSet()
}
@TypeConverter fun toMode(value: Int): AppMode = AppMode.fromValue(value)
@TypeConverter fun fromMode(mode: AppMode): Int = mode.value
@TypeConverter fun toDnsProtocol(value: Int): DnsProtocol = DnsProtocol.fromValue(value)
@TypeConverter fun fromDnsProtocol(mode: DnsProtocol): Int = mode.value
}
@@ -0,0 +1,21 @@
package com.zaneschepke.wireguardautotunnel.client.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.client.data.entity.AutoTunnelSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface AutoTunnelSettingsDao {
@Query("SELECT * FROM auto_tunnel_settings LIMIT 1")
suspend fun getAutoTunnelSettings(): AutoTunnelSettings?
@Upsert suspend fun upsert(autoTunnelSettings: AutoTunnelSettings)
@Query("SELECT * FROM auto_tunnel_settings LIMIT 1")
fun getAutoTunnelSettingsFlow(): Flow<AutoTunnelSettings?>
@Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled")
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.client.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.client.data.entity.DnsSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface DnsSettingsDao {
@Query("SELECT * FROM dns_settings LIMIT 1") suspend fun getDnsSettings(): DnsSettings?
@Upsert suspend fun upsert(dnsSettings: DnsSettings)
@Query("SELECT * FROM dns_settings LIMIT 1") fun getDnsSettingsFlow(): Flow<DnsSettings?>
}
@@ -0,0 +1,28 @@
package com.zaneschepke.wireguardautotunnel.client.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.client.data.entity.GeneralSettings
import com.zaneschepke.wireguardautotunnel.client.data.model.AppMode
import kotlinx.coroutines.flow.Flow
@Dao
interface GeneralSettingsDao {
@Query("SELECT * FROM general_settings LIMIT 1")
suspend fun getGeneralSettings(): GeneralSettings?
@Upsert suspend fun upsert(generalSettings: GeneralSettings)
@Query("SELECT * FROM general_settings LIMIT 1")
fun getGeneralSettingsFlow(): Flow<GeneralSettings?>
@Query("UPDATE general_settings SET theme = :theme WHERE id = 1")
suspend fun updateTheme(theme: String)
@Query("UPDATE general_settings SET locale = :locale WHERE id = 1")
suspend fun updateLocale(locale: String)
@Query("UPDATE general_settings SET app_mode = :appMode WHERE id = 1")
suspend fun updateAppMode(appMode: AppMode)
}
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.client.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.client.data.entity.LockdownSettings
import kotlinx.coroutines.flow.Flow
@Dao
interface LockdownSettingsDao {
@Query("SELECT * FROM lockdown_settings LIMIT 1")
suspend fun getLockdownSettings(): LockdownSettings?
@Upsert suspend fun upsert(lockdownSettings: LockdownSettings)
@Query("SELECT * FROM lockdown_settings LIMIT 1")
fun getLockdownSettingsFlow(): Flow<LockdownSettings?>
}
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.client.data.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import com.zaneschepke.wireguardautotunnel.client.data.entity.ProxySettings
import kotlinx.coroutines.flow.Flow
@Dao
interface ProxySettingsDao {
@Upsert suspend fun upsert(proxySettings: ProxySettings)
@Query("SELECT * FROM proxy_settings LIMIT 1") suspend fun getProxySettings(): ProxySettings?
@Query("SELECT * FROM proxy_settings LIMIT 1") fun getProxySettingsFlow(): Flow<ProxySettings?>
}
@@ -0,0 +1,84 @@
package com.zaneschepke.wireguardautotunnel.client.data.dao
import androidx.room.*
import com.zaneschepke.wireguardautotunnel.client.data.entity.TunnelConfig
import kotlinx.coroutines.flow.Flow
@Dao
interface TunnelConfigDao {
@Upsert suspend fun upsert(t: TunnelConfig)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveAll(t: List<TunnelConfig>)
@Query("SELECT * FROM tunnel_config WHERE id=:id") suspend fun getById(id: Long): TunnelConfig?
@Query("UPDATE tunnel_config SET active = 0 WHERE active = 1")
suspend fun resetActiveTunnels()
@Query("SELECT * FROM tunnel_config WHERE name=:name")
suspend fun getByName(name: String): TunnelConfig?
@Query("SELECT * FROM tunnel_config WHERE active=1")
suspend fun getActive(): List<TunnelConfig>
@Query("SELECT * FROM tunnel_config") suspend fun getAll(): List<TunnelConfig>
@Delete suspend fun delete(t: TunnelConfig)
@Delete suspend fun delete(t: List<TunnelConfig>)
@Query("DELETE FROM tunnel_config WHERE name = :name")
suspend fun deleteByName(name: String)
@Query("SELECT COUNT('id') FROM tunnel_config") suspend fun count(): Long
@Query("SELECT * FROM tunnel_config WHERE tunnel_networks LIKE '%' || :name || '%'")
suspend fun findByTunnelNetworkName(name: String): List<TunnelConfig>
@Query("UPDATE tunnel_config SET is_primary_tunnel = 0 WHERE is_primary_tunnel =1")
suspend fun resetPrimaryTunnel()
@Query("UPDATE tunnel_config SET is_ethernet_tunnel = 0 WHERE is_ethernet_tunnel =1")
suspend fun resetEthernetTunnel()
@Query("SELECT * FROM tunnel_config WHERE is_primary_tunnel=1")
suspend fun findByPrimary(): List<TunnelConfig>
@Query(
"""
SELECT * FROM tunnel_config
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
ORDER BY
CASE WHEN is_primary_tunnel = 1 THEN 0 ELSE 1 END,
position ASC
LIMIT 1
"""
)
suspend fun getDefaultTunnel(): TunnelConfig?
@Query(
"""
SELECT * FROM tunnel_config
WHERE name != '${TunnelConfig.GLOBAL_CONFIG_NAME}'
ORDER BY
CASE WHEN active = 1 THEN 0
WHEN is_primary_tunnel = 1 THEN 1
ELSE 2 END,
position ASC
LIMIT 1
"""
)
suspend fun getStartTunnel(): TunnelConfig?
@Query("SELECT * FROM tunnel_config ORDER BY position")
fun getAllFlow(): Flow<List<TunnelConfig>>
@Query("SELECT * FROM tunnel_config WHERE name != :globalName ORDER BY position")
fun getAllTunnelsExceptGlobal(
globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME
): Flow<List<TunnelConfig>>
@Query("SELECT * FROM tunnel_config WHERE name = :globalName LIMIT 1")
fun getGlobalTunnel(globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME): Flow<TunnelConfig?>
}
@@ -0,0 +1,25 @@
package com.zaneschepke.wireguardautotunnel.client.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "auto_tunnel_settings")
data class AutoTunnelSettings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_tunnel_enabled", defaultValue = "0")
val isAutoTunnelEnabled: Boolean = false,
@ColumnInfo(name = "trusted_network_ssids", defaultValue = "")
val trustedNetworkSSIDs: Set<String> = emptySet(),
@ColumnInfo(name = "is_tunnel_on_ethernet_enabled", defaultValue = "0")
val isTunnelOnEthernetEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_wifi_enabled", defaultValue = "0")
val isTunnelOnWifiEnabled: Boolean = false,
@ColumnInfo(name = "is_wildcards_enabled", defaultValue = "0")
val isWildcardsEnabled: Boolean = false,
@ColumnInfo(name = "is_stop_on_no_internet_enabled", defaultValue = "0")
val isStopOnNoInternetEnabled: Boolean = false,
@ColumnInfo(name = "is_tunnel_on_unsecure_enabled", defaultValue = "0")
val isTunnelOnUnsecureEnabled: Boolean = false,
@ColumnInfo(name = "start_on_boot", defaultValue = "0") val startOnBoot: Boolean = false,
)
@@ -0,0 +1,16 @@
package com.zaneschepke.wireguardautotunnel.client.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.client.data.model.DnsProtocol
@Entity(tableName = "dns_settings")
data class DnsSettings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "dns_protocol", defaultValue = "0")
val dnsProtocol: DnsProtocol = DnsProtocol.fromValue(0),
@ColumnInfo(name = "dns_endpoint") val dnsEndpoint: String? = null,
@ColumnInfo(name = "global_tunnel_dns_enabled", defaultValue = "0")
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -0,0 +1,17 @@
package com.zaneschepke.wireguardautotunnel.client.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.zaneschepke.wireguardautotunnel.client.data.model.AppMode
@Entity(tableName = "general_settings")
data class GeneralSettings(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "is_restore_on_boot_enabled", defaultValue = "0")
val isRestoreOnBootEnabled: Boolean = false,
@ColumnInfo(name = "app_mode", defaultValue = "0") val appMode: AppMode = AppMode.fromValue(0),
@ColumnInfo(name = "theme", defaultValue = "AUTOMATIC") val theme: String = "AUTOMATIC",
@ColumnInfo(name = "locale") val locale: String? = null,
@ColumnInfo(name = "already_donated", defaultValue = "0") val alreadyDonated: Boolean = false,
)
@@ -0,0 +1,11 @@
package com.zaneschepke.wireguardautotunnel.client.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "lockdown_settings")
data class LockdownSettings(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "bypass_lan", defaultValue = "0") val bypassLan: Boolean = false
)
@@ -0,0 +1,18 @@
package com.zaneschepke.wireguardautotunnel.client.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "proxy_settings")
data class ProxySettings(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "socks5_proxy_enabled", defaultValue = "0")
val socks5ProxyEnabled: Boolean = false,
@ColumnInfo(name = "socks5_proxy_bind_address") val socks5ProxyBindAddress: String? = null,
@ColumnInfo(name = "http_proxy_enable", defaultValue = "0")
val httpProxyEnabled: Boolean = false,
@ColumnInfo(name = "http_proxy_bind_address") val httpProxyBindAddress: String? = null,
@ColumnInfo(name = "proxy_username") val proxyUsername: String? = null,
@ColumnInfo(name = "proxy_password") val proxyPassword: String? = null,
)
@@ -0,0 +1,32 @@
package com.zaneschepke.wireguardautotunnel.client.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.zaneschepke.wireguardautotunnel.client.data.AppKeyringConverter
import com.zaneschepke.wireguardautotunnel.client.data.model.EncryptedField
@Entity(tableName = "tunnel_config", indices = [Index(value = ["name"], unique = true)])
data class TunnelConfig(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "name") val name: String,
@field:TypeConverters(AppKeyringConverter::class)
@ColumnInfo(name = "quick_config") val quickConfig: EncryptedField,
@ColumnInfo(name = "tunnel_networks", defaultValue = "")
val tunnelNetworks: Set<String> = setOf(),
@ColumnInfo(name = "is_primary_tunnel", defaultValue = "false")
val isPrimaryTunnel: Boolean = false,
@ColumnInfo(name = "active", defaultValue = "false") val active: Boolean = false,
@ColumnInfo(name = "ping_target", defaultValue = "null") var pingTarget: String? = null,
@ColumnInfo(name = "is_ethernet_tunnel", defaultValue = "false")
val isEthernetTunnel: Boolean = false,
@ColumnInfo(name = "is_ipv4_preferred", defaultValue = "true")
val isIpv4Preferred: Boolean = true,
@ColumnInfo(name = "position", defaultValue = "0") val position: Int = 0,
) {
companion object {
const val GLOBAL_CONFIG_NAME = "4675ab06-903a-438b-8485-6ea4187a9512"
}
}
@@ -0,0 +1,30 @@
package com.zaneschepke.wireguardautotunnel.client.data.mapper
import com.zaneschepke.wireguardautotunnel.client.data.entity.AutoTunnelSettings as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.model.AutoTunnelSettings as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
isAutoTunnelEnabled = isAutoTunnelEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
isWildcardsEnabled = isWildcardsEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
startOnBoot = startOnBoot,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
isAutoTunnelEnabled = isAutoTunnelEnabled,
trustedNetworkSSIDs = trustedNetworkSSIDs,
isTunnelOnEthernetEnabled = isTunnelOnEthernetEnabled,
isTunnelOnWifiEnabled = isTunnelOnWifiEnabled,
isWildcardsEnabled = isWildcardsEnabled,
isStopOnNoInternetEnabled = isStopOnNoInternetEnabled,
isTunnelOnUnsecureEnabled = isTunnelOnUnsecureEnabled,
startOnBoot = startOnBoot,
)
@@ -0,0 +1,21 @@
package com.zaneschepke.wireguardautotunnel.client.data.mapper
import com.zaneschepke.wireguardautotunnel.client.data.model.DnsProtocol
import com.zaneschepke.wireguardautotunnel.client.data.entity.DnsSettings as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.model.DnsSettings as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
dnsProtocol = dnsProtocol.value,
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
dnsProtocol = DnsProtocol.fromValue(dnsProtocol),
dnsEndpoint = dnsEndpoint,
isGlobalTunnelDnsEnabled = isGlobalTunnelDnsEnabled,
)
@@ -0,0 +1,13 @@
package com.zaneschepke.wireguardautotunnel.client.data.mapper
import com.zaneschepke.wireguardautotunnel.client.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.model.LockdownSettings as Domain
fun Entity.toDomain(): Domain =
Domain(id = id, bypassLan = bypassLan)
fun Domain.toEntity(): Entity =
Entity(
id = id,
bypassLan = bypassLan
)
@@ -0,0 +1,26 @@
package com.zaneschepke.wireguardautotunnel.client.data.mapper
import com.zaneschepke.wireguardautotunnel.client.data.entity.ProxySettings as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.model.ProxySettings as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
socks5ProxyEnabled = socks5ProxyEnabled,
socks5ProxyBindAddress = socks5ProxyBindAddress,
httpProxyEnabled = httpProxyEnabled,
httpProxyBindAddress = httpProxyBindAddress,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
socks5ProxyEnabled = socks5ProxyEnabled,
socks5ProxyBindAddress = socks5ProxyBindAddress,
httpProxyEnabled = httpProxyEnabled,
httpProxyBindAddress = httpProxyBindAddress,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword,
)
@@ -0,0 +1,25 @@
package com.zaneschepke.wireguardautotunnel.client.data.mapper
import com.zaneschepke.wireguardautotunnel.client.data.model.Theme
import com.zaneschepke.wireguardautotunnel.client.data.entity.GeneralSettings as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.model.GeneralSettings as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
appMode = appMode,
theme = Theme.valueOf(theme.uppercase()),
locale = locale,
alreadyDonated = alreadyDonated,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
isRestoreOnBootEnabled = isRestoreOnBootEnabled,
appMode = appMode,
theme = theme.name,
locale = locale,
alreadyDonated = alreadyDonated,
)
@@ -0,0 +1,33 @@
package com.zaneschepke.wireguardautotunnel.client.data.mapper
import com.zaneschepke.wireguardautotunnel.client.data.model.EncryptedField
import com.zaneschepke.wireguardautotunnel.client.data.entity.TunnelConfig as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig as Domain
fun Entity.toDomain(): Domain =
Domain(
id = id,
name = name,
quickConfig = quickConfig.value,
tunnelNetworks = tunnelNetworks,
isPrimaryTunnel = isPrimaryTunnel,
active = active,
pingTarget = pingTarget,
isEthernetTunnel = isEthernetTunnel,
isIpv4Preferred = isIpv4Preferred,
position = position,
)
fun Domain.toEntity(): Entity =
Entity(
id = id,
name = name,
quickConfig = EncryptedField(quickConfig),
tunnelNetworks = tunnelNetworks,
isPrimaryTunnel = isPrimaryTunnel,
active = active,
pingTarget = pingTarget,
isEthernetTunnel = isEthernetTunnel,
isIpv4Preferred = isIpv4Preferred,
position = position,
)
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.client.data.model
enum class AppMode(val value: Int) {
VPN(0),
PROXY(1),
LOCK_DOWN(2),
KERNEL(3);
companion object {
fun fromValue(value: Int): com.zaneschepke.wireguardautotunnel.client.data.model.AppMode = entries.find { it.value == value } ?: VPN
}
}
@@ -0,0 +1,30 @@
package com.zaneschepke.wireguardautotunnel.client.data.model
enum class DnsProtocol(val value: Int) {
SYSTEM(0),
DOH(1);
companion object {
fun fromValue(value: Int): com.zaneschepke.wireguardautotunnel.client.data.model.DnsProtocol =
_root_ide_package_.com.zaneschepke.wireguardautotunnel.client.data.model.DnsProtocol.entries.find { it.value == value } ?: SYSTEM
}
}
enum class DnsProvider(private val systemAddress: String, private val dohAddress: String) {
CLOUDFLARE("1.1.1.1", "https://1.1.1.1/dns-query"),
ADGUARD("94.140.14.14", "https://94.140.14.14/dns-query");
fun asAddress(protocol: com.zaneschepke.wireguardautotunnel.client.data.model.DnsProtocol): String {
return when (protocol) {
_root_ide_package_.com.zaneschepke.wireguardautotunnel.client.data.model.DnsProtocol.SYSTEM -> systemAddress
_root_ide_package_.com.zaneschepke.wireguardautotunnel.client.data.model.DnsProtocol.DOH -> dohAddress
}
}
companion object {
fun fromAddress(address: String): com.zaneschepke.wireguardautotunnel.client.data.model.DnsProvider {
return entries.find { it.systemAddress == address || it.dohAddress == address }
?: CLOUDFLARE
}
}
}
@@ -0,0 +1,4 @@
package com.zaneschepke.wireguardautotunnel.client.data.model
@JvmInline
value class EncryptedField(val value: String)
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.client.data.model
enum class Theme {
AUTOMATIC,
LIGHT,
DARK,
DARKER,
AMOLED,
DYNAMIC,
}
@@ -0,0 +1,29 @@
package com.zaneschepke.wireguardautotunnel.client.data.repository
import com.zaneschepke.wireguardautotunnel.client.data.dao.AutoTunnelSettingsDao
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.client.domain.repository.AutoTunnelSettingsRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import com.zaneschepke.wireguardautotunnel.client.data.entity.AutoTunnelSettings as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.model.AutoTunnelSettings as Domain
class RoomAutoTunnelSettingsRepository(private val autoTunnelSettingsDao: AutoTunnelSettingsDao) :
AutoTunnelSettingsRepository {
override suspend fun upsert(autoTunnelSettings: Domain) {
autoTunnelSettingsDao.upsert(autoTunnelSettings.toEntity())
}
override val flow: Flow<Domain>
get() =
autoTunnelSettingsDao.getAutoTunnelSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getAutoTunnelSettings(): Domain {
return (autoTunnelSettingsDao.getAutoTunnelSettings() ?: Entity()).toDomain()
}
override suspend fun updateAutoTunnelEnabled(enabled: Boolean) {
autoTunnelSettingsDao.updateAutoTunnelEnabled(enabled)
}
}
@@ -0,0 +1,24 @@
package com.zaneschepke.wireguardautotunnel.client.data.repository
import com.zaneschepke.wireguardautotunnel.client.data.dao.DnsSettingsDao
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.client.domain.repository.DnsSettingsRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import com.zaneschepke.wireguardautotunnel.client.data.entity.DnsSettings as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.model.DnsSettings as Domain
class RoomDnsSettingsRepository(private val dnsSettingsDao: DnsSettingsDao) :
DnsSettingsRepository {
override suspend fun upsert(dnsSettings: Domain) {
dnsSettingsDao.upsert(dnsSettings.toEntity())
}
override val flow: Flow<Domain>
get() = dnsSettingsDao.getDnsSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getDnsSettings(): Domain {
return (dnsSettingsDao.getDnsSettings() ?: Entity()).toDomain()
}
}
@@ -0,0 +1,23 @@
package com.zaneschepke.wireguardautotunnel.client.data.repository
import com.zaneschepke.wireguardautotunnel.client.data.dao.LockdownSettingsDao
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.client.domain.repository.LockdownSettingsRepository
import kotlinx.coroutines.flow.map
import com.zaneschepke.wireguardautotunnel.client.data.entity.LockdownSettings as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.model.LockdownSettings as Domain
class RoomLockdownSettingsRepository(private val lockdownSettingsDao: LockdownSettingsDao) :
LockdownSettingsRepository {
override suspend fun upsert(lockdownSettings: Domain) {
lockdownSettingsDao.upsert(lockdownSettings.toEntity())
}
override val flow =
lockdownSettingsDao.getLockdownSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getLockdownSettings(): Domain {
return (lockdownSettingsDao.getLockdownSettings() ?: Entity()).toDomain()
}
}
@@ -0,0 +1,23 @@
package com.zaneschepke.wireguardautotunnel.client.data.repository
import com.zaneschepke.wireguardautotunnel.client.data.dao.ProxySettingsDao
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.client.domain.repository.ProxySettingsRepository
import kotlinx.coroutines.flow.map
import com.zaneschepke.wireguardautotunnel.client.data.entity.ProxySettings as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.model.ProxySettings as Domain
class RoomProxySettingsRepository(private val proxySettingsDao: ProxySettingsDao) :
ProxySettingsRepository {
override suspend fun upsert(proxySettings: Domain) {
proxySettingsDao.upsert(proxySettings.toEntity())
}
override val flow = proxySettingsDao.getProxySettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getProxySettings(): Domain {
return (proxySettingsDao.getProxySettings() ?: Entity()).toDomain()
}
}
@@ -0,0 +1,37 @@
package com.zaneschepke.wireguardautotunnel.client.data.repository
import com.zaneschepke.wireguardautotunnel.client.data.dao.GeneralSettingsDao
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.client.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.client.data.model.Theme
import com.zaneschepke.wireguardautotunnel.client.domain.model.GeneralSettings as Domain
import com.zaneschepke.wireguardautotunnel.client.data.entity.GeneralSettings as Entity
import com.zaneschepke.wireguardautotunnel.client.domain.repository.GeneralSettingRepository
import kotlinx.coroutines.flow.map
class RoomSettingsRepository(private val settingsDao: GeneralSettingsDao) :
GeneralSettingRepository {
override suspend fun upsert(generalSettings: Domain) {
settingsDao.upsert(generalSettings.toEntity())
}
override val flow = settingsDao.getGeneralSettingsFlow().map { (it ?: Entity()).toDomain() }
override suspend fun getGeneralSettings(): Domain {
return (settingsDao.getGeneralSettings() ?: Entity()).toDomain()
}
override suspend fun updateTheme(theme: Theme) {
settingsDao.updateTheme(theme.name)
}
override suspend fun updateLocale(locale: String) {
settingsDao.updateLocale(locale)
}
override suspend fun updateAppMode(appMode: AppMode) {
settingsDao.updateAppMode(appMode)
}
}
@@ -0,0 +1,97 @@
package com.zaneschepke.wireguardautotunnel.client.data.repository
import com.zaneschepke.wireguardautotunnel.client.data.dao.TunnelConfigDao
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toDomain
import com.zaneschepke.wireguardautotunnel.client.data.mapper.toEntity
import com.zaneschepke.wireguardautotunnel.client.domain.repository.TunnelRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig as Domain
class RoomTunnelRepository(private val tunnelConfigDao: TunnelConfigDao) : TunnelRepository {
override val flow =
tunnelConfigDao.getAllFlow().map { it.map { tunnelConfig -> tunnelConfig.toDomain() } }
override val userTunnelsFlow =
tunnelConfigDao.getAllTunnelsExceptGlobal().map {
it.map { tunnelConfig -> tunnelConfig.toDomain() }
}
override val globalTunnelFlow: Flow<Domain?> =
tunnelConfigDao.getGlobalTunnel().map { it?.toDomain() }
override suspend fun getAll(): List<Domain> {
return tunnelConfigDao.getAll().map { it.toDomain() }
}
override suspend fun save(tunnelConfig: Domain) {
tunnelConfigDao.upsert(tunnelConfig.toEntity())
}
override suspend fun saveAll(tunnelConfigList: List<Domain>) {
tunnelConfigDao.saveAll(tunnelConfigList.map { tunnelConfig -> tunnelConfig.toEntity() })
}
override suspend fun updatePrimaryTunnel(tunnelConfig: Domain?) {
tunnelConfigDao.resetPrimaryTunnel()
tunnelConfig?.let { save(it.copy(isPrimaryTunnel = true)) }
}
override suspend fun resetActiveTunnels() {
tunnelConfigDao.resetActiveTunnels()
}
override suspend fun updateEthernetTunnel(tunnelConfig: Domain?) {
tunnelConfigDao.resetEthernetTunnel()
tunnelConfig?.let { save(it.copy(isEthernetTunnel = true)) }
}
override suspend fun delete(tunnelConfig: Domain) {
tunnelConfigDao.delete(tunnelConfig.toEntity())
}
override suspend fun deleteByName(name: String) {
tunnelConfigDao.deleteByName(name)
}
override suspend fun getById(id: Int): Domain? {
return tunnelConfigDao.getById(id.toLong())?.toDomain()
}
override suspend fun getActive(): List<Domain> {
return tunnelConfigDao.getActive().map { it.toDomain() }
}
override suspend fun getDefaultTunnel(): Domain? {
return tunnelConfigDao.getDefaultTunnel()?.toDomain()
}
override suspend fun getStartTunnel(): Domain? {
return tunnelConfigDao.getStartTunnel()?.toDomain()
}
override suspend fun getTunnelByName(name: String): Domain? {
return tunnelConfigDao.getByName(name)?.toDomain()
}
override suspend fun count(): Int {
return tunnelConfigDao.count().toInt()
}
override suspend fun findByTunnelName(name: String): Domain? {
return tunnelConfigDao.getByName(name)?.toDomain()
}
override suspend fun findByTunnelNetworksName(name: String): List<Domain> {
return tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() }
}
override suspend fun findPrimary(): List<Domain> {
return tunnelConfigDao.findByPrimary().map { it.toDomain() }
}
override suspend fun delete(tunnels: List<Domain>) {
tunnelConfigDao.delete(tunnels.map { it.toEntity() })
}
}
@@ -0,0 +1,25 @@
package com.zaneschepke.wireguardautotunnel.client.data.service
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.client.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.client.domain.repository.extensions.saveTunnelsUniquely
import com.zaneschepke.wireguardautotunnel.client.service.QuickConfigMap
import com.zaneschepke.wireguardautotunnel.client.service.QuickString
import com.zaneschepke.wireguardautotunnel.client.service.TunnelImportService
import com.zaneschepke.wireguardautotunnel.client.service.TunnelName
class DefaultTunnelImportService(
private val tunnelRepository: TunnelRepository,
) : TunnelImportService {
override suspend fun import(config: QuickString, name: TunnelName?) {
import(mapOf(config to name))
}
override suspend fun import(configs: QuickConfigMap) {
val tunnelConfigs =
configs.map { (config, name) -> TunnelConfig.fromQuickString(config, name) }
val existingNames = tunnelRepository.getAll().map { it.name }
tunnelRepository.saveTunnelsUniquely(tunnelConfigs, existingNames)
}
}
@@ -0,0 +1,19 @@
package com.zaneschepke.wireguardautotunnel.client.data.service
import com.zaneschepke.wireguardautotunnel.client.service.DaemonHealthService
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.http.*
class UdsDaemonHealthService(
private val client : HttpClient
) : DaemonHealthService {
override suspend fun alive(): Boolean {
return try {
client.get("/status") {
}.status.isSuccess()
} catch (_ : Exception) {
false
}
}
}
@@ -0,0 +1,107 @@
package com.zaneschepke.wireguardautotunnel.client.data.service
import com.zaneschepke.wireguardautotunnel.client.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.client.service.TunnelCommandService
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.BackendMode
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.BackendStatus
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.StartTunnelRequest
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.websocket.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.serialization.json.Json
import okio.IOException
class UdsTunnelCommandService(
private val client: HttpClient,
private val tunnelRepository: TunnelRepository
) : TunnelCommandService {
private val json = Json { ignoreUnknownKeys = true }
override suspend fun startTunnel(id: Int): Result<Unit> = runCatching {
val tunnelConfig = tunnelRepository.getById(id)
?: throw IOException("Tunnel $id not found")
val request = StartTunnelRequest(
id = id,
name = tunnelConfig.name,
quickConfig = tunnelConfig.quickConfig
)
val response = client.post("/tunnel/start") {
setBody(json.encodeToString(request))
contentType(ContentType.Application.Json)
}
if (!response.status.isSuccess()) {
throw IOException("Failed to start tunnel $id: ${response.status.value} - ${response.bodyAsText()}")
}
}
override suspend fun stopTunnel(id: Int): Result<Unit> = runCatching {
val response = client.post("/tunnel/stop/$id")
if (!response.status.isSuccess()) {
throw IOException("Failed to stop tunnel $id: ${response.status.value} - ${response.bodyAsText()}")
}
}
override suspend fun setMode(mode: BackendMode): Result<Unit> = runCatching {
val response = client.post("/tunnel/mode") {
setBody(json.encodeToString(mode))
contentType(ContentType.Text.Plain)
}
if (!response.status.isSuccess()) {
throw IOException("Failed to set mode: ${response.bodyAsText()}")
}
}
override suspend fun setKillSwitch(enabled: Boolean): Result<Unit> = runCatching {
val response = client.post("/tunnel/kill-switch") {
setBody(enabled.toString())
contentType(ContentType.Text.Plain)
}
if (!response.status.isSuccess()) {
throw IOException("Failed to set kill switch: ${response.bodyAsText()}")
}
}
override suspend fun getStatus(): Result<BackendStatus> = runCatching {
val response = client.get("/tunnel/status")
if (!response.status.isSuccess()) {
throw IOException("Failed to get status: ${response.status.value} - ${response.bodyAsText()}")
}
response.body<BackendStatus>()
}
override fun statusFlow(): Flow<BackendStatus> = callbackFlow {
val session = client.webSocketSession("/tunnel/status/stream")
try {
for (frame in session.incoming) {
if (frame is Frame.Text) {
val dto = json.decodeFromString<BackendStatus>(frame.readText())
trySend(dto)
}
}
} catch (e: Exception) {
close(e)
} finally {
session.close()
awaitClose()
}
}.flowOn(Dispatchers.IO)
}
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.client.di
enum class Secret {
IPC
}
@@ -0,0 +1,59 @@
package com.zaneschepke.wireguardautotunnel.client.di
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import com.zaneschepke.wireguardautotunnel.client.data.AppDatabase
import com.zaneschepke.wireguardautotunnel.client.data.AppKeyringConverter
import com.zaneschepke.wireguardautotunnel.client.data.DatabaseCallback
import com.zaneschepke.wireguardautotunnel.client.data.DatabaseConverters
import com.zaneschepke.wireguardautotunnel.client.data.dao.*
import com.zaneschepke.wireguardautotunnel.client.data.repository.*
import com.zaneschepke.wireguardautotunnel.client.domain.repository.*
import com.zaneschepke.wireguardautotunnel.core.crypto.Crypto
import com.zaneschepke.wireguardautotunnel.keyring.Keyring
import kotlinx.coroutines.Dispatchers
import org.koin.dsl.module
import java.io.File
import javax.crypto.SecretKey
val databaseModule = module {
single<RoomDatabase.Callback> { DatabaseCallback(lazy { get<AppDatabase>() }) }
single<SecretKey> {
val dbKey = AppDatabase.DB_SECRET_KEY
val keyring = Keyring(AppDatabase.DB_KEYRING)
val encodedSecret = keyring.get(dbKey) ?: run {
val secret = Crypto.generateRandomBase64EncodedAesKey()
keyring.put(dbKey, secret)
secret
}
Crypto.decodeKey(encodedSecret)
}
single<AppDatabase> {
val dbFileName = AppDatabase.DB_FILE_NAME
val dbDir = AppDatabase.getDatabaseDir()
dbDir.mkdirs()
val dbFile = File(dbDir, dbFileName)
Room.databaseBuilder<AppDatabase>(dbFile.absolutePath)
.setDriver(BundledSQLiteDriver())
.fallbackToDestructiveMigration(true)
.addCallback(get())
.addTypeConverter(DatabaseConverters())
.addTypeConverter(AppKeyringConverter())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
single<TunnelConfigDao> { get<AppDatabase>().tunnelConfigDao() }
single<AutoTunnelSettingsDao> { get<AppDatabase>().autoTunnelSettingsDao() }
single<DnsSettingsDao> { get<AppDatabase>().dnsSettingsDao() }
single<LockdownSettingsDao> { get<AppDatabase>().lockdownSettingsDao() }
single<ProxySettingsDao> { get<AppDatabase>().proxySettingsDao() }
single<GeneralSettingsDao> { get<AppDatabase>().generalSettingsDao() }
single<TunnelRepository>() { RoomTunnelRepository(get()) }
single<AutoTunnelSettingsRepository>() { RoomAutoTunnelSettingsRepository(get()) }
single<DnsSettingsRepository>() { RoomDnsSettingsRepository(get()) }
single<LockdownSettingsRepository>() { RoomLockdownSettingsRepository(get()) }
single<ProxySettingsRepository>() { RoomProxySettingsRepository(get()) }
}
@@ -0,0 +1,73 @@
package com.zaneschepke.wireguardautotunnel.client.di
import com.zaneschepke.wireguardautotunnel.client.data.service.DefaultTunnelImportService
import com.zaneschepke.wireguardautotunnel.client.data.service.UdsDaemonHealthService
import com.zaneschepke.wireguardautotunnel.client.data.service.UdsTunnelCommandService
import com.zaneschepke.wireguardautotunnel.client.service.DaemonHealthService
import com.zaneschepke.wireguardautotunnel.client.service.TunnelCommandService
import com.zaneschepke.wireguardautotunnel.client.service.TunnelImportService
import com.zaneschepke.wireguardautotunnel.core.crypto.HmacProtector
import com.zaneschepke.wireguardautotunnel.core.ipc.IPC
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.SecureCommand
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import org.koin.dsl.module
val serviceModule = module {
single {
// so daemon knows where to look for secret
val user = System.getProperty("user.name")
HttpClient(CIO) {
defaultRequest {
unixSocket(IPC.getDaemonSocketPath())
}
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
encodeDefaults = true
})
}
install(WebSockets)
install("HmacSigner") {
requestPipeline.intercept(HttpRequestPipeline.Before) {
if (subject is SecureCommand) {
return@intercept
}
val payload = when (val body = subject) {
is String -> body
is TextContent -> body.text
else -> ""
}
val timestamp = System.currentTimeMillis() / 1000
val signature = HmacProtector.generateSignature(
IPC.getIPCSecret(),
timestamp,
payload
)
val secureCommand = SecureCommand(timestamp, signature, user, payload)
context.contentType(ContentType.Application.Json)
context.setBody(secureCommand)
proceedWith(secureCommand)
}
}
}
}
single<DaemonHealthService> { UdsDaemonHealthService(get()) }
single<TunnelCommandService> { UdsTunnelCommandService(get(), tunnelRepository = get()) }
single<TunnelImportService> {
DefaultTunnelImportService(get())
}
}
@@ -0,0 +1,19 @@
package com.zaneschepke.wireguardautotunnel.client.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class AutoTunnelSettings(
val id: Int = 0,
val isAutoTunnelEnabled: Boolean = false,
val isTunnelOnMobileDataEnabled: Boolean = false,
val trustedNetworkSSIDs: Set<String> = emptySet(),
val isTunnelOnEthernetEnabled: Boolean = false,
val isTunnelOnWifiEnabled: Boolean = false,
val isWildcardsEnabled: Boolean = false,
val isStopOnNoInternetEnabled: Boolean = false,
val debounceDelaySeconds: Int = 3,
val isTunnelOnUnsecureEnabled: Boolean = false,
val wifiDetectionMethod: Int = 0,
val startOnBoot: Boolean = false,
)
@@ -0,0 +1,11 @@
package com.zaneschepke.wireguardautotunnel.client.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class DnsSettings(
val id: Int = 0,
val dnsProtocol: Int = 0,
val dnsEndpoint: String? = null,
val isGlobalTunnelDnsEnabled: Boolean = false,
)
@@ -0,0 +1,15 @@
package com.zaneschepke.wireguardautotunnel.client.domain.model
import com.zaneschepke.wireguardautotunnel.client.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.client.data.model.Theme
import kotlinx.serialization.Serializable
@Serializable
data class GeneralSettings(
val id: Int = 0,
val isRestoreOnBootEnabled: Boolean = false,
val appMode: AppMode = AppMode.fromValue(0),
val theme: Theme = Theme.AUTOMATIC,
val locale: String? = null,
val alreadyDonated: Boolean = false,
)
@@ -0,0 +1,11 @@
package com.zaneschepke.wireguardautotunnel.client.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class LockdownSettings(
val id: Long = 0L,
val bypassLan: Boolean = false,
val metered: Boolean = false,
val dualStack: Boolean = false,
)
@@ -0,0 +1,19 @@
package com.zaneschepke.wireguardautotunnel.client.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class ProxySettings(
val id: Long = 0,
val socks5ProxyEnabled: Boolean = false,
val socks5ProxyBindAddress: String? = null,
val httpProxyEnabled: Boolean = false,
val httpProxyBindAddress: String? = null,
val proxyUsername: String? = null,
val proxyPassword: String? = null,
) {
companion object {
const val DEFAULT_SOCKS_BIND_ADDRESS = "127.0.0.1:25344"
const val DEFAULT_HTTP_BIND_ADDRESS = "127.0.0.1:25345"
}
}
@@ -0,0 +1,109 @@
package com.zaneschepke.wireguardautotunnel.client.domain.model
import com.zaneschepke.wireguardautotunnel.parser.Config
import kotlinx.serialization.Serializable
import kotlin.collections.get
@Serializable
data class TunnelConfig(
val id: Int = 0,
val name: String,
val quickConfig: String,
val tunnelNetworks: Set<String> = setOf(),
val isPrimaryTunnel: Boolean = false,
val active: Boolean = false,
val pingTarget: String? = null,
val isEthernetTunnel: Boolean = false,
val isIpv4Preferred: Boolean = true,
val position: Int = 0,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TunnelConfig) return false
return id == other.id &&
name == other.name &&
quickConfig == other.quickConfig &&
isPrimaryTunnel == other.isPrimaryTunnel &&
isEthernetTunnel == other.isEthernetTunnel &&
pingTarget == other.pingTarget &&
tunnelNetworks == other.tunnelNetworks &&
isIpv4Preferred == other.isIpv4Preferred
}
override fun hashCode(): Int {
var result = id
result = 31 * result + name.hashCode()
result = 31 * result + quickConfig.hashCode()
return result
}
fun asConfig(): Config {
return Config.parseQuickString(quickConfig)
}
companion object {
fun generateRandom8Digits(): String {
val digits = ('0'..'9').toList()
return (1..8).map { digits.random() }.joinToString("")
}
private fun generateDefaultTunnelName(config: Config? = null): String {
return config?.peers[0]?.host ?: generateRandom8Digits()
}
fun configFromQuick(quick: String): Config {
return Config.parseQuickString(quick)
}
fun fromQuickString(quick: String, name: String? = null): TunnelConfig {
val config = configFromQuick(quick)
return tunnelConfFromConfig(config, name)
}
private fun tunnelConfFromConfig(config: Config, name: String? = null): TunnelConfig {
return TunnelConfig(
name = name ?: generateDefaultTunnelName(config),
quickConfig = config.asQuickString(),
)
}
private const val IPV6_ALL_NETWORKS = "::/0"
private const val IPV4_ALL_NETWORKS = "0.0.0.0/0"
val ALL_IPS = listOf(IPV4_ALL_NETWORKS, IPV6_ALL_NETWORKS)
val IPV4_PUBLIC_NETWORKS =
setOf(
"0.0.0.0/5",
"8.0.0.0/7",
"11.0.0.0/8",
"12.0.0.0/6",
"16.0.0.0/4",
"32.0.0.0/3",
"64.0.0.0/2",
"128.0.0.0/3",
"160.0.0.0/5",
"168.0.0.0/6",
"172.0.0.0/12",
"172.32.0.0/11",
"172.64.0.0/10",
"172.128.0.0/9",
"173.0.0.0/8",
"174.0.0.0/7",
"176.0.0.0/4",
"192.0.0.0/9",
"192.128.0.0/11",
"192.160.0.0/13",
"192.169.0.0/16",
"192.170.0.0/15",
"192.172.0.0/14",
"192.176.0.0/12",
"192.192.0.0/10",
"193.0.0.0/8",
"194.0.0.0/7",
"196.0.0.0/6",
"200.0.0.0/5",
"208.0.0.0/4",
)
val LAN_BYPASS_ALLOWED_IPS = setOf(IPV6_ALL_NETWORKS) + IPV4_PUBLIC_NETWORKS
}
}
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.client.domain.repository
import com.zaneschepke.wireguardautotunnel.client.domain.model.AutoTunnelSettings
import kotlinx.coroutines.flow.Flow
interface AutoTunnelSettingsRepository {
suspend fun upsert(autoTunnelSettings: AutoTunnelSettings)
val flow: Flow<AutoTunnelSettings>
suspend fun getAutoTunnelSettings(): AutoTunnelSettings
suspend fun updateAutoTunnelEnabled(enabled: Boolean)
}
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.client.domain.repository
import com.zaneschepke.wireguardautotunnel.client.domain.model.DnsSettings
import kotlinx.coroutines.flow.Flow
interface DnsSettingsRepository {
suspend fun upsert(dnsSettings: DnsSettings)
val flow: Flow<DnsSettings>
suspend fun getDnsSettings(): DnsSettings
}
@@ -0,0 +1,20 @@
package com.zaneschepke.wireguardautotunnel.client.domain.repository
import com.zaneschepke.wireguardautotunnel.client.data.model.AppMode
import com.zaneschepke.wireguardautotunnel.client.data.model.Theme
import com.zaneschepke.wireguardautotunnel.client.domain.model.GeneralSettings
import kotlinx.coroutines.flow.Flow
interface GeneralSettingRepository {
suspend fun upsert(generalSettings: GeneralSettings)
val flow: Flow<GeneralSettings>
suspend fun getGeneralSettings(): GeneralSettings
suspend fun updateTheme(theme: Theme)
suspend fun updateLocale(locale: String)
suspend fun updateAppMode(appMode: AppMode)
}
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.client.domain.repository
import com.zaneschepke.wireguardautotunnel.client.domain.model.LockdownSettings
import kotlinx.coroutines.flow.Flow
interface LockdownSettingsRepository {
suspend fun upsert(lockdownSettings: LockdownSettings)
val flow: Flow<LockdownSettings>
suspend fun getLockdownSettings(): LockdownSettings
}
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.client.domain.repository
import com.zaneschepke.wireguardautotunnel.client.domain.model.ProxySettings
import kotlinx.coroutines.flow.Flow
interface ProxySettingsRepository {
suspend fun upsert(proxySettings: ProxySettings)
val flow: Flow<ProxySettings>
suspend fun getProxySettings(): ProxySettings
}
@@ -0,0 +1,48 @@
package com.zaneschepke.wireguardautotunnel.client.domain.repository
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig
import kotlinx.coroutines.flow.Flow
interface TunnelRepository {
val flow: Flow<List<TunnelConfig>>
val userTunnelsFlow: Flow<List<TunnelConfig>>
val globalTunnelFlow: Flow<TunnelConfig?>
suspend fun getAll(): List<TunnelConfig>
suspend fun save(tunnelConfig: TunnelConfig)
suspend fun saveAll(tunnelConfigList: List<TunnelConfig>)
suspend fun updatePrimaryTunnel(tunnelConfig: TunnelConfig?)
suspend fun resetActiveTunnels()
suspend fun updateEthernetTunnel(tunnelConfig: TunnelConfig?)
suspend fun delete(tunnelConfig: TunnelConfig)
suspend fun deleteByName(name: String)
suspend fun getById(id: Int): TunnelConfig?
suspend fun getActive(): List<TunnelConfig>
suspend fun getDefaultTunnel(): TunnelConfig?
suspend fun getStartTunnel(): TunnelConfig?
suspend fun getTunnelByName(name: String): TunnelConfig?
suspend fun count(): Int
suspend fun findByTunnelName(name: String): TunnelConfig?
suspend fun findByTunnelNetworksName(name: String): List<TunnelConfig>
suspend fun findPrimary(): List<TunnelConfig>
suspend fun delete(tunnels: List<TunnelConfig>)
}
@@ -0,0 +1,47 @@
package com.zaneschepke.wireguardautotunnel.client.domain.repository.extensions
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.client.domain.repository.TunnelRepository
suspend fun TunnelRepository.saveTunnelsUniquely(
tunnels: List<TunnelConfig>,
existingNames: List<String>,
) {
val uniqueTunnels =
generateUniquelyNamedConfigs(
tunnels,
existingNames
)
saveAll(uniqueTunnels)
}
private fun generateUniquelyNamedConfigs(
incoming: List<TunnelConfig>,
existingNames: List<String>,
): List<TunnelConfig> {
val usedNames = existingNames.toMutableSet()
val result = mutableListOf<TunnelConfig>()
val regex = Regex("(.+)\\s*\\((\\d+)\\)$")
for (tun in incoming) {
var baseName = tun.name
var uniqueName = tun.name
var counter = 1
val matchResult = regex.find(baseName)
if (matchResult != null) {
baseName = matchResult.groupValues[1].trimEnd()
counter = matchResult.groupValues[2].toIntOrNull()?.plus(1) ?: 1
uniqueName = "$baseName ($counter)"
}
while (uniqueName in usedNames) {
uniqueName = "$baseName ($counter)"
counter++
}
usedNames.add(uniqueName)
result.add(tun.copy(name = uniqueName))
}
return result
}
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.client.service
interface DaemonHealthService {
suspend fun alive(): Boolean
}
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.client.service
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.BackendMode
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.BackendStatus
import kotlinx.coroutines.flow.Flow
interface TunnelCommandService {
suspend fun startTunnel(id: Int): Result<Unit>
suspend fun stopTunnel(id: Int): Result<Unit>
suspend fun setMode(mode: BackendMode): Result<Unit>
suspend fun setKillSwitch(enabled: Boolean): Result<Unit>
suspend fun getStatus(): Result<BackendStatus>
fun statusFlow(): Flow<BackendStatus>
}
@@ -0,0 +1,11 @@
package com.zaneschepke.wireguardautotunnel.client.service
typealias QuickString = String
typealias TunnelName = String
typealias QuickConfigMap = Map<QuickString, TunnelName?>
interface TunnelImportService {
suspend fun import(config: QuickString, name: TunnelName? = null)
suspend fun import(configs: QuickConfigMap)
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WG Tunnel</string>
</resources>
+63
View File
@@ -0,0 +1,63 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.composeHotReload)
alias(libs.plugins.conveyor)
}
group = "com.zaneschepke.wireguardautotunnel"
version = libs.versions.app.get()
kotlin {
jvm()
sourceSets {
commonMain.dependencies {
implementation(project(":client"))
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.components.resources)
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutinesSwing)
}
}
}
compose.desktop {
application {
mainClass = "com.zaneschepke.wireguardautotunnel.desktop.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm, TargetFormat.AppImage)
packageName = "com.zaneschepke.wireguardautotunnel.desktop"
packageVersion = libs.versions.app.get()
}
}
}
// Conveyor
dependencies {
linuxAmd64(libs.desktop.jvm.linux.x64)
macAmd64(libs.desktop.jvm.macos.x64)
macAarch64(libs.desktop.jvm.macos.arm64)
windowsAmd64(libs.desktop.jvm.windows.x64)
windowsAarch64(libs.desktop.jvm.windows.arm64)
}
tasks.named<Delete>("clean") {
delete(file("generated.conveyor.conf"))
}
@@ -0,0 +1,44 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="450dp"
android:height="450dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M56.25,18V46L32,60 7.75,46V18L32,4Z"
android:fillColor="#6075f2"/>
<path
android:pathData="m41.5,26.5v11L32,43V60L56.25,46V18Z"
android:fillColor="#6b57ff"/>
<path
android:pathData="m32,43 l-9.5,-5.5v-11L7.75,18V46L32,60Z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="23.131"
android:centerY="18.441"
android:gradientRadius="42.132"
android:type="radial">
<item android:offset="0" android:color="#FF5383EC"/>
<item android:offset="0.867" android:color="#FF7F52FF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M22.5,26.5 L32,21 41.5,26.5 56.25,18 32,4 7.75,18Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="44.172"
android:startY="4.377"
android:endX="17.973"
android:endY="34.035"
android:type="linear">
<item android:offset="0" android:color="#FF33C3FF"/>
<item android:offset="0.878" android:color="#FF5383EC"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="m32,21 l9.526,5.5v11L32,43 22.474,37.5v-11z"
android:fillColor="#000000"/>
</vector>
@@ -0,0 +1,27 @@
package com.zaneschepke.wireguardautotunnel.desktop
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun App() {
MaterialTheme {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
}
}
}
@@ -0,0 +1,15 @@
package com.zaneschepke.wireguardautotunnel.desktop
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import dev.icerock.moko.resources.compose.stringResource
import com.zaneschepke.wireguardautotunnel.SharedRes
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = stringResource(SharedRes.strings.app_name)
) {
App()
}
}
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.desktop
class JVMPlatform {
val name: String = "Java ${System.getProperty("java.version")}"
}
fun getPlatform() = JVMPlatform()
@@ -0,0 +1,12 @@
package com.zaneschepke.wireguardautotunnel.desktop
import kotlin.test.Test
import kotlin.test.assertEquals
class ComposeAppDesktopTest {
@Test
fun example() {
assertEquals(3, 1 + 2)
}
}
+204
View File
@@ -0,0 +1,204 @@
include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf")
include required("composeApp/generated.conveyor.conf")
app {
fsname = wgtunnel
display-name = "WG Tunnel"
description = "WG Tunnel: WireGuard and AmneziaWG VPN client with auto-tunneling, lockdown and proxying."
license = MIT
homepage = "https://wgtunnel.com"
site.base-url = "http://localhost"
icons = ["icon.png"]
jvm {
# for performance
options += "-XX:+UseG1GC"
options += "-XX:+UseStringDeduplication"
# for high-res displays
system-properties {
"sun.java2d.uiScale" = "1.0"
"apple.laf.useScreenMenuBar" = "true"
}
modules = [ detect ]
gui {
main-class = com.zaneschepke.wireguardautotunnel.desktop.MainKt
}
cli {
wgtctl {
main-class = com.zaneschepke.wireguardautotunnel.cli.MainKt
exe-name = wgtctl
}
daemon {
main-class = com.zaneschepke.wireguardautotunnel.daemon.MainKt
console = false
}
}
}
inputs += "composeApp/build/libs/*.jar"
inputs += "daemon/build/install/daemon/lib/*.jar"
inputs += "cli/build/install/cli/lib/*.jar"
// Target platforms
machines = [
linux.amd64.glibc,
windows.amd64,
// windows.aarch64,
mac.amd64,
mac.aarch64
]
linux {
deb.depends = ["systemd"]
rpm.requires = ["systemd"]
desktop-file {
"Desktop Entry" {
Categories = "Network;Security;Settings;Utility;"
}
}
# for CLI
symlinks = [
/usr/bin/wgtunnel -> ${app.linux.install-path}/bin/wgtunnel,
/usr/bin/wgtctl -> ${app.linux.install-path}/bin/wgtctl,
/usr/bin/wgt -> ${app.linux.install-path}/bin/wgtctl
]
services {
daemon {
include "/stdlib/linux/service.conf"
file-name = "wgtunnel-daemon.service"
Unit {
Description = "WG Tunnel Daemon"
Documentation = "https://wgtunnel.com"
Before = "network-online.target"
After = "NetworkManager.service systemd-resolved.service"
StartLimitBurst = 5
StartLimitIntervalSec = 20
}
Service {
Restart = always
RestartSec = 1s
ExecStart = ${app.linux.install-path}/bin/daemon
Type = exec
StandardOutput = journal
StandardError = journal
Environment = [
"WG_TUNNEL_SERVICE=1",
"HOME=%S/wgtunnel"
]
WorkingDirectory = ${app.linux.install-path}
# Allow socket access
UMask = 0000
ProtectSystem = full
StateDirectory = "wgtunnel"
LogsDirectory = "wgtunnel"
ConfigurationDirectory = "wgtunnel"
RuntimeDirectory = "wgtunnel"
RuntimeDirectoryMode = 0755
RuntimeDirectoryPreserve = "restart"
# Added CAP_DAC_OVERRIDE for per user IPC key read
CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_DAC_OVERRIDE"
AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_DAC_OVERRIDE"
RestrictAddressFamilies = "AF_INET AF_INET6 AF_NETLINK AF_UNIX"
KillSignal = SIGTERM
TimeoutStopSec = 30
ReadWritePaths = [
"/run/wgtunnel",
"/etc/resolv.conf",
"/var/lib/wgtunnel",
"/home", # Need home to be able to read user's IPC key
"/etc/resolv.conf",
"/run/systemd/resolve",
"/run/systemd/resolve/stub-resolv.conf",
"/run/systemd/resolve/resolv.conf"
]
}
Install {
WantedBy = "multi-user.target"
}
}
}
}
mac {
entitlements-plist = {
"com.apple.security.network.client" = true
"com.apple.security.network.server" = true
}
}
windows {
inputs += daemon/winsw/artifacts/publish/WinSW-x64.exe -> service-wrapper.exe
aarch64 {
inputs += tunnel/tools/wintun/arm64/wintun.dll -> wintun.dll
}
amd64 {
inputs += tunnel/tools/wintun/amd64/wintun.dll -> wintun.dll
}
manifests {
exe {
requested-execution-level = asInvoker
}
msix {
display-name = "WG Tunnel"
description = "WireGuard and AmneziaWG VPN client with auto-tunneling, lockdown and proxying."
min-version = "10.0.19041.0"
capabilities += "rescap:allowElevation"
capabilities += "rescap:localSystemServices"
capabilities += "rescap:packagedServices"
namespaces {
desktop6 = "http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
uap3 = "http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
}
ignorable-namespaces += "desktop6"
ignorable-namespaces += "uap3"
extensions-xml = """
<desktop6:Extension Category="windows.service" Executable="bin/service-wrapper.exe" EntryPoint="Windows.FullTrustApplication">
<desktop6:Service Name="wgtunnel-daemon" StartupType="auto" StartAccount="localSystem" />
</desktop6:Extension>
"""
virtualization {
excluded-directories += "LocalAppData/Temp"
excluded-directories += "CommonAppData/wgtunnel"
excluded-directories += "CommonAppData/wgtunnel/logs"
}
}
}
start-on-login = false
updates = background
}
}
conveyor.compatibility-level = 21
+1
View File
@@ -0,0 +1 @@
/build
+18
View File
@@ -0,0 +1,18 @@
plugins {
kotlin("jvm")
alias(libs.plugins.serialization)
}
dependencies {
implementation(libs.kotlinx.serialization)
implementation(libs.apache.commons.lang3)
// Logging
implementation(libs.kermit)
implementation(libs.logback.classic)
implementation(libs.kotlinx.coroutines.core)
// Backoff
implementation(libs.kotlin.retry)
}
@@ -0,0 +1,60 @@
package com.zaneschepke.wireguardautotunnel.core.crypto
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.io.encoding.Base64
object Crypto {
const val KEY_ALGORITHM = "AES"
const val CYPHER = "AES/GCM/NoPadding"
private val random = SecureRandom()
fun generateRandomBase64(byteLength: Int = 32): String {
val bytes = ByteArray(byteLength)
random.nextBytes(bytes)
return Base64.encode(bytes)
}
fun generateRandomAESKey() : SecretKey {
val keyBytes = ByteArray(32)
random.nextBytes(keyBytes)
return SecretKeySpec(keyBytes, KEY_ALGORITHM)
}
fun generateRandomBase64EncodedAesKey() : String {
return Base64.encode(generateRandomAESKey().encoded)
}
fun decodeKey(key: String): SecretKey {
return SecretKeySpec(Base64.decode(key), KEY_ALGORITHM)
}
fun encryptWithMasterKey(plainText: String, key: SecretKey): String {
val cipher = Cipher.getInstance(CYPHER)
val iv = ByteArray(12) // 96-bit IV for GCM
random.nextBytes(iv)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.ENCRYPT_MODE, key, spec)
val cipherText = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
// store IV + ciphertext together, base64-encoded
val combined = iv + cipherText
return Base64.encode(combined)
}
fun decryptWithMasterKey(encrypted: String, key: SecretKey): String {
val combined = Base64.decode(encrypted)
val iv = combined.copyOfRange(0, 12)
val cipherText = combined.copyOfRange(12, combined.size)
val cipher = Cipher.getInstance(CYPHER)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, key, spec)
val decrypted = cipher.doFinal(cipherText)
return String(decrypted, Charsets.UTF_8)
}
}
@@ -0,0 +1,27 @@
package com.zaneschepke.wireguardautotunnel.core.crypto
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.SecureCommand
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.io.encoding.Base64
import kotlin.math.abs
object HmacProtector {
private const val ALGORITHM = "HmacSHA256"
fun generateSignature(key: String, timestamp: Long, payload: String?): String {
val mac = Mac.getInstance(ALGORITHM)
mac.init(SecretKeySpec(key.toByteArray(), ALGORITHM))
val dataToSign = "$timestamp${payload ?: ""}"
return Base64.encode(mac.doFinal(dataToSign.toByteArray()))
}
fun verify(key: String, command: SecureCommand): Boolean {
val now = System.currentTimeMillis() / 1000
// 30 seconds window to prevent replay attacks
if (abs(now - command.timestamp) > 30) return false
val expected = generateSignature(key, command.timestamp, command.payload)
return expected == command.signature
}
}
@@ -0,0 +1,198 @@
package com.zaneschepke.wireguardautotunnel.core.helper
import co.touchlab.kermit.Logger
import com.github.michaelbull.retry.policy.binaryExponentialBackoff
import com.github.michaelbull.retry.policy.plus
import com.github.michaelbull.retry.policy.stopAtAttempts
import com.github.michaelbull.retry.retry
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.apache.commons.lang3.SystemUtils
import java.io.File
import java.io.FileNotFoundException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.attribute.PosixFilePermissions
object PermissionsHelper {
val socketRetryPolicy = binaryExponentialBackoff<Throwable>(min = 10L, max = 250L) + stopAtAttempts(25)
// unix
const val WORLD_WRITABLE_OCTAL = "666"
const val WORLD_READWRITE_SYMBOLIC = "rw-rw-rw-"
const val OWNER_FULL_CONTROL_OCTAL = "755"
const val OWNER_FULL_CONTROL_SYMBOLIC = "rwxr-xr-x"
const val OWNER_ONLY_PRIVATE_FILE = "rw-------"
const val OWNER_ONLY_PRIVATE_DIR = "rwx------"
// windows universal SIDs
private const val SID_SYSTEM = "*S-1-5-18"
private const val SID_ADMINISTRATORS = "*S-1-5-32-544"
private const val SID_USERS = "*S-1-5-32-545"
private const val SID_CREATOR_OWNER = "*S-1-3-0"
// windows permission flags
private const val WIN_DIR_MODIFY_INHERIT = ":(OI)(CI)(M)"
private const val WIN_FULL_CONTROL_INHERIT = ":(OI)(CI)(F)"
fun setupDirectoryPermissionsUnix(runtimeDirPath: String) {
val path = Paths.get(runtimeDirPath)
if (Files.exists(path)) {
try {
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(OWNER_FULL_CONTROL_SYMBOLIC))
Logger.i { "Successfully set directory permissions to " }
} catch (e: Exception) {
Logger.e { "POSIX native permissions failed: ${e.message} → falling back to chmod" }
try {
val exitCode = ProcessBuilder("chmod", OWNER_FULL_CONTROL_OCTAL, runtimeDirPath)
.start()
.waitFor()
if (exitCode == 0) {
Logger.i { "Successfully set directory permissions using chmod" }
} else {
Logger.e { "chmod failed with exit code $exitCode" }
}
} catch (chmodEx: Exception) {
Logger.e { "Failed to execute chmod: ${chmodEx.message}" }
}
}
} else {
Logger.w { "Runtime directory $runtimeDirPath not found" }
}
}
fun setupDirectoryPermissionsWindows(runtimeDirPath: String) {
try {
val process = ProcessBuilder(
"icacls", runtimeDirPath,
"/grant", "$SID_USERS$WIN_DIR_MODIFY_INHERIT",
"/grant", "$SID_SYSTEM$WIN_FULL_CONTROL_INHERIT",
"/grant", "$SID_ADMINISTRATORS$WIN_FULL_CONTROL_INHERIT"
).start()
if (process.waitFor() != 0) {
val error = process.errorStream.bufferedReader().use { it.readText() }
Logger.e { "icacls directory setup failed: $error" }
}
} catch (e: Exception) {
Logger.e(e) { "Failed to set Windows directory ACLs" }
}
}
suspend fun setupSocketPermissionsWithPollUnix(socketPath: String) = withContext(Dispatchers.IO) {
val socketFile = File(socketPath)
runCatching {
retry(socketRetryPolicy) {
if (!socketFile.exists()) {
throw FileNotFoundException("Socket $socketPath not found yet")
}
setupSocketPermissionsUnix(socketPath)
}
val socketPerms = Files.getPosixFilePermissions(Paths.get(socketPath))
Logger.i { "Final socket permissions: $socketPerms" }
}.onFailure {
Logger.e { "Socket $socketPath failed to appear. Daemon likely failed to start: ${it.message}" }
}
}
suspend fun setupSocketPermissionsWithPollWindows(socketPath: String) = withContext(Dispatchers.IO) {
val socketFile = File(socketPath)
runCatching {
retry(socketRetryPolicy) {
if (!socketFile.exists()) throw FileNotFoundException("Socket not found yet")
setupDirectoryPermissionsWindows(socketPath)
}
logWindowsACLs(socketPath)
}.onFailure {
Logger.e { "Socket $socketPath failed to appear on Windows: ${it.message}" }
}
}
fun setupSocketPermissionsUnix(socketPath: String) {
val path = Paths.get(socketPath)
try {
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(WORLD_READWRITE_SYMBOLIC))
Logger.i { "Successfully set socket permissions to 0666" }
} catch (e: Exception) {
Logger.e { "POSIX native permissions failed: ${e.message} → falling back to chmod" }
try {
val exitCode = ProcessBuilder("chmod", WORLD_WRITABLE_OCTAL, socketPath)
.start()
.waitFor()
if (exitCode == 0) {
Logger.i { "Successfully set socket permissions using chmod" }
} else {
Logger.e { "chmod failed with exit code $exitCode" }
throw IllegalStateException("chmod exited with non-zero status")
}
} catch (chmodEx: Exception) {
Logger.e { "All POSIX methods failed: ${chmodEx.message} → using JVM fallback" }
// try file API
val socketFile = path.toFile()
val readOk = socketFile.setReadable(true, false)
val writeOk = socketFile.setWritable(true, false)
if (readOk && writeOk) {
Logger.w { "Applied weak Java fallback permissions (readable/writable for all)" }
} else {
Logger.e { "Failed to set any permissions on socket $socketPath" }
}
}
}
}
fun setOwnerOnly(path: Path) {
try {
if (SystemUtils.IS_OS_WINDOWS) {
applyWindowsOwnerOnlyPermissions(path)
} else {
applyPosixOwnerOnlyPermissions(path)
}
} catch (e: Exception) {
Logger.e(e) { "Failed to set permissions for: $path" }
}
}
private fun applyPosixOwnerOnlyPermissions(path: Path) {
val isDir = Files.isDirectory(path)
val permsString = if (isDir) OWNER_ONLY_PRIVATE_DIR else OWNER_ONLY_PRIVATE_FILE
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permsString))
}
private fun applyWindowsOwnerOnlyPermissions(path: Path) {
try {
val process = ProcessBuilder(
"icacls", path.toString(),
"/inheritance:r", // remove inherited perms
"/grant:r", "$SID_SYSTEM$WIN_FULL_CONTROL_INHERIT",
"/grant:r", "$SID_CREATOR_OWNER$WIN_FULL_CONTROL_INHERIT",
"/grant:r", "$SID_ADMINISTRATORS$WIN_FULL_CONTROL_INHERIT"
).start()
if (process.waitFor() != 0) {
Logger.e { "icacls owner-only failed for $path" }
}
} catch (e: Exception) {
Logger.e(e) { "Error applying owner-only Windows perms" }
}
}
private fun logWindowsACLs(path: String) {
runCatching {
val output = ProcessBuilder("icacls", path).start().inputStream.bufferedReader().readText()
Logger.i { "Final ACLs for $path: $output" }
}
}
}
@@ -0,0 +1,76 @@
package com.zaneschepke.wireguardautotunnel.core.ipc
import co.touchlab.kermit.Logger
import com.zaneschepke.wireguardautotunnel.core.crypto.Crypto
import com.zaneschepke.wireguardautotunnel.core.helper.PermissionsHelper
import org.apache.commons.lang3.SystemUtils
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
object IPC {
const val KEY_FILE = "ipc.key"
const val USER_FOLDER = ".wgtunnel"
const val SOCKET_FILE_NAME = "daemon.sock"
fun resolveKeyForUser(user: String): String? {
if (!user.matches(Regex("^[a-zA-Z0-9._-]+$"))) {
Logger.w { "Invalid username format: $user" }
return null
}
return try {
val userHome = getUserHome(user)
val keyPath = Paths.get(userHome, USER_FOLDER, KEY_FILE)
if (Files.exists(keyPath)) {
keyPath.toFile().readText().trim().takeIf { it.isNotBlank() }
} else {
Logger.w { "IPC key not found for user: $user$keyPath" }
null
}
} catch (e: Exception) {
Logger.Companion.e(e) { "Failed to resolve IPC key for user: $user" }
null
}
}
// should be called by client ONLY
fun getIPCSecret() : String {
val ipcFile = File(System.getProperty("user.home"), "${IPC.USER_FOLDER}/${IPC.KEY_FILE}")
if (!ipcFile.parentFile.exists()) ipcFile.parentFile.mkdirs()
return if (!ipcFile.exists()) {
val secret = Crypto.generateRandomBase64(32)
ipcFile.writeText(secret)
// Set 600 permissions immediately
PermissionsHelper.setOwnerOnly(ipcFile.toPath())
secret
} else {
ipcFile.readText()
}
}
private fun getUserHome(user: String): String {
return when {
SystemUtils.IS_OS_WINDOWS -> "C:\\Users\\$user"
SystemUtils.IS_OS_MAC_OSX -> "/Users/$user"
else -> "/home/$user"
}
}
fun getDaemonSocketPath(): String {
return when {
SystemUtils.IS_OS_WINDOWS -> {
val baseDir = System.getenv("PROGRAMDATA") + "\\wgtunnel"
"$baseDir\\$SOCKET_FILE_NAME"
}
SystemUtils.IS_OS_MAC_OSX -> {
"/tmp/wgtunnel/$SOCKET_FILE_NAME"
}
else -> {
"/run/wgtunnel/$SOCKET_FILE_NAME"
}
}
}
}
@@ -0,0 +1,8 @@
package com.zaneschepke.wireguardautotunnel.core.ipc.dto
import kotlinx.serialization.Serializable
@Serializable
enum class BackendMode {
KERNEL, USERSPACE, PROXY
}
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.core.ipc.dto
import kotlinx.serialization.Serializable
@Serializable
data class BackendStatus(
val killSwitchEnabled: Boolean,
val mode: BackendMode,
val activeTunnels: List<TunnelStatus>
)
@@ -0,0 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.ipc.dto
import kotlinx.serialization.Serializable
@Serializable
data class SecureCommand(
val timestamp: Long,
val signature: String,
val userHint: String,
val payload: String? = null
)
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.core.ipc.dto
import kotlinx.serialization.Serializable
@Serializable
data class StartTunnelRequest(
val id: Int,
val name: String,
val quickConfig: String
)
@@ -0,0 +1,8 @@
package com.zaneschepke.wireguardautotunnel.core.ipc.dto
import kotlinx.serialization.Serializable
@Serializable
enum class TunnelState {
DOWN, STARTING, HEALTHY, HANDSHAKE_FAILURE, RESOLVING_DNS, UNKNOWN
}
@@ -0,0 +1,10 @@
package com.zaneschepke.wireguardautotunnel.core.ipc.dto
import kotlinx.serialization.Serializable
@Serializable
data class TunnelStatus(
val id: Int,
val name: String,
val state: TunnelState
)

Some files were not shown because too many files have changed in this diff Show More