➕ add ini files for both client and server
Long-lived servers can have their nickname configured in `server.ini`. Clients can have a hosts-file-like nickname lookup in `client.ini`.main
							parent
							
								
									73434756b6
								
							
						
					
					
						commit
						ed58df2e6b
					
				|  | @ -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", | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
							
								
								
									
										53
									
								
								README.md
								
								
								
								
							
							
						
						
									
										53
									
								
								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: | ||||
|  |  | |||
|  | @ -2,6 +2,10 @@ use crate::prelude::*; | |||
| 
 | ||||
| pub const LOOKAROUND_VERSION: &'static str = env! ("CARGO_PKG_VERSION"); | ||||
| 
 | ||||
| pub fn find_project_dirs () -> Option <ProjectDirs> { | ||||
| 	ProjectDirs::from ("", "ReactorScram", "LookAround") | ||||
| } | ||||
| 
 | ||||
| #[derive (Debug, thiserror::Error)] | ||||
| pub enum AppError { | ||||
| 	#[error (transparent)] | ||||
|  |  | |||
							
								
								
									
										124
									
								
								src/client.rs
								
								
								
								
							
							
						
						
									
										124
									
								
								src/client.rs
								
								
								
								
							|  | @ -5,9 +5,14 @@ struct ServerResponse { | |||
| 	nickname: Option <String>, | ||||
| } | ||||
| 
 | ||||
| struct ConfigFile { | ||||
| 	nicknames: HashMap <String, String>, | ||||
| } | ||||
| 
 | ||||
| struct ClientParams { | ||||
| 	common: app_common::Params, | ||||
| 	bind_addrs: Vec <Ipv4Addr>, | ||||
| 	nicknames: HashMap <String, String>, | ||||
| 	timeout_ms: u64, | ||||
| } | ||||
| 
 | ||||
|  | @ -21,13 +26,13 @@ pub async fn client <I: Iterator <Item=String>> (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 <I: Iterator <Item=String>> (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 <I: Iterator <Item=String>> (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 <I: Iterator <Item=String>> (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 <I: Iterator <Item=String>> (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 <I: Iterator <Item=String>> (mut args: I) | |||
| 	Ok (ClientParams { | ||||
| 		common: Default::default (), | ||||
| 		bind_addrs, | ||||
| 		nicknames, | ||||
| 		timeout_ms, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| async fn make_socket (params: &ClientParams) -> Result <Arc <UdpSocket>, AppError> { | ||||
| fn load_config_file () -> ConfigFile { | ||||
| 	let mut nicknames: HashMap <String, String> = 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 <Ipv4Addr>, | ||||
| ) -> Result <Arc <UdpSocket>, 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 <String, String>, | ||||
| 	peers: &mut HashMap <SocketAddr, ServerResponse> | ||||
| ) { | ||||
| 	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 <String, String>, | ||||
| 	mac: Option <[u8; 6]>, | ||||
| 	peer_nickname: Option <String> | ||||
| ) -> Option <String> | ||||
| { | ||||
| 	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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
							
								
								
									
										10
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										10
									
								
								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 ()? | ||||
| 	{ | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -39,6 +39,23 @@ fn configure <I: Iterator <Item=String>> (mut args: I) -> Result <Params, AppErr | |||
| 	let mut bind_addrs = vec![]; | ||||
| 	let mut nickname = String::new (); | ||||
| 	
 | ||||
| 	if let Some (proj_dirs) = find_project_dirs () { | ||||
| 		let mut ini = Ini::new_cs (); | ||||
| 		let path = proj_dirs.config_dir ().join ("server.ini"); | ||||
| 		if ini.load (&path).is_ok () { | ||||
| 			if let Some (x) = ini.get ("server", "nickname") { | ||||
| 				nickname = x; | ||||
| 				eprintln! ("Loaded nickname {:?}", nickname); | ||||
| 			} | ||||
| 		} | ||||
| 		else { | ||||
| 			eprintln! ("Can't load ini from {:?}, didn't load default configs", path); | ||||
| 		} | ||||
| 	} | ||||
| 	else { | ||||
| 		eprintln! ("Can't find config dir, didn't load default configs"); | ||||
| 	} | ||||
| 	
 | ||||
| 	while let Some (arg) = args.next () { | ||||
| 		match arg.as_str () { | ||||
| 			"--bind-addr" => { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 _
						_