Add Iroh protocol router scaffold
This commit is contained in:
parent
0f2ad755d9
commit
cf5a405276
4 changed files with 170 additions and 16 deletions
|
|
@ -104,7 +104,7 @@ Roadmap items should be actionable and checkable:
|
||||||
- Local daemon, local control socket, local identity, local store, local CAS,
|
- Local daemon, local control socket, local identity, local store, local CAS,
|
||||||
daemon-owned Iroh endpoint startup, built-in relay-mode config, SSH
|
daemon-owned Iroh endpoint startup, built-in relay-mode config, SSH
|
||||||
certificate metadata, revocation metadata, user service definitions, and a
|
certificate metadata, revocation metadata, user service definitions, and a
|
||||||
pinned `geth-iroh` endpoint wrapper exist.
|
pinned `geth-iroh` endpoint wrapper with protocol-router scaffold exist.
|
||||||
- Custom relay maps, mDNS discovery, peer auth over Iroh, cr-sqlite, iroh-docs,
|
- Custom relay maps, mDNS discovery, peer auth over Iroh, cr-sqlite, iroh-docs,
|
||||||
iroh-gossip, iroh-blobs, Automerge sync, real auth enforcement, OpenSSH KRL
|
iroh-gossip, iroh-blobs, Automerge sync, real auth enforcement, OpenSSH KRL
|
||||||
generation, and Keyhive/BeeKEM-style authorization are future roadmap items
|
generation, and Keyhive/BeeKEM-style authorization are future roadmap items
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::net::{SocketAddrV4, SocketAddrV6};
|
use std::net::{SocketAddrV4, SocketAddrV6};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
|
@ -36,11 +37,123 @@ impl GethIrohConfig {
|
||||||
relay_mode,
|
relay_mode,
|
||||||
bind_ipv4: None,
|
bind_ipv4: None,
|
||||||
bind_ipv6: None,
|
bind_ipv6: None,
|
||||||
alpns: all_alpns(),
|
alpns: default_protocol_router().alpns(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum ProtocolKind {
|
||||||
|
Control,
|
||||||
|
Kv,
|
||||||
|
Cas,
|
||||||
|
Pubsub,
|
||||||
|
Pipe,
|
||||||
|
Db,
|
||||||
|
Document,
|
||||||
|
SshProxy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolKind {
|
||||||
|
#[must_use]
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Control => "control",
|
||||||
|
Self::Kv => "kv",
|
||||||
|
Self::Cas => "cas",
|
||||||
|
Self::Pubsub => "pubsub",
|
||||||
|
Self::Pipe => "pipe",
|
||||||
|
Self::Db => "db",
|
||||||
|
Self::Document => "document",
|
||||||
|
Self::SshProxy => "ssh-proxy",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ProtocolDescriptor {
|
||||||
|
pub kind: ProtocolKind,
|
||||||
|
pub name: String,
|
||||||
|
pub alpn: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolDescriptor {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(kind: ProtocolKind, alpn: &'static [u8]) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
name: kind.name().to_owned(),
|
||||||
|
alpn: alpn.to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct ProtocolRouter {
|
||||||
|
protocols: BTreeMap<Vec<u8>, ProtocolDescriptor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolRouter {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(&mut self, descriptor: ProtocolDescriptor) -> Result<(), RouterError> {
|
||||||
|
if self.protocols.contains_key(&descriptor.alpn) {
|
||||||
|
return Err(RouterError::DuplicateAlpn {
|
||||||
|
alpn: display_alpn(&descriptor.alpn),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.protocols.insert(descriptor.alpn.clone(), descriptor);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn require(&self, alpn: &[u8]) -> Result<&ProtocolDescriptor, RouterError> {
|
||||||
|
self.protocols
|
||||||
|
.get(alpn)
|
||||||
|
.ok_or_else(|| RouterError::UnknownAlpn {
|
||||||
|
alpn: display_alpn(alpn),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn alpns(&self) -> Vec<Vec<u8>> {
|
||||||
|
self.protocols.keys().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn descriptors(&self) -> Vec<ProtocolDescriptor> {
|
||||||
|
self.protocols.values().cloned().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn default_protocol_router() -> ProtocolRouter {
|
||||||
|
let mut router = ProtocolRouter::new();
|
||||||
|
for descriptor in default_protocol_descriptors() {
|
||||||
|
router
|
||||||
|
.register(descriptor)
|
||||||
|
.expect("default geth ALPNs are unique");
|
||||||
|
}
|
||||||
|
router
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn default_protocol_descriptors() -> Vec<ProtocolDescriptor> {
|
||||||
|
vec![
|
||||||
|
ProtocolDescriptor::new(ProtocolKind::Control, ALPN_CONTROL),
|
||||||
|
ProtocolDescriptor::new(ProtocolKind::Kv, ALPN_KV),
|
||||||
|
ProtocolDescriptor::new(ProtocolKind::Cas, ALPN_CAS),
|
||||||
|
ProtocolDescriptor::new(ProtocolKind::Pubsub, ALPN_PUBSUB),
|
||||||
|
ProtocolDescriptor::new(ProtocolKind::Pipe, ALPN_PIPE),
|
||||||
|
ProtocolDescriptor::new(ProtocolKind::Db, ALPN_DB),
|
||||||
|
ProtocolDescriptor::new(ProtocolKind::Document, ALPN_DOCUMENT),
|
||||||
|
ProtocolDescriptor::new(ProtocolKind::SshProxy, ALPN_SSH_PROXY),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum GethRelayMode {
|
pub enum GethRelayMode {
|
||||||
|
|
@ -142,19 +255,7 @@ pub fn load_secret_key(path: &Path) -> Result<iroh::SecretKey, IrohError> {
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn all_alpns() -> Vec<Vec<u8>> {
|
pub fn all_alpns() -> Vec<Vec<u8>> {
|
||||||
[
|
default_protocol_router().alpns()
|
||||||
ALPN_CONTROL,
|
|
||||||
ALPN_KV,
|
|
||||||
ALPN_CAS,
|
|
||||||
ALPN_PUBSUB,
|
|
||||||
ALPN_PIPE,
|
|
||||||
ALPN_DB,
|
|
||||||
ALPN_DOCUMENT,
|
|
||||||
ALPN_SSH_PROXY,
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.map(<[u8]>::to_vec)
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -189,12 +290,24 @@ pub enum IrohError {
|
||||||
Bind(Box<iroh::endpoint::BindError>),
|
Bind(Box<iroh::endpoint::BindError>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum RouterError {
|
||||||
|
#[error("duplicate geth ALPN registration: {alpn}")]
|
||||||
|
DuplicateAlpn { alpn: String },
|
||||||
|
#[error("unknown geth ALPN: {alpn}")]
|
||||||
|
UnknownAlpn { alpn: String },
|
||||||
|
}
|
||||||
|
|
||||||
impl From<iroh::endpoint::BindError> for IrohError {
|
impl From<iroh::endpoint::BindError> for IrohError {
|
||||||
fn from(error: iroh::endpoint::BindError) -> Self {
|
fn from(error: iroh::endpoint::BindError) -> Self {
|
||||||
Self::Bind(Box::new(error))
|
Self::Bind(Box::new(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_alpn(alpn: &[u8]) -> String {
|
||||||
|
String::from_utf8(alpn.to_vec()).unwrap_or_else(|_| format!("0x{}", hex::encode(alpn)))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -208,6 +321,41 @@ mod tests {
|
||||||
assert_eq!(alpns.len(), 8);
|
assert_eq!(alpns.len(), 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_router_registers_all_protocols() {
|
||||||
|
let router = default_protocol_router();
|
||||||
|
assert_eq!(
|
||||||
|
router.require(ALPN_CONTROL).expect("control").kind,
|
||||||
|
ProtocolKind::Control
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
router.require(ALPN_SSH_PROXY).expect("ssh proxy").kind,
|
||||||
|
ProtocolKind::SshProxy
|
||||||
|
);
|
||||||
|
assert_eq!(router.descriptors().len(), 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn router_rejects_duplicate_alpns() {
|
||||||
|
let mut router = ProtocolRouter::new();
|
||||||
|
router
|
||||||
|
.register(ProtocolDescriptor::new(ProtocolKind::Control, ALPN_CONTROL))
|
||||||
|
.expect("first registration");
|
||||||
|
let error = router
|
||||||
|
.register(ProtocolDescriptor::new(ProtocolKind::Cas, ALPN_CONTROL))
|
||||||
|
.expect_err("duplicate error");
|
||||||
|
assert!(matches!(error, RouterError::DuplicateAlpn { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn router_rejects_unknown_alpn() {
|
||||||
|
let router = default_protocol_router();
|
||||||
|
let error = router
|
||||||
|
.require(b"/geth/unknown/1")
|
||||||
|
.expect_err("unknown error");
|
||||||
|
assert!(matches!(error, RouterError::UnknownAlpn { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iroh_secret_key_persists() {
|
fn iroh_secret_key_persists() {
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,12 @@ material, and shuts down through `Endpoint::close().await`. The default config
|
||||||
uses Iroh's default relay policy; local-only/offline development can set
|
uses Iroh's default relay policy; local-only/offline development can set
|
||||||
`[iroh].relay_mode = "disabled"`.
|
`[iroh].relay_mode = "disabled"`.
|
||||||
|
|
||||||
|
Module ALPNs are registered through `geth-iroh`'s protocol router scaffold. The
|
||||||
|
router owns the default protocol descriptors, rejects duplicate ALPN
|
||||||
|
registrations, and returns explicit unknown-ALPN errors. It does not yet accept
|
||||||
|
or dispatch remote streams; peer authentication and module handlers are later
|
||||||
|
Phase 1 work.
|
||||||
|
|
||||||
The target product should use Iroh relay support for practical internet
|
The target product should use Iroh relay support for practical internet
|
||||||
connectivity and mDNS/LAN discovery for local networks. These are connectivity
|
connectivity and mDNS/LAN discovery for local networks. These are connectivity
|
||||||
and candidate-discovery mechanisms only. They do not grant trust, mutate
|
and candidate-discovery mechanisms only. They do not grant trust, mutate
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ geth-to-geth connections without granting trust from discovery alone.
|
||||||
- Discovered EndpointIDs do not grant module access without keychain/auth
|
- Discovered EndpointIDs do not grant module access without keychain/auth
|
||||||
validation.
|
validation.
|
||||||
|
|
||||||
- `[ ]` Protocol/router scaffold.
|
- `[x]` Protocol/router scaffold.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- ALPN constants are registered through one module router.
|
- ALPN constants are registered through one module router.
|
||||||
- Unknown ALPNs are rejected explicitly.
|
- Unknown ALPNs are rejected explicitly.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue