Implement opt-in overlay TUN runtime

This commit is contained in:
Eric Wendland 2026-05-23 02:08:51 +02:00
commit d9728a326d
15 changed files with 2315 additions and 84 deletions

View file

@ -189,14 +189,25 @@ Current prototype commands:
geth overlay plan home
geth overlay plan home --cidr 172.22.0.0/24
geth overlay join home --secret <resource-secret>
geth overlay interface-plan home --platform linux
geth overlay up home
geth overlay down home
geth overlay send home <node> --packet-base64 <ipv4-packet>
geth overlay recv home
geth overlay leave home
Current limits:
- join/leave are planning stubs; no TUN/Wintun interface is created yet
- host network changes must remain explicit opt-in in future versions
- overlay up creates a real TUN/Wintun-style L3 device and usually needs
privileges or host network entitlements
- host network changes are explicit opt-in only
- 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
Bearer invite flow:
geth resource create overlay home
geth secret bearer create resource:overlay:home --capability overlay.join
geth overlay join home --secret <bearer-token>
"#;
const GUIDE_SMOKE_TEST: &str = r#"Minimal smoke test:
@ -540,6 +551,37 @@ pub enum OverlayCommand {
Leave {
name: String,
},
InterfacePlan {
name: String,
#[arg(long)]
platform: Option<String>,
},
Up {
name: String,
#[arg(long)]
bearer_secret: Option<String>,
#[arg(long)]
mtu: Option<u16>,
},
Down {
name: String,
},
Peers {
name: String,
},
Send {
name: String,
node: String,
#[arg(long)]
packet_base64: String,
#[arg(long)]
bearer_secret: Option<String>,
},
Recv {
name: String,
#[arg(long)]
peek: bool,
},
}
#[derive(Debug, Subcommand)]
@ -1297,6 +1339,32 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
ControlRequest::OverlayJoin { name, secret, cidr }
}
OverlayCommand::Leave { name } => ControlRequest::OverlayLeave { name },
OverlayCommand::InterfacePlan { name, platform } => {
ControlRequest::OverlayInterfacePlan { name, platform }
}
OverlayCommand::Up {
name,
bearer_secret,
mtu,
} => ControlRequest::OverlayUp {
name,
bearer_secret,
mtu,
},
OverlayCommand::Down { name } => ControlRequest::OverlayDown { name },
OverlayCommand::Peers { name } => ControlRequest::OverlayPeers { name },
OverlayCommand::Send {
name,
node,
packet_base64,
bearer_secret,
} => ControlRequest::OverlaySend {
name,
node,
packet_base64,
bearer_secret,
},
OverlayCommand::Recv { name, peek } => ControlRequest::OverlayRecv { name, peek },
},
Command::Resource {
command: ResourceCommand::List,
@ -1989,6 +2057,12 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
ControlResponse::OverlayJoined { join } => {
println!("overlay: {}", join.plan.name);
println!("resource: {}", join.plan.resource);
println!("cidr: {}", join.network.cidr);
println!(
"virtual_ip: {}",
join.network.virtual_ip.as_deref().unwrap_or("unassigned")
);
println!("state: {:?}", join.network.state);
println!("enabled: {}", join.enabled);
println!("note: {}", join.note);
}
@ -2001,6 +2075,99 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
println!("stopped: {stopped}");
println!("note: {note}");
}
ControlResponse::OverlayInterfacePlanned { plan } => {
println!("overlay: {}", plan.name);
println!("platform: {}", plan.platform);
println!("interface: {}", plan.interface_name);
println!("cidr: {}", plan.cidr);
println!(
"virtual_ip: {}",
plan.virtual_ip.as_deref().unwrap_or("unassigned")
);
println!("requires_privileges: {}", plan.requires_privileges);
for command in plan.commands {
println!("command: {command}");
}
for note in plan.notes {
println!("note: {note}");
}
}
ControlResponse::OverlayRuntimeStarted { status } => {
println!("overlay: {}", status.name);
println!("interface: {}", status.interface_name);
println!("virtual_ip: {}", status.virtual_ip);
println!("cidr: {}", status.cidr);
println!("mtu: {}", status.mtu);
println!("packets_from_tun: {}", status.packets_from_tun);
println!("packets_to_tun: {}", status.packets_to_tun);
println!("packets_to_peers: {}", status.packets_to_peers);
if let Some(error) = status.last_error {
println!("last_error: {error}");
}
println!("note: {}", status.note);
}
ControlResponse::OverlayRuntimeStopped {
name,
stopped,
note,
} => {
println!("overlay: {name}");
println!("stopped: {stopped}");
println!("note: {note}");
}
ControlResponse::OverlayPeers { name, peers, note } => {
println!("overlay: {name}");
if peers.is_empty() {
println!("no overlay peer candidates");
} else {
for peer in peers {
println!(
"{}\t{}\t{}\t{}",
peer.node_id,
peer.endpoint_id.as_deref().unwrap_or("no-endpoint"),
peer.virtual_ip.as_deref().unwrap_or("no-virtual-ip"),
peer.state
);
}
}
println!("note: {note}");
}
ControlResponse::OverlayPacketSent {
peer_node_id,
peer_agent_id,
endpoint_id,
packet,
allowed,
reason,
note,
} => {
println!("peer: {peer_node_id}");
println!("agent: {peer_agent_id}");
println!("endpoint: {endpoint_id}");
println!("allowed: {allowed}");
println!("reason: {reason}");
if let Some(packet) = packet {
println!("packet: {}", packet.id);
println!("size_bytes: {}", packet.size_bytes);
}
println!("note: {note}");
}
ControlResponse::OverlayPackets {
name,
packets,
drained,
note,
} => {
println!("overlay: {name}");
println!("drained: {drained}");
for packet in packets {
println!(
"{}\t{}\t{}\t{} bytes",
packet.id, packet.source_node, packet.destination_node, packet.size_bytes
);
}
println!("note: {note}");
}
ControlResponse::CasAdded { hash, size_bytes } => {
println!("{hash} {size_bytes} bytes");
}

View file

@ -5,7 +5,10 @@ use geth_discovery::{DiscoveredPeer, PeerCard};
use geth_document::{DocumentResource, DocumentState};
use geth_keychain::{KeychainOp, KeychainOpSignature, NodeEnrollmentRequest, NodeRecord};
use geth_kv::{KvEntry, KvResource, KvSyncEntry};
use geth_overlay::{OverlayJoinPlan, OverlayNetworkStatus, OverlayPlan};
use geth_overlay::{
OverlayInterfacePlan, OverlayJoinPlan, OverlayNetworkStatus, OverlayPacket, OverlayPlan,
OverlayRuntimeStatus,
};
use geth_pipe::{PipeConnection, PipeListener, PipeMessage};
use geth_pubsub::PubsubMessage;
use geth_resource::ResourceDescriptor;
@ -57,6 +60,31 @@ pub enum ControlRequest {
OverlayLeave {
name: String,
},
OverlayInterfacePlan {
name: String,
platform: Option<String>,
},
OverlayUp {
name: String,
bearer_secret: Option<String>,
mtu: Option<u16>,
},
OverlayDown {
name: String,
},
OverlayPeers {
name: String,
},
OverlaySend {
name: String,
node: String,
packet_base64: String,
bearer_secret: Option<String>,
},
OverlayRecv {
name: String,
peek: bool,
},
CasAdd {
path: PathBuf,
},
@ -501,6 +529,37 @@ pub enum ControlResponse {
stopped: bool,
note: String,
},
OverlayInterfacePlanned {
plan: OverlayInterfacePlan,
},
OverlayRuntimeStarted {
status: OverlayRuntimeStatus,
},
OverlayRuntimeStopped {
name: String,
stopped: bool,
note: String,
},
OverlayPeers {
name: String,
peers: Vec<OverlayPeer>,
note: String,
},
OverlayPacketSent {
peer_node_id: String,
peer_agent_id: String,
endpoint_id: String,
packet: Option<OverlayPacket>,
allowed: bool,
reason: String,
note: String,
},
OverlayPackets {
name: String,
packets: Vec<OverlayPacket>,
drained: bool,
note: String,
},
CasAdded {
hash: BlobHash,
size_bytes: u64,
@ -990,6 +1049,14 @@ pub struct CasProvider {
pub last_seen_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayPeer {
pub node_id: String,
pub endpoint_id: Option<String>,
pub virtual_ip: Option<String>,
pub state: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum PeerControlRequest {
@ -1408,6 +1475,38 @@ pub enum PipeWireResponse {
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum OverlayWireRequest {
Packet {
peer_card: PeerCard,
network: String,
packet_base64: String,
nonce: String,
bearer_proof: Option<BearerProof>,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum OverlayWireResponse {
PacketAccepted {
node_id: String,
agent_id: String,
endpoint_id: String,
remote_endpoint_id: String,
packet: Option<Box<OverlayPacket>>,
allowed: bool,
reason: String,
evaluated_ops: usize,
nonce: String,
note: String,
},
Error {
message: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncWatermark {
pub stream: String,
@ -1518,6 +1617,28 @@ pub fn decode_pipe_wire_response(line: &str) -> Result<PipeWireResponse, Control
serde_json::from_str(line).map_err(ControlError::from)
}
pub fn encode_overlay_wire_request(request: &OverlayWireRequest) -> Result<String, ControlError> {
let mut line = serde_json::to_string(request)?;
line.push('\n');
Ok(line)
}
pub fn decode_overlay_wire_request(line: &str) -> Result<OverlayWireRequest, ControlError> {
serde_json::from_str(line).map_err(ControlError::from)
}
pub fn encode_overlay_wire_response(
response: &OverlayWireResponse,
) -> Result<String, ControlError> {
let mut line = serde_json::to_string(response)?;
line.push('\n');
Ok(line)
}
pub fn decode_overlay_wire_response(line: &str) -> Result<OverlayWireResponse, ControlError> {
serde_json::from_str(line).map_err(ControlError::from)
}
#[cfg(test)]
mod tests {
use super::*;
@ -1587,6 +1708,27 @@ mod tests {
request
);
let request = ControlRequest::OverlayUp {
name: "home-lan".to_owned(),
bearer_secret: Some("gbt_route".to_owned()),
mtu: Some(1280),
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
request
);
let request = ControlRequest::OverlaySend {
name: "home-lan".to_owned(),
node: "node:peer".to_owned(),
packet_base64: "RQAAFAAAQABAAQAAqBYAAawWAAI=".to_owned(),
bearer_secret: Some("gbt_invite".to_owned()),
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
request
);
let response = ControlResponse::CasHas {
hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
present: true,
@ -1605,6 +1747,69 @@ mod tests {
response
);
let response = ControlResponse::OverlayJoined {
join: geth_overlay::OverlayJoinPlan {
plan: geth_overlay::plan_overlay("home-lan", None, geth_overlay::OVERLAY_ALPN)
.expect("overlay plan"),
network: geth_overlay::membership_status(
&geth_overlay::joined_overlay_membership(
"home-lan",
None,
"node:local",
"invite",
10,
)
.expect("overlay membership"),
),
enabled: true,
note: "membership persisted".to_owned(),
},
};
assert_eq!(
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
response
);
let response = ControlResponse::OverlayRuntimeStarted {
status: geth_overlay::OverlayRuntimeStatus {
name: "home-lan".to_owned(),
interface_name: "geth-homelan".to_owned(),
virtual_ip: "172.22.0.10".to_owned(),
cidr: "172.22.0.0/24".to_owned(),
mtu: 1280,
started_at_ms: 12,
packets_from_tun: 1,
packets_to_tun: 2,
packets_to_peers: 3,
last_error: None,
note: "active".to_owned(),
},
};
assert_eq!(
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
response
);
let response = ControlResponse::OverlayPackets {
name: "home-lan".to_owned(),
packets: vec![geth_overlay::OverlayPacket {
id: "overlay-packet:test".to_owned(),
network: "home-lan".to_owned(),
source_node: "node:peer".to_owned(),
destination_node: "node:local".to_owned(),
packet_base64: "RQAAFAAAQABAAQAAqBYAAawWAAI=".to_owned(),
size_bytes: 20,
received_at_ms: 11,
note: "received".to_owned(),
}],
drained: true,
note: "packets".to_owned(),
};
assert_eq!(
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
response
);
let response = ControlResponse::NodeList {
nodes: vec![NodeRecord {
id: geth_types::NodeId::new("node:local"),
@ -2641,6 +2846,63 @@ mod tests {
response
);
let request = OverlayWireRequest::Packet {
peer_card: PeerCard {
node_id: "node:caller".into(),
agent_id: "agent:caller".into(),
endpoints: Vec::new(),
issued_at: geth_types::UnixMillis(1),
signature: geth_discovery::SignatureMetadata {
namespace: "geth.peer-card.v1@geth.local".to_owned(),
signer: "agent:caller".to_owned(),
public_key: "key".to_owned(),
signature: "sig".to_owned(),
},
},
network: "home".to_owned(),
packet_base64: "RQAAFAAAQABAAQAAqBYAAawWAAI=".to_owned(),
nonce: "nonce".to_owned(),
bearer_proof: Some(BearerProof {
secret: "bearer:test".into(),
resource: "resource:overlay:home".into(),
capabilities: vec!["overlay.route".into()],
nonce: "nonce".to_owned(),
response: "response".to_owned(),
}),
};
assert_eq!(
decode_overlay_wire_request(&encode_overlay_wire_request(&request).expect("encode"))
.expect("decode"),
request
);
let response = OverlayWireResponse::PacketAccepted {
node_id: "node:peer".to_owned(),
agent_id: "agent:peer".to_owned(),
endpoint_id: "endpoint:peer".to_owned(),
remote_endpoint_id: "endpoint:caller".to_owned(),
packet: Some(Box::new(geth_overlay::OverlayPacket {
id: "overlay-packet:test".to_owned(),
network: "home".to_owned(),
source_node: "node:caller".to_owned(),
destination_node: "node:peer".to_owned(),
packet_base64: "RQAAFAAAQABAAQAAqBYAAawWAAI=".to_owned(),
size_bytes: 20,
received_at_ms: 12,
note: "overlay".to_owned(),
})),
allowed: true,
reason: "direct grant".to_owned(),
evaluated_ops: 1,
nonce: "nonce".to_owned(),
note: "overlay wire".to_owned(),
};
assert_eq!(
decode_overlay_wire_response(&encode_overlay_wire_response(&response).expect("encode"))
.expect("decode"),
response
);
let request = PipeWireRequest::TcpConnect {
peer_card: PeerCard {
node_id: "node:caller".into(),

View file

@ -42,6 +42,7 @@ iroh-blobs.workspace = true
iroh-docs.workspace = true
iroh-gossip.workspace = true
swarm-discovery.workspace = true
tun-rs.workspace = true
[dev-dependencies]
rusqlite.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,7 @@ rust-version.workspace = true
license.workspace = true
[dependencies]
blake3.workspace = true
serde.workspace = true
thiserror.workspace = true
geth-types = { path = "../geth-types" }

View file

@ -34,6 +34,7 @@ pub struct OverlayPlan {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayJoinPlan {
pub plan: OverlayPlan,
pub network: OverlayNetworkStatus,
pub enabled: bool,
pub note: String,
}
@ -57,6 +58,59 @@ pub struct OverlayNetworkStatus {
pub note: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayInterfacePlan {
pub name: String,
pub platform: String,
pub interface_name: String,
pub cidr: String,
pub virtual_ip: Option<String>,
pub requires_privileges: bool,
pub commands: Vec<String>,
pub notes: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayRuntimeStatus {
pub name: String,
pub interface_name: String,
pub virtual_ip: String,
pub cidr: String,
pub mtu: u16,
pub started_at_ms: i64,
pub packets_from_tun: u64,
pub packets_to_tun: u64,
pub packets_to_peers: u64,
pub last_error: Option<String>,
pub note: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayPacket {
pub id: String,
pub network: String,
pub source_node: String,
pub destination_node: String,
pub packet_base64: String,
pub size_bytes: usize,
pub received_at_ms: i64,
pub note: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayMembership {
pub name: String,
pub resource: ResourceId,
pub cidr: String,
pub state: OverlayState,
pub local_node_id: String,
pub virtual_ip: Option<String>,
pub secret_fingerprint: String,
pub joined_at_ms: i64,
pub updated_at_ms: i64,
pub packet_runtime: String,
}
#[derive(Debug, thiserror::Error)]
pub enum OverlayError {
#[error("invalid overlay name `{0}`")]
@ -65,6 +119,12 @@ pub enum OverlayError {
InvalidCidr(String),
#[error("overlay join requires a non-empty resource secret")]
EmptySecret,
#[error("overlay CIDR `{0}` has no usable host addresses")]
NoUsableHostAddress(String),
#[error("invalid IPv4 packet: {0}")]
InvalidIpv4Packet(String),
#[error("unsupported overlay platform `{0}`")]
UnsupportedPlatform(String),
}
pub fn validate_overlay_name(name: &str) -> Result<(), OverlayError> {
@ -104,11 +164,173 @@ pub fn validate_overlay_secret(secret: &str) -> Result<(), OverlayError> {
}
}
pub fn validate_ipv4_packet(packet: &[u8]) -> Result<(), OverlayError> {
if packet.len() < 20 {
return Err(OverlayError::InvalidIpv4Packet(
"packet shorter than IPv4 header".to_owned(),
));
}
if packet[0] >> 4 != 4 {
return Err(OverlayError::InvalidIpv4Packet(
"packet version is not IPv4".to_owned(),
));
}
let header_len = usize::from(packet[0] & 0x0f) * 4;
if header_len < 20 || header_len > packet.len() {
return Err(OverlayError::InvalidIpv4Packet(
"invalid IPv4 header length".to_owned(),
));
}
let total_len = u16::from_be_bytes([packet[2], packet[3]]) as usize;
if total_len < header_len || total_len > packet.len() {
return Err(OverlayError::InvalidIpv4Packet(
"invalid IPv4 total length".to_owned(),
));
}
Ok(())
}
pub fn ipv4_source_destination(packet: &[u8]) -> Result<(String, String), OverlayError> {
validate_ipv4_packet(packet)?;
Ok((
Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]).to_string(),
Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]).to_string(),
))
}
#[must_use]
pub fn overlay_state_key(name: &str) -> String {
format!("overlay:{name}")
}
#[must_use]
pub fn overlay_packet_key(network: &str, received_at_ms: i64, packet_id: &str) -> String {
format!("overlay-packet:{network}:{received_at_ms}:{packet_id}")
}
#[must_use]
pub fn overlay_resource_id(name: &str) -> ResourceId {
ResourceId::new(format!("resource:overlay:{name}"))
}
#[must_use]
pub fn overlay_secret_fingerprint(secret: &str) -> String {
format!("blake3:{}", blake3::hash(secret.as_bytes()))
}
#[must_use]
pub fn overlay_packet_id(
network: &str,
source_node: &str,
destination_node: &str,
packet: &[u8],
received_at_ms: i64,
) -> String {
format!(
"overlay-packet:{}",
blake3::hash(
format!("{network}\0{source_node}\0{destination_node}\0{received_at_ms}\0").as_bytes()
)
.to_hex()
) + &blake3::hash(packet).to_hex()[..16]
}
pub fn joined_overlay_membership(
name: &str,
cidr: Option<&str>,
local_node_id: &str,
secret: &str,
joined_at_ms: i64,
) -> Result<OverlayMembership, OverlayError> {
validate_overlay_secret(secret)?;
let cidr = cidr.unwrap_or(DEFAULT_OVERLAY_CIDR);
validate_overlay_name(name)?;
validate_overlay_cidr(cidr)?;
Ok(OverlayMembership {
name: name.to_owned(),
resource: overlay_resource_id(name),
cidr: cidr.to_owned(),
state: OverlayState::Joined,
local_node_id: local_node_id.to_owned(),
virtual_ip: Some(deterministic_virtual_ip(cidr, local_node_id)?),
secret_fingerprint: overlay_secret_fingerprint(secret),
joined_at_ms,
updated_at_ms: joined_at_ms,
packet_runtime:
"joined: run `geth overlay up <name>` to start the explicit TUN/Wintun runtime"
.to_owned(),
})
}
pub fn stopped_overlay_membership(
mut membership: OverlayMembership,
updated_at_ms: i64,
) -> OverlayMembership {
membership.state = OverlayState::Stopped;
membership.updated_at_ms = updated_at_ms;
membership.packet_runtime = "inactive: overlay membership stopped".to_owned();
membership
}
#[must_use]
pub fn membership_status(membership: &OverlayMembership) -> OverlayNetworkStatus {
OverlayNetworkStatus {
name: membership.name.clone(),
resource: membership.resource.clone(),
cidr: membership.cidr.clone(),
state: membership.state.clone(),
virtual_ip: membership.virtual_ip.clone(),
peers: Vec::new(),
note: membership.packet_runtime.clone(),
}
}
pub fn interface_plan(
plan: &OverlayPlan,
platform: Option<&str>,
virtual_ip: Option<&str>,
) -> Result<OverlayInterfacePlan, OverlayError> {
let platform = platform.unwrap_or(std::env::consts::OS);
let interface_name = format!("geth-{}", interface_slug(&plan.name));
let prefix = cidr_prefix(&plan.cidr)?;
let virtual_ip = virtual_ip.map(ToOwned::to_owned);
let addr = virtual_ip
.as_ref()
.map(|ip| format!("{ip}/{prefix}"))
.unwrap_or_else(|| format!("<virtual-ip>/{prefix}"));
let commands = match platform {
"linux" => vec![
format!("ip tuntap add dev {interface_name} mode tun user <user>"),
format!("ip addr add {addr} dev {interface_name}"),
format!("ip link set {interface_name} up"),
],
"macos" | "darwin" => vec![
"open a utun device from the geth daemon process".to_owned(),
format!("ifconfig <utunN> inet {} {} up", addr, plan.cidr),
],
"windows" => vec![
"install or open a Wintun adapter for the current user context".to_owned(),
format!("assign {addr} to the Wintun adapter"),
],
other => return Err(OverlayError::UnsupportedPlatform(other.to_owned())),
};
Ok(OverlayInterfacePlan {
name: plan.name.clone(),
platform: platform.to_owned(),
interface_name,
cidr: plan.cidr.clone(),
virtual_ip,
requires_privileges: true,
commands,
notes: vec![
"generated plan only; run `geth overlay up <name>` to activate host networking"
.to_owned(),
"activation is explicit and user-scoped".to_owned(),
"all overlay packets remain carried over /geth/overlay/1".to_owned(),
],
})
}
pub fn plan_overlay(
name: &str,
cidr: Option<&str>,
@ -128,7 +350,7 @@ pub fn plan_overlay(
CAPABILITY_ADMIN.to_owned(),
],
discovery: "future overlay discovery may use mDNS, peer exchange, and resource metadata; discovery remains untrusted".to_owned(),
runtime: "planned only in this prototype; no TUN/Wintun interface is created".to_owned(),
runtime: "explicit opt-in runtime available with `geth overlay up <name>`; creates a TUN/Wintun-style L3 interface".to_owned(),
security: vec![
"all overlay packets must be carried over daemon-owned Iroh connections".to_owned(),
"knowing an EndpointID or overlay name must not grant overlay access".to_owned(),
@ -137,15 +359,73 @@ pub fn plan_overlay(
],
implementation_notes: vec![
"inspired by iroh-lan's Iroh-carried packet overlay".to_owned(),
"future packet runtime should register /geth/overlay/1 on the shared geth Iroh router".to_owned(),
"future host integration may need TUN/Wintun privileges and must remain explicitly opt-in".to_owned(),
"packet runtime registers /geth/overlay/1 on the shared geth Iroh router".to_owned(),
"host integration may need TUN/Wintun privileges and remains explicitly opt-in".to_owned(),
],
})
}
#[must_use]
pub fn overlay_status_note() -> &'static str {
"overlay runtime is scaffolded but inactive; use `geth overlay plan <name>` to inspect the intended resource and capabilities"
"overlay membership is persisted locally; run `geth overlay up <name>` to start the explicit TUN/Wintun runtime"
}
pub fn deterministic_virtual_ip(cidr: &str, seed: &str) -> Result<String, OverlayError> {
let (network, prefix) = parse_ipv4_cidr(cidr)?;
let host_bits = 32_u8.saturating_sub(prefix);
let address = if host_bits == 0 {
network
} else {
let host_space = 1_u64 << host_bits;
if host_space <= 2 {
return Err(OverlayError::NoUsableHostAddress(cidr.to_owned()));
}
let hash = blake3::hash(seed.as_bytes());
let mut bytes = [0_u8; 8];
bytes.copy_from_slice(&hash.as_bytes()[..8]);
let value = u64::from_le_bytes(bytes);
let host_offset = 1 + (value % (host_space - 2));
network + host_offset as u32
};
Ok(Ipv4Addr::from(address).to_string())
}
pub fn cidr_prefix(cidr: &str) -> Result<u8, OverlayError> {
parse_ipv4_cidr(cidr).map(|(_, prefix)| prefix)
}
fn parse_ipv4_cidr(cidr: &str) -> Result<(u32, u8), OverlayError> {
let (addr, prefix) = cidr
.split_once('/')
.ok_or_else(|| OverlayError::InvalidCidr(cidr.to_owned()))?;
let addr = addr
.parse::<Ipv4Addr>()
.map_err(|_| OverlayError::InvalidCidr(cidr.to_owned()))?;
let prefix = prefix
.parse::<u8>()
.map_err(|_| OverlayError::InvalidCidr(cidr.to_owned()))?;
if prefix > 32 {
return Err(OverlayError::InvalidCidr(cidr.to_owned()));
}
let mask = if prefix == 0 {
0
} else {
u32::MAX << (32 - prefix)
};
Ok((u32::from(addr) & mask, prefix))
}
fn interface_slug(name: &str) -> String {
let slug = name
.chars()
.filter(|ch| ch.is_ascii_alphanumeric())
.take(8)
.collect::<String>();
if slug.is_empty() {
"net".to_owned()
} else {
slug
}
}
#[cfg(test)]
@ -177,6 +457,73 @@ mod tests {
assert_eq!(plan.cidr, DEFAULT_OVERLAY_CIDR);
assert!(plan.capabilities.contains(&CAPABILITY_JOIN.to_owned()));
assert!(plan.security.iter().any(|note| note.contains("Iroh")));
assert!(plan.runtime.contains("planned only"));
assert!(plan.runtime.contains("overlay up"));
}
#[test]
fn joined_membership_derives_virtual_ip_without_storing_secret() {
let membership = joined_overlay_membership(
"home",
Some("172.22.0.0/24"),
"node:local",
"invite-token",
42,
)
.expect("membership");
assert_eq!(
membership.resource,
ResourceId::new("resource:overlay:home")
);
assert_eq!(membership.virtual_ip, Some("172.22.0.168".to_owned()));
assert_ne!(membership.secret_fingerprint, "invite-token");
assert_eq!(membership.joined_at_ms, 42);
let status = membership_status(&membership);
assert_eq!(status.state, OverlayState::Joined);
assert_eq!(status.virtual_ip, membership.virtual_ip);
}
#[test]
fn tiny_overlay_cidrs_do_not_assign_hosts() {
assert!(
joined_overlay_membership("tiny", Some("10.0.0.0/31"), "node:local", "secret", 1)
.is_err()
);
}
#[test]
fn validates_ipv4_packets() {
let packet = [
0x45, 0x00, 0x00, 0x14, 0x00, 0x00, 0x40, 0x00, 64, 1, 0, 0, 172, 22, 0, 1, 172, 22, 0,
2,
];
validate_ipv4_packet(&packet).expect("valid packet");
assert!(validate_ipv4_packet(&packet[..10]).is_err());
let mut bad = packet;
bad[0] = 0x65;
assert!(validate_ipv4_packet(&bad).is_err());
let (_, destination) = ipv4_source_destination(&packet).expect("addresses");
assert_eq!(destination, "172.22.0.2");
}
#[test]
fn interface_plan_is_generated_without_mutating_host_networking() {
let plan = plan_overlay("home", None, OVERLAY_ALPN).expect("plan");
let interface =
interface_plan(&plan, Some("linux"), Some("172.22.0.10")).expect("interface plan");
assert_eq!(interface.interface_name, "geth-home");
assert!(interface.requires_privileges);
assert!(
interface
.commands
.iter()
.any(|cmd| cmd.contains("ip tuntap"))
);
assert!(
interface
.notes
.iter()
.any(|note| note.contains("generated"))
);
}
}

View file

@ -905,6 +905,14 @@ impl Store {
}
}
pub fn delete_module_state(&self, module: &str) -> Result<(), StoreError> {
self.conn.execute(
"DELETE FROM module_state WHERE module = ?1",
params![module],
)?;
Ok(())
}
pub fn list_module_states_with_prefix(
&self,
prefix: &str,
@ -1714,6 +1722,15 @@ mod tests {
.len(),
1
);
store
.delete_module_state("live-sync:node:laptop:ssh-certs")
.expect("delete state");
assert_eq!(
store
.get_module_state("live-sync:node:laptop:ssh-certs")
.expect("get deleted state"),
None
);
}
#[test]

View file

@ -24,6 +24,7 @@ geth-discovery = { path = "../geth-discovery" }
geth-iroh = { path = "../geth-iroh" }
geth-keychain = { path = "../geth-keychain" }
geth-node = { path = "../geth-node" }
geth-overlay = { path = "../geth-overlay" }
geth-ssh-identity = { path = "../geth-ssh-identity" }
geth-store = { path = "../geth-store" }
geth-types = { path = "../geth-types" }

View file

@ -1446,7 +1446,135 @@ fn overlay_plan_is_available_without_starting_packet_runtime() {
assert_eq!(plan.resource.to_string(), "resource:overlay:home-lan");
assert_eq!(plan.alpn, "/geth/overlay/1");
assert!(plan.capabilities.contains(&"overlay.join".to_owned()));
assert!(plan.runtime.contains("no TUN/Wintun"));
assert!(plan.runtime.contains("overlay up"));
}
other => panic!("unexpected response: {other:?}"),
}
}
#[test]
fn overlay_join_persists_membership_resource_and_leave_state() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let joined = geth_node::handle_request(
&node,
geth_control::ControlRequest::OverlayJoin {
name: "home-lan".to_owned(),
secret: "local-bootstrap-secret".to_owned(),
cidr: Some("172.22.0.0/24".to_owned()),
},
)
.expect("overlay join");
match joined {
geth_control::ControlResponse::OverlayJoined { join } => {
assert!(join.enabled);
assert_eq!(join.plan.resource.to_string(), "resource:overlay:home-lan");
assert_eq!(join.network.state, geth_overlay::OverlayState::Joined);
assert!(join.network.virtual_ip.is_some());
assert!(join.note.contains("local bootstrap overlay"));
}
other => panic!("unexpected response: {other:?}"),
}
let resources = geth_node::handle_request(&node, geth_control::ControlRequest::ResourceList)
.expect("resource list");
match resources {
geth_control::ControlResponse::ResourceList { resources } => {
assert!(resources.iter().any(|resource| {
resource.id.to_string() == "resource:overlay:home-lan"
&& resource.kind == geth_types::ResourceKind::Overlay
}));
}
other => panic!("unexpected response: {other:?}"),
}
let status = geth_node::handle_request(&node, geth_control::ControlRequest::OverlayStatus)
.expect("overlay status");
match status {
geth_control::ControlResponse::OverlayStatus { networks, note } => {
assert_eq!(networks.len(), 1);
assert_eq!(networks[0].state, geth_overlay::OverlayState::Joined);
assert!(note.contains("persisted locally"));
}
other => panic!("unexpected response: {other:?}"),
}
geth_node::handle_request(
&node,
geth_control::ControlRequest::OverlayLeave {
name: "home-lan".to_owned(),
},
)
.expect("overlay leave");
let status = geth_node::handle_request(&node, geth_control::ControlRequest::OverlayStatus)
.expect("overlay status after leave");
match status {
geth_control::ControlResponse::OverlayStatus { networks, .. } => {
assert_eq!(networks.len(), 1);
assert_eq!(networks[0].state, geth_overlay::OverlayState::Stopped);
}
other => panic!("unexpected response: {other:?}"),
}
}
#[test]
fn overlay_join_uses_bearer_invite_when_one_exists() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let resource = "resource:overlay:locked".to_owned();
geth_node::handle_request(
&node,
geth_control::ControlRequest::ResourceCreate {
kind: "overlay".to_owned(),
name: "locked".to_owned(),
},
)
.expect("create overlay resource");
let access = geth_node::handle_request(
&node,
geth_control::ControlRequest::SecretBearerCreate {
resource: resource.clone(),
capabilities: vec!["overlay.join".to_owned()],
expires_at_ms: None,
},
)
.expect("create bearer invite");
let token = match access {
geth_control::ControlResponse::SecretBearerCreated { access } => {
access.token.expect("private bearer token").to_string()
}
other => panic!("unexpected response: {other:?}"),
};
let denied = geth_node::handle_request(
&node,
geth_control::ControlRequest::OverlayJoin {
name: "locked".to_owned(),
secret: "wrong-token".to_owned(),
cidr: None,
},
)
.expect_err("bad bearer should be rejected");
assert!(denied.to_string().contains("overlay join requires"));
let joined = geth_node::handle_request(
&node,
geth_control::ControlRequest::OverlayJoin {
name: "locked".to_owned(),
secret: token,
cidr: None,
},
)
.expect("authorized overlay join");
match joined {
geth_control::ControlResponse::OverlayJoined { join } => {
assert!(join.enabled);
assert_eq!(join.network.state, geth_overlay::OverlayState::Joined);
assert!(join.note.contains("bearer overlay.join authorization"));
}
other => panic!("unexpected response: {other:?}"),
}