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