bugfixes for alpha.10 (#3032)

* bugfixes for alpha.10

* bump raspi kernel

* rpi kernel bump

* alpha.11
This commit is contained in:
Aiden McClelland
2025-09-23 16:42:17 -06:00
committed by GitHub
parent c62ca4b183
commit bc62de795e
26 changed files with 5883 additions and 3419 deletions

View File

@@ -38,7 +38,7 @@
}, },
"../sdk/dist": { "../sdk/dist": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.40", "version": "0.4.0-beta.41",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

9
core/Cargo.lock generated
View File

@@ -4632,6 +4632,7 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"ts-rs", "ts-rs",
"typeid",
"yasi", "yasi",
"zbus", "zbus",
] ]
@@ -7265,7 +7266,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "start-os" name = "start-os"
version = "0.4.0-alpha.10" version = "0.4.0-alpha.11"
dependencies = [ dependencies = [
"aes 0.7.5", "aes 0.7.5",
"arti-client", "arti-client",
@@ -9249,6 +9250,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.18.0" version = "1.18.0"

View File

@@ -35,5 +35,6 @@ ts-rs = "9"
thiserror = "2.0" thiserror = "2.0"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tracing = "0.1.39" tracing = "0.1.39"
typeid = "1"
yasi = { version = "0.1.6", features = ["serde", "ts-rs"] } yasi = { version = "0.1.6", features = ["serde", "ts-rs"] }
zbus = "5" zbus = "5"

View File

@@ -188,6 +188,7 @@ impl Display for ErrorKind {
#[derive(Debug)] #[derive(Debug)]
pub struct Error { pub struct Error {
pub source: color_eyre::eyre::Error, pub source: color_eyre::eyre::Error,
pub debug: Option<color_eyre::eyre::Error>,
pub kind: ErrorKind, pub kind: ErrorKind,
pub revision: Option<Revision>, pub revision: Option<Revision>,
pub task: Option<JoinHandle<()>>, pub task: Option<JoinHandle<()>>,
@@ -199,9 +200,15 @@ impl Display for Error {
} }
} }
impl Error { impl Error {
pub fn new<E: Into<color_eyre::eyre::Error>>(source: E, kind: ErrorKind) -> Self { pub fn new<E: Into<color_eyre::eyre::Error> + std::fmt::Debug + 'static>(
source: E,
kind: ErrorKind,
) -> Self {
let debug = (typeid::of::<E>() == typeid::of::<color_eyre::eyre::Error>())
.then(|| eyre!("{source:?}"));
Error { Error {
source: source.into(), source: source.into(),
debug,
kind, kind,
revision: None, revision: None,
task: None, task: None,
@@ -209,11 +216,8 @@ impl Error {
} }
pub fn clone_output(&self) -> Self { pub fn clone_output(&self) -> Self {
Error { Error {
source: ErrorData { source: eyre!("{}", self.source),
details: format!("{}", self.source), debug: self.debug.as_ref().map(|e| eyre!("{e}")),
debug: format!("{:?}", self.source),
}
.into(),
kind: self.kind, kind: self.kind,
revision: self.revision.clone(), revision: self.revision.clone(),
task: None, task: None,
@@ -539,25 +543,24 @@ where
impl<T, E> ResultExt<T, E> for Result<T, E> impl<T, E> ResultExt<T, E> for Result<T, E>
where where
color_eyre::eyre::Error: From<E>, color_eyre::eyre::Error: From<E>,
E: std::fmt::Debug + 'static,
{ {
fn with_kind(self, kind: ErrorKind) -> Result<T, Error> { fn with_kind(self, kind: ErrorKind) -> Result<T, Error> {
self.map_err(|e| Error { self.map_err(|e| Error::new(e, kind))
source: e.into(),
kind,
revision: None,
task: None,
})
} }
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> { fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> {
self.map_err(|e| { self.map_err(|e| {
let (kind, ctx) = f(&e); let (kind, ctx) = f(&e);
let debug = (typeid::of::<E>() == typeid::of::<color_eyre::eyre::Error>())
.then(|| eyre!("{ctx}: {e:?}"));
let source = color_eyre::eyre::Error::from(e); let source = color_eyre::eyre::Error::from(e);
let ctx = format!("{}: {}", ctx, source); let with_ctx = format!("{ctx}: {source}");
let source = source.wrap_err(ctx); let source = source.wrap_err(with_ctx);
Error { Error {
kind, kind,
source, source,
debug,
revision: None, revision: None,
task: None, task: None,
} }
@@ -578,25 +581,24 @@ where
} }
impl<T> ResultExt<T, Error> for Result<T, Error> { impl<T> ResultExt<T, Error> for Result<T, Error> {
fn with_kind(self, kind: ErrorKind) -> Result<T, Error> { fn with_kind(self, kind: ErrorKind) -> Result<T, Error> {
self.map_err(|e| Error { self.map_err(|e| Error { kind, ..e })
source: e.source,
kind,
revision: e.revision,
task: e.task,
})
} }
fn with_ctx<F: FnOnce(&Error) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> { fn with_ctx<F: FnOnce(&Error) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> {
self.map_err(|e| { self.map_err(|e| {
let (kind, ctx) = f(&e); let (kind, ctx) = f(&e);
let source = e.source; let source = e.source;
let ctx = format!("{}: {}", ctx, source); let with_ctx = format!("{ctx}: {source}");
let source = source.wrap_err(ctx); let source = source.wrap_err(with_ctx);
let debug = e.debug.map(|e| {
let with_ctx = format!("{ctx}: {e}");
e.wrap_err(with_ctx)
});
Error { Error {
kind, kind,
source, source,
revision: e.revision, debug,
task: e.task, ..e
} }
}) })
} }

View File

@@ -14,7 +14,7 @@ keywords = [
name = "start-os" name = "start-os"
readme = "README.md" readme = "README.md"
repository = "https://github.com/Start9Labs/start-os" repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.10" # VERSION_BUMP version = "0.4.0-alpha.11" # VERSION_BUMP
license = "MIT" license = "MIT"
[lib] [lib]

View File

@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@@ -202,8 +202,10 @@ pub struct NetworkInfo {
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)] #[ts(export)]
pub struct DnsSettings { pub struct DnsSettings {
pub dhcp_servers: Vec<SocketAddr>, #[ts(type = "string[]")]
pub static_servers: Option<Vec<SocketAddr>>, pub dhcp_servers: VecDeque<SocketAddr>,
#[ts(type = "string[] | null")]
pub static_servers: Option<VecDeque<SocketAddr>>,
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)] #[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)]

View File

@@ -10,8 +10,8 @@ use tracing::instrument;
use super::filesystem::{FileSystem, MountType, ReadOnly, ReadWrite}; use super::filesystem::{FileSystem, MountType, ReadOnly, ReadWrite};
use super::util::unmount; use super::util::unmount;
use crate::Error;
use crate::util::{Invoke, Never}; use crate::util::{Invoke, Never};
use crate::Error;
pub const TMP_MOUNTPOINT: &'static str = "/media/startos/tmp"; pub const TMP_MOUNTPOINT: &'static str = "/media/startos/tmp";
@@ -74,7 +74,7 @@ impl MountGuard {
} }
pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> { pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> {
if self.mounted { if self.mounted {
unmount(&self.mountpoint, false).await?; unmount(&self.mountpoint, !cfg!(feature = "unstable")).await?;
if delete_mountpoint { if delete_mountpoint {
match tokio::fs::remove_dir(&self.mountpoint).await { match tokio::fs::remove_dir(&self.mountpoint).await {
Err(e) if e.raw_os_error() == Some(39) => Ok(()), // directory not empty Err(e) if e.raw_os_error() == Some(39) => Ok(()), // directory not empty

View File

@@ -1,5 +1,5 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use std::collections::BTreeMap; use std::collections::{BTreeMap, VecDeque};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
@@ -23,7 +23,9 @@ use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseI
use hickory_server::ServerFuture; use hickory_server::ServerFuture;
use imbl::OrdMap; use imbl::OrdMap;
use imbl_value::InternedString; use imbl_value::InternedString;
use itertools::Itertools;
use models::{GatewayId, OptionExt, PackageId}; use models::{GatewayId, OptionExt, PackageId};
use patch_db::json_ptr::JsonPointer;
use rpc_toolkit::{ use rpc_toolkit::{
from_fn_async, from_fn_blocking, Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async, from_fn_blocking, Context, HandlerArgs, HandlerExt, ParentHandler,
}; };
@@ -36,6 +38,7 @@ use crate::db::model::public::NetworkInterfaceInfo;
use crate::db::model::Database; use crate::db::model::Database;
use crate::net::gateway::NetworkInterfaceWatcher; use crate::net::gateway::NetworkInterfaceWatcher;
use crate::prelude::*; use crate::prelude::*;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::io::file_string_stream; use crate::util::io::file_string_stream;
use crate::util::serde::{display_serializable, HandlerExtSerde}; use crate::util::serde::{display_serializable, HandlerExtSerde};
use crate::util::sync::{SyncRwLock, Watch}; use crate::util::sync::{SyncRwLock, Watch};
@@ -161,97 +164,146 @@ impl DnsClient {
Self { Self {
client: client.clone(), client: client.clone(),
_thread: tokio::spawn(async move { _thread: tokio::spawn(async move {
loop { let (bg, mut runner) = BackgroundJobQueue::new();
if let Err::<(), Error>(e) = async { runner
let mut stream = file_string_stream("/run/systemd/resolve/resolv.conf") .run_while(async move {
.filter_map(|a| futures::future::ready(a.transpose())) let dhcp_ns_db = db.clone();
.boxed(); bg.add_job(async move {
let mut conf: String = stream loop {
.next() if let Err(e) = async {
.await let mut stream =
.or_not_found("/run/systemd/resolve/resolv.conf")??; file_string_stream("/run/systemd/resolve/resolv.conf")
let mut prev_nameservers = Vec::new(); .filter_map(|a| futures::future::ready(a.transpose()))
let mut bg = BTreeMap::<SocketAddr, BoxFuture<_>>::new(); .boxed();
loop { while let Some(conf) = stream.next().await {
let nameservers = conf let conf: String = conf?;
.lines() let mut nameservers = conf
.map(|l| l.trim()) .lines()
.filter_map(|l| l.strip_prefix("nameserver ")) .map(|l| l.trim())
.skip(2) .filter_map(|l| l.strip_prefix("nameserver "))
.map(|n| { .map(|n| {
n.parse::<SocketAddr>() n.parse::<SocketAddr>().or_else(|_| {
.or_else(|_| n.parse::<IpAddr>().map(|a| (a, 53).into())) n.parse::<IpAddr>().map(|a| (a, 53).into())
}) })
.collect::<Result<Vec<_>, _>>()?; })
let static_nameservers = db .collect::<Result<VecDeque<_>, _>>()?;
.mutate(|db| { if nameservers
let dns = db .front()
.as_public_mut() .map_or(false, |addr| addr.ip().is_loopback())
.as_server_info_mut()
.as_network_mut()
.as_dns_mut();
dns.as_dhcp_servers_mut().ser(&nameservers)?;
dns.as_static_servers().de()
})
.await
.result?;
let nameservers = static_nameservers.unwrap_or(nameservers);
if nameservers != prev_nameservers {
let mut existing: BTreeMap<_, _> =
client.peek(|c| c.iter().cloned().collect());
let mut new = Vec::with_capacity(nameservers.len());
for addr in &nameservers {
if let Some(existing) = existing.remove(addr) {
new.push((*addr, existing));
} else {
let client = if let Ok((client, bg_thread)) =
Client::connect(
UdpClientStream::builder(
*addr,
TokioRuntimeProvider::new(),
)
.build(),
)
.await
{ {
bg.insert(*addr, bg_thread.boxed()); nameservers.pop_front();
client }
} else { if nameservers.front().map_or(false, |addr| {
let (stream, sender) = TcpClientStream::new( addr.ip() == IpAddr::from([1, 1, 1, 1])
*addr, }) {
None, nameservers.pop_front();
Some(Duration::from_secs(30)), }
TokioRuntimeProvider::new(), dhcp_ns_db
); .mutate(|db| {
let (client, bg_thread) = let dns = db
Client::new(stream, sender, None) .as_public_mut()
.await .as_server_info_mut()
.with_kind(ErrorKind::Network)?; .as_network_mut()
bg.insert(*addr, bg_thread.boxed()); .as_dns_mut();
client dns.as_dhcp_servers_mut().ser(&nameservers)
}; })
new.push((*addr, client)); .await
.result?
} }
Ok::<_, Error>(())
}
.await
{
tracing::error!("{e}");
tracing::debug!("{e:?}");
tokio::time::sleep(Duration::from_secs(1)).await;
} }
bg.retain(|n, _| nameservers.iter().any(|a| a == n));
prev_nameservers = nameservers;
client.replace(new);
} }
tokio::select! { });
c = stream.next() => conf = c.or_not_found("/run/systemd/resolve/resolv.conf")??, loop {
_ = futures::future::join( if let Err::<(), Error>(e) = async {
futures::future::join_all(bg.values_mut()), let mut static_changed = db
futures::future::pending::<()>(), .subscribe(
) => (), "/public/serverInfo/network/dns/staticServers"
.parse::<JsonPointer>()
.with_kind(ErrorKind::Database)?,
)
.await;
let mut prev_nameservers = VecDeque::new();
let mut bg = BTreeMap::<SocketAddr, BoxFuture<_>>::new();
loop {
let dns = db
.peek()
.await
.into_public()
.into_server_info()
.into_network()
.into_dns();
let nameservers = dns
.as_static_servers()
.transpose_ref()
.unwrap_or_else(|| dns.as_dhcp_servers())
.de()?;
if nameservers != prev_nameservers {
let mut existing: BTreeMap<_, _> =
client.peek(|c| c.iter().cloned().collect());
let mut new = Vec::with_capacity(nameservers.len());
for addr in &nameservers {
if let Some(existing) = existing.remove(addr) {
new.push((*addr, existing));
} else {
let client = if let Ok((client, bg_thread)) =
Client::connect(
UdpClientStream::builder(
*addr,
TokioRuntimeProvider::new(),
)
.build(),
)
.await
{
bg.insert(*addr, bg_thread.boxed());
client
} else {
let (stream, sender) = TcpClientStream::new(
*addr,
None,
Some(Duration::from_secs(30)),
TokioRuntimeProvider::new(),
);
let (client, bg_thread) =
Client::new(stream, sender, None)
.await
.with_kind(ErrorKind::Network)?;
bg.insert(*addr, bg_thread.boxed());
client
};
new.push((*addr, client));
}
}
bg.retain(|n, _| nameservers.iter().any(|a| a == n));
prev_nameservers = nameservers;
client.replace(new);
}
futures::future::select(
static_changed.recv().boxed(),
futures::future::join(
futures::future::join_all(bg.values_mut()),
futures::future::pending::<()>(),
),
)
.await;
}
} }
.await
{
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
tokio::time::sleep(Duration::from_secs(1)).await;
} }
} })
.await .await;
{
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
}
}) })
.into(), .into(),
} }

View File

@@ -7,16 +7,19 @@ use helpers::NonDetachingJoinHandle;
use id_pool::IdPool; use id_pool::IdPool;
use imbl::OrdMap; use imbl::OrdMap;
use models::GatewayId; use models::GatewayId;
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::context::{CliContext, RpcContext};
use crate::db::model::public::NetworkInterfaceInfo; use crate::db::model::public::NetworkInterfaceInfo;
use crate::net::gateway::{DynInterfaceFilter, InterfaceFilter}; use crate::net::gateway::{DynInterfaceFilter, InterfaceFilter, SecureFilter};
use crate::net::utils::ipv6_is_link_local; use crate::net::utils::ipv6_is_link_local;
use crate::prelude::*; use crate::prelude::*;
use crate::util::Invoke; use crate::util::serde::{display_serializable, HandlerExtSerde};
use crate::util::sync::Watch; use crate::util::sync::Watch;
use crate::util::Invoke;
pub const START9_BRIDGE_IFACE: &str = "lxcbr0"; pub const START9_BRIDGE_IFACE: &str = "lxcbr0";
pub const FIRST_DYNAMIC_PRIVATE_PORT: u16 = 49152; pub const FIRST_DYNAMIC_PRIVATE_PORT: u16 = 49152;
@@ -42,6 +45,42 @@ impl AvailablePorts {
} }
} }
pub fn forward_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new().subcommand(
"dump-table",
from_fn_async(
|ctx: RpcContext| async move { ctx.net_controller.forward.dump_table().await },
)
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, res);
}
let mut table = Table::new();
table.add_row(row![bc => "FROM", "TO", "FILTER / GATEWAY"]);
for (external, target) in res.0 {
table.add_row(row![external, target.target, target.filter]);
for (source, gateway) in target.gateways {
table.add_row(row![
format!("{}:{}", source, external),
target.target,
gateway
]);
}
}
table.print_tty(false)?;
Ok(())
})
.with_call_remote::<CliContext>(),
)
}
struct ForwardRequest { struct ForwardRequest {
external: u16, external: u16,
target: SocketAddr, target: SocketAddr,
@@ -49,6 +88,7 @@ struct ForwardRequest {
rc: Weak<()>, rc: Weak<()>,
} }
#[derive(Clone)]
struct ForwardEntry { struct ForwardEntry {
external: u16, external: u16,
target: SocketAddr, target: SocketAddr,
@@ -96,7 +136,7 @@ impl ForwardEntry {
let mut keep = BTreeSet::<SocketAddr>::new(); let mut keep = BTreeSet::<SocketAddr>::new();
for (iface, info) in ip_info for (iface, info) in ip_info
.iter() .iter()
.chain([NetworkInterfaceInfo::loopback()]) // .chain([NetworkInterfaceInfo::loopback()])
.filter(|(id, info)| filter_ref.filter(*id, *info)) .filter(|(id, info)| filter_ref.filter(*id, *info))
{ {
if let Some(ip_info) = &info.ip_info { if let Some(ip_info) = &info.ip_info {
@@ -155,10 +195,9 @@ impl ForwardEntry {
*self = Self::new(external, target, rc); *self = Self::new(external, target, rc);
self.update(ip_info, Some(filter)).await?; self.update(ip_info, Some(filter)).await?;
} else { } else {
if self.prev_filter != filter {
self.update(ip_info, Some(filter)).await?;
}
self.rc = rc; self.rc = rc;
self.update(ip_info, Some(filter).filter(|f| f != &self.prev_filter))
.await?;
} }
Ok(()) Ok(())
} }
@@ -174,7 +213,7 @@ impl Drop for ForwardEntry {
} }
} }
#[derive(Default)] #[derive(Default, Clone)]
struct ForwardState { struct ForwardState {
state: BTreeMap<u16, ForwardEntry>, state: BTreeMap<u16, ForwardEntry>,
} }
@@ -197,7 +236,7 @@ impl ForwardState {
for entry in self.state.values_mut() { for entry in self.state.values_mut() {
entry.update(ip_info, None).await?; entry.update(ip_info, None).await?;
} }
self.state.retain(|_, fwd| !fwd.forwards.is_empty()); self.state.retain(|_, fwd| fwd.rc.strong_count() > 0);
Ok(()) Ok(())
} }
} }
@@ -209,28 +248,68 @@ fn err_has_exited<T>(_: T) -> Error {
) )
} }
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ForwardTable(pub BTreeMap<u16, ForwardTarget>);
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ForwardTarget {
pub target: SocketAddr,
pub filter: String,
pub gateways: BTreeMap<SocketAddr, GatewayId>,
}
impl From<&ForwardState> for ForwardTable {
fn from(value: &ForwardState) -> Self {
Self(
value
.state
.iter()
.map(|(external, entry)| {
(
*external,
ForwardTarget {
target: entry.target,
filter: format!("{:?}", entry.prev_filter),
gateways: entry.forwards.clone(),
},
)
})
.collect(),
)
}
}
enum ForwardCommand {
Forward(ForwardRequest, oneshot::Sender<Result<(), Error>>),
Sync(oneshot::Sender<Result<(), Error>>),
DumpTable(oneshot::Sender<ForwardTable>),
}
#[test]
fn test() {
assert_ne!(
false.into_dyn(),
SecureFilter { secure: false }.into_dyn().into_dyn()
);
}
pub struct PortForwardController { pub struct PortForwardController {
req: mpsc::UnboundedSender<(Option<ForwardRequest>, oneshot::Sender<Result<(), Error>>)>, req: mpsc::UnboundedSender<ForwardCommand>,
_thread: NonDetachingJoinHandle<()>, _thread: NonDetachingJoinHandle<()>,
} }
impl PortForwardController { impl PortForwardController {
pub fn new(mut ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>) -> Self { pub fn new(mut ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>) -> Self {
let (req_send, mut req_recv) = mpsc::unbounded_channel::<( let (req_send, mut req_recv) = mpsc::unbounded_channel::<ForwardCommand>();
Option<ForwardRequest>,
oneshot::Sender<Result<(), Error>>,
)>();
let thread = NonDetachingJoinHandle::from(tokio::spawn(async move { let thread = NonDetachingJoinHandle::from(tokio::spawn(async move {
let mut state = ForwardState::default(); let mut state = ForwardState::default();
let mut interfaces = ip_info.read_and_mark_seen(); let mut interfaces = ip_info.read_and_mark_seen();
loop { loop {
tokio::select! { tokio::select! {
msg = req_recv.recv() => { msg = req_recv.recv() => {
if let Some((msg, re)) = msg { if let Some(cmd) = msg {
if let Some(req) = msg { match cmd {
re.send(state.handle_request(req, &interfaces).await).ok(); ForwardCommand::Forward(req, re) => re.send(state.handle_request(req, &interfaces).await).ok(),
} else { ForwardCommand::Sync(re) => re.send(state.sync(&interfaces).await).ok(),
re.send(state.sync(&interfaces).await).ok(); ForwardCommand::DumpTable(re) => re.send((&state).into()).ok(),
} };
} else { } else {
break; break;
} }
@@ -250,19 +329,19 @@ impl PortForwardController {
pub async fn add( pub async fn add(
&self, &self,
external: u16, external: u16,
filter: impl InterfaceFilter, filter: DynInterfaceFilter,
target: SocketAddr, target: SocketAddr,
) -> Result<Arc<()>, Error> { ) -> Result<Arc<()>, Error> {
let rc = Arc::new(()); let rc = Arc::new(());
let (send, recv) = oneshot::channel(); let (send, recv) = oneshot::channel();
self.req self.req
.send(( .send(ForwardCommand::Forward(
Some(ForwardRequest { ForwardRequest {
external, external,
target, target,
filter: filter.into_dyn(), filter,
rc: Arc::downgrade(&rc), rc: Arc::downgrade(&rc),
}), },
send, send,
)) ))
.map_err(err_has_exited)?; .map_err(err_has_exited)?;
@@ -271,13 +350,25 @@ impl PortForwardController {
} }
pub async fn gc(&self) -> Result<(), Error> { pub async fn gc(&self) -> Result<(), Error> {
let (send, recv) = oneshot::channel(); let (send, recv) = oneshot::channel();
self.req.send((None, send)).map_err(err_has_exited)?; self.req
.send(ForwardCommand::Sync(send))
.map_err(err_has_exited)?;
recv.await.map_err(err_has_exited)? recv.await.map_err(err_has_exited)?
} }
pub async fn dump_table(&self) -> Result<ForwardTable, Error> {
let (req, res) = oneshot::channel();
self.req
.send(ForwardCommand::DumpTable(req))
.map_err(err_has_exited)?;
res.await.map_err(err_has_exited)
}
} }
async fn forward(interface: &str, source: SocketAddr, target: SocketAddr) -> Result<(), Error> { async fn forward(interface: &str, source: SocketAddr, target: SocketAddr) -> Result<(), Error> {
if source.is_ipv6() {
return Ok(()); // TODO: socat? ip6tables?
}
Command::new("/usr/lib/startos/scripts/forward-port") Command::new("/usr/lib/startos/scripts/forward-port")
.env("iiface", interface) .env("iiface", interface)
.env("oiface", START9_BRIDGE_IFACE) .env("oiface", START9_BRIDGE_IFACE)
@@ -291,6 +382,9 @@ async fn forward(interface: &str, source: SocketAddr, target: SocketAddr) -> Res
} }
async fn unforward(interface: &str, source: SocketAddr, target: SocketAddr) -> Result<(), Error> { async fn unforward(interface: &str, source: SocketAddr, target: SocketAddr) -> Result<(), Error> {
if source.is_ipv6() {
return Ok(()); // TODO: socat? ip6tables?
}
Command::new("/usr/lib/startos/scripts/forward-port") Command::new("/usr/lib/startos/scripts/forward-port")
.env("UNDO", "1") .env("UNDO", "1")
.env("iiface", interface) .env("iiface", interface)

View File

@@ -1329,6 +1329,9 @@ impl InterfaceFilter for DynInterfaceFilter {
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
self.0.as_any() self.0.as_any()
} }
fn into_dyn(self) -> DynInterfaceFilter {
self
}
} }
impl DynInterfaceFilter { impl DynInterfaceFilter {
fn new<T: InterfaceFilter>(value: T) -> Self { fn new<T: InterfaceFilter>(value: T) -> Self {

View File

@@ -33,6 +33,10 @@ pub fn net_api<C: Context>() -> ParentHandler<C> {
"dns", "dns",
dns::dns_api::<C>().with_about("Manage and query DNS"), dns::dns_api::<C>().with_about("Manage and query DNS"),
) )
.subcommand(
"forward",
forward::forward_api::<C>().with_about("Manage port forwards"),
)
.subcommand( .subcommand(
"gateway", "gateway",
gateway::gateway_api::<C>().with_about("View and edit gateway configurations"), gateway::gateway_api::<C>().with_about("View and edit gateway configurations"),

View File

@@ -3,13 +3,13 @@ use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use clap::Parser;
use clap::builder::ValueParserFactory; use clap::builder::ValueParserFactory;
use clap::Parser;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use helpers::const_true; use helpers::const_true;
use imbl_value::InternedString; use imbl_value::InternedString;
use models::{FromStrParser, PackageId}; use models::{FromStrParser, PackageId};
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async}; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
@@ -155,6 +155,16 @@ pub async fn remove(
for id in ids { for id in ids {
n.remove(&id)?; n.remove(&id)?;
} }
let mut unread = 0;
for (_, n) in n.as_entries()? {
if !n.as_seen().de()? {
unread += 1;
}
}
db.as_public_mut()
.as_server_info_mut()
.as_unread_notification_count_mut()
.ser(&unread)?;
Ok(()) Ok(())
}) })
.await .await
@@ -179,6 +189,16 @@ pub async fn remove_before(
for id in n.keys()?.range(..before) { for id in n.keys()?.range(..before) {
n.remove(&id)?; n.remove(&id)?;
} }
let mut unread = 0;
for (_, n) in n.as_entries()? {
if !n.as_seen().de()? {
unread += 1;
}
}
db.as_public_mut()
.as_server_info_mut()
.as_unread_notification_count_mut()
.ser(&unread)?;
Ok(()) Ok(())
}) })
.await .await

View File

@@ -1,7 +1,7 @@
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use futures::future::{BoxFuture, FusedFuture, abortable, pending}; use futures::future::{abortable, pending, BoxFuture, FusedFuture};
use futures::stream::{AbortHandle, Abortable, BoxStream}; use futures::stream::{AbortHandle, Abortable, BoxStream};
use futures::{Future, FutureExt, Stream, StreamExt}; use futures::{Future, FutureExt, Stream, StreamExt};
use tokio::sync::watch; use tokio::sync::watch;

View File

@@ -1575,7 +1575,7 @@ pub fn file_string_stream(
loop { loop {
match stream.watches().add( match stream.watches().add(
&path, &path,
WatchMask::MODIFY | WatchMask::MOVE_SELF | WatchMask::MOVED_TO | WatchMask::DELETE_SELF, WatchMask::MODIFY | WatchMask::CLOSE_WRITE | WatchMask::MOVE_SELF | WatchMask::MOVED_TO | WatchMask::DELETE_SELF,
) { ) {
Ok(_) => break, Ok(_) => break,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => { Err(e) if e.kind() == std::io::ErrorKind::NotFound => {

View File

@@ -14,10 +14,10 @@ use ::serde::{Deserialize, Serialize};
use async_trait::async_trait; use async_trait::async_trait;
use color_eyre::eyre::{self, eyre}; use color_eyre::eyre::{self, eyre};
use fd_lock_rs::FdLock; use fd_lock_rs::FdLock;
use futures::FutureExt;
use futures::future::BoxFuture; use futures::future::BoxFuture;
pub use helpers::NonDetachingJoinHandle; use futures::FutureExt;
use helpers::canonicalize; use helpers::canonicalize;
pub use helpers::NonDetachingJoinHandle;
use imbl_value::InternedString; use imbl_value::InternedString;
use lazy_static::lazy_static; use lazy_static::lazy_static;
pub use models::VersionString; pub use models::VersionString;
@@ -25,7 +25,7 @@ use pin_project::pin_project;
use sha2::Digest; use sha2::Digest;
use tokio::fs::File; use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncReadExt, BufReader}; use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
use tokio::sync::{Mutex, OwnedMutexGuard, RwLock, oneshot}; use tokio::sync::{oneshot, Mutex, OwnedMutexGuard, RwLock};
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
use url::Url; use url::Url;
@@ -197,17 +197,17 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn invoke(&mut self, error_kind: crate::ErrorKind) -> Result<Vec<u8>, Error> { async fn invoke(&mut self, error_kind: crate::ErrorKind) -> Result<Vec<u8>, Error> {
let cmd_str = self
.cmd
.as_std()
.get_program()
.to_string_lossy()
.into_owned();
self.cmd.kill_on_drop(true); self.cmd.kill_on_drop(true);
if self.input.is_some() { if self.input.is_some() {
self.cmd.stdin(Stdio::piped()); self.cmd.stdin(Stdio::piped());
} }
if self.pipe.is_empty() { if self.pipe.is_empty() {
let cmd_str = self
.cmd
.as_std()
.get_program()
.to_string_lossy()
.into_owned();
if self.capture { if self.capture {
self.cmd.stdout(Stdio::piped()); self.cmd.stdout(Stdio::piped());
self.cmd.stderr(Stdio::piped()); self.cmd.stderr(Stdio::piped());
@@ -256,6 +256,7 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
.take() .take()
.map(|i| Box::new(i) as Box<dyn AsyncRead + Unpin + Send>); .map(|i| Box::new(i) as Box<dyn AsyncRead + Unpin + Send>);
for (idx, cmd) in IntoIterator::into_iter(cmds).enumerate() { for (idx, cmd) in IntoIterator::into_iter(cmds).enumerate() {
let cmd_str = cmd.as_std().get_program().to_string_lossy().into_owned();
let last = idx == len - 1; let last = idx == len - 1;
if self.capture || !last { if self.capture || !last {
cmd.stdout(Stdio::piped()); cmd.stdout(Stdio::piped());

View File

@@ -5,14 +5,14 @@ use std::panic::{RefUnwindSafe, UnwindSafe};
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use futures::future::BoxFuture; use futures::future::BoxFuture;
use futures::{Future, FutureExt}; use futures::{Future, FutureExt};
use imbl_value::{InternedString, to_value}; use imbl_value::{to_value, InternedString};
use patch_db::json_ptr::ROOT; use patch_db::json_ptr::ROOT;
use crate::Error;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::Database; use crate::db::model::Database;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::PhaseProgressTrackerHandle; use crate::progress::PhaseProgressTrackerHandle;
use crate::Error;
mod v0_3_5; mod v0_3_5;
mod v0_3_5_1; mod v0_3_5_1;
@@ -50,8 +50,9 @@ mod v0_4_0_alpha_8;
mod v0_4_0_alpha_9; mod v0_4_0_alpha_9;
mod v0_4_0_alpha_10; mod v0_4_0_alpha_10;
mod v0_4_0_alpha_11;
pub type Current = v0_4_0_alpha_10::Version; // VERSION_BUMP pub type Current = v0_4_0_alpha_11::Version; // VERSION_BUMP
impl Current { impl Current {
#[instrument(skip(self, db))] #[instrument(skip(self, db))]
@@ -164,7 +165,8 @@ enum Version {
V0_4_0_alpha_7(Wrapper<v0_4_0_alpha_7::Version>), V0_4_0_alpha_7(Wrapper<v0_4_0_alpha_7::Version>),
V0_4_0_alpha_8(Wrapper<v0_4_0_alpha_8::Version>), V0_4_0_alpha_8(Wrapper<v0_4_0_alpha_8::Version>),
V0_4_0_alpha_9(Wrapper<v0_4_0_alpha_9::Version>), V0_4_0_alpha_9(Wrapper<v0_4_0_alpha_9::Version>),
V0_4_0_alpha_10(Wrapper<v0_4_0_alpha_10::Version>), // VERSION_BUMP V0_4_0_alpha_10(Wrapper<v0_4_0_alpha_10::Version>),
V0_4_0_alpha_11(Wrapper<v0_4_0_alpha_11::Version>), // VERSION_BUMP
Other(exver::Version), Other(exver::Version),
} }
@@ -217,7 +219,8 @@ impl Version {
Self::V0_4_0_alpha_7(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_7(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_8(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_8(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_9(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_9(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_10(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP Self::V0_4_0_alpha_10(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_11(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::Other(v) => { Self::Other(v) => {
return Err(Error::new( return Err(Error::new(
eyre!("unknown version {v}"), eyre!("unknown version {v}"),
@@ -262,7 +265,8 @@ impl Version {
Version::V0_4_0_alpha_7(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_7(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_8(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_8(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_9(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_9(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_10(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::V0_4_0_alpha_10(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_11(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::Other(x) => x.clone(), Version::Other(x) => x.clone(),
} }
} }

View File

@@ -0,0 +1,37 @@
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{v0_4_0_alpha_10, VersionT};
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_11: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 11.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_10::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_11.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument]
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -61,7 +61,7 @@ PLATFORM_CONFIG_EXTRAS=()
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
PLATFORM_CONFIG_EXTRAS+=( --firmware-binary false ) PLATFORM_CONFIG_EXTRAS+=( --firmware-binary false )
PLATFORM_CONFIG_EXTRAS+=( --firmware-chroot false ) PLATFORM_CONFIG_EXTRAS+=( --firmware-chroot false )
PLATFORM_CONFIG_EXTRAS+=( --linux-packages linux-image-6.12.20+rpt ) PLATFORM_CONFIG_EXTRAS+=( --linux-packages linux-image-6.12.47+rpt )
PLATFORM_CONFIG_EXTRAS+=( --linux-flavours "rpi-v8 rpi-2712" ) PLATFORM_CONFIG_EXTRAS+=( --linux-flavours "rpi-v8 rpi-2712" )
elif [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then elif [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
PLATFORM_CONFIG_EXTRAS+=( --linux-flavours rockchip64 ) PLATFORM_CONFIG_EXTRAS+=( --linux-flavours rockchip64 )
@@ -204,8 +204,8 @@ if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
echo "Configuring raspi kernel '\$v'" echo "Configuring raspi kernel '\$v'"
extract-ikconfig "/usr/lib/modules/\$v/kernel/kernel/configs.ko.xz" > /boot/config-\$v extract-ikconfig "/usr/lib/modules/\$v/kernel/kernel/configs.ko.xz" > /boot/config-\$v
done done
mkinitramfs -c gzip -o /boot/initramfs8 6.12.25-v8+ mkinitramfs -c gzip -o /boot/initramfs8 6.12.47-v8+
mkinitramfs -c gzip -o /boot/initramfs_2712 6.12.25-v8-16k+ mkinitramfs -c gzip -o /boot/initramfs_2712 6.12.47-v8-16k+
fi fi
useradd --shell /bin/bash -G startos -m start9 useradd --shell /bin/bash -G startos -m start9
@@ -231,7 +231,6 @@ lb chroot
lb installer lb installer
lb binary_chroot lb binary_chroot
lb chroot_prep install all mode-apt-install-binary mode-archives-chroot lb chroot_prep install all mode-apt-install-binary mode-archives-chroot
ln -sf /run/systemd/resolve/stub-resolv.conf chroot/chroot/etc/resolv.conf
lb binary_rootfs lb binary_rootfs
cp $prep_results_dir/binary/live/filesystem.squashfs $RESULTS_DIR/$IMAGE_BASENAME.squashfs cp $prep_results_dir/binary/live/filesystem.squashfs $RESULTS_DIR/$IMAGE_BASENAME.squashfs

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DnsSettings = { export type DnsSettings = {
dhcpServers: Array<string> dhcpServers: string[]
staticServers: Array<string> | null staticServers: string[] | null
} }

View File

@@ -61,7 +61,7 @@ import {
} from "../../base/lib/inits" } from "../../base/lib/inits"
import { DropGenerator } from "../../base/lib/util/Drop" import { DropGenerator } from "../../base/lib/util/Drop"
export const OSVersion = testTypeVersion("0.4.0-alpha.10") export const OSVersion = testTypeVersion("0.4.0-alpha.11")
// prettier-ignore // prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> = type AnyNeverCond<T extends any[], Then, Else> =

View File

@@ -164,7 +164,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
readonly started: readonly started:
| ((onTerm: () => PromiseLike<void>) => PromiseLike<null>) | ((onTerm: () => PromiseLike<void>) => PromiseLike<null>)
| null, | null,
readonly daemons: Promise<Daemon<Manifest>>[],
readonly ids: Ids[], readonly ids: Ids[],
readonly healthDaemons: HealthDaemon<Manifest>[], readonly healthDaemons: HealthDaemon<Manifest>[],
) {} ) {}
@@ -193,7 +192,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
options.started, options.started,
[], [],
[], [],
[],
) )
} }
@@ -215,13 +213,11 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
ready, ready,
this.effects, this.effects,
) )
const daemons = daemon ? [...this.daemons, daemon] : [...this.daemons]
const ids = [...this.ids, id] as (Ids | Id)[] const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon] const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>( return new Daemons<Manifest, Ids | Id>(
this.effects, this.effects,
this.started, this.started,
daemons,
ids, ids,
healthDaemons, healthDaemons,
) )
@@ -358,7 +354,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
const prev = this const prev = this
const res = (options: AddHealthCheckParams<Ids, Id> | null) => { const res = (options: AddHealthCheckParams<Ids, Id> | null) => {
if (!options) return prev if (!options) return prev
return prev.addDaemonImpl(id, null, options.requires, EXIT_SUCCESS) return prev.addDaemonImpl(id, null, options.requires, options.ready)
} }
if (options instanceof Function) { if (options instanceof Function) {
const opts = options() const opts = options()
@@ -404,7 +400,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
const daemons = await new Daemons<Manifest, Ids>( const daemons = await new Daemons<Manifest, Ids>(
this.effects, this.effects,
this.started, this.started,
[...this.daemons, daemon],
this.ids, this.ids,
[...this.healthDaemons, healthDaemon], [...this.healthDaemons, healthDaemon],
).build() ).build()

View File

@@ -1,12 +1,12 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.40", "version": "0.4.0-beta.41",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.40", "version": "0.4.0-beta.41",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.40", "version": "0.4.0-beta.41",
"description": "Software development kit to facilitate packaging services for StartOS", "description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js", "main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts", "types": "./package/lib/index.d.ts",

8693
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "startos-ui", "name": "startos-ui",
"version": "0.4.0-alpha.10", "version": "0.4.0-alpha.11",
"author": "Start9 Labs, Inc", "author": "Start9 Labs, Inc",
"homepage": "https://start9.com/", "homepage": "https://start9.com/",
"license": "MIT", "license": "MIT",

View File

@@ -110,7 +110,7 @@ export namespace Mock {
squashfs: { squashfs: {
aarch64: { aarch64: {
publishedAt: '2025-04-21T20:58:48.140749883Z', publishedAt: '2025-04-21T20:58:48.140749883Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_aarch64.squashfs', url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.11/startos-0.4.0-alpha.11-33ae46f~dev_aarch64.squashfs',
commitment: { commitment: {
hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=', hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=',
size: 1343500288, size: 1343500288,
@@ -122,7 +122,7 @@ export namespace Mock {
}, },
'aarch64-nonfree': { 'aarch64-nonfree': {
publishedAt: '2025-04-21T21:07:00.249285116Z', publishedAt: '2025-04-21T21:07:00.249285116Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_aarch64-nonfree.squashfs', url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.11/startos-0.4.0-alpha.11-33ae46f~dev_aarch64-nonfree.squashfs',
commitment: { commitment: {
hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=', hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=',
size: 1653075968, size: 1653075968,
@@ -134,7 +134,7 @@ export namespace Mock {
}, },
raspberrypi: { raspberrypi: {
publishedAt: '2025-04-21T21:16:12.933319237Z', publishedAt: '2025-04-21T21:16:12.933319237Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_raspberrypi.squashfs', url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.11/startos-0.4.0-alpha.11-33ae46f~dev_raspberrypi.squashfs',
commitment: { commitment: {
hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=', hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=',
size: 1490731008, size: 1490731008,
@@ -146,7 +146,7 @@ export namespace Mock {
}, },
x86_64: { x86_64: {
publishedAt: '2025-04-21T21:14:20.246908903Z', publishedAt: '2025-04-21T21:14:20.246908903Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_x86_64.squashfs', url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.11/startos-0.4.0-alpha.11-33ae46f~dev_x86_64.squashfs',
commitment: { commitment: {
hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=', hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=',
size: 1411657728, size: 1411657728,
@@ -158,7 +158,7 @@ export namespace Mock {
}, },
'x86_64-nonfree': { 'x86_64-nonfree': {
publishedAt: '2025-04-21T21:15:17.955265284Z', publishedAt: '2025-04-21T21:15:17.955265284Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_x86_64-nonfree.squashfs', url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.11/startos-0.4.0-alpha.11-33ae46f~dev_x86_64-nonfree.squashfs',
commitment: { commitment: {
hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=', hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=',
size: 1731035136, size: 1731035136,
@@ -385,7 +385,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org', docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.40', sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -420,7 +420,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org', docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.40', sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -465,7 +465,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org', docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.40', sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -500,7 +500,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org', docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.40', sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -547,7 +547,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/', docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.5', releaseNotes: 'Upstream release to 0.17.5',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.40', sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: LND_ICON, icon: LND_ICON,
sourceVersion: null, sourceVersion: null,
@@ -595,7 +595,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/', docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.4', releaseNotes: 'Upstream release to 0.17.4',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.40', sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: LND_ICON, icon: LND_ICON,
sourceVersion: null, sourceVersion: null,
@@ -647,7 +647,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org', docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.40', sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -682,7 +682,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org', docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.40', sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -727,7 +727,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/', docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release and minor fixes.', releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.40', sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: LND_ICON, icon: LND_ICON,
sourceVersion: null, sourceVersion: null,
@@ -775,7 +775,7 @@ export namespace Mock {
marketingSite: '', marketingSite: '',
releaseNotes: 'Upstream release and minor fixes.', releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.40', sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: PROXY_ICON, icon: PROXY_ICON,
sourceVersion: null, sourceVersion: null,