From cd0cd78e3070c5860ca60c2db9e3a975bff72efd Mon Sep 17 00:00:00 2001
From: John Smith <jsmith@example.com>
Date: Wed, 6 Jul 2022 23:15:51 -0400
Subject: [PATCH] eyre work

---
 Cargo.lock                                    |   1 +
 Cargo.toml                                    |   3 +
 veilid-core/src/attachment_manager.rs         |  33 +++--
 veilid-core/src/core_context.rs               |  10 +-
 veilid-core/src/dht/key.rs                    |  49 ++++---
 veilid-core/src/dht/tests/test_dht_key.rs     |  34 ++---
 veilid-core/src/lib.rs                        |   1 +
 veilid-core/src/network_manager/mod.rs        |   4 +-
 .../src/routing_table/stats_accounting.rs     |   2 +-
 veilid-core/src/rpc_processor/debug.rs        |  28 ++--
 .../src/tests/common/test_veilid_config.rs    |   2 +-
 veilid-core/src/veilid_api/debug.rs           |  30 ++--
 veilid-core/src/veilid_api/mod.rs             | 130 +++++++++++++++++-
 veilid-core/src/veilid_config.rs              |  86 +++++++-----
 veilid-server/Cargo.toml                      |   1 +
 veilid-server/src/cmdline.rs                  |  38 +++--
 veilid-server/src/main.rs                     |   7 +-
 veilid-server/src/server.rs                   |  24 ++--
 veilid-server/src/settings.rs                 |  49 ++++---
 veilid-server/src/unix.rs                     |  21 ++-
 veilid-server/src/veilid_logs.rs              |  21 ++-
 21 files changed, 345 insertions(+), 229 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index ae43417e..9e28534c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5047,6 +5047,7 @@ dependencies = [
  "capnpc",
  "cfg-if 1.0.0",
  "clap 3.2.8",
+ "color-eyre",
  "config",
  "ctrlc",
  "daemonize",
diff --git a/Cargo.toml b/Cargo.toml
index 733f27c0..3ca2a512 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,3 +30,6 @@ rtnetlink = { path = "./external/netlink/rtnetlink" }
 [profile.release]
 opt-level = "s"
 lto = true
+
+[profile.dev.package.backtrace]
+opt-level = 3
\ No newline at end of file
diff --git a/veilid-core/src/attachment_manager.rs b/veilid-core/src/attachment_manager.rs
index cc142732..48576901 100644
--- a/veilid-core/src/attachment_manager.rs
+++ b/veilid-core/src/attachment_manager.rs
@@ -222,26 +222,29 @@ impl AttachmentManager {
 
     #[instrument(level = "debug", skip(self))]
     async fn attachment_maintainer(self) {
-        trace!("attachment starting");
+        debug!("attachment starting");
         let netman = {
             let mut inner = self.inner.lock();
             inner.attach_timestamp = Some(intf::get_timestamp());
             inner.network_manager.clone()
         };
 
-        let mut started = true;
-        if let Err(err) = netman.startup().await {
-            error!("network startup failed: {}", err);
-            started = false;
-        }
+        let mut restart;
+        loop {
+            restart = false;
+            if let Err(err) = netman.startup().await {
+                error!("network startup failed: {}", err);
+                netman.shutdown().await;
+                break;
+            }
 
-        if started {
-            trace!("started maintaining peers");
+            debug!("started maintaining peers");
             while self.inner.lock().maintain_peers {
                 // tick network manager
                 if let Err(err) = netman.tick().await {
                     error!("Error in network manager: {}", err);
                     self.inner.lock().maintain_peers = false;
+                    restart = true;
                     break;
                 }
 
@@ -250,10 +253,18 @@ impl AttachmentManager {
                 // sleep should be at the end in case maintain_peers changes state
                 intf::sleep(1000).await;
             }
-            trace!("stopped maintaining peers");
+            debug!("stopped maintaining peers");
 
-            trace!("stopping network");
+            debug!("stopping network");
             netman.shutdown().await;
+
+            if !restart {
+                break;
+            }
+
+            debug!("completely restarting attachment");
+            // chill out for a second first, give network stack time to settle out
+            intf::sleep(1000).await;
         }
 
         trace!("stopping attachment");
@@ -261,7 +272,7 @@ impl AttachmentManager {
         let _output = attachment_machine
             .consume(&AttachmentInput::AttachmentStopped)
             .await;
-        trace!("attachment stopped");
+        debug!("attachment stopped");
         self.inner.lock().attach_timestamp = None;
     }
 
diff --git a/veilid-core/src/core_context.rs b/veilid-core/src/core_context.rs
index 1c0d8f76..7647fb37 100644
--- a/veilid-core/src/core_context.rs
+++ b/veilid-core/src/core_context.rs
@@ -76,7 +76,7 @@ impl ServicesContext {
         // Init node id from config now that protected store is set up
         if let Err(e) = self.config.init_node_id(protected_store.clone()).await {
             self.shutdown().await;
-            return Err(VeilidAPIError::Internal { message: e });
+            return Err(e);
         }
 
         // Set up tablestore
@@ -177,9 +177,7 @@ impl VeilidCoreContext {
         // Set up config from callback
         trace!("setup config with callback");
         let mut config = VeilidConfig::new();
-        if let Err(e) = config.setup(config_callback) {
-            return Err(VeilidAPIError::Internal { message: e });
-        }
+        config.setup(config_callback)?;
 
         Self::new_common(update_callback, config).await
     }
@@ -192,9 +190,7 @@ impl VeilidCoreContext {
         // Set up config from callback
         trace!("setup config with json");
         let mut config = VeilidConfig::new();
-        if let Err(e) = config.setup_from_json(config_json) {
-            return Err(VeilidAPIError::Internal { message: e });
-        }
+        config.setup_from_json(config_json)?;
         Self::new_common(update_callback, config).await
     }
 
diff --git a/veilid-core/src/dht/key.rs b/veilid-core/src/dht/key.rs
index 7d3ff345..ea890a5a 100644
--- a/veilid-core/src/dht/key.rs
+++ b/veilid-core/src/dht/key.rs
@@ -1,17 +1,18 @@
+use crate::veilid_rng::*;
 use crate::xx::*;
+use crate::*;
+
 use core::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd};
 use core::convert::{TryFrom, TryInto};
 use core::fmt;
 use core::hash::{Hash, Hasher};
 
-use crate::veilid_rng::*;
-use ed25519_dalek::{Keypair, PublicKey, Signature};
-use serde::{Deserialize, Serialize};
-
 use data_encoding::BASE64URL_NOPAD;
 use digest::generic_array::typenum::U64;
 use digest::{Digest, Output};
+use ed25519_dalek::{Keypair, PublicKey, Signature};
 use generic_array::GenericArray;
+use serde::{Deserialize, Serialize};
 
 //////////////////////////////////////////////////////////////////////
 
@@ -71,14 +72,14 @@ macro_rules! byte_array_type {
                 Self { bytes, valid: true }
             }
 
-            pub fn try_from_vec(v: Vec<u8>) -> Result<Self, String> {
+            pub fn try_from_vec(v: Vec<u8>) -> Result<Self, VeilidAPIError> {
                 let mut this = Self {
                     bytes: [0u8; $size],
                     valid: true,
                 };
 
                 if v.len() != $size {
-                    return Err(format!(
+                    apibail_generic!(format!(
                         "Expected a Vec of length {} but it was {}",
                         $size,
                         v.len()
@@ -139,25 +140,25 @@ macro_rules! byte_array_type {
                 BASE64URL_NOPAD.encode(&self.bytes)
             }
 
-            pub fn try_decode(input: &str) -> Result<Self, String> {
+            pub fn try_decode(input: &str) -> Result<Self, VeilidAPIError> {
                 let mut bytes = [0u8; $size];
 
                 let res = BASE64URL_NOPAD.decode_len(input.len());
                 match res {
                     Ok(v) => {
                         if v != $size {
-                            return Err("Incorrect length in decode".to_owned());
+                            apibail_generic!("Incorrect length in decode");
                         }
                     }
                     Err(_) => {
-                        return Err("Failed to decode".to_owned());
+                        apibail_generic!("Failed to decode");
                     }
                 }
 
                 let res = BASE64URL_NOPAD.decode_mut(input.as_bytes(), &mut bytes);
                 match res {
                     Ok(_) => Ok(Self::new(bytes)),
-                    Err(_) => Err("Failed to decode".to_owned()),
+                    Err(_) => apierr_generic!("Failed to decode"),
                 }
             }
         }
@@ -255,28 +256,28 @@ macro_rules! byte_array_type {
         }
 
         impl TryFrom<String> for $name {
-            type Error = String;
+            type Error = VeilidAPIError;
             fn try_from(value: String) -> Result<Self, Self::Error> {
                 $name::try_from(value.as_str())
             }
         }
 
         impl TryFrom<&str> for $name {
-            type Error = String;
+            type Error = VeilidAPIError;
             fn try_from(value: &str) -> Result<Self, Self::Error> {
                 let mut out = $name::default();
                 if value == "" {
                     return Ok(out);
                 }
                 if value.len() != ($size * 2) {
-                    return Err(concat!(stringify!($name), " is incorrect length").to_owned());
+                    apibail_generic!(concat!(stringify!($name), " is incorrect length"));
                 }
                 match hex::decode_to_slice(value, &mut out.bytes) {
                     Ok(_) => {
                         out.valid = true;
                         Ok(out)
                     }
-                    Err(err) => Err(format!("{}", err)),
+                    Err(err) => apierr_generic!(err),
                 }
             }
         }
@@ -372,7 +373,7 @@ pub fn sign(
     dht_key: &DHTKey,
     dht_key_secret: &DHTKeySecret,
     data: &[u8],
-) -> Result<DHTSignature, String> {
+) -> Result<DHTSignature, VeilidAPIError> {
     assert!(dht_key.valid);
     assert!(dht_key_secret.valid);
 
@@ -381,32 +382,36 @@ pub fn sign(
 
     kpb[..DHT_KEY_SECRET_LENGTH].copy_from_slice(&dht_key_secret.bytes);
     kpb[DHT_KEY_SECRET_LENGTH..].copy_from_slice(&dht_key.bytes);
-    let keypair = Keypair::from_bytes(&kpb).map_err(|_| "Keypair is invalid".to_owned())?;
+    let keypair = Keypair::from_bytes(&kpb).map_err(mapapierr_parse!("Keypair is invalid"))?;
 
     let mut dig = Blake3Digest512::new();
     dig.update(data);
 
     let sig = keypair
         .sign_prehashed(dig, None)
-        .map_err(|_| "Signature failed".to_owned())?;
+        .map_err(VeilidAPIError::internal)?;
 
     let dht_sig = DHTSignature::new(sig.to_bytes());
     Ok(dht_sig)
 }
 
-pub fn verify(dht_key: &DHTKey, data: &[u8], signature: &DHTSignature) -> Result<(), String> {
+pub fn verify(
+    dht_key: &DHTKey,
+    data: &[u8],
+    signature: &DHTSignature,
+) -> Result<(), VeilidAPIError> {
     assert!(dht_key.valid);
     assert!(signature.valid);
     let pk =
-        PublicKey::from_bytes(&dht_key.bytes).map_err(|_| "Public key is invalid".to_owned())?;
-    let sig =
-        Signature::from_bytes(&signature.bytes).map_err(|_| "Signature is invalid".to_owned())?;
+        PublicKey::from_bytes(&dht_key.bytes).map_err(mapapierr_parse!("Public key is invalid"))?;
+    let sig = Signature::from_bytes(&signature.bytes)
+        .map_err(mapapierr_parse!("Signature is invalid"))?;
 
     let mut dig = Blake3Digest512::new();
     dig.update(data);
 
     pk.verify_prehashed(dig, None, &sig)
-        .map_err(|_| "Verification failed".to_owned())?;
+        .map_err(mapapierr_parse!("Verification failed"))?;
     Ok(())
 }
 
diff --git a/veilid-core/src/dht/tests/test_dht_key.rs b/veilid-core/src/dht/tests/test_dht_key.rs
index 643e5c25..0d6eee4e 100644
--- a/veilid-core/src/dht/tests/test_dht_key.rs
+++ b/veilid-core/src/dht/tests/test_dht_key.rs
@@ -57,14 +57,8 @@ pub async fn test_sign_and_verify() {
 
     assert_eq!(key::verify(&dht_key, LOREM_IPSUM.as_bytes(), &a1), Ok(()));
     assert_eq!(key::verify(&dht_key2, LOREM_IPSUM.as_bytes(), &a2), Ok(()));
-    assert_eq!(
-        key::verify(&dht_key, LOREM_IPSUM.as_bytes(), &b1),
-        Err("Verification failed".to_owned())
-    );
-    assert_eq!(
-        key::verify(&dht_key2, LOREM_IPSUM.as_bytes(), &b2),
-        Err("Verification failed".to_owned())
-    );
+    assert!(key::verify(&dht_key, LOREM_IPSUM.as_bytes(), &b1).is_err());
+    assert!(key::verify(&dht_key2, LOREM_IPSUM.as_bytes(), &b2).is_err());
 
     // Try verifications that should work
     assert_eq!(
@@ -84,22 +78,10 @@ pub async fn test_sign_and_verify() {
         Ok(())
     );
     // Try verifications that shouldn't work
-    assert_eq!(
-        key::verify(&dht_key2, LOREM_IPSUM.as_bytes(), &dht_sig),
-        Err("Verification failed".to_owned())
-    );
-    assert_eq!(
-        key::verify(&dht_key, LOREM_IPSUM.as_bytes(), &dht_sig2),
-        Err("Verification failed".to_owned())
-    );
-    assert_eq!(
-        key::verify(&dht_key2, CHEEZBURGER.as_bytes(), &dht_sig_c),
-        Err("Verification failed".to_owned())
-    );
-    assert_eq!(
-        key::verify(&dht_key, CHEEZBURGER.as_bytes(), &dht_sig),
-        Err("Verification failed".to_owned())
-    );
+    assert!(key::verify(&dht_key2, LOREM_IPSUM.as_bytes(), &dht_sig).is_err());
+    assert!(key::verify(&dht_key, LOREM_IPSUM.as_bytes(), &dht_sig2).is_err());
+    assert!(key::verify(&dht_key2, CHEEZBURGER.as_bytes(), &dht_sig_c).is_err());
+    assert!(key::verify(&dht_key, CHEEZBURGER.as_bytes(), &dht_sig).is_err());
 }
 
 pub async fn test_key_conversions() {
@@ -214,9 +196,9 @@ pub async fn test_encode_decode() {
 
     // Failures
     let f1 = key::DHTKeySecret::try_decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
-    assert_eq!(f1, Err("Incorrect length in decode".to_owned()));
+    assert!(f1.is_err());
     let f2 = key::DHTKeySecret::try_decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&");
-    assert_eq!(f2, Err("Failed to decode".to_owned()));
+    assert!(f2.is_err());
 }
 
 async fn test_hash() {
diff --git a/veilid-core/src/lib.rs b/veilid-core/src/lib.rs
index c861d556..4820c9dc 100644
--- a/veilid-core/src/lib.rs
+++ b/veilid-core/src/lib.rs
@@ -29,6 +29,7 @@ mod receipt_manager;
 mod routing_table;
 mod rpc_processor;
 mod veilid_api;
+#[macro_use]
 mod veilid_config;
 mod veilid_layer_filter;
 mod veilid_rng;
diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs
index 3668630e..c945ed08 100644
--- a/veilid-core/src/network_manager/mod.rs
+++ b/veilid-core/src/network_manager/mod.rs
@@ -1227,8 +1227,8 @@ impl NetworkManager {
                 // pinging this node regularly to keep itself in the routing table
                 routing_table.lookup_node_ref(recipient_id).ok_or_else(|| {
                     format!(
-                        "Inbound relay asked for recipient not in routing table: {}",
-                        recipient_id
+                        "Inbound relay asked for recipient not in routing table: sender_id={:?} recipient={:?}",
+                        sender_id, recipient_id
                     )
                 })?
             };
diff --git a/veilid-core/src/routing_table/stats_accounting.rs b/veilid-core/src/routing_table/stats_accounting.rs
index c99b7327..7ef69bd6 100644
--- a/veilid-core/src/routing_table/stats_accounting.rs
+++ b/veilid-core/src/routing_table/stats_accounting.rs
@@ -98,7 +98,7 @@ impl LatencyStatsAccounting {
         self.rolling_latencies.push_back(latency);
 
         let mut ls = LatencyStats {
-            fastest: 0,
+            fastest: u64::MAX,
             average: 0,
             slowest: 0,
         };
diff --git a/veilid-core/src/rpc_processor/debug.rs b/veilid-core/src/rpc_processor/debug.rs
index 62766d5e..27780e1c 100644
--- a/veilid-core/src/rpc_processor/debug.rs
+++ b/veilid-core/src/rpc_processor/debug.rs
@@ -10,17 +10,20 @@ pub enum RPCError {
     Internal(String),
 }
 
-pub fn rpc_error_internal<T: AsRef<str>>(x: T) -> RPCError {
-    error!("RPCError Internal: {}", x.as_ref());
-    RPCError::Internal(x.as_ref().to_owned())
+pub fn rpc_error_internal<T: ToString>(x: T) -> RPCError {
+    let x = x.to_string();
+    error!("RPCError Internal: {}", x);
+    RPCError::Internal(x)
 }
-pub fn rpc_error_invalid_format<T: AsRef<str>>(x: T) -> RPCError {
-    error!("RPCError Invalid Format: {}", x.as_ref());
-    RPCError::InvalidFormat(x.as_ref().to_owned())
+pub fn rpc_error_invalid_format<T: ToString>(x: T) -> RPCError {
+    let x = x.to_string();
+    error!("RPCError Invalid Format: {}", x);
+    RPCError::InvalidFormat(x)
 }
-pub fn rpc_error_protocol<T: AsRef<str>>(x: T) -> RPCError {
-    error!("RPCError Protocol: {}", x.as_ref());
-    RPCError::Protocol(x.as_ref().to_owned())
+pub fn rpc_error_protocol<T: ToString>(x: T) -> RPCError {
+    let x = x.to_string();
+    error!("RPCError Protocol: {}", x);
+    RPCError::Protocol(x)
 }
 pub fn rpc_error_capnp_error(e: capnp::Error) -> RPCError {
     error!("RPCError Protocol: capnp error: {}", &e.description);
@@ -30,9 +33,10 @@ pub fn rpc_error_capnp_notinschema(e: capnp::NotInSchema) -> RPCError {
     error!("RPCError Protocol: not in schema: {}", &e.0);
     RPCError::Protocol(format!("not in schema: {}", &e.0))
 }
-pub fn rpc_error_unimplemented<T: AsRef<str>>(x: T) -> RPCError {
-    error!("RPCError Unimplemented: {}", x.as_ref());
-    RPCError::Unimplemented(x.as_ref().to_owned())
+pub fn rpc_error_unimplemented<T: ToString>(x: T) -> RPCError {
+    let x = x.to_string();
+    error!("RPCError Unimplemented: {}", x);
+    RPCError::Unimplemented(x)
 }
 
 impl fmt::Display for RPCError {
diff --git a/veilid-core/src/tests/common/test_veilid_config.rs b/veilid-core/src/tests/common/test_veilid_config.rs
index 3abf45c4..9b88206f 100644
--- a/veilid-core/src/tests/common/test_veilid_config.rs
+++ b/veilid-core/src/tests/common/test_veilid_config.rs
@@ -260,7 +260,7 @@ fn config_callback(key: String) -> ConfigCallbackReturn {
         _ => {
             let err = format!("config key '{}' doesn't exist", key);
             debug!("{}", err);
-            Err(err)
+            apierr_internal!(err)
         }
     }
 }
diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs
index f24b8d52..259f6947 100644
--- a/veilid-core/src/veilid_api/debug.rs
+++ b/veilid-core/src/veilid_api/debug.rs
@@ -135,18 +135,14 @@ impl VeilidAPI {
         let config = self.config()?;
         let args = args.trim_start();
         if args.is_empty() {
-            return config
-                .get_key_json("")
-                .map_err(|e| VeilidAPIError::Internal { message: e });
+            return config.get_key_json("");
         }
         let (arg, rest) = args.split_once(' ').unwrap_or((args, ""));
         let rest = rest.trim_start().to_owned();
 
         // One argument is 'config get'
         if rest.is_empty() {
-            return config
-                .get_key_json(arg)
-                .map_err(|e| VeilidAPIError::Internal { message: e });
+            return config.get_key_json(arg);
         }
 
         // More than one argument is 'config set'
@@ -156,15 +152,11 @@ impl VeilidAPI {
             self.get_state().await?.attachment.state,
             AttachmentState::Detached
         ) {
-            return Err(VeilidAPIError::Internal {
-                message: "Must be detached to change config".to_owned(),
-            });
+            apibail_internal!("Must be detached to change config");
         }
 
         // Change the config key
-        config
-            .set_key_json(arg, &rest)
-            .map_err(|e| VeilidAPIError::Internal { message: e })?;
+        config.set_key_json(arg, &rest)?;
         Ok("Config value set".to_owned())
     }
 
@@ -177,9 +169,7 @@ impl VeilidAPI {
                     self.get_state().await?.attachment.state,
                     AttachmentState::Detached | AttachmentState::Detaching
                 ) {
-                    return Err(VeilidAPIError::Internal {
-                        message: "Must be detached to purge".to_owned(),
-                    });
+                    apibail_internal!("Must be detached to purge");
                 }
                 self.network_manager()?.routing_table().purge();
                 Ok("Buckets purged".to_owned())
@@ -203,10 +193,8 @@ impl VeilidAPI {
             self.get_state().await?.attachment.state,
             AttachmentState::Detached
         ) {
-            return Err(VeilidAPIError::Internal {
-                message: "Not detached".to_owned(),
-            });
-        };
+            apibail_internal!("Not detached");
+        }
 
         self.attach().await?;
 
@@ -218,9 +206,7 @@ impl VeilidAPI {
             self.get_state().await?.attachment.state,
             AttachmentState::Detaching
         ) {
-            return Err(VeilidAPIError::Internal {
-                message: "Not attached".to_owned(),
-            });
+            apibail_internal!("Not attached");
         };
 
         self.detach().await?;
diff --git a/veilid-core/src/veilid_api/mod.rs b/veilid-core/src/veilid_api/mod.rs
index 59903d7d..025c5852 100644
--- a/veilid-core/src/veilid_api/mod.rs
+++ b/veilid-core/src/veilid_api/mod.rs
@@ -31,6 +31,62 @@ use xx::*;
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////
 
+#[allow(unused_macros)]
+#[macro_export]
+macro_rules! apierr_generic {
+    ($x:expr) => {
+        Err(VeilidAPIError::generic($x))
+    };
+}
+
+#[allow(unused_macros)]
+#[macro_export]
+macro_rules! apierr_internal {
+    ($x:expr) => {
+        Err(VeilidAPIError::internal($x))
+    };
+}
+
+#[allow(unused_macros)]
+#[macro_export]
+macro_rules! apierr_parse {
+    ($x:expr, $y:expr) => {
+        Err(VeilidAPIError::parse_error($x, $y))
+    };
+}
+
+#[allow(unused_macros)]
+#[macro_export]
+macro_rules! mapapierr_parse {
+    ($x:expr) => {
+        |e| VeilidAPIError::parse_error($x, e)
+    };
+}
+
+#[allow(unused_macros)]
+#[macro_export]
+macro_rules! apibail_generic {
+    ($x:expr) => {
+        return Err(VeilidAPIError::generic($x));
+    };
+}
+
+#[allow(unused_macros)]
+#[macro_export]
+macro_rules! apibail_internal {
+    ($x:expr) => {
+        return Err(VeilidAPIError::internal($x));
+    };
+}
+
+#[allow(unused_macros)]
+#[macro_export]
+macro_rules! apibail_parse {
+    ($x:expr, $y:expr) => {
+        return Err(VeilidAPIError::parse_error($x, $y));
+    };
+}
+
 #[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Ord, Serialize, Deserialize)]
 #[serde(tag = "kind")]
 pub enum VeilidAPIError {
@@ -66,8 +122,63 @@ pub enum VeilidAPIError {
         context: String,
         argument: String,
     },
+    Generic {
+        message: String,
+    },
 }
 
+impl VeilidAPIError {
+    pub fn node_not_found(node_id: NodeId) -> Self {
+        Self::NodeNotFound { node_id }
+    }
+    pub fn no_dial_info(node_id: NodeId) -> Self {
+        Self::NoDialInfo { node_id }
+    }
+    pub fn no_peer_info(node_id: NodeId) -> Self {
+        Self::NoPeerInfo { node_id }
+    }
+    pub fn internal<T: ToString>(msg: T) -> Self {
+        Self::Internal {
+            message: msg.to_string(),
+        }
+    }
+    pub fn unimplemented<T: ToString>(msg: T) -> Self {
+        Self::Unimplemented {
+            message: msg.to_string(),
+        }
+    }
+    pub fn parse_error<T: ToString, S: ToString>(msg: T, value: S) -> Self {
+        Self::ParseError {
+            message: msg.to_string(),
+            value: value.to_string(),
+        }
+    }
+    pub fn invalid_argument<T: ToString, S: ToString, R: ToString>(
+        context: T,
+        argument: S,
+        value: R,
+    ) -> Self {
+        Self::InvalidArgument {
+            context: context.to_string(),
+            argument: argument.to_string(),
+            value: value.to_string(),
+        }
+    }
+    pub fn missing_argument<T: ToString, S: ToString>(context: T, argument: S) -> Self {
+        Self::MissingArgument {
+            context: context.to_string(),
+            argument: argument.to_string(),
+        }
+    }
+    pub fn generic<T: ToString>(msg: T) -> Self {
+        Self::Generic {
+            message: msg.to_string(),
+        }
+    }
+}
+
+impl std::error::Error for VeilidAPIError {}
+
 impl fmt::Display for VeilidAPIError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
         match self {
@@ -111,6 +222,9 @@ impl fmt::Display for VeilidAPIError {
                     context, argument
                 )
             }
+            VeilidAPIError::Generic { message } => {
+                write!(f, "VeilidAPIError::Generic({})", message)
+            }
         }
     }
 }
@@ -1321,9 +1435,11 @@ impl SignedNodeInfo {
         node_id: NodeId,
         signature: DHTSignature,
         timestamp: u64,
-    ) -> Result<Self, String> {
-        let mut node_info_bytes = serde_cbor::to_vec(&node_info).map_err(map_to_string)?;
-        let mut timestamp_bytes = serde_cbor::to_vec(&timestamp).map_err(map_to_string)?;
+    ) -> Result<Self, VeilidAPIError> {
+        let mut node_info_bytes = serde_cbor::to_vec(&node_info)
+            .map_err(mapapierr_parse!("failed to encode node info as cbor"))?;
+        let mut timestamp_bytes = serde_cbor::to_vec(&timestamp)
+            .map_err(mapapierr_parse!("failed to encode timestamp as cbor"))?;
 
         node_info_bytes.append(&mut timestamp_bytes);
 
@@ -1339,11 +1455,13 @@ impl SignedNodeInfo {
         node_info: NodeInfo,
         node_id: NodeId,
         secret: &DHTKeySecret,
-    ) -> Result<Self, String> {
+    ) -> Result<Self, VeilidAPIError> {
         let timestamp = intf::get_timestamp();
 
-        let mut node_info_bytes = serde_cbor::to_vec(&node_info).map_err(map_to_string)?;
-        let mut timestamp_bytes = serde_cbor::to_vec(&timestamp).map_err(map_to_string)?;
+        let mut node_info_bytes = serde_cbor::to_vec(&node_info)
+            .map_err(mapapierr_parse!("failed to encode node info as cbor"))?;
+        let mut timestamp_bytes = serde_cbor::to_vec(&timestamp)
+            .map_err(mapapierr_parse!("failed to encode timestamp as cbor"))?;
 
         node_info_bytes.append(&mut timestamp_bytes);
 
diff --git a/veilid-core/src/veilid_config.rs b/veilid-core/src/veilid_config.rs
index 3132cb79..304cb51c 100644
--- a/veilid-core/src/veilid_config.rs
+++ b/veilid-core/src/veilid_config.rs
@@ -5,11 +5,11 @@ use serde::*;
 ////////////////////////////////////////////////////////////////////////////////////////////////
 cfg_if! {
     if #[cfg(target_arch = "wasm32")] {
-        pub type ConfigCallbackReturn = Result<Box<dyn core::any::Any>, String>;
+        pub type ConfigCallbackReturn = Result<Box<dyn core::any::Any>, VeilidAPIError>;
         pub type ConfigCallback = Arc<dyn Fn(String) -> ConfigCallbackReturn>;
 
     } else {
-        pub type ConfigCallbackReturn = Result<Box<dyn core::any::Any + Send>, String>;
+        pub type ConfigCallbackReturn = Result<Box<dyn core::any::Any + Send>, VeilidAPIError>;
         pub type ConfigCallback = Arc<dyn Fn(String) -> ConfigCallbackReturn + Send + Sync>;
     }
 }
@@ -281,10 +281,10 @@ impl VeilidConfig {
         }
     }
 
-    pub fn setup_from_json(&mut self, config: String) -> Result<(), String> {
+    pub fn setup_from_json(&mut self, config: String) -> Result<(), VeilidAPIError> {
         {
             let mut inner = self.inner.write();
-            *inner = serde_json::from_str(&config).map_err(map_to_string)?;
+            *inner = serde_json::from_str(&config).map_err(VeilidAPIError::generic)?;
         }
 
         // Validate settings
@@ -293,14 +293,14 @@ impl VeilidConfig {
         Ok(())
     }
 
-    pub fn setup(&mut self, cb: ConfigCallback) -> Result<(), String> {
+    pub fn setup(&mut self, cb: ConfigCallback) -> Result<(), VeilidAPIError> {
         macro_rules! get_config {
             ($key:expr) => {
                 let keyname = &stringify!($key)[6..];
                 $key = *cb(keyname.to_owned())?.downcast().map_err(|_| {
-                    let err = format!("incorrect type for key: {}", keyname);
+                    let err = format!("incorrect type for key {}", keyname);
                     debug!("{}", err);
-                    err
+                    VeilidAPIError::generic(err)
                 })?;
             };
         }
@@ -411,12 +411,12 @@ impl VeilidConfig {
         self.inner.write()
     }
 
-    pub fn get_key_json(&self, key: &str) -> Result<String, String> {
+    pub fn get_key_json(&self, key: &str) -> Result<String, VeilidAPIError> {
         let c = self.get();
 
         // Generate json from whole config
-        let jc = serde_json::to_string(&*c).map_err(map_to_string)?;
-        let jvc = json::parse(&jc).map_err(map_to_string)?;
+        let jc = serde_json::to_string(&*c).map_err(VeilidAPIError::generic)?;
+        let jvc = json::parse(&jc).map_err(VeilidAPIError::generic)?;
 
         // Find requested subkey
         if key.is_empty() {
@@ -427,25 +427,25 @@ impl VeilidConfig {
             let mut out = &jvc;
             for k in keypath {
                 if !out.has_key(k) {
-                    return Err(format!("invalid subkey '{}' in key '{}'", k, key));
+                    apibail_parse!(format!("invalid subkey in key '{}'", key), k);
                 }
                 out = &out[k];
             }
             Ok(out.to_string())
         }
     }
-    pub fn set_key_json(&self, key: &str, value: &str) -> Result<(), String> {
+    pub fn set_key_json(&self, key: &str, value: &str) -> Result<(), VeilidAPIError> {
         let mut c = self.get_mut();
 
         // Split key into path parts
         let keypath: Vec<&str> = key.split('.').collect();
 
         // Convert value into jsonvalue
-        let newval = json::parse(value).map_err(map_to_string)?;
+        let newval = json::parse(value).map_err(VeilidAPIError::generic)?;
 
         // Generate json from whole config
-        let jc = serde_json::to_string(&*c).map_err(map_to_string)?;
-        let mut jvc = json::parse(&jc).map_err(map_to_string)?;
+        let jc = serde_json::to_string(&*c).map_err(VeilidAPIError::generic)?;
+        let mut jvc = json::parse(&jc).map_err(VeilidAPIError::generic)?;
 
         // Find requested subkey
         let newconfigstring = if let Some((objkeyname, objkeypath)) = keypath.split_last() {
@@ -453,12 +453,12 @@ impl VeilidConfig {
             let mut out = &mut jvc;
             for k in objkeypath {
                 if !out.has_key(*k) {
-                    return Err(format!("invalid subkey '{}' in key '{}'", *k, key));
+                    apibail_parse!(format!("invalid subkey in key '{}'", key), k);
                 }
                 out = &mut out[*k];
             }
             if !out.has_key(objkeyname) {
-                return Err(format!("invalid subkey '{}' in key '{}'", objkeyname, key));
+                apibail_parse!(format!("invalid subkey in key '{}'", key), objkeyname);
             }
             out[*objkeyname] = newval;
             jvc.to_string()
@@ -473,11 +473,11 @@ impl VeilidConfig {
         Ok(())
     }
 
-    fn validate(&self) -> Result<(), String> {
+    fn validate(&self) -> Result<(), VeilidAPIError> {
         let inner = self.inner.read();
 
         if inner.program_name.is_empty() {
-            return Err("Program name must not be empty in 'program_name'".to_owned());
+            apibail_generic!("Program name must not be empty in 'program_name'");
         }
 
         // if inner.network.protocol.udp.enabled {
@@ -486,29 +486,29 @@ impl VeilidConfig {
         if inner.network.protocol.tcp.listen {
             // Validate TCP settings
             if inner.network.protocol.tcp.max_connections == 0 {
-                return Err("TCP max connections must be > 0 in config key 'network.protocol.tcp.max_connections'".to_owned());
+                apibail_generic!("TCP max connections must be > 0 in config key 'network.protocol.tcp.max_connections'");
             }
         }
         if inner.network.protocol.ws.listen {
             // Validate WS settings
             if inner.network.protocol.ws.max_connections == 0 {
-                return Err("WS max connections must be > 0 in config key 'network.protocol.ws.max_connections'".to_owned());
+                apibail_generic!("WS max connections must be > 0 in config key 'network.protocol.ws.max_connections'");
             }
             if inner.network.application.https.enabled
                 && inner.network.application.https.path == inner.network.protocol.ws.path
             {
-                return Err("WS path conflicts with HTTPS application path in config key 'network.protocol.ws.path'".to_owned());
+                apibail_generic!("WS path conflicts with HTTPS application path in config key 'network.protocol.ws.path'");
             }
             if inner.network.application.http.enabled
                 && inner.network.application.http.path == inner.network.protocol.ws.path
             {
-                return Err("WS path conflicts with HTTP application path in config key 'network.protocol.ws.path'".to_owned());
+                apibail_generic!("WS path conflicts with HTTP application path in config key 'network.protocol.ws.path'");
             }
         }
         if inner.network.protocol.wss.listen {
             // Validate WSS settings
             if inner.network.protocol.wss.max_connections == 0 {
-                return Err("WSS max connections must be > 0 in config key 'network.protocol.wss.max_connections'".to_owned());
+                apibail_generic!("WSS max connections must be > 0 in config key 'network.protocol.wss.max_connections'");
             }
             if inner
                 .network
@@ -519,19 +519,19 @@ impl VeilidConfig {
                 .map(|u| u.is_empty())
                 .unwrap_or_default()
             {
-                return Err(
-                    "WSS URL must be specified in config key 'network.protocol.wss.url'".to_owned(),
+                apibail_generic!(
+                    "WSS URL must be specified in config key 'network.protocol.wss.url'"
                 );
             }
             if inner.network.application.https.enabled
                 && inner.network.application.https.path == inner.network.protocol.wss.path
             {
-                return Err("WSS path conflicts with HTTPS application path in config key 'network.protocol.ws.path'".to_owned());
+                apibail_generic!("WSS path conflicts with HTTPS application path in config key 'network.protocol.ws.path'");
             }
             if inner.network.application.http.enabled
                 && inner.network.application.http.path == inner.network.protocol.wss.path
             {
-                return Err("WSS path conflicts with HTTP application path in config key 'network.protocol.ws.path'".to_owned());
+                apibail_generic!("WSS path conflicts with HTTP application path in config key 'network.protocol.ws.path'");
             }
         }
         if inner.network.application.https.enabled {
@@ -545,9 +545,8 @@ impl VeilidConfig {
                 .map(|u| u.is_empty())
                 .unwrap_or_default()
             {
-                return Err(
+                apibail_generic!(
                     "HTTPS URL must be specified in config key 'network.application.https.url'"
-                        .to_owned(),
                 );
             }
         }
@@ -556,15 +555,22 @@ impl VeilidConfig {
 
     // Get the node id from config if one is specified
     // Must be done -after- protected store startup
-    pub async fn init_node_id(&self, protected_store: intf::ProtectedStore) -> Result<(), String> {
+    pub async fn init_node_id(
+        &self,
+        protected_store: intf::ProtectedStore,
+    ) -> Result<(), VeilidAPIError> {
         let mut node_id = self.inner.read().network.node_id;
         let mut node_id_secret = self.inner.read().network.node_id_secret;
         // See if node id was previously stored in the protected store
         if !node_id.valid {
             debug!("pulling node id from storage");
-            if let Some(s) = protected_store.load_user_secret_string("node_id").await? {
+            if let Some(s) = protected_store
+                .load_user_secret_string("node_id")
+                .await
+                .map_err(VeilidAPIError::internal)?
+            {
                 debug!("node id found in storage");
-                node_id = DHTKey::try_decode(s.as_str())?
+                node_id = DHTKey::try_decode(s.as_str()).map_err(VeilidAPIError::internal)?
             } else {
                 debug!("node id not found in storage");
             }
@@ -575,10 +581,12 @@ impl VeilidConfig {
             debug!("pulling node id secret from storage");
             if let Some(s) = protected_store
                 .load_user_secret_string("node_id_secret")
-                .await?
+                .await
+                .map_err(VeilidAPIError::internal)?
             {
                 debug!("node id secret found in storage");
-                node_id_secret = DHTKeySecret::try_decode(s.as_str())?
+                node_id_secret =
+                    DHTKeySecret::try_decode(s.as_str()).map_err(VeilidAPIError::internal)?
             } else {
                 debug!("node id secret not found in storage");
             }
@@ -588,7 +596,7 @@ impl VeilidConfig {
         if node_id.valid && node_id_secret.valid {
             // Validate node id
             if !dht::validate_key(&node_id, &node_id_secret) {
-                return Err("node id secret and node id key don't match".to_owned());
+                apibail_generic!("node id secret and node id key don't match");
             }
         }
 
@@ -605,10 +613,12 @@ impl VeilidConfig {
         // Save the node id / secret in storage
         protected_store
             .save_user_secret_string("node_id", node_id.encode().as_str())
-            .await?;
+            .await
+            .map_err(VeilidAPIError::internal)?;
         protected_store
             .save_user_secret_string("node_id_secret", node_id_secret.encode().as_str())
-            .await?;
+            .await
+            .map_err(VeilidAPIError::internal)?;
 
         self.inner.write().network.node_id = node_id;
         self.inner.write().network.node_id_secret = node_id_secret;
diff --git a/veilid-server/Cargo.toml b/veilid-server/Cargo.toml
index 49871349..b93f2afd 100644
--- a/veilid-server/Cargo.toml
+++ b/veilid-server/Cargo.toml
@@ -30,6 +30,7 @@ tokio = { version = "^1", features = ["full"], optional = true }
 tokio-stream = { version = "^0", features = ["net"], optional = true }
 tokio-util = { version = "^0", features = ["compat"], optional = true}
 async-tungstenite = { version = "^0", features = ["async-tls"] }
+color-eyre = "^0.6"
 clap = "^3"
 directories = "^4"
 capnp = "^0"
diff --git a/veilid-server/src/cmdline.rs b/veilid-server/src/cmdline.rs
index c6a6a32b..f7617e11 100644
--- a/veilid-server/src/cmdline.rs
+++ b/veilid-server/src/cmdline.rs
@@ -1,4 +1,5 @@
 use crate::settings::*;
+use crate::*;
 use clap::{Arg, ArgMatches, Command};
 use std::ffi::OsStr;
 use std::path::Path;
@@ -145,11 +146,11 @@ fn do_clap_matches(default_config_path: &OsStr) -> Result<clap::ArgMatches, clap
     Ok(matches.get_matches())
 }
 
-pub fn process_command_line() -> Result<(Settings, ArgMatches), String> {
+pub fn process_command_line() -> EyreResult<(Settings, ArgMatches)> {
     // Get command line options
     let default_config_path = Settings::get_default_config_path();
     let matches = do_clap_matches(default_config_path.as_os_str())
-        .map_err(|e| format!("failed to parse command line: {}", e))?;
+        .wrap_err("failed to parse command line: {}")?;
 
     // Check for one-off commands
     #[cfg(debug_assertions)]
@@ -169,8 +170,7 @@ pub fn process_command_line() -> Result<(Settings, ArgMatches), String> {
         None
     };
 
-    let settings =
-        Settings::new(settings_path).map_err(|e| format!("configuration is invalid: {}", e))?;
+    let settings = Settings::new(settings_path).wrap_err("configuration is invalid")?;
 
     // write lock the settings
     let mut settingsrw = settings.write();
@@ -185,15 +185,13 @@ pub fn process_command_line() -> Result<(Settings, ArgMatches), String> {
     }
     if matches.occurrences_of("subnode-index") != 0 {
         let subnode_index = match matches.value_of("subnode-index") {
-            Some(x) => x
-                .parse()
-                .map_err(|e| format!("couldn't parse subnode index: {}", e))?,
+            Some(x) => x.parse().wrap_err("couldn't parse subnode index")?,
             None => {
-                return Err("value not specified for subnode-index".to_owned());
+                bail!("value not specified for subnode-index");
             }
         };
         if subnode_index == 0 {
-            return Err("value of subnode_index should be between 1 and 65535".to_owned());
+            bail!("value of subnode_index should be between 1 and 65535");
         }
         settingsrw.testing.subnode_index = subnode_index;
     }
@@ -214,7 +212,7 @@ pub fn process_command_line() -> Result<(Settings, ArgMatches), String> {
                 .expect("should not be null because of default missing value")
                 .to_string(),
         )
-        .map_err(|e| format!("failed to parse OTLP address: {}", e))?;
+        .wrap_err("failed to parse OTLP address")?;
         settingsrw.logging.otlp.level = LogLevel::Trace;
     }
     if matches.is_present("attach") {
@@ -242,13 +240,13 @@ pub fn process_command_line() -> Result<(Settings, ArgMatches), String> {
 
         // Split or get secret
         let (k, s) = if let Some((k, s)) = v.split_once(':') {
-            let k = DHTKey::try_decode(k)?;
+            let k = DHTKey::try_decode(k).wrap_err("failed to decode node id from command line")?;
             let s = DHTKeySecret::try_decode(s)?;
             (k, s)
         } else {
             let k = DHTKey::try_decode(v)?;
             let buffer = rpassword::prompt_password("Enter secret key (will not echo): ")
-                .map_err(|e| e.to_string())?;
+                .wrap_err("invalid secret key")?;
             let buffer = buffer.trim().to_string();
             let s = DHTKeySecret::try_decode(&buffer)?;
             (k, s)
@@ -270,7 +268,7 @@ pub fn process_command_line() -> Result<(Settings, ArgMatches), String> {
                 out
             }
             None => {
-                return Err("value not specified for bootstrap".to_owned());
+                bail!("value not specified for bootstrap");
             }
         };
         settingsrw.core.network.bootstrap = bootstrap_list;
@@ -284,17 +282,15 @@ pub fn process_command_line() -> Result<(Settings, ArgMatches), String> {
                 for x in x.split(',') {
                     let x = x.trim();
                     println!("    {}", x);
-                    out.push(ParsedNodeDialInfo::from_str(x).map_err(|e| {
-                        format!(
-                            "unable to parse dial info in bootstrap node list: {} for {}",
-                            e, x
-                        )
-                    })?);
+                    out.push(
+                        ParsedNodeDialInfo::from_str(x)
+                            .wrap_err("unable to parse dial info in bootstrap node list")?,
+                    );
                 }
                 out
             }
             None => {
-                return Err("value not specified for bootstrap node list".to_owned());
+                bail!("value not specified for bootstrap node list");
             }
         };
         settingsrw.core.network.bootstrap_nodes = bootstrap_list;
@@ -315,7 +311,7 @@ pub fn process_command_line() -> Result<(Settings, ArgMatches), String> {
     // Apply subnode index if we're testing
     settings
         .apply_subnode_index()
-        .map_err(|_| "failed to apply subnode index".to_owned())?;
+        .wrap_err("failed to apply subnode index")?;
 
     Ok((settings, matches))
 }
diff --git a/veilid-server/src/main.rs b/veilid-server/src/main.rs
index be0e6d12..a99b6033 100644
--- a/veilid-server/src/main.rs
+++ b/veilid-server/src/main.rs
@@ -14,6 +14,8 @@ mod veilid_logs;
 mod windows;
 
 use cfg_if::*;
+#[allow(unused_imports)]
+use color_eyre::eyre::{bail, ensure, eyre, Result as EyreResult, WrapErr};
 use server::*;
 use tools::*;
 use tracing::*;
@@ -25,16 +27,17 @@ pub mod veilid_client_capnp {
 }
 
 #[instrument(err)]
-fn main() -> Result<(), String> {
+fn main() -> EyreResult<()> {
     #[cfg(windows)]
     let _ = ansi_term::enable_ansi_support();
+    color_eyre::install()?;
 
     let (settings, matches) = cmdline::process_command_line()?;
 
     // --- Dump Config ---
     if matches.occurrences_of("dump-config") != 0 {
         return serde_yaml::to_writer(std::io::stdout(), &*settings.read())
-            .map_err(|e| e.to_string());
+            .wrap_err("failed to write yaml");
     }
 
     // --- Generate DHT Key ---
diff --git a/veilid-server/src/server.rs b/veilid-server/src/server.rs
index b035deda..bb807bb4 100644
--- a/veilid-server/src/server.rs
+++ b/veilid-server/src/server.rs
@@ -2,6 +2,7 @@ use crate::client_api;
 use crate::settings::*;
 use crate::tools::*;
 use crate::veilid_logs::*;
+use crate::*;
 use flume::{unbounded, Receiver, Sender};
 use lazy_static::*;
 use parking_lot::Mutex;
@@ -34,16 +35,16 @@ pub async fn run_veilid_server(
     settings: Settings,
     server_mode: ServerMode,
     veilid_logs: VeilidLogs,
-) -> Result<(), String> {
+) -> EyreResult<()> {
     run_veilid_server_internal(settings, server_mode, veilid_logs).await
 }
 
-#[instrument(err, skip_all)]
+//#[instrument(err, skip_all)]
 pub async fn run_veilid_server_internal(
     settings: Settings,
     server_mode: ServerMode,
     veilid_logs: VeilidLogs,
-) -> Result<(), String> {
+) -> EyreResult<()> {
     trace!(?settings, ?server_mode);
 
     let settingsr = settings.read();
@@ -65,7 +66,7 @@ pub async fn run_veilid_server_internal(
     // Start Veilid Core and get API
     let veilid_api = veilid_core::api_startup(update_callback, config_callback)
         .await
-        .map_err(|e| format!("VeilidCore startup failed: {}", e))?;
+        .wrap_err("VeilidCore startup failed")?;
 
     // Start client api if one is requested
     let mut capi = if settingsr.client_api.enabled && matches!(server_mode, ServerMode::Normal) {
@@ -98,9 +99,10 @@ pub async fn run_veilid_server_internal(
     if auto_attach {
         info!("Auto-attach to the Veilid network");
         if let Err(e) = veilid_api.attach().await {
-            let outerr = format!("Auto-attaching to the Veilid network failed: {:?}", e);
-            error!("{}", outerr);
-            out = Err(outerr);
+            out = Err(eyre!(
+                "Auto-attaching to the Veilid network failed: {:?}",
+                e
+            ));
             shutdown();
         }
     }
@@ -116,9 +118,7 @@ pub async fn run_veilid_server_internal(
                     }
                 }
                 Err(e) => {
-                    let outerr = format!("Getting state failed: {:?}", e);
-                    error!("{}", outerr);
-                    out = Err(outerr);
+                    out = Err(eyre!("Getting state failed: {:?}", e));
                     break;
                 }
             }
@@ -129,9 +129,7 @@ pub async fn run_veilid_server_internal(
                 print!("{}", v);
             }
             Err(e) => {
-                let outerr = format!("Getting TXT record failed: {:?}", e);
-                error!("{}", outerr);
-                out = Err(outerr);
+                out = Err(eyre!("Getting TXT record failed: {:?}", e));
             }
         };
         shutdown();
diff --git a/veilid-server/src/settings.rs b/veilid-server/src/settings.rs
index 1174be44..5ede034c 100644
--- a/veilid-server/src/settings.rs
+++ b/veilid-server/src/settings.rs
@@ -1,5 +1,7 @@
 #![allow(clippy::bool_assert_comparison)]
 
+use crate::*;
+
 use directories::*;
 use parking_lot::*;
 
@@ -11,8 +13,9 @@ use std::str::FromStr;
 use std::sync::Arc;
 use url::Url;
 use veilid_core::xx::*;
+use veilid_core::*;
 
-pub fn load_default_config() -> Result<config::Config, config::ConfigError> {
+pub fn load_default_config() -> EyreResult<config::Config> {
     let default_config = String::from(
         r#"---
 daemon:
@@ -172,21 +175,18 @@ core:
             config::FileFormat::Yaml,
         ))
         .build()
+        .wrap_err("failed to parse default config")
 }
 
-pub fn load_config(
-    cfg: config::Config,
-    config_file: &Path,
-) -> Result<config::Config, config::ConfigError> {
+pub fn load_config(cfg: config::Config, config_file: &Path) -> EyreResult<config::Config> {
     if let Some(config_file_str) = config_file.to_str() {
         config::Config::builder()
             .add_source(cfg)
             .add_source(config::File::new(config_file_str, config::FileFormat::Yaml))
             .build()
+            .wrap_err("failed to load config")
     } else {
-        Err(config::ConfigError::Message(
-            "config file path is not valid UTF-8".to_owned(),
-        ))
+        bail!("config file path is not valid UTF-8")
     }
 }
 
@@ -254,9 +254,11 @@ pub struct ParsedUrl {
 }
 
 impl ParsedUrl {
-    pub fn offset_port(&mut self, offset: u16) -> Result<(), ()> {
+    pub fn offset_port(&mut self, offset: u16) -> EyreResult<()> {
         // Bump port on url
-        self.url.set_port(Some(self.url.port().unwrap() + offset))?;
+        self.url
+            .set_port(Some(self.url.port().unwrap() + offset))
+            .map_err(|_| eyre!("failed to set port on url"))?;
         self.urlstring = self.url.to_string();
         Ok(())
     }
@@ -388,16 +390,16 @@ impl serde::Serialize for NamedSocketAddrs {
 }
 
 impl NamedSocketAddrs {
-    pub fn offset_port(&mut self, offset: u16) -> Result<(), ()> {
+    pub fn offset_port(&mut self, offset: u16) -> EyreResult<()> {
         // Bump port on name
         if let Some(split) = self.name.rfind(':') {
             let hoststr = &self.name[0..split];
             let portstr = &self.name[split + 1..];
-            let port: u16 = portstr.parse::<u16>().map_err(drop)? + offset;
+            let port: u16 = portstr.parse::<u16>().wrap_err("failed to parse port")? + offset;
 
             self.name = format!("{}:{}", hoststr, port);
         } else {
-            return Err(());
+            bail!("no port specified to offset");
         }
 
         // Bump port on addresses
@@ -655,7 +657,7 @@ pub struct Settings {
 }
 
 impl Settings {
-    pub fn new(config_file: Option<&OsStr>) -> Result<Self, config::ConfigError> {
+    pub fn new(config_file: Option<&OsStr>) -> EyreResult<Self> {
         // Load the default config
         let mut cfg = load_default_config()?;
 
@@ -681,7 +683,7 @@ impl Settings {
         self.inner.write()
     }
 
-    pub fn apply_subnode_index(&self) -> Result<(), ()> {
+    pub fn apply_subnode_index(&self) -> EyreResult<()> {
         let mut settingsrw = self.write();
         let idx = settingsrw.testing.subnode_index;
         if idx == 0 {
@@ -869,7 +871,7 @@ impl Settings {
         pk_path
     }
 
-    pub fn set(&self, key: &str, value: &str) -> Result<(), String> {
+    pub fn set(&self, key: &str, value: &str) -> EyreResult<()> {
         let mut inner = self.inner.write();
 
         macro_rules! set_config_value {
@@ -882,9 +884,11 @@ impl Settings {
                             return Ok(());
                         }
                         Err(e) => {
-                            return Err(format!(
+                            return Err(eyre!(
                                 "invalid type for key {}, value: {}: {}",
-                                key, value, e
+                                key,
+                                value,
+                                e
                             ))
                         }
                     }
@@ -1005,7 +1009,7 @@ impl Settings {
         set_config_value!(inner.core.network.protocol.wss.listen_address, value);
         set_config_value!(inner.core.network.protocol.wss.path, value);
         set_config_value!(inner.core.network.protocol.wss.url, value);
-        Err("settings key not found".to_owned())
+        Err(eyre!("settings key not found"))
     }
 
     pub fn get_core_config_callback(&self) -> veilid_core::ConfigCallback {
@@ -1013,7 +1017,7 @@ impl Settings {
 
         Arc::new(move |key: String| {
             let inner = inner.read();
-            let out: Result<Box<dyn core::any::Any + Send>, String> = match key.as_str() {
+            let out: ConfigCallbackReturn = match key.as_str() {
                 "program_name" => Ok(Box::new("veilid-server".to_owned())),
                 "namespace" => Ok(Box::new(if inner.testing.subnode_index == 0 {
                     "".to_owned()
@@ -1365,7 +1369,10 @@ impl Settings {
                         .as_ref()
                         .map(|a| a.urlstring.clone()),
                 )),
-                _ => Err(format!("config key '{}' doesn't exist", key)),
+                _ => Err(VeilidAPIError::generic(format!(
+                    "config key '{}' doesn't exist",
+                    key
+                ))),
             };
             out
         })
diff --git a/veilid-server/src/unix.rs b/veilid-server/src/unix.rs
index 5ba7fa5b..f3374823 100644
--- a/veilid-server/src/unix.rs
+++ b/veilid-server/src/unix.rs
@@ -2,6 +2,7 @@ use crate::server::*;
 use crate::settings::Settings;
 use crate::tools::*;
 use crate::veilid_logs::*;
+use crate::*;
 use clap::ArgMatches;
 use futures_util::StreamExt;
 use signal_hook::consts::signal::*;
@@ -26,7 +27,7 @@ async fn handle_signals(mut signals: Signals) {
 }
 
 #[instrument(err)]
-pub fn run_daemon(settings: Settings, _matches: ArgMatches) -> Result<(), String> {
+pub fn run_daemon(settings: Settings, _matches: ArgMatches) -> EyreResult<()> {
     let daemon = {
         let mut daemon = daemonize::Daemonize::new();
         let s = settings.read();
@@ -64,10 +65,7 @@ pub fn run_daemon(settings: Settings, _matches: ArgMatches) -> Result<(), String
         }
 
         let stdout_file = if let Some(stdout_file) = &s.daemon.stdout_file {
-            Some(
-                std::fs::File::create(stdout_file)
-                    .map_err(|e| format!("Failed to create stdio file: {}", e))?,
-            )
+            Some(std::fs::File::create(stdout_file).wrap_err("Failed to create stdio file")?)
         } else {
             None
         };
@@ -79,12 +77,11 @@ pub fn run_daemon(settings: Settings, _matches: ArgMatches) -> Result<(), String
                         .as_ref()
                         .unwrap()
                         .try_clone()
-                        .map_err(|e| format!("Failed to clone stdout file: {}", e))?,
+                        .wrap_err("Failed to clone stdout file")?,
                 );
             } else {
                 daemon = daemon.stderr(
-                    std::fs::File::create(stderr_file)
-                        .map_err(|e| format!("Failed to create stderr file: {}", e))?,
+                    std::fs::File::create(stderr_file).wrap_err("Failed to create stderr file")?,
                 );
             }
         }
@@ -101,13 +98,11 @@ pub fn run_daemon(settings: Settings, _matches: ArgMatches) -> Result<(), String
         let veilid_logs = VeilidLogs::setup(settings.clone())?;
 
         // Daemonize
-        daemon
-            .start()
-            .map_err(|e| format!("Failed to daemonize: {}", e))?;
+        daemon.start().wrap_err("Failed to daemonize")?;
 
         // Catch signals
-        let signals = Signals::new(&[SIGHUP, SIGTERM, SIGINT, SIGQUIT])
-            .map_err(|e| format!("failed to init signals: {}", e))?;
+        let signals =
+            Signals::new(&[SIGHUP, SIGTERM, SIGINT, SIGQUIT]).wrap_err("failed to init signals")?;
         let handle = signals.handle();
 
         let signals_task = spawn(handle_signals(signals));
diff --git a/veilid-server/src/veilid_logs.rs b/veilid-server/src/veilid_logs.rs
index 9bfaaa15..aa8b2962 100644
--- a/veilid-server/src/veilid_logs.rs
+++ b/veilid-server/src/veilid_logs.rs
@@ -1,4 +1,5 @@
 use crate::settings::*;
+use crate::*;
 use cfg_if::*;
 use opentelemetry::sdk::*;
 use opentelemetry::*;
@@ -22,7 +23,7 @@ pub struct VeilidLogs {
 }
 
 impl VeilidLogs {
-    pub fn setup(settings: Settings) -> Result<VeilidLogs, String> {
+    pub fn setup(settings: Settings) -> EyreResult<VeilidLogs> {
         let settingsr = settings.read();
 
         // Set up subscriber and layers
@@ -77,7 +78,7 @@ impl VeilidLogs {
                     )]),
                 ))
                 .install_batch(batch)
-                .map_err(|e| format!("failed to install OpenTelemetry tracer: {}", e))?;
+                .wrap_err("failed to install OpenTelemetry tracer")?;
 
             let filter = veilid_core::VeilidLayerFilter::new(
                 convert_loglevel(settingsr.logging.otlp.level),
@@ -101,13 +102,11 @@ impl VeilidLogs {
                 .parent()
                 .unwrap_or(Path::new(&MAIN_SEPARATOR.to_string()))
                 .canonicalize()
-                .map_err(|e| {
-                    format!(
-                        "File log path parent does not exist: {} ({})",
-                        settingsr.logging.file.path, e
-                    )
-                })?;
-            let log_filename = full_path.file_name().ok_or(format!(
+                .wrap_err(format!(
+                    "File log path parent does not exist: {}",
+                    settingsr.logging.file.path
+                ))?;
+            let log_filename = full_path.file_name().ok_or(eyre!(
                 "File log filename not specified in path: {}",
                 settingsr.logging.file.path
             ))?;
@@ -149,7 +148,7 @@ impl VeilidLogs {
                         convert_loglevel(settingsr.logging.system.level),
                         None,
                     );
-                    let layer =tracing_journald::layer().map_err(|e| format!("failed to set up journald logging: {}", e))?
+                    let layer =tracing_journald::layer().wrap_err("failed to set up journald logging")?
                         .with_filter(filter.clone());
                     filters.insert("system", filter);
                     layers.push(layer.boxed());
@@ -160,7 +159,7 @@ impl VeilidLogs {
         let subscriber = subscriber.with(layers);
         subscriber
             .try_init()
-            .map_err(|e| format!("failed to initialize logging: {}", e))?;
+            .wrap_err("failed to initialize logging")?;
 
         Ok(VeilidLogs {
             inner: Arc::new(Mutex::new(VeilidLogsInner {