mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-06 11:38:24 +00:00
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:
+233
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user