diff --git a/common/Cargo.toml b/common/Cargo.toml index 16e39e1..7dbd723 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -6,3 +6,39 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +vnt = { path = "../vnt", package = "vnt", default-features = false } +anyhow = "1.0.82" +console = "0.15.2" +log = "0.4.17" +log4rs = { version = "1.3.0", optional = true } + +serde = "1.0" +serde_yaml = "0.9.32" +getopts = "0.2.21" +gethostname = "0.4.3" +uuid = {version = "1.8.0",features = ["v4"]} + +[features] +default = [] +openssl = ["vnt/openssl"] +openssl-vendored = ["vnt/openssl-vendored"] +ring-cipher = ["vnt/ring-cipher"] +aes_cbc = ["vnt/aes_cbc"] +aes_ecb = ["vnt/aes_ecb"] +sm4_cbc = ["vnt/sm4_cbc"] +aes_gcm = ["vnt/aes_gcm"] +chacha20_poly1305 = ["vnt/chacha20_poly1305"] +server_encrypt = ["vnt/server_encrypt"] +ip_proxy = ["vnt/ip_proxy"] +port_mapping = ["vnt/port_mapping"] +lz4 = ["vnt/lz4_compress"] +zstd = ["vnt/zstd_compress"] + +command = [] +file_config = [] +log = ["log4rs"] +integrated_tun = ["vnt/integrated_tun"] + +[build-dependencies] +rand = "0.8.5" +chrono = "0.4.23" \ No newline at end of file diff --git a/common/build.rs b/common/build.rs new file mode 100644 index 0000000..8970ed9 --- /dev/null +++ b/common/build.rs @@ -0,0 +1,16 @@ +use rand::Rng; +use std::fs::File; +use std::io::Write; + +fn main() { + let now_time = chrono::Local::now(); + let serial_number = format!( + "{}-{}", + &now_time.format("%y%m%d%H%M").to_string(), + rand::thread_rng().gen_range(100..1000) + ); + let generated_code = format!(r#"pub const SERIAL_NUMBER: &str = "{}";"#, serial_number); + let dest_path = "src/generated_serial_number.rs"; + let mut file = File::create(&dest_path).unwrap(); + file.write_all(generated_code.as_bytes()).unwrap(); +} diff --git a/common/src/callback.rs b/common/src/callback.rs new file mode 100644 index 0000000..a1801a3 --- /dev/null +++ b/common/src/callback.rs @@ -0,0 +1,51 @@ +use std::process; + +use console::style; +use vnt::{ConnectInfo, ErrorInfo, ErrorType, HandshakeInfo, RegisterInfo, VntCallback}; + +#[derive(Clone)] +pub struct VntHandler {} + +impl VntCallback for VntHandler { + fn success(&self) { + println!(" {} ", style("====== Connect Successfully ======").green()) + } + #[cfg(feature = "vnt-model")] + fn create_tun(&self, info: vnt::DeviceInfo) { + println!("create_tun {}", info) + } + + fn connect(&self, info: ConnectInfo) { + println!("connect {}", info) + } + + fn handshake(&self, info: HandshakeInfo) -> bool { + println!("handshake {}", info); + true + } + + fn register(&self, info: RegisterInfo) -> bool { + println!("register {}", style(info).green()); + true + } + + fn error(&self, info: ErrorInfo) { + log::error!("error {:?}", info); + println!("{}", style(format!("error {}", info)).red()); + match info.code { + ErrorType::TokenError + | ErrorType::AddressExhausted + | ErrorType::IpAlreadyExists + | ErrorType::InvalidIp + | ErrorType::LocalIpExists => { + self.stop(); + } + _ => {} + } + } + + fn stop(&self) { + println!("stopped"); + process::exit(0) + } +} diff --git a/common/src/cli.rs b/common/src/cli.rs new file mode 100644 index 0000000..27917cc --- /dev/null +++ b/common/src/cli.rs @@ -0,0 +1,463 @@ +use crate::args_parse::{ips_parse, out_ips_parse}; +#[cfg(feature = "command")] +use crate::command; +use crate::{config, generated_serial_number}; +use anyhow::anyhow; +use console::style; +use getopts::Options; +use std::io; +use std::net::Ipv4Addr; +use std::path::PathBuf; +use std::str::FromStr; +use vnt::channel::punch::PunchModel; +use vnt::channel::UseChannelType; +use vnt::cipher::CipherModel; +use vnt::compression::Compressor; +use vnt::core::Config; + +pub fn app_home() -> io::Result { + let root_path = match std::env::current_exe() { + Ok(path) => { + if let Some(v) = path.as_path().parent() { + v.to_path_buf() + } else { + log::warn!("current_exe parent none:{:?}", path); + PathBuf::new() + } + } + Err(e) => { + log::warn!("current_exe err:{:?}", e); + PathBuf::new() + } + }; + let path = root_path.join("env"); + if !path.exists() { + std::fs::create_dir_all(&path)?; + } + Ok(path) +} + +pub fn parse_args_config() -> anyhow::Result, bool)>> { + #[cfg(feature = "log")] + let _ = log4rs::init_file("log4rs.yaml", Default::default()); + let args: Vec = std::env::args().collect(); + let program = args[0].clone(); + let mut opts = Options::new(); + opts.optopt("k", "", "组网标识", ""); + opts.optopt("n", "", "设备名称", ""); + opts.optopt("d", "", "设备标识", ""); + opts.optflag("c", "", "关闭交互式命令"); + opts.optopt("s", "", "注册和中继服务器地址", ""); + opts.optmulti("e", "", "stun服务器", ""); + opts.optflag("a", "", "使用tap模式"); + opts.optopt("", "nic", "虚拟网卡名称,windows下使用tap则必填", ""); + opts.optmulti("i", "", "配置点对网(IP代理)入站时使用", ""); + opts.optmulti("o", "", "配置点对网出站时使用", ""); + opts.optopt("w", "", "客户端加密", ""); + opts.optflag("W", "", "服务端加密"); + opts.optopt("u", "", "自定义mtu(默认为1430)", ""); + opts.optflag("", "tcp", "tcp"); + opts.optopt("", "ip", "指定虚拟ip", ""); + opts.optflag("", "relay", "仅使用服务器转发"); + opts.optopt("", "par", "任务并行度(必须为正整数)", ""); + opts.optopt("", "model", "加密模式", ""); + opts.optflag("", "finger", "指纹校验"); + opts.optopt("", "punch", "取值ipv4/ipv6", ""); + opts.optopt("", "ports", "监听的端口", ""); + opts.optflag("", "cmd", "开启窗口输入"); + opts.optflag("", "no-proxy", "关闭内置代理"); + opts.optflag("", "first-latency", "优先延迟"); + opts.optopt("", "use-channel", "使用通道 relay/p2p", ""); + opts.optopt("", "packet-loss", "丢包率", ""); + opts.optopt("", "packet-delay", "延迟", ""); + opts.optmulti("", "dns", "dns", ""); + opts.optmulti("", "mapping", "mapping", ""); + opts.optmulti("", "vnt-mapping", "vnt-mapping", ""); + opts.optopt("f", "", "配置文件", ""); + opts.optopt("", "compressor", "压缩算法", ""); + //"后台运行时,查看其他设备列表" + opts.optflag("", "add", "后台运行时,添加地址"); + opts.optflag("", "list", "后台运行时,查看其他设备列表"); + opts.optflag("", "all", "后台运行时,查看其他设备完整信息"); + opts.optflag("", "info", "后台运行时,查看当前设备信息"); + opts.optflag("", "route", "后台运行时,查看数据转发路径"); + opts.optflag("", "stop", "停止后台运行"); + opts.optflag("h", "help", "帮助"); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => { + print_usage(&program, opts); + return Err(anyhow::anyhow!("{}", f.to_string())); + } + }; + if matches.opt_present("h") || args.len() == 1 { + print_usage(&program, opts); + return Ok(None); + } + + #[cfg(feature = "command")] + if matches.opt_present("list") { + command::command(command::CommandEnum::List); + return Ok(None); + } else if matches.opt_present("info") { + command::command(command::CommandEnum::Info); + return Ok(None); + } else if matches.opt_present("stop") { + command::command(command::CommandEnum::Stop); + return Ok(None); + } else if matches.opt_present("route") { + command::command(command::CommandEnum::Route); + return Ok(None); + } else if matches.opt_present("all") { + command::command(command::CommandEnum::All); + return Ok(None); + } + let conf = matches.opt_str("f"); + let (config, vnt_link_config, cmd) = if conf.is_some() { + match config::read_config(&conf.unwrap()) { + Ok(c) => c, + Err(e) => { + return Err(anyhow::anyhow!("conf err {}", e)); + } + } + } else { + if !matches.opt_present("k") { + print_usage(&program, opts); + return Err(anyhow::anyhow!("parameter -k not found .")); + } + #[cfg(target_os = "windows")] + #[cfg(feature = "integrated_tun")] + let tap = matches.opt_present("a"); + #[cfg(feature = "integrated_tun")] + let device_name = matches.opt_str("nic"); + let token: String = matches.opt_get("k").unwrap().unwrap(); + let device_id = matches.opt_get_default("d", String::new()).unwrap(); + let device_id = if device_id.is_empty() { + config::get_device_id() + } else { + device_id + }; + if device_id.is_empty() { + print_usage(&program, opts); + return Err(anyhow::anyhow!("parameter -d not found .")); + } + let name = matches + .opt_get_default( + "n", + gethostname::gethostname() + .to_str() + .unwrap_or("UnknownName") + .to_string(), + ) + .unwrap(); + let server_address_str = matches + .opt_get_default("s", "vnt.wherewego.top:29872".to_string()) + .unwrap(); + + let mut stun_server = matches.opt_strs("e"); + if stun_server.is_empty() { + stun_server.push("stun1.l.google.com:19302".to_string()); + stun_server.push("stun2.l.google.com:19302".to_string()); + stun_server.push("stun.miwifi.com:3478".to_string()); + } + let dns = matches.opt_strs("dns"); + let in_ip = matches.opt_strs("i"); + let in_ip = match ips_parse(&in_ip) { + Ok(in_ip) => in_ip, + Err(e) => { + print_usage(&program, opts); + println!(); + println!("-i: {:?} {}", in_ip, e); + return Err(anyhow::anyhow!("example: -i 192.168.0.0/24,10.26.0.3")); + } + }; + let out_ip = matches.opt_strs("o"); + let out_ip = match out_ips_parse(&out_ip) { + Ok(out_ip) => out_ip, + Err(e) => { + print_usage(&program, opts); + println!(); + println!("-o: {:?} {}", out_ip, e); + return Err(anyhow::anyhow!("example: -o 0.0.0.0/0")); + } + }; + let password: Option = matches.opt_get("w").unwrap(); + let server_encrypt = matches.opt_present("W"); + #[cfg(not(feature = "server_encrypt"))] + { + if server_encrypt { + println!("Server encryption not supported"); + return Err(anyhow::anyhow!("Server encryption not supported")); + } + } + let mtu: Option = matches.opt_get("u").unwrap(); + let mtu = if let Some(mtu) = mtu { + match u32::from_str(&mtu) { + Ok(mtu) => Some(mtu), + Err(e) => { + print_usage(&program, opts); + println!(); + println!("'-u {}' {}", mtu, e); + return Err(anyhow::anyhow!("'-u {}' {}", mtu, e)); + } + } + } else { + None + }; + let virtual_ip: Option = matches.opt_get("ip").unwrap(); + let virtual_ip = + virtual_ip.map(|v| Ipv4Addr::from_str(&v).expect(&format!("'--ip {}' error", v))); + if let Some(virtual_ip) = virtual_ip { + if virtual_ip.is_unspecified() || virtual_ip.is_broadcast() || virtual_ip.is_multicast() + { + return Err(anyhow::anyhow!("'--ip {}' invalid", virtual_ip)); + } + } + let tcp_channel = matches.opt_present("tcp"); + let relay = matches.opt_present("relay"); + + let cipher_model = match matches.opt_get::("model") { + Ok(model) => { + #[cfg(not(any(feature = "aes_gcm", feature = "server_encrypt")))] + { + if password.is_some() && model.is_none() { + return Err(anyhow::anyhow!("'--model ' undefined")); + } + model.unwrap_or(CipherModel::None) + } + #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] + model.unwrap_or(CipherModel::AesGcm) + } + Err(e) => { + return Err(anyhow::anyhow!("'--model ' invalid,{}", e)); + } + }; + + let finger = matches.opt_present("finger"); + let punch_model = matches + .opt_get::("punch") + .unwrap() + .unwrap_or(PunchModel::All); + let use_channel_type = matches + .opt_get::("use-channel") + .unwrap() + .unwrap_or_else(|| { + if relay { + UseChannelType::Relay + } else { + UseChannelType::All + } + }); + + let ports = matches + .opt_get::("ports") + .unwrap_or(None) + .map(|v| v.split(",").map(|x| x.parse().unwrap_or(0)).collect()); + + let cmd = matches.opt_present("cmd"); + #[cfg(feature = "ip_proxy")] + #[cfg(feature = "integrated_tun")] + let no_proxy = matches.opt_present("no-proxy"); + let first_latency = matches.opt_present("first-latency"); + let packet_loss = matches + .opt_get::("packet-loss") + .expect("--packet-loss"); + let packet_delay = matches + .opt_get::("packet-delay") + .expect("--packet-delay") + .unwrap_or(0); + #[cfg(feature = "port_mapping")] + let port_mapping_list = matches.opt_strs("mapping"); + let vnt_mapping_list = matches.opt_strs("vnt-mapping"); + let compressor = if let Some(compressor) = matches.opt_str("compressor").as_ref() { + Compressor::from_str(compressor) + .map_err(|e| anyhow!("{}", e)) + .unwrap() + } else { + Compressor::None + }; + let config = match Config::new( + #[cfg(feature = "integrated_tun")] + #[cfg(target_os = "windows")] + tap, + token, + device_id, + name, + server_address_str, + dns, + stun_server, + in_ip, + out_ip, + password, + mtu, + tcp_channel, + virtual_ip, + #[cfg(feature = "integrated_tun")] + #[cfg(feature = "ip_proxy")] + no_proxy, + server_encrypt, + cipher_model, + finger, + punch_model, + ports, + first_latency, + #[cfg(feature = "integrated_tun")] + device_name, + use_channel_type, + packet_loss, + packet_delay, + #[cfg(feature = "port_mapping")] + port_mapping_list, + compressor, + ) { + Ok(config) => config, + Err(e) => { + println!("config.toml error: {}", e); + std::process::exit(1); + } + }; + (config, vnt_mapping_list, cmd) + }; + println!("version {}", vnt::VNT_VERSION); + println!("Serial:{}", generated_serial_number::SERIAL_NUMBER); + log::info!( + "version:{},Serial:{}", + vnt::VNT_VERSION, + generated_serial_number::SERIAL_NUMBER + ); + Ok(Some((config, vnt_link_config, cmd))) +} + +fn print_usage(program: &str, _opts: Options) { + println!("Usage: {} [options]", program); + println!("version:{}", vnt::VNT_VERSION); + println!("Serial:{}", generated_serial_number::SERIAL_NUMBER); + println!("Options:"); + println!( + " -k {}", + green("使用相同的token,就能组建一个局域网络".to_string()) + ); + println!(" -n 给设备一个名字,便于区分不同设备,默认使用系统版本"); + println!(" -d 设备唯一标识符,不使用--ip参数时,服务端凭此参数分配虚拟ip,注意不能重复"); + println!(" -s 注册和中继服务器地址,以'TXT:'开头表示解析TXT记录"); + println!(" -e stun服务器,用于探测NAT类型,可使用多个地址,如-e stun1.l.google.com -e stun2.l.google.com"); + #[cfg(target_os = "windows")] + #[cfg(feature = "integrated_tun")] + println!( + " -a 使用tap模式,默认使用tun模式,使用tap时需要配合'--nic'参数指定tap网卡" + ); + println!(" -i 配置点对网(IP代理)时使用,-i 192.168.0.0/24,10.26.0.3表示允许接收网段192.168.0.0/24的数据"); + println!(" 并转发到10.26.0.3,可指定多个网段"); + println!(" -o 配置点对网时使用,-o 192.168.0.0/24表示允许将数据转发到192.168.0.0/24,可指定多个网段"); + + println!(" -w 使用该密码生成的密钥对客户端数据进行加密,并且服务端无法解密,使用相同密码的客户端才能通信"); + #[cfg(feature = "server_encrypt")] + println!(" -W 加密当前客户端和服务端通信的数据,请留意服务端指纹是否正确"); + println!(" -u 自定义mtu(不加密默认为1450,加密默认为1410)"); + #[cfg(feature = "file_config")] + println!(" -f 读取配置文件中的配置"); + + println!(" --tcp 和服务端使用tcp通信,默认使用udp,遇到udp qos时可指定使用tcp"); + println!(" --ip 指定虚拟ip,指定的ip不能和其他设备重复,必须有效并且在服务端所属网段下,默认情况由服务端分配"); + let mut enums = String::new(); + #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] + enums.push_str("/aes_gcm"); + #[cfg(feature = "chacha20_poly1305")] + enums.push_str("/chacha20_poly1305/chacha20"); + #[cfg(feature = "aes_cbc")] + enums.push_str("/aes_cbc"); + #[cfg(feature = "aes_ecb")] + enums.push_str("/aes_ecb"); + #[cfg(feature = "sm4_cbc")] + enums.push_str("/sm4_cbc"); + enums.push_str("/xor"); + println!( + " --model 加密模式(默认aes_gcm),可选值{}", + &enums[1..] + ); + #[cfg(any( + feature = "aes_gcm", + feature = "chacha20_poly1305", + feature = "server_encrypt", + feature = "aes_cbc", + feature = "aes_ecb", + feature = "sm4_cbc" + ))] + println!(" --finger 增加数据指纹校验,可增加安全性,如果服务端开启指纹校验,则客户端也必须开启"); + println!(" --punch 取值ipv4/ipv6/all,ipv4表示仅使用ipv4打洞"); + println!(" --ports 取值0~65535,指定本地监听的一组端口,默认监听两个随机端口,使用过多端口会增加网络负担"); + #[cfg(feature = "command")] + println!(" --cmd 开启交互式命令,使用此参数开启控制台输入"); + #[cfg(feature = "ip_proxy")] + #[cfg(feature = "integrated_tun")] + println!(" --no-proxy 关闭内置代理,如需点对网则需要配置网卡NAT转发"); + println!(" --first-latency 优先低延迟的通道,默认情况优先使用p2p通道"); + println!(" --use-channel 使用通道 relay/p2p/all,默认两者都使用"); + #[cfg(not(feature = "vn-link-model"))] + println!(" --nic 指定虚拟网卡名称"); + println!(" --packet-loss <0> 模拟丢包,取值0~1之间的小数,程序会按设定的概率主动丢包,可用于模拟弱网"); + println!( + " --packet-delay <0> 模拟延迟,整数,单位毫秒(ms),程序会按设定的值延迟发包,可用于模拟弱网" + ); + println!(" --dns DNS服务器地址,可使用多个dns,不指定时使用系统解析"); + + #[cfg(feature = "port_mapping")] + println!(" --mapping 端口映射,例如 --mapping udp:0.0.0.0:80-domain:80 映射目标是本地路由能访问的设备"); + + #[cfg(all(feature = "lz4", feature = "zstd"))] + println!(" --compressor 启用压缩,可选值lz4/zstd<,level>,level为压缩级别,例如 --compressor lz4 或--compressor zstd,10"); + #[cfg(feature = "lz4")] + #[cfg(not(feature = "zstd"))] + println!(" --compressor 启用压缩,可选值lz4,例如 --compressor lz4"); + #[cfg(feature = "zstd")] + #[cfg(not(feature = "lz4"))] + println!(" --compressor 启用压缩,可选值zstd<,level>,level为压缩级别,例如 --compressor zstd,10"); + + #[cfg(not(feature = "integrated_tun"))] + println!( + " --vnt-mapping {}", + green( + "vnt地址映射,例如 --vnt-mapping tcp:80-10.26.0.10:80 映射目标是vnt网络或其子网中的设备" + .to_string() + ) + ); + println!(); + #[cfg(feature = "command")] + { + // #[cfg(not(feature = "integrated_tun"))] + // println!( + // " --add {}", + // yellow("后台运行时,添加VNT地址映射 用法同'--vnt-mapping'".to_string()) + // ); + println!( + " --list {}", + yellow("后台运行时,查看其他设备列表".to_string()) + ); + println!( + " --all {}", + yellow("后台运行时,查看其他设备完整信息".to_string()) + ); + println!( + " --info {}", + yellow("后台运行时,查看当前设备信息".to_string()) + ); + println!( + " --route {}", + yellow("后台运行时,查看数据转发路径".to_string()) + ); + println!( + " --stop {}", + yellow("停止后台运行".to_string()) + ); + } + println!(" -h, --help 帮助"); +} + +fn green(str: String) -> impl std::fmt::Display { + style(str).green() +} + +#[cfg(feature = "command")] +fn yellow(str: String) -> impl std::fmt::Display { + style(str).yellow() +} diff --git a/common/src/command/client.rs b/common/src/command/client.rs new file mode 100644 index 0000000..8143fb6 --- /dev/null +++ b/common/src/command/client.rs @@ -0,0 +1,81 @@ +use serde::Deserialize; +use std::io; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}; +use std::str::FromStr; +use std::time::Duration; + +use crate::command::entity::{DeviceItem, Info, RouteItem}; + +pub struct CommandClient { + buf: Vec, + udp: UdpSocket, +} + +impl CommandClient { + pub fn new() -> io::Result { + let port = read_command_port().unwrap_or_else(|e| { + log::warn!("read_command_port:{:?}", e); + 39271 + }); + let udp = UdpSocket::bind("127.0.0.1:0")?; + udp.set_read_timeout(Some(Duration::from_secs(5)))?; + udp.connect(SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::new(127, 0, 0, 1), + port, + )))?; + Ok(Self { + udp, + buf: vec![0; 65536 * 8], + }) + } +} +fn read_command_port() -> io::Result { + let path_buf = crate::cli::app_home()?.join("command-port"); + let port = std::fs::read_to_string(path_buf)?; + match u16::from_str(&port) { + Ok(port) => Ok(port), + Err(_) => { + return Err(io::Error::new( + io::ErrorKind::Other, + "'command-port' file error", + )); + } + } +} + +impl CommandClient { + pub fn list(&mut self) -> io::Result> { + self.send_cmd(b"list") + } + pub fn route(&mut self) -> io::Result> { + self.send_cmd(b"route") + } + pub fn info(&mut self) -> io::Result { + self.send_cmd(b"info") + } + fn send_cmd<'a, V: Deserialize<'a>>(&'a mut self, cmd: &[u8]) -> io::Result { + self.udp.send(cmd)?; + let len = self.udp.recv(&mut self.buf)?; + match serde_yaml::from_slice::(&self.buf[..len]) { + Ok(val) => Ok(val), + Err(e) => { + log::error!( + "send_cmd {:?} {:?},{:?}", + std::str::from_utf8(cmd), + std::str::from_utf8(&self.buf[..len]), + e + ); + Err(io::Error::new( + io::ErrorKind::Other, + format!("data error {:?} buf_len={}", e, len), + )) + } + } + } + pub fn stop(&self) -> io::Result { + self.udp.send(b"stop")?; + let mut buf = [0; 10240]; + let len = self.udp.recv(&mut buf)?; + Ok(String::from_utf8(buf[..len].to_vec()).unwrap()) + } +} diff --git a/common/src/command/entity.rs b/common/src/command/entity.rs new file mode 100644 index 0000000..5c06d18 --- /dev/null +++ b/common/src/command/entity.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; +use std::net::{Ipv4Addr, SocketAddr}; +#[derive(Serialize, Deserialize, Debug)] +pub struct Info { + pub name: String, + pub virtual_ip: String, + pub virtual_gateway: String, + pub virtual_netmask: String, + pub connect_status: String, + pub relay_server: String, + pub nat_type: String, + pub public_ips: String, + pub local_addr: String, + pub ipv6_addr: String, + pub up: u64, + pub down: u64, + pub port_mapping_list: Vec<(bool, SocketAddr, String)>, + pub in_ips: Vec<(u32, u32, Ipv4Addr)>, + pub out_ips: Vec<(u32, u32)>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct RouteItem { + pub destination: String, + pub next_hop: String, + pub metric: String, + pub rt: String, + pub interface: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DeviceItem { + pub name: String, + pub virtual_ip: String, + pub nat_type: String, + pub public_ips: String, + pub local_ip: String, + pub ipv6: String, + pub nat_traversal_type: String, + pub rt: String, + pub status: String, + pub client_secret: bool, + pub client_secret_hash: Vec, + pub current_client_secret: bool, + pub current_client_secret_hash: Vec, +} diff --git a/common/src/command/mod.rs b/common/src/command/mod.rs new file mode 100644 index 0000000..069e622 --- /dev/null +++ b/common/src/command/mod.rs @@ -0,0 +1,242 @@ +use std::io; +use vnt::core::Vnt; + +use crate::command::entity::{DeviceItem, Info, RouteItem}; +use crate::console_out; + +pub mod client; +pub mod entity; +pub mod server; + +pub enum CommandEnum { + Route, + List, + All, + Info, + Stop, +} + +pub fn command_str(cmd: &str, vnt: &Vnt) -> bool { + if cmd.is_empty() { + return false; + } + match cmd.to_lowercase().trim() { + "list" => { + let list = command_list(&vnt); + console_out::console_device_list(list); + } + "info" => { + let info = command_info(&vnt); + console_out::console_info(info); + } + "route" => { + let route = command_route(&vnt); + console_out::console_route_table(route); + } + "all" => { + let list = command_list(&vnt); + console_out::console_device_list_all(list); + } + "stop" => { + let _ = vnt.stop(); + return false; + } + _ => {} + } + println!(); + return true; +} + +pub fn command(cmd: CommandEnum) { + if let Err(e) = command_(cmd) { + println!("cmd: {:?}", e); + } +} + +fn command_(cmd: CommandEnum) -> io::Result<()> { + let mut command_client = client::CommandClient::new()?; + match cmd { + CommandEnum::Route => { + let list = command_client.route()?; + console_out::console_route_table(list); + } + CommandEnum::List => { + let list = command_client.list()?; + console_out::console_device_list(list); + } + CommandEnum::All => { + let list = command_client.list()?; + console_out::console_device_list_all(list); + } + CommandEnum::Info => { + let info = command_client.info()?; + console_out::console_info(info); + } + CommandEnum::Stop => { + command_client.stop()?; + } + } + Ok(()) +} + +pub fn command_route(vnt: &Vnt) -> Vec { + let route_table = vnt.route_table(); + let mut route_list = Vec::with_capacity(route_table.len()); + for (destination, routes) in route_table { + for route in routes { + let next_hop = vnt + .route_key(&route.route_key()) + .map_or(String::new(), |v| v.to_string()); + let metric = route.metric.to_string(); + let rt = if route.rt < 0 { + "".to_string() + } else { + route.rt.to_string() + }; + let interface = if route.is_tcp { + format!("tcp@{}", route.addr) + } else { + route.addr.to_string() + }; + let item = RouteItem { + destination: destination.to_string(), + next_hop, + metric, + rt, + interface, + }; + route_list.push(item); + } + } + route_list +} + +pub fn command_list(vnt: &Vnt) -> Vec { + let info = vnt.current_device(); + let device_list = vnt.device_list(); + let mut list = Vec::new(); + let current_client_secret = vnt.client_encrypt(); + let client_encrypt_hash = vnt.client_encrypt_hash().unwrap_or(&[]); + for peer in device_list { + let name = peer.name; + let virtual_ip = peer.virtual_ip.to_string(); + let (nat_type, public_ips, local_ip, ipv6) = + if let Some(nat_info) = vnt.peer_nat_info(&peer.virtual_ip) { + let nat_type = format!("{:?}", nat_info.nat_type); + let public_ips: Vec = + nat_info.public_ips.iter().map(|v| v.to_string()).collect(); + let public_ips = public_ips.join(","); + let local_ip = nat_info + .local_ipv4() + .map(|v| v.to_string()) + .unwrap_or("None".to_string()); + let ipv6 = nat_info + .ipv6() + .map(|v| v.to_string()) + .unwrap_or("None".to_string()); + (nat_type, public_ips, local_ip, ipv6) + } else { + ( + "".to_string(), + "".to_string(), + "".to_string(), + "".to_string(), + ) + }; + let (nat_traversal_type, rt) = if let Some(route) = vnt.route(&peer.virtual_ip) { + let nat_traversal_type = if route.metric == 1 { + if route.is_tcp { + "tcp-p2p" + } else { + "p2p" + } + } else { + let next_hop = vnt.route_key(&route.route_key()); + if let Some(next_hop) = next_hop { + if info.is_gateway(&next_hop) { + "server-relay" + } else { + "client-relay" + } + } else { + "server-relay" + } + } + .to_string(); + let rt = if route.rt < 0 { + "".to_string() + } else { + route.rt.to_string() + }; + (nat_traversal_type, rt) + } else { + ("relay".to_string(), "".to_string()) + }; + let status = format!("{:?}", peer.status); + let client_secret = peer.client_secret; + let item = DeviceItem { + name, + virtual_ip, + nat_type, + public_ips, + local_ip, + ipv6, + nat_traversal_type, + rt, + status, + client_secret, + client_secret_hash: peer.client_secret_hash, + current_client_secret, + current_client_secret_hash: client_encrypt_hash.to_vec(), + }; + list.push(item); + } + list +} + +pub fn command_info(vnt: &Vnt) -> Info { + let current_device = vnt.current_device(); + let nat_info = vnt.nat_info(); + let name = vnt.name().to_string(); + let virtual_ip = current_device.virtual_ip().to_string(); + let virtual_gateway = current_device.virtual_gateway().to_string(); + let virtual_netmask = current_device.virtual_netmask.to_string(); + let connect_status = format!("{:?}", vnt.connection_status()); + let relay_server = current_device.connect_server.to_string(); + let nat_type = format!("{:?}", nat_info.nat_type); + let public_ips: Vec = nat_info.public_ips.iter().map(|v| v.to_string()).collect(); + let public_ips = public_ips.join(","); + let local_addr = nat_info + .local_ipv4() + .map(|v| v.to_string()) + .unwrap_or("None".to_string()); + let ipv6_addr = nat_info + .ipv6() + .map(|v| v.to_string()) + .unwrap_or("None".to_string()); + let up = vnt.up_stream(); + let down = vnt.down_stream(); + #[cfg(feature = "port_mapping")] + let port_mapping_list = vnt.config().port_mapping_list.clone(); + #[cfg(not(feature = "port_mapping"))] + let port_mapping_list = vec![]; + let in_ips = vnt.config().in_ips.clone(); + let out_ips = vnt.config().out_ips.clone(); + Info { + name, + virtual_ip, + virtual_gateway, + virtual_netmask, + connect_status, + relay_server, + nat_type, + public_ips, + local_addr, + ipv6_addr, + up, + down, + port_mapping_list, + in_ips, + out_ips, + } +} diff --git a/common/src/command/server.rs b/common/src/command/server.rs new file mode 100644 index 0000000..3ac61a2 --- /dev/null +++ b/common/src/command/server.rs @@ -0,0 +1,77 @@ +use std::io; +use std::io::Write; +use std::net::UdpSocket; +use vnt::core::Vnt; + +pub struct CommandServer {} + +impl CommandServer { + pub fn new() -> Self { + Self {} + } +} + +impl CommandServer { + pub fn start(self, vnt: Vnt) -> io::Result<()> { + let udp = if let Ok(udp) = UdpSocket::bind("127.0.0.1:39271") { + udp + } else { + UdpSocket::bind("127.0.0.1:0")? + }; + let addr = udp.local_addr()?; + log::info!("启动后台cmd:{:?}", addr); + if let Err(e) = save_port(addr.port()) { + log::warn!("保存后台命令端口失败:{:?}", e); + } + + let mut buf = [0u8; 64]; + loop { + let (len, addr) = udp.recv_from(&mut buf)?; + match std::str::from_utf8(&buf[..len]) { + Ok(cmd) => { + if let Ok(out) = command(cmd, &vnt) { + if let Err(e) = udp.send_to(out.as_bytes(), addr) { + log::warn!("cmd={},err={:?}", cmd, e); + } + if "stopped" == &out { + break; + } + } + } + Err(e) => { + log::warn!("{:?}", e); + } + } + } + Ok(()) + } +} +fn save_port(port: u16) -> io::Result<()> { + let path_buf = crate::cli::app_home()?.join("command-port"); + let mut file = std::fs::File::create(path_buf)?; + file.write_all(port.to_string().as_bytes())?; + file.sync_all() +} + +fn command(cmd: &str, vnt: &Vnt) -> io::Result { + let cmd = cmd.trim(); + let out_str = match cmd { + "route" => serde_yaml::to_string(&crate::command::command_route(vnt)) + .unwrap_or_else(|e| format!("error {:?}", e)), + "list" => serde_yaml::to_string(&crate::command::command_list(vnt)) + .unwrap_or_else(|e| format!("error {:?}", e)), + "info" => serde_yaml::to_string(&crate::command::command_info(vnt)) + .unwrap_or_else(|e| format!("error {:?}", e)), + "stop" => { + vnt.stop(); + "stopped".to_string() + } + _ => { + format!( + "command '{}' not found. Try to enter: 'route'/'list'/'stop' \n", + cmd + ) + } + }; + Ok(out_str) +} diff --git a/common/src/config/file_config.rs b/common/src/config/file_config.rs new file mode 100644 index 0000000..ad88861 --- /dev/null +++ b/common/src/config/file_config.rs @@ -0,0 +1,181 @@ +use anyhow::anyhow; +use std::net::Ipv4Addr; +use std::str::FromStr; + +use crate::args_parse; +use crate::config::get_device_id; +use serde::{Deserialize, Serialize}; +use vnt::channel::punch::PunchModel; +use vnt::channel::UseChannelType; +use vnt::cipher::CipherModel; +use vnt::compression::Compressor; +use vnt::core::Config; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(default)] +pub struct FileConfig { + #[cfg(target_os = "windows")] + pub tap: bool, + pub token: String, + pub device_id: String, + pub name: String, + pub server_address: String, + pub stun_server: Vec, + pub dns: Vec, + pub in_ips: Vec, + pub out_ips: Vec, + pub password: Option, + pub mtu: Option, + pub tcp: bool, + pub ip: Option, + pub use_channel: String, + #[cfg(feature = "ip_proxy")] + pub no_proxy: bool, + pub server_encrypt: bool, + pub cipher_model: Option, + pub finger: bool, + pub punch_model: String, + pub ports: Option>, + pub cmd: bool, + pub first_latency: bool, + pub device_name: Option, + pub packet_loss: Option, + pub packet_delay: u32, + #[cfg(feature = "port_mapping")] + pub mapping: Vec, + pub compressor: Option, + pub vnt_mapping: Vec, +} + +impl Default for FileConfig { + fn default() -> Self { + Self { + #[cfg(target_os = "windows")] + tap: false, + token: "".to_string(), + device_id: get_device_id(), + name: gethostname::gethostname() + .to_str() + .unwrap_or("UnknownName") + .to_string(), + server_address: "nat1.wherewego.top:29872".to_string(), + stun_server: vec![ + "stun1.l.google.com:19302".to_string(), + "stun2.l.google.com:19302".to_string(), + "stun.miwifi.com:3478".to_string(), + ], + dns: vec![], + in_ips: vec![], + out_ips: vec![], + password: None, + mtu: None, + tcp: false, + ip: None, + use_channel: "all".to_string(), + #[cfg(feature = "ip_proxy")] + no_proxy: false, + server_encrypt: false, + cipher_model: None, + finger: false, + punch_model: "all".to_string(), + ports: None, + cmd: false, + first_latency: false, + device_name: None, + packet_loss: None, + packet_delay: 0, + #[cfg(feature = "port_mapping")] + mapping: vec![], + compressor: None, + vnt_mapping: vec![], + } + } +} + +pub fn read_config(file_path: &str) -> anyhow::Result<(Config, Vec, bool)> { + let conf = std::fs::read_to_string(file_path)?; + let file_conf = match serde_yaml::from_str::(&conf) { + Ok(val) => val, + Err(e) => { + log::error!("{:?}", e); + return Err(anyhow!("{}", e)); + } + }; + if file_conf.token.is_empty() { + return Err(anyhow!("token is_empty")); + } + + let in_ips = match args_parse::ips_parse(&file_conf.in_ips) { + Ok(in_ips) => in_ips, + Err(e) => { + return Err(anyhow!("in_ips {:?} error:{}", &file_conf.in_ips, e)); + } + }; + let out_ips = match args_parse::out_ips_parse(&file_conf.out_ips) { + Ok(out_ips) => out_ips, + Err(e) => { + return Err(anyhow!("out_ips {:?} error:{}", &file_conf.out_ips, e)); + } + }; + let virtual_ip = match file_conf.ip.clone().map(|v| Ipv4Addr::from_str(&v)) { + None => None, + Some(r) => Some(r.map_err(|e| anyhow!("ip {:?} error:{}", &file_conf.ip, e))?), + }; + let cipher_model = { + #[cfg(not(any(feature = "aes_gcm", feature = "server_encrypt")))] + if file_conf.password.is_some() && file_conf.cipher_model.is_none() { + Err(anyhow!("cipher_model undefined"))? + } else if let Some(v) = file_conf.cipher_model { + CipherModel::from_str(&v).map_err(|e| anyhow!("{}", e))? + } else { + CipherModel::None + } + #[cfg(any(feature = "aes_gcm", feature = "server_encrypt"))] + CipherModel::AesGcm + }; + + let punch_model = PunchModel::from_str(&file_conf.punch_model).map_err(|e| anyhow!("{}", e))?; + let use_channel_type = + UseChannelType::from_str(&file_conf.use_channel).map_err(|e| anyhow!("{}", e))?; + let compressor = if let Some(compressor) = file_conf.compressor.as_ref() { + Compressor::from_str(compressor).map_err(|e| anyhow!("{}", e))? + } else { + Compressor::None + }; + let config = Config::new( + #[cfg(target_os = "windows")] + #[cfg(feature = "integrated_tun")] + file_conf.tap, + file_conf.token, + file_conf.device_id, + file_conf.name, + file_conf.server_address, + file_conf.dns, + file_conf.stun_server, + in_ips, + out_ips, + file_conf.password, + file_conf.mtu, + file_conf.tcp, + virtual_ip, + #[cfg(feature = "integrated_tun")] + #[cfg(feature = "ip_proxy")] + file_conf.no_proxy, + file_conf.server_encrypt, + cipher_model, + file_conf.finger, + punch_model, + file_conf.ports, + file_conf.first_latency, + #[cfg(feature = "integrated_tun")] + file_conf.device_name, + use_channel_type, + file_conf.packet_loss, + file_conf.packet_delay, + #[cfg(feature = "port_mapping")] + file_conf.mapping, + compressor, + )?; + + Ok((config, file_conf.vnt_mapping, file_conf.cmd)) +} diff --git a/common/src/config/mod.rs b/common/src/config/mod.rs new file mode 100644 index 0000000..048b91e --- /dev/null +++ b/common/src/config/mod.rs @@ -0,0 +1,32 @@ +#[cfg(feature = "file_config")] +mod file_config; + +use crate::identifier; +#[cfg(feature = "file_config")] +pub use file_config::read_config; + +#[cfg(not(feature = "file_config"))] +pub fn read_config(_file_path: &str) -> anyhow::Result<(vnt::core::Config, Vec, bool)> { + unimplemented!() +} + +pub fn get_device_id() -> String { + if let Some(id) = identifier::get_unique_identifier() { + id + } else { + let path_buf = match crate::cli::app_home() { + Ok(path_buf) => path_buf.join("device-id"), + Err(e) => { + log::warn!("{:?}", e); + return String::new(); + } + }; + if let Ok(id) = std::fs::read_to_string(path_buf.as_path()) { + id + } else { + let id = uuid::Uuid::new_v4().to_string(); + let _ = std::fs::write(path_buf, &id); + id + } + } +} diff --git a/common/src/console_out/mod.rs b/common/src/console_out/mod.rs new file mode 100644 index 0000000..44bde16 --- /dev/null +++ b/common/src/console_out/mod.rs @@ -0,0 +1,239 @@ +use console::{style, Style}; +use std::net::Ipv4Addr; + +use crate::command::entity::{DeviceItem, Info, RouteItem}; + +pub mod table; + +pub fn console_info(status: Info) { + println!("Name: {}", style(status.name).green()); + println!("Virtual ip: {}", style(status.virtual_ip).green()); + println!("Virtual gateway: {}", style(status.virtual_gateway).green()); + println!("Virtual netmask: {}", style(status.virtual_netmask).green()); + if status.connect_status.eq_ignore_ascii_case("Connected") { + println!( + "Connection status: {}", + style(status.connect_status).green() + ); + } else { + println!("Connection status: {}", style(status.connect_status).red()); + } + + println!("NAT type: {}", style(status.nat_type).green()); + println!("Relay server: {}", style(status.relay_server).green()); + println!("Public ips: {}", style(status.public_ips).green()); + println!("Local addr: {}", style(status.local_addr).green()); + println!("IPv6: {}", style(status.ipv6_addr).green()); + println!("Up: {}", style(convert(status.up)).green()); + println!("Down: {}", style(convert(status.down)).green()); + + if !status.port_mapping_list.is_empty() { + println!("------------------------------------------"); + println!("Port mapping {}", status.port_mapping_list.len()); + for (is_tcp, addr, dest) in status.port_mapping_list { + if is_tcp { + println!(" TCP: {} -> {}", addr, dest) + } else { + println!(" UDP: {} -> {}", addr, dest) + } + } + } + if !status.in_ips.is_empty() || !status.out_ips.is_empty() { + println!("------------------------------------------"); + } + if !status.in_ips.is_empty() { + println!("IP forwarding {}", status.in_ips.len()); + for (dest, mask, ip) in status.in_ips { + println!( + " -- {} --> {}/{}", + ip, + Ipv4Addr::from(dest), + mask.count_ones() + ) + } + } + if !status.out_ips.is_empty() { + println!("Allows network {}", status.out_ips.len()); + for (dest, mask) in status.out_ips { + println!(" {}/{}", Ipv4Addr::from(dest), mask.count_ones()) + } + } +} + +fn convert(num: u64) -> String { + let gigabytes = num / (1024 * 1024 * 1024); + let remaining_bytes = num % (1024 * 1024 * 1024); + let megabytes = remaining_bytes / (1024 * 1024); + let remaining_bytes = remaining_bytes % (1024 * 1024); + let kilobytes = remaining_bytes / 1024; + let remaining_bytes = remaining_bytes % 1024; + let mut s = String::new(); + if gigabytes > 0 { + s.push_str(&format!("{} GB ", gigabytes)); + } + if megabytes > 0 { + s.push_str(&format!("{} MB ", megabytes)); + } + if kilobytes > 0 { + s.push_str(&format!("{} KB ", kilobytes)); + } + if remaining_bytes > 0 { + s.push_str(&format!("{} bytes", remaining_bytes)); + } + s +} + +pub fn console_route_table(mut list: Vec) { + if list.is_empty() { + println!("No route found"); + return; + } + list.sort_by(|t1, t2| t1.destination.cmp(&t2.destination)); + let mut out_list = Vec::with_capacity(list.len()); + + out_list.push(vec![ + ("Destination".to_string(), Style::new()), + ("Next Hop".to_string(), Style::new()), + ("Metric".to_string(), Style::new()), + ("Rt".to_string(), Style::new()), + ("Interface".to_string(), Style::new()), + ]); + for item in list { + out_list.push(vec![ + (item.destination, Style::new().green()), + (item.next_hop, Style::new().green()), + (item.metric, Style::new().green()), + (item.rt, Style::new().green()), + (item.interface, Style::new().green()), + ]); + } + + table::println_table(out_list) +} + +pub fn console_device_list(mut list: Vec) { + if list.is_empty() { + println!("No other devices found"); + return; + } + list.sort_by(|t1, t2| t1.virtual_ip.cmp(&t2.virtual_ip)); + list.sort_by(|t1, t2| t1.status.cmp(&t2.status)); + let mut out_list = Vec::with_capacity(list.len()); + //表头 + out_list.push(vec![ + ("Name".to_string(), Style::new()), + ("Virtual Ip".to_string(), Style::new()), + ("Status".to_string(), Style::new()), + ("P2P/Relay".to_string(), Style::new()), + ("Rt".to_string(), Style::new()), + ]); + for item in list { + if &item.status == "Online" { + if item.client_secret != item.current_client_secret + || (!item.current_client_secret_hash.is_empty() + && !item.client_secret_hash.is_empty() + && item.current_client_secret_hash != item.client_secret_hash) + { + //加密状态不一致,无法通信的 + out_list.push(vec![ + (item.name, Style::new().red()), + (item.virtual_ip, Style::new().red()), + (item.status, Style::new().red()), + ("Mismatch".to_string(), Style::new().red()), + ("".to_string(), Style::new().red()), + ]); + } else { + if item.nat_traversal_type.contains("p2p") { + out_list.push(vec![ + (item.name, Style::new().green()), + (item.virtual_ip, Style::new().green()), + (item.status, Style::new().green()), + (item.nat_traversal_type, Style::new().green()), + (item.rt, Style::new().green()), + ]); + } else { + out_list.push(vec![ + (item.name, Style::new().yellow()), + (item.virtual_ip, Style::new().yellow()), + (item.status, Style::new().yellow()), + (item.nat_traversal_type, Style::new().yellow()), + (item.rt, Style::new().yellow()), + ]); + } + } + } else { + out_list.push(vec![ + (item.name, Style::new().color256(102)), + (item.virtual_ip, Style::new().color256(102)), + (item.status, Style::new().color256(102)), + ("".to_string(), Style::new().color256(102)), + ("".to_string(), Style::new().color256(102)), + ]); + } + } + table::println_table(out_list) +} + +pub fn console_device_list_all(mut list: Vec) { + if list.is_empty() { + println!("No other devices found"); + return; + } + list.sort_by(|t1, t2| t1.virtual_ip.cmp(&t2.virtual_ip)); + list.sort_by(|t1, t2| t1.status.cmp(&t2.status)); + let mut out_list = Vec::with_capacity(list.len()); + //表头 + out_list.push(vec![ + ("Name".to_string(), Style::new()), + ("Virtual Ip".to_string(), Style::new()), + ("Status".to_string(), Style::new()), + ("P2P/Relay".to_string(), Style::new()), + ("Rt".to_string(), Style::new()), + ("NAT Type".to_string(), Style::new()), + ("Public Ips".to_string(), Style::new()), + ("Local Ip".to_string(), Style::new()), + ("IPv6".to_string(), Style::new()), + ]); + for item in list { + if &item.status == "Online" { + if &item.nat_traversal_type == "p2p" { + out_list.push(vec![ + (item.name, Style::new().green()), + (item.virtual_ip, Style::new().green()), + (item.status, Style::new().green()), + (item.nat_traversal_type, Style::new().green()), + (item.rt, Style::new().green()), + (item.nat_type, Style::new().green()), + (item.public_ips, Style::new().green()), + (item.local_ip, Style::new().green()), + (item.ipv6, Style::new().green()), + ]); + } else { + out_list.push(vec![ + (item.name, Style::new().yellow()), + (item.virtual_ip, Style::new().yellow()), + (item.status, Style::new().yellow()), + (item.nat_traversal_type, Style::new().yellow()), + (item.rt, Style::new().yellow()), + (item.nat_type, Style::new().yellow()), + (item.public_ips, Style::new().yellow()), + (item.local_ip, Style::new().yellow()), + (item.ipv6, Style::new().yellow()), + ]); + } + } else { + out_list.push(vec![ + (item.name, Style::new().color256(102)), + (item.virtual_ip, Style::new().color256(102)), + (item.status, Style::new().color256(102)), + ("".to_string(), Style::new().color256(102)), + ("".to_string(), Style::new().color256(102)), + ("".to_string(), Style::new().color256(102)), + ("".to_string(), Style::new().color256(102)), + ("".to_string(), Style::new().color256(102)), + ("".to_string(), Style::new().color256(102)), + ]); + } + } + table::println_table(out_list) +} diff --git a/common/src/console_out/table.rs b/common/src/console_out/table.rs new file mode 100644 index 0000000..aacc006 --- /dev/null +++ b/common/src/console_out/table.rs @@ -0,0 +1,23 @@ +use console::Style; + +pub fn println_table(table: Vec>) { + if table.is_empty() { + return; + } + let mut width_list = vec![0; table[0].len()]; + for in_list in table.iter() { + for (index, (item, _)) in in_list.iter().enumerate() { + let width = console::measure_text_width(item) + 4; + if width_list[index] < width { + width_list[index] = width; + } + } + } + for in_list in table { + for (col, (item, style)) in in_list.iter().enumerate() { + let str = format!("{:1$}", item, width_list[col]); + print!("{}", style.apply_to(str)); + } + println!() + } +} diff --git a/common/src/generated_serial_number.rs b/common/src/generated_serial_number.rs new file mode 100644 index 0000000..4e3bcad --- /dev/null +++ b/common/src/generated_serial_number.rs @@ -0,0 +1 @@ +pub const SERIAL_NUMBER: &str = "2406151201-282"; diff --git a/common/src/lib.rs b/common/src/lib.rs index 7ce4c80..d218387 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,2 +1,12 @@ pub mod args_parse; +#[cfg(feature = "command")] +pub mod command; +pub mod config; +#[cfg(feature = "command")] +mod console_out; pub mod identifier; + +pub mod cli; +mod generated_serial_number; + +pub mod callback;