gate4agent_node_wire/
call_home.rs1use std::time::Duration;
17
18use gate4agent_node_protocol::{
19 read_json_frame_limited_body_timeout, write_json_frame_limited, FrameError,
20 NodeCallHomeAnnounce, NodeId, BUILD_STAMP, MAX_NODE_HELLO_FRAME_BYTES,
21};
22use tokio::io::{AsyncRead, AsyncWrite};
23
24const ANNOUNCE_TIMEOUT_MS: u64 = 2_000;
31
32#[derive(Debug)]
34pub enum CallHomeAnnounceError {
35 TimedOut,
37 Frame(FrameError),
39 BuildStamp { announced: String },
44 InvalidNodeId,
49}
50
51impl std::fmt::Display for CallHomeAnnounceError {
52 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 Self::TimedOut => write!(formatter, "call-home peer did not announce itself in time"),
55 Self::Frame(error) => write!(formatter, "call-home announce frame is invalid: {error}"),
56 Self::BuildStamp { announced } => write!(
57 formatter,
58 "build stamp mismatch: local={BUILD_STAMP} remote={announced}",
59 ),
60 Self::InvalidNodeId => write!(formatter, "call-home announce carried an invalid node id"),
61 }
62 }
63}
64
65impl std::error::Error for CallHomeAnnounceError {}
66
67pub async fn write_call_home_announce<W>(
69 writer: &mut W,
70 node_id: &NodeId,
71) -> Result<(), FrameError>
72where
73 W: AsyncWrite + Unpin,
74{
75 write_json_frame_limited(
76 writer,
77 &NodeCallHomeAnnounce::new(node_id.as_str()),
78 MAX_NODE_HELLO_FRAME_BYTES,
79 )
80 .await
81}
82
83pub async fn read_call_home_announce<R>(reader: &mut R) -> Result<NodeId, CallHomeAnnounceError>
89where
90 R: AsyncRead + Unpin,
91{
92 let announce: NodeCallHomeAnnounce = tokio::time::timeout(
93 Duration::from_millis(ANNOUNCE_TIMEOUT_MS),
94 read_json_frame_limited_body_timeout(
95 reader,
96 MAX_NODE_HELLO_FRAME_BYTES,
97 Duration::from_millis(ANNOUNCE_TIMEOUT_MS),
98 ),
99 )
100 .await
101 .map_err(|_| CallHomeAnnounceError::TimedOut)?
102 .map_err(CallHomeAnnounceError::Frame)?;
103 if announce.build_stamp != BUILD_STAMP {
104 return Err(CallHomeAnnounceError::BuildStamp {
105 announced: announce.build_stamp,
106 });
107 }
108 NodeId::new(announce.node_id).map_err(|_| CallHomeAnnounceError::InvalidNodeId)
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[tokio::test]
119 async fn a_node_names_itself_and_the_relay_reads_the_name() {
120 let node_id = NodeId::new("fixture-node").unwrap();
121 let mut wire = Vec::new();
122 write_call_home_announce(&mut wire, &node_id).await.unwrap();
123 let read = read_call_home_announce(&mut wire.as_slice()).await.unwrap();
124 assert_eq!(read, node_id);
125 }
126
127 #[tokio::test]
131 async fn a_build_stamp_mismatch_names_itself_rather_than_looking_like_garbage() {
132 let foreign_stamp = "f".repeat(BUILD_STAMP.len());
133 let mut wire = Vec::new();
134 write_json_frame_limited(
135 &mut wire,
136 &NodeCallHomeAnnounce {
137 build_stamp: foreign_stamp.clone(),
138 node_id: "fixture-node".to_owned(),
139 },
140 MAX_NODE_HELLO_FRAME_BYTES,
141 )
142 .await
143 .unwrap();
144 let error = read_call_home_announce(&mut wire.as_slice()).await.unwrap_err();
145 assert!(
146 matches!(&error, CallHomeAnnounceError::BuildStamp { announced } if *announced == foreign_stamp),
147 "expected a named build stamp mismatch, got {error:?}",
148 );
149 assert_eq!(
150 error.to_string(),
151 format!("build stamp mismatch: local={BUILD_STAMP} remote={foreign_stamp}"),
152 );
153 }
154
155 #[tokio::test]
157 async fn a_socket_that_says_nothing_is_dropped_rather_than_held() {
158 let (client, mut server) = tokio::io::duplex(64);
159 let error = read_call_home_announce(&mut server).await.unwrap_err();
163 drop(client);
164 assert!(
165 matches!(error, CallHomeAnnounceError::TimedOut),
166 "expected the announce deadline to fire, got {error:?}",
167 );
168 }
169}