nftables-napi 0.2.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,9 +35,9 @@ const { NftManager } = require("nftables-napi");
35
35
 
36
36
  const nft = new NftManager({
37
37
  tableName: "myfw",
38
- sets: ["blacklist"],
39
- outSets: ["blocklist"],
40
- outPortSets: ["blocked_ports"],
38
+ ingressAddrSets: ["blacklist"],
39
+ egressAddrSets: ["blocklist"],
40
+ egressPortSets: ["blocked_ports"],
41
41
  });
42
42
 
43
43
  await nft.createTable();
@@ -82,9 +82,9 @@ await nft.deleteTable();
82
82
  | Option | Type | Required | Description |
83
83
  | --- | --- | --- | --- |
84
84
  | `tableName` | `string` | Yes | Base table name. IPv6 table auto-appends `'6'`. |
85
- | `sets` | `string[]` | Yes | Input/forward IP set names (≥1). Block by **source** address on input and forward chains. Rules: log + named counter + drop. IPv6 sets auto-append `'6'`. |
86
- | `outSets` | `string[]` | No | Output IP set names. Block by **destination** address on output chain. Rules: named counter + drop (no log). IPv6 sets auto-append `'6'`. |
87
- | `outPortSets` | `string[]` | No | Output port set names. Block by **destination port** (TCP/UDP) on output chain using concatenated `inet_proto . inet_service` sets. Ports are added to both IPv4 and IPv6 tables. IPv6 sets auto-append `'6'`. |
85
+ | `ingressAddrSets` | `string[]` | Yes | Input/forward IP set names (≥1). Block by **source** address on input and forward chains. Rules: log + named counter + drop. IPv6 sets auto-append `'6'`. |
86
+ | `egressAddrSets` | `string[]` | No | Output IP set names. Block by **destination** address on output chain. Rules: named counter + drop (no log). IPv6 sets auto-append `'6'`. |
87
+ | `egressPortSets` | `string[]` | No | Output port set names. Block by **destination port** (TCP/UDP) on output chain using concatenated `inet_proto . inet_service` sets. Ports are added to both IPv4 and IPv6 tables. IPv6 sets auto-append `'6'`. |
88
88
 
89
89
  ### Methods
90
90
 
@@ -99,7 +99,7 @@ All methods return `Promise<void>` and throw on error.
99
99
 
100
100
  #### IP address operations
101
101
 
102
- Work with both `sets` (input/forward) and `outSets` (output).
102
+ Work with both `ingressAddrSets` (input/forward) and `egressAddrSets` (output).
103
103
 
104
104
  | Method | Description |
105
105
  | --- | --- |
@@ -110,7 +110,7 @@ Work with both `sets` (input/forward) and `outSets` (output).
110
110
 
111
111
  #### Port operations
112
112
 
113
- Work with `outPortSets` only. Ports are added to both IPv4 and IPv6 tables.
113
+ Work with `egressPortSets` only. Ports are added to both IPv4 and IPv6 tables.
114
114
 
115
115
  | Method | Description |
116
116
  | --- | --- |
@@ -121,7 +121,7 @@ Work with `outPortSets` only. Ports are added to both IPv4 and IPv6 tables.
121
121
 
122
122
  ### What `createTable()` builds
123
123
 
124
- For a config with `sets: ["bl"]`, `outSets: ["out"]`, `outPortSets: ["ports"]`:
124
+ For a config with `ingressAddrSets: ["bl"]`, `egressAddrSets: ["out"]`, `egressPortSets: ["ports"]`:
125
125
 
126
126
  ```
127
127
  table ip myfw {
package/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Native nftables manager for Linux firewall.
3
- * Manages IPv4/IPv6 tables with dynamic sets via libnftnl + libmnl (direct netlink, no nft CLI).
3
+ * Manages IPv4/IPv6 tables with dynamic sets and CIDR ranges via libnftnl + libmnl (direct netlink, no nft CLI).
4
4
  * Requires CAP_NET_ADMIN or root privileges.
5
5
  */
6
6
 
@@ -9,31 +9,31 @@ export interface NftManagerOptions {
9
9
  /** Base table name. IPv6 table auto-appends '6'. */
10
10
  tableName: string;
11
11
  /**
12
- * Input/forward IP set names (≥1 required). Block by source address.
12
+ * Ingress IP set names (≥1 required). Block by source address.
13
13
  * Rules: log prefix "<setName>: " + named counter + drop on input and forward chains.
14
14
  * IPv6 sets auto-append '6'.
15
15
  */
16
- sets: string[];
16
+ ingressAddrSets: string[];
17
17
  /**
18
- * Output IP set names (optional). Block by destination address.
18
+ * Egress IP set names (optional). Block by destination address.
19
19
  * Rules: named counter + drop on output chain (no log).
20
20
  * IPv6 sets auto-append '6'.
21
21
  */
22
- outSets?: string[];
22
+ egressAddrSets?: string[];
23
23
  /**
24
- * Output port set names (optional). Block by tcp/udp destination port.
24
+ * Egress port set names (optional). Block by tcp/udp destination port.
25
25
  * Rules: single concatenated (proto . port) lookup + named counter + drop on output chain (no log).
26
26
  * Port is added to BOTH IPv4 and IPv6 tables (ports are family-independent).
27
27
  * IPv6 sets auto-append '6'.
28
28
  */
29
- outPortSets?: string[];
29
+ egressPortSets?: string[];
30
30
  }
31
31
 
32
32
  /** Options for adding a single address. */
33
33
  export interface AddAddressOptions {
34
- /** IPv4 or IPv6 address (e.g., "1.2.3.4" or "2001:db8::1"). */
34
+ /** IPv4/IPv6 address or CIDR (e.g., "1.2.3.4", "10.0.0.0/8", "2001:db8::/32"). */
35
35
  ip: string;
36
- /** Target set name (must match one from constructor's sets or outSets). */
36
+ /** Target set name (must match one from constructor's ingressAddrSets or egressAddrSets). */
37
37
  set: string;
38
38
  /** Timeout in seconds. Omit for permanent. */
39
39
  timeout?: number;
@@ -41,17 +41,17 @@ export interface AddAddressOptions {
41
41
 
42
42
  /** Options for removing a single address. */
43
43
  export interface RemoveAddressOptions {
44
- /** IPv4 or IPv6 address to remove. */
44
+ /** IPv4/IPv6 address or CIDR to remove (must match exactly as added). */
45
45
  ip: string;
46
- /** Target set name (must match one from constructor's sets or outSets). */
46
+ /** Target set name (must match one from constructor's ingressAddrSets or egressAddrSets). */
47
47
  set: string;
48
48
  }
49
49
 
50
50
  /** Options for bulk adding addresses. */
51
51
  export interface AddAddressesOptions {
52
- /** Array of IPv4/IPv6 addresses. */
52
+ /** Array of IPv4/IPv6 addresses or CIDRs. */
53
53
  ips: string[];
54
- /** Target set name (must match one from constructor's sets or outSets). */
54
+ /** Target set name (must match one from constructor's ingressAddrSets or egressAddrSets). */
55
55
  set: string;
56
56
  /** Timeout in seconds. Omit for permanent. */
57
57
  timeout?: number;
@@ -59,9 +59,9 @@ export interface AddAddressesOptions {
59
59
 
60
60
  /** Options for bulk removing addresses. */
61
61
  export interface RemoveAddressesOptions {
62
- /** Array of IPv4/IPv6 addresses to remove. */
62
+ /** Array of IPv4/IPv6 addresses or CIDRs to remove. */
63
63
  ips: string[];
64
- /** Target set name (must match one from constructor's sets or outSets). */
64
+ /** Target set name (must match one from constructor's ingressAddrSets or egressAddrSets). */
65
65
  set: string;
66
66
  }
67
67
 
@@ -69,7 +69,7 @@ export interface RemoveAddressesOptions {
69
69
  export interface AddPortOptions {
70
70
  /** Port number (0-65535). */
71
71
  port: number;
72
- /** Target port set name (must match one from constructor's outPortSets). */
72
+ /** Target port set name (must match one from constructor's egressPortSets). */
73
73
  set: string;
74
74
  /** Protocol: 'tcp', 'udp', or omit for both. Default: both. */
75
75
  protocol?: 'tcp' | 'udp';
@@ -81,7 +81,7 @@ export interface AddPortOptions {
81
81
  export interface RemovePortOptions {
82
82
  /** Port number (0-65535). */
83
83
  port: number;
84
- /** Target port set name (must match one from constructor's outPortSets). */
84
+ /** Target port set name (must match one from constructor's egressPortSets). */
85
85
  set: string;
86
86
  /** Protocol: 'tcp', 'udp', or omit for both. Default: both. */
87
87
  protocol?: 'tcp' | 'udp';
@@ -91,7 +91,7 @@ export interface RemovePortOptions {
91
91
  export interface AddPortsOptions {
92
92
  /** Array of port numbers (0-65535). */
93
93
  ports: number[];
94
- /** Target port set name (must match one from constructor's outPortSets). */
94
+ /** Target port set name (must match one from constructor's egressPortSets). */
95
95
  set: string;
96
96
  /** Protocol: 'tcp', 'udp', or omit for both. Default: both. */
97
97
  protocol?: 'tcp' | 'udp';
@@ -103,7 +103,7 @@ export interface AddPortsOptions {
103
103
  export interface RemovePortsOptions {
104
104
  /** Array of port numbers (0-65535). */
105
105
  ports: number[];
106
- /** Target port set name (must match one from constructor's outPortSets). */
106
+ /** Target port set name (must match one from constructor's egressPortSets). */
107
107
  set: string;
108
108
  /** Protocol: 'tcp', 'udp', or omit for both. Default: both. */
109
109
  protocol?: 'tcp' | 'udp';
@@ -127,9 +127,9 @@ export class NftManager {
127
127
  * Creates:
128
128
  * - Named counter "processed" (global traffic counter per chain)
129
129
  * - Named counter per set (blocked traffic counter)
130
- * - Input chain with log + counter + drop rules (for sets)
131
- * - Forward chain with log + counter + drop rules (for sets)
132
- * - Output chain with counter + drop rules (for outSets and outPortSets, no log)
130
+ * - Input chain with log + counter + drop rules (for ingressAddrSets)
131
+ * - Forward chain with log + counter + drop rules (for ingressAddrSets)
132
+ * - Output chain with counter + drop rules (for egressAddrSets and egressPortSets, no log)
133
133
  * - Per-element counters on all sets
134
134
  *
135
135
  * @throws {Error} if nftables operation fails
@@ -145,9 +145,9 @@ export class NftManager {
145
145
  deleteTable(): Promise<void>;
146
146
 
147
147
  /**
148
- * Adds an IP address to a set.
149
- * Auto-detects IPv4 vs IPv6 and routes to the correct table/set.
150
- * Works with both input sets (sets) and output sets (outSets).
148
+ * Adds an IP address or CIDR range to a set.
149
+ * Auto-detects IPv4 vs IPv6 and routes to the correct table/set. Accepts CIDR notation (e.g., "10.0.0.0/8").
150
+ * Works with both input sets (ingressAddrSets) and output sets (egressAddrSets).
151
151
  *
152
152
  * @param options - Address, target set name, and optional timeout.
153
153
  * @throws {TypeError} if options or fields have wrong types
@@ -156,9 +156,9 @@ export class NftManager {
156
156
  addAddress(options: AddAddressOptions): Promise<void>;
157
157
 
158
158
  /**
159
- * Removes an IP address from a set.
159
+ * Removes an IP address or CIDR range from a set.
160
160
  * Idempotent — no error if IP is not in the set.
161
- * Works with both input sets (sets) and output sets (outSets).
161
+ * Works with both input sets (ingressAddrSets) and output sets (egressAddrSets).
162
162
  *
163
163
  * @param options - Address and target set name.
164
164
  * @throws {TypeError} if options or fields have wrong types
@@ -167,7 +167,7 @@ export class NftManager {
167
167
  removeAddress(options: RemoveAddressOptions): Promise<void>;
168
168
 
169
169
  /**
170
- * Adds multiple IP addresses to a set in bulk.
170
+ * Adds multiple IP addresses or CIDR ranges to a set in bulk.
171
171
  * Empty arrays are a no-op.
172
172
  *
173
173
  * @param options - Array of addresses, target set name, and optional timeout.
@@ -177,7 +177,7 @@ export class NftManager {
177
177
  addAddresses(options: AddAddressesOptions): Promise<void>;
178
178
 
179
179
  /**
180
- * Removes multiple IP addresses from a set in bulk.
180
+ * Removes multiple IP addresses or CIDR ranges from a set in bulk.
181
181
  * Idempotent — no error if IPs are not in the set.
182
182
  * Empty arrays are a no-op.
183
183
  *
package/lib/index.js CHANGED
@@ -6,7 +6,6 @@ let binding;
6
6
  try {
7
7
  binding = require('node-gyp-build')(path.join(__dirname, '..'));
8
8
  } catch (e) {
9
- if (process.platform === 'linux') throw e;
10
9
  binding = null;
11
10
  }
12
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nftables-napi",
3
- "version": "0.2.0",
3
+ "version": "0.4.1",
4
4
  "description": "Native Node.js binding for nftables via libnftnl+libmnl — nftables firewall management",
5
5
  "author": {
6
6
  "name": "kastov",
@@ -54,6 +54,12 @@ inline constexpr uint32_t TRANSPORT_DPORT_LEN = 2;
54
54
  inline constexpr uint32_t DATATYPE_PROTO_SERVICE = (12 << 6 | 13);
55
55
  inline constexpr uint32_t PROTO_SERVICE_KEY_LEN = 8;
56
56
 
57
+ // Byte offsets within the 8-byte concatenated (proto . port) key
58
+ // Layout: [proto:1][pad:3][port_hi:1][port_lo:1][pad:2]
59
+ inline constexpr uint32_t PORT_KEY_PROTO_OFFSET = 0;
60
+ inline constexpr uint32_t PORT_KEY_PORT_HI_OFFSET = 4;
61
+ inline constexpr uint32_t PORT_KEY_PORT_LO_OFFSET = 5;
62
+
57
63
  // L4 protocol numbers
58
64
  inline constexpr uint8_t PROTO_TCP = 6;
59
65
  inline constexpr uint8_t PROTO_UDP = 17;
@@ -4,7 +4,8 @@
4
4
 
5
5
  struct ParsedAddr {
6
6
  uint32_t family; // nft::FAMILY_IPV4 or nft::FAMILY_IPV6 (== NFPROTO_IPV4/IPV6)
7
- uint8_t bytes[16];
7
+ uint8_t bytes[16]; // key (start of interval)
8
+ uint8_t end_bytes[16]; // key_end (exclusive end of interval)
8
9
  uint32_t len; // 4 for IPv4, 16 for IPv6
9
10
  };
10
11
 
@@ -67,6 +67,7 @@ static NlResult bulk_set_elem_op(
67
67
  auto* e = nftnl_set_elem_alloc();
68
68
  if (!e) return {false, "nftnl_set_elem_alloc failed"};
69
69
  nftnl_set_elem_set(e, NFTNL_SET_ELEM_KEY, family_addrs[i]->bytes, family_addrs[i]->len);
70
+ nftnl_set_elem_set(e, NFTNL_SET_ELEM_KEY_END, family_addrs[i]->end_bytes, family_addrs[i]->len);
70
71
  if (timeout_ms > 0) {
71
72
  nftnl_set_elem_set_u64(e, NFTNL_SET_ELEM_TIMEOUT, timeout_ms);
72
73
  }
@@ -154,9 +155,9 @@ static NlResult bulk_port_elem_op(
154
155
  if (!e) return {false, "nftnl_set_elem_alloc failed"};
155
156
  // 8-byte concatenated key: [proto][pad:3][port_hi][port_lo][pad:2]
156
157
  uint8_t key[8] = {};
157
- key[0] = elems[i].proto;
158
- key[4] = static_cast<uint8_t>(elems[i].port >> 8);
159
- key[5] = static_cast<uint8_t>(elems[i].port & 0xFF);
158
+ key[nft::PORT_KEY_PROTO_OFFSET] = elems[i].proto;
159
+ key[nft::PORT_KEY_PORT_HI_OFFSET] = static_cast<uint8_t>(elems[i].port >> 8);
160
+ key[nft::PORT_KEY_PORT_LO_OFFSET] = static_cast<uint8_t>(elems[i].port & 0xFF);
160
161
  nftnl_set_elem_set(e, NFTNL_SET_ELEM_KEY, key, sizeof(key));
161
162
  if (timeout_ms > 0) {
162
163
  nftnl_set_elem_set_u64(e, NFTNL_SET_ELEM_TIMEOUT, timeout_ms);
@@ -69,7 +69,7 @@ static bool add_counter_obj(NlBatch& batch, uint32_t family, const char* table,
69
69
 
70
70
  static bool add_set(NlBatch& batch, uint32_t family, const char* table,
71
71
  const char* name, uint32_t key_type, uint32_t key_len,
72
- uint32_t set_id,
72
+ uint32_t set_id, bool interval = false,
73
73
  const uint8_t* concat_field_lens = nullptr,
74
74
  size_t concat_field_count = 0) {
75
75
  auto s = nft::make_set();
@@ -81,6 +81,8 @@ static bool add_set(NlBatch& batch, uint32_t family, const char* table,
81
81
  nftnl_set_set_u32(s.get(), NFTNL_SET_KEY_LEN, key_len);
82
82
 
83
83
  uint32_t set_flags = NFT_SET_TIMEOUT | NFT_SET_EXPR;
84
+ if (interval)
85
+ set_flags |= NFT_SET_INTERVAL;
84
86
  if (concat_field_lens && concat_field_count > 0)
85
87
  set_flags |= NFT_SET_CONCAT;
86
88
  nftnl_set_set_u32(s.get(), NFTNL_SET_FLAGS, set_flags);
@@ -297,10 +299,11 @@ NlResult CreateTableOp::execute(NlSocket& sock) {
297
299
  key_len_v4 = IPV4_ADDR_LEN;
298
300
  key_len_v6 = IPV6_ADDR_LEN;
299
301
  }
302
+ bool is_interval = (sd.kind != SetKind::OutPort);
300
303
  if (!add_set(batch, NFPROTO_IPV4, cfg_->table_v4.c_str(), sd.name.c_str(),
301
- key_type_v4, key_len_v4, sid++, concat_fields, concat_count)
304
+ key_type_v4, key_len_v4, sid++, is_interval, concat_fields, concat_count)
302
305
  || !add_set(batch, NFPROTO_IPV6, cfg_->table_v6.c_str(), sd.name_v6.c_str(),
303
- key_type_v6, key_len_v6, sid++, concat_fields, concat_count))
306
+ key_type_v6, key_len_v6, sid++, is_interval, concat_fields, concat_count))
304
307
  return {false, "failed to build set '" + sd.name + "'"};
305
308
  }
306
309
 
@@ -13,11 +13,12 @@
13
13
 
14
14
  static constexpr double MAX_TIMEOUT_SEC = 4294967295.0; // UINT32_MAX seconds (~136 years)
15
15
 
16
- static ParsedAddr to_parsed_addr(const IpAddr& addr) {
16
+ static ParsedAddr to_parsed_addr(const CidrAddr& cidr) {
17
17
  ParsedAddr pa{};
18
- pa.family = (addr.family == IpFamily::IPv4) ? nft::FAMILY_IPV4 : nft::FAMILY_IPV6;
19
- pa.len = addr.len;
20
- std::memcpy(pa.bytes, addr.bytes.data(), addr.len);
18
+ pa.family = (cidr.network.family == IpFamily::IPv4) ? nft::FAMILY_IPV4 : nft::FAMILY_IPV6;
19
+ pa.len = cidr.network.len;
20
+ std::memcpy(pa.bytes, cidr.network.bytes.data(), cidr.network.len);
21
+ std::memcpy(pa.end_bytes, cidr.end.bytes.data(), cidr.end.len);
21
22
  return pa;
22
23
  }
23
24
 
@@ -34,14 +35,14 @@ static std::vector<ParsedAddr> parse_ip_array(Napi::Env env, Napi::Array arr) {
34
35
  }
35
36
 
36
37
  std::string ip = val.As<Napi::String>().Utf8Value();
37
- IpAddr addr = parse_ip(ip);
38
- if (addr.family == IpFamily::Invalid) {
39
- Napi::Error::New(env, "Invalid IP address at index " + std::to_string(i) + ": " + ip)
38
+ CidrAddr cidr = parse_ip_or_cidr(ip);
39
+ if (cidr.network.family == IpFamily::Invalid) {
40
+ Napi::Error::New(env, "Invalid IP address or CIDR at index " + std::to_string(i) + ": " + ip)
40
41
  .ThrowAsJavaScriptException();
41
42
  return {};
42
43
  }
43
44
 
44
- addrs.push_back(to_parsed_addr(addr));
45
+ addrs.push_back(to_parsed_addr(cidr));
45
46
  }
46
47
 
47
48
  return addrs;
@@ -147,23 +148,23 @@ static std::vector<uint16_t> parse_port_array(Napi::Env env, Napi::Array arr,
147
148
 
148
149
  // Parse optional protocol: 'tcp', 'udp', or absent (both).
149
150
  // 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) {
151
+ // Returns std::nullopt on error (after throwing JS exception).
152
+ static std::optional<uint8_t> parse_protocol(Napi::Env env, Napi::Object opts, const char* method_name) {
152
153
  if (!opts.Has("protocol") || opts.Get("protocol").IsUndefined() || opts.Get("protocol").IsNull()) {
153
- return 0; // both
154
+ return uint8_t{0}; // both
154
155
  }
155
156
  Napi::Value pv = opts.Get("protocol");
156
157
  if (!pv.IsString()) {
157
158
  std::string msg = std::string(method_name) + ": 'protocol' must be 'tcp' or 'udp'";
158
159
  Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
159
- return 255;
160
+ return std::nullopt;
160
161
  }
161
162
  std::string proto = pv.As<Napi::String>().Utf8Value();
162
163
  if (proto == "tcp") return nft::PROTO_TCP;
163
164
  if (proto == "udp") return nft::PROTO_UDP;
164
165
  std::string msg = std::string(method_name) + ": 'protocol' must be 'tcp' or 'udp'";
165
166
  Napi::Error::New(env, msg).ThrowAsJavaScriptException();
166
- return 255;
167
+ return std::nullopt;
167
168
  }
168
169
 
169
170
  static std::vector<PortElem> make_port_elems(uint16_t port, uint8_t proto) {
@@ -250,7 +251,7 @@ NftManager::NftManager(const Napi::CallbackInfo& info)
250
251
 
251
252
  if (info.Length() < 1 || !info[0].IsObject()) {
252
253
  Napi::TypeError::New(env,
253
- "NftManager requires options object with tableName and sets")
254
+ "NftManager requires options object with tableName and ingressAddrSets")
254
255
  .ThrowAsJavaScriptException();
255
256
  return;
256
257
  }
@@ -264,18 +265,18 @@ NftManager::NftManager(const Napi::CallbackInfo& info)
264
265
  return;
265
266
  }
266
267
 
267
- // sets — required non-empty array
268
- if (!opts.Has("sets") || !opts.Get("sets").IsArray()) {
269
- Napi::TypeError::New(env, "NftManager: 'sets' is required and must be an array of strings")
268
+ // ingressAddrSets — required non-empty array
269
+ if (!opts.Has("ingressAddrSets") || !opts.Get("ingressAddrSets").IsArray()) {
270
+ Napi::TypeError::New(env, "NftManager: 'ingressAddrSets' is required and must be an array of strings")
270
271
  .ThrowAsJavaScriptException();
271
272
  return;
272
273
  }
273
274
 
274
- Napi::Array sets_arr = opts.Get("sets").As<Napi::Array>();
275
+ Napi::Array sets_arr = opts.Get("ingressAddrSets").As<Napi::Array>();
275
276
  uint32_t len = sets_arr.Length();
276
277
 
277
278
  if (len == 0) {
278
- Napi::Error::New(env, "NftManager: 'sets' must contain at least one set name")
279
+ Napi::Error::New(env, "NftManager: 'ingressAddrSets' must contain at least one set name")
279
280
  .ThrowAsJavaScriptException();
280
281
  return;
281
282
  }
@@ -286,25 +287,25 @@ NftManager::NftManager(const Napi::CallbackInfo& info)
286
287
  for (uint32_t i = 0; i < len; ++i) {
287
288
  Napi::Value val = sets_arr[i];
288
289
  if (!val.IsString()) {
289
- Napi::TypeError::New(env, "NftManager: 'sets[" + std::to_string(i) + "]' must be a string")
290
+ Napi::TypeError::New(env, "NftManager: 'ingressAddrSets[" + std::to_string(i) + "]' must be a string")
290
291
  .ThrowAsJavaScriptException();
291
292
  return;
292
293
  }
293
294
  std::string name = val.As<Napi::String>().Utf8Value();
294
295
  if (name.empty()) {
295
- Napi::Error::New(env, "NftManager: 'sets[" + std::to_string(i) + "]' must not be empty")
296
+ Napi::Error::New(env, "NftManager: 'ingressAddrSets[" + std::to_string(i) + "]' must not be empty")
296
297
  .ThrowAsJavaScriptException();
297
298
  return;
298
299
  }
299
300
  in_sets.push_back(std::move(name));
300
301
  }
301
302
 
302
- // Parse optional outSets (OutIP)
303
- std::vector<std::string> out_sets = parse_optional_string_array(env, opts, "outSets", "NftManager");
303
+ // Parse optional egressAddrSets (OutIP)
304
+ std::vector<std::string> out_sets = parse_optional_string_array(env, opts, "egressAddrSets", "NftManager");
304
305
  if (env.IsExceptionPending()) return;
305
306
 
306
- // Parse optional outPortSets (OutPort)
307
- std::vector<std::string> out_port_sets = parse_optional_string_array(env, opts, "outPortSets", "NftManager");
307
+ // Parse optional egressPortSets (OutPort)
308
+ std::vector<std::string> out_port_sets = parse_optional_string_array(env, opts, "egressPortSets", "NftManager");
308
309
  if (env.IsExceptionPending()) return;
309
310
 
310
311
  // Cross-array duplicate check: all names must be unique across all arrays
@@ -393,14 +394,14 @@ Napi::Value NftManager::AddAddress(const Napi::CallbackInfo& info) {
393
394
  auto timeout_ms = parse_timeout(env, opts, "addAddress");
394
395
  if (!timeout_ms) return env.Undefined();
395
396
 
396
- IpAddr addr = parse_ip(ip);
397
- if (addr.family == IpFamily::Invalid) {
398
- Napi::Error::New(env, "Invalid IP address: " + ip).ThrowAsJavaScriptException();
397
+ CidrAddr cidr = parse_ip_or_cidr(ip);
398
+ if (cidr.network.family == IpFamily::Invalid) {
399
+ Napi::Error::New(env, "Invalid IP address or CIDR: " + ip).ThrowAsJavaScriptException();
399
400
  return env.Undefined();
400
401
  }
401
402
 
402
403
  std::vector<ParsedAddr> addrs;
403
- addrs.push_back(to_parsed_addr(addr));
404
+ addrs.push_back(to_parsed_addr(cidr));
404
405
  auto op = std::make_unique<BulkAddSetElemOp>(std::move(addrs), *timeout_ms, config_, *set_idx);
405
406
 
406
407
  auto deferred = Napi::Promise::Deferred::New(env);
@@ -432,14 +433,14 @@ Napi::Value NftManager::RemoveAddress(const Napi::CallbackInfo& info) {
432
433
  auto set_idx = parse_set_name(env, opts, "removeAddress", *config_, true, false);
433
434
  if (!set_idx) return env.Undefined();
434
435
 
435
- IpAddr addr = parse_ip(ip);
436
- if (addr.family == IpFamily::Invalid) {
437
- Napi::Error::New(env, "Invalid IP address: " + ip).ThrowAsJavaScriptException();
436
+ CidrAddr cidr = parse_ip_or_cidr(ip);
437
+ if (cidr.network.family == IpFamily::Invalid) {
438
+ Napi::Error::New(env, "Invalid IP address or CIDR: " + ip).ThrowAsJavaScriptException();
438
439
  return env.Undefined();
439
440
  }
440
441
 
441
442
  std::vector<ParsedAddr> addrs;
442
- addrs.push_back(to_parsed_addr(addr));
443
+ addrs.push_back(to_parsed_addr(cidr));
443
444
  auto op = std::make_unique<BulkDelSetElemOp>(std::move(addrs), config_, *set_idx);
444
445
 
445
446
  auto deferred = Napi::Promise::Deferred::New(env);
@@ -552,13 +553,13 @@ Napi::Value NftManager::AddPort(const Napi::CallbackInfo& info) {
552
553
  auto set_idx = parse_set_name(env, opts, "addPort", *config_, false, true);
553
554
  if (!set_idx) return env.Undefined();
554
555
 
555
- uint8_t proto = parse_protocol(env, opts, "addPort");
556
- if (proto == 255) return env.Undefined();
556
+ auto proto = parse_protocol(env, opts, "addPort");
557
+ if (!proto) return env.Undefined();
557
558
 
558
559
  auto timeout_ms = parse_timeout(env, opts, "addPort");
559
560
  if (!timeout_ms) return env.Undefined();
560
561
 
561
- auto elems = make_port_elems(*port, proto);
562
+ auto elems = make_port_elems(*port, *proto);
562
563
  auto op = std::make_unique<BulkAddPortElemOp>(std::move(elems), *timeout_ms, config_, *set_idx);
563
564
 
564
565
  auto deferred = Napi::Promise::Deferred::New(env);
@@ -586,10 +587,10 @@ Napi::Value NftManager::RemovePort(const Napi::CallbackInfo& info) {
586
587
  auto set_idx = parse_set_name(env, opts, "removePort", *config_, false, true);
587
588
  if (!set_idx) return env.Undefined();
588
589
 
589
- uint8_t proto = parse_protocol(env, opts, "removePort");
590
- if (proto == 255) return env.Undefined();
590
+ auto proto = parse_protocol(env, opts, "removePort");
591
+ if (!proto) return env.Undefined();
591
592
 
592
- auto elems = make_port_elems(*port, proto);
593
+ auto elems = make_port_elems(*port, *proto);
593
594
  auto op = std::make_unique<BulkDelPortElemOp>(std::move(elems), config_, *set_idx);
594
595
 
595
596
  auto deferred = Napi::Promise::Deferred::New(env);
@@ -621,8 +622,8 @@ Napi::Value NftManager::AddPorts(const Napi::CallbackInfo& info) {
621
622
  auto set_idx = parse_set_name(env, opts, "addPorts", *config_, false, true);
622
623
  if (!set_idx) return env.Undefined();
623
624
 
624
- uint8_t proto = parse_protocol(env, opts, "addPorts");
625
- if (proto == 255) return env.Undefined();
625
+ auto proto = parse_protocol(env, opts, "addPorts");
626
+ if (!proto) return env.Undefined();
626
627
 
627
628
  auto timeout_ms = parse_timeout(env, opts, "addPorts");
628
629
  if (!timeout_ms) return env.Undefined();
@@ -637,7 +638,7 @@ Napi::Value NftManager::AddPorts(const Napi::CallbackInfo& info) {
637
638
  return promise;
638
639
  }
639
640
 
640
- auto elems = make_port_elems_bulk(ports, proto);
641
+ auto elems = make_port_elems_bulk(ports, *proto);
641
642
  auto op = std::make_unique<BulkAddPortElemOp>(std::move(elems), *timeout_ms, config_, *set_idx);
642
643
  auto deferred = Napi::Promise::Deferred::New(env);
643
644
  auto promise = deferred.Promise();
@@ -668,8 +669,8 @@ Napi::Value NftManager::RemovePorts(const Napi::CallbackInfo& info) {
668
669
  auto set_idx = parse_set_name(env, opts, "removePorts", *config_, false, true);
669
670
  if (!set_idx) return env.Undefined();
670
671
 
671
- uint8_t proto = parse_protocol(env, opts, "removePorts");
672
- if (proto == 255) return env.Undefined();
672
+ auto proto = parse_protocol(env, opts, "removePorts");
673
+ if (!proto) return env.Undefined();
673
674
 
674
675
  std::vector<uint16_t> ports = parse_port_array(env, arr, "removePorts");
675
676
  if (env.IsExceptionPending()) return env.Undefined();
@@ -681,7 +682,7 @@ Napi::Value NftManager::RemovePorts(const Napi::CallbackInfo& info) {
681
682
  return promise;
682
683
  }
683
684
 
684
- auto elems = make_port_elems_bulk(ports, proto);
685
+ auto elems = make_port_elems_bulk(ports, *proto);
685
686
  auto op = std::make_unique<BulkDelPortElemOp>(std::move(elems), config_, *set_idx);
686
687
  auto deferred = Napi::Promise::Deferred::New(env);
687
688
  auto promise = deferred.Promise();
@@ -4,6 +4,10 @@
4
4
  #include <cmath>
5
5
  #include <cstring>
6
6
 
7
+ namespace {
8
+
9
+ // Parse IP string, validate via inet_pton, store binary form.
10
+ // Returns IpAddr with family=Invalid on failure.
7
11
  IpAddr parse_ip(const std::string& ip) {
8
12
  IpAddr result;
9
13
 
@@ -26,6 +30,157 @@ IpAddr parse_ip(const std::string& ip) {
26
30
  return result;
27
31
  }
28
32
 
33
+ // Big-endian increment by 1 with carry propagation.
34
+ void increment_ip(uint8_t* bytes, uint32_t len) {
35
+ for (int i = static_cast<int>(len) - 1; i >= 0; --i) {
36
+ if (++bytes[i] != 0) {
37
+ return; // no carry
38
+ }
39
+ }
40
+ // Full overflow (e.g., 255.255.255.255 + 1) — bytes wrap to all zeros.
41
+ }
42
+
43
+ // Compute exclusive end address from network + prefix_len.
44
+ // end = network address with host bits zeroed, then the prefix portion incremented.
45
+ // For /8 on 10.0.0.0: byte[0]=10, increment -> 11, rest stays 0 -> 11.0.0.0
46
+ void compute_cidr_end(const IpAddr& network, uint8_t prefix_len, IpAddr& end) {
47
+ end.family = network.family;
48
+ end.len = network.len;
49
+
50
+ uint32_t full_bytes = prefix_len / 8;
51
+ uint32_t remainder_bits = prefix_len % 8;
52
+
53
+ // Copy prefix bytes
54
+ for (uint32_t i = 0; i < full_bytes && i < network.len; ++i) {
55
+ end.bytes[i] = network.bytes[i];
56
+ }
57
+
58
+ if (remainder_bits > 0 && full_bytes < network.len) {
59
+ // Mask off host bits in the boundary byte
60
+ uint8_t mask = static_cast<uint8_t>(0xFF << (8 - remainder_bits));
61
+ end.bytes[full_bytes] = network.bytes[full_bytes] & mask;
62
+ }
63
+
64
+ // Zero all bytes after the prefix boundary
65
+ uint32_t start_zero = full_bytes + (remainder_bits > 0 ? 1 : 0);
66
+ for (uint32_t i = start_zero; i < network.len; ++i) {
67
+ end.bytes[i] = 0;
68
+ }
69
+
70
+ // Increment the prefix portion to get the exclusive end.
71
+ // We increment at bit position prefix_len. This is equivalent to adding
72
+ // 1 to the (prefix_len)-bit number formed by the first prefix_len bits.
73
+ if (remainder_bits > 0) {
74
+ // The boundary byte has some prefix bits. We need to increment
75
+ // at the bit position. Add (1 << (8 - remainder_bits)) to that byte,
76
+ // with carry propagation into earlier bytes.
77
+ uint8_t add_val = static_cast<uint8_t>(1 << (8 - remainder_bits));
78
+ uint16_t carry = add_val;
79
+ for (int i = static_cast<int>(full_bytes); i >= 0 && carry > 0; --i) {
80
+ carry += end.bytes[i];
81
+ end.bytes[i] = static_cast<uint8_t>(carry & 0xFF);
82
+ carry >>= 8;
83
+ }
84
+ } else {
85
+ // Prefix ends on a byte boundary. Increment the last prefix byte
86
+ // with carry into earlier bytes.
87
+ if (full_bytes > 0) {
88
+ uint16_t carry = 1;
89
+ for (int i = static_cast<int>(full_bytes) - 1; i >= 0 && carry > 0; --i) {
90
+ carry += end.bytes[i];
91
+ end.bytes[i] = static_cast<uint8_t>(carry & 0xFF);
92
+ carry >>= 8;
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ } // namespace
99
+
100
+ CidrAddr parse_ip_or_cidr(const std::string& input) {
101
+ CidrAddr result{};
102
+
103
+ auto slash_pos = input.find('/');
104
+
105
+ if (slash_pos == std::string::npos) {
106
+ // Plain IP address
107
+ IpAddr ip = parse_ip(input);
108
+ if (ip.family == IpFamily::Invalid) {
109
+ return result;
110
+ }
111
+
112
+ result.network = ip;
113
+
114
+ // end = ip + 1
115
+ result.end = ip;
116
+ increment_ip(result.end.bytes.data(), result.end.len);
117
+ return result;
118
+ }
119
+
120
+ // CIDR notation: "base/prefix"
121
+ std::string base_str = input.substr(0, slash_pos);
122
+ std::string prefix_str = input.substr(slash_pos + 1);
123
+
124
+ // Reject empty prefix or non-numeric prefix
125
+ if (prefix_str.empty()) {
126
+ return result;
127
+ }
128
+ for (char c : prefix_str) {
129
+ if (c < '0' || c > '9') {
130
+ return result;
131
+ }
132
+ }
133
+
134
+ // Manual parse — no exceptions needed (digits already validated above).
135
+ // Reject prefix strings longer than 3 chars (max valid: "128").
136
+ if (prefix_str.size() > 3) {
137
+ return result;
138
+ }
139
+ int prefix = 0;
140
+ for (char c : prefix_str) {
141
+ prefix = prefix * 10 + (c - '0');
142
+ }
143
+
144
+ IpAddr ip = parse_ip(base_str);
145
+ if (ip.family == IpFamily::Invalid) {
146
+ return result;
147
+ }
148
+
149
+ uint8_t max_prefix = (ip.family == IpFamily::IPv4) ? 32 : 128;
150
+
151
+ // Reject /0 (too dangerous) and prefix > max
152
+ if (prefix < 1 || prefix > max_prefix) {
153
+ return result;
154
+ }
155
+
156
+ auto prefix_len = static_cast<uint8_t>(prefix);
157
+
158
+ // Verify host bits are zero
159
+ uint32_t full_bytes = prefix_len / 8;
160
+ uint32_t remainder_bits = prefix_len % 8;
161
+
162
+ // Check boundary byte: host bits must be zero
163
+ if (remainder_bits > 0 && full_bytes < ip.len) {
164
+ uint8_t mask = static_cast<uint8_t>(0xFF >> remainder_bits);
165
+ if ((ip.bytes[full_bytes] & mask) != 0) {
166
+ return result; // host bits set in boundary byte
167
+ }
168
+ }
169
+
170
+ // Check all bytes after the prefix boundary must be zero
171
+ uint32_t start_check = full_bytes + (remainder_bits > 0 ? 1 : 0);
172
+ for (uint32_t i = start_check; i < ip.len; ++i) {
173
+ if (ip.bytes[i] != 0) {
174
+ return result; // host bits set
175
+ }
176
+ }
177
+
178
+ result.network = ip;
179
+
180
+ compute_cidr_end(ip, prefix_len, result.end);
181
+ return result;
182
+ }
183
+
29
184
  PortVal parse_port(double value) {
30
185
  if (std::isnan(value) || value < 0.0 || value > 65535.0) {
31
186
  return {0, false};
package/src/validation.h CHANGED
@@ -16,9 +16,18 @@ struct IpAddr {
16
16
  uint32_t len = 0;
17
17
  };
18
18
 
19
- // Parse IP string, validate via inet_pton, store binary form.
20
- // Returns IpAddr with family=Invalid on failure.
21
- [[nodiscard]] IpAddr parse_ip(const std::string& ip);
19
+ struct CidrAddr {
20
+ IpAddr network; // network address (e.g., 10.0.0.0)
21
+ IpAddr end; // exclusive end address (e.g., 11.0.0.0 for /8)
22
+ };
23
+
24
+ // Parse IP string or CIDR notation. Accepts:
25
+ // "1.2.3.4" -> CidrAddr{network=1.2.3.4, end=1.2.3.5}
26
+ // "10.0.0.0/8" -> CidrAddr{network=10.0.0.0, end=11.0.0.0}
27
+ // "2001:db8::/32" -> CidrAddr{network=2001:db8::, end=2001:db9::}
28
+ // Returns CidrAddr with network.family=Invalid on failure.
29
+ // Rejects: host bits set (192.168.1.1/24), prefix > 32/128, /0 (too dangerous).
30
+ [[nodiscard]] CidrAddr parse_ip_or_cidr(const std::string& input);
22
31
 
23
32
  struct PortVal {
24
33
  uint16_t port;