@push.rocks/smartnftables 1.5.0 → 1.7.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 +18 -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 +2 -0
- package/dist_ts/classes.managednftables.js +2 -2
- package/dist_ts/index.d.ts +2 -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 +9 -1
- package/package.json +1 -1
- package/readme.md +100 -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 +3 -0
- package/rust/src/egress.poolguard.rs +30 -0
- package/rust/src/egress.rs +27 -0
- package/rust/src/egress_tests.rs +85 -0
- package/rust/src/main.rs +10 -5
- package/rust/src/managed.rs +4 -0
- package/rust/src/owner.rs +19 -1
- package/rust/src/owner_egress_tests.rs +1 -1
- package/rust/src/owner_egress_traffic_tests.rs +21 -10
- package/rust/src/owner_host_traffic_tests.rs +4 -10
- package/rust/src/owner_poolguard_tests.rs +265 -0
- package/rust/src/owner_tests.rs +3 -0
- package/rust/src/wire.links.rs +11 -0
- package/rust/src/wire.rs +14 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.manageddockerforwarding.ts +113 -0
- package/ts/classes.managednftables.ts +1 -1
- package/ts/index.ts +2 -0
- package/ts/managed.docker.types.ts +49 -0
- package/ts/managed.egress.types.ts +10 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
use super::policy::{Options, Rule};
|
|
2
|
+
use crate::{Error, Result};
|
|
3
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
4
|
+
use std::io::{Read, Write};
|
|
5
|
+
use std::os::fd::AsRawFd;
|
|
6
|
+
use std::os::unix::{fs::MetadataExt, process::CommandExt};
|
|
7
|
+
use std::process::{Command, Stdio};
|
|
8
|
+
use std::time::{Duration, Instant};
|
|
9
|
+
|
|
10
|
+
const IPTABLES: &str = "/usr/sbin/iptables-nft";
|
|
11
|
+
const SAVE: &str = "/usr/sbin/iptables-nft-save";
|
|
12
|
+
const RESTORE: &str = "/usr/sbin/iptables-nft-restore";
|
|
13
|
+
const MAX_OUTPUT: usize = 2_097_152;
|
|
14
|
+
#[cfg(test)]
|
|
15
|
+
#[path="docker_process_tests.rs"]
|
|
16
|
+
mod process_tests;
|
|
17
|
+
|
|
18
|
+
fn nonblocking(fd: i32) -> Result<()> {
|
|
19
|
+
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
|
|
20
|
+
if flags < 0 || unsafe { libc::fcntl(fd,libc::F_SETFL,flags|libc::O_NONBLOCK) } < 0 {return Err(Error::Unavailable);}
|
|
21
|
+
Ok(())
|
|
22
|
+
}
|
|
23
|
+
pub fn deadline() -> Instant { Instant::now()+Duration::from_secs(8) }
|
|
24
|
+
fn command(path: &str, args: &[&str], input: &[u8], deadline: Instant) -> Result<String> {
|
|
25
|
+
if Instant::now()>=deadline {return Err(Error::Unavailable);}
|
|
26
|
+
let metadata=std::fs::metadata(path).map_err(|_|Error::Unavailable)?;
|
|
27
|
+
if !metadata.is_file() || metadata.uid()!=0 || metadata.mode() & 0o022 != 0 { return Err(Error::Permission); }
|
|
28
|
+
let mut child=Command::new(path).args(args).env_clear().env("PATH","/usr/sbin:/usr/bin:/sbin:/bin")
|
|
29
|
+
.env("LC_ALL","C").stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).process_group(0)
|
|
30
|
+
.spawn().map_err(|_|Error::Unavailable)?;
|
|
31
|
+
let result=(|| {
|
|
32
|
+
let mut stdin=child.stdin.take();
|
|
33
|
+
let mut stdout=child.stdout.take().ok_or(Error::Unavailable)?;
|
|
34
|
+
let mut stderr=child.stderr.take().ok_or(Error::Unavailable)?;
|
|
35
|
+
for fd in [stdin.as_ref().ok_or(Error::Unavailable)?.as_raw_fd(),stdout.as_raw_fd(),stderr.as_raw_fd()] {nonblocking(fd)?;}
|
|
36
|
+
let mut written=0;let mut output=Vec::new();let mut errors=Vec::new();
|
|
37
|
+
let mut out_eof=false;let mut err_eof=false;let mut status=None;
|
|
38
|
+
loop {
|
|
39
|
+
if Instant::now()>=deadline {return Err(Error::Unavailable);}
|
|
40
|
+
if written==input.len() {stdin=None;}
|
|
41
|
+
if let Some(pipe)=stdin.as_mut() {
|
|
42
|
+
match pipe.write(&input[written..]) {
|
|
43
|
+
Ok(0)=>return Err(Error::Unavailable),Ok(count)=>written+=count,
|
|
44
|
+
Err(error) if error.kind()==std::io::ErrorKind::WouldBlock=>{},
|
|
45
|
+
Err(_)=>return Err(Error::Unavailable),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (pipe,bytes,eof) in [(&mut stdout as &mut dyn Read,&mut output,&mut out_eof),(&mut stderr as &mut dyn Read,&mut errors,&mut err_eof)] {
|
|
49
|
+
let mut buffer=[0;8192];
|
|
50
|
+
loop {
|
|
51
|
+
match pipe.read(&mut buffer) {
|
|
52
|
+
Ok(0)=>{*eof=true;break;},
|
|
53
|
+
Ok(count)=>{if bytes.len()+count>MAX_OUTPUT{return Err(Error::Protocol);}bytes.extend_from_slice(&buffer[..count]);},
|
|
54
|
+
Err(error) if error.kind()==std::io::ErrorKind::WouldBlock=>break,
|
|
55
|
+
Err(_)=>return Err(Error::Unavailable),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if status.is_none() {status=child.try_wait().map_err(|_|Error::Unavailable)?;}
|
|
60
|
+
if let Some(status)=status.filter(|_|out_eof&&err_eof) {
|
|
61
|
+
if !status.success() || written!=input.len() {
|
|
62
|
+
if path==IPTABLES && args.get(2)==Some(&"-C") && status.code()==Some(1)
|
|
63
|
+
&& errors==b"iptables: Bad rule (does a matching rule exist in that chain?).\n" {
|
|
64
|
+
return Err(Error::Conflict);
|
|
65
|
+
}
|
|
66
|
+
return Err(Error::Unavailable);
|
|
67
|
+
}
|
|
68
|
+
return String::from_utf8(output).map_err(|_|Error::Protocol);
|
|
69
|
+
}
|
|
70
|
+
std::thread::sleep(Duration::from_millis(2));
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
if result.is_err() {
|
|
74
|
+
// Stop and join this exact mutator before reporting an inconclusive ACK.
|
|
75
|
+
if child.try_wait().ok().flatten().is_none() {
|
|
76
|
+
unsafe {libc::kill(-(child.id() as i32),libc::SIGKILL);}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
child.wait().map_err(|_|Error::Unavailable)?;
|
|
80
|
+
result
|
|
81
|
+
}
|
|
82
|
+
pub fn version(deadline: Instant) -> Result<String> {
|
|
83
|
+
let mut version=None;
|
|
84
|
+
for path in [IPTABLES,SAVE,RESTORE] {
|
|
85
|
+
let output=command(path,&["--version"],&[],deadline)?;
|
|
86
|
+
let parts:Vec<_>=output.split_whitespace().collect();
|
|
87
|
+
if parts.len()!=3 {return Err(Error::UnsupportedBackend);}
|
|
88
|
+
let observed=format!("{} {}",parts[1],parts[2]);
|
|
89
|
+
super::policy::validate_backend(&observed)?;
|
|
90
|
+
if version.as_ref().is_some_and(|v|v!=&observed) {return Err(Error::Conflict);}
|
|
91
|
+
version=Some(observed);
|
|
92
|
+
}
|
|
93
|
+
version.ok_or(Error::Unavailable)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Tokenize save output only; no token is ever evaluated by a shell.
|
|
97
|
+
fn tokens(line: &str) -> Result<Vec<String>> {
|
|
98
|
+
if line.len()>16_384 {return Err(Error::Protocol);}
|
|
99
|
+
let mut values=Vec::new();let mut word=String::new();let mut quoted=false;let mut escaped=false;let mut active=false;
|
|
100
|
+
for byte in line.bytes() {
|
|
101
|
+
if !(32..127).contains(&byte) {return Err(Error::Protocol);}
|
|
102
|
+
if escaped {word.push(byte as char);escaped=false;active=true;continue;}
|
|
103
|
+
if byte==b'\\' {escaped=true;continue;}
|
|
104
|
+
if byte==b'"' {quoted=!quoted;active=true;continue;}
|
|
105
|
+
if byte==b' ' && !quoted {if active {values.push(std::mem::take(&mut word));active=false;}} else {word.push(byte as char);active=true;}
|
|
106
|
+
}
|
|
107
|
+
if quoted||escaped {return Err(Error::Protocol);}
|
|
108
|
+
if active {values.push(word);}
|
|
109
|
+
if values.len()>512 {return Err(Error::Protocol);}
|
|
110
|
+
Ok(values)
|
|
111
|
+
}
|
|
112
|
+
fn normalized(args: &[String]) -> Result<Vec<(String, Vec<String>)>> {
|
|
113
|
+
let mut values=BTreeMap::new();let mut modules=BTreeSet::new();let mut index=0;
|
|
114
|
+
while index<args.len() {
|
|
115
|
+
let key=&args[index];let count=if key=="--tcp-flags" {2} else {1};
|
|
116
|
+
if !["-i","-o","-s","-d","-p","-m","--sport","--dport","--tcp-flags","--ctstate","--ctproto","--ctorigsrc","--ctorigsrcport","--ctdir","--comment","-j"].contains(&key.as_str()) {return Err(Error::Conflict);}
|
|
117
|
+
let mut value=args.get(index+1..index+1+count).ok_or(Error::Conflict)?.to_vec();index+=1+count;
|
|
118
|
+
if key=="-m" {if !modules.insert(value[0].clone()) {return Err(Error::Conflict);}continue;}
|
|
119
|
+
if key=="--ctorigsrc" && !value[0].contains('/') {value[0].push_str("/32");}
|
|
120
|
+
if key=="--ctproto" {value[0]=match value[0].as_str() {"tcp"=>"6".into(),"udp"=>"17".into(),_=>value[0].clone()};}
|
|
121
|
+
if values.insert(key.clone(),value).is_some() {return Err(Error::Conflict);}
|
|
122
|
+
}
|
|
123
|
+
values.insert("-m".into(),modules.into_iter().collect());
|
|
124
|
+
Ok(values.into_iter().collect())
|
|
125
|
+
}
|
|
126
|
+
pub struct Snapshot {
|
|
127
|
+
pub backend: String,
|
|
128
|
+
pub rows: Vec<Vec<String>>,
|
|
129
|
+
pub user: Vec<Vec<String>>,
|
|
130
|
+
raw: Option<super::graph::Snapshot>,
|
|
131
|
+
}
|
|
132
|
+
impl Snapshot {
|
|
133
|
+
pub fn read(deadline: Instant) -> Result<Self> {
|
|
134
|
+
let backend=version(deadline)?;
|
|
135
|
+
let (raw,text)=super::graph::Snapshot::read(||command(SAVE,&["-t","filter"],&[],deadline))?;
|
|
136
|
+
let mut snapshot=Self::parse(backend,&text)?;
|
|
137
|
+
snapshot.raw=Some(raw);
|
|
138
|
+
Ok(snapshot)
|
|
139
|
+
}
|
|
140
|
+
pub fn parse(backend: String, text: &str) -> Result<Self> {
|
|
141
|
+
if text.len()>MAX_OUTPUT {return Err(Error::Protocol);}
|
|
142
|
+
let mut table=false;let mut committed=false;let mut chains=BTreeMap::new();let mut rows=Vec::new();
|
|
143
|
+
for line in text.lines().filter(|line|!line.starts_with('#')&&!line.is_empty()) {
|
|
144
|
+
if line=="*filter" {if table||committed {return Err(Error::Conflict);}table=true;continue;}
|
|
145
|
+
if line=="COMMIT" {if !table||committed {return Err(Error::Conflict);}committed=true;continue;}
|
|
146
|
+
if !table||committed {return Err(Error::Conflict);}
|
|
147
|
+
let row=tokens(line)?;
|
|
148
|
+
if line.starts_with(':') {
|
|
149
|
+
if row.len()!=3 || chains.insert(row[0][1..].to_string(),row[1].clone()).is_some() {return Err(Error::Conflict);}
|
|
150
|
+
} else {
|
|
151
|
+
if row.len()<2 || row[0]!="-A" || rows.len()>=4096 {return Err(Error::Conflict);}
|
|
152
|
+
rows.push(row);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if !committed || chains.get("FORWARD").map(String::as_str)!=Some("DROP")
|
|
156
|
+
|| chains.get("DOCKER-USER").map(String::as_str)!=Some("-")
|
|
157
|
+
|| chains.get("DOCKER-FORWARD").map(String::as_str)!=Some("-") {return Err(Error::Conflict);}
|
|
158
|
+
let forward:Vec<_>=rows.iter().filter(|r|r[1]=="FORWARD").collect();
|
|
159
|
+
for (index,target) in ["DOCKER-USER","DOCKER-FORWARD"].iter().enumerate() {
|
|
160
|
+
if forward.get(index).map(|r|r.iter().map(String::as_str).collect::<Vec<_>>())!=Some(vec!["-A","FORWARD","-j",target]) {return Err(Error::Conflict);}
|
|
161
|
+
}
|
|
162
|
+
let user=rows.iter().filter(|r|r[1]=="DOCKER-USER").cloned().collect();
|
|
163
|
+
Ok(Self {backend,rows,user,raw:None})
|
|
164
|
+
}
|
|
165
|
+
/// Text proves placement/count and logical policy; -C additionally checks
|
|
166
|
+
/// match-extension bytes hidden by save output. Deletion uses the same spec.
|
|
167
|
+
pub fn verified_matches(&self, options: &Options, expected: &[Rule], deadline: Instant) -> Result<bool> {
|
|
168
|
+
if !self.matches(options,expected)? {return Ok(false);}
|
|
169
|
+
let raw=self.raw.as_ref().ok_or(Error::Conflict)?;
|
|
170
|
+
if !raw.matches(expected)? {return Ok(false);}
|
|
171
|
+
for rule in expected {
|
|
172
|
+
let mut args=vec!["-t","filter","-C","DOCKER-USER"];
|
|
173
|
+
args.extend(rule.args.iter().map(String::as_str));
|
|
174
|
+
match command(IPTABLES,&args,&[],deadline) {
|
|
175
|
+
Ok(_)=>{},Err(Error::Conflict)=>return Ok(false),Err(error)=>return Err(error),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
raw.unchanged()
|
|
179
|
+
}
|
|
180
|
+
pub fn matches(&self, options: &Options, expected: &[Rule]) -> Result<bool> {
|
|
181
|
+
let prefix=format!("snftd1:{}:",options.owner_id);
|
|
182
|
+
// One node-level contribution owns the leading block. Do not prepend
|
|
183
|
+
// another managed owner and silently invalidate its placement proof.
|
|
184
|
+
if self.rows.iter().any(|r|r.iter().any(|t|t.starts_with("snftd1:")&&!t.starts_with(&prefix))) {return Err(Error::ForeignOwner);}
|
|
185
|
+
let owned:Vec<_>=self.rows.iter().filter(|r|r.iter().any(|t|t.starts_with(&prefix))).collect();
|
|
186
|
+
if owned.len()!=expected.len() || self.user.len()<expected.len() {return Ok(false);}
|
|
187
|
+
for (index,rule) in expected.iter().enumerate() {
|
|
188
|
+
let row=&self.user[index];
|
|
189
|
+
if row[1]!="DOCKER-USER" || !row.iter().any(|t|t==&rule.comment)
|
|
190
|
+
|| normalized(&row[2..])?!=normalized(&rule.args)? {return Ok(false);}
|
|
191
|
+
}
|
|
192
|
+
Ok(true)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
pub fn replace(previous: &[Rule], target: &[Rule], deadline: Instant) -> Result<()> {
|
|
196
|
+
let mut batch="*filter\n".to_string();
|
|
197
|
+
for rule in previous {batch.push_str(&format!("-D DOCKER-USER {}\n",rule.args.join(" ")));}
|
|
198
|
+
for rule in target.iter().rev() {batch.push_str(&format!("-I DOCKER-USER 1 {}\n",rule.args.join(" ")));}
|
|
199
|
+
batch.push_str("COMMIT\n");
|
|
200
|
+
if batch.len()>210_000 {return Err(Error::Invalid);}
|
|
201
|
+
command(RESTORE,&["--noflush","--wait","5"],batch.as_bytes(),deadline)?;
|
|
202
|
+
Ok(())
|
|
203
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//! Ordered iptables-nft graph identity. Command-state reconstruction loses order.
|
|
2
|
+
use super::policy::Rule;
|
|
3
|
+
use crate::{policy::{compare, expr, meta, verdict}, wire::{self, Attr, Socket}, Error, Result};
|
|
4
|
+
|
|
5
|
+
fn value<'a>(rule: &'a Rule, key: &str) -> Result<&'a str> {
|
|
6
|
+
rule.args.windows(2).find(|pair|pair[0]==key).map(|pair|pair[1].as_str()).ok_or(Error::Invalid)
|
|
7
|
+
}
|
|
8
|
+
fn payload(base: u32, offset: u32, length: u32) -> Attr {
|
|
9
|
+
expr("payload",vec![Attr::u32(1,1),Attr::u32(2,base),Attr::u32(3,offset),Attr::u32(4,length)])
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// Fixed Linux UAPI / xtables 1.8.10 and 1.8.11 constructors, not a raw rule API.
|
|
13
|
+
pub(super) fn expected(rule: &Rule) -> Result<Vec<Attr>> {
|
|
14
|
+
let reply=value(rule,"--ctdir")?=="REPLY";
|
|
15
|
+
let new=value(rule,"--ctstate")?=="NEW";
|
|
16
|
+
let protocol=match value(rule,"-p")? {"tcp"=>6_u16,"udp"=>17,_=>return Err(Error::Invalid)};
|
|
17
|
+
let address=value(rule,"--ctorigsrc")?.parse::<std::net::Ipv4Addr>().map_err(|_|Error::Invalid)?.octets();
|
|
18
|
+
let ports=value(rule,"--ctorigsrcport")?;
|
|
19
|
+
let (first,last)=ports.split_once(':').unwrap_or((ports,ports));
|
|
20
|
+
let first=first.parse::<u16>().map_err(|_|Error::Invalid)?;
|
|
21
|
+
let last=last.parse::<u16>().map_err(|_|Error::Invalid)?;
|
|
22
|
+
let mut result=vec![payload(1,if reply {16} else {12},4),compare(address.to_vec())];
|
|
23
|
+
for (key,argument) in [(6,"-i"),(7,"-o")] {
|
|
24
|
+
result.extend(meta(key,[value(rule,argument)?.as_bytes(),&[0]].concat()));
|
|
25
|
+
}
|
|
26
|
+
result.extend([payload(1,9,1),compare(vec![protocol as u8])]);
|
|
27
|
+
if new && protocol==6 {
|
|
28
|
+
result.extend([payload(2,13,1),expr("bitwise",vec![
|
|
29
|
+
Attr::u32(1,1),Attr::u32(2,1),Attr::u32(3,1),Attr::u32(6,0),
|
|
30
|
+
Attr::nested(4,vec![Attr::bytes(1,vec![0x17])]),Attr::nested(5,vec![Attr::bytes(1,vec![0])]),
|
|
31
|
+
]),compare(vec![2])]);
|
|
32
|
+
}
|
|
33
|
+
result.push(payload(2,if reply {2} else {0},2));
|
|
34
|
+
result.push(if first==last {compare(first.to_be_bytes().to_vec())} else {
|
|
35
|
+
expr("range",vec![Attr::u32(1,1),Attr::u32(2,0),
|
|
36
|
+
Attr::nested(3,vec![Attr::bytes(1,first.to_be_bytes().to_vec())]),
|
|
37
|
+
Attr::nested(4,vec![Attr::bytes(1,last.to_be_bytes().to_vec())])])
|
|
38
|
+
});
|
|
39
|
+
// xt_conntrack_mtinfo3 is 164 bytes, XT_ALIGN'ed to 168 on both shipped
|
|
40
|
+
// 64-bit Linux targets. All fields and padding are initialized explicitly.
|
|
41
|
+
let mut conntrack=vec![0;168];
|
|
42
|
+
conntrack[..4].copy_from_slice(&address);
|
|
43
|
+
conntrack[16..32].fill(0xff); // canonical bare-host nf_inet_addr mask
|
|
44
|
+
for (offset,number) in [(136,protocol),(138,first),(146,0x1107),
|
|
45
|
+
(148,if reply {0x1000} else {0}),(150,if new {8} else {2}),(154,last)] {
|
|
46
|
+
conntrack[offset..offset+2].copy_from_slice(&number.to_ne_bytes());
|
|
47
|
+
}
|
|
48
|
+
result.push(expr("match",vec![Attr::string(1,"conntrack"),Attr::u32(2,3),Attr::bytes(3,conntrack)]));
|
|
49
|
+
let mut comment=vec![0;256];
|
|
50
|
+
if rule.comment.len()>=comment.len() {return Err(Error::Invalid);}
|
|
51
|
+
comment[..rule.comment.len()].copy_from_slice(rule.comment.as_bytes());
|
|
52
|
+
result.push(expr("match",vec![Attr::string(1,"comment"),Attr::u32(2,0),Attr::bytes(3,comment)]));
|
|
53
|
+
result.push(expr("counter",vec![Attr::u64(1,0),Attr::u64(2,0)]));
|
|
54
|
+
result.push(verdict(1,None));
|
|
55
|
+
Ok(result)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Kernel dumps may omit nested flags and reorder fields; list order is exact.
|
|
59
|
+
fn equivalent(actual: &[Attr], expected: &[Attr], ordered: bool) -> Result<bool> {
|
|
60
|
+
if actual.len()!=expected.len() {return Ok(false);}
|
|
61
|
+
let mut actual=actual.iter().collect::<Vec<_>>();let mut expected=expected.iter().collect::<Vec<_>>();
|
|
62
|
+
if !ordered {actual.sort_by_key(|a|a.id());expected.sort_by_key(|a|a.id());}
|
|
63
|
+
for (actual,expected) in actual.into_iter().zip(expected) {
|
|
64
|
+
if actual.id()!=expected.id() {return Ok(false);}
|
|
65
|
+
if expected.kind & 0x8000 != 0 {
|
|
66
|
+
let expected=wire::attrs(&expected.value)?;
|
|
67
|
+
if !equivalent(&wire::attrs(&actual.value)?,&expected,expected.iter().all(|a|a.id()==1))? {return Ok(false);}
|
|
68
|
+
} else if actual.value!=expected.value {return Ok(false);}
|
|
69
|
+
}
|
|
70
|
+
Ok(true)
|
|
71
|
+
}
|
|
72
|
+
pub(super) fn matches_rule(row: &[Attr], rule: &Rule) -> Result<bool> {
|
|
73
|
+
matches_graph(row,"DOCKER-USER",&expected(rule)?)
|
|
74
|
+
}
|
|
75
|
+
pub(super) fn matches_forward(row: &[Attr], target: &str) -> Result<bool> {
|
|
76
|
+
if !["DOCKER-USER","DOCKER-FORWARD"].contains(&target) {return Err(Error::Invalid);}
|
|
77
|
+
matches_graph(row,"FORWARD",&[expr("counter",vec![Attr::u64(1,0),Attr::u64(2,0)]),verdict(-3,Some(target))])
|
|
78
|
+
}
|
|
79
|
+
fn matches_graph(row: &[Attr], chain: &str, expected: &[Attr]) -> Result<bool> {
|
|
80
|
+
if row.iter().any(|a|![1,2,3,4,6,8].contains(&a.id()))
|
|
81
|
+
|| wire::text(row,1)?!="filter" || wire::text(row,2)?!=chain
|
|
82
|
+
|| wire::handle(row,3)?==0 {return Ok(false);}
|
|
83
|
+
for id in [6,8] {
|
|
84
|
+
if row.iter().any(|a|a.id()==id) {
|
|
85
|
+
let attr=wire::one(row,id)?;
|
|
86
|
+
if id==6 {wire::handle(row,id)?;} else if !attr.value.is_empty() {return Ok(false);}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let mut expressions=wire::attrs(&wire::one(row,4)?.value)?;
|
|
90
|
+
for expression in &mut expressions {
|
|
91
|
+
let mut fields=wire::attrs(&expression.value)?;
|
|
92
|
+
if wire::text(&fields,1)?=="counter" {
|
|
93
|
+
let mut data=wire::attrs(&wire::one(&fields,2)?.value)?;
|
|
94
|
+
// Counter values change without a ruleset generation change. No
|
|
95
|
+
// expression, including the counter itself, may move or disappear.
|
|
96
|
+
data.retain(|a|!(a.id()==3 && a.value.is_empty()));
|
|
97
|
+
if data.len()!=2 {return Ok(false);}
|
|
98
|
+
for id in [1,2] {wire::handle(&data,id)?;}
|
|
99
|
+
for attribute in &mut data {attribute.value=0_u64.to_be_bytes().to_vec();}
|
|
100
|
+
let field=fields.iter_mut().find(|a|a.id()==2).ok_or(Error::Protocol)?;
|
|
101
|
+
*field=Attr::nested(2,data);
|
|
102
|
+
expression.value=wire::encode_attrs(&fields);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
equivalent(&expressions,expected,true)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub struct Snapshot { generation: u32, rows: Vec<Vec<Attr>> }
|
|
109
|
+
impl Snapshot {
|
|
110
|
+
pub fn read(save: impl FnOnce()->Result<String>) -> Result<(Self,String)> {
|
|
111
|
+
let mut socket=Socket::open()?;let generation=socket.generation()?;
|
|
112
|
+
let text=save()?;
|
|
113
|
+
let rows=socket.query_ipv4(7,vec![Attr::string(1,"filter"),Attr::string(2,"DOCKER-USER")],true)?;
|
|
114
|
+
let forward=socket.query_ipv4(7,vec![Attr::string(1,"filter"),Attr::string(2,"FORWARD")],true)?;
|
|
115
|
+
if socket.generation()!=Ok(generation) {return Err(Error::Conflict);}
|
|
116
|
+
for (index,target) in ["DOCKER-USER","DOCKER-FORWARD"].iter().enumerate() {
|
|
117
|
+
if !matches_forward(forward.get(index).ok_or(Error::Conflict)?,target)? {return Err(Error::Conflict);}
|
|
118
|
+
}
|
|
119
|
+
Ok((Self {generation,rows},text))
|
|
120
|
+
}
|
|
121
|
+
pub fn matches(&self, expected: &[Rule]) -> Result<bool> {
|
|
122
|
+
if self.rows.len()<expected.len() {return Ok(false);}
|
|
123
|
+
for (row,rule) in self.rows.iter().zip(expected) {
|
|
124
|
+
if !matches_rule(row,rule)? {return Ok(false);}
|
|
125
|
+
}
|
|
126
|
+
Ok(true)
|
|
127
|
+
}
|
|
128
|
+
pub fn unchanged(&self) -> Result<bool> {Ok(Socket::open()?.generation()?==self.generation)}
|
|
129
|
+
}
|
|
@@ -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
|
+
}
|