1use crate::auth_lys::{
13 AccessAction, AccessDecisionError, AccessResource, AdmittedLysPass, DeliberateDecision,
14 GrantReceipt, LysDeliberateConsumer, ResourceKind, SubjectBinding, SubjectBindingError,
15 decide_deliberate,
16};
17use crate::link_handshake::{LocalMachine, MachineIdentity};
18use std::collections::BTreeMap;
19
20#[path = "link_route_call.rs"]
21pub mod calls;
22#[path = "link_certificate.rs"]
23pub mod certificate;
24#[path = "link_federated.rs"]
25pub mod federated;
26mod life;
27pub use life::LinkLifeCounters;
28pub(crate) use life::{CloseLinks, LinkLife, LinkRegistry};
29
30#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
32pub enum LinkEnd {
33 #[error("peer_gone")]
35 PeerGone,
36 #[error("revoked")]
38 Revoked,
39 #[error("refused: {0}")]
41 Refused(String),
42 #[error("replaced")]
44 Replaced,
45 #[error("shutdown")]
47 Shutdown,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum LinkAction {
53 Export,
55 Import,
57}
58
59impl LinkAction {
60 #[must_use]
62 pub const fn access_action(self) -> AccessAction {
63 match self {
64 Self::Export => AccessAction::LinkExport,
65 Self::Import => AccessAction::LinkImport,
66 }
67 }
68
69 #[must_use]
71 pub const fn other(self) -> Self {
72 match self {
73 Self::Export => Self::Import,
74 Self::Import => Self::Export,
75 }
76 }
77}
78
79impl From<liminal::protocol::LinkEnd> for LinkAction {
80 fn from(end: liminal::protocol::LinkEnd) -> Self {
81 match end {
82 liminal::protocol::LinkEnd::Export => Self::Export,
83 liminal::protocol::LinkEnd::Import => Self::Import,
84 }
85 }
86}
87
88#[derive(Debug)]
97pub struct LinkHost {
98 local: LocalMachine,
99 rights: Box<dyn LinkRights>,
100 recorded: BTreeMap<String, MachineIdentity>,
101 peers: Option<crate::config::peers::PeersConfig>,
102 service_key: Option<[u8; 32]>,
103 certificate_standing: Option<std::sync::Arc<certificate::standing::Standing>>,
104 far: Option<std::sync::Arc<federated::FarTopology>>,
105 executor: std::sync::OnceLock<std::sync::Weak<dyn calls::CallExecutor>>,
106}
107
108impl LinkHost {
109 #[must_use]
111 pub const fn new(
112 local: LocalMachine,
113 rights: Box<dyn LinkRights>,
114 recorded: BTreeMap<String, MachineIdentity>,
115 ) -> Self {
116 Self {
117 local,
118 rights,
119 recorded,
120 peers: None,
121 service_key: None,
122 certificate_standing: None,
123 far: None,
124 executor: std::sync::OnceLock::new(),
125 }
126 }
127
128 #[must_use]
130 pub const fn local(&self) -> &LocalMachine {
131 &self.local
132 }
133
134 #[must_use]
136 pub fn rights(&self) -> &dyn LinkRights {
137 self.rights.as_ref()
138 }
139
140 #[must_use]
142 pub fn recorded_machine(&self, holder: &str) -> Option<MachineIdentity> {
143 self.recorded.get(holder).copied()
144 }
145
146 pub fn with_peer_security(
151 mut self,
152 peers: crate::config::peers::PeersConfig,
153 service_key: [u8; 32],
154 ) -> Result<Self, &'static str> {
155 peers.validate()?;
156 let verifying = ed25519_dalek::VerifyingKey::from_bytes(&service_key)
157 .map_err(|_| "certificate_service_key_refused")?;
158 if verifying.is_weak() {
159 return Err("certificate_service_key_refused");
160 }
161 self.far = Some(std::sync::Arc::new(federated::FarTopology::new(
162 federated::ProjectionLimits {
163 bytes: peers.max_topology_bytes,
164 stations: peers.max_stations,
165 peers: peers.max_peers,
166 },
167 )));
168 self.peers = Some(peers);
169 self.service_key = Some(service_key);
170 Ok(self)
171 }
172 #[must_use]
174 pub const fn peers(&self) -> Option<&crate::config::peers::PeersConfig> {
175 self.peers.as_ref()
176 }
177 #[must_use]
179 pub const fn far(&self) -> Option<&std::sync::Arc<federated::FarTopology>> {
180 self.far.as_ref()
181 }
182 #[must_use]
184 pub const fn service_key(&self) -> Option<&[u8; 32]> {
185 self.service_key.as_ref()
186 }
187 #[must_use]
189 pub const fn standing(&self) -> Option<&std::sync::Arc<certificate::standing::Standing>> {
190 self.certificate_standing.as_ref()
191 }
192 #[must_use]
194 pub fn with_standing(
195 mut self,
196 standing: std::sync::Arc<certificate::standing::Standing>,
197 ) -> Self {
198 self.certificate_standing = Some(standing);
199 self
200 }
201 pub fn install_call_executor(
206 &self,
207 executor: &std::sync::Arc<dyn calls::CallExecutor>,
208 ) -> Result<(), calls::CallError> {
209 self.executor
210 .set(std::sync::Arc::downgrade(executor))
211 .map_err(|_| calls::CallError::Unavailable)
212 }
213 #[must_use]
215 pub fn call_executor(&self) -> Option<std::sync::Arc<dyn calls::CallExecutor>> {
216 self.executor.get().and_then(std::sync::Weak::upgrade)
217 }
218}
219
220pub trait LinkRights: Send + Sync + std::fmt::Debug {
224 fn holds(
229 &self,
230 action: LinkAction,
231 subject: &SubjectBinding,
232 now: u64,
233 ) -> Result<GrantReceipt, AccessDecisionError>;
234}
235
236#[derive(Clone, Debug, PartialEq, Eq)]
238pub struct Link {
239 network_id: String,
240 id: String,
241 exporter: MachineIdentity,
242 importer: MachineIdentity,
243}
244
245#[derive(Debug, PartialEq, Eq)]
247pub enum LinkError {
248 Identifier(&'static str),
250 SameMachine,
252}
253
254impl std::fmt::Display for LinkError {
255 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 match self {
257 Self::Identifier(member) => write!(formatter, "link {member} is not an identifier"),
258 Self::SameMachine => formatter.write_str("a link joins two different machines"),
259 }
260 }
261}
262
263impl std::error::Error for LinkError {}
264
265#[derive(Debug, PartialEq, Eq)]
267pub enum LinkRefusal {
268 Binding(SubjectBindingError),
270 Export(AccessDecisionError),
272 Import(AccessDecisionError),
274}
275
276impl std::fmt::Display for LinkRefusal {
277 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 match self {
279 Self::Binding(SubjectBindingError(member)) => {
280 write!(formatter, "crossing refused: subject {member}")
281 }
282 Self::Export(error) => write!(formatter, "crossing refused at export: {error}"),
283 Self::Import(error) => write!(formatter, "crossing refused at import: {error}"),
284 }
285 }
286}
287
288impl std::error::Error for LinkRefusal {}
289
290#[derive(Clone, Debug, PartialEq, Eq)]
292pub struct Crossing {
293 pub export: GrantReceipt,
295 pub import: GrantReceipt,
297}
298
299impl Link {
300 pub fn new(
306 network_id: String,
307 id: String,
308 exporter: MachineIdentity,
309 importer: MachineIdentity,
310 ) -> Result<Self, LinkError> {
311 for (member, value) in [("network", &network_id), ("id", &id)] {
312 if value.is_empty() || value.chars().any(char::is_whitespace) {
313 return Err(LinkError::Identifier(member));
314 }
315 }
316 if exporter == importer {
317 return Err(LinkError::SameMachine);
318 }
319 Ok(Self {
320 network_id,
321 id,
322 exporter,
323 importer,
324 })
325 }
326
327 #[must_use]
329 pub fn network_id(&self) -> &str {
330 &self.network_id
331 }
332
333 #[must_use]
335 pub fn resource(&self) -> AccessResource {
336 AccessResource {
337 kind: ResourceKind::Link,
338 id: self.id.clone(),
339 }
340 }
341
342 #[must_use]
344 pub const fn exporter(&self) -> &MachineIdentity {
345 &self.exporter
346 }
347
348 #[must_use]
350 pub const fn importer(&self) -> &MachineIdentity {
351 &self.importer
352 }
353
354 pub fn open<C: LysDeliberateConsumer>(
362 &self,
363 end: LinkAction,
364 consumer: &C,
365 pass: &AdmittedLysPass<C::Pass>,
366 now: u64,
367 ) -> Result<DeliberateDecision, AccessDecisionError> {
368 decide_deliberate(consumer, pass, &self.resource(), end.access_action(), now)
369 }
370
371 pub fn crossing(
377 &self,
378 exporter: &dyn LinkRights,
379 importer: &dyn LinkRights,
380 subject: &str,
381 now: u64,
382 ) -> Result<Crossing, LinkRefusal> {
383 let bound =
384 SubjectBinding::link(self.network_id.clone(), self.id.clone(), subject.to_owned())
385 .map_err(LinkRefusal::Binding)?;
386 let export = exporter
387 .holds(LinkAction::Export, &bound, now)
388 .map_err(LinkRefusal::Export)?;
389 let import = importer
390 .holds(LinkAction::Import, &bound, now)
391 .map_err(LinkRefusal::Import)?;
392 Ok(Crossing { export, import })
393 }
394}