diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml new file mode 100644 index 0000000..7f731ef --- /dev/null +++ b/crates/api/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "api" +version = "0.1.0" +license.workspace = true +edition.workspace = true +authors.workspace = true + +[dependencies] +log.workspace = true +thiserror.workspace = true +tracing.workspace = true +tokio.workspace = true +libcollar.workspace = true +store = { path = "../store" } +tower-http = { version = "0.6.4", features = ["trace"] } +axum = "0.8.4" diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs new file mode 100644 index 0000000..a768ab8 --- /dev/null +++ b/crates/api/src/lib.rs @@ -0,0 +1,36 @@ +use std::sync::Arc; + +use tower_http::trace::TraceLayer; + +use axum::{ + routing::get, + extract::{State, Request, Json, Path, Extension, Query}, + routing::post, +}; + +pub use axum::serve; + +struct AppState { + collar: libcollar::State, +} + +pub fn app(collar: libcollar::State) -> axum::Router { + let state = Arc::new(AppState { collar }); + + // build our application with a single route + let app = axum::Router::new() + .route("/", get(|| async { "collared." })) + .route("/ng/eiface/{name}", post(post_ng_eiface)) + .with_state(state) + .layer(tower_http::trace::TraceLayer::new_for_http()); + + app +} + +async fn post_ng_eiface(State(state): State>, Path(name): Path) -> Result { + let fname = name.clone(); + let result = state.collar.leash().gated(move || { + Ok(libcollar::ng::new_eiface(&name)) + }).await; + Ok(format!("sup {} => {:?}", &fname, result)) +} diff --git a/crates/libcollar/Cargo.toml b/crates/libcollar/Cargo.toml new file mode 100644 index 0000000..2fbc331 --- /dev/null +++ b/crates/libcollar/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "libcollar" +version = "0.1.0" +license.workspace = true +edition.workspace = true +authors.workspace = true + +[dependencies] +log.workspace = true +thiserror.workspace = true +tracing.workspace = true +tokio.workspace = true +store.workspace = true +serde.workspace = true +ifconfig = { path = "../ifconfig" } +ng = { path = "../ng" } diff --git a/src/error.rs b/crates/libcollar/src/error.rs similarity index 100% rename from src/error.rs rename to crates/libcollar/src/error.rs diff --git a/crates/libcollar/src/jail/jail.rs b/crates/libcollar/src/jail/jail.rs new file mode 100644 index 0000000..1cd2474 --- /dev/null +++ b/crates/libcollar/src/jail/jail.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Deserialize)] +pub struct Jail { + store_id: u64, + name: String +} diff --git a/crates/libcollar/src/jail/mod.rs b/crates/libcollar/src/jail/mod.rs new file mode 100644 index 0000000..483ab07 --- /dev/null +++ b/crates/libcollar/src/jail/mod.rs @@ -0,0 +1,2 @@ +mod jail; +pub use jail; diff --git a/crates/libcollar/src/lib.rs b/crates/libcollar/src/lib.rs new file mode 100644 index 0000000..8f510d2 --- /dev/null +++ b/crates/libcollar/src/lib.rs @@ -0,0 +1,6 @@ +mod error; +pub mod net; +pub use error::{Error, Result}; +pub use ng; +mod libcollar; +pub use libcollar::*; diff --git a/crates/libcollar/src/libcollar.rs b/crates/libcollar/src/libcollar.rs new file mode 100644 index 0000000..e245bf5 --- /dev/null +++ b/crates/libcollar/src/libcollar.rs @@ -0,0 +1,17 @@ +use crate::Result; + +pub struct State { + leash: net::Leash, +} + +pub fn new() -> Result { + Ok(State { + leash: net::Leash::new(), + }) +} + +impl State { + pub fn leash(&self) -> &net::Leash { + &self.leash + } +} diff --git a/src/lib.rs b/crates/libcollar/src/net/leash.rs similarity index 95% rename from src/lib.rs rename to crates/libcollar/src/net/leash.rs index 40b40a4..fce5a5b 100644 --- a/src/lib.rs +++ b/crates/libcollar/src/net/leash.rs @@ -1,45 +1,43 @@ -pub mod error; - use std::sync::Arc; use tokio::sync::Mutex; -use crate::error::{Error, Result}; +use crate::{Error, Result}; pub struct NetworkLeash { lock: Arc> } impl NetworkLeash { pub fn new() -> Self { tracing::trace!("Creating new NetworkLeash"); Self { lock: Arc::new(Mutex::new(())), } } pub async fn with_interface(&self, name: &str, operation: F) -> Result where F: FnOnce(&mut ifconfig::Iface) -> Result + Send + 'static, R: Send + 'static, { let _guard = self.lock.lock().await; let name = name.to_string(); tokio::task::spawn_blocking(move || { let mut iface = ifconfig::Iface::new(&name)?; operation(&mut iface) }).await? } pub async fn gated(&self, operation: F) -> Result where F: FnOnce() -> Result + Send + 'static, R: Send + 'static, { let _guard = self.lock.lock().await; tokio::task::spawn_blocking(move || { operation() }).await? } } diff --git a/crates/libcollar/src/net/mod.rs b/crates/libcollar/src/net/mod.rs new file mode 100644 index 0000000..f27d84d --- /dev/null +++ b/crates/libcollar/src/net/mod.rs @@ -0,0 +1,2 @@ +mod leash; +pub use leash::NetworkLeash as Leash; diff --git a/crates/libcollar/src/net/network.rs b/crates/libcollar/src/net/network.rs new file mode 100644 index 0000000..d81246c --- /dev/null +++ b/crates/libcollar/src/net/network.rs @@ -0,0 +1,5 @@ +pub struct Network { + store_id: u64, + name: String, + handler: String, +} diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml new file mode 100644 index 0000000..e58282d --- /dev/null +++ b/crates/store/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "store" +version = "0.1.0" +license.workspace = true +edition.workspace = true +authors.workspace = true + +[dependencies] +log.workspace = true +thiserror.workspace = true +sled = "0.34.7" diff --git a/crates/store/src/error.rs b/crates/store/src/error.rs new file mode 100644 index 0000000..e43e59c --- /dev/null +++ b/crates/store/src/error.rs @@ -0,0 +1,7 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + StoreError(#[from] sled::Error) +} + +pub type Result = ::std::result::Result; diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs new file mode 100644 index 0000000..d6e4129 --- /dev/null +++ b/crates/store/src/lib.rs @@ -0,0 +1,8 @@ +mod error; +mod store; + +pub use error::{Result, Error}; +pub use store::Store; +pub use store::open; + +mod namespace; diff --git a/crates/store/src/namespace.rs b/crates/store/src/namespace.rs new file mode 100644 index 0000000..b9b849c --- /dev/null +++ b/crates/store/src/namespace.rs @@ -0,0 +1,142 @@ +// This module implements "namespaces" which are buckets of unique values +// that maps to keys elsewhere in storage. +// +// the `names` tree is a k/v of `name` -> `id` where `id` is a u64 from `generate_id`. +// the `spaces` tree is a k/v of `id` -> `name` where `id` is a u64 the `names` tree id and `name` is a str. +use crate::Result; + +#[derive(Debug, Clone)] +pub struct NamespaceStore { + names: sled::Tree, + spaces: sled::Tree, +} + +impl NamespaceStore { + pub(crate) fn open(db: &sled::Db) -> Result { + Ok(NamespaceStore { + names: db.open_tree("namespaces/1/n")?, + spaces: db.open_tree("namespaces/1/s")?, + }) + } + + // define a namespace. + // inserts a key `namespace` into `names`, where the value is a random u64. + pub fn define(&self, namespace: &str) -> Result<()> { + self.names.transaction(|db| { + match db.get(namespace)? { + Some(_) => Ok(()), + None => { + let id = db.generate_id()?; + db.insert(namespace, &id.to_be_bytes())?; + Ok(()) + } + } + })?; + Ok(()) + } + + pub fn resolve(&self, namespace: &str, key: &str) -> Result { + let result = (&self.names, &self.spaces).transaction(|(names, spaces)| { + // Get ID of the namespace from `names` + let namespace_id = match names.get(namespace)? { + Some(id_bytes) => { + let id_array: [u8; 8] = id_bytes.as_ref().try_into() + .map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?; + u64::from_be_bytes(id_array) + } + None => return Ok(false), // namespace doesn't exist + }; + + // Create composite key: namespace_id + key + let mut composite_key = Vec::new(); + composite_key.extend_from_slice(&namespace_id.to_be_bytes()); + composite_key.extend_from_slice(key.as_bytes()); + + // Check if key exists in spaces + Ok(spaces.get(&composite_key)?.is_some()) + })?; + Ok(result) + } + + pub fn reserve(&self, namespace: &str, key: &str, value: &str) -> Result { + let result = (&self.names, &self.spaces).transaction(|(names, spaces)| { + // Get ID of the namespace from `names` + let namespace_id = match names.get(namespace)? { + Some(id_bytes) => { + let id_array: [u8; 8] = id_bytes.as_ref().try_into() + .map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?; + u64::from_be_bytes(id_array) + } + None => return Ok(false), // namespace doesn't exist + }; + + // Create composite key: namespace_id + key + let mut composite_key = Vec::new(); + composite_key.extend_from_slice(&namespace_id.to_be_bytes()); + composite_key.extend_from_slice(key.as_bytes()); + + // Check if key already exists + if spaces.get(&composite_key)?.is_some() { + return Ok(false); // key already exists + } + + // Insert the key-value pair + spaces.insert(&composite_key, value.as_bytes())?; + Ok(true) + })?; + Ok(result) + } + + pub fn get(&self, namespace: &str, key: &str) -> Result> { + let result = (&self.names, &self.spaces).transaction(|(names, spaces)| { + // Get ID of the namespace from `names` + let namespace_id = match names.get(namespace)? { + Some(id_bytes) => { + let id_array: [u8; 8] = id_bytes.as_ref().try_into() + .map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?; + u64::from_be_bytes(id_array) + } + None => return Ok(None), // namespace doesn't exist + }; + + // Create composite key: namespace_id + key + let mut composite_key = Vec::new(); + composite_key.extend_from_slice(&namespace_id.to_be_bytes()); + composite_key.extend_from_slice(key.as_bytes()); + + // Get value from spaces + match spaces.get(&composite_key)? { + Some(value_bytes) => { + let value = String::from_utf8(value_bytes.to_vec()) + .map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?; + Ok(Some(value)) + } + None => Ok(None), + } + })?; + Ok(result) + } + + pub fn remove(&self, namespace: &str, key: &str) -> Result { + let result = (&self.names, &self.spaces).transaction(|(names, spaces)| { + // Get ID of the namespace from `names` + let namespace_id = match names.get(namespace)? { + Some(id_bytes) => { + let id_array: [u8; 8] = id_bytes.as_ref().try_into() + .map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?; + u64::from_be_bytes(id_array) + } + None => return Ok(false), // namespace doesn't exist + }; + + // Create composite key: namespace_id + key + let mut composite_key = Vec::new(); + composite_key.extend_from_slice(&namespace_id.to_be_bytes()); + composite_key.extend_from_slice(key.as_bytes()); + + // Remove the key-value pair + Ok(spaces.remove(&composite_key)?.is_some()) + })?; + Ok(result) + } +} \ No newline at end of file diff --git a/crates/store/src/store.rs b/crates/store/src/store.rs new file mode 100644 index 0000000..ab9d6c2 --- /dev/null +++ b/crates/store/src/store.rs @@ -0,0 +1,42 @@ +use crate::Result; + +#[derive(Debug, Clone)] +pub struct Db { + prefix: String, + db: sled::Db +} + +#[derive(Debug, Clone)] +pub struct Store { + db: Db, + namespaces: namespaces::NamespaceStore, +} + +impl Db { + pub fn open(path: String, prefix: String) -> Result { + let db = sled::open(path)?; + Ok(Db { prefix, db }) + } + + pub fn open_tree(&self, name: &str) -> Result { + self.db.open_tree(self.tree_path(name)) + } + + pub fn tree_path(&self, name: &str) -> String { + format!("t1/{}/{}", self.prefix, name) + } +} + +pub fn open() -> Result { + let db = Db::open("libcollar_store".to_string(), "bonefire".to_string())?; + Ok(Store { + db: db, + namespaces: crate::namespaces::open(&db)?, + }) +} + +impl Store { + fn tree_path(&self, name: &str) -> String { + make_tree_path(self.prefix, name) + } +} diff --git a/src/bin/collar-ng.rs b/src/bin/collar-ng.rs index 1d08d62..e0e134c 100644 --- a/src/bin/collar-ng.rs +++ b/src/bin/collar-ng.rs @@ -1,68 +1,68 @@ use std::env::args; use env_logger::Env; use log::error; -use ng; +use libcollar::ng; fn main() { let env = Env::default() .filter_or("LOG_LEVEL", "trace") .write_style_or("LOG_STYLE", "always"); env_logger::init_from_env(env); let mut argv = args(); let prog = argv.next().unwrap(); match argv.next() { None => println!("Usage: {} (args)", prog), Some(str) => match str.as_str() { "new-eiface" => { match argv.next() { None => println!("Usage: {} new-eiface ", prog), Some(name) => match ng::new_eiface(&name) { Ok(s) => println!("ok: {:#?}", s), Err(e) => error!("error: {:?}", e), } } }, "new-eiface-bridge" => { match argv.next() { None => println!("Usage: {} new-bridge ", prog), Some(bridge) => match argv.next() { None => println!("Usage: {} new-bridge ", prog), Some(name) => match ng::new_bridge(&bridge, &name) { Ok(s) => println!("ok: {}", s), Err(e) => error!("error: {:?}", e) } } } } "join-eiface-bridge" => { match argv.next() { None => println!("Usage: {} bridge-eiface ", prog), Some(bridge) => match argv.next() { None => println!("Usage: {} bridge-eiface ", prog), Some(name) => match ng::bridge_eiface(&bridge, &name) { Ok(s) => println!("ok: {}", s), Err(e) => error!("error: {:?}", e) } } } } "bridge" => { match argv.next() { None => println!("Usage: {} ", prog), Some(path) => match ng::go(&path) { Ok(s) => println!("{}", s), Err(e) => error!("error: {:?}", e) } } } _ => println!("Unknown action: {}", str) } } } diff --git a/src/bin/collared.rs b/src/bin/collared.rs index 62580e8..7b6fbbb 100644 --- a/src/bin/collared.rs +++ b/src/bin/collared.rs @@ -1,53 +1,27 @@ use std::env::args; +use libcollar; +use api::app; use tokio::net::TcpListener; -use tower_http::trace::TraceLayer; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -use std::sync::Arc; - -use axum::{ - routing::get, - extract::{State, Request, Json, Path, Extension, Query}, - routing::post, -}; - -struct AppState { - leash: collar::NetworkLeash, -} +use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::main] async fn main() { tracing_subscriber::registry() .with( - tracing_subscriber::filter::EnvFilter::try_from_default_env().unwrap_or_else(|_| { - "collar=trace,tower_http=debug,axum::rejection=trace" + EnvFilter::try_from_default_env().unwrap_or_else(|_| { + "collar=trace,libcollar=trace,ng=trace,ifconfig=trace,api=trace,store=trace,tower_http=debug,axum::rejection=trace" .into() }), ) .with(tracing_subscriber::fmt::layer()) .init(); - let leash = collar::NetworkLeash::new(); - let state = Arc::new(AppState { leash }); - - // build our application with a single route - let app = axum::Router::new() - .route("/", get(|| async { "collared." })) - .route("/ng/eiface/{name}", post(post_ng_eiface)) - .with_state(state) - .layer(tower_http::trace::TraceLayer::new_for_http()); + let app = app(libcollar::new().expect("Failed to initialize libcollar")); - // run our app with hyper, listening globally on port 3000 - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + let listen = "0.0.0.0:3000"; + let listener = TcpListener::bind(listen).await.expect("Failed to bind listener"); tracing::info!("listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app).await.unwrap(); -} -async fn post_ng_eiface(State(state): State>, Path(name): Path) -> Result { - let fname = name.clone(); - let result = state.leash.gated(move || { - Ok(ng::new_eiface(&name)) - }).await; - Ok(format!("sup {} => {:?}", &fname, result)) + api::serve(listener, app).await.unwrap(); }