From 478aef6952f18524a9527526b0a1db2d4a3cdb1e Mon Sep 17 00:00:00 2001 From: zaneschepke Date: Tue, 3 Feb 2026 06:40:04 -0500 Subject: [PATCH] initial commit --- .gitignore | 21 + .gitmodules | 6 + README.md | 8 + build.gradle.kts | 52 + buildSrc/build.gradle.kts | 13 + buildSrc/settings.gradle.kts | 1 + buildSrc/src/main/kotlin/LocalProperties.kt | 19 + buildSrc/src/main/kotlin/System.kt | 5 + buildSrc/src/main/kotlin/Tasks.kt | 38 + cli/.gitignore | 1 + cli/build.gradle.kts | 49 + .../wireguardautotunnel/cli/CliRoot.kt | 32 + .../wireguardautotunnel/cli/Main.kt | 24 + .../commands/handler/CliExceptionHandler.kt | 18 + .../cli/commands/tunnel/TunnelCommand.kt | 20 + .../commands/tunnel/TunnelDeleteCommand.kt | 38 + .../cli/commands/tunnel/TunnelDownCommand.kt | 30 + .../commands/tunnel/TunnelImportCommand.kt | 67 + .../cli/commands/tunnel/TunnelListCommand.kt | 48 + .../cli/commands/tunnel/TunnelUpCommand.kt | 29 + .../cli/provider/ManifestVersionProvider.kt | 10 + .../cli/strategy/CliExecutionStrategy.kt | 33 + client/.gitignore | 1 + client/build.gradle.kts | 56 + .../1.json | 341 ++++++ .../1.json | 341 ++++++ .../client/data/AppKeyringConverter.kt | 24 + .../client/data/Database.kt | 69 ++ .../client/data/DatabaseCallback.kt | 15 + .../client/data/DatabaseConverters.kt | 45 + .../client/data/dao/AutoTunnelSettingsDao.kt | 21 + .../client/data/dao/DnsSettingsDao.kt | 16 + .../client/data/dao/GeneralSettingsDao.kt | 28 + .../client/data/dao/LockdownSettingsDao.kt | 18 + .../client/data/dao/ProxySettingsDao.kt | 16 + .../client/data/dao/TunnelConfigDao.kt | 84 ++ .../client/data/entity/AutoTunnelSettings.kt | 25 + .../client/data/entity/DnsSettings.kt | 16 + .../client/data/entity/GeneralSettings.kt | 17 + .../client/data/entity/LockdownSettings.kt | 11 + .../client/data/entity/ProxySettings.kt | 18 + .../client/data/entity/TunnelConfig.kt | 32 + .../data/mapper/AutoTunnelSettingsMapper.kt | 30 + .../client/data/mapper/DnsSettingsMapper.kt | 21 + .../client/data/mapper/LockdownMapper.kt | 13 + .../client/data/mapper/ProxySettingsMapper.kt | 26 + .../client/data/mapper/SettingsMapper.kt | 25 + .../client/data/mapper/TunnelConfigMapper.kt | 33 + .../client/data/model/AppMode.kt | 12 + .../client/data/model/Dns.kt | 30 + .../client/data/model/EncryptedField.kt | 4 + .../client/data/model/Theme.kt | 10 + .../RoomAutoTunnelSettingsRepository.kt | 29 + .../repository/RoomDnsSettingsRepository.kt | 24 + .../RoomLockdownSettingsRepository.kt | 23 + .../repository/RoomProxySettingsRepository.kt | 23 + .../data/repository/RoomSettingsRepository.kt | 37 + .../data/repository/RoomTunnelRepository.kt | 97 ++ .../service/DefaultTunnelImportService.kt | 25 + .../data/service/UdsDaemonHealthService.kt | 19 + .../data/service/UdsTunnelCommandService.kt | 107 ++ .../client/di/Qualifiers.kt | 5 + .../client/di/databaseModule.kt | 59 + .../client/di/serviceModule.kt | 73 ++ .../client/domain/model/AutoTunnelSettings.kt | 19 + .../client/domain/model/DnsSettings.kt | 11 + .../client/domain/model/GeneralSettings.kt | 15 + .../client/domain/model/LockdownSettings.kt | 11 + .../client/domain/model/ProxySettings.kt | 19 + .../client/domain/model/TunnelConfig.kt | 109 ++ .../AutoTunnelSettingsRepository.kt | 14 + .../repository/DnsSettingsRepository.kt | 12 + .../repository/GeneralSettingRepository.kt | 20 + .../repository/LockdownSettingsRepository.kt | 12 + .../repository/ProxySettingsRepository.kt | 12 + .../domain/repository/TunnelRepository.kt | 48 + .../repository/extensions/TunnelRepository.kt | 47 + .../client/service/DaemonHealthService.kt | 5 + .../client/service/TunnelCommandService.kt | 14 + .../client/service/TunnelImportService.kt | 11 + .../moko-resources/base/strings.xml | 4 + composeApp/build.gradle.kts | 63 + .../drawable/compose-multiplatform.xml | 44 + .../wireguardautotunnel/desktop/App.kt | 27 + .../wireguardautotunnel/desktop/Main.kt | 15 + .../wireguardautotunnel/desktop/Platform.kt | 7 + .../desktop/ComposeAppDesktopTest.kt | 12 + conveyor.conf | 204 ++++ core/.gitignore | 1 + core/build.gradle.kts | 18 + .../wireguardautotunnel/core/crypto/Crypto.kt | 60 + .../core/crypto/HmacProtector.kt | 27 + .../core/helper/PermissionsHelper.kt | 198 +++ .../wireguardautotunnel/core/ipc/IPC.kt | 76 ++ .../core/ipc/dto/BackendMode.kt | 8 + .../core/ipc/dto/BackendStatus.kt | 10 + .../core/ipc/dto/SecureCommand.kt | 11 + .../core/ipc/dto/StartTunnelRequest.kt | 10 + .../core/ipc/dto/TunnelState.kt | 8 + .../core/ipc/dto/TunnelStatus.kt | 10 + daemon/.gitignore | 1 + daemon/build.gradle.kts | 87 ++ .../wireguardautotunnel/daemon/Main.kt | 32 + .../daemon/TunnelDaemon.kt | 105 ++ .../daemon/data/DaemonCacheRepository.kt | 10 + .../data/KStoreDaemonCacheRepository.kt | 105 ++ .../daemon/data/model/DaemonCacheData.kt | 9 + .../daemon/data/model/KillSwitchSettings.kt | 6 + .../daemon/di/daemonModule.kt | 23 + .../daemon/dto/Extensions.kt | 37 + .../daemon/plugin/UDSPlugins.kt | 44 + .../daemon/routes/tunnelCommandRoutes.kt | 109 ++ .../daemon/tunnel/RunningTunnel.kt | 15 + .../wireguardautotunnel/daemon/util/Logger.kt | 11 + .../daemon/util/UdsExtensions.kt | 35 + .../src/main/resources/macos/cli.entitlements | 10 + .../main/resources/macos/daemon.entitlements | 13 + .../resources/macos/wgtunnel-daemon.plist | 34 + daemon/winsw | 1 + gradle.properties | 7 + gradle/libs.versions.toml | 127 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 251 ++++ gradlew.bat | 94 ++ icon.png | Bin 0 -> 21668 bytes keyring/.gitignore | 3 + keyring/build.gradle.kts | 38 + .../wireguardautotunnel/keyring/Keyring.kt | 29 + .../keyring/NativeKeyring.kt | 29 + keyring/tools/keyring-go/Makefile | 70 ++ keyring/tools/keyring-go/go.mod | 12 + keyring/tools/keyring-go/go.sum | 22 + keyring/tools/keyring-go/keyring.go | 75 ++ parser/build.gradle.kts | 17 + .../wireguardautotunnel/parser/Config.kt | 157 +++ .../parser/ConfigParseException.kt | 9 + .../wireguardautotunnel/parser/ErrorType.kt | 22 + .../parser/InterfaceSection.kt | 84 ++ .../wireguardautotunnel/parser/PeerSection.kt | 53 + .../wireguardautotunnel/parser/crypto/Key.kt | 143 +++ .../parser/util/Extensions.kt | 22 + .../parser/util/NetworkUtils.kt | 82 ++ settings.gradle.kts | 24 + tunnel/.gitignore | 3 + tunnel/build.gradle.kts | 53 + .../tunnel/AmneziaBackend.kt | 141 +++ .../wireguardautotunnel/tunnel/Backend.kt | 26 + .../wireguardautotunnel/tunnel/Tunnel.kt | 32 + .../tunnel/native/AwgTunnel.kt | 35 + .../tunnel/native/StatusCodeCallback.kt | 7 + .../tunnel/util/BackendException.kt | 8 + tunnel/tools/amneziawg-tools | 1 + tunnel/tools/libwg-go/Makefile | 111 ++ tunnel/tools/libwg-go/constants/constants.go | 7 + tunnel/tools/libwg-go/dns/dns.go | 165 +++ tunnel/tools/libwg-go/dns/resolver_unix.go | 41 + tunnel/tools/libwg-go/dns/resolver_windows.go | 26 + tunnel/tools/libwg-go/go.mod | 61 + tunnel/tools/libwg-go/go.sum | 91 ++ tunnel/tools/libwg-go/ipc/ipc_bsd.go | 29 + tunnel/tools/libwg-go/ipc/ipc_linux.go | 29 + tunnel/tools/libwg-go/ipc/ipc_windows.go | 20 + .../tools/libwg-go/killswitch/killswitch.go | 51 + tunnel/tools/libwg-go/main.go | 9 + tunnel/tools/libwg-go/proxy/proxy.go | 231 ++++ tunnel/tools/libwg-go/shared/shared.go | 63 + tunnel/tools/libwg-go/util/util.go | 16 + tunnel/tools/libwg-go/vpn/bind/bind_darwin.go | 13 + tunnel/tools/libwg-go/vpn/bind/linux_bind.go | 38 + .../tools/libwg-go/vpn/bind/windows_bind.go | 12 + tunnel/tools/libwg-go/vpn/dns/dns_linux.go | 294 +++++ tunnel/tools/libwg-go/vpn/dns/dns_windows.go | 79 ++ .../tools/libwg-go/vpn/firewall/firewall.go | 21 + .../tools/libwg-go/vpn/firewall/mark/mark.go | 16 + .../vpn/firewall/osfirewall/firewall_linux.go | 1085 +++++++++++++++++ .../vpn/firewall/osfirewall/firewall_macos.go | 290 +++++ .../firewall/osfirewall/firewall_windows.go | 677 ++++++++++ .../osfirewall/firewallmgr/manager.go | 25 + .../vpn/router/osrouter/router_linux.go | 476 ++++++++ .../vpn/router/osrouter/router_windows.go | 766 ++++++++++++ tunnel/tools/libwg-go/vpn/router/router.go | 78 ++ tunnel/tools/libwg-go/vpn/vpn.go | 378 ++++++ tunnel/tools/wintun/amd64/wintun.dll | Bin 0 -> 427552 bytes tunnel/tools/wintun/arm64/wintun.dll | Bin 0 -> 222488 bytes 185 files changed, 11241 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 100644 build.gradle.kts create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/settings.gradle.kts create mode 100644 buildSrc/src/main/kotlin/LocalProperties.kt create mode 100644 buildSrc/src/main/kotlin/System.kt create mode 100644 buildSrc/src/main/kotlin/Tasks.kt create mode 100644 cli/.gitignore create mode 100644 cli/build.gradle.kts create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/CliRoot.kt create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/Main.kt create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/handler/CliExceptionHandler.kt create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelCommand.kt create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelDeleteCommand.kt create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelDownCommand.kt create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelImportCommand.kt create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelListCommand.kt create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelUpCommand.kt create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/provider/ManifestVersionProvider.kt create mode 100644 cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/strategy/CliExecutionStrategy.kt create mode 100644 client/.gitignore create mode 100644 client/build.gradle.kts create mode 100644 client/schemas/com.zaneschepke.wireguardautotunnel.client.data.AppDatabase/1.json create mode 100644 client/schemas/com.zaneschepke.wireguardautotunnel.shared.data.AppDatabase/1.json create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/AppKeyringConverter.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/Database.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/DatabaseCallback.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/DatabaseConverters.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/AutoTunnelSettingsDao.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/DnsSettingsDao.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/GeneralSettingsDao.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/LockdownSettingsDao.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/ProxySettingsDao.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/TunnelConfigDao.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/AutoTunnelSettings.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/DnsSettings.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/GeneralSettings.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/LockdownSettings.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/ProxySettings.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/TunnelConfig.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/AutoTunnelSettingsMapper.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/DnsSettingsMapper.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/LockdownMapper.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/ProxySettingsMapper.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/SettingsMapper.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/TunnelConfigMapper.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/AppMode.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/Dns.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/EncryptedField.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/Theme.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomAutoTunnelSettingsRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomDnsSettingsRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomLockdownSettingsRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomProxySettingsRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomSettingsRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomTunnelRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/DefaultTunnelImportService.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/UdsDaemonHealthService.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/UdsTunnelCommandService.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/Qualifiers.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/databaseModule.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/serviceModule.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/AutoTunnelSettings.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/DnsSettings.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/GeneralSettings.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/LockdownSettings.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/ProxySettings.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/TunnelConfig.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/AutoTunnelSettingsRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/DnsSettingsRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/GeneralSettingRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/LockdownSettingsRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/ProxySettingsRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/TunnelRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/extensions/TunnelRepository.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/DaemonHealthService.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/TunnelCommandService.kt create mode 100644 client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/TunnelImportService.kt create mode 100644 client/src/commonMain/moko-resources/base/strings.xml create mode 100644 composeApp/build.gradle.kts create mode 100644 composeApp/src/jvmMain/composeResources/drawable/compose-multiplatform.xml create mode 100644 composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/App.kt create mode 100644 composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/Main.kt create mode 100644 composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/Platform.kt create mode 100644 composeApp/src/jvmTest/kotlin/com/zaneschepke/wireguardautotunnel/desktop/ComposeAppDesktopTest.kt create mode 100644 conveyor.conf create mode 100644 core/.gitignore create mode 100644 core/build.gradle.kts create mode 100644 core/src/main/java/com/zaneschepke/wireguardautotunnel/core/crypto/Crypto.kt create mode 100644 core/src/main/java/com/zaneschepke/wireguardautotunnel/core/crypto/HmacProtector.kt create mode 100644 core/src/main/java/com/zaneschepke/wireguardautotunnel/core/helper/PermissionsHelper.kt create mode 100644 core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/IPC.kt create mode 100644 core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/BackendMode.kt create mode 100644 core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/BackendStatus.kt create mode 100644 core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/SecureCommand.kt create mode 100644 core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/StartTunnelRequest.kt create mode 100644 core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/TunnelState.kt create mode 100644 core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/TunnelStatus.kt create mode 100644 daemon/.gitignore create mode 100644 daemon/build.gradle.kts create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/Main.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/TunnelDaemon.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/DaemonCacheRepository.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/KStoreDaemonCacheRepository.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/model/DaemonCacheData.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/model/KillSwitchSettings.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/di/daemonModule.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/dto/Extensions.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/plugin/UDSPlugins.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/routes/tunnelCommandRoutes.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/tunnel/RunningTunnel.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/util/Logger.kt create mode 100644 daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/util/UdsExtensions.kt create mode 100644 daemon/src/main/resources/macos/cli.entitlements create mode 100644 daemon/src/main/resources/macos/daemon.entitlements create mode 100644 daemon/src/main/resources/macos/wgtunnel-daemon.plist create mode 160000 daemon/winsw create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 icon.png create mode 100644 keyring/.gitignore create mode 100644 keyring/build.gradle.kts create mode 100644 keyring/src/main/kotlin/com/zaneschepke/wireguardautotunnel/keyring/Keyring.kt create mode 100644 keyring/src/main/kotlin/com/zaneschepke/wireguardautotunnel/keyring/NativeKeyring.kt create mode 100644 keyring/tools/keyring-go/Makefile create mode 100644 keyring/tools/keyring-go/go.mod create mode 100644 keyring/tools/keyring-go/go.sum create mode 100644 keyring/tools/keyring-go/keyring.go create mode 100644 parser/build.gradle.kts create mode 100644 parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/Config.kt create mode 100644 parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/ConfigParseException.kt create mode 100644 parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/ErrorType.kt create mode 100644 parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/InterfaceSection.kt create mode 100644 parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/PeerSection.kt create mode 100644 parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/crypto/Key.kt create mode 100644 parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/util/Extensions.kt create mode 100644 parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/util/NetworkUtils.kt create mode 100644 settings.gradle.kts create mode 100644 tunnel/.gitignore create mode 100644 tunnel/build.gradle.kts create mode 100644 tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/AmneziaBackend.kt create mode 100644 tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/Backend.kt create mode 100644 tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/Tunnel.kt create mode 100644 tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/native/AwgTunnel.kt create mode 100644 tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/native/StatusCodeCallback.kt create mode 100644 tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/util/BackendException.kt create mode 160000 tunnel/tools/amneziawg-tools create mode 100644 tunnel/tools/libwg-go/Makefile create mode 100644 tunnel/tools/libwg-go/constants/constants.go create mode 100644 tunnel/tools/libwg-go/dns/dns.go create mode 100644 tunnel/tools/libwg-go/dns/resolver_unix.go create mode 100644 tunnel/tools/libwg-go/dns/resolver_windows.go create mode 100755 tunnel/tools/libwg-go/go.mod create mode 100644 tunnel/tools/libwg-go/go.sum create mode 100644 tunnel/tools/libwg-go/ipc/ipc_bsd.go create mode 100644 tunnel/tools/libwg-go/ipc/ipc_linux.go create mode 100644 tunnel/tools/libwg-go/ipc/ipc_windows.go create mode 100644 tunnel/tools/libwg-go/killswitch/killswitch.go create mode 100644 tunnel/tools/libwg-go/main.go create mode 100755 tunnel/tools/libwg-go/proxy/proxy.go create mode 100755 tunnel/tools/libwg-go/shared/shared.go create mode 100755 tunnel/tools/libwg-go/util/util.go create mode 100644 tunnel/tools/libwg-go/vpn/bind/bind_darwin.go create mode 100644 tunnel/tools/libwg-go/vpn/bind/linux_bind.go create mode 100644 tunnel/tools/libwg-go/vpn/bind/windows_bind.go create mode 100644 tunnel/tools/libwg-go/vpn/dns/dns_linux.go create mode 100644 tunnel/tools/libwg-go/vpn/dns/dns_windows.go create mode 100644 tunnel/tools/libwg-go/vpn/firewall/firewall.go create mode 100644 tunnel/tools/libwg-go/vpn/firewall/mark/mark.go create mode 100644 tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_linux.go create mode 100644 tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_macos.go create mode 100644 tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_windows.go create mode 100644 tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewallmgr/manager.go create mode 100644 tunnel/tools/libwg-go/vpn/router/osrouter/router_linux.go create mode 100644 tunnel/tools/libwg-go/vpn/router/osrouter/router_windows.go create mode 100644 tunnel/tools/libwg-go/vpn/router/router.go create mode 100755 tunnel/tools/libwg-go/vpn/vpn.go create mode 100644 tunnel/tools/wintun/amd64/wintun.dll create mode 100644 tunnel/tools/wintun/arm64/wintun.dll diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..367c7a6 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..604db4d --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6525db6 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# WG Tunnel - Desktop + +A WIP project for WG Tunnel desktop. + +## Supported Platforms +- macOS (Future) +- Windows +- Linux \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..06c7951 --- /dev/null +++ b/build.gradle.kts @@ -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 { + jvmToolchain(jvmVersion) + } + } + + plugins.withId("org.jetbrains.kotlin.multiplatform") { + extensions.configure { + 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("clean") { + delete(layout.buildDirectory) +} \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..e16cc6d --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -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") +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 0000000..3f87d39 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "buildSrc" \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/LocalProperties.kt b/buildSrc/src/main/kotlin/LocalProperties.kt new file mode 100644 index 0000000..7e4b8e5 --- /dev/null +++ b/buildSrc/src/main/kotlin/LocalProperties.kt @@ -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) +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/System.kt b/buildSrc/src/main/kotlin/System.kt new file mode 100644 index 0000000..fc831ef --- /dev/null +++ b/buildSrc/src/main/kotlin/System.kt @@ -0,0 +1,5 @@ +object SystemVar { + fun fromEnvironment(envVar : String) : String? { + return System.getenv(envVar) + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Tasks.kt b/buildSrc/src/main/kotlin/Tasks.kt new file mode 100644 index 0000000..8e5a92b --- /dev/null +++ b/buildSrc/src/main/kotlin/Tasks.kt @@ -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(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" + ) + } +} \ No newline at end of file diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts new file mode 100644 index 0000000..d2ff5d4 --- /dev/null +++ b/cli/build.gradle.kts @@ -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("installDist") { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +application { + mainClass.set("com.zaneschepke.wireguardautotunnel.cli.MainKt") +} + +tasks.withType { + manifest { + attributes( + "Implementation-Title" to project.name, + "Implementation-Version" to libs.versions.app.get(), + "Main-Class" to application.mainClass.get() + ) + } +} + + diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/CliRoot.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/CliRoot.kt new file mode 100644 index 0000000..69e9fcf --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/CliRoot.kt @@ -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") + + } +} \ No newline at end of file diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/Main.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/Main.kt new file mode 100644 index 0000000..7309e3b --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/Main.kt @@ -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) { + 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) +} \ No newline at end of file diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/handler/CliExceptionHandler.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/handler/CliExceptionHandler.kt new file mode 100644 index 0000000..c7a038b --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/handler/CliExceptionHandler.kt @@ -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 + } +} diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelCommand.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelCommand.kt new file mode 100644 index 0000000..a67e003 --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelCommand.kt @@ -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..") + } +} \ No newline at end of file diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelDeleteCommand.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelDeleteCommand.kt new file mode 100644 index 0000000..eba4e2d --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelDeleteCommand.kt @@ -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 { + + 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 = "", 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 + } +} \ No newline at end of file diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelDownCommand.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelDownCommand.kt new file mode 100644 index 0000000..2050446 --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelDownCommand.kt @@ -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 = "", 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"}") + } + } + } +} \ No newline at end of file diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelImportCommand.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelImportCommand.kt new file mode 100644 index 0000000..9a12231 --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelImportCommand.kt @@ -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 { + + 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 + } + +} diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelListCommand.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelListCommand.kt new file mode 100644 index 0000000..9e6d791 --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelListCommand.kt @@ -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 { + + 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 + } +} diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelUpCommand.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelUpCommand.kt new file mode 100644 index 0000000..e704f46 --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/commands/tunnel/TunnelUpCommand.kt @@ -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 = "", 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"}") + } + } + } +} \ No newline at end of file diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/provider/ManifestVersionProvider.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/provider/ManifestVersionProvider.kt new file mode 100644 index 0000000..a6177d6 --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/provider/ManifestVersionProvider.kt @@ -0,0 +1,10 @@ +package com.zaneschepke.wireguardautotunnel.cli.provider + +import picocli.CommandLine + +class ManifestVersionProvider : CommandLine.IVersionProvider { + override fun getVersion(): Array { + val version = ManifestVersionProvider::class.java.getPackage().implementationVersion + return if (version != null) arrayOf(version) else arrayOf("Unknown version") + } +} \ No newline at end of file diff --git a/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/strategy/CliExecutionStrategy.kt b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/strategy/CliExecutionStrategy.kt new file mode 100644 index 0000000..1a903cb --- /dev/null +++ b/cli/src/main/java/com/zaneschepke/wireguardautotunnel/cli/strategy/CliExecutionStrategy.kt @@ -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) + } +} \ No newline at end of file diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/client/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/client/build.gradle.kts b/client/build.gradle.kts new file mode 100644 index 0000000..5f1557f --- /dev/null +++ b/client/build.gradle.kts @@ -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) +} + + + diff --git a/client/schemas/com.zaneschepke.wireguardautotunnel.client.data.AppDatabase/1.json b/client/schemas/com.zaneschepke.wireguardautotunnel.client.data.AppDatabase/1.json new file mode 100644 index 0000000..23dd2f2 --- /dev/null +++ b/client/schemas/com.zaneschepke.wireguardautotunnel.client.data.AppDatabase/1.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/client/schemas/com.zaneschepke.wireguardautotunnel.shared.data.AppDatabase/1.json b/client/schemas/com.zaneschepke.wireguardautotunnel.shared.data.AppDatabase/1.json new file mode 100644 index 0000000..23dd2f2 --- /dev/null +++ b/client/schemas/com.zaneschepke.wireguardautotunnel.shared.data.AppDatabase/1.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/AppKeyringConverter.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/AppKeyringConverter.kt new file mode 100644 index 0000000..fc471d7 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/AppKeyringConverter.kt @@ -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) + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/Database.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/Database.kt new file mode 100644 index 0000000..0008ed2 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/Database.kt @@ -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 { + override fun initialize(): AppDatabase +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/DatabaseCallback.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/DatabaseCallback.kt new file mode 100644 index 0000000..9d405ca --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/DatabaseCallback.kt @@ -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) : 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") + } +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/DatabaseConverters.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/DatabaseConverters.kt new file mode 100644 index 0000000..985b36c --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/DatabaseConverters.kt @@ -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 { + return Json.encodeToString(value) + } + + @TypeConverter + fun stringToList(value: String): List { + if (value.isBlank() || value.isEmpty()) return mutableListOf() + return try { + Json.decodeFromString>(value) + } catch (e: Exception) { + val list = value.split(",").toMutableList() + val json = listToString(list) + Json.decodeFromString>(json) + } + } + + @TypeConverter + fun setToString(value: Set): String { + return listToString(value.toList()) + } + + @TypeConverter + fun stringToSet(value: String): Set { + 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 +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/AutoTunnelSettingsDao.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/AutoTunnelSettingsDao.kt new file mode 100644 index 0000000..6de6ad0 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/AutoTunnelSettingsDao.kt @@ -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 + + @Query("UPDATE auto_tunnel_settings SET is_tunnel_enabled = :enabled") + suspend fun updateAutoTunnelEnabled(enabled: Boolean) +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/DnsSettingsDao.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/DnsSettingsDao.kt new file mode 100644 index 0000000..5457cec --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/DnsSettingsDao.kt @@ -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 +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/GeneralSettingsDao.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/GeneralSettingsDao.kt new file mode 100644 index 0000000..d51d577 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/GeneralSettingsDao.kt @@ -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 + + @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) +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/LockdownSettingsDao.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/LockdownSettingsDao.kt new file mode 100644 index 0000000..96d2c7b --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/LockdownSettingsDao.kt @@ -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 +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/ProxySettingsDao.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/ProxySettingsDao.kt new file mode 100644 index 0000000..52dc8f4 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/ProxySettingsDao.kt @@ -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 +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/TunnelConfigDao.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/TunnelConfigDao.kt new file mode 100644 index 0000000..abfc505 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/dao/TunnelConfigDao.kt @@ -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) + + @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 + + @Query("SELECT * FROM tunnel_config") suspend fun getAll(): List + + @Delete suspend fun delete(t: TunnelConfig) + + @Delete suspend fun delete(t: List) + + @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 + + @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 + + @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> + + @Query("SELECT * FROM tunnel_config WHERE name != :globalName ORDER BY position") + fun getAllTunnelsExceptGlobal( + globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME + ): Flow> + + @Query("SELECT * FROM tunnel_config WHERE name = :globalName LIMIT 1") + fun getGlobalTunnel(globalName: String = TunnelConfig.GLOBAL_CONFIG_NAME): Flow +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/AutoTunnelSettings.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/AutoTunnelSettings.kt new file mode 100644 index 0000000..efe4c09 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/AutoTunnelSettings.kt @@ -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 = 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, +) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/DnsSettings.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/DnsSettings.kt new file mode 100644 index 0000000..a453198 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/DnsSettings.kt @@ -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, +) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/GeneralSettings.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/GeneralSettings.kt new file mode 100644 index 0000000..a0ee85b --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/GeneralSettings.kt @@ -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, +) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/LockdownSettings.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/LockdownSettings.kt new file mode 100644 index 0000000..2e6216d --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/LockdownSettings.kt @@ -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 +) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/ProxySettings.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/ProxySettings.kt new file mode 100644 index 0000000..60bc5f0 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/ProxySettings.kt @@ -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, +) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/TunnelConfig.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/TunnelConfig.kt new file mode 100644 index 0000000..902d5ec --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/entity/TunnelConfig.kt @@ -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 = 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" + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/AutoTunnelSettingsMapper.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/AutoTunnelSettingsMapper.kt new file mode 100644 index 0000000..b5105f1 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/AutoTunnelSettingsMapper.kt @@ -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, + ) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/DnsSettingsMapper.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/DnsSettingsMapper.kt new file mode 100644 index 0000000..0d8925e --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/DnsSettingsMapper.kt @@ -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, + ) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/LockdownMapper.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/LockdownMapper.kt new file mode 100644 index 0000000..68003fa --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/LockdownMapper.kt @@ -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 + ) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/ProxySettingsMapper.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/ProxySettingsMapper.kt new file mode 100644 index 0000000..f837af3 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/ProxySettingsMapper.kt @@ -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, + ) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/SettingsMapper.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/SettingsMapper.kt new file mode 100644 index 0000000..f430af8 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/SettingsMapper.kt @@ -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, + ) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/TunnelConfigMapper.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/TunnelConfigMapper.kt new file mode 100644 index 0000000..2cf7804 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/mapper/TunnelConfigMapper.kt @@ -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, + ) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/AppMode.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/AppMode.kt new file mode 100644 index 0000000..1f249fd --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/AppMode.kt @@ -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 + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/Dns.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/Dns.kt new file mode 100644 index 0000000..91561ae --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/Dns.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/EncryptedField.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/EncryptedField.kt new file mode 100644 index 0000000..d9ff6b6 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/EncryptedField.kt @@ -0,0 +1,4 @@ +package com.zaneschepke.wireguardautotunnel.client.data.model + +@JvmInline +value class EncryptedField(val value: String) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/Theme.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/Theme.kt new file mode 100644 index 0000000..6c6ce2b --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/model/Theme.kt @@ -0,0 +1,10 @@ +package com.zaneschepke.wireguardautotunnel.client.data.model + +enum class Theme { + AUTOMATIC, + LIGHT, + DARK, + DARKER, + AMOLED, + DYNAMIC, +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomAutoTunnelSettingsRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomAutoTunnelSettingsRepository.kt new file mode 100644 index 0000000..5baab72 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomAutoTunnelSettingsRepository.kt @@ -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 + 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) + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomDnsSettingsRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomDnsSettingsRepository.kt new file mode 100644 index 0000000..e218996 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomDnsSettingsRepository.kt @@ -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 + get() = dnsSettingsDao.getDnsSettingsFlow().map { (it ?: Entity()).toDomain() } + + override suspend fun getDnsSettings(): Domain { + return (dnsSettingsDao.getDnsSettings() ?: Entity()).toDomain() + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomLockdownSettingsRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomLockdownSettingsRepository.kt new file mode 100644 index 0000000..31e9afc --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomLockdownSettingsRepository.kt @@ -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() + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomProxySettingsRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomProxySettingsRepository.kt new file mode 100644 index 0000000..2fdeeb3 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomProxySettingsRepository.kt @@ -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() + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomSettingsRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomSettingsRepository.kt new file mode 100644 index 0000000..1fb168d --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomSettingsRepository.kt @@ -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) + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomTunnelRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomTunnelRepository.kt new file mode 100644 index 0000000..4d1fcc6 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/repository/RoomTunnelRepository.kt @@ -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 = + tunnelConfigDao.getGlobalTunnel().map { it?.toDomain() } + + override suspend fun getAll(): List { + return tunnelConfigDao.getAll().map { it.toDomain() } + } + + override suspend fun save(tunnelConfig: Domain) { + tunnelConfigDao.upsert(tunnelConfig.toEntity()) + } + + override suspend fun saveAll(tunnelConfigList: List) { + 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 { + 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 { + return tunnelConfigDao.findByTunnelNetworkName(name).map { it.toDomain() } + } + + override suspend fun findPrimary(): List { + return tunnelConfigDao.findByPrimary().map { it.toDomain() } + } + + override suspend fun delete(tunnels: List) { + tunnelConfigDao.delete(tunnels.map { it.toEntity() }) + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/DefaultTunnelImportService.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/DefaultTunnelImportService.kt new file mode 100644 index 0000000..ea1b731 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/DefaultTunnelImportService.kt @@ -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) + } +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/UdsDaemonHealthService.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/UdsDaemonHealthService.kt new file mode 100644 index 0000000..d0bf95c --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/UdsDaemonHealthService.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/UdsTunnelCommandService.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/UdsTunnelCommandService.kt new file mode 100644 index 0000000..2cae94a --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/data/service/UdsTunnelCommandService.kt @@ -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 = 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 = 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 = 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 = 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 = runCatching { + val response = client.get("/tunnel/status") + + if (!response.status.isSuccess()) { + throw IOException("Failed to get status: ${response.status.value} - ${response.bodyAsText()}") + } + + response.body() + } + + override fun statusFlow(): Flow = callbackFlow { + val session = client.webSocketSession("/tunnel/status/stream") + + try { + for (frame in session.incoming) { + if (frame is Frame.Text) { + val dto = json.decodeFromString(frame.readText()) + trySend(dto) + } + } + } catch (e: Exception) { + close(e) + } finally { + session.close() + awaitClose() + } + }.flowOn(Dispatchers.IO) +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/Qualifiers.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/Qualifiers.kt new file mode 100644 index 0000000..b979692 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/Qualifiers.kt @@ -0,0 +1,5 @@ +package com.zaneschepke.wireguardautotunnel.client.di + +enum class Secret { + IPC +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/databaseModule.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/databaseModule.kt new file mode 100644 index 0000000..16431b1 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/databaseModule.kt @@ -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 { DatabaseCallback(lazy { get() }) } + single { + 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 { + val dbFileName = AppDatabase.DB_FILE_NAME + val dbDir = AppDatabase.getDatabaseDir() + dbDir.mkdirs() + val dbFile = File(dbDir, dbFileName) + Room.databaseBuilder(dbFile.absolutePath) + .setDriver(BundledSQLiteDriver()) + .fallbackToDestructiveMigration(true) + .addCallback(get()) + .addTypeConverter(DatabaseConverters()) + .addTypeConverter(AppKeyringConverter()) + .setQueryCoroutineContext(Dispatchers.IO) + .build() + } + + single { get().tunnelConfigDao() } + single { get().autoTunnelSettingsDao() } + single { get().dnsSettingsDao() } + single { get().lockdownSettingsDao() } + single { get().proxySettingsDao() } + single { get().generalSettingsDao() } + + single() { RoomTunnelRepository(get()) } + single() { RoomAutoTunnelSettingsRepository(get()) } + single() { RoomDnsSettingsRepository(get()) } + single() { RoomLockdownSettingsRepository(get()) } + single() { RoomProxySettingsRepository(get()) } +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/serviceModule.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/serviceModule.kt new file mode 100644 index 0000000..651f4b1 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/di/serviceModule.kt @@ -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 { UdsDaemonHealthService(get()) } + single { UdsTunnelCommandService(get(), tunnelRepository = get()) } + single { + DefaultTunnelImportService(get()) + } +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/AutoTunnelSettings.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/AutoTunnelSettings.kt new file mode 100644 index 0000000..862c91d --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/AutoTunnelSettings.kt @@ -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 = 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, +) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/DnsSettings.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/DnsSettings.kt new file mode 100644 index 0000000..3ad5ee3 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/DnsSettings.kt @@ -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, +) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/GeneralSettings.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/GeneralSettings.kt new file mode 100644 index 0000000..a6fd91c --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/GeneralSettings.kt @@ -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, +) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/LockdownSettings.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/LockdownSettings.kt new file mode 100644 index 0000000..0b150ec --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/LockdownSettings.kt @@ -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, +) diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/ProxySettings.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/ProxySettings.kt new file mode 100644 index 0000000..338f6d0 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/ProxySettings.kt @@ -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" + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/TunnelConfig.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/TunnelConfig.kt new file mode 100644 index 0000000..91e2b6f --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/model/TunnelConfig.kt @@ -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 = 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 + } +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/AutoTunnelSettingsRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/AutoTunnelSettingsRepository.kt new file mode 100644 index 0000000..9efe8af --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/AutoTunnelSettingsRepository.kt @@ -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 + + suspend fun getAutoTunnelSettings(): AutoTunnelSettings + + suspend fun updateAutoTunnelEnabled(enabled: Boolean) +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/DnsSettingsRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/DnsSettingsRepository.kt new file mode 100644 index 0000000..e460768 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/DnsSettingsRepository.kt @@ -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 + + suspend fun getDnsSettings(): DnsSettings +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/GeneralSettingRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/GeneralSettingRepository.kt new file mode 100644 index 0000000..28df0db --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/GeneralSettingRepository.kt @@ -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 + + suspend fun getGeneralSettings(): GeneralSettings + + suspend fun updateTheme(theme: Theme) + + suspend fun updateLocale(locale: String) + + suspend fun updateAppMode(appMode: AppMode) +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/LockdownSettingsRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/LockdownSettingsRepository.kt new file mode 100644 index 0000000..8d21055 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/LockdownSettingsRepository.kt @@ -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 + + suspend fun getLockdownSettings(): LockdownSettings +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/ProxySettingsRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/ProxySettingsRepository.kt new file mode 100644 index 0000000..d9fdb75 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/ProxySettingsRepository.kt @@ -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 + + suspend fun getProxySettings(): ProxySettings +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/TunnelRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/TunnelRepository.kt new file mode 100644 index 0000000..ee5baf9 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/TunnelRepository.kt @@ -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> + + val userTunnelsFlow: Flow> + + val globalTunnelFlow: Flow + + suspend fun getAll(): List + + suspend fun save(tunnelConfig: TunnelConfig) + + suspend fun saveAll(tunnelConfigList: List) + + 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 + + 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 + + suspend fun findPrimary(): List + + suspend fun delete(tunnels: List) +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/extensions/TunnelRepository.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/extensions/TunnelRepository.kt new file mode 100644 index 0000000..471f3a8 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/domain/repository/extensions/TunnelRepository.kt @@ -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, + existingNames: List, +) { + val uniqueTunnels = + generateUniquelyNamedConfigs( + tunnels, + existingNames + ) + saveAll(uniqueTunnels) +} + +private fun generateUniquelyNamedConfigs( + incoming: List, + existingNames: List, +): List { + val usedNames = existingNames.toMutableSet() + val result = mutableListOf() + 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 +} diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/DaemonHealthService.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/DaemonHealthService.kt new file mode 100644 index 0000000..e695a1a --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/DaemonHealthService.kt @@ -0,0 +1,5 @@ +package com.zaneschepke.wireguardautotunnel.client.service + +interface DaemonHealthService { + suspend fun alive(): Boolean +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/TunnelCommandService.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/TunnelCommandService.kt new file mode 100644 index 0000000..cfeed16 --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/TunnelCommandService.kt @@ -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 + suspend fun stopTunnel(id: Int): Result + suspend fun setMode(mode: BackendMode): Result + suspend fun setKillSwitch(enabled: Boolean): Result + suspend fun getStatus(): Result + fun statusFlow(): Flow +} \ No newline at end of file diff --git a/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/TunnelImportService.kt b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/TunnelImportService.kt new file mode 100644 index 0000000..fc44cbb --- /dev/null +++ b/client/src/commonMain/kotlin/com/zaneschepke/wireguardautotunnel/client/service/TunnelImportService.kt @@ -0,0 +1,11 @@ +package com.zaneschepke.wireguardautotunnel.client.service + + +typealias QuickString = String +typealias TunnelName = String +typealias QuickConfigMap = Map + +interface TunnelImportService { + suspend fun import(config: QuickString, name: TunnelName? = null) + suspend fun import(configs: QuickConfigMap) +} \ No newline at end of file diff --git a/client/src/commonMain/moko-resources/base/strings.xml b/client/src/commonMain/moko-resources/base/strings.xml new file mode 100644 index 0000000..b96ca50 --- /dev/null +++ b/client/src/commonMain/moko-resources/base/strings.xml @@ -0,0 +1,4 @@ + + + WG Tunnel + \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts new file mode 100644 index 0000000..90bff1b --- /dev/null +++ b/composeApp/build.gradle.kts @@ -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("clean") { + delete(file("generated.conveyor.conf")) +} diff --git a/composeApp/src/jvmMain/composeResources/drawable/compose-multiplatform.xml b/composeApp/src/jvmMain/composeResources/drawable/compose-multiplatform.xml new file mode 100644 index 0000000..eba956a --- /dev/null +++ b/composeApp/src/jvmMain/composeResources/drawable/compose-multiplatform.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/App.kt b/composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/App.kt new file mode 100644 index 0000000..da577c1 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/App.kt @@ -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, + ) { + + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/Main.kt b/composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/Main.kt new file mode 100644 index 0000000..91427dd --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/Main.kt @@ -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() + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/Platform.kt b/composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/Platform.kt new file mode 100644 index 0000000..5fe7b01 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/zaneschepke/wireguardautotunnel/desktop/Platform.kt @@ -0,0 +1,7 @@ +package com.zaneschepke.wireguardautotunnel.desktop + +class JVMPlatform { + val name: String = "Java ${System.getProperty("java.version")}" +} + +fun getPlatform() = JVMPlatform() \ No newline at end of file diff --git a/composeApp/src/jvmTest/kotlin/com/zaneschepke/wireguardautotunnel/desktop/ComposeAppDesktopTest.kt b/composeApp/src/jvmTest/kotlin/com/zaneschepke/wireguardautotunnel/desktop/ComposeAppDesktopTest.kt new file mode 100644 index 0000000..da7f94d --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/com/zaneschepke/wireguardautotunnel/desktop/ComposeAppDesktopTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/conveyor.conf b/conveyor.conf new file mode 100644 index 0000000..a681ea8 --- /dev/null +++ b/conveyor.conf @@ -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 = """ + + + + """ + + virtualization { + excluded-directories += "LocalAppData/Temp" + excluded-directories += "CommonAppData/wgtunnel" + excluded-directories += "CommonAppData/wgtunnel/logs" + } + } + } + + start-on-login = false + updates = background + } +} +conveyor.compatibility-level = 21 \ No newline at end of file diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..b374523 --- /dev/null +++ b/core/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/crypto/Crypto.kt b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/crypto/Crypto.kt new file mode 100644 index 0000000..d75ebb1 --- /dev/null +++ b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/crypto/Crypto.kt @@ -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) + } +} \ No newline at end of file diff --git a/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/crypto/HmacProtector.kt b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/crypto/HmacProtector.kt new file mode 100644 index 0000000..be79185 --- /dev/null +++ b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/crypto/HmacProtector.kt @@ -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 + } +} \ No newline at end of file diff --git a/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/helper/PermissionsHelper.kt b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/helper/PermissionsHelper.kt new file mode 100644 index 0000000..15e8203 --- /dev/null +++ b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/helper/PermissionsHelper.kt @@ -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(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" } + } + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/IPC.kt b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/IPC.kt new file mode 100644 index 0000000..79a849c --- /dev/null +++ b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/IPC.kt @@ -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" + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/BackendMode.kt b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/BackendMode.kt new file mode 100644 index 0000000..e239cff --- /dev/null +++ b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/BackendMode.kt @@ -0,0 +1,8 @@ +package com.zaneschepke.wireguardautotunnel.core.ipc.dto + +import kotlinx.serialization.Serializable + +@Serializable +enum class BackendMode { + KERNEL, USERSPACE, PROXY +} \ No newline at end of file diff --git a/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/BackendStatus.kt b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/BackendStatus.kt new file mode 100644 index 0000000..6aec62f --- /dev/null +++ b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/BackendStatus.kt @@ -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 +) \ No newline at end of file diff --git a/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/SecureCommand.kt b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/SecureCommand.kt new file mode 100644 index 0000000..366cd85 --- /dev/null +++ b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/SecureCommand.kt @@ -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 +) \ No newline at end of file diff --git a/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/StartTunnelRequest.kt b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/StartTunnelRequest.kt new file mode 100644 index 0000000..23dac73 --- /dev/null +++ b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/StartTunnelRequest.kt @@ -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 +) \ No newline at end of file diff --git a/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/TunnelState.kt b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/TunnelState.kt new file mode 100644 index 0000000..d298cc9 --- /dev/null +++ b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/TunnelState.kt @@ -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 +} \ No newline at end of file diff --git a/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/TunnelStatus.kt b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/TunnelStatus.kt new file mode 100644 index 0000000..8e837b3 --- /dev/null +++ b/core/src/main/java/com/zaneschepke/wireguardautotunnel/core/ipc/dto/TunnelStatus.kt @@ -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 +) \ No newline at end of file diff --git a/daemon/.gitignore b/daemon/.gitignore new file mode 100644 index 0000000..4029495 --- /dev/null +++ b/daemon/.gitignore @@ -0,0 +1 @@ +/output \ No newline at end of file diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts new file mode 100644 index 0000000..c974931 --- /dev/null +++ b/daemon/build.gradle.kts @@ -0,0 +1,87 @@ +plugins { + kotlin("jvm") + application + alias(libs.plugins.serialization) +} + +dependencies { + implementation(project(":tunnel")) + implementation(project(":parser")) + implementation(project(":core")) + + // DI + implementation(libs.koin.core) + + implementation(libs.bundles.ktor.server.jvm) + + implementation(libs.kotlinx.coroutines.core) + + // Logging + implementation(libs.kermit) + implementation(libs.logback.classic) + + testImplementation(kotlin("test")) + + // secure caching + implementation(libs.kstore) + implementation(libs.kstore.file) + + implementation(libs.kotlinx.serialization) + + // Util + implementation(libs.apache.commons.lang3) +} + +application { + mainClass.set("com.zaneschepke.wireguardautotunnel.daemon.MainKt") +} + +tasks.test { + useJUnitPlatform() +} + +val cleanDotNet = tasks.register("cleanDotNet") { + group = "build" + workingDir = file("winsw/src") + commandLine("dotnet", "clean", "-c", "Release") +} + +tasks.named("clean") { + dependsOn(cleanDotNet) + + delete(file("output")) + // Clean up WinSW specific artifacts + delete(file("winsw/src/WinSW/bin")) + delete(file("winsw/src/WinSW/obj")) + delete(file("winsw/artifacts")) +} + +tasks.named("installDist") { + dependsOn("buildWinSW") +} + +tasks.register("buildWinSW") { + val winSwDir = "winsw/src/WinSW" + group = "build" + description = "Build Windows service wrapper." + workingDir = file(winSwDir) + + inputs.files( + fileTree(winSwDir) { + include("**/*.cs", "**/*.csproj", "**/appsettings.json") + exclude("bin/**", "obj/**") + } + ).withPropertyName("winSwSourceFiles") + .withPathSensitivity(PathSensitivity.RELATIVE) + + outputs.dir(file("$winSwDir/bin/Release/net7.0-windows/win-x64/publish")) + .withPropertyName("winSwPublishDir") + + commandLine("dotnet", "publish", "WinSW.csproj", + "-f", "net7.0-windows", + "-c", "Release", + "-r", "win-x64", + "--self-contained", "true", + "-p:PublishSingleFile=true" + ) +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/Main.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/Main.kt new file mode 100644 index 0000000..916dbbf --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/Main.kt @@ -0,0 +1,32 @@ +package com.zaneschepke.wireguardautotunnel.daemon + +import co.touchlab.kermit.Logger +import com.zaneschepke.wireguardautotunnel.daemon.di.daemonModule +import com.zaneschepke.wireguardautotunnel.daemon.util.initLogger +import org.koin.core.context.startKoin +import org.koin.java.KoinJavaComponent.inject +import kotlin.system.exitProcess + +fun main() { + initLogger() + startKoin { + modules(daemonModule) + } + + val daemon : TunnelDaemon by inject(TunnelDaemon::class.java) + + try { + Runtime.getRuntime().addShutdownHook( + Thread { + Logger.i { "Stopping daemon..." } + daemon.stop() + } + ) + + Logger.i { "Starting daemon..." } + daemon.run() + } catch (e: Exception) { + Logger.e(e) { "Shutting down..." } + exitProcess(1) + } +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/TunnelDaemon.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/TunnelDaemon.kt new file mode 100644 index 0000000..c6e1797 --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/TunnelDaemon.kt @@ -0,0 +1,105 @@ +package com.zaneschepke.wireguardautotunnel.daemon + +import co.touchlab.kermit.Logger +import com.zaneschepke.wireguardautotunnel.core.helper.PermissionsHelper +import com.zaneschepke.wireguardautotunnel.daemon.data.DaemonCacheRepository +import com.zaneschepke.wireguardautotunnel.daemon.plugin.UDSPlugins +import com.zaneschepke.wireguardautotunnel.daemon.routes.tunnelCommandRoutes +import com.zaneschepke.wireguardautotunnel.tunnel.Backend +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.cio.* +import io.ktor.server.engine.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.routing.* +import io.ktor.server.websocket.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import org.apache.commons.lang3.SystemUtils +import java.io.File +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicBoolean + +class TunnelDaemon(private val json: Json, + private val backend: Backend, + private val cacheRepository: DaemonCacheRepository, + private val socketPath: String +) { + private var server: EmbeddedServer<*, *>? = null + private val running = AtomicBoolean(false) + private val shutdownLatch = CountDownLatch(1) + private val scope = CoroutineScope(Dispatchers.IO) + + // run the daemon + internal fun run() { + startUdsServer() + shutdownLatch.await() // block main thread until stop() + } + + + fun startUdsServer() { + if (!running.compareAndSet(false, true)) return + + Logger.i { "Starting IPC server" } + + val socketFile = File(socketPath) + val runtimeDir = socketFile.parentFile + runtimeDir.mkdirs() + + when { + SystemUtils.IS_OS_WINDOWS -> PermissionsHelper.setupDirectoryPermissionsWindows(runtimeDir.absolutePath) + SystemUtils.IS_OS_UNIX -> PermissionsHelper.setupDirectoryPermissionsUnix(runtimeDir.absolutePath) + } + + socketFile.delete() // delete old socket if exists + + + server = embeddedServer(CIO, configure = { + unixConnector(socketPath) + }) { + install(ContentNegotiation) { + json(json) + } + install(WebSockets) + routing { + get("/status") { call.response.status(HttpStatusCode.OK) } + route("/tunnel") { + install(UDSPlugins.hmacShieldPlugin) + tunnelCommandRoutes(json, backend) + } + } + monitor.subscribe(ApplicationStarted) { + Logger.i { "IPC server started successfully" } + } + }.start(wait = false) + + scope.launch { + when { + SystemUtils.IS_OS_UNIX -> PermissionsHelper.setupSocketPermissionsWithPollUnix(socketPath) + SystemUtils.IS_OS_WINDOWS -> PermissionsHelper.setupSocketPermissionsWithPollWindows(socketPath) + } + } + + scope.launch { + // TODO handle startup with cached settings + val settings = cacheRepository.getKillSwitchSettings() + val startConfigs = cacheRepository.getStartConfigs() + Logger.d { "Got kill switch settings $settings" } + Logger.d { "Got start configs of size ${startConfigs.size}" } + } + } + + + fun stop() { + if (!running.compareAndSet(true, false)) return + Logger.i { "Daemon stop initiated - closing all tunnels" } + backend.shutdown() + Logger.i { "All tunnels closed - stopping server" } + server?.stop(gracePeriodMillis = 1_000, timeoutMillis = 2_000) + shutdownLatch.countDown() + Logger.i { "UDS server fully stopped" } + } +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/DaemonCacheRepository.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/DaemonCacheRepository.kt new file mode 100644 index 0000000..09e22a2 --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/DaemonCacheRepository.kt @@ -0,0 +1,10 @@ +package com.zaneschepke.wireguardautotunnel.daemon.data + +import com.zaneschepke.wireguardautotunnel.daemon.data.model.KillSwitchSettings + +interface DaemonCacheRepository { + suspend fun getKillSwitchSettings(): KillSwitchSettings + suspend fun setKillSwitchSettings(settings: KillSwitchSettings) + suspend fun getStartConfigs(): Set + suspend fun setStartConfigs(configs: Set) +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/KStoreDaemonCacheRepository.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/KStoreDaemonCacheRepository.kt new file mode 100644 index 0000000..3a73c2f --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/KStoreDaemonCacheRepository.kt @@ -0,0 +1,105 @@ +package com.zaneschepke.wireguardautotunnel.daemon.data + +import co.touchlab.kermit.Logger +import com.zaneschepke.wireguardautotunnel.daemon.data.model.DaemonCacheData +import com.zaneschepke.wireguardautotunnel.daemon.data.model.KillSwitchSettings +import io.github.xxfast.kstore.KStore +import io.github.xxfast.kstore.file.storeOf +import kotlinx.io.files.Path +import kotlinx.serialization.json.Json +import org.apache.commons.lang3.SystemUtils +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.attribute.PosixFilePermissions + +class KStoreDaemonCacheRepository( + private val baseCacheDir: java.nio.file.Path = getCacheBaseDir() +) : DaemonCacheRepository { + + companion object { + const val CACHE_FILE_NAME = "cache.json" + + private fun getCacheBaseDir(): java.nio.file.Path { + return when { + SystemUtils.IS_OS_MAC_OSX -> Paths.get("/Library/Application Support/wgtunnel") + SystemUtils.IS_OS_WINDOWS -> Paths.get(System.getenv("PROGRAMDATA") + "\\wgtunnel") + else -> Paths.get("/var/lib/wgtunnel") + } + } + } + + init { + Files.createDirectories(baseCacheDir) + setSecurePermissions(baseCacheDir) + } + + private fun getStore(): KStore { + val storePathNio = baseCacheDir.resolve(CACHE_FILE_NAME) + val storeKPath = Path(storePathNio.toString()) + + if (!Files.exists(storePathNio)) { + Files.createFile(storePathNio) + } + + if (Files.size(storePathNio) == 0L) { + val defaultData = DaemonCacheData() + val defaultJson = Json.encodeToString(defaultData) + Files.writeString(storePathNio, defaultJson) + } + + setSecurePermissions(storePathNio) + + return storeOf( + file = storeKPath, + default = DaemonCacheData() + ) + } + + + private fun setSecurePermissions(path: java.nio.file.Path) { + val os = System.getProperty("os.name").lowercase() + try { + if (!os.contains("win")) { + val isDirectory = Files.isDirectory(path) + val permsString = if (isDirectory) "rwx------" else "rw-------" // 700 for dirs, 600 for files + val perms = PosixFilePermissions.fromString(permsString) + Files.setPosixFilePermissions(path, perms) + } else { + val process = ProcessBuilder( + "icacls", path.toString(), + "/inheritance:r", // remove inherited permissions + "/grant:r", "SYSTEM:(F)", // full control to system + "/grant:r", "Administrators:(F)" // full control to admin + ).start() + val exitCode = process.waitFor() + if (exitCode != 0) { + Logger.e { "icacls failed with code $exitCode"} + } + } + } catch (e: Exception) { + Logger.e(e) { "Failed to set permissions"} + } + } + + override suspend fun getKillSwitchSettings(): KillSwitchSettings { + return getStore().get()?.killSwitch ?: KillSwitchSettings(false, false) + } + + override suspend fun setKillSwitchSettings(settings: KillSwitchSettings) { + val store = getStore() + store.update { current -> + current?.copy(killSwitch = settings) ?: DaemonCacheData(killSwitch = settings) + } + } + + override suspend fun getStartConfigs(): Set { + return getStore().get()?.startConfigs ?: emptySet() + } + + override suspend fun setStartConfigs(configs: Set) { + val store = getStore() + store.update { current -> + current?.copy(startConfigs = configs) ?: DaemonCacheData(startConfigs = configs) + } + } +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/model/DaemonCacheData.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/model/DaemonCacheData.kt new file mode 100644 index 0000000..9a71922 --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/model/DaemonCacheData.kt @@ -0,0 +1,9 @@ +package com.zaneschepke.wireguardautotunnel.daemon.data.model + +import kotlinx.serialization.Serializable + +@Serializable +data class DaemonCacheData( + val killSwitch: KillSwitchSettings = KillSwitchSettings(false, false), + val startConfigs: Set = emptySet() +) diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/model/KillSwitchSettings.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/model/KillSwitchSettings.kt new file mode 100644 index 0000000..c95ea31 --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/data/model/KillSwitchSettings.kt @@ -0,0 +1,6 @@ +package com.zaneschepke.wireguardautotunnel.daemon.data.model + +import kotlinx.serialization.Serializable + +@Serializable +data class KillSwitchSettings(val enabled: Boolean, val bypassLan: Boolean) diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/di/daemonModule.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/di/daemonModule.kt new file mode 100644 index 0000000..21d4a6b --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/di/daemonModule.kt @@ -0,0 +1,23 @@ +package com.zaneschepke.wireguardautotunnel.daemon.di + +import com.zaneschepke.wireguardautotunnel.core.ipc.IPC +import com.zaneschepke.wireguardautotunnel.daemon.TunnelDaemon +import com.zaneschepke.wireguardautotunnel.daemon.data.DaemonCacheRepository +import com.zaneschepke.wireguardautotunnel.daemon.data.KStoreDaemonCacheRepository +import com.zaneschepke.wireguardautotunnel.tunnel.AmneziaBackend +import com.zaneschepke.wireguardautotunnel.tunnel.Backend +import kotlinx.serialization.json.Json +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val daemonModule = module { + single { + Json { + ignoreUnknownKeys = true + encodeDefaults = true + } + } + single { AmneziaBackend() } + single { KStoreDaemonCacheRepository() } + single { TunnelDaemon(get(), get(), get(), IPC.getDaemonSocketPath()) } +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/dto/Extensions.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/dto/Extensions.kt new file mode 100644 index 0000000..ad82b25 --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/dto/Extensions.kt @@ -0,0 +1,37 @@ +package com.zaneschepke.wireguardautotunnel.daemon.dto + +import com.zaneschepke.wireguardautotunnel.core.ipc.dto.BackendMode +import com.zaneschepke.wireguardautotunnel.core.ipc.dto.BackendStatus +import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelState +import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelStatus +import com.zaneschepke.wireguardautotunnel.tunnel.Backend +import com.zaneschepke.wireguardautotunnel.tunnel.Tunnel + +fun Tunnel.State.toDto(): TunnelState = when (this) { + Tunnel.State.Down -> TunnelState.DOWN + Tunnel.State.Starting -> TunnelState.STARTING + is Tunnel.State.Up.Healthy -> TunnelState.HEALTHY + is Tunnel.State.Up.HandshakeFailure -> TunnelState.HANDSHAKE_FAILURE + is Tunnel.State.Up.ResolvingDns -> TunnelState.RESOLVING_DNS + is Tunnel.State.Up.Unknown -> TunnelState.UNKNOWN +} + +fun Backend.Mode.toDto(): BackendMode = when (this) { + Backend.Mode.Userspace -> BackendMode.USERSPACE + Backend.Mode.Proxy -> BackendMode.PROXY +} + +fun Backend.Status.toDto(): BackendStatus { + val activeList = activeTunnels.map { (tunnel, state) -> + TunnelStatus( + id = tunnel.id, + name = tunnel.name, + state = state.toDto() + ) + } + return BackendStatus( + killSwitchEnabled = killSwitchEnabled, + mode = mode.toDto(), + activeTunnels = activeList + ) +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/plugin/UDSPlugins.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/plugin/UDSPlugins.kt new file mode 100644 index 0000000..6b41bcd --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/plugin/UDSPlugins.kt @@ -0,0 +1,44 @@ +package com.zaneschepke.wireguardautotunnel.daemon.plugin + +import co.touchlab.kermit.Logger +import com.zaneschepke.wireguardautotunnel.core.ipc.IPC +import com.zaneschepke.wireguardautotunnel.core.crypto.HmacProtector +import com.zaneschepke.wireguardautotunnel.core.ipc.dto.SecureCommand +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.createRouteScopedPlugin +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.util.AttributeKey + +object UDSPlugins { + + const val VERIFIED_PAYLOAD_KEY = "verifiedPayload" + + val hmacShieldPlugin = createRouteScopedPlugin("HmacShield") { + val payloadKey = AttributeKey(VERIFIED_PAYLOAD_KEY) + + onCall { call -> + try { + Logger.d { "Verifying request..." } + val command = call.receive() + + Logger.d { "Resolving users secret..." } + val secret = IPC.resolveKeyForUser(command.userHint) + ?: return@onCall call.respond(HttpStatusCode.Unauthorized, "Unable to resolve key for user") + + Logger.d { "Verifying users secret..." } + if (!HmacProtector.verify(secret, command)) { + Logger.e { "Invalid user secret. Unauthorized request." } + call.respond(HttpStatusCode.Unauthorized, "Invalid HMAC Handshake") + return@onCall + } + Logger.d { "Constructing verified payload..." } + command.payload?.let { call.attributes.put(payloadKey, it) } + + } catch (e: Exception) { + Logger.e(e){ "Invalid user secret. Unauthorized request." } + call.respond(HttpStatusCode.BadRequest, "Secure envelope missing or malformed: ${e.message}") + } + } + } +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/routes/tunnelCommandRoutes.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/routes/tunnelCommandRoutes.kt new file mode 100644 index 0000000..2892407 --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/routes/tunnelCommandRoutes.kt @@ -0,0 +1,109 @@ +// 3. Updated tunnelCommandRoutes.kt (no TunnelRepository dependency) +package com.zaneschepke.wireguardautotunnel.daemon.routes + +import co.touchlab.kermit.Logger +import com.zaneschepke.wireguardautotunnel.core.ipc.dto.BackendMode +import com.zaneschepke.wireguardautotunnel.core.ipc.dto.StartTunnelRequest +import com.zaneschepke.wireguardautotunnel.daemon.dto.toDto +import com.zaneschepke.wireguardautotunnel.daemon.tunnel.RunningTunnel +import com.zaneschepke.wireguardautotunnel.daemon.util.unwrapVerifiedPayload +import com.zaneschepke.wireguardautotunnel.daemon.util.verifiedPayload +import com.zaneschepke.wireguardautotunnel.tunnel.Backend +import io.ktor.http.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.websocket.* +import io.ktor.websocket.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import org.koin.java.KoinJavaComponent.inject + +fun Route.tunnelCommandRoutes(json: Json, backend: Backend) { + + val logger = Logger.withTag("TunnelCommands") + + // START TUNNEL + post("/start") { + val request = call.unwrapVerifiedPayload( + json = json, + logger = logger, + logMessage = "Failed to parse start request" + ) ?: return@post + + logger.i { "Starting tunnel ${request.id} (${request.name})" } + + val tunnel = RunningTunnel(request.id, request.name) + + val result = backend.start(tunnel, request.quickConfig) + + if (result.isFailure) { + logger.e(result.exceptionOrNull()) { "Failed to start tunnel ${request.id}" } + call.respond(HttpStatusCode.InternalServerError, "Failed to start tunnel") + } else { + call.respond(HttpStatusCode.OK, "Tunnel ${request.id} started") + } + } + + // STOP TUNNEL + post("/stop/{id}") { + val id = call.parameters["id"]?.toIntOrNull() + ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing or invalid id") + + logger.i { "Stopping tunnel $id" } + + backend.stop(id) + + call.respond(HttpStatusCode.OK, "Tunnel $id stopped") + } + +// post("/mode") { +// val mode = call.unwrapVerifiedPayload( +// json = json, +// logger = logger, +// logMessage = "Failed to parse mode request" +// ) ?: return@post +// +// logger.i { "Setting backend mode to $mode" } +// backend.setMode(mode) +// call.respond(HttpStatusCode.OK, "Mode set to $mode") +// } +// +// post("/kill-switch") { +// val enabledStr = call.verifiedPayload()?.trim() +// ?: return@post call.respond(HttpStatusCode.BadRequest, "Missing enabled value") +// +// val enabled = enabledStr.equals("true", ignoreCase = true) +// +// logger.i { "Setting kill switch to $enabled" } +// val result = backend.setKillSwitch(enabled) +// if (result.isFailure) { +// call.respond(HttpStatusCode.InternalServerError, "Failed to toggle kill switch") +// } else { +// call.respond(HttpStatusCode.OK, "Kill switch set to $enabled") +// } +// } +// +// get("/status") { +// val status = backend.status.first() +// call.respond(HttpStatusCode.OK, status.toDto()) +// } +// +// webSocket("/status/stream") { +// logger.i { "Client connected to /tunnel/status/stream" } +// try { +// backend.status +// .map { it.toDto() } +// .collect { dto -> +// val text = json.encodeToString(dto) +// send(Frame.Text(text)) +// } +// } catch (e: Exception) { +// logger.e(e) { "Error streaming status" } +// } finally { +// logger.i { "Client disconnected from status stream" } +// } +// } +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/tunnel/RunningTunnel.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/tunnel/RunningTunnel.kt new file mode 100644 index 0000000..9eb2697 --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/tunnel/RunningTunnel.kt @@ -0,0 +1,15 @@ +package com.zaneschepke.wireguardautotunnel.daemon.tunnel + +import co.touchlab.kermit.Logger +import com.zaneschepke.wireguardautotunnel.tunnel.Tunnel + +class RunningTunnel( + override val id: Int, + override val name: String, + override val features: Set = emptySet() +) : Tunnel { + + override fun updateState(state: Tunnel.State) { + Logger.i { "Tunnel $id ($name) state changed → $state" } + } +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/util/Logger.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/util/Logger.kt new file mode 100644 index 0000000..9977cff --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/util/Logger.kt @@ -0,0 +1,11 @@ +package com.zaneschepke.wireguardautotunnel.daemon.util + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import co.touchlab.kermit.platformLogWriter + +fun initLogger() { + Logger.setLogWriters(platformLogWriter()) + Logger.setMinSeverity(Severity.Debug) + Logger.setTag("Daemon") +} \ No newline at end of file diff --git a/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/util/UdsExtensions.kt b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/util/UdsExtensions.kt new file mode 100644 index 0000000..ce99b02 --- /dev/null +++ b/daemon/src/main/kotlin/com/zaneschepke/wireguardautotunnel/daemon/util/UdsExtensions.kt @@ -0,0 +1,35 @@ +package com.zaneschepke.wireguardautotunnel.daemon.util + +import co.touchlab.kermit.Logger +import com.zaneschepke.wireguardautotunnel.daemon.plugin.UDSPlugins +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.ApplicationCall +import io.ktor.server.response.respond +import io.ktor.util.AttributeKey +import kotlinx.serialization.json.Json + +inline fun ApplicationCall.verifiedPayload(json: Json): Result { + val payloadStr = attributes.getOrNull(AttributeKey(UDSPlugins.VERIFIED_PAYLOAD_KEY)) + ?: return Result.failure(IllegalArgumentException("Missing payload")) + + return try { + Result.success(json.decodeFromString(payloadStr)) + } catch (e: Exception) { + Result.failure(e) + } +} + +suspend inline fun ApplicationCall.unwrapVerifiedPayload( + json: Json, + logger: Logger, + logMessage: String = "Failed to parse payload", + errorResponseMessage: String = "Invalid JSON payload" +): T? { + val result = verifiedPayload(json) + if (result.isFailure) { + logger.e(result.exceptionOrNull()) { logMessage } + respond(HttpStatusCode.BadRequest, errorResponseMessage) + return null + } + return result.getOrThrow() +} \ No newline at end of file diff --git a/daemon/src/main/resources/macos/cli.entitlements b/daemon/src/main/resources/macos/cli.entitlements new file mode 100644 index 0000000..7266d08 --- /dev/null +++ b/daemon/src/main/resources/macos/cli.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-write + + + \ No newline at end of file diff --git a/daemon/src/main/resources/macos/daemon.entitlements b/daemon/src/main/resources/macos/daemon.entitlements new file mode 100644 index 0000000..4568f32 --- /dev/null +++ b/daemon/src/main/resources/macos/daemon.entitlements @@ -0,0 +1,13 @@ + + + + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + + \ No newline at end of file diff --git a/daemon/src/main/resources/macos/wgtunnel-daemon.plist b/daemon/src/main/resources/macos/wgtunnel-daemon.plist new file mode 100644 index 0000000..6e286cc --- /dev/null +++ b/daemon/src/main/resources/macos/wgtunnel-daemon.plist @@ -0,0 +1,34 @@ + + + + + Label + com.zaneschepke.wgtunnel.daemon + ProgramArguments + + /Applications/WireGuard AutoTunnel.app/Contents/MacOS/wgtunnel + _run + + RunAtLoad + + KeepAlive + + StandardOutPath + /Library/Logs/wgtunnel/stdout.log + StandardErrorPath + /Library/Logs/wgtunnel/stderr.log + EnvironmentVariables + + WG_TUNNEL_SERVICE + 1 + HOME + /Library/Application Support/wgtunnel + + WorkingDirectory + /Library/Application Support/wgtunnel + ThrottleInterval + 1 + ExitTimeOut + 30 + + \ No newline at end of file diff --git a/daemon/winsw b/daemon/winsw new file mode 160000 index 0000000..8b5db6e --- /dev/null +++ b/daemon/winsw @@ -0,0 +1 @@ +Subproject commit 8b5db6eaa050d31cda12afc6d870855f23180d37 diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..5dbc22c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,7 @@ +#Kotlin +kotlin.code.style=official +kotlin.daemon.jvmargs=-Xmx3072M +#Gradle +org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8 +org.gradle.configuration-cache=true +org.gradle.caching=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d5ed3c8 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,127 @@ +[versions] +app = "1.0.0" +jvm = "21" +androidx-lifecycle = "2.9.6" +composeHotReload = "1.0.0" +compose-plugin = "1.11.0-alpha01" +junit = "4.13.2" +kotlin = "2.3.0" +kotlinx-coroutines = "1.10.2" +material3 = "1.10.0-alpha05" +serialization = "1.9.0" +cryptoRand = "0.6.0" +curve25519Kotlin = "0.0.8" +koin = "4.2.0-beta2" +ktor = "3.3.3" +conveyor = "1.13" +ksp = "2.3.0" +desktopJvm = "1.11.0-alpha01" +jnaPlatform = "5.18.1" +buildconfig = "6.0.7" +retry = "2.0.2" + +moko = "0.25.2" + +# Logging +logbackClassic = "1.5.24" +kermit = "2.0.8" + +picocli = "4.7.7" + +androidx-room = "2.8.4" +androidx-sqlite = "2.6.2" +kstore = "1.0.0" + +lang3 = "3.20.0" + +[bundles] +ktor-client-jvm = ["ktor-client-core-jvm", "ktor-client-cio-jvm", "ktor-client-content-negotiation-jvm", "ktor-serialization-json-jvm", "ktor-client-okhttp", "ktor-client-websockets-jvm"] +ktor-server-jvm = ["ktor-server-cio-jvm", "ktor-serialization-json-jvm", "ktor-server-content-negotiation-jvm", "ktor-server-core-jvm", "ktor-server-websockets-jvm"] + +[libraries] +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +junit = { module = "junit:junit", version.ref = "junit" } +androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose-plugin" } +compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-plugin" } +compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" } +compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-plugin" } +compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-plugin" } +compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "compose-plugin" } +kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } + +# Serialization +kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } +kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "serialization" } + +# cryto +crypto-rand = { module = "org.kotlincrypto.random:crypto-rand", version.ref = "cryptoRand" } +curve25519-kotlin = { module = "io.github.andreypfau:curve25519-kotlin", version.ref = "curve25519Kotlin" } + +# DI +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } + +# Targets +desktop-jvm-linux-x64 = { module = "org.jetbrains.compose.desktop:desktop-jvm-linux-x64", version.ref = "desktopJvm" } +desktop-jvm-macos-arm64 = { module = "org.jetbrains.compose.desktop:desktop-jvm-macos-arm64", version.ref = "desktopJvm" } +desktop-jvm-macos-x64 = { module = "org.jetbrains.compose.desktop:desktop-jvm-macos-x64", version.ref = "desktopJvm" } +desktop-jvm-windows-x64 = { module = "org.jetbrains.compose.desktop:desktop-jvm-windows-x64", version.ref = "desktopJvm" } +desktop-jvm-windows-arm64 = { module = "org.jetbrains.compose.desktop:desktop-jvm-windows-arm64", version.ref = "desktopJvm" } + +# Ktor Server +ktor-server-core-jvm = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" } +ktor-server-cio-jvm = { module = "io.ktor:ktor-server-cio-jvm", version.ref = "ktor" } +ktor-server-content-negotiation-jvm = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktor" } +ktor-server-websockets-jvm = { module = "io.ktor:ktor-server-websockets-jvm", version.ref = "ktor" } + +# Ktor Client +ktor-client-core-jvm = { module = "io.ktor:ktor-client-core-jvm", version.ref = "ktor" } +ktor-client-cio-jvm = { module = "io.ktor:ktor-client-cio-jvm", version.ref = "ktor" } +ktor-client-content-negotiation-jvm = { module = "io.ktor:ktor-client-content-negotiation-jvm", version.ref = "ktor" } +ktor-serialization-json-jvm = { module = "io.ktor:ktor-serialization-kotlinx-json-jvm", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-client-websockets-jvm = { module = "io.ktor:ktor-client-websockets-jvm", version.ref = "ktor" } + +# Coroutines +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } + +# Logging +kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackClassic" } + +# CLI +picocli = { module = "info.picocli:picocli", version.ref = "picocli" } +picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" } + +jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jnaPlatform" } + +# Storage +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } +androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "androidx-sqlite" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" } +kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" } +kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" } + +# Util +apache-commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "lang3" } + +# Resources +moko-core = { module = "dev.icerock.moko:resources", version.ref = "moko" } +moko-compose = { module = "dev.icerock.moko:resources-compose", version.ref = "moko" } + +kotlin-retry = { module = "com.michael-bull.kotlin-retry:kotlin-retry", version.ref = "retry" } + +[plugins] +composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" } +jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } +composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "ksp" } +conveyor = { id = "dev.hydraulic.conveyor", version.ref = "conveyor" } +room = { id = "androidx.room", version.ref = "androidx-room" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" } +buildconfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildconfig"} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ca3b4025d419f1a41c891c025b988190e2c7d0f5 GIT binary patch literal 21668 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 Raufj{+cS=*YpetA{tqO2bB6!` literal 0 HcmV?d00001 diff --git a/keyring/.gitignore b/keyring/.gitignore new file mode 100644 index 0000000..91be53f --- /dev/null +++ b/keyring/.gitignore @@ -0,0 +1,3 @@ +/build +tools/keyring-go/out +src/main/resources/* \ No newline at end of file diff --git a/keyring/build.gradle.kts b/keyring/build.gradle.kts new file mode 100644 index 0000000..98bd9d6 --- /dev/null +++ b/keyring/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + kotlin("jvm") +} + +dependencies { + implementation(libs.jna.platform) +} + +tasks.register("buildGoLibs") { + val libDir = "tools/keyring-go" + group = "build" + description = "Builds Go shared libs using Makefile" + workingDir = file(libDir) + + inputs.dir(file(libDir)) + .withPropertyName("goSourceDir") + .withPathSensitivity(PathSensitivity.RELATIVE) + + outputs.dir(file("src/main/resources")) + .withPropertyName("outputResourcesDir") + + commandLine("make", "all") +} + +tasks.named("processResources") { + dependsOn("buildGoLibs") +} + +val cleanGoLibs = tasks.register("cleanGoLibs") { + workingDir = file("tools/keyring-go") + commandLine("make", "clean") +} + +tasks.named("clean") { + dependsOn(cleanGoLibs) + delete(file("tools/keyring-go/out")) + delete(file("src/main/resources")) +} diff --git a/keyring/src/main/kotlin/com/zaneschepke/wireguardautotunnel/keyring/Keyring.kt b/keyring/src/main/kotlin/com/zaneschepke/wireguardautotunnel/keyring/Keyring.kt new file mode 100644 index 0000000..972d3fd --- /dev/null +++ b/keyring/src/main/kotlin/com/zaneschepke/wireguardautotunnel/keyring/Keyring.kt @@ -0,0 +1,29 @@ +package com.zaneschepke.wireguardautotunnel.keyring + +import com.sun.jna.Native +import com.sun.jna.Pointer + +class Keyring(private val service: String) { + + private val native = NativeKeyring.INSTANCE + + fun put(name: String, value: String) { + val result = native.storeSecret(service, name, value) + check(result == 1) { + "Failed to store secret: $name" + } + } + + fun get(name: String): String? { + val ptr: Pointer = native.getSecret(service, name) ?: return null + return try { + ptr.getString(0) + } finally { + Native.free(Pointer.nativeValue(ptr)) + } + } + + fun delete(name: String) { + native.deleteSecret(service, name) + } +} diff --git a/keyring/src/main/kotlin/com/zaneschepke/wireguardautotunnel/keyring/NativeKeyring.kt b/keyring/src/main/kotlin/com/zaneschepke/wireguardautotunnel/keyring/NativeKeyring.kt new file mode 100644 index 0000000..0956541 --- /dev/null +++ b/keyring/src/main/kotlin/com/zaneschepke/wireguardautotunnel/keyring/NativeKeyring.kt @@ -0,0 +1,29 @@ +package com.zaneschepke.wireguardautotunnel.keyring + +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.Pointer + +interface NativeKeyring : Library { + + fun storeSecret( + service: String, + name: String, + value: String + ): Int + + fun getSecret( + service: String, + name: String + ): Pointer? + + fun deleteSecret( + service: String, + name: String + ): Int + + companion object { + val INSTANCE: NativeKeyring = + Native.load("keyring", NativeKeyring::class.java) + } +} diff --git a/keyring/tools/keyring-go/Makefile b/keyring/tools/keyring-go/Makefile new file mode 100644 index 0000000..04e7a59 --- /dev/null +++ b/keyring/tools/keyring-go/Makefile @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: Apache-2.0 + +DESTDIR ?= $(CURDIR)/out +RESOURCEDIR ?= $(CURDIR)/../../src/main/resources +HOST_OS := $(shell uname -s | tr '[:upper:]' '[:lower:]') + +ifeq ($(HOST_OS),darwin) + PLATFORMS := darwin-amd64 darwin-arm64 linux-amd64 windows-amd64 windows-arm64 +else + PLATFORMS := linux-amd64 windows-amd64 +endif + +export CGO_ENABLED := 1 + +# Standard Go build command with -a (force rebuild) +# Added -v so you can see the progress in Gradle logs +GOBUILD := go build -a -v -buildmode=c-shared -trimpath -ldflags="-buildid=" + +default: all + +# Macro to keep the build targets clean +# Usage: $(call go-build,GOOS,GOARCH,CC,OUTPUT) +define go-build + @mkdir -p "$(DESTDIR)" + GOOS=$(1) GOARCH=$(2) CC=$(3) $(GOBUILD) -o "$(4)" . +endef + +$(DESTDIR)/libkeyring-linux-amd64.so: $(wildcard *.go) + $(call go-build,linux,amd64,gcc,$@) + +$(DESTDIR)/libkeyring-windows-amd64.dll: $(wildcard *.go) + $(call go-build,windows,amd64,x86_64-w64-mingw32-gcc,$@) + +$(DESTDIR)/libkeyring-windows-arm64.dll: $(wildcard *.go) + $(call go-build,windows,arm64,aarch64-w64-mingw32-gcc,$@) + +$(DESTDIR)/libkeyring-darwin-amd64.dylib: $(wildcard *.go) + $(call go-build,darwin,amd64,clang,$@) + +$(DESTDIR)/libkeyring-darwin-arm64.dylib: $(wildcard *.go) + $(call go-build,darwin,arm64,clang,$@) + +build_all: $(foreach plat,$(PLATFORMS),$(DESTDIR)/libkeyring-$(plat).$(if $(findstring windows,$(plat)),dll,$(if $(findstring darwin,$(plat)),dylib,so))) + +copy_to_resources: build_all + @for plat in $(PLATFORMS); do \ + os=`echo "$$plat" | cut -d- -f1`; \ + arch=`echo "$$plat" | cut -d- -f2`; \ + jna_dir=; \ + if [ "$$os" = "linux" ] && [ "$$arch" = "amd64" ]; then jna_dir=linux-x86-64; fi; \ + if [ "$$os" = "windows" ] && [ "$$arch" = "amd64" ]; then jna_dir=win32-x86-64; fi; \ + if [ "$$os" = "windows" ] && [ "$$arch" = "arm64" ]; then jna_dir=win32-aarch64; fi; \ + if [ "$$os" = "darwin" ] && [ "$$arch" = "amd64" ]; then jna_dir=darwin-x86-64; fi; \ + if [ "$$os" = "darwin" ] && [ "$$arch" = "arm64" ]; then jna_dir=darwin-aarch64; fi; \ + libext=so; \ + if [ "$$os" = "windows" ]; then libext=dll; fi; \ + if [ "$$os" = "darwin" ]; then libext=dylib; fi; \ + dest_dir="$(RESOURCEDIR)/$$jna_dir"; \ + mkdir -p "$$dest_dir"; \ + cp "$(DESTDIR)/libkeyring-$$plat.$$libext" "$$dest_dir/libkeyring.$$libext"; \ + echo "Copied $$plat -> $$dest_dir/libkeyring.$$libext"; \ + done + +all: copy_to_resources + +clean: + rm -rf "$(DESTDIR)" + +.PHONY: default all build_all copy_to_resources clean +.DELETE_ON_ERROR: \ No newline at end of file diff --git a/keyring/tools/keyring-go/go.mod b/keyring/tools/keyring-go/go.mod new file mode 100644 index 0000000..4cfa222 --- /dev/null +++ b/keyring/tools/keyring-go/go.mod @@ -0,0 +1,12 @@ +module github.com/wgtunnel/desktop/keyring + +go 1.25.5 + +require github.com/zalando/go-keyring v0.2.6 + +require ( + al.essio.dev/pkg/shellescape v1.5.1 // indirect + github.com/danieljoos/wincred v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + golang.org/x/sys v0.26.0 // indirect +) diff --git a/keyring/tools/keyring-go/go.sum b/keyring/tools/keyring-go/go.sum new file mode 100644 index 0000000..8c1e0e0 --- /dev/null +++ b/keyring/tools/keyring-go/go.sum @@ -0,0 +1,22 @@ +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= +github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/keyring/tools/keyring-go/keyring.go b/keyring/tools/keyring-go/keyring.go new file mode 100644 index 0000000..02d6dd7 --- /dev/null +++ b/keyring/tools/keyring-go/keyring.go @@ -0,0 +1,75 @@ +package main + +/* +#include +*/ +import "C" + +import ( + "errors" + + "github.com/zalando/go-keyring" +) + +//export storeSecret +func storeSecret(service *C.char, name *C.char, value *C.char) C.int { + if service == nil || name == nil || value == nil { + return C.int(-1) + } + + err := keyring.Set( + C.GoString(service), + C.GoString(name), + C.GoString(value), + ) + + if err != nil { + return C.int(-1) + } + + return C.int(1) +} + +//export getSecret +func getSecret(service *C.char, name *C.char) *C.char { + if service == nil || name == nil { + return nil + } + + value, err := keyring.Get( + C.GoString(service), + C.GoString(name), + ) + + if err != nil { + if errors.Is(err, keyring.ErrNotFound) { + return nil + } + return nil + } + + return C.CString(value) +} + +//export deleteSecret +func deleteSecret(service *C.char, name *C.char) C.int { + if service == nil || name == nil { + return C.int(-1) + } + + err := keyring.Delete( + C.GoString(service), + C.GoString(name), + ) + + if err != nil { + if errors.Is(err, keyring.ErrNotFound) { + return C.int(-1) + } + return C.int(-1) + } + + return C.int(1) +} + +func main() {} diff --git a/parser/build.gradle.kts b/parser/build.gradle.kts new file mode 100644 index 0000000..f4d3fbf --- /dev/null +++ b/parser/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + kotlin("jvm") + alias(libs.plugins.serialization) +} + +dependencies { + testImplementation(kotlin("test")) + + implementation(libs.kotlinx.serialization.core) + + implementation(libs.crypto.rand) + implementation(libs.curve25519.kotlin) +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/Config.kt b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/Config.kt new file mode 100644 index 0000000..b4916d4 --- /dev/null +++ b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/Config.kt @@ -0,0 +1,157 @@ +package com.zaneschepke.wireguardautotunnel.parser + +import com.zaneschepke.wireguardautotunnel.parser.crypto.Key +import com.zaneschepke.wireguardautotunnel.parser.util.getBool +import com.zaneschepke.wireguardautotunnel.parser.util.getInt +import com.zaneschepke.wireguardautotunnel.parser.util.getList +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Config( + @SerialName("Interface") val `interface`: InterfaceSection, + @SerialName("Peer") val peers: List = emptyList() +) { + + @Throws(ConfigParseException::class) + fun validate() { + `interface`.validate() + peers.forEachIndexed { index, peer -> peer.validate(index) } + } + + fun asQuickString(): String = buildString { + appendLine("[Interface]") + appendLine("PrivateKey = ${`interface`.privateKey}") + `interface`.address?.let { appendLine("Address = $it") } + `interface`.dns?.let { appendLine("DNS = $it") } + `interface`.listenPort?.let { appendLine("ListenPort = $it") } + `interface`.mtu?.let { appendLine("MTU = $it") } + `interface`.fwMark?.let { appendLine("FwMark = $it") } + `interface`.table?.let { appendLine("Table = $it") } + `interface`.saveConfig?.let { appendLine("SaveConfig = $it") } + + // AmneziaWG + `interface`.jC?.let { appendLine("Jc = $it") } + `interface`.jMin?.let { appendLine("Jmin = $it") } + `interface`.jMax?.let { appendLine("Jmax = $it") } + `interface`.s1?.let { appendLine("S1 = $it") } + `interface`.s2?.let { appendLine("S2 = $it") } + `interface`.s3?.let { appendLine("S3 = $it") } + `interface`.s4?.let { appendLine("S4 = $it") } + `interface`.h1?.let { appendLine("H1 = $it") } + `interface`.h2?.let { appendLine("H2 = $it") } + `interface`.h3?.let { appendLine("H3 = $it") } + `interface`.h4?.let { appendLine("H4 = $it") } + `interface`.i1?.let { appendLine("I1 = $it") } + `interface`.i2?.let { appendLine("I2 = $it") } + `interface`.i3?.let { appendLine("I3 = $it") } + `interface`.i4?.let { appendLine("I4 = $it") } + `interface`.i5?.let { appendLine("I5 = $it") } + + `interface`.includedApplications?.let { appendLine("IncludedApplications = ${it.joinToString(",")}") } + `interface`.excludedApplications?.let { appendLine("ExcludedApplications = ${it.joinToString(",")}") } + + peers.forEach { peer -> + append("\n[Peer]\n") + appendLine("PublicKey = ${peer.publicKey}") + peer.endpoint?.let { appendLine("Endpoint = $it") } + peer.allowedIPs?.let { appendLine("AllowedIPs = $it") } + peer.presharedKey?.let { appendLine("PresharedKey = $it") } + peer.persistentKeepalive?.let { appendLine("PersistentKeepalive = $it") } + } + }.trim() + + fun rotateInterfaceKey(): Config { + val privateKey = Key.generatePrivateKey() + val newInterface = `interface`.copy(privateKey = privateKey.toBase64()) + return copy(`interface` = newInterface) + } + + companion object { + @Throws(ConfigParseException::class) + fun parseQuickString(configString: String): Config { + val interfaceMap = mutableMapOf() + val peerMaps = mutableListOf>() + var currentSection: MutableMap? = null + + configString.lines().forEach { line -> + val trimmed = line.split("#", ";")[0].trim() + if (trimmed.isEmpty()) return@forEach + + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + currentSection = when (trimmed.substring(1, trimmed.length - 1).lowercase()) { + "interface" -> interfaceMap + "peer" -> mutableMapOf().also { peerMaps.add(it) } + else -> null // ignore unknown + } + return@forEach + } + + val parts = trimmed.split("=", limit = 2) + if (parts.size == 2) { + currentSection?.put(parts[0].trim(), parts[1].trim()) + } + } + + if (interfaceMap.isEmpty()) throw ConfigParseException(ErrorType.MISSING_REQUIRED_FIELD, "Interface") + + return Config( + `interface` = buildInterface(interfaceMap), + peers = peerMaps.map { buildPeer(it) } + ).also { it.validate() } + } + + private fun buildInterface(m: Map) = InterfaceSection( + privateKey = m["PrivateKey"] ?: "", + address = m["Address"], + dns = m["DNS"], + listenPort = m.getInt("ListenPort", "Interface"), + mtu = m.getInt("MTU", "Interface"), + fwMark = m.getInt("FwMark", "Interface"), + table = m["Table"], + saveConfig = m.getBool("SaveConfig", "Interface"), + jC = m.getInt("Jc", "Interface"), + jMin = m.getInt("Jmin", "Interface"), + jMax = m.getInt("Jmax", "Interface"), + s1 = m.getInt("S1", "Interface"), + s2 = m.getInt("S2", "Interface"), + s3 = m.getInt("S3", "Interface"), + s4 = m.getInt("S4", "Interface"), + h1 = m["H1"], h2 = m["H2"], h3 = m["H3"], h4 = m["H4"], + i1 = m["I1"], i2 = m["I2"], i3 = m["I3"], i4 = m["I4"], i5 = m["I5"], + includedApplications = m.getList("IncludedApplications"), + excludedApplications = m.getList("ExcludedApplications") + ) + + private fun buildPeer(m: Map) = PeerSection( + publicKey = m["PublicKey"] ?: "", + allowedIPs = m["AllowedIPs"], + endpoint = m["Endpoint"], + presharedKey = m["PresharedKey"], + persistentKeepalive = m.getInt("PersistentKeepalive", "Peer") + ) + + fun parseEndpoint(endpoint: String): Pair { + var host: String + var portStr: String? + if (endpoint.startsWith("[")) { + val endBracket = endpoint.lastIndexOf("]") + if (endBracket == -1 || !endpoint.substring(endBracket + 1).startsWith(":")) return null to null + host = endpoint.take(endBracket + 1) + portStr = endpoint.substring(endBracket + 2) + } else { + val parts = endpoint.split(":", limit = 2) + if (parts.size != 2) return null to null + host = parts[0] + portStr = parts[1] + } + return host to portStr + } + + internal fun generatePublicKeyFromPrivate(privateBase64: String): String { + val privateKey = Key.fromBase64(privateBase64) + val publicKey = Key.generatePublicKey(privateKey) + return publicKey.toBase64() + } + } +} \ No newline at end of file diff --git a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/ConfigParseException.kt b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/ConfigParseException.kt new file mode 100644 index 0000000..f78a64c --- /dev/null +++ b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/ConfigParseException.kt @@ -0,0 +1,9 @@ +package com.zaneschepke.wireguardautotunnel.parser + +class ConfigParseException( + val errorType: ErrorType, + val field: String, + val value: Any? = null, + val extra: String? = null, + message: String = "$field: $errorType${value?.let { " (value: $it)" } ?: ""}${extra?.let { " ($it)" } ?: ""}" +) : Exception(message) \ No newline at end of file diff --git a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/ErrorType.kt b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/ErrorType.kt new file mode 100644 index 0000000..68482a7 --- /dev/null +++ b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/ErrorType.kt @@ -0,0 +1,22 @@ +package com.zaneschepke.wireguardautotunnel.parser + +enum class ErrorType { + MISSING_REQUIRED_FIELD, + INVALID_BASE64_KEY, + INVALID_PORT_RANGE, + INVALID_MTU_RANGE, + INVALID_FWMARK, + INVALID_JC_RANGE, + INVALID_JMIN_JMAX_ORDER, + INVALID_JMAX_MTU, + INVALID_PADDING_NEGATIVE, + INVALID_HEADER_FORMAT, + INVALID_SIGNATURE_FORMAT, + INVALID_ENDPOINT_FORMAT, + INVALID_KEEPALIVE_NEGATIVE, + INVALID_CIDR, + INVALID_IP, + INVALID_HOSTNAME, + INVALID_DNS_ENTRY, + INVALID_VALUE_FORMAT +} \ No newline at end of file diff --git a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/InterfaceSection.kt b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/InterfaceSection.kt new file mode 100644 index 0000000..d99edb9 --- /dev/null +++ b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/InterfaceSection.kt @@ -0,0 +1,84 @@ +package com.zaneschepke.wireguardautotunnel.parser + +import com.zaneschepke.wireguardautotunnel.parser.util.NetworkUtils +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class InterfaceSection( + @SerialName("PrivateKey") val privateKey: String, + @SerialName("Address") val address: String? = null, + @SerialName("ListenPort") val listenPort: Int? = null, + @SerialName("DNS") val dns: String? = null, + @SerialName("MTU") val mtu: Int? = null, + // Linux + @SerialName("FwMark") val fwMark: Int? = null, + @SerialName("Table") val table: String? = null, + @SerialName("SaveConfig") val saveConfig: Boolean? = null, + // Desktop or Rooted Android + @SerialName("PreUp") val preUp: String? = null, + @SerialName("PostUp") val postUp: String? = null, + @SerialName("PreDown") val preDown: String? = null, + @SerialName("PostDown") val postDown: String? = null, + // Android + @SerialName("IncludedApplications") val includedApplications: List? = null, + @SerialName("ExcludedApplications") val excludedApplications: List? = null, + // Amnezia + @SerialName("Jc") val jC: Int? = null, + @SerialName("Jmin") val jMin: Int? = null, + @SerialName("Jmax") val jMax: Int? = null, + @SerialName("S1") val s1: Int? = null, + @SerialName("S2") val s2: Int? = null, + @SerialName("S3") val s3: Int? = null, + @SerialName("S4") val s4: Int? = null, + @SerialName("H1") val h1: String? = null, + @SerialName("H2") val h2: String? = null, + @SerialName("H3") val h3: String? = null, + @SerialName("H4") val h4: String? = null, + @SerialName("I1") val i1: String? = null, + @SerialName("I2") val i2: String? = null, + @SerialName("I3") val i3: String? = null, + @SerialName("I4") val i4: String? = null, + @SerialName("I5") val i5: String? = null, +) { + val publicKey: String = Config.generatePublicKeyFromPrivate(privateKey) + + @Throws(ConfigParseException::class) + fun validate() { + if (privateKey.isBlank()) throw ConfigParseException(ErrorType.MISSING_REQUIRED_FIELD, "Interface.PrivateKey") + if (!NetworkUtils.isValidBase64(privateKey)) throw ConfigParseException(ErrorType.INVALID_BASE64_KEY, "Interface.PrivateKey", privateKey) + + listenPort?.let { if (it !in 0..65535) throw ConfigParseException(ErrorType.INVALID_PORT_RANGE, "Interface.ListenPort", it) } + mtu?.let { if (it !in 576..9000) throw ConfigParseException(ErrorType.INVALID_MTU_RANGE, "Interface.MTU", it) } + fwMark?.let { if (it < 0) throw ConfigParseException(ErrorType.INVALID_FWMARK, "Interface.FwMark", it) } + + jC?.let { if (it !in 4..12) throw ConfigParseException(ErrorType.INVALID_JC_RANGE, "Interface.Jc", it) } + if (jMin != null && jMax != null) { + if (jMin > jMax) throw ConfigParseException(ErrorType.INVALID_JMIN_JMAX_ORDER, "Interface.Jmin/Jmax") + if (jMax >= (mtu ?: 1500)) throw ConfigParseException(ErrorType.INVALID_JMAX_MTU, "Interface.Jmax", jMax) + } + + listOf(s1, s2, s3, s4).forEachIndexed { i, s -> + if (s != null && s < 0) throw ConfigParseException(ErrorType.INVALID_PADDING_NEGATIVE, "Interface.S${i + 1}", s) + } + + listOf(h1, h2, h3, h4).forEachIndexed { i, h -> + if (h != null && !NetworkUtils.isValidAmneziaHeader(h)) { + throw ConfigParseException(ErrorType.INVALID_HEADER_FORMAT, "Interface.H${i + 1}", h) + } + } + + listOf(i1, i2, i3, i4, i5).forEachIndexed { i, sig -> + if (sig != null && !NetworkUtils.isValidHexSignature(sig)) { + throw ConfigParseException(ErrorType.INVALID_SIGNATURE_FORMAT, "Interface.I${i + 1}", sig) + } + } + + address?.split(",")?.map { it.trim() }?.forEach { + if (it.isNotBlank() && !NetworkUtils.isValidCidr(it)) throw ConfigParseException(ErrorType.INVALID_CIDR, "Interface.Address", it) + } + dns?.split(",")?.map { it.trim() }?.forEach { + if (it.isNotBlank() && !NetworkUtils.isValidDnsEntry(it)) throw ConfigParseException(ErrorType.INVALID_DNS_ENTRY, "Interface.DNS", it) + } + } +} \ No newline at end of file diff --git a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/PeerSection.kt b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/PeerSection.kt new file mode 100644 index 0000000..f82a327 --- /dev/null +++ b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/PeerSection.kt @@ -0,0 +1,53 @@ +package com.zaneschepke.wireguardautotunnel.parser + +import com.zaneschepke.wireguardautotunnel.parser.util.NetworkUtils +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PeerSection( + @SerialName("PublicKey") val publicKey: String, + @SerialName("AllowedIPs") val allowedIPs: String? = null, + @SerialName("Endpoint") val endpoint: String? = null, + @SerialName("PresharedKey") val presharedKey: String? = null, + @SerialName("PersistentKeepalive") val persistentKeepalive: Int? = null +) { + + @Throws(ConfigParseException::class) + fun validate(index: Int) { + val prefix = "Peer[$index]" + if (publicKey.isBlank()) throw ConfigParseException(ErrorType.MISSING_REQUIRED_FIELD, "$prefix.PublicKey") + if (!NetworkUtils.isValidBase64(publicKey)) throw ConfigParseException(ErrorType.INVALID_BASE64_KEY, "$prefix.PublicKey", publicKey) + + persistentKeepalive?.let { if (it !in 0..65535) throw ConfigParseException(ErrorType.INVALID_KEEPALIVE_NEGATIVE, "$prefix.PersistentKeepalive", it) } + + endpoint?.let { + val (host, portStr) = Config.parseEndpoint(it) + val port = portStr?.toIntOrNull() + if (host == null || port == null || port !in 0..65535) { + throw ConfigParseException(ErrorType.INVALID_ENDPOINT_FORMAT, "$prefix.Endpoint", it) + } + if (!NetworkUtils.isValidDnsEntry(host)) { + throw ConfigParseException(ErrorType.INVALID_HOSTNAME, "$prefix.Endpoint host", host) + } + } + + allowedIPs?.split(",")?.map { it.trim() }?.forEach { + if (it.isNotBlank() && !NetworkUtils.isValidCidr(it)) { + throw ConfigParseException(ErrorType.INVALID_CIDR, "$prefix.AllowedIPs", it) + } + } + } + + val host: String? get() { + val (h, _) = endpoint?.let { Config.parseEndpoint(it) } ?: return null + return h + } + + val port: Int? get() { + val (_, p) = endpoint?.let { Config.parseEndpoint(it) } ?: return null + return p?.toIntOrNull() + } + val isStaticallyConfigured: Boolean + get() = host?.let { NetworkUtils.isValidIp(it) } ?: false +} \ No newline at end of file diff --git a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/crypto/Key.kt b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/crypto/Key.kt new file mode 100644 index 0000000..8e96ead --- /dev/null +++ b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/crypto/Key.kt @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2026 WG Tunnel. +// Adapted from WireGuard LLC. + +package com.zaneschepke.wireguardautotunnel.parser.crypto + +import io.github.andreypfau.curve25519.x25519.X25519 +import org.kotlincrypto.random.CryptoRand +import kotlin.experimental.and +import kotlin.experimental.or + +class KeyFormatException : Exception { + constructor(format: Key.Format, type: Key.Type) : super("Invalid key format: $format, type: $type") +} + +class Key private constructor(private val key: ByteArray) { + + fun getBytes(): ByteArray = key.copyOf() + + fun toBase64(): String { + val output = CharArray(Format.BASE64.length) + var i = 0 + while (i < key.size / 3) { + encodeBase64(key, i * 3, output, i * 4) + i++ + } + val endSegment = byteArrayOf(key[i * 3], key[i * 3 + 1], 0) + encodeBase64(endSegment, 0, output, i * 4) + output[Format.BASE64.length - 1] = '=' + return output.concatToString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Key) return false + return key.contentEquals(other.key) + } + + override fun hashCode(): Int { + var ret = 0 + var i = 0 + while (i < key.size / 4) { + ret = ret xor ((key[i * 4 + 0].toInt() shr 0) + (key[i * 4 + 1].toInt() shr 8) + + (key[i * 4 + 2].toInt() shr 16) + (key[i * 4 + 3].toInt() shr 24)) + i++ + } + return ret + } + + companion object { + fun fromBase64(str: String): Key { + val input = str.toCharArray() + if (input.size != Format.BASE64.length || input[Format.BASE64.length - 1] != '=') { + throw KeyFormatException(Format.BASE64, Type.LENGTH) + } + val key = ByteArray(Format.BINARY.length) + var ret = 0 + var i = 0 + while (i < key.size / 3) { + val value = decodeBase64(input, i * 4) + ret = ret or (value ushr 31) + key[i * 3] = ((value ushr 16) and 0xff).toByte() + key[i * 3 + 1] = ((value ushr 8) and 0xff).toByte() + key[i * 3 + 2] = (value and 0xff).toByte() + i++ + } + val endSegment = charArrayOf(input[i * 4], input[i * 4 + 1], input[i * 4 + 2], 'A') + val value = decodeBase64(endSegment, 0) + ret = ret or ((value ushr 31) or (value and 0xff)) + key[i * 3] = ((value ushr 16) and 0xff).toByte() + key[i * 3 + 1] = ((value ushr 8) and 0xff).toByte() + + if (ret != 0) { + throw KeyFormatException(Format.BASE64, Type.CONTENTS) + } + return Key(key) + } + + fun fromBytes(bytes: ByteArray): Key { + if (bytes.size != Format.BINARY.length) { + throw KeyFormatException(Format.BINARY, Type.LENGTH) + } + return Key(bytes) + } + + fun generatePrivateKey(): Key { + val privateKey = ByteArray(Format.BINARY.length) + CryptoRand.nextBytes(privateKey) + privateKey[0] = privateKey[0] and 248.toByte() + privateKey[31] = privateKey[31] and 127.toByte() + privateKey[31] = privateKey[31] or 64.toByte() + return Key(privateKey) + } + + fun generatePublicKey(privateKey: Key): Key { + val publicKey = ByteArray(Format.BINARY.length) + X25519.x25519(privateKey.getBytes(), output = publicKey) + return Key(publicKey) + } + + private fun decodeBase64(src: CharArray, srcOffset: Int): Int { + var value = 0 + for (i in 0 until 4) { + val c = src[i + srcOffset].code + value = value or (-1 + + ((((('A'.code - 1) - c) and (c - ('Z'.code + 1))) ushr 8) and (c - 64)) + + ((((('a'.code - 1) - c) and (c - ('z'.code + 1))) ushr 8) and (c - 70)) + + ((((('0'.code - 1) - c) and (c - ('9'.code + 1))) ushr 8) and (c + 5)) + + (((('+'.code - 1) - c) and (c - ('+'.code + 1))) ushr 8 and 63) + + (((('/'.code - 1) - c) and (c - ('/'.code + 1))) ushr 8 and 64) + ) shl (18 - 6 * i) + } + return value + } + + private fun encodeBase64(src: ByteArray, srcOffset: Int, dest: CharArray, destOffset: Int) { + val input = byteArrayOf( + (src[srcOffset].toInt() shr 2 and 63).toByte(), + ((src[srcOffset].toInt() shl 4 or (src[1 + srcOffset].toInt() and 0xff ushr 4)) and 63).toByte(), + ((src[1 + srcOffset].toInt() shl 2 or (src[2 + srcOffset].toInt() and 0xff ushr 6)) and 63).toByte(), + (src[2 + srcOffset].toInt() and 63).toByte() + ) + for (i in 0 until 4) { + dest[i + destOffset] = (input[i].toInt() + 'A'.code + + (((25 - input[i].toInt()) ushr 8) and 6) - + (((51 - input[i].toInt()) ushr 8) and 75) - + (((61 - input[i].toInt()) ushr 8) and 15) + + (((62 - input[i].toInt()) ushr 8) and 3)).toChar() + } + } + } + + enum class Format(val length: Int) { + BASE64(44), + BINARY(32), + HEX(64) + } + + enum class Type { + LENGTH, + CONTENTS + } +} \ No newline at end of file diff --git a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/util/Extensions.kt b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/util/Extensions.kt new file mode 100644 index 0000000..efa4620 --- /dev/null +++ b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/util/Extensions.kt @@ -0,0 +1,22 @@ +package com.zaneschepke.wireguardautotunnel.parser.util + +import com.zaneschepke.wireguardautotunnel.parser.ConfigParseException +import com.zaneschepke.wireguardautotunnel.parser.ErrorType + +fun Map.getInt(key: String, section: String): Int? { + val value = this[key] ?: return null + return value.toIntOrNull() ?: throw ConfigParseException(ErrorType.INVALID_VALUE_FORMAT, "$section.$key", value) +} + +fun Map.getBool(key: String, section: String): Boolean? { + val value = this[key] ?: return null + return when (value.lowercase()) { + "true", "yes", "on" -> true + "false", "no", "off" -> false + else -> throw ConfigParseException(ErrorType.INVALID_VALUE_FORMAT, "$section.$key", value) + } +} + +fun Map.getList(key: String): List? { + return this[key]?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } +} \ No newline at end of file diff --git a/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/util/NetworkUtils.kt b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/util/NetworkUtils.kt new file mode 100644 index 0000000..58c0015 --- /dev/null +++ b/parser/src/main/kotlin/com/zaneschepke/wireguardautotunnel/parser/util/NetworkUtils.kt @@ -0,0 +1,82 @@ +package com.zaneschepke.wireguardautotunnel.parser.util + +import java.net.InetAddress + +object NetworkUtils { + private val hostnameRegex = Regex("^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])*$") + + + fun isValidIp(ip: String): Boolean { + val sanitized = ip.removeSurrounding("[", "]") + if (sanitized.any { it.lowercaseChar() in 'g'..'z' }) return false + + return try { + InetAddress.getAllByName(sanitized).isNotEmpty() + } catch (e: Exception) { + false + } + } + + fun isValidCidr(cidr: String): Boolean { + val parts = cidr.split("/", limit = 2) + val ip = parts[0] + + if (parts.size == 1) { + return isValidIp(ip) + } + + val prefix = parts[1].toIntOrNull() ?: return false + if (!isValidIp(ip)) return false + + return try { + val addr = InetAddress.getByName(ip.removeSurrounding("[", "]")) + val maxPrefix = if (addr is java.net.Inet4Address) 32 else 128 + prefix in 0..maxPrefix + } catch (e: Exception) { + false + } + } + + fun isValidDnsEntry(entry: String): Boolean { + if (entry.isBlank()) return false + // Safe: isValidIp is offline, isValidHostname is regex. + return isValidIp(entry) || isValidHostname(entry) + } + + + fun isValidHostname(host: String): Boolean { + val cleaned = host.removeSurrounding("[", "]") + return hostnameRegex.matches(cleaned) && cleaned.length <= 253 + } + + fun isValidBase64(str: String): Boolean { + // WireGuard keys are always 44 chars (32 bytes encoded) + if (str.length != 44 || !str.endsWith("=")) return false + val base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" + return str.all { it in base64Chars } + } + + fun isValidAmneziaHeader(header: String): Boolean { + val maxUInt32 = 4294967295L + return try { + if (header.contains("-")) { + val parts = header.split("-") + if (parts.size != 2) return false + val start = parts[0].trim().toLong() + val end = parts[1].trim().toLong() + start in 0..maxUInt32 && end in 0..maxUInt32 && start <= end + } else { + header.trim().toLong() in 0..maxUInt32 + } + } catch (_: Exception) { + false + } + } + + fun isValidHexSignature(signature: String): Boolean { + val hex = signature.removePrefix("0x").trim() + if (hex.isEmpty() || hex.length % 2 != 0) return false + val hexChars = "0123456789abcdefABCDEF" + return hex.all { it in hexChars } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..2ba0702 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,24 @@ +rootProject.name = "wgtunnel" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + maven("https://maven.hq.hydraulic.software") + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +include(":composeApp", ":parser", ":daemon", ":tunnel", ":cli", ":client", ":keyring", ":core") diff --git a/tunnel/.gitignore b/tunnel/.gitignore new file mode 100644 index 0000000..e5d0f92 --- /dev/null +++ b/tunnel/.gitignore @@ -0,0 +1,3 @@ +src/main/resources/* +/tools/libwg-go/build +/tools/libwg-go/out \ No newline at end of file diff --git a/tunnel/build.gradle.kts b/tunnel/build.gradle.kts new file mode 100644 index 0000000..d8034c2 --- /dev/null +++ b/tunnel/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + kotlin("jvm") +} + +dependencies { + testImplementation(kotlin("test")) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.jna.platform) +} + +tasks.test { + useJUnitPlatform() +} + + +tasks.register("buildGoLibs") { + val goDir = "tools/libwg-go" + group = "build" + description = "Builds Go shared libs using Makefile" + workingDir = file(goDir) + + // Track only source files + inputs.files( + fileTree(goDir) { + include("**/*.go", "**/go.mod", "**/go.sum", "Makefile") + exclude("out/**", "build/**", ".gocache/**") + } + ).withPropertyName("goSourceFiles") + .withPathSensitivity(PathSensitivity.RELATIVE) + + outputs.dir(file("src/main/resources")) + .withPropertyName("outputResourcesDir") + + commandLine("make", "all") +} + +tasks.named("processResources") { + dependsOn("buildGoLibs") +} + +val cleanGoLibs = tasks.register("cleanGoLibs") { + workingDir = file("tools/libwg-go") + commandLine("make", "clean") +} + +// 3. Update the main clean task +tasks.named("clean") { + dependsOn(cleanGoLibs) + delete(file("tools/libwg-go/build")) + delete(file("tools/libwg-go/out")) + delete(file("src/main/resources")) +} diff --git a/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/AmneziaBackend.kt b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/AmneziaBackend.kt new file mode 100644 index 0000000..4ec45e5 --- /dev/null +++ b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/AmneziaBackend.kt @@ -0,0 +1,141 @@ +package com.zaneschepke.wireguardautotunnel.tunnel + +import com.zaneschepke.wireguardautotunnel.tunnel.native.AwgTunnel +import com.zaneschepke.wireguardautotunnel.tunnel.native.StatusCodeCallback +import com.zaneschepke.wireguardautotunnel.tunnel.util.BackendException +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.util.concurrent.ConcurrentHashMap +import kotlin.concurrent.withLock + +class AmneziaBackend : Backend { + private val tun = AwgTunnel.INSTANCE + + private var currentMode: Backend.Mode = Backend.Mode.Userspace + + private val _status = MutableStateFlow(Backend.Status(false, currentMode, emptyMap())) + + override val status: Flow = _status.asStateFlow() + + private val tunnelHandles = ConcurrentHashMap() + private val tunnelJobs = ConcurrentHashMap() + + private val backendScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + init { + initKillSwitchStatus() + } + + private fun initKillSwitchStatus() { + val status = tun.getKillSwitchStatus() + val enabled = status == 1 + _status.update { it.copy(killSwitchEnabled = enabled) } + } + + @Synchronized + override fun start(tunnel: Tunnel, config: String): Result = runCatching { + if (_status.value.activeTunnels.any { it.key.id == tunnel.id }) { + return Result.success(Unit) + } + + tunnel.updateState(Tunnel.State.Starting) + _status.update { it.copy(activeTunnels = it.activeTunnels + (tunnel to Tunnel.State.Starting)) } + + val statusFlow = callbackFlow { + val statusCallback = object : StatusCodeCallback { + override fun onTunnelStatusCode(handle: Int, statusCode: Int) { + trySend(statusCode) + } + } + + val handle = when(currentMode) { + Backend.Mode.Proxy -> tun.awgProxyTurnOn(config, statusCallback) + Backend.Mode.Userspace -> tun.awgTurnOn(config, statusCallback) + } + + if (handle < 0) { + close(BackendException.BackendFailure(IllegalStateException("Tunnel failed to start with handle: $handle"))) + tunnel.updateState(Tunnel.State.Down) + _status.update { it.copy(activeTunnels = it.activeTunnels - tunnel) } + } else { + tunnelHandles[tunnel] = handle + _status.update { it.copy(activeTunnels = it.activeTunnels + (tunnel to Tunnel.State.Up.Unknown)) } + } + awaitCancellation() + }.buffer(Channel.BUFFERED) + + tunnelJobs[tunnel] = backendScope.launch { + statusFlow.collect { statusCode -> + val tunnelState = mapStatusCodeToState(statusCode) + _status.update { it.copy(activeTunnels = it.activeTunnels + (tunnel to tunnelState)) } + tunnel.updateState(tunnelState) + } + }.apply { invokeOnCompletion { + tunnelJobs.remove(tunnel) + } } + }.onFailure { throwable -> + tunnel.updateState(Tunnel.State.Down) + tunnelJobs.remove(tunnel)?.cancel() + _status.update { it.copy(activeTunnels = it.activeTunnels - tunnel) } + return Result.failure(BackendException.BackendFailure(throwable)) + } + + @Synchronized + override fun stop(id: Int) { + val tunnel = tunnelHandles.keys.firstOrNull { t -> t.id == id } ?: return + val handle = tunnelHandles.remove(tunnel) ?: return + + when(currentMode) { + Backend.Mode.Proxy -> tun.awgProxyTurnOff(handle) + Backend.Mode.Userspace -> tun.awgTurnOff(handle) + } + + tunnelJobs.remove(tunnel)?.cancel() + + tunnel.updateState(Tunnel.State.Down) + _status.update { it.copy(activeTunnels = it.activeTunnels - tunnel) } + } + + override fun setMode(mode: Backend.Mode) { + if (mode == currentMode) return + shutdown() + currentMode = mode + } + + override fun shutdown() { + + when(currentMode) { + Backend.Mode.Proxy -> tun.awgProxyTurnOffAll() + Backend.Mode.Userspace -> tun.awgTurnOffAll() + } + + tunnelJobs.values.forEach { it.cancel() } + tunnelJobs.clear() + tunnelHandles.clear() + _status.update { it.copy(activeTunnels = emptyMap()) } + } + + override fun setKillSwitch(enabled: Boolean): Result { + if (_status.value.killSwitchEnabled == enabled) return Result.success(Unit) + val setValue = if (enabled) 1 else 0 + val status = tun.setKillSwitch(setValue) + if (status == -1) return Result.failure(BackendException.KillSwitchSetFailed("")) + val killSwitchEnabled = status == 1 + _status.update { it.copy(killSwitchEnabled = killSwitchEnabled) } + return Result.success(Unit) + } + + private fun mapStatusCodeToState(statusCode: Int): Tunnel.State { + // Matching native status codes + return when (statusCode) { + 0 -> Tunnel.State.Up.Healthy + 1 -> Tunnel.State.Up.HandshakeFailure + 2 -> Tunnel.State.Up.ResolvingDns + 3 -> Tunnel.State.Up.Unknown + else -> Tunnel.State.Down // unknow or negative error code consider down + } + } +} \ No newline at end of file diff --git a/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/Backend.kt b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/Backend.kt new file mode 100644 index 0000000..21af197 --- /dev/null +++ b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/Backend.kt @@ -0,0 +1,26 @@ +package com.zaneschepke.wireguardautotunnel.tunnel + +import kotlinx.coroutines.flow.Flow + +interface Backend { + fun start(tunnel: Tunnel, config : String) : Result + fun stop(id : Int) + fun setMode(mode: Mode) + + fun setKillSwitch(enabled: Boolean) : Result + + fun shutdown() + + val status : Flow + + sealed interface Mode { + data object Userspace: Mode + data object Proxy : Mode + } + + data class Status( + val killSwitchEnabled: Boolean, + val mode: Mode, + val activeTunnels: Map + ) +} \ No newline at end of file diff --git a/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/Tunnel.kt b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/Tunnel.kt new file mode 100644 index 0000000..a7f946e --- /dev/null +++ b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/Tunnel.kt @@ -0,0 +1,32 @@ +package com.zaneschepke.wireguardautotunnel.tunnel + +import kotlinx.coroutines.flow.StateFlow + +interface Tunnel { + val id: Int + val name: String + val features : Set + + fun updateState(state: State) + + sealed interface State { + sealed class Up : State { + data object Healthy : Up() + data object ResolvingDns : Up() + data object HandshakeFailure : Up() + data object Unknown : Up() + } + data object Down : State + data object Starting : State + } + + sealed interface Feature { + data object DynamicDNS : Feature + data class PingMonitor( + val intervalSeconds: Int = 30, + val attempts: Int = 3, + val timeoutSeconds: Int? = null, + val target: String? = null, + ) : Feature + } +} \ No newline at end of file diff --git a/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/native/AwgTunnel.kt b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/native/AwgTunnel.kt new file mode 100644 index 0000000..81db0d8 --- /dev/null +++ b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/native/AwgTunnel.kt @@ -0,0 +1,35 @@ +package com.zaneschepke.wireguardautotunnel.tunnel.native + +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.Pointer + +interface AwgTunnel : Library { + + // Normal tunnel methods + fun awgTurnOn( + cfg: String?, + callback: StatusCodeCallback? + ): Int + fun awgTurnOff(handle: Int) + fun awgGetConfig(handle: Int): Pointer? + fun awgTurnOffAll() + + // Proxy tunnel methods + fun awgProxyTurnOn( + cfg: String?, + callback: StatusCodeCallback? + ): Int + fun awgProxyGetConfig(handle: Int): Pointer? + fun awgProxyTurnOffAll() + fun awgProxyTurnOff(handle: Int) + + + fun setKillSwitch(value: Int) : Int // 1 for enable, 0 for disable, return 1 or -1 for error + + fun getKillSwitchStatus() : Int // 1 for enabled, 0 for disabled + + companion object { + val INSTANCE: AwgTunnel = Native.load("wg", AwgTunnel::class.java) + } +} \ No newline at end of file diff --git a/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/native/StatusCodeCallback.kt b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/native/StatusCodeCallback.kt new file mode 100644 index 0000000..e1fb9ef --- /dev/null +++ b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/native/StatusCodeCallback.kt @@ -0,0 +1,7 @@ +package com.zaneschepke.wireguardautotunnel.tunnel.native + +import com.sun.jna.Callback + +interface StatusCodeCallback : Callback{ + fun onTunnelStatusCode(handle: Int, statusCode: Int) +} \ No newline at end of file diff --git a/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/util/BackendException.kt b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/util/BackendException.kt new file mode 100644 index 0000000..c9dd4f9 --- /dev/null +++ b/tunnel/src/main/kotlin/com/zaneschepke/wireguardautotunnel/tunnel/util/BackendException.kt @@ -0,0 +1,8 @@ +package com.zaneschepke.wireguardautotunnel.tunnel.util + +sealed class BackendException : Exception() { + data class InvalidConfig(val reason: String) : BackendException() + data class PermissionDenied(val requiredPermission: String) : BackendException() + data class KillSwitchSetFailed(val reason : String) : BackendException() + data class BackendFailure(override val cause: Throwable) : BackendException() +} \ No newline at end of file diff --git a/tunnel/tools/amneziawg-tools b/tunnel/tools/amneziawg-tools new file mode 160000 index 0000000..5c6ffd6 --- /dev/null +++ b/tunnel/tools/amneziawg-tools @@ -0,0 +1 @@ +Subproject commit 5c6ffd6168f7c69199200a91803fa02e1b8c4152 diff --git a/tunnel/tools/libwg-go/Makefile b/tunnel/tools/libwg-go/Makefile new file mode 100644 index 0000000..415d4b5 --- /dev/null +++ b/tunnel/tools/libwg-go/Makefile @@ -0,0 +1,111 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright © 2026 WG Tunnel. +# Adapted from WireGuard LLC. + +BUILDDIR ?= $(CURDIR)/build +DESTDIR ?= $(CURDIR)/out +RESOURCEDIR ?= $(CURDIR)/../../src/main/resources + +NDK_GO_ARCH_MAP_x86_64 := amd64 +NDK_GO_ARCH_MAP_aarch64 := arm64 + +GO_VERSION := 1.25.5 +GO_DIR := $(BUILDDIR)/go-$(GO_VERSION) +export GOCACHE := $(BUILDDIR)/go-cache + +GO_PLATFORM := $(shell uname -s | tr '[:upper:]' '[:lower:]')-$(NDK_GO_ARCH_MAP_$(shell uname -m)) + +GO_TARBALL := go$(GO_VERSION).$(GO_PLATFORM).tar.gz +GO_HASH_darwin-amd64 := b69d51bce599e5381a94ce15263ae644ec84667a5ce23d58dc2e63e2c12a9f56 +GO_HASH_darwin-arm64 := bed8ebe824e3d3b27e8471d1307f803fc6ab8e1d0eb7a4ae196979bd9b801dd3 +GO_HASH_linux-amd64 := 9e9b755d63b36acf30c12a9a3fc379243714c1c6d3dd72861da637f336ebb35b + +export GOROOT := $(GO_DIR) +export PATH := $(GO_DIR)/bin:$(PATH) +export CGO_ENABLED := 1 + +HOST_OS := $(shell uname -s | tr '[:upper:]' '[:lower:]') +ifeq ($(HOST_OS),darwin) +PLATFORMS := darwin-amd64 darwin-arm64 linux-amd64 windows-amd64 windows-arm64 +else +PLATFORMS := linux-amd64 windows-amd64 +endif + +default: all + +$(BUILDDIR)/$(GO_TARBALL): + mkdir -p "$(dir $@)" + curl -o "$@.tmp" "https://dl.google.com/go/$(GO_TARBALL)" + echo "$(GO_HASH_$(GO_PLATFORM)) $@.tmp" | sha256sum -c + mv "$@.tmp" "$@" + +$(GO_DIR)/.prepared: $(BUILDDIR)/$(GO_TARBALL) + mkdir -p "$(dir $@)" + tar -C "$(dir $@)" --strip-components=1 -xzf "$^" + cd "$(dir $@)/src/runtime" && sed -i 's/CLOCK_MONOTONIC/BOOTTIME/g' sys_linux_*.s + cd "$(dir $@)/src/runtime" && sed -i 's/ $1/ $7/g' sys_linux_*.s + cd "$(dir $@)/src/runtime" && sed -i '/libc_mach_absolute_time/a \//go:cgo_import_dynamic libc_mach_continuous_time mach_continuous_time "/usr/lib/libSystem.B.dylib"' sys_darwin.go + cd "$(dir $@)/src/runtime" && sed -i 's/mach_absolute_time/mach_continuous_time/g' sys_darwin_amd64.s sys_darwin_arm64.s + touch "$@" + +$(DESTDIR)/libwg-linux-amd64.so: $(GO_DIR)/.prepared go.mod go.sum $(wildcard *.go) $(wildcard **/*.go) + @mkdir -p "$(DESTDIR)" + @if [ "$(HOST_OS)" = "darwin" ]; then \ + GOOS=linux GOARCH=amd64 CC="zig cc -target x86_64-linux-gnu-musl" CXX="zig c++ -target x86_64-linux-gnu-musl" go build -ldflags="-buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode=c-shared .; \ + else \ + GOOS=linux GOARCH=amd64 go build -ldflags="-buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode=c-shared .; \ + fi + +$(DESTDIR)/libwg-windows-amd64.dll: $(GO_DIR)/.prepared go.mod go.sum $(wildcard *.go) $(wildcard **/*.go) + @mkdir -p "$(DESTDIR)" + GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ \ + go build -ldflags="-buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode=c-shared . + +$(DESTDIR)/libwg-windows-arm64.dll: $(GO_DIR)/.prepared go.mod go.sum $(wildcard *.go) $(wildcard **/*.go) + @mkdir -p "$(DESTDIR)" + GOOS=windows GOARCH=arm64 CC=aarch64-w64-mingw32-gcc CXX=aarch64-w64-mingw32-g++ \ + go build -ldflags="-buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode=c-shared . + +$(DESTDIR)/libwg-darwin-amd64.dylib: $(GO_DIR)/.prepared go.mod go.sum $(wildcard *.go) $(wildcard **/*.go) + @mkdir -p "$(DESTDIR)" + GOOS=darwin GOARCH=amd64 go build -ldflags="-buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode=c-shared . + +$(DESTDIR)/libwg-darwin-arm64.dylib: $(GO_DIR)/.prepared go.mod go.sum $(wildcard *.go) $(wildcard **/*.go) + @mkdir -p "$(DESTDIR)" + GOOS=darwin GOARCH=arm64 go build -ldflags="-buildid=" -v -trimpath -buildvcs=false -o "$@" -buildmode=c-shared . + +go-clean-cache: $(GO_DIR)/.prepared + @mkdir -p $(GOCACHE) + -@go clean -cache > /dev/null 2>&1 + +build_all: go-clean-cache \ + $(foreach plat,$(PLATFORMS),$(DESTDIR)/libwg-$(plat).$(if $(findstring windows,$(plat)),dll,$(if $(findstring darwin,$(plat)),dylib,so))) + +copy_to_resources: build_all + @for plat in $(PLATFORMS); do \ + os=`echo "$$plat" | cut -d- -f1`; \ + arch=`echo "$$plat" | cut -d- -f2`; \ + jna_dir=; \ + if [ "$$os" = "linux" ] && [ "$$arch" = "amd64" ]; then jna_dir=linux-x86-64; fi; \ + if [ "$$os" = "windows" ] && [ "$$arch" = "amd64" ]; then jna_dir=win32-x86-64; fi; \ + if [ "$$os" = "windows" ] && [ "$$arch" = "arm64" ]; then jna_dir=win32-aarch64; fi; \ + if [ "$$os" = "darwin" ] && [ "$$arch" = "amd64" ]; then jna_dir=darwin-x86-64; fi; \ + if [ "$$os" = "darwin" ] && [ "$$arch" = "arm64" ]; then jna_dir=darwin-aarch64; fi; \ + libext=so; \ + if [ "$$os" = "windows" ]; then libext=dll; fi; \ + if [ "$$os" = "darwin" ]; then libext=dylib; fi; \ + dest_dir="$(RESOURCEDIR)/$$jna_dir"; \ + mkdir -p "$$dest_dir"; \ + cp "$(DESTDIR)/libwg-$$plat.$$libext" "$$dest_dir/libwg.$$libext"; \ + echo "Copied $$plat -> $$dest_dir/libwg.$$libext"; \ + done + + +all: copy_to_resources + +clean: + rm -rf $(BUILDDIR) $(DESTDIR) + +.PHONY: default all build_all copy_to_resources clean +.DELETE_ON_ERROR: \ No newline at end of file diff --git a/tunnel/tools/libwg-go/constants/constants.go b/tunnel/tools/libwg-go/constants/constants.go new file mode 100644 index 0000000..3651fa5 --- /dev/null +++ b/tunnel/tools/libwg-go/constants/constants.go @@ -0,0 +1,7 @@ +package constants + +const ( + IfacePrefix = "wgtun" + IfaceName = IfacePrefix + "%d" + DummyAddress = "100.64.0.1" +) diff --git a/tunnel/tools/libwg-go/dns/dns.go b/tunnel/tools/libwg-go/dns/dns.go new file mode 100644 index 0000000..ed58602 --- /dev/null +++ b/tunnel/tools/libwg-go/dns/dns.go @@ -0,0 +1,165 @@ +// Package dnsresolver provides modular DNS resolution with backoff retries using AdguardTeam/dnsproxy. +// It supports various protocols (plain DNS, DoT, DoH, DoQ, DNSCrypt) via upstream URLs. +// Example upstream formats: +// - Plain UDP: "udp://1.1.1.1:53" +// - Plain TCP: "tcp://1.1.1.1:53" +// - DoT: "tls://1.1.1.1:853" +// - DoH: "https://cloudflare-dns.com/dns-query" +// - DoQ: "quic://dns.adguard-dns.com:853" +// - DNSCrypt: "sdns://AQIAAAAAAAAAFDEuZTAuMC4xOjg0NDMg04wIk9UdC5pYol3Wg92WwgQzOKk8J6SxvE-rO4jDW56HAgBgML0pB4" + +package dns + +import ( + "context" + "errors" + "fmt" + "net" + "net/netip" + "sync" + "time" + + "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/cenkalti/backoff/v5" + "github.com/miekg/dns" +) + +// ResolverOptions configures the DNS resolver. +type ResolverOptions struct { + UpstreamURL string + Timeout time.Duration +} + +// DefaultOptions returns default resolver options with 1.1.1.1 over UDP. +func DefaultOptions() ResolverOptions { + return ResolverOptions{ + UpstreamURL: "udp://1.1.1.1:53", + Timeout: 5 * time.Second, + } +} + +type Resolved struct { + V4 []netip.Addr + V6 []netip.Addr +} + +func resolveInner(host string, ipType uint16, u upstream.Upstream, dialer *net.Dialer, wg *sync.WaitGroup) ([]netip.Addr, error) { + var addr []netip.Addr + defer wg.Done() + + req := &dns.Msg{} + req.Id = dns.Id() + req.RecursionDesired = true + req.SetQuestion(dns.Fqdn(host), ipType) + + req.SetEdns0(4096, true) + + // Since upstream.Options doesn't take a dialer, we use the miekg/dns client + // directly with our custom dialer to ensure the SO_MARK/Binding is applied. + client := &dns.Client{ + Net: "udp", + Dialer: dialer, + Timeout: 5 * time.Second, + UDPSize: 4096, + } + + // We use the Address from the upstream (e.g., "1.1.1.1:53") + res, _, err := client.Exchange(req, u.Address()) + if err != nil { + return nil, err + } + + if res.Rcode != dns.RcodeSuccess { + return nil, fmt.Errorf("DNS query failed with Rcode: %d", res.Rcode) + } + + for _, ans := range res.Answer { + switch ipType { + case dns.TypeA: + if a, ok := ans.(*dns.A); ok { + if ip, err := netip.ParseAddr(a.A.String()); err == nil { + addr = append(addr, ip) + } + } + case dns.TypeAAAA: + if aaaa, ok := ans.(*dns.AAAA); ok { + if ip, err := netip.ParseAddr(aaaa.AAAA.String()); err == nil { + addr = append(addr, ip) + } + } + } + } + return addr, nil +} + +func Resolve(host string, opts ResolverOptions, preferIpv6 bool) ([]netip.Addr, []netip.Addr, error) { + dialer, err := GetBypassDialer(preferIpv6) + if err != nil { + return nil, nil, fmt.Errorf("bypass dialer failed: %w", err) + } + + // 2. Setup the library just to handle URL parsing and certificates + // We pass the CustomResolver (which uses our bypass dialer) for bootstrapping + u, err := upstream.AddressToUpstream(opts.UpstreamURL, &upstream.Options{ + Bootstrap: CustomResolver(preferIpv6), + Timeout: opts.Timeout, + PreferIPv6: preferIpv6, + }) + if err != nil { + return nil, nil, err + } + defer u.Close() + + var wg sync.WaitGroup + var v4, v6 []netip.Addr + var v4Err, v6Err error + + wg.Add(2) + // 3. We use the 'dialer' directly in resolveInner + go func() { v4, v4Err = resolveInner(host, dns.TypeA, u, dialer, &wg) }() + go func() { v6, v6Err = resolveInner(host, dns.TypeAAAA, u, dialer, &wg) }() + wg.Wait() + + if v4Err != nil && v6Err != nil { + return nil, nil, errors.Join(v4Err, v6Err) + } + + if len(v4) == 0 && len(v6) == 0 { + if v4Err != nil { + return nil, nil, v4Err + } + if v6Err != nil { + return nil, nil, v6Err + } + return nil, nil, errors.New("no IP addresses found") + } + + return v4, v6, nil +} + +// ResolveWithBackoff retries resolution with exponential backoff until success +func ResolveWithBackoff(ctx context.Context, host string, opts ResolverOptions, preferIpv6 bool, logger *device.Logger) (Resolved, error) { + logger.Verbosef("Starting DNS resolution...") + operation := func() (Resolved, error) { + if err := ctx.Err(); err != nil { + return Resolved{}, backoff.Permanent(err) + } + v4, v6, err := Resolve(host, opts, preferIpv6) + if err != nil { + logger.Errorf("Error resolving host %s: %v, retrying...", host, err) + return Resolved{}, err + } + if len(v4) == 0 && len(v6) == 0 { + logger.Errorf("No IPs resolved for host %s, retrying...", host) + return Resolved{}, errors.New("no IPs resolved") + } + logger.Verbosef("Host successfully resolved.") + return Resolved{V4: v4, V6: v6}, nil + } + + return backoff.Retry(ctx, operation, + backoff.WithBackOff(backoff.NewExponentialBackOff()), + backoff.WithMaxElapsedTime(0), // retry forever + ) +} diff --git a/tunnel/tools/libwg-go/dns/resolver_unix.go b/tunnel/tools/libwg-go/dns/resolver_unix.go new file mode 100644 index 0000000..e882b98 --- /dev/null +++ b/tunnel/tools/libwg-go/dns/resolver_unix.go @@ -0,0 +1,41 @@ +//go:build unix + +package dns + +import ( + "context" + "net" + "syscall" + + "github.com/wgtunnel/desktop/tunnel/vpn/firewall/mark" +) + +// GetBypassDialer returns a dialer that bypasses the VPN via SO_MARK +func GetBypassDialer(preferIpv6 bool) (*net.Dialer, error) { + return &net.Dialer{ + Control: func(network, address string, c syscall.RawConn) error { + var opErr error + err := c.Control(func(fd uintptr) { + opErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark.LinuxBootstrapMarkNum) + }) + if err != nil { + return err + } + return opErr + }, + }, nil +} + +// CustomResolver is still needed for the dnsproxy Bootstrap field +func CustomResolver(preferIpv6 bool) *net.Resolver { + return &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d, err := GetBypassDialer(preferIpv6) + if err != nil { + return nil, err + } + return d.DialContext(ctx, network, address) + }, + } +} diff --git a/tunnel/tools/libwg-go/dns/resolver_windows.go b/tunnel/tools/libwg-go/dns/resolver_windows.go new file mode 100644 index 0000000..d2eb766 --- /dev/null +++ b/tunnel/tools/libwg-go/dns/resolver_windows.go @@ -0,0 +1,26 @@ +//go:build windows + +package dns + +import ( + "context" + "net" +) + +// GetBypassDialer returns a standard dialer for Windows. +// Since the process is already bypassed in the Windows Firewall, +// no special socket marking or binding is required. +func GetBypassDialer(preferIpv6 bool) (*net.Dialer, error) { + return &net.Dialer{}, nil +} + +// CustomResolver returns a standard net.Resolver for Windows. +func CustomResolver(preferIpv6 bool) *net.Resolver { + return &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d, _ := GetBypassDialer(preferIpv6) + return d.DialContext(ctx, network, address) + }, + } +} diff --git a/tunnel/tools/libwg-go/go.mod b/tunnel/tools/libwg-go/go.mod new file mode 100755 index 0000000..a80768d --- /dev/null +++ b/tunnel/tools/libwg-go/go.mod @@ -0,0 +1,61 @@ +module github.com/wgtunnel/desktop/tunnel + +go 1.25.5 + +require ( + github.com/AdguardTeam/dnsproxy v0.78.2 + github.com/amnezia-vpn/amneziawg-go v0.2.16 + github.com/artem-russkikh/wireproxy-awg v1.0.12 + github.com/cenkalti/backoff/v5 v5.0.3 + github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 + github.com/google/nftables v0.3.0 + github.com/vishvananda/netlink v1.3.1 + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba + golang.zx2c4.com/wireguard/windows v0.5.3 + inet.af/wf v0.0.0-20221017222439-36129f591884 + tailscale.com v1.94.1 +) + +require ( + github.com/AdguardTeam/golibs v0.35.7 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/ameshkov/dnscrypt/v2 v2.4.0 // indirect + github.com/ameshkov/dnsstamps v1.0.3 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/mdlayher/netlink v1.8.0 // indirect + github.com/mdlayher/socket v0.5.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/vishvananda/netns v0.0.5 // indirect + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect + golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/tools v0.41.0 // indirect + honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0 // indirect +) + +require ( + github.com/MakeNowJust/heredoc/v2 v2.0.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/miekg/dns v1.1.72 + github.com/things-go/go-socks5 v0.1.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 + golang.org/x/time v0.14.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 // indirect; ind +) + +//replace github.com/amnezia-vpn/amneziawg-go => github.com/wgtunnel/amneziawg-go v0.0.0-20251225080458-6a08ea62878d + +//replace github.com/artem-russkikh/wireproxy-awg => github.com/wgtunnel/wireproxy-awg v0.0.0-20251215030122-ffaf05dda47f + +// local dev +replace github.com/amnezia-vpn/amneziawg-go => ../../../../amneziawg-go + +// +replace github.com/artem-russkikh/wireproxy-awg => ../../../../wireproxy-awg diff --git a/tunnel/tools/libwg-go/go.sum b/tunnel/tools/libwg-go/go.sum new file mode 100644 index 0000000..c591e86 --- /dev/null +++ b/tunnel/tools/libwg-go/go.sum @@ -0,0 +1,91 @@ +github.com/AdguardTeam/dnsproxy v0.78.2 h1:g+ba4vh72hAv9zIE+OPSEnu77utSKxIF6u2jNhYAR7g= +github.com/AdguardTeam/dnsproxy v0.78.2/go.mod h1:gwr+7Dc0e7QddQLC9JLGjL5NSKcqw0ESsNMRI5Q67Ps= +github.com/AdguardTeam/golibs v0.35.7 h1:pTQpixUos7mALr3jqb0pigfrkiqPAX1hiYUi/yeBWiA= +github.com/AdguardTeam/golibs v0.35.7/go.mod h1:meFdRqMtG/PLW6LD20MYAlcRbwAVowlbunHgE17xz9s= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A= +github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM= +github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o= +github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI= +github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= +github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg= +github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM= +github.com/mdlayher/netlink v1.8.0 h1:e7XNIYJKD7hUct3Px04RuIGJbBxy1/c4nX7D5YyvvlM= +github.com/mdlayher/netlink v1.8.0/go.mod h1:UhgKXUlDQhzb09DrCl2GuRNEglHmhYoWAHid9HK3594= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/things-go/go-socks5 v0.1.0 h1:4f5dz0iMQ6cA4wseFmyLmCHmg3SWJTW92ndrKS6oERg= +github.com/things-go/go-socks5 v0.1.0/go.mod h1:Riabiyu52kLsla0YmJqunt1c1JEl6iXSr4bRd7swFEA= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96 h1:RMc8anw0hCPcg5CZYN2PEQ8nMwosk461R6vFwPrCFVg= +golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= +golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 h1:2gap+Kh/3F47cO6hAu3idFvsJ0ue6TRcEi2IUkv/F8k= +gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM= +honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0 h1:5SXjd4ET5dYijLaf0O3aOenC0Z4ZafIWSpjUzsQaNho= +honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0/go.mod h1:EPDDhEZqVHhWuPI5zPAsjU0U7v9xNIWjoOVyZ5ZcniQ= +inet.af/wf v0.0.0-20221017222439-36129f591884 h1:zg9snq3Cpy50lWuVqDYM7AIRVTtU50y5WXETMFohW/Q= +inet.af/wf v0.0.0-20221017222439-36129f591884/go.mod h1:bSAQ38BYbY68uwpasXOTZo22dKGy9SNvI6PZFeKomZE= +tailscale.com v1.94.1 h1:0dAst/ozTuFkgmxZULc3oNwR9+qPIt5ucvzH7kaM0Jw= +tailscale.com v1.94.1/go.mod h1:gLnVrEOP32GWvroaAHHGhjSGMPJ1i4DvqNwEg+Yuov4= diff --git a/tunnel/tools/libwg-go/ipc/ipc_bsd.go b/tunnel/tools/libwg-go/ipc/ipc_bsd.go new file mode 100644 index 0000000..c7b1dfb --- /dev/null +++ b/tunnel/tools/libwg-go/ipc/ipc_bsd.go @@ -0,0 +1,29 @@ +//go:build darwin || freebsd || openbsd + +package ipc + +import ( + "net" + + "github.com/amnezia-vpn/amneziawg-go/ipc" + "github.com/wgtunnel/desktop/tunnel/shared" +) + +func SetupIPC(name string) (net.Listener, error) { + var socketDirectory = "/run/wgtunnel" + + uapiFile, err := ipc.UAPIOpen(socketDirectory, name) + if err != nil { + shared.LogError("IPC", "UAPIOpen: %v", err) + return nil, err + } + + uapi, err := ipc.UAPIListen(socketDirectory, name, uapiFile) + if err != nil { + uapiFile.Close() + shared.LogError("IPC", "UAPIListen: %v", err) + return nil, err + } + + return uapi, nil +} diff --git a/tunnel/tools/libwg-go/ipc/ipc_linux.go b/tunnel/tools/libwg-go/ipc/ipc_linux.go new file mode 100644 index 0000000..c4667eb --- /dev/null +++ b/tunnel/tools/libwg-go/ipc/ipc_linux.go @@ -0,0 +1,29 @@ +//go:build linux + +package ipc + +import ( + "net" + + "github.com/amnezia-vpn/amneziawg-go/ipc" + "github.com/wgtunnel/desktop/tunnel/shared" +) + +func SetupIPC(name string) (net.Listener, error) { + var socketDirectory = "/run/wgtunnel" + + uapiFile, err := ipc.UAPIOpen(socketDirectory, name) + if err != nil { + shared.LogError("IPC", "UAPIOpen: %v", err) + return nil, err + } + + uapi, err := ipc.UAPIListen(socketDirectory, name, uapiFile) + if err != nil { + uapiFile.Close() + shared.LogError("IPC", "UAPIListen: %v", err) + return nil, err + } + + return uapi, nil +} diff --git a/tunnel/tools/libwg-go/ipc/ipc_windows.go b/tunnel/tools/libwg-go/ipc/ipc_windows.go new file mode 100644 index 0000000..902abb7 --- /dev/null +++ b/tunnel/tools/libwg-go/ipc/ipc_windows.go @@ -0,0 +1,20 @@ +//go:build windows + +package ipc + +import ( + "net" + + "github.com/amnezia-vpn/amneziawg-go/ipc" + "github.com/wgtunnel/desktop/tunnel/shared" +) + +func SetupIPC(name string) (net.Listener, error) { + uapi, err := ipc.UAPIListen(name) + if err != nil { + shared.LogError("IPC", "UAPIListen: %v", err) + return nil, err + } + + return uapi, nil +} diff --git a/tunnel/tools/libwg-go/killswitch/killswitch.go b/tunnel/tools/libwg-go/killswitch/killswitch.go new file mode 100644 index 0000000..49ad9e9 --- /dev/null +++ b/tunnel/tools/libwg-go/killswitch/killswitch.go @@ -0,0 +1,51 @@ +//go:build !android + +package killswitch + +import "C" +import ( + "github.com/wgtunnel/desktop/tunnel/shared" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall/osfirewall/firewallmgr" +) + +var logger = shared.NewLogger("KillSwitch") + +//export setKillSwitch +func setKillSwitch(enabled C.int) C.int { + fw, err := firewallmgr.Get() + if err != nil { + logger.Errorf("Failed to get firewall: %v", err) + return C.int(-1) + } + + if enabled == 1 { + err := fw.Enable() + if err != nil { + logger.Errorf("Failed to enable kill switch: %v", err) + return C.int(-1) + } + logger.Verbosef("Kill switch enabled") + } else { + err := fw.Disable() + if err != nil { + logger.Errorf("Failed to disable kill switch: %v", err) + return C.int(-1) + } + logger.Verbosef("Kill switch disabled") + } + return enabled +} + +//export getKillSwitchStatus +func getKillSwitchStatus() C.int { + fw, err := firewallmgr.Get() + if err != nil { + logger.Errorf("Failed to get firewall: %v", err) + return C.int(0) + } + + if fw.IsEnabled() { + return C.int(1) + } + return C.int(0) +} diff --git a/tunnel/tools/libwg-go/main.go b/tunnel/tools/libwg-go/main.go new file mode 100644 index 0000000..5f78def --- /dev/null +++ b/tunnel/tools/libwg-go/main.go @@ -0,0 +1,9 @@ +package main + +import ( + _ "github.com/wgtunnel/desktop/tunnel/killswitch" + _ "github.com/wgtunnel/desktop/tunnel/proxy" + _ "github.com/wgtunnel/desktop/tunnel/vpn" +) + +func main() {} diff --git a/tunnel/tools/libwg-go/proxy/proxy.go b/tunnel/tools/libwg-go/proxy/proxy.go new file mode 100755 index 0000000..6cbd8ab --- /dev/null +++ b/tunnel/tools/libwg-go/proxy/proxy.go @@ -0,0 +1,231 @@ +//go:build !android + +package proxy + +/* +#include +typedef void (*StatusCodeCallback)(int32_t handle, int32_t status); +*/ +import "C" +import ( + "context" + "sync" + "syscall" + + "os" + "os/signal" + + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun/netstack" + wireproxyawg "github.com/artem-russkikh/wireproxy-awg" + ipc "github.com/wgtunnel/desktop/tunnel/ipc" + "github.com/wgtunnel/desktop/tunnel/shared" + "github.com/wgtunnel/desktop/tunnel/util" +) + +var ( + tag = "AwgProxy" + virtualTunnelHandles = make(map[int32]*wireproxyawg.VirtualTun) + ctx context.Context + cancelFunc context.CancelFunc +) + +func init() { + // Handle signals for clean shutdown + go handleSignals() +} + +func handleSignals() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + <-sigs + awgProxyTurnOffAll() + os.Exit(0) +} + +//export awgProxyTurnOn +func awgProxyTurnOn(config *C.char, callback C.StatusCodeCallback) C.int { + handle, err2 := util.GenerateHandle(virtualTunnelHandles) + if err2 != nil { + shared.LogError(tag, "Unable to find empty handle", err2) + return C.int(-1) + } + + shared.StoreTunnelCallback(handle, shared.StatusCodeCallback(callback)) + + goConfig := C.GoString(config) + + conf, err := wireproxyawg.ParseConfigString(goConfig) + if err != nil { + shared.LogError(tag, "Invalid config file", err) + return C.int(-1) + } + + setting, err := wireproxyawg.CreateIPCRequest(conf.Device, false) + if err != nil { + shared.LogError(tag, "Create IPC request failed", err) + return C.int(-1) + } + + tun, tnet, err := netstack.CreateNetTUN(setting.DeviceAddr, setting.DNS, setting.MTU) + if err != nil { + shared.LogError(tag, "Create TUN failed", err) + return C.int(-1) + } + + name, err := tun.Name() + if err != nil { + shared.LogError(tag, "Get TUN name failed", err) + return C.int(-1) + } + + bind := conn.NewDefaultBind() + + statusCB := func(code device.StatusCode) { + // use goroutine to avoid any blocking from JNA + go shared.NotifyStatusCode(handle, int32(code)) + } + + dev := device.NewDevice(tun, bind, shared.NewLogger("Tun/"+name), false, statusCB) + + err = dev.IpcSet(setting.IpcRequest) + if err != nil { + shared.LogError(tag, "Ipc setting failed", err) + return C.int(-1) + } + + uapi, _ := ipc.SetupIPC(name) + + go func() { + for { + connection, err := uapi.Accept() + if err != nil { + return + } + go dev.IpcHandle(connection) + } + }() + + err = dev.Up() + if err != nil { + shared.LogError(tag, "Failed to bring up device", err) + uapi.Close() + dev.Close() + return C.int(-1) + } + + virtualTun := &wireproxyawg.VirtualTun{ + Tnet: tnet, + Dev: dev, + Logger: shared.NewLogger("Proxy"), + Uapi: uapi, + Conf: conf.Device, + PingRecord: make(map[string]uint64), + PingRecordLock: new(sync.Mutex), + } + + virtualTunnelHandles[handle] = virtualTun + + // Create cancellable context + ctx, cancelFunc = context.WithCancel(context.Background()) + + // Spawn all routines with context + for _, spawner := range conf.Routines { + shared.LogDebug(tag, "Spawning routine..") + go func(s wireproxyawg.RoutineSpawner) { + if err := s.SpawnRoutine(ctx, virtualTun); err != nil { + shared.LogError(tag, "Routine failed: %v", err) + } + }(spawner) + } + + shared.LogDebug(tag, "Done starting proxy and tunnel") + return C.int(handle) +} + +func awgUpdateProxyTunnelPeers(tunnelHandle int32, settings string) int32 { + handle, ok := virtualTunnelHandles[tunnelHandle] + if !ok { + shared.LogError(tag, "Tunnel is not up") + return -1 + } + + conf, err := wireproxyawg.ParseConfigString(settings) + if err != nil { + shared.LogError(tag, "Invalid config file", err) + return -1 + } + + ipcRequest, err := wireproxyawg.CreatePeerIPCRequest(conf.Device) + if err != nil { + shared.LogError(tag, "CreateIPCRequest: %v", err) + return -1 + } + + err = handle.Dev.IpcSet(ipcRequest.IpcRequest) + if err != nil { + shared.LogError(tag, "IpcSet: %v", err) + return -1 + } + + shared.LogDebug(tag, "Configuration updated successfully") + return 0 +} + +//export awgProxyGetConfig +func awgProxyGetConfig(tunnelHandle C.int) *C.char { + goTunnelHandle := int32(tunnelHandle) + handle, ok := virtualTunnelHandles[goTunnelHandle] + if !ok { + shared.LogError(tag, "Tunnel is not up") + return nil + } + settings, err := handle.Dev.IpcGet() + if err != nil { + shared.LogError(tag, "Failed to get device config: %v", err) + return nil + } + return C.CString(settings) +} + +//export awgProxyTurnOffAll +func awgProxyTurnOffAll() { + if cancelFunc != nil { + shared.LogDebug(tag, "Stopping proxy routines..") + cancelFunc() + cancelFunc = nil + } + handles := make([]int32, 0, len(virtualTunnelHandles)) + for h := range virtualTunnelHandles { + handles = append(handles, h) + } + for _, handle := range handles { + awgProxyTurnOff(C.int(handle)) + } + virtualTunnelHandles = make(map[int32]*wireproxyawg.VirtualTun) + shared.LogDebug(tag, "Proxy fully reset: %d handles closed", len(handles)) +} + +//export awgProxyTurnOff +func awgProxyTurnOff(virtualTunnelHandle C.int) { + goVirtualTunnelHandle := int32(virtualTunnelHandle) + virtualTun, ok := virtualTunnelHandles[goVirtualTunnelHandle] + if !ok { + shared.LogError(tag, "Tunnel handle %d not found", goVirtualTunnelHandle) + return + } + shared.LogDebug(tag, "Tearing down tunnel %d", goVirtualTunnelHandle) + + // Disable UAPI listener and underlying file + if virtualTun.Uapi != nil { + virtualTun.Uapi.Close() + } + + if virtualTun.Dev != nil { + virtualTun.Dev.Close() + } + + delete(virtualTunnelHandles, goVirtualTunnelHandle) + shared.LogDebug(tag, "Tunnel %d fully closed (UAPI/Dev/Bind purged)", goVirtualTunnelHandle) +} diff --git a/tunnel/tools/libwg-go/shared/shared.go b/tunnel/tools/libwg-go/shared/shared.go new file mode 100755 index 0000000..529f439 --- /dev/null +++ b/tunnel/tools/libwg-go/shared/shared.go @@ -0,0 +1,63 @@ +package shared + +/* +#include +typedef void (*StatusCodeCallback)(int32_t handle, int32_t status); + +void callStatusCallback(StatusCodeCallback cb, int32_t handle, int32_t status) { + if (cb) cb(handle, status); +} +*/ +import "C" +import ( + "log" + + "github.com/amnezia-vpn/amneziawg-go/device" +) + +var tag = "AmneziaWG" + +func LogDebug(format string, args ...interface{}) { + log.Printf("[DEBUG] %s: "+format+"\n", append([]interface{}{tag}, args...)...) +} + +func LogWarn(format string, args ...interface{}) { + log.Printf("[WARN] %s: "+format+"\n", append([]interface{}{tag}, args...)...) +} + +func LogError(format string, args ...interface{}) { + log.Printf("[ERROR] %s: "+format+"\n", append([]interface{}{tag}, args...)...) +} + +func NewLogger(prefix string) *device.Logger { + return &device.Logger{ + Verbosef: func(format string, args ...any) { + LogDebug(prefix+": "+format, args...) + }, + Errorf: func(format string, args ...any) { + LogError(prefix+": "+format, args...) + }, + } +} + +type StatusCodeCallback C.StatusCodeCallback + +var tunnelCallbacks = make(map[int32]StatusCodeCallback) + +func StoreTunnelCallback(handle int32, cb StatusCodeCallback) { + if cb != nil { + tunnelCallbacks[handle] = cb + } +} + +func NotifyStatusCode(handle int32, status int32) { + if cb, ok := tunnelCallbacks[handle]; ok && cb != nil { + C.callStatusCallback(cb, C.int32_t(handle), C.int32_t(status)) + } +} + +const ( + StatusHealthy = iota + StatusHandshakeFailure + StatusResolvingDNS +) diff --git a/tunnel/tools/libwg-go/util/util.go b/tunnel/tools/libwg-go/util/util.go new file mode 100755 index 0000000..ebd493d --- /dev/null +++ b/tunnel/tools/libwg-go/util/util.go @@ -0,0 +1,16 @@ +package util + +import ( + "fmt" + "math" +) + +// GenerateHandle generates a unique int32 handle for a given map. +func GenerateHandle[K int32, V any](handles map[K]V) (int32, error) { + for i := int32(0); i < math.MaxInt32; i++ { + if _, exists := handles[K(i)]; !exists { + return i, nil + } + } + return -1, fmt.Errorf("unable to find handle") +} diff --git a/tunnel/tools/libwg-go/vpn/bind/bind_darwin.go b/tunnel/tools/libwg-go/vpn/bind/bind_darwin.go new file mode 100644 index 0000000..c21f6d1 --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/bind/bind_darwin.go @@ -0,0 +1,13 @@ +//go:build darwin + +package bind + +import ( + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" +) + +func SetupBind(logger *device.Logger, bind conn.Bind) error { + + return nil // No fwmark on non-Linux; no-op +} diff --git a/tunnel/tools/libwg-go/vpn/bind/linux_bind.go b/tunnel/tools/libwg-go/vpn/bind/linux_bind.go new file mode 100644 index 0000000..a164126 --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/bind/linux_bind.go @@ -0,0 +1,38 @@ +//go:build linux && !android + +package bind + +import ( + "fmt" + "syscall" + + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall/mark" + "golang.org/x/sys/unix" +) + +func SetupBind(logger *device.Logger, bind conn.Bind) error { + stdBind, ok := bind.(*conn.StdNetBind) + if !ok { + return fmt.Errorf("failed to cast to StdNetBind") + } + stdBind.SetControl(func(network, address string, c syscall.RawConn) error { + var opErr error + err := c.Control(func(fd uintptr) { + logger.Verbosef("Control called on socket FD %d - setting fwmark...", fd) + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, mark.LinuxBypassMarkNum); err != nil { + opErr = err + logger.Errorf("Failed to set fwmark on FD %d: %v", fd, err) + } else { + logger.Verbosef("Fwmark %d set on FD %d", mark.LinuxBypassMarkNum, fd) + } + }) + if err != nil { + return err + } + return opErr + }) + logger.Verbosef("Set control func on bind to apply fwmark on socket ops") + return nil +} diff --git a/tunnel/tools/libwg-go/vpn/bind/windows_bind.go b/tunnel/tools/libwg-go/vpn/bind/windows_bind.go new file mode 100644 index 0000000..0edde61 --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/bind/windows_bind.go @@ -0,0 +1,12 @@ +//go:build windows + +package bind + +import ( + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" +) + +func SetupBind(logger *device.Logger, bind conn.Bind) error { + return nil +} diff --git a/tunnel/tools/libwg-go/vpn/dns/dns_linux.go b/tunnel/tools/libwg-go/vpn/dns/dns_linux.go new file mode 100644 index 0000000..06ab24f --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/dns/dns_linux.go @@ -0,0 +1,294 @@ +//go:build linux + +package dns + +import ( + "context" + "fmt" + "net/netip" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/godbus/dbus/v5" + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" +) + +const ( + dbusDest = "org.freedesktop.resolve1" + dbusInterface = "org.freedesktop.resolve1.Manager" + dbusPath = "/org/freedesktop/resolve1" + + resolvConfPath = "/etc/resolv.conf" + resolvConfBak = "/etc/resolv.conf.bak.wgt" +) + +// Conn represents a systemd-resolved dbus connection. +type Conn struct { + conn *dbus.Conn + obj dbus.BusObject +} + +func newConn() (*Conn, error) { + conn, err := dbus.SystemBusPrivate() + if err != nil { + return nil, fmt.Errorf("failed to init private conn to system bus: %w", err) + } + methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} + err = conn.Auth(methods) + if err != nil { + conn.Close() + return nil, fmt.Errorf("failed to auth with external method: %w", err) + } + err = conn.Hello() + if err != nil { + conn.Close() + return nil, fmt.Errorf("failed to make hello call: %w", err) + } + return &Conn{ + conn: conn, + obj: conn.Object(dbusDest, dbus.ObjectPath(dbusPath)), + }, nil +} + +// Call wraps obj.CallWithContext by using 0 as flags and formats the method with the dbus manager interface. +func (c *Conn) Call(ctx context.Context, method string, args ...interface{}) *dbus.Call { + return c.obj.CallWithContext(ctx, fmt.Sprintf("%s.%s", dbusInterface, method), 0, args...) +} + +// Close closes the current dbus connection. +func (c *Conn) Close() error { + return c.conn.Close() +} + +// SetDns configures DNS servers and search domains, using systemd-resolved if available (per-interface), +// falling back to overwriting /etc/resolv.conf otherwise. +func SetDns(iface string, dns []netip.Addr, searchDomains []string, fullTunnel bool, logger *device.Logger) error { + index, err := getInterfaceIndex(iface) + if isSystemdResolvedActive() { + if err != nil { + logger.Errorf("Failed to get interface name, falling back to resolv.conf: %w", err) + return setDnsFile(dns, searchDomains, fullTunnel) + } + logger.Verbosef("Configuring systemd-resolver...") + return setDnsSystemd(index, dns, searchDomains, fullTunnel) + } + logger.Verbosef("Systemd-resolver not detected, falling back to resolv.conf...") + return setDnsFile(dns, searchDomains, fullTunnel) +} + +func getInterfaceIndex(ifName string) (int, error) { + link, err := netlink.LinkByName(ifName) + if err != nil { + return 0, fmt.Errorf("failed to get link for %s: %w", ifName, err) + } + return link.Attrs().Index, nil +} + +// RevertDns reverts DNS configuration, using systemd-resolved if available, or restoring the resolv.conf backup otherwise. +func RevertDns(iface string, logger *device.Logger) error { + index, err := getInterfaceIndex(iface) + if isSystemdResolvedActive() { + if err != nil { + logger.Errorf("Failed to get interface name, attempting to revert resolv.conf from backup...") + return revertDnsFile() + } + logger.Verbosef("Reverting systemd-resolver...") + return revertDnsSystemd(index) + } + logger.Verbosef("Systemd-resolver not detected, attempting to revert dns from backup...") + return revertDnsFile() +} + +// isSystemdResolvedActive checks if systemd-resolved is available and responsive via DBus. +func isSystemdResolvedActive() bool { + conn, err := newConn() + if err != nil { + return false + } + defer conn.Close() + + // Test with a simple local resolve (flags=0) + var addresses []struct { + IfIndex int + Family int + Address []byte + } + var canonical string + var outflags uint64 + call := conn.Call(context.Background(), "ResolveHostname", 0, "localhost", unix.AF_UNSPEC, uint64(0)) + if call.Err != nil { + return false + } + err = call.Store(&addresses, &canonical, &outflags) + return err == nil +} + +// setDnsSystemd configures DNS via systemd-resolved DBus (per-interface). +func setDnsSystemd(ifIndex int, dns []netip.Addr, searchDomains []string, fullTunnel bool) error { + conn, err := newConn() + if err != nil { + return fmt.Errorf("dbus connect: %w", err) + } + defer conn.Close() + + type dnsEntry struct { + Family int32 + Address []byte + } + + var linkDNS []dnsEntry + for _, ip := range dns { + fam := int32(unix.AF_INET) + if ip.Is6() { + fam = int32(unix.AF_INET6) + } + linkDNS = append(linkDNS, dnsEntry{ + Family: fam, + Address: ip.AsSlice(), + }) + } + call := conn.Call(context.Background(), "SetLinkDNS", ifIndex, linkDNS) + if call.Err != nil { + return fmt.Errorf("set link DNS: %w", call.Err) + } + + type domainEntry struct { + Domain string + Routing bool + } + + var linkDomains []domainEntry + for _, domain := range searchDomains { + linkDomains = append(linkDomains, domainEntry{ + Domain: domain, + Routing: false, + }) + } + // full tunnel, add "~." as routing domain to capture all queries + if fullTunnel && len(dns) > 0 { + linkDomains = append(linkDomains, domainEntry{ + Domain: "~.", + Routing: true, + }) + } + call = conn.Call(context.Background(), "SetLinkDomains", ifIndex, linkDomains) + if call.Err != nil { + return fmt.Errorf("set link domains: %w", call.Err) + } + + // set the link as the default DNS route for full tunnel + if fullTunnel { + call = conn.Call(context.Background(), "SetLinkDefaultRoute", ifIndex, true) + if call.Err != nil { + return fmt.Errorf("set link default route: %w", call.Err) + } + } + + return nil +} + +// revertDnsSystemd reverts DNS configuration via systemd-resolved DBus. +func revertDnsSystemd(ifIndex int) error { + conn, err := newConn() + if err != nil { + return fmt.Errorf("dbus connect: %w", err) + } + defer conn.Close() + + // revert default route + call := conn.Call(context.Background(), "SetLinkDefaultRoute", ifIndex, false) + if call.Err != nil { + return fmt.Errorf("revert link default route: %w", call.Err) + } + + // revert all settings for the link + call = conn.Call(context.Background(), "RevertLink", ifIndex) + if call.Err != nil { + return fmt.Errorf("revert link: %w", call.Err) + } + + return nil +} + +// setDnsFile is the fallback: overwrites /etc/resolv.conf and locks if fullTunnel. +func setDnsFile(dns []netip.Addr, searchDomains []string, fullTunnel bool) error { + if err := backupResolvConf(); err != nil { + return err + } + + // Write new conf + f, err := os.Create(resolvConfPath) + if err != nil { + return err + } + defer f.Close() + + for _, d := range dns { + fmt.Fprintf(f, "nameserver %s\n", d.String()) + } + if len(searchDomains) > 0 { + fmt.Fprintf(f, "search %s\n", strings.Join(searchDomains, " ")) + } + + // attempt lock if full tunnel + if fullTunnel { + if err := lockResolvConf(true); err != nil { + } + } + + return nil +} + +// revertDnsFile is the fallback: restores backup and unlocks. +func revertDnsFile() error { + lockResolvConf(false) + + if _, err := os.Stat(resolvConfBak); os.IsNotExist(err) { + return nil + } + + src, err := os.ReadFile(resolvConfBak) + if err != nil { + return err + } + if err := os.WriteFile(resolvConfPath, src, 0644); err != nil { + return err + } + os.Remove(resolvConfBak) + return nil +} + +// backupResolvConf backs up resolv.conf if not already done. +func backupResolvConf() error { + if _, err := os.Stat(resolvConfBak); err == nil { + return nil + } + src, err := os.ReadFile(resolvConfPath) + if err != nil { + return err + } + return os.WriteFile(resolvConfBak, src, 0644) +} + +// lockResolvConf locks/unlocks with chattr (immutable). +func lockResolvConf(lock bool) error { + arg := "-i" + if lock { + arg = "+i" + } + // use filepath.Abs to handle symlinks properly + absPath, err := filepath.Abs(resolvConfPath) + if err != nil { + return err + } + cmd := exec.Command("chattr", arg, absPath) + if err := cmd.Run(); err != nil { + return fmt.Errorf("chattr %s: %w", arg, err) + } + return nil +} diff --git a/tunnel/tools/libwg-go/vpn/dns/dns_windows.go b/tunnel/tools/libwg-go/vpn/dns/dns_windows.go new file mode 100644 index 0000000..33defd4 --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/dns/dns_windows.go @@ -0,0 +1,79 @@ +//go:build windows + +package dns + +import ( + "fmt" + "net/netip" + "os/exec" + "strings" + + "github.com/amnezia-vpn/amneziawg-go/device" + "golang.org/x/net/nettest" + "golang.org/x/sys/windows" + "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" +) + +func SetDNS(luid winipcfg.LUID, dns []netip.Addr, searchDomains []string, fullTunnel bool, logger *device.Logger) error { + if fullTunnel { + // se global search domains + if len(searchDomains) > 0 { + pscmd := "Set-DnsClientGlobalSetting -SuffixSearchList @('" + strings.Join(searchDomains, "','") + "')" + cmd := exec.Command("powershell", "-Command", pscmd) + if err := cmd.Run(); err != nil { + logger.Errorf("set global search: %v", err) + } + } + } + + // set DNS on interface + v4dns, v6dns := []netip.Addr{}, []netip.Addr{} + for _, d := range dns { + if d.Is4() { + v4dns = append(v4dns, d) + } else if d.Is6() && nettest.SupportsIPv6() { + v6dns = append(v6dns, d) + } + } + + // v4 + if len(v4dns) > 0 || len(searchDomains) > 0 { + err := luid.SetDNS(windows.AF_INET, v4dns, searchDomains) + if err != nil { + return fmt.Errorf("set v4 dns: %w", err) + } + } + + // v6 + if len(v6dns) > 0 || len(searchDomains) > 0 { + err := luid.SetDNS(windows.AF_INET6, v6dns, searchDomains) + if err != nil { + return fmt.Errorf("set v6 dns: %w", err) + } + } + + return nil +} + +func RevertDNS(luid winipcfg.LUID, fullTunnel bool, originalSearchDomains []string, logger *device.Logger) error { + if fullTunnel && originalSearchDomains != nil { + // restore original global search + pscmd := "Set-DnsClientGlobalSetting -SuffixSearchList @('" + strings.Join(originalSearchDomains, "','") + "')" + cmd := exec.Command("powershell", "-Command", pscmd) + if err := cmd.Run(); err != nil { + logger.Errorf("restore global search: %v", err) + } + originalSearchDomains = nil + } else if fullTunnel { + // clear if no original + pscmd := "Set-DnsClientGlobalSetting -SuffixSearchList @()" + cmd := exec.Command("powershell", "-Command", pscmd) + cmd.Run() + } + + // clear DNS interface + luid.FlushDNS(windows.AF_INET) + luid.FlushDNS(windows.AF_INET6) + + return nil +} diff --git a/tunnel/tools/libwg-go/vpn/firewall/firewall.go b/tunnel/tools/libwg-go/vpn/firewall/firewall.go new file mode 100644 index 0000000..1eb969d --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/firewall/firewall.go @@ -0,0 +1,21 @@ +package firewall + +import "net/netip" + +// Firewall is responsible for managing the system's firewall rules, especially the kill switch. It operates independently of the router. +type Firewall interface { + + // Enable activates the kill switch, blocking all outbound traffic except + // explicitly allowed bypasses. + Enable() error + + // IsEnabled reports whether the kill switch is currently active. + IsEnabled() bool + + // Disable deactivates the kill switch and cleans up all rules. + Disable() error + + // AllowLocalNetworks adds bypass rules for the specified local network prefixes. Requires kill switch enabled and + // operates independently of tunnel/router bypasses. + AllowLocalNetworks([]netip.Prefix) error +} diff --git a/tunnel/tools/libwg-go/vpn/firewall/mark/mark.go b/tunnel/tools/libwg-go/vpn/firewall/mark/mark.go new file mode 100644 index 0000000..c6eb995 --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/firewall/mark/mark.go @@ -0,0 +1,16 @@ +package mark + +const ( + // LinuxFwmarkMaskNum Used to isolate bits 16-23 which are the safe range for custom marks + LinuxFwmarkMaskNum = 0xff0000 + // Our mark num + LinuxBypassMarkNum = 0x100000 + // LinuxBootstrapMarkNum is specifically for the DNS Resolver + LinuxBootstrapMarkNum = 0x200000 +) + +var ( + // LinuxBootstrapMarkBytes is the Little Endian representation for nftables + // 0x200000 -> [00, 00, 20, 00] + LinuxBootstrapMarkBytes = []byte{0x00, 0x00, 0x20, 0x00} +) diff --git a/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_linux.go b/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_linux.go new file mode 100644 index 0000000..dec66ff --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_linux.go @@ -0,0 +1,1085 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2026 WG Tunnel. +// Adapted from Tailscale + +//go:build linux && !android + +package osfirewall + +import ( + "encoding/binary" + "errors" + "fmt" + "net/netip" + "reflect" + "sync/atomic" + + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/google/nftables" + "github.com/google/nftables/expr" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall/mark" + "golang.org/x/net/nettest" + "golang.org/x/sys/unix" +) + +const ( + baseChainInput = "INPUT" + baseChainOutput = "OUTPUT" + baseChainPostrouting = "POSTROUTING" + baseChainForward = "FORWARD" + chainNameForward = "wgtunnel-forward" + chainNameInput = "wgtunnel-input" + chainNamePostrouting = "wgtunnel-postrouting" + chainNameOutput = "wgtunnel-output" + chainTypeRegular = "" +) + +type LinuxFirewall struct { + conn *nftables.Conn + nft4 *nftable // IPv4 tables, never nil + nft6 *nftable // IPv6 tables or nil if no IPv6 support + + v6Available bool + + tunnelPort uint16 + + killSwitchEnabled atomic.Bool + logger *device.Logger + + localAddrRules []*nftables.Rule // For tracking AllowedLocalNetworks rules + tunnelRules map[string][]*nftables.Rule // For tracking iface tunnel bypass rules +} + +func New(logger *device.Logger) (firewall.Firewall, error) { + conn, err := nftables.New() + if err != nil { + return nil, fmt.Errorf("nftables connection: %w", err) + } + + nft4 := &nftable{Proto: nftables.TableFamilyIPv4} + + supportsV6 := nettest.SupportsIPv6() + + var nft6 *nftable + if supportsV6 { + nft6 = &nftable{Proto: nftables.TableFamilyIPv6} + } + logger.Verbosef("nftables mode, v6 support: %v", supportsV6) + + f := &LinuxFirewall{ + conn: conn, + nft4: nft4, + nft6: nft6, + v6Available: supportsV6, + logger: logger, + tunnelRules: make(map[string][]*nftables.Rule), + } + return f, nil +} + +func (f *LinuxFirewall) AddTunnelBypasses(iface string) error { + if !f.IsEnabled() { + return errors.New("kill switch must be enabled to add tunnel bypasses") + } + + // remove old rules + _ = f.RemoveTunnelBypasses(iface) + + var newRules []*nftables.Rule + + for _, table := range f.getTables() { + outputChain, err := getChainFromTable(f.conn, table.Filter, chainNameOutput) + inputChain, _ := getChainFromTable(f.conn, table.Filter, chainNameInput) + if err != nil { + return fmt.Errorf("get output chain: %w", err) + } + + // apply tunnel mark + bootstrapRule := createFwmarkRule(table.Filter, outputChain, mark.LinuxBootstrapMarkNum) + f.conn.InsertRule(bootstrapRule) + newRules = append(newRules, bootstrapRule) + + // allow input for DNS boostrap + stateRule := &nftables.Rule{ + Table: table.Filter, + Chain: inputChain, + Exprs: []expr.Any{ + &expr.Ct{Key: expr.CtKeySTATE, Register: 1}, + &expr.Bitwise{ + SourceRegister: 1, + DestRegister: 1, + Len: 4, + Mask: []byte{0x06, 0x00, 0x00, 0x00}, // ESTABLISHED (2) | RELATED (4) + Xor: []byte{0x00, 0x00, 0x00, 0x00}, + }, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: []byte{0x00, 0x00, 0x00, 0x00}, + }, + &expr.Counter{}, + &expr.Verdict{Kind: expr.VerdictAccept}, + }, + } + f.conn.InsertRule(stateRule) + newRules = append(newRules, stateRule) + + // add tunnel interface bypass rule + tunnelBypassRule := &nftables.Rule{ + Table: table.Filter, + Chain: outputChain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyOIFNAME, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte(iface + "\x00"), + }, + &expr.Counter{}, + &expr.Verdict{Kind: expr.VerdictAccept}, + }, + } + existing, _ := findRule(f.conn, tunnelBypassRule) + if existing == nil { + f.conn.InsertRule(tunnelBypassRule) + newRules = append(newRules, tunnelBypassRule) + } + + // Add prefix bypass rules + //for _, prefix := range prefixes { + // if prefix.Addr().Is6() && !f.v6Available { + // continue + // } + // rule, err := createRangeRule(table.Filter, outputChain, prefix, expr.VerdictAccept) + // if err != nil { + // return fmt.Errorf("create bypass rule for %v: %w", prefix, err) + // } + // existing, _ = findRule(f.conn, rule) + // if existing == nil { + // f.conn.InsertRule(rule) + // newRules = append(newRules, rule) + // } + //} + } + + if err := f.conn.Flush(); err != nil { + return fmt.Errorf("flush after adding tunnel bypasses: %w", err) + } + + if f.tunnelRules == nil { + f.tunnelRules = make(map[string][]*nftables.Rule) + } + f.tunnelRules[iface] = newRules + + f.logger.Verbosef("Added/Updated tunnel bypasses for iface %s", iface) + return nil +} + +func (f *LinuxFirewall) RemoveTunnelBypasses(iface string) error { + if !f.IsEnabled() { + f.logger.Verbosef("Firewall is not enabled, skipping") + return nil + } + + rules, ok := f.tunnelRules[iface] + if !ok { + return nil + } + + for _, rule := range rules { + f.conn.DelRule(rule) + } + + if err := f.conn.Flush(); err != nil { + return fmt.Errorf("flush after removing tunnel bypasses: %w", err) + } + + delete(f.tunnelRules, iface) + + f.logger.Verbosef("Removed tunnel bypasses for iface %s", iface) + return nil +} + +func (f *LinuxFirewall) Disable() error { + if !f.IsEnabled() { + f.logger.Verbosef("Firewall is not enabled, skipping") + return nil + } + + // remove hooks + if err := f.deleteCustomHooks(); err != nil { + f.logger.Errorf("del hooks: %v", err) + } + + // flush base rules + if err := f.flushCustomChains(); err != nil { + f.logger.Errorf("del base: %v", err) + } + + // delete chains + if err := f.deleteCustomChains(); err != nil { + f.logger.Errorf("del chains: %v", err) + } + + // delete tables + for _, family := range []nftables.TableFamily{nftables.TableFamilyIPv4, nftables.TableFamilyIPv6} { + if err := deleteTableIfExists(f.conn, family, "filter"); err != nil { + f.logger.Errorf("delete filter table (%v): %v", family, err) + } + if err := deleteTableIfExists(f.conn, family, "nat"); err != nil { + f.logger.Errorf("delete nat table (%v): %v", family, err) + } + } + + if err := f.conn.Flush(); err != nil { + return fmt.Errorf("final flush: %w", err) + } + + f.localAddrRules = nil + f.tunnelRules = make(map[string][]*nftables.Rule) + + f.killSwitchEnabled.Store(false) + + f.logger.Verbosef("Firewall cleaned up and kill switch disabled") + return nil +} + +func (f *LinuxFirewall) AllowLocalNetworks(prefixes []netip.Prefix) error { + if !f.IsEnabled() { + return errors.New("kill switch must be enabled to allow local networks") + } + + // remove any old rules + for _, rule := range f.localAddrRules { + f.conn.DelRule(rule) + } + f.localAddrRules = nil + + // add bypass rules for each prefix + for _, table := range f.getTables() { + outputChain, err := getChainFromTable(f.conn, table.Filter, chainNameOutput) + if err != nil { + return fmt.Errorf("get output chain: %w", err) + } + for _, prefix := range prefixes { + if prefix.Addr().Is6() && !f.v6Available { + continue + } + rule, err := createRangeRule(table.Filter, outputChain, prefix, expr.VerdictAccept) + if err != nil { + return fmt.Errorf("create bypass rule for %v: %w", prefix, err) + } + existing, _ := findRule(f.conn, rule) + if existing == nil { + f.conn.AddRule(rule) + f.localAddrRules = append(f.localAddrRules, rule) + } + } + } + if err := f.conn.Flush(); err != nil { + return fmt.Errorf("flush after bypassing local addrs: %w", err) + } + f.logger.Verbosef("Bypassed local addrs: %v", prefixes) + return nil +} + +func (f *LinuxFirewall) IsEnabled() bool { + return f.killSwitchEnabled.Load() +} + +type nftable struct { + Proto nftables.TableFamily + Filter *nftables.Table + Nat *nftables.Table +} + +type chainInfo struct { + table *nftables.Table + name string + chainType nftables.ChainType + chainHook *nftables.ChainHook + chainPriority *nftables.ChainPriority + chainPolicy *nftables.ChainPolicy +} + +var ErrChainNotFound = errors.New("chain not found") + +type errorChainNotFound struct { + chainName string + tableName string +} + +func (e errorChainNotFound) Error() string { + return fmt.Sprintf("chain %s not found in table %s", e.chainName, e.tableName) +} + +func (e errorChainNotFound) Is(target error) bool { + return target == ErrChainNotFound +} + +// SetTunnelPort adds punch rules for inbound UDP on the port. +func (f *LinuxFirewall) SetTunnelPort(port uint16) error { + for _, table := range f.getTables() { + inputChain, err := getChainFromTable(f.conn, table.Filter, chainNameInput) + if err != nil { + return fmt.Errorf("get input chain: %w", err) + } + if err := addAcceptOnPortRule(f.conn, table.Filter, inputChain, port); err != nil { + return fmt.Errorf("add accept on port rule: %w", err) + } + } + if err := f.conn.Flush(); err != nil { + return fmt.Errorf("flush after adding port punch: %w", err) + } + f.tunnelPort = port + f.logger.Verbosef("Added tunnel port punch for UDP port %d", port) + return nil +} + +// addAcceptOnPortRule adds the rule if not exist +func addAcceptOnPortRule(conn *nftables.Conn, table *nftables.Table, chain *nftables.Chain, port uint16) error { + rule := createAcceptOnPortRule(table, chain, port) + existing, err := findRule(conn, rule) + if err != nil { + return fmt.Errorf("find rule: %w", err) + } + if existing != nil { + return nil // Already exists + } + conn.InsertRule(rule) + return nil // Flush called outside +} + +// createAcceptOnPortRule creates ACCEPT rule for UDP dport. +func createAcceptOnPortRule(table *nftables.Table, chain *nftables.Chain, port uint16) *nftables.Rule { + portBytes := make([]byte, 2) + // for network byte order + binary.BigEndian.PutUint16(portBytes, port) + return &nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + // load layer 4 protocol (for UDP/TCP) in register 1 temp storage + &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + Register: 1, + }, + // check if loaded register 1 storage is UDP + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte{unix.IPPROTO_UDP}, + }, + // load the destination port from register 1 + newLoadDportExpr(1), + // check if the port matches our wg listener port + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: portBytes, + }, + &expr.Counter{}, + // allow it on the firewall + &expr.Verdict{ + Kind: expr.VerdictAccept, + }, + }, + } +} + +// newLoadDportExpr loads dport to register +func newLoadDportExpr(destReg uint32) expr.Any { + return &expr.Payload{ + DestRegister: destReg, + Base: expr.PayloadBaseTransportHeader, + Offset: 2, + Len: 2, + } +} + +// deleteTableIfExists deletes a nftables table if it exists. +func deleteTableIfExists(c *nftables.Conn, family nftables.TableFamily, name string) error { + t, err := getTableIfExists(c, family, name) + if err != nil { + return fmt.Errorf("get table: %w", err) + } + if t == nil { + return nil // Not exist + } + c.DelTable(t) + if err := c.Flush(); err != nil { + return fmt.Errorf("del table: %w", err) + } + return nil +} + +// getTableIfExists returns the table if it exists. +func getTableIfExists(c *nftables.Conn, family nftables.TableFamily, name string) (*nftables.Table, error) { + tables, err := c.ListTables() + if err != nil { + return nil, fmt.Errorf("get tables: %w", err) + } + for _, table := range tables { + if table.Name == name && table.Family == family { + return table, nil + } + } + return nil, nil +} + +// createTableIfNotExist creates a nftables table if not exist. +func createTableIfNotExist(c *nftables.Conn, family nftables.TableFamily, name string) (*nftables.Table, error) { + if t, err := getTableIfExists(c, family, name); err != nil { + return nil, fmt.Errorf("get table: %w", err) + } else if t != nil { + return t, nil + } + t := c.AddTable(&nftables.Table{ + Family: family, + Name: name, + }) + if err := c.Flush(); err != nil { + return nil, fmt.Errorf("add table: %w", err) + } + return t, nil +} + +// getChainFromTable returns the chain if it exists. +func getChainFromTable(c *nftables.Conn, table *nftables.Table, name string) (*nftables.Chain, error) { + chains, err := c.ListChainsOfTableFamily(table.Family) + if err != nil { + return nil, fmt.Errorf("list chains: %w", err) + } + for _, chain := range chains { + if chain.Table.Name == table.Name && chain.Name == name { + return chain, nil + } + } + return nil, errorChainNotFound{chainName: name, tableName: table.Name} +} + +// createChainIfNotExist creates a chain if not exist. +func createChainIfNotExist(c *nftables.Conn, cinfo chainInfo) error { + _, err := getOrCreateChain(c, cinfo) + return err +} + +func getOrCreateChain(c *nftables.Conn, cinfo chainInfo) (*nftables.Chain, error) { + chain, err := getChainFromTable(c, cinfo.table, cinfo.name) + if err != nil && !errors.Is(err, ErrChainNotFound) { + return nil, fmt.Errorf("get chain: %w", err) + } else if err == nil { + // Existing chain; check compatibility if needed + return chain, nil + } + + chain = c.AddChain(&nftables.Chain{ + Name: cinfo.name, + Table: cinfo.table, + Type: cinfo.chainType, + Hooknum: cinfo.chainHook, + Priority: cinfo.chainPriority, + Policy: cinfo.chainPolicy, + }) + + if err := c.Flush(); err != nil { + return nil, fmt.Errorf("add chain: %w", err) + } + + return chain, nil +} + +// deleteChainIfExists deletes a chain if it exists. +func deleteChainIfExists(c *nftables.Conn, table *nftables.Table, name string) error { + chain, err := getChainFromTable(c, table, name) + if err != nil && !errors.Is(err, errorChainNotFound{table.Name, name}) { + return fmt.Errorf("get chain: %w", err) + } else if err != nil { + return nil // Not exist + } + + c.FlushChain(chain) + c.DelChain(chain) + + if err := c.Flush(); err != nil { + return fmt.Errorf("flush and delete chain: %w", err) + } + + return nil +} + +// getTables returns v4/v6 tables based on system support. +func (f *LinuxFirewall) getTables() []*nftable { + if f.v6Available { + return []*nftable{f.nft4, f.nft6} + } + return []*nftable{f.nft4} +} + +// getNFTByAddr selects v4/v6 table by addr family. +func (f *LinuxFirewall) getNFTByAddr(addr netip.Addr) (*nftable, error) { + if addr.Is6() && !f.v6Available { + return nil, fmt.Errorf("nftables for IPv6 not available") + } + if addr.Is6() { + return f.nft6, nil + } + return f.nft4, nil +} + +// findRule finds a rule by matching expressions. +func findRule(conn *nftables.Conn, rule *nftables.Rule) (*nftables.Rule, error) { + rules, err := conn.GetRules(rule.Table, rule.Chain) + if err != nil { + return nil, fmt.Errorf("get rules: %w", err) + } + for _, r := range rules { + if len(r.Exprs) != len(rule.Exprs) { + continue + } + match := true + for i, e := range r.Exprs { + if _, ok := e.(*expr.Counter); ok { + continue // Skip counters + } + if !reflect.DeepEqual(e, rule.Exprs[i]) { + match = false + break + } + } + if match { + return r, nil + } + } + return nil, nil +} + +func (f *LinuxFirewall) Enable() error { + if f.IsEnabled() { + f.logger.Verbosef("Kill switch already active, skipping activation") + return nil + } + + polAccept := nftables.ChainPolicyAccept + for _, table := range f.getTables() { + // Create filter table + filter, err := createTableIfNotExist(f.conn, table.Proto, "filter") + if err != nil { + return fmt.Errorf("create filter table: %w", err) + } + table.Filter = filter + + _, err = getOrCreateChain(f.conn, chainInfo{filter, baseChainForward, nftables.ChainTypeFilter, nftables.ChainHookForward, nftables.ChainPriorityFilter, &polAccept}) + if err != nil { + return fmt.Errorf("create FORWARD chain: %w", err) + } + _, err = getOrCreateChain(f.conn, chainInfo{filter, baseChainInput, nftables.ChainTypeFilter, nftables.ChainHookInput, nftables.ChainPriorityFilter, &polAccept}) + if err != nil { + return fmt.Errorf("create INPUT chain: %w", err) + } + _, err = getOrCreateChain(f.conn, chainInfo{filter, baseChainOutput, nftables.ChainTypeFilter, nftables.ChainHookOutput, nftables.ChainPriorityFilter, &polAccept}) + if err != nil { + return fmt.Errorf("create OUTPUT chain: %w", err) + } + + // Custom chains (regular, jumped to from conventional) + if err = createChainIfNotExist(f.conn, chainInfo{filter, chainNameForward, chainTypeRegular, nil, nil, nil}); err != nil { + return fmt.Errorf("create wgtunnel-forward chain: %w", err) + } + if err = createChainIfNotExist(f.conn, chainInfo{filter, chainNameInput, chainTypeRegular, nil, nil, nil}); err != nil { + return fmt.Errorf("create wgtunnel-input chain: %w", err) + } + if err = createChainIfNotExist(f.conn, chainInfo{filter, chainNameOutput, chainTypeRegular, nil, nil, nil}); err != nil { + return fmt.Errorf("create wgtunnel-output chain: %w", err) + } + + nat, err := createTableIfNotExist(f.conn, table.Proto, "nat") + if err != nil { + return fmt.Errorf("create nat table: %w", err) + } + table.Nat = nat + + _, err = getOrCreateChain(f.conn, chainInfo{nat, baseChainPostrouting, nftables.ChainTypeNAT, nftables.ChainHookPostrouting, nftables.ChainPriorityNATSource, &polAccept}) + if err != nil { + return fmt.Errorf("create POSTROUTING chain: %w", err) + } + if err = createChainIfNotExist(f.conn, chainInfo{nat, chainNamePostrouting, chainTypeRegular, nil, nil, nil}); err != nil { + return fmt.Errorf("create wgtunnel-postrouting chain: %w", err) + } + } + + if err := f.conn.Flush(); err != nil { + return fmt.Errorf("flush after chain creation: %w", err) + } + + if err := f.addHooks(); err != nil { + return fmt.Errorf("add hooks: %w", err) + } + + if err := f.addKillSwitchRules(); err != nil { + return fmt.Errorf("add kill switch rules: %w", err) + } + + f.killSwitchEnabled.Store(true) + return nil +} + +// addHooks adds jump rules from conventional chains to custom ones. +func (f *LinuxFirewall) addHooks() error { + conn := f.conn + + for _, table := range f.getTables() { + inputChain, err := getChainFromTable(conn, table.Filter, baseChainInput) + if err != nil { + return fmt.Errorf("get INPUT chain: %w", err) + } + if err = addHookRule(conn, table.Filter, inputChain, chainNameInput); err != nil { + return fmt.Errorf("add INPUT hook: %w", err) + } + + forwardChain, err := getChainFromTable(conn, table.Filter, baseChainForward) + if err != nil { + return fmt.Errorf("get FORWARD chain: %w", err) + } + if err = addHookRule(conn, table.Filter, forwardChain, chainNameForward); err != nil { + return fmt.Errorf("add FORWARD hook: %w", err) + } + + outputChain, err := getChainFromTable(conn, table.Filter, baseChainOutput) + if err != nil { + return fmt.Errorf("get OUTPUT chain: %w", err) + } + if err = addHookRule(conn, table.Filter, outputChain, chainNameOutput); err != nil { + return fmt.Errorf("add OUTPUT hook: %w", err) + } + + postroutingChain, err := getChainFromTable(conn, table.Nat, baseChainPostrouting) + if err != nil { + return fmt.Errorf("get POSTROUTING chain: %w", err) + } + if err = addHookRule(conn, table.Nat, postroutingChain, chainNamePostrouting); err != nil { + return fmt.Errorf("add POSTROUTING hook: %w", err) + } + } + return nil +} + +// createHookRule creates a jump rule. +func createHookRule(table *nftables.Table, fromChain *nftables.Chain, toChainName string) *nftables.Rule { + return &nftables.Rule{ + Table: table, + Chain: fromChain, + Exprs: []expr.Any{ + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictJump, + Chain: toChainName, + }, + }, + } +} + +// addHookRule inserts a jump rule at the top. +func addHookRule(conn *nftables.Conn, table *nftables.Table, fromChain *nftables.Chain, toChainName string) error { + rule := createHookRule(table, fromChain, toChainName) + conn.InsertRule(rule) + return conn.Flush() +} + +// addKillSwitchRules adds bypass for fwmark and DROP at end (private helper). +func (f *LinuxFirewall) addKillSwitchRules() error { + f.logger.Verbosef("Adding kill switch rules...") + + for _, table := range f.getTables() { + + inputChain, err := getChainFromTable(f.conn, table.Filter, chainNameInput) + if err != nil { + return fmt.Errorf("get input chain: %w", err) + } + + // allow loopback + if err := f.addLoopbackRule(table.Filter, inputChain); err != nil { + return err + } + + // allow Established/Related traffic for reply + if err := f.addEstablishedRule(table.Filter, inputChain); err != nil { + return err + } + + // drop everything else + dropRule := createDropRule(table.Filter, inputChain) + f.conn.AddRule(dropRule) + + outputChain, err := getChainFromTable(f.conn, table.Filter, chainNameOutput) + if err != nil { + return fmt.Errorf("get output chain: %w", err) + } + + // allow loopback on output + if err := f.addLoopbackRule(table.Filter, outputChain); err != nil { + return err + } + + // allow the marked tunnel traffic + bypassRule := createFwmarkRule(table.Filter, outputChain, mark.LinuxBypassMarkNum) + f.conn.InsertRule(bypassRule) + + // drop everything else + dropRule = createDropRule(table.Filter, outputChain) + f.conn.AddRule(dropRule) + + forwardChain, err := getChainFromTable(f.conn, table.Filter, chainNameForward) + if err != nil { + return fmt.Errorf("get forward chain: %w", err) + } + + // drop all forwarded traffic + dropRule = createDropRule(table.Filter, forwardChain) + f.conn.AddRule(dropRule) + } + + if err := f.conn.Flush(); err != nil { + return fmt.Errorf("flush after adding kill switch: %w", err) + } + f.logger.Verbosef("Kill switch rules added.") + return nil +} + +// addTunnelInterfaceRule adds a rule to let our tun interface escape firewall +func (f *LinuxFirewall) addTunnelInterfaceRule(iface string, table *nftables.Table, chain *nftables.Chain) error { + tunnelBypassRule := &nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: expr.MetaKeyOIFNAME, + Register: 1, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte(iface + "\x00"), + }, + &expr.Counter{}, + &expr.Verdict{Kind: expr.VerdictAccept}, + }, + } + existing, _ := findRule(f.conn, tunnelBypassRule) + if existing == nil { + f.conn.InsertRule(tunnelBypassRule) + } + return nil +} + +func (f *LinuxFirewall) addLoopbackRule(table *nftables.Table, chain *nftables.Chain) error { + loRule := &nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{ + Key: getIfKeyForChain(chain), + Register: 1, + }, + // Compare Register 1 to "lo" + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte("lo\x00"), // Null-terminated string + }, + &expr.Counter{}, + &expr.Verdict{Kind: expr.VerdictAccept}, + }, + } + f.conn.InsertRule(loRule) + return nil +} + +// Helper to determine if we should look at Input or Output interface +func getIfKeyForChain(chain *nftables.Chain) expr.MetaKey { + if chain.Name == chainNameInput { + return expr.MetaKeyIIFNAME + } + return expr.MetaKeyOIFNAME +} + +func (f *LinuxFirewall) addEstablishedRule(table *nftables.Table, chain *nftables.Chain) error { + rule := &nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + // Load Connection Tracking State + &expr.Ct{ + Key: expr.CtKeySTATE, + Register: 1, + }, + // Bitwise check for Established (0x02) | Related (0x04) = 0x06 + &expr.Bitwise{ + SourceRegister: 1, + DestRegister: 1, + Len: 4, + Mask: []byte{0x06, 0x00, 0x00, 0x00}, // Bits 1 and 2 (Est/Rel) + Xor: []byte{0x00, 0x00, 0x00, 0x00}, + }, + &expr.Cmp{ + Op: expr.CmpOpNeq, // If result is NOT 0, it matched one of the bits + Register: 1, + Data: []byte{0x00, 0x00, 0x00, 0x00}, + }, + &expr.Counter{}, + &expr.Verdict{Kind: expr.VerdictAccept}, + }, + } + + f.conn.InsertRule(rule) + return nil +} + +// delKillSwitchRules removes kill switch by flushing chains +func (f *LinuxFirewall) delKillSwitchRules() error { + f.logger.Verbosef("Removing kill switch rules...") + + for _, table := range f.getTables() { + if outputChain, err := getChainFromTable(f.conn, table.Filter, chainNameOutput); err == nil { + f.conn.FlushChain(outputChain) + } + + if inputChain, err := getChainFromTable(f.conn, table.Filter, chainNameInput); err == nil { + f.conn.FlushChain(inputChain) + } + + if forwardChain, err := getChainFromTable(f.conn, table.Filter, chainNameForward); err == nil { + f.conn.FlushChain(forwardChain) + } + } + + if err := f.conn.Flush(); err != nil { + return fmt.Errorf("flush after deleting kill switch: %w", err) + } + + f.logger.Verbosef("Kill switch rules removed.") + + return nil +} + +// createDropRule creates a simple DROP rule with counter +func createDropRule(table *nftables.Table, chain *nftables.Chain) *nftables.Rule { + return &nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Counter{}, + &expr.Verdict{Kind: expr.VerdictDrop}, + }, + } +} + +// createRangeRule creates ACCEPT for dst IP in prefix/range (adapted for daddr). +func createRangeRule( + table *nftables.Table, + chain *nftables.Chain, + rng netip.Prefix, + decision expr.VerdictKind, +) (*nftables.Rule, error) { + var loadExpr expr.Any + var maskLen uint32 + var mask []byte + var xor []byte + var err error + + if rng.Addr().Is4() { + loadExpr, err = newLoadDaddrExpr(nftables.TableFamilyIPv4, 1) + if err != nil { + return nil, fmt.Errorf("newLoadDaddrExpr: %w", err) + } + maskLen = 4 + mask = maskOf(rng) + xor = []byte{0x00, 0x00, 0x00, 0x00} + } else { + loadExpr, err = newLoadDaddrExpr(nftables.TableFamilyIPv6, 1) + if err != nil { + return nil, fmt.Errorf("newLoadDaddrExpr: %w", err) + } + maskLen = 16 + bits := rng.Bits() + mask = make([]byte, 16) + for i := 0; i < bits/8; i++ { + mask[i] = 0xff + } + if bits%8 != 0 { + mask[bits/8] = 0xff << (8 - uint(bits%8)) + } + xor = make([]byte, 16) + } + + netip := rng.Addr().AsSlice() + rule := &nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + loadExpr, + &expr.Bitwise{ + SourceRegister: 1, + DestRegister: 1, + Len: maskLen, + Mask: mask, + Xor: xor, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: netip, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: decision, + }, + }, + } + return rule, nil +} + +// newLoadDaddrExpr loads destination addr into register. +func newLoadDaddrExpr(proto nftables.TableFamily, destReg uint32) (expr.Any, error) { + switch proto { + case nftables.TableFamilyIPv4: + return &expr.Payload{ + DestRegister: destReg, + Base: expr.PayloadBaseNetworkHeader, + Offset: 16, // IPv4 offset + Len: 4, + }, nil + case nftables.TableFamilyIPv6: + return &expr.Payload{ + DestRegister: destReg, + Base: expr.PayloadBaseNetworkHeader, + Offset: 24, // IPv6 offset + Len: 16, + }, nil + default: + return nil, fmt.Errorf("unsupported family %v", proto) + } +} + +// createFwmarkRule generates a rule for a specific mark within our mask +func createFwmarkRule(table *nftables.Table, chain *nftables.Chain, markVal uint32) *nftables.Rule { + maskBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(maskBytes, mark.LinuxFwmarkMaskNum) + + markBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(markBytes, markVal) + + return &nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyMARK, Register: 1}, + &expr.Bitwise{ + SourceRegister: 1, + DestRegister: 1, + Len: 4, + Mask: maskBytes, + Xor: []byte{0x00, 0x00, 0x00, 0x00}, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: markBytes, + }, + &expr.Counter{}, + &expr.Verdict{Kind: expr.VerdictAccept}, + }, + } +} + +// maskOf returns CIDR mask bytes +func maskOf(pfx netip.Prefix) []byte { + mask := make([]byte, 4) + binary.BigEndian.PutUint32(mask, ^(uint32(0xffffffff) >> pfx.Bits())) + return mask +} + +// deleteCustomHooks removes jump rules from base to custom chains +func (f *LinuxFirewall) deleteCustomHooks() error { + conn := f.conn + for _, table := range f.getTables() { + if table == nil || table.Filter == nil { + continue // skip if table or filter not initialized + } + inputChain, err := getChainFromTable(conn, table.Filter, baseChainInput) + if err == nil && inputChain != nil { + deleteHookRule(conn, table.Filter, inputChain, chainNameInput) + } + + forwardChain, err := getChainFromTable(conn, table.Filter, baseChainForward) + if err == nil && forwardChain != nil { + deleteHookRule(conn, table.Filter, forwardChain, chainNameForward) + } + + outputChain, err := getChainFromTable(conn, table.Filter, baseChainOutput) + if err == nil && outputChain != nil { + deleteHookRule(conn, table.Filter, outputChain, chainNameOutput) + } + + if table.Nat == nil { + continue + } + postroutingChain, err := getChainFromTable(conn, table.Nat, baseChainPostrouting) + if err == nil && postroutingChain != nil { + deleteHookRule(conn, table.Nat, postroutingChain, chainNamePostrouting) + } + } + return conn.Flush() +} + +// deleteHookRule deletes a specific jump rule if it exists +func deleteHookRule(conn *nftables.Conn, table *nftables.Table, fromChain *nftables.Chain, toChainName string) error { + rule := createHookRule(table, fromChain, toChainName) + existing, err := findRule(conn, rule) + if err != nil || existing == nil { + return err // Or nil if not found + } + conn.DelRule(existing) + return nil +} + +// deleteCustomChains deletes custom chains +func (f *LinuxFirewall) deleteCustomChains() error { + for _, table := range f.getTables() { + deleteChainIfExists(f.conn, table.Filter, chainNameForward) + deleteChainIfExists(f.conn, table.Filter, chainNameInput) + deleteChainIfExists(f.conn, table.Filter, chainNameOutput) + deleteChainIfExists(f.conn, table.Nat, chainNamePostrouting) + } + return f.conn.Flush() +} + +// flushCustomChains flushes rules from custom chains +func (f *LinuxFirewall) flushCustomChains() error { + for _, table := range f.getTables() { + inputChain, err := getChainFromTable(f.conn, table.Filter, chainNameInput) + if err == nil { + f.conn.FlushChain(inputChain) + } + + forwardChain, err := getChainFromTable(f.conn, table.Filter, chainNameForward) + if err == nil { + f.conn.FlushChain(forwardChain) + } + + outputChain, err := getChainFromTable(f.conn, table.Filter, chainNameOutput) + if err == nil { + f.conn.FlushChain(outputChain) + } + + postrouteChain, err := getChainFromTable(f.conn, table.Nat, chainNamePostrouting) + if err == nil { + f.conn.FlushChain(postrouteChain) + } + } + return f.conn.Flush() +} diff --git a/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_macos.go b/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_macos.go new file mode 100644 index 0000000..b067bfe --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_macos.go @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2026 WG Tunnel. +// Adapted from Tailscale + +//go:build darwin + +package osfirewall + +import ( + "errors" + "fmt" + "net/netip" + "os" + "os/exec" + "path/filepath" + "strings" + "unsafe" + + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall" + "golang.org/x/net/bpf" + "golang.org/x/sys/unix" +) + +const ( + anchorName = "wgtunnel" + pfConfPath = "/etc/pf.conf" // System PF config; we'll append our anchor +) + +// macFirewall implements the firewall.Firewall interface for macOS using PF (Packet Filter). +type macFirewall struct { + tunnelPort uint16 // WireGuard listen port for inbound punch + killSwitchEnabled bool // Track if kill switch is active (not atomic, as PF is stateful) + v6Available bool // Whether the host supports IPv6 + logger *device.Logger +} + +func New(logger *device.Logger) (firewall.Firewall, error) { + v6err := CheckIPv6(logger) + supportsV6 := v6err == nil + logger.Verbosef("PF mode, v6 support: %v", supportsV6) + + return &macFirewall{ + v6Available: supportsV6, + logger: logger, + }, nil +} + +func (f *macFirewall) HasV6Available() bool { + return f.v6Available +} + +func (f *macFirewall) Active() bool { + return f.killSwitchEnabled +} + +// Enable initializes the firewall (e.g., ensures PF is enabled and our anchor is referenced). +func (f *macFirewall) Up() error { + // Ensure PF is enabled (macOS default is off; enable if needed) + if err := execSudoCommand("pfctl", "-e"); err != nil && !strings.Contains(err.Error(), "already enabled") { + return fmt.Errorf("enable PF: %w", err) + } + + // Add our anchor to /etc/pf.conf if not present (append if needed) + conf, err := os.ReadFile(pfConfPath) + if err != nil { + return fmt.Errorf("read pf.conf: %w", err) + } + if !strings.Contains(string(conf), fmt.Sprintf(`anchor "%s"`, anchorName)) { + if err := os.AppendFile(pfConfPath, []byte(fmt.Sprintf(`\nanchor "%s"\nload anchor "%s" from "/etc/pf.anchors/%s"\n`, anchorName, anchorName, anchorName)), 0644); err != nil { + return fmt.Errorf("append to pf.conf: %w", err) + } + if err := execSudoCommand("pfctl", "-f", pfConfPath); err != nil { + return fmt.Errorf("reload pf.conf: %w", err) + } + } + + f.logger.Verbosef("PF initialized") + return nil +} + +// SetTunnelPort sets the UDP port for the WireGuard tunnel and adds punch rules. +func (f *macFirewall) SetTunnelPort(port uint16) error { + rule := fmt.Sprintf("pass in quick proto udp to any port %d keep state", port) + if err := f.addRuleToAnchor(rule); err != nil { + return fmt.Errorf("add port punch rule: %w", err) + } + f.tunnelPort = port + f.logger.Verbosef("Added tunnel port punch for UDP port %d", port) + return nil +} + +// ToggleKillSwitch enables/disables the kill switch. +func (f *macFirewall) ToggleKillSwitch(enable bool) error { + if enable == f.killSwitchEnabled { + return nil + } + + if enable { + if err := f.addKillSwitchRules(); err != nil { + return fmt.Errorf("add kill switch rules: %w", err) + } + } else { + if err := f.delKillSwitchRules(); err != nil { + return fmt.Errorf("del kill switch rules: %w", err) + } + } + + f.killSwitchEnabled = enable + f.logger.Verbosef("Kill switch toggled: %v", enable) + return nil +} + +// addKillSwitchRules adds PF rules for kill switch (block non-tunnel outbound, with exemptions). +func (f *macFirewall) addKillSwitchRules() error { + rules := []string{ + "block out all", // Default block outbound + "pass out quick on utun0 all", // Allow on tunnel (adjust 'utun0' dynamically if needed) + "pass out quick to all", // Placeholder for SetBypassRoutes + // Add loopback allowance + "pass out quick on lo0 all", + "pass in quick on lo0 all", + // Established/related (PF handles keep state) + "pass out all keep state", + "pass in all keep state", + } + + if err := f.writeRulesToAnchor(rules); err != nil { + return err + } + f.logger.Verbosef("Kill switch rules added") + return nil +} + +// delKillSwitchRules removes kill switch rules by clearing the anchor. +func (f *macFirewall) delKillSwitchRules() error { + if err := f.clearAnchor(); err != nil { + return err + } + f.logger.Verbosef("Kill switch rules removed") + return nil +} + +// SetBypassRoutes adds exemptions for bootstrap routes. +func (f *macFirewall) SetBypassRoutes(bypassRoutes []netip.Prefix) error { + var rules []string + for _, route := range bypassRoutes { + rules = append(rules, fmt.Sprintf("pass out quick to %s all", route.String())) + } + if err := f.addRulesToAnchor(rules); err != nil { + return fmt.Errorf("add bypass routes: %w", err) + } + f.logger.Verbosef("Added bypass routes: %v", bypassRoutes) + return nil +} + +// TemporaryBypassSocket uses BPF to attach a filter to the socket for bypass. +func (f *macFirewall) TemporaryBypassSocket(fd int) (func() error, error) { + // Compile a simple BPF program to allow specific traffic (e.g., UDP to VPN ports) + // Example: Allow UDP dport == your tunnel port (adjust as needed) + // This is a basic UDP check; extend for port/IP as needed + instructions := []bpf.Instruction{ + bpf.LoadAbsolute{Off: 9, Size: 1}, // Load IP protocol (offset 9 in IP header) + bpf.JumpIf{Cond: bpf.JumpEqual, Val: 17, SkipFalse: 2}, // Check if UDP (17), jump to reject if not + // Add port check here if needed, e.g.: + // bpf.LoadAbsolute{Off: 22, Size: 2}, // Load UDP dport (network byte order, offset 20 src +2 dst in UDP) + // bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(f.tunnelPort), SkipFalse: 1}, + bpf.RetConstant{Val: 65535}, // Accept (return max packet length) + bpf.RetConstant{Val: 0}, // Reject + } + + prog, err := bpf.Assemble(instructions) // Compile to machine code + if err != nil { + return nil, fmt.Errorf("assemble BPF: %w", err) + } + + // Prepare SockFprog struct + sockFprog := unix.SockFprog{ + Len: uint16(len(prog)), + Filter: (*unix.Sockfilter)(unsafe.Pointer(&prog[0])), + } + + // Attach to socket using exported SetsockoptSockFprog + if err := unix.SetsockoptSockFprog(fd, unix.SOL_SOCKET, unix.SO_ATTACH_FILTER, &sockFprog); err != nil { + return nil, fmt.Errorf("attach BPF to fd %d: %w", fd, err) + } + f.logger.Verbosef("BPF bypass attached to fd %d", fd) + + return func() error { + // Detach BPF + if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_DETACH_FILTER, 0); err != nil { + f.logger.Errorf("Failed to detach BPF on fd %d: %v", fd, err) + return err + } + f.logger.Verbosef("BPF detached from fd %d", fd) + return nil + }, nil +} + +// Helper: writeRulesToAnchor writes rules to anchor file and reloads. +func (f *macFirewall) writeRulesToAnchor(rules []string) error { + anchorPath := filepath.Join("/etc/pf.anchors", anchorName) + content := strings.Join(rules, "\n") + if err := os.WriteFile(anchorPath, []byte(content), 0644); err != nil { + return fmt.Errorf("write anchor file: %w", err) + } + return f.reloadAnchor() +} + +// addRuleToAnchor appends a single rule and reloads. +func (f *macFirewall) addRuleToAnchor(rule string) error { + anchorPath := filepath.Join("/etc/pf.anchors", anchorName) + file, err := os.OpenFile(anchorPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return fmt.Errorf("open anchor file: %w", err) + } + defer file.Close() + if _, err := file.WriteString(rule + "\n"); err != nil { + return fmt.Errorf("append rule: %w", err) + } + return f.reloadAnchor() +} + +// addRulesToAnchor appends multiple rules and reloads. +func (f *macFirewall) addRulesToAnchor(rules []string) error { + anchorPath := filepath.Join("/etc/pf.anchors", anchorName) + file, err := os.OpenFile(anchorPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return fmt.Errorf("open anchor file: %w", err) + } + defer file.Close() + for _, rule := range rules { + if _, err := file.WriteString(rule + "\n"); err != nil { + return fmt.Errorf("append rule: %w", err) + } + } + return f.reloadAnchor() +} + +// clearAnchor clears the anchor file and reloads. +func (f *macFirewall) clearAnchor() error { + anchorPath := filepath.Join("/etc/pf.anchors", anchorName) + if err := os.WriteFile(anchorPath, []byte{}, 0644); err != nil { + return fmt.Errorf("clear anchor file: %w", err) + } + return f.reloadAnchor() +} + +// reloadAnchor reloads the PF anchor. +func (f *macFirewall) reloadAnchor() error { + if err := execSudoCommand("pfctl", "-a", anchorName, "-F", "all"); err != nil { + return fmt.Errorf("flush anchor: %w", err) + } + if err := execSudoCommand("pfctl", "-a", anchorName, "-f", filepath.Join("/etc/pf.anchors", anchorName)); err != nil { + return fmt.Errorf("load anchor: %w", err) + } + return nil +} + +// execSudoCommand runs a command with sudo (assumes sudo is available; handle prompts in app if needed). +func execSudoCommand(name string, args ...string) error { + cmd := exec.Command("sudo", append([]string{name}, args...)...) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %w\nOutput: %s", name, err, output) + } + return nil +} + +func CheckIPv6(logger *device.Logger) error { + // Similar to Linux: Check sysctl or interfaces for IPv6 + interfaces, err := net.Interfaces() + if err != nil { + return err + } + for _, iface := range interfaces { + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, addr := range addrs { + ip, _, err := net.ParseCIDR(addr.String()) + if err == nil && ip.To16() != nil && ip.To4() == nil { + logger.Verbosef("IPv6 detected on interface %s", iface.Name) + return nil + } + } + } + return errors.New("no IPv6 interfaces found") +} diff --git a/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_windows.go b/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_windows.go new file mode 100644 index 0000000..cc0e1a5 --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewall_windows.go @@ -0,0 +1,677 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2026 WG Tunnel. +// Adapted from Tailscale + +//go:build windows + +package osfirewall + +import ( + "fmt" + "net/netip" + "os" + "sync" + "sync/atomic" + + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall" + "golang.org/x/net/nettest" + "golang.org/x/sys/windows" + "inet.af/wf" + "tailscale.com/net/netaddr" +) + +type WindowsFirewall struct { + mu sync.Mutex // protect shared state + + logger *device.Logger + + session *wf.Session + + providerID wf.ProviderID + sublayerID wf.SublayerID + + iface string + + luid uint64 + + killSwitchEnabled atomic.Bool + + tunRules []*wf.Rule + localAddrRules []*wf.Rule + permittedRoutes map[netip.Prefix][]*wf.Rule +} + +type weight uint64 + +const ( + weightDaemonTraffic weight = 15 + weightKnownTraffic weight = 12 + weightCatchAll weight = 0 +) + +type protocol int + +const ( + protocolV4 protocol = iota + protocolV6 + protocolAll +) + +type direction int + +const ( + directionInbound direction = iota + directionOutbound + directionBoth +) + +// Known addresses. +var ( + linkLocalRange = netip.MustParsePrefix("fe80::/10") + linkLocalDHCPMulticast = netip.MustParseAddr("ff02::1:2") + siteLocalDHCPMulticast = netip.MustParseAddr("ff05::1:3") + linkLocalRouterMulticast = netip.MustParseAddr("ff02::2") +) + +func New(logger *device.Logger) (firewall.Firewall, error) { + session, err := wf.New(&wf.Options{ + Name: "WG Tunnel firewall", + Description: "Manages WG Tunnel firewall rules", + Dynamic: true, // Removes rules on close + }) + + if err != nil { + return nil, fmt.Errorf("create WFP session: %w", err) + } + + guid, err := windows.GenerateGUID() + if err != nil { + return nil, err + } + + providerID := wf.ProviderID(guid) + if err := session.AddProvider(&wf.Provider{ + ID: providerID, + Name: "WG Tunnel provider", + }); err != nil { + return nil, err + } + + guid, err = windows.GenerateGUID() + if err != nil { + return nil, err + } + + sublayerID := wf.SublayerID(guid) + if err := session.AddSublayer(&wf.Sublayer{ + ID: sublayerID, + Name: "WG Tunnel permissive and blocking filters", + Weight: uint16(weightCatchAll), + }); err != nil { + return nil, err + } + + f := &WindowsFirewall{ + logger: logger, + session: session, + providerID: providerID, + sublayerID: sublayerID, + permittedRoutes: make(map[netip.Prefix][]*wf.Rule), + tunRules: make([]*wf.Rule, 0), + } + return f, nil +} + +// addPermissiveRulesForPrefixes is a helper to add permissive rules for a list of prefixes +func (f *WindowsFirewall) addPermissiveRulesForPrefixes(prefixes []netip.Prefix, namePrefix string) (map[netip.Prefix][]*wf.Rule, error) { + f.mu.Lock() + defer f.mu.Unlock() + + addedByPrefix := make(map[netip.Prefix][]*wf.Rule) + var partialAdds []netip.Prefix // rollback tracking + for _, prefix := range prefixes { + if prefix.Addr().Is6() && !nettest.SupportsIPv6() { + continue + } + conditions := []*wf.Match{ + { + Field: wf.FieldIPRemoteAddress, + Op: wf.MatchTypeEqual, + Value: prefix, + }, + } + var p protocol + if prefix.Addr().Is4() { + p = protocolV4 + } else { + p = protocolV6 + } + rules, err := f.addRules(namePrefix+prefix.String(), weightKnownTraffic, conditions, wf.ActionPermit, p, directionBoth) + if err != nil { + for _, addedPrefix := range partialAdds { + if delErr := f.removeRules(addedByPrefix[addedPrefix]); delErr != nil { + f.logger.Errorf("Failed to delete partial rules for %v during rollback: %v", addedPrefix, delErr) + } + } + return nil, fmt.Errorf("add permissive rules for %v: %w", prefix, err) + } + addedByPrefix[prefix] = rules + partialAdds = append(partialAdds, prefix) + } + return addedByPrefix, nil +} + +// removeRules is a helper to remove a list of rules +func (f *WindowsFirewall) removeRules(rules []*wf.Rule) error { + f.mu.Lock() + defer f.mu.Unlock() + + for _, rule := range rules { + if err := f.session.DeleteRule(rule.ID); err != nil { + f.logger.Errorf("Failed to delete rule %s: %v", rule.Name, err) + // Continue to try deleting others + } + } + return nil +} + +func (f *WindowsFirewall) AllowLocalNetworks(addrs []netip.Prefix) error { + // cleanup old local addr rules + if err := f.removeRules(f.localAddrRules); err != nil { + f.logger.Errorf("Failed to remove old local addr rules: %v", err) + } + f.mu.Lock() + f.localAddrRules = nil + f.mu.Unlock() + + // add new rules + addedByPrefix, err := f.addPermissiveRulesForPrefixes(addrs, "bypass for local addr ") + if err != nil { + return err + } + f.mu.Lock() + f.localAddrRules = nil + for _, rules := range addedByPrefix { + f.localAddrRules = append(f.localAddrRules, rules...) + } + f.mu.Unlock() + f.logger.Verbosef("Bypassed local addrs in firewall") + return nil +} + +func (f *WindowsFirewall) UpdatePermittedRoutes(newRoutes []netip.Prefix) error { + f.mu.Lock() + // routes to remove + var routesToRemove []netip.Prefix + for existing := range f.permittedRoutes { + found := false + for _, newRoute := range newRoutes { + if existing == newRoute { + found = true + break + } + } + if !found { + routesToRemove = append(routesToRemove, existing) + } + } + f.mu.Unlock() + for _, r := range routesToRemove { + f.mu.Lock() + rules := f.permittedRoutes[r] + f.mu.Unlock() + if err := f.removeRules(rules); err != nil { + f.logger.Errorf("Failed to remove permitted route %v: %v", r, err) + } + f.mu.Lock() + delete(f.permittedRoutes, r) + f.mu.Unlock() + } + + // routes to add + var routesToAdd []netip.Prefix + f.mu.Lock() + for _, newRoute := range newRoutes { + if _, exists := f.permittedRoutes[newRoute]; !exists { + routesToAdd = append(routesToAdd, newRoute) + } + } + f.mu.Unlock() + + // add new rules + addedByPrefix, err := f.addPermissiveRulesForPrefixes(routesToAdd, "permitted route - ") + if err != nil { + return err + } + f.mu.Lock() + for prefix, rules := range addedByPrefix { + f.permittedRoutes[prefix] = rules + } + f.mu.Unlock() + + f.logger.Verbosef("Updated permitted routes: %v", newRoutes) + return nil +} + +// permitDaemon allows the daemon process through firewall +func (f *WindowsFirewall) permitDaemon(w weight) error { + f.mu.Lock() + defer f.mu.Unlock() + currentFile, err := os.Executable() + if err != nil { + return err + } + + appID, err := wf.AppID(currentFile) + if err != nil { + return fmt.Errorf("could not get app id for %q: %w", currentFile, err) + } + conditions := []*wf.Match{ + { + Field: wf.FieldALEAppID, + Op: wf.MatchTypeEqual, + Value: appID, + }, + } + _, err = f.addRules("unrestricted traffic for daemon", w, conditions, wf.ActionPermit, protocolAll, directionBoth) + return err +} + +func (f *WindowsFirewall) BypassTunnel(luid uint64, listenPort uint16) error { + f.mu.Lock() + f.luid = luid + f.mu.Unlock() + if err := f.permitTunInterface(weightDaemonTraffic); err != nil { + return fmt.Errorf("permitTunInterface failed: %w", err) + } + if err := f.permitListenPort(weightDaemonTraffic, listenPort); err != nil { + return fmt.Errorf("permitListenPort failed: %w", err) + } + return nil +} + +func (f *WindowsFirewall) Enable() error { + f.mu.Lock() + if f.killSwitchEnabled.Load() { + f.mu.Unlock() + f.logger.Verbosef("Kill switch already active, skipping activation") + return nil + } + f.mu.Unlock() + if err := f.permitDaemon(weightDaemonTraffic); err != nil { + return fmt.Errorf("permitTailscaleService failed: %w", err) + } + if err := f.permitLoopback(weightDaemonTraffic); err != nil { + return fmt.Errorf("permitLoopback failed: %w", err) + } + if err := f.permitDHCPv4(weightKnownTraffic); err != nil { + return fmt.Errorf("permitDHCPv4 failed: %w", err) + } + + if nettest.SupportsIPv6() { + if err := f.permitDHCPv6(weightKnownTraffic); err != nil { + return fmt.Errorf("permitDHCPv6 failed: %w", err) + } + + if err := f.permitNDP(weightKnownTraffic); err != nil { + return fmt.Errorf("permitNDP failed: %w", err) + } + } + + if err := f.blockAll(weightCatchAll); err != nil { + return fmt.Errorf("blockAll failed: %w", err) + } + + f.killSwitchEnabled.Store(true) + return nil +} + +func (f *WindowsFirewall) IsEnabled() bool { + return f.killSwitchEnabled.Load() +} + +func (f *WindowsFirewall) RemoveTunnelRules() error { + f.mu.Lock() + tunRulesCopy := make([]*wf.Rule, len(f.tunRules)) + copy(tunRulesCopy, f.tunRules) + f.tunRules = nil + f.mu.Unlock() + if err := f.removeRules(tunRulesCopy); err != nil { + f.logger.Errorf("Failed to remove tun rules: %v", err) + } + + f.mu.Lock() + permittedCopy := make(map[netip.Prefix][]*wf.Rule, len(f.permittedRoutes)) + for k, v := range f.permittedRoutes { + permittedCopy[k] = v + } + f.permittedRoutes = make(map[netip.Prefix][]*wf.Rule) + f.mu.Unlock() + for prefix, rules := range permittedCopy { + if err := f.removeRules(rules); err != nil { + f.logger.Errorf("Failed to remove permitted route %s: %v", prefix, err) + } + } + + f.logger.Verbosef("Tunnel rules and permitted routes removed") + return nil +} + +func (f *WindowsFirewall) Disable() error { + f.mu.Lock() + defer f.mu.Unlock() + if err := f.session.Close(); err != nil { + f.logger.Errorf("Failed to close WFP session: %v", err) + } + f.killSwitchEnabled.Store(false) + f.logger.Verbosef("Firewall rules and kill switch cleaned up") + return nil +} + +func (f *WindowsFirewall) permitLoopback(w weight) error { + f.mu.Lock() + defer f.mu.Unlock() + condition := []*wf.Match{ + { + Field: wf.FieldFlags, + Op: wf.MatchTypeFlagsAllSet, + Value: wf.ConditionFlagIsLoopback, + }, + } + _, err := f.addRules("on loopback", w, condition, wf.ActionPermit, protocolAll, directionBoth) + return err +} + +func (f *WindowsFirewall) permitListenPort(w weight, listenPort uint16) error { + f.mu.Lock() + defer f.mu.Unlock() + conditions := []*wf.Match{ + {Field: wf.FieldIPLocalInterface, Op: wf.MatchTypeEqual, Value: f.luid}, + {Field: wf.FieldIPProtocol, Op: wf.MatchTypeEqual, Value: wf.IPProtoUDP}, + {Field: wf.FieldIPLocalPort, Op: wf.MatchTypeEqual, Value: listenPort}, + } + rules, err := f.addRules("WireGuard UDP", w, conditions, wf.ActionPermit, protocolAll, directionInbound) + if err != nil { + return err + } + f.tunRules = append(f.tunRules, rules...) + return nil +} + +func (f *WindowsFirewall) permitDHCPv6(w weight) error { + f.mu.Lock() + defer f.mu.Unlock() + var dhcpConditions = func(remoteAddrs ...any) []*wf.Match { + conditions := []*wf.Match{ + { + Field: wf.FieldIPProtocol, + Op: wf.MatchTypeEqual, + Value: wf.IPProtoUDP, + }, + { + Field: wf.FieldIPLocalAddress, + Op: wf.MatchTypeEqual, + Value: linkLocalRange, + }, + { + Field: wf.FieldIPLocalPort, + Op: wf.MatchTypeEqual, + Value: uint16(546), + }, + { + Field: wf.FieldIPRemotePort, + Op: wf.MatchTypeEqual, + Value: uint16(547), + }, + } + for _, a := range remoteAddrs { + conditions = append(conditions, &wf.Match{ + Field: wf.FieldIPRemoteAddress, + Op: wf.MatchTypeEqual, + Value: a, + }) + } + return conditions + } + conditions := dhcpConditions(linkLocalDHCPMulticast, siteLocalDHCPMulticast) + if _, err := f.addRules("DHCP request", w, conditions, wf.ActionPermit, protocolV6, directionOutbound); err != nil { + return err + } + conditions = dhcpConditions(linkLocalRange) + if _, err := f.addRules("DHCP response", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil { + return err + } + return nil +} + +func (f *WindowsFirewall) permitDHCPv4(w weight) error { + f.mu.Lock() + defer f.mu.Unlock() + var dhcpConditions = func(remoteAddrs ...any) []*wf.Match { + conditions := []*wf.Match{ + { + Field: wf.FieldIPProtocol, + Op: wf.MatchTypeEqual, + Value: wf.IPProtoUDP, + }, + { + Field: wf.FieldIPLocalPort, + Op: wf.MatchTypeEqual, + Value: uint16(68), + }, + { + Field: wf.FieldIPRemotePort, + Op: wf.MatchTypeEqual, + Value: uint16(67), + }, + } + for _, a := range remoteAddrs { + conditions = append(conditions, &wf.Match{ + Field: wf.FieldIPRemoteAddress, + Op: wf.MatchTypeEqual, + Value: a, + }) + } + return conditions + } + conditions := dhcpConditions(netaddr.IPv4(255, 255, 255, 255)) + if _, err := f.addRules("DHCP request", w, conditions, wf.ActionPermit, protocolV4, directionOutbound); err != nil { + return err + } + + conditions = dhcpConditions() + if _, err := f.addRules("DHCP response", w, conditions, wf.ActionPermit, protocolV4, directionInbound); err != nil { + return err + } + return nil +} + +func (f *WindowsFirewall) permitNDP(w weight) error { + f.mu.Lock() + defer f.mu.Unlock() + // These are aliased according to: + // https://social.msdn.microsoft.com/Forums/azure/en-US/eb2aa3cd-5f1c-4461-af86-61e7d43ccc23/filtering-icmp-by-type-code?forum=wfp + fieldICMPType := wf.FieldIPLocalPort + fieldICMPCode := wf.FieldIPRemotePort + + var icmpConditions = func(t, c uint16, remoteAddress any) []*wf.Match { + conditions := []*wf.Match{ + { + Field: wf.FieldIPProtocol, + Op: wf.MatchTypeEqual, + Value: wf.IPProtoICMPV6, + }, + { + Field: fieldICMPType, + Op: wf.MatchTypeEqual, + Value: t, + }, + { + Field: fieldICMPCode, + Op: wf.MatchTypeEqual, + Value: c, + }, + } + if remoteAddress != nil { + conditions = append(conditions, &wf.Match{ + Field: wf.FieldIPRemoteAddress, + Op: wf.MatchTypeEqual, + Value: linkLocalRouterMulticast, + }) + } + return conditions + } + /* TODO: actually handle the hop limit somehow! The rules should vaguely be: + * - icmpv6 133: must be outgoing, dst must be FF02::2/128, hop limit must be 255 + * - icmpv6 134: must be incoming, src must be FE80::/10, hop limit must be 255 + * - icmpv6 135: either incoming or outgoing, hop limit must be 255 + * - icmpv6 136: either incoming or outgoing, hop limit must be 255 + * - icmpv6 137: must be incoming, src must be FE80::/10, hop limit must be 255 + */ + + // + // Router Solicitation Message + // ICMP type 133, code 0. Outgoing. + // + conditions := icmpConditions(133, 0, linkLocalRouterMulticast) + if _, err := f.addRules("NDP type 133", w, conditions, wf.ActionPermit, protocolV6, directionOutbound); err != nil { + return err + } + + // + // Router Advertisement Message + // ICMP type 134, code 0. Incoming. + // + conditions = icmpConditions(134, 0, linkLocalRange) + if _, err := f.addRules("NDP type 134", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil { + return err + } + + // + // Neighbor Solicitation Message + // ICMP type 135, code 0. Bi-directional. + // + conditions = icmpConditions(135, 0, nil) + if _, err := f.addRules("NDP type 135", w, conditions, wf.ActionPermit, protocolV6, directionBoth); err != nil { + return err + } + + // + // Neighbor Advertisement Message + // ICMP type 136, code 0. Bi-directional. + // + conditions = icmpConditions(136, 0, nil) + if _, err := f.addRules("NDP type 136", w, conditions, wf.ActionPermit, protocolV6, directionBoth); err != nil { + return err + } + + // + // Redirect Message + // ICMP type 137, code 0. Incoming. + // + conditions = icmpConditions(137, 0, linkLocalRange) + if _, err := f.addRules("NDP type 137", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil { + return err + } + return nil +} + +func (f *WindowsFirewall) blockAll(w weight) error { + f.mu.Lock() + defer f.mu.Unlock() + _, err := f.addRules("all", w, nil, wf.ActionBlock, protocolAll, directionBoth) + return err +} + +// addRules adds WFP rules with the given parameters +func (f *WindowsFirewall) addRules(name string, w weight, conditions []*wf.Match, action wf.Action, p protocol, d direction) ([]*wf.Rule, error) { + f.mu.Lock() + defer f.mu.Unlock() + var rules []*wf.Rule + for _, layer := range p.getLayers(d) { + r, err := f.newRule(name, w, layer, conditions, action) + if err != nil { + return nil, err + } + if err := f.session.AddRule(r); err != nil { + return nil, err + } + rules = append(rules, r) + } + return rules, nil +} + +// getLayers returns the wf.LayerIDs where the rules should be added based on the protocol and direction. +func (p protocol) getLayers(d direction) []wf.LayerID { + var layers []wf.LayerID + if p == protocolAll || p == protocolV4 { + if d == directionBoth || d == directionInbound { + layers = append(layers, wf.LayerALEAuthRecvAcceptV4) + } + if d == directionBoth || d == directionOutbound { + layers = append(layers, wf.LayerALEAuthConnectV4) + } + } + if p == protocolAll || p == protocolV6 { + if d == directionBoth || d == directionInbound { + layers = append(layers, wf.LayerALEAuthRecvAcceptV6) + } + if d == directionBoth || d == directionOutbound { + layers = append(layers, wf.LayerALEAuthConnectV6) + } + } + return layers +} + +func (f *WindowsFirewall) newRule(name string, w weight, layer wf.LayerID, conditions []*wf.Match, action wf.Action) (*wf.Rule, error) { + f.mu.Lock() + defer f.mu.Unlock() + id, err := windows.GenerateGUID() + if err != nil { + return nil, err + } + return &wf.Rule{ + Name: "WGTunnel-" + ruleName(action, layer, name), + ID: wf.RuleID(id), + Provider: f.providerID, + Sublayer: f.sublayerID, + Layer: layer, + Weight: uint64(w), + Conditions: conditions, + Action: action, + }, nil +} + +func ruleName(action wf.Action, layerID wf.LayerID, name string) string { + switch layerID { + case wf.LayerALEAuthConnectV4: + return fmt.Sprintf("%s outbound %s (IPv4)", action, name) + case wf.LayerALEAuthConnectV6: + return fmt.Sprintf("%s outbound %s (IPv6)", action, name) + case wf.LayerALEAuthRecvAcceptV4: + return fmt.Sprintf("%s inbound %s (IPv4)", action, name) + case wf.LayerALEAuthRecvAcceptV6: + return fmt.Sprintf("%s inbound %s (IPv6)", action, name) + } + return "" +} + +// permitTunInterface allows tun interface through firewall, requires luid to be set +func (f *WindowsFirewall) permitTunInterface(w weight) error { + f.mu.Lock() + defer f.mu.Unlock() + condition := []*wf.Match{ + { + Field: wf.FieldIPLocalInterface, + Op: wf.MatchTypeEqual, + Value: f.luid, + }, + } + rules, err := f.addRules("on TUN", w, condition, wf.ActionPermit, protocolAll, directionBoth) + if err != nil { + return err + } + f.tunRules = append(f.tunRules, rules...) + return nil +} diff --git a/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewallmgr/manager.go b/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewallmgr/manager.go new file mode 100644 index 0000000..7876b05 --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/firewall/osfirewall/firewallmgr/manager.go @@ -0,0 +1,25 @@ +package firewallmgr + +import ( + "sync" + + "github.com/wgtunnel/desktop/tunnel/shared" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall/osfirewall" +) + +var ( + instance firewall.Firewall + once sync.Once + initErr error +) + +func Get() (firewall.Firewall, error) { + once.Do(func() { + instance, initErr = osfirewall.New( + shared.NewLogger("Firewall"), + ) + }) + + return instance, initErr +} diff --git a/tunnel/tools/libwg-go/vpn/router/osrouter/router_linux.go b/tunnel/tools/libwg-go/vpn/router/osrouter/router_linux.go new file mode 100644 index 0000000..da1c797 --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/router/osrouter/router_linux.go @@ -0,0 +1,476 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2026 WG Tunnel. +// Adapted from Tailscale + +//go:build linux + +package osrouter + +import ( + "fmt" + "net" + "net/netip" + "slices" + + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun" + "github.com/vishvananda/netlink" + "github.com/wgtunnel/desktop/tunnel/vpn/dns" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall/mark" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall/osfirewall" + "github.com/wgtunnel/desktop/tunnel/vpn/router" + "golang.org/x/net/nettest" + "golang.org/x/sys/unix" +) + +const ( + rulePrioBootstrap = 50 + tunnelTableID = 52 + rulePrioMark = 100 + rulePrioExclude = 150 + rulePrioDefault = 200 +) + +type linuxRouter struct { + iface string + fw *osfirewall.LinuxFirewall + logger *device.Logger + prevConfig *router.Config + weEngagedKS bool + v4Full bool + v6Full bool + v6Available bool + + policyRules map[int][]*netlink.Rule +} + +func New(iface string, fw firewall.Firewall, _ tun.Device, logger *device.Logger) (router.Router, error) { + return &linuxRouter{ + iface: iface, + fw: fw.(*osfirewall.LinuxFirewall), + logger: logger, + v6Available: nettest.SupportsIPv6(), + policyRules: make(map[int][]*netlink.Rule), + }, nil +} + +func (r *linuxRouter) Set(c *router.Config) error { + newC := r.normalizeConfig(c) + prevC := r.normalizeConfig(r.prevConfig) + + if r.isUnchanged(newC) { + r.logger.Verbosef("Config unchanged, skipping") + return nil + } + + link, err := netlink.LinkByName(r.iface) + if err != nil { + return fmt.Errorf("get link %s: %w", r.iface, err) + } + + if err := netlink.LinkSetUp(link); err != nil { + return fmt.Errorf("set link up: %w", err) + } + + if err := r.syncFirewallState(newC); err != nil { + return err + } + + r.syncDeviceParams(link, newC, prevC) + + r.cleanupPreviousState(link, newC, prevC) + if err := r.applyNewAddresses(link, newC); err != nil { + return err + } + + if err := r.syncRoutingAndRules(link, newC); err != nil { + return err + } + + if err := r.syncDNS(newC, prevC); err != nil { + return err + } + + r.updatePrevState(newC) + return nil +} + +// Close closes the router. +func (r *linuxRouter) Close() error { + // revert DNS before cleanup + if r.prevConfig != nil { + if err := dns.RevertDns(r.iface, r.logger); err != nil { + r.logger.Errorf("revert DNS on close: %v", err) + } + } + + // cleanup + if err := r.Set(nil); err != nil { + r.logger.Errorf("cleanup set nil: %v", err) + } + + if r.weEngagedKS && r.fw.IsEnabled() { + r.logger.Verbosef("Disabling full tunnel kill switch for iface: %s", r.iface) + if err := r.fw.Disable(); err != nil { + return fmt.Errorf("failed to disable firewall: %w", err) + } + } else if r.fw.IsEnabled() { + r.logger.Verbosef("Removing firewall rules for iface: %s", r.iface) + if err := r.fw.RemoveTunnelBypasses(r.iface); err != nil { + return fmt.Errorf("failed remove firewall rules for iface %s : %v", r.iface, err) + } + } + + r.deletePolicyRules(netlink.FAMILY_V4) + r.deletePolicyRules(netlink.FAMILY_V6) + + r.logger.Verbosef("Router closed") + return nil +} + +func (r *linuxRouter) cleanupPreviousState(link netlink.Link, newC, prevC *router.Config) { + if r.prevConfig == nil { + return + } + + // remove old addresses + for _, a := range prevC.TunnelAddrs { + if !slices.Contains(newC.TunnelAddrs, a) { + ipnet := prefixToIPNet(a) + if err := netlink.AddrDel(link, &netlink.Addr{IPNet: ipnet}); err != nil { + r.logger.Errorf("del addr %v: %v", a, err) + } + } + } + + // remove old routes + prevV4Full := hasDefault(prevC, true) + prevV6Full := hasDefault(prevC, false) + newV4Full := hasDefault(newC, true) + newV6Full := hasDefault(newC, false) + + for _, rt := range prevC.Routes { + if !slices.Contains(newC.Routes, rt) { + table := unix.RT_TABLE_MAIN + if (rt.Addr().Is4() && prevV4Full) || (rt.Addr().Is6() && prevV6Full) { + table = tunnelTableID + } + dst := prefixToIPNet(rt) + route := &netlink.Route{LinkIndex: link.Attrs().Index, Dst: dst, Table: table} + _ = netlink.RouteDel(route) + } + } + + // clean up marks + if prevV4Full && !newV4Full { + r.deletePolicyRules(netlink.FAMILY_V4) + r.deleteBootstrapPolicyRules(netlink.FAMILY_V4) + } + if prevV6Full && !newV6Full { + r.deletePolicyRules(netlink.FAMILY_V6) + r.deleteBootstrapPolicyRules(netlink.FAMILY_V6) + } +} + +func (r *linuxRouter) normalizeConfig(c *router.Config) *router.Config { + if c == nil { + return &router.Config{} + } + return c +} + +func addrExists(existing []netlink.Addr, target *net.IPNet) bool { + for _, a := range existing { + if a.IPNet != nil && a.IPNet.String() == target.String() { + return true + } + } + return false +} + +func (r *linuxRouter) deleteExcludeRule(lr netip.Prefix) { + fam := netlink.FAMILY_V4 + if lr.Addr().Is6() { + fam = netlink.FAMILY_V6 + } + + dst := prefixToIPNet(lr) + rule := netlink.NewRule() + rule.Family = fam + rule.Priority = rulePrioExclude + rule.Dst = dst + rule.Table = unix.RT_TABLE_MAIN + + // ignore the error if rule is already gone + if err := netlink.RuleDel(rule); err != nil { + r.logger.Verbosef("del exclude rule %v: %v (ignored)", lr, err) + } +} + +func (r *linuxRouter) isUnchanged(newC *router.Config) bool { + if r.prevConfig == nil { + return false + } + return newC.Equal(r.prevConfig) +} + +func (r *linuxRouter) updatePrevState(newC *router.Config) { + r.v4Full = hasDefault(newC, true) + r.v6Full = hasDefault(newC, false) + r.prevConfig = newC.Clone() + r.logger.Verbosef("Router state updated: full v4=%v v6=%v", r.v4Full, r.v6Full) +} + +func (r *linuxRouter) syncFirewallState(newC *router.Config) error { + v4Full := hasDefault(newC, true) + v6Full := hasDefault(newC, false) + requiresKS := v4Full || v6Full + + if requiresKS && !r.fw.IsEnabled() { + if err := r.fw.Enable(); err != nil { + return fmt.Errorf("enable firewall: %w", err) + } + r.weEngagedKS = true + // add our marks for the tunnel and bootstrap + if err := r.fw.AddTunnelBypasses(r.iface); err != nil { + return fmt.Errorf("add firewall bypasses: %w", err) + } + } else if !requiresKS && r.weEngagedKS { + if err := r.fw.Disable(); err != nil { + return fmt.Errorf("disable firewall: %w", err) + } + r.weEngagedKS = false + } + return nil +} + +func (r *linuxRouter) syncDeviceParams(link netlink.Link, newC, prevC *router.Config) { + // sync mtu + if newC.MTU > 0 && newC.MTU != prevC.MTU { + _ = netlink.LinkSetMTU(link, newC.MTU) + } + + // sync ListenPort for fw + if newC.ListenPort != 0 && newC.ListenPort != prevC.ListenPort { + _ = r.fw.SetTunnelPort(newC.ListenPort) + } +} + +func (r *linuxRouter) syncDNS(newC, prevC *router.Config) error { + v4Full := hasDefault(newC, true) + v6Full := hasDefault(newC, false) + prevV4Full := hasDefault(prevC, true) + prevV6Full := hasDefault(prevC, false) + + // handle if DNS settings or tunnel state changed + dnsChanged := !slices.Equal(newC.DNS, prevC.DNS) || + !slices.Equal(newC.SearchDomains, prevC.SearchDomains) + stateChanged := (v4Full != prevV4Full) || (v6Full != prevV6Full) + + if dnsChanged || stateChanged { + return dns.SetDns(r.iface, newC.DNS, newC.SearchDomains, v4Full || v6Full, r.logger) + } + return nil +} + +func (r *linuxRouter) applyNewAddresses(link netlink.Link, newC *router.Config) error { + existingAddrs, _ := netlink.AddrList(link, netlink.FAMILY_ALL) + + for _, a := range newC.TunnelAddrs { + if a.Addr().Is6() && !r.v6Available { + continue + } + + ipNet := prefixToIPNet(a) + + if !addrExists(existingAddrs, ipNet) { + if err := netlink.AddrReplace(link, &netlink.Addr{IPNet: ipNet}); err != nil { + return fmt.Errorf("failed to add addr %v: %w", a, err) + } + } + } + return nil +} + +func (r *linuxRouter) syncRoutingAndRules(link netlink.Link, newC *router.Config) error { + v4Full := hasDefault(newC, true) + v6Full := hasDefault(newC, false) + + families := []int{netlink.FAMILY_V4} + if r.v6Available { + families = append(families, netlink.FAMILY_V6) + } + + for _, fam := range families { + isFull := (fam == netlink.FAMILY_V4 && v4Full) || (fam == netlink.FAMILY_V6 && v6Full) + + if isFull { + // add unnel rules + if err := r.addPolicyRules(fam); err != nil { + return err + } + // add bootstrap mark rule for DNS bootstrap + if err := r.addBootstrapPolicyRules(fam); err != nil { + return err + } + } + + routes := filterRoutes(newC.Routes, fam == netlink.FAMILY_V4) + table := unix.RT_TABLE_MAIN + if isFull { + table = tunnelTableID + } + + for _, rt := range routes { + if err := r.replaceRouteIdempotent(link, rt, table); err != nil { + return err + } + } + } + return nil +} + +func (r *linuxRouter) addBootstrapPolicyRules(family int) error { + mask := uint32(mark.LinuxFwmarkMaskNum) + rule := netlink.NewRule() + rule.Family = family + rule.Mark = mark.LinuxBootstrapMarkNum + rule.Mask = &mask + rule.Priority = 50 // set as high priority, above main tunnel rules + rule.Table = unix.RT_TABLE_MAIN // force bypass to ISP table + + return r.addRuleIdempotent(rule) +} + +func (r *linuxRouter) deleteBootstrapPolicyRules(family int) error { + rule := netlink.NewRule() + rule.Family = family + rule.Mark = mark.LinuxBootstrapMarkNum + rule.Priority = rulePrioBootstrap + return netlink.RuleDel(rule) +} + +func (r *linuxRouter) addRuleIdempotent(rule *netlink.Rule) error { + rules, err := netlink.RuleList(rule.Family) + if err != nil { + return err + } + + for _, existing := range rules { + if existing.Mark == rule.Mark && existing.Priority == rule.Priority && existing.Table == rule.Table { + return nil // Already exists + } + } + return netlink.RuleAdd(rule) +} + +func (r *linuxRouter) replaceRouteIdempotent(link netlink.Link, rt netip.Prefix, table int) error { + dst := prefixToIPNet(rt) + route := &netlink.Route{ + LinkIndex: link.Attrs().Index, + Dst: dst, + Table: table, + Type: unix.RTN_UNICAST, + } + return netlink.RouteReplace(route) +} + +// hasDefault returns true if config has default route for v4 (true) or v6 (false). +func hasDefault(c *router.Config, v4 bool) bool { + if c == nil { + return false + } + for _, rt := range c.Routes { + if rt.Bits() == 0 && ((v4 && rt.Addr().Is4()) || (!v4 && rt.Addr().Is6())) { + return true + } + } + return false +} + +// filterRoutes returns routes for v4 (true) or v6 (false). +func filterRoutes(routes []netip.Prefix, v4 bool) []netip.Prefix { + var filtered []netip.Prefix + for _, rt := range routes { + if (v4 && rt.Addr().Is4()) || (!v4 && rt.Addr().Is6()) { + filtered = append(filtered, rt) + } + } + return filtered +} + +// prefixToIPNet converts netip.Prefix to *net.IPNet. +func prefixToIPNet(p netip.Prefix) *net.IPNet { + if !p.IsValid() { + return nil + } + bits := p.Bits() + addr := p.Addr() + ip := net.IP(addr.AsSlice()) + mask := net.CIDRMask(bits, addr.BitLen()) + return &net.IPNet{IP: ip, Mask: mask} +} + +// addPolicyRules adds mark-based and default tunnel table rules for the family. +func (r *linuxRouter) addPolicyRules(fam int) error { + rules, err := netlink.RuleList(fam) + if err != nil { + return fmt.Errorf("list rules fam %d: %w", fam, err) + } + + // Mark rule: fwmark bypass -> main + markRule := netlink.NewRule() + markRule.Family = fam + markRule.Priority = rulePrioMark + markRule.Mark = mark.LinuxBypassMarkNum + markRule.Table = unix.RT_TABLE_MAIN + + markExists := false + for _, existing := range rules { + if existing.Priority == markRule.Priority && existing.Mark == markRule.Mark && existing.Table == markRule.Table { + markExists = true + break + } + } + if !markExists { + if err := netlink.RuleAdd(markRule); err != nil { + return fmt.Errorf("add mark rule fam %d: %w", fam, err) + } + r.policyRules[fam] = append(r.policyRules[fam], markRule) + } else { + r.logger.Verbosef("Mark rule fam %d already exists, skipping", fam) + } + + defaultRule := netlink.NewRule() + defaultRule.Family = fam + defaultRule.Priority = rulePrioDefault + defaultRule.Table = tunnelTableID + + defaultExists := false + for _, existing := range rules { + if existing.Priority == defaultRule.Priority && existing.Table == defaultRule.Table && existing.Dst == nil { + defaultExists = true + break + } + } + if !defaultExists { + if err := netlink.RuleAdd(defaultRule); err != nil { + return fmt.Errorf("add default tunnel rule fam %d: %w", fam, err) + } + r.policyRules[fam] = append(r.policyRules[fam], defaultRule) + } else { + r.logger.Verbosef("Default tunnel rule fam %d already exists, skipping", fam) + } + return nil +} + +// deletePolicyRules deletes the policy rules for the family. +func (r *linuxRouter) deletePolicyRules(fam int) { + for _, rule := range r.policyRules[fam] { + if err := netlink.RuleDel(rule); err != nil { + r.logger.Verbosef("del policy rule fam %d (prio %d): %v (ignored)", fam, rule.Priority, err) + } + } + r.policyRules[fam] = nil +} diff --git a/tunnel/tools/libwg-go/vpn/router/osrouter/router_windows.go b/tunnel/tools/libwg-go/vpn/router/osrouter/router_windows.go new file mode 100644 index 0000000..7e4bca8 --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/router/osrouter/router_windows.go @@ -0,0 +1,766 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright © 2026 WG Tunnel. +// Adapted from Tailscale + +//go:build windows + +package osrouter + +import ( + "errors" + "fmt" + "net/netip" + "os/exec" + "slices" + "sort" + "strings" + "syscall" + + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun" + "github.com/wgtunnel/desktop/tunnel/vpn/dns" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall/osfirewall" + "github.com/wgtunnel/desktop/tunnel/vpn/router" + "go4.org/netipx" + "golang.org/x/net/nettest" + "golang.org/x/sys/windows" + "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" +) + +type windowsRouter struct { + iface string + fw *osfirewall.WindowsFirewall + logger *device.Logger + prevConfig *router.Config + weEngagedKS bool + v6Available bool + nativeTun *tun.NativeTun + luid winipcfg.LUID + rawLuid uint64 + originalSearchDomains []string +} + +func New(iface string, fw firewall.Firewall, tunnel tun.Device, logger *device.Logger) (router.Router, error) { + nativeTun := tunnel.(*tun.NativeTun) + // get windows interface id + rawLuid := nativeTun.LUID() + return &windowsRouter{ + iface: iface, + fw: fw.(*osfirewall.WindowsFirewall), + logger: logger, + v6Available: nettest.SupportsIPv6(), + nativeTun: nativeTun, + rawLuid: rawLuid, + luid: winipcfg.LUID(rawLuid), + }, nil +} + +func (r *windowsRouter) Set(c *router.Config) error { + newC := c + if newC == nil { + newC = &router.Config{} + } + prevC := r.prevConfig + if prevC == nil { + prevC = &router.Config{} + } + + if newC.Equal(prevC) { + r.logger.Verbosef("Config unchanged, skipping") + return nil + } + + err := r.configureInterface(newC) + if err != nil { + r.logger.Errorf("ConfigureInterface: %v", err) + return err + } + + // dns + prevFull := prevC.HasAnyDefaultRoute() + newFull := newC.HasAnyDefaultRoute() + if !slices.Equal(newC.DNS, prevC.DNS) || !slices.Equal(newC.SearchDomains, prevC.SearchDomains) || newFull != prevFull { + if newFull && r.originalSearchDomains == nil { + var err error + r.originalSearchDomains, err = r.getGlobalSearchDomains() + if err != nil { + r.logger.Errorf("Failed to get original search domains: %v", err) + } + } + if err := dns.SetDNS(r.luid, newC.DNS, newC.SearchDomains, newFull, r.logger); err != nil { + return err + } + } + + requiresKS := newFull + if requiresKS && !r.fw.IsEnabled() { + if err := r.fw.Enable(); err != nil { + return err + } + if err := r.fw.BypassTunnel(r.rawLuid, newC.ListenPort); err != nil { + return err + } + //if err := r.fw.AllowLocalNetworks(newC.ExcludedRoutes); err != nil { + // return err + //} + r.weEngagedKS = true + } else if !requiresKS && r.weEngagedKS { + if err := r.fw.Disable(); err != nil { + return err + } + r.weEngagedKS = false + } + + if err := flushCaches(); err != nil { + r.logger.Errorf("flush dns: %v", err) + } + + r.prevConfig = newC.Clone() + return nil +} + +// subtractPrefixes returns the list of prefixes that cover "super" minus all "exclusions" +func subtractPrefixes(super netip.Prefix, exclusions []netip.Prefix) []netip.Prefix { + if !super.IsValid() { + return nil + } + + result := []netip.Prefix{super} + + for _, excl := range exclusions { + if !excl.IsValid() || excl.Bits() != excl.Addr().BitLen() { // skip non-host + continue + } + if !super.Contains(excl.Addr()) { + continue + } + + var newResult []netip.Prefix + for _, r := range result { + if !r.Contains(excl.Addr()) { + newResult = append(newResult, r) + continue + } + + // split the containing prefix + current := r + for current.Bits() < excl.Bits() { + splitBit := current.Bits() + lowMask := netip.PrefixFrom(current.Addr(), splitBit+1) + + if lowMask.Contains(excl.Addr()) { + // Add the high (sibling) half + siblingAddr := flipBit(current.Addr(), splitBit) + sibling := netip.PrefixFrom(siblingAddr, splitBit+1) + newResult = append(newResult, sibling) + current = lowMask // split the the low half + } else { + // add the low half, continue with high + newResult = append(newResult, lowMask) + highAddr := flipBit(current.Addr(), splitBit) + current = netip.PrefixFrom(highAddr, splitBit+1) + } + } + // drop the matching prefix to excluded + } + result = newResult + } + return result +} + +// getBit returns the value of the i-th bit +func getBit(addr netip.Addr, i int) bool { + if i < 0 || i >= addr.BitLen() { + return false + } + if addr.Is4() { + b := addr.As4() + byteIdx := i / 8 + bitIdx := 7 - (i % 8) // MSB first + return (b[byteIdx] & (1 << bitIdx)) != 0 + } + b := addr.As16() + byteIdx := i / 8 + bitIdx := 7 - (i % 8) + return (b[byteIdx] & (1 << bitIdx)) != 0 +} + +// setBit returns a new Addr with the i-th bit set to value +func setBit(addr netip.Addr, i int, value bool) netip.Addr { + if addr.Is4() { + b := addr.As4() + byteIdx := i / 8 + bitIdx := 7 - (i % 8) + if value { + b[byteIdx] |= 1 << bitIdx + } else { + b[byteIdx] &^= 1 << bitIdx + } + return netip.AddrFrom4(b) + } + b := addr.As16() + byteIdx := i / 8 + bitIdx := 7 - (i % 8) + if value { + b[byteIdx] |= 1 << bitIdx + } else { + b[byteIdx] &^= 1 << bitIdx + } + return netip.AddrFrom16(b) +} + +// flipBit returns a new Addr with the i-th bit flipped +func flipBit(addr netip.Addr, i int) netip.Addr { + return setBit(addr, i, !getBit(addr, i)) +} + +func (r *windowsRouter) Close() error { + if r.prevConfig != nil { + dns.RevertDNS(r.luid, r.prevConfig.HasAnyDefaultRoute(), r.originalSearchDomains, r.logger) + } + + r.Set(nil) + + r.logger.Verbosef("Router closed") + return nil +} + +// configureInterface uses the split route specificity approach to prevent routing loops +func (r *windowsRouter) configureInterface(cfg *router.Config) error { + iface, err := interfaceFromLUID(r.luid, winipcfg.GAAFlagIncludeAllInterfaces) + if err != nil { + return fmt.Errorf("getting interface: %v", err) + } + + _, err = r.setPrivateNetwork() + if err != nil { + r.logger.Verbosef("**WARNING** failed to set private network: %v", err) + } + + ipif4, err := r.luid.IPInterface(windows.AF_INET) + if err != nil && !errors.Is(err, windows.ERROR_NOT_FOUND) { + return fmt.Errorf("getting AF_INET interface: %v", err) + } + ipif6, err := r.luid.IPInterface(windows.AF_INET6) + if err != nil && !errors.Is(err, windows.ERROR_NOT_FOUND) { + return fmt.Errorf("getting AF_INET6 interface: %v", err) + } + + // Set up local tunnel addresses and gateways + var localAddr4, localAddr6 netip.Addr + var gatewayAddr4, gatewayAddr6 netip.Addr + addresses := make([]netip.Prefix, 0, len(cfg.TunnelAddrs)) + for _, addr := range cfg.TunnelAddrs { + if (addr.Addr().Is4() && ipif4 == nil) || (addr.Addr().Is6() && ipif6 == nil) { + continue + } + addresses = append(addresses, addr) + if addr.Addr().Is4() && !gatewayAddr4.IsValid() { + localAddr4 = addr.Addr() + gatewayAddr4 = netip.MustParseAddr("192.0.2.1") + } else if addr.Addr().Is6() && !gatewayAddr6.IsValid() { + localAddr6 = addr.Addr() + gatewayAddr6 = netip.MustParseAddr("fc00::1") + } + } + + var routes []*routeData + foundDefault4 := false + foundDefault6 := false + + for _, route := range cfg.Routes { + if (route.Addr().Is4() && ipif4 == nil) || (route.Addr().Is6() && ipif6 == nil) { + continue + } + + // Initialize IPv6 gateway if needed + if route.Addr().Is6() && !gatewayAddr6.IsValid() { + ip := netip.MustParseAddr("fc00::dead:beef") + addresses = append(addresses, netip.PrefixFrom(ip, ip.BitLen())) + gatewayAddr6 = ip + } + + var gateway, localAddr netip.Addr + if route.Addr().Is4() { + localAddr = localAddr4 + gateway = gatewayAddr4 + } else if route.Addr().Is6() { + localAddr = localAddr6 + gateway = gatewayAddr6 + } + + // split route for higher specificity over default route + if route.Bits() == 0 { + var splits []netip.Prefix + if route.Addr().Is4() { + splits = []netip.Prefix{ + netip.MustParsePrefix("0.0.0.0/1"), + netip.MustParsePrefix("128.0.0.0/1"), + } + foundDefault4 = true + } else { + splits = []netip.Prefix{ + netip.MustParsePrefix("::/1"), + netip.MustParsePrefix("8000::/1"), + } + foundDefault6 = true + } + + for _, p := range splits { + routes = append(routes, &routeData{ + RouteData: winipcfg.RouteData{ + Destination: p, + NextHop: gateway, + Metric: 0, + }, + }) + } + continue + } + + // non-default routes + if route.Addr().Unmap() == localAddr { + continue + } + if route.IsSingleIP() { + gateway = localAddr + } + + routes = append(routes, &routeData{ + RouteData: winipcfg.RouteData{ + Destination: route, + NextHop: gateway, + Metric: 0, + }, + }) + } + + err = syncAddresses(iface, addresses) + if err != nil { + return fmt.Errorf("syncAddresses: %v", err) + } + + slices.SortFunc(routes, (*routeData).Compare) + + var deduplicatedRoutes []*routeData + for i := 0; i < len(routes); i++ { + if i > 0 && routes[i].Destination == routes[i-1].Destination { + continue + } + deduplicatedRoutes = append(deduplicatedRoutes, routes[i]) + } + + iface, err = interfaceFromLUID(r.luid, winipcfg.GAAFlagIncludeAllInterfaces) + if err != nil { + return fmt.Errorf("getting interface after syncAddresses: %v", err) + } + + var errAcc error + err = syncRoutes(iface, deduplicatedRoutes, cfg.TunnelAddrs) + if err != nil { + errAcc = errors.Join(errAcc, err) + } + + if ipif4 != nil { + ipif4, err = r.luid.IPInterface(windows.AF_INET) + if err != nil { + return fmt.Errorf("getting AF_INET interface: %v", err) + } + if foundDefault4 { + ipif4.UseAutomaticMetric = false + ipif4.Metric = 0 + } + ipif4.NLMTU = uint32(cfg.MTU) + err = ipif4.Set() + if err != nil { + errAcc = errors.Join(errAcc, err) + } + } + + if ipif6 != nil { + ipif6, err = r.luid.IPInterface(windows.AF_INET6) + if err != nil { + return fmt.Errorf("getting AF_INET6 interface: %v", err) + } + if foundDefault6 { + ipif6.UseAutomaticMetric = false + ipif6.Metric = 0 + } + ipif6.NLMTU = uint32(cfg.MTU) + ipif6.DadTransmits = 0 + ipif6.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled + err = ipif6.Set() + if err != nil { + errAcc = errors.Join(errAcc, err) + } + } + + return errAcc +} + +func isIPv6LinkLocal(a netip.Prefix) bool { + return a.Addr().Is6() && a.Addr().IsLinkLocalUnicast() +} + +// ipAdapterUnicastAddressToPrefix converts windows IpAdapterUnicastAddress to netip.Prefix +func ipAdapterUnicastAddressToPrefix(u *windows.IpAdapterUnicastAddress) netip.Prefix { + ip, _ := netip.AddrFromSlice(u.Address.IP()) + return netip.PrefixFrom(ip.Unmap(), int(u.OnLinkPrefixLength)) +} + +// unicastIPNets returns all unicast net.IPNet for ifc interface. +func unicastIPNets(ifc *winipcfg.IPAdapterAddresses) []netip.Prefix { + var nets []netip.Prefix + for addr := ifc.FirstUnicastAddress; addr != nil; addr = addr.Next { + nets = append(nets, ipAdapterUnicastAddressToPrefix(addr)) + } + return nets +} + +func syncAddresses(ifc *winipcfg.IPAdapterAddresses, want []netip.Prefix) error { + got := unicastIPNets(ifc) + add, del := deltaNets(got, want) + var erracc error + ll := make([]netip.Prefix, 0) + for _, a := range del { + if isIPv6LinkLocal(a) { + ll = append(ll, a) + continue + } + err := ifc.LUID.DeleteIPAddress(a) + if err != nil { + erracc = errors.Join(erracc, fmt.Errorf("deleting IP %q: %v", a, err)) + } + } + for _, a := range add { + err := ifc.LUID.AddIPAddress(a) + if err != nil { + erracc = errors.Join(erracc, fmt.Errorf("adding IP %q: %v", a, err)) + } + } + for _, a := range ll { + mib, err := ifc.LUID.IPAddress(a.Addr()) + if err != nil { + erracc = errors.Join(erracc, fmt.Errorf("setting skip-as-source on IP %q: unable to retrieve MIB: %v", a, err)) + continue + } + if !mib.SkipAsSource { + mib.SkipAsSource = true + if err := mib.Set(); err != nil { + erracc = errors.Join(erracc, fmt.Errorf("setting skip-as-source on IP %q: unable to set MIB: %v", a, err)) + } + } + } + return erracc +} + +// routeData wraps winipcfg.RouteData with an additional field that permits +// caching of the associated MibIPForwardRow2; by keeping it around, we can +// avoid unnecessary lookups of information that we already have. +type routeData struct { + winipcfg.RouteData + Row *winipcfg.MibIPforwardRow2 +} + +func (rd *routeData) Compare(other *routeData) int { + v := rd.Destination.Addr().Compare(other.Destination.Addr()) + if v != 0 { + return v + } + b1, b2 := rd.Destination.Bits(), other.Destination.Bits() + if b1 != b2 { + if b1 > b2 { + return -1 + } + return 1 + } + v = rd.NextHop.Compare(other.NextHop) + if v != 0 { + return v + } + if rd.Metric < other.Metric { + return -1 + } else if rd.Metric > other.Metric { + return 1 + } + return 0 +} + +func deltaRouteData(a, b []*routeData) (add, del []*routeData) { + add = make([]*routeData, 0, len(b)) + del = make([]*routeData, 0, len(a)) + slices.SortFunc(a, (*routeData).Compare) + slices.SortFunc(b, (*routeData).Compare) + + i, j := 0, 0 + for i < len(a) && j < len(b) { + switch a[i].Compare(b[j]) { + case -1: + del = append(del, a[i]) + i++ + case 0: + i++ + j++ + case 1: + add = append(add, b[j]) + j++ + } + } + del = append(del, a[i:]...) + add = append(add, b[j:]...) + return +} + +// getInterfaceRoutes returns all the interface's routes. +func getInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses, family winipcfg.AddressFamily) (matches []*winipcfg.MibIPforwardRow2, err error) { + routes, err := winipcfg.GetIPForwardTable2(family) + if err != nil { + return nil, err + } + for i := range routes { + if routes[i].InterfaceLUID == ifc.LUID { + matches = append(matches, &routes[i]) + } + } + return +} + +func getAllInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses) ([]*routeData, error) { + routes4, err := getInterfaceRoutes(ifc, windows.AF_INET) + if err != nil { + return nil, err + } + + routes6, err := getInterfaceRoutes(ifc, windows.AF_INET6) + if err != nil { + // TODO: what if v6 unavailable? + return nil, err + } + + rd := make([]*routeData, 0, len(routes4)+len(routes6)) + for _, r := range routes4 { + rd = append(rd, &routeData{ + RouteData: winipcfg.RouteData{ + Destination: r.DestinationPrefix.Prefix(), + NextHop: r.NextHop.Addr(), + Metric: r.Metric, + }, + Row: r, + }) + } + for _, r := range routes6 { + rd = append(rd, &routeData{ + RouteData: winipcfg.RouteData{ + Destination: r.DestinationPrefix.Prefix(), + NextHop: r.NextHop.Addr(), + Metric: r.Metric, + }, + Row: r, + }) + } + return rd, nil +} + +func filterRoutes(routes []*routeData, dontDelete []netip.Prefix) []*routeData { + ddm := make(map[netip.Prefix]bool) + for _, dd := range dontDelete { + ddm[dd] = true + } + for _, r := range routes { + nr := r.Destination + if !nr.IsValid() { + continue + } + if nr.IsSingleIP() { + continue + } + lastIP := netipx.RangeOfPrefix(nr).To() + ddm[netip.PrefixFrom(lastIP, lastIP.BitLen())] = true + } + filtered := make([]*routeData, 0, len(routes)) + for _, r := range routes { + rr := r.Destination + if rr.IsValid() && ddm[rr] { + continue + } + filtered = append(filtered, r) + } + return filtered +} + +// syncRoutes incrementally sets multiples routes on an interface. +// This avoids a full ifc.FlushRoutes call. +// dontDelete is a list of interface address routes that the +// synchronization logic should never delete. +func syncRoutes(ifc *winipcfg.IPAdapterAddresses, want []*routeData, dontDelete []netip.Prefix) error { + existingRoutes, err := getAllInterfaceRoutes(ifc) + if err != nil { + return err + } + got := filterRoutes(existingRoutes, dontDelete) + + add, del := deltaRouteData(got, want) + + var errs []error + for _, a := range del { + var err error + if a.Row == nil { + // DeleteRoute requires a routing table lookup, so only do that if + // a does not already have the row. + err = ifc.LUID.DeleteRoute(a.Destination, a.NextHop) + } else { + // delete the row directly. + err = a.Row.Delete() + } + if err != nil { + dstStr := a.Destination.String() + if dstStr == "169.254.255.255/32" { + // Issue 785 (Tailscale). Ignore these routes + // failing to delete. Harmless. + continue + } + errs = append(errs, fmt.Errorf("deleting route %v: %v", dstStr, err)) + } + } + + for _, a := range add { + err := ifc.LUID.AddRoute(a.Destination, a.NextHop, a.Metric) + if err != nil { + errs = append(errs, fmt.Errorf("adding route %v: %v", &a.Destination, err)) + } + } + + return errors.Join(errs...) +} + +// deltaNets returns the changes to turn a into b. +func deltaNets(a, b []netip.Prefix) (add, del []netip.Prefix) { + add = make([]netip.Prefix, 0, len(b)) + del = make([]netip.Prefix, 0, len(a)) + sortNets(a) + sortNets(b) + + i, j := 0, 0 + for i < len(a) && j < len(b) { + switch netCompare(a[i], b[j]) { + case -1: + del = append(del, a[i]) + i++ + case 0: + i++ + j++ + case 1: + add = append(add, b[j]) + j++ + default: + panic("unexpected compare result") + } + } + del = append(del, a[i:]...) + add = append(add, b[j:]...) + return +} + +func netCompare(a, b netip.Prefix) int { + aip, bip := a.Addr().Unmap(), b.Addr().Unmap() + v := aip.Compare(bip) + if v != 0 { + return v + } + if a.Bits() == b.Bits() { + return 0 + } + if a.Bits() > b.Bits() { + return -1 + } + return 1 +} + +func sortNets(s []netip.Prefix) { + sort.Slice(s, func(i, j int) bool { + return netCompare(s[i], s[j]) < 0 + }) +} + +func (r *windowsRouter) getGlobalSearchDomains() ([]string, error) { + cmd := exec.Command("powershell", "-Command", "(Get-DnsClientGlobalSetting).SuffixSearchList") + output, err := cmd.Output() + if err != nil { + return nil, err + } + lines := strings.Split(strings.TrimSpace(string(output)), "\r\n") + var domains []string + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed != "" { + domains = append(domains, trimmed) + } + } + return domains, nil +} + +func (r *windowsRouter) setPrivateNetwork() (bool, error) { + alias := r.iface + + // Check if visible and get current category + cmd := exec.Command("powershell", "-Command", fmt.Sprintf(`Get-NetConnectionProfile -InterfaceAlias "%s" | Select-Object -ExpandProperty NetworkCategory`, alias)) + output, err := cmd.CombinedOutput() + if err != nil { + r.logger.Verbosef("setPrivateNetwork: Get-NetConnectionProfile failed: %v", err) + return false, err + } + + category := strings.TrimSpace(string(output)) + if category == "" { + r.logger.Verbosef("setPrivateNetwork: Adapter not found") + return false, nil + } + + if category == "Private" || category == "DomainAuthenticated" { + r.logger.Verbosef("setPrivateNetwork: Already private/domain, skipping") + return true, nil + } + + // Set to Private + cmd = exec.Command("powershell", "-Command", fmt.Sprintf(`Set-NetConnectionProfile -InterfaceAlias "%s" -NetworkCategory Private`, alias)) + output, err = cmd.CombinedOutput() + if err != nil { + r.logger.Errorf("setPrivateNetwork: Set-NetConnectionProfile failed: %v (output: %s)", err, output) + return false, err + } + + r.logger.Verbosef("setPrivateNetwork: Success") + return true, nil +} + +// interfaceFromLUID returns IPAdapterAddresses with specified LUID. +func interfaceFromLUID(luid winipcfg.LUID, flags winipcfg.GAAFlags) (*winipcfg.IPAdapterAddresses, error) { + addresses, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, flags) + if err != nil { + return nil, err + } + for _, addr := range addresses { + if addr.LUID == luid { + return addr, nil + } + } + return nil, fmt.Errorf("interfaceFromLUID: interface with LUID %v not found", luid) +} + +// Flush clears the local resolver cache. +// Only Windows has a public dns.Flush, needed in router_windows.go. Other +// platforms like Linux need a different flush implementation depending on +// the DNS manager. There is a FlushCaches method on the manager which +// can be used on all platforms. +func flushCaches() error { + cmd := exec.Command("ipconfig", "/flushdns") + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: windows.DETACHED_PROCESS, + } + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%v (output: %s)", err, out) + } + return nil +} diff --git a/tunnel/tools/libwg-go/vpn/router/router.go b/tunnel/tools/libwg-go/vpn/router/router.go new file mode 100644 index 0000000..07a39af --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/router/router.go @@ -0,0 +1,78 @@ +package router + +import ( + "net/netip" + "reflect" + "slices" +) + +// Router is responsible for managing the system network stack. +type Router interface { + // Set updates the OS network stack with a new Config. It may be + // called multiple times with identical Configs, which the + // implementation should handle gracefully. If it is a full tunnel config, kill switch is enabled + // for the duration of the tunnel already independently enabled. + Set(*Config) error + + // Close closes the router, cleaning up routes and disabling kill switch if it was enabled by the router. + Close() error +} + +// Config is the subset of configuration that is relevant to our Router +type Config struct { + // TunnelAddrs are the addresses for the tunnel interface + TunnelAddrs []netip.Prefix + + // DNS configured for the tunnel, falls back to system if not set + DNS []netip.Addr + + SearchDomains []string + + // Routes are the routes that point into the tunnel + // interface. These are the /32 and /128 routes to peers, (AllowedIps). + Routes []netip.Prefix + + // Falls back to WG default if not set + MTU int + + // Generated by system if not set + ListenPort uint16 +} + +func (c *Config) Equal(b *Config) bool { + if c == nil && b == nil { + return true + } + if (c == nil) != (b == nil) { + return false + } + return reflect.DeepEqual(c, b) +} + +func (c *Config) Clone() *Config { + if c == nil { + return nil + } + c2 := *c + c2.TunnelAddrs = slices.Clone(c.TunnelAddrs) + c2.DNS = slices.Clone(c.DNS) + c2.Routes = slices.Clone(c.Routes) + return &c2 +} + +// HasDefaultRoute checks if tunnel is full tunnel +func (c *Config) hasDefaultRoute(v4 bool) bool { + if c == nil { + return false + } + for _, rt := range c.Routes { + if rt.Bits() == 0 && ((v4 && rt.Addr().Is4()) || (!v4 && rt.Addr().Is6())) { + return true + } + } + return false +} + +func (c *Config) HasAnyDefaultRoute() bool { + return c.hasDefaultRoute(true) || c.hasDefaultRoute(false) +} diff --git a/tunnel/tools/libwg-go/vpn/vpn.go b/tunnel/tools/libwg-go/vpn/vpn.go new file mode 100755 index 0000000..d3d5a6b --- /dev/null +++ b/tunnel/tools/libwg-go/vpn/vpn.go @@ -0,0 +1,378 @@ +//go:build !android + +package vpn + +/* +#include +typedef void (*StatusCodeCallback)(int32_t handle, int32_t status); +*/ +import "C" +import ( + "context" + "errors" + "net" + "net/netip" + "os" + "os/signal" + "sync" + "sync/atomic" + "syscall" + + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun" + wireproxyawg "github.com/artem-russkikh/wireproxy-awg" + "github.com/wgtunnel/desktop/tunnel/constants" + "github.com/wgtunnel/desktop/tunnel/dns" + "github.com/wgtunnel/desktop/tunnel/ipc" + "github.com/wgtunnel/desktop/tunnel/shared" + "github.com/wgtunnel/desktop/tunnel/util" + bind2 "github.com/wgtunnel/desktop/tunnel/vpn/bind" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall" + "github.com/wgtunnel/desktop/tunnel/vpn/firewall/osfirewall/firewallmgr" + "github.com/wgtunnel/desktop/tunnel/vpn/router" + "github.com/wgtunnel/desktop/tunnel/vpn/router/osrouter" +) + +type TunnelHandle struct { + device *device.Device + uapi net.Listener + router router.Router + cancel context.CancelFunc + needsResolving atomic.Bool +} + +var ( + tag = "AwgVPN" + tunnelHandles = make(map[int32]*TunnelHandle) + resolvingHandles = sync.Map{} + logger = shared.NewLogger(tag) +) + +func init() { + // handle shutdown signals + go handleSignals() +} + +func handleSignals() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + <-sigs + awgTurnOffAll() + os.Exit(0) +} + +//export awgTurnOn +func awgTurnOn(settings *C.char, callback C.StatusCodeCallback) C.int { + handleID, err := util.GenerateHandle(tunnelHandles) + if err != nil { + shared.LogError(tag, "Unable to find empty handle", err) + return C.int(-1) + } + + shared.StoreTunnelCallback(handleID, shared.StatusCodeCallback(callback)) + + h := &TunnelHandle{} + var success bool + + defer func() { + if !success { + shared.LogDebug(tag, "Startup failed, cleaning up partial resources for handle %d", handleID) + h.close() + resolvingHandles.Delete(handleID) + } + }() + + goSettings := C.GoString(settings) + conf, err := wireproxyawg.ParseConfigString(goSettings) + if err != nil { + shared.LogError(tag, "Invalid config file", err) + return C.int(-1) + } + + // Create a context to manage resolution goroutines + tunnelCtx, tunnelCancel := context.WithCancel(context.Background()) + h.cancel = tunnelCancel + + // Check for peers needing resolution, but we wait to start resolution until the firewall bypasses are set + type peerToResolve struct { + index int + host string + } + var resolutionQueue []peerToResolve + + for i := range conf.Device.Peers { + peer := &conf.Device.Peers[i] + if peer.NeedsResolution() { + host, port, err := net.SplitHostPort(*peer.Endpoint) + if err != nil { + shared.LogError(tag, "Failed to parse endpoint", err) + continue + } + // set dummy, non-routable address with original port + dummyEndpoint := constants.DummyAddress + ":" + port + peer.Endpoint = &dummyEndpoint + + resolutionQueue = append(resolutionQueue, peerToResolve{i, host}) + } + } + + tunnel, err := tun.CreateTUN(constants.IfaceName, conf.Device.MTU) + if err != nil { + shared.LogError(tag, "Create TUN failed", err) + return C.int(-1) + } + + bind := conn.NewDefaultBind() + if err := bind2.SetupBind(logger, bind); err != nil { + tunnel.Close() + return C.int(-1) + } + + statusCB := func(code device.StatusCode) { + go shared.NotifyStatusCode(handleID, int32(code)) + } + + h.device = device.NewDevice(tunnel, bind, logger, false, statusCB) + + var listenPort uint16 = 0 + if conf.Device.ListenPort != nil { + listenPort = uint16(*conf.Device.ListenPort) + } + + _, port, err := h.device.Bind().Open(listenPort) + if err != nil { + shared.LogError(tag, "Failed to open bind", err) + return C.int(-1) + } + + ifaceName, _ := tunnel.Name() + uapi, err := ipc.SetupIPC(ifaceName) + if err != nil { + shared.LogError(tag, "Setup IPC failed", err) + return C.int(-1) + } + h.uapi = uapi + + go func(d *device.Device, l net.Listener) { + for { + connection, err := l.Accept() + if err != nil { + return + } + go d.IpcHandle(connection) + } + }(h.device, h.uapi) + + ipcRequest, err := wireproxyawg.CreateIPCRequest(conf.Device, false) + if err != nil { + return C.int(-1) + } + if err := h.device.IpcSet(ipcRequest.IpcRequest); err != nil { + return C.int(-1) + } + + fw, err := newFirewall() + if err != nil { + return C.int(-1) + } + + r, err := newRouter(ifaceName, fw, tunnel) + if err != nil { + return C.int(-1) + } + h.router = r + + if err := h.device.Up(); err != nil { + return C.int(-1) + } + + // parse config to router config for router/fw + routerCfg, err := parseToRouterConfig(conf, port) + if err != nil { + return C.int(-1) + } + if err := h.router.Set(routerCfg); err != nil { + return C.int(-1) + } + + // try to resolve DNS to replace our dummy endpoints + for _, p := range resolutionQueue { + go resolveAndUpdatePeer(tunnelCtx, handleID, conf, p.index, p.host) + } + + success = true + tunnelHandles[handleID] = h + shared.LogDebug(tag, "Device started successfully; DNS bypasses active for handle %d", handleID) + + return C.int(handleID) +} + +// resolveAndUpdatePeer resolves the host and updates the peer's endpoint if successful. +func resolveAndUpdatePeer(ctx context.Context, tunnelHandle int32, conf *wireproxyawg.Configuration, peerIndex int, host string) { + + resolvingHandles.Store(tunnelHandle, true) + shared.NotifyStatusCode(tunnelHandle, shared.StatusResolvingDNS) + + select { + case <-ctx.Done(): + shared.LogDebug(tag, "Tunnel context cancelled, stopping resolver for %s", host) + resolvingHandles.Delete(tunnelHandle) + return + default: + } + + opts := dns.DefaultOptions() + // TODO make configurable by user + preferIPv6 := false + + resolved, err := dns.ResolveWithBackoff(ctx, host, opts, preferIPv6, logger) + if err != nil { + shared.LogError(tag, "Permanent failure resolving %s: %v", host, err) + return + } + shared.LogDebug(tag, "Successfully resolved the tunnel peer endpoints..") + + var ip netip.Addr + if preferIPv6 && len(resolved.V6) > 0 { + ip = resolved.V6[0] + shared.LogDebug(tag, "Successfully set peer endpoint to preferred resolved ipv6..") + } else if len(resolved.V4) > 0 { + ip = resolved.V4[0] + shared.LogDebug(tag, "Successfully set peer endpoint to resolved ipv4..") + } else { + shared.LogError(tag, "No suitable IP resolved for %s", host) + return + } + + shared.LogDebug(tag, "Updating config with resolved peer endpoints..") + // Update the peer config's peer endpoint from dummy + peer := &conf.Device.Peers[peerIndex] + if err := peer.UpdateEndpointIP(ip); err != nil { + shared.LogError(tag, "Failed to update endpoint for peer %s: %v", peer.PublicKey, err) + return + } + + // Update peers via UAPI + ipcRequest, err := wireproxyawg.CreatePeerIPCRequest(conf.Device) + if err != nil { + shared.LogError(tag, "CreatePeerIPCRequest: %v", err) + return + } + + handle, ok := tunnelHandles[tunnelHandle] + if !ok || handle.cancel == nil { + shared.LogDebug(tag, "Tunnel down, skipping update for %s", host) + return + } + if err := handle.device.IpcSet(ipcRequest.IpcRequest); err != nil { + shared.LogError(tag, "Failed to update peers: %v", err) + return + } + + shared.LogDebug(tag, "Successfully updated peer with resolved endpoint for %s", host) + resolvingHandles.Delete(tunnelHandle) +} + +func (h *TunnelHandle) close() { + if h == nil { + return + } + + // stop all goroutines + if h.cancel != nil { + h.cancel() + } + + // close UAPI listener + if h.uapi != nil { + _ = h.uapi.Close() + } + + // close router to clean up router and firewall rules + if h.router != nil { + _ = h.router.Close() + } + + // close tun device + if h.device != nil { + h.device.Close() + } +} + +//export awgTurnOff +func awgTurnOff(tunnelHandle C.int) { + id := int32(tunnelHandle) + handle, ok := tunnelHandles[id] + if !ok { + shared.LogError(tag, "Tunnel is not up") + return + } + + delete(tunnelHandles, id) + handle.close() + resolvingHandles.Delete(id) +} + +//export awgGetConfig +func awgGetConfig(tunnelHandle C.int) *C.char { + goTunnelHandle := int32(tunnelHandle) + handle, ok := tunnelHandles[goTunnelHandle] + if !ok { + return nil + } + settings, err := handle.device.IpcGet() + if err != nil { + shared.LogError(tag, "Failed to get device config: %v", err) + return nil + } + return C.CString(settings) +} + +//export awgTurnOffAll +func awgTurnOffAll() { + for handle := range tunnelHandles { + awgTurnOff(C.int(handle)) + } + tunnelHandles = make(map[int32]*TunnelHandle) +} + +func newRouter(iface string, fw firewall.Firewall, tunnel tun.Device) (router.Router, error) { + return osrouter.New(iface, fw, tunnel, shared.NewLogger("Router")) +} + +func newFirewall() (firewall.Firewall, error) { + return firewallmgr.Get() +} + +func parseToRouterConfig(conf *wireproxyawg.Configuration, listenPort uint16) (*router.Config, error) { + device := conf.Device + if device == nil { + return nil, errors.New("no [Interface] section found in config") + } + + cfg := &router.Config{ + MTU: device.MTU, + } + + // Normalize and add tunnel addresses for router + for _, addr := range device.Address { + bitLen := 32 + if addr.Is6() { + bitLen = 128 + } + prefix := netip.PrefixFrom(addr, bitLen).Masked() + cfg.TunnelAddrs = append(cfg.TunnelAddrs, prefix) + } + + cfg.DNS = device.DNS + cfg.SearchDomains = device.SearchDomains + cfg.ListenPort = listenPort + + // Add peer routes (AllowedIPs) to router routes + for _, peer := range device.Peers { + cfg.Routes = append(cfg.Routes, peer.AllowedIPs...) + } + + return cfg, nil +} diff --git a/tunnel/tools/wintun/amd64/wintun.dll b/tunnel/tools/wintun/amd64/wintun.dll new file mode 100644 index 0000000000000000000000000000000000000000..aee04e77bfbd6601adbcd5a46435f059e31cf6e4 GIT binary patch literal 427552 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

l6@4tc8N5VA~I#-O;aK8jwH}g?6 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 S2U$OlRhm)2P)^SQ)BgZ@5?ieR literal 0 HcmV?d00001 diff --git a/tunnel/tools/wintun/arm64/wintun.dll b/tunnel/tools/wintun/arm64/wintun.dll new file mode 100644 index 0000000000000000000000000000000000000000..dc4e4aeeb1400f75fa40bfd4d2503a029c03c52c GIT binary patch literal 222488 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%0D)mq_#ZFXgZHZPKCT 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 literal 0 HcmV?d00001