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.
@@ -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 == set_str) return i;
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
- if (i > 0) valid += ", ";
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
- std::vector<std::string> set_names;
145
- set_names.reserve(len);
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
- for (size_t j = 0; j < set_names.size(); ++j) {
161
- if (set_names[j] == name) {
162
- Napi::Error::New(env, "NftManager: duplicate set name '" + name + "'")
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, set_names));
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::AddAddress(const Napi::CallbackInfo& info) {
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
- // set required string
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
- // set required string
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
- // set required string
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
- // set required string
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::DeleteTable(const Napi::CallbackInfo& info) {
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::make_unique<DeleteTableOp>(config_), std::move(deferred));
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
  };
@@ -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
+ }