Compare commits

...

57 Commits
v0.1.1 ... main

Author SHA1 Message Date
_ 2cc1ed4a92 ⬆️ use directories v5.0.0 instead of vendoring it 2023-03-31 05:45:40 +00:00
Reactor Scram bd73d832fa
Update rust.yml
Finally read the part that says "this is deprecated use the thing GitHub has built in"

frik
2021-12-16 16:14:07 -06:00
Reactor Scram fd43fbbb1e
Update rust.yml 2021-12-16 16:12:46 -06:00
Reactor Scram 97d7d000dc
Update rust.yml
oof
2021-12-16 16:11:11 -06:00
Reactor Scram bda991bc67
Update rust.yml
you are getting to watch a perma-noob learn about CI in real time, this is special
2021-12-16 15:56:34 -06:00
Reactor Scram ed816a0d6f
Update rust.yml 2021-12-16 15:50:45 -06:00
_ b05a87cc9c use `config_local_dir` 2021-12-16 21:47:12 +00:00
_ 354a74aeaa vendor my fork of `directories`.
I'll swap back to the crates.io version if `config_local_dir` is merged
upstream, or if it times out I'll fork it.
2021-12-16 21:46:25 +00:00
_ d9fd0fd29d 🚧 wip: working on avalanche idea to make MACs easier for humans to read 2021-12-16 20:51:44 +00:00
_ f121e3fd55 📝 add idea 2021-12-13 00:25:15 +00:00
_ ab42de5823 🚨 clippy pass 2021-12-09 18:22:23 +00:00
_ 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
_ a0a64cd79c 📝 one more pass on README 2021-12-09 16:53:32 +00:00
_ 0815691af2 🐛 bug: fix readme 2021-12-09 16:50:54 +00:00
_ 4f66c0495e add `find-nick` command 2021-12-09 16:46:55 +00:00
_ b261d7ba4a ♻️ refactor: refactor the client a lot so I can reuse its code for new subcommands 2021-12-09 16:21:14 +00:00
_ 5665f484a2 🔇 remove debugging println 2021-12-09 15:49:23 +00:00
_ 814fee2bd5 🚧 2021-12-09 15:46:00 +00:00
_ e7496a7c0e 🚧 wip: add CLI docs 2021-12-09 15:45:22 +00:00
_ 18e38f0611 add `--timeout-ms` for client
Empirical testing shows that 200 ms is probably enough on my LAN, so
I set the default to 500 ms.
2021-12-09 15:34:42 +00:00
_ 30ebb528eb bump to v0.1.5 2021-12-08 21:53:27 -06:00
_ 221a0bef2f ignore errors if an interface can't join multicast
This works okay on my home network, but it's a little more magical than
I wanted - I can't force it to pick up the wifi interface. If the
Ethernet is plugged in, the laptop always and only picks that, even
if I know the server only asked the Ethernet interface.

This is fine, but only because my Ethernet happens to be faster than
my Wifi. I'm not sure how it will behave at work, where WiFi and
Ethernet may be separate networks.

At least the error messages are better now, so I can figure out why
it wasn't auto-starting with systemd.
2021-12-08 21:48:41 -06:00
_ 7a0880fc02 do it similar on the client 2021-12-08 21:40:03 -06:00
_ fd4f70b1c9 🐛 bug: think I figured out the server part
turns out you can join multiple multicast groups on the same socket.
And then I think the OS or the network somehow decides how to route
packets to you. Without touching anything else, if I'm plugged into
Ethernet, it picks that address (when running client on the desktop)
and when I unplug it, it picks the laptop's WiFi.
2021-12-08 21:31:07 -06:00
_ 0bb702f312 🚧 wip: not working the way I expect 2021-12-09 03:11:28 +00:00
_ 1b7c2ce0f4 ♻️ refactor: preparing to serve on multiple interfaces 2021-12-09 02:26:58 +00:00
_ 39bcea54d4 ♻️ refactor: make the self-IP code available within the program 2021-12-09 02:06:01 +00:00
_ 2ba1dc2834 🐛 bug: fix server crashing when you send it a packet of ":V\n" 2021-12-09 01:46:03 +00:00
_ 33e6ae29ca ♻️ refactor: replace all unwraps in my code with question marks 2021-12-09 01:39:54 +00:00
_ c7681ce9f5 ♻️ refactor 2021-12-09 01:28:29 +00:00
_ b620bcfe06 ♻️ refactor: make it a little more idiomatic 2021-12-09 00:45:18 +00:00
_ 5d8bb3282b ♻️ refactor: literal translation to async 2021-12-09 00:34:59 +00:00
_ bf9d185092 🚨 fix all clippy warnings 2021-12-09 00:23:48 +00:00
_ ee51bb7d3d ♻️ refactor: move more Windows-only code behind cfg flags 2021-12-09 00:11:47 +00:00
_ dbedc6083e ♻️ refactor: fix some unused code warnings 2021-12-09 00:03:56 +00:00
_ 50bfd422f9 ditch abort-on-panic 2021-12-08 23:56:22 +00:00
_ 5d6c566317 add Tokio dep
Release build only went from 1.9 to 2.0 MB, but that could cause I'm not really using Tokio yet
2021-12-08 23:55:22 +00:00
_ 2b4695934e add `my-ips` impl for Linux and refactor it into a module 2021-12-08 16:48:43 -06:00
_ cf283a2eaa add `my-ips` subcommand, currently for Windows only 2021-12-08 16:04:45 -06:00
_ 214bbc0da9 📝 add issue 2021-12-08 15:12:13 -06:00
_ 9251dc327d add `--bind-addr` CLI args to both client and server.
This lets you pick an interface. I can't enumerate them automatically yet.
2021-12-08 14:55:27 -06:00
_ c8ed7e5d06 📝 bump to 0.1.4, forgot to update the docs 2021-12-08 02:19:56 +00:00
_ 3871d87a0a bump to 0.1.3 2021-12-08 02:14:23 +00:00
_ aa75119f39 add nicknames 2021-12-08 02:10:58 +00:00
_ ca8fcc1104 add test for old-style Response packets 2021-12-08 00:26:13 +00:00
_ 6dc4eb2771 📝 remove port from readme 2021-12-07 13:55:17 +00:00
_ 42d0557612 release: bump to 0.1.2 2021-12-06 21:21:12 -06:00
_ 494c44fbcf ♻️ refactor: pass address / port params to subcommands
Needed to land CLI arg changes eventually
2021-12-06 21:21:12 -06:00
_ f47fb4f1ba 🐛 bug: flip hashmap key and value so peers are de-duped by IP instead of claimed MAC. 2021-12-06 21:21:12 -06:00
_ 4955119074 📝 add issue for CLI args 2021-12-06 21:21:12 -06:00
_ fd4062416e 💄 hide port from output since it's alwasy 9040 2021-12-06 21:21:12 -06:00
_ 258cab8e4d 📝 brainstorming 2021-12-06 21:21:12 -06:00
_ d0b15c8397 📝 document issue I noticed at work 2021-12-06 21:21:12 -06:00
_ c1992fd562 🐛 bug: forgot to check in Cargo.lock after the version bump 2021-12-06 21:21:12 -06:00
ReactorScram 517052f28b
Create rust.yml 2021-12-05 18:33:50 -06:00
16 changed files with 1392 additions and 249 deletions

25
.github/workflows/rust.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: Rust
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
submodules: recursive
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

0
.gitmodules vendored Normal file
View File

206
Cargo.lock generated
View File

@ -4,9 +4,9 @@ version = 3
[[package]]
name = "autocfg"
version = "1.0.1"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
@ -26,6 +26,32 @@ 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 = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74be3be809c18e089de43bdc504652bb2bc473fca8756131f8689db8cf079ba9"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b"
dependencies = [
"libc",
"redox_users",
"windows-sys",
]
[[package]]
name = "getrandom"
version = "0.2.3"
@ -39,17 +65,30 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.109"
version = "0.2.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98a04dce437184842841303488f70d0188c5f51437d2a834dc097eafa909a01"
checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "lookaround"
version = "0.1.0"
version = "0.1.6"
dependencies = [
"configparser",
"directories",
"mac_address",
"nix 0.25.0",
"rand",
"thiserror",
"tokio",
]
[[package]]
@ -58,7 +97,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89544d9544366f6cda81244514a80809b137b5a179947b73bfa9f2797480de69"
dependencies = [
"nix",
"nix 0.22.2",
"winapi",
]
@ -71,6 +110,28 @@ dependencies = [
"autocfg",
]
[[package]]
name = "mio"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [
"libc",
"log",
"miow",
"ntapi",
"winapi",
]
[[package]]
name = "miow"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"winapi",
]
[[package]]
name = "nix"
version = "0.22.2"
@ -84,6 +145,41 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nix"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
dependencies = [
"autocfg",
"bitflags",
"cfg-if",
"libc",
"memoffset",
"pin-utils",
]
[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
"winapi",
]
[[package]]
name = "pin-project-lite"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.15"
@ -148,6 +244,25 @@ dependencies = [
"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]]
name = "syn"
version = "1.0.82"
@ -179,6 +294,19 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
dependencies = [
"autocfg",
"libc",
"mio",
"pin-project-lite",
"winapi",
]
[[package]]
name = "unicode-xid"
version = "0.2.2"
@ -212,3 +340,69 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"

View File

@ -9,15 +9,18 @@ license = "AGPL-3.0"
name = "lookaround"
readme = "README.md"
repository = "https://six-five-six-four.com/git/reactor/lookaround"
version = "0.1.1"
version = "0.1.6"
[dependencies]
configparser = "3.0.0"
directories = "5.0.0"
mac_address = "1.1.2"
nix = "0.25.0"
rand = "0.8.4"
thiserror = "1.0.30"
tokio = { version = "1.14.0", features = ["fs", "net", "rt", "time"] }
[profile.release]
codegen-units = 1
lto = true
opt-level = "z"
panic = "abort"

View File

@ -3,35 +3,56 @@
_Has this ever happened to you?_
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.
```text
$ ping $(lookaround find-nick laptop)
PING 192.168.1.101 (192.168.1.101) 56(84) bytes of data.
...
$ lookaround client
Found 3 peers:
11:11:11:11:11:11 = 192.168.1.101:9040
22:22:22:22:22:22 = 192.168.1.102:9040
33:33:33:33:33:33 = 192.168.1.103:9040
11:11:11:11:11:11 = 192.168.1.101 `laptop`
22:22:22:22:22:22 = 192.168.1.102 `desktop`
33:33:33:33:33:33 = 192.168.1.103 `old-laptop`
```
The LookAround client uses IP multicast to find LookAround servers within the
same multicast domain.
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.
MAC addresses change slower than IP addresses, so if you know that
`11:11:11:11:11:11` is your laptop, and your laptop is running LookAround,
LookAround will find the IP for you.
## Installing
## 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 run the server as a normal user all the time,
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]
@ -39,6 +60,7 @@ Description=LookAround
[Service]
ExecStart=/home/user/.cargo/bin/lookaround server
Restart=always
[Install]
WantedBy=default.target
@ -53,17 +75,43 @@ 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 haven't installed it with systemd yet)
Run the server manually: (To test before installing)
```bash
lookaround server
lookaround server --nickname my-computer
```
Run a client to ping all servers in the same multi-cast domain:
On a client computer:
```bash
# Use the `find-nick` subcommnad to find an IP...
lookaround find-nick laptop
# Prints `192.168.1.101`
# Or ping it...
ping $(lookaround find-nick laptop)
# Or SSH to it...
ssh user@$(lookaround find-nick laptop)
# Or pull a file from it
# (after starting `nc -l -p 9000 < some-file` on the laptop)
nc $(lookaround find-nick laptop) 9000
# Use the `client` subcommand to find all servers in the same multicast domain
lookaround client
# Use a longer timeout if servers need more than 500 ms to respond
lookaround client --timeout-ms 1000
```
## Contributing
@ -77,3 +125,6 @@ Use the [kazupon Git commit message convention](https://github.com/kazupon/git-c
## This Git repo
This repo's upstream is https://six-five-six-four.com/git/reactor/lookaround.
It's mirrored on my GitHub, https://github.com/ReactorScram/lookaround
I don't use GitHub issues, so issues are in issues.md in the repo.

6
ideas.md Normal file
View File

@ -0,0 +1,6 @@
Cool ideas that can be done but probably won't be.
- Advertise TCP services in server response
- Arbitrary TCP forwarding of (stdin? stdout? TCP?) with interface cutover
- Netcat replacement "Just send a file" _including filename_
- Public-key crypto for trusting peers on first use (Hard cause it requires mutable disk state)

23
issues.md Normal file
View File

@ -0,0 +1,23 @@
**Issues**
Just doing ULIDs cause `rusty_ulid` makes it easy.
# 01FP9843V1J3H9JMHXFPJSV2QJ
Have to disable VirtualBox virtual interface thingy to make it work.
Might also misbehave on systems with both Ethernet and WiFi connections.
I think this is because the `UdpSocket`, when I tell it to bind to
`0.0.0.0`, doesn't actually bind to all interfaces, it picks an interface
and binds to it.
I don't have any systems at home to replicate this on. And if I have
to poll multiple sockets, I'll probably just drag in Tokio even
though I was hoping not to use it - It's nicer than threading.
I don't think Tokio has a way to iterate over network interfaces
and get their IPs, so I might have to find another dependency
for that. I think on Linux I can get it from `/sys/class/net` but
I can't remember the trick for that. I think last time I did this
(for that work project) I just punted to Qt.

73
src/app_common.rs Normal file
View File

@ -0,0 +1,73 @@
use crate::prelude::*;
pub const LOOKAROUND_VERSION: &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)]
AddrParse (#[from] std::net::AddrParseError),
#[error (transparent)]
CliArgs (#[from] CliArgError),
#[error ("Operation timed out")]
Elapsed (#[from] tokio::time::error::Elapsed),
#[error (transparent)]
Io (#[from] std::io::Error),
#[error (transparent)]
Ip (#[from] crate::ip::IpError),
#[error (transparent)]
Join (#[from] tokio::task::JoinError),
#[error (transparent)]
MacAddr (#[from] mac_address::MacAddressError),
#[error (transparent)]
Message (#[from] crate::message::MessageError),
#[error (transparent)]
ParseInt (#[from] std::num::ParseIntError),
#[error (transparent)]
Tlv (#[from] crate::tlv::TlvError),
}
#[derive (Debug, thiserror::Error)]
pub enum CliArgError {
#[error ("Missing value for argument `{0}`")]
MissingArgumentValue (String),
#[error ("Missing required argument <{0}>")]
MissingRequiredArg (String),
#[error ("First argument should be a subcommand")]
MissingSubcommand,
#[error ("Unknown subcommand `{0}`")]
UnknownSubcommand (String),
#[error ("Unrecognized argument `{0}`")]
UnrecognizedArgument (String),
}
pub async fn recv_msg_from (socket: &UdpSocket) -> Result <(Vec <Message>, SocketAddr), AppError>
{
let mut buf = vec! [0u8; PACKET_SIZE];
let (bytes_recved, remote_addr) = socket.recv_from (&mut buf).await?;
buf.truncate (bytes_recved);
let msgs = Message::from_slice2 (&buf)?;
Ok ((msgs, remote_addr))
}
#[derive (Clone)]
pub struct Params {
// Servers bind on this port, clients must send to the port
pub server_port: u16,
// Clients and servers will all join the same multicast addr
pub multicast_addr: Ipv4Addr,
}
impl Default for Params {
fn default () -> Self {
Self {
server_port: 9040,
multicast_addr: Ipv4Addr::new (225, 100, 99, 98),
}
}
}

40
src/avalanche.rs Normal file
View File

@ -0,0 +1,40 @@
type Mac = [u8; 6];
pub fn debug () {
for input in [
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 1],
] {
assert_eq! (unmix (mix (input)), input);
}
println! ("Passed");
}
// NOT intended for any cryptography or security. This is TRIVIALLY reversible.
// It's just to make it easier for humans to tell apart MACs where only a couple
// numbers differ.
fn mix (i: Mac) -> Mac {
[
i [0] ^ i [5],
i [1] ^ i [4],
i [2] ^ i [3],
i [3],
i [4],
i [5],
]
}
fn unmix (i: Mac) -> Mac {
[
i [0] ^ i [5],
i [1] ^ i [4],
i [2] ^ i [3],
i [3],
i [4],
i [5],
]
}

300
src/client.rs Normal file
View File

@ -0,0 +1,300 @@
use crate::prelude::*;
struct ServerResponse {
mac: Option <[u8; 6]>,
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,
}
pub async fn client <I: Iterator <Item=String>> (args: I) -> Result <(), AppError> {
match get_mac_address() {
Ok(Some(ma)) => {
println!("Our MAC addr = {}", ma);
}
Ok(None) => println!("No MAC address found."),
Err(e) => println!("{:?}", e),
}
let params = configure_client (args)?;
let socket = make_socket (&params.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, params.nicknames, &mut peers)).await.ok ();
let mut peers: Vec <_> = peers.into_iter ().collect ();
peers.sort_by_key (|(_, v)| v.mac);
println! ("Found {} peers:", peers.len ());
for (ip, resp) in peers.into_iter () {
let mac = match resp.mac {
None => {
println! ("<Unknown> = {}", ip);
continue;
},
Some (x) => x,
};
let nickname = match resp.nickname {
None => {
println! ("{} = {}", MacAddress::new (mac), ip.ip ());
continue;
},
Some (x) => x,
};
println! ("{} = {} `{}`", MacAddress::new (mac), ip.ip (), nickname);
}
Ok (())
}
pub async fn find_nick <I: Iterator <Item=String>> (mut args: I) -> Result <(), AppError>
{
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 () {
"--timeout-ms" => {
timeout_ms = match args.next () {
None => return Err (CliArgError::MissingArgumentValue (arg).into ()),
Some (x) => u64::from_str (&x)?,
};
},
_ => nick = Some (arg),
}
}
let needle_nick = nick.ok_or_else (|| CliArgError::MissingRequiredArg ("nickname".to_string ()))?;
let needle_nick = Some (needle_nick);
let common_params = Default::default ();
let socket = make_socket (&common_params, get_ips ()?).await?;
let msg = Message::new_request1 ().to_vec ()?;
tokio::spawn (send_requests (Arc::clone (&socket), common_params, msg));
timeout (Duration::from_millis (timeout_ms), async move { loop {
let (msgs, remote_addr) = match recv_msg_from (&socket).await {
Err (_) => continue,
Ok (x) => x,
};
let mut resp = ServerResponse {
mac: None,
nickname: None,
};
for msg in msgs.into_iter () {
match msg {
Message::Response1 (x) => resp.mac = x,
Message::Response2 (x) => resp.nickname = Some (x.nickname),
_ => (),
}
}
resp.nickname = get_peer_nickname (&nicknames, resp.mac, resp.nickname);
if resp.nickname == needle_nick {
println! ("{}", remote_addr.ip ());
return;
}
}}).await?;
Ok (())
}
fn configure_client <I: Iterator <Item=String>> (mut args: I)
-> Result <ClientParams, AppError>
{
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" => {
bind_addrs.push (match args.next () {
None => return Err (CliArgError::MissingArgumentValue (arg).into ()),
Some (x) => Ipv4Addr::from_str (&x)?,
});
},
"--timeout-ms" => {
timeout_ms = match args.next () {
None => return Err (CliArgError::MissingArgumentValue (arg).into ()),
Some (x) => u64::from_str (&x)?,
};
},
_ => return Err (CliArgError::UnrecognizedArgument (arg).into ()),
}
}
if bind_addrs.is_empty () {
bind_addrs = get_ips ()?;
}
Ok (ClientParams {
common: Default::default (),
bind_addrs,
nicknames,
timeout_ms,
})
}
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_local_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 &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);
}
}
Ok (Arc::new (socket))
}
async fn send_requests (
socket: Arc <UdpSocket>,
params: app_common::Params,
msg: Vec <u8>,
)
-> Result <(), AppError>
{
for _ in 0..10 {
socket.send_to (&msg, (params.multicast_addr, params.server_port)).await?;
sleep (Duration::from_millis (100)).await;
}
Ok::<_, AppError> (())
}
async fn listen_for_responses (
socket: &UdpSocket,
nicknames: HashMap <String, String>,
peers: &mut HashMap <SocketAddr, ServerResponse>
) {
loop {
let (msgs, remote_addr) = match recv_msg_from (socket).await {
Err (_) => continue,
Ok (x) => x,
};
let mut resp = ServerResponse {
mac: None,
nickname: None,
};
for msg in msgs.into_iter () {
match msg {
Message::Response1 (x) => resp.mac = x,
Message::Response2 (x) => resp.nickname = Some (x.nickname),
_ => (),
}
}
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_deref () {
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);
}
}
}

122
src/ip.rs Normal file
View File

@ -0,0 +1,122 @@
use std::{
net::Ipv4Addr,
process::Command,
str::FromStr,
};
#[derive (Debug, thiserror::Error)]
pub enum IpError {
#[error (transparent)]
Io (#[from] std::io::Error),
#[error (transparent)]
FromUtf8 (#[from] std::string::FromUtf8Error),
#[error ("Self-IP detection is not implemented on Mac OS")]
NotImplementedOnMac,
}
#[cfg(target_os = "linux")]
pub fn get_ips () -> Result <Vec <Ipv4Addr>, IpError> {
let output = linux::get_ip_addr_output ()?;
Ok (linux::parse_ip_addr_output (&output))
}
#[cfg(target_os = "macos")]
pub fn get_ips () -> Result <Vec <Ipv4Addr>, IpError> {
Err (IpError::NotImplementedOnMac)
}
#[cfg(target_os = "windows")]
pub fn get_ips () -> Result <Vec <Ipv4Addr>, IpError> {
let output = windows::get_ip_config_output ()?;
Ok (windows::parse_ip_config_output (&output))
}
#[cfg(target_os = "linux")]
pub mod linux {
use super::*;
pub fn get_ip_addr_output () -> Result <String, IpError> {
let output = Command::new ("ip")
.arg ("addr")
.output ()?;
let output = output.stdout.as_slice ();
let output = String::from_utf8 (output.to_vec ())?;
Ok (output)
}
pub fn parse_ip_addr_output (output: &str) -> Vec <Ipv4Addr> {
// I wrote this in FP style because I was bored.
output.lines ()
.map (|l| l.trim_start ())
.filter_map (|l| l.strip_prefix ("inet "))
.filter_map (|l| l.find ('/').map (|x| &l [0..x]))
.filter_map (|l| Ipv4Addr::from_str (l).ok ())
.filter (|a| ! a.is_loopback ())
.collect ()
}
}
#[cfg(target_os = "windows")]
pub mod windows {
use super::*;
pub fn get_ip_config_output () -> Result <String, IpError> {
let output = Command::new ("ipconfig")
.output ()?;
let output = output.stdout.as_slice ();
let output = String::from_utf8 (output.to_vec ())?;
Ok (output)
}
pub fn parse_ip_config_output (output: &str) -> Vec <Ipv4Addr> {
let mut addrs = vec! [];
for line in output.lines () {
let line = line.trim_start ();
// Maybe only works on English locales?
if ! line.starts_with ("IPv4 Address") {
continue;
}
let colon_pos = match line.find (':') {
None => continue,
Some (x) => x,
};
let line = &line [colon_pos + 2..];
let addr = match Ipv4Addr::from_str (line) {
Err (_) => continue,
Ok (x) => x,
};
addrs.push (addr);
}
addrs
}
#[cfg (test)]
mod test {
use super::*;
#[test]
fn test () {
for (input, expected) in [
(
r"
IPv4 Address . . .. . . . : 192.168.1.1
",
vec! [
Ipv4Addr::new (192, 168, 1, 1),
]
),
] {
let actual = parse_ip_config_output (input);
assert_eq! (actual, expected);
}
}
}
}

View File

@ -1,192 +1,61 @@
use std::{
collections::HashMap,
env,
net::{
Ipv4Addr,
SocketAddr,
SocketAddrV4,
UdpSocket,
},
time::{Duration, Instant},
};
use prelude::*;
use mac_address::{
MacAddress,
get_mac_address,
};
use thiserror::Error;
mod message;
mod tlv;
use message::{
PACKET_SIZE,
Message,
};
#[derive (Debug, Error)]
enum AppError {
#[error (transparent)]
CliArgs (#[from] CliArgError),
#[error (transparent)]
Io (#[from] std::io::Error),
#[error (transparent)]
MacAddr (#[from] mac_address::MacAddressError),
#[error (transparent)]
Message (#[from] message::MessageError),
#[error (transparent)]
Tlv (#[from] tlv::TlvError),
}
#[derive (Debug, Error)]
enum CliArgError {
#[error ("First argument should be a subcommand")]
MissingSubcommand,
#[error ("Unknown subcommand `{0}`")]
UnknownSubcommand (String),
}
struct CommonParams {
// Servers bind on this port, clients must send to the port
server_port: u16,
// Clients and servers will all join the same multicast addr
multicast_addr: Ipv4Addr,
}
impl Default for CommonParams {
fn default () -> Self {
Self {
server_port: 9040,
multicast_addr: Ipv4Addr::new (225, 100, 99, 98),
}
}
}
pub mod app_common;
mod avalanche;
mod client;
mod ip;
pub mod message;
mod prelude;
mod server;
pub mod tlv;
fn main () -> Result <(), AppError> {
let rt = tokio::runtime::Builder::new_current_thread ()
.enable_io ()
.enable_time ()
.build ()?;
rt.block_on (async_main ())?;
Ok (())
}
async fn async_main () -> Result <(), AppError> {
let mut args = env::args ();
let _exe_name = args.next ();
match get_mac_address() {
Ok(Some(ma)) => {
println!("Our MAC addr = {}", ma);
}
Ok(None) => println!("No MAC address found."),
Err(e) => println!("{:?}", e),
}
let subcommand: Option <String> = args.next ();
match args.next ().as_ref ().map (|s| &s[..]) {
match subcommand.as_ref ().map (|x| &x[..]) {
None => return Err (CliArgError::MissingSubcommand.into ()),
Some ("client") => client ()?,
Some ("server") => server ()?,
Some ("--version") => println! ("lookaround v{}", LOOKAROUND_VERSION),
Some ("client") => client::client (args).await?,
Some ("config") => config (),
Some ("debug-avalanche") => avalanche::debug (),
Some ("find-nick") => client::find_nick (args).await?,
Some ("my-ips") => my_ips ()?,
Some ("server") => server::server (args).await?,
Some (x) => return Err (CliArgError::UnknownSubcommand (x.to_string ()).into ()),
}
Ok (())
}
fn client () -> Result <(), AppError> {
use rand::RngCore;
let params = CommonParams::default ();
let socket = UdpSocket::bind ("0.0.0.0:0")?;
socket.join_multicast_v4 (&params.multicast_addr, &([0u8, 0, 0, 0].into ()))?;
socket.set_read_timeout (Some (Duration::from_millis (1_000)))?;
let mut idem_id = [0u8; 8];
rand::thread_rng ().fill_bytes (&mut idem_id);
let msg = Message::Request {
idem_id,
mac: None,
}.to_vec ()?;
for _ in 0..10 {
socket.send_to (&msg, (params.multicast_addr, params.server_port))?;
std::thread::sleep (Duration::from_millis (100));
fn config () {
if let Some (proj_dirs) = ProjectDirs::from ("", "ReactorScram", "LookAround") {
println! ("Using config dir {:?}", proj_dirs.config_local_dir ());
}
let start_time = Instant::now ();
let mut peers = HashMap::with_capacity (10);
while Instant::now () < start_time + Duration::from_secs (2) {
let (resp, remote_addr) = match recv_msg_from (&socket) {
Err (_) => continue,
Ok (x) => x,
};
let peer_mac_addr = match resp {
Message::Response (mac) => mac,
_ => continue,
};
peers.insert (peer_mac_addr, remote_addr);
else {
println! ("Can't detect config dir.");
}
let mut peers: Vec <_> = peers.into_iter ().collect ();
peers.sort ();
println! ("Found {} peers:", peers.len ());
for (mac, ip) in &peers {
match mac {
Some (mac) => println! ("{} = {}", MacAddress::new (*mac), ip),
None => println! ("<Unknown> = {}", ip),
}
}
fn my_ips () -> Result <(), AppError> {
for addr in ip::get_ips ()?
{
println! ("{:?}", addr);
}
Ok (())
}
fn server () -> Result <(), AppError> {
let our_mac = get_mac_address ()?.map (|x| x.bytes ());
if our_mac.is_none () {
println! ("Warning: Can't find our own MAC address. We won't be able to respond to MAC-specific lookaround requests");
}
let params = CommonParams::default ();
let socket = UdpSocket::bind (SocketAddrV4::new (Ipv4Addr::UNSPECIFIED, params.server_port)).unwrap ();
socket.join_multicast_v4 (&params.multicast_addr, &([0u8, 0, 0, 0].into ())).unwrap ();
let mut recent_idem_ids = Vec::with_capacity (32);
loop {
println! ("Waiting for messages...");
let (req, remote_addr) = recv_msg_from (&socket)?;
let resp = match req {
Message::Request {
mac: None,
idem_id,
} => {
if recent_idem_ids.contains (&idem_id) {
println! ("Ignoring request we already processed");
None
}
else {
recent_idem_ids.insert (0, idem_id);
recent_idem_ids.truncate (30);
Some (Message::Response (our_mac))
}
},
_ => continue,
};
if let Some (resp) = resp {
socket.send_to (&resp.to_vec ()?, remote_addr).unwrap ();
}
}
}
fn recv_msg_from (socket: &UdpSocket) -> Result <(Message, SocketAddr), AppError>
{
let mut buf = vec! [0u8; PACKET_SIZE];
let (bytes_recved, remote_addr) = socket.recv_from (&mut buf)?;
buf.truncate (bytes_recved);
let msg = Message::from_slice (&buf)?;
Ok ((msg, remote_addr))
}

View File

@ -1,40 +1,79 @@
use std::{
io::Cursor,
};
use crate::tlv;
use thiserror::Error;
use crate::prelude::*;
const MAGIC_NUMBER: [u8; 4] = [0x9a, 0x4a, 0x43, 0x81];
pub const PACKET_SIZE: usize = 1024;
#[derive (Debug)]
pub enum Message {
Request {
idem_id: [u8; 8],
mac: Option <[u8; 6]>
},
Response (Option <[u8; 6]>),
}
type Mac = [u8; 6];
#[derive (Debug, Error)]
pub enum MessageError {
#[error (transparent)]
Io (#[from] std::io::Error),
#[error (transparent)]
Tlv (#[from] tlv::TlvError),
#[error ("Unknown type")]
UnknownType,
#[derive (Debug, PartialEq)]
pub enum Message {
// 1
Request1 {
idem_id: [u8; 8],
mac: Option <Mac>
},
// 2
Response1 (Option <Mac>),
// 3
Response2 (Response2),
}
impl Message {
pub fn write <W: std::io::Write> (&self, w: &mut W) -> Result <(), std::io::Error>
{
w.write_all (&MAGIC_NUMBER)?;
pub fn new_request1 () -> Message {
let mut idem_id = [0u8; 8];
rand::thread_rng ().fill_bytes (&mut idem_id);
Message::Request1 {
idem_id,
mac: None,
}
}
}
#[derive (Debug, PartialEq)]
pub struct Response2 {
pub idem_id: [u8; 8],
pub nickname: String,
}
#[derive (Debug, thiserror::Error)]
pub enum MessageError {
#[error (transparent)]
Io (#[from] std::io::Error),
#[error ("Length prefix too long")]
LengthPrefixTooLong ((usize, usize)),
#[error (transparent)]
Tlv (#[from] tlv::TlvError),
#[error (transparent)]
TryFromInt (#[from] std::num::TryFromIntError),
#[error ("Unknown type")]
UnknownType,
#[error (transparent)]
FromUtf8 (#[from] std::string::FromUtf8Error),
}
#[derive (Default)]
struct DummyWriter {
position: usize,
}
impl Write for DummyWriter {
fn flush (&mut self) -> std::io::Result <()> {
Ok (())
}
fn write (&mut self, buf: &[u8]) -> std::io::Result <usize> {
self.position += buf.len ();
Ok (buf.len ())
}
}
impl Message {
pub fn write <T> (&self, w: &mut Cursor <T>) -> Result <(), MessageError>
where Cursor <T>: Write
{
match self {
Self::Request {
Self::Request1 {
idem_id,
mac,
}=> {
@ -42,16 +81,39 @@ impl Message {
w.write_all (&idem_id[..])?;
Self::write_mac_opt (w, *mac)?;
},
Self::Response (mac) => {
Self::Response1 (mac) => {
w.write_all (&[2])?;
Self::write_mac_opt (w, *mac)?;
},
Self::Response2 (x) => {
w.write_all (&[3])?;
// Measure length with dummy writes
// This is dumb, I'm just messing around to see if I can do
// this without allocating.
let mut dummy_writer = DummyWriter::default ();
Self::write_response_2 (&mut dummy_writer, x)?;
// Write length and real params to real output
let len = u32::try_from (dummy_writer.position)?;
w.write_all (&len.to_le_bytes ())?;
Self::write_response_2 (w, x)?;
},
}
Ok (())
}
fn write_mac_opt <W: std::io::Write> (w: &mut W, mac: Option <[u8; 6]>) -> Result <(), std::io::Error>
fn write_response_2 <W: Write> (w: &mut W, params: &Response2)
-> Result <(), MessageError>
{
w.write_all (&params.idem_id)?;
let nickname = params.nickname.as_bytes ();
tlv::Writer::<_>::lv_bytes (w, nickname)?;
Ok (())
}
fn write_mac_opt <W: Write> (w: &mut W, mac: Option <[u8; 6]>) -> Result <(), std::io::Error>
{
match mac {
Some (mac) => {
@ -63,14 +125,23 @@ impl Message {
Ok (())
}
pub fn to_vec (&self) -> Result <Vec <u8>, tlv::TlvError> {
pub fn to_vec (&self) -> Result <Vec <u8>, MessageError> {
let mut cursor = Cursor::new (Vec::with_capacity (PACKET_SIZE));
cursor.write_all (&MAGIC_NUMBER)?;
self.write (&mut cursor)?;
Ok (cursor.into_inner ())
}
pub fn read <R: std::io::Read> (r: &mut R) -> Result <Self, MessageError> {
tlv::Reader::expect (r, &MAGIC_NUMBER)?;
pub fn many_to_vec (msgs: &[Self]) -> Result <Vec <u8>, MessageError> {
let mut cursor = Cursor::new (Vec::with_capacity (PACKET_SIZE));
cursor.write_all (&MAGIC_NUMBER)?;
for msg in msgs {
msg.write (&mut cursor)?;
}
Ok (cursor.into_inner ())
}
fn read2 <R: std::io::Read> (r: &mut R) -> Result <Self, MessageError> {
let t = tlv::Reader::u8 (r)?;
Ok (match t {
@ -79,14 +150,28 @@ impl Message {
r.read_exact (&mut idem_id)?;
let mac = Self::read_mac_opt (r)?;
Self::Request {
Self::Request1 {
idem_id,
mac,
}
},
2 => {
let mac = Self::read_mac_opt (r)?;
Self::Response (mac)
Self::Response1 (mac)
},
3 => {
tlv::Reader::<_>::length (r)?;
let mut idem_id = [0; 8];
r.read_exact (&mut idem_id)?;
let nickname = tlv::Reader::<_>::lv_bytes_to_vec (r, 64)?;
let nickname = String::from_utf8 (nickname)?;
Self::Response2 (Response2 {
idem_id,
nickname,
})
},
_ => return Err (MessageError::UnknownType),
})
@ -105,16 +190,158 @@ impl Message {
})
}
pub fn from_slice (buf: &[u8]) -> Result <Self, MessageError> {
pub fn from_slice2 (buf: &[u8]) -> Result <Vec <Self>, MessageError> {
let mut cursor = Cursor::new (buf);
Self::read (&mut cursor)
tlv::Reader::expect (&mut cursor, &MAGIC_NUMBER)?;
let mut msgs = Vec::with_capacity (2);
while cursor.position () < u64::try_from (buf.len ())? {
let msg = Self::read2 (&mut cursor)?;
msgs.push (msg);
}
Ok (msgs)
}
}
#[cfg (test)]
mod test {
use super::*;
#[test]
fn test_1 () {
fn test_write_2 () -> Result <(), MessageError> {
for (input, expected) in [
(
vec! [
Message::Request1 {
idem_id: [1, 2, 3, 4, 5, 6, 7, 8,],
mac: None,
},
],
vec! [
154, 74, 67, 129,
// Request tag
1,
// Idem ID
1, 2, 3, 4, 5, 6, 7, 8,
// MAC is None
0,
],
),
(
vec! [
Message::Response1 (Some ([0x11, 0x22, 0x33, 0x44, 0x55, 0x66])),
Message::Response2 (Response2 {
idem_id: [1, 2, 3, 4, 5, 6, 7, 8,],
nickname: ":V".to_string (),
}),
],
vec! [
// Magic number for LookAround packets
154, 74, 67, 129,
// Response1 tag
2,
// MAC is Some
1,
// MAC
17, 34, 51, 68, 85, 102,
// Response2 tag
3,
// Length prefix
14, 0, 0, 0,
// Idem ID
1, 2, 3, 4, 5, 6, 7, 8,
// Length-prefixed string
2, 0, 0, 0,
58, 86,
],
),
] {
let actual = Message::many_to_vec (&input)?;
assert_eq! (actual, expected, "{:?}", input);
}
Ok (())
}
#[test]
fn test_write_1 () -> Result <(), MessageError> {
for (input, expected) in [
(
Message::Request1 {
idem_id: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08],
mac: None,
},
vec! [
154, 74, 67, 129,
// Request tag
1,
// Idem ID
1, 2, 3, 4, 5, 6, 7, 8,
// MAC is None
0,
],
),
(
Message::Response1 (Some ([0x11, 0x22, 0x33, 0x44, 0x55, 0x66])),
vec! [
// Magic number for LookAround packets
154, 74, 67, 129,
// Response tag
2,
// MAC is Some
1,
// MAC
17, 34, 51, 68, 85, 102,
],
),
(
Message::Response1 (None),
vec! [
// Magic number for LookAround packets
154, 74, 67, 129,
// Response tag
2,
// MAC is None
0,
],
),
].into_iter () {
let actual = input.to_vec ()?;
assert_eq! (actual, expected, "{:?}", input);
}
Ok (())
}
#[test]
fn test_read_2 () -> Result <(), MessageError> {
for input in [
vec! [
Message::Request1 {
idem_id: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08],
mac: None,
},
],
vec! [
Message::Response1 (Some ([0x11, 0x22, 0x33, 0x44, 0x55, 0x66])),
],
vec! [
Message::Response1 (None),
],
vec! [
Message::Response1 (Some ([0x11, 0x22, 0x33, 0x44, 0x55, 0x66])),
Message::Response2 (Response2 {
idem_id: [1, 2, 3, 4, 5, 6, 7, 8,],
nickname: ":V".to_string (),
}),
],
].into_iter () {
let encoded = Message::many_to_vec (&input)?;
let decoded = Message::from_slice2 (&encoded)?;
assert_eq! (input, decoded);
}
Ok (())
}
}

52
src/prelude.rs Normal file
View File

@ -0,0 +1,52 @@
pub use std::{
collections::HashMap,
env,
io::{
Cursor,
Write,
},
net::{
Ipv4Addr,
SocketAddr,
SocketAddrV4,
},
str::FromStr,
sync::Arc,
time::{
Duration,
Instant,
},
};
pub use configparser::ini::Ini;
pub use directories::ProjectDirs;
pub use mac_address::{
MacAddress,
get_mac_address,
};
pub use rand::RngCore;
pub use tokio::{
net::UdpSocket,
time::{
sleep,
timeout,
},
};
pub use crate::{
app_common::{
self,
LOOKAROUND_VERSION,
AppError,
CliArgError,
find_project_dirs,
recv_msg_from,
},
ip::get_ips,
message::{
self,
PACKET_SIZE,
Message,
},
tlv,
};

148
src/server.rs Normal file
View File

@ -0,0 +1,148 @@
use crate::prelude::*;
#[derive (Clone)]
struct Params {
common: app_common::Params,
bind_addrs: Vec <Ipv4Addr>,
nickname: String,
our_mac: Option <[u8; 6]>,
}
pub async fn server <I: Iterator <Item=String>> (args: I) -> Result <(), AppError>
{
match get_mac_address() {
Ok(Some(ma)) => {
println!("Our MAC addr = {}", ma);
}
Ok(None) => println!("No MAC address found."),
Err(e) => println!("{:?}", e),
}
let params = configure (args)?;
let socket = UdpSocket::bind (SocketAddrV4::new (Ipv4Addr::UNSPECIFIED, params.common.server_port)).await?;
for bind_addr in &params.bind_addrs {
if let Err (e) = socket.join_multicast_v4 (params.common.multicast_addr, *bind_addr) {
println! ("Error joining multicast group with iface {}: {:?}", bind_addr, e);
}
}
serve_interface (params, socket).await?;
Ok (())
}
fn configure <I: Iterator <Item=String>> (mut args: I) -> Result <Params, AppError>
{
let common = app_common::Params::default ();
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_local_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" => {
bind_addrs.push (match args.next () {
None => return Err (CliArgError::MissingArgumentValue (arg).into ()),
Some (x) => Ipv4Addr::from_str (&x)?,
});
},
"--nickname" => {
nickname = match args.next () {
None => return Err (CliArgError::MissingArgumentValue (arg).into ()),
Some (x) => x
};
},
_ => return Err (CliArgError::UnrecognizedArgument (arg).into ()),
}
}
let our_mac = get_mac_address ()?.map (|x| x.bytes ());
if our_mac.is_none () {
println! ("Warning: Can't find our own MAC address. We won't be able to respond to MAC-specific lookaround requests");
}
if bind_addrs.is_empty () {
println! ("No bind addresses given, auto-detecting all local IPs");
bind_addrs = get_ips ()?;
}
Ok (Params {
common,
bind_addrs,
nickname,
our_mac,
})
}
async fn serve_interface (
params: Params,
socket: UdpSocket,
)
-> Result <(), AppError>
{
let mut recent_idem_ids = Vec::with_capacity (32);
loop {
println! ("Listening...");
let (req_msgs, remote_addr) = match recv_msg_from (&socket).await {
Ok (x) => x,
Err (e) => {
println! ("Error while receiving message: {:?}", e);
continue;
},
};
let req = match req_msgs.into_iter ().next () {
Some (x) => x,
_ => {
println! ("Don't know how to handle this message, ignoring");
continue;
},
};
let resp = match req {
Message::Request1 {
mac: None,
idem_id,
} => {
if recent_idem_ids.contains (&idem_id) {
None
}
else {
recent_idem_ids.insert (0, idem_id);
recent_idem_ids.truncate (30);
Some (vec! [
Message::Response1 (params.our_mac),
Message::Response2 (message::Response2 {
idem_id,
nickname: params.nickname.clone (),
}),
])
}
},
_ => continue,
};
if let Some (resp) = resp {
socket.send_to (&Message::many_to_vec (&resp)?, remote_addr).await?;
}
}
}

View File

@ -6,8 +6,14 @@ type Result <T> = std::result::Result <T, TlvError>;
pub enum TlvError {
#[error ("Buffer too big")]
BufferTooBig,
#[error ("Caller-provided buffer too small")]
CallerBufferTooSmall,
// Violets are purple,
// To live is to suffer,
// The data is too big,
// For the gosh-darn buffer.
#[error ("Data too big")]
DataTooBig,
#[error (transparent)]
Io (#[from] std::io::Error),
#[error ("Actual bytes didn't match expected bytes")]
@ -54,22 +60,24 @@ impl <R: std::io::Read> Reader <R> {
Ok (())
}
fn length (r: &mut R) -> Result <u32> {
pub fn length (r: &mut R) -> Result <u32> {
let mut buf = [0; 4];
r.read_exact (&mut buf)?;
Ok (u32::from_le_bytes (buf))
}
pub fn lv_bytes (r: &mut R, buf: &mut [u8]) -> Result <u32> {
pub fn lv_bytes_to_vec (r: &mut R, limit: usize) -> Result <Vec <u8>> {
let l = Self::length (r)?;
if usize::try_from (l)? > buf.len () {
return Err (TlvError::CallerBufferTooSmall);
let l = usize::try_from (l)?;
if l > limit {
return Err (TlvError::DataTooBig);
}
r.read_exact (&mut buf [0..usize::try_from (l)?])?;
let mut v = vec! [0u8; l];
r.read_exact (&mut v)?;
Ok (l)
Ok (v)
}
pub fn u8 (r: &mut R) -> std::io::Result <u8> {
@ -82,15 +90,17 @@ impl <R: std::io::Read> Reader <R> {
#[cfg (test)]
mod test {
use super::*;
#[test]
fn test_1 () {
fn test_1 () -> Result <()> {
use std::io::Cursor;
let b = "hi there".as_bytes ();
let mut w = Cursor::new (Vec::default ());
super::Writer::lv_bytes (&mut w, b).unwrap ();
super::Writer::lv_bytes (&mut w, b)?;
let v = w.into_inner ();
@ -102,11 +112,11 @@ mod test {
let mut r = Cursor::new (v);
let mut buf = vec! [0; 1024];
let buf = Reader::lv_bytes_to_vec (&mut r, 1024)?;
let bytes_read = super::Reader::lv_bytes (&mut r, &mut buf).unwrap ();
assert_eq! (buf.len (), b.len ());
assert_eq! (b, &buf);
assert_eq! (usize::try_from (bytes_read).unwrap (), b.len ());
assert_eq! (b, &buf [0..usize::try_from (bytes_read).unwrap ()]);
Ok (())
}
}