From df8e4e6c88a8bfb6c1ec6b07dcda64093b477cbe Mon Sep 17 00:00:00 2001 From: Defelo Date: Tue, 24 Feb 2026 17:22:58 +0100 Subject: [PATCH] node: Parse IPv6 addresses in square brackets --- CHANGELOG.md | 6 +++++ crates/radicle/src/node.rs | 47 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b68edd5d..91f1866c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This would result in timeouts when commands are run from the `rad` CLI. The `Service` has now learned to return results when an error occurs which will be reported back to the user. +- Parsing addresses involving an IPv6 host failed if they were enclosed in + square brackets, e.g. in `rad node connect z6Mk...@[::1]:8776`. + Also, ambiguous addresses would parse, e.g. `::1:8776` would be + indistinguishable from `[::1]:8776`. Since a port number is always required + along a host when providing an address, IPv6 addresses now always require + brackets to avoid confusion. ## Deprecations diff --git a/crates/radicle/src/node.rs b/crates/radicle/src/node.rs index 2dd62c9c..dcea1777 100644 --- a/crates/radicle/src/node.rs +++ b/crates/radicle/src/node.rs @@ -19,6 +19,8 @@ pub mod timestamp; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use std::io::{BufRead, BufReader}; use std::marker::PhantomData; +use std::net::IpAddr; +use std::net::Ipv6Addr; use std::ops::{ControlFlow, Deref}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -30,7 +32,7 @@ use std::os::unix::net::UnixStream; use uds_windows::UnixStream; use amplify::WrapperMut; -use cyphernet::addr::NetAddr; +use cyphernet::addr::{AddrParseError, NetAddr}; use localtime::{LocalDuration, LocalTime}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -422,7 +424,7 @@ impl TryFrom<&sqlite::Value> for Alias { /// Peer public protocol address. #[derive(Clone, Eq, PartialEq, Debug, Hash, From, Wrapper, WrapperMut, Serialize, Deserialize)] -#[wrapper(Deref, Display, FromStr)] +#[wrapper(Deref, Display)] #[wrapper_mut(DerefMut)] #[cfg_attr( feature = "schemars", @@ -490,6 +492,31 @@ impl Address { } } +impl FromStr for Address { + type Err = AddrParseError; + + fn from_str(s: &str) -> Result { + let (host, port) = s.rsplit_once(':').ok_or(AddrParseError::PortAbsent)?; + + let host = if let Some(host) = host + .strip_prefix('[') + .and_then(|host| host.strip_suffix(']')) + { + HostName::Ip(host.parse::()?.into()) + } else { + // Require IPv6 addresses to always be enclosed in `[` and `]`. + host.parse().and_then(|host| match host { + HostName::Ip(IpAddr::V6(_)) => Err(AddrParseError::UnknownAddressFormat), + host => Ok(host), + })? + }; + + let port = port.parse().map_err(|_| AddrParseError::InvalidPort)?; + + Ok(Self(NetAddr::new(host, port))) + } +} + impl cyphernet::addr::Host for Address { fn requires_proxy(&self) -> bool { self.0.requires_proxy() @@ -1448,6 +1475,22 @@ mod test { assert!(Alias::from_str("cloudhead\n").is_err()); } + #[test] + fn test_address() { + assert!(Address::from_str("127.0.0.1:8776").is_ok()); + assert!(Address::from_str("[::1]:8776").is_ok()); + assert!(Address::from_str("[::ffff:127.0.0.1]:8776").is_ok()); + assert!(Address::from_str("localhost:8776").is_ok()); + + assert!(Address::from_str("").is_err()); + assert!(Address::from_str(":").is_err()); + assert!(Address::from_str("127.0.0.1").is_err()); + assert!(Address::from_str("127.0.0.1:xyz").is_err()); + assert!(Address::from_str("[invalid]:8776").is_err()); + assert!(Address::from_str("[127.0.0.1]:8776").is_err()); + assert!(Address::from_str("::1:8776").is_err()); + } + #[test] fn test_command_result() { #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]