diff --git a/BOOTSTRAP-SETUP.md b/BOOTSTRAP-SETUP.md
index 5f25405a..624eedee 100755
--- a/BOOTSTRAP-SETUP.md
+++ b/BOOTSTRAP-SETUP.md
@@ -1,4 +1,11 @@
-# Starting a Generic/Public Veilid Bootstrap Server
+# Creating a Veilid Bootstrap Server
+
+There are two versions of the Veilid bootstrap:
+
+ * Version 0: Unsigned bootstrap - Any node can bootstrap with your server, but no guarantees are provided to those nodes about which network was connected, as DNS is subject to MITM attacks
+ * Version 1: Signed bootstrap - Any node can bootstrap with your server, and if they have your bootstrap signing public key in their trusted keys config, they will get a verified bootstrap to a specific network
+
+These instructions will cover both versions, however Version 1 is preferable, as Version 0 will eventually be deprecated.
## Instance Recommended Setup
@@ -8,6 +15,8 @@ Storage: 25GB
IP: Static v4 & v6
Firewall: 5150/TCP/UDP inbound allow all
+You will need to ensure your bootstrap server has PTR records for its hostname, as the TXT records generated for the bootstrap will rely on the hostname existing and being consistent for all of its IP addresses.
+
## Install Veilid
Follow instructions in [INSTALL.md](./INSTALL.md)
@@ -20,11 +29,36 @@ Follow instructions in [INSTALL.md](./INSTALL.md)
sudo systemctl stop veilid-server.service
```
+### Create a bootstrap signing key
+
+You need a 'bootstrap signing key' to sign your bootstrap server records. A single signing key can be used to sign multiple bootstrap server records. If you don't have one yet, with `veilid-server` not already running:
+
+```shell
+sudo -u veilid veilid-server --generate-key-pair VLD0
+```
+which outputs a key in this form (example: `VLD0:NAPctwUP5NNynWdkX8rcUz_yk44v-cHuDM9ZzvsDXnQ:-ncghvgw2NFQK2RH2vCfvCJj3M3gTVOD-UM08-7n6kQ`):
+```
+VLD0:PUBLIC_KEY:SECRET_KEY
+```
+
+Copy down the generated keypair and store it in a secure location, preferably offline.
+Remove the part after the second colon (the SECRET_KEY), and this is your 'Bootstrap Signing Public Key' (should look like: `VLD0:NAPctwUP5NNynWdkX8rcUz_yk44v-cHuDM9ZzvsDXnQ`)
+
### Setup the config
-In `/etc/veilid-server/veilid-server.conf`, ensure `bootstrap: ['bootstrap.']` in the `routing_table:` section.
+In `/etc/veilid-server/veilid-server.conf` ensure these keys are in the in the `routing_table:` section
-If you came here from the [dev network setup](./dev-setup/dev-network-setup.md) guide, this is when you set the network key.
+- `bootstrap: ['bootstrap.']`
+
+- V0: Use an empty bootstrap key list to enable unverified bootstrap
+ - `bootstrap_keys: []`
+- V1: Add your bootstrap signing public key to this list.
+ - If your signing key is the only one:
+ - `bootstrap_keys: ['VLD0:']`
+ - You may also want to include any other signing keys for bootstraps you trust. If this is a bootstrap for the main Veilid network, include Veilid Foundation's signing keys here as well
+ - `bootstrap_keys: ['VLD0:', 'VLD0:Vj0lKDdUQXmQ5Ol1SZdlvXkBHUccBcQvGLN9vbLSI7k', 'VLD0:QeQJorqbXtC7v3OlynCZ_W3m76wGNeB5NTF81ypqHAo','VLD0:QNdcl-0OiFfYVj9331XVR6IqZ49NG-E18d5P7lwi4TA']`
+
+(If you came here from the [dev network setup](./dev-setup/dev-network-setup.md) guide, this is when you set the network key as well in the `network_key_password` field of the `network:` section)
**Switch to veilid user**
@@ -53,7 +87,12 @@ veilid-server --set-node-id [PUBLIC_KEY] --delete-table-store
Copy the output to secure storage. This information will be use to setup DNS records.
```shell
-veilid-server --dump-txt-record
+veilid-server --dump-txt-record
+```
+
+(will look like this, but with your own key:)
+```shell
+veilid-server --dump-txt-record VLD0:NAPctwUP5NNynWdkX8rcUz_yk44v-cHuDM9ZzvsDXnQ:-ncghvgw2NFQK2RH2vCfvCJj3M3gTVOD-UM08-7n6kQ
```
### Start the Veilid service
@@ -78,12 +117,25 @@ Create the following DNS Records for your domain:
(This example assumes two bootstrap servers are being created)
-| Record | Value | Record Type |
-|-----------|-----------------------------|-------------|
-|bootstrap | 1,2 | TXT |
-|1.bootstrap| IPv4 | A |
-|1.bootstrap| IPv6 | AAAA |
-|1.bootstrap| output of --dump-txt-record | TXT |
-|2.bootstrap| IPv4 | A |
-|2.bootstrap| IPv6 | AAAA |
-|2.bootstrap| output of --dump-txt-record | TXT |
+V1:
+| Record | Value | Record Type |
+| ------------ | ---------------------------- | ----------- |
+| bootstrap-v1 | IPv4 of bootstrap 1 | A |
+| bootstrap-v1 | IPv4 of bootstrap 2 | A |
+| bootstrap-v1 | IPv6 of bootstrap 1 | AAAA |
+| bootstrap-v1 | IPv6 of bootstrap 2 | AAAA |
+| bootstrap-v1 | TXTRecord v0 for bootstrap 1 | TXT |
+| bootstrap-v1 | TXTRecord v1 for bootstrap 1 | TXT |
+| bootstrap-v1 | TXTRecord v0 for bootstrap 2 | TXT |
+| bootstrap-v1 | TXTRecord v1 for bootstrap 2 | TXT |
+
+V0:
+| Record | Value | Record Type |
+| ----------- | ---------------------------- | ----------- |
+| bootstrap | 1,2 | TXT |
+| 1.bootstrap | IPv4 | A |
+| 1.bootstrap | IPv6 | AAAA |
+| 1.bootstrap | TXTRecord v0 for bootstrap 1 | TXT |
+| 2.bootstrap | IPv4 | A |
+| 2.bootstrap | IPv6 | AAAA |
+| 2.bootstrap | TXTRecord v0 for bootstrap 2 | TXT |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1dc27dc..4d1dda04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,13 +10,14 @@
- `VeilidConfigInner` -> `VeilidConfig`
- veilid-core:
+ - **Security** Signed bootstrap v1 added which closes #293: https://gitlab.com/veilid/veilid/-/issues/293
- Allow shutdown even if tables are closed
- New, more robust, watchvalue implementation
- Consensus is now counted from the nodes closest to the key, excluding attempts that have failed, but including new nodes that show up, requiring N out of the M closest nodes to have succeeded and all have been attempted.
- Watching a node now also triggers an background inspection+valueget to detect if values have changed online
- Fanout queue disqualifaction for distance-based rejections reimplemented
- Local rehydration implemented. DHT record subkey data that does not have sufficient consensus online is re-pushed to keep it alive when records are opened.
- - Direct bootstrap now filters out Relayed nodes correctly
+ - Direct bootstrap v0 now filters out Relayed nodes correctly
- Closed issue #400: https://gitlab.com/veilid/veilid/-/issues/400
- Closed issue #377: https://gitlab.com/veilid/veilid/-/issues/377
- Add the `veilid_features()` API, which lists the compile-time features that were enabled when `veilid-core` was built (available in language bindings as well). ([!401](https://gitlab.com/veilid/veilid/-/issues/400))
diff --git a/dev-setup/dev-network-setup.md b/dev-setup/dev-network-setup.md
index e5e822bb..e9d05626 100644
--- a/dev-setup/dev-network-setup.md
+++ b/dev-setup/dev-network-setup.md
@@ -6,7 +6,7 @@ There will be times when a contibutor wishes to dynamically test their work on l
This document outlines the process of using the steps found in [INSTALL.md](../INSTALL.md) and [BOOTSTRAP-SETUP.md](../BOOTSTRAP-SETUP.md) with some modifications which results in a reasonably isolated and independent network of Veilid development nodes which do not communicate with nodes on the actual Veilid network.
-The minimum topology of a dev network is 1 bootstrap server and 4 nodes, all with public IP addresses with port 5150/TCP open. This allows enabling public address detection and private routing. The minimum specifications are 1 vCPU, 1GB RAM, and 25 GB storage.
+The minimum topology of a dev network is 1 bootstrap server and 4 nodes, all with public IP addresses with port 5150/TCP open. It is preferable to open port 5150/UDP as well. This allows enabling public address detection and private routing. The minimum specifications are 1 vCPU, 1GB RAM, and 25 GB storage.
## Quick Start
diff --git a/doc/config/sample.config b/doc/config/sample.config
index 2727eaf0..4073e2c3 100644
--- a/doc/config/sample.config
+++ b/doc/config/sample.config
@@ -55,7 +55,8 @@ core:
routing_table:
node_id: null
node_id_secret: null
- bootstrap: ['bootstrap.veilid.net']
+ bootstrap: ['bootstrap-v1.veilid.net']
+ bootstrap_keys: ['VLD0:Vj0lKDdUQXmQ5Ol1SZdlvXkBHUccBcQvGLN9vbLSI7k','VLD0:QeQJorqbXtC7v3OlynCZ_W3m76wGNeB5NTF81ypqHAo','VLD0:QNdcl-0OiFfYVj9331XVR6IqZ49NG-E18d5P7lwi4TA']
limit_over_attached: 64
limit_fully_attached: 32
limit_attached_strong: 16
diff --git a/doc/config/veilid-bootstrap-config b/doc/config/veilid-bootstrap-config
index ffd201c5..cc16bd9a 100644
--- a/doc/config/veilid-bootstrap-config
+++ b/doc/config/veilid-bootstrap-config
@@ -17,17 +17,18 @@ logging:
enabled: false
core:
capabilities:
- disable: ['TUNL','SGNL','RLAY','DIAL','DHTV','DHTW','APPM','ROUT']
+ disable: ["TUNL", "SGNL", "RLAY", "DIAL", "DHTV", "DHTW", "APPM", "ROUT"]
network:
upnp: false
dht:
min_peer_count: 2
detect_address_changes: false
routing_table:
- bootstrap: ['bootstrap.']
+ bootstrap: ["bootstrap."]
+ bootstrap_keys: ["VLD0:"]
protected_store:
- insecure_fallback_directory: '/var/db/veilid-server/protected_store'
+ insecure_fallback_directory: "/var/db/veilid-server/protected_store"
table_store:
- directory: '/var/db/veilid-server/table_store'
+ directory: "/var/db/veilid-server/table_store"
block_store:
- directory: '/var/db/veilid-server/block_store'
\ No newline at end of file
+ directory: "/var/db/veilid-server/block_store"
diff --git a/doc/config/veilid-dev-bootstrap-config b/doc/config/veilid-dev-bootstrap-config
index 2e819ac9..b22ae2ac 100644
--- a/doc/config/veilid-dev-bootstrap-config
+++ b/doc/config/veilid-dev-bootstrap-config
@@ -22,18 +22,19 @@ logging:
enabled: false
core:
capabilities:
- disable: ['TUNL','SGNL','RLAY','DIAL','DHTV','DHTW','APPM']
+ disable: ["TUNL", "SGNL", "RLAY", "DIAL", "DHTV", "DHTW", "APPM"]
network:
upnp: false
dht:
min_peer_count: 2
detect_address_changes: false
routing_table:
- bootstrap: ['bootstrap.']
- network_key_password: ''
+ bootstrap: ["bootstrap."]
+ bootstrap_keys: ["VLD0:"]
+ network_key_password: ""
protected_store:
- insecure_fallback_directory: '/var/db/veilid-server/protected_store'
+ insecure_fallback_directory: "/var/db/veilid-server/protected_store"
table_store:
- directory: '/var/db/veilid-server/table_store'
+ directory: "/var/db/veilid-server/table_store"
block_store:
- directory: '/var/db/veilid-server/block_store'
\ No newline at end of file
+ directory: "/var/db/veilid-server/block_store"
diff --git a/doc/config/veilid-dev-node-config b/doc/config/veilid-dev-node-config
index f2bf161d..3258fbc7 100644
--- a/doc/config/veilid-dev-node-config
+++ b/doc/config/veilid-dev-node-config
@@ -3,10 +3,10 @@
#
# Private Development Node Configuration
#
-# This config is templated to setup a Velid node with a
+# This config is templated to setup a Velid node with a
# network_key_password. Set the network key to whatever you
-# set within your private bootstrap server's config. Treat it
-# like a password.
+# set within your private bootstrap server's config. Treat it
+# like a password.
# -----------------------------------------------------------
---
@@ -21,18 +21,19 @@ logging:
enabled: false
core:
capabilities:
- disable: ['APPM']
+ disable: ["APPM"]
network:
upnp: false
dht:
min_peer_count: 10
detect_address_changes: false
routing_table:
- bootstrap: ['bootstrap.']
- network_key_password: ''
+ bootstrap: ["bootstrap."]
+ bootstrap_keys: ["VLD0:"]
+ network_key_password: ""
protected_store:
- insecure_fallback_directory: '/var/db/veilid-server/protected_store'
+ insecure_fallback_directory: "/var/db/veilid-server/protected_store"
table_store:
- directory: '/var/db/veilid-server/table_store'
+ directory: "/var/db/veilid-server/table_store"
block_store:
- directory: '/var/db/veilid-server/block_store'
\ No newline at end of file
+ directory: "/var/db/veilid-server/block_store"
diff --git a/doc/config/veilid-server-config.md b/doc/config/veilid-server-config.md
new file mode 100644
index 00000000..98cdb650
--- /dev/null
+++ b/doc/config/veilid-server-config.md
@@ -0,0 +1,321 @@
+---
+title: Veilid Server Configuration
+keywords:
+- config
+- veilid-server
+status: Draft
+---
+# Veilid Server Configuration
+
+## Configuration File
+
+`veilid-server` may be run using configuration from both command-line arguments
+and the `veilid-server.conf` file.
+
+## Global Directives
+
+| Directive | Description |
+| ---------------------------- | ------------------------------------- |
+| [daemon](#daemon) | Run `veilid-server` in the background |
+| [client\_api](#client_api) | |
+| [auto\_attach](#auto_attach) | |
+| [logging](#logging) | |
+| [testing](#testing) | |
+| [core](#core) | |
+
+
+### daemon
+
+```yaml
+daemon:
+ enabled: false
+```
+
+### client_api
+
+```yaml
+client_api:
+ enabled: true
+ listen_address: 'localhost:5959'
+```
+
+| Parameter | Description |
+| -------------------------------------------- | ----------- |
+| [enabled](#client_apienabled) | |
+| [listen\_address](#client_apilisten_address) | |
+
+#### client\_api:enabled
+
+**TODO**
+
+#### client\_api:listen\_address
+
+**TODO**
+
+### auto\_attach
+
+```yaml
+auto_attach: true
+```
+
+### logging
+
+```yaml
+logging:
+ system:
+ enabled: false
+ level: 'info'
+ terminal:
+ enabled: true
+ level: 'info'
+ file:
+ enabled: false
+ path: ''
+ append: true
+ level: 'info'
+ api:
+ enabled: true
+ level: 'info'
+ otlp:
+ enabled: false
+ level: 'trace'
+ grpc_endpoint: 'localhost:4317'
+```
+
+| Parameter | Description |
+| ---------------------------- | ----------- |
+| [system](#loggingsystem) | |
+| [terminal](#loggingterminal) | |
+| [file](#loggingfile) | |
+| [api](#loggingapi) | |
+| [otlp](#loggingotlp) | |
+
+#### logging:system
+
+```yaml
+system:
+ enabled: false
+ level: 'info'
+```
+
+#### logging:terminal
+
+```yaml
+terminal:
+ enabled: true
+ level: 'info'
+```
+
+#### logging:file
+
+```yaml
+file:
+ enabled: false
+ path: ''
+ append: true
+ level: 'info'
+```
+
+#### logging:api
+
+```yaml
+api:
+ enabled: true
+ level: 'info'
+```
+
+#### logging:otlp
+
+```yaml
+otlp:
+ enabled: false
+ level: 'trace'
+ grpc_endpoint: 'localhost:4317'
+```
+
+### testing
+
+```yaml
+testing:
+ subnode_index: 0
+ subnode_count: 1
+```
+
+### core
+
+| Parameter | Description |
+| ---------------------------------------- | ----------- |
+| [protected\_store](#coreprotected_store) | |
+| [table\_store](#coretable_store) | |
+| [block\_store](#block_store) | |
+| [network](#corenetwork) | |
+
+#### core:protected\_store
+
+```yaml
+protected_store:
+ allow_insecure_fallback: true
+ always_use_insecure_storage: true
+ directory: '%DIRECTORY%'
+ delete: false
+```
+
+#### core:table\_store
+
+```yaml
+table_store:
+ directory: '%TABLE_STORE_DIRECTORY%'
+ delete: false
+```
+
+#### core:block\_store
+
+```yaml
+block_store:
+ directory: '%BLOCK_STORE_DIRECTORY%'
+ delete: false
+```
+
+#### core:network
+
+```yaml
+network:
+ connection_initial_timeout_ms: 2000
+ connection_inactivity_timeout_ms: 60000
+ max_connections_per_ip4: 32
+ max_connections_per_ip6_prefix: 32
+ max_connections_per_ip6_prefix_size: 56
+ max_connection_frequency_per_min: 128
+ client_allowlist_timeout_ms: 300000
+ reverse_connection_receipt_time_ms: 5000
+ hole_punch_receipt_time_ms: 5000
+ network_key_password: null
+ disable_capabilites: []
+ node_id: null
+ node_id_secret: null
+ upnp: true
+ detect_address_changes: true
+ enable_local_peer_scope: false
+ restricted_nat_retries: 0
+```
+
+| Parameter | Description |
+| ------------------------------------------- | ----------- |
+| [routing\_table](#corenetworkrouting_table) | |
+| [rpc](#corenetworkrpc) | |
+| [dht](#corenetworkdht) | |
+| [tls](#corenetworktls) | |
+| [application](#corenetworkapplication) | |
+| [protocol](#corenetworkprotocol) | |
+
+#### core:network:routing\_table
+
+```yaml
+routing_table:
+ node_id: null
+ node_id_secret: null
+ bootstrap: ['bootstrap-v1.veilid.net']
+ bootstrap_keys: ['VLD0:Vj0lKDdUQXmQ5Ol1SZdlvXkBHUccBcQvGLN9vbLSI7k','VLD0:QeQJorqbXtC7v3OlynCZ_W3m76wGNeB5NTF81ypqHAo','VLD0:QNdcl-0OiFfYVj9331XVR6IqZ49NG-E18d5P7lwi4TA']
+ limit_over_attached: 64
+ limit_fully_attached: 32
+ limit_attached_strong: 16
+ limit_attached_good: 8
+ limit_attached_weak: 4
+```
+
+#### core:network:rpc
+
+```yaml
+rpc:
+ concurrency: 0
+ queue_size: 1024
+ max_timestamp_behind_ms: 10000
+ max_timestamp_ahead_ms: 10000
+ timeout_ms: 5000
+ max_route_hop_count: 4
+ default_route_hop_count: 1
+```
+
+#### core:network:dht
+
+```yaml
+dht:
+ max_find_node_count: 20
+ resolve_node_timeout_ms: 10000
+ resolve_node_count: 1
+ resolve_node_fanout: 4
+ get_value_timeout_ms: 10000
+ get_value_count: 3
+ get_value_fanout: 4
+ set_value_timeout_ms: 10000
+ set_value_count: 5
+ set_value_fanout: 4
+ min_peer_count: 20
+ min_peer_refresh_time_ms: 60000
+ validate_dial_info_receipt_time_ms: 2000
+ local_subkey_cache_size: 128
+ local_max_subkey_cache_memory_mb: 256
+ remote_subkey_cache_size: 1024
+ remote_max_records: 65536
+ remote_max_subkey_cache_memory_mb: %REMOTE_MAX_SUBKEY_CACHE_MEMORY_MB%
+ remote_max_storage_space_mb: 0
+ public_watch_limit: 32
+ member_watch_limit: 8
+ max_watch_expiration_ms: 600000
+```
+
+#### core:network:tls
+
+```yaml
+tls:
+ certificate_path: '%CERTIFICATE_PATH%'
+ private_key_path: '%PRIVATE_KEY_PATH%'
+ connection_initial_timeout_ms: 2000
+```
+
+#### core:network:application
+
+```yaml
+application:
+ https:
+ enabled: false
+ listen_address: ':5150'
+ path: 'app'
+ # url: 'https://localhost:5150'
+ http:
+ enabled: false
+ listen_address: ':5150'
+ path: 'app'
+ # url: 'http://localhost:5150'
+```
+
+#### core:network:protocol
+
+```yaml
+protocol:
+ udp:
+ enabled: true
+ socket_pool_size: 0
+ listen_address: ':5150'
+ # public_address: ''
+ tcp:
+ connect: true
+ listen: true
+ max_connections: 32
+ listen_address: ':5150'
+ #'public_address: ''
+ ws:
+ connect: true
+ listen: true
+ max_connections: 16
+ listen_address: ':5150'
+ path: 'ws'
+ # url: 'ws://localhost:5150/ws'
+ wss:
+ connect: true
+ listen: false
+ max_connections: 16
+ listen_address: ':5150'
+ path: 'ws'
+ # url: ''
+```
diff --git a/veilid-core/src/attachment_manager.rs b/veilid-core/src/attachment_manager.rs
index b74cceda..4abb5ae3 100644
--- a/veilid-core/src/attachment_manager.rs
+++ b/veilid-core/src/attachment_manager.rs
@@ -23,7 +23,7 @@ impl Default for AttachmentManagerStartupContext {
#[derive(Debug)]
struct AttachmentManagerInner {
last_attachment_state: AttachmentState,
- last_routing_table_health: Option,
+ last_routing_table_health: Option>,
maintain_peers: bool,
started_ts: Timestamp,
attach_ts: Option,
@@ -123,14 +123,14 @@ impl AttachmentManager {
// Check if the routing table health is different
if let Some(last_routing_table_health) = &inner.last_routing_table_health {
// If things are the same, just return
- if last_routing_table_health == &health {
+ if last_routing_table_health.as_ref() == &health {
return;
}
}
// Swap in new health numbers
let opt_previous_health = inner.last_routing_table_health.take();
- inner.last_routing_table_health = Some(health.clone());
+ inner.last_routing_table_health = Some(Arc::new(health.clone()));
// Calculate new attachment state
let config = self.config();
@@ -414,10 +414,6 @@ impl AttachmentManager {
}
}
- // pub fn get_attachment_state(&self) -> AttachmentState {
- // self.inner.lock().last_attachment_state
- // }
-
fn get_veilid_state_inner(inner: &AttachmentManagerInner) -> Box {
let now = Timestamp::now();
let uptime = now - inner.started_ts;
@@ -444,4 +440,14 @@ impl AttachmentManager {
let inner = self.inner.lock();
Self::get_veilid_state_inner(&inner)
}
+
+ #[expect(dead_code)]
+ pub fn get_attachment_state(&self) -> AttachmentState {
+ self.inner.lock().last_attachment_state
+ }
+
+ #[expect(dead_code)]
+ pub fn get_last_routing_table_health(&self) -> Option> {
+ self.inner.lock().last_routing_table_health.clone()
+ }
}
diff --git a/veilid-core/src/intf/native/system.rs b/veilid-core/src/intf/native/system.rs
index 57e68367..68a2380a 100644
--- a/veilid-core/src/intf/native/system.rs
+++ b/veilid-core/src/intf/native/system.rs
@@ -104,13 +104,14 @@ pub async fn txt_lookup>(host: S) -> EyreResult> {
if (*p_record).wType == DNS_TYPE_TEXT.0 {
let count:usize = (*p_record).Data.TXT.dwStringCount.try_into().unwrap();
let string_array: *const PSTR = &(*p_record).Data.TXT.pStringArray[0];
+ let mut record_out = Vec::::new();
for n in 0..count {
let pstr: PSTR = *(string_array.add(n));
let c_str: &CStr = CStr::from_ptr(pstr.0 as *const i8);
- if let Ok(str_slice) = c_str.to_str() {
- let str_buf: String = str_slice.to_owned();
- out.push(str_buf);
- }
+ record_out.extend_from_slice(c_str.to_bytes());
+ }
+ if let Ok(s) = String::from_utf8(record_out) {
+ out.push(s);
}
}
p_record = (*p_record).pNext;
@@ -148,8 +149,12 @@ pub async fn txt_lookup>(host: S) -> EyreResult> {
})).await?;
let mut out = Vec::new();
for x in txt_result.iter() {
- for s in x.txt_data() {
- out.push(String::from_utf8(s.to_vec()).wrap_err("utf8 conversion error")?);
+ let mut record_out = Vec::::new();
+ for txtd in x.txt_data() {
+ record_out.extend_from_slice(txtd);
+ }
+ if let Ok(s) = String::from_utf8(record_out) {
+ out.push(s);
}
}
Ok(out)
diff --git a/veilid-core/src/network_manager/bootstrap/bootstrap_record.rs b/veilid-core/src/network_manager/bootstrap/bootstrap_record.rs
new file mode 100644
index 00000000..c255cfe3
--- /dev/null
+++ b/veilid-core/src/network_manager/bootstrap/bootstrap_record.rs
@@ -0,0 +1,492 @@
+use super::*;
+
+impl_veilid_log_facility!("net");
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct BootstrapRecord {
+ node_ids: TypedKeyGroup,
+ envelope_support: Vec,
+ dial_info_details: Vec,
+ timestamp_secs: Option,
+ extra: Vec,
+}
+
+impl BootstrapRecord {
+ pub fn new(
+ node_ids: TypedKeyGroup,
+ mut envelope_support: Vec,
+ mut dial_info_details: Vec,
+ timestamp_secs: Option,
+ extra: Vec,
+ ) -> Self {
+ envelope_support.sort();
+ dial_info_details.sort();
+
+ Self {
+ node_ids,
+ envelope_support,
+ dial_info_details,
+ timestamp_secs,
+ extra,
+ }
+ }
+
+ pub fn node_ids(&self) -> &TypedKeyGroup {
+ &self.node_ids
+ }
+ pub fn envelope_support(&self) -> &[u8] {
+ &self.envelope_support
+ }
+ pub fn dial_info_details(&self) -> &[DialInfoDetail] {
+ &self.dial_info_details
+ }
+ pub fn timestamp_secs(&self) -> Option {
+ self.timestamp_secs
+ }
+ #[expect(dead_code)]
+ pub fn extra(&self) -> &[String] {
+ &self.extra
+ }
+
+ pub fn merge(&mut self, other: BootstrapRecord) {
+ self.node_ids.add_all(&other.node_ids);
+ for x in other.envelope_support {
+ if !self.envelope_support.contains(&x) {
+ self.envelope_support.push(x);
+ self.envelope_support.sort();
+ }
+ }
+ for did in other.dial_info_details {
+ if !self.dial_info_details.contains(&did) {
+ self.dial_info_details.push(did);
+ }
+ }
+ self.dial_info_details.sort();
+ if let Some(ts) = self.timestamp_secs.as_mut() {
+ if let Some(other_ts) = other.timestamp_secs {
+ // Use earliest timestamp if merging
+ ts.min_assign(other_ts);
+ } else {
+ // Do nothing
+ }
+ } else {
+ self.timestamp_secs = other.timestamp_secs;
+ }
+ self.extra.extend_from_slice(&other.extra);
+ }
+
+ async fn to_vcommon_string(
+ &self,
+ dial_info_converter: &dyn DialInfoConverter,
+ ) -> EyreResult {
+ let valid_envelope_versions = self
+ .envelope_support()
+ .iter()
+ .map(|x| x.to_string())
+ .collect::>()
+ .join(",");
+
+ let node_ids = self
+ .node_ids
+ .iter()
+ .map(|x| x.to_string())
+ .collect::>()
+ .join(",");
+
+ let mut short_urls = Vec::new();
+ let mut some_hostname = Option::::None;
+ for did in self.dial_info_details() {
+ let ShortDialInfo {
+ short_url,
+ hostname,
+ } = dial_info_converter.to_short(did.dial_info.clone()).await;
+ if let Some(h) = &some_hostname {
+ if h != &hostname {
+ bail!(
+ "Inconsistent hostnames for dial info: {} vs {}",
+ some_hostname.unwrap(),
+ hostname
+ );
+ }
+ } else {
+ some_hostname = Some(hostname);
+ }
+
+ short_urls.push(short_url);
+ }
+ if some_hostname.is_none() || short_urls.is_empty() {
+ bail!("No dial info for bootstrap host");
+ }
+ short_urls.sort();
+ short_urls.dedup();
+
+ let vcommon = format!(
+ "|{}|{}|{}|{}",
+ valid_envelope_versions,
+ node_ids,
+ some_hostname.as_ref().unwrap(),
+ short_urls.join(",")
+ );
+
+ Ok(vcommon)
+ }
+
+ pub async fn to_v0_string(
+ &self,
+ dial_info_converter: &dyn DialInfoConverter,
+ ) -> EyreResult {
+ let vcommon = self.to_vcommon_string(dial_info_converter).await?;
+ Ok(format!("{}{}", BOOTSTRAP_TXT_VERSION_0, vcommon))
+ }
+
+ pub async fn to_v1_string(
+ &self,
+ network_manager: &NetworkManager,
+ dial_info_converter: &dyn DialInfoConverter,
+ signing_key_pair: TypedKeyPair,
+ ) -> EyreResult {
+ let vcommon = self.to_vcommon_string(dial_info_converter).await?;
+ let ts = if let Some(ts) = self.timestamp_secs() {
+ ts
+ } else {
+ bail!("timestamp required for bootstrap v1 format");
+ };
+ let mut v1 = format!("{}{}|{}|", BOOTSTRAP_TXT_VERSION_1, vcommon, ts);
+
+ let crypto = network_manager.crypto();
+
+ let sig = match crypto.generate_signatures(v1.as_bytes(), &[signing_key_pair], |kp, sig| {
+ TypedSignature::new(kp.kind, sig).to_string()
+ }) {
+ Ok(v) => {
+ let Some(sig) = v.first().cloned() else {
+ bail!("No signature generated");
+ };
+ sig
+ }
+ Err(e) => {
+ bail!("Failed to generate signature: {}", e);
+ }
+ };
+
+ v1 += &sig;
+ Ok(v1)
+ }
+
+ pub fn new_from_v0_str(
+ network_manager: &NetworkManager,
+ dial_info_converter: &dyn DialInfoConverter,
+ record_str: &str,
+ ) -> EyreResult