nftables-napi 0.0.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/LICENSE +661 -0
- package/README.md +104 -0
- package/binding.gyp +45 -0
- package/lib/index.d.ts +120 -0
- package/lib/index.js +24 -0
- package/package.json +45 -0
- package/prebuilds/linux-arm64/nftables-napi.node +0 -0
- package/prebuilds/linux-x64/nftables-napi.node +0 -0
- package/src/addon.cpp +9 -0
- package/src/netlink/constants.h +39 -0
- package/src/netlink/nft_config.h +43 -0
- package/src/netlink/nftnl_raii.h +69 -0
- package/src/netlink/nl_batch.cpp +67 -0
- package/src/netlink/nl_batch.h +36 -0
- package/src/netlink/nl_result.h +8 -0
- package/src/netlink/nl_socket.cpp +117 -0
- package/src/netlink/nl_socket.h +28 -0
- package/src/netlink/operation.h +11 -0
- package/src/netlink/parsed_addr.h +14 -0
- package/src/netlink/set_ops.cpp +113 -0
- package/src/netlink/set_ops.h +32 -0
- package/src/netlink/table_ops.cpp +221 -0
- package/src/netlink/table_ops.h +23 -0
- package/src/nft_manager.cpp +366 -0
- package/src/nft_manager.h +44 -0
- package/src/validation.cpp +26 -0
- package/src/validation.h +21 -0
- package/src/workers/nft_worker.cpp +30 -0
- package/src/workers/nft_worker.h +30 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#include "nft_manager.h"
|
|
2
|
+
#include "validation.h"
|
|
3
|
+
#include "workers/nft_worker.h"
|
|
4
|
+
#include "netlink/parsed_addr.h"
|
|
5
|
+
#include "netlink/table_ops.h"
|
|
6
|
+
#include "netlink/set_ops.h"
|
|
7
|
+
|
|
8
|
+
#include <cmath>
|
|
9
|
+
#include <cstring>
|
|
10
|
+
#include <optional>
|
|
11
|
+
#include <vector>
|
|
12
|
+
|
|
13
|
+
static constexpr double MAX_TIMEOUT_SEC = 4294967295.0; // UINT32_MAX seconds (~136 years)
|
|
14
|
+
|
|
15
|
+
static ParsedAddr to_parsed_addr(const IpAddr& addr) {
|
|
16
|
+
ParsedAddr pa{};
|
|
17
|
+
pa.family = (addr.family == IpFamily::IPv4) ? nft::FAMILY_IPV4 : nft::FAMILY_IPV6;
|
|
18
|
+
pa.len = addr.len;
|
|
19
|
+
std::memcpy(pa.bytes, addr.bytes.data(), addr.len);
|
|
20
|
+
return pa;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static std::vector<ParsedAddr> parse_ip_array(Napi::Env env, Napi::Array arr) {
|
|
24
|
+
std::vector<ParsedAddr> addrs;
|
|
25
|
+
addrs.reserve(arr.Length());
|
|
26
|
+
|
|
27
|
+
for (uint32_t i = 0; i < arr.Length(); ++i) {
|
|
28
|
+
Napi::Value val = arr[i];
|
|
29
|
+
if (!val.IsString()) {
|
|
30
|
+
Napi::TypeError::New(env, "each element must be a string")
|
|
31
|
+
.ThrowAsJavaScriptException();
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
std::string ip = val.As<Napi::String>().Utf8Value();
|
|
36
|
+
IpAddr addr = parse_ip(ip);
|
|
37
|
+
if (addr.family == IpFamily::Invalid) {
|
|
38
|
+
Napi::Error::New(env, "Invalid IP address at index " + std::to_string(i) + ": " + ip)
|
|
39
|
+
.ThrowAsJavaScriptException();
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
addrs.push_back(to_parsed_addr(addr));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return addrs;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Returns the parsed TargetSet or sets a JS exception and returns std::nullopt.
|
|
50
|
+
static std::optional<nft::TargetSet> parse_target_set(Napi::Env env, Napi::Object opts, const char* method_name) {
|
|
51
|
+
if (!opts.Has("set") || !opts.Get("set").IsString()) {
|
|
52
|
+
std::string msg = std::string(method_name) + ": 'set' is required and must be a string ('blacklist' or 'droplist')";
|
|
53
|
+
Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
|
|
54
|
+
return std::nullopt;
|
|
55
|
+
}
|
|
56
|
+
std::string set_str = opts.Get("set").As<Napi::String>().Utf8Value();
|
|
57
|
+
if (set_str == "blacklist") return nft::TargetSet::Blacklist;
|
|
58
|
+
if (set_str == "droplist") return nft::TargetSet::Droplist;
|
|
59
|
+
std::string msg = std::string(method_name) + ": 'set' must be 'blacklist' or 'droplist'";
|
|
60
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
61
|
+
return std::nullopt;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Returns timeout in ms (0 = permanent/no timeout). Sets JS exception on error.
|
|
65
|
+
// Returns std::nullopt on error.
|
|
66
|
+
static std::optional<uint64_t> parse_timeout(Napi::Env env, Napi::Object opts, const char* method_name) {
|
|
67
|
+
if (!opts.Has("timeout") || opts.Get("timeout").IsUndefined() || opts.Get("timeout").IsNull()) {
|
|
68
|
+
return uint64_t{0}; // permanent
|
|
69
|
+
}
|
|
70
|
+
Napi::Value tv = opts.Get("timeout");
|
|
71
|
+
if (!tv.IsNumber()) {
|
|
72
|
+
std::string msg = std::string(method_name) + ": 'timeout' must be a number (seconds)";
|
|
73
|
+
Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
|
|
74
|
+
return std::nullopt;
|
|
75
|
+
}
|
|
76
|
+
double timeout_sec = tv.As<Napi::Number>().DoubleValue();
|
|
77
|
+
if (std::isnan(timeout_sec) || timeout_sec <= 0 || timeout_sec > MAX_TIMEOUT_SEC) {
|
|
78
|
+
std::string msg = std::string(method_name) + ": 'timeout' must be a positive number (seconds)";
|
|
79
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
80
|
+
return std::nullopt;
|
|
81
|
+
}
|
|
82
|
+
return static_cast<uint64_t>(timeout_sec * 1000.0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Napi::Object NftManager::Init(Napi::Env env, Napi::Object exports) {
|
|
86
|
+
Napi::Function func = DefineClass(env, "NftManager", {
|
|
87
|
+
InstanceMethod<&NftManager::CreateTable>("createTable"),
|
|
88
|
+
InstanceMethod<&NftManager::AddAddress>("addAddress"),
|
|
89
|
+
InstanceMethod<&NftManager::RemoveAddress>("removeAddress"),
|
|
90
|
+
InstanceMethod<&NftManager::AddAddresses>("addAddresses"),
|
|
91
|
+
InstanceMethod<&NftManager::RemoveAddresses>("removeAddresses"),
|
|
92
|
+
InstanceMethod<&NftManager::DeleteTable>("deleteTable"),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
env.SetInstanceData(new Napi::FunctionReference(Napi::Persistent(func)));
|
|
96
|
+
|
|
97
|
+
exports.Set("NftManager", func);
|
|
98
|
+
return exports;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
NftManager::NftManager(const Napi::CallbackInfo& info)
|
|
102
|
+
: Napi::ObjectWrap<NftManager>(info) {
|
|
103
|
+
Napi::Env env = info.Env();
|
|
104
|
+
|
|
105
|
+
// 1. Validate options object exists
|
|
106
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
107
|
+
Napi::TypeError::New(env,
|
|
108
|
+
"NftManager requires options object with tableName, blacklistSetName, droplistSetName")
|
|
109
|
+
.ThrowAsJavaScriptException();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
114
|
+
|
|
115
|
+
// 2. Extract and validate 3 required string fields
|
|
116
|
+
if (!opts.Has("tableName") || !opts.Get("tableName").IsString()) {
|
|
117
|
+
Napi::TypeError::New(env, "NftManager: 'tableName' is required and must be a string")
|
|
118
|
+
.ThrowAsJavaScriptException();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!opts.Has("blacklistSetName") || !opts.Get("blacklistSetName").IsString()) {
|
|
122
|
+
Napi::TypeError::New(env, "NftManager: 'blacklistSetName' is required and must be a string")
|
|
123
|
+
.ThrowAsJavaScriptException();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!opts.Has("droplistSetName") || !opts.Get("droplistSetName").IsString()) {
|
|
127
|
+
Napi::TypeError::New(env, "NftManager: 'droplistSetName' is required and must be a string")
|
|
128
|
+
.ThrowAsJavaScriptException();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
std::string table_name = opts.Get("tableName").As<Napi::String>().Utf8Value();
|
|
133
|
+
std::string blacklist_set_name = opts.Get("blacklistSetName").As<Napi::String>().Utf8Value();
|
|
134
|
+
std::string droplist_set_name = opts.Get("droplistSetName").As<Napi::String>().Utf8Value();
|
|
135
|
+
|
|
136
|
+
// 3. Create NftConfig from names
|
|
137
|
+
config_ = std::make_shared<const nft::NftConfig>(
|
|
138
|
+
nft::NftConfig::from_names(table_name, blacklist_set_name, droplist_set_name));
|
|
139
|
+
|
|
140
|
+
// 4. Open netlink socket
|
|
141
|
+
sock_ = std::make_shared<NlSocket>();
|
|
142
|
+
|
|
143
|
+
// 5. Validate socket
|
|
144
|
+
if (!sock_->is_valid()) {
|
|
145
|
+
Napi::Error::New(env, "Failed to open netlink socket. Ensure CAP_NET_ADMIN or root.")
|
|
146
|
+
.ThrowAsJavaScriptException();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Napi::Value NftManager::CreateTable(const Napi::CallbackInfo& info) {
|
|
152
|
+
Napi::Env env = info.Env();
|
|
153
|
+
|
|
154
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
155
|
+
auto promise = deferred.Promise();
|
|
156
|
+
Enqueue(std::make_unique<CreateTableOp>(config_), std::move(deferred));
|
|
157
|
+
return promise;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Napi::Value NftManager::AddAddress(const Napi::CallbackInfo& info) {
|
|
161
|
+
Napi::Env env = info.Env();
|
|
162
|
+
|
|
163
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
164
|
+
Napi::TypeError::New(env, "addAddress requires an options object: { ip, set, timeout? }")
|
|
165
|
+
.ThrowAsJavaScriptException();
|
|
166
|
+
return env.Undefined();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
170
|
+
|
|
171
|
+
// ip — required string
|
|
172
|
+
if (!opts.Has("ip") || !opts.Get("ip").IsString()) {
|
|
173
|
+
Napi::TypeError::New(env, "addAddress: 'ip' is required and must be a string")
|
|
174
|
+
.ThrowAsJavaScriptException();
|
|
175
|
+
return env.Undefined();
|
|
176
|
+
}
|
|
177
|
+
std::string ip = opts.Get("ip").As<Napi::String>().Utf8Value();
|
|
178
|
+
|
|
179
|
+
// set — required string ('blacklist' or 'droplist')
|
|
180
|
+
auto target = parse_target_set(env, opts, "addAddress");
|
|
181
|
+
if (!target) return env.Undefined();
|
|
182
|
+
|
|
183
|
+
// timeout — optional number (seconds). If absent, 0 = permanent
|
|
184
|
+
auto timeout_ms = parse_timeout(env, opts, "addAddress");
|
|
185
|
+
if (!timeout_ms) return env.Undefined();
|
|
186
|
+
|
|
187
|
+
// Validate IP
|
|
188
|
+
IpAddr addr = parse_ip(ip);
|
|
189
|
+
if (addr.family == IpFamily::Invalid) {
|
|
190
|
+
Napi::Error::New(env, "Invalid IP address: " + ip)
|
|
191
|
+
.ThrowAsJavaScriptException();
|
|
192
|
+
return env.Undefined();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
std::vector<ParsedAddr> addrs;
|
|
196
|
+
addrs.push_back(to_parsed_addr(addr));
|
|
197
|
+
auto op = std::make_unique<BulkAddSetElemOp>(std::move(addrs), *timeout_ms, config_, *target);
|
|
198
|
+
|
|
199
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
200
|
+
auto promise = deferred.Promise();
|
|
201
|
+
Enqueue(std::move(op), std::move(deferred));
|
|
202
|
+
return promise;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
Napi::Value NftManager::RemoveAddress(const Napi::CallbackInfo& info) {
|
|
206
|
+
Napi::Env env = info.Env();
|
|
207
|
+
|
|
208
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
209
|
+
Napi::TypeError::New(env, "removeAddress requires an options object: { ip, set }")
|
|
210
|
+
.ThrowAsJavaScriptException();
|
|
211
|
+
return env.Undefined();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
215
|
+
|
|
216
|
+
// ip — required string
|
|
217
|
+
if (!opts.Has("ip") || !opts.Get("ip").IsString()) {
|
|
218
|
+
Napi::TypeError::New(env, "removeAddress: 'ip' is required and must be a string")
|
|
219
|
+
.ThrowAsJavaScriptException();
|
|
220
|
+
return env.Undefined();
|
|
221
|
+
}
|
|
222
|
+
std::string ip = opts.Get("ip").As<Napi::String>().Utf8Value();
|
|
223
|
+
|
|
224
|
+
// set — required string ('blacklist' or 'droplist')
|
|
225
|
+
auto target = parse_target_set(env, opts, "removeAddress");
|
|
226
|
+
if (!target) return env.Undefined();
|
|
227
|
+
|
|
228
|
+
// Validate IP
|
|
229
|
+
IpAddr addr = parse_ip(ip);
|
|
230
|
+
if (addr.family == IpFamily::Invalid) {
|
|
231
|
+
Napi::Error::New(env, "Invalid IP address: " + ip)
|
|
232
|
+
.ThrowAsJavaScriptException();
|
|
233
|
+
return env.Undefined();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
std::vector<ParsedAddr> addrs;
|
|
237
|
+
addrs.push_back(to_parsed_addr(addr));
|
|
238
|
+
auto op = std::make_unique<BulkDelSetElemOp>(std::move(addrs), config_, *target);
|
|
239
|
+
|
|
240
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
241
|
+
auto promise = deferred.Promise();
|
|
242
|
+
Enqueue(std::move(op), std::move(deferred));
|
|
243
|
+
return promise;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
Napi::Value NftManager::AddAddresses(const Napi::CallbackInfo& info) {
|
|
247
|
+
Napi::Env env = info.Env();
|
|
248
|
+
|
|
249
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
250
|
+
Napi::TypeError::New(env, "addAddresses requires an options object: { ips, set, timeout? }")
|
|
251
|
+
.ThrowAsJavaScriptException();
|
|
252
|
+
return env.Undefined();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
256
|
+
|
|
257
|
+
// ips — required Array of strings
|
|
258
|
+
if (!opts.Has("ips") || !opts.Get("ips").IsArray()) {
|
|
259
|
+
Napi::TypeError::New(env, "addAddresses: 'ips' is required and must be an array of strings")
|
|
260
|
+
.ThrowAsJavaScriptException();
|
|
261
|
+
return env.Undefined();
|
|
262
|
+
}
|
|
263
|
+
Napi::Array arr = opts.Get("ips").As<Napi::Array>();
|
|
264
|
+
|
|
265
|
+
// set — required string ('blacklist' or 'droplist')
|
|
266
|
+
auto target = parse_target_set(env, opts, "addAddresses");
|
|
267
|
+
if (!target) return env.Undefined();
|
|
268
|
+
|
|
269
|
+
// timeout — optional number (seconds). If absent, 0 = permanent
|
|
270
|
+
auto timeout_ms = parse_timeout(env, opts, "addAddresses");
|
|
271
|
+
if (!timeout_ms) return env.Undefined();
|
|
272
|
+
|
|
273
|
+
std::vector<ParsedAddr> addrs = parse_ip_array(env, arr);
|
|
274
|
+
if (env.IsExceptionPending()) return env.Undefined();
|
|
275
|
+
|
|
276
|
+
// Empty arrays: early-exit with resolved promise
|
|
277
|
+
if (addrs.empty()) {
|
|
278
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
279
|
+
auto promise = deferred.Promise();
|
|
280
|
+
deferred.Resolve(env.Undefined());
|
|
281
|
+
return promise;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
auto op = std::make_unique<BulkAddSetElemOp>(std::move(addrs), *timeout_ms, config_, *target);
|
|
285
|
+
|
|
286
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
287
|
+
auto promise = deferred.Promise();
|
|
288
|
+
Enqueue(std::move(op), std::move(deferred));
|
|
289
|
+
return promise;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
Napi::Value NftManager::RemoveAddresses(const Napi::CallbackInfo& info) {
|
|
293
|
+
Napi::Env env = info.Env();
|
|
294
|
+
|
|
295
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
296
|
+
Napi::TypeError::New(env, "removeAddresses requires an options object: { ips, set }")
|
|
297
|
+
.ThrowAsJavaScriptException();
|
|
298
|
+
return env.Undefined();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
302
|
+
|
|
303
|
+
// ips — required Array of strings
|
|
304
|
+
if (!opts.Has("ips") || !opts.Get("ips").IsArray()) {
|
|
305
|
+
Napi::TypeError::New(env, "removeAddresses: 'ips' is required and must be an array of strings")
|
|
306
|
+
.ThrowAsJavaScriptException();
|
|
307
|
+
return env.Undefined();
|
|
308
|
+
}
|
|
309
|
+
Napi::Array arr = opts.Get("ips").As<Napi::Array>();
|
|
310
|
+
|
|
311
|
+
// set — required string ('blacklist' or 'droplist')
|
|
312
|
+
auto target = parse_target_set(env, opts, "removeAddresses");
|
|
313
|
+
if (!target) return env.Undefined();
|
|
314
|
+
|
|
315
|
+
std::vector<ParsedAddr> addrs = parse_ip_array(env, arr);
|
|
316
|
+
if (env.IsExceptionPending()) return env.Undefined();
|
|
317
|
+
|
|
318
|
+
// Empty arrays: early-exit with resolved promise
|
|
319
|
+
if (addrs.empty()) {
|
|
320
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
321
|
+
auto promise = deferred.Promise();
|
|
322
|
+
deferred.Resolve(env.Undefined());
|
|
323
|
+
return promise;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
auto op = std::make_unique<BulkDelSetElemOp>(std::move(addrs), config_, *target);
|
|
327
|
+
|
|
328
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
329
|
+
auto promise = deferred.Promise();
|
|
330
|
+
Enqueue(std::move(op), std::move(deferred));
|
|
331
|
+
return promise;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
Napi::Value NftManager::DeleteTable(const Napi::CallbackInfo& info) {
|
|
335
|
+
Napi::Env env = info.Env();
|
|
336
|
+
|
|
337
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
338
|
+
auto promise = deferred.Promise();
|
|
339
|
+
Enqueue(std::make_unique<DeleteTableOp>(config_), std::move(deferred));
|
|
340
|
+
return promise;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
void NftManager::Enqueue(std::unique_ptr<NlOperation> op, Napi::Promise::Deferred deferred) {
|
|
344
|
+
queue_.push({std::move(op), std::move(deferred)});
|
|
345
|
+
DrainQueue();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
void NftManager::DrainQueue() {
|
|
349
|
+
if (worker_active_ || queue_.empty()) return;
|
|
350
|
+
|
|
351
|
+
worker_active_ = true;
|
|
352
|
+
this->Ref();
|
|
353
|
+
|
|
354
|
+
auto pending = std::move(queue_.front());
|
|
355
|
+
queue_.pop();
|
|
356
|
+
|
|
357
|
+
auto* worker = new NftWorker(
|
|
358
|
+
Env(), sock_, std::move(pending.op), std::move(pending.deferred), this);
|
|
359
|
+
worker->Queue();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
void NftManager::OnWorkerDone() {
|
|
363
|
+
worker_active_ = false;
|
|
364
|
+
this->Unref();
|
|
365
|
+
DrainQueue();
|
|
366
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <napi.h>
|
|
4
|
+
#include <memory>
|
|
5
|
+
#include <queue>
|
|
6
|
+
|
|
7
|
+
#include "netlink/nl_socket.h"
|
|
8
|
+
#include "netlink/operation.h"
|
|
9
|
+
#include "netlink/nft_config.h"
|
|
10
|
+
|
|
11
|
+
struct PendingOp {
|
|
12
|
+
std::unique_ptr<NlOperation> op;
|
|
13
|
+
Napi::Promise::Deferred deferred;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class NftManager : public Napi::ObjectWrap<NftManager> {
|
|
17
|
+
public:
|
|
18
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
19
|
+
NftManager(const Napi::CallbackInfo& info);
|
|
20
|
+
~NftManager() override = default;
|
|
21
|
+
|
|
22
|
+
// Called by NftWorker when async work completes
|
|
23
|
+
void OnWorkerDone();
|
|
24
|
+
|
|
25
|
+
private:
|
|
26
|
+
Napi::Value CreateTable(const Napi::CallbackInfo& info);
|
|
27
|
+
Napi::Value AddAddress(const Napi::CallbackInfo& info);
|
|
28
|
+
Napi::Value RemoveAddress(const Napi::CallbackInfo& info);
|
|
29
|
+
Napi::Value AddAddresses(const Napi::CallbackInfo& info);
|
|
30
|
+
Napi::Value RemoveAddresses(const Napi::CallbackInfo& info);
|
|
31
|
+
Napi::Value DeleteTable(const Napi::CallbackInfo& info);
|
|
32
|
+
|
|
33
|
+
void Enqueue(std::unique_ptr<NlOperation> op, Napi::Promise::Deferred deferred);
|
|
34
|
+
void DrainQueue();
|
|
35
|
+
|
|
36
|
+
// Thread safety: sock_ is accessed from worker threads in Execute(), but
|
|
37
|
+
// worker_active_ ensures only one NftWorker runs at a time. All queue
|
|
38
|
+
// management (Enqueue, DrainQueue, OnWorkerDone) runs on the JS main thread.
|
|
39
|
+
// This invariant MUST be preserved if refactoring the queue.
|
|
40
|
+
std::shared_ptr<NlSocket> sock_;
|
|
41
|
+
std::shared_ptr<const nft::NftConfig> config_;
|
|
42
|
+
std::queue<PendingOp> queue_;
|
|
43
|
+
bool worker_active_ = false;
|
|
44
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#include "validation.h"
|
|
2
|
+
|
|
3
|
+
#include <arpa/inet.h>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
|
|
6
|
+
IpAddr parse_ip(const std::string& ip) {
|
|
7
|
+
IpAddr result;
|
|
8
|
+
|
|
9
|
+
struct in_addr addr4;
|
|
10
|
+
if (inet_pton(AF_INET, ip.c_str(), &addr4) == 1) {
|
|
11
|
+
result.family = IpFamily::IPv4;
|
|
12
|
+
std::memcpy(result.bytes.data(), &addr4, sizeof(addr4));
|
|
13
|
+
result.len = sizeof(addr4);
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
struct in6_addr addr6;
|
|
18
|
+
if (inet_pton(AF_INET6, ip.c_str(), &addr6) == 1) {
|
|
19
|
+
result.family = IpFamily::IPv6;
|
|
20
|
+
std::memcpy(result.bytes.data(), &addr6, sizeof(addr6));
|
|
21
|
+
result.len = sizeof(addr6);
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result;
|
|
26
|
+
}
|
package/src/validation.h
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <array>
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
enum class IpFamily {
|
|
8
|
+
IPv4,
|
|
9
|
+
IPv6,
|
|
10
|
+
Invalid
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
struct IpAddr {
|
|
14
|
+
IpFamily family = IpFamily::Invalid;
|
|
15
|
+
std::array<uint8_t, 16> bytes{};
|
|
16
|
+
uint32_t len = 0;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Parse IP string, validate via inet_pton, store binary form.
|
|
20
|
+
// Returns IpAddr with family=Invalid on failure.
|
|
21
|
+
IpAddr parse_ip(const std::string& ip);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#include "nft_worker.h"
|
|
2
|
+
#include "../nft_manager.h"
|
|
3
|
+
|
|
4
|
+
NftWorker::NftWorker(Napi::Env env,
|
|
5
|
+
std::shared_ptr<NlSocket> sock,
|
|
6
|
+
std::unique_ptr<NlOperation> op,
|
|
7
|
+
Napi::Promise::Deferred deferred,
|
|
8
|
+
NftManager* owner)
|
|
9
|
+
: Napi::AsyncWorker(env, "NftWorker"),
|
|
10
|
+
deferred_(std::move(deferred)),
|
|
11
|
+
sock_(std::move(sock)),
|
|
12
|
+
op_(std::move(op)),
|
|
13
|
+
owner_(owner) {}
|
|
14
|
+
|
|
15
|
+
void NftWorker::Execute() {
|
|
16
|
+
NlResult result = op_->execute(*sock_);
|
|
17
|
+
if (!result.success) {
|
|
18
|
+
SetError(result.error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
void NftWorker::OnOK() {
|
|
23
|
+
deferred_.Resolve(Env().Undefined());
|
|
24
|
+
owner_->OnWorkerDone();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
void NftWorker::OnError(const Napi::Error& e) {
|
|
28
|
+
deferred_.Reject(e.Value());
|
|
29
|
+
owner_->OnWorkerDone();
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <napi.h>
|
|
4
|
+
#include <memory>
|
|
5
|
+
|
|
6
|
+
#include "../netlink/operation.h"
|
|
7
|
+
#include "../netlink/nl_socket.h"
|
|
8
|
+
|
|
9
|
+
// Forward declaration — NftManager calls OnWorkerDone() from OnOK/OnError
|
|
10
|
+
class NftManager;
|
|
11
|
+
|
|
12
|
+
class NftWorker : public Napi::AsyncWorker {
|
|
13
|
+
public:
|
|
14
|
+
NftWorker(Napi::Env env,
|
|
15
|
+
std::shared_ptr<NlSocket> sock,
|
|
16
|
+
std::unique_ptr<NlOperation> op,
|
|
17
|
+
Napi::Promise::Deferred deferred,
|
|
18
|
+
NftManager* owner);
|
|
19
|
+
|
|
20
|
+
protected:
|
|
21
|
+
void Execute() override;
|
|
22
|
+
void OnOK() override;
|
|
23
|
+
void OnError(const Napi::Error& e) override;
|
|
24
|
+
|
|
25
|
+
private:
|
|
26
|
+
Napi::Promise::Deferred deferred_;
|
|
27
|
+
std::shared_ptr<NlSocket> sock_;
|
|
28
|
+
std::unique_ptr<NlOperation> op_;
|
|
29
|
+
NftManager* owner_;
|
|
30
|
+
};
|