Add overlay planning commands
This commit is contained in:
parent
7d4a288729
commit
c2dee50dae
18 changed files with 928 additions and 16 deletions
|
|
@ -1,18 +1,239 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use base64::Engine;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use geth_config::GethPaths;
|
||||
use geth_control::{ControlRequest, ControlResponse};
|
||||
use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport};
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const TOP_LEVEL_AFTER_HELP: &str = r#"Common starts:
|
||||
geth guide init
|
||||
geth guide owner-setup
|
||||
geth guide overlay
|
||||
geth init
|
||||
geth init --admin-key ~/.ssh/id_ed25519_sk.pub --signing-key ~/.ssh/id_ed25519_sk --node-name laptop
|
||||
geth daemon run
|
||||
geth status
|
||||
|
||||
Use `geth <command> --help` for command-specific examples."#;
|
||||
|
||||
const INIT_LONG_ABOUT: &str = r#"Initialize local geth state.
|
||||
|
||||
With no owner options, `geth init` creates local directories, config, metadata
|
||||
store, and the daemon agent identity. This is enough for local CAS/KV/document
|
||||
testing and for later enrollment into an owner's mesh.
|
||||
|
||||
Owner setup is SSH-admin-rooted. `--admin-key` is the OpenSSH public key that is
|
||||
recorded as an admin trust anchor. `--signing-key` is the matching private SSH
|
||||
key path used immediately through `ssh-keygen -Y sign` to sign the initial
|
||||
keychain/auth statements. For security-key/YubiKey keys, use the private key
|
||||
stub path such as `~/.ssh/id_ed25519_sk`; ssh-keygen/ssh-agent will trigger the
|
||||
hardware-backed signing flow.
|
||||
|
||||
If any owner setup option is used (`--admin-key`, `--signing-key`, `--owner`,
|
||||
`--node-name`, or `--capability`), both `--admin-key` and `--signing-key` are
|
||||
required so geth never creates unsigned owner/device/node statements by
|
||||
accident."#;
|
||||
|
||||
const INIT_AFTER_HELP: &str = r#"Examples:
|
||||
# Local-only node for development or later enrollment:
|
||||
geth init
|
||||
|
||||
# Owner/admin node using an SSH or YubiKey-backed admin key:
|
||||
geth init \
|
||||
--admin-key ~/.ssh/id_ed25519_sk.pub \
|
||||
--signing-key ~/.ssh/id_ed25519_sk \
|
||||
--owner eric \
|
||||
--node-name laptop
|
||||
|
||||
# Owner node with initial resource grants:
|
||||
geth init \
|
||||
--admin-key ~/.ssh/id_ed25519.pub \
|
||||
--signing-key ~/.ssh/id_ed25519 \
|
||||
--node-name laptop \
|
||||
--capability resource:cas:local=cas.fetch \
|
||||
--capability resource:ssh-proxy:local=ssh_proxy.connect
|
||||
|
||||
Key paths:
|
||||
--admin-key OpenSSH public key, usually *.pub. Stored as the trust anchor.
|
||||
--signing-key Matching private key or security-key stub. Used to sign init ops.
|
||||
|
||||
Related:
|
||||
geth guide owner-setup
|
||||
geth keychain status
|
||||
geth node list"#;
|
||||
|
||||
const GUIDE_INDEX: &str = r#"Usage: geth guide <topic>
|
||||
|
||||
Topics:
|
||||
init Local-only init versus owner/admin init.
|
||||
owner-setup First owner node with SSH/YubiKey admin trust.
|
||||
enrollment Add another node/device to the owner mesh.
|
||||
keys Meaning of --admin-key and --signing-key.
|
||||
overlay Optional Iroh overlay network planning.
|
||||
service Install and manage geth as a user service.
|
||||
smoke-test Minimal commands to verify a node and daemon."#;
|
||||
|
||||
const GUIDE_INIT: &str = r#"geth init has two modes.
|
||||
|
||||
Local-only:
|
||||
geth init
|
||||
|
||||
Creates GETH_HOME, config.toml, geth.sqlite, CAS directories, and a local agent
|
||||
identity. Use this for local testing or for a node that will later request
|
||||
enrollment into an owner's mesh.
|
||||
|
||||
Owner/admin:
|
||||
geth init --admin-key ~/.ssh/id_ed25519_sk.pub --signing-key ~/.ssh/id_ed25519_sk --node-name laptop
|
||||
|
||||
This records signed owner, device, node, and agent bindings. Use it on the
|
||||
machine where you control the admin SSH/YubiKey key. Once initialized, inspect:
|
||||
|
||||
geth keychain status
|
||||
geth node list
|
||||
"#;
|
||||
|
||||
const GUIDE_OWNER_SETUP: &str = r#"Owner setup flow:
|
||||
|
||||
1. Pick or create an SSH admin key. Security-key/YubiKey-backed OpenSSH keys are
|
||||
supported through ssh-keygen:
|
||||
|
||||
ssh-keygen -t ed25519-sk -f ~/.ssh/id_ed25519_sk
|
||||
|
||||
2. Initialize the owner node:
|
||||
|
||||
geth init \
|
||||
--admin-key ~/.ssh/id_ed25519_sk.pub \
|
||||
--signing-key ~/.ssh/id_ed25519_sk \
|
||||
--owner eric \
|
||||
--node-name owner-laptop
|
||||
|
||||
3. Start the daemon and export a peer card:
|
||||
|
||||
geth daemon run
|
||||
geth peer export --out /tmp/owner.peer.json
|
||||
|
||||
`--admin-key` is public and replicated as the admin trust anchor.
|
||||
`--signing-key` is private and only used locally to sign canonical init ops.
|
||||
"#;
|
||||
|
||||
const GUIDE_ENROLLMENT: &str = r#"Add another node/device:
|
||||
|
||||
On the new node:
|
||||
geth init
|
||||
geth keychain init --admin-key ~/.ssh/id_ed25519_sk.pub
|
||||
geth peer import /tmp/owner.peer.json
|
||||
geth node enroll request --node-name workstation --out /tmp/workstation-enrollment.json
|
||||
geth node enroll submit owner --path /tmp/workstation-enrollment.json
|
||||
|
||||
On the owner/YubiKey machine:
|
||||
geth node enroll list
|
||||
geth node enroll approve <request-id> --signing-key ~/.ssh/id_ed25519_sk
|
||||
|
||||
Back on the new node:
|
||||
geth sync now owner
|
||||
geth node list
|
||||
|
||||
Enrollment approval records signed keychain/auth operations. Discovery and peer
|
||||
cards alone never grant trust or capabilities.
|
||||
"#;
|
||||
|
||||
const GUIDE_KEYS: &str = r#"Key terminology:
|
||||
|
||||
--admin-key
|
||||
OpenSSH public key path, usually ending in .pub. This key is recorded in the
|
||||
geth keychain as an admin trust anchor. It is safe to distribute.
|
||||
|
||||
--signing-key
|
||||
Matching private key path, or the OpenSSH security-key/YubiKey stub path. geth
|
||||
shells out to ssh-keygen -Y sign with explicit namespaces to sign canonical
|
||||
geth keychain/auth operations. The private key is not copied into geth state.
|
||||
|
||||
Examples:
|
||||
Software key:
|
||||
--admin-key ~/.ssh/id_ed25519.pub --signing-key ~/.ssh/id_ed25519
|
||||
|
||||
YubiKey/FIDO OpenSSH key:
|
||||
--admin-key ~/.ssh/id_ed25519_sk.pub --signing-key ~/.ssh/id_ed25519_sk
|
||||
|
||||
If you use --owner, --node-name, or --capability during init, geth requires both
|
||||
key options because those fields create signed owner/device/node statements.
|
||||
"#;
|
||||
|
||||
const GUIDE_SERVICE: &str = r#"Install geth as a user service:
|
||||
|
||||
geth daemon service install --start
|
||||
geth daemon service status
|
||||
geth daemon service stop
|
||||
geth daemon service start
|
||||
geth daemon service uninstall
|
||||
|
||||
Service installation targets user service managers, not system services:
|
||||
Linux: systemd --user
|
||||
macOS: launchd user agent
|
||||
Windows: current-user scheduled task
|
||||
|
||||
Preview definitions without installing:
|
||||
geth daemon service print
|
||||
"#;
|
||||
|
||||
const GUIDE_OVERLAY: &str = r#"Optional overlay network:
|
||||
|
||||
geth has an experimental overlay-network design inspired by iroh-lan. The
|
||||
intended future runtime is a private L3-style packet overlay carried over geth's
|
||||
daemon-owned Iroh endpoint.
|
||||
|
||||
Current prototype commands:
|
||||
geth overlay status
|
||||
geth overlay plan home
|
||||
geth overlay plan home --cidr 172.22.0.0/24
|
||||
geth overlay join home --secret <resource-secret>
|
||||
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
|
||||
- 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
|
||||
"#;
|
||||
|
||||
const GUIDE_SMOKE_TEST: &str = r#"Minimal smoke test:
|
||||
|
||||
Terminal 1:
|
||||
export GETH_HOME="$(mktemp -d)"
|
||||
geth init
|
||||
geth daemon run
|
||||
|
||||
Terminal 2:
|
||||
export GETH_HOME="<same dir>"
|
||||
geth status
|
||||
geth node id
|
||||
echo "hello geth" > /tmp/hello-geth.txt
|
||||
geth cas add /tmp/hello-geth.txt
|
||||
geth cas list
|
||||
geth keychain status
|
||||
|
||||
For two-node owner/enrollment testing, use:
|
||||
geth guide owner-setup
|
||||
geth guide enrollment
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "geth", about = "Personal local-first Iroh mesh runtime")]
|
||||
#[command(
|
||||
name = "geth",
|
||||
about = "Personal local-first Iroh mesh runtime",
|
||||
after_long_help = TOP_LEVEL_AFTER_HELP
|
||||
)]
|
||||
pub struct Cli {
|
||||
#[arg(long, global = true)]
|
||||
#[arg(long, global = true, help = "Print machine-readable JSON output")]
|
||||
pub json: bool,
|
||||
#[arg(long, global = true)]
|
||||
#[arg(
|
||||
long,
|
||||
global = true,
|
||||
help = "Print newline-delimited JSON output for streaming commands"
|
||||
)]
|
||||
pub jsonl: bool,
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
|
|
@ -20,16 +241,43 @@ pub struct Cli {
|
|||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
Guide {
|
||||
#[arg(value_enum)]
|
||||
topic: Option<GuideTopic>,
|
||||
},
|
||||
#[command(long_about = INIT_LONG_ABOUT, after_long_help = INIT_AFTER_HELP)]
|
||||
Init {
|
||||
#[arg(long)]
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "OPENSSH_PUBLIC_KEY",
|
||||
help = "OpenSSH public key recorded as the owner/admin trust anchor",
|
||||
long_help = "Path to the OpenSSH public key recorded as the owner/admin trust anchor, usually ~/.ssh/<key>.pub. This key is public and replicated in the geth keychain."
|
||||
)]
|
||||
admin_key: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "OPENSSH_PRIVATE_KEY",
|
||||
help = "Matching private key or YubiKey/FIDO stub used to sign init statements",
|
||||
long_help = "Path to the matching private OpenSSH key, or security-key/YubiKey stub such as ~/.ssh/id_ed25519_sk. geth uses ssh-keygen -Y sign with explicit geth namespaces; it does not copy the private key into geth state."
|
||||
)]
|
||||
signing_key: Option<PathBuf>,
|
||||
#[arg(long, default_value = "owner")]
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "owner",
|
||||
help = "Owner/user display name recorded during owner init"
|
||||
)]
|
||||
owner: String,
|
||||
#[arg(long, default_value = "local")]
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "local",
|
||||
help = "Friendly node name recorded during owner init"
|
||||
)]
|
||||
node_name: String,
|
||||
#[arg(long = "capability")]
|
||||
#[arg(
|
||||
long = "capability",
|
||||
value_name = "RESOURCE=CAPABILITY",
|
||||
help = "Initial capability grant for this node; repeatable"
|
||||
)]
|
||||
capabilities: Vec<String>,
|
||||
},
|
||||
Daemon {
|
||||
|
|
@ -49,6 +297,10 @@ pub enum Command {
|
|||
#[command(subcommand)]
|
||||
command: PeerCommand,
|
||||
},
|
||||
Overlay {
|
||||
#[command(subcommand)]
|
||||
command: OverlayCommand,
|
||||
},
|
||||
Resource {
|
||||
#[command(subcommand)]
|
||||
command: ResourceCommand,
|
||||
|
|
@ -95,6 +347,17 @@ pub enum Command {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, ValueEnum)]
|
||||
pub enum GuideTopic {
|
||||
Init,
|
||||
OwnerSetup,
|
||||
Enrollment,
|
||||
Keys,
|
||||
Overlay,
|
||||
Service,
|
||||
SmokeTest,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum DaemonCommand {
|
||||
Run,
|
||||
|
|
@ -259,6 +522,26 @@ pub enum PeerCommand {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum OverlayCommand {
|
||||
Status,
|
||||
Plan {
|
||||
name: String,
|
||||
#[arg(long)]
|
||||
cidr: Option<String>,
|
||||
},
|
||||
Join {
|
||||
name: String,
|
||||
#[arg(long)]
|
||||
secret: String,
|
||||
#[arg(long)]
|
||||
cidr: Option<String>,
|
||||
},
|
||||
Leave {
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum ResourceCommand {
|
||||
List,
|
||||
|
|
@ -740,6 +1023,9 @@ pub async fn run() -> Result<()> {
|
|||
let cli = Cli::parse();
|
||||
let paths = GethPaths::resolve().context("resolve geth paths")?;
|
||||
match cli.command {
|
||||
Command::Guide { topic } => {
|
||||
print_guide(topic, cli.json || cli.jsonl)?;
|
||||
}
|
||||
Command::Init {
|
||||
admin_key,
|
||||
signing_key,
|
||||
|
|
@ -832,6 +1118,31 @@ pub async fn run() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn print_guide(topic: Option<GuideTopic>, json: bool) -> Result<()> {
|
||||
let (name, body) = match topic {
|
||||
None => ("index", GUIDE_INDEX),
|
||||
Some(GuideTopic::Init) => ("init", GUIDE_INIT),
|
||||
Some(GuideTopic::OwnerSetup) => ("owner-setup", GUIDE_OWNER_SETUP),
|
||||
Some(GuideTopic::Enrollment) => ("enrollment", GUIDE_ENROLLMENT),
|
||||
Some(GuideTopic::Keys) => ("keys", GUIDE_KEYS),
|
||||
Some(GuideTopic::Overlay) => ("overlay", GUIDE_OVERLAY),
|
||||
Some(GuideTopic::Service) => ("service", GUIDE_SERVICE),
|
||||
Some(GuideTopic::SmokeTest) => ("smoke-test", GUIDE_SMOKE_TEST),
|
||||
};
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"topic": name,
|
||||
"body": body,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
print!("{body}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn request_for_command(command: Command) -> Result<ControlRequest> {
|
||||
Ok(match command {
|
||||
Command::Status => ControlRequest::Status,
|
||||
|
|
@ -979,6 +1290,14 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
capability,
|
||||
},
|
||||
},
|
||||
Command::Overlay { command } => match command {
|
||||
OverlayCommand::Status => ControlRequest::OverlayStatus,
|
||||
OverlayCommand::Plan { name, cidr } => ControlRequest::OverlayPlan { name, cidr },
|
||||
OverlayCommand::Join { name, secret, cidr } => {
|
||||
ControlRequest::OverlayJoin { name, secret, cidr }
|
||||
}
|
||||
OverlayCommand::Leave { name } => ControlRequest::OverlayLeave { name },
|
||||
},
|
||||
Command::Resource {
|
||||
command: ResourceCommand::List,
|
||||
} => ControlRequest::ResourceList,
|
||||
|
|
@ -1447,7 +1766,9 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
},
|
||||
},
|
||||
},
|
||||
Command::Init { .. } | Command::Daemon { .. } => bail!("command is handled directly"),
|
||||
Command::Guide { .. } | Command::Init { .. } | Command::Daemon { .. } => {
|
||||
bail!("command is handled directly")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1633,6 +1954,53 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
resource.kind, resource.name, resource.id
|
||||
);
|
||||
}
|
||||
ControlResponse::OverlayStatus { networks, note } => {
|
||||
if networks.is_empty() {
|
||||
println!("no overlay networks active");
|
||||
} else {
|
||||
for network in networks {
|
||||
println!(
|
||||
"{}\t{}\t{}\t{:?}\t{} peers",
|
||||
network.name,
|
||||
network.resource,
|
||||
network.cidr,
|
||||
network.state,
|
||||
network.peers.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
println!("note: {note}");
|
||||
}
|
||||
ControlResponse::OverlayPlanned { plan } => {
|
||||
println!("overlay: {}", plan.name);
|
||||
println!("resource: {}", plan.resource);
|
||||
println!("cidr: {}", plan.cidr);
|
||||
println!("alpn: {}", plan.alpn);
|
||||
println!("capabilities: {}", plan.capabilities.join(","));
|
||||
println!("discovery: {}", plan.discovery);
|
||||
println!("runtime: {}", plan.runtime);
|
||||
for note in plan.security {
|
||||
println!("security: {note}");
|
||||
}
|
||||
for note in plan.implementation_notes {
|
||||
println!("implementation: {note}");
|
||||
}
|
||||
}
|
||||
ControlResponse::OverlayJoined { join } => {
|
||||
println!("overlay: {}", join.plan.name);
|
||||
println!("resource: {}", join.plan.resource);
|
||||
println!("enabled: {}", join.enabled);
|
||||
println!("note: {}", join.note);
|
||||
}
|
||||
ControlResponse::OverlayLeft {
|
||||
name,
|
||||
stopped,
|
||||
note,
|
||||
} => {
|
||||
println!("overlay: {name}");
|
||||
println!("stopped: {stopped}");
|
||||
println!("note: {note}");
|
||||
}
|
||||
ControlResponse::CasAdded { hash, size_bytes } => {
|
||||
println!("{hash} {size_bytes} bytes");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue