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