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