use clap::Parser; use std::{ fs::File, io, path::{Path, PathBuf}, sync::mpsc::{channel, Sender}, thread, }; use anyhow::Result; use ax25::frame::{Ax25Frame, CommandResponse, FrameContent}; use pcap_file::{ pcap::{PcapHeader, PcapPacket, PcapWriter}, DataLink, }; use std::io::Read; use std::net::TcpStream; use std::time::Duration; use std::time::SystemTime; struct KissPkt { timestamp: Duration, orig_len: u32, data: Vec, } fn tnc_loop(tx: Sender, mut stream: T) -> Result<()> { let mut buffer = [0_u8; 4096]; let mut frame = vec![]; let mut escaped = false; let mut timestamp = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let mut orig_len = 0; loop { let n = stream.read(&mut buffer)?; if n == 0 { return Ok(()); } for byte in buffer[..n].iter() { orig_len += 1; if escaped { match *byte { 0xDC => frame.push(0xC0), 0xDD => frame.push(0xDB), _ => { /* ERROR: invalid escape sequence */ } } escaped = false; } else { match *byte { 0xDB => escaped = true, 0xC0 => { //TODO send and clear frame if not empty if frame.len() == 0 { orig_len = 1; timestamp = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); continue; }; tx.send(KissPkt { timestamp, orig_len, data: frame.clone(), })?; frame.clear(); } b => frame.push(b), } } } } } #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Arg { #[arg(short, long, default_value_t = false)] verbose: bool, #[arg(short, long, value_name = "FILE")] output: Option, input: String, } fn main() -> Result<()> { let arg = Arg::parse(); let mut file = arg.output.map(|path| { //TODO automatically add file extension let file_out = File::create(path).expect("Error opening file for writing"); let header = PcapHeader { datalink: DataLink::AX25_KISS, ..Default::default() }; PcapWriter::with_header(file_out, header).expect("Error writing file") }); let (tx, rx) = channel(); match arg.input.as_str() { "-" => { let file = io::BufReader::new(io::stdin()); thread::spawn(move || tnc_loop(tx, file).unwrap()); } s if Path::new(s).exists() => { let file = io::BufReader::new(File::open(s)?); thread::spawn(move || tnc_loop(tx, file).unwrap()); } addr => { let stream = TcpStream::connect(addr)?; let strm = stream.try_clone()?; thread::spawn(move || tnc_loop(tx, strm).unwrap()); } } while let Ok(pkt) = rx.recv() { //let time = SystemTime::now(); //let timestamp = time.duration_since(SystemTime::UNIX_EPOCH); let mut s = String::new(); for byte in pkt.data.iter() { //if byte >= ' '.into() && byte <= '~'.into() { if *byte >= 32 && *byte <= 126 { s.push((*byte).into()); } else { s.push_str(&format!("\\{:x?} ", *byte)); } } if arg.verbose { if let Ok(frame) = Ax25Frame::from_bytes(&pkt.data[1..]) { frame.print_long(); } else { //TODO print time stamp println!("{}", s); } } else { if let Ok(frame) = Ax25Frame::from_bytes(&pkt.data[1..]) { frame.print_short(); } else { //TODO print time stamp println!("{}", s); } } if let Some(ref mut writer) = file { let pkt = PcapPacket::new_owned(pkt.timestamp, pkt.orig_len, pkt.data); writer.write_packet(&pkt).unwrap(); } } Ok(()) } trait PrintData { fn type_str_short(&self) -> &'static str; fn print_short(&self); fn print_long(&self); } impl PrintData for Ax25Frame { fn type_str_short(&self) -> &'static str { match self.content { FrameContent::Information(_) => "Info", FrameContent::ReceiveReady(_) => "RecvRdy", FrameContent::ReceiveNotReady(_) => "RecvNotRdy", FrameContent::Reject(_) => "Reject", FrameContent::SetAsynchronousBalancedMode(_) => "AsyncBalMode", //12 long FrameContent::Disconnect(_) => "Disconnect", FrameContent::DisconnectedMode(_) => "DisconMode", FrameContent::UnnumberedAcknowledge(_) => "UnNumAck", FrameContent::FrameReject(_) => "FrameReject", FrameContent::UnnumberedInformation(_) => "UnNumInfo", FrameContent::UnknownContent(_) => "Unknown", } } fn print_short(&self) { //Format: "[source] -> [dest] via [..] [CMD/RSP/NIL] [frameType]: [DATA str rep]" let src = self.source.to_string(); let dst = self.destination.to_string(); //let via: String = self.route.iter().map(|re| format!(" {} ", re.repeater.to_string())).collect(); //todo let rsp = match self.command_or_response { Some(CommandResponse::Command) => "CMD", Some(CommandResponse::Response) => "RSP", None => "NIL", }; let ftype = self.type_str_short(); let mut routed = false; for route in self.route.iter() { if route.has_repeated { routed = true; break; } } let arrow_shaft = if routed { "=" } else { "-" }; //let mut rawdata; let (rawdata, finfo) = match &self.content { FrameContent::Information(_i) => ( _i.info.clone(), format!( "pid: {:?}, seq: {}/{}, info: ", _i.pid, _i.send_sequence, _i.receive_sequence ), ), FrameContent::ReceiveReady(rr) => ( vec![], format!( "seq: {}, poll/final: {}", rr.receive_sequence, rr.poll_or_final ), ), FrameContent::ReceiveNotReady(rnr) => ( vec![], format!( "seq: {}, poll/final: {}", rnr.receive_sequence, rnr.poll_or_final ), ), FrameContent::Reject(rnr) => ( vec![], format!( "seq: {}, poll/final: {}", rnr.receive_sequence, rnr.poll_or_final ), ), FrameContent::SetAsynchronousBalancedMode(abm) => { (vec![], format!("poll: {}", abm.poll)) } FrameContent::Disconnect(abm) => (vec![], format!("poll: {}", abm.poll)), FrameContent::DisconnectedMode(abm) => { (vec![], format!("final bit: {}", abm.final_bit)) } FrameContent::UnnumberedAcknowledge(abm) => { (vec![], format!("final bit: {}", abm.final_bit)) } FrameContent::FrameReject(fr) => ( vec![], format!( "[{}{}{}{}], seq: {}/{}, raw: {}, cmdRsp: {}, final: {}", if fr.z { "Z" } else { "" }, if fr.y { "Y" } else { "" }, if fr.x { "X" } else { "" }, if fr.w { "W" } else { "" }, fr.send_sequence, fr.receive_sequence, fr.rejected_control_field_raw, match fr.command_response { CommandResponse::Command => "CMD", CommandResponse::Response => "RSP", }, fr.final_bit ), ), FrameContent::UnnumberedInformation(ui) => { (ui.info.clone(), format!("pid: {:?} info:", ui.pid)) } FrameContent::UnknownContent(ukn) => (ukn.raw.clone(), format!("")), }; let mut data = String::new(); for byte in rawdata.iter() { //if byte >= ' '.into() && byte <= '~'.into() { if *byte >= 32 && *byte <= 126 { data.push((*byte).into()); } else { data.push_str(&format!("\\{:x?} ", *byte)); } } println!( "AX.25 {:12} {} {:9} {}> {:9} {}", ftype, rsp, src, arrow_shaft, dst, if !data.is_empty() { data } else { finfo } ); } fn print_long(&self) { //Format: "[source] -> [dest] via [..] [CMD/RSP/NIL] [frameType]: [DATA str rep]" let src = self.source.to_string(); let dst = self.destination.to_string(); let via: String = self .route .iter() .map(|re| format!(" {} ", re.repeater.to_string())) .collect(); //todo let rsp = match self.command_or_response { Some(CommandResponse::Command) => "CMD", Some(CommandResponse::Response) => "RSP", None => "NIL", }; let ftype = self.type_str_short(); //let mut rawdata; let (rawdata, finfo) = match &self.content { FrameContent::Information(_i) => ( _i.info.clone(), format!( "pid: {:?}, seq: {}/{}, info: ", _i.pid, _i.send_sequence, _i.receive_sequence ), ), FrameContent::ReceiveReady(rr) => ( vec![], format!( "seq: {}, poll/final: {}", rr.receive_sequence, rr.poll_or_final ), ), FrameContent::ReceiveNotReady(rnr) => ( vec![], format!( "seq: {}, poll/final: {}", rnr.receive_sequence, rnr.poll_or_final ), ), FrameContent::Reject(rnr) => ( vec![], format!( "seq: {}, poll/final: {}", rnr.receive_sequence, rnr.poll_or_final ), ), FrameContent::SetAsynchronousBalancedMode(abm) => { (vec![], format!("poll: {}", abm.poll)) } FrameContent::Disconnect(abm) => (vec![], format!("poll: {}", abm.poll)), FrameContent::DisconnectedMode(abm) => { (vec![], format!("final bit: {}", abm.final_bit)) } FrameContent::UnnumberedAcknowledge(abm) => { (vec![], format!("final bit: {}", abm.final_bit)) } FrameContent::FrameReject(fr) => ( vec![], format!( "[{}{}{}{}], seq: {}/{}, raw: {}, cmdRsp: {}, final: {}", if fr.z { "Z" } else { "" }, if fr.y { "Y" } else { "" }, if fr.x { "X" } else { "" }, if fr.w { "W" } else { "" }, fr.send_sequence, fr.receive_sequence, fr.rejected_control_field_raw, match fr.command_response { CommandResponse::Command => "CMD", CommandResponse::Response => "RSP", }, fr.final_bit ), ), FrameContent::UnnumberedInformation(ui) => { (ui.info.clone(), format!("pid: {:?} info:", ui.pid)) } FrameContent::UnknownContent(ukn) => (ukn.raw.clone(), format!("")), }; let mut data = String::new(); for byte in rawdata.iter() { //if byte >= ' '.into() && byte <= '~'.into() { if *byte >= 32 && *byte <= 126 { data.push((*byte).into()); } else { data.push_str(&format!("\\{:x?} ", *byte)); } } println!( "AX.25 {:12} {} {:9} -> {:9} via [{}] {} {}", ftype, rsp, src, dst, via, finfo, data ); } }