commit 020739397c9a9f5e002bcbd9b1f4b6446fb60762 Author: Lucas Schumacher Date: Sat Sep 23 14:17:32 2023 -0400 initial ver diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54f259a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +*.pcap diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..95d0390 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,306 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "ax25" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fec6189a57a9668a17aa3368d110b1451450df47b85200f43d93357fb21f3b" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "byteorder_slice" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b294e30387378958e8bf8f4242131b930ea615ff81e8cac2440cea0a6013190" +dependencies = [ + "byteorder", +] + +[[package]] +name = "clap" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "derive-into-owned" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d94d81e3819a7b06a8638f448bc6339371ca9b6076a99d4a43eece3c4c923" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "kissdump" +version = "0.1.0" +dependencies = [ + "anyhow", + "ax25", + "clap", + "pcap-file", +] + +[[package]] +name = "pcap-file" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc1f139757b058f9f37b76c48501799d12c9aa0aa4c0d4c980b062ee925d1b2" +dependencies = [ + "byteorder_slice", + "derive-into-owned", + "thiserror", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0ca50dd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "kissdump" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.75" +ax25 = "0.3.0" +clap = { version = "4.4.2", features = ["derive"] } +pcap-file = "2.0.0" diff --git a/examples/raw2pcap.rs b/examples/raw2pcap.rs new file mode 100644 index 0000000..4c9f0dc --- /dev/null +++ b/examples/raw2pcap.rs @@ -0,0 +1,251 @@ +use clap::Parser; +use std::{ + fs::File, + path::PathBuf, +}; +use std::io::Read; + +use anyhow::Result; +use pcap_file::{DataLink, pcap::{PcapPacket, PcapWriter, PcapHeader}}; +use std::time::SystemTime; + +use ax25::frame::{CommandResponse, Ax25Frame, FrameContent}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Arg { + #[arg(short, long, default_value_t = false)] + verbose: bool, + infile: PathBuf, + outfile: PathBuf, +} +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 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, 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); + } +} + +fn main() -> Result<()> { + let mut information = 0; + let mut receive_ready = 0; + let mut receive_not_ready = 0; + let mut reject = 0; + let mut set_asynchronous_balanced_mode = 0; + let mut disconnect = 0; + let mut disconnected_mode = 0; + let mut unnumbered_acknowledge = 0; + let mut frame_reject = 0; + let mut unnumbered_information = 0; + let mut unknown_content = 0; + + let arg = Arg::parse(); + + let verbose = arg.verbose; + + let indisplay = arg.infile.display(); + let mut file_in = match File::open(&arg.infile) { + Err(why) => panic!("couldn't open {}: {}", indisplay, why), + Ok(file) => file, + }; + + let file_out = File::create(&arg.outfile).expect("Error opening file for writing"); + let header = PcapHeader{datalink: DataLink::AX25_KISS, ..Default::default()}; + let mut out_file = PcapWriter::with_header(file_out, header).expect("Error writing file"); + + + let mut buffer = vec![0_u8; 4096]; + let mut rawframe = vec![]; + let mut escaped = false; + let mut timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let mut orig_len = 0; + // while let Ok(n) = file_in.by_ref().take(4096).read_to_end(&mut buffer) { + while let Ok(n) = file_in.read(&mut buffer) { + if n <= 0 {break;} + for byte in buffer[..n].iter() { + orig_len += 1; + if escaped { + match *byte { + 0xDC => rawframe.push(0xC0), + 0xDD => rawframe.push(0xDB), + _ => { /* ERROR: invalid escape sequence */ } + } + escaped = false; + } else { + match *byte { + 0xDB => escaped = true, + 0xC0 => { + //TODO send and clear frame if not empty + if rawframe.len() == 0 { + orig_len = 1; + timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + continue; + }; + + if verbose { + if let Ok(frame) = Ax25Frame::from_bytes(&rawframe[1..]) { + match frame.content { + FrameContent::Information(_) => information += 1, + FrameContent::ReceiveReady(_) => receive_ready += 1, + FrameContent::ReceiveNotReady(_) => receive_not_ready += 1, + FrameContent::Reject(_) => reject += 1, + FrameContent::SetAsynchronousBalancedMode(_) => set_asynchronous_balanced_mode += 1, + FrameContent::Disconnect(_) => disconnect += 1, + FrameContent::DisconnectedMode(_) => disconnected_mode += 1, + FrameContent::UnnumberedAcknowledge(_) => unnumbered_acknowledge += 1, + FrameContent::FrameReject(_) => frame_reject += 1, + FrameContent::UnnumberedInformation(_) => unnumbered_information += 1, + FrameContent::UnknownContent(_) => unknown_content +=1, + } + frame.print_short(); + //println!("{{source: {}, dest: {}, route: {:?}, cmd/rsp: {:?}, content: {:?}}}", frame.source, frame.destination, frame.route, frame.command_or_response, frame.content); + //println!("{}\n", frame.info_string_lossy().unwrap_or("Could not parse AX.25 frame data".to_string())); + }else { + let mut s = String::new(); + for byte in rawframe.iter() { + //if byte >= ' '.into() && byte <= '~'.into() { + if *byte >= 32 && *byte <= 126 { + s.push((*byte).into()); + } else { + s.push_str(&format!("\\{:x?} ", *byte)); + } + } + //TODO print time stamp + println!("{}", s); + } + } + + let pkt = PcapPacket::new_owned(timestamp, orig_len, rawframe.clone()); + out_file.write_packet(&pkt).unwrap(); + + //tx.send(KissPkt{ timestamp, orig_len, data: frame.clone()})?; + rawframe.clear(); + orig_len = 1; + timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + } + b => rawframe.push(b), + } + } + } + } + + println!("\n---Frame type counts---"); + println!(" Info: {}", information); + println!(" RecvRdy: {}", receive_ready); + println!(" RecvNotRdy: {}", receive_not_ready); + println!(" Reject: {}", reject); + println!(" AsyncBalMode: {}", set_asynchronous_balanced_mode); + println!(" Disconnect: {}", disconnect); + println!(" DisconMode: {}", disconnected_mode); + println!(" UnNumAck: {}", unnumbered_acknowledge); + println!(" FrameReject: {}", frame_reject); + println!(" UnNumInfo: {}", unnumbered_information); + println!(" Unknown: {}", unknown_content); + + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..39ebe6b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,358 @@ +use clap::Parser; +use std::{ + fs::File, + 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: TcpStream) -> 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)?; + 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), + } + } + } + } + //Ok(()) +} +const DEFAULT_ADDR: &str = "127.0.0.1:8001"; +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Arg { + #[arg(short, long, default_value_t = false)] + verbose: bool, + #[arg(short, long)] + file: Option, + address: Option, +} + +fn main() -> Result<()> { + let arg = Arg::parse(); + let address = arg.address.unwrap_or(DEFAULT_ADDR.to_string()); + let mut file = arg.file.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") + }); + //println!("file: {:?}", arg.file); + //println!("address: {}", address); + + let (tx, rx) = channel(); + let stream = TcpStream::connect(address)?; + + let q = tx.clone(); + let strm = stream.try_clone()?; + thread::spawn(move || tnc_loop(q, 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 + ); + } +}