rns_net/interface/
ax25_kiss.rs1use std::collections::HashMap;
4use std::io;
5
6use rns_core::transport::types::InterfaceId;
7
8use super::kiss_iface::{Ax25Address, KissFactory, KissIfaceConfig};
9use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
10
11const HEADER_LEN: usize = 16;
12const CONTROL_UI: u8 = 0x03;
13const PID_NO_LAYER_3: u8 = 0xF0;
14
15pub fn validate_address(callsign: &str, ssid: u8) -> Result<Ax25Address, String> {
16 if !(3..=6).contains(&callsign.len()) || !callsign.is_ascii() {
17 return Err("AX.25 callsign must be 3-6 ASCII characters".into());
18 }
19 if ssid > 15 {
20 return Err("AX.25 SSID must be in 0..15".into());
21 }
22 Ok(Ax25Address {
23 callsign: callsign.to_ascii_uppercase(),
24 ssid,
25 })
26}
27
28fn encode_address(address: &Ax25Address, last: bool) -> [u8; 7] {
29 let mut encoded = [0x20; 7];
32 for (output, input) in encoded[..6].iter_mut().zip(address.callsign.bytes()) {
33 *output = input.to_ascii_uppercase() << 1;
34 }
35 encoded[6] = 0x60 | (address.ssid << 1) | u8::from(last);
36 encoded
37}
38
39pub fn encode_ui_frame(source: &Ax25Address, payload: &[u8]) -> Vec<u8> {
40 let destination = Ax25Address {
41 callsign: "APZRNS".into(),
42 ssid: 0,
43 };
44 let mut frame = Vec::with_capacity(HEADER_LEN + payload.len());
45 frame.extend_from_slice(&encode_address(&destination, false));
46 frame.extend_from_slice(&encode_address(source, true));
47 frame.push(CONTROL_UI);
48 frame.push(PID_NO_LAYER_3);
49 frame.extend_from_slice(payload);
50 frame
51}
52
53pub fn decode_ui_frame(frame: &[u8]) -> Option<&[u8]> {
54 if frame.len() < HEADER_LEN
55 || frame[14] != CONTROL_UI
56 || frame[15] != PID_NO_LAYER_3
57 || frame[6] & 1 != 0
58 || frame[13] & 1 == 0
59 {
60 return None;
61 }
62 let destination: Vec<u8> = frame[..6].iter().map(|byte| byte >> 1).collect();
63 if destination.as_slice() != b"APZRNS" || ((frame[6] >> 1) & 0x0f) != 0 {
64 return None;
65 }
66 Some(&frame[HEADER_LEN..])
67}
68
69pub struct Ax25KissFactory;
70
71impl InterfaceFactory for Ax25KissFactory {
72 fn type_name(&self) -> &str {
73 "AX25KISSInterface"
74 }
75
76 fn default_ifac_size(&self) -> usize {
77 8
78 }
79
80 fn parse_config(
81 &self,
82 name: &str,
83 id: InterfaceId,
84 params: &HashMap<String, String>,
85 ) -> Result<Box<dyn InterfaceConfigData>, String> {
86 let base = KissFactory.parse_config(name, id, params)?;
87 let mut config = *base
88 .into_any()
89 .downcast::<KissIfaceConfig>()
90 .map_err(|_| "wrong KISS config type".to_string())?;
91 let callsign = params
92 .get("callsign")
93 .or_else(|| params.get("source_callsign"))
94 .ok_or_else(|| "AX25KISSInterface requires 'callsign'".to_string())?;
95 let ssid = params
96 .get("ssid")
97 .or_else(|| params.get("source_ssid"))
98 .map(|value| value.parse::<u8>())
99 .transpose()
100 .map_err(|_| "invalid AX.25 SSID".to_string())?
101 .unwrap_or(0);
102 config.ax25_source = Some(validate_address(callsign, ssid)?);
103 config.interface_type_name = self.type_name().into();
104 Ok(Box::new(config))
105 }
106
107 fn start(
108 &self,
109 config: Box<dyn InterfaceConfigData>,
110 ctx: StartContext,
111 ) -> io::Result<StartResult> {
112 KissFactory.start(config, ctx)
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn ui_frame_roundtrip_and_header() {
122 let source = validate_address("N0CALL", 7).unwrap();
123 let frame = encode_ui_frame(&source, b"rns");
124 assert_eq!(&frame[..6], &[0x82, 0xa0, 0xb4, 0xa4, 0x9c, 0xa6]);
125 assert_eq!(frame[14..16], [CONTROL_UI, PID_NO_LAYER_3]);
126 assert_eq!(decode_ui_frame(&frame), Some(b"rns".as_slice()));
127 }
128
129 #[test]
130 fn address_validation_matches_upstream_bounds() {
131 assert!(validate_address("AB", 0).is_err());
132 assert!(validate_address("ABCDEFG", 0).is_err());
133 assert!(validate_address("N0CALL", 16).is_err());
134 assert!(validate_address("N0CALL", 15).is_ok());
135 }
136}