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 +9 -9
- package/lib/index.d.ts +29 -29
- package/lib/index.js +0 -1
- package/package.json +1 -1
- package/prebuilds/linux-arm64/nftables-napi.node +0 -0
- package/prebuilds/linux-x64/nftables-napi.node +0 -0
- package/src/netlink/constants.h +6 -0
- package/src/netlink/parsed_addr.h +2 -1
- package/src/netlink/set_ops.cpp +4 -3
- package/src/netlink/table_ops.cpp +6 -3
- package/src/nft_manager.cpp +46 -45
- package/src/validation.cpp +155 -0
- package/src/validation.h +12 -3
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
-
*
|
|
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
|
-
|
|
16
|
+
ingressAddrSets: string[];
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
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
|
-
|
|
22
|
+
egressAddrSets?: string[];
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
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
|
-
|
|
29
|
+
egressPortSets?: string[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** Options for adding a single address. */
|
|
33
33
|
export interface AddAddressOptions {
|
|
34
|
-
/** IPv4 or
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
131
|
-
* - Forward chain with log + counter + drop rules (for
|
|
132
|
-
* - Output chain with counter + drop rules (for
|
|
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 (
|
|
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 (
|
|
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
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/src/netlink/constants.h
CHANGED
|
@@ -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
|
|
package/src/netlink/set_ops.cpp
CHANGED
|
@@ -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[
|
|
158
|
-
key[
|
|
159
|
-
key[
|
|
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
|
|
package/src/nft_manager.cpp
CHANGED
|
@@ -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
|
|
16
|
+
static ParsedAddr to_parsed_addr(const CidrAddr& cidr) {
|
|
17
17
|
ParsedAddr pa{};
|
|
18
|
-
pa.family = (
|
|
19
|
-
pa.len =
|
|
20
|
-
std::memcpy(pa.bytes,
|
|
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
|
-
|
|
38
|
-
if (
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
268
|
-
if (!opts.Has("
|
|
269
|
-
Napi::TypeError::New(env, "NftManager: '
|
|
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("
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
303
|
-
std::vector<std::string> out_sets = parse_optional_string_array(env, opts, "
|
|
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
|
|
307
|
-
std::vector<std::string> out_port_sets = parse_optional_string_array(env, opts, "
|
|
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
|
-
|
|
397
|
-
if (
|
|
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(
|
|
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
|
-
|
|
436
|
-
if (
|
|
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(
|
|
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
|
-
|
|
556
|
-
if (proto
|
|
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
|
-
|
|
590
|
-
if (proto
|
|
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
|
-
|
|
625
|
-
if (proto
|
|
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
|
-
|
|
672
|
-
if (proto
|
|
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();
|
package/src/validation.cpp
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|