nftables-napi 0.1.0 → 0.2.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/README.md +144 -24
- package/binding.gyp +4 -2
- package/lib/index.d.ts +126 -9
- package/lib/index.js +2 -2
- package/package.json +4 -5
- package/prebuilds/linux-arm64/nftables-napi.node +0 -0
- package/prebuilds/linux-x64/nftables-napi.node +0 -0
- package/src/netlink/constants.h +25 -0
- package/src/netlink/nft_config.h +19 -8
- package/src/netlink/nftnl_raii.h +14 -4
- package/src/netlink/nl_batch.h +4 -4
- package/src/netlink/nl_result.h +1 -1
- package/src/netlink/nl_socket.cpp +20 -16
- package/src/netlink/nl_socket.h +2 -2
- package/src/netlink/operation.h +1 -1
- package/src/netlink/set_ops.cpp +88 -0
- package/src/netlink/set_ops.h +31 -0
- package/src/netlink/table_ops.cpp +196 -20
- package/src/nft_manager.cpp +364 -46
- package/src/nft_manager.h +5 -0
- package/src/validation.cpp +12 -0
- package/src/validation.h +10 -1
package/src/nft_manager.cpp
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include "netlink/parsed_addr.h"
|
|
5
5
|
#include "netlink/table_ops.h"
|
|
6
6
|
#include "netlink/set_ops.h"
|
|
7
|
+
#include "netlink/constants.h"
|
|
7
8
|
|
|
8
9
|
#include <cmath>
|
|
9
10
|
#include <cstring>
|
|
@@ -46,9 +47,12 @@ static std::vector<ParsedAddr> parse_ip_array(Napi::Env env, Napi::Array arr) {
|
|
|
46
47
|
return addrs;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
// Parse set name, filtering by allowed SetKind values.
|
|
51
|
+
// allow_ip=true matches InIP and OutIP; allow_port=true matches OutPort.
|
|
49
52
|
static std::optional<size_t> parse_set_name(Napi::Env env, Napi::Object opts,
|
|
50
53
|
const char* method_name,
|
|
51
|
-
const nft::NftConfig& cfg
|
|
54
|
+
const nft::NftConfig& cfg,
|
|
55
|
+
bool allow_ip, bool allow_port) {
|
|
52
56
|
if (!opts.Has("set") || !opts.Get("set").IsString()) {
|
|
53
57
|
std::string msg = std::string(method_name) + ": 'set' is required and must be a string";
|
|
54
58
|
Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
|
|
@@ -57,12 +61,19 @@ static std::optional<size_t> parse_set_name(Napi::Env env, Napi::Object opts,
|
|
|
57
61
|
std::string set_str = opts.Get("set").As<Napi::String>().Utf8Value();
|
|
58
62
|
|
|
59
63
|
for (size_t i = 0; i < cfg.sets.size(); ++i) {
|
|
60
|
-
if (cfg.sets[i].name
|
|
64
|
+
if (cfg.sets[i].name != set_str) continue;
|
|
65
|
+
bool is_ip = (cfg.sets[i].kind == nft::SetKind::InIP || cfg.sets[i].kind == nft::SetKind::OutIP);
|
|
66
|
+
bool is_port = (cfg.sets[i].kind == nft::SetKind::OutPort);
|
|
67
|
+
if ((allow_ip && is_ip) || (allow_port && is_port)) return i;
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
// Build valid names for error message
|
|
63
71
|
std::string valid;
|
|
64
72
|
for (size_t i = 0; i < cfg.sets.size(); ++i) {
|
|
65
|
-
|
|
73
|
+
bool is_ip = (cfg.sets[i].kind == nft::SetKind::InIP || cfg.sets[i].kind == nft::SetKind::OutIP);
|
|
74
|
+
bool is_port = (cfg.sets[i].kind == nft::SetKind::OutPort);
|
|
75
|
+
if (!((allow_ip && is_ip) || (allow_port && is_port))) continue;
|
|
76
|
+
if (!valid.empty()) valid += ", ";
|
|
66
77
|
valid += "'" + cfg.sets[i].name + "'";
|
|
67
78
|
}
|
|
68
79
|
std::string msg = std::string(method_name) + ": 'set' must be one of: " + valid;
|
|
@@ -71,7 +82,6 @@ static std::optional<size_t> parse_set_name(Napi::Env env, Napi::Object opts,
|
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
// Returns timeout in ms (0 = permanent/no timeout). Sets JS exception on error.
|
|
74
|
-
// Returns std::nullopt on error.
|
|
75
85
|
static std::optional<uint64_t> parse_timeout(Napi::Env env, Napi::Object opts, const char* method_name) {
|
|
76
86
|
if (!opts.Has("timeout") || opts.Get("timeout").IsUndefined() || opts.Get("timeout").IsNull()) {
|
|
77
87
|
return uint64_t{0}; // permanent
|
|
@@ -91,6 +101,129 @@ static std::optional<uint64_t> parse_timeout(Napi::Env env, Napi::Object opts, c
|
|
|
91
101
|
return static_cast<uint64_t>(timeout_sec * 1000.0);
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
// Parse a single port from opts.port
|
|
105
|
+
static std::optional<uint16_t> parse_port_value(Napi::Env env, Napi::Object opts,
|
|
106
|
+
const char* method_name) {
|
|
107
|
+
if (!opts.Has("port") || !opts.Get("port").IsNumber()) {
|
|
108
|
+
std::string msg = std::string(method_name) + ": 'port' is required and must be a number";
|
|
109
|
+
Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
|
|
110
|
+
return std::nullopt;
|
|
111
|
+
}
|
|
112
|
+
double val = opts.Get("port").As<Napi::Number>().DoubleValue();
|
|
113
|
+
PortVal pv = parse_port(val);
|
|
114
|
+
if (!pv.valid) {
|
|
115
|
+
Napi::Error::New(env, std::string(method_name) + ": 'port' must be an integer 0-65535")
|
|
116
|
+
.ThrowAsJavaScriptException();
|
|
117
|
+
return std::nullopt;
|
|
118
|
+
}
|
|
119
|
+
return pv.port;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Parse array of ports from opts.ports
|
|
123
|
+
static std::vector<uint16_t> parse_port_array(Napi::Env env, Napi::Array arr,
|
|
124
|
+
const char* method_name) {
|
|
125
|
+
std::vector<uint16_t> ports;
|
|
126
|
+
ports.reserve(arr.Length());
|
|
127
|
+
|
|
128
|
+
for (uint32_t i = 0; i < arr.Length(); ++i) {
|
|
129
|
+
Napi::Value val = arr[i];
|
|
130
|
+
if (!val.IsNumber()) {
|
|
131
|
+
Napi::TypeError::New(env, std::string(method_name) + ": each port must be a number")
|
|
132
|
+
.ThrowAsJavaScriptException();
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
double d = val.As<Napi::Number>().DoubleValue();
|
|
136
|
+
PortVal pv = parse_port(d);
|
|
137
|
+
if (!pv.valid) {
|
|
138
|
+
Napi::Error::New(env, std::string(method_name) +
|
|
139
|
+
": invalid port at index " + std::to_string(i))
|
|
140
|
+
.ThrowAsJavaScriptException();
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
ports.push_back(pv.port);
|
|
144
|
+
}
|
|
145
|
+
return ports;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse optional protocol: 'tcp', 'udp', or absent (both).
|
|
149
|
+
// Returns 0 for both, PROTO_TCP for tcp, PROTO_UDP for udp.
|
|
150
|
+
// Returns 255 on error (after throwing JS exception).
|
|
151
|
+
static uint8_t parse_protocol(Napi::Env env, Napi::Object opts, const char* method_name) {
|
|
152
|
+
if (!opts.Has("protocol") || opts.Get("protocol").IsUndefined() || opts.Get("protocol").IsNull()) {
|
|
153
|
+
return 0; // both
|
|
154
|
+
}
|
|
155
|
+
Napi::Value pv = opts.Get("protocol");
|
|
156
|
+
if (!pv.IsString()) {
|
|
157
|
+
std::string msg = std::string(method_name) + ": 'protocol' must be 'tcp' or 'udp'";
|
|
158
|
+
Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
|
|
159
|
+
return 255;
|
|
160
|
+
}
|
|
161
|
+
std::string proto = pv.As<Napi::String>().Utf8Value();
|
|
162
|
+
if (proto == "tcp") return nft::PROTO_TCP;
|
|
163
|
+
if (proto == "udp") return nft::PROTO_UDP;
|
|
164
|
+
std::string msg = std::string(method_name) + ": 'protocol' must be 'tcp' or 'udp'";
|
|
165
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
166
|
+
return 255;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
static std::vector<PortElem> make_port_elems(uint16_t port, uint8_t proto) {
|
|
170
|
+
if (proto == 0) {
|
|
171
|
+
return {{nft::PROTO_TCP, port}, {nft::PROTO_UDP, port}};
|
|
172
|
+
}
|
|
173
|
+
return {{proto, port}};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static std::vector<PortElem> make_port_elems_bulk(const std::vector<uint16_t>& ports, uint8_t proto) {
|
|
177
|
+
std::vector<PortElem> elems;
|
|
178
|
+
if (proto == 0) {
|
|
179
|
+
elems.reserve(ports.size() * 2);
|
|
180
|
+
for (uint16_t p : ports) {
|
|
181
|
+
elems.push_back({nft::PROTO_TCP, p});
|
|
182
|
+
elems.push_back({nft::PROTO_UDP, p});
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
elems.reserve(ports.size());
|
|
186
|
+
for (uint16_t p : ports) {
|
|
187
|
+
elems.push_back({proto, p});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return elems;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Helper: parse optional string array from opts, returns empty vector if not present
|
|
194
|
+
static std::vector<std::string> parse_optional_string_array(Napi::Env env, Napi::Object opts,
|
|
195
|
+
const char* field_name,
|
|
196
|
+
const char* context) {
|
|
197
|
+
if (!opts.Has(field_name) || opts.Get(field_name).IsUndefined() || opts.Get(field_name).IsNull()) {
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
Napi::Value val = opts.Get(field_name);
|
|
201
|
+
if (!val.IsArray()) {
|
|
202
|
+
std::string msg = std::string(context) + ": '" + field_name + "' must be an array of strings";
|
|
203
|
+
Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
Napi::Array arr = val.As<Napi::Array>();
|
|
207
|
+
std::vector<std::string> result;
|
|
208
|
+
result.reserve(arr.Length());
|
|
209
|
+
for (uint32_t i = 0; i < arr.Length(); ++i) {
|
|
210
|
+
Napi::Value v = arr[i];
|
|
211
|
+
if (!v.IsString()) {
|
|
212
|
+
std::string msg = std::string(context) + ": '" + field_name + "[" + std::to_string(i) + "]' must be a string";
|
|
213
|
+
Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
std::string name = v.As<Napi::String>().Utf8Value();
|
|
217
|
+
if (name.empty()) {
|
|
218
|
+
std::string msg = std::string(context) + ": '" + field_name + "[" + std::to_string(i) + "]' must not be empty";
|
|
219
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
220
|
+
return {};
|
|
221
|
+
}
|
|
222
|
+
result.push_back(std::move(name));
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
94
227
|
Napi::Object NftManager::Init(Napi::Env env, Napi::Object exports) {
|
|
95
228
|
Napi::Function func = DefineClass(env, "NftManager", {
|
|
96
229
|
InstanceMethod<&NftManager::CreateTable>("createTable"),
|
|
@@ -99,6 +232,10 @@ Napi::Object NftManager::Init(Napi::Env env, Napi::Object exports) {
|
|
|
99
232
|
InstanceMethod<&NftManager::AddAddresses>("addAddresses"),
|
|
100
233
|
InstanceMethod<&NftManager::RemoveAddresses>("removeAddresses"),
|
|
101
234
|
InstanceMethod<&NftManager::DeleteTable>("deleteTable"),
|
|
235
|
+
InstanceMethod<&NftManager::AddPort>("addPort"),
|
|
236
|
+
InstanceMethod<&NftManager::RemovePort>("removePort"),
|
|
237
|
+
InstanceMethod<&NftManager::AddPorts>("addPorts"),
|
|
238
|
+
InstanceMethod<&NftManager::RemovePorts>("removePorts"),
|
|
102
239
|
});
|
|
103
240
|
|
|
104
241
|
env.SetInstanceData(new Napi::FunctionReference(Napi::Persistent(func)));
|
|
@@ -120,12 +257,14 @@ NftManager::NftManager(const Napi::CallbackInfo& info)
|
|
|
120
257
|
|
|
121
258
|
Napi::Object opts = info[0].As<Napi::Object>();
|
|
122
259
|
|
|
260
|
+
// tableName — required string
|
|
123
261
|
if (!opts.Has("tableName") || !opts.Get("tableName").IsString()) {
|
|
124
262
|
Napi::TypeError::New(env, "NftManager: 'tableName' is required and must be a string")
|
|
125
263
|
.ThrowAsJavaScriptException();
|
|
126
264
|
return;
|
|
127
265
|
}
|
|
128
266
|
|
|
267
|
+
// sets — required non-empty array
|
|
129
268
|
if (!opts.Has("sets") || !opts.Get("sets").IsArray()) {
|
|
130
269
|
Napi::TypeError::New(env, "NftManager: 'sets' is required and must be an array of strings")
|
|
131
270
|
.ThrowAsJavaScriptException();
|
|
@@ -141,9 +280,9 @@ NftManager::NftManager(const Napi::CallbackInfo& info)
|
|
|
141
280
|
return;
|
|
142
281
|
}
|
|
143
282
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
283
|
+
// Parse required sets (InIP)
|
|
284
|
+
std::vector<std::string> in_sets;
|
|
285
|
+
in_sets.reserve(len);
|
|
147
286
|
for (uint32_t i = 0; i < len; ++i) {
|
|
148
287
|
Napi::Value val = sets_arr[i];
|
|
149
288
|
if (!val.IsString()) {
|
|
@@ -157,20 +296,37 @@ NftManager::NftManager(const Napi::CallbackInfo& info)
|
|
|
157
296
|
.ThrowAsJavaScriptException();
|
|
158
297
|
return;
|
|
159
298
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
299
|
+
in_sets.push_back(std::move(name));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Parse optional outSets (OutIP)
|
|
303
|
+
std::vector<std::string> out_sets = parse_optional_string_array(env, opts, "outSets", "NftManager");
|
|
304
|
+
if (env.IsExceptionPending()) return;
|
|
305
|
+
|
|
306
|
+
// Parse optional outPortSets (OutPort)
|
|
307
|
+
std::vector<std::string> out_port_sets = parse_optional_string_array(env, opts, "outPortSets", "NftManager");
|
|
308
|
+
if (env.IsExceptionPending()) return;
|
|
309
|
+
|
|
310
|
+
// Cross-array duplicate check: all names must be unique across all arrays
|
|
311
|
+
std::vector<std::string> all_names;
|
|
312
|
+
all_names.reserve(in_sets.size() + out_sets.size() + out_port_sets.size());
|
|
313
|
+
all_names.insert(all_names.end(), in_sets.begin(), in_sets.end());
|
|
314
|
+
all_names.insert(all_names.end(), out_sets.begin(), out_sets.end());
|
|
315
|
+
all_names.insert(all_names.end(), out_port_sets.begin(), out_port_sets.end());
|
|
316
|
+
for (size_t i = 0; i < all_names.size(); ++i) {
|
|
317
|
+
for (size_t j = i + 1; j < all_names.size(); ++j) {
|
|
318
|
+
if (all_names[i] == all_names[j]) {
|
|
319
|
+
Napi::Error::New(env, "NftManager: duplicate set name '" + all_names[i] + "'")
|
|
163
320
|
.ThrowAsJavaScriptException();
|
|
164
321
|
return;
|
|
165
322
|
}
|
|
166
323
|
}
|
|
167
|
-
set_names.push_back(std::move(name));
|
|
168
324
|
}
|
|
169
325
|
|
|
170
326
|
std::string table_name = opts.Get("tableName").As<Napi::String>().Utf8Value();
|
|
171
327
|
|
|
172
328
|
config_ = std::make_shared<const nft::NftConfig>(
|
|
173
|
-
nft::NftConfig::from_names(table_name,
|
|
329
|
+
nft::NftConfig::from_names(table_name, in_sets, out_sets, out_port_sets));
|
|
174
330
|
|
|
175
331
|
sock_ = std::make_shared<NlSocket>();
|
|
176
332
|
|
|
@@ -179,29 +335,51 @@ NftManager::NftManager(const Napi::CallbackInfo& info)
|
|
|
179
335
|
.ThrowAsJavaScriptException();
|
|
180
336
|
return;
|
|
181
337
|
}
|
|
338
|
+
|
|
339
|
+
valid_ = true;
|
|
182
340
|
}
|
|
183
341
|
|
|
342
|
+
// ── Table lifecycle ─────────────────────────────────────────────────────────
|
|
343
|
+
|
|
184
344
|
Napi::Value NftManager::CreateTable(const Napi::CallbackInfo& info) {
|
|
185
345
|
Napi::Env env = info.Env();
|
|
186
|
-
|
|
346
|
+
if (!valid_) {
|
|
347
|
+
Napi::Error::New(env, "NftManager is not initialized").ThrowAsJavaScriptException();
|
|
348
|
+
return env.Undefined();
|
|
349
|
+
}
|
|
187
350
|
auto deferred = Napi::Promise::Deferred::New(env);
|
|
188
351
|
auto promise = deferred.Promise();
|
|
189
352
|
Enqueue(std::make_unique<CreateTableOp>(config_), std::move(deferred));
|
|
190
353
|
return promise;
|
|
191
354
|
}
|
|
192
355
|
|
|
193
|
-
Napi::Value NftManager::
|
|
356
|
+
Napi::Value NftManager::DeleteTable(const Napi::CallbackInfo& info) {
|
|
194
357
|
Napi::Env env = info.Env();
|
|
358
|
+
if (!valid_) {
|
|
359
|
+
Napi::Error::New(env, "NftManager is not initialized").ThrowAsJavaScriptException();
|
|
360
|
+
return env.Undefined();
|
|
361
|
+
}
|
|
362
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
363
|
+
auto promise = deferred.Promise();
|
|
364
|
+
Enqueue(std::make_unique<DeleteTableOp>(config_), std::move(deferred));
|
|
365
|
+
return promise;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── IP address operations ───────────────────────────────────────────────────
|
|
195
369
|
|
|
370
|
+
Napi::Value NftManager::AddAddress(const Napi::CallbackInfo& info) {
|
|
371
|
+
Napi::Env env = info.Env();
|
|
372
|
+
if (!valid_) {
|
|
373
|
+
Napi::Error::New(env, "NftManager is not initialized").ThrowAsJavaScriptException();
|
|
374
|
+
return env.Undefined();
|
|
375
|
+
}
|
|
196
376
|
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
197
377
|
Napi::TypeError::New(env, "addAddress requires an options object: { ip, set, timeout? }")
|
|
198
378
|
.ThrowAsJavaScriptException();
|
|
199
379
|
return env.Undefined();
|
|
200
380
|
}
|
|
201
|
-
|
|
202
381
|
Napi::Object opts = info[0].As<Napi::Object>();
|
|
203
382
|
|
|
204
|
-
// ip — required string
|
|
205
383
|
if (!opts.Has("ip") || !opts.Get("ip").IsString()) {
|
|
206
384
|
Napi::TypeError::New(env, "addAddress: 'ip' is required and must be a string")
|
|
207
385
|
.ThrowAsJavaScriptException();
|
|
@@ -209,19 +387,15 @@ Napi::Value NftManager::AddAddress(const Napi::CallbackInfo& info) {
|
|
|
209
387
|
}
|
|
210
388
|
std::string ip = opts.Get("ip").As<Napi::String>().Utf8Value();
|
|
211
389
|
|
|
212
|
-
|
|
213
|
-
auto set_idx = parse_set_name(env, opts, "addAddress", *config_);
|
|
390
|
+
auto set_idx = parse_set_name(env, opts, "addAddress", *config_, true, false);
|
|
214
391
|
if (!set_idx) return env.Undefined();
|
|
215
392
|
|
|
216
|
-
// timeout — optional number (seconds). If absent, 0 = permanent
|
|
217
393
|
auto timeout_ms = parse_timeout(env, opts, "addAddress");
|
|
218
394
|
if (!timeout_ms) return env.Undefined();
|
|
219
395
|
|
|
220
|
-
// Validate IP
|
|
221
396
|
IpAddr addr = parse_ip(ip);
|
|
222
397
|
if (addr.family == IpFamily::Invalid) {
|
|
223
|
-
Napi::Error::New(env, "Invalid IP address: " + ip)
|
|
224
|
-
.ThrowAsJavaScriptException();
|
|
398
|
+
Napi::Error::New(env, "Invalid IP address: " + ip).ThrowAsJavaScriptException();
|
|
225
399
|
return env.Undefined();
|
|
226
400
|
}
|
|
227
401
|
|
|
@@ -237,16 +411,17 @@ Napi::Value NftManager::AddAddress(const Napi::CallbackInfo& info) {
|
|
|
237
411
|
|
|
238
412
|
Napi::Value NftManager::RemoveAddress(const Napi::CallbackInfo& info) {
|
|
239
413
|
Napi::Env env = info.Env();
|
|
240
|
-
|
|
414
|
+
if (!valid_) {
|
|
415
|
+
Napi::Error::New(env, "NftManager is not initialized").ThrowAsJavaScriptException();
|
|
416
|
+
return env.Undefined();
|
|
417
|
+
}
|
|
241
418
|
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
242
419
|
Napi::TypeError::New(env, "removeAddress requires an options object: { ip, set }")
|
|
243
420
|
.ThrowAsJavaScriptException();
|
|
244
421
|
return env.Undefined();
|
|
245
422
|
}
|
|
246
|
-
|
|
247
423
|
Napi::Object opts = info[0].As<Napi::Object>();
|
|
248
424
|
|
|
249
|
-
// ip — required string
|
|
250
425
|
if (!opts.Has("ip") || !opts.Get("ip").IsString()) {
|
|
251
426
|
Napi::TypeError::New(env, "removeAddress: 'ip' is required and must be a string")
|
|
252
427
|
.ThrowAsJavaScriptException();
|
|
@@ -254,15 +429,12 @@ Napi::Value NftManager::RemoveAddress(const Napi::CallbackInfo& info) {
|
|
|
254
429
|
}
|
|
255
430
|
std::string ip = opts.Get("ip").As<Napi::String>().Utf8Value();
|
|
256
431
|
|
|
257
|
-
|
|
258
|
-
auto set_idx = parse_set_name(env, opts, "removeAddress", *config_);
|
|
432
|
+
auto set_idx = parse_set_name(env, opts, "removeAddress", *config_, true, false);
|
|
259
433
|
if (!set_idx) return env.Undefined();
|
|
260
434
|
|
|
261
|
-
// Validate IP
|
|
262
435
|
IpAddr addr = parse_ip(ip);
|
|
263
436
|
if (addr.family == IpFamily::Invalid) {
|
|
264
|
-
Napi::Error::New(env, "Invalid IP address: " + ip)
|
|
265
|
-
.ThrowAsJavaScriptException();
|
|
437
|
+
Napi::Error::New(env, "Invalid IP address: " + ip).ThrowAsJavaScriptException();
|
|
266
438
|
return env.Undefined();
|
|
267
439
|
}
|
|
268
440
|
|
|
@@ -278,16 +450,17 @@ Napi::Value NftManager::RemoveAddress(const Napi::CallbackInfo& info) {
|
|
|
278
450
|
|
|
279
451
|
Napi::Value NftManager::AddAddresses(const Napi::CallbackInfo& info) {
|
|
280
452
|
Napi::Env env = info.Env();
|
|
281
|
-
|
|
453
|
+
if (!valid_) {
|
|
454
|
+
Napi::Error::New(env, "NftManager is not initialized").ThrowAsJavaScriptException();
|
|
455
|
+
return env.Undefined();
|
|
456
|
+
}
|
|
282
457
|
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
283
458
|
Napi::TypeError::New(env, "addAddresses requires an options object: { ips, set, timeout? }")
|
|
284
459
|
.ThrowAsJavaScriptException();
|
|
285
460
|
return env.Undefined();
|
|
286
461
|
}
|
|
287
|
-
|
|
288
462
|
Napi::Object opts = info[0].As<Napi::Object>();
|
|
289
463
|
|
|
290
|
-
// ips — required Array of strings
|
|
291
464
|
if (!opts.Has("ips") || !opts.Get("ips").IsArray()) {
|
|
292
465
|
Napi::TypeError::New(env, "addAddresses: 'ips' is required and must be an array of strings")
|
|
293
466
|
.ThrowAsJavaScriptException();
|
|
@@ -295,18 +468,15 @@ Napi::Value NftManager::AddAddresses(const Napi::CallbackInfo& info) {
|
|
|
295
468
|
}
|
|
296
469
|
Napi::Array arr = opts.Get("ips").As<Napi::Array>();
|
|
297
470
|
|
|
298
|
-
|
|
299
|
-
auto set_idx = parse_set_name(env, opts, "addAddresses", *config_);
|
|
471
|
+
auto set_idx = parse_set_name(env, opts, "addAddresses", *config_, true, false);
|
|
300
472
|
if (!set_idx) return env.Undefined();
|
|
301
473
|
|
|
302
|
-
// timeout — optional number (seconds). If absent, 0 = permanent
|
|
303
474
|
auto timeout_ms = parse_timeout(env, opts, "addAddresses");
|
|
304
475
|
if (!timeout_ms) return env.Undefined();
|
|
305
476
|
|
|
306
477
|
std::vector<ParsedAddr> addrs = parse_ip_array(env, arr);
|
|
307
478
|
if (env.IsExceptionPending()) return env.Undefined();
|
|
308
479
|
|
|
309
|
-
// Empty arrays: early-exit with resolved promise
|
|
310
480
|
if (addrs.empty()) {
|
|
311
481
|
auto deferred = Napi::Promise::Deferred::New(env);
|
|
312
482
|
auto promise = deferred.Promise();
|
|
@@ -315,7 +485,6 @@ Napi::Value NftManager::AddAddresses(const Napi::CallbackInfo& info) {
|
|
|
315
485
|
}
|
|
316
486
|
|
|
317
487
|
auto op = std::make_unique<BulkAddSetElemOp>(std::move(addrs), *timeout_ms, config_, *set_idx);
|
|
318
|
-
|
|
319
488
|
auto deferred = Napi::Promise::Deferred::New(env);
|
|
320
489
|
auto promise = deferred.Promise();
|
|
321
490
|
Enqueue(std::move(op), std::move(deferred));
|
|
@@ -324,16 +493,17 @@ Napi::Value NftManager::AddAddresses(const Napi::CallbackInfo& info) {
|
|
|
324
493
|
|
|
325
494
|
Napi::Value NftManager::RemoveAddresses(const Napi::CallbackInfo& info) {
|
|
326
495
|
Napi::Env env = info.Env();
|
|
327
|
-
|
|
496
|
+
if (!valid_) {
|
|
497
|
+
Napi::Error::New(env, "NftManager is not initialized").ThrowAsJavaScriptException();
|
|
498
|
+
return env.Undefined();
|
|
499
|
+
}
|
|
328
500
|
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
329
501
|
Napi::TypeError::New(env, "removeAddresses requires an options object: { ips, set }")
|
|
330
502
|
.ThrowAsJavaScriptException();
|
|
331
503
|
return env.Undefined();
|
|
332
504
|
}
|
|
333
|
-
|
|
334
505
|
Napi::Object opts = info[0].As<Napi::Object>();
|
|
335
506
|
|
|
336
|
-
// ips — required Array of strings
|
|
337
507
|
if (!opts.Has("ips") || !opts.Get("ips").IsArray()) {
|
|
338
508
|
Napi::TypeError::New(env, "removeAddresses: 'ips' is required and must be an array of strings")
|
|
339
509
|
.ThrowAsJavaScriptException();
|
|
@@ -341,14 +511,12 @@ Napi::Value NftManager::RemoveAddresses(const Napi::CallbackInfo& info) {
|
|
|
341
511
|
}
|
|
342
512
|
Napi::Array arr = opts.Get("ips").As<Napi::Array>();
|
|
343
513
|
|
|
344
|
-
|
|
345
|
-
auto set_idx = parse_set_name(env, opts, "removeAddresses", *config_);
|
|
514
|
+
auto set_idx = parse_set_name(env, opts, "removeAddresses", *config_, true, false);
|
|
346
515
|
if (!set_idx) return env.Undefined();
|
|
347
516
|
|
|
348
517
|
std::vector<ParsedAddr> addrs = parse_ip_array(env, arr);
|
|
349
518
|
if (env.IsExceptionPending()) return env.Undefined();
|
|
350
519
|
|
|
351
|
-
// Empty arrays: early-exit with resolved promise
|
|
352
520
|
if (addrs.empty()) {
|
|
353
521
|
auto deferred = Napi::Promise::Deferred::New(env);
|
|
354
522
|
auto promise = deferred.Promise();
|
|
@@ -357,6 +525,41 @@ Napi::Value NftManager::RemoveAddresses(const Napi::CallbackInfo& info) {
|
|
|
357
525
|
}
|
|
358
526
|
|
|
359
527
|
auto op = std::make_unique<BulkDelSetElemOp>(std::move(addrs), config_, *set_idx);
|
|
528
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
529
|
+
auto promise = deferred.Promise();
|
|
530
|
+
Enqueue(std::move(op), std::move(deferred));
|
|
531
|
+
return promise;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Port operations ─────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
Napi::Value NftManager::AddPort(const Napi::CallbackInfo& info) {
|
|
537
|
+
Napi::Env env = info.Env();
|
|
538
|
+
if (!valid_) {
|
|
539
|
+
Napi::Error::New(env, "NftManager is not initialized").ThrowAsJavaScriptException();
|
|
540
|
+
return env.Undefined();
|
|
541
|
+
}
|
|
542
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
543
|
+
Napi::TypeError::New(env, "addPort requires an options object: { port, set, timeout? }")
|
|
544
|
+
.ThrowAsJavaScriptException();
|
|
545
|
+
return env.Undefined();
|
|
546
|
+
}
|
|
547
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
548
|
+
|
|
549
|
+
auto port = parse_port_value(env, opts, "addPort");
|
|
550
|
+
if (!port) return env.Undefined();
|
|
551
|
+
|
|
552
|
+
auto set_idx = parse_set_name(env, opts, "addPort", *config_, false, true);
|
|
553
|
+
if (!set_idx) return env.Undefined();
|
|
554
|
+
|
|
555
|
+
uint8_t proto = parse_protocol(env, opts, "addPort");
|
|
556
|
+
if (proto == 255) return env.Undefined();
|
|
557
|
+
|
|
558
|
+
auto timeout_ms = parse_timeout(env, opts, "addPort");
|
|
559
|
+
if (!timeout_ms) return env.Undefined();
|
|
560
|
+
|
|
561
|
+
auto elems = make_port_elems(*port, proto);
|
|
562
|
+
auto op = std::make_unique<BulkAddPortElemOp>(std::move(elems), *timeout_ms, config_, *set_idx);
|
|
360
563
|
|
|
361
564
|
auto deferred = Napi::Promise::Deferred::New(env);
|
|
362
565
|
auto promise = deferred.Promise();
|
|
@@ -364,15 +567,130 @@ Napi::Value NftManager::RemoveAddresses(const Napi::CallbackInfo& info) {
|
|
|
364
567
|
return promise;
|
|
365
568
|
}
|
|
366
569
|
|
|
367
|
-
Napi::Value NftManager::
|
|
570
|
+
Napi::Value NftManager::RemovePort(const Napi::CallbackInfo& info) {
|
|
368
571
|
Napi::Env env = info.Env();
|
|
572
|
+
if (!valid_) {
|
|
573
|
+
Napi::Error::New(env, "NftManager is not initialized").ThrowAsJavaScriptException();
|
|
574
|
+
return env.Undefined();
|
|
575
|
+
}
|
|
576
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
577
|
+
Napi::TypeError::New(env, "removePort requires an options object: { port, set }")
|
|
578
|
+
.ThrowAsJavaScriptException();
|
|
579
|
+
return env.Undefined();
|
|
580
|
+
}
|
|
581
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
582
|
+
|
|
583
|
+
auto port = parse_port_value(env, opts, "removePort");
|
|
584
|
+
if (!port) return env.Undefined();
|
|
585
|
+
|
|
586
|
+
auto set_idx = parse_set_name(env, opts, "removePort", *config_, false, true);
|
|
587
|
+
if (!set_idx) return env.Undefined();
|
|
588
|
+
|
|
589
|
+
uint8_t proto = parse_protocol(env, opts, "removePort");
|
|
590
|
+
if (proto == 255) return env.Undefined();
|
|
591
|
+
|
|
592
|
+
auto elems = make_port_elems(*port, proto);
|
|
593
|
+
auto op = std::make_unique<BulkDelPortElemOp>(std::move(elems), config_, *set_idx);
|
|
369
594
|
|
|
370
595
|
auto deferred = Napi::Promise::Deferred::New(env);
|
|
371
596
|
auto promise = deferred.Promise();
|
|
372
|
-
Enqueue(std::
|
|
597
|
+
Enqueue(std::move(op), std::move(deferred));
|
|
598
|
+
return promise;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
Napi::Value NftManager::AddPorts(const Napi::CallbackInfo& info) {
|
|
602
|
+
Napi::Env env = info.Env();
|
|
603
|
+
if (!valid_) {
|
|
604
|
+
Napi::Error::New(env, "NftManager is not initialized").ThrowAsJavaScriptException();
|
|
605
|
+
return env.Undefined();
|
|
606
|
+
}
|
|
607
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
608
|
+
Napi::TypeError::New(env, "addPorts requires an options object: { ports, set, timeout? }")
|
|
609
|
+
.ThrowAsJavaScriptException();
|
|
610
|
+
return env.Undefined();
|
|
611
|
+
}
|
|
612
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
613
|
+
|
|
614
|
+
if (!opts.Has("ports") || !opts.Get("ports").IsArray()) {
|
|
615
|
+
Napi::TypeError::New(env, "addPorts: 'ports' is required and must be an array of numbers")
|
|
616
|
+
.ThrowAsJavaScriptException();
|
|
617
|
+
return env.Undefined();
|
|
618
|
+
}
|
|
619
|
+
Napi::Array arr = opts.Get("ports").As<Napi::Array>();
|
|
620
|
+
|
|
621
|
+
auto set_idx = parse_set_name(env, opts, "addPorts", *config_, false, true);
|
|
622
|
+
if (!set_idx) return env.Undefined();
|
|
623
|
+
|
|
624
|
+
uint8_t proto = parse_protocol(env, opts, "addPorts");
|
|
625
|
+
if (proto == 255) return env.Undefined();
|
|
626
|
+
|
|
627
|
+
auto timeout_ms = parse_timeout(env, opts, "addPorts");
|
|
628
|
+
if (!timeout_ms) return env.Undefined();
|
|
629
|
+
|
|
630
|
+
std::vector<uint16_t> ports = parse_port_array(env, arr, "addPorts");
|
|
631
|
+
if (env.IsExceptionPending()) return env.Undefined();
|
|
632
|
+
|
|
633
|
+
if (ports.empty()) {
|
|
634
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
635
|
+
auto promise = deferred.Promise();
|
|
636
|
+
deferred.Resolve(env.Undefined());
|
|
637
|
+
return promise;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
auto elems = make_port_elems_bulk(ports, proto);
|
|
641
|
+
auto op = std::make_unique<BulkAddPortElemOp>(std::move(elems), *timeout_ms, config_, *set_idx);
|
|
642
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
643
|
+
auto promise = deferred.Promise();
|
|
644
|
+
Enqueue(std::move(op), std::move(deferred));
|
|
645
|
+
return promise;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
Napi::Value NftManager::RemovePorts(const Napi::CallbackInfo& info) {
|
|
649
|
+
Napi::Env env = info.Env();
|
|
650
|
+
if (!valid_) {
|
|
651
|
+
Napi::Error::New(env, "NftManager is not initialized").ThrowAsJavaScriptException();
|
|
652
|
+
return env.Undefined();
|
|
653
|
+
}
|
|
654
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
655
|
+
Napi::TypeError::New(env, "removePorts requires an options object: { ports, set }")
|
|
656
|
+
.ThrowAsJavaScriptException();
|
|
657
|
+
return env.Undefined();
|
|
658
|
+
}
|
|
659
|
+
Napi::Object opts = info[0].As<Napi::Object>();
|
|
660
|
+
|
|
661
|
+
if (!opts.Has("ports") || !opts.Get("ports").IsArray()) {
|
|
662
|
+
Napi::TypeError::New(env, "removePorts: 'ports' is required and must be an array of numbers")
|
|
663
|
+
.ThrowAsJavaScriptException();
|
|
664
|
+
return env.Undefined();
|
|
665
|
+
}
|
|
666
|
+
Napi::Array arr = opts.Get("ports").As<Napi::Array>();
|
|
667
|
+
|
|
668
|
+
auto set_idx = parse_set_name(env, opts, "removePorts", *config_, false, true);
|
|
669
|
+
if (!set_idx) return env.Undefined();
|
|
670
|
+
|
|
671
|
+
uint8_t proto = parse_protocol(env, opts, "removePorts");
|
|
672
|
+
if (proto == 255) return env.Undefined();
|
|
673
|
+
|
|
674
|
+
std::vector<uint16_t> ports = parse_port_array(env, arr, "removePorts");
|
|
675
|
+
if (env.IsExceptionPending()) return env.Undefined();
|
|
676
|
+
|
|
677
|
+
if (ports.empty()) {
|
|
678
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
679
|
+
auto promise = deferred.Promise();
|
|
680
|
+
deferred.Resolve(env.Undefined());
|
|
681
|
+
return promise;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
auto elems = make_port_elems_bulk(ports, proto);
|
|
685
|
+
auto op = std::make_unique<BulkDelPortElemOp>(std::move(elems), config_, *set_idx);
|
|
686
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
687
|
+
auto promise = deferred.Promise();
|
|
688
|
+
Enqueue(std::move(op), std::move(deferred));
|
|
373
689
|
return promise;
|
|
374
690
|
}
|
|
375
691
|
|
|
692
|
+
// ── Queue management ────────────────────────────────────────────────────────
|
|
693
|
+
|
|
376
694
|
void NftManager::Enqueue(std::unique_ptr<NlOperation> op, Napi::Promise::Deferred deferred) {
|
|
377
695
|
queue_.push({std::move(op), std::move(deferred)});
|
|
378
696
|
DrainQueue();
|
package/src/nft_manager.h
CHANGED
|
@@ -29,6 +29,10 @@ private:
|
|
|
29
29
|
Napi::Value AddAddresses(const Napi::CallbackInfo& info);
|
|
30
30
|
Napi::Value RemoveAddresses(const Napi::CallbackInfo& info);
|
|
31
31
|
Napi::Value DeleteTable(const Napi::CallbackInfo& info);
|
|
32
|
+
Napi::Value AddPort(const Napi::CallbackInfo& info);
|
|
33
|
+
Napi::Value RemovePort(const Napi::CallbackInfo& info);
|
|
34
|
+
Napi::Value AddPorts(const Napi::CallbackInfo& info);
|
|
35
|
+
Napi::Value RemovePorts(const Napi::CallbackInfo& info);
|
|
32
36
|
|
|
33
37
|
void Enqueue(std::unique_ptr<NlOperation> op, Napi::Promise::Deferred deferred);
|
|
34
38
|
void DrainQueue();
|
|
@@ -41,4 +45,5 @@ private:
|
|
|
41
45
|
std::shared_ptr<const nft::NftConfig> config_;
|
|
42
46
|
std::queue<PendingOp> queue_;
|
|
43
47
|
bool worker_active_ = false;
|
|
48
|
+
bool valid_ = false;
|
|
44
49
|
};
|
package/src/validation.cpp
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#include "validation.h"
|
|
2
2
|
|
|
3
3
|
#include <arpa/inet.h>
|
|
4
|
+
#include <cmath>
|
|
4
5
|
#include <cstring>
|
|
5
6
|
|
|
6
7
|
IpAddr parse_ip(const std::string& ip) {
|
|
@@ -24,3 +25,14 @@ IpAddr parse_ip(const std::string& ip) {
|
|
|
24
25
|
|
|
25
26
|
return result;
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
PortVal parse_port(double value) {
|
|
30
|
+
if (std::isnan(value) || value < 0.0 || value > 65535.0) {
|
|
31
|
+
return {0, false};
|
|
32
|
+
}
|
|
33
|
+
double truncated = std::trunc(value);
|
|
34
|
+
if (value != truncated) {
|
|
35
|
+
return {0, false}; // reject fractional ports
|
|
36
|
+
}
|
|
37
|
+
return {static_cast<uint16_t>(truncated), true};
|
|
38
|
+
}
|