1use crc32fast::Hasher;
9
10pub const GHOSTLINK_ETHERTYPE: u16 = 0x88B5;
12
13pub const PROTOCOL_VERSION: u8 = 1;
15
16pub const MAX_PAYLOAD_SIZE: usize = 256;
18
19#[derive(Clone, Copy, Debug)]
21pub struct FrameHeader {
22 pub ether_type: u16,
24 pub kind: u8,
26 pub version: u8,
28 pub crc: u32,
30}
31
32impl FrameHeader {
33 const HEADER_SIZE: usize = 8; pub fn new(ether_type: u16, kind: u8, payload: &[u8]) -> Self {
37 let mut hasher = Hasher::new();
38 hasher.update(payload);
39
40 Self {
41 ether_type,
42 kind,
43 version: PROTOCOL_VERSION,
44 crc: hasher.finalize(),
45 }
46 }
47
48 pub fn encode(&self) -> [u8; Self::HEADER_SIZE] {
50 let mut header = [0u8; Self::HEADER_SIZE];
51 header[0..2].copy_from_slice(&self.ether_type.to_le_bytes());
52 header[2] = self.kind;
53 header[3] = self.version;
54 header[4..].copy_from_slice(&self.crc.to_le_bytes());
55 header
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum FrameKind {
62 Discovery = 1,
64 Join = 2,
66 Attestation = 3,
68 HealthCheck = 4,
70 ResourceAdvert = 5,
72}
73
74impl FrameKind {
75 const fn as_u8(&self) -> u8 {
76 match self {
77 Self::Discovery => 1,
78 Self::Join => 2,
79 Self::Attestation => 3,
80 Self::HealthCheck => 4,
81 Self::ResourceAdvert => 5,
82 }
83 }
84}
85
86impl TryFrom<u8> for FrameKind {
87 type Error = &'static str;
88
89 fn try_from(value: u8) -> Result<Self, Self::Error> {
90 match value {
91 1 => Ok(Self::Discovery),
92 2 => Ok(Self::Join),
93 3 => Ok(Self::Attestation),
94 4 => Ok(Self::HealthCheck),
95 5 => Ok(Self::ResourceAdvert),
96 _ => Err("unknown frame kind"),
97 }
98 }
99}
100
101#[derive(Clone, Debug, Default)]
103pub struct NodeResources {
104 pub id: String,
106 pub vram_gb: f32,
108 pub system_memory_gb: f32,
110 pub compute_capability: String,
112 pub gpu_name: Option<String>,
114}
115
116impl NodeResources {
117 pub fn new(
119 id: impl Into<String>,
120 vram_gb: f32,
121 system_memory_gb: f32,
122 compute_capability: impl Into<String>,
123 gpu_name: Option<String>,
124 ) -> Self {
125 Self {
126 id: id.into(),
127 vram_gb,
128 system_memory_gb,
129 compute_capability: compute_capability.into(),
130 gpu_name,
131 }
132 }
133
134 #[inline]
138 pub fn encode_payload(&self, max_size: usize) -> Vec<u8> {
139 let id_bytes = self.id.as_bytes();
140 let cc_bytes = self.compute_capability.as_bytes();
141 let gpu_bytes = self.gpu_name.as_ref().map(|name| name.as_bytes());
142
143 if id_bytes.len() > u8::MAX as usize || cc_bytes.len() > u8::MAX as usize {
144 return Vec::new();
145 }
146 if let Some(gpu_bytes) = gpu_bytes {
147 if gpu_bytes.len() > u8::MAX as usize {
148 return Vec::new();
149 }
150 }
151
152 let payload_len =
153 11 + id_bytes.len() + cc_bytes.len() + gpu_bytes.map_or(0, |bytes| 2 + bytes.len());
154 if payload_len > max_size {
155 return Vec::new();
156 }
157
158 let mut payload = Vec::with_capacity(payload_len);
159 payload.push(id_bytes.len() as u8);
160 payload.extend_from_slice(id_bytes);
161 payload.extend_from_slice(unsafe {
162 std::slice::from_raw_parts(&self.vram_gb as *const f32 as *const u8, 4)
163 });
164 payload.extend_from_slice(unsafe {
165 std::slice::from_raw_parts(&self.system_memory_gb as *const f32 as *const u8, 4)
166 });
167 payload.push(cc_bytes.len() as u8);
168 payload.extend_from_slice(cc_bytes);
169
170 if let Some(gpu_bytes) = gpu_bytes {
171 payload.push(1);
172 payload.push(gpu_bytes.len() as u8);
173 payload.extend_from_slice(gpu_bytes);
174 } else {
175 payload.push(0);
176 }
177
178 payload
179 }
180
181 pub fn decode_payload(payload: &[u8]) -> Result<Self, String> {
183 if payload.len() < 16 {
184 return Err("payload too short".into());
185 }
186
187 let id_len = payload[0] as usize;
189 if id_len > payload.len() - 4 {
190 return Err("invalid ID length".into());
191 }
192
193 let id = unsafe { std::str::from_utf8_unchecked(&payload[1..1 + id_len]) };
195
196 let vram_bytes: [u8; 4] = payload[1 + id_len..5 + id_len].try_into().unwrap();
198 let vram_gb = f32::from_le_bytes(vram_bytes);
199
200 let mem_bytes: [u8; 4] = payload[5 + id_len..9 + id_len].try_into().unwrap();
202 let system_memory_gb = f32::from_le_bytes(mem_bytes);
203
204 let cc_len = payload[9 + id_len] as usize;
206 if cc_len > payload.len() - 10 {
207 return Err("invalid CC length".into());
208 }
209
210 let cc =
212 unsafe { std::str::from_utf8_unchecked(&payload[10 + id_len..10 + id_len + cc_len]) };
213
214 let has_gpu_name = payload[10 + id_len + cc_len] == 1;
216
217 Ok(Self {
218 id: id.to_string(),
219 vram_gb,
220 system_memory_gb,
221 compute_capability: cc.to_string(),
222 gpu_name: if has_gpu_name {
223 let gpu_len = payload[10 + id_len + cc_len + 1] as usize;
224 if gpu_len > payload.len() - 12 - id_len - cc_len {
225 return Err("invalid GPU name length".into());
226 }
227 Some(
228 unsafe {
229 std::str::from_utf8_unchecked(
230 &payload[10 + id_len + cc_len + 2..10 + id_len + cc_len + 2 + gpu_len],
231 )
232 }
233 .to_string(),
234 )
235 } else {
236 None
237 },
238 })
239 }
240}
241
242#[derive(Clone, Debug)]
244pub struct DiscoveryFrame {
245 pub kind: FrameKind,
246 pub node: NodeResources,
247}
248
249impl DiscoveryFrame {
250 #[inline]
252 pub fn encode(&self) -> Vec<u8> {
253 let payload = self.node.encode_payload(MAX_PAYLOAD_SIZE);
254
255 let mut hasher = Hasher::new();
257 hasher.update(&payload);
258 let crc = hasher.finalize();
259
260 let header = FrameHeader {
262 ether_type: GHOSTLINK_ETHERTYPE,
263 kind: self.kind.as_u8(),
264 version: PROTOCOL_VERSION,
265 crc,
266 };
267 let header_bytes = header.encode();
268
269 let mut frame = Vec::with_capacity(header_bytes.len() + payload.len());
271 frame.extend_from_slice(&header_bytes);
272 frame.extend_from_slice(&payload);
273
274 frame
275 }
276
277 #[inline]
279 pub fn decode(bytes: &[u8]) -> Result<Self, String> {
280 if bytes.len() < FrameHeader::HEADER_SIZE {
281 return Err("frame too short".into());
282 }
283
284 let ether_type = u16::from_le_bytes([bytes[0], bytes[1]]);
286 let kind = FrameKind::try_from(bytes[2])?;
287 let version = bytes[3];
288 let expected_crc = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
289
290 if ether_type != GHOSTLINK_ETHERTYPE {
291 return Err(format!("unexpected EtherType 0x{ether_type:04x}"));
292 }
293
294 if version != PROTOCOL_VERSION {
295 return Err(format!("unsupported protocol version {version}"));
296 }
297
298 let payload_start = FrameHeader::HEADER_SIZE;
300 let payload_end = bytes.len();
301 let payload = &bytes[payload_start..payload_end];
302
303 let mut hasher = Hasher::new();
305 hasher.update(payload);
306 let computed_crc = hasher.finalize();
307
308 if computed_crc != expected_crc {
309 return Err(format!(
310 "CRC mismatch: expected 0x{expected_crc:08x}, got 0x{computed_crc:08x}"
311 ));
312 }
313
314 let node = NodeResources::decode_payload(payload)?;
316
317 Ok(Self { kind, node })
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn discovery_frames_round_trip() {
327 let frame = DiscoveryFrame {
328 kind: FrameKind::Join,
329 node: NodeResources::new("node-b", 12.0, 32.0, "8.6", None),
330 };
331
332 let encoded = frame.encode();
333 let decoded = DiscoveryFrame::decode(&encoded).unwrap();
334
335 assert_eq!(decoded.kind, frame.kind);
336 assert_eq!(decoded.node.id, frame.node.id);
337 assert_eq!(decoded.node.vram_gb, frame.node.vram_gb);
338 }
339
340 #[test]
341 fn crc_verification_fails_on_modified_payload() {
342 let frame = DiscoveryFrame {
343 kind: FrameKind::Discovery,
344 node: NodeResources::new("node-a", 24.0, 64.0, "8.9", Some("RTX4090".to_string())),
345 };
346
347 let encoded = frame.encode();
348 let mut modified = encoded.clone();
349 modified[10] ^= 0xFF; let result = DiscoveryFrame::decode(&modified);
352 assert!(result.is_err());
353 assert!(result.unwrap_err().contains("CRC mismatch"));
354 }
355
356 #[test]
357 fn rejects_wrong_ether_type() {
358 let mut fake_frame = vec![0u8; 10];
359 fake_frame[0] = 0xB5u8; fake_frame[1] = 0xFF; let result = DiscoveryFrame::decode(&fake_frame);
363 assert!(result.is_err());
364 }
365
366 #[test]
367 fn handles_gpu_name_field() {
368 let frame = DiscoveryFrame {
369 kind: FrameKind::Discovery,
370 node: NodeResources::new(
371 "gpu-node-1",
372 24.0,
373 64.0,
374 "9.0",
375 Some("NVIDIA GeForce RTX 4090".to_string()),
376 ),
377 };
378
379 let encoded = frame.encode();
380 let decoded = DiscoveryFrame::decode(&encoded).unwrap();
381
382 assert_eq!(
383 decoded.node.gpu_name,
384 Some("NVIDIA GeForce RTX 4090".to_string())
385 );
386 }
387}