@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
package/rust/src/egress.rs
CHANGED
|
@@ -115,6 +115,10 @@ pub struct HostGrant {
|
|
|
115
115
|
pub destination_address: String,
|
|
116
116
|
pub destination_port: u16,
|
|
117
117
|
}
|
|
118
|
+
/// One stateful one-way flow between two workloads behind the same router: the
|
|
119
|
+
/// source workload dials one exact port of the destination workload, which may
|
|
120
|
+
/// only answer. It is the same exact tuple as a host grant.
|
|
121
|
+
pub type WorkloadGrant = HostGrant;
|
|
118
122
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
119
123
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
120
124
|
pub struct RouterScope {
|
|
@@ -131,6 +135,9 @@ pub struct RouterScope {
|
|
|
131
135
|
/// Absent and empty are the same canonical policy, like publications.
|
|
132
136
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
133
137
|
pub host_grants: Vec<HostGrant>,
|
|
138
|
+
/// Absent and empty are the same canonical policy, like publications.
|
|
139
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
140
|
+
pub workload_grants: Vec<WorkloadGrant>,
|
|
134
141
|
}
|
|
135
142
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
136
143
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -161,6 +168,20 @@ pub struct HostScope {
|
|
|
161
168
|
/// Absent and empty are the same canonical policy, like publications.
|
|
162
169
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
163
170
|
pub host_grants: Vec<HostGrant>,
|
|
171
|
+
/// Ids of platform endpoints this host serves on an address of one of its
|
|
172
|
+
/// bound links. Absent and empty are the same canonical policy.
|
|
173
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
174
|
+
pub local_platform_endpoints: Vec<String>,
|
|
175
|
+
}
|
|
176
|
+
/// One loopback TCP service that only one local user may dial: packets the host
|
|
177
|
+
/// sends to the exact address and port from a socket of any other user reject,
|
|
178
|
+
/// and packets arriving there on any interface but loopback drop.
|
|
179
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
180
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
181
|
+
pub struct LocalTcpPortOwner {
|
|
182
|
+
pub address: String,
|
|
183
|
+
pub port: u16,
|
|
184
|
+
pub uid: u32,
|
|
164
185
|
}
|
|
165
186
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
166
187
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -171,6 +192,10 @@ pub struct AllocationPoolGuardScope {
|
|
|
171
192
|
/// Absent and empty are the same canonical policy.
|
|
172
193
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
173
194
|
pub host_grants: Vec<HostGrant>,
|
|
195
|
+
/// Host-wide loopback port ownership. Absent and empty are the same
|
|
196
|
+
/// canonical policy.
|
|
197
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
198
|
+
pub local_tcp_port_owners: Vec<LocalTcpPortOwner>,
|
|
174
199
|
}
|
|
175
200
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
176
201
|
#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
|
|
@@ -565,6 +590,46 @@ fn normalize_router_host_grants(value: &mut RouterScope) -> Result<()> {
|
|
|
565
590
|
value.host_grants = grants;
|
|
566
591
|
Ok(())
|
|
567
592
|
}
|
|
593
|
+
/// Both ends are exact current addresses of two different veth workload endpoints
|
|
594
|
+
/// of this scope, neither router-local nor a platform endpoint. Endpoint prefixes
|
|
595
|
+
/// are protected, so both directions stay in the default conntrack zone ahead of
|
|
596
|
+
/// every leased classifier.
|
|
597
|
+
fn normalize_router_workload_grants(value: &mut RouterScope) -> Result<()> {
|
|
598
|
+
let mut grants = std::mem::take(&mut value.workload_grants);
|
|
599
|
+
let scope = &*value;
|
|
600
|
+
let workload = |ip: &str| -> Result<&policy::Endpoint> {
|
|
601
|
+
let target = format!("{ip}/32");
|
|
602
|
+
let endpoint = scope
|
|
603
|
+
.endpoints
|
|
604
|
+
.iter()
|
|
605
|
+
.find(|endpoint| {
|
|
606
|
+
endpoint
|
|
607
|
+
.source_prefixes
|
|
608
|
+
.iter()
|
|
609
|
+
.any(|source| covers(source, &target) == Ok(true))
|
|
610
|
+
})
|
|
611
|
+
.ok_or(Error::Invalid)?;
|
|
612
|
+
let local = scope
|
|
613
|
+
.links
|
|
614
|
+
.iter()
|
|
615
|
+
.chain(std::iter::once(&scope.handoff))
|
|
616
|
+
.any(|link| link.required_ipv4_addresses.iter().any(|item| item == ip));
|
|
617
|
+
let platform = scope
|
|
618
|
+
.protection
|
|
619
|
+
.platform_endpoints
|
|
620
|
+
.iter()
|
|
621
|
+
.any(|item| item.address == ip);
|
|
622
|
+
require(endpoint.interface_kind == "veth" && !local && !platform)?;
|
|
623
|
+
Ok(endpoint)
|
|
624
|
+
};
|
|
625
|
+
normalize_host_grants(&mut grants, |grant| {
|
|
626
|
+
let source = workload(&grant.source_address)?;
|
|
627
|
+
let destination = workload(&grant.destination_address)?;
|
|
628
|
+
require(source.id != destination.id)
|
|
629
|
+
})?;
|
|
630
|
+
value.workload_grants = grants;
|
|
631
|
+
Ok(())
|
|
632
|
+
}
|
|
568
633
|
fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
569
634
|
require(value.links.len() <= 32 && value.generations.len() <= 32)?;
|
|
570
635
|
let private = policy::Policy {
|
|
@@ -737,7 +802,8 @@ fn normalize_router(value: &mut RouterScope, revision: u64) -> Result<()> {
|
|
|
737
802
|
)
|
|
738
803
|
}))?;
|
|
739
804
|
normalize_router_published(value)?;
|
|
740
|
-
normalize_router_host_grants(value)
|
|
805
|
+
normalize_router_host_grants(value)?;
|
|
806
|
+
normalize_router_workload_grants(value)
|
|
741
807
|
}
|
|
742
808
|
fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
743
809
|
require(
|
|
@@ -760,8 +826,32 @@ fn normalize_pool_guard(value: &mut AllocationPoolGuardScope) -> Result<()> {
|
|
|
760
826
|
require(covers_address(&value.prefixes, &grant.destination_address)?)
|
|
761
827
|
})?;
|
|
762
828
|
value.host_grants = grants;
|
|
829
|
+
normalize_local_tcp_port_owners(&mut value.local_tcp_port_owners)
|
|
830
|
+
}
|
|
831
|
+
/// One exact IPv4 loopback host address and port per entry, never a prefix,
|
|
832
|
+
/// wildcard or range, each with exactly one owning uid. `(uid_t)-1` is not a
|
|
833
|
+
/// user; it is the kernel's "unchanged" sentinel.
|
|
834
|
+
fn normalize_local_tcp_port_owners(owners: &mut [LocalTcpPortOwner]) -> Result<()> {
|
|
835
|
+
require(owners.len() <= LOCAL_TCP_PORT_OWNERS)?;
|
|
836
|
+
owners.sort();
|
|
837
|
+
for (index, owner) in owners.iter().enumerate() {
|
|
838
|
+
let ip = address(&owner.address)?;
|
|
839
|
+
require(
|
|
840
|
+
ip >> 24 == 127
|
|
841
|
+
&& ip != 0x7f00_0000
|
|
842
|
+
&& ip != 0x7fff_ffff
|
|
843
|
+
&& owner.port > 0
|
|
844
|
+
&& owner.uid != u32::MAX
|
|
845
|
+
&& owners[..index]
|
|
846
|
+
.iter()
|
|
847
|
+
.all(|other| other.address != owner.address || other.port != owner.port),
|
|
848
|
+
)?;
|
|
849
|
+
}
|
|
763
850
|
Ok(())
|
|
764
851
|
}
|
|
852
|
+
/// Two rules per entry; the maximum fits the atomic graph beside a guard with
|
|
853
|
+
/// 64 pools and 1024 host grants.
|
|
854
|
+
const LOCAL_TCP_PORT_OWNERS: usize = 8;
|
|
765
855
|
/// The host dials from an exact current address of exactly one handoff link, into
|
|
766
856
|
/// protected space that is neither host-local, a platform endpoint nor a leased
|
|
767
857
|
/// router transit address: only a workload behind that handoff remains.
|
|
@@ -878,14 +968,31 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
|
|
|
878
968
|
.flat_map(|item| item.link.required_ipv4_addresses.iter().cloned())
|
|
879
969
|
.chain(value.uplink.required_ipv4_addresses.iter().cloned())
|
|
880
970
|
.collect();
|
|
971
|
+
// A platform endpoint on a host address is served here, not forwarded, so
|
|
972
|
+
// only a declared local endpoint may hold one; it must hold a bound address,
|
|
973
|
+
// which rtnetlink verifies at apply, recovery and inspection.
|
|
974
|
+
value.local_platform_endpoints.sort();
|
|
975
|
+
require(
|
|
976
|
+
value
|
|
977
|
+
.local_platform_endpoints
|
|
978
|
+
.windows(2)
|
|
979
|
+
.all(|pair| pair[0] != pair[1]),
|
|
980
|
+
)?;
|
|
981
|
+
for id in &value.local_platform_endpoints {
|
|
982
|
+
let endpoint = value
|
|
983
|
+
.protection
|
|
984
|
+
.platform_endpoints
|
|
985
|
+
.iter()
|
|
986
|
+
.find(|endpoint| endpoint.id == *id)
|
|
987
|
+
.ok_or(Error::Invalid)?;
|
|
988
|
+
require(local_addresses.contains(&endpoint.address))?;
|
|
989
|
+
}
|
|
881
990
|
for ip in &local_addresses {
|
|
882
991
|
require(
|
|
883
992
|
covers_address(&value.protection.prefixes, ip)?
|
|
884
|
-
&& value
|
|
885
|
-
.
|
|
886
|
-
|
|
887
|
-
.iter()
|
|
888
|
-
.all(|endpoint| endpoint.address != *ip),
|
|
993
|
+
&& value.protection.platform_endpoints.iter().all(|endpoint| {
|
|
994
|
+
endpoint.address != *ip || value.local_platform_endpoints.contains(&endpoint.id)
|
|
995
|
+
}),
|
|
889
996
|
)?;
|
|
890
997
|
}
|
|
891
998
|
for handoff in &mut value.handoffs {
|
|
@@ -949,8 +1056,17 @@ fn normalize_host(value: &mut HostScope) -> Result<()> {
|
|
|
949
1056
|
#[path = "egress_hostgrant_tests.rs"]
|
|
950
1057
|
pub(crate) mod hostgrant_tests;
|
|
951
1058
|
#[cfg(test)]
|
|
1059
|
+
#[path = "egress_localplatform_tests.rs"]
|
|
1060
|
+
pub(crate) mod localplatform_tests;
|
|
1061
|
+
#[cfg(test)]
|
|
1062
|
+
#[path = "egress_localport_tests.rs"]
|
|
1063
|
+
pub(crate) mod localport_tests;
|
|
1064
|
+
#[cfg(test)]
|
|
952
1065
|
#[path = "egress_tests.rs"]
|
|
953
1066
|
pub(crate) mod tests;
|
|
1067
|
+
#[cfg(test)]
|
|
1068
|
+
#[path = "egress_workloadgrant_tests.rs"]
|
|
1069
|
+
pub(crate) mod workloadgrant_tests;
|
|
954
1070
|
impl Policy {
|
|
955
1071
|
pub fn prepare(self) -> Result<Prepared> {
|
|
956
1072
|
use sha2::{Digest, Sha256};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//! Workload grants: stateful one-way flows from one workload endpoint to one
|
|
2
|
+
//! exact port of another, both behind the same router. Like host grants they
|
|
3
|
+
//! compile to a constant number of rules around exact set lookups. The tuple key
|
|
4
|
+
//! carries both protocols and the live and originally tracked tuple, so a
|
|
5
|
+
//! translated flow never matches. A second set binds every grant address to the
|
|
6
|
+
//! exact link that holds it (index and name), and both the incoming and the
|
|
7
|
+
//! outgoing link are looked up on every packet. Only the grant's source opens;
|
|
8
|
+
//! the destination only answers.
|
|
9
|
+
use super::hostgrant::{lookup, meta_to, payload_to, tuple_key, tuple_lookup};
|
|
10
|
+
use super::*;
|
|
11
|
+
use std::collections::BTreeSet;
|
|
12
|
+
|
|
13
|
+
/// Every exact workload-to-workload flow of the scope.
|
|
14
|
+
pub(super) const TUPLE_SET: &str = "workload_grant";
|
|
15
|
+
/// Every grant address with the exact link that holds it.
|
|
16
|
+
pub(super) const LINK_SET: &str = "workload_link";
|
|
17
|
+
|
|
18
|
+
/// The link a packet crosses and the address on its side of that link: the
|
|
19
|
+
/// incoming link and the source, or the outgoing link and the destination.
|
|
20
|
+
fn link_lookup(incoming: bool) -> Vec<Attr> {
|
|
21
|
+
vec![
|
|
22
|
+
meta_to(if incoming { 4 } else { 5 }, 4),
|
|
23
|
+
meta_to(if incoming { 6 } else { 7 }, 5),
|
|
24
|
+
payload_to(1, if incoming { 12 } else { 16 }, 4, 9),
|
|
25
|
+
lookup(LINK_SET),
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
fn link_key(link: &LocalLink, address: &str) -> Result<Vec<u8>> {
|
|
29
|
+
let mut key = link.interface_index.to_ne_bytes().to_vec();
|
|
30
|
+
let mut name = link.interface_name.as_bytes().to_vec();
|
|
31
|
+
name.resize(16, 0);
|
|
32
|
+
key.extend(name);
|
|
33
|
+
key.extend(super::address(address)?.to_be_bytes());
|
|
34
|
+
Ok(key)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub(super) fn sets(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
38
|
+
if scope.workload_grants.is_empty() {
|
|
39
|
+
return Ok(());
|
|
40
|
+
}
|
|
41
|
+
let mut keys = Vec::new();
|
|
42
|
+
let mut links = BTreeSet::new();
|
|
43
|
+
for grant in &scope.workload_grants {
|
|
44
|
+
keys.push(tuple_key(grant, None)?);
|
|
45
|
+
for address in [&grant.source_address, &grant.destination_address] {
|
|
46
|
+
links.insert(link_key(
|
|
47
|
+
super::router::endpoint_link(scope, address)?,
|
|
48
|
+
address,
|
|
49
|
+
)?);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
program.set(TUPLE_SET, 3, 32)?;
|
|
53
|
+
program.elements(TUPLE_SET, keys)?;
|
|
54
|
+
program.set(LINK_SET, 4, 24)?;
|
|
55
|
+
program.elements(LINK_SET, links.into_iter().collect())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Four forward admissions whatever the number of grants: the ESTABLISHED
|
|
59
|
+
/// original direction, a NEW UDP opening, a NEW TCP opening restricted to SYN
|
|
60
|
+
/// with FIN/RST/ACK clear, and the ESTABLISHED reply, each in the default
|
|
61
|
+
/// conntrack zone. A reply is only ever ESTABLISHED, so the destination
|
|
62
|
+
/// workload can never open a flow toward the source through a grant.
|
|
63
|
+
pub(super) fn admissions(program: &mut Program<'_>, scope: &RouterScope) -> Result<()> {
|
|
64
|
+
if scope.workload_grants.is_empty() {
|
|
65
|
+
return Ok(());
|
|
66
|
+
}
|
|
67
|
+
let admission = |reply: bool, connection_state: u32, transport: Option<&str>| {
|
|
68
|
+
let mut result = ipv4();
|
|
69
|
+
result.extend(ct(17, None, 0_u16.to_ne_bytes().to_vec()));
|
|
70
|
+
result.extend(ct(1, None, vec![u8::from(reply)]));
|
|
71
|
+
result.extend(state(connection_state));
|
|
72
|
+
if let Some(transport) = transport {
|
|
73
|
+
result.extend(meta(16, vec![protocol(transport)]));
|
|
74
|
+
if transport == "tcp" {
|
|
75
|
+
result.extend(opening_tcp());
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
result.extend(link_lookup(true));
|
|
79
|
+
result.extend(link_lookup(false));
|
|
80
|
+
result.extend(tuple_lookup(TUPLE_SET, false, reply));
|
|
81
|
+
result
|
|
82
|
+
};
|
|
83
|
+
program.end("forward", admission(false, 2, None), 1)?;
|
|
84
|
+
for transport in ["udp", "tcp"] {
|
|
85
|
+
program.end("forward", admission(false, 8, Some(transport)), 1)?;
|
|
86
|
+
}
|
|
87
|
+
program.end("forward", admission(true, 2, None), 1)
|
|
88
|
+
}
|
|
@@ -15,15 +15,15 @@ pub(crate) fn granted(mut value: Value, grants: Value) -> Value {
|
|
|
15
15
|
value["scope"]["hostGrants"] = grants;
|
|
16
16
|
value
|
|
17
17
|
}
|
|
18
|
-
fn normalized(value: Value) -> crate::Result<Policy> {
|
|
18
|
+
pub(super) fn normalized(value: Value) -> crate::Result<Policy> {
|
|
19
19
|
serde_json::from_value::<Policy>(value)
|
|
20
20
|
.map_err(|_| crate::Error::Invalid)?
|
|
21
21
|
.normalize()
|
|
22
22
|
}
|
|
23
|
-
fn prepared(value: Value) -> Prepared {
|
|
23
|
+
pub(super) fn prepared(value: Value) -> Prepared {
|
|
24
24
|
normalized(value).unwrap().prepare().unwrap()
|
|
25
25
|
}
|
|
26
|
-
fn bytes(prepared: &Prepared) -> Vec<u8> {
|
|
26
|
+
pub(super) fn bytes(prepared: &Prepared) -> Vec<u8> {
|
|
27
27
|
let mut result = Vec::new();
|
|
28
28
|
for (kind, attributes) in prepared.program("snft_host_grant").unwrap() {
|
|
29
29
|
result.extend(kind.to_be_bytes());
|
|
@@ -38,28 +38,36 @@ fn exact() -> Value {
|
|
|
38
38
|
|
|
39
39
|
/// One packet with the conntrack facts a filter hook observes.
|
|
40
40
|
#[derive(Clone, Copy)]
|
|
41
|
-
struct Flow {
|
|
42
|
-
input: Option<(u32, &'static str)>,
|
|
43
|
-
output: Option<(u32, &'static str)>,
|
|
44
|
-
protocol: u8,
|
|
45
|
-
source: [u8; 4],
|
|
46
|
-
destination: [u8; 4],
|
|
47
|
-
source_port: u16,
|
|
48
|
-
destination_port: u16,
|
|
49
|
-
tcp_flags: u8,
|
|
50
|
-
state: u32,
|
|
51
|
-
reply: bool,
|
|
52
|
-
zone: u16,
|
|
41
|
+
pub(super) struct Flow {
|
|
42
|
+
pub(super) input: Option<(u32, &'static str)>,
|
|
43
|
+
pub(super) output: Option<(u32, &'static str)>,
|
|
44
|
+
pub(super) protocol: u8,
|
|
45
|
+
pub(super) source: [u8; 4],
|
|
46
|
+
pub(super) destination: [u8; 4],
|
|
47
|
+
pub(super) source_port: u16,
|
|
48
|
+
pub(super) destination_port: u16,
|
|
49
|
+
pub(super) tcp_flags: u8,
|
|
50
|
+
pub(super) state: u32,
|
|
51
|
+
pub(super) reply: bool,
|
|
52
|
+
pub(super) zone: u16,
|
|
53
53
|
/// Original-direction conntrack tuple: source, destination, ports.
|
|
54
|
-
original: ([u8; 4], [u8; 4], u16, u16),
|
|
54
|
+
pub(super) original: ([u8; 4], [u8; 4], u16, u16),
|
|
55
|
+
/// The file system uid of the packet's user socket; None without one, when
|
|
56
|
+
/// `meta skuid` breaks the rule like the kernel.
|
|
57
|
+
pub(super) uid: Option<u32>,
|
|
55
58
|
}
|
|
56
59
|
const HOST: [u8; 4] = [10, 240, 0, 1];
|
|
57
60
|
const WORK: [u8; 4] = [10, 241, 0, 2];
|
|
58
61
|
const NEW: u32 = 8;
|
|
59
|
-
const ESTABLISHED: u32 = 2;
|
|
62
|
+
pub(super) const ESTABLISHED: u32 = 2;
|
|
60
63
|
const SYN: u8 = 0x02;
|
|
61
|
-
const ACK: u8 = 0x10;
|
|
62
|
-
|
|
64
|
+
pub(super) const ACK: u8 = 0x10;
|
|
65
|
+
/// The `reject with tcp reset` decision, distinct from a plain drop.
|
|
66
|
+
pub(super) const REJECT: i32 = i32::MIN;
|
|
67
|
+
pub(super) fn opening(
|
|
68
|
+
input: Option<(u32, &'static str)>,
|
|
69
|
+
output: Option<(u32, &'static str)>,
|
|
70
|
+
) -> Flow {
|
|
63
71
|
Flow {
|
|
64
72
|
input,
|
|
65
73
|
output,
|
|
@@ -73,6 +81,7 @@ fn opening(input: Option<(u32, &'static str)>, output: Option<(u32, &'static str
|
|
|
73
81
|
reply: false,
|
|
74
82
|
zone: 0,
|
|
75
83
|
original: (HOST, WORK, 443, 8080),
|
|
84
|
+
uid: None,
|
|
76
85
|
}
|
|
77
86
|
}
|
|
78
87
|
/// The workload's answer to `flow`, arriving on the reverse links.
|
|
@@ -181,17 +190,17 @@ fn foreign(flow: Flow) -> Vec<Flow> {
|
|
|
181
190
|
/// kernel does: the terminal verdict, the chain that reached it (`policy` for the
|
|
182
191
|
/// base chain's accept policy) and the conntrack zone a raw rule stored.
|
|
183
192
|
#[derive(Debug, PartialEq, Eq)]
|
|
184
|
-
struct Decision {
|
|
185
|
-
verdict: i32,
|
|
186
|
-
chain: String,
|
|
187
|
-
zone: u16,
|
|
193
|
+
pub(super) struct Decision {
|
|
194
|
+
pub(super) verdict: i32,
|
|
195
|
+
pub(super) chain: String,
|
|
196
|
+
pub(super) zone: u16,
|
|
188
197
|
}
|
|
189
198
|
/// The compiled table: rules per chain and element keys per set.
|
|
190
199
|
struct Table {
|
|
191
200
|
chains: BTreeMap<String, Vec<Vec<Attr>>>,
|
|
192
201
|
sets: BTreeMap<String, Vec<Vec<u8>>>,
|
|
193
202
|
}
|
|
194
|
-
fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
|
|
203
|
+
pub(super) fn chains(prepared: &Prepared) -> BTreeMap<String, Vec<Vec<Attr>>> {
|
|
195
204
|
table(prepared).chains
|
|
196
205
|
}
|
|
197
206
|
fn table(prepared: &Prepared) -> Table {
|
|
@@ -246,6 +255,7 @@ fn load(flow: &Flow, name: &str, fields: &[Attr]) -> Option<Vec<u8>> {
|
|
|
246
255
|
Some(match name {
|
|
247
256
|
"meta" => match wire::number(fields, 2).unwrap() {
|
|
248
257
|
4 => interface(flow.input, false),
|
|
258
|
+
10 => flow.uid?.to_ne_bytes().to_vec(),
|
|
249
259
|
5 => interface(flow.output, false),
|
|
250
260
|
6 => interface(flow.input, true),
|
|
251
261
|
7 => interface(flow.output, true),
|
|
@@ -340,10 +350,14 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
340
350
|
}
|
|
341
351
|
}
|
|
342
352
|
"cmp" => {
|
|
343
|
-
assert_eq!(wire::number(&fields, 2).unwrap(), 0);
|
|
344
353
|
let expected = data(&fields, 3);
|
|
345
354
|
let at = offset(&fields, 1);
|
|
346
|
-
|
|
355
|
+
let equal = registers[at..at + expected.len()] == expected[..];
|
|
356
|
+
matched = match wire::number(&fields, 2).unwrap() {
|
|
357
|
+
0 => equal,
|
|
358
|
+
1 => !equal,
|
|
359
|
+
operation => panic!("cmp {operation}"),
|
|
360
|
+
};
|
|
347
361
|
}
|
|
348
362
|
"range" => {
|
|
349
363
|
assert_eq!(wire::number(&fields, 2).unwrap(), 0);
|
|
@@ -381,6 +395,10 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
381
395
|
registers[at..at + value.len()].copy_from_slice(&value);
|
|
382
396
|
}
|
|
383
397
|
"nat" => {}
|
|
398
|
+
"reject" => {
|
|
399
|
+
assert_eq!(fields, vec![Attr::u32(1, 1)]);
|
|
400
|
+
verdict = Some((REJECT, None));
|
|
401
|
+
}
|
|
384
402
|
other => panic!("{other}"),
|
|
385
403
|
}
|
|
386
404
|
if !matched {
|
|
@@ -391,7 +409,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
391
409
|
continue;
|
|
392
410
|
}
|
|
393
411
|
match verdict {
|
|
394
|
-
Some((code @ (0 | 1), _)) => {
|
|
412
|
+
Some((code @ (0 | 1 | REJECT), _)) => {
|
|
395
413
|
return Step::Final(Decision {
|
|
396
414
|
verdict: code,
|
|
397
415
|
chain: chain.into(),
|
|
@@ -412,7 +430,7 @@ fn run(table: &Table, chain: &str, flow: &Flow, zone: &mut u16) -> Step {
|
|
|
412
430
|
Step::Next
|
|
413
431
|
}
|
|
414
432
|
/// A base chain that ends without a terminal verdict accepts by policy.
|
|
415
|
-
fn decide(prepared: &Prepared, chain: &str, flow: &Flow) -> Decision {
|
|
433
|
+
pub(super) fn decide(prepared: &Prepared, chain: &str, flow: &Flow) -> Decision {
|
|
416
434
|
let mut zone = 0;
|
|
417
435
|
match run(&table(prepared), chain, flow, &mut zone) {
|
|
418
436
|
Step::Final(decision) => decision,
|
|
@@ -793,7 +811,7 @@ fn host_grants_reject_ranges_wildcards_and_unbound_addresses() {
|
|
|
793
811
|
}
|
|
794
812
|
|
|
795
813
|
/// `count` distinct exact grants to the fixture workload, both protocols.
|
|
796
|
-
fn many(count: usize) -> Value {
|
|
814
|
+
pub(super) fn many(count: usize) -> Value {
|
|
797
815
|
json!((0..count)
|
|
798
816
|
.map(|index| host_grant(
|
|
799
817
|
if index % 2 == 0 { "tcp" } else { "udp" },
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
//! Host-local platform endpoints in host transit: leased flows from a handoff
|
|
2
|
+
//! into a platform endpoint served on a host address, and nothing else.
|
|
3
|
+
use super::hostgrant_tests::{
|
|
4
|
+
bytes, chains, decide, normalized, opening, prepared, Flow, ACK, ESTABLISHED,
|
|
5
|
+
};
|
|
6
|
+
use super::tests::host;
|
|
7
|
+
use serde_json::{json, Value};
|
|
8
|
+
|
|
9
|
+
const TRANSIT: [u8; 4] = [10, 240, 0, 2];
|
|
10
|
+
const UPLINK: [u8; 4] = [192, 0, 2, 2];
|
|
11
|
+
const HANDOFF: [u8; 4] = [10, 240, 0, 1];
|
|
12
|
+
const LINK: Option<(u32, &str)> = Some((4, "handoff"));
|
|
13
|
+
|
|
14
|
+
/// The host fixture with two platform endpoints on host addresses: a TCP hub
|
|
15
|
+
/// on the uplink address and a UDP store on the handoff address.
|
|
16
|
+
pub(crate) fn served(mut value: Value) -> Value {
|
|
17
|
+
value["scope"]["protection"]["platformEndpoints"]
|
|
18
|
+
.as_array_mut()
|
|
19
|
+
.unwrap()
|
|
20
|
+
.extend([
|
|
21
|
+
json!({"id":"hub","address":"192.0.2.2","protocol":"tcp","port":8443}),
|
|
22
|
+
json!({"id":"store","address":"10.240.0.1","protocol":"udp","port":7000}),
|
|
23
|
+
]);
|
|
24
|
+
value
|
|
25
|
+
}
|
|
26
|
+
pub(crate) fn local(mut value: Value, ids: Value) -> Value {
|
|
27
|
+
value["scope"]["localPlatformEndpoints"] = ids;
|
|
28
|
+
value
|
|
29
|
+
}
|
|
30
|
+
fn exact() -> Value {
|
|
31
|
+
local(served(host()), json!(["store", "hub"]))
|
|
32
|
+
}
|
|
33
|
+
/// A leased opening from the transit source address arriving on the handoff.
|
|
34
|
+
fn arrival(protocol: u8, source_port: u16, destination: [u8; 4], port: u16) -> Flow {
|
|
35
|
+
Flow {
|
|
36
|
+
input: LINK,
|
|
37
|
+
output: None,
|
|
38
|
+
protocol,
|
|
39
|
+
source: TRANSIT,
|
|
40
|
+
destination,
|
|
41
|
+
source_port,
|
|
42
|
+
destination_port: port,
|
|
43
|
+
original: (TRANSIT, destination, source_port, port),
|
|
44
|
+
..opening(LINK, None)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/// The host's answer to `flow`, leaving on the handoff.
|
|
48
|
+
fn answer(flow: Flow) -> Flow {
|
|
49
|
+
Flow {
|
|
50
|
+
input: None,
|
|
51
|
+
output: LINK,
|
|
52
|
+
source: flow.destination,
|
|
53
|
+
destination: flow.source,
|
|
54
|
+
source_port: flow.destination_port,
|
|
55
|
+
destination_port: flow.source_port,
|
|
56
|
+
tcp_flags: ACK,
|
|
57
|
+
state: ESTABLISHED,
|
|
58
|
+
reply: true,
|
|
59
|
+
..flow
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn local_platform_endpoints_absent_and_empty_keep_the_host_digest_and_compiled_bytes() {
|
|
65
|
+
let baseline = prepared(host());
|
|
66
|
+
let empty = prepared(local(host(), json!([])));
|
|
67
|
+
assert_eq!(empty, baseline);
|
|
68
|
+
assert_eq!(bytes(&empty), bytes(&baseline));
|
|
69
|
+
let with = prepared(exact());
|
|
70
|
+
assert!(serde_json::to_string(&with.policy)
|
|
71
|
+
.unwrap()
|
|
72
|
+
.contains("\"localPlatformEndpoints\":[\"hub\",\"store\"]"));
|
|
73
|
+
// Canonical order.
|
|
74
|
+
assert_eq!(
|
|
75
|
+
prepared(local(served(host()), json!(["hub", "store"]))),
|
|
76
|
+
with
|
|
77
|
+
);
|
|
78
|
+
// The endpoints and the chains exist only when declared.
|
|
79
|
+
let graph = chains(&with);
|
|
80
|
+
assert!(
|
|
81
|
+
graph.contains_key("local_platform_in")
|
|
82
|
+
&& !chains(&baseline).contains_key("local_platform_in")
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn local_platform_endpoint_on_a_host_address_is_refused_unless_declared() {
|
|
88
|
+
// The refusal stays for every undeclared endpoint on a host address.
|
|
89
|
+
assert!(normalized(served(host())).is_err());
|
|
90
|
+
assert!(normalized(local(served(host()), json!(["hub"]))).is_err());
|
|
91
|
+
assert!(normalized(local(served(host()), json!(["store"]))).is_err());
|
|
92
|
+
assert!(normalized(exact()).is_ok());
|
|
93
|
+
for invalid in [
|
|
94
|
+
// Unknown, duplicate, or an endpoint the host does not hold.
|
|
95
|
+
json!(["hub", "store", "missing"]),
|
|
96
|
+
json!(["hub", "store", "hub"]),
|
|
97
|
+
json!(["hub", "store", "dns"]),
|
|
98
|
+
json!(["hub", "store", "relay"]),
|
|
99
|
+
json!(["hub", "store", ""]),
|
|
100
|
+
json!(["hub", "store", 1]),
|
|
101
|
+
json!("hub"),
|
|
102
|
+
json!({"hub": true}),
|
|
103
|
+
] {
|
|
104
|
+
assert!(
|
|
105
|
+
normalized(local(served(host()), invalid.clone())).is_err(),
|
|
106
|
+
"{invalid}"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn local_platform_endpoint_admits_only_leased_exact_untranslated_flows_and_their_replies() {
|
|
113
|
+
let baseline = prepared(host());
|
|
114
|
+
let with = prepared(exact());
|
|
115
|
+
let tcp = arrival(6, 10500, UPLINK, 8443);
|
|
116
|
+
let udp = arrival(17, 10500, HANDOFF, 7000);
|
|
117
|
+
let decided = |flow: &Flow| {
|
|
118
|
+
let chain = if flow.input.is_some() {
|
|
119
|
+
"input"
|
|
120
|
+
} else {
|
|
121
|
+
"output"
|
|
122
|
+
};
|
|
123
|
+
(
|
|
124
|
+
decide(&baseline, chain, flow).verdict,
|
|
125
|
+
decide(&with, chain, flow).verdict,
|
|
126
|
+
)
|
|
127
|
+
};
|
|
128
|
+
// Every admitted packet: the TCP opening, established TCP, the UDP opening,
|
|
129
|
+
// and both replies. The baseline denies each at the handoff.
|
|
130
|
+
for flow in [
|
|
131
|
+
tcp,
|
|
132
|
+
Flow {
|
|
133
|
+
state: ESTABLISHED,
|
|
134
|
+
tcp_flags: ACK,
|
|
135
|
+
..tcp
|
|
136
|
+
},
|
|
137
|
+
udp,
|
|
138
|
+
answer(tcp),
|
|
139
|
+
answer(udp),
|
|
140
|
+
] {
|
|
141
|
+
assert_eq!(decided(&flow), (0, 1));
|
|
142
|
+
}
|
|
143
|
+
// Everything else keeps the baseline decision: a source port outside the
|
|
144
|
+
// leased range of the protocol, another source address, another port or
|
|
145
|
+
// protocol, a flow translated in transit, another zone, a TCP opening
|
|
146
|
+
// without SYN, and a host-origin opening toward the transit address.
|
|
147
|
+
for flow in [
|
|
148
|
+
arrival(6, 20000, UPLINK, 8443),
|
|
149
|
+
arrival(6, 9999, UPLINK, 8443),
|
|
150
|
+
arrival(6, 10500, UPLINK, 8444),
|
|
151
|
+
arrival(17, 10500, UPLINK, 8443),
|
|
152
|
+
arrival(6, 10500, HANDOFF, 7000),
|
|
153
|
+
arrival(17, 10500, HANDOFF, 7001),
|
|
154
|
+
Flow {
|
|
155
|
+
source: [10, 240, 0, 3],
|
|
156
|
+
original: ([10, 240, 0, 3], UPLINK, 10500, 8443),
|
|
157
|
+
..tcp
|
|
158
|
+
},
|
|
159
|
+
Flow {
|
|
160
|
+
original: (TRANSIT, HANDOFF, 10500, 9000),
|
|
161
|
+
..tcp
|
|
162
|
+
},
|
|
163
|
+
Flow { zone: 7, ..tcp },
|
|
164
|
+
Flow {
|
|
165
|
+
tcp_flags: ACK,
|
|
166
|
+
..tcp
|
|
167
|
+
},
|
|
168
|
+
Flow {
|
|
169
|
+
reply: false,
|
|
170
|
+
state: 8,
|
|
171
|
+
tcp_flags: 0x02,
|
|
172
|
+
original: (UPLINK, TRANSIT, 8443, 10500),
|
|
173
|
+
..answer(tcp)
|
|
174
|
+
},
|
|
175
|
+
Flow {
|
|
176
|
+
state: 8,
|
|
177
|
+
..answer(tcp)
|
|
178
|
+
},
|
|
179
|
+
] {
|
|
180
|
+
let (before, after) = decided(&flow);
|
|
181
|
+
assert_eq!(after, before, "{:?}", flow.original);
|
|
182
|
+
}
|
|
183
|
+
// A translated arrival that the source range admits is still refused: the
|
|
184
|
+
// original tuple is part of every check.
|
|
185
|
+
assert_eq!(
|
|
186
|
+
decided(&Flow {
|
|
187
|
+
original: (TRANSIT, HANDOFF, 10500, 9000),
|
|
188
|
+
..tcp
|
|
189
|
+
}),
|
|
190
|
+
(0, 0)
|
|
191
|
+
);
|
|
192
|
+
// Forwarded traffic and the uplink keep their exact previous rules. (The
|
|
193
|
+
// protected chains list every platform endpoint, served here or not.)
|
|
194
|
+
let graph = chains(&with);
|
|
195
|
+
let reference = chains(&baseline);
|
|
196
|
+
for name in ["forward", "post"] {
|
|
197
|
+
assert_eq!(graph[name], reference[name], "{name}");
|
|
198
|
+
}
|
|
199
|
+
// One jump per leased range per direction, ahead of the handoff denials.
|
|
200
|
+
assert_eq!(graph["input"].len(), reference["input"].len() + 2);
|
|
201
|
+
assert_eq!(graph["output"].len(), reference["output"].len() + 2);
|
|
202
|
+
assert_eq!(graph["local_platform_in"].len(), 4);
|
|
203
|
+
assert_eq!(graph["local_platform_back"].len(), 2);
|
|
204
|
+
}
|