Add overlay planning commands
This commit is contained in:
parent
7d4a288729
commit
c2dee50dae
18 changed files with 928 additions and 16 deletions
11
crates/geth-overlay/Cargo.toml
Normal file
11
crates/geth-overlay/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "geth-overlay"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
geth-types = { path = "../geth-types" }
|
||||
182
crates/geth-overlay/src/lib.rs
Normal file
182
crates/geth-overlay/src/lib.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
use geth_types::ResourceId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
pub const DEFAULT_OVERLAY_CIDR: &str = "172.22.0.0/24";
|
||||
pub const OVERLAY_ALPN: &str = "/geth/overlay/1";
|
||||
|
||||
pub const CAPABILITY_JOIN: &str = "overlay.join";
|
||||
pub const CAPABILITY_ROUTE: &str = "overlay.route";
|
||||
pub const CAPABILITY_ADMIN: &str = "overlay.admin";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum OverlayState {
|
||||
Planned,
|
||||
Joined,
|
||||
Running,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OverlayPlan {
|
||||
pub name: String,
|
||||
pub resource: ResourceId,
|
||||
pub cidr: String,
|
||||
pub alpn: String,
|
||||
pub capabilities: Vec<String>,
|
||||
pub discovery: String,
|
||||
pub runtime: String,
|
||||
pub security: Vec<String>,
|
||||
pub implementation_notes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OverlayJoinPlan {
|
||||
pub plan: OverlayPlan,
|
||||
pub enabled: bool,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[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: OverlayState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OverlayNetworkStatus {
|
||||
pub name: String,
|
||||
pub resource: ResourceId,
|
||||
pub cidr: String,
|
||||
pub state: OverlayState,
|
||||
pub virtual_ip: Option<String>,
|
||||
pub peers: Vec<OverlayPeer>,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum OverlayError {
|
||||
#[error("invalid overlay name `{0}`")]
|
||||
InvalidName(String),
|
||||
#[error("invalid overlay IPv4 CIDR `{0}`")]
|
||||
InvalidCidr(String),
|
||||
#[error("overlay join requires a non-empty resource secret")]
|
||||
EmptySecret,
|
||||
}
|
||||
|
||||
pub fn validate_overlay_name(name: &str) -> Result<(), OverlayError> {
|
||||
if name.is_empty()
|
||||
|| name.len() > 63
|
||||
|| !name
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
|
||||
|| name.starts_with('.')
|
||||
|| name.starts_with('-')
|
||||
{
|
||||
return Err(OverlayError::InvalidName(name.to_owned()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_overlay_cidr(cidr: &str) -> Result<(), OverlayError> {
|
||||
let (addr, prefix) = cidr
|
||||
.split_once('/')
|
||||
.ok_or_else(|| OverlayError::InvalidCidr(cidr.to_owned()))?;
|
||||
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()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_overlay_secret(secret: &str) -> Result<(), OverlayError> {
|
||||
if secret.trim().is_empty() {
|
||||
Err(OverlayError::EmptySecret)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn overlay_resource_id(name: &str) -> ResourceId {
|
||||
ResourceId::new(format!("resource:overlay:{name}"))
|
||||
}
|
||||
|
||||
pub fn plan_overlay(
|
||||
name: &str,
|
||||
cidr: Option<&str>,
|
||||
alpn: &str,
|
||||
) -> Result<OverlayPlan, OverlayError> {
|
||||
validate_overlay_name(name)?;
|
||||
let cidr = cidr.unwrap_or(DEFAULT_OVERLAY_CIDR);
|
||||
validate_overlay_cidr(cidr)?;
|
||||
Ok(OverlayPlan {
|
||||
name: name.to_owned(),
|
||||
resource: overlay_resource_id(name),
|
||||
cidr: cidr.to_owned(),
|
||||
alpn: alpn.to_owned(),
|
||||
capabilities: vec![
|
||||
CAPABILITY_JOIN.to_owned(),
|
||||
CAPABILITY_ROUTE.to_owned(),
|
||||
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(),
|
||||
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(),
|
||||
"overlay membership must be resource-authorized with overlay.join/overlay.route capabilities".to_owned(),
|
||||
"shared overlay secrets are resource-scoped and must not mutate node identity".to_owned(),
|
||||
],
|
||||
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(),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
#[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"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validates_overlay_names() {
|
||||
validate_overlay_name("home-lan").expect("valid name");
|
||||
validate_overlay_name("dev.mesh_1").expect("valid name");
|
||||
assert!(validate_overlay_name("").is_err());
|
||||
assert!(validate_overlay_name("../lan").is_err());
|
||||
assert!(validate_overlay_name("-lan").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_overlay_cidrs() {
|
||||
validate_overlay_cidr(DEFAULT_OVERLAY_CIDR).expect("valid cidr");
|
||||
validate_overlay_cidr("10.44.0.0/16").expect("valid cidr");
|
||||
assert!(validate_overlay_cidr("10.44.0.0").is_err());
|
||||
assert!(validate_overlay_cidr("10.44.0.0/33").is_err());
|
||||
assert!(validate_overlay_cidr("not-a-cidr").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlay_plan_is_iroh_only_and_resource_scoped() {
|
||||
let plan = plan_overlay("home", None, OVERLAY_ALPN).expect("plan");
|
||||
assert_eq!(plan.resource, ResourceId::new("resource:overlay:home"));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue