@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 +17 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/managed.egress.types.d.ts +25 -4
- package/package.json +4 -4
- package/readme.md +89 -2
- package/rust/src/egress.compile.rs +105 -38
- package/rust/src/egress.hostgrant.rs +74 -34
- package/rust/src/egress.keys.rs +383 -52
- package/rust/src/egress.leased.rs +71 -42
- package/rust/src/egress.poolguard.rs +72 -0
- 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 +65 -1
- package/rust/src/egress.workloadgrant.rs +30 -28
- package/rust/src/egress_hostgrant_tests.rs +12 -0
- package/rust/src/egress_keys_tests.rs +150 -0
- package/rust/src/egress_poolguardpublished_tests.rs +418 -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/ts/managed.egress.types.ts +26 -4
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
|
|
Binary file
|
|
@@ -1,13 +1,13 @@
|
|
|
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-
|
|
10
|
-
"tsrustVersion": "
|
|
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
|
}
|
|
Binary file
|
|
@@ -1,13 +1,13 @@
|
|
|
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-
|
|
10
|
-
"tsrustVersion": "
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
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
|
+
"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.
|
|
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": "
|
|
16
|
-
"@git.zone/tstest": "^7.
|
|
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
|
|
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
|
-
///
|
|
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
|
}
|