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-wireguard-turbo

v1.0.4

Published

WireGuard TurboModule for React Native (Android) with :tunnel bundled.

Readme

⚡ React Native WireGuard (Android) — Codegen + TurboModule

Let me introduce WireGuard VPN library for Android using React Native Codegen (TurboModule).

RN 0.80+ required.

Methods: connect, disconnect, getStatus, ping

Auto: foreground system notification while connected (Play Store-compliant). Disappears on disconnect.

Note: Android requires a native VPN consent dialog. RN can’t trigger it by itself — you must add a small change in MainActivity (bare RN) or use the provided Expo plugin.

If you need iOS/macOS/Windows: not supported here. Android only.

⚙️ Requirements

React Native 0.80+

Android minSdk 24+, target/compile 35 recommended

Kotlin 1.9+

Java 17+ (works with 22 as well)

WireGuard config string (standard wg quick format)

📦 Install

npm

npm i react-native-wireguard-turbo

yarn

yarn add react-native-wireguard-turbo

📲 Android setup (bare RN)

Trigger VPN consent from MainActivity (once; before first connect)

MainActivity.kt

package your.app.package

import android.content.Intent
import android.net.VpnService
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.facebook.react.ReactActivity

class MainActivity : ReactActivity() {

  private lateinit var vpnConsentLauncher: ActivityResultLauncher<Intent>

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    vpnConsentLauncher = registerForActivityResult(
      ActivityResultContracts.StartActivityForResult()
    ) { /* your logic goes here */ }

    ensureVpnConsent()
  }

  private fun ensureVpnConsent() {
    val intent = VpnService.prepare(this)
    if (intent != null) {
      vpnConsentLauncher.launch(intent) // shows the system dialog
    }
    // else: already granted previously
  }

  override fun getMainComponentName(): String = "App"
}

🔑 Permissions (Android Manifest)

These are typically merged by the library. If your app is strict, ensure:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

The library runs a foreground VPN service when connected and removes the notification when disconnected. No extra service wiring should be needed if manifest merging is on.

🧩 Expo setup (prebuild / custom dev client)

Add the plugin so VPN consent and any native bits are injected automatically.

app.json or app.config.js

{
  "expo": {
    "plugins": [
      ["<your-package-name>/expo-plugin", {}]
    ]
  }
}

Write you plugin. Check Expo plugin example below:

// plugins/ensure-vpn-consent.js
const { withMainActivity } = require("@expo/config-plugins");

module.exports = (config) =>
  withMainActivity(config, (cfg) => {
    let src = cfg.modResults.contents;
    if (cfg.modResults.language !== "kt") throw new Error("Expected Kotlin MainActivity.kt");

    // 0) Do NOT touch the anchor; bail if it’s missing.
    if (!src.includes("super.onCreate(null)")) return cfg;

    // Ensure imports (idempotent)
    const addImport = (imp) => {
      if (!src.includes(`import ${imp}`)) {
        src = src.replace(/(package .*?\n)/, `$1import ${imp}\n`);
      }
    };
    addImport("android.content.Intent");
    addImport("android.net.VpnService");
    addImport("androidx.activity.result.ActivityResultLauncher");
    addImport("androidx.activity.result.contract.ActivityResultContracts");

    // Add field inside class (once)
    if (!src.includes("vpnConsentLauncher: ActivityResultLauncher<Intent>")) {
      src = src.replace(
        /(class\s+MainActivity\s*:\s*ReactActivity\(\)\s*\{\s*)/,
        `$1\n  private lateinit var vpnConsentLauncher: ActivityResultLauncher<Intent>\n`
      );
    }

    // Inject launcher + consent call immediately AFTER the anchor line (once)
    if (!src.includes("// VPN_CONSENT_START")) {
      src = src.replace(
        /super\.onCreate\(null\)\s*\n/,
        `super.onCreate(null)\n` +
          `    // VPN_CONSENT_START\n` +
          `    vpnConsentLauncher = registerForActivityResult(\n` +
          `      ActivityResultContracts.StartActivityForResult()\n` +
          `    ) { /* handle result if needed */ }\n` +
          `    VpnService.prepare(this)?.let { vpnConsentLauncher.launch(it) }\n` +
          `    // VPN_CONSENT_END\n`
      );
    }

    cfg.modResults.contents = src;
    return cfg;
  });

Then:

npx expo prebuild
npx expo run:android

This requires a custom dev client. It will not work on Expo Go.

Usage

import { WG } from 'react-native-wireguard-turbo'

const cfg = `
[Interface]
PrivateKey = YourVeeRYSeCRETkeY=
Address = 10.0.0.2/32
DNS = 1.1.1.1

[Peer]
PublicKey = YourVeeRYSeCRETkeY=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = your.server.com:51820
PersistentKeepalive = 25
`.trim()

async function startVpn() {
  // Make sure user granted VPN consent first (see MainActivity / plugin)
  await WG.connect(cfg) // throws on invalid config or failure
}

async function stopVpn() {
  await WG.disconnect()
}

async function status() {
  const s = await WG.getStatus()
  // 'DOWN' | 'UP' | 'ERROR' | 'UNKNOWN'
  console.log(s)
}

async function heartbeat() {
  const ms = await WG.ping() // round-trip latency estimate (ms) or -1 if unknown
  console.log('ping', ms)
}

🧩 API

connect(config: string): Promise<void>

Starts the WireGuard tunnel from a full wg config string. Foreground notification appears while connected.

disconnect(): Promise<void>

Stops the tunnel. Notification is removed.

getStatus(): Promise<'DOWN' | 'UP' | 'ERROR' | 'UNKNOWN'>

Just ping to test status.

ping(): Promise<string> // "pong"

The tunnel rebinds automatically if the traffic source changed (for example from Wi-Fi to mobile network).

Foreground notification (Play Store rule)

Shown automatically while VPN is connected (system channel e.g., "wg").

Removed on disconnect() and on service stop.

Network change handling

Handled automatically with rebind(). Call it if you detect manual network switches.

🐞 Troubleshooting

Consent dialog never appears: You didn’t call VpnService.prepare(...) from MainActivity (or didn’t add the Expo plugin).

Connected but no traffic: Check AllowedIPs includes 0.0.0.0/0, ::/0 (or the routes you actually want). Also verify server allows your peer, and MTU is sane.

Notification missing on Android 13+: Request POST_NOTIFICATIONS permission or you won’t see it.

🛜 Example

import { View, TouchableOpacity, Text } from 'react-native';
import WG from 'react-native-wireguard-turbo';

const cfg = `[Interface]
Address = xxx.xxx.xx.xx/xx
PrivateKey = YourVeeRYSeCRETkeY=
DNS = 8.8.8.8

[Peer]
PublicKey = YourVeeRYSeCRETkeY=
AllowedIPs = 0.0.0.0/0
Endpoint = xx.xxx.xxx.xx:51820
PersistentKeepalive = 25`;

export default function HomeScreen() {
  return (
    <View style={{ flex: 1 }}>
      <TouchableOpacity
        style={{
          marginTop: 50,
        }}
        onPress={async () => {
          console.log('Button pressed');
          try {
            await WG.connect(cfg);
            console.log('Manual connect SUCCESS');
          } catch (e) {
            console.log('Manual connect FAILED', e);
          }
        }}
      >
        <Text>Connect</Text>
      </TouchableOpacity>
   </View>
  );
}

⚡ React Native WireGuard (IOS)

Installation on iOS is pretty much the same, but you need to do a few additional things:

You need your teamId and appGroupId. Fill them in in app.json.

Then, on developer.apple.com:

You need to create:

  1. App Group Go to Certificates > Identifiers > App Groups Create: your_name

  2. Main App ID (your_name) Capabilities to enable: App Groups + Network Extensions Link the app group you created above.

  3. Extension App ID (your_name.WireGuardNetworkExtension) Capabilities to enable: App Groups + Network Extensions (Packet Tunnel Provider) Link the same app group.

The linking happens inside each App ID—when you enable the App Groups capability, it lets you select which groups to attach. Both the main app and the extension must have the same group attached so they can share UserDefaults.

After that, Xcode's automatic signing picks it all up when you build.

For the main app and the extension App ID, enable only these two:

App Groups Network Extensions

What to toggle inside Network Extensions:

Enable Packet Tunnel—that's the only one you need.

After creating both App IDs, open each one and, under the App Groups capability, click Configure—it will show your app groups list. Select your_name and save.

Do this for both App IDs. That's the link.

If you want to change, let's say, the server address or description from “WireGuard,” open WireGuardModule.swift, find these lines:

proto.serverAddress = "WireGuard"
manager.localizedDescription = "WireGuard"

and change them!

You also need a plugin to create and manage targets. This is an example (or use AI—they are very good at creating plugins):

const {
  withXcodeProject,
  withEntitlementsPlist,
  withDangerousMod,
} = require("@expo/config-plugins");
const path = require("path");
const fs = require("fs");

const EXTENSION_NAME = "WireGuardNetworkExtension";

function withWireGuard(config, options = {}) {
  const appGroupId =
    options.appGroupId || `group.${config.ios?.bundleIdentifier}`;

  config = withMainAppEntitlements(config, appGroupId);
  config = withExtensionFiles(config, appGroupId);
  config = withExtensionTarget(config, appGroupId);
  config = withMainAppSigning(config);

  return config;
}

function withMainAppEntitlements(config, appGroupId) {
  return withEntitlementsPlist(config, (c) => {
    c.modResults["com.apple.developer.networking.networkextension"] = [
      "packet-tunnel-provider",
    ];
    c.modResults["com.apple.security.application-groups"] = [appGroupId];
    return c;
  });
}

function withExtensionFiles(config, appGroupId) {
  return withDangerousMod(config, [
    "ios",
    async (c) => {
      const iosRoot = c.modRequest.platformProjectRoot;
      const extDir = path.join(iosRoot, EXTENSION_NAME);
      const extensionBundleId = `${c.ios?.bundleIdentifier}.${EXTENSION_NAME}`;

      if (!fs.existsSync(extDir)) {
        fs.mkdirSync(extDir, { recursive: true });
      }

      const src = path.join(
        c.modRequest.projectRoot,
        "node_modules/react-native-wireguard-turbo/ios/WireGuardTunnelProvider.swift",
      );
      fs.copyFileSync(src, path.join(extDir, "PacketTunnelProvider.swift"));

      const wgQuickSrc = path.join(
        c.modRequest.projectRoot,
        "node_modules/react-native-wireguard-turbo/ios/TunnelConfiguration+WgQuickConfig.swift",
      );
      const stringSrc = path.join(
        c.modRequest.projectRoot,
        "node_modules/react-native-wireguard-turbo/ios/String+ArrayConversion.swift",
      );
      fs.copyFileSync(
        wgQuickSrc,
        path.join(extDir, "TunnelConfiguration+WgQuickConfig.swift"),
      );

      fs.copyFileSync(
        stringSrc,
        path.join(extDir, "String+ArrayConversion.swift"),
      );

      fs.writeFileSync(
        path.join(extDir, `${EXTENSION_NAME}-Info.plist`),
        `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleDisplayName</key>
  <string>${EXTENSION_NAME}</string>
  <key>CFBundleExecutable</key>
  <string>$(EXECUTABLE_NAME)</string>
  <key>CFBundleIdentifier</key>
  <string>${extensionBundleId}</string>
  <key>CFBundleInfoDictionaryVersion</key>
  <string>6.0</string>
  <key>CFBundleName</key>
  <string>$(PRODUCT_NAME)</string>
  <key>CFBundlePackageType</key>
  <string>XPC!</string>
  <key>CFBundleShortVersionString</key>
  <string>$(MARKETING_VERSION)</string>
  <key>CFBundleVersion</key>
  <string>1</string>
  <key>NSExtension</key>
  <dict>
    <key>NSExtensionPointIdentifier</key>
    <string>com.apple.networkextension.packet-tunnel</string>
    <key>NSExtensionPrincipalClass</key>
    <string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
  </dict>
</dict>
</plist>`,
      );

      fs.writeFileSync(
        path.join(extDir, `${EXTENSION_NAME}.entitlements`),
        `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.developer.networking.networkextension</key>
  <array>
    <string>packet-tunnel-provider</string>
  </array>
  <key>com.apple.security.application-groups</key>
  <array>
    <string>${appGroupId}</string>
  </array>
</dict>
</plist>`,
      );

      return c;
    },
  ]);
}

function withExtensionTarget(config, appGroupId) {
  return withXcodeProject(config, (c) => {
    const project = c.modResults;
    const extensionBundleId = `${c.ios?.bundleIdentifier}.${EXTENSION_NAME}`;

    const targets = project.pbxNativeTargetSection();
    const exists = Object.values(targets).some(
      (t) => t && t.name === EXTENSION_NAME,
    );
    if (exists) return c;

    const target = project.addTarget(
      EXTENSION_NAME,
      "app_extension",
      EXTENSION_NAME,
      extensionBundleId,
    );

    const extGroup = project.addPbxGroup([], EXTENSION_NAME, EXTENSION_NAME);
    const mainGroupUuid = project.getFirstProject().firstProject.mainGroup;
    project.addToPbxGroup(extGroup.uuid, mainGroupUuid);

    project.addBuildPhase(
      [
        `${EXTENSION_NAME}/PacketTunnelProvider.swift`,
        `${EXTENSION_NAME}/TunnelConfiguration+WgQuickConfig.swift`,
        `${EXTENSION_NAME}/String+ArrayConversion.swift`,
      ],
      "PBXSourcesBuildPhase",
      "Sources",
      target.uuid,
    );

    project.addBuildPhase(
      [
        "../node_modules/react-native-wireguard-turbo/ios/WireGuardKit.xcframework",
      ],
      "PBXFrameworksBuildPhase",
      "Frameworks",
      target.uuid,
    );

    const buildConfigs = project.pbxXCBuildConfigurationSection();
    Object.keys(buildConfigs).forEach((key) => {
      const cfg = buildConfigs[key];
      if (
        cfg.buildSettings &&
        cfg.buildSettings.PRODUCT_NAME === `"${EXTENSION_NAME}"`
      ) {
        cfg.buildSettings.SWIFT_VERSION = "5.5";
        cfg.buildSettings.IPHONEOS_DEPLOYMENT_TARGET = "15.0";
        cfg.buildSettings.INFOPLIST_FILE = `${EXTENSION_NAME}/${EXTENSION_NAME}-Info.plist`;
        cfg.buildSettings.CODE_SIGN_ENTITLEMENTS = `${EXTENSION_NAME}/${EXTENSION_NAME}.entitlements`;
        cfg.buildSettings.DEVELOPMENT_TEAM = c.ios?.appleTeamId || "";
        cfg.buildSettings.CODE_SIGN_STYLE = "Automatic";
        cfg.buildSettings.MARKETING_VERSION = "1.0.0";
        cfg.buildSettings.CURRENT_PROJECT_VERSION = "1";
      }
    });

    return c;
  });
}

function withMainAppSigning(config) {
  return withXcodeProject(config, (c) => {
    const project = c.modResults;

    const nativeTargets = project.pbxNativeTargetSection();
    const mainTarget = Object.values(nativeTargets).find(
      (t) => t && t.productType === '"com.apple.product-type.application"',
    );
    if (!mainTarget) return c;

    const configList =
      project.pbxXCConfigurationList()[mainTarget.buildConfigurationList];
    if (!configList) return c;

    const buildConfigs = project.pbxXCBuildConfigurationSection();
    configList.buildConfigurations.forEach(({ value: configId }) => {
      const cfg = buildConfigs[configId];
      if (cfg && cfg.buildSettings) {
        cfg.buildSettings.CODE_SIGN_STYLE = "Automatic";
        cfg.buildSettings.DEVELOPMENT_TEAM = c.ios?.appleTeamId || "";
      }
    });

    return c;
  });
}

module.exports = withWireGuard;

It would be very helpful if EXTENSION_NAME = "WireGuardNetworkExtension"; matched your Extension App ID (your_name.WireGuardNetworkExtension).

There is also a method, setSubscriptionExpiry(ms: number), and you can use it:

await WG.setSubscriptionExpiry(Date.now() + 1000 * 60)

If the user launches the VPN from VPN Settings, not from the application, it checks whether this date has expired before connecting and rejects the connection if it has.

📖 More details

I’ve written a full article describing the process of creating this library:
👉 Read on Medium

📜 License

GPL-2.0. Include your LICENSE file in the repo and npm package.

If you found any bug or just have something to tell about this library — feel free to contact me:

[email protected]