appium-ios-tuntap 0.1.10 → 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,15 @@
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
+
7
+ ## [0.2.0](https://github.com/appium/appium-ios-tuntap/compare/v0.1.10...v0.2.0) (2026-04-13)
8
+
9
+ ### Features
10
+
11
+ * Supply the package with prebuilt addon ([#31](https://github.com/appium/appium-ios-tuntap/issues/31)) ([c475d1c](https://github.com/appium/appium-ios-tuntap/commit/c475d1ce89a4b44d0ebed90f9b4b8fc519241079))
12
+
1
13
  ## [0.1.10](https://github.com/appium/appium-ios-tuntap/compare/v0.1.9...v0.1.10) (2026-04-13)
2
14
 
3
15
  ### Bug Fixes
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/lib/TunTap.js CHANGED
@@ -1,14 +1,18 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { isIPv6 } from 'node:net';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
3
5
  import { TunTapDeviceError, TunTapError, TunTapPermissionError } from './errors.js';
4
6
  import { log } from './logger.js';
5
7
  import { createTunTapPlatform } from './platform/create-platform.js';
6
8
  const require = createRequire(import.meta.url);
9
+ /** Package root (contains binding.gyp, prebuilds/, or build/ after compile). */
10
+ const pkgRoot = path.join(fileURLToPath(new URL('.', import.meta.url)), '..');
7
11
  const DEFAULT_READ_BUFFER_SIZE = 4096;
8
12
  const MAX_BUFFER_SIZE = 0xffff; // 65535
9
13
  const DEFAULT_MTU = 1500;
10
14
  const MIN_MTU = 1280;
11
- const nativeTuntap = require('../build/Release/tuntap.node');
15
+ const nativeTuntap = require('node-gyp-build')(pkgRoot);
12
16
  /**
13
17
  * Validates an IPv6 route destination (address with optional CIDR prefix).
14
18
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-tuntap",
3
- "version": "0.1.10",
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",
@@ -9,19 +9,22 @@
9
9
  "clean": "rm -rf build node_modules lib",
10
10
  "build": "npx tsc",
11
11
  "build:addon": "node-gyp rebuild",
12
+ "build:prebuilds": "prebuildify --napi --strip",
12
13
  "lint": "eslint .",
13
14
  "lint:fix": "npm run lint -- --fix",
14
15
  "format": "prettier -w ./src ./test",
15
16
  "format:check": "prettier --check ./src ./test",
16
- "prepare": "npm run build:addon && npm run build",
17
+ "install": "node-gyp-build",
18
+ "prepare": "npm run build",
17
19
  "test": "sudo npm run test:integration && npm run test:unit",
18
20
  "test:unit": "sudo npx mocha 'test/tuntap-unit.spec.mjs' --exit --timeout 2m",
19
21
  "test:integration": "sudo npx mocha 'test/tuntap-integration.spec.mjs' --exit --timeout 2m"
20
22
  },
21
23
  "files": [
22
24
  "src/tuntap.cc",
25
+ "src/native",
23
26
  "lib",
24
- "build",
27
+ "prebuilds",
25
28
  "binding.gyp",
26
29
  "package.json",
27
30
  "README.md",
@@ -45,6 +48,7 @@
45
48
  "dependencies": {
46
49
  "@appium/support": "^7.0.0-rc.1",
47
50
  "node-addon-api": "^8.5.0",
51
+ "node-gyp-build": "^4.8.4",
48
52
  "typescript": "^6.0.2"
49
53
  },
50
54
  "devDependencies": {
@@ -54,7 +58,8 @@
54
58
  "@types/node": "^25.0.1",
55
59
  "conventional-changelog-conventionalcommits": "^9.0.0",
56
60
  "semantic-release": "^25.0.2",
57
- "prettier": "^3.0.0"
61
+ "prettier": "^3.0.0",
62
+ "prebuildify": "^6.0.1"
58
63
  },
59
64
  "prettier": {
60
65
  "bracketSpacing": false,
@@ -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
+