Audit fixes: key permissions, lock contention, Docker IP filter, doc updates
Security: identity.key written with 0600 permissions (Unix). Docker bridge IPs (172.17-31.x) filtered from is_shareable_addr to prevent topology disclosure in relay introductions. Lock contention: ManifestPush relay and DeleteRecord CDN notify now gather connections under lock, then send outside lock. UI: syncBtn null guard prevents crash on hidden element. Documentation: design.html version badge updated to v0.4.4. Self Last Encounter threshold corrected from 3h to 4h. Multi-Device Identity section rewritten for multi-identity-per-device (complete) + multi-device (planned) + post merge (planned). MEMORY.md updated to v0.4.4+ status. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
18a40756d8
commit
fb1e92985c
5 changed files with 85 additions and 58 deletions
|
|
@ -4935,41 +4935,31 @@ impl ConnectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Send CDN delete notices (async, best-effort)
|
||||
// Gather connections for CDN delete notices under lock, then send outside
|
||||
let mut delete_notices: Vec<(iroh::endpoint::Connection, crate::protocol::BlobDeleteNoticePayload)> = Vec::new();
|
||||
for (cid, downstream, upstream) in &blob_cleanup {
|
||||
// Notify downstream (with our upstream info for tree healing)
|
||||
let upstream_info = upstream.as_ref().map(|(nid, addrs)| {
|
||||
PeerWithAddress {
|
||||
n: hex::encode(nid),
|
||||
a: addrs.clone(),
|
||||
}
|
||||
});
|
||||
let ds_payload = crate::protocol::BlobDeleteNoticePayload {
|
||||
cid: *cid,
|
||||
upstream_node: upstream_info,
|
||||
};
|
||||
let upstream_info = upstream.as_ref().map(|(nid, addrs)| PeerWithAddress { n: hex::encode(nid), a: addrs.clone() });
|
||||
let ds_payload = crate::protocol::BlobDeleteNoticePayload { cid: *cid, upstream_node: upstream_info };
|
||||
for (ds_nid, _) in downstream {
|
||||
if let Some(pc) = cm.connections_ref().get(ds_nid) {
|
||||
if let Ok(mut send) = pc.connection.open_uni().await {
|
||||
let _ = write_typed_message(&mut send, MessageType::BlobDeleteNotice, &ds_payload).await;
|
||||
let _ = send.finish();
|
||||
}
|
||||
delete_notices.push((pc.connection.clone(), ds_payload.clone()));
|
||||
}
|
||||
}
|
||||
// Notify upstream (no upstream info — just "remove me")
|
||||
if let Some((up_nid, _)) = upstream {
|
||||
let up_payload = crate::protocol::BlobDeleteNoticePayload {
|
||||
cid: *cid,
|
||||
upstream_node: None,
|
||||
};
|
||||
let up_payload = crate::protocol::BlobDeleteNoticePayload { cid: *cid, upstream_node: None };
|
||||
if let Some(pc) = cm.connections_ref().get(up_nid) {
|
||||
if let Ok(mut send) = pc.connection.open_uni().await {
|
||||
let _ = write_typed_message(&mut send, MessageType::BlobDeleteNotice, &up_payload).await;
|
||||
let _ = send.finish();
|
||||
}
|
||||
delete_notices.push((pc.connection.clone(), up_payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(cm);
|
||||
// Send outside lock
|
||||
for (conn, payload) in &delete_notices {
|
||||
if let Ok(mut send) = conn.open_uni().await {
|
||||
let _ = write_typed_message(&mut send, MessageType::BlobDeleteNotice, payload).await;
|
||||
let _ = send.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageType::VisibilityUpdate => {
|
||||
let payload: crate::protocol::VisibilityUpdatePayload =
|
||||
|
|
@ -5167,13 +5157,18 @@ impl ConnectionManager {
|
|||
}
|
||||
|
||||
drop(storage);
|
||||
// Relay to downstream (best-effort via mesh connections)
|
||||
for (ds_nid, relay_payload) in &relay_targets {
|
||||
if let Some(pc) = cm.connections_ref().get(ds_nid) {
|
||||
if let Ok(mut send) = pc.connection.open_uni().await {
|
||||
let _ = write_typed_message(&mut send, MessageType::ManifestPush, relay_payload).await;
|
||||
let _ = send.finish();
|
||||
}
|
||||
// Gather relay connections under lock, then relay outside
|
||||
let relay_conns: Vec<(iroh::endpoint::Connection, crate::protocol::ManifestPushPayload)> = relay_targets.iter()
|
||||
.filter_map(|(ds_nid, payload)| {
|
||||
cm.connections_ref().get(ds_nid).map(|pc| (pc.connection.clone(), payload.clone()))
|
||||
})
|
||||
.collect();
|
||||
drop(cm);
|
||||
// Relay outside lock
|
||||
for (conn, relay_payload) in &relay_conns {
|
||||
if let Ok(mut send) = conn.open_uni().await {
|
||||
let _ = write_typed_message(&mut send, MessageType::ManifestPush, relay_payload).await;
|
||||
let _ = send.finish();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5291,8 +5286,7 @@ impl ConnectionManager {
|
|||
});
|
||||
}
|
||||
|
||||
drop(cm);
|
||||
debug!(peer = hex::encode(remote_node_id), stored, relayed = relay_targets.len(), "Received manifest push");
|
||||
debug!(peer = hex::encode(remote_node_id), stored, relayed = relay_conns.len(), "Received manifest push");
|
||||
}
|
||||
MessageType::SocialDisconnectNotice => {
|
||||
let payload: SocialDisconnectNoticePayload = read_payload(recv, MAX_PAYLOAD).await?;
|
||||
|
|
|
|||
|
|
@ -188,8 +188,10 @@ impl IdentityManager {
|
|||
let id_dir = self.base_dir.join("identities").join(&node_id_hex);
|
||||
std::fs::create_dir_all(&id_dir)?;
|
||||
|
||||
// Write identity key
|
||||
std::fs::write(id_dir.join("identity.key"), seed)?;
|
||||
// Write identity key with restricted permissions
|
||||
let key_path = id_dir.join("identity.key");
|
||||
std::fs::write(&key_path, seed)?;
|
||||
set_key_permissions(&key_path);
|
||||
|
||||
// Write metadata
|
||||
let now = now_ms();
|
||||
|
|
@ -223,7 +225,9 @@ impl IdentityManager {
|
|||
}
|
||||
|
||||
std::fs::create_dir_all(&id_dir)?;
|
||||
std::fs::write(id_dir.join("identity.key"), seed)?;
|
||||
let key_path = id_dir.join("identity.key");
|
||||
std::fs::write(&key_path, seed)?;
|
||||
set_key_permissions(&key_path);
|
||||
|
||||
let now = now_ms();
|
||||
let meta = IdentityMeta {
|
||||
|
|
@ -390,3 +394,16 @@ fn now_ms() -> u64 {
|
|||
.unwrap_or_default()
|
||||
.as_millis() as u64
|
||||
}
|
||||
|
||||
/// Set restrictive permissions on identity key files (user-only read/write).
|
||||
fn set_key_permissions(path: &std::path::Path) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = path; // no-op on non-Unix (Windows uses ACLs, Android sandboxes)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,11 +61,22 @@ fn is_public_ip(ip: IpAddr) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
/// Filter out addresses that are never useful to share (loopback, link-local, unspecified).
|
||||
/// Keeps LAN addresses (192.168.x, 10.x, 172.16-31.x) since peers might be on the same LAN.
|
||||
/// Filter out addresses that are never useful to share (loopback, link-local, unspecified,
|
||||
/// Docker bridge). Keeps common LAN addresses (192.168.x, 10.x) for same-WiFi discovery.
|
||||
/// Excludes 172.17-31.x (Docker/container bridges) to avoid topology disclosure.
|
||||
pub(crate) fn is_shareable_addr(addr: &SocketAddr) -> bool {
|
||||
match addr.ip() {
|
||||
IpAddr::V4(v4) => !v4.is_loopback() && !v4.is_link_local() && !v4.is_unspecified(),
|
||||
IpAddr::V4(v4) => {
|
||||
if v4.is_loopback() || v4.is_link_local() || v4.is_unspecified() {
|
||||
return false;
|
||||
}
|
||||
// Exclude Docker bridge range (172.17.0.0 - 172.31.255.255)
|
||||
let octets = v4.octets();
|
||||
if octets[0] == 172 && octets[1] >= 17 {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
IpAddr::V6(v6) => !v6.is_loopback() && !v6.is_unspecified(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue