diff --git a/Cargo.lock b/Cargo.lock index d6eaf66a..038cd099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -488,6 +488,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexopt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8" + [[package]] name = "libc" version = "0.2.133" @@ -808,6 +814,7 @@ dependencies = [ "colored", "crossbeam-channel", "fastrand", + "lexopt", "log", "nakamoto-net", "nakamoto-net-poll", diff --git a/radicle-node/Cargo.toml b/radicle-node/Cargo.toml index 6c5f45d7..8226cf2d 100644 --- a/radicle-node/Cargo.toml +++ b/radicle-node/Cargo.toml @@ -13,6 +13,7 @@ chrono = { version = "0.4.0" } colored = { version = "1.9.0" } crossbeam-channel = { version = "0.5.6" } fastrand = { version = "1.8.0" } +lexopt = { version = "0.2.1" } log = { version = "0.4.17", features = ["std"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } diff --git a/radicle-node/src/lib.rs b/radicle-node/src/lib.rs index f0488a8a..21045cca 100644 --- a/radicle-node/src/lib.rs +++ b/radicle-node/src/lib.rs @@ -22,6 +22,7 @@ pub mod prelude { pub use crate::hash::Digest; pub use crate::identity::{Did, Id}; pub use crate::service::filter::Filter; + pub use crate::service::message::Address; pub use crate::service::{DisconnectReason, Envelope, Event, Message, Network, NodeId}; pub use crate::storage::refs::Refs; pub use crate::storage::WriteStorage; diff --git a/radicle-node/src/main.rs b/radicle-node/src/main.rs index 64797372..9e182f1e 100644 --- a/radicle-node/src/main.rs +++ b/radicle-node/src/main.rs @@ -1,15 +1,69 @@ use std::thread; -use std::{env, net}; +use std::{env, net, process}; -use radicle_node::{client, control}; +use radicle_node::prelude::Address; +use radicle_node::{client, control, git, service}; type Reactor = nakamoto_net_poll::Reactor; +#[derive(Debug)] +struct Options { + connect: Vec
, + listen: Vec, + git_url: git::Url, +} + +impl Options { + fn from_env() -> Result { + use lexopt::prelude::*; + let mut parser = lexopt::Parser::from_env(); + let mut connect = Vec::new(); + let mut listen = Vec::new(); + let mut git_url = None; + + while let Some(arg) = parser.next()? { + match arg { + Long("connect") => { + let addr = parser.value()?.parse()?; + connect.push(addr); + } + Long("listen") => { + let addr = parser.value()?.parse()?; + listen.push(addr); + } + Long("git-url") => { + let url = git::Url::from_bytes(parser.value()?.into_string()?.as_bytes()) + .map_err(|e| format!("invalid URL: {}", e))?; + git_url = Some(url); + } + Long("help") => { + println!("usage: radicle-node [--connect ].."); + process::exit(0); + } + _ => return Err(arg.unexpected()), + } + } + Ok(Self { + connect, + listen, + git_url: git_url.ok_or("a Git URL must be specified with `--git-url`")?, + }) + } +} + fn main() -> anyhow::Result<()> { + let options = Options::from_env()?; let profile = radicle::Profile::load()?; let client = client::Client::::new(profile)?; let handle = client.handle(); - let config = client::Config::default(); + let config = client::Config { + service: service::Config { + connect: options.connect, + git_url: options.git_url, + ..service::Config::default() + }, + listen: options.listen, + }; let socket = env::var("RAD_SOCKET").unwrap_or_else(|_| control::DEFAULT_SOCKET_NAME.to_owned()); let t1 = thread::spawn(move || control::listen(socket, handle)); diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index b6a42cf5..4d57f5b8 100644 --- a/radicle-node/src/service.rs +++ b/radicle-node/src/service.rs @@ -28,6 +28,7 @@ use crate::git; use crate::git::Url; use crate::identity::{Doc, Id}; use crate::service::config::ProjectTracking; +use crate::service::message::Address; use crate::service::message::{NodeAnnouncement, RefsAnnouncement}; use crate::service::peer::{Session, SessionError, SessionState}; use crate::storage; @@ -464,8 +465,9 @@ where } pub fn attempted(&mut self, addr: &std::net::SocketAddr) { + let address = Address::from(*addr); let ip = addr.ip(); - let persistent = self.config.is_persistent(addr); + let persistent = self.config.is_persistent(&address); let peer = self .sessions .entry(ip) @@ -481,6 +483,7 @@ where link: Link, ) { let ip = addr.ip(); + let address = addr.into(); debug!("Connected to {} ({:?})", ip, link); @@ -505,7 +508,7 @@ where } else { self.sessions.insert( ip, - Session::new(addr, Link::Inbound, self.config.is_persistent(&addr)), + Session::new(addr, Link::Inbound, self.config.is_persistent(&address)), ); } } @@ -516,6 +519,7 @@ where reason: nakamoto::DisconnectReason, ) { let since = self.local_time(); + let address = Address::from(*addr); let ip = addr.ip(); debug!("Disconnected from {} ({})", ip, reason); @@ -524,7 +528,7 @@ where peer.state = SessionState::Disconnected { since }; // Attempt to re-connect to persistent peers. - if self.config.is_persistent(addr) && peer.attempts() < MAX_CONNECTION_ATTEMPTS { + if self.config.is_persistent(&address) && peer.attempts() < MAX_CONNECTION_ATTEMPTS { if reason.is_dial_err() { return; } diff --git a/radicle-node/src/service/config.rs b/radicle-node/src/service/config.rs index f5b53e94..b7604224 100644 --- a/radicle-node/src/service/config.rs +++ b/radicle-node/src/service/config.rs @@ -1,5 +1,3 @@ -use std::net; - use crate::collections::HashSet; use crate::git; use crate::git::Url; @@ -65,7 +63,7 @@ pub enum RemoteTracking { pub struct Config { /// Peers to connect to on startup. /// Connections to these peers will be maintained. - pub connect: Vec, + pub connect: Vec
, /// Peer-to-peer network. pub network: Network, /// Project tracking policy. @@ -99,7 +97,7 @@ impl Default for Config { } impl Config { - pub fn is_persistent(&self, addr: &net::SocketAddr) -> bool { + pub fn is_persistent(&self, addr: &Address) -> bool { self.connect.contains(addr) } diff --git a/radicle-node/src/service/message.rs b/radicle-node/src/service/message.rs index ca1b236c..97b5f4de 100644 --- a/radicle-node/src/service/message.rs +++ b/radicle-node/src/service/message.rs @@ -1,5 +1,8 @@ +use std::str::FromStr; use std::{fmt, io, net}; +use thiserror::Error; + use crate::crypto; use crate::git; use crate::identity::Id; @@ -24,6 +27,12 @@ pub type NodeFeatures = [u8; 32]; // TODO: We should check the length and charset when deserializing. pub struct Hostname(String); +impl fmt::Display for Hostname { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + /// Peer public protocol address. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Address { @@ -59,6 +68,52 @@ impl From for Address { } } +#[derive(Debug, Error)] +pub enum AddressParseError { + #[error("unsupported address type `{0}`")] + Unsupported(String), +} + +impl FromStr for Address { + type Err = AddressParseError; + + fn from_str(s: &str) -> Result { + if let Ok(addr) = net::SocketAddr::from_str(s) { + match addr.ip() { + net::IpAddr::V4(ip) => Ok(Self::Ipv4 { + ip, + port: addr.port(), + }), + net::IpAddr::V6(ip) => Ok(Self::Ipv6 { + ip, + port: addr.port(), + }), + } + } else { + Err(Self::Err::Unsupported(s.to_owned())) + } + } +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Ipv4 { ip, port } => { + write!(f, "{}:{}", ip, port) + } + Self::Ipv6 { ip, port } => { + write!(f, "{}:{}", ip, port) + } + Self::Hostname { host, port } => { + write!(f, "{}:{}", host, port) + } + Self::Onion { key, port, .. } => { + write!(f, "{}:{}", key, port) + } + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Subscribe { /// Subscribe to events matching this filter. diff --git a/radicle-node/src/service/reactor.rs b/radicle-node/src/service/reactor.rs index 6c9c94a4..7274df35 100644 --- a/radicle-node/src/service/reactor.rs +++ b/radicle-node/src/service/reactor.rs @@ -44,9 +44,21 @@ impl Reactor { } /// Connect to a peer. - pub fn connect(&mut self, addr: net::SocketAddr) { + pub fn connect(&mut self, addr: impl Into
) { // TODO: Make sure we don't try to connect more than once to the same address. - self.io.push_back(Io::Connect(addr)); + match addr.into() { + Address::Ipv4 { ip, port } => { + self.io + .push_back(Io::Connect(net::SocketAddr::new(net::IpAddr::V4(ip), port))); + } + Address::Ipv6 { ip, port } => { + self.io + .push_back(Io::Connect(net::SocketAddr::new(net::IpAddr::V6(ip), port))); + } + other => { + log::error!("Unsupported address type `{}`", other); + } + } } /// Disconnect a peer. diff --git a/radicle-node/src/test/peer.rs b/radicle-node/src/test/peer.rs index bb04d78f..e173249c 100644 --- a/radicle-node/src/test/peer.rs +++ b/radicle-node/src/test/peer.rs @@ -125,6 +125,10 @@ where } } + pub fn address(&self) -> Address { + simulator::Peer::addr(self).into() + } + pub fn timestamp(&self) -> Timestamp { self.service.clock().timestamp() } diff --git a/radicle-node/src/test/tests.rs b/radicle-node/src/test/tests.rs index 7ef70a8d..1d1e0b08 100644 --- a/radicle-node/src/test/tests.rs +++ b/radicle-node/src/test/tests.rs @@ -78,7 +78,7 @@ fn test_persistent_peer_connect() { let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty()); let eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty()); let config = Config { - connect: vec![bob.addr(), eve.addr()], + connect: vec![bob.address(), eve.address()], ..Config::default() }; let mut alice = Peer::config( @@ -302,7 +302,7 @@ fn test_persistent_peer_reconnect() { let mut alice = Peer::config( "alice", Config { - connect: vec![bob.addr(), eve.addr()], + connect: vec![bob.address(), eve.address()], ..Config::default() }, [7, 7, 7, 7],