react-native-ovpn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/Openvpn.podspec +34 -0
- package/README.md +80 -0
- package/android/build.gradle +98 -0
- package/android/libs/README.md +46 -0
- package/android/libs/ics-openvpn.aar +0 -0
- package/android/src/main/AndroidManifest.xml +54 -0
- package/android/src/main/java/com/openvpn/NotificationHelper.kt +59 -0
- package/android/src/main/java/com/openvpn/OpenvpnEventBus.kt +52 -0
- package/android/src/main/java/com/openvpn/OpenvpnException.kt +6 -0
- package/android/src/main/java/com/openvpn/OpenvpnModule.kt +140 -0
- package/android/src/main/java/com/openvpn/OpenvpnPackage.kt +31 -0
- package/android/src/main/java/com/openvpn/OpenvpnService.kt +248 -0
- package/android/src/main/java/com/openvpn/PermissionLauncher.kt +39 -0
- package/android/src/main/java/com/openvpn/ProfileBuilder.kt +68 -0
- package/android/src/main/res/drawable/ic_vpn_default.xml +10 -0
- package/android/src/main/res/values/strings.xml +6 -0
- package/android/src/test/java/com/openvpn/NotificationHelperTest.kt +49 -0
- package/android/src/test/java/com/openvpn/ProfileBuilderTest.kt +83 -0
- package/app.plugin.js +3 -0
- package/ios/Openvpn-Bridging-Header.h +8 -0
- package/ios/Openvpn.h +5 -0
- package/ios/Openvpn.mm +123 -0
- package/ios/OpenvpnAppGroup.swift +59 -0
- package/ios/OpenvpnConstants.swift +46 -0
- package/ios/OpenvpnEventBridge.swift +58 -0
- package/ios/OpenvpnManager.swift +219 -0
- package/ios/PacketTunnelProvider/Info.plist +31 -0
- package/ios/PacketTunnelProvider/PacketTunnelProvider.swift +199 -0
- package/ios/PacketTunnelProvider/README.md +106 -0
- package/lib/module/NativeOpenvpn.js +5 -0
- package/lib/module/NativeOpenvpn.js.map +1 -0
- package/lib/module/OpenVPNClient.js +185 -0
- package/lib/module/OpenVPNClient.js.map +1 -0
- package/lib/module/errors.js +13 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/reconnect.js +51 -0
- package/lib/module/reconnect.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/plugin/src/android/index.d.ts +5 -0
- package/lib/typescript/plugin/src/android/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidAarCheck.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidAarCheck.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidLegacyPackaging.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidLegacyPackaging.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidMinSdk.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidMinSdk.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidNotificationIcon.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidNotificationIcon.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidPermissions.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidPermissions.d.ts.map +1 -0
- package/lib/typescript/plugin/src/android/withAndroidService.d.ts +5 -0
- package/lib/typescript/plugin/src/android/withAndroidService.d.ts.map +1 -0
- package/lib/typescript/plugin/src/index.d.ts +6 -0
- package/lib/typescript/plugin/src/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/index.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/index.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/withIosDeploymentTarget.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/withIosDeploymentTarget.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/withIosEntitlements.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/withIosEntitlements.d.ts.map +1 -0
- package/lib/typescript/plugin/src/ios/withIosInfoPlist.d.ts +5 -0
- package/lib/typescript/plugin/src/ios/withIosInfoPlist.d.ts.map +1 -0
- package/lib/typescript/plugin/src/types.d.ts +14 -0
- package/lib/typescript/plugin/src/types.d.ts.map +1 -0
- package/lib/typescript/src/NativeOpenvpn.d.ts +41 -0
- package/lib/typescript/src/NativeOpenvpn.d.ts.map +1 -0
- package/lib/typescript/src/OpenVPNClient.d.ts +37 -0
- package/lib/typescript/src/OpenVPNClient.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +9 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/reconnect.d.ts +23 -0
- package/lib/typescript/src/reconnect.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +41 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +193 -0
- package/plugin/build/android/index.d.ts +4 -0
- package/plugin/build/android/index.js +24 -0
- package/plugin/build/android/withAndroidAarCheck.d.ts +4 -0
- package/plugin/build/android/withAndroidAarCheck.js +60 -0
- package/plugin/build/android/withAndroidLegacyPackaging.d.ts +4 -0
- package/plugin/build/android/withAndroidLegacyPackaging.js +18 -0
- package/plugin/build/android/withAndroidMinSdk.d.ts +4 -0
- package/plugin/build/android/withAndroidMinSdk.js +13 -0
- package/plugin/build/android/withAndroidNotificationIcon.d.ts +4 -0
- package/plugin/build/android/withAndroidNotificationIcon.js +64 -0
- package/plugin/build/android/withAndroidPermissions.d.ts +4 -0
- package/plugin/build/android/withAndroidPermissions.js +30 -0
- package/plugin/build/android/withAndroidService.d.ts +4 -0
- package/plugin/build/android/withAndroidService.js +40 -0
- package/plugin/build/index.d.ts +5 -0
- package/plugin/build/index.js +18 -0
- package/plugin/build/ios/index.d.ts +4 -0
- package/plugin/build/ios/index.js +15 -0
- package/plugin/build/ios/withIosDeploymentTarget.d.ts +4 -0
- package/plugin/build/ios/withIosDeploymentTarget.js +28 -0
- package/plugin/build/ios/withIosEntitlements.d.ts +4 -0
- package/plugin/build/ios/withIosEntitlements.js +15 -0
- package/plugin/build/ios/withIosInfoPlist.d.ts +4 -0
- package/plugin/build/ios/withIosInfoPlist.js +14 -0
- package/plugin/build/types.d.ts +13 -0
- package/plugin/build/types.js +2 -0
- package/src/NativeOpenvpn.ts +46 -0
- package/src/OpenVPNClient.ts +239 -0
- package/src/errors.ts +29 -0
- package/src/index.ts +12 -0
- package/src/reconnect.ts +68 -0
- package/src/types.ts +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rasel
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/Openvpn.podspec
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "Openvpn"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => "13.0" }
|
|
14
|
+
s.source = { :git => "https://github.com/Raselj71/react-native-ovpn.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
+
s.private_header_files = "ios/*.h"
|
|
18
|
+
|
|
19
|
+
# The PacketTunnelProvider sources are templates copied into the consumer's
|
|
20
|
+
# own Network Extension target. They must NOT be compiled into our pod (host
|
|
21
|
+
# app code), so exclude them here.
|
|
22
|
+
s.exclude_files = "ios/PacketTunnelProvider/**/*"
|
|
23
|
+
|
|
24
|
+
# OpenVPNAdapter — AGPLv3, the canonical iOS OpenVPN binding.
|
|
25
|
+
s.dependency 'OpenVPNAdapter', '~> 0.8.0'
|
|
26
|
+
|
|
27
|
+
s.pod_target_xcconfig = {
|
|
28
|
+
"SWIFT_OBJC_BRIDGING_HEADER" => "$(PODS_TARGET_SRCROOT)/ios/Openvpn-Bridging-Header.h",
|
|
29
|
+
"DEFINES_MODULE" => "YES",
|
|
30
|
+
"SWIFT_VERSION" => "5.0"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
install_modules_dependencies(s)
|
|
34
|
+
end
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# react-native-ovpn
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-ovpn)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
OpenVPN client for React Native — Android, iOS, and Expo. New Architecture (TurboModule), bounded auto-reconnect, kill-switch, custom DNS.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { OpenVPNClient } from 'react-native-ovpn';
|
|
10
|
+
|
|
11
|
+
const client = new OpenVPNClient();
|
|
12
|
+
client.on('state', (s) => console.log(s)); // 'connecting' | 'connected' | ...
|
|
13
|
+
client.on('stats', ({ bytesIn, bytesOut }) => {});
|
|
14
|
+
|
|
15
|
+
await client.requestPermission();
|
|
16
|
+
await client.connect({ config: ovpnString, username: 'alice', password: 's3cret' });
|
|
17
|
+
await client.disconnect();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Documentation
|
|
21
|
+
|
|
22
|
+
- **[Getting started](docs/getting-started.md)** — install, Android setup, iOS setup, Expo setup
|
|
23
|
+
- **[API reference](docs/api.md)** — full method/event/type reference
|
|
24
|
+
- **[OpenVPN config support](docs/ovpn-support.md)** — which `.ovpn` directives work
|
|
25
|
+
- **[Recipes](docs/examples.md)** — kill-switch, custom DNS, reconnect, 2FA
|
|
26
|
+
- **[Troubleshooting](docs/troubleshooting.md)** — common errors and fixes
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install react-native-ovpn
|
|
32
|
+
# or
|
|
33
|
+
yarn add react-native-ovpn
|
|
34
|
+
# or
|
|
35
|
+
pnpm add react-native-ovpn
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Expo:** add to `app.config.js`:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
plugins: [
|
|
42
|
+
['react-native-ovpn', {
|
|
43
|
+
iosAppGroup: 'group.com.example.myapp.openvpn',
|
|
44
|
+
iosExtensionBundleIdentifier: 'com.example.myapp.OpenVPNTunnel',
|
|
45
|
+
}],
|
|
46
|
+
];
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then `npx expo prebuild --clean`.
|
|
50
|
+
|
|
51
|
+
**Bare React Native:** see [Getting started](docs/getting-started.md).
|
|
52
|
+
|
|
53
|
+
## Feature matrix
|
|
54
|
+
|
|
55
|
+
| Feature | Android | iOS |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| Connect / disconnect | ✅ | ✅ |
|
|
58
|
+
| State + stats + log events | ✅ | ✅ |
|
|
59
|
+
| Auto-reconnect (bounded backoff) | ✅ | ✅ |
|
|
60
|
+
| Username + password auth | ✅ | ✅ |
|
|
61
|
+
| Certificate-only auth | ✅ | ✅ |
|
|
62
|
+
| TLS-auth / tls-crypt / tls-crypt-v2 | ✅ | ✅ |
|
|
63
|
+
| Legacy cipher fallback (AES-128-CBC) | ✅ (auto-injected) | ✅ |
|
|
64
|
+
| Custom DNS override | ✅ | ✅ |
|
|
65
|
+
| Kill-switch | ✅ | ❌ |
|
|
66
|
+
| Foreground notification | ✅ | n/a |
|
|
67
|
+
| Per-app routing | ❌ | ❌ |
|
|
68
|
+
| Expo config plugin | ✅ | ✅ |
|
|
69
|
+
|
|
70
|
+
See [OpenVPN config support](docs/ovpn-support.md) for the full directive-level matrix.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
Wrapper code: MIT.
|
|
75
|
+
|
|
76
|
+
**Important** — Android embeds [ics-openvpn](https://github.com/schwabe/ics-openvpn) (**GPLv2**), iOS embeds [OpenVPNAdapter](https://github.com/ss-abramchuk/OpenVPNAdapter) (**AGPLv3**). Apps that ship `react-native-ovpn` inherit those copyleft obligations. Fine for personal, internal, or open-source apps; **not compatible** with closed-source commercial distribution without separate commercial agreements with the upstream projects.
|
|
77
|
+
|
|
78
|
+
## Contributing
|
|
79
|
+
|
|
80
|
+
[Development workflow](CONTRIBUTING.md) · [Code of conduct](CODE_OF_CONDUCT.md)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.Openvpn = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
targetSdkVersion: 36
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
ext.getExtOrDefault = { prop ->
|
|
10
|
+
if (rootProject.ext.has(prop)) {
|
|
11
|
+
return rootProject.ext.get(prop)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return Openvpn[prop]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
repositories {
|
|
18
|
+
google()
|
|
19
|
+
mavenCentral()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dependencies {
|
|
23
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
24
|
+
// noinspection DifferentKotlinGradleVersion
|
|
25
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
apply plugin: "com.android.library"
|
|
31
|
+
apply plugin: "kotlin-android"
|
|
32
|
+
|
|
33
|
+
apply plugin: "com.facebook.react"
|
|
34
|
+
|
|
35
|
+
android {
|
|
36
|
+
namespace "com.openvpn"
|
|
37
|
+
|
|
38
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
39
|
+
|
|
40
|
+
defaultConfig {
|
|
41
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
42
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildFeatures {
|
|
46
|
+
buildConfig true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
buildTypes {
|
|
50
|
+
release {
|
|
51
|
+
minifyEnabled false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lint {
|
|
56
|
+
disable "GradleCompatible"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
compileOptions {
|
|
60
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
61
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
testOptions {
|
|
65
|
+
unitTests {
|
|
66
|
+
includeAndroidResources = true
|
|
67
|
+
returnDefaultValues = true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
packagingOptions {
|
|
72
|
+
resources {
|
|
73
|
+
excludes += [
|
|
74
|
+
"META-INF/AL2.0",
|
|
75
|
+
"META-INF/LGPL2.1",
|
|
76
|
+
"**/*.po",
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
repositories {
|
|
83
|
+
google()
|
|
84
|
+
mavenCentral()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dependencies {
|
|
88
|
+
implementation "com.facebook.react:react-android"
|
|
89
|
+
// ics-openvpn engine, committed to the repo at android/libs/ — see
|
|
90
|
+
// android/libs/README.md for how it was produced.
|
|
91
|
+
implementation files("$projectDir/libs/ics-openvpn.aar")
|
|
92
|
+
implementation "androidx.core:core-ktx:1.13.1"
|
|
93
|
+
|
|
94
|
+
testImplementation "junit:junit:4.13.2"
|
|
95
|
+
testImplementation "org.robolectric:robolectric:4.13"
|
|
96
|
+
testImplementation "androidx.test:core:1.5.0"
|
|
97
|
+
testImplementation "androidx.test.ext:junit:1.1.5"
|
|
98
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# `android/libs/` — vendored AAR location
|
|
2
|
+
|
|
3
|
+
`ics-openvpn.aar` lives here and **is committed to the repository**. Consumers and CI never need to rebuild it. Rebuild only when bumping the ics-openvpn submodule pointer.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
android/libs/ics-openvpn.aar (~5–10 MB binary, tracked in git)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## How it was produced (rebuilding when needed)
|
|
10
|
+
|
|
11
|
+
ics-openvpn's upstream `main` Gradle module is an **application**, not a library, so we cannot consume it via a normal `implementation project(...)` dependency or a Maven coordinate. Instead, we build it locally once into an AAR and commit the binary.
|
|
12
|
+
|
|
13
|
+
Run the build script from the repo root:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
pwsh ./scripts/build-ics-openvpn-aar.ps1
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That script:
|
|
20
|
+
|
|
21
|
+
1. Patches `third_party/ics-openvpn/main/build.gradle.kts` in-place to use the `com.android.library` plugin (so the build emits an AAR).
|
|
22
|
+
2. Runs `./gradlew :main:assembleSkeletonOvpn23Release` inside the submodule (skeleton = no UI; ovpn23 = both OpenVPN 2.x and 3.x).
|
|
23
|
+
3. Copies the resulting AAR to `android/libs/ics-openvpn.aar`.
|
|
24
|
+
4. Reverts the gradle patch so the submodule stays clean.
|
|
25
|
+
|
|
26
|
+
After running the script, **commit the AAR** so other clones and CI don't need NDK:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git add android/libs/ics-openvpn.aar
|
|
30
|
+
git commit -m "build(android): vendor ics-openvpn AAR (v0.7.59 skeleton-ovpn23)"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Prerequisites for running the script
|
|
34
|
+
|
|
35
|
+
- Java 17+ (Temurin or Android Studio JBR)
|
|
36
|
+
- Android SDK with build-tools and platform-35
|
|
37
|
+
- Android NDK 27.x or 28.x
|
|
38
|
+
- CMake 3.22+ (Android SDK CMake component)
|
|
39
|
+
- SWIG (`choco install swig` on Windows; `brew install swig` on macOS)
|
|
40
|
+
- **Nested submodules initialized:** `git -C third_party/ics-openvpn submodule update --init --recursive` (pulls in asio, lz4, mbedtls, openssl, openvpn, openvpn3)
|
|
41
|
+
|
|
42
|
+
## When to rebuild
|
|
43
|
+
|
|
44
|
+
- After bumping the submodule pointer (security fixes, new ics-openvpn release)
|
|
45
|
+
- After changing the chosen flavor (e.g., `ovpn2` instead of `ovpn23`)
|
|
46
|
+
- Otherwise: never — the AAR is a stable artifact
|
|
Binary file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
4
|
+
|
|
5
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
6
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
7
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
8
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
|
9
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
10
|
+
<!-- ics-openvpn schedules a PERSISTED keepalive JobService that survives
|
|
11
|
+
reboots; the OS requires this permission on apps that do that. -->
|
|
12
|
+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
13
|
+
|
|
14
|
+
<!--
|
|
15
|
+
The bundled ics-openvpn.aar declares <application android:name=... label=... theme=...>
|
|
16
|
+
inherited from its original "application" build. Tell the manifest merger to drop
|
|
17
|
+
those attributes at this layer so they don't conflict with the consumer's app manifest.
|
|
18
|
+
-->
|
|
19
|
+
<application
|
|
20
|
+
tools:remove="android:name,android:label,android:theme,android:icon,android:allowBackup">
|
|
21
|
+
<service
|
|
22
|
+
android:name="com.openvpn.OpenvpnService"
|
|
23
|
+
android:permission="android.permission.BIND_VPN_SERVICE"
|
|
24
|
+
android:foregroundServiceType="specialUse"
|
|
25
|
+
android:exported="false">
|
|
26
|
+
<property
|
|
27
|
+
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
28
|
+
android:value="vpn" />
|
|
29
|
+
<intent-filter>
|
|
30
|
+
<action android:name="android.net.VpnService" />
|
|
31
|
+
</intent-filter>
|
|
32
|
+
</service>
|
|
33
|
+
|
|
34
|
+
<!--
|
|
35
|
+
ics-openvpn's OpenVPNService.startOpenVPN schedules this JobService
|
|
36
|
+
directly via JobScheduler. The original AAR manifest declared it but
|
|
37
|
+
we stripped the AAR manifest to avoid <application>-attribute conflicts.
|
|
38
|
+
Re-declare it here so Android can find the component.
|
|
39
|
+
-->
|
|
40
|
+
<service
|
|
41
|
+
android:name="de.blinkt.openvpn.core.keepVPNAlive"
|
|
42
|
+
android:exported="false"
|
|
43
|
+
android:permission="android.permission.BIND_JOB_SERVICE" />
|
|
44
|
+
|
|
45
|
+
<!--
|
|
46
|
+
Status-broadcast service used by ics-openvpn internally for IPC between
|
|
47
|
+
the foreground OpenVPN service and its API consumers. Declared here for
|
|
48
|
+
the same reason as keepVPNAlive.
|
|
49
|
+
-->
|
|
50
|
+
<service
|
|
51
|
+
android:name="de.blinkt.openvpn.core.OpenVPNStatusService"
|
|
52
|
+
android:exported="false" />
|
|
53
|
+
</application>
|
|
54
|
+
</manifest>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package com.openvpn
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import androidx.core.app.NotificationCompat
|
|
9
|
+
|
|
10
|
+
data class NotificationOverrides(
|
|
11
|
+
val title: String? = null,
|
|
12
|
+
val text: String? = null,
|
|
13
|
+
val smallIcon: String? = null,
|
|
14
|
+
val channelId: String? = null,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
object NotificationHelper {
|
|
18
|
+
|
|
19
|
+
private const val DEFAULT_CHANNEL_ID = "openvpn"
|
|
20
|
+
|
|
21
|
+
fun build(context: Context, overrides: NotificationOverrides): Notification {
|
|
22
|
+
val channelId = overrides.channelId ?: DEFAULT_CHANNEL_ID
|
|
23
|
+
ensureChannel(
|
|
24
|
+
context,
|
|
25
|
+
channelId,
|
|
26
|
+
context.getString(R.string.openvpn_notification_channel_name),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
val title = overrides.title ?: context.getString(R.string.openvpn_notification_title)
|
|
30
|
+
val text = overrides.text ?: context.getString(R.string.openvpn_notification_text)
|
|
31
|
+
val iconResId = resolveIcon(context, overrides.smallIcon)
|
|
32
|
+
|
|
33
|
+
return NotificationCompat.Builder(context, channelId)
|
|
34
|
+
.setContentTitle(title)
|
|
35
|
+
.setContentText(text)
|
|
36
|
+
.setSmallIcon(iconResId)
|
|
37
|
+
.setOngoing(true)
|
|
38
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
39
|
+
.build()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fun ensureChannel(context: Context, channelId: String, channelName: String) {
|
|
43
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
44
|
+
val mgr = context.getSystemService(NotificationManager::class.java) ?: return
|
|
45
|
+
if (mgr.getNotificationChannel(channelId) != null) return
|
|
46
|
+
val channel = NotificationChannel(
|
|
47
|
+
channelId,
|
|
48
|
+
channelName,
|
|
49
|
+
NotificationManager.IMPORTANCE_LOW,
|
|
50
|
+
)
|
|
51
|
+
mgr.createNotificationChannel(channel)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private fun resolveIcon(context: Context, name: String?): Int {
|
|
55
|
+
if (name.isNullOrBlank()) return R.drawable.ic_vpn_default
|
|
56
|
+
val res = context.resources.getIdentifier(name, "drawable", context.packageName)
|
|
57
|
+
return if (res != 0) res else R.drawable.ic_vpn_default
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package com.openvpn
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
6
|
+
|
|
7
|
+
object OpenvpnEventBus {
|
|
8
|
+
|
|
9
|
+
@Volatile
|
|
10
|
+
private var reactContext: ReactApplicationContext? = null
|
|
11
|
+
|
|
12
|
+
fun attach(context: ReactApplicationContext) {
|
|
13
|
+
reactContext = context
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fun detach() {
|
|
17
|
+
reactContext = null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fun emitState(state: String) {
|
|
21
|
+
emit("OpenVpn:state", state)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fun emitStats(bytesIn: Long, bytesOut: Long, durationMs: Long) {
|
|
25
|
+
val payload = Arguments.createMap().apply {
|
|
26
|
+
putDouble("bytesIn", bytesIn.toDouble())
|
|
27
|
+
putDouble("bytesOut", bytesOut.toDouble())
|
|
28
|
+
putDouble("durationMs", durationMs.toDouble())
|
|
29
|
+
}
|
|
30
|
+
emit("OpenVpn:stats", payload)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fun emitLog(line: String) {
|
|
34
|
+
emit("OpenVpn:log", line)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fun emitError(code: String, nativeMessage: String?) {
|
|
38
|
+
val payload = Arguments.createMap().apply {
|
|
39
|
+
putString("code", code)
|
|
40
|
+
putString("nativeMessage", nativeMessage)
|
|
41
|
+
}
|
|
42
|
+
emit("OpenVpn:error", payload)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private fun emit(eventName: String, payload: Any?) {
|
|
46
|
+
val ctx = reactContext ?: return
|
|
47
|
+
if (!ctx.hasActiveReactInstance()) return
|
|
48
|
+
ctx
|
|
49
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
50
|
+
.emit(eventName, payload)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
package com.openvpn
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import com.facebook.react.bridge.ActivityEventListener
|
|
6
|
+
import com.facebook.react.bridge.Arguments
|
|
7
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
8
|
+
import com.facebook.react.bridge.Promise
|
|
9
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
10
|
+
import com.facebook.react.bridge.ReadableMap
|
|
11
|
+
import com.facebook.react.bridge.WritableMap
|
|
12
|
+
|
|
13
|
+
class OpenvpnModule(reactContext: ReactApplicationContext) :
|
|
14
|
+
NativeOpenvpnSpec(reactContext), LifecycleEventListener, ActivityEventListener {
|
|
15
|
+
|
|
16
|
+
private val context: ReactApplicationContext = reactContext
|
|
17
|
+
|
|
18
|
+
init {
|
|
19
|
+
OpenvpnEventBus.attach(reactContext)
|
|
20
|
+
reactContext.addLifecycleEventListener(this)
|
|
21
|
+
reactContext.addActivityEventListener(this)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun getName(): String = NAME
|
|
25
|
+
|
|
26
|
+
override fun requestPermission(promise: Promise) {
|
|
27
|
+
PermissionLauncher.request(context, promise)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override fun connect(params: ReadableMap, promise: Promise) {
|
|
31
|
+
try {
|
|
32
|
+
// Reject per-app routing (deferred from Plan 2)
|
|
33
|
+
val allowed = if (params.hasKey("allowedApps")) params.getArray("allowedApps") else null
|
|
34
|
+
val disallowed = if (params.hasKey("disallowedApps")) params.getArray("disallowedApps") else null
|
|
35
|
+
if ((allowed?.size() ?: 0) > 0 || (disallowed?.size() ?: 0) > 0) {
|
|
36
|
+
promise.reject(
|
|
37
|
+
"INVALID_CONFIG",
|
|
38
|
+
"per-app routing (allowedApps/disallowedApps) not yet implemented on Android",
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
val ovpn = params.getString("config")
|
|
44
|
+
?: throw OpenvpnException("INVALID_CONFIG", "config missing")
|
|
45
|
+
val username = params.getString("username") ?: ""
|
|
46
|
+
val password = params.getString("password") ?: ""
|
|
47
|
+
val killSwitch = if (params.hasKey("killSwitch")) params.getBoolean("killSwitch") else false
|
|
48
|
+
val dns = if (params.hasKey("dns")) {
|
|
49
|
+
params.getArray("dns")?.let { array ->
|
|
50
|
+
(0 until array.size()).mapNotNull { array.getString(it) }
|
|
51
|
+
} ?: emptyList()
|
|
52
|
+
} else emptyList()
|
|
53
|
+
val notificationMap = if (params.hasKey("notification")) params.getMap("notification") else null
|
|
54
|
+
|
|
55
|
+
val profile = ProfileBuilder.build(ovpn, username, password, dns, killSwitch)
|
|
56
|
+
|
|
57
|
+
val intent = Intent(context, OpenvpnService::class.java).apply {
|
|
58
|
+
action = OpenvpnService.ACTION_START
|
|
59
|
+
putExtra(OpenvpnService.EXTRA_PROFILE, profile)
|
|
60
|
+
notificationMap?.let {
|
|
61
|
+
if (it.hasKey("title")) putExtra(OpenvpnService.EXTRA_NOTIF_TITLE, it.getString("title"))
|
|
62
|
+
if (it.hasKey("text")) putExtra(OpenvpnService.EXTRA_NOTIF_TEXT, it.getString("text"))
|
|
63
|
+
if (it.hasKey("smallIcon")) putExtra(OpenvpnService.EXTRA_NOTIF_ICON, it.getString("smallIcon"))
|
|
64
|
+
if (it.hasKey("channelId")) putExtra(OpenvpnService.EXTRA_NOTIF_CHANNEL, it.getString("channelId"))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
69
|
+
context.startForegroundService(intent)
|
|
70
|
+
} else {
|
|
71
|
+
context.startService(intent)
|
|
72
|
+
}
|
|
73
|
+
// The JS-side connect() Promise resolves on the 'connected' state event,
|
|
74
|
+
// so we resolve this native promise as soon as the start request is
|
|
75
|
+
// accepted by the system.
|
|
76
|
+
promise.resolve(null)
|
|
77
|
+
} catch (e: OpenvpnException) {
|
|
78
|
+
promise.reject(e.code, e.nativeMessage)
|
|
79
|
+
} catch (e: Exception) {
|
|
80
|
+
promise.reject("NATIVE_ERROR", e.message ?: "unknown error")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override fun disconnect(promise: Promise) {
|
|
85
|
+
try {
|
|
86
|
+
val intent = Intent(context, OpenvpnService::class.java).apply {
|
|
87
|
+
action = OpenvpnService.ACTION_STOP
|
|
88
|
+
}
|
|
89
|
+
context.startService(intent)
|
|
90
|
+
promise.resolve(null)
|
|
91
|
+
} catch (e: Exception) {
|
|
92
|
+
promise.reject("NATIVE_ERROR", e.message ?: "unknown error")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override fun getStatus(promise: Promise) {
|
|
97
|
+
val map: WritableMap = Arguments.createMap()
|
|
98
|
+
// State is delivered via the 'OpenVpn:state' event channel.
|
|
99
|
+
// This method returns a minimal snapshot; consumers should prefer events.
|
|
100
|
+
map.putString("state", "idle")
|
|
101
|
+
promise.resolve(map)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun getStats(promise: Promise) {
|
|
105
|
+
val map: WritableMap = Arguments.createMap()
|
|
106
|
+
map.putDouble("bytesIn", 0.0)
|
|
107
|
+
map.putDouble("bytesOut", 0.0)
|
|
108
|
+
map.putDouble("durationMs", 0.0)
|
|
109
|
+
promise.resolve(map)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override fun addListener(eventName: String?) {
|
|
113
|
+
// required by RN's NativeEventEmitter contract; no-op
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
override fun removeListeners(count: Double) {
|
|
117
|
+
// required by RN's NativeEventEmitter contract; no-op
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
override fun onHostResume() = Unit
|
|
121
|
+
override fun onHostPause() = Unit
|
|
122
|
+
override fun onHostDestroy() {
|
|
123
|
+
OpenvpnEventBus.detach()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override fun onActivityResult(
|
|
127
|
+
activity: android.app.Activity,
|
|
128
|
+
requestCode: Int,
|
|
129
|
+
resultCode: Int,
|
|
130
|
+
data: Intent?,
|
|
131
|
+
) {
|
|
132
|
+
PermissionLauncher.onActivityResult(requestCode, resultCode, data)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
override fun onNewIntent(intent: Intent) = Unit
|
|
136
|
+
|
|
137
|
+
companion object {
|
|
138
|
+
const val NAME = NativeOpenvpnSpec.NAME
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.openvpn
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class OpenvpnPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == OpenvpnModule.NAME) {
|
|
13
|
+
OpenvpnModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
|
20
|
+
mapOf(
|
|
21
|
+
OpenvpnModule.NAME to ReactModuleInfo(
|
|
22
|
+
name = OpenvpnModule.NAME,
|
|
23
|
+
className = OpenvpnModule.NAME,
|
|
24
|
+
canOverrideExistingModule = false,
|
|
25
|
+
needsEagerInit = false,
|
|
26
|
+
isCxxModule = false,
|
|
27
|
+
isTurboModule = true
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|