@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.
- package/changelog.md +31 -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 +69 -3
- package/package.json +6 -6
- package/readme.md +342 -61
- package/rust/src/egress.compile.rs +157 -76
- package/rust/src/egress.host.rs +104 -44
- package/rust/src/egress.hostgrant.rs +14 -14
- package/rust/src/egress.keys.rs +305 -0
- package/rust/src/egress.leased.rs +399 -0
- package/rust/src/egress.poolguard.rs +45 -0
- package/rust/src/egress.private.rs +197 -0
- package/rust/src/egress.published.rs +370 -0
- package/rust/src/egress.router.rs +64 -295
- package/rust/src/egress.rs +248 -17
- package/rust/src/egress.workloadgrant.rs +88 -0
- package/rust/src/egress_hostgrant_tests.rs +196 -47
- package/rust/src/egress_localplatform_tests.rs +204 -0
- package/rust/src/egress_localport_tests.rs +266 -0
- package/rust/src/egress_publishedrange_tests.rs +812 -0
- package/rust/src/egress_routerequivalence_tests.rs +448 -0
- package/rust/src/egress_tests.rs +132 -230
- package/rust/src/egress_workloadgrant_tests.rs +313 -0
- package/rust/src/owner.rs +77 -15
- package/rust/src/owner_host_traffic_tests.rs +4 -0
- package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
- package/rust/src/owner_identity_tests.rs +228 -80
- package/rust/src/owner_localplatform_tests.rs +274 -0
- package/rust/src/owner_localport_tests.rs +241 -0
- package/rust/src/owner_publishedrange_traffic_tests.rs +306 -0
- package/rust/src/owner_router_traffic_tests.rs +2 -0
- package/rust/src/owner_scale_traffic_tests.rs +383 -0
- package/rust/src/owner_tests.rs +15 -0
- package/rust/src/owner_workloadgrant_tests.rs +278 -0
- package/rust/src/policy.rs +29 -25
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/managed.egress.types.ts +71 -3
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
//! Loopback TCP port ownership in the allocation-pool guard: canonical input,
|
|
2
|
+
//! strict validation, the exact compiled decision per hook and the atomic budget.
|
|
3
|
+
use super::hostgrant_tests::{
|
|
4
|
+
bytes, chains, decide, granted, many, normalized, opening, prepared, Flow, ACK, ESTABLISHED,
|
|
5
|
+
REJECT,
|
|
6
|
+
};
|
|
7
|
+
use super::tests::pool_guard;
|
|
8
|
+
use crate::wire;
|
|
9
|
+
use serde_json::{json, Value};
|
|
10
|
+
|
|
11
|
+
pub(crate) fn owner(address: &str, port: u16, uid: u32) -> Value {
|
|
12
|
+
json!({"address":address,"port":port,"uid":uid})
|
|
13
|
+
}
|
|
14
|
+
pub(crate) fn owned(mut value: Value, owners: Value) -> Value {
|
|
15
|
+
value["scope"]["localTcpPortOwners"] = owners;
|
|
16
|
+
value
|
|
17
|
+
}
|
|
18
|
+
/// The fixture: a root-only CRI stream port and a port owned by uid 1000.
|
|
19
|
+
fn exact() -> Value {
|
|
20
|
+
json!([
|
|
21
|
+
owner("127.0.0.1", 10010, 0),
|
|
22
|
+
owner("127.0.0.1", 10011, 1000)
|
|
23
|
+
])
|
|
24
|
+
}
|
|
25
|
+
const LOOPBACK: [u8; 4] = [127, 0, 0, 1];
|
|
26
|
+
const LO: Option<(u32, &str)> = Some((1, "lo"));
|
|
27
|
+
/// A local TCP opening from `uid` to the loopback `port`, as OUTPUT sees it.
|
|
28
|
+
fn dial(port: u16, uid: Option<u32>) -> Flow {
|
|
29
|
+
Flow {
|
|
30
|
+
input: None,
|
|
31
|
+
output: LO,
|
|
32
|
+
source: LOOPBACK,
|
|
33
|
+
destination: LOOPBACK,
|
|
34
|
+
source_port: 40000,
|
|
35
|
+
destination_port: port,
|
|
36
|
+
original: (LOOPBACK, LOOPBACK, 40000, port),
|
|
37
|
+
uid,
|
|
38
|
+
..opening(None, LO)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[test]
|
|
43
|
+
fn local_tcp_port_owners_absent_and_empty_keep_the_guard_digest_and_compiled_bytes() {
|
|
44
|
+
let baseline = prepared(pool_guard());
|
|
45
|
+
let empty = prepared(owned(pool_guard(), json!([])));
|
|
46
|
+
assert_eq!(empty, baseline);
|
|
47
|
+
assert_eq!(bytes(&empty), bytes(&baseline));
|
|
48
|
+
let with = prepared(owned(pool_guard(), exact()));
|
|
49
|
+
assert_ne!(with.digest, baseline.digest);
|
|
50
|
+
// Canonical order: the same set of owners is the same policy.
|
|
51
|
+
let mut reversed = exact();
|
|
52
|
+
reversed.as_array_mut().unwrap().reverse();
|
|
53
|
+
assert_eq!(prepared(owned(pool_guard(), reversed)), with);
|
|
54
|
+
// The owning uid is part of the policy identity.
|
|
55
|
+
let changed = json!([
|
|
56
|
+
owner("127.0.0.1", 10010, 1),
|
|
57
|
+
owner("127.0.0.1", 10011, 1000)
|
|
58
|
+
]);
|
|
59
|
+
assert_ne!(prepared(owned(pool_guard(), changed)).digest, with.digest);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[test]
|
|
63
|
+
fn local_tcp_port_owner_rejects_every_other_uid_at_its_exact_loopback_destination() {
|
|
64
|
+
let baseline = prepared(pool_guard());
|
|
65
|
+
let with = prepared(owned(pool_guard(), exact()));
|
|
66
|
+
// Every flow the baseline accepts; the owners change only their exact tuples.
|
|
67
|
+
for flow in [
|
|
68
|
+
dial(10010, Some(0)),
|
|
69
|
+
dial(10010, Some(1000)),
|
|
70
|
+
dial(10012, Some(1000)),
|
|
71
|
+
] {
|
|
72
|
+
assert_eq!(decide(&baseline, "output", &flow).verdict, 1);
|
|
73
|
+
}
|
|
74
|
+
// The owner dials its port; every other user's socket is reset.
|
|
75
|
+
assert_eq!(decide(&with, "output", &dial(10010, Some(0))).verdict, 1);
|
|
76
|
+
for uid in [1, 1000, 65534, u32::MAX - 1] {
|
|
77
|
+
assert_eq!(
|
|
78
|
+
decide(&with, "output", &dial(10010, Some(uid))).verdict,
|
|
79
|
+
REJECT,
|
|
80
|
+
"{uid}"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
assert_eq!(decide(&with, "output", &dial(10011, Some(1000))).verdict, 1);
|
|
84
|
+
assert_eq!(
|
|
85
|
+
decide(&with, "output", &dial(10011, Some(0))).verdict,
|
|
86
|
+
REJECT
|
|
87
|
+
);
|
|
88
|
+
// Not only openings: an established packet of another user is reset too.
|
|
89
|
+
let established = Flow {
|
|
90
|
+
state: ESTABLISHED,
|
|
91
|
+
tcp_flags: ACK,
|
|
92
|
+
..dial(10010, Some(1000))
|
|
93
|
+
};
|
|
94
|
+
assert_eq!(decide(&with, "output", &established).verdict, REJECT);
|
|
95
|
+
// Without a user socket `meta skuid` breaks the rule: a kernel reply or an
|
|
96
|
+
// orphaned socket's teardown cannot open a connection.
|
|
97
|
+
assert_eq!(decide(&with, "output", &dial(10010, None)).verdict, 1);
|
|
98
|
+
// Another port, another loopback address and UDP keep the baseline decision.
|
|
99
|
+
for flow in [
|
|
100
|
+
dial(10012, Some(1000)),
|
|
101
|
+
Flow {
|
|
102
|
+
destination: [127, 0, 0, 2],
|
|
103
|
+
..dial(10010, Some(1000))
|
|
104
|
+
},
|
|
105
|
+
Flow {
|
|
106
|
+
protocol: 17,
|
|
107
|
+
..dial(10010, Some(1000))
|
|
108
|
+
},
|
|
109
|
+
] {
|
|
110
|
+
assert_eq!(
|
|
111
|
+
decide(&with, "output", &flow),
|
|
112
|
+
decide(&baseline, "output", &flow)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
// INPUT admits the destination only from loopback; any other interface, as
|
|
116
|
+
// route_localnet or prerouting DNAT could deliver it, drops.
|
|
117
|
+
let arriving = |input| Flow {
|
|
118
|
+
input,
|
|
119
|
+
output: None,
|
|
120
|
+
uid: None,
|
|
121
|
+
label: [0; 16],
|
|
122
|
+
..dial(10010, None)
|
|
123
|
+
};
|
|
124
|
+
assert_eq!(decide(&with, "input", &arriving(LO)).verdict, 1);
|
|
125
|
+
for input in [Some((2, "ens18")), Some((1, "ens18")), Some((2, "lo"))] {
|
|
126
|
+
let flow = arriving(input);
|
|
127
|
+
assert_eq!(decide(&baseline, "input", &flow).verdict, 1);
|
|
128
|
+
// Index 1 is the loopback device in every namespace; a device named "lo"
|
|
129
|
+
// with another index is not loopback.
|
|
130
|
+
let expected = if input.unwrap().0 == 1 { 1 } else { 0 };
|
|
131
|
+
assert_eq!(decide(&with, "input", &flow).verdict, expected, "{input:?}");
|
|
132
|
+
}
|
|
133
|
+
// Guarded pool traffic from any user keeps the guard's decision.
|
|
134
|
+
let pool = Flow {
|
|
135
|
+
uid: Some(1000),
|
|
136
|
+
label: [0; 16],
|
|
137
|
+
..opening(None, Some((4, "handoff")))
|
|
138
|
+
};
|
|
139
|
+
assert_eq!(
|
|
140
|
+
decide(&with, "output", &pool),
|
|
141
|
+
decide(&baseline, "output", &pool)
|
|
142
|
+
);
|
|
143
|
+
assert_eq!(decide(&with, "output", &pool).verdict, 0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[test]
|
|
147
|
+
fn local_tcp_port_owners_lead_the_base_chains_and_leave_the_guard_unchanged() {
|
|
148
|
+
let reference = chains(&prepared(granted(pool_guard(), many(4))));
|
|
149
|
+
let graph = chains(&prepared(owned(granted(pool_guard(), many(4)), exact())));
|
|
150
|
+
for name in ["pool_guard", "pool_reply", "forward"] {
|
|
151
|
+
assert_eq!(graph[name], reference[name], "{name}");
|
|
152
|
+
}
|
|
153
|
+
// Two owners, one OUTPUT and one INPUT rule each, ahead of everything the
|
|
154
|
+
// guard and its host grants compile, so nothing earlier accepts past them.
|
|
155
|
+
for name in ["output", "input"] {
|
|
156
|
+
assert_eq!(graph[name].len(), reference[name].len() + 2, "{name}");
|
|
157
|
+
assert_eq!(graph[name][2..], reference[name][..], "{name}");
|
|
158
|
+
}
|
|
159
|
+
let names = |expressions: &Vec<wire::Attr>| -> Vec<String> {
|
|
160
|
+
expressions
|
|
161
|
+
.iter()
|
|
162
|
+
.map(|expression| wire::text(&wire::attrs(&expression.value).unwrap(), 1).unwrap())
|
|
163
|
+
.collect()
|
|
164
|
+
};
|
|
165
|
+
for rule in &graph["output"][..2] {
|
|
166
|
+
assert_eq!(names(rule).last().unwrap(), "reject");
|
|
167
|
+
}
|
|
168
|
+
for rule in &graph["input"][..2] {
|
|
169
|
+
assert_eq!(names(rule).last().unwrap(), "immediate");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn local_tcp_port_owners_reject_non_loopback_wildcard_duplicate_and_unknown_input() {
|
|
175
|
+
for owners in [
|
|
176
|
+
json!([owner("10.0.0.1", 10010, 0)]),
|
|
177
|
+
json!([owner("0.0.0.0", 10010, 0)]),
|
|
178
|
+
json!([owner("128.0.0.1", 10010, 0)]),
|
|
179
|
+
json!([owner("127.0.0.0", 10010, 0)]),
|
|
180
|
+
json!([owner("127.255.255.255", 10010, 0)]),
|
|
181
|
+
json!([owner("127.0.0.01", 10010, 0)]),
|
|
182
|
+
json!([owner("127.0.0.1/32", 10010, 0)]),
|
|
183
|
+
json!([owner("127.0.0.0/8", 10010, 0)]),
|
|
184
|
+
json!([owner("::1", 10010, 0)]),
|
|
185
|
+
json!([owner("localhost", 10010, 0)]),
|
|
186
|
+
json!([owner("127.0.0.1", 0, 0)]),
|
|
187
|
+
json!([{"address":"127.0.0.1","port":65536,"uid":0}]),
|
|
188
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":-1}]),
|
|
189
|
+
json!([owner("127.0.0.1", 10010, u32::MAX)]),
|
|
190
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":4294967296_u64}]),
|
|
191
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":0.5}]),
|
|
192
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":"0"}]),
|
|
193
|
+
json!([{"address":"127.0.0.1","port":10010}]),
|
|
194
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":0,"uids":[0]}]),
|
|
195
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":0,"protocol":"tcp"}]),
|
|
196
|
+
// One owner per exact address and port.
|
|
197
|
+
json!([owner("127.0.0.1", 10010, 0), owner("127.0.0.1", 10010, 0)]),
|
|
198
|
+
json!([
|
|
199
|
+
owner("127.0.0.1", 10010, 0),
|
|
200
|
+
owner("127.0.0.1", 10010, 1000)
|
|
201
|
+
]),
|
|
202
|
+
json!({}),
|
|
203
|
+
Value::Null,
|
|
204
|
+
] {
|
|
205
|
+
assert!(
|
|
206
|
+
normalized(owned(pool_guard(), owners.clone())).is_err(),
|
|
207
|
+
"{owners}"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
// Other loopback host addresses and the full port and uid ranges are valid.
|
|
211
|
+
assert!(normalized(owned(
|
|
212
|
+
pool_guard(),
|
|
213
|
+
json!([
|
|
214
|
+
owner("127.0.0.1", 1, 0),
|
|
215
|
+
owner("127.0.0.1", 65535, u32::MAX - 1),
|
|
216
|
+
owner("127.0.0.2", 1, 0),
|
|
217
|
+
owner("127.255.255.254", 10010, 1000)
|
|
218
|
+
])
|
|
219
|
+
))
|
|
220
|
+
.is_ok());
|
|
221
|
+
// Only the allocation-pool guard carries loopback ownership.
|
|
222
|
+
for fixture in [super::tests::host(), super::tests::router()] {
|
|
223
|
+
assert!(normalized(owned(fixture, exact())).is_err());
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn local_tcp_port_owners_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
229
|
+
let deletion = 768 * (20 + 72 + 36 + 12);
|
|
230
|
+
let maximum: Vec<_> = (0..8)
|
|
231
|
+
.map(|index| owner("127.0.0.1", 10000 + index, 0))
|
|
232
|
+
.collect();
|
|
233
|
+
// The guard's own maximum beside the owners: 64 pools and 1024 host grants.
|
|
234
|
+
let mut grants = many(1024);
|
|
235
|
+
for grant in grants.as_array_mut().unwrap() {
|
|
236
|
+
grant["destinationAddress"] = json!("10.1.0.2");
|
|
237
|
+
}
|
|
238
|
+
let mut value = owned(granted(pool_guard(), grants), json!(maximum));
|
|
239
|
+
value["scope"]["prefixes"] = json!((0..64)
|
|
240
|
+
.map(|part| format!("10.{part}.0.0/16"))
|
|
241
|
+
.collect::<Vec<_>>());
|
|
242
|
+
let prepared = prepared(value.clone());
|
|
243
|
+
prepared.validate().unwrap();
|
|
244
|
+
let program = prepared
|
|
245
|
+
.program(&format!("snft_{}", "x".repeat(59)))
|
|
246
|
+
.unwrap();
|
|
247
|
+
let size = |kinds: &[u16]| -> usize {
|
|
248
|
+
program
|
|
249
|
+
.iter()
|
|
250
|
+
.filter(|(kind, _)| kinds.contains(kind))
|
|
251
|
+
.map(|(_, attributes)| wire::encode_attrs(attributes).len() + 20)
|
|
252
|
+
.sum()
|
|
253
|
+
};
|
|
254
|
+
let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
|
|
255
|
+
assert!(program.len() < 768 && graph <= super::compile::RULE_BYTES && graph + elements <= super::compile::TARGET_BYTES);
|
|
256
|
+
assert!(deletion + graph + elements + 64 <= wire::BATCH_BYTES);
|
|
257
|
+
println!(
|
|
258
|
+
"LOCAL_TCP_PORT_OWNER_MAX owners=8 prefixes=64 grants=1024 operations={} rule_bytes={graph}",
|
|
259
|
+
program.len()
|
|
260
|
+
);
|
|
261
|
+
value["scope"]["localTcpPortOwners"]
|
|
262
|
+
.as_array_mut()
|
|
263
|
+
.unwrap()
|
|
264
|
+
.push(owner("127.0.0.1", 10008, 0));
|
|
265
|
+
assert!(normalized(value).is_err());
|
|
266
|
+
}
|