This repository has moved. Development now continues in the appium-ios monorepo.
A native TUN/TAP interface module for Node.js that works on macOS, Linux, and Windows, with enhanced error handling and thread safety.
This module provides a Node.js interface to TUN/TAP virtual network devices, allowing you to create and manage network tunnels from JavaScript/TypeScript. It's useful for VPNs, network tunneling, and other network-related applications.
- Cross-platform: Works on macOS (utun), Linux (TUN/TAP), and Windows (WinTun)
- TypeScript support: Full TypeScript definitions included
- Thread safety: Safe to use from multiple Node.js worker threads
- Resource management: Devices are closed on normal process exit; the kernel releases the fd otherwise
- Enhanced error handling: Custom error types for better debugging
- Input validation: Validates IPv6 addresses, MTU ranges, and buffer sizes
- Performance optimized: Built with C++17 and compiler optimizations
- Network statistics: Get interface statistics (RX/TX bytes, packets, errors)
npm install appium-ios-tuntapOn macOS, the module uses the built-in utun interfaces. No additional setup is required, but you'll need administrator privileges to create and configure the interfaces.
On Linux, the module requires:
-
TUN/TAP Kernel Module: The TUN/TAP kernel module must be loaded.
# Check if the module is loaded lsmod | grep tun # If not loaded, load it sudo modprobe tun # To load it automatically at boot echo "tun" | sudo tee -a /etc/modules
-
Permissions: The user running the application needs access to
/dev/net/tun.# Option 1: Run your application with sudo sudo node your-app.js # Option 2: Add your user to the 'tun' group (if it exists) sudo usermod -a -G tun your-username # Option 3: Create a udev rule to set permissions echo 'KERNEL=="tun", GROUP="your-username", MODE="0660"' | sudo tee /etc/udev/rules.d/99-tuntap.rules sudo udevadm control --reload-rules sudo udevadm trigger
-
iproute2 Package: The
ipcommand is required for configuring interfaces.# Debian/Ubuntu sudo apt install iproute2 # CentOS/RHEL sudo yum install iproute # Arch Linux sudo pacman -S iproute2
-
Development Headers: If you're building from source, you'll need the Linux kernel headers.
# Debian/Ubuntu sudo apt install linux-headers-$(uname -r) # CentOS/RHEL sudo yum install kernel-devel # Arch Linux sudo pacman -S linux-headers
On Windows the module uses WinTun (the same userspace TUN driver shipped with WireGuard). Requirements:
- Apple Mobile Device drivers: required for USB communication with iOS devices. Install them through Apple's standalone iTunes package for Windows (the non-Microsoft Store installer includes the Apple Mobile Device Support drivers).
wintun.dll: ships with the package. The official signed binaries foramd64,arm64,x86, andarmare bundled undervendor/wintun/bin/<arch>/wintun.dll; the addon discovers the right one automatically based on its own compile-time architecture. No download or copy step is required.- Administrator privileges: required to create the kernel adapter and configure addresses/routes via
netsh. Launch your shell with Run as administrator. - OpenSSL for source builds: release prebuilds statically link OpenSSL. If compiling from source on Windows, install static OpenSSL (for example
vcpkg install openssl:x64-windows-static) and setOPENSSL_ROOT_DIRto the installed triplet directory. - Build toolchain (only if compiling from source): Visual Studio Build Tools 2022 with the C++ workload, the Windows 10 SDK, and Python 3.x on
PATH.
import { TunTap } from 'appium-ios-tuntap';
// Create a TUN device
const tun = new TunTap();
// Open the device
if (tun.open()) {
console.log(`Opened TUN device: ${tun.name}`);
// Configure the device with an IPv6 address and MTU
await tun.configure('fd00::1', 1500);
// Add a route
await tun.addRoute('fd00::/64');
// Read from the device
const data = tun.read(4096);
if (data.length > 0) {
console.log(`Read ${data.length} bytes`);
}
// Write to the device
const buffer = Buffer.from([/* your packet data */]);
const bytesWritten = tun.write(buffer);
console.log(`Wrote ${bytesWritten} bytes`);
// Get interface statistics
const stats = await tun.getStats();
console.log('RX bytes:', stats.rxBytes);
console.log('TX bytes:', stats.txBytes);
// Close the device when done
tun.close();
}import { TunTap, TunTapError, TunTapPermissionError, TunTapDeviceError } from 'appium-ios-tuntap';
try {
const tun = new TunTap();
tun.open();
await tun.configure('fe80::1', 1500);
// ... use the device ...
tun.close();
} catch (err) {
if (err instanceof TunTapPermissionError) {
console.error('Permission denied. Please run with sudo.');
} else if (err instanceof TunTapDeviceError) {
console.error('Device error:', err.message);
} else if (err instanceof TunTapError) {
console.error('TUN/TAP error:', err.message);
} else {
console.error('Unexpected error:', err);
}
}import { connectToTunnelLockdown } from 'appium-ios-tuntap';
// Plain TCP socket to CoreDeviceProxy + lockdown pair-record PEM (do not use Node TLS first)
const { socket, cert, key } = await startCoreDeviceProxyTcp(...);
const tunnel = await connectToTunnelLockdown(socket, { cert, key });
console.log('Tunnel established:', tunnel.Address);
await tunnel.closer();connectToTunnelLockdown() and connectToTunnelPsk() are supported on macOS, Linux, and Windows. On Windows, run from an elevated shell so WinTun adapter creation and netsh route configuration can succeed.
new TunTap(name?: string, platform?: NodeJS.Platform)- Create a new TUN/TAP device instance.platformselects the OS backend used for addressing and routing, and defaults toprocess.platform.
open(): boolean- Open the TUN deviceclose(): boolean- Close the TUN deviceread(maxSize?: number): Buffer- Read data from the device (default: 4096 bytes). Non-blocking: returns an empty buffer when no packet is queued. Unsupported while aTunnelForwarderis active on the device — concurrent reads race with the forwarder (lost packets or crashes).write(data: Buffer): number- Write data to the device. Unsupported while aTunnelForwarderis active — concurrent writes race with the forwarder.configure(address: string, mtu?: number): Promise<void>- Configure IPv6 address and MTUaddRoute(destination: string): Promise<void>- Add a route to the deviceremoveRoute(destination: string): Promise<void>- Remove a route from the devicegetStats(): Promise<Stats>- Get interface statistics (rxBytes,txBytes,rxPackets,txPackets,rxErrors,txErrors)
name: string- The device name (e.g., 'utun0', 'tun0')fd: number- The native file descriptor on POSIX (macOS/Linux). Returns-1on Windows; Wintun does not expose a numeric file descriptor.isOpen: boolean-open()has succeeded andclose()has not runisClosed: boolean-close()has been called; the device cannot be reopened
TunTapError- Base error class for all TUN/TAP errorsTunTapPermissionError- Thrown when there are permission issuesTunTapDeviceError- Thrown when the device is not available or cannot be opened
The module does not install SIGINT/SIGTERM handlers — a library should not take over signals from the
application that embeds it. It registers a process.once('exit') hook that closes any open device on a normal
exit.
exit handlers do not run when a process is terminated by a signal, so on Ctrl+C the device is not closed by
this module; the kernel releases the file descriptor and tears down the interface when the process dies. If you
need cleanup to run on a signal, install your own handler and call close():
process.once('SIGINT', () => {
tun.close();
process.exit(130);
});MTU to request from the device in the CDTunnel handshake. Defaults to 1280 (the IPv6 minimum). Accepts integers in 1280–65000; out-of-range values are clamped and invalid values fall back to the default with a warning. The device decides the granted value, which is what actually gets applied — the tunnel log shows requested X, granted Y.
Note: a device may grant a value it cannot reliably carry (observed: a grant of 16000 where packets above ~8 KB were silently dropped, breaking AFC). Validate any non-default value with the AFC tests below before adopting it.
Any change touching the forwarding path (native forwarder, backends, tunnel MTU, TLS) must be verified against a real device with the AFC tests in appium-ios-remotexpc. With a live tunnel up (npm run tunnel-creation):
# Throughput: pushes a 10 MiB file over AFC, reports MiB/s
npm run test:afc-push-perf
# Stability: 20 rounds of 10 MiB push + pull + sha256 verify over one AFC session
AFC_STABILITY_ITERATIONS=20 npm run test:afc-tunnel-stabilityBoth must pass, and push throughput should be flat or better versus a run on the unchanged build. Compare only runs on the same link: USB and Wi-Fi tunnels differ by an order of magnitude (check the Connection: type logged by tunnel-creation).
-
"TUN/TAP device not available": The TUN/TAP kernel module is not loaded.
- Solution:
sudo modprobe tun
- Solution:
-
"Permission denied" when opening /dev/net/tun: The user doesn't have sufficient permissions.
- Solution: Run with sudo or add your user to the 'tun' group.
-
"Permission denied" when configuring the interface: The user doesn't have sudo privileges.
- Solution: Run the application with sudo or configure sudo to allow the specific commands without a password.
-
"Command not found" when configuring the interface: The
ipcommand is not available.- Solution: Install the iproute2 package.
-
"Failed to create control socket": The application doesn't have sufficient permissions.
- Solution: Run with sudo.
-
"Could not find an available utun device": All utun devices are in use.
- Solution: Close other applications that might be using utun devices.
Set APPIUM_TUNTAP_DEBUG=1 to enable tunnel debug logging. The TypeScript layer logs through
@appium/support; the native forwarder writes [fwd] #N event key=value lines to stderr.
APPIUM_TUNTAP_DEBUG=1 node your-app.jsMost tests for this module require root privileges (sudo) to create and manage TUN/TAP devices.
- If you run the tests without root, privileged tests will be automatically skipped.
- Some tests may interact with system networking; use caution on production systems.
- The test suite is designed to clean up after itself, but always verify no stray TUN/TAP devices remain after running.
From the project root, run:
sudo npm run test:unitOn Windows, use an elevated PowerShell:
npm run build:addon
npm run test:unitOr, to run all tests:
sudo npm testIf you are not running as root, you will see a message that tests are skipped.
Windows tunnel-forwarder end-to-end testing requires a real device tunnel source. From an elevated PowerShell, build the addon, run the unit tests, then establish a CoreDevice/RemoteXPC tunnel and verify the forwarded tunnel remains stable under AFC or similar traffic.
- Build the project, then run the compiled CLI utility:
npm run build sudo node lib/test/test-tuntap.js
- While it is running, press
Ctrl+C. - Confirm the process exits and the interface is gone (
ifconfigon macOS,ip linkon Linux).
The interface disappears because the kernel closes the file descriptor when the process dies, not because the module handled the signal — see Signal Handling.
Apache-2.0
This package redistributes the official signed WinTun DLLs (version 0.14.1) from wintun.net under the bundled-binary license shipped by the WinTun project. The unmodified binaries and the upstream license live under vendor/wintun/:
vendor/wintun/bin/{amd64,arm64,x86,arm}/wintun.dllvendor/wintun/LICENSE.txt— the upstream WinTun license; required when redistributing the DLL
Maintainers can refresh the bundled binaries with npm run refresh:wintun after bumping WINTUN_VERSION in scripts/fetch-wintun.mjs.