{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",
"toml",
"ureq",
"url",
"warp",
"wgctrl",
]
@ -1169,6 +1170,7 @@ dependencies = [
"structopt",
"toml",
"ureq",
"url",
"wgctrl",
]

View File

@ -283,6 +283,7 @@ fn redeem_invite(
target_conf: PathBuf,
) -> Result<(), Error> {
println!("{} bringing up the interface.", "[*]".dimmed());
let resolved_endpoint = config.server.external_endpoint.resolve()?;
wg::up(
&iface,
&config.interface.private_key,
@ -291,7 +292,7 @@ fn redeem_invite(
Some((
&config.server.public_key,
config.server.internal_endpoint.ip(),
config.server.external_endpoint,
resolved_endpoint,
)),
)?;
@ -369,6 +370,7 @@ fn fetch(
}
println!("{} bringing up the interface.", "[*]".dimmed());
let resolved_endpoint = config.server.external_endpoint.resolve()?;
wg::up(
interface,
&config.interface.private_key,
@ -377,7 +379,7 @@ fn fetch(
Some((
&config.server.public_key,
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()
);
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);
}
if let Some(last_handshake) = peer.stats.last_handshake_time {

View File

@ -34,6 +34,7 @@ subtle = "2"
structopt = "0.3"
thiserror = "1"
ureq = { version = "2", default-features = false }
url = "2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
toml = "0.5"
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 {
if peer.contents.endpoint.is_none() {
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)
.path("/v1/user/endpoint")
.body(serde_json::to_string(&EndpointContents::Set(
"1.1.1.1:51820".parse()?
"1.1.1.1:51820".parse().unwrap()
))?)
.reply(&filter)
.await

View File

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

View File

@ -5,7 +5,7 @@ use indoc::printdoc;
use rusqlite::{params, Connection};
use shared::{
prompts::{self, hostname_validator},
CidrContents, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
CidrContents, Endpoint, PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
};
use wgctrl::KeyPair;
@ -32,7 +32,7 @@ pub struct InitializeOpts {
/// This server's external endpoint (ex: 100.100.100.100:51820)
#[structopt(long, conflicts_with = "auto-external-endpoint")]
pub external_endpoint: Option<SocketAddr>,
pub external_endpoint: Option<Endpoint>,
/// Auto-resolve external endpoint
#[structopt(long = "auto-external-endpoint")]
@ -49,7 +49,7 @@ struct DbInitData {
server_cidr: IpNetwork,
our_ip: IpAddr,
public_key_base64: String,
endpoint: SocketAddr,
endpoint: Endpoint,
}
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.
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()
} else {
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 {
let ip = external_ip.ok_or("couldn't get external IP")?;
(ip, 51820).into()
SocketAddr::new(ip, 51820).into()
} else {
prompts::ask_endpoint(external_ip)?
}

View File

@ -69,7 +69,7 @@ impl Server {
let opts = InitializeOpts {
network_name: Some(interface.clone()),
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),
auto_external_endpoint: false,
};

View File

@ -17,4 +17,5 @@ serde = { version = "1", features = ["derive"] }
structopt = "0.3"
toml = "0.5"
ureq = { version = "2", default-features = false }
url = "2"
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 indoc::writedoc;
use ipnetwork::IpNetwork;
@ -46,7 +46,7 @@ pub struct ServerInfo {
pub public_key: String,
/// 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.
pub internal_endpoint: SocketAddr,

View File

@ -1,7 +1,7 @@
use crate::{
interface_config::{InterfaceConfig, InterfaceInfo, ServerInfo},
AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, Error, Peer, PeerContents,
PERSISTENT_KEEPALIVE_INTERVAL_SECS,
AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, Endpoint, Error, Peer,
PeerContents, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
};
use colored::*;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
@ -299,6 +299,7 @@ pub fn save_peer_invitation(
server: ServerInfo {
external_endpoint: server_peer
.endpoint
.clone()
.expect("The innernet server should have a WireGuard endpoint"),
internal_endpoint: *server_api_addr,
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.");
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);
if let Some(ip) = external_ip {
endpoint_builder.default(SocketAddr::new(ip, 51820));
endpoint_builder.default(SocketAddr::new(ip, 51820).into());
} else {
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))
}
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 {
Some(ask_endpoint(None)?)
} else {

View File

@ -2,13 +2,15 @@ use crate::prompts::hostname_validator;
use ipnetwork::IpNetwork;
use serde::{Deserialize, Serialize};
use std::{
fmt::{Display, Formatter},
net::{IpAddr, SocketAddr},
fmt::{self, Display, Formatter},
net::{IpAddr, SocketAddr, ToSocketAddrs},
ops::Deref,
path::Path,
str::FromStr,
vec,
};
use structopt::StructOpt;
use url::Host;
use wgctrl::{InterfaceName, InvalidInterfaceName, Key, PeerConfig, PeerConfigBuilder};
#[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)]
#[serde(tag = "option", content = "content")]
pub enum EndpointContents {
Set(SocketAddr),
Set(Endpoint),
Unset,
}
impl Into<Option<SocketAddr>> for EndpointContents {
fn into(self) -> Option<SocketAddr> {
impl Into<Option<Endpoint>> for EndpointContents {
fn into(self) -> Option<Endpoint> {
match self {
Self::Set(addr) => Some(addr),
Self::Unset => None,
@ -53,8 +145,8 @@ impl Into<Option<SocketAddr>> for EndpointContents {
}
}
impl From<Option<SocketAddr>> for EndpointContents {
fn from(option: Option<SocketAddr>) -> Self {
impl From<Option<Endpoint>> for EndpointContents {
fn from(option: Option<Endpoint>) -> Self {
match option {
Some(addr) => Self::Set(addr),
None => Self::Unset,
@ -246,7 +338,7 @@ pub struct PeerContents {
pub ip: IpAddr,
pub cidr_id: i64,
pub public_key: String,
pub endpoint: Option<SocketAddr>,
pub endpoint: Option<Endpoint>,
pub persistent_keepalive_interval: Option<u16>,
pub is_admin: bool,
pub is_disabled: bool,
@ -287,8 +379,11 @@ impl Peer {
pub fn diff(&self, peer: &PeerConfig) -> Option<PeerDiff> {
assert_eq!(self.public_key, peer.public_key.to_base64());
let endpoint_diff = if peer.endpoint != self.endpoint {
self.endpoint
let endpoint_diff = if let Some(ref endpoint) = self.endpoint {
match endpoint.resolve() {
Ok(resolved) if Some(resolved) != peer.endpoint => Some(resolved),
_ => None,
}
} else {
None
};
@ -331,7 +426,9 @@ impl<'a> From<&'a Peer> for PeerConfigBuilder {
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)
} else {
builder