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