➕ 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"
|
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]]
|
[[package]]
|
||||||
name = "directories"
|
name = "directories"
|
||||||
version = "4.0.1"
|
version = "4.0.1"
|
||||||
|
@ -76,6 +82,7 @@ dependencies = [
|
||||||
name = "lookaround"
|
name = "lookaround"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"configparser",
|
||||||
"directories",
|
"directories",
|
||||||
"mac_address",
|
"mac_address",
|
||||||
"rand",
|
"rand",
|
||||||
|
|
|
@ -12,6 +12,7 @@ repository = "https://six-five-six-four.com/git/reactor/lookaround"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
configparser = "3.0.0"
|
||||||
directories = "4.0.1"
|
directories = "4.0.1"
|
||||||
mac_address = "1.1.2"
|
mac_address = "1.1.2"
|
||||||
rand = "0.8.4"
|
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
|
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:
|
||||||
|
|
|
@ -2,6 +2,10 @@ use crate::prelude::*;
|
||||||
|
|
||||||
pub const LOOKAROUND_VERSION: &'static str = env! ("CARGO_PKG_VERSION");
|
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)]
|
||||||
|
|
124
src/client.rs
124
src/client.rs
|
@ -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 (¶ms).await?;
|
let socket = make_socket (¶ms.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 (¶ms).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 ¶ms.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,7 +215,8 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -30,6 +30,7 @@ async fn async_main () -> Result <(), AppError> {
|
||||||
None => return Err (CliArgError::MissingSubcommand.into ()),
|
None => return Err (CliArgError::MissingSubcommand.into ()),
|
||||||
Some ("--version") => println! ("lookaround v{}", LOOKAROUND_VERSION),
|
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?,
|
||||||
|
@ -39,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 ()?
|
||||||
{
|
{
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -37,6 +39,7 @@ pub use crate::{
|
||||||
LOOKAROUND_VERSION,
|
LOOKAROUND_VERSION,
|
||||||
AppError,
|
AppError,
|
||||||
CliArgError,
|
CliArgError,
|
||||||
|
find_project_dirs,
|
||||||
recv_msg_from,
|
recv_msg_from,
|
||||||
},
|
},
|
||||||
ip::get_ips,
|
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 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" => {
|
||||||
|
|
Loading…
Reference in New Issue