@push.rocks/smartnftables 2.4.0 → 2.5.1
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 +19 -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/classes.manageddockerforwarding.js +3 -3
- package/dist_ts/classes.managednftables.d.ts +5 -0
- package/dist_ts/classes.managednftables.js +13 -6
- package/dist_ts/managed.egress.types.d.ts +31 -1
- package/package.json +8 -8
- package/readme.md +91 -3
- package/rust/src/egress.compile.rs +49 -1
- package/rust/src/egress.host.rs +22 -0
- package/rust/src/egress.hostgrant.rs +187 -0
- package/rust/src/egress.poolguard.rs +7 -0
- package/rust/src/egress.router.rs +45 -4
- package/rust/src/egress.rs +150 -1
- package/rust/src/egress_hostgrant_tests.rs +948 -0
- package/rust/src/main.rs +6 -2
- package/rust/src/owner.rs +116 -4
- package/rust/src/owner_egress_tests.rs +1 -1
- package/rust/src/owner_hostgrant_traffic_tests.rs +280 -0
- package/rust/src/owner_identity_tests.rs +80 -1
- package/rust/src/owner_poolguard_tests.rs +69 -0
- package/rust/src/owner_router_traffic_tests.rs +1 -1
- package/rust/src/owner_tests.rs +3 -0
- package/rust/src/policy.rs +1 -1
- package/rust/src/wire.rs +15 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +2 -2
- package/ts/classes.managednftables.ts +13 -5
- package/ts/managed.egress.types.ts +32 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//! Host grants compile to a constant number of rules around exact-tuple set
|
|
2
|
+
//! lookups, so the rule graph does not grow with the number of grants. The key
|
|
3
|
+
//! carries every operand the rules would otherwise compare one by one: the
|
|
4
|
+
//! variable interface (index and name), the packet and conntrack protocols, the
|
|
5
|
+
//! live tuple and the originally tracked tuple. An element is one grant with its
|
|
6
|
+
//! live tuple equal to its original tuple, so translated flows never match.
|
|
7
|
+
use super::*;
|
|
8
|
+
|
|
9
|
+
/// Every exact host-origin flow of the scope, keyed on its full tuple.
|
|
10
|
+
pub(super) const TUPLE_SET: &str = "host_grant";
|
|
11
|
+
/// Router raw ingress runs before conntrack: exact arriving tuples only.
|
|
12
|
+
pub(super) const ARRIVAL_SET: &str = "host_grant_arrival";
|
|
13
|
+
|
|
14
|
+
/// Register numbers as Linux dumps them: 16-byte aligned registers use the
|
|
15
|
+
/// legacy NFT_REG_1..4 names, every other 4-byte register NFT_REG32_xx.
|
|
16
|
+
fn register(index: u32) -> u32 {
|
|
17
|
+
if index % 4 == 0 {
|
|
18
|
+
index / 4
|
|
19
|
+
} else {
|
|
20
|
+
index - 4 + 8
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
fn store(name: &str, mut data: Vec<Attr>, index: u32) -> Attr {
|
|
24
|
+
data.insert(0, Attr::u32(1, register(index)));
|
|
25
|
+
expr(name, data)
|
|
26
|
+
}
|
|
27
|
+
fn meta_to(key: u32, index: u32) -> Attr {
|
|
28
|
+
store("meta", vec![Attr::u32(2, key)], index)
|
|
29
|
+
}
|
|
30
|
+
fn payload_to(base: u32, offset: u32, length: u32, index: u32) -> Attr {
|
|
31
|
+
store(
|
|
32
|
+
"payload",
|
|
33
|
+
vec![
|
|
34
|
+
Attr::u32(2, base),
|
|
35
|
+
Attr::u32(3, offset),
|
|
36
|
+
Attr::u32(4, length),
|
|
37
|
+
],
|
|
38
|
+
index,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
fn ct_to(key: u32, original: bool, index: u32) -> Attr {
|
|
42
|
+
let mut data = vec![Attr::u32(2, key)];
|
|
43
|
+
if original {
|
|
44
|
+
data.push(Attr::bytes(3, vec![0]));
|
|
45
|
+
}
|
|
46
|
+
store("ct", data, index)
|
|
47
|
+
}
|
|
48
|
+
fn lookup(set: &str) -> Attr {
|
|
49
|
+
expr(
|
|
50
|
+
"lookup",
|
|
51
|
+
vec![
|
|
52
|
+
Attr::string(1, set),
|
|
53
|
+
Attr::u32(2, register(4)),
|
|
54
|
+
Attr::u32(5, 0),
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
fn word(bytes: &[u8]) -> Vec<u8> {
|
|
59
|
+
let mut result = bytes.to_vec();
|
|
60
|
+
result.resize(4, 0);
|
|
61
|
+
result
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Key loads for one direction, then the lookup. `linked` adds the variable
|
|
65
|
+
/// interface: the outgoing interface of the original direction, the incoming
|
|
66
|
+
/// interface of the reply.
|
|
67
|
+
fn tuple_lookup(linked: bool, reply: bool) -> Vec<Attr> {
|
|
68
|
+
let mut result = Vec::new();
|
|
69
|
+
let mut index = 4;
|
|
70
|
+
if linked {
|
|
71
|
+
result.push(meta_to(if reply { 4 } else { 5 }, index));
|
|
72
|
+
result.push(meta_to(if reply { 6 } else { 7 }, index + 1));
|
|
73
|
+
index += 5;
|
|
74
|
+
}
|
|
75
|
+
result.push(meta_to(16, index));
|
|
76
|
+
result.push(ct_to(10, false, index + 1));
|
|
77
|
+
// The grant's source and destination are the packet's own addresses in the
|
|
78
|
+
// original direction and swapped on the reply; the port is the workload's.
|
|
79
|
+
result.push(payload_to(1, if reply { 16 } else { 12 }, 4, index + 2));
|
|
80
|
+
result.push(payload_to(1, if reply { 12 } else { 16 }, 4, index + 3));
|
|
81
|
+
result.push(payload_to(2, if reply { 0 } else { 2 }, 2, index + 4));
|
|
82
|
+
result.push(ct_to(19, true, index + 5));
|
|
83
|
+
result.push(ct_to(20, true, index + 6));
|
|
84
|
+
result.push(ct_to(12, true, index + 7));
|
|
85
|
+
result.push(lookup(TUPLE_SET));
|
|
86
|
+
result
|
|
87
|
+
}
|
|
88
|
+
fn tuple_key(grant: &HostGrant, link: Option<&LocalLink>) -> Result<Vec<u8>> {
|
|
89
|
+
let protocol = word(&[protocol(&grant.protocol)]);
|
|
90
|
+
let source = address(&grant.source_address)?.to_be_bytes();
|
|
91
|
+
let destination = address(&grant.destination_address)?.to_be_bytes();
|
|
92
|
+
let port = word(&grant.destination_port.to_be_bytes());
|
|
93
|
+
let mut key = Vec::new();
|
|
94
|
+
if let Some(link) = link {
|
|
95
|
+
key.extend(link.interface_index.to_ne_bytes());
|
|
96
|
+
let mut name = link.interface_name.as_bytes().to_vec();
|
|
97
|
+
name.resize(16, 0);
|
|
98
|
+
key.extend(name);
|
|
99
|
+
}
|
|
100
|
+
for field in [
|
|
101
|
+
&protocol[..],
|
|
102
|
+
&protocol,
|
|
103
|
+
&source,
|
|
104
|
+
&destination,
|
|
105
|
+
&port,
|
|
106
|
+
&source,
|
|
107
|
+
&destination,
|
|
108
|
+
&port,
|
|
109
|
+
] {
|
|
110
|
+
key.extend(field);
|
|
111
|
+
}
|
|
112
|
+
Ok(key)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// The scope's exact tuple set. `link` returns the variable interface of a
|
|
116
|
+
/// grant, or None for a link-independent scope.
|
|
117
|
+
pub(super) fn tuple_set<'s>(
|
|
118
|
+
program: &mut Program<'_>,
|
|
119
|
+
grants: &'s [HostGrant],
|
|
120
|
+
link: impl Fn(&HostGrant) -> Result<Option<&'s LocalLink>>,
|
|
121
|
+
) -> Result<()> {
|
|
122
|
+
let mut keys = Vec::new();
|
|
123
|
+
for grant in grants {
|
|
124
|
+
keys.push(tuple_key(grant, link(grant)?)?);
|
|
125
|
+
}
|
|
126
|
+
let length = keys.first().map(Vec::len).ok_or(crate::Error::Invalid)?;
|
|
127
|
+
program.set(TUPLE_SET, 1, length)?;
|
|
128
|
+
program.elements(TUPLE_SET, keys)
|
|
129
|
+
}
|
|
130
|
+
pub(super) fn arrival_set(program: &mut Program<'_>, grants: &[HostGrant]) -> Result<()> {
|
|
131
|
+
let mut keys = Vec::new();
|
|
132
|
+
for grant in grants {
|
|
133
|
+
let mut key = word(&[protocol(&grant.protocol)]);
|
|
134
|
+
key.extend(address(&grant.source_address)?.to_be_bytes());
|
|
135
|
+
key.extend(address(&grant.destination_address)?.to_be_bytes());
|
|
136
|
+
key.extend(word(&grant.destination_port.to_be_bytes()));
|
|
137
|
+
keys.push(key);
|
|
138
|
+
}
|
|
139
|
+
program.set(ARRIVAL_SET, 2, 16)?;
|
|
140
|
+
program.elements(ARRIVAL_SET, keys)
|
|
141
|
+
}
|
|
142
|
+
/// Exact arriving tuple, before conntrack: protocol, addresses and port.
|
|
143
|
+
pub(super) fn arrival_lookup() -> Vec<Attr> {
|
|
144
|
+
vec![
|
|
145
|
+
meta_to(16, 4),
|
|
146
|
+
payload_to(1, 12, 4, 5),
|
|
147
|
+
payload_to(1, 16, 4, 6),
|
|
148
|
+
payload_to(2, 2, 2, 7),
|
|
149
|
+
lookup(ARRIVAL_SET),
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Four admissions whatever the number of grants: the ESTABLISHED original
|
|
154
|
+
/// direction, a NEW UDP opening, a NEW TCP opening restricted to SYN with
|
|
155
|
+
/// FIN/RST/ACK clear, and the ESTABLISHED reply. Each checks the default
|
|
156
|
+
/// conntrack zone and the direction before one exact-tuple lookup. `fixed`
|
|
157
|
+
/// adds the scope's fixed interface for each direction.
|
|
158
|
+
pub(super) fn admissions(
|
|
159
|
+
program: &mut Program<'_>,
|
|
160
|
+
chains: [&str; 2],
|
|
161
|
+
linked: bool,
|
|
162
|
+
fixed: impl Fn(bool) -> Vec<Attr>,
|
|
163
|
+
) -> Result<()> {
|
|
164
|
+
let envelope = |reply: bool, connection_state: u32| {
|
|
165
|
+
let mut result = ipv4();
|
|
166
|
+
result.extend(fixed(reply));
|
|
167
|
+
result.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
|
|
168
|
+
result.extend(ct(1, None, vec![u8::from(reply)]));
|
|
169
|
+
result.extend(state(connection_state));
|
|
170
|
+
result
|
|
171
|
+
};
|
|
172
|
+
let mut established = envelope(false, 2);
|
|
173
|
+
established.extend(tuple_lookup(linked, false));
|
|
174
|
+
program.end(chains[0], established, 1)?;
|
|
175
|
+
for transport in ["udp", "tcp"] {
|
|
176
|
+
let mut opening = envelope(false, 8);
|
|
177
|
+
opening.extend(meta(16, vec![protocol(transport)]));
|
|
178
|
+
if transport == "tcp" {
|
|
179
|
+
opening.extend(opening_tcp());
|
|
180
|
+
}
|
|
181
|
+
opening.extend(tuple_lookup(linked, false));
|
|
182
|
+
program.end(chains[0], opening, 1)?;
|
|
183
|
+
}
|
|
184
|
+
let mut reply = envelope(true, 2);
|
|
185
|
+
reply.extend(tuple_lookup(linked, true));
|
|
186
|
+
program.end(chains[1], reply, 1)
|
|
187
|
+
}
|
|
@@ -7,6 +7,13 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &AllocationPoolGuardScop
|
|
|
7
7
|
for name in ["pool_guard", "pool_reply"] {
|
|
8
8
|
program.chain(name, None)?;
|
|
9
9
|
}
|
|
10
|
+
// The only exceptions: an exact host-origin flow into a pool and its reply,
|
|
11
|
+
// ahead of the guard. Everything else, including a translated flow that merely
|
|
12
|
+
// arrives at a granted tuple, still meets the unconditional guard below.
|
|
13
|
+
if !scope.host_grants.is_empty() {
|
|
14
|
+
hostgrant::tuple_set(program, &scope.host_grants, |_| Ok(None))?;
|
|
15
|
+
hostgrant::admissions(program, ["output", "input"], false, |_| vec![])?;
|
|
16
|
+
}
|
|
10
17
|
for name in ["input", "forward", "output"] {
|
|
11
18
|
program.jump(name, ipv4(), "pool_guard")?;
|
|
12
19
|
}
|
|
@@ -47,11 +47,11 @@ fn raw_return(scope: &RouterScope, grant: &Grant, generation: &Generation) -> Re
|
|
|
47
47
|
));
|
|
48
48
|
Ok(result)
|
|
49
49
|
}
|
|
50
|
-
/// The exact veth link of the workload endpoint that holds
|
|
50
|
+
/// The exact veth link of the workload endpoint that holds an endpoint address.
|
|
51
51
|
/// Validation admits only a current endpoint address, and endpoint prefixes are
|
|
52
|
-
/// pairwise disjoint, so one publication never spans two
|
|
53
|
-
fn
|
|
54
|
-
let target = format!("{}/32"
|
|
52
|
+
/// pairwise disjoint, so one publication or host grant never spans two links.
|
|
53
|
+
fn endpoint_link<'a>(scope: &'a RouterScope, address: &str) -> Result<&'a LocalLink> {
|
|
54
|
+
let target = format!("{address}/32");
|
|
55
55
|
let endpoint = scope
|
|
56
56
|
.endpoints
|
|
57
57
|
.iter()
|
|
@@ -68,6 +68,9 @@ fn published_link<'a>(scope: &'a RouterScope, port: &RouterPublishedPort) -> Res
|
|
|
68
68
|
.find(|link| link.interface_index == endpoint.interface_index)
|
|
69
69
|
.ok_or(crate::Error::Invalid)
|
|
70
70
|
}
|
|
71
|
+
fn published_link<'a>(scope: &'a RouterScope, port: &RouterPublishedPort) -> Result<&'a LocalLink> {
|
|
72
|
+
endpoint_link(scope, &port.endpoint_address)
|
|
73
|
+
}
|
|
71
74
|
/// Inbound publication envelope. Both directions check the exact links, the
|
|
72
75
|
/// translated current tuple and the original conntrack destination, so foreign
|
|
73
76
|
/// NAT cannot borrow this admission for another flow.
|
|
@@ -130,6 +133,39 @@ fn published_endpoint(program: &mut Program<'_>, scope: &RouterScope) -> Result<
|
|
|
130
133
|
}
|
|
131
134
|
Ok(())
|
|
132
135
|
}
|
|
136
|
+
/// Handoff ingress classification for an exact host grant. Like a publication it
|
|
137
|
+
/// is the explicit authority for its tuple, so it leads the protected-source
|
|
138
|
+
/// denial that otherwise drops every packet from the host's transit address, and
|
|
139
|
+
/// it never sets a generation zone: a host grant is an ordinary default-zone flow.
|
|
140
|
+
/// The workload's replies need no classifier of their own: their destination is
|
|
141
|
+
/// protected, so they return to the default zone ahead of every leased classifier.
|
|
142
|
+
fn host_grant_return(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
143
|
+
if !scope.host_grants.is_empty() {
|
|
144
|
+
let mut expressions = ipv4();
|
|
145
|
+
expressions.extend(link(&scope.handoff, true));
|
|
146
|
+
expressions.extend(hostgrant::arrival_lookup());
|
|
147
|
+
program.end("raw_return", expressions, 1)?;
|
|
148
|
+
}
|
|
149
|
+
Ok(())
|
|
150
|
+
}
|
|
151
|
+
/// The workload link of each grant is part of its key; the handoff is fixed.
|
|
152
|
+
fn host_grant_admissions(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
153
|
+
if !scope.host_grants.is_empty() {
|
|
154
|
+
hostgrant::admissions(program, ["forward", "forward"], true, |reply| {
|
|
155
|
+
link(&scope.handoff, !reply)
|
|
156
|
+
})?;
|
|
157
|
+
}
|
|
158
|
+
Ok(())
|
|
159
|
+
}
|
|
160
|
+
fn host_grant_sets(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
161
|
+
if !scope.host_grants.is_empty() {
|
|
162
|
+
hostgrant::tuple_set(program, &scope.host_grants, |grant| {
|
|
163
|
+
endpoint_link(scope, &grant.destination_address).map(Some)
|
|
164
|
+
})?;
|
|
165
|
+
hostgrant::arrival_set(program, &scope.host_grants)?;
|
|
166
|
+
}
|
|
167
|
+
Ok(())
|
|
168
|
+
}
|
|
133
169
|
fn published_translation(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
134
170
|
for port in &scope.published_ports {
|
|
135
171
|
let mut expressions = ipv4();
|
|
@@ -196,6 +232,7 @@ fn raw(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
|
196
232
|
// platform return envelope is. The host publishes into this address, so its
|
|
197
233
|
// clients are not confined to unprotected space.
|
|
198
234
|
published_return(program, scope)?;
|
|
235
|
+
host_grant_return(program, scope)?;
|
|
199
236
|
// Both sides of a publication lead the leased classifiers of every generation,
|
|
200
237
|
// so neither direction depends on the client's choice of source port.
|
|
201
238
|
published_endpoint(program, scope)?;
|
|
@@ -327,6 +364,7 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope, revision:
|
|
|
327
364
|
for name in ["raw_return", "protected_out", "protected_back"] {
|
|
328
365
|
program.chain(name, None)?;
|
|
329
366
|
}
|
|
367
|
+
host_grant_sets(program, scope)?;
|
|
330
368
|
let private = policy::Policy {
|
|
331
369
|
schema_version: 1,
|
|
332
370
|
revision,
|
|
@@ -354,8 +392,11 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope, revision:
|
|
|
354
392
|
// precedes the capture barrier: the barrier denies every protected original
|
|
355
393
|
// destination, which includes this transit address, and an exact inbound
|
|
356
394
|
// publication is the explicit authority for that exception.
|
|
395
|
+
// Host grants take the same position for the same reason: the barrier
|
|
396
|
+
// denies the host's protected transit address in both directions.
|
|
357
397
|
if chain == "forward" {
|
|
358
398
|
published_admissions(program, scope)?;
|
|
399
|
+
host_grant_admissions(program, scope)?;
|
|
359
400
|
}
|
|
360
401
|
for expressions in parts.grants {
|
|
361
402
|
program.rule(chain, expressions)?;
|
package/rust/src/egress.rs
CHANGED
|
@@ -103,6 +103,18 @@ pub struct RouterPublishedPort {
|
|
|
103
103
|
pub endpoint_address: String,
|
|
104
104
|
pub transit_source_address: String,
|
|
105
105
|
}
|
|
106
|
+
/// One exact host-origin flow: the node's own host network namespace dials one
|
|
107
|
+
/// port of one local workload at its lease address, from the transit host
|
|
108
|
+
/// address of the current handoff. The same grant is compiled in every scope the
|
|
109
|
+
/// flow crosses; each scope binds it to the authority that scope proves.
|
|
110
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
111
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
112
|
+
pub struct HostGrant {
|
|
113
|
+
pub protocol: String,
|
|
114
|
+
pub source_address: String,
|
|
115
|
+
pub destination_address: String,
|
|
116
|
+
pub destination_port: u16,
|
|
117
|
+
}
|
|
106
118
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
107
119
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
108
120
|
pub struct RouterScope {
|
|
@@ -116,6 +128,9 @@ pub struct RouterScope {
|
|
|
116
128
|
/// keeps its exact previous digest and compiled graph.
|
|
117
129
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
118
130
|
pub published_ports: Vec<RouterPublishedPort>,
|
|
131
|
+
/// Absent and empty are the same canonical policy, like publications.
|
|
132
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
133
|
+
pub host_grants: Vec<HostGrant>,
|
|
119
134
|
}
|
|
120
135
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
121
136
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -143,12 +158,19 @@ pub struct HostScope {
|
|
|
143
158
|
/// keeps its exact previous digest and compiled graph.
|
|
144
159
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
145
160
|
pub published_ports: Vec<PublishedPort>,
|
|
161
|
+
/// Absent and empty are the same canonical policy, like publications.
|
|
162
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
163
|
+
pub host_grants: Vec<HostGrant>,
|
|
146
164
|
}
|
|
147
165
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
148
166
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
149
167
|
pub struct AllocationPoolGuardScope {
|
|
150
168
|
pub authority_digest: String,
|
|
151
169
|
pub prefixes: Vec<String>,
|
|
170
|
+
/// The only exceptions to the guard: exact host-origin flows into a pool.
|
|
171
|
+
/// Absent and empty are the same canonical policy.
|
|
172
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
173
|
+
pub host_grants: Vec<HostGrant>,
|
|
152
174
|
}
|
|
153
175
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
154
176
|
#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -473,6 +495,76 @@ fn normalize_router_published(value: &mut RouterScope) -> Result<()> {
|
|
|
473
495
|
value.published_ports = ports;
|
|
474
496
|
Ok(())
|
|
475
497
|
}
|
|
498
|
+
/// Exact tuples only: one protocol, one unicast source and destination address
|
|
499
|
+
/// and one destination port. Addresses never carry a prefix and ports never a
|
|
500
|
+
/// range, so a grant cannot widen past its flow. `bound` checks the authority of
|
|
501
|
+
/// the enclosing scope.
|
|
502
|
+
fn normalize_host_grants(
|
|
503
|
+
grants: &mut [HostGrant],
|
|
504
|
+
bound: impl Fn(&HostGrant) -> Result<()>,
|
|
505
|
+
) -> Result<()> {
|
|
506
|
+
// The contract maximum per projection; set lookups keep the rule graph
|
|
507
|
+
// constant, and the element budget holds this many in every scope.
|
|
508
|
+
require(grants.len() <= 1024)?;
|
|
509
|
+
grants.sort();
|
|
510
|
+
for (index, grant) in grants.iter().enumerate() {
|
|
511
|
+
require(
|
|
512
|
+
protocol(&grant.protocol)
|
|
513
|
+
&& grant.destination_port > 0
|
|
514
|
+
&& (index == 0 || grants[index - 1] != *grant),
|
|
515
|
+
)?;
|
|
516
|
+
unicast(&grant.source_address)?;
|
|
517
|
+
unicast(&grant.destination_address)?;
|
|
518
|
+
require(grant.source_address != grant.destination_address)?;
|
|
519
|
+
bound(grant)?;
|
|
520
|
+
}
|
|
521
|
+
Ok(())
|
|
522
|
+
}
|
|
523
|
+
/// The router forwards a host grant from its handoff to one workload: the
|
|
524
|
+
/// destination is an exact current veth endpoint address of this scope, and the
|
|
525
|
+
/// source is protected handoff-side space that is neither router-local, a
|
|
526
|
+
/// platform endpoint nor any workload's address.
|
|
527
|
+
fn normalize_router_host_grants(value: &mut RouterScope) -> Result<()> {
|
|
528
|
+
let mut grants = std::mem::take(&mut value.host_grants);
|
|
529
|
+
let scope = &*value;
|
|
530
|
+
let local = |ip: &str| {
|
|
531
|
+
scope
|
|
532
|
+
.links
|
|
533
|
+
.iter()
|
|
534
|
+
.chain(std::iter::once(&scope.handoff))
|
|
535
|
+
.any(|link| link.required_ipv4_addresses.iter().any(|item| item == ip))
|
|
536
|
+
};
|
|
537
|
+
let platform = |ip: &str| {
|
|
538
|
+
scope
|
|
539
|
+
.protection
|
|
540
|
+
.platform_endpoints
|
|
541
|
+
.iter()
|
|
542
|
+
.any(|endpoint| endpoint.address == ip)
|
|
543
|
+
};
|
|
544
|
+
let workload = |ip: &str| {
|
|
545
|
+
let target = format!("{ip}/32");
|
|
546
|
+
scope.endpoints.iter().find(|endpoint| {
|
|
547
|
+
endpoint
|
|
548
|
+
.source_prefixes
|
|
549
|
+
.iter()
|
|
550
|
+
.any(|source| covers(source, &target) == Ok(true))
|
|
551
|
+
})
|
|
552
|
+
};
|
|
553
|
+
normalize_host_grants(&mut grants, |grant| {
|
|
554
|
+
let endpoint = workload(&grant.destination_address).ok_or(Error::Invalid)?;
|
|
555
|
+
require(
|
|
556
|
+
endpoint.interface_kind == "veth"
|
|
557
|
+
&& !local(&grant.destination_address)
|
|
558
|
+
&& !platform(&grant.destination_address)
|
|
559
|
+
&& covers_address(&scope.protection.prefixes, &grant.source_address)?
|
|
560
|
+
&& !local(&grant.source_address)
|
|
561
|
+
&& !platform(&grant.source_address)
|
|
562
|
+
&& workload(&grant.source_address).is_none(),
|
|
563
|
+
)
|
|
564
|
+
})?;
|
|
565
|
+
value.host_grants = grants;
|
|
566
|
+
Ok(())
|
|
567
|
+
}
|
|
476
568
|
fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
477
569
|
require(value.links.len() <= 32 && value.generations.len() <= 32)?;
|
|
478
570
|
let private = policy::Policy {
|
|
@@ -644,7 +736,8 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
|
644
736
|
item.source_port_ranges.as_slice(),
|
|
645
737
|
)
|
|
646
738
|
}))?;
|
|
647
|
-
normalize_router_published(value)
|
|
739
|
+
normalize_router_published(value)?;
|
|
740
|
+
normalize_router_host_grants(value)
|
|
648
741
|
}
|
|
649
742
|
fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
650
743
|
require(
|
|
@@ -661,6 +754,58 @@ fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
|
661
754
|
require(!overlaps(item, previous)?)?;
|
|
662
755
|
}
|
|
663
756
|
}
|
|
757
|
+
// An exception is only ever an exact flow into a guarded pool.
|
|
758
|
+
let mut grants = std::mem::take(&mut value.host_grants);
|
|
759
|
+
normalize_host_grants(&mut grants, |grant| {
|
|
760
|
+
require(covers_address(&value.prefixes, &grant.destination_address)?)
|
|
761
|
+
})?;
|
|
762
|
+
value.host_grants = grants;
|
|
763
|
+
Ok(())
|
|
764
|
+
}
|
|
765
|
+
/// The host dials from an exact current address of exactly one handoff link, into
|
|
766
|
+
/// protected space that is neither host-local, a platform endpoint nor a leased
|
|
767
|
+
/// router transit address: only a workload behind that handoff remains.
|
|
768
|
+
fn normalize_host_host_grants(value: &mut HostScope) -> Result<()> {
|
|
769
|
+
let mut grants = std::mem::take(&mut value.host_grants);
|
|
770
|
+
let scope = &*value;
|
|
771
|
+
normalize_host_grants(&mut grants, |grant| {
|
|
772
|
+
let destination = &grant.destination_address;
|
|
773
|
+
require(
|
|
774
|
+
scope
|
|
775
|
+
.handoffs
|
|
776
|
+
.iter()
|
|
777
|
+
.filter(|handoff| {
|
|
778
|
+
handoff
|
|
779
|
+
.link
|
|
780
|
+
.required_ipv4_addresses
|
|
781
|
+
.contains(&grant.source_address)
|
|
782
|
+
})
|
|
783
|
+
.count()
|
|
784
|
+
== 1
|
|
785
|
+
&& !scope
|
|
786
|
+
.uplink
|
|
787
|
+
.required_ipv4_addresses
|
|
788
|
+
.contains(&grant.source_address)
|
|
789
|
+
&& covers_address(&scope.protection.prefixes, destination)?
|
|
790
|
+
&& scope
|
|
791
|
+
.handoffs
|
|
792
|
+
.iter()
|
|
793
|
+
.map(|handoff| &handoff.link)
|
|
794
|
+
.chain(std::iter::once(&scope.uplink))
|
|
795
|
+
.all(|link| !link.required_ipv4_addresses.contains(destination))
|
|
796
|
+
&& scope
|
|
797
|
+
.protection
|
|
798
|
+
.platform_endpoints
|
|
799
|
+
.iter()
|
|
800
|
+
.all(|endpoint| endpoint.address != *destination)
|
|
801
|
+
&& scope
|
|
802
|
+
.handoffs
|
|
803
|
+
.iter()
|
|
804
|
+
.flat_map(|handoff| handoff.allocations.iter())
|
|
805
|
+
.all(|allocation| allocation.transit_source_address != *destination),
|
|
806
|
+
)
|
|
807
|
+
})?;
|
|
808
|
+
value.host_grants = grants;
|
|
664
809
|
Ok(())
|
|
665
810
|
}
|
|
666
811
|
/// Inbound publication is bounded by the same authority the scope already proves:
|
|
@@ -777,6 +922,7 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
|
|
|
777
922
|
}
|
|
778
923
|
}
|
|
779
924
|
normalize_published(value)?;
|
|
925
|
+
normalize_host_host_grants(value)?;
|
|
780
926
|
unique_links(
|
|
781
927
|
value
|
|
782
928
|
.handoffs
|
|
@@ -799,6 +945,9 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
|
|
|
799
945
|
)
|
|
800
946
|
}
|
|
801
947
|
|
|
948
|
+
#[cfg(test)]
|
|
949
|
+
#[path = "egress_hostgrant_tests.rs"]
|
|
950
|
+
pub(crate) mod hostgrant_tests;
|
|
802
951
|
#[cfg(test)]
|
|
803
952
|
#[path = "egress_tests.rs"]
|
|
804
953
|
pub(crate) mod tests;
|