@push.rocks/smartnftables 2.5.2 → 2.6.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 +12 -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 +44 -0
- package/package.json +2 -2
- package/readme.md +142 -3
- package/rust/src/egress.compile.rs +2 -0
- package/rust/src/egress.host.rs +88 -1
- package/rust/src/egress.hostgrant.rs +13 -13
- package/rust/src/egress.poolguard.rs +45 -0
- package/rust/src/egress.router.rs +3 -1
- package/rust/src/egress.rs +122 -6
- package/rust/src/egress.workloadgrant.rs +88 -0
- package/rust/src/egress_hostgrant_tests.rs +47 -29
- package/rust/src/egress_localplatform_tests.rs +204 -0
- package/rust/src/egress_localport_tests.rs +264 -0
- package/rust/src/egress_workloadgrant_tests.rs +314 -0
- package/rust/src/owner.rs +8 -1
- package/rust/src/owner_hostgrant_traffic_tests.rs +9 -4
- package/rust/src/owner_identity_tests.rs +62 -0
- package/rust/src/owner_localplatform_tests.rs +274 -0
- package/rust/src/owner_localport_tests.rs +241 -0
- package/rust/src/owner_tests.rs +9 -0
- package/rust/src/owner_workloadgrant_tests.rs +278 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/managed.egress.types.ts +46 -0
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
..dial(10010, None)
|
|
122
|
+
};
|
|
123
|
+
assert_eq!(decide(&with, "input", &arriving(LO)).verdict, 1);
|
|
124
|
+
for input in [Some((2, "ens18")), Some((1, "ens18")), Some((2, "lo"))] {
|
|
125
|
+
let flow = arriving(input);
|
|
126
|
+
assert_eq!(decide(&baseline, "input", &flow).verdict, 1);
|
|
127
|
+
// Index 1 is the loopback device in every namespace; a device named "lo"
|
|
128
|
+
// with another index is not loopback.
|
|
129
|
+
let expected = if input.unwrap().0 == 1 { 1 } else { 0 };
|
|
130
|
+
assert_eq!(decide(&with, "input", &flow).verdict, expected, "{input:?}");
|
|
131
|
+
}
|
|
132
|
+
// Guarded pool traffic from any user keeps the guard's decision.
|
|
133
|
+
let pool = Flow {
|
|
134
|
+
uid: Some(1000),
|
|
135
|
+
..opening(None, Some((4, "handoff")))
|
|
136
|
+
};
|
|
137
|
+
assert_eq!(
|
|
138
|
+
decide(&with, "output", &pool),
|
|
139
|
+
decide(&baseline, "output", &pool)
|
|
140
|
+
);
|
|
141
|
+
assert_eq!(decide(&with, "output", &pool).verdict, 0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#[test]
|
|
145
|
+
fn local_tcp_port_owners_lead_the_base_chains_and_leave_the_guard_unchanged() {
|
|
146
|
+
let reference = chains(&prepared(granted(pool_guard(), many(4))));
|
|
147
|
+
let graph = chains(&prepared(owned(granted(pool_guard(), many(4)), exact())));
|
|
148
|
+
for name in ["pool_guard", "pool_reply", "forward"] {
|
|
149
|
+
assert_eq!(graph[name], reference[name], "{name}");
|
|
150
|
+
}
|
|
151
|
+
// Two owners, one OUTPUT and one INPUT rule each, ahead of everything the
|
|
152
|
+
// guard and its host grants compile, so nothing earlier accepts past them.
|
|
153
|
+
for name in ["output", "input"] {
|
|
154
|
+
assert_eq!(graph[name].len(), reference[name].len() + 2, "{name}");
|
|
155
|
+
assert_eq!(graph[name][2..], reference[name][..], "{name}");
|
|
156
|
+
}
|
|
157
|
+
let names = |expressions: &Vec<wire::Attr>| -> Vec<String> {
|
|
158
|
+
expressions
|
|
159
|
+
.iter()
|
|
160
|
+
.map(|expression| wire::text(&wire::attrs(&expression.value).unwrap(), 1).unwrap())
|
|
161
|
+
.collect()
|
|
162
|
+
};
|
|
163
|
+
for rule in &graph["output"][..2] {
|
|
164
|
+
assert_eq!(names(rule).last().unwrap(), "reject");
|
|
165
|
+
}
|
|
166
|
+
for rule in &graph["input"][..2] {
|
|
167
|
+
assert_eq!(names(rule).last().unwrap(), "immediate");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn local_tcp_port_owners_reject_non_loopback_wildcard_duplicate_and_unknown_input() {
|
|
173
|
+
for owners in [
|
|
174
|
+
json!([owner("10.0.0.1", 10010, 0)]),
|
|
175
|
+
json!([owner("0.0.0.0", 10010, 0)]),
|
|
176
|
+
json!([owner("128.0.0.1", 10010, 0)]),
|
|
177
|
+
json!([owner("127.0.0.0", 10010, 0)]),
|
|
178
|
+
json!([owner("127.255.255.255", 10010, 0)]),
|
|
179
|
+
json!([owner("127.0.0.01", 10010, 0)]),
|
|
180
|
+
json!([owner("127.0.0.1/32", 10010, 0)]),
|
|
181
|
+
json!([owner("127.0.0.0/8", 10010, 0)]),
|
|
182
|
+
json!([owner("::1", 10010, 0)]),
|
|
183
|
+
json!([owner("localhost", 10010, 0)]),
|
|
184
|
+
json!([owner("127.0.0.1", 0, 0)]),
|
|
185
|
+
json!([{"address":"127.0.0.1","port":65536,"uid":0}]),
|
|
186
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":-1}]),
|
|
187
|
+
json!([owner("127.0.0.1", 10010, u32::MAX)]),
|
|
188
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":4294967296_u64}]),
|
|
189
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":0.5}]),
|
|
190
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":"0"}]),
|
|
191
|
+
json!([{"address":"127.0.0.1","port":10010}]),
|
|
192
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":0,"uids":[0]}]),
|
|
193
|
+
json!([{"address":"127.0.0.1","port":10010,"uid":0,"protocol":"tcp"}]),
|
|
194
|
+
// One owner per exact address and port.
|
|
195
|
+
json!([owner("127.0.0.1", 10010, 0), owner("127.0.0.1", 10010, 0)]),
|
|
196
|
+
json!([
|
|
197
|
+
owner("127.0.0.1", 10010, 0),
|
|
198
|
+
owner("127.0.0.1", 10010, 1000)
|
|
199
|
+
]),
|
|
200
|
+
json!({}),
|
|
201
|
+
Value::Null,
|
|
202
|
+
] {
|
|
203
|
+
assert!(
|
|
204
|
+
normalized(owned(pool_guard(), owners.clone())).is_err(),
|
|
205
|
+
"{owners}"
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
// Other loopback host addresses and the full port and uid ranges are valid.
|
|
209
|
+
assert!(normalized(owned(
|
|
210
|
+
pool_guard(),
|
|
211
|
+
json!([
|
|
212
|
+
owner("127.0.0.1", 1, 0),
|
|
213
|
+
owner("127.0.0.1", 65535, u32::MAX - 1),
|
|
214
|
+
owner("127.0.0.2", 1, 0),
|
|
215
|
+
owner("127.255.255.254", 10010, 1000)
|
|
216
|
+
])
|
|
217
|
+
))
|
|
218
|
+
.is_ok());
|
|
219
|
+
// Only the allocation-pool guard carries loopback ownership.
|
|
220
|
+
for fixture in [super::tests::host(), super::tests::router()] {
|
|
221
|
+
assert!(normalized(owned(fixture, exact())).is_err());
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[test]
|
|
226
|
+
fn local_tcp_port_owners_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
227
|
+
let deletion = 768 * (20 + 72 + 36 + 12);
|
|
228
|
+
let maximum: Vec<_> = (0..8)
|
|
229
|
+
.map(|index| owner("127.0.0.1", 10000 + index, 0))
|
|
230
|
+
.collect();
|
|
231
|
+
// The guard's own maximum beside the owners: 64 pools and 1024 host grants.
|
|
232
|
+
let mut grants = many(1024);
|
|
233
|
+
for grant in grants.as_array_mut().unwrap() {
|
|
234
|
+
grant["destinationAddress"] = json!("10.1.0.2");
|
|
235
|
+
}
|
|
236
|
+
let mut value = owned(granted(pool_guard(), grants), json!(maximum));
|
|
237
|
+
value["scope"]["prefixes"] = json!((0..64)
|
|
238
|
+
.map(|part| format!("10.{part}.0.0/16"))
|
|
239
|
+
.collect::<Vec<_>>());
|
|
240
|
+
let prepared = prepared(value.clone());
|
|
241
|
+
prepared.validate().unwrap();
|
|
242
|
+
let program = prepared
|
|
243
|
+
.program(&format!("snft_{}", "x".repeat(59)))
|
|
244
|
+
.unwrap();
|
|
245
|
+
let size = |kinds: &[u16]| -> usize {
|
|
246
|
+
program
|
|
247
|
+
.iter()
|
|
248
|
+
.filter(|(kind, _)| kinds.contains(kind))
|
|
249
|
+
.map(|(_, attributes)| wire::encode_attrs(attributes).len() + 20)
|
|
250
|
+
.sum()
|
|
251
|
+
};
|
|
252
|
+
let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
|
|
253
|
+
assert!(program.len() < 768 && graph <= 100_000 && elements <= super::compile::ELEMENT_BYTES);
|
|
254
|
+
assert!(deletion + graph + elements + 64 <= wire::BATCH_BYTES);
|
|
255
|
+
println!(
|
|
256
|
+
"LOCAL_TCP_PORT_OWNER_MAX owners=8 prefixes=64 grants=1024 operations={} rule_bytes={graph}",
|
|
257
|
+
program.len()
|
|
258
|
+
);
|
|
259
|
+
value["scope"]["localTcpPortOwners"]
|
|
260
|
+
.as_array_mut()
|
|
261
|
+
.unwrap()
|
|
262
|
+
.push(owner("127.0.0.1", 10008, 0));
|
|
263
|
+
assert!(normalized(value).is_err());
|
|
264
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
//! Workload grants: stateful one-way flows between two workload endpoints of
|
|
2
|
+
//! one router scope, with no reverse opening.
|
|
3
|
+
use super::hostgrant_tests::{
|
|
4
|
+
bytes, chains, decide, host_grant, many, normalized, opening, prepared, Flow, ACK, ESTABLISHED,
|
|
5
|
+
};
|
|
6
|
+
use super::tests::{host, pool_guard, router};
|
|
7
|
+
use crate::wire;
|
|
8
|
+
use serde_json::{json, Value};
|
|
9
|
+
|
|
10
|
+
const SOURCE: [u8; 4] = [10, 241, 0, 2];
|
|
11
|
+
const TARGET: [u8; 4] = [10, 241, 1, 2];
|
|
12
|
+
const SOURCE_LINK: Option<(u32, &str)> = Some((2, "work"));
|
|
13
|
+
const TARGET_LINK: Option<(u32, &str)> = Some((5, "target"));
|
|
14
|
+
|
|
15
|
+
/// The router fixture with a second veth workload endpoint, `target`.
|
|
16
|
+
pub(crate) fn two_workloads() -> Value {
|
|
17
|
+
let mut value = router();
|
|
18
|
+
value["scope"]["endpoints"]
|
|
19
|
+
.as_array_mut()
|
|
20
|
+
.unwrap()
|
|
21
|
+
.push(json!(
|
|
22
|
+
{"id":"target","interfaceIndex":5,"interfaceName":"target","interfaceKind":"veth",
|
|
23
|
+
"sourcePrefixes":["10.241.1.2/32"]}));
|
|
24
|
+
value["scope"]["links"].as_array_mut().unwrap().push(json!(
|
|
25
|
+
{"interfaceIndex":5,"interfaceName":"target","interfaceKind":"veth",
|
|
26
|
+
"macAddress":"02:00:00:00:00:05","interfaceLinkIndex":105,
|
|
27
|
+
"requiredIpv4Addresses":["10.241.1.1"]}));
|
|
28
|
+
value
|
|
29
|
+
}
|
|
30
|
+
pub(crate) fn workload_grant(protocol: &str, source: &str, destination: &str, port: u16) -> Value {
|
|
31
|
+
host_grant(protocol, source, destination, port)
|
|
32
|
+
}
|
|
33
|
+
pub(crate) fn granted(mut value: Value, grants: Value) -> Value {
|
|
34
|
+
value["scope"]["workloadGrants"] = grants;
|
|
35
|
+
value
|
|
36
|
+
}
|
|
37
|
+
fn exact() -> Value {
|
|
38
|
+
json!([
|
|
39
|
+
workload_grant("tcp", "10.241.0.2", "10.241.1.2", 8080),
|
|
40
|
+
workload_grant("udp", "10.241.0.2", "10.241.1.2", 5353)
|
|
41
|
+
])
|
|
42
|
+
}
|
|
43
|
+
/// The source workload's opening toward the target, as FORWARD sees it.
|
|
44
|
+
fn dial(protocol: u8, port: u16) -> Flow {
|
|
45
|
+
Flow {
|
|
46
|
+
input: SOURCE_LINK,
|
|
47
|
+
output: TARGET_LINK,
|
|
48
|
+
protocol,
|
|
49
|
+
source: SOURCE,
|
|
50
|
+
destination: TARGET,
|
|
51
|
+
source_port: 40000,
|
|
52
|
+
destination_port: port,
|
|
53
|
+
original: (SOURCE, TARGET, 40000, port),
|
|
54
|
+
..opening(SOURCE_LINK, TARGET_LINK)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
fn answer(flow: Flow) -> Flow {
|
|
58
|
+
Flow {
|
|
59
|
+
input: flow.output,
|
|
60
|
+
output: flow.input,
|
|
61
|
+
source: flow.destination,
|
|
62
|
+
destination: flow.source,
|
|
63
|
+
source_port: flow.destination_port,
|
|
64
|
+
destination_port: flow.source_port,
|
|
65
|
+
tcp_flags: ACK,
|
|
66
|
+
state: ESTABLISHED,
|
|
67
|
+
reply: true,
|
|
68
|
+
..flow
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn workload_grants_absent_and_empty_keep_the_router_digest_and_compiled_bytes() {
|
|
74
|
+
let baseline = prepared(two_workloads());
|
|
75
|
+
let empty = prepared(granted(two_workloads(), json!([])));
|
|
76
|
+
assert_eq!(empty, baseline);
|
|
77
|
+
assert_eq!(bytes(&empty), bytes(&baseline));
|
|
78
|
+
let with = prepared(granted(two_workloads(), exact()));
|
|
79
|
+
assert_ne!(with.digest, baseline.digest);
|
|
80
|
+
let mut reversed = exact();
|
|
81
|
+
reversed.as_array_mut().unwrap().reverse();
|
|
82
|
+
assert_eq!(prepared(granted(two_workloads(), reversed)), with);
|
|
83
|
+
// Only the router scope carries workload grants.
|
|
84
|
+
for fixture in [host(), pool_guard()] {
|
|
85
|
+
assert!(normalized(granted(fixture, exact())).is_err());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn workload_grant_opens_one_way_and_admits_only_the_exact_answer() {
|
|
91
|
+
let baseline = prepared(two_workloads());
|
|
92
|
+
let with = prepared(granted(two_workloads(), exact()));
|
|
93
|
+
let decided = |flow: &Flow| {
|
|
94
|
+
(
|
|
95
|
+
decide(&baseline, "forward", flow).verdict,
|
|
96
|
+
decide(&with, "forward", flow).verdict,
|
|
97
|
+
)
|
|
98
|
+
};
|
|
99
|
+
let tcp = dial(6, 8080);
|
|
100
|
+
let udp = dial(17, 5353);
|
|
101
|
+
// The openings, the established original direction and both answers pass;
|
|
102
|
+
// the baseline denies each at the private endpoint denials.
|
|
103
|
+
for flow in [
|
|
104
|
+
tcp,
|
|
105
|
+
Flow {
|
|
106
|
+
state: ESTABLISHED,
|
|
107
|
+
tcp_flags: ACK,
|
|
108
|
+
..tcp
|
|
109
|
+
},
|
|
110
|
+
udp,
|
|
111
|
+
answer(tcp),
|
|
112
|
+
answer(udp),
|
|
113
|
+
] {
|
|
114
|
+
assert_eq!(decided(&flow), (0, 1), "{:?}", flow.original);
|
|
115
|
+
}
|
|
116
|
+
// No reverse opening: the target cannot open toward the source, not even
|
|
117
|
+
// from the granted port to the source's port, and an answer that is not
|
|
118
|
+
// ESTABLISHED is refused.
|
|
119
|
+
let reverse = |port: u16, source_port: u16| Flow {
|
|
120
|
+
reply: false,
|
|
121
|
+
state: 8,
|
|
122
|
+
tcp_flags: 0x02,
|
|
123
|
+
original: (TARGET, SOURCE, source_port, port),
|
|
124
|
+
..answer(Flow {
|
|
125
|
+
source_port: port,
|
|
126
|
+
destination_port: source_port,
|
|
127
|
+
..tcp
|
|
128
|
+
})
|
|
129
|
+
};
|
|
130
|
+
for flow in [
|
|
131
|
+
reverse(40000, 8080),
|
|
132
|
+
reverse(8080, 40000),
|
|
133
|
+
reverse(5353, 40000),
|
|
134
|
+
Flow {
|
|
135
|
+
state: 8,
|
|
136
|
+
..answer(tcp)
|
|
137
|
+
},
|
|
138
|
+
Flow {
|
|
139
|
+
state: 8,
|
|
140
|
+
..answer(udp)
|
|
141
|
+
},
|
|
142
|
+
] {
|
|
143
|
+
assert_eq!(decided(&flow), (0, 0), "{:?}", flow.original);
|
|
144
|
+
}
|
|
145
|
+
// Everything the grant does not name keeps the baseline decision: another
|
|
146
|
+
// port or protocol, a spoofed or renamed link, another address, a flow
|
|
147
|
+
// translated in transit, another zone and a TCP opening without SYN.
|
|
148
|
+
for flow in [
|
|
149
|
+
dial(6, 8081),
|
|
150
|
+
dial(17, 8080),
|
|
151
|
+
dial(6, 5353),
|
|
152
|
+
Flow {
|
|
153
|
+
input: Some((3, "vpn")),
|
|
154
|
+
..tcp
|
|
155
|
+
},
|
|
156
|
+
Flow {
|
|
157
|
+
input: Some((2, "renamed")),
|
|
158
|
+
..tcp
|
|
159
|
+
},
|
|
160
|
+
Flow {
|
|
161
|
+
input: Some((9, "work")),
|
|
162
|
+
..tcp
|
|
163
|
+
},
|
|
164
|
+
Flow {
|
|
165
|
+
output: Some((5, "renamed")),
|
|
166
|
+
..tcp
|
|
167
|
+
},
|
|
168
|
+
Flow {
|
|
169
|
+
destination: [10, 241, 1, 3],
|
|
170
|
+
original: (SOURCE, [10, 241, 1, 3], 40000, 8080),
|
|
171
|
+
..tcp
|
|
172
|
+
},
|
|
173
|
+
Flow {
|
|
174
|
+
original: (SOURCE, TARGET, 40000, 9090),
|
|
175
|
+
..tcp
|
|
176
|
+
},
|
|
177
|
+
Flow {
|
|
178
|
+
original: ([10, 241, 0, 9], TARGET, 40000, 8080),
|
|
179
|
+
..tcp
|
|
180
|
+
},
|
|
181
|
+
Flow { zone: 17, ..tcp },
|
|
182
|
+
Flow {
|
|
183
|
+
tcp_flags: ACK,
|
|
184
|
+
..tcp
|
|
185
|
+
},
|
|
186
|
+
] {
|
|
187
|
+
let (before, after) = decided(&flow);
|
|
188
|
+
assert_eq!(after, before, "{:?} {:?}", flow.input, flow.original);
|
|
189
|
+
}
|
|
190
|
+
// Everything else in the graph is unchanged.
|
|
191
|
+
let graph = chains(&with);
|
|
192
|
+
let reference = chains(&baseline);
|
|
193
|
+
for (name, rules) in &reference {
|
|
194
|
+
if name != "forward" {
|
|
195
|
+
assert_eq!(&graph[name], rules, "{name}");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
assert_eq!(graph["forward"].len(), reference["forward"].len() + 4);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[test]
|
|
202
|
+
fn workload_grants_reject_unbound_same_link_local_platform_and_tun_ends() {
|
|
203
|
+
assert!(normalized(granted(two_workloads(), exact())).is_ok());
|
|
204
|
+
let grant = |source: &str, destination: &str| {
|
|
205
|
+
granted(
|
|
206
|
+
two_workloads(),
|
|
207
|
+
json!([workload_grant("tcp", source, destination, 8080)]),
|
|
208
|
+
)
|
|
209
|
+
};
|
|
210
|
+
let mut tun = two_workloads();
|
|
211
|
+
tun["scope"]["endpoints"][1]["sourcePrefixes"] = json!(["10.242.0.0/24"]);
|
|
212
|
+
for invalid in [
|
|
213
|
+
// An end outside every workload endpoint, or the same endpoint twice.
|
|
214
|
+
grant("10.241.0.2", "10.241.9.2"),
|
|
215
|
+
grant("10.241.9.2", "10.241.1.2"),
|
|
216
|
+
grant("10.241.0.2", "10.241.0.2"),
|
|
217
|
+
// A router-local address or a TUN endpoint is not a veth workload.
|
|
218
|
+
grant("10.241.0.2", "10.241.1.1"),
|
|
219
|
+
grant("10.242.0.2", "10.241.1.2"),
|
|
220
|
+
grant("10.241.0.2", "10.242.0.2"),
|
|
221
|
+
// Prefixes, wildcards, ports and duplicates, as for host grants.
|
|
222
|
+
grant("10.241.0.2", "10.241.1.0/24"),
|
|
223
|
+
grant("0.0.0.0", "10.241.1.2"),
|
|
224
|
+
granted(
|
|
225
|
+
two_workloads(),
|
|
226
|
+
json!([workload_grant("tcp", "10.241.0.2", "10.241.1.2", 0)]),
|
|
227
|
+
),
|
|
228
|
+
granted(
|
|
229
|
+
two_workloads(),
|
|
230
|
+
json!([workload_grant("sctp", "10.241.0.2", "10.241.1.2", 80)]),
|
|
231
|
+
),
|
|
232
|
+
granted(
|
|
233
|
+
two_workloads(),
|
|
234
|
+
json!([
|
|
235
|
+
workload_grant("tcp", "10.241.0.2", "10.241.1.2", 80),
|
|
236
|
+
workload_grant("tcp", "10.241.0.2", "10.241.1.2", 80)
|
|
237
|
+
]),
|
|
238
|
+
),
|
|
239
|
+
granted(
|
|
240
|
+
two_workloads(),
|
|
241
|
+
json!([{"protocol":"tcp","sourceAddress":"10.241.0.2","destinationAddress":"10.241.1.2",
|
|
242
|
+
"destinationPort":80,"bidirectional":true}]),
|
|
243
|
+
),
|
|
244
|
+
granted(
|
|
245
|
+
two_workloads(),
|
|
246
|
+
json!((0..1025_u16)
|
|
247
|
+
.map(|port| workload_grant("tcp", "10.241.0.2", "10.241.1.2", 1 + port))
|
|
248
|
+
.collect::<Vec<_>>()),
|
|
249
|
+
),
|
|
250
|
+
] {
|
|
251
|
+
assert!(
|
|
252
|
+
normalized(invalid.clone()).is_err(),
|
|
253
|
+
"{}",
|
|
254
|
+
invalid["scope"]["workloadGrants"]
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
// Both directions between two workloads are two separate grants.
|
|
258
|
+
assert!(normalized(granted(
|
|
259
|
+
two_workloads(),
|
|
260
|
+
json!([
|
|
261
|
+
workload_grant("tcp", "10.241.0.2", "10.241.1.2", 8080),
|
|
262
|
+
workload_grant("tcp", "10.241.1.2", "10.241.0.2", 8080)
|
|
263
|
+
])
|
|
264
|
+
))
|
|
265
|
+
.is_ok());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// 1024 grants fit the atomic reserve with a constant rule graph; the element
|
|
269
|
+
/// budget is shared with the scope's host grants.
|
|
270
|
+
#[test]
|
|
271
|
+
fn workload_grants_meet_the_atomic_reserve_at_the_contract_maximum() {
|
|
272
|
+
let deletion = 768 * (20 + 72 + 36 + 12);
|
|
273
|
+
let maximum: Vec<_> = (0..1024_u16)
|
|
274
|
+
.map(|index| {
|
|
275
|
+
workload_grant(
|
|
276
|
+
if index % 2 == 0 { "tcp" } else { "udp" },
|
|
277
|
+
"10.241.0.2",
|
|
278
|
+
"10.241.1.2",
|
|
279
|
+
20000 + index / 2,
|
|
280
|
+
)
|
|
281
|
+
})
|
|
282
|
+
.collect();
|
|
283
|
+
let small = prepared(granted(two_workloads(), exact()));
|
|
284
|
+
let prepared = prepared(granted(two_workloads(), json!(maximum)));
|
|
285
|
+
assert_eq!(
|
|
286
|
+
chains(&prepared)["forward"].len(),
|
|
287
|
+
chains(&small)["forward"].len()
|
|
288
|
+
);
|
|
289
|
+
let program = prepared
|
|
290
|
+
.program(&format!("snft_{}", "x".repeat(59)))
|
|
291
|
+
.unwrap();
|
|
292
|
+
let size = |kinds: &[u16]| -> usize {
|
|
293
|
+
program
|
|
294
|
+
.iter()
|
|
295
|
+
.filter(|(kind, _)| kinds.contains(kind))
|
|
296
|
+
.map(|(_, attributes)| wire::encode_attrs(attributes).len() + 20)
|
|
297
|
+
.sum()
|
|
298
|
+
};
|
|
299
|
+
let (graph, elements) = (size(&[3, 6, 9]), size(&[12]));
|
|
300
|
+
assert!(program.len() < 768 && graph <= 100_000 && elements <= super::compile::ELEMENT_BYTES);
|
|
301
|
+
assert!(deletion + graph + elements + 64 <= wire::BATCH_BYTES);
|
|
302
|
+
println!(
|
|
303
|
+
"WORKLOAD_GRANT_MAX grants=1024 operations={} rule_bytes={graph} element_bytes={elements}",
|
|
304
|
+
program.len()
|
|
305
|
+
);
|
|
306
|
+
// Host grants and workload grants share one element budget per target:
|
|
307
|
+
// half the host grant maximum fits beside the workload maximum, both maxima
|
|
308
|
+
// together are refused before any kernel work.
|
|
309
|
+
let mut both = granted(two_workloads(), json!(maximum));
|
|
310
|
+
both["scope"]["hostGrants"] = many(512);
|
|
311
|
+
normalized(both.clone()).unwrap().prepare().unwrap();
|
|
312
|
+
both["scope"]["hostGrants"] = many(1024);
|
|
313
|
+
assert!(normalized(both).unwrap().prepare().is_err());
|
|
314
|
+
}
|
package/rust/src/owner.rs
CHANGED
|
@@ -741,7 +741,7 @@ fn rule_identity(attributes: &[Attr]) -> Result<(String, Vec<u8>)> {
|
|
|
741
741
|
"cmp" => &[3],
|
|
742
742
|
"bitwise" => &[4, 5],
|
|
743
743
|
"range" => &[3, 4],
|
|
744
|
-
"meta" | "payload" | "ct" | "nat" | "lookup" => &[],
|
|
744
|
+
"meta" | "payload" | "ct" | "nat" | "lookup" | "reject" => &[],
|
|
745
745
|
_ => return Err(Error::Conflict),
|
|
746
746
|
};
|
|
747
747
|
for attribute in &mut data {
|
|
@@ -775,6 +775,7 @@ fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
|
|
|
775
775
|
"lookup" => &[1, 2, 5],
|
|
776
776
|
"ct" | "range" => &[1, 2, 3, 4],
|
|
777
777
|
"nat" => &[1, 2, 3, 4, 5, 6, 7],
|
|
778
|
+
"reject" => &[1],
|
|
778
779
|
_ => return Ok(()),
|
|
779
780
|
};
|
|
780
781
|
for attribute in data {
|
|
@@ -825,6 +826,12 @@ fn validate_egress_expression(name: &str, data: &[Attr]) -> Result<()> {
|
|
|
825
826
|
wire::number(data, 6)?;
|
|
826
827
|
}
|
|
827
828
|
}
|
|
829
|
+
"reject" => {
|
|
830
|
+
// Only a TCP reset, which carries no ICMP code.
|
|
831
|
+
if data.len() != 1 || wire::number(data, 1)? != 1 {
|
|
832
|
+
return Err(Error::Conflict);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
828
835
|
"range" => {
|
|
829
836
|
wire::number(data, 1)?;
|
|
830
837
|
if wire::number(data, 2)? > 1 || data.len() != 4 {
|
|
@@ -79,13 +79,13 @@ fn guard_policy(revision: u64, granted: bool) -> egress::Policy {
|
|
|
79
79
|
policy
|
|
80
80
|
}
|
|
81
81
|
/// One policy owner in its own namespace; every transition runs there.
|
|
82
|
-
struct Hop {
|
|
82
|
+
pub(super) struct Hop {
|
|
83
83
|
namespace: &'static str,
|
|
84
84
|
owner: Option<Owner>,
|
|
85
85
|
applied: Option<Applied>,
|
|
86
86
|
}
|
|
87
87
|
impl Hop {
|
|
88
|
-
fn new(namespace: &'static str, table: &'static str) -> Self {
|
|
88
|
+
pub(super) fn new(namespace: &'static str, table: &'static str) -> Self {
|
|
89
89
|
let owner = in_namespace(namespace, move || Owner::new(options(table)).unwrap());
|
|
90
90
|
Self {
|
|
91
91
|
namespace,
|
|
@@ -93,7 +93,12 @@ impl Hop {
|
|
|
93
93
|
applied: None,
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
fn apply(
|
|
96
|
+
pub(super) fn apply(
|
|
97
|
+
&mut self,
|
|
98
|
+
build: fn(u64, bool) -> egress::Policy,
|
|
99
|
+
revision: u64,
|
|
100
|
+
granted: bool,
|
|
101
|
+
) {
|
|
97
102
|
let mut owner = self.owner.take().unwrap();
|
|
98
103
|
let previous = self.applied.take();
|
|
99
104
|
let (owner, applied) = in_namespace(self.namespace, move || {
|
|
@@ -108,7 +113,7 @@ impl Hop {
|
|
|
108
113
|
self.owner = Some(owner);
|
|
109
114
|
self.applied = Some(applied);
|
|
110
115
|
}
|
|
111
|
-
fn release(mut self) {
|
|
116
|
+
pub(super) fn release(mut self) {
|
|
112
117
|
let mut owner = self.owner.take().unwrap();
|
|
113
118
|
let applied = self.applied.take().unwrap();
|
|
114
119
|
in_namespace(self.namespace, move || {
|