nftables-napi 0.0.2 → 0.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # nftables-napi
2
2
 
3
- Native Node.js binding for nftables via libnftnl + libmnl. Manages IPv4/IPv6 blacklist tables with timeout support through direct netlink communication — no shell commands, no `nft` CLI.
3
+ Native Node.js binding for nftables via libnftnl + libmnl. Manages IPv4/IPv6 tables with dynamic sets and timeout support through direct netlink communication — no shell commands, no `nft` CLI.
4
4
 
5
5
  Requires Linux with `CAP_NET_ADMIN` or root.
6
6
 
@@ -35,8 +35,7 @@ const { NftManager } = require("nftables-napi");
35
35
 
36
36
  const nft = new NftManager({
37
37
  tableName: "tablename",
38
- blacklistSetName: "blacklist",
39
- droplistSetName: "droplist",
38
+ sets: ["blacklist", "droplist"],
40
39
  });
41
40
 
42
41
  await nft.createTable();
@@ -62,13 +61,10 @@ await nft.deleteTable();
62
61
 
63
62
  ### `new NftManager(options)`
64
63
 
65
- | Option | Type | Required | Description |
66
- | ------------------ | -------- | -------- | -------------------------------------------- |
67
- | `tableName` | `string` | Yes | Base table name (IPv6 auto-appends `'6'`) |
68
- | `blacklistSetName` | `string` | Yes | Blacklist set name (IPv6 auto-appends `'6'`) |
69
- | `droplistSetName` | `string` | Yes | Droplist set name (IPv6 auto-appends `'6'`) |
70
-
71
- Log prefixes are auto-generated: `'{setName}: '` for each set.
64
+ | Option | Type | Required | Description |
65
+ | ----------- | ---------- | -------- | ------------------------------------------------------------------------------------------------ |
66
+ | `tableName` | `string` | Yes | Base table name (IPv6 table auto-appends `'6'`) |
67
+ | `sets` | `string[]` | Yes | Set names (1+, unique, non-empty). IPv6 sets auto-append `'6'`. Log prefix: `'{name}: '` |
72
68
 
73
69
  ### Methods
74
70
 
@@ -76,8 +72,8 @@ All methods return `Promise<void>`.
76
72
 
77
73
  | Method | Description |
78
74
  | -------------------------------------- | --------------------------------------------------------------------- |
79
- | `createTable()` | Create IPv4/IPv6 tables with blacklist and droplist sets. Idempotent. |
80
- | `deleteTable()` | Delete both tables. Idempotent. |
75
+ | `createTable()` | Create IPv4/IPv6 tables with all configured sets and filter chains. Idempotent. |
76
+ | `deleteTable()` | Delete both tables. Idempotent. |
81
77
  | `addAddress({ ip, set, timeout? })` | Add IP to set. `timeout` in seconds, omit for permanent. |
82
78
  | `removeAddress({ ip, set })` | Remove IP from set. Idempotent. |
83
79
  | `addAddresses({ ips, set, timeout? })` | Bulk add to set. Chunked for efficient netlink communication. |
package/lib/index.d.ts CHANGED
@@ -1,28 +1,23 @@
1
1
  /**
2
2
  * Native nftables manager for Linux firewall.
3
- * Manages IPv4/IPv6 blacklist/droplist tables via libnftnl + libmnl (direct netlink, no nft CLI).
3
+ * Manages IPv4/IPv6 tables with dynamic sets via libnftnl + libmnl (direct netlink, no nft CLI).
4
4
  * Requires CAP_NET_ADMIN or root privileges.
5
5
  */
6
6
 
7
- /** Target set for address operations. */
8
- export type TargetSet = 'blacklist' | 'droplist';
9
-
10
7
  /** Constructor options. All fields are required — no defaults. */
11
8
  export interface NftManagerOptions {
12
9
  /** Base table name. IPv6 table auto-appends '6'. */
13
10
  tableName: string;
14
- /** Blacklist set name. IPv6 set auto-appends '6'. */
15
- blacklistSetName: string;
16
- /** Droplist set name. IPv6 set auto-appends '6'. */
17
- droplistSetName: string;
11
+ /** Set names. At least 1, no duplicates, non-empty strings. IPv6 sets auto-append '6'. */
12
+ sets: string[];
18
13
  }
19
14
 
20
15
  /** Options for adding a single address. */
21
16
  export interface AddAddressOptions {
22
17
  /** IPv4 or IPv6 address (e.g., "1.2.3.4" or "2001:db8::1"). */
23
18
  ip: string;
24
- /** Target set: 'blacklist' or 'droplist'. */
25
- set: TargetSet;
19
+ /** Target set name (must match one from constructor's sets array). */
20
+ set: string;
26
21
  /** Timeout in seconds. Omit for permanent ban. */
27
22
  timeout?: number;
28
23
  }
@@ -31,16 +26,16 @@ export interface AddAddressOptions {
31
26
  export interface RemoveAddressOptions {
32
27
  /** IPv4 or IPv6 address to remove. */
33
28
  ip: string;
34
- /** Target set: 'blacklist' or 'droplist'. */
35
- set: TargetSet;
29
+ /** Target set name (must match one from constructor's sets array). */
30
+ set: string;
36
31
  }
37
32
 
38
33
  /** Options for bulk adding addresses. */
39
34
  export interface AddAddressesOptions {
40
35
  /** Array of IPv4/IPv6 addresses. */
41
36
  ips: string[];
42
- /** Target set: 'blacklist' or 'droplist'. */
43
- set: TargetSet;
37
+ /** Target set name (must match one from constructor's sets array). */
38
+ set: string;
44
39
  /** Timeout in seconds. Omit for permanent ban. */
45
40
  timeout?: number;
46
41
  }
@@ -49,8 +44,8 @@ export interface AddAddressesOptions {
49
44
  export interface RemoveAddressesOptions {
50
45
  /** Array of IPv4/IPv6 addresses to remove. */
51
46
  ips: string[];
52
- /** Target set: 'blacklist' or 'droplist'. */
53
- set: TargetSet;
47
+ /** Target set name (must match one from constructor's sets array). */
48
+ set: string;
54
49
  }
55
50
 
56
51
  export class NftManager {
@@ -58,21 +53,25 @@ export class NftManager {
58
53
  * Creates a new NftManager instance.
59
54
  * Opens a netlink socket and validates configuration.
60
55
  *
61
- * @param options - Required configuration with table and set names.
56
+ * @param options - Required configuration with table name and set names.
62
57
  * @throws {TypeError} if options are missing or have wrong types
63
58
  * @throws {Error} if netlink socket cannot be opened (missing CAP_NET_ADMIN)
64
59
  */
65
60
  constructor(options: NftManagerOptions);
66
61
 
67
62
  /**
68
- * Creates IPv4 and IPv6 tables with blacklist/droplist sets and filter chains.
63
+ * Creates IPv4 and IPv6 tables with all configured sets and filter chains.
69
64
  * Idempotent — destroys existing tables first, then recreates.
65
+ *
66
+ * @throws {Error} if nftables operation fails
70
67
  */
71
68
  createTable(): Promise<void>;
72
69
 
73
70
  /**
74
71
  * Deletes both IPv4 and IPv6 tables.
75
72
  * Idempotent — no error if tables don't exist.
73
+ *
74
+ * @throws {Error} if nftables operation fails
76
75
  */
77
76
  deleteTable(): Promise<void>;
78
77
 
@@ -80,9 +79,9 @@ export class NftManager {
80
79
  * Adds an IP address to a set.
81
80
  * Auto-detects IPv4 vs IPv6 and routes to the correct table/set.
82
81
  *
83
- * @param options - Address, target set, and optional timeout
84
- * @throws {TypeError} if options are invalid
85
- * @throws {Error} if IP is invalid or nftables operation fails
82
+ * @param options - Address, target set name, and optional timeout.
83
+ * @throws {TypeError} if options or fields have wrong types
84
+ * @throws {Error} if IP is invalid, set name is unknown, or nftables operation fails
86
85
  */
87
86
  addAddress(options: AddAddressOptions): Promise<void>;
88
87
 
@@ -90,20 +89,19 @@ export class NftManager {
90
89
  * Removes an IP address from a set.
91
90
  * Idempotent — no error if IP is not in the set.
92
91
  *
93
- * @param options - Address and target set
94
- * @throws {TypeError} if options are invalid
95
- * @throws {Error} if IP is invalid or nftables operation fails
92
+ * @param options - Address and target set name.
93
+ * @throws {TypeError} if options or fields have wrong types
94
+ * @throws {Error} if IP is invalid, set name is unknown, or nftables operation fails
96
95
  */
97
96
  removeAddress(options: RemoveAddressOptions): Promise<void>;
98
97
 
99
98
  /**
100
99
  * Adds multiple IP addresses to a set in bulk.
101
- * Addresses are chunked for efficient netlink communication.
102
100
  * Empty arrays are a no-op.
103
101
  *
104
- * @param options - Addresses, target set, and optional timeout
105
- * @throws {TypeError} if options are invalid
106
- * @throws {Error} if any IP is invalid or nftables operation fails
102
+ * @param options - Array of addresses, target set name, and optional timeout.
103
+ * @throws {TypeError} if options or fields have wrong types
104
+ * @throws {Error} if any IP is invalid, set name is unknown, or nftables operation fails
107
105
  */
108
106
  addAddresses(options: AddAddressesOptions): Promise<void>;
109
107
 
@@ -112,9 +110,9 @@ export class NftManager {
112
110
  * Idempotent — no error if IPs are not in the set.
113
111
  * Empty arrays are a no-op.
114
112
  *
115
- * @param options - Addresses and target set
116
- * @throws {TypeError} if options are invalid
117
- * @throws {Error} if any IP is invalid or nftables operation fails
113
+ * @param options - Array of addresses and target set name.
114
+ * @throws {TypeError} if options or fields have wrong types
115
+ * @throws {Error} if any IP is invalid, set name is unknown, or nftables operation fails
118
116
  */
119
117
  removeAddresses(options: RemoveAddressesOptions): Promise<void>;
120
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nftables-napi",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "Native Node.js binding for nftables via libnftnl+libmnl — nftables firewall management",
5
5
  "author": {
6
6
  "name": "kastov",
@@ -1,42 +1,31 @@
1
1
  #pragma once
2
2
 
3
3
  #include <string>
4
+ #include <vector>
4
5
 
5
6
  namespace nft {
6
7
 
7
- enum class TargetSet {
8
- Blacklist,
9
- Droplist
8
+ struct SetDef {
9
+ std::string name; // user-facing key = nftables IPv4 set name
10
+ std::string name_v6; // name + "6"
11
+ std::string log_prefix; // name + ": "
10
12
  };
11
13
 
12
14
  struct NftConfig {
13
15
  std::string table_v4;
14
16
  std::string table_v6;
15
- std::string set_v4;
16
- std::string set_v6;
17
- std::string drop_set_v4;
18
- std::string drop_set_v6;
19
- std::string blacklist_log_prefix;
20
- std::string droplist_log_prefix;
17
+ std::vector<SetDef> sets;
21
18
 
22
19
  static NftConfig from_names(const std::string& table_name,
23
- const std::string& blacklist_set_name,
24
- const std::string& droplist_set_name) {
25
- return {table_name, table_name + "6",
26
- blacklist_set_name, blacklist_set_name + "6",
27
- droplist_set_name, droplist_set_name + "6",
28
- blacklist_set_name + ": ",
29
- droplist_set_name + ": "};
30
- }
31
-
32
- const std::string& resolve_set_v4(TargetSet ts) const {
33
- return (ts == TargetSet::Blacklist) ? set_v4 : drop_set_v4;
34
- }
35
- const std::string& resolve_set_v6(TargetSet ts) const {
36
- return (ts == TargetSet::Blacklist) ? set_v6 : drop_set_v6;
37
- }
38
- const std::string& resolve_log_prefix(TargetSet ts) const {
39
- return (ts == TargetSet::Blacklist) ? blacklist_log_prefix : droplist_log_prefix;
20
+ const std::vector<std::string>& set_names) {
21
+ NftConfig cfg;
22
+ cfg.table_v4 = table_name;
23
+ cfg.table_v6 = table_name + "6";
24
+ cfg.sets.reserve(set_names.size());
25
+ for (const auto& n : set_names) {
26
+ cfg.sets.push_back({n, n + "6", n + ": "});
27
+ }
28
+ return cfg;
40
29
  }
41
30
  };
42
31
 
@@ -13,8 +13,6 @@ extern "C" {
13
13
 
14
14
  #include <algorithm>
15
15
 
16
- using namespace nft;
17
-
18
16
  static_assert(nft::FAMILY_IPV4 == NFPROTO_IPV4, "nft::FAMILY_IPV4 must match NFPROTO_IPV4");
19
17
  static_assert(nft::FAMILY_IPV6 == NFPROTO_IPV6, "nft::FAMILY_IPV6 must match NFPROTO_IPV6");
20
18
 
@@ -25,10 +23,9 @@ static NlResult bulk_set_elem_op(
25
23
  NlSocket& sock,
26
24
  SetElemAction action,
27
25
  const nft::NftConfig& cfg,
28
- nft::TargetSet target,
26
+ size_t set_idx,
29
27
  uint64_t timeout_ms = 0)
30
28
  {
31
- // Partition by family
32
29
  std::vector<const ParsedAddr*> v4, v6;
33
30
  for (const auto& a : addrs) {
34
31
  if (a.family == NFPROTO_IPV4) v4.push_back(&a);
@@ -41,20 +38,20 @@ static NlResult bulk_set_elem_op(
41
38
  ? (NLM_F_CREATE | NLM_F_ACK) : NLM_F_ACK;
42
39
  const bool ignore_enoent = (action == SetElemAction::Del);
43
40
 
44
- // Helper lambda to process one family
41
+ const auto& sd = cfg.sets[set_idx];
42
+
45
43
  auto process = [&](uint32_t family, const std::vector<const ParsedAddr*>& family_addrs) -> NlResult {
46
44
  if (family_addrs.empty()) return {true, ""};
47
45
 
48
46
  const char* table = (family == NFPROTO_IPV4)
49
47
  ? cfg.table_v4.c_str() : cfg.table_v6.c_str();
50
48
  const char* set_name = (family == NFPROTO_IPV4)
51
- ? cfg.resolve_set_v4(target).c_str()
52
- : cfg.resolve_set_v6(target).c_str();
53
- uint32_t key_type = (family == NFPROTO_IPV4) ? DATATYPE_IPADDR : DATATYPE_IP6ADDR;
54
- uint32_t key_len = (family == NFPROTO_IPV4) ? IPV4_ADDR_LEN : IPV6_ADDR_LEN;
49
+ ? sd.name.c_str() : sd.name_v6.c_str();
50
+ uint32_t key_type = (family == NFPROTO_IPV4) ? nft::DATATYPE_IPADDR : nft::DATATYPE_IP6ADDR;
51
+ uint32_t key_len = (family == NFPROTO_IPV4) ? nft::IPV4_ADDR_LEN : nft::IPV6_ADDR_LEN;
55
52
 
56
- for (size_t offset = 0; offset < family_addrs.size(); offset += BULK_CHUNK_SIZE) {
57
- size_t end = std::min(offset + static_cast<size_t>(BULK_CHUNK_SIZE), family_addrs.size());
53
+ for (size_t offset = 0; offset < family_addrs.size(); offset += nft::BULK_CHUNK_SIZE) {
54
+ size_t end = std::min(offset + static_cast<size_t>(nft::BULK_CHUNK_SIZE), family_addrs.size());
58
55
 
59
56
  auto s = nft::make_set();
60
57
  if (!s) return {false, "nftnl_set_alloc failed"};
@@ -72,7 +69,7 @@ static NlResult bulk_set_elem_op(
72
69
  if (timeout_ms > 0) {
73
70
  nftnl_set_elem_set_u64(e, NFTNL_SET_ELEM_TIMEOUT, timeout_ms);
74
71
  }
75
- nftnl_set_elem_add(s.get(), e); // set owns e now
72
+ nftnl_set_elem_add(s.get(), e);
76
73
  }
77
74
 
78
75
  NlBatch batch;
@@ -96,18 +93,18 @@ static NlResult bulk_set_elem_op(
96
93
  }
97
94
 
98
95
  BulkAddSetElemOp::BulkAddSetElemOp(std::vector<ParsedAddr> addrs, uint64_t timeout_ms,
99
- std::shared_ptr<const nft::NftConfig> config, nft::TargetSet target)
96
+ std::shared_ptr<const nft::NftConfig> config, size_t set_idx)
100
97
  : addrs_(std::move(addrs)), timeout_ms_(timeout_ms),
101
- cfg_(std::move(config)), target_(target) {}
98
+ cfg_(std::move(config)), set_idx_(set_idx) {}
102
99
 
103
100
  NlResult BulkAddSetElemOp::execute(NlSocket& sock) {
104
- return bulk_set_elem_op(addrs_, sock, SetElemAction::Add, *cfg_, target_, timeout_ms_);
101
+ return bulk_set_elem_op(addrs_, sock, SetElemAction::Add, *cfg_, set_idx_, timeout_ms_);
105
102
  }
106
103
 
107
104
  BulkDelSetElemOp::BulkDelSetElemOp(std::vector<ParsedAddr> addrs,
108
- std::shared_ptr<const nft::NftConfig> config, nft::TargetSet target)
109
- : addrs_(std::move(addrs)), cfg_(std::move(config)), target_(target) {}
105
+ std::shared_ptr<const nft::NftConfig> config, size_t set_idx)
106
+ : addrs_(std::move(addrs)), cfg_(std::move(config)), set_idx_(set_idx) {}
110
107
 
111
108
  NlResult BulkDelSetElemOp::execute(NlSocket& sock) {
112
- return bulk_set_elem_op(addrs_, sock, SetElemAction::Del, *cfg_, target_);
109
+ return bulk_set_elem_op(addrs_, sock, SetElemAction::Del, *cfg_, set_idx_);
113
110
  }
@@ -9,24 +9,24 @@
9
9
  class BulkAddSetElemOp final : public NlOperation {
10
10
  public:
11
11
  BulkAddSetElemOp(std::vector<ParsedAddr> addrs, uint64_t timeout_ms,
12
- std::shared_ptr<const nft::NftConfig> config, nft::TargetSet target);
12
+ std::shared_ptr<const nft::NftConfig> config, size_t set_idx);
13
13
  NlResult execute(NlSocket& sock) override;
14
14
 
15
15
  private:
16
16
  std::vector<ParsedAddr> addrs_;
17
17
  uint64_t timeout_ms_;
18
18
  std::shared_ptr<const nft::NftConfig> cfg_;
19
- nft::TargetSet target_;
19
+ size_t set_idx_;
20
20
  };
21
21
 
22
22
  class BulkDelSetElemOp final : public NlOperation {
23
23
  public:
24
24
  BulkDelSetElemOp(std::vector<ParsedAddr> addrs,
25
- std::shared_ptr<const nft::NftConfig> config, nft::TargetSet target);
25
+ std::shared_ptr<const nft::NftConfig> config, size_t set_idx);
26
26
  NlResult execute(NlSocket& sock) override;
27
27
 
28
28
  private:
29
29
  std::vector<ParsedAddr> addrs_;
30
30
  std::shared_ptr<const nft::NftConfig> cfg_;
31
- nft::TargetSet target_;
31
+ size_t set_idx_;
32
32
  };
@@ -158,19 +158,13 @@ NlResult CreateTableOp::execute(NlSocket& sock) {
158
158
  || !add_table(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), NFT_MSG_NEWTABLE, NLM_F_CREATE))
159
159
  return {false, "failed to build tables"};
160
160
 
161
- // Blacklist sets
162
- if (!add_set(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), cfg_->set_v4.c_str(),
163
- DATATYPE_IPADDR, IPV4_ADDR_LEN, sid++)
164
- || !add_set(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), cfg_->set_v6.c_str(),
165
- DATATYPE_IP6ADDR, IPV6_ADDR_LEN, sid++))
166
- return {false, "failed to build blacklist sets"};
167
-
168
- // Droplist sets
169
- if (!add_set(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), cfg_->drop_set_v4.c_str(),
170
- DATATYPE_IPADDR, IPV4_ADDR_LEN, sid++)
171
- || !add_set(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), cfg_->drop_set_v6.c_str(),
172
- DATATYPE_IP6ADDR, IPV6_ADDR_LEN, sid++))
173
- return {false, "failed to build droplist sets"};
161
+ for (const auto& sd : cfg_->sets) {
162
+ if (!add_set(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), sd.name.c_str(),
163
+ DATATYPE_IPADDR, IPV4_ADDR_LEN, sid++)
164
+ || !add_set(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), sd.name_v6.c_str(),
165
+ DATATYPE_IP6ADDR, IPV6_ADDR_LEN, sid++))
166
+ return {false, "failed to build set '" + sd.name + "'"};
167
+ }
174
168
 
175
169
  if (!add_chain(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), CHAIN_INPUT, NF_INET_LOCAL_IN)
176
170
  || !add_chain(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), CHAIN_FORWARD, NF_INET_FORWARD)
@@ -178,31 +172,18 @@ NlResult CreateTableOp::execute(NlSocket& sock) {
178
172
  || !add_chain(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), CHAIN_FORWARD, NF_INET_FORWARD))
179
173
  return {false, "failed to build chains"};
180
174
 
181
- const char* bl_lp = cfg_->blacklist_log_prefix.c_str();
182
-
183
- // Blacklist rules
184
- if (!add_rule(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), CHAIN_INPUT,
185
- cfg_->set_v4.c_str(), IPV4_SRC_OFFSET, IPV4_ADDR_LEN, bl_lp)
186
- || !add_rule(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), CHAIN_FORWARD,
187
- cfg_->set_v4.c_str(), IPV4_SRC_OFFSET, IPV4_ADDR_LEN, bl_lp)
188
- || !add_rule(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), CHAIN_INPUT,
189
- cfg_->set_v6.c_str(), IPV6_SRC_OFFSET, IPV6_ADDR_LEN, bl_lp)
190
- || !add_rule(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), CHAIN_FORWARD,
191
- cfg_->set_v6.c_str(), IPV6_SRC_OFFSET, IPV6_ADDR_LEN, bl_lp))
192
- return {false, "failed to build blacklist rules"};
193
-
194
- const char* dl_lp = cfg_->droplist_log_prefix.c_str();
195
-
196
- // Droplist rules
197
- if (!add_rule(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), CHAIN_INPUT,
198
- cfg_->drop_set_v4.c_str(), IPV4_SRC_OFFSET, IPV4_ADDR_LEN, dl_lp)
199
- || !add_rule(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), CHAIN_FORWARD,
200
- cfg_->drop_set_v4.c_str(), IPV4_SRC_OFFSET, IPV4_ADDR_LEN, dl_lp)
201
- || !add_rule(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), CHAIN_INPUT,
202
- cfg_->drop_set_v6.c_str(), IPV6_SRC_OFFSET, IPV6_ADDR_LEN, dl_lp)
203
- || !add_rule(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), CHAIN_FORWARD,
204
- cfg_->drop_set_v6.c_str(), IPV6_SRC_OFFSET, IPV6_ADDR_LEN, dl_lp))
205
- return {false, "failed to build droplist rules"};
175
+ for (const auto& sd : cfg_->sets) {
176
+ const char* lp = sd.log_prefix.c_str();
177
+ if (!add_rule(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), CHAIN_INPUT,
178
+ sd.name.c_str(), IPV4_SRC_OFFSET, IPV4_ADDR_LEN, lp)
179
+ || !add_rule(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), CHAIN_FORWARD,
180
+ sd.name.c_str(), IPV4_SRC_OFFSET, IPV4_ADDR_LEN, lp)
181
+ || !add_rule(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), CHAIN_INPUT,
182
+ sd.name_v6.c_str(), IPV6_SRC_OFFSET, IPV6_ADDR_LEN, lp)
183
+ || !add_rule(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), CHAIN_FORWARD,
184
+ sd.name_v6.c_str(), IPV6_SRC_OFFSET, IPV6_ADDR_LEN, lp))
185
+ return {false, "failed to build rules for set '" + sd.name + "'"};
186
+ }
206
187
 
207
188
  return batch.execute(sock);
208
189
  }
@@ -46,17 +46,26 @@ static std::vector<ParsedAddr> parse_ip_array(Napi::Env env, Napi::Array arr) {
46
46
  return addrs;
47
47
  }
48
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) {
49
+ static std::optional<size_t> parse_set_name(Napi::Env env, Napi::Object opts,
50
+ const char* method_name,
51
+ const nft::NftConfig& cfg) {
51
52
  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
+ std::string msg = std::string(method_name) + ": 'set' is required and must be a string";
53
54
  Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
54
55
  return std::nullopt;
55
56
  }
56
57
  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'";
58
+
59
+ for (size_t i = 0; i < cfg.sets.size(); ++i) {
60
+ if (cfg.sets[i].name == set_str) return i;
61
+ }
62
+
63
+ std::string valid;
64
+ for (size_t i = 0; i < cfg.sets.size(); ++i) {
65
+ if (i > 0) valid += ", ";
66
+ valid += "'" + cfg.sets[i].name + "'";
67
+ }
68
+ std::string msg = std::string(method_name) + ": 'set' must be one of: " + valid;
60
69
  Napi::Error::New(env, msg).ThrowAsJavaScriptException();
61
70
  return std::nullopt;
62
71
  }
@@ -102,45 +111,69 @@ NftManager::NftManager(const Napi::CallbackInfo& info)
102
111
  : Napi::ObjectWrap<NftManager>(info) {
103
112
  Napi::Env env = info.Env();
104
113
 
105
- // 1. Validate options object exists
106
114
  if (info.Length() < 1 || !info[0].IsObject()) {
107
115
  Napi::TypeError::New(env,
108
- "NftManager requires options object with tableName, blacklistSetName, droplistSetName")
116
+ "NftManager requires options object with tableName and sets")
109
117
  .ThrowAsJavaScriptException();
110
118
  return;
111
119
  }
112
120
 
113
121
  Napi::Object opts = info[0].As<Napi::Object>();
114
122
 
115
- // 2. Extract and validate 3 required string fields
116
123
  if (!opts.Has("tableName") || !opts.Get("tableName").IsString()) {
117
124
  Napi::TypeError::New(env, "NftManager: 'tableName' is required and must be a string")
118
125
  .ThrowAsJavaScriptException();
119
126
  return;
120
127
  }
121
- if (!opts.Has("blacklistSetName") || !opts.Get("blacklistSetName").IsString()) {
122
- Napi::TypeError::New(env, "NftManager: 'blacklistSetName' is required and must be a string")
128
+
129
+ if (!opts.Has("sets") || !opts.Get("sets").IsArray()) {
130
+ Napi::TypeError::New(env, "NftManager: 'sets' is required and must be an array of strings")
123
131
  .ThrowAsJavaScriptException();
124
132
  return;
125
133
  }
126
- if (!opts.Has("droplistSetName") || !opts.Get("droplistSetName").IsString()) {
127
- Napi::TypeError::New(env, "NftManager: 'droplistSetName' is required and must be a string")
134
+
135
+ Napi::Array sets_arr = opts.Get("sets").As<Napi::Array>();
136
+ uint32_t len = sets_arr.Length();
137
+
138
+ if (len == 0) {
139
+ Napi::Error::New(env, "NftManager: 'sets' must contain at least one set name")
128
140
  .ThrowAsJavaScriptException();
129
141
  return;
130
142
  }
131
143
 
144
+ std::vector<std::string> set_names;
145
+ set_names.reserve(len);
146
+
147
+ for (uint32_t i = 0; i < len; ++i) {
148
+ Napi::Value val = sets_arr[i];
149
+ if (!val.IsString()) {
150
+ Napi::TypeError::New(env, "NftManager: 'sets[" + std::to_string(i) + "]' must be a string")
151
+ .ThrowAsJavaScriptException();
152
+ return;
153
+ }
154
+ std::string name = val.As<Napi::String>().Utf8Value();
155
+ if (name.empty()) {
156
+ Napi::Error::New(env, "NftManager: 'sets[" + std::to_string(i) + "]' must not be empty")
157
+ .ThrowAsJavaScriptException();
158
+ return;
159
+ }
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 + "'")
163
+ .ThrowAsJavaScriptException();
164
+ return;
165
+ }
166
+ }
167
+ set_names.push_back(std::move(name));
168
+ }
169
+
132
170
  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
171
 
136
- // 3. Create NftConfig from names
137
172
  config_ = std::make_shared<const nft::NftConfig>(
138
- nft::NftConfig::from_names(table_name, blacklist_set_name, droplist_set_name));
173
+ nft::NftConfig::from_names(table_name, set_names));
139
174
 
140
- // 4. Open netlink socket
141
175
  sock_ = std::make_shared<NlSocket>();
142
176
 
143
- // 5. Validate socket
144
177
  if (!sock_->is_valid()) {
145
178
  Napi::Error::New(env, "Failed to open netlink socket. Ensure CAP_NET_ADMIN or root.")
146
179
  .ThrowAsJavaScriptException();
@@ -176,9 +209,9 @@ Napi::Value NftManager::AddAddress(const Napi::CallbackInfo& info) {
176
209
  }
177
210
  std::string ip = opts.Get("ip").As<Napi::String>().Utf8Value();
178
211
 
179
- // set — required string ('blacklist' or 'droplist')
180
- auto target = parse_target_set(env, opts, "addAddress");
181
- if (!target) return env.Undefined();
212
+ // set — required string
213
+ auto set_idx = parse_set_name(env, opts, "addAddress", *config_);
214
+ if (!set_idx) return env.Undefined();
182
215
 
183
216
  // timeout — optional number (seconds). If absent, 0 = permanent
184
217
  auto timeout_ms = parse_timeout(env, opts, "addAddress");
@@ -194,7 +227,7 @@ Napi::Value NftManager::AddAddress(const Napi::CallbackInfo& info) {
194
227
 
195
228
  std::vector<ParsedAddr> addrs;
196
229
  addrs.push_back(to_parsed_addr(addr));
197
- auto op = std::make_unique<BulkAddSetElemOp>(std::move(addrs), *timeout_ms, config_, *target);
230
+ auto op = std::make_unique<BulkAddSetElemOp>(std::move(addrs), *timeout_ms, config_, *set_idx);
198
231
 
199
232
  auto deferred = Napi::Promise::Deferred::New(env);
200
233
  auto promise = deferred.Promise();
@@ -221,9 +254,9 @@ Napi::Value NftManager::RemoveAddress(const Napi::CallbackInfo& info) {
221
254
  }
222
255
  std::string ip = opts.Get("ip").As<Napi::String>().Utf8Value();
223
256
 
224
- // set — required string ('blacklist' or 'droplist')
225
- auto target = parse_target_set(env, opts, "removeAddress");
226
- if (!target) return env.Undefined();
257
+ // set — required string
258
+ auto set_idx = parse_set_name(env, opts, "removeAddress", *config_);
259
+ if (!set_idx) return env.Undefined();
227
260
 
228
261
  // Validate IP
229
262
  IpAddr addr = parse_ip(ip);
@@ -235,7 +268,7 @@ Napi::Value NftManager::RemoveAddress(const Napi::CallbackInfo& info) {
235
268
 
236
269
  std::vector<ParsedAddr> addrs;
237
270
  addrs.push_back(to_parsed_addr(addr));
238
- auto op = std::make_unique<BulkDelSetElemOp>(std::move(addrs), config_, *target);
271
+ auto op = std::make_unique<BulkDelSetElemOp>(std::move(addrs), config_, *set_idx);
239
272
 
240
273
  auto deferred = Napi::Promise::Deferred::New(env);
241
274
  auto promise = deferred.Promise();
@@ -262,9 +295,9 @@ Napi::Value NftManager::AddAddresses(const Napi::CallbackInfo& info) {
262
295
  }
263
296
  Napi::Array arr = opts.Get("ips").As<Napi::Array>();
264
297
 
265
- // set — required string ('blacklist' or 'droplist')
266
- auto target = parse_target_set(env, opts, "addAddresses");
267
- if (!target) return env.Undefined();
298
+ // set — required string
299
+ auto set_idx = parse_set_name(env, opts, "addAddresses", *config_);
300
+ if (!set_idx) return env.Undefined();
268
301
 
269
302
  // timeout — optional number (seconds). If absent, 0 = permanent
270
303
  auto timeout_ms = parse_timeout(env, opts, "addAddresses");
@@ -281,7 +314,7 @@ Napi::Value NftManager::AddAddresses(const Napi::CallbackInfo& info) {
281
314
  return promise;
282
315
  }
283
316
 
284
- auto op = std::make_unique<BulkAddSetElemOp>(std::move(addrs), *timeout_ms, config_, *target);
317
+ auto op = std::make_unique<BulkAddSetElemOp>(std::move(addrs), *timeout_ms, config_, *set_idx);
285
318
 
286
319
  auto deferred = Napi::Promise::Deferred::New(env);
287
320
  auto promise = deferred.Promise();
@@ -308,9 +341,9 @@ Napi::Value NftManager::RemoveAddresses(const Napi::CallbackInfo& info) {
308
341
  }
309
342
  Napi::Array arr = opts.Get("ips").As<Napi::Array>();
310
343
 
311
- // set — required string ('blacklist' or 'droplist')
312
- auto target = parse_target_set(env, opts, "removeAddresses");
313
- if (!target) return env.Undefined();
344
+ // set — required string
345
+ auto set_idx = parse_set_name(env, opts, "removeAddresses", *config_);
346
+ if (!set_idx) return env.Undefined();
314
347
 
315
348
  std::vector<ParsedAddr> addrs = parse_ip_array(env, arr);
316
349
  if (env.IsExceptionPending()) return env.Undefined();
@@ -323,7 +356,7 @@ Napi::Value NftManager::RemoveAddresses(const Napi::CallbackInfo& info) {
323
356
  return promise;
324
357
  }
325
358
 
326
- auto op = std::make_unique<BulkDelSetElemOp>(std::move(addrs), config_, *target);
359
+ auto op = std::make_unique<BulkDelSetElemOp>(std::move(addrs), config_, *set_idx);
327
360
 
328
361
  auto deferred = Napi::Promise::Deferred::New(env);
329
362
  auto promise = deferred.Promise();