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.
@@ -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
+ }
@@ -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
+ };