{client,server}: allow hostnames in endpoints (#56)

use new Endpoint type instead of SocketAddr in appropriate places
pull/59/head
Jake McGinty 2021-04-21 00:35:10 +09:00 committed by GitHub
parent e2ea2ddded
commit 0a26bdedce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 138 additions and 31 deletions

2
Cargo.lock generated
View File

@ -1138,6 +1138,7 @@ dependencies = [
"tokio", "tokio",
"toml", "toml",
"ureq", "ureq",
"url",
"warp", "warp",
"wgctrl", "wgctrl",
] ]
@ -1169,6 +1170,7 @@ dependencies = [
"structopt", "structopt",
"toml", "toml",
"ureq", "ureq",
"url",
"wgctrl", "wgctrl",
] ]

View File

@ -283,6 +283,7 @@ fn redeem_invite(
target_conf: PathBuf, target_conf: PathBuf,
) -> Result<(), Error> { ) -> Result<(), Error> {
println!("{} bringing up the interface.", "[*]".dimmed()); println!("{} bringing up the interface.", "[*]".dimmed());
let resolved_endpoint = config.server.external_endpoint.resolve()?;
wg::up( wg::up(
&iface, &iface,
&config.interface.private_key, &config.interface.private_key,
@ -291,7 +292,7 @@ fn redeem_invite(
Some(( Some((
&config.server.public_key, &config.server.public_key,
config.server.internal_endpoint.ip(), config.server.internal_endpoint.ip(),
config.server.external_endpoint, resolved_endpoint,
)), )),
)?; )?;
@ -369,6 +370,7 @@ fn fetch(
} }
println!("{} bringing up the interface.", "[*]".dimmed()); println!("{} bringing up the interface.", "[*]".dimmed());
let resolved_endpoint = config.server.external_endpoint.resolve()?;
wg::up( wg::up(
interface, interface,
&config.interface.private_key, &config.interface.private_key,
@ -377,7 +379,7 @@ fn fetch(
Some(( Some((
&config.server.public_key, &config.server.public_key,
config.server.internal_endpoint.ip(), config.server.internal_endpoint.ip(),
config.server.external_endpoint, resolved_endpoint,
)), )),
)? )?
} }
@ -821,7 +823,7 @@ fn print_peer(our_peer: &Peer, peer: &PeerInfo, short: bool) -> Result<(), Error
&our_peer.public_key[..10].yellow() &our_peer.public_key[..10].yellow()
); );
println!(" {}: {}", "ip".bold(), our_peer.ip); println!(" {}: {}", "ip".bold(), our_peer.ip);
if let Some(endpoint) = our_peer.endpoint { if let Some(ref endpoint) = our_peer.endpoint {
println!(" {}: {}", "endpoint".bold(), endpoint); println!(" {}: {}", "endpoint".bold(), endpoint);
} }
if let Some(last_handshake) = peer.stats.last_handshake_time { if let Some(last_handshake) = peer.stats.last_handshake_time {

View File

@ -34,6 +34,7 @@ subtle = "2"
structopt = "0.3" structopt = "0.3"
thiserror = "1" thiserror = "1"
ureq = { version = "2", default-features = false } ureq = { version = "2", default-features = false }
url = "2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
toml = "0.5" toml = "0.5"
warp = { git = "https://github.com/tonarino/warp", default-features = false } # pending https://github.com/seanmonstar/warp/issues/830 warp = { git = "https://github.com/tonarino/warp", default-features = false } # pending https://github.com/seanmonstar/warp/issues/830

View File

@ -11,7 +11,7 @@ pub fn inject_endpoints(session: &Session, peers: &mut Vec<Peer>) {
for mut peer in peers { for mut peer in peers {
if peer.contents.endpoint.is_none() { if peer.contents.endpoint.is_none() {
if let Some(endpoint) = session.context.endpoints.get(&peer.public_key) { if let Some(endpoint) = session.context.endpoints.get(&peer.public_key) {
peer.contents.endpoint = Some(endpoint.to_owned()); peer.contents.endpoint = Some(endpoint.to_owned().into());
} }
} }
} }

View File

@ -200,7 +200,7 @@ mod tests {
.put_request_from_ip(test::DEVELOPER1_PEER_IP) .put_request_from_ip(test::DEVELOPER1_PEER_IP)
.path("/v1/user/endpoint") .path("/v1/user/endpoint")
.body(serde_json::to_string(&EndpointContents::Set( .body(serde_json::to_string(&EndpointContents::Set(
"1.1.1.1:51820".parse()? "1.1.1.1:51820".parse().unwrap()
))?) ))?)
.reply(&filter) .reply(&filter)
.await .await

View File

@ -95,7 +95,7 @@ impl DatabasePeer {
ip.to_string(), ip.to_string(),
cidr_id, cidr_id,
&public_key, &public_key,
endpoint.map(|endpoint| endpoint.to_string()), endpoint.as_ref().map(|endpoint| endpoint.to_string()),
is_admin, is_admin,
is_disabled, is_disabled,
is_redeemed, is_redeemed,
@ -138,7 +138,10 @@ impl DatabasePeer {
WHERE id = ?5", WHERE id = ?5",
params![ params![
new_contents.name, new_contents.name,
new_contents.endpoint.map(|endpoint| endpoint.to_string()), new_contents
.endpoint
.as_ref()
.map(|endpoint| endpoint.to_string()),
new_contents.is_admin, new_contents.is_admin,
new_contents.is_disabled, new_contents.is_disabled,
self.id, self.id,

View File

@ -5,7 +5,7 @@ use indoc::printdoc;
use rusqlite::{params, Connection}; use rusqlite::{params, Connection};
use shared::{ use shared::{
prompts::{self, hostname_validator}, prompts::{self, hostname_validator},
CidrContents, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS, CidrContents, Endpoint, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
}; };
use wgctrl::KeyPair; use wgctrl::KeyPair;
@ -32,7 +32,7 @@ pub struct InitializeOpts {
/// This server's external endpoint (ex: 100.100.100.100:51820) /// This server's external endpoint (ex: 100.100.100.100:51820)
#[structopt(long, conflicts_with = "auto-external-endpoint")] #[structopt(long, conflicts_with = "auto-external-endpoint")]
pub external_endpoint: Option<SocketAddr>, pub external_endpoint: Option<Endpoint>,
/// Auto-resolve external endpoint /// Auto-resolve external endpoint
#[structopt(long = "auto-external-endpoint")] #[structopt(long = "auto-external-endpoint")]
@ -49,7 +49,7 @@ struct DbInitData {
server_cidr: IpNetwork, server_cidr: IpNetwork,
our_ip: IpAddr, our_ip: IpAddr,
public_key_base64: String, public_key_base64: String,
endpoint: SocketAddr, endpoint: Endpoint,
} }
fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(), Error> { fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(), Error> {
@ -126,7 +126,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
// This probably won't error because of the `hostname_validator` regex. // This probably won't error because of the `hostname_validator` regex.
let name = name.parse()?; let name = name.parse()?;
let endpoint: SocketAddr = if let Some(endpoint) = opts.external_endpoint { let endpoint: Endpoint = if let Some(endpoint) = opts.external_endpoint {
endpoint.clone() endpoint.clone()
} else { } else {
let external_ip: Option<IpAddr> = ureq::get("http://4.icanhazip.com") let external_ip: Option<IpAddr> = ureq::get("http://4.icanhazip.com")
@ -139,7 +139,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
if opts.auto_external_endpoint { if opts.auto_external_endpoint {
let ip = external_ip.ok_or("couldn't get external IP")?; let ip = external_ip.ok_or("couldn't get external IP")?;
(ip, 51820).into() SocketAddr::new(ip, 51820).into()
} else { } else {
prompts::ask_endpoint(external_ip)? prompts::ask_endpoint(external_ip)?
} }

View File

@ -69,7 +69,7 @@ impl Server {
let opts = InitializeOpts { let opts = InitializeOpts {
network_name: Some(interface.clone()), network_name: Some(interface.clone()),
network_cidr: Some(ROOT_CIDR.parse()?), network_cidr: Some(ROOT_CIDR.parse()?),
external_endpoint: Some("155.155.155.155:54321".parse()?), external_endpoint: Some("155.155.155.155:54321".parse().unwrap()),
listen_port: Some(54321), listen_port: Some(54321),
auto_external_endpoint: false, auto_external_endpoint: false,
}; };

View File

@ -17,4 +17,5 @@ serde = { version = "1", features = ["derive"] }
structopt = "0.3" structopt = "0.3"
toml = "0.5" toml = "0.5"
ureq = { version = "2", default-features = false } ureq = { version = "2", default-features = false }
url = "2"
wgctrl = { path = "../wgctrl-rs" } wgctrl = { path = "../wgctrl-rs" }

View File

@ -1,4 +1,4 @@
use crate::{ensure_dirs_exist, Error, IoErrorContext, CLIENT_CONFIG_PATH}; use crate::{ensure_dirs_exist, Endpoint, Error, IoErrorContext, CLIENT_CONFIG_PATH};
use colored::*; use colored::*;
use indoc::writedoc; use indoc::writedoc;
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
@ -46,7 +46,7 @@ pub struct ServerInfo {
pub public_key: String, pub public_key: String,
/// The external internet endpoint to reach the server. /// The external internet endpoint to reach the server.
pub external_endpoint: SocketAddr, pub external_endpoint: Endpoint,
/// An internal endpoint in the WireGuard network that hosts the coordination API. /// An internal endpoint in the WireGuard network that hosts the coordination API.
pub internal_endpoint: SocketAddr, pub internal_endpoint: SocketAddr,

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
interface_config::{InterfaceConfig, InterfaceInfo, ServerInfo}, interface_config::{InterfaceConfig, InterfaceInfo, ServerInfo},
AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, Error, Peer, PeerContents, AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, Endpoint, Error, Peer,
PERSISTENT_KEEPALIVE_INTERVAL_SECS, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
}; };
use colored::*; use colored::*;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select}; use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
@ -299,6 +299,7 @@ pub fn save_peer_invitation(
server: ServerInfo { server: ServerInfo {
external_endpoint: server_peer external_endpoint: server_peer
.endpoint .endpoint
.clone()
.expect("The innernet server should have a WireGuard endpoint"), .expect("The innernet server should have a WireGuard endpoint"),
internal_endpoint: *server_api_addr, internal_endpoint: *server_api_addr,
public_key: server_peer.public_key.clone(), public_key: server_peer.public_key.clone(),
@ -362,7 +363,7 @@ pub fn set_listen_port(
} }
} }
pub fn ask_endpoint(external_ip: Option<IpAddr>) -> Result<SocketAddr, Error> { pub fn ask_endpoint(external_ip: Option<IpAddr>) -> Result<Endpoint, Error> {
println!("getting external IP address."); println!("getting external IP address.");
let external_ip = if external_ip.is_some() { let external_ip = if external_ip.is_some() {
@ -379,7 +380,7 @@ pub fn ask_endpoint(external_ip: Option<IpAddr>) -> Result<SocketAddr, Error> {
let mut endpoint_builder = Input::with_theme(&*THEME); let mut endpoint_builder = Input::with_theme(&*THEME);
if let Some(ip) = external_ip { if let Some(ip) = external_ip {
endpoint_builder.default(SocketAddr::new(ip, 51820)); endpoint_builder.default(SocketAddr::new(ip, 51820).into());
} else { } else {
println!("failed to get external IP."); println!("failed to get external IP.");
} }
@ -389,7 +390,7 @@ pub fn ask_endpoint(external_ip: Option<IpAddr>) -> Result<SocketAddr, Error> {
.map_err(|e| Error::from(e)) .map_err(|e| Error::from(e))
} }
pub fn override_endpoint(unset: bool) -> Result<Option<Option<SocketAddr>>, Error> { pub fn override_endpoint(unset: bool) -> Result<Option<Option<Endpoint>>, Error> {
let endpoint = if !unset { let endpoint = if !unset {
Some(ask_endpoint(None)?) Some(ask_endpoint(None)?)
} else { } else {

View File

@ -2,13 +2,15 @@ use crate::prompts::hostname_validator;
use ipnetwork::IpNetwork; use ipnetwork::IpNetwork;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
fmt::{Display, Formatter}, fmt::{self, Display, Formatter},
net::{IpAddr, SocketAddr}, net::{IpAddr, SocketAddr, ToSocketAddrs},
ops::Deref, ops::Deref,
path::Path, path::Path,
str::FromStr, str::FromStr,
vec,
}; };
use structopt::StructOpt; use structopt::StructOpt;
use url::Host;
use wgctrl::{InterfaceName, InvalidInterfaceName, Key, PeerConfig, PeerConfigBuilder}; use wgctrl::{InterfaceName, InvalidInterfaceName, Key, PeerConfig, PeerConfigBuilder};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -37,15 +39,105 @@ impl Deref for Interface {
} }
} }
#[derive(Clone, Debug, PartialEq)]
/// An external endpoint that supports both IP and domain name hosts.
pub struct Endpoint {
host: Host,
port: u16,
}
impl From<SocketAddr> for Endpoint {
fn from(addr: SocketAddr) -> Self {
match addr {
SocketAddr::V4(v4addr) => Self {
host: Host::Ipv4(*v4addr.ip()),
port: v4addr.port(),
},
SocketAddr::V6(v6addr) => Self {
host: Host::Ipv6(*v6addr.ip()),
port: v6addr.port(),
},
}
}
}
impl FromStr for Endpoint {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.rsplitn(2, ':').collect::<Vec<&str>>().as_slice() {
[port, host] => {
let port = port.parse().map_err(|_| "couldn't parse port")?;
let host = Host::parse(host).map_err(|_| "couldn't parse host")?;
Ok(Endpoint { host, port })
},
_ => Err("couldn't parse in form of 'host:port'"),
}
}
}
impl Serialize for Endpoint {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Endpoint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct EndpointVisitor;
impl<'de> serde::de::Visitor<'de> for EndpointVisitor {
type Value = Endpoint;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid host:port endpoint")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
s.parse().map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_str(EndpointVisitor)
}
}
impl fmt::Display for Endpoint {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.host.fmt(f)?;
f.write_str(":")?;
self.port.fmt(f)
}
}
impl Endpoint {
pub fn resolve(&self) -> Result<SocketAddr, String> {
let mut addrs = self
.to_string()
.to_socket_addrs()
.map_err(|e| e.to_string())?;
addrs
.next()
.ok_or_else(|| "failed to resolve address".to_string())
}
}
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(tag = "option", content = "content")] #[serde(tag = "option", content = "content")]
pub enum EndpointContents { pub enum EndpointContents {
Set(SocketAddr), Set(Endpoint),
Unset, Unset,
} }
impl Into<Option<SocketAddr>> for EndpointContents { impl Into<Option<Endpoint>> for EndpointContents {
fn into(self) -> Option<SocketAddr> { fn into(self) -> Option<Endpoint> {
match self { match self {
Self::Set(addr) => Some(addr), Self::Set(addr) => Some(addr),
Self::Unset => None, Self::Unset => None,
@ -53,8 +145,8 @@ impl Into<Option<SocketAddr>> for EndpointContents {
} }
} }
impl From<Option<SocketAddr>> for EndpointContents { impl From<Option<Endpoint>> for EndpointContents {
fn from(option: Option<SocketAddr>) -> Self { fn from(option: Option<Endpoint>) -> Self {
match option { match option {
Some(addr) => Self::Set(addr), Some(addr) => Self::Set(addr),
None => Self::Unset, None => Self::Unset,
@ -246,7 +338,7 @@ pub struct PeerContents {
pub ip: IpAddr, pub ip: IpAddr,
pub cidr_id: i64, pub cidr_id: i64,
pub public_key: String, pub public_key: String,
pub endpoint: Option<SocketAddr>, pub endpoint: Option<Endpoint>,
pub persistent_keepalive_interval: Option<u16>, pub persistent_keepalive_interval: Option<u16>,
pub is_admin: bool, pub is_admin: bool,
pub is_disabled: bool, pub is_disabled: bool,
@ -287,8 +379,11 @@ impl Peer {
pub fn diff(&self, peer: &PeerConfig) -> Option<PeerDiff> { pub fn diff(&self, peer: &PeerConfig) -> Option<PeerDiff> {
assert_eq!(self.public_key, peer.public_key.to_base64()); assert_eq!(self.public_key, peer.public_key.to_base64());
let endpoint_diff = if peer.endpoint != self.endpoint { let endpoint_diff = if let Some(ref endpoint) = self.endpoint {
self.endpoint match endpoint.resolve() {
Ok(resolved) if Some(resolved) != peer.endpoint => Some(resolved),
_ => None,
}
} else { } else {
None None
}; };
@ -331,7 +426,9 @@ impl<'a> From<&'a Peer> for PeerConfigBuilder {
builder builder
}; };
if let Some(endpoint) = peer.endpoint { let resolved = peer.endpoint.as_ref().map(|e| e.resolve().ok()).flatten();
if let Some(endpoint) = resolved {
builder.set_endpoint(endpoint) builder.set_endpoint(endpoint)
} else { } else {
builder builder