wip refactor

This commit is contained in:
Aiden McClelland
2026-02-12 14:51:33 -07:00
parent 339e5f799a
commit db7f3341ac
21 changed files with 967 additions and 668 deletions

663
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ PROFILE=${PROFILE:-debug}
if [ "${PROFILE}" = "release" ]; then if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release" BUILD_FLAGS="--release"
else else
if [ "$PROFILE" != "debug"]; then if [ "$PROFILE" != "debug" ]; then
>&2 echo "Unknown profile $PROFILE: falling back to debug..." >&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug PROFILE=debug
fi fi

View File

@@ -20,8 +20,9 @@ use crate::db::model::Database;
use crate::db::model::package::AllPackageData; use crate::db::model::package::AllPackageData;
use crate::net::acme::AcmeProvider; use crate::net::acme::AcmeProvider;
use crate::net::host::Host; use crate::net::host::Host;
use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, Bindings, DerivedAddressInfo, NetInfo}; use crate::net::host::binding::{
use crate::net::utils::ipv6_is_local; AddSslOptions, BindInfo, BindOptions, Bindings, DerivedAddressInfo, NetInfo,
};
use crate::net::vhost::AlpnInfo; use crate::net::vhost::AlpnInfo;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::FullProgress; use crate::progress::FullProgress;
@@ -91,7 +92,7 @@ impl Public {
.collect(), .collect(),
), ),
public_domains: BTreeMap::new(), public_domains: BTreeMap::new(),
private_domains: BTreeSet::new(), private_domains: BTreeMap::new(),
}, },
wifi: WifiInfo { wifi: WifiInfo {
enabled: true, enabled: true,
@@ -242,44 +243,12 @@ pub struct DnsSettings {
#[ts(export)] #[ts(export)]
pub struct NetworkInterfaceInfo { pub struct NetworkInterfaceInfo {
pub name: Option<InternedString>, pub name: Option<InternedString>,
#[ts(skip)]
pub public: Option<bool>,
pub secure: Option<bool>, pub secure: Option<bool>,
pub ip_info: Option<Arc<IpInfo>>, pub ip_info: Option<Arc<IpInfo>>,
#[serde(default, rename = "type")] #[serde(default, rename = "type")]
pub gateway_type: Option<GatewayType>, pub gateway_type: Option<GatewayType>,
} }
impl NetworkInterfaceInfo { impl NetworkInterfaceInfo {
pub fn public(&self) -> bool {
self.public.unwrap_or_else(|| {
!self.ip_info.as_ref().map_or(true, |ip_info| {
let ip4s = ip_info
.subnets
.iter()
.filter_map(|ipnet| {
if let IpAddr::V4(ip4) = ipnet.addr() {
Some(ip4)
} else {
None
}
})
.collect::<BTreeSet<_>>();
if !ip4s.is_empty() {
return ip4s
.iter()
.all(|ip4| ip4.is_loopback() || ip4.is_private() || ip4.is_link_local());
}
ip_info.subnets.iter().all(|ipnet| {
if let IpAddr::V6(ip6) = ipnet.addr() {
ipv6_is_local(ip6)
} else {
true
}
})
})
})
}
pub fn secure(&self) -> bool { pub fn secure(&self) -> bool {
self.secure.unwrap_or(false) self.secure.unwrap_or(false)
} }
@@ -316,7 +285,20 @@ pub enum NetworkInterfaceType {
Loopback, Loopback,
} }
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, clap::ValueEnum)] #[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Deserialize,
Serialize,
TS,
clap::ValueEnum,
)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum GatewayType { pub enum GatewayType {

View File

@@ -71,7 +71,7 @@ impl SignatureAuthContext for RpcContext {
.as_network() .as_network()
.as_host() .as_host()
.as_private_domains() .as_private_domains()
.de() .keys()
.map(|k| k.into_iter()) .map(|k| k.into_iter())
.transpose(), .transpose(),
) )

View File

@@ -10,7 +10,7 @@ use color_eyre::eyre::eyre;
use futures::{FutureExt, StreamExt, TryStreamExt}; use futures::{FutureExt, StreamExt, TryStreamExt};
use hickory_server::authority::{AuthorityObject, Catalog, MessageResponseBuilder}; use hickory_server::authority::{AuthorityObject, Catalog, MessageResponseBuilder};
use hickory_server::proto::op::{Header, ResponseCode}; use hickory_server::proto::op::{Header, ResponseCode};
use hickory_server::proto::rr::{LowerName, Name, Record, RecordType}; use hickory_server::proto::rr::{Name, Record, RecordType};
use hickory_server::resolver::config::{ResolverConfig, ResolverOpts}; use hickory_server::resolver::config::{ResolverConfig, ResolverOpts};
use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo}; use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo};
use hickory_server::store::forwarder::{ForwardAuthority, ForwardConfig}; use hickory_server::store::forwarder::{ForwardAuthority, ForwardConfig};

View File

@@ -56,7 +56,7 @@ pub fn gateway_api<C: Context>() -> ParentHandler<C> {
} }
let mut table = Table::new(); let mut table = Table::new();
table.add_row(row![bc => "INTERFACE", "TYPE", "PUBLIC", "ADDRESSES", "WAN IP"]); table.add_row(row![bc => "INTERFACE", "TYPE", "ADDRESSES", "WAN IP"]);
for (iface, info) in res { for (iface, info) in res {
table.add_row(row![ table.add_row(row![
iface, iface,
@@ -64,7 +64,6 @@ pub fn gateway_api<C: Context>() -> ParentHandler<C> {
.as_ref() .as_ref()
.and_then(|ip_info| ip_info.device_type) .and_then(|ip_info| ip_info.device_type)
.map_or_else(|| "UNKNOWN".to_owned(), |ty| format!("{ty:?}")), .map_or_else(|| "UNKNOWN".to_owned(), |ty| format!("{ty:?}")),
info.public(),
info.ip_info.as_ref().map_or_else( info.ip_info.as_ref().map_or_else(
|| "<DISCONNECTED>".to_owned(), || "<DISCONNECTED>".to_owned(),
|ip_info| ip_info |ip_info| ip_info
@@ -94,22 +93,6 @@ pub fn gateway_api<C: Context>() -> ParentHandler<C> {
.with_about("about.show-gateways-startos-can-listen-on") .with_about("about.show-gateways-startos-can-listen-on")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand(
"set-public",
from_fn_async(set_public)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("about.indicate-gateway-inbound-access-from-wan")
.with_call_remote::<CliContext>(),
)
.subcommand(
"unset-public",
from_fn_async(unset_public)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("about.allow-gateway-infer-inbound-access-from-wan")
.with_call_remote::<CliContext>(),
)
.subcommand( .subcommand(
"forget", "forget",
from_fn_async(forget_iface) from_fn_async(forget_iface)
@@ -134,40 +117,6 @@ async fn list_interfaces(
Ok(ctx.net_controller.net_iface.watcher.ip_info()) Ok(ctx.net_controller.net_iface.watcher.ip_info())
} }
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
struct NetworkInterfaceSetPublicParams {
#[arg(help = "help.arg.gateway-id")]
gateway: GatewayId,
#[arg(help = "help.arg.is-public")]
public: Option<bool>,
}
async fn set_public(
ctx: RpcContext,
NetworkInterfaceSetPublicParams { gateway, public }: NetworkInterfaceSetPublicParams,
) -> Result<(), Error> {
ctx.net_controller
.net_iface
.set_public(&gateway, Some(public.unwrap_or(true)))
.await
}
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
struct UnsetPublicParams {
#[arg(help = "help.arg.gateway-id")]
gateway: GatewayId,
}
async fn unset_public(
ctx: RpcContext,
UnsetPublicParams { gateway }: UnsetPublicParams,
) -> Result<(), Error> {
ctx.net_controller
.net_iface
.set_public(&gateway, None)
.await
}
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
struct ForgetGatewayParams { struct ForgetGatewayParams {
#[arg(help = "help.arg.gateway-id")] #[arg(help = "help.arg.gateway-id")]
@@ -910,12 +859,11 @@ async fn watch_ip(
write_to.send_if_modified( write_to.send_if_modified(
|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| { |m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
let (name, public, secure, gateway_type, prev_wan_ip) = m let (name, secure, gateway_type, prev_wan_ip) = m
.get(&iface) .get(&iface)
.map_or((None, None, None, None, None), |i| { .map_or((None, None, None, None), |i| {
( (
i.name.clone(), i.name.clone(),
i.public,
i.secure, i.secure,
i.gateway_type, i.gateway_type,
i.ip_info i.ip_info
@@ -929,7 +877,6 @@ async fn watch_ip(
iface.clone(), iface.clone(),
NetworkInterfaceInfo { NetworkInterfaceInfo {
name, name,
public,
secure, secure,
ip_info: Some(ip_info.clone()), ip_info: Some(ip_info.clone()),
gateway_type, gateway_type,
@@ -1192,43 +1139,6 @@ impl NetworkInterfaceController {
} }
} }
pub async fn set_public(
&self,
interface: &GatewayId,
public: Option<bool>,
) -> Result<(), Error> {
let mut sub = self
.db
.subscribe(
"/public/serverInfo/network/gateways"
.parse::<JsonPointer<_, _>>()
.with_kind(ErrorKind::Database)?,
)
.await;
let mut err = None;
let changed = self.watcher.ip_info.send_if_modified(|ip_info| {
let prev = std::mem::replace(
&mut match ip_info.get_mut(interface).or_not_found(interface) {
Ok(a) => a,
Err(e) => {
err = Some(e);
return false;
}
}
.public,
public,
);
prev != public
});
if let Some(e) = err {
return Err(e);
}
if changed {
sub.recv().await;
}
Ok(())
}
pub async fn forget(&self, interface: &GatewayId) -> Result<(), Error> { pub async fn forget(&self, interface: &GatewayId) -> Result<(), Error> {
let mut sub = self let mut sub = self
.db .db

View File

@@ -20,7 +20,7 @@ use crate::util::serde::{HandlerExtSerde, display_serializable};
pub struct HostAddress { pub struct HostAddress {
pub address: InternedString, pub address: InternedString,
pub public: Option<PublicDomainConfig>, pub public: Option<PublicDomainConfig>,
pub private: bool, pub private: Option<BTreeSet<GatewayId>>,
} }
#[derive(Debug, Clone, Deserialize, Serialize, TS)] #[derive(Debug, Clone, Deserialize, Serialize, TS)]
@@ -53,7 +53,7 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
for domain in &public { for domain in &public {
check_domain(&mut domains, domain.clone())?; check_domain(&mut domains, domain.clone())?;
} }
for domain in host.as_private_domains().de()? { for domain in host.as_private_domains().keys()? {
if !public.contains(&domain) { if !public.contains(&domain) {
check_domain(&mut domains, domain)?; check_domain(&mut domains, domain)?;
} }
@@ -63,13 +63,13 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
host.as_public_domains_mut() host.as_public_domains_mut()
.mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?; .mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?;
host.as_private_domains_mut() host.as_private_domains_mut()
.mutate(|d| Ok(d.retain(|d| !domains.contains(d))))?; .mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?;
let public = host.as_public_domains().keys()?; let public = host.as_public_domains().keys()?;
for domain in &public { for domain in &public {
check_domain(&mut domains, domain.clone())?; check_domain(&mut domains, domain.clone())?;
} }
for domain in host.as_private_domains().de()? { for domain in host.as_private_domains().keys()? {
if !public.contains(&domain) { if !public.contains(&domain) {
check_domain(&mut domains, domain)?; check_domain(&mut domains, domain)?;
} }
@@ -146,21 +146,7 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
} }
let mut table = Table::new(); let mut table = Table::new();
table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]); todo!("find a good way to represent this");
for entry in &res {
if let Some(PublicDomainConfig { gateway, acme }) = &entry.public {
table.add_row(row![
entry.address,
&format!(
"{} ({gateway})",
if entry.private { "YES" } else { "ONLY" }
),
acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE")
]);
} else {
table.add_row(row![entry.address, &format!("NO"), "N/A"]);
}
}
table.print_tty(false)?; table.print_tty(false)?;
@@ -248,18 +234,20 @@ pub async fn remove_public_domain<Kind: HostApiKind>(
pub struct AddPrivateDomainParams { pub struct AddPrivateDomainParams {
#[arg(help = "help.arg.fqdn")] #[arg(help = "help.arg.fqdn")]
pub fqdn: InternedString, pub fqdn: InternedString,
pub gateway: GatewayId,
} }
pub async fn add_private_domain<Kind: HostApiKind>( pub async fn add_private_domain<Kind: HostApiKind>(
ctx: RpcContext, ctx: RpcContext,
AddPrivateDomainParams { fqdn }: AddPrivateDomainParams, AddPrivateDomainParams { fqdn, gateway }: AddPrivateDomainParams,
inheritance: Kind::Inheritance, inheritance: Kind::Inheritance,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.db ctx.db
.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.insert(fqdn)))?; .upsert(&fqdn, || Ok(BTreeSet::new()))?
.mutate(|d| Ok(d.insert(gateway)))?;
handle_duplicates(db) handle_duplicates(db)
}) })
.await .await

View File

@@ -1,4 +1,5 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::net::SocketAddr;
use std::str::FromStr; use std::str::FromStr;
use clap::Parser; use clap::Parser;
@@ -7,6 +8,7 @@ use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_f
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use crate::HostId;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::prelude::Map; use crate::db::prelude::Map;
use crate::net::forward::AvailablePorts; use crate::net::forward::AvailablePorts;
@@ -16,7 +18,6 @@ use crate::net::vhost::AlpnInfo;
use crate::prelude::*; use crate::prelude::*;
use crate::util::FromStrParser; use crate::util::FromStrParser;
use crate::util::serde::{HandlerExtSerde, display_serializable}; use crate::util::serde::{HandlerExtSerde, display_serializable};
use crate::HostId;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)]
#[ts(export)] #[ts(export)]
@@ -49,31 +50,36 @@ impl FromStr for BindId {
#[ts(export)] #[ts(export)]
#[model = "Model<Self>"] #[model = "Model<Self>"]
pub struct DerivedAddressInfo { pub struct DerivedAddressInfo {
/// User-controlled: private addresses the user has disabled /// User override: enable these addresses (only for public IP & port)
pub private_disabled: BTreeSet<HostnameInfo>, pub enabled: BTreeSet<SocketAddr>,
/// User-controlled: public addresses the user has enabled /// User override: disable these addresses (only for domains and private IP & port)
pub public_enabled: BTreeSet<HostnameInfo>, pub disabled: BTreeSet<(InternedString, u16)>,
/// COMPUTED: NetServiceData::update — all possible addresses for this binding /// COMPUTED: NetServiceData::update — all possible addresses for this binding
pub possible: BTreeSet<HostnameInfo>, pub available: BTreeSet<HostnameInfo>,
} }
impl DerivedAddressInfo { impl DerivedAddressInfo {
/// Returns addresses that are currently enabled. /// Returns addresses that are currently enabled after applying overrides.
/// Private addresses are enabled by default (disabled if in private_disabled). /// Default: public IPs are disabled, everything else is enabled.
/// Public addresses are disabled by default (enabled if in public_enabled). /// Explicit `enabled`/`disabled` overrides take precedence.
pub fn enabled(&self) -> BTreeSet<&HostnameInfo> { pub fn enabled(&self) -> BTreeSet<&HostnameInfo> {
self.possible self.available
.iter() .iter()
.filter(|h| { .filter(|h| {
if h.public { if h.public && h.metadata.is_ip() {
self.public_enabled.contains(h) // Public IPs: disabled by default, explicitly enabled via SocketAddr
h.to_socket_addr().map_or(
true, // should never happen, but would rather see them if it does
|sa| self.enabled.contains(&sa),
)
} else { } else {
!self.private_disabled.contains(h) !self
.disabled
.contains(&(h.host.clone(), h.port.unwrap_or_default())) // disablable addresses will always have a port
} }
}) })
.collect() .collect()
} }
} }
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
@@ -199,9 +205,9 @@ impl BindInfo {
options, options,
net: lan, net: lan,
addresses: DerivedAddressInfo { addresses: DerivedAddressInfo {
private_disabled: addresses.private_disabled, enabled: addresses.enabled,
public_enabled: addresses.public_enabled, disabled: addresses.disabled,
possible: BTreeSet::new(), available: BTreeSet::new(),
}, },
}) })
} }
@@ -328,17 +334,27 @@ pub async fn set_address_enabled<Kind: HostApiKind>(
.as_bindings_mut() .as_bindings_mut()
.mutate(|b| { .mutate(|b| {
let bind = b.get_mut(&internal_port).or_not_found(internal_port)?; let bind = b.get_mut(&internal_port).or_not_found(internal_port)?;
if address.public { if address.public && address.metadata.is_ip() {
// Public IPs: toggle via SocketAddr in `enabled` set
let sa = address.to_socket_addr().ok_or_else(|| {
Error::new(
eyre!("cannot convert address to socket addr"),
ErrorKind::InvalidRequest,
)
})?;
if enabled { if enabled {
bind.addresses.public_enabled.insert(address.clone()); bind.addresses.enabled.insert(sa);
} else { } else {
bind.addresses.public_enabled.remove(&address); bind.addresses.enabled.remove(&sa);
} }
} else { } else {
// Domains and private IPs: toggle via (host, port) in `disabled` set
let port = address.port.unwrap_or(if address.ssl { 443 } else { 80 });
let key = (address.host.clone(), port);
if enabled { if enabled {
bind.addresses.private_disabled.remove(&address); bind.addresses.disabled.remove(&key);
} else { } else {
bind.addresses.private_disabled.insert(address.clone()); bind.addresses.disabled.insert(key);
} }
} }
Ok(()) Ok(())

View File

@@ -3,19 +3,23 @@ use std::future::Future;
use std::panic::RefUnwindSafe; use std::panic::RefUnwindSafe;
use clap::Parser; use clap::Parser;
use imbl::OrdMap;
use imbl_value::InternedString; use imbl_value::InternedString;
use itertools::Itertools; use itertools::Itertools;
use patch_db::DestructureMut;
use rpc_toolkit::{Context, Empty, HandlerExt, OrEmpty, ParentHandler, from_fn_async}; use rpc_toolkit::{Context, Empty, HandlerExt, OrEmpty, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::db::model::public::NetworkInterfaceInfo;
use crate::net::forward::AvailablePorts; use crate::net::forward::AvailablePorts;
use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api}; use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api};
use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding}; use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding};
use crate::net::service_interface::{HostnameInfo, HostnameMetadata};
use crate::prelude::*; use crate::prelude::*;
use crate::{HostId, PackageId}; use crate::{GatewayId, HostId, PackageId};
pub mod address; pub mod address;
pub mod binding; pub mod binding;
@@ -27,7 +31,7 @@ pub mod binding;
pub struct Host { pub struct Host {
pub bindings: Bindings, pub bindings: Bindings,
pub public_domains: BTreeMap<InternedString, PublicDomainConfig>, pub public_domains: BTreeMap<InternedString, PublicDomainConfig>,
pub private_domains: BTreeSet<InternedString>, pub private_domains: BTreeMap<InternedString, BTreeSet<GatewayId>>,
} }
impl AsRef<Host> for Host { impl AsRef<Host> for Host {
@@ -45,20 +49,183 @@ impl Host {
.map(|(address, config)| HostAddress { .map(|(address, config)| HostAddress {
address: address.clone(), address: address.clone(),
public: Some(config.clone()), public: Some(config.clone()),
private: self.private_domains.contains(address), private: self.private_domains.get(address).cloned(),
}) })
.chain( .chain(
self.private_domains self.private_domains
.iter() .iter()
.filter(|a| !self.public_domains.contains_key(*a)) .filter(|(domain, _)| !self.public_domains.contains_key(*domain))
.map(|address| HostAddress { .map(|(domain, gateways)| HostAddress {
address: address.clone(), address: domain.clone(),
public: None, public: None,
private: true, private: Some(gateways.clone()),
}), }),
) )
} }
} }
impl Model<Host> {
pub fn update_addresses(
&mut self,
gateways: &OrdMap<GatewayId, NetworkInterfaceInfo>,
available_ports: &AvailablePorts,
) -> Result<(), Error> {
let this = self.destructure_mut();
for (_, bind) in this.bindings.as_entries_mut()? {
let net = bind.as_net().de()?;
let opt = bind.as_options().de()?;
let mut available = BTreeSet::new();
for (gid, g) in gateways {
let Some(ip_info) = &g.ip_info else {
continue;
};
let gateway_secure = g.secure();
for subnet in &ip_info.subnets {
let host = InternedString::from_display(&subnet.addr());
let metadata = if subnet.addr().is_ipv4() {
HostnameMetadata::Ipv4 {
gateway: gid.clone(),
}
} else {
HostnameMetadata::Ipv6 {
gateway: gid.clone(),
scope_id: ip_info.scope_id,
}
};
if let Some(port) = net.assigned_port.filter(|_| {
opt.secure
.map_or(gateway_secure, |s| !(s.ssl && opt.add_ssl.is_some()))
}) {
available.insert(HostnameInfo {
ssl: opt.secure.map_or(false, |s| s.ssl),
public: false,
host: host.clone(),
port: Some(port),
metadata: metadata.clone(),
});
}
if let Some(port) = net.assigned_ssl_port {
available.insert(HostnameInfo {
ssl: true,
public: false,
host: host.clone(),
port: Some(port),
metadata,
});
}
}
if let Some(wan_ip) = &ip_info.wan_ip {
let host = InternedString::from_display(&wan_ip);
let metadata = HostnameMetadata::Ipv4 {
gateway: gid.clone(),
};
if let Some(port) = net.assigned_port.filter(|_| {
opt.secure.map_or(
false, // the public internet is never secure
|s| !(s.ssl && opt.add_ssl.is_some()),
)
}) {
available.insert(HostnameInfo {
ssl: opt.secure.map_or(false, |s| s.ssl),
public: true,
host: host.clone(),
port: Some(port),
metadata: metadata.clone(),
});
}
if let Some(port) = net.assigned_ssl_port {
available.insert(HostnameInfo {
ssl: true,
public: true,
host: host.clone(),
port: Some(port),
metadata,
});
}
}
}
for (domain, info) in this.public_domains.de()? {
let metadata = HostnameMetadata::PublicDomain {
gateway: info.gateway.clone(),
};
if let Some(port) = net.assigned_port.filter(|_| {
opt.secure.map_or(
false, // the public internet is never secure
|s| !(s.ssl && opt.add_ssl.is_some()),
)
}) {
available.insert(HostnameInfo {
ssl: opt.secure.map_or(false, |s| s.ssl),
public: true,
host: domain.clone(),
port: Some(port),
metadata: metadata.clone(),
});
}
if let Some(mut port) = net.assigned_ssl_port {
if let Some(preferred) = opt
.add_ssl
.as_ref()
.map(|s| s.preferred_external_port)
.filter(|p| available_ports.is_ssl(*p))
{
port = preferred;
}
available.insert(HostnameInfo {
ssl: true,
public: true,
host: domain.clone(),
port: Some(port),
metadata,
});
}
}
for (domain, domain_gateways) in this.private_domains.de()? {
if let Some(port) = net.assigned_port.filter(|_| {
opt.secure
.map_or(true, |s| !(s.ssl && opt.add_ssl.is_some()))
}) {
let gateways = if opt.secure.is_some() {
domain_gateways.clone()
} else {
domain_gateways
.iter()
.cloned()
.filter(|g| gateways.get(g).map_or(false, |g| g.secure()))
.collect()
};
available.insert(HostnameInfo {
ssl: opt.secure.map_or(false, |s| s.ssl),
public: true,
host: domain.clone(),
port: Some(port),
metadata: HostnameMetadata::PrivateDomain { gateways },
});
}
if let Some(mut port) = net.assigned_ssl_port {
if let Some(preferred) = opt
.add_ssl
.as_ref()
.map(|s| s.preferred_external_port)
.filter(|p| available_ports.is_ssl(*p))
{
port = preferred;
}
available.insert(HostnameInfo {
ssl: true,
public: true,
host: domain.clone(),
port: Some(port),
metadata: HostnameMetadata::PrivateDomain {
gateways: domain_gateways,
},
});
}
}
bind.as_addresses_mut().as_available_mut().ser(&available)?;
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[model = "Model<Self>"] #[model = "Model<Self>"]

View File

@@ -23,7 +23,7 @@ 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::{GatewayInfo, HostnameInfo, IpHostname}; use crate::net::service_interface::{HostnameInfo, HostnameMetadata};
use crate::net::socks::SocksController; use crate::net::socks::SocksController;
use crate::net::utils::ipv6_is_local; use crate::net::utils::ipv6_is_local;
use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController}; use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController};
@@ -261,10 +261,7 @@ impl NetServiceData {
let mut private_dns: BTreeSet<InternedString> = BTreeSet::new(); let mut private_dns: BTreeSet<InternedString> = BTreeSet::new();
let binds = self.binds.entry(id.clone()).or_default(); let binds = self.binds.entry(id.clone()).or_default();
let peek = ctrl.db.peek().await;
let server_info = peek.as_public().as_server_info();
let net_ifaces = ctrl.net_iface.watcher.ip_info(); let net_ifaces = ctrl.net_iface.watcher.ip_info();
let hostname = server_info.as_hostname().de()?;
let host_addresses: Vec<_> = host.addresses().collect(); let host_addresses: Vec<_> = host.addresses().collect();
// Collect private DNS entries (domains without public config) // Collect private DNS entries (domains without public config)
@@ -277,7 +274,7 @@ impl NetServiceData {
} }
} }
// ── Phase 1: Compute possible addresses ── // ── Phase 1: Compute available addresses ──
for (_port, bind) in host.bindings.iter_mut() { for (_port, bind) in host.bindings.iter_mut() {
if !bind.enabled { if !bind.enabled {
continue; continue;
@@ -286,7 +283,83 @@ impl NetServiceData {
continue; continue;
} }
bind.addresses.possible.clear(); 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 for (gateway_id, info) in net_ifaces
.iter() .iter()
.filter(|(_, info)| { .filter(|(_, info)| {
@@ -296,111 +369,97 @@ impl NetServiceData {
}) })
.filter(|(_, info)| info.ip_info.is_some()) .filter(|(_, info)| info.ip_info.is_some())
{ {
let gateway = GatewayInfo {
id: gateway_id.clone(),
name: info
.name
.clone()
.or_else(|| info.ip_info.as_ref().map(|i| i.name.clone()))
.unwrap_or_else(|| gateway_id.clone().into()),
public: info.public(),
};
let port = bind.net.assigned_port.filter(|_| { let port = bind.net.assigned_port.filter(|_| {
bind.options.secure.map_or(false, |s| { bind.options.secure.map_or(false, |s| {
!(s.ssl && bind.options.add_ssl.is_some()) || info.secure() !(s.ssl && bind.options.add_ssl.is_some()) || info.secure()
}) })
}); });
// .local addresses (private only, non-public, non-wireguard gateways)
if !info.public()
&& info.ip_info.as_ref().map_or(false, |i| {
i.device_type != Some(NetworkInterfaceType::Wireguard)
})
{
bind.addresses.possible.insert(HostnameInfo {
gateway: gateway.clone(),
public: false,
hostname: IpHostname::Local {
value: InternedString::from_display(&{
let hostname = &hostname;
lazy_format!("{hostname}.local")
}),
port,
ssl_port: bind.net.assigned_ssl_port,
},
});
}
// Domain addresses
for HostAddress {
address,
public,
private,
} in host_addresses.iter().cloned()
{
let private = private && !info.public();
let public =
public.as_ref().map_or(false, |p| &p.gateway == gateway_id);
if public || private {
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 {
(port, bind.net.assigned_ssl_port)
};
bind.addresses.possible.insert(HostnameInfo {
gateway: gateway.clone(),
public,
hostname: IpHostname::Domain {
value: address.clone(),
port: domain_port,
ssl_port: domain_ssl_port,
},
});
}
}
// IP addresses
if let Some(ip_info) = &info.ip_info { if let Some(ip_info) = &info.ip_info {
let public = info.public(); let public = info.public();
// WAN IP (public)
if let Some(wan_ip) = ip_info.wan_ip { if let Some(wan_ip) = ip_info.wan_ip {
bind.addresses.possible.insert(HostnameInfo { let host_str = InternedString::from_display(&wan_ip);
gateway: gateway.clone(), if let Some(p) = port {
public: true, bind.addresses.available.insert(HostnameInfo {
hostname: IpHostname::Ipv4 { ssl: false,
value: wan_ip, public: true,
port, host: host_str.clone(),
ssl_port: bind.net.assigned_ssl_port, 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 { for ipnet in &ip_info.subnets {
match ipnet { match ipnet {
IpNet::V4(net) => { IpNet::V4(net) => {
if !public { if !public {
bind.addresses.possible.insert(HostnameInfo { let host_str = InternedString::from_display(&net.addr());
gateway: gateway.clone(), if let Some(p) = port {
public, bind.addresses.available.insert(HostnameInfo {
hostname: IpHostname::Ipv4 { ssl: false,
value: net.addr(), public: false,
port, host: host_str.clone(),
ssl_port: bind.net.assigned_ssl_port, 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) => { IpNet::V6(net) => {
bind.addresses.possible.insert(HostnameInfo { let is_public = public && !ipv6_is_local(net.addr());
gateway: gateway.clone(), let host_str = InternedString::from_display(&net.addr());
public: public && !ipv6_is_local(net.addr()), if let Some(p) = port {
hostname: IpHostname::Ipv6 { bind.addresses.available.insert(HostnameInfo {
value: net.addr(), ssl: false,
scope_id: ip_info.scope_id, public: is_public,
port, host: host_str.clone(),
ssl_port: bind.net.assigned_ssl_port, 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,
},
});
}
} }
} }
} }
@@ -435,11 +494,8 @@ impl NetServiceData {
let server_private_ips: BTreeSet<IpAddr> = enabled_addresses let server_private_ips: BTreeSet<IpAddr> = enabled_addresses
.iter() .iter()
.filter(|a| !a.public) .filter(|a| !a.public)
.filter_map(|a| { .flat_map(|a| a.metadata.gateways())
net_ifaces .filter_map(|gw| net_ifaces.get(gw).and_then(|info| info.ip_info.as_ref()))
.get(&a.gateway.id)
.and_then(|info| info.ip_info.as_ref())
})
.flat_map(|ip_info| ip_info.subnets.iter().map(|s| s.addr())) .flat_map(|ip_info| ip_info.subnets.iter().map(|s| s.addr()))
.collect(); .collect();
@@ -465,32 +521,36 @@ impl NetServiceData {
// Domain vhosts: group by (domain, ssl_port), merge public/private sets // Domain vhosts: group by (domain, ssl_port), merge public/private sets
for addr_info in &enabled_addresses { for addr_info in &enabled_addresses {
if let IpHostname::Domain { if !addr_info.ssl {
value: domain, continue;
ssl_port: Some(domain_ssl_port), }
.. match &addr_info.metadata {
} = &addr_info.hostname HostnameMetadata::PublicDomain { .. }
{ | HostnameMetadata::PrivateDomain { .. } => {}
let key = (Some(domain.clone()), *domain_ssl_port); _ => continue,
let target = vhosts.entry(key).or_insert_with(|| ProxyTarget { }
public: BTreeSet::new(), let domain = &addr_info.host;
private: BTreeSet::new(), let domain_ssl_port = addr_info.port.unwrap_or(443);
acme: host_addresses let key = (Some(domain.clone()), domain_ssl_port);
.iter() let target = vhosts.entry(key).or_insert_with(|| ProxyTarget {
.find(|a| &a.address == domain) public: BTreeSet::new(),
.and_then(|a| a.public.as_ref()) private: BTreeSet::new(),
.and_then(|p| p.acme.clone()), acme: host_addresses
addr, .iter()
add_x_forwarded_headers: ssl.add_x_forwarded_headers, .find(|a| a.address == *domain)
connect_ssl: connect_ssl .and_then(|a| a.public.as_ref())
.clone() .and_then(|p| p.acme.clone()),
.map(|_| ctrl.tls_client_config.clone()), addr,
}); add_x_forwarded_headers: ssl.add_x_forwarded_headers,
if addr_info.public { connect_ssl: connect_ssl.clone().map(|_| ctrl.tls_client_config.clone()),
target.public.insert(addr_info.gateway.id.clone()); });
} else { if addr_info.public {
// Add interface IPs for this gateway to private set for gw in addr_info.metadata.gateways() {
if let Some(info) = net_ifaces.get(&addr_info.gateway.id) { target.public.insert(gw.clone());
}
} else {
for gw in addr_info.metadata.gateways() {
if let Some(info) = net_ifaces.get(gw) {
if let Some(ip_info) = &info.ip_info { if let Some(ip_info) = &info.ip_info {
for subnet in &ip_info.subnets { for subnet in &ip_info.subnets {
target.private.insert(subnet.addr()); target.private.insert(subnet.addr());
@@ -512,16 +572,14 @@ impl NetServiceData {
let fwd_public: BTreeSet<GatewayId> = enabled_addresses let fwd_public: BTreeSet<GatewayId> = enabled_addresses
.iter() .iter()
.filter(|a| a.public) .filter(|a| a.public)
.map(|a| a.gateway.id.clone()) .flat_map(|a| a.metadata.gateways())
.cloned()
.collect(); .collect();
let fwd_private: BTreeSet<IpAddr> = enabled_addresses let fwd_private: BTreeSet<IpAddr> = enabled_addresses
.iter() .iter()
.filter(|a| !a.public) .filter(|a| !a.public)
.filter_map(|a| { .flat_map(|a| a.metadata.gateways())
net_ifaces .filter_map(|gw| net_ifaces.get(gw).and_then(|i| i.ip_info.as_ref()))
.get(&a.gateway.id)
.and_then(|i| i.ip_info.as_ref())
})
.flat_map(|ip| ip.subnets.iter().map(|s| s.addr())) .flat_map(|ip| ip.subnets.iter().map(|s| s.addr()))
.collect(); .collect();
forwards.insert( forwards.insert(
@@ -634,8 +692,8 @@ impl NetServiceData {
for (port, bind) in host.bindings.0 { for (port, bind) in host.bindings.0 {
if let Some(b) = bindings.as_idx_mut(&port) { if let Some(b) = bindings.as_idx_mut(&port) {
b.as_addresses_mut() b.as_addresses_mut()
.as_possible_mut() .as_available_mut()
.ser(&bind.addresses.possible)?; .ser(&bind.addresses.available)?;
} }
} }
Ok(()) Ok(())

View File

@@ -1,22 +1,74 @@
use std::net::{Ipv4Addr, Ipv6Addr}; use std::collections::BTreeSet;
use std::net::SocketAddr;
use imbl_value::InternedString; use imbl_value::{InOMap, InternedString};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use crate::{GatewayId, HostId, ServiceInterfaceId}; use crate::prelude::*;
use crate::{GatewayId, HostId, PackageId, ServiceInterfaceId};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct HostnameInfo { pub struct HostnameInfo {
pub gateway: GatewayInfo, pub ssl: bool,
pub public: bool, pub public: bool,
pub hostname: IpHostname, pub host: InternedString,
pub port: Option<u16>,
pub metadata: HostnameMetadata,
} }
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "kebab-case")]
#[serde(rename_all_fields = "camelCase")]
#[serde(tag = "kind")]
pub enum HostnameMetadata {
Ipv4 {
gateway: GatewayId,
},
Ipv6 {
gateway: GatewayId,
scope_id: u32,
},
PrivateDomain {
gateways: BTreeSet<GatewayId>,
},
PublicDomain {
gateway: GatewayId,
},
Plugin {
package: PackageId,
#[serde(flatten)]
extra: InOMap<InternedString, Value>,
},
}
impl HostnameInfo { impl HostnameInfo {
pub fn to_socket_addr(&self) -> Option<SocketAddr> {
let ip = self.host.parse().ok()?;
Some(SocketAddr::new(ip, self.port?))
}
pub fn to_san_hostname(&self) -> InternedString { pub fn to_san_hostname(&self) -> InternedString {
self.hostname.to_san_hostname() self.host.clone()
}
}
impl HostnameMetadata {
pub fn is_ip(&self) -> bool {
matches!(self, Self::Ipv4 { .. } | Self::Ipv6 { .. })
}
pub fn gateways(&self) -> Box<dyn Iterator<Item = &GatewayId> + '_> {
match self {
Self::Ipv4 { gateway }
| Self::Ipv6 { gateway, .. }
| Self::PublicDomain { gateway } => Box::new(std::iter::once(gateway)),
Self::PrivateDomain { gateways } => Box::new(gateways.iter()),
Self::Plugin { .. } => Box::new(std::iter::empty()),
}
} }
} }
@@ -29,48 +81,6 @@ pub struct GatewayInfo {
pub public: bool, pub public: bool,
} }
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all_fields = "camelCase")]
#[serde(tag = "kind")]
pub enum IpHostname {
Ipv4 {
value: Ipv4Addr,
port: Option<u16>,
ssl_port: Option<u16>,
},
Ipv6 {
value: Ipv6Addr,
#[serde(default)]
scope_id: u32,
port: Option<u16>,
ssl_port: Option<u16>,
},
Local {
#[ts(type = "string")]
value: InternedString,
port: Option<u16>,
ssl_port: Option<u16>,
},
Domain {
#[ts(type = "string")]
value: InternedString,
port: Option<u16>,
ssl_port: Option<u16>,
},
}
impl IpHostname {
pub fn to_san_hostname(&self) -> InternedString {
match self {
Self::Ipv4 { value, .. } => InternedString::from_display(value),
Self::Ipv6 { value, .. } => InternedString::from_display(value),
Self::Local { value, .. } => value.clone(),
Self::Domain { value, .. } => value.clone(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]

View File

@@ -82,7 +82,6 @@ pub async fn add_tunnel(
iface.clone(), iface.clone(),
NetworkInterfaceInfo { NetworkInterfaceInfo {
name: Some(name), name: Some(name),
public: None,
secure: None, secure: None,
ip_info: None, ip_info: None,
gateway_type, gateway_type,
@@ -193,15 +192,12 @@ pub async fn remove_tunnel(
.mutate(|db| { .mutate(|db| {
for host in all_hosts(db) { for host in all_hosts(db) {
let host = host?; let host = host?;
host.as_bindings_mut().mutate(|b| { host.as_private_domains_mut().mutate(|d| {
Ok(b.values_mut().for_each(|v| { for gateways in d.values_mut() {
v.addresses gateways.remove(&id);
.private_disabled }
.retain(|h| h.gateway.id != id); d.retain(|_, gateways| !gateways.is_empty());
v.addresses Ok(())
.public_enabled
.retain(|h| h.gateway.id != id);
}))
})?; })?;
} }

View File

@@ -58,12 +58,12 @@ pub async fn get_ssl_certificate(
Ok(m.as_public_domains() Ok(m.as_public_domains()
.keys()? .keys()?
.into_iter() .into_iter()
.chain(m.as_private_domains().de()?) .chain(m.as_private_domains().keys()?)
.chain( .chain(
m.as_bindings() m.as_bindings()
.de()? .de()?
.values() .values()
.flat_map(|b| b.addresses.possible.iter().cloned()) .flat_map(|b| b.addresses.available.iter().cloned())
.map(|h| h.to_san_hostname()), .map(|h| h.to_san_hostname()),
) )
.collect::<Vec<InternedString>>()) .collect::<Vec<InternedString>>())
@@ -182,12 +182,12 @@ pub async fn get_ssl_key(
Ok(m.as_public_domains() Ok(m.as_public_domains()
.keys()? .keys()?
.into_iter() .into_iter()
.chain(m.as_private_domains().de()?) .chain(m.as_private_domains().keys()?)
.chain( .chain(
m.as_bindings() m.as_bindings()
.de()? .de()?
.values() .values()
.flat_map(|b| b.addresses.possible.iter().cloned()) .flat_map(|b| b.addresses.available.iter().cloned())
.map(|h| h.to_san_hostname()), .map(|h| h.to_san_hostname()),
) )
.collect::<Vec<InternedString>>()) .collect::<Vec<InternedString>>())

View File

@@ -164,6 +164,19 @@ fn migrate_host(host: Option<&mut Value>) {
// Remove hostnameInfo from host // Remove hostnameInfo from host
host.remove("hostnameInfo"); host.remove("hostnameInfo");
// Migrate privateDomains from array to object (BTreeSet -> BTreeMap<_, BTreeSet<GatewayId>>)
if let Some(private_domains) = host.get("privateDomains").and_then(|v| v.as_array()).cloned() {
let mut new_pd: Value = serde_json::json!({}).into();
for domain in private_domains {
if let Some(d) = domain.as_str() {
if let Some(obj) = new_pd.as_object_mut() {
obj.insert(d.into(), serde_json::json!([]).into());
}
}
}
host.insert("privateDomains".into(), new_pd);
}
// For each binding: add "addresses" field, remove gateway-level fields from "net" // For each binding: add "addresses" field, remove gateway-level fields from "net"
if let Some(bindings) = host.get_mut("bindings").and_then(|b| b.as_object_mut()) { if let Some(bindings) = host.get_mut("bindings").and_then(|b| b.as_object_mut()) {
for (_, binding) in bindings.iter_mut() { for (_, binding) in bindings.iter_mut() {
@@ -173,9 +186,9 @@ fn migrate_host(host: Option<&mut Value>) {
binding_obj.insert( binding_obj.insert(
"addresses".into(), "addresses".into(),
serde_json::json!({ serde_json::json!({
"privateDisabled": [], "enabled": [],
"publicEnabled": [], "disabled": [],
"possible": [] "available": []
}) })
.into(), .into(),
); );

View File

@@ -5,11 +5,11 @@ export type DerivedAddressInfo = {
/** /**
* User-controlled: private addresses the user has disabled * User-controlled: private addresses the user has disabled
*/ */
privateDisabled: Array<HostnameInfo> enabled: Array<HostnameInfo>
/** /**
* User-controlled: public addresses the user has enabled * User-controlled: public addresses the user has enabled
*/ */
publicEnabled: Array<HostnameInfo> disabled: Array<HostnameInfo>
/** /**
* COMPUTED: NetServiceData::update — all possible addresses for this binding * COMPUTED: NetServiceData::update — all possible addresses for this binding
*/ */

View File

@@ -226,12 +226,16 @@ function filterRec(
return hostnames return hostnames
} }
function isDefaultEnabled(h: HostnameInfo): boolean {
return !(h.public && (h.hostname.kind === 'ipv4' || h.hostname.kind === 'ipv6'))
}
function enabledAddresses(addr: DerivedAddressInfo): HostnameInfo[] { function enabledAddresses(addr: DerivedAddressInfo): HostnameInfo[] {
return addr.possible.filter((h) => return addr.possible.filter((h) => {
h.public if (addr.enabled.some((e) => deepEqual(e, h))) return true
? addr.publicEnabled.some((e) => deepEqual(e, h)) if (addr.disabled.some((d) => deepEqual(d, h))) return false
: !addr.privateDisabled.some((d) => deepEqual(d, h)), return isDefaultEnabled(h)
) })
} }
export const filledAddress = ( export const filledAddress = (

View File

@@ -257,9 +257,9 @@ export class InterfaceService {
if (!binding) return [] if (!binding) return []
const addr = binding.addresses const addr = binding.addresses
const enabled = addr.possible.filter(h => const enabled = addr.possible.filter(h =>
h.public addr.enabled.some(e => utils.deepEqual(e, h)) ||
? addr.publicEnabled.some(e => utils.deepEqual(e, h)) (!addr.disabled.some(d => utils.deepEqual(d, h)) &&
: !addr.privateDisabled.some(d => utils.deepEqual(d, h)), !(h.public && (h.hostname.kind === 'ipv4' || h.hostname.kind === 'ipv6'))),
) )
return enabled.filter( return enabled.filter(
h => h =>

View File

@@ -134,9 +134,12 @@ export default class ServiceInterfaceRoute {
gateways: gateways:
gateways.map(g => ({ gateways.map(g => ({
enabled: enabled:
(g.public (binding?.addresses.enabled.some(a => a.gateway.id === g.id) ||
? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id) (!binding?.addresses.disabled.some(a => a.gateway.id === g.id) &&
: !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false, binding?.addresses.possible.some(a =>
a.gateway.id === g.id &&
!(a.public && (a.hostname.kind === 'ipv4' || a.hostname.kind === 'ipv6'))
))) ?? false,
...g, ...g,
})) || [], })) || [],
publicDomains: getPublicDomains(host.publicDomains, gateways), publicDomains: getPublicDomains(host.publicDomains, gateways),

View File

@@ -95,9 +95,12 @@ export default class StartOsUiComponent {
), ),
gateways: gateways.map(g => ({ gateways: gateways.map(g => ({
enabled: enabled:
(g.public (binding?.addresses.enabled.some(a => a.gateway.id === g.id) ||
? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id) (!binding?.addresses.disabled.some(a => a.gateway.id === g.id) &&
: !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false, binding?.addresses.possible.some(a =>
a.gateway.id === g.id &&
!(a.public && (a.hostname.kind === 'ipv4' || a.hostname.kind === 'ipv6'))
))) ?? false,
...g, ...g,
})), })),
publicDomains: getPublicDomains(network.host.publicDomains, gateways), publicDomains: getPublicDomains(network.host.publicDomains, gateways),

View File

@@ -2128,8 +2128,8 @@ export namespace Mock {
assignedSslPort: 443, assignedSslPort: 443,
}, },
addresses: { addresses: {
privateDisabled: [], enabled: [],
publicEnabled: [], disabled: [],
possible: [ possible: [
{ {
gateway: { id: 'eth0', name: 'Ethernet', public: false }, gateway: { id: 'eth0', name: 'Ethernet', public: false },
@@ -2214,8 +2214,8 @@ export namespace Mock {
assignedSslPort: null, assignedSslPort: null,
}, },
addresses: { addresses: {
privateDisabled: [], enabled: [],
publicEnabled: [], disabled: [],
possible: [], possible: [],
}, },
options: { options: {
@@ -2237,8 +2237,8 @@ export namespace Mock {
assignedSslPort: null, assignedSslPort: null,
}, },
addresses: { addresses: {
privateDisabled: [], enabled: [],
publicEnabled: [], disabled: [],
possible: [], possible: [],
}, },
options: { options: {

View File

@@ -40,8 +40,8 @@ export const mockPatchData: DataModel = {
assignedSslPort: 443, assignedSslPort: 443,
}, },
addresses: { addresses: {
privateDisabled: [], enabled: [],
publicEnabled: [], disabled: [],
possible: [ possible: [
{ {
gateway: { id: 'eth0', name: 'Ethernet', public: false }, gateway: { id: 'eth0', name: 'Ethernet', public: false },
@@ -516,8 +516,8 @@ export const mockPatchData: DataModel = {
assignedSslPort: 443, assignedSslPort: 443,
}, },
addresses: { addresses: {
privateDisabled: [], enabled: [],
publicEnabled: [], disabled: [],
possible: [ possible: [
{ {
gateway: { id: 'eth0', name: 'Ethernet', public: false }, gateway: { id: 'eth0', name: 'Ethernet', public: false },
@@ -602,8 +602,8 @@ export const mockPatchData: DataModel = {
assignedSslPort: null, assignedSslPort: null,
}, },
addresses: { addresses: {
privateDisabled: [], enabled: [],
publicEnabled: [], disabled: [],
possible: [], possible: [],
}, },
options: { options: {
@@ -625,8 +625,8 @@ export const mockPatchData: DataModel = {
assignedSslPort: null, assignedSslPort: null,
}, },
addresses: { addresses: {
privateDisabled: [], enabled: [],
publicEnabled: [], disabled: [],
possible: [], possible: [],
}, },
options: { options: {