Add overlay planning commands
This commit is contained in:
parent
7d4a288729
commit
c2dee50dae
18 changed files with 928 additions and 16 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
/target/
|
/target/
|
||||||
*.swp
|
*.swp
|
||||||
|
/references
|
||||||
|
|
|
||||||
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -1491,6 +1491,7 @@ dependencies = [
|
||||||
"geth-document",
|
"geth-document",
|
||||||
"geth-keychain",
|
"geth-keychain",
|
||||||
"geth-kv",
|
"geth-kv",
|
||||||
|
"geth-overlay",
|
||||||
"geth-pipe",
|
"geth-pipe",
|
||||||
"geth-pubsub",
|
"geth-pubsub",
|
||||||
"geth-resource",
|
"geth-resource",
|
||||||
|
|
@ -1614,6 +1615,7 @@ dependencies = [
|
||||||
"geth-iroh",
|
"geth-iroh",
|
||||||
"geth-keychain",
|
"geth-keychain",
|
||||||
"geth-kv",
|
"geth-kv",
|
||||||
|
"geth-overlay",
|
||||||
"geth-pipe",
|
"geth-pipe",
|
||||||
"geth-pubsub",
|
"geth-pubsub",
|
||||||
"geth-resource",
|
"geth-resource",
|
||||||
|
|
@ -1637,6 +1639,15 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "geth-overlay"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"geth-types",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "geth-pipe"
|
name = "geth-pipe"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ members = [
|
||||||
"crates/geth-pipe",
|
"crates/geth-pipe",
|
||||||
"crates/geth-document",
|
"crates/geth-document",
|
||||||
"crates/geth-pubsub",
|
"crates/geth-pubsub",
|
||||||
|
"crates/geth-overlay",
|
||||||
"crates/geth-cas",
|
"crates/geth-cas",
|
||||||
"crates/geth-ssh-proxy",
|
"crates/geth-ssh-proxy",
|
||||||
"crates/geth-testkit",
|
"crates/geth-testkit",
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -86,6 +86,9 @@ metadata from an authorized peer over Iroh.
|
||||||
|
|
||||||
The bootstrap implementation provides:
|
The bootstrap implementation provides:
|
||||||
|
|
||||||
|
- `geth guide [init|owner-setup|enrollment|keys|overlay|service|smoke-test]` for
|
||||||
|
embedded workflow help, including `--admin-key` / `--signing-key` setup
|
||||||
|
examples
|
||||||
- `geth init`
|
- `geth init`
|
||||||
- `geth init --admin-key <public-key> --signing-key <private-key> --node-name <name>`
|
- `geth init --admin-key <public-key> --signing-key <private-key> --node-name <name>`
|
||||||
- `geth daemon run`
|
- `geth daemon run`
|
||||||
|
|
@ -110,6 +113,14 @@ The bootstrap implementation provides:
|
||||||
- `geth peer list`
|
- `geth peer list`
|
||||||
- `geth peer ping <node-id>`
|
- `geth peer ping <node-id>`
|
||||||
- `geth peer auth-check <node-id> <resource> <capability>`
|
- `geth peer auth-check <node-id> <resource> <capability>`
|
||||||
|
- optional overlay-network planning:
|
||||||
|
`geth overlay status`,
|
||||||
|
`geth overlay plan <name> [--cidr 172.22.0.0/24]`,
|
||||||
|
`geth overlay join <name> --secret <resource-secret> [--cidr 172.22.0.0/24]`,
|
||||||
|
and `geth overlay leave <name>`. These commands expose the resource,
|
||||||
|
capability, and ALPN shape for a future Iroh-carried packet overlay inspired
|
||||||
|
by iroh-lan. The prototype does not create TUN/Wintun interfaces or route
|
||||||
|
packets yet.
|
||||||
- `geth resource list`
|
- `geth resource list`
|
||||||
- `geth resource create <kind> <name>`
|
- `geth resource create <kind> <name>`
|
||||||
- `geth keychain init [--admin-key <path>] [--signing-key <path>]`
|
- `geth keychain init [--admin-key <path>] [--signing-key <path>]`
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,239 @@
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||||
use geth_config::GethPaths;
|
use geth_config::GethPaths;
|
||||||
use geth_control::{ControlRequest, ControlResponse};
|
use geth_control::{ControlRequest, ControlResponse};
|
||||||
use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport};
|
use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::PathBuf;
|
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)]
|
#[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 {
|
pub struct Cli {
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true, help = "Print machine-readable JSON output")]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
#[arg(long, global = true)]
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
help = "Print newline-delimited JSON output for streaming commands"
|
||||||
|
)]
|
||||||
pub jsonl: bool,
|
pub jsonl: bool,
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Command,
|
pub command: Command,
|
||||||
|
|
@ -20,16 +241,43 @@ pub struct Cli {
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
|
Guide {
|
||||||
|
#[arg(value_enum)]
|
||||||
|
topic: Option<GuideTopic>,
|
||||||
|
},
|
||||||
|
#[command(long_about = INIT_LONG_ABOUT, after_long_help = INIT_AFTER_HELP)]
|
||||||
Init {
|
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>,
|
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>,
|
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,
|
owner: String,
|
||||||
#[arg(long, default_value = "local")]
|
#[arg(
|
||||||
|
long,
|
||||||
|
default_value = "local",
|
||||||
|
help = "Friendly node name recorded during owner init"
|
||||||
|
)]
|
||||||
node_name: String,
|
node_name: String,
|
||||||
#[arg(long = "capability")]
|
#[arg(
|
||||||
|
long = "capability",
|
||||||
|
value_name = "RESOURCE=CAPABILITY",
|
||||||
|
help = "Initial capability grant for this node; repeatable"
|
||||||
|
)]
|
||||||
capabilities: Vec<String>,
|
capabilities: Vec<String>,
|
||||||
},
|
},
|
||||||
Daemon {
|
Daemon {
|
||||||
|
|
@ -49,6 +297,10 @@ pub enum Command {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: PeerCommand,
|
command: PeerCommand,
|
||||||
},
|
},
|
||||||
|
Overlay {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: OverlayCommand,
|
||||||
|
},
|
||||||
Resource {
|
Resource {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: ResourceCommand,
|
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)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum DaemonCommand {
|
pub enum DaemonCommand {
|
||||||
Run,
|
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)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum ResourceCommand {
|
pub enum ResourceCommand {
|
||||||
List,
|
List,
|
||||||
|
|
@ -740,6 +1023,9 @@ pub async fn run() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let paths = GethPaths::resolve().context("resolve geth paths")?;
|
let paths = GethPaths::resolve().context("resolve geth paths")?;
|
||||||
match cli.command {
|
match cli.command {
|
||||||
|
Command::Guide { topic } => {
|
||||||
|
print_guide(topic, cli.json || cli.jsonl)?;
|
||||||
|
}
|
||||||
Command::Init {
|
Command::Init {
|
||||||
admin_key,
|
admin_key,
|
||||||
signing_key,
|
signing_key,
|
||||||
|
|
@ -832,6 +1118,31 @@ pub async fn run() -> Result<()> {
|
||||||
Ok(())
|
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> {
|
fn request_for_command(command: Command) -> Result<ControlRequest> {
|
||||||
Ok(match command {
|
Ok(match command {
|
||||||
Command::Status => ControlRequest::Status,
|
Command::Status => ControlRequest::Status,
|
||||||
|
|
@ -979,6 +1290,14 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
||||||
capability,
|
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::Resource {
|
||||||
command: ResourceCommand::List,
|
command: ResourceCommand::List,
|
||||||
} => ControlRequest::ResourceList,
|
} => 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
|
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 } => {
|
ControlResponse::CasAdded { hash, size_bytes } => {
|
||||||
println!("{hash} {size_bytes} bytes");
|
println!("{hash} {size_bytes} bytes");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ geth-discovery = { path = "../geth-discovery" }
|
||||||
geth-document = { path = "../geth-document" }
|
geth-document = { path = "../geth-document" }
|
||||||
geth-keychain = { path = "../geth-keychain" }
|
geth-keychain = { path = "../geth-keychain" }
|
||||||
geth-kv = { path = "../geth-kv" }
|
geth-kv = { path = "../geth-kv" }
|
||||||
|
geth-overlay = { path = "../geth-overlay" }
|
||||||
geth-pipe = { path = "../geth-pipe" }
|
geth-pipe = { path = "../geth-pipe" }
|
||||||
geth-pubsub = { path = "../geth-pubsub" }
|
geth-pubsub = { path = "../geth-pubsub" }
|
||||||
geth-resource = { path = "../geth-resource" }
|
geth-resource = { path = "../geth-resource" }
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use geth_discovery::{DiscoveredPeer, PeerCard};
|
||||||
use geth_document::{DocumentResource, DocumentState};
|
use geth_document::{DocumentResource, DocumentState};
|
||||||
use geth_keychain::{KeychainOp, KeychainOpSignature, NodeEnrollmentRequest, NodeRecord};
|
use geth_keychain::{KeychainOp, KeychainOpSignature, NodeEnrollmentRequest, NodeRecord};
|
||||||
use geth_kv::{KvEntry, KvResource, KvSyncEntry};
|
use geth_kv::{KvEntry, KvResource, KvSyncEntry};
|
||||||
|
use geth_overlay::{OverlayJoinPlan, OverlayNetworkStatus, OverlayPlan};
|
||||||
use geth_pipe::{PipeConnection, PipeListener, PipeMessage};
|
use geth_pipe::{PipeConnection, PipeListener, PipeMessage};
|
||||||
use geth_pubsub::PubsubMessage;
|
use geth_pubsub::PubsubMessage;
|
||||||
use geth_resource::ResourceDescriptor;
|
use geth_resource::ResourceDescriptor;
|
||||||
|
|
@ -43,6 +44,19 @@ pub enum ControlRequest {
|
||||||
kind: String,
|
kind: String,
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
|
OverlayStatus,
|
||||||
|
OverlayPlan {
|
||||||
|
name: String,
|
||||||
|
cidr: Option<String>,
|
||||||
|
},
|
||||||
|
OverlayJoin {
|
||||||
|
name: String,
|
||||||
|
secret: String,
|
||||||
|
cidr: Option<String>,
|
||||||
|
},
|
||||||
|
OverlayLeave {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
CasAdd {
|
CasAdd {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
|
|
@ -472,6 +486,21 @@ pub enum ControlResponse {
|
||||||
ResourceCreated {
|
ResourceCreated {
|
||||||
resource: ResourceDescriptor,
|
resource: ResourceDescriptor,
|
||||||
},
|
},
|
||||||
|
OverlayStatus {
|
||||||
|
networks: Vec<OverlayNetworkStatus>,
|
||||||
|
note: String,
|
||||||
|
},
|
||||||
|
OverlayPlanned {
|
||||||
|
plan: OverlayPlan,
|
||||||
|
},
|
||||||
|
OverlayJoined {
|
||||||
|
join: OverlayJoinPlan,
|
||||||
|
},
|
||||||
|
OverlayLeft {
|
||||||
|
name: String,
|
||||||
|
stopped: bool,
|
||||||
|
note: String,
|
||||||
|
},
|
||||||
CasAdded {
|
CasAdded {
|
||||||
hash: BlobHash,
|
hash: BlobHash,
|
||||||
size_bytes: u64,
|
size_bytes: u64,
|
||||||
|
|
@ -1549,6 +1578,15 @@ mod tests {
|
||||||
request
|
request
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let request = ControlRequest::OverlayPlan {
|
||||||
|
name: "home-lan".to_owned(),
|
||||||
|
cidr: Some("172.22.0.0/24".to_owned()),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
|
||||||
|
request
|
||||||
|
);
|
||||||
|
|
||||||
let response = ControlResponse::CasHas {
|
let response = ControlResponse::CasHas {
|
||||||
hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
|
hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
|
||||||
present: true,
|
present: true,
|
||||||
|
|
@ -1558,6 +1596,15 @@ mod tests {
|
||||||
response
|
response
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let response = ControlResponse::OverlayPlanned {
|
||||||
|
plan: geth_overlay::plan_overlay("home-lan", None, geth_overlay::OVERLAY_ALPN)
|
||||||
|
.expect("overlay plan"),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
|
||||||
|
response
|
||||||
|
);
|
||||||
|
|
||||||
let response = ControlResponse::NodeList {
|
let response = ControlResponse::NodeList {
|
||||||
nodes: vec![NodeRecord {
|
nodes: vec![NodeRecord {
|
||||||
id: geth_types::NodeId::new("node:local"),
|
id: geth_types::NodeId::new("node:local"),
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,18 @@ impl AgentKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(path: &Path) -> Result<Self, CryptoError> {
|
pub fn load(path: &Path) -> Result<Self, CryptoError> {
|
||||||
let hex_key = std::fs::read_to_string(path)?;
|
let key_file = std::fs::read(path)?;
|
||||||
let bytes = hex::decode(hex_key.trim())?;
|
let bytes = if let Ok(hex_key) = std::str::from_utf8(&key_file) {
|
||||||
let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| CryptoError::InvalidKey)?;
|
let trimmed = hex_key.trim();
|
||||||
|
if trimmed.len() == 64 && trimmed.bytes().all(|byte| byte.is_ascii_hexdigit()) {
|
||||||
|
hex::decode(trimmed)?
|
||||||
|
} else {
|
||||||
|
key_file
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
key_file
|
||||||
|
};
|
||||||
|
let key_bytes = signing_key_bytes_from_file(bytes)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
signing_key: SigningKey::from_bytes(&key_bytes),
|
signing_key: SigningKey::from_bytes(&key_bytes),
|
||||||
})
|
})
|
||||||
|
|
@ -86,6 +95,14 @@ impl AgentKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn signing_key_bytes_from_file(bytes: Vec<u8>) -> Result<[u8; 32], CryptoError> {
|
||||||
|
match bytes.len() {
|
||||||
|
32 => bytes.try_into().map_err(|_| CryptoError::InvalidKey),
|
||||||
|
64 => bytes[..32].try_into().map_err(|_| CryptoError::InvalidKey),
|
||||||
|
_ => Err(CryptoError::InvalidKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn verify(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), CryptoError> {
|
pub fn verify(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), CryptoError> {
|
||||||
let key_bytes: [u8; 32] = public_key.try_into().map_err(|_| CryptoError::InvalidKey)?;
|
let key_bytes: [u8; 32] = public_key.try_into().map_err(|_| CryptoError::InvalidKey)?;
|
||||||
let verifying_key =
|
let verifying_key =
|
||||||
|
|
@ -132,6 +149,35 @@ mod tests {
|
||||||
assert_eq!(first.agent_id(), second.agent_id());
|
assert_eq!(first.agent_id(), second.agent_id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_identity_loads_raw_32_byte_key_material() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let path = dir.path().join("agent.ed25519");
|
||||||
|
std::fs::write(&path, [0_u8; 32]).expect("write raw key");
|
||||||
|
|
||||||
|
let loaded = AgentKey::load(&path).expect("load raw key");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
loaded.signing_key.to_bytes(),
|
||||||
|
[0_u8; 32],
|
||||||
|
"raw binary keys can contain bytes that are valid UTF-8 but not hex text"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_identity_loads_raw_64_byte_keypair_material() {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let path = dir.path().join("agent.ed25519");
|
||||||
|
let key = AgentKey::generate();
|
||||||
|
let mut keypair = key.signing_key.to_bytes().to_vec();
|
||||||
|
keypair.extend_from_slice(&key.verifying_key().to_bytes());
|
||||||
|
std::fs::write(&path, keypair).expect("write raw keypair");
|
||||||
|
|
||||||
|
let loaded = AgentKey::load(&path).expect("load raw keypair");
|
||||||
|
|
||||||
|
assert_eq!(key.agent_id(), loaded.agent_id());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blake3_helper_matches_known_hash() {
|
fn blake3_helper_matches_known_hash() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ pub const ALPN_PIPE: &[u8] = b"/geth/pipe/1";
|
||||||
pub const ALPN_DB: &[u8] = b"/geth/db/1";
|
pub const ALPN_DB: &[u8] = b"/geth/db/1";
|
||||||
pub const ALPN_DOCUMENT: &[u8] = b"/geth/document/1";
|
pub const ALPN_DOCUMENT: &[u8] = b"/geth/document/1";
|
||||||
pub const ALPN_SSH_PROXY: &[u8] = b"/geth/ssh-proxy/1";
|
pub const ALPN_SSH_PROXY: &[u8] = b"/geth/ssh-proxy/1";
|
||||||
|
pub const ALPN_OVERLAY: &[u8] = b"/geth/overlay/1";
|
||||||
|
|
||||||
pub const IROH_VERSION: &str = "0.95.1";
|
pub const IROH_VERSION: &str = "0.95.1";
|
||||||
pub const IROH_BLOBS_VERSION: &str = "0.97.0";
|
pub const IROH_BLOBS_VERSION: &str = "0.97.0";
|
||||||
|
|
@ -60,6 +61,7 @@ pub enum ProtocolKind {
|
||||||
Db,
|
Db,
|
||||||
Document,
|
Document,
|
||||||
SshProxy,
|
SshProxy,
|
||||||
|
Overlay,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProtocolKind {
|
impl ProtocolKind {
|
||||||
|
|
@ -74,6 +76,7 @@ impl ProtocolKind {
|
||||||
Self::Db => "db",
|
Self::Db => "db",
|
||||||
Self::Document => "document",
|
Self::Document => "document",
|
||||||
Self::SshProxy => "ssh-proxy",
|
Self::SshProxy => "ssh-proxy",
|
||||||
|
Self::Overlay => "overlay",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -158,6 +161,7 @@ pub fn default_protocol_descriptors() -> Vec<ProtocolDescriptor> {
|
||||||
ProtocolDescriptor::new(ProtocolKind::Db, ALPN_DB),
|
ProtocolDescriptor::new(ProtocolKind::Db, ALPN_DB),
|
||||||
ProtocolDescriptor::new(ProtocolKind::Document, ALPN_DOCUMENT),
|
ProtocolDescriptor::new(ProtocolKind::Document, ALPN_DOCUMENT),
|
||||||
ProtocolDescriptor::new(ProtocolKind::SshProxy, ALPN_SSH_PROXY),
|
ProtocolDescriptor::new(ProtocolKind::SshProxy, ALPN_SSH_PROXY),
|
||||||
|
ProtocolDescriptor::new(ProtocolKind::Overlay, ALPN_OVERLAY),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,7 +435,8 @@ mod tests {
|
||||||
assert!(alpns.contains(&ALPN_CONTROL.to_vec()));
|
assert!(alpns.contains(&ALPN_CONTROL.to_vec()));
|
||||||
assert!(alpns.contains(&ALPN_CAS.to_vec()));
|
assert!(alpns.contains(&ALPN_CAS.to_vec()));
|
||||||
assert!(alpns.contains(&ALPN_SSH_PROXY.to_vec()));
|
assert!(alpns.contains(&ALPN_SSH_PROXY.to_vec()));
|
||||||
assert_eq!(alpns.len(), 8);
|
assert!(alpns.contains(&ALPN_OVERLAY.to_vec()));
|
||||||
|
assert_eq!(alpns.len(), 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -478,7 +483,11 @@ mod tests {
|
||||||
router.require(ALPN_SSH_PROXY).expect("ssh proxy").kind,
|
router.require(ALPN_SSH_PROXY).expect("ssh proxy").kind,
|
||||||
ProtocolKind::SshProxy
|
ProtocolKind::SshProxy
|
||||||
);
|
);
|
||||||
assert_eq!(router.descriptors().len(), 8);
|
assert_eq!(
|
||||||
|
router.require(ALPN_OVERLAY).expect("overlay").kind,
|
||||||
|
ProtocolKind::Overlay
|
||||||
|
);
|
||||||
|
assert_eq!(router.descriptors().len(), 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ geth-document = { path = "../geth-document" }
|
||||||
geth-iroh = { path = "../geth-iroh" }
|
geth-iroh = { path = "../geth-iroh" }
|
||||||
geth-keychain = { path = "../geth-keychain" }
|
geth-keychain = { path = "../geth-keychain" }
|
||||||
geth-kv = { path = "../geth-kv" }
|
geth-kv = { path = "../geth-kv" }
|
||||||
|
geth-overlay = { path = "../geth-overlay" }
|
||||||
geth-pipe = { path = "../geth-pipe" }
|
geth-pipe = { path = "../geth-pipe" }
|
||||||
geth-pubsub = { path = "../geth-pubsub" }
|
geth-pubsub = { path = "../geth-pubsub" }
|
||||||
geth-resource = { path = "../geth-resource" }
|
geth-resource = { path = "../geth-resource" }
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ pub enum NodeError {
|
||||||
Cas(#[from] geth_cas::CasError),
|
Cas(#[from] geth_cas::CasError),
|
||||||
#[error("db error: {0}")]
|
#[error("db error: {0}")]
|
||||||
Db(#[from] geth_db::DbError),
|
Db(#[from] geth_db::DbError),
|
||||||
|
#[error("overlay error: {0}")]
|
||||||
|
Overlay(#[from] geth_overlay::OverlayError),
|
||||||
#[error("control error: {0}")]
|
#[error("control error: {0}")]
|
||||||
Control(#[from] geth_control::ControlError),
|
Control(#[from] geth_control::ControlError),
|
||||||
#[error("codec error: {0}")]
|
#[error("codec error: {0}")]
|
||||||
|
|
@ -6591,6 +6593,41 @@ pub fn handle_request(
|
||||||
resource: stored_resource_to_descriptor(stored)?,
|
resource: stored_resource_to_descriptor(stored)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
ControlRequest::OverlayStatus => Ok(ControlResponse::OverlayStatus {
|
||||||
|
networks: Vec::new(),
|
||||||
|
note: geth_overlay::overlay_status_note().to_owned(),
|
||||||
|
}),
|
||||||
|
ControlRequest::OverlayPlan { name, cidr } => {
|
||||||
|
let plan = geth_overlay::plan_overlay(
|
||||||
|
&name,
|
||||||
|
cidr.as_deref(),
|
||||||
|
&String::from_utf8_lossy(geth_iroh::ALPN_OVERLAY),
|
||||||
|
)?;
|
||||||
|
Ok(ControlResponse::OverlayPlanned { plan })
|
||||||
|
}
|
||||||
|
ControlRequest::OverlayJoin { name, secret, cidr } => {
|
||||||
|
geth_overlay::validate_overlay_secret(&secret)?;
|
||||||
|
let plan = geth_overlay::plan_overlay(
|
||||||
|
&name,
|
||||||
|
cidr.as_deref(),
|
||||||
|
&String::from_utf8_lossy(geth_iroh::ALPN_OVERLAY),
|
||||||
|
)?;
|
||||||
|
Ok(ControlResponse::OverlayJoined {
|
||||||
|
join: geth_overlay::OverlayJoinPlan {
|
||||||
|
plan,
|
||||||
|
enabled: false,
|
||||||
|
note: "overlay join is recorded as an implementation plan only; this prototype does not create a TUN/Wintun interface or route packets".to_owned(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ControlRequest::OverlayLeave { name } => {
|
||||||
|
geth_overlay::validate_overlay_name(&name)?;
|
||||||
|
Ok(ControlResponse::OverlayLeft {
|
||||||
|
name,
|
||||||
|
stopped: false,
|
||||||
|
note: "no overlay packet runtime is active in this prototype".to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
ControlRequest::CasAdd { path } => {
|
ControlRequest::CasAdd { path } => {
|
||||||
let cas = LocalCas::new(node.paths.cas_dir());
|
let cas = LocalCas::new(node.paths.cas_dir());
|
||||||
let info = cas.add_path(&path)?;
|
let info = cas.add_path(&path)?;
|
||||||
|
|
|
||||||
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -53,6 +53,7 @@ string_id!(DocumentId);
|
||||||
string_id!(KvId);
|
string_id!(KvId);
|
||||||
string_id!(DbId);
|
string_id!(DbId);
|
||||||
string_id!(PipeId);
|
string_id!(PipeId);
|
||||||
|
string_id!(OverlayId);
|
||||||
string_id!(TopicId);
|
string_id!(TopicId);
|
||||||
string_id!(AuthOpId);
|
string_id!(AuthOpId);
|
||||||
string_id!(KeyId);
|
string_id!(KeyId);
|
||||||
|
|
@ -74,6 +75,7 @@ pub enum ResourceKind {
|
||||||
Pubsub,
|
Pubsub,
|
||||||
Cas,
|
Cas,
|
||||||
SshProxy,
|
SshProxy,
|
||||||
|
Overlay,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceKind {
|
impl ResourceKind {
|
||||||
|
|
@ -87,6 +89,7 @@ impl ResourceKind {
|
||||||
Self::Pubsub => "pubsub",
|
Self::Pubsub => "pubsub",
|
||||||
Self::Cas => "cas",
|
Self::Cas => "cas",
|
||||||
Self::SshProxy => "ssh-proxy",
|
Self::SshProxy => "ssh-proxy",
|
||||||
|
Self::Overlay => "overlay",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +112,7 @@ impl std::str::FromStr for ResourceKind {
|
||||||
"pubsub" => Ok(Self::Pubsub),
|
"pubsub" => Ok(Self::Pubsub),
|
||||||
"cas" => Ok(Self::Cas),
|
"cas" => Ok(Self::Cas),
|
||||||
"ssh-proxy" | "ssh" => Ok(Self::SshProxy),
|
"ssh-proxy" | "ssh" => Ok(Self::SshProxy),
|
||||||
|
"overlay" => Ok(Self::Overlay),
|
||||||
_ => Err(TypeParseError::UnknownResourceKind(value.to_owned())),
|
_ => Err(TypeParseError::UnknownResourceKind(value.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,55 @@ fn geth_init_in_temp_home() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_help_documents_owner_init_keys() {
|
||||||
|
let home = tempfile::tempdir().expect("tempdir");
|
||||||
|
let output = run_geth(home.path(), &["init", "--help"]);
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"stderr: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert!(stdout.contains("--admin-key <OPENSSH_PUBLIC_KEY>"));
|
||||||
|
assert!(stdout.contains("--signing-key <OPENSSH_PRIVATE_KEY>"));
|
||||||
|
assert!(stdout.contains("OpenSSH public key"));
|
||||||
|
assert!(stdout.contains("YubiKey"));
|
||||||
|
assert!(stdout.contains("both `--admin-key` and `--signing-key` are"));
|
||||||
|
assert!(stdout.contains("geth guide owner-setup"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guide_command_explains_key_roles() {
|
||||||
|
let home = tempfile::tempdir().expect("tempdir");
|
||||||
|
let output = run_geth(home.path(), &["guide", "keys"]);
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"stderr: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert!(stdout.contains("--admin-key"));
|
||||||
|
assert!(stdout.contains("trust anchor"));
|
||||||
|
assert!(stdout.contains("--signing-key"));
|
||||||
|
assert!(stdout.contains("ssh-keygen -Y sign"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guide_command_explains_overlay_boundaries() {
|
||||||
|
let home = tempfile::tempdir().expect("tempdir");
|
||||||
|
let output = run_geth(home.path(), &["guide", "overlay"]);
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"stderr: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert!(stdout.contains("geth overlay plan home"));
|
||||||
|
assert!(stdout.contains("TUN/Wintun"));
|
||||||
|
assert!(stdout.contains("all overlay packets must be carried over Iroh"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn owner_init_requires_admin_key_and_signing_key() {
|
fn owner_init_requires_admin_key_and_signing_key() {
|
||||||
let home = tempfile::tempdir().expect("tempdir");
|
let home = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
@ -1378,6 +1427,31 @@ fn initialized_node_can_roundtrip_cas_blob() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overlay_plan_is_available_without_starting_packet_runtime() {
|
||||||
|
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 response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::OverlayPlan {
|
||||||
|
name: "home-lan".to_owned(),
|
||||||
|
cidr: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("overlay plan");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::OverlayPlanned { plan } => {
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn private_cas_blob_uses_resource_secret_epoch() {
|
fn private_cas_blob_uses_resource_secret_epoch() {
|
||||||
let home = tempfile::tempdir().expect("tempdir");
|
let home = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
|
||||||
66
docs/adr/0017-optional-iroh-overlay-network.md
Normal file
66
docs/adr/0017-optional-iroh-overlay-network.md
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# ADR 0017: Optional Iroh Overlay Network
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted for prototype scaffolding.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`iroh-lan` demonstrates a useful pattern for a private packet overlay carried by
|
||||||
|
Iroh connections. That model is attractive for geth because it could make local
|
||||||
|
services, scripts, and devices reachable through stable mesh resources without
|
||||||
|
exposing host networking directly to untrusted discovery.
|
||||||
|
|
||||||
|
Geth must keep its core invariants:
|
||||||
|
|
||||||
|
- all remote geth communication is over Iroh
|
||||||
|
- the daemon owns the Iroh endpoint
|
||||||
|
- discovery is untrusted
|
||||||
|
- resource authorization gates access
|
||||||
|
- SSH is not a geth transport
|
||||||
|
|
||||||
|
Packet overlays also carry host-network risk. TUN/Wintun setup is
|
||||||
|
platform-specific, may require privileges, and can route arbitrary traffic if
|
||||||
|
misconfigured.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Add an optional `overlay` resource kind and reserve `/geth/overlay/1` for a
|
||||||
|
future Iroh-carried packet overlay. The initial implementation is a typed model
|
||||||
|
and CLI/control planning surface:
|
||||||
|
|
||||||
|
- `geth overlay status`
|
||||||
|
- `geth overlay plan <name> [--cidr <cidr>]`
|
||||||
|
- `geth overlay join <name> --secret <resource-secret> [--cidr <cidr>]`
|
||||||
|
- `geth overlay leave <name>`
|
||||||
|
|
||||||
|
The prototype does not create TUN/Wintun interfaces, assign virtual IPs, or
|
||||||
|
route packets. Join/leave are explicit stubs that describe the intended
|
||||||
|
resource, capabilities, ALPN, and security boundaries.
|
||||||
|
|
||||||
|
Overlay resources use capabilities:
|
||||||
|
|
||||||
|
- `overlay.join`
|
||||||
|
- `overlay.route`
|
||||||
|
- `overlay.admin`
|
||||||
|
|
||||||
|
Future overlay implementation must use the shared daemon-owned Iroh endpoint and
|
||||||
|
must not create a second endpoint or non-Iroh transport.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
The CLI can now document and test the intended overlay shape without changing
|
||||||
|
host networking. This keeps the prototype safe to run on development machines
|
||||||
|
while preserving a clear path toward an iroh-lan-inspired overlay.
|
||||||
|
|
||||||
|
Future work must add:
|
||||||
|
|
||||||
|
- persisted overlay membership/resource metadata
|
||||||
|
- resource-secret-backed join authorization
|
||||||
|
- platform-specific opt-in TUN/Wintun management
|
||||||
|
- packet routing over `/geth/overlay/1`
|
||||||
|
- peer/IP coordination through trusted resource metadata and untrusted discovery
|
||||||
|
candidates
|
||||||
|
|
||||||
|
No current code claims VPN-grade isolation, forward secrecy, or automatic host
|
||||||
|
network security.
|
||||||
|
|
@ -16,7 +16,8 @@ as a privileged system service.
|
||||||
|
|
||||||
Remote geth node-to-node communication is Iroh-only. The daemon will own one
|
Remote geth node-to-node communication is Iroh-only. The daemon will own one
|
||||||
shared Iroh endpoint and register module protocols on ALPNs such as
|
shared Iroh endpoint and register module protocols on ALPNs such as
|
||||||
`/geth/cas/1`, `/geth/kv/1`, `/geth/pipe/1`, and `/geth/ssh-proxy/1`.
|
`/geth/cas/1`, `/geth/kv/1`, `/geth/pipe/1`, `/geth/ssh-proxy/1`, and
|
||||||
|
`/geth/overlay/1`.
|
||||||
|
|
||||||
The pinned Iroh integration uses `iroh = 0.95.1`. `geth-iroh` wraps
|
The pinned Iroh integration uses `iroh = 0.95.1`. `geth-iroh` wraps
|
||||||
`iroh::Endpoint::builder()`, configures geth ALPNs with `Builder::alpns`, uses
|
`iroh::Endpoint::builder()`, configures geth ALPNs with `Builder::alpns`, uses
|
||||||
|
|
@ -150,6 +151,7 @@ Resource kinds:
|
||||||
- `pubsub`: lossy notifications and presence
|
- `pubsub`: lossy notifications and presence
|
||||||
- `cas`: content-addressed blobs
|
- `cas`: content-addressed blobs
|
||||||
- `ssh-proxy`: SSH/admin proxying over Iroh
|
- `ssh-proxy`: SSH/admin proxying over Iroh
|
||||||
|
- `overlay`: optional Iroh-carried packet overlay planning
|
||||||
|
|
||||||
## Module Overview
|
## Module Overview
|
||||||
|
|
||||||
|
|
@ -244,6 +246,20 @@ message and broadcasting it through a deterministic native `iroh-gossip` topic.
|
||||||
the gossip topic when the caller has `pubsub.subscribe`, and returns the peer's
|
the gossip topic when the caller has `pubsub.subscribe`, and returns the peer's
|
||||||
current daemon-lifetime snapshot. Private topics remain future work.
|
current daemon-lifetime snapshot. Private topics remain future work.
|
||||||
|
|
||||||
|
`geth-overlay` defines an optional packet-overlay plan inspired by `iroh-lan`.
|
||||||
|
The target runtime is a private L3-style overlay where packets from an explicit
|
||||||
|
TUN/Wintun interface are carried over the daemon-owned Iroh endpoint on
|
||||||
|
`/geth/overlay/1`. The overlay is a geth resource (`resource:overlay:<name>`)
|
||||||
|
with `overlay.join`, `overlay.route`, and `overlay.admin` capabilities. The
|
||||||
|
prototype exposes `geth overlay status`, `geth overlay plan <name>`,
|
||||||
|
`geth overlay join <name> --secret <resource-secret>`, and `geth overlay leave
|
||||||
|
<name>` as planning/control stubs only. They validate names, CIDRs, resource
|
||||||
|
IDs, capabilities, and security notes, but they do not create host network
|
||||||
|
interfaces, assign virtual IPs, or route packets yet. Future implementation must
|
||||||
|
remain explicitly opt-in because TUN/Wintun setup may need platform-specific
|
||||||
|
privileges. Overlay discovery can use mDNS, peer exchange, and resource
|
||||||
|
metadata, but discovery remains untrusted and cannot grant overlay access.
|
||||||
|
|
||||||
`geth-pipe` currently supports `pipe listen/connect/send/recv` against a
|
`geth-pipe` currently supports `pipe listen/connect/send/recv` against a
|
||||||
daemon-lifetime runtime. `geth pipe connect <name> --node <node-id>` sends an
|
daemon-lifetime runtime. `geth pipe connect <name> --node <node-id>` sends an
|
||||||
authorized remote connect request over the protected Iroh control ALPN. The
|
authorized remote connect request over the protected Iroh control ALPN. The
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,9 @@ Implementation order:
|
||||||
- `[x]` CLI errors for stale peer cards, missing endpoint bindings, missing
|
- `[x]` CLI errors for stale peer cards, missing endpoint bindings, missing
|
||||||
grants, unavailable relays, unavailable service managers, and unsupported
|
grants, unavailable relays, unavailable service managers, and unsupported
|
||||||
platform features tell the operator what command to run next.
|
platform features tell the operator what command to run next.
|
||||||
|
- `[x]` `geth init --help` and `geth guide <topic>` explain owner setup,
|
||||||
|
enrollment, key roles, service installation, and smoke-test workflows from
|
||||||
|
the binary itself.
|
||||||
- `[x]` `geth sync status --json` is sufficient for scripts to detect stale
|
- `[x]` `geth sync status --json` is sufficient for scripts to detect stale
|
||||||
peers and failed streams.
|
peers and failed streams.
|
||||||
|
|
||||||
|
|
@ -572,6 +575,29 @@ Goal: add authorized stream-oriented management workflows over Iroh.
|
||||||
request/response forwarding exchange when local Iroh endpoint binding is
|
request/response forwarding exchange when local Iroh endpoint binding is
|
||||||
available in the test environment.
|
available in the test environment.
|
||||||
|
|
||||||
|
- `[~]` Optional Iroh overlay network.
|
||||||
|
Acceptance criteria:
|
||||||
|
- `[x]` Add a focused `geth-overlay` crate for overlay names, CIDRs,
|
||||||
|
resource IDs, capabilities, status, and plan models.
|
||||||
|
- `[x]` Reserve `/geth/overlay/1` in the daemon-owned Iroh protocol router.
|
||||||
|
- `[x]` Add `overlay` as a resource kind and document capabilities:
|
||||||
|
`overlay.join`, `overlay.route`, and `overlay.admin`.
|
||||||
|
- `[x]` Add CLI/control commands for `geth overlay status`, `plan`, `join`,
|
||||||
|
and `leave`.
|
||||||
|
- `[x]` Prototype commands make clear that no TUN/Wintun interface is created
|
||||||
|
and no packets are routed yet.
|
||||||
|
- `[x]` Tests cover overlay validation and control serialization.
|
||||||
|
- `[ ]` Persist overlay network configuration and membership state as
|
||||||
|
resource metadata.
|
||||||
|
- `[ ]` Implement resource-authorized overlay join using resource secrets
|
||||||
|
without granting node identity.
|
||||||
|
- `[ ]` Add platform-specific, opt-in TUN/Wintun interface management with
|
||||||
|
generated-definition tests and no privileged test requirements.
|
||||||
|
- `[ ]` Route IPv4 packets over `/geth/overlay/1` using the shared daemon
|
||||||
|
Iroh endpoint.
|
||||||
|
- `[ ]` Add live peer/IP coordination over trusted resource metadata and
|
||||||
|
untrusted discovery candidates.
|
||||||
|
|
||||||
- `[~]` Unix socket forwarding where supported.
|
- `[~]` Unix socket forwarding where supported.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `[x]` Unix socket forwarding is available on Unix platforms through
|
- `[x]` Unix socket forwarding is available on Unix platforms through
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue