diff --git a/Cargo.lock b/Cargo.lock
index ebd2d157..b94cb7d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -854,9 +854,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.82"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
 dependencies = [
  "libc",
 ]
@@ -1533,9 +1533,9 @@ dependencies = [
 
 [[package]]
 name = "dashmap"
-version = "5.5.0"
+version = "5.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d"
+checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28"
 dependencies = [
  "cfg-if 1.0.0",
  "hashbrown 0.14.0",
@@ -2249,9 +2249,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.3.20"
+version = "0.3.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
+checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
 dependencies = [
  "bytes 1.4.0",
  "fnv",
@@ -3757,12 +3757,12 @@ dependencies = [
 
 [[package]]
 name = "petgraph"
-version = "0.6.3"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
+checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
 dependencies = [
  "fixedbitset",
- "indexmap 1.9.3",
+ "indexmap 2.0.0",
 ]
 
 [[package]]
@@ -4463,9 +4463,9 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.183"
+version = "1.0.185"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c"
+checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31"
 dependencies = [
  "serde_derive",
 ]
@@ -4491,9 +4491,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.183"
+version = "1.0.185"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816"
+checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -4750,9 +4750,9 @@ dependencies = [
 
 [[package]]
 name = "slab"
-version = "0.4.8"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
 dependencies = [
  "autocfg",
 ]
diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml
index c337170b..2303fb00 100644
--- a/veilid-core/Cargo.toml
+++ b/veilid-core/Cargo.toml
@@ -13,8 +13,24 @@ crate-type = ["cdylib", "staticlib", "rlib"]
 
 # Common features
 default = ["enable-crypto-vld0"]
-rt-async-std = ["async-std", "async-std-resolver", "async_executors/async_std", "rtnetlink/smol_socket", "veilid-tools/rt-async-std"]
-rt-tokio = ["tokio", "tokio-util", "tokio-stream", "trust-dns-resolver/tokio-runtime", "async_executors/tokio_tp", "async_executors/tokio_io", "async_executors/tokio_timer", "rtnetlink/tokio_socket", "veilid-tools/rt-tokio"]
+rt-async-std = [
+    "async-std",
+    "async-std-resolver",
+    "async_executors/async_std",
+    "rtnetlink/smol_socket",
+    "veilid-tools/rt-async-std",
+]
+rt-tokio = [
+    "tokio",
+    "tokio-util",
+    "tokio-stream",
+    "trust-dns-resolver/tokio-runtime",
+    "async_executors/tokio_tp",
+    "async_executors/tokio_io",
+    "async_executors/tokio_timer",
+    "rtnetlink/tokio_socket",
+    "veilid-tools/rt-tokio",
+]
 rt-wasm-bindgen = ["veilid-tools/rt-wasm-bindgen", "async_executors/bindgen"]
 
 # Crypto support features
@@ -36,7 +52,7 @@ network-result-extra = ["veilid-tools/network-result-extra"]
 [dependencies]
 
 # Tools
-veilid-tools = { path = "../veilid-tools", features = [ "tracing" ] }
+veilid-tools = { path = "../veilid-tools", features = ["tracing"] }
 paste = "1.0.14"
 once_cell = "1.18.0"
 owning_ref = "0.4.1"
@@ -57,7 +73,7 @@ eyre = "0.6.8"
 thiserror = "1.0.47"
 
 # Data structures
-enumset = { version= "1.1.2", features = ["serde"] }
+enumset = { version = "1.1.2", features = ["serde"] }
 keyvaluedb = { path = "../external/keyvaluedb/keyvaluedb" }
 range-set-blaze = "0.1.9"
 weak-table = "0.3.2"
@@ -65,15 +81,31 @@ generic-array = "0.14.7"
 hashlink = { path = "../external/hashlink", features = ["serde_impl"] }
 
 # System
-futures-util = { version = "0.3.28", default_features = false, features = ["alloc"] }
+futures-util = { version = "0.3.28", default_features = false, features = [
+    "alloc",
+] }
 flume = { version = "0.11.0", features = ["async"] }
 parking_lot = "0.12.1"
 stop-token = { version = "0.7.0", default-features = false }
 
 # Crypto
-ed25519-dalek = { version = "2.0.0", default_features = false, features = ["alloc", "rand_core", "digest"] }
-x25519-dalek = { version = "2.0.0", default_features = false, features = ["alloc", "static_secrets"] }
-curve25519-dalek = { version = "4.0.0", default_features = false, features = ["alloc"] }
+ed25519-dalek = { version = "2.0.0", default_features = false, features = [
+    "alloc",
+    "rand_core",
+    "digest",
+    "zeroize",
+] }
+x25519-dalek = { version = "2.0.0", default_features = false, features = [
+    "alloc",
+    "static_secrets",
+    "zeroize",
+    "precomputed-tables",
+] }
+curve25519-dalek = { version = "4.0.0", default_features = false, features = [
+    "alloc",
+    "zeroize",
+    "precomputed-tables",
+] }
 blake3 = { version = "1.4.1" }
 chacha20poly1305 = "0.10.1"
 chacha20 = "0.9.1"
@@ -82,17 +114,20 @@ argon2 = "0.5.1"
 # Network
 async-std-resolver = { version = "0.22.0", optional = true }
 trust-dns-resolver = { version = "0.22.0", optional = true }
-enum-as-inner = "=0.5.1" # temporary fix for trust-dns-resolver v0.22.0
+enum-as-inner = "=0.5.1"                                     # temporary fix for trust-dns-resolver v0.22.0
 
 # Serialization
 capnp = { version = "0.17.2", default_features = false }
-serde = { version = "1.0.183", features = ["derive" ] }
+serde = { version = "1.0.183", features = ["derive"] }
 serde_json = { version = "1.0.105" }
 serde-big-array = "0.5.1"
 json = "0.12.4"
 data-encoding = { version = "2.4.0" }
 schemars = "0.8.12"
-lz4_flex = { version = "0.11.1", default-features = false, features = ["safe-encode", "safe-decode"] }
+lz4_flex = { version = "0.11.1", default-features = false, features = [
+    "safe-encode",
+    "safe-decode",
+] }
 
 # Dependencies for native builds only
 # Linux, Windows, Mac, iOS, Android
@@ -106,12 +141,17 @@ libc = "0.2.147"
 nix = "0.26.2"
 
 # System
-async-std = { version = "1.12.0", features = ["unstable"], optional = true}
-tokio = { version = "1.32.0", features = ["full"], optional = true}
-tokio-util = { version = "0.7.8", features = ["compat"], optional = true}
-tokio-stream = { version = "0.1.14", features = ["net"], optional = true}
+async-std = { version = "1.12.0", features = ["unstable"], optional = true }
+tokio = { version = "1.32.0", features = ["full"], optional = true }
+tokio-util = { version = "0.7.8", features = ["compat"], optional = true }
+tokio-stream = { version = "0.1.14", features = ["net"], optional = true }
 async-io = { version = "1.13.0" }
-futures-util = { version = "0.3.28", default-features = false, features = ["async-await", "sink", "std", "io"] }
+futures-util = { version = "0.3.28", default-features = false, features = [
+    "async-await",
+    "sink",
+    "std",
+    "io",
+] }
 
 # Data structures
 keyring-manager = { path = "../external/keyring-manager" }
@@ -119,7 +159,7 @@ keyvaluedb-sqlite = { path = "../external/keyvaluedb/keyvaluedb-sqlite" }
 
 # Network
 async-tungstenite = { version = "0.23.0", features = ["async-tls"] }
-igd  = { path = "../external/rust-igd" }
+igd = { path = "../external/rust-igd" }
 async-tls = "0.12.0"
 webpki = "0.22.0"
 webpki-roots = "0.25.2"
@@ -134,7 +174,10 @@ socket2 = { version = "0.5.3", features = ["all"] }
 getrandom = { version = "0.2.4", features = ["js"] }
 
 # System
-async_executors = { version = "0.7.0", default-features = false, features = [ "bindgen", "timer" ]}
+async_executors = { version = "0.7.0", default-features = false, features = [
+    "bindgen",
+    "timer",
+] }
 async-lock = "2.8.0"
 wasm-bindgen = "0.2.87"
 js-sys = "0.3.64"
@@ -181,14 +224,17 @@ ifstructs = "0.1.1"
 
 # Dependencies for Linux or Android
 [target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies]
-rtnetlink = { version = "=0.13.0", default-features = false}
+rtnetlink = { version = "=0.13.0", default-features = false }
 netlink-sys = { version = "=0.8.5" }
 netlink-packet-route = { version = "=0.17.0" }
 
 # Dependencies for Windows
 [target.'cfg(target_os = "windows")'.dependencies]
-winapi = { version = "0.3.9", features = [ "iptypes", "iphlpapi" ] }
-windows = { version = "0.51.1", features = [ "Win32_NetworkManagement_Dns", "Win32_Foundation" ]}
+winapi = { version = "0.3.9", features = ["iptypes", "iphlpapi"] }
+windows = { version = "0.51.1", features = [
+    "Win32_NetworkManagement_Dns",
+    "Win32_Foundation",
+] }
 windows-permissions = "0.2.4"
 
 # Dependencies for iOS
@@ -207,7 +253,7 @@ features = ["bundled"]
 serial_test = "2.0.0"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
-simplelog = { version = "0.12.1", features=["test"] }
+simplelog = { version = "0.12.1", features = ["test"] }
 
 [target.'cfg(target_arch = "wasm32")'.dev-dependencies]
 wasm-bindgen-test = "0.3.37"
diff --git a/veilid-core/src/lib.rs b/veilid-core/src/lib.rs
index 2446cfe4..ce93567c 100644
--- a/veilid-core/src/lib.rs
+++ b/veilid-core/src/lib.rs
@@ -61,7 +61,7 @@ pub fn veilid_version() -> (u32, u32, u32) {
 #[cfg(target_os = "android")]
 pub use intf::android::veilid_core_setup_android;
 
-pub static DEFAULT_LOG_IGNORE_LIST: [&str; 21] = [
+pub static DEFAULT_LOG_IGNORE_LIST: [&str; 23] = [
     "mio",
     "h2",
     "hyper",
@@ -83,6 +83,8 @@ pub static DEFAULT_LOG_IGNORE_LIST: [&str; 21] = [
     "trust_dns_resolver",
     "trust_dns_proto",
     "attohttpc",
+    "ws_stream_wasm",
+    "keyvaluedb_web",
 ];
 
 use cfg_if::*;
diff --git a/veilid-core/src/network_manager/address_filter.rs b/veilid-core/src/network_manager/address_filter.rs
index 417ebd4e..5b17e3d0 100644
--- a/veilid-core/src/network_manager/address_filter.rs
+++ b/veilid-core/src/network_manager/address_filter.rs
@@ -4,6 +4,8 @@ use alloc::collections::btree_map::Entry;
 // XXX: Move to config eventually?
 const PUNISHMENT_DURATION_MIN: usize = 60;
 const MAX_PUNISHMENTS_BY_NODE_ID: usize = 65536;
+const DIAL_INFO_FAILURE_DURATION_MIN: usize = 10;
+const MAX_DIAL_INFO_FAILURES: usize = 65536;
 
 #[derive(ThisError, Debug, Clone, Copy, PartialEq, Eq)]
 pub enum AddressFilterError {
@@ -28,6 +30,7 @@ struct AddressFilterInner {
     punishments_by_ip4: BTreeMap<Ipv4Addr, Timestamp>,
     punishments_by_ip6_prefix: BTreeMap<Ipv6Addr, Timestamp>,
     punishments_by_node_id: BTreeMap<TypedKey, Timestamp>,
+    dial_info_failures: BTreeMap<DialInfo, Timestamp>,
 }
 
 struct AddressFilterUnlockedInner {
@@ -36,6 +39,7 @@ struct AddressFilterUnlockedInner {
     max_connections_per_ip6_prefix_size: usize,
     max_connection_frequency_per_min: usize,
     punishment_duration_min: usize,
+    dial_info_failure_duration_min: usize,
     routing_table: RoutingTable,
 }
 
@@ -56,6 +60,10 @@ impl fmt::Debug for AddressFilterUnlockedInner {
                 &self.max_connection_frequency_per_min,
             )
             .field("punishment_duration_min", &self.punishment_duration_min)
+            .field(
+                "dial_info_failure_duration_min",
+                &self.dial_info_failure_duration_min,
+            )
             .finish()
     }
 }
@@ -78,6 +86,7 @@ impl AddressFilter {
                 max_connection_frequency_per_min: c.network.max_connection_frequency_per_min
                     as usize,
                 punishment_duration_min: PUNISHMENT_DURATION_MIN,
+                dial_info_failure_duration_min: DIAL_INFO_FAILURE_DURATION_MIN,
                 routing_table,
             }),
             inner: Arc::new(Mutex::new(AddressFilterInner {
@@ -88,10 +97,17 @@ impl AddressFilter {
                 punishments_by_ip4: BTreeMap::new(),
                 punishments_by_ip6_prefix: BTreeMap::new(),
                 punishments_by_node_id: BTreeMap::new(),
+                dial_info_failures: BTreeMap::new(),
             })),
         }
     }
 
+    // When the network restarts, some of the address filter can be cleared
+    pub fn restart(&self) {
+        let mut inner = self.inner.lock();
+        inner.dial_info_failures.clear();
+    }
+
     fn purge_old_timestamps(&self, inner: &mut AddressFilterInner, cur_ts: Timestamp) {
         // v4
         {
@@ -180,6 +196,22 @@ impl AddressFilter {
                 }
             }
         }
+        // dial info
+        {
+            let mut dead_keys = Vec::<DialInfo>::new();
+            for (key, value) in &mut inner.dial_info_failures {
+                // Drop failures older than the failure duration
+                if cur_ts.as_u64().saturating_sub(value.as_u64())
+                    > self.unlocked_inner.dial_info_failure_duration_min as u64 * 60_000_000u64
+                {
+                    dead_keys.push(key.clone());
+                }
+            }
+            for key in dead_keys {
+                log_net!(debug ">>> DIALINFO PERMIT: {}", key);
+                inner.dial_info_failures.remove(&key);
+            }
+        }
     }
 
     fn is_ip_addr_punished_inner(&self, inner: &AddressFilterInner, ipblock: IpAddr) -> bool {
@@ -198,6 +230,14 @@ impl AddressFilter {
         false
     }
 
+    fn get_dial_info_failed_ts_inner(
+        &self,
+        inner: &AddressFilterInner,
+        dial_info: &DialInfo,
+    ) -> Option<Timestamp> {
+        inner.dial_info_failures.get(dial_info).copied()
+    }
+
     pub fn is_ip_addr_punished(&self, addr: IpAddr) -> bool {
         let inner = self.inner.lock();
         let ipblock = ip_to_ipblock(
@@ -207,6 +247,27 @@ impl AddressFilter {
         self.is_ip_addr_punished_inner(&*inner, ipblock)
     }
 
+    pub fn get_dial_info_failed_ts(&self, dial_info: &DialInfo) -> Option<Timestamp> {
+        let inner = self.inner.lock();
+        self.get_dial_info_failed_ts_inner(&*inner, dial_info)
+    }
+
+    pub fn set_dial_info_failed(&self, dial_info: DialInfo) {
+        let ts = get_aligned_timestamp();
+
+        let mut inner = self.inner.lock();
+        if inner.dial_info_failures.len() >= MAX_DIAL_INFO_FAILURES {
+            log_net!(debug ">>> DIALINFO FAILURE TABLE FULL: {}", dial_info);
+            return;
+        }
+        log_net!(debug ">>> DIALINFO FAILURE: {:?}", dial_info);
+        inner
+            .dial_info_failures
+            .entry(dial_info)
+            .and_modify(|v| *v = ts)
+            .or_insert(ts);
+    }
+
     pub fn punish_ip_addr(&self, addr: IpAddr) {
         log_net!(debug ">>> PUNISHED: {}", addr);
         let ts = get_aligned_timestamp();
diff --git a/veilid-core/src/network_manager/mod.rs b/veilid-core/src/network_manager/mod.rs
index a36569bb..c0fb86d5 100644
--- a/veilid-core/src/network_manager/mod.rs
+++ b/veilid-core/src/network_manager/mod.rs
@@ -211,20 +211,7 @@ impl NetworkManager {
         // Make the network key
         let network_key = {
             let c = config.get();
-            let network_key_password = if let Some(nkp) = c.network.network_key_password.clone() {
-                Some(nkp)
-            } else {
-                if c.network
-                    .routing_table
-                    .bootstrap
-                    .contains(&"bootstrap.veilid.net".to_owned())
-                {
-                    None
-                } else {
-                    Some(c.network.routing_table.bootstrap.join(","))
-                }
-            };
-
+            let network_key_password = c.network.network_key_password.clone();
             let network_key = if let Some(network_key_password) = network_key_password {
                 if !network_key_password.is_empty() {
                     info!("Using network key");
@@ -380,6 +367,9 @@ impl NetworkManager {
             return Ok(());
         }
 
+        // Clean address filter for things that should not be persistent
+        self.address_filter().restart();
+
         // Create network components
         let connection_manager = ConnectionManager::new(self.clone());
         let net = Network::new(
diff --git a/veilid-core/src/network_manager/native/mod.rs b/veilid-core/src/network_manager/native/mod.rs
index e39220ed..1cc1e7b1 100644
--- a/veilid-core/src/network_manager/native/mod.rs
+++ b/veilid-core/src/network_manager/native/mod.rs
@@ -384,6 +384,21 @@ impl Network {
 
     ////////////////////////////////////////////////////////////
 
+    // Record DialInfo failures
+    pub async fn record_dial_info_failure<T, F: Future<Output = EyreResult<NetworkResult<T>>>>(
+        &self,
+        dial_info: DialInfo,
+        fut: F,
+    ) -> EyreResult<NetworkResult<T>> {
+        let network_result = fut.await?;
+        if matches!(network_result, NetworkResult::NoConnection(_)) {
+            self.network_manager()
+                .address_filter()
+                .set_dial_info_failed(dial_info);
+        }
+        Ok(network_result)
+    }
+
     // Send data to a dial info, unbound, using a new connection from a random port
     // This creates a short-lived connection in the case of connection-oriented protocols
     // for the purpose of sending this one message.
@@ -394,59 +409,62 @@ impl Network {
         dial_info: DialInfo,
         data: Vec<u8>,
     ) -> EyreResult<NetworkResult<()>> {
-        let data_len = data.len();
-        let connect_timeout_ms = {
-            let c = self.config.get();
-            c.network.connection_initial_timeout_ms
-        };
+        self.record_dial_info_failure(dial_info.clone(), async move {
+            let data_len = data.len();
+            let connect_timeout_ms = {
+                let c = self.config.get();
+                c.network.connection_initial_timeout_ms
+            };
 
-        if self
-            .network_manager()
-            .address_filter()
-            .is_ip_addr_punished(dial_info.address().to_ip_addr())
-        {
-            return Ok(NetworkResult::no_connection_other("punished"));
-        }
+            if self
+                .network_manager()
+                .address_filter()
+                .is_ip_addr_punished(dial_info.address().to_ip_addr())
+            {
+                return Ok(NetworkResult::no_connection_other("punished"));
+            }
 
-        match dial_info.protocol_type() {
-            ProtocolType::UDP => {
-                let peer_socket_addr = dial_info.to_socket_addr();
-                let h = RawUdpProtocolHandler::new_unspecified_bound_handler(&peer_socket_addr)
+            match dial_info.protocol_type() {
+                ProtocolType::UDP => {
+                    let peer_socket_addr = dial_info.to_socket_addr();
+                    let h = RawUdpProtocolHandler::new_unspecified_bound_handler(&peer_socket_addr)
+                        .await
+                        .wrap_err("create socket failure")?;
+                    let _ = network_result_try!(h
+                        .send_message(data, peer_socket_addr)
+                        .await
+                        .map(NetworkResult::Value)
+                        .wrap_err("send message failure")?);
+                }
+                ProtocolType::TCP => {
+                    let peer_socket_addr = dial_info.to_socket_addr();
+                    let pnc = network_result_try!(RawTcpProtocolHandler::connect(
+                        None,
+                        peer_socket_addr,
+                        connect_timeout_ms
+                    )
                     .await
-                    .wrap_err("create socket failure")?;
-                let _ = network_result_try!(h
-                    .send_message(data, peer_socket_addr)
+                    .wrap_err("connect failure")?);
+                    network_result_try!(pnc.send(data).await.wrap_err("send failure")?);
+                }
+                ProtocolType::WS | ProtocolType::WSS => {
+                    let pnc = network_result_try!(WebsocketProtocolHandler::connect(
+                        None,
+                        &dial_info,
+                        connect_timeout_ms
+                    )
                     .await
-                    .map(NetworkResult::Value)
-                    .wrap_err("send message failure")?);
+                    .wrap_err("connect failure")?);
+                    network_result_try!(pnc.send(data).await.wrap_err("send failure")?);
+                }
             }
-            ProtocolType::TCP => {
-                let peer_socket_addr = dial_info.to_socket_addr();
-                let pnc = network_result_try!(RawTcpProtocolHandler::connect(
-                    None,
-                    peer_socket_addr,
-                    connect_timeout_ms
-                )
-                .await
-                .wrap_err("connect failure")?);
-                network_result_try!(pnc.send(data).await.wrap_err("send failure")?);
-            }
-            ProtocolType::WS | ProtocolType::WSS => {
-                let pnc = network_result_try!(WebsocketProtocolHandler::connect(
-                    None,
-                    &dial_info,
-                    connect_timeout_ms
-                )
-                .await
-                .wrap_err("connect failure")?);
-                network_result_try!(pnc.send(data).await.wrap_err("send failure")?);
-            }
-        }
-        // Network accounting
-        self.network_manager()
-            .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
+            // Network accounting
+            self.network_manager()
+                .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
 
-        Ok(NetworkResult::Value(()))
+            Ok(NetworkResult::Value(()))
+        })
+        .await
     }
 
     // Send data to a dial info, unbound, using a new connection from a random port
@@ -461,85 +479,95 @@ impl Network {
         data: Vec<u8>,
         timeout_ms: u32,
     ) -> EyreResult<NetworkResult<Vec<u8>>> {
-        let data_len = data.len();
-        let connect_timeout_ms = {
-            let c = self.config.get();
-            c.network.connection_initial_timeout_ms
-        };
+        self.record_dial_info_failure(dial_info.clone(), async move {
+            let data_len = data.len();
+            let connect_timeout_ms = {
+                let c = self.config.get();
+                c.network.connection_initial_timeout_ms
+            };
 
-        if self
-            .network_manager()
-            .address_filter()
-            .is_ip_addr_punished(dial_info.address().to_ip_addr())
-        {
-            return Ok(NetworkResult::no_connection_other("punished"));
-        }
-
-        match dial_info.protocol_type() {
-            ProtocolType::UDP => {
-                let peer_socket_addr = dial_info.to_socket_addr();
-                let h = RawUdpProtocolHandler::new_unspecified_bound_handler(&peer_socket_addr)
-                    .await
-                    .wrap_err("create socket failure")?;
-                network_result_try!(h
-                    .send_message(data, peer_socket_addr)
-                    .await
-                    .wrap_err("send message failure")?);
-                self.network_manager()
-                    .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
-
-                // receive single response
-                let mut out = vec![0u8; MAX_MESSAGE_SIZE];
-                let (recv_len, recv_addr) = network_result_try!(timeout(
-                    timeout_ms,
-                    h.recv_message(&mut out).instrument(Span::current())
-                )
-                .await
-                .into_network_result())
-                .wrap_err("recv_message failure")?;
-
-                let recv_socket_addr = recv_addr.remote_address().to_socket_addr();
-                self.network_manager()
-                    .stats_packet_rcvd(recv_socket_addr.ip(), ByteCount::new(recv_len as u64));
-
-                // if the from address is not the same as the one we sent to, then drop this
-                if recv_socket_addr != peer_socket_addr {
-                    bail!("wrong address");
-                }
-                out.resize(recv_len, 0u8);
-                Ok(NetworkResult::Value(out))
+            if self
+                .network_manager()
+                .address_filter()
+                .is_ip_addr_punished(dial_info.address().to_ip_addr())
+            {
+                return Ok(NetworkResult::no_connection_other("punished"));
             }
-            ProtocolType::TCP | ProtocolType::WS | ProtocolType::WSS => {
-                let pnc = network_result_try!(match dial_info.protocol_type() {
-                    ProtocolType::UDP => unreachable!(),
-                    ProtocolType::TCP => {
-                        let peer_socket_addr = dial_info.to_socket_addr();
-                        RawTcpProtocolHandler::connect(None, peer_socket_addr, connect_timeout_ms)
-                            .await
-                            .wrap_err("connect failure")?
-                    }
-                    ProtocolType::WS | ProtocolType::WSS => {
-                        WebsocketProtocolHandler::connect(None, &dial_info, connect_timeout_ms)
-                            .await
-                            .wrap_err("connect failure")?
-                    }
-                });
 
-                network_result_try!(pnc.send(data).await.wrap_err("send failure")?);
-                self.network_manager()
-                    .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
+            match dial_info.protocol_type() {
+                ProtocolType::UDP => {
+                    let peer_socket_addr = dial_info.to_socket_addr();
+                    let h = RawUdpProtocolHandler::new_unspecified_bound_handler(&peer_socket_addr)
+                        .await
+                        .wrap_err("create socket failure")?;
+                    network_result_try!(h
+                        .send_message(data, peer_socket_addr)
+                        .await
+                        .wrap_err("send message failure")?);
+                    self.network_manager()
+                        .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
 
-                let out = network_result_try!(network_result_try!(timeout(timeout_ms, pnc.recv())
+                    // receive single response
+                    let mut out = vec![0u8; MAX_MESSAGE_SIZE];
+                    let (recv_len, recv_addr) = network_result_try!(timeout(
+                        timeout_ms,
+                        h.recv_message(&mut out).instrument(Span::current())
+                    )
                     .await
                     .into_network_result())
-                .wrap_err("recv failure")?);
+                    .wrap_err("recv_message failure")?;
 
-                self.network_manager()
-                    .stats_packet_rcvd(dial_info.to_ip_addr(), ByteCount::new(out.len() as u64));
+                    let recv_socket_addr = recv_addr.remote_address().to_socket_addr();
+                    self.network_manager()
+                        .stats_packet_rcvd(recv_socket_addr.ip(), ByteCount::new(recv_len as u64));
 
-                Ok(NetworkResult::Value(out))
+                    // if the from address is not the same as the one we sent to, then drop this
+                    if recv_socket_addr != peer_socket_addr {
+                        bail!("wrong address");
+                    }
+                    out.resize(recv_len, 0u8);
+                    Ok(NetworkResult::Value(out))
+                }
+                ProtocolType::TCP | ProtocolType::WS | ProtocolType::WSS => {
+                    let pnc = network_result_try!(match dial_info.protocol_type() {
+                        ProtocolType::UDP => unreachable!(),
+                        ProtocolType::TCP => {
+                            let peer_socket_addr = dial_info.to_socket_addr();
+                            RawTcpProtocolHandler::connect(
+                                None,
+                                peer_socket_addr,
+                                connect_timeout_ms,
+                            )
+                            .await
+                            .wrap_err("connect failure")?
+                        }
+                        ProtocolType::WS | ProtocolType::WSS => {
+                            WebsocketProtocolHandler::connect(None, &dial_info, connect_timeout_ms)
+                                .await
+                                .wrap_err("connect failure")?
+                        }
+                    });
+
+                    network_result_try!(pnc.send(data).await.wrap_err("send failure")?);
+                    self.network_manager()
+                        .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
+
+                    let out =
+                        network_result_try!(network_result_try!(timeout(timeout_ms, pnc.recv())
+                            .await
+                            .into_network_result())
+                        .wrap_err("recv failure")?);
+
+                    self.network_manager().stats_packet_rcvd(
+                        dial_info.to_ip_addr(),
+                        ByteCount::new(out.len() as u64),
+                    );
+
+                    Ok(NetworkResult::Value(out))
+                }
             }
-        }
+        })
+        .await
     }
 
     #[cfg_attr(feature="verbose-tracing", instrument(level="trace", err, skip(self, data), fields(data.len = data.len())))]
@@ -609,41 +637,44 @@ impl Network {
         dial_info: DialInfo,
         data: Vec<u8>,
     ) -> EyreResult<NetworkResult<ConnectionDescriptor>> {
-        let data_len = data.len();
-        let connection_descriptor;
-        if dial_info.protocol_type() == ProtocolType::UDP {
-            // Handle connectionless protocol
-            let peer_socket_addr = dial_info.to_socket_addr();
-            let ph = match self.find_best_udp_protocol_handler(&peer_socket_addr, &None) {
-                Some(ph) => ph,
-                None => bail!("no appropriate UDP protocol handler for dial_info"),
-            };
-            connection_descriptor = network_result_try!(ph
-                .send_message(data, peer_socket_addr)
-                .await
-                .wrap_err("failed to send data to dial info")?);
-        } else {
-            // Handle connection-oriented protocols
-            let conn = network_result_try!(
-                self.connection_manager()
-                    .get_or_create_connection(dial_info.clone())
-                    .await?
-            );
+        self.record_dial_info_failure(dial_info.clone(), async move {
+            let data_len = data.len();
+            let connection_descriptor;
+            if dial_info.protocol_type() == ProtocolType::UDP {
+                // Handle connectionless protocol
+                let peer_socket_addr = dial_info.to_socket_addr();
+                let ph = match self.find_best_udp_protocol_handler(&peer_socket_addr, &None) {
+                    Some(ph) => ph,
+                    None => bail!("no appropriate UDP protocol handler for dial_info"),
+                };
+                connection_descriptor = network_result_try!(ph
+                    .send_message(data, peer_socket_addr)
+                    .await
+                    .wrap_err("failed to send data to dial info")?);
+            } else {
+                // Handle connection-oriented protocols
+                let conn = network_result_try!(
+                    self.connection_manager()
+                        .get_or_create_connection(dial_info.clone())
+                        .await?
+                );
 
-            if let ConnectionHandleSendResult::NotSent(_) = conn.send_async(data).await {
-                return Ok(NetworkResult::NoConnection(io::Error::new(
-                    io::ErrorKind::ConnectionReset,
-                    "failed to send",
-                )));
+                if let ConnectionHandleSendResult::NotSent(_) = conn.send_async(data).await {
+                    return Ok(NetworkResult::NoConnection(io::Error::new(
+                        io::ErrorKind::ConnectionReset,
+                        "failed to send",
+                    )));
+                }
+                connection_descriptor = conn.connection_descriptor();
             }
-            connection_descriptor = conn.connection_descriptor();
-        }
 
-        // Network accounting
-        self.network_manager()
-            .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
+            // Network accounting
+            self.network_manager()
+                .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
 
-        Ok(NetworkResult::value(connection_descriptor))
+            Ok(NetworkResult::value(connection_descriptor))
+        })
+        .await
     }
 
     /////////////////////////////////////////////////////////////////
diff --git a/veilid-core/src/network_manager/send_data.rs b/veilid-core/src/network_manager/send_data.rs
index 70d9ce90..64aa550f 100644
--- a/veilid-core/src/network_manager/send_data.rs
+++ b/veilid-core/src/network_manager/send_data.rs
@@ -357,6 +357,24 @@ impl NetworkManager {
         //     log_net!(debug "Node contact failing over to Ordered for {}", target_node_ref.to_string().cyan());
         //     sequencing = Sequencing::PreferOrdered;
         // }
+    
+        // Deprioritize dial info that have recently failed
+        let address_filter = self.address_filter();
+        let mut dial_info_failures_map = BTreeMap::<DialInfo, Timestamp>::new();
+        for did in peer_b.signed_node_info().node_info().all_filtered_dial_info_details(DialInfoDetail::NO_SORT, |_| true) {
+            if let Some(ts) = address_filter.get_dial_info_failed_ts(&did.dial_info) {
+                dial_info_failures_map.insert(did.dial_info, ts);
+            }
+        }
+        let dif_sort: Option<Arc<dyn Fn(&DialInfoDetail, &DialInfoDetail) -> core::cmp::Ordering>> = if dial_info_failures_map.is_empty() {
+            None
+        } else {
+            Some(Arc::new(move |a: &DialInfoDetail, b: &DialInfoDetail| {    
+                let ats = dial_info_failures_map.get(&a.dial_info).copied().unwrap_or_default();
+                let bts = dial_info_failures_map.get(&b.dial_info).copied().unwrap_or_default();
+                ats.cmp(&bts)
+            }))
+        };
 
         // Get the best contact method with these parameters from the routing domain
         let cm = routing_table.get_contact_method(
@@ -365,6 +383,7 @@ impl NetworkManager {
             &peer_b,
             dial_info_filter,
             sequencing,
+            dif_sort,
         );
 
         // Translate the raw contact method to a referenced contact method
diff --git a/veilid-core/src/network_manager/wasm/mod.rs b/veilid-core/src/network_manager/wasm/mod.rs
index 7c3b40e0..c9a0c445 100644
--- a/veilid-core/src/network_manager/wasm/mod.rs
+++ b/veilid-core/src/network_manager/wasm/mod.rs
@@ -118,47 +118,66 @@ impl Network {
 
     /////////////////////////////////////////////////////////////////
 
+    // Record DialInfo failures
+    pub async fn record_dial_info_failure<T, F: Future<Output = EyreResult<NetworkResult<T>>>>(
+        &self,
+        dial_info: DialInfo,
+        fut: F,
+    ) -> EyreResult<NetworkResult<T>> {
+        let network_result = fut.await?;
+        if matches!(network_result, NetworkResult::NoConnection(_)) {
+            self.network_manager()
+                .address_filter()
+                .set_dial_info_failed(dial_info);
+        }
+        Ok(network_result)
+    }
+
     #[cfg_attr(feature="verbose-tracing", instrument(level="trace", err, skip(self, data), fields(data.len = data.len())))]
     pub async fn send_data_unbound_to_dial_info(
         &self,
         dial_info: DialInfo,
         data: Vec<u8>,
     ) -> EyreResult<NetworkResult<()>> {
-        let data_len = data.len();
-        let timeout_ms = {
-            let c = self.config.get();
-            c.network.connection_initial_timeout_ms
-        };
+        self.record_dial_info_failure(dial_info.clone(), async move {
+            let data_len = data.len();
+            let timeout_ms = {
+                let c = self.config.get();
+                c.network.connection_initial_timeout_ms
+            };
 
-        if self
-            .network_manager()
-            .address_filter()
-            .is_ip_addr_punished(dial_info.address().to_ip_addr())
-        {
-            return Ok(NetworkResult::no_connection_other("punished"));
-        }
-
-        match dial_info.protocol_type() {
-            ProtocolType::UDP => {
-                bail!("no support for UDP protocol")
+            if self
+                .network_manager()
+                .address_filter()
+                .is_ip_addr_punished(dial_info.address().to_ip_addr())
+            {
+                return Ok(NetworkResult::no_connection_other("punished"));
             }
-            ProtocolType::TCP => {
-                bail!("no support for TCP protocol")
-            }
-            ProtocolType::WS | ProtocolType::WSS => {
-                let pnc =
-                    network_result_try!(WebsocketProtocolHandler::connect(&dial_info, timeout_ms)
-                        .await
-                        .wrap_err("connect failure")?);
-                network_result_try!(pnc.send(data).await.wrap_err("send failure")?);
-            }
-        };
 
-        // Network accounting
-        self.network_manager()
-            .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
+            match dial_info.protocol_type() {
+                ProtocolType::UDP => {
+                    bail!("no support for UDP protocol")
+                }
+                ProtocolType::TCP => {
+                    bail!("no support for TCP protocol")
+                }
+                ProtocolType::WS | ProtocolType::WSS => {
+                    let pnc = network_result_try!(WebsocketProtocolHandler::connect(
+                        &dial_info, timeout_ms
+                    )
+                    .await
+                    .wrap_err("connect failure")?);
+                    network_result_try!(pnc.send(data).await.wrap_err("send failure")?);
+                }
+            };
 
-        Ok(NetworkResult::Value(()))
+            // Network accounting
+            self.network_manager()
+                .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
+
+            Ok(NetworkResult::Value(()))
+        })
+        .await
     }
 
     // Send data to a dial info, unbound, using a new connection from a random port
@@ -173,53 +192,59 @@ impl Network {
         data: Vec<u8>,
         timeout_ms: u32,
     ) -> EyreResult<NetworkResult<Vec<u8>>> {
-        let data_len = data.len();
-        let connect_timeout_ms = {
-            let c = self.config.get();
-            c.network.connection_initial_timeout_ms
-        };
+        self.record_dial_info_failure(dial_info.clone(), async move {
+            let data_len = data.len();
+            let connect_timeout_ms = {
+                let c = self.config.get();
+                c.network.connection_initial_timeout_ms
+            };
 
-        if self
-            .network_manager()
-            .address_filter()
-            .is_ip_addr_punished(dial_info.address().to_ip_addr())
-        {
-            return Ok(NetworkResult::no_connection_other("punished"));
-        }
+            if self
+                .network_manager()
+                .address_filter()
+                .is_ip_addr_punished(dial_info.address().to_ip_addr())
+            {
+                return Ok(NetworkResult::no_connection_other("punished"));
+            }
 
-        match dial_info.protocol_type() {
-            ProtocolType::UDP => {
-                bail!("no support for UDP protocol")
-            }
-            ProtocolType::TCP => {
-                bail!("no support for TCP protocol")
-            }
-            ProtocolType::WS | ProtocolType::WSS => {
-                let pnc = network_result_try!(match dial_info.protocol_type() {
-                    ProtocolType::UDP => unreachable!(),
-                    ProtocolType::TCP => unreachable!(),
-                    ProtocolType::WS | ProtocolType::WSS => {
-                        WebsocketProtocolHandler::connect(&dial_info, connect_timeout_ms)
+            match dial_info.protocol_type() {
+                ProtocolType::UDP => {
+                    bail!("no support for UDP protocol")
+                }
+                ProtocolType::TCP => {
+                    bail!("no support for TCP protocol")
+                }
+                ProtocolType::WS | ProtocolType::WSS => {
+                    let pnc = network_result_try!(match dial_info.protocol_type() {
+                        ProtocolType::UDP => unreachable!(),
+                        ProtocolType::TCP => unreachable!(),
+                        ProtocolType::WS | ProtocolType::WSS => {
+                            WebsocketProtocolHandler::connect(&dial_info, connect_timeout_ms)
+                                .await
+                                .wrap_err("connect failure")?
+                        }
+                    });
+
+                    network_result_try!(pnc.send(data).await.wrap_err("send failure")?);
+                    self.network_manager()
+                        .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
+
+                    let out =
+                        network_result_try!(network_result_try!(timeout(timeout_ms, pnc.recv())
                             .await
-                            .wrap_err("connect failure")?
-                    }
-                });
+                            .into_network_result())
+                        .wrap_err("recv failure")?);
 
-                network_result_try!(pnc.send(data).await.wrap_err("send failure")?);
-                self.network_manager()
-                    .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
+                    self.network_manager().stats_packet_rcvd(
+                        dial_info.to_ip_addr(),
+                        ByteCount::new(out.len() as u64),
+                    );
 
-                let out = network_result_try!(network_result_try!(timeout(timeout_ms, pnc.recv())
-                    .await
-                    .into_network_result())
-                .wrap_err("recv failure")?);
-
-                self.network_manager()
-                    .stats_packet_rcvd(dial_info.to_ip_addr(), ByteCount::new(out.len() as u64));
-
-                Ok(NetworkResult::Value(out))
+                    Ok(NetworkResult::Value(out))
+                }
             }
-        }
+        })
+        .await
     }
 
     #[cfg_attr(feature="verbose-tracing", instrument(level="trace", err, skip(self, data), fields(data.len = data.len())))]
@@ -273,34 +298,37 @@ impl Network {
         dial_info: DialInfo,
         data: Vec<u8>,
     ) -> EyreResult<NetworkResult<ConnectionDescriptor>> {
-        let data_len = data.len();
-        if dial_info.protocol_type() == ProtocolType::UDP {
-            bail!("no support for UDP protocol");
-        }
-        if dial_info.protocol_type() == ProtocolType::TCP {
-            bail!("no support for TCP protocol");
-        }
+        self.record_dial_info_failure(dial_info.clone(), async move {
+            let data_len = data.len();
+            if dial_info.protocol_type() == ProtocolType::UDP {
+                bail!("no support for UDP protocol");
+            }
+            if dial_info.protocol_type() == ProtocolType::TCP {
+                bail!("no support for TCP protocol");
+            }
 
-        // Handle connection-oriented protocols
-        let conn = network_result_try!(
-            self.connection_manager()
-                .get_or_create_connection(dial_info.clone())
-                .await?
-        );
+            // Handle connection-oriented protocols
+            let conn = network_result_try!(
+                self.connection_manager()
+                    .get_or_create_connection(dial_info.clone())
+                    .await?
+            );
 
-        if let ConnectionHandleSendResult::NotSent(_) = conn.send_async(data).await {
-            return Ok(NetworkResult::NoConnection(io::Error::new(
-                io::ErrorKind::ConnectionReset,
-                "failed to send",
-            )));
-        }
-        let connection_descriptor = conn.connection_descriptor();
+            if let ConnectionHandleSendResult::NotSent(_) = conn.send_async(data).await {
+                return Ok(NetworkResult::NoConnection(io::Error::new(
+                    io::ErrorKind::ConnectionReset,
+                    "failed to send",
+                )));
+            }
+            let connection_descriptor = conn.connection_descriptor();
 
-        // Network accounting
-        self.network_manager()
-            .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
+            // Network accounting
+            self.network_manager()
+                .stats_packet_sent(dial_info.to_ip_addr(), ByteCount::new(data_len as u64));
 
-        Ok(NetworkResult::value(connection_descriptor))
+            Ok(NetworkResult::value(connection_descriptor))
+        })
+        .await
     }
 
     /////////////////////////////////////////////////////////////////
@@ -320,8 +348,8 @@ impl Network {
             }
 
             // XXX: See issue #92
-            let family_global = AddressTypeSet::all();
-            let family_local = AddressTypeSet::all();
+            let family_global = AddressTypeSet::from(AddressType::IPV4);
+            let family_local = AddressTypeSet::from(AddressType::IPV4);
 
             ProtocolConfig {
                 outbound,
diff --git a/veilid-core/src/routing_table/mod.rs b/veilid-core/src/routing_table/mod.rs
index 2f63df4a..82d9ed78 100644
--- a/veilid-core/src/routing_table/mod.rs
+++ b/veilid-core/src/routing_table/mod.rs
@@ -538,6 +538,7 @@ impl RoutingTable {
         peer_b: &PeerInfo,
         dial_info_filter: DialInfoFilter,
         sequencing: Sequencing,
+        dif_sort: Option<Arc<dyn Fn(&DialInfoDetail, &DialInfoDetail) -> core::cmp::Ordering>>,
     ) -> ContactMethod {
         self.inner.read().get_contact_method(
             routing_domain,
@@ -545,6 +546,7 @@ impl RoutingTable {
             peer_b,
             dial_info_filter,
             sequencing,
+            dif_sort,
         )
     }
 
diff --git a/veilid-core/src/routing_table/route_spec_store/route_spec_store.rs b/veilid-core/src/routing_table/route_spec_store/route_spec_store.rs
index fb85fcc9..1889abca 100644
--- a/veilid-core/src/routing_table/route_spec_store/route_spec_store.rs
+++ b/veilid-core/src/routing_table/route_spec_store/route_spec_store.rs
@@ -401,6 +401,7 @@ impl RouteSpecStore {
                         current_node,
                         DialInfoFilter::all(),
                         sequencing,
+                        None,
                     );
                     if matches!(cm, ContactMethod::Unreachable) {
                         reachable = false;
@@ -415,6 +416,7 @@ impl RouteSpecStore {
                             current_node,
                             DialInfoFilter::all(),
                             Sequencing::EnsureOrdered,
+                            None,
                         );
                         if matches!(cm, ContactMethod::Unreachable) {
                             can_do_sequenced = false;
@@ -438,6 +440,7 @@ impl RouteSpecStore {
                         current_node,
                         DialInfoFilter::all(),
                         sequencing,
+                        None,
                     );
                     if matches!(cm, ContactMethod::Unreachable) {
                         reachable = false;
@@ -452,6 +455,7 @@ impl RouteSpecStore {
                             current_node,
                             DialInfoFilter::all(),
                             Sequencing::EnsureOrdered,
+                            None,
                         );
                         if matches!(cm, ContactMethod::Unreachable) {
                             can_do_sequenced = false;
diff --git a/veilid-core/src/routing_table/routing_domains.rs b/veilid-core/src/routing_table/routing_domains.rs
index 7f60cc04..d92dddc9 100644
--- a/veilid-core/src/routing_table/routing_domains.rs
+++ b/veilid-core/src/routing_table/routing_domains.rs
@@ -220,6 +220,7 @@ pub trait RoutingDomainDetail {
         peer_b: &PeerInfo,
         dial_info_filter: DialInfoFilter,
         sequencing: Sequencing,
+        dif_sort: Option<Arc<dyn Fn(&DialInfoDetail, &DialInfoDetail) -> core::cmp::Ordering>>,
     ) -> ContactMethod;
 }
 
@@ -245,6 +246,7 @@ fn first_filtered_dial_info_detail_between_nodes(
     to_node: &NodeInfo,
     dial_info_filter: &DialInfoFilter,
     sequencing: Sequencing,
+    dif_sort: Option<Arc<dyn Fn(&DialInfoDetail, &DialInfoDetail) -> core::cmp::Ordering>>
 ) -> Option<DialInfoDetail> {
     let dial_info_filter = dial_info_filter.clone().filtered(
         &DialInfoFilter::all()
@@ -253,11 +255,28 @@ fn first_filtered_dial_info_detail_between_nodes(
     );
 
     // Apply sequencing and get sort
+    // Include sorting by external dial info sort for rotating through dialinfo
+    // based on an external preference table, for example the one kept by 
+    // AddressFilter to deprioritize dialinfo that have recently failed to connect
     let (ordered, dial_info_filter) = dial_info_filter.with_sequencing(sequencing);
-    let sort = if ordered {
-        Some(DialInfoDetail::ordered_sequencing_sort)
+    let sort: Option<Box<dyn Fn(&DialInfoDetail, &DialInfoDetail) -> core::cmp::Ordering>> = if ordered {
+        if let Some(dif_sort) = dif_sort {
+            Some(Box::new(move |a, b| {
+                let mut ord = dif_sort(a,b);
+                if ord == core::cmp::Ordering::Equal {
+                    ord = DialInfoDetail::ordered_sequencing_sort(a,b);
+                }
+                ord
+            }))
+        } else {
+            Some(Box::new(move |a,b| { DialInfoDetail::ordered_sequencing_sort(a,b) }))
+        }
     } else {
-        None
+        if let Some(dif_sort) = dif_sort {
+            Some(Box::new(move |a,b| { dif_sort(a,b) }))
+        } else {
+            None
+        }
     };
 
     // If the filter is dead then we won't be able to connect
@@ -287,6 +306,7 @@ impl RoutingDomainDetail for PublicInternetRoutingDomainDetail {
         peer_b: &PeerInfo,
         dial_info_filter: DialInfoFilter,
         sequencing: Sequencing,
+        dif_sort: Option<Arc<dyn Fn(&DialInfoDetail, &DialInfoDetail) -> core::cmp::Ordering>>,
     ) -> ContactMethod {
         // Get the nodeinfos for convenience
         let node_a = peer_a.signed_node_info().node_info();
@@ -304,7 +324,7 @@ impl RoutingDomainDetail for PublicInternetRoutingDomainDetail {
 
         // Get the best match dial info for node B if we have it
         if let Some(target_did) =
-            first_filtered_dial_info_detail_between_nodes(node_a, node_b, &dial_info_filter, sequencing)
+            first_filtered_dial_info_detail_between_nodes(node_a, node_b, &dial_info_filter, sequencing, dif_sort.clone())
         {
             // Do we need to signal before going inbound?
             if !target_did.class.requires_signal() {
@@ -334,6 +354,7 @@ impl RoutingDomainDetail for PublicInternetRoutingDomainDetail {
                     node_b_relay,
                     &dial_info_filter,
                     sequencing,
+                    dif_sort.clone(),
                 )
                 .is_some()
                 {
@@ -347,6 +368,7 @@ impl RoutingDomainDetail for PublicInternetRoutingDomainDetail {
                             node_a,
                             &dial_info_filter,
                             sequencing,
+                            dif_sort.clone()
                         ) {
                             // Ensure we aren't on the same public IP address (no hairpin nat)
                             if reverse_did.dial_info.to_ip_addr()
@@ -373,6 +395,7 @@ impl RoutingDomainDetail for PublicInternetRoutingDomainDetail {
                             node_b,
                             &udp_dial_info_filter,
                             sequencing,
+                            dif_sort.clone()
                         ) {
                             // Does node A have a direct udp dialinfo that node B can reach?
                             if let Some(reverse_udp_did) = first_filtered_dial_info_detail_between_nodes(
@@ -380,6 +403,7 @@ impl RoutingDomainDetail for PublicInternetRoutingDomainDetail {
                                 node_a,
                                 &udp_dial_info_filter,
                                 sequencing,
+                                dif_sort.clone(),
                             ) {
                                 // Ensure we aren't on the same public IP address (no hairpin nat)
                                 if reverse_udp_did.dial_info.to_ip_addr()
@@ -422,6 +446,7 @@ impl RoutingDomainDetail for PublicInternetRoutingDomainDetail {
                 &node_b_relay,
                 &dial_info_filter,
                 sequencing,
+                dif_sort.clone()
             )
             .is_some()
             {
@@ -496,6 +521,7 @@ impl RoutingDomainDetail for LocalNetworkRoutingDomainDetail {
         peer_b: &PeerInfo,
         dial_info_filter: DialInfoFilter,
         sequencing: Sequencing,
+        dif_sort: Option<Arc<dyn Fn(&DialInfoDetail, &DialInfoDetail) -> core::cmp::Ordering>>,
     ) -> ContactMethod {
         // Scope the filter down to protocols node A can do outbound
         let dial_info_filter = dial_info_filter.filtered(
@@ -504,20 +530,31 @@ impl RoutingDomainDetail for LocalNetworkRoutingDomainDetail {
                 .with_protocol_type_set(peer_a.signed_node_info().node_info().outbound_protocols()),
         );
         
-        // Get first filtered dialinfo
-        let (sort, dial_info_filter) = match sequencing {
-            Sequencing::NoPreference => (None, dial_info_filter),
-            Sequencing::PreferOrdered => (
-                Some(DialInfoDetail::ordered_sequencing_sort),
-                dial_info_filter,
-            ),
-            Sequencing::EnsureOrdered => (
-                Some(DialInfoDetail::ordered_sequencing_sort),
-                dial_info_filter.filtered(
-                    &DialInfoFilter::all().with_protocol_type_set(ProtocolType::all_ordered_set()),
-                ),
-            ),
+        // Apply sequencing and get sort
+        // Include sorting by external dial info sort for rotating through dialinfo
+        // based on an external preference table, for example the one kept by 
+        // AddressFilter to deprioritize dialinfo that have recently failed to connect
+        let (ordered, dial_info_filter) = dial_info_filter.with_sequencing(sequencing);
+        let sort: Option<Box<dyn Fn(&DialInfoDetail, &DialInfoDetail) -> core::cmp::Ordering>> = if ordered {
+            if let Some(dif_sort) = dif_sort {
+                Some(Box::new(move |a, b| {
+                    let mut ord = dif_sort(a,b);
+                    if ord == core::cmp::Ordering::Equal {
+                        ord = DialInfoDetail::ordered_sequencing_sort(a,b);
+                    }
+                    ord
+                }))
+            } else {
+                Some(Box::new(move |a,b| { DialInfoDetail::ordered_sequencing_sort(a,b) }))
+            }
+        } else {
+            if let Some(dif_sort) = dif_sort {
+                Some(Box::new(move |a,b| { dif_sort(a,b) }))
+            } else {
+                None
+            }
         };
+
         // If the filter is dead then we won't be able to connect
         if dial_info_filter.is_dead() {
             return ContactMethod::Unreachable;
diff --git a/veilid-core/src/routing_table/routing_table_inner.rs b/veilid-core/src/routing_table/routing_table_inner.rs
index 029f5da8..b6f24908 100644
--- a/veilid-core/src/routing_table/routing_table_inner.rs
+++ b/veilid-core/src/routing_table/routing_table_inner.rs
@@ -226,9 +226,10 @@ impl RoutingTableInner {
         peer_b: &PeerInfo,
         dial_info_filter: DialInfoFilter,
         sequencing: Sequencing,
+        dif_sort: Option<Arc<dyn Fn(&DialInfoDetail, &DialInfoDetail) -> core::cmp::Ordering>>,
     ) -> ContactMethod {
         self.with_routing_domain(routing_domain, |rdd| {
-            rdd.get_contact_method(self, peer_a, peer_b, dial_info_filter, sequencing)
+            rdd.get_contact_method(self, peer_a, peer_b, dial_info_filter, sequencing, dif_sort)
         })
     }
 
diff --git a/veilid-flutter/example/pubspec.lock b/veilid-flutter/example/pubspec.lock
index b3d588dd..73c75fc4 100644
--- a/veilid-flutter/example/pubspec.lock
+++ b/veilid-flutter/example/pubspec.lock
@@ -403,7 +403,7 @@ packages:
       path: ".."
       relative: true
     source: path
-    version: "0.1.9"
+    version: "0.1.10"
   web:
     dependency: transitive
     description:
diff --git a/veilid-flutter/example/reset_run.bat b/veilid-flutter/example/reset_run.bat
index 3d53a241..c6bea309 100644
--- a/veilid-flutter/example/reset_run.bat
+++ b/veilid-flutter/example/reset_run.bat
@@ -1,2 +1,2 @@
 @echo off
-flutter run --dart-define=DELETE_TABLE_STORE=1 --dart-define=DELETE_PROTECTED_STORE=1 --dart-define=DELETE_BLOCK_STORE=1
+flutter run --dart-define=DELETE_TABLE_STORE=1 --dart-define=DELETE_PROTECTED_STORE=1 --dart-define=DELETE_BLOCK_STORE=1 %*
diff --git a/veilid-flutter/example/reset_run.sh b/veilid-flutter/example/reset_run.sh
index eb6fda85..7c9c0c73 100755
--- a/veilid-flutter/example/reset_run.sh
+++ b/veilid-flutter/example/reset_run.sh
@@ -1,2 +1,2 @@
 #!/bin/bash
-flutter run --dart-define=DELETE_TABLE_STORE=1 --dart-define=DELETE_PROTECTED_STORE=1 --dart-define=DELETE_BLOCK_STORE=1
+flutter run --dart-define=DELETE_TABLE_STORE=1 --dart-define=DELETE_PROTECTED_STORE=1 --dart-define=DELETE_BLOCK_STORE=1 $@
diff --git a/veilid-flutter/lib/default_config.dart b/veilid-flutter/lib/default_config.dart
index 5ee45061..660722cd 100644
--- a/veilid-flutter/lib/default_config.dart
+++ b/veilid-flutter/lib/default_config.dart
@@ -58,129 +58,133 @@ int getRemoteMaxStorageSpaceMb() {
   return 256;
 }
 
-Future<VeilidConfig> getDefaultVeilidConfig(String programName) async =>
-    VeilidConfig(
-      programName: programName,
-      namespace: '',
-      capabilities: const VeilidConfigCapabilities(disable: []),
-      protectedStore: const VeilidConfigProtectedStore(
-        allowInsecureFallback: false,
-        alwaysUseInsecureStorage: false,
-        directory: '',
-        delete: false,
-        deviceEncryptionKeyPassword: '',
+Future<VeilidConfig> getDefaultVeilidConfig(String programName) async {
+  // ignore: do_not_use_environment
+  const bootstrap = String.fromEnvironment('BOOTSTRAP');
+  return VeilidConfig(
+    programName: programName,
+    namespace: '',
+    capabilities: const VeilidConfigCapabilities(disable: []),
+    protectedStore: const VeilidConfigProtectedStore(
+      allowInsecureFallback: false,
+      alwaysUseInsecureStorage: false,
+      directory: '',
+      delete: false,
+      deviceEncryptionKeyPassword: '',
+    ),
+    tableStore: VeilidConfigTableStore(
+      directory: kIsWeb
+          ? ''
+          : p.join((await getApplicationSupportDirectory()).absolute.path,
+              'table_store'),
+      delete: false,
+    ),
+    blockStore: VeilidConfigBlockStore(
+      directory: kIsWeb
+          ? ''
+          : p.join((await getApplicationSupportDirectory()).absolute.path,
+              'block_store'),
+      delete: false,
+    ),
+    network: VeilidConfigNetwork(
+      connectionInitialTimeoutMs: 2000,
+      connectionInactivityTimeoutMs: 60000,
+      maxConnectionsPerIp4: 32,
+      maxConnectionsPerIp6Prefix: 32,
+      maxConnectionsPerIp6PrefixSize: 56,
+      maxConnectionFrequencyPerMin: 128,
+      clientWhitelistTimeoutMs: 300000,
+      reverseConnectionReceiptTimeMs: 5000,
+      holePunchReceiptTimeMs: 5000,
+      routingTable: VeilidConfigRoutingTable(
+        nodeId: [],
+        nodeIdSecret: [],
+        bootstrap: bootstrap.isNotEmpty
+            ? bootstrap.split(',')
+            : (kIsWeb
+                ? ['ws://bootstrap.veilid.net:5150/ws']
+                : ['bootstrap.veilid.net']),
+        limitOverAttached: 64,
+        limitFullyAttached: 32,
+        limitAttachedStrong: 16,
+        limitAttachedGood: 8,
+        limitAttachedWeak: 4,
       ),
-      tableStore: VeilidConfigTableStore(
-        directory: kIsWeb
-            ? ''
-            : p.join((await getApplicationSupportDirectory()).absolute.path,
-                'table_store'),
-        delete: false,
+      rpc: const VeilidConfigRPC(
+        concurrency: 0,
+        queueSize: 1024,
+        maxTimestampBehindMs: 10000,
+        maxTimestampAheadMs: 10000,
+        timeoutMs: 5000,
+        maxRouteHopCount: 4,
+        defaultRouteHopCount: 1,
       ),
-      blockStore: VeilidConfigBlockStore(
-        directory: kIsWeb
-            ? ''
-            : p.join((await getApplicationSupportDirectory()).absolute.path,
-                'block_store'),
-        delete: false,
-      ),
-      network: VeilidConfigNetwork(
+      dht: VeilidConfigDHT(
+          resolveNodeTimeoutMs: 10000,
+          resolveNodeCount: 1,
+          resolveNodeFanout: 4,
+          maxFindNodeCount: 20,
+          getValueTimeoutMs: 10000,
+          getValueCount: 3,
+          getValueFanout: 4,
+          setValueTimeoutMs: 10000,
+          setValueCount: 4,
+          setValueFanout: 6,
+          minPeerCount: 20,
+          minPeerRefreshTimeMs: 60000,
+          validateDialInfoReceiptTimeMs: 2000,
+          localSubkeyCacheSize: getLocalSubkeyCacheSize(),
+          localMaxSubkeyCacheMemoryMb: await getLocalMaxSubkeyCacheMemoryMb(),
+          remoteSubkeyCacheSize: getRemoteSubkeyCacheSize(),
+          remoteMaxRecords: getRemoteMaxRecords(),
+          remoteMaxSubkeyCacheMemoryMb: await getRemoteMaxSubkeyCacheMemoryMb(),
+          remoteMaxStorageSpaceMb: getRemoteMaxStorageSpaceMb()),
+      upnp: true,
+      detectAddressChanges: true,
+      restrictedNatRetries: 0,
+      tls: const VeilidConfigTLS(
+        certificatePath: '',
+        privateKeyPath: '',
         connectionInitialTimeoutMs: 2000,
-        connectionInactivityTimeoutMs: 60000,
-        maxConnectionsPerIp4: 32,
-        maxConnectionsPerIp6Prefix: 32,
-        maxConnectionsPerIp6PrefixSize: 56,
-        maxConnectionFrequencyPerMin: 128,
-        clientWhitelistTimeoutMs: 300000,
-        reverseConnectionReceiptTimeMs: 5000,
-        holePunchReceiptTimeMs: 5000,
-        routingTable: const VeilidConfigRoutingTable(
-          nodeId: [],
-          nodeIdSecret: [],
-          bootstrap: kIsWeb
-              ? ['ws://bootstrap.veilid.net:5150/ws']
-              : ['bootstrap.veilid.net'],
-          limitOverAttached: 64,
-          limitFullyAttached: 32,
-          limitAttachedStrong: 16,
-          limitAttachedGood: 8,
-          limitAttachedWeak: 4,
+      ),
+      application: const VeilidConfigApplication(
+          https: VeilidConfigHTTPS(
+            enabled: false,
+            listenAddress: '',
+            path: '',
+          ),
+          http: VeilidConfigHTTP(
+            enabled: false,
+            listenAddress: '',
+            path: '',
+          )),
+      protocol: const VeilidConfigProtocol(
+        udp: VeilidConfigUDP(
+          enabled: !kIsWeb,
+          socketPoolSize: 0,
+          listenAddress: '',
         ),
-        rpc: const VeilidConfigRPC(
-          concurrency: 0,
-          queueSize: 1024,
-          maxTimestampBehindMs: 10000,
-          maxTimestampAheadMs: 10000,
-          timeoutMs: 5000,
-          maxRouteHopCount: 4,
-          defaultRouteHopCount: 1,
+        tcp: VeilidConfigTCP(
+          connect: !kIsWeb,
+          listen: !kIsWeb,
+          maxConnections: 32,
+          listenAddress: '',
         ),
-        dht: VeilidConfigDHT(
-            resolveNodeTimeoutMs: 10000,
-            resolveNodeCount: 1,
-            resolveNodeFanout: 4,
-            maxFindNodeCount: 20,
-            getValueTimeoutMs: 10000,
-            getValueCount: 3,
-            getValueFanout: 4,
-            setValueTimeoutMs: 10000,
-            setValueCount: 4,
-            setValueFanout: 6,
-            minPeerCount: 20,
-            minPeerRefreshTimeMs: 60000,
-            validateDialInfoReceiptTimeMs: 2000,
-            localSubkeyCacheSize: getLocalSubkeyCacheSize(),
-            localMaxSubkeyCacheMemoryMb: await getLocalMaxSubkeyCacheMemoryMb(),
-            remoteSubkeyCacheSize: getRemoteSubkeyCacheSize(),
-            remoteMaxRecords: getRemoteMaxRecords(),
-            remoteMaxSubkeyCacheMemoryMb:
-                await getRemoteMaxSubkeyCacheMemoryMb(),
-            remoteMaxStorageSpaceMb: getRemoteMaxStorageSpaceMb()),
-        upnp: true,
-        detectAddressChanges: true,
-        restrictedNatRetries: 0,
-        tls: const VeilidConfigTLS(
-          certificatePath: '',
-          privateKeyPath: '',
-          connectionInitialTimeoutMs: 2000,
+        ws: VeilidConfigWS(
+          connect: true,
+          listen: !kIsWeb,
+          maxConnections: 32,
+          listenAddress: '',
+          path: 'ws',
         ),
-        application: const VeilidConfigApplication(
-            https: VeilidConfigHTTPS(
-              enabled: false,
-              listenAddress: '',
-              path: '',
-            ),
-            http: VeilidConfigHTTP(
-              enabled: false,
-              listenAddress: '',
-              path: '',
-            )),
-        protocol: const VeilidConfigProtocol(
-          udp: VeilidConfigUDP(
-            enabled: !kIsWeb,
-            socketPoolSize: 0,
-            listenAddress: '',
-          ),
-          tcp: VeilidConfigTCP(
-            connect: !kIsWeb,
-            listen: !kIsWeb,
-            maxConnections: 32,
-            listenAddress: '',
-          ),
-          ws: VeilidConfigWS(
-            connect: true,
-            listen: !kIsWeb,
-            maxConnections: 16,
-            listenAddress: '',
-            path: 'ws',
-          ),
-          wss: VeilidConfigWSS(
-            connect: true,
-            listen: false,
-            maxConnections: 16,
-            listenAddress: '',
-            path: 'ws',
-          ),
+        wss: VeilidConfigWSS(
+          connect: true,
+          listen: false,
+          maxConnections: 32,
+          listenAddress: '',
+          path: 'ws',
         ),
       ),
-    );
+    ),
+  );
+}
diff --git a/veilid-server/src/settings.rs b/veilid-server/src/settings.rs
index 4066e59e..631e405a 100644
--- a/veilid-server/src/settings.rs
+++ b/veilid-server/src/settings.rs
@@ -143,14 +143,14 @@ core:
             ws:
                 connect: true
                 listen: true
-                max_connections: 16
+                max_connections: 32
                 listen_address: ''
                 path: 'ws'
                 # url: 'ws://localhost:5150/ws'
             wss:
                 connect: true
                 listen: false
-                max_connections: 16
+                max_connections: 32
                 listen_address: ''
                 path: 'ws'
                 # url: ''
@@ -1686,7 +1686,7 @@ mod tests {
         //
         assert_eq!(s.core.network.protocol.ws.connect, true);
         assert_eq!(s.core.network.protocol.ws.listen, true);
-        assert_eq!(s.core.network.protocol.ws.max_connections, 16);
+        assert_eq!(s.core.network.protocol.ws.max_connections, 32);
         assert_eq!(s.core.network.protocol.ws.listen_address.name, "");
         assert_eq!(s.core.network.protocol.ws.listen_address.addrs, vec![]);
         assert_eq!(
@@ -1697,7 +1697,7 @@ mod tests {
         //
         assert_eq!(s.core.network.protocol.wss.connect, true);
         assert_eq!(s.core.network.protocol.wss.listen, false);
-        assert_eq!(s.core.network.protocol.wss.max_connections, 16);
+        assert_eq!(s.core.network.protocol.wss.max_connections, 32);
         assert_eq!(s.core.network.protocol.wss.listen_address.name, "");
         assert_eq!(s.core.network.protocol.wss.listen_address.addrs, vec![]);
         assert_eq!(
diff --git a/veilid-wasm/wasm_build.sh b/veilid-wasm/wasm_build.sh
index b450baeb..c38f8bff 100755
--- a/veilid-wasm/wasm_build.sh
+++ b/veilid-wasm/wasm_build.sh
@@ -34,7 +34,7 @@ else
     OUTPUTDIR=../target/wasm32-unknown-unknown/debug/pkg
     INPUTDIR=../target/wasm32-unknown-unknown/debug
 
-    RUSTFLAGS="-O -g" cargo build --target wasm32-unknown-unknown
+    RUSTFLAGS="-O -g $RUSTFLAGS" cargo build --target wasm32-unknown-unknown
     mkdir -p $OUTPUTDIR
     wasm-bindgen --out-dir $OUTPUTDIR --target web --keep-debug --debug $INPUTDIR/veilid_wasm.wasm
     ./wasm-sourcemap.py $OUTPUTDIR/veilid_wasm_bg.wasm -o $OUTPUTDIR/veilid_wasm_bg.wasm.map --dwarfdump $DWARFDUMP
@@ -44,4 +44,4 @@ fi
 popd &> /dev/null
 
 # Print for use with scripts
-echo SUCCESS:OUTPUTDIR=$(get_abs_filename $OUTPUTDIR)
\ No newline at end of file
+echo SUCCESS:OUTPUTDIR=$(get_abs_filename $OUTPUTDIR)