add DnsConfig and ZoneConfig with proto definitions

This commit is contained in:
Luna Yao
2026-02-05 18:30:09 +01:00
parent 7abdfd35e8
commit 3f3aec8edd
10 changed files with 319 additions and 15 deletions
+2 -1
View File
@@ -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",
+19 -12
View File
@@ -164,6 +164,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
"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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
"#[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);
+14
View File
@@ -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<std::path::PathBuf>) {}
fn get_dns(&self) -> DnsConfig;
fn set_dns(&self, dns: DnsConfig);
fn dump(&self) -> String;
}
@@ -444,6 +448,8 @@ struct Config {
peer: Option<Vec<PeerConfig>>,
proxy_network: Option<Vec<ProxyNetworkConfig>>,
dns: Option<DnsConfig>,
vpn_portal_config: Option<VpnPortalConfig>,
routes: Option<Vec<cidr::Ipv4Cidr>>,
@@ -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 =
+112
View File
@@ -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<LowerName> =
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<ZoneConfig>,
name: LowerName,
pub domain: LowerName,
pub addresses: Vec<SocketAddr>,
pub listeners: Vec<NameServerAddr>,
}
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<String>,
#[serde(default)]
pub forwarders: Vec<NameServerAddr>,
}
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(),
}
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod config;
mod utils;
+106
View File
@@ -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::<String>()
.trim_matches('-')
.to_string()
})
.filter(|label| !label.is_empty())
.collect::<Vec<_>>()
.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<NameServerAddr> for NameServerConfig {
fn from(value: NameServerAddr) -> Self {
Self::new(value.addr, value.protocol)
}
}
impl TryFrom<Url> for NameServerAddr {
type Error = Error;
fn try_from(value: Url) -> Result<Self, Self::Error> {
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::<IpAddr>()
.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<Self, Self::Err> {
let url = if s.parse::<IpAddr>().is_ok() || s.parse::<SocketAddr>().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)
}
}
+2 -2
View File
@@ -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;
+26
View File
@@ -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;
}
+34
View File
@@ -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(())
}
}
+2
View File
@@ -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;