feat: Initial functionality

This commit is contained in:
Jan-Bulthuis 2025-08-09 18:20:11 +02:00
commit 87e9eed8ab
9 changed files with 3607 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

2783
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "madd"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8.4"
base64ct = { version = "1.8.0", features = ["std", "alloc"] }
clap = { version = "4.5.43", features = ["derive"] }
dns-update = "0.1.5"
env_logger = "0.11.8"
ipnet = { version = "2.11.0", features = ["serde"] }
log = "0.4.27"
rand = "0.9.2"
serde = { version = "1.0.219", features = ["derive"] }
ssh-key = { version = "0.6.7", features = [
"serde",
"ed25519",
"rsa",
"crypto",
] }
tokio = { version = "1.47.1", features = ["full"] }
toml = "0.9.5"

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Machine Authenticated Dynamic DNS
Machines can authenticate themselves to MADD using their SSH host key and a challenge response system. When authenticated the MADD server updates their hostname in a DNS server supporting RFC2316 Dynamic DNS updates.

30
client.sh Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
private_key="${MADD_PRIV_KEY:-"/etc/ssh/ssh_host_ed25519_key"}"
public_key="${MADD_PUB_KEY:-"$private_key.pub"}"
hostname="${MADD_HOSTNAME:-"$(hostname -s)"}"
requested_ip=$MADD_IP # TODO: Obtain IP automatically
endpoint=$MADD_ENDPOINT
echo "(MADD) Updating DNS..."
echo
echo " Endpoint: $endpoint"
echo "Priv. key: $private_key"
echo " Pub. key: $public_key"
echo " Hostname: $hostname"
echo " IP: $requested_ip"
echo
# Generate the request and receive the identifier to sign
identifier=$(curl "$endpoint/request/$hostname/$requested_ip" --fail-with-body --data-binary @$public_key 2>/dev/null)
# Sign the request using the SSH key
signed=$(echo -n "$identifier $hostname $requested_ip" | ssh-keygen -Y sign -f $private_key -n madd 2>/dev/null)
# Escape slashes in the identifier
identifier_escaped=$(sed "s|/|%2F|g" <<< "$identifier")
# Submit the signed request
curl "$endpoint/signed/$identifier_escaped" --fail-with-body --data "$signed"

22
madd.toml Normal file
View File

@ -0,0 +1,22 @@
# Socket address to bind the MADD server to
bind = "127.0.0.1:3000"
# DNS zone under which the hosts are registered
zone = "lab.example.com"
# List of subnets where hosts can register hostnames to
networks = ["10.0.0.0/8"]
# Maximum number of self-registrations allowed per host key
registration_limit = 1
# DNS server to use for registration, must support RFC 2136
dns_server = "127.0.0.1:53"
# TSIG key configuration for DNS updates
tsig_key_name = "madd"
tsig_key_file = "/etc/madd/madd.tsig"
tsig_algorithm = "hmac-sha256"
# Directory to store data files, must be writable by the user running MADD
data_dir = "/var/lib/madd"

190
src/api.rs Normal file
View File

@ -0,0 +1,190 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use axum::{
extract::{ConnectInfo, Path, State},
http::StatusCode,
response::IntoResponse,
routing::post,
};
use base64ct::{Base64, Encoding};
use log::{info, warn};
use ssh_key::{PublicKey, SshSig};
use tokio::{
sync::{mpsc, oneshot},
task::JoinHandle,
time::Instant,
};
use crate::{
Config,
dns::{Command, CreateRequest, DNSRequest, Identifier, RequestError, SignRequest},
};
#[derive(Clone)]
struct ServerState {
cmd_tx: mpsc::Sender<Command>,
}
pub async fn start_server(tx: mpsc::Sender<Command>, config: &Config) -> JoinHandle<()> {
let state = ServerState { cmd_tx: tx };
let router = axum::Router::new()
.route("/request/{hostname}/{ip_addr}", post(create_request))
.route("/signed/{identifier}", post(signed_request))
.with_state(state);
let listener = tokio::net::TcpListener::bind(config.bind).await.unwrap();
let join_handle = tokio::spawn(async move {
match axum::serve(
listener,
router.into_make_service_with_connect_info::<SocketAddr>(),
)
.into_future()
.await
{
Ok(_) => todo!(),
Err(_) => todo!(),
}
});
info!("MADD running on http://{}", config.bind);
join_handle
}
async fn create_request(
ConnectInfo(connect_info): ConnectInfo<SocketAddr>,
Path((hostname, requested_ip)): Path<(String, Ipv4Addr)>,
State(state): State<ServerState>,
body: String,
) -> impl IntoResponse {
let host_ip = match connect_info.ip() {
IpAddr::V4(ipv4_addr) => ipv4_addr,
IpAddr::V6(ipv6_addr) => {
return (
StatusCode::BAD_REQUEST,
format!("Access from ipv6 address {ipv6_addr} not allowed"),
);
}
};
// Parse SSH public key
let ssh_key = match PublicKey::from_openssh(&body) {
Ok(key) => key,
Err(e) => {
warn!("Failed to parse public key submitted by client {connect_info}: {e}");
return (StatusCode::BAD_REQUEST, String::from("Invalid public key"));
}
};
// Create request
let (tx, rx) = oneshot::channel();
let request = Command::CreateRequest(Box::new(CreateRequest {
request: DNSRequest {
hostname,
time: Instant::now(),
ssh_key,
requested_ip,
host_ip,
},
response_channel: tx,
}));
let _ = state.cmd_tx.send(request).await;
let identifier = match rx.await {
Ok(res) => res,
Err(e) => unreachable!("Oneshot response channel sender was dropped unexpectedly: {e}"),
};
let encoded = identifier.to_base64();
(StatusCode::OK, encoded)
}
async fn signed_request(
ConnectInfo(connect_info): ConnectInfo<SocketAddr>,
Path(identifier): Path<String>,
State(state): State<ServerState>,
body: String,
) -> impl IntoResponse {
let host_ip = match connect_info.ip() {
IpAddr::V4(ipv4_addr) => ipv4_addr,
IpAddr::V6(ipv6_addr) => {
return (
StatusCode::BAD_REQUEST,
format!("Access from ipv6 address {ipv6_addr} not allowed"),
);
}
};
// Decode the identifier
let mut bytes = [0; 256];
match Base64::decode(identifier, &mut bytes) {
Ok(_) => (), // TODO: Check for length?
Err(_) => todo!(),
}
let identifier = Identifier(bytes);
// Read the signed value
let signed = match SshSig::from_pem(&body) {
Ok(sig) => sig,
Err(e) => {
warn!("Failed to parse ssh signature submitted by client {connect_info}: {e}");
return (StatusCode::BAD_REQUEST, String::from("Bad SSH signature"));
}
};
// Create request
let (tx, rx) = oneshot::channel();
let request = Command::SignRequest(Box::new(SignRequest {
identifier,
signed,
time: Instant::now(),
host_ip,
response_channel: tx,
}));
let _ = state.cmd_tx.send(request).await;
match rx.await {
Ok(res) => match res {
Ok(_) => (
StatusCode::OK,
String::from("Request fulfilled successfully"),
),
Err(e) => match e {
RequestError::HostIpMismatch => (
StatusCode::BAD_REQUEST,
String::from(
"Signed request must be submitted from the same host that created the request",
),
),
RequestError::InvalidSignature => (
StatusCode::BAD_REQUEST,
String::from("Invalid SSH signature"),
),
RequestError::RequestExpired => {
(StatusCode::FORBIDDEN, String::from("Request expired"))
}
RequestError::RequestedIpNotAllowed => (
StatusCode::BAD_REQUEST,
String::from("Requested IP not allowed"),
),
RequestError::UpdateFailed => (
StatusCode::INTERNAL_SERVER_ERROR,
String::from("Failed to update DNS record"),
),
RequestError::AlreadyRegistered => (
StatusCode::FORBIDDEN,
String::from("Hostname already registered by another host"),
),
RequestError::TooManyRegistrations => (
StatusCode::FORBIDDEN,
String::from("Registration limit reached"),
),
},
},
Err(e) => unreachable!("Oneshot response channel sender was dropped unexpectedly: {e}"),
}
}

472
src/dns.rs Normal file
View File

@ -0,0 +1,472 @@
use std::{
collections::HashMap, hash::Hash, net::Ipv4Addr, ops::Deref, str::FromStr, time::Duration,
};
use base64ct::{Base64, Encoding};
use dns_update::DnsUpdater;
use log::{error, info, warn};
use rand::{Rng, SeedableRng, rngs::StdRng};
use serde::{Deserialize, Serialize, de};
use ssh_key::{PublicKey, SshSig};
use tokio::{
sync::{mpsc, oneshot},
task::JoinHandle,
time::Instant,
};
use crate::Config;
// const MAX_HOSTNAME_LENGTH: usize = 15;
// #[derive(Debug)]
// pub struct Hostname(String);
// impl Deref for Hostname {
// type Target = String;
// fn deref(&self) -> &Self::Target {
// &self.0
// }
// }
// impl ser::Serialize for Hostname {
// fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
// where
// S: ser::Serializer,
// {
// serializer.serialize_str(&self.0)
// }
// }
// impl<'de> de::Deserialize<'de> for Hostname {
// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
// where
// D: serde::Deserializer<'de>,
// {
// <String as de::Deserialize>::deserialize(deserializer).and_then(|inner| {
// if inner.len() > MAX_HOSTNAME_LENGTH {
// Err(de::Error::invalid_length(
// inner.len(),
// &"a shorter hostname",
// ))
// } else {
// Ok(Self(inner))
// }
// })
// }
// }
#[derive(Debug, Clone)]
pub struct DnsAddress(dns_update::providers::rfc2136::DnsAddress);
impl Deref for DnsAddress {
type Target = dns_update::providers::rfc2136::DnsAddress;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'de> Deserialize<'de> for DnsAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
<String as de::Deserialize>::deserialize(deserializer).and_then(|inner| {
match dns_update::providers::rfc2136::DnsAddress::try_from(&inner) {
Ok(addr) => Ok(DnsAddress(addr)),
Err(_) => Err(de::Error::custom(format!("Invalid DNS address: {inner}"))),
}
})
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct Registrations(HashMap<String, Registration>);
#[derive(Deserialize, Serialize, Debug)]
pub struct Registration {
pub ip: Ipv4Addr,
pub public_key: PublicKey,
}
pub struct TsigAlgorithm(dns_update::TsigAlgorithm);
impl Deref for TsigAlgorithm {
type Target = dns_update::TsigAlgorithm;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'de> Deserialize<'de> for TsigAlgorithm {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
<String as de::Deserialize>::deserialize(deserializer).and_then(|inner| {
match dns_update::TsigAlgorithm::from_str(&inner) {
Ok(algorithm) => Ok(TsigAlgorithm(algorithm)),
Err(_) => Err(de::Error::unknown_variant(
&inner,
&[
"hmac-md5",
"gss",
"hmac-sha1",
"hmac-sha224",
"hmac-sha256",
"hmac-sha256-128",
"hmac-sha384",
"hmac-sha384-192",
"hmac-sha512",
"hmac-sha512-256",
],
)),
}
})
}
}
impl std::fmt::Debug for TsigAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let algorithm_name = match self.0 {
dns_update::TsigAlgorithm::HmacMd5 => "hmac-md5",
dns_update::TsigAlgorithm::Gss => "gss",
dns_update::TsigAlgorithm::HmacSha1 => "hmac-sha1",
dns_update::TsigAlgorithm::HmacSha224 => "hmac-sha224",
dns_update::TsigAlgorithm::HmacSha256 => "hmac-sha256",
dns_update::TsigAlgorithm::HmacSha256_128 => "hmac-sha256-128",
dns_update::TsigAlgorithm::HmacSha384 => "hmac-sha384",
dns_update::TsigAlgorithm::HmacSha384_192 => "hmac-sha384-192",
dns_update::TsigAlgorithm::HmacSha512 => "hmac-sha512",
dns_update::TsigAlgorithm::HmacSha512_256 => "hmac-sha512-256",
};
write!(f, "{algorithm_name}")
}
}
impl Clone for TsigAlgorithm {
fn clone(&self) -> Self {
Self(match self.0 {
dns_update::TsigAlgorithm::HmacMd5 => dns_update::TsigAlgorithm::HmacMd5,
dns_update::TsigAlgorithm::Gss => dns_update::TsigAlgorithm::Gss,
dns_update::TsigAlgorithm::HmacSha1 => dns_update::TsigAlgorithm::HmacSha1,
dns_update::TsigAlgorithm::HmacSha224 => dns_update::TsigAlgorithm::HmacSha224,
dns_update::TsigAlgorithm::HmacSha256 => dns_update::TsigAlgorithm::HmacSha256,
dns_update::TsigAlgorithm::HmacSha256_128 => dns_update::TsigAlgorithm::HmacSha256_128,
dns_update::TsigAlgorithm::HmacSha384 => dns_update::TsigAlgorithm::HmacSha384,
dns_update::TsigAlgorithm::HmacSha384_192 => dns_update::TsigAlgorithm::HmacSha384_192,
dns_update::TsigAlgorithm::HmacSha512 => dns_update::TsigAlgorithm::HmacSha512,
dns_update::TsigAlgorithm::HmacSha512_256 => dns_update::TsigAlgorithm::HmacSha512_256,
})
}
}
#[derive(Hash, PartialEq, Eq, Clone)]
pub struct Identifier(pub [u8; 256]);
impl Deref for Identifier {
type Target = [u8; 256];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Identifier {
pub fn to_base64(&self) -> String {
Base64::encode_string(&self.0)
}
}
pub enum Command {
CreateRequest(Box<CreateRequest>),
SignRequest(Box<SignRequest>),
}
#[derive(Clone)]
pub struct DNSRequest {
pub hostname: String,
pub time: Instant,
pub ssh_key: PublicKey,
// TODO: Handle Ipv6
pub requested_ip: Ipv4Addr,
pub host_ip: Ipv4Addr,
}
pub struct CreateRequest {
pub request: DNSRequest,
pub response_channel: oneshot::Sender<Identifier>,
}
pub struct SignRequest {
pub identifier: Identifier,
pub signed: SshSig,
pub time: Instant,
pub host_ip: Ipv4Addr,
pub response_channel: oneshot::Sender<Result<(), RequestError>>,
}
#[derive(Debug, Clone)]
pub enum RequestError {
HostIpMismatch,
RequestExpired,
InvalidSignature,
RequestedIpNotAllowed,
UpdateFailed,
AlreadyRegistered,
TooManyRegistrations,
}
pub async fn start_client(rx: mpsc::Receiver<Command>, config: &Config) -> JoinHandle<()> {
tokio::spawn(run_client(rx, config.clone()))
}
struct ClientState {
config: Config,
requests: HashMap<Identifier, DNSRequest>,
dns_updater: DnsUpdater,
registrations: Registrations,
}
async fn run_client(rx: mpsc::Receiver<Command>, config: Config) {
let mut rx = rx;
let requests = HashMap::new();
let dns_updater = get_dns_updater(&config);
let registrations = init_registrations(&config).await;
init_dns_registrations(&registrations, &config, &dns_updater).await;
let mut state = ClientState {
config,
requests,
dns_updater,
registrations,
};
loop {
match rx.recv().await {
Some(cmd) => handle_cmd(cmd, &mut state).await,
None => return,
};
}
}
async fn init_registrations(config: &Config) -> Registrations {
let path = config.data_dir.join("registrations.toml");
if !path.exists() {
return Registrations(HashMap::new());
}
let contents = tokio::fs::read_to_string(path).await.unwrap();
match toml::from_str(&contents) {
Ok(registrations) => registrations,
Err(e) => {
panic!("Failed to parse registrations file: {e}");
}
}
}
async fn init_dns_registrations(
registrations: &Registrations,
config: &Config,
updater: &DnsUpdater,
) {
for (hostname, registration) in registrations.0.iter() {
execute_dns_update(hostname, &registration.ip, config, updater)
.await
.unwrap();
}
}
async fn write_registrations(registrations: &Registrations, config: &Config) {
let path = config.data_dir.join("registrations.toml");
let contents = toml::to_string(registrations).unwrap();
tokio::fs::write(path, contents).await.unwrap();
}
fn get_dns_updater(config: &Config) -> DnsUpdater {
let addr = format!("tcp://{}", config.dns_server);
let key_name = &config.tsig_key_name;
let key = match std::fs::read_to_string(&config.tsig_key_file) {
Ok(contents) => Base64::decode_vec(&contents).unwrap(),
Err(e) => panic!("Failed to read TSIG key file: {e}"),
};
let algorithm = config.tsig_algorithm.clone().0;
info!("Creating DNS client for {addr}");
dns_update::DnsUpdater::new_rfc2136_tsig(addr, key_name, key, algorithm)
.expect("Failed to create DNS client")
}
async fn handle_cmd(cmd: Command, state: &mut ClientState) {
match cmd {
Command::CreateRequest(create_request) => {
handle_create_request(*create_request, state).await
}
Command::SignRequest(sign_request) => handle_sign_request(*sign_request, state).await,
}
}
async fn handle_create_request(request: CreateRequest, state: &mut ClientState) {
let random_value: [u8; 256] = StdRng::from_os_rng().random();
let identifier = Identifier(random_value);
state
.requests
.insert(identifier.clone(), request.request.clone());
let _ = request.response_channel.send(identifier);
info!(
"Registered host update request for {} to {} from {}.",
&request.request.hostname, &request.request.requested_ip, &request.request.host_ip
)
}
async fn handle_sign_request(request: SignRequest, state: &mut ClientState) {
let dns_request = match state.requests.remove(&request.identifier) {
Some(req) => req,
None => todo!(),
};
// The request must be submitted and signed by the same host
if dns_request.host_ip != request.host_ip {
warn!(
"Host IP mismatch for signed request for host {}: expected from {}, received from {}",
dns_request.hostname, dns_request.host_ip, request.host_ip
);
let _ = request
.response_channel
.send(Err(RequestError::HostIpMismatch));
return;
}
// The requested IP must be within the allowed networks
if !state
.config
.networks
.iter()
.any(|net| net.contains(&dns_request.requested_ip))
{
warn!(
"Requested IP {} for host {} is not allowed",
dns_request.requested_ip, dns_request.hostname
);
let _ = request
.response_channel
.send(Err(RequestError::RequestedIpNotAllowed));
return;
}
// The signed request must be submitted within 10 seconds of request creation
if request.time - dns_request.time > Duration::from_secs(10) {
warn!("Request expired: signed request is older than 10 seconds");
let _ = request
.response_channel
.send(Err(RequestError::RequestExpired));
return;
}
// Verify the SSH signature
let original_text = format!(
"{} {} {}",
request.identifier.to_base64(),
dns_request.hostname,
dns_request.requested_ip
);
match dns_request
.ssh_key
.verify("madd", original_text.as_bytes(), &request.signed)
{
Ok(_) => (),
Err(e) => {
warn!(
"Failed to verify SSH signature for host {}: {}",
dns_request.hostname, e
);
let _ = request
.response_channel
.send(Err(RequestError::InvalidSignature));
return;
}
}
// Check with the registrations if the hostname has not already been registered
if let Some(registration) = state.registrations.0.get(&dns_request.hostname)
&& registration.public_key.key_data() != dns_request.ssh_key.key_data()
{
warn!(
"Host {} attempted to register {} which has already been registered by a different host",
dns_request.host_ip, dns_request.hostname
);
let _ = request
.response_channel
.send(Err(RequestError::AlreadyRegistered));
return;
}
// Restrict the number of self-registrations per host
if !state.registrations.0.contains_key(&dns_request.hostname)
&& state
.registrations
.0
.iter()
.filter(|(_, registration)| {
registration.public_key.key_data() == dns_request.ssh_key.key_data()
})
.count()
>= state.config.registration_limit
{
warn!(
"Host {} attempted to register {} but has already reached the registration limit",
dns_request.host_ip, dns_request.hostname
);
let _ = request
.response_channel
.send(Err(RequestError::TooManyRegistrations));
return;
}
// Create the registration
state.registrations.0.insert(
dns_request.hostname.clone(),
Registration {
ip: dns_request.requested_ip,
public_key: dns_request.ssh_key,
},
);
write_registrations(&state.registrations, &state.config).await;
// Execute the DNS update request
match execute_dns_update(
&dns_request.hostname,
&dns_request.requested_ip,
&state.config,
&state.dns_updater,
)
.await
{
Ok(_) => (),
Err(e) => {
let _ = request.response_channel.send(Err(e));
return;
}
};
let _ = request.response_channel.send(Ok(()));
info!(
"Executed host update request for {} to {} from {}.",
&dns_request.hostname, &dns_request.requested_ip, &dns_request.host_ip
)
}
async fn execute_dns_update(
hostname: &String,
ip: &Ipv4Addr,
config: &Config,
updater: &DnsUpdater,
) -> Result<(), RequestError> {
let name = &format!("{}.{}", hostname, config.zone);
let record = dns_update::DnsRecord::A { content: *ip };
let ttl = 60;
let origin = &config.zone;
match updater
.delete(name, origin, dns_update::DnsRecordType::A)
.await
{
Ok(_) => (),
Err(e) => {
error!("Failed to delete existing DNS record: {e}");
return Err(RequestError::UpdateFailed);
}
};
match updater.create(name, record, ttl, origin).await {
Ok(_) => Ok(()),
Err(e) => {
error!("Failed to create DNS record: {e}");
Err(RequestError::UpdateFailed)
}
}
}

83
src/main.rs Normal file
View File

@ -0,0 +1,83 @@
use clap::Parser;
use dns::TsigAlgorithm;
use ipnet::Ipv4Net;
use log::{debug, info};
use serde::Deserialize;
use std::{net::SocketAddr, path::PathBuf};
use tokio::sync::mpsc;
mod api;
mod dns;
/// Machine Authenticated Dynamic DNS (MADD)
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// Path to configuration file
#[arg(short, long, default_value = "/etc/madd/madd.toml")]
config_file: PathBuf,
/// Log level (e.g., info, debug, error). Can also be set via the LOG_LEVEL environment variable
#[arg(short, long, default_value = "info")]
log_level: String,
}
fn get_args() -> Args {
Args::parse()
}
fn init_logging(args: &Args) {
let env = env_logger::Env::new()
.filter("LOG_LEVEL")
.default_filter_or(args.log_level.clone())
.write_style("LOG_STYLE");
env_logger::init_from_env(env);
}
#[derive(Deserialize, Debug, Clone)]
struct Config {
bind: SocketAddr,
zone: String,
networks: Vec<Ipv4Net>,
registration_limit: usize,
data_dir: PathBuf,
dns_server: SocketAddr,
tsig_key_name: String,
tsig_key_file: PathBuf,
tsig_algorithm: TsigAlgorithm,
}
fn get_config(args: &Args) -> Config {
let path = &args.config_file;
let file_contents = match std::fs::read_to_string(path) {
Ok(contents) => contents,
Err(e) => panic!(
"Failed to read config file from \"{}\": {e}",
path.display()
),
};
match toml::from_str(&file_contents) {
Ok(config) => config,
Err(e) => panic!("Failed to parse config file: {e}"),
}
}
#[tokio::main]
async fn main() {
// Parse command line arguments
let args = get_args();
// Initialize logging
init_logging(&args);
info!("Starting MADD");
// Gather configuration
let config = get_config(&args);
debug!("{config:?}");
// Start DNS client and API server
let (tx, rx) = mpsc::channel(16);
let handle_client = dns::start_client(rx, &config).await;
let handle_server = api::start_server(tx, &config).await;
let _ = (handle_client.await.unwrap(), handle_server.await.unwrap());
}