appium-ios-tuntap 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.3.0](https://github.com/appium/appium-ios-tuntap/compare/v0.2.5...v0.3.0) (2026-05-22)
2
+
3
+ ### Features
4
+
5
+ * add Windows (WinTun) native backend ([#43](https://github.com/appium/appium-ios-tuntap/issues/43)) ([565b4c1](https://github.com/appium/appium-ios-tuntap/commit/565b4c1cfd2ddf4956ed32ade4f8208cd0d4f0f6)), closes [#ifdef](https://github.com/appium/appium-ios-tuntap/issues/ifdef)
6
+
1
7
  ## [0.2.5](https://github.com/appium/appium-ios-tuntap/compare/v0.2.4...v0.2.5) (2026-05-14)
2
8
 
3
9
  ### Code Refactoring
package/binding.gyp CHANGED
@@ -20,7 +20,7 @@
20
20
  "-Wno-unused-parameter",
21
21
  "-fPIC"
22
22
  ],
23
- "cflags_cc": [
23
+ "cflags_cc": [
24
24
  "-std=c++17",
25
25
  "-Wno-vla-extension",
26
26
  "-O3",
@@ -48,7 +48,7 @@
48
48
  ]
49
49
  },
50
50
  "msvs_settings": {
51
- "VCCLCompilerTool": {
51
+ "VCCLCompilerTool": {
52
52
  "ExceptionHandling": 1,
53
53
  "AdditionalOptions": [
54
54
  "/std:c++17",
@@ -56,7 +56,7 @@
56
56
  ]
57
57
  }
58
58
  },
59
- "defines": [
59
+ "defines": [
60
60
  "NAPI_CPP_EXCEPTIONS",
61
61
  "NAPI_VERSION=8"
62
62
  ],
@@ -89,6 +89,22 @@
89
89
  "-framework", "CoreFoundation"
90
90
  ]
91
91
  }
92
+ }],
93
+ ["OS=='win'", {
94
+ "sources": [
95
+ "src/native/handle.cc",
96
+ "src/native/wintun_loader.cc",
97
+ "src/native/tun_backend_windows.cc"
98
+ ],
99
+ "libraries": [
100
+ "iphlpapi.lib",
101
+ "ws2_32.lib"
102
+ ],
103
+ "defines": [
104
+ "_WIN32_WINNT=0x0A00",
105
+ "WIN32_LEAN_AND_MEAN",
106
+ "NOMINMAX"
107
+ ]
92
108
  }]
93
109
  ]
94
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-ios-tuntap",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Native TUN/TAP interface module for Node.js",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -0,0 +1,59 @@
1
+ #ifdef _WIN32
2
+
3
+ #include "handle.h"
4
+
5
+ namespace {
6
+
7
+ bool IsRealHandle(HANDLE handle) {
8
+ return handle != nullptr && handle != INVALID_HANDLE_VALUE;
9
+ }
10
+
11
+ } // namespace
12
+
13
+ Handle::Handle() : handle_(nullptr) {}
14
+
15
+ Handle::Handle(HANDLE handle) : handle_(handle) {}
16
+
17
+ Handle::~Handle() {
18
+ if (IsRealHandle(handle_)) {
19
+ ::CloseHandle(handle_);
20
+ }
21
+ }
22
+
23
+ Handle::Handle(Handle&& other) noexcept : handle_(other.handle_) {
24
+ other.handle_ = nullptr;
25
+ }
26
+
27
+ Handle& Handle::operator=(Handle&& other) noexcept {
28
+ if (this != &other) {
29
+ if (IsRealHandle(handle_)) {
30
+ ::CloseHandle(handle_);
31
+ }
32
+ handle_ = other.handle_;
33
+ other.handle_ = nullptr;
34
+ }
35
+ return *this;
36
+ }
37
+
38
+ HANDLE Handle::get() const {
39
+ return handle_;
40
+ }
41
+
42
+ HANDLE Handle::release() {
43
+ HANDLE temp = handle_;
44
+ handle_ = nullptr;
45
+ return temp;
46
+ }
47
+
48
+ bool Handle::is_valid() const {
49
+ return IsRealHandle(handle_);
50
+ }
51
+
52
+ void Handle::reset(HANDLE handle) {
53
+ if (IsRealHandle(handle_)) {
54
+ ::CloseHandle(handle_);
55
+ }
56
+ handle_ = handle;
57
+ }
58
+
59
+ #endif
@@ -0,0 +1,30 @@
1
+ #pragma once
2
+
3
+ #ifdef _WIN32
4
+
5
+ #include <windows.h>
6
+
7
+ // RAII wrapper for a Win32 `HANDLE`. Mirrors `FileDescriptor` so backends can
8
+ // rely on the same lifetime semantics regardless of OS.
9
+ class Handle {
10
+ public:
11
+ Handle();
12
+ explicit Handle(HANDLE handle);
13
+ ~Handle();
14
+
15
+ Handle(const Handle&) = delete;
16
+ Handle& operator=(const Handle&) = delete;
17
+
18
+ Handle(Handle&& other) noexcept;
19
+ Handle& operator=(Handle&& other) noexcept;
20
+
21
+ HANDLE get() const;
22
+ HANDLE release();
23
+ bool is_valid() const;
24
+ void reset(HANDLE handle = nullptr);
25
+
26
+ private:
27
+ HANDLE handle_;
28
+ };
29
+
30
+ #endif
@@ -1,7 +1,7 @@
1
1
  #pragma once
2
2
 
3
- #if !defined(__linux__) && !defined(__APPLE__)
4
- #error "appium-ios-tuntap native addon supports only Linux and macOS"
3
+ #if !defined(__linux__) && !defined(__APPLE__) && !defined(_WIN32)
4
+ #error "appium-ios-tuntap native addon supports only Linux, macOS, and Windows"
5
5
  #endif
6
6
 
7
7
  #include <cstddef>
@@ -27,9 +27,25 @@ enum class ReadPacketStatus {
27
27
  Error,
28
28
  };
29
29
 
30
+ /**
31
+ * Backend abstraction that hides OS-specific TUN device handling from the
32
+ * N-API surface.
33
+ *
34
+ * Each backend owns:
35
+ * - its native handle (POSIX file descriptor or Win32 `HANDLE`)
36
+ * - the receive-loop primitive it needs (libuv `uv_poll_t` on POSIX, a
37
+ * dedicated worker thread plus a Win32 event on Windows)
38
+ */
30
39
  class TunPlatformBackend {
31
40
  public:
41
+ // Invoked once per packet read by the receive loop. Always called on a
42
+ // background thread (libuv loop thread on POSIX, worker thread on Windows);
43
+ // the caller in `tuntap.cc` is responsible for marshalling onto the JS
44
+ // thread via `Napi::ThreadSafeFunction`.
32
45
  using PacketCallback = std::function<void(std::vector<uint8_t>)>;
46
+
47
+ // Invoked at most once when the receive loop encounters a fatal error and
48
+ // stops. The receive loop must not deliver any further packets afterwards.
33
49
  using ErrorCallback = std::function<void(const std::string&)>;
34
50
 
35
51
  virtual ~TunPlatformBackend() = default;
@@ -47,6 +63,8 @@ public:
47
63
  size_t length,
48
64
  std::string& error) = 0;
49
65
 
66
+ // Begin asynchronous packet delivery. `loop` is supplied by Node-API and is
67
+ // used by POSIX backends for `uv_poll_init`; Windows ignores it.
50
68
  virtual bool StartReceiveLoop(uv_loop_t* loop,
51
69
  size_t buffer_size,
52
70
  PacketCallback on_packet,
@@ -54,6 +72,8 @@ public:
54
72
  std::string& error) = 0;
55
73
  virtual void StopReceiveLoop() = 0;
56
74
 
75
+ // Returns the underlying POSIX file descriptor when one exists. Backends
76
+ // without a numeric fd (e.g. Wintun on Windows) return `-1`.
57
77
  virtual int GetNativeFd() const { return -1; }
58
78
  };
59
79
 
@@ -0,0 +1,370 @@
1
+ #ifdef _WIN32
2
+
3
+ #include "tun_backend.h"
4
+
5
+ #include <windows.h>
6
+
7
+ #include <atomic>
8
+ #include <chrono>
9
+ #include <cstring>
10
+ #include <sstream>
11
+ #include <thread>
12
+ #include <utility>
13
+ #include <vector>
14
+
15
+ #include "handle.h"
16
+ #include "wintun_loader.h"
17
+
18
+ namespace {
19
+
20
+ constexpr LPCWSTR kTunnelType = L"AppiumTunTap";
21
+ constexpr DWORD kSessionCapacity = 0x400000; // 4 MiB; must be power of two.
22
+
23
+ // WinTun adapter names are limited to MAX_ADAPTER_NAME-1 wide chars (~127).
24
+ // We stay well under that.
25
+ std::wstring BuildDefaultAdapterName() {
26
+ std::wostringstream oss;
27
+ oss << L"appium-tun" << ::GetCurrentProcessId() << L"-" << ::GetTickCount();
28
+ return oss.str();
29
+ }
30
+
31
+ class WindowsTunBackend : public TunPlatformBackend {
32
+ public:
33
+ WindowsTunBackend() = default;
34
+ ~WindowsTunBackend() override {
35
+ StopReceiveLoop();
36
+ EndSessionInternal();
37
+ CloseAdapterInternal();
38
+ }
39
+
40
+ WindowsTunBackend(const WindowsTunBackend&) = delete;
41
+ WindowsTunBackend& operator=(const WindowsTunBackend&) = delete;
42
+ WindowsTunBackend(WindowsTunBackend&&) = delete;
43
+ WindowsTunBackend& operator=(WindowsTunBackend&&) = delete;
44
+
45
+ bool OpenDevice(const std::string& requested_name,
46
+ std::string& out_interface_name,
47
+ std::string& error) override {
48
+ auto& api = WintunApi::Instance();
49
+ if (!api.Load(error)) {
50
+ return false;
51
+ }
52
+
53
+ std::wstring adapter_name =
54
+ requested_name.empty() ? BuildDefaultAdapterName() : Utf8ToUtf16(requested_name);
55
+ if (adapter_name.empty()) {
56
+ error = "Failed to encode adapter name as UTF-16";
57
+ return false;
58
+ }
59
+
60
+ // Creating or opening a WinTun adapter requires the process to run with
61
+ // administrator privileges (elevated). Without elevation `CreateAdapter`
62
+ // fails with ERROR_ACCESS_DENIED, which `FormatLastError` surfaces in the
63
+ // error string below. This mirrors the root (EUID 0) requirement of the
64
+ // POSIX backends.
65
+ //
66
+ // Prefer creating a fresh adapter so `WintunCloseAdapter` on shutdown
67
+ // also tears down the kernel object. Falling back to `OpenAdapter`
68
+ // handles the "leftover from a crashed process" case where the adapter
69
+ // already exists; that adapter will likewise be removed on close, which
70
+ // is the desired behavior — we do not want stale adapters to accumulate.
71
+ adapter_ = api.CreateAdapter(adapter_name.c_str(), kTunnelType, nullptr);
72
+ if (!adapter_) {
73
+ DWORD created_err = ::GetLastError();
74
+ adapter_ = api.OpenAdapter(adapter_name.c_str());
75
+ if (!adapter_) {
76
+ DWORD opened_err = ::GetLastError();
77
+ error = "Failed to create or open WinTun adapter: create failed with " +
78
+ FormatLastError(created_err) + "; open failed with " +
79
+ FormatLastError(opened_err);
80
+ return false;
81
+ }
82
+ }
83
+
84
+ session_ = api.StartSession(adapter_, kSessionCapacity);
85
+ if (!session_) {
86
+ error = "Failed to start WinTun session: " + FormatLastError(::GetLastError());
87
+ CloseAdapterInternal();
88
+ return false;
89
+ }
90
+
91
+ read_event_ = api.GetReadWaitEvent(session_);
92
+ if (!read_event_) {
93
+ error = "Failed to acquire WinTun read-wait event: " + FormatLastError(::GetLastError());
94
+ EndSessionInternal();
95
+ CloseAdapterInternal();
96
+ return false;
97
+ }
98
+
99
+ interface_name_ = Utf16ToUtf8(adapter_name);
100
+ if (interface_name_.empty()) {
101
+ error = "Failed to encode adapter name as UTF-8";
102
+ EndSessionInternal();
103
+ CloseAdapterInternal();
104
+ return false;
105
+ }
106
+
107
+ out_interface_name = interface_name_;
108
+ return true;
109
+ }
110
+
111
+ void CloseDevice() override {
112
+ StopReceiveLoop();
113
+ EndSessionInternal();
114
+ CloseAdapterInternal();
115
+ interface_name_.clear();
116
+ }
117
+
118
+ bool IsOpen() const override { return session_ != nullptr; }
119
+
120
+ ReadPacketStatus ReadPacket(size_t max_payload_size,
121
+ std::vector<uint8_t>& out,
122
+ std::string& error) override {
123
+ if (!session_) {
124
+ error = "Device not open";
125
+ return ReadPacketStatus::Error;
126
+ }
127
+
128
+ auto& api = WintunApi::Instance();
129
+ DWORD packet_size = 0;
130
+ BYTE* packet = api.ReceivePacket(session_, &packet_size);
131
+ if (!packet) {
132
+ DWORD err = ::GetLastError();
133
+ switch (err) {
134
+ case ERROR_NO_MORE_ITEMS:
135
+ out.clear();
136
+ return ReadPacketStatus::NoData;
137
+ case ERROR_HANDLE_EOF:
138
+ out.clear();
139
+ return ReadPacketStatus::Closed;
140
+ default:
141
+ out.clear();
142
+ error = "WintunReceivePacket failed: " + FormatLastError(err);
143
+ return ReadPacketStatus::Error;
144
+ }
145
+ }
146
+
147
+ const size_t copy_len = static_cast<size_t>(packet_size) > max_payload_size
148
+ ? max_payload_size
149
+ : static_cast<size_t>(packet_size);
150
+ out.assign(packet, packet + copy_len);
151
+ api.ReleaseReceivePacket(session_, packet);
152
+ return ReadPacketStatus::Data;
153
+ }
154
+
155
+ ssize_t WritePacket(const uint8_t* data,
156
+ size_t length,
157
+ std::string& error) override {
158
+ if (!session_) {
159
+ error = "Device not open";
160
+ return -1;
161
+ }
162
+ if (length == 0) {
163
+ return 0;
164
+ }
165
+ if (length > WINTUN_MAX_IP_PACKET_SIZE) {
166
+ error = "Packet exceeds WINTUN_MAX_IP_PACKET_SIZE";
167
+ return -1;
168
+ }
169
+
170
+ auto& api = WintunApi::Instance();
171
+ BYTE* slot = api.AllocateSendPacket(session_, static_cast<DWORD>(length));
172
+ if (!slot) {
173
+ DWORD err = ::GetLastError();
174
+ if (err == ERROR_HANDLE_EOF) {
175
+ error = "WinTun adapter is terminating";
176
+ } else if (err == ERROR_BUFFER_OVERFLOW) {
177
+ error = "WinTun send-ring is full";
178
+ } else {
179
+ error = "WintunAllocateSendPacket failed: " + FormatLastError(err);
180
+ }
181
+ return -1;
182
+ }
183
+
184
+ std::memcpy(slot, data, length);
185
+ api.SendPacket(session_, slot);
186
+ return static_cast<ssize_t>(length);
187
+ }
188
+
189
+ // The worker thread can begin invoking `on_packet`/`on_error` immediately
190
+ // after this returns; callers must therefore not assume callbacks fire
191
+ // only after the function returns. In practice the callbacks defined in
192
+ // `tuntap.cc` either marshal to libuv via TSFN (`on_packet`) or take
193
+ // `device_mutex_` (`on_error`). Brief contention on `device_mutex_` is
194
+ // expected and resolves on the order of microseconds because
195
+ // `std::thread`'s constructor does not block on the worker reaching its
196
+ // first instruction. There is no deadlock risk because the calling JS
197
+ // thread releases the lock as soon as `StartPolling` returns.
198
+ bool StartReceiveLoop(uv_loop_t* /*loop*/,
199
+ size_t buffer_size,
200
+ PacketCallback on_packet,
201
+ ErrorCallback on_error,
202
+ std::string& error) override {
203
+ if (!session_) {
204
+ error = "Device not open";
205
+ return false;
206
+ }
207
+ if (buffer_size == 0) {
208
+ error = "Invalid receive-loop parameters";
209
+ return false;
210
+ }
211
+ if (worker_running_.load()) {
212
+ error = "Receive loop already started";
213
+ return false;
214
+ }
215
+
216
+ quit_event_.reset(::CreateEventW(nullptr, /*manualReset=*/TRUE,
217
+ /*initialState=*/FALSE, nullptr));
218
+ if (!quit_event_.is_valid()) {
219
+ error = "Failed to create quit event: " + FormatLastError(::GetLastError());
220
+ return false;
221
+ }
222
+
223
+ worker_running_.store(true);
224
+ try {
225
+ worker_ = std::thread(&WindowsTunBackend::WorkerMain, this, buffer_size,
226
+ std::move(on_packet), std::move(on_error));
227
+ } catch (const std::system_error& sysErr) {
228
+ worker_running_.store(false);
229
+ quit_event_.reset();
230
+ error = std::string("Failed to spawn receive thread: ") + sysErr.what();
231
+ return false;
232
+ }
233
+ return true;
234
+ }
235
+
236
+ void StopReceiveLoop() override {
237
+ if (!worker_.joinable()) {
238
+ // Either never started or already cleaned up. Reset the event in case
239
+ // it was created without ever spawning a thread.
240
+ quit_event_.reset();
241
+ return;
242
+ }
243
+ worker_running_.store(false);
244
+ if (quit_event_.is_valid()) {
245
+ ::SetEvent(quit_event_.get());
246
+ }
247
+ worker_.join();
248
+ quit_event_.reset();
249
+ }
250
+
251
+ // WinTun exposes no POSIX file descriptor: its readable object is a Win32
252
+ // event `HANDLE`, not a numeric fd. Always -1 — the N-API layer treats -1
253
+ // as "no pollable fd" and drives delivery through `StartReceiveLoop`.
254
+ int GetNativeFd() const override { return -1; }
255
+
256
+ private:
257
+ static std::string Utf16ToUtf8(const std::wstring& utf16) {
258
+ if (utf16.empty()) {
259
+ return std::string();
260
+ }
261
+ int len = ::WideCharToMultiByte(CP_UTF8, 0, utf16.c_str(),
262
+ static_cast<int>(utf16.size()), nullptr, 0,
263
+ nullptr, nullptr);
264
+ if (len <= 0) {
265
+ return std::string();
266
+ }
267
+ std::string out(static_cast<size_t>(len), '\0');
268
+ ::WideCharToMultiByte(CP_UTF8, 0, utf16.c_str(),
269
+ static_cast<int>(utf16.size()), out.data(), len,
270
+ nullptr, nullptr);
271
+ return out;
272
+ }
273
+
274
+ void EndSessionInternal() {
275
+ if (session_) {
276
+ WintunApi::Instance().EndSession(session_);
277
+ session_ = nullptr;
278
+ }
279
+ read_event_ = nullptr;
280
+ }
281
+
282
+ void CloseAdapterInternal() {
283
+ if (adapter_) {
284
+ WintunApi::Instance().CloseAdapter(adapter_);
285
+ adapter_ = nullptr;
286
+ }
287
+ }
288
+
289
+ void WorkerMain(size_t buffer_size,
290
+ PacketCallback on_packet,
291
+ ErrorCallback on_error) {
292
+ auto& api = WintunApi::Instance();
293
+ HANDLE wait_handles[2] = {read_event_, quit_event_.get()};
294
+
295
+ while (worker_running_.load()) {
296
+ // Drain everything available before going back to wait. WinTun's
297
+ // read-wait event is auto-reset on signal, so we must consume all
298
+ // queued packets before re-arming.
299
+ bool drained = false;
300
+ while (worker_running_.load()) {
301
+ DWORD packet_size = 0;
302
+ BYTE* packet = api.ReceivePacket(session_, &packet_size);
303
+ if (packet) {
304
+ const size_t copy_len = static_cast<size_t>(packet_size) > buffer_size
305
+ ? buffer_size
306
+ : static_cast<size_t>(packet_size);
307
+ std::vector<uint8_t> data(packet, packet + copy_len);
308
+ api.ReleaseReceivePacket(session_, packet);
309
+ if (on_packet) {
310
+ on_packet(std::move(data));
311
+ }
312
+ continue;
313
+ }
314
+
315
+ DWORD err = ::GetLastError();
316
+ if (err == ERROR_NO_MORE_ITEMS) {
317
+ drained = true;
318
+ break;
319
+ }
320
+ if (err == ERROR_HANDLE_EOF) {
321
+ if (on_error) {
322
+ on_error("WinTun adapter terminating");
323
+ }
324
+ worker_running_.store(false);
325
+ return;
326
+ }
327
+ if (on_error) {
328
+ on_error("WintunReceivePacket failed: " + FormatLastError(err));
329
+ }
330
+ worker_running_.store(false);
331
+ return;
332
+ }
333
+
334
+ if (!worker_running_.load()) {
335
+ return;
336
+ }
337
+ if (!drained) {
338
+ continue;
339
+ }
340
+
341
+ DWORD wait = ::WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE);
342
+ if (wait == WAIT_OBJECT_0 + 1) {
343
+ // quit_event signalled.
344
+ return;
345
+ }
346
+ if (wait != WAIT_OBJECT_0) {
347
+ if (on_error) {
348
+ on_error("WaitForMultipleObjects failed: " + FormatLastError(::GetLastError()));
349
+ }
350
+ return;
351
+ }
352
+ }
353
+ }
354
+
355
+ WINTUN_ADAPTER_HANDLE adapter_ = nullptr;
356
+ WINTUN_SESSION_HANDLE session_ = nullptr;
357
+ HANDLE read_event_ = nullptr; // Owned by `session_`; do not CloseHandle.
358
+ Handle quit_event_;
359
+ std::thread worker_;
360
+ std::atomic<bool> worker_running_{false};
361
+ std::string interface_name_;
362
+ };
363
+
364
+ } // namespace
365
+
366
+ std::unique_ptr<TunPlatformBackend> CreatePlatformBackend() {
367
+ return std::make_unique<WindowsTunBackend>();
368
+ }
369
+
370
+ #endif
@@ -0,0 +1,192 @@
1
+ #ifdef _WIN32
2
+
3
+ #include "wintun_loader.h"
4
+
5
+ #include <vector>
6
+
7
+ namespace {
8
+
9
+ constexpr LPCWSTR kWintunDllName = L"wintun.dll";
10
+
11
+ // Returns the directory that contains the addon module that this code is
12
+ // linked into. Used to locate `wintun.dll` shipped next to `tuntap.node`.
13
+ std::wstring GetAddonDirectory() {
14
+ HMODULE module = nullptr;
15
+ // Pass the address of any function in this translation unit so the resolver
16
+ // returns the addon's own module rather than the host process executable.
17
+ if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
18
+ GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
19
+ reinterpret_cast<LPCWSTR>(&GetAddonDirectory),
20
+ &module)) {
21
+ return std::wstring();
22
+ }
23
+
24
+ std::vector<wchar_t> buffer(MAX_PATH);
25
+ for (;;) {
26
+ DWORD len = GetModuleFileNameW(module, buffer.data(),
27
+ static_cast<DWORD>(buffer.size()));
28
+ if (len == 0) {
29
+ return std::wstring();
30
+ }
31
+ if (len < buffer.size()) {
32
+ buffer.resize(len);
33
+ break;
34
+ }
35
+ buffer.resize(buffer.size() * 2);
36
+ }
37
+
38
+ std::wstring path(buffer.begin(), buffer.end());
39
+ size_t pos = path.find_last_of(L"\\/");
40
+ if (pos == std::wstring::npos) {
41
+ return std::wstring();
42
+ }
43
+ path.resize(pos);
44
+ return path;
45
+ }
46
+
47
+ template <typename T>
48
+ bool Resolve(HMODULE module, const char* name, T& out, std::string& error) {
49
+ out = reinterpret_cast<T>(::GetProcAddress(module, name));
50
+ if (!out) {
51
+ error = std::string("Failed to resolve wintun.dll export: ") + name;
52
+ return false;
53
+ }
54
+ return true;
55
+ }
56
+
57
+ } // namespace
58
+
59
+ WintunApi& WintunApi::Instance() {
60
+ static WintunApi instance;
61
+ return instance;
62
+ }
63
+
64
+ bool WintunApi::Load(std::string& error) {
65
+ std::lock_guard<std::mutex> lock(load_mutex_);
66
+ if (loaded_) {
67
+ return true;
68
+ }
69
+
70
+ // Try addon-directory first so `wintun.dll` shipped next to `tuntap.node`
71
+ // wins over a stale system copy.
72
+ std::wstring addon_dir = GetAddonDirectory();
73
+ if (!addon_dir.empty()) {
74
+ std::wstring local = addon_dir + L"\\" + kWintunDllName;
75
+ if (TryLoadFrom(local.c_str())) {
76
+ return ResolveEntryPoints(error);
77
+ }
78
+ }
79
+
80
+ // Fall back to the OS-default search list (Application dir, System32, …)
81
+ // by passing only the file name.
82
+ if (TryLoadFrom(kWintunDllName)) {
83
+ return ResolveEntryPoints(error);
84
+ }
85
+
86
+ error = std::string("Failed to load wintun.dll: ") + FormatLastError(::GetLastError()) +
87
+ ". Make sure wintun.dll is shipped next to tuntap.node or installed system-wide.";
88
+ return false;
89
+ }
90
+
91
+ bool WintunApi::TryLoadFrom(LPCWSTR path) {
92
+ // `LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR` lets a DLL loaded from an explicit
93
+ // path resolve its own dependencies from its own directory. WinTun has
94
+ // no third-party dependencies today, but this is the documented-safe
95
+ // flag set for loading by absolute path and is free to include.
96
+ module_ = ::LoadLibraryExW(path, nullptr,
97
+ LOAD_LIBRARY_SEARCH_APPLICATION_DIR |
98
+ LOAD_LIBRARY_SEARCH_SYSTEM32 |
99
+ LOAD_LIBRARY_SEARCH_USER_DIRS |
100
+ LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR);
101
+ return module_ != nullptr;
102
+ }
103
+
104
+ bool WintunApi::ResolveEntryPoints(std::string& error) {
105
+ if (!module_) {
106
+ error = "wintun.dll module handle is null";
107
+ return false;
108
+ }
109
+
110
+ if (!Resolve(module_, "WintunCreateAdapter", CreateAdapter, error) ||
111
+ !Resolve(module_, "WintunOpenAdapter", OpenAdapter, error) ||
112
+ !Resolve(module_, "WintunCloseAdapter", CloseAdapter, error) ||
113
+ !Resolve(module_, "WintunGetRunningDriverVersion", GetRunningDriverVersion, error) ||
114
+ !Resolve(module_, "WintunStartSession", StartSession, error) ||
115
+ !Resolve(module_, "WintunEndSession", EndSession, error) ||
116
+ !Resolve(module_, "WintunGetReadWaitEvent", GetReadWaitEvent, error) ||
117
+ !Resolve(module_, "WintunReceivePacket", ReceivePacket, error) ||
118
+ !Resolve(module_, "WintunReleaseReceivePacket", ReleaseReceivePacket, error) ||
119
+ !Resolve(module_, "WintunAllocateSendPacket", AllocateSendPacket, error) ||
120
+ !Resolve(module_, "WintunSendPacket", SendPacket, error)) {
121
+ // Some pointers may already have been resolved before the failure.
122
+ // Null everything so callers cannot read stale addresses into a
123
+ // module that we are about to FreeLibrary.
124
+ ClearEntryPoints();
125
+ ::FreeLibrary(module_);
126
+ module_ = nullptr;
127
+ return false;
128
+ }
129
+
130
+ loaded_ = true;
131
+ return true;
132
+ }
133
+
134
+ void WintunApi::ClearEntryPoints() {
135
+ CreateAdapter = nullptr;
136
+ OpenAdapter = nullptr;
137
+ CloseAdapter = nullptr;
138
+ GetRunningDriverVersion = nullptr;
139
+ StartSession = nullptr;
140
+ EndSession = nullptr;
141
+ GetReadWaitEvent = nullptr;
142
+ ReceivePacket = nullptr;
143
+ ReleaseReceivePacket = nullptr;
144
+ AllocateSendPacket = nullptr;
145
+ SendPacket = nullptr;
146
+ }
147
+
148
+ std::string FormatLastError(DWORD error_code) {
149
+ if (error_code == 0) {
150
+ return "no error";
151
+ }
152
+
153
+ LPSTR buffer = nullptr;
154
+ DWORD len = ::FormatMessageA(
155
+ FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
156
+ FORMAT_MESSAGE_IGNORE_INSERTS,
157
+ nullptr, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
158
+ reinterpret_cast<LPSTR>(&buffer), 0, nullptr);
159
+
160
+ std::string message;
161
+ if (len && buffer) {
162
+ message.assign(buffer, len);
163
+ ::LocalFree(buffer);
164
+ while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) {
165
+ message.pop_back();
166
+ }
167
+ }
168
+
169
+ if (message.empty()) {
170
+ message = "Unknown error";
171
+ }
172
+ return message + " (code=" + std::to_string(error_code) + ")";
173
+ }
174
+
175
+ std::wstring Utf8ToUtf16(const std::string& utf8) {
176
+ if (utf8.empty()) {
177
+ return std::wstring();
178
+ }
179
+
180
+ int len = ::MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(),
181
+ static_cast<int>(utf8.size()), nullptr, 0);
182
+ if (len <= 0) {
183
+ return std::wstring();
184
+ }
185
+
186
+ std::wstring result(static_cast<size_t>(len), L'\0');
187
+ ::MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(),
188
+ static_cast<int>(utf8.size()), result.data(), len);
189
+ return result;
190
+ }
191
+
192
+ #endif
@@ -0,0 +1,93 @@
1
+ #pragma once
2
+
3
+ #ifdef _WIN32
4
+
5
+ #include <windows.h>
6
+ #include <ifdef.h>
7
+
8
+ #include <mutex>
9
+ #include <string>
10
+
11
+ // Function-pointer typedefs reproduced from the WireGuard-supplied wintun.h
12
+ // (https://git.zx2c4.com/wintun/plain/api/wintun.h, GPL-2.0 OR MIT). Only the
13
+ // pieces we use are declared here so we do not have to vendor the full
14
+ // upstream header.
15
+
16
+ typedef struct _WINTUN_ADAPTER* WINTUN_ADAPTER_HANDLE;
17
+ typedef struct _TUN_SESSION* WINTUN_SESSION_HANDLE;
18
+
19
+ #ifndef WINTUN_MIN_RING_CAPACITY
20
+ #define WINTUN_MIN_RING_CAPACITY 0x20000
21
+ #endif
22
+ #ifndef WINTUN_MAX_RING_CAPACITY
23
+ #define WINTUN_MAX_RING_CAPACITY 0x4000000
24
+ #endif
25
+ #ifndef WINTUN_MAX_IP_PACKET_SIZE
26
+ #define WINTUN_MAX_IP_PACKET_SIZE 0xFFFF
27
+ #endif
28
+
29
+ typedef WINTUN_ADAPTER_HANDLE(WINAPI* WINTUN_CREATE_ADAPTER_FUNC)(LPCWSTR Name,
30
+ LPCWSTR TunnelType,
31
+ const GUID* RequestedGUID);
32
+ typedef WINTUN_ADAPTER_HANDLE(WINAPI* WINTUN_OPEN_ADAPTER_FUNC)(LPCWSTR Name);
33
+ typedef VOID(WINAPI* WINTUN_CLOSE_ADAPTER_FUNC)(WINTUN_ADAPTER_HANDLE Adapter);
34
+ typedef BOOL(WINAPI* WINTUN_DELETE_DRIVER_FUNC)(VOID);
35
+ typedef VOID(WINAPI* WINTUN_GET_ADAPTER_LUID_FUNC)(WINTUN_ADAPTER_HANDLE Adapter, NET_LUID* Luid);
36
+ typedef DWORD(WINAPI* WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC)(VOID);
37
+
38
+ typedef WINTUN_SESSION_HANDLE(WINAPI* WINTUN_START_SESSION_FUNC)(WINTUN_ADAPTER_HANDLE Adapter,
39
+ DWORD Capacity);
40
+ typedef VOID(WINAPI* WINTUN_END_SESSION_FUNC)(WINTUN_SESSION_HANDLE Session);
41
+ typedef HANDLE(WINAPI* WINTUN_GET_READ_WAIT_EVENT_FUNC)(WINTUN_SESSION_HANDLE Session);
42
+ typedef BYTE*(WINAPI* WINTUN_RECEIVE_PACKET_FUNC)(WINTUN_SESSION_HANDLE Session, DWORD* PacketSize);
43
+ typedef VOID(WINAPI* WINTUN_RELEASE_RECEIVE_PACKET_FUNC)(WINTUN_SESSION_HANDLE Session,
44
+ const BYTE* Packet);
45
+ typedef BYTE*(WINAPI* WINTUN_ALLOCATE_SEND_PACKET_FUNC)(WINTUN_SESSION_HANDLE Session,
46
+ DWORD PacketSize);
47
+ typedef VOID(WINAPI* WINTUN_SEND_PACKET_FUNC)(WINTUN_SESSION_HANDLE Session, const BYTE* Packet);
48
+
49
+ // Singleton container for the resolved entry points. Callers must invoke
50
+ // `Load` (returning false on failure) before reading any function pointer.
51
+ class WintunApi {
52
+ public:
53
+ static WintunApi& Instance();
54
+
55
+ // Loads `wintun.dll` and resolves all required entry points. On the first
56
+ // failure `error` describes which step went wrong; subsequent calls succeed
57
+ // immediately as long as a previous call already loaded the library.
58
+ bool Load(std::string& error);
59
+
60
+ WINTUN_CREATE_ADAPTER_FUNC CreateAdapter = nullptr;
61
+ WINTUN_OPEN_ADAPTER_FUNC OpenAdapter = nullptr;
62
+ WINTUN_CLOSE_ADAPTER_FUNC CloseAdapter = nullptr;
63
+ WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC GetRunningDriverVersion = nullptr;
64
+ WINTUN_START_SESSION_FUNC StartSession = nullptr;
65
+ WINTUN_END_SESSION_FUNC EndSession = nullptr;
66
+ WINTUN_GET_READ_WAIT_EVENT_FUNC GetReadWaitEvent = nullptr;
67
+ WINTUN_RECEIVE_PACKET_FUNC ReceivePacket = nullptr;
68
+ WINTUN_RELEASE_RECEIVE_PACKET_FUNC ReleaseReceivePacket = nullptr;
69
+ WINTUN_ALLOCATE_SEND_PACKET_FUNC AllocateSendPacket = nullptr;
70
+ WINTUN_SEND_PACKET_FUNC SendPacket = nullptr;
71
+
72
+ private:
73
+ WintunApi() = default;
74
+ WintunApi(const WintunApi&) = delete;
75
+ WintunApi& operator=(const WintunApi&) = delete;
76
+
77
+ bool TryLoadFrom(LPCWSTR path);
78
+ bool ResolveEntryPoints(std::string& error);
79
+ void ClearEntryPoints();
80
+
81
+ std::mutex load_mutex_;
82
+ HMODULE module_ = nullptr;
83
+ bool loaded_ = false;
84
+ };
85
+
86
+ // Builds a UTF-8 description of the latest Win32 error suitable for embedding
87
+ // in `std::string` error messages.
88
+ std::string FormatLastError(DWORD error_code);
89
+
90
+ // Converts a UTF-8 string to UTF-16 for Win32 wide-char APIs.
91
+ std::wstring Utf8ToUtf16(const std::string& utf8);
92
+
93
+ #endif