nftables-napi 0.3.0 → 0.4.2

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/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
 
@@ -31,7 +31,7 @@ export interface NftManagerOptions {
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
36
  /** Target set name (must match one from constructor's ingressAddrSets or egressAddrSets). */
37
37
  set: string;
@@ -41,7 +41,7 @@ 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
46
  /** Target set name (must match one from constructor's ingressAddrSets or egressAddrSets). */
47
47
  set: string;
@@ -49,7 +49,7 @@ export interface RemoveAddressOptions {
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
54
  /** Target set name (must match one from constructor's ingressAddrSets or egressAddrSets). */
55
55
  set: string;
@@ -59,7 +59,7 @@ 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
64
  /** Target set name (must match one from constructor's ingressAddrSets or egressAddrSets). */
65
65
  set: string;
@@ -145,8 +145,8 @@ 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.
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
150
  * Works with both input sets (ingressAddrSets) and output sets (egressAddrSets).
151
151
  *
152
152
  * @param options - Address, target set name, and optional timeout.
@@ -156,7 +156,7 @@ 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
161
  * Works with both input sets (ingressAddrSets) and output sets (egressAddrSets).
162
162
  *
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nftables-napi",
3
- "version": "0.3.0",
3
+ "version": "0.4.2",
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
 
@@ -64,13 +64,21 @@ static NlResult bulk_set_elem_op(
64
64
  nftnl_set_set_u32(s.get(), NFTNL_SET_KEY_LEN, key_len);
65
65
 
66
66
  for (size_t i = offset; i < end; ++i) {
67
- auto* e = nftnl_set_elem_alloc();
68
- if (!e) return {false, "nftnl_set_elem_alloc failed"};
69
- nftnl_set_elem_set(e, NFTNL_SET_ELEM_KEY, family_addrs[i]->bytes, family_addrs[i]->len);
67
+ // Start element: carries the key, timeout, and counters
68
+ auto* e_start = nftnl_set_elem_alloc();
69
+ if (!e_start) return {false, "nftnl_set_elem_alloc failed"};
70
+ nftnl_set_elem_set(e_start, NFTNL_SET_ELEM_KEY, family_addrs[i]->bytes, family_addrs[i]->len);
70
71
  if (timeout_ms > 0) {
71
- nftnl_set_elem_set_u64(e, NFTNL_SET_ELEM_TIMEOUT, timeout_ms);
72
+ nftnl_set_elem_set_u64(e_start, NFTNL_SET_ELEM_TIMEOUT, timeout_ms);
72
73
  }
73
- nftnl_set_elem_add(s.get(), e);
74
+ nftnl_set_elem_add(s.get(), e_start);
75
+
76
+ // End element: exclusive upper bound with INTERVAL_END flag
77
+ auto* e_end = nftnl_set_elem_alloc();
78
+ if (!e_end) return {false, "nftnl_set_elem_alloc failed"};
79
+ nftnl_set_elem_set(e_end, NFTNL_SET_ELEM_KEY, family_addrs[i]->end_bytes, family_addrs[i]->len);
80
+ nftnl_set_elem_set_u32(e_end, NFTNL_SET_ELEM_FLAGS, NFT_SET_ELEM_INTERVAL_END);
81
+ nftnl_set_elem_add(s.get(), e_end);
74
82
  }
75
83
 
76
84
  NlBatch batch;
@@ -154,9 +162,9 @@ static NlResult bulk_port_elem_op(
154
162
  if (!e) return {false, "nftnl_set_elem_alloc failed"};
155
163
  // 8-byte concatenated key: [proto][pad:3][port_hi][port_lo][pad:2]
156
164
  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);
165
+ key[nft::PORT_KEY_PROTO_OFFSET] = elems[i].proto;
166
+ key[nft::PORT_KEY_PORT_HI_OFFSET] = static_cast<uint8_t>(elems[i].port >> 8);
167
+ key[nft::PORT_KEY_PORT_LO_OFFSET] = static_cast<uint8_t>(elems[i].port & 0xFF);
160
168
  nftnl_set_elem_set(e, NFTNL_SET_ELEM_KEY, key, sizeof(key));
161
169
  if (timeout_ms > 0) {
162
170
  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) {
@@ -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;