Gateways, domains, and new service interface (#3001)

* add support for inbound proxies

* backend changes

* fix file type

* proxy -> tunnel, implement backend apis

* wip start-tunneld

* add domains and gateways, remove routers, fix docs links

* dont show hidden actions

* show and test dns

* edit instead of chnage acme and change gateway

* refactor: domains page

* refactor: gateways page

* domains and acme refactor

* certificate authorities

* refactor public/private gateways

* fix fe types

* domains mostly finished

* refactor: add file control to form service

* add ip util to sdk

* domains api + migration

* start service interface page, WIP

* different options for clearnet domains

* refactor: styles for interfaces page

* minor

* better placeholder for no addresses

* start sorting addresses

* best address logic

* comments

* fix unnecessary export

* MVP of service interface page

* domains preferred

* fix: address comments

* only translations left

* wip: start-tunnel & fix build

* forms for adding domain, rework things based on new ideas

* fix: dns testing

* public domain, max width, descriptions for dns

* nix StartOS domains, implement public and private domains at interface scope

* restart tor instead of reset

* better icon for restart tor

* dns

* fix sort functions for public and private domains

* with todos

* update types

* clean up tech debt, bump dependencies

* revert to ts-rs v9

* fix all types

* fix dns form

* add missing translations

* it builds

* fix: comments (#3009)

* fix: comments

* undo default

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix: refactor legacy components (#3010)

* fix: comments

* fix: refactor legacy components

* remove default again

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* more translations

* wip

* fix deadlock

* coukd work

* simple renaming

* placeholder for empty service interfaces table

* honor hidden form values

* remove logs

* reason instead of description

* fix dns

* misc fixes

* implement toggling gateways for service interface

* fix showing dns records

* move status column in service list

* remove unnecessary truthy check

* refactor: refactor forms components and remove legacy Taiga UI package (#3012)

* handle wh file uploads

* wip: debugging tor

* socks5 proxy working

* refactor: fix multiple comments (#3013)

* refactor: fix multiple comments

* styling changes, add documentation to sidebar

* translations for dns page

* refactor: subtle colors

* rearrange service page

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix file_stream and remove non-terminating test

* clean  up logs

* support for sccache

* fix gha sccache

* more marketplace translations

* install wizard clarity

* stub hostnameInfo in migration

* fix address info after setup, fix styling on SI page, new 040 release notes

* remove tor logs from os

* misc fixes

* reset tor still not functioning...

* update ts

* minor styling and wording

* chore: some fixes (#3015)

* fix gateway renames

* different handling for public domains

* styling fixes

* whole navbar should not be clickable on service show page

* timeout getState request

* remove links from changelog

* misc fixes from pairing

* use custom name for gateway in more places

* fix dns parsing

* closes #3003

* closes #2999

* chore: some fixes (#3017)

* small copy change

* revert hardcoded error for testing

* dont require port forward if gateway is public

* use old wan ip when not available

* fix .const hanging on undefined

* fix test

* fix doc test

* fix renames

* update deps

* allow specifying dependency metadata directly

* temporarily make dependencies not cliackable in marketplace listings

* fix socks bind

* fix test

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
Matt Hill
2025-09-09 21:43:51 -06:00
committed by GitHub
parent 1cc9a1a30b
commit add01ebc68
537 changed files with 19940 additions and 20551 deletions

View File

@@ -1,69 +1,332 @@
use std::borrow::Borrow;
use std::collections::BTreeMap;
use std::net::Ipv4Addr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::{Arc, Weak};
use std::time::Duration;
use clap::Parser;
use color_eyre::eyre::eyre;
use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt, TryStreamExt};
use helpers::NonDetachingJoinHandle;
use models::PackageId;
use hickory_client::client::Client;
use hickory_client::proto::runtime::TokioRuntimeProvider;
use hickory_client::proto::tcp::TcpClientStream;
use hickory_client::proto::udp::UdpClientStream;
use hickory_client::proto::xfer::{DnsExchangeBackground, DnsRequestOptions};
use hickory_client::proto::DnsHandle;
use hickory_server::authority::MessageResponseBuilder;
use hickory_server::proto::op::{Header, ResponseCode};
use hickory_server::proto::rr::{Name, Record, RecordType};
use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo};
use hickory_server::ServerFuture;
use imbl::OrdMap;
use imbl_value::InternedString;
use itertools::Itertools;
use models::{GatewayId, OptionExt, PackageId};
use rpc_toolkit::{
from_fn_async, from_fn_blocking, Context, HandlerArgs, HandlerExt, ParentHandler,
};
use serde::{Deserialize, Serialize};
use tokio::net::{TcpListener, UdpSocket};
use tokio::process::Command;
use tokio::sync::RwLock;
use tracing::instrument;
use trust_dns_server::authority::MessageResponseBuilder;
use trust_dns_server::proto::op::{Header, ResponseCode};
use trust_dns_server::proto::rr::{Name, Record, RecordType};
use trust_dns_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo};
use trust_dns_server::ServerFuture;
use crate::net::forward::START9_BRIDGE_IFACE;
use crate::util::sync::Watch;
use crate::util::Invoke;
use crate::{Error, ErrorKind, ResultExt};
use crate::context::RpcContext;
use crate::db::model::public::NetworkInterfaceInfo;
use crate::db::model::Database;
use crate::net::gateway::NetworkInterfaceWatcher;
use crate::prelude::*;
use crate::util::io::file_string_stream;
use crate::util::serde::{display_serializable, HandlerExtSerde};
use crate::util::sync::{SyncRwLock, Watch};
pub fn dns_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"query",
from_fn_blocking(query_dns::<C>)
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
if let Some(format) = params.format {
return display_serializable(format, res);
}
if let Some(ip) = res {
println!("{}", ip)
}
Ok(())
})
.with_about("Test the DNS configuration for a domain"),
)
.subcommand(
"set-static",
from_fn_async(set_static_dns)
.no_display()
.with_about("Set static DNS servers"),
)
}
#[derive(Deserialize, Serialize, Parser)]
pub struct QueryDnsParams {
pub fqdn: InternedString,
}
pub fn query_dns<C: Context>(
_: C,
QueryDnsParams { fqdn }: QueryDnsParams,
) -> Result<Option<Ipv4Addr>, Error> {
let hints = dns_lookup::AddrInfoHints {
flags: 0,
address: libc::AF_INET,
socktype: 0,
protocol: 0,
};
dns_lookup::getaddrinfo(Some(&*fqdn), None, Some(hints))
.map(Some)
.or_else(|e| {
if matches!(
e.kind(),
dns_lookup::LookupErrorKind::NoName | dns_lookup::LookupErrorKind::NoData
) {
Ok(None)
} else {
Err(std::io::Error::from(e))
}
})
.with_kind(ErrorKind::Network)?
.into_iter()
.flatten()
.find_map(|a| match a.map(|a| a.sockaddr.ip()) {
Ok(IpAddr::V4(a)) => Some(Ok(a)),
Err(e) => Some(Err(e)),
_ => None,
})
.transpose()
.map_err(Error::from)
}
#[derive(Deserialize, Serialize, Parser)]
pub struct SetStaticDnsParams {
pub servers: Option<Vec<String>>,
}
pub async fn set_static_dns(
ctx: RpcContext,
SetStaticDnsParams { servers }: SetStaticDnsParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_public_mut()
.as_server_info_mut()
.as_network_mut()
.as_dns_mut()
.as_static_servers_mut()
.ser(
&servers
.map(|s| {
s.into_iter()
.map(|s| {
s.parse::<SocketAddr>()
.or_else(|_| s.parse::<IpAddr>().map(|a| (a, 53).into()))
})
.collect()
})
.transpose()?,
)
})
.await
.result
}
#[derive(Default)]
struct ResolveMap {
private_domains: BTreeMap<InternedString, Weak<()>>,
services: BTreeMap<Option<PackageId>, BTreeMap<Ipv4Addr, Weak<()>>>,
}
pub struct DnsController {
services: Weak<RwLock<BTreeMap<Option<PackageId>, BTreeMap<Ipv4Addr, Weak<()>>>>>,
resolve: Weak<SyncRwLock<ResolveMap>>,
#[allow(dead_code)]
dns_server: NonDetachingJoinHandle<Result<(), Error>>,
dns_server: NonDetachingJoinHandle<()>,
}
struct DnsClient {
client: Arc<SyncRwLock<Vec<(SocketAddr, hickory_client::client::Client)>>>,
_thread: NonDetachingJoinHandle<()>,
}
impl DnsClient {
pub fn new(db: TypedPatchDb<Database>) -> Self {
let client = Arc::new(SyncRwLock::new(Vec::new()));
Self {
client: client.clone(),
_thread: tokio::spawn(async move {
loop {
if let Err::<(), Error>(e) = async {
let mut stream = file_string_stream("/run/systemd/resolve/resolv.conf")
.filter_map(|a| futures::future::ready(a.transpose()))
.boxed();
let mut conf: String = stream
.next()
.await
.or_not_found("/run/systemd/resolve/resolv.conf")??;
let mut prev_nameservers = Vec::new();
let mut bg = BTreeMap::<SocketAddr, BoxFuture<_>>::new();
loop {
let nameservers = conf
.lines()
.map(|l| l.trim())
.filter_map(|l| l.strip_prefix("nameserver "))
.skip(2)
.map(|n| {
n.parse::<SocketAddr>()
.or_else(|_| n.parse::<IpAddr>().map(|a| (a, 53).into()))
})
.collect::<Result<Vec<_>, _>>()?;
let static_nameservers = db
.mutate(|db| {
let dns = db
.as_public_mut()
.as_server_info_mut()
.as_network_mut()
.as_dns_mut();
dns.as_dhcp_servers_mut().ser(&nameservers)?;
dns.as_static_servers().de()
})
.await
.result?;
let nameservers = static_nameservers.unwrap_or(nameservers);
if nameservers != prev_nameservers {
let mut existing: BTreeMap<_, _> =
client.peek(|c| c.iter().cloned().collect());
let mut new = Vec::with_capacity(nameservers.len());
for addr in &nameservers {
if let Some(existing) = existing.remove(addr) {
new.push((*addr, existing));
} else {
let client = if let Ok((client, bg_thread)) =
Client::connect(
UdpClientStream::builder(
*addr,
TokioRuntimeProvider::new(),
)
.build(),
)
.await
{
bg.insert(*addr, bg_thread.boxed());
client
} else {
let (stream, sender) = TcpClientStream::new(
*addr,
None,
Some(Duration::from_secs(30)),
TokioRuntimeProvider::new(),
);
let (client, bg_thread) =
Client::new(stream, sender, None)
.await
.with_kind(ErrorKind::Network)?;
bg.insert(*addr, bg_thread.boxed());
client
};
new.push((*addr, client));
}
}
bg.retain(|n, _| nameservers.iter().any(|a| a == n));
prev_nameservers = nameservers;
client.replace(new);
}
tokio::select! {
c = stream.next() => conf = c.or_not_found("/run/systemd/resolve/resolv.conf")??,
_ = futures::future::join(
futures::future::join_all(bg.values_mut()),
futures::future::pending::<()>(),
) => (),
}
}
}
.await
{
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
}
})
.into(),
}
}
fn lookup(
&self,
query: hickory_client::proto::op::Query,
options: DnsRequestOptions,
) -> Vec<hickory_client::proto::xfer::DnsExchangeSend> {
self.client.peek(|c| {
c.iter()
.map(|(_, c)| c.lookup(query.clone(), options.clone()))
.collect()
})
}
}
struct Resolver {
services: Arc<RwLock<BTreeMap<Option<PackageId>, BTreeMap<Ipv4Addr, Weak<()>>>>>,
client: DnsClient,
net_iface: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
resolve: Arc<SyncRwLock<ResolveMap>>,
}
impl Resolver {
async fn resolve(&self, name: &Name) -> Option<Vec<Ipv4Addr>> {
match name.iter().next_back() {
Some(b"embassy") | Some(b"startos") => {
if let Some(pkg) = name.iter().rev().skip(1).next() {
if let Some(ip) = self.services.read().await.get(&Some(
std::str::from_utf8(pkg)
.unwrap_or_default()
.parse()
.unwrap_or_default(),
)) {
fn resolve(&self, name: &Name, src: IpAddr) -> Option<Vec<IpAddr>> {
self.resolve.peek(|r| {
if r.private_domains
.get(&*name.to_lowercase().to_ascii())
.map_or(false, |d| d.strong_count() > 0)
{
if let Some(res) = self.net_iface.peek(|i| {
i.values()
.chain([NetworkInterfaceInfo::lxc_bridge().1])
.flat_map(|i| i.ip_info.as_ref())
.find(|i| i.subnets.iter().any(|s| s.contains(&src)))
.map(|ip_info| {
let mut res = ip_info.subnets.iter().collect::<Vec<_>>();
res.sort_by_cached_key(|a| !a.contains(&src));
res.into_iter().map(|s| s.addr()).collect()
})
}) {
return Some(res);
}
}
match name.iter().next_back() {
Some(b"embassy") | Some(b"startos") => {
if let Some(pkg) = name.iter().rev().skip(1).next() {
if let Some(ip) = r.services.get(&Some(
std::str::from_utf8(pkg)
.unwrap_or_default()
.parse()
.unwrap_or_default(),
)) {
Some(
ip.iter()
.filter(|(_, rc)| rc.strong_count() > 0)
.map(|(ip, _)| (*ip).into())
.collect(),
)
} else {
None
}
} else if let Some(ip) = r.services.get(&None) {
Some(
ip.iter()
.filter(|(_, rc)| rc.strong_count() > 0)
.map(|(ip, _)| *ip)
.map(|(ip, _)| (*ip).into())
.collect(),
)
} else {
None
}
} else if let Some(ip) = self.services.read().await.get(&None) {
Some(
ip.iter()
.filter(|(_, rc)| rc.strong_count() > 0)
.map(|(ip, _)| *ip)
.collect(),
)
} else {
None
}
_ => None,
}
_ => None,
}
})
}
}
@@ -74,132 +337,215 @@ impl RequestHandler for Resolver {
request: &Request,
mut response_handle: R,
) -> ResponseInfo {
let query = request.request_info().query;
if let Some(ip) = self.resolve(query.name().borrow()).await {
match query.query_type() {
RecordType::A => {
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
Header::response_from_request(request.header()),
&ip.into_iter()
.map(|ip| {
Record::from_rdata(
request.request_info().query.name().to_owned().into(),
0,
trust_dns_server::proto::rr::RData::A(ip.into()),
)
})
.collect::<Vec<_>>(),
[],
[],
[],
),
)
.await
match async {
let req = request.request_info()?;
let query = req.query;
if let Some(ip) = self.resolve(query.name().borrow(), req.src.ip()) {
match query.query_type() {
RecordType::A => {
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
header,
&ip.into_iter()
.filter_map(|a| {
if let IpAddr::V4(a) = a {
Some(a)
} else {
None
}
})
.map(|ip| {
Record::from_rdata(
query.name().to_owned().into(),
0,
hickory_server::proto::rr::RData::A(ip.into()),
)
})
.collect::<Vec<_>>(),
[],
[],
[],
),
)
.await
}
RecordType::AAAA => {
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
header,
&ip.into_iter()
.filter_map(|a| {
if let IpAddr::V6(a) = a {
Some(a)
} else {
None
}
})
.map(|ip| {
Record::from_rdata(
query.name().to_owned().into(),
0,
hickory_server::proto::rr::RData::AAAA(ip.into()),
)
})
.collect::<Vec<_>>(),
[],
[],
[],
),
)
.await
}
_ => {
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
header.into(),
[],
[],
[],
[],
),
)
.await
}
}
_ => {
let res = Header::response_from_request(request.header());
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
res.into(),
[],
[],
[],
[],
),
)
.await
} else {
let query = query.original().clone();
let mut streams = self.client.lookup(query, DnsRequestOptions::default());
let mut err = None;
for stream in streams.iter_mut() {
match tokio::time::timeout(Duration::from_secs(5), stream.next()).await {
Ok(Some(Err(e))) => err = Some(e),
Ok(Some(Ok(msg))) => {
return response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
Header::response_from_request(request.header()),
msg.answers(),
msg.name_servers(),
&msg.soa().map(|s| s.to_owned().into_record_of_rdata()),
msg.additionals(),
),
)
.await;
}
_ => (),
}
}
if let Some(e) = err {
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
header.set_response_code(ResponseCode::ServFail);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
header,
[],
[],
[],
[],
),
)
.await
}
}
.await
{
Ok(a) => a,
Err(e) => {
tracing::error!("{}", e);
tracing::debug!("{:?}", e);
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
header.set_response_code(ResponseCode::ServFail);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
header,
[],
[],
[],
[],
),
)
.await
.unwrap_or(header.into())
}
} else {
let mut res = Header::response_from_request(request.header());
res.set_response_code(ResponseCode::NXDomain);
response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
res.into(),
[],
[],
[],
[],
),
)
.await
}
.unwrap_or_else(|e| {
tracing::error!("{}", e);
tracing::debug!("{:?}", e);
let mut res = Header::response_from_request(request.header());
res.set_response_code(ResponseCode::ServFail);
res.into()
})
}
}
impl DnsController {
#[instrument(skip_all)]
pub async fn init(mut lxcbr_status: Watch<bool>) -> Result<Self, Error> {
let services = Arc::new(RwLock::new(BTreeMap::new()));
pub async fn init(
db: TypedPatchDb<Database>,
watcher: &NetworkInterfaceWatcher,
) -> Result<Self, Error> {
let resolve = Arc::new(SyncRwLock::new(ResolveMap::default()));
let mut server = ServerFuture::new(Resolver {
services: services.clone(),
client: DnsClient::new(db),
net_iface: watcher.subscribe(),
resolve: resolve.clone(),
});
let dns_server = tokio::spawn(async move {
server.register_listener(
TcpListener::bind((Ipv4Addr::LOCALHOST, 53))
let dns_server = tokio::spawn(
async move {
server.register_listener(
TcpListener::bind((Ipv6Addr::UNSPECIFIED, 53))
.await
.with_kind(ErrorKind::Network)?,
Duration::from_secs(30),
);
server.register_socket(
UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 53))
.await
.with_kind(ErrorKind::Network)?,
);
server
.block_until_done()
.await
.with_kind(ErrorKind::Network)?,
Duration::from_secs(30),
);
server.register_socket(
UdpSocket::bind((Ipv4Addr::LOCALHOST, 53))
.await
.with_kind(ErrorKind::Network)?,
);
lxcbr_status.wait_for(|a| *a).await;
Command::new("resolvectl")
.arg("dns")
.arg(START9_BRIDGE_IFACE)
.arg("127.0.0.1")
.invoke(ErrorKind::Network)
.await?;
Command::new("resolvectl")
.arg("domain")
.arg(START9_BRIDGE_IFACE)
.arg("embassy")
.invoke(ErrorKind::Network)
.await?;
server
.block_until_done()
.await
.map_err(|e| Error::new(e, ErrorKind::Network))
})
.with_kind(ErrorKind::Network)
}
.map(|r| {
r.log_err();
}),
)
.into();
Ok(Self {
services: Arc::downgrade(&services),
resolve: Arc::downgrade(&resolve),
dns_server,
})
}
pub async fn add(&self, pkg_id: Option<PackageId>, ip: Ipv4Addr) -> Result<Arc<()>, Error> {
if let Some(services) = Weak::upgrade(&self.services) {
let mut writable = services.write().await;
let mut ips = writable.remove(&pkg_id).unwrap_or_default();
let rc = if let Some(rc) = Weak::upgrade(&ips.remove(&ip).unwrap_or_default()) {
rc
} else {
Arc::new(())
};
ips.insert(ip, Arc::downgrade(&rc));
writable.insert(pkg_id, ips);
Ok(rc)
pub fn add_service(&self, pkg_id: Option<PackageId>, ip: Ipv4Addr) -> Result<Arc<()>, Error> {
if let Some(resolve) = Weak::upgrade(&self.resolve) {
resolve.mutate(|writable| {
let ips = writable.services.entry(pkg_id).or_default();
let weak = ips.entry(ip).or_default();
let rc = if let Some(rc) = Weak::upgrade(&*weak) {
rc
} else {
let new = Arc::new(());
*weak = Arc::downgrade(&new);
new
};
Ok(rc)
})
} else {
Err(Error::new(
eyre!("DNS Server Thread has exited"),
@@ -208,17 +554,65 @@ impl DnsController {
}
}
pub async fn gc(&self, pkg_id: Option<PackageId>, ip: Ipv4Addr) -> Result<(), Error> {
if let Some(services) = Weak::upgrade(&self.services) {
let mut writable = services.write().await;
let mut ips = writable.remove(&pkg_id).unwrap_or_default();
if let Some(rc) = Weak::upgrade(&ips.remove(&ip).unwrap_or_default()) {
ips.insert(ip, Arc::downgrade(&rc));
}
if !ips.is_empty() {
writable.insert(pkg_id, ips);
}
Ok(())
pub fn gc_service(&self, pkg_id: Option<PackageId>, ip: Ipv4Addr) -> Result<(), Error> {
if let Some(resolve) = Weak::upgrade(&self.resolve) {
resolve.mutate(|writable| {
let mut ips = writable.services.remove(&pkg_id).unwrap_or_default();
if let Some(rc) = Weak::upgrade(&ips.remove(&ip).unwrap_or_default()) {
ips.insert(ip, Arc::downgrade(&rc));
}
if !ips.is_empty() {
writable.services.insert(pkg_id, ips);
}
Ok(())
})
} else {
Err(Error::new(
eyre!("DNS Server Thread has exited"),
crate::ErrorKind::Network,
))
}
}
pub fn add_private_domain(&self, fqdn: InternedString) -> Result<Arc<()>, Error> {
if let Some(resolve) = Weak::upgrade(&self.resolve) {
resolve.mutate(|writable| {
let weak = writable.private_domains.entry(fqdn).or_default();
let rc = if let Some(rc) = Weak::upgrade(&*weak) {
rc
} else {
let new = Arc::new(());
*weak = Arc::downgrade(&new);
new
};
Ok(rc)
})
} else {
Err(Error::new(
eyre!("DNS Server Thread has exited"),
crate::ErrorKind::Network,
))
}
}
pub fn gc_private_domains<'a, BK: Ord + 'a>(
&self,
domains: impl IntoIterator<Item = &'a BK> + 'a,
) -> Result<(), Error>
where
InternedString: Borrow<BK>,
{
if let Some(resolve) = Weak::upgrade(&self.resolve) {
resolve.mutate(|writable| {
for domain in domains {
if let Some((k, v)) = writable.private_domains.remove_entry(domain) {
if v.strong_count() > 0 {
writable.private_domains.insert(k, v);
}
}
}
Ok(())
})
} else {
Err(Error::new(
eyre!("DNS Server Thread has exited"),