diff --git a/core/Cargo.lock b/core/Cargo.lock index d99d6dd67..4e9a781fc 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -7419,10 +7419,12 @@ dependencies = [ "tokio-tungstenite", "tokio-util", "toml", + "tor-cell", "tor-hscrypto", "tor-hsservice", "tor-keymgr", "tor-llcrypto", + "tor-proto", "tor-rtcompat", "tower-service", "tracing", diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index f345370fa..b39fd8e29 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -225,10 +225,12 @@ 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-util = { version = "0.7.9", features = ["io"] } +tor-cell = { version = "0.33" } tor-hscrypto = { version = "0.33", features = ["full"] } tor-hsservice = { version = "0.33" } tor-keymgr = { version = "0.33", features = ["ephemeral-keystore"] } tor-llcrypto = { version = "0.33", features = ["full"] } +tor-proto = { version = "0.33" } tor-rtcompat = { version = "0.33", features = ["tokio", "rustls"] } tower-service = "0.3.3" tracing = "0.1.39" diff --git a/core/startos/src/net/gateway.rs b/core/startos/src/net/gateway.rs index 2d2251d92..90036e1cf 100644 --- a/core/startos/src/net/gateway.rs +++ b/core/startos/src/net/gateway.rs @@ -622,8 +622,8 @@ async fn watch_ip( .chain(ip6_proxy.address_data().await?) .collect_vec(); let lan_ip = [ - ip4_proxy.gateway().await?.parse::()?, - ip6_proxy.gateway().await?.parse::()?, + dbg!(ip4_proxy.gateway().await?).parse::()?, + dbg!(ip6_proxy.gateway().await?).parse::()?, ] .into_iter() .collect(); @@ -852,7 +852,13 @@ impl NetworkInterfaceController { ) -> Result<(), Error> { tracing::debug!("syncronizing {info:?} to db"); - let dns = todo!(); + // let dns = todo!(); + let dns = info + .values() + .filter_map(|i| i.ip_info.as_ref()) + .flat_map(|i| &i.dns_servers) + .copied() + .collect(); db.mutate(|db| { let net = db.as_public_mut().as_server_info_mut().as_network_mut(); diff --git a/core/startos/src/net/host/binding.rs b/core/startos/src/net/host/binding.rs index 3d304b716..d37a74dad 100644 --- a/core/startos/src/net/host/binding.rs +++ b/core/startos/src/net/host/binding.rs @@ -12,8 +12,8 @@ use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::db::model::public::NetworkInterfaceInfo; use crate::net::forward::AvailablePorts; -use crate::net::host::HostApiKind; use crate::net::gateway::InterfaceFilter; +use crate::net::host::HostApiKind; use crate::net::vhost::AlpnInfo; use crate::prelude::*; use crate::util::serde::{display_serializable, HandlerExtSerde}; @@ -58,8 +58,10 @@ pub struct BindInfo { #[ts(export)] pub struct NetInfo { #[ts(as = "BTreeSet::")] + #[serde(default)] pub private_disabled: OrdSet, #[ts(as = "BTreeSet::")] + #[serde(default)] pub public_enabled: OrdSet, pub assigned_port: Option, pub assigned_ssl_port: Option, diff --git a/core/startos/src/net/tor.rs b/core/startos/src/net/tor.rs index 056d4134b..aacbe4cc6 100644 --- a/core/startos/src/net/tor.rs +++ b/core/startos/src/net/tor.rs @@ -9,7 +9,7 @@ use arti_client::{TorClient, TorClientConfig}; use base64::Engine; use clap::Parser; use color_eyre::eyre::eyre; -use futures::FutureExt; +use futures::{FutureExt, Stream, StreamExt}; use helpers::NonDetachingJoinHandle; use imbl_value::InternedString; use itertools::Itertools; @@ -18,15 +18,21 @@ use regex::Regex; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use safelog::DisplayRedacted; use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWriteExt; +use tokio::net::TcpStream; +use tokio::sync::oneshot; +use tor_cell::relaycell::msg::Connected; use tor_hscrypto::pk::{HsId, HsIdKeypair}; use tor_hsservice::status::State as ArtiOnionServiceState; -use tor_hsservice::{HsNickname, RunningOnionService}; +use tor_hsservice::{HsNickname, RendRequest, RunningOnionService}; use tor_keymgr::config::ArtiKeystoreKind; +use tor_proto::stream::IncomingStreamRequest; use tor_rtcompat::tokio::TokioRustlsRuntime; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::prelude::*; +use crate::util::actor::background::BackgroundJobQueue; use crate::util::serde::{ deserialize_from_str, display_serializable, serialize_display, Base64, HandlerExtSerde, WithIoFormat, BASE64, @@ -210,25 +216,6 @@ impl std::fmt::Debug for OnionStore { } } -enum ErrorLogSeverity { - Fatal { wipe_state: bool }, - Unknown { wipe_state: bool }, -} - -lazy_static! { - static ref LOG_REGEXES: Vec<(Regex, ErrorLogSeverity)> = vec![( - Regex::new("This could indicate a route manipulation attack, network overload, bad local network connectivity, or a bug\\.").unwrap(), - ErrorLogSeverity::Unknown { wipe_state: true } - ),( - Regex::new("died due to an invalid selected path").unwrap(), - ErrorLogSeverity::Fatal { wipe_state: false } - ),( - Regex::new("Tor has not observed any network activity for the past").unwrap(), - ErrorLogSeverity::Unknown { wipe_state: false } - )]; - static ref PROGRESS_REGEX: Regex = Regex::new("PROGRESS=([0-9]+)").unwrap(); -} - pub fn tor_api() -> ParentHandler { ParentHandler::new() .subcommand( @@ -368,6 +355,11 @@ pub enum OnionServiceState { Recovering, Broken, } +impl From for OnionServiceState { + fn from(value: ArtiOnionServiceState) -> Self { + todo!() + } +} pub async fn list_services( ctx: RpcContext, @@ -403,7 +395,9 @@ impl TorController { let addr = key.onion_address(); match s.entry(addr) { Entry::Occupied(e) => Ok(e.get().clone()), - Entry::Vacant(e) => Ok(e.insert(OnionService::launch(&self.client, key)?).clone()), + Entry::Vacant(e) => Ok(e + .insert(OnionService::launch(self.client.clone(), key)?) + .clone()), } }) } @@ -457,31 +451,123 @@ impl TorController { #[derive(Clone)] pub struct OnionService(Arc); struct OnionServiceData { - service: Arc, + service: Arc>>>, bindings: Arc>>>>, _thread: NonDetachingJoinHandle<()>, } impl OnionService { - fn launch(client: &TorClient, key: TorSecretKey) -> Result { - let (service, stream) = client.launch_onion_service_with_hsid( - OnionServiceConfigBuilder::default() - .nickname( - key.onion_address() - .to_string() - .trim_end_matches(".onion") - .parse::() - .with_kind(ErrorKind::Tor)?, - ) - .build() - .with_kind(ErrorKind::Tor)?, - key.0, - )?; - let bindings = Arc::new(SyncRwLock::new(BTreeMap::new())); + fn launch(client: TorClient, key: TorSecretKey) -> Result { + let service = Arc::new(SyncMutex::new(None)); + let bindings = Arc::new(SyncRwLock::new(BTreeMap::< + u16, + BTreeMap>, + >::new())); Ok(Self(Arc::new(OnionServiceData { service: service.clone(), bindings: bindings.clone(), _thread: tokio::spawn(async move { - todo!(); + let (bg, mut runner) = BackgroundJobQueue::new(); + runner + .run_while(async { + loop { + if let Err(e) = async { + let (new_service, mut stream) = client + .launch_onion_service_with_hsid( + OnionServiceConfigBuilder::default() + .nickname( + key.onion_address() + .to_string() + .trim_end_matches(".onion") + .parse::() + .with_kind(ErrorKind::Tor)?, + ) + .build() + .with_kind(ErrorKind::Tor)?, + key.clone().0, + ) + .with_kind(ErrorKind::Tor)?; + service.replace(Some(new_service)); + while let Some(req) = stream.next().await { + bg.add_job({ + let bg = bg.clone(); + let bindings = bindings.clone(); + async move { + if let Err(e) = async { + let mut stream = + req.accept().await.with_kind(ErrorKind::Tor)?; + while let Some(req) = stream.next().await { + let IncomingStreamRequest::Begin(begin) = + req.request() + else { + continue; // TODO: reject instead? + }; + 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 { + continue; // TODO: reject instead? + }; + 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::Tor)?; + if let Err(e) = + tokio::io::copy_bidirectional( + &mut outgoing, + &mut incoming, + ) + .await + { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + } + incoming.flush().await?; + outgoing.flush().await?; + incoming.shutdown().await?; + outgoing.shutdown().await?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + } + }); + } + Ok::<_, Error>(()) + } + .await + { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + } + } + }); + } + Ok::<_, Error>(()) + } + .await + { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + } + } + }) + .await }) .into(), }))) @@ -491,14 +577,35 @@ impl OnionService { &self, bindings: impl IntoIterator, ) -> Rcs { - todo!() + self.0.bindings.mutate(|b| { + bindings + .into_iter() + .map(|(port, target)| { + let entry = b.entry(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 { - todo!() + 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> { - todo!() + self.0._thread.abort(); + Ok(()) } } diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index 9a6c51df3..9d5de56c3 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -34,7 +34,6 @@ pub mod recipe; ├── manifest.json ├── icon. ├── LICENSE.md - ├── instructions.md ├── dependencies │ └── │ ├── metadata.json diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs index fa9f46fc3..1e4aa0de0 100644 --- a/core/startos/src/s9pk/v2/pack.rs +++ b/core/startos/src/s9pk/v2/pack.rs @@ -147,8 +147,6 @@ pub struct PackParams { pub icon: Option, #[arg(long)] pub license: Option, - #[arg(long)] - pub instructions: Option, #[arg(long, conflicts_with = "no-assets")] pub assets: Option, #[arg(long, conflicts_with = "assets")] @@ -240,12 +238,6 @@ impl PackParams { .await? } } - fn instructions(&self) -> PathBuf { - self.instructions - .as_ref() - .cloned() - .unwrap_or_else(|| self.path().join("instructions.md")) - } fn assets(&self) -> PathBuf { self.assets .as_ref() @@ -797,20 +789,10 @@ pub async fn list_ingredients(_: CliContext, params: PackParams) -> Result { warn!("failed to load manifest: {e}"); debug!("{e:?}"); - return Ok(vec![ - js_path, - params.icon().await?, - params.license().await?, - params.instructions(), - ]); + return Ok(vec![js_path, params.icon().await?, params.license().await?]); } }; - let mut ingredients = vec![ - js_path, - params.icon().await?, - params.license().await?, - params.instructions(), - ]; + let mut ingredients = vec![js_path, params.icon().await?, params.license().await?]; for (_, dependency) in manifest.dependencies.0 { if let Some(PathOrUrl::Path(p)) = dependency.s9pk { diff --git a/core/startos/src/util/actor/background.rs b/core/startos/src/util/actor/background.rs index 7666cbf04..ee5ea2cf4 100644 --- a/core/startos/src/util/actor/background.rs +++ b/core/startos/src/util/actor/background.rs @@ -63,3 +63,29 @@ impl Future for BackgroundJobRunner { } } } +impl BackgroundJobRunner { + pub fn run_while( + &mut self, + fut: Fut, + ) -> impl Future + Send { + #[pin_project::pin_project] + struct RunWhile<'a, Fut> { + #[pin] + runner: &'a mut BackgroundJobRunner, + #[pin] + fut: Fut, + } + impl<'a, Fut: Future> Future for RunWhile<'a, Fut> { + type Output = Fut::Output; + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + let this = self.project(); + this.runner.poll(cx); + this.fut.poll(cx) + } + } + RunWhile { runner: self, fut } + } +} diff --git a/core/startos/src/util/sync.rs b/core/startos/src/util/sync.rs index b5d8dca0a..40427486c 100644 --- a/core/startos/src/util/sync.rs +++ b/core/startos/src/util/sync.rs @@ -19,6 +19,9 @@ impl SyncMutex { pub fn peek U, U>(&self, f: F) -> U { f(&*self.0.lock().unwrap()) } + pub fn replace(&self, value: T) -> T { + std::mem::replace(&mut *self.0.lock().unwrap(), value) + } } #[derive(Debug, Default)]