Skip to main content

buntrail_services/connections/
container_identity.rs

1//! Engine metadata is discovery evidence; only an exec probe verifies the effective UID.
2use 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}