react-native-ovpn 0.1.0 → 0.1.1
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/README.md +503 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,29 +1,75 @@
|
|
|
1
1
|
# react-native-ovpn
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/react-native-ovpn)
|
|
4
|
-
[](
|
|
4
|
+
[](#license)
|
|
5
5
|
|
|
6
|
-
OpenVPN client for React Native — Android, iOS, and Expo
|
|
6
|
+
OpenVPN client for React Native — **Android, iOS, and Expo**. Built as a TurboModule (New Architecture compatible), with bounded auto-reconnect, kill switch, custom DNS, and a foreground-service notification.
|
|
7
7
|
|
|
8
8
|
```ts
|
|
9
9
|
import { OpenVPNClient } from 'react-native-ovpn';
|
|
10
10
|
|
|
11
11
|
const client = new OpenVPNClient();
|
|
12
|
-
client.on('state', (s) => console.log(s));
|
|
12
|
+
client.on('state', (s) => console.log(s)); // 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'disconnecting'
|
|
13
13
|
client.on('stats', ({ bytesIn, bytesOut }) => {});
|
|
14
|
+
client.on('error', (err) => console.warn(err.code, err.message));
|
|
14
15
|
|
|
15
16
|
await client.requestPermission();
|
|
16
|
-
await client.connect({
|
|
17
|
+
await client.connect({
|
|
18
|
+
config: ovpnFileContents,
|
|
19
|
+
username: 'alice',
|
|
20
|
+
password: 's3cret',
|
|
21
|
+
});
|
|
22
|
+
// ...
|
|
17
23
|
await client.disconnect();
|
|
24
|
+
client.dispose();
|
|
18
25
|
```
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
---
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
29
|
+
## Table of contents
|
|
30
|
+
|
|
31
|
+
- [Features](#features)
|
|
32
|
+
- [Install](#install)
|
|
33
|
+
- [Expo (recommended)](#expo-recommended)
|
|
34
|
+
- [Bare React Native](#bare-react-native)
|
|
35
|
+
- [API](#api)
|
|
36
|
+
- [`OpenVPNClient`](#openvpnclient)
|
|
37
|
+
- [Events](#events)
|
|
38
|
+
- [Types](#types)
|
|
39
|
+
- [Errors](#errors)
|
|
40
|
+
- [Recipes](#recipes)
|
|
41
|
+
- [Kill switch](#kill-switch)
|
|
42
|
+
- [Custom DNS](#custom-dns)
|
|
43
|
+
- [Per-app routing](#per-app-routing-disallowedallowed-apps)
|
|
44
|
+
- [Handling auto-reconnect](#handling-auto-reconnect)
|
|
45
|
+
- [Reading live bandwidth stats](#reading-live-bandwidth-stats)
|
|
46
|
+
- [Customizing the foreground notification](#customizing-the-foreground-notification)
|
|
47
|
+
- [OpenVPN config support](#openvpn-config-support)
|
|
48
|
+
- [Troubleshooting](#troubleshooting)
|
|
49
|
+
- [How it works](#how-it-works)
|
|
50
|
+
- [License](#license)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
| Feature | Android | iOS |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| Connect / disconnect | ✅ | ✅ |
|
|
59
|
+
| State + stats + log events | ✅ | ✅ |
|
|
60
|
+
| Auto-reconnect with bounded exponential backoff | ✅ | ✅ |
|
|
61
|
+
| Username + password auth | ✅ | ✅ |
|
|
62
|
+
| Certificate-only auth | ✅ | ✅ |
|
|
63
|
+
| TLS-auth / tls-crypt / tls-crypt-v2 | ✅ | ✅ |
|
|
64
|
+
| Legacy cipher fallback (AES-128-CBC) — auto-injected | ✅ | ✅ |
|
|
65
|
+
| Custom DNS override | ✅ | ✅ |
|
|
66
|
+
| Kill switch | ✅ | ❌ (Apple limitation) |
|
|
67
|
+
| Foreground service notification | ✅ | n/a |
|
|
68
|
+
| Per-app routing (allowed / disallowed apps) | ✅ | ❌ |
|
|
69
|
+
| New Architecture (TurboModule + Fabric) | ✅ | ✅ |
|
|
70
|
+
| Expo config plugin | ✅ | ✅ |
|
|
71
|
+
|
|
72
|
+
---
|
|
27
73
|
|
|
28
74
|
## Install
|
|
29
75
|
|
|
@@ -35,46 +81,466 @@ yarn add react-native-ovpn
|
|
|
35
81
|
pnpm add react-native-ovpn
|
|
36
82
|
```
|
|
37
83
|
|
|
38
|
-
|
|
84
|
+
### Expo (recommended)
|
|
85
|
+
|
|
86
|
+
Add the config plugin to your `app.config.js` (or `app.json`):
|
|
39
87
|
|
|
40
88
|
```js
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
export default {
|
|
90
|
+
expo: {
|
|
91
|
+
// ...
|
|
92
|
+
plugins: [
|
|
93
|
+
[
|
|
94
|
+
'react-native-ovpn',
|
|
95
|
+
{
|
|
96
|
+
// iOS only — Apple requires an App Group shared between the host
|
|
97
|
+
// app and the PacketTunnel extension. Format: group.<your-bundle-id>.
|
|
98
|
+
iosAppGroup: 'group.com.example.myapp',
|
|
99
|
+
|
|
100
|
+
// Optional — the extension's bundle id. Defaults to
|
|
101
|
+
// <host-bundle-id>.OpenVPNTunnel
|
|
102
|
+
iosExtensionBundleIdentifier: 'com.example.myapp.OpenVPNTunnel',
|
|
103
|
+
|
|
104
|
+
// Optional — Android notification channel name shown to users
|
|
105
|
+
androidNotificationChannelName: 'VPN',
|
|
106
|
+
|
|
107
|
+
// Optional — path (relative to the project root) to a small PNG/vector
|
|
108
|
+
// notification icon. Falls back to the app icon if omitted.
|
|
109
|
+
androidNotificationIcon: './assets/notification-icon.png',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
};
|
|
47
115
|
```
|
|
48
116
|
|
|
49
|
-
Then
|
|
117
|
+
Then regenerate the native projects:
|
|
50
118
|
|
|
51
|
-
|
|
119
|
+
```bash
|
|
120
|
+
npx expo prebuild --clean
|
|
121
|
+
npx expo run:android
|
|
122
|
+
# or run:ios after the iOS extension setup below
|
|
123
|
+
```
|
|
52
124
|
|
|
53
|
-
|
|
125
|
+
> **iOS extension is manual.** Apple requires you to add the
|
|
126
|
+
> *PacketTunnel* extension target through Xcode (App Groups, Network
|
|
127
|
+
> Extensions entitlements, signing). The package ships a Swift template
|
|
128
|
+
> at `ios/PacketTunnelProvider/`. See [iOS PacketTunnel setup](#ios-packettunnel-extension-setup) below.
|
|
54
129
|
|
|
55
|
-
|
|
130
|
+
### Bare React Native
|
|
131
|
+
|
|
132
|
+
#### Android
|
|
133
|
+
|
|
134
|
+
The native side is powered by [ics-openvpn](https://github.com/schwabe/ics-openvpn), vendored as a prebuilt AAR. autolinking handles the rest. You will need:
|
|
135
|
+
|
|
136
|
+
1. **Permissions** in `android/app/src/main/AndroidManifest.xml`:
|
|
137
|
+
```xml
|
|
138
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
139
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
140
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
141
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
2. **Activity result** for the `VpnService.prepare()` system dialog —
|
|
145
|
+
our `requestPermission()` method takes care of this for you.
|
|
146
|
+
|
|
147
|
+
3. **MinSdk** of 23 or higher. If you're on Expo SDK 50+, this is already
|
|
148
|
+
the default.
|
|
149
|
+
|
|
150
|
+
4. **Java desugaring** (already enabled by React Native 0.71+ via the
|
|
151
|
+
default Gradle config).
|
|
152
|
+
|
|
153
|
+
#### iOS
|
|
154
|
+
|
|
155
|
+
Open `ios/<YourApp>.xcworkspace` in Xcode and add the PacketTunnel
|
|
156
|
+
extension target — see [iOS PacketTunnel extension setup](#ios-packettunnel-extension-setup).
|
|
157
|
+
|
|
158
|
+
Then run `pod install` from the `ios` directory.
|
|
159
|
+
|
|
160
|
+
#### iOS PacketTunnel extension setup
|
|
161
|
+
|
|
162
|
+
> ⚠️ iOS requires a separate **Network Extension** target inside your
|
|
163
|
+
> Xcode project. Apple does not allow this to be created from Expo's
|
|
164
|
+
> config plugin or `pod install` alone — it must be added manually,
|
|
165
|
+
> one time, in Xcode.
|
|
166
|
+
|
|
167
|
+
1. In Xcode, **File → New → Target** → *Network Extension* → *Packet Tunnel Provider*
|
|
168
|
+
2. Name it **OpenVPNTunnel** (or match your `iosExtensionBundleIdentifier`)
|
|
169
|
+
3. Add both the host app and the extension to the same **App Group**
|
|
170
|
+
(matches `iosAppGroup` in the plugin config)
|
|
171
|
+
4. Replace the auto-generated `PacketTunnelProvider.swift` with the one
|
|
172
|
+
that ships at `node_modules/react-native-ovpn/ios/PacketTunnelProvider/PacketTunnelProvider.swift`
|
|
173
|
+
5. Add the **Network Extensions** entitlement to both targets
|
|
174
|
+
(`com.apple.developer.networking.networkextension` →
|
|
175
|
+
`packet-tunnel-provider`)
|
|
176
|
+
6. The extension target needs the same `OpenVPNAdapter` pod —
|
|
177
|
+
`pod install` after editing your `Podfile` to include the extension
|
|
178
|
+
7. Re-build
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## API
|
|
183
|
+
|
|
184
|
+
### `OpenVPNClient`
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { OpenVPNClient } from 'react-native-ovpn';
|
|
188
|
+
|
|
189
|
+
const client = new OpenVPNClient();
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| Method | Returns | Notes |
|
|
56
193
|
| --- | --- | --- |
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
194
|
+
| `requestPermission()` | `Promise<boolean>` | Shows the OS `VpnService.prepare` dialog (Android) / Network Extension permission (iOS). Must be called before the first `connect()`. Resolves `true` once granted. |
|
|
195
|
+
| `connect(options)` | `Promise<void>` | Resolves once the tunnel reaches `connected`. Rejects on hard errors (auth failure, malformed config, etc.). |
|
|
196
|
+
| `disconnect()` | `Promise<void>` | Tears down the tunnel and cancels any pending reconnect attempts. |
|
|
197
|
+
| `getStatus()` | `Promise<Status>` | Snapshot of the current native state — useful on app reopen if your JS process was killed but the foreground service stayed alive. |
|
|
198
|
+
| `getStats()` | `Promise<Stats>` | Latest byte counters. Cheaper than subscribing to `stats` events. |
|
|
199
|
+
| `on(event, listener)` | `void` | Subscribe to a tunnel event. See [Events](#events). |
|
|
200
|
+
| `off(event, listener)` | `void` | Unsubscribe a single listener. |
|
|
201
|
+
| `removeAllListeners()` | `void` | Drop all listeners across all events. |
|
|
202
|
+
| `dispose()` | `void` | Remove all listeners and tear down native subscriptions. Call when the client instance is no longer needed. |
|
|
203
|
+
|
|
204
|
+
`ConnectOptions`:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
type ConnectOptions = {
|
|
208
|
+
/** Full .ovpn file contents as a string. */
|
|
209
|
+
config: string;
|
|
210
|
+
|
|
211
|
+
/** username/password for `auth-user-pass` configs. Pass empty strings for cert-only. */
|
|
212
|
+
username: string;
|
|
213
|
+
password: string;
|
|
214
|
+
|
|
215
|
+
/** Enable kill switch — block all traffic when tunnel drops. Android only. */
|
|
216
|
+
killSwitch?: boolean;
|
|
217
|
+
|
|
218
|
+
/** Override DNS servers used inside the tunnel. */
|
|
219
|
+
dns?: string[];
|
|
220
|
+
|
|
221
|
+
/** Android: only these apps tunnel through the VPN. Mutually exclusive with disallowedApps. */
|
|
222
|
+
allowedApps?: string[];
|
|
223
|
+
|
|
224
|
+
/** Android: every app EXCEPT these tunnels. Mutually exclusive with allowedApps. */
|
|
225
|
+
disallowedApps?: string[];
|
|
226
|
+
|
|
227
|
+
/** Customize the foreground service notification. Android only. */
|
|
228
|
+
notification?: NotificationOptions;
|
|
229
|
+
|
|
230
|
+
/** Bounded auto-reconnect policy. */
|
|
231
|
+
reconnect?: ReconnectOptions;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
type NotificationOptions = {
|
|
235
|
+
title?: string;
|
|
236
|
+
text?: string;
|
|
237
|
+
smallIcon?: string; // resource name (without extension) of a drawable
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
type ReconnectOptions = {
|
|
241
|
+
maxRetries?: number; // default 5
|
|
242
|
+
baseDelayMs?: number; // default 1000
|
|
243
|
+
maxDelayMs?: number; // default 60000
|
|
244
|
+
};
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Events
|
|
248
|
+
|
|
249
|
+
`client.on(event, listener)` — listener signatures:
|
|
250
|
+
|
|
251
|
+
| Event | Payload | When |
|
|
252
|
+
| --- | --- | --- |
|
|
253
|
+
| `state` | `VPNState` | Tunnel state transitions: `'idle'` → `'connecting'` → `'connected'`. Drops emit `'reconnecting'` first, then `'disconnected'` only after retries are exhausted. |
|
|
254
|
+
| `stats` | `{ bytesIn: number; bytesOut: number; durationMs: number }` | Throttled bandwidth counters (every ~1s on Android, ~3s on iOS). |
|
|
255
|
+
| `log` | `string` | Single log line from the upstream OpenVPN engine. Verbose — only attach when debugging. |
|
|
256
|
+
| `error` | `OpenVPNError` | Recoverable or fatal errors. See [Errors](#errors). |
|
|
257
|
+
| `reconnecting` | `{ attempt: number; delayMs: number }` | Fires per retry attempt while the scheduler is active. |
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
type VPNState =
|
|
261
|
+
| 'idle'
|
|
262
|
+
| 'connecting'
|
|
263
|
+
| 'connected'
|
|
264
|
+
| 'reconnecting'
|
|
265
|
+
| 'disconnecting'
|
|
266
|
+
| 'disconnected';
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Types
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
type Status = {
|
|
273
|
+
state: VPNState;
|
|
274
|
+
/** ms since epoch when 'connected' was first reached (this session). */
|
|
275
|
+
connectedSince?: number;
|
|
276
|
+
/** Server hostname or IP currently in use. */
|
|
277
|
+
server?: string;
|
|
278
|
+
localIp?: string;
|
|
279
|
+
remoteIp?: string;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
type Stats = {
|
|
283
|
+
bytesIn: number;
|
|
284
|
+
bytesOut: number;
|
|
285
|
+
durationMs: number;
|
|
286
|
+
};
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Errors
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { OpenVPNError, ERROR_CODES, type ErrorCode } from 'react-native-ovpn';
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`ErrorCode` is a union of:
|
|
296
|
+
|
|
297
|
+
| Code | Meaning | Recoverable? |
|
|
298
|
+
| --- | --- | --- |
|
|
299
|
+
| `AUTH_FAILED` | Bad username/password, expired cert, blocked account. | ❌ |
|
|
300
|
+
| `TLS_HANDSHAKE_FAILED` | Server cert mismatch or untrusted CA. | ❌ |
|
|
301
|
+
| `CONNECTION_REFUSED` | Server rejected the connection (capacity, IP ban). | ⚠️ retry maybe |
|
|
302
|
+
| `CONNECTION_TIMEOUT` | Couldn't reach the server. | ⚠️ retry |
|
|
303
|
+
| `DNS_RESOLUTION_FAILED` | Couldn't resolve the `remote` hostname. | ⚠️ retry |
|
|
304
|
+
| `PERMISSION_DENIED` | User declined the VPN system dialog. | ❌ (re-call `requestPermission`) |
|
|
305
|
+
| `MALFORMED_CONFIG` | The `.ovpn` couldn't be parsed. | ❌ |
|
|
306
|
+
| `RECONNECT_EXHAUSTED` | All retry attempts failed. | ❌ |
|
|
307
|
+
| `NATIVE_ERROR` | Anything else surfaced from the native engine. | depends |
|
|
308
|
+
|
|
309
|
+
`HARD_ERROR_CODES` lists the non-recoverable codes. The client uses this set internally to stop the auto-reconnect loop on auth-class failures.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Recipes
|
|
69
314
|
|
|
70
|
-
|
|
315
|
+
### Kill switch
|
|
316
|
+
|
|
317
|
+
Blocks all traffic when the tunnel drops, so your app never accidentally leaks plain-text packets. **Android only** — iOS doesn't expose this to non-Apple VPN apps.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
await client.connect({
|
|
321
|
+
config: ovpn,
|
|
322
|
+
username: 'alice',
|
|
323
|
+
password: 's3cret',
|
|
324
|
+
killSwitch: true,
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Custom DNS
|
|
329
|
+
|
|
330
|
+
Override DNS servers inside the tunnel (e.g. force Cloudflare's 1.1.1.1 instead of the server's defaults):
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
await client.connect({
|
|
334
|
+
config: ovpn,
|
|
335
|
+
username: '',
|
|
336
|
+
password: '',
|
|
337
|
+
dns: ['1.1.1.1', '1.0.0.1'],
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Per-app routing (disallowed/allowed apps)
|
|
342
|
+
|
|
343
|
+
Tunnel only your app:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
await client.connect({
|
|
347
|
+
config: ovpn,
|
|
348
|
+
username: '',
|
|
349
|
+
password: '',
|
|
350
|
+
allowedApps: ['com.your.app'],
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Or tunnel everything **except** Spotify (it has its own region locks):
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
await client.connect({
|
|
358
|
+
// ...
|
|
359
|
+
disallowedApps: ['com.spotify.music'],
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Handling auto-reconnect
|
|
364
|
+
|
|
365
|
+
By default the client retries 5 times with exponential backoff (1s → 2s → 4s → … capped at 60s). Bump it for flaky networks:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
client.on('reconnecting', ({ attempt, delayMs }) => {
|
|
369
|
+
console.log(`reconnect ${attempt} in ${delayMs}ms`);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await client.connect({
|
|
373
|
+
config: ovpn,
|
|
374
|
+
username: '',
|
|
375
|
+
password: '',
|
|
376
|
+
reconnect: { maxRetries: 10, baseDelayMs: 500, maxDelayMs: 30_000 },
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
When all retries fail you'll get `state: 'disconnected'` + an `error` with code `RECONNECT_EXHAUSTED`.
|
|
381
|
+
|
|
382
|
+
### Reading live bandwidth stats
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
const [stats, setStats] = useState({ bytesIn: 0, bytesOut: 0 });
|
|
386
|
+
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
const handler = (s) => setStats(s);
|
|
389
|
+
client.on('stats', handler);
|
|
390
|
+
return () => client.off('stats', handler);
|
|
391
|
+
}, []);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Customizing the foreground notification
|
|
395
|
+
|
|
396
|
+
Android requires a sticky notification while the VPN service runs.
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
await client.connect({
|
|
400
|
+
// ...
|
|
401
|
+
notification: {
|
|
402
|
+
title: 'My App VPN',
|
|
403
|
+
text: 'Connected to United States',
|
|
404
|
+
smallIcon: 'notification_icon', // res/drawable/notification_icon.png
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
To change the notification channel name, configure it once in the Expo plugin (`androidNotificationChannelName`) — it ships in the manifest at prebuild time.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## OpenVPN config support
|
|
414
|
+
|
|
415
|
+
The library passes your `.ovpn` to the upstream engine as-is. Most directives work. Known-good and known-bad below.
|
|
416
|
+
|
|
417
|
+
### ✅ Supported directives
|
|
418
|
+
|
|
419
|
+
`client`, `dev tun`, `proto tcp|udp`, `remote`, `resolv-retry`, `nobind`, `persist-key`, `persist-tun`, `remote-cert-tls`, `auth-user-pass`, `cipher`, `data-ciphers`, `auth`, `verb`, `mute`, `keepalive`, `<ca>...</ca>`, `<cert>...</cert>`, `<key>...</key>`, `<tls-auth>...</tls-auth>`, `<tls-crypt>...</tls-crypt>`, `<tls-crypt-v2>...</tls-crypt-v2>`, `tls-cipher`, `comp-lzo`, `compress`, `redirect-gateway`, `route`, `dhcp-option DNS`, `script-security` (parsed but ignored — see below).
|
|
420
|
+
|
|
421
|
+
**Legacy ciphers (AES-128-CBC, AES-256-CBC):** The library auto-injects
|
|
422
|
+
`data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305:AES-256-CBC:AES-128-CBC`
|
|
423
|
+
and `data-ciphers-fallback AES-128-CBC` if your `.ovpn` doesn't specify
|
|
424
|
+
them, so VPN Gate / SoftEther / older corporate servers negotiate cleanly.
|
|
425
|
+
|
|
426
|
+
### ⚠️ Caveats
|
|
427
|
+
|
|
428
|
+
| Directive | Behavior |
|
|
429
|
+
| --- | --- |
|
|
430
|
+
| `dev tap` | Not supported — Layer-2 tunneling isn't available on Android/iOS. |
|
|
431
|
+
| `script-security`, `up`, `down` | Stripped at runtime — sandboxed platforms don't allow shell hooks. |
|
|
432
|
+
| `management` | Stripped — the engine speaks management to the wrapper internally. |
|
|
433
|
+
| `pkcs12` | Inline `<ca>/<cert>/<key>` works; PKCS#12 file paths don't. Extract to PEM first. |
|
|
434
|
+
| `--config /path/to/file.ovpn` | N/A — pass the contents as a string in `connect({ config })`. |
|
|
435
|
+
|
|
436
|
+
### ❌ Not supported
|
|
437
|
+
|
|
438
|
+
`fragment`, `mssfix N`, `route-method`, `dhcp-renew`, `pull-filter`, `route-noexec`, `client-cert-not-required` (deprecated), `route-delay` (silently ignored).
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Troubleshooting
|
|
443
|
+
|
|
444
|
+
### `RESOLVE: Cannot resolve host address: <hostname>`
|
|
445
|
+
|
|
446
|
+
DNS is failing inside the tunnel pre-handshake. Common causes:
|
|
447
|
+
|
|
448
|
+
- Server is offline (VPN Gate / public servers rotate every few hours)
|
|
449
|
+
- Phone's underlying internet is down — try opening a browser
|
|
450
|
+
- Custom DNS in `connect({ dns })` is wrong — drop it temporarily
|
|
451
|
+
|
|
452
|
+
### `TLS Error: TLS handshake failed`
|
|
453
|
+
|
|
454
|
+
Your `.ovpn`'s embedded CA cert doesn't match the server's. Re-download the
|
|
455
|
+
`.ovpn` from your provider — server certs rotate.
|
|
456
|
+
|
|
457
|
+
### `AUTH_FAILED` after a known-good username/password
|
|
458
|
+
|
|
459
|
+
- Some providers issue a separate VPN password (not your dashboard login). Check the provider's docs.
|
|
460
|
+
- 2FA-protected accounts need an *application password* that pre-applies the OTP.
|
|
461
|
+
|
|
462
|
+
### The tunnel connects, but no traffic flows
|
|
463
|
+
|
|
464
|
+
- `redirect-gateway def1 bypass-dhcp` should be in the `.ovpn` — without it, only `route` directives get installed.
|
|
465
|
+
- On Android, check Settings → Connections → More connection settings → VPN — your app should appear as the active VPN.
|
|
466
|
+
|
|
467
|
+
### Android: app killed but tunnel stays running, then UI shows "Not Connected"
|
|
468
|
+
|
|
469
|
+
The JS process can die while the `VpnService` (a foreground service) stays alive. Call `getStatus()` on app reopen to reconcile:
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
client.getStatus().then((s) => {
|
|
474
|
+
if (s.state === 'connected') {
|
|
475
|
+
// update UI to connected state
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}, []);
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Android: Samsung / Xiaomi / OPPO killing the service after ~5 minutes
|
|
482
|
+
|
|
483
|
+
These OEMs aggressively kill background services. Have your app prompt for **Battery optimization exemption**:
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
import * as IntentLauncher from 'expo-intent-launcher';
|
|
487
|
+
import { Platform } from 'react-native';
|
|
488
|
+
|
|
489
|
+
if (Platform.OS === 'android') {
|
|
490
|
+
await IntentLauncher.startActivityAsync(
|
|
491
|
+
'android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS',
|
|
492
|
+
{ data: `package:${YOUR_PACKAGE_ID}` }
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
You must also declare the permission in your manifest (Expo: add to `app.config.js` `android.permissions: ['REQUEST_IGNORE_BATTERY_OPTIMIZATIONS']`).
|
|
498
|
+
|
|
499
|
+
### iOS: extension fails to load, app reports `PERMISSION_DENIED`
|
|
500
|
+
|
|
501
|
+
- App Group entitlement is missing on **one** of the two targets (host or extension). Both need it, with the same group identifier.
|
|
502
|
+
- The PacketTunnel target's bundle id must start with the host app's bundle id (`com.example.app.OpenVPNTunnel` if host is `com.example.app`).
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## How it works
|
|
507
|
+
|
|
508
|
+
- **Android**: A thin Kotlin wrapper around [ics-openvpn](https://github.com/schwabe/ics-openvpn) (vendored as a prebuilt AAR). Tunneling runs in a `VpnService` (`OpenvpnService`). The wrapper translates `connect()` parameters into the engine's `ProfileBuilder`, then forwards engine state callbacks back over a TurboModule event emitter.
|
|
509
|
+
|
|
510
|
+
- **iOS**: Uses [OpenVPNAdapter](https://github.com/ss-abramchuk/OpenVPNAdapter) running in a Packet Tunnel Provider extension. The host app talks to the extension via `NETunnelProviderManager`. App Group shared user defaults pass state events between the two processes.
|
|
511
|
+
|
|
512
|
+
- **Auto-reconnect**: A pure-JS scheduler (`Scheduler` class) drives retries with exponential backoff. The state event collapses `disconnected → reconnecting` in the same JS tick so the UI never paints "Not Connected" between retries.
|
|
513
|
+
|
|
514
|
+
- **Codegen**: Native specs in `src/NativeOpenvpn.ts` produce both old-architecture and Fabric-compatible bindings via React Native's codegen.
|
|
515
|
+
|
|
516
|
+
---
|
|
71
517
|
|
|
72
518
|
## License
|
|
73
519
|
|
|
74
|
-
Wrapper code: MIT
|
|
520
|
+
Wrapper code (this package): **MIT**.
|
|
521
|
+
|
|
522
|
+
**Important — copyleft inheritance:** This library embeds two upstream
|
|
523
|
+
projects that ship under copyleft licenses:
|
|
75
524
|
|
|
76
|
-
**
|
|
525
|
+
- **Android**: [ics-openvpn](https://github.com/schwabe/ics-openvpn) → **GPL-2.0**
|
|
526
|
+
- **iOS**: [OpenVPNAdapter](https://github.com/ss-abramchuk/OpenVPNAdapter) → **AGPL-3.0**
|
|
527
|
+
|
|
528
|
+
Any app that ships `react-native-ovpn` inherits those obligations:
|
|
529
|
+
|
|
530
|
+
- ✅ Personal projects, internal-only apps, open-source apps → fine
|
|
531
|
+
- ✅ Compliance for GPL/AGPL → publish your app's source under a
|
|
532
|
+
GPL/AGPL-compatible license, or
|
|
533
|
+
- ❌ Closed-source commercial distribution → **not legal** without
|
|
534
|
+
separate commercial agreements with the upstream maintainers (Arne Schwabe
|
|
535
|
+
for ics-openvpn, Sergey Abramchuk for OpenVPNAdapter)
|
|
536
|
+
|
|
537
|
+
Use accordingly. See `LICENSE` in the package root for the MIT text covering
|
|
538
|
+
this library's own code.
|
|
539
|
+
|
|
540
|
+
---
|
|
77
541
|
|
|
78
542
|
## Contributing
|
|
79
543
|
|
|
80
|
-
|
|
544
|
+
Issues and PRs welcome. Please open an issue first for substantial changes.
|
|
545
|
+
|
|
546
|
+
Maintained by [@Raselj71](https://github.com/Raselj71).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-ovpn",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenVPN client for React Native — Android, iOS, and Expo. New Architecture (TurboModule), kill switch, custom DNS, bounded auto-reconnect.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|