appium-ios-tuntap 0.2.4 → 0.2.5

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,9 @@
1
+ ## [0.2.5](https://github.com/appium/appium-ios-tuntap/compare/v0.2.4...v0.2.5) (2026-05-14)
2
+
3
+ ### Code Refactoring
4
+
5
+ * 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))
6
+
1
7
  ## [0.2.4](https://github.com/appium/appium-ios-tuntap/compare/v0.2.3...v0.2.4) (2026-05-13)
2
8
 
3
9
  ### 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\")"
@@ -64,6 +63,8 @@
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": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-tuntap",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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,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
@@ -6,17 +6,19 @@
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,
@@ -27,11 +29,32 @@ enum class ReadPacketStatus {
27
29
 
28
30
  class TunPlatformBackend {
29
31
  public:
32
+ using PacketCallback = std::function<void(std::vector<uint8_t>)>;
33
+ using ErrorCallback = std::function<void(const std::string&)>;
34
+
30
35
  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;
36
+
37
+ virtual bool OpenDevice(const std::string& requested_name,
38
+ std::string& out_interface_name,
39
+ std::string& error) = 0;
40
+ virtual void CloseDevice() = 0;
41
+ virtual bool IsOpen() const = 0;
42
+
43
+ virtual ReadPacketStatus ReadPacket(size_t max_payload_size,
44
+ std::vector<uint8_t>& out,
45
+ std::string& error) = 0;
46
+ virtual ssize_t WritePacket(const uint8_t* data,
47
+ size_t length,
48
+ std::string& error) = 0;
49
+
50
+ virtual bool StartReceiveLoop(uv_loop_t* loop,
51
+ size_t buffer_size,
52
+ PacketCallback on_packet,
53
+ ErrorCallback on_error,
54
+ std::string& error) = 0;
55
+ virtual void StopReceiveLoop() = 0;
56
+
57
+ virtual int GetNativeFd() const { return -1; }
34
58
  };
35
59
 
36
60
  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
-
@@ -12,40 +12,35 @@
12
12
  #include <linux/if.h>
13
13
  #include <linux/if_tun.h>
14
14
 
15
- namespace {
15
+ #include <utility>
16
16
 
17
- bool SetNonBlocking(int fd, std::string& error) {
18
- int flags = fcntl(fd, F_GETFL, 0);
19
- if (flags < 0) {
20
- error = std::string("Failed to get file descriptor flags: ") + strerror(errno);
21
- return false;
22
- }
23
- if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
24
- error = std::string("Failed to set non-blocking mode: ") + strerror(errno);
25
- return false;
26
- }
27
- return true;
28
- }
17
+ #include "file_descriptor.h"
18
+ #include "posix_tun_backend.h"
19
+ #include "posix_uv_poll_loop.h"
20
+
21
+ namespace {
29
22
 
30
23
  constexpr const char* kTunDevicePath = "/dev/net/tun";
31
24
 
32
- class LinuxTunBackend : public TunPlatformBackend {
25
+ class LinuxTunBackend : public PosixTunBackend {
33
26
  public:
34
- bool OpenDevice(const std::string& requested_name, OpenResult& out, std::string& error) override {
27
+ bool OpenDevice(const std::string& requested_name,
28
+ std::string& out_interface_name,
29
+ std::string& error) override {
35
30
  struct stat statbuf;
36
31
  if (stat(kTunDevicePath, &statbuf) != 0) {
37
32
  error =
38
- "TUN/TAP device not available: /dev/net/tun does not exist. "
39
- "Please ensure the TUN/TAP kernel module is loaded (modprobe tun).";
33
+ "TUN/TAP device not available: /dev/net/tun does not exist. "
34
+ "Please ensure the TUN/TAP kernel module is loaded (modprobe tun).";
40
35
  return false;
41
36
  }
42
37
 
43
38
  FileDescriptor temp_fd(open(kTunDevicePath, O_RDWR));
44
39
  if (!temp_fd.is_valid()) {
45
40
  error =
46
- std::string("Failed to open ") + kTunDevicePath + ": " + strerror(errno) +
47
- ". This usually means you don't have sufficient permissions. "
48
- "Try running with sudo or add your user to the 'tun' group.";
41
+ std::string("Failed to open ") + kTunDevicePath + ": " + strerror(errno) +
42
+ ". This usually means you don't have sufficient permissions. "
43
+ "Try running with sudo or add your user to the 'tun' group.";
49
44
  return false;
50
45
  }
51
46
 
@@ -67,14 +62,22 @@ public:
67
62
  return false;
68
63
  }
69
64
 
70
- out.fd = std::move(temp_fd);
71
- out.interface_name = std::string(ifr.ifr_name);
65
+ fd_ = std::move(temp_fd);
66
+ interface_name_ = std::string(ifr.ifr_name);
67
+ out_interface_name = interface_name_;
72
68
  return true;
73
69
  }
74
70
 
75
- ReadPacketStatus ReadPacket(int fd, size_t max_payload_size, std::vector<uint8_t>& out, std::string& error) override {
71
+ ReadPacketStatus ReadPacket(size_t max_payload_size,
72
+ std::vector<uint8_t>& out,
73
+ std::string& error) override {
74
+ if (!fd_.is_valid()) {
75
+ error = "Device not open";
76
+ return ReadPacketStatus::Error;
77
+ }
78
+
76
79
  out.resize(max_payload_size);
77
- ssize_t bytes_read = read(fd, out.data(), out.size());
80
+ ssize_t bytes_read = read(fd_.get(), out.data(), out.size());
78
81
  if (bytes_read < 0) {
79
82
  if (errno == EAGAIN || errno == EWOULDBLOCK) {
80
83
  out.clear();
@@ -92,8 +95,14 @@ public:
92
95
  return ReadPacketStatus::Data;
93
96
  }
94
97
 
95
- ssize_t WritePacket(int fd, const uint8_t* data, size_t length, std::string& error) override {
96
- ssize_t bytes_written = write(fd, data, length);
98
+ ssize_t WritePacket(const uint8_t* data,
99
+ size_t length,
100
+ std::string& error) override {
101
+ if (!fd_.is_valid()) {
102
+ error = "Device not open";
103
+ return -1;
104
+ }
105
+ ssize_t bytes_written = write(fd_.get(), data, length);
97
106
  if (bytes_written < 0) {
98
107
  error = std::string("Write error: ") + strerror(errno);
99
108
  return -1;
@@ -109,4 +118,3 @@ std::unique_ptr<TunPlatformBackend> CreatePlatformBackend() {
109
118
  }
110
119
 
111
120
  #endif
112
-
package/src/tuntap.cc CHANGED
@@ -1,14 +1,13 @@
1
1
  #include <napi.h>
2
- #include <uv.h>
3
2
 
4
- #include <string>
5
- #include <vector>
6
- #include <memory>
7
- #include <mutex>
8
3
  #include <atomic>
9
4
  #include <cstdio>
5
+ #include <memory>
6
+ #include <mutex>
7
+ #include <string>
8
+ #include <utility>
9
+ #include <vector>
10
10
 
11
- #include "native/file_descriptor.h"
12
11
  #include "native/tun_backend.h"
13
12
 
14
13
  class TunDevice : public Napi::ObjectWrap<TunDevice> {
@@ -30,24 +29,22 @@ private:
30
29
  Napi::Value GetFd(const Napi::CallbackInfo& info);
31
30
  Napi::Value StartPolling(const Napi::CallbackInfo& info);
32
31
 
33
- FileDescriptor fd_;
34
- std::string name_;
35
32
  std::unique_ptr<TunPlatformBackend> backend_;
33
+ std::string requested_name_;
34
+ std::string interface_name_;
36
35
  std::atomic<bool> is_open_;
37
36
  std::mutex device_mutex_;
38
37
 
39
- uv_poll_t* poll_handle_ = nullptr;
40
38
  Napi::ThreadSafeFunction tsfn_;
39
+ std::atomic<bool> polling_;
41
40
  static constexpr size_t MAX_POLL_BUFFER = 65535;
42
- size_t poll_buffer_size_ = MAX_POLL_BUFFER;
43
41
 
44
- void StopPolling();
45
- static void PollCallback(uv_poll_t* handle, int status, int events);
42
+ void StopPollingLocked();
43
+ void ReleaseTsfnLocked();
46
44
  };
47
45
 
48
46
  Napi::FunctionReference TunDevice::constructor;
49
47
 
50
- // Defines and exports the JS class constructor: new TunDevice(name?)
51
48
  Napi::Object TunDevice::Init(Napi::Env env, Napi::Object exports) {
52
49
  Napi::HandleScope scope(env);
53
50
 
@@ -68,25 +65,24 @@ Napi::Object TunDevice::Init(Napi::Env env, Napi::Object exports) {
68
65
  return exports;
69
66
  }
70
67
 
71
- // Creates a TunDevice wrapper; optional first arg is requested interface name.
72
68
  TunDevice::TunDevice(const Napi::CallbackInfo& info)
73
- : Napi::ObjectWrap<TunDevice>(info), backend_(CreatePlatformBackend()), is_open_(false) {
69
+ : Napi::ObjectWrap<TunDevice>(info),
70
+ backend_(CreatePlatformBackend()),
71
+ is_open_(false),
72
+ polling_(false) {
74
73
  Napi::Env env = info.Env();
75
74
  Napi::HandleScope scope(env);
76
75
 
77
76
  if (info.Length() > 0 && info[0].IsString()) {
78
- name_ = info[0].As<Napi::String>().Utf8Value();
77
+ requested_name_ = info[0].As<Napi::String>().Utf8Value();
79
78
  }
80
79
  }
81
80
 
82
- // Ensures fd/poll resources are closed when object is destroyed.
83
81
  TunDevice::~TunDevice() {
84
82
  std::lock_guard<std::mutex> lock(device_mutex_);
85
83
  CloseInternal();
86
84
  }
87
85
 
88
- // JS: open() -> boolean
89
- // Opens the backend device and configures the fd as non-blocking.
90
86
  Napi::Value TunDevice::Open(const Napi::CallbackInfo& info) {
91
87
  Napi::Env env = info.Env();
92
88
  std::lock_guard<std::mutex> lock(device_mutex_);
@@ -100,22 +96,18 @@ Napi::Value TunDevice::Open(const Napi::CallbackInfo& info) {
100
96
  return Napi::Boolean::New(env, false);
101
97
  }
102
98
 
103
- OpenResult result;
104
99
  std::string error;
105
- if (!backend_->OpenDevice(name_, result, error)) {
100
+ std::string assigned_name;
101
+ if (!backend_->OpenDevice(requested_name_, assigned_name, error)) {
106
102
  Napi::Error::New(env, error).ThrowAsJavaScriptException();
107
103
  return Napi::Boolean::New(env, false);
108
104
  }
109
105
 
110
- fd_ = std::move(result.fd);
111
- name_ = result.interface_name;
106
+ interface_name_ = std::move(assigned_name);
112
107
  is_open_ = true;
113
-
114
108
  return Napi::Boolean::New(env, true);
115
109
  }
116
110
 
117
- // JS: close() -> boolean
118
- // Safely closes device resources; calling multiple times is allowed.
119
111
  Napi::Value TunDevice::Close(const Napi::CallbackInfo& info) {
120
112
  Napi::Env env = info.Env();
121
113
  std::lock_guard<std::mutex> lock(device_mutex_);
@@ -123,13 +115,11 @@ Napi::Value TunDevice::Close(const Napi::CallbackInfo& info) {
123
115
  return Napi::Boolean::New(env, true);
124
116
  }
125
117
 
126
- // JS: read(bufferSize?) -> Buffer
127
- // Reads one payload packet, or returns an empty Buffer when no data is available.
128
118
  Napi::Value TunDevice::Read(const Napi::CallbackInfo& info) {
129
119
  Napi::Env env = info.Env();
130
120
  std::lock_guard<std::mutex> lock(device_mutex_);
131
121
 
132
- if (!is_open_ || !fd_.is_valid()) {
122
+ if (!is_open_ || !backend_ || !backend_->IsOpen()) {
133
123
  Napi::Error::New(env, "Device not open").ThrowAsJavaScriptException();
134
124
  return env.Null();
135
125
  }
@@ -145,25 +135,22 @@ Napi::Value TunDevice::Read(const Napi::CallbackInfo& info) {
145
135
 
146
136
  std::vector<uint8_t> packet;
147
137
  std::string error;
148
- ReadPacketStatus read_status = backend_->ReadPacket(fd_.get(), buffer_size, packet, error);
149
- if (read_status == ReadPacketStatus::Error) {
138
+ ReadPacketStatus rs = backend_->ReadPacket(buffer_size, packet, error);
139
+ if (rs == ReadPacketStatus::Error) {
150
140
  Napi::Error::New(env, error).ThrowAsJavaScriptException();
151
141
  return env.Null();
152
142
  }
153
- if (read_status == ReadPacketStatus::NoData || read_status == ReadPacketStatus::Closed) {
143
+ if (rs == ReadPacketStatus::NoData || rs == ReadPacketStatus::Closed) {
154
144
  return Napi::Buffer<uint8_t>::New(env, 0);
155
145
  }
156
-
157
146
  return Napi::Buffer<uint8_t>::Copy(env, packet.data(), packet.size());
158
147
  }
159
148
 
160
- // JS: write(buffer) -> number
161
- // Writes one packet and returns payload bytes accepted by the backend.
162
149
  Napi::Value TunDevice::Write(const Napi::CallbackInfo& info) {
163
150
  Napi::Env env = info.Env();
164
151
  std::lock_guard<std::mutex> lock(device_mutex_);
165
152
 
166
- if (!is_open_ || !fd_.is_valid()) {
153
+ if (!is_open_ || !backend_ || !backend_->IsOpen()) {
167
154
  Napi::Error::New(env, "Device not open").ThrowAsJavaScriptException();
168
155
  return Napi::Number::New(env, -1);
169
156
  }
@@ -178,35 +165,29 @@ Napi::Value TunDevice::Write(const Napi::CallbackInfo& info) {
178
165
  size_t length = buffer.Length();
179
166
 
180
167
  std::string error;
181
- ssize_t bytes_written = backend_->WritePacket(fd_.get(), data, length, error);
168
+ ssize_t bytes_written = backend_->WritePacket(data, length, error);
182
169
  if (bytes_written < 0) {
183
170
  Napi::Error::New(env, error).ThrowAsJavaScriptException();
184
171
  return Napi::Number::New(env, -1);
185
172
  }
186
- return Napi::Number::New(env, bytes_written);
173
+ return Napi::Number::New(env, static_cast<double>(bytes_written));
187
174
  }
188
175
 
189
- // JS: getName() -> string
190
- // Returns the assigned interface name after open().
191
176
  Napi::Value TunDevice::GetName(const Napi::CallbackInfo& info) {
192
177
  std::lock_guard<std::mutex> lock(device_mutex_);
193
- return Napi::String::New(info.Env(), name_);
178
+ return Napi::String::New(info.Env(), interface_name_);
194
179
  }
195
180
 
196
- // JS: getFd() -> number
197
- // Returns the native file descriptor, or -1 before open()/after close().
198
181
  Napi::Value TunDevice::GetFd(const Napi::CallbackInfo& info) {
199
182
  std::lock_guard<std::mutex> lock(device_mutex_);
200
- return Napi::Number::New(info.Env(), fd_.get());
183
+ return Napi::Number::New(info.Env(), backend_ ? backend_->GetNativeFd() : -1);
201
184
  }
202
185
 
203
- // JS: startPolling(callback, bufferSize?) -> void
204
- // Starts libuv polling and invokes callback with packet payload Buffers.
205
186
  Napi::Value TunDevice::StartPolling(const Napi::CallbackInfo& info) {
206
187
  Napi::Env env = info.Env();
207
188
  std::lock_guard<std::mutex> lock(device_mutex_);
208
189
 
209
- if (!is_open_ || !fd_.is_valid()) {
190
+ if (!is_open_ || !backend_ || !backend_->IsOpen()) {
210
191
  Napi::Error::New(env, "Device not open").ThrowAsJavaScriptException();
211
192
  return env.Null();
212
193
  }
@@ -216,134 +197,91 @@ Napi::Value TunDevice::StartPolling(const Napi::CallbackInfo& info) {
216
197
  return env.Null();
217
198
  }
218
199
 
219
- StopPolling();
200
+ StopPollingLocked();
220
201
 
221
- // Optional buffer size as second argument (default: MAX_POLL_BUFFER)
222
- poll_buffer_size_ = MAX_POLL_BUFFER;
202
+ size_t buffer_size = MAX_POLL_BUFFER;
223
203
  if (info.Length() > 1 && info[1].IsNumber()) {
224
204
  auto size = info[1].As<Napi::Number>().Uint32Value();
225
205
  if (size == 0 || size > MAX_POLL_BUFFER) {
226
206
  Napi::RangeError::New(env, "Buffer size must be between 1 and " + std::to_string(MAX_POLL_BUFFER)).ThrowAsJavaScriptException();
227
207
  return env.Null();
228
208
  }
229
- poll_buffer_size_ = size;
209
+ buffer_size = size;
230
210
  }
231
211
 
232
212
  tsfn_ = Napi::ThreadSafeFunction::New(
233
- env,
234
- info[0].As<Napi::Function>(),
235
- "TunDeviceDataCallback",
236
- 0,
237
- 1
238
- );
213
+ env,
214
+ info[0].As<Napi::Function>(),
215
+ "TunDeviceDataCallback",
216
+ 0,
217
+ 1);
239
218
 
240
219
  uv_loop_t* loop = nullptr;
241
220
  napi_status napi_st = napi_get_uv_event_loop(env, &loop);
242
221
  if (napi_st != napi_ok || loop == nullptr) {
243
- tsfn_.Release();
244
- tsfn_ = nullptr;
222
+ ReleaseTsfnLocked();
245
223
  Napi::Error::New(env, "Failed to acquire event loop").ThrowAsJavaScriptException();
246
224
  return env.Null();
247
225
  }
248
226
 
249
- auto handle = std::make_unique<uv_poll_t>();
250
- if (uv_poll_init(loop, handle.get(), fd_.get()) != 0) {
251
- tsfn_.Release();
252
- tsfn_ = nullptr;
253
- Napi::Error::New(env, "Failed to initialize poll handle").ThrowAsJavaScriptException();
254
- return env.Null();
255
- }
256
-
257
- handle->data = this;
258
- if (uv_poll_start(handle.get(), UV_READABLE, PollCallback) != 0) {
259
- // Properly close the initialized-but-not-started handle
260
- uv_close(reinterpret_cast<uv_handle_t*>(handle.release()), [](uv_handle_t* h) {
261
- delete reinterpret_cast<uv_poll_t*>(h);
262
- });
263
- tsfn_.Release();
264
- tsfn_ = nullptr;
265
- Napi::Error::New(env, "Failed to start polling").ThrowAsJavaScriptException();
227
+ Napi::ThreadSafeFunction tsfn = tsfn_;
228
+ auto packet_cb = [tsfn](std::vector<uint8_t> packet) mutable {
229
+ tsfn.BlockingCall(
230
+ [packet = std::move(packet)](Napi::Env env, Napi::Function jsCallback) {
231
+ if (env == nullptr || jsCallback.IsEmpty()) {
232
+ return;
233
+ }
234
+ jsCallback.Call({Napi::Buffer<uint8_t>::Copy(env, packet.data(), packet.size())});
235
+ });
236
+ };
237
+ // Terminal errors from the receive loop (poll error, device closed, read
238
+ // error) call back here so the JS-side polling_ flag and TSFN are released
239
+ // promptly. Callback runs on the libuv thread, which only fires between JS
240
+ // ticks, so acquiring `device_mutex_` is safe.
241
+ auto error_cb = [this](const std::string& message) {
242
+ fprintf(stderr, "tuntap receive loop error: %s\n", message.c_str());
243
+ std::lock_guard<std::mutex> lock(device_mutex_);
244
+ polling_ = false;
245
+ ReleaseTsfnLocked();
246
+ };
247
+
248
+ std::string start_error;
249
+ if (!backend_->StartReceiveLoop(loop, buffer_size, std::move(packet_cb), std::move(error_cb), start_error)) {
250
+ ReleaseTsfnLocked();
251
+ Napi::Error::New(env, start_error).ThrowAsJavaScriptException();
266
252
  return env.Null();
267
253
  }
268
254
 
269
- poll_handle_ = handle.release();
255
+ polling_ = true;
270
256
  return env.Undefined();
271
257
  }
272
258
 
273
- // Node-API module entrypoint.
274
259
  Napi::Object Init(Napi::Env env, Napi::Object exports) {
275
260
  return TunDevice::Init(env, exports);
276
261
  }
277
262
 
278
263
  NODE_API_MODULE(tuntap, Init)
279
- // #endregion
280
-
281
- // #region Private implementation details
282
264
 
283
265
  void TunDevice::CloseInternal() {
284
266
  if (is_open_.exchange(false)) {
285
- StopPolling();
286
- fd_.reset();
267
+ StopPollingLocked();
268
+ if (backend_) {
269
+ backend_->CloseDevice();
270
+ }
271
+ interface_name_.clear();
287
272
  }
288
273
  }
289
274
 
290
- void TunDevice::StopPolling() {
291
- if (poll_handle_) {
292
- uv_poll_stop(poll_handle_);
293
- // Must use uv_close before freeing a libuv handle
294
- uv_close(reinterpret_cast<uv_handle_t*>(poll_handle_), [](uv_handle_t* handle) {
295
- delete reinterpret_cast<uv_poll_t*>(handle);
296
- });
297
- poll_handle_ = nullptr;
275
+ void TunDevice::StopPollingLocked() {
276
+ if (polling_.exchange(false) && backend_) {
277
+ backend_->StopReceiveLoop();
298
278
  }
279
+ ReleaseTsfnLocked();
280
+ }
281
+
282
+ void TunDevice::ReleaseTsfnLocked() {
299
283
  if (tsfn_) {
300
284
  tsfn_.Release();
301
285
  tsfn_ = nullptr;
302
286
  }
303
287
  }
304
-
305
- void TunDevice::PollCallback(uv_poll_t* handle, int status, int events) {
306
- if (status < 0) {
307
- fprintf(stderr, "tuntap poll error: %s\n", uv_strerror(status));
308
- auto* self = static_cast<TunDevice*>(handle->data);
309
- if (self) {
310
- self->StopPolling();
311
- }
312
- return;
313
- }
314
-
315
- if (!(events & UV_READABLE)) {
316
- return;
317
- }
318
-
319
- auto* self = static_cast<TunDevice*>(handle->data);
320
- if (!self || !self->is_open_.load() || !self->fd_.is_valid()) {
321
- return;
322
- }
323
-
324
- std::vector<uint8_t> packet;
325
- std::string error;
326
- ReadPacketStatus read_status = self->backend_->ReadPacket(self->fd_.get(), self->poll_buffer_size_, packet, error);
327
- // Backend reported an unrecoverable read failure; stop polling this fd.
328
- if (read_status == ReadPacketStatus::Error) {
329
- fprintf(stderr, "tuntap read error: %s\n", error.c_str());
330
- self->StopPolling();
331
- return;
332
- }
333
- // EOF/peer close: device is no longer readable, so tear down polling.
334
- if (read_status == ReadPacketStatus::Closed) {
335
- self->StopPolling();
336
- return;
337
- }
338
- // Transient empty read (e.g. EAGAIN): keep poll active and wait for next event.
339
- if (read_status == ReadPacketStatus::NoData) {
340
- return;
341
- }
342
-
343
- self->tsfn_.BlockingCall(
344
- [packet = std::move(packet)](Napi::Env env, Napi::Function jsCallback) {
345
- if (env == nullptr || jsCallback.IsEmpty()) return;
346
- jsCallback.Call({ Napi::Buffer<uint8_t>::Copy(env, packet.data(), packet.size()) });
347
- }
348
- );
349
- }