Skip to main content

rns_net/interface/
ax25_kiss.rs

1//! AX.25 UI framing carried over a KISS TNC.
2
3use 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    // Preserve Python 1.3.8's 0x20 padding byte (including its non-standard
30    // unshifted representation) for exact wire compatibility.
31    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}