feat: implement preferred port allocation and per-address enable/disable

- Add AvailablePorts::try_alloc() with SSL tracking (BTreeMap<u16, bool>)
- Add DerivedAddressInfo on BindInfo with private_disabled/public_enabled/possible sets
- Add Bindings wrapper with Map impl for patchdb indexed access
- Flatten HostAddress from single-variant enum to struct
- Replace set-gateway-enabled RPC with set-address-enabled
- Remove hostname_info from Host; computed addresses now in BindInfo.addresses.possible
- Compute possible addresses inline in NetServiceData::update()
- Update DB migration, SDK types, frontend, and container-runtime
This commit is contained in:
Aiden McClelland
2026-02-10 17:38:51 -07:00
parent 73274ef6e0
commit 4e638fb58e
33 changed files with 996 additions and 952 deletions

View File

@@ -29,6 +29,24 @@ make update-startbox REMOTE=start9@<ip> # Fastest iteration (binary + UI)
make test-core # Run Rust tests make test-core # Run Rust tests
``` ```
### Verifying code changes
When making changes across multiple layers (Rust, SDK, web, container-runtime), verify in this order:
1. **Rust**: `cargo check -p start-os` — verifies core compiles
2. **TS bindings**: `make ts-bindings` — regenerates TypeScript types from Rust `#[ts(export)]` structs
- Runs `./core/build/build-ts.sh` to export ts-rs types to `core/bindings/`
- Syncs `core/bindings/``sdk/base/lib/osBindings/` via rsync
- If you manually edit files in `sdk/base/lib/osBindings/`, you must still rebuild the SDK (step 3)
3. **SDK bundle**: `cd sdk && make baseDist dist` — compiles SDK source into packages
- `baseDist/` is consumed by `/web` (via `@start9labs/start-sdk-base`)
- `dist/` is consumed by `/container-runtime` (via `@start9labs/start-sdk`)
- Web and container-runtime reference the **built** SDK, not source files
4. **Web type check**: `cd web && npm run check` — type-checks all Angular projects
5. **Container runtime type check**: `cd container-runtime && npm run check` — type-checks the runtime
**Important**: Editing `sdk/base/lib/osBindings/*.ts` alone is NOT sufficient — you must rebuild the SDK bundle (step 3) before web/container-runtime can see the changes.
## Architecture ## Architecture
### Core (`/core`) ### Core (`/core`)

View File

@@ -33,114 +33,39 @@ Pending tasks for AI agents. Remove items when completed.
`preferred_external_port: 443` won't get 443 as its `assigned_ssl_port` (it's taken), but it CAN `preferred_external_port: 443` won't get 443 as its `assigned_ssl_port` (it's taken), but it CAN
still have domain vhost entries on port 443 — SNI routes by hostname. still have domain vhost entries on port 443 — SNI routes by hostname.
#### 1. Preferred Port Allocation for Ownership (`forward.rs`, `binding.rs`) #### 1. Preferred Port Allocation for Ownership ✅ DONE
Expand `AvailablePorts` to support trying a preferred port before falling back to the dynamic range: `AvailablePorts::try_alloc(port) -> Option<u16>` added to `forward.rs`. `BindInfo::new()` and
`BindInfo::update()` attempt the preferred port first, falling back to dynamic-range allocation.
- Add `try_alloc(port) -> Option<u16>`: Attempts to exclusively allocate a specific port. Returns #### 2. Per-Address Enable/Disable ✅ DONE
`None` if the port is already allocated or restricted.
- Enforce the restricted port list (currently noted in `vhost.rs:89`: `<=1024, >=32768, 5355, 5432,
9050, 6010, 9051, 5353`) — skip the preferred port if restricted, except for ports the OS itself
uses (80, 443).
- No SSL-vs-non-SSL distinction or refcounting needed at this layer — ownership is always exclusive.
SSL port sharing for domains is handled entirely by the VHostController via SNI.
Modify `BindInfo::new()` and `BindInfo::update()` to attempt the preferred port first: Gateway-level `private_disabled`/`public_enabled` on `NetInfo` replaced with per-address
`DerivedAddressInfo` on `BindInfo`. `hostname_info` removed from `Host` — computed addresses now
live in `BindInfo.addresses.possible`.
``` **`DerivedAddressInfo` struct** (on `BindInfo`):
assigned_ssl_port = try_alloc(ssl.preferred_external_port)
.unwrap_or(dynamic_pool.alloc())
assigned_port = try_alloc(options.preferred_external_port)
.unwrap_or(dynamic_pool.alloc())
```
After this change, `assigned_ssl_port` may match the preferred port if it was available, or fall back
to the dynamic range as before.
#### 2. Per-Address Enable/Disable (replaces gateway overrides)
**Current model being removed**: `NetInfo` has `private_disabled: OrdSet<GatewayId>` and
`public_enabled: OrdSet<GatewayId>` — gateway-level toggles where private gateways are enabled by
default and public gateways are disabled by default. The `set-gateway-enabled` RPC endpoint and the
`InterfaceFilter` impl on `NetInfo` use these sets. This model is unintuitive because users think in
terms of individual addresses, not gateways.
**New model**: Per-address enable/disable using `DerivedAddressInfo` on `BindInfo`. Instead of
gateway-level toggles, users toggle individual addresses. The `hostnameInfo` field moves from `Host`
to `BindInfo.addresses` (as the computed `possible` set).
**`DerivedAddressInfo` struct** (added to `BindInfo`):
```rust ```rust
pub struct DerivedAddressInfo { pub struct DerivedAddressInfo {
/// User-controlled: private-gateway addresses the user has disabled
pub private_disabled: BTreeSet<HostnameInfo>, pub private_disabled: BTreeSet<HostnameInfo>,
/// User-controlled: public-gateway addresses the user has enabled
pub public_enabled: BTreeSet<HostnameInfo>, pub public_enabled: BTreeSet<HostnameInfo>,
/// COMPUTED by update(): all possible addresses for this binding pub possible: BTreeSet<HostnameInfo>, // COMPUTED by update()
pub possible: BTreeSet<HostnameInfo>,
} }
``` ```
`DerivedAddressInfo::enabled()` returns `possible` filtered by the two sets: private addresses are `DerivedAddressInfo::enabled()` returns `possible` filtered by the two sets. `HostnameInfo` derives
enabled by default (disabled if in `private_disabled`), public addresses are disabled by default `Ord` for `BTreeSet` usage. `AddressFilter` (implementing `InterfaceFilter`) derives enabled
(enabled if in `public_enabled`). Requires `HostnameInfo` to derive `Ord` for `BTreeSet` usage. gateway set from `DerivedAddressInfo` for vhost/forward filtering.
**How disabling works per address type**: **RPC endpoint**: `set-gateway-enabled` replaced with `set-address-enabled` (on both
`server.host.binding` and `package.host.binding`).
The enforcement mechanism varies by address type because different addresses are reached through **How disabling works per address type** (enforcement deferred to Section 3):
different network paths:
- **WAN IP:port** (public gateway IP addresses): Disabled via **source-IP gating** in the vhost - **WAN/LAN IP:port**: Will be enforced via **source-IP gating** in the vhost layer (Section 3).
layer (Section 3). Public and private traffic share the same port listener, so we can't just
remove the vhost entry — that would also block private traffic. Instead, the vhost target is
tagged with which source-IP classes it accepts. When a WAN IP address is disabled, the vhost
target rejects connections whose source IP matches the gateway (i.e., NAT'd internet traffic)
or falls outside the gateway's LAN subnets. LAN traffic to the same port is unaffected.
- **LAN IP:port** (private gateway IP addresses): Also enforced via **source-IP gating**. When
disabled, the vhost target rejects connections from LAN subnets on that gateway. This is the
inverse of the WAN case — same mechanism, different source-IP class.
- **Hostname-based addresses** (`.local`, domains): Disabled by **not creating the vhost/SNI - **Hostname-based addresses** (`.local`, domains): Disabled by **not creating the vhost/SNI
entry** for that hostname. Since hostname-based routing uses SNI (SSL) or Host header (HTTP), entry** for that hostname.
removing the entry means the hostname simply doesn't resolve to a backend. No traffic reaches
the service for that hostname.
**Backend changes**:
- **Remove from `NetInfo`**: Delete the `private_disabled` and `public_enabled` fields entirely.
`NetInfo` becomes just `{ assigned_port: Option<u16>, assigned_ssl_port: Option<u16> }`.
- **Add `addresses: DerivedAddressInfo` to `BindInfo`**: User-controlled sets (`private_disabled`,
`public_enabled`) are preserved across updates; `possible` is recomputed by `update()`.
- **Remove `hostname_info` from `Host`**: Computed addresses now live in `BindInfo.addresses.possible`
instead of being a top-level field on `Host` that was never persisted to the DB.
- **Default behavior preserved**: Private-gateway addresses default to enabled, public-gateway
addresses default to disabled, via the `enabled()` method on `DerivedAddressInfo`.
- **Remove `set-gateway-enabled`** RPC endpoint from `binding.rs`.
- **Remove `InterfaceFilter` impl for `NetInfo`**: The per-gateway filter logic is replaced by
per-address filtering derived from `DerivedAddressInfo`.
**New RPC endpoint** (`binding.rs`):
Following the existing `HostApiKind` pattern, replace `set-gateway-enabled` with:
- **`set-address-enabled`** — Toggle an individual address on or off.
```ts
interface BindingSetAddressEnabledParams {
internalPort: number
address: HostnameInfo // identifies the address directly (no separate AddressId type needed)
enabled: boolean | null // null = reset to default
}
```
Mutates `BindInfo.addresses.private_disabled` / `.public_enabled` based on the address's `public`
field. If `public == true` and enabled, add to `public_enabled`; if disabled, remove. If
`public == false` and enabled, remove from `private_disabled`; if disabled, add. Uses `sync_db`
metadata.
This yields two RPC methods:
- `server.host.binding.set-address-enabled`
- `package.host.binding.set-address-enabled`
#### 3. Eliminate the Port 5443 Hack: Source-IP-Based WAN Blocking (`vhost.rs`, `net_controller.rs`) #### 3. Eliminate the Port 5443 Hack: Source-IP-Based WAN Blocking (`vhost.rs`, `net_controller.rs`)
@@ -206,7 +131,7 @@ Pending tasks for AI agents. Remove items when completed.
##### View Page ##### View Page
Displays all computed addresses for the interface (from `hostname_info`) as a flat list. For each Displays all computed addresses for the interface (from `BindInfo.addresses`) as a flat list. For each
address, show: URL, type (IPv4, IPv6, .local, domain), access level (public/private), address, show: URL, type (IPv4, IPv6, .local, domain), access level (public/private),
gateway name, SSL indicator, enable/disable state, port forward info for public addresses, and a test button gateway name, SSL indicator, enable/disable state, port forward info for public addresses, and a test button
for reachability (see Section 7). for reachability (see Section 7).
@@ -282,13 +207,13 @@ Pending tasks for AI agents. Remove items when completed.
| File | Role | | File | Role |
|------|------| |------|------|
| `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation | | `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation, `try_alloc()` for preferred ports |
| `core/src/net/host/binding.rs` | `BindInfo`/`NetInfo`/`DerivedAddressInfo` — remove gateway overrides, add per-address enable/disable sets, new RPC endpoints | | `core/src/net/host/binding.rs` | `Bindings` (Map wrapper for patchdb), `BindInfo`/`NetInfo`/`DerivedAddressInfo`/`AddressFilter` — per-address enable/disable, `set-address-enabled` RPC |
| `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — compute `enabled` on `HostnameInfo`, vhost/forward/DNS reconciliation, 5443 hack removal | | `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — computes `DerivedAddressInfo.possible`, vhost/forward/DNS reconciliation, 5443 hack removal |
| `core/src/net/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private | | `core/src/net/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private |
| `core/src/net/gateway.rs` | `InterfaceFilter` — remove `NetInfo` impl, simplify | | `core/src/net/gateway.rs` | `InterfaceFilter` trait and filter types (`AddressFilter`, `PublicFilter`, etc.) |
| `core/src/net/service_interface.rs` | `HostnameInfo` — add `Ord` derives for use in `BTreeSet` | | `core/src/net/service_interface.rs` | `HostnameInfo` — derives `Ord` for `BTreeSet` usage |
| `core/src/net/host/address.rs` | Existing domain/onion CRUD endpoints (no changes needed) | | `core/src/net/host/address.rs` | `HostAddress` (flattened struct), domain CRUD endpoints |
| `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed | | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed |
| `core/src/db/model/public.rs` | Public DB model — port forward mapping | | `core/src/db/model/public.rs` | Public DB model — port forward mapping |

View File

@@ -82,17 +82,14 @@ export class DockerProcedureContainer extends Drop {
}), }),
) )
} else if (volumeMount.type === "certificate") { } else if (volumeMount.type === "certificate") {
const hostInfo = await effects.getHostInfo({
hostId: volumeMount["interface-id"],
})
const hostnames = [ const hostnames = [
`${packageId}.embassy`, `${packageId}.embassy`,
...new Set( ...new Set(
Object.values( Object.values(hostInfo?.bindings || {})
( .flatMap((b) => b.addresses.possible)
await effects.getHostInfo({
hostId: volumeMount["interface-id"],
})
)?.hostnameInfo || {},
)
.flatMap((h) => h)
.map((h) => h.hostname.value), .map((h) => h.hostname.value),
).values(), ).values(),
] ]

View File

@@ -1244,8 +1244,8 @@ async function updateConfig(
? "" ? ""
: catchFn( : catchFn(
() => () =>
filled.addressInfo!.filter({ kind: "mdns" })! filled.addressInfo!.filter({ kind: "mdns" })!.hostnames[0]
.hostnames[0].hostname.value, .hostname.value,
) || "" ) || ""
mutConfigValue[key] = url mutConfigValue[key] = url
} }

View File

@@ -20,7 +20,7 @@ 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, NetInfo}; use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, Bindings, DerivedAddressInfo, NetInfo};
use crate::net::utils::ipv6_is_local; use crate::net::utils::ipv6_is_local;
use crate::net::vhost::AlpnInfo; use crate::net::vhost::AlpnInfo;
use crate::prelude::*; use crate::prelude::*;
@@ -63,7 +63,8 @@ impl Public {
post_init_migration_todos: BTreeMap::new(), post_init_migration_todos: BTreeMap::new(),
network: NetworkInfo { network: NetworkInfo {
host: Host { host: Host {
bindings: [( bindings: Bindings(
[(
80, 80,
BindInfo { BindInfo {
enabled: false, enabled: false,
@@ -82,16 +83,15 @@ impl Public {
net: NetInfo { net: NetInfo {
assigned_port: None, assigned_port: None,
assigned_ssl_port: Some(443), assigned_ssl_port: Some(443),
private_disabled: OrdSet::new(),
public_enabled: OrdSet::new(),
}, },
addresses: DerivedAddressInfo::default(),
}, },
)] )]
.into_iter() .into_iter()
.collect(), .collect(),
),
public_domains: BTreeMap::new(), public_domains: BTreeMap::new(),
private_domains: BTreeSet::new(), private_domains: BTreeSet::new(),
hostname_info: BTreeMap::new(),
}, },
wifi: WifiInfo { wifi: WifiInfo {
enabled: true, enabled: true,

View File

@@ -4,8 +4,8 @@ use std::sync::{Arc, Weak};
use std::time::Duration; use std::time::Duration;
use futures::channel::oneshot; use futures::channel::oneshot;
use id_pool::IdPool;
use iddqd::{IdOrdItem, IdOrdMap}; use iddqd::{IdOrdItem, IdOrdMap};
use rand::Rng;
use imbl::OrdMap; use imbl::OrdMap;
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async}; use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -23,25 +23,49 @@ use crate::util::serde::{HandlerExtSerde, display_serializable};
use crate::util::sync::Watch; use crate::util::sync::Watch;
pub const START9_BRIDGE_IFACE: &str = "lxcbr0"; pub const START9_BRIDGE_IFACE: &str = "lxcbr0";
pub const FIRST_DYNAMIC_PRIVATE_PORT: u16 = 49152; const EPHEMERAL_PORT_START: u16 = 49152;
// vhost.rs:89 — not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353
const RESTRICTED_PORTS: &[u16] = &[5353, 5355, 5432, 6010, 9050, 9051];
fn is_restricted(port: u16) -> bool {
port <= 1024 || RESTRICTED_PORTS.contains(&port)
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct AvailablePorts(IdPool); pub struct AvailablePorts(BTreeMap<u16, bool>);
impl AvailablePorts { impl AvailablePorts {
pub fn new() -> Self { pub fn new() -> Self {
Self(IdPool::new_ranged(FIRST_DYNAMIC_PRIVATE_PORT..u16::MAX)) Self(BTreeMap::new())
} }
pub fn alloc(&mut self) -> Result<u16, Error> { pub fn alloc(&mut self, ssl: bool) -> Result<u16, Error> {
self.0.request_id().ok_or_else(|| { let mut rng = rand::rng();
Error::new( for _ in 0..1000 {
let port = rng.random_range(EPHEMERAL_PORT_START..u16::MAX);
if !self.0.contains_key(&port) {
self.0.insert(port, ssl);
return Ok(port);
}
}
Err(Error::new(
eyre!("{}", t!("net.forward.no-dynamic-ports-available")), eyre!("{}", t!("net.forward.no-dynamic-ports-available")),
ErrorKind::Network, ErrorKind::Network,
) ))
}) }
/// Try to allocate a specific port. Returns Some(port) if available, None if taken/restricted.
pub fn try_alloc(&mut self, port: u16, ssl: bool) -> Option<u16> {
if is_restricted(port) || self.0.contains_key(&port) {
return None;
}
self.0.insert(port, ssl);
Some(port)
}
/// Returns whether a given allocated port is SSL.
pub fn is_ssl(&self, port: u16) -> bool {
self.0.get(&port).copied().unwrap_or(false)
} }
pub fn free(&mut self, ports: impl IntoIterator<Item = u16>) { pub fn free(&mut self, ports: impl IntoIterator<Item = u16>) {
for port in ports { for port in ports {
self.0.return_id(port).unwrap_or_default(); self.0.remove(&port);
} }
} }
} }

View File

@@ -1685,19 +1685,9 @@ where
#[test] #[test]
fn test_filter() { fn test_filter() {
use crate::net::host::binding::NetInfo;
let wg1 = "wg1".parse::<GatewayId>().unwrap(); let wg1 = "wg1".parse::<GatewayId>().unwrap();
assert!(!InterfaceFilter::filter( assert!(!InterfaceFilter::filter(
&AndFilter( &AndFilter(IdFilter(wg1.clone()), PublicFilter { public: false }).into_dyn(),
NetInfo {
private_disabled: [wg1.clone()].into_iter().collect(),
public_enabled: Default::default(),
assigned_port: None,
assigned_ssl_port: None,
},
AndFilter(IdFilter(wg1.clone()), PublicFilter { public: false }),
)
.into_dyn(),
&wg1, &wg1,
&NetworkInterfaceInfo { &NetworkInterfaceInfo {
name: None, name: None,

View File

@@ -16,15 +16,11 @@ use crate::prelude::*;
use crate::util::serde::{HandlerExtSerde, display_serializable}; use crate::util::serde::{HandlerExtSerde, display_serializable};
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")]
#[serde(rename_all_fields = "camelCase")] pub struct HostAddress {
#[serde(tag = "kind")] pub address: InternedString,
pub enum HostAddress { pub public: Option<PublicDomainConfig>,
Domain { pub private: bool,
address: InternedString,
public: Option<PublicDomainConfig>,
private: bool,
},
} }
#[derive(Debug, Clone, Deserialize, Serialize, TS)] #[derive(Debug, Clone, Deserialize, Serialize, TS)]
@@ -151,29 +147,18 @@ 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"]); table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]);
for address in &res { for entry in &res {
match address { if let Some(PublicDomainConfig { gateway, acme }) = &entry.public {
HostAddress::Domain {
address,
public: Some(PublicDomainConfig { gateway, acme }),
private,
} => {
table.add_row(row![ table.add_row(row![
address, entry.address,
&format!( &format!(
"{} ({gateway})", "{} ({gateway})",
if *private { "YES" } else { "ONLY" } if entry.private { "YES" } else { "ONLY" }
), ),
acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE") acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE")
]); ]);
} } else {
HostAddress::Domain { table.add_row(row![entry.address, &format!("NO"), "N/A"]);
address,
public: None,
..
} => {
table.add_row(row![address, &format!("NO"), "N/A"]);
}
} }
} }

View File

@@ -3,16 +3,17 @@ use std::str::FromStr;
use clap::Parser; use clap::Parser;
use clap::builder::ValueParserFactory; use clap::builder::ValueParserFactory;
use imbl::OrdSet;
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async}; use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::public::NetworkInterfaceInfo; use crate::db::model::public::NetworkInterfaceInfo;
use crate::db::prelude::Map;
use crate::net::forward::AvailablePorts; use crate::net::forward::AvailablePorts;
use crate::net::gateway::InterfaceFilter; use crate::net::gateway::InterfaceFilter;
use crate::net::host::HostApiKind; use crate::net::host::HostApiKind;
use crate::net::service_interface::HostnameInfo;
use crate::net::vhost::AlpnInfo; use crate::net::vhost::AlpnInfo;
use crate::prelude::*; use crate::prelude::*;
use crate::util::FromStrParser; use crate::util::FromStrParser;
@@ -45,51 +46,137 @@ impl FromStr for BindId {
} }
} }
#[derive(Debug, Deserialize, Serialize, TS)] #[derive(Debug, Default, Clone, Deserialize, Serialize, TS, HasModel)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
#[model = "Model<Self>"]
pub struct DerivedAddressInfo {
/// User-controlled: private-gateway addresses the user has disabled
pub private_disabled: BTreeSet<HostnameInfo>,
/// User-controlled: public-gateway addresses the user has enabled
pub public_enabled: BTreeSet<HostnameInfo>,
/// COMPUTED: NetServiceData::update — all possible addresses for this binding
pub possible: BTreeSet<HostnameInfo>,
}
impl DerivedAddressInfo {
/// Returns addresses that are currently enabled.
/// Private addresses are enabled by default (disabled if in private_disabled).
/// Public addresses are disabled by default (enabled if in public_enabled).
pub fn enabled(&self) -> BTreeSet<&HostnameInfo> {
self.possible
.iter()
.filter(|h| {
if h.public {
self.public_enabled.contains(h)
} else {
!self.private_disabled.contains(h)
}
})
.collect()
}
/// Derive a gateway-level InterfaceFilter from the enabled addresses.
/// A gateway passes the filter if it has any enabled address for this binding.
pub fn gateway_filter(&self) -> AddressFilter {
let enabled_gateways: BTreeSet<GatewayId> = self
.enabled()
.into_iter()
.map(|h| h.gateway.id.clone())
.collect();
AddressFilter(enabled_gateways)
}
}
/// Gateway-level filter derived from DerivedAddressInfo.
/// Passes if the gateway has at least one enabled address.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AddressFilter(pub BTreeSet<GatewayId>);
impl InterfaceFilter for AddressFilter {
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
info.ip_info.is_some() && self.0.contains(id)
}
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[model = "Model<Self>"]
#[ts(export)]
pub struct Bindings(pub BTreeMap<u16, BindInfo>);
impl Map for Bindings {
type Key = u16;
type Value = BindInfo;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Self::key_string(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(InternedString::from_display(key))
}
}
impl std::ops::Deref for Bindings {
type Target = BTreeMap<u16, BindInfo>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for Bindings {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct BindInfo { pub struct BindInfo {
pub enabled: bool, pub enabled: bool,
pub options: BindOptions, pub options: BindOptions,
pub net: NetInfo, pub net: NetInfo,
pub addresses: DerivedAddressInfo,
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct NetInfo { pub struct NetInfo {
#[ts(as = "BTreeSet::<GatewayId>")]
#[serde(default)]
pub private_disabled: OrdSet<GatewayId>,
#[ts(as = "BTreeSet::<GatewayId>")]
#[serde(default)]
pub public_enabled: OrdSet<GatewayId>,
pub assigned_port: Option<u16>, pub assigned_port: Option<u16>,
pub assigned_ssl_port: Option<u16>, pub assigned_ssl_port: Option<u16>,
} }
impl InterfaceFilter for NetInfo {
fn filter(&self, _id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
info.ip_info.is_some()
}
}
impl BindInfo { impl BindInfo {
pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result<Self, Error> { pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result<Self, Error> {
let mut assigned_port = None; let mut assigned_port = None;
let mut assigned_ssl_port = None; let mut assigned_ssl_port = None;
if options.add_ssl.is_some() { if let Some(ssl) = &options.add_ssl {
assigned_ssl_port = Some(available_ports.alloc()?); assigned_ssl_port = available_ports
.try_alloc(ssl.preferred_external_port, true)
.or_else(|| Some(available_ports.alloc(true).ok()?));
} }
if options if options
.secure .secure
.map_or(true, |s| !(s.ssl && options.add_ssl.is_some())) .map_or(true, |s| !(s.ssl && options.add_ssl.is_some()))
{ {
assigned_port = Some(available_ports.alloc()?); assigned_port = available_ports
.try_alloc(options.preferred_external_port, false)
.or_else(|| Some(available_ports.alloc(false).ok()?));
} }
Ok(Self { Ok(Self {
enabled: true, enabled: true,
options, options,
net: NetInfo { net: NetInfo {
private_disabled: OrdSet::new(),
public_enabled: OrdSet::new(),
assigned_port, assigned_port,
assigned_ssl_port, assigned_ssl_port,
}, },
addresses: DerivedAddressInfo::default(),
}) })
} }
pub fn update( pub fn update(
@@ -97,7 +184,11 @@ impl BindInfo {
available_ports: &mut AvailablePorts, available_ports: &mut AvailablePorts,
options: BindOptions, options: BindOptions,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let Self { net: mut lan, .. } = self; let Self {
net: mut lan,
addresses,
..
} = self;
if options if options
.secure .secure
.map_or(true, |s| !(s.ssl && options.add_ssl.is_some())) .map_or(true, |s| !(s.ssl && options.add_ssl.is_some()))
@@ -105,19 +196,26 @@ impl BindInfo {
{ {
lan.assigned_port = if let Some(port) = lan.assigned_port.take() { lan.assigned_port = if let Some(port) = lan.assigned_port.take() {
Some(port) Some(port)
} else if let Some(port) =
available_ports.try_alloc(options.preferred_external_port, false)
{
Some(port)
} else { } else {
Some(available_ports.alloc()?) Some(available_ports.alloc(false)?)
}; };
} else { } else {
if let Some(port) = lan.assigned_port.take() { if let Some(port) = lan.assigned_port.take() {
available_ports.free([port]); available_ports.free([port]);
} }
} }
if options.add_ssl.is_some() { if let Some(ssl) = &options.add_ssl {
lan.assigned_ssl_port = if let Some(port) = lan.assigned_ssl_port.take() { lan.assigned_ssl_port = if let Some(port) = lan.assigned_ssl_port.take() {
Some(port) Some(port)
} else if let Some(port) = available_ports.try_alloc(ssl.preferred_external_port, true)
{
Some(port)
} else { } else {
Some(available_ports.alloc()?) Some(available_ports.alloc(true)?)
}; };
} else { } else {
if let Some(port) = lan.assigned_ssl_port.take() { if let Some(port) = lan.assigned_ssl_port.take() {
@@ -128,22 +226,17 @@ impl BindInfo {
enabled: true, enabled: true,
options, options,
net: lan, net: lan,
addresses: DerivedAddressInfo {
private_disabled: addresses.private_disabled,
public_enabled: addresses.public_enabled,
possible: BTreeSet::new(),
},
}) })
} }
pub fn disable(&mut self) { pub fn disable(&mut self) {
self.enabled = false; self.enabled = false;
} }
} }
impl InterfaceFilter for NetInfo {
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
info.ip_info.is_some()
&& if info.public() {
self.public_enabled.contains(id)
} else {
!self.private_disabled.contains(id)
}
}
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, TS)] #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)] #[ts(export)]
@@ -188,7 +281,7 @@ pub fn binding<C: Context, Kind: HostApiKind>()
let mut table = Table::new(); let mut table = Table::new();
table.add_row(row![bc => "INTERNAL PORT", "ENABLED", "EXTERNAL PORT", "EXTERNAL SSL PORT"]); table.add_row(row![bc => "INTERNAL PORT", "ENABLED", "EXTERNAL PORT", "EXTERNAL SSL PORT"]);
for (internal, info) in res { for (internal, info) in res.iter() {
table.add_row(row![ table.add_row(row![
internal, internal,
info.enabled, info.enabled,
@@ -213,12 +306,12 @@ pub fn binding<C: Context, Kind: HostApiKind>()
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand( .subcommand(
"set-gateway-enabled", "set-address-enabled",
from_fn_async(set_gateway_enabled::<Kind>) from_fn_async(set_address_enabled::<Kind>)
.with_metadata("sync_db", Value::Bool(true)) .with_metadata("sync_db", Value::Bool(true))
.with_inherited(Kind::inheritance) .with_inherited(Kind::inheritance)
.no_display() .no_display()
.with_about("about.set-gateway-enabled-for-binding") .with_about("about.set-address-enabled-for-binding")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
} }
@@ -227,7 +320,7 @@ pub async fn list_bindings<Kind: HostApiKind>(
ctx: RpcContext, ctx: RpcContext,
_: Empty, _: Empty,
inheritance: Kind::Inheritance, inheritance: Kind::Inheritance,
) -> Result<BTreeMap<u16, BindInfo>, Error> { ) -> Result<Bindings, Error> {
Kind::host_for(&inheritance, &mut ctx.db.peek().await)? Kind::host_for(&inheritance, &mut ctx.db.peek().await)?
.as_bindings() .as_bindings()
.de() .de()
@@ -236,50 +329,44 @@ pub async fn list_bindings<Kind: HostApiKind>(
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct BindingGatewaySetEnabledParams { pub struct BindingSetAddressEnabledParams {
#[arg(help = "help.arg.internal-port")] #[arg(help = "help.arg.internal-port")]
internal_port: u16, internal_port: u16,
#[arg(help = "help.arg.gateway-id")] #[arg(long, help = "help.arg.address")]
gateway: GatewayId, address: String,
#[arg(long, help = "help.arg.binding-enabled")] #[arg(long, help = "help.arg.binding-enabled")]
enabled: Option<bool>, enabled: Option<bool>,
} }
pub async fn set_gateway_enabled<Kind: HostApiKind>( pub async fn set_address_enabled<Kind: HostApiKind>(
ctx: RpcContext, ctx: RpcContext,
BindingGatewaySetEnabledParams { BindingSetAddressEnabledParams {
internal_port, internal_port,
gateway, address,
enabled, enabled,
}: BindingGatewaySetEnabledParams, }: BindingSetAddressEnabledParams,
inheritance: Kind::Inheritance, inheritance: Kind::Inheritance,
) -> Result<(), Error> { ) -> Result<(), Error> {
let enabled = enabled.unwrap_or(true); let enabled = enabled.unwrap_or(true);
let gateway_public = ctx let address: HostnameInfo =
.net_controller serde_json::from_str(&address).with_kind(ErrorKind::Deserialization)?;
.net_iface
.watcher
.ip_info()
.get(&gateway)
.or_not_found(&gateway)?
.public();
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
Kind::host_for(&inheritance, db)? Kind::host_for(&inheritance, db)?
.as_bindings_mut() .as_bindings_mut()
.mutate(|b| { .mutate(|b| {
let net = &mut b.get_mut(&internal_port).or_not_found(internal_port)?.net; let bind = b.get_mut(&internal_port).or_not_found(internal_port)?;
if gateway_public { if address.public {
if enabled { if enabled {
net.public_enabled.insert(gateway); bind.addresses.public_enabled.insert(address.clone());
} else { } else {
net.public_enabled.remove(&gateway); bind.addresses.public_enabled.remove(&address);
} }
} else { } else {
if enabled { if enabled {
net.private_disabled.remove(&gateway); bind.addresses.private_disabled.remove(&address);
} else { } else {
net.private_disabled.insert(gateway); bind.addresses.private_disabled.insert(address.clone());
} }
} }
Ok(()) Ok(())

View File

@@ -13,8 +13,7 @@ use crate::context::RpcContext;
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
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, binding}; use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding};
use crate::net::service_interface::HostnameInfo;
use crate::prelude::*; use crate::prelude::*;
use crate::{HostId, PackageId}; use crate::{HostId, PackageId};
@@ -26,11 +25,9 @@ pub mod binding;
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)] #[ts(export)]
pub struct Host { pub struct Host {
pub bindings: BTreeMap<u16, BindInfo>, pub bindings: Bindings,
pub public_domains: BTreeMap<InternedString, PublicDomainConfig>, pub public_domains: BTreeMap<InternedString, PublicDomainConfig>,
pub private_domains: BTreeSet<InternedString>, pub private_domains: BTreeSet<InternedString>,
/// COMPUTED: NetService::update
pub hostname_info: BTreeMap<u16, Vec<HostnameInfo>>, // internal port -> Hostnames
} }
impl AsRef<Host> for Host { impl AsRef<Host> for Host {
@@ -45,7 +42,7 @@ impl Host {
pub fn addresses<'a>(&'a self) -> impl Iterator<Item = HostAddress> + 'a { pub fn addresses<'a>(&'a self) -> impl Iterator<Item = HostAddress> + 'a {
self.public_domains self.public_domains
.iter() .iter()
.map(|(address, config)| HostAddress::Domain { .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.contains(address),
@@ -54,7 +51,7 @@ impl Host {
self.private_domains self.private_domains
.iter() .iter()
.filter(|a| !self.public_domains.contains_key(*a)) .filter(|a| !self.public_domains.contains_key(*a))
.map(|address| HostAddress::Domain { .map(|address| HostAddress {
address: address.clone(), address: address.clone(),
public: None, public: None,
private: true, private: true,

View File

@@ -18,8 +18,8 @@ use crate::hostname::Hostname;
use crate::net::dns::DnsController; use crate::net::dns::DnsController;
use crate::net::forward::{InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule}; use crate::net::forward::{InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule};
use crate::net::gateway::{ use crate::net::gateway::{
AndFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, NetworkInterfaceController, OrFilter, AndFilter, AnyFilter, DynInterfaceFilter, IdFilter, InterfaceFilter,
PublicFilter, SecureFilter, NetworkInterfaceController, OrFilter, PublicFilter, SecureFilter,
}; };
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};
@@ -202,7 +202,7 @@ impl NetServiceData {
.as_entries_mut()? .as_entries_mut()?
{ {
host.as_bindings_mut().mutate(|b| { host.as_bindings_mut().mutate(|b| {
for (internal_port, info) in b { for (internal_port, info) in b.iter_mut() {
if !except.contains(&BindId { if !except.contains(&BindId {
id: host_id.clone(), id: host_id.clone(),
internal_port: *internal_port, internal_port: *internal_port,
@@ -233,7 +233,7 @@ impl NetServiceData {
.as_network_mut() .as_network_mut()
.as_host_mut(); .as_host_mut();
host.as_bindings_mut().mutate(|b| { host.as_bindings_mut().mutate(|b| {
for (internal_port, info) in b { for (internal_port, info) in b.iter_mut() {
if !except.contains(&BindId { if !except.contains(&BindId {
id: HostId::default(), id: HostId::default(),
internal_port: *internal_port, internal_port: *internal_port,
@@ -251,11 +251,15 @@ impl NetServiceData {
} }
} }
async fn update(&mut self, ctrl: &NetController, id: HostId, host: Host) -> Result<(), Error> { async fn update(
&mut self,
ctrl: &NetController,
id: HostId,
mut host: Host,
) -> Result<(), Error> {
let mut forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter)> = BTreeMap::new(); let mut forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter)> = BTreeMap::new();
let mut vhosts: BTreeMap<(Option<InternedString>, u16), ProxyTarget> = BTreeMap::new(); let mut vhosts: BTreeMap<(Option<InternedString>, u16), ProxyTarget> = BTreeMap::new();
let mut private_dns: BTreeSet<InternedString> = BTreeSet::new(); let mut private_dns: BTreeSet<InternedString> = BTreeSet::new();
let mut hostname_info: BTreeMap<u16, Vec<HostnameInfo>> = BTreeMap::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 peek = ctrl.db.peek().await;
@@ -264,12 +268,30 @@ impl NetServiceData {
let server_info = peek.as_public().as_server_info(); 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 hostname = server_info.as_hostname().de()?;
for (port, bind) in &host.bindings { let host_addresses: Vec<_> = host.addresses().collect();
for (port, bind) in host.bindings.iter_mut() {
if !bind.enabled { if !bind.enabled {
continue; continue;
} }
if bind.net.assigned_port.is_some() || bind.net.assigned_ssl_port.is_some() { if bind.net.assigned_port.is_none() && bind.net.assigned_ssl_port.is_none() {
continue;
}
let mut hostnames = BTreeSet::new(); let mut hostnames = BTreeSet::new();
let mut gw_filter = AnyFilter(
[PublicFilter { public: false }.into_dyn()]
.into_iter()
.chain(
bind.addresses
.public_enabled
.iter()
.map(|a| a.gateway.id.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.map(IdFilter)
.map(InterfaceFilter::into_dyn),
)
.collect(),
);
if let Some(ssl) = &bind.options.add_ssl { if let Some(ssl) = &bind.options.add_ssl {
let external = bind let external = bind
.net .net
@@ -289,23 +311,22 @@ impl NetServiceData {
vhosts.insert( vhosts.insert(
(hostname, external), (hostname, external),
ProxyTarget { ProxyTarget {
filter: bind.net.clone().into_dyn(), filter: gw_filter.clone().into_dyn(),
acme: None, acme: None,
addr, addr,
add_x_forwarded_headers: ssl.add_x_forwarded_headers, add_x_forwarded_headers: ssl.add_x_forwarded_headers,
connect_ssl: connect_ssl connect_ssl: connect_ssl
.clone() .clone()
.map(|_| ctrl.tls_client_config.clone()), .map(|_| ctrl.tls_client_config.clone()),
}, }, // TODO: allow public traffic?
); );
} }
for address in host.addresses() { for HostAddress {
match address {
HostAddress::Domain {
address, address,
public, public,
private, private,
} => { } in host_addresses.iter().cloned()
{
if hostnames.insert(address.clone()) { if hostnames.insert(address.clone()) {
let address = Some(address.clone()); let address = Some(address.clone());
if ssl.preferred_external_port == 443 { if ssl.preferred_external_port == 443 {
@@ -323,8 +344,7 @@ impl NetServiceData {
.into_dyn(), .into_dyn(),
acme: public.acme.clone(), acme: public.acme.clone(),
addr, addr,
add_x_forwarded_headers: ssl add_x_forwarded_headers: ssl.add_x_forwarded_headers,
.add_x_forwarded_headers,
connect_ssl: connect_ssl connect_ssl: connect_ssl
.clone() .clone()
.map(|_| ctrl.tls_client_config.clone()), .map(|_| ctrl.tls_client_config.clone()),
@@ -352,8 +372,7 @@ impl NetServiceData {
.into_dyn(), .into_dyn(),
acme: public.acme.clone(), acme: public.acme.clone(),
addr, addr,
add_x_forwarded_headers: ssl add_x_forwarded_headers: ssl.add_x_forwarded_headers,
.add_x_forwarded_headers,
connect_ssl: connect_ssl connect_ssl: connect_ssl
.clone() .clone()
.map(|_| ctrl.tls_client_config.clone()), .map(|_| ctrl.tls_client_config.clone()),
@@ -370,8 +389,7 @@ impl NetServiceData {
.into_dyn(), .into_dyn(),
acme: None, acme: None,
addr, addr,
add_x_forwarded_headers: ssl add_x_forwarded_headers: ssl.add_x_forwarded_headers,
.add_x_forwarded_headers,
connect_ssl: connect_ssl connect_ssl: connect_ssl
.clone() .clone()
.map(|_| ctrl.tls_client_config.clone()), .map(|_| ctrl.tls_client_config.clone()),
@@ -392,15 +410,13 @@ impl NetServiceData {
) )
.into_dyn() .into_dyn()
} else { } else {
IdFilter(public.gateway.clone()) IdFilter(public.gateway.clone()).into_dyn()
.into_dyn()
}, },
) )
.into_dyn(), .into_dyn(),
acme: public.acme.clone(), acme: public.acme.clone(),
addr, addr,
add_x_forwarded_headers: ssl add_x_forwarded_headers: ssl.add_x_forwarded_headers,
.add_x_forwarded_headers,
connect_ssl: connect_ssl connect_ssl: connect_ssl
.clone() .clone()
.map(|_| ctrl.tls_client_config.clone()), .map(|_| ctrl.tls_client_config.clone()),
@@ -417,8 +433,7 @@ impl NetServiceData {
.into_dyn(), .into_dyn(),
acme: None, acme: None,
addr, addr,
add_x_forwarded_headers: ssl add_x_forwarded_headers: ssl.add_x_forwarded_headers,
.add_x_forwarded_headers,
connect_ssl: connect_ssl connect_ssl: connect_ssl
.clone() .clone()
.map(|_| ctrl.tls_client_config.clone()), .map(|_| ctrl.tls_client_config.clone()),
@@ -429,8 +444,6 @@ impl NetServiceData {
} }
} }
} }
}
}
if bind if bind
.options .options
.secure .secure
@@ -451,8 +464,7 @@ impl NetServiceData {
), ),
); );
} }
let mut bind_hostname_info: Vec<HostnameInfo> = bind.addresses.possible.clear();
hostname_info.remove(port).unwrap_or_default();
for (gateway_id, info) in net_ifaces for (gateway_id, info) in net_ifaces
.iter() .iter()
.filter(|(_, info)| { .filter(|(_, info)| {
@@ -481,7 +493,7 @@ impl NetServiceData {
i.device_type != Some(NetworkInterfaceType::Wireguard) i.device_type != Some(NetworkInterfaceType::Wireguard)
}) })
{ {
bind_hostname_info.push(HostnameInfo { bind.addresses.possible.insert(HostnameInfo {
gateway: gateway.clone(), gateway: gateway.clone(),
public: false, public: false,
hostname: IpHostname::Local { hostname: IpHostname::Local {
@@ -494,19 +506,17 @@ impl NetServiceData {
}, },
}); });
} }
for address in host.addresses() { for HostAddress {
if let HostAddress::Domain {
address, address,
public, public,
private, private,
} = address } in host_addresses.iter().cloned()
{ {
if public.is_none() { if public.is_none() {
private_dns.insert(address.clone()); private_dns.insert(address.clone());
} }
let private = private && !info.public(); let private = private && !info.public();
let public = let public = public.as_ref().map_or(false, |p| &p.gateway == gateway_id);
public.as_ref().map_or(false, |p| &p.gateway == gateway_id);
if public || private { if public || private {
if bind if bind
.options .options
@@ -514,7 +524,7 @@ impl NetServiceData {
.as_ref() .as_ref()
.map_or(false, |ssl| ssl.preferred_external_port == 443) .map_or(false, |ssl| ssl.preferred_external_port == 443)
{ {
bind_hostname_info.push(HostnameInfo { bind.addresses.possible.insert(HostnameInfo {
gateway: gateway.clone(), gateway: gateway.clone(),
public, public,
hostname: IpHostname::Domain { hostname: IpHostname::Domain {
@@ -524,7 +534,7 @@ impl NetServiceData {
}, },
}); });
} else { } else {
bind_hostname_info.push(HostnameInfo { bind.addresses.possible.insert(HostnameInfo {
gateway: gateway.clone(), gateway: gateway.clone(),
public, public,
hostname: IpHostname::Domain { hostname: IpHostname::Domain {
@@ -536,11 +546,10 @@ impl NetServiceData {
} }
} }
} }
}
if let Some(ip_info) = &info.ip_info { if let Some(ip_info) = &info.ip_info {
let public = info.public(); let public = info.public();
if let Some(wan_ip) = ip_info.wan_ip { if let Some(wan_ip) = ip_info.wan_ip {
bind_hostname_info.push(HostnameInfo { bind.addresses.possible.insert(HostnameInfo {
gateway: gateway.clone(), gateway: gateway.clone(),
public: true, public: true,
hostname: IpHostname::Ipv4 { hostname: IpHostname::Ipv4 {
@@ -554,7 +563,7 @@ impl NetServiceData {
match ipnet { match ipnet {
IpNet::V4(net) => { IpNet::V4(net) => {
if !public { if !public {
bind_hostname_info.push(HostnameInfo { bind.addresses.possible.insert(HostnameInfo {
gateway: gateway.clone(), gateway: gateway.clone(),
public, public,
hostname: IpHostname::Ipv4 { hostname: IpHostname::Ipv4 {
@@ -566,7 +575,7 @@ impl NetServiceData {
} }
} }
IpNet::V6(net) => { IpNet::V6(net) => {
bind_hostname_info.push(HostnameInfo { bind.addresses.possible.insert(HostnameInfo {
gateway: gateway.clone(), gateway: gateway.clone(),
public: public && !ipv6_is_local(net.addr()), public: public && !ipv6_is_local(net.addr()),
hostname: IpHostname::Ipv6 { hostname: IpHostname::Ipv6 {
@@ -581,8 +590,6 @@ impl NetServiceData {
} }
} }
} }
hostname_info.insert(*port, bind_hostname_info);
}
} }
let all = binds let all = binds
@@ -673,11 +680,28 @@ 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_possible_mut()
.ser(&bind.addresses.possible)?;
}
}
Ok(())
})
.await;
res.result?;
if let Some(pkg_id) = self.id.as_ref() { 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)) { if let Some(cbs) = ctrl.callbacks.get_host_info(&(pkg_id.clone(), id)) {
cbs.call(vector![]).await?; cbs.call(vector![]).await?;
} }
} }
}
Ok(()) Ok(())
} }

View File

@@ -6,7 +6,7 @@ use ts_rs::TS;
use crate::{GatewayId, HostId, ServiceInterfaceId}; use crate::{GatewayId, HostId, ServiceInterfaceId};
#[derive(Clone, Debug, 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 {
@@ -20,7 +20,7 @@ impl HostnameInfo {
} }
} }
#[derive(Clone, Debug, 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 GatewayInfo { pub struct GatewayInfo {
@@ -29,7 +29,7 @@ pub struct GatewayInfo {
pub public: bool, pub public: bool,
} }
#[derive(Clone, Debug, 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")]
#[serde(rename_all_fields = "camelCase")] #[serde(rename_all_fields = "camelCase")]

View File

@@ -175,8 +175,12 @@ pub async fn remove_tunnel(
let host = host?; let host = host?;
host.as_bindings_mut().mutate(|b| { host.as_bindings_mut().mutate(|b| {
Ok(b.values_mut().for_each(|v| { Ok(b.values_mut().for_each(|v| {
v.net.private_disabled.remove(&id); v.addresses
v.net.public_enabled.remove(&id); .private_disabled
.retain(|h| h.gateway.id != id);
v.addresses
.public_enabled
.retain(|h| h.gateway.id != id);
})) }))
})?; })?;
} }

View File

@@ -60,10 +60,10 @@ pub async fn get_ssl_certificate(
.into_iter() .into_iter()
.chain(m.as_private_domains().de()?) .chain(m.as_private_domains().de()?)
.chain( .chain(
m.as_hostname_info() m.as_bindings()
.de()? .de()?
.values() .values()
.flatten() .flat_map(|b| b.addresses.possible.iter().cloned())
.map(|h| h.to_san_hostname()), .map(|h| h.to_san_hostname()),
) )
.collect::<Vec<InternedString>>()) .collect::<Vec<InternedString>>())
@@ -184,10 +184,10 @@ pub async fn get_ssl_key(
.into_iter() .into_iter()
.chain(m.as_private_domains().de()?) .chain(m.as_private_domains().de()?)
.chain( .chain(
m.as_hostname_info() m.as_bindings()
.de()? .de()?
.values() .values()
.flatten() .flat_map(|b| b.addresses.possible.iter().cloned())
.map(|h| h.to_san_hostname()), .map(|h| h.to_san_hostname()),
) )
.collect::<Vec<InternedString>>()) .collect::<Vec<InternedString>>())

View File

@@ -57,29 +57,6 @@ impl VersionT for Version {
} }
} }
// Remove onion entries from hostnameInfo in server host
migrate_hostname_info(
db.get_mut("public")
.and_then(|p| p.get_mut("serverInfo"))
.and_then(|s| s.get_mut("network"))
.and_then(|n| n.get_mut("host")),
);
// Remove onion entries from hostnameInfo in all package hosts
if let Some(packages) = db
.get_mut("public")
.and_then(|p| p.get_mut("packageData"))
.and_then(|p| p.as_object_mut())
{
for (_, package) in packages.iter_mut() {
if let Some(hosts) = package.get_mut("hosts").and_then(|h| h.as_object_mut()) {
for (_, host) in hosts.iter_mut() {
migrate_hostname_info(Some(host));
}
}
}
}
// Remove onion store from private keyStore // Remove onion store from private keyStore
if let Some(key_store) = db if let Some(key_store) = db
.get_mut("private") .get_mut("private")
@@ -89,6 +66,29 @@ impl VersionT for Version {
key_store.remove("onion"); key_store.remove("onion");
} }
// Migrate server host: remove hostnameInfo, add addresses to bindings, clean net
migrate_host(
db.get_mut("public")
.and_then(|p| p.get_mut("serverInfo"))
.and_then(|s| s.get_mut("network"))
.and_then(|n| n.get_mut("host")),
);
// Migrate all package hosts
if let Some(packages) = db
.get_mut("public")
.and_then(|p| p.get_mut("packageData"))
.and_then(|p| p.as_object_mut())
{
for (_, package) in packages.iter_mut() {
if let Some(hosts) = package.get_mut("hosts").and_then(|h| h.as_object_mut()) {
for (_, host) in hosts.iter_mut() {
migrate_host(Some(host));
}
}
}
}
Ok(Value::Null) Ok(Value::Null)
} }
fn down(self, _db: &mut Value) -> Result<(), Error> { fn down(self, _db: &mut Value) -> Result<(), Error> {
@@ -96,20 +96,35 @@ impl VersionT for Version {
} }
} }
fn migrate_hostname_info(host: Option<&mut Value>) { fn migrate_host(host: Option<&mut Value>) {
if let Some(hostname_info) = host let Some(host) = host.and_then(|h| h.as_object_mut()) else {
.and_then(|h| h.get_mut("hostnameInfo")) return;
.and_then(|h| h.as_object_mut()) };
{
for (_, infos) in hostname_info.iter_mut() { // Remove hostnameInfo from host
if let Some(arr) = infos.as_array_mut() { host.remove("hostnameInfo");
// Remove onion entries
arr.retain(|info| info.get("kind").and_then(|k| k.as_str()) != Some("onion")); // For each binding: add "addresses" field, remove gateway-level fields from "net"
// Strip "kind" field from remaining entries (HostnameInfo flattened from enum to struct) if let Some(bindings) = host.get_mut("bindings").and_then(|b| b.as_object_mut()) {
for info in arr.iter_mut() { for (_, binding) in bindings.iter_mut() {
if let Some(obj) = info.as_object_mut() { if let Some(binding_obj) = binding.as_object_mut() {
obj.remove("kind"); // Add addresses if not present
if !binding_obj.contains_key("addresses") {
binding_obj.insert(
"addresses".into(),
serde_json::json!({
"privateDisabled": [],
"publicEnabled": [],
"possible": []
})
.into(),
);
} }
// Remove gateway-level privateDisabled/publicEnabled from net
if let Some(net) = binding_obj.get_mut("net").and_then(|n| n.as_object_mut()) {
net.remove("privateDisabled");
net.remove("publicEnabled");
} }
} }
} }

View File

@@ -1,5 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BindOptions } from './BindOptions' import type { BindOptions } from './BindOptions'
import type { DerivedAddressInfo } from './DerivedAddressInfo'
import type { NetInfo } from './NetInfo' import type { NetInfo } from './NetInfo'
export type BindInfo = { enabled: boolean; options: BindOptions; net: NetInfo } export type BindInfo = {
enabled: boolean
options: BindOptions
net: NetInfo
addresses: DerivedAddressInfo
}

View File

@@ -1,8 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from './GatewayId'
export type BindingGatewaySetEnabledParams = { export type BindingSetAddressEnabledParams = {
internalPort: number internalPort: number
gateway: GatewayId address: string
enabled: boolean | null enabled: boolean | null
} }

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BindInfo } from './BindInfo'
export type Bindings = { [key: number]: BindInfo }

View File

@@ -0,0 +1,17 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HostnameInfo } from './HostnameInfo'
export type DerivedAddressInfo = {
/**
* User-controlled: private-gateway addresses the user has disabled
*/
privateDisabled: Array<HostnameInfo>
/**
* User-controlled: public-gateway addresses the user has enabled
*/
publicEnabled: Array<HostnameInfo>
/**
* COMPUTED: NetServiceData::update — all possible addresses for this binding
*/
possible: Array<HostnameInfo>
}

View File

@@ -1,14 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BindInfo } from './BindInfo' import type { Bindings } from './Bindings'
import type { HostnameInfo } from './HostnameInfo'
import type { PublicDomainConfig } from './PublicDomainConfig' import type { PublicDomainConfig } from './PublicDomainConfig'
export type Host = { export type Host = {
bindings: { [key: number]: BindInfo } bindings: Bindings
publicDomains: { [key: string]: PublicDomainConfig } publicDomains: { [key: string]: PublicDomainConfig }
privateDomains: Array<string> privateDomains: Array<string>
/**
* COMPUTED: NetService::update
*/
hostnameInfo: { [key: number]: Array<HostnameInfo> }
} }

View File

@@ -1,9 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from './GatewayId'
export type NetInfo = { export type NetInfo = {
privateDisabled: Array<GatewayId>
publicEnabled: Array<GatewayId>
assignedPort: number | null assignedPort: number | null
assignedSslPort: number | null assignedSslPort: number | null
} }

View File

@@ -36,7 +36,8 @@ export { BackupTargetFS } from './BackupTargetFS'
export { Base64 } from './Base64' export { Base64 } from './Base64'
export { BindId } from './BindId' export { BindId } from './BindId'
export { BindInfo } from './BindInfo' export { BindInfo } from './BindInfo'
export { BindingGatewaySetEnabledParams } from './BindingGatewaySetEnabledParams' export { BindingSetAddressEnabledParams } from './BindingSetAddressEnabledParams'
export { Bindings } from './Bindings'
export { BindOptions } from './BindOptions' export { BindOptions } from './BindOptions'
export { BindParams } from './BindParams' export { BindParams } from './BindParams'
export { Blake3Commitment } from './Blake3Commitment' export { Blake3Commitment } from './Blake3Commitment'
@@ -64,6 +65,7 @@ export { Dependencies } from './Dependencies'
export { DependencyMetadata } from './DependencyMetadata' export { DependencyMetadata } from './DependencyMetadata'
export { DependencyRequirement } from './DependencyRequirement' export { DependencyRequirement } from './DependencyRequirement'
export { DepInfo } from './DepInfo' export { DepInfo } from './DepInfo'
export { DerivedAddressInfo } from './DerivedAddressInfo'
export { Description } from './Description' export { Description } from './Description'
export { DesiredStatus } from './DesiredStatus' export { DesiredStatus } from './DesiredStatus'
export { DestroySubcontainerFsParams } from './DestroySubcontainerFsParams' export { DestroySubcontainerFsParams } from './DestroySubcontainerFsParams'

View File

@@ -1,6 +1,12 @@
import { PackageId, ServiceInterfaceId, ServiceInterfaceType } from '../types' import { PackageId, ServiceInterfaceId, ServiceInterfaceType } from '../types'
import { knownProtocols } from '../interfaces/Host' import { knownProtocols } from '../interfaces/Host'
import { AddressInfo, Host, Hostname, HostnameInfo } from '../types' import {
AddressInfo,
DerivedAddressInfo,
Host,
Hostname,
HostnameInfo,
} from '../types'
import { Effects } from '../Effects' import { Effects } from '../Effects'
import { DropGenerator, DropPromise } from './Drop' import { DropGenerator, DropPromise } from './Drop'
import { IpAddress, IPV6_LINK_LOCAL } from './ip' import { IpAddress, IPV6_LINK_LOCAL } from './ip'
@@ -220,6 +226,14 @@ function filterRec(
return hostnames return hostnames
} }
function enabledAddresses(addr: DerivedAddressInfo): HostnameInfo[] {
return addr.possible.filter((h) =>
h.public
? addr.publicEnabled.some((e) => deepEqual(e, h))
: !addr.privateDisabled.some((d) => deepEqual(d, h)),
)
}
export const filledAddress = ( export const filledAddress = (
host: Host, host: Host,
addressInfo: AddressInfo, addressInfo: AddressInfo,
@@ -229,7 +243,8 @@ export const filledAddress = (
const u = toUrls(h) const u = toUrls(h)
return [u.url, u.sslUrl].filter((u) => u !== null) return [u.url, u.sslUrl].filter((u) => u !== null)
} }
const hostnames = host.hostnameInfo[addressInfo.internalPort] ?? [] const binding = host.bindings[addressInfo.internalPort]
const hostnames = binding ? enabledAddresses(binding.addresses) : []
function filledAddressFromHostnames<F extends Filter>( function filledAddressFromHostnames<F extends Filter>(
hostnames: HostnameInfo[], hostnames: HostnameInfo[],

View File

@@ -83,32 +83,8 @@ export class InterfaceGatewaysComponent {
readonly gateways = input.required<InterfaceGateway[] | undefined>() readonly gateways = input.required<InterfaceGateway[] | undefined>()
async onToggle(gateway: InterfaceGateway) { async onToggle(_gateway: InterfaceGateway) {
const addressInfo = this.interface.value()!.addressInfo // TODO: Replace with per-address toggle UI (Section 6 frontend overhaul).
const pkgId = this.interface.packageId() // Gateway-level toggle replaced by set-address-enabled RPC.
const loader = this.loader.open().subscribe()
try {
if (pkgId) {
await this.api.pkgBindingToggleGateway({
gateway: gateway.id,
enabled: !gateway.enabled,
internalPort: addressInfo.internalPort,
host: addressInfo.hostId,
package: pkgId,
})
} else {
await this.api.serverBindingToggleGateway({
gateway: gateway.id,
enabled: !gateway.enabled,
internalPort: 80,
})
}
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
} }
} }

View File

@@ -252,10 +252,16 @@ export class InterfaceService {
serviceInterface: T.ServiceInterface, serviceInterface: T.ServiceInterface,
host: T.Host, host: T.Host,
): T.HostnameInfo[] { ): T.HostnameInfo[] {
let hostnameInfo = const binding =
host.hostnameInfo[serviceInterface.addressInfo.internalPort] host.bindings[serviceInterface.addressInfo.internalPort]
return ( if (!binding) return []
hostnameInfo?.filter( const addr = binding.addresses
const enabled = addr.possible.filter(h =>
h.public
? addr.publicEnabled.some(e => utils.deepEqual(e, h))
: !addr.privateDisabled.some(d => utils.deepEqual(d, h)),
)
return enabled.filter(
h => h =>
this.config.accessType === 'localhost' || this.config.accessType === 'localhost' ||
!( !(
@@ -263,7 +269,6 @@ export class InterfaceService {
utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) || utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) ||
h.gateway.id === 'lo' h.gateway.id === 'lo'
), ),
) || []
) )
} }

View File

@@ -135,8 +135,8 @@ export default class ServiceInterfaceRoute {
gateways.map(g => ({ gateways.map(g => ({
enabled: enabled:
(g.public (g.public
? binding?.net.publicEnabled.includes(g.id) ? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id)
: !binding?.net.privateDisabled.includes(g.id)) ?? false, : !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false,
...g, ...g,
})) || [], })) || [],
publicDomains: getPublicDomains(host.publicDomains, gateways), publicDomains: getPublicDomains(host.publicDomains, gateways),

View File

@@ -96,8 +96,8 @@ export default class StartOsUiComponent {
gateways: gateways.map(g => ({ gateways: gateways.map(g => ({
enabled: enabled:
(g.public (g.public
? binding?.net.publicEnabled.includes(g.id) ? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id)
: !binding?.net.privateDisabled.includes(g.id)) ?? false, : !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false,
...g, ...g,
})), })),
publicDomains: getPublicDomains(network.host.publicDomains, gateways), publicDomains: getPublicDomains(network.host.publicDomains, gateways),

View File

@@ -2126,22 +2126,12 @@ export namespace Mock {
net: { net: {
assignedPort: 80, assignedPort: 80,
assignedSslPort: 443, assignedSslPort: 443,
publicEnabled: [], },
addresses: {
privateDisabled: [], privateDisabled: [],
}, publicEnabled: [],
options: { possible: [
addSsl: null,
preferredExternalPort: 443,
secure: { ssl: true },
},
},
},
publicDomains: {},
privateDomains: [],
hostnameInfo: {
80: [
{ {
kind: 'ip',
gateway: { id: 'eth0', name: 'Ethernet', public: false }, gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -2152,7 +2142,6 @@ export namespace Mock {
}, },
}, },
{ {
kind: 'ip',
gateway: { id: 'wlan0', name: 'Wireless', public: false }, gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -2163,7 +2152,6 @@ export namespace Mock {
}, },
}, },
{ {
kind: 'ip',
gateway: { id: 'wlan0', name: 'Wireless', public: false }, gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -2174,7 +2162,6 @@ export namespace Mock {
}, },
}, },
{ {
kind: 'ip',
gateway: { id: 'wlan0', name: 'Wireless', public: false }, gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -2185,7 +2172,6 @@ export namespace Mock {
}, },
}, },
{ {
kind: 'ip',
gateway: { id: 'eth0', name: 'Ethernet', public: false }, gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -2197,7 +2183,6 @@ export namespace Mock {
}, },
}, },
{ {
kind: 'ip',
gateway: { id: 'wlan0', name: 'Wireless', public: false }, gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -2210,6 +2195,15 @@ export namespace Mock {
}, },
], ],
}, },
options: {
addSsl: null,
preferredExternalPort: 443,
secure: { ssl: true },
},
},
},
publicDomains: {},
privateDomains: [],
}, },
bcdefgh: { bcdefgh: {
bindings: { bindings: {
@@ -2218,8 +2212,11 @@ export namespace Mock {
net: { net: {
assignedPort: 8332, assignedPort: 8332,
assignedSslPort: null, assignedSslPort: null,
publicEnabled: [], },
addresses: {
privateDisabled: [], privateDisabled: [],
publicEnabled: [],
possible: [],
}, },
options: { options: {
addSsl: null, addSsl: null,
@@ -2230,9 +2227,6 @@ export namespace Mock {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
hostnameInfo: {
8332: [],
},
}, },
cdefghi: { cdefghi: {
bindings: { bindings: {
@@ -2241,8 +2235,11 @@ export namespace Mock {
net: { net: {
assignedPort: 8333, assignedPort: 8333,
assignedSslPort: null, assignedSslPort: null,
publicEnabled: [], },
addresses: {
privateDisabled: [], privateDisabled: [],
publicEnabled: [],
possible: [],
}, },
options: { options: {
addSsl: null, addSsl: null,
@@ -2253,9 +2250,6 @@ export namespace Mock {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
hostnameInfo: {
8333: [],
},
}, },
}, },
storeExposedDependents: [], storeExposedDependents: [],

View File

@@ -281,13 +281,13 @@ export namespace RR {
} }
export type RemoveAcmeRes = null export type RemoveAcmeRes = null
export type ServerBindingToggleGatewayReq = { export type ServerBindingSetAddressEnabledReq = {
// server.host.binding.set-gateway-enabled // server.host.binding.set-address-enabled
gateway: T.GatewayId
internalPort: 80 internalPort: 80
enabled: boolean address: string // JSON-serialized HostnameInfo
enabled: boolean | null // null = reset to default
} }
export type ServerBindingToggleGatewayRes = null export type ServerBindingSetAddressEnabledRes = null
export type OsUiAddPublicDomainReq = { export type OsUiAddPublicDomainReq = {
// server.host.address.domain.public.add // server.host.address.domain.public.add
@@ -315,16 +315,16 @@ export namespace RR {
} }
export type OsUiRemovePrivateDomainRes = null export type OsUiRemovePrivateDomainRes = null
export type PkgBindingToggleGatewayReq = Omit< export type PkgBindingSetAddressEnabledReq = Omit<
ServerBindingToggleGatewayReq, ServerBindingSetAddressEnabledReq,
'internalPort' 'internalPort'
> & { > & {
// package.host.binding.set-gateway-enabled // package.host.binding.set-address-enabled
internalPort: number internalPort: number
package: T.PackageId // string package: T.PackageId // string
host: T.HostId // string host: T.HostId // string
} }
export type PkgBindingToggleGatewayRes = null export type PkgBindingSetAddressEnabledRes = null
export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & { export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & {
// package.host.address.domain.public.add // package.host.address.domain.public.add

View File

@@ -336,9 +336,9 @@ export abstract class ApiService {
abstract removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes> abstract removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes>
abstract serverBindingToggleGateway( abstract serverBindingSetAddressEnabled(
params: RR.ServerBindingToggleGatewayReq, params: RR.ServerBindingSetAddressEnabledReq,
): Promise<RR.ServerBindingToggleGatewayRes> ): Promise<RR.ServerBindingSetAddressEnabledRes>
abstract osUiAddPublicDomain( abstract osUiAddPublicDomain(
params: RR.OsUiAddPublicDomainReq, params: RR.OsUiAddPublicDomainReq,
@@ -356,9 +356,9 @@ export abstract class ApiService {
params: RR.OsUiRemovePrivateDomainReq, params: RR.OsUiRemovePrivateDomainReq,
): Promise<RR.OsUiRemovePrivateDomainRes> ): Promise<RR.OsUiRemovePrivateDomainRes>
abstract pkgBindingToggleGateway( abstract pkgBindingSetAddressEnabled(
params: RR.PkgBindingToggleGatewayReq, params: RR.PkgBindingSetAddressEnabledReq,
): Promise<RR.PkgBindingToggleGatewayRes> ): Promise<RR.PkgBindingSetAddressEnabledRes>
abstract pkgAddPublicDomain( abstract pkgAddPublicDomain(
params: RR.PkgAddPublicDomainReq, params: RR.PkgAddPublicDomainReq,

View File

@@ -607,11 +607,11 @@ export class LiveApiService extends ApiService {
}) })
} }
async serverBindingToggleGateway( async serverBindingSetAddressEnabled(
params: RR.ServerBindingToggleGatewayReq, params: RR.ServerBindingSetAddressEnabledReq,
): Promise<RR.ServerBindingToggleGatewayRes> { ): Promise<RR.ServerBindingSetAddressEnabledRes> {
return this.rpcRequest({ return this.rpcRequest({
method: 'server.host.binding.set-gateway-enabled', method: 'server.host.binding.set-address-enabled',
params, params,
}) })
} }
@@ -652,11 +652,11 @@ export class LiveApiService extends ApiService {
}) })
} }
async pkgBindingToggleGateway( async pkgBindingSetAddressEnabled(
params: RR.PkgBindingToggleGatewayReq, params: RR.PkgBindingSetAddressEnabledReq,
): Promise<RR.PkgBindingToggleGatewayRes> { ): Promise<RR.PkgBindingSetAddressEnabledRes> {
return this.rpcRequest({ return this.rpcRequest({
method: 'package.host.binding.set-gateway-enabled', method: 'package.host.binding.set-address-enabled',
params, params,
}) })
} }

View File

@@ -1348,20 +1348,12 @@ export class MockApiService extends ApiService {
return null return null
} }
async serverBindingToggleGateway( async serverBindingSetAddressEnabled(
params: RR.ServerBindingToggleGatewayReq, params: RR.ServerBindingSetAddressEnabledReq,
): Promise<RR.ServerBindingToggleGatewayRes> { ): Promise<RR.ServerBindingSetAddressEnabledRes> {
await pauseFor(2000) await pauseFor(2000)
const patch = [ // Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
{
op: PatchOp.REPLACE,
path: `/serverInfo/network/host/bindings/${params.internalPort}/net/publicEnabled`,
value: params.enabled ? [params.gateway] : [],
},
]
this.mockRevision(patch)
return null return null
} }
@@ -1380,10 +1372,9 @@ export class MockApiService extends ApiService {
}, },
{ {
op: PatchOp.ADD, op: PatchOp.ADD,
path: `/serverInfo/host/hostnameInfo/80/0`, path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
value: { value: {
kind: 'ip', gateway: { id: 'eth0', name: 'Ethernet', public: false },
gatewayId: 'eth0',
public: true, public: true,
hostname: { hostname: {
kind: 'domain', kind: 'domain',
@@ -1412,7 +1403,7 @@ export class MockApiService extends ApiService {
}, },
{ {
op: PatchOp.REMOVE, op: PatchOp.REMOVE,
path: `/serverInfo/host/hostnameInfo/80/0`, path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
}, },
] ]
this.mockRevision(patch) this.mockRevision(patch)
@@ -1433,10 +1424,9 @@ export class MockApiService extends ApiService {
}, },
{ {
op: PatchOp.ADD, op: PatchOp.ADD,
path: `/serverInfo/host/hostnameInfo/80/0`, path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
value: { value: {
kind: 'ip', gateway: { id: 'eth0', name: 'Ethernet', public: false },
gatewayId: 'eth0',
public: false, public: false,
hostname: { hostname: {
kind: 'domain', kind: 'domain',
@@ -1466,7 +1456,7 @@ export class MockApiService extends ApiService {
}, },
{ {
op: PatchOp.REMOVE, op: PatchOp.REMOVE,
path: `/serverInfo/host/hostnameInfo/80/0`, path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
}, },
] ]
this.mockRevision(patch) this.mockRevision(patch)
@@ -1474,20 +1464,12 @@ export class MockApiService extends ApiService {
return null return null
} }
async pkgBindingToggleGateway( async pkgBindingSetAddressEnabled(
params: RR.PkgBindingToggleGatewayReq, params: RR.PkgBindingSetAddressEnabledReq,
): Promise<RR.PkgBindingToggleGatewayRes> { ): Promise<RR.PkgBindingSetAddressEnabledRes> {
await pauseFor(2000) await pauseFor(2000)
const patch = [ // Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
{
op: PatchOp.REPLACE,
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/privateDisabled`,
value: params.enabled ? [] : [params.gateway],
},
]
this.mockRevision(patch)
return null return null
} }
@@ -1506,10 +1488,9 @@ export class MockApiService extends ApiService {
}, },
{ {
op: PatchOp.ADD, op: PatchOp.ADD,
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`,
value: { value: {
kind: 'ip', gateway: { id: 'eth0', name: 'Ethernet', public: false },
gatewayId: 'eth0',
public: true, public: true,
hostname: { hostname: {
kind: 'domain', kind: 'domain',
@@ -1538,7 +1519,7 @@ export class MockApiService extends ApiService {
}, },
{ {
op: PatchOp.REMOVE, op: PatchOp.REMOVE,
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`,
}, },
] ]
this.mockRevision(patch) this.mockRevision(patch)
@@ -1559,10 +1540,9 @@ export class MockApiService extends ApiService {
}, },
{ {
op: PatchOp.ADD, op: PatchOp.ADD,
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`,
value: { value: {
kind: 'ip', gateway: { id: 'eth0', name: 'Ethernet', public: false },
gatewayId: 'eth0',
public: false, public: false,
hostname: { hostname: {
kind: 'domain', kind: 'domain',
@@ -1592,7 +1572,7 @@ export class MockApiService extends ApiService {
}, },
{ {
op: PatchOp.REMOVE, op: PatchOp.REMOVE,
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`,
}, },
] ]
this.mockRevision(patch) this.mockRevision(patch)

View File

@@ -38,8 +38,74 @@ export const mockPatchData: DataModel = {
net: { net: {
assignedPort: null, assignedPort: null,
assignedSslPort: 443, assignedSslPort: 443,
publicEnabled: [], },
addresses: {
privateDisabled: [], privateDisabled: [],
publicEnabled: [],
possible: [
{
gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false,
hostname: {
kind: 'local',
value: 'adjective-noun.local',
port: null,
sslPort: 443,
},
},
{
gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false,
hostname: {
kind: 'local',
value: 'adjective-noun.local',
port: null,
sslPort: 443,
},
},
{
gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false,
hostname: {
kind: 'ipv4',
value: '10.0.0.1',
port: null,
sslPort: 443,
},
},
{
gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false,
hostname: {
kind: 'ipv4',
value: '10.0.0.2',
port: null,
sslPort: 443,
},
},
{
gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false,
hostname: {
kind: 'ipv6',
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
scopeId: 2,
port: null,
sslPort: 443,
},
},
{
gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false,
hostname: {
kind: 'ipv6',
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
scopeId: 3,
port: null,
sslPort: 443,
},
},
],
}, },
options: { options: {
preferredExternalPort: 80, preferredExternalPort: 80,
@@ -54,78 +120,6 @@ export const mockPatchData: DataModel = {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
hostnameInfo: {
80: [
{
kind: 'ip',
gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false,
hostname: {
kind: 'local',
value: 'adjective-noun.local',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false,
hostname: {
kind: 'local',
value: 'adjective-noun.local',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false,
hostname: {
kind: 'ipv4',
value: '10.0.0.1',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false,
hostname: {
kind: 'ipv4',
value: '10.0.0.2',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false,
hostname: {
kind: 'ipv6',
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
scopeId: 2,
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false,
hostname: {
kind: 'ipv6',
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
scopeId: 3,
port: null,
sslPort: 443,
},
},
],
},
}, },
gateways: { gateways: {
eth0: { eth0: {
@@ -503,22 +497,12 @@ export const mockPatchData: DataModel = {
net: { net: {
assignedPort: 80, assignedPort: 80,
assignedSslPort: 443, assignedSslPort: 443,
publicEnabled: [], },
addresses: {
privateDisabled: [], privateDisabled: [],
}, publicEnabled: [],
options: { possible: [
addSsl: null,
preferredExternalPort: 443,
secure: { ssl: true },
},
},
},
publicDomains: {},
privateDomains: [],
hostnameInfo: {
80: [
{ {
kind: 'ip',
gateway: { id: 'eth0', name: 'Ethernet', public: false }, gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -529,7 +513,6 @@ export const mockPatchData: DataModel = {
}, },
}, },
{ {
kind: 'ip',
gateway: { id: 'wlan0', name: 'Wireless', public: false }, gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -540,7 +523,6 @@ export const mockPatchData: DataModel = {
}, },
}, },
{ {
kind: 'ip',
gateway: { id: 'eth0', name: 'Ethernet', public: false }, gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -551,7 +533,6 @@ export const mockPatchData: DataModel = {
}, },
}, },
{ {
kind: 'ip',
gateway: { id: 'wlan0', name: 'Wireless', public: false }, gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -562,7 +543,6 @@ export const mockPatchData: DataModel = {
}, },
}, },
{ {
kind: 'ip',
gateway: { id: 'eth0', name: 'Ethernet', public: false }, gateway: { id: 'eth0', name: 'Ethernet', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -574,7 +554,6 @@ export const mockPatchData: DataModel = {
}, },
}, },
{ {
kind: 'ip',
gateway: { id: 'wlan0', name: 'Wireless', public: false }, gateway: { id: 'wlan0', name: 'Wireless', public: false },
public: false, public: false,
hostname: { hostname: {
@@ -587,6 +566,15 @@ export const mockPatchData: DataModel = {
}, },
], ],
}, },
options: {
addSsl: null,
preferredExternalPort: 443,
secure: { ssl: true },
},
},
},
publicDomains: {},
privateDomains: [],
}, },
bcdefgh: { bcdefgh: {
bindings: { bindings: {
@@ -595,8 +583,11 @@ export const mockPatchData: DataModel = {
net: { net: {
assignedPort: 8332, assignedPort: 8332,
assignedSslPort: null, assignedSslPort: null,
publicEnabled: [], },
addresses: {
privateDisabled: [], privateDisabled: [],
publicEnabled: [],
possible: [],
}, },
options: { options: {
addSsl: null, addSsl: null,
@@ -607,9 +598,6 @@ export const mockPatchData: DataModel = {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
hostnameInfo: {
8332: [],
},
}, },
cdefghi: { cdefghi: {
bindings: { bindings: {
@@ -618,8 +606,11 @@ export const mockPatchData: DataModel = {
net: { net: {
assignedPort: 8333, assignedPort: 8333,
assignedSslPort: null, assignedSslPort: null,
publicEnabled: [], },
addresses: {
privateDisabled: [], privateDisabled: [],
publicEnabled: [],
possible: [],
}, },
options: { options: {
addSsl: null, addSsl: null,
@@ -630,9 +621,6 @@ export const mockPatchData: DataModel = {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
hostnameInfo: {
8333: [],
},
}, },
}, },
storeExposedDependents: [], storeExposedDependents: [],