Close Phase 4 overlay and Unix forwarding gaps

This commit is contained in:
Eric Wendland 2026-05-30 12:24:40 +02:00
commit b0768999db
9 changed files with 210 additions and 13 deletions

View file

@ -261,6 +261,7 @@ Current limits:
- discovery can suggest peers, but never grants overlay access
- overlay access must be resource-authorized with overlay.join/overlay.route
- all overlay packets must be carried over Iroh, not SSH or another transport
- release/platform notes live in docs/overlay-platforms.md
Bearer invite flow:
geth resource create overlay home

View file

@ -161,6 +161,8 @@ pub enum NodeError {
IrohPeer(String),
#[error("overlay runtime error: {0}")]
OverlayRuntime(String),
#[error("unsupported platform: {0}")]
UnsupportedPlatform(String),
}
#[derive(Clone, Debug)]
@ -635,6 +637,11 @@ pub async fn run_unix_forward(
target_path: PathBuf,
bearer_secret: Option<String>,
) -> Result<(), NodeError> {
if !geth_pipe::unix_forward_supported() {
return Err(NodeError::UnsupportedPlatform(
geth_pipe::unix_forward_unsupported_message().to_owned(),
));
}
let listen_path = geth_pipe::validate_unix_forward_path(&listen_path)?;
geth_pipe::validate_unix_forward_path(&target_path)?;
let listener = UnixListener::bind(&listen_path)?;
@ -9034,10 +9041,14 @@ pub fn handle_request(
})
}
ControlRequest::PipeSend { node: Some(_), .. } => Err(NodeError::IrohEndpointUnavailable),
ControlRequest::PipeTcpForward { .. }
| ControlRequest::PipeTcpStream { .. }
| ControlRequest::PipeUnixForward { .. }
| ControlRequest::PipeUnixStream { .. } => Err(NodeError::IrohEndpointUnavailable),
ControlRequest::PipeTcpForward { .. } | ControlRequest::PipeTcpStream { .. } => {
Err(NodeError::IrohEndpointUnavailable)
}
ControlRequest::PipeUnixForward { .. } | ControlRequest::PipeUnixStream { .. } => {
Err(NodeError::UnsupportedPlatform(
geth_pipe::unix_forward_unsupported_message().to_owned(),
))
}
ControlRequest::PipeRecv { name, peek } => {
geth_pipe::validate_pipe_name(&name)?;
let messages = pipe_messages(node, &name, !peek)?;
@ -10397,11 +10408,19 @@ fn overlay_peers(store: &Store, network: &str) -> Result<Vec<OverlayPeer>, NodeE
let cidr = overlay_membership(store, network)?
.map(|membership| membership.cidr)
.unwrap_or_else(|| geth_overlay::DEFAULT_OVERLAY_CIDR.to_owned());
let resource = geth_overlay::overlay_resource_id(network).to_string();
store
.list_peer_cards()?
.into_iter()
.map(|stored| {
let card: PeerCard = serde_json::from_str(&stored.card_json)?;
let peer = PrincipalId::new(card.node_id.to_string());
let route_authorized = can_sync_resource(
store,
&peer,
resource.as_str(),
geth_overlay::CAPABILITY_ROUTE,
)?;
Ok(OverlayPeer {
node_id: card.node_id.to_string(),
endpoint_id: card
@ -10412,7 +10431,11 @@ fn overlay_peers(store: &Store, network: &str) -> Result<Vec<OverlayPeer>, NodeE
&cidr,
card.node_id.as_str(),
)?),
state: "candidate-only".to_owned(),
state: if route_authorized {
"route-authorized-candidate".to_owned()
} else {
"candidate-only".to_owned()
},
})
})
.collect()
@ -13161,6 +13184,69 @@ mod tests {
}));
}
#[test]
fn overlay_peers_combine_trusted_route_metadata_with_untrusted_candidates() {
let store = Store::open_memory().expect("open store");
let key = AgentKey::generate();
let card = PeerCard::signed(
"node:right".into(),
&key,
vec![EndpointCandidate {
endpoint_id: "endpoint:right".to_owned(),
relay_url: None,
direct_addresses: Vec::new(),
source: DiscoverySource::Imported,
}],
UnixMillis(1),
)
.expect("peer card");
store
.upsert_peer_card(&StoredPeerCard {
peer_id: card.node_id.to_string(),
card_json: serde_json::to_string(&card).expect("card json"),
updated_at_ms: 1,
})
.expect("store peer card");
store
.put_module_state(&StoredModuleState {
module: geth_overlay::overlay_state_key("home"),
state_json: serde_json::to_string(
&geth_overlay::joined_overlay_membership(
"home",
Some("172.22.0.0/24"),
"node:left",
"secret",
1,
)
.expect("membership"),
)
.expect("membership json"),
updated_at_ms: 1,
})
.expect("store membership");
let peers = overlay_peers(&store, "home").expect("candidate peers");
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].node_id, "node:right");
assert_eq!(peers[0].state, "candidate-only");
assert_eq!(
peers[0].virtual_ip,
Some(
geth_overlay::deterministic_virtual_ip("172.22.0.0/24", "node:right")
.expect("virtual ip")
)
);
grant_test_capability(
&store,
"node:right",
"resource:overlay:home",
geth_overlay::CAPABILITY_ROUTE,
);
let peers = overlay_peers(&store, "home").expect("authorized peers");
assert_eq!(peers[0].state, "route-authorized-candidate");
}
#[test]
fn lan_discovery_address_selection_uses_iroh_direct_addresses() {
let key = AgentKey::generate();

View file

@ -327,6 +327,7 @@ pub fn interface_plan(
.to_owned(),
"activation is explicit and user-scoped".to_owned(),
"all overlay packets remain carried over /geth/overlay/1".to_owned(),
"release archives include docs/overlay-platforms.md with Linux TUN, macOS entitlement, and Windows Wintun guidance".to_owned(),
],
})
}
@ -525,5 +526,17 @@ mod tests {
.iter()
.any(|note| note.contains("generated"))
);
assert!(
interface
.notes
.iter()
.any(|note| note.contains("overlay-platforms.md"))
);
let macos = interface_plan(&plan, Some("macos"), Some("172.22.0.10"))
.expect("macOS interface plan");
assert!(macos.commands.iter().any(|cmd| cmd.contains("utun")));
let windows = interface_plan(&plan, Some("windows"), Some("172.22.0.10"))
.expect("Windows interface plan");
assert!(windows.commands.iter().any(|cmd| cmd.contains("Wintun")));
}
}

View file

@ -47,6 +47,18 @@ pub enum PipeError {
InvalidUnixSocketPath(String),
}
pub const UNIX_FORWARD_UNSUPPORTED_MESSAGE: &str = "Unix socket forwarding is only supported on Unix platforms; use TCP forwarding on this platform";
#[must_use]
pub const fn unix_forward_supported() -> bool {
cfg!(unix)
}
#[must_use]
pub fn unix_forward_unsupported_message() -> &'static str {
UNIX_FORWARD_UNSUPPORTED_MESSAGE
}
pub fn validate_pipe_name(name: &str) -> Result<(), PipeError> {
if name.is_empty()
|| !name
@ -124,6 +136,12 @@ mod tests {
assert!(validate_tcp_forward_target_addr("192.0.2.10:22").is_err());
}
#[test]
fn unix_forward_support_is_explicit_for_platforms() {
assert_eq!(unix_forward_supported(), cfg!(unix));
assert!(unix_forward_unsupported_message().contains("Unix socket forwarding"));
}
#[test]
fn unix_forward_paths_must_be_absolute_without_parent_components() {
assert!(validate_unix_forward_path(Path::new("/tmp/geth.sock")).is_ok());