buntrail_services/connections/
container_identity.rs1use buntrail_core::contracts::*;
3use sha2::{Digest, Sha256};
4
5#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
6pub enum ContainerState {
7 Running,
8 Stopped,
9 Paused,
10 Unsupported,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14pub struct ContainerDescriptor {
15 pub engine_endpoint: Vec<u8>,
16 pub immutable_id: String,
17 pub name: String,
18 pub state: ContainerState,
19 pub configured_user: String,
20 pub requested_user: Option<String>,
21 pub actual_uid: Option<u32>,
22 pub architecture: Option<String>,
23 pub start_generation: Option<Vec<u8>>,
24}
25
26fn invalid(message: &str) -> CoreError {
27 CoreError::new(ErrorCode::InvalidArgument, message)
28}
29fn valid_start(start: Option<&[u8]>) -> bool {
30 start.is_some_and(|value| {
31 !value.is_empty() && value.len() <= 256 && !value.starts_with(b"0001-")
32 })
33}
34#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct ContainerBinding {
37 pub host_security_binding: Vec<u8>,
38 pub container: ContainerIdentity,
39 pub uid: u32,
40 pub start_generation: Option<Vec<u8>>,
41}
42impl ContainerBinding {
43 pub fn new(
44 descriptor: &ContainerDescriptor,
45 host_security_binding: Vec<u8>,
46 uid: u32,
47 ) -> Result<Self, CoreError> {
48 if descriptor.state != ContainerState::Running
49 || !valid_start(descriptor.start_generation.as_deref())
50 {
51 return Err(CoreError::new(
52 ErrorCode::TargetChanged,
53 "A verified running container start is required before helper startup",
54 ));
55 }
56 descriptor.target(&host_security_binding, uid)?;
57 Ok(Self {
58 host_security_binding,
59 container: ContainerIdentity {
60 engine_endpoint: descriptor.engine_endpoint.clone(),
61 immutable_id: descriptor.immutable_id.as_bytes().to_vec(),
62 },
63 uid,
64 start_generation: descriptor.start_generation.clone(),
65 })
66 }
67 pub fn target_for_uid(&self, uid: u32) -> Result<TargetIdentity, CoreError> {
68 if uid != self.uid
69 || self.container.immutable_id.len() != 64
70 || !self
71 .container
72 .immutable_id
73 .iter()
74 .all(u8::is_ascii_hexdigit)
75 || self.container.engine_endpoint.is_empty()
76 || self.container.engine_endpoint.len() > 65536
77 || !valid_start(self.start_generation.as_deref())
78 {
79 return Err(CoreError::new(
80 ErrorCode::TargetChanged,
81 "Container user or binding changed at helper startup",
82 ));
83 }
84 let d = ContainerDescriptor {
85 engine_endpoint: self.container.engine_endpoint.clone(),
86 immutable_id: String::from_utf8(self.container.immutable_id.clone())
87 .map_err(|_| invalid("Invalid container ID"))?,
88 name: String::new(),
89 state: ContainerState::Running,
90 configured_user: String::new(),
91 requested_user: None,
92 actual_uid: Some(uid),
93 architecture: None,
94 start_generation: self.start_generation.clone(),
95 };
96 d.target(&self.host_security_binding, uid)
97 }
98 pub fn current_target(&self) -> Result<TargetIdentity, CoreError> {
99 if !cfg!(target_os = "linux") {
100 return Err(CoreError::new(
101 ErrorCode::Unavailable,
102 "Container helper requires supported Linux",
103 ));
104 }
105 let root = std::fs::metadata("/").map_err(crate::files::io_error)?;
106 if !root.is_dir() {
107 return Err(invalid("Container filesystem root is unavailable"));
108 }
109 self.target_for_uid(unsafe { libc::geteuid() })
110 }
111}
112impl ContainerDescriptor {
113 pub fn decode(
114 endpoint: Vec<u8>,
115 bytes: &[u8],
116 requested_user: Option<String>,
117 ) -> Result<Self, CoreError> {
118 if endpoint.is_empty() || endpoint.len() > 65536 || bytes.len() > 65536 {
119 return Err(invalid("Container metadata exceeded its bound"));
120 }
121 let v: serde_json::Value =
122 serde_json::from_slice(bytes).map_err(|_| invalid("Malformed container metadata"))?;
123 let id = v
124 .get("Id")
125 .or_else(|| v.get("ID"))
126 .and_then(|v| v.as_str())
127 .ok_or_else(|| invalid("Container ID is missing"))?;
128 if id.len() != 64 || !id.bytes().all(|b| b.is_ascii_hexdigit()) {
129 return Err(invalid("A full immutable container ID is required"));
130 }
131 let name = v
132 .get("Name")
133 .and_then(|v| v.as_str())
134 .ok_or_else(|| invalid("Container name is missing"))?;
135 if name.len() > 4096 || name.chars().any(char::is_control) {
136 return Err(invalid("Invalid container name"));
137 }
138 let state = match v.pointer("/State/Status").and_then(|v| v.as_str()) {
139 Some("running")
140 if v.pointer("/State/Running").and_then(|v| v.as_bool()) == Some(true) =>
141 {
142 ContainerState::Running
143 }
144 Some("created" | "configured" | "exited" | "stopped") => ContainerState::Stopped,
145 Some("paused") => ContainerState::Paused,
146 _ => ContainerState::Unsupported,
147 };
148 let configured_user = v
149 .pointer("/Config/User")
150 .and_then(|v| v.as_str())
151 .unwrap_or("")
152 .to_owned();
153 for user in [Some(configured_user.as_str()), requested_user.as_deref()]
154 .into_iter()
155 .flatten()
156 {
157 if user.len() > 4096 || user.chars().any(char::is_control) || user.starts_with('-') {
158 return Err(invalid("Invalid container user"));
159 }
160 }
161 let start_generation = v
162 .pointer("/State/StartedAt")
163 .and_then(|v| v.as_str())
164 .filter(|s| !s.is_empty() && !s.starts_with("0001-"))
165 .map(|s| s.as_bytes().to_vec());
166 if start_generation.as_ref().is_some_and(|s| s.len() > 256) {
167 return Err(invalid("Invalid container start generation"));
168 }
169 if state == ContainerState::Running && !valid_start(start_generation.as_deref()) {
170 return Err(invalid(
171 "Running container has no verified start generation; refresh and verify the original container before connecting",
172 ));
173 }
174 let architecture = v
175 .get("Architecture")
176 .and_then(|s| s.as_str())
177 .map(str::to_owned);
178 if architecture
179 .as_ref()
180 .is_some_and(|s| s.is_empty() || s.len() > 128 || s.chars().any(char::is_control))
181 {
182 return Err(invalid("Invalid container architecture"));
183 }
184 Ok(Self {
185 engine_endpoint: endpoint,
186 immutable_id: id.to_owned(),
187 name: name.trim_start_matches('/').into(),
188 state,
189 configured_user,
190 requested_user,
191 actual_uid: None,
192 architecture,
193 start_generation,
194 })
195 }
196 pub fn revalidate(&self, current: &Self) -> Result<(), CoreError> {
197 if self.engine_endpoint != current.engine_endpoint
198 || self.immutable_id != current.immutable_id
199 || self.requested_user != current.requested_user
200 || self.configured_user != current.configured_user
201 || self
202 .actual_uid
203 .zip(current.actual_uid)
204 .is_some_and(|(before, now)| before != now)
205 {
206 return Err(CoreError::new(
207 ErrorCode::TargetChanged,
208 "Container endpoint, immutable ID or selected user changed; select and verify the target again",
209 ));
210 }
211 Ok(())
212 }
213 pub fn target(
214 &self,
215 host_security_binding: &[u8],
216 uid: u32,
217 ) -> Result<TargetIdentity, CoreError> {
218 if host_security_binding.is_empty() || host_security_binding.len() > 65536 {
219 return Err(invalid("Verified engine host binding is required"));
220 }
221 let mut namespace = Sha256::new();
222 namespace.update(b"buntrail-container-filesystem-v1\0");
223 for bytes in [
224 self.engine_endpoint.as_slice(),
225 self.immutable_id.as_bytes(),
226 ] {
227 namespace.update((bytes.len() as u64).to_be_bytes());
228 namespace.update(bytes);
229 }
230 Ok(TargetIdentity {
231 security_binding: host_security_binding.to_vec(),
232 effective_user: uid.to_be_bytes().to_vec(),
233 filesystem_namespace: namespace.finalize().to_vec(),
234 container: Some(ContainerIdentity {
235 engine_endpoint: self.engine_endpoint.clone(),
236 immutable_id: self.immutable_id.as_bytes().to_vec(),
237 }),
238 })
239 }
240}