@push.rocks/smartnftables 1.4.0 → 1.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 +26 -0
- package/dist_rust/smartnftables_linux_amd64_musl +0 -0
- package/dist_rust/smartnftables_linux_amd64_musl.tsrust-build.json +4 -4
- package/dist_rust/smartnftables_linux_arm64_musl +0 -0
- package/dist_rust/smartnftables_linux_arm64_musl.tsrust-build.json +4 -4
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.manageddockerforwarding.d.ts +17 -0
- package/dist_ts/classes.manageddockerforwarding.js +128 -0
- package/dist_ts/classes.managednftables.d.ts +9 -7
- package/dist_ts/classes.managednftables.js +2 -2
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/managed.docker.types.d.ts +78 -0
- package/dist_ts/managed.docker.types.js +2 -0
- package/dist_ts/managed.egress.types.d.ts +91 -0
- package/dist_ts/managed.egress.types.js +2 -0
- package/dist_ts/managed.types.d.ts +20 -18
- package/package.json +2 -2
- package/readme.md +143 -6
- package/rust/src/docker.frontend.rs +203 -0
- package/rust/src/docker.graph.rs +129 -0
- package/rust/src/docker.policy.rs +140 -0
- package/rust/src/docker.rs +191 -0
- package/rust/src/docker_process_tests.rs +29 -0
- package/rust/src/docker_tests.rs +148 -0
- package/rust/src/egress.compile.rs +288 -0
- package/rust/src/egress.host.rs +103 -0
- package/rust/src/egress.router.rs +231 -0
- package/rust/src/egress.rs +682 -0
- package/rust/src/egress_tests.rs +332 -0
- package/rust/src/main.rs +13 -6
- package/rust/src/managed.rs +105 -0
- package/rust/src/owner.rs +115 -7
- package/rust/src/owner_coexistence_tests.rs +77 -0
- package/rust/src/owner_egress_identity_tests.rs +155 -0
- package/rust/src/owner_egress_tests.rs +153 -0
- package/rust/src/owner_egress_traffic_tests.rs +470 -0
- package/rust/src/owner_host_traffic_tests.rs +305 -0
- package/rust/src/owner_identity_tests.rs +169 -0
- package/rust/src/owner_link_tests.rs +119 -0
- package/rust/src/owner_packet_fixture.rs +200 -0
- package/rust/src/owner_tests.rs +33 -7
- package/rust/src/policy.rs +120 -81
- package/rust/src/tests.rs +22 -0
- package/rust/src/wire.links.rs +279 -0
- package/rust/src/wire.rs +16 -66
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +113 -0
- package/ts/classes.managednftables.ts +13 -13
- package/ts/index.ts +3 -0
- package/ts/managed.docker.types.ts +49 -0
- package/ts/managed.egress.types.ts +85 -0
- package/ts/managed.types.ts +20 -16
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
use crate::{egress, managed, owner, policy, Error, Result};
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
use sha2::{Digest, Sha256};
|
|
4
|
+
|
|
5
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
6
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
7
|
+
pub struct Policy {
|
|
8
|
+
pub schema_version: u8,
|
|
9
|
+
pub revision: u64,
|
|
10
|
+
pub barrier: owner::Applied,
|
|
11
|
+
}
|
|
12
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
13
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
14
|
+
pub struct Prepared {
|
|
15
|
+
pub policy: Policy,
|
|
16
|
+
pub backend: String,
|
|
17
|
+
pub digest: String,
|
|
18
|
+
}
|
|
19
|
+
pub fn validate_backend(value: &str) -> Result<()> {
|
|
20
|
+
let patch=value.strip_prefix("v1.8.").and_then(|v|v.strip_suffix(" (nf_tables)")).ok_or(Error::UnsupportedBackend)?;
|
|
21
|
+
let number=patch.parse::<u16>().map_err(|_|Error::UnsupportedBackend)?;
|
|
22
|
+
if number < 10 || number.to_string()!=patch {return Err(Error::UnsupportedBackend);}
|
|
23
|
+
Ok(())
|
|
24
|
+
}
|
|
25
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
26
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
27
|
+
pub struct Options {
|
|
28
|
+
pub owner_id: String,
|
|
29
|
+
pub instance_id: String,
|
|
30
|
+
}
|
|
31
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
32
|
+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
33
|
+
pub struct Identity {
|
|
34
|
+
pub owner_id: String,
|
|
35
|
+
pub instance_id: String,
|
|
36
|
+
pub boot_id: String,
|
|
37
|
+
pub namespace_device: String,
|
|
38
|
+
pub namespace_inode: String,
|
|
39
|
+
}
|
|
40
|
+
impl Options {
|
|
41
|
+
pub fn validate(&self) -> Result<()> {
|
|
42
|
+
if !policy::identifier(&self.owner_id) || !policy::identifier(&self.instance_id) || self.instance_id.len() < 16 {
|
|
43
|
+
return Err(Error::Invalid);
|
|
44
|
+
}
|
|
45
|
+
Ok(())
|
|
46
|
+
}
|
|
47
|
+
pub fn identity(&self) -> Result<Identity> {
|
|
48
|
+
self.validate()?;
|
|
49
|
+
let (boot_id, namespace_device, namespace_inode) = owner::context()?;
|
|
50
|
+
Ok(Identity { owner_id:self.owner_id.clone(), instance_id:self.instance_id.clone(), boot_id, namespace_device, namespace_inode })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
impl Policy {
|
|
54
|
+
pub fn scope(&self) -> Result<&egress::HostScope> {
|
|
55
|
+
match &self.barrier.prepared.policy {
|
|
56
|
+
managed::Policy::Egress(egress::Policy { scope:egress::Scope::HostTransit(scope), .. }) => Ok(scope),
|
|
57
|
+
_ => Err(Error::Invalid),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
pub fn prepare(self, backend: String) -> Result<Prepared> {
|
|
61
|
+
validate_backend(&backend)?;
|
|
62
|
+
if self.schema_version != 1 || self.revision == 0 || self.revision > 9_007_199_254_740_991 {
|
|
63
|
+
return Err(Error::Invalid);
|
|
64
|
+
}
|
|
65
|
+
self.barrier.prepared.validate()?;
|
|
66
|
+
self.scope()?;
|
|
67
|
+
let receipt = &self.barrier.receipt;
|
|
68
|
+
let handle = receipt.table_handle.parse::<u64>().map_err(|_| Error::Invalid)?;
|
|
69
|
+
if handle == 0 || handle.to_string() != receipt.table_handle
|
|
70
|
+
|| receipt.revision != self.barrier.prepared.policy.revision()
|
|
71
|
+
|| receipt.digest != self.barrier.prepared.digest
|
|
72
|
+
|| !policy::identifier(&receipt.identity.owner_id)
|
|
73
|
+
|| !policy::identifier(&receipt.identity.instance_id)
|
|
74
|
+
|| receipt.identity.instance_id.len() < 16
|
|
75
|
+
|| !policy::identifier(&receipt.identity.table_name)
|
|
76
|
+
|| !receipt.identity.table_name.starts_with("snft_") {
|
|
77
|
+
return Err(Error::Invalid);
|
|
78
|
+
}
|
|
79
|
+
let mut hash = Sha256::new();
|
|
80
|
+
hash.update(b"smartnftables/docker-forwarding-v1\0");
|
|
81
|
+
hash.update(backend.as_bytes());
|
|
82
|
+
hash.update([0]);
|
|
83
|
+
hash.update(serde_json::to_vec(&self).map_err(|_| Error::Invalid)?);
|
|
84
|
+
let prepared = Prepared { policy:self, backend, digest:format!("sha256:{:x}",hash.finalize()) };
|
|
85
|
+
// Bound the fully expanded restore transaction before any subprocess.
|
|
86
|
+
prepared.rules(&Options { owner_id:"x".repeat(64), instance_id:"x".repeat(64) })?;
|
|
87
|
+
Ok(prepared)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
91
|
+
pub struct Rule {
|
|
92
|
+
pub comment: String,
|
|
93
|
+
pub args: Vec<String>,
|
|
94
|
+
}
|
|
95
|
+
fn port_range(first: u16, last: u16) -> String {
|
|
96
|
+
if first == last { first.to_string() } else { format!("{first}:{last}") }
|
|
97
|
+
}
|
|
98
|
+
impl Prepared {
|
|
99
|
+
pub fn validate(&self) -> Result<()> {
|
|
100
|
+
if self.policy.clone().prepare(self.backend.clone())? != *self { return Err(Error::Invalid); }
|
|
101
|
+
Ok(())
|
|
102
|
+
}
|
|
103
|
+
pub fn rules(&self, options: &Options) -> Result<Vec<Rule>> {
|
|
104
|
+
options.validate()?;
|
|
105
|
+
let scope = self.policy.scope()?;
|
|
106
|
+
let mut result = Vec::new();
|
|
107
|
+
let mut bytes = 0;
|
|
108
|
+
for handoff in &scope.handoffs {
|
|
109
|
+
for allocation in &handoff.allocations {
|
|
110
|
+
for range in &allocation.source_port_ranges {
|
|
111
|
+
for state in ["NEW", "ESTABLISHED", "REPLY"] {
|
|
112
|
+
let reply = state == "REPLY";
|
|
113
|
+
let mut args: Vec<String> = vec![
|
|
114
|
+
"-i".into(), if reply { scope.uplink.interface_name.clone() } else { handoff.link.interface_name.clone() },
|
|
115
|
+
"-o".into(), if reply { handoff.link.interface_name.clone() } else { scope.uplink.interface_name.clone() },
|
|
116
|
+
if reply { "-d" } else { "-s" }.into(), format!("{}/32", allocation.transit_source_address),
|
|
117
|
+
"-p".into(),range.protocol.clone(),"-m".into(),range.protocol.clone(),
|
|
118
|
+
if reply { "--dport" } else { "--sport" }.into(),port_range(range.first,range.last),
|
|
119
|
+
];
|
|
120
|
+
if state == "NEW" && range.protocol == "tcp" {
|
|
121
|
+
args.extend(["--tcp-flags","FIN,SYN,RST,ACK","SYN"].map(String::from));
|
|
122
|
+
}
|
|
123
|
+
args.extend(["-m","conntrack","--ctstate",if reply {"ESTABLISHED"} else {state},"--ctproto",if range.protocol == "tcp" {"6"} else {"17"},"--ctorigsrc"].map(String::from));
|
|
124
|
+
// Canonical xtables host form: explicit /32 has different unused
|
|
125
|
+
// conntrack mask bytes and does not survive a save/restore round trip.
|
|
126
|
+
args.push(allocation.transit_source_address.clone());
|
|
127
|
+
args.push("--ctorigsrcport".into());args.push(port_range(range.first,range.last));
|
|
128
|
+
args.extend(["--ctdir",if reply {"REPLY"} else {"ORIGINAL"},"-m","comment","--comment"].map(String::from));
|
|
129
|
+
let comment = format!("snftd1:{}:{}:{}:{}",options.owner_id,options.instance_id,self.digest.strip_prefix("sha256:").ok_or(Error::Invalid)?,result.len());
|
|
130
|
+
args.push(comment.clone());args.extend(["-j","ACCEPT"].map(String::from));
|
|
131
|
+
bytes += args.iter().map(|arg|arg.len()+1).sum::<usize>()+32;
|
|
132
|
+
if result.len() >= 192 || bytes > 100_000 { return Err(Error::Invalid); }
|
|
133
|
+
result.push(Rule {comment,args});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
Ok(result)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
use crate::{decode, encode, owner, wire, Error, Request, Result};
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
#[path = "docker.policy.rs"]
|
|
4
|
+
mod policy;
|
|
5
|
+
#[path = "docker.frontend.rs"]
|
|
6
|
+
mod frontend;
|
|
7
|
+
#[path = "docker.graph.rs"]
|
|
8
|
+
mod graph;
|
|
9
|
+
use policy::{Identity, Options, Prepared};
|
|
10
|
+
#[cfg(test)]
|
|
11
|
+
#[path="docker_tests.rs"]
|
|
12
|
+
mod tests;
|
|
13
|
+
|
|
14
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
15
|
+
#[serde(rename_all="camelCase",deny_unknown_fields)]
|
|
16
|
+
pub struct Receipt {
|
|
17
|
+
pub identity: Identity,
|
|
18
|
+
pub backend: String,
|
|
19
|
+
pub revision: u64,
|
|
20
|
+
pub digest: String,
|
|
21
|
+
}
|
|
22
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
23
|
+
#[serde(rename_all="camelCase",deny_unknown_fields)]
|
|
24
|
+
pub struct Applied { pub receipt: Receipt, pub prepared: Prepared }
|
|
25
|
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
26
|
+
#[serde(rename_all="camelCase",deny_unknown_fields)]
|
|
27
|
+
pub struct Transition {
|
|
28
|
+
#[serde(deserialize_with="crate::policy::required_nullable")]
|
|
29
|
+
pub previous: Option<Applied>,
|
|
30
|
+
pub target: Prepared,
|
|
31
|
+
}
|
|
32
|
+
#[derive(Clone, Debug, Serialize)]
|
|
33
|
+
#[serde(rename_all="camelCase")]
|
|
34
|
+
pub struct Status {
|
|
35
|
+
pub identity: Identity,
|
|
36
|
+
pub state: String,
|
|
37
|
+
pub present: bool,
|
|
38
|
+
pub applied: Option<Applied>,
|
|
39
|
+
pub pending: Option<Transition>,
|
|
40
|
+
pub error: Option<String>,
|
|
41
|
+
}
|
|
42
|
+
pub struct Owner {
|
|
43
|
+
options: Options,
|
|
44
|
+
identity: Identity,
|
|
45
|
+
applied: Option<Applied>,
|
|
46
|
+
pending: Option<Transition>,
|
|
47
|
+
error: Option<Error>,
|
|
48
|
+
released: bool,
|
|
49
|
+
}
|
|
50
|
+
impl Owner {
|
|
51
|
+
fn new(options: Options) -> Result<Self> {
|
|
52
|
+
let identity=options.identity()?;
|
|
53
|
+
Ok(Self {options,identity,applied:None,pending:None,error:None,released:false})
|
|
54
|
+
}
|
|
55
|
+
fn context(&self) -> Result<()> {
|
|
56
|
+
if self.options.identity()? != self.identity {return Err(Error::Conflict);}
|
|
57
|
+
Ok(())
|
|
58
|
+
}
|
|
59
|
+
fn validate_target(&self, target: &Prepared) -> Result<()> {
|
|
60
|
+
target.validate()?;
|
|
61
|
+
self.context()?;
|
|
62
|
+
let identity=&target.policy.barrier.receipt.identity;
|
|
63
|
+
if identity.boot_id!=self.identity.boot_id || identity.namespace_device!=self.identity.namespace_device
|
|
64
|
+
|| identity.namespace_inode!=self.identity.namespace_inode {return Err(Error::Conflict);}
|
|
65
|
+
Ok(())
|
|
66
|
+
}
|
|
67
|
+
fn validate_receipt(&self, expected: &Applied) -> Result<()> {
|
|
68
|
+
self.validate_target(&expected.prepared)?;
|
|
69
|
+
if expected.receipt.identity!=self.identity || expected.receipt.digest!=expected.prepared.digest
|
|
70
|
+
|| expected.receipt.revision!=expected.prepared.policy.revision
|
|
71
|
+
|| expected.receipt.backend!=expected.prepared.backend {return Err(Error::Conflict);}
|
|
72
|
+
Ok(())
|
|
73
|
+
}
|
|
74
|
+
fn fence(target: &Prepared) -> Result<()> {
|
|
75
|
+
let scope=target.policy.scope()?;
|
|
76
|
+
wire::verify_bound_interfaces_down(scope.handoffs.iter().map(|handoff| {
|
|
77
|
+
let link=&handoff.link;
|
|
78
|
+
wire::BoundInterface {index:link.interface_index,name:&link.interface_name,kind:&link.interface_kind,
|
|
79
|
+
mac:link.mac_address.as_deref(),link_index:link.interface_link_index,addresses:&link.required_ipv4_addresses}
|
|
80
|
+
}))
|
|
81
|
+
}
|
|
82
|
+
fn applied(&self, prepared: Prepared, backend: String) -> Applied {
|
|
83
|
+
Applied {receipt:Receipt {identity:self.identity.clone(),backend,revision:prepared.policy.revision,digest:prepared.digest.clone()},prepared}
|
|
84
|
+
}
|
|
85
|
+
fn reconcile(&mut self, transition: Transition, deadline: std::time::Instant) -> Result<Applied> {
|
|
86
|
+
self.validate_target(&transition.target)?;
|
|
87
|
+
if let Some(previous)=&transition.previous {
|
|
88
|
+
self.validate_receipt(previous)?;
|
|
89
|
+
if previous.prepared.policy.revision>=transition.target.policy.revision {return Err(Error::Conflict);}
|
|
90
|
+
}
|
|
91
|
+
if self.released || self.pending.as_ref().is_some_and(|v|v!=&transition)
|
|
92
|
+
|| self.applied.as_ref().is_some_and(|v|Some(v)!=transition.previous.as_ref() && v.prepared!=transition.target) {return Err(Error::Conflict);}
|
|
93
|
+
self.pending=Some(transition.clone());
|
|
94
|
+
let result=(|| {
|
|
95
|
+
let target_rules=transition.target.rules(&self.options)?;
|
|
96
|
+
let previous_rules=transition.previous.as_ref().map(|v|v.prepared.rules(&self.options)).transpose()?.unwrap_or_default();
|
|
97
|
+
let snapshot=frontend::Snapshot::read(deadline)?;
|
|
98
|
+
if snapshot.backend!=transition.target.backend {return Err(Error::Conflict);}
|
|
99
|
+
if transition.previous.as_ref().is_some_and(|v|v.receipt.backend!=snapshot.backend) {return Err(Error::Conflict);}
|
|
100
|
+
owner::Owner::verify_external(&transition.target.policy.barrier)?;
|
|
101
|
+
if snapshot.verified_matches(&self.options,&target_rules,deadline)? {
|
|
102
|
+
return Ok(self.applied(transition.target.clone(),snapshot.backend));
|
|
103
|
+
}
|
|
104
|
+
if !snapshot.verified_matches(&self.options,&previous_rules,deadline)? {return Err(Error::Conflict);}
|
|
105
|
+
if let Some(previous)=&transition.previous {Self::fence(&previous.prepared)?;}
|
|
106
|
+
Self::fence(&transition.target)?;
|
|
107
|
+
frontend::replace(&previous_rules,&target_rules,deadline)?;
|
|
108
|
+
self.context()?;
|
|
109
|
+
let current=frontend::Snapshot::read(deadline)?;
|
|
110
|
+
if current.backend!=snapshot.backend || !current.verified_matches(&self.options,&target_rules,deadline)? {return Err(Error::Conflict);}
|
|
111
|
+
owner::Owner::verify_external(&transition.target.policy.barrier)?;
|
|
112
|
+
if let Some(previous)=&transition.previous {Self::fence(&previous.prepared)?;}
|
|
113
|
+
Self::fence(&transition.target)?;
|
|
114
|
+
Ok(self.applied(transition.target.clone(),current.backend))
|
|
115
|
+
})();
|
|
116
|
+
match result {
|
|
117
|
+
Ok(applied)=>{self.applied=Some(applied.clone());self.pending=None;self.error=None;Ok(applied)},
|
|
118
|
+
Err(error)=>{self.error=Some(error);Err(error)},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
fn inspect(&mut self, deadline: std::time::Instant) -> Status {
|
|
122
|
+
let mut present=false;
|
|
123
|
+
if let Some(applied)=self.applied.clone().filter(|_|!self.released) {
|
|
124
|
+
let result=(|| {
|
|
125
|
+
self.validate_receipt(&applied)?;
|
|
126
|
+
let current=frontend::Snapshot::read(deadline)?;
|
|
127
|
+
if current.backend!=applied.receipt.backend || !current.verified_matches(&self.options,&applied.prepared.rules(&self.options)?,deadline)? {return Err(Error::Conflict);}
|
|
128
|
+
owner::Owner::verify_external(&applied.prepared.policy.barrier)
|
|
129
|
+
})();
|
|
130
|
+
match result {Ok(())=>present=self.pending.is_none()&&self.error.is_none(),Err(error)=>self.error=Some(error)}
|
|
131
|
+
}
|
|
132
|
+
Status {identity:self.identity.clone(),state:if self.released {"released"} else if self.pending.is_some()||self.error.is_some() {"failed-owned"} else if present {"applied"} else {"idle"}.into(),
|
|
133
|
+
present,applied:self.applied.clone(),pending:self.pending.clone(),error:self.error.map(|v|v.code().into())}
|
|
134
|
+
}
|
|
135
|
+
fn release(&mut self, expected: Applied, deadline: std::time::Instant) -> Result<()> {
|
|
136
|
+
self.validate_receipt(&expected)?;
|
|
137
|
+
if self.pending.is_some() || self.applied.as_ref().is_some_and(|v|v!=&expected) {return Err(Error::Conflict);}
|
|
138
|
+
let result=(|| {
|
|
139
|
+
let current=frontend::Snapshot::read(deadline)?;
|
|
140
|
+
if current.backend!=expected.receipt.backend {return Err(Error::Conflict);}
|
|
141
|
+
if current.verified_matches(&self.options,&[],deadline)? {return Ok(());}
|
|
142
|
+
let rules=expected.prepared.rules(&self.options)?;
|
|
143
|
+
if !current.verified_matches(&self.options,&rules,deadline)? {return Err(Error::Conflict);}
|
|
144
|
+
Self::fence(&expected.prepared)?;
|
|
145
|
+
frontend::replace(&rules,&[],deadline)?;
|
|
146
|
+
self.context()?;
|
|
147
|
+
let after=frontend::Snapshot::read(deadline)?;
|
|
148
|
+
if after.backend!=current.backend || !after.verified_matches(&self.options,&[],deadline)? {return Err(Error::Conflict);}
|
|
149
|
+
Self::fence(&expected.prepared)
|
|
150
|
+
})();
|
|
151
|
+
match result {Ok(())=>{self.applied=Some(expected);self.error=None;self.released=true;Ok(())},Err(error)=>{self.error=Some(error);Err(error)}}
|
|
152
|
+
}
|
|
153
|
+
fn close(&mut self, deadline: std::time::Instant) -> Result<()> {
|
|
154
|
+
if let Some(pending)=self.pending.clone() {
|
|
155
|
+
self.context()?;
|
|
156
|
+
let current=frontend::Snapshot::read(deadline)?;
|
|
157
|
+
if current.backend!=pending.target.backend {return Err(Error::Conflict);}
|
|
158
|
+
let known=if current.verified_matches(&self.options,&pending.target.rules(&self.options)?,deadline)? {
|
|
159
|
+
Some(self.applied(pending.target,current.backend.clone()))
|
|
160
|
+
} else if let Some(previous)=pending.previous {
|
|
161
|
+
if current.backend!=previous.receipt.backend || !current.verified_matches(&self.options,&previous.prepared.rules(&self.options)?,deadline)? {return Err(Error::Conflict);}
|
|
162
|
+
Some(previous)
|
|
163
|
+
} else if current.verified_matches(&self.options,&[],deadline)? {None} else {return Err(Error::Conflict);};
|
|
164
|
+
self.applied=known;self.pending=None;
|
|
165
|
+
}
|
|
166
|
+
if let Some(applied)=self.applied.clone() {self.release(applied,deadline)?;} else {
|
|
167
|
+
self.context()?;
|
|
168
|
+
if !frontend::Snapshot::read(deadline)?.verified_matches(&self.options,&[],deadline)? {return Err(Error::Conflict);}
|
|
169
|
+
self.released=true;self.error=None;
|
|
170
|
+
}
|
|
171
|
+
Ok(())
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
pub fn dispatch(owner: &mut Option<Owner>, request: Request) -> Result<serde_json::Value> {
|
|
175
|
+
let deadline=frontend::deadline();
|
|
176
|
+
match request.method.as_str() {
|
|
177
|
+
"prepareDockerForwarding"=>encode(decode::<policy::Policy>(request.params)?.prepare(frontend::version(deadline)?)?),
|
|
178
|
+
"openDockerForwarding"=>{if owner.is_some(){return Err(Error::Conflict);}*owner=Some(Owner::new(decode(request.params)?)?);encode(owner.as_mut().unwrap().inspect(deadline))},
|
|
179
|
+
"reconcileDockerForwarding"=>encode(owner.as_mut().ok_or(Error::Conflict)?.reconcile(decode(request.params)?,deadline)?),
|
|
180
|
+
"inspectDockerForwarding"|"closeDockerForwarding"=>{
|
|
181
|
+
if decode::<serde_json::Value>(request.params)?!=serde_json::json!({}){return Err(Error::Invalid);}
|
|
182
|
+
let owner=owner.as_mut().ok_or(Error::Conflict)?;
|
|
183
|
+
if request.method=="inspectDockerForwarding" {encode(owner.inspect(deadline))} else {
|
|
184
|
+
if let Err(error)=owner.close(deadline) {owner.error=Some(error);return Err(error);}
|
|
185
|
+
Ok(serde_json::json!({"released":true}))
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"releaseDockerForwarding"=>{owner.as_mut().ok_or(Error::Conflict)?.release(decode(request.params)?,deadline)?;Ok(serde_json::json!({"released":true}))},
|
|
189
|
+
_=>Err(Error::Invalid),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
#[test]
|
|
4
|
+
fn frontend_subprocess_drains_large_input_and_output_with_a_clean_environment() {
|
|
5
|
+
let input="bounded-input\n".repeat(12_000);
|
|
6
|
+
assert_eq!(command("/usr/bin/cat",&[],input.as_bytes(),deadline()).unwrap(),input);
|
|
7
|
+
let environment=command("/usr/bin/env",&[],&[],deadline()).unwrap();
|
|
8
|
+
let values:BTreeSet<_>=environment.lines().collect();
|
|
9
|
+
assert_eq!(values,BTreeSet::from(["PATH=/usr/sbin:/usr/bin:/sbin:/bin","LC_ALL=C"]));
|
|
10
|
+
}
|
|
11
|
+
fn child_failure(kind: &str, source: &str, expected: Error) {
|
|
12
|
+
let directory=std::env::temp_dir().join(format!("smartnftables-command-{}-{kind}",std::process::id()));
|
|
13
|
+
std::fs::create_dir(&directory).unwrap();
|
|
14
|
+
let pidfile=directory.join("pid");
|
|
15
|
+
let source=format!("import os,pathlib,time;pathlib.Path({:?}).write_text(str(os.getpid()));{source}",pidfile.to_str().unwrap());
|
|
16
|
+
assert_eq!(command("/usr/bin/python3",&["-c",&source],&[],deadline()),Err(expected));
|
|
17
|
+
let pid=std::fs::read_to_string(&pidfile).unwrap().parse::<i32>().unwrap();
|
|
18
|
+
assert_eq!(unsafe{libc::kill(pid,0)},-1);
|
|
19
|
+
assert_eq!(std::io::Error::last_os_error().raw_os_error(),Some(libc::ESRCH));
|
|
20
|
+
std::fs::remove_file(pidfile).unwrap();std::fs::remove_dir(directory).unwrap();
|
|
21
|
+
}
|
|
22
|
+
#[test]
|
|
23
|
+
fn frontend_subprocess_output_overflow_stops_and_joins_the_exact_child() {
|
|
24
|
+
child_failure("output","os.write(1,b'x'*2097153);time.sleep(60)",Error::Protocol);
|
|
25
|
+
}
|
|
26
|
+
#[test]
|
|
27
|
+
fn frontend_subprocess_timeout_stops_and_joins_the_exact_child() {
|
|
28
|
+
child_failure("timeout","time.sleep(60)",Error::Unavailable);
|
|
29
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
use serde_json::json;
|
|
3
|
+
|
|
4
|
+
fn options() -> Options {Options {owner_id:"dockerfixture".into(),instance_id:"qualification_20260913".into()}}
|
|
5
|
+
fn policy() -> policy::Policy {
|
|
6
|
+
let prepared:crate::managed::Prepared=serde_json::from_value::<crate::egress::Policy>(crate::egress::tests::host()).unwrap().prepare().unwrap().into();
|
|
7
|
+
policy::Policy {schema_version:1,revision:1,barrier:owner::Applied {
|
|
8
|
+
receipt:owner::Receipt {identity:owner::Identity {owner_id:"barrier".into(),instance_id:"qualification_20260913".into(),table_name:"snft_fixture".into(),boot_id:"00000000-0000-4000-8000-000000000000".into(),namespace_device:"4".into(),namespace_inode:"1".into()},table_handle:"1".into(),revision:1,digest:prepared.digest.clone()},prepared}}
|
|
9
|
+
}
|
|
10
|
+
fn text(rules: &[policy::Rule]) -> String {
|
|
11
|
+
let mut text="*filter\n:INPUT ACCEPT [0:0]\n:FORWARD DROP [0:0]\n:OUTPUT ACCEPT [0:0]\n:DOCKER-USER - [0:0]\n:DOCKER-FORWARD - [0:0]\n-A FORWARD -j DOCKER-USER\n-A FORWARD -j DOCKER-FORWARD\n".to_owned();
|
|
12
|
+
for rule in rules {text.push_str(&format!("-A DOCKER-USER {}\n",rule.args.join(" ")));}
|
|
13
|
+
text.push_str("-A DOCKER-USER -m comment --comment \"foreign unrelated rule\" -j RETURN\nCOMMIT\n");text
|
|
14
|
+
}
|
|
15
|
+
fn snapshot(text: &str) -> frontend::Snapshot {frontend::Snapshot::parse("v1.8.11 (nf_tables)".into(),text).unwrap()}
|
|
16
|
+
|
|
17
|
+
#[test]
|
|
18
|
+
fn docker_compiler_derives_only_scoped_directed_ranges_from_host_barrier() {
|
|
19
|
+
let prepared=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap();let rules=prepared.rules(&options()).unwrap();
|
|
20
|
+
assert_eq!(rules.len(),6);
|
|
21
|
+
assert!(rules.iter().all(|r|r.args.contains(&"--ctorigsrcport".into()) && r.args.contains(&"--ctdir".into()) && !r.args.iter().any(|s|s.contains("RELATED"))));
|
|
22
|
+
let new_tcp=&rules[0].args;
|
|
23
|
+
assert!(new_tcp.windows(3).any(|w|w==["--tcp-flags","FIN,SYN,RST,ACK","SYN"]));
|
|
24
|
+
assert!(rules.iter().all(|r|r.comment.len()<256));
|
|
25
|
+
assert!(rules.iter().all(|r|r.args.windows(2).any(|w|w[0]=="--ctorigsrc" && !w[1].contains('/'))));
|
|
26
|
+
assert!(snapshot(&text(&rules)).matches(&options(),&rules).unwrap());
|
|
27
|
+
}
|
|
28
|
+
#[test]
|
|
29
|
+
fn docker_prepare_rejects_router_receipts_bad_hashes_and_unbounded_restore() {
|
|
30
|
+
let mut value=policy();value.schema_version=2;assert!(value.prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
31
|
+
let mut value=policy();value.barrier.receipt.digest="sha256:wrong".into();assert!(value.prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
32
|
+
let mut value=policy();value.barrier.prepared.policy=crate::managed::Policy::Egress(serde_json::from_value(crate::egress::tests::router()).unwrap());assert!(value.prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
33
|
+
let value=serde_json::to_value(policy()).unwrap();
|
|
34
|
+
let mut extra=value.clone();extra["rules"]=json!([]);assert!(serde_json::from_value::<policy::Policy>(extra).is_err());
|
|
35
|
+
let mut extra=value;extra["barrier"]["receipt"]["tableHandle"]=json!("01");assert!(serde_json::from_value::<policy::Policy>(extra).unwrap().prepare("v1.8.11 (nf_tables)".into()).is_err());
|
|
36
|
+
}
|
|
37
|
+
#[test]
|
|
38
|
+
fn docker_policy_digest_binds_barrier_handle_and_revision() {
|
|
39
|
+
let expected=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap();
|
|
40
|
+
assert_ne!(policy().prepare("v1.8.10 (nf_tables)".into()).unwrap().digest,expected.digest);
|
|
41
|
+
for backend in ["v1.8. (nf_tables)","v1.8.9 (nf_tables)","v1.8.11 (legacy)","v1.8.011 (nf_tables)"] {
|
|
42
|
+
assert!(policy().prepare(backend.into()).is_err());
|
|
43
|
+
}
|
|
44
|
+
let mut value=policy();value.barrier.receipt.table_handle="2".into();assert_ne!(value.prepare("v1.8.11 (nf_tables)".into()).unwrap().digest,expected.digest);
|
|
45
|
+
let mut value=policy();value.revision=2;assert_ne!(value.prepare("v1.8.11 (nf_tables)".into()).unwrap().digest,expected.digest);
|
|
46
|
+
let mut forged=expected;forged.policy.revision=2;assert!(forged.validate().is_err());
|
|
47
|
+
}
|
|
48
|
+
#[test]
|
|
49
|
+
fn docker_snapshot_accepts_only_equivalent_frontend_rendering() {
|
|
50
|
+
let rules=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap();
|
|
51
|
+
let original=text(&rules);
|
|
52
|
+
let rendered=original.replace("--ctorigsrc 10.240.0.2/32","--ctorigsrc 10.240.0.2");
|
|
53
|
+
assert!(snapshot(&rendered).matches(&options(),&rules).unwrap());
|
|
54
|
+
for (before,after) in [("--ctdir ORIGINAL","--ctdir REPLY"),("--ctstate NEW","--ctstate RELATED"),("--sport 10000:10999","--sport 1:65535"),("-o ens18","-o foreign"),("-j ACCEPT","-j RETURN"),("FIN,SYN,RST,ACK SYN","FIN,SYN,RST,ACK ACK")] {
|
|
55
|
+
assert!(!snapshot(&original.replacen(before,after,1)).matches(&options(),&rules).unwrap());
|
|
56
|
+
}
|
|
57
|
+
assert!(snapshot(&original.replacen("-j ACCEPT","-m mark --mark 1 -j ACCEPT",1)).matches(&options(),&rules).is_err());
|
|
58
|
+
}
|
|
59
|
+
#[test]
|
|
60
|
+
fn docker_snapshot_rejects_duplicates_moved_owner_and_foreign_instance() {
|
|
61
|
+
let rules=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap();
|
|
62
|
+
let original=text(&rules);
|
|
63
|
+
let duplicate=original.replace("COMMIT",&format!("-A DOCKER-USER {}\nCOMMIT",rules[0].args.join(" ")));
|
|
64
|
+
assert!(!snapshot(&duplicate).matches(&options(),&rules).unwrap());
|
|
65
|
+
let moved=original.replacen("-A DOCKER-USER", "-A INPUT",1);
|
|
66
|
+
assert!(!snapshot(&moved).matches(&options(),&rules).unwrap());
|
|
67
|
+
assert!(!snapshot(&original.replace("qualification_20260913","foreign_instance_2026")).matches(&options(),&rules).unwrap());
|
|
68
|
+
assert_eq!(snapshot(&original.replace("snftd1:dockerfixture:","snftd1:anotherowner:")).matches(&options(),&[]),Err(Error::ForeignOwner));
|
|
69
|
+
let prepended=original.replacen("-A DOCKER-USER","-A DOCKER-USER -j RETURN\n-A DOCKER-USER",1);
|
|
70
|
+
assert!(!snapshot(&prepended).matches(&options(),&rules).unwrap());
|
|
71
|
+
assert!(!snapshot(&original).matches(&options(),&[]).unwrap());
|
|
72
|
+
}
|
|
73
|
+
#[test]
|
|
74
|
+
fn docker_snapshot_rejects_wrong_backend_path_policy_chain_order_and_table_framing() {
|
|
75
|
+
let original=text(&[]);
|
|
76
|
+
for changed in [original.replace(":FORWARD DROP",":FORWARD ACCEPT"),original.replace("-A FORWARD -j DOCKER-USER","-A FORWARD -j ACCEPT"),original.replace("*filter","*nat"),original.replace("COMMIT","COMMIT\n*filter"),original.replace(":DOCKER-USER - [0:0]\n","")] {
|
|
77
|
+
assert!(frontend::Snapshot::parse("v1.8.11 (nf_tables)".into(),&changed).is_err());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[test]
|
|
82
|
+
fn docker_raw_graph_rejects_early_verdicts_reordered_matches_and_hidden_bytes() {
|
|
83
|
+
let rule=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap().remove(0);
|
|
84
|
+
let expressions=graph::expected(&rule).unwrap();
|
|
85
|
+
let row=|expressions|vec![crate::wire::Attr::string(1,"filter"),crate::wire::Attr::string(2,"DOCKER-USER"),
|
|
86
|
+
crate::wire::Attr::u64(3,42),crate::wire::Attr::nested(4,expressions)];
|
|
87
|
+
assert!(graph::matches_rule(&row(expressions.clone()),&rule).unwrap());
|
|
88
|
+
let mut early=expressions.clone();let verdict=early.pop().unwrap();early.insert(0,verdict);
|
|
89
|
+
assert!(!graph::matches_rule(&row(early),&rule).unwrap());
|
|
90
|
+
let mut reordered=expressions.clone();reordered.swap(0,1);
|
|
91
|
+
assert!(!graph::matches_rule(&row(reordered),&rule).unwrap());
|
|
92
|
+
let mut moved_counter=expressions.clone();let length=moved_counter.len();moved_counter.swap(length-1,length-2);
|
|
93
|
+
assert!(!graph::matches_rule(&row(moved_counter),&rule).unwrap());
|
|
94
|
+
let mut hidden=expressions.clone();
|
|
95
|
+
for expression in &mut hidden {
|
|
96
|
+
let mut fields=crate::wire::attrs(&expression.value).unwrap();
|
|
97
|
+
if crate::wire::text(&fields,1).unwrap()!="match" {continue;}
|
|
98
|
+
let mut data=crate::wire::attrs(&crate::wire::one(&fields,2).unwrap().value).unwrap();
|
|
99
|
+
if crate::wire::text(&data,1).unwrap()!="conntrack" {continue;}
|
|
100
|
+
data.iter_mut().find(|a|a.id()==3).unwrap().value[20..32].fill(0);
|
|
101
|
+
*fields.iter_mut().find(|a|a.id()==2).unwrap()=crate::wire::Attr::nested(2,data);
|
|
102
|
+
expression.value=crate::wire::encode_attrs(&fields);
|
|
103
|
+
}
|
|
104
|
+
assert!(!graph::matches_rule(&row(hidden),&rule).unwrap());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn docker_raw_graph_accepts_kernel_field_order_flags_and_changing_counters_only() {
|
|
109
|
+
let rule=policy().prepare("v1.8.11 (nf_tables)".into()).unwrap().rules(&options()).unwrap().remove(0);
|
|
110
|
+
let mut expressions=graph::expected(&rule).unwrap();
|
|
111
|
+
for expression in &mut expressions {
|
|
112
|
+
let mut fields=crate::wire::attrs(&expression.value).unwrap();
|
|
113
|
+
if crate::wire::text(&fields,1).unwrap()=="counter" {
|
|
114
|
+
*fields.iter_mut().find(|a|a.id()==2).unwrap()=crate::wire::Attr::nested(2,
|
|
115
|
+
vec![crate::wire::Attr::u64(2,123),crate::wire::Attr::u64(1,456)]);
|
|
116
|
+
}
|
|
117
|
+
fields.reverse();expression.value=crate::wire::encode_attrs(&fields);
|
|
118
|
+
}
|
|
119
|
+
fn kernel(attributes: &mut [crate::wire::Attr]) {
|
|
120
|
+
for attribute in attributes {
|
|
121
|
+
if attribute.kind&0x8000!=0 {
|
|
122
|
+
let mut children=crate::wire::attrs(&attribute.value).unwrap();kernel(&mut children);
|
|
123
|
+
attribute.value=crate::wire::encode_attrs(&children);
|
|
124
|
+
}
|
|
125
|
+
attribute.kind &= 0x3fff;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
kernel(&mut expressions);
|
|
129
|
+
let row=vec![crate::wire::Attr::string(1,"filter"),crate::wire::Attr::string(2,"DOCKER-USER"),
|
|
130
|
+
crate::wire::Attr::u64(3,42),crate::wire::Attr::nested(4,expressions)];
|
|
131
|
+
assert!(graph::matches_rule(&row,&rule).unwrap());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn docker_forward_jumps_require_exact_unconditional_order() {
|
|
136
|
+
use crate::{policy::{expr,verdict},wire::Attr};
|
|
137
|
+
let row=|expressions|vec![Attr::string(1,"filter"),Attr::string(2,"FORWARD"),
|
|
138
|
+
Attr::u64(3,42),Attr::nested(4,expressions)];
|
|
139
|
+
for target in ["DOCKER-USER","DOCKER-FORWARD"] {
|
|
140
|
+
let expected=vec![expr("counter",vec![Attr::u64(1,7),Attr::u64(2,11)]),verdict(-3,Some(target))];
|
|
141
|
+
assert!(graph::matches_forward(&row(expected.clone()),target).unwrap());
|
|
142
|
+
let mut early=expected.clone();early.insert(0,verdict(1,None));
|
|
143
|
+
assert!(!graph::matches_forward(&row(early),target).unwrap());
|
|
144
|
+
let mut reordered=expected.clone();reordered.reverse();
|
|
145
|
+
assert!(!graph::matches_forward(&row(reordered),target).unwrap());
|
|
146
|
+
assert!(!graph::matches_forward(&row(expected),if target=="DOCKER-USER" {"DOCKER-FORWARD"} else {"DOCKER-USER"}).unwrap());
|
|
147
|
+
}
|
|
148
|
+
}
|