appium-ios-tuntap 0.2.5 → 0.4.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.4.0](https://github.com/appium/appium-ios-tuntap/compare/v0.3.0...v0.4.0) (2026-05-30)
2
+
3
+ ### Features
4
+
5
+ * implement Windows (WinTun) JavaScript platform layer ([#44](https://github.com/appium/appium-ios-tuntap/issues/44)) ([da2a9bb](https://github.com/appium/appium-ios-tuntap/commit/da2a9bba1d7226f67ce0f00c533df72164aac858))
6
+
7
+ ## [0.3.0](https://github.com/appium/appium-ios-tuntap/compare/v0.2.5...v0.3.0) (2026-05-22)
8
+
9
+ ### Features
10
+
11
+ * add Windows (WinTun) native backend ([#43](https://github.com/appium/appium-ios-tuntap/issues/43)) ([565b4c1](https://github.com/appium/appium-ios-tuntap/commit/565b4c1cfd2ddf4956ed32ade4f8208cd0d4f0f6)), closes [#ifdef](https://github.com/appium/appium-ios-tuntap/issues/ifdef)
12
+
1
13
  ## [0.2.5](https://github.com/appium/appium-ios-tuntap/compare/v0.2.4...v0.2.5) (2026-05-14)
2
14
 
3
15
  ### Code Refactoring
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TunTap Bridge
2
2
 
3
- A native TUN/TAP interface module for Node.js that works on both macOS and Linux, with enhanced error handling, signal management, and thread safety.
3
+ A native TUN/TAP interface module for Node.js that works on macOS, Linux, and Windows, with enhanced error handling, signal management, and thread safety.
4
4
 
5
5
  ## Description
6
6
 
@@ -8,7 +8,7 @@ This module provides a Node.js interface to TUN/TAP virtual network devices, all
8
8
 
9
9
  ## Features
10
10
 
11
- - **Cross-platform**: Works on macOS (utun) and Linux (TUN/TAP)
11
+ - **Cross-platform**: Works on macOS (utun), Linux (TUN/TAP), and Windows (WinTun)
12
12
  - **TypeScript support**: Full TypeScript definitions included
13
13
  - **Signal handling**: Graceful shutdown on SIGINT/SIGTERM
14
14
  - **Thread safety**: Safe to use from multiple Node.js worker threads
@@ -88,6 +88,14 @@ On Linux, the module requires:
88
88
  sudo pacman -S linux-headers
89
89
  ```
90
90
 
91
+ ### Windows
92
+
93
+ On Windows the module uses [WinTun](https://www.wintun.net/) (the same userspace TUN driver shipped with WireGuard). Requirements:
94
+
95
+ 1. **`wintun.dll`**: ships with the package. The official signed binaries for `amd64`, `arm64`, `x86`, and `arm` are bundled under `vendor/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.
96
+ 2. **Administrator privileges**: required to create the kernel adapter and configure addresses/routes via `netsh`. Launch your shell with **Run as administrator**.
97
+ 3. **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`.
98
+
91
99
  ## Usage
92
100
 
93
101
  ### Basic Usage
@@ -207,7 +215,7 @@ socket.connect(port, host, async () => {
207
215
 
208
216
  #### Properties
209
217
  - `name: string` - The device name (e.g., 'utun0', 'tun0')
210
- - `fd: number` - The file descriptor of the device
218
+ - `fd: number` - The native file descriptor on POSIX (macOS/Linux). Returns `-1` on Windows; Wintun does not expose a numeric file descriptor.
211
219
 
212
220
  ### Error Types
213
221
 
@@ -294,3 +302,12 @@ This ensures the signal handler works as intended.
294
302
  ## License
295
303
 
296
304
  Apache-2.0
305
+
306
+ ### Third-party software
307
+
308
+ This package redistributes the official signed **WinTun** DLLs (version 0.14.1) from [wintun.net](https://www.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/):
309
+
310
+ - `vendor/wintun/bin/{amd64,arm64,x86,arm}/wintun.dll`
311
+ - `vendor/wintun/LICENSE.txt` &mdash; the upstream WinTun license; required when redistributing the DLL
312
+
313
+ Maintainers can refresh the bundled binaries with `npm run refresh:wintun` after bumping `WINTUN_VERSION` in [scripts/fetch-wintun.mjs](scripts/fetch-wintun.mjs).
package/binding.gyp CHANGED
@@ -20,7 +20,7 @@
20
20
  "-Wno-unused-parameter",
21
21
  "-fPIC"
22
22
  ],
23
- "cflags_cc": [
23
+ "cflags_cc": [
24
24
  "-std=c++17",
25
25
  "-Wno-vla-extension",
26
26
  "-O3",
@@ -48,15 +48,15 @@
48
48
  ]
49
49
  },
50
50
  "msvs_settings": {
51
- "VCCLCompilerTool": {
51
+ "VCCLCompilerTool": {
52
52
  "ExceptionHandling": 1,
53
53
  "AdditionalOptions": [
54
- "/std:c++17",
54
+ "/std:c++20",
55
55
  "/O2"
56
56
  ]
57
57
  }
58
58
  },
59
- "defines": [
59
+ "defines": [
60
60
  "NAPI_CPP_EXCEPTIONS",
61
61
  "NAPI_VERSION=8"
62
62
  ],
@@ -89,6 +89,22 @@
89
89
  "-framework", "CoreFoundation"
90
90
  ]
91
91
  }
92
+ }],
93
+ ["OS=='win'", {
94
+ "sources": [
95
+ "src/native/handle.cc",
96
+ "src/native/wintun_loader.cc",
97
+ "src/native/tun_backend_windows.cc"
98
+ ],
99
+ "libraries": [
100
+ "iphlpapi.lib",
101
+ "ws2_32.lib"
102
+ ],
103
+ "defines": [
104
+ "_WIN32_WINNT=0x0A00",
105
+ "WIN32_LEAN_AND_MEAN",
106
+ "NOMINMAX"
107
+ ]
92
108
  }]
93
109
  ]
94
110
  }
@@ -1,6 +1,7 @@
1
1
  import { DarwinTunTapPlatform } from './darwin.js';
2
2
  import { LinuxTunTapPlatform } from './linux.js';
3
3
  import { UnsupportedTunTapPlatform } from './unsupported.js';
4
+ import { WindowsTunTapPlatform } from './windows.js';
4
5
  /** @internal Built-in {@link TunTapPlatform} for a Node `process.platform` value. */
5
6
  export function createTunTapPlatform(platform) {
6
7
  switch (platform) {
@@ -8,6 +9,8 @@ export function createTunTapPlatform(platform) {
8
9
  return new DarwinTunTapPlatform();
9
10
  case 'linux':
10
11
  return new LinuxTunTapPlatform();
12
+ case 'win32':
13
+ return new WindowsTunTapPlatform();
11
14
  default:
12
15
  return new UnsupportedTunTapPlatform(platform);
13
16
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Throws {@link TunTapPermissionError} unless the current process is an
3
+ * elevated (Administrator) Windows process. Mirrors `assertEffectiveRoot`
4
+ * from {@link ./require-root.ts} for POSIX.
5
+ */
6
+ export declare function assertAdminOnWindows(): Promise<void>;
7
+ /**
8
+ * Returns true when the current process is running with Administrator
9
+ * privileges on Windows. Implementation runs `net session` (which always
10
+ * exists, regardless of locale) and inspects the exit code.
11
+ *
12
+ * The result is memoized for the lifetime of the process; admin status cannot
13
+ * change between calls without restarting the shell.
14
+ */
15
+ export declare const isAdministrator: (() => Promise<boolean>) & {
16
+ cache: Map<unknown, Promise<boolean>>;
17
+ };
@@ -0,0 +1,32 @@
1
+ import { util } from '@appium/support';
2
+ import { TunTapPermissionError } from '../errors.js';
3
+ import { execFileAsync } from './exec.js';
4
+ /**
5
+ * Throws {@link TunTapPermissionError} unless the current process is an
6
+ * elevated (Administrator) Windows process. Mirrors `assertEffectiveRoot`
7
+ * from {@link ./require-root.ts} for POSIX.
8
+ */
9
+ export async function assertAdminOnWindows() {
10
+ if (await isAdministrator()) {
11
+ return;
12
+ }
13
+ throw new TunTapPermissionError('TUN interface configuration and routing require Administrator privileges on Windows. ' +
14
+ 'Re-launch the shell with "Run as administrator".');
15
+ }
16
+ /**
17
+ * Returns true when the current process is running with Administrator
18
+ * privileges on Windows. Implementation runs `net session` (which always
19
+ * exists, regardless of locale) and inspects the exit code.
20
+ *
21
+ * The result is memoized for the lifetime of the process; admin status cannot
22
+ * change between calls without restarting the shell.
23
+ */
24
+ export const isAdministrator = util.memoize(async function isAdministratorUncached() {
25
+ try {
26
+ await execFileAsync('net', ['session'], { windowsHide: true });
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ });
@@ -0,0 +1,13 @@
1
+ import type { TunTapInterfaceStats, TunTapPlatform } from './types.js';
2
+ /** Windows implementation backed by `netsh` for configuration/routing and
3
+ * PowerShell `Get-NetAdapterStatistics` for byte counters. */
4
+ export declare class WindowsTunTapPlatform implements TunTapPlatform {
5
+ /** @inheritdoc */
6
+ configure(interfaceName: string, address: string, mtu: number): Promise<void>;
7
+ /** @inheritdoc */
8
+ addRoute(interfaceName: string, destination: string): Promise<void>;
9
+ /** @inheritdoc */
10
+ removeRoute(interfaceName: string, destination: string): Promise<void>;
11
+ /** @inheritdoc */
12
+ getStats(interfaceName: string): Promise<TunTapInterfaceStats>;
13
+ }
@@ -0,0 +1,195 @@
1
+ import { TunTapError } from '../errors.js';
2
+ import { log } from '../logger.js';
3
+ import { assertAdminOnWindows } from './require-admin.js';
4
+ import { execFileAsync } from './exec.js';
5
+ /** Tightly-restricted character set for adapter names passed into PowerShell. */
6
+ const SAFE_NAME_RE = /^[A-Za-z0-9_\- ]+$/;
7
+ /** Phrases that indicate `netsh` could not find the requested route/address. */
8
+ const MISSING_TARGET_HINTS = [
9
+ 'element not found',
10
+ 'cannot find',
11
+ 'no matching',
12
+ 'does not exist',
13
+ 'not found',
14
+ ];
15
+ /** Windows implementation backed by `netsh` for configuration/routing and
16
+ * PowerShell `Get-NetAdapterStatistics` for byte counters. */
17
+ export class WindowsTunTapPlatform {
18
+ /** @inheritdoc */
19
+ async configure(interfaceName, address, mtu) {
20
+ await assertAdminOnWindows();
21
+ assertSafeAdapterName(interfaceName);
22
+ log.debug(`[win] configure: interface=${interfaceName} address=${address} mtu=${mtu}`);
23
+ await addIpv6Address(interfaceName, address);
24
+ await setIpv6Mtu(interfaceName, mtu);
25
+ }
26
+ /** @inheritdoc */
27
+ async addRoute(interfaceName, destination) {
28
+ await assertAdminOnWindows();
29
+ assertSafeAdapterName(interfaceName);
30
+ log.debug(`[win] addRoute: interface=${interfaceName} destination=${destination}`);
31
+ await addIpv6Route(interfaceName, destination);
32
+ // WinTun presents as an Ethernet adapter, so Windows requires Neighbor
33
+ // Discovery (NDP) before it will send packets through the interface.
34
+ // For /128 host routes we seed a static neighbor entry so NDP is bypassed
35
+ // and the first connection attempt is not silently dropped.
36
+ if (destination.endsWith('/128')) {
37
+ const address = destination.slice(0, -4);
38
+ await addStaticNeighbor(interfaceName, address);
39
+ }
40
+ }
41
+ /** @inheritdoc */
42
+ async removeRoute(interfaceName, destination) {
43
+ await assertAdminOnWindows();
44
+ assertSafeAdapterName(interfaceName);
45
+ await deleteIpv6Route(interfaceName, destination);
46
+ }
47
+ /** @inheritdoc */
48
+ async getStats(interfaceName) {
49
+ assertSafeAdapterName(interfaceName);
50
+ const script = `Get-NetAdapterStatistics -Name '${interfaceName}' ` +
51
+ '| Select-Object ReceivedBytes,SentBytes,ReceivedUnicastPackets,' +
52
+ 'SentUnicastPackets,ReceivedDiscardedPackets,OutboundDiscardedPackets ' +
53
+ '| ConvertTo-Json -Compress';
54
+ const { stdout } = await execFileAsync('powershell', [
55
+ '-NoProfile',
56
+ '-NonInteractive',
57
+ '-ExecutionPolicy',
58
+ 'Bypass',
59
+ '-Command',
60
+ script,
61
+ ]);
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(stdout);
65
+ }
66
+ catch {
67
+ throw new TunTapError(`Failed to parse Get-NetAdapterStatistics output: ${stdout.trim()}`);
68
+ }
69
+ const num = (key) => {
70
+ const value = parsed[key];
71
+ const n = typeof value === 'number' ? value : parseInt(String(value ?? ''), 10);
72
+ return Number.isFinite(n) ? n : 0;
73
+ };
74
+ return {
75
+ rxBytes: num('ReceivedBytes'),
76
+ rxPackets: num('ReceivedUnicastPackets'),
77
+ rxErrors: num('ReceivedDiscardedPackets'),
78
+ txBytes: num('SentBytes'),
79
+ txPackets: num('SentUnicastPackets'),
80
+ txErrors: num('OutboundDiscardedPackets'),
81
+ };
82
+ }
83
+ }
84
+ /** Validates an adapter name before embedding in a PowerShell expression. */
85
+ function assertSafeAdapterName(interfaceName) {
86
+ if (!SAFE_NAME_RE.test(interfaceName)) {
87
+ throw new TunTapError(`Refusing to use adapter name with unsupported characters: ${JSON.stringify(interfaceName)}`);
88
+ }
89
+ }
90
+ function isMissingTargetError(err) {
91
+ const message = String(err?.message ?? '').toLowerCase();
92
+ return MISSING_TARGET_HINTS.some((hint) => message.includes(hint));
93
+ }
94
+ async function addIpv6Address(interfaceName, address) {
95
+ try {
96
+ const r = await execFileAsync('netsh', [
97
+ 'interface',
98
+ 'ipv6',
99
+ 'add',
100
+ 'address',
101
+ `interface=${interfaceName}`,
102
+ `address=${address}/64`,
103
+ 'store=active',
104
+ ]);
105
+ log.debug(`[win] add address ok: ${r.stdout.trim() || '(no output)'}`);
106
+ }
107
+ catch (err) {
108
+ const message = err.message ?? '';
109
+ log.warn(`[win] add address err: ${message}`);
110
+ if (!/already exists|object already/i.test(message)) {
111
+ throw err;
112
+ }
113
+ log.warn(`Address ${address} may already be configured on ${interfaceName}`);
114
+ }
115
+ }
116
+ async function setIpv6Mtu(interfaceName, mtu) {
117
+ try {
118
+ const r = await execFileAsync('netsh', [
119
+ 'interface',
120
+ 'ipv6',
121
+ 'set',
122
+ 'subinterface',
123
+ interfaceName,
124
+ `mtu=${mtu}`,
125
+ 'store=active',
126
+ ]);
127
+ log.debug(`[win] set mtu ok: ${r.stdout.trim() || '(no output)'}`);
128
+ }
129
+ catch (err) {
130
+ log.warn(`[win] set mtu err: ${err.message ?? err}`);
131
+ throw err;
132
+ }
133
+ }
134
+ async function addIpv6Route(interfaceName, destination) {
135
+ try {
136
+ const r = await execFileAsync('netsh', [
137
+ 'interface',
138
+ 'ipv6',
139
+ 'add',
140
+ 'route',
141
+ destination,
142
+ interfaceName,
143
+ 'store=active',
144
+ ]);
145
+ log.debug(`[win] add route ok: ${r.stdout.trim() || '(no output)'}`);
146
+ }
147
+ catch (err) {
148
+ const message = err.message ?? '';
149
+ log.warn(`[win] add route err: ${message}`);
150
+ if (/already exists|object already/i.test(message)) {
151
+ log.debug(`Route to ${destination} already exists`);
152
+ return;
153
+ }
154
+ throw err;
155
+ }
156
+ }
157
+ async function deleteIpv6Route(interfaceName, destination) {
158
+ try {
159
+ await execFileAsync('netsh', [
160
+ 'interface',
161
+ 'ipv6',
162
+ 'delete',
163
+ 'route',
164
+ destination,
165
+ interfaceName,
166
+ 'store=active',
167
+ ]);
168
+ }
169
+ catch (err) {
170
+ if (isMissingTargetError(err)) {
171
+ return;
172
+ }
173
+ throw err;
174
+ }
175
+ }
176
+ async function addStaticNeighbor(interfaceName, address) {
177
+ log.debug(`[win] addStaticNeighbor: interface=${interfaceName} address=${address}`);
178
+ try {
179
+ const r = await execFileAsync('netsh', [
180
+ 'interface',
181
+ 'ipv6',
182
+ 'add',
183
+ 'neighbor',
184
+ interfaceName,
185
+ address,
186
+ '00-00-00-00-00-01',
187
+ 'store=active',
188
+ ]);
189
+ log.debug(`[win] add neighbor ok: ${r.stdout.trim() || '(no output)'}`);
190
+ }
191
+ catch (err) {
192
+ const msg = err.message ?? String(err);
193
+ log.warn(`[win] add neighbor err: ${msg}`);
194
+ }
195
+ }
package/lib/tunnel.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { TunTap } from './TunTap.js';
2
2
  import { EventEmitter } from 'node:events';
3
- import { Socket } from 'node:net';
3
+ import type { Socket } from 'node:net';
4
4
  import { Buffer } from 'node:buffer';
5
5
  export interface PacketData {
6
6
  protocol: 'TCP' | 'UDP';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-tuntap",
3
- "version": "0.2.5",
3
+ "version": "0.4.0",
4
4
  "description": "Native TUN/TAP interface module for Node.js",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -15,16 +15,19 @@
15
15
  "format": "prettier -w ./src ./test",
16
16
  "format:check": "prettier --check ./src ./test",
17
17
  "install": "node-gyp-build",
18
+ "refresh:wintun": "node scripts/fetch-wintun.mjs",
18
19
  "prepare": "npm run build",
19
20
  "test": "npm run test:integration && npm run test:unit",
20
- "test:unit": "mocha 'test/unit/**/*.spec.mjs' --exit --timeout 2m",
21
- "test:integration": "mocha 'test/integration/**/*.spec.mjs' --exit --timeout 2m"
21
+ "test:unit": "mocha \"test/unit/**/*.spec.mjs\" --exit --timeout 2m",
22
+ "test:integration": "mocha \"test/integration/**/*.spec.mjs\" --exit --timeout 2m"
22
23
  },
23
24
  "files": [
24
25
  "src/tuntap.cc",
25
26
  "src/native",
26
27
  "lib",
27
28
  "prebuilds",
29
+ "scripts",
30
+ "vendor",
28
31
  "binding.gyp",
29
32
  "package.json",
30
33
  "README.md",
@@ -56,6 +59,7 @@
56
59
  "@semantic-release/changelog": "^6.0.3",
57
60
  "@semantic-release/git": "^10.0.1",
58
61
  "@types/node": "^25.0.1",
62
+ "commander": "^14.0.3",
59
63
  "conventional-changelog-conventionalcommits": "^9.0.0",
60
64
  "mocha": "^11.7.5",
61
65
  "prebuildify": "^6.0.1",
@@ -0,0 +1,70 @@
1
+ // Maintainer-only helper that refreshes the bundled WinTun binaries committed
2
+ // under vendor/wintun/. The npm `install` hook does NOT invoke this script —
3
+ // the package ships with the official signed DLLs already checked in. Run
4
+ // `npm run refresh:wintun -- --version <semver>` to pull a different release.
5
+
6
+ import {fs, logger, net, tempDir, zip} from '@appium/support';
7
+ import {Command} from 'commander';
8
+ import {join, dirname} from 'node:path';
9
+ import {fileURLToPath} from 'node:url';
10
+
11
+ const log = logger.getLogger('refresh-wintun');
12
+
13
+ const DEFAULT_WINTUN_VERSION = '0.14.1';
14
+ const BUNDLED_ARCHES = ['amd64', 'arm64', 'x86', 'arm'];
15
+
16
+ const rootDir = join(dirname(fileURLToPath(import.meta.url)), '..');
17
+ const vendorDir = join(rootDir, 'vendor', 'wintun');
18
+
19
+ async function deployDll(arch, extractDir) {
20
+ const destDir = join(vendorDir, 'bin', arch);
21
+ await fs.mkdir(destDir, {recursive: true});
22
+ const src = join(extractDir, 'wintun', 'bin', arch, 'wintun.dll');
23
+ const dest = join(destDir, 'wintun.dll');
24
+ await fs.copyFile(src, dest);
25
+ log.info(`wintun.dll (${arch}) -> ${dest}`);
26
+ }
27
+
28
+ async function deployLicense(extractDir) {
29
+ const src = join(extractDir, 'wintun', 'LICENSE.txt');
30
+ const dest = join(vendorDir, 'LICENSE.txt');
31
+ await fs.copyFile(src, dest);
32
+ log.info(`LICENSE.txt -> ${dest}`);
33
+ }
34
+
35
+ async function refreshWintun(version) {
36
+ const url = `https://www.wintun.net/builds/wintun-${version}.zip`;
37
+ const tmpDir = await tempDir.openDir();
38
+ try {
39
+ const zipPath = join(tmpDir, 'wintun.zip');
40
+ const extractDir = join(tmpDir, 'out');
41
+ await fs.mkdir(extractDir, {recursive: true});
42
+
43
+ log.info(`Downloading WinTun ${version}...`);
44
+ await net.downloadFile(url, zipPath);
45
+ await zip.extractAllTo(zipPath, extractDir);
46
+
47
+ await fs.mkdir(vendorDir, {recursive: true});
48
+ await Promise.all([
49
+ ...BUNDLED_ARCHES.map((arch) => deployDll(arch, extractDir)),
50
+ deployLicense(extractDir),
51
+ ]);
52
+ } finally {
53
+ await fs.rimraf(tmpDir);
54
+ }
55
+ }
56
+
57
+ const program = new Command();
58
+ program
59
+ .name('refresh-wintun')
60
+ .description('Refresh the bundled WinTun binaries under vendor/wintun/')
61
+ .option(
62
+ '-v, --version <semver>',
63
+ 'WinTun release version to download',
64
+ DEFAULT_WINTUN_VERSION,
65
+ )
66
+ .action(async (options) => {
67
+ await refreshWintun(options.version);
68
+ });
69
+
70
+ await program.parseAsync(process.argv);
@@ -0,0 +1,59 @@
1
+ #ifdef _WIN32
2
+
3
+ #include "handle.h"
4
+
5
+ namespace {
6
+
7
+ bool IsRealHandle(HANDLE handle) {
8
+ return handle != nullptr && handle != INVALID_HANDLE_VALUE;
9
+ }
10
+
11
+ } // namespace
12
+
13
+ Handle::Handle() : handle_(nullptr) {}
14
+
15
+ Handle::Handle(HANDLE handle) : handle_(handle) {}
16
+
17
+ Handle::~Handle() {
18
+ if (IsRealHandle(handle_)) {
19
+ ::CloseHandle(handle_);
20
+ }
21
+ }
22
+
23
+ Handle::Handle(Handle&& other) noexcept : handle_(other.handle_) {
24
+ other.handle_ = nullptr;
25
+ }
26
+
27
+ Handle& Handle::operator=(Handle&& other) noexcept {
28
+ if (this != &other) {
29
+ if (IsRealHandle(handle_)) {
30
+ ::CloseHandle(handle_);
31
+ }
32
+ handle_ = other.handle_;
33
+ other.handle_ = nullptr;
34
+ }
35
+ return *this;
36
+ }
37
+
38
+ HANDLE Handle::get() const {
39
+ return handle_;
40
+ }
41
+
42
+ HANDLE Handle::release() {
43
+ HANDLE temp = handle_;
44
+ handle_ = nullptr;
45
+ return temp;
46
+ }
47
+
48
+ bool Handle::is_valid() const {
49
+ return IsRealHandle(handle_);
50
+ }
51
+
52
+ void Handle::reset(HANDLE handle) {
53
+ if (IsRealHandle(handle_)) {
54
+ ::CloseHandle(handle_);
55
+ }
56
+ handle_ = handle;
57
+ }
58
+
59
+ #endif
@@ -0,0 +1,30 @@
1
+ #pragma once
2
+
3
+ #ifdef _WIN32
4
+
5
+ #include <windows.h>
6
+
7
+ // RAII wrapper for a Win32 `HANDLE`. Mirrors `FileDescriptor` so backends can
8
+ // rely on the same lifetime semantics regardless of OS.
9
+ class Handle {
10
+ public:
11
+ Handle();
12
+ explicit Handle(HANDLE handle);
13
+ ~Handle();
14
+
15
+ Handle(const Handle&) = delete;
16
+ Handle& operator=(const Handle&) = delete;
17
+
18
+ Handle(Handle&& other) noexcept;
19
+ Handle& operator=(Handle&& other) noexcept;
20
+
21
+ HANDLE get() const;
22
+ HANDLE release();
23
+ bool is_valid() const;
24
+ void reset(HANDLE handle = nullptr);
25
+
26
+ private:
27
+ HANDLE handle_;
28
+ };
29
+
30
+ #endif
@@ -1,7 +1,7 @@
1
1
  #pragma once
2
2
 
3
- #if !defined(__linux__) && !defined(__APPLE__)
4
- #error "appium-ios-tuntap native addon supports only Linux and macOS"
3
+ #if !defined(__linux__) && !defined(__APPLE__) && !defined(_WIN32)
4
+ #error "appium-ios-tuntap native addon supports only Linux, macOS, and Windows"
5
5
  #endif
6
6
 
7
7
  #include <cstddef>
@@ -27,9 +27,25 @@ enum class ReadPacketStatus {
27
27
  Error,
28
28
  };
29
29
 
30
+ /**
31
+ * Backend abstraction that hides OS-specific TUN device handling from the
32
+ * N-API surface.
33
+ *
34
+ * Each backend owns:
35
+ * - its native handle (POSIX file descriptor or Win32 `HANDLE`)
36
+ * - the receive-loop primitive it needs (libuv `uv_poll_t` on POSIX, a
37
+ * dedicated worker thread plus a Win32 event on Windows)
38
+ */
30
39
  class TunPlatformBackend {
31
40
  public:
41
+ // Invoked once per packet read by the receive loop. Always called on a
42
+ // background thread (libuv loop thread on POSIX, worker thread on Windows);
43
+ // the caller in `tuntap.cc` is responsible for marshalling onto the JS
44
+ // thread via `Napi::ThreadSafeFunction`.
32
45
  using PacketCallback = std::function<void(std::vector<uint8_t>)>;
46
+
47
+ // Invoked at most once when the receive loop encounters a fatal error and
48
+ // stops. The receive loop must not deliver any further packets afterwards.
33
49
  using ErrorCallback = std::function<void(const std::string&)>;
34
50
 
35
51
  virtual ~TunPlatformBackend() = default;
@@ -47,6 +63,8 @@ public:
47
63
  size_t length,
48
64
  std::string& error) = 0;
49
65
 
66
+ // Begin asynchronous packet delivery. `loop` is supplied by Node-API and is
67
+ // used by POSIX backends for `uv_poll_init`; Windows ignores it.
50
68
  virtual bool StartReceiveLoop(uv_loop_t* loop,
51
69
  size_t buffer_size,
52
70
  PacketCallback on_packet,
@@ -54,6 +72,8 @@ public:
54
72
  std::string& error) = 0;
55
73
  virtual void StopReceiveLoop() = 0;
56
74
 
75
+ // Returns the underlying POSIX file descriptor when one exists. Backends
76
+ // without a numeric fd (e.g. Wintun on Windows) return `-1`.
57
77
  virtual int GetNativeFd() const { return -1; }
58
78
  };
59
79