Compare commits

..

3 Commits

Author SHA1 Message Date
_ ed58df2e6b 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`.
2021-12-09 18:15:03 +00:00
_ 73434756b6 this will eventually go into 0.1.6 2021-12-09 17:02:04 +00:00
_ 8aae200ebf add `directories` dep and `--version` subcommand 2021-12-09 17:01:29 +00:00
8 changed files with 239 additions and 29 deletions

49
Cargo.lock generated
View File

@ -26,6 +26,32 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.3" version = "0.2.3"
@ -54,8 +80,10 @@ dependencies = [
[[package]] [[package]]
name = "lookaround" name = "lookaround"
version = "0.1.5" version = "0.1.6"
dependencies = [ dependencies = [
"configparser",
"directories",
"mac_address", "mac_address",
"rand", "rand",
"thiserror", "thiserror",
@ -195,6 +223,25 @@ dependencies = [
"rand_core", "rand_core",
] ]
[[package]]
name = "redox_syscall"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.82" version = "1.0.82"

View File

@ -9,9 +9,11 @@ license = "AGPL-3.0"
name = "lookaround" name = "lookaround"
readme = "README.md" readme = "README.md"
repository = "https://six-five-six-four.com/git/reactor/lookaround" repository = "https://six-five-six-four.com/git/reactor/lookaround"
version = "0.1.5" version = "0.1.6"
[dependencies] [dependencies]
configparser = "3.0.0"
directories = "4.0.1"
mac_address = "1.1.2" mac_address = "1.1.2"
rand = "0.8.4" rand = "0.8.4"
thiserror = "1.0.30" thiserror = "1.0.30"

View File

@ -19,29 +19,48 @@ Found 3 peers:
LookAround is a Rust program for looking up your computers' MAC and IP addresses 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. 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 ## Installing
same multicast domain, similar to Avahi and Bonjour.
Systems self-identify by MAC address and nicknames. Public keys with Make sure Cargo is installed from [RustUp.](https://rustup.rs/)
TOFU semantics are intended before v1.0.0.
## Installation
Use the Cargo package manager from [Rust](https://rustup.rs/) to install LookAround.
```bash ```bash
# Install LookAround with Cargo
cargo install lookaround 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. Create the files `client.ini` and/or `server.ini` in that directory
put this systemd unit in `~/.config/systemd/user/lookaround.service`: (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 ```ini
[Unit] [Unit]
Description=LookAround Description=LookAround
[Service] [Service]
ExecStart=/home/user/.cargo/bin/lookaround server --nickname my-desktop ExecStart=/home/user/.cargo/bin/lookaround server
Restart=always
[Install] [Install]
WantedBy=default.target WantedBy=default.target
@ -56,11 +75,19 @@ systemctl --user status lookaround
systemctl --user enable 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 ## Usage
Run the server manually: (If you didn't configure auto-start) Run the server manually: (To test before installing)
```bash ```bash
lookaround server --nickname my-desktop lookaround server --nickname my-computer
``` ```
On a client computer: On a client computer:

View File

@ -1,5 +1,11 @@
use crate::prelude::*; 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)] #[derive (Debug, thiserror::Error)]
pub enum AppError { pub enum AppError {
#[error (transparent)] #[error (transparent)]

View File

@ -5,9 +5,14 @@ struct ServerResponse {
nickname: Option <String>, nickname: Option <String>,
} }
struct ConfigFile {
nicknames: HashMap <String, String>,
}
struct ClientParams { struct ClientParams {
common: app_common::Params, common: app_common::Params,
bind_addrs: Vec <Ipv4Addr>, bind_addrs: Vec <Ipv4Addr>,
nicknames: HashMap <String, String>,
timeout_ms: u64, 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 params = configure_client (args)?;
let socket = make_socket (&params).await?; let socket = make_socket (&params.common, params.bind_addrs).await?;
let msg = Message::new_request1 ().to_vec ()?; let msg = Message::new_request1 ().to_vec ()?;
tokio::spawn (send_requests (Arc::clone (&socket), params.common, msg)); tokio::spawn (send_requests (Arc::clone (&socket), params.common, msg));
let mut peers = HashMap::with_capacity (10); 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 (); let mut peers: Vec <_> = peers.into_iter ().collect ();
peers.sort_by_key (|(_, v)| v.mac); 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 nick = None;
let mut timeout_ms = 500; let mut timeout_ms = 500;
let ConfigFile {
nicknames,
} = load_config_file ();
while let Some (arg) = args.next () { while let Some (arg) = args.next () {
match arg.as_str () { 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 = nick.ok_or_else (|| CliArgError::MissingRequiredArg ("nickname".to_string ()))?;
let needle_nick = Some (needle_nick); let needle_nick = Some (needle_nick);
let params = ClientParams { let common_params = Default::default ();
common: Default::default (),
bind_addrs: get_ips ()?,
timeout_ms,
};
let socket = make_socket (&params).await?; let socket = make_socket (&common_params, get_ips ()?).await?;
let msg = Message::new_request1 ().to_vec ()?; 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 { let (msgs, remote_addr) = match recv_msg_from (&socket).await {
Err (_) => continue, Err (_) => continue,
Ok (x) => x, 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 { if resp.nickname == needle_nick {
println! ("{}", remote_addr.ip ()); println! ("{}", remote_addr.ip ());
return; return;
@ -120,6 +126,10 @@ fn configure_client <I: Iterator <Item=String>> (mut args: I)
let mut bind_addrs = vec! []; let mut bind_addrs = vec! [];
let mut timeout_ms = 500; let mut timeout_ms = 500;
let ConfigFile {
nicknames,
} = load_config_file ();
while let Some (arg) = args.next () { while let Some (arg) = args.next () {
match arg.as_str () { match arg.as_str () {
"--bind-addr" => { "--bind-addr" => {
@ -145,15 +155,43 @@ fn configure_client <I: Iterator <Item=String>> (mut args: I)
Ok (ClientParams { Ok (ClientParams {
common: Default::default (), common: Default::default (),
bind_addrs, bind_addrs,
nicknames,
timeout_ms, 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?; let socket = UdpSocket::bind (SocketAddrV4::new (Ipv4Addr::UNSPECIFIED, 0)).await?;
for bind_addr in &params.bind_addrs { for bind_addr in &bind_addrs {
if let Err (e) = socket.join_multicast_v4 (params.common.multicast_addr, *bind_addr) { if let Err (e) = socket.join_multicast_v4 (common_params.multicast_addr, *bind_addr) {
println! ("Error joining multicast group with iface {}: {:?}", bind_addr, e); println! ("Error joining multicast group with iface {}: {:?}", bind_addr, e);
} }
} }
@ -178,6 +216,7 @@ async fn send_requests (
async fn listen_for_responses ( async fn listen_for_responses (
socket: &UdpSocket, socket: &UdpSocket,
nicknames: HashMap <String, String>,
peers: &mut HashMap <SocketAddr, ServerResponse> peers: &mut HashMap <SocketAddr, ServerResponse>
) { ) {
loop { 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); 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);
}
}
}

View File

@ -28,7 +28,9 @@ async fn async_main () -> Result <(), AppError> {
match subcommand.as_ref ().map (|x| &x[..]) { match subcommand.as_ref ().map (|x| &x[..]) {
None => return Err (CliArgError::MissingSubcommand.into ()), None => return Err (CliArgError::MissingSubcommand.into ()),
Some ("--version") => println! ("lookaround v{}", LOOKAROUND_VERSION),
Some ("client") => client::client (args).await?, Some ("client") => client::client (args).await?,
Some ("config") => config (),
Some ("find-nick") => client::find_nick (args).await?, Some ("find-nick") => client::find_nick (args).await?,
Some ("my-ips") => my_ips ()?, Some ("my-ips") => my_ips ()?,
Some ("server") => server::server (args).await?, Some ("server") => server::server (args).await?,
@ -38,6 +40,15 @@ async fn async_main () -> Result <(), AppError> {
Ok (()) 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> { fn my_ips () -> Result <(), AppError> {
for addr in ip::get_ips ()? for addr in ip::get_ips ()?
{ {

View File

@ -18,6 +18,8 @@ pub use std::{
}, },
}; };
pub use configparser::ini::Ini;
pub use directories::ProjectDirs;
pub use mac_address::{ pub use mac_address::{
MacAddress, MacAddress,
get_mac_address, get_mac_address,
@ -34,8 +36,10 @@ pub use tokio::{
pub use crate::{ pub use crate::{
app_common::{ app_common::{
self, self,
LOOKAROUND_VERSION,
AppError, AppError,
CliArgError, CliArgError,
find_project_dirs,
recv_msg_from, recv_msg_from,
}, },
ip::get_ips, ip::get_ips,

View File

@ -39,6 +39,23 @@ fn configure <I: Iterator <Item=String>> (mut args: I) -> Result <Params, AppErr
let mut bind_addrs = vec![]; let mut bind_addrs = vec![];
let mut nickname = String::new (); 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 () { while let Some (arg) = args.next () {
match arg.as_str () { match arg.as_str () {
"--bind-addr" => { "--bind-addr" => {