appium-ios-tuntap 0.2.4 → 0.3.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.3.0](https://github.com/appium/appium-ios-tuntap/compare/v0.2.5...v0.3.0) (2026-05-22)
2
+
3
+ ### Features
4
+
5
+ * 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)
6
+
7
+ ## [0.2.5](https://github.com/appium/appium-ios-tuntap/compare/v0.2.4...v0.2.5) (2026-05-14)
8
+
9
+ ### Code Refactoring
10
+
11
+ * backends own fd, polling, and lifecycle ([#42](https://github.com/appium/appium-ios-tuntap/issues/42)) ([f089944](https://github.com/appium/appium-ios-tuntap/commit/f08994477cb9909b597386d0893abbf9b6a3c137))
12
+
1
13
  ## [0.2.4](https://github.com/appium/appium-ios-tuntap/compare/v0.2.3...v0.2.4) (2026-05-13)
2
14
 
3
15
  ### Code Refactoring
package/binding.gyp CHANGED
@@ -3,8 +3,7 @@
3
3
  {
4
4
  "target_name": "tuntap",
5
5
  "sources": [
6
- "src/tuntap.cc",
7
- "src/native/file_descriptor.cc"
6
+ "src/tuntap.cc"
8
7
  ],
9
8
  "include_dirs": [
10
9
  "<!@(node -p \"require('node-addon-api').include\")"
@@ -21,7 +20,7 @@
21
20
  "-Wno-unused-parameter",
22
21
  "-fPIC"
23
22
  ],
24
- "cflags_cc": [
23
+ "cflags_cc": [
25
24
  "-std=c++17",
26
25
  "-Wno-vla-extension",
27
26
  "-O3",
@@ -49,7 +48,7 @@
49
48
  ]
50
49
  },
51
50
  "msvs_settings": {
52
- "VCCLCompilerTool": {
51
+ "VCCLCompilerTool": {
53
52
  "ExceptionHandling": 1,
54
53
  "AdditionalOptions": [
55
54
  "/std:c++17",
@@ -57,13 +56,15 @@
57
56
  ]
58
57
  }
59
58
  },
60
- "defines": [
59
+ "defines": [
61
60
  "NAPI_CPP_EXCEPTIONS",
62
61
  "NAPI_VERSION=8"
63
62
  ],
64
63
  "conditions": [
65
64
  ["OS=='linux'", {
66
65
  "sources": [
66
+ "src/native/file_descriptor.cc",
67
+ "src/native/posix_uv_poll_loop.cc",
67
68
  "src/native/tun_backend_linux.cc"
68
69
  ],
69
70
  "cflags": [
@@ -78,6 +79,8 @@
78
79
  }],
79
80
  ["OS=='mac'", {
80
81
  "sources": [
82
+ "src/native/file_descriptor.cc",
83
+ "src/native/posix_uv_poll_loop.cc",
81
84
  "src/native/tun_backend_darwin.cc"
82
85
  ],
83
86
  "xcode_settings": {
@@ -86,6 +89,22 @@
86
89
  "-framework", "CoreFoundation"
87
90
  ]
88
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
+ ]
89
108
  }]
90
109
  ]
91
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-tuntap",
3
- "version": "0.2.4",
3
+ "version": "0.3.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",
@@ -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
@@ -0,0 +1,58 @@
1
+ #pragma once
2
+
3
+ #if defined(__APPLE__) || defined(__linux__)
4
+
5
+ #include <string>
6
+ #include <utility>
7
+ #include <vector>
8
+
9
+ #include "file_descriptor.h"
10
+ #include "posix_uv_poll_loop.h"
11
+ #include "tun_backend.h"
12
+
13
+ // Shared base class for POSIX TUN backends (Darwin, Linux). Owns the file
14
+ // descriptor, the assigned interface name, and the libuv poll loop. Concrete
15
+ // subclasses implement only the platform-specific OpenDevice, ReadPacket, and
16
+ // WritePacket.
17
+ class PosixTunBackend : public TunPlatformBackend {
18
+ public:
19
+ void CloseDevice() override {
20
+ poll_loop_.Stop();
21
+ fd_.reset();
22
+ interface_name_.clear();
23
+ }
24
+
25
+ bool IsOpen() const override { return fd_.is_valid(); }
26
+
27
+ bool StartReceiveLoop(uv_loop_t* loop,
28
+ size_t buffer_size,
29
+ PacketCallback on_packet,
30
+ ErrorCallback on_error,
31
+ std::string& error) override {
32
+ if (!fd_.is_valid()) {
33
+ error = "Device not open";
34
+ return false;
35
+ }
36
+ return poll_loop_.Start(
37
+ loop,
38
+ fd_.get(),
39
+ buffer_size,
40
+ [this](size_t size, std::vector<uint8_t>& out, std::string& err) {
41
+ return ReadPacket(size, out, err);
42
+ },
43
+ std::move(on_packet),
44
+ std::move(on_error),
45
+ error);
46
+ }
47
+
48
+ void StopReceiveLoop() override { poll_loop_.Stop(); }
49
+
50
+ int GetNativeFd() const override { return fd_.get(); }
51
+
52
+ protected:
53
+ FileDescriptor fd_;
54
+ std::string interface_name_;
55
+ PosixUvPollLoop poll_loop_;
56
+ };
57
+
58
+ #endif
@@ -0,0 +1,137 @@
1
+ #if defined(__APPLE__) || defined(__linux__)
2
+
3
+ #include "posix_uv_poll_loop.h"
4
+
5
+ #include <cstdio>
6
+ #include <errno.h>
7
+ #include <fcntl.h>
8
+ #include <string.h>
9
+ #include <utility>
10
+
11
+ PosixUvPollLoop::~PosixUvPollLoop() {
12
+ Stop();
13
+ }
14
+
15
+ bool PosixUvPollLoop::Start(uv_loop_t* loop,
16
+ int fd,
17
+ size_t buffer_size,
18
+ ReadFn read_fn,
19
+ TunPlatformBackend::PacketCallback on_packet,
20
+ TunPlatformBackend::ErrorCallback on_error,
21
+ std::string& error) {
22
+ if (handle_) {
23
+ error = "Receive loop already started";
24
+ return false;
25
+ }
26
+ if (!loop || fd < 0 || buffer_size == 0) {
27
+ error = "Invalid receive-loop parameters";
28
+ return false;
29
+ }
30
+
31
+ auto state = std::make_unique<State>();
32
+ state->buffer_size = buffer_size;
33
+ state->read_fn = std::move(read_fn);
34
+ state->on_packet = std::move(on_packet);
35
+ state->on_error = std::move(on_error);
36
+ state->owner = this;
37
+
38
+ auto handle = std::make_unique<uv_poll_t>();
39
+ if (uv_poll_init(loop, handle.get(), fd) != 0) {
40
+ error = "Failed to initialize poll handle";
41
+ return false;
42
+ }
43
+
44
+ handle->data = state.get();
45
+ if (uv_poll_start(handle.get(), UV_READABLE, &PosixUvPollLoop::OnPoll) != 0) {
46
+ uv_close(reinterpret_cast<uv_handle_t*>(handle.release()),
47
+ [](uv_handle_t* h) { delete reinterpret_cast<uv_poll_t*>(h); });
48
+ error = "Failed to start polling";
49
+ return false;
50
+ }
51
+
52
+ state_ = std::move(state);
53
+ handle_ = handle.release();
54
+ return true;
55
+ }
56
+
57
+ void PosixUvPollLoop::Stop() {
58
+ if (!handle_) {
59
+ return;
60
+ }
61
+
62
+ uv_poll_stop(handle_);
63
+ handle_->data = nullptr;
64
+ uv_close(reinterpret_cast<uv_handle_t*>(handle_),
65
+ &PosixUvPollLoop::OnHandleClosed);
66
+ handle_ = nullptr;
67
+ state_.reset();
68
+ }
69
+
70
+ void PosixUvPollLoop::OnPoll(uv_poll_t* handle, int status, int events) {
71
+ auto* state = static_cast<State*>(handle->data);
72
+ if (!state) {
73
+ return;
74
+ }
75
+
76
+ // Tear down the loop and notify the owner of a terminal condition. The
77
+ // callback is copied locally first because Stop() destroys `state` (and the
78
+ // std::function it owns) synchronously.
79
+ auto handle_terminal = [&](const std::string& msg) {
80
+ auto cb = state->on_error;
81
+ PosixUvPollLoop* owner = state->owner;
82
+ if (owner) {
83
+ owner->Stop();
84
+ }
85
+ if (cb) {
86
+ cb(msg);
87
+ }
88
+ };
89
+
90
+ if (status < 0) {
91
+ handle_terminal(std::string("Poll error: ") + uv_strerror(status));
92
+ return;
93
+ }
94
+
95
+ if (!(events & UV_READABLE) || !state->read_fn) {
96
+ return;
97
+ }
98
+
99
+ std::vector<uint8_t> packet;
100
+ std::string error;
101
+ ReadPacketStatus rs = state->read_fn(state->buffer_size, packet, error);
102
+
103
+ switch (rs) {
104
+ case ReadPacketStatus::Data:
105
+ if (state->on_packet) {
106
+ state->on_packet(std::move(packet));
107
+ }
108
+ return;
109
+ case ReadPacketStatus::NoData:
110
+ return;
111
+ case ReadPacketStatus::Closed:
112
+ handle_terminal("Device closed");
113
+ return;
114
+ case ReadPacketStatus::Error:
115
+ handle_terminal(error);
116
+ return;
117
+ }
118
+ }
119
+
120
+ void PosixUvPollLoop::OnHandleClosed(uv_handle_t* handle) {
121
+ delete reinterpret_cast<uv_poll_t*>(handle);
122
+ }
123
+
124
+ bool SetNonBlocking(int fd, std::string& error) {
125
+ int flags = fcntl(fd, F_GETFL, 0);
126
+ if (flags < 0) {
127
+ error = std::string("Failed to get file descriptor flags: ") + strerror(errno);
128
+ return false;
129
+ }
130
+ if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
131
+ error = std::string("Failed to set non-blocking mode: ") + strerror(errno);
132
+ return false;
133
+ }
134
+ return true;
135
+ }
136
+
137
+ #endif
@@ -0,0 +1,55 @@
1
+ #pragma once
2
+
3
+ #if defined(__APPLE__) || defined(__linux__)
4
+
5
+ #include <cstddef>
6
+ #include <functional>
7
+ #include <memory>
8
+ #include <string>
9
+ #include <vector>
10
+
11
+ #include <uv.h>
12
+
13
+ #include "tun_backend.h"
14
+
15
+ class PosixUvPollLoop {
16
+ public:
17
+ using ReadFn = std::function<ReadPacketStatus(size_t,
18
+ std::vector<uint8_t>&,
19
+ std::string&)>;
20
+
21
+ PosixUvPollLoop() = default;
22
+ ~PosixUvPollLoop();
23
+
24
+ PosixUvPollLoop(const PosixUvPollLoop&) = delete;
25
+ PosixUvPollLoop& operator=(const PosixUvPollLoop&) = delete;
26
+
27
+ bool Start(uv_loop_t* loop,
28
+ int fd,
29
+ size_t buffer_size,
30
+ ReadFn read_fn,
31
+ TunPlatformBackend::PacketCallback on_packet,
32
+ TunPlatformBackend::ErrorCallback on_error,
33
+ std::string& error);
34
+
35
+ void Stop();
36
+
37
+ private:
38
+ struct State {
39
+ size_t buffer_size = 0;
40
+ ReadFn read_fn;
41
+ TunPlatformBackend::PacketCallback on_packet;
42
+ TunPlatformBackend::ErrorCallback on_error;
43
+ PosixUvPollLoop* owner = nullptr;
44
+ };
45
+
46
+ static void OnPoll(uv_poll_t* handle, int status, int events);
47
+ static void OnHandleClosed(uv_handle_t* handle);
48
+
49
+ uv_poll_t* handle_ = nullptr;
50
+ std::unique_ptr<State> state_;
51
+ };
52
+
53
+ bool SetNonBlocking(int fd, std::string& error);
54
+
55
+ #endif
@@ -1,22 +1,24 @@
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>
8
8
  #include <cstdint>
9
+ #include <functional>
9
10
  #include <memory>
10
11
  #include <string>
11
- #include <sys/types.h>
12
12
  #include <vector>
13
13
 
14
- #include "file_descriptor.h"
14
+ #ifdef _WIN32
15
+ #include <BaseTsd.h>
16
+ using ssize_t = SSIZE_T;
17
+ #else
18
+ #include <sys/types.h>
19
+ #endif
15
20
 
16
- struct OpenResult {
17
- FileDescriptor fd;
18
- std::string interface_name;
19
- };
21
+ #include <uv.h>
20
22
 
21
23
  enum class ReadPacketStatus {
22
24
  Data,
@@ -25,13 +27,54 @@ enum class ReadPacketStatus {
25
27
  Error,
26
28
  };
27
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
+ */
28
39
  class TunPlatformBackend {
29
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`.
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.
49
+ using ErrorCallback = std::function<void(const std::string&)>;
50
+
30
51
  virtual ~TunPlatformBackend() = default;
31
- virtual bool OpenDevice(const std::string& requested_name, OpenResult& out, std::string& error) = 0;
32
- virtual ReadPacketStatus ReadPacket(int fd, size_t max_payload_size, std::vector<uint8_t>& out, std::string& error) = 0;
33
- virtual ssize_t WritePacket(int fd, const uint8_t* data, size_t length, std::string& error) = 0;
52
+
53
+ virtual bool OpenDevice(const std::string& requested_name,
54
+ std::string& out_interface_name,
55
+ std::string& error) = 0;
56
+ virtual void CloseDevice() = 0;
57
+ virtual bool IsOpen() const = 0;
58
+
59
+ virtual ReadPacketStatus ReadPacket(size_t max_payload_size,
60
+ std::vector<uint8_t>& out,
61
+ std::string& error) = 0;
62
+ virtual ssize_t WritePacket(const uint8_t* data,
63
+ size_t length,
64
+ std::string& error) = 0;
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.
68
+ virtual bool StartReceiveLoop(uv_loop_t* loop,
69
+ size_t buffer_size,
70
+ PacketCallback on_packet,
71
+ ErrorCallback on_error,
72
+ std::string& error) = 0;
73
+ virtual void StopReceiveLoop() = 0;
74
+
75
+ // Returns the underlying POSIX file descriptor when one exists. Backends
76
+ // without a numeric fd (e.g. Wintun on Windows) return `-1`.
77
+ virtual int GetNativeFd() const { return -1; }
34
78
  };
35
79
 
36
80
  std::unique_ptr<TunPlatformBackend> CreatePlatformBackend();
37
-
@@ -14,30 +14,23 @@
14
14
  #include <netinet/in.h>
15
15
  #include <netinet6/in6_var.h>
16
16
 
17
- #include <fcntl.h>
17
+ #include <utility>
18
+
19
+ #include "file_descriptor.h"
20
+ #include "posix_tun_backend.h"
21
+ #include "posix_uv_poll_loop.h"
18
22
 
19
23
  #define UTUN_CONTROL_NAME "com.apple.net.utun_control"
20
24
 
21
25
  namespace {
22
26
 
23
- bool SetNonBlocking(int fd, std::string& error) {
24
- int flags = fcntl(fd, F_GETFL, 0);
25
- if (flags < 0) {
26
- error = std::string("Failed to get file descriptor flags: ") + strerror(errno);
27
- return false;
28
- }
29
- if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
30
- error = std::string("Failed to set non-blocking mode: ") + strerror(errno);
31
- return false;
32
- }
33
- return true;
34
- }
35
-
36
- class DarwinTunBackend : public TunPlatformBackend {
27
+ class DarwinTunBackend : public PosixTunBackend {
37
28
  public:
38
29
  static constexpr size_t kUtunHeaderSize = 4;
39
30
 
40
- bool OpenDevice(const std::string& requested_name, OpenResult& out, std::string& error) override {
31
+ bool OpenDevice(const std::string& requested_name,
32
+ std::string& out_interface_name,
33
+ std::string& error) override {
41
34
  struct ctl_info ctl_info;
42
35
  struct sockaddr_ctl socket_addr;
43
36
 
@@ -84,14 +77,22 @@ public:
84
77
  return false;
85
78
  }
86
79
 
87
- out.fd = std::move(temp_fd);
88
- out.interface_name = std::string(interface_name);
80
+ fd_ = std::move(temp_fd);
81
+ interface_name_ = std::string(interface_name);
82
+ out_interface_name = interface_name_;
89
83
  return true;
90
84
  }
91
85
 
92
- ReadPacketStatus ReadPacket(int fd, size_t max_payload_size, std::vector<uint8_t>& out, std::string& error) override {
86
+ ReadPacketStatus ReadPacket(size_t max_payload_size,
87
+ std::vector<uint8_t>& out,
88
+ std::string& error) override {
89
+ if (!fd_.is_valid()) {
90
+ error = "Device not open";
91
+ return ReadPacketStatus::Error;
92
+ }
93
+
93
94
  out.resize(max_payload_size + kUtunHeaderSize);
94
- ssize_t bytes_read = read(fd, out.data(), out.size());
95
+ ssize_t bytes_read = read(fd_.get(), out.data(), out.size());
95
96
  if (bytes_read < 0) {
96
97
  if (errno == EAGAIN || errno == EWOULDBLOCK) {
97
98
  out.clear();
@@ -116,21 +117,28 @@ public:
116
117
  return ReadPacketStatus::Data;
117
118
  }
118
119
 
119
- ssize_t WritePacket(int fd, const uint8_t* data, size_t length, std::string& error) override {
120
+ ssize_t WritePacket(const uint8_t* data,
121
+ size_t length,
122
+ std::string& error) override {
123
+ if (!fd_.is_valid()) {
124
+ error = "Device not open";
125
+ return -1;
126
+ }
127
+
120
128
  std::vector<uint8_t> frame(length + kUtunHeaderSize);
121
129
  uint32_t family = htonl(AF_INET6);
122
130
  memcpy(frame.data(), &family, kUtunHeaderSize);
123
131
  memcpy(frame.data() + kUtunHeaderSize, data, length);
124
132
 
125
- ssize_t bytes_written = write(fd, frame.data(), frame.size());
133
+ ssize_t bytes_written = write(fd_.get(), frame.data(), frame.size());
126
134
  if (bytes_written < 0) {
127
135
  error = std::string("Write error: ") + strerror(errno);
128
136
  return -1;
129
137
  }
130
138
 
131
139
  return bytes_written > static_cast<ssize_t>(kUtunHeaderSize)
132
- ? bytes_written - static_cast<ssize_t>(kUtunHeaderSize)
133
- : 0;
140
+ ? bytes_written - static_cast<ssize_t>(kUtunHeaderSize)
141
+ : 0;
134
142
  }
135
143
 
136
144
  private:
@@ -168,4 +176,3 @@ std::unique_ptr<TunPlatformBackend> CreatePlatformBackend() {
168
176
  }
169
177
 
170
178
  #endif
171
-