├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── broadcast.rs └── multicast.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.13" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "atty" 14 | version = "0.2.14" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 17 | dependencies = [ 18 | "hermit-abi", 19 | "libc", 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "autodiscover-rs" 25 | version = "0.1.1" 26 | dependencies = [ 27 | "env_logger", 28 | "log", 29 | "socket2", 30 | ] 31 | 32 | [[package]] 33 | name = "cfg-if" 34 | version = "0.1.10" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 37 | 38 | [[package]] 39 | name = "env_logger" 40 | version = "0.7.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 43 | dependencies = [ 44 | "atty", 45 | "humantime", 46 | "log", 47 | "regex", 48 | "termcolor", 49 | ] 50 | 51 | [[package]] 52 | name = "hermit-abi" 53 | version = "0.1.14" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" 56 | dependencies = [ 57 | "libc", 58 | ] 59 | 60 | [[package]] 61 | name = "humantime" 62 | version = "1.3.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 65 | dependencies = [ 66 | "quick-error", 67 | ] 68 | 69 | [[package]] 70 | name = "lazy_static" 71 | version = "1.4.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 74 | 75 | [[package]] 76 | name = "libc" 77 | version = "0.2.71" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" 80 | 81 | [[package]] 82 | name = "log" 83 | version = "0.4.8" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 86 | dependencies = [ 87 | "cfg-if", 88 | ] 89 | 90 | [[package]] 91 | name = "memchr" 92 | version = "2.3.3" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 95 | 96 | [[package]] 97 | name = "quick-error" 98 | version = "1.2.3" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 101 | 102 | [[package]] 103 | name = "redox_syscall" 104 | version = "0.1.56" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 107 | 108 | [[package]] 109 | name = "regex" 110 | version = "1.3.9" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" 113 | dependencies = [ 114 | "aho-corasick", 115 | "memchr", 116 | "regex-syntax", 117 | "thread_local", 118 | ] 119 | 120 | [[package]] 121 | name = "regex-syntax" 122 | version = "0.6.18" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 125 | 126 | [[package]] 127 | name = "socket2" 128 | version = "0.3.12" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" 131 | dependencies = [ 132 | "cfg-if", 133 | "libc", 134 | "redox_syscall", 135 | "winapi", 136 | ] 137 | 138 | [[package]] 139 | name = "termcolor" 140 | version = "1.1.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 143 | dependencies = [ 144 | "winapi-util", 145 | ] 146 | 147 | [[package]] 148 | name = "thread_local" 149 | version = "1.0.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 152 | dependencies = [ 153 | "lazy_static", 154 | ] 155 | 156 | [[package]] 157 | name = "winapi" 158 | version = "0.3.9" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 161 | dependencies = [ 162 | "winapi-i686-pc-windows-gnu", 163 | "winapi-x86_64-pc-windows-gnu", 164 | ] 165 | 166 | [[package]] 167 | name = "winapi-i686-pc-windows-gnu" 168 | version = "0.4.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 171 | 172 | [[package]] 173 | name = "winapi-util" 174 | version = "0.1.5" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 177 | dependencies = [ 178 | "winapi", 179 | ] 180 | 181 | [[package]] 182 | name = "winapi-x86_64-pc-windows-gnu" 183 | version = "0.4.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 186 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "autodiscover-rs" 3 | version = "0.1.1" 4 | authors = ["Your Name "] 5 | edition = "2018" 6 | license-file = "LICENSE" 7 | description = "autodiscover-rs implements a simple algorithm to detect peers on an IP network, connects to them, and calls back with the connected stream. The algorthm supports both UDP broadcast and multicasting." 8 | homepage = "https://github.com/over-codes/autodiscover-rs" 9 | repository = "https://github.com/over-codes/autodiscover-rs" 10 | readme = "README.md" 11 | keywords = ["networking", "linux", "broadcast", "multicast"] 12 | categories = ["network-programming"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | socket2 = "0.3.12" 18 | log = "0.4" 19 | 20 | [dev-dependencies] 21 | env_logger = "0.7.1" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 over-codes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # autodiscover-rs 2 | 3 | autodiscover-rs implements a simple algorithm to detect peers on an 4 | IP network, connects to them, and calls back with the connected stream. The algorithm supports both UDP broadcast and multicasting. 5 | 6 | ## Usage 7 | 8 | Cargo.toml 9 | ```toml 10 | autodiscover-rs = "0.1.0" 11 | ``` 12 | 13 | In your app: 14 | 15 | ```Rust 16 | use std::net::{TcpListener, TcpStream}; 17 | use std::thread; 18 | 19 | use autodiscover_rs::{self, Method}; 20 | use env_logger; 21 | 22 | fn handle_client(stream: std::io::Result) { 23 | println!("Got a connection from {:?}", stream.unwrap().peer_addr()); 24 | } 25 | 26 | fn main() -> std::io::Result<()> { 27 | env_logger::init(); 28 | // make sure to bind before announcing ready 29 | let listener = TcpListener::bind(":::0")?; 30 | // get the port we were bound too; note that the trailing :0 above gives us a random unused port 31 | let socket = listener.local_addr()?; 32 | thread::spawn(move || { 33 | // this function blocks forever; running it a seperate thread 34 | autodiscover_rs::run(&socket, Method::Multicast("[ff0e::1]:1337".parse().unwrap()), |s| { 35 | // change this to task::spawn if using async_std or tokio 36 | thread::spawn(|| handle_client(s)); 37 | }).unwrap(); 38 | }); 39 | let mut incoming = listener.incoming(); 40 | while let Some(stream) = incoming.next() { 41 | // if you are using an async library, such as async_std or tokio, you can convert the stream to the 42 | // appropriate type before using task::spawn from your library of choice. 43 | thread::spawn(|| handle_client(stream)); 44 | } 45 | Ok(()) 46 | } 47 | ``` 48 | 49 | ## Notes 50 | 51 | The algorithm for peer discovery is to: 52 | - Send a message to the broadcast/multicast address with the configured 'listen address' compressed to a 6 byte (IPv4) or 18 byte (IPv6) packet 53 | - Start listening for new messages on the broadcast/multicast address; when one is recv., connect to it and run the callback 54 | 55 | This has a few gotchas: 56 | - If a broadcast packet goes missing, some connections won't be made 57 | 58 | 59 | ### Packet format 60 | 61 | The IP address we are broadcasting is of the form: 62 | 63 | IPv4: 64 | 65 | ```Rust 66 | buff[0..4].clone_from_slice(&addr.ip().octets()); 67 | buff[4..6].clone_from_slice(&addr.port().to_be_bytes()); 68 | ``` 69 | 70 | IPv6: 71 | 72 | ```Rust 73 | buff[0..16].clone_from_slice(&addr.ip().octets()); 74 | buff[16..18].clone_from_slice(&addr.port().to_be_bytes()); 75 | ``` 76 | 77 | ## TODO 78 | 79 | 1. Provide features for async frameworks (such as async_std and tokio) 80 | 2. Figure out some way of testing this 81 | 3. Provides some mechanism to stop the thread listening for discovery 82 | -------------------------------------------------------------------------------- /examples/broadcast.rs: -------------------------------------------------------------------------------- 1 | use std::net::{TcpListener, TcpStream}; 2 | use std::thread; 3 | 4 | use autodiscover_rs::{self, Method}; 5 | use env_logger; 6 | 7 | fn handle_client(stream: std::io::Result) { 8 | println!("Got a connection from {:?}", stream.unwrap().peer_addr()); 9 | } 10 | 11 | fn main() -> std::io::Result<()> { 12 | env_logger::init(); 13 | // make sure to bind before announcing ready 14 | let listener = TcpListener::bind("0.0.0.0:0")?; 15 | let socket = listener.local_addr()?; 16 | thread::spawn(move || { 17 | autodiscover_rs::run( 18 | &socket, 19 | Method::Broadcast("255.255.255.255:2020".parse().unwrap()), 20 | |s| { 21 | // change this to be async if using tokio or async_std 22 | thread::spawn(|| handle_client(s)); 23 | }, 24 | ) 25 | .unwrap(); 26 | }); 27 | let mut incoming = listener.incoming(); 28 | while let Some(stream) = incoming.next() { 29 | thread::spawn(|| handle_client(stream)); 30 | } 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /examples/multicast.rs: -------------------------------------------------------------------------------- 1 | use std::net::{TcpListener, TcpStream}; 2 | use std::thread; 3 | 4 | use autodiscover_rs::{self, Method}; 5 | use env_logger; 6 | 7 | fn handle_client(stream: std::io::Result) { 8 | println!("Got a connection from {:?}", stream.unwrap().peer_addr()); 9 | } 10 | 11 | fn main() -> std::io::Result<()> { 12 | env_logger::init(); 13 | // make sure to bind before announcing ready 14 | let listener = TcpListener::bind(":::0")?; 15 | // get the port we were bound too; note that the trailing :0 above gives us a random unused port 16 | let socket = listener.local_addr()?; 17 | thread::spawn(move || { 18 | // this function blocks forever; running it a seperate thread 19 | autodiscover_rs::run( 20 | &socket, 21 | Method::Multicast("[ff0e::1]:1337".parse().unwrap()), 22 | |s| { 23 | // change this to task::spawn if using async_std or tokio 24 | thread::spawn(|| handle_client(s)); 25 | }, 26 | ) 27 | .unwrap(); 28 | }); 29 | let mut incoming = listener.incoming(); 30 | while let Some(stream) = incoming.next() { 31 | // if you are using an async library, such as async_std or tokio, you can convert the stream to the 32 | // appropriate type before using task::spawn from your library of choice. 33 | thread::spawn(|| handle_client(stream)); 34 | } 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! autodiscovery-rs provides a function to automatically detect and connect to peers. 2 | //! 3 | //! # Examples 4 | //! 5 | //! ```rust,no_run 6 | //! use std::net::{TcpListener, TcpStream}; 7 | //! use std::thread; 8 | //! use autodiscover_rs::Method; 9 | //! use env_logger; 10 | //! 11 | //! fn handle_client(stream: std::io::Result) { 12 | //! println!("Got a connection from {:?}", stream.unwrap().peer_addr()); 13 | //! } 14 | //! 15 | //! fn main() -> std::io::Result<()> { 16 | //! env_logger::init(); 17 | //! // make sure to bind before announcing ready 18 | //! let listener = TcpListener::bind(":::0")?; 19 | //! // get the port we were bound too; note that the trailing :0 above gives us a random unused port 20 | //! let socket = listener.local_addr()?; 21 | //! thread::spawn(move || { 22 | //! // this function blocks forever; running it a seperate thread 23 | //! autodiscover_rs::run(&socket, Method::Multicast("[ff0e::1]:1337".parse().unwrap()), |s| { 24 | //! // change this to task::spawn if using async_std or tokio 25 | //! thread::spawn(|| handle_client(s)); 26 | //! }).unwrap(); 27 | //! }); 28 | //! let mut incoming = listener.incoming(); 29 | //! while let Some(stream) = incoming.next() { 30 | //! // if you are using an async library, such as async_std or tokio, you can convert the stream to the 31 | //! // appropriate type before using task::spawn from your library of choice. 32 | //! thread::spawn(|| handle_client(stream)); 33 | //! } 34 | //! Ok(()) 35 | //! } 36 | //! ``` 37 | use log::{trace, warn}; 38 | use socket2::{Domain, Socket, Type}; 39 | use std::convert::TryInto; 40 | use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream, UdpSocket}; 41 | 42 | /// Method describes whether a multicast or broadcast method for sending discovery messages should be used. 43 | pub enum Method { 44 | /// Broadcast is an IPv4-only method of sending discovery messages; use a value such as `"255.255.255.255:1337".parse()` or 45 | /// `"192.168.0.255:1337".parse()` when using this method. The latter value will be specific to your network setup. 46 | Broadcast(SocketAddr), 47 | /// Multicast supports both IPv6 and IPv4 for sending discovery methods; use a value such as `"224.0.0.1".parse()` for IPv4, or 48 | /// `"[ff0e::1]:1337".parse()` for IPv6. To be frank, IPv6 confuses me, but that address worked on my machine. 49 | Multicast(SocketAddr), 50 | } 51 | 52 | fn handle_broadcast_message)>( 53 | socket: UdpSocket, 54 | my_socket: &SocketAddr, 55 | callback: &F, 56 | ) -> std::io::Result<()> { 57 | let mut buff = vec![0; 18]; 58 | loop { 59 | let (bytes, _) = socket.recv_from(&mut buff)?; 60 | if let Ok(socket) = parse_bytes(bytes, &buff) { 61 | if socket == *my_socket { 62 | trace!("saw connection attempt from myself, this should happen once"); 63 | continue; 64 | } 65 | let stream = TcpStream::connect(socket); 66 | callback(stream); 67 | } 68 | } 69 | } 70 | 71 | fn parse_bytes(len: usize, buff: &[u8]) -> Result { 72 | let addr = match len { 73 | 6 => { 74 | let ip = IpAddr::V4(u32::from_be_bytes(buff[0..4].try_into().unwrap()).into()); 75 | let port = u16::from_be_bytes(buff[4..6].try_into().unwrap()); 76 | SocketAddr::new(ip, port) 77 | } 78 | 18 => { 79 | let ip: [u8; 16] = buff[0..16].try_into().unwrap(); 80 | let ip = ip.into(); 81 | let port = u16::from_be_bytes(buff[16..18].try_into().unwrap()); 82 | SocketAddr::new(ip, port) 83 | } 84 | _ => { 85 | warn!("Dropping malformed packet; length was {}", len); 86 | return Err(()); 87 | } 88 | }; 89 | Ok(addr) 90 | } 91 | 92 | fn to_bytes(connect_to: &SocketAddr) -> Vec { 93 | match connect_to { 94 | SocketAddr::V6(addr) => { 95 | // length is 16 bytes + 2 bytes 96 | let mut buff = vec![0; 18]; 97 | buff[0..16].clone_from_slice(&addr.ip().octets()); 98 | buff[16..18].clone_from_slice(&addr.port().to_be_bytes()); 99 | buff 100 | } 101 | SocketAddr::V4(addr) => { 102 | // length is 4 bytes + 2 bytes 103 | let mut buff = vec![0; 6]; 104 | buff[0..4].clone_from_slice(&addr.ip().octets()); 105 | buff[4..6].clone_from_slice(&addr.port().to_be_bytes()); 106 | buff 107 | } 108 | } 109 | } 110 | 111 | /// run will block forever. It sends a notification using the configured method, then listens for other notifications and begins 112 | /// connecting to them, calling spawn_callback (which should return right away!) with the connected streams. The connect_to address 113 | /// should be a socket we have already bind'ed too, since we advertise that to other autodiscovery clients. 114 | pub fn run)>( 115 | connect_to: &SocketAddr, 116 | method: Method, 117 | spawn_callback: F, 118 | ) -> std::io::Result<()> { 119 | match method { 120 | Method::Broadcast(addr) => { 121 | let socket = Socket::new(Domain::ipv4(), Type::dgram(), None)?; 122 | socket.set_reuse_address(true)?; 123 | socket.set_broadcast(true)?; 124 | socket.bind(&addr.into())?; 125 | let socket: UdpSocket = socket.into_udp_socket(); 126 | let result = socket.send_to(&to_bytes(connect_to), addr)?; 127 | trace!("sent {} byte announcement to {:?}", result, addr); 128 | handle_broadcast_message(socket, connect_to, &spawn_callback)?; 129 | } 130 | Method::Multicast(addr) => { 131 | let socket = Socket::new(Domain::ipv6(), Type::dgram(), None)?; 132 | socket.set_reuse_address(true)?; 133 | socket.bind(&addr.into())?; 134 | let socket: UdpSocket = socket.into_udp_socket(); 135 | match addr.ip() { 136 | IpAddr::V4(addr) => { 137 | let iface: Ipv4Addr = 0u32.into(); 138 | socket.join_multicast_v4(&addr, &iface)?; 139 | } 140 | IpAddr::V6(addr) => { 141 | socket.join_multicast_v6(&addr, 0)?; 142 | } 143 | } 144 | // we need a different, temporary socket, to send multicast in IPv6 145 | { 146 | let socket = UdpSocket::bind(":::0")?; 147 | let result = socket.send_to(&to_bytes(connect_to), addr)?; 148 | trace!("sent {} byte announcement to {:?}", result, addr); 149 | } 150 | handle_broadcast_message(socket, connect_to, &spawn_callback)?; 151 | } 152 | } 153 | warn!("It looks like I stopped listening; this shouldn't happen."); 154 | Ok(()) 155 | } 156 | --------------------------------------------------------------------------------