appium-ios-tuntap 0.2.0 → 0.2.1

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.1](https://github.com/appium/appium-ios-tuntap/compare/v0.2.0...v0.2.1) (2026-04-13)
2
+
3
+ ### Miscellaneous Chores
4
+
5
+ * Refactor native tuntap implementation ([#33](https://github.com/appium/appium-ios-tuntap/issues/33)) ([c24636e](https://github.com/appium/appium-ios-tuntap/commit/c24636ed054dd37a36a08b6b5c09bfd7f40d55b1))
6
+
1
7
  ## [0.2.0](https://github.com/appium/appium-ios-tuntap/compare/v0.1.10...v0.2.0) (2026-04-13)
2
8
 
3
9
  ### Features
package/binding.gyp CHANGED
@@ -2,7 +2,11 @@
2
2
  "targets": [
3
3
  {
4
4
  "target_name": "tuntap",
5
- "sources": [ "src/tuntap.cc" ],
5
+ "sources": [
6
+ "src/tuntap.cc",
7
+ "src/native/file_descriptor.cc",
8
+ "src/native/tun_backend_common.cc"
9
+ ],
6
10
  "include_dirs": [
7
11
  "<!@(node -p \"require('node-addon-api').include\")"
8
12
  ],
@@ -60,6 +64,9 @@
60
64
  ],
61
65
  "conditions": [
62
66
  ["OS=='linux'", {
67
+ "sources": [
68
+ "src/native/tun_backend_linux.cc"
69
+ ],
63
70
  "cflags": [
64
71
  "-pthread"
65
72
  ],
@@ -71,6 +78,9 @@
71
78
  ]
72
79
  }],
73
80
  ["OS=='mac'", {
81
+ "sources": [
82
+ "src/native/tun_backend_darwin.cc"
83
+ ],
74
84
  "xcode_settings": {
75
85
  "OTHER_LDFLAGS": [
76
86
  "-framework", "SystemConfiguration",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-tuntap",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Native TUN/TAP interface module for Node.js",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "files": [
24
24
  "src/tuntap.cc",
25
+ "src/native",
25
26
  "lib",
26
27
  "prebuilds",
27
28
  "binding.gyp",
@@ -0,0 +1,49 @@
1
+ #include "file_descriptor.h"
2
+
3
+ #include <unistd.h>
4
+
5
+ FileDescriptor::FileDescriptor() : fd_(-1) {}
6
+
7
+ FileDescriptor::FileDescriptor(int fd) : fd_(fd) {}
8
+
9
+ FileDescriptor::~FileDescriptor() {
10
+ if (fd_ >= 0) {
11
+ ::close(fd_);
12
+ }
13
+ }
14
+
15
+ FileDescriptor::FileDescriptor(FileDescriptor&& other) noexcept : fd_(other.fd_) {
16
+ other.fd_ = -1;
17
+ }
18
+
19
+ FileDescriptor& FileDescriptor::operator=(FileDescriptor&& other) noexcept {
20
+ if (this != &other) {
21
+ if (fd_ >= 0) {
22
+ ::close(fd_);
23
+ }
24
+ fd_ = other.fd_;
25
+ other.fd_ = -1;
26
+ }
27
+ return *this;
28
+ }
29
+
30
+ int FileDescriptor::get() const {
31
+ return fd_;
32
+ }
33
+
34
+ int FileDescriptor::release() {
35
+ int temp = fd_;
36
+ fd_ = -1;
37
+ return temp;
38
+ }
39
+
40
+ bool FileDescriptor::is_valid() const {
41
+ return fd_ >= 0;
42
+ }
43
+
44
+ void FileDescriptor::reset(int fd) {
45
+ if (fd_ >= 0) {
46
+ ::close(fd_);
47
+ }
48
+ fd_ = fd;
49
+ }
@@ -0,0 +1,22 @@
1
+ #pragma once
2
+
3
+ class FileDescriptor {
4
+ public:
5
+ FileDescriptor();
6
+ explicit FileDescriptor(int fd);
7
+ ~FileDescriptor();
8
+
9
+ FileDescriptor(const FileDescriptor&) = delete;
10
+ FileDescriptor& operator=(const FileDescriptor&) = delete;
11
+
12
+ FileDescriptor(FileDescriptor&& other) noexcept;
13
+ FileDescriptor& operator=(FileDescriptor&& other) noexcept;
14
+
15
+ int get() const;
16
+ int release();
17
+ bool is_valid() const;
18
+ void reset(int fd = -1);
19
+
20
+ private:
21
+ int fd_;
22
+ };
@@ -0,0 +1,38 @@
1
+ #pragma once
2
+
3
+ #if !defined(__linux__) && !defined(__APPLE__)
4
+ #error "appium-ios-tuntap native addon supports only Linux and macOS"
5
+ #endif
6
+
7
+ #include <cstddef>
8
+ #include <cstdint>
9
+ #include <memory>
10
+ #include <string>
11
+ #include <sys/types.h>
12
+ #include <vector>
13
+
14
+ #include "file_descriptor.h"
15
+
16
+ struct OpenResult {
17
+ FileDescriptor fd;
18
+ std::string interface_name;
19
+ };
20
+
21
+ enum class ReadPacketStatus {
22
+ Data,
23
+ NoData,
24
+ Closed,
25
+ Error,
26
+ };
27
+
28
+ class TunPlatformBackend {
29
+ public:
30
+ 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;
34
+ };
35
+
36
+ bool SetNonBlocking(int fd, std::string& error);
37
+ std::unique_ptr<TunPlatformBackend> CreatePlatformBackend();
38
+
@@ -0,0 +1,27 @@
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
+
@@ -0,0 +1,152 @@
1
+ #ifdef __APPLE__
2
+
3
+ #include "tun_backend.h"
4
+
5
+ #include <errno.h>
6
+ #include <string.h>
7
+ #include <sys/ioctl.h>
8
+ #include <sys/kern_control.h>
9
+ #include <sys/socket.h>
10
+ #include <sys/sys_domain.h>
11
+ #include <unistd.h>
12
+
13
+ #include <net/if_utun.h>
14
+ #include <netinet/in.h>
15
+ #include <netinet6/in6_var.h>
16
+
17
+ #define UTUN_CONTROL_NAME "com.apple.net.utun_control"
18
+
19
+ namespace {
20
+
21
+ class DarwinTunBackend : public TunPlatformBackend {
22
+ public:
23
+ static constexpr size_t kUtunHeaderSize = 4;
24
+
25
+ bool OpenDevice(const std::string& requested_name, OpenResult& out, std::string& error) override {
26
+ struct ctl_info ctl_info;
27
+ struct sockaddr_ctl socket_addr;
28
+
29
+ FileDescriptor temp_fd(socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL));
30
+ if (!temp_fd.is_valid()) {
31
+ error = std::string("Failed to create control socket: ") + strerror(errno);
32
+ return false;
33
+ }
34
+
35
+ memset(&ctl_info, 0, sizeof(ctl_info));
36
+ strncpy(ctl_info.ctl_name, UTUN_CONTROL_NAME, sizeof(ctl_info.ctl_name) - 1);
37
+ ctl_info.ctl_name[sizeof(ctl_info.ctl_name) - 1] = '\0';
38
+
39
+ if (ioctl(temp_fd.get(), CTLIOCGINFO, &ctl_info) < 0) {
40
+ error = std::string("Failed to get utun control info: ") + strerror(errno);
41
+ return false;
42
+ }
43
+
44
+ memset(&socket_addr, 0, sizeof(socket_addr));
45
+ socket_addr.sc_len = sizeof(socket_addr);
46
+ socket_addr.sc_family = AF_SYSTEM;
47
+ socket_addr.ss_sysaddr = SYSPROTO_CONTROL;
48
+ socket_addr.sc_id = ctl_info.ctl_id;
49
+
50
+ int utun_unit = ParseRequestedUtunUnit(requested_name);
51
+ if (utun_unit > 0) {
52
+ socket_addr.sc_unit = utun_unit;
53
+ if (connect(temp_fd.get(), reinterpret_cast<struct sockaddr*>(&socket_addr), sizeof(socket_addr)) < 0) {
54
+ error = std::string("Failed to connect to utun with specified unit: ") + strerror(errno);
55
+ return false;
56
+ }
57
+ } else if (!ConnectFirstAvailableUnit(temp_fd.get(), socket_addr, error)) {
58
+ return false;
59
+ }
60
+
61
+ char interface_name[20];
62
+ socklen_t interface_name_len = sizeof(interface_name);
63
+ if (getsockopt(temp_fd.get(), SYSPROTO_CONTROL, UTUN_OPT_IFNAME, interface_name, &interface_name_len) < 0) {
64
+ error = std::string("Failed to get utun interface name: ") + strerror(errno);
65
+ return false;
66
+ }
67
+
68
+ out.fd = std::move(temp_fd);
69
+ out.interface_name = std::string(interface_name);
70
+ return true;
71
+ }
72
+
73
+ ReadPacketStatus ReadPacket(int fd, size_t max_payload_size, std::vector<uint8_t>& out, std::string& error) override {
74
+ out.resize(max_payload_size + kUtunHeaderSize);
75
+ ssize_t bytes_read = read(fd, out.data(), out.size());
76
+ if (bytes_read < 0) {
77
+ if (errno == EAGAIN || errno == EWOULDBLOCK) {
78
+ out.clear();
79
+ return ReadPacketStatus::NoData;
80
+ }
81
+ error = std::string("Read error: ") + strerror(errno);
82
+ return ReadPacketStatus::Error;
83
+ }
84
+ if (bytes_read == 0) {
85
+ out.clear();
86
+ return ReadPacketStatus::Closed;
87
+ }
88
+ if (bytes_read <= static_cast<ssize_t>(kUtunHeaderSize)) {
89
+ out.clear();
90
+ return ReadPacketStatus::NoData;
91
+ }
92
+
93
+ const auto payload_len = static_cast<size_t>(bytes_read - kUtunHeaderSize);
94
+ // Collapse the utun 4-byte address-family prefix in-place.
95
+ memmove(out.data(), out.data() + kUtunHeaderSize, payload_len);
96
+ out.resize(payload_len);
97
+ return ReadPacketStatus::Data;
98
+ }
99
+
100
+ ssize_t WritePacket(int fd, const uint8_t* data, size_t length, std::string& error) override {
101
+ std::vector<uint8_t> frame(length + kUtunHeaderSize);
102
+ uint32_t family = htonl(AF_INET6);
103
+ memcpy(frame.data(), &family, kUtunHeaderSize);
104
+ memcpy(frame.data() + kUtunHeaderSize, data, length);
105
+
106
+ ssize_t bytes_written = write(fd, frame.data(), frame.size());
107
+ if (bytes_written < 0) {
108
+ error = std::string("Write error: ") + strerror(errno);
109
+ return -1;
110
+ }
111
+
112
+ return bytes_written > static_cast<ssize_t>(kUtunHeaderSize)
113
+ ? bytes_written - static_cast<ssize_t>(kUtunHeaderSize)
114
+ : 0;
115
+ }
116
+
117
+ private:
118
+ static int ParseRequestedUtunUnit(const std::string& requested_name) {
119
+ if (requested_name.empty() || requested_name.find("utun") != 0) {
120
+ return 0;
121
+ }
122
+ try {
123
+ return std::stoi(requested_name.substr(4)) + 1;
124
+ } catch (...) {
125
+ return 0;
126
+ }
127
+ }
128
+
129
+ static bool ConnectFirstAvailableUnit(int fd, struct sockaddr_ctl& socket_addr, std::string& error) {
130
+ for (socket_addr.sc_unit = 1; socket_addr.sc_unit < 255; socket_addr.sc_unit++) {
131
+ if (connect(fd, reinterpret_cast<struct sockaddr*>(&socket_addr), sizeof(socket_addr)) == 0) {
132
+ return true;
133
+ }
134
+ if (errno != EBUSY) {
135
+ error = std::string("Failed to connect to utun control socket: ") + strerror(errno);
136
+ return false;
137
+ }
138
+ }
139
+
140
+ error = "Could not find an available utun device";
141
+ return false;
142
+ }
143
+ };
144
+
145
+ } // namespace
146
+
147
+ std::unique_ptr<TunPlatformBackend> CreatePlatformTunBackend() {
148
+ return std::make_unique<DarwinTunBackend>();
149
+ }
150
+
151
+ #endif
152
+
@@ -0,0 +1,94 @@
1
+ #ifdef __linux__
2
+
3
+ #include "tun_backend.h"
4
+
5
+ #include <errno.h>
6
+ #include <fcntl.h>
7
+ #include <string.h>
8
+ #include <sys/ioctl.h>
9
+ #include <sys/stat.h>
10
+ #include <unistd.h>
11
+
12
+ #include <linux/if.h>
13
+ #include <linux/if_tun.h>
14
+
15
+ namespace {
16
+ constexpr const char* kTunDevicePath = "/dev/net/tun";
17
+
18
+ class LinuxTunBackend : public TunPlatformBackend {
19
+ public:
20
+ bool OpenDevice(const std::string& requested_name, OpenResult& out, std::string& error) override {
21
+ struct stat statbuf;
22
+ if (stat(kTunDevicePath, &statbuf) != 0) {
23
+ 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).";
26
+ return false;
27
+ }
28
+
29
+ FileDescriptor temp_fd(open(kTunDevicePath, O_RDWR));
30
+ if (!temp_fd.is_valid()) {
31
+ 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.";
35
+ return false;
36
+ }
37
+
38
+ struct ifreq ifr;
39
+ memset(&ifr, 0, sizeof(ifr));
40
+ ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
41
+
42
+ if (!requested_name.empty()) {
43
+ strncpy(ifr.ifr_name, requested_name.c_str(), IFNAMSIZ - 1);
44
+ ifr.ifr_name[IFNAMSIZ - 1] = '\0';
45
+ }
46
+
47
+ if (ioctl(temp_fd.get(), TUNSETIFF, &ifr) < 0) {
48
+ error = std::string("Failed to configure TUN device: ") + strerror(errno);
49
+ return false;
50
+ }
51
+
52
+ out.fd = std::move(temp_fd);
53
+ out.interface_name = std::string(ifr.ifr_name);
54
+ return true;
55
+ }
56
+
57
+ ReadPacketStatus ReadPacket(int fd, size_t max_payload_size, std::vector<uint8_t>& out, std::string& error) override {
58
+ out.resize(max_payload_size);
59
+ ssize_t bytes_read = read(fd, out.data(), out.size());
60
+ if (bytes_read < 0) {
61
+ if (errno == EAGAIN || errno == EWOULDBLOCK) {
62
+ out.clear();
63
+ return ReadPacketStatus::NoData;
64
+ }
65
+ error = std::string("Read error: ") + strerror(errno);
66
+ return ReadPacketStatus::Error;
67
+ }
68
+ if (bytes_read == 0) {
69
+ out.clear();
70
+ return ReadPacketStatus::Closed;
71
+ }
72
+
73
+ out.resize(static_cast<size_t>(bytes_read));
74
+ return ReadPacketStatus::Data;
75
+ }
76
+
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);
79
+ if (bytes_written < 0) {
80
+ error = std::string("Write error: ") + strerror(errno);
81
+ return -1;
82
+ }
83
+ return bytes_written;
84
+ }
85
+ };
86
+
87
+ } // namespace
88
+
89
+ std::unique_ptr<TunPlatformBackend> CreatePlatformTunBackend() {
90
+ return std::make_unique<LinuxTunBackend>();
91
+ }
92
+
93
+ #endif
94
+
package/src/tuntap.cc CHANGED
@@ -1,82 +1,15 @@
1
1
  #include <napi.h>
2
- #include <unistd.h>
3
- #include <fcntl.h>
2
+ #include <uv.h>
3
+
4
4
  #include <string>
5
- #include <string.h>
6
- #include <errno.h>
7
- #include <sys/ioctl.h>
8
5
  #include <vector>
9
6
  #include <memory>
10
7
  #include <mutex>
11
8
  #include <atomic>
12
- #include <uv.h>
13
-
14
- #ifdef __APPLE__
15
- #include <sys/kern_control.h>
16
- #include <sys/socket.h>
17
- #include <sys/sys_domain.h>
18
- #include <net/if_utun.h>
19
- #include <netinet/in.h>
20
- #include <netinet6/in6_var.h>
21
- #define UTUN_CONTROL_NAME "com.apple.net.utun_control"
22
- #define UTUN_HEADER_SIZE 4
23
- #else
24
- #include <linux/if.h>
25
- #include <linux/if_tun.h>
26
- #include <sys/stat.h>
27
- #define UTUN_HEADER_SIZE 0
28
- #endif
29
-
30
- // RAII wrapper for file descriptors
31
- class FileDescriptor {
32
- private:
33
- int fd_;
34
-
35
- public:
36
- FileDescriptor() : fd_(-1) {}
37
- explicit FileDescriptor(int fd) : fd_(fd) {}
38
-
39
- ~FileDescriptor() {
40
- if (fd_ >= 0) {
41
- ::close(fd_);
42
- }
43
- }
44
-
45
- FileDescriptor(const FileDescriptor&) = delete;
46
- FileDescriptor& operator=(const FileDescriptor&) = delete;
47
-
48
- FileDescriptor(FileDescriptor&& other) noexcept : fd_(other.fd_) {
49
- other.fd_ = -1;
50
- }
51
-
52
- FileDescriptor& operator=(FileDescriptor&& other) noexcept {
53
- if (this != &other) {
54
- if (fd_ >= 0) {
55
- ::close(fd_);
56
- }
57
- fd_ = other.fd_;
58
- other.fd_ = -1;
59
- }
60
- return *this;
61
- }
9
+ #include <cstdio>
62
10
 
63
- int get() const { return fd_; }
64
-
65
- int release() {
66
- int temp = fd_;
67
- fd_ = -1;
68
- return temp;
69
- }
70
-
71
- bool is_valid() const { return fd_ >= 0; }
72
-
73
- void reset(int fd = -1) {
74
- if (fd_ >= 0) {
75
- ::close(fd_);
76
- }
77
- fd_ = fd;
78
- }
79
- };
11
+ #include "native/file_descriptor.h"
12
+ #include "native/tun_backend.h"
80
13
 
81
14
  class TunDevice : public Napi::ObjectWrap<TunDevice> {
82
15
  public:
@@ -99,6 +32,7 @@ private:
99
32
 
100
33
  FileDescriptor fd_;
101
34
  std::string name_;
35
+ std::unique_ptr<TunPlatformBackend> backend_;
102
36
  std::atomic<bool> is_open_;
103
37
  std::mutex device_mutex_;
104
38
 
@@ -113,6 +47,7 @@ private:
113
47
 
114
48
  Napi::FunctionReference TunDevice::constructor;
115
49
 
50
+ // Defines and exports the JS class constructor: new TunDevice(name?)
116
51
  Napi::Object TunDevice::Init(Napi::Env env, Napi::Object exports) {
117
52
  Napi::HandleScope scope(env);
118
53
 
@@ -133,8 +68,9 @@ Napi::Object TunDevice::Init(Napi::Env env, Napi::Object exports) {
133
68
  return exports;
134
69
  }
135
70
 
71
+ // Creates a TunDevice wrapper; optional first arg is requested interface name.
136
72
  TunDevice::TunDevice(const Napi::CallbackInfo& info)
137
- : Napi::ObjectWrap<TunDevice>(info), is_open_(false) {
73
+ : Napi::ObjectWrap<TunDevice>(info), backend_(CreatePlatformBackend()), is_open_(false) {
138
74
  Napi::Env env = info.Env();
139
75
  Napi::HandleScope scope(env);
140
76
 
@@ -143,18 +79,14 @@ TunDevice::TunDevice(const Napi::CallbackInfo& info)
143
79
  }
144
80
  }
145
81
 
82
+ // Ensures fd/poll resources are closed when object is destroyed.
146
83
  TunDevice::~TunDevice() {
147
84
  std::lock_guard<std::mutex> lock(device_mutex_);
148
85
  CloseInternal();
149
86
  }
150
87
 
151
- void TunDevice::CloseInternal() {
152
- if (is_open_.exchange(false)) {
153
- StopPolling();
154
- fd_.reset();
155
- }
156
- }
157
-
88
+ // JS: open() -> boolean
89
+ // Opens the backend device and configures the fd as non-blocking.
158
90
  Napi::Value TunDevice::Open(const Napi::CallbackInfo& info) {
159
91
  Napi::Env env = info.Env();
160
92
  std::lock_guard<std::mutex> lock(device_mutex_);
@@ -162,140 +94,33 @@ Napi::Value TunDevice::Open(const Napi::CallbackInfo& info) {
162
94
  if (is_open_) {
163
95
  return Napi::Boolean::New(env, true);
164
96
  }
165
-
166
- #ifdef __APPLE__
167
- // macOS: create utun interface via PF_SYSTEM control socket
168
- struct ctl_info ctlInfo;
169
- struct sockaddr_ctl sc;
170
-
171
- FileDescriptor temp_fd(socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL));
172
- if (!temp_fd.is_valid()) {
173
- Napi::Error::New(env, std::string("Failed to create control socket: ") + strerror(errno))
97
+ if (!backend_) {
98
+ Napi::Error::New(env, "Unsupported platform: no native TUN backend available")
174
99
  .ThrowAsJavaScriptException();
175
100
  return Napi::Boolean::New(env, false);
176
101
  }
177
102
 
178
- memset(&ctlInfo, 0, sizeof(ctlInfo));
179
- strncpy(ctlInfo.ctl_name, UTUN_CONTROL_NAME, sizeof(ctlInfo.ctl_name) - 1);
180
- ctlInfo.ctl_name[sizeof(ctlInfo.ctl_name) - 1] = '\0';
181
-
182
- if (ioctl(temp_fd.get(), CTLIOCGINFO, &ctlInfo) < 0) {
183
- Napi::Error::New(env, std::string("Failed to get utun control info: ") + strerror(errno))
184
- .ThrowAsJavaScriptException();
103
+ OpenResult result;
104
+ std::string error;
105
+ if (!backend_->OpenDevice(name_, result, error)) {
106
+ Napi::Error::New(env, error).ThrowAsJavaScriptException();
185
107
  return Napi::Boolean::New(env, false);
186
108
  }
187
109
 
188
- memset(&sc, 0, sizeof(sc));
189
- sc.sc_len = sizeof(sc);
190
- sc.sc_family = AF_SYSTEM;
191
- sc.ss_sysaddr = SYSPROTO_CONTROL;
192
- sc.sc_id = ctlInfo.ctl_id;
193
-
194
- // Parse utun number if provided, otherwise auto-select (utun0 = unit 1)
195
- int utun_unit = 0;
196
- if (!name_.empty() && name_.find("utun") == 0) {
197
- try {
198
- utun_unit = std::stoi(name_.substr(4)) + 1; // +1 because kernel uses unit=1 for utun0
199
- } catch(...) {
200
- utun_unit = 0;
201
- }
202
- }
203
-
204
- if (utun_unit > 0) {
205
- sc.sc_unit = utun_unit;
206
- if (connect(temp_fd.get(), (struct sockaddr*)&sc, sizeof(sc)) < 0) {
207
- Napi::Error::New(env, std::string("Failed to connect to utun with specified unit: ") + strerror(errno))
208
- .ThrowAsJavaScriptException();
209
- return Napi::Boolean::New(env, false);
210
- }
211
- } else {
212
- bool connected = false;
213
- for (sc.sc_unit = 1; sc.sc_unit < 255; sc.sc_unit++) {
214
- if (connect(temp_fd.get(), (struct sockaddr*)&sc, sizeof(sc)) == 0) {
215
- connected = true;
216
- break;
217
- } else if (errno != EBUSY) {
218
- Napi::Error::New(env, std::string("Failed to connect to utun control socket: ") + strerror(errno))
219
- .ThrowAsJavaScriptException();
220
- return Napi::Boolean::New(env, false);
221
- }
222
- }
223
-
224
- if (!connected) {
225
- Napi::Error::New(env, "Could not find an available utun device").ThrowAsJavaScriptException();
226
- return Napi::Boolean::New(env, false);
227
- }
228
- }
229
-
230
- char utunname[20];
231
- socklen_t utunname_len = sizeof(utunname);
232
- if (getsockopt(temp_fd.get(), SYSPROTO_CONTROL, UTUN_OPT_IFNAME, utunname, &utunname_len) < 0) {
233
- Napi::Error::New(env, std::string("Failed to get utun interface name: ") + strerror(errno))
234
- .ThrowAsJavaScriptException();
235
- return Napi::Boolean::New(env, false);
236
- }
237
-
238
- name_ = std::string(utunname);
239
-
240
- #else
241
- // Linux: create TUN device via /dev/net/tun
242
- struct stat statbuf;
243
- if (stat("/dev/net/tun", &statbuf) != 0) {
244
- Napi::Error::New(env,
245
- "TUN/TAP device not available: /dev/net/tun does not exist. "
246
- "Please ensure the TUN/TAP kernel module is loaded (modprobe tun).")
247
- .ThrowAsJavaScriptException();
110
+ if (!SetNonBlocking(result.fd.get(), error)) {
111
+ Napi::Error::New(env, error).ThrowAsJavaScriptException();
248
112
  return Napi::Boolean::New(env, false);
249
113
  }
250
114
 
251
- FileDescriptor temp_fd(open("/dev/net/tun", O_RDWR));
252
- if (!temp_fd.is_valid()) {
253
- Napi::Error::New(env,
254
- std::string("Failed to open /dev/net/tun: ") + strerror(errno) +
255
- ". This usually means you don't have sufficient permissions. "
256
- "Try running with sudo or add your user to the 'tun' group.")
257
- .ThrowAsJavaScriptException();
258
- return Napi::Boolean::New(env, false);
259
- }
260
-
261
- struct ifreq ifr;
262
- memset(&ifr, 0, sizeof(ifr));
263
- ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
264
-
265
- if (!name_.empty()) {
266
- strncpy(ifr.ifr_name, name_.c_str(), IFNAMSIZ - 1);
267
- ifr.ifr_name[IFNAMSIZ - 1] = '\0';
268
- }
269
-
270
- if (ioctl(temp_fd.get(), TUNSETIFF, &ifr) < 0) {
271
- Napi::Error::New(env, std::string("Failed to configure TUN device: ") + strerror(errno))
272
- .ThrowAsJavaScriptException();
273
- return Napi::Boolean::New(env, false);
274
- }
275
-
276
- name_ = std::string(ifr.ifr_name);
277
- #endif
278
-
279
- // Set non-blocking mode
280
- int flags = fcntl(temp_fd.get(), F_GETFL, 0);
281
- if (flags < 0) {
282
- Napi::Error::New(env, std::string("Failed to get file descriptor flags: ") + strerror(errno))
283
- .ThrowAsJavaScriptException();
284
- return Napi::Boolean::New(env, false);
285
- }
286
-
287
- if (fcntl(temp_fd.get(), F_SETFL, flags | O_NONBLOCK) < 0) {
288
- Napi::Error::New(env, std::string("Failed to set non-blocking mode: ") + strerror(errno))
289
- .ThrowAsJavaScriptException();
290
- return Napi::Boolean::New(env, false);
291
- }
292
-
293
- fd_ = std::move(temp_fd);
115
+ fd_ = std::move(result.fd);
116
+ name_ = result.interface_name;
294
117
  is_open_ = true;
295
118
 
296
119
  return Napi::Boolean::New(env, true);
297
120
  }
298
121
 
122
+ // JS: close() -> boolean
123
+ // Safely closes device resources; calling multiple times is allowed.
299
124
  Napi::Value TunDevice::Close(const Napi::CallbackInfo& info) {
300
125
  Napi::Env env = info.Env();
301
126
  std::lock_guard<std::mutex> lock(device_mutex_);
@@ -303,6 +128,8 @@ Napi::Value TunDevice::Close(const Napi::CallbackInfo& info) {
303
128
  return Napi::Boolean::New(env, true);
304
129
  }
305
130
 
131
+ // JS: read(bufferSize?) -> Buffer
132
+ // Reads one payload packet, or returns an empty Buffer when no data is available.
306
133
  Napi::Value TunDevice::Read(const Napi::CallbackInfo& info) {
307
134
  Napi::Env env = info.Env();
308
135
  std::lock_guard<std::mutex> lock(device_mutex_);
@@ -321,38 +148,22 @@ Napi::Value TunDevice::Read(const Napi::CallbackInfo& info) {
321
148
  }
322
149
  }
323
150
 
324
- #ifdef __APPLE__
325
- // macOS: reads include a 4-byte protocol family prefix that must be stripped
326
- std::vector<uint8_t> raw(buffer_size + 4);
327
- ssize_t n = read(fd_.get(), raw.data(), raw.size());
328
- if (n <= 0) {
329
- if (errno == EAGAIN || errno == EWOULDBLOCK) {
330
- return Napi::Buffer<uint8_t>::New(env, 0);
331
- }
332
- Napi::Error::New(env, std::string("Read error: ") + strerror(errno))
333
- .ThrowAsJavaScriptException();
151
+ std::vector<uint8_t> packet;
152
+ std::string error;
153
+ ReadPacketStatus read_status = backend_->ReadPacket(fd_.get(), buffer_size, packet, error);
154
+ if (read_status == ReadPacketStatus::Error) {
155
+ Napi::Error::New(env, error).ThrowAsJavaScriptException();
334
156
  return env.Null();
335
157
  }
336
- if (n <= 4) {
158
+ if (read_status == ReadPacketStatus::NoData || read_status == ReadPacketStatus::Closed) {
337
159
  return Napi::Buffer<uint8_t>::New(env, 0);
338
160
  }
339
- return Napi::Buffer<uint8_t>::Copy(env, raw.data() + 4, n - 4);
340
- #else
341
- // Linux: raw IP packets directly
342
- std::vector<uint8_t> raw(buffer_size);
343
- ssize_t n = read(fd_.get(), raw.data(), raw.size());
344
- if (n < 0) {
345
- if (errno == EAGAIN || errno == EWOULDBLOCK) {
346
- return Napi::Buffer<uint8_t>::New(env, 0);
347
- }
348
- Napi::Error::New(env, std::string("Read error: ") + strerror(errno))
349
- .ThrowAsJavaScriptException();
350
- return env.Null();
351
- }
352
- return Napi::Buffer<uint8_t>::Copy(env, raw.data(), n);
353
- #endif
161
+
162
+ return Napi::Buffer<uint8_t>::Copy(env, packet.data(), packet.size());
354
163
  }
355
164
 
165
+ // JS: write(buffer) -> number
166
+ // Writes one packet and returns payload bytes accepted by the backend.
356
167
  Napi::Value TunDevice::Write(const Napi::CallbackInfo& info) {
357
168
  Napi::Env env = info.Env();
358
169
  std::lock_guard<std::mutex> lock(device_mutex_);
@@ -371,41 +182,31 @@ Napi::Value TunDevice::Write(const Napi::CallbackInfo& info) {
371
182
  uint8_t* data = buffer.Data();
372
183
  size_t length = buffer.Length();
373
184
 
374
- #ifdef __APPLE__
375
- // macOS: prepend 4-byte AF_INET6 protocol family header
376
- std::vector<uint8_t> frame(length + 4);
377
- uint32_t family = htonl(AF_INET6);
378
- memcpy(frame.data(), &family, 4);
379
- memcpy(frame.data() + 4, data, length);
380
-
381
- ssize_t bytes_written = write(fd_.get(), frame.data(), frame.size());
382
- if (bytes_written < 0) {
383
- Napi::Error::New(env, std::string("Write error: ") + strerror(errno))
384
- .ThrowAsJavaScriptException();
385
- return Napi::Number::New(env, -1);
386
- }
387
- return Napi::Number::New(env, bytes_written > 4 ? bytes_written - 4 : 0);
388
- #else
389
- ssize_t bytes_written = write(fd_.get(), data, length);
185
+ std::string error;
186
+ ssize_t bytes_written = backend_->WritePacket(fd_.get(), data, length, error);
390
187
  if (bytes_written < 0) {
391
- Napi::Error::New(env, std::string("Write error: ") + strerror(errno))
392
- .ThrowAsJavaScriptException();
188
+ Napi::Error::New(env, error).ThrowAsJavaScriptException();
393
189
  return Napi::Number::New(env, -1);
394
190
  }
395
191
  return Napi::Number::New(env, bytes_written);
396
- #endif
397
192
  }
398
193
 
194
+ // JS: getName() -> string
195
+ // Returns the assigned interface name after open().
399
196
  Napi::Value TunDevice::GetName(const Napi::CallbackInfo& info) {
400
197
  std::lock_guard<std::mutex> lock(device_mutex_);
401
198
  return Napi::String::New(info.Env(), name_);
402
199
  }
403
200
 
201
+ // JS: getFd() -> number
202
+ // Returns the native file descriptor, or -1 before open()/after close().
404
203
  Napi::Value TunDevice::GetFd(const Napi::CallbackInfo& info) {
405
204
  std::lock_guard<std::mutex> lock(device_mutex_);
406
205
  return Napi::Number::New(info.Env(), fd_.get());
407
206
  }
408
207
 
208
+ // JS: startPolling(callback, bufferSize?) -> void
209
+ // Starts libuv polling and invokes callback with packet payload Buffers.
409
210
  Napi::Value TunDevice::StartPolling(const Napi::CallbackInfo& info) {
410
211
  Napi::Env env = info.Env();
411
212
  std::lock_guard<std::mutex> lock(device_mutex_);
@@ -474,6 +275,23 @@ Napi::Value TunDevice::StartPolling(const Napi::CallbackInfo& info) {
474
275
  return env.Undefined();
475
276
  }
476
277
 
278
+ // Node-API module entrypoint.
279
+ Napi::Object Init(Napi::Env env, Napi::Object exports) {
280
+ return TunDevice::Init(env, exports);
281
+ }
282
+
283
+ NODE_API_MODULE(tuntap, Init)
284
+ // #endregion
285
+
286
+ // #region Private implementation details
287
+
288
+ void TunDevice::CloseInternal() {
289
+ if (is_open_.exchange(false)) {
290
+ StopPolling();
291
+ fd_.reset();
292
+ }
293
+ }
294
+
477
295
  void TunDevice::StopPolling() {
478
296
  if (poll_handle_) {
479
297
  uv_poll_stop(poll_handle_);
@@ -508,33 +326,29 @@ void TunDevice::PollCallback(uv_poll_t* handle, int status, int events) {
508
326
  return;
509
327
  }
510
328
 
511
- std::vector<uint8_t> buffer(self->poll_buffer_size_ + UTUN_HEADER_SIZE);
512
- ssize_t bytes_read = read(self->fd_.get(), buffer.data(), buffer.size());
513
-
514
- if (bytes_read == 0) {
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());
515
335
  self->StopPolling();
516
336
  return;
517
337
  }
518
- if (bytes_read < 0) {
519
- if (errno != EAGAIN && errno != EWOULDBLOCK) {
520
- fprintf(stderr, "tuntap read error: %s\n", strerror(errno));
521
- self->StopPolling();
522
- }
338
+ // EOF/peer close: device is no longer readable, so tear down polling.
339
+ if (read_status == ReadPacketStatus::Closed) {
340
+ self->StopPolling();
523
341
  return;
524
342
  }
525
-
526
- if (bytes_read > UTUN_HEADER_SIZE) {
527
- self->tsfn_.BlockingCall(
528
- [buf = std::move(buffer), bytes_read](Napi::Env env, Napi::Function jsCallback) {
529
- if (env == nullptr || jsCallback.IsEmpty()) return;
530
- jsCallback.Call({ Napi::Buffer<uint8_t>::Copy(env, buf.data() + UTUN_HEADER_SIZE, bytes_read - UTUN_HEADER_SIZE) });
531
- }
532
- );
343
+ // Transient empty read (e.g. EAGAIN): keep poll active and wait for next event.
344
+ if (read_status == ReadPacketStatus::NoData) {
345
+ return;
533
346
  }
534
- }
535
347
 
536
- Napi::Object Init(Napi::Env env, Napi::Object exports) {
537
- return TunDevice::Init(env, exports);
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
+ );
538
354
  }
539
-
540
- NODE_API_MODULE(tuntap, Init)