1use std::io::{BufRead, BufReader, Write};
35use std::net::{SocketAddr, TcpListener, TcpStream};
36use std::sync::Arc;
37use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
38use std::thread::JoinHandle;
39use std::time::Duration;
40
41use crate::durability_policy::DurabilityMode;
42
43use ed25519_dalek::{Verifier, VerifyingKey};
44use tracing::{debug, error, info};
45
46use melin_app::auth::{AuthorizedKeys, Permission};
47use melin_wire_protocol::control::TransportResponse;
48use melin_wire_protocol::control_codec;
49
50pub fn spawn(
59 bind_addr: SocketAddr,
60 promote: Option<Arc<AtomicBool>>,
61 rotate_requested: Option<Arc<AtomicBool>>,
62 durability_mode: Option<Arc<AtomicU8>>,
63 shutdown: Arc<AtomicBool>,
64 authorized_keys: Arc<AuthorizedKeys>,
65) -> JoinHandle<()> {
66 std::thread::Builder::new()
67 .name("admin-listener".into())
68 .spawn(move || {
69 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
70 run(
71 bind_addr,
72 promote.as_deref(),
73 rotate_requested.as_deref(),
74 durability_mode.as_deref(),
75 &shutdown,
76 &authorized_keys,
77 )
78 }));
79 if let Err(panic) = result {
80 let msg = panic_message(&panic);
81 error!(addr = %bind_addr, panic = %msg, "admin listener thread panicked");
82 }
83 })
84 .expect("failed to spawn admin listener thread")
85}
86
87fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
88 if let Some(s) = payload.downcast_ref::<&'static str>() {
89 (*s).to_string()
90 } else if let Some(s) = payload.downcast_ref::<String>() {
91 s.clone()
92 } else {
93 "<non-string panic payload>".to_string()
94 }
95}
96
97fn run(
98 bind_addr: SocketAddr,
99 promote: Option<&AtomicBool>,
100 rotate_requested: Option<&AtomicBool>,
101 durability_mode: Option<&AtomicU8>,
102 shutdown: &AtomicBool,
103 authorized_keys: &AuthorizedKeys,
104) {
105 let listener = match TcpListener::bind(bind_addr) {
106 Ok(l) => l,
107 Err(e) => {
108 error!(addr = %bind_addr, error = %e, "admin listener bind failed");
109 return;
110 }
111 };
112 listener
113 .set_nonblocking(true)
114 .expect("set admin listener nonblocking");
115
116 info!(
117 addr = %bind_addr,
118 promote_enabled = promote.is_some(),
119 rotate_enabled = rotate_requested.is_some(),
120 durability_enabled = durability_mode.is_some(),
121 "admin listener started"
122 );
123
124 loop {
125 if shutdown.load(Ordering::Relaxed) {
126 return;
127 }
128
129 match listener.accept() {
130 Ok((stream, peer)) => {
131 debug!(peer = %peer, "admin connection accepted");
132 handle_connection(
133 stream,
134 promote,
135 rotate_requested,
136 durability_mode,
137 authorized_keys,
138 );
139 }
140 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
141 std::thread::sleep(Duration::from_millis(100));
142 }
143 Err(e) => {
144 debug!(error = %e, "admin listener accept error");
145 std::thread::sleep(Duration::from_millis(100));
146 }
147 }
148 }
149}
150
151fn authenticate(stream: &mut TcpStream, authorized_keys: &AuthorizedKeys) -> Result<(), String> {
155 let mut nonce = [0u8; 32];
156 getrandom::fill(&mut nonce).map_err(|e| format!("getrandom failed: {e}"))?;
157
158 let mut buf = [0u8; 128];
159 let written =
160 control_codec::encode_transport_response(&TransportResponse::Challenge { nonce }, &mut buf)
161 .map_err(|e| format!("encode Challenge: {e}"))?;
162 stream
163 .write_all(&buf[..written])
164 .map_err(|e| format!("send Challenge: {e}"))?;
165 stream
166 .flush()
167 .map_err(|e| format!("flush Challenge: {e}"))?;
168
169 let mut len_buf = [0u8; 4];
170 std::io::Read::read_exact(stream, &mut len_buf)
171 .map_err(|e| format!("read auth frame length: {e}"))?;
172 let frame_len = u32::from_le_bytes(len_buf) as usize;
173 if frame_len > 256 {
174 send_auth_failed(stream);
175 return Err(format!("auth frame too large: {frame_len}"));
176 }
177 let mut frame_buf = [0u8; 256];
178 std::io::Read::read_exact(stream, &mut frame_buf[..frame_len])
179 .map_err(|e| format!("read auth frame payload: {e}"))?;
180
181 let (_seq, cr) = match control_codec::decode_challenge_response(&frame_buf[..frame_len]) {
182 Ok(pair) => pair,
183 Err(e) => {
184 send_auth_failed(stream);
185 return Err(format!("decode ChallengeResponse: {e}"));
186 }
187 };
188
189 let (signature_bytes, public_key_bytes) = (cr.signature, cr.public_key);
190
191 let permission = match authorized_keys.lookup(&public_key_bytes) {
192 Some(perm) => perm,
193 None => {
194 send_auth_failed(stream);
195 return Err("unknown public key".into());
196 }
197 };
198 if permission != Permission::Operator {
199 send_auth_failed(stream);
200 return Err(format!(
201 "admin endpoint requires operator key, got {permission:?}"
202 ));
203 }
204
205 let verifying_key = VerifyingKey::from_bytes(&public_key_bytes).map_err(|e| {
206 send_auth_failed(stream);
207 format!("invalid public key: {e}")
208 })?;
209 let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes);
210 verifying_key.verify(&nonce, &signature).map_err(|e| {
211 send_auth_failed(stream);
212 format!("signature verification failed: {e}")
213 })?;
214
215 let written =
216 control_codec::encode_transport_response(&TransportResponse::ServerReady, &mut buf)
217 .map_err(|e| format!("encode ServerReady: {e}"))?;
218 stream
219 .write_all(&buf[..written])
220 .map_err(|e| format!("send ServerReady: {e}"))?;
221 stream
222 .flush()
223 .map_err(|e| format!("flush ServerReady: {e}"))?;
224
225 Ok(())
226}
227
228fn send_auth_failed(stream: &mut TcpStream) {
229 let mut buf = [0u8; 8];
230 if let Ok(written) =
231 control_codec::encode_transport_response(&TransportResponse::AuthFailed, &mut buf)
232 {
233 send_best_effort(stream, &buf[..written]);
237 }
238}
239
240fn send_best_effort(stream: &mut TcpStream, payload: &[u8]) {
246 if let Err(e) = stream.write_all(payload) {
247 debug!(error = %e, "admin write failed");
248 return;
249 }
250 if let Err(e) = stream.flush() {
251 debug!(error = %e, "admin flush failed");
252 }
253}
254
255fn handle_connection(
258 mut stream: TcpStream,
259 promote: Option<&AtomicBool>,
260 rotate_requested: Option<&AtomicBool>,
261 durability_mode: Option<&AtomicU8>,
262 authorized_keys: &AuthorizedKeys,
263) {
264 stream.set_read_timeout(Some(Duration::from_secs(5))).ok();
265 stream.set_write_timeout(Some(Duration::from_secs(5))).ok();
266
267 if let Err(reason) = authenticate(&mut stream, authorized_keys) {
268 debug!(reason = %reason, "admin auth failed");
269 return;
270 }
271
272 let cloned = match stream.try_clone() {
273 Ok(s) => s,
274 Err(e) => {
275 debug!(error = %e, "failed to clone admin stream");
276 return;
277 }
278 };
279 let mut reader = BufReader::new(cloned);
280
281 let mut line = String::new();
282 if reader.read_line(&mut line).is_err() {
283 debug!("failed to read from admin connection");
284 return;
285 }
286
287 let trimmed = line.trim();
288 match trimmed {
289 "PROMOTE" => match promote {
290 Some(flag) => {
291 flag.store(true, Ordering::Release);
292 send_best_effort(&mut stream, b"OK\n");
293 info!("promotion triggered by operator");
294 }
295 None => {
296 send_best_effort(&mut stream, b"ERR PROMOTE not available on this node\n");
297 debug!("rejected PROMOTE — flag not wired (primary node?)");
298 }
299 },
300 "ROTATE" => match rotate_requested {
301 Some(flag) => {
302 flag.store(true, Ordering::Release);
303 send_best_effort(&mut stream, b"OK\n");
304 info!("rotation requested by operator");
305 }
306 None => {
307 send_best_effort(&mut stream, b"ERR ROTATE not available on this node\n");
308 debug!("rejected ROTATE — flag not wired");
309 }
310 },
311 cmd if cmd.starts_with("DURABILITY") => {
312 let mut parts = cmd.splitn(2, char::is_whitespace);
319 let _ = parts.next();
321 let arg = parts.next().map(str::trim).unwrap_or("");
322 handle_durability(&mut stream, durability_mode, arg);
323 }
324 other => {
325 debug!(received = %other, "unknown admin command");
326 send_best_effort(&mut stream, b"ERR unknown command\n");
327 }
328 }
329}
330
331fn handle_durability(stream: &mut TcpStream, durability_mode: Option<&AtomicU8>, arg: &str) {
338 let Some(atomic) = durability_mode else {
339 send_best_effort(stream, b"ERR DURABILITY not available on this node\n");
340 debug!("rejected DURABILITY — atomic not wired (replica node?)");
341 return;
342 };
343 if arg.is_empty() {
344 send_best_effort(
345 stream,
346 b"ERR DURABILITY requires a mode (local|hybrid|durably-replicated)\n",
347 );
348 debug!("rejected DURABILITY — missing argument");
349 return;
350 }
351 let Some(next) = DurabilityMode::parse(arg) else {
352 let mut buf = [0u8; 128];
357 let msg = format_unknown_mode(&mut buf, arg);
358 send_best_effort(stream, msg);
359 debug!(received = %arg, "rejected DURABILITY — unknown mode");
360 return;
361 };
362 let prev_byte = atomic.swap(next.as_u8(), Ordering::Relaxed);
367 let prev = DurabilityMode::from_u8(prev_byte)
368 .map(|m| m.as_str())
369 .unwrap_or("<corrupted>");
370 send_best_effort(stream, b"OK\n");
371 info!(
372 prev = prev,
373 next = next.as_str(),
374 "durability mode changed by operator"
375 );
376}
377
378fn format_unknown_mode<'a>(buf: &'a mut [u8], arg: &str) -> &'a [u8] {
383 use std::io::Write as _;
384 let mut cursor = std::io::Cursor::new(&mut buf[..]);
385 let _ = writeln!(
389 cursor,
390 "ERR DURABILITY unknown mode `{arg}` (expected local|hybrid|durably-replicated)"
391 );
392 let n = cursor.position() as usize;
393 &cursor.into_inner()[..n]
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use std::io::{BufRead, BufReader, Read, Write};
400
401 use ed25519_dalek::{Signer, SigningKey};
402 use melin_wire_protocol::control_codec::{
403 TAG_AUTH_FAILED, TAG_CHALLENGE, TAG_CHALLENGE_RESPONSE, TAG_SERVER_READY,
404 };
405
406 fn operator_keys() -> (SigningKey, Arc<AuthorizedKeys>) {
407 let signing_key = SigningKey::from_bytes(&[0xAD; 32]);
408 let pub_b64 = base64::Engine::encode(
409 &base64::engine::general_purpose::STANDARD,
410 signing_key.verifying_key().to_bytes(),
411 );
412 let content = format!("operator {pub_b64} test-ops\n");
413 let keys = AuthorizedKeys::parse(&content).expect("parse authorized_keys");
414 (signing_key, Arc::new(keys))
415 }
416
417 fn trader_keys() -> (SigningKey, Arc<AuthorizedKeys>) {
418 let signing_key = SigningKey::from_bytes(&[0xBD; 32]);
419 let pub_b64 = base64::Engine::encode(
420 &base64::engine::general_purpose::STANDARD,
421 signing_key.verifying_key().to_bytes(),
422 );
423 let content = format!("trader {pub_b64} test-trader\n");
424 let keys = AuthorizedKeys::parse(&content).expect("parse authorized_keys");
425 (signing_key, Arc::new(keys))
426 }
427
428 fn client_authenticate(stream: &mut TcpStream, key: &SigningKey) -> u8 {
433 let mut len_buf = [0u8; 4];
435 stream.read_exact(&mut len_buf).expect("read challenge len");
436 let frame_len = u32::from_le_bytes(len_buf) as usize;
437 let mut frame_buf = vec![0u8; frame_len];
438 stream
439 .read_exact(&mut frame_buf)
440 .expect("read challenge payload");
441 assert_eq!(frame_buf[0], TAG_CHALLENGE, "expected Challenge");
442 let nonce = &frame_buf[1..33];
443
444 let signature = key.sign(nonce);
447 let mut frame = Vec::with_capacity(105);
448 frame.extend_from_slice(&0u64.to_le_bytes()); frame.push(TAG_CHALLENGE_RESPONSE);
450 frame.extend_from_slice(&signature.to_bytes());
451 frame.extend_from_slice(&key.verifying_key().to_bytes());
452 stream
453 .write_all(&(frame.len() as u32).to_le_bytes())
454 .expect("send result len");
455 stream.write_all(&frame).expect("send");
456 stream.flush().expect("flush");
457
458 let mut len_buf2 = [0u8; 4];
460 stream.read_exact(&mut len_buf2).expect("read result len");
461 let result_len = u32::from_le_bytes(len_buf2) as usize;
462 let mut result_buf = vec![0u8; result_len];
463 stream
464 .read_exact(&mut result_buf)
465 .expect("read result payload");
466 result_buf[0]
467 }
468
469 fn ephemeral_listener() -> (TcpListener, SocketAddr) {
470 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
471 let addr = listener.local_addr().unwrap();
472 (listener, addr)
473 }
474
475 fn send_command(addr: SocketAddr, key: &SigningKey, command: &[u8]) -> String {
478 let mut stream = TcpStream::connect(addr).unwrap();
479 assert_eq!(client_authenticate(&mut stream, key), TAG_SERVER_READY);
480 stream.write_all(command).unwrap();
481 stream.flush().unwrap();
482 let mut reader = BufReader::new(stream);
483 let mut line = String::new();
484 reader.read_line(&mut line).unwrap();
485 line.trim().to_string()
486 }
487
488 #[test]
489 fn promote_command_sets_flag_when_wired() {
490 let (listener, addr) = ephemeral_listener();
491 drop(listener);
492
493 let (key, auth_keys) = operator_keys();
494 let promote = Arc::new(AtomicBool::new(false));
495 let rotate = Arc::new(AtomicBool::new(false));
496 let shutdown = Arc::new(AtomicBool::new(false));
497 let _h = spawn(
498 addr,
499 Some(Arc::clone(&promote)),
500 Some(Arc::clone(&rotate)),
501 None,
502 Arc::clone(&shutdown),
503 auth_keys,
504 );
505 std::thread::sleep(Duration::from_millis(200));
506
507 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
508 assert!(promote.load(Ordering::Acquire));
509 assert!(!rotate.load(Ordering::Acquire));
510
511 shutdown.store(true, Ordering::Release);
512 }
513
514 #[test]
515 fn rotate_command_sets_flag_when_wired() {
516 let (listener, addr) = ephemeral_listener();
517 drop(listener);
518
519 let (key, auth_keys) = operator_keys();
520 let promote = Arc::new(AtomicBool::new(false));
521 let rotate = Arc::new(AtomicBool::new(false));
522 let shutdown = Arc::new(AtomicBool::new(false));
523 let _h = spawn(
524 addr,
525 Some(Arc::clone(&promote)),
526 Some(Arc::clone(&rotate)),
527 None,
528 Arc::clone(&shutdown),
529 auth_keys,
530 );
531 std::thread::sleep(Duration::from_millis(200));
532
533 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
534 assert!(rotate.load(Ordering::Acquire));
535 assert!(!promote.load(Ordering::Acquire));
536
537 shutdown.store(true, Ordering::Release);
538 }
539
540 #[test]
543 fn promote_rejected_when_not_wired() {
544 let (listener, addr) = ephemeral_listener();
545 drop(listener);
546
547 let (key, auth_keys) = operator_keys();
548 let rotate = Arc::new(AtomicBool::new(false));
549 let shutdown = Arc::new(AtomicBool::new(false));
550 let _h = spawn(
551 addr,
552 None,
553 Some(Arc::clone(&rotate)),
554 None,
555 Arc::clone(&shutdown),
556 auth_keys,
557 );
558 std::thread::sleep(Duration::from_millis(200));
559
560 let resp = send_command(addr, &key, b"PROMOTE\n");
561 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
562 assert!(!rotate.load(Ordering::Acquire));
563
564 shutdown.store(true, Ordering::Release);
565 }
566
567 #[test]
569 fn rotate_rejected_when_not_wired() {
570 let (listener, addr) = ephemeral_listener();
571 drop(listener);
572
573 let (key, auth_keys) = operator_keys();
574 let promote = Arc::new(AtomicBool::new(false));
575 let shutdown = Arc::new(AtomicBool::new(false));
576 let _h = spawn(
577 addr,
578 Some(Arc::clone(&promote)),
579 None,
580 None,
581 Arc::clone(&shutdown),
582 auth_keys,
583 );
584 std::thread::sleep(Duration::from_millis(200));
585
586 let resp = send_command(addr, &key, b"ROTATE\n");
587 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
588 assert!(!promote.load(Ordering::Acquire));
589
590 shutdown.store(true, Ordering::Release);
591 }
592
593 #[test]
597 fn listener_handles_multiple_commands() {
598 let (listener, addr) = ephemeral_listener();
599 drop(listener);
600
601 let (key, auth_keys) = operator_keys();
602 let promote = Arc::new(AtomicBool::new(false));
603 let rotate = Arc::new(AtomicBool::new(false));
604 let shutdown = Arc::new(AtomicBool::new(false));
605 let _h = spawn(
606 addr,
607 Some(Arc::clone(&promote)),
608 Some(Arc::clone(&rotate)),
609 None,
610 Arc::clone(&shutdown),
611 auth_keys,
612 );
613 std::thread::sleep(Duration::from_millis(200));
614
615 for _ in 0..3 {
618 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
619 assert!(rotate.load(Ordering::Acquire));
620 rotate
621 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Relaxed)
622 .expect("flag should still be set");
623 std::thread::sleep(Duration::from_millis(100));
624 }
625
626 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
628 assert!(promote.load(Ordering::Acquire));
629
630 shutdown.store(true, Ordering::Release);
631 }
632
633 #[test]
634 fn unknown_command_rejected() {
635 let (listener, addr) = ephemeral_listener();
636 drop(listener);
637
638 let (key, auth_keys) = operator_keys();
639 let promote = Arc::new(AtomicBool::new(false));
640 let rotate = Arc::new(AtomicBool::new(false));
641 let shutdown = Arc::new(AtomicBool::new(false));
642 let _h = spawn(
643 addr,
644 Some(Arc::clone(&promote)),
645 Some(Arc::clone(&rotate)),
646 None,
647 Arc::clone(&shutdown),
648 auth_keys,
649 );
650 std::thread::sleep(Duration::from_millis(200));
651
652 let resp = send_command(addr, &key, b"INVALID\n");
653 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
654 assert!(!promote.load(Ordering::Acquire));
655 assert!(!rotate.load(Ordering::Acquire));
656
657 shutdown.store(true, Ordering::Release);
658 }
659
660 #[test]
661 fn non_operator_key_rejected() {
662 let (listener, addr) = ephemeral_listener();
663 drop(listener);
664
665 let (trader_key, auth_keys) = trader_keys();
666 let promote = Arc::new(AtomicBool::new(false));
667 let rotate = Arc::new(AtomicBool::new(false));
668 let shutdown = Arc::new(AtomicBool::new(false));
669 let _h = spawn(
670 addr,
671 Some(Arc::clone(&promote)),
672 Some(Arc::clone(&rotate)),
673 None,
674 Arc::clone(&shutdown),
675 auth_keys,
676 );
677 std::thread::sleep(Duration::from_millis(200));
678
679 let mut stream = TcpStream::connect(addr).unwrap();
680 let result = client_authenticate(&mut stream, &trader_key);
681 assert_eq!(result, TAG_AUTH_FAILED);
682 assert!(!promote.load(Ordering::Acquire));
683 assert!(!rotate.load(Ordering::Acquire));
684
685 shutdown.store(true, Ordering::Release);
686 }
687
688 fn run_durability(initial: DurabilityMode, cmd: &[u8]) -> (String, Option<DurabilityMode>) {
693 let (listener, addr) = ephemeral_listener();
694 drop(listener);
695
696 let (key, auth_keys) = operator_keys();
697 let mode = Arc::new(AtomicU8::new(initial.as_u8()));
698 let shutdown = Arc::new(AtomicBool::new(false));
699 let _h = spawn(
700 addr,
701 None,
702 None,
703 Some(Arc::clone(&mode)),
704 Arc::clone(&shutdown),
705 auth_keys,
706 );
707 std::thread::sleep(Duration::from_millis(200));
708
709 let resp = send_command(addr, &key, cmd);
710 let after = DurabilityMode::from_u8(mode.load(Ordering::Relaxed));
711 shutdown.store(true, Ordering::Release);
712 (resp, after)
713 }
714
715 #[test]
716 fn durability_command_swaps_mode() {
717 let (resp, after) = run_durability(DurabilityMode::Hybrid, b"DURABILITY local\n");
718 assert_eq!(resp, "OK");
719 assert_eq!(after, Some(DurabilityMode::Local));
720 }
721
722 #[test]
723 fn durability_command_accepts_each_mode() {
724 for target in [
725 DurabilityMode::Local,
726 DurabilityMode::Hybrid,
727 DurabilityMode::DurablyReplicated,
728 ] {
729 let cmd = format!("DURABILITY {}\n", target.as_str());
730 let (resp, after) = run_durability(DurabilityMode::Local, cmd.as_bytes());
731 assert_eq!(resp, "OK", "mode {target}");
732 assert_eq!(after, Some(target));
733 }
734 }
735
736 #[test]
737 fn durability_command_rejects_unknown_mode() {
738 let (resp, after) = run_durability(DurabilityMode::Hybrid, b"DURABILITY fast\n");
739 assert!(
740 resp.starts_with("ERR DURABILITY unknown mode"),
741 "expected unknown-mode ERR, got {resp}"
742 );
743 assert_eq!(after, Some(DurabilityMode::Hybrid));
745 }
746
747 #[test]
748 fn durability_command_rejects_missing_argument() {
749 let (resp, after) = run_durability(DurabilityMode::Hybrid, b"DURABILITY\n");
750 assert!(
751 resp.starts_with("ERR DURABILITY requires a mode"),
752 "expected missing-arg ERR, got {resp}"
753 );
754 assert_eq!(after, Some(DurabilityMode::Hybrid));
755 }
756
757 #[test]
758 fn durability_command_rejected_when_not_wired() {
759 let (listener, addr) = ephemeral_listener();
762 drop(listener);
763
764 let (key, auth_keys) = operator_keys();
765 let promote = Arc::new(AtomicBool::new(false));
766 let shutdown = Arc::new(AtomicBool::new(false));
767 let _h = spawn(
768 addr,
769 Some(Arc::clone(&promote)),
770 None,
771 None,
772 Arc::clone(&shutdown),
773 auth_keys,
774 );
775 std::thread::sleep(Duration::from_millis(200));
776
777 let resp = send_command(addr, &key, b"DURABILITY local\n");
778 assert!(
779 resp.starts_with("ERR DURABILITY not available"),
780 "expected not-available ERR, got {resp}"
781 );
782
783 shutdown.store(true, Ordering::Release);
784 }
785}