npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-ovpn

v0.1.2

Published

OpenVPN client for React Native — Android, iOS, and Expo. New Architecture (TurboModule), kill switch, custom DNS, bounded auto-reconnect.

Downloads

409

Readme

react-native-ovpn

npm license

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.

import { OpenVPNClient } from 'react-native-ovpn';

const client = new OpenVPNClient();
client.on('state', (s) => console.log(s)); // 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'disconnecting'
client.on('stats', ({ bytesIn, bytesOut }) => {});
client.on('error', (err) => console.warn(err.code, err.message));

await client.requestPermission();
await client.connect({
  config: ovpnFileContents,
  username: 'alice',
  password: 's3cret',
});
// ...
await client.disconnect();
client.dispose();

Demo

https://github.com/user-attachments/assets/6d19d50c-0ed3-4f73-994e-027c241ab149

The video renders inline on GitHub. On npmjs.com, open it directly.


Table of contents


Features

| Feature | Android | iOS | | --- | --- | --- | | Connect / disconnect | ✅ | ✅ | | State + stats + log events | ✅ | ✅ | | Auto-reconnect with bounded exponential backoff | ✅ | ✅ | | Username + password auth | ✅ | ✅ | | Certificate-only auth | ✅ | ✅ | | TLS-auth / tls-crypt / tls-crypt-v2 | ✅ | ✅ | | Legacy cipher fallback (AES-128-CBC) — auto-injected | ✅ | ✅ | | Custom DNS override | ✅ | ✅ | | Kill switch | ✅ | ❌ (Apple limitation) | | Foreground service notification | ✅ | n/a | | Per-app routing (allowed / disallowed apps) | ✅ | ❌ | | New Architecture (TurboModule + Fabric) | ✅ | ✅ | | Expo config plugin | ✅ | ✅ |


Install

npm install react-native-ovpn
# or
yarn add react-native-ovpn
# or
pnpm add react-native-ovpn

Expo (recommended)

Add the config plugin to your app.config.js (or app.json):

export default {
  expo: {
    // ...
    plugins: [
      [
        'react-native-ovpn',
        {
          // iOS only — Apple requires an App Group shared between the host
          // app and the PacketTunnel extension. Format: group.<your-bundle-id>.
          iosAppGroup: 'group.com.example.myapp',

          // Optional — the extension's bundle id. Defaults to
          // <host-bundle-id>.OpenVPNTunnel
          iosExtensionBundleIdentifier: 'com.example.myapp.OpenVPNTunnel',

          // Optional — Android notification channel name shown to users
          androidNotificationChannelName: 'VPN',

          // Optional — path (relative to the project root) to a small PNG/vector
          // notification icon. Falls back to the app icon if omitted.
          androidNotificationIcon: './assets/notification-icon.png',
        },
      ],
    ],
  },
};

Then regenerate the native projects:

npx expo prebuild --clean
npx expo run:android
# or run:ios after the iOS extension setup below

iOS extension is manual. Apple requires you to add the PacketTunnel extension target through Xcode (App Groups, Network Extensions entitlements, signing). The package ships a Swift template at ios/PacketTunnelProvider/. See iOS PacketTunnel setup below.

Bare React Native

Android

The native side is powered by ics-openvpn, vendored as a prebuilt AAR. autolinking handles the rest. You will need:

  1. Permissions in android/app/src/main/AndroidManifest.xml:

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
  2. Activity result for the VpnService.prepare() system dialog — our requestPermission() method takes care of this for you.

  3. MinSdk of 23 or higher. If you're on Expo SDK 50+, this is already the default.

  4. Java desugaring (already enabled by React Native 0.71+ via the default Gradle config).

iOS

Open ios/<YourApp>.xcworkspace in Xcode and add the PacketTunnel extension target — see iOS PacketTunnel extension setup.

Then run pod install from the ios directory.

iOS PacketTunnel extension setup

⚠️ iOS requires a separate Network Extension target inside your Xcode project. Apple does not allow this to be created from Expo's config plugin or pod install alone — it must be added manually, one time, in Xcode.

  1. In Xcode, File → New → TargetNetwork ExtensionPacket Tunnel Provider
  2. Name it OpenVPNTunnel (or match your iosExtensionBundleIdentifier)
  3. Add both the host app and the extension to the same App Group (matches iosAppGroup in the plugin config)
  4. Replace the auto-generated PacketTunnelProvider.swift with the one that ships at node_modules/react-native-ovpn/ios/PacketTunnelProvider/PacketTunnelProvider.swift
  5. Add the Network Extensions entitlement to both targets (com.apple.developer.networking.networkextensionpacket-tunnel-provider)
  6. The extension target needs the same OpenVPNAdapter pod — pod install after editing your Podfile to include the extension
  7. Re-build

API

OpenVPNClient

import { OpenVPNClient } from 'react-native-ovpn';

const client = new OpenVPNClient();

| Method | Returns | Notes | | --- | --- | --- | | 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. | | connect(options) | Promise<void> | Resolves once the tunnel reaches connected. Rejects on hard errors (auth failure, malformed config, etc.). | | disconnect() | Promise<void> | Tears down the tunnel and cancels any pending reconnect attempts. | | 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. | | getStats() | Promise<Stats> | Latest byte counters. Cheaper than subscribing to stats events. | | on(event, listener) | void | Subscribe to a tunnel event. See Events. | | off(event, listener) | void | Unsubscribe a single listener. | | removeAllListeners() | void | Drop all listeners across all events. | | dispose() | void | Remove all listeners and tear down native subscriptions. Call when the client instance is no longer needed. |

ConnectOptions:

type ConnectOptions = {
  /** Full .ovpn file contents as a string. */
  config: string;

  /** username/password for `auth-user-pass` configs. Pass empty strings for cert-only. */
  username: string;
  password: string;

  /** Enable kill switch — block all traffic when tunnel drops. Android only. */
  killSwitch?: boolean;

  /** Override DNS servers used inside the tunnel. */
  dns?: string[];

  /** Android: only these apps tunnel through the VPN. Mutually exclusive with disallowedApps. */
  allowedApps?: string[];

  /** Android: every app EXCEPT these tunnels. Mutually exclusive with allowedApps. */
  disallowedApps?: string[];

  /** Customize the foreground service notification. Android only. */
  notification?: NotificationOptions;

  /** Bounded auto-reconnect policy. */
  reconnect?: ReconnectOptions;
};

type NotificationOptions = {
  title?: string;
  text?: string;
  smallIcon?: string; // resource name (without extension) of a drawable
};

type ReconnectOptions = {
  maxRetries?: number;       // default 5
  baseDelayMs?: number;      // default 1000
  maxDelayMs?: number;       // default 60000
};

Events

client.on(event, listener) — listener signatures:

| Event | Payload | When | | --- | --- | --- | | state | VPNState | Tunnel state transitions: 'idle''connecting''connected'. Drops emit 'reconnecting' first, then 'disconnected' only after retries are exhausted. | | stats | { bytesIn: number; bytesOut: number; durationMs: number } | Throttled bandwidth counters (every ~1s on Android, ~3s on iOS). | | log | string | Single log line from the upstream OpenVPN engine. Verbose — only attach when debugging. | | error | OpenVPNError | Recoverable or fatal errors. See Errors. | | reconnecting | { attempt: number; delayMs: number } | Fires per retry attempt while the scheduler is active. |

type VPNState =
  | 'idle'
  | 'connecting'
  | 'connected'
  | 'reconnecting'
  | 'disconnecting'
  | 'disconnected';

Types

type Status = {
  state: VPNState;
  /** ms since epoch when 'connected' was first reached (this session). */
  connectedSince?: number;
  /** Server hostname or IP currently in use. */
  server?: string;
  localIp?: string;
  remoteIp?: string;
};

type Stats = {
  bytesIn: number;
  bytesOut: number;
  durationMs: number;
};

Errors

import { OpenVPNError, ERROR_CODES, type ErrorCode } from 'react-native-ovpn';

ErrorCode is a union of:

| Code | Meaning | Recoverable? | | --- | --- | --- | | AUTH_FAILED | Bad username/password, expired cert, blocked account. | ❌ | | TLS_HANDSHAKE_FAILED | Server cert mismatch or untrusted CA. | ❌ | | CONNECTION_REFUSED | Server rejected the connection (capacity, IP ban). | ⚠️ retry maybe | | CONNECTION_TIMEOUT | Couldn't reach the server. | ⚠️ retry | | DNS_RESOLUTION_FAILED | Couldn't resolve the remote hostname. | ⚠️ retry | | PERMISSION_DENIED | User declined the VPN system dialog. | ❌ (re-call requestPermission) | | MALFORMED_CONFIG | The .ovpn couldn't be parsed. | ❌ | | RECONNECT_EXHAUSTED | All retry attempts failed. | ❌ | | NATIVE_ERROR | Anything else surfaced from the native engine. | depends |

HARD_ERROR_CODES lists the non-recoverable codes. The client uses this set internally to stop the auto-reconnect loop on auth-class failures.


Recipes

Kill switch

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.

await client.connect({
  config: ovpn,
  username: 'alice',
  password: 's3cret',
  killSwitch: true,
});

Custom DNS

Override DNS servers inside the tunnel (e.g. force Cloudflare's 1.1.1.1 instead of the server's defaults):

await client.connect({
  config: ovpn,
  username: '',
  password: '',
  dns: ['1.1.1.1', '1.0.0.1'],
});

Per-app routing (disallowed/allowed apps)

Tunnel only your app:

await client.connect({
  config: ovpn,
  username: '',
  password: '',
  allowedApps: ['com.your.app'],
});

Or tunnel everything except Spotify (it has its own region locks):

await client.connect({
  // ...
  disallowedApps: ['com.spotify.music'],
});

Handling auto-reconnect

By default the client retries 5 times with exponential backoff (1s → 2s → 4s → … capped at 60s). Bump it for flaky networks:

client.on('reconnecting', ({ attempt, delayMs }) => {
  console.log(`reconnect ${attempt} in ${delayMs}ms`);
});

await client.connect({
  config: ovpn,
  username: '',
  password: '',
  reconnect: { maxRetries: 10, baseDelayMs: 500, maxDelayMs: 30_000 },
});

When all retries fail you'll get state: 'disconnected' + an error with code RECONNECT_EXHAUSTED.

Reading live bandwidth stats

const [stats, setStats] = useState({ bytesIn: 0, bytesOut: 0 });

useEffect(() => {
  const handler = (s) => setStats(s);
  client.on('stats', handler);
  return () => client.off('stats', handler);
}, []);

Customizing the foreground notification

Android requires a sticky notification while the VPN service runs.

await client.connect({
  // ...
  notification: {
    title: 'My App VPN',
    text: 'Connected to United States',
    smallIcon: 'notification_icon', // res/drawable/notification_icon.png
  },
});

To change the notification channel name, configure it once in the Expo plugin (androidNotificationChannelName) — it ships in the manifest at prebuild time.


OpenVPN config support

The library passes your .ovpn to the upstream engine as-is. Most directives work. Known-good and known-bad below.

✅ Supported directives

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).

Legacy ciphers (AES-128-CBC, AES-256-CBC): The library auto-injects data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305:AES-256-CBC:AES-128-CBC and data-ciphers-fallback AES-128-CBC if your .ovpn doesn't specify them, so VPN Gate / SoftEther / older corporate servers negotiate cleanly.

⚠️ Caveats

| Directive | Behavior | | --- | --- | | dev tap | Not supported — Layer-2 tunneling isn't available on Android/iOS. | | script-security, up, down | Stripped at runtime — sandboxed platforms don't allow shell hooks. | | management | Stripped — the engine speaks management to the wrapper internally. | | pkcs12 | Inline <ca>/<cert>/<key> works; PKCS#12 file paths don't. Extract to PEM first. | | --config /path/to/file.ovpn | N/A — pass the contents as a string in connect({ config }). |

❌ Not supported

fragment, mssfix N, route-method, dhcp-renew, pull-filter, route-noexec, client-cert-not-required (deprecated), route-delay (silently ignored).


Troubleshooting

RESOLVE: Cannot resolve host address: <hostname>

DNS is failing inside the tunnel pre-handshake. Common causes:

  • Server is offline (VPN Gate / public servers rotate every few hours)
  • Phone's underlying internet is down — try opening a browser
  • Custom DNS in connect({ dns }) is wrong — drop it temporarily

TLS Error: TLS handshake failed

Your .ovpn's embedded CA cert doesn't match the server's. Re-download the .ovpn from your provider — server certs rotate.

AUTH_FAILED after a known-good username/password

  • Some providers issue a separate VPN password (not your dashboard login). Check the provider's docs.
  • 2FA-protected accounts need an application password that pre-applies the OTP.

The tunnel connects, but no traffic flows

  • redirect-gateway def1 bypass-dhcp should be in the .ovpn — without it, only route directives get installed.
  • On Android, check Settings → Connections → More connection settings → VPN — your app should appear as the active VPN.

Android: app killed but tunnel stays running, then UI shows "Not Connected"

The JS process can die while the VpnService (a foreground service) stays alive. Call getStatus() on app reopen to reconcile:

useEffect(() => {
  client.getStatus().then((s) => {
    if (s.state === 'connected') {
      // update UI to connected state
    }
  });
}, []);

Android: Samsung / Xiaomi / OPPO killing the service after ~5 minutes

These OEMs aggressively kill background services. Have your app prompt for Battery optimization exemption:

import * as IntentLauncher from 'expo-intent-launcher';
import { Platform } from 'react-native';

if (Platform.OS === 'android') {
  await IntentLauncher.startActivityAsync(
    'android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS',
    { data: `package:${YOUR_PACKAGE_ID}` }
  );
}

You must also declare the permission in your manifest (Expo: add to app.config.js android.permissions: ['REQUEST_IGNORE_BATTERY_OPTIMIZATIONS']).

iOS: extension fails to load, app reports PERMISSION_DENIED

  • App Group entitlement is missing on one of the two targets (host or extension). Both need it, with the same group identifier.
  • 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).

How it works

  • Android: A thin Kotlin wrapper around 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.

  • iOS: Uses 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.

  • 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.

  • Codegen: Native specs in src/NativeOpenvpn.ts produce both old-architecture and Fabric-compatible bindings via React Native's codegen.


License

Wrapper code (this package): MIT.

Important — copyleft inheritance: This library embeds two upstream projects that ship under copyleft licenses:

Any app that ships react-native-ovpn inherits those obligations:

  • ✅ Personal projects, internal-only apps, open-source apps → fine
  • ✅ Compliance for GPL/AGPL → publish your app's source under a GPL/AGPL-compatible license, or
  • ❌ Closed-source commercial distribution → not legal without separate commercial agreements with the upstream maintainers (Arne Schwabe for ics-openvpn, Sergey Abramchuk for OpenVPNAdapter)

Use accordingly. See LICENSE in the package root for the MIT text covering this library's own code.


Contributing

Issues and PRs welcome. Please open an issue first for substantial changes.

Maintained by @Raselj71.