diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 410726c8..204fadf4 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -56,7 +56,7 @@ itertools = "0.14.0" strum = { version = "0.27.2", features = ["derive"] } -gethostname = "0.5.0" +gethostname = "1.1.0" futures = { version = "0.3", features = ["bilock", "unstable"] } @@ -148,6 +148,7 @@ rand = "0.8.5" serde = { version = "1.0", features = ["derive"] } pnet = { version = "0.35.0", features = ["serde"] } serde_json = "1" +serde_with = "3" clap = { version = "4.5.30", features = [ "string", diff --git a/easytier/build.rs b/easytier/build.rs index 520a3f7f..afd35818 100644 --- a/easytier/build.rs +++ b/easytier/build.rs @@ -164,6 +164,7 @@ fn main() -> Result<(), Box> { "src/proto/api_manage.proto", "src/proto/web.proto", "src/proto/magic_dns.proto", + "src/proto/dns.proto", "src/proto/acl.proto", ]; @@ -171,17 +172,19 @@ fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed={proto_file}"); } - let out = PathBuf::from(env::var("OUT_DIR").unwrap()); + let out = PathBuf::from(env::var("OUT_DIR")?); let descriptor_file = out.join("descriptors.bin"); let mut config = prost_build::Config::new(); config - .type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]") + .protoc_arg("--experimental_allow_proto3_optional") + .file_descriptor_set_path(&descriptor_file) .extern_path(".google.protobuf.Any", "::prost_wkt_types::Any") .extern_path(".google.protobuf.Timestamp", "::prost_wkt_types::Timestamp") - .extern_path(".google.protobuf.Value", "::prost_wkt_types::Value") - .file_descriptor_set_path(&descriptor_file) - .protoc_arg("--experimental_allow_proto3_optional") + .extern_path(".google.protobuf.Value", "::prost_wkt_types::Value"); + + config + .type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]") .type_attribute("peer_rpc.DirectConnectedPeerInfo", "#[derive(Hash)]") .type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]") .type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]") @@ -190,19 +193,23 @@ fn main() -> Result<(), Box> { "#[derive(Hash, Eq)]", ) .type_attribute("peer_rpc.RouteForeignNetworkSummary", "#[derive(Hash, Eq)]") - .type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]") - .field_attribute(".api.manage.NetworkConfig", "#[serde(default)]") - .service_generator(Box::new(easytier_rpc_build::ServiceGenerator::default())) - .btree_map(["."]) - .skip_debug([".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]); + .type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]"); - config.compile_protos(&proto_files, &["src/proto/"])?; + config.field_attribute("api.manage.NetworkConfig", "#[serde(default)]"); + + config.skip_debug([".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]); + + config + .btree_map(["."]) + .service_generator(Box::new(easytier_rpc_build::ServiceGenerator::default())) + .protoc_arg("--experimental_allow_proto3_optional") + .compile_protos(&proto_files, &["src/proto/"])?; prost_reflect_build::Builder::new() .file_descriptor_set_bytes("crate::proto::DESCRIPTOR_POOL_BYTES") .compile_protos_with_config(config, &proto_files_reflect, &["src/proto/"])?; - let descriptor_bytes = std::fs::read(descriptor_file).unwrap(); + let descriptor_bytes = std::fs::read(descriptor_file)?; let descriptor = FileDescriptorSet::decode(&descriptor_bytes[..]).unwrap(); prost_wkt_build::add_serde(out, descriptor); diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index e4c9c5e6..1f8ef94d 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -16,6 +16,7 @@ use tokio::io::AsyncReadExt as _; use crate::{ common::stun::StunInfoCollector, + dns::config::DnsConfig, instance::dns_server::DEFAULT_ET_DNS_ZONE, proto::{ acl::Acl, @@ -207,6 +208,9 @@ pub trait ConfigLoader: Send + Sync { } fn set_credential_file(&self, _path: Option) {} + fn get_dns(&self) -> DnsConfig; + fn set_dns(&self, dns: DnsConfig); + fn dump(&self) -> String; } @@ -444,6 +448,8 @@ struct Config { peer: Option>, proxy_network: Option>, + dns: Option, + vpn_portal_config: Option, routes: Option>, @@ -868,6 +874,14 @@ impl ConfigLoader for TomlConfigLoader { self.config.lock().unwrap().credential_file = path; } + fn get_dns(&self) -> DnsConfig { + self.config.lock().unwrap().dns.clone().unwrap_or_default() + } + + fn set_dns(&self, dns: DnsConfig) { + self.config.lock().unwrap().dns = Some(dns); + } + fn dump(&self) -> String { let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap(); let default_flags_hashmap = diff --git a/easytier/src/dns/config.rs b/easytier/src/dns/config.rs new file mode 100644 index 00000000..f0cbe0a0 --- /dev/null +++ b/easytier/src/dns/config.rs @@ -0,0 +1,112 @@ +use crate::dns::utils::{sanitize, NameServerAddr}; +use crate::proto::dns::{DnsConfigKind, DnsConfigPb, ZoneConfigPb}; +use gethostname::gethostname; +use hickory_proto::rr::LowerName; +use serde::{Deserialize, Serialize}; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::str::FromStr; +use std::sync::LazyLock; + +pub const DNS_DEFAULT_ADDRESS: SocketAddr = + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(100, 100, 100, 101), 53)); +pub static DNS_DEFAULT_TLD: LazyLock = + LazyLock::new(|| LowerName::from_str("et.net.").unwrap()); + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(default)] +pub struct DnsConfig { + #[serde(rename = "zone")] + pub zones: Vec, + name: LowerName, + pub domain: LowerName, + pub addresses: Vec, + pub listeners: Vec, +} + +impl DnsConfig { + pub fn get_name(&self) -> String { + if self.name.is_empty() { + gethostname().to_string_lossy().to_string() + } else { + self.name.to_string() + } + } + + pub fn set_name(&mut self, name: &str) { + self.name = match LowerName::from_str(name) { + Ok(name) => name, + Err(_) => { + let sanitized = sanitize(name); + tracing::debug!("invalid hostname: {}, sanitized to: {}", name, sanitized); + LowerName::from_str(&sanitized).unwrap_or_default() + } + }; + } + + pub fn to_pb(&self, kind: DnsConfigKind) -> DnsConfigPb { + let pb = DnsConfigPb { + kind: kind.into(), + name: self.get_name(), + domain: self.domain.to_string(), + + ..Default::default() + }; + + match kind { + DnsConfigKind::Local => DnsConfigPb { + zones: self.zones.iter().map(Into::into).collect(), + addresses: self.addresses.clone().into_iter().map(Into::into).collect(), + listeners: self.listeners.iter().map(ToString::to_string).collect(), + + ..pb + }, + + DnsConfigKind::Remote => DnsConfigPb { + zones: self + .zones + .iter() + .filter(|z| z.broadcast) + .map(Into::into) + .collect(), + + ..pb + }, + } + } +} + +impl Default for DnsConfig { + fn default() -> Self { + Self { + name: LowerName::default(), + domain: DNS_DEFAULT_TLD.clone(), + addresses: vec![DNS_DEFAULT_ADDRESS], + listeners: vec![], + zones: vec![], + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] +pub struct ZoneConfig { + #[serde(default)] + pub broadcast: bool, + pub origin: LowerName, + #[serde(default)] + pub ttl: u32, + #[serde(default)] + pub records: Vec, + #[serde(default)] + pub forwarders: Vec, +} + +impl From<&ZoneConfig> for ZoneConfigPb { + fn from(value: &ZoneConfig) -> Self { + Self { + origin: value.origin.to_string(), + ttl: value.ttl, + records: value.records.clone(), + forwarders: value.forwarders.iter().map(ToString::to_string).collect(), + } + } +} diff --git a/easytier/src/dns/mod.rs b/easytier/src/dns/mod.rs index e69de29b..3a9134a6 100644 --- a/easytier/src/dns/mod.rs +++ b/easytier/src/dns/mod.rs @@ -0,0 +1,2 @@ +pub mod config; +mod utils; diff --git a/easytier/src/dns/utils.rs b/easytier/src/dns/utils.rs new file mode 100644 index 00000000..f7cb3f41 --- /dev/null +++ b/easytier/src/dns/utils.rs @@ -0,0 +1,106 @@ +use anyhow::{anyhow, Error}; +use hickory_proto::xfer::Protocol; +use hickory_resolver::config::NameServerConfig; +use idna::AsciiDenyList; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use std::fmt::{Display, Formatter}; +use std::net::{IpAddr, SocketAddr}; +use std::str::FromStr; +use url::Url; + +pub fn sanitize(name: &str) -> String { + let dot = name.ends_with('.'); + let mut name = idna::domain_to_ascii_cow(name.as_ref(), AsciiDenyList::EMPTY) + .unwrap_or_default() + .into_owned() + .to_lowercase() + .split('.') + .map(|label| { + label + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }) + .take(63) + .collect::() + .trim_matches('-') + .to_string() + }) + .filter(|label| !label.is_empty()) + .collect::>() + .join("."); + name.truncate(253); + if dot { + name.push('.'); + } + name +} + +static DNS_SUPPORTED_PROTOCOLS: [Protocol; 2] = [ + Protocol::Udp, + Protocol::Tcp, + // Protocol::Tls, + // Protocol::Https, + // Protocol::Quic, + // Protocol::H3, +]; + +#[derive(Debug, Clone, SerializeDisplay, DeserializeFromStr, PartialEq, Eq, Hash)] +pub struct NameServerAddr { + protocol: Protocol, + addr: SocketAddr, +} + +impl From for NameServerConfig { + fn from(value: NameServerAddr) -> Self { + Self::new(value.addr, value.protocol) + } +} + +impl TryFrom for NameServerAddr { + type Error = Error; + + fn try_from(value: Url) -> Result { + let scheme = value.scheme(); + let protocol = *DNS_SUPPORTED_PROTOCOLS + .iter() + .find(|p| p.to_string() == scheme) + .ok_or(anyhow!("unsupported scheme: {}", scheme))?; + let addr = value.host_str().ok_or(anyhow!("host not found"))?; + let addr = addr + .trim_start_matches('[') + .trim_end_matches(']') + .parse::() + .map_err(|e| anyhow!("invalid ip address '{}': {}", addr, e))?; + let port = if let Some(port) = value.port() { + port + } else { + match protocol { + Protocol::Udp | Protocol::Tcp => 53, + _ => return Err(anyhow!("port not found")), + } + }; + + Ok(Self { + protocol, + addr: SocketAddr::new(addr, port), + }) + } +} + +impl FromStr for NameServerAddr { + type Err = Error; + fn from_str(s: &str) -> Result { + let url = if s.parse::().is_ok() || s.parse::().is_ok() { + Url::parse(&format!("udp://{}", s))? + } else { + Url::parse(s)? + }; + + url.try_into() + } +} + +impl Display for NameServerAddr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}://{}", self.protocol, self.addr) + } +} diff --git a/easytier/src/lib.rs b/easytier/src/lib.rs index e719052a..097d7a97 100644 --- a/easytier/src/lib.rs +++ b/easytier/src/lib.rs @@ -6,12 +6,12 @@ use clap::Command; use clap_complete::{Generator, Shell}; mod arch; +#[cfg(feature = "magic-dns")] +mod dns; mod gateway; pub mod instance; mod peer_center; mod vpn_portal; -#[cfg(feature = "magic-dns")] -mod dns; pub mod common; pub mod connector; diff --git a/easytier/src/proto/dns.proto b/easytier/src/proto/dns.proto new file mode 100644 index 00000000..b522f76c --- /dev/null +++ b/easytier/src/proto/dns.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +import "common.proto"; + +package dns; + +enum DnsConfigKind { + LOCAL = 0; + REMOTE = 1; +} + +message DnsConfigPb { + DnsConfigKind kind = 1; + repeated ZoneConfigPb zones = 2; + string name = 3; + string domain = 4; + repeated common.SocketAddr addresses = 5; + repeated string listeners = 6; +} + +message ZoneConfigPb { + string origin = 1; + uint32 ttl = 2; + repeated string records = 3; + repeated string forwarders = 4; +} diff --git a/easytier/src/proto/dns.rs b/easytier/src/proto/dns.rs new file mode 100644 index 00000000..22dce587 --- /dev/null +++ b/easytier/src/proto/dns.rs @@ -0,0 +1,34 @@ +use std::fmt::Display; + +include!(concat!(env!("OUT_DIR"), "/dns.rs")); + +impl Display for ZoneConfigPb { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "; EasyTier Magic DNS zone file")?; + writeln!(f, "; https://github.com/easytier/easytier")?; + + if !self.forwarders.is_empty() { + writeln!(f, "; Forwarders:")?; + for forwarder in &self.forwarders { + writeln!(f, "; \t{}", forwarder)?; + } + } + writeln!(f)?; + + write!(f, "$ORIGIN {}", self.origin)?; + if !self.origin.ends_with('.') { + write!(f, ".")?; + } + writeln!(f)?; + + writeln!(f, "$TTL {}", self.ttl)?; + + writeln!(f)?; + + for record in &self.records { + writeln!(f, "{}", record)?; + } + + Ok(()) + } +} diff --git a/easytier/src/proto/mod.rs b/easytier/src/proto/mod.rs index bffca9b0..63363c3f 100644 --- a/easytier/src/proto/mod.rs +++ b/easytier/src/proto/mod.rs @@ -4,6 +4,8 @@ pub mod rpc_types; pub mod acl; pub mod api; pub mod common; +#[cfg(feature = "magic-dns")] +pub mod dns; pub mod error; #[cfg(feature = "magic-dns")] pub mod magic_dns;