Add peer configuration and connection initiation

Config changes:
- Add ConnectPolicy enum (AutoConnect, OnDemand, Manual)
- Add PeerAddress struct (transport, addr, priority)
- Add PeerConfig struct (npub, alias, addresses, connect_policy)
- Add top-level 'peers' section to Config (after transports)
- Add accessor methods: peers(), auto_connect_peers()

Node changes:
- Add find_transport_for_type() to match transports by name
- Add initiate_peer_connections() called after transports start
- Add initiate_peer_connection() creates Link and Peer in Connecting state
- Node::start() now initiates connections to auto-connect peers

This completes the node startup sequence through step 5 (connect
to static peers). Peers are created in Connecting state; the auth
handshake implementation will transition them to Active.
This commit is contained in:
Johnathan Corgan
2026-01-31 22:53:26 +00:00
parent 738775258a
commit d2851d8406
2 changed files with 377 additions and 4 deletions
+233
View File
@@ -249,6 +249,136 @@ impl TransportsConfig {
}
}
// ============================================================================
// Peer Configuration
// ============================================================================
/// Connection policy for a peer.
///
/// Determines when and how to establish a connection to a peer.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConnectPolicy {
/// Connect to this peer automatically on node startup.
/// This is the only policy supported in the initial implementation.
#[default]
AutoConnect,
/// Connect only when traffic needs to be routed through this peer (future).
OnDemand,
/// Wait for explicit API call to connect (future).
Manual,
}
/// A transport-specific address for reaching a peer.
///
/// Each peer can have multiple addresses across different transports,
/// allowing fallback if one transport is unavailable.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PeerAddress {
/// Transport type (e.g., "udp", "tor", "ethernet").
pub transport: String,
/// Transport-specific address string.
///
/// Format depends on transport type:
/// - UDP: "host:port" (e.g., "192.168.1.1:4000")
/// - Tor: "onion_address:port" (e.g., "xyz...abc.onion:4000")
/// - Ethernet: "interface/mac" (future)
pub addr: String,
/// Priority for address selection (lower = preferred).
/// When multiple addresses are available, lower priority addresses
/// are tried first.
#[serde(default = "default_priority")]
pub priority: u8,
}
fn default_priority() -> u8 {
100
}
impl PeerAddress {
/// Create a new peer address.
pub fn new(transport: impl Into<String>, addr: impl Into<String>) -> Self {
Self {
transport: transport.into(),
addr: addr.into(),
priority: default_priority(),
}
}
/// Create a new peer address with priority.
pub fn with_priority(transport: impl Into<String>, addr: impl Into<String>, priority: u8) -> Self {
Self {
transport: transport.into(),
addr: addr.into(),
priority,
}
}
}
/// Configuration for a known peer.
///
/// Peers are identified by their Nostr public key (npub) and can have
/// multiple transport addresses for reaching them.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PeerConfig {
/// The peer's Nostr public key in npub (bech32) or hex format.
pub npub: String,
/// Human-readable alias for the peer (optional).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
/// Transport addresses for reaching this peer.
/// At least one address is required.
pub addresses: Vec<PeerAddress>,
/// Connection policy for this peer.
#[serde(default)]
pub connect_policy: ConnectPolicy,
}
impl PeerConfig {
/// Create a new peer config with a single address.
pub fn new(npub: impl Into<String>, transport: impl Into<String>, addr: impl Into<String>) -> Self {
Self {
npub: npub.into(),
alias: None,
addresses: vec![PeerAddress::new(transport, addr)],
connect_policy: ConnectPolicy::default(),
}
}
/// Set an alias for the peer.
pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
self.alias = Some(alias.into());
self
}
/// Add an additional address for the peer.
pub fn with_address(mut self, addr: PeerAddress) -> Self {
self.addresses.push(addr);
self
}
/// Get addresses sorted by priority (lowest first).
pub fn addresses_by_priority(&self) -> Vec<&PeerAddress> {
let mut addrs: Vec<_> = self.addresses.iter().collect();
addrs.sort_by_key(|a| a.priority);
addrs
}
/// Check if this peer should auto-connect on startup.
pub fn is_auto_connect(&self) -> bool {
matches!(self.connect_policy, ConnectPolicy::AutoConnect)
}
}
/// Root configuration structure.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
@@ -263,6 +393,10 @@ pub struct Config {
/// Transport instances (`transports.*`).
#[serde(default, skip_serializing_if = "TransportsConfig::is_empty")]
pub transports: TransportsConfig,
/// Static peers to connect to (`peers`).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub peers: Vec<PeerConfig>,
}
impl Config {
@@ -363,6 +497,10 @@ impl Config {
}
// Merge transports section
self.transports.merge(other.transports);
// Merge peers (replace if non-empty)
if !other.peers.is_empty() {
self.peers = other.peers;
}
}
/// Create an Identity from this configuration.
@@ -386,6 +524,16 @@ impl Config {
self.node.leaf_only
}
/// Get the configured peers.
pub fn peers(&self) -> &[PeerConfig] {
&self.peers
}
/// Get peers that should auto-connect on startup.
pub fn auto_connect_peers(&self) -> impl Iterator<Item = &PeerConfig> {
self.peers.iter().filter(|p| p.is_auto_connect())
}
/// Serialize this configuration to YAML.
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(self)
@@ -679,4 +827,89 @@ transports: {}
// All named instances should have Some(name)
assert!(items.iter().all(|(name, _)| name.is_some()));
}
#[test]
fn test_parse_peer_config() {
let yaml = r#"
peers:
- npub: "npub1abc123"
alias: "gateway"
addresses:
- transport: udp
addr: "192.168.1.1:4000"
priority: 1
- transport: tor
addr: "xyz.onion:4000"
priority: 2
connect_policy: auto_connect
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.peers.len(), 1);
let peer = &config.peers[0];
assert_eq!(peer.npub, "npub1abc123");
assert_eq!(peer.alias, Some("gateway".to_string()));
assert_eq!(peer.addresses.len(), 2);
assert!(peer.is_auto_connect());
// Check addresses are sorted by priority
let sorted = peer.addresses_by_priority();
assert_eq!(sorted[0].transport, "udp");
assert_eq!(sorted[0].priority, 1);
assert_eq!(sorted[1].transport, "tor");
assert_eq!(sorted[1].priority, 2);
}
#[test]
fn test_parse_peer_minimal() {
let yaml = r#"
peers:
- npub: "npub1xyz"
addresses:
- transport: udp
addr: "10.0.0.1:4000"
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.peers.len(), 1);
let peer = &config.peers[0];
assert_eq!(peer.npub, "npub1xyz");
assert!(peer.alias.is_none());
// Default connect_policy is auto_connect
assert!(peer.is_auto_connect());
// Default priority is 100
assert_eq!(peer.addresses[0].priority, 100);
}
#[test]
fn test_parse_multiple_peers() {
let yaml = r#"
peers:
- npub: "npub1peer1"
addresses:
- transport: udp
addr: "10.0.0.1:4000"
- npub: "npub1peer2"
addresses:
- transport: udp
addr: "10.0.0.2:4000"
connect_policy: on_demand
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.peers.len(), 2);
assert_eq!(config.auto_connect_peers().count(), 1);
}
#[test]
fn test_peer_config_builder() {
let peer = PeerConfig::new("npub1test", "udp", "192.168.1.1:4000")
.with_alias("test-peer")
.with_address(PeerAddress::with_priority("tor", "xyz.onion:4000", 50));
assert_eq!(peer.npub, "npub1test");
assert_eq!(peer.alias, Some("test-peer".to_string()));
assert_eq!(peer.addresses.len(), 2);
assert!(peer.is_auto_connect());
}
}
+144 -4
View File
@@ -6,19 +6,22 @@
use crate::bloom::BloomState;
use crate::cache::CoordCache;
use crate::config::PeerConfig;
use crate::peer::Peer;
use crate::transport::{
packet_channel, Link, LinkId, PacketRx, PacketTx, TransportHandle, TransportId,
packet_channel, Link, LinkDirection, LinkId, PacketRx, PacketTx, TransportAddr,
TransportHandle, TransportId,
};
use crate::transport::udp::UdpTransport;
use crate::tree::TreeState;
use crate::tun::{run_tun_reader, shutdown_tun_interface, TunDevice, TunError, TunState, TunTx};
use crate::{Config, ConfigError, Identity, IdentityError, NodeId};
use crate::{Config, ConfigError, Identity, IdentityError, NodeId, PeerIdentity};
use std::collections::HashMap;
use std::fmt;
use std::thread::{self, JoinHandle};
use std::time::Duration;
use thiserror::Error;
use tracing::{info, warn};
use tracing::{debug, info, warn};
/// Errors related to node operations.
#[derive(Debug, Error)]
@@ -35,6 +38,9 @@ pub enum NodeError {
#[error("transport not found: {0}")]
TransportNotFound(TransportId),
#[error("no transport available for type: {0}")]
NoTransportForType(String),
#[error("link not found: {0}")]
LinkNotFound(LinkId),
@@ -44,6 +50,9 @@ pub enum NodeError {
#[error("peer already exists: {0:?}")]
PeerAlreadyExists(NodeId),
#[error("invalid peer npub '{npub}': {reason}")]
InvalidPeerNpub { npub: String, reason: String },
#[error("max peers exceeded: {max}")]
MaxPeersExceeded { max: usize },
@@ -315,6 +324,129 @@ impl Node {
transports
}
/// Find an operational transport that matches the given transport type name.
fn find_transport_for_type(&self, transport_type: &str) -> Option<TransportId> {
self.transports
.iter()
.find(|(_, handle)| {
handle.transport_type().name == transport_type && handle.is_operational()
})
.map(|(id, _)| *id)
}
/// Initiate connections to configured static peers.
///
/// For each peer configured with AutoConnect policy, creates a link and
/// peer entry. The peer starts in Connecting state; authentication
/// handshake will be handled by the event loop.
fn initiate_peer_connections(&mut self) {
// Collect peer configs to avoid borrow conflicts
let peer_configs: Vec<_> = self.config.auto_connect_peers().cloned().collect();
if peer_configs.is_empty() {
debug!("No static peers configured");
return;
}
info!(count = peer_configs.len(), "Initiating static peer connections");
for peer_config in peer_configs {
if let Err(e) = self.initiate_peer_connection(&peer_config) {
warn!(
npub = %peer_config.npub,
alias = ?peer_config.alias,
error = %e,
"Failed to initiate peer connection"
);
}
}
}
/// Initiate a connection to a single peer.
fn initiate_peer_connection(&mut self, peer_config: &PeerConfig) -> Result<(), NodeError> {
// Parse the peer's npub to get their identity
let peer_identity = PeerIdentity::from_npub(&peer_config.npub).map_err(|e| {
NodeError::InvalidPeerNpub {
npub: peer_config.npub.clone(),
reason: e.to_string(),
}
})?;
let peer_node_id = *peer_identity.node_id();
// Check if peer already exists
if self.peers.contains_key(&peer_node_id) {
debug!(
npub = %peer_config.npub,
"Peer already exists, skipping"
);
return Ok(());
}
// Try addresses in priority order until one works
for addr in peer_config.addresses_by_priority() {
// Find a transport matching this address type
let transport_id = match self.find_transport_for_type(&addr.transport) {
Some(id) => id,
None => {
debug!(
transport = %addr.transport,
addr = %addr.addr,
"No operational transport for address type"
);
continue;
}
};
// Allocate link ID and create link
let link_id = self.allocate_link_id();
let remote_addr = TransportAddr::from_string(&addr.addr);
// For UDP, links are immediately "connected" (connectionless)
// TODO: For connection-oriented transports, state would be Connecting
let link = Link::connectionless(
link_id,
transport_id,
remote_addr,
LinkDirection::Outbound,
Duration::from_millis(100), // Base RTT estimate for UDP
);
self.links.insert(link_id, link);
// Create peer in Connecting state
let mut peer = Peer::discovered(peer_identity.clone(), link_id);
peer.set_connecting();
let alias_display = peer_config
.alias
.as_deref()
.map(|a| format!(" ({})", a))
.unwrap_or_default();
info!(
npub = %peer_config.npub,
node_id = %peer_node_id,
transport = %addr.transport,
addr = %addr.addr,
link_id = %link_id,
"Peer connection initiated{}",
alias_display
);
self.peers.insert(peer_node_id, peer);
// Successfully initiated connection via this address
return Ok(());
}
// No address worked
Err(NodeError::NoTransportForType(format!(
"no operational transport for any of {}'s addresses",
peer_config.npub
)))
}
// === Identity Accessors ===
/// Get this node's identity.
@@ -655,8 +787,16 @@ impl Node {
}
}
// Connect to static peers (step 5 per architecture doc)
self.initiate_peer_connections();
self.state = NodeState::Running;
info!(state = %self.state, "Node started");
info!(
state = %self.state,
transports = self.transports.len(),
peers = self.peers.len(),
"Node started"
);
Ok(())
}