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 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
@@ -0,0 +1,4 @@
1
+ // Expo config plugin entrypoint.
2
+ // Expo resolves config plugins from this file when present.
3
+ module.exports = require('./plugin/withWireGuardVpn');
4
+