1use super::actions::PublishingActions;
21use super::broker::{Broker, BrokerRefusal, ObservedEcho};
22use super::budgets::LysBudgets;
23use super::context_watch::ContextReporter;
24use super::keys::{ChannelKeySender, ConnectionPublisher, ServicePublisher};
25use super::lys_http::{HttpLys, LysAnswer, LysHttp};
26use super::seats::LysSeatStarter;
27use super::{
28 BudgetPolicy, BudgetSource, KeySender, SeatStarter, ServiceAction, ServiceEffectError,
29 ServiceKind, ServiceRequest, Services,
30};
31use crate::auth_lys::LysPassConsumer;
32use crate::config::types::LysConfig;
33use crate::server::connection::LiminalConnectionServices;
34use std::sync::Arc;
35mod debug;
36
37pub struct ServiceBindings {
39 administrative: Option<AdministrativeBindings>,
40 context: Arc<ContextReporter>,
41 watch_context: bool,
42 broker: Option<Broker>,
43 echo: Option<ObservedEcho>,
44 relay: Option<Arc<super::relay::Relay>>,
45 relay_topology_local: Option<String>,
46}
47
48#[derive(Debug)]
49struct AdministrativeBindings {
50 seats: LysSeatStarter,
51 keys: ChannelKeySender,
52 budgets: LysBudgets,
53 actions: PublishingActions,
54}
55
56impl std::fmt::Debug for ServiceBindings {
57 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 formatter
59 .debug_struct("ServiceBindings")
60 .field("administrative", &self.administrative)
61 .field("watch_context", &self.watch_context)
62 .field("broker", &self.broker)
63 .field("echo", &self.echo)
64 .field("relay", &self.relay)
65 .finish_non_exhaustive()
66 }
67}
68
69impl ServiceBindings {
70 #[must_use]
74 pub fn new(
75 http: &Arc<dyn LysHttp>,
76 bearer: &str,
77 machine: String,
78 publisher: &Arc<dyn ServicePublisher>,
79 ) -> Self {
80 let context = Arc::new(ContextReporter::new(Arc::clone(http), bearer.to_owned()));
81 Self {
82 administrative: Some(AdministrativeBindings {
83 seats: LysSeatStarter::new(Arc::clone(http), bearer.to_owned(), machine),
84 keys: ChannelKeySender::new(Arc::clone(publisher)),
85 budgets: LysBudgets::new(Arc::clone(http), bearer.to_owned()),
86 actions: PublishingActions::new(Arc::clone(publisher), Arc::clone(&context)),
87 }),
88 context,
89 watch_context: false,
90 broker: None,
91 echo: None,
92 relay: None,
93 relay_topology_local: None,
94 }
95 }
96
97 #[must_use]
99 pub const fn watches_context(&self) -> bool {
100 self.watch_context
101 }
102
103 #[must_use]
105 pub fn services(&self) -> Services<'_> {
106 Services {
107 seats: self,
108 keys: self,
109 budgets: self,
110 actions: self,
111 }
112 }
113
114 #[must_use]
116 pub fn context_reporter(&self) -> Arc<ContextReporter> {
117 Arc::clone(&self.context)
118 }
119
120 #[must_use]
122 pub const fn broker(&self) -> Option<&Broker> {
123 self.broker.as_ref()
124 }
125
126 #[must_use]
128 pub const fn relay(&self) -> Option<&Arc<super::relay::Relay>> {
129 self.relay.as_ref()
130 }
131
132 #[must_use]
134 pub fn relay_topology_local(&self) -> Option<&str> {
135 self.relay_topology_local.as_deref()
136 }
137
138 pub fn serve_broker(
143 &self,
144 session: Option<&dyn crate::auth_lys::session::LysSession>,
145 subject: &str,
146 request: &ServiceRequest<'_>,
147 now: u64,
148 ) -> Result<serde_json::Value, BrokerRefusal> {
149 let broker = self
150 .broker
151 .as_ref()
152 .ok_or(BrokerRefusal::Named("broker_not_configured"))?;
153 let session = session.ok_or(BrokerRefusal::Named("pass_not_admitted"))?;
154 broker.serve(session, subject, request, now)
155 }
156
157 pub fn configure(
167 lys: &LysConfig,
168 services: Arc<LiminalConnectionServices>,
169 handle: tokio::runtime::Handle,
170 now: u64,
171 ) -> Result<Option<Self>, ServiceEffectError> {
172 if lys.services.is_none()
173 && lys.broker.is_none()
174 && !lys.relay.as_ref().is_some_and(|relay| relay.enabled)
175 {
176 return Ok(None);
177 }
178 let machine = lys
179 .machine
180 .as_ref()
181 .ok_or_else(|| ServiceEffectError("auth.lys.machine is absent".into()))?;
182 let pass = match crate::link_pass::MachinePass::configured(machine) {
183 Ok(pass) => pass,
184 Err(error) if !machine.agent_id.is_empty() && error.source_absent() => return Ok(None),
185 Err(error) => return Err(ServiceEffectError(error.to_string())),
186 };
187 let bearer = machine_bearer(lys, now)?;
188 let http: Arc<dyn LysHttp> = Arc::new(MachineHttp {
189 http: HttpLys::new(lys.api_url.as_deref().unwrap_or(&lys.issuer), handle)?,
190 pass,
191 });
192 let host = lys
193 .services
194 .as_ref()
195 .map_or("broker", |section| section.host.as_str());
196 let topology_local = services.console().device().to_owned();
197 let publisher: Arc<dyn ServicePublisher> =
198 Arc::new(ConnectionPublisher::new(services, host.to_owned()));
199 let mut bindings = if let Some(section) = &lys.services {
200 let mut bindings = Self::new(&http, &bearer, section.machine.clone(), &publisher);
201 bindings.watch_context = section.context_watch;
202 bindings
203 } else {
204 Self {
205 administrative: None,
206 context: Arc::new(ContextReporter::new(Arc::clone(&http), bearer)),
207 watch_context: false,
208 broker: None,
209 echo: None,
210 relay: None,
211 relay_topology_local: None,
212 }
213 };
214 if let Some(section) = &lys.broker {
215 let machine = lys.machine.as_ref().ok_or_else(|| {
216 ServiceEffectError("auth.lys.broker requires auth.lys.machine".to_owned())
217 })?;
218 let recorded = machine
219 .machines
220 .iter()
221 .map(|(holder, key)| {
222 let mut bytes = [0; 32];
223 hex::decode_to_slice(key, &mut bytes).map_err(|_| {
224 ServiceEffectError(format!(
225 "auth.lys.machine.machines: invalid key for holder {holder}"
226 ))
227 })?;
228 let identity = crate::link_handshake::MachineIdentity::new(bytes);
229 identity.x25519_public().map_err(|error| {
230 ServiceEffectError(format!(
231 "auth.lys.machine.machines: holder {holder}: {error}"
232 ))
233 })?;
234 Ok((holder.clone(), identity))
235 })
236 .collect::<Result<_, ServiceEffectError>>()?;
237 let broker = Broker::new(lys.network_id.clone(), recorded, publisher);
238 bindings.echo = Some(
239 ObservedEcho::bind(section.udp_listen, broker.counters()).map_err(|error| {
240 ServiceEffectError(format!("auth.lys.broker.udp_listen: {error}"))
241 })?,
242 );
243 bindings.broker = Some(broker);
244 }
245 if lys.relay.as_ref().is_some_and(|relay| relay.enabled) {
246 let machine = lys.machine.as_ref().ok_or_else(|| {
247 ServiceEffectError("auth.lys.relay requires auth.lys.machine".to_owned())
248 })?;
249 let recorded = machine
250 .machines
251 .iter()
252 .map(|(holder, key)| {
253 let mut bytes = [0; 32];
254 hex::decode_to_slice(key, &mut bytes).map_err(|_| {
255 ServiceEffectError(format!("invalid recorded machine for holder {holder}"))
256 })?;
257 let identity = crate::link_handshake::MachineIdentity::new(bytes);
258 identity
259 .x25519_public()
260 .map_err(|error| ServiceEffectError(error.to_string()))?;
261 Ok((holder.clone(), identity))
262 })
263 .collect::<Result<_, ServiceEffectError>>()?;
264 bindings.relay = Some(super::relay::Relay::new(lys.network_id.clone(), recorded));
265 bindings.relay_topology_local = Some(topology_local);
266 }
267 Ok(Some(bindings))
268 }
269
270 fn administrative(&self) -> Result<&AdministrativeBindings, ServiceEffectError> {
271 self.administrative
272 .as_ref()
273 .ok_or_else(|| ServiceEffectError("auth.lys.services is not configured".to_owned()))
274 }
275}
276
277impl SeatStarter for ServiceBindings {
278 fn start(&self, seat: &str, operation: &str) -> Result<(), ServiceEffectError> {
279 self.administrative()?.seats.start(seat, operation)
280 }
281}
282
283impl KeySender for ServiceBindings {
284 fn send(&self, session: &str, keys: &[u8], operation: &str) -> Result<(), ServiceEffectError> {
285 self.administrative()?.keys.send(session, keys, operation)
286 }
287}
288
289impl BudgetSource for ServiceBindings {
290 fn read(&self, name: &str) -> Result<Option<BudgetPolicy>, ServiceEffectError> {
291 self.administrative()?.budgets.read(name)
292 }
293}
294
295impl ServiceAction for ServiceBindings {
296 fn act(
297 &self,
298 service: ServiceKind,
299 payload: &[u8],
300 operation: &str,
301 ) -> Result<(), ServiceEffectError> {
302 self.administrative()?
303 .actions
304 .act(service, payload, operation)
305 }
306}
307
308pub fn machine_bearer(lys: &LysConfig, now: u64) -> Result<String, ServiceEffectError> {
317 let machine = lys.machine.as_ref().ok_or_else(|| {
318 ServiceEffectError(
319 "auth.lys.services or auth.lys.broker needs auth.lys.machine: services use this machine's pass"
320 .to_owned(),
321 )
322 })?;
323 let keys_json = std::fs::read_to_string(&lys.keys_path).map_err(|error| {
324 ServiceEffectError(format!(
325 "auth.lys.keys_path {}: {error}",
326 lys.keys_path.display()
327 ))
328 })?;
329 let adapter = crate::auth_lys::lys_pass_adapter::LysPassAdapter::from_keys_json(
330 &keys_json,
331 lys.issuer.clone(),
332 )
333 .map_err(|error| ServiceEffectError(format!("auth.lys key set refused: {error}")))?;
334 let pass = crate::link_pass::MachinePass::configured(machine)
335 .map_err(|error| ServiceEffectError(error.to_string()))?;
336 let pass_bytes = pass
337 .current()
338 .map_err(|error| ServiceEffectError(error.to_string()))?;
339 adapter
340 .verify_offline(
341 pass_bytes.as_bytes(),
342 crate::auth_lys::LIMINAL_PASS_AUDIENCE,
343 now,
344 )
345 .map_err(|error| ServiceEffectError(format!("auth.lys.machine pass refused: {error}")))?;
346 Ok(pass_bytes.trim().to_owned())
347}
348
349struct MachineHttp {
351 http: HttpLys,
352 pass: crate::link_pass::MachinePass,
353}
354impl LysHttp for MachineHttp {
355 fn get(&self, _: &str, path: &str) -> Result<LysAnswer, ServiceEffectError> {
356 let token = self
357 .pass
358 .current()
359 .map_err(|error| ServiceEffectError(error.to_string()))?;
360 self.http.get(&token, path)
361 }
362 fn post(
363 &self,
364 _: &str,
365 path: &str,
366 body: &serde_json::Value,
367 ) -> Result<LysAnswer, ServiceEffectError> {
368 let token = self
369 .pass
370 .current()
371 .map_err(|error| ServiceEffectError(error.to_string()))?;
372 self.http.post(&token, path, body)
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::ServiceBindings;
379 use crate::config::types::LysConfig;
380 use crate::server::connection::LiminalConnectionServices;
381 use crate::services::ServiceRequest;
382 use lys_core::keys::Ed25519Identity;
383 use serde_json::json;
384 use std::net::UdpSocket;
385 use std::sync::Arc;
386 use std::time::Duration;
387
388 fn base64url(bytes: &[u8]) -> String {
389 const ALPHABET: &[u8; 64] =
390 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
391 let mut encoded = String::new();
392 for chunk in bytes.chunks(3) {
393 let first = chunk[0];
394 encoded.push(char::from(ALPHABET[usize::from(first >> 2)]));
395 let second = chunk.get(1).copied().unwrap_or(0);
396 encoded.push(char::from(
397 ALPHABET[usize::from((first & 3) << 4 | second >> 4)],
398 ));
399 if chunk.len() > 1 {
400 let third = chunk.get(2).copied().unwrap_or(0);
401 encoded.push(char::from(
402 ALPHABET[usize::from((second & 15) << 2 | third >> 6)],
403 ));
404 if chunk.len() > 2 {
405 encoded.push(char::from(ALPHABET[usize::from(third & 63)]));
406 }
407 }
408 }
409 encoded
410 }
411
412 fn machine_pass() -> Result<(String, String), serde_json::Error> {
413 let issuer = Ed25519Identity::from_seed(&zeroize::Zeroizing::new([9; 32]));
414 let keys = json!({"keys": [{"kty": "OKP", "crv": "Ed25519", "kid": "broker",
415 "x": base64url(&issuer.public_key_bytes()), "alg": "EdDSA", "use": "sig"}]});
416 let claims = json!({"iss": "https://issuer.example", "sub": "broker", "aud": "liminal",
417 "iat": 0, "exp": 20, "rights": [],
418 "holder": {"id": "broker", "kind": "machine", "responsible": "owner"}});
419 let header = base64url(br#"{"alg":"EdDSA","typ":"JWT","kid":"broker"}"#);
420 let signed = format!("{header}.{}", base64url(&serde_json::to_vec(&claims)?));
421 let token = format!("{signed}.{}", base64url(&issuer.sign(signed.as_bytes())));
422 Ok((keys.to_string(), token))
423 }
424
425 #[test]
426 fn broker_only_configuration_binds_echo_and_refuses_unadmitted_calls()
427 -> Result<(), Box<dyn std::error::Error>> {
428 let root = tempfile::tempdir()?;
429 let keys = root.path().join("issuer-keys.json");
430 let pass = root.path().join("machine.pass");
431 let (keys_json, token) = machine_pass()?;
432 std::fs::write(&keys, keys_json)?;
433 std::fs::write(&pass, token)?;
434 let lys: LysConfig = toml::from_str(&format!(
435 r#"
436issuer = "https://issuer.example"
437network_id = "network-a"
438keys_path = "{}"
439[broker]
440udp_listen = "127.0.0.1:0"
441[machine]
442identity_path = "unused"
443pass_path = "{}"
444"#,
445 keys.display(),
446 pass.display()
447 ))?;
448 let runtime = tokio::runtime::Builder::new_current_thread().build()?;
449 let services = Arc::new(LiminalConnectionServices::empty()?);
450 let bindings = ServiceBindings::configure(&lys, services, runtime.handle().clone(), 10)?
451 .ok_or("broker did not configure")?;
452 assert!(bindings.broker().is_some());
453 let refused = bindings
454 .serve_broker(
455 None,
456 "net.broker.lookup",
457 &ServiceRequest {
458 operation: "lookup",
459 payload: b"{}",
460 },
461 10,
462 )
463 .err()
464 .ok_or("unadmitted call accepted")?;
465 assert_eq!(refused.to_string(), "pass_not_admitted");
466 let refused = bindings
467 .services()
468 .seats
469 .start("unused", "operation")
470 .err()
471 .ok_or("unconfigured administrative service acted")?;
472 assert!(
473 refused
474 .to_string()
475 .contains("auth.lys.services is not configured")
476 );
477 let address = bindings.echo.as_ref().ok_or("echo missing")?.local_addr();
478 let socket = UdpSocket::bind("127.0.0.1:0")?;
479 socket.set_read_timeout(Some(Duration::from_secs(3)))?;
480 socket.send_to(&[b"LMOB".as_slice(), &[3; 16]].concat(), address)?;
481 let mut reply = [0; 64];
482 let (length, _) = socket.recv_from(&mut reply)?;
483 assert_eq!(length, 27);
484 assert_eq!(&reply[..20], &[b"LMOA".as_slice(), &[3; 16]].concat());
485 drop(bindings);
486 let rebound = UdpSocket::bind(address)?;
487 assert_eq!(rebound.local_addr()?, address);
488 Ok(())
489 }
490}