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_protocol::codec;
403 use melin_protocol::message::{Request, ResponseKind};
404
405 fn operator_keys() -> (SigningKey, Arc<AuthorizedKeys>) {
406 let signing_key = SigningKey::from_bytes(&[0xAD; 32]);
407 let pub_b64 = base64::Engine::encode(
408 &base64::engine::general_purpose::STANDARD,
409 signing_key.verifying_key().to_bytes(),
410 );
411 let content = format!("operator {pub_b64} test-ops\n");
412 let keys = AuthorizedKeys::parse(&content).expect("parse authorized_keys");
413 (signing_key, Arc::new(keys))
414 }
415
416 fn trader_keys() -> (SigningKey, Arc<AuthorizedKeys>) {
417 let signing_key = SigningKey::from_bytes(&[0xBD; 32]);
418 let pub_b64 = base64::Engine::encode(
419 &base64::engine::general_purpose::STANDARD,
420 signing_key.verifying_key().to_bytes(),
421 );
422 let content = format!("trader {pub_b64} test-trader\n");
423 let keys = AuthorizedKeys::parse(&content).expect("parse authorized_keys");
424 (signing_key, Arc::new(keys))
425 }
426
427 fn client_authenticate(stream: &mut TcpStream, key: &SigningKey) -> ResponseKind {
428 let mut len_buf = [0u8; 4];
429 stream.read_exact(&mut len_buf).expect("read challenge len");
430 let frame_len = u32::from_le_bytes(len_buf) as usize;
431 let mut frame_buf = vec![0u8; frame_len];
432 stream
433 .read_exact(&mut frame_buf)
434 .expect("read challenge payload");
435 let response = codec::decode_response(&frame_buf).expect("decode challenge");
436 let nonce = match response {
437 ResponseKind::Challenge { nonce } => nonce,
438 other => panic!("expected Challenge, got {other:?}"),
439 };
440
441 let signature = key.sign(&nonce);
442 let request = Request::ChallengeResponse {
443 signature: signature.to_bytes(),
444 public_key: key.verifying_key().to_bytes(),
445 };
446 let mut encode_buf = [0u8; 256];
447 let written = codec::encode_request(&request, 0, &mut encode_buf).expect("encode");
448 stream.write_all(&encode_buf[..written]).expect("send");
449 stream.flush().expect("flush");
450
451 let mut len_buf2 = [0u8; 4];
452 stream.read_exact(&mut len_buf2).expect("read result len");
453 let result_len = u32::from_le_bytes(len_buf2) as usize;
454 let mut result_buf = vec![0u8; result_len];
455 stream
456 .read_exact(&mut result_buf)
457 .expect("read result payload");
458 codec::decode_response(&result_buf).expect("decode result")
459 }
460
461 fn ephemeral_listener() -> (TcpListener, SocketAddr) {
462 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
463 let addr = listener.local_addr().unwrap();
464 (listener, addr)
465 }
466
467 fn send_command(addr: SocketAddr, key: &SigningKey, command: &[u8]) -> String {
470 let mut stream = TcpStream::connect(addr).unwrap();
471 assert!(matches!(
472 client_authenticate(&mut stream, key),
473 ResponseKind::ServerReady
474 ));
475 stream.write_all(command).unwrap();
476 stream.flush().unwrap();
477 let mut reader = BufReader::new(stream);
478 let mut line = String::new();
479 reader.read_line(&mut line).unwrap();
480 line.trim().to_string()
481 }
482
483 #[test]
484 fn promote_command_sets_flag_when_wired() {
485 let (listener, addr) = ephemeral_listener();
486 drop(listener);
487
488 let (key, auth_keys) = operator_keys();
489 let promote = Arc::new(AtomicBool::new(false));
490 let rotate = Arc::new(AtomicBool::new(false));
491 let shutdown = Arc::new(AtomicBool::new(false));
492 let _h = spawn(
493 addr,
494 Some(Arc::clone(&promote)),
495 Some(Arc::clone(&rotate)),
496 None,
497 Arc::clone(&shutdown),
498 auth_keys,
499 );
500 std::thread::sleep(Duration::from_millis(200));
501
502 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
503 assert!(promote.load(Ordering::Acquire));
504 assert!(!rotate.load(Ordering::Acquire));
505
506 shutdown.store(true, Ordering::Release);
507 }
508
509 #[test]
510 fn rotate_command_sets_flag_when_wired() {
511 let (listener, addr) = ephemeral_listener();
512 drop(listener);
513
514 let (key, auth_keys) = operator_keys();
515 let promote = Arc::new(AtomicBool::new(false));
516 let rotate = Arc::new(AtomicBool::new(false));
517 let shutdown = Arc::new(AtomicBool::new(false));
518 let _h = spawn(
519 addr,
520 Some(Arc::clone(&promote)),
521 Some(Arc::clone(&rotate)),
522 None,
523 Arc::clone(&shutdown),
524 auth_keys,
525 );
526 std::thread::sleep(Duration::from_millis(200));
527
528 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
529 assert!(rotate.load(Ordering::Acquire));
530 assert!(!promote.load(Ordering::Acquire));
531
532 shutdown.store(true, Ordering::Release);
533 }
534
535 #[test]
538 fn promote_rejected_when_not_wired() {
539 let (listener, addr) = ephemeral_listener();
540 drop(listener);
541
542 let (key, auth_keys) = operator_keys();
543 let rotate = Arc::new(AtomicBool::new(false));
544 let shutdown = Arc::new(AtomicBool::new(false));
545 let _h = spawn(
546 addr,
547 None,
548 Some(Arc::clone(&rotate)),
549 None,
550 Arc::clone(&shutdown),
551 auth_keys,
552 );
553 std::thread::sleep(Duration::from_millis(200));
554
555 let resp = send_command(addr, &key, b"PROMOTE\n");
556 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
557 assert!(!rotate.load(Ordering::Acquire));
558
559 shutdown.store(true, Ordering::Release);
560 }
561
562 #[test]
564 fn rotate_rejected_when_not_wired() {
565 let (listener, addr) = ephemeral_listener();
566 drop(listener);
567
568 let (key, auth_keys) = operator_keys();
569 let promote = Arc::new(AtomicBool::new(false));
570 let shutdown = Arc::new(AtomicBool::new(false));
571 let _h = spawn(
572 addr,
573 Some(Arc::clone(&promote)),
574 None,
575 None,
576 Arc::clone(&shutdown),
577 auth_keys,
578 );
579 std::thread::sleep(Duration::from_millis(200));
580
581 let resp = send_command(addr, &key, b"ROTATE\n");
582 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
583 assert!(!promote.load(Ordering::Acquire));
584
585 shutdown.store(true, Ordering::Release);
586 }
587
588 #[test]
592 fn listener_handles_multiple_commands() {
593 let (listener, addr) = ephemeral_listener();
594 drop(listener);
595
596 let (key, auth_keys) = operator_keys();
597 let promote = Arc::new(AtomicBool::new(false));
598 let rotate = Arc::new(AtomicBool::new(false));
599 let shutdown = Arc::new(AtomicBool::new(false));
600 let _h = spawn(
601 addr,
602 Some(Arc::clone(&promote)),
603 Some(Arc::clone(&rotate)),
604 None,
605 Arc::clone(&shutdown),
606 auth_keys,
607 );
608 std::thread::sleep(Duration::from_millis(200));
609
610 for _ in 0..3 {
613 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
614 assert!(rotate.load(Ordering::Acquire));
615 rotate
616 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Relaxed)
617 .expect("flag should still be set");
618 std::thread::sleep(Duration::from_millis(100));
619 }
620
621 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
623 assert!(promote.load(Ordering::Acquire));
624
625 shutdown.store(true, Ordering::Release);
626 }
627
628 #[test]
629 fn unknown_command_rejected() {
630 let (listener, addr) = ephemeral_listener();
631 drop(listener);
632
633 let (key, auth_keys) = operator_keys();
634 let promote = Arc::new(AtomicBool::new(false));
635 let rotate = Arc::new(AtomicBool::new(false));
636 let shutdown = Arc::new(AtomicBool::new(false));
637 let _h = spawn(
638 addr,
639 Some(Arc::clone(&promote)),
640 Some(Arc::clone(&rotate)),
641 None,
642 Arc::clone(&shutdown),
643 auth_keys,
644 );
645 std::thread::sleep(Duration::from_millis(200));
646
647 let resp = send_command(addr, &key, b"INVALID\n");
648 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
649 assert!(!promote.load(Ordering::Acquire));
650 assert!(!rotate.load(Ordering::Acquire));
651
652 shutdown.store(true, Ordering::Release);
653 }
654
655 #[test]
656 fn non_operator_key_rejected() {
657 let (listener, addr) = ephemeral_listener();
658 drop(listener);
659
660 let (trader_key, auth_keys) = trader_keys();
661 let promote = Arc::new(AtomicBool::new(false));
662 let rotate = Arc::new(AtomicBool::new(false));
663 let shutdown = Arc::new(AtomicBool::new(false));
664 let _h = spawn(
665 addr,
666 Some(Arc::clone(&promote)),
667 Some(Arc::clone(&rotate)),
668 None,
669 Arc::clone(&shutdown),
670 auth_keys,
671 );
672 std::thread::sleep(Duration::from_millis(200));
673
674 let mut stream = TcpStream::connect(addr).unwrap();
675 let result = client_authenticate(&mut stream, &trader_key);
676 assert!(matches!(result, ResponseKind::AuthFailed));
677 assert!(!promote.load(Ordering::Acquire));
678 assert!(!rotate.load(Ordering::Acquire));
679
680 shutdown.store(true, Ordering::Release);
681 }
682
683 fn run_durability(initial: DurabilityMode, cmd: &[u8]) -> (String, Option<DurabilityMode>) {
688 let (listener, addr) = ephemeral_listener();
689 drop(listener);
690
691 let (key, auth_keys) = operator_keys();
692 let mode = Arc::new(AtomicU8::new(initial.as_u8()));
693 let shutdown = Arc::new(AtomicBool::new(false));
694 let _h = spawn(
695 addr,
696 None,
697 None,
698 Some(Arc::clone(&mode)),
699 Arc::clone(&shutdown),
700 auth_keys,
701 );
702 std::thread::sleep(Duration::from_millis(200));
703
704 let resp = send_command(addr, &key, cmd);
705 let after = DurabilityMode::from_u8(mode.load(Ordering::Relaxed));
706 shutdown.store(true, Ordering::Release);
707 (resp, after)
708 }
709
710 #[test]
711 fn durability_command_swaps_mode() {
712 let (resp, after) = run_durability(DurabilityMode::Hybrid, b"DURABILITY local\n");
713 assert_eq!(resp, "OK");
714 assert_eq!(after, Some(DurabilityMode::Local));
715 }
716
717 #[test]
718 fn durability_command_accepts_each_mode() {
719 for target in [
720 DurabilityMode::Local,
721 DurabilityMode::Hybrid,
722 DurabilityMode::DurablyReplicated,
723 ] {
724 let cmd = format!("DURABILITY {}\n", target.as_str());
725 let (resp, after) = run_durability(DurabilityMode::Local, cmd.as_bytes());
726 assert_eq!(resp, "OK", "mode {target}");
727 assert_eq!(after, Some(target));
728 }
729 }
730
731 #[test]
732 fn durability_command_rejects_unknown_mode() {
733 let (resp, after) = run_durability(DurabilityMode::Hybrid, b"DURABILITY fast\n");
734 assert!(
735 resp.starts_with("ERR DURABILITY unknown mode"),
736 "expected unknown-mode ERR, got {resp}"
737 );
738 assert_eq!(after, Some(DurabilityMode::Hybrid));
740 }
741
742 #[test]
743 fn durability_command_rejects_missing_argument() {
744 let (resp, after) = run_durability(DurabilityMode::Hybrid, b"DURABILITY\n");
745 assert!(
746 resp.starts_with("ERR DURABILITY requires a mode"),
747 "expected missing-arg ERR, got {resp}"
748 );
749 assert_eq!(after, Some(DurabilityMode::Hybrid));
750 }
751
752 #[test]
753 fn durability_command_rejected_when_not_wired() {
754 let (listener, addr) = ephemeral_listener();
757 drop(listener);
758
759 let (key, auth_keys) = operator_keys();
760 let promote = Arc::new(AtomicBool::new(false));
761 let shutdown = Arc::new(AtomicBool::new(false));
762 let _h = spawn(
763 addr,
764 Some(Arc::clone(&promote)),
765 None,
766 None,
767 Arc::clone(&shutdown),
768 auth_keys,
769 );
770 std::thread::sleep(Duration::from_millis(200));
771
772 let resp = send_command(addr, &key, b"DURABILITY local\n");
773 assert!(
774 resp.starts_with("ERR DURABILITY not available"),
775 "expected not-available ERR, got {resp}"
776 );
777
778 shutdown.store(true, Ordering::Release);
779 }
780}