├── .gitignore ├── scripts ├── debugger.entitlements ├── build_universal_mac.sh └── run_with_leaks.sh ├── Cargo.toml ├── LICENSE ├── lib ├── tls │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── upgrade.rs │ │ ├── verifier.rs │ │ └── certificate.rs └── mdns │ ├── Cargo.toml │ ├── src │ ├── behaviour │ │ ├── socket.rs │ │ ├── timer.rs │ │ ├── iface │ │ │ ├── query.rs │ │ │ └── dns.rs │ │ └── iface.rs │ ├── lib.rs │ └── behaviour.rs │ └── CHANGELOG.md ├── src ├── main.rs └── network.rs ├── .github └── workflows │ └── draft-release.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | private-key.pem 3 | # IntelliJ based IDEs 4 | .idea 5 | 6 | # Finder (MacOS) folder config 7 | .DS_Store 8 | 9 | .cargo-home 10 | lib/**/target -------------------------------------------------------------------------------- /scripts/debugger.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.apple.security.get-task-allow 5 | 6 | 7 | -------------------------------------------------------------------------------- /scripts/build_universal_mac.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # This script builds a universal binary under macOS 5 | # Need to have both `arch64-apple-darwin` and `x86_64-apple-darwin` installed as target. 6 | 7 | cargo build --target=aarch64-apple-darwin --release 8 | cargo build --target=x86_64-apple-darwin --release 9 | mkdir -p ./target/universal-apple-darwin/release 10 | lipo -create -output ./target/universal-apple-darwin/release/p2p-clipboard ./target/x86_64-apple-darwin/release/p2p-clipboard ./target/aarch64-apple-darwin/release/p2p-clipboard -------------------------------------------------------------------------------- /scripts/run_with_leaks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # This script is a utility on Apple platforms to run 5 | # p2p-clipboard binaries under the `leaks` CLI tool, 6 | # which can help to diagnose memory leakage in any kind of 7 | # native or runtime-managed code. 8 | 9 | example_name="p2p-clipboard" 10 | 11 | script_dir=$(dirname $BASH_SOURCE[0]) 12 | 13 | # Build the example 14 | cargo build 15 | 16 | # Sign it with the required entitlements for process debugging. 17 | codesign -s - -v -f --entitlements "$script_dir/debugger.entitlements" "./target/debug/$example_name" 18 | 19 | # Run the example binary under `leaks` to look for any leaked objects. 20 | leaks --atExit -- ./target/debug/$example_name -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "p2p-clipboard" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = "A Peer-to-Peer clipboard syncing tool." 6 | license = "MIT" 7 | authors = ["gnattu"] 8 | 9 | [dependencies] 10 | arboard = { version = "3.6.1",features = ["wayland-data-control"] } 11 | clipboard-master = { git = "https://github.com/gnattu/clipboard-master.git" } 12 | tokio = { version = "1.38", features = ["full"] } 13 | async-task = "4.7.0" 14 | libp2p = { version = "0.56.0", features = ["tokio", "gossipsub", "macros", "identify", "kad", "tcp", "yamux"] } 15 | futures = "0.3.30" 16 | log = "0.4.20" 17 | clap = { version = "4.4.18", features = ["derive"] } 18 | ed25519-dalek = { version = "2.1.0", features = ["pkcs8", "pem"] } 19 | machine-uid = "0.5.1" 20 | zstd = "0.13.0" 21 | libp2p-mdns = { path = "./lib/mdns", features = ["tokio"] } 22 | libp2p-tls = { path = "./lib/tls" } 23 | hex-literal = "0.4.1" 24 | env_logger = "0.11.1" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 gnattu 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 | -------------------------------------------------------------------------------- /lib/tls/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.6.0 2 | 3 | - Sync upstream v0.6.0 4 | 5 | ## 0.4.0 6 | 7 | - Fork from upstream, tweaked for p2p-clipboard 8 | - Added runtime detection for cipher-suite selection. Prefer AES-GCM on CPUs with aes acceleration, otherwise prefer ChaCha20-Poly1305 9 | - Added extra authentication through a user-defined PSK. When verifying the signature defined in the certificate extension, the PSK is required to unroll the signature. 10 | 11 | ## 0.3.0 12 | 13 | - Migrate to `{In,Out}boundConnectionUpgrade` traits. 14 | See [PR 4695](https://github.com/libp2p/rust-libp2p/pull/4695). 15 | 16 | ## 0.2.1 17 | 18 | - Switch from webpki to rustls-webpki. 19 | This is a part of the resolution of the [RUSTSEC-2023-0052]. 20 | See [PR 4381]. 21 | 22 | [PR 4381]: https://github.com/libp2p/rust-libp2p/pull/4381 23 | [RUSTSEC-2023-0052]: https://rustsec.org/advisories/RUSTSEC-2023-0052.html 24 | 25 | ## 0.2.0 26 | 27 | - Raise MSRV to 1.65. 28 | See [PR 3715]. 29 | 30 | [PR 3715]: https://github.com/libp2p/rust-libp2p/pull/3715 31 | 32 | ## 0.1.0 33 | 34 | - Promote to `v0.1.0`. 35 | 36 | ## 0.1.0-alpha.2 37 | 38 | - Update to `libp2p-core` `v0.39.0`. 39 | 40 | ## 0.1.0-alpha 41 | 42 | Initial release. 43 | -------------------------------------------------------------------------------- /lib/mdns/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libp2p-mdns" 3 | edition = "2021" 4 | version = "0.48.0" 5 | description = "Implementation of the libp2p mDNS discovery method, tweaked for p2p-clipboard" 6 | authors = ["Parity Technologies "] 7 | license = "MIT" 8 | repository = "https://github.com/libp2p/rust-libp2p" 9 | keywords = ["peer-to-peer", "libp2p", "networking"] 10 | categories = ["network-programming", "asynchronous"] 11 | 12 | [dependencies] 13 | futures = "0.3.30" 14 | if-watch = "3.2.0" 15 | libp2p-core = "0.43.1" 16 | libp2p-swarm = "0.47.0" 17 | libp2p-identity = "0.2.13" 18 | rand = "0.8" 19 | smallvec = "1.13.2" 20 | socket2 = { version = "0.6.0", features = ["all"] } 21 | tokio = { version = "1.38", default-features = false, features = ["net", "time"], optional = true} 22 | tracing = "0.1.37" 23 | hickory-proto = { version = "0.25.2", default-features = false, features = ["mdns"] } 24 | uuid = { version = "1.7.0", features = ["v5"] } 25 | 26 | [features] 27 | tokio = ["dep:tokio", "if-watch/tokio"] 28 | 29 | [dev-dependencies] 30 | libp2p-swarm = { version = "0.47.0", features = ["tokio"] } 31 | tokio = { version = "1.35", default-features = false, features = ["macros", "rt", "rt-multi-thread", "time"] } 32 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 33 | 34 | # Passing arguments to the docsrs builder in order to properly document cfg's. 35 | # More information: https://docs.rs/about/builds#cross-compiling 36 | [package.metadata.docs.rs] 37 | all-features = true 38 | rustdoc-args = ["--cfg", "docsrs"] 39 | rustc-args = ["--cfg", "docsrs"] 40 | -------------------------------------------------------------------------------- /lib/tls/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libp2p-tls" 3 | version = "0.6.0" 4 | edition = "2021" 5 | description = "TLS configuration based on libp2p TLS specs. Tweaked for p2p-clipboard" 6 | repository = "https://github.com/libp2p/rust-libp2p" 7 | license = "MIT" 8 | exclude = ["src/test_assets"] 9 | 10 | [dependencies] 11 | futures = "0.3.31" 12 | futures-rustls = { version = "0.26.0", default-features = false } 13 | libp2p-core = "0.43.1" 14 | libp2p-identity = "0.2.13" 15 | rcgen = "0.13" 16 | ring = "0.17.12" 17 | thiserror = "2" 18 | webpki = { version = "0.101.4", package = "rustls-webpki", features = ["std"] } 19 | x509-parser = "0.16.0" 20 | yasna = "0.5.2" 21 | cpufeatures = "0.2.17" 22 | 23 | # Exposed dependencies. Breaking changes to these are breaking changes to us. 24 | [dependencies.rustls] 25 | version = "0.23.4" 26 | default-features = false 27 | features = ["ring", "std"] # Must enable this to allow for custom verification code. 28 | 29 | [dev-dependencies] 30 | hex = "0.4.3" 31 | hex-literal = "0.4.1" 32 | libp2p-core = "0.43.1" 33 | libp2p-identity = { version = "0.2.13", features = ["ed25519", "rsa", "secp256k1", "ecdsa", "rand"] } 34 | libp2p-swarm = { version = "0.47.0", features = ["tokio"] } 35 | libp2p-yamux = { version = "0.47.0" } 36 | tokio = { version = "1.38", features = ["full"] } 37 | 38 | # Passing arguments to the docsrs builder in order to properly document cfg's. 39 | # More information: https://docs.rs/about/builds#cross-compiling 40 | [package.metadata.docs.rs] 41 | all-features = true 42 | rustdoc-args = ["--cfg", "docsrs"] 43 | rustc-args = ["--cfg", "docsrs"] 44 | -------------------------------------------------------------------------------- /lib/mdns/src/behaviour/socket.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Parity Technologies (UK) Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | use std::{ 22 | io::Error, 23 | net::{SocketAddr, UdpSocket}, 24 | task::{Context, Poll}, 25 | }; 26 | 27 | /// Interface that must be implemented by the different runtimes to use the [`UdpSocket`] in async 28 | /// mode 29 | #[allow(unreachable_pub)] // Users should not depend on this. 30 | pub trait AsyncSocket: Unpin + Send + 'static { 31 | /// Create the async socket from the [`std::net::UdpSocket`] 32 | fn from_std(socket: UdpSocket) -> std::io::Result 33 | where 34 | Self: Sized; 35 | 36 | /// Attempts to receive a single packet on the socket 37 | /// from the remote address to which it is connected. 38 | fn poll_read( 39 | &mut self, 40 | _cx: &mut Context, 41 | _buf: &mut [u8], 42 | ) -> Poll>; 43 | 44 | /// Attempts to send data on the socket to a given address. 45 | fn poll_write( 46 | &mut self, 47 | _cx: &mut Context, 48 | _packet: &[u8], 49 | _to: SocketAddr, 50 | ) -> Poll>; 51 | } 52 | 53 | #[cfg(feature = "tokio")] 54 | pub(crate) mod tokio { 55 | use ::tokio::{io::ReadBuf, net::UdpSocket as TkUdpSocket}; 56 | 57 | use super::*; 58 | 59 | /// Tokio ASync Socket` 60 | pub(crate) type TokioUdpSocket = TkUdpSocket; 61 | impl AsyncSocket for TokioUdpSocket { 62 | fn from_std(socket: UdpSocket) -> std::io::Result { 63 | socket.set_nonblocking(true)?; 64 | TokioUdpSocket::from_std(socket) 65 | } 66 | 67 | fn poll_read( 68 | &mut self, 69 | cx: &mut Context, 70 | buf: &mut [u8], 71 | ) -> Poll> { 72 | let mut rbuf = ReadBuf::new(buf); 73 | match self.poll_recv_from(cx, &mut rbuf) { 74 | Poll::Pending => Poll::Pending, 75 | Poll::Ready(Err(err)) => Poll::Ready(Err(err)), 76 | Poll::Ready(Ok(addr)) => Poll::Ready(Ok((rbuf.filled().len(), addr))), 77 | } 78 | } 79 | 80 | fn poll_write( 81 | &mut self, 82 | cx: &mut Context, 83 | packet: &[u8], 84 | to: SocketAddr, 85 | ) -> Poll> { 86 | match self.poll_send_to(cx, packet, to) { 87 | Poll::Pending => Poll::Pending, 88 | Poll::Ready(Err(err)) => Poll::Ready(Err(err)), 89 | Poll::Ready(Ok(_len)) => Poll::Ready(Ok(())), 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/mdns/src/behaviour/timer.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Parity Technologies (UK) Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | use std::time::{Duration, Instant}; 22 | 23 | /// Simple wrapper for the different type of timers 24 | #[derive(Debug)] 25 | #[cfg(feature = "tokio")] 26 | pub struct Timer { 27 | inner: T, 28 | } 29 | 30 | /// Builder interface to homogenize the different implementations 31 | #[allow(unreachable_pub)] // Users should not depend on this. 32 | pub trait Builder: Send + Unpin + 'static { 33 | /// Creates a timer that emits an event once at the given time instant. 34 | fn at(instant: Instant) -> Self; 35 | 36 | /// Creates a timer that emits events periodically. 37 | fn interval(duration: Duration) -> Self; 38 | 39 | /// Creates a timer that emits events periodically, starting at start. 40 | fn interval_at(start: Instant, duration: Duration) -> Self; 41 | } 42 | 43 | #[cfg(feature = "tokio")] 44 | pub(crate) mod tokio { 45 | use std::{ 46 | pin::Pin, 47 | task::{Context, Poll}, 48 | }; 49 | 50 | use ::tokio::time::{self, Instant as TokioInstant, Interval, MissedTickBehavior}; 51 | use futures::Stream; 52 | 53 | use super::*; 54 | 55 | /// Tokio wrapper 56 | pub(crate) type TokioTimer = Timer; 57 | impl Builder for TokioTimer { 58 | fn at(instant: Instant) -> Self { 59 | // Taken from: https://docs.rs/async-io/1.7.0/src/async_io/lib.rs.html#91 60 | let mut inner = time::interval_at( 61 | TokioInstant::from_std(instant), 62 | Duration::new(u64::MAX, 1_000_000_000 - 1), 63 | ); 64 | inner.set_missed_tick_behavior(MissedTickBehavior::Skip); 65 | Self { inner } 66 | } 67 | 68 | fn interval(duration: Duration) -> Self { 69 | let mut inner = time::interval_at(TokioInstant::now() + duration, duration); 70 | inner.set_missed_tick_behavior(MissedTickBehavior::Skip); 71 | Self { inner } 72 | } 73 | 74 | fn interval_at(start: Instant, duration: Duration) -> Self { 75 | let mut inner = time::interval_at(TokioInstant::from_std(start), duration); 76 | inner.set_missed_tick_behavior(MissedTickBehavior::Skip); 77 | Self { inner } 78 | } 79 | } 80 | 81 | impl Stream for TokioTimer { 82 | type Item = TokioInstant; 83 | 84 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 85 | self.inner.poll_tick(cx).map(Some) 86 | } 87 | 88 | fn size_hint(&self) -> (usize, Option) { 89 | (usize::MAX, None) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/mdns/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Parity Technologies (UK) Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! Implementation of the libp2p-specific [mDNS](https://github.com/libp2p/specs/blob/master/discovery/mdns.md) protocol. 22 | //! 23 | //! mDNS is a protocol defined by [RFC 6762](https://tools.ietf.org/html/rfc6762) that allows 24 | //! querying nodes that correspond to a certain domain name. 25 | //! 26 | //! In the context of libp2p, the mDNS protocol is used to discover other nodes on the local 27 | //! network that support libp2p. 28 | //! 29 | //! # Usage 30 | //! 31 | //! This crate provides `TokioMdns` which implements the `NetworkBehaviour` trait. This struct will automatically discover other 32 | //! libp2p nodes on the local network. 33 | 34 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 35 | 36 | use std::net::{Ipv4Addr, Ipv6Addr}; 37 | use std::time::Duration; 38 | 39 | mod behaviour; 40 | #[cfg(feature = "tokio")] 41 | pub use crate::behaviour::tokio; 42 | pub use crate::behaviour::{Behaviour, Event}; 43 | 44 | pub(crate) const DEFAULT_SERVICE_NAME: &str = "_p2pclipboard._udp.local"; 45 | /// The meta query for looking up the `SERVICE_NAME`. 46 | const META_QUERY_SERVICE: &[u8] = b"_services._dns-sd._udp.local"; 47 | /// `META_QUERY_SERVICE` as a Fully Qualified Domain Name. 48 | const META_QUERY_SERVICE_FQDN: &str = "_services._dns-sd._udp.local."; 49 | 50 | pub const IPV4_MDNS_MULTICAST_ADDRESS: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251); 51 | pub const IPV6_MDNS_MULTICAST_ADDRESS: Ipv6Addr = Ipv6Addr::new(0xFF02, 0, 0, 0, 0, 0, 0, 0xFB); 52 | 53 | /// Configuration for mDNS. 54 | #[derive(Debug, Clone)] 55 | pub struct Config { 56 | /// TTL to use for mdns records. 57 | pub ttl: Duration, 58 | /// Interval at which to poll the network for new peers. This isn't 59 | /// necessary during normal operation but avoids the case that an 60 | /// initial packet was lost and not discovering any peers until a new 61 | /// peer joins the network. Receiving an mdns packet resets the timer 62 | /// preventing unnecessary traffic. 63 | pub query_interval: Duration, 64 | /// Use IPv6 instead of IPv4. 65 | pub enable_ipv6: bool, 66 | /// When set to true, all mDNS operation will be no-op. 67 | pub disabled: bool, 68 | /// Optional service fingerprint. 69 | /// 70 | /// If set, a 128bit UUID derived from this seed will be appended to the mDNS service name, 71 | /// preventing discovery across different fingerprints. 72 | /// 73 | /// Do not use sensitive information directly. UUID v5 is not a secure hash. 74 | pub service_fingerprint: Option>, 75 | } 76 | 77 | impl Default for Config { 78 | fn default() -> Self { 79 | Self { 80 | ttl: Duration::from_secs(6 * 60), 81 | query_interval: Duration::from_secs(5 * 60), 82 | enable_ipv6: false, 83 | disabled: false, 84 | service_fingerprint: None, 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/tls/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Parity Technologies (UK) Ltd. 2 | // Copyright 2022 Protocol Labs. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a 5 | // copy of this software and associated documentation files (the "Software"), 6 | // to deal in the Software without restriction, including without limitation 7 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | // and/or sell copies of the Software, and to permit persons to whom the 9 | // Software is furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | // DEALINGS IN THE SOFTWARE. 21 | 22 | //! TLS configuration based on libp2p TLS specs. 23 | //! 24 | //! See . 25 | 26 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 27 | 28 | pub mod certificate; 29 | mod upgrade; 30 | mod verifier; 31 | 32 | use libp2p_identity::Keypair; 33 | use libp2p_identity::PeerId; 34 | use std::sync::Arc; 35 | 36 | pub use futures_rustls::TlsStream; 37 | pub use upgrade::Config; 38 | pub use upgrade::UpgradeError; 39 | 40 | const P2P_ALPN: [u8; 12] = *b"p2pclipboard"; 41 | cpufeatures::new!(cpuid_aes, "aes"); 42 | 43 | /// Create a TLS client configuration for libp2p. 44 | pub fn make_client_config( 45 | keypair: &Keypair, 46 | remote_peer_id: Option, 47 | psk: Option 48 | ) -> Result { 49 | let (certificate, private_key) = certificate::generate(keypair, psk.clone())?; 50 | let token = cpuid_aes::init(); 51 | let mut provider = rustls::crypto::ring::default_provider(); 52 | let cipher_suites = match token.get() { 53 | true => verifier::CIPHERSUITES, 54 | false => verifier::CIPHERSUITES_WITHOUT_AES, 55 | }; 56 | provider.cipher_suites = cipher_suites.to_vec(); 57 | 58 | let cert_resolver = Arc::new( 59 | certificate::AlwaysResolvesCert::new(certificate, &private_key) 60 | .expect("Client cert key DER is valid; qed"), 61 | ); 62 | 63 | let mut crypto = rustls::ClientConfig::builder_with_provider(provider.into()) 64 | .with_protocol_versions(verifier::PROTOCOL_VERSIONS) 65 | .expect("Cipher suites and kx groups are configured; qed") 66 | .dangerous() 67 | .with_custom_certificate_verifier(Arc::new( 68 | verifier::Libp2pCertificateVerifier::with_remote_peer_id(remote_peer_id, psk), 69 | )) 70 | .with_client_cert_resolver(cert_resolver); 71 | crypto.alpn_protocols = vec![P2P_ALPN.to_vec()]; 72 | 73 | Ok(crypto) 74 | } 75 | 76 | /// Create a TLS server configuration for libp2p. 77 | pub fn make_server_config( 78 | keypair: &Keypair, 79 | psk: Option 80 | ) -> Result { 81 | let (certificate, private_key) = certificate::generate(keypair, psk.clone())?; 82 | let token = cpuid_aes::init(); 83 | let mut provider = rustls::crypto::ring::default_provider(); 84 | let cipher_suites = match token.get() { 85 | true => verifier::CIPHERSUITES, 86 | false => verifier::CIPHERSUITES_WITHOUT_AES, 87 | }; 88 | provider.cipher_suites = cipher_suites.to_vec(); 89 | 90 | let cert_resolver = Arc::new( 91 | certificate::AlwaysResolvesCert::new(certificate, &private_key) 92 | .expect("Server cert key DER is valid; qed"), 93 | ); 94 | 95 | let mut crypto = rustls::ServerConfig::builder_with_provider(provider.into()) 96 | .with_protocol_versions(verifier::PROTOCOL_VERSIONS) 97 | .expect("Cipher suites and kx groups are configured; qed") 98 | .with_client_cert_verifier(Arc::new(verifier::Libp2pCertificateVerifier::new(psk))) 99 | .with_cert_resolver(cert_resolver); 100 | crypto.alpn_protocols = vec![P2P_ALPN.to_vec()]; 101 | 102 | Ok(crypto) 103 | } 104 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod network; 2 | 3 | use arboard::Clipboard; 4 | use clap::Parser; 5 | use clipboard_master::{CallbackResult, ClipboardHandler, Master}; 6 | use env_logger::Env; 7 | use log::{debug, error}; 8 | use std::io; 9 | use tokio::sync::{mpsc, oneshot}; 10 | struct Handler { 11 | sender: mpsc::Sender, 12 | } 13 | 14 | impl ClipboardHandler for Handler { 15 | fn on_clipboard_change(&mut self) -> CallbackResult { 16 | debug!("Clipboard change happened!"); 17 | get_clipboard_content(self.sender.clone()); 18 | CallbackResult::Next 19 | } 20 | 21 | fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { 22 | error!("Error: {}", error); 23 | CallbackResult::Next 24 | } 25 | 26 | fn sleep_interval(&self) -> core::time::Duration { 27 | core::time::Duration::from_millis(1000) 28 | } 29 | } 30 | 31 | #[derive(Parser, Debug)] 32 | #[command(version = env!("CARGO_PKG_VERSION"), author = env!("CARGO_PKG_AUTHORS"), about = env!("CARGO_PKG_DESCRIPTION"))] 33 | struct Args { 34 | /// The remote peer to connect to on boot up. 35 | #[arg(short, long, num_args = 2, value_names = ["IP:PORT", "PEER_ID"])] 36 | connect: Option>, 37 | /// Path to custom private key. The key should be an ED25519 private key in PEM format. 38 | #[arg(short, long, value_name = "PATH")] 39 | key: Option, 40 | /// Local address to listen on. 41 | #[arg(short, long, value_name = "IP:PORT")] 42 | listen: Option, 43 | /// Pre-shared key. Only nodes with same key can connect to each other. 44 | #[arg(short, long)] 45 | psk: Option, 46 | /// If set, no mDNS broadcasts will be made. 47 | #[arg(short, long)] 48 | no_mdns: bool, 49 | } 50 | 51 | fn get_clipboard_content(sender: mpsc::Sender) { 52 | let mut ctx = match Clipboard::new() { 53 | Ok(context) => context, 54 | Err(err) => { 55 | error!("Error creating ClipboardContext: {}", err); 56 | return; 57 | } 58 | }; 59 | 60 | // TODO: handle non-text contents 61 | match ctx.get_text() { 62 | Ok(contents) => { 63 | sender.try_send(contents).unwrap(); 64 | } 65 | Err(err) => error!("Error getting clipboard contents: {}", err), 66 | } 67 | } 68 | 69 | fn set_clipboard_content(content: &str) { 70 | let mut ctx = match Clipboard::new() { 71 | Ok(context) => context, 72 | Err(err) => { 73 | error!("Error creating ClipboardContext: {}", err); 74 | return; 75 | } 76 | }; 77 | let _ = ctx.set_text(content); 78 | } 79 | 80 | fn create_clipboard_monitor(sender: mpsc::Sender) -> Master { 81 | let handler = Handler { sender }; 82 | let master = Master::new(handler); 83 | return master.unwrap(); 84 | } 85 | 86 | async fn channel_proxy(mut rx: mpsc::Receiver, mut shutdown: oneshot::Receiver<()>) { 87 | loop { 88 | tokio::select! { 89 | Some(message) = rx.recv() => { 90 | set_clipboard_content(message.as_ref()); 91 | debug!("Proxy received: {}", message); 92 | }, 93 | _ = &mut shutdown => { 94 | debug!("Proxy shutdown received"); 95 | return; 96 | }, 97 | } 98 | } 99 | } 100 | 101 | #[tokio::main] 102 | async fn main() { 103 | env_logger::Builder::from_env(Env::default().default_filter_or("info")) 104 | .target(env_logger::Target::Stdout) 105 | .init(); 106 | let Args { 107 | connect, 108 | key, 109 | listen, 110 | psk, 111 | no_mdns, 112 | } = Args::parse(); 113 | loop { 114 | // clipboard tx channel 115 | let (from_clipboard_tx, from_clipboard_rx) = mpsc::channel::(32); 116 | // clipboard rx channel 117 | let (to_clipboard_tx, to_clipboard_rx) = mpsc::channel::(32); 118 | let (shutdown_proxy_tx, shutdown_proxy_rx) = oneshot::channel::<()>(); 119 | // We cannot move handlers on Windows because *mut c_void cannot be moved. Create a channel to capture the shutdown channel. 120 | let (shutdown_channel_tx, shutdown_channel_rx) = oneshot::channel(); 121 | let _ = tokio::spawn(channel_proxy(to_clipboard_rx, shutdown_proxy_rx)); 122 | // Clipboard functionality is fully synchronous, so it is impossible to have it integrated in tokio runtime as it is. 123 | // We have to start a dedicated thread instead of run it in tokio runtime. 124 | std::thread::spawn(move || { 125 | let mut monitor = create_clipboard_monitor(from_clipboard_tx); 126 | let shutdown = monitor.shutdown_channel(); 127 | let _ = shutdown_channel_tx.send(shutdown); 128 | monitor.run() 129 | }); 130 | let result = network::start_network( 131 | from_clipboard_rx, 132 | to_clipboard_tx, 133 | connect.clone(), 134 | key.clone(), 135 | listen.clone(), 136 | psk.clone(), 137 | no_mdns, 138 | ) 139 | .await; 140 | if let Err(error_in_network) = result { 141 | error!("Fatal Error: {}", error_in_network); 142 | std::process::exit(1); 143 | } 144 | shutdown_channel_rx.await.unwrap().signal(); 145 | let _ = shutdown_proxy_tx.send(()); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/tls/src/upgrade.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Protocol Labs. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | use crate::certificate; 22 | use crate::certificate::P2pCertificate; 23 | use futures::future::BoxFuture; 24 | use futures::AsyncWrite; 25 | use futures::{AsyncRead, FutureExt}; 26 | use futures_rustls::TlsStream; 27 | use libp2p_core::upgrade::{InboundConnectionUpgrade, OutboundConnectionUpgrade}; 28 | use libp2p_core::UpgradeInfo; 29 | use libp2p_identity as identity; 30 | use libp2p_identity::PeerId; 31 | use rustls::{CommonState, pki_types::ServerName}; 32 | use std::net::{IpAddr, Ipv4Addr}; 33 | use std::sync::Arc; 34 | 35 | #[derive(thiserror::Error, Debug)] 36 | pub enum UpgradeError { 37 | #[error("Failed to generate certificate")] 38 | CertificateGeneration(#[from] certificate::GenError), 39 | #[error("Failed to upgrade server connection")] 40 | ServerUpgrade(std::io::Error), 41 | #[error("Failed to upgrade client connection")] 42 | ClientUpgrade(std::io::Error), 43 | #[error("Failed to parse certificate")] 44 | BadCertificate(#[from] certificate::ParseError), 45 | } 46 | 47 | #[derive(Clone)] 48 | pub struct Config { 49 | server: rustls::ServerConfig, 50 | client: rustls::ClientConfig, 51 | pre_shared_key: Option, 52 | } 53 | 54 | impl Config { 55 | pub fn new(identity: &identity::Keypair) -> Result { 56 | Ok(Self { 57 | server: crate::make_server_config(identity, None)?, 58 | client: crate::make_client_config(identity, None, None)?, 59 | pre_shared_key: None, 60 | }) 61 | } 62 | 63 | pub fn new_with_psk(psk: Option) -> impl Fn(&identity::Keypair) -> Result { 64 | move |identity| Ok(Self { 65 | server: crate::make_server_config(identity, psk.clone())?, 66 | client: crate::make_client_config(identity, None, psk.clone())?, 67 | pre_shared_key: psk.clone(), 68 | }) 69 | } 70 | } 71 | 72 | impl UpgradeInfo for Config { 73 | type Info = &'static str; 74 | type InfoIter = std::iter::Once; 75 | 76 | fn protocol_info(&self) -> Self::InfoIter { 77 | std::iter::once("/tls/1.0.0") 78 | } 79 | } 80 | 81 | impl InboundConnectionUpgrade for Config 82 | where 83 | C: AsyncRead + AsyncWrite + Send + Unpin + 'static, 84 | { 85 | type Output = (PeerId, TlsStream); 86 | type Error = UpgradeError; 87 | type Future = BoxFuture<'static, Result>; 88 | 89 | fn upgrade_inbound(self, socket: C, _: Self::Info) -> Self::Future { 90 | async move { 91 | let stream = futures_rustls::TlsAcceptor::from(Arc::new(self.server)) 92 | .accept(socket) 93 | .await 94 | .map_err(UpgradeError::ServerUpgrade)?; 95 | 96 | let peer_id = extract_single_certificate(stream.get_ref().1, self.pre_shared_key)?.peer_id(); 97 | 98 | Ok((peer_id, stream.into())) 99 | } 100 | .boxed() 101 | } 102 | } 103 | 104 | impl OutboundConnectionUpgrade for Config 105 | where 106 | C: AsyncRead + AsyncWrite + Send + Unpin + 'static, 107 | { 108 | type Output = (PeerId, TlsStream); 109 | type Error = UpgradeError; 110 | type Future = BoxFuture<'static, Result>; 111 | 112 | fn upgrade_outbound(self, socket: C, _: Self::Info) -> Self::Future { 113 | async move { 114 | // Spec: In order to keep this flexibility for future versions, clients that only 115 | // support the version of the handshake defined in this document MUST NOT send any value 116 | // in the Server Name Indication. Setting `ServerName` to unspecified will 117 | // disable the use of the SNI extension. 118 | let name = ServerName::IpAddress(rustls::pki_types::IpAddr::from(IpAddr::V4( 119 | Ipv4Addr::UNSPECIFIED, 120 | ))); 121 | 122 | let stream = futures_rustls::TlsConnector::from(Arc::new(self.client)) 123 | .connect(name, socket) 124 | .await 125 | .map_err(UpgradeError::ClientUpgrade)?; 126 | 127 | let peer_id = extract_single_certificate(stream.get_ref().1, self.pre_shared_key)?.peer_id(); 128 | 129 | Ok((peer_id, stream.into())) 130 | } 131 | .boxed() 132 | } 133 | } 134 | 135 | fn extract_single_certificate( 136 | state: &CommonState, 137 | psk: Option, 138 | ) -> Result, certificate::ParseError> { 139 | let Some([cert]) = state.peer_certificates() else { 140 | panic!("config enforces exactly one certificate"); 141 | }; 142 | 143 | certificate::parse(cert, psk) 144 | } 145 | -------------------------------------------------------------------------------- /.github/workflows/draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Draft release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: "Git tag for the release (e.g. v0.1.0)" 8 | required: true 9 | type: string 10 | draft: 11 | description: "Create the GitHub release as a draft" 12 | required: true 13 | default: true 14 | type: boolean 15 | prerelease: 16 | description: "Mark the GitHub release as a prerelease" 17 | required: true 18 | default: false 19 | type: boolean 20 | 21 | permissions: 22 | contents: write 23 | 24 | jobs: 25 | build: 26 | name: Build (${{ matrix.asset }}) 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | include: 31 | - os: ubuntu-latest 32 | target: x86_64-unknown-linux-gnu 33 | asset: p2p-clipboard-x86_64-unknown-linux-gnu.zip 34 | kind: linux 35 | - os: ubuntu-24.04-arm 36 | target: aarch64-unknown-linux-gnu 37 | asset: p2p-clipboard-aarch64-unknown-linux-gnu.zip 38 | kind: linux 39 | - os: windows-latest 40 | target: x86_64-pc-windows-msvc 41 | asset: p2p-clipboard-x86_64-pc-windows-msvc.zip 42 | kind: windows 43 | - os: macos-latest 44 | target: "" 45 | asset: p2p-clipboard-universal-apple-darwin.zip 46 | kind: macos-universal 47 | 48 | runs-on: ${{ matrix.os }} 49 | steps: 50 | - uses: actions/checkout@v4 51 | 52 | - name: Install Rust toolchain 53 | uses: dtolnay/rust-toolchain@stable 54 | 55 | - name: Install Rust target 56 | if: ${{ matrix.target != '' }} 57 | run: | 58 | rustup target add "${{ matrix.target }}" 59 | 60 | - name: Install macOS targets (universal build) 61 | if: matrix.kind == 'macos-universal' 62 | run: | 63 | rustup target add aarch64-apple-darwin x86_64-apple-darwin 64 | 65 | - name: Rust cache 66 | uses: Swatinem/rust-cache@v2 67 | 68 | - name: Install Linux system deps 69 | if: matrix.kind == 'linux' 70 | run: | 71 | sudo apt-get update 72 | sudo apt-get install -y --no-install-recommends pkg-config libwayland-dev libxkbcommon-dev 73 | 74 | - name: Build (Linux) 75 | if: matrix.kind == 'linux' 76 | run: | 77 | cargo build --locked --release --target "${{ matrix.target }}" 78 | 79 | - name: Package (Linux) 80 | if: matrix.kind == 'linux' 81 | run: | 82 | rm -rf dist 83 | mkdir -p dist 84 | cp "target/${{ matrix.target }}/release/p2p-clipboard" dist/p2p-clipboard 85 | (cd dist && zip -9 "../${{ matrix.asset }}" p2p-clipboard) 86 | 87 | - name: Build (Windows) 88 | if: matrix.kind == 'windows' 89 | run: | 90 | cargo build --locked --release --target "${{ matrix.target }}" 91 | 92 | - name: Package (Windows) 93 | if: matrix.kind == 'windows' 94 | shell: pwsh 95 | run: | 96 | Remove-Item -Recurse -Force dist -ErrorAction SilentlyContinue 97 | New-Item -ItemType Directory -Path dist | Out-Null 98 | Copy-Item "target\\${{ matrix.target }}\\release\\p2p-clipboard.exe" "dist\\p2p-clipboard.exe" 99 | Compress-Archive -Path "dist\\p2p-clipboard.exe" -DestinationPath "${{ matrix.asset }}" -Force 100 | 101 | - name: Build (macOS universal) 102 | if: matrix.kind == 'macos-universal' 103 | run: | 104 | bash scripts/build_universal_mac.sh 105 | 106 | - name: Package (macOS universal) 107 | if: matrix.kind == 'macos-universal' 108 | run: | 109 | rm -rf dist 110 | mkdir -p dist 111 | cp "target/universal-apple-darwin/release/p2p-clipboard" dist/p2p-clipboard 112 | (cd dist && zip -9 "../${{ matrix.asset }}" p2p-clipboard) 113 | 114 | - name: Upload artifact 115 | uses: actions/upload-artifact@v4 116 | with: 117 | name: ${{ matrix.asset }} 118 | path: ${{ matrix.asset }} 119 | if-no-files-found: error 120 | 121 | release: 122 | name: Draft GitHub release 123 | runs-on: ubuntu-latest 124 | needs: [build] 125 | env: 126 | GH_TOKEN: ${{ github.token }} 127 | GH_REPO: ${{ github.repository }} 128 | TAG: ${{ inputs.tag }} 129 | DRAFT: ${{ inputs.draft }} 130 | PRERELEASE: ${{ inputs.prerelease }} 131 | steps: 132 | - name: Download build artifacts 133 | uses: actions/download-artifact@v4 134 | with: 135 | path: dist 136 | 137 | - name: Create or update release 138 | run: | 139 | set -euo pipefail 140 | 141 | draft_flag=() 142 | if [ "${DRAFT}" = "true" ]; then 143 | draft_flag+=(--draft) 144 | fi 145 | 146 | prerelease_flag=() 147 | if [ "${PRERELEASE}" = "true" ]; then 148 | prerelease_flag+=(--prerelease) 149 | fi 150 | 151 | if gh release view "${TAG}" --repo "${GH_REPO}" >/dev/null 2>&1; then 152 | echo "Release ${TAG} already exists. Uploading assets." 153 | else 154 | gh release create "${TAG}" --repo "${GH_REPO}" \ 155 | --title "${TAG}" \ 156 | --notes "Automated build artifacts." \ 157 | --target "${GITHUB_SHA}" \ 158 | "${draft_flag[@]}" \ 159 | "${prerelease_flag[@]}" 160 | fi 161 | 162 | mapfile -d '' assets < <(find dist -type f -name 'p2p-clipboard-*.zip' -print0) 163 | if [ "${#assets[@]}" -eq 0 ]; then 164 | echo "No zip assets found under dist/" 165 | exit 1 166 | fi 167 | 168 | gh release upload "${TAG}" --repo "${GH_REPO}" "${assets[@]}" --clobber 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # p2p-clipboard 2 | 3 | p2p-clipboard is a Peer-to-Peer cross-platform clipboard syncing tool. It enables users to synchronize clipboard contents across multiple machines without the need for a centralized server. 4 | 5 | Currently, it supports Windows, macOS, and partially Linux platforms. See the [Limitation section](#limitation) for Linux support status. 6 | 7 | ## Features 8 | 9 | - Peer-to-Peer clipboard syncing: Sync clipboard contents across machines seamlessly. 10 | - Cross-platform compatibility: Works on Windows, macOS, and partially on Linux. 11 | - Decentralized and [flexible architecture](#network-flexibility): No need for a centralized server, and works for most network topologies. 12 | - Easy setup and usage: Zero config for basic usage. 13 | 14 | ## Installation 15 | 16 | ### GUI Wrappers: 17 | 18 | - Tray application wrapper for Windows is available at [p2p-clipboard-windows](https://github.com/gnattu/p2p-clipboard-windows). 19 | - Menu bar application wrapper for macOS is available at [p2p-clipboard-mac](https://github.com/gnattu/p2p-clipboard-mac). 20 | 21 | ### Pre-built binaries: 22 | 23 | Pre-built binaries for Windows(x86-64), macOS(universal) and Linux(x86-64 and arm64) are available on the [release](./release) page. 24 | 25 | macOS users need to do `xattr -d com.apple.quarantine /path/to/p2p-clipboard` first to run it. 26 | 27 | ### Build from source: 28 | 29 | ```shell 30 | git clone https://github.com/gnattu/p2p-clipboard.git 31 | cd p2p-clipboard 32 | cargo build --release 33 | ``` 34 | Please note: if you are using Linux, you will also need `libxcb` and its dev-dependencies installed. 35 | 36 | ## Usage 37 | 38 | ```shell 39 | A Peer-to-Peer clipboard syncing tool. 40 | 41 | Usage: p2p-clipboard [OPTIONS] 42 | 43 | Options: 44 | -c, --connect The remote peer to connect to on boot up 45 | -k, --key Path to custom private key. The key should be an ED25519 private key in PEM format 46 | -l, --listen Local address to listen on 47 | -p, --psk Pre-shared key. Only nodes with same key can connect to each other 48 | -n, --no-mdns If set, no mDNS broadcasts will be made 49 | -h, --help Print help 50 | -V, --version Print version 51 | 52 | ``` 53 | 54 | ### Short Version: 55 | 56 | 1. Run p2p-clipboard on each machine you want to sync clipboard contents. 57 | 2. Ensure that machines are connected to the same network. 58 | 3. Copy text to the clipboard on one machine, and it should be synchronized with other machines automatically. 59 | 60 | ### Long Version: 61 | 62 | To synchronize the clipboard, you need at least two nodes connected to each other to form a peer-to-peer network. 63 | 64 | p2p-clipboard offers two network bootstrapping modes: 65 | 66 | 1. **Automatic Discovery:** By default, p2p-clipboard uses mDNS to automatically find peers within the same network. You just run it, and it should discover peers in the same network and start sharing clipboard. 67 | 68 | 2. **Manual Bootstrapping:** For more complicated networks where mDNS cannot be used, you can manually specify a "boot node" during startup. This node serves as the initial connection point, and it can be any node already in the p2p network. You specifiy it with `-c` or `--connect`, and p2p-clipboard will find all other peers through that peer. 69 | 70 | You can manually select which IP or port to use with `-l` or `--listen` option. If you only want to spcify IP and don't care about port number, you use `0` as the port number: `-l 127.0.0.1:0`. If you only want to spcify a port number and want to use all IPs, you use `0.0.0.0` as the IP address: `-l 0.0.0.0:12450` 71 | 72 | Each node needs to have a unique keypair as its identifier in the p2p network. It is an `ED25519` keypair and is used for encrypting traffic as well. The `PeerID` is the string representation of the public key of that keypair. By default, the keypair is derived from your machine ID. If you want to specify your own key, you can generate your own private key with the following command: 73 | 74 | ```shell 75 | openssl genpkey -algorithm Ed25519 -out private_key.pem 76 | ``` 77 | 78 | And then use `-k`or `--key` to use it. 79 | 80 | If not everyone in your local network is trusted by you, you can specify an extra pre-shared key to make your p2p network private. The pre-shared key can be any string and is specified with `-p` or `--psk`. Only nodes with the same pre-shared key can be peers with each other. This key won't be sent over network. 81 | 82 | If you plan to use p2p-clipboard in a public network, you may want to use `-n` or `--no-mdns` to disable peer discovery with mDNS and manually specify a boot node. 83 | 84 | ## Network Flexibility 85 | 86 | The p2p-clipboard Network is designed to be highly flexible, allowing nodes to operate in various network configurations. Unlike many other projects, it does not require all nodes to be in the same network subnet, nor does it strictly demand direct IP access to other nodes. 87 | 88 | In the p2p-clipboard Network, when a peer receives a new message (the clipboard content) from another peer, it stores the message and forwards a copy to all other peers it is connected. This makes it possible to ensure seamless communication between nodes, even when direct IP connections are not possible. 89 | 90 | For example, PeerB can connect to both PeerA and PeerC. However, PeerA and PeerC do not have a direct IP connection between them. In this case, PeerB will automatically act as a forwarder, ensuring that information can still be transmitted between PeerA and PeerC. 91 | 92 | ``` 93 | Peer A Peer B Peer C 94 | +---------+ +---------+ +---------+ 95 | | | | | | | 96 | | A |<--->| B |<--->| C | 97 | | | | | | | 98 | +---------+ +---------+ +---------+ 99 | ``` 100 | 101 | This will be useful when some peers in your network are behind some kind of NAT, which makes other peers unable to connect to them directly. In this case, a peer behind NAT will try to establish a connection from its side to all other peers in the network. However, we may also have some other peers which are also behind NAT, making it impossible to have direct connections between them. In this case, as long as there is a forwarding path available between these two peers, they can still have the clipboard synced. 102 | 103 | ## Limitation 104 | 105 | Currently has following limitation: 106 | 107 | - Only supports pure text contents. 108 | - The max payload size over network is hardcoded to 64KB after compression at the moment, which is ~150KB raw data. This should be sufficient for most use cases, but it may be increased in the future. 109 | - The default zero-configuration setup is suitable only when everyone in your local network is trusted by you. While all data is encrypted with TLS, the default setting allows anyone running p2pclipboard in your local network to read your clipboard, potentially exposing sensitive information. **Use a PSK if not everyone in your LAN is trusted.** 110 | - **For Linux users:** Not all Wayland compositors are supported, so if your desktop environmen uses an unsupported compositor, you will need to use X11 instead. The Wayland standard protocol does not allow windowless applications like p2p-clipboard to access the user clipboard. As of v0.2.0, p2p-clipboard relies on the [ext_data_control_v1](https://wayland.app/protocols/wayland-protocols/336) wayland extension to interact with the wayland clipboard. The support status of the extension is listed [here](https://wayland.app/protocols/wayland-protocols/336#compositor-support) 111 | 112 | ## License 113 | 114 | This project is licensed under the [MIT License](LICENSE). 115 | -------------------------------------------------------------------------------- /lib/mdns/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.48.0 2 | 3 | - Sync upstream `libp2p-mdns` `0.48.0`. 4 | - Keep p2p-clipboard fork behavior: `Config::disabled` and `Config::service_fingerprint` (dynamic service name, default `_p2pclipboard._udp.local`). 5 | 6 | ## 0.47.0 7 | 8 | - Emit `ToSwarm::NewExternalAddrOfPeer` on discovery. 9 | - Upgrade `hickory-proto`. 10 | 11 | ## 0.46.0 12 | 13 | - Fork from upstream, tweaked for p2p-clipboard 14 | - Removed std-async implementation 15 | - Added a config to disable all mdns operation 16 | - Added a config allowing to specify a fingerprint for the service name 17 | 18 | ## 0.45.1 19 | 20 | - Ensure `Multiaddr` handled and returned by `Behaviour` are `/p2p` terminated. 21 | See [PR 4596](https://github.com/libp2p/rust-libp2p/pull/4596). 22 | - Fix a bug in the `Behaviour::poll` method causing missed mdns packets. 23 | See [PR 4861](https://github.com/libp2p/rust-libp2p/pull/4861). 24 | 25 | ## 0.45.0 26 | 27 | - Don't perform IO in `Behaviour::poll`. 28 | See [PR 4623](https://github.com/libp2p/rust-libp2p/pull/4623). 29 | 30 | ## 0.44.0 31 | 32 | - Change `mdns::Event` to hold `Vec` and remove `DiscoveredAddrsIter` and `ExpiredAddrsIter`. 33 | See [PR 3621]. 34 | 35 | - Raise MSRV to 1.65. 36 | See [PR 3715]. 37 | - Remove deprecated `Mdns` prefixed items. See [PR 3699]. 38 | - Faster peer discovery with adaptive initial interval. See [PR 3975]. 39 | 40 | [PR 3975]: https://github.com/libp2p/rust-libp2p/pull/3975 41 | [PR 3621]: https://github.com/libp2p/rust-libp2p/pull/3621 42 | [PR 3715]: https://github.com/libp2p/rust-libp2p/pull/3715 43 | [PR 3699]: https://github.com/libp2p/rust-libp2p/pull/3699 44 | 45 | ## 0.43.1 46 | 47 | - Derive `Clone` for `mdns::Event`. See [PR 3606]. 48 | 49 | [PR 3606]: https://github.com/libp2p/rust-libp2p/pull/3606 50 | 51 | ## 0.43.0 52 | 53 | - Update to `libp2p-core` `v0.39.0`. 54 | 55 | - Require the node's local `PeerId` to be passed into the constructor of `libp2p_mdns::Behaviour`. See [PR 3153]. 56 | 57 | - Update to `libp2p-swarm` `v0.42.0`. 58 | 59 | - Don't expire mDNS records when the last connection was closed. 60 | mDNS records will only be expired when the TTL is reached and the DNS record is no longer valid. 61 | See [PR 3367]. 62 | 63 | [PR 3153]: https://github.com/libp2p/rust-libp2p/pull/3153 64 | [PR 3367]: https://github.com/libp2p/rust-libp2p/pull/3367 65 | 66 | ## 0.42.0 67 | 68 | - Update to `libp2p-core` `v0.38.0`. 69 | 70 | - Update to `libp2p-swarm` `v0.41.0`. 71 | 72 | - Update to `if-watch` `3.0.0` and both rename `TokioMdns` to `Behaviour` living in `tokio::Behaviour`, 73 | and move and rename `Mdns` to `async_io::Behaviour`. See [PR 3096]. 74 | 75 | - Remove the remaning `Mdns` prefixes from types as per [discussion 2174]. 76 | I.e the `Mdns` prefix has been removed from various types like `MdnsEvent`. 77 | Users should prefer importing the mdns protocol as a module (`use libp2p::mdns;`), 78 | and refer to its types via `mdns::`. For example: `mdns::Behaviour` or `mdns::Event`. 79 | 80 | - Replace `GenMdns`'s `NetworkBehaviour` implemention `inject_*` methods with the new `on_*` methods. 81 | See [PR 3011]. 82 | 83 | - Use `trust-dns-proto` to parse DNS messages. See [PR 3102]. 84 | 85 | - Update `rust-version` to reflect the actual MSRV: 1.62.0. See [PR 3090]. 86 | 87 | [discussion 2174]: https://github.com/libp2p/rust-libp2p/discussions/2174 88 | [PR 3096]: https://github.com/libp2p/rust-libp2p/pull/3096 89 | [PR 3011]: https://github.com/libp2p/rust-libp2p/pull/3011 90 | [PR 3102]: https://github.com/libp2p/rust-libp2p/pull/3102 91 | [PR 3090]: https://github.com/libp2p/rust-libp2p/pull/3090 92 | 93 | ## 0.41.0 94 | 95 | - Remove default features. If you previously depended on `async-io` you need to enable this explicitly now. See [PR 2918]. 96 | 97 | - Update to `libp2p-core` `v0.37.0`. 98 | 99 | - Update to `libp2p-swarm` `v0.40.0`. 100 | 101 | - Fix a bug that could cause a delay of ~10s until peers would get discovered when using the tokio runtime. See [PR 2939]. 102 | 103 | - Removed the `lazy_static` dependency. See [PR 2977]. 104 | 105 | - Update to `if-watch` `v2.0.0` and thus the `async` method `Mdns::new` and `TokioMdns::new` becomes synchronous. See [PR 2978]. 106 | 107 | [PR 2918]: https://github.com/libp2p/rust-libp2p/pull/2918 108 | [PR 2939]: https://github.com/libp2p/rust-libp2p/pull/2939 109 | [PR 2977]: https://github.com/libp2p/rust-libp2p/pull/2977 110 | [PR 2978]: https://github.com/libp2p/rust-libp2p/pull/2978 111 | 112 | ## 0.40.0 113 | 114 | - Update to `libp2p-swarm` `v0.39.0`. 115 | 116 | - Allow users to choose between async-io and tokio runtime 117 | in the mdns protocol implementation. `async-io` is a default 118 | feature, with an additional `tokio` feature (see [PR 2748]) 119 | 120 | - Fix high CPU usage with Tokio library (see [PR 2748]). 121 | 122 | - Update to `libp2p-core` `v0.36.0`. 123 | 124 | [PR 2748]: https://github.com/libp2p/rust-libp2p/pull/2748 125 | 126 | ## 0.39.0 127 | 128 | - Update to `libp2p-swarm` `v0.38.0`. 129 | - Update to `if-watch` `v1.1.1`. 130 | 131 | - Update to `libp2p-core` `v0.35.0`. 132 | 133 | ## 0.38.0 134 | 135 | - Update to `libp2p-core` `v0.34.0`. 136 | 137 | - Update to `libp2p-swarm` `v0.37.0`. 138 | 139 | ## 0.37.0 140 | 141 | - Update to `libp2p-core` `v0.33.0`. 142 | 143 | - Update to `libp2p-swarm` `v0.36.0`. 144 | 145 | ## 0.36.0 146 | 147 | - Update to `libp2p-swarm` `v0.35.0`. 148 | 149 | ## 0.35.0 [2022-02-22] 150 | 151 | - Update to `libp2p-core` `v0.32.0`. 152 | 153 | - Update to `libp2p-swarm` `v0.34.0`. 154 | 155 | - Merge NetworkBehaviour's inject_\* paired methods (see PR 2445). 156 | 157 | [PR 2445]: https://github.com/libp2p/rust-libp2p/pull/2445 158 | 159 | ## 0.34.0 [2022-01-27] 160 | 161 | - Update dependencies. 162 | 163 | - Use a random alphanumeric string instead of the local peer ID for mDNS peer 164 | name (see [PR 2311]). 165 | 166 | Note that previous versions of `libp2p-mdns` expect the peer name to be a 167 | valid peer ID. Thus they will be unable to discover nodes running this new 168 | version of `libp2p-mdns`. 169 | 170 | - Migrate to Rust edition 2021 (see [PR 2339]). 171 | 172 | - Fix generation of peer expiration event and listen on specified IP version (see [PR 2359]). 173 | 174 | - Support multiple interfaces (see [PR 2383]). 175 | 176 | [PR 2339]: https://github.com/libp2p/rust-libp2p/pull/2339 177 | 178 | [PR 2311]: https://github.com/libp2p/rust-libp2p/pull/2311/ 179 | 180 | [PR 2359]: https://github.com/libp2p/rust-libp2p/pull/2359 181 | 182 | [PR 2383]: https://github.com/libp2p/rust-libp2p/pull/2383 183 | 184 | ## 0.33.0 [2021-11-16] 185 | 186 | - Update dependencies. 187 | 188 | ## 0.32.0 [2021-11-01] 189 | 190 | - Make default features of `libp2p-core` optional. 191 | [PR 2181](https://github.com/libp2p/rust-libp2p/pull/2181) 192 | 193 | - Update dependencies. 194 | 195 | - Add support for IPv6. To enable set the multicast address 196 | in `MdnsConfig` to `IPV6_MDNS_MULTICAST_ADDRESS`. 197 | See [PR 2161] for details. 198 | 199 | - Prevent timers from firing at the same time. See [PR 2212] for details. 200 | 201 | [PR 2161]: https://github.com/libp2p/rust-libp2p/pull/2161/ 202 | [PR 2212]: https://github.com/libp2p/rust-libp2p/pull/2212/ 203 | 204 | ## 0.31.0 [2021-07-12] 205 | 206 | - Update dependencies. 207 | 208 | ## 0.30.2 [2021-05-06] 209 | 210 | - Fix discovered event emission. 211 | [PR 2065](https://github.com/libp2p/rust-libp2p/pull/2065) 212 | 213 | ## 0.30.1 [2021-04-21] 214 | 215 | - Fix timely discovery of peers after listening on a new address. 216 | [PR 2053](https://github.com/libp2p/rust-libp2p/pull/2053/) 217 | 218 | ## 0.30.0 [2021-04-13] 219 | 220 | - Derive `Debug` and `Clone` for `MdnsConfig`. 221 | 222 | - Update `libp2p-swarm`. 223 | 224 | ## 0.29.0 [2021-03-17] 225 | 226 | - Introduce `MdnsConfig` with configurable TTL of discovered peer 227 | records and configurable multicast query interval. The default 228 | query interval is increased from 20 seconds to 5 minutes, to 229 | significantly reduce bandwidth usage. To ensure timely peer 230 | discovery in the majority of cases, a multicast query is 231 | initiated whenever a change on a network interface is detected, 232 | which includes MDNS initialisation at node startup. If necessary 233 | the MDNS query interval can be reduced via the `MdnsConfig`. 234 | The `MdnsService` has been removed from the public API, making 235 | it compulsory that all uses occur through the `Mdns` `NetworkBehaviour`. 236 | An `MdnsConfig` must now be given to `Mdns::new()`. 237 | [PR 1977](https://github.com/libp2p/rust-libp2p/pull/1977). 238 | 239 | - Update `libp2p-swarm`. 240 | 241 | ## 0.28.1 [2021-02-15] 242 | 243 | - Update dependencies. 244 | 245 | ## 0.28.0 [2021-01-12] 246 | 247 | - Update dependencies. 248 | 249 | ## 0.27.0 [2020-12-17] 250 | 251 | - Update `libp2p-swarm` and `libp2p-core`. 252 | 253 | ## 0.26.0 [2020-12-08] 254 | 255 | - Create multiple multicast response packets as required to avoid 256 | hitting the limit of 9000 bytes per MDNS packet. 257 | [PR 1877](https://github.com/libp2p/rust-libp2p/pull/1877). 258 | 259 | - Detect interface changes and join the MDNS multicast 260 | group on all interfaces as they become available. 261 | [PR 1830](https://github.com/libp2p/rust-libp2p/pull/1830). 262 | 263 | - Replace the use of macros for abstracting over `tokio` 264 | and `async-std` with the use of `async-io`. As a result 265 | there may now be an additional reactor thread running 266 | called `async-io` when using `tokio`, with the futures 267 | still being polled by the `tokio` runtime. 268 | [PR 1830](https://github.com/libp2p/rust-libp2p/pull/1830). 269 | 270 | ## 0.25.0 [2020-11-25] 271 | 272 | - Update `libp2p-swarm` and `libp2p-core`. 273 | 274 | ## 0.24.0 [2020-11-09] 275 | 276 | - Update dependencies. 277 | 278 | ## 0.23.0 [2020-10-16] 279 | 280 | - Update `libp2p-swarm` and `libp2p-core`. 281 | 282 | - Double receive buffer to 4KiB. [PR 1779](https://github.com/libp2p/rust-libp2p/pull/1779/files). 283 | 284 | ## 0.22.0 [2020-09-09] 285 | 286 | - Update `libp2p-swarm` and `libp2p-core`. 287 | 288 | ## 0.21.0 [2020-08-18] 289 | 290 | - Bump `libp2p-core` and `libp2p-swarm` dependencies. 291 | 292 | - Allow libp2p-mdns to use either async-std or tokio to drive required UDP 293 | socket ([PR 1699](https://github.com/libp2p/rust-libp2p/pull/1699)). 294 | 295 | ## 0.20.0 [2020-07-01] 296 | 297 | - Updated dependencies. 298 | 299 | ## 0.19.2 [2020-06-22] 300 | 301 | - Updated dependencies. 302 | -------------------------------------------------------------------------------- /lib/mdns/src/behaviour/iface/query.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Parity Technologies (UK) Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | use std::{ 22 | fmt, 23 | net::SocketAddr, 24 | str, 25 | time::{Duration, Instant}, 26 | }; 27 | 28 | use hickory_proto::{ 29 | op::Message, 30 | rr::{Name, RData}, 31 | }; 32 | use libp2p_core::multiaddr::{Multiaddr, Protocol}; 33 | use libp2p_identity::PeerId; 34 | use libp2p_swarm::_address_translation; 35 | 36 | use super::dns; 37 | use crate::META_QUERY_SERVICE_FQDN; 38 | 39 | /// A valid mDNS packet received by the service. 40 | #[derive(Debug)] 41 | pub(crate) enum MdnsPacket { 42 | /// A query made by a remote. 43 | Query(MdnsQuery), 44 | /// A response sent by a remote in response to one of our queries. 45 | Response(MdnsResponse), 46 | /// A request for service discovery. 47 | ServiceDiscovery(MdnsServiceDiscovery), 48 | } 49 | 50 | impl MdnsPacket { 51 | pub(crate) fn new_from_bytes( 52 | buf: &[u8], 53 | from: SocketAddr, 54 | service_name_fqdn: &str, 55 | ) -> Result, hickory_proto::ProtoError> { 56 | let packet = Message::from_vec(buf)?; 57 | 58 | if packet.query().is_none() { 59 | return Ok(Some(MdnsPacket::Response(MdnsResponse::new( 60 | &packet, 61 | from, 62 | service_name_fqdn, 63 | )))); 64 | } 65 | 66 | if packet 67 | .queries() 68 | .iter() 69 | .any(|q| q.name().to_utf8() == service_name_fqdn) 70 | { 71 | return Ok(Some(MdnsPacket::Query(MdnsQuery { 72 | from, 73 | query_id: packet.header().id(), 74 | }))); 75 | } 76 | 77 | if packet 78 | .queries() 79 | .iter() 80 | .any(|q| q.name().to_utf8() == META_QUERY_SERVICE_FQDN) 81 | { 82 | // TODO: what if multiple questions, 83 | // one with SERVICE_NAME and one with META_QUERY_SERVICE? 84 | return Ok(Some(MdnsPacket::ServiceDiscovery(MdnsServiceDiscovery { 85 | from, 86 | query_id: packet.header().id(), 87 | }))); 88 | } 89 | 90 | Ok(None) 91 | } 92 | } 93 | 94 | /// A received mDNS query. 95 | pub(crate) struct MdnsQuery { 96 | /// Sender of the address. 97 | from: SocketAddr, 98 | /// Id of the received DNS query. We need to pass this ID back in the results. 99 | query_id: u16, 100 | } 101 | 102 | impl MdnsQuery { 103 | /// Source address of the packet. 104 | pub(crate) fn remote_addr(&self) -> &SocketAddr { 105 | &self.from 106 | } 107 | 108 | /// Query id of the packet. 109 | pub(crate) fn query_id(&self) -> u16 { 110 | self.query_id 111 | } 112 | } 113 | 114 | impl fmt::Debug for MdnsQuery { 115 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 116 | f.debug_struct("MdnsQuery") 117 | .field("from", self.remote_addr()) 118 | .field("query_id", &self.query_id) 119 | .finish() 120 | } 121 | } 122 | 123 | /// A received mDNS service discovery query. 124 | pub(crate) struct MdnsServiceDiscovery { 125 | /// Sender of the address. 126 | from: SocketAddr, 127 | /// Id of the received DNS query. We need to pass this ID back in the results. 128 | query_id: u16, 129 | } 130 | 131 | impl MdnsServiceDiscovery { 132 | /// Source address of the packet. 133 | pub(crate) fn remote_addr(&self) -> &SocketAddr { 134 | &self.from 135 | } 136 | 137 | /// Query id of the packet. 138 | pub(crate) fn query_id(&self) -> u16 { 139 | self.query_id 140 | } 141 | } 142 | 143 | impl fmt::Debug for MdnsServiceDiscovery { 144 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 145 | f.debug_struct("MdnsServiceDiscovery") 146 | .field("from", self.remote_addr()) 147 | .field("query_id", &self.query_id) 148 | .finish() 149 | } 150 | } 151 | 152 | /// A received mDNS response. 153 | pub(crate) struct MdnsResponse { 154 | peers: Vec, 155 | from: SocketAddr, 156 | } 157 | 158 | impl MdnsResponse { 159 | /// Creates a new `MdnsResponse` based on the provided `Packet`. 160 | pub(crate) fn new(packet: &Message, from: SocketAddr, service_name_fqdn: &str) -> MdnsResponse { 161 | let peers = packet 162 | .answers() 163 | .iter() 164 | .filter_map(|record| { 165 | if record.name().to_string() != service_name_fqdn { 166 | return None; 167 | } 168 | 169 | let RData::PTR(record_value) = record.data() else { 170 | return None; 171 | }; 172 | 173 | MdnsPeer::new(packet, record_value, record.ttl()) 174 | }) 175 | .collect(); 176 | 177 | MdnsResponse { peers, from } 178 | } 179 | 180 | pub(crate) fn extract_discovered( 181 | &self, 182 | now: Instant, 183 | local_peer_id: PeerId, 184 | ) -> impl Iterator + '_ { 185 | self.discovered_peers() 186 | .filter(move |peer| peer.id() != &local_peer_id) 187 | .flat_map(move |peer| { 188 | let observed = self.observed_address(); 189 | let new_expiration = now + peer.ttl(); 190 | 191 | peer.addresses().iter().filter_map(move |address| { 192 | let new_addr = _address_translation(address, &observed)?; 193 | let new_addr = new_addr.with_p2p(*peer.id()).ok()?; 194 | 195 | Some((*peer.id(), new_addr, new_expiration)) 196 | }) 197 | }) 198 | } 199 | 200 | /// Source address of the packet. 201 | pub(crate) fn remote_addr(&self) -> &SocketAddr { 202 | &self.from 203 | } 204 | 205 | fn observed_address(&self) -> Multiaddr { 206 | // We replace the IP address with the address we observe the 207 | // remote as and the address they listen on. 208 | let obs_ip = Protocol::from(self.remote_addr().ip()); 209 | let obs_port = Protocol::Udp(self.remote_addr().port()); 210 | 211 | Multiaddr::empty().with(obs_ip).with(obs_port) 212 | } 213 | 214 | /// Returns the list of peers that have been reported in this packet. 215 | /// 216 | /// > **Note**: Keep in mind that this will also contain the responses we sent ourselves. 217 | fn discovered_peers(&self) -> impl Iterator { 218 | self.peers.iter() 219 | } 220 | } 221 | 222 | impl fmt::Debug for MdnsResponse { 223 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 224 | f.debug_struct("MdnsResponse") 225 | .field("from", self.remote_addr()) 226 | .finish() 227 | } 228 | } 229 | 230 | /// A peer discovered by the service. 231 | pub(crate) struct MdnsPeer { 232 | addrs: Vec, 233 | /// Id of the peer. 234 | peer_id: PeerId, 235 | /// TTL of the record in seconds. 236 | ttl: u32, 237 | } 238 | 239 | impl MdnsPeer { 240 | /// Creates a new `MdnsPeer` based on the provided `Packet`. 241 | pub(crate) fn new(packet: &Message, record_value: &Name, ttl: u32) -> Option { 242 | let mut my_peer_id: Option = None; 243 | let addrs = packet 244 | .additionals() 245 | .iter() 246 | .filter_map(|add_record| { 247 | if add_record.name() != record_value { 248 | return None; 249 | } 250 | 251 | if let RData::TXT(ref txt) = add_record.data() { 252 | Some(txt) 253 | } else { 254 | None 255 | } 256 | }) 257 | .flat_map(|txt| txt.iter()) 258 | .filter_map(|txt| { 259 | // TODO: wrong, txt can be multiple character strings 260 | let addr = dns::decode_character_string(txt).ok()?; 261 | 262 | if !addr.starts_with(b"dnsaddr=") { 263 | return None; 264 | } 265 | 266 | let mut addr = str::from_utf8(&addr[8..]).ok()?.parse::().ok()?; 267 | 268 | match addr.pop() { 269 | Some(Protocol::P2p(peer_id)) => { 270 | if let Some(pid) = &my_peer_id { 271 | if peer_id != *pid { 272 | return None; 273 | } 274 | } else { 275 | my_peer_id.replace(peer_id); 276 | } 277 | } 278 | _ => return None, 279 | }; 280 | Some(addr) 281 | }) 282 | .collect(); 283 | 284 | my_peer_id.map(|peer_id| MdnsPeer { 285 | addrs, 286 | peer_id, 287 | ttl, 288 | }) 289 | } 290 | 291 | /// Returns the id of the peer. 292 | #[inline] 293 | pub(crate) fn id(&self) -> &PeerId { 294 | &self.peer_id 295 | } 296 | 297 | /// Returns the requested time-to-live for the record. 298 | #[inline] 299 | pub(crate) fn ttl(&self) -> Duration { 300 | Duration::from_secs(u64::from(self.ttl)) 301 | } 302 | 303 | /// Returns the list of addresses the peer says it is listening on. 304 | /// 305 | /// Filters out invalid addresses. 306 | pub(crate) fn addresses(&self) -> &Vec { 307 | &self.addrs 308 | } 309 | } 310 | 311 | impl fmt::Debug for MdnsPeer { 312 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 313 | f.debug_struct("MdnsPeer") 314 | .field("peer_id", &self.peer_id) 315 | .finish() 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /lib/tls/src/verifier.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Parity Technologies (UK) Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! TLS 1.3 certificates and handshakes handling for libp2p 22 | //! 23 | //! This module handles a verification of a client/server certificate chain 24 | //! and signatures allegedly by the given certificates. 25 | 26 | use crate::certificate; 27 | use libp2p_identity::PeerId; 28 | use rustls::{ 29 | crypto::ring::cipher_suite::{ 30 | TLS13_AES_128_GCM_SHA256, TLS13_AES_256_GCM_SHA384, TLS13_CHACHA20_POLY1305_SHA256, 31 | }, 32 | client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, 33 | server::danger::{ClientCertVerified, ClientCertVerifier}, 34 | CertificateError, OtherError, DigitallySignedStruct, DistinguishedName, SignatureScheme, 35 | SupportedCipherSuite, SupportedProtocolVersion, 36 | pki_types::CertificateDer, 37 | }; 38 | use std::sync::Arc; 39 | 40 | /// The protocol versions supported by this verifier. 41 | /// 42 | /// The spec says: 43 | /// 44 | /// > The libp2p handshake uses TLS 1.3 (and higher). 45 | /// > Endpoints MUST NOT negotiate lower TLS versions. 46 | pub(crate) static PROTOCOL_VERSIONS: &[&SupportedProtocolVersion] = &[&rustls::version::TLS13]; 47 | /// A list of the TLS 1.3 cipher suites supported by rustls. 48 | // By default, rustls creates client/server configs with both 49 | // TLS 1.3 __and__ 1.2 cipher suites. But we don't need 1.2. 50 | pub(crate) static CIPHERSUITES: &[SupportedCipherSuite] = &[ 51 | // TLS1.3 suites 52 | TLS13_AES_256_GCM_SHA384, 53 | TLS13_AES_128_GCM_SHA256, 54 | TLS13_CHACHA20_POLY1305_SHA256, 55 | ]; 56 | 57 | // Some CPU does not support hardware AES, so we want to use ChaCha20 exclusively. 58 | pub(crate) static CIPHERSUITES_WITHOUT_AES: &[SupportedCipherSuite] = &[ 59 | // TLS1.3 suites without aes 60 | TLS13_CHACHA20_POLY1305_SHA256, 61 | ]; 62 | 63 | /// Implementation of the `rustls` certificate verification traits for libp2p. 64 | /// 65 | /// Only TLS 1.3 is supported. TLS 1.2 should be disabled in the configuration of `rustls`. 66 | #[derive(Debug)] 67 | pub(crate) struct Libp2pCertificateVerifier { 68 | /// The peer ID we intend to connect to 69 | remote_peer_id: Option, 70 | /// The pre-shared key specified by user, if any 71 | pre_shared_key: Option, 72 | } 73 | 74 | /// libp2p requires the following of X.509 server certificate chains: 75 | /// 76 | /// - Exactly one certificate must be presented. 77 | /// - The certificate must be self-signed. 78 | /// - The certificate must have a valid libp2p extension that includes a 79 | /// signature of its public key. 80 | impl Libp2pCertificateVerifier { 81 | pub(crate) fn new(pre_shared_key: Option) -> Self { 82 | Self { 83 | remote_peer_id: None, 84 | pre_shared_key, 85 | } 86 | } 87 | pub(crate) fn with_remote_peer_id(remote_peer_id: Option, pre_shared_key: Option) -> Self { 88 | Self { remote_peer_id, pre_shared_key } 89 | } 90 | 91 | /// Return the list of SignatureSchemes that this verifier will handle, 92 | /// in `verify_tls12_signature` and `verify_tls13_signature` calls. 93 | /// 94 | /// This should be in priority order, with the most preferred first. 95 | fn verification_schemes() -> Vec { 96 | vec![ 97 | // TODO SignatureScheme::ECDSA_NISTP521_SHA512 is not supported by `ring` yet 98 | SignatureScheme::ECDSA_NISTP384_SHA384, 99 | SignatureScheme::ECDSA_NISTP256_SHA256, 100 | // TODO SignatureScheme::ED448 is not supported by `ring` yet 101 | SignatureScheme::ED25519, 102 | // In particular, RSA SHOULD NOT be used unless 103 | // no elliptic curve algorithms are supported. 104 | SignatureScheme::RSA_PSS_SHA512, 105 | SignatureScheme::RSA_PSS_SHA384, 106 | SignatureScheme::RSA_PSS_SHA256, 107 | SignatureScheme::RSA_PKCS1_SHA512, 108 | SignatureScheme::RSA_PKCS1_SHA384, 109 | SignatureScheme::RSA_PKCS1_SHA256, 110 | ] 111 | } 112 | } 113 | 114 | impl ServerCertVerifier for Libp2pCertificateVerifier { 115 | fn verify_server_cert( 116 | &self, 117 | end_entity: &CertificateDer, 118 | intermediates: &[CertificateDer], 119 | _server_name: &rustls::pki_types::ServerName, 120 | _ocsp_response: &[u8], 121 | _now: rustls::pki_types::UnixTime, 122 | ) -> Result { 123 | let peer_id = verify_presented_certs(end_entity, intermediates, self.pre_shared_key.clone())?; 124 | 125 | if let Some(remote_peer_id) = self.remote_peer_id { 126 | // The public host key allows the peer to calculate the peer ID of the peer 127 | // it is connecting to. Clients MUST verify that the peer ID derived from 128 | // the certificate matches the peer ID they intended to connect to, 129 | // and MUST abort the connection if there is a mismatch. 130 | if remote_peer_id != peer_id { 131 | return Err(rustls::Error::InvalidCertificate( 132 | CertificateError::ApplicationVerificationFailure, 133 | )); 134 | } 135 | } 136 | 137 | Ok(ServerCertVerified::assertion()) 138 | } 139 | 140 | fn verify_tls12_signature( 141 | &self, 142 | _message: &[u8], 143 | _cert: &CertificateDer, 144 | _dss: &DigitallySignedStruct, 145 | ) -> Result { 146 | unreachable!("`PROTOCOL_VERSIONS` only allows TLS 1.3") 147 | } 148 | 149 | fn verify_tls13_signature( 150 | &self, 151 | message: &[u8], 152 | cert: &CertificateDer, 153 | dss: &DigitallySignedStruct, 154 | ) -> Result { 155 | verify_tls13_signature(cert, dss.scheme, message, dss.signature(), self.pre_shared_key.clone()) 156 | } 157 | 158 | fn supported_verify_schemes(&self) -> Vec { 159 | Self::verification_schemes() 160 | } 161 | } 162 | 163 | /// libp2p requires the following of X.509 client certificate chains: 164 | /// 165 | /// - Exactly one certificate must be presented. In particular, client 166 | /// authentication is mandatory in libp2p. 167 | /// - The certificate must be self-signed. 168 | /// - The certificate must have a valid libp2p extension that includes a 169 | /// signature of its public key. 170 | impl ClientCertVerifier for Libp2pCertificateVerifier { 171 | fn offer_client_auth(&self) -> bool { 172 | true 173 | } 174 | 175 | fn root_hint_subjects(&self) -> &[DistinguishedName] { 176 | &[] 177 | } 178 | 179 | fn verify_client_cert( 180 | &self, 181 | end_entity: &CertificateDer, 182 | intermediates: &[CertificateDer], 183 | _now: rustls::pki_types::UnixTime, 184 | ) -> Result { 185 | verify_presented_certs(end_entity, intermediates, self.pre_shared_key.clone())?; 186 | 187 | Ok(ClientCertVerified::assertion()) 188 | } 189 | 190 | fn verify_tls12_signature( 191 | &self, 192 | _message: &[u8], 193 | _cert: &CertificateDer, 194 | _dss: &DigitallySignedStruct, 195 | ) -> Result { 196 | unreachable!("`PROTOCOL_VERSIONS` only allows TLS 1.3") 197 | } 198 | 199 | fn verify_tls13_signature( 200 | &self, 201 | message: &[u8], 202 | cert: &CertificateDer, 203 | dss: &DigitallySignedStruct, 204 | ) -> Result { 205 | verify_tls13_signature(cert, dss.scheme, message, dss.signature(), self.pre_shared_key.clone()) 206 | } 207 | 208 | fn supported_verify_schemes(&self) -> Vec { 209 | Self::verification_schemes() 210 | } 211 | } 212 | 213 | /// When receiving the certificate chain, an endpoint 214 | /// MUST check these conditions and abort the connection attempt if 215 | /// (a) the presented certificate is not yet valid, OR 216 | /// (b) if it is expired. 217 | /// Endpoints MUST abort the connection attempt if more than one certificate is received, 218 | /// or if the certificate’s self-signature is not valid. 219 | fn verify_presented_certs( 220 | end_entity: &CertificateDer, 221 | intermediates: &[CertificateDer], 222 | pre_shared_key: Option, 223 | ) -> Result { 224 | if !intermediates.is_empty() { 225 | return Err(rustls::Error::General( 226 | "libp2p-tls requires exactly one certificate".into(), 227 | )); 228 | } 229 | 230 | let cert = certificate::parse(end_entity, pre_shared_key)?; 231 | 232 | Ok(cert.peer_id()) 233 | } 234 | 235 | fn verify_tls13_signature( 236 | cert: &CertificateDer, 237 | signature_scheme: SignatureScheme, 238 | message: &[u8], 239 | signature: &[u8], 240 | pre_shared_key: Option, 241 | ) -> Result { 242 | certificate::parse(cert, pre_shared_key)?.verify_signature(signature_scheme, message, signature)?; 243 | 244 | Ok(HandshakeSignatureValid::assertion()) 245 | } 246 | 247 | impl From for rustls::Error { 248 | fn from(certificate::ParseError(e): certificate::ParseError) -> Self { 249 | use webpki::Error::*; 250 | match e { 251 | BadDer => rustls::Error::InvalidCertificate(CertificateError::BadEncoding), 252 | e => { 253 | rustls::Error::InvalidCertificate(CertificateError::Other(OtherError(Arc::new(e)))) 254 | } 255 | } 256 | } 257 | } 258 | impl From for rustls::Error { 259 | fn from(certificate::VerificationError(e): certificate::VerificationError) -> Self { 260 | use webpki::Error::*; 261 | match e { 262 | InvalidSignatureForPublicKey => { 263 | rustls::Error::InvalidCertificate(CertificateError::BadSignature) 264 | } 265 | other => rustls::Error::InvalidCertificate(CertificateError::Other(OtherError( 266 | Arc::new(other), 267 | ))), 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /lib/mdns/src/behaviour/iface/dns.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Parity Technologies (UK) Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! (M)DNS encoding and decoding on top of the `dns_parser` library. 22 | 23 | use std::{borrow::Cow, cmp, error, fmt, str, time::Duration}; 24 | 25 | use libp2p_core::Multiaddr; 26 | use libp2p_identity::PeerId; 27 | use rand::{distributions::Alphanumeric, thread_rng, Rng}; 28 | 29 | use crate::META_QUERY_SERVICE; 30 | 31 | /// DNS TXT records can have up to 255 characters as a single string value. 32 | /// 33 | /// Current values are usually around 170-190 bytes long, varying primarily 34 | /// with the length of the contained `Multiaddr`. 35 | const MAX_TXT_VALUE_LENGTH: usize = 255; 36 | 37 | /// A conservative maximum size (in bytes) of a complete TXT record, 38 | /// as encoded by [`append_txt_record`]. 39 | const MAX_TXT_RECORD_SIZE: usize = MAX_TXT_VALUE_LENGTH + 45; 40 | 41 | /// The maximum DNS packet size is 9000 bytes less the maximum 42 | /// sizes of the IP (60) and UDP (8) headers. 43 | const MAX_PACKET_SIZE: usize = 9000 - 68; 44 | 45 | /// A conservative maximum number of records that can be packed into 46 | /// a single DNS UDP packet, allowing up to 100 bytes of MDNS packet 47 | /// header data to be added by [`query_response_packet()`]. 48 | const MAX_RECORDS_PER_PACKET: usize = (MAX_PACKET_SIZE - 100) / MAX_TXT_RECORD_SIZE; 49 | 50 | /// An encoded MDNS packet. 51 | pub(crate) type MdnsPacket = Vec; 52 | /// Decodes a `` (as defined by RFC1035) into a `Vec` of ASCII characters. 53 | // TODO: better error type? 54 | pub(crate) fn decode_character_string(mut from: &[u8]) -> Result, ()> { 55 | if from.is_empty() { 56 | return Ok(Cow::Owned(Vec::new())); 57 | } 58 | 59 | // Remove the initial and trailing " if any. 60 | if from[0] == b'"' { 61 | if from.len() == 1 || from.last() != Some(&b'"') { 62 | return Err(()); 63 | } 64 | let len = from.len(); 65 | from = &from[1..len - 1]; 66 | } 67 | 68 | // TODO: remove the backslashes if any 69 | Ok(Cow::Borrowed(from)) 70 | } 71 | 72 | /// Builds the binary representation of a DNS query to send on the network. 73 | pub(crate) fn build_query(service_name: &[u8]) -> MdnsPacket { 74 | let mut out = Vec::with_capacity(33 + service_name.len()); 75 | 76 | // Program-generated transaction ID; unused by our implementation. 77 | append_u16(&mut out, rand::random()); 78 | 79 | // 0x0 flag for a regular query. 80 | append_u16(&mut out, 0x0); 81 | 82 | // Number of questions. 83 | append_u16(&mut out, 0x1); 84 | 85 | // Number of answers, authorities, and additionals. 86 | append_u16(&mut out, 0x0); 87 | append_u16(&mut out, 0x0); 88 | append_u16(&mut out, 0x0); 89 | 90 | // Our single question. 91 | // The name. 92 | append_qname(&mut out, service_name); 93 | 94 | // Flags. 95 | append_u16(&mut out, 0x0c); 96 | append_u16(&mut out, 0x01); 97 | 98 | out 99 | } 100 | 101 | /// Builds the response to an address discovery DNS query. 102 | /// 103 | /// If there are more than 2^16-1 addresses, ignores the rest. 104 | pub(crate) fn build_query_response<'a>( 105 | id: u16, 106 | peer_id: PeerId, 107 | addresses: impl ExactSizeIterator, 108 | ttl: Duration, 109 | service_name: &[u8], 110 | ) -> Vec { 111 | // Convert the TTL into seconds. 112 | let ttl = duration_to_secs(ttl); 113 | 114 | // Add a limit to 2^16-1 addresses, as the protocol limits to this number. 115 | let addresses = addresses.take(65535); 116 | 117 | let peer_name_bytes = generate_peer_name(); 118 | debug_assert!(peer_name_bytes.len() <= 0xffff); 119 | 120 | // The accumulated response packets. 121 | let mut packets = Vec::new(); 122 | 123 | // The records accumulated per response packet. 124 | let mut records = Vec::with_capacity(addresses.len() * MAX_TXT_RECORD_SIZE); 125 | 126 | // Encode the addresses as TXT records, and multiple TXT records into a 127 | // response packet. 128 | for addr in addresses { 129 | let txt_to_send = format!("dnsaddr={}/p2p/{}", addr, peer_id.to_base58()); 130 | let mut txt_record = Vec::with_capacity(txt_to_send.len()); 131 | match append_txt_record(&mut txt_record, &peer_name_bytes, ttl, &txt_to_send) { 132 | Ok(()) => { 133 | records.push(txt_record); 134 | } 135 | Err(e) => { 136 | tracing::warn!(address=%addr, "Excluding address from response: {:?}", e); 137 | } 138 | } 139 | 140 | if records.len() == MAX_RECORDS_PER_PACKET { 141 | packets.push(query_response_packet( 142 | id, 143 | &peer_name_bytes, 144 | &records, 145 | ttl, 146 | service_name, 147 | )); 148 | records.clear(); 149 | } 150 | } 151 | 152 | // If there are still unpacked records, i.e. if the number of records is not 153 | // a multiple of `MAX_RECORDS_PER_PACKET`, create a final packet. 154 | if !records.is_empty() { 155 | packets.push(query_response_packet( 156 | id, 157 | &peer_name_bytes, 158 | &records, 159 | ttl, 160 | service_name, 161 | )); 162 | } 163 | 164 | // If no packets have been built at all, because `addresses` is empty, 165 | // construct an empty response packet. 166 | if packets.is_empty() { 167 | packets.push(query_response_packet( 168 | id, 169 | &peer_name_bytes, 170 | &Vec::new(), 171 | ttl, 172 | service_name, 173 | )); 174 | } 175 | 176 | packets 177 | } 178 | 179 | /// Builds the response to a service discovery DNS query. 180 | pub(crate) fn build_service_discovery_response( 181 | id: u16, 182 | ttl: Duration, 183 | service_name: &[u8], 184 | ) -> MdnsPacket { 185 | // Convert the TTL into seconds. 186 | let ttl = duration_to_secs(ttl); 187 | 188 | // This capacity was determined empirically. 189 | let mut out = Vec::with_capacity(69); 190 | 191 | append_u16(&mut out, id); 192 | // 0x84 flag for an answer. 193 | append_u16(&mut out, 0x8400); 194 | // Number of questions, answers, authorities, additionals. 195 | append_u16(&mut out, 0x0); 196 | append_u16(&mut out, 0x1); 197 | append_u16(&mut out, 0x0); 198 | append_u16(&mut out, 0x0); 199 | 200 | // Our single answer. 201 | // The name. 202 | append_qname(&mut out, META_QUERY_SERVICE); 203 | 204 | // Flags. 205 | append_u16(&mut out, 0x000c); 206 | append_u16(&mut out, 0x8001); 207 | 208 | // TTL for the answer 209 | append_u32(&mut out, ttl); 210 | 211 | // Service name. 212 | { 213 | let mut name = Vec::with_capacity(service_name.len() + 2); 214 | append_qname(&mut name, service_name); 215 | append_u16(&mut out, name.len() as u16); 216 | out.extend_from_slice(&name); 217 | } 218 | 219 | out 220 | } 221 | 222 | /// Constructs an MDNS query response packet for an address lookup. 223 | fn query_response_packet( 224 | id: u16, 225 | peer_id: &[u8], 226 | records: &[Vec], 227 | ttl: u32, 228 | service_name: &[u8], 229 | ) -> MdnsPacket { 230 | let mut out = Vec::with_capacity(records.len() * MAX_TXT_RECORD_SIZE); 231 | 232 | append_u16(&mut out, id); 233 | // 0x84 flag for an answer. 234 | append_u16(&mut out, 0x8400); 235 | // Number of questions, answers, authorities, additionals. 236 | append_u16(&mut out, 0x0); 237 | append_u16(&mut out, 0x1); 238 | append_u16(&mut out, 0x0); 239 | append_u16(&mut out, records.len() as u16); 240 | 241 | // Our single answer. 242 | // The name. 243 | append_qname(&mut out, service_name); 244 | 245 | // Flags. 246 | append_u16(&mut out, 0x000c); 247 | append_u16(&mut out, 0x0001); 248 | 249 | // TTL for the answer 250 | append_u32(&mut out, ttl); 251 | 252 | // Peer Id. 253 | append_u16(&mut out, peer_id.len() as u16); 254 | out.extend_from_slice(peer_id); 255 | 256 | // The TXT records. 257 | for record in records { 258 | out.extend_from_slice(record); 259 | } 260 | 261 | out 262 | } 263 | 264 | /// Returns the number of secs of a duration. 265 | fn duration_to_secs(duration: Duration) -> u32 { 266 | let secs = duration 267 | .as_secs() 268 | .saturating_add(u64::from(duration.subsec_nanos() > 0)); 269 | cmp::min(secs, From::from(u32::MAX)) as u32 270 | } 271 | 272 | /// Appends a big-endian u32 to `out`. 273 | fn append_u32(out: &mut Vec, value: u32) { 274 | out.push(((value >> 24) & 0xff) as u8); 275 | out.push(((value >> 16) & 0xff) as u8); 276 | out.push(((value >> 8) & 0xff) as u8); 277 | out.push((value & 0xff) as u8); 278 | } 279 | 280 | /// Appends a big-endian u16 to `out`. 281 | fn append_u16(out: &mut Vec, value: u16) { 282 | out.push(((value >> 8) & 0xff) as u8); 283 | out.push((value & 0xff) as u8); 284 | } 285 | 286 | /// Generates and returns a random alphanumeric string of `length` size. 287 | fn random_string(length: usize) -> String { 288 | thread_rng() 289 | .sample_iter(&Alphanumeric) 290 | .take(length) 291 | .map(char::from) 292 | .collect() 293 | } 294 | 295 | /// Generates a random peer name as bytes for a DNS query. 296 | fn generate_peer_name() -> Vec { 297 | // Use a variable-length random string for mDNS peer name. 298 | // See https://github.com/libp2p/rust-libp2p/pull/2311/ 299 | let peer_name = random_string(32 + thread_rng().gen_range(0..32)); 300 | 301 | // allocate with a little extra padding for QNAME encoding 302 | let mut peer_name_bytes = Vec::with_capacity(peer_name.len() + 32); 303 | append_qname(&mut peer_name_bytes, peer_name.as_bytes()); 304 | 305 | peer_name_bytes 306 | } 307 | 308 | /// Appends a `QNAME` (as defined by RFC1035) to the `Vec`. 309 | /// 310 | /// # Panic 311 | /// 312 | /// Panics if `name` has a zero-length component or a component that is too long. 313 | /// This is fine considering that this function is not public and is only called in a controlled 314 | /// environment. 315 | fn append_qname(out: &mut Vec, name: &[u8]) { 316 | debug_assert!(name.is_ascii()); 317 | 318 | for element in name.split(|&c| c == b'.') { 319 | assert!(element.len() < 64, "Service name has a label too long"); 320 | assert_ne!(element.len(), 0, "Service name contains zero length label"); 321 | out.push(element.len() as u8); 322 | for chr in element.iter() { 323 | out.push(*chr); 324 | } 325 | } 326 | 327 | out.push(0); 328 | } 329 | 330 | /// Appends a `` (as defined by RFC1035) to the `Vec`. 331 | fn append_character_string(out: &mut Vec, ascii_str: &str) -> Result<(), MdnsResponseError> { 332 | if !ascii_str.is_ascii() { 333 | return Err(MdnsResponseError::NonAsciiMultiaddr); 334 | } 335 | 336 | if !ascii_str.bytes().any(|c| c == b' ') { 337 | out.extend_from_slice(ascii_str.as_bytes()); 338 | return Ok(()); 339 | } 340 | 341 | out.push(b'"'); 342 | 343 | for &chr in ascii_str.as_bytes() { 344 | if chr == b'\\' { 345 | out.push(b'\\'); 346 | out.push(b'\\'); 347 | } else if chr == b'"' { 348 | out.push(b'\\'); 349 | out.push(b'"'); 350 | } else { 351 | out.push(chr); 352 | } 353 | } 354 | 355 | out.push(b'"'); 356 | Ok(()) 357 | } 358 | 359 | /// Appends a TXT record to `out`. 360 | fn append_txt_record( 361 | out: &mut Vec, 362 | name: &[u8], 363 | ttl_secs: u32, 364 | value: &str, 365 | ) -> Result<(), MdnsResponseError> { 366 | // The name. 367 | out.extend_from_slice(name); 368 | 369 | // Flags. 370 | out.push(0x00); 371 | out.push(0x10); // TXT record. 372 | out.push(0x80); 373 | out.push(0x01); 374 | 375 | // TTL for the answer 376 | append_u32(out, ttl_secs); 377 | 378 | // Add the strings. 379 | if value.len() > MAX_TXT_VALUE_LENGTH { 380 | return Err(MdnsResponseError::TxtRecordTooLong); 381 | } 382 | let mut buffer = vec![value.len() as u8]; 383 | append_character_string(&mut buffer, value)?; 384 | 385 | append_u16(out, buffer.len() as u16); 386 | out.extend_from_slice(&buffer); 387 | Ok(()) 388 | } 389 | 390 | /// Errors that can occur on encoding an MDNS response. 391 | #[derive(Debug)] 392 | enum MdnsResponseError { 393 | TxtRecordTooLong, 394 | NonAsciiMultiaddr, 395 | } 396 | 397 | impl fmt::Display for MdnsResponseError { 398 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 399 | match self { 400 | MdnsResponseError::TxtRecordTooLong => { 401 | write!(f, "TXT record invalid because it is too long") 402 | } 403 | MdnsResponseError::NonAsciiMultiaddr => write!( 404 | f, 405 | "A multiaddr contains non-ASCII characters when serialized" 406 | ), 407 | } 408 | } 409 | } 410 | 411 | impl error::Error for MdnsResponseError {} 412 | -------------------------------------------------------------------------------- /lib/mdns/src/behaviour.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Parity Technologies (UK) Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | mod iface; 22 | mod socket; 23 | mod timer; 24 | 25 | use std::{ 26 | cmp, 27 | collections::{ 28 | hash_map::{Entry, HashMap}, 29 | VecDeque, 30 | }, 31 | convert::Infallible, 32 | fmt, 33 | future::Future, 34 | io, 35 | net::IpAddr, 36 | pin::Pin, 37 | sync::{Arc, RwLock}, 38 | task::{Context, Poll}, 39 | time::Instant, 40 | }; 41 | 42 | use futures::{channel::mpsc, Stream, StreamExt}; 43 | use if_watch::IfEvent; 44 | use libp2p_core::{transport::PortUse, Endpoint, Multiaddr}; 45 | use libp2p_identity::PeerId; 46 | use libp2p_swarm::{ 47 | behaviour::FromSwarm, dummy, ConnectionDenied, ConnectionId, ListenAddresses, NetworkBehaviour, 48 | THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, 49 | }; 50 | use smallvec::SmallVec; 51 | 52 | use self::iface::InterfaceState; 53 | use crate::{ 54 | behaviour::{socket::AsyncSocket, timer::Builder}, 55 | Config, 56 | }; 57 | 58 | /// An abstraction to allow for compatibility with various async runtimes. 59 | pub trait Provider: 'static { 60 | /// The Async Socket type. 61 | type Socket: AsyncSocket; 62 | /// The Async Timer type. 63 | type Timer: Builder + Stream; 64 | /// The IfWatcher type. 65 | type Watcher: Stream> + fmt::Debug + Unpin; 66 | 67 | type TaskHandle: Abort; 68 | 69 | /// Create a new instance of the `IfWatcher` type. 70 | fn new_watcher() -> Result; 71 | 72 | #[track_caller] 73 | fn spawn(task: impl Future + Send + 'static) -> Self::TaskHandle; 74 | } 75 | 76 | #[allow(unreachable_pub)] // Not re-exported. 77 | pub trait Abort { 78 | fn abort(self); 79 | } 80 | 81 | /// The type of a [`Behaviour`] using the `tokio` implementation. 82 | #[cfg(feature = "tokio")] 83 | pub mod tokio { 84 | use std::future::Future; 85 | 86 | use if_watch::tokio::IfWatcher; 87 | use tokio::task::JoinHandle; 88 | 89 | use super::Provider; 90 | use crate::behaviour::{socket::tokio::TokioUdpSocket, timer::tokio::TokioTimer, Abort}; 91 | 92 | #[doc(hidden)] 93 | pub enum Tokio {} 94 | 95 | impl Provider for Tokio { 96 | type Socket = TokioUdpSocket; 97 | type Timer = TokioTimer; 98 | type Watcher = IfWatcher; 99 | type TaskHandle = JoinHandle<()>; 100 | 101 | fn new_watcher() -> Result { 102 | IfWatcher::new() 103 | } 104 | 105 | fn spawn(task: impl Future + Send + 'static) -> Self::TaskHandle { 106 | tokio::spawn(task) 107 | } 108 | } 109 | 110 | impl Abort for JoinHandle<()> { 111 | fn abort(self) { 112 | JoinHandle::abort(&self) 113 | } 114 | } 115 | 116 | pub type Behaviour = super::Behaviour; 117 | } 118 | 119 | /// A `NetworkBehaviour` for mDNS. Automatically discovers peers on the local network and adds 120 | /// them to the topology. 121 | #[derive(Debug)] 122 | pub struct Behaviour

123 | where 124 | P: Provider, 125 | { 126 | /// InterfaceState config. 127 | config: Config, 128 | 129 | /// Iface watcher. 130 | if_watch: P::Watcher, 131 | 132 | /// Handles to tasks running the mDNS queries. 133 | if_tasks: HashMap, 134 | 135 | query_response_receiver: mpsc::Receiver<(PeerId, Multiaddr, Instant)>, 136 | query_response_sender: mpsc::Sender<(PeerId, Multiaddr, Instant)>, 137 | 138 | /// List of nodes that we have discovered, the address, and when their TTL expires. 139 | /// 140 | /// Each combination of `PeerId` and `Multiaddr` can only appear once, but the same `PeerId` 141 | /// can appear multiple times. 142 | discovered_nodes: SmallVec<[(PeerId, Multiaddr, Instant); 8]>, 143 | 144 | /// Future that fires when the TTL of at least one node in `discovered_nodes` expires. 145 | /// 146 | /// `None` if `discovered_nodes` is empty. 147 | closest_expiration: Option, 148 | 149 | /// The current set of listen addresses. 150 | /// 151 | /// This is shared across all interface tasks using an [`RwLock`]. 152 | /// The [`Behaviour`] updates this upon new [`FromSwarm`] 153 | /// events where as [`InterfaceState`]s read from it to answer inbound mDNS queries. 154 | listen_addresses: Arc>, 155 | 156 | local_peer_id: PeerId, 157 | 158 | /// Pending behaviour events to be emitted. 159 | pending_events: VecDeque>, 160 | } 161 | 162 | impl

Behaviour

163 | where 164 | P: Provider, 165 | { 166 | /// Builds a new `Mdns` behaviour. 167 | pub fn new(config: Config, local_peer_id: PeerId) -> io::Result { 168 | let (tx, rx) = mpsc::channel(10); // Chosen arbitrarily. 169 | 170 | Ok(Self { 171 | config, 172 | if_watch: P::new_watcher()?, 173 | if_tasks: Default::default(), 174 | query_response_receiver: rx, 175 | query_response_sender: tx, 176 | discovered_nodes: Default::default(), 177 | closest_expiration: Default::default(), 178 | listen_addresses: Default::default(), 179 | local_peer_id, 180 | pending_events: Default::default(), 181 | }) 182 | } 183 | 184 | /// Returns true if the given `PeerId` is in the list of nodes discovered through mDNS. 185 | #[deprecated(note = "Use `discovered_nodes` iterator instead.")] 186 | pub fn has_node(&self, peer_id: &PeerId) -> bool { 187 | self.discovered_nodes().any(|p| p == peer_id) 188 | } 189 | 190 | /// Returns the list of nodes that we have discovered through mDNS and that are not expired. 191 | pub fn discovered_nodes(&self) -> impl ExactSizeIterator { 192 | self.discovered_nodes.iter().map(|(p, _, _)| p) 193 | } 194 | 195 | /// Expires a node before the ttl. 196 | #[deprecated(note = "Unused API. Will be removed in the next release.")] 197 | pub fn expire_node(&mut self, peer_id: &PeerId) { 198 | let now = Instant::now(); 199 | for (peer, _addr, expires) in &mut self.discovered_nodes { 200 | if peer == peer_id { 201 | *expires = now; 202 | } 203 | } 204 | self.closest_expiration = Some(P::Timer::at(now)); 205 | } 206 | } 207 | 208 | impl

NetworkBehaviour for Behaviour

209 | where 210 | P: Provider, 211 | { 212 | type ConnectionHandler = dummy::ConnectionHandler; 213 | type ToSwarm = Event; 214 | 215 | fn handle_established_inbound_connection( 216 | &mut self, 217 | _: ConnectionId, 218 | _: PeerId, 219 | _: &Multiaddr, 220 | _: &Multiaddr, 221 | ) -> Result, ConnectionDenied> { 222 | Ok(dummy::ConnectionHandler) 223 | } 224 | 225 | fn handle_pending_outbound_connection( 226 | &mut self, 227 | _connection_id: ConnectionId, 228 | maybe_peer: Option, 229 | _addresses: &[Multiaddr], 230 | _effective_role: Endpoint, 231 | ) -> Result, ConnectionDenied> { 232 | let Some(peer_id) = maybe_peer else { 233 | return Ok(vec![]); 234 | }; 235 | 236 | Ok(self 237 | .discovered_nodes 238 | .iter() 239 | .filter(|(peer, _, _)| peer == &peer_id) 240 | .map(|(_, addr, _)| addr.clone()) 241 | .collect()) 242 | } 243 | 244 | fn handle_established_outbound_connection( 245 | &mut self, 246 | _: ConnectionId, 247 | _: PeerId, 248 | _: &Multiaddr, 249 | _: Endpoint, 250 | _: PortUse, 251 | ) -> Result, ConnectionDenied> { 252 | Ok(dummy::ConnectionHandler) 253 | } 254 | 255 | fn on_connection_handler_event( 256 | &mut self, 257 | _: PeerId, 258 | _: ConnectionId, 259 | ev: THandlerOutEvent, 260 | ) { 261 | libp2p_core::util::unreachable(ev) 262 | } 263 | 264 | fn on_swarm_event(&mut self, event: FromSwarm) { 265 | self.listen_addresses 266 | .write() 267 | .unwrap_or_else(|e| e.into_inner()) 268 | .on_swarm_event(&event); 269 | } 270 | 271 | #[tracing::instrument(level = "trace", name = "NetworkBehaviour::poll", skip(self, cx))] 272 | fn poll( 273 | &mut self, 274 | cx: &mut Context<'_>, 275 | ) -> Poll>> { 276 | if self.config.disabled { 277 | self.pending_events.clear(); 278 | return Poll::Pending; 279 | } 280 | 281 | loop { 282 | // Check for pending events and emit them. 283 | if let Some(event) = self.pending_events.pop_front() { 284 | return Poll::Ready(event); 285 | } 286 | 287 | // Poll ifwatch. 288 | while let Poll::Ready(Some(event)) = Pin::new(&mut self.if_watch).poll_next(cx) { 289 | match event { 290 | Ok(IfEvent::Up(inet)) => { 291 | let addr = inet.addr(); 292 | if addr.is_loopback() { 293 | continue; 294 | } 295 | if addr.is_ipv4() && self.config.enable_ipv6 296 | || addr.is_ipv6() && !self.config.enable_ipv6 297 | { 298 | continue; 299 | } 300 | if let Entry::Vacant(e) = self.if_tasks.entry(addr) { 301 | match InterfaceState::::new( 302 | addr, 303 | self.config.clone(), 304 | self.local_peer_id, 305 | self.listen_addresses.clone(), 306 | self.query_response_sender.clone(), 307 | ) { 308 | Ok(iface_state) => { 309 | e.insert(P::spawn(iface_state)); 310 | } 311 | Err(err) => { 312 | tracing::error!("failed to create `InterfaceState`: {}", err) 313 | } 314 | } 315 | } 316 | } 317 | Ok(IfEvent::Down(inet)) => { 318 | if let Some(handle) = self.if_tasks.remove(&inet.addr()) { 319 | tracing::info!(instance=%inet.addr(), "dropping instance"); 320 | 321 | handle.abort(); 322 | } 323 | } 324 | Err(err) => tracing::error!("if watch returned an error: {}", err), 325 | } 326 | } 327 | // Emit discovered event. 328 | let mut discovered = Vec::new(); 329 | 330 | while let Poll::Ready(Some((peer, addr, expiration))) = 331 | self.query_response_receiver.poll_next_unpin(cx) 332 | { 333 | if let Some((_, _, cur_expires)) = self 334 | .discovered_nodes 335 | .iter_mut() 336 | .find(|(p, a, _)| *p == peer && *a == addr) 337 | { 338 | *cur_expires = cmp::max(*cur_expires, expiration); 339 | } else { 340 | tracing::info!(%peer, address=%addr, "discovered peer on address"); 341 | self.discovered_nodes.push((peer, addr.clone(), expiration)); 342 | discovered.push((peer, addr.clone())); 343 | 344 | self.pending_events 345 | .push_back(ToSwarm::NewExternalAddrOfPeer { 346 | peer_id: peer, 347 | address: addr, 348 | }); 349 | } 350 | } 351 | 352 | if !discovered.is_empty() { 353 | let event = Event::Discovered(discovered); 354 | // Push to the front of the queue so that the behavior event is reported before 355 | // the individual discovered addresses. 356 | self.pending_events 357 | .push_front(ToSwarm::GenerateEvent(event)); 358 | continue; 359 | } 360 | // Emit expired event. 361 | let now = Instant::now(); 362 | let mut closest_expiration = None; 363 | let mut expired = Vec::new(); 364 | self.discovered_nodes.retain(|(peer, addr, expiration)| { 365 | if *expiration <= now { 366 | tracing::info!(%peer, address=%addr, "expired peer on address"); 367 | expired.push((*peer, addr.clone())); 368 | return false; 369 | } 370 | closest_expiration = 371 | Some(closest_expiration.unwrap_or(*expiration).min(*expiration)); 372 | true 373 | }); 374 | if !expired.is_empty() { 375 | let event = Event::Expired(expired); 376 | self.pending_events.push_back(ToSwarm::GenerateEvent(event)); 377 | continue; 378 | } 379 | if let Some(closest_expiration) = closest_expiration { 380 | let mut timer = P::Timer::at(closest_expiration); 381 | let _ = Pin::new(&mut timer).poll_next(cx); 382 | 383 | self.closest_expiration = Some(timer); 384 | } 385 | 386 | return Poll::Pending; 387 | } 388 | } 389 | } 390 | 391 | /// Event that can be produced by the `Mdns` behaviour. 392 | #[derive(Debug, Clone)] 393 | pub enum Event { 394 | /// Discovered nodes through mDNS. 395 | Discovered(Vec<(PeerId, Multiaddr)>), 396 | 397 | /// The given combinations of `PeerId` and `Multiaddr` have expired. 398 | /// 399 | /// Each discovered record has a time-to-live. When this TTL expires and the address hasn't 400 | /// been refreshed, we remove it from the list and emit it as an `Expired` event. 401 | Expired(Vec<(PeerId, Multiaddr)>), 402 | } 403 | -------------------------------------------------------------------------------- /lib/mdns/src/behaviour/iface.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Parity Technologies (UK) Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | mod dns; 22 | mod query; 23 | 24 | use std::{ 25 | collections::VecDeque, 26 | future::Future, 27 | io, 28 | net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}, 29 | pin::Pin, 30 | sync::{Arc, RwLock}, 31 | task::{Context, Poll}, 32 | time::{Duration, Instant}, 33 | }; 34 | 35 | use futures::{channel::mpsc, SinkExt, StreamExt}; 36 | use libp2p_core::Multiaddr; 37 | use libp2p_identity::PeerId; 38 | use libp2p_swarm::ListenAddresses; 39 | use socket2::{Domain, Socket, Type}; 40 | 41 | use self::{ 42 | dns::{build_query, build_query_response, build_service_discovery_response}, 43 | query::MdnsPacket, 44 | }; 45 | use crate::{ 46 | behaviour::{socket::AsyncSocket, timer::Builder}, 47 | Config, 48 | }; 49 | use uuid::Uuid; 50 | 51 | /// Initial interval for starting probe 52 | const INITIAL_TIMEOUT_INTERVAL: Duration = Duration::from_millis(500); 53 | 54 | #[derive(Debug, Clone)] 55 | enum ProbeState { 56 | Probing(Duration), 57 | Finished(Duration), 58 | } 59 | 60 | impl Default for ProbeState { 61 | fn default() -> Self { 62 | ProbeState::Probing(INITIAL_TIMEOUT_INTERVAL) 63 | } 64 | } 65 | 66 | impl ProbeState { 67 | fn interval(&self) -> &Duration { 68 | match self { 69 | ProbeState::Probing(query_interval) => query_interval, 70 | ProbeState::Finished(query_interval) => query_interval, 71 | } 72 | } 73 | } 74 | 75 | /// An mDNS instance for a networking interface. To discover all peers when having multiple 76 | /// interfaces an [`InterfaceState`] is required for each interface. 77 | #[derive(Debug)] 78 | pub(crate) struct InterfaceState { 79 | /// Address this instance is bound to. 80 | addr: IpAddr, 81 | /// Receive socket. 82 | recv_socket: U, 83 | /// Send socket. 84 | send_socket: U, 85 | 86 | listen_addresses: Arc>, 87 | 88 | query_response_sender: mpsc::Sender<(PeerId, Multiaddr, Instant)>, 89 | 90 | /// Buffer used for receiving data from the main socket. 91 | /// RFC6762 discourages packets larger than the interface MTU, but allows sizes of up to 9000 92 | /// bytes, if it can be ensured that all participating devices can handle such large packets. 93 | /// For computers with several interfaces and IP addresses responses can easily reach sizes in 94 | /// the range of 3000 bytes, so 4096 seems sensible for now. For more information see 95 | /// [rfc6762](https://tools.ietf.org/html/rfc6762#page-46). 96 | recv_buffer: [u8; 4096], 97 | /// Buffers pending to send on the main socket. 98 | send_buffer: VecDeque>, 99 | /// Discovery interval. 100 | query_interval: Duration, 101 | /// Discovery timer. 102 | timeout: T, 103 | /// Multicast address. 104 | multicast_addr: IpAddr, 105 | /// Discovered addresses. 106 | discovered: VecDeque<(PeerId, Multiaddr, Instant)>, 107 | /// TTL 108 | ttl: Duration, 109 | probe_state: ProbeState, 110 | local_peer_id: PeerId, 111 | service_name: Vec, 112 | service_name_fqdn: String, 113 | } 114 | 115 | impl InterfaceState 116 | where 117 | U: AsyncSocket, 118 | T: Builder + futures::Stream, 119 | { 120 | /// Builds a new [`InterfaceState`]. 121 | pub(crate) fn new( 122 | addr: IpAddr, 123 | config: Config, 124 | local_peer_id: PeerId, 125 | listen_addresses: Arc>, 126 | query_response_sender: mpsc::Sender<(PeerId, Multiaddr, Instant)>, 127 | ) -> io::Result { 128 | tracing::info!(address=%addr, "creating instance on iface address"); 129 | let recv_socket = match addr { 130 | IpAddr::V4(addr) => { 131 | let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(socket2::Protocol::UDP))?; 132 | socket.set_reuse_address(true)?; 133 | #[cfg(unix)] 134 | socket.set_reuse_port(true)?; 135 | socket.bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 5353).into())?; 136 | socket.set_multicast_loop_v4(true)?; 137 | socket.set_multicast_ttl_v4(255)?; 138 | socket.join_multicast_v4(&crate::IPV4_MDNS_MULTICAST_ADDRESS, &addr)?; 139 | U::from_std(UdpSocket::from(socket))? 140 | } 141 | IpAddr::V6(_) => { 142 | let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(socket2::Protocol::UDP))?; 143 | socket.set_reuse_address(true)?; 144 | #[cfg(unix)] 145 | socket.set_reuse_port(true)?; 146 | socket.bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 5353).into())?; 147 | socket.set_multicast_loop_v6(true)?; 148 | // TODO: find interface matching addr. 149 | socket.join_multicast_v6(&crate::IPV6_MDNS_MULTICAST_ADDRESS, 0)?; 150 | U::from_std(UdpSocket::from(socket))? 151 | } 152 | }; 153 | let bind_addr = match addr { 154 | IpAddr::V4(_) => SocketAddr::new(addr, 0), 155 | IpAddr::V6(_addr) => { 156 | // TODO: if-watch should return the scope_id of an address 157 | // as a workaround we bind to unspecified, which means that 158 | // this probably won't work when using multiple interfaces. 159 | // SocketAddr::V6(SocketAddrV6::new(addr, 0, 0, scope_id)) 160 | SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0) 161 | } 162 | }; 163 | let send_socket = U::from_std(UdpSocket::bind(bind_addr)?)?; 164 | 165 | // randomize timer to prevent all converging and firing at the same time. 166 | let query_interval = { 167 | use rand::Rng; 168 | let mut rng = rand::thread_rng(); 169 | let jitter = rng.gen_range(0..100); 170 | config.query_interval + Duration::from_millis(jitter) 171 | }; 172 | let multicast_addr = match addr { 173 | IpAddr::V4(_) => IpAddr::V4(crate::IPV4_MDNS_MULTICAST_ADDRESS), 174 | IpAddr::V6(_) => IpAddr::V6(crate::IPV6_MDNS_MULTICAST_ADDRESS), 175 | }; 176 | 177 | let service_name = match config.service_fingerprint.as_deref() { 178 | Some(seed) => { 179 | let uuid = Uuid::new_v5(&Uuid::NAMESPACE_DNS, seed); 180 | format!("_p2pclip-{}._udp.local", uuid.simple()) 181 | } 182 | None => crate::DEFAULT_SERVICE_NAME.to_string(), 183 | }; 184 | 185 | Ok(Self { 186 | addr, 187 | recv_socket, 188 | send_socket, 189 | listen_addresses, 190 | query_response_sender, 191 | recv_buffer: [0; 4096], 192 | send_buffer: Default::default(), 193 | discovered: Default::default(), 194 | query_interval, 195 | timeout: T::interval_at(Instant::now(), INITIAL_TIMEOUT_INTERVAL), 196 | multicast_addr, 197 | ttl: config.ttl, 198 | probe_state: Default::default(), 199 | local_peer_id, 200 | service_name: service_name.as_bytes().to_vec(), 201 | service_name_fqdn: format!("{service_name}."), 202 | }) 203 | } 204 | 205 | pub(crate) fn reset_timer(&mut self) { 206 | tracing::trace!(address=%self.addr, probe_state=?self.probe_state, "reset timer"); 207 | let interval = *self.probe_state.interval(); 208 | self.timeout = T::interval(interval); 209 | } 210 | 211 | fn mdns_socket(&self) -> SocketAddr { 212 | SocketAddr::new(self.multicast_addr, 5353) 213 | } 214 | } 215 | 216 | impl Future for InterfaceState 217 | where 218 | U: AsyncSocket, 219 | T: Builder + futures::Stream, 220 | { 221 | type Output = (); 222 | 223 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 224 | let this = self.get_mut(); 225 | 226 | loop { 227 | // 1st priority: Low latency: Create packet ASAP after timeout. 228 | if this.timeout.poll_next_unpin(cx).is_ready() { 229 | tracing::trace!(address=%this.addr, "sending query on iface"); 230 | this.send_buffer 231 | .push_back(build_query(&this.service_name)); 232 | tracing::trace!(address=%this.addr, probe_state=?this.probe_state, "tick"); 233 | 234 | // Stop to probe when the initial interval reach the query interval 235 | if let ProbeState::Probing(interval) = this.probe_state { 236 | let interval = interval * 2; 237 | this.probe_state = if interval >= this.query_interval { 238 | ProbeState::Finished(this.query_interval) 239 | } else { 240 | ProbeState::Probing(interval) 241 | }; 242 | } 243 | 244 | this.reset_timer(); 245 | } 246 | 247 | // 2nd priority: Keep local buffers small: Send packets to remote. 248 | if let Some(packet) = this.send_buffer.pop_front() { 249 | match this.send_socket.poll_write(cx, &packet, this.mdns_socket()) { 250 | Poll::Ready(Ok(_)) => { 251 | tracing::trace!(address=%this.addr, "sent packet on iface address"); 252 | continue; 253 | } 254 | Poll::Ready(Err(err)) => { 255 | tracing::error!(address=%this.addr, "error sending packet on iface address {}", err); 256 | continue; 257 | } 258 | Poll::Pending => { 259 | this.send_buffer.push_front(packet); 260 | } 261 | } 262 | } 263 | 264 | // 3rd priority: Keep local buffers small: Return discovered addresses. 265 | if this.query_response_sender.poll_ready_unpin(cx).is_ready() { 266 | if let Some(discovered) = this.discovered.pop_front() { 267 | match this.query_response_sender.try_send(discovered) { 268 | Ok(()) => {} 269 | Err(e) if e.is_disconnected() => { 270 | return Poll::Ready(()); 271 | } 272 | Err(e) => { 273 | this.discovered.push_front(e.into_inner()); 274 | } 275 | } 276 | 277 | continue; 278 | } 279 | } 280 | 281 | // 4th priority: Remote work: Answer incoming requests. 282 | match this 283 | .recv_socket 284 | .poll_read(cx, &mut this.recv_buffer) 285 | .map_ok(|(len, from)| { 286 | MdnsPacket::new_from_bytes( 287 | &this.recv_buffer[..len], 288 | from, 289 | &this.service_name_fqdn, 290 | ) 291 | }) 292 | { 293 | Poll::Ready(Ok(Ok(Some(MdnsPacket::Query(query))))) => { 294 | tracing::trace!( 295 | address=%this.addr, 296 | remote_address=%query.remote_addr(), 297 | "received query from remote address on address" 298 | ); 299 | 300 | this.send_buffer.extend(build_query_response( 301 | query.query_id(), 302 | this.local_peer_id, 303 | this.listen_addresses 304 | .read() 305 | .unwrap_or_else(|e| e.into_inner()) 306 | .iter(), 307 | this.ttl, 308 | &this.service_name, 309 | )); 310 | continue; 311 | } 312 | Poll::Ready(Ok(Ok(Some(MdnsPacket::Response(response))))) => { 313 | tracing::trace!( 314 | address=%this.addr, 315 | remote_address=%response.remote_addr(), 316 | "received response from remote address on address" 317 | ); 318 | 319 | this.discovered 320 | .extend(response.extract_discovered(Instant::now(), this.local_peer_id)); 321 | 322 | // Stop probing when we have a valid response 323 | if !this.discovered.is_empty() { 324 | this.probe_state = ProbeState::Finished(this.query_interval); 325 | this.reset_timer(); 326 | } 327 | continue; 328 | } 329 | Poll::Ready(Ok(Ok(Some(MdnsPacket::ServiceDiscovery(disc))))) => { 330 | tracing::trace!( 331 | address=%this.addr, 332 | remote_address=%disc.remote_addr(), 333 | "received service discovery from remote address on address" 334 | ); 335 | 336 | this.send_buffer 337 | .push_back(build_service_discovery_response( 338 | disc.query_id(), 339 | this.ttl, 340 | &this.service_name, 341 | )); 342 | continue; 343 | } 344 | Poll::Ready(Err(err)) if err.kind() == std::io::ErrorKind::WouldBlock => { 345 | // No more bytes available on the socket to read 346 | continue; 347 | } 348 | Poll::Ready(Err(err)) => { 349 | tracing::error!("failed reading datagram: {}", err); 350 | return Poll::Ready(()); 351 | } 352 | Poll::Ready(Ok(Err(err))) => { 353 | tracing::debug!("Parsing mdns packet failed: {:?}", err); 354 | continue; 355 | } 356 | Poll::Ready(Ok(Ok(None))) => continue, 357 | Poll::Pending => {} 358 | } 359 | 360 | return Poll::Pending; 361 | } 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /lib/tls/src/certificate.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Parity Technologies (UK) Ltd. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | //! X.509 certificate handling for libp2p 22 | //! 23 | //! This module handles generation, signing, and verification of certificates. 24 | 25 | use libp2p_identity as identity; 26 | use libp2p_identity::PeerId; 27 | use x509_parser::{prelude::*, signature_algorithm::SignatureAlgorithm}; 28 | use ring::{hmac, aead}; 29 | use std::sync::Arc; 30 | 31 | /// The libp2p Public Key Extension is a X.509 extension 32 | /// with the Object Identifier 1.3.6.1.4.1.53594.1.1, 33 | /// allocated by IANA to the libp2p project at Protocol Labs. 34 | const P2P_EXT_OID: [u64; 9] = [1, 3, 6, 1, 4, 1, 53594, 1, 1]; 35 | 36 | /// The peer signs the concatenation of the string `libp2p-tls-handshake:` 37 | /// and the public key that it used to generate the certificate carrying 38 | /// the libp2p Public Key Extension, using its private host key. 39 | /// This signature provides cryptographic proof that the peer was 40 | /// in possession of the private host key at the time the certificate was signed. 41 | const P2P_SIGNING_PREFIX: [u8; 21] = *b"libp2p-tls-handshake:"; 42 | 43 | // Certificates MUST use the NamedCurve encoding for elliptic curve parameters. 44 | // Similarly, hash functions with an output length less than 256 bits MUST NOT be used. 45 | static P2P_SIGNATURE_ALGORITHM: &rcgen::SignatureAlgorithm = &rcgen::PKCS_ECDSA_P256_SHA256; 46 | 47 | #[derive(Debug)] 48 | pub struct AlwaysResolvesCert(Arc); 49 | 50 | impl AlwaysResolvesCert { 51 | pub fn new( 52 | cert: rustls::pki_types::CertificateDer<'static>, 53 | key: &rustls::pki_types::PrivateKeyDer<'_>, 54 | ) -> Result { 55 | let certified_key = rustls::sign::CertifiedKey::new( 56 | vec![cert], 57 | rustls::crypto::ring::sign::any_ecdsa_type(key)?, 58 | ); 59 | Ok(Self(Arc::new(certified_key))) 60 | } 61 | } 62 | 63 | impl rustls::client::ResolvesClientCert for AlwaysResolvesCert { 64 | fn resolve( 65 | &self, 66 | _root_hint_subjects: &[&[u8]], 67 | _sigschemes: &[rustls::SignatureScheme], 68 | ) -> Option> { 69 | Some(Arc::clone(&self.0)) 70 | } 71 | 72 | fn has_certs(&self) -> bool { 73 | true 74 | } 75 | } 76 | 77 | impl rustls::server::ResolvesServerCert for AlwaysResolvesCert { 78 | fn resolve( 79 | &self, 80 | _client_hello: rustls::server::ClientHello<'_>, 81 | ) -> Option> { 82 | Some(Arc::clone(&self.0)) 83 | } 84 | } 85 | 86 | /// Generates a self-signed TLS certificate that includes a libp2p-specific 87 | /// certificate extension containing the public key of the given keypair. 88 | pub fn generate( 89 | identity_keypair: &identity::Keypair, 90 | pre_shared_key: Option, 91 | ) -> Result< 92 | ( 93 | rustls::pki_types::CertificateDer<'static>, 94 | rustls::pki_types::PrivateKeyDer<'static>, 95 | ), 96 | GenError, 97 | > { 98 | // Keypair used to sign the certificate. 99 | // SHOULD NOT be related to the host's key. 100 | // Endpoints MAY generate a new key and certificate 101 | // for every connection attempt, or they MAY reuse the same key 102 | // and certificate for multiple connections. 103 | let certificate_keypair = rcgen::KeyPair::generate_for(P2P_SIGNATURE_ALGORITHM)?; 104 | let rustls_key = rustls::pki_types::PrivateKeyDer::from( 105 | rustls::pki_types::PrivatePkcs8KeyDer::from(certificate_keypair.serialize_der()), 106 | ); 107 | 108 | let certificate = { 109 | let mut params = rcgen::CertificateParams::default(); 110 | params.distinguished_name = rcgen::DistinguishedName::new(); 111 | params.custom_extensions.push(make_libp2p_extension( 112 | identity_keypair, 113 | &certificate_keypair, 114 | pre_shared_key, 115 | )?); 116 | params.self_signed(&certificate_keypair)? 117 | }; 118 | 119 | Ok((certificate.into(), rustls_key)) 120 | } 121 | 122 | /// Attempts to parse the provided bytes as a [`P2pCertificate`]. 123 | /// 124 | /// For this to succeed, the certificate must contain the specified extension and the signature must 125 | /// match the embedded public key. 126 | pub fn parse<'a>(certificate: &'a rustls::pki_types::CertificateDer<'a>, pre_shared_key: Option) -> Result, ParseError> { 127 | let certificate = parse_unverified(certificate.as_ref(), pre_shared_key)?; 128 | 129 | certificate.verify()?; 130 | 131 | Ok(certificate) 132 | } 133 | 134 | /// Encrypt the signature in the libp2p Public Key Extension with PSK and public key 135 | fn encrypt_ext_signature(raw_signature: Vec, psk: &str, public_key: &[u8]) -> Result, ring::error::Unspecified> { 136 | let mut out = raw_signature.clone(); 137 | let s_key = hmac::Key::new(hmac::HMAC_SHA256, public_key); 138 | let psk_key = hmac::sign(&s_key, psk.as_ref()); 139 | let u_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, psk_key.as_ref())?; 140 | let nonce = aead::Nonce::assume_unique_for_key(*b"p2pclipboard"); 141 | let cipher = aead::LessSafeKey::new(u_key); 142 | cipher.seal_in_place_append_tag(nonce, aead::Aad::empty(), &mut out)?; 143 | Ok(out) 144 | } 145 | 146 | /// Decrypt the signature in the libp2p Public Key Extension with PSK and public key 147 | fn decrypt_ext_signature(raw_signature: Vec, psk: &str, public_key: &[u8]) -> Result, ring::error::Unspecified> { 148 | let mut buf = raw_signature.clone(); 149 | let s_key = hmac::Key::new(hmac::HMAC_SHA256, public_key); 150 | let psk_key = hmac::sign(&s_key, psk.as_ref()); 151 | let u_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, psk_key.as_ref())?; 152 | let nonce = aead::Nonce::assume_unique_for_key(*b"p2pclipboard"); 153 | let cipher = aead::LessSafeKey::new(u_key); 154 | Ok(Vec::from(cipher.open_in_place(nonce, aead::Aad::empty(), &mut buf)?)) 155 | } 156 | 157 | /// An X.509 certificate with a libp2p-specific extension 158 | /// is used to secure libp2p connections. 159 | #[derive(Debug)] 160 | pub struct P2pCertificate<'a> { 161 | certificate: X509Certificate<'a>, 162 | /// This is a specific libp2p Public Key Extension with two values: 163 | /// * the public host key 164 | /// * a signature performed using the private host key 165 | extension: P2pExtension, 166 | } 167 | 168 | /// The contents of the specific libp2p extension, containing the public host key 169 | /// and a signature performed using the private host key. 170 | #[derive(Debug)] 171 | pub struct P2pExtension { 172 | public_key: identity::PublicKey, 173 | /// This signature provides cryptographic proof that the peer was 174 | /// in possession of the private host key at the time the certificate was signed. 175 | signature: Vec, 176 | } 177 | 178 | #[derive(Debug, thiserror::Error)] 179 | #[error(transparent)] 180 | pub struct GenError(#[from] rcgen::Error); 181 | 182 | #[derive(Debug, thiserror::Error)] 183 | #[error(transparent)] 184 | pub struct ParseError(#[from] pub(crate) webpki::Error); 185 | 186 | #[derive(Debug, thiserror::Error)] 187 | #[error(transparent)] 188 | pub struct VerificationError(#[from] pub(crate) webpki::Error); 189 | 190 | /// Internal function that only parses but does not verify the certificate. 191 | /// 192 | /// Useful for testing but unsuitable for production. 193 | fn parse_unverified(der_input: &[u8], pre_shared_key: Option) -> Result, webpki::Error> { 194 | let x509 = X509Certificate::from_der(der_input) 195 | .map(|(_rest_input, x509)| x509) 196 | .map_err(|_| webpki::Error::BadDer)?; 197 | 198 | let p2p_ext_oid = der_parser::oid::Oid::from(&P2P_EXT_OID) 199 | .expect("This is a valid OID of p2p extension; qed"); 200 | 201 | let mut libp2p_extension = None; 202 | 203 | for ext in x509.extensions() { 204 | let oid = &ext.oid; 205 | if oid == &p2p_ext_oid && libp2p_extension.is_some() { 206 | // The extension was already parsed 207 | return Err(webpki::Error::BadDer); 208 | } 209 | 210 | if oid == &p2p_ext_oid { 211 | // The public host key and the signature are ASN.1-encoded 212 | // into the SignedKey data structure, which is carried 213 | // in the libp2p Public Key Extension. 214 | // SignedKey ::= SEQUENCE { 215 | // publicKey OCTET STRING, 216 | // signature OCTET STRING 217 | // } 218 | let (public_key, signature): (Vec, Vec) = 219 | yasna::decode_der(ext.value).map_err(|_| webpki::Error::ExtensionValueInvalid)?; 220 | // The publicKey field of SignedKey contains the public host key 221 | // of the endpoint, encoded using the following protobuf: 222 | // enum KeyType { 223 | // RSA = 0; 224 | // Ed25519 = 1; 225 | // Secp256k1 = 2; 226 | // ECDSA = 3; 227 | // } 228 | // message PublicKey { 229 | // required KeyType Type = 1; 230 | // required bytes Data = 2; 231 | // } 232 | let signature = match pre_shared_key { 233 | Some(ref psk) => decrypt_ext_signature(signature, psk, &public_key) 234 | .map_err(|_| webpki::Error::ExtensionValueInvalid)?, 235 | None => signature 236 | }; 237 | let public_key = identity::PublicKey::try_decode_protobuf(&public_key) 238 | .map_err(|_| webpki::Error::UnknownIssuer)?; 239 | 240 | let ext = P2pExtension { 241 | public_key, 242 | signature, 243 | }; 244 | libp2p_extension = Some(ext); 245 | continue; 246 | } 247 | 248 | if ext.critical { 249 | // Endpoints MUST abort the connection attempt if the certificate 250 | // contains critical extensions that the endpoint does not understand. 251 | return Err(webpki::Error::UnsupportedCriticalExtension); 252 | } 253 | 254 | // Implementations MUST ignore non-critical extensions with unknown OIDs. 255 | } 256 | 257 | // The certificate MUST contain the libp2p Public Key Extension. 258 | // If this extension is missing, endpoints MUST abort the connection attempt. 259 | let extension = libp2p_extension.ok_or(webpki::Error::BadDer)?; 260 | 261 | let certificate = P2pCertificate { 262 | certificate: x509, 263 | extension, 264 | }; 265 | 266 | Ok(certificate) 267 | } 268 | 269 | fn make_libp2p_extension( 270 | identity_keypair: &identity::Keypair, 271 | certificate_keypair: &rcgen::KeyPair, 272 | pre_shared_key: Option, 273 | ) -> Result { 274 | let serialized_pubkey = identity_keypair.public().encode_protobuf(); 275 | // The peer signs the concatenation of the string `libp2p-tls-handshake:` 276 | // and the public key that it used to generate the certificate carrying 277 | // the libp2p Public Key Extension, using its private host key. 278 | let signature = { 279 | let mut msg = vec![]; 280 | msg.extend(P2P_SIGNING_PREFIX); 281 | msg.extend(certificate_keypair.public_key_der()); 282 | 283 | let raw = identity_keypair 284 | .sign(&msg) 285 | .map_err(|_| rcgen::Error::RingUnspecified)?; 286 | match pre_shared_key { 287 | Some(psk) => encrypt_ext_signature(raw, &psk, &serialized_pubkey) 288 | .map_err(|_| rcgen::Error::RingKeyRejected("PSK encryption failed".into()))?, 289 | None => raw 290 | } 291 | }; 292 | 293 | // The public host key and the signature are ASN.1-encoded 294 | // into the SignedKey data structure, which is carried 295 | // in the libp2p Public Key Extension. 296 | // SignedKey ::= SEQUENCE { 297 | // publicKey OCTET STRING, 298 | // signature OCTET STRING 299 | // } 300 | let extension_content = { 301 | yasna::encode_der(&(serialized_pubkey, signature)) 302 | }; 303 | 304 | // This extension MAY be marked critical. 305 | let mut ext = rcgen::CustomExtension::from_oid_content(&P2P_EXT_OID, extension_content); 306 | ext.set_criticality(true); 307 | 308 | Ok(ext) 309 | } 310 | 311 | impl P2pCertificate<'_> { 312 | /// The [`PeerId`] of the remote peer. 313 | pub fn peer_id(&self) -> PeerId { 314 | self.extension.public_key.to_peer_id() 315 | } 316 | 317 | /// Verify the `signature` of the `message` signed by the private key corresponding to the 318 | /// public key stored in the certificate. 319 | pub fn verify_signature( 320 | &self, 321 | signature_scheme: rustls::SignatureScheme, 322 | message: &[u8], 323 | signature: &[u8], 324 | ) -> Result<(), VerificationError> { 325 | let pk = self.public_key(signature_scheme)?; 326 | pk.verify(message, signature) 327 | .map_err(|_| webpki::Error::InvalidSignatureForPublicKey)?; 328 | 329 | Ok(()) 330 | } 331 | 332 | /// Get a [`ring::signature::UnparsedPublicKey`] for this `signature_scheme`. 333 | /// Return `Error` if the `signature_scheme` does not match the public key signature 334 | /// and hashing algorithm or if the `signature_scheme` is not supported. 335 | fn public_key( 336 | &self, 337 | signature_scheme: rustls::SignatureScheme, 338 | ) -> Result, webpki::Error> { 339 | use ring::signature; 340 | use rustls::SignatureScheme::*; 341 | 342 | let current_signature_scheme = self.signature_scheme()?; 343 | if signature_scheme != current_signature_scheme { 344 | // This certificate was signed with a different signature scheme 345 | return Err(webpki::Error::UnsupportedSignatureAlgorithmForPublicKey); 346 | } 347 | 348 | let verification_algorithm: &dyn signature::VerificationAlgorithm = match signature_scheme { 349 | RSA_PKCS1_SHA256 => &signature::RSA_PKCS1_2048_8192_SHA256, 350 | RSA_PKCS1_SHA384 => &signature::RSA_PKCS1_2048_8192_SHA384, 351 | RSA_PKCS1_SHA512 => &signature::RSA_PKCS1_2048_8192_SHA512, 352 | ECDSA_NISTP256_SHA256 => &signature::ECDSA_P256_SHA256_ASN1, 353 | ECDSA_NISTP384_SHA384 => &signature::ECDSA_P384_SHA384_ASN1, 354 | ECDSA_NISTP521_SHA512 => { 355 | // See https://github.com/briansmith/ring/issues/824 356 | return Err(webpki::Error::UnsupportedSignatureAlgorithm); 357 | } 358 | RSA_PSS_SHA256 => &signature::RSA_PSS_2048_8192_SHA256, 359 | RSA_PSS_SHA384 => &signature::RSA_PSS_2048_8192_SHA384, 360 | RSA_PSS_SHA512 => &signature::RSA_PSS_2048_8192_SHA512, 361 | ED25519 => &signature::ED25519, 362 | ED448 => { 363 | // See https://github.com/briansmith/ring/issues/463 364 | return Err(webpki::Error::UnsupportedSignatureAlgorithm); 365 | } 366 | // Similarly, hash functions with an output length less than 256 bits 367 | // MUST NOT be used, due to the possibility of collision attacks. 368 | // In particular, MD5 and SHA1 MUST NOT be used. 369 | RSA_PKCS1_SHA1 => return Err(webpki::Error::UnsupportedSignatureAlgorithm), 370 | ECDSA_SHA1_Legacy => return Err(webpki::Error::UnsupportedSignatureAlgorithm), 371 | _ => return Err(webpki::Error::UnsupportedSignatureAlgorithm), 372 | }; 373 | let spki = &self.certificate.tbs_certificate.subject_pki; 374 | let key = signature::UnparsedPublicKey::new( 375 | verification_algorithm, 376 | spki.subject_public_key.as_ref(), 377 | ); 378 | 379 | Ok(key) 380 | } 381 | 382 | /// This method validates the certificate according to libp2p TLS 1.3 specs. 383 | /// The certificate MUST: 384 | /// 1. be valid at the time it is received by the peer; 385 | /// 2. use the NamedCurve encoding; 386 | /// 3. use hash functions with an output length not less than 256 bits; 387 | /// 4. be self signed; 388 | /// 5. contain a valid signature in the specific libp2p extension. 389 | fn verify(&self) -> Result<(), webpki::Error> { 390 | use webpki::Error; 391 | // The certificate MUST have NotBefore and NotAfter fields set 392 | // such that the certificate is valid at the time it is received by the peer. 393 | if !self.certificate.validity().is_valid() { 394 | return Err(Error::InvalidCertValidity); 395 | } 396 | 397 | // Certificates MUST use the NamedCurve encoding for elliptic curve parameters. 398 | // Similarly, hash functions with an output length less than 256 bits 399 | // MUST NOT be used, due to the possibility of collision attacks. 400 | // In particular, MD5 and SHA1 MUST NOT be used. 401 | // Endpoints MUST abort the connection attempt if it is not used. 402 | let signature_scheme = self.signature_scheme()?; 403 | // Endpoints MUST abort the connection attempt if the certificate’s 404 | // self-signature is not valid. 405 | let raw_certificate = self.certificate.tbs_certificate.as_ref(); 406 | let signature = self.certificate.signature_value.as_ref(); 407 | // check if self signed 408 | self.verify_signature(signature_scheme, raw_certificate, signature) 409 | .map_err(|_| Error::SignatureAlgorithmMismatch)?; 410 | 411 | let subject_pki = self.certificate.public_key().raw; 412 | 413 | // The peer signs the concatenation of the string `libp2p-tls-handshake:` 414 | // and the public key that it used to generate the certificate carrying 415 | // the libp2p Public Key Extension, using its private host key. 416 | let mut msg = vec![]; 417 | msg.extend(P2P_SIGNING_PREFIX); 418 | msg.extend(subject_pki); 419 | 420 | // This signature provides cryptographic proof that the peer was in possession 421 | // of the private host key at the time the certificate was signed. 422 | // Peers MUST verify the signature, and abort the connection attempt 423 | // if signature verification fails. 424 | let user_owns_sk = self 425 | .extension 426 | .public_key 427 | .verify(&msg, &self.extension.signature); 428 | if !user_owns_sk { 429 | return Err(Error::UnknownIssuer); 430 | } 431 | 432 | Ok(()) 433 | } 434 | 435 | /// Return the signature scheme corresponding to [`AlgorithmIdentifier`]s 436 | /// of `subject_pki` and `signature_algorithm` 437 | /// according to . 438 | fn signature_scheme(&self) -> Result { 439 | // Certificates MUST use the NamedCurve encoding for elliptic curve parameters. 440 | // Endpoints MUST abort the connection attempt if it is not used. 441 | use oid_registry::*; 442 | use rustls::SignatureScheme::*; 443 | 444 | let signature_algorithm = &self.certificate.signature_algorithm; 445 | let pki_algorithm = &self.certificate.tbs_certificate.subject_pki.algorithm; 446 | 447 | if pki_algorithm.algorithm == OID_PKCS1_RSAENCRYPTION { 448 | if signature_algorithm.algorithm == OID_PKCS1_SHA256WITHRSA { 449 | return Ok(RSA_PKCS1_SHA256); 450 | } 451 | if signature_algorithm.algorithm == OID_PKCS1_SHA384WITHRSA { 452 | return Ok(RSA_PKCS1_SHA384); 453 | } 454 | if signature_algorithm.algorithm == OID_PKCS1_SHA512WITHRSA { 455 | return Ok(RSA_PKCS1_SHA512); 456 | } 457 | if signature_algorithm.algorithm == OID_PKCS1_RSASSAPSS { 458 | // According to https://datatracker.ietf.org/doc/html/rfc4055#section-3.1: 459 | // Inside of params there should be a sequence of: 460 | // - Hash Algorithm 461 | // - Mask Algorithm 462 | // - Salt Length 463 | // - Trailer Field 464 | 465 | // We are interested in Hash Algorithm only 466 | 467 | if let Ok(SignatureAlgorithm::RSASSA_PSS(params)) = 468 | SignatureAlgorithm::try_from(signature_algorithm) 469 | { 470 | let hash_oid = params.hash_algorithm_oid(); 471 | if hash_oid == &OID_NIST_HASH_SHA256 { 472 | return Ok(RSA_PSS_SHA256); 473 | } 474 | if hash_oid == &OID_NIST_HASH_SHA384 { 475 | return Ok(RSA_PSS_SHA384); 476 | } 477 | if hash_oid == &OID_NIST_HASH_SHA512 { 478 | return Ok(RSA_PSS_SHA512); 479 | } 480 | } 481 | 482 | // Default hash algo is SHA-1, however: 483 | // In particular, MD5 and SHA1 MUST NOT be used. 484 | return Err(webpki::Error::UnsupportedSignatureAlgorithm); 485 | } 486 | } 487 | 488 | if pki_algorithm.algorithm == OID_KEY_TYPE_EC_PUBLIC_KEY { 489 | let signature_param = pki_algorithm 490 | .parameters 491 | .as_ref() 492 | .ok_or(webpki::Error::BadDer)? 493 | .as_oid() 494 | .map_err(|_| webpki::Error::BadDer)?; 495 | if signature_param == OID_EC_P256 496 | && signature_algorithm.algorithm == OID_SIG_ECDSA_WITH_SHA256 497 | { 498 | return Ok(ECDSA_NISTP256_SHA256); 499 | } 500 | if signature_param == OID_NIST_EC_P384 501 | && signature_algorithm.algorithm == OID_SIG_ECDSA_WITH_SHA384 502 | { 503 | return Ok(ECDSA_NISTP384_SHA384); 504 | } 505 | if signature_param == OID_NIST_EC_P521 506 | && signature_algorithm.algorithm == OID_SIG_ECDSA_WITH_SHA512 507 | { 508 | return Ok(ECDSA_NISTP521_SHA512); 509 | } 510 | return Err(webpki::Error::UnsupportedSignatureAlgorithm); 511 | } 512 | 513 | if signature_algorithm.algorithm == OID_SIG_ED25519 { 514 | return Ok(ED25519); 515 | } 516 | if signature_algorithm.algorithm == OID_SIG_ED448 { 517 | return Ok(ED448); 518 | } 519 | 520 | Err(webpki::Error::UnsupportedSignatureAlgorithm) 521 | } 522 | 523 | } -------------------------------------------------------------------------------- /src/network.rs: -------------------------------------------------------------------------------- 1 | use ed25519_dalek::{pkcs8::DecodePrivateKey, SigningKey}; 2 | use futures::prelude::*; 3 | use hex_literal::hex; 4 | use libp2p::gossipsub::{Behaviour, PublishError}; 5 | use libp2p::kad::QueryResult; 6 | use libp2p::swarm::ConnectionError; 7 | use libp2p::{ 8 | gossipsub::{self, IdentTopic, MessageAuthenticity, MessageId, ValidationMode}, 9 | identify, 10 | identity::{Keypair, PublicKey}, 11 | kad::{self, store::MemoryStore}, 12 | multiaddr::Protocol, 13 | swarm::{self, NetworkBehaviour, SwarmEvent}, 14 | tcp, yamux, Multiaddr, PeerId, Swarm, 15 | }; 16 | use libp2p_mdns as mdns; 17 | use libp2p_tls as tls; 18 | use log::{debug, error, info, warn}; 19 | use machine_uid; 20 | use std::collections::{hash_map::DefaultHasher, HashMap, HashSet, VecDeque}; 21 | use std::hash::{Hash, Hasher}; 22 | use std::{ 23 | error::Error, 24 | net::{Ipv4Addr, SocketAddrV4}, 25 | time::{Duration, SystemTime}, 26 | }; 27 | use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; 28 | use tokio::sync::{mpsc, oneshot}; 29 | 30 | #[derive(NetworkBehaviour)] 31 | struct P2pClipboardBehaviour { 32 | gossipsub: Behaviour, 33 | kademlia: kad::Behaviour, 34 | identify: identify::Behaviour, 35 | mdns: mdns::tokio::Behaviour, 36 | } 37 | 38 | #[derive(Clone, Eq, Hash, PartialEq, Debug)] 39 | struct PeerEndpointCache { 40 | peer_id: PeerId, 41 | address: Multiaddr, 42 | } 43 | 44 | #[derive(Clone, Eq, Hash, PartialEq, Debug)] 45 | struct ConnectionRetryTask { 46 | target: PeerEndpointCache, 47 | retry_count: usize, 48 | } 49 | 50 | #[derive(Default, Clone)] 51 | struct CompressionTransform; 52 | 53 | // Not used directly, we will derive new keys using machine ID from this key. 54 | // We have to do this to have a stable Peer ID when no key is specified by the user. 55 | const ID_SEED: [u8; 118] = hex!("2d2d2d2d2d424547494e2050524956415445204b45592d2d2d2d2d0a4d43344341514177425159444b3256774243494549444c3968565958485271304f48386f774a72363169416a45385a52614263363254373761723564397339670a2d2d2d2d2d454e442050524956415445204b45592d2d2d2d2d"); 56 | 57 | impl gossipsub::DataTransform for CompressionTransform { 58 | fn inbound_transform( 59 | &self, 60 | raw_message: gossipsub::RawMessage, 61 | ) -> Result { 62 | let buf: Vec = zstd::decode_all(&*raw_message.data)?; 63 | Ok(gossipsub::Message { 64 | source: raw_message.source, 65 | data: buf, 66 | sequence_number: raw_message.sequence_number, 67 | topic: raw_message.topic, 68 | }) 69 | } 70 | 71 | fn outbound_transform( 72 | &self, 73 | _topic: &gossipsub::TopicHash, 74 | data: Vec, 75 | ) -> Result, std::io::Error> { 76 | let compressed_bytes = zstd::encode_all(&*data, 0)?; 77 | debug!("Compressed size {}", compressed_bytes.len()); 78 | Ok(compressed_bytes) 79 | } 80 | } 81 | 82 | async fn retry_waiting_thread( 83 | mut rx: UnboundedReceiver, 84 | callback: UnboundedSender, 85 | mut shutdown: oneshot::Receiver<()>, 86 | ) { 87 | async fn delayed_callback( 88 | callback: UnboundedSender, 89 | delay: Duration, 90 | task: PeerEndpointCache, 91 | ) { 92 | tokio::time::sleep(delay).await; 93 | let _ = callback.send(task); 94 | } 95 | loop { 96 | tokio::select! { 97 | Some(task) = rx.recv() => { 98 | let seconds = task.retry_count.checked_pow(2).unwrap_or(0) + 1; 99 | let payload = task.target; 100 | tokio::spawn(delayed_callback(callback.clone(), Duration::from_secs(seconds as u64), payload)); 101 | }, 102 | _ = &mut shutdown => { 103 | debug!("Connection retry waiting thread shutdown received"); 104 | return; 105 | }, 106 | } 107 | } 108 | } 109 | 110 | pub async fn start_network( 111 | rx: mpsc::Receiver, 112 | tx: mpsc::Sender, 113 | connect_arg: Option>, 114 | key_arg: Option, 115 | listen_arg: Option, 116 | psk: Option, 117 | disable_mdns: bool, 118 | ) -> Result<(), Box> { 119 | let id_keys = match key_arg { 120 | Some(arg) => { 121 | let pem = std::fs::read_to_string(arg)?; 122 | let mut verifying_key_bytes = SigningKey::from_pkcs8_pem(&pem)?.to_bytes(); 123 | Keypair::ed25519_from_bytes(&mut verifying_key_bytes)? 124 | } 125 | None => { 126 | let id: String = machine_uid::get()?; 127 | let mut key_bytes = 128 | SigningKey::from_pkcs8_pem(std::str::from_utf8(&ID_SEED)?)?.to_bytes(); 129 | let key = Keypair::ed25519_from_bytes(&mut key_bytes)?; 130 | let mut new_key = key 131 | .derive_secret(id.as_ref()) 132 | .expect("can derive secret for ed25519"); 133 | Keypair::ed25519_from_bytes(&mut new_key)? 134 | } 135 | }; 136 | let peer_id = PeerId::from(id_keys.public()); 137 | info!("Local peer id: {}", peer_id.to_base58()); 138 | 139 | // Create a Gossipsub topic 140 | let gossipsub_topic = IdentTopic::new("p2p_clipboard"); 141 | 142 | // get optional boot address and peerId 143 | let (boot_addr, boot_peer_id) = match connect_arg { 144 | Some(arg) => { 145 | // Clap should already guarantee length == 2, just for sanity 146 | if arg.len() == 2 { 147 | let peer_id = arg[1].clone().parse::(); 148 | let addr_input = arg[0].clone(); 149 | let sock_addr = parse_ipv4_with_port(Some(addr_input)); 150 | let multiaddr = match sock_addr { 151 | Ok((ip, port)) => Ok(format!("/ip4/{}/tcp/{}", ip, port) 152 | .parse::() 153 | .unwrap()), 154 | Err(_) => Err(()), 155 | } 156 | .unwrap_or_else(|_| { 157 | error!("Connect address is not a valid socket address"); 158 | std::process::exit(1); 159 | }); 160 | (Some(multiaddr), peer_id.ok()) 161 | } else { 162 | (None, None) 163 | } 164 | } 165 | None => (None, None), 166 | }; 167 | 168 | // Create a Swarm to manage peers and events. 169 | let mut swarm: Swarm = { 170 | let mut chat_behaviour = P2pClipboardBehaviour { 171 | gossipsub: create_gossipsub_behavior(id_keys.clone()), 172 | kademlia: create_kademlia_behavior(peer_id), 173 | identify: create_identify_behavior(id_keys.public()), 174 | mdns: create_mdns_behavior(peer_id, psk.clone(), disable_mdns), 175 | }; 176 | 177 | // subscribes to our topic 178 | chat_behaviour 179 | .gossipsub 180 | .subscribe(&gossipsub_topic) 181 | .unwrap(); 182 | 183 | libp2p::SwarmBuilder::with_existing_identity(id_keys) 184 | .with_tokio() 185 | .with_tcp( 186 | tcp::Config::default(), 187 | tls::Config::new_with_psk(psk), 188 | yamux::Config::default, 189 | )? 190 | .with_behaviour(|_key| chat_behaviour)? 191 | .with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_secs(60))) 192 | .build() 193 | }; 194 | 195 | swarm 196 | .behaviour_mut() 197 | .kademlia 198 | .set_mode(Some(kad::Mode::Server)); 199 | 200 | let multiaddr = match listen_arg { 201 | Some(socket_addr_string) => { 202 | if let Ok((ip, port)) = parse_ipv4_with_port(Some(socket_addr_string)) { 203 | Ok(format!("/ip4/{}/tcp/{}", ip, port)) 204 | } else { 205 | Err(()) 206 | } 207 | } 208 | // Listen on all interfaces and whatever port the OS assigns 209 | None => Ok("/ip4/0.0.0.0/tcp/0".to_string()), 210 | } 211 | .unwrap_or_else(|_| { 212 | error!("Listen address is not a valid socket address"); 213 | std::process::exit(1); 214 | }); 215 | 216 | let _ = swarm.listen_on(multiaddr.parse()?).unwrap_or_else(|_| { 217 | error!("Cannot listen on specified address"); 218 | std::process::exit(1); 219 | }); 220 | 221 | // FIXME: Can we swap the boot_node to some fallback node if it is temporarily unavailable? 222 | let boot_node = { 223 | // Reach out to another node if specified 224 | if let Some(boot_addr) = boot_addr { 225 | debug!("Will dial {}", &boot_addr); 226 | swarm 227 | .behaviour_mut() 228 | .kademlia 229 | .add_address(&boot_peer_id.unwrap(), boot_addr.clone()); 230 | let _ = swarm.dial(boot_addr.clone()); 231 | let _ = swarm.disconnect_peer_id(boot_peer_id.unwrap()); 232 | Some(PeerEndpointCache { 233 | peer_id: boot_peer_id.unwrap(), 234 | address: boot_addr.clone(), 235 | }) 236 | } else { 237 | None 238 | } 239 | }; 240 | let (retry_queue_tx, retry_queue_rx) = mpsc::unbounded_channel::(); 241 | let (retry_callback_queue_tx, retry_callback_queue_rx) = 242 | mpsc::unbounded_channel::(); 243 | let (shutdown_channel_tx, shutdown_channel_rx) = oneshot::channel::<()>(); 244 | let _retry_handle = tokio::spawn(retry_waiting_thread( 245 | retry_queue_rx, 246 | retry_callback_queue_tx, 247 | shutdown_channel_rx, 248 | )); 249 | let swarm_handle = tokio::spawn(run( 250 | swarm, 251 | gossipsub_topic, 252 | rx, 253 | tx, 254 | boot_node, 255 | retry_queue_tx, 256 | retry_callback_queue_rx, 257 | )); 258 | swarm_handle.await?; 259 | let _ = shutdown_channel_tx.send(()); 260 | Ok(()) 261 | } 262 | 263 | async fn run( 264 | mut swarm: Swarm, 265 | gossipsub_topic: IdentTopic, 266 | mut rx: mpsc::Receiver, 267 | tx: mpsc::Sender, 268 | boot_node: Option, 269 | retry_queue_tx: UnboundedSender, 270 | mut retry_callback_queue_rx: UnboundedReceiver, 271 | ) { 272 | // We have to cache all endpoints so that we can reconnect to the p2p networks when our IP has changed. 273 | let mut endpoint_cache: VecDeque = VecDeque::new(); 274 | let mut unique_endpoints: HashSet = HashSet::new(); 275 | let mut current_listen_addresses: HashSet = HashSet::new(); 276 | // We also need to cache the announced identity for each node, because they will be different from what we actually connects. 277 | let mut announced_identities: HashMap> = HashMap::new(); 278 | let mut failing_connections: HashMap = HashMap::new(); 279 | 280 | let mut t = SystemTime::now(); 281 | let mut sleep; 282 | loop { 283 | let to_publish = { 284 | sleep = Box::pin(tokio::time::sleep(Duration::from_secs(30)).fuse()); 285 | tokio::select! { 286 | Some(message) = rx.recv() => { 287 | debug!("Received local clipboard: {}", message.clone()); 288 | Some((gossipsub_topic.clone(), message.clone())) 289 | }, 290 | event = swarm.select_next_some() => match event { 291 | SwarmEvent::Behaviour(P2pClipboardBehaviourEvent::Gossipsub(ref gossip_event)) => { 292 | if let gossipsub::Event::Message { 293 | propagation_source: peer_id, 294 | message_id: id, 295 | message, 296 | } = gossip_event 297 | { 298 | debug!("Got message: {} with id: {} from peer: {:?}", 299 | String::from_utf8_lossy(&message.data), 300 | id, 301 | peer_id); 302 | tx.send(String::from_utf8_lossy(&message.data).parse().unwrap()).await.expect("Panic when sending to channel"); 303 | } 304 | None 305 | } 306 | SwarmEvent::Behaviour(P2pClipboardBehaviourEvent::Identify(ref identify_event)) => { 307 | match identify_event { 308 | identify::Event::Received { 309 | connection_id: _, 310 | peer_id, 311 | info: 312 | identify::Info { 313 | listen_addrs, 314 | .. 315 | }, 316 | } => { 317 | // We will receive identify info for 3 reasons: 318 | // 1. A new peer want to give us its info for negotiation 319 | // 2. An existing peer periodically ping us with these info to show existence 320 | // 3. An existing peer has its network config changed and want to tell us its new address 321 | // FIXME: In some cases the peers could behind some kind of NAT so they can have their addresses changed without announcing the change. 322 | let old_addrs = announced_identities.insert( 323 | *peer_id, 324 | listen_addrs.clone() 325 | ); 326 | if let Some(old_vec) = old_addrs { 327 | let new: HashSet = listen_addrs.iter().cloned().collect(); 328 | let old: HashSet = old_vec.iter().cloned().collect(); 329 | // Announced in old but not in new announcement, remove it from routing table 330 | let changes = old.difference(&new); 331 | for addr in changes { 332 | debug!("Removing expired addr {addr} trough identify"); 333 | swarm.behaviour_mut().kademlia.remove_address(&peer_id, addr); 334 | } 335 | } 336 | for addr in listen_addrs { 337 | debug!("received addr {addr} trough identify"); 338 | if !is_multiaddr_local(addr) { 339 | swarm.behaviour_mut().kademlia.add_address(&peer_id, addr.clone()); 340 | } 341 | } 342 | } 343 | _ => { 344 | debug!("got other identify event"); 345 | } 346 | } 347 | None 348 | } 349 | SwarmEvent::Behaviour(P2pClipboardBehaviourEvent::Kademlia(ref kad_event)) => { 350 | match kad_event { 351 | kad::Event::RoutingUpdated {peer, ..} => { 352 | debug!("Routing updated for {:#?}", peer); 353 | }, 354 | kad::Event::OutboundQueryProgressed { 355 | result: QueryResult::GetClosestPeers(result), 356 | .. 357 | } => { 358 | match result { 359 | Ok(kad::GetClosestPeersOk { key: _, peers }) => { 360 | if !peers.is_empty() { 361 | debug!("Query finished with closest peers: {:#?}", peers); 362 | for peer in peers { 363 | debug!("Got peer {:?}", peer); 364 | } 365 | } else { 366 | error!("Query finished with no closest peers.") 367 | } 368 | } 369 | Err(kad::GetClosestPeersError::Timeout { peers, .. }) => { 370 | if !peers.is_empty() { 371 | error!("Query timed out with closest peers: {:#?}", peers); 372 | for peer in peers { 373 | debug!("Got peer {:?}", peer); 374 | } 375 | } else { 376 | error!("Query timed out with no closest peers."); 377 | } 378 | } 379 | }; 380 | } 381 | _ => {} 382 | } 383 | None 384 | } 385 | SwarmEvent::NewListenAddr { ref address, .. } => { 386 | info!("Local node is listening on {address}"); 387 | let non_local_addr_count = current_listen_addresses 388 | .iter() 389 | .filter(|&addr| !is_multiaddr_local(addr)) 390 | .count(); 391 | current_listen_addresses.insert(address.clone()); 392 | let mut peers_to_push: Vec = Vec::new(); 393 | if let Some(boot_node_clone) = boot_node.as_ref() { 394 | peers_to_push.push(boot_node_clone.peer_id.clone()); 395 | } 396 | swarm.behaviour_mut().identify.push(peers_to_push.clone()); 397 | let connected_peers = swarm.connected_peers(); 398 | let connected_peers_count = connected_peers.count(); 399 | debug!("Connected to {connected_peers_count} peers"); 400 | if let None = boot_node.as_ref() { 401 | info!("No boot node specified. Waiting for connection."); 402 | } else if connected_peers_count == 0 || non_local_addr_count == 0 { 403 | debug!("No connected peers or recovered from no network, we need to manually re-dial to the boot node"); 404 | if let Some(real_boot_node) = boot_node.as_ref() { 405 | let _ = swarm.dial(real_boot_node.address.clone()); 406 | } 407 | } 408 | let _ = swarm.behaviour_mut().kademlia.bootstrap(); 409 | None 410 | } 411 | SwarmEvent::ExpiredListenAddr { ref address, .. } => { 412 | warn!("Local node no longer listening on {address}"); 413 | current_listen_addresses.remove(address); 414 | let non_local_addr_count = current_listen_addresses 415 | .iter() 416 | .filter(|&addr| !is_multiaddr_local(addr)) 417 | .count(); 418 | let mut peers_to_push: Vec = Vec::new(); 419 | if let Some(boot_node_clone) = boot_node.as_ref() { 420 | peers_to_push.push(boot_node_clone.peer_id.clone()); 421 | } 422 | swarm.behaviour_mut().identify.push(peers_to_push.clone()); 423 | if non_local_addr_count > 0 { 424 | if let Some(real_boot_node) = boot_node.as_ref() { 425 | // Because when main address is teared down, the network usually needs some time to recover 426 | // We send it to the retry queue directly 427 | let retry_task = match failing_connections.get(&real_boot_node) { 428 | Some(task) => task.clone(), 429 | None => ConnectionRetryTask { 430 | target: real_boot_node.clone(), 431 | retry_count: 0, 432 | } 433 | }; 434 | failing_connections.insert(real_boot_node.clone(), retry_task.clone()); 435 | let _ = retry_queue_tx.send(retry_task); 436 | } 437 | } 438 | None 439 | } 440 | SwarmEvent::ConnectionEstablished { 441 | ref peer_id, 442 | ref endpoint, 443 | .. 444 | } => { 445 | // We only care about IP and protocol port, ignoring the p2p suffix 446 | let real_address = get_non_p2p_multiaddr(endpoint.get_remote_address().clone()); 447 | let cache = PeerEndpointCache { 448 | peer_id: peer_id.clone(), 449 | address: real_address.clone(), 450 | }; 451 | debug!("Adding endpoint {real_address} to cache"); 452 | if !unique_endpoints.insert(cache.clone()) { 453 | // The item is already present in the set (duplicate) 454 | debug!("endpoint {real_address} already in cache, reordering"); 455 | endpoint_cache.retain(|existing_item| existing_item != &cache); 456 | } else { 457 | info!("Connected to peer {}", peer_id); 458 | } 459 | failing_connections.retain(|c, _| c.peer_id != *peer_id); 460 | endpoint_cache.push_front(cache); 461 | None 462 | } 463 | SwarmEvent::ConnectionClosed { ref peer_id, ref cause,ref endpoint, ref num_established, .. } => { 464 | if *num_established == 0 { 465 | warn!("Peer {} has disconnected", peer_id); 466 | // When the last connection has closed, we should drop that peer from our cache. 467 | // Ideally, all entries related with the peer should have already been removed. 468 | // However, some edge cases that does not correctly trigger the event handlers do occur in rare cases. 469 | // Remove these entries explicitly if that happens. 470 | unique_endpoints.retain(|x| x.peer_id != *peer_id); 471 | endpoint_cache.retain(|x| x.peer_id != *peer_id); 472 | } 473 | if let Some(connection_error) = cause.as_ref().clone() { 474 | unique_endpoints.retain(|x| x.address != *endpoint.get_remote_address()); 475 | endpoint_cache.retain(|x| x.address != *endpoint.get_remote_address()); 476 | match connection_error { 477 | ConnectionError::IO(_io_error) => { 478 | // Handle IO error 479 | // An IO error usually means a network problem occurred on local side, and we will try to re-connect. 480 | let addr = endpoint.get_remote_address(); 481 | if endpoint.is_dialer() { 482 | let failed_connection = PeerEndpointCache { 483 | address: addr.clone(), 484 | peer_id: peer_id.clone(), 485 | }; 486 | let retry_task = match failing_connections.get(&failed_connection) { 487 | Some(task) => task.clone(), 488 | None => ConnectionRetryTask { 489 | target: failed_connection.clone(), 490 | retry_count: 0, 491 | } 492 | }; 493 | failing_connections.insert(failed_connection, retry_task.clone()); 494 | let _ = retry_queue_tx.send(retry_task); 495 | } 496 | } 497 | _ => {} 498 | } 499 | } 500 | None 501 | } 502 | SwarmEvent::OutgoingConnectionError { 503 | ref peer_id, 504 | ref error, 505 | connection_id, 506 | } => { 507 | debug!("OutgoingConnectionError to {peer_id:?} on {connection_id:?} - {error:?}"); 508 | // We need to decide if this was a critical error and the peer should be removed from the routing table. 509 | // For a peer that has successfully connected but then disconnected, the SwarmEvent::ConnectionClosed handler handles that. 510 | // If the error goes here, it usually means we cannot connect to that peer with given address in the first place. 511 | let should_clean_peer = match error { 512 | swarm::DialError::Transport(errors) => { 513 | // Most of the transport error comes from local end and the remote peer should not be removed. 514 | // Even if it is really the remote end, it is hard to tell because if we have multiple 515 | // IP addresses and not all of them is able to connect to the remote endpoint, we may still 516 | // have other IP address that is able to connect to that peer. 517 | // To mitigate that, only remove that specific endpoint instead of everything about that peer. 518 | debug!("Dial errors len : {:?}", errors.len()); 519 | let mut non_recoverable = false; 520 | for (addr, err) in errors { 521 | debug!("OutgoingTransport error : {err:?}"); 522 | match err { 523 | libp2p::TransportError::MultiaddrNotSupported(addr) => { 524 | error!("Multiaddr not supported : {addr:?}"); 525 | // If we can't dial a peer on a given address, we should remove it from the routing table 526 | // Currently we should not have such problem in production as all nodes using selected Multiaddr 527 | // This could occur during development, added for sanity. 528 | non_recoverable = true 529 | } 530 | libp2p::TransportError::Other(err) => { 531 | let should_hold_and_retry = ["NetworkUnreachable", "Timeout"]; 532 | if let Some(inner) = err.get_ref() { 533 | let error_msg = format!("{inner:?}"); 534 | debug!("Problematic error encountered: {inner:?}"); 535 | if let Some(peer) = peer_id { 536 | let failed_connection = PeerEndpointCache { 537 | address: addr.clone(), 538 | peer_id: peer.clone(), 539 | }; 540 | // This is not the best way to match an Error, but the Error we get here is very complicated, like: 541 | // `Other(Custom { kind: Other, error: Timeout })` 542 | // `Other(Left(Left(Os { code: 51, kind: NetworkUnreachable, message: "Network is unreachable" })` 543 | // `Other(Left(Left(Os { code: 61, kind: ConnectionRefused, message: "Connection refused" })` 544 | // Makes appropriate matching very hard if we want to match a specific type of error. 545 | if should_hold_and_retry.iter().any(|err| error_msg.contains(err)) { 546 | let retry_task = match failing_connections.get(&failed_connection) { 547 | Some(task) => task.clone(), 548 | None => ConnectionRetryTask { 549 | target: failed_connection.clone(), 550 | retry_count: 0, 551 | } 552 | }; 553 | failing_connections.insert(failed_connection, retry_task.clone()); 554 | let _ = retry_queue_tx.send(retry_task); 555 | } else { 556 | // If we are not retrying, we should do some cleanup at this endpoint. 557 | unique_endpoints.retain(|endpoint| endpoint.address != *addr); 558 | endpoint_cache.retain(|endpoint| endpoint.address != *addr); 559 | failing_connections.remove(&failed_connection); 560 | swarm.behaviour_mut().kademlia.remove_address(peer, addr); 561 | } 562 | } 563 | }; 564 | } 565 | } 566 | } 567 | non_recoverable 568 | } 569 | swarm::DialError::NoAddresses => { 570 | // We cannot dial peers without addresses 571 | error!("OutgoingConnectionError: No address provided"); 572 | true 573 | } 574 | swarm::DialError::Aborted => { 575 | error!("OutgoingConnectionError: Aborted"); 576 | false 577 | } 578 | swarm::DialError::DialPeerConditionFalse(_) => { 579 | error!("OutgoingConnectionError: DialPeerConditionFalse"); 580 | false 581 | } 582 | swarm::DialError::LocalPeerId { .. } => { 583 | error!("OutgoingConnectionError: LocalPeerId: We are dialing ourselves"); 584 | true 585 | } 586 | swarm::DialError::WrongPeerId { obtained, address } => { 587 | error!("OutgoingConnectionError: WrongPeerId: obtained: {obtained:?}, address: {address:?}"); 588 | true 589 | } 590 | swarm::DialError::Denied { cause } => { 591 | error!("OutgoingConnectionError: Denied: {cause:?}"); 592 | true 593 | } 594 | }; 595 | 596 | if should_clean_peer { 597 | if let Some(dead_peer) = peer_id 598 | { 599 | warn!("Cleaning out dead peer {dead_peer:?}"); 600 | unique_endpoints.retain(|endpoint| endpoint.peer_id != *dead_peer); 601 | endpoint_cache.retain(|endpoint| endpoint.peer_id != *dead_peer); 602 | swarm.behaviour_mut().kademlia.remove_peer(dead_peer); 603 | } 604 | } 605 | None 606 | } 607 | SwarmEvent::Behaviour(P2pClipboardBehaviourEvent::Mdns(mdns::Event::Discovered(list))) => { 608 | for (_peer_id, addr) in list { 609 | debug!("mDNS discovered a new peer: {_peer_id}"); 610 | let _ = swarm.dial(addr.clone()); 611 | } 612 | None 613 | }, 614 | SwarmEvent::Behaviour(P2pClipboardBehaviourEvent::Mdns(mdns::Event::Expired(list))) => { 615 | for (peer_id, addr) in list { 616 | debug!("mDNS expired a peer: {peer_id}"); 617 | // For most time this address should already be removed. 618 | swarm.behaviour_mut().kademlia.remove_address(&peer_id, &addr); 619 | } 620 | None 621 | }, 622 | _ => {None} 623 | }, 624 | Some(failing_connection) = retry_callback_queue_rx.recv() => { 625 | if let Some(retry_task) = failing_connections.get_mut(&failing_connection) { 626 | // Perform some check to ensure doing a connection retry is reasonable 627 | let is_network_ok = { 628 | let non_local_listener_count = current_listen_addresses 629 | .iter() 630 | .filter(|&addr| !is_multiaddr_local(addr)) 631 | .count(); 632 | if swarm.connected_peers().count() > 0 { 633 | // If we already have other peers connected, we have at least one working network interface 634 | true 635 | } else if non_local_listener_count > 0 { 636 | // If we have a non-local listening address, let's hope that address will work. 637 | let link_local_listener_count = current_listen_addresses 638 | .iter() 639 | .filter(|&addr| is_multiaddr_link_local(addr)) 640 | .count(); 641 | // Special cases for link-local addresses. 642 | // Some OS services will use tun interfaces with link-local addresses which may confuses us. 643 | // Users could also have interfaces not configured. 644 | if is_multiaddr_link_local(&retry_task.target.address) { 645 | if link_local_listener_count == 0 { 646 | debug!("Connecting to link-local address {}, but we don't have any link-local addresses.", retry_task.target.address); 647 | false 648 | } else { 649 | true 650 | } 651 | } else { 652 | if link_local_listener_count < non_local_listener_count { 653 | true 654 | } else { 655 | debug!("Connecting to address {}, but all we have are link-local addresses.", retry_task.target.address); 656 | false 657 | } 658 | } 659 | } else { 660 | false 661 | } 662 | }; 663 | let is_task_ok = retry_task.retry_count <= 3; 664 | let already_connected = swarm.is_connected(&retry_task.target.peer_id); 665 | if !is_network_ok { 666 | debug!("We don't have working network connections yet, waiting."); 667 | let _ = retry_queue_tx.send(retry_task.clone()); 668 | } else if !is_task_ok { 669 | error!("Connect to {} with {} failed too many times, give up.", retry_task.target.peer_id, retry_task.target.address); 670 | failing_connections.remove(&failing_connection); 671 | } else if already_connected { 672 | debug!("Already connected to {}, stop retrying.", retry_task.target.peer_id); 673 | failing_connections.remove(&failing_connection); 674 | } else { 675 | retry_task.retry_count += 1; 676 | let _ = swarm.dial(retry_task.target.address.clone()); 677 | } 678 | } 679 | None 680 | }, 681 | _ = &mut sleep => { 682 | debug!("Long idle detected, doing periodic jobs"); 683 | let stale_peers: Vec<_> = swarm.behaviour_mut().gossipsub.all_peers() 684 | .filter(|(_, topics)| topics.is_empty()) 685 | .map(|(peer, _)| peer.clone()) 686 | .collect(); 687 | for peer in stale_peers { 688 | // This is a strange upstream bug. Sometimes a peer may appear connected but without any topic subscriptions. 689 | // If this happens we want to drop the connection and wait for the remote peer to reconnect later. 690 | let _ = &swarm.disconnect_peer_id(peer); 691 | } 692 | if let Some(boot) = boot_node.as_ref() { 693 | // We started with a boot node, so we want to make sure that we do have at least one peer. 694 | // Although the boot node may be offline, we want to connect to it if we don't have any other peers, in case it is started later. 695 | let all_peers = swarm.connected_peers(); 696 | let should_redial_boot_node = all_peers.count() < 1; 697 | if should_redial_boot_node { 698 | let _ = swarm.dial(boot.address.clone()); 699 | } 700 | } 701 | // Look up ourselves to improve awareness in the network. 702 | let self_id = *swarm.local_peer_id(); 703 | swarm.behaviour_mut().kademlia.get_closest_peers(self_id); 704 | None 705 | } 706 | } 707 | }; 708 | let d = SystemTime::now() 709 | .duration_since(t) 710 | .unwrap_or_else(|_| Duration::from_secs(0)); 711 | t = SystemTime::now(); 712 | if d > Duration::from_secs(60) { 713 | // We should already update the timer after each loop completes, and we have a 30-second timeout for periodic tasks. 714 | // If the duration of this loop execution is too long (2 * timeout), our execution may be suspended midway. 715 | // A common reason for such suspension is the host OS entering a power-saving energy state. 716 | // We need to break out of the loop and restart swarm since most of our underlying connections will break. 717 | // The easiest way to recover is to restart. 718 | warn!("Handler completed longer than expected, restarting swarm"); 719 | return; 720 | } 721 | if let Some((topic, line)) = to_publish { 722 | if let Err(err) = swarm 723 | .behaviour_mut() 724 | .gossipsub 725 | .publish(topic.clone(), line.as_bytes()) 726 | { 727 | match err { 728 | PublishError::Duplicate => {} 729 | _ => { 730 | error!("Error publishing message: {}", err); 731 | } 732 | } 733 | } 734 | } 735 | } 736 | } 737 | 738 | fn create_gossipsub_behavior(id_keys: Keypair) -> Behaviour { 739 | // Hash the message and use it as ID 740 | // Duplicated message will be ignored and not sent because they will have same hash 741 | let message_id_fn = |message: &gossipsub::Message| { 742 | let mut s = DefaultHasher::new(); 743 | message.data.hash(&mut s); 744 | MessageId::from(s.finish().to_string()) 745 | }; 746 | 747 | let gossipsub_config = gossipsub::ConfigBuilder::default() 748 | .heartbeat_interval(Duration::from_secs(10)) 749 | .validation_mode(ValidationMode::Strict) 750 | .message_id_fn(message_id_fn) 751 | .do_px() 752 | .build() 753 | .expect("Valid config"); 754 | Behaviour::new_with_transform( 755 | MessageAuthenticity::Signed(id_keys), 756 | gossipsub_config, 757 | CompressionTransform, 758 | ) 759 | .expect("Correct configuration") 760 | } 761 | 762 | fn create_kademlia_behavior(local_peer_id: PeerId) -> kad::Behaviour { 763 | let mut cfg = kad::Config::default(); 764 | cfg.set_query_timeout(Duration::from_secs(5 * 60)); 765 | let store = MemoryStore::new(local_peer_id); 766 | kad::Behaviour::with_config(local_peer_id, store, cfg) 767 | } 768 | 769 | fn create_identify_behavior(local_public_key: PublicKey) -> identify::Behaviour { 770 | identify::Behaviour::new(identify::Config::new( 771 | "/p2pclipboard/1.0.0".into(), 772 | local_public_key, 773 | )) 774 | } 775 | 776 | fn create_mdns_behavior( 777 | local_peer_id: PeerId, 778 | pre_shared_key: Option, 779 | disable_mdns: bool, 780 | ) -> mdns::Behaviour { 781 | let mut mdns_config = mdns::Config::default(); 782 | let fingerprint = match pre_shared_key { 783 | Some(psk) => { 784 | let mut seed_key_bytes = 785 | SigningKey::from_pkcs8_pem(std::str::from_utf8(&ID_SEED).unwrap()) 786 | .unwrap() 787 | .to_bytes(); 788 | let seed_key = Keypair::ed25519_from_bytes(&mut seed_key_bytes).unwrap(); 789 | Some(Vec::from( 790 | seed_key 791 | .derive_secret(psk.as_ref()) 792 | .expect("can derive secret for ed25519"), 793 | )) 794 | } 795 | None => None, 796 | }; 797 | mdns_config.service_fingerprint = fingerprint; 798 | mdns_config.disabled = disable_mdns; 799 | mdns::tokio::Behaviour::new(mdns_config, local_peer_id).expect("mdns correct") 800 | } 801 | 802 | fn parse_ipv4_with_port(input: Option) -> Result<(Ipv4Addr, u16), &'static str> { 803 | if let Some(input_str) = input { 804 | let parts: Vec<&str> = input_str.split(':').collect(); 805 | 806 | if parts.len() == 2 { 807 | let socket_address: Result = input_str.parse(); 808 | match socket_address { 809 | Ok(socket) => Ok((*socket.ip(), socket.port())), 810 | Err(_) => Err("Invalid input format"), 811 | } 812 | } else if parts.len() == 1 { 813 | let ip_addr: Result = parts[0].parse(); 814 | match ip_addr { 815 | Ok(ip) => Ok((ip, 0)), 816 | _ => Err("Invalid IP address or port number"), 817 | } 818 | } else { 819 | Err("Invalid input format") 820 | } 821 | } else { 822 | Err("Input is None") 823 | } 824 | } 825 | 826 | fn get_non_p2p_multiaddr(mut origin_addr: Multiaddr) -> Multiaddr { 827 | while origin_addr.iter().count() > 2 { 828 | let _ = origin_addr.pop(); 829 | } 830 | return origin_addr; 831 | } 832 | 833 | fn is_multiaddr_link_local(addr: &Multiaddr) -> bool { 834 | if let Protocol::Ip4(ip) = addr.iter().collect::>()[0] { 835 | return ip.is_link_local(); 836 | } 837 | return false; 838 | } 839 | 840 | fn is_multiaddr_local(addr: &Multiaddr) -> bool { 841 | addr.iter().collect::>()[0] == Protocol::Ip4(Ipv4Addr::new(127, 0, 0, 1)) 842 | } 843 | --------------------------------------------------------------------------------