diff --git a/Cargo.lock b/Cargo.lock index 28a14f2..c0eba7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "configparser" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06821ea598337a8412cf47c5b71c3bc694a7f0aed188ac28b836fab164a2c202" + [[package]] name = "directories" version = "4.0.1" @@ -76,6 +82,7 @@ dependencies = [ name = "lookaround" version = "0.1.6" dependencies = [ + "configparser", "directories", "mac_address", "rand", diff --git a/Cargo.toml b/Cargo.toml index 1aae2fd..c6e9d2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ repository = "https://six-five-six-four.com/git/reactor/lookaround" version = "0.1.6" [dependencies] +configparser = "3.0.0" directories = "4.0.1" mac_address = "1.1.2" rand = "0.8.4" diff --git a/README.md b/README.md index 32f755b..11ec0e7 100644 --- a/README.md +++ b/README.md @@ -19,29 +19,48 @@ Found 3 peers: LookAround is a Rust program for looking up your computers' MAC and IP addresses within a LAN. There's no central server, so it's not a look-up, it's a look-around. -The client uses IP multicast to find servers within the -same multicast domain, similar to Avahi and Bonjour. +## Installing -Systems self-identify by MAC address and nicknames. Public keys with -TOFU semantics are intended before v1.0.0. - -## Installation - -Use the Cargo package manager from [Rust](https://rustup.rs/) to install LookAround. +Make sure Cargo is installed from [RustUp.](https://rustup.rs/) ```bash +# Install LookAround with Cargo cargo install lookaround + +# Find your config directory +# Prints something like `Using config dir "/home/user/.config/lookaround"` +lookaround config ``` -To auto-start the server as a normal user. -put this systemd unit in `~/.config/systemd/user/lookaround.service`: +Create the files `client.ini` and/or `server.ini` in that directory +(e.g. /home/user/.config/lookaround/server.ini) + +```ini +# Clients can store MAC-nickname pairs in client.ini, like a hosts file. +# This is useful if your servers are short-lived and you want the clients +# to be the source of truth for nicknames. +[nicknames] +11-11-11-11-11-11 = laptop +22-22-22-22-22-22 = desktop +``` + +```ini +# Long-lived servers can have their nickname configured in server.ini +[server] +nickname = my-computer +``` + +## Auto-Start (Linux) + +Put this systemd unit in `~/.config/systemd/user/lookaround.service`: ```ini [Unit] Description=LookAround [Service] -ExecStart=/home/user/.cargo/bin/lookaround server --nickname my-desktop +ExecStart=/home/user/.cargo/bin/lookaround server +Restart=always [Install] WantedBy=default.target @@ -56,11 +75,19 @@ systemctl --user status lookaround systemctl --user enable lookaround ``` +## Auto-Start (Windows) + +(untested) + +- Create a shortcut to the LookAround exe +- Change the shortcut's target to end in `lookaround.exe server` so it will run the server +- Cut-paste the shortcut into the Startup folder in `C:\ProgramData\somewhere` + ## Usage -Run the server manually: (If you didn't configure auto-start) +Run the server manually: (To test before installing) ```bash -lookaround server --nickname my-desktop +lookaround server --nickname my-computer ``` On a client computer: diff --git a/src/app_common.rs b/src/app_common.rs index 38e0e36..a4dd6f0 100644 --- a/src/app_common.rs +++ b/src/app_common.rs @@ -2,6 +2,10 @@ use crate::prelude::*; pub const LOOKAROUND_VERSION: &'static str = env! ("CARGO_PKG_VERSION"); +pub fn find_project_dirs () -> Option { + ProjectDirs::from ("", "ReactorScram", "LookAround") +} + #[derive (Debug, thiserror::Error)] pub enum AppError { #[error (transparent)] diff --git a/src/client.rs b/src/client.rs index 1f45376..bc959ad 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,9 +5,14 @@ struct ServerResponse { nickname: Option , } +struct ConfigFile { + nicknames: HashMap , +} + struct ClientParams { common: app_common::Params, bind_addrs: Vec , + nicknames: HashMap , timeout_ms: u64, } @@ -21,13 +26,13 @@ pub async fn client > (args: I) -> Result <(), AppErro } let params = configure_client (args)?; - let socket = make_socket (¶ms).await?; + let socket = make_socket (¶ms.common, params.bind_addrs).await?; let msg = Message::new_request1 ().to_vec ()?; tokio::spawn (send_requests (Arc::clone (&socket), params.common, msg)); let mut peers = HashMap::with_capacity (10); - timeout (Duration::from_millis (params.timeout_ms), listen_for_responses (&*socket, &mut peers)).await.ok (); + timeout (Duration::from_millis (params.timeout_ms), listen_for_responses (&*socket, params.nicknames, &mut peers)).await.ok (); let mut peers: Vec <_> = peers.into_iter ().collect (); peers.sort_by_key (|(_, v)| v.mac); @@ -60,6 +65,9 @@ pub async fn find_nick > (mut args: I) -> Result <(), { let mut nick = None; let mut timeout_ms = 500; + let ConfigFile { + nicknames, + } = load_config_file (); while let Some (arg) = args.next () { match arg.as_str () { @@ -76,17 +84,13 @@ pub async fn find_nick > (mut args: I) -> Result <(), let needle_nick = nick.ok_or_else (|| CliArgError::MissingRequiredArg ("nickname".to_string ()))?; let needle_nick = Some (needle_nick); - let params = ClientParams { - common: Default::default (), - bind_addrs: get_ips ()?, - timeout_ms, - }; + let common_params = Default::default (); - let socket = make_socket (¶ms).await?; + let socket = make_socket (&common_params, get_ips ()?).await?; let msg = Message::new_request1 ().to_vec ()?; - tokio::spawn (send_requests (Arc::clone (&socket), params.common, msg)); + tokio::spawn (send_requests (Arc::clone (&socket), common_params, msg)); - timeout (Duration::from_millis (params.timeout_ms), async move { loop { + timeout (Duration::from_millis (timeout_ms), async move { loop { let (msgs, remote_addr) = match recv_msg_from (&socket).await { Err (_) => continue, Ok (x) => x, @@ -105,6 +109,8 @@ pub async fn find_nick > (mut args: I) -> Result <(), } } + resp.nickname = get_peer_nickname (&nicknames, resp.mac, resp.nickname); + if resp.nickname == needle_nick { println! ("{}", remote_addr.ip ()); return; @@ -120,6 +126,10 @@ fn configure_client > (mut args: I) let mut bind_addrs = vec! []; let mut timeout_ms = 500; + let ConfigFile { + nicknames, + } = load_config_file (); + while let Some (arg) = args.next () { match arg.as_str () { "--bind-addr" => { @@ -145,15 +155,43 @@ fn configure_client > (mut args: I) Ok (ClientParams { common: Default::default (), bind_addrs, + nicknames, timeout_ms, }) } -async fn make_socket (params: &ClientParams) -> Result , AppError> { +fn load_config_file () -> ConfigFile { + let mut nicknames: HashMap = Default::default (); + + if let Some (proj_dirs) = find_project_dirs () { + let mut ini = Ini::new_cs (); + let path = proj_dirs.config_dir ().join ("client.ini"); + if ini.load (&path).is_ok () { + let map_ref = ini.get_map_ref (); + if let Some (x) = map_ref.get ("nicknames") { + for (k, v) in x { + if let Some (v) = v { + let k = k.replace ('-', ":"); + nicknames.insert (k, v.to_string ()); + } + } + } + } + } + + ConfigFile { + nicknames, + } +} + +async fn make_socket ( + common_params: &app_common::Params, + bind_addrs: Vec , +) -> Result , AppError> { let socket = UdpSocket::bind (SocketAddrV4::new (Ipv4Addr::UNSPECIFIED, 0)).await?; - for bind_addr in ¶ms.bind_addrs { - if let Err (e) = socket.join_multicast_v4 (params.common.multicast_addr, *bind_addr) { + for bind_addr in &bind_addrs { + if let Err (e) = socket.join_multicast_v4 (common_params.multicast_addr, *bind_addr) { println! ("Error joining multicast group with iface {}: {:?}", bind_addr, e); } } @@ -177,7 +215,8 @@ async fn send_requests ( } async fn listen_for_responses ( - socket: &UdpSocket, + socket: &UdpSocket, + nicknames: HashMap , peers: &mut HashMap ) { loop { @@ -199,6 +238,63 @@ async fn listen_for_responses ( } } + resp.nickname = get_peer_nickname (&nicknames, resp.mac, resp.nickname); + peers.insert (remote_addr, resp); } } + +fn get_peer_nickname ( + nicknames: &HashMap , + mac: Option <[u8; 6]>, + peer_nickname: Option +) -> Option +{ + match peer_nickname.as_ref ().map (String::as_str) { + None => (), + Some ("") => (), + _ => return peer_nickname, + } + + if let Some (mac) = &mac { + return nicknames.get (&format! ("{}", MacAddress::new (*mac))).cloned () + } + + None +} + +#[cfg (test)] +mod test { + use super::*; + + #[test] + fn test_nicknames () { + let mut nicks = HashMap::new (); + + for (k, v) in [ + ("01:01:01:01:01:01", "phoenix") + ] { + nicks.insert (k.to_string (), v.to_string ()); + } + + for (num, (mac, peer_nickname), expected) in [ + // Somehow the server returns no MAC nor nick. In this case we are helpless + ( 1, (None, None), None), + // If the server tells us its MAC, we can look up our nickname for it + ( 2, (Some ([1, 1, 1, 1, 1, 1]), None), Some ("phoenix")), + // Unless it's not in our nick list. + ( 3, (Some ([1, 1, 1, 1, 1, 2]), None), None), + // If the server tells us its nickname, that always takes priority + ( 4, (None, Some ("snowflake")), Some ("snowflake")), + ( 5, (Some ([1, 1, 1, 1, 1, 1]), Some ("snowflake")), Some ("snowflake")), + ( 6, (Some ([1, 1, 1, 1, 1, 2]), Some ("snowflake")), Some ("snowflake")), + // But blank nicknames are treated like None + ( 7, (None, Some ("")), None), + ( 8, (Some ([1, 1, 1, 1, 1, 1]), Some ("")), Some ("phoenix")), + ( 9, (Some ([1, 1, 1, 1, 1, 2]), Some ("")), None), + ] { + let actual = get_peer_nickname (&nicks, mac, peer_nickname.map (str::to_string)); + assert_eq! (actual.as_ref ().map (String::as_str), expected, "{}", num); + } + } +} diff --git a/src/main.rs b/src/main.rs index 600ee1b..2495135 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ async fn async_main () -> Result <(), AppError> { None => return Err (CliArgError::MissingSubcommand.into ()), Some ("--version") => println! ("lookaround v{}", LOOKAROUND_VERSION), Some ("client") => client::client (args).await?, + Some ("config") => config (), Some ("find-nick") => client::find_nick (args).await?, Some ("my-ips") => my_ips ()?, Some ("server") => server::server (args).await?, @@ -39,6 +40,15 @@ async fn async_main () -> Result <(), AppError> { Ok (()) } +fn config () { + if let Some (proj_dirs) = ProjectDirs::from ("", "ReactorScram", "LookAround") { + println! ("Using config dir {:?}", proj_dirs.config_dir ()); + } + else { + println! ("Can't detect config dir."); + } +} + fn my_ips () -> Result <(), AppError> { for addr in ip::get_ips ()? { diff --git a/src/prelude.rs b/src/prelude.rs index 46368a1..e432662 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -18,6 +18,8 @@ pub use std::{ }, }; +pub use configparser::ini::Ini; +pub use directories::ProjectDirs; pub use mac_address::{ MacAddress, get_mac_address, @@ -37,6 +39,7 @@ pub use crate::{ LOOKAROUND_VERSION, AppError, CliArgError, + find_project_dirs, recv_msg_from, }, ip::get_ips, diff --git a/src/server.rs b/src/server.rs index f33ce92..787ddb3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -39,6 +39,23 @@ fn configure > (mut args: I) -> Result {