Showing preview only (1,591K chars total). Download the full file or copy to clipboard to get everything.
Repository: Sovereign-Engineering/obscuravpn-client
Branch: main
Commit: e6df5885246b
Files: 443
Total size: 1.4 MB
Directory structure:
gitextract_1ne4upm2/
├── .editorconfig
├── .envrc
├── .git-blame-ignore-revs
├── .github/
│ └── workflows/
│ └── checks.yml
├── .gitignore
├── .shellcheckrc
├── .swiftformat
├── LICENSE.md
├── README.md
├── android/
│ ├── .gitignore
│ ├── .idea/
│ │ ├── .gitignore
│ │ ├── .name
│ │ ├── AndroidProjectSystem.xml
│ │ ├── codeStyles/
│ │ │ ├── Project.xml
│ │ │ └── codeStyleConfig.xml
│ │ ├── compiler.xml
│ │ ├── detekt.xml
│ │ ├── deviceManager.xml
│ │ ├── dictionaries/
│ │ │ └── project.xml
│ │ ├── gradle.xml
│ │ ├── kotlinc.xml
│ │ ├── ktfmt.xml
│ │ ├── migrations.xml
│ │ ├── misc.xml
│ │ ├── runConfigurations.xml
│ │ └── vcs.xml
│ ├── app/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── google-services.json
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ ├── foss/
│ │ │ └── java/
│ │ │ └── net/
│ │ │ └── obscura/
│ │ │ └── vpnclientapp/
│ │ │ └── BillingFacade.kt
│ │ ├── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── aidl/
│ │ │ │ └── net/
│ │ │ │ └── obscura/
│ │ │ │ └── vpnclientapp/
│ │ │ │ └── services/
│ │ │ │ └── IObscuraVpnService.aidl
│ │ │ ├── assets/
│ │ │ │ └── adi-registration.properties
│ │ │ ├── java/
│ │ │ │ └── net/
│ │ │ │ └── obscura/
│ │ │ │ └── vpnclientapp/
│ │ │ │ ├── App.kt
│ │ │ │ ├── activities/
│ │ │ │ │ └── MainActivity.kt
│ │ │ │ ├── client/
│ │ │ │ │ ├── ErrorCodeException.kt
│ │ │ │ │ ├── JsonConfig.kt
│ │ │ │ │ ├── ManagerCmd.kt
│ │ │ │ │ ├── ManagerCmdOk.kt
│ │ │ │ │ ├── ObscuraLibrary.java
│ │ │ │ │ └── RustFfi.kt
│ │ │ │ ├── helpers/
│ │ │ │ │ └── Process.kt
│ │ │ │ ├── preferences/
│ │ │ │ │ └── Preferences.kt
│ │ │ │ ├── services/
│ │ │ │ │ ├── ContextExtension.kt
│ │ │ │ │ ├── IntentExtension.kt
│ │ │ │ │ ├── ObscuraVpnService.kt
│ │ │ │ │ └── OsNetworkConfig.kt
│ │ │ │ ├── sharing/
│ │ │ │ │ └── DebugArchiveFileProvider.java
│ │ │ │ └── ui/
│ │ │ │ ├── BillingModule.kt
│ │ │ │ ├── JsonFfiBroadcastReceiver.kt
│ │ │ │ ├── NetworkStatusObserver.kt
│ │ │ │ ├── ObscuraUI.kt
│ │ │ │ ├── ObscuraWebView.kt
│ │ │ │ ├── OsStatus.kt
│ │ │ │ ├── OsStatusManager.kt
│ │ │ │ ├── VpnPermissionRequestManager.kt
│ │ │ │ ├── VpnStatusObserver.kt
│ │ │ │ └── bridge/
│ │ │ │ ├── WebCmd.kt
│ │ │ │ ├── WebCmdBridge.kt
│ │ │ │ └── WebCmdHelpers.kt
│ │ │ └── res/
│ │ │ ├── drawable/
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ ├── icon_about.xml
│ │ │ │ ├── icon_account.xml
│ │ │ │ ├── icon_connection.xml
│ │ │ │ ├── icon_location.xml
│ │ │ │ └── icon_settings.xml
│ │ │ ├── layout/
│ │ │ │ └── activity_main.xml
│ │ │ ├── menu/
│ │ │ │ └── nav_menu.xml
│ │ │ ├── values/
│ │ │ │ ├── colors.xml
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── ids.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── themes.xml
│ │ │ ├── values-night/
│ │ │ │ └── themes.xml
│ │ │ └── xml/
│ │ │ ├── backup_rules.xml
│ │ │ ├── data_extraction_rules.xml
│ │ │ └── debug_archive_file_provider_paths.xml
│ │ └── play/
│ │ └── java/
│ │ └── net/
│ │ └── obscura/
│ │ └── vpnclientapp/
│ │ └── BillingFacade.kt
│ ├── build.gradle.kts
│ ├── buildSrc/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── settings.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── kotlin/
│ │ └── VersionName.kt
│ ├── detekt.yml
│ ├── gradle/
│ │ ├── libs.versions.toml
│ │ ├── mitm-cache/
│ │ │ └── deps.json
│ │ └── wrapper/
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── lib/
│ │ ├── billing/
│ │ │ ├── build.gradle.kts
│ │ │ └── src/
│ │ │ └── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java/
│ │ │ └── net/
│ │ │ └── obscura/
│ │ │ └── lib/
│ │ │ └── billing/
│ │ │ ├── BillingConnection.kt
│ │ │ └── BillingManager.kt
│ │ └── util/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── net/
│ │ └── obscura/
│ │ └── lib/
│ │ └── util/
│ │ ├── ExternallyTaggedEnumSerializer.kt
│ │ ├── ExternallyTaggedEnumVariantSerializer.kt
│ │ └── Log.kt
│ ├── lint.xml
│ └── settings.gradle.kts
├── apple/
│ ├── Configurations/
│ │ ├── .gitignore
│ │ ├── Base.xcconfig
│ │ ├── Debug-app-network-extension.xcconfig
│ │ ├── Debug-app.xcconfig
│ │ ├── Debug-system-network-extension.xcconfig
│ │ ├── Debug.xcconfig
│ │ ├── Release-app-network-extension.xcconfig
│ │ ├── Release-app.xcconfig
│ │ ├── Release-system-network-extension.xcconfig
│ │ ├── Release.xcconfig
│ │ ├── app-network-extension.xcconfig
│ │ ├── app.xcconfig
│ │ ├── bundle-ids.xcconfig
│ │ └── system-network-extension.xcconfig
│ ├── ExportOptions.plist
│ ├── Packet Tunnel Provider/
│ │ ├── Keychain.swift
│ │ ├── NetworkSettings.swift
│ │ ├── PacketTunnelProvider.swift
│ │ ├── RustFfi.swift
│ │ └── main.swift
│ ├── app-network-extension/
│ │ ├── Info.plist
│ │ └── entitlements.entitlements
│ ├── cbindgen-apple.toml
│ ├── client/
│ │ ├── Assets.xcassets/
│ │ │ ├── AccentColor.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── DecoPrimer.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── EmotePrimer.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── MenuBarConnected.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── MenuBarConnectedDown.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── MenuBarConnectedUp.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── MenuBarConnectedUpDown.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── MenuBarConnecting-1.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── MenuBarConnecting-2.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── MenuBarConnecting-3.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── MenuBarDisconnected.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── ObscuraOrange.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── UpdateAvailable.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── custom.globe.badge.gearshape.fill.symbolset/
│ │ │ └── Contents.json
│ │ ├── Constants.swift
│ │ ├── ContentView.swift
│ │ ├── DebugBundle+XP.swift
│ │ ├── DebugBundle.swift
│ │ ├── DebugBundleExtensionInfo.swift
│ │ ├── Info.plist
│ │ ├── LoginItem.swift
│ │ ├── LoopingVideoPlayer.swift
│ │ ├── Notifications.swift
│ │ ├── OSLogEntryEncodable.swift
│ │ ├── OsStatus.swift
│ │ ├── Preview Content/
│ │ │ └── Preview Assets.xcassets/
│ │ │ └── Contents.json
│ │ ├── ScriptMessageHandlers.swift
│ │ ├── StartupStatus.swift
│ │ ├── StatusItem/
│ │ │ ├── AccountStatusItem.swift
│ │ │ ├── BandwidthStatus.swift
│ │ │ ├── MenuItemView.swift
│ │ │ ├── ObscuraToggle.swift
│ │ │ └── StatusMenu.swift
│ │ ├── Store/
│ │ │ ├── Obscura VPN Local.storekit
│ │ │ ├── Obscura VPN.storekit
│ │ │ ├── Product+Convenience.swift
│ │ │ ├── StoreKitListener.swift
│ │ │ └── StoreKitModel.swift
│ │ ├── Style/
│ │ │ ├── Appearance.swift
│ │ │ ├── ConditionallyDisabled.swift
│ │ │ ├── HyperlinkButtonStyle.swift
│ │ │ └── NoFadeButtonStyle.swift
│ │ ├── TunnelProvider.swift
│ │ ├── UXKit/
│ │ │ ├── UXImage.swift
│ │ │ ├── UXViewController.swift
│ │ │ └── UXViewRepresentable.swift
│ │ ├── UpdaterDriver+XP.swift
│ │ ├── Webviews/
│ │ │ ├── ExternalWebView.swift
│ │ │ ├── ObscuraUIIOSWrapperAndTabs.swift
│ │ │ ├── ObscuraUIMacOSWrapper.swift
│ │ │ ├── ObscuraUIWebView.swift
│ │ │ ├── ObscuraUIWebViewMacOSWrapper.swift
│ │ │ └── WebviewsController.swift
│ │ ├── app_state.swift
│ │ ├── client-ios.entitlements
│ │ ├── client-macos.entitlements
│ │ ├── command.swift
│ │ ├── extensions/
│ │ │ └── NEVPNStatus.swift
│ │ ├── iOS/
│ │ │ ├── MailDelegate.swift
│ │ │ └── iOSClientApp.swift
│ │ ├── initNetworkExtension.swift
│ │ ├── macOS/
│ │ │ ├── CheckForUpdatesView.swift
│ │ │ ├── ClientApp.swift
│ │ │ ├── InstallSystemExtensionView.swift
│ │ │ ├── RegisterLoginItemView.swift
│ │ │ ├── SparkleUpdater.swift
│ │ │ ├── UpdateSystemExtensionView.swift
│ │ │ └── UpdaterDriver.swift
│ │ └── startup.swift
│ ├── client.xcodeproj/
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace/
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata/
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ └── swiftpm/
│ │ │ └── Package.resolved
│ │ └── xcshareddata/
│ │ └── xcschemes/
│ │ ├── App Network Extension.xcscheme
│ │ ├── Dev Client.xcscheme
│ │ ├── Obscura VPN iOS.xcscheme
│ │ ├── Prod Client.xcscheme
│ │ └── System Network Extension.xcscheme
│ ├── dmg-building/
│ │ └── installer_background.tiff
│ ├── libobscuravpn_client/
│ │ └── .gitignore
│ ├── obscuravpn-client.xcodeproj/
│ │ └── project.pbxproj
│ ├── shared/
│ │ ├── Account+CustomStringConvertible.swift
│ │ ├── Account.swift
│ │ ├── AccountInfo+Util.swift
│ │ ├── Box.swift
│ │ ├── Concurrency.swift
│ │ ├── ConcurrencyTests.swift
│ │ ├── Debug.swift
│ │ ├── FfiCb.swift
│ │ ├── InfoDict.swift
│ │ ├── Json.swift
│ │ ├── NetworkExtensionIpc.swift
│ │ ├── NotificationIds.swift
│ │ ├── ObservableValue.swift
│ │ ├── Sleep.swift
│ │ ├── String.swift
│ │ ├── StringError.swift
│ │ ├── Swift.swift
│ │ ├── WatchableValue.swift
│ │ └── time.swift
│ ├── system-network-extension/
│ │ ├── Info.plist
│ │ └── entitlements.entitlements
│ ├── third-party/
│ │ └── CwlSysctl.swift
│ └── xcodescripts/
│ ├── cargo-build-static-lib.bash
│ ├── nix-build-web-bundle.bash
│ ├── nix-web-dev-server-start.bash
│ ├── nix-web-dev-server-stop.bash
│ ├── pre-action.bash
│ └── set-build-info.bash
├── bin/
│ ├── gradle-deps-update.sh
│ ├── log-sleeps.py
│ ├── log-summary.py
│ └── log-text.py
├── contrib/
│ ├── bin/
│ │ ├── build-obscuravpn-dmg.bash
│ │ ├── check-in-obscura-nix-shell.bash
│ │ ├── find-nix-files.bash
│ │ ├── find-shellcheck-files.bash
│ │ ├── linux-packages.bash
│ │ ├── linux-test.bash
│ │ ├── linux_run_service.sh
│ │ ├── ls-non-ignored-files.bash
│ │ ├── nixfmt-auto-files.bash
│ │ ├── package-arch.bash
│ │ ├── package-deb.bash
│ │ ├── package-rpm.bash
│ │ └── shellcheck-auto-files.bash
│ ├── licenses.mjs
│ └── shell/
│ ├── source-die.bash
│ ├── source-echoerr.bash
│ └── source-nix.sh
├── flake.nix
├── justfile
├── linux/
│ ├── arch_Dockerfile
│ ├── arch_PKGBUILD
│ ├── deb_Dockerfile
│ ├── deb_control
│ ├── deb_install
│ ├── deb_rules
│ ├── obscura-preset.conf
│ ├── obscura-sysusers.conf
│ ├── obscura.service
│ ├── rpm_Dockerfile
│ ├── rpm_obscura.spec
│ ├── rpm_rpmlintrc
│ └── vm/
│ ├── archlinux-desktop-cloud-init/
│ │ ├── meta-data
│ │ └── user-data
│ ├── debian-desktop.preseed.cfg
│ ├── fedora43-desktop.ks
│ └── ubuntu24.04-desktop-cloud-init/
│ ├── meta-data
│ └── user-data
├── obscura-ui/
│ ├── .gitattributes
│ ├── .gitignore
│ ├── .vscode/
│ │ └── settings.json
│ ├── README.md
│ ├── index.html
│ ├── justfile
│ ├── package.json
│ ├── postcss.config.js
│ ├── src/
│ │ ├── App.module.css
│ │ ├── App.tsx
│ │ ├── Providers.tsx
│ │ ├── bridge/
│ │ │ ├── SystemProvider.tsx
│ │ │ ├── android.ts
│ │ │ └── commands.ts
│ │ ├── common/
│ │ │ ├── KeyedSet.ts
│ │ │ ├── accountUtils.ts
│ │ │ ├── api.ts
│ │ │ ├── appContext.ts
│ │ │ ├── common.module.css
│ │ │ ├── debuggingArchiveHook.tsx
│ │ │ ├── exitUtils.ts
│ │ │ ├── fmt.ts
│ │ │ ├── links.ts
│ │ │ ├── localStorage.ts
│ │ │ ├── notifIds.ts
│ │ │ ├── useAsync.ts
│ │ │ ├── useExitList.ts
│ │ │ ├── useLoadable.ts
│ │ │ ├── useMailto.ts
│ │ │ ├── useSharedWatchable.ts
│ │ │ └── utils.ts
│ │ ├── components/
│ │ │ ├── AccountNumberSection.tsx
│ │ │ ├── AnimatedChevron.tsx
│ │ │ ├── BoltBadgeAuto.tsx
│ │ │ ├── ButtonLink.tsx
│ │ │ ├── CachedColorScheme.tsx
│ │ │ ├── ConfirmationDialog.module.css
│ │ │ ├── ConfirmationDialog.tsx
│ │ │ ├── DebuggingArchive.module.css
│ │ │ ├── DebuggingArchive.tsx
│ │ │ ├── DevSendCommand.tsx
│ │ │ ├── DevSetApiUrl.tsx
│ │ │ ├── ExternalLinkIcon.tsx
│ │ │ ├── Licenses.tsx
│ │ │ ├── Mantine.tsx
│ │ │ ├── ObscuraChip.tsx
│ │ │ ├── ObscuraWordmark.tsx
│ │ │ ├── PaymentManagementSheet.tsx
│ │ │ ├── ScrollableView.module.css
│ │ │ ├── ScrollableView.tsx
│ │ │ ├── SecondaryButton.tsx
│ │ │ ├── SelectCreatable.tsx
│ │ │ ├── Socials.tsx
│ │ │ └── VpnErrorFmt.tsx
│ │ ├── index.css
│ │ ├── main.tsx
│ │ ├── translations/
│ │ │ ├── en.json
│ │ │ ├── i18n.ts
│ │ │ └── i18next.d.ts
│ │ ├── views/
│ │ │ ├── About.module.css
│ │ │ ├── About.tsx
│ │ │ ├── AccountView.module.css
│ │ │ ├── AccountView.tsx
│ │ │ ├── ConnectionView.module.css
│ │ │ ├── ConnectionView.tsx
│ │ │ ├── DeveloperView.tsx
│ │ │ ├── HelpView.tsx
│ │ │ ├── Location.module.css
│ │ │ ├── LocationView.tsx
│ │ │ ├── LogInView.tsx
│ │ │ ├── LoginView.module.css
│ │ │ ├── Settings.module.css
│ │ │ ├── Settings.tsx
│ │ │ ├── SplashScreen.tsx
│ │ │ ├── index.ts
│ │ │ └── render-fallbacks/
│ │ │ └── FallbackAppRender.tsx
│ │ └── wdyr.ts
│ ├── tsconfig.json
│ ├── vite-env.d.ts
│ └── vite.config.ts
├── rustlib/
│ ├── .cargo/
│ │ └── config.toml
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── about.toml
│ ├── build.rs
│ ├── examples/
│ │ ├── connect.rs
│ │ └── list-relay-rtts.rs
│ ├── rust-toolchain.toml
│ ├── rustfmt.toml
│ └── src/
│ ├── android/
│ │ ├── class_cache.rs
│ │ ├── ffi.rs
│ │ ├── future.rs
│ │ ├── mod.rs
│ │ ├── os_impl.rs
│ │ ├── tunnel.rs
│ │ └── util.rs
│ ├── apple/
│ │ ├── ffi.rs
│ │ ├── mod.rs
│ │ └── os_impl.rs
│ ├── backoff.rs
│ ├── backoff_test.rs
│ ├── bin/
│ │ └── obscura/
│ │ ├── add_operator.rs
│ │ ├── client/
│ │ │ ├── client_error.rs
│ │ │ ├── ipc.rs
│ │ │ └── mod.rs
│ │ ├── main.rs
│ │ └── service/
│ │ ├── mod.rs
│ │ └── os/
│ │ ├── linux/
│ │ │ ├── dns/
│ │ │ │ ├── mod.rs
│ │ │ │ └── resolved.rs
│ │ │ ├── ipc.rs
│ │ │ ├── mod.rs
│ │ │ ├── network_manager.rs
│ │ │ ├── routes/
│ │ │ │ ├── mod.rs
│ │ │ │ └── netlink.rs
│ │ │ ├── service_lock.rs
│ │ │ ├── start_error.rs
│ │ │ └── tun.rs
│ │ ├── mod.rs
│ │ └── windows/
│ │ ├── adapters.rs
│ │ ├── gaa.rs
│ │ ├── iphelper.rs
│ │ ├── mod.rs
│ │ ├── nrpt.rs
│ │ ├── start_error.rs
│ │ └── tun.rs
│ ├── cached_value.rs
│ ├── client_state.rs
│ ├── config/
│ │ ├── cached.rs
│ │ ├── dns_cache.rs
│ │ ├── feature_flags.rs
│ │ ├── mod.rs
│ │ ├── persistence.rs
│ │ └── persistence_test.rs
│ ├── constants.rs
│ ├── debug_archive/
│ │ ├── builder.rs
│ │ ├── dns.rs
│ │ ├── http.rs
│ │ ├── info.rs
│ │ ├── mod.rs
│ │ ├── task.rs
│ │ └── zipper.rs
│ ├── dns.rs
│ ├── errors.rs
│ ├── exit_selection.rs
│ ├── ffi_helpers.rs
│ ├── lib.rs
│ ├── liveness.rs
│ ├── logging.rs
│ ├── manager.rs
│ ├── manager_cmd.rs
│ ├── net.rs
│ ├── network_config.rs
│ ├── os/
│ │ ├── mod.rs
│ │ ├── os_trait.rs
│ │ └── packet_buffer.rs
│ ├── positive_u31.rs
│ ├── quicwg.rs
│ ├── rate_limited_log.rs
│ ├── relay_selection.rs
│ ├── serde_safe.rs
│ ├── tokio.rs
│ └── tunnel_state.rs
├── tag.json
└── taplo.toml
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# https://editorconfig.org/#supported-properties
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[justfile]
indent_style = tab
indent_size = tab
[*.{bash,sh}]
indent_style = tab
indent_size = tab
[*.html]
indent_size = 2
[*.nix]
indent_size = 2
[*.plist]
indent_style = tab
indent_size = tab
[*.yml]
indent_size = 2
[*{.ts,tsx,js,jsx,json}]
indent_size = 2
# https://github.com/facebook/ktfmt/blob/main/docs/editorconfig/.editorconfig-kotlinlang
[*.{kt,kts}]
indent_style = space
insert_final_newline = true
max_line_length = 120
indent_size = 4
ij_continuation_indent_size = 4
ij_java_names_count_to_use_import_on_demand = 9999
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
ij_kotlin_align_multiline_method_parentheses = false
ij_kotlin_align_multiline_parameters = true
ij_kotlin_align_multiline_parameters_in_calls = false
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_assignment_wrap = normal
ij_kotlin_blank_lines_after_class_header = 0
ij_kotlin_blank_lines_around_block_when_branches = 0
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
ij_kotlin_block_comment_at_first_column = true
ij_kotlin_call_parameters_new_line_after_left_paren = true
ij_kotlin_call_parameters_right_paren_on_new_line = false
ij_kotlin_call_parameters_wrap = on_every_item
ij_kotlin_catch_on_new_line = false
ij_kotlin_class_annotation_wrap = split_into_lines
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_for_chained_calls = true
ij_kotlin_continuation_indent_for_expression_bodies = true
ij_kotlin_continuation_indent_in_argument_lists = true
ij_kotlin_continuation_indent_in_elvis = false
ij_kotlin_continuation_indent_in_if_conditions = false
ij_kotlin_continuation_indent_in_parameter_lists = false
ij_kotlin_continuation_indent_in_supertype_lists = false
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = normal
ij_kotlin_field_annotation_wrap = off
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
ij_kotlin_imports_layout = *
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
ij_kotlin_keep_blank_lines_in_declarations = 2
ij_kotlin_keep_first_column_comment = true
ij_kotlin_keep_indents_on_empty_lines = false
ij_kotlin_keep_line_breaks = true
ij_kotlin_lbrace_on_next_line = false
ij_kotlin_line_comment_add_space = false
ij_kotlin_line_comment_at_first_column = true
ij_kotlin_method_annotation_wrap = split_into_lines
ij_kotlin_method_call_chain_wrap = normal
ij_kotlin_method_parameters_new_line_after_left_paren = true
ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_method_parameters_wrap = on_every_item
ij_kotlin_name_count_to_use_star_import = 9999
ij_kotlin_name_count_to_use_star_import_for_members = 9999
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true
ij_kotlin_space_after_type_colon = true
ij_kotlin_space_before_catch_parentheses = true
ij_kotlin_space_before_comma = false
ij_kotlin_space_before_extend_colon = true
ij_kotlin_space_before_for_parentheses = true
ij_kotlin_space_before_if_parentheses = true
ij_kotlin_space_before_lambda_arrow = true
ij_kotlin_space_before_type_colon = false
ij_kotlin_space_before_when_parentheses = true
ij_kotlin_space_before_while_parentheses = true
ij_kotlin_spaces_around_additive_operators = true
ij_kotlin_spaces_around_assignment_operators = true
ij_kotlin_spaces_around_equality_operators = true
ij_kotlin_spaces_around_function_type_arrow = true
ij_kotlin_spaces_around_logical_operators = true
ij_kotlin_spaces_around_multiplicative_operators = true
ij_kotlin_spaces_around_range = false
ij_kotlin_spaces_around_relational_operators = true
ij_kotlin_spaces_around_unary_operator = false
ij_kotlin_spaces_around_when_arrow = true
ij_kotlin_variable_annotation_wrap = off
ij_kotlin_while_on_new_line = false
ij_kotlin_wrap_elvis_expressions = 1
ij_kotlin_wrap_expression_body_functions = 1
ij_kotlin_wrap_first_method_in_call_chain = false
ktfmt_trailing_comma_management_strategy = only_add
================================================
FILE: .envrc
================================================
use flake
================================================
FILE: .git-blame-ignore-revs
================================================
70ff75289a9cbce36f7b70a20b4f9c9f82e3b25e
================================================
FILE: .github/workflows/checks.yml
================================================
name: Checks
on:
workflow_dispatch:
pull_request:
branches:
- '**'
push:
branches:
- 'main'
concurrency:
# group is workflow specific and based on the branch (e.g. PRs) or tag
group: ${{ github.workflow }}-${{ github.ref }}
# we don't want to cancel checks on the main branch
cancel-in-progress: ${{ github.ref_name != 'main' }}
jobs:
flake_check_linux:
name: Flake Check Linux
runs-on: [nix]
steps:
- name: git checkout
uses: actions/checkout@v3
- name: Nix Checks
shell: bash
run: |
nix flake check \
--keep-going \
--no-update-lock-file \
--print-build-logs \
--show-trace
- name: Nix Build
shell: bash
run: |
nix build '.#apks-foss' \
--no-update-lock-file \
--print-build-logs \
--show-trace
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: obscura-debug.apk
path: result/app-foss-debug.apk
retention-days: 30
flake_check_macos:
name: Flake Check macOS
runs-on:
# https://namespace.so/docs/features/faster-github-actions#using-runner-labels
- nscloud-macos-sequoia-arm64-12x28-with-cache
- nscloud-cache-tag-obscuravpn-client
- nscloud-cache-size-50gb
steps:
- name: git checkout
uses: actions/checkout@v3
- uses: namespacelabs/nscloud-cache-action@v1
with:
# The action fails to mount at `/nix` and we want to let the Nix installer handle that anyways.
path: /tmp/nix
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
with:
determinate: false
mac-volume-label: obscuravpn-client
- name: Nix Checks
shell: bash
run: |
nix flake check \
--keep-going \
--no-update-lock-file \
--print-build-logs \
--show-trace
macos_build:
name: MacOS build
runs-on:
# https://namespace.so/docs/reference/github-actions/runner-configuration#runner-labels
- nscloud-macos-tahoe-arm64-12x28-with-cache
- nscloud-cache-tag-obscuravpn-client
- nscloud-cache-size-50gb
steps:
- name: git checkout
uses: actions/checkout@v3
- name: unshallow and fetch git tags
shell: bash
run: |
git fetch --prune --unshallow --tags
- uses: namespacelabs/nscloud-cache-action@v1
with:
# The action fails to mount at `/nix` and we want to let the Nix installer handle that anyways.
path: /tmp/nix
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
with:
determinate: false
mac-volume-label: obscuravpn-client
- name: Build macOS client
shell: bash
run: |
xcodebuild build \
-workspace apple/client.xcodeproj/project.xcworkspace \
-scheme 'Prod Client' \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED="NO" CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO"
ios_build:
name: iOS All
runs-on:
# https://namespace.so/docs/reference/github-actions/runner-configuration#runner-labels
- nscloud-macos-tahoe-arm64-12x28-with-cache
- nscloud-cache-tag-obscuravpn-client
- nscloud-cache-size-50gb
steps:
- name: git checkout
uses: actions/checkout@v3
- name: unshallow and fetch git tags
shell: bash
run: |
git fetch --prune --unshallow --tags
- uses: namespacelabs/nscloud-cache-action@v1
with:
# The action fails to mount at `/nix` and we want to let the Nix installer handle that anyways.
path: /tmp/nix
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
with:
determinate: false
mac-volume-label: obscuravpn-client
- name: Build iOS client
shell: bash
run: |
xcodebuild build \
-workspace apple/client.xcodeproj/project.xcworkspace \
-scheme 'Obscura VPN iOS' \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED="NO" CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO"
sync:
name: Sync
if: |
github.repository == 'sovereign-engineering/obscuravpn-client-internal'
&& github.ref == 'refs/heads/main'
needs:
- flake_check_linux
- flake_check_macos
- ios_build
- macos_build
runs-on: nscloud-ubuntu-20.04-amd64-2x2
environment: sync
steps:
- name: git checkout
uses: actions/checkout@v3
with:
fetch-depth: 0 # We need all history to check for internal commits.
- name: Check Restricted Commits
run: |
echo "Ensuring that we have full history."
git log --pretty=oneline 5a25968c93bda974d63f9f96e2be38d7277d0993
echo "Ensuring that internal history is not present in HEAD."
! git merge-base --is-ancestor 5a25968c93bda974d63f9f96e2be38d7277d0993 HEAD
- name: Push to Public
env:
SSH_PRIVATE_KEY: ${{ secrets.OBSCURA_CLIENT_SSH_KEY }}
run: |
echo "$SSH_PRIVATE_KEY" > "$RUNNER_TEMP/ssh-private-key"
chmod 600 "$RUNNER_TEMP/ssh-private-key"
export GIT_SSH_COMMAND="ssh -i $RUNNER_TEMP/ssh-private-key"
git push git@github.com:Sovereign-Engineering/obscuravpn-client.git HEAD:main
================================================
FILE: .gitignore
================================================
/.direnv/
/.idea/
/.devcontainer/
xcuserdata
.DS_STORE
/.claude/
# Nix
result
result-*
# DMG
*.dmg
# Android
*.apk
*.apk.idsig
*.aab
# Linux
/linux/vm/*.qcow2
/linux/vm/*.iso
*.rpm
*.pkg.tar.zst
*.deb
# Windows
windows/wintun*/*
================================================
FILE: .shellcheckrc
================================================
source-path=.
external-sources=true
================================================
FILE: .swiftformat
================================================
--self insert
--disable andOperator,unusedArguments,hoistPatternLet
--trailing-commas collections-only # Old versions of Swift can't handle failing commas in functions. Switch to always (default) when we don't use those anymore.
================================================
FILE: LICENSE.md
================================================
# PolyForm Noncommercial License 1.0.0
<https://polyformproject.org/licenses/noncommercial/1.0.0>
## Acceptance
In order to get any license under these terms, you must agree
to them as both strict obligations and conditions to all
your licenses.
## Copyright License
The licensor grants you a copyright license for the
software to do everything you might do with the software
that would otherwise infringe the licensor's copyright
in it for any permitted purpose. However, you may
only distribute the software according to [Distribution
License](#distribution-license) and make changes or new works
based on the software according to [Changes and New Works
License](#changes-and-new-works-license).
## Distribution License
The licensor grants you an additional copyright license
to distribute copies of the software. Your license
to distribute covers distributing the software with
changes and new works permitted by [Changes and New Works
License](#changes-and-new-works-license).
## Notices
You must ensure that anyone who gets a copy of any part of
the software from you also gets a copy of these terms or the
URL for them above, as well as copies of any plain-text lines
beginning with `Required Notice:` that the licensor provided
with the software. For example:
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
## Changes and New Works License
The licensor grants you an additional copyright license to
make changes and new works based on the software for any
permitted purpose.
## Patent License
The licensor grants you a patent license for the software that
covers patent claims the licensor can license, or becomes able
to license, that you would infringe by using the software.
## Noncommercial Purposes
Any noncommercial purpose is a permitted purpose.
## Personal Uses
Personal use for research, experiment, and testing for
the benefit of public knowledge, personal study, private
entertainment, hobby projects, amateur pursuits, or religious
observance, without any anticipated commercial application,
is use for a permitted purpose.
## Noncommercial Organizations
Use by any charitable organization, educational institution,
public research organization, public safety or health
organization, environmental protection organization,
or government institution is use for a permitted purpose
regardless of the source of funding or obligations resulting
from the funding.
## Fair Use
You may have "fair use" rights for the software under the
law. These terms do not limit them.
## No Other Rights
These terms do not allow you to sublicense or transfer any of
your licenses to anyone else, or prevent the licensor from
granting licenses to anyone else. These terms do not imply
any other licenses.
## Patent Defense
If you make any written claim that the software infringes or
contributes to infringement of any patent, your patent license
for the software granted under these terms ends immediately. If
your company makes such a claim, your patent license ends
immediately for work on behalf of your company.
## Violations
The first time you are notified in writing that you have
violated any of these terms, or done anything with the software
not covered by your licenses, your licenses can nonetheless
continue if you come into full compliance with these terms,
and take practical steps to correct past violations, within
32 days of receiving notice. Otherwise, all your licenses
end immediately.
## No Liability
***As far as the law allows, the software comes as is, without
any warranty or condition, and the licensor will not be liable
to you for any damages arising out of these terms or the use
or nature of the software, under any kind of legal claim.***
## Definitions
The **licensor** is the individual or entity offering these
terms, and the **software** is the software the licensor makes
available under these terms.
**You** refers to the individual or entity agreeing to these
terms.
**Your company** is any legal entity, sole proprietorship,
or other kind of organization that you work for, plus all
organizations that have control over, are under the control of,
or are under common control with that organization. **Control**
means ownership of substantially all the assets of an entity,
or the power to direct its management and policies by vote,
contract, or otherwise. Control can be direct or indirect.
**Your licenses** are all the licenses granted to you for the
software under these terms.
**Use** means anything you do with the software requiring one
of your licenses.
================================================
FILE: README.md
================================================
# Obscura VPN Client
Obscura VPN library, CLI client, and App
## Support
No support is provided for this code directly. However, if you are experiencing issues with your Obscura VPN service please contact <support@obscura.net>.
## Contributions
At this time we are unable to accept external contributions. This is something that we plan to resolve soon. However until we finish the paperwork we are unable to look at any patches and will close all PRs without looking at them.
## macOS App
On macOS the app installs and manages a [network extension](https://developer.apple.com/documentation/networkextension) (system extension).
The network extension manages the virtual device and maintains the tunnel using the Rust code as library.
### Setup
1. [Setup Nix](#nix-setup)
1. Install dependencies: `nix-env -iA nixpkgs.{cmake,rustup}`
1. Open the main Xcode project
```bash
nix develop --print-build-logs --command just xcode-open
```
1. In Xcode, login with an account with membership in "Sovereign Engineering Inc."
1. Register development machine in Apple Developer portal (can be done in Xcode)
1. [Enable system extension developer mode](#enabling-system-extension-developer-mode)
1. Setup Developer ID provisioning profile and codesigning for `Prod Client` build scheme
1. Go to https://developer.apple.com/account/resources/profiles/list
- Download "Developer ID: System Network Extension"
- Download "Developer ID: VPN Client App"
1. Install both provisioning profiles by double-clicking them.
1. Ask Carl to send the Developer ID codesigning certificate and the corresponding password
1. Double click the certificate, enter the password, and install it to your "login" keychain
## Building and Running
### For macOS and iOS
1. Open the main Xcode project:
```bash
nix develop --print-build-logs --command just xcode-open
```
1. Pick a build scheme using Xcode's GUI, one of:
ℹ️ **INFO**: Xcode differentiates between "build schemes" and "build configurations", see [Apple's docs on this](https://developer.apple.com/documentation/xcode/build-system) for more details.
1. `Dev Client`: Development Client
General purpose for development. Uses the main UI with additional developer and pre-release features exposed.
Uses the `Debug*` build configurations. Codesigned with the `Apple Development` xcode-managed identity.
⚠️ **WARNING**: When using this build scheme, make sure you are quitting the app via the top-right status menu bar and **NOT** using Xcode's "Stop" as doing so does not actually stop the dev server. This is because stopping via Xcode doesn't run the build scheme's "Run → Post-actions"
1. `Prod Client`: The App with a static web bundle
Useful for reproducing what the final shippable app will look like and be built as.
Uses the `Release*` build configurations. Codesigned with the `Developer ID Application: Sovereign Engineering Inc. (5G943LR562)` manually-managed identity.
The static web bundle built with the build scheme's "Build → Pre-actions".
If you encounter trouble with this build scheme, especially with codesigning or provisioning profiles:
1. Make sure that you've completed the relevant steps in [setup](#setup)
1. See additional instructions in [Confirming "Developer ID" Setup](#confirming-developer-id-setup)
1. `Bare Client`: The App with a minimal HTML UI
Useful for fine-grain control and debugging.
Uses the `Debug*` build configurations. Codesigned with the `Apple Development` xcode-managed identity.
1. Build or Run the App
- `⌘ + B` (Build), or
- `⌘ + R` (Run)
💡 **TIP**: It may initially _seem_ like Xcode is doing nothing when you run or build, but it may just be running the build scheme's "Pre-actions", see the "Report navigator" in Xcode's top-left app menu: "View → Navigators → Reports" to track the actual status.
💡 **TIP**: If a build fails with `could not find included file 'buildversion.xcconfig' in search paths`, see the [relevant troubleshooting entry](#error-on-build-in-clean-repo-could-not-find-included-file-buildversionxcconfig-in-search-paths).
-----
Xcode places built products in a deeply nested directory structure that it controls, with seperate folders for each build configuration. The easiest way to locate where the app is:
1. "Run" the app
1. Once the app's icon appears on the macOS Dock, `⌘-Click` the app icon to reveal it in the finder.
💡 **TIP**: It is highly recommended to read through various sections in [Development Tips](#development-tips) to better understand the various ways we've configured the Xcode build system to work with our development process.
### For Android
#### Nix Builds
Nix builds provide an easy way to get a fully built APK. They are hermetic and reliable. However, they provide only coarse grained caching so if you are iterating during development you may prefer to use [Incremental Builds](#incremental-builds).
```sh
nix build '.#apks-foss'
apksigner sign --ks your-keystore.jks --ks-pass pass:hunter2 --out=obscura-signed.apk result/app-foss-release-unsigned.apk # Sign.
adb install obscura-signed.apk # Push to your device.
```
Instead of `app-foss-release-unsigned` you can also use `app-foss-debug` for the debug build. Note that just the Android portion is a debug build, the Rust core and UI are still release builds.
#### Incremental Builds
The Android app requires a special build of the Rust library and Obscura UI. These are built using Nix, while the Android app itself can be built using [Android Studio](https://developer.android.com/studio) for local development, or the Gradle build system to create an official build.
1. Build the Obscura UI
```bash
OBS_WEB_PLATFORM="android" nix develop '.#web' --print-build-logs -c just web-bundle-build
```
2. Build the Rust library
```bash
nix develop '.#android' --command bash -c 'cd rustlib && cargo ndk -t arm64-v8a build --release'
```
3. Open Android Studio and point it at the `android` directory, or
4. Use Gradle to build everything
```bash
nix develop '.#android' --command bash -c 'cd android && gradle --no-daemon $GRADLE_OPTS build'
```
In order to iterate you can just repeat the steps. 1 and 2 are only required if you changed the UI or Rust core respectively but the final APK build must always be re-run.
#### Gradle Dependencies
To ensure hermetic builds we pin our Gradle dependencies. If you change the dependencies you will need to regenerate the pin file.
```
bin/gradle-deps-update.sh
```
### For Windows
Install [Visual Studio](https://visualstudio.microsoft.com/downloads/) with the following Workloads:
- Desktop development with C++
- WinUI application development
On Windows, definitely ARM64 machines, you need to add `C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\Llvm\ARM64\bin` to path.
Download the signed [wintun 0.14.1 DLLs](https://www.wintun.net/).
You can use `Get-FileHash -Path .\wintun-0.14.1.zip -Algorithm SHA256` to verify the hash against `SHA2-256: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51`.
Extract to `windows/wintun-0.14.1` such that `windows/wintun-0.14.1/bin/arm64/wintun.dll` is a file.
To test the service, run `sudo cargo run service`. You need to enable `sudo` under System > Advanced settings. Alternatively, you can run `cargo run service` in an administrative terminal.
The default config directory is `%APPDATA%\Obscura`. When testing the service, you may find it beneficial to manually add in an account number.
A helpful command to clean DNS query manually is `Remove-DnsClientNrptRule -Name "{fb157da8-6578-4f53-81ea-0a9168e96c1f}"`.
## Swift unit tests
"Swift Testing" tests are placed in `*Test.swift` files, which need to be a member of the `Tests` target. Testing (not running) with the `Tests` scheme builds and executes all tests.
## Debugging
### Logs
Both app and network extension logs are available via [Apple's unified logging system](https://developer.apple.com/documentation/os/logging).
#### Analyzing Logs
There are tools for analyzing logs available as `bin/log-*`. They accept log files in JSON lines format. This can be found in the app's Debug Bundle or from the Apple `log` command by specifying `--style=ndjson`.
The main tool is `bin/log-text.py` which just turns the logs into a readable text format as well as applying some basic filtering with a few CLI options to apply more filters. Other tools are available, run with `--help` to get information about what they do.
For more in-depth analysis you are likely best using the tools as a starting point and modifying them as needed or using other tools like `jq`, `sqlite` or `duckdb`. If your analysis is generally useful consider committing it.
#### Stream Logs
This will output logs starting at the point in time when you run this command:
```bash
log stream --info --debug --predicate 'process CONTAINS[c] "obscura" || subsystem CONTAINS[c] "obscura"'
```
#### View Past Logs
> [!WARNING]
> Since Apple may or may not persist logs at the `INFO` or `DEBUG` level, logs at these level might be lost. See [Apple's developer docs on this](https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code#3665947) for more information.
>
> You may be able to set a log configuration to ensure that these logs are persisted, though this has not been tested, please update this `README` with instructions if you successfully test this. See [Apple's docs on "Customizing Logging Behavior While Debugging"](https://developer.apple.com/documentation/os/logging/customizing_logging_behavior_while_debugging) for more information.
```bash
log show --last 200 --info --debug --color always --predicate 'process CONTAINS[c] "obscura" || subsystem CONTAINS[c] "obscura"' | less +G -R
```
#### UserDefaults
```sh
defaults read "net.obscura.vpn-client-app"
# delete all defaults including Sparkle related keys (SU*)
defaults delete-all "net.obscura.vpn-client-app"
# delete keys individually
defaults delete "net.obscura.vpn-client-app" <key>
```
## Running Checks
### Linting
```bash
nix develop --print-build-logs --command just lint
```
### Formatting
#### Checking
```bash
nix flake check
```
#### Auto-fixing
```bash
nix develop --print-build-logs --command just format-fix
```
## Building a Notarized Disk Image
1. Save authentication credentials for the Apple notary service (only need to do once)
```bash
xcrun notarytool store-credentials "notarytool-password" --team-id 5G943LR562
```
Use [appleid.apple.com](https://appleid.apple.com/account/manage) --> App-Specific Passwords
1. (OPTIONAL) If we're doing a release, tag the version `git tag -s v/1.23 -m v/1.23 && git push --tags`.
1. Unlock the "Login" keychain: `security unlock-keychain`
1. Build the signed and notarized disk image: `just build-dmg`
💡 **TIP**: This command uses AppleScript automation of Finder to change the background of Disk Images, so Finder windows may open.
The built disk image will appear in the current working directory as "Obscura VPN.dmg"
## Troubleshooting
### `cargo` not rebuilding when it should
A lot of Xcode-set properties don't properly trigger a rebuild from `cargo` even
though they're supposed to. The most prominent of which is `MACOSX_DEPLOYMENT_TARGET`.
This is easily worked-around by "Product → Clean Build Folder..." in Xcode then rerunning the build.
Upstream status on this:
- https://github.com/rust-lang/cc-rs/issues/906
- https://github.com/rust-lang/rust/issues/118204
## Development Tips
### Enabling system extension developer mode
This is necessary for:
- The `systemextensionsctl` commands to work, and
- To allow installing and running system extensions from places other than `/Applications`
According to [Apple's docs for system extensions](https://developer.apple.com/documentation/driverkit/debugging_and_testing_system_extensions#3557204), as of 2024-07-04:
> You must place all system extensions in the `Contents/Library/SystemExtensions` directory of your app bundle, and the app itself must be installed in one of the system’s `Applications` directories. To allow development of your app outside of these directories, use the `systemextensionsctl` command-line tool to enable developer mode. When in developer mode, the system doesn't check the location of your system extension prior to loading it, so you can load it from anywhere in the file system.
To accomplish this:
1. [Disable system integrity protection](https://developer.apple.com/documentation/security/disabling_and_enabling_system_integrity_protection)
1. Then, run
```bash
systemextensionsctl developer on
```
### Removing network extension (system extension)
1. Ensure that [system extension developer mode is enabled](#enabling-system-extension-developer-mode)
1. Then, run
```bash
systemextensionsctl uninstall 5G943LR562 net.obscura.vpn-client-app.system-network-extension
```
### Nix Setup
- Install [`nix`](https://nixos.org/download/) (only the package manager is needed)
- Enable [`flake`s](https://nixos.wiki/wiki/Flakes)
Add the following to `~/.config/nix/nix.conf` or `/etc/nix/nix.conf`:
```
experimental-features = nix-command flakes
```
- Optional, but strongly recommended: Set up [`nix-direnv`](https://github.com/nix-community/nix-direnv) and integrate it with your preferred shell
If you do this, you can omit the `nix develop ... --command` parts, as `cd`-ing into the repository directory will set up your environment variables with the correct tools as long as you've `direnv allow`-ed the directory.
### Confirming "Developer ID" Setup
To confirm that the Developer ID provisioning profile and codesigning are set up correctly (required for the `Prod Client` build scheme):
1. Pick the `Prod Client` build scheme in Xcode
1. Create an Archive
Choose from Xcode's top-left app menu: "Product → Archive"
1. Ensure that the "Archive" action succeeds in the "Report navigator"
Choose from Xcode's top-left app menu: "View → Navigators → Reports"
## Linux
> [!WARNING]
> As of 2024-07-04, the Linux client is not maintained.
```bash
cargo build --release && sudo RUST_LOG=info ./target/release/obscuravpn-client
```
================================================
FILE: android/.gitignore
================================================
*.iml
.gradle
.kotlin
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/appInsightsSettings.xml
.DS_Store
/build
/lib/*/build
/captures
.externalNativeBuild
.cxx
local.properties
================================================
FILE: android/.idea/.gitignore
================================================
# Default ignored files
/shelf/
/workspace.xml
/deploymentTargetSelector.xml
================================================
FILE: android/.idea/.name
================================================
ObscuraVPN
================================================
FILE: android/.idea/AndroidProjectSystem.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
================================================
FILE: android/.idea/codeStyles/Project.xml
================================================
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
================================================
FILE: android/.idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
================================================
FILE: android/.idea/compiler.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>
================================================
FILE: android/.idea/detekt.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DetektPluginSettings">
<option name="configurationFiles">
<list>
<option value="$PROJECT_DIR$/detekt.yml" />
</list>
</option>
<option name="enableDetekt" value="true" />
</component>
</project>
================================================
FILE: android/.idea/deviceManager.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>
================================================
FILE: android/.idea/dictionaries/project.xml
================================================
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>obscura</w>
<w>obscuravpn</w>
<w>vpnclientapp</w>
<w>vpnservice</w>
</words>
</dictionary>
</component>
================================================
FILE: android/.idea/gradle.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<compositeConfiguration>
<compositeBuild compositeDefinitionSource="SCRIPT">
<builds>
<build path="$PROJECT_DIR$/buildSrc" name="buildSrc">
<projects>
<project path="$PROJECT_DIR$/buildSrc" />
</projects>
</build>
</builds>
</compositeBuild>
</compositeConfiguration>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/buildSrc" />
<option value="$PROJECT_DIR$/lib" />
<option value="$PROJECT_DIR$/lib/billing" />
<option value="$PROJECT_DIR$/lib/util" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
================================================
FILE: android/.idea/kotlinc.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.3.10" />
</component>
</project>
================================================
FILE: android/.idea/ktfmt.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KtfmtSettings">
<option name="customBlockIndent" value="4" />
<option name="customMaxLineLength" value="120" />
<option name="customTrailingCommaManagementStrategy" value="Only add" />
<option name="enableKtfmt" value="Enabled" />
<option name="uiFormatterStyle" value="Custom" />
</component>
</project>
================================================
FILE: android/.idea/migrations.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
================================================
FILE: android/.idea/misc.xml
================================================
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
================================================
FILE: android/.idea/runConfigurations.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
================================================
FILE: android/.idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
================================================
FILE: android/app/.gitignore
================================================
/build
================================================
FILE: android/app/build.gradle.kts
================================================
import com.android.build.api.dsl.ApplicationExtension
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.hilt.android)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.ksp)
}
extensions.configure<ApplicationExtension> {
buildToolsVersion = "36.0.0"
namespace = "net.obscura.vpnclientapp"
compileSdk = 36
defaultConfig {
applicationId = "net.obscura.vpnclientapp"
minSdk = 31
targetSdk = 36
versionCode = 1
versionName = project.getVersionName(project.rootDir)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
aidl = true
buildConfig = true
}
buildTypes {
getByName("debug") {
applicationIdSuffix = ".debug"
isMinifyEnabled = false
isShrinkResources = false
resValue("string", "app_name", "Obscura VPN (Debug)")
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
flavorDimensions += listOf("billing")
productFlavors {
create("foss") {
dimension = "billing"
isDefault = true
}
create("play") { dimension = "billing" }
}
}
kotlin {
compilerOptions { freeCompilerArgs.add("-opt-in=kotlinx.serialization.ExperimentalSerializationApi") }
jvmToolchain(21)
}
dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle)
implementation(libs.androidx.webkit)
implementation(libs.hilt.android)
implementation(libs.kotlinx.serialization.json)
implementation(libs.material)
implementation(project(":lib:util"))
"playImplementation"(project(":lib:billing"))
ksp(libs.hilt.android.compiler)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
================================================
FILE: android/app/google-services.json
================================================
{
"project_info": {
"project_number": "980686794718",
"project_id": "obscura-vpn",
"storage_bucket": "obscura-vpn.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:980686794718:android:db99381cf88e8d6712774f",
"android_client_info": {
"package_name": "net.obscura.vpnclientapp"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDQpmOigUAURQCeVDA489R8ds78uvAdhCg"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
================================================
FILE: android/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: android/app/src/foss/java/net/obscura/vpnclientapp/BillingFacade.kt
================================================
package net.obscura.vpnclientapp
import android.content.Context
import net.obscura.vpnclientapp.activities.MainActivity
import net.obscura.vpnclientapp.client.errorCodeUnsupportedOnOS
class BillingFacade(@Suppress("UNUSED_PARAMETER") context: Context) {
@Suppress("RedundantSuspendModifier")
suspend fun launchFlow(@Suppress("UNUSED_PARAMETER") mainActivity: MainActivity): Boolean =
throw errorCodeUnsupportedOnOS()
fun destroy() = Unit
}
================================================
FILE: android/app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE"
tools:ignore="ForegroundServicesPolicy" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@drawable/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.ObscuraVPN">
<!--
`WebView` necessitates aggressive use of `android:configChanges`:
https://developer.android.com/develop/ui/compose/quick-guides/content/manage-webview-state
-->
<activity
android:name=".activities.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:exported="true"
android:enabled="true"
android:launchMode="singleInstance"
android:windowSoftInputMode="adjustNothing">
<intent-filter>
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.MAIN" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.VIEW" />
<data android:scheme="obscuravpn" />
</intent-filter>
</activity>
<provider
android:authorities="${applicationId}.debug_archive_file_provider"
android:name=".sharing.DebugArchiveFileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/debug_archive_file_provider_paths" />
</provider>
<service
android:name=".services.ObscuraVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="false"
android:foregroundServiceType="systemExempted"
android:process=":vpnservice"
tools:ignore="VpnServicePolicy">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<receiver
android:name=".ui.JsonFfiBroadcastReceiver"
android:exported="false" />
<meta-data
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
</application>
</manifest>
================================================
FILE: android/app/src/main/aidl/net/obscura/vpnclientapp/services/IObscuraVpnService.aidl
================================================
// IObscuraVpnService.aidl
package net.obscura.vpnclientapp.services;
interface IObscuraVpnService {
void startTunnel(String exitSelector);
void stopTunnel();
// Submits the command to the ObscuraLibrary.jsonFfi(String, CompletableFuture<String>)
// function, returning back a unique ID. To receive the result of the command, listen on the
// CommandBridge.Receiver (BroadcastReceiver) for an Intent with the "id" extra, "result" extra
// (indicating success) or "exception" extra (indicating an ErrorCodeException).
void jsonFfi(long id, String command);
}
================================================
FILE: android/app/src/main/assets/adi-registration.properties
================================================
CPRHUVTQKXB6SAAAAAAAAAAAAA
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/App.kt
================================================
package net.obscura.vpnclientapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import net.obscura.lib.util.Logger
private val log = Logger(App::class)
@HiltAndroidApp
class App : Application() {
override fun onCreate() {
super.onCreate()
log.info("app version: ${BuildConfig.VERSION_NAME}")
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/activities/MainActivity.kt
================================================
package net.obscura.vpnclientapp.activities
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Bundle
import android.os.IBinder
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.WindowCompat
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import net.obscura.lib.util.Logger
import net.obscura.vpnclientapp.BillingFacade
import net.obscura.vpnclientapp.R
import net.obscura.vpnclientapp.helpers.requireUIProcess
import net.obscura.vpnclientapp.preferences.Preferences
import net.obscura.vpnclientapp.services.IObscuraVpnService
import net.obscura.vpnclientapp.services.bindVpnService
import net.obscura.vpnclientapp.services.unbindVpnService
import net.obscura.vpnclientapp.ui.ObscuraUI
import net.obscura.vpnclientapp.ui.OsStatusManager
import net.obscura.vpnclientapp.ui.VpnPermissionRequestManager
private val log = Logger(MainActivity::class)
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), ServiceConnection, SharedPreferences.OnSharedPreferenceChangeListener {
@Inject lateinit var billingFacade: BillingFacade
@Inject lateinit var osStatusManager: OsStatusManager
@Inject lateinit var vpnPermissionRequestManager: VpnPermissionRequestManager
private lateinit var preferences: Preferences
private lateinit var ui: ObscuraUI
private var isFreshLaunch: Boolean = true
private var isVpnServiceBound: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireUIProcess()
this.isFreshLaunch = savedInstanceState == null
// Edge-to-edge is the future for Android
// https://developer.android.com/develop/ui/views/layout/edge-to-edge
WindowCompat.enableEdgeToEdge(this.window)
setContentView(R.layout.activity_main)
ui = findViewById(R.id.ui)
onBackPressedDispatcher.addCallback {
if (ui.canGoBack) {
ui.goBack()
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
isEnabled = true
}
}
preferences = Preferences(this).apply { registerListener(this@MainActivity) }
applyColorScheme()
this.isVpnServiceBound = this.bindVpnService(this)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.data?.let { uri -> this.ui.handleObscuraUri(uri) }
}
override fun onResume() {
super.onResume()
ui.onResume()
}
override fun onPause() {
super.onPause()
ui.onPause()
}
override fun onDestroy() {
super.onDestroy()
this.preferences.unregisterListener(this)
if (this.isVpnServiceBound) {
this.unbindVpnService(this)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
log.debug("configuration changed: $newConfig")
this.ui.invalidate()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
log.debug("onServiceConnected $name $service")
this.ui.onCreate(
this.isFreshLaunch,
IObscuraVpnService.Stub.asInterface(service),
this,
this.osStatusManager,
)
this.isFreshLaunch = false
}
override fun onServiceDisconnected(name: ComponentName?) {
log.debug("onServiceDisconnected $name")
this.ui.onDestroy()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == "color-scheme") {
applyColorScheme()
}
}
private fun applyColorScheme() {
AppCompatDelegate.setDefaultNightMode(
when (this.preferences.colorScheme) {
Preferences.ColorScheme.Auto -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
Preferences.ColorScheme.Dark -> AppCompatDelegate.MODE_NIGHT_YES
Preferences.ColorScheme.Light -> AppCompatDelegate.MODE_NIGHT_NO
}
)
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/client/ErrorCodeException.kt
================================================
package net.obscura.vpnclientapp.client
import androidx.annotation.Keep
// This `Keep` annotation is applied defensively to ensure that this class won't be stripped even if
// it's only constructed on the Rust side.
@Keep data class ErrorCodeException(val errorCode: String) : Exception(errorCode)
fun errorCodeOther() = ErrorCodeException("other")
fun errorCodePurchaseFailed() = ErrorCodeException("purchaseFailed")
fun errorCodePurchaseFailedAlreadyOwned() = ErrorCodeException("purchaseFailedAlreadyOwned")
fun errorCodeLegacyAlwaysOn() = ErrorCodeException("errorLegacyAlwaysOn")
fun errorCodeOtherAppAlwaysOn() = ErrorCodeException("errorOtherAppAlwaysOn")
fun errorCodePermissionNotGranted() = ErrorCodeException("errorPermissionNotGranted")
fun errorCodeUnsupportedOnOS() = ErrorCodeException("errorUnsupportedOnOS")
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/client/JsonConfig.kt
================================================
package net.obscura.vpnclientapp.client
import kotlinx.serialization.json.ClassDiscriminatorMode
import kotlinx.serialization.json.Json
val jsonConfig = Json {
this.classDiscriminatorMode = ClassDiscriminatorMode.NONE
this.encodeDefaults = true
this.ignoreUnknownKeys = true
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/client/ManagerCmd.kt
================================================
package net.obscura.vpnclientapp.client
import kotlinx.serialization.KeepGeneratedSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
import net.obscura.lib.util.ExternallyTaggedEnumVariantSerializer
sealed interface ManagerCmd {
@KeepGeneratedSerializer
@Serializable(with = CreateDebugArchive.Serializer::class)
data class CreateDebugArchive(
val userFeedback: String?,
) : ManagerCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<CreateDebugArchive>("createDebugArchive", generatedSerializer())
}
@KeepGeneratedSerializer
@Serializable(with = GetStatus.Serializer::class)
data class GetStatus(val knownVersion: String?) : ManagerCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<GetStatus>("getStatus", generatedSerializer())
}
@KeepGeneratedSerializer
@Serializable(with = SetTunnelArgs.Serializer::class)
data class SetTunnelArgs(
val args: Map<String, JsonObject>? = null,
val active: Boolean? = null,
) : ManagerCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<SetTunnelArgs>("setTunnelArgs", generatedSerializer())
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/client/ManagerCmdOk.kt
================================================
package net.obscura.vpnclientapp.client
import kotlinx.serialization.KeepGeneratedSerializer
import kotlinx.serialization.Serializable
import net.obscura.lib.util.ExternallyTaggedEnumSerializer
import net.obscura.lib.util.ExternallyTaggedEnumVariantSerializer
sealed interface ManagerCmdOk {
@Serializable
data class GetStatus(
val accountId: String?,
val autoConnect: Boolean,
val inNewAccountFlow: Boolean,
val version: String,
val vpnStatus: VpnStatus,
) : ManagerCmdOk {
@Serializable(with = VpnStatus.Serializer::class)
sealed interface VpnStatus {
object Serializer :
ExternallyTaggedEnumSerializer<VpnStatus>(
VpnStatus::class,
listOf(
Connected.Serializer,
Connecting.Serializer,
Disconnected.Serializer,
),
)
@KeepGeneratedSerializer
@Serializable(with = Connected.Serializer::class)
class Connected : VpnStatus {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<Connected>("connected", generatedSerializer())
}
@KeepGeneratedSerializer
@Serializable(with = Connecting.Serializer::class)
class Connecting : VpnStatus {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<Connecting>("connecting", generatedSerializer())
}
@KeepGeneratedSerializer
@Serializable(with = Disconnected.Serializer::class)
class Disconnected : VpnStatus {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<Disconnected>("disconnected", generatedSerializer())
}
}
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/client/ObscuraLibrary.java
================================================
package net.obscura.vpnclientapp.client;
import android.app.Application;
import android.content.Context;
import androidx.annotation.Keep;
import java.util.concurrent.CompletableFuture;
public class ObscuraLibrary {
/** Opaque handle returned by Rust initialization, required by all subsequent native FFI calls. */
@Keep
public static class FfiHandle {
private FfiHandle() {}
}
static FfiHandle load(Context context, String userAgent) {
if (!Application.getProcessName().endsWith(":vpnservice")) {
throw new IllegalStateException("Using this class outside of the :vpnservice process is not allowed.");
}
System.loadLibrary("obscuravpn_client");
return ObscuraLibrary.initialize(context.getFilesDir().getAbsolutePath(), userAgent);
}
static native FfiHandle initialize(String configDir, String userAgent);
static native void jsonFfi(FfiHandle handle, String json, CompletableFuture<String> future);
static native void setNetworkInterface(FfiHandle handle, String name, int index);
static native void unsetNetworkInterface(FfiHandle handle);
static native void forwardLog(int level, String tag, String message, String messageId, String throwableString);
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/client/RustFfi.kt
================================================
package net.obscura.vpnclientapp.client
import android.content.Context
import java.util.concurrent.CompletableFuture
import net.obscura.lib.util.Logger
class RustFfi(context: Context, userAgent: String) {
private val handle: ObscuraLibrary.FfiHandle =
ObscuraLibrary.load(
context,
userAgent,
)
fun logger(tag: String) =
Logger(tag) { params ->
ObscuraLibrary.forwardLog(
params.level.ordinal,
params.tag,
params.message,
params.messageId ?: "JavaNoID",
params.tr?.toString(),
)
}
fun jsonFfi(json: String, future: CompletableFuture<String>) {
ObscuraLibrary.jsonFfi(handle, json, future)
}
fun setNetworkInterface(name: String, index: Int) {
ObscuraLibrary.setNetworkInterface(handle, name, index)
}
fun unsetNetworkInterface() {
ObscuraLibrary.unsetNetworkInterface(handle)
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/helpers/Process.kt
================================================
package net.obscura.vpnclientapp.helpers
import android.app.Application
/** Ensures the calling process is :vpnservice. */
fun requireVpnServiceProcess() {
val currentProcess = Application.getProcessName()
if (!currentProcess.endsWith(":vpnservice")) {
throw RuntimeException("Called outside of the :vpnservice process ($currentProcess)")
}
}
/** Ensures the calling process is the main application process. */
fun requireUIProcess() {
val currentProcess = Application.getProcessName()
if (currentProcess.contains(":")) {
throw RuntimeException("Called outside of the application process ($currentProcess)")
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/preferences/Preferences.kt
================================================
package net.obscura.vpnclientapp.preferences
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.obscura.vpnclientapp.client.jsonConfig
import net.obscura.vpnclientapp.helpers.requireUIProcess
class Preferences(context: Context) {
init {
requireUIProcess()
}
@Serializable
enum class ColorScheme {
@SerialName("dark") Dark,
@SerialName("light") Light,
@SerialName("auto") Auto,
}
private val sharedPreferences = context.getSharedPreferences("preferences", Context.MODE_PRIVATE)
var colorScheme: ColorScheme
get() = jsonConfig.decodeFromString<ColorScheme>(this.sharedPreferences.getString("color-scheme", "\"auto\"")!!)
set(value) {
this.sharedPreferences.edit(commit = true) { putString("color-scheme", jsonConfig.encodeToString(value)) }
}
fun registerListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
this.sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
}
fun unregisterListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
this.sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/services/ContextExtension.kt
================================================
package net.obscura.vpnclientapp.services
import android.content.Context
import android.content.Context.BIND_AUTO_CREATE
import android.content.Intent
import android.content.ServiceConnection
import android.net.VpnService.prepare
import net.obscura.lib.util.Logger
import net.obscura.vpnclientapp.client.errorCodeOther
private val log = Logger("ContextExtension")
fun Context.bindVpnService(serviceConnection: ServiceConnection): Boolean {
log.info("binding VPN service")
val intent = Intent(this, ObscuraVpnService::class.java)
return try {
val isBinding = this.bindService(intent, serviceConnection, BIND_AUTO_CREATE)
if (!isBinding) {
log.error("missing permissions or service not found")
}
isBinding
} catch (e: SecurityException) {
log.error("missing permissions or service not found", tr = e)
this.unbindVpnService(serviceConnection)
false
}
}
fun Context.unbindVpnService(serviceConnection: ServiceConnection) {
log.info("unbinding VPN service")
try {
this.unbindService(serviceConnection)
} catch (e: IllegalArgumentException) {
log.error("VPN service connection not registered", tr = e)
}
}
sealed interface PrepareResult {
data class CreateProfile(val intent: Intent) : PrepareResult
data object Ready : PrepareResult
data object LegacyAlwaysOn : PrepareResult
}
fun Context.prepareVpnService(): PrepareResult =
try {
log.info("preparing VPN service")
prepare(this)?.let { PrepareResult.CreateProfile(it) } ?: PrepareResult.Ready
} catch (e: IllegalStateException) {
// This is undocumented, but `prepare` throws when a Legacy VPN is set to Always-On.
// Legacy VPN profiles are created manually using the "+" button on "Network & Internet" -> "VPN".
// https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/services/core/java/com/android/server/VpnManagerService.java;l=226;drc=0b5a5f8c78ce8e8800b527216b70db35489b7c41
// https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/services/core/java/com/android/server/VpnManagerService.java;l=545-557;drc=0b5a5f8c78ce8e8800b527216b70db35489b7c41
log.error("a Legacy VPN profile is set to Always-On", tr = e)
PrepareResult.LegacyAlwaysOn
}
fun Context.startVpnService(): Result<Unit> =
try {
log.info("starting VPN service")
this.startForegroundService(Intent(this, ObscuraVpnService::class.java))
Result.success(Unit)
} catch (e: SecurityException) {
log.error("missing permissions or service not found", tr = e)
Result.failure(errorCodeOther())
} catch (e: IllegalStateException) {
log.error("app not foregrounded", tr = e)
Result.failure(errorCodeOther())
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/services/IntentExtension.kt
================================================
package net.obscura.vpnclientapp.services
import android.content.Intent
import net.obscura.lib.util.Logger
import net.obscura.vpnclientapp.client.ErrorCodeException
import net.obscura.vpnclientapp.client.errorCodeOther
private val log = Logger("IntentExtension")
private const val EXTRA_ID = "id"
private const val EXTRA_VALUE = "value"
private const val EXTRA_ERROR_CODE = "errorCode"
fun Intent.putJsonFfiExtras(id: Long, value: String?, exception: Throwable?) {
this.putExtra(EXTRA_ID, id)
this.putExtra(EXTRA_VALUE, value)
this.putExtra(
EXTRA_ERROR_CODE,
when (exception) {
is ErrorCodeException -> exception.errorCode
is Throwable -> {
log.error("job $id threw unexpected exception type: $exception", tr = exception)
null
}
else -> {
if (value == null) {
log.error("job $id completed with no response")
}
null
}
},
)
}
data class JsonFfiIntentPayload(val id: Long, val result: Result<String>)
fun Intent.getJsonFfiExtras(): JsonFfiIntentPayload {
val id = this.getLongExtra(EXTRA_ID, -1)
val value = this.getStringExtra(EXTRA_VALUE)
val errorCode = this.getStringExtra(EXTRA_ERROR_CODE)
return JsonFfiIntentPayload(
id,
if (value != null) {
log.trace("job $id completed with value: $value")
Result.success(value)
} else {
log.trace("job $id completed with error code: $errorCode")
Result.failure(errorCode?.let { ErrorCodeException(it) } ?: errorCodeOther())
},
)
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/services/ObscuraVpnService.kt
================================================
package net.obscura.vpnclientapp.services
import android.Manifest
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.VpnService
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.ParcelFileDescriptor
import android.system.OsConstants
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import java.net.NetworkInterface
import java.util.concurrent.CompletableFuture
import net.obscura.lib.util.Logger
import net.obscura.vpnclientapp.BuildConfig
import net.obscura.vpnclientapp.R
import net.obscura.vpnclientapp.activities.MainActivity
import net.obscura.vpnclientapp.client.ManagerCmd
import net.obscura.vpnclientapp.client.ManagerCmdOk
import net.obscura.vpnclientapp.client.RustFfi
import net.obscura.vpnclientapp.client.jsonConfig
import net.obscura.vpnclientapp.helpers.requireVpnServiceProcess
import net.obscura.vpnclientapp.ui.JsonFfiBroadcastReceiver
private val logNoFfi = Logger(ObscuraVpnService::class)
@SuppressLint("VpnServicePolicy")
class ObscuraVpnService : VpnService() {
private class Binder(
val service: ObscuraVpnService,
) : IObscuraVpnService.Stub() {
override fun startTunnel(exitSelector: String?) {
service.log.info("startTunnel $exitSelector", "CddrThRg")
service.startTunnel(exitSelector)
}
override fun stopTunnel() {
service.log.info("stopTunnel", "Gf6f2lwW")
service.stopTunnel()
}
override fun jsonFfi(
id: Long,
command: String,
) {
val future = CompletableFuture<String>()
service.rustFfi.jsonFfi(command, future)
future.handle { value: String?, exception: Throwable? ->
try {
service.sendBroadcast(
Intent(service, JsonFfiBroadcastReceiver::class.java).apply {
this.putJsonFfiExtras(id, value, exception)
}
)
} catch (e: Throwable) {
service.log.error("failed to broadcast job $id result: $e", messageId = "L74T4QBq", tr = e)
}
}
}
}
companion object {
private const val NOTIFICATION_CHANNEL_ID = "vpn_channel"
private const val NOTIFICATION_ID = 1
private val instance = java.util.concurrent.atomic.AtomicReference<ObscuraVpnService?>(null)
@androidx.annotation.Keep
@JvmStatic
fun ffiSetNetworkConfig(json: String): Int {
val service = instance.get()
if (service == null) {
logNoFfi.error("ffiSetNetworkConfig called with no active service", "wK3xLm9p")
return -1
}
val config: OsNetworkConfig =
try {
jsonConfig.decodeFromString(json)
} catch (e: Exception) {
service.log.error("failed to parse os network config: $e", "yN4zPn0q", e)
return -1
}
val pfd =
try {
service.applyNetworkConfig(config)
} catch (e: Exception) {
service.log.error("failed to apply os network config: $e", "U6hVQEJR", e)
return -1
}
return pfd?.detachFd() ?: -1
}
}
private data class NetworkInterfaceProps(val name: String, val index: Int)
private lateinit var rustFfi: RustFfi
private lateinit var log: Logger
private lateinit var handler: Handler
private val connectivityManager
get() = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
private var vpnStatus: ManagerCmdOk.GetStatus.VpnStatus? = null
private var currentNetwork: Network? = null
override fun onCreate() {
super.onCreate()
logNoFfi.info("ObscuraVpnService onCreate entry")
rustFfi = RustFfi(this, "obscura.net/android/${BuildConfig.VERSION_NAME}")
log = rustFfi.logger(logNoFfi.tag)
if (instance.getAndSet(this) != null) {
log.error("instance already initialized", "xR4mNb7c")
}
requireVpnServiceProcess()
log.info("onCreate", "vqiGa01f")
handler = Handler(Looper.getMainLooper())
val networkRequest =
NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build()
val service = this
connectivityManager.registerBestMatchingNetworkCallback(
networkRequest,
object : NetworkCallback() {
override fun onAvailable(network: Network) {
service.currentNetwork = network
service.updateInterface(network)
}
override fun onLost(network: Network) {
if (network == service.currentNetwork) {
service.currentNetwork = null
service.updateInterface(null)
}
}
},
handler,
)
createNotificationChannel()
loadStatus(null)
}
private fun start() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
this.startForeground(
NOTIFICATION_ID,
this.buildNotification(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
)
} else {
this.startForeground(NOTIFICATION_ID, this.buildNotification())
}
}
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int,
): Int {
log.info("onStartCommand $intent ${intent?.action} $flags $startId", "C9rsG0uh")
this.start()
if (intent?.action == SERVICE_INTERFACE) {
log.info("onStartCommand was system-initiated", "sktWFegO")
this.startTunnel(null)
}
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder {
log.info("onBind $intent ${intent?.action}", "lckBR8hX")
if (intent?.action == SERVICE_INTERFACE) {
log.info("onBind was system-initiated", "4olaayXf")
this.start()
}
return Binder(this)
}
override fun onRebind(intent: Intent?) {
log.info("onRebind $intent ${intent?.action}", "AcVtL2Ub")
super.onRebind(intent)
if (intent?.action == SERVICE_INTERFACE) {
log.info("onRebind was system-initiated", "YsdxJ7Ni")
this.start()
}
}
override fun onUnbind(intent: Intent?): Boolean {
log.info("onUnbind $intent ${intent?.action}", "woAdA7g2")
if (intent?.action == SERVICE_INTERFACE) {
log.info("onUnbind was system-initiated", "oNOWQoPR")
this.stopTunnel()
this.stopForeground(STOP_FOREGROUND_DETACH)
}
return true
}
private fun onStatusUpdated(status: ManagerCmdOk.GetStatus) {
log.info("status updated $status", "xXx7PxdD")
vpnStatus = status.vpnStatus
loadStatus(status.version)
updateNotification()
}
override fun onRevoke() {
super.onRevoke()
log.info("onRevoke", "V3qS5kil")
this.stopTunnel()
this.stopForeground(STOP_FOREGROUND_DETACH)
}
override fun onDestroy() {
super.onDestroy()
if (instance.getAndSet(null) == null) {
log.error("instance already cleared", "bQ5wKr8d")
}
log.info("onDestroy", "yNLRpqaN")
stopTunnel()
}
private fun updateNotification() {
// permission should already have been granted, but checking here to avoid crashes and to fix
// the lint errors
if (
ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS,
) == PackageManager.PERMISSION_GRANTED
) {
NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, buildNotification())
}
}
private fun buildNotification() =
NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentIntent(
PendingIntent.getActivity(
this,
0,
Intent().apply {
this.action = Intent.ACTION_MAIN
this.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
this.setClassName(
BuildConfig.APPLICATION_ID,
MainActivity::class.qualifiedName!!,
)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
)
.setContentTitle(getString(R.string.app_name))
.setContentText(
getString(
R.string.notification_vpn_text,
when (this.vpnStatus) {
is ManagerCmdOk.GetStatus.VpnStatus.Connected ->
getString(R.string.notification_vpn_status_connected)
is ManagerCmdOk.GetStatus.VpnStatus.Connecting ->
getString(R.string.notification_vpn_status_connecting)
is ManagerCmdOk.GetStatus.VpnStatus.Disconnected,
null -> getString(R.string.notification_vpn_status_disconnected)
},
),
)
.setSmallIcon(R.drawable.ic_stat_name)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setOngoing(true)
.setLocalOnly(true)
.setOnlyAlertOnce(true)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build()
private fun createNotificationChannel() {
NotificationManagerCompat.from(this)
.createNotificationChannel(
NotificationChannelCompat.Builder(
NOTIFICATION_CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_LOW,
)
.setName(getString(R.string.notification_channel_vpn_name))
.build(),
)
}
private fun loadStatus(knownVersion: String?) {
log.info("load status $knownVersion", "8pXipD8h")
CompletableFuture<String>().also {
rustFfi.jsonFfi(
jsonConfig.encodeToString(ManagerCmd.GetStatus(knownVersion)),
it,
)
it.handle { data, tr ->
log.info("getStatus completed $data", "oiAyY4gh", tr)
data?.let { data -> onStatusUpdated(jsonConfig.decodeFromString(data)) }
}
}
}
private fun setTunnelArgs(exit: String?, active: Boolean?) {
CompletableFuture<String>().also {
rustFfi.jsonFfi(
jsonConfig.encodeToString(
ManagerCmd.SetTunnelArgs(
args = exit?.let { exit -> jsonConfig.decodeFromString(exit) },
active,
),
),
it,
)
}
}
private fun stopTunnel() {
setTunnelArgs(null, false)
}
private fun startTunnel(exitSelector: String?) {
setTunnelArgs(exitSelector, true)
}
private fun applyNetworkConfig(networkConfig: OsNetworkConfig): ParcelFileDescriptor? {
log.info("applying network config", "q9cnmRY0")
val pfd =
Builder()
.apply {
// always disallow current app so it doesn't get routed through the VPN
addDisallowedApplication(applicationInfo.packageName)
setMtu(networkConfig.mtu)
// Inherit meteredness from the underlying network (set via setUnderlyingNetworks).
// Without this, VpnService.Builder defaults to always marking the VPN as metered,
// regardless of the underlying network.
setMetered(false)
if (!networkConfig.useSystemDns) {
networkConfig.dns.forEach { addDnsServer(it) }
}
networkConfig.ipv4.split("/").let { addAddress(it[0], if (it.size == 2) it[1].toInt() else 32) }
networkConfig.ipv6.split("/").let { addAddress(it[0], if (it.size == 2) it[1].toInt() else 128) }
addRoute("0.0.0.0", 0)
addRoute("::", 0)
allowFamily(OsConstants.AF_INET)
allowFamily(OsConstants.AF_INET6)
}
.establish()
if (pfd == null) {
log.error("VpnService.Builder.establish() returned null", "tR7uWe2x")
}
return pfd
}
private fun getNetworkInterfaceProps(network: Network?): NetworkInterfaceProps? {
val network = network ?: return null
val linkProperties =
this.connectivityManager.getLinkProperties(network)
?: run {
log.error("failed to get link properties for network: $network", "W0JKaOGP")
return null
}
val name =
linkProperties.interfaceName
?: run {
log.error("network has no interface name: $network", "ukjpaGLl")
return null
}
val ni =
NetworkInterface.getByName(name)
?: run {
log.error("failed to get interface by name: $name", "JvEt0GtR")
return null
}
log.info("setting network interface: $name ${ni.index}", "pOsKRATd")
return NetworkInterfaceProps(name, ni.index)
}
private fun updateInterface(network: Network?) {
log.info("network interface changed: $network", "crWriIOe")
this.setUnderlyingNetworks(if (network != null) arrayOf(network) else emptyArray())
val networkInterface = this.getNetworkInterfaceProps(network)
if (networkInterface != null) {
rustFfi.setNetworkInterface(networkInterface.name, networkInterface.index)
} else {
rustFfi.unsetNetworkInterface()
}
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/services/OsNetworkConfig.kt
================================================
package net.obscura.vpnclientapp.services
import kotlinx.serialization.Serializable
// Keep synchronized with rustlib/src/network_config.rs
@Serializable
data class OsNetworkConfig(
val dns: List<String>,
val ipv4: String,
val ipv6: String,
val mtu: Int,
val useSystemDns: Boolean,
)
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/sharing/DebugArchiveFileProvider.java
================================================
package net.obscura.vpnclientapp.sharing;
import androidx.core.content.FileProvider;
import net.obscura.vpnclientapp.R;
// We need to extend `FileProvider` because some OEMs strip `meta-data` tags from the manifest:
// https://github.com/androidx/androidx/commit/a4385569db989747caf6b110b345a09ceb86acc7
// ...unfortunately, Kotlin subclasses don't inherit static methods, so we need to use Java.
public class DebugArchiveFileProvider extends FileProvider {
public DebugArchiveFileProvider() {
super(R.xml.debug_archive_file_provider_paths);
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/BillingModule.kt
================================================
package net.obscura.vpnclientapp.ui
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.ActivityRetainedLifecycle
import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityRetainedScoped
import net.obscura.vpnclientapp.BillingFacade
// Lifecycle/scope discussion:
// https://www.revenuecat.com/blog/engineering/hilt-sdk-lifecycle/
@Module
@InstallIn(ActivityRetainedComponent::class)
object BillingModule {
@Provides
@ActivityRetainedScoped
fun provideBillingFacade(
@ApplicationContext context: Context,
lifecycle: ActivityRetainedLifecycle,
): BillingFacade {
val billing = BillingFacade(context)
lifecycle.addOnClearedListener { billing.destroy() }
return billing
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/JsonFfiBroadcastReceiver.kt
================================================
package net.obscura.vpnclientapp.ui
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.completeWith
import net.obscura.lib.util.Logger
import net.obscura.vpnclientapp.services.IObscuraVpnService
import net.obscura.vpnclientapp.services.getJsonFfiExtras
private val log = Logger(JsonFfiBroadcastReceiver::class)
class JsonFfiBroadcastReceiver : BroadcastReceiver() {
companion object {
private val waiting by lazy { ConcurrentHashMap<Long, CompletableDeferred<String>>() }
private val currentId = AtomicLong(0)
internal fun waitForResponse(
binder: IObscuraVpnService,
cmd: String,
) =
CompletableDeferred<String>().also { job ->
val id = this.currentId.incrementAndGet()
log.trace("job $id registered: $cmd")
try {
binder.jsonFfi(id, cmd)
this.waiting[id] = job
} catch (e: Throwable) {
log.error("job $id failed: $e", tr = e)
job.completeExceptionally(e)
}
}
}
override fun onReceive(context: Context, intent: Intent) {
val args = intent.getJsonFfiExtras()
waiting.remove(args.id)?.completeWith(args.result)
?: run { log.error("job ${args.id} already completed (or never registered)") }
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/NetworkStatusObserver.kt
================================================
package net.obscura.vpnclientapp.ui
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import net.obscura.lib.util.Logger
private val log = Logger(NetworkStatusObserver::class)
// Network callbacks run on the "connectivity thread" by default:
// https://developer.android.com/develop/connectivity/network-ops/reading-network-state#listening-events
internal class NetworkStatusObserver(context: Context, private val callback: Callback) :
ConnectivityManager.NetworkCallback() {
interface Callback {
fun onAvailableNetworksChanged(availableNetworks: Int)
}
private var availableNetworks = 0
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
init {
val networkRequest =
NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
this.connectivityManager.registerNetworkCallback(networkRequest, this)
}
override fun onAvailable(network: Network) {
this.availableNetworks += 1
log.debug("network available: $network (available networks: ${this.availableNetworks})")
this.callback.onAvailableNetworksChanged(this.availableNetworks)
}
override fun onLost(network: Network) {
this.availableNetworks = (this.availableNetworks - 1).coerceAtLeast(0)
log.debug("network lost: $network (available networks: ${this.availableNetworks})")
this.callback.onAvailableNetworksChanged(this.availableNetworks)
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/ObscuraUI.kt
================================================
package net.obscura.vpnclientapp.ui
import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.postDelayed
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.navigation.NavigationBarView
import net.obscura.lib.util.Logger
import net.obscura.vpnclientapp.R
import net.obscura.vpnclientapp.activities.MainActivity
import net.obscura.vpnclientapp.client.ManagerCmdOk
import net.obscura.vpnclientapp.services.IObscuraVpnService
private val log = Logger(ObscuraUI::class)
class ObscuraUI @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
private lateinit var vpnStatusObserver: VpnStatusObserver
val canGoBack
get() = (webView?.canGoBack() ?: false) || (bottomNavigation.selectedItemId != R.id.nav_connection)
private lateinit var webViewContainer: FrameLayout
private lateinit var bottomNavigation: BottomNavigationView
private var loggedIn: Boolean = false
private var webView: ObscuraWebView? = null
private val itemReselectedListener = NavigationBarView.OnItemReselectedListener { navigateToTab(it.itemId) }
private val itemSelectedListener =
NavigationBarView.OnItemSelectedListener {
navigateToTab(it.itemId)
true
}
private fun setLoggedIn(loggedIn: Boolean) {
this.bottomNavigation.visibility = if (loggedIn) VISIBLE else GONE
this.loggedIn = loggedIn
}
override fun onFinishInflate() {
super.onFinishInflate()
this.webViewContainer = this.findViewById(R.id.web_view_container)
this.bottomNavigation = this.findViewById(R.id.nav_view)
this.bottomNavigation.visibility = GONE
this.bottomNavigation.setOnItemReselectedListener(itemReselectedListener)
this.bottomNavigation.setOnItemSelectedListener(itemSelectedListener)
// TODO: Synchronize padding with IME animation
// https://linear.app/soveng/issue/OBS-3233/android-ime-animation-jank
// TODO: Edge-to-edge `WebView`
// https://linear.app/soveng/issue/OBS-3237/android-edge-to-edge-webview
ViewCompat.setOnApplyWindowInsetsListener(this.webViewContainer) { view, windowInsets ->
val insetsMask =
WindowInsetsCompat.Type.displayCutout()
.or(WindowInsetsCompat.Type.navigationBars())
.or(WindowInsetsCompat.Type.statusBars())
val insets = windowInsets.getInsets(insetsMask)
val imeMask = WindowInsetsCompat.Type.ime()
val bottom =
if (windowInsets.isVisible(imeMask)) {
windowInsets.getInsets(imeMask).bottom
} else if (!this.loggedIn) {
insets.bottom
} else {
0
}
// Only use non-zero insets when there's overlap
// https://developer.android.com/develop/ui/views/layout/webapps/understand-window-insets#bounds-overlap
view.setPadding(insets.left, insets.top, insets.right, bottom)
// Child `WebView` should ignore any insets we applied here
// https://developer.android.com/develop/ui/views/layout/webapps/understand-window-insets#inset-handling
WindowInsetsCompat.Builder(windowInsets).setInsets(insetsMask.or(imeMask), Insets.NONE).build()
}
ViewCompat.setOnApplyWindowInsetsListener(this.bottomNavigation) { view, windowInsets ->
// Hide bottom nav when IME is visible
// https://github.com/software-mansion/react-native-screens/issues/3647
val showBottomNav = this.loggedIn && !windowInsets.isVisible(WindowInsetsCompat.Type.ime())
view.visibility = if (showBottomNav) VISIBLE else GONE
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(systemBars.left, 0, systemBars.right, systemBars.bottom)
WindowInsetsCompat.CONSUMED
}
}
fun onCreate(
isFreshLaunch: Boolean,
binder: IObscuraVpnService,
mainActivity: MainActivity,
osStatusManager: OsStatusManager,
) {
onDestroy()
this.vpnStatusObserver =
VpnStatusObserver(
binder,
object : VpnStatusObserver.Callback {
private var isAutoConnectEligible = isFreshLaunch
override suspend fun onStatusChanged(status: ManagerCmdOk.GetStatus) {
osStatusManager.update {
this.vpnStatus =
when (status.vpnStatus) {
is ManagerCmdOk.GetStatus.VpnStatus.Connected -> OsStatus.OsVpnStatus.Connected
is ManagerCmdOk.GetStatus.VpnStatus.Connecting -> OsStatus.OsVpnStatus.Connecting
is ManagerCmdOk.GetStatus.VpnStatus.Disconnected ->
OsStatus.OsVpnStatus.Disconnected
}
}
this@ObscuraUI.setLoggedIn(status.accountId != null && !status.inNewAccountFlow)
val shouldAutoConnect =
this.isAutoConnectEligible &&
status.autoConnect &&
status.vpnStatus is ManagerCmdOk.GetStatus.VpnStatus.Disconnected
this.isAutoConnectEligible = false
if (shouldAutoConnect) {
mainActivity.vpnPermissionRequestManager
.requestVpnStart()
.mapCatching { binder.startTunnel(null) }
.onSuccess { log.info("auto-connected VPN") }
.onFailure { log.error("failed to auto-connect VPN: ${it.message}", tr = it) }
}
}
},
)
mainActivity.lifecycle.addObserver(this.vpnStatusObserver)
webView =
ObscuraWebView(context, binder, mainActivity, osStatusManager).apply {
webViewContainer.addView(
this,
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
)
onPageLoadedCallback = {
if (bottomNavigation.selectedItemId != R.id.nav_connection) {
// TODO: make sure UI picks this up correctly
var delay = 0L
while (delay < 100L) {
postDelayed(delay) { navigateToTab(bottomNavigation.selectedItemId) }
delay += 10
}
}
}
}
}
fun onResume() {
webView?.onResume()
}
fun onPause() {
webView?.onPause()
}
fun onDestroy() {
bottomNavigation.visibility = GONE
webViewContainer.removeAllViews()
this.webView?.bridge?.cancel()
this.webView?.destroy()
this.webView = null
}
override fun invalidate() {
super.invalidate()
this.webView?.invalidate()
}
fun goBack() {
if (webView?.canGoBack() ?: false) {
webView?.goBack()
} else if (bottomNavigation.selectedItemId != R.id.nav_connection) {
bottomNavigation.selectedItemId = R.id.nav_connection
}
}
private fun navigateToTab(id: Int) {
val path =
when (id) {
R.id.nav_connection -> ""
R.id.nav_location -> "location"
R.id.nav_account -> "account"
R.id.nav_settings -> "settings"
R.id.nav_about -> "about"
else -> {
log.error("unrecognized view id: $id")
return
}
}
this.webView?.navigate(path)
}
fun handleObscuraUri(uri: Uri) {
log.debug("handling deep link: $uri")
val id =
when (uri.path) {
"/account" -> R.id.nav_account
"/location" -> R.id.nav_location
else -> {
log.error("unrecognized path for deep link: $uri")
return
}
}
this.bottomNavigation.selectedItemId = id
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/ObscuraWebView.kt
================================================
package net.obscura.vpnclientapp.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.webkit.WebMessage
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.net.toUri
import androidx.webkit.WebViewAssetLoader
import net.obscura.vpnclientapp.activities.MainActivity
import net.obscura.vpnclientapp.services.IObscuraVpnService
import net.obscura.vpnclientapp.ui.bridge.WebCmdBridge
@SuppressLint("SetJavaScriptEnabled", "ViewConstructor")
class ObscuraWebView
@JvmOverloads
constructor(
context: Context,
binder: IObscuraVpnService,
mainActivity: MainActivity,
osStatusManager: OsStatusManager,
attrs: AttributeSet? = null,
) : WebView(context, attrs) {
companion object {
val ORIGIN = "https://appassets.androidplatform.net".toUri()
val HOME = "$ORIGIN/assets/web/index.html"
}
val bridge =
WebCmdBridge(context, binder, mainActivity, osStatusManager) { data ->
post { postWebMessage(WebMessage("android/$data"), ORIGIN) }
}
var onPageLoadedCallback: ((String) -> Unit)? = null
init {
settings.domStorageEnabled = true
settings.javaScriptEnabled = true
addJavascriptInterface(bridge, "obscuraAndroidCommandBridge")
WebViewAssetLoader.Builder()
.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(context))
.addPathHandler("/res/", WebViewAssetLoader.ResourcesPathHandler(context))
.build()
.also { assetLoader ->
webViewClient =
object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest,
): Boolean {
val shouldOverride = request.url.host != ORIGIN.host
if (shouldOverride && request.isForMainFrame) {
context.startActivity(
Intent(
Intent.ACTION_VIEW,
if (request.url.scheme == "http") {
request.url.buildUpon().scheme("https").build()
} else {
request.url
},
)
)
}
return shouldOverride
}
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest,
) = assetLoader.shouldInterceptRequest(request.url)
override fun onPageFinished(view: WebView?, url: String) {
super.onPageFinished(view, url)
onPageLoadedCallback?.invoke(url)
}
}
}
loadUrl(HOME)
}
fun navigate(path: String) {
postWebMessage(WebMessage("android-navigate/$path"), ORIGIN)
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/OsStatus.kt
================================================
package net.obscura.vpnclientapp.ui
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class OsStatus(
val version: String,
val internetAvailable: Boolean,
val osVpnStatus: OsVpnStatus,
val srcVersion: String,
val updaterStatus: UpdaterStatus,
val debugBundleStatus: DebugBundleStatus,
val canSendMail: Boolean,
val loginItemStatus: LoginItemStatus?,
val playBilling: Boolean,
) {
// TODO: https://linear.app/soveng/issue/OBS-2640/change-nevpnstatus-to-be-platform-agnostic
@Serializable
enum class OsVpnStatus {
@SerialName("disconnected") Disconnected,
@SerialName("connecting") Connecting,
@SerialName("connected") Connected,
}
@Serializable data class LoginItemStatus(val registered: Boolean, val error: String?)
@Serializable
data class DebugBundleStatus(
var inProgress: Boolean?,
var latestPath: String?,
var inProgressCounter: Long,
)
@Serializable
data class UpdaterStatus(
val type: String, // TODO UpdaterStatusType
val appcast: AppcastSummary?,
val error: String?,
val errorCode: Long?,
) {
@Serializable
data class AppcastSummary(
val date: String,
val description: String,
val version: String,
val minSystemVersionSdk: Boolean,
)
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/OsStatusManager.kt
================================================
package net.obscura.vpnclientapp.ui
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.completeWith
import net.obscura.lib.util.Logger
import net.obscura.vpnclientapp.BuildConfig
import net.obscura.vpnclientapp.client.jsonConfig
private val log = Logger(OsStatusManager::class)
@Singleton // Prevents loss of state on activity destruction
class OsStatusManager @Inject constructor(@ApplicationContext context: Context) : NetworkStatusObserver.Callback {
data class State(
var debugBundleStatus: OsStatus.DebugBundleStatus =
OsStatus.DebugBundleStatus(
inProgress = false,
latestPath = null,
inProgressCounter = 0,
),
var internetAvailable: Boolean = false,
var vpnStatus: OsStatus.OsVpnStatus = OsStatus.OsVpnStatus.Disconnected,
)
private data class VersionedState(val version: UUID, val state: State)
private var current = VersionedState(UUID.randomUUID(), State())
private val waiting = ArrayList<CompletableDeferred<String>>()
init {
NetworkStatusObserver(context, this)
}
override fun onAvailableNetworksChanged(availableNetworks: Int) {
this.update { this.internetAvailable = availableNetworks > 0 }
}
@Synchronized
fun update(block: State.() -> Unit = {}) {
val version = UUID.randomUUID()
val result = runCatching {
block(this.current.state)
OsStatus(
version = version.toString(),
internetAvailable = this.current.state.internetAvailable,
osVpnStatus = this.current.state.vpnStatus,
srcVersion = BuildConfig.VERSION_NAME,
updaterStatus =
OsStatus.UpdaterStatus(
type = "uninitiated",
appcast = null,
error = null,
errorCode = null,
),
debugBundleStatus = this.current.state.debugBundleStatus,
canSendMail = true,
loginItemStatus = null,
playBilling =
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
(BuildConfig.FLAVOR == "play"),
)
.let { jsonConfig.encodeToString(it) }
}
this.current = VersionedState(version, this.current.state)
log.debug("updated OS status: ${this.current}")
this.waiting.forEach { it.completeWith(result) }
this.waiting.clear()
}
@Synchronized
fun waitForUpdate(knownVersion: String?): Deferred<String> =
CompletableDeferred<String>().also {
this.waiting.add(it)
if (this.current.version.toString() != knownVersion) {
this.update()
}
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/VpnPermissionRequestManager.kt
================================================
package net.obscura.vpnclientapp.ui
import android.Manifest
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeSource
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.onSubscription
import net.obscura.lib.util.Logger
import net.obscura.vpnclientapp.client.errorCodeLegacyAlwaysOn
import net.obscura.vpnclientapp.client.errorCodeOther
import net.obscura.vpnclientapp.client.errorCodeOtherAppAlwaysOn
import net.obscura.vpnclientapp.client.errorCodePermissionNotGranted
import net.obscura.vpnclientapp.services.PrepareResult
import net.obscura.vpnclientapp.services.prepareVpnService
import net.obscura.vpnclientapp.services.startVpnService
private val log = Logger(VpnPermissionRequestManager::class)
@ActivityScoped
class VpnPermissionRequestManager @Inject constructor(private val activity: FragmentActivity) {
private val vpnPermissionRequestCancelThreshold: Duration = 150.milliseconds
private val vpnPermissionRequestResultTx = MutableSharedFlow<ActivityResult>(extraBufferCapacity = 1)
private val vpnPermissionRequestResultRx = this.vpnPermissionRequestResultTx.asSharedFlow()
private val vpnPermissionRequestLauncher: ActivityResultLauncher<Intent> =
this.activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
log.debug("VPN permission request activity result: $result")
val wasEmitted = this.vpnPermissionRequestResultTx.tryEmit(result)
if (!wasEmitted) {
log.warn("multiple VPN permission requests while collecting")
}
}
private val notificationPermissionRequestLauncher: ActivityResultLauncher<String> =
this.activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
// We don't actually care if we're granted permission, since this is
// just the user's preference between "classic" foreground service
// notifications vs. the modern Task Manager.
log.debug("notification permission request activity result: $isGranted")
}
private fun requestNotificationPermission() {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this.activity, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
this.notificationPermissionRequestLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
private fun onSuccess(): Result<Unit> {
this.requestNotificationPermission()
return this.activity.startVpnService()
}
// Android 12+ has no API for checking if another app has Always-On enabled. Instead, the permission request
// receives `RESULT_CANCELED` immediately, requiring us to use a heuristic to determine that we're silently
// unable to request VPN permissions from the user.
private fun onCanceled(vpnPermissionRequestStart: TimeSource.Monotonic.ValueTimeMark): Result<Unit> {
val vpnPermissionRequestEnd = TimeSource.Monotonic.markNow()
val elapsed = vpnPermissionRequestEnd - vpnPermissionRequestStart
log.debug("$elapsed elapsed between VPN permission request launch and cancellation")
return if (elapsed > this.vpnPermissionRequestCancelThreshold) {
log.debug("heuristic determined that cancellation was user-initiated")
Result.failure(errorCodePermissionNotGranted())
} else {
log.debug("heuristic determined that cancellation was automatic")
Result.failure(errorCodeOtherAppAlwaysOn())
}
}
suspend fun requestVpnStart(): Result<Unit> =
when (val prepareResult = this.activity.prepareVpnService()) {
is PrepareResult.CreateProfile -> {
val vpnPermissionRequestStart = TimeSource.Monotonic.markNow()
val vpnPermissionRequestResult =
this.vpnPermissionRequestResultRx
.onSubscription {
this@VpnPermissionRequestManager.vpnPermissionRequestLauncher.launch(prepareResult.intent)
}
.firstOrNull()
?: run {
log.error("VPN permission request result flow was empty")
return Result.failure(errorCodeOther())
}
when (vpnPermissionRequestResult.resultCode) {
RESULT_OK -> this.onSuccess()
RESULT_CANCELED -> this.onCanceled(vpnPermissionRequestStart)
else -> {
log.error("unexpected VPN start activity result: $vpnPermissionRequestResult")
Result.failure(errorCodeOther())
}
}
}
is PrepareResult.Ready -> this.onSuccess()
is PrepareResult.LegacyAlwaysOn -> Result.failure(errorCodeLegacyAlwaysOn())
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/VpnStatusObserver.kt
================================================
package net.obscura.vpnclientapp.ui
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import net.obscura.lib.util.Logger
import net.obscura.vpnclientapp.client.ManagerCmd
import net.obscura.vpnclientapp.client.ManagerCmdOk
import net.obscura.vpnclientapp.client.jsonConfig
import net.obscura.vpnclientapp.services.IObscuraVpnService
private val log = Logger(VpnStatusObserver::class)
class VpnStatusObserver(
private val binder: IObscuraVpnService,
private val callback: Callback,
) : DefaultLifecycleObserver {
interface Callback {
suspend fun onStatusChanged(status: ManagerCmdOk.GetStatus)
}
private var job: Job? = null
override fun onStart(owner: LifecycleOwner) {
this.job =
owner.lifecycleScope.launch {
var knownVersion: String? = null
while (this.isActive) {
try {
val status =
JsonFfiBroadcastReceiver.waitForResponse(
this@VpnStatusObserver.binder,
jsonConfig.encodeToString(ManagerCmd.GetStatus(knownVersion)),
)
.await()
.let { jsonConfig.decodeFromString<ManagerCmdOk.GetStatus>(it) }
knownVersion = status.version
log.debug("updated VPN status: $status")
this@VpnStatusObserver.callback.onStatusChanged(status)
} catch (e: CancellationException) {
log.debug("VPN status job canceled: ${e.message}")
throw e
} catch (e: Throwable) {
log.error("failed to update VPN status: $e", tr = e)
}
delay(10.milliseconds)
}
}
}
override fun onStop(owner: LifecycleOwner) {
this.job?.cancel(CancellationException("lifecycle owner stopped"))
}
override fun onDestroy(owner: LifecycleOwner) {
this.job?.cancel(CancellationException("lifecycle owner destroyed"))
owner.lifecycle.removeObserver(this)
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/bridge/WebCmd.kt
================================================
package net.obscura.vpnclientapp.ui.bridge
import android.content.Context
import kotlinx.serialization.KeepGeneratedSerializer
import kotlinx.serialization.Serializable
import net.obscura.lib.util.ExternallyTaggedEnumSerializer
import net.obscura.lib.util.ExternallyTaggedEnumVariantSerializer
import net.obscura.vpnclientapp.activities.MainActivity
import net.obscura.vpnclientapp.client.ManagerCmd
import net.obscura.vpnclientapp.client.errorCodeUnsupportedOnOS
import net.obscura.vpnclientapp.client.jsonConfig
import net.obscura.vpnclientapp.preferences.Preferences
import net.obscura.vpnclientapp.services.IObscuraVpnService
import net.obscura.vpnclientapp.ui.JsonFfiBroadcastReceiver
import net.obscura.vpnclientapp.ui.OsStatusManager
private val jsonUnit = jsonConfig.encodeToString(Unit)
@Serializable(with = WebCmd.Serializer::class)
internal sealed interface WebCmd {
object Serializer :
ExternallyTaggedEnumSerializer<WebCmd>(
WebCmd::class,
listOf(
DebuggingArchive.Serializer,
EmailDebugArchive.Serializer,
GetOsStatus.Serializer,
JsonFfiCmd.Serializer,
PurchaseSubscription.Serializer,
RevealItemInDir.Serializer,
SetColorScheme.Serializer,
SetFeatureFlag.Serializer,
ShareDebugArchive.Serializer,
StartTunnel.Serializer,
StopTunnel.Serializer,
),
)
data class Args(
val context: Context,
val binder: IObscuraVpnService,
val mainActivity: MainActivity,
val osStatusManager: OsStatusManager,
)
suspend fun run(args: Args): String
@KeepGeneratedSerializer
@Serializable(with = DebuggingArchive.Serializer::class)
data class DebuggingArchive(
val userFeedback: String?,
) : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<DebuggingArchive>("debuggingArchive", generatedSerializer())
// Eventually, all platforms should just use the JSON FFI command to create debug archives, but for now,
// adapting the command here is the least invasive change.
// TODO: https://linear.app/soveng/issue/OBS-3095/cross-platform-debug-archive-story
override suspend fun run(args: Args) =
jsonUnit.also {
args.osStatusManager.update { this.debugBundleStatus.inProgress = true }
val path = runCatching {
JsonFfiCmd(jsonConfig.encodeToString(ManagerCmd.CreateDebugArchive(userFeedback))).run(args).let {
jsonConfig.decodeFromString<String>(it)
}
}
args.osStatusManager.update {
this.debugBundleStatus.inProgress = false
path.onSuccess { this.debugBundleStatus.latestPath = it }
}
path.getOrThrow()
}
}
@KeepGeneratedSerializer
@Serializable(with = EmailDebugArchive.Serializer::class)
data class EmailDebugArchive(
val path: String,
val subject: String,
val body: String,
) : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<EmailDebugArchive>("emailDebugArchive", generatedSerializer())
override suspend fun run(args: Args) =
jsonUnit.also { shareDebugArchive(args.context, path, true, subject, body) }
}
@KeepGeneratedSerializer
@Serializable(with = GetOsStatus.Serializer::class)
data class GetOsStatus(val knownVersion: String?) : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<GetOsStatus>("getOsStatus", generatedSerializer())
override suspend fun run(args: Args) = args.osStatusManager.waitForUpdate(knownVersion).await()
}
@KeepGeneratedSerializer
@Serializable(with = JsonFfiCmd.Serializer::class)
data class JsonFfiCmd(
val cmd: String,
) : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<JsonFfiCmd>("jsonFfiCmd", generatedSerializer())
override suspend fun run(args: Args) = JsonFfiBroadcastReceiver.waitForResponse(args.binder, this.cmd).await()
}
@KeepGeneratedSerializer
@Serializable(with = PurchaseSubscription.Serializer::class)
class PurchaseSubscription : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<PurchaseSubscription>(
"purchaseSubscription",
generatedSerializer(),
)
override suspend fun run(args: Args) =
args.mainActivity.billingFacade.launchFlow(args.mainActivity).let { jsonConfig.encodeToString(it) }
}
@KeepGeneratedSerializer
@Serializable(with = RevealItemInDir.Serializer::class)
data class RevealItemInDir(
val path: String,
) : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<RevealItemInDir>(
"revealItemInDir",
generatedSerializer(),
)
override suspend fun run(args: Args) = throw errorCodeUnsupportedOnOS()
}
@KeepGeneratedSerializer
@Serializable(with = SetColorScheme.Serializer::class)
data class SetColorScheme(
val value: Preferences.ColorScheme,
) : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<SetColorScheme>(
"setColorScheme",
generatedSerializer(),
)
override suspend fun run(args: Args) = jsonUnit.also { Preferences(args.context).colorScheme = this.value }
}
@KeepGeneratedSerializer
@Serializable(with = SetFeatureFlag.Serializer::class)
data class SetFeatureFlag(
val flag: String,
val active: Boolean,
) : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<SetFeatureFlag>(
"setFeatureFlag",
generatedSerializer(),
)
override suspend fun run(args: Args) = throw errorCodeUnsupportedOnOS()
}
@KeepGeneratedSerializer
@Serializable(with = ShareDebugArchive.Serializer::class)
data class ShareDebugArchive(
val path: String,
) : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<ShareDebugArchive>(
"shareDebugArchive",
generatedSerializer(),
)
override suspend fun run(args: Args) = jsonUnit.also { shareDebugArchive(args.context, path, false) }
}
@KeepGeneratedSerializer
@Serializable(with = StartTunnel.Serializer::class)
data class StartTunnel(
val tunnelArgs: String? = null,
) : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<StartTunnel>(
"startTunnel",
generatedSerializer(),
)
override suspend fun run(args: Args) =
jsonUnit.also {
args.mainActivity.vpnPermissionRequestManager.requestVpnStart().getOrThrow()
args.binder.startTunnel(this@StartTunnel.tunnelArgs)
}
}
@KeepGeneratedSerializer
@Serializable(with = StopTunnel.Serializer::class)
class StopTunnel : WebCmd {
internal object Serializer :
ExternallyTaggedEnumVariantSerializer<StopTunnel>(
"stopTunnel",
generatedSerializer(),
)
override suspend fun run(args: Args) = jsonUnit.also { args.binder.stopTunnel() }
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/bridge/WebCmdBridge.kt
================================================
package net.obscura.vpnclientapp.ui.bridge
import android.content.Context
import android.webkit.JavascriptInterface
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import net.obscura.lib.util.Logger
import net.obscura.vpnclientapp.activities.MainActivity
import net.obscura.vpnclientapp.client.ErrorCodeException
import net.obscura.vpnclientapp.client.errorCodeOther
import net.obscura.vpnclientapp.client.jsonConfig
import net.obscura.vpnclientapp.services.IObscuraVpnService
import net.obscura.vpnclientapp.ui.OsStatusManager
private val log = Logger(WebCmdBridge::class)
class WebCmdBridge(
private val context: Context,
private val binder: IObscuraVpnService,
private val mainActivity: MainActivity,
private val osStatusManager: OsStatusManager,
private val postMessage: (data: String) -> Unit,
) {
private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
@Serializable
private data class Accept(
val id: Long,
val data: String,
)
@Serializable
private data class Reject(
val id: Long,
val error: String,
)
private fun accept(id: Long, data: String) {
this.postMessage(jsonConfig.encodeToString(Accept(id, data)))
}
private fun reject(id: Long, exception: ErrorCodeException) {
this.postMessage(jsonConfig.encodeToString(Reject(id, exception.errorCode)))
}
@JavascriptInterface
fun invoke(data: String, id: Long) {
this.scope.launch {
try {
this@WebCmdBridge.accept(
id,
jsonConfig
.decodeFromString<WebCmd>(data)
.run(WebCmd.Args(context, binder, this@WebCmdBridge.mainActivity, osStatusManager)),
)
} catch (exception: CancellationException) {
log.debug("invoke job canceled: ${exception.message}")
throw exception
} catch (exception: ErrorCodeException) {
this@WebCmdBridge.reject(id, exception)
} catch (exception: Throwable) {
log.error("unexpected exception type: $exception", tr = exception)
this@WebCmdBridge.reject(id, errorCodeOther())
}
}
}
fun cancel() {
this.scope.cancel()
}
}
================================================
FILE: android/app/src/main/java/net/obscura/vpnclientapp/ui/bridge/WebCmdHelpers.kt
================================================
package net.obscura.vpnclientapp.ui.bridge
import android.content.Context
import android.content.Intent
import java.io.File
import net.obscura.vpnclientapp.sharing.DebugArchiveFileProvider
internal fun shareDebugArchive(
context: Context,
path: String,
email: Boolean,
subject: String? = null,
body: String? = null,
) {
val uri =
DebugArchiveFileProvider.getUriForFile(
context,
"${context.packageName}.debug_archive_file_provider",
File(path),
)
val intent =
Intent(Intent.ACTION_SEND).apply {
this.type = "application/zip"
this.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
this.putExtra(Intent.EXTRA_STREAM, uri)
if (email) {
this.putExtra(Intent.EXTRA_EMAIL, arrayOf("support@obscura.net"))
this.putExtra(Intent.EXTRA_SUBJECT, subject)
this.putExtra(Intent.EXTRA_TEXT, body)
}
}
if (email) {
// There unfortunately isn't a way to only show email apps *and* have attachments. By not
// using the chooser here, we at least give the user the option to save their previously
// selected email app.
context.startActivity(intent)
} else {
context.startActivity(Intent.createChooser(intent, null))
}
}
================================================
FILE: android/app/src/main/res/drawable/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="MonochromeLauncherIcon">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
================================================
FILE: android/app/src/main/res/drawable/icon_about.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M453,680h60v-240h-60v240ZM479.98,366q14.02,0 23.52,-9.2T513,334q0,-14.45 -9.48,-24.22 -9.48,-9.78 -23.5,-9.78t-23.52,9.78Q447,319.55 447,334q0,13.6 9.48,22.8 9.48,9.2 23.5,9.2ZM480.27,880q-82.74,0 -155.5,-31.5Q252,817 197.5,762.5t-86,-127.34Q80,562.32 80,479.5t31.5,-155.66Q143,251 197.5,197t127.34,-85.5Q397.68,80 480.5,80t155.66,31.5Q709,143 763,197t85.5,127Q880,397 880,479.73q0,82.74 -31.5,155.5Q817,708 763,762.32q-54,54.31 -127,86Q563,880 480.27,880ZM480.5,820Q622,820 721,720.5t99,-241Q820,338 721.19,239T480,140q-141,0 -240.5,98.81T140,480q0,141 99.5,240.5t241,99.5ZM480,480Z"
android:fillColor="#000"/>
</vector>
================================================
FILE: android/app/src/main/res/drawable/icon_account.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M222,705q63,-44 125,-67.5T480,614q71,0 133.5,23.5T739,705q44,-54 62.5,-109T820,480q0,-145 -97.5,-242.5T480,140q-145,0 -242.5,97.5T140,480q0,61 19,116t63,109ZM479.81,510q-57.81,0 -97.31,-39.69 -39.5,-39.68 -39.5,-97.5 0,-57.81 39.69,-97.31 39.68,-39.5 97.5,-39.5 57.81,0 97.31,39.69 39.5,39.68 39.5,97.5 0,57.81 -39.69,97.31 -39.68,39.5 -97.5,39.5ZM480.47,880Q398,880 325,848.5t-127.5,-86q-54.5,-54.5 -86,-127.27Q80,562.47 80,479.73 80,397 111.5,324.5q31.5,-72.5 86,-127t127.27,-86q72.76,-31.5 155.5,-31.5 82.73,0 155.23,31.5 72.5,31.5 127,86t86,127.03q31.5,72.53 31.5,155T848.5,635q-31.5,73 -86,127.5t-127.03,86Q562.94,880 480.47,880ZM480,820q55,0 107.5,-16T691,748q-51,-36 -104,-55t-107,-19q-54,0 -107,19t-104,55q51,40 103.5,56T480,820ZM480,450q34,0 55.5,-21.5T557,373q0,-34 -21.5,-55.5T480,296q-34,0 -55.5,21.5T403,373q0,34 21.5,55.5T480,450ZM480,373ZM480,747Z"
android:fillColor="#000"/>
</vector>
================================================
FILE: android/app/src/main/res/drawable/icon_connection.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,880q-83,0 -156,-31.5t-127,-86q-54,-54.5 -85.5,-128T80,478q0,-83 31.5,-155.5T197,196q54,-54 127,-85t156,-31q26,0 50.5,4t49.5,9l-20,57q-20,-6 -40,-8t-40,-2q-42,35 -62,86t-34,104h196v60L373,390q-3,22 -4.5,44t-1.5,44q0,23 1.5,45.5T373,569h215q3,-23 4.5,-48t1.5,-61h60q0,36 -2,61t-4,48h160q6,-23 9,-47.5t3,-61.5h60q0,94 -31.5,171T763,763.5q-54,55.5 -127,86T480,880ZM152,569h159q-2,-23 -3,-45.5t-1,-45.5q0,-22 1,-44t4,-44L152,390q-6,22 -9,43.5t-3,44.5q0,23 3,45.5t9,45.5ZM395,810q-27,-42 -44.5,-87.5T322,629L172,629q35,66 93.5,111.5T395,810ZM172,330h151q10,-48 27.5,-93t43.5,-86q-73,18 -131,65t-91,114ZM480,822q38,-39 61,-89t36,-104L384,629q12,54 35,103.5t61,89.5ZM566,809q71,-22 129,-68t93,-112L639,629q-11,48 -28.5,93.5T566,809ZM674,400q-14,0 -24,-10t-10,-24v-132q0,-14 10,-24t24,-10h6v-40q0,-33 23.5,-56.5T760,80q33,0 56.5,23.5T840,160v40h6q14,0 24,10t10,24v132q0,14 -10,24t-24,10L674,400ZM720,200h80v-40q0,-17 -11.5,-28.5T760,120q-17,0 -28.5,11T720,158v42Z"
android:fillColor="#000"/>
</vector>
================================================
FILE: android/app/src/main/res/drawable/icon_location.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q157,0 270.5,104.5T878,443q-14,-8 -30,-14t-33,-9q-15,-87 -70,-156T607,161v18q0,35 -24,61t-59,26h-87v87q0,17 -13.5,28T393,392h-83v88h258q15,0 26,11t13,26q-13,23 -20,48.5t-7,51.5q0,60 34,112t66,98q-45,26 -95,39.5T480,880ZM437,819v-82q-35,0 -59,-26t-24,-61v-44L149,401q-5,20 -7,39.5t-2,39.5q0,130 84.5,227T437,819ZM780,880q-4,0 -6.5,-2t-3.5,-5q-13,-38 -34.5,-70.5T689,739q-21,-26 -35,-57t-14,-65q0,-58 41,-99t99,-41q58,0 99,41t41,99q0,34 -14,65t-35,57q-24,31 -46,63.5T790,873q-1,3 -3.5,5t-6.5,2ZM780,797q14,-21 28.5,-41t30.5,-40q17,-22 29,-46.5t12,-52.5q0,-42 -29,-71t-71,-29q-42,0 -71,29t-29,71q0,28 12,52.5t29,46.5q16,20 30.5,40t28.5,41ZM780,667q-21,0 -35.5,-14.5T730,617q0,-21 14.5,-35.5T780,567q21,0 35.5,14.5T830,617q0,21 -14.5,35.5T780,667Z"
android:fillColor="#000"/>
</vector>
================================================
FILE: android/app/src/main/res/drawable/icon_settings.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m388,880 l-20,-126q-19,-7 -40,-19t-37,-25l-118,54 -93,-164 108,-79q-2,-9 -2.5,-20.5T185,480q0,-9 0.5,-20.5T188,439L80,360l93,-164 118,54q16,-13 37,-25t40,-18l20,-127h184l20,126q19,7 40.5,18.5T669,250l118,-54 93,164 -108,77q2,10 2.5,21.5t0.5,21.5q0,10 -0.5,21t-2.5,21l108,78 -93,164 -118,-54q-16,13 -36.5,25.5T592,754L572,880L388,880ZM436,820h88l14,-112q33,-8 62.5,-25t53.5,-41l106,46 40,-72 -94,-69q4,-17 6.5,-33.5T715,480q0,-17 -2,-33.5t-7,-33.5l94,-69 -40,-72 -106,46q-23,-26 -52,-43.5T538,252l-14,-112h-88l-14,112q-34,7 -63.5,24T306,318l-106,-46 -40,72 94,69q-4,17 -6.5,33.5T245,480q0,17 2.5,33.5T254,547l-94,69 40,72 106,-46q24,24 53.5,41t62.5,25l14,112ZM480,610q54,0 92,-38t38,-92q0,-54 -38,-92t-92,-38q-54,0 -92,38t-38,92q0,54 38,92t92,38ZM480,480Z"
android:fillColor="#000"/>
</vector>
================================================
FILE: android/app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<net.obscura.vpnclientapp.ui.ObscuraUI xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ui"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/web_view_container"
android:isScrollContainer="true"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:labelVisibilityMode="labeled"
app:elevation="8dp"
android:elevation="8dp" />
</LinearLayout>
</net.obscura.vpnclientapp.ui.ObscuraUI>
================================================
FILE: android/app/src/main/res/menu/nav_menu.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_connection"
android:icon="@drawable/icon_connection"
android:title="@string/nav_connection" />
<item
android:id="@+id/nav_location"
android:icon="@drawable/icon_location"
android:title="@string/nav_location" />
<item
android:id="@+id/nav_account"
android:icon="@drawable/icon_account"
android:title="@string/nav_account" />
<item
android:id="@+id/nav_settings"
android:icon="@drawable/icon_settings"
android:title="@string/nav_settings" />
<item
android:id="@+id/nav_about"
android:icon="@drawable/icon_about"
android:title="@string/nav_about" />
</menu>
================================================
FILE: android/app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#ff000000</color>
<color name="white">#ffffff</color>
<color name="orange_400">#f56b39</color>
<color name="orange_600">#dc4d26</color>
<color name="orange_800">#9f2d00</color>
<color name="slate_400">#90a1b9</color>
<color name="slate_600">#45556c</color>
<color name="slate_800">#303030</color>
<color name="background_light">#ffffff</color>
<color name="background_dark">#303030</color>
</resources>
================================================
FILE: android/app/src/main/res/values/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#F55D24</color>
</resources>
================================================
FILE: android/app/src/main/res/values/ids.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="notification_id_vpn" type="id" />
</resources>
================================================
FILE: android/app/src/main/res/values/strings.xml
================================================
<resources>
<string name="app_name" translatable="false">Obscura VPN</string>
<string name="notification_channel_vpn_name" translatable="false">Obscura VPN</string>
<string name="notification_vpn_text">Status: %1$s</string>
<string name="notification_vpn_status_connecting">Connecting…</string>
<string name="notification_vpn_status_connected">Connected</string>
<string name="notification_vpn_status_disconnected">Disconnected</string>
<string name="nav_connection">Connection</string>
<string name="nav_location">Location</string>
<string name="nav_account">Account</string>
<string name="nav_settings">Settings</string>
<string name="nav_about">About</string>
</resources>
================================================
FILE: android/app/src/main/res/values/themes.xml
================================================
<resources>
<style name="Theme.ObscuraVPN" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/orange_600</item>
<item name="colorPrimaryVariant">@color/orange_800</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorSecondary">@color/slate_600</item>
<item name="colorSecondaryVariant">@color/slate_800</item>
<item name="colorOnSecondary">@color/black</item>
<item name="android:windowBackground">@color/background_light</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="android:windowLightStatusBar">true</item>
</style>
</resources>
================================================
FILE: android/app/src/main/res/values-night/themes.xml
================================================
<resources>
<style name="Theme.ObscuraVPN" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/orange_400</item>
<item name="colorPrimaryVariant">@color/orange_800</item>
<item name="colorOnPrimary">@color/black</item>
<item name="colorSecondary">@color/slate_400</item>
<item name="colorSecondaryVariant">@color/slate_400</item>
<item name="colorOnSecondary">@color/black</item>
<item name="android:windowBackground">@color/background_dark</item>
<item name="android:windowLightNavigationBar">false</item>
<item name="android:windowLightStatusBar">false</item>
</style>
</resources>
================================================
FILE: android/app/src/main/res/xml/backup_rules.xml
================================================
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
================================================
FILE: android/app/src/main/res/xml/data_extraction_rules.xml
================================================
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
================================================
FILE: android/app/src/main/res/xml/debug_archive_file_provider_paths.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="debug-archives" path="debug-archives/" />
</paths>
================================================
FILE: android/app/src/play/java/net/obscura/vpnclientapp/BillingFacade.kt
================================================
package net.obscura.vpnclientapp
import android.content.Context
import net.obscura.lib.billing.BillingManager
import net.obscura.vpnclientapp.activities.MainActivity
import net.obscura.vpnclientapp.client.errorCodePurchaseFailed
import net.obscura.vpnclientapp.client.errorCodePurchaseFailedAlreadyOwned
class BillingFacade(context: Context) {
private val billingManager = BillingManager(context)
suspend fun launchFlow(mainActivity: MainActivity) =
when (this@BillingFacade.billingManager.launchFlow(mainActivity)) {
BillingManager.PurchaseResult.Completed -> true
BillingManager.PurchaseResult.Canceled -> false
BillingManager.PurchaseResult.AlreadyOwned -> throw errorCodePurchaseFailedAlreadyOwned()
BillingManager.PurchaseResult.Failed -> throw errorCodePurchaseFailed()
}
fun destroy() = this.billingManager.destroy()
}
================================================
FILE: android/build.gradle.kts
================================================
import com.ncorti.ktfmt.gradle.KtfmtExtension
import com.ncorti.ktfmt.gradle.TrailingCommaManagementStrategy
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
plugins {
// Only declare a plugin here if it must be loaded once rather than per-subproject
// https://discuss.gradle.org/t/why-duplicate-plugins-in-top-level-build-scripts/49087/2
// https://www.reddit.com/r/androiddev/comments/1errttm/comment/li1vm93/
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.hilt.android) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.ksp) apply false
// These are only here for the `subprojects` block to work
alias(libs.plugins.detekt) apply false
alias(libs.plugins.ktfmt) apply false
}
subprojects {
apply(plugin = rootProject.libs.plugins.detekt.get().pluginId)
apply(plugin = rootProject.libs.plugins.ktfmt.get().pluginId)
// https://detekt.dev/docs/gettingstarted/gradle/#kotlin-dsl-3
extensions.configure<DetektExtension> {
config.setFrom(rootProject.file("detekt.yml"))
parallel = true
}
extensions.configure<KtfmtExtension> {
blockIndent.set(4)
continuationIndent.set(4)
maxWidth.set(120)
removeUnusedImports.set(true)
trailingCommaManagementStrategy.set(TrailingCommaManagementStrategy.ONLY_ADD)
}
}
================================================
FILE: android/buildSrc/.gitignore
================================================
/build
================================================
FILE: android/buildSrc/build.gradle.kts
================================================
import com.ncorti.ktfmt.gradle.KtfmtExtension
import com.ncorti.ktfmt.gradle.TrailingCommaManagementStrategy
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.kotlin.dsl.configure
plugins {
alias(libs.plugins.detekt)
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.ktfmt)
}
dependencies {
implementation(gradleKotlinDsl())
implementation(libs.kotlinx.serialization.json)
}
extensions.configure<DetektExtension> {
config.setFrom(rootProject.file("../detekt.yml"))
parallel = true
}
extensions.configure<KtfmtExtension> {
kotlinLangStyle()
maxWidth.set(120)
removeUnusedImports.set(true)
trailingCommaManagementStrategy.set(TrailingCommaManagementStrategy.ONLY_ADD)
}
================================================
FILE: android/buildSrc/settings.gradle.kts
================================================
dependencyResolutionManagement {
@Suppress("UnstableApiUsage")
repositories {
google()
mavenCentral()
}
@Suppress("UnstableApiUsage") repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } }
}
================================================
FILE: android/buildSrc/src/main/kotlin/VersionName.kt
================================================
import java.io.File
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.slf4j.Logger
import org.slf4j.LoggerFactory
private val logger: Logger = LoggerFactory.getLogger("version-name")
fun Project.getVersionName(projectRootDir: File): String =
providers.of(VersionName::class.java) { it.parameters.projectRootDir.set(projectRootDir) }.get()
abstract class VersionName : ValueSource<String, VersionName.Parameters> {
interface Parameters : ValueSourceParameters {
val projectRootDir: Property<File>
}
@Serializable private data class Tag(val version: String)
private val json: Json = Json { ignoreUnknownKeys = true }
private fun fallback(): String {
logger.warn("building outside of nix; not intended for distribution")
val tagString = File(parameters.projectRootDir.get().parentFile, "tag.json").readText()
val tag = this.json.decodeFromString<Tag>(tagString)
return "v${tag.version}.1-dev"
}
override fun obtain(): String {
val version = System.getenv("OBSCURA_VERSION")
logger.info("OBSCURA_VERSION = $version")
return version ?: this.fallback()
}
}
================================================
FILE: android/detekt.yml
================================================
build:
maxIssues: 0
excludeCorrectable: false
weights:
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1
config:
validation: true
warningsAsErrors: false
checkExhaustiveness: false
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
excludes: ""
processors:
active: true
exclude:
- "DetektProgressListener"
# - 'KtFileCountProcessor'
# - 'PackageCountProcessor'
# - 'ClassCountProcessor'
# - 'FunctionCountProcessor'
# - 'PropertyCountProcessor'
# - 'ProjectComplexityProcessor'
# - 'ProjectCognitiveComplexityProcessor'
# - 'ProjectLLOCProcessor'
# - 'ProjectCLOCProcessor'
# - 'ProjectLOCProcessor'
# - 'ProjectSLOCProcessor'
# - 'LicenseHeaderLoaderExtension'
console-reports:
active: true
exclude:
- "ProjectStatisticsReport"
- "ComplexityReport"
- "NotificationReport"
- "FindingsReport"
- "FileBasedFindingsReport"
# - 'LiteFindingsReport'
output-reports:
active: true
exclude:
# - 'TxtOutputReport'
# - 'XmlOutputReport'
# - 'HtmlOutputReport'
# - 'MdOutputReport'
# - 'SarifOutputReport'
comments:
active: true
AbsentOrWrongFileLicense:
active: false
licenseTemplateFile: "license.template"
licenseTemplateIsRegex: false
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
DeprecatedBlockTag:
active: false
EndOfSentenceFormat:
active: false
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
KDocReferencesNonPublicProperty:
active: false
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
OutdatedDocumentation:
active: false
matchTypeParameters: true
matchDeclarationsOrder: true
allowParamOnConstructorProperties: false
UndocumentedPublicClass:
active: false
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
searchInNestedClass: true
searchInInnerClass: true
searchInInnerObject: true
searchInInnerInterface: true
searchInProtectedClass: false
ignoreDefaultCompanionObject: false
UndocumentedPublicFunction:
active: false
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
searchProtectedFunction: false
UndocumentedPublicProperty:
active: false
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
searchProtectedProperty: false
complexity:
active: true
CognitiveComplexMethod:
active: false
threshold: 15
ComplexCondition:
active: true
threshold: 4
ComplexInterface:
active: false
threshold: 10
includeStaticDeclarations: false
includePrivateDeclarations: false
ignoreOverloaded: false
CyclomaticComplexMethod:
active: false
threshold: 15
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false
nestingFunctions:
- "also"
- "apply"
- "forEach"
- "isNotNull"
- "ifNull"
- "let"
- "run"
- "use"
- "with"
LabeledExpression:
active: false
ignoredLabels: []
LargeClass:
active: true
threshold: 600
LongMethod:
active: true
threshold: 60
LongParameterList:
active: true
functionThreshold: 6
constructorThreshold: 7
ignoreDefaultParameters: false
ignoreDataClasses: true
ignoreAnnotatedParameter: []
MethodOverloading:
active: false
threshold: 6
NamedArguments:
active: false
threshold: 3
ignoreArgumentsMatchingNames: false
NestedBlockDepth:
active: true
threshold: 4
NestedScopeFunctions:
active: false
threshold: 1
functions:
- "kotlin.apply"
- "kotlin.run"
- "kotlin.with"
- "kotlin.let"
- "kotlin.also"
ReplaceSafeCallChainWithRun:
active: false
StringLiteralDuplication:
active: false
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
threshold: 3
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: "$^"
TooManyFunctions:
active: false
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
thresholdInFiles: 11
thresholdInClasses: 11
thresholdInInterfaces: 11
thresholdInObjects: 11
thresholdInEnums: 11
ignoreDeprecated: false
ignorePrivate: false
ignoreOverridden: false
ignoreAnnotatedFunctions: []
coroutines:
active: true
GlobalCoroutineUsage:
active: false
InjectDispatcher:
active: true
dispatcherNames:
- "IO"
- "Default"
- "Unconfined"
RedundantSuspendModifier:
active: true
SleepInsteadOfDelay:
active: true
SuspendFunSwallowedCancellation:
active: false
SuspendFunWithCoroutineScopeReceiver:
active: false
SuspendFunWithFlowReturnType:
active: true
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: "_|(ignore|expected).*"
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
active: true
EmptyDoWhileBlock:
active: true
EmptyElseBlock:
active: true
EmptyFinallyBlock:
active: true
EmptyForBlock:
active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: false
EmptyIfBlock:
active: true
EmptyInitBlock:
active: true
EmptyKtFile:
active: true
EmptySecondaryConstructor:
active: true
EmptyTryBlock:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
active: true
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
methodNames:
- "equals"
- "finalize"
- "hashCode"
- "toString"
InstanceOfCheckForException:
active: true
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
NotImplementedDeclaration:
active: false
ObjectExtendsThrowable:
active: false
PrintStackTrace:
active: true
RethrowCaughtException:
active: true
ReturnFromFinally:
active: true
ignoreLabeled: false
SwallowedException:
active: true
ignoredExceptionTypes:
- "InterruptedException"
- "MalformedURLException"
- "NumberFormatException"
- "ParseException"
allowedExceptionNameRegex: "_|(ignore|expected).*"
ThrowingExceptionFromFinally:
active: true
ThrowingExceptionInMain:
active: false
ThrowingExceptionsWithoutMessageOrCause:
active: true
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
exceptions:
- "ArrayIndexOutOfBoundsException"
- "Exception"
- "IllegalArgumentException"
- "IllegalMonitorStateException"
- "IllegalStateException"
- "IndexOutOfBoundsException"
- "NullPointerException"
- "RuntimeException"
- "Throwable"
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
active: false
TooGenericExceptionThrown:
active: false
exceptionNames:
- "Error"
- "Exception"
- "RuntimeException"
- "Throwable"
naming:
active: true
BooleanPropertyNaming:
active: false
allowedPattern: "^(is|has|are)"
ClassNaming:
active: true
classPattern: "[A-Z][a-zA-Z0-9]*"
ConstructorParameterNaming:
active: true
parameterPattern: "[a-z][A-Za-z0-9]*"
privateParameterPattern: "[a-z][A-Za-z0-9]*"
excludeClassPattern: "$^"
EnumNaming:
active: true
enumEntryPattern: "[A-Z][_a-zA-Z0-9]*"
ForbiddenClassName:
active: false
forbiddenName: []
FunctionMaxLength:
active: false
maximumFunctionNameLength: 30
FunctionMinLength:
active: false
minimumFunctionNameLength: 3
FunctionNaming:
active: true
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
functionPattern: "[a-z][a-zA-Z0-9]*"
excludeClassPattern: "$^"
FunctionParameterNaming:
active: true
parameterPattern: "[a-z][A-Za-z0-9]*"
excludeClassPattern: "$^"
InvalidPackageDeclaration:
active: true
rootPackage: ""
requireRootInDeclaration: false
LambdaParameterNaming:
active: false
parameterPattern: "[a-z][A-Za-z0-9]*|_"
MatchingDeclarationName:
active: true
mustBeFirst: true
multiplatformTargets:
- "ios"
- "android"
- "js"
- "jvm"
- "native"
- "iosArm64"
- "iosX64"
- "macosX64"
- "mingwX64"
- "linuxX64"
MemberNameEqualsClassName:
active: true
ignoreOverridden: true
NoNameShadowing:
active: true
NonBooleanPropertyPrefixedWithIs:
active: false
ObjectPropertyNaming:
active: true
constantPattern: "[A-Za-z][_A-Za-z0-9]*"
propertyPattern: "[A-Za-z][_A-Za-z0-9]*"
privatePropertyPattern: "(_)?[A-Za-z][_A-Za-z0-9]*"
PackageNaming:
active: true
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
constantPattern: "[A-Z][_A-Z0-9]*"
propertyPattern: "[A-Za-z][_A-Za-z0-9]*"
privatePropertyPattern: "_?[A-Za-z][_A-Za-z0-9]*"
VariableMaxLength:
active: false
maximumVariableNameLength: 64
VariableMinLength:
active: false
minimumVariableNameLength: 1
VariableNaming:
active: true
variablePattern: "[a-z][A-Za-z0-9]*"
privateVariablePattern: "(_)?[a-z][A-Za-z0-9]*"
excludeClassPattern: "$^"
performance:
active: true
ArrayPrimitive:
active: true
CouldBeSequence:
active: false
threshold: 3
ForEachOnRange:
active: true
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
SpreadOperator:
active: true
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
UnnecessaryPartOfBinaryExpression:
active: false
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
AvoidReferentialEquality:
active: true
forbiddenTypePatterns:
- "kotlin.String"
CastNullableToNonNullableType:
active: false
CastToNullableType:
active: false
Deprecation:
active: false
DontDowncastCollectionTypes:
active: false
DoubleMutabilityForCollection:
active: true
mutableTypes:
- "kotlin.collections.MutableList"
- "kotlin.collections.MutableMap"
- "kotlin.collections.MutableSet"
- "java.util.ArrayList"
- "java.util.LinkedHashSet"
- "java.util.HashSet"
- "java.util.LinkedHashMap"
- "java.util.HashMap"
ElseCaseInsteadOfExhaustiveWhen:
active: false
ignoredSubjectTypes: []
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
ExitOutsideMain:
active: false
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
active: true
IgnoredReturnValue:
active: true
restrictToConfig: true
returnValueAnnotations:
- "CheckResult"
- "*.CheckResult"
- "CheckReturnValue"
- "*.CheckReturnValue"
ignoreReturnValueAnnotations:
- "CanIgnoreReturnValue"
- "*.CanIgnoreReturnValue"
returnValueTypes:
- "kotlin.sequences.Sequence"
- "kotlinx.coroutines.flow.*Flow"
- "java.util.stream.*Stream"
ignoreFunctionCall: []
ImplicitDefaultLocale:
active: true
ImplicitUnitReturnType:
active: false
allowExplicitReturnType: true
InvalidRange:
active: true
IteratorHasNextCallsNextMethod:
active: true
IteratorNotThrowingNoSuchElementException:
active: true
LateinitUsage:
active: false
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
ignoreOnClassesPattern: ""
MapGetWithNotNullAssertionOperator:
active: true
MissingPackageDeclaration:
active: false
excludes: ["**/*.kts"]
NullCheckOnMutableProperty:
active: false
NullableToStringCall:
active: false
PropertyUsedBeforeDeclaration:
active: false
UnconditionalJumpStatementInLoop:
active: false
UnnecessaryNotNullCheck:
active: false
UnnecessaryNotNullOperator:
active: true
UnnecessarySafeCall:
active: true
UnreachableCatchBlock:
active: true
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: true
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
]
UnsafeCast:
active: true
UnusedUnaryOperator:
active: true
UselessPostfixExpression:
active: true
WrongEqualsTypeParameter:
active: true
style:
active: true
AlsoCouldBeApply:
active: false
BracesOnIfStatements:
active: false
singleLine: "never"
multiLine: "always"
BracesOnWhenStatements:
active: false
singleLine: "necessary"
multiLine: "consistent"
CanBeNonNullable:
active: false
CascadingCallWrapping:
active: false
includeElvis: true
ClassOrdering:
active: false
CollapsibleIfStatements:
active: false
DataClassContainsFunctions:
active: false
conversionFunctionPrefix:
- "to"
allowOperators: false
DataClassShouldBeImmutable:
active: false
DestructuringDeclarationWithTooManyEntries:
active: true
maxDestructuringEntries: 3
DoubleNegativeLambda:
active: false
negativeFunctions:
- reason: "Use `takeIf` instead."
value: "takeUnless"
- reason: "Use `all` instead."
value: "none"
negativeFunctionNameParts:
- "not"
- "non"
EqualsNullCall:
active: true
EqualsOnSignatureLine:
active: false
ExplicitCollectionElementAccessMethod:
active: false
ExplicitItLambdaParameter:
active: true
ExpressionBodySyntax:
active: false
includeLineWrapping: false
ForbiddenAnnotation:
active: false
annotations:
- reason: "it is a java annotation. Use `Suppress` instead."
value: "java.lang.SuppressWarnings"
- reason: "it is a java annotation. Use `kotlin.Deprecated` instead."
value: "java.lang.Deprecated"
- reason: "it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead."
value: "java.lang.annotation.Documented"
- reason: "it is a java annotation. Use `kotlin.annotation.Target` instead."
value: "java.lang.annotation.Target"
- reason: "it is a java annotation. Use `kotlin.annotation.Retention` instead."
value: "java.lang.annotation.Retention"
- reason: "it is a java annotation. Use `kotlin.annotation.Repeatable` instead."
value: "java.lang.annotation.Repeatable"
- reason: "Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265"
value: "java.lang.annotation.Inherited"
ForbiddenComment:
active: false
comments:
- reason: "Forbidden FIXME todo marker in comment, please fix the problem."
value: "FIXME:"
- reason: "Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code."
value: "STOPSHIP:"
- reason: "Forbidden TODO todo marker in comment, please do the changes."
value: "TODO:"
allowedPatterns: ""
ForbiddenImport:
active: false
imports: []
forbiddenPatterns: ""
ForbiddenMethodCall:
active: false
methods:
- reason: "print does not allow you to configure the output stream. Use a logger instead."
value: "kotlin.io.print"
- reason: "println does not allow you to configure the output stream. Use a logger instead."
value: "kotlin.io.println"
ForbiddenSuppress:
active: false
rules: []
ForbiddenVoid:
active: true
ignoreOverridden: false
ignoreUsageInGenerics: false
FunctionOnlyReturningConstant:
active: true
ignoreOverridableFunction: true
ignoreActualFunction: true
excludedFunctions: []
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 1
MagicNumber:
active: false
excludes:
[
"**/test/**",
"**/androidTest/**",
"**/commonTest/**",
"**/jvmTest/**",
"**/androidUnitTest/**",
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
"**/*.kts",
]
ignoreNumbers:
- "-1"
- "0"
- "1"
- "2"
ignoreHashCodeFunction: true
ignorePropertyDeclaration: false
ignoreLocalVariableDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
ignoreRanges: false
ignoreExtensionFunctions: true
MandatoryBracesLoops:
active: false
MaxChainedCallsOnSameLine:
active: false
maxChainedCalls: 5
MaxLineLength:
active: true
maxLineLength: 120
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
excludeRawStrings: true
MayBeConst:
active: true
ModifierOrder:
active: true
MultilineLambdaItParameter:
active: false
MultilineRawStringIndentation:
active: false
indentSize: 4
trimmingMethods:
- "trimIndent"
- "trimMargin"
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
NoTabs:
active: false
NullableBooleanCheck:
active: false
ObjectLiteralToLambda:
active: true
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:
active: true
RedundantExplicitType:
active: false
RedundantHigherOrderMapUsage:
active: true
RedundantVisibilityModifierRule:
active: false
ReturnCount:
active: false
max: 2
excludedFunctions:
- "equals"
excludeLabeled: false
excludeReturnFromLambda: true
excludeGuardClauses: false
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: true
SpacingBetweenPackageAndImports:
active: false
StringShouldBeRawString:
active: false
maxEscapedCharacterCount: 2
ignoredCharacters: []
ThrowsCount:
active: true
max: 2
excludeGuardClauses: false
TrailingWhitespace:
active: false
TrimMultilineRawString:
active: false
trimmingMethods:
- "trimIndent"
- "trimMargin"
UnderscoresInNumericLiterals:
active: false
acceptableLength: 4
allowNonStandardGrouping: false
UnnecessaryAbstractClass:
active: true
UnnecessaryAnnotationUseSiteTarget:
active: false
UnnecessaryApply:
active: true
UnnecessaryBackticks:
active: false
UnnecessaryBracesAroundTrailingLambda:
active: false
UnnecessaryFilter:
active: true
UnnecessaryInheritance:
active: true
UnnecessaryInnerClass:
active: false
UnnecessaryLet:
active: false
UnnecessaryParentheses:
active: false
allowForUnclearPrecedence: false
UntilInsteadOfRangeTo:
active: false
UnusedImports:
active: false
UnusedParameter:
active: true
allowedNames: "ignored|expected"
UnusedPrivateClass:
active: true
UnusedPrivateMember:
active: true
allowedNames: ""
UnusedPrivateProperty:
active: true
allowedNames: "_|ignored|expected|serialVersionUID"
UseAnyOrNoneInsteadOfFind:
active: true
UseArrayLiteralsInAnnotations:
active: true
UseCheckNotNull:
active: true
UseCheckOrError:
active: true
UseDataClass:
active: false
allowVars: false
UseEmptyCounterpart:
active: false
UseIfEmptyOrIfBlank:
active: false
UseIfInsteadOfWhen:
active: false
ignoreWhenContainingVariableDeclaration: false
UseIsNullOrEmpty:
active: true
UseLet:
active: false
UseOrEmpty:
active: true
UseRequire:
active: true
UseRequireNotNull:
active: true
UseSumOfInsteadOfFlatMapSize:
active: false
UselessCallOnNotNull:
active: true
UtilityClassWithPublicConstructor:
active: true
VarCouldBeVal:
active: true
ignoreLateinitVar: false
WildcardImport:
active: true
excludeImports:
- "java.util.*"
================================================
FILE: android/gradle/libs.versions.toml
================================================
[versions]
android-billingclient = "8.3.0"
android-gradle-plugin = "8.13.0"
androidx-junit = "1.3.0"
androidx-lifecycle = "2.10.0"
appcompat = "1.7.1"
core-ktx = "1.18.0"
#noinspection NewerVersionAvailable
dagger = "2.58" # Later versions require AGP 9+
detekt = "1.23.8"
espresso-core = "3.7.0"
junit = "4.13.2"
kotlin = "2.3.10"
kotlinx-serialization-json = "1.10.0"
ksp = "2.3.6"
ktfmt = "0.25.0"
material = "1.13.0"
play-services-location = "21.3.0"
webkit = "1.15.0"
[libraries]
android-billingclient = { module = "com.android.billingclient:billing-ktx", version.ref = "android-billingclient" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-junit" }
androidx-lifecycle = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "dagger" }
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "dagger" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "play-services-location" }
[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "dagger" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" }
================================================
FILE: android/gradle/mitm-cache/deps.json
================================================
{
"!comment": "This is a nixpkgs Gradle dependency lockfile. For more details, refer to the Gradle section in the nixpkgs manual.",
"!version": 1,
"https://dl.google.com/dl/android/maven2": {
"androidx/activity#activity/1.2.3": {
"module": "sha256-6eLdnZmtrzQzaRh89qPwOA/50lRJRM38RzEO+86xs1E=",
"pom": "sha256-vK3ckl1R1VZsRaSmzzvwXHFEpjSHEwjQ5z0+JwOtdOA="
},
"androidx/activity#activity/1.8.0": {
"module": "sha256-0UXYtTz9Ef0m5H591FwAcTPvluok9nFctlPHN2RdHfY=",
"pom": "sha256-ZRMShfU8sELo/9GJ5wXa/JuK5tYkNeh3ismrO44/dW8="
},
"androidx/activity/activity/1.2.3/activity-1.2.3": {
"aar": "sha256-Hc4HBcM0prLvAzgkGNx1hvTlfuI4FyZ7QD6oz8Nsgk4="
},
"androidx/activity/activity/1.8.0/activity-1.8.0": {
"aar": "sha256-06Z2cJ3qBPKoUG4q6FBS//dj21Jqx/FrBN5Q/dBbByA="
},
"androidx/annotation#annotation-experimental/1.3.0": {
"module": "sha256-Xuvq/wHQQuBtzykqv4lkrTkeSwFZ8AkPFiU9YEXTjaA=",
"pom": "sha256-ui/kb3T2iFHiNCRhk6uJbAwB+glM03LBBRtr8E15sLs="
},
"androidx/annotation#annotation-experimental/1.3.1": {
"module": "sha256-m2l0p9/ibTwgndY+FvjuJGG1egkXiRYMoetJK7G/P4Q=",
"pom": "sha256-Qm+UMQtHvfAE3CA42uqpQ8XOT0sGPob3i8U24jLj3fw="
},
"androidx/annotation#annotation-experimental/1.4.1": {
"module": "sha256-KsL3EG4S8mNCW0pN/ICYlEf7iVZ1/pAthnWap0/RK30=",
"pom": "sha256-/leyKEF/TXxneQPcYftKfPmT1gNJneJtjYET5HfMTxs="
},
"androidx/annotation#annotation-jvm/1.8.1": {
"module": "sha256-yVnjsM3HXBXv4BYF+laqefAz45I44VBji4+r3mqhIaA=",
"pom": "sha256-1JIDczqm+uBGw6PeTnlu7TR1lXVUhqZCc5iYRHWXULQ="
},
"androidx/annotation#annotation-jvm/1.9.1": {
"jar": "sha256-HjQ5F+vye6lv5NxSscrX/TK3OPvGNVu2zVs7MF1yEtA=",
"module": "sha256-A/tlkXfIYY5HQlklwRvJHzhHA+omwmW+myXNeSkrURw=",
"pom": "sha256-ibmcIY1gAZMWtQqreYFnB7whaWyJagMOGxrgOJYby44="
},
"androidx/annotation#annotation/1.1.0": {
"pom": "sha256-LpNyuneA70SVKtv4a2bh8IaCweUnfJJhhfZWShN5nv4="
},
"androidx/annotation#annotation/1.3.0": {
"jar": "sha256-l9xFr+/joeQh2kK4tun5BJFHfEX8YXggPjpeigXuhVM=",
"module": "sha256-lRbCrkQoTqC9PQ6t4O5jiHm3CMvjHjr5K6lsMAYE68M=",
"pom": "sha256-BJUXxSVqS7yFKfzckMTqrzWBgKwgXJFmB2ffJkZe+7A="
},
"androidx/annotation#annotation/1.8.1": {
"module": "sha256-5jhuha/dhlBE4hZXXkk+05pjpjJb2SU3miFCnDlByLU=",
"pom": "sha256-txIll07Ah+uWwl72gZ9VscIvUw6FykRrpzX7Zu0E/1w="
},
"androidx/annotation#annotation/1.9.1": {
"module": "sha256-8gSwW3KKl1YXGLxxYkLkfGKcAIWoDudPylPU1ji8vj8=",
"pom": "sha256-xzOIHC4X1ffIZhzAKpFZyxYLeyCUon1ZORbIfT4lBjY="
},
"androidx/annotation/annotation-experimental/1.4.1/annotation-experimental-1.4.1": {
"aar": "sha256-a9THx0dvgmDNO9u4EYNYPpP8n3kMJ96n3DFBgcv4eqA="
},
"androidx/appcompat#appcompat-resources/1.7.1": {
"module": "sha256-FjzF4PJJQz3bABb70BGrjeB88j4H5hjHRkRojibNGOg=",
"pom": "sha256-L6+SLow9cG8p2GKz+7OqE0dwXCHG6uGFhtK7H3Nm6gE="
},
"androidx/appcompat#appcompat/1.7.1": {
"module": "sha256-i78WeSyBzC3Qg2gXg6U4L1cwB+7oFXGCky8bRU4rfwQ=",
"pom": "sha256-H3mRDMIN4Zis84Z7HpP57VndtkNYWy6cKO5VUYrxvAA="
},
"androidx/appcompat/appcompat-resources/1.7.1/appcompat-resources-1.7.1": {
"aar": "sha256-ji2zEiTKU7EIx4TaKzYZWQYnFtQWshDP7z1aOCgwbfA="
},
"androidx/appcompat/appcompat/1.7.1/appcompat-1.7.1": {
"aar": "sha256-KtM0oyOygEbom3OMd9GEyz3MoypVGrBIhRsv2iOjuiY="
},
"androidx/arch/core#core-common/2.1.0": {
"jar": "sha256-/hI3vwKdBj5/Kf45rq9z73TIsKNlhIb8KdPFQyZlOIk=",
"pom": "sha256-g7uzlg6qvGAKw2bJTLWUFORBUyodaqk4iwuL/6zlzwE="
},
"androidx/arch/core#core-common/2.2.0": {
"jar": "sha256-ZTCKBrHADuGGy54ZMhOD8EO5k4E/FSLEf0o+MwO9ukE=",
"module": "sha256-7fQgDP3C2UYjIlLJnl3LnGG7kJ61RQsmE9HU/cl0uYE=",
"pom": "sha256-HhfUr41kJb4qafivTWVKh+BFYlmp7vFUKGm8sCNUfig="
},
"androidx/arch/core#core-runtime/2.1.0": {
"pom": "sha256-wMTtAWDNLKGDkAFd6LOStpfBczJ8aywJR9TmL2lYwF0="
},
"androidx/arch/core#core-runtime/2.2.0": {
"module": "sha256-qLF1E5SeXbbJYBwwvhnflTdi3Yd1EvHiz8+ugdJECUQ=",
"pom": "sha256-D5U3BlmBQNnYc1zdWusfo1kz9OLzHAvz64GgU6TIwaU="
},
"androidx/arch/core/core-runtime/2.1.0/core-runtime-2.1.0": {
"aar": "sha256-3XdhW9PdJ1r7EbYt8luuRrELShF803lDr0W9y/h1WFI="
},
"androidx/arch/core/core-runtime/2.2.0/core-runtime-2.2.0": {
"aar": "sha256-ob5eDKorB2I4Yq9q4hs6sHGBIyRRhNDjDeqBtT+ZCkc="
},
"androidx/cardview#cardview/1.0.0": {
"pom": "sha256-5k704ItYNY/ie1meb+gKGxU9sBTGRL7uYwqycQYcPmw="
},
"androidx/cardview/cardview/1.0.0/cardview-1.0.0": {
"aar": "sha256-EZPATCKj1rWUba6fToxZ1q3eanG2vV2H+5nYLdoa/sc="
},
"androidx/collection#collection-jvm/1.4.2": {
"jar": "sha256-mEzpvXgAVer7ihBayj9RRoa/exticux8ZFqYzkD8fbQ=",
"module": "sha256-qtazU2wPDlcKpzPVFB1w+mubOt03D3OjEcpMpd7iVEg=",
"pom": "sha256-rbs3nisWDwjmPjQa0Eu8xwM/bYaOxRrBHkM0ZCpOOe8="
},
"androidx/collection#collection/1.0.0": {
"pom": "sha256-p5E6UnWtaOVV0mEuvowUw2exU+FMpIoYcqZImQIOVO8="
},
"androidx/collection#collection/1.1.0": {
"jar": "sha256-YyoOVAdGHed0QJNSlA4pKikQN3JCB6eHggx32vfTO3I=",
"pom": "sha256-Z+kGbKSs/cbjzFCCk8MboDmAV/8Rjk9wseGBPJo0VtE="
},
"androidx/collection#collection/1.4.2": {
"module": "sha256-AybSz1rb5ZIxKBDKH3HGwMww91PEPwfHQCNht4ineEw=",
"pom": "sha256-TyYVoXrqz+EraCmCFKGfKhnGjGbVkAdb27+2WevOK38="
},
"androidx/concurrent#concurrent-futures-ktx/1.2.0": {
"jar": "sha256-4fPhe7Q1jM1sd8pF9wY1yauiNyYfGeqk9koCGMAOKj4=",
"module": "sha256-gj9Gms2YSt/TCz0KV36093lqA3QqUm73DBWDtZS0O4A=",
"pom": "sha256-eGnTmqendUpPMRdxq/2dlcy9f/sn6XD4yxX99bU1W20="
},
"androidx/concurrent#concurrent-futures/1.0.0": {
"pom": "sha256-RQW5peMKlBi1mprWcCw+QZOupuaRo9A88iDHZArQg+I="
},
"androidx/concurrent#concurrent-futures/1.1.0": {
"jar": "sha256-DOBnxRSg0QSdG+vfcJ40TtMmb+l0QnVoKTfNyxMzTp4=",
"module": "sha256-d2OaCwUeIlELrZOv/OoOvXge8SS/m3YhqVdJk3vPzf0=",
"pom": "sha256-dIo0610T0Z0Yet7K6oJmf97V8bC5imVeE+0uSos9iuY="
},
"androidx/constraintlayout#constraintlayout-core/1.0.0": {
"jar": "sha256-SWyMeWZL2XDGpr56lgog3RV9/Fqi8Y5bgzN2f/vFmRE=",
"module": "sha256-Ge5Jb2b8BVlilFTdAdNJ89PJWVmAs3No/NkfFH7WBNs=",
"pom": "sha256-k+AWIukLWHm7lQ/ttcQXyAJFVf3HeUgbzuIaO1l3y4w="
},
"androidx/constraintlayout#constraintlayout/2.1.0": {
"module": "sha256-FBrMbPAzDdTjYwcolcSyaMC1lfhNck4ukE685fK3Lkg=",
"pom": "sha256-JRBK9a6R7+X+bJF47uNfawLjE0lQVzjM603y34OFtYU="
},
"androidx/constraintlayout/constraintlayout/2.1.0/constraintlayout-2.1.0": {
"aar": "sha256-pFinViNjvVnCCafeqEcyag4rJrN6iD3D4pBlp5oceIs="
},
"androidx/coordinatorlayout#coordinatorlayout/1.1.0": {
"pom": "sha256-pnxSyd36/y/7L9S5fNlPo4LoN+qKWHTQKeCgT6Y+XK8="
},
"androidx/coordinatorlayout/coordinatorlayout/1.1.0/coordinatorlayout-1.1.0": {
"aar": "sha256-RKnjCr9WrxAlxSoK9Qb+6cQTGqVe/aUvn9lFEhHF6Ms="
},
"androidx/core#core-ktx/1.18.0": {
"module": "sha256-P6V5wR3g9vN4ACwPC7S75xEAY0+A+LKClReJ6n0PuEk=",
"pom": "sha256-Ol4ACeBlF2J3VrkgPZqPBV/ah7IVoXt3W65WhpeFQZY="
},
"androidx/core#core-viewtree/1.0.0": {
"module": "sha256-EThs+kbLv922pAWfFDVMAGkc9l09Y8NhiBioMybvPH8=",
"pom": "sha256-1PLtEXb6jFYSuA90yVKoeZFCqe02AioaI4/eWxQFgNk="
},
"androidx/core#core/1.13.0": {
"module": "sha256-Lg5uXBIFt0YqDFw+MrWMLlJUNYEu2JlGx75nN0k7UeM=",
"pom": "sha256-RQLk7YtZEiAhrJocExLiMm5LD0P37Lu8m1Dud0KVdNQ="
},
"androidx/core#core/1.18.0": {
"module": "sha256-tn/3ofm+lWh9+obWBVFH/B2LvjsdwZBIxDKvyhir/ZU=",
"pom": "sha256-V9ZE67N9MJrZWddAFX2OhCTic7nGcoQuT0x1jtxtMOc="
},
"androidx/core#core/1.2.0": {
"pom": "sha256-PR9ON7d92SNTh5oECrTOL3BnmLy98GYUdJHDZCs/eaY="
},
"androidx/core/core-ktx/1.18.0/core-ktx-1.18.0": {
"aar": "sha256-x8UR49+Dj8vLYazjaUTea7fBDrWXvlRpdPABEZk5yF0="
},
"androidx/core/core-viewtree/1.0.0/core-viewtree-1.0.0": {
"aar": "sha256-3BtnjVjrzyv6FYe+aP+CZpTOPSISUbnvMNTUs2KX5t4="
},
"androidx/core/core/1.18.0/core-1.18.0": {
"aar": "sha256-MR2DrGfTlAduwh0S7S0QpEtZyykpt9zgDlqQqThC430="
},
"androidx/core/core/1.2.0/core-1.2.0": {
"aar": "sha256-UkuLiM62p0p+ROa1Z6E1Zg8hF5mQTLIYv+5b4RZoILI="
},
"androidx/cursoradapter#cursoradapter/1.0.0": {
"pom": "sha256-YtlciYUK8hAwsZ8U1ffs1ti8yaMBTFkALsmWJMqsgQA="
},
"androidx/cursoradapter/cursoradapter/1.0.0/cursoradapter-1.0.0": {
"aar": "sha256-qByP54gV+kffW3Sd61JyetEfk5faWLFgF/TrLBHihWQ="
},
"androidx/customview#customview/1.0.0": {
"pom": "sha256-zp5HuHGE9b1eE56b7NWyZHbULXjDG/L97cN6y0G5rUk="
},
"androidx/customview#customview/1.1.0": {
"pom": "sha256-yBTUNfc+nm0WmIbQ65a1xTYf60hEn7uzFckIwDxYjJQ="
},
"androidx/customview/customview/1.0.0/customview-1.0.0": {
"aar": "sha256-IOW49lJqNFlaYE9WcY2oEWfAtAp6lKV9qjVWY/JZTfI="
},
"androidx/customview/customview/1.1.0/customview-1.1.0": {
"aar": "sha256-AfdqsEN3CpewVARvmBVxe4LOA1XAKWfRbGGYE1ncGJo="
},
"androidx/databinding#databinding-common/8.13.0": {
"jar": "sha256-Zsq4JjnawPbCQzRkwJOwdNYIxLuIfsOKm4vErJgSZzI=",
"pom": "sha256-C8GPkSrwK3kO+jmsVu2w5b/Inq/gmXaApazWXJinGU4="
},
"androidx/databinding#databinding-compiler-common/8.13.0": {
"jar": "sha256-jpAGDUEdIEGfnvXOaYk8/EoyP2ETQDB4nAuy2xo99Hw=",
"pom": "sha256-JFCytoqObW96rQCoVlRbPN+sSEprHwr2IWhATDIybTQ="
},
"androidx/drawerlayout#drawerlayout/1.0.0": {
"pom": "sha256-2mczQlqD9c6FCHj6cgEII0X+18Zo3VhVD90ZwDlsb6Q="
},
"androidx/drawerlayout#drawerlayout/1.1.1": {
"pom": "sha256-wS+pA7pTAFlipyQF83iWYIcY9BZz/EpiMS5FNhMvb0U="
},
"androidx/drawerlayout/drawerlayout/1.1.1/drawerlayout-1.1.1": {
"aar": "sha256-LF8NyjeOt4yixEA/mInHfaowWTAiYPJqB/6fY8CJJv4="
},
"androidx/dynamicanimation#dynamicanimation/1.1.0": {
"module": "sha256-n19EJUtexrF7SWW4tkGFHSiI29R0xsqBqcr1EnGPAmI=",
"pom": "sha256-yuy3s3lYXY5AcOuvrjl0oTfGaJ3oB3+RZZNbNgV4SpM="
},
"androidx/dynamicanimation/dynamicanimation/1.1.0/dynamicanimation-1.1.0": {
"aar": "sha256-kGLMZjcTakmtF/hgPHJRkFL/mlaeYX/BtttwhjBNqI8="
},
"androidx/emoji2#emoji2-views-helper/1.2.0": {
"module": "sha256-o6nbWBq/F4ewH/FcQPBZUw6OZPOTfKoteI9C6zmJMmg=",
"pom": "sha256-21kFLonRO3lGSwS9H4dNeX8dl9g/STf2aaU+H2ioAjc="
},
"androidx/emoji2#emoji2-views-helper/1.3.0": {
"module": "sha256-CZdLte+XgN6dVnFdcRcaNcePsuF/2GV3OwyDo6ysA5w=",
"pom": "sha256-JnOw9YWdQ2Gk6aMEV4UqZ0XQw8hbXgQSBwSD85Y+9SY="
},
"androidx/emoji2#emoji2/1.3.0": {
"module": "sha256-3chR7bpl/RWnobw60YZI4vcy3VrY7zYCIkvOBkf1tNE=",
"pom": "sha256-FOu+qCNPATIRll/dCLQu6QfOAHWvMhAYyMKGXC5dsOs="
},
"androidx/emoji2/emoji2-views-helper/1.3.0/emoji2-views-helper-1.3.0": {
"aar": "sha256-mhNRKVpPc53w7+g0Stqpr7NIVsOvWE1KmvvsEFpFuQs="
},
"androidx/emoji2/emoji2/1.3.0/emoji2-1.3.0": {
"aar": "sha256-K/I4GLI6mW3aobX9W7MhKdr/a7stzhUWbi/M3SAQsaU="
},
"androidx/fragment#fragment/1.0.0": {
"pom": "sha256-4ynWczYelNLo9NTRTh8FhjaL1D+xnv1XZs50mLzM0WI="
},
"androidx/fragment#fragment/1.1.0": {
"pom": "sha256-73jrJ6wC3fNUXV+KOFfHOig3oBhT+NX893JRAR21JUQ="
},
"androidx/fragment#fragment/1.5.4": {
"module": "sha256-rzJggI3OtlMu/C1yFb5Fhywkppna2n13v/c4zjuFp/A=",
"pom": "sha256-+MoYd8ZuZCxVSQfhzlpJOol8opJwyxniu21CS6Q7bJg="
},
"androidx/fragment/fragment/1.1.0/fragment-1.1.0": {
"aar": "sha256-oUyLjyFT8SjoAPvSZqa+qxwoOYKinsVw0swF0wfYFJY="
},
"androidx/fragment/fragment/1.5.4/fragment-1.5.4": {
"aar": "sha256-vDwkMd2kLpS7lRHFh+rokNJ25Kr+OTqNp7ABaRhtr94="
},
"androidx/graphics#graphics-shapes-android/1.0.1": {
"module": "sha256-mlUIyvAtePzycpOU6X57J6Q3hic+F8y2W/jhsApZgyQ=",
"pom": "sha256-gd57zIVJCE0QTVrzOxIPk+ENRUJKFaRUz6HyQwSqTRE="
},
"androidx/graphics#graphics-shapes/1.0.1": {
"module": "sha256-LX9tVQQimfnE+EeKoJS8QJmjRpAnef8wkf7R38K2L1M=",
"pom": "sha256-drUM5mT6RKSxZUIIHgiJkc8xaubnI6pM0BpicZ8aiic="
},
"androidx/graphics/graphics-shapes-android/1.0.1/graphics-shapes-android-1.0.1": {
"aar": "sha256-cXEmmkEi/quwcXaN9G0Qs2Lkesdf0KqNvK+h9E9htAw="
},
"androidx/interpolator#interpolator/1.0.0": {
"pom": "sha256-DdwHzDlpn0js2eyJS1gwwPCeIugpWSlO3zchciTIi3s="
},
"androidx/interpolator/interpolator/1.0.0/interpolator-1.0.0": {
"aar": "sha256-MxkxNaZP4h+iw17sZojxp25RJgbA/IPcG2ieN63Xcyo="
},
"androidx/lifecycle#lifecycle-common-jvm/2.10.0": {
"jar": "sha256-FZQwgth7zXiDA5j6N38sixJkPeKQ0JBu2OSaLTNd21Q=",
"module": "sha256-k9kBazr9A2OaQH9RoRnVyk2umI3jdsOA8OUd2diOaG0=",
"pom": "sha256-ygYrmrsCmLDUCuEMLRygbUEp9fYO/cXoxExsKzN10Vs="
},
"androidx/lifecycle#lifecycle-common/2.10.0": {
"module": "sha256-XRJHse373J3e3L5SXJ0mKVZ7HFOMt6nGIM0EShJLXHM=",
"pom": "sha256-EAT03wURixPCqQmEa63GZkbtJ3lEcGpgTk3YKE1TVIs="
},
"androidx/lifecycle#lifecycle-common/2.3.1": {
"jar": "sha256-FYSPtW2zL0x83HKzJAAxg9UqSITWvwm+cIrH9YfRObU=",
"module": "sha256-X7fIUU2MVsraXinvidwCiecZQqtMsLLm3KE3udy4/dQ=",
"pom": "sha256-jNI9iJoUCVxs4WhA0psaY4j6XhFRRMEwnU1tRpwbw1E="
},
"androidx/lifecycle#lifecycle-common/2.5.1": {
"module": "sha256-fUvClhzVvTmeNiHUNPDEU91srfiR+RepRswpGr3ajxo=",
"pom": "sha256-gtIZLj3p/df0W5CI4nMV59vsAtCUYy+qajklFpDSjMA="
},
"androidx/lifecycle#lifecycle-common/2.6.1": {
"module": "sha256-k3R6kUXLNrxxAF9Zjt4y4rEUmt5aFuYrDklpNFvGLYU=",
"pom": "sha256-PmaDDVn65S4QMLS+ckseV3rIFNJzNf34GnEoPcCCnxo="
},
"androidx/lifecycle#lifecycle-common/2.6.2": {
"module": "sha256-D6fyj1z/ikBqT3hwskPLDW16fCD6p6K+yv9ZB64S+cw=",
"pom": "sha256-VzBM2sTaKJpuzdBzjhax2IWPHvjp+r4tZaljcZ/YHbo="
},
"androidx/lifecycle#lifecycle-livedata-core-ktx/2.10.0": {
"module": "sha256-KyigMgHzB3sq4+KFP5g5RK/fUYsCk5rPyf6eX8q4cnU=",
"pom": "sha256-2YIILDJ57rnBh9mJdmXKmpDTRD812txokmH0snxy2aw="
},
"androidx/lifecycle#lifecycle-livedata-core/2.10.0": {
"module": "sha256-HYO9XzzMEpjtolue0SjowYf4MOfzr40ClL5oirsDw10=",
"pom": "sha256-S+1ekc0qfAFVgyC2oXbbuGf8o8TGl9uUjiA75F+RDWQ="
},
"androidx/lifecycle#lifecycle-livedata-core/2.3.1": {
"module": "sha256-seCV1VDTmn1sgVdh1tvj/WTrMaOdwoFG54u/LAG6j0E=",
"pom": "sha256-+5liIuPeR/G964s2WQuMEYmKT3L5E42hhzZdMQOhQ/s="
},
"androidx/lifecycle#lifecycle-livedata-core/2.5.1": {
"module": "sha256-PziOngeJAZcMK/z8Av7K6UjeS0a+UhGRmuB9ASyimA0=",
"pom": "sha256-ZC8Oldo/OGux0TbvihFIHczThMKKCT2Utce82qLvpbM="
},
"androidx/lifecycle#lifecycle-livedata-core/2.6.1": {
"module": "sha256-6cDcPwrFRBnAz+2P9c7LgpQ6fFj3pUFp8NhJssYKNVI=",
"pom": "sha256-4PZ+Ky5hLxO8Dzzqck2u3vpdRJQFHrRfW+5tiKbaiFk="
},
"androidx/lifecycle#lifecycle-livedata/2.0.0": {
"pom": "sha256-qEhC/8DxTlGNt1wFzBEmgKikoWT6eDlb4y2IMEpDlCM="
},
"androidx/lifecycle#lifecycle-livedata/2.10.0": {
"module": "sha256-Om1HOzEEyYNQHeSIPWunaE3IMzDEYO+Vg5CoJlsHgxA=",
"pom": "sha256-trk2hsVaHKjsIOfYSl/jQSZ+XaxuOQ+xzKOYODER6wg="
},
"androidx/lifecycle#lifecycle-process/2.10.0": {
"module": "sha256-xorsfMHQ0Z9RMo59KMkdz7SsjhqYQ8/WGB4aqUrhnJs=",
"pom": "sha256-osMGIyCHP5rCne0dnKSiQVUpwuRWjVO18rgT7FKtxmQ="
},
"androidx/lifecycle#lifecycle-process/2.4.1": {
"module": "sha256-46rj7QS0dE/zFFLpj9KZ4639KNO1cjZh2WeLkvoJzrQ=",
"pom": "sha256-2n4uEMrn12JaCuRUAXoSV0nheRoTnC13feka3YlBCvU="
},
"androidx/lifecycle#lifecycle-process/2.6.1": {
"module": "sha256-WMnic3HM96IqIz9Ekm00jJ0H54xBpWWIpCZf9q52ZFo=",
"pom": "sha256-QR/JuQIWdanSvS1alUhSAR2KMXJjtuM5lGRJpgFdOoY="
},
"androidx/lifecycle#lifecycle-runtime-android/2.10.0": {
"module": "sha256-dJtuekQikUWB4HldnUj7T5bao/7pd0dCH/QjSGAYX0c=",
"pom": "sha256-6X7gHa9vTcy5AL23Zyuqta08fCXEVJH9zPXroztur/8="
},
"androidx/lifecycle#lifecycle-runtime-ktx-android/2.10.0": {
"module": "sha256-mAob4UbK+harOxaAmR3eFLIHeksT1bMUJ8uY3Ikw8ks=",
"pom": "sha256-XBomndDwrOh/IoMog7iYvXtCUtaofwTBc0fYPjEoZg4="
},
"androidx/lifecycle#lifecycle-runtime-ktx/2.10.0": {
"module": "sha256-VW8VlXN2hYtuOrwI+z31ZupetAZxSSUxlL8qBJYG204=",
"pom": "sha256-BgrRdTOB56+deluJgp85Tvb9R/d+MbpMgSRivDe9phg="
},
"androidx/lifecycle#lifecycle-runtime/2.0.0": {
"pom": "sha256-qSpG+nrsisMmpdV4c0otW2MgaXaZa54GyuFxs1sKtt4="
},
"androidx/lifecycle#lifecycle-runtime/2.10.0": {
"module": "sha256-2oBfoBekrM4T9QFGmnWj8wYkjuvFj1vMKAGbABXfumU=",
"pom": "sha256-Sote7FTV7N8JnqU7Lk6fJNspZ+pdOTCXtqbFSe1pMI0="
},
"androidx/lifecycle#lifecycle-runtime/2.3.1": {
"module": "sha256-KnuQ5QSbZ0s2vM/WhnezoLMXiz98Lvfd9hjTiVWYxM4=",
"pom": "sha256-tEQqhPw5ftsukIof33E8aufTqHZBoJ7hPRDsjuELMx8="
},
"androidx/lifecycle#lifecycle-runtime/2.6.1": {
"module": "sha256-pMuwGkLQcEe9jYcAF8lqGwt7RnMyDoa2YxehO+LsEMc=",
"pom": "sha256-QecdAD1DfxadToraWcsZgvG0e30lwjAQuPyB1SinQNU="
},
"androidx/lifecycle#lifecycle-runtime/2.6.2": {
"module": "sha256-6gExhGq+H+nepZrG3+Hw+52LbWAMnv+aH9StXuXny8c=",
"pom": "sha256-gXUlVUbipfUQhl+ErOaAZglUcwJAsZBdkXW0NFvtqXc="
},
"androidx/lifecycle#lifecycle-viewmodel-android/2.10.0": {
"module": "sha256-BrB6B64Xx1kqMMUzw9RqEl/LG+8JbX5hNOJCDbJGTQo=",
"pom": "sha256-CqFxpQ9QqFyQaHrYLc038xr90b/SRjwhXRMilg5R4ig="
},
"androidx/lifecycle#lifecycle-viewmodel-savedstate-android/2.10.0": {
"module": "sha256-lJ0qhitrWEzuka9wqeAXvbEX0iW7RgltqUJKFyjKeek=",
"pom": "sha256-X81Cm6V+Hd4bQ4U05Twj1idNI04zx0iWP2+qn4GTPIc="
},
"androidx/lifecycle#lifecycle-viewmodel-savedstate/2.10.0": {
"module": "sha256-UIV3gePd+OOZBeTkpXowSWwkX5kB0bfT8XOCOCGINH0=",
"pom": "sha256-WWVv/K4eDhRwQ7DW2JlmpflnUvBSkev6AoQG7ZRETdE="
},
"androidx/lifecycle#lifecycle-viewmodel-savedstate/2.3.1": {
"module": "sha256-gINxC3WKwJaJHpH1HZHuVqRFsmXXvs8jA1U3cyfAQYs=",
"pom": "sha256-IV5A7oT9r7Ke8liXexlrro+lpsejo0EUJ8eHsnHk9Fw="
},
"androidx/lifecycle#lifecycle-viewmodel-savedstate/2.5.1": {
"module": "sha256-KazV/mFLP4kSPrg49ojWJeqotCLI0ZBbSK2OdgzXrYs=",
"pom": "sha256-PXf6Ow4CbYa3I7vBNHQuIy6Pmc4YoR31t4lWLppsCm8="
},
"androidx/lifecycle#lifecycle-viewmodel-savedstate/2.6.1": {
"module": "sha256-2vuGSXY9KcKc2ie8IvzauanvxTwP/5rj3pCILquqiUQ=",
"pom": "sha256-qjTLhYY4S44xTP1lsYExl/Mve6cdJSmhHAcerwJ0Hls="
},
"androidx/lifecycle#lifecycle-viewmodel/2.10.0": {
"module": "sha256-T9cd9zMkXEbkuI6FtKkLgsmvSjWe8x8r3yUPk6AsdFI=",
"pom": "sha256-HYYK/wylmNSOjoPaP05naQeEmFdKGNO4iewrjECQsZg="
},
"androidx/lifecycle#lifecycle-viewmodel/2.3.1": {
"module": "sha256-pTGFPf4xbJC3Rm0kvpTb5gpg71SlLJBMhjgZhiAuUfQ=",
"pom": "sha256-I/RerQuaA4OC2cOq+ctR9QvrzLY4q4PfnYQqO/CMQRo="
},
"androidx/lifecycle#lifecycle-viewmodel/2.5.1": {
"module": "sha256-AeQTtzy+OMtxTcW9shvYYJMRJMjl8jaYA/SqzEkIHJ8=",
"pom": "sha256-k+XPzLyf0vihCS7AawdXvYNOY9zSqAAbWUI5qx5KbeE="
},
"androidx/lifecycle#lifecycle-viewmodel/2.6.1": {
"module": "sha256-K0BvrqXBLyuN9LemCTH4RmSPLh9NeDYeGY0RhPGaR5c=",
"pom": "sha256-3C6OZdtT0hZZon7ZO5Zt7jNsHC6OhyhhZ3OJqZuLkTQ="
},
"androidx/lifecycle/lifecycle-livedata-core-ktx/2.10.0/lifecycle-livedata-core-ktx-2.10.0": {
"aar": "sha256-XxhCl0mhg+GReNhmWuQr97YxC1A1iung4MxFKv1uC8M="
},
"androidx/lifecycle/lifecycle-livedata-core/2.10.0/lifecycle-livedata-core-2.10.0": {
"aar": "sha256-Et1hqYQ8zrtFR9Pr4vbQMMqPaYjSL4+tGcCvk7SpfpU="
},
"androidx/lifecycle/lifecycle-livedata-core/2.3.1/lifecycle-livedata-core-2.3.1": {
"aar": "sha256-5V04w3JGDwoDmX3clQxnInURNA/XT4Y02Z0pZTzYGrE="
},
"androidx/lifecycle/lifecycle-livedata/2.0.0/lifecycle-livedata-2.0.0": {
"aar": "sha256-yCYJztjEmPCnAaMPtncbt0gIYNruhNguCoHuhu33ujk="
},
"androidx/lifecycle/lifecycle-livedata/2.10.0/lifecycle-livedata-2.10.0": {
"aar": "sha256-PowAn8iNocUuTtBagSadWsm9QFBG8wAwf146F+IBpt8="
},
"androidx/lifecycle/lifecycle-process/2.10.0/lifecycle-process-2.10.0": {
"aar": "sha256-ELtbsSdz3KEdg0PAFM/RfftAln1uzjfo8V0E2M6wd1w="
},
"androidx/lifecycle/lifecycle-runtime-android/2.10.0/lifecycle-runtime-android-2.10.0": {
"aar": "sha256-IZOhVz1iPzeyDH0n0aj5A6cvZRzG8y5XlPhd2nRP7nU="
},
"androidx/lifecycle/lifecycle-runtime-ktx-android/2.10.0/lifecycle-runtime-ktx-android-2.10.0": {
"aar": "sha256-hxiDcDM5+HKE0jLLQ24xfG9K9WEtkrQ/J8axO5IQn6c="
},
"androidx/lifecycle/lifecycle-runtime/2.3.1/lifecycle-runtime-2.3.1": {
"aar": "sha256-3SlPSmiccf+Hf9QfO2ejpi93YNRM5CDmEw8fw1adjwA="
},
"androidx/lifecycle/lifecycle-viewmodel-android/2.10.0/lifecycle-viewmodel-android-2.10.0": {
"aar": "sha256-kwMocDACfKC4z5inSN3rEh7Bv6ExLABpQo5HZburzng="
},
"androidx/lifecycle/lifecycle-viewmodel-savedstate-android/2.10.0/lifecycle-viewmodel-savedstate-android-2.10.0": {
"aar": "sha256-GUBCOfpYQyLC+B6WWeFivAEbdG0p3JUKEdJgYk2DIl4="
},
"androidx/lifecycle/lifecycle-viewmodel-savedstate/2.10.0/lifecycle-viewmodel-savedstate-2.10.0": {
"aar": "sha256-H6InIMh7D7PBIk+FgM9pNp88/E2ZIXCLuUSfh4x8s+k="
},
"androidx/lifecycle/lifecycle-viewmodel-savedstate/2.3.1/lifecycle-viewmodel-savedstate-2.3.1": {
"aar": "sha256-lxN6ivajF3ahTkhmq4CMfAp5G0hL28eIu9g+ZkB1ZMA="
},
"androidx/lifecycle/lifecycle-viewmodel/2.10.0/lifecycle-viewmodel-2.10.0": {
"aar": "sha256-S4Cc+etzI9IAUE6Vkh/rd3/wgCFcrbgjd0LV7Qult5I="
},
"androidx/lifecycle/lifecycle-viewmodel/2.3.1/lifecycle-viewmodel-2.3.1": {
"aar": "sha256-tttMJ0oS/4WkdH4eZmnH6Yrvolcazp0fGm+mvkF86Dg="
},
"androidx/loader#loader/1.0.0": {
"pom": "sha256-yXjVUICLR0NKpJpjFkEQpQtVsLzGFgqTouN9URDfjF4="
},
"androidx/loader/loader/1.0.0/loader-1.0.0": {
"aar": "sha256-Efc1yztVxFjUcL7Z4lJUN1tRi0sbrWkmeDpwJtsPUCU="
},
"androidx/profileinstaller#profileinstaller/1.3.1": {
"modul
gitextract_1ne4upm2/ ├── .editorconfig ├── .envrc ├── .git-blame-ignore-revs ├── .github/ │ └── workflows/ │ └── checks.yml ├── .gitignore ├── .shellcheckrc ├── .swiftformat ├── LICENSE.md ├── README.md ├── android/ │ ├── .gitignore │ ├── .idea/ │ │ ├── .gitignore │ │ ├── .name │ │ ├── AndroidProjectSystem.xml │ │ ├── codeStyles/ │ │ │ ├── Project.xml │ │ │ └── codeStyleConfig.xml │ │ ├── compiler.xml │ │ ├── detekt.xml │ │ ├── deviceManager.xml │ │ ├── dictionaries/ │ │ │ └── project.xml │ │ ├── gradle.xml │ │ ├── kotlinc.xml │ │ ├── ktfmt.xml │ │ ├── migrations.xml │ │ ├── misc.xml │ │ ├── runConfigurations.xml │ │ └── vcs.xml │ ├── app/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── google-services.json │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── foss/ │ │ │ └── java/ │ │ │ └── net/ │ │ │ └── obscura/ │ │ │ └── vpnclientapp/ │ │ │ └── BillingFacade.kt │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── aidl/ │ │ │ │ └── net/ │ │ │ │ └── obscura/ │ │ │ │ └── vpnclientapp/ │ │ │ │ └── services/ │ │ │ │ └── IObscuraVpnService.aidl │ │ │ ├── assets/ │ │ │ │ └── adi-registration.properties │ │ │ ├── java/ │ │ │ │ └── net/ │ │ │ │ └── obscura/ │ │ │ │ └── vpnclientapp/ │ │ │ │ ├── App.kt │ │ │ │ ├── activities/ │ │ │ │ │ └── MainActivity.kt │ │ │ │ ├── client/ │ │ │ │ │ ├── ErrorCodeException.kt │ │ │ │ │ ├── JsonConfig.kt │ │ │ │ │ ├── ManagerCmd.kt │ │ │ │ │ ├── ManagerCmdOk.kt │ │ │ │ │ ├── ObscuraLibrary.java │ │ │ │ │ └── RustFfi.kt │ │ │ │ ├── helpers/ │ │ │ │ │ └── Process.kt │ │ │ │ ├── preferences/ │ │ │ │ │ └── Preferences.kt │ │ │ │ ├── services/ │ │ │ │ │ ├── ContextExtension.kt │ │ │ │ │ ├── IntentExtension.kt │ │ │ │ │ ├── ObscuraVpnService.kt │ │ │ │ │ └── OsNetworkConfig.kt │ │ │ │ ├── sharing/ │ │ │ │ │ └── DebugArchiveFileProvider.java │ │ │ │ └── ui/ │ │ │ │ ├── BillingModule.kt │ │ │ │ ├── JsonFfiBroadcastReceiver.kt │ │ │ │ ├── NetworkStatusObserver.kt │ │ │ │ ├── ObscuraUI.kt │ │ │ │ ├── ObscuraWebView.kt │ │ │ │ ├── OsStatus.kt │ │ │ │ ├── OsStatusManager.kt │ │ │ │ ├── VpnPermissionRequestManager.kt │ │ │ │ ├── VpnStatusObserver.kt │ │ │ │ └── bridge/ │ │ │ │ ├── WebCmd.kt │ │ │ │ ├── WebCmdBridge.kt │ │ │ │ └── WebCmdHelpers.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ ├── ic_launcher.xml │ │ │ │ ├── icon_about.xml │ │ │ │ ├── icon_account.xml │ │ │ │ ├── icon_connection.xml │ │ │ │ ├── icon_location.xml │ │ │ │ └── icon_settings.xml │ │ │ ├── layout/ │ │ │ │ └── activity_main.xml │ │ │ ├── menu/ │ │ │ │ └── nav_menu.xml │ │ │ ├── values/ │ │ │ │ ├── colors.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── ids.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ ├── values-night/ │ │ │ │ └── themes.xml │ │ │ └── xml/ │ │ │ ├── backup_rules.xml │ │ │ ├── data_extraction_rules.xml │ │ │ └── debug_archive_file_provider_paths.xml │ │ └── play/ │ │ └── java/ │ │ └── net/ │ │ └── obscura/ │ │ └── vpnclientapp/ │ │ └── BillingFacade.kt │ ├── build.gradle.kts │ ├── buildSrc/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── settings.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── VersionName.kt │ ├── detekt.yml │ ├── gradle/ │ │ ├── libs.versions.toml │ │ ├── mitm-cache/ │ │ │ └── deps.json │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── lib/ │ │ ├── billing/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── java/ │ │ │ └── net/ │ │ │ └── obscura/ │ │ │ └── lib/ │ │ │ └── billing/ │ │ │ ├── BillingConnection.kt │ │ │ └── BillingManager.kt │ │ └── util/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── net/ │ │ └── obscura/ │ │ └── lib/ │ │ └── util/ │ │ ├── ExternallyTaggedEnumSerializer.kt │ │ ├── ExternallyTaggedEnumVariantSerializer.kt │ │ └── Log.kt │ ├── lint.xml │ └── settings.gradle.kts ├── apple/ │ ├── Configurations/ │ │ ├── .gitignore │ │ ├── Base.xcconfig │ │ ├── Debug-app-network-extension.xcconfig │ │ ├── Debug-app.xcconfig │ │ ├── Debug-system-network-extension.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release-app-network-extension.xcconfig │ │ ├── Release-app.xcconfig │ │ ├── Release-system-network-extension.xcconfig │ │ ├── Release.xcconfig │ │ ├── app-network-extension.xcconfig │ │ ├── app.xcconfig │ │ ├── bundle-ids.xcconfig │ │ └── system-network-extension.xcconfig │ ├── ExportOptions.plist │ ├── Packet Tunnel Provider/ │ │ ├── Keychain.swift │ │ ├── NetworkSettings.swift │ │ ├── PacketTunnelProvider.swift │ │ ├── RustFfi.swift │ │ └── main.swift │ ├── app-network-extension/ │ │ ├── Info.plist │ │ └── entitlements.entitlements │ ├── cbindgen-apple.toml │ ├── client/ │ │ ├── Assets.xcassets/ │ │ │ ├── AccentColor.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── DecoPrimer.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── EmotePrimer.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── MenuBarConnected.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── MenuBarConnectedDown.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── MenuBarConnectedUp.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── MenuBarConnectedUpDown.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── MenuBarConnecting-1.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── MenuBarConnecting-2.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── MenuBarConnecting-3.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── MenuBarDisconnected.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── ObscuraOrange.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── UpdateAvailable.imageset/ │ │ │ │ └── Contents.json │ │ │ └── custom.globe.badge.gearshape.fill.symbolset/ │ │ │ └── Contents.json │ │ ├── Constants.swift │ │ ├── ContentView.swift │ │ ├── DebugBundle+XP.swift │ │ ├── DebugBundle.swift │ │ ├── DebugBundleExtensionInfo.swift │ │ ├── Info.plist │ │ ├── LoginItem.swift │ │ ├── LoopingVideoPlayer.swift │ │ ├── Notifications.swift │ │ ├── OSLogEntryEncodable.swift │ │ ├── OsStatus.swift │ │ ├── Preview Content/ │ │ │ └── Preview Assets.xcassets/ │ │ │ └── Contents.json │ │ ├── ScriptMessageHandlers.swift │ │ ├── StartupStatus.swift │ │ ├── StatusItem/ │ │ │ ├── AccountStatusItem.swift │ │ │ ├── BandwidthStatus.swift │ │ │ ├── MenuItemView.swift │ │ │ ├── ObscuraToggle.swift │ │ │ └── StatusMenu.swift │ │ ├── Store/ │ │ │ ├── Obscura VPN Local.storekit │ │ │ ├── Obscura VPN.storekit │ │ │ ├── Product+Convenience.swift │ │ │ ├── StoreKitListener.swift │ │ │ └── StoreKitModel.swift │ │ ├── Style/ │ │ │ ├── Appearance.swift │ │ │ ├── ConditionallyDisabled.swift │ │ │ ├── HyperlinkButtonStyle.swift │ │ │ └── NoFadeButtonStyle.swift │ │ ├── TunnelProvider.swift │ │ ├── UXKit/ │ │ │ ├── UXImage.swift │ │ │ ├── UXViewController.swift │ │ │ └── UXViewRepresentable.swift │ │ ├── UpdaterDriver+XP.swift │ │ ├── Webviews/ │ │ │ ├── ExternalWebView.swift │ │ │ ├── ObscuraUIIOSWrapperAndTabs.swift │ │ │ ├── ObscuraUIMacOSWrapper.swift │ │ │ ├── ObscuraUIWebView.swift │ │ │ ├── ObscuraUIWebViewMacOSWrapper.swift │ │ │ └── WebviewsController.swift │ │ ├── app_state.swift │ │ ├── client-ios.entitlements │ │ ├── client-macos.entitlements │ │ ├── command.swift │ │ ├── extensions/ │ │ │ └── NEVPNStatus.swift │ │ ├── iOS/ │ │ │ ├── MailDelegate.swift │ │ │ └── iOSClientApp.swift │ │ ├── initNetworkExtension.swift │ │ ├── macOS/ │ │ │ ├── CheckForUpdatesView.swift │ │ │ ├── ClientApp.swift │ │ │ ├── InstallSystemExtensionView.swift │ │ │ ├── RegisterLoginItemView.swift │ │ │ ├── SparkleUpdater.swift │ │ │ ├── UpdateSystemExtensionView.swift │ │ │ └── UpdaterDriver.swift │ │ └── startup.swift │ ├── client.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── swiftpm/ │ │ │ └── Package.resolved │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ ├── App Network Extension.xcscheme │ │ ├── Dev Client.xcscheme │ │ ├── Obscura VPN iOS.xcscheme │ │ ├── Prod Client.xcscheme │ │ └── System Network Extension.xcscheme │ ├── dmg-building/ │ │ └── installer_background.tiff │ ├── libobscuravpn_client/ │ │ └── .gitignore │ ├── obscuravpn-client.xcodeproj/ │ │ └── project.pbxproj │ ├── shared/ │ │ ├── Account+CustomStringConvertible.swift │ │ ├── Account.swift │ │ ├── AccountInfo+Util.swift │ │ ├── Box.swift │ │ ├── Concurrency.swift │ │ ├── ConcurrencyTests.swift │ │ ├── Debug.swift │ │ ├── FfiCb.swift │ │ ├── InfoDict.swift │ │ ├── Json.swift │ │ ├── NetworkExtensionIpc.swift │ │ ├── NotificationIds.swift │ │ ├── ObservableValue.swift │ │ ├── Sleep.swift │ │ ├── String.swift │ │ ├── StringError.swift │ │ ├── Swift.swift │ │ ├── WatchableValue.swift │ │ └── time.swift │ ├── system-network-extension/ │ │ ├── Info.plist │ │ └── entitlements.entitlements │ ├── third-party/ │ │ └── CwlSysctl.swift │ └── xcodescripts/ │ ├── cargo-build-static-lib.bash │ ├── nix-build-web-bundle.bash │ ├── nix-web-dev-server-start.bash │ ├── nix-web-dev-server-stop.bash │ ├── pre-action.bash │ └── set-build-info.bash ├── bin/ │ ├── gradle-deps-update.sh │ ├── log-sleeps.py │ ├── log-summary.py │ └── log-text.py ├── contrib/ │ ├── bin/ │ │ ├── build-obscuravpn-dmg.bash │ │ ├── check-in-obscura-nix-shell.bash │ │ ├── find-nix-files.bash │ │ ├── find-shellcheck-files.bash │ │ ├── linux-packages.bash │ │ ├── linux-test.bash │ │ ├── linux_run_service.sh │ │ ├── ls-non-ignored-files.bash │ │ ├── nixfmt-auto-files.bash │ │ ├── package-arch.bash │ │ ├── package-deb.bash │ │ ├── package-rpm.bash │ │ └── shellcheck-auto-files.bash │ ├── licenses.mjs │ └── shell/ │ ├── source-die.bash │ ├── source-echoerr.bash │ └── source-nix.sh ├── flake.nix ├── justfile ├── linux/ │ ├── arch_Dockerfile │ ├── arch_PKGBUILD │ ├── deb_Dockerfile │ ├── deb_control │ ├── deb_install │ ├── deb_rules │ ├── obscura-preset.conf │ ├── obscura-sysusers.conf │ ├── obscura.service │ ├── rpm_Dockerfile │ ├── rpm_obscura.spec │ ├── rpm_rpmlintrc │ └── vm/ │ ├── archlinux-desktop-cloud-init/ │ │ ├── meta-data │ │ └── user-data │ ├── debian-desktop.preseed.cfg │ ├── fedora43-desktop.ks │ └── ubuntu24.04-desktop-cloud-init/ │ ├── meta-data │ └── user-data ├── obscura-ui/ │ ├── .gitattributes │ ├── .gitignore │ ├── .vscode/ │ │ └── settings.json │ ├── README.md │ ├── index.html │ ├── justfile │ ├── package.json │ ├── postcss.config.js │ ├── src/ │ │ ├── App.module.css │ │ ├── App.tsx │ │ ├── Providers.tsx │ │ ├── bridge/ │ │ │ ├── SystemProvider.tsx │ │ │ ├── android.ts │ │ │ └── commands.ts │ │ ├── common/ │ │ │ ├── KeyedSet.ts │ │ │ ├── accountUtils.ts │ │ │ ├── api.ts │ │ │ ├── appContext.ts │ │ │ ├── common.module.css │ │ │ ├── debuggingArchiveHook.tsx │ │ │ ├── exitUtils.ts │ │ │ ├── fmt.ts │ │ │ ├── links.ts │ │ │ ├── localStorage.ts │ │ │ ├── notifIds.ts │ │ │ ├── useAsync.ts │ │ │ ├── useExitList.ts │ │ │ ├── useLoadable.ts │ │ │ ├── useMailto.ts │ │ │ ├── useSharedWatchable.ts │ │ │ └── utils.ts │ │ ├── components/ │ │ │ ├── AccountNumberSection.tsx │ │ │ ├── AnimatedChevron.tsx │ │ │ ├── BoltBadgeAuto.tsx │ │ │ ├── ButtonLink.tsx │ │ │ ├── CachedColorScheme.tsx │ │ │ ├── ConfirmationDialog.module.css │ │ │ ├── ConfirmationDialog.tsx │ │ │ ├── DebuggingArchive.module.css │ │ │ ├── DebuggingArchive.tsx │ │ │ ├── DevSendCommand.tsx │ │ │ ├── DevSetApiUrl.tsx │ │ │ ├── ExternalLinkIcon.tsx │ │ │ ├── Licenses.tsx │ │ │ ├── Mantine.tsx │ │ │ ├── ObscuraChip.tsx │ │ │ ├── ObscuraWordmark.tsx │ │ │ ├── PaymentManagementSheet.tsx │ │ │ ├── ScrollableView.module.css │ │ │ ├── ScrollableView.tsx │ │ │ ├── SecondaryButton.tsx │ │ │ ├── SelectCreatable.tsx │ │ │ ├── Socials.tsx │ │ │ └── VpnErrorFmt.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── translations/ │ │ │ ├── en.json │ │ │ ├── i18n.ts │ │ │ └── i18next.d.ts │ │ ├── views/ │ │ │ ├── About.module.css │ │ │ ├── About.tsx │ │ │ ├── AccountView.module.css │ │ │ ├── AccountView.tsx │ │ │ ├── ConnectionView.module.css │ │ │ ├── ConnectionView.tsx │ │ │ ├── DeveloperView.tsx │ │ │ ├── HelpView.tsx │ │ │ ├── Location.module.css │ │ │ ├── LocationView.tsx │ │ │ ├── LogInView.tsx │ │ │ ├── LoginView.module.css │ │ │ ├── Settings.module.css │ │ │ ├── Settings.tsx │ │ │ ├── SplashScreen.tsx │ │ │ ├── index.ts │ │ │ └── render-fallbacks/ │ │ │ └── FallbackAppRender.tsx │ │ └── wdyr.ts │ ├── tsconfig.json │ ├── vite-env.d.ts │ └── vite.config.ts ├── rustlib/ │ ├── .cargo/ │ │ └── config.toml │ ├── .gitignore │ ├── Cargo.toml │ ├── about.toml │ ├── build.rs │ ├── examples/ │ │ ├── connect.rs │ │ └── list-relay-rtts.rs │ ├── rust-toolchain.toml │ ├── rustfmt.toml │ └── src/ │ ├── android/ │ │ ├── class_cache.rs │ │ ├── ffi.rs │ │ ├── future.rs │ │ ├── mod.rs │ │ ├── os_impl.rs │ │ ├── tunnel.rs │ │ └── util.rs │ ├── apple/ │ │ ├── ffi.rs │ │ ├── mod.rs │ │ └── os_impl.rs │ ├── backoff.rs │ ├── backoff_test.rs │ ├── bin/ │ │ └── obscura/ │ │ ├── add_operator.rs │ │ ├── client/ │ │ │ ├── client_error.rs │ │ │ ├── ipc.rs │ │ │ └── mod.rs │ │ ├── main.rs │ │ └── service/ │ │ ├── mod.rs │ │ └── os/ │ │ ├── linux/ │ │ │ ├── dns/ │ │ │ │ ├── mod.rs │ │ │ │ └── resolved.rs │ │ │ ├── ipc.rs │ │ │ ├── mod.rs │ │ │ ├── network_manager.rs │ │ │ ├── routes/ │ │ │ │ ├── mod.rs │ │ │ │ └── netlink.rs │ │ │ ├── service_lock.rs │ │ │ ├── start_error.rs │ │ │ └── tun.rs │ │ ├── mod.rs │ │ └── windows/ │ │ ├── adapters.rs │ │ ├── gaa.rs │ │ ├── iphelper.rs │ │ ├── mod.rs │ │ ├── nrpt.rs │ │ ├── start_error.rs │ │ └── tun.rs │ ├── cached_value.rs │ ├── client_state.rs │ ├── config/ │ │ ├── cached.rs │ │ ├── dns_cache.rs │ │ ├── feature_flags.rs │ │ ├── mod.rs │ │ ├── persistence.rs │ │ └── persistence_test.rs │ ├── constants.rs │ ├── debug_archive/ │ │ ├── builder.rs │ │ ├── dns.rs │ │ ├── http.rs │ │ ├── info.rs │ │ ├── mod.rs │ │ ├── task.rs │ │ └── zipper.rs │ ├── dns.rs │ ├── errors.rs │ ├── exit_selection.rs │ ├── ffi_helpers.rs │ ├── lib.rs │ ├── liveness.rs │ ├── logging.rs │ ├── manager.rs │ ├── manager_cmd.rs │ ├── net.rs │ ├── network_config.rs │ ├── os/ │ │ ├── mod.rs │ │ ├── os_trait.rs │ │ └── packet_buffer.rs │ ├── positive_u31.rs │ ├── quicwg.rs │ ├── rate_limited_log.rs │ ├── relay_selection.rs │ ├── serde_safe.rs │ ├── tokio.rs │ └── tunnel_state.rs ├── tag.json └── taplo.toml
SYMBOL INDEX (987 symbols across 127 files)
FILE: android/app/src/main/java/net/obscura/vpnclientapp/client/ObscuraLibrary.java
class ObscuraLibrary (line 8) | public class ObscuraLibrary {
class FfiHandle (line 10) | @Keep
method FfiHandle (line 12) | private FfiHandle() {}
method load (line 15) | static FfiHandle load(Context context, String userAgent) {
method initialize (line 23) | static native FfiHandle initialize(String configDir, String userAgent);
method jsonFfi (line 25) | static native void jsonFfi(FfiHandle handle, String json, CompletableF...
method setNetworkInterface (line 27) | static native void setNetworkInterface(FfiHandle handle, String name, ...
method unsetNetworkInterface (line 28) | static native void unsetNetworkInterface(FfiHandle handle);
method forwardLog (line 30) | static native void forwardLog(int level, String tag, String message, S...
FILE: android/app/src/main/java/net/obscura/vpnclientapp/sharing/DebugArchiveFileProvider.java
class DebugArchiveFileProvider (line 9) | public class DebugArchiveFileProvider extends FileProvider {
method DebugArchiveFileProvider (line 10) | public DebugArchiveFileProvider() {
FILE: bin/log-sleeps.py
function fmt_time (line 11) | def fmt_time(timestamp):
function is_sleep_log (line 14) | def is_sleep_log(log):
FILE: bin/log-summary.py
function format_log_time (line 40) | def format_log_time(log):
function format_time (line 44) | def format_time(date):
FILE: bin/log-text.py
function format_time (line 49) | def format_time(date):
function format_log (line 72) | def format_log(log):
FILE: contrib/licenses.mjs
function rpartition (line 3) | function rpartition(s, p) {
constant LICENSES_NODE (line 14) | const LICENSES_NODE = JSON.parse(readFileSync(process.env.LICENSES_NODE));
constant LICENSES_RUST (line 15) | const LICENSES_RUST = JSON.parse(readFileSync(process.env.LICENSES_RUST));
function addLicense (line 25) | function addLicense(info) {
function cmp (line 253) | function cmp(l, r) {
FILE: obscura-ui/src/App.tsx
type View (line 24) | interface View {
function tryConnect (line 72) | async function tryConnect(exit: commands.ExitSelector) {
function disconnectFromVpn (line 96) | async function disconnectFromVpn() {
function notifyVpnError (line 103) | function notifyVpnError(errorEnum: string) {
function handleNewStatus (line 118) | function handleNewStatus(newStatus: AppStatus) {
function resetState (line 216) | function resetState() {
function RenderView (line 329) | function RenderView({ view }: { view: View }) {
FILE: obscura-ui/src/bridge/SystemProvider.tsx
constant PLATFORM (line 3) | const PLATFORM = import.meta.env.OBS_WEB_PLATFORM as Platform;
type Platform (line 6) | enum Platform {
function systemName (line 12) | function systemName(): string {
constant IS_HANDHELD_DEVICE (line 23) | const IS_HANDHELD_DEVICE = PLATFORM === Platform.iOS ||
constant CONNECT_REQUIRES_ONLINE (line 29) | const CONNECT_REQUIRES_ONLINE = PLATFORM === Platform.iOS || PLATFORM ==...
function useSystemChecks (line 31) | function useSystemChecks() {
function logReactError (line 40) | async function logReactError(error: Error, info: ErrorInfo) {
FILE: obscura-ui/src/bridge/commands.ts
function WKWebViewInvoke (line 13) | async function WKWebViewInvoke(command: string, args: Object) {
function invoke (line 27) | async function invoke(command: string, args: Object = {}): Promise<unkno...
class CommandError (line 37) | class CommandError extends Error {
method constructor (line 40) | constructor(code: string) {
method i18nKey (line 46) | i18nKey() {
function jsonFfiCmd (line 53) | async function jsonFfiCmd(cmd: string, arg = {}, timeoutMs: number | nul...
function status (line 62) | async function status(lastStatusId: string | null = null): Promise<AppSt...
function osStatus (line 70) | async function osStatus(lastOsStatusId: string | null = null): Promise<O...
function login (line 74) | function login(accountId: AccountId, validate = false) {
function logout (line 78) | function logout() {
function setApiUrl (line 82) | async function setApiUrl(url: string | null): Promise<void> {
function setApiHostAlternate (line 86) | async function setApiHostAlternate(host: string | null): Promise<void> {
function setSniRelay (line 90) | async function setSniRelay(host: string | null): Promise<void> {
function setStrictLeakPrevention (line 94) | async function setStrictLeakPrevention(enable: boolean): Promise<void> {
function setColorScheme (line 98) | async function setColorScheme(value: 'dark' | 'light' | 'auto'): Promise...
type TunnelArgs (line 103) | interface TunnelArgs {
type ExitSelectorId (line 107) | interface ExitSelectorId {
type ExitSelectorCity (line 111) | interface ExitSelectorCity {
type ExitSelectorCountry (line 116) | interface ExitSelectorCountry {
type ExitSelector (line 121) | type ExitSelector =
function connect (line 128) | async function connect(exit: ExitSelector): Promise<void> {
function disconnect (line 137) | async function disconnect(): Promise<void> {
function debuggingArchive (line 141) | async function debuggingArchive(userFeedback: string): Promise<String> {
function revealItemInDir (line 145) | function revealItemInDir(path: String) {
function emailDebugArchive (line 149) | async function emailDebugArchive(path: String, subject: String, body: St...
function shareDebugArchive (line 154) | async function shareDebugArchive(path: String): Promise<void> {
type Notice (line 158) | interface Notice {
function registerAsLoginItem (line 164) | async function registerAsLoginItem(): Promise<void> {
function unregisterAsLoginItem (line 168) | async function unregisterAsLoginItem(): Promise<void> {
function developerResetUserDefaults (line 172) | async function developerResetUserDefaults(): Promise<void> {
function checkForUpdates (line 176) | async function checkForUpdates(): Promise<void> {
function installUpdate (line 180) | async function installUpdate(): Promise<void> {
type TrafficStats (line 184) | interface TrafficStats {
function getTrafficStats (line 192) | async function getTrafficStats(): Promise<TrafficStats> {
type CachedValue (line 196) | interface CachedValue<T> {
type ExitList (line 202) | interface ExitList {
function getExitList (line 206) | async function getExitList(version?: string): Promise<CachedValue<ExitLi...
function refreshExitList (line 214) | async function refreshExitList(freshnessS: number): Promise<void> {
function deleteAccount (line 220) | async function deleteAccount(): Promise<void> {
function getAccount (line 224) | async function getAccount(): Promise<AccountInfo> {
function setInNewAccountFlow (line 229) | function setInNewAccountFlow(value: boolean) {
function setPinnedExits (line 233) | function setPinnedExits(newPinnedExits: PinnedLocation[]) {
function rotateWgKey (line 237) | function rotateWgKey() {
function setAutoConnect (line 241) | function setAutoConnect(enable: boolean) {
function setUseSystemDns (line 245) | function setUseSystemDns(enable: boolean) {
function setFeatureFlag (line 249) | async function setFeatureFlag(flag: FeatureFlagKey, active: boolean) {
function setDnsContentBlock (line 253) | async function setDnsContentBlock(value: DNSContentBlock): Promise<void> {
function getSubscriptionProductDisplay (line 257) | async function getSubscriptionProductDisplay(): Promise<SubscriptionProd...
function storeKitAssociateAccount (line 261) | async function storeKitAssociateAccount(): Promise<void> {
function storeKitPurchaseSubscription (line 265) | async function storeKitPurchaseSubscription(): Promise<boolean> {
function storeKitRestorePurchases (line 269) | async function storeKitRestorePurchases(): Promise<void> {
function showOfferCodeRedemption (line 273) | async function showOfferCodeRedemption(): Promise<void> {
function resetOfferCodeRedemptionSuccess (line 277) | async function resetOfferCodeRedemptionSuccess(): Promise<void> {
function playPurchaseSubscription (line 283) | async function playPurchaseSubscription(): Promise<boolean> {
type UseCommandOptions (line 287) | interface UseCommandOptions<CommandArgs extends any[]> {
function useCommand (line 303) | function useCommand<CommandArgs extends any[]>({ command, showNotificati...
FILE: obscura-ui/src/common/KeyedSet.ts
class KeyedSet (line 1) | class KeyedSet<V extends L, L = V, K = unknown> {
method constructor (line 5) | constructor(
method add (line 22) | add(v: V): V | undefined {
method extend (line 34) | extend(values: Iterable<V>) {
method get (line 40) | get(v: L): V | undefined {
method getKey (line 44) | getKey(k: K): V | undefined {
method has (line 48) | has(v: L): boolean {
method hasKey (line 52) | hasKey(k: K): boolean {
method size (line 56) | get size(): number {
method [Symbol.iterator] (line 15) | [Symbol.iterator](): Iterator<V> {
FILE: obscura-ui/src/common/accountUtils.ts
constant INVERSE (line 27) | const INVERSE = [0, 4, 3, 2, 1, 5, 6, 7, 8, 9];
function rawChecksum (line 29) | function rawChecksum(digits: string): number {
function checkDigit (line 39) | function checkDigit(digits: string): number {
function validChecksum (line 43) | function validChecksum(digits: string): boolean {
constant ACCOUNT_ID_LENGTH (line 47) | const ACCOUNT_ID_LENGTH = 19;
constant MAX_ID (line 48) | const MAX_ID = 10n ** BigInt(ACCOUNT_ID_LENGTH);
constant USER_ACCOUNT_NUMBER_LEN (line 49) | const USER_ACCOUNT_NUMBER_LEN = ACCOUNT_ID_LENGTH + 1;
constant ACCOUNT_ID_DISPLAY_CHUNK_SIZE (line 50) | const ACCOUNT_ID_DISPLAY_CHUNK_SIZE = 4;
constant ACCOUNT_ID_CHUNK_RE (line 51) | const ACCOUNT_ID_CHUNK_RE = new RegExp(`.{${ACCOUNT_ID_DISPLAY_CHUNK_SIZ...
function generateAccountId (line 53) | function generateAccountId(): BigInt {
function generateAccountNumber (line 65) | function generateAccountNumber(): AccountId {
type AccountId (line 70) | interface AccountId {
function accountIdToString (line 75) | function accountIdToString(id: AccountId): string {
type ObscuraAccountErrorCode (line 79) | const enum ObscuraAccountErrorCode {
class ObscuraAccountIdError (line 85) | class ObscuraAccountIdError extends Error {
method constructor (line 88) | constructor(code: ObscuraAccountErrorCode, message: string) {
method i18nKey (line 94) | i18nKey() {
function parseAccountIdInt (line 100) | function parseAccountIdInt(id: string): AccountId {
function parseAccountIdInput (line 113) | function parseAccountIdInput(input: string): AccountId {
function normalizeAccountIdInput (line 117) | function normalizeAccountIdInput(id: string): string {
function formatPartialAccountId (line 121) | function formatPartialAccountId(accountId: string): string {
constant OBSCURA_WEBPAGE (line 129) | const OBSCURA_WEBPAGE = 'https://obscura.com';
constant CHECK_STATUS_WEBPAGE (line 130) | const CHECK_STATUS_WEBPAGE = `${OBSCURA_WEBPAGE}/check`;
constant LEGAL_WEBPAGE (line 131) | const LEGAL_WEBPAGE = `${OBSCURA_WEBPAGE}/legal`;
constant APP_ACCOUNT_TAB (line 132) | const APP_ACCOUNT_TAB = 'obscuravpn:///account';
constant APP_MANAGE_SUBSCRIPTION (line 134) | const APP_MANAGE_SUBSCRIPTION = `obscuravpn:///manage-subscription`;
function payUrl (line 136) | function payUrl(accountId: AccountId): string {
function subscriptionUrl (line 140) | function subscriptionUrl(accountId: AccountId): string {
function tunnelsUrl (line 144) | function tunnelsUrl(accountId: AccountId): string {
FILE: obscura-ui/src/common/api.ts
type Exit (line 6) | interface Exit {
function getContinent (line 17) | function getContinent(countryData: ICountryData): TContinentCode {
function getCountry (line 22) | function getCountry(country_code: string): ICountryData {
function getExitCountry (line 26) | function getExitCountry(exit: Exit): ICountryData {
type AccountInfo (line 33) | interface AccountInfo {
type TopUpInfo (line 43) | interface TopUpInfo {
function hasCredit (line 47) | function hasCredit(accountInfo: AccountInfo | undefined): boolean {
type SubscriptionInfo (line 52) | interface SubscriptionInfo {
function hasActiveSubscription (line 60) | function hasActiveSubscription(account: AccountInfo): boolean {
function isRenewing (line 72) | function isRenewing(account: AccountInfo): boolean {
function paidUntil (line 79) | function paidUntil(account: AccountInfo): Date | null {
function activeAppleSubscription (line 86) | function activeAppleSubscription(account: AccountInfo): boolean {
function accountIsExpired (line 96) | function accountIsExpired(accountInfo: AccountInfo): boolean {
type TimeRemaining (line 104) | interface TimeRemaining {
function accountTimeRemaining (line 113) | function accountTimeRemaining(account: AccountInfo): TimeRemaining {
type SubscriptionStatus (line 130) | const enum SubscriptionStatus {
type AppleSubscriptionStatus (line 142) | const enum AppleSubscriptionStatus {
type AppleSubscriptionInfo (line 150) | interface AppleSubscriptionInfo {
function hasAppleSubscription (line 156) | function hasAppleSubscription(accountInfo: AccountInfo | undefined): boo...
function useReRenderWhenExpired (line 165) | function useReRenderWhenExpired(account: AccountStatus | null) {
FILE: obscura-ui/src/common/appContext.ts
type NEVPNStatus (line 6) | enum NEVPNStatus {
type UpdaterStatusType (line 15) | enum UpdaterStatusType {
type AppcastSummary (line 23) | interface AppcastSummary {
type UpdaterStatus (line 30) | interface UpdaterStatus {
type OsStatus (line 37) | interface OsStatus {
type SubscriptionProductModel (line 64) | interface SubscriptionProductModel {
type TransportKind (line 72) | enum TransportKind {
type VpnStatus (line 77) | interface VpnStatus {
function getCityFromStatus (line 93) | function getCityFromStatus(status: VpnStatus): ExitSelectorCity | undefi...
function getCityFromArgs (line 98) | function getCityFromArgs(exitSelector: ExitSelector | undefined): ExitSe...
function getTunnelArgs (line 102) | function getTunnelArgs(status: VpnStatus): TunnelArgs | undefined {
type PinnedLocation (line 106) | interface PinnedLocation {
type AccountStatus (line 114) | interface AccountStatus {
type KnownFeatureFlagKey (line 120) | enum KnownFeatureFlagKey {
type FeatureFlagKey (line 127) | type FeatureFlagKey = KnownFeatureFlagKey | string;
type FeatureFlagValue (line 129) | type FeatureFlagValue = boolean | null;
function featureFlagEnabled (line 131) | function featureFlagEnabled(value: FeatureFlagValue | undefined): boolean {
type DNSContentBlock (line 135) | interface DNSContentBlock {
type AppStatus (line 144) | interface AppStatus {
type IAppContext (line 160) | interface IAppContext {
type ConnectionInProgress (line 179) | enum ConnectionInProgress {
function useIsConnecting (line 191) | function useIsConnecting() {
function useIsTransitioning (line 199) | function useIsTransitioning() {
function isConnecting (line 208) | function isConnecting(connectionInProgress: ConnectionInProgress) {
function connectionIsIdle (line 218) | function connectionIsIdle(connectionInProgress: ConnectionInProgress, vp...
FILE: obscura-ui/src/common/debuggingArchiveHook.tsx
type ArchiveState (line 10) | type ArchiveState = { inProgress: boolean, error?: Error };
function useDebuggingArchive (line 12) | function useDebuggingArchive(): (userFeedback: string) => Promise<void> {
FILE: obscura-ui/src/common/exitUtils.ts
function getCountryFlag (line 6) | function getCountryFlag(countryCode: string): string {
function getExitCountryFlag (line 16) | function getExitCountryFlag(exit: Exit): string {
function exitsSortComparator (line 21) | function exitsSortComparator(left: Exit, right: Exit): number {
function continentCmp (line 44) | function continentCmp(left: string, right: string): number {
function exitLocation (line 48) | function exitLocation(exit: Exit): PinnedLocation {
function exitCityEquals (line 57) | function exitCityEquals(left?: ExitSelectorCity, right?: ExitSelectorCit...
FILE: obscura-ui/src/common/fmt.ts
function fmt (line 1) | function fmt(lits: TemplateStringsArray, ...values: unknown[]): string {
class InterpolatedError (line 39) | class InterpolatedError extends Error {
method constructor (line 40) | constructor(
function err (line 48) | function err(lits: TemplateStringsArray, ...values: unknown[]): Error {
FILE: obscura-ui/src/common/links.ts
constant EMAIL (line 1) | const EMAIL = 'support@obscura.net';
constant DISCORD_SERVER (line 2) | const DISCORD_SERVER = 'https://discord.gg/xsP2Fp7s6r';
constant MATRIX_SERVER (line 3) | const MATRIX_SERVER = 'https://matrix.to/#/!CznDYbvmUUGxsJaWuW:matrix.so...
constant TWITTER (line 4) | const TWITTER = 'https://x.com/obscuravpn';
FILE: obscura-ui/src/common/localStorage.ts
type LocalStorageKey (line 1) | enum LocalStorageKey {
function getCustomApiUrls (line 5) | function getCustomApiUrls(): string[] {
function setCustomApiUrls (line 10) | function setCustomApiUrls(customApiUrls: string[]): string | null {
function localStorageGet (line 14) | function localStorageGet(key: LocalStorageKey): string | null {
function localStorageSet (line 18) | function localStorageSet(key: LocalStorageKey, value: string): string | ...
function localStorageRemove (line 24) | function localStorageRemove(key: LocalStorageKey): string | null {
FILE: obscura-ui/src/common/notifIds.ts
type NotificationId (line 1) | enum NotificationId {
FILE: obscura-ui/src/common/useAsync.ts
constant NEVER_LOADED (line 5) | const NEVER_LOADED = 0;
type UseAsyncArgs (line 7) | interface UseAsyncArgs<T> {
type RefreshCallback (line 14) | type RefreshCallback<T> = (value?: T, error?: unknown) => void;
class UseAsyncState (line 16) | class UseAsyncState<T> {
method setValue (line 28) | setValue(version: number, value: T, callbacks?: RefreshCallback<T>[]):...
method setError (line 49) | setError(version: number, error: unknown, callbacks?: RefreshCallback<...
type UseAsyncResult (line 70) | interface UseAsyncResult<T> {
function useAsync (line 108) | function useAsync<T>({
FILE: obscura-ui/src/common/useExitList.ts
type UseExitListArgs (line 5) | interface UseExitListArgs {
type UseExitListResult (line 9) | interface UseExitListResult {
constant EXIT_WATCHABLE (line 14) | const EXIT_WATCHABLE = makeWatchable(refreshExitList, getExitList);
function useExitList (line 16) | function useExitList({
FILE: obscura-ui/src/common/useLoadable.ts
type UseLoadableArgs (line 4) | interface UseLoadableArgs<T> extends UseAsyncArgs<T> {
function useLoadable (line 8) | function useLoadable<T>({
FILE: obscura-ui/src/common/useMailto.ts
function useMailto (line 8) | function useMailto(osStatus: OsStatus) {
FILE: obscura-ui/src/common/useSharedWatchable.ts
type Versioned (line 4) | interface Versioned {
type SharedWatchable (line 8) | interface SharedWatchable<T extends Versioned> {
function makeWatchable (line 19) | function makeWatchable<T extends Versioned>(
type UseWatchableResult (line 35) | interface UseWatchableResult<T> {
function useSharedWatchable (line 40) | function useSharedWatchable<T extends Versioned>(
FILE: obscura-ui/src/common/utils.ts
constant HEADER_TITLE (line 8) | const HEADER_TITLE = 'Obscura VPN';
constant IS_DEVELOPMENT (line 9) | const IS_DEVELOPMENT = import.meta.env.MODE === 'development';
constant MIN_LOAD_MS (line 10) | const MIN_LOAD_MS = 400;
function useCookie (line 12) | function useCookie(key: string, defaultValue: string, options: Cookies.C...
function notify (line 25) | function notify(title: string, body?: string) {
function sleep (line 29) | function sleep(ms: number) {
function downloadFile (line 33) | function downloadFile(filename: string, content: BlobPart, contentType =...
function isPromise (line 42) | function isPromise(v: unknown): v is PromiseLike<unknown> {
function arraysEqual (line 46) | function arraysEqual<T>(a: T[], b: T[]) {
function useLocalForage (line 63) | function useLocalForage<T>(key: string, defaultValue: T) {
function getLatestState (line 96) | function getLatestState<S>(setter: Dispatch<SetStateAction<S>>) {
function percentEncodeQuery (line 105) | function percentEncodeQuery(params: Record<string, string>) {
constant DEFAULT_ERROR_MSG (line 111) | const DEFAULT_ERROR_MSG = "An unexpected error has occurred.";
function errMsg (line 113) | function errMsg(error: unknown): string {
function normalizeError (line 121) | function normalizeError(error: unknown): Error {
function multiRef (line 131) | function multiRef<T>(...refs: ForwardedRef<T>[]): RefCallback<T> {
function randomChoice (line 145) | function randomChoice<T>(arr: T[]): T {
function fmtTime (line 155) | function fmtTime(ms: number) {
function zeroPad (line 163) | function zeroPad(num: number, width: number) {
function usePrimaryColorResolved (line 167) | function usePrimaryColorResolved() {
function normalizeString (line 173) | function normalizeString(str: string): string {
function normalizedIncludes (line 183) | function normalizedIncludes(needle: string, haystack: string) {
FILE: obscura-ui/src/components/AccountNumberSection.tsx
function AccountNumberSection (line 13) | function AccountNumberSection({ accountId, logOut }: { accountId: Obscur...
FILE: obscura-ui/src/components/AnimatedChevron.tsx
function AnimatedChevron (line 3) | function AnimatedChevron({ rotated }: { rotated: Boolean }) {
FILE: obscura-ui/src/components/BoltBadgeAuto.tsx
function BoltBadgeAuto (line 3) | function BoltBadgeAuto({ height = '1.25em', fill = 'white' }) {
FILE: obscura-ui/src/components/ButtonLink.tsx
type ButtonLinkProps (line 5) | interface ButtonLinkProps extends PropsWithChildren {
function ButtonLink (line 13) | function ButtonLink({ children, href, onClick, variant, inline = false, ...
FILE: obscura-ui/src/components/CachedColorScheme.tsx
function CachedColorScheme (line 12) | function CachedColorScheme({ children }: PropsWithChildren) {
FILE: obscura-ui/src/components/ConfirmationDialog.tsx
type ConfirmationDialogProps (line 7) | interface ConfirmationDialogProps extends PropsWithChildren {
function ConfirmationDialog (line 18) | function ConfirmationDialog({ opened, onClose, drawerSize = 'xs', title,...
type MobileDrawerProps (line 47) | type MobileDrawerProps = Omit<DrawerProps, 'classNames' | 'styles' | 'po...
function MobileDrawer (line 49) | function MobileDrawer({ size, title, opened, onClose, children, withClos...
FILE: obscura-ui/src/components/DebuggingArchive.tsx
constant ICON_SIZE (line 19) | const ICON_SIZE = 20;
type DebuggingArchiveVariant (line 21) | enum DebuggingArchiveVariant {
function DebuggingArchive (line 27) | function DebuggingArchive({ osStatus, variant = DebuggingArchiveVariant....
type SupportMessageProps (line 182) | interface SupportMessageProps {
function SupportMessage (line 188) | function SupportMessage({ osStatus, size, color }: SupportMessageProps) {
type ArchiveActionButtonsProps (line 197) | interface ArchiveActionButtonsProps {
function ArchiveActionButtons (line 202) | function ArchiveActionButtons({ osStatus, inProgress = false }: ArchiveA...
FILE: obscura-ui/src/components/DevSendCommand.tsx
function DevSendCommand (line 5) | function DevSendCommand() {
FILE: obscura-ui/src/components/DevSetApiUrl.tsx
function DevSetApiUrl (line 10) | function DevSetApiUrl() {
FILE: obscura-ui/src/components/Licenses.tsx
type Licenses (line 10) | interface Licenses {
type LicenseCommon (line 15) | interface LicenseCommon {
type License (line 20) | interface License extends LicenseCommon {
type LicenseSet (line 25) | interface LicenseSet extends LicenseCommon {
type UsedBy (line 29) | interface UsedBy {
function Licenses (line 35) | function Licenses(): React.ReactNode {
FILE: obscura-ui/src/components/Mantine.tsx
function Mantine (line 17) | function Mantine({ children }: PropsWithChildren) {
FILE: obscura-ui/src/components/ObscuraChip.tsx
function ObscuraChip (line 8) | function ObscuraChip({ children }: PropsWithChildren) {
FILE: obscura-ui/src/components/ObscuraWordmark.tsx
function ObscuraWordmark (line 4) | function ObscuraWordmark() {
FILE: obscura-ui/src/components/PaymentManagementSheet.tsx
type PaymentManagementSheetProps (line 16) | interface PaymentManagementSheetProps {
function PaymentManagementSheet (line 21) | function PaymentManagementSheet({ opened, onClose }: PaymentManagementSh...
function ProcessingPaymentSheet (line 79) | function ProcessingPaymentSheet({ opened }: { opened: boolean }) {
type AccountInfoOverviewProps (line 101) | interface AccountInfoOverviewProps {
function AccountInfoOverview (line 105) | function AccountInfoOverview({ accountInfo }: AccountInfoOverviewProps) {
function InfoRow (line 122) | function InfoRow({ title, importance, dataBolded, data, dataColor }: Row...
type Importance (line 144) | type Importance = 'high' | 'medium' | 'low';
type RowProps (line 146) | interface RowProps {
type Section (line 154) | type Section = React.ReactElement[];
type SubscriptionProductCardProps (line 156) | interface SubscriptionProductCardProps {
function AppleSubscriptionProductCard (line 161) | function AppleSubscriptionProductCard({ product, subscribed }: Subscript...
function AndroidSubscriptionProductCard (line 230) | function AndroidSubscriptionProductCard() {
function showErrorNotification (line 258) | function showErrorNotification(t: TFunction, e: unknown) {
function useBuildSections (line 269) | function useBuildSections(accountInfo: AccountInfo): Section[] {
function getStripeStatusColor (line 334) | function getStripeStatusColor(status: SubscriptionStatus): string {
function getAppleSubscriptionStatusColor (line 352) | function getAppleSubscriptionStatusColor(status: AppleSubscriptionStatus...
function appleStatusToTranslationKey (line 367) | function appleStatusToTranslationKey(status: AppleSubscriptionStatus): T...
FILE: obscura-ui/src/components/ScrollableView.tsx
function ScrollableView (line 8) | function ScrollableView({ children }: PropsWithChildren) {
function ScrollToTop (line 19) | function ScrollToTop() {
FILE: obscura-ui/src/components/SecondaryButton.tsx
function SecondaryButton (line 5) | function SecondaryButton({ children, onClick }: PropsWithChildren & { on...
FILE: obscura-ui/src/components/SelectCreatable.tsx
type Choice (line 4) | interface Choice {
function SelectCreatable (line 9) | function SelectCreatable({ defaultValue, choices, onSubmit, inputBasePro...
FILE: obscura-ui/src/components/Socials.tsx
function Socials (line 7) | function Socials() {
FILE: obscura-ui/src/components/VpnErrorFmt.tsx
function VpnError (line 4) | function VpnError({ errorEnum }: { errorEnum: string }) {
FILE: obscura-ui/src/translations/i18n.ts
type TranslationKey (line 7) | type TranslationKey = keyof typeof en;
function fmtErrorI18n (line 35) | function fmtErrorI18n(t: TFunction, error: CommandError): string {
FILE: obscura-ui/src/translations/i18next.d.ts
type CustomTypeOptions (line 4) | interface CustomTypeOptions {
FILE: obscura-ui/src/views/About.tsx
function About (line 20) | function About() {
type UpdaterErrorProps (line 105) | interface UpdaterErrorProps {
function errorCodeIsLatestVersion (line 110) | function errorCodeIsLatestVersion(errorcode: number | undefined) {
function UpdaterError (line 114) | function UpdaterError({ errorCode, error }: UpdaterErrorProps) {
FILE: obscura-ui/src/views/AccountView.tsx
function Account (line 31) | function Account() {
type AccountStatusProps (line 105) | interface AccountStatusProps {
function AccountStatusCard (line 109) | function AccountStatusCard() {
function AccountInfoUnavailable (line 136) | function AccountInfoUnavailable() {
function AccountPaidUpSubscriptionActive (line 151) | function AccountPaidUpSubscriptionActive({ accountInfo }: AccountStatusP...
function SubscriptionActive (line 164) | function SubscriptionActive({ accountInfo }: AccountStatusProps) {
function SubscriptionPaused (line 182) | function SubscriptionPaused({ accountInfo }: AccountStatusProps) {
function AccountExpired (line 195) | function AccountExpired() {
function AccountPaidUp (line 206) | function AccountPaidUp({ accountInfo }: AccountStatusProps) {
function AccountExpiringSoon (line 224) | function AccountExpiringSoon({ accountInfo }: AccountStatusProps) {
type ExpiryMessages (line 251) | interface ExpiryMessages {
function useExpiryMessages (line 256) | function useExpiryMessages(
type AccountStatusCardTemplateProps (line 297) | interface AccountStatusCardTemplateProps {
function AccountStatusCardTemplate (line 303) | function AccountStatusCardTemplate({
function AccountRefreshButton (line 337) | function AccountRefreshButton({ smallerSize = false }: { smallerSize?: b...
function WGConfigurations (line 374) | function WGConfigurations() {
function DeleteAccount (line 388) | function DeleteAccount({ onClick }: { onClick: () => void }) {
function ManagePaymentsButton (line 400) | function ManagePaymentsButton({ mobile = false }: { mobile?: boolean }) {
function MobileLogOut (line 421) | function MobileLogOut({ logOut }: { logOut: () => void }) {
FILE: obscura-ui/src/views/ConnectionView.tsx
constant BUTTON_WIDTH (line 47) | const BUTTON_WIDTH = 320;
function Connection (line 49) | function Connection() {
function PrimaryConnectButton (line 140) | function PrimaryConnectButton() {
function ConnectionProgressBar (line 178) | function ConnectionProgressBar() {
type PulsingProgressProps (line 250) | interface PulsingProgressProps extends ProgressRootProps {
function usePulsingProgress (line 255) | function usePulsingProgress({ activated, bars = 2, w }: PulsingProgressP...
constant DECO_CONNECTING_ARRAY (line 297) | const DECO_CONNECTING_ARRAY = {
constant DEC_LAST_IDX (line 301) | const DEC_LAST_IDX = DECO_CONNECTING_ARRAY.light.length - 1;
function Deco (line 303) | function Deco() {
constant MASCOT_CONNECTING (line 346) | const MASCOT_CONNECTING = [
function Mascot (line 359) | function Mascot() {
function LocationSelect (line 418) | function LocationSelect(): ReactNode {
function LocationConnectTopCaption (line 567) | function LocationConnectTopCaption() {
type LocationConnectRightButtonProps (line 579) | interface LocationConnectRightButtonProps {
function LocationConnectRightButton (line 584) | function LocationConnectRightButton({ dropdownOpened, selectedCity }: Lo...
type CityOptionsProps (line 612) | interface CityOptionsProps {
type ItemRightSectionProps (line 619) | interface ItemRightSectionProps {
function CityOptions (line 625) | function CityOptions({ locations, pinnedLocationSet, lastChosenExit, onE...
FILE: obscura-ui/src/views/DeveloperView.tsx
function DeveloperViewer (line 15) | function DeveloperViewer() {
FILE: obscura-ui/src/views/HelpView.tsx
function Help (line 11) | function Help() {
FILE: obscura-ui/src/views/LocationView.tsx
function LocationView (line 31) | function LocationView() {
type LocationCarProps (line 210) | interface LocationCarProps {
function LocationCard (line 219) | function LocationCard({ exit, connected, showLastChosen = false, onSelec...
function NoExitServers (line 261) | function NoExitServers() {
function VpnStatusCard (line 306) | function VpnStatusCard() {
function CurrentSession (line 404) | function CurrentSession() {
function ExitInfo (line 422) | function ExitInfo({ exitPubKey, connectedExit }: { exitPubKey: string, c...
type ExitInfoProps (line 439) | interface ExitInfoProps {
function ExitInfoCollapse (line 448) | function ExitInfoCollapse({ exitProviderId, exitPubKey, connectedExit, p...
function ExitInfoDrawer (line 490) | function ExitInfoDrawer({ exitProviderId, exitPubKey, connectedExit, pro...
type MatchesPublicKeyProp (line 523) | interface MatchesPublicKeyProp {
function MatchesPublicKey (line 528) | function MatchesPublicKey({ exitProviderId, exitProviderURL }: MatchesPu...
FILE: obscura-ui/src/views/LogInView.tsx
type LogInProps (line 24) | interface LogInProps {
constant COPY_ACCOUNT_WIDTH (line 29) | const COPY_ACCOUNT_WIDTH = IS_HANDHELD_DEVICE ? 300 : '24ch';
constant BACKGROUND_IMAGE (line 30) | const BACKGROUND_IMAGE = IS_HANDHELD_DEVICE ? DecoOrangeBottom : DecoOra...
constant BACKGROUND_POSITION (line 31) | const BACKGROUND_POSITION = IS_HANDHELD_DEVICE ? 'bottom' : 'top';
constant TOP_SPACING (line 32) | const TOP_SPACING = IS_HANDHELD_DEVICE ? '16vh' : '28vh';
function LogIn (line 34) | function LogIn({ accountNumber, accountActive }: LogInProps) {
constant SPINNING_DURATION (line 150) | const SPINNING_DURATION = 900;
constant ANIMATION_HEIGHT (line 151) | const ANIMATION_HEIGHT = 20;
type AccountGenerationProps (line 153) | interface AccountGenerationProps {
function AccountGeneration (line 159) | function AccountGeneration({ generatedAccountId, accountActive, loading ...
function AccountId (line 280) | function AccountId({ accountId }: { accountId: ObscuraAccount.AccountId ...
function DigitsWheel (line 300) | function DigitsWheel({ digit }: { digit: string }) {
type DigitProps (line 317) | interface DigitProps {
function Digit (line 322) | function Digit({ mv, number }: DigitProps) {
FILE: obscura-ui/src/views/Settings.tsx
constant APPLE_PLATFORMS (line 17) | const APPLE_PLATFORMS = new Set([Platform.macOS, Platform.iOS]);
constant IS_APPLE (line 18) | const IS_APPLE = APPLE_PLATFORMS.has(PLATFORM);
constant CUSTOM_DNS_PLATFORMS_EXCLUDED (line 20) | const CUSTOM_DNS_PLATFORMS_EXCLUDED = new Set([Platform.Android]);
constant CUSTOM_DNS_SUPPORTED (line 21) | const CUSTOM_DNS_SUPPORTED = !CUSTOM_DNS_PLATFORMS_EXCLUDED.has(PLATFORM);
function Settings (line 23) | function Settings() {
function DnsSettings (line 35) | function DnsSettings() {
function GeneralSettings (line 79) | function GeneralSettings() {
function NetworkSettings (line 137) | function NetworkSettings() {
function ExperimentalSettings (line 172) | function ExperimentalSettings() {
function AppearanceSettings (line 203) | function AppearanceSettings() {
function StrictLeakPreventionSwitch (line 241) | function StrictLeakPreventionSwitch() {
function FeatureFlagToggle (line 274) | function FeatureFlagToggle({ featureFlagKey }: { featureFlagKey: Feature...
constant FEATURE_FLAG_CUSTOM_UI (line 304) | const FEATURE_FLAG_CUSTOM_UI: Partial<Record<FeatureFlagKey, (t: ReturnT...
FILE: obscura-ui/src/views/SplashScreen.tsx
type SplashScreenProps (line 9) | interface SplashScreenProps {
function SplashScreen (line 14) | function SplashScreen({ text = '', osStatus }: SplashScreenProps) {
FILE: obscura-ui/src/views/render-fallbacks/FallbackAppRender.tsx
function FallbackAppRender (line 5) | function FallbackAppRender({ error, resetErrorBoundary }: FallbackProps) {
FILE: obscura-ui/vite-env.d.ts
type Window (line 1) | interface Window {
FILE: rustlib/build.rs
constant OUTPUT_HEADER_PATH_ENVVAR (line 7) | const OUTPUT_HEADER_PATH_ENVVAR: &str = "OBSCURA_CLIENT_RUSTLIB_CBINDGEN...
constant CBINDGEN_CONFIG_PATH_ENVVAR (line 8) | const CBINDGEN_CONFIG_PATH_ENVVAR: &str = "OBSCURA_CLIENT_RUSTLIB_CBINDG...
function main (line 10) | fn main() {
function copy_to_bin_dir (line 64) | fn copy_to_bin_dir(src: &Path, file_name: &str) {
function emit_wintun_dll_hash (line 82) | fn emit_wintun_dll_hash(dll_path: &Path) {
constant WINTUN_VERSION (line 91) | const WINTUN_VERSION: &str = "0.14.1";
function get_wintun_dll_src (line 94) | fn get_wintun_dll_src(manifest_dir: &String) -> PathBuf {
FILE: rustlib/examples/connect.rs
type Args (line 13) | struct Args {
function main (line 23) | async fn main() -> Result<(), Box<dyn std::error::Error>> {
FILE: rustlib/examples/list-relay-rtts.rs
type Args (line 11) | struct Args {
function main (line 19) | async fn main() -> Result<(), Box<dyn std::error::Error>> {
FILE: rustlib/src/android/class_cache.rs
type ClassCache (line 8) | pub struct ClassCache {
method new (line 18) | pub fn new(env: &mut JNIEnv) -> anyhow::Result<Arc<Self>> {
method ffi_handle (line 28) | pub fn ffi_handle(&self) -> &JClass<'static> {
method error_code_exception (line 32) | pub fn error_code_exception(&self) -> &JClass<'static> {
method vpn_service (line 36) | pub fn vpn_service(&self) -> &JClass<'static> {
FILE: rustlib/src/android/ffi.rs
type Global (line 22) | pub struct Global {
function global_from_handle (line 34) | fn global_from_handle(_handle: &JObject) -> &'static Global {
constant RUST_LOG_DIR_NAME (line 38) | const RUST_LOG_DIR_NAME: &str = "rust-log";
function JNI_OnLoad (line 42) | pub extern "C" fn JNI_OnLoad(_vm: *mut jni::sys::JavaVM, _reserved: *mut...
function initialize (line 46) | fn initialize(env: &mut JNIEnv, j_config_dir: &JString, j_user_agent: &J...
function Java_net_obscura_vpnclientapp_client_ObscuraLibrary_initialize (line 73) | pub extern "C" fn Java_net_obscura_vpnclientapp_client_ObscuraLibrary_in...
function json_ffi (line 96) | fn json_ffi(global: &'static Global, env: &mut JNIEnv, j_json_cmd: &JStr...
function Java_net_obscura_vpnclientapp_client_ObscuraLibrary_jsonFfi (line 130) | pub extern "C" fn Java_net_obscura_vpnclientapp_client_ObscuraLibrary_js...
function set_network_interface (line 144) | fn set_network_interface(env: &mut JNIEnv, global: &'static Global, j_na...
function Java_net_obscura_vpnclientapp_client_ObscuraLibrary_setNetworkInterface (line 156) | pub extern "C" fn Java_net_obscura_vpnclientapp_client_ObscuraLibrary_se...
function Java_net_obscura_vpnclientapp_client_ObscuraLibrary_unsetNetworkInterface (line 171) | pub extern "C" fn Java_net_obscura_vpnclientapp_client_ObscuraLibrary_un...
function forward_log (line 179) | fn forward_log(
function Java_net_obscura_vpnclientapp_client_ObscuraLibrary_forwardLog (line 209) | pub extern "C" fn Java_net_obscura_vpnclientapp_client_ObscuraLibrary_fo...
function call_set_network_config (line 223) | pub(super) async fn call_set_network_config(class_cache: Arc<ClassCache>...
FILE: rustlib/src/android/future.rs
function signal_json_ffi_future (line 9) | pub fn signal_json_ffi_future(
FILE: rustlib/src/android/os_impl.rs
type AndroidOsImpl (line 9) | pub struct AndroidOsImpl {
method new (line 17) | pub fn new(jvm: Arc<JavaVM>, class_cache: Arc<ClassCache>) -> Self {
method set_network_interface (line 21) | pub fn set_network_interface(&self, network_interface: Option<NetworkI...
method network_interface (line 27) | fn network_interface(&self) -> watch::Receiver<Option<NetworkInterface>> {
method set_os_network_config (line 31) | async fn set_os_network_config(&self, network_config: OsNetworkConfig, t...
method unset_os_network_config (line 56) | async fn unset_os_network_config(&self) -> Result<(), ()> {
method packet_for_os (line 61) | fn packet_for_os(&self, packet: Bytes) {
FILE: rustlib/src/android/tunnel.rs
type Tun (line 9) | pub struct Tun {
method spawn (line 15) | pub fn spawn(fd: OwnedFd, tunnel: QuicWgConnPacketSender) -> Result<Se...
method write (line 52) | pub fn write(&self, packet: &[u8]) {
FILE: rustlib/src/android/util.rs
function throw_runtime_exception (line 6) | pub fn throw_runtime_exception(env: &mut JNIEnv, msg: impl Display) {
type Utf8JavaStr (line 14) | pub struct Utf8JavaStr<'a, 'b> {
function new (line 22) | pub fn new(env: &mut JNIEnv<'b>, obj: &'a JString<'a>, name: &str, messa...
function from_nullable (line 54) | pub fn from_nullable(env: &mut JNIEnv<'b>, obj: &'a JString<'a>, name: &...
function as_str (line 58) | pub fn as_str(&self) -> &str {
function as_path (line 62) | pub fn as_path(&self) -> &Utf8Path {
method drop (line 68) | fn drop(&mut self) {
function fmt (line 77) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
FILE: rustlib/src/apple/ffi.rs
function initialize_apple_system_logging (line 25) | pub extern "C" fn initialize_apple_system_logging(log_dir: FfiStr) -> *m...
type Global (line 36) | pub struct Global {
function initialize (line 49) | pub unsafe extern "C" fn initialize(
type LogLevel (line 104) | pub enum LogLevel {
function forward_log (line 113) | pub extern "C" fn forward_log(level: LogLevel, message: FfiStr, file_id:...
function send_packet (line 131) | pub unsafe extern "C" fn send_packet(global: *const Global, packet: FfiB...
function set_network_interface (line 146) | pub unsafe extern "C" fn set_network_interface(global: *const Global, in...
function wake (line 165) | pub unsafe extern "C" fn wake(global: *const Global) {
function json_ffi_cmd (line 175) | pub unsafe extern "C" fn json_ffi_cmd(
FILE: rustlib/src/apple/os_impl.rs
type SetNetworkConfigCb (line 13) | pub type SetNetworkConfigCb =
function set_network_config_done (line 22) | pub extern "C" fn set_network_config_done(context: *mut c_void, success:...
type AppleOsImpl (line 28) | pub struct AppleOsImpl {
method new (line 36) | pub fn new(receive_cb: extern "C" fn(FfiBytes), set_network_config_cb:...
method set_network_interface (line 45) | pub fn set_network_interface(&self, network_interface: Option<NetworkI...
method send_packet (line 49) | pub fn send_packet(&self, packet: &[u8]) {
method network_interface (line 55) | fn network_interface(&self) -> watch::Receiver<Option<NetworkInterface>> {
method set_os_network_config (line 59) | async fn set_os_network_config(&self, network_config: OsNetworkConfig, t...
method unset_os_network_config (line 83) | async fn unset_os_network_config(&self) -> Result<(), ()> {
method packet_for_os (line 88) | fn packet_for_os(&self, packet: Bytes) {
FILE: rustlib/src/backoff.rs
type Backoff (line 7) | pub struct Backoff {
constant BACKGROUND (line 13) | pub const BACKGROUND: Self = Backoff { base: Duration::from_secs(1), m...
method take (line 17) | pub fn take(&self, attempts: usize) -> BackoffIter {
type BackoffIter (line 22) | pub struct BackoffIter {
method wait (line 29) | pub async fn wait(&mut self) -> bool {
type Item (line 41) | type Item = Duration;
method next (line 43) | fn next(&mut self) -> Option<Self::Item> {
method size_hint (line 59) | fn size_hint(&self) -> (usize, Option<usize>) {
FILE: rustlib/src/backoff_test.rs
function test (line 6) | fn test() {
FILE: rustlib/src/bin/obscura/add_operator.rs
function run_add_operator (line 4) | pub async fn run_add_operator(users: Vec<String>) -> ! {
function add_operator_impl (line 8) | fn add_operator_impl(mut users: Vec<String>) -> ! {
FILE: rustlib/src/bin/obscura/client/client_error.rs
type ClientError (line 4) | pub enum ClientError {
method from (line 18) | fn from(error: ManagerCmdErrorCode) -> ClientError {
FILE: rustlib/src/bin/obscura/client/ipc.rs
function run_command (line 14) | pub async fn run_command<O: DeserializeOwned>(cmd: ManagerCmd) -> Result...
function ipc_test (line 62) | pub async fn ipc_test(_: ClientIpcTestArgs) -> Result<(), ClientError> {
function try_group_refresh_fix (line 71) | pub async fn try_group_refresh_fix() {
function build_sg_exec_cmd (line 192) | fn build_sg_exec_cmd<'a>(
FILE: rustlib/src/bin/obscura/client/mod.rs
function run (line 14) | pub async fn run(global_args: GlobalArgs, cmd: ClientCommand) -> Result<...
function status (line 30) | async fn status(args: ClientStatusArgs) -> Result<(), ClientError> {
function login (line 58) | async fn login(args: ClientLoginArgs) -> Result<(), ClientError> {
function go_to_target_state (line 68) | async fn go_to_target_state(target_state: Option<TunnelArgs>) -> Result<...
function vpn_status_summary (line 86) | fn vpn_status_summary(vpn_status: &VpnStatus) -> String {
function account_info_summary (line 102) | fn account_info_summary(account_info: &AccountInfo) -> String {
FILE: rustlib/src/bin/obscura/main.rs
function get_data_dir (line 15) | fn get_data_dir() -> String {
function get_data_dir (line 20) | fn get_data_dir() -> String {
type ServiceArgs (line 31) | pub struct ServiceArgs {
type ClientLoginArgs (line 40) | pub struct ClientLoginArgs {
type ClientStartArgs (line 49) | pub struct ClientStartArgs {}
type ClientStopArgs (line 52) | pub struct ClientStopArgs {}
type ClientStatusArgs (line 55) | pub struct ClientStatusArgs {
type ClientIpcTestArgs (line 65) | pub struct ClientIpcTestArgs {}
type ClientCommand (line 68) | pub enum ClientCommand {
type Command (line 77) | pub enum Command {
type Cli (line 93) | pub struct Cli {
type GlobalArgs (line 101) | pub struct GlobalArgs {
function main (line 107) | async fn main() {
function run_service (line 131) | async fn run_service(args: ServiceArgs) -> ! {
function run_service (line 138) | async fn run_service(_args: ServiceArgs) -> ! {
function run_client (line 144) | async fn run_client(global_args: GlobalArgs, args: ClientCommand) {
function run_client (line 152) | async fn run_client(_global_args: GlobalArgs, _args: ClientCommand) {
FILE: rustlib/src/bin/obscura/service/mod.rs
function run (line 11) | pub async fn run(args: ServiceArgs) -> Result<Infallible, Box<dyn Error>> {
FILE: rustlib/src/bin/obscura/service/os/linux/dns/mod.rs
type DnsManagerArg (line 7) | pub enum DnsManagerArg {
type DnsManager (line 15) | pub enum DnsManager {
function choose_dns_manager (line 21) | pub async fn choose_dns_manager(dns_manager_arg: DnsManagerArg) -> Resul...
FILE: rustlib/src/bin/obscura/service/os/linux/dns/resolved.rs
function zbus_connect (line 5) | async fn zbus_connect() -> Result<zbus_systemd::resolve1::ManagerProxy<'...
function detect (line 16) | pub async fn detect() -> bool {
function set_dns (line 32) | pub async fn set_dns(tun: &NetworkInterface, dns: &[IpAddr]) -> Result<(...
function reset_dns (line 54) | pub async fn reset_dns(tun: &NetworkInterface) -> Result<(), ()> {
FILE: rustlib/src/bin/obscura/service/os/linux/ipc.rs
constant SOCKET_PATH (line 9) | pub const SOCKET_PATH: &str = "/run/obscura.sock";
type ServiceIpc (line 11) | pub struct ServiceIpc {
method new (line 16) | pub async fn new(_lock: &ServiceLock) -> Result<Self, LinuxServiceStar...
method next (line 54) | pub async fn next(&self) -> (Vec<u8>, Box<dyn FnOnce(Vec<u8>) + Send>) {
method handle_stream (line 58) | async fn handle_stream(mut stream: UnixStream, sender: Sender<(Vec<u8>...
FILE: rustlib/src/bin/obscura/service/os/linux/mod.rs
type LinuxOsImpl (line 24) | pub struct LinuxOsImpl {
method new (line 34) | pub async fn new(dns_manager_arg: DnsManagerArg) -> Result<Self, Linux...
method next_manager_command (line 106) | pub async fn next_manager_command(&self) -> (ManagerCmd, Box<dyn FnOnc...
method network_interface (line 52) | fn network_interface(&self) -> Receiver<Option<NetworkInterface>> {
method set_os_network_config (line 56) | async fn set_os_network_config(&self, network_config: OsNetworkConfig, t...
method unset_os_network_config (line 82) | async fn unset_os_network_config(&self) -> Result<(), ()> {
method packet_for_os (line 99) | fn packet_for_os(&self, packet: Bytes) {
constant JSON_OTHER_ERROR (line 126) | const JSON_OTHER_ERROR: &str = r#"{"Err":"other"}"#;
function test_other_error_json (line 129) | fn test_other_error_json() {
FILE: rustlib/src/bin/obscura/service/os/linux/network_manager.rs
constant MIN_VERSION (line 11) | const MIN_VERSION: Version = Version::new(1, 52, 1);
type NetworkManager (line 19) | trait NetworkManager {
method get_device_by_ip_iface (line 20) | fn get_device_by_ip_iface(&self, iface: &str) -> zbus::Result<zbus::zv...
method version (line 22) | fn version(&self) -> zbus::Result<String>;
type Device (line 27) | pub trait Device {
method get_applied_connection (line 28) | fn get_applied_connection(&self, flags: u32) -> zbus::Result<(HashMap<...
method reapply (line 29) | fn reapply(&self, connection: HashMap<String, HashMap<String, zbus::zv...
method state (line 31) | fn state(&self) -> zbus::Result<u32>;
function connect (line 35) | async fn connect() -> Result<(NetworkManagerProxy<'static>, Version), ()> {
function device_proxy (line 51) | pub async fn device_proxy(self, interface: &NetworkInterface) -> Result<...
function detect (line 73) | pub async fn detect() -> bool {
function set_dns_and_routes (line 81) | pub async fn set_dns_and_routes(tun: &NetworkInterface, network_config: ...
function reset_dns_and_routes (line 87) | pub async fn reset_dns_and_routes(tun: &NetworkInterface) -> Result<(), ...
function apply_device_settings (line 94) | async fn apply_device_settings(
function build_device_settings (line 124) | fn build_device_settings(
function ipv4_to_u32 (line 212) | fn ipv4_to_u32(ip: Ipv4Addr) -> u32 {
function route_to_dbus_hashmap (line 217) | fn route_to_dbus_hashmap(net: IpNetwork) -> Result<HashMap<String, zbus:...
FILE: rustlib/src/bin/obscura/service/os/linux/routes/netlink.rs
constant PREFERRED_INTERFACE_WATCH_ERROR_BACKOFF (line 21) | const PREFERRED_INTERFACE_WATCH_ERROR_BACKOFF: Duration = Duration::from...
function netlink_connect (line 23) | pub async fn netlink_connect() -> Result<rtnetlink::Handle, ()> {
function add_routes (line 35) | pub async fn add_routes(tun: &NetworkInterface, routes: &[IpNetwork]) ->...
function del_routes (line 46) | pub async fn del_routes(tun: &NetworkInterface, routes: &[IpNetwork]) ->...
function build_route_messages (line 72) | fn build_route_messages(tun: &NetworkInterface, routes: &[IpNetwork]) ->...
function watch_preferred_network_interface (line 85) | pub async fn watch_preferred_network_interface() -> Receiver<Option<Netw...
function watch_preferred_network_interface_one (line 104) | async fn watch_preferred_network_interface_one(sender: &Sender<Option<Ne...
function get_preferred_network_interface (line 133) | async fn get_preferred_network_interface(handle: &rtnetlink::Handle) -> ...
type DefaultRoute (line 176) | struct DefaultRoute {
FILE: rustlib/src/bin/obscura/service/os/linux/service_lock.rs
constant LOCK_PATH (line 5) | const LOCK_PATH: &str = "/run/obscura.lock";
type ServiceLock (line 7) | pub struct ServiceLock {
method new (line 12) | pub fn new() -> Result<Self, LinuxServiceStartError> {
FILE: rustlib/src/bin/obscura/service/os/linux/start_error.rs
type LinuxServiceStartError (line 2) | pub enum LinuxServiceStartError {
FILE: rustlib/src/bin/obscura/service/os/linux/tun.rs
constant TUN_MIN_LOG_SILENCE (line 16) | const TUN_MIN_LOG_SILENCE: Duration = Duration::from_secs(5);
constant TUN_NAME (line 17) | const TUN_NAME: &str = "obscuravpn";
type Tun (line 19) | pub struct Tun {
method create (line 26) | pub async fn create() -> anyhow::Result<Self> {
method interface (line 41) | pub fn interface(&self) -> NetworkInterface {
method send (line 45) | pub fn send(&self, packet: Bytes) {
method receive (line 56) | async fn receive(dev: &tun_rs::AsyncDevice, packet_buffer: &mut Packet...
method set_config (line 81) | pub fn set_config(&self, mtu: u16, ipv4: Ipv4Addr, ipv6: Ipv6Network) ...
method spawn_read_task (line 127) | pub fn spawn_read_task(&self, tunnel: QuicWgConnPacketSender) {
FILE: rustlib/src/bin/obscura/service/os/mod.rs
constant ROUTES (line 13) | pub const ROUTES: [IpNetwork; 4] = [
FILE: rustlib/src/bin/obscura/service/os/windows/adapters.rs
constant WATCH_ERROR_BACKOFF (line 18) | const WATCH_ERROR_BACKOFF: Duration = Duration::from_secs(1);
function watch_active_adapter (line 20) | pub fn watch_active_adapter() -> Receiver<Option<NetworkInterface>> {
function watch_active_adapter_thread (line 26) | fn watch_active_adapter_thread(sender: &Sender<Option<NetworkInterface>>) {
function watch_addr_changes (line 34) | fn watch_addr_changes(sender: &Sender<Option<NetworkInterface>>) {
type PhysicalAdapter (line 98) | pub struct PhysicalAdapter {
function find_active_physical_adapter (line 108) | pub fn find_active_physical_adapter() -> Result<Option<PhysicalAdapter>,...
function get_active_physical_adapter (line 182) | fn get_active_physical_adapter() -> Result<Option<NetworkInterface>, ()> {
function test_not_wsl_vethernet (line 204) | fn test_not_wsl_vethernet() {
FILE: rustlib/src/bin/obscura/service/os/windows/gaa.rs
type GAABufferInit (line 9) | pub struct GAABufferInit {
method new (line 15) | pub fn new() -> Result<Option<Self>, ()> {
FILE: rustlib/src/bin/obscura/service/os/windows/iphelper.rs
function add_routes (line 14) | pub fn add_routes(adapter: &wintun::Adapter) -> Result<(), ()> {
function remove_routes (line 26) | pub fn remove_routes(adapter: &wintun::Adapter) -> Result<(), ()> {
function build_forward_row (line 40) | fn build_forward_row(if_index: u32, dest: IpAddr, prefix_len: u8) -> MIB...
function add_route (line 64) | fn add_route(if_index: u32, route: &IpNetwork) -> Result<(), ()> {
function remove_route (line 85) | fn remove_route(if_index: u32, route: &IpNetwork) -> Result<(), ()> {
function make_sockaddr_in (line 106) | fn make_sockaddr_in(addr: Ipv4Addr) -> SOCKADDR_IN {
function make_sockaddr_in6 (line 114) | fn make_sockaddr_in6(addr: Ipv6Addr) -> SOCKADDR_IN6 {
function family_name (line 122) | fn family_name(family: ADDRESS_FAMILY) -> &'static str {
function set_metric (line 132) | fn set_metric(adapter: &wintun::Adapter, automatic: bool, metric: u32) -...
function set_low_metric (line 188) | pub fn set_low_metric(adapter: &wintun::Adapter) -> Result<(), ()> {
function reset_interface_metric (line 194) | pub fn reset_interface_metric(adapter: &wintun::Adapter) -> Result<(), (...
function get_system_directory (line 200) | fn get_system_directory() -> std::path::PathBuf {
function run_command (line 221) | async fn run_command(cmd: &mut tokio::process::Command, friendly_name: &...
function set_ipv4_address (line 235) | pub async fn set_ipv4_address(adapter: &wintun::Adapter, ipv4: Ipv4Addr)...
function set_ipv6_address (line 247) | pub async fn set_ipv6_address(adapter: &wintun::Adapter, ipv6: Ipv6Netwo...
function set_mtu (line 258) | pub async fn set_mtu(adapter: &wintun::Adapter, mtu: u16) -> Result<(), ...
function set_dns_servers (line 278) | pub async fn set_dns_servers(adapter: &wintun::Adapter, dns: &[IpAddr]) ...
function set_interface_dns_settings (line 288) | fn set_interface_dns_settings(interface: windows::core::GUID, dns: &[IpA...
function set_dns_servers_netsh (line 310) | async fn set_dns_servers_netsh(adapter: &wintun::Adapter, dns: &[IpAddr]...
function flush_dns_cache (line 348) | pub async fn flush_dns_cache() -> Result<(), ()> {
FILE: rustlib/src/bin/obscura/service/os/windows/mod.rs
type WindowsOsImpl (line 20) | pub struct WindowsOsImpl {
method new (line 27) | pub async fn new() -> Result<Self, WindowsServiceStartError> {
method next_manager_command (line 36) | pub async fn next_manager_command(&self) -> (ManagerCmd, Box<dyn FnOnc...
method network_interface (line 50) | fn network_interface(&self) -> Receiver<Option<NetworkInterface>> {
method set_os_network_config (line 54) | async fn set_os_network_config(&self, network_config: OsNetworkConfig, t...
method unset_os_network_config (line 64) | async fn unset_os_network_config(&self) -> Result<(), ()> {
method packet_for_os (line 69) | fn packet_for_os(&self, packet: Bytes) {
FILE: rustlib/src/bin/obscura/service/os/windows/nrpt.rs
constant NRPT_RULE_GUID (line 5) | const NRPT_RULE_GUID: &str = "fb157da8-6578-4f53-81ea-0a9168e96c1f";
constant NRPT_COMMENT_VALUE (line 7) | const NRPT_COMMENT_VALUE: &str = "Redirect all DNS queries to nameserver...
constant NRPT_DISPLAY_NAME_VALUE (line 8) | const NRPT_DISPLAY_NAME_VALUE: &str = "Obscura VPN Rule";
constant NRPT_RULES_PATH (line 10) | const NRPT_RULES_PATH: &str = r"SYSTEM\CurrentControlSet\Services\Dnscac...
function get_rule_path (line 12) | fn get_rule_path() -> String {
function set_nrpt_value (line 16) | fn set_nrpt_value(key: &RegKey, name: &str, value: &impl ToRegValue) -> ...
function create_rule (line 22) | pub fn create_rule(name_servers: &[IpAddr]) -> Result<(), ()> {
function delete_rules (line 59) | pub fn delete_rules() -> Result<bool, ()> {
function test_rule_path (line 77) | fn test_rule_path() {
FILE: rustlib/src/bin/obscura/service/os/windows/start_error.rs
type WindowsServiceStartError (line 4) | pub enum WindowsServiceStartError {
FILE: rustlib/src/bin/obscura/service/os/windows/tun.rs
constant WINTUN_DLL_SHA256 (line 22) | const WINTUN_DLL_SHA256: &str = env!("WINTUN_DLL_SHA256");
constant TUN_MIN_LOG_SILENCE (line 24) | const TUN_MIN_LOG_SILENCE: Duration = Duration::from_secs(5);
constant TUN_NAME (line 28) | const TUN_NAME: &str = "ObscuraVPN";
type Tun (line 30) | pub struct Tun {
method create (line 37) | pub async fn create() -> Result<Self, WindowsServiceStartError> {
method send (line 61) | pub fn send(&self, packet: Bytes) {
method put_packet_in_buffer (line 92) | fn put_packet_in_buffer(packet: wintun::Packet, buffer: &mut [u8]) -> ...
method receive (line 120) | async fn receive(session: &Arc<wintun::Session>, packet_buffer: &mut P...
method set_config (line 164) | pub async fn set_config(&self, mtu: u16, ipv4: Ipv4Addr, ipv6: Ipv6Net...
method spawn_read_task (line 187) | pub fn spawn_read_task(&self, tunnel: QuicWgConnPacketSender) {
method shutdown (line 199) | pub async fn shutdown(&self) -> Result<(), ()> {
function verify_and_load_wintun (line 212) | fn verify_and_load_wintun() -> Result<Wintun, WindowsServiceStartError> {
FILE: rustlib/src/cached_value.rs
type CachedValue (line 9) | pub struct CachedValue<T> {
FILE: rustlib/src/client_state.rs
type ClientStateHandle (line 50) | pub struct ClientStateHandle(Arc<Sender<ClientState>>);
method borrow (line 155) | pub fn borrow(&self) -> tokio::sync::watch::Ref<'_, ClientState> {
method subscribe (line 159) | pub fn subscribe(&self) -> Receiver<ClientState> {
method change_config (line 163) | fn change_config(&self, f: impl FnOnce(&mut Config)) {
method change (line 171) | fn change<T>(&self, f: impl FnOnce(&mut ClientState) -> T) -> T {
method set_account_id (line 180) | pub fn set_account_id(&self, account_id_and_auth_token: Option<(Accoun...
method get_exit_list (line 217) | pub fn get_exit_list(&self) -> Option<ConfigCached<Arc<ExitList>>> {
method set_pinned_exits (line 221) | pub fn set_pinned_exits(&self, pinned_locations: Vec<PinnedLocation>) {
method set_feature_flag (line 227) | pub fn set_feature_flag(&self, flag: &str, active: bool) {
method set_tunnel_target_state (line 233) | pub fn set_tunnel_target_state(&self, tunnel_args: Option<TunnelArgs>,...
method set_api_host_alternate (line 244) | pub fn set_api_host_alternate(&self, value: Option<String>) {
method set_sni_relay (line 259) | pub fn set_sni_relay(&self, value: Option<String>) {
method set_in_new_account_flow (line 271) | pub fn set_in_new_account_flow(&self, value: bool) {
method set_api_url (line 277) | pub fn set_api_url(&self, url: Option<String>) {
method set_dns_content_block (line 287) | pub fn set_dns_content_block(&self, value: DnsContentBlock) {
method set_network_interface (line 291) | pub fn set_network_interface(&self, network_interface: Option<NetworkI...
method set_auto_connect (line 328) | pub fn set_auto_connect(&self, enable: bool) {
method set_use_system_dns (line 334) | pub fn set_use_system_dns(&self, enable: bool) {
method connect (line 338) | pub async fn connect(
method choose_exit (line 367) | fn choose_exit(&self, selector: &ExitSelector, relay: &OneRelay, selec...
method new_tunnel (line 377) | async fn new_tunnel(
method remove_local_tunnels (line 487) | pub async fn remove_local_tunnels(&self) -> Result<(), ApiError> {
method select_relay (line 499) | pub async fn select_relay(&self, network_interface: Option<&NetworkInt...
method make_api_client (line 574) | pub fn make_api_client(&self, account_id: AccountId) -> Result<Client,...
method api_client (line 578) | fn api_client(&self) -> Result<Arc<Client>, ApiError> {
method cache_auth_token (line 596) | fn cache_auth_token(&self) {
method api_request (line 605) | pub async fn api_request<C: Cmd>(&self, cmd: C) -> Result<C::Output, A...
method cached_api_request (line 612) | pub async fn cached_api_request<C: ETagCmd>(&self, cmd: C, etag: Optio...
method base_url (line 619) | pub fn base_url(&self) -> String {
method user_agent (line 623) | pub fn user_agent(&self) -> String {
method maybe_update_exits (line 627) | pub async fn maybe_update_exits(&self, freshness: Duration) -> Result<...
method update_account_info (line 657) | pub fn update_account_info(&self, account_info: &AccountInfo) {
method rotate_wireguard_key_if_required (line 665) | pub fn rotate_wireguard_key_if_required(&self) {
method register_cached_wireguard_key_if_new (line 674) | pub async fn register_cached_wireguard_key_if_new(&self) -> Result<(),...
method rotate_wg_key (line 708) | pub fn rotate_wg_key(&self) {
method get_debug_info (line 716) | pub async fn get_debug_info(&self) -> DebugInfo {
method update_dns_cache (line 761) | pub fn update_dns_cache(&self, name: &str, addrs: &[SocketAddr]) {
type ClientState (line 52) | pub struct ClientState {
method new (line 79) | pub fn new(
method target_state (line 105) | pub fn target_state(&self) -> TargetState {
method config (line 117) | pub fn config(&self) -> &Config {
method base_url (line 121) | pub fn base_url(&self) -> String {
method make_api_client (line 125) | fn make_api_client(&self, account_id: AccountId) -> Result<Client, Api...
type AccountStatus (line 64) | pub struct AccountStatus {
method eq (line 72) | fn eq(&self, other: &Self) -> bool {
type WeakClientStateHandle (line 769) | pub struct WeakClientStateHandle(Weak<Sender<ClientState>>);
method upgrade (line 772) | pub fn upgrade(&self) -> Option<ClientStateHandle> {
FILE: rustlib/src/config/cached.rs
type ConfigCached (line 10) | pub struct ConfigCached<T> {
function new (line 17) | pub fn new(value: T, version: Version) -> Self {
function etag (line 21) | pub fn etag(&self) -> Option<&[u8]> {
function staleness (line 28) | pub fn staleness(&self) -> Duration {
function version (line 32) | pub fn version(&self) -> &[u8] {
method eq (line 41) | fn eq(&self, other: &Self) -> bool {
type Version (line 49) | pub enum Version {
method artificial (line 55) | pub fn artificial() -> Self {
FILE: rustlib/src/config/dns_cache.rs
type DnsCache (line 7) | pub struct DnsCache {
method get (line 12) | pub fn get(&self, name: &str) -> Vec<SocketAddr> {
method set (line 16) | pub fn set(&mut self, name: &str, addr: &[SocketAddr]) {
method default (line 22) | fn default() -> Self {
FILE: rustlib/src/config/feature_flags.rs
type FeatureFlags (line 11) | pub struct FeatureFlags {
constant KEYS (line 25) | pub const KEYS: &'static [&'static str] = FeatureFlagKey::VARIANTS;
method set (line 27) | pub fn set(&mut self, flag: &str, active: bool) {
method change (line 31) | fn change(&mut self, flag: &str, value: Option<bool>) {
type FeatureFlagKey (line 47) | pub enum FeatureFlagKey {
function check_flag_list (line 59) | fn check_flag_list() {
FILE: rustlib/src/config/mod.rs
type ConfigHandle (line 13) | pub struct ConfigHandle {
method new (line 20) | pub fn new(config_dir: PathBuf, keychain_wg_sk: Option<&[u8]>) -> Resu...
method change (line 25) | pub fn change<T>(&mut self, f: impl FnOnce(&mut Config) -> T) -> T {
method check_persisted (line 40) | pub fn check_persisted(&self) -> Result<(), ConfigDirty> {
type Target (line 46) | type Target = Config;
method deref (line 48) | fn deref(&self) -> &Self::Target {
FILE: rustlib/src/config/persistence.rs
constant CONFIG_FILE (line 29) | pub(super) const CONFIG_FILE: &str = "config.json";
type ConfigSaveError (line 32) | pub enum ConfigSaveError {
type ConfigLoadError (line 46) | pub enum ConfigLoadError {
function try_load (line 55) | fn try_load(path: &Path) -> Result<Option<Config>, ConfigLoadError> {
function load (line 75) | pub fn load(config_dir: &Path, keychain_wg_sk: Option<&[u8]>) -> Result<...
function save (line 135) | pub fn save(config_dir: &Path, config: &Config) -> Result<(), ConfigSave...
type Config (line 228) | pub struct Config {
method migrate (line 285) | pub fn migrate(&mut self) {
type ConfigDebug (line 296) | pub struct ConfigDebug {
method from (line 319) | fn from(config: Config) -> Self {
type PinnedLocation (line 373) | pub struct PinnedLocation {
type WireGuardKeyCache (line 383) | pub struct WireGuardKeyCache {
method fmt (line 415) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
method try_set_secret_key_from_keychain (line 437) | pub fn try_set_secret_key_from_keychain(&mut self, keychain_secret_key...
method ensure_key_pair (line 470) | fn ensure_key_pair(&mut self, set_keychain_wg_sk: Option<&KeychainSetS...
method use_key_pair (line 491) | pub fn use_key_pair(&mut self, set_keychain_wg_sk: Option<&KeychainSet...
method rotate_now (line 497) | pub fn rotate_now(&mut self, set_keychain_wg_sk: Option<&KeychainSetSe...
method rotate_now_internal (line 500) | fn rotate_now_internal(&mut self, set_keychain_wg_sk: Option<&Keychain...
method rotate_if_required (line 526) | pub fn rotate_if_required(&mut self, set_keychain_wg_sk: Option<&Keych...
method need_registration (line 534) | pub fn need_registration(&mut self, set_keychain_wg_sk: Option<&Keycha...
method registered (line 542) | pub fn registered(&mut self, registered_public_key: PublicKey, removed...
type WireGuardKeyCacheKeyPair (line 397) | pub enum WireGuardKeyCacheKeyPair {
type KeychainSetSecretKeyFn (line 412) | pub type KeychainSetSecretKeyFn = Box<dyn (Fn(&[u8; 32]) -> bool) + Sync...
FILE: rustlib/src/config/persistence_test.rs
function random_config (line 23) | fn random_config() -> Config {
function load_no_config (line 34) | fn load_no_config() {
function load_config (line 40) | fn load_config() {
function load_invalid_json (line 51) | fn load_invalid_json() {
function load_empty (line 80) | fn load_empty() {
function load_no_permission (line 100) | fn load_no_permission() {
function test_ignore_invalid_fields (line 120) | fn test_ignore_invalid_fields() {
FILE: rustlib/src/constants.rs
constant DEFAULT_API_DOMAIN (line 6) | pub const DEFAULT_API_DOMAIN: &str = "v1.api.prod.obscura.net";
constant DEFAULT_API_URL (line 7) | pub const DEFAULT_API_URL: &str = formatcp!("https://{DEFAULT_API_DOMAIN...
constant DNS_CACHE_SEED (line 8) | pub const DNS_CACHE_SEED: &[(&str, &[SocketAddr])] = &[(DEFAULT_API_DOMA...
constant DEFAULT_API_BACKUP_DOMAIN (line 10) | pub const DEFAULT_API_BACKUP_DOMAIN: &str = "crimsonlance.net";
constant DEFAULT_RELAY_SNI (line 11) | pub const DEFAULT_RELAY_SNI: &str = "example.com";
FILE: rustlib/src/debug_archive/builder.rs
type DebugArchiveBuilder (line 9) | pub struct DebugArchiveBuilder {
method new (line 14) | pub fn new() -> anyhow::Result<Self> {
method write_error (line 31) | fn write_error(&mut self, name: &str, error: impl Debug + Display) {
method add (line 38) | fn add(&mut self, name: &str, f: impl FnOnce(&mut Zipper) -> anyhow::R...
method add_bytes (line 44) | pub fn add_bytes(&mut self, name: &str, ext: &str, data: &[u8]) {
method add_txt (line 48) | pub fn add_txt(&mut self, name: &str, text: &str) {
method add_json (line 53) | pub fn add_json(&mut self, name: &str, value: &impl Serialize) {
method add_path (line 59) | pub fn add_path(&mut self, name: &str, ext: Option<&str>, path: &Utf8P...
method add_cmd (line 69) | pub fn add_cmd(&mut self, name: &str, ext: &str, mut cmd: diva::Comman...
method finish (line 83) | pub fn finish(self) -> anyhow::Result<Utf8PathBuf> {
FILE: rustlib/src/debug_archive/dns.rs
type DnsTask (line 9) | pub struct DnsTask {
function debug_dns (line 13) | pub async fn debug_dns(host_port: &'static str) -> DebugTask<DnsTask> {
FILE: rustlib/src/debug_archive/http.rs
type HttpTask (line 15) | pub struct HttpTask {
function debug_http (line 24) | pub async fn debug_http(url: &'static str, dns: Option<DnsTask>, sni: bo...
type FixedResolver (line 76) | struct FixedResolver(Vec<IpAddr>);
method resolve (line 79) | fn resolve(&self, _: reqwest::dns::Name) -> Resolving {
FILE: rustlib/src/debug_archive/info.rs
type DebugInfo (line 9) | pub struct DebugInfo {
FILE: rustlib/src/debug_archive/mod.rs
function create_debug_archive (line 12) | pub fn create_debug_archive(user_feedback: Option<&str>, debug_info: Deb...
FILE: rustlib/src/debug_archive/task.rs
constant TASK_TIMEOUT (line 9) | const TASK_TIMEOUT: Duration = Duration::from_secs(60);
type DebugTask (line 12) | pub struct DebugTask<T> {
type TaskResult (line 18) | pub enum TaskResult<T> {
function get (line 26) | pub fn get(&self) -> Option<&T> {
function run_debug_task (line 34) | pub async fn run_debug_task<T>(task: impl Future<Output = Result<T, Box<...
function debug_panic_error (line 45) | pub fn debug_panic_error<T>(error: JoinError) -> DebugTask<T> {
FILE: rustlib/src/debug_archive/zipper.rs
type Zipper (line 10) | pub struct Zipper {
method new (line 19) | pub fn new(dst_parent: &Utf8Path, name: String) -> anyhow::Result<Self> {
method root (line 33) | fn root(&self) -> &Utf8Path {
method create_dir (line 40) | pub fn create_dir(&mut self, dst: impl AsRef<Utf8Path>) -> anyhow::Res...
method write_file (line 48) | pub fn write_file(&mut self, dst: impl AsRef<Utf8Path>, data: &[u8]) -...
method copy_from_fs (line 63) | pub fn copy_from_fs(&mut self, src: &Utf8Path, dst: &Utf8Path) -> anyh...
method finish (line 96) | pub fn finish(self) -> anyhow::Result<Utf8PathBuf> {
FILE: rustlib/src/dns.rs
type DnsResolver (line 8) | pub struct DnsResolver {
method new (line 13) | pub fn new(client_state: WeakClientStateHandle) -> Arc<Self> {
method resolve (line 19) | fn resolve(&self, name: Name) -> Resolving {
function resolve_and_cache (line 43) | async fn resolve_and_cache(client_state: WeakClientStateHandle, name: St...
FILE: rustlib/src/errors.rs
type ConnectErrorCode (line 25) | pub enum ConnectErrorCode {
method as_static_str (line 38) | pub fn as_static_str(&self) -> &'static str {
method from (line 44) | fn from(err: &TunnelConnectError) -> Self {
type TunnelConnectError (line 95) | pub enum TunnelConnectError {
type ApiError (line 123) | pub enum ApiError {
method api_error_kind (line 148) | pub fn api_error_kind(&self) -> Option<&obscuravpn_api::cmd::ApiErrorK...
type ConfigDirtyOrApiError (line 131) | pub enum ConfigDirtyOrApiError {
type ConfigDirty (line 139) | pub struct ConfigDirty;
method fmt (line 142) | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
type RelaySelectionError (line 157) | pub enum RelaySelectionError {
type ErrorAt (line 167) | pub struct ErrorAt<T: std::error::Error> {
function from (line 173) | fn from(error: T) -> Self {
FILE: rustlib/src/exit_selection.rs
type ExitSelector (line 11) | pub enum ExitSelector {
method matches (line 26) | pub fn matches(&self, candidate: &OneExit) -> bool {
method default (line 37) | fn default() -> Self {
type ExitSelectionState (line 43) | pub struct ExitSelectionState {
method select_next_exit (line 51) | pub fn select_next_exit<'a>(&mut self, selector: &ExitSelector, exits:...
method rank (line 74) | fn rank(candidate: &OneExit, relay_city_code: &CityCode, relay_preferr...
method exclude (line 81) | fn exclude(&self, candidate: &OneExit) -> bool {
FILE: rustlib/src/ffi_helpers.rs
type FfiBytes (line 6) | pub struct FfiBytes<'a> {
function as_slice (line 13) | pub fn as_slice(&self) -> &'a [u8] {
function to_vec (line 25) | pub fn to_vec(&self) -> Vec<u8> {
function from (line 31) | fn from(b: &'a T) -> Self {
type FfiBytesExt (line 37) | pub trait FfiBytesExt {
method ffi (line 38) | fn ffi(&self) -> FfiBytes<'_>;
method ffi (line 42) | fn ffi(&self) -> FfiBytes<'_> {
type FfiStr (line 48) | pub struct FfiStr<'a> {
function as_str (line 53) | pub fn as_str(&self) -> &'a str {
function fmt (line 59) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
function from (line 65) | fn from(s: &'a T) -> Self {
type FfiStrExt (line 71) | pub trait FfiStrExt {
method ffi_str (line 72) | fn ffi_str(&self) -> FfiStr<'_>;
method ffi_str (line 76) | fn ffi_str(&self) -> FfiStr<'_> {
FILE: rustlib/src/liveness.rs
constant MAX_ALLOWED_LOST_PROBES (line 9) | const MAX_ALLOWED_LOST_PROBES: usize = 4;
constant MAX_ALLOWED_LOST_PROBES_AFTER_SLEEP (line 10) | const MAX_ALLOWED_LOST_PROBES_AFTER_SLEEP: usize = 1;
constant BUSY_PING_PERIOD (line 11) | const BUSY_PING_PERIOD: Duration = Duration::from_secs(1);
constant IDLE_PING_PERIOD (line 12) | const IDLE_PING_PERIOD: Duration = Duration::from_secs(55);
constant MIN_PROBE_LOST_PERIOD (line 20) | const MIN_PROBE_LOST_PERIOD: Duration = Duration::from_secs(1);
constant MAX_PROBE_LOST_PERIOD (line 21) | const MAX_PROBE_LOST_PERIOD: Duration = Duration::from_secs(30);
constant SLOW_PONG_WINDOW (line 22) | const SLOW_PONG_WINDOW: u32 = 100;
constant PROBE_PREFIX (line 25) | const PROBE_PREFIX: &[u8; 32] = b"obs-ping\x75\xf8\xb9\x47\x4b\xe1\x61\x...
type LivenessChecker (line 27) | pub struct LivenessChecker {
method new (line 46) | pub fn new(mtu: u16, client_ip: Ipv4Addr, ping_target_ip: Ipv4Addr) ->...
method probe_lost_period (line 60) | fn probe_lost_period(&self) -> Duration {
method lost_probe_count_and_time_of_next_loss (line 69) | fn lost_probe_count_and_time_of_next_loss(&self, now: Instant) -> (usi...
method sent_traffic (line 82) | pub fn sent_traffic(&mut self) -> Option<Vec<u8>> {
method wake (line 95) | pub fn wake(&mut self) -> Vec<u8> {
method poll (line 105) | pub fn poll(&mut self) -> LivenessCheckerPoll {
method process_potential_probe_response (line 161) | pub fn process_potential_probe_response(&mut self, packet: &[u8]) -> O...
method update_slowest_pongs_list (line 216) | fn update_slowest_pongs_list(&mut self, id_seq: u32, rtt: Duration) {
method send_ping (line 230) | fn send_ping(&mut self, now: Instant) -> Vec<u8> {
type SentPing (line 39) | struct SentPing {
type LivenessCheckerPoll (line 257) | pub enum LivenessCheckerPoll {
type ReceivedPong (line 263) | struct ReceivedPong {
function test_probe_packet_size (line 273) | fn test_probe_packet_size() {
FILE: rustlib/src/logging.rs
type LogPersistence (line 13) | pub struct LogPersistence {
method log_dir (line 19) | pub fn log_dir(&self) -> &Utf8Path {
function build_log_roller (line 25) | fn build_log_roller(log_dir: &Utf8Path) -> anyhow::Result<(NonBlocking, ...
function build_log_roller (line 50) | fn build_log_roller(log_dir: &Utf8Path) -> anyhow::Result<(NonBlocking, ...
function filter (line 55) | fn filter() -> EnvFilter {
function init (line 59) | pub fn init(base_layer: impl Layer<Registry> + Send + Sync, persistence_...
FILE: rustlib/src/manager.rs
type Manager (line 32) | pub struct Manager {
method new (line 147) | pub fn new(
method maybe_update_exits (line 166) | pub async fn maybe_update_exits(&self, freshness: Duration) -> Result<...
method subscribe (line 170) | pub fn subscribe(&self) -> Receiver<Status> {
method traffic_stats (line 174) | pub fn traffic_stats(&self) -> ManagerTrafficStats {
method login (line 178) | pub async fn login(&self, account_id: AccountId, validate: bool) -> Re...
method logout (line 201) | pub fn logout(&self) -> Result<(), ConfigDirty> {
method set_api_url (line 205) | pub fn set_api_url(&self, value: Option<String>) {
method api_request (line 209) | pub async fn api_request<C: Cmd>(&self, cmd: C) -> Result<C::Output, A...
method apple_associate_account (line 213) | pub async fn apple_associate_account(&self, app_transaction_jws: Strin...
method delete_account (line 217) | pub async fn delete_account(&self) -> Result<DeleteAccountOutput, ApiE...
method get_account_info (line 221) | pub async fn get_account_info(&self) -> Result<AccountInfo, ApiError> {
method propagate_updates_to_status_task (line 227) | async fn propagate_updates_to_status_task(this: Arc<Self>, _: ()) {
method wireguard_key_registraction_task (line 254) | async fn wireguard_key_registraction_task(this: Arc<Self>, _: ()) {
method preferred_network_interface_task (line 283) | pub async fn preferred_network_interface_task(this: Arc<Self>, mut net...
method create_debug_archive (line 294) | pub async fn create_debug_archive(&self, user_feedback: Option<&str>) ...
method get_debug_info (line 301) | pub async fn get_debug_info(&self) -> DebugInfo {
method wake (line 305) | pub fn wake(&self) {
method get_exit_list (line 311) | pub async fn get_exit_list(&self, known_version: Option<Vec<u8>>) -> R...
method run_on_client_state (line 330) | pub fn run_on_client_state(&self, f: impl FnOnce(&ClientStateHandle)) ...
type Status (line 42) | pub struct Status {
method new (line 60) | fn new(version: Uuid, vpn_status: VpnStatus, client_state: &ClientStat...
type VpnStatus (line 97) | pub enum VpnStatus {
method from_tunnel_state (line 121) | fn from_tunnel_state(tunnel_state: &TunnelState) -> Self {
type TunnelArgs (line 116) | pub struct TunnelArgs {
type ManagerTrafficStats (line 338) | pub struct ManagerTrafficStats {
FILE: rustlib/src/manager_cmd.rs
type ManagerCmdErrorCode (line 38) | pub enum ManagerCmdErrorCode {
method as_static_str (line 50) | pub fn as_static_str(&self) -> &'static str {
method from (line 56) | fn from(error: &ConfigDirty) -> Self {
method from (line 63) | fn from(error: &ConfigDirtyOrApiError) -> Self {
method from (line 73) | fn from(error: &ApiError) -> Self {
type ManagerCmd (line 113) | pub enum ManagerCmd {
method from_json (line 210) | pub fn from_json(json_cmd: &[u8]) -> Result<Self, ManagerCmdErrorCode> {
method run (line 235) | pub async fn run(self, manager: &Manager) -> Result<ManagerCmdOk, Mana...
type ManagerCmdOk (line 180) | pub enum ManagerCmdOk {
method from (line 196) | fn from((): ()) -> Self {
function map_result (line 201) | fn map_result<T, E>(result: Result<T, E>) -> Result<ManagerCmdOk, Manage...
FILE: rustlib/src/net.rs
type NetworkInterface (line 16) | pub struct NetworkInterface {
function new_udp (line 25) | pub fn new_udp(network_interface: Option<&NetworkInterface>) -> io::Resu...
constant SIOCGIFMTU (line 46) | const SIOCGIFMTU: libc::Ioctl = libc::SIOCGIFMTU as libc::Ioctl;
constant SIOCGIFMTU (line 49) | const SIOCGIFMTU: libc::c_ulong = 3223349555;
function interface_mtu (line 52) | pub fn interface_mtu(interface: &NetworkInterface) -> anyhow::Result<i32> {
function interface_mtu (line 76) | pub fn interface_mtu(interface: &NetworkInterface) -> anyhow::Result<i32> {
function new_quic (line 80) | pub fn new_quic(udp: std::net::UdpSocket, mtu: Option<u16>, force_small_...
FILE: rustlib/src/network_config.rs
constant MULLVAD_EXIT_PROVIDER_NAME (line 8) | const MULLVAD_EXIT_PROVIDER_NAME: &str = "Mullvad VPN";
type TunnelNetworkConfig (line 11) | pub struct TunnelNetworkConfig {
method new (line 19) | pub fn new(tunnel_config: &ObfuscatedTunnelConfig, mtu: u16) -> Result...
method dummy (line 36) | fn dummy() -> Self {
type NetworkConfigError (line 47) | pub enum NetworkConfigError {
type DnsConfig (line 57) | pub enum DnsConfig {
type DnsContentBlock (line 65) | pub struct DnsContentBlock {
method mullvad_dns_ip (line 75) | fn mullvad_dns_ip(self) -> Option<Ipv4Addr> {
type OsNetworkConfig (line 93) | pub struct OsNetworkConfig {
method new (line 102) | pub fn new(
method dummy (line 126) | pub fn dummy(dns_content_block: DnsContentBlock, use_system_dns: bool)...
FILE: rustlib/src/os/os_trait.rs
type Os (line 6) | pub trait Os: Sync + Send + 'static {
method network_interface (line 8) | fn network_interface(&self) -> tokio::sync::watch::Receiver<Option<Net...
method set_os_network_config (line 13) | fn set_os_network_config(&self, network_config: OsNetworkConfig, tunne...
method unset_os_network_config (line 16) | fn unset_os_network_config(&self) -> impl Future<Output = Result<(), (...
method packet_for_os (line 19) | fn packet_for_os(&self, packet: Bytes);
FILE: rustlib/src/os/packet_buffer.rs
constant PACKET_CAPACITY (line 1) | const PACKET_CAPACITY: usize = 100;
constant BUFFER_CAPACITY (line 2) | const BUFFER_CAPACITY: usize = 1500 * 100 + u16::MAX as usize;
type PacketBuffer (line 3) | pub struct PacketBuffer {
method buffer (line 11) | pub fn buffer(&mut self) -> Option<&mut [u8]> {
method commit (line 15) | pub fn commit(&mut self, size: u16) {
method take_iter (line 20) | pub fn take_iter(&mut self) -> PacketBufferIter<'_> {
method default (line 29) | fn default() -> Self {
type PacketBufferIter (line 39) | pub struct PacketBufferIter<'a> {
type Item (line 45) | type Item = &'a [u8];
method next (line 47) | fn next(&mut self) -> Option<Self::Item> {
method size_hint (line 55) | fn size_hint(&self) -> (usize, Option<usize>) {
FILE: rustlib/src/positive_u31.rs
type PositiveU31 (line 9) | pub struct PositiveU31 {
type Error (line 14) | type Error = TryFromIntError;
method try_from (line 15) | fn try_from(value: u32) -> Result<Self, Self::Error> {
function from (line 22) | fn from(value: PositiveU31) -> Self {
function from (line 28) | fn from(value: PositiveU31) -> Self {
method from (line 34) | fn from(value: PositiveU31) -> Self {
method from (line 40) | fn from(value: PositiveU31) -> Self {
FILE: rustlib/src/quicwg.rs
constant WG_FIRST_HANDSHAKE_RESENDS (line 41) | const WG_FIRST_HANDSHAKE_RESENDS: usize = 25;
constant WG_FIRST_HANDSHAKE_TIMEOUT (line 42) | const WG_FIRST_HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(100);
constant QUIC_IDLE_TIMEOUT (line 45) | pub const QUIC_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
constant QUIC_STEP_TIMEOUT (line 47) | const QUIC_STEP_TIMEOUT: Duration = Duration::from_secs(30);
constant WG_TIMER_TICK (line 52) | const WG_TIMER_TICK: Duration = Duration::from_secs(1);
constant TUNNEL_MTU (line 53) | pub const TUNNEL_MTU: u16 = 1280;
constant DEFAULT_UDP_PAYLOAD_SIZE (line 56) | pub const DEFAULT_UDP_PAYLOAD_SIZE: u16 = 1350;
constant IPV4_UDP_OVERHEAD (line 58) | pub const IPV4_UDP_OVERHEAD: u16 = 20 + 8;
constant LIVENESS_MTU (line 60) | const LIVENESS_MTU: u16 = 100;
constant WG_FRAGMENT_BUFFER_LEN (line 69) | const WG_FRAGMENT_BUFFER_LEN: NonZeroU32 = NonZeroU32::new(10_000).unwra...
constant WG_FRAGMENT_MAX_SIZE (line 75) | const WG_FRAGMENT_MAX_SIZE: u16 = 1540;
type QuicWgReceiveError (line 78) | pub enum QuicWgReceiveError {
type QuicWgConnectError (line 86) | pub enum QuicWgConnectError {
type QuicWgRelayHandshakeError (line 100) | pub enum QuicWgRelayHandshakeError {
type RelayErrorResponse (line 119) | pub struct RelayErrorResponse {
method new (line 129) | pub fn new(error_code: NonZeroU32, message: &[u8]) -> Self {
type UnexpectedOpCode (line 126) | pub struct UnexpectedOpCode(RelayOpCode);
type QuicWgWireguardHandshakeError (line 135) | pub enum QuicWgWireguardHandshakeError {
type QuicWgConn (line 146) | pub struct QuicWgConn {
method connect (line 215) | pub async fn connect(
method build_first_wg_handshake_init (line 257) | fn build_first_wg_handshake_init(wg: &mut Tunn) -> Result<Bytes, QuicW...
method wait_for_first_handshake_response (line 266) | async fn wait_for_first_handshake_response(
method first_wg_handshake (line 296) | async fn first_wg_handshake(
method handle_result (line 327) | fn handle_result(wg_sender: &WgSender, res: TunnResult<'_>) -> Control...
method send (line 344) | fn send<'a>(&self, packets: impl Iterator<Item = &'a [u8]>) {
method wake (line 358) | pub fn wake(&self) {
method send_single_packet (line 364) | fn send_single_packet(&self, wg_state: &mut WgState, packet: &[u8]) {
method receive (line 386) | pub async fn receive(&self) -> Result<Bytes, QuicWgReceiveError> {
method traffic_stats (line 471) | pub fn traffic_stats(&self) -> QuicWgTrafficStats {
method exit_public_key (line 475) | pub fn exit_public_key(&self) -> PublicKey {
method client_public_key (line 479) | pub fn client_public_key(&self) -> PublicKey {
method transport (line 483) | pub fn transport(&self) -> TransportKind {
type QuicWgTrafficStats (line 157) | pub struct QuicWgTrafficStats {
type WgState (line 164) | struct WgState {
type TickStats (line 177) | struct TickStats {
type TransportKind (line 194) | pub enum TransportKind {
type QuicWgConnPacketSender (line 200) | pub struct QuicWgConnPacketSender(Weak<QuicWgConn>);
method new (line 203) | pub fn new(conn: Option<&Arc<QuicWgConn>>) -> Self {
method send (line 207) | pub fn send<'a>(&self, packets: impl Iterator<Item = &'a [u8]>) {
type QuicWgConnHandshaking (line 491) | pub struct QuicWgConnHandshaking {
method start_quic (line 498) | pub async fn start_quic(
method start_tcp_tls (line 528) | pub async fn start_tcp_tls(
method exchange_protocol_identifiers (line 553) | async fn exchange_protocol_identifiers(&mut self) -> Result<(), QuicWg...
method measure_rtt (line 585) | pub async fn measure_rtt(&mut self) -> Result<Duration, QuicWgConnectE...
method authenticate (line 607) | async fn authenticate(mut self, token: Uuid) -> Result<Transport, Quic...
method stop (line 615) | async fn stop(&mut self) -> Result<(), QuicWgRelayHandshakeError> {
method abandon (line 623) | pub async fn abandon(mut self) {
method rustls_config (line 640) | fn rustls_config(relay_cert: CertificateDer<'static>) -> Result<rustls...
method tcp_tls_config (line 650) | fn tcp_tls_config(relay_cert: CertificateDer<'static>) -> Result<TlsCo...
method quic_config (line 656) | fn quic_config(
method recv_ok_resp (line 696) | async fn recv_ok_resp(&mut self) -> Result<(), QuicWgRelayHandshakeErr...
method recv_ok_resp_no_timeout (line 704) | async fn recv_ok_resp_no_timeout(&mut self) -> Result<(), QuicWgRelayH...
method send_op (line 739) | async fn send_op(&mut self, op: RelayOpCode, arg: &[u8]) -> Result<(),...
method send_op_no_timeout (line 747) | async fn send_op_no_timeout(&mut self, op: RelayOpCode, arg: &[u8]) ->...
method transport_kind (line 757) | pub fn transport_kind(self) -> TransportKind {
type Transport (line 765) | enum Transport {
method into_wg_send_recv (line 775) | fn into_wg_send_recv(self) -> (WgSender, WgReceiver, Option<(quinn::Se...
type WgSender (line 845) | enum WgSender {
method max_wg_message_size (line 851) | fn max_wg_message_size(&self) -> Option<u16> {
method send_wg_message (line 858) | fn send_wg_message(&self, wg_message: Bytes) {
type WgReceiver (line 879) | enum WgReceiver {
method new_tcp_tls (line 889) | fn new_tcp_tls(traffic_state: watch::Sender<WgTrafficState>, relay_rea...
method receive_wg_message (line 897) | async fn receive_wg_message(&self) -> io::Result<Bytes> {
type WgTrafficState (line 924) | struct WgTrafficState {
function send_message (line 929) | async fn send_message<T: AsyncWrite + Unpin>(transport: &mut T, code: Me...
function recv_skip (line 938) | async fn recv_skip<T: AsyncRead + Unpin>(transport: &mut T, mut n: usize...
function recv_message (line 950) | async fn recv_message<T: AsyncRead + Unpin>(transport: &mut T) -> Result...
function recv_fixed (line 971) | async fn recv_fixed<const N: usize, T: AsyncRead + Unpin>(transport: &mu...
type VerifyVpnServerCert (line 978) | struct VerifyVpnServerCert {
method verify_server_cert (line 984) | fn verify_server_cert(
method verify_tls12_signature (line 997) | fn verify_tls12_signature(
method verify_tls13_signature (line 1005) | fn verify_tls13_signature(
method supported_verify_schemes (line 1014) | fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
FILE: rustlib/src/relay_selection.rs
function race_relay_handshakes (line 11) | pub fn race_relay_handshakes(
FILE: rustlib/src/serde_safe.rs
type TryParse (line 3) | pub enum TryParse<T> {
function deserialize (line 8) | pub fn deserialize<'de, T: Default + serde::Deserialize<'de>, D: serde::...
FILE: rustlib/src/tokio.rs
type AbortOnDrop (line 1) | pub struct AbortOnDrop(pub tokio::task::AbortHandle);
method spawn (line 4) | pub fn spawn<F: Future<Output = ()> + Send + 'static>(f: F) -> Self {
method from (line 16) | fn from(handle: tokio::task::AbortHandle) -> Self {
method from (line 22) | fn from(handle: tokio::task::JoinHandle<()>) -> Self {
method drop (line 10) | fn drop(&mut self) {
FILE: rustlib/src/tunnel_state.rs
type TargetState (line 24) | pub struct TargetState {
type TunnelState (line 32) | pub enum TunnelState {
method new (line 57) | pub fn new(client_state: ClientStateHandle, os_impl: Arc<impl Os>) -> ...
method traffic_stats (line 63) | pub fn traffic_stats(&self) -> ManagerTrafficStats {
method set_disconnected (line 83) | fn set_disconnected(&mut self) {
method set_connecting (line 87) | fn set_connecting(&mut self, new_args: &TunnelArgs, network_interface:...
method set_connected (line 102) | fn set_connected(
method set_connect_error (line 122) | fn set_connect_error(&mut self, error: TunnelConnectError) {
method get_conn (line 133) | pub fn get_conn(&self) -> Option<Arc<QuicWgConn>> {
method get_connected (line 141) | fn get_connected(&self) -> Option<Connected> {
method matches_target (line 151) | fn matches_target(&self, target_tunnel_args: Option<&TunnelArgs>, targ...
method maintain (line 161) | async fn maintain(tunnel_state: Sender<TunnelState>, client_state: Cli...
type Connected (line 53) | type Connected = (Arc<QuicWgConn>, TunnelNetworkConfig, OneExit, OneRelay);
function poll_until_change (line 309) | async fn poll_until_change<O>(watch: &mut Receiver<ClientState>, target_...
Condensed preview — 443 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,598K chars).
[
{
"path": ".editorconfig",
"chars": 4472,
"preview": "# https://editorconfig.org/#supported-properties\n\nroot = true\n\n[*]\ncharset = utf-8\nindent_size = 4\nindent_style = space\n"
},
{
"path": ".envrc",
"chars": 10,
"preview": "use flake\n"
},
{
"path": ".git-blame-ignore-revs",
"chars": 41,
"preview": "70ff75289a9cbce36f7b70a20b4f9c9f82e3b25e\n"
},
{
"path": ".github/workflows/checks.yml",
"chars": 5571,
"preview": "name: Checks\n\non:\n workflow_dispatch:\n pull_request:\n branches:\n - '**'\n push:\n branches:\n - 'main'\n\n"
},
{
"path": ".gitignore",
"chars": 234,
"preview": "/.direnv/\n/.idea/\n/.devcontainer/\nxcuserdata\n.DS_STORE\n/.claude/\n\n# Nix\nresult\nresult-*\n\n# DMG\n*.dmg\n\n# Android\n*.apk\n*."
},
{
"path": ".shellcheckrc",
"chars": 36,
"preview": "source-path=.\nexternal-sources=true\n"
},
{
"path": ".swiftformat",
"chars": 229,
"preview": "--self insert\n--disable andOperator,unusedArguments,hoistPatternLet\n--trailing-commas collections-only # Old versions of"
},
{
"path": "LICENSE.md",
"chars": 4563,
"preview": "# PolyForm Noncommercial License 1.0.0\n\n<https://polyformproject.org/licenses/noncommercial/1.0.0>\n\n## Acceptance\n\nIn or"
},
{
"path": "README.md",
"chars": 14406,
"preview": "# Obscura VPN Client\n\nObscura VPN library, CLI client, and App\n\n## Support\n\nNo support is provided for this code directl"
},
{
"path": "android/.gitignore",
"chars": 277,
"preview": "*.iml\n.gradle\n.kotlin\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/na"
},
{
"path": "android/.idea/.gitignore",
"chars": 77,
"preview": "# Default ignored files\n/shelf/\n/workspace.xml\n/deploymentTargetSelector.xml\n"
},
{
"path": "android/.idea/.name",
"chars": 10,
"preview": "ObscuraVPN"
},
{
"path": "android/.idea/AndroidProjectSystem.xml",
"chars": 213,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"AndroidProjectSystem\">\n <option name="
},
{
"path": "android/.idea/codeStyles/Project.xml",
"chars": 5348,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <code_scheme name=\"Project\" version=\"173\">\n <option name=\"RIGHT_MA"
},
{
"path": "android/.idea/codeStyles/codeStyleConfig.xml",
"chars": 142,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <state>\n <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n "
},
{
"path": "android/.idea/compiler.xml",
"chars": 169,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"CompilerConfiguration\">\n <bytecodeTar"
},
{
"path": "android/.idea/detekt.xml",
"chars": 308,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"DetektPluginSettings\">\n <option name="
},
{
"path": "android/.idea/deviceManager.xml",
"chars": 353,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"DeviceTable\">\n <option name=\"columnSo"
},
{
"path": "android/.idea/dictionaries/project.xml",
"chars": 220,
"preview": "<component name=\"ProjectDictionaryState\">\n <dictionary name=\"project\">\n <words>\n <w>obscura</w>\n <w>obscur"
},
{
"path": "android/.idea/gradle.xml",
"chars": 1310,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"GradleMigrationSettings\" migrationVersio"
},
{
"path": "android/.idea/kotlinc.xml",
"chars": 230,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"KotlinJpsPluginSettings\">\n <option na"
},
{
"path": "android/.idea/ktfmt.xml",
"chars": 406,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"KtfmtSettings\">\n <option name=\"custom"
},
{
"path": "android/.idea/migrations.xml",
"chars": 255,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"ProjectMigrations\">\n <option name=\"Mi"
},
{
"path": "android/.idea/misc.xml",
"chars": 409,
"preview": "<project version=\"4\">\n <component name=\"ExternalStorageConfigurationManager\" enabled=\"true\" />\n <component name=\"Proje"
},
{
"path": "android/.idea/runConfigurations.xml",
"chars": 965,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"RunConfigurationProducerService\">\n <o"
},
{
"path": "android/.idea/vcs.xml",
"chars": 184,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"VcsDirectoryMappings\">\n <mapping dire"
},
{
"path": "android/app/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "android/app/build.gradle.kts",
"chars": 2226,
"preview": "import com.android.build.api.dsl.ApplicationExtension\n\nplugins {\n alias(libs.plugins.android.application)\n alias(l"
},
{
"path": "android/app/google-services.json",
"chars": 677,
"preview": "{\n \"project_info\": {\n \"project_number\": \"980686794718\",\n \"project_id\": \"obscura-vpn\",\n \"storage_bucket\": \"obsc"
},
{
"path": "android/app/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "android/app/src/foss/java/net/obscura/vpnclientapp/BillingFacade.kt",
"chars": 463,
"preview": "package net.obscura.vpnclientapp\n\nimport android.content.Context\nimport net.obscura.vpnclientapp.activities.MainActivity"
},
{
"path": "android/app/src/main/AndroidManifest.xml",
"chars": 3471,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:tools=\"http://schemas.android.com/tools\"\n xmlns:android=\"http:"
},
{
"path": "android/app/src/main/aidl/net/obscura/vpnclientapp/services/IObscuraVpnService.aidl",
"chars": 588,
"preview": "// IObscuraVpnService.aidl\npackage net.obscura.vpnclientapp.services;\n\ninterface IObscuraVpnService {\n void startTunn"
},
{
"path": "android/app/src/main/assets/adi-registration.properties",
"chars": 27,
"preview": "CPRHUVTQKXB6SAAAAAAAAAAAAA\n"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/App.kt",
"chars": 349,
"preview": "package net.obscura.vpnclientapp\n\nimport android.app.Application\nimport dagger.hilt.android.HiltAndroidApp\nimport net.ob"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/activities/MainActivity.kt",
"chars": 4398,
"preview": "package net.obscura.vpnclientapp.activities\n\nimport android.content.ComponentName\nimport android.content.Intent\nimport a"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/client/ErrorCodeException.kt",
"chars": 834,
"preview": "package net.obscura.vpnclientapp.client\n\nimport androidx.annotation.Keep\n\n// This `Keep` annotation is applied defensive"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/client/JsonConfig.kt",
"chars": 291,
"preview": "package net.obscura.vpnclientapp.client\n\nimport kotlinx.serialization.json.ClassDiscriminatorMode\nimport kotlinx.seriali"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/client/ManagerCmd.kt",
"chars": 1287,
"preview": "package net.obscura.vpnclientapp.client\n\nimport kotlinx.serialization.KeepGeneratedSerializer\nimport kotlinx.serializati"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/client/ManagerCmdOk.kt",
"chars": 1916,
"preview": "package net.obscura.vpnclientapp.client\n\nimport kotlinx.serialization.KeepGeneratedSerializer\nimport kotlinx.serializati"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/client/ObscuraLibrary.java",
"chars": 1256,
"preview": "package net.obscura.vpnclientapp.client;\n\nimport android.app.Application;\nimport android.content.Context;\nimport android"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/client/RustFfi.kt",
"chars": 1002,
"preview": "package net.obscura.vpnclientapp.client\n\nimport android.content.Context\nimport java.util.concurrent.CompletableFuture\nim"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/helpers/Process.kt",
"chars": 657,
"preview": "package net.obscura.vpnclientapp.helpers\n\nimport android.app.Application\n\n/** Ensures the calling process is :vpnservice"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/preferences/Preferences.kt",
"chars": 1352,
"preview": "package net.obscura.vpnclientapp.preferences\n\nimport android.content.Context\nimport android.content.SharedPreferences\nim"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/services/ContextExtension.kt",
"chars": 2886,
"preview": "package net.obscura.vpnclientapp.services\n\nimport android.content.Context\nimport android.content.Context.BIND_AUTO_CREAT"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/services/IntentExtension.kt",
"chars": 1678,
"preview": "package net.obscura.vpnclientapp.services\n\nimport android.content.Intent\nimport net.obscura.lib.util.Logger\nimport net.o"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/services/ObscuraVpnService.kt",
"chars": 15107,
"preview": "package net.obscura.vpnclientapp.services\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/services/OsNetworkConfig.kt",
"chars": 306,
"preview": "package net.obscura.vpnclientapp.services\n\nimport kotlinx.serialization.Serializable\n\n// Keep synchronized with rustlib/"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/sharing/DebugArchiveFileProvider.java",
"chars": 564,
"preview": "package net.obscura.vpnclientapp.sharing;\n\nimport androidx.core.content.FileProvider;\nimport net.obscura.vpnclientapp.R;"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/BillingModule.kt",
"chars": 917,
"preview": "package net.obscura.vpnclientapp.ui\n\nimport android.content.Context\nimport dagger.Module\nimport dagger.Provides\nimport d"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/JsonFfiBroadcastReceiver.kt",
"chars": 1596,
"preview": "package net.obscura.vpnclientapp.ui\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport andr"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/NetworkStatusObserver.kt",
"chars": 1827,
"preview": "package net.obscura.vpnclientapp.ui\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport androi"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/ObscuraUI.kt",
"chars": 8776,
"preview": "package net.obscura.vpnclientapp.ui\n\nimport android.content.Context\nimport android.net.Uri\nimport android.util.Attribute"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/ObscuraWebView.kt",
"chars": 3407,
"preview": "package net.obscura.vpnclientapp.ui\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport androi"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/OsStatus.kt",
"chars": 1439,
"preview": "package net.obscura.vpnclientapp.ui\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/OsStatusManager.kt",
"chars": 3160,
"preview": "package net.obscura.vpnclientapp.ui\n\nimport android.content.Context\nimport dagger.hilt.android.qualifiers.ApplicationCon"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/VpnPermissionRequestManager.kt",
"chars": 5723,
"preview": "package net.obscura.vpnclientapp.ui\n\nimport android.Manifest\nimport android.app.Activity.RESULT_CANCELED\nimport android."
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/VpnStatusObserver.kt",
"chars": 2542,
"preview": "package net.obscura.vpnclientapp.ui\n\nimport androidx.lifecycle.DefaultLifecycleObserver\nimport androidx.lifecycle.Lifecy"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/bridge/WebCmd.kt",
"chars": 7753,
"preview": "package net.obscura.vpnclientapp.ui.bridge\n\nimport android.content.Context\nimport kotlinx.serialization.KeepGeneratedSer"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/bridge/WebCmdBridge.kt",
"chars": 2571,
"preview": "package net.obscura.vpnclientapp.ui.bridge\n\nimport android.content.Context\nimport android.webkit.JavascriptInterface\nimp"
},
{
"path": "android/app/src/main/java/net/obscura/vpnclientapp/ui/bridge/WebCmdHelpers.kt",
"chars": 1359,
"preview": "package net.obscura.vpnclientapp.ui.bridge\n\nimport android.content.Context\nimport android.content.Intent\nimport java.io."
},
{
"path": "android/app/src/main/res/drawable/ic_launcher.xml",
"chars": 359,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xml"
},
{
"path": "android/app/src/main/res/drawable/icon_about.xml",
"chars": 844,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"48dp\"\n android:height=\"48dp\"\n "
},
{
"path": "android/app/src/main/res/drawable/icon_account.xml",
"chars": 1123,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"48dp\"\n android:height=\"48dp\"\n "
},
{
"path": "android/app/src/main/res/drawable/icon_connection.xml",
"chars": 1220,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"48dp\"\n android:height=\"48dp\"\n "
},
{
"path": "android/app/src/main/res/drawable/icon_location.xml",
"chars": 1111,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"48dp\"\n android:height=\"48dp\"\n "
},
{
"path": "android/app/src/main/res/drawable/icon_settings.xml",
"chars": 1015,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"48dp\"\n android:height=\"48dp\"\n "
},
{
"path": "android/app/src/main/res/layout/activity_main.xml",
"chars": 1218,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<net.obscura.vpnclientapp.ui.ObscuraUI xmlns:android=\"http://schemas.android.com"
},
{
"path": "android/app/src/main/res/menu/nav_menu.xml",
"chars": 827,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <item\n "
},
{
"path": "android/app/src/main/res/values/colors.xml",
"chars": 514,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"black\">#ff000000</color>\n <color name=\"white\">#ff"
},
{
"path": "android/app/src/main/res/values/ic_launcher_background.xml",
"chars": 120,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"ic_launcher_background\">#F55D24</color>\n</resources>"
},
{
"path": "android/app/src/main/res/values/ids.xml",
"chars": 114,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <item name=\"notification_id_vpn\" type=\"id\" />\n</resources>\n"
},
{
"path": "android/app/src/main/res/values/strings.xml",
"chars": 720,
"preview": "<resources>\n <string name=\"app_name\" translatable=\"false\">Obscura VPN</string>\n <string name=\"notification_channel"
},
{
"path": "android/app/src/main/res/values/themes.xml",
"chars": 691,
"preview": "<resources>\n <style name=\"Theme.ObscuraVPN\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n <item name=\"colorP"
},
{
"path": "android/app/src/main/res/values-night/themes.xml",
"chars": 692,
"preview": "<resources>\n <style name=\"Theme.ObscuraVPN\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n <item name=\"colorP"
},
{
"path": "android/app/src/main/res/xml/backup_rules.xml",
"chars": 479,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Sample backup rules file; uncomment and customize as necessary.\n See htt"
},
{
"path": "android/app/src/main/res/xml/data_extraction_rules.xml",
"chars": 552,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Sample data extraction rules file; uncomment and customize as necessary.\n "
},
{
"path": "android/app/src/main/res/xml/debug_archive_file_provider_paths.xml",
"chars": 120,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n <cache-path name=\"debug-archives\" path=\"debug-archives/\" />\n</paths>\n"
},
{
"path": "android/app/src/play/java/net/obscura/vpnclientapp/BillingFacade.kt",
"chars": 906,
"preview": "package net.obscura.vpnclientapp\n\nimport android.content.Context\nimport net.obscura.lib.billing.BillingManager\nimport ne"
},
{
"path": "android/build.gradle.kts",
"chars": 1446,
"preview": "import com.ncorti.ktfmt.gradle.KtfmtExtension\nimport com.ncorti.ktfmt.gradle.TrailingCommaManagementStrategy\nimport io.g"
},
{
"path": "android/buildSrc/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "android/buildSrc/build.gradle.kts",
"chars": 792,
"preview": "import com.ncorti.ktfmt.gradle.KtfmtExtension\nimport com.ncorti.ktfmt.gradle.TrailingCommaManagementStrategy\nimport io.g"
},
{
"path": "android/buildSrc/settings.gradle.kts",
"chars": 316,
"preview": "dependencyResolutionManagement {\n @Suppress(\"UnstableApiUsage\")\n repositories {\n google()\n mavenCent"
},
{
"path": "android/buildSrc/src/main/kotlin/VersionName.kt",
"chars": 1366,
"preview": "import java.io.File\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport org.gradle.a"
},
{
"path": "android/detekt.yml",
"chars": 22423,
"preview": "build:\n maxIssues: 0\n excludeCorrectable: false\n weights:\n # complexity: 2\n # LongParameterList: 1\n # style:"
},
{
"path": "android/gradle/libs.versions.toml",
"chars": 2694,
"preview": "[versions]\nandroid-billingclient = \"8.3.0\"\nandroid-gradle-plugin = \"8.13.0\"\nandroidx-junit = \"1.3.0\"\nandroidx-lifecycle "
},
{
"path": "android/gradle/mitm-cache/deps.json",
"chars": 134446,
"preview": "{\n \"!comment\": \"This is a nixpkgs Gradle dependency lockfile. For more details, refer to the Gradle section in the nixpk"
},
{
"path": "android/gradle/wrapper/gradle-wrapper.properties",
"chars": 232,
"preview": "#Mon Sep 15 22:54:28 CEST 2025\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\:/"
},
{
"path": "android/gradle.properties",
"chars": 1372,
"preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
},
{
"path": "android/gradlew",
"chars": 5766,
"preview": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0"
},
{
"path": "android/gradlew.bat",
"chars": 2763,
"preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
},
{
"path": "android/lib/billing/build.gradle.kts",
"chars": 715,
"preview": "import com.android.build.api.dsl.LibraryExtension\n\nplugins {\n alias(libs.plugins.android.library)\n alias(libs.plug"
},
{
"path": "android/lib/billing/src/main/AndroidManifest.xml",
"chars": 111,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" />\n"
},
{
"path": "android/lib/billing/src/main/java/net/obscura/lib/billing/BillingConnection.kt",
"chars": 2653,
"preview": "package net.obscura.lib.billing\n\nimport android.content.Context\nimport com.android.billingclient.api.BillingClient\nimpor"
},
{
"path": "android/lib/billing/src/main/java/net/obscura/lib/billing/BillingManager.kt",
"chars": 5633,
"preview": "package net.obscura.lib.billing\n\nimport android.app.Activity\nimport android.content.Context\nimport com.android.billingcl"
},
{
"path": "android/lib/util/build.gradle.kts",
"chars": 496,
"preview": "import com.android.build.api.dsl.LibraryExtension\n\nplugins {\n alias(libs.plugins.android.library)\n alias(libs.plug"
},
{
"path": "android/lib/util/src/main/AndroidManifest.xml",
"chars": 111,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" />\n"
},
{
"path": "android/lib/util/src/main/java/net/obscura/lib/util/ExternallyTaggedEnumSerializer.kt",
"chars": 681,
"preview": "package net.obscura.lib.util\n\nimport kotlin.reflect.KClass\nimport kotlinx.serialization.KSerializer\nimport kotlinx.seria"
},
{
"path": "android/lib/util/src/main/java/net/obscura/lib/util/ExternallyTaggedEnumVariantSerializer.kt",
"chars": 713,
"preview": "package net.obscura.lib.util\n\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.json.JsonElement\nimp"
},
{
"path": "android/lib/util/src/main/java/net/obscura/lib/util/Log.kt",
"chars": 1890,
"preview": "package net.obscura.lib.util\n\nimport android.util.Log\nimport kotlin.reflect.KClass\n\nenum class LogLevel {\n TRACE,\n "
},
{
"path": "android/lint.xml",
"chars": 176,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<lint>\n <!-- We can't update AGP until nixpkgs updates it -->\n <issue id=\"A"
},
{
"path": "android/settings.gradle.kts",
"chars": 904,
"preview": "pluginManagement {\n repositories {\n google {\n content {\n includeGroupByRegex(\"com\\\\."
},
{
"path": "apple/Configurations/.gitignore",
"chars": 23,
"preview": "/buildversion.xcconfig\n"
},
{
"path": "apple/Configurations/Base.xcconfig",
"chars": 132,
"preview": "#include? \"buildversion.xcconfig\"\n#include \"bundle-ids.xcconfig\"\n\nIPHONEOS_DEPLOYMENT_TARGET = 18.0\nMACOSX_DEPLOYMENT_TA"
},
{
"path": "apple/Configurations/Debug-app-network-extension.xcconfig",
"chars": 282,
"preview": "// Avoid accidental checkins:\n// git update-index --skip-worktree apple/Configurations/Debug*.xcconfig\n// git update-ind"
},
{
"path": "apple/Configurations/Debug-app.xcconfig",
"chars": 264,
"preview": "// Avoid accidental checkins:\n// git update-index --skip-worktree apple/Configurations/Debug*.xcconfig\n// git update-ind"
},
{
"path": "apple/Configurations/Debug-system-network-extension.xcconfig",
"chars": 285,
"preview": "// Avoid accidental checkins:\n// git update-index --skip-worktree apple/Configurations/Debug*.xcconfig\n// git update-ind"
},
{
"path": "apple/Configurations/Debug.xcconfig",
"chars": 430,
"preview": "// Avoid accidental checkins:\n// git update-index --skip-worktree apple/Configurations/Debug*.xcconfig\n// git update-ind"
},
{
"path": "apple/Configurations/Release-app-network-extension.xcconfig",
"chars": 70,
"preview": "#include \"Release.xcconfig\"\n#include \"app-network-extension.xcconfig\"\n"
},
{
"path": "apple/Configurations/Release-app.xcconfig",
"chars": 115,
"preview": "#include \"Release.xcconfig\"\n#include \"app.xcconfig\"\n\nPROVISIONING_PROFILE_SPECIFIER = Developer ID: VPN Client App\n"
},
{
"path": "apple/Configurations/Release-system-network-extension.xcconfig",
"chars": 146,
"preview": "#include \"Release.xcconfig\"\n#include \"system-network-extension.xcconfig\"\n\nPROVISIONING_PROFILE_SPECIFIER = Developer ID:"
},
{
"path": "apple/Configurations/Release.xcconfig",
"chars": 337,
"preview": "#include \"Base.xcconfig\"\n\nCODE_SIGN_IDENTITY = Developer ID Application: Sovereign Engineering Inc. (5G943LR562)\nCODE_SI"
},
{
"path": "apple/Configurations/app-network-extension.xcconfig",
"chars": 303,
"preview": "PRODUCT_BUNDLE_IDENTIFIER = $(OBSCURA_APP_NETWORK_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER)\nINFOPLIST_KEY_CFBundleDisplayName"
},
{
"path": "apple/Configurations/app.xcconfig",
"chars": 135,
"preview": "PRODUCT_BUNDLE_IDENTIFIER = $(OBSCURA_APP_PRODUCT_BUNDLE_IDENTIFIER)\n\nINFOPLIST_FILE = client/Info.plist\nGENERATE_INFOPL"
},
{
"path": "apple/Configurations/bundle-ids.xcconfig",
"chars": 1163,
"preview": "OBSCURA_SYSTEM_NETWORK_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(OBSCURA_APP_PRODUCT_BUNDLE_IDENTIFIER).system-network-ext"
},
{
"path": "apple/Configurations/system-network-extension.xcconfig",
"chars": 312,
"preview": "PRODUCT_BUNDLE_IDENTIFIER = $(OBSCURA_SYSTEM_NETWORK_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER)\nINFOPLIST_KEY_CFBundleDisplayN"
},
{
"path": "apple/ExportOptions.plist",
"chars": 720,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "apple/Packet Tunnel Provider/Keychain.swift",
"chars": 1490,
"preview": "import Foundation\nimport Security\n\nprivate let wgSecretKeyquery: [String: Any] = [\n kSecClass as String: kSecClassGen"
},
{
"path": "apple/Packet Tunnel Provider/NetworkSettings.swift",
"chars": 2721,
"preview": "import Foundation\nimport NetworkExtension\n\nextension NEPacketTunnelNetworkSettings {\n static func build(_ osNetworkCo"
},
{
"path": "apple/Packet Tunnel Provider/PacketTunnelProvider.swift",
"chars": 21568,
"preview": "import Combine\nimport libobscuravpn_client\nimport NetworkExtension\nimport OSLog\nimport UniformTypeIdentifiers\nimport Use"
},
{
"path": "apple/Packet Tunnel Provider/RustFfi.swift",
"chars": 4525,
"preview": "import Foundation\nimport libobscuravpn_client\nimport Network\n\nfunc ffiInitializeSystemLogging(_ logDir: String?) -> Opaq"
},
{
"path": "apple/Packet Tunnel Provider/main.swift",
"chars": 311,
"preview": "import Foundation\nimport NetworkExtension\n\n// TODO: Use `std::panic::set_backtrace_style()` in Rust initialization once "
},
{
"path": "apple/app-network-extension/Info.plist",
"chars": 2113,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "apple/app-network-extension/entitlements.entitlements",
"chars": 587,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "apple/cbindgen-apple.toml",
"chars": 651,
"preview": "# NOTE: This is a `cbindgen` config that's specific to the apple platforms, as\n# only those platformw will have the Targ"
},
{
"path": "apple/client/Assets.xcassets/AccentColor.colorset/Contents.json",
"chars": 123,
"preview": "{\n \"colors\" : [\n {\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }"
},
{
"path": "apple/client/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1451,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Icon 04 1024w.png\",\n \"idiom\" : \"universal\",\n \"platform\" : \"ios\",\n "
},
{
"path": "apple/client/Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "apple/client/Assets.xcassets/DecoPrimer.imageset/Contents.json",
"chars": 309,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"deco-primer.svg\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "apple/client/Assets.xcassets/EmotePrimer.imageset/Contents.json",
"chars": 307,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"DecoEmote.svg\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "apple/client/Assets.xcassets/MenuBarConnected.imageset/Contents.json",
"chars": 379,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Connected.svg\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "apple/client/Assets.xcassets/MenuBarConnectedDown.imageset/Contents.json",
"chars": 421,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Connected - Down Cutout.svg\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1"
},
{
"path": "apple/client/Assets.xcassets/MenuBarConnectedUp.imageset/Contents.json",
"chars": 415,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Connected - Up Cutout.svg\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\""
},
{
"path": "apple/client/Assets.xcassets/MenuBarConnectedUpDown.imageset/Contents.json",
"chars": 430,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Connected - Up Down Cutout.svg\",\n \"idiom\" : \"universal\",\n \"scale\" :"
},
{
"path": "apple/client/Assets.xcassets/MenuBarConnecting-1.imageset/Contents.json",
"chars": 415,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Menu Bar Connecting 1.svg\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\""
},
{
"path": "apple/client/Assets.xcassets/MenuBarConnecting-2.imageset/Contents.json",
"chars": 415,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Menu Bar Connecting 2.svg\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\""
},
{
"path": "apple/client/Assets.xcassets/MenuBarConnecting-3.imageset/Contents.json",
"chars": 415,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Menu Bar Connecting 3.svg\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\""
},
{
"path": "apple/client/Assets.xcassets/MenuBarDisconnected.imageset/Contents.json",
"chars": 388,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Disconnected.svg\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "apple/client/Assets.xcassets/ObscuraOrange.colorset/Contents.json",
"chars": 481,
"preview": "{\n \"colors\" : [\n {\n \"color\" : {\n \"color-space\" : \"srgb\",\n \"components\" : {\n \"alpha\" : \"1"
},
{
"path": "apple/client/Assets.xcassets/UpdateAvailable.imageset/Contents.json",
"chars": 400,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"Update Available.svg\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n "
},
{
"path": "apple/client/Assets.xcassets/custom.globe.badge.gearshape.fill.symbolset/Contents.json",
"chars": 184,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n },\n \"symbols\" : [\n {\n \"filename\" : \"custom.globe.bad"
},
{
"path": "apple/client/Constants.swift",
"chars": 1036,
"preview": "import Foundation\n\nenum UserDefaultKeys {\n static let LoginItemRegistered = \"LoginItemRegistered\"\n static let Sele"
},
{
"path": "apple/client/ContentView.swift",
"chars": 13720,
"preview": "import OrderedCollections\nimport OSLog\nimport SwiftUI\n#if !os(macOS)\n import UIKit\n#endif\nimport UniformTypeIdentifie"
},
{
"path": "apple/client/DebugBundle+XP.swift",
"chars": 932,
"preview": "import Foundation\n\npublic class DebugBundleStatus: Encodable {\n var inProgressCounter: Int = 0\n var inProgress: Bo"
},
{
"path": "apple/client/DebugBundle.swift",
"chars": 34786,
"preview": "#if os(macOS)\n import AppKit\n#else\n import StoreKit\n import UIKit\n#endif\nimport Foundation\nimport NetworkExtens"
},
{
"path": "apple/client/DebugBundleExtensionInfo.swift",
"chars": 1875,
"preview": "import Foundation\nimport NetworkExtension\nimport OSLog\nimport SystemExtensions\n\nprivate let logger = Logger(subsystem: B"
},
{
"path": "apple/client/Info.plist",
"chars": 2703,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "apple/client/LoginItem.swift",
"chars": 1556,
"preview": "import OSLog\nimport ServiceManagement\n\nprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: \""
},
{
"path": "apple/client/LoopingVideoPlayer.swift",
"chars": 1210,
"preview": "import AVKit\nimport SwiftUI\n\nstruct LoopingVideoPlayer: View {\n @State private var player: AVQueuePlayer\n @State p"
},
{
"path": "apple/client/Notifications.swift",
"chars": 1730,
"preview": "import OSLog\nimport UserNotifications\n\nprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: \""
},
{
"path": "apple/client/OSLogEntryEncodable.swift",
"chars": 4160,
"preview": "import OSLog\n\nenum OSLogEntryCodingKeys: String, CodingKey {\n case activityIdentifier\n case category\n case comp"
},
{
"path": "apple/client/OsStatus.swift",
"chars": 4489,
"preview": "import Foundation\n#if os(iOS)\n import MessageUI\n#endif\nimport Network\nimport NetworkExtension\nimport OSLog\n\nprivate l"
},
{
"path": "apple/client/Preview Content/Preview Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "apple/client/ScriptMessageHandlers.swift",
"chars": 2165,
"preview": "import Foundation\nimport OSLog\nimport WebKit\n\nprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, cate"
},
{
"path": "apple/client/StartupStatus.swift",
"chars": 330,
"preview": "enum StartupStatus {\n case initial\n #if os(macOS)\n case networkExtensionInit(NetworkExtensionInit, NetworkE"
},
{
"path": "apple/client/StatusItem/AccountStatusItem.swift",
"chars": 2089,
"preview": "import Cocoa\nimport OSLog\nimport SwiftUI\nimport UserNotifications\n\nfunc getExpiredInDaysText(_ days: UInt64) -> String {"
},
{
"path": "apple/client/StatusItem/BandwidthStatus.swift",
"chars": 3602,
"preview": "import Cocoa\nimport SwiftUI\n\nclass BandwidthStatusModel: ObservableObject {\n @Published var uploadBandwidth = Bandwid"
},
{
"path": "apple/client/StatusItem/MenuItemView.swift",
"chars": 5140,
"preview": "import Cocoa\nimport SwiftUI\n\n// https://github.com/j-f1/MenuBuilder/blob/ba0202c5ff6d63f0fd7ec6b1da11a769eff15000/Source"
},
{
"path": "apple/client/StatusItem/ObscuraToggle.swift",
"chars": 9171,
"preview": "import Cocoa\nimport OSLog\nimport SwiftUI\nimport UserNotifications\n\nprivate let logger = Logger(subsystem: Bundle.main.bu"
},
{
"path": "apple/client/StatusItem/StatusMenu.swift",
"chars": 31264,
"preview": "import AppKit\nimport Combine\nimport OSLog\nimport SwiftUI\nimport UserNotifications\n\nprivate let logger = Logger(subsystem"
},
{
"path": "apple/client/Store/Obscura VPN Local.storekit",
"chars": 2999,
"preview": "{\n \"appPolicies\" : {\n \"eula\" : \"\",\n \"policies\" : [\n {\n \"locale\" : \"en_US\",\n \"policyText\" : \"\","
},
{
"path": "apple/client/Store/Obscura VPN.storekit",
"chars": 2513,
"preview": "{\n \"appPolicies\" : {\n \"eula\" : \"\",\n \"policies\" : [\n {\n \"locale\" : \"en_US\",\n \"policyText\" : \"\","
},
{
"path": "apple/client/Store/Product+Convenience.swift",
"chars": 241,
"preview": "import StoreKit\n\nextension Product {\n func subscriptionPeriodFormatted() -> String? {\n guard let subscription "
},
{
"path": "apple/client/Store/StoreKitListener.swift",
"chars": 2167,
"preview": "import os\nimport StoreKit\n\nprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: \"StoreKitList"
},
{
"path": "apple/client/Store/StoreKitModel.swift",
"chars": 5207,
"preview": "import os\nimport StoreKit\n\nprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: \"StoreKitMode"
},
{
"path": "apple/client/Style/Appearance.swift",
"chars": 309,
"preview": "import SwiftUI\n\nenum AppAppearance: String, Codable {\n case dark\n case light\n case auto\n\n var colorScheme: C"
},
{
"path": "apple/client/Style/ConditionallyDisabled.swift",
"chars": 931,
"preview": "import SwiftUI\n\nstruct ConditionallyDisabledModifier: ViewModifier {\n let isDisabled: Bool\n let explanation: Strin"
},
{
"path": "apple/client/Style/HyperlinkButtonStyle.swift",
"chars": 285,
"preview": "import SwiftUI\n\nstruct HyperlinkButtonStyle: ButtonStyle {\n @Environment(\\.isEnabled) private var isEnabled\n\n func"
},
{
"path": "apple/client/Style/NoFadeButtonStyle.swift",
"chars": 604,
"preview": "import SwiftUI\n\nstruct NoFadeButtonStyle: ButtonStyle {\n var backgroundColor: Color = .init(\"ObscuraOrange\")\n @Env"
},
{
"path": "apple/client/TunnelProvider.swift",
"chars": 13058,
"preview": "import Foundation\nimport Network\nimport NetworkExtension\nimport OSLog\n\nprivate let logger = Logger(subsystem: Bundle.mai"
},
{
"path": "apple/client/UXKit/UXImage.swift",
"chars": 619,
"preview": "/*\n Many UIKit and AppKit classes have fairly similar interfaces\n To that end you can get away with code like this. Ther"
},
{
"path": "apple/client/UXKit/UXViewController.swift",
"chars": 452,
"preview": "/*\n Many UIKit and AppKit classes have fairly similar interfaces\n To that end you can get away with code like this. Ther"
},
{
"path": "apple/client/UXKit/UXViewRepresentable.swift",
"chars": 443,
"preview": "/*\n Many UIKit and AppKit classes have fairly similar interfaces\n To that end you can get away with code like this. Ther"
},
{
"path": "apple/client/UpdaterDriver+XP.swift",
"chars": 688,
"preview": "import Foundation\n\nenum UpdaterStatusType: String, Codable {\n case uninitiated\n case initiated\n case available\n"
},
{
"path": "apple/client/Webviews/ExternalWebView.swift",
"chars": 935,
"preview": "import WebKit\n\nstruct ExternalWebView: UXViewRepresentable {\n let webView: WKWebView\n\n init(appState: AppState) {\n"
},
{
"path": "apple/client/Webviews/ObscuraUIIOSWrapperAndTabs.swift",
"chars": 4921,
"preview": "import Combine\nimport OrderedCollections\nimport SwiftUI\nimport UIKit\nimport WebKit\n\n// In SwiftUI in iOS targeting a min"
},
{
"path": "apple/client/Webviews/ObscuraUIMacOSWrapper.swift",
"chars": 536,
"preview": "import SwiftUI\nimport WebKit\n\nstruct ObscuraUIMacOSWrapper: UXViewRepresentable {\n let webView: ObscuraUIWebView\n\n "
},
{
"path": "apple/client/Webviews/ObscuraUIWebView.swift",
"chars": 4899,
"preview": "import SwiftUI\nimport WebKit\n\nclass ObscuraUIWebView: WKWebView {\n init(appState: AppState) {\n let webConfigur"
},
{
"path": "apple/client/Webviews/ObscuraUIWebViewMacOSWrapper.swift",
"chars": 555,
"preview": "import SwiftUI\nimport WebKit\n\nstruct ObscuraUIWebViewMacOSWrapper: View {\n let webView: ObscuraUIWebView\n\n init(we"
},
{
"path": "apple/client/Webviews/WebviewsController.swift",
"chars": 5631,
"preview": "import OSLog\nimport SwiftUI\nimport WebKit\n\nprivate let logger = Logger(\n subsystem: Bundle.main.bundleIdentifier!,\n "
},
{
"path": "apple/client/app_state.swift",
"chars": 20941,
"preview": "import Foundation\n#if os(iOS)\n import MessageUI\n import StoreKit\n#endif\nimport NetworkExtension\nimport OSLog\nimpor"
},
{
"path": "apple/client/client-ios.entitlements",
"chars": 610,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "apple/client/client-macos.entitlements",
"chars": 729,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "apple/client/command.swift",
"chars": 5478,
"preview": "#if os(macOS)\n import AppKit\n#endif\nimport Foundation\nimport OSLog\n\nprivate let logger = Logger(subsystem: Bundle.mai"
},
{
"path": "apple/client/extensions/NEVPNStatus.swift",
"chars": 742,
"preview": "import Foundation\nimport NetworkExtension\n\nextension NEVPNStatus: Encodable {\n public func encode(to encoder: any Enc"
},
{
"path": "apple/client/iOS/MailDelegate.swift",
"chars": 836,
"preview": "import MessageUI\nimport OSLog\n\nprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: \"Mail\")\n\n"
},
{
"path": "apple/client/iOS/iOSClientApp.swift",
"chars": 686,
"preview": "import OSLog\nimport SwiftUI\n\nprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: \"App\")\n\n@ma"
},
{
"path": "apple/client/initNetworkExtension.swift",
"chars": 8882,
"preview": "import Foundation\nimport NetworkExtension\nimport OSLog\nimport SystemExtensions\n\nprivate let logger = Logger(subsystem: B"
},
{
"path": "apple/client/macOS/CheckForUpdatesView.swift",
"chars": 831,
"preview": "import Sparkle\nimport SwiftUI\n\n/**\n This is the view for the Check for Updates menu item\n\n Note this intermediate view i"
},
{
"path": "apple/client/macOS/ClientApp.swift",
"chars": 14921,
"preview": "import Combine\nimport Network\nimport NetworkExtension\nimport OSLog\nimport Sparkle\nimport SwiftUI\nimport SystemExtensions"
},
{
"path": "apple/client/macOS/InstallSystemExtensionView.swift",
"chars": 4214,
"preview": "import SwiftUI\n\nlet macOS14DemoVideo = Bundle.main.url(forResource: \"videos/macOS 14 System Extension Demo\", withExtensi"
},
{
"path": "apple/client/macOS/RegisterLoginItemView.swift",
"chars": 1218,
"preview": "import SwiftUI\n\nstruct RegisterLoginItemView: View {\n var value: ObservableValue<Bool>\n @Environment(\\.openURL) pr"
},
{
"path": "apple/client/macOS/SparkleUpdater.swift",
"chars": 1589,
"preview": "import Combine\nimport Sparkle\n\nclass SparkleUpdater {\n /**\n Sparkle updater.\n\n - seealso: [How to integrate t"
},
{
"path": "apple/client/macOS/UpdateSystemExtensionView.swift",
"chars": 1045,
"preview": "import SwiftUI\n\nstruct UpdateSystemExtensionView: View {\n @ObservedObject var startupModel: StartupModel\n var subt"
},
{
"path": "apple/client/macOS/UpdaterDriver.swift",
"chars": 4161,
"preview": "import OSLog\nimport Sparkle\n\nprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: \"UpdaterDri"
},
{
"path": "apple/client/startup.swift",
"chars": 11501,
"preview": "import AVKit\nimport NetworkExtension\nimport OSLog\nimport SwiftUI\nimport UserNotifications\n\nprivate let logger = Logger(s"
},
{
"path": "apple/client.xcodeproj/project.pbxproj",
"chars": 102073,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 73;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "apple/client.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 135,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:\">\n </FileRef"
},
{
"path": "apple/client.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "apple/client.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
"chars": 181,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
}
]
// ... and 243 more files (download for full content)
About this extraction
This page contains the full source code of the Sovereign-Engineering/obscuravpn-client GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 443 files (1.4 MB), approximately 416.0k tokens, and a symbol index with 987 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.