@push.rocks/smartnftables 2.5.2 → 3.0.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.
Files changed (41) hide show
  1. package/changelog.md +31 -0
  2. package/dist_rust/smartnftables_linux_amd64_musl +0 -0
  3. package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +5 -5
  4. package/dist_rust/smartnftables_linux_arm64_musl +0 -0
  5. package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +5 -5
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/managed.egress.types.d.ts +69 -3
  8. package/package.json +6 -6
  9. package/readme.md +342 -61
  10. package/rust/src/egress.compile.rs +157 -76
  11. package/rust/src/egress.host.rs +104 -44
  12. package/rust/src/egress.hostgrant.rs +14 -14
  13. package/rust/src/egress.keys.rs +305 -0
  14. package/rust/src/egress.leased.rs +399 -0
  15. package/rust/src/egress.poolguard.rs +45 -0
  16. package/rust/src/egress.private.rs +197 -0
  17. package/rust/src/egress.published.rs +370 -0
  18. package/rust/src/egress.router.rs +64 -295
  19. package/rust/src/egress.rs +248 -17
  20. package/rust/src/egress.workloadgrant.rs +88 -0
  21. package/rust/src/egress_hostgrant_tests.rs +196 -47
  22. package/rust/src/egress_localplatform_tests.rs +204 -0
  23. package/rust/src/egress_localport_tests.rs +266 -0
  24. package/rust/src/egress_publishedrange_tests.rs +812 -0
  25. package/rust/src/egress_routerequivalence_tests.rs +448 -0
  26. package/rust/src/egress_tests.rs +132 -230
  27. package/rust/src/egress_workloadgrant_tests.rs +313 -0
  28. package/rust/src/owner.rs +77 -15
  29. package/rust/src/owner_host_traffic_tests.rs +4 -0
  30. package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
  31. package/rust/src/owner_identity_tests.rs +228 -80
  32. package/rust/src/owner_localplatform_tests.rs +274 -0
  33. package/rust/src/owner_localport_tests.rs +241 -0
  34. package/rust/src/owner_publishedrange_traffic_tests.rs +306 -0
  35. package/rust/src/owner_router_traffic_tests.rs +2 -0
  36. package/rust/src/owner_scale_traffic_tests.rs +383 -0
  37. package/rust/src/owner_tests.rs +15 -0
  38. package/rust/src/owner_workloadgrant_tests.rs +278 -0
  39. package/rust/src/policy.rs +29 -25
  40. package/ts/00_commitinfo_data.ts +1 -1
  41. package/ts/managed.egress.types.ts +71 -3
@@ -90,12 +90,37 @@ fn covers(parent: &str, child: &str) -> Result<bool> {
90
90
  }
91
91
 
92
92
  impl Policy {
93
- pub fn prepare(mut self) -> Result<Prepared> {
93
+ pub fn prepare(self) -> Result<Prepared> {
94
+ let policy = self.normalize(32, 128)?;
95
+ let digest = format!(
96
+ "sha256:{:x}",
97
+ Sha256::digest(serde_json::to_vec(&policy).map_err(|_| Error::Invalid)?)
98
+ );
99
+ let prepared = Prepared { policy, digest };
100
+ // Compile during preparation; an oversized native batch never reaches the kernel.
101
+ let program = prepared.program(&format!("snft_{}", "x".repeat(59)))?;
102
+ // Reserve enough of the batch for deleting a previous maximum
103
+ // graph, then installing this complete graph in the same transaction.
104
+ if program.len() > 768
105
+ || program
106
+ .iter()
107
+ .map(|(_, attrs)| wire::encode_attrs(attrs).len() + 20)
108
+ .sum::<usize>()
109
+ > 100_000
110
+ {
111
+ return Err(Error::Invalid);
112
+ }
113
+ Ok(prepared)
114
+ }
115
+ /// Validates and orders the endpoints and rules. Schema v1 compiles one rule
116
+ /// per endpoint and grant, so it admits 32 endpoints and 128 rules; the
117
+ /// set-backed router scope of schema v2 admits more.
118
+ pub(crate) fn normalize(mut self, endpoints: usize, rules: usize) -> Result<Self> {
94
119
  if self.schema_version != 1
95
120
  || self.revision == 0
96
121
  || self.revision > 9_007_199_254_740_991
97
- || self.endpoints.len() > 32
98
- || self.rules.len() > 128
122
+ || self.endpoints.len() > endpoints
123
+ || self.rules.len() > rules
99
124
  {
100
125
  return Err(Error::Invalid);
101
126
  }
@@ -185,28 +210,7 @@ impl Policy {
185
210
  }
186
211
  }
187
212
  }
188
- let digest = format!(
189
- "sha256:{:x}",
190
- Sha256::digest(serde_json::to_vec(&self).map_err(|_| Error::Invalid)?)
191
- );
192
- let prepared = Prepared {
193
- policy: self,
194
- digest,
195
- };
196
- // Compile during preparation; an oversized native batch never reaches the kernel.
197
- let program = prepared.program(&format!("snft_{}", "x".repeat(59)))?;
198
- // Reserve enough of the batch for deleting a previous maximum
199
- // graph, then installing this complete graph in the same transaction.
200
- if program.len() > 768
201
- || program
202
- .iter()
203
- .map(|(_, attrs)| wire::encode_attrs(attrs).len() + 20)
204
- .sum::<usize>()
205
- > 100_000
206
- {
207
- return Err(Error::Invalid);
208
- }
209
- Ok(prepared)
213
+ Ok(self)
210
214
  }
211
215
  }
212
216
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartnftables',
6
- version: '2.5.2',
6
+ version: '3.0.0',
7
7
  description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.'
8
8
  }
@@ -60,9 +60,14 @@ export interface IManagedNftEgressGenerationV2 extends IManagedNftHandoffAllocat
60
60
  * `transitSourceAddress:transitPort`, this translates on to the workload. */
61
61
  export interface IManagedNftRouterPublishedPortV2 {
62
62
  protocol: 'tcp' | 'udp';
63
- /** Published handoff port, 1–65535 and unique per protocol across the scope.
64
- * It is the `targetPort` of the matching host-transit publication. */
63
+ /** Published handoff port, 1–65535; published ports never overlap per protocol across the
64
+ * scope. It is the `targetPort` of the matching host-transit publication. */
65
65
  transitPort: number;
66
+ /** Optional last port of a published range `transitPort..transitPortEnd`, greater than
67
+ * `transitPort`. A range keeps every port, so `endpointPort` must equal `transitPort`; like one
68
+ * port it is one interval set element per set, and its translation keeps the port. Absent for
69
+ * one port, which is its only canonical form. */
70
+ transitPortEnd?: number;
66
71
  endpointPort: number;
67
72
  /** Exact current workload endpoint address behind one veth endpoint of this
68
73
  * scope. Never a router-local address, a gateway or a platform endpoint. */
@@ -70,6 +75,12 @@ export interface IManagedNftRouterPublishedPortV2 {
70
75
  /** Exact leased transit source address of one current generation, present on
71
76
  * `handoff`. No wildcard, secondary-address or route inference. */
72
77
  transitSourceAddress: string;
78
+ /** Optional: the workload may open flows from `endpointAddress` and its published endpoint
79
+ * port(s) toward unprotected space, source-translated to `transitSourceAddress` and the
80
+ * published transit port (a range keeps the port). The protected union stays denied, and its
81
+ * endpoint ports may not overlap another publication's on the same address. Absent and
82
+ * `false` are the same canonical policy. */
83
+ symmetric?: boolean;
73
84
  }
74
85
 
75
86
  /** One exact host-origin flow: the node's own host network namespace dials one port of one local
@@ -88,6 +99,22 @@ export interface IManagedNftHostGrant {
88
99
  destinationPort: number;
89
100
  }
90
101
 
102
+ /** One stateful one-way flow between two workloads behind the same router: the source workload opens
103
+ * connections to one exact port of the destination workload, which may only answer. Both addresses are
104
+ * exact current addresses of two different veth workload endpoints of the scope. It admits the tuple
105
+ * (a TCP opening only with SYN) and its ESTABLISHED replies in the default conntrack zone and nothing
106
+ * else: no reverse opening, no translation and no source-port selection. Grant the reverse direction as
107
+ * a second grant. */
108
+ export interface IManagedNftWorkloadGrant {
109
+ protocol: 'tcp' | 'udp';
110
+ /** Exact unicast address of the dialling workload endpoint, never a prefix. */
111
+ sourceAddress: string;
112
+ /** Exact unicast address of the dialled workload endpoint, never a prefix. */
113
+ destinationAddress: string;
114
+ /** The destination workload's exact listening port, 1–65535; never a range. */
115
+ destinationPort: number;
116
+ }
117
+
91
118
  /** One combined private/TUN/local/egress table; never compose a second ACCEPT over v1 terminal drops. */
92
119
  export interface IManagedNftRouterEgressScopeV2 {
93
120
  kind: 'routerEgress';
@@ -111,18 +138,33 @@ export interface IManagedNftRouterEgressScopeV2 {
111
138
  * workload. Absent and empty are the same canonical policy, digest and compiled graph; at most 1024,
112
139
  * held in a named set behind a constant number of rules. */
113
140
  hostGrants?: IManagedNftHostGrant[];
141
+ /** Optional stateful one-way workload-to-workload grants. Absent and empty are the same canonical
142
+ * policy, digest and compiled graph; at most 1024, held in two named sets behind four rules. They
143
+ * share the scope's set element budget with `hostGrants`. */
144
+ workloadGrants?: IManagedNftWorkloadGrant[];
114
145
  }
115
146
 
116
147
  /** One inbound uplink publication, compiled inside the same host-transit generation. */
117
148
  export interface IManagedNftPublishedPortV2 {
118
149
  protocol: 'tcp' | 'udp';
119
- /** Published uplink port, 1–65535 and unique per protocol across the scope. */
150
+ /** Published uplink port, 1–65535; published ports never overlap per protocol across the scope. */
120
151
  hostPort: number;
152
+ /** Optional last port of a published range `hostPort..hostPortEnd`, greater than `hostPort`.
153
+ * A range keeps every port, so `targetPort` must equal `hostPort`; like one port it is one
154
+ * interval set element per set, and its translation keeps the port. Absent for one port, which
155
+ * is its only canonical form. */
156
+ hostPortEnd?: number;
121
157
  targetPort: number;
122
158
  /** Exact leased transit source address of one current allocation; it selects the handoff. */
123
159
  targetAddress: string;
124
160
  /** Exact current uplink address that publishes this port. No wildcard or route inference. */
125
161
  hostIp: string;
162
+ /** Optional: flows from `targetAddress` and the published target port(s) may leave through the
163
+ * uplink toward unprotected space, source-translated to `hostIp` and the published uplink port
164
+ * (a range keeps the port). The protected union stays denied, its target ports may not fall in
165
+ * a leased range of `targetAddress` or overlap another publication's. Absent and `false` are
166
+ * the same canonical policy. */
167
+ symmetric?: boolean;
126
168
  }
127
169
 
128
170
  /** Shared-host capture barrier and explicit outer SNAT; Docker forwarding remains a separate owner. */
@@ -143,6 +185,28 @@ export interface IManagedNftHostTransitScopeV2 {
143
185
  * host-local, a platform endpoint nor a leased transit source address. Absent and empty are the
144
186
  * same canonical policy; at most 1024, held in a named set behind a constant number of rules. */
145
187
  hostGrants?: IManagedNftHostGrant[];
188
+ /** Optional ids of `protection.platformEndpoints` this host serves itself, on an exact current address
189
+ * of the uplink or of a handoff link (rtnetlink verifies it at apply, recovery and inspection). Only a
190
+ * declared endpoint may hold a host address. Leased flows reach it from their exact handoff with the
191
+ * leased source address and a source port of the allocation's range for the endpoint's protocol; its
192
+ * ESTABLISHED replies return. Absent and empty are the same canonical policy, digest and compiled graph. */
193
+ localPlatformEndpoints?: string[];
194
+ }
195
+
196
+ /** One loopback TCP service that only one local user may dial. Every packet the host sends to the
197
+ * exact address and port from a socket of any other user is rejected with a TCP reset, after ordinary
198
+ * output destination NAT; the address and port arriving on any interface but loopback are dropped.
199
+ * The service must bind exactly this address: a wildcard listener stays reachable through others. */
200
+ export interface IManagedNftLocalTcpPortOwner {
201
+ /** Exact IPv4 loopback host address in 127.0.0.0/8, e.g. `127.0.0.1`. Never a prefix or wildcard. */
202
+ address: string;
203
+ /** The service's exact listening port, 1–65535; never a range. */
204
+ port: number;
205
+ /** The only user id whose sockets may send to the port: the socket file's file-system uid as
206
+ * the user namespace owning the network namespace sees it, 0–4294967294. Packets without a socket file carry no uid and
207
+ * are not matched: kernel replies, the teardown of an orphaned socket, and in-kernel sockets such as NFS or CIFS clients,
208
+ * which only privileged mounts create. */
209
+ uid: number;
146
210
  }
147
211
 
148
212
  /** Host-wide IPv4 destination denial for caller-authenticated private allocation pools.
@@ -156,6 +220,10 @@ export interface IManagedNftAllocationPoolGuardScopeV2 {
156
220
  * listed pool, and their replies. Absent and empty are the same canonical policy; at most 1024,
157
221
  * held in a named set behind a constant number of rules. */
158
222
  hostGrants?: IManagedNftHostGrant[];
223
+ /** Optional host-wide loopback port ownership, compiled ahead of the guard in the same table.
224
+ * Absent and empty are the same canonical policy, digest and compiled graph; at most 8, one per
225
+ * exact address and port. */
226
+ localTcpPortOwners?: IManagedNftLocalTcpPortOwner[];
159
227
  }
160
228
 
161
229
  export interface IManagedNftPolicyV2 {