A production-ready fork of react-native-wireguard-vpn (v1.0.22) with two critical Android fixes applied that are required for real-world VPN application deployment.
Author: Abdul Ahad — github.com/AbdulAHAD968
Base package: react-native-wireguard-vpn@1.0.22 by usama7365 (MIT)
The original react-native-wireguard-vpn package has two production-blocking issues on Android:
1. VPN Permission Not Requested
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.
2. No Split Tunneling Support
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.
Before the tunnel is brought up, VpnService.prepare() is now called. If the permission dialog needs to be shown, the native module:
- Launches the system intent via
startActivityForResult(intent, 1000) - Rejects the connect promise with error code
VPN_PERMISSION_REQUIRED - Returns immediately — the tunnel is not started
The caller handles VPN_PERMISSION_REQUIRED and re-calls connect() after the user grants permission (via AppState change detection or equivalent).
val intent = VpnService.prepare(reactApplicationContext)
if (intent != null) {
val activity = getCurrentActivity()
if (activity != null) {
activity.startActivityForResult(intent, 1000)
promise.reject("VPN_PERMISSION_REQUIRED", "Please accept the Android VPN permission dialog.")
return
}
}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.
if (config.hasKey("excludedApps")) {
val excludedApps = config.getArray("excludedApps")?.toArrayList()
excludedApps?.forEach { app ->
(app as? String)?.let { packageName ->
try {
interfaceBuilder.excludeApplication(packageName)
} catch (e: Exception) {
println("Failed to exclude app $packageName: ${e.message}")
}
}
}
}npm install react-native-wireguard-vpn-patched
# or
yarn add react-native-wireguard-vpn-patchedIf you are migrating from react-native-wireguard-vpn, uninstall it first:
npm uninstall react-native-wireguard-vpnimport WireGuard from 'react-native-wireguard-vpn-patched';
import { AppState } from 'react-native';
// Initialize once on app start
await WireGuard.initialize();
// Basic connection
await WireGuard.connect({
privateKey: '<client-private-key>',
publicKey: '<server-public-key>',
serverAddress: '203.0.113.1',
serverPort: 51820,
address: '10.64.0.2/32',
allowedIPs: ['0.0.0.0/0', '::/0'],
dns: ['1.1.1.1', '8.8.8.8'],
mtu: 1420,
});import { AppState } from 'react-native';
const connect = async () => {
try {
await WireGuard.connect(config);
// tunnel is up
} catch (err) {
if (err.code === 'VPN_PERMISSION_REQUIRED') {
// System dialog is now visible to the user.
// Listen for the app coming back to foreground, then retry.
const sub = AppState.addEventListener('change', async (state) => {
if (state === 'active') {
sub.remove();
await WireGuard.connect(config); // second call succeeds if user accepted
}
});
}
}
};Pass an excludedApps array of Android package names. Those apps will route through the device's regular internet connection, bypassing the VPN tunnel.
await WireGuard.connect({
// ...base config...
excludedApps: [
'com.google.android.apps.maps',
'com.example.bankingapp',
],
});Initializes the WireGuard Go backend. Must be called once before connect().
Establishes the VPN tunnel. Rejects with VPN_PERMISSION_REQUIRED on first run on Android if the user has not yet granted VPN permission.
Tears down the active tunnel.
Returns the current tunnel state.
Returns true if WireGuard is supported on the current device.
interface WireGuardConfig {
privateKey: string;
publicKey: string;
serverAddress: string;
serverPort: number;
address?: string | string[]; // tunnel interface IP, e.g. "10.64.0.2/32"
allowedIPs: string[]; // routing rules, e.g. ["0.0.0.0/0", "::/0"]
dns?: string[];
mtu?: number;
presharedKey?: string;
excludedApps?: string[]; // Android only — package names to bypass VPN
}
interface WireGuardStatus {
isConnected: boolean;
tunnelState: 'ACTIVE' | 'INACTIVE' | 'CONNECTING' | 'DISCONNECTING' | 'ERROR' | 'UNKNOWN';
status: 'CONNECTED' | 'DISCONNECTED' | 'CONNECTING' | 'DISCONNECTING' | 'ERROR' | 'UNKNOWN';
error?: string;
}| Requirement | Minimum Version |
|---|---|
| React Native | 0.72.0 |
| Android API | 21 (Android 5) |
| compileSdkVersion | 34 |
| Kotlin | 1.8.0 |
| Platform | Permission Fix | Split Tunneling |
|---|---|---|
| Android | Yes | Yes |
| iOS | N/A (handled by OS) | Not yet |
iOS split tunneling is not implemented in this fork. The iOS implementation is unchanged from the upstream package.
| Feature | react-native-wireguard-vpn |
This fork |
|---|---|---|
| Android VPN permission request | No | Yes |
Split tunneling (excludedApps) |
No | Yes |
VPN_PERMISSION_REQUIRED error code |
No | Yes |
TypeScript: excludedApps field |
No | Yes |
| Base version | 1.0.22 | 1.0.22-patch.2 |
MIT — same as the upstream package.
Original package copyright: usama7365
Fork modifications copyright: Abdul Ahad (ahad06074@gmail.com)