mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
wip: iroh
This commit is contained in:
2095
core/Cargo.lock
generated
2095
core/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ color-eyre = "0.6.2"
|
||||
ed25519-dalek = { version = "2.0.0", features = ["serde"] }
|
||||
gpt = "4.1.0"
|
||||
lazy_static = "1.4"
|
||||
lettre = { version = "0.11", default-features = false }
|
||||
mbrman = "0.6.0"
|
||||
exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [
|
||||
"serde",
|
||||
|
||||
@@ -94,6 +94,7 @@ pub enum ErrorKind {
|
||||
DBus = 75,
|
||||
InstallFailed = 76,
|
||||
UpdateFailed = 77,
|
||||
Smtp = 78,
|
||||
}
|
||||
impl ErrorKind {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
@@ -176,6 +177,7 @@ impl ErrorKind {
|
||||
DBus => "DBus Error",
|
||||
InstallFailed => "Install Failed",
|
||||
UpdateFailed => "Update Failed",
|
||||
Smtp => "SMTP Error",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,6 +372,21 @@ impl From<patch_db::value::Error> for Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<lettre::error::Error> for Error {
|
||||
fn from(e: lettre::error::Error) -> Self {
|
||||
Error::new(e, ErrorKind::Smtp)
|
||||
}
|
||||
}
|
||||
impl From<lettre::transport::smtp::Error> for Error {
|
||||
fn from(e: lettre::transport::smtp::Error) -> Self {
|
||||
Error::new(e, ErrorKind::Smtp)
|
||||
}
|
||||
}
|
||||
impl From<lettre::address::AddressError> for Error {
|
||||
fn from(e: lettre::address::AddressError) -> Self {
|
||||
Error::new(e, ErrorKind::Smtp)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct ErrorData {
|
||||
|
||||
@@ -51,7 +51,7 @@ default = ["cli", "startd", "registry", "cli-container", "tunnel"]
|
||||
dev = []
|
||||
docker = []
|
||||
registry = []
|
||||
startd = ["mail-send"]
|
||||
startd = []
|
||||
test = []
|
||||
tunnel = []
|
||||
unstable = ["console-subscriber", "tokio/tracing"]
|
||||
@@ -81,25 +81,26 @@ async-stream = "0.3.5"
|
||||
async-trait = "0.1.74"
|
||||
axum = { version = "0.8.4", features = ["ws"] }
|
||||
barrage = "0.2.3"
|
||||
backhand = "0.21.0"
|
||||
backhand = "0.23.0"
|
||||
base32 = "0.5.0"
|
||||
base64 = "0.22.1"
|
||||
base64ct = "1.6.0"
|
||||
basic-cookies = "0.1.4"
|
||||
bech32 = "0.11.0"
|
||||
blake3 = { version = "1.5.0", features = ["mmap", "rayon"] }
|
||||
bytes = "1"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
clap = { version = "4.4.12", features = ["string"] }
|
||||
color-eyre = "0.6.2"
|
||||
console = "0.15.7"
|
||||
console = "0.16.0"
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
const_format = "0.2.34"
|
||||
cookie = "0.18.0"
|
||||
cookie_store = "0.21.0"
|
||||
cookie_store = "0.22.0"
|
||||
der = { version = "0.7.9", features = ["derive", "pem"] }
|
||||
digest = "0.10.7"
|
||||
divrem = "1.0.0"
|
||||
dns-lookup = "2.1.0"
|
||||
dns-lookup = "3.0.0"
|
||||
ed25519 = { version = "2.2.3", features = ["pkcs8", "pem", "alloc"] }
|
||||
ed25519-dalek = { version = "2.2.0", features = [
|
||||
"serde",
|
||||
@@ -141,10 +142,11 @@ imbl = { version = "6", features = ["serde", "small-chunks"] }
|
||||
imbl-value = { version = "0.4.3", features = ["ts-rs"] }
|
||||
include_dir = { version = "0.7.3", features = ["metadata"] }
|
||||
indexmap = { version = "2.0.2", features = ["serde"] }
|
||||
indicatif = { version = "0.17.7", features = ["tokio"] }
|
||||
indicatif = { version = "0.18.0", features = ["tokio"] }
|
||||
inotify = "0.11.0"
|
||||
integer-encoding = { version = "4.0.0", features = ["tokio_async"] }
|
||||
ipnet = { version = "2.8.0", features = ["serde"] }
|
||||
iroh = { version = "0.91.2", features = ["discovery-pkarr-dht"] }
|
||||
isocountry = "0.3.2"
|
||||
itertools = "0.14.0"
|
||||
jaq-core = "0.10.1"
|
||||
@@ -153,7 +155,16 @@ josekit = "0.10.3"
|
||||
jsonpath_lib = { git = "https://github.com/Start9Labs/jsonpath.git" }
|
||||
lazy_async_pool = "0.3.3"
|
||||
lazy_format = "2.0"
|
||||
lazy_static = "1.4.0"
|
||||
lazy_static = "1.5.0"
|
||||
lettre = { version = "0.11.18", default-features = false, features = [
|
||||
"smtp-transport",
|
||||
"pool",
|
||||
"hostname",
|
||||
"builder",
|
||||
"tokio1-rustls",
|
||||
"rustls-platform-verifier",
|
||||
"aws-lc-rs",
|
||||
] }
|
||||
libc = "0.2.149"
|
||||
log = "0.4.20"
|
||||
mio = "1"
|
||||
@@ -186,23 +197,23 @@ pkcs8 = { version = "0.10.2", features = ["std"] }
|
||||
prettytable-rs = "0.10.0"
|
||||
procfs = { version = "0.17.0", optional = true }
|
||||
proptest = "1.3.1"
|
||||
proptest-derive = "0.5.0"
|
||||
proptest-derive = "0.6.0"
|
||||
pty-process = { version = "0.5.1", optional = true }
|
||||
qrcode = "0.14.1"
|
||||
rand = "0.9.2"
|
||||
regex = "1.10.2"
|
||||
reqwest = { version = "0.12.4", features = ["stream", "json", "socks"] }
|
||||
reqwest_cookie_store = "0.8.0"
|
||||
reqwest_cookie_store = "0.9.0"
|
||||
rpassword = "7.2.0"
|
||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" }
|
||||
rust-argon2 = "2.0.0"
|
||||
rust-argon2 = "3.0.0"
|
||||
rustyline-async = "0.4.1"
|
||||
safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
semver = { version = "1.0.20", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_cbor = { package = "ciborium", version = "0.2.1" }
|
||||
serde_json = "1.0"
|
||||
serde_toml = { package = "toml", version = "0.8.2" }
|
||||
serde_toml = { package = "toml", version = "0.9.5" }
|
||||
serde_urlencoded = "0.7"
|
||||
serde_with = { version = "3.4.0", features = ["macros", "json"] }
|
||||
serde_yaml = { package = "serde_yml", version = "0.0.12" }
|
||||
@@ -227,7 +238,7 @@ tokio = { version = "1.38.1", features = ["full"] }
|
||||
tokio-rustls = "0.26.0"
|
||||
tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] }
|
||||
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
|
||||
tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
|
||||
tokio-tungstenite = { version = "0.27.0", features = ["native-tls", "url"] }
|
||||
tokio-util = { version = "0.7.9", features = ["io"] }
|
||||
tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit" }
|
||||
tor-hscrypto = { version = "0.33", features = [
|
||||
@@ -259,7 +270,6 @@ urlencoding = "2.1.3"
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
zbus = "5.1.1"
|
||||
zeroize = "1.6.0"
|
||||
mail-send = { git = "https://github.com/dr-bonez/mail-send.git", branch = "main", optional = true }
|
||||
rustls = "0.23.20"
|
||||
rustls-pki-types = { version = "1.10.1", features = ["alloc"] }
|
||||
|
||||
|
||||
585
core/startos/src/net/iroh.rs
Normal file
585
core/startos/src/net/iroh.rs
Normal file
@@ -0,0 +1,585 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use imbl_value::InternedString;
|
||||
use iroh::{Endpoint, NodeId, SecretKey};
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::prelude::*;
|
||||
use crate::util::actor::background::BackgroundJobQueue;
|
||||
use crate::util::io::ReadWriter;
|
||||
use crate::util::serde::{
|
||||
deserialize_from_str, display_serializable, serialize_display, HandlerExtSerde, Pem,
|
||||
PemEncoding, WithIoFormat,
|
||||
};
|
||||
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
|
||||
|
||||
const HRP: bech32::Hrp = bech32::Hrp::parse_unchecked("iroh");
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct IrohAddress(pub NodeId);
|
||||
impl std::fmt::Display for IrohAddress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
bech32::encode_lower_to_fmt::<bech32::Bech32m, _>(f, HRP, self.0.as_bytes())
|
||||
.map_err(|_| std::fmt::Error)?;
|
||||
write!(f, ".p2p.start9.to")
|
||||
}
|
||||
}
|
||||
impl FromStr for IrohAddress {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Some(b32) = s.strip_suffix(".p2p.start9.to") {
|
||||
let (hrp, data) = bech32::decode(b32).with_kind(ErrorKind::ParseNetAddress)?;
|
||||
ensure_code!(
|
||||
hrp == HRP,
|
||||
ErrorKind::ParseNetAddress,
|
||||
"not an iroh address"
|
||||
);
|
||||
Ok(Self(
|
||||
NodeId::from_bytes(&*<Box<[u8; 32]>>::try_from(data).map_err(|_| {
|
||||
Error::new(eyre!("invalid length"), ErrorKind::ParseNetAddress)
|
||||
})?)
|
||||
.with_kind(ErrorKind::ParseNetAddress)?,
|
||||
))
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("Invalid iroh address"),
|
||||
ErrorKind::ParseNetAddress,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Serialize for IrohAddress {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serialize_display(self, serializer)
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for IrohAddress {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserialize_from_str(deserializer)
|
||||
}
|
||||
}
|
||||
impl PartialEq for IrohAddress {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.as_ref() == other.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl Eq for IrohAddress {}
|
||||
impl PartialOrd for IrohAddress {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.0.as_ref().partial_cmp(other.0.as_ref())
|
||||
}
|
||||
}
|
||||
impl Ord for IrohAddress {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.0.as_ref().cmp(other.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IrohSecretKey(pub SecretKey);
|
||||
impl IrohSecretKey {
|
||||
pub fn iroh_address(&self) -> IrohAddress {
|
||||
IrohAddress(self.0.public())
|
||||
}
|
||||
pub fn generate() -> Self {
|
||||
Self(SecretKey::generate(
|
||||
&mut ssh_key::rand_core::OsRng::default(),
|
||||
))
|
||||
}
|
||||
}
|
||||
impl PemEncoding for IrohSecretKey {
|
||||
fn from_pem<E: serde::de::Error>(pem: &str) -> Result<Self, E> {
|
||||
ed25519_dalek::SigningKey::from_pem(pem)
|
||||
.map(From::from)
|
||||
.map(Self)
|
||||
}
|
||||
fn to_pem<E: serde::ser::Error>(&self) -> Result<String, E> {
|
||||
self.0.secret().to_pem()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
pub struct IrohKeyStore(BTreeMap<IrohAddress, Pem<IrohSecretKey>>);
|
||||
impl Map for IrohKeyStore {
|
||||
type Key = IrohAddress;
|
||||
type Value = Pem<IrohSecretKey>;
|
||||
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
||||
Self::key_string(key)
|
||||
}
|
||||
fn key_string(key: &Self::Key) -> Result<imbl_value::InternedString, Error> {
|
||||
Ok(InternedString::from_display(key))
|
||||
}
|
||||
}
|
||||
impl IrohKeyStore {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn insert(&mut self, key: IrohSecretKey) {
|
||||
self.0.insert(key.iroh_address(), Pem::new(key));
|
||||
}
|
||||
}
|
||||
impl Model<IrohKeyStore> {
|
||||
pub fn new_key(&mut self) -> Result<IrohSecretKey, Error> {
|
||||
let key = IrohSecretKey::generate();
|
||||
self.insert(&key.iroh_address(), &Pem::new(key))?;
|
||||
Ok(key)
|
||||
}
|
||||
pub fn insert_key(&mut self, key: &IrohSecretKey) -> Result<(), Error> {
|
||||
self.insert(&key.iroh_address(), Pem::new_ref(key))
|
||||
}
|
||||
pub fn get_key(&self, address: &IrohAddress) -> Result<IrohSecretKey, Error> {
|
||||
self.as_idx(address)
|
||||
.or_not_found(lazy_format!("private key for {address}"))?
|
||||
.de()
|
||||
.map(|k| k.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iroh_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"list-services",
|
||||
from_fn_async(list_services)
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|handle, result| display_services(handle.params, result))
|
||||
.with_about("Display the status of running iroh services")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"key",
|
||||
key::<C>().with_about("Manage the iroh service key store"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn key<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"generate",
|
||||
from_fn_async(generate_key)
|
||||
.with_about("Generate an iroh service key and add it to the key store")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_key)
|
||||
.with_about("Add an iroh service key to the key store")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_keys)
|
||||
.with_custom_display_fn(|_, res| {
|
||||
for addr in res {
|
||||
println!("{addr}");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.with_about("List iroh services with keys in the key store")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn generate_key(ctx: RpcContext) -> Result<IrohAddress, Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
Ok(db
|
||||
.as_private_mut()
|
||||
.as_key_store_mut()
|
||||
.as_iroh_mut()
|
||||
.new_key()?
|
||||
.iroh_address())
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
pub struct AddKeyParams {
|
||||
pub key: Pem<IrohSecretKey>,
|
||||
}
|
||||
|
||||
pub async fn add_key(
|
||||
ctx: RpcContext,
|
||||
AddKeyParams { key }: AddKeyParams,
|
||||
) -> Result<IrohAddress, Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_private_mut()
|
||||
.as_key_store_mut()
|
||||
.as_iroh_mut()
|
||||
.insert_key(&key.0)
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Ok(key.iroh_address())
|
||||
}
|
||||
|
||||
pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<IrohAddress>, Error> {
|
||||
ctx.db
|
||||
.peek()
|
||||
.await
|
||||
.into_private()
|
||||
.into_key_store()
|
||||
.into_iroh()
|
||||
.keys()
|
||||
}
|
||||
|
||||
pub fn display_services(
|
||||
params: WithIoFormat<Empty>,
|
||||
services: BTreeMap<IrohAddress, IrohServiceInfo>,
|
||||
) -> Result<(), Error> {
|
||||
use prettytable::*;
|
||||
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, services);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc => "ADDRESS", "BINDINGS"]);
|
||||
for (service, info) in services {
|
||||
let row = row![
|
||||
&service.to_string(),
|
||||
&info
|
||||
.bindings
|
||||
.into_iter()
|
||||
.map(|((subdomain, port), addr)| lazy_format!("{subdomain}:{port} -> {addr}"))
|
||||
.join("; ")
|
||||
];
|
||||
table.add_row(row);
|
||||
}
|
||||
table.print_tty(false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IrohServiceInfo {
|
||||
pub bindings: BTreeMap<(InternedString, u16), SocketAddr>,
|
||||
}
|
||||
|
||||
pub async fn list_services(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
) -> Result<BTreeMap<IrohAddress, IrohServiceInfo>, Error> {
|
||||
ctx.net_controller.iroh.list_services().await
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IrohController(Arc<IrohControllerInner>);
|
||||
struct IrohControllerInner {
|
||||
// client: Endpoint,
|
||||
services: SyncMutex<BTreeMap<IrohAddress, IrohService>>,
|
||||
}
|
||||
impl IrohController {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
Ok(Self(Arc::new(IrohControllerInner {
|
||||
services: SyncMutex::new(BTreeMap::new()),
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn service(&self, key: IrohSecretKey) -> Result<IrohService, Error> {
|
||||
self.0.services.mutate(|s| {
|
||||
use std::collections::btree_map::Entry;
|
||||
let addr = key.iroh_address();
|
||||
match s.entry(addr) {
|
||||
Entry::Occupied(e) => Ok(e.get().clone()),
|
||||
Entry::Vacant(e) => Ok(e
|
||||
.insert(IrohService::launch(self.0.client.clone(), key)?)
|
||||
.clone()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn gc(&self, addr: Option<IrohAddress>) -> Result<(), Error> {
|
||||
if let Some(addr) = addr {
|
||||
if let Some(s) = self.0.services.mutate(|s| {
|
||||
let rm = if let Some(s) = s.get(&addr) {
|
||||
!s.gc()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if rm {
|
||||
s.remove(&addr)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
s.shutdown().await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
for s in self.0.services.mutate(|s| {
|
||||
let mut rm = Vec::new();
|
||||
s.retain(|_, s| {
|
||||
if s.gc() {
|
||||
true
|
||||
} else {
|
||||
rm.push(s.clone());
|
||||
false
|
||||
}
|
||||
});
|
||||
rm
|
||||
}) {
|
||||
s.shutdown().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_services(&self) -> Result<BTreeMap<IrohAddress, IrohServiceInfo>, Error> {
|
||||
Ok(self
|
||||
.0
|
||||
.services
|
||||
.peek(|s| s.iter().map(|(a, s)| (a.clone(), s.info())).collect()))
|
||||
}
|
||||
|
||||
pub async fn connect_iroh(
|
||||
&self,
|
||||
addr: &IrohAddress,
|
||||
port: u16,
|
||||
) -> Result<Box<dyn ReadWriter + Unpin + Send + Sync + 'static>, Error> {
|
||||
if let Some(target) = self.0.services.peek(|s| {
|
||||
s.get(addr).and_then(|s| {
|
||||
s.0.bindings.peek(|b| {
|
||||
b.get(&port).and_then(|b| {
|
||||
b.iter()
|
||||
.find(|(_, rc)| rc.strong_count() > 0)
|
||||
.map(|(a, _)| *a)
|
||||
})
|
||||
})
|
||||
})
|
||||
}) {
|
||||
Ok(Box::new(
|
||||
TcpStream::connect(target)
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?,
|
||||
))
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IrohService(Arc<IrohServiceData>);
|
||||
struct IrohServiceData {
|
||||
service: Endpoint,
|
||||
bindings: Arc<SyncRwLock<BTreeMap<(InternedString, u16), BTreeMap<SocketAddr, Weak<()>>>>>,
|
||||
_thread: NonDetachingJoinHandle<()>,
|
||||
}
|
||||
impl IrohService {
|
||||
fn launch(
|
||||
mut client: Watch<(usize, IrohClient<TokioRustlsRuntime>)>,
|
||||
key: IrohSecretKey,
|
||||
) -> Result<Self, Error> {
|
||||
let service = Arc::new(SyncMutex::new(None));
|
||||
let bindings = Arc::new(SyncRwLock::new(BTreeMap::<
|
||||
u16,
|
||||
BTreeMap<SocketAddr, Weak<()>>,
|
||||
>::new()));
|
||||
Ok(Self(Arc::new(IrohServiceData {
|
||||
service: service.clone(),
|
||||
bindings: bindings.clone(),
|
||||
_thread: tokio::spawn(async move {
|
||||
let (bg, mut runner) = BackgroundJobQueue::new();
|
||||
runner
|
||||
.run_while(async {
|
||||
loop {
|
||||
if let Err(e) = async {
|
||||
client.wait_for(|(_,c)| c.bootstrap_status().ready_for_traffic()).await;
|
||||
let epoch = client.peek(|(e, c)| {
|
||||
ensure_code!(c.bootstrap_status().ready_for_traffic(), ErrorKind::Iroh, "client recycled");
|
||||
Ok::<_, Error>(*e)
|
||||
})?;
|
||||
let (new_service, stream) = client.peek(|(_, c)| {
|
||||
c.launch_onion_service_with_hsid(
|
||||
IrohServiceConfigBuilder::default()
|
||||
.nickname(
|
||||
key.iroh_address()
|
||||
.to_string()
|
||||
.trim_end_matches(".onion")
|
||||
.parse::<HsNickname>()
|
||||
.with_kind(ErrorKind::Iroh)?,
|
||||
)
|
||||
.build()
|
||||
.with_kind(ErrorKind::Iroh)?,
|
||||
key.clone().0,
|
||||
)
|
||||
.with_kind(ErrorKind::Iroh)
|
||||
})?;
|
||||
let mut status_stream = new_service.status_events();
|
||||
bg.add_job(async move {
|
||||
while let Some(status) = status_stream.next().await {
|
||||
// TODO: health daemon?
|
||||
}
|
||||
});
|
||||
service.replace(Some(new_service));
|
||||
let mut stream = tor_hsservice::handle_rend_requests(stream);
|
||||
while let Some(req) = tokio::select! {
|
||||
req = stream.next() => req,
|
||||
_ = client.wait_for(|(e, _)| *e != epoch) => None
|
||||
} {
|
||||
bg.add_job({
|
||||
let bg = bg.clone();
|
||||
let bindings = bindings.clone();
|
||||
async move {
|
||||
if let Err(e) = async {
|
||||
let IncomingStreamRequest::Begin(begin) =
|
||||
req.request()
|
||||
else {
|
||||
return req
|
||||
.reject(tor_cell::relaycell::msg::End::new_with_reason(
|
||||
tor_cell::relaycell::msg::EndReason::DONE,
|
||||
))
|
||||
.await
|
||||
.with_kind(ErrorKind::Iroh);
|
||||
};
|
||||
let Some(target) = bindings.peek(|b| {
|
||||
b.get(&begin.port()).and_then(|a| {
|
||||
a.iter()
|
||||
.find(|(_, rc)| rc.strong_count() > 0)
|
||||
.map(|(addr, _)| *addr)
|
||||
})
|
||||
}) else {
|
||||
return req
|
||||
.reject(tor_cell::relaycell::msg::End::new_with_reason(
|
||||
tor_cell::relaycell::msg::EndReason::DONE,
|
||||
))
|
||||
.await
|
||||
.with_kind(ErrorKind::Iroh);
|
||||
};
|
||||
bg.add_job(async move {
|
||||
if let Err(e) = async {
|
||||
let mut outgoing =
|
||||
TcpStream::connect(target)
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?;
|
||||
let mut incoming = req
|
||||
.accept(Connected::new_empty())
|
||||
.await
|
||||
.with_kind(ErrorKind::Iroh)?;
|
||||
if let Err(e) =
|
||||
tokio::io::copy_bidirectional(
|
||||
&mut outgoing,
|
||||
&mut incoming,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Iroh Stream Error: {e}");
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::trace!("Iroh Stream Error: {e}");
|
||||
tracing::trace!("{e:?}");
|
||||
}
|
||||
});
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::trace!("Iroh Request Error: {e}");
|
||||
tracing::trace!("{e:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!("Iroh Client Error: {e}");
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
})
|
||||
.into(),
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn proxy_all<Rcs: FromIterator<Arc<()>>>(
|
||||
&self,
|
||||
bindings: impl IntoIterator<Item = (InternedString, u16, SocketAddr)>,
|
||||
) -> Rcs {
|
||||
self.0.bindings.mutate(|b| {
|
||||
bindings
|
||||
.into_iter()
|
||||
.map(|(subdomain, port, target)| {
|
||||
let entry = b
|
||||
.entry((subdomain, port))
|
||||
.or_default()
|
||||
.entry(target)
|
||||
.or_default();
|
||||
if let Some(rc) = entry.upgrade() {
|
||||
rc
|
||||
} else {
|
||||
let rc = Arc::new(());
|
||||
*entry = Arc::downgrade(&rc);
|
||||
rc
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn gc(&self) -> bool {
|
||||
self.0.bindings.mutate(|b| {
|
||||
b.retain(|_, targets| {
|
||||
targets.retain(|_, rc| rc.strong_count() > 0);
|
||||
!targets.is_empty()
|
||||
});
|
||||
!b.is_empty()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn shutdown(self) -> Result<(), Error> {
|
||||
self.0.service.replace(None);
|
||||
self.0._thread.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn state(&self) -> IrohServiceState {
|
||||
self.0
|
||||
.service
|
||||
.peek(|s| s.as_ref().map(|s| s.status().state().into()))
|
||||
.unwrap_or(IrohServiceState::Bootstrapping)
|
||||
}
|
||||
|
||||
pub fn info(&self) -> IrohServiceInfo {
|
||||
IrohServiceInfo {
|
||||
state: self.state(),
|
||||
bindings: self.0.bindings.peek(|b| {
|
||||
b.iter()
|
||||
.filter_map(|(port, b)| {
|
||||
b.iter()
|
||||
.find(|(_, rc)| rc.strong_count() > 0)
|
||||
.map(|(addr, _)| (*port, *addr))
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,17 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::account::AccountInfo;
|
||||
use crate::net::acme::AcmeCertStore;
|
||||
use crate::net::iroh::IrohKeyStore;
|
||||
use crate::net::ssl::CertStore;
|
||||
use crate::net::tor::OnionStore;
|
||||
use crate::net::tor::OnionKeyStore;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct KeyStore {
|
||||
pub onion: OnionStore,
|
||||
pub onion: OnionKeyStore,
|
||||
#[serde(default)]
|
||||
pub iroh: IrohKeyStore,
|
||||
pub local_certs: CertStore,
|
||||
#[serde(default)]
|
||||
pub acme: AcmeCertStore,
|
||||
@@ -17,7 +20,8 @@ pub struct KeyStore {
|
||||
impl KeyStore {
|
||||
pub fn new(account: &AccountInfo) -> Result<Self, Error> {
|
||||
let mut res = Self {
|
||||
onion: OnionStore::new(),
|
||||
onion: OnionKeyStore::new(),
|
||||
iroh: IrohKeyStore::new(),
|
||||
local_certs: CertStore::new(account)?,
|
||||
acme: AcmeCertStore::new(),
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod dns;
|
||||
pub mod forward;
|
||||
pub mod gateway;
|
||||
pub mod host;
|
||||
pub mod iroh;
|
||||
pub mod keys;
|
||||
pub mod mdns;
|
||||
pub mod net_controller;
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::net::gateway::{
|
||||
use crate::net::host::address::HostAddress;
|
||||
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
|
||||
use crate::net::host::{host_for, Host, Hosts};
|
||||
use crate::net::iroh::IrohController;
|
||||
use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname};
|
||||
use crate::net::socks::SocksController;
|
||||
use crate::net::tor::{OnionAddress, TorController, TorSecretKey};
|
||||
@@ -37,6 +38,7 @@ use crate::HOST_IP;
|
||||
pub struct NetController {
|
||||
pub(crate) db: TypedPatchDb<Database>,
|
||||
pub(super) tor: TorController,
|
||||
pub(super) iroh: IrohController,
|
||||
pub(super) vhost: VHostController,
|
||||
pub(crate) net_iface: Arc<NetworkInterfaceController>,
|
||||
pub(super) dns: DnsController,
|
||||
@@ -54,10 +56,12 @@ impl NetController {
|
||||
) -> Result<Self, Error> {
|
||||
let net_iface = Arc::new(NetworkInterfaceController::new(db.clone()));
|
||||
let tor = TorController::new()?;
|
||||
let iroh = IrohController::new()?;
|
||||
let socks = SocksController::new(socks_listen, tor.clone())?;
|
||||
Ok(Self {
|
||||
db: db.clone(),
|
||||
tor,
|
||||
iroh,
|
||||
vhost: VHostController::new(db.clone(), net_iface.clone()),
|
||||
dns: DnsController::init(db, &net_iface.watcher).await?,
|
||||
forward: PortForwardController::new(net_iface.watcher.subscribe()),
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::sync::{Arc, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use arti_client::config::onion_service::OnionServiceConfigBuilder;
|
||||
use arti_client::{DataStream, TorClient, TorClientConfig};
|
||||
use arti_client::{TorClient, TorClientConfig};
|
||||
use base64::Engine;
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
@@ -62,7 +62,7 @@ impl FromStr for OnionAddress {
|
||||
Cow::Owned(format!("{s}.onion"))
|
||||
}
|
||||
.parse::<HsId>()
|
||||
.with_kind(ErrorKind::Tor)?,
|
||||
.with_kind(ErrorKind::ParseNetAddress)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -165,8 +165,8 @@ impl<'de> Deserialize<'de> for TorSecretKey {
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
pub struct OnionStore(BTreeMap<OnionAddress, TorSecretKey>);
|
||||
impl Map for OnionStore {
|
||||
pub struct OnionKeyStore(BTreeMap<OnionAddress, TorSecretKey>);
|
||||
impl Map for OnionKeyStore {
|
||||
type Key = OnionAddress;
|
||||
type Value = TorSecretKey;
|
||||
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
||||
@@ -176,7 +176,7 @@ impl Map for OnionStore {
|
||||
Ok(InternedString::from_display(key))
|
||||
}
|
||||
}
|
||||
impl OnionStore {
|
||||
impl OnionKeyStore {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
@@ -184,7 +184,7 @@ impl OnionStore {
|
||||
self.0.insert(key.onion_address(), key);
|
||||
}
|
||||
}
|
||||
impl Model<OnionStore> {
|
||||
impl Model<OnionKeyStore> {
|
||||
pub fn new_key(&mut self) -> Result<TorSecretKey, Error> {
|
||||
let key = TorSecretKey::generate();
|
||||
self.insert(&key.onion_address(), &key)?;
|
||||
@@ -199,7 +199,7 @@ impl Model<OnionStore> {
|
||||
.de()
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for OnionStore {
|
||||
impl std::fmt::Debug for OnionKeyStore {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
struct OnionStoreMap<'a>(&'a BTreeMap<OnionAddress, TorSecretKey>);
|
||||
impl<'a> std::fmt::Debug for OnionStoreMap<'a> {
|
||||
@@ -227,7 +227,7 @@ pub fn tor_api<C: Context>() -> ParentHandler<C> {
|
||||
from_fn_async(list_services)
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|handle, result| display_services(handle.params, result))
|
||||
.with_about("Display Tor V3 Onion Addresses")
|
||||
.with_about("Show the status of running onion services")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
|
||||
@@ -9,7 +9,7 @@ use color_eyre::eyre::eyre;
|
||||
use futures::{FutureExt, TryStreamExt};
|
||||
use imbl::vector;
|
||||
use imbl_value::InternedString;
|
||||
use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async};
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use rustls::RootCertStore;
|
||||
use rustls_pki_types::CertificateDer;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
@@ -24,12 +24,12 @@ use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::util::Invoke;
|
||||
use crate::util::cpupower::{Governor, get_available_governors, set_governor};
|
||||
use crate::util::cpupower::{get_available_governors, set_governor, Governor};
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::net::WebSocketExt;
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
|
||||
use crate::util::sync::Watch;
|
||||
use crate::util::Invoke;
|
||||
use crate::{MAIN_DATA, PACKAGE_DATA};
|
||||
|
||||
pub fn experimental<C: Context>() -> ParentHandler<C> {
|
||||
@@ -1024,7 +1024,7 @@ pub struct TestSmtpParams {
|
||||
#[arg(long)]
|
||||
pub login: String,
|
||||
#[arg(long)]
|
||||
pub password: Option<String>,
|
||||
pub password: String,
|
||||
}
|
||||
pub async fn test_smtp(
|
||||
_: RpcContext,
|
||||
@@ -1037,74 +1037,23 @@ pub async fn test_smtp(
|
||||
password,
|
||||
}: TestSmtpParams,
|
||||
) -> Result<(), Error> {
|
||||
#[cfg(feature = "mail-send")]
|
||||
{
|
||||
use mail_send::SmtpClientBuilder;
|
||||
use mail_send::mail_builder::{self, MessageBuilder};
|
||||
use rustls_pki_types::pem::PemObject;
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
|
||||
let Some(pass_val) = password else {
|
||||
return Err(Error::new(
|
||||
eyre!("mail-send requires a password"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
};
|
||||
|
||||
let mut root_cert_store = RootCertStore::empty();
|
||||
let pem = tokio::fs::read("/etc/ssl/certs/ca-certificates.crt").await?;
|
||||
for cert in CertificateDer::pem_slice_iter(&pem) {
|
||||
root_cert_store.add_parsable_certificates([cert.with_kind(ErrorKind::OpenSsl)?]);
|
||||
}
|
||||
|
||||
let cfg = Arc::new(
|
||||
rustls::ClientConfig::builder_with_provider(Arc::new(
|
||||
rustls::crypto::ring::default_provider(),
|
||||
))
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_root_certificates(root_cert_store)
|
||||
.with_no_client_auth(),
|
||||
);
|
||||
let client = SmtpClientBuilder::new_with_tls_config(server, port, cfg)
|
||||
.implicit_tls(false)
|
||||
.credentials((login.split("@").next().unwrap().to_owned(), pass_val));
|
||||
|
||||
fn parse_address<'a>(addr: &'a str) -> mail_builder::headers::address::Address<'a> {
|
||||
if addr.find("<").map_or(false, |start| {
|
||||
addr.find(">").map_or(false, |end| start < end)
|
||||
}) {
|
||||
addr.split_once("<")
|
||||
.map(|(name, addr)| (name.trim(), addr.strip_suffix(">").unwrap_or(addr)))
|
||||
.unwrap()
|
||||
.into()
|
||||
} else {
|
||||
addr.into()
|
||||
}
|
||||
}
|
||||
|
||||
let message = MessageBuilder::new()
|
||||
.from(parse_address(&from))
|
||||
.to(parse_address(&to))
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay(&server)?
|
||||
.credentials(Credentials::new(login, password))
|
||||
.build()
|
||||
.send(
|
||||
Message::builder()
|
||||
.from(from.parse()?)
|
||||
.to(to.parse()?)
|
||||
.subject("StartOS Test Email")
|
||||
.text_body("This is a test email sent from your StartOS Server");
|
||||
client
|
||||
.connect()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::new(
|
||||
eyre!("mail-send connection error: {:?}", e),
|
||||
ErrorKind::Unknown,
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body("This is a test email sent from your StartOS Server".to_owned())?,
|
||||
)
|
||||
})?
|
||||
.send(message)
|
||||
.await
|
||||
.map_err(|e| Error::new(eyre!("mail-send send error: {:?}", e), ErrorKind::Unknown))?;
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(feature = "mail-send"))]
|
||||
Err(Error::new(
|
||||
eyre!("test-smtp requires mail-send feature to be enabled"),
|
||||
ErrorKind::InvalidRequest,
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user