Implement opt-in overlay TUN runtime
This commit is contained in:
parent
c2dee50dae
commit
d9728a326d
15 changed files with 2315 additions and 84 deletions
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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:?}"),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue