Advertise signed peer cards on LAN
This commit is contained in:
parent
b0f208b05a
commit
d5a548182b
10 changed files with 306 additions and 7 deletions
|
|
@ -106,8 +106,9 @@ Roadmap items should be actionable and checkable:
|
||||||
certificate metadata, revocation metadata, user service definitions, and a
|
certificate metadata, revocation metadata, user service definitions, and a
|
||||||
pinned `geth-iroh` endpoint wrapper with protocol-router scaffold, peer-card
|
pinned `geth-iroh` endpoint wrapper with protocol-router scaffold, peer-card
|
||||||
types, manual signed peer-card export/import/list commands, `geth peer ping`
|
types, manual signed peer-card export/import/list commands, `geth peer ping`
|
||||||
and `geth peer auth-check` over Iroh, untrusted discovery-backend trait,
|
and `geth peer auth-check` over Iroh, signed peer-card LAN discovery payloads,
|
||||||
custom relay-map config, and Iroh local-network discovery toggle exist.
|
untrusted discovery-backend trait, custom relay-map config, and Iroh
|
||||||
|
local-network discovery toggle exist.
|
||||||
- Canonical signed-operation envelopes exist for keychain/auth signature
|
- Canonical signed-operation envelopes exist for keychain/auth signature
|
||||||
payloads. The keychain reducer builds an active identity view for admin keys,
|
payloads. The keychain reducer builds an active identity view for admin keys,
|
||||||
users, devices, nodes, agents, and endpoint bindings.
|
users, devices, nodes, agents, and endpoint bindings.
|
||||||
|
|
|
||||||
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -1172,10 +1172,12 @@ dependencies = [
|
||||||
name = "geth-discovery"
|
name = "geth-discovery"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"geth-crypto",
|
"geth-crypto",
|
||||||
"geth-types",
|
"geth-types",
|
||||||
"hex",
|
"hex",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1247,6 +1249,7 @@ dependencies = [
|
||||||
"geth-types",
|
"geth-types",
|
||||||
"iroh",
|
"iroh",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"swarm-discovery",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ rand_core = { version = "0.6", features = ["getrandom"] }
|
||||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
swarm-discovery = "0.4.1"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
time = { version = "0.3", features = ["formatting", "serde"] }
|
time = { version = "0.3", features = ["formatting", "serde"] }
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,9 @@ imported peer card and exchange a signed candidate-only peer-card ping.
|
||||||
Iroh control request: the remote daemon verifies that the caller's signed peer
|
Iroh control request: the remote daemon verifies that the caller's signed peer
|
||||||
card binds the actual Iroh EndpointID before reducing resource-local auth ops.
|
card binds the actual Iroh EndpointID before reducing resource-local auth ops.
|
||||||
Importing or pinging a peer card never grants capabilities by itself.
|
Importing or pinging a peer card never grants capabilities by itself.
|
||||||
|
When `[iroh].local_discovery = true`, the daemon also advertises and discovers
|
||||||
|
signed peer cards on LAN using a geth-specific mDNS TXT payload. That payload is
|
||||||
|
candidate metadata only; all geth node-to-node requests still run over Iroh.
|
||||||
|
|
||||||
Other command groups exist as explicit stubs: `ssh proxy`.
|
Other command groups exist as explicit stubs: `ssh proxy`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
geth-crypto = { path = "../geth-crypto" }
|
geth-crypto = { path = "../geth-crypto" }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
|
use base64::Engine;
|
||||||
use geth_crypto::AgentKey;
|
use geth_crypto::AgentKey;
|
||||||
use geth_types::{AgentId, NodeId, UnixMillis};
|
use geth_types::{AgentId, NodeId, UnixMillis};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub const PEER_CARD_SIGNATURE_NAMESPACE: &str = "geth.peer-card.v1@geth.local";
|
pub const PEER_CARD_SIGNATURE_NAMESPACE: &str = "geth.peer-card.v1@geth.local";
|
||||||
|
pub const PEER_CARD_LAN_DISCOVERY_SERVICE: &str = "geth-peer-card";
|
||||||
|
pub const PEER_CARD_TXT_VERSION_KEY: &str = "geth";
|
||||||
|
pub const PEER_CARD_TXT_VERSION: &str = "peer-card-v1";
|
||||||
|
pub const PEER_CARD_TXT_CHUNKS_KEY: &str = "card-chunks";
|
||||||
|
pub const PEER_CARD_TXT_CHUNK_PREFIX: &str = "card-";
|
||||||
|
const PEER_CARD_TXT_CHUNK_BYTES: usize = 200;
|
||||||
|
const PEER_CARD_TXT_MAX_CHUNKS: usize = 32;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct PeerCard {
|
pub struct PeerCard {
|
||||||
|
|
@ -164,6 +172,86 @@ pub trait DiscoveryBackend {
|
||||||
fn candidates(&self) -> Result<Vec<DiscoveredPeer>, DiscoveryError>;
|
fn candidates(&self) -> Result<Vec<DiscoveredPeer>, DiscoveryError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn peer_card_txt_attributes(
|
||||||
|
card: &PeerCard,
|
||||||
|
) -> Result<Vec<(String, Option<String>)>, DiscoveryError> {
|
||||||
|
card.validate_candidate()?;
|
||||||
|
let json = serde_json::to_vec(card)?;
|
||||||
|
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(json);
|
||||||
|
let chunks = encoded
|
||||||
|
.as_bytes()
|
||||||
|
.chunks(PEER_CARD_TXT_CHUNK_BYTES)
|
||||||
|
.map(|chunk| {
|
||||||
|
std::str::from_utf8(chunk)
|
||||||
|
.expect("base64 is utf-8")
|
||||||
|
.to_owned()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if chunks.is_empty() || chunks.len() > PEER_CARD_TXT_MAX_CHUNKS {
|
||||||
|
return Err(DiscoveryError::PeerCardTxtTooLarge {
|
||||||
|
chunks: chunks.len(),
|
||||||
|
max_chunks: PEER_CARD_TXT_MAX_CHUNKS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut attributes = vec![
|
||||||
|
(
|
||||||
|
PEER_CARD_TXT_VERSION_KEY.to_owned(),
|
||||||
|
Some(PEER_CARD_TXT_VERSION.to_owned()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
PEER_CARD_TXT_CHUNKS_KEY.to_owned(),
|
||||||
|
Some(chunks.len().to_string()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (index, chunk) in chunks.into_iter().enumerate() {
|
||||||
|
attributes.push((
|
||||||
|
format!("{PEER_CARD_TXT_CHUNK_PREFIX}{index:02}"),
|
||||||
|
Some(chunk),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peer_card_from_txt_attributes<'a>(
|
||||||
|
attributes: impl IntoIterator<Item = (&'a str, Option<&'a str>)>,
|
||||||
|
) -> Result<PeerCard, DiscoveryError> {
|
||||||
|
let attributes = attributes.into_iter().collect::<Vec<_>>();
|
||||||
|
let version = txt_value(&attributes, PEER_CARD_TXT_VERSION_KEY)
|
||||||
|
.ok_or_else(|| DiscoveryError::MissingTxtAttribute(PEER_CARD_TXT_VERSION_KEY.to_owned()))?;
|
||||||
|
if version != PEER_CARD_TXT_VERSION {
|
||||||
|
return Err(DiscoveryError::UnsupportedTxtVersion(version.to_owned()));
|
||||||
|
}
|
||||||
|
let chunk_count = txt_value(&attributes, PEER_CARD_TXT_CHUNKS_KEY)
|
||||||
|
.ok_or_else(|| DiscoveryError::MissingTxtAttribute(PEER_CARD_TXT_CHUNKS_KEY.to_owned()))?
|
||||||
|
.parse::<usize>()
|
||||||
|
.map_err(|_| DiscoveryError::InvalidTxtAttribute(PEER_CARD_TXT_CHUNKS_KEY.to_owned()))?;
|
||||||
|
if chunk_count == 0 || chunk_count > PEER_CARD_TXT_MAX_CHUNKS {
|
||||||
|
return Err(DiscoveryError::PeerCardTxtTooLarge {
|
||||||
|
chunks: chunk_count,
|
||||||
|
max_chunks: PEER_CARD_TXT_MAX_CHUNKS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut encoded = String::new();
|
||||||
|
for index in 0..chunk_count {
|
||||||
|
let key = format!("{PEER_CARD_TXT_CHUNK_PREFIX}{index:02}");
|
||||||
|
let chunk = txt_value(&attributes, &key)
|
||||||
|
.ok_or_else(|| DiscoveryError::MissingTxtAttribute(key.clone()))?;
|
||||||
|
encoded.push_str(chunk);
|
||||||
|
}
|
||||||
|
let json = base64::engine::general_purpose::STANDARD_NO_PAD.decode(encoded)?;
|
||||||
|
let card: PeerCard = serde_json::from_slice(&json)?;
|
||||||
|
card.validate_candidate()?;
|
||||||
|
Ok(card)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn txt_value<'a>(attributes: &[(&'a str, Option<&'a str>)], key: &str) -> Option<&'a str> {
|
||||||
|
attributes
|
||||||
|
.iter()
|
||||||
|
.find_map(|(candidate, value)| (*candidate == key).then_some(*value).flatten())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum DiscoveryError {
|
pub enum DiscoveryError {
|
||||||
#[error("peer card has no endpoint candidates")]
|
#[error("peer card has no endpoint candidates")]
|
||||||
|
|
@ -180,6 +268,18 @@ pub enum DiscoveryError {
|
||||||
Hex(#[from] hex::FromHexError),
|
Hex(#[from] hex::FromHexError),
|
||||||
#[error("peer card signature error: {0}")]
|
#[error("peer card signature error: {0}")]
|
||||||
Crypto(#[from] geth_crypto::CryptoError),
|
Crypto(#[from] geth_crypto::CryptoError),
|
||||||
|
#[error("peer card TXT payload is too large: {chunks} chunks, max {max_chunks}")]
|
||||||
|
PeerCardTxtTooLarge { chunks: usize, max_chunks: usize },
|
||||||
|
#[error("missing peer card TXT attribute: {0}")]
|
||||||
|
MissingTxtAttribute(String),
|
||||||
|
#[error("invalid peer card TXT attribute: {0}")]
|
||||||
|
InvalidTxtAttribute(String),
|
||||||
|
#[error("unsupported peer card TXT version: {0}")]
|
||||||
|
UnsupportedTxtVersion(String),
|
||||||
|
#[error("peer card TXT base64 error: {0}")]
|
||||||
|
Base64(#[from] base64::DecodeError),
|
||||||
|
#[error("peer card JSON error: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -241,4 +341,47 @@ mod tests {
|
||||||
Err(DiscoveryError::Crypto(geth_crypto::CryptoError::Verify))
|
Err(DiscoveryError::Crypto(geth_crypto::CryptoError::Verify))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn peer_card_txt_attributes_roundtrip_signed_payload() {
|
||||||
|
let card = signed_card();
|
||||||
|
let attributes = peer_card_txt_attributes(&card).expect("txt attributes");
|
||||||
|
assert_eq!(
|
||||||
|
attributes
|
||||||
|
.iter()
|
||||||
|
.find(|(key, _)| key == PEER_CARD_TXT_VERSION_KEY)
|
||||||
|
.and_then(|(_, value)| value.as_deref()),
|
||||||
|
Some(PEER_CARD_TXT_VERSION)
|
||||||
|
);
|
||||||
|
assert!(attributes.iter().any(|(key, _)| key == "card-00"));
|
||||||
|
assert!(
|
||||||
|
attributes
|
||||||
|
.iter()
|
||||||
|
.all(|(key, value)| key.len() + value.as_deref().unwrap_or("").len() <= 254)
|
||||||
|
);
|
||||||
|
|
||||||
|
let decoded = peer_card_from_txt_attributes(
|
||||||
|
attributes
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| (key.as_str(), value.as_deref())),
|
||||||
|
)
|
||||||
|
.expect("decode txt attributes");
|
||||||
|
|
||||||
|
assert_eq!(decoded, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn peer_card_txt_attributes_reject_missing_chunk() {
|
||||||
|
let card = signed_card();
|
||||||
|
let attributes = peer_card_txt_attributes(&card).expect("txt attributes");
|
||||||
|
let without_first_chunk = attributes
|
||||||
|
.iter()
|
||||||
|
.filter(|(key, _)| key != "card-00")
|
||||||
|
.map(|(key, value)| (key.as_str(), value.as_deref()));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
peer_card_from_txt_attributes(without_first_chunk),
|
||||||
|
Err(DiscoveryError::MissingTxtAttribute(key)) if key == "card-00"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||||
geth-store = { path = "../geth-store" }
|
geth-store = { path = "../geth-store" }
|
||||||
geth-types = { path = "../geth-types" }
|
geth-types = { path = "../geth-types" }
|
||||||
iroh.workspace = true
|
iroh.workspace = true
|
||||||
|
swarm-discovery.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ use geth_control::{
|
||||||
use geth_crypto::AgentKey;
|
use geth_crypto::AgentKey;
|
||||||
use geth_db::DbResource;
|
use geth_db::DbResource;
|
||||||
use geth_discovery::{
|
use geth_discovery::{
|
||||||
DiscoveredPeer, DiscoverySource, EndpointCandidate, PeerCard, discovery_is_untrusted_note,
|
DiscoveredPeer, DiscoverySource, EndpointCandidate, PEER_CARD_LAN_DISCOVERY_SERVICE, PeerCard,
|
||||||
|
discovery_is_untrusted_note, peer_card_from_txt_attributes, peer_card_txt_attributes,
|
||||||
};
|
};
|
||||||
use geth_document::{DocumentResource, DocumentState};
|
use geth_document::{DocumentResource, DocumentState};
|
||||||
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
|
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
|
||||||
|
|
@ -193,6 +194,7 @@ pub async fn run_daemon(paths: GethPaths) -> Result<(), NodeError> {
|
||||||
{
|
{
|
||||||
spawn_iroh_control_accept_loop(node.clone(), endpoint);
|
spawn_iroh_control_accept_loop(node.clone(), endpoint);
|
||||||
}
|
}
|
||||||
|
let _peer_card_lan_discovery = start_peer_card_lan_discovery(&node).await;
|
||||||
if Path::new(&paths.socket_path()).exists() {
|
if Path::new(&paths.socket_path()).exists() {
|
||||||
std::fs::remove_file(paths.socket_path())?;
|
std::fs::remove_file(paths.socket_path())?;
|
||||||
}
|
}
|
||||||
|
|
@ -259,6 +261,121 @@ async fn handle_stream(node: LocalNode, stream: UnixStream) -> Result<(), NodeEr
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn start_peer_card_lan_discovery(node: &LocalNode) -> Option<swarm_discovery::DropGuard> {
|
||||||
|
if !node.iroh_status.local_discovery || !node.iroh_status.enabled {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let card = match local_peer_card(node, DiscoverySource::Mdns, true).await {
|
||||||
|
Ok(card) => card,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(%error, "signed peer-card LAN discovery disabled");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let attributes = match peer_card_txt_attributes(&card) {
|
||||||
|
Ok(attributes) => attributes,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(%error, "could not encode peer-card LAN TXT payload");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some((port, addrs)) = lan_discovery_addrs(&card) else {
|
||||||
|
tracing::warn!(
|
||||||
|
"signed peer-card LAN discovery disabled because no direct Iroh address is known"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let paths = node.paths.clone();
|
||||||
|
let self_node_id = node.node_id.clone();
|
||||||
|
let discoverer = match swarm_discovery::Discoverer::new_interactive(
|
||||||
|
PEER_CARD_LAN_DISCOVERY_SERVICE.to_owned(),
|
||||||
|
lan_discovery_peer_id(&node.agent_id),
|
||||||
|
)
|
||||||
|
.with_addrs(port, addrs)
|
||||||
|
.with_txt_attributes(attributes)
|
||||||
|
{
|
||||||
|
Ok(discoverer) => discoverer,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(%error, "could not build peer-card LAN discovery");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.with_callback(move |_peer_id, peer| {
|
||||||
|
if peer.is_expiry() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let card = match peer_card_from_txt_attributes(peer.txt_attributes()) {
|
||||||
|
Ok(card) => card,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::debug!(%error, "ignoring invalid peer-card LAN discovery payload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if card.node_id.as_str() == self_node_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let discovered = match DiscoveredPeer::candidate(
|
||||||
|
card.clone(),
|
||||||
|
UnixMillis(geth_store::now_ms()),
|
||||||
|
DiscoverySource::Mdns,
|
||||||
|
) {
|
||||||
|
Ok(discovered) => discovered,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::debug!(%error, "ignoring invalid LAN peer-card candidate");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match Store::open(&paths.metadata_db()).and_then(|store| {
|
||||||
|
store.upsert_peer_card(&StoredPeerCard {
|
||||||
|
peer_id: card.node_id.to_string(),
|
||||||
|
card_json: serde_json::to_string(&card).map_err(geth_store::StoreError::from)?,
|
||||||
|
updated_at_ms: discovered.discovered_at.0,
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
Ok(()) => tracing::debug!(node = %card.node_id, "stored LAN peer-card candidate"),
|
||||||
|
Err(error) => tracing::warn!(%error, "could not store LAN peer-card candidate"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match discoverer.spawn(&tokio::runtime::Handle::current()) {
|
||||||
|
Ok(guard) => {
|
||||||
|
tracing::info!(
|
||||||
|
service = PEER_CARD_LAN_DISCOVERY_SERVICE,
|
||||||
|
"signed peer-card LAN discovery running"
|
||||||
|
);
|
||||||
|
Some(guard)
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(%error, "could not start signed peer-card LAN discovery");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lan_discovery_peer_id(agent_id: &str) -> String {
|
||||||
|
format!("geth-{}", geth_crypto::blake3_hex(agent_id.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lan_discovery_addrs(card: &PeerCard) -> Option<(u16, Vec<std::net::IpAddr>)> {
|
||||||
|
let mut parsed = card
|
||||||
|
.endpoints
|
||||||
|
.iter()
|
||||||
|
.flat_map(|endpoint| endpoint.direct_addresses.iter())
|
||||||
|
.filter_map(|addr| addr.parse::<std::net::SocketAddr>().ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
parsed.sort_unstable();
|
||||||
|
parsed.dedup();
|
||||||
|
let port = parsed.first()?.port();
|
||||||
|
let addrs = parsed
|
||||||
|
.into_iter()
|
||||||
|
.filter(|addr| addr.port() == port)
|
||||||
|
.map(|addr| addr.ip())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
(!addrs.is_empty()).then_some((port, addrs))
|
||||||
|
}
|
||||||
|
|
||||||
async fn export_peer_card(
|
async fn export_peer_card(
|
||||||
node: &LocalNode,
|
node: &LocalNode,
|
||||||
out: Option<std::path::PathBuf>,
|
out: Option<std::path::PathBuf>,
|
||||||
|
|
@ -2143,6 +2260,32 @@ mod tests {
|
||||||
.expect("write config");
|
.expect("write config");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lan_discovery_address_selection_uses_iroh_direct_addresses() {
|
||||||
|
let key = AgentKey::generate();
|
||||||
|
let card = PeerCard::signed(
|
||||||
|
"node:test".into(),
|
||||||
|
&key,
|
||||||
|
vec![EndpointCandidate {
|
||||||
|
endpoint_id: "endpoint:test".to_owned(),
|
||||||
|
relay_url: None,
|
||||||
|
direct_addresses: vec![
|
||||||
|
"127.0.0.1:1111".to_owned(),
|
||||||
|
"127.0.0.2:1111".to_owned(),
|
||||||
|
"127.0.0.3:2222".to_owned(),
|
||||||
|
],
|
||||||
|
source: DiscoverySource::Mdns,
|
||||||
|
}],
|
||||||
|
UnixMillis(1),
|
||||||
|
)
|
||||||
|
.expect("peer card");
|
||||||
|
|
||||||
|
let (port, addrs) = lan_discovery_addrs(&card).expect("lan addresses");
|
||||||
|
|
||||||
|
assert_eq!(port, 1111);
|
||||||
|
assert_eq!(addrs.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn peer_ping_uses_signed_peer_card_over_iroh() {
|
async fn peer_ping_uses_signed_peer_card_over_iroh() {
|
||||||
let left_home = tempfile::tempdir().expect("left home");
|
let left_home = tempfile::tempdir().expect("left home");
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,10 @@ them. `geth peer ping <node-id>` dials an imported peer card over Iroh and
|
||||||
exchanges signed peer-card metadata. `geth peer auth-check <node-id>
|
exchanges signed peer-card metadata. `geth peer auth-check <node-id>
|
||||||
<resource> <capability>` sends a protected Iroh control request that validates
|
<resource> <capability>` sends a protected Iroh control request that validates
|
||||||
the caller's signed peer card against the actual Iroh EndpointID before
|
the caller's signed peer card against the actual Iroh EndpointID before
|
||||||
evaluating resource-local capabilities. Automatic signed peer-card
|
evaluating resource-local capabilities. When local discovery is enabled, the
|
||||||
advertisement over LAN discovery remains separate future work.
|
daemon also advertises and discovers signed peer cards through a geth-specific
|
||||||
|
mDNS service. The LAN payload is TXT-encoded signed metadata only; remote geth
|
||||||
|
traffic still uses Iroh.
|
||||||
|
|
||||||
Peer cards are the discovery payload. A peer card carries node ID, agent ID,
|
Peer cards are the discovery payload. A peer card carries node ID, agent ID,
|
||||||
endpoint candidates, timestamp, signing public key, and an Ed25519 signature
|
endpoint candidates, timestamp, signing public key, and an Ed25519 signature
|
||||||
|
|
|
||||||
|
|
@ -94,13 +94,13 @@ geth-to-geth connections without granting trust from discovery alone.
|
||||||
- The daemon registers Iroh's local mDNS-like discovery service when enabled.
|
- The daemon registers Iroh's local mDNS-like discovery service when enabled.
|
||||||
- `geth status --json` reports whether local-network discovery is enabled.
|
- `geth status --json` reports whether local-network discovery is enabled.
|
||||||
|
|
||||||
- `[ ]` Signed peer-card LAN discovery payloads.
|
- `[x]` Signed peer-card LAN discovery payloads.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `[x]` Manual `geth peer export/import/list` can exchange signed peer cards
|
- `[x]` Manual `geth peer export/import/list` can exchange signed peer cards
|
||||||
and store them as untrusted candidates.
|
and store them as untrusted candidates.
|
||||||
- `[x]` Exported daemon peer cards include Iroh EndpointID plus available
|
- `[x]` Exported daemon peer cards include Iroh EndpointID plus available
|
||||||
relay/direct address candidates.
|
relay/direct address candidates.
|
||||||
- `[ ]` The daemon can advertise and discover signed geth peer cards over LAN
|
- `[x]` The daemon can advertise and discover signed geth peer cards over LAN
|
||||||
discovery.
|
discovery.
|
||||||
- `[x]` Imported peer cards are stored only as untrusted peer candidates.
|
- `[x]` Imported peer cards are stored only as untrusted peer candidates.
|
||||||
- `[x]` Discovered EndpointIDs do not grant module access without keychain/auth
|
- `[x]` Discovered EndpointIDs do not grant module access without keychain/auth
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue