Skip to content

Commit

Permalink
Add IPv6 support for net::Address (#827)
Browse files Browse the repository at this point in the history
* Enable IPv6 parsing of net::Address

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Add CHANGELOG entry

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Update net::Address docs to reflect changes

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Reorder items in CHANGELOG

Signed-off-by: Thane Thomson <connect@thanethomson.com>
  • Loading branch information
thanethomson authored Apr 6, 2021
1 parent 1c5c850 commit d1c8877
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 62 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
* `[tendermint-rpc]` A `tendermint-rpc` CLI has been added to simplify
interaction with RPC endpoints from the command line ([#820])

### IMPROVEMENTS

* `[tendermint]` IPv6 support has been added for `net::Address` ([#5])

### BUG FIXES

* `[tendermint-light-client]` Due to the RPC client's `validators` method
Expand All @@ -57,6 +61,7 @@
* `[tendermint-rpc]` Fix intermittent deserialization failures of the consensus
state response ([#836])

[#5]: https://github.com/informalsystems/tendermint-rs/issues/5
[#794]: https://github.com/informalsystems/tendermint-rs/pull/794
[#812]: https://github.com/informalsystems/tendermint-rs/pull/812
[#820]: https://github.com/informalsystems/tendermint-rs/pull/820
Expand Down
1 change: 1 addition & 0 deletions tendermint/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ subtle-encoding = { version = "0.5", features = ["bech32-preview"] }
thiserror = "1"
tendermint-proto = { version = "0.18.1", path = "../proto" }
toml = { version = "0.5" }
url = { version = "2.2" }
zeroize = { version = "1.1", features = ["zeroize_derive"] }

k256 = { version = "0.7", optional = true, features = ["ecdsa"] }
Expand Down
162 changes: 100 additions & 62 deletions tendermint/src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ use crate::{
error::{Error, Kind},
node,
};
use anomaly::{fail, format_err};

use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
use std::{
fmt::{self, Display},
path::PathBuf,
str::{self, FromStr},
};
use url::Url;

/// URI prefix for TCP connections
pub const TCP_PREFIX: &str = "tcp://";
Expand All @@ -20,6 +20,12 @@ pub const TCP_PREFIX: &str = "tcp://";
pub const UNIX_PREFIX: &str = "unix://";

/// Remote address (TCP or UNIX socket)
///
/// For TCP-based addresses, this supports both IPv4 and IPv6 addresses and
/// hostnames.
///
/// If the scheme is not supplied (i.e. `tcp://` or `unix://`) when parsing
/// from a string, it is assumed to be a TCP address.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Address {
/// TCP connections
Expand Down Expand Up @@ -61,69 +67,37 @@ impl FromStr for Address {
type Err = Error;

fn from_str(addr: &str) -> Result<Self, Error> {
if addr.starts_with(TCP_PREFIX) {
Self::parse_tcp_addr(&addr.strip_prefix(TCP_PREFIX).unwrap())
} else if addr.starts_with(UNIX_PREFIX) {
Ok(Address::Unix {
path: PathBuf::from(&addr.strip_prefix(UNIX_PREFIX).unwrap()),
})
} else if addr.contains("://") {
// The only supported URI prefixes are `tcp://` and `unix://`
fail!(Kind::Parse, "invalid address prefix: {:?}", addr)
let prefixed_addr = if addr.contains("://") {
addr.to_owned()
} else {
// If the address has no URI prefix, assume TCP
Self::parse_tcp_addr(addr)
}
}
}

impl Address {
/// Parse a TCP address (without a `tcp://` prefix).
///
/// This is used internally by `Address::from_str`.
fn parse_tcp_addr(addr: &str) -> Result<Self, Error> {
// TODO(tarcieri): use the `uri` (or other) crate for this
let authority_parts = addr.split('@').collect::<Vec<_>>();

let (peer_id, authority) = match authority_parts.len() {
1 => (None, authority_parts[0]),
2 => (Some(authority_parts[0].parse()?), authority_parts[1]),
_ => fail!(
Kind::Parse,
"invalid {} address (bad authority): {}",
TCP_PREFIX,
addr
),
// If the address has no scheme, assume it's TCP
format!("{}{}", TCP_PREFIX, addr)
};

let host_and_port: Vec<&str> = authority.split(':').collect();

if host_and_port.len() != 2 {
fail!(
Kind::Parse,
"invalid {} address (missing port): {}",
TCP_PREFIX,
addr
);
let url = Url::parse(&prefixed_addr).map_err(|e| Kind::Parse.context(e))?;
match url.scheme() {
"tcp" => Ok(Self::Tcp {
peer_id: if !url.username().is_empty() {
Some(url.username().parse()?)
} else {
None
},
host: url
.host_str()
.ok_or_else(|| {
Kind::Parse.context(format!("invalid TCP address (missing host): {}", addr))
})?
.to_owned(),
port: url.port().ok_or_else(|| {
Kind::Parse.context(format!("invalid TCP address (missing port): {}", addr))
})?,
}),
"unix" => Ok(Self::Unix {
path: PathBuf::from(url.path()),
}),
_ => Err(Kind::Parse
.context(format!("invalid address scheme: {:?}", addr))
.into()),
}

// TODO(tarcieri): default for missing hostname?
let host = host_and_port[0].to_owned();

let port = host_and_port[1].parse::<u16>().map_err(|_| {
format_err!(
Kind::Parse,
"invalid {} address (bad port): {}",
TCP_PREFIX,
addr
)
})?;

Ok(Address::Tcp {
peer_id,
host,
port,
})
}
}

Expand All @@ -138,9 +112,12 @@ mod tests {
use super::*;
use crate::node;

/// Example TCP node address
const EXAMPLE_TCP_ADDR: &str =
"tcp://abd636b766dcefb5322d8ca40011ec2cb35efbc2@35.192.61.41:26656";
const EXAMPLE_TCP_ADDR_WITHOUT_ID: &str = "tcp://35.192.61.41:26656";
const EXAMPLE_UNIX_ADDR: &str = "unix:///tmp/node.sock";
const EXAMPLE_TCP_IPV6_ADDR: &str =
"tcp://abd636b766dcefb5322d8ca40011ec2cb35efbc2@[2001:0000:3238:DFE1:0063:0000:0000:FEFB]:26656";

#[test]
fn parse_tcp_addr() {
Expand All @@ -166,4 +143,65 @@ mod tests {
}
}
}

#[test]
fn parse_tcp_addr_without_id() {
let addr = EXAMPLE_TCP_ADDR_WITHOUT_ID.parse::<Address>().unwrap();
let addr_without_prefix = EXAMPLE_TCP_ADDR_WITHOUT_ID[TCP_PREFIX.len()..]
.parse::<Address>()
.unwrap();
for addr in &[addr, addr_without_prefix] {
match addr {
Address::Tcp {
peer_id,
host,
port,
} => {
assert!(peer_id.is_none());
assert_eq!(host, "35.192.61.41");
assert_eq!(*port, 26656);
}
other => panic!("unexpected address type: {:?}", other),
}
}
}

#[test]
fn parse_unix_addr() {
let addr = EXAMPLE_UNIX_ADDR.parse::<Address>().unwrap();
match addr {
Address::Unix { path } => {
assert_eq!(path.to_str().unwrap(), "/tmp/node.sock");
}
other => panic!("unexpected address type: {:?}", other),
}
}

#[test]
fn parse_tcp_ipv6_addr() {
let addr = EXAMPLE_TCP_IPV6_ADDR.parse::<Address>().unwrap();
let addr_without_prefix = EXAMPLE_TCP_IPV6_ADDR[TCP_PREFIX.len()..]
.parse::<Address>()
.unwrap();
for addr in &[addr, addr_without_prefix] {
match addr {
Address::Tcp {
peer_id,
host,
port,
} => {
assert_eq!(
peer_id.unwrap(),
"abd636b766dcefb5322d8ca40011ec2cb35efbc2"
.parse::<node::Id>()
.unwrap()
);
// The parser URL strips the leading zeroes and converts to lowercase hex
assert_eq!(host, "[2001:0:3238:dfe1:63::fefb]");
assert_eq!(*port, 26656);
}
other => panic!("unexpected address type: {:?}", other),
}
}
}
}

0 comments on commit d1c8877

Please sign in to comment.