react-native-wireguard-vpn-patched 1.0.22-patch.2
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 +21 -0
- package/README.md +238 -0
- package/android/build.gradle +60 -0
- package/android/src/main/java/com/wireguardvpn/WireGuardVpnModule.kt +353 -0
- package/android/src/main/java/com/wireguardvpn/WireGuardVpnPackage.kt +16 -0
- package/app.plugin.js +4 -0
- package/ios/WireGuardVpn.h +5 -0
- package/ios/WireGuardVpn.m +294 -0
- package/lib/index.d.ts +53 -0
- package/lib/index.js +46 -0
- package/lib/index.js.map +1 -0
- package/package.json +56 -0
- package/plugin/withWireGuardVpn.js +105 -0
- package/react-native-wireguard-vpn-patched.podspec +21 -0
- package/src/index.d.ts +41 -0
- package/src/index.ts +99 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Usama Aamir {Feline Predator}
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# react-native-wireguard-vpn-patched
|
|
2
|
+
|
|
3
|
+
A production-ready fork of [`react-native-wireguard-vpn`](https://github.com/usama7365/react-native-wireguard-vpn) (v1.0.22) with two critical Android fixes applied that are required for real-world VPN application deployment.
|
|
4
|
+
|
|
5
|
+
**Author:** Abdul Ahad — [github.com/AbdulAHAD968](https://github.com/AbdulAHAD968)
|
|
6
|
+
**Base package:** `react-native-wireguard-vpn@1.0.22` by usama7365 (MIT)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why This Fork Exists
|
|
11
|
+
|
|
12
|
+
The original `react-native-wireguard-vpn` package has two production-blocking issues on Android:
|
|
13
|
+
|
|
14
|
+
**1. VPN Permission Not Requested**
|
|
15
|
+
|
|
16
|
+
Android requires explicit user consent before any app can establish a VPN tunnel (`VpnService.prepare()`). The original `connect()` method skipped this check entirely, causing silent failures or crashes on devices where the permission had not been pre-granted. There was no mechanism for the app to know permission was needed, and no way to present the system dialog.
|
|
17
|
+
|
|
18
|
+
**2. No Split Tunneling Support**
|
|
19
|
+
|
|
20
|
+
The original module provided no way to exclude specific applications from the VPN tunnel. Android exposes `VpnService.Builder.addDisallowedApplication()` (API 21+) for this purpose, but it was not wired up in the native module. Any production VPN application is expected to offer this as a user-facing feature.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Changes in This Fork
|
|
25
|
+
|
|
26
|
+
### Android — `WireGuardVpnModule.kt`
|
|
27
|
+
|
|
28
|
+
#### Fix 1: VPN Permission Request
|
|
29
|
+
|
|
30
|
+
Before the tunnel is brought up, `VpnService.prepare()` is now called. If the permission dialog needs to be shown, the native module:
|
|
31
|
+
|
|
32
|
+
- Launches the system intent via `startActivityForResult(intent, 1000)`
|
|
33
|
+
- Rejects the connect promise with error code `VPN_PERMISSION_REQUIRED`
|
|
34
|
+
- Returns immediately — the tunnel is not started
|
|
35
|
+
|
|
36
|
+
The caller handles `VPN_PERMISSION_REQUIRED` and re-calls `connect()` after the user grants permission (via `AppState` change detection or equivalent).
|
|
37
|
+
|
|
38
|
+
```kotlin
|
|
39
|
+
val intent = VpnService.prepare(reactApplicationContext)
|
|
40
|
+
if (intent != null) {
|
|
41
|
+
val activity = getCurrentActivity()
|
|
42
|
+
if (activity != null) {
|
|
43
|
+
activity.startActivityForResult(intent, 1000)
|
|
44
|
+
promise.reject("VPN_PERMISSION_REQUIRED", "Please accept the Android VPN permission dialog.")
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Fix 2: Split Tunneling via `excludedApps`
|
|
51
|
+
|
|
52
|
+
The `connect()` method now accepts an optional `excludedApps` string array. Each entry is an Android package name. The native module calls `interfaceBuilder.excludeApplication(packageName)` for each, routing those apps outside the VPN tunnel. Failures for individual packages are logged and non-fatal.
|
|
53
|
+
|
|
54
|
+
```kotlin
|
|
55
|
+
if (config.hasKey("excludedApps")) {
|
|
56
|
+
val excludedApps = config.getArray("excludedApps")?.toArrayList()
|
|
57
|
+
excludedApps?.forEach { app ->
|
|
58
|
+
(app as? String)?.let { packageName ->
|
|
59
|
+
try {
|
|
60
|
+
interfaceBuilder.excludeApplication(packageName)
|
|
61
|
+
} catch (e: Exception) {
|
|
62
|
+
println("Failed to exclude app $packageName: ${e.message}")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install react-native-wireguard-vpn-patched
|
|
75
|
+
# or
|
|
76
|
+
yarn add react-native-wireguard-vpn-patched
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If you are migrating from `react-native-wireguard-vpn`, uninstall it first:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm uninstall react-native-wireguard-vpn
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import WireGuard from 'react-native-wireguard-vpn-patched';
|
|
91
|
+
import { AppState } from 'react-native';
|
|
92
|
+
|
|
93
|
+
// Initialize once on app start
|
|
94
|
+
await WireGuard.initialize();
|
|
95
|
+
|
|
96
|
+
// Basic connection
|
|
97
|
+
await WireGuard.connect({
|
|
98
|
+
privateKey: '<client-private-key>',
|
|
99
|
+
publicKey: '<server-public-key>',
|
|
100
|
+
serverAddress: '203.0.113.1',
|
|
101
|
+
serverPort: 51820,
|
|
102
|
+
address: '10.64.0.2/32',
|
|
103
|
+
allowedIPs: ['0.0.0.0/0', '::/0'],
|
|
104
|
+
dns: ['1.1.1.1', '8.8.8.8'],
|
|
105
|
+
mtu: 1420,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Handling VPN Permission (Android)
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { AppState } from 'react-native';
|
|
113
|
+
|
|
114
|
+
const connect = async () => {
|
|
115
|
+
try {
|
|
116
|
+
await WireGuard.connect(config);
|
|
117
|
+
// tunnel is up
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (err.code === 'VPN_PERMISSION_REQUIRED') {
|
|
120
|
+
// System dialog is now visible to the user.
|
|
121
|
+
// Listen for the app coming back to foreground, then retry.
|
|
122
|
+
const sub = AppState.addEventListener('change', async (state) => {
|
|
123
|
+
if (state === 'active') {
|
|
124
|
+
sub.remove();
|
|
125
|
+
await WireGuard.connect(config); // second call succeeds if user accepted
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Split Tunneling
|
|
134
|
+
|
|
135
|
+
Pass an `excludedApps` array of Android package names. Those apps will route through the device's regular internet connection, bypassing the VPN tunnel.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
await WireGuard.connect({
|
|
139
|
+
// ...base config...
|
|
140
|
+
excludedApps: [
|
|
141
|
+
'com.google.android.apps.maps',
|
|
142
|
+
'com.example.bankingapp',
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## API Reference
|
|
150
|
+
|
|
151
|
+
### `WireGuard.initialize(): Promise<void>`
|
|
152
|
+
|
|
153
|
+
Initializes the WireGuard Go backend. Must be called once before `connect()`.
|
|
154
|
+
|
|
155
|
+
### `WireGuard.connect(config: WireGuardConfig): Promise<void>`
|
|
156
|
+
|
|
157
|
+
Establishes the VPN tunnel. Rejects with `VPN_PERMISSION_REQUIRED` on first run on Android if the user has not yet granted VPN permission.
|
|
158
|
+
|
|
159
|
+
### `WireGuard.disconnect(): Promise<void>`
|
|
160
|
+
|
|
161
|
+
Tears down the active tunnel.
|
|
162
|
+
|
|
163
|
+
### `WireGuard.getStatus(): Promise<WireGuardStatus>`
|
|
164
|
+
|
|
165
|
+
Returns the current tunnel state.
|
|
166
|
+
|
|
167
|
+
### `WireGuard.isSupported(): Promise<boolean>`
|
|
168
|
+
|
|
169
|
+
Returns `true` if WireGuard is supported on the current device.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Types
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
interface WireGuardConfig {
|
|
177
|
+
privateKey: string;
|
|
178
|
+
publicKey: string;
|
|
179
|
+
serverAddress: string;
|
|
180
|
+
serverPort: number;
|
|
181
|
+
address?: string | string[]; // tunnel interface IP, e.g. "10.64.0.2/32"
|
|
182
|
+
allowedIPs: string[]; // routing rules, e.g. ["0.0.0.0/0", "::/0"]
|
|
183
|
+
dns?: string[];
|
|
184
|
+
mtu?: number;
|
|
185
|
+
presharedKey?: string;
|
|
186
|
+
excludedApps?: string[]; // Android only — package names to bypass VPN
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface WireGuardStatus {
|
|
190
|
+
isConnected: boolean;
|
|
191
|
+
tunnelState: 'ACTIVE' | 'INACTIVE' | 'CONNECTING' | 'DISCONNECTING' | 'ERROR' | 'UNKNOWN';
|
|
192
|
+
status: 'CONNECTED' | 'DISCONNECTED' | 'CONNECTING' | 'DISCONNECTING' | 'ERROR' | 'UNKNOWN';
|
|
193
|
+
error?: string;
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Requirements
|
|
200
|
+
|
|
201
|
+
| Requirement | Minimum Version |
|
|
202
|
+
|------------------|-----------------|
|
|
203
|
+
| React Native | 0.72.0 |
|
|
204
|
+
| Android API | 21 (Android 5) |
|
|
205
|
+
| compileSdkVersion| 34 |
|
|
206
|
+
| Kotlin | 1.8.0 |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Platform Support
|
|
211
|
+
|
|
212
|
+
| Platform | Permission Fix | Split Tunneling |
|
|
213
|
+
|----------|---------------|-----------------|
|
|
214
|
+
| Android | Yes | Yes |
|
|
215
|
+
| iOS | N/A (handled by OS) | Not yet |
|
|
216
|
+
|
|
217
|
+
iOS split tunneling is not implemented in this fork. The iOS implementation is unchanged from the upstream package.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Differences from Upstream
|
|
222
|
+
|
|
223
|
+
| Feature | `react-native-wireguard-vpn` | This fork |
|
|
224
|
+
|--------------------------------|------------------------------|-----------|
|
|
225
|
+
| Android VPN permission request | No | Yes |
|
|
226
|
+
| Split tunneling (`excludedApps`) | No | Yes |
|
|
227
|
+
| `VPN_PERMISSION_REQUIRED` error code | No | Yes |
|
|
228
|
+
| TypeScript: `excludedApps` field | No | Yes |
|
|
229
|
+
| Base version | 1.0.22 | 1.0.22-patch.2 |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT — same as the upstream package.
|
|
236
|
+
|
|
237
|
+
Original package copyright: usama7365
|
|
238
|
+
Fork modifications copyright: Abdul Ahad (ahad06074@gmail.com)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.kotlin_version = '1.8.0'
|
|
3
|
+
repositories {
|
|
4
|
+
google()
|
|
5
|
+
mavenCentral()
|
|
6
|
+
}
|
|
7
|
+
dependencies {
|
|
8
|
+
classpath 'com.android.tools.build:gradle:8.1.0'
|
|
9
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
allprojects {
|
|
14
|
+
repositories {
|
|
15
|
+
google()
|
|
16
|
+
mavenCentral()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
apply plugin: 'com.android.library'
|
|
21
|
+
apply plugin: 'kotlin-android'
|
|
22
|
+
|
|
23
|
+
android {
|
|
24
|
+
compileSdkVersion 34
|
|
25
|
+
namespace "com.wireguardvpn"
|
|
26
|
+
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion 21
|
|
29
|
+
targetSdkVersion 34
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
buildTypes {
|
|
33
|
+
release {
|
|
34
|
+
minifyEnabled false
|
|
35
|
+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
compileOptions {
|
|
40
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
41
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
kotlinOptions {
|
|
45
|
+
jvmTarget = '17'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
sourceSets {
|
|
49
|
+
main {
|
|
50
|
+
manifest.srcFile 'src/main/AndroidManifest.xml'
|
|
51
|
+
res.srcDirs = ['src/main/res']
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
dependencies {
|
|
57
|
+
implementation "com.facebook.react:react-native:+"
|
|
58
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
59
|
+
implementation 'com.wireguard.android:tunnel:1.0.20211029'
|
|
60
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
package com.wireguardvpn
|
|
2
|
+
|
|
3
|
+
import android.net.VpnService
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import com.facebook.react.bridge.*
|
|
6
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
7
|
+
import com.wireguard.android.backend.GoBackend
|
|
8
|
+
import com.wireguard.config.Config
|
|
9
|
+
import com.wireguard.config.Interface
|
|
10
|
+
import com.wireguard.config.Peer
|
|
11
|
+
import com.wireguard.android.backend.Tunnel
|
|
12
|
+
import java.net.InetAddress
|
|
13
|
+
import com.wireguard.config.InetNetwork
|
|
14
|
+
import com.wireguard.config.ParseException
|
|
15
|
+
import com.wireguard.crypto.Key
|
|
16
|
+
|
|
17
|
+
class WireGuardVpnModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
18
|
+
private var backend: GoBackend? = null
|
|
19
|
+
private var tunnel: Tunnel? = null
|
|
20
|
+
private var config: Config? = null
|
|
21
|
+
|
|
22
|
+
private fun emitVpnState(tunnelState: String, status: String, isConnected: Boolean) {
|
|
23
|
+
val payload = Arguments.createMap().apply {
|
|
24
|
+
putBoolean("isConnected", isConnected)
|
|
25
|
+
putString("tunnelState", tunnelState)
|
|
26
|
+
putString("status", status)
|
|
27
|
+
}
|
|
28
|
+
reactApplicationContext
|
|
29
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
30
|
+
.emit("vpnStateChanged", payload)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private fun mapTunnelState(newState: Tunnel.State): Triple<String, String, Boolean> {
|
|
34
|
+
val stateName = newState?.name ?: "UNKNOWN"
|
|
35
|
+
val tunnelState = when {
|
|
36
|
+
newState == Tunnel.State.UP -> "ACTIVE"
|
|
37
|
+
newState == Tunnel.State.DOWN -> "INACTIVE"
|
|
38
|
+
stateName.contains("CONNECT", ignoreCase = true) -> "CONNECTING"
|
|
39
|
+
stateName.contains("DISCONNECT", ignoreCase = true) -> "DISCONNECTING"
|
|
40
|
+
else -> "ERROR"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
val status = when (tunnelState) {
|
|
44
|
+
"ACTIVE" -> "CONNECTED"
|
|
45
|
+
"INACTIVE" -> "DISCONNECTED"
|
|
46
|
+
"CONNECTING" -> "CONNECTING"
|
|
47
|
+
"DISCONNECTING" -> "DISCONNECTING"
|
|
48
|
+
"ERROR" -> "ERROR"
|
|
49
|
+
else -> "UNKNOWN"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Triple(tunnelState, status, newState == Tunnel.State.UP)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override fun getName() = "WireGuardVpnModule"
|
|
56
|
+
|
|
57
|
+
@ReactMethod
|
|
58
|
+
fun initialize(promise: Promise) {
|
|
59
|
+
try {
|
|
60
|
+
backend = GoBackend(reactApplicationContext)
|
|
61
|
+
promise.resolve(null)
|
|
62
|
+
} catch (e: Exception) {
|
|
63
|
+
promise.reject("INIT_ERROR", "Failed to initialize WireGuard: ${e.message}")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@ReactMethod
|
|
68
|
+
fun connect(config: ReadableMap, promise: Promise) {
|
|
69
|
+
try {
|
|
70
|
+
println("Starting VPN connection process...")
|
|
71
|
+
|
|
72
|
+
// --- SURGICAL PERMISSION CHECK ---
|
|
73
|
+
val intent = VpnService.prepare(reactApplicationContext)
|
|
74
|
+
if (intent != null) {
|
|
75
|
+
val activity = getCurrentActivity()
|
|
76
|
+
if (activity != null) {
|
|
77
|
+
activity.startActivityForResult(intent, 1000)
|
|
78
|
+
promise.reject("VPN_PERMISSION_REQUIRED", "Please accept the Android VPN permission dialog.")
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ---------------------------------
|
|
83
|
+
|
|
84
|
+
println("Received config: $config")
|
|
85
|
+
|
|
86
|
+
if (backend == null) {
|
|
87
|
+
println("Backend is null, initializing...")
|
|
88
|
+
backend = GoBackend(reactApplicationContext)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
val interfaceBuilder = Interface.Builder()
|
|
92
|
+
|
|
93
|
+
// Parse private key
|
|
94
|
+
val privateKey = config.getString("privateKey") ?: throw Exception("Private key is required")
|
|
95
|
+
try {
|
|
96
|
+
println("Parsing private key: $privateKey")
|
|
97
|
+
interfaceBuilder.parsePrivateKey(privateKey)
|
|
98
|
+
println("Private key parsed successfully")
|
|
99
|
+
} catch (e: ParseException) {
|
|
100
|
+
println("Failed to parse private key: ${e.message}")
|
|
101
|
+
throw Exception("Invalid private key format: ${e.message}, Key: $privateKey")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Interface address: client's VPN tunnel IP (e.g. 10.64.0.1/32). Do NOT use allowedIPs here
|
|
105
|
+
// or the Go backend can return "Bad address" (0.0.0.0/0 or ::/0 are invalid for interface).
|
|
106
|
+
val rawAddress = when {
|
|
107
|
+
config.hasKey("address") && config.getType("address") == ReadableType.Array ->
|
|
108
|
+
config.getArray("address")?.toArrayList().orEmpty()
|
|
109
|
+
config.hasKey("address") && config.getString("address") != null ->
|
|
110
|
+
listOf(config.getString("address")!!)
|
|
111
|
+
else -> listOf("10.64.0.1/32")
|
|
112
|
+
}
|
|
113
|
+
val interfaceAddresses = rawAddress.filterIsInstance<String>().map { it.trim() }.filter { it.isNotBlank() }
|
|
114
|
+
.ifEmpty { listOf("10.64.0.1/32") }
|
|
115
|
+
try {
|
|
116
|
+
interfaceAddresses.forEach { addr ->
|
|
117
|
+
interfaceBuilder.addAddress(InetNetwork.parse(addr))
|
|
118
|
+
}
|
|
119
|
+
} catch (e: ParseException) {
|
|
120
|
+
throw Exception("Invalid interface address format: ${e.message}. Use CIDR like 10.64.0.1/32, not 0.0.0.0/0. Addresses: $interfaceAddresses")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Peer allowed IPs: which traffic to route through VPN (e.g. 0.0.0.0/0, ::/0)
|
|
124
|
+
val allowedIPs = config.getArray("allowedIPs")?.toArrayList()
|
|
125
|
+
?: throw Exception("allowedIPs array is required")
|
|
126
|
+
val normalizedAllowedIPs = allowedIPs.mapNotNull { ip ->
|
|
127
|
+
(ip as? String)?.trim()?.takeIf { it.isNotBlank() }
|
|
128
|
+
?.replace("::0/0", "::/0") // normalize IPv6 default route (e.g. Mullvad)
|
|
129
|
+
}
|
|
130
|
+
if (normalizedAllowedIPs.isEmpty()) throw Exception("allowedIPs must contain at least one CIDR")
|
|
131
|
+
|
|
132
|
+
// Parse DNS servers
|
|
133
|
+
if (config.hasKey("dns")) {
|
|
134
|
+
val dnsServers = config.getArray("dns")?.toArrayList()
|
|
135
|
+
try {
|
|
136
|
+
println("Parsing DNS servers: $dnsServers")
|
|
137
|
+
dnsServers?.forEach { dns ->
|
|
138
|
+
(dns as? String)?.let { dnsString ->
|
|
139
|
+
interfaceBuilder.addDnsServer(InetAddress.getByName(dnsString))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
println("DNS servers parsed successfully")
|
|
143
|
+
} catch (e: Exception) {
|
|
144
|
+
println("Failed to parse DNS servers: ${e.message}")
|
|
145
|
+
throw Exception("Invalid DNS server format: ${e.message}, DNS: $dnsServers")
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Set MTU if provided
|
|
150
|
+
if (config.hasKey("mtu")) {
|
|
151
|
+
val mtu = config.getInt("mtu")
|
|
152
|
+
if (mtu < 1280 || mtu > 65535) {
|
|
153
|
+
throw Exception("MTU must be between 1280 and 65535, got: $mtu")
|
|
154
|
+
}
|
|
155
|
+
interfaceBuilder.setMtu(mtu)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Split Tunneling: exclude apps from VPN tunnel
|
|
159
|
+
if (config.hasKey("excludedApps")) {
|
|
160
|
+
val excludedApps = config.getArray("excludedApps")?.toArrayList()
|
|
161
|
+
excludedApps?.forEach { app ->
|
|
162
|
+
(app as? String)?.let { packageName ->
|
|
163
|
+
try {
|
|
164
|
+
interfaceBuilder.excludeApplication(packageName)
|
|
165
|
+
println("Excluded app from VPN: $packageName")
|
|
166
|
+
} catch (e: Exception) {
|
|
167
|
+
println("Failed to exclude app $packageName: ${e.message}")
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
val peerBuilder = Peer.Builder()
|
|
174
|
+
|
|
175
|
+
// Parse public key
|
|
176
|
+
val publicKey = config.getString("publicKey") ?: throw Exception("Public key is required")
|
|
177
|
+
try {
|
|
178
|
+
println("Parsing public key: $publicKey")
|
|
179
|
+
peerBuilder.parsePublicKey(publicKey)
|
|
180
|
+
println("Public key parsed successfully")
|
|
181
|
+
} catch (e: ParseException) {
|
|
182
|
+
println("Failed to parse public key: ${e.message}")
|
|
183
|
+
throw Exception("Invalid public key format: ${e.message}, Key: $publicKey")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Parse preshared key if provided
|
|
187
|
+
if (config.hasKey("presharedKey")) {
|
|
188
|
+
val presharedKey = config.getString("presharedKey")
|
|
189
|
+
try {
|
|
190
|
+
println("Parsing preshared key: $presharedKey")
|
|
191
|
+
presharedKey?.let { keyString ->
|
|
192
|
+
val key = Key.fromBase64(keyString)
|
|
193
|
+
peerBuilder.setPreSharedKey(key)
|
|
194
|
+
}
|
|
195
|
+
println("Preshared key parsed successfully")
|
|
196
|
+
} catch (e: Exception) {
|
|
197
|
+
println("Failed to parse preshared key: ${e.message}")
|
|
198
|
+
throw Exception("Invalid preshared key format: ${e.message}, Key: $presharedKey")
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Parse endpoint
|
|
203
|
+
val serverAddress = config.getString("serverAddress") ?: throw Exception("Server address is required")
|
|
204
|
+
val serverPort = config.getInt("serverPort")
|
|
205
|
+
if (serverPort < 1 || serverPort > 65535) {
|
|
206
|
+
throw Exception("Port must be between 1 and 65535, got: $serverPort")
|
|
207
|
+
}
|
|
208
|
+
val endpoint = "$serverAddress:$serverPort"
|
|
209
|
+
try {
|
|
210
|
+
println("Parsing endpoint: $endpoint")
|
|
211
|
+
peerBuilder.parseEndpoint(endpoint)
|
|
212
|
+
println("Endpoint parsed successfully")
|
|
213
|
+
} catch (e: ParseException) {
|
|
214
|
+
println("Failed to parse endpoint: ${e.message}")
|
|
215
|
+
throw Exception("Invalid endpoint format: ${e.message}, Endpoint: $endpoint")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Add allowed IPs to peer (routing; do not use for interface address)
|
|
219
|
+
try {
|
|
220
|
+
normalizedAllowedIPs.forEach { ipString ->
|
|
221
|
+
peerBuilder.addAllowedIp(InetNetwork.parse(ipString))
|
|
222
|
+
}
|
|
223
|
+
} catch (e: ParseException) {
|
|
224
|
+
throw Exception("Invalid peer allowedIP format: ${e.message}. Use ::/0 for IPv6 default, not ::0/0. IPs: $normalizedAllowedIPs")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
println("Building WireGuard config...")
|
|
228
|
+
val configBuilder = Config.Builder()
|
|
229
|
+
configBuilder.setInterface(interfaceBuilder.build())
|
|
230
|
+
configBuilder.addPeer(peerBuilder.build())
|
|
231
|
+
|
|
232
|
+
this.config = configBuilder.build()
|
|
233
|
+
println("WireGuard config built successfully")
|
|
234
|
+
|
|
235
|
+
this.tunnel = object : Tunnel {
|
|
236
|
+
override fun getName(): String = "WireGuardTunnel"
|
|
237
|
+
override fun onStateChange(newState: Tunnel.State) {
|
|
238
|
+
println("WireGuard tunnel state changed to: $newState")
|
|
239
|
+
val (tunnelState, simpleStatus, isConnected) = mapTunnelState(newState)
|
|
240
|
+
emitVpnState(tunnelState, simpleStatus, isConnected)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
println("Checking backend and tunnel state...")
|
|
246
|
+
println("Backend initialized: $backend")
|
|
247
|
+
println("Tunnel initialized: $tunnel")
|
|
248
|
+
println("Config ready: $config")
|
|
249
|
+
|
|
250
|
+
println("Attempting to set tunnel state to UP...")
|
|
251
|
+
if (backend == null) {
|
|
252
|
+
throw Exception("Backend is null")
|
|
253
|
+
}
|
|
254
|
+
if (tunnel == null) {
|
|
255
|
+
throw Exception("Tunnel is null")
|
|
256
|
+
}
|
|
257
|
+
if (this.config == null) {
|
|
258
|
+
throw Exception("Config is null")
|
|
259
|
+
}
|
|
260
|
+
backend?.setState(tunnel!!, Tunnel.State.UP, this.config!!)
|
|
261
|
+
println("Successfully set tunnel state to UP")
|
|
262
|
+
promise.resolve(null)
|
|
263
|
+
} catch (e: Exception) {
|
|
264
|
+
val msg = e.message ?: e.toString()
|
|
265
|
+
val causeMsg = e.cause?.message
|
|
266
|
+
println("Failed to set tunnel state: $msg")
|
|
267
|
+
if (causeMsg != null) println("Cause: $causeMsg")
|
|
268
|
+
e.printStackTrace()
|
|
269
|
+
throw Exception(if (causeMsg != null) "$msg: $causeMsg" else msg)
|
|
270
|
+
}
|
|
271
|
+
} catch (e: Exception) {
|
|
272
|
+
val msg = e.message ?: e.toString()
|
|
273
|
+
val causeMsg = e.cause?.message
|
|
274
|
+
val fullMsg = buildString {
|
|
275
|
+
append(msg)
|
|
276
|
+
if (causeMsg != null) append(" (cause: $causeMsg)")
|
|
277
|
+
if (msg.contains("Bad address", ignoreCase = true) || msg.contains("BackendException", ignoreCase = true)) {
|
|
278
|
+
append(". Tip: ensure 'address' is a tunnel CIDR like 10.64.0.1/32, not 0.0.0.0/0; use allowedIPs only for routing.")
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
println("Connection failed: $fullMsg")
|
|
282
|
+
e.printStackTrace()
|
|
283
|
+
promise.reject("CONNECT_ERROR", fullMsg)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
@ReactMethod
|
|
288
|
+
fun disconnect(promise: Promise) {
|
|
289
|
+
try {
|
|
290
|
+
if (tunnel != null && backend != null && config != null) {
|
|
291
|
+
backend?.setState(tunnel!!, Tunnel.State.DOWN, config!!)
|
|
292
|
+
promise.resolve(null)
|
|
293
|
+
} else {
|
|
294
|
+
promise.reject("DISCONNECT_ERROR", "Tunnel not initialized")
|
|
295
|
+
}
|
|
296
|
+
} catch (e: Exception) {
|
|
297
|
+
promise.reject("DISCONNECT_ERROR", "Failed to disconnect: ${e.message}")
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@ReactMethod
|
|
302
|
+
fun getStatus(promise: Promise) {
|
|
303
|
+
try {
|
|
304
|
+
val state = if (tunnel != null && backend != null) {
|
|
305
|
+
backend?.getState(tunnel!!)
|
|
306
|
+
} else {
|
|
307
|
+
Tunnel.State.DOWN
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
val stateName = state?.name ?: "UNKNOWN"
|
|
311
|
+
|
|
312
|
+
val tunnelState = when (state) {
|
|
313
|
+
Tunnel.State.UP -> "ACTIVE"
|
|
314
|
+
Tunnel.State.DOWN -> "INACTIVE"
|
|
315
|
+
else -> {
|
|
316
|
+
// Tunnel.State enum name mapping is best-effort.
|
|
317
|
+
if (stateName.contains("CONNECT", ignoreCase = true)) "CONNECTING"
|
|
318
|
+
else if (stateName.contains("DISCONNECT", ignoreCase = true)) "DISCONNECTING"
|
|
319
|
+
else "ERROR"
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
val simpleStatus = when (tunnelState) {
|
|
324
|
+
"ACTIVE" -> "CONNECTED"
|
|
325
|
+
"INACTIVE" -> "DISCONNECTED"
|
|
326
|
+
"CONNECTING" -> "CONNECTING"
|
|
327
|
+
"DISCONNECTING" -> "DISCONNECTING"
|
|
328
|
+
"ERROR" -> "ERROR"
|
|
329
|
+
else -> "UNKNOWN"
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
val out = Arguments.createMap().apply {
|
|
333
|
+
putBoolean("isConnected", state == Tunnel.State.UP)
|
|
334
|
+
putString("tunnelState", tunnelState)
|
|
335
|
+
putString("status", simpleStatus)
|
|
336
|
+
}
|
|
337
|
+
promise.resolve(out)
|
|
338
|
+
} catch (e: Exception) {
|
|
339
|
+
val status = Arguments.createMap().apply {
|
|
340
|
+
putBoolean("isConnected", false)
|
|
341
|
+
putString("tunnelState", "ERROR")
|
|
342
|
+
putString("status", "ERROR")
|
|
343
|
+
putString("error", e.message)
|
|
344
|
+
}
|
|
345
|
+
promise.resolve(status)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@ReactMethod
|
|
350
|
+
fun isSupported(promise: Promise) {
|
|
351
|
+
promise.resolve(true)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.wireguardvpn
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class WireGuardVpnPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return listOf(WireGuardVpnModule(reactContext))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
}
|
package/app.plugin.js
ADDED