@push.rocks/smartnftables 2.3.0 → 2.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 +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 +53 -1
- package/package.json +7 -7
- package/readme.md +153 -4
- 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 +189 -3
- package/rust/src/egress.rs +244 -1
- package/rust/src/egress_hostgrant_tests.rs +948 -0
- package/rust/src/egress_tests.rs +414 -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_egress_traffic_tests.rs +3 -3
- package/rust/src/owner_host_traffic_tests.rs +4 -4
- package/rust/src/owner_hostgrant_traffic_tests.rs +280 -0
- package/rust/src/owner_identity_tests.rs +125 -1
- package/rust/src/owner_poolguard_tests.rs +69 -0
- package/rust/src/owner_router_traffic_tests.rs +393 -0
- package/rust/src/owner_tests.rs +6 -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 +55 -1
|
@@ -4,27 +4,74 @@ use crate::wire::{self, Attr};
|
|
|
4
4
|
|
|
5
5
|
#[path = "egress.host.rs"]
|
|
6
6
|
mod host;
|
|
7
|
+
#[path = "egress.hostgrant.rs"]
|
|
8
|
+
mod hostgrant;
|
|
7
9
|
#[path = "egress.poolguard.rs"]
|
|
8
10
|
mod poolguard;
|
|
9
11
|
#[path = "egress.router.rs"]
|
|
10
12
|
mod router;
|
|
11
13
|
|
|
14
|
+
/// Set elements of one target. Together with the 100,000-byte rule graph and the
|
|
15
|
+
/// handle-only deletion of a previous maximum graph this bounds one replacement
|
|
16
|
+
/// batch below `wire::BATCH_BYTES`.
|
|
17
|
+
pub(super) const ELEMENT_BYTES: usize = 100_000;
|
|
18
|
+
|
|
12
19
|
pub(super) struct Program<'a> {
|
|
13
20
|
table: &'a str,
|
|
14
21
|
stamp: String,
|
|
15
22
|
ordinal: usize,
|
|
16
23
|
bytes: usize,
|
|
24
|
+
element_bytes: usize,
|
|
17
25
|
operations: Vec<(u16, Vec<Attr>)>,
|
|
18
26
|
}
|
|
19
27
|
impl<'a> Program<'a> {
|
|
20
28
|
fn push(&mut self, operation: u16, attributes: Vec<Attr>) -> Result<()> {
|
|
21
29
|
self.bytes += wire::encode_attrs(&attributes).len() + 20;
|
|
22
30
|
// Same atomic replacement reserve as v1: a previous bounded graph must
|
|
23
|
-
// fit its handle-only deletions beside the complete target
|
|
31
|
+
// fit its handle-only deletions beside the complete target, and the
|
|
32
|
+
// target's set elements, in one `wire::BATCH_BYTES` batch.
|
|
24
33
|
require(self.bytes <= 100_000 && self.operations.len() < 768)?;
|
|
25
34
|
self.operations.push((operation, attributes));
|
|
26
35
|
Ok(())
|
|
27
36
|
}
|
|
37
|
+
/// One named exact-match set. CONSTANT: its elements cannot change once a
|
|
38
|
+
/// rule binds it, so replacement always creates a complete new set.
|
|
39
|
+
fn set(&mut self, name: &str, id: u32, key_length: usize) -> Result<()> {
|
|
40
|
+
self.push(
|
|
41
|
+
9,
|
|
42
|
+
vec![
|
|
43
|
+
Attr::string(1, self.table),
|
|
44
|
+
Attr::string(2, name),
|
|
45
|
+
Attr::u32(3, 2),
|
|
46
|
+
Attr::u32(4, 0),
|
|
47
|
+
Attr::u32(5, key_length as u32),
|
|
48
|
+
Attr::u32(10, id),
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
/// The elements of one set, 256 to a message so every nested attribute
|
|
53
|
+
/// stays far below netlink's 16-bit attribute length. Elements have their
|
|
54
|
+
/// own budget: a replacement deletes the previous set, and so its elements,
|
|
55
|
+
/// by one handle.
|
|
56
|
+
fn elements(&mut self, name: &str, keys: Vec<Vec<u8>>) -> Result<()> {
|
|
57
|
+
for chunk in keys.chunks(256) {
|
|
58
|
+
let elements = chunk
|
|
59
|
+
.iter()
|
|
60
|
+
.map(|key| {
|
|
61
|
+
Attr::nested(1, vec![Attr::nested(1, vec![Attr::bytes(1, key.clone())])])
|
|
62
|
+
})
|
|
63
|
+
.collect();
|
|
64
|
+
let attributes = vec![
|
|
65
|
+
Attr::string(1, self.table),
|
|
66
|
+
Attr::string(2, name),
|
|
67
|
+
Attr::nested(3, elements),
|
|
68
|
+
];
|
|
69
|
+
self.element_bytes += wire::encode_attrs(&attributes).len() + 20;
|
|
70
|
+
require(self.element_bytes <= ELEMENT_BYTES && self.operations.len() < 768)?;
|
|
71
|
+
self.operations.push((12, attributes));
|
|
72
|
+
}
|
|
73
|
+
Ok(())
|
|
74
|
+
}
|
|
28
75
|
fn chain(&mut self, name: &str, base: Option<(u32, i32, &str)>) -> Result<()> {
|
|
29
76
|
let mut data = vec![Attr::string(1, self.table), Attr::string(3, name)];
|
|
30
77
|
if let Some((hook, priority, kind)) = base {
|
|
@@ -298,6 +345,7 @@ pub(super) fn compile(prepared: &Prepared, table: &str) -> Result<Vec<(u16, Vec<
|
|
|
298
345
|
stamp: format!("snft2:{}:{}", prepared.policy.revision, prepared.digest),
|
|
299
346
|
ordinal: 0,
|
|
300
347
|
bytes: 0,
|
|
348
|
+
element_bytes: 0,
|
|
301
349
|
operations: Vec::new(),
|
|
302
350
|
};
|
|
303
351
|
match &prepared.policy.scope {
|
package/rust/src/egress.host.rs
CHANGED
|
@@ -135,12 +135,34 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &HostScope) -> Result<()
|
|
|
135
135
|
for name in ["protected_out", "protected_back"] {
|
|
136
136
|
program.chain(name, None)?;
|
|
137
137
|
}
|
|
138
|
+
if !scope.host_grants.is_empty() {
|
|
139
|
+
// Validation admits only an address of exactly one handoff link.
|
|
140
|
+
hostgrant::tuple_set(program, &scope.host_grants, |grant| {
|
|
141
|
+
scope
|
|
142
|
+
.handoffs
|
|
143
|
+
.iter()
|
|
144
|
+
.find(|handoff| {
|
|
145
|
+
handoff
|
|
146
|
+
.link
|
|
147
|
+
.required_ipv4_addresses
|
|
148
|
+
.contains(&grant.source_address)
|
|
149
|
+
})
|
|
150
|
+
.map(|handoff| Some(&handoff.link))
|
|
151
|
+
.ok_or(crate::Error::Invalid)
|
|
152
|
+
})?;
|
|
153
|
+
}
|
|
138
154
|
protection(program, scope, false)?;
|
|
139
155
|
protection(program, scope, true)?;
|
|
140
156
|
// Published admission precedes the capture barrier: the barrier denies every
|
|
141
157
|
// protected original destination, which includes this uplink address, and an
|
|
142
158
|
// exact inbound publication is the explicit authority for that exception.
|
|
143
159
|
published(program, scope)?;
|
|
160
|
+
// Host-origin grants leave through the exact handoff that holds their source
|
|
161
|
+
// address, which is part of every set key, and precede the terminal output
|
|
162
|
+
// and input denials. Nothing else of the host's own traffic crosses a handoff.
|
|
163
|
+
if !scope.host_grants.is_empty() {
|
|
164
|
+
hostgrant::admissions(program, ["output", "input"], true, |_| vec![])?;
|
|
165
|
+
}
|
|
144
166
|
for handoff in &scope.handoffs {
|
|
145
167
|
program.jump("forward", link(&handoff.link, true), "protected_out")?;
|
|
146
168
|
program.jump("forward", link(&handoff.link, false), "protected_back")?;
|
|
@@ -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,6 +47,161 @@ 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 an endpoint address.
|
|
51
|
+
/// Validation admits only a current endpoint address, and endpoint prefixes are
|
|
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
|
+
let endpoint = scope
|
|
56
|
+
.endpoints
|
|
57
|
+
.iter()
|
|
58
|
+
.find(|endpoint| {
|
|
59
|
+
endpoint
|
|
60
|
+
.source_prefixes
|
|
61
|
+
.iter()
|
|
62
|
+
.any(|source| super::super::covers(source, &target) == Ok(true))
|
|
63
|
+
})
|
|
64
|
+
.ok_or(crate::Error::Invalid)?;
|
|
65
|
+
scope
|
|
66
|
+
.links
|
|
67
|
+
.iter()
|
|
68
|
+
.find(|link| link.interface_index == endpoint.interface_index)
|
|
69
|
+
.ok_or(crate::Error::Invalid)
|
|
70
|
+
}
|
|
71
|
+
fn published_link<'a>(scope: &'a RouterScope, port: &RouterPublishedPort) -> Result<&'a LocalLink> {
|
|
72
|
+
endpoint_link(scope, &port.endpoint_address)
|
|
73
|
+
}
|
|
74
|
+
/// Inbound publication envelope. Both directions check the exact links, the
|
|
75
|
+
/// translated current tuple and the original conntrack destination, so foreign
|
|
76
|
+
/// NAT cannot borrow this admission for another flow.
|
|
77
|
+
fn published_envelope(
|
|
78
|
+
scope: &RouterScope,
|
|
79
|
+
endpoint: &LocalLink,
|
|
80
|
+
port: &RouterPublishedPort,
|
|
81
|
+
reply: bool,
|
|
82
|
+
) -> Result<Vec<Attr>> {
|
|
83
|
+
let mut expressions = ipv4();
|
|
84
|
+
expressions.extend(link(&scope.handoff, !reply));
|
|
85
|
+
expressions.extend(link(endpoint, reply));
|
|
86
|
+
expressions.extend(meta(16, vec![protocol(&port.protocol)]));
|
|
87
|
+
expressions.extend(ct(10, None, vec![protocol(&port.protocol)]));
|
|
88
|
+
expressions.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
|
|
89
|
+
expressions.extend(ct(1, None, vec![u8::from(reply)]));
|
|
90
|
+
let target = format!("{}/32", port.endpoint_address);
|
|
91
|
+
expressions.extend(current_address(&target, reply)?);
|
|
92
|
+
expressions.extend(current_port(port.endpoint_port, reply));
|
|
93
|
+
let published = format!("{}/32", port.transit_source_address);
|
|
94
|
+
expressions.extend(original_address(&published, false)?);
|
|
95
|
+
expressions.extend(original_port(port.transit_port, false));
|
|
96
|
+
Ok(expressions)
|
|
97
|
+
}
|
|
98
|
+
/// Handoff ingress classification for an exact current publication. Raw runs
|
|
99
|
+
/// before conntrack, so it matches the arriving tuple only and never sets a
|
|
100
|
+
/// generation zone: a publication is an ordinary default-zone flow.
|
|
101
|
+
fn published_return(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
102
|
+
for port in &scope.published_ports {
|
|
103
|
+
let mut expressions = ipv4();
|
|
104
|
+
expressions.extend(link(&scope.handoff, true));
|
|
105
|
+
expressions.extend(meta(16, vec![protocol(&port.protocol)]));
|
|
106
|
+
expressions.extend(current_address(
|
|
107
|
+
&format!("{}/32", port.transit_source_address),
|
|
108
|
+
false,
|
|
109
|
+
)?);
|
|
110
|
+
expressions.extend(current_port(port.transit_port, false));
|
|
111
|
+
program.end("raw_return", expressions, 1)?;
|
|
112
|
+
}
|
|
113
|
+
Ok(())
|
|
114
|
+
}
|
|
115
|
+
/// Endpoint ingress classification for an exact current publication. The workload
|
|
116
|
+
/// side of a publication is an ordinary default-zone flow too, so it is matched on
|
|
117
|
+
/// its exact link, address and dedicated published port before the leased
|
|
118
|
+
/// classifiers below can zone it: a client whose source port equals a leased
|
|
119
|
+
/// grant's destination port would otherwise move the workload's published packets
|
|
120
|
+
/// into that generation, where they are not its flow. This only classifies; both
|
|
121
|
+
/// the anti-spoof guards and the publication's admissions still decide admission.
|
|
122
|
+
fn published_endpoint(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
123
|
+
for port in &scope.published_ports {
|
|
124
|
+
let mut expressions = ipv4();
|
|
125
|
+
expressions.extend(link(published_link(scope, port)?, true));
|
|
126
|
+
expressions.extend(meta(16, vec![protocol(&port.protocol)]));
|
|
127
|
+
expressions.extend(current_address(
|
|
128
|
+
&format!("{}/32", port.endpoint_address),
|
|
129
|
+
true,
|
|
130
|
+
)?);
|
|
131
|
+
expressions.extend(current_port(port.endpoint_port, true));
|
|
132
|
+
program.end("raw_in", expressions, 1)?;
|
|
133
|
+
}
|
|
134
|
+
Ok(())
|
|
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
|
+
}
|
|
169
|
+
fn published_translation(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
170
|
+
for port in &scope.published_ports {
|
|
171
|
+
let mut expressions = ipv4();
|
|
172
|
+
expressions.extend(link(&scope.handoff, true));
|
|
173
|
+
expressions.extend(meta(16, vec![protocol(&port.protocol)]));
|
|
174
|
+
expressions.extend(ct(10, None, vec![protocol(&port.protocol)]));
|
|
175
|
+
expressions.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
|
|
176
|
+
expressions.extend(ct(1, None, vec![0]));
|
|
177
|
+
expressions.extend(current_address(
|
|
178
|
+
&format!("{}/32", port.transit_source_address),
|
|
179
|
+
false,
|
|
180
|
+
)?);
|
|
181
|
+
expressions.extend(current_port(port.transit_port, false));
|
|
182
|
+
expressions.extend(dnat(&port.endpoint_address, port.endpoint_port)?);
|
|
183
|
+
program.rule("pre", expressions)?;
|
|
184
|
+
}
|
|
185
|
+
Ok(())
|
|
186
|
+
}
|
|
187
|
+
fn published_admissions(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
188
|
+
for port in &scope.published_ports {
|
|
189
|
+
let endpoint = published_link(scope, port)?;
|
|
190
|
+
let original = published_envelope(scope, endpoint, port, false)?;
|
|
191
|
+
for connection_state in [2, 8] {
|
|
192
|
+
let mut expressions = original.clone();
|
|
193
|
+
expressions.extend(state(connection_state));
|
|
194
|
+
if connection_state == 8 && port.protocol == "tcp" {
|
|
195
|
+
expressions.extend(opening_tcp());
|
|
196
|
+
}
|
|
197
|
+
program.end("forward", expressions, 1)?;
|
|
198
|
+
}
|
|
199
|
+
let mut reply = published_envelope(scope, endpoint, port, true)?;
|
|
200
|
+
reply.extend(state(2));
|
|
201
|
+
program.end("forward", reply, 1)?;
|
|
202
|
+
}
|
|
203
|
+
Ok(())
|
|
204
|
+
}
|
|
50
205
|
fn protected(
|
|
51
206
|
program: &mut Program<'_>,
|
|
52
207
|
chain: &str,
|
|
@@ -67,10 +222,20 @@ fn protected(
|
|
|
67
222
|
program.end(chain, vec![], -5)
|
|
68
223
|
}
|
|
69
224
|
fn raw(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
70
|
-
// Handoff ingress must classify only exact leased return envelopes
|
|
71
|
-
// ingress never reaches private/default-zone
|
|
225
|
+
// Handoff ingress must classify only exact leased return envelopes and exact
|
|
226
|
+
// inbound publications. Unknown ingress never reaches private/default-zone
|
|
227
|
+
// rules, even after a rename.
|
|
72
228
|
program.jump("raw_in", link(&scope.handoff, true), "raw_return")?;
|
|
73
229
|
program.deny_link("raw_in", &scope.handoff, true)?;
|
|
230
|
+
// A publication is the explicit authority for its exact tuple, so it is
|
|
231
|
+
// classified ahead of the protected-source denial below, exactly as a
|
|
232
|
+
// platform return envelope is. The host publishes into this address, so its
|
|
233
|
+
// clients are not confined to unprotected space.
|
|
234
|
+
published_return(program, scope)?;
|
|
235
|
+
host_grant_return(program, scope)?;
|
|
236
|
+
// Both sides of a publication lead the leased classifiers of every generation,
|
|
237
|
+
// so neither direction depends on the client's choice of source port.
|
|
238
|
+
published_endpoint(program, scope)?;
|
|
74
239
|
for platform in [true, false] {
|
|
75
240
|
if !platform {
|
|
76
241
|
for prefix in &scope.protection.prefixes {
|
|
@@ -190,9 +355,16 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope, revision:
|
|
|
190
355
|
] {
|
|
191
356
|
program.chain(name, Some((hook, priority, kind)))?;
|
|
192
357
|
}
|
|
358
|
+
if !scope.published_ports.is_empty() {
|
|
359
|
+
// Published destination NAT lives in this table's own prerouting chain, so
|
|
360
|
+
// it is created, replaced and deleted with the exact generation. An empty
|
|
361
|
+
// publication list keeps the previous chain set and compiled bytes.
|
|
362
|
+
program.chain("pre", Some((0, -100, "nat")))?;
|
|
363
|
+
}
|
|
193
364
|
for name in ["raw_return", "protected_out", "protected_back"] {
|
|
194
365
|
program.chain(name, None)?;
|
|
195
366
|
}
|
|
367
|
+
host_grant_sets(program, scope)?;
|
|
196
368
|
let private = policy::Policy {
|
|
197
369
|
schema_version: 1,
|
|
198
370
|
revision,
|
|
@@ -210,9 +382,23 @@ pub(super) fn compile(program: &mut Program<'_>, scope: &RouterScope, revision:
|
|
|
210
382
|
raw(program, scope)?;
|
|
211
383
|
protected(program, "protected_out", scope, false)?;
|
|
212
384
|
protected(program, "protected_back", scope, true)?;
|
|
385
|
+
published_translation(program, scope)?;
|
|
213
386
|
for chain in ["input", "forward", "output"] {
|
|
214
387
|
let parts = private.filter_rules(chain)?;
|
|
215
|
-
for expressions in parts.guards
|
|
388
|
+
for expressions in parts.guards {
|
|
389
|
+
program.rule(chain, expressions)?;
|
|
390
|
+
}
|
|
391
|
+
// Published admission keeps the private anti-spoof guards ahead of it and
|
|
392
|
+
// precedes the capture barrier: the barrier denies every protected original
|
|
393
|
+
// destination, which includes this transit address, and an exact inbound
|
|
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.
|
|
397
|
+
if chain == "forward" {
|
|
398
|
+
published_admissions(program, scope)?;
|
|
399
|
+
host_grant_admissions(program, scope)?;
|
|
400
|
+
}
|
|
401
|
+
for expressions in parts.grants {
|
|
216
402
|
program.rule(chain, expressions)?;
|
|
217
403
|
}
|
|
218
404
|
grants(program, scope, chain, true)?;
|