@push.rocks/smartnftables 4.3.1 → 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 CHANGED
@@ -1,5 +1,22 @@
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
+
10
+ ## 2026-10-06 - 4.4.0
11
+
12
+ ### Features
13
+
14
+ - `allocationPoolGuard.publishedPorts`: publications are exceptions of the allocation-pool guard. A `hostTransit` publication translates an inbound uplink flow to the router's transit address, which lies in a guarded pool, so the guard dropped every published flow in FORWARD by its current destination (and every reply by its current source) on any host that runs both tables: Pallet's docker-shared lab node delivered nothing on its published ports (serve.zone lab 2026-10-06, D12). Each entry `{ protocol, hostPort, hostPortEnd?, targetPort, targetAddress }` admits the flow translated from outside every pool to that exact pool address and port, and its replies, in the default conntrack zone: two or three rules in the guard chain after the original-destination denial, around the CONSTANT interval set `pool_published`. Absent and empty keep every guard's digest and compiled bytes. Readme: "Publications through the allocation-pool guard". Tests: `egress_poolguardpublished_tests` (exact admission, and the guard's own refusal for 18 foreign flows (other address, port, protocol, zone and untranslated flows), unchanged local and reply chains, canonical form, refusals, the largest guard's atomic budget).
15
+
16
+ ### Maintenance
17
+
18
+ - Release tooling: pnpm 12.8.1, `@git.zone/cli` ^8.8.2, `@git.zone/tstest` ^7.1.0 and `@git.zone/tsrust` 4.0.0. tsrust 4 always remaps local paths and fails a build whose binary still records one, so the obsolete `remapLocalPaths` setting leaves `.smartconfig.json`.
19
+
3
20
  ## 2026-10-02 - 4.3.1
4
21
 
5
22
  ### Fixes
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "cf73bfcf658ee25f8a084e50cefb3f43d2f949b0cdcc46a57b4e91a9c0497a20",
3
+ "binarySha256": "f30fb315a0059adfc4afe0728a8304fe14a5a45e2437cb4e76d5c79c7dbf8917",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "4.3.1",
7
- "gitCommit": "134a7f1fd3986cc97c9450108f7a79c7b5b395ec",
6
+ "projectVersion": "4.5.0",
7
+ "gitCommit": "f648edff675c7693b2ec8bca6138bd7d1e2da65b",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-10-02T22:57:27.524Z",
10
- "tsrustVersion": "3.0.0",
9
+ "builtAt": "2026-10-06T05:09:47.306Z",
10
+ "tsrustVersion": "4.0.0",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_amd64_musl"
13
13
  }
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "format": "tsrust.build-provenance.v2",
3
- "binarySha256": "ae5bf59b529f990c5f0a5ff8c8898cb42d3dcb5ce724c3786f64c69eb5f03bdd",
3
+ "binarySha256": "3347480d9c39a2ee4891c13f7c6a12d50d9175e15776f1c6999e1574a491dfd8",
4
4
  "buildInfo": {
5
5
  "projectName": "@push.rocks/smartnftables",
6
- "projectVersion": "4.3.1",
7
- "gitCommit": "134a7f1fd3986cc97c9450108f7a79c7b5b395ec",
6
+ "projectVersion": "4.5.0",
7
+ "gitCommit": "f648edff675c7693b2ec8bca6138bd7d1e2da65b",
8
8
  "gitDirty": false,
9
- "builtAt": "2026-10-02T22:57:37.239Z",
10
- "tsrustVersion": "3.0.0",
9
+ "builtAt": "2026-10-06T05:09:57.015Z",
10
+ "tsrustVersion": "4.0.0",
11
11
  "binary": "smartnftables",
12
12
  "target": "linux_arm64_musl"
13
13
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '4.3.1',
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=
@@ -217,17 +217,38 @@ export interface IManagedNftLocalTcpPortOwner {
217
217
  * which only privileged mounts create. */
218
218
  uid: number;
219
219
  }
220
+ /** One publication an allocation-pool guard admits into a pool: a flow whose original destination lies
221
+ * outside every listed pool, on `hostPort` (or the range `hostPort..hostPortEnd`), translated to exactly
222
+ * `targetAddress` and `targetPort`, and its replies, in the default conntrack zone. The guard checks
223
+ * no link and no uplink address: the scope that translates the flow (`hostTransit.publishedPorts`)
224
+ * binds those. Compile the same publication into the guard and the host-transit scope it crosses. */
225
+ export interface IManagedNftPoolGuardPublishedPort {
226
+ protocol: 'tcp' | 'udp';
227
+ /** Published port, 1–65535; each protocol and port is published at most once per guard. */
228
+ hostPort: number;
229
+ /** Optional last port of a published range, greater than `hostPort`; a range keeps every port,
230
+ * so `targetPort` must equal `hostPort`. Absent for one port, its only canonical form. */
231
+ hostPortEnd?: number;
232
+ targetPort: number;
233
+ /** Exact unicast address inside a listed pool that the publication is translated to. */
234
+ targetAddress: string;
235
+ }
220
236
  /** Host-wide IPv4 destination denial for caller-authenticated private allocation pools.
221
- * It has no link dependency; exact host grants are its only exceptions. It is not allocation-release or boot-order proof. */
237
+ * It has no link dependency; exact host grants and publications are its only exceptions. It is not
238
+ * allocation-release or boot-order proof. */
222
239
  export interface IManagedNftAllocationPoolGuardScopeV2 {
223
240
  kind: 'allocationPoolGuard';
224
241
  authorityDigest: string;
225
242
  /** Complete current pool list: 1–64 canonical, disjoint RFC1918 prefixes. */
226
243
  prefixes: string[];
227
- /** The only exceptions to the guard: exact host-origin flows whose `destinationAddress` lies in a
228
- * listed pool, and their replies. Absent and empty are the same canonical policy; at most 1024,
229
- * held in a named set behind a constant number of rules. */
244
+ /** Exceptions to the guard: exact host-origin flows whose `destinationAddress` lies in a listed
245
+ * pool, and their replies. Absent and empty are the same canonical policy; at most 1024, held in a
246
+ * named set behind a constant number of rules. */
230
247
  hostGrants?: IManagedNftHostGrant[];
248
+ /** Exceptions to the guard: publications translated from outside every listed pool into one, and
249
+ * their replies. Absent and empty are the same canonical policy, digest and compiled graph; at most
250
+ * 1024, held in a named set behind a constant number of rules. */
251
+ publishedPorts?: IManagedNftPoolGuardPublishedPort[];
231
252
  /** Optional host-wide loopback port ownership, compiled ahead of the guard in the same table.
232
253
  * Absent and empty are the same canonical policy, digest and compiled graph; at most 8, one per
233
254
  * exact address and port. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartnftables",
3
- "version": "4.3.1",
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",
@@ -9,11 +9,11 @@
9
9
  "author": "Task Venture Capital GmbH",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
- "@git.zone/cli": "^8.4.0",
12
+ "@git.zone/cli": "^8.8.2",
13
13
  "@git.zone/tsbuild": "^5.1.2",
14
14
  "@git.zone/tsrun": "^3.0.2",
15
- "@git.zone/tsrust": "3.0.0",
16
- "@git.zone/tstest": "^7.0.0",
15
+ "@git.zone/tsrust": "4.0.0",
16
+ "@git.zone/tstest": "^7.1.0",
17
17
  "@types/node": "^26.6.3",
18
18
  "typescript": "^7.0.2"
19
19
  },
package/readme.md CHANGED
@@ -324,7 +324,7 @@ allocation-pool guard policy kinds.
324
324
  | --- | --- |
325
325
  | `routerEgress` | Private `endpoints` and `rules`, one exact `links` binding per endpoint, a separate veth `handoff`, `protection`, and active `generations`. Private veth/TUN/local DNS and egress share one table so terminal private denial cannot override a separate egress table. Optional `publishedPorts` add the inbound second hop from the handoff to a workload endpoint, one port or a port range; both directions are classified into the default conntrack zone ahead of every leased classifier, so a published endpoint port is dedicated to its publication and never becomes leased egress. A `symmetric` publication also lets the workload open flows from its published ports. Optional `hostGrants` forward exact host-origin flows from the handoff to a workload endpoint. Optional `workloadGrants` let one workload open one exact port of another, one way. |
326
326
  | `hostTransit` | Exact handoff `link`/`allocations` pairs, complete `protection`, an explicit veth or Ethernet `uplink`, and its current `snatAddress`. It checks each handoff's leased source address and protocol/port range, default conntrack zone, direction, uplink, and protected destinations before outer SNAT. Optional `publishedPorts` add inbound uplink destination NAT inside the same generation, one port or a port range, and a `symmetric` publication also carries the workload's own flows from its published ports out through the uplink. Optional `hostGrants` let the host's own address on a handoff dial exact workload ports. Optional `localPlatformEndpoints` serve platform endpoints on the host's own addresses to leased flows. Optional `exclusiveForwarding` makes the table the host's only forwarding owner: every other forwarded packet drops. |
327
- | `allocationPoolGuard` | An authenticated `authorityDigest` and complete current allocation-pool `prefixes`. Installs host-wide IPv4 destination denial before any handoff exists, without link, uplink or SNAT dependencies. Optional `hostGrants` are its only exceptions. Optional `localTcpPortOwners` restrict loopback TCP ports to one local user each. |
327
+ | `allocationPoolGuard` | An authenticated `authorityDigest` and complete current allocation-pool `prefixes`. Installs host-wide IPv4 destination denial before any handoff exists, without link, uplink or SNAT dependencies. Optional `hostGrants` and `publishedPorts` are its only exceptions. Optional `localTcpPortOwners` restrict loopback TCP ports to one local user each. |
328
328
 
329
329
  `allocationPoolGuard` accepts 1–64 canonical, disjoint RFC1918 prefixes. Supply
330
330
  the actual allocation pools, not the broader protected union containing management
@@ -742,6 +742,42 @@ These flows need no private `rules`. A private rule is stateless: answering thro
742
742
  private rules needs a reverse rule, which would also let the destination open toward
743
743
  the source.
744
744
 
745
+ #### Publications through the allocation-pool guard
746
+
747
+ A `hostTransit` publication translates an inbound flow from the uplink into the
748
+ router's transit address, which lies in a guarded pool, so the guard denies it by
749
+ its current destination and its replies by their current source.
750
+ `allocationPoolGuard.publishedPorts` is optional and lists the publications the
751
+ guard admits: each `{ protocol, hostPort, hostPortEnd?, targetPort, targetAddress }`
752
+ names the published port or range, and the exact pool address and port it is
753
+ translated to (a range keeps every port, so `targetPort` equals `hostPort`). The
754
+ target must be a unicast address inside a listed pool; each protocol and port is
755
+ published at most once; up to 1024 fit. Compile the same publications into the
756
+ guard and into the host-transit scope that translates them, which binds the uplink
757
+ address and the handoff link the guard does not check.
758
+
759
+ ```typescript
760
+ const guard: IManagedNftPolicyV2 = { schemaVersion: 2, revision: 1, scope: {
761
+ kind: 'allocationPoolGuard', authorityDigest, prefixes: ['10.240.0.0/16', '10.241.0.0/16'],
762
+ publishedPorts: [{ protocol: 'tcp', hostPort: 443, targetPort: 443, targetAddress: '10.240.0.1' },
763
+ { protocol: 'udp', hostPort: 20000, hostPortEnd: 20200, targetPort: 20000, targetAddress: '10.240.0.1' }] } };
764
+ ```
765
+
766
+ The admissions sit in the guard chain right after its denial of every pool as an
767
+ original destination, so only a flow translated from outside every pool can match,
768
+ and ahead of every current-address denial: an ESTABLISHED packet of either
769
+ direction and a NEW opening per protocol (TCP only with SYN and FIN/RST/ACK clear),
770
+ in the default conntrack zone, each one lookup of the protocol, the reply tuple's
771
+ source address and port (where the flow was translated to, in both directions) and
772
+ the originally tracked destination port in the CONSTANT interval set
773
+ `pool_published`. Another target, target port, published port, protocol or zone, a
774
+ non-SYN opening and an original destination inside a pool meet the guard unchanged.
775
+ A publication is the exception of every hook it crosses; a forwarded publication
776
+ crosses FORWARD alone. Absent and empty are the same canonical policy, digest and
777
+ compiled bytes. The largest guard (64 pools, 1024 host grants, 8 loopback port
778
+ owners and 1024 publications) compiles to 99,688 rule bytes and 74,640 element
779
+ bytes at revision 1.
780
+
745
781
  #### Loopback TCP port owners
746
782
 
747
783
  `allocationPoolGuard.localTcpPortOwners` is optional and restricts a loopback TCP
@@ -1005,6 +1041,56 @@ and netdev hooks do not see routed packets and are not reported. Guard
1005
1041
  either Docker chain or any legacy table is present. The read is a point in time;
1006
1042
  the caller still owns exclusive authority over who may add a forward owner later.
1007
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
+
1008
1094
  ### Native qualification
1009
1095
 
1010
1096
  The separate [Docker fixture](test/native/readme.docker.md) exercises the native
@@ -1016,7 +1102,8 @@ Its checked-in input manifest and per-run receipts identify the tested artifacts
1016
1102
  snapshot and bounded-subprocess tests. The ignored native cases require the explicitly marked disposable guest
1017
1103
  from `test/native/qualify.py`; requesting them on an ordinary host fails its scope
1018
1104
  check. Qualification uses an offline Linux 6.18.35 x86_64 guest with no host disks,
1019
- mounts or external network backend. One VirtIO device connects only to a singleton
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
1020
1107
  QEMU-internal hub; packet-path tests use guest-owned namespaces, veth pairs and TUN.
1021
1108
  It covers
1022
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
- /// One named exact-key hash set, or a map to `data_length` bytes. CONSTANT
80
- /// like every set here.
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
- key_length: u32,
86
- data_length: Option<u32>,
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 data_length.is_some() { 8 } else { 0 }),
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
- if let Some(length) = data_length {
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
- /// `data_length` bytes. `fields` are byte lengths; each occupies whole
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: &[u32],
156
- data_length: Option<u32>,
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 data_length.is_some() { 8 } else { 0 }),
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
- if let Some(length) = data_length {
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(|length| Attr::nested(1, vec![Attr::u32(1, *length)]))
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.operations)
504
+ Ok(program)
438
505
  }