mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
feat: refactor NetService to watch DB and reconcile network state
- NetService sync task now uses PatchDB DbWatch instead of being called directly after DB mutations - Read gateways from DB instead of network interface context when updating host addresses - gateway sync updates all host addresses in the DB - Add Watch<u64> channel for callers to wait on sync completion - Fix ts-rs codegen bug with #[ts(skip)] on flattened Plugin field - Update SDK getServiceInterface.ts for new HostnameInfo shape - Remove unnecessary HTTPS redirect in static_server.rs - Fix tunnel/api.rs to filter for WAN IPv4 address
This commit is contained in:
@@ -32,6 +32,7 @@ use crate::context::{CliContext, RpcContext};
|
|||||||
use crate::db::model::Database;
|
use crate::db::model::Database;
|
||||||
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
|
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
|
||||||
use crate::net::forward::START9_BRIDGE_IFACE;
|
use crate::net::forward::START9_BRIDGE_IFACE;
|
||||||
|
use crate::net::host::all_hosts;
|
||||||
use crate::net::gateway::device::DeviceProxy;
|
use crate::net::gateway::device::DeviceProxy;
|
||||||
use crate::net::web_server::{Accept, AcceptStream, MetadataVisitor, TcpMetadata};
|
use crate::net::web_server::{Accept, AcceptStream, MetadataVisitor, TcpMetadata};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -1003,7 +1004,12 @@ impl NetworkInterfaceController {
|
|||||||
.as_server_info_mut()
|
.as_server_info_mut()
|
||||||
.as_network_mut()
|
.as_network_mut()
|
||||||
.as_gateways_mut()
|
.as_gateways_mut()
|
||||||
.ser(info)
|
.ser(info)?;
|
||||||
|
let ports = db.as_private().as_available_ports().de()?;
|
||||||
|
for host in all_hosts(db) {
|
||||||
|
host?.update_addresses(info, &ports)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -193,7 +193,10 @@ pub async fn add_public_domain<Kind: HostApiKind>(
|
|||||||
Kind::host_for(&inheritance, db)?
|
Kind::host_for(&inheritance, db)?
|
||||||
.as_public_domains_mut()
|
.as_public_domains_mut()
|
||||||
.insert(&fqdn, &PublicDomainConfig { acme, gateway })?;
|
.insert(&fqdn, &PublicDomainConfig { acme, gateway })?;
|
||||||
handle_duplicates(db)
|
handle_duplicates(db)?;
|
||||||
|
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
|
||||||
|
let ports = db.as_private().as_available_ports().de()?;
|
||||||
|
Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
@@ -221,7 +224,10 @@ pub async fn remove_public_domain<Kind: HostApiKind>(
|
|||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
Kind::host_for(&inheritance, db)?
|
Kind::host_for(&inheritance, db)?
|
||||||
.as_public_domains_mut()
|
.as_public_domains_mut()
|
||||||
.remove(&fqdn)
|
.remove(&fqdn)?;
|
||||||
|
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
|
||||||
|
let ports = db.as_private().as_available_ports().de()?;
|
||||||
|
Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
@@ -248,7 +254,10 @@ pub async fn add_private_domain<Kind: HostApiKind>(
|
|||||||
.as_private_domains_mut()
|
.as_private_domains_mut()
|
||||||
.upsert(&fqdn, || Ok(BTreeSet::new()))?
|
.upsert(&fqdn, || Ok(BTreeSet::new()))?
|
||||||
.mutate(|d| Ok(d.insert(gateway)))?;
|
.mutate(|d| Ok(d.insert(gateway)))?;
|
||||||
handle_duplicates(db)
|
handle_duplicates(db)?;
|
||||||
|
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
|
||||||
|
let ports = db.as_private().as_available_ports().de()?;
|
||||||
|
Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
@@ -266,7 +275,10 @@ pub async fn remove_private_domain<Kind: HostApiKind>(
|
|||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
Kind::host_for(&inheritance, db)?
|
Kind::host_for(&inheritance, db)?
|
||||||
.as_private_domains_mut()
|
.as_private_domains_mut()
|
||||||
.mutate(|d| Ok(d.remove(&domain)))
|
.mutate(|d| Ok(d.remove(&domain)))?;
|
||||||
|
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
|
||||||
|
let ports = db.as_private().as_available_ports().de()?;
|
||||||
|
Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -3,17 +3,15 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
|||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use imbl::vector;
|
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use ipnet::IpNet;
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio_rustls::rustls::ClientConfig as TlsClientConfig;
|
use tokio_rustls::rustls::ClientConfig as TlsClientConfig;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use patch_db::json_ptr::JsonPointer;
|
||||||
|
|
||||||
use crate::db::model::Database;
|
use crate::db::model::Database;
|
||||||
use crate::db::model::public::NetworkInterfaceType;
|
|
||||||
use crate::error::ErrorCollection;
|
|
||||||
use crate::hostname::Hostname;
|
use crate::hostname::Hostname;
|
||||||
use crate::net::dns::DnsController;
|
use crate::net::dns::DnsController;
|
||||||
use crate::net::forward::{
|
use crate::net::forward::{
|
||||||
@@ -23,13 +21,13 @@ use crate::net::gateway::NetworkInterfaceController;
|
|||||||
use crate::net::host::address::HostAddress;
|
use crate::net::host::address::HostAddress;
|
||||||
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
|
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
|
||||||
use crate::net::host::{Host, Hosts, host_for};
|
use crate::net::host::{Host, Hosts, host_for};
|
||||||
use crate::net::service_interface::{HostnameInfo, HostnameMetadata};
|
use crate::net::service_interface::HostnameMetadata;
|
||||||
use crate::net::socks::SocksController;
|
use crate::net::socks::SocksController;
|
||||||
use crate::net::utils::ipv6_is_local;
|
|
||||||
use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController};
|
use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::service::effects::callbacks::ServiceCallbacks;
|
use crate::service::effects::callbacks::ServiceCallbacks;
|
||||||
use crate::util::serde::MaybeUtf8String;
|
use crate::util::serde::MaybeUtf8String;
|
||||||
|
use crate::util::sync::Watch;
|
||||||
use crate::{GatewayId, HOST_IP, HostId, OptionExt, PackageId};
|
use crate::{GatewayId, HOST_IP, HostId, OptionExt, PackageId};
|
||||||
|
|
||||||
pub struct NetController {
|
pub struct NetController {
|
||||||
@@ -182,79 +180,11 @@ impl NetServiceData {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn clear_bindings(
|
|
||||||
&mut self,
|
|
||||||
ctrl: &NetController,
|
|
||||||
except: BTreeSet<BindId>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
if let Some(pkg_id) = &self.id {
|
|
||||||
let hosts = ctrl
|
|
||||||
.db
|
|
||||||
.mutate(|db| {
|
|
||||||
let mut res = Hosts::default();
|
|
||||||
for (host_id, host) in db
|
|
||||||
.as_public_mut()
|
|
||||||
.as_package_data_mut()
|
|
||||||
.as_idx_mut(pkg_id)
|
|
||||||
.or_not_found(pkg_id)?
|
|
||||||
.as_hosts_mut()
|
|
||||||
.as_entries_mut()?
|
|
||||||
{
|
|
||||||
host.as_bindings_mut().mutate(|b| {
|
|
||||||
for (internal_port, info) in b.iter_mut() {
|
|
||||||
if !except.contains(&BindId {
|
|
||||||
id: host_id.clone(),
|
|
||||||
internal_port: *internal_port,
|
|
||||||
}) {
|
|
||||||
info.disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
res.0.insert(host_id, host.de()?);
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.result?;
|
|
||||||
let mut errors = ErrorCollection::new();
|
|
||||||
for (id, host) in hosts.0 {
|
|
||||||
errors.handle(self.update(ctrl, id, host).await);
|
|
||||||
}
|
|
||||||
errors.into_result()
|
|
||||||
} else {
|
|
||||||
let host = ctrl
|
|
||||||
.db
|
|
||||||
.mutate(|db| {
|
|
||||||
let host = db
|
|
||||||
.as_public_mut()
|
|
||||||
.as_server_info_mut()
|
|
||||||
.as_network_mut()
|
|
||||||
.as_host_mut();
|
|
||||||
host.as_bindings_mut().mutate(|b| {
|
|
||||||
for (internal_port, info) in b.iter_mut() {
|
|
||||||
if !except.contains(&BindId {
|
|
||||||
id: HostId::default(),
|
|
||||||
internal_port: *internal_port,
|
|
||||||
}) {
|
|
||||||
info.disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
host.de()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.result?;
|
|
||||||
self.update(ctrl, HostId::default(), host).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update(
|
async fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
ctrl: &NetController,
|
ctrl: &NetController,
|
||||||
id: HostId,
|
id: HostId,
|
||||||
mut host: Host,
|
host: Host,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut forwards: BTreeMap<u16, (SocketAddrV4, ForwardRequirements)> = BTreeMap::new();
|
let mut forwards: BTreeMap<u16, (SocketAddrV4, ForwardRequirements)> = BTreeMap::new();
|
||||||
let mut vhosts: BTreeMap<(Option<InternedString>, u16), ProxyTarget> = BTreeMap::new();
|
let mut vhosts: BTreeMap<(Option<InternedString>, u16), ProxyTarget> = BTreeMap::new();
|
||||||
@@ -274,200 +204,7 @@ impl NetServiceData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Phase 1: Compute available addresses ──
|
// ── Build controller entries from enabled addresses ──
|
||||||
for (_port, bind) in host.bindings.iter_mut() {
|
|
||||||
if !bind.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if bind.net.assigned_port.is_none() && bind.net.assigned_ssl_port.is_none() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bind.addresses.available.clear();
|
|
||||||
|
|
||||||
// Domain port: non-SSL port for domains (secure-filtered, gateway-independent)
|
|
||||||
let domain_base_port = bind.net.assigned_port.filter(|_| {
|
|
||||||
bind.options
|
|
||||||
.secure
|
|
||||||
.map_or(false, |s| !s.ssl || bind.options.add_ssl.is_none())
|
|
||||||
});
|
|
||||||
let (domain_port, domain_ssl_port) = if bind
|
|
||||||
.options
|
|
||||||
.add_ssl
|
|
||||||
.as_ref()
|
|
||||||
.map_or(false, |ssl| ssl.preferred_external_port == 443)
|
|
||||||
{
|
|
||||||
(None, Some(443))
|
|
||||||
} else {
|
|
||||||
(domain_base_port, bind.net.assigned_ssl_port)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Domain addresses
|
|
||||||
for HostAddress {
|
|
||||||
address, public, ..
|
|
||||||
} in host_addresses.iter().cloned()
|
|
||||||
{
|
|
||||||
// Public domain entry
|
|
||||||
if let Some(pub_config) = &public {
|
|
||||||
let metadata = HostnameMetadata::PublicDomain {
|
|
||||||
gateway: pub_config.gateway.clone(),
|
|
||||||
};
|
|
||||||
if let Some(p) = domain_port {
|
|
||||||
bind.addresses.available.insert(HostnameInfo {
|
|
||||||
ssl: false,
|
|
||||||
public: true,
|
|
||||||
host: address.clone(),
|
|
||||||
port: Some(p),
|
|
||||||
metadata: metadata.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(sp) = domain_ssl_port {
|
|
||||||
bind.addresses.available.insert(HostnameInfo {
|
|
||||||
ssl: true,
|
|
||||||
public: true,
|
|
||||||
host: address.clone(),
|
|
||||||
port: Some(sp),
|
|
||||||
metadata,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Private domain entry
|
|
||||||
if let Some(gateways) = host.private_domains.get(&address) {
|
|
||||||
if !gateways.is_empty() {
|
|
||||||
let metadata = HostnameMetadata::PrivateDomain {
|
|
||||||
gateways: gateways.clone(),
|
|
||||||
};
|
|
||||||
if let Some(p) = domain_port {
|
|
||||||
bind.addresses.available.insert(HostnameInfo {
|
|
||||||
ssl: false,
|
|
||||||
public: false,
|
|
||||||
host: address.clone(),
|
|
||||||
port: Some(p),
|
|
||||||
metadata: metadata.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(sp) = domain_ssl_port {
|
|
||||||
bind.addresses.available.insert(HostnameInfo {
|
|
||||||
ssl: true,
|
|
||||||
public: false,
|
|
||||||
host: address.clone(),
|
|
||||||
port: Some(sp),
|
|
||||||
metadata,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IP addresses (per-gateway)
|
|
||||||
for (gateway_id, info) in net_ifaces
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, info)| {
|
|
||||||
info.ip_info.as_ref().map_or(false, |i| {
|
|
||||||
!matches!(i.device_type, Some(NetworkInterfaceType::Bridge))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.filter(|(_, info)| info.ip_info.is_some())
|
|
||||||
{
|
|
||||||
let port = bind.net.assigned_port.filter(|_| {
|
|
||||||
bind.options.secure.map_or(false, |s| {
|
|
||||||
!(s.ssl && bind.options.add_ssl.is_some()) || info.secure()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(ip_info) = &info.ip_info {
|
|
||||||
let public = info.public();
|
|
||||||
// WAN IP (public)
|
|
||||||
if let Some(wan_ip) = ip_info.wan_ip {
|
|
||||||
let host_str = InternedString::from_display(&wan_ip);
|
|
||||||
if let Some(p) = port {
|
|
||||||
bind.addresses.available.insert(HostnameInfo {
|
|
||||||
ssl: false,
|
|
||||||
public: true,
|
|
||||||
host: host_str.clone(),
|
|
||||||
port: Some(p),
|
|
||||||
metadata: HostnameMetadata::Ipv4 {
|
|
||||||
gateway: gateway_id.clone(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(sp) = bind.net.assigned_ssl_port {
|
|
||||||
bind.addresses.available.insert(HostnameInfo {
|
|
||||||
ssl: true,
|
|
||||||
public: true,
|
|
||||||
host: host_str,
|
|
||||||
port: Some(sp),
|
|
||||||
metadata: HostnameMetadata::Ipv4 {
|
|
||||||
gateway: gateway_id.clone(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Subnet IPs
|
|
||||||
for ipnet in &ip_info.subnets {
|
|
||||||
match ipnet {
|
|
||||||
IpNet::V4(net) => {
|
|
||||||
if !public {
|
|
||||||
let host_str = InternedString::from_display(&net.addr());
|
|
||||||
if let Some(p) = port {
|
|
||||||
bind.addresses.available.insert(HostnameInfo {
|
|
||||||
ssl: false,
|
|
||||||
public: false,
|
|
||||||
host: host_str.clone(),
|
|
||||||
port: Some(p),
|
|
||||||
metadata: HostnameMetadata::Ipv4 {
|
|
||||||
gateway: gateway_id.clone(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(sp) = bind.net.assigned_ssl_port {
|
|
||||||
bind.addresses.available.insert(HostnameInfo {
|
|
||||||
ssl: true,
|
|
||||||
public: false,
|
|
||||||
host: host_str,
|
|
||||||
port: Some(sp),
|
|
||||||
metadata: HostnameMetadata::Ipv4 {
|
|
||||||
gateway: gateway_id.clone(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IpNet::V6(net) => {
|
|
||||||
let is_public = public && !ipv6_is_local(net.addr());
|
|
||||||
let host_str = InternedString::from_display(&net.addr());
|
|
||||||
if let Some(p) = port {
|
|
||||||
bind.addresses.available.insert(HostnameInfo {
|
|
||||||
ssl: false,
|
|
||||||
public: is_public,
|
|
||||||
host: host_str.clone(),
|
|
||||||
port: Some(p),
|
|
||||||
metadata: HostnameMetadata::Ipv6 {
|
|
||||||
gateway: gateway_id.clone(),
|
|
||||||
scope_id: ip_info.scope_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(sp) = bind.net.assigned_ssl_port {
|
|
||||||
bind.addresses.available.insert(HostnameInfo {
|
|
||||||
ssl: true,
|
|
||||||
public: is_public,
|
|
||||||
host: host_str,
|
|
||||||
port: Some(sp),
|
|
||||||
metadata: HostnameMetadata::Ipv6 {
|
|
||||||
gateway: gateway_id.clone(),
|
|
||||||
scope_id: ip_info.scope_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Phase 2: Build controller entries from enabled addresses ──
|
|
||||||
for (port, bind) in host.bindings.iter() {
|
for (port, bind) in host.bindings.iter() {
|
||||||
if !bind.enabled {
|
if !bind.enabled {
|
||||||
continue;
|
continue;
|
||||||
@@ -685,74 +422,16 @@ impl NetServiceData {
|
|||||||
}
|
}
|
||||||
ctrl.dns.gc_private_domains(&rm)?;
|
ctrl.dns.gc_private_domains(&rm)?;
|
||||||
|
|
||||||
let res = ctrl
|
|
||||||
.db
|
|
||||||
.mutate(|db| {
|
|
||||||
let bindings = host_for(db, self.id.as_ref(), &id)?.as_bindings_mut();
|
|
||||||
for (port, bind) in host.bindings.0 {
|
|
||||||
if let Some(b) = bindings.as_idx_mut(&port) {
|
|
||||||
b.as_addresses_mut()
|
|
||||||
.as_available_mut()
|
|
||||||
.ser(&bind.addresses.available)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
res.result?;
|
|
||||||
if let Some(pkg_id) = self.id.as_ref() {
|
|
||||||
if res.revision.is_some() {
|
|
||||||
if let Some(cbs) = ctrl.callbacks.get_host_info(&(pkg_id.clone(), id)) {
|
|
||||||
cbs.call(vector![]).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_all(&mut self) -> Result<(), Error> {
|
|
||||||
let ctrl = self.net_controller()?;
|
|
||||||
if let Some(id) = self.id.clone() {
|
|
||||||
for (host_id, host) in ctrl
|
|
||||||
.db
|
|
||||||
.peek()
|
|
||||||
.await
|
|
||||||
.as_public()
|
|
||||||
.as_package_data()
|
|
||||||
.as_idx(&id)
|
|
||||||
.or_not_found(&id)?
|
|
||||||
.as_hosts()
|
|
||||||
.as_entries()?
|
|
||||||
{
|
|
||||||
tracing::info!("Updating host {host_id} for {id}");
|
|
||||||
self.update(&*ctrl, host_id.clone(), host.de()?).await?;
|
|
||||||
tracing::info!("Updated host {host_id} for {id}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tracing::info!("Updating host for Main UI");
|
|
||||||
self.update(
|
|
||||||
&*ctrl,
|
|
||||||
HostId::default(),
|
|
||||||
ctrl.db
|
|
||||||
.peek()
|
|
||||||
.await
|
|
||||||
.as_public()
|
|
||||||
.as_server_info()
|
|
||||||
.as_network()
|
|
||||||
.as_host()
|
|
||||||
.de()?,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
tracing::info!("Updated host for Main UI");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NetService {
|
pub struct NetService {
|
||||||
shutdown: bool,
|
shutdown: bool,
|
||||||
data: Arc<Mutex<NetServiceData>>,
|
data: Arc<Mutex<NetServiceData>>,
|
||||||
sync_task: JoinHandle<()>,
|
sync_task: JoinHandle<()>,
|
||||||
|
synced: Watch<u64>,
|
||||||
}
|
}
|
||||||
impl NetService {
|
impl NetService {
|
||||||
fn dummy() -> Self {
|
fn dummy() -> Self {
|
||||||
@@ -766,26 +445,79 @@ impl NetService {
|
|||||||
binds: BTreeMap::new(),
|
binds: BTreeMap::new(),
|
||||||
})),
|
})),
|
||||||
sync_task: tokio::spawn(futures::future::ready(())),
|
sync_task: tokio::spawn(futures::future::ready(())),
|
||||||
|
synced: Watch::new(0u64),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(data: NetServiceData) -> Result<Self, Error> {
|
fn new(data: NetServiceData) -> Result<Self, Error> {
|
||||||
let mut ip_info = data.net_controller()?.net_iface.watcher.subscribe();
|
let ctrl = data.net_controller()?;
|
||||||
|
let pkg_id = data.id.clone();
|
||||||
|
let db = ctrl.db.clone();
|
||||||
|
drop(ctrl);
|
||||||
|
|
||||||
|
let synced = Watch::new(0u64);
|
||||||
|
let synced_writer = synced.clone();
|
||||||
|
|
||||||
let data = Arc::new(Mutex::new(data));
|
let data = Arc::new(Mutex::new(data));
|
||||||
let thread_data = data.clone();
|
let thread_data = data.clone();
|
||||||
|
|
||||||
let sync_task = tokio::spawn(async move {
|
let sync_task = tokio::spawn(async move {
|
||||||
loop {
|
if let Some(ref id) = pkg_id {
|
||||||
if let Err(e) = thread_data.lock().await.update_all().await {
|
let ptr: JsonPointer = format!("/public/packageData/{}/hosts", id)
|
||||||
tracing::error!("Failed to update network info: {e}");
|
.parse()
|
||||||
tracing::debug!("{e:?}");
|
.unwrap();
|
||||||
|
let mut watch = db.watch(ptr).await.typed::<Hosts>();
|
||||||
|
loop {
|
||||||
|
if let Err(e) = watch.changed().await {
|
||||||
|
tracing::error!("DB watch disconnected for {id}: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Err(e) = async {
|
||||||
|
let hosts = watch.peek()?.de()?;
|
||||||
|
let mut data = thread_data.lock().await;
|
||||||
|
let ctrl = data.net_controller()?;
|
||||||
|
for (host_id, host) in hosts.0 {
|
||||||
|
data.update(&*ctrl, host_id, host).await?;
|
||||||
|
}
|
||||||
|
Ok::<_, Error>(())
|
||||||
|
}
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!("Failed to update network info for {id}: {e}");
|
||||||
|
tracing::debug!("{e:?}");
|
||||||
|
}
|
||||||
|
synced_writer.send_modify(|v| *v += 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let ptr: JsonPointer = "/public/serverInfo/network/host".parse().unwrap();
|
||||||
|
let mut watch = db.watch(ptr).await.typed::<Host>();
|
||||||
|
loop {
|
||||||
|
if let Err(e) = watch.changed().await {
|
||||||
|
tracing::error!("DB watch disconnected for Main UI: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Err(e) = async {
|
||||||
|
let host = watch.peek()?.de()?;
|
||||||
|
let mut data = thread_data.lock().await;
|
||||||
|
let ctrl = data.net_controller()?;
|
||||||
|
data.update(&*ctrl, HostId::default(), host).await?;
|
||||||
|
Ok::<_, Error>(())
|
||||||
|
}
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!("Failed to update network info for Main UI: {e}");
|
||||||
|
tracing::debug!("{e:?}");
|
||||||
|
}
|
||||||
|
synced_writer.send_modify(|v| *v += 1);
|
||||||
}
|
}
|
||||||
ip_info.changed().await;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
shutdown: false,
|
shutdown: false,
|
||||||
data,
|
data,
|
||||||
sync_task,
|
sync_task,
|
||||||
|
synced,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,60 +527,113 @@ impl NetService {
|
|||||||
internal_port: u16,
|
internal_port: u16,
|
||||||
options: BindOptions,
|
options: BindOptions,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut data = self.data.lock().await;
|
let (ctrl, pkg_id) = {
|
||||||
let pkg_id = &data.id;
|
let data = self.data.lock().await;
|
||||||
let ctrl = data.net_controller()?;
|
(data.net_controller()?, data.id.clone())
|
||||||
let host = ctrl
|
};
|
||||||
.db
|
ctrl.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
|
let gateways = db
|
||||||
|
.as_public()
|
||||||
|
.as_server_info()
|
||||||
|
.as_network()
|
||||||
|
.as_gateways()
|
||||||
|
.de()?;
|
||||||
let mut ports = db.as_private().as_available_ports().de()?;
|
let mut ports = db.as_private().as_available_ports().de()?;
|
||||||
let host = host_for(db, pkg_id.as_ref(), &id)?;
|
let host = host_for(db, pkg_id.as_ref(), &id)?;
|
||||||
host.add_binding(&mut ports, internal_port, options)?;
|
host.add_binding(&mut ports, internal_port, options)?;
|
||||||
let host = host.de()?;
|
host.update_addresses(&gateways, &ports)?;
|
||||||
db.as_private_mut().as_available_ports_mut().ser(&ports)?;
|
db.as_private_mut().as_available_ports_mut().ser(&ports)?;
|
||||||
Ok(host)
|
Ok(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result
|
||||||
data.update(&*ctrl, id, host).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear_bindings(&self, except: BTreeSet<BindId>) -> Result<(), Error> {
|
pub async fn clear_bindings(&self, except: BTreeSet<BindId>) -> Result<(), Error> {
|
||||||
let mut data = self.data.lock().await;
|
let (ctrl, pkg_id) = {
|
||||||
let ctrl = data.net_controller()?;
|
let data = self.data.lock().await;
|
||||||
data.clear_bindings(&*ctrl, except).await
|
(data.net_controller()?, data.id.clone())
|
||||||
|
};
|
||||||
|
ctrl.db
|
||||||
|
.mutate(|db| {
|
||||||
|
let gateways = db
|
||||||
|
.as_public()
|
||||||
|
.as_server_info()
|
||||||
|
.as_network()
|
||||||
|
.as_gateways()
|
||||||
|
.de()?;
|
||||||
|
let ports = db.as_private().as_available_ports().de()?;
|
||||||
|
if let Some(ref pkg_id) = pkg_id {
|
||||||
|
for (host_id, host) in db
|
||||||
|
.as_public_mut()
|
||||||
|
.as_package_data_mut()
|
||||||
|
.as_idx_mut(pkg_id)
|
||||||
|
.or_not_found(pkg_id)?
|
||||||
|
.as_hosts_mut()
|
||||||
|
.as_entries_mut()?
|
||||||
|
{
|
||||||
|
host.as_bindings_mut().mutate(|b| {
|
||||||
|
for (internal_port, info) in b.iter_mut() {
|
||||||
|
if !except.contains(&BindId {
|
||||||
|
id: host_id.clone(),
|
||||||
|
internal_port: *internal_port,
|
||||||
|
}) {
|
||||||
|
info.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
host.update_addresses(&gateways, &ports)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let host = db
|
||||||
|
.as_public_mut()
|
||||||
|
.as_server_info_mut()
|
||||||
|
.as_network_mut()
|
||||||
|
.as_host_mut();
|
||||||
|
host.as_bindings_mut().mutate(|b| {
|
||||||
|
for (internal_port, info) in b.iter_mut() {
|
||||||
|
if !except.contains(&BindId {
|
||||||
|
id: HostId::default(),
|
||||||
|
internal_port: *internal_port,
|
||||||
|
}) {
|
||||||
|
info.disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
host.update_addresses(&gateways, &ports)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(&self, id: HostId, host: Host) -> Result<(), Error> {
|
pub async fn sync_host(&self, _id: HostId) -> Result<(), Error> {
|
||||||
let mut data = self.data.lock().await;
|
let current = self.synced.peek(|v| *v);
|
||||||
let ctrl = data.net_controller()?;
|
let mut w = self.synced.clone();
|
||||||
data.update(&*ctrl, id, host).await
|
w.wait_for(|v| *v > current).await;
|
||||||
}
|
Ok(())
|
||||||
|
|
||||||
pub async fn sync_host(&self, id: HostId) -> Result<(), Error> {
|
|
||||||
let mut data = self.data.lock().await;
|
|
||||||
let ctrl = data.net_controller()?;
|
|
||||||
let host = host_for(&mut ctrl.db.peek().await, data.id.as_ref(), &id)?.de()?;
|
|
||||||
data.update(&*ctrl, id, host).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_all(mut self) -> Result<(), Error> {
|
pub async fn remove_all(mut self) -> Result<(), Error> {
|
||||||
self.sync_task.abort();
|
if Weak::upgrade(&self.data.lock().await.controller).is_none() {
|
||||||
let mut data = self.data.lock().await;
|
|
||||||
if let Some(ctrl) = Weak::upgrade(&data.controller) {
|
|
||||||
self.shutdown = true;
|
|
||||||
data.clear_bindings(&*ctrl, Default::default()).await?;
|
|
||||||
|
|
||||||
drop(ctrl);
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
self.shutdown = true;
|
self.shutdown = true;
|
||||||
tracing::warn!("NetService dropped after NetController is shutdown");
|
tracing::warn!("NetService dropped after NetController is shutdown");
|
||||||
Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("NetController is shutdown"),
|
eyre!("NetController is shutdown"),
|
||||||
crate::ErrorKind::Network,
|
crate::ErrorKind::Network,
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
|
let current = self.synced.peek(|v| *v);
|
||||||
|
self.clear_bindings(Default::default()).await?;
|
||||||
|
let mut w = self.synced.clone();
|
||||||
|
w.wait_for(|v| *v > current).await;
|
||||||
|
self.sync_task.abort();
|
||||||
|
self.shutdown = true;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_ip(&self) -> Ipv4Addr {
|
pub async fn get_ip(&self) -> Ipv4Addr {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ pub enum HostnameMetadata {
|
|||||||
Plugin {
|
Plugin {
|
||||||
package: PackageId,
|
package: PackageId,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
|
#[ts(skip)]
|
||||||
extra: InOMap<InternedString, Value>,
|
extra: InOMap<InternedString, Value>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ use async_compression::tokio::bufread::GzipEncoder;
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::{self as x, Request};
|
use axum::extract::{self as x, Request};
|
||||||
use axum::response::{IntoResponse, Redirect, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::routing::{any, get};
|
use axum::routing::{any, get};
|
||||||
use base64::display::Base64Display;
|
use base64::display::Base64Display;
|
||||||
use digest::Digest;
|
use digest::Digest;
|
||||||
use futures::future::ready;
|
use futures::future::ready;
|
||||||
use http::header::{
|
use http::header::{
|
||||||
ACCEPT_ENCODING, ACCEPT_RANGES, CACHE_CONTROL, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH,
|
ACCEPT_ENCODING, ACCEPT_RANGES, CACHE_CONTROL, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH,
|
||||||
CONTENT_RANGE, CONTENT_TYPE, ETAG, HOST, RANGE,
|
CONTENT_RANGE, CONTENT_TYPE, ETAG, RANGE,
|
||||||
};
|
};
|
||||||
use http::request::Parts as RequestParts;
|
use http::request::Parts as RequestParts;
|
||||||
use http::{HeaderValue, Method, StatusCode};
|
use http::{HeaderValue, Method, StatusCode};
|
||||||
@@ -36,8 +36,6 @@ use crate::middleware::auth::Auth;
|
|||||||
use crate::middleware::auth::session::ValidSessionToken;
|
use crate::middleware::auth::session::ValidSessionToken;
|
||||||
use crate::middleware::cors::Cors;
|
use crate::middleware::cors::Cors;
|
||||||
use crate::middleware::db::SyncDb;
|
use crate::middleware::db::SyncDb;
|
||||||
use crate::net::gateway::GatewayInfo;
|
|
||||||
use crate::net::tls::TlsHandshakeInfo;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::rpc_continuations::{Guid, RpcContinuations};
|
use crate::rpc_continuations::{Guid, RpcContinuations};
|
||||||
use crate::s9pk::S9pk;
|
use crate::s9pk::S9pk;
|
||||||
@@ -89,30 +87,6 @@ impl UiContext for RpcContext {
|
|||||||
.middleware(SyncDb::new())
|
.middleware(SyncDb::new())
|
||||||
}
|
}
|
||||||
fn extend_router(self, router: Router) -> Router {
|
fn extend_router(self, router: Router) -> Router {
|
||||||
async fn https_redirect_if_public_http(
|
|
||||||
req: Request,
|
|
||||||
next: axum::middleware::Next,
|
|
||||||
) -> Response {
|
|
||||||
if req
|
|
||||||
.extensions()
|
|
||||||
.get::<GatewayInfo>()
|
|
||||||
.map_or(false, |p| p.info.public())
|
|
||||||
&& req.extensions().get::<TlsHandshakeInfo>().is_none()
|
|
||||||
{
|
|
||||||
Redirect::temporary(&format!(
|
|
||||||
"https://{}{}",
|
|
||||||
req.headers()
|
|
||||||
.get(HOST)
|
|
||||||
.and_then(|s| s.to_str().ok())
|
|
||||||
.unwrap_or("localhost"),
|
|
||||||
req.uri()
|
|
||||||
))
|
|
||||||
.into_response()
|
|
||||||
} else {
|
|
||||||
next.run(req).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
router
|
router
|
||||||
.route("/proxy/{url}", {
|
.route("/proxy/{url}", {
|
||||||
let ctx = self.clone();
|
let ctx = self.clone();
|
||||||
@@ -136,7 +110,6 @@ impl UiContext for RpcContext {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.layer(axum::middleware::from_fn(https_redirect_if_public_http))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,10 +175,13 @@ pub async fn remove_tunnel(
|
|||||||
|
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
|
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
|
||||||
|
let ports = db.as_private().as_available_ports().de()?;
|
||||||
for host in all_hosts(db) {
|
for host in all_hosts(db) {
|
||||||
let host = host?;
|
let host = host?;
|
||||||
host.as_public_domains_mut()
|
host.as_public_domains_mut()
|
||||||
.mutate(|p| Ok(p.retain(|_, v| v.gateway != id)))?;
|
.mutate(|p| Ok(p.retain(|_, v| v.gateway != id)))?;
|
||||||
|
host.update_addresses(&gateways, &ports)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -190,6 +193,8 @@ pub async fn remove_tunnel(
|
|||||||
|
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
|
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
|
||||||
|
let ports = db.as_private().as_available_ports().de()?;
|
||||||
for host in all_hosts(db) {
|
for host in all_hosts(db) {
|
||||||
let host = host?;
|
let host = host?;
|
||||||
host.as_private_domains_mut().mutate(|d| {
|
host.as_private_domains_mut().mutate(|d| {
|
||||||
@@ -199,6 +204,7 @@ pub async fn remove_tunnel(
|
|||||||
d.retain(|_, gateways| !gateways.is_empty());
|
d.retain(|_, gateways| !gateways.is_empty());
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
host.update_addresses(&gateways, &ports)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
use patch_db::json_ptr::JsonPointer;
|
||||||
|
|
||||||
|
use crate::db::model::Database;
|
||||||
use crate::net::ssl::FullchainCertData;
|
use crate::net::ssl::FullchainCertData;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::service::effects::context::EffectContext;
|
use crate::service::effects::context::EffectContext;
|
||||||
@@ -29,7 +32,7 @@ struct ServiceCallbackMap {
|
|||||||
get_service_interface: BTreeMap<(PackageId, ServiceInterfaceId), Vec<CallbackHandler>>,
|
get_service_interface: BTreeMap<(PackageId, ServiceInterfaceId), Vec<CallbackHandler>>,
|
||||||
list_service_interfaces: BTreeMap<PackageId, Vec<CallbackHandler>>,
|
list_service_interfaces: BTreeMap<PackageId, Vec<CallbackHandler>>,
|
||||||
get_system_smtp: Vec<CallbackHandler>,
|
get_system_smtp: Vec<CallbackHandler>,
|
||||||
get_host_info: BTreeMap<(PackageId, HostId), Vec<CallbackHandler>>,
|
get_host_info: BTreeMap<(PackageId, HostId), (NonDetachingJoinHandle<()>, Vec<CallbackHandler>)>,
|
||||||
get_ssl_certificate: EqMap<
|
get_ssl_certificate: EqMap<
|
||||||
(BTreeSet<InternedString>, FullchainCertData, Algorithm),
|
(BTreeSet<InternedString>, FullchainCertData, Algorithm),
|
||||||
(NonDetachingJoinHandle<()>, Vec<CallbackHandler>),
|
(NonDetachingJoinHandle<()>, Vec<CallbackHandler>),
|
||||||
@@ -57,7 +60,7 @@ impl ServiceCallbacks {
|
|||||||
});
|
});
|
||||||
this.get_system_smtp
|
this.get_system_smtp
|
||||||
.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
|
.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
|
||||||
this.get_host_info.retain(|_, v| {
|
this.get_host_info.retain(|_, (_, v)| {
|
||||||
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
|
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
|
||||||
!v.is_empty()
|
!v.is_empty()
|
||||||
});
|
});
|
||||||
@@ -141,29 +144,57 @@ impl ServiceCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn add_get_host_info(
|
pub(super) fn add_get_host_info(
|
||||||
&self,
|
self: &Arc<Self>,
|
||||||
|
db: &TypedPatchDb<Database>,
|
||||||
package_id: PackageId,
|
package_id: PackageId,
|
||||||
host_id: HostId,
|
host_id: HostId,
|
||||||
handler: CallbackHandler,
|
handler: CallbackHandler,
|
||||||
) {
|
) {
|
||||||
self.mutate(|this| {
|
self.mutate(|this| {
|
||||||
this.get_host_info
|
this.get_host_info
|
||||||
.entry((package_id, host_id))
|
.entry((package_id.clone(), host_id.clone()))
|
||||||
.or_default()
|
.or_insert_with(|| {
|
||||||
|
let ptr: JsonPointer = format!(
|
||||||
|
"/public/packageData/{}/hosts/{}",
|
||||||
|
package_id, host_id
|
||||||
|
)
|
||||||
|
.parse()
|
||||||
|
.expect("valid json pointer");
|
||||||
|
let db = db.clone();
|
||||||
|
let callbacks = Arc::clone(self);
|
||||||
|
let key = (package_id, host_id);
|
||||||
|
(
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut sub = db.subscribe(ptr).await;
|
||||||
|
while sub.recv().await.is_some() {
|
||||||
|
if let Some(cbs) = callbacks.mutate(|this| {
|
||||||
|
this.get_host_info
|
||||||
|
.remove(&key)
|
||||||
|
.map(|(_, handlers)| CallbackHandlers(handlers))
|
||||||
|
.filter(|cb| !cb.0.is_empty())
|
||||||
|
}) {
|
||||||
|
if let Err(e) = cbs.call(vector![]).await {
|
||||||
|
tracing::error!(
|
||||||
|
"Error in host info callback: {e}"
|
||||||
|
);
|
||||||
|
tracing::debug!("{e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// entry was removed when we consumed handlers,
|
||||||
|
// so stop watching — a new subscription will be
|
||||||
|
// created if the service re-registers
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.1
|
||||||
.push(handler);
|
.push(handler);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn get_host_info(&self, id: &(PackageId, HostId)) -> Option<CallbackHandlers> {
|
|
||||||
self.mutate(|this| {
|
|
||||||
Some(CallbackHandlers(
|
|
||||||
this.get_host_info.remove(id).unwrap_or_default(),
|
|
||||||
))
|
|
||||||
.filter(|cb| !cb.0.is_empty())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn add_get_ssl_certificate(
|
pub(super) fn add_get_ssl_certificate(
|
||||||
&self,
|
&self,
|
||||||
ctx: EffectContext,
|
ctx: EffectContext,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ pub async fn get_host_info(
|
|||||||
if let Some(callback) = callback {
|
if let Some(callback) = callback {
|
||||||
let callback = callback.register(&context.seed.persistent_container);
|
let callback = callback.register(&context.seed.persistent_container);
|
||||||
context.seed.ctx.callbacks.add_get_host_info(
|
context.seed.ctx.callbacks.add_get_host_info(
|
||||||
|
&context.seed.ctx.db,
|
||||||
package_id.clone(),
|
package_id.clone(),
|
||||||
host_id.clone(),
|
host_id.clone(),
|
||||||
CallbackHandler::new(&context, callback),
|
CallbackHandler::new(&context, callback),
|
||||||
|
|||||||
@@ -414,14 +414,11 @@ pub async fn show_config(
|
|||||||
i.iter().find_map(|(_, info)| {
|
i.iter().find_map(|(_, info)| {
|
||||||
info.ip_info
|
info.ip_info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|_| info.public())
|
.and_then(|ip_info| ip_info.wan_ip)
|
||||||
.iter()
|
.map(IpAddr::from)
|
||||||
.find_map(|info| info.subnets.iter().next())
|
|
||||||
.copied()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.or_not_found("a public IP address")?
|
.or_not_found("a public IP address")?
|
||||||
.addr()
|
|
||||||
};
|
};
|
||||||
Ok(client
|
Ok(client
|
||||||
.client_config(
|
.client_config(
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ export type HostnameMetadata =
|
|||||||
| { kind: 'ipv6'; gateway: GatewayId; scopeId: number }
|
| { kind: 'ipv6'; gateway: GatewayId; scopeId: number }
|
||||||
| { kind: 'private-domain'; gateways: Array<GatewayId> }
|
| { kind: 'private-domain'; gateways: Array<GatewayId> }
|
||||||
| { kind: 'public-domain'; gateway: GatewayId }
|
| { kind: 'public-domain'; gateway: GatewayId }
|
||||||
| ({ kind: 'plugin'; package: PackageId } & { [key in string]?: unknown })
|
| { kind: 'plugin'; package: PackageId }
|
||||||
|
|||||||
@@ -49,19 +49,20 @@ type VisibilityFilter<V extends 'public' | 'private'> = V extends 'public'
|
|||||||
: never
|
: never
|
||||||
type KindFilter<K extends FilterKinds> = K extends 'mdns'
|
type KindFilter<K extends FilterKinds> = K extends 'mdns'
|
||||||
?
|
?
|
||||||
| (HostnameInfo & { hostname: { kind: 'local' } })
|
| (HostnameInfo & { metadata: { kind: 'private-domain' } })
|
||||||
| KindFilter<Exclude<K, 'mdns'>>
|
| KindFilter<Exclude<K, 'mdns'>>
|
||||||
: K extends 'domain'
|
: K extends 'domain'
|
||||||
?
|
?
|
||||||
| (HostnameInfo & { hostname: { kind: 'domain' } })
|
| (HostnameInfo & { metadata: { kind: 'private-domain' } })
|
||||||
|
| (HostnameInfo & { metadata: { kind: 'public-domain' } })
|
||||||
| KindFilter<Exclude<K, 'domain'>>
|
| KindFilter<Exclude<K, 'domain'>>
|
||||||
: K extends 'ipv4'
|
: K extends 'ipv4'
|
||||||
?
|
?
|
||||||
| (HostnameInfo & { hostname: { kind: 'ipv4' } })
|
| (HostnameInfo & { metadata: { kind: 'ipv4' } })
|
||||||
| KindFilter<Exclude<K, 'ipv4'>>
|
| KindFilter<Exclude<K, 'ipv4'>>
|
||||||
: K extends 'ipv6'
|
: K extends 'ipv6'
|
||||||
?
|
?
|
||||||
| (HostnameInfo & { hostname: { kind: 'ipv6' } })
|
| (HostnameInfo & { metadata: { kind: 'ipv6' } })
|
||||||
| KindFilter<Exclude<K, 'ipv6'>>
|
| KindFilter<Exclude<K, 'ipv6'>>
|
||||||
: K extends 'ip'
|
: K extends 'ip'
|
||||||
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
|
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
|
||||||
@@ -108,10 +109,7 @@ type FormatReturnTy<
|
|||||||
export type Filled<F extends Filter = {}> = {
|
export type Filled<F extends Filter = {}> = {
|
||||||
hostnames: HostnameInfo[]
|
hostnames: HostnameInfo[]
|
||||||
|
|
||||||
toUrls: (h: HostnameInfo) => {
|
toUrl: (h: HostnameInfo) => UrlString
|
||||||
url: UrlString | null
|
|
||||||
sslUrl: UrlString | null
|
|
||||||
}
|
|
||||||
|
|
||||||
format: <Format extends Formats = 'urlstring'>(
|
format: <Format extends Formats = 'urlstring'>(
|
||||||
format?: Format,
|
format?: Format,
|
||||||
@@ -152,37 +150,29 @@ const unique = <A>(values: A[]) => Array.from(new Set(values))
|
|||||||
export const addressHostToUrl = (
|
export const addressHostToUrl = (
|
||||||
{ scheme, sslScheme, username, suffix }: AddressInfo,
|
{ scheme, sslScheme, username, suffix }: AddressInfo,
|
||||||
hostname: HostnameInfo,
|
hostname: HostnameInfo,
|
||||||
): { url: UrlString | null; sslUrl: UrlString | null } => {
|
): UrlString => {
|
||||||
const res = []
|
const effectiveScheme = hostname.ssl ? sslScheme : scheme
|
||||||
const fmt = (scheme: string | null, host: HostnameInfo, port: number) => {
|
let host: string
|
||||||
|
if (hostname.metadata.kind === 'ipv6') {
|
||||||
|
host = IPV6_LINK_LOCAL.contains(hostname.host)
|
||||||
|
? `[${hostname.host}%${hostname.metadata.scopeId}]`
|
||||||
|
: `[${hostname.host}]`
|
||||||
|
} else {
|
||||||
|
host = hostname.host
|
||||||
|
}
|
||||||
|
let portStr = ''
|
||||||
|
if (hostname.port !== null) {
|
||||||
const excludePort =
|
const excludePort =
|
||||||
scheme &&
|
effectiveScheme &&
|
||||||
scheme in knownProtocols &&
|
effectiveScheme in knownProtocols &&
|
||||||
port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort
|
hostname.port ===
|
||||||
let hostname
|
knownProtocols[effectiveScheme as keyof typeof knownProtocols]
|
||||||
if (host.hostname.kind === 'domain') {
|
.defaultPort
|
||||||
hostname = host.hostname.value
|
if (!excludePort) portStr = `:${hostname.port}`
|
||||||
} else if (host.hostname.kind === 'ipv6') {
|
|
||||||
hostname = IPV6_LINK_LOCAL.contains(host.hostname.value)
|
|
||||||
? `[${host.hostname.value}%${host.hostname.scopeId}]`
|
|
||||||
: `[${host.hostname.value}]`
|
|
||||||
} else {
|
|
||||||
hostname = host.hostname.value
|
|
||||||
}
|
|
||||||
return `${scheme ? `${scheme}://` : ''}${
|
|
||||||
username ? `${username}@` : ''
|
|
||||||
}${hostname}${excludePort ? '' : `:${port}`}${suffix}`
|
|
||||||
}
|
}
|
||||||
let url = null
|
return `${effectiveScheme ? `${effectiveScheme}://` : ''}${
|
||||||
if (hostname.hostname.port !== null) {
|
username ? `${username}@` : ''
|
||||||
url = fmt(scheme, hostname, hostname.hostname.port)
|
}${host}${portStr}${suffix}`
|
||||||
}
|
|
||||||
let sslUrl = null
|
|
||||||
if (hostname.hostname.sslPort !== null) {
|
|
||||||
sslUrl = fmt(sslScheme, hostname, hostname.hostname.sslPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { url, sslUrl }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterRec(
|
function filterRec(
|
||||||
@@ -209,15 +199,19 @@ function filterRec(
|
|||||||
hostnames = hostnames.filter(
|
hostnames = hostnames.filter(
|
||||||
(h) =>
|
(h) =>
|
||||||
invert !==
|
invert !==
|
||||||
((kind.has('mdns') && h.hostname.kind === 'local') ||
|
((kind.has('mdns') &&
|
||||||
(kind.has('domain') && h.hostname.kind === 'domain') ||
|
h.metadata.kind === 'private-domain' &&
|
||||||
(kind.has('ipv4') && h.hostname.kind === 'ipv4') ||
|
h.host.endsWith('.local')) ||
|
||||||
(kind.has('ipv6') && h.hostname.kind === 'ipv6') ||
|
(kind.has('domain') &&
|
||||||
|
(h.metadata.kind === 'private-domain' ||
|
||||||
|
h.metadata.kind === 'public-domain')) ||
|
||||||
|
(kind.has('ipv4') && h.metadata.kind === 'ipv4') ||
|
||||||
|
(kind.has('ipv6') && h.metadata.kind === 'ipv6') ||
|
||||||
(kind.has('localhost') &&
|
(kind.has('localhost') &&
|
||||||
['localhost', '127.0.0.1', '::1'].includes(h.hostname.value)) ||
|
['localhost', '127.0.0.1', '::1'].includes(h.host)) ||
|
||||||
(kind.has('link-local') &&
|
(kind.has('link-local') &&
|
||||||
h.hostname.kind === 'ipv6' &&
|
h.metadata.kind === 'ipv6' &&
|
||||||
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname.value)))),
|
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.host)))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,15 +220,26 @@ function filterRec(
|
|||||||
return hostnames
|
return hostnames
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDefaultEnabled(h: HostnameInfo): boolean {
|
function isPublicIp(h: HostnameInfo): boolean {
|
||||||
return !(h.public && (h.hostname.kind === 'ipv4' || h.hostname.kind === 'ipv6'))
|
return h.public && (h.metadata.kind === 'ipv4' || h.metadata.kind === 'ipv6')
|
||||||
}
|
}
|
||||||
|
|
||||||
function enabledAddresses(addr: DerivedAddressInfo): HostnameInfo[] {
|
function enabledAddresses(addr: DerivedAddressInfo): HostnameInfo[] {
|
||||||
return addr.possible.filter((h) => {
|
return addr.available.filter((h) => {
|
||||||
if (addr.enabled.some((e) => deepEqual(e, h))) return true
|
if (isPublicIp(h)) {
|
||||||
if (addr.disabled.some((d) => deepEqual(d, h))) return false
|
// Public IPs: disabled by default, explicitly enabled via SocketAddr string
|
||||||
return isDefaultEnabled(h)
|
if (h.port === null) return true
|
||||||
|
const sa =
|
||||||
|
h.metadata.kind === 'ipv6'
|
||||||
|
? `[${h.host}]:${h.port}`
|
||||||
|
: `${h.host}:${h.port}`
|
||||||
|
return addr.enabled.includes(sa)
|
||||||
|
} else {
|
||||||
|
// Everything else: enabled by default, explicitly disabled via [host, port] tuple
|
||||||
|
return !addr.disabled.some(
|
||||||
|
([host, port]) => host === h.host && port === (h.port ?? 0),
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,11 +247,7 @@ export const filledAddress = (
|
|||||||
host: Host,
|
host: Host,
|
||||||
addressInfo: AddressInfo,
|
addressInfo: AddressInfo,
|
||||||
): FilledAddressInfo => {
|
): FilledAddressInfo => {
|
||||||
const toUrls = addressHostToUrl.bind(null, addressInfo)
|
const toUrl = addressHostToUrl.bind(null, addressInfo)
|
||||||
const toUrlArray = (h: HostnameInfo) => {
|
|
||||||
const u = toUrls(h)
|
|
||||||
return [u.url, u.sslUrl].filter((u) => u !== null)
|
|
||||||
}
|
|
||||||
const binding = host.bindings[addressInfo.internalPort]
|
const binding = host.bindings[addressInfo.internalPort]
|
||||||
const hostnames = binding ? enabledAddresses(binding.addresses) : []
|
const hostnames = binding ? enabledAddresses(binding.addresses) : []
|
||||||
|
|
||||||
@@ -266,11 +267,11 @@ export const filledAddress = (
|
|||||||
return {
|
return {
|
||||||
...addressInfo,
|
...addressInfo,
|
||||||
hostnames,
|
hostnames,
|
||||||
toUrls,
|
toUrl,
|
||||||
format: <Format extends Formats = 'urlstring'>(format?: Format) => {
|
format: <Format extends Formats = 'urlstring'>(format?: Format) => {
|
||||||
let res: FormatReturnTy<{}, Format>[] = hostnames as any
|
let res: FormatReturnTy<{}, Format>[] = hostnames as any
|
||||||
if (format === 'hostname-info') return res
|
if (format === 'hostname-info') return res
|
||||||
const urls = hostnames.flatMap(toUrlArray)
|
const urls = hostnames.map(toUrl)
|
||||||
if (format === 'url') res = urls.map((u) => new URL(u)) as any
|
if (format === 'url') res = urls.map((u) => new URL(u)) as any
|
||||||
else res = urls as any
|
else res = urls as any
|
||||||
return res
|
return res
|
||||||
|
|||||||
8483
web/package-lock.json
generated
8483
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user