@push.rocks/smartnftables 4.4.0 → 4.5.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/changelog.md +7 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +4 -4
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +4 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/package.json +1 -1
- package/readme.md +52 -1
- package/rust/src/egress.compile.rs +105 -38
- package/rust/src/egress.hostgrant.rs +74 -42
- package/rust/src/egress.keys.rs +380 -55
- package/rust/src/egress.leased.rs +71 -42
- package/rust/src/egress.poolguard.rs +8 -13
- package/rust/src/egress.private.rs +32 -37
- package/rust/src/egress.published.rs +74 -47
- package/rust/src/egress.router.rs +1 -1
- package/rust/src/egress.rs +8 -0
- package/rust/src/egress.workloadgrant.rs +30 -28
- package/rust/src/egress_keys_tests.rs +150 -0
- package/rust/src/egress_tests.rs +47 -4
- package/rust/src/owner.rs +96 -28
- package/rust/src/owner_listing_tests.rs +634 -0
- package/rust/src/owner_tests.rs +3 -0
- package/ts/00_commitinfo_data.ts +1 -1
package/changelog.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-10-06 - 4.5.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Typed sets: `nft` decodes every managed set and lists the rules that look them up, up to nftables' own limit below (Pallet lab RUN 9, O9-1). Every set was declared with key type 0, data type 0 and no userdata, so nftables 1.0.9 aborted listing any rule that looked one up (`netlink_delinearize.c`: `dtype != &invalid_type`) and segfaulted listing the sets themselves. Each key field now has one descriptor (length, nft type, byte order, `typeof` expression) from which both the rule's loads and the set declaration derive, and a lookup whose loads do not fit its set's key refuses to compile (`INVALID`). Sets carry their key and data types and nft's userdata (byte orders, `typeof` key and data, data interval) byte for byte as nftables 1.0.9 writes them for the same `typeof` declaration; keys beyond four fields carry their type and byte order alone, as nft cannot describe them by `typeof`. Policy digests are unchanged; compiled bytes change for every graph with sets, and stripping the types again reproduces the 4.4.0 goldens byte for byte. nftables itself still crashes listing a rule that tests membership of seven or more concatenated fields (host grants, workload grants, published flows, ported private rules), also on nftables master; readme "Listing the managed tables with `nft`" states what lists today. Tests: nft-captured userdata bytes (`egress_keys_tests`), and in the isolated guest, now carrying the host's nftables 1.0.9, every declared set against the set nft creates from the same `typeof`, every set, chain and ruleset listed, the nft crash pinned, and the untyped control aborting.
|
|
8
|
+
- Upgrading over a table applied by 4.4.x or earlier: its graph with untyped sets is adopted, inspected, verified (`verify_external`) and released as the applied graph, and `reconcile()` never treats it as already applied but replaces the whole graph with typed sets in its single batch, including a retry of the transition that applied it. This acceptance is legacy for kernel state left by 4.4.x and goes with the next major. The step is one-way per boot: 4.4.x refuses typed sets (`CONFLICT`). Tests: graph agreement (exact, untyped, mixed, retyped, changed element) and, in the isolated guest, adoption and replacement of a 4.4.x graph by a new and by the same transition (claim plus one batch), external verification and release.
|
|
9
|
+
|
|
3
10
|
## 2026-10-06 - 4.4.0
|
|
4
11
|
|
|
5
12
|
### Features
|
|
Binary file
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": "tsrust.build-provenance.v2",
|
|
3
|
-
"binarySha256": "
|
|
3
|
+
"binarySha256": "f30fb315a0059adfc4afe0728a8304fe14a5a45e2437cb4e76d5c79c7dbf8917",
|
|
4
4
|
"buildInfo": {
|
|
5
5
|
"projectName": "@push.rocks/smartnftables",
|
|
6
|
-
"projectVersion": "4.
|
|
7
|
-
"gitCommit": "
|
|
6
|
+
"projectVersion": "4.5.0",
|
|
7
|
+
"gitCommit": "f648edff675c7693b2ec8bca6138bd7d1e2da65b",
|
|
8
8
|
"gitDirty": false,
|
|
9
|
-
"builtAt": "2026-10-
|
|
9
|
+
"builtAt": "2026-10-06T05:09:47.306Z",
|
|
10
10
|
"tsrustVersion": "4.0.0",
|
|
11
11
|
"binary": "smartnftables",
|
|
12
12
|
"target": "linux_amd64_musl"
|
|
Binary file
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": "tsrust.build-provenance.v2",
|
|
3
|
-
"binarySha256": "
|
|
3
|
+
"binarySha256": "3347480d9c39a2ee4891c13f7c6a12d50d9175e15776f1c6999e1574a491dfd8",
|
|
4
4
|
"buildInfo": {
|
|
5
5
|
"projectName": "@push.rocks/smartnftables",
|
|
6
|
-
"projectVersion": "4.
|
|
7
|
-
"gitCommit": "
|
|
6
|
+
"projectVersion": "4.5.0",
|
|
7
|
+
"gitCommit": "f648edff675c7693b2ec8bca6138bd7d1e2da65b",
|
|
8
8
|
"gitDirty": false,
|
|
9
|
-
"builtAt": "2026-10-
|
|
9
|
+
"builtAt": "2026-10-06T05:09:57.015Z",
|
|
10
10
|
"tsrustVersion": "4.0.0",
|
|
11
11
|
"binary": "smartnftables",
|
|
12
12
|
"target": "linux_arm64_musl"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartnftables',
|
|
6
|
-
version: '4.
|
|
6
|
+
version: '4.5.0',
|
|
7
7
|
description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwyQkFBMkI7SUFDakMsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLG1IQUFtSDtDQUNqSSxDQUFBIn0=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartnftables",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
package/readme.md
CHANGED
|
@@ -1041,6 +1041,56 @@ and netdev hooks do not see routed packets and are not reported. Guard
|
|
|
1041
1041
|
either Docker chain or any legacy table is present. The read is a point in time;
|
|
1042
1042
|
the caller still owns exclusive authority over who may add a forward owner later.
|
|
1043
1043
|
|
|
1044
|
+
### Listing the managed tables with `nft`
|
|
1045
|
+
|
|
1046
|
+
Every set the compiler declares carries the types `nft` needs to decode it: the
|
|
1047
|
+
key and data types, their byte orders and, for keys of up to four fields, the
|
|
1048
|
+
`typeof` expressions, byte for byte as nftables 1.0.9 writes them for the same
|
|
1049
|
+
`typeof` declaration (`nft add set ... '{ typeof meta iif . meta iifname; ... }'`).
|
|
1050
|
+
A lookup's loads must fit its set's declared key field by field, or compiling
|
|
1051
|
+
refuses with `INVALID`. Before 4.5.0 every set was declared with key type 0 and
|
|
1052
|
+
no userdata, so `nft list` aborted (`dtype != &invalid_type`) or crashed on any
|
|
1053
|
+
table that held one.
|
|
1054
|
+
|
|
1055
|
+
What an operator can list today with nftables 1.0.9:
|
|
1056
|
+
|
|
1057
|
+
- `nft list set|map inet <table> <set>` for every set. Keys of up to four fields
|
|
1058
|
+
list as `typeof ...`; longer keys as `type ...`. nftables 1.0.9 cannot
|
|
1059
|
+
describe a longer key by `typeof` (it stops at four fields, `NFT_REG32_SIZE`),
|
|
1060
|
+
and its 32-bit type holds at most five fields, so the elements of a key of six
|
|
1061
|
+
or more fields, or of five with a `ct zone`, print wrongly. The generation map
|
|
1062
|
+
`leased_generation` lists as `type 0 : ct_label`: its key is the zone padded to
|
|
1063
|
+
a register, as every release before typed sets declared it.
|
|
1064
|
+
- `nft list chain` for every chain whose rules test membership of keys of at most
|
|
1065
|
+
six fields. A map lookup of a key of more than four fields lists, but nft may
|
|
1066
|
+
render its data wrongly or drop the statement (seen for the leased flow maps
|
|
1067
|
+
and `published_symmetric`).
|
|
1068
|
+
- `nft list ruleset`, `nft list table` and `nft list chain` crash (SIGSEGV) on any
|
|
1069
|
+
rule that tests membership of a concatenation of seven or more fields. This is
|
|
1070
|
+
an nftables defect, also on nftables master: `expr_postprocess_concat` walks
|
|
1071
|
+
past the five fields the 32-bit type holds, for sets nft created itself just
|
|
1072
|
+
as for these. Host grants (8 or 10 fields), workload grants (8), published
|
|
1073
|
+
flows (8) and ported private rules (7) use such lookups, so on nodes that use
|
|
1074
|
+
them `nft list ruleset` crashes until a fixed nftables is installed. List the
|
|
1075
|
+
sets and the other chains individually meanwhile.
|
|
1076
|
+
|
|
1077
|
+
The isolated qualification below proves each of these against the host's
|
|
1078
|
+
nftables 1.0.9 and pins the crash, so it fails once a fixed `nft` lands.
|
|
1079
|
+
|
|
1080
|
+
#### Tables applied before 4.5.0
|
|
1081
|
+
|
|
1082
|
+
A table applied by 4.4.x or earlier holds the same graph with untyped sets, and
|
|
1083
|
+
Linux enforces it identically: the kernel stores and dumps the key type and
|
|
1084
|
+
userdata but never interprets them. This release adopts such a graph, inspects,
|
|
1085
|
+
verifies and releases it as the applied graph, and never treats it as already
|
|
1086
|
+
applied: the next `reconcile()`, including a retry of the transition that
|
|
1087
|
+
applied it, replaces the whole graph with typed sets in the same single batch
|
|
1088
|
+
as any replacement, so enforcement never lapses. No migration step is needed.
|
|
1089
|
+
The transition is one-way per boot: 4.4.x refuses a table holding typed sets
|
|
1090
|
+
(`CONFLICT`), so a downgrade on the same boot cannot adopt it, while a reboot
|
|
1091
|
+
drops the table for either version. The acceptance of untyped sets is legacy
|
|
1092
|
+
support for kernel state left by 4.4.x and goes with the next major release.
|
|
1093
|
+
|
|
1044
1094
|
### Native qualification
|
|
1045
1095
|
|
|
1046
1096
|
The separate [Docker fixture](test/native/readme.docker.md) exercises the native
|
|
@@ -1052,7 +1102,8 @@ Its checked-in input manifest and per-run receipts identify the tested artifacts
|
|
|
1052
1102
|
snapshot and bounded-subprocess tests. The ignored native cases require the explicitly marked disposable guest
|
|
1053
1103
|
from `test/native/qualify.py`; requesting them on an ordinary host fails its scope
|
|
1054
1104
|
check. Qualification uses an offline Linux 6.18.35 x86_64 guest with no host disks,
|
|
1055
|
-
mounts or external network backend
|
|
1105
|
+
mounts or external network backend; it carries the host's nftables 1.0.9 binary
|
|
1106
|
+
and libraries for the listing proof. One VirtIO device connects only to a singleton
|
|
1056
1107
|
QEMU-internal hub; packet-path tests use guest-owned namespaces, veth pairs and TUN.
|
|
1057
1108
|
It covers
|
|
1058
1109
|
UDP/TCP grants, direct-IP denial, source spoofing, IPv6 denial, renamed interfaces,
|
|
@@ -21,6 +21,8 @@ mod router;
|
|
|
21
21
|
#[path = "egress.workloadgrant.rs"]
|
|
22
22
|
mod workloadgrant;
|
|
23
23
|
|
|
24
|
+
use keys::Field;
|
|
25
|
+
|
|
24
26
|
/// The atomic replacement budget. One replacement is one nfnetlink batch in one
|
|
25
27
|
/// `sendmsg`, and Linux refuses a netlink message above the socket send buffer:
|
|
26
28
|
/// `SO_SNDBUF` clamped to `net.core.wmem_max` and doubled, 425,984 bytes with the
|
|
@@ -54,6 +56,22 @@ pub(super) struct Program<'a> {
|
|
|
54
56
|
bytes: usize,
|
|
55
57
|
element_bytes: usize,
|
|
56
58
|
operations: Vec<(u16, Vec<Attr>)>,
|
|
59
|
+
/// Each declared set's key and a map's data.
|
|
60
|
+
declared: std::collections::BTreeMap<String, Declared>,
|
|
61
|
+
}
|
|
62
|
+
/// One declared set: its key fields and a map's data fields.
|
|
63
|
+
struct Declared {
|
|
64
|
+
key: Vec<Field>,
|
|
65
|
+
data: Option<Vec<Field>>,
|
|
66
|
+
}
|
|
67
|
+
/// A declared set as nft names it: its key's and a map's data's `typeof`
|
|
68
|
+
/// expressions, and whether it is an interval set.
|
|
69
|
+
#[cfg(test)]
|
|
70
|
+
#[derive(Clone, Debug)]
|
|
71
|
+
pub(crate) struct SetDeclaration {
|
|
72
|
+
pub(crate) key: Vec<&'static str>,
|
|
73
|
+
pub(crate) data: Option<Vec<&'static str>>,
|
|
74
|
+
pub(crate) interval: bool,
|
|
57
75
|
}
|
|
58
76
|
impl<'a> Program<'a> {
|
|
59
77
|
fn push(&mut self, operation: u16, attributes: Vec<Attr>) -> Result<()> {
|
|
@@ -76,26 +94,59 @@ impl<'a> Program<'a> {
|
|
|
76
94
|
self.element_bytes += wire::encode_attrs(&attributes).len() + 20;
|
|
77
95
|
self.admit(12, attributes)
|
|
78
96
|
}
|
|
79
|
-
///
|
|
80
|
-
///
|
|
97
|
+
/// The key's type and length and a map's data type and length, as nft
|
|
98
|
+
/// declares them, after which the userdata carries their byte orders and
|
|
99
|
+
/// `typeof` descriptions. Remembers the lengths every element must have.
|
|
100
|
+
fn typed(&mut self, name: &str, key: &[Field], data: Option<&[Field]>) -> Vec<Attr> {
|
|
101
|
+
let mut attributes = vec![
|
|
102
|
+
Attr::u32(4, keys::datatype(key)),
|
|
103
|
+
Attr::u32(5, keys::key_length(key)),
|
|
104
|
+
];
|
|
105
|
+
if let Some(data) = data {
|
|
106
|
+
attributes.extend([
|
|
107
|
+
Attr::u32(6, keys::datatype(data)),
|
|
108
|
+
Attr::u32(7, keys::data_length(data)),
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
self.declared.insert(
|
|
112
|
+
name.to_string(),
|
|
113
|
+
Declared {
|
|
114
|
+
key: key.to_vec(),
|
|
115
|
+
data: data.map(<[Field]>::to_vec),
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
attributes
|
|
119
|
+
}
|
|
120
|
+
/// Whether an element's key and data have the lengths its set declares.
|
|
121
|
+
fn fits(&self, name: &str, key: &[u8], data: Option<&Vec<u8>>) -> Result<()> {
|
|
122
|
+
let declared = self.declared.get(name).ok_or(crate::Error::Invalid)?;
|
|
123
|
+
let length = |bytes: &[u8]| u32::try_from(bytes.len()).ok();
|
|
124
|
+
require(
|
|
125
|
+
length(key) == Some(keys::key_length(&declared.key))
|
|
126
|
+
&& data.map(|data| length(data))
|
|
127
|
+
== declared
|
|
128
|
+
.data
|
|
129
|
+
.as_deref()
|
|
130
|
+
.map(|data| Some(keys::data_length(data))),
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
/// One named exact-key hash set, or a map to `data`. CONSTANT like every
|
|
134
|
+
/// set here.
|
|
81
135
|
fn exact_set(
|
|
82
136
|
&mut self,
|
|
83
137
|
name: &str,
|
|
84
138
|
id: u32,
|
|
85
|
-
|
|
86
|
-
|
|
139
|
+
key: &[Field],
|
|
140
|
+
data: Option<&[Field]>,
|
|
87
141
|
) -> Result<()> {
|
|
88
142
|
let mut attributes = vec![
|
|
89
143
|
Attr::string(1, self.table),
|
|
90
144
|
Attr::string(2, name),
|
|
91
|
-
Attr::u32(3, 2 | if
|
|
92
|
-
Attr::u32(4, 0),
|
|
93
|
-
Attr::u32(5, key_length),
|
|
145
|
+
Attr::u32(3, 2 | if data.is_some() { 8 } else { 0 }),
|
|
94
146
|
];
|
|
95
|
-
|
|
96
|
-
attributes.extend([Attr::u32(6, 0), Attr::u32(7, length)]);
|
|
97
|
-
}
|
|
147
|
+
attributes.extend(self.typed(name, key, data));
|
|
98
148
|
attributes.push(Attr::u32(10, id));
|
|
149
|
+
attributes.push(Attr::bytes(13, keys::userdata(key, data)?));
|
|
99
150
|
self.push(9, attributes)
|
|
100
151
|
}
|
|
101
152
|
fn exact_elements(
|
|
@@ -103,6 +154,9 @@ impl<'a> Program<'a> {
|
|
|
103
154
|
name: &str,
|
|
104
155
|
elements: Vec<(Vec<u8>, Option<Vec<u8>>)>,
|
|
105
156
|
) -> Result<()> {
|
|
157
|
+
for (key, data) in &elements {
|
|
158
|
+
self.fits(name, key, data.as_ref())?;
|
|
159
|
+
}
|
|
106
160
|
for chunk in elements.chunks(256) {
|
|
107
161
|
let list = chunk
|
|
108
162
|
.iter()
|
|
@@ -122,21 +176,6 @@ impl<'a> Program<'a> {
|
|
|
122
176
|
}
|
|
123
177
|
Ok(())
|
|
124
178
|
}
|
|
125
|
-
/// One named exact-match set. CONSTANT: its elements cannot change once a
|
|
126
|
-
/// rule binds it, so replacement always creates a complete new set.
|
|
127
|
-
fn set(&mut self, name: &str, id: u32, key_length: usize) -> Result<()> {
|
|
128
|
-
self.push(
|
|
129
|
-
9,
|
|
130
|
-
vec![
|
|
131
|
-
Attr::string(1, self.table),
|
|
132
|
-
Attr::string(2, name),
|
|
133
|
-
Attr::u32(3, 2),
|
|
134
|
-
Attr::u32(4, 0),
|
|
135
|
-
Attr::u32(5, key_length as u32),
|
|
136
|
-
Attr::u32(10, id),
|
|
137
|
-
],
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
179
|
/// The elements of one set, 256 to a message so every nested attribute
|
|
141
180
|
/// stays far below netlink's 16-bit attribute length. Elements have their
|
|
142
181
|
/// own budget: a replacement deletes the previous set, and so its elements,
|
|
@@ -146,27 +185,21 @@ impl<'a> Program<'a> {
|
|
|
146
185
|
}
|
|
147
186
|
/// One named CONSTANT set of concatenated fields in which every field is an
|
|
148
187
|
/// inclusive interval (the kernel's pipapo backend), optionally a map to
|
|
149
|
-
/// `
|
|
150
|
-
/// 32-bit registers of the key.
|
|
188
|
+
/// `data`. Each field occupies whole 32-bit registers of the key.
|
|
151
189
|
fn interval_set(
|
|
152
190
|
&mut self,
|
|
153
191
|
name: &str,
|
|
154
192
|
id: u32,
|
|
155
|
-
fields: &[
|
|
156
|
-
|
|
193
|
+
fields: &[Field],
|
|
194
|
+
data: Option<&[Field]>,
|
|
157
195
|
) -> Result<()> {
|
|
158
|
-
let key_length: u32 = fields.iter().map(|length| length.div_ceil(4) * 4).sum();
|
|
159
196
|
let mut attributes = vec![
|
|
160
197
|
Attr::string(1, self.table),
|
|
161
198
|
Attr::string(2, name),
|
|
162
199
|
// CONSTANT | INTERVAL | CONCAT, and MAP for a map.
|
|
163
|
-
Attr::u32(3, 2 | 4 | 0x80 | if
|
|
164
|
-
Attr::u32(4, 0),
|
|
165
|
-
Attr::u32(5, key_length),
|
|
200
|
+
Attr::u32(3, 2 | 4 | 0x80 | if data.is_some() { 8 } else { 0 }),
|
|
166
201
|
];
|
|
167
|
-
|
|
168
|
-
attributes.extend([Attr::u32(6, 0), Attr::u32(7, length)]);
|
|
169
|
-
}
|
|
202
|
+
attributes.extend(self.typed(name, fields, data));
|
|
170
203
|
attributes.extend([
|
|
171
204
|
Attr::nested(
|
|
172
205
|
9,
|
|
@@ -174,11 +207,12 @@ impl<'a> Program<'a> {
|
|
|
174
207
|
2,
|
|
175
208
|
fields
|
|
176
209
|
.iter()
|
|
177
|
-
.map(|
|
|
210
|
+
.map(|field| Attr::nested(1, vec![Attr::u32(1, field.length)]))
|
|
178
211
|
.collect(),
|
|
179
212
|
)],
|
|
180
213
|
),
|
|
181
214
|
Attr::u32(10, id),
|
|
215
|
+
Attr::bytes(13, keys::userdata(fields, data)?),
|
|
182
216
|
]);
|
|
183
217
|
self.push(9, attributes)
|
|
184
218
|
}
|
|
@@ -186,6 +220,10 @@ impl<'a> Program<'a> {
|
|
|
186
220
|
/// An element whose bounds are equal carries no last key; Linux takes the
|
|
187
221
|
/// first as the last and dumps it the same way.
|
|
188
222
|
fn interval_elements(&mut self, name: &str, elements: Vec<IntervalElement>) -> Result<()> {
|
|
223
|
+
for element in &elements {
|
|
224
|
+
self.fits(name, &element.first, element.data.as_ref())?;
|
|
225
|
+
self.fits(name, &element.last, element.data.as_ref())?;
|
|
226
|
+
}
|
|
189
227
|
for chunk in elements.chunks(256) {
|
|
190
228
|
let list = chunk
|
|
191
229
|
.iter()
|
|
@@ -421,6 +459,34 @@ fn snat(address: &str, range: Option<(u16, u16)>) -> Result<Vec<Attr>> {
|
|
|
421
459
|
Ok(result)
|
|
422
460
|
}
|
|
423
461
|
pub(super) fn compile(prepared: &Prepared, table: &str) -> Result<Vec<(u16, Vec<Attr>)>> {
|
|
462
|
+
Ok(compiled(prepared, table)?.operations)
|
|
463
|
+
}
|
|
464
|
+
/// Every set the compiled graph declares, by name.
|
|
465
|
+
#[cfg(test)]
|
|
466
|
+
pub(super) fn declared(
|
|
467
|
+
prepared: &Prepared,
|
|
468
|
+
table: &str,
|
|
469
|
+
) -> Result<std::collections::BTreeMap<String, SetDeclaration>> {
|
|
470
|
+
let program = compiled(prepared, table)?;
|
|
471
|
+
let names = |fields: &[Field]| fields.iter().map(|field| field.name).collect();
|
|
472
|
+
let mut result = std::collections::BTreeMap::new();
|
|
473
|
+
for (kind, attributes) in &program.operations {
|
|
474
|
+
if *kind == 9 {
|
|
475
|
+
let name = wire::text(attributes, 2)?;
|
|
476
|
+
let declared = program.declared.get(&name).ok_or(crate::Error::Invalid)?;
|
|
477
|
+
result.insert(
|
|
478
|
+
name,
|
|
479
|
+
SetDeclaration {
|
|
480
|
+
key: names(&declared.key),
|
|
481
|
+
data: declared.data.as_deref().map(names),
|
|
482
|
+
interval: wire::number(attributes, 3)? & 4 != 0,
|
|
483
|
+
},
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
Ok(result)
|
|
488
|
+
}
|
|
489
|
+
fn compiled<'a>(prepared: &Prepared, table: &'a str) -> Result<Program<'a>> {
|
|
424
490
|
let mut program = Program {
|
|
425
491
|
table,
|
|
426
492
|
stamp: format!("snft2:{}:{}", prepared.policy.revision, prepared.digest),
|
|
@@ -428,11 +494,12 @@ pub(super) fn compile(prepared: &Prepared, table: &str) -> Result<Vec<(u16, Vec<
|
|
|
428
494
|
bytes: 0,
|
|
429
495
|
element_bytes: 0,
|
|
430
496
|
operations: Vec::new(),
|
|
497
|
+
declared: std::collections::BTreeMap::new(),
|
|
431
498
|
};
|
|
432
499
|
match &prepared.policy.scope {
|
|
433
500
|
Scope::RouterEgress(scope) => router::compile(&mut program, scope)?,
|
|
434
501
|
Scope::HostTransit(scope) => host::compile(&mut program, scope)?,
|
|
435
502
|
Scope::AllocationPoolGuard(scope) => poolguard::compile(&mut program, scope)?,
|
|
436
503
|
}
|
|
437
|
-
Ok(program
|
|
504
|
+
Ok(program)
|
|
438
505
|
}
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
//! variable interface (index and name), the packet and conntrack protocols, the
|
|
5
5
|
//! live tuple and the originally tracked tuple. An element is one grant with its
|
|
6
6
|
//! live tuple equal to its original tuple, so translated flows never match.
|
|
7
|
+
use super::keys::{
|
|
8
|
+
Field, Loads, CT_ORIGINAL_DADDR, CT_ORIGINAL_DPORT, CT_ORIGINAL_SADDR, CT_PROTOCOL, IP_DADDR,
|
|
9
|
+
IP_SADDR, L4PROTO, OIF, OIFNAME, TH_DPORT,
|
|
10
|
+
};
|
|
7
11
|
use super::*;
|
|
8
12
|
|
|
9
13
|
/// Every exact host-origin flow of the scope, keyed on its full tuple.
|
|
@@ -11,6 +15,42 @@ pub(super) const TUPLE_SET: &str = "host_grant";
|
|
|
11
15
|
/// Router raw ingress runs before conntrack: exact arriving tuples only.
|
|
12
16
|
pub(super) const ARRIVAL_SET: &str = "host_grant_arrival";
|
|
13
17
|
|
|
18
|
+
/// The tuple key of a link-independent scope: both protocols, the live tuple
|
|
19
|
+
/// and the originally tracked tuple, as the original direction loads them.
|
|
20
|
+
pub(super) const TUPLE: [Field; 8] = [
|
|
21
|
+
L4PROTO,
|
|
22
|
+
CT_PROTOCOL,
|
|
23
|
+
IP_SADDR,
|
|
24
|
+
IP_DADDR,
|
|
25
|
+
TH_DPORT,
|
|
26
|
+
CT_ORIGINAL_SADDR,
|
|
27
|
+
CT_ORIGINAL_DADDR,
|
|
28
|
+
CT_ORIGINAL_DPORT,
|
|
29
|
+
];
|
|
30
|
+
/// The tuple key behind the variable interface: its index and name first.
|
|
31
|
+
const LINKED_TUPLE: [Field; 10] = [
|
|
32
|
+
OIF,
|
|
33
|
+
OIFNAME,
|
|
34
|
+
L4PROTO,
|
|
35
|
+
CT_PROTOCOL,
|
|
36
|
+
IP_SADDR,
|
|
37
|
+
IP_DADDR,
|
|
38
|
+
TH_DPORT,
|
|
39
|
+
CT_ORIGINAL_SADDR,
|
|
40
|
+
CT_ORIGINAL_DADDR,
|
|
41
|
+
CT_ORIGINAL_DPORT,
|
|
42
|
+
];
|
|
43
|
+
/// The exact arriving tuple: protocol, addresses and port.
|
|
44
|
+
const ARRIVAL: [Field; 4] = [L4PROTO, IP_SADDR, IP_DADDR, TH_DPORT];
|
|
45
|
+
|
|
46
|
+
fn tuple(linked: bool) -> &'static [Field] {
|
|
47
|
+
if linked {
|
|
48
|
+
&LINKED_TUPLE
|
|
49
|
+
} else {
|
|
50
|
+
&TUPLE
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
14
54
|
/// Register numbers as Linux dumps them: 16-byte aligned registers use the
|
|
15
55
|
/// legacy NFT_REG_1..4 names, every other 4-byte register NFT_REG32_xx.
|
|
16
56
|
pub(super) fn register(index: u32) -> u32 {
|
|
@@ -38,21 +78,14 @@ pub(super) fn payload_to(base: u32, offset: u32, length: u32, index: u32) -> Att
|
|
|
38
78
|
index,
|
|
39
79
|
)
|
|
40
80
|
}
|
|
41
|
-
|
|
81
|
+
/// A conntrack key of the original (0) or reply (1) tuple, or of neither.
|
|
82
|
+
pub(super) fn ct_to(key: u32, direction: Option<u8>, index: u32) -> Attr {
|
|
42
83
|
let mut data = vec![Attr::u32(2, key)];
|
|
43
|
-
if
|
|
44
|
-
data.push(Attr::bytes(3, vec![
|
|
84
|
+
if let Some(direction) = direction {
|
|
85
|
+
data.push(Attr::bytes(3, vec![direction]));
|
|
45
86
|
}
|
|
46
87
|
store("ct", data, index)
|
|
47
88
|
}
|
|
48
|
-
/// A conntrack key of the reply direction's tuple.
|
|
49
|
-
pub(super) fn ct_reply_to(key: u32, index: u32) -> Attr {
|
|
50
|
-
store(
|
|
51
|
-
"ct",
|
|
52
|
-
vec![Attr::u32(2, key), Attr::bytes(3, vec![1])],
|
|
53
|
-
index,
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
89
|
pub(super) fn lookup(set: &str) -> Attr {
|
|
57
90
|
expr(
|
|
58
91
|
"lookup",
|
|
@@ -72,26 +105,22 @@ fn word(bytes: &[u8]) -> Vec<u8> {
|
|
|
72
105
|
/// Key loads for one direction, then the lookup in `set`. `linked` adds the
|
|
73
106
|
/// variable interface: the outgoing interface of the original direction, the
|
|
74
107
|
/// incoming interface of the reply.
|
|
75
|
-
pub(super) fn tuple_lookup(set: &str, linked: bool, reply: bool) -> Vec<Attr
|
|
76
|
-
let mut
|
|
77
|
-
let mut index = 4;
|
|
108
|
+
pub(super) fn tuple_lookup(set: &str, linked: bool, reply: bool) -> Result<Vec<Attr>> {
|
|
109
|
+
let mut loads = Loads::new();
|
|
78
110
|
if linked {
|
|
79
|
-
|
|
80
|
-
result.push(meta_to(if reply { 6 } else { 7 }, index + 1));
|
|
81
|
-
index += 5;
|
|
111
|
+
loads.link(reply);
|
|
82
112
|
}
|
|
83
|
-
result.push(meta_to(16, index));
|
|
84
|
-
result.push(ct_to(10, false, index + 1));
|
|
85
113
|
// The grant's source and destination are the packet's own addresses in the
|
|
86
114
|
// original direction and swapped on the reply; the port is the workload's.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
loads
|
|
116
|
+
.protocols()
|
|
117
|
+
.address(!reply)
|
|
118
|
+
.address(reply)
|
|
119
|
+
.port(reply)
|
|
120
|
+
.original_address(true)
|
|
121
|
+
.original_address(false)
|
|
122
|
+
.original_port(false);
|
|
123
|
+
loads.lookup(set, tuple(linked))
|
|
95
124
|
}
|
|
96
125
|
pub(super) fn tuple_key(grant: &HostGrant, link: Option<&LocalLink>) -> Result<Vec<u8>> {
|
|
97
126
|
let protocol = word(&[protocol(&grant.protocol)]);
|
|
@@ -128,11 +157,15 @@ pub(super) fn tuple_set<'s>(
|
|
|
128
157
|
link: impl Fn(&HostGrant) -> Result<Option<&'s LocalLink>>,
|
|
129
158
|
) -> Result<()> {
|
|
130
159
|
let mut keys = Vec::new();
|
|
160
|
+
let mut linked = false;
|
|
131
161
|
for grant in grants {
|
|
132
|
-
|
|
162
|
+
let link = link(grant)?;
|
|
163
|
+
linked = link.is_some();
|
|
164
|
+
keys.push(tuple_key(grant, link)?);
|
|
133
165
|
}
|
|
134
|
-
|
|
135
|
-
|
|
166
|
+
// Every element has the declared key's length, so a scope never mixes them.
|
|
167
|
+
require(!keys.is_empty())?;
|
|
168
|
+
program.exact_set(TUPLE_SET, 1, tuple(linked), None)?;
|
|
136
169
|
program.elements(TUPLE_SET, keys)
|
|
137
170
|
}
|
|
138
171
|
pub(super) fn arrival_set(program: &mut Program<'_>, grants: &[HostGrant]) -> Result<()> {
|
|
@@ -144,18 +177,17 @@ pub(super) fn arrival_set(program: &mut Program<'_>, grants: &[HostGrant]) -> Re
|
|
|
144
177
|
key.extend(word(&grant.destination_port.to_be_bytes()));
|
|
145
178
|
keys.push(key);
|
|
146
179
|
}
|
|
147
|
-
program.
|
|
180
|
+
program.exact_set(ARRIVAL_SET, 2, &ARRIVAL, None)?;
|
|
148
181
|
program.elements(ARRIVAL_SET, keys)
|
|
149
182
|
}
|
|
150
183
|
/// Exact arriving tuple, before conntrack: protocol, addresses and port.
|
|
151
|
-
pub(super) fn arrival_lookup() -> Vec<Attr
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
lookup(ARRIVAL_SET)
|
|
158
|
-
]
|
|
184
|
+
pub(super) fn arrival_lookup() -> Result<Vec<Attr>> {
|
|
185
|
+
Loads::new()
|
|
186
|
+
.protocol()
|
|
187
|
+
.address(true)
|
|
188
|
+
.address(false)
|
|
189
|
+
.port(false)
|
|
190
|
+
.lookup(ARRIVAL_SET, &ARRIVAL)
|
|
159
191
|
}
|
|
160
192
|
|
|
161
193
|
/// Four admissions whatever the number of grants: the ESTABLISHED original
|
|
@@ -178,7 +210,7 @@ pub(super) fn admissions(
|
|
|
178
210
|
result
|
|
179
211
|
};
|
|
180
212
|
let mut established = envelope(false, 2);
|
|
181
|
-
established.extend(tuple_lookup(TUPLE_SET, linked, false));
|
|
213
|
+
established.extend(tuple_lookup(TUPLE_SET, linked, false)?);
|
|
182
214
|
program.end(chains[0], established, 1)?;
|
|
183
215
|
for transport in ["udp", "tcp"] {
|
|
184
216
|
let mut opening = envelope(false, 8);
|
|
@@ -186,10 +218,10 @@ pub(super) fn admissions(
|
|
|
186
218
|
if transport == "tcp" {
|
|
187
219
|
opening.extend(opening_tcp());
|
|
188
220
|
}
|
|
189
|
-
opening.extend(tuple_lookup(TUPLE_SET, linked, false));
|
|
221
|
+
opening.extend(tuple_lookup(TUPLE_SET, linked, false)?);
|
|
190
222
|
program.end(chains[0], opening, 1)?;
|
|
191
223
|
}
|
|
192
224
|
let mut reply = envelope(true, 2);
|
|
193
|
-
reply.extend(tuple_lookup(TUPLE_SET, linked, true));
|
|
225
|
+
reply.extend(tuple_lookup(TUPLE_SET, linked, true)?);
|
|
194
226
|
program.end(chains[1], reply, 1)
|
|
195
227
|
}
|