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,
|
||||
daemon-owned Iroh endpoint startup, built-in relay-mode config, SSH
|
||||
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,
|
||||
iroh-gossip, iroh-blobs, Automerge sync, real auth enforcement, OpenSSH KRL
|
||||
generation, and Keyhive/BeeKEM-style authorization are future roadmap items
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::{SocketAddrV4, SocketAddrV6};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
|
@ -36,11 +37,123 @@ impl GethIrohConfig {
|
|||
relay_mode,
|
||||
bind_ipv4: 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)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum GethRelayMode {
|
||||
|
|
@ -142,19 +255,7 @@ pub fn load_secret_key(path: &Path) -> Result<iroh::SecretKey, IrohError> {
|
|||
|
||||
#[must_use]
|
||||
pub fn all_alpns() -> Vec<Vec<u8>> {
|
||||
[
|
||||
ALPN_CONTROL,
|
||||
ALPN_KV,
|
||||
ALPN_CAS,
|
||||
ALPN_PUBSUB,
|
||||
ALPN_PIPE,
|
||||
ALPN_DB,
|
||||
ALPN_DOCUMENT,
|
||||
ALPN_SSH_PROXY,
|
||||
]
|
||||
.into_iter()
|
||||
.map(<[u8]>::to_vec)
|
||||
.collect()
|
||||
default_protocol_router().alpns()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -189,12 +290,24 @@ pub enum IrohError {
|
|||
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 {
|
||||
fn from(error: iroh::endpoint::BindError) -> Self {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -208,6 +321,41 @@ mod tests {
|
|||
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]
|
||||
fn iroh_secret_key_persists() {
|
||||
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
|
||||
`[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
|
||||
connectivity and mDNS/LAN discovery for local networks. These are connectivity
|
||||
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
|
||||
validation.
|
||||
|
||||
- `[ ]` Protocol/router scaffold.
|
||||
- `[x]` Protocol/router scaffold.
|
||||
Acceptance criteria:
|
||||
- ALPN constants are registered through one module router.
|
||||
- Unknown ALPNs are rejected explicitly.
|
||||
|
|
|
|||
Loading…
Reference in a new issue