mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
18
CLAUDE.md
18
CLAUDE.md
@@ -29,6 +29,24 @@ make update-startbox REMOTE=start9@<ip> # Fastest iteration (binary + UI)
|
||||
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
|
||||
|
||||
### Core (`/core`)
|
||||
|
||||
123
agents/TODO.md
123
agents/TODO.md
@@ -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
|
||||
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
|
||||
`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.
|
||||
#### 2. Per-Address Enable/Disable ✅ DONE
|
||||
|
||||
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`.
|
||||
|
||||
```
|
||||
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`):
|
||||
**`DerivedAddressInfo` struct** (on `BindInfo`):
|
||||
|
||||
```rust
|
||||
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 by update(): all possible addresses for this binding
|
||||
pub possible: BTreeSet<HostnameInfo>,
|
||||
pub possible: BTreeSet<HostnameInfo>, // COMPUTED by update()
|
||||
}
|
||||
```
|
||||
|
||||
`DerivedAddressInfo::enabled()` returns `possible` filtered by the two sets: private addresses are
|
||||
enabled by default (disabled if in `private_disabled`), public addresses are disabled by default
|
||||
(enabled if in `public_enabled`). Requires `HostnameInfo` to derive `Ord` for `BTreeSet` usage.
|
||||
`DerivedAddressInfo::enabled()` returns `possible` filtered by the two sets. `HostnameInfo` derives
|
||||
`Ord` for `BTreeSet` usage. `AddressFilter` (implementing `InterfaceFilter`) derives enabled
|
||||
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
|
||||
different network paths:
|
||||
**How disabling works per address type** (enforcement deferred to Section 3):
|
||||
|
||||
- **WAN IP:port** (public gateway IP addresses): Disabled via **source-IP gating** in the vhost
|
||||
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.
|
||||
- **WAN/LAN IP:port**: Will be enforced via **source-IP gating** in the vhost layer (Section 3).
|
||||
- **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),
|
||||
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`
|
||||
entry** for that hostname.
|
||||
|
||||
#### 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
|
||||
|
||||
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),
|
||||
gateway name, SSL indicator, enable/disable state, port forward info for public addresses, and a test button
|
||||
for reachability (see Section 7).
|
||||
@@ -282,13 +207,13 @@ Pending tasks for AI agents. Remove items when completed.
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation |
|
||||
| `core/src/net/host/binding.rs` | `BindInfo`/`NetInfo`/`DerivedAddressInfo` — remove gateway overrides, add per-address enable/disable sets, new RPC endpoints |
|
||||
| `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — compute `enabled` on `HostnameInfo`, vhost/forward/DNS reconciliation, 5443 hack removal |
|
||||
| `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation, `try_alloc()` for preferred ports |
|
||||
| `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()` — 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/gateway.rs` | `InterfaceFilter` — remove `NetInfo` impl, simplify |
|
||||
| `core/src/net/service_interface.rs` | `HostnameInfo` — add `Ord` derives for use in `BTreeSet` |
|
||||
| `core/src/net/host/address.rs` | Existing domain/onion CRUD endpoints (no changes needed) |
|
||||
| `core/src/net/gateway.rs` | `InterfaceFilter` trait and filter types (`AddressFilter`, `PublicFilter`, etc.) |
|
||||
| `core/src/net/service_interface.rs` | `HostnameInfo` — derives `Ord` for `BTreeSet` usage |
|
||||
| `core/src/net/host/address.rs` | `HostAddress` (flattened struct), domain CRUD endpoints |
|
||||
| `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed |
|
||||
| `core/src/db/model/public.rs` | Public DB model — port forward mapping |
|
||||
|
||||
|
||||
@@ -82,17 +82,14 @@ export class DockerProcedureContainer extends Drop {
|
||||
}),
|
||||
)
|
||||
} else if (volumeMount.type === "certificate") {
|
||||
const hostInfo = await effects.getHostInfo({
|
||||
hostId: volumeMount["interface-id"],
|
||||
})
|
||||
const hostnames = [
|
||||
`${packageId}.embassy`,
|
||||
...new Set(
|
||||
Object.values(
|
||||
(
|
||||
await effects.getHostInfo({
|
||||
hostId: volumeMount["interface-id"],
|
||||
})
|
||||
)?.hostnameInfo || {},
|
||||
)
|
||||
.flatMap((h) => h)
|
||||
Object.values(hostInfo?.bindings || {})
|
||||
.flatMap((b) => b.addresses.possible)
|
||||
.map((h) => h.hostname.value),
|
||||
).values(),
|
||||
]
|
||||
|
||||
@@ -1244,8 +1244,8 @@ async function updateConfig(
|
||||
? ""
|
||||
: catchFn(
|
||||
() =>
|
||||
filled.addressInfo!.filter({ kind: "mdns" })!
|
||||
.hostnames[0].hostname.value,
|
||||
filled.addressInfo!.filter({ kind: "mdns" })!.hostnames[0]
|
||||
.hostname.value,
|
||||
) || ""
|
||||
mutConfigValue[key] = url
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::db::model::Database;
|
||||
use crate::db::model::package::AllPackageData;
|
||||
use crate::net::acme::AcmeProvider;
|
||||
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::vhost::AlpnInfo;
|
||||
use crate::prelude::*;
|
||||
@@ -63,7 +63,8 @@ impl Public {
|
||||
post_init_migration_todos: BTreeMap::new(),
|
||||
network: NetworkInfo {
|
||||
host: Host {
|
||||
bindings: [(
|
||||
bindings: Bindings(
|
||||
[(
|
||||
80,
|
||||
BindInfo {
|
||||
enabled: false,
|
||||
@@ -82,16 +83,15 @@ impl Public {
|
||||
net: NetInfo {
|
||||
assigned_port: None,
|
||||
assigned_ssl_port: Some(443),
|
||||
private_disabled: OrdSet::new(),
|
||||
public_enabled: OrdSet::new(),
|
||||
},
|
||||
addresses: DerivedAddressInfo::default(),
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
public_domains: BTreeMap::new(),
|
||||
private_domains: BTreeSet::new(),
|
||||
hostname_info: BTreeMap::new(),
|
||||
},
|
||||
wifi: WifiInfo {
|
||||
enabled: true,
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use id_pool::IdPool;
|
||||
use iddqd::{IdOrdItem, IdOrdMap};
|
||||
use rand::Rng;
|
||||
use imbl::OrdMap;
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -23,25 +23,49 @@ use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
use crate::util::sync::Watch;
|
||||
|
||||
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)]
|
||||
pub struct AvailablePorts(IdPool);
|
||||
pub struct AvailablePorts(BTreeMap<u16, bool>);
|
||||
impl AvailablePorts {
|
||||
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> {
|
||||
self.0.request_id().ok_or_else(|| {
|
||||
Error::new(
|
||||
pub fn alloc(&mut self, ssl: bool) -> Result<u16, Error> {
|
||||
let mut rng = rand::rng();
|
||||
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")),
|
||||
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>) {
|
||||
for port in ports {
|
||||
self.0.return_id(port).unwrap_or_default();
|
||||
self.0.remove(&port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1685,19 +1685,9 @@ where
|
||||
|
||||
#[test]
|
||||
fn test_filter() {
|
||||
use crate::net::host::binding::NetInfo;
|
||||
let wg1 = "wg1".parse::<GatewayId>().unwrap();
|
||||
assert!(!InterfaceFilter::filter(
|
||||
&AndFilter(
|
||||
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(),
|
||||
&AndFilter(IdFilter(wg1.clone()), PublicFilter { public: false }).into_dyn(),
|
||||
&wg1,
|
||||
&NetworkInterfaceInfo {
|
||||
name: None,
|
||||
|
||||
@@ -16,15 +16,11 @@ use crate::prelude::*;
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(rename_all_fields = "camelCase")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum HostAddress {
|
||||
Domain {
|
||||
address: InternedString,
|
||||
public: Option<PublicDomainConfig>,
|
||||
private: bool,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HostAddress {
|
||||
pub address: InternedString,
|
||||
pub public: Option<PublicDomainConfig>,
|
||||
pub private: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||
@@ -151,29 +147,18 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]);
|
||||
for address in &res {
|
||||
match address {
|
||||
HostAddress::Domain {
|
||||
address,
|
||||
public: Some(PublicDomainConfig { gateway, acme }),
|
||||
private,
|
||||
} => {
|
||||
for entry in &res {
|
||||
if let Some(PublicDomainConfig { gateway, acme }) = &entry.public {
|
||||
table.add_row(row![
|
||||
address,
|
||||
entry.address,
|
||||
&format!(
|
||||
"{} ({gateway})",
|
||||
if *private { "YES" } else { "ONLY" }
|
||||
if entry.private { "YES" } else { "ONLY" }
|
||||
),
|
||||
acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE")
|
||||
]);
|
||||
}
|
||||
HostAddress::Domain {
|
||||
address,
|
||||
public: None,
|
||||
..
|
||||
} => {
|
||||
table.add_row(row![address, &format!("NO"), "N/A"]);
|
||||
}
|
||||
} else {
|
||||
table.add_row(row![entry.address, &format!("NO"), "N/A"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,17 @@ use std::str::FromStr;
|
||||
|
||||
use clap::Parser;
|
||||
use clap::builder::ValueParserFactory;
|
||||
use imbl::OrdSet;
|
||||
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::public::NetworkInterfaceInfo;
|
||||
use crate::db::prelude::Map;
|
||||
use crate::net::forward::AvailablePorts;
|
||||
use crate::net::gateway::InterfaceFilter;
|
||||
use crate::net::host::HostApiKind;
|
||||
use crate::net::service_interface::HostnameInfo;
|
||||
use crate::net::vhost::AlpnInfo;
|
||||
use crate::prelude::*;
|
||||
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")]
|
||||
#[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 enabled: bool,
|
||||
pub options: BindOptions,
|
||||
pub net: NetInfo,
|
||||
pub addresses: DerivedAddressInfo,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
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_ssl_port: Option<u16>,
|
||||
}
|
||||
impl InterfaceFilter for NetInfo {
|
||||
fn filter(&self, _id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
|
||||
info.ip_info.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl BindInfo {
|
||||
pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result<Self, Error> {
|
||||
let mut assigned_port = None;
|
||||
let mut assigned_ssl_port = None;
|
||||
if options.add_ssl.is_some() {
|
||||
assigned_ssl_port = Some(available_ports.alloc()?);
|
||||
if let Some(ssl) = &options.add_ssl {
|
||||
assigned_ssl_port = available_ports
|
||||
.try_alloc(ssl.preferred_external_port, true)
|
||||
.or_else(|| Some(available_ports.alloc(true).ok()?));
|
||||
}
|
||||
if options
|
||||
.secure
|
||||
.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 {
|
||||
enabled: true,
|
||||
options,
|
||||
net: NetInfo {
|
||||
private_disabled: OrdSet::new(),
|
||||
public_enabled: OrdSet::new(),
|
||||
assigned_port,
|
||||
assigned_ssl_port,
|
||||
},
|
||||
addresses: DerivedAddressInfo::default(),
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
@@ -97,7 +184,11 @@ impl BindInfo {
|
||||
available_ports: &mut AvailablePorts,
|
||||
options: BindOptions,
|
||||
) -> Result<Self, Error> {
|
||||
let Self { net: mut lan, .. } = self;
|
||||
let Self {
|
||||
net: mut lan,
|
||||
addresses,
|
||||
..
|
||||
} = self;
|
||||
if options
|
||||
.secure
|
||||
.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() {
|
||||
Some(port)
|
||||
} else if let Some(port) =
|
||||
available_ports.try_alloc(options.preferred_external_port, false)
|
||||
{
|
||||
Some(port)
|
||||
} else {
|
||||
Some(available_ports.alloc()?)
|
||||
Some(available_ports.alloc(false)?)
|
||||
};
|
||||
} else {
|
||||
if let Some(port) = lan.assigned_port.take() {
|
||||
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() {
|
||||
Some(port)
|
||||
} else if let Some(port) = available_ports.try_alloc(ssl.preferred_external_port, true)
|
||||
{
|
||||
Some(port)
|
||||
} else {
|
||||
Some(available_ports.alloc()?)
|
||||
Some(available_ports.alloc(true)?)
|
||||
};
|
||||
} else {
|
||||
if let Some(port) = lan.assigned_ssl_port.take() {
|
||||
@@ -128,22 +226,17 @@ impl BindInfo {
|
||||
enabled: true,
|
||||
options,
|
||||
net: lan,
|
||||
addresses: DerivedAddressInfo {
|
||||
private_disabled: addresses.private_disabled,
|
||||
public_enabled: addresses.public_enabled,
|
||||
possible: BTreeSet::new(),
|
||||
},
|
||||
})
|
||||
}
|
||||
pub fn disable(&mut self) {
|
||||
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)]
|
||||
#[ts(export)]
|
||||
@@ -188,7 +281,7 @@ pub fn binding<C: Context, Kind: HostApiKind>()
|
||||
|
||||
let mut table = Table::new();
|
||||
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![
|
||||
internal,
|
||||
info.enabled,
|
||||
@@ -213,12 +306,12 @@ pub fn binding<C: Context, Kind: HostApiKind>()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"set-gateway-enabled",
|
||||
from_fn_async(set_gateway_enabled::<Kind>)
|
||||
"set-address-enabled",
|
||||
from_fn_async(set_address_enabled::<Kind>)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_inherited(Kind::inheritance)
|
||||
.no_display()
|
||||
.with_about("about.set-gateway-enabled-for-binding")
|
||||
.with_about("about.set-address-enabled-for-binding")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
@@ -227,7 +320,7 @@ pub async fn list_bindings<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<BTreeMap<u16, BindInfo>, Error> {
|
||||
) -> Result<Bindings, Error> {
|
||||
Kind::host_for(&inheritance, &mut ctx.db.peek().await)?
|
||||
.as_bindings()
|
||||
.de()
|
||||
@@ -236,50 +329,44 @@ pub async fn list_bindings<Kind: HostApiKind>(
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct BindingGatewaySetEnabledParams {
|
||||
pub struct BindingSetAddressEnabledParams {
|
||||
#[arg(help = "help.arg.internal-port")]
|
||||
internal_port: u16,
|
||||
#[arg(help = "help.arg.gateway-id")]
|
||||
gateway: GatewayId,
|
||||
#[arg(long, help = "help.arg.address")]
|
||||
address: String,
|
||||
#[arg(long, help = "help.arg.binding-enabled")]
|
||||
enabled: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn set_gateway_enabled<Kind: HostApiKind>(
|
||||
pub async fn set_address_enabled<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
BindingGatewaySetEnabledParams {
|
||||
BindingSetAddressEnabledParams {
|
||||
internal_port,
|
||||
gateway,
|
||||
address,
|
||||
enabled,
|
||||
}: BindingGatewaySetEnabledParams,
|
||||
}: BindingSetAddressEnabledParams,
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<(), Error> {
|
||||
let enabled = enabled.unwrap_or(true);
|
||||
let gateway_public = ctx
|
||||
.net_controller
|
||||
.net_iface
|
||||
.watcher
|
||||
.ip_info()
|
||||
.get(&gateway)
|
||||
.or_not_found(&gateway)?
|
||||
.public();
|
||||
let address: HostnameInfo =
|
||||
serde_json::from_str(&address).with_kind(ErrorKind::Deserialization)?;
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_bindings_mut()
|
||||
.mutate(|b| {
|
||||
let net = &mut b.get_mut(&internal_port).or_not_found(internal_port)?.net;
|
||||
if gateway_public {
|
||||
let bind = b.get_mut(&internal_port).or_not_found(internal_port)?;
|
||||
if address.public {
|
||||
if enabled {
|
||||
net.public_enabled.insert(gateway);
|
||||
bind.addresses.public_enabled.insert(address.clone());
|
||||
} else {
|
||||
net.public_enabled.remove(&gateway);
|
||||
bind.addresses.public_enabled.remove(&address);
|
||||
}
|
||||
} else {
|
||||
if enabled {
|
||||
net.private_disabled.remove(&gateway);
|
||||
bind.addresses.private_disabled.remove(&address);
|
||||
} else {
|
||||
net.private_disabled.insert(gateway);
|
||||
bind.addresses.private_disabled.insert(address.clone());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -13,8 +13,7 @@ use crate::context::RpcContext;
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::net::forward::AvailablePorts;
|
||||
use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api};
|
||||
use crate::net::host::binding::{BindInfo, BindOptions, binding};
|
||||
use crate::net::service_interface::HostnameInfo;
|
||||
use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding};
|
||||
use crate::prelude::*;
|
||||
use crate::{HostId, PackageId};
|
||||
|
||||
@@ -26,11 +25,9 @@ pub mod binding;
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct Host {
|
||||
pub bindings: BTreeMap<u16, BindInfo>,
|
||||
pub bindings: Bindings,
|
||||
pub public_domains: BTreeMap<InternedString, PublicDomainConfig>,
|
||||
pub private_domains: BTreeSet<InternedString>,
|
||||
/// COMPUTED: NetService::update
|
||||
pub hostname_info: BTreeMap<u16, Vec<HostnameInfo>>, // internal port -> Hostnames
|
||||
}
|
||||
|
||||
impl AsRef<Host> for Host {
|
||||
@@ -45,7 +42,7 @@ impl Host {
|
||||
pub fn addresses<'a>(&'a self) -> impl Iterator<Item = HostAddress> + 'a {
|
||||
self.public_domains
|
||||
.iter()
|
||||
.map(|(address, config)| HostAddress::Domain {
|
||||
.map(|(address, config)| HostAddress {
|
||||
address: address.clone(),
|
||||
public: Some(config.clone()),
|
||||
private: self.private_domains.contains(address),
|
||||
@@ -54,7 +51,7 @@ impl Host {
|
||||
self.private_domains
|
||||
.iter()
|
||||
.filter(|a| !self.public_domains.contains_key(*a))
|
||||
.map(|address| HostAddress::Domain {
|
||||
.map(|address| HostAddress {
|
||||
address: address.clone(),
|
||||
public: None,
|
||||
private: true,
|
||||
|
||||
@@ -18,8 +18,8 @@ use crate::hostname::Hostname;
|
||||
use crate::net::dns::DnsController;
|
||||
use crate::net::forward::{InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule};
|
||||
use crate::net::gateway::{
|
||||
AndFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, NetworkInterfaceController, OrFilter,
|
||||
PublicFilter, SecureFilter,
|
||||
AndFilter, AnyFilter, DynInterfaceFilter, IdFilter, InterfaceFilter,
|
||||
NetworkInterfaceController, OrFilter, PublicFilter, SecureFilter,
|
||||
};
|
||||
use crate::net::host::address::HostAddress;
|
||||
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
|
||||
@@ -202,7 +202,7 @@ impl NetServiceData {
|
||||
.as_entries_mut()?
|
||||
{
|
||||
host.as_bindings_mut().mutate(|b| {
|
||||
for (internal_port, info) in b {
|
||||
for (internal_port, info) in b.iter_mut() {
|
||||
if !except.contains(&BindId {
|
||||
id: host_id.clone(),
|
||||
internal_port: *internal_port,
|
||||
@@ -233,7 +233,7 @@ impl NetServiceData {
|
||||
.as_network_mut()
|
||||
.as_host_mut();
|
||||
host.as_bindings_mut().mutate(|b| {
|
||||
for (internal_port, info) in b {
|
||||
for (internal_port, info) in b.iter_mut() {
|
||||
if !except.contains(&BindId {
|
||||
id: HostId::default(),
|
||||
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 vhosts: BTreeMap<(Option<InternedString>, u16), ProxyTarget> = BTreeMap::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 peek = ctrl.db.peek().await;
|
||||
@@ -264,12 +268,30 @@ impl NetServiceData {
|
||||
let server_info = peek.as_public().as_server_info();
|
||||
let net_ifaces = ctrl.net_iface.watcher.ip_info();
|
||||
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 {
|
||||
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 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 {
|
||||
let external = bind
|
||||
.net
|
||||
@@ -289,23 +311,22 @@ impl NetServiceData {
|
||||
vhosts.insert(
|
||||
(hostname, external),
|
||||
ProxyTarget {
|
||||
filter: bind.net.clone().into_dyn(),
|
||||
filter: gw_filter.clone().into_dyn(),
|
||||
acme: None,
|
||||
addr,
|
||||
add_x_forwarded_headers: ssl.add_x_forwarded_headers,
|
||||
connect_ssl: connect_ssl
|
||||
.clone()
|
||||
.map(|_| ctrl.tls_client_config.clone()),
|
||||
},
|
||||
}, // TODO: allow public traffic?
|
||||
);
|
||||
}
|
||||
for address in host.addresses() {
|
||||
match address {
|
||||
HostAddress::Domain {
|
||||
for HostAddress {
|
||||
address,
|
||||
public,
|
||||
private,
|
||||
} => {
|
||||
} in host_addresses.iter().cloned()
|
||||
{
|
||||
if hostnames.insert(address.clone()) {
|
||||
let address = Some(address.clone());
|
||||
if ssl.preferred_external_port == 443 {
|
||||
@@ -323,8 +344,7 @@ impl NetServiceData {
|
||||
.into_dyn(),
|
||||
acme: public.acme.clone(),
|
||||
addr,
|
||||
add_x_forwarded_headers: ssl
|
||||
.add_x_forwarded_headers,
|
||||
add_x_forwarded_headers: ssl.add_x_forwarded_headers,
|
||||
connect_ssl: connect_ssl
|
||||
.clone()
|
||||
.map(|_| ctrl.tls_client_config.clone()),
|
||||
@@ -352,8 +372,7 @@ impl NetServiceData {
|
||||
.into_dyn(),
|
||||
acme: public.acme.clone(),
|
||||
addr,
|
||||
add_x_forwarded_headers: ssl
|
||||
.add_x_forwarded_headers,
|
||||
add_x_forwarded_headers: ssl.add_x_forwarded_headers,
|
||||
connect_ssl: connect_ssl
|
||||
.clone()
|
||||
.map(|_| ctrl.tls_client_config.clone()),
|
||||
@@ -370,8 +389,7 @@ impl NetServiceData {
|
||||
.into_dyn(),
|
||||
acme: None,
|
||||
addr,
|
||||
add_x_forwarded_headers: ssl
|
||||
.add_x_forwarded_headers,
|
||||
add_x_forwarded_headers: ssl.add_x_forwarded_headers,
|
||||
connect_ssl: connect_ssl
|
||||
.clone()
|
||||
.map(|_| ctrl.tls_client_config.clone()),
|
||||
@@ -392,15 +410,13 @@ impl NetServiceData {
|
||||
)
|
||||
.into_dyn()
|
||||
} else {
|
||||
IdFilter(public.gateway.clone())
|
||||
.into_dyn()
|
||||
IdFilter(public.gateway.clone()).into_dyn()
|
||||
},
|
||||
)
|
||||
.into_dyn(),
|
||||
acme: public.acme.clone(),
|
||||
addr,
|
||||
add_x_forwarded_headers: ssl
|
||||
.add_x_forwarded_headers,
|
||||
add_x_forwarded_headers: ssl.add_x_forwarded_headers,
|
||||
connect_ssl: connect_ssl
|
||||
.clone()
|
||||
.map(|_| ctrl.tls_client_config.clone()),
|
||||
@@ -417,8 +433,7 @@ impl NetServiceData {
|
||||
.into_dyn(),
|
||||
acme: None,
|
||||
addr,
|
||||
add_x_forwarded_headers: ssl
|
||||
.add_x_forwarded_headers,
|
||||
add_x_forwarded_headers: ssl.add_x_forwarded_headers,
|
||||
connect_ssl: connect_ssl
|
||||
.clone()
|
||||
.map(|_| ctrl.tls_client_config.clone()),
|
||||
@@ -429,8 +444,6 @@ impl NetServiceData {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if bind
|
||||
.options
|
||||
.secure
|
||||
@@ -451,8 +464,7 @@ impl NetServiceData {
|
||||
),
|
||||
);
|
||||
}
|
||||
let mut bind_hostname_info: Vec<HostnameInfo> =
|
||||
hostname_info.remove(port).unwrap_or_default();
|
||||
bind.addresses.possible.clear();
|
||||
for (gateway_id, info) in net_ifaces
|
||||
.iter()
|
||||
.filter(|(_, info)| {
|
||||
@@ -481,7 +493,7 @@ impl NetServiceData {
|
||||
i.device_type != Some(NetworkInterfaceType::Wireguard)
|
||||
})
|
||||
{
|
||||
bind_hostname_info.push(HostnameInfo {
|
||||
bind.addresses.possible.insert(HostnameInfo {
|
||||
gateway: gateway.clone(),
|
||||
public: false,
|
||||
hostname: IpHostname::Local {
|
||||
@@ -494,19 +506,17 @@ impl NetServiceData {
|
||||
},
|
||||
});
|
||||
}
|
||||
for address in host.addresses() {
|
||||
if let HostAddress::Domain {
|
||||
for HostAddress {
|
||||
address,
|
||||
public,
|
||||
private,
|
||||
} = address
|
||||
} in host_addresses.iter().cloned()
|
||||
{
|
||||
if public.is_none() {
|
||||
private_dns.insert(address.clone());
|
||||
}
|
||||
let private = private && !info.public();
|
||||
let public =
|
||||
public.as_ref().map_or(false, |p| &p.gateway == gateway_id);
|
||||
let public = public.as_ref().map_or(false, |p| &p.gateway == gateway_id);
|
||||
if public || private {
|
||||
if bind
|
||||
.options
|
||||
@@ -514,7 +524,7 @@ impl NetServiceData {
|
||||
.as_ref()
|
||||
.map_or(false, |ssl| ssl.preferred_external_port == 443)
|
||||
{
|
||||
bind_hostname_info.push(HostnameInfo {
|
||||
bind.addresses.possible.insert(HostnameInfo {
|
||||
gateway: gateway.clone(),
|
||||
public,
|
||||
hostname: IpHostname::Domain {
|
||||
@@ -524,7 +534,7 @@ impl NetServiceData {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
bind_hostname_info.push(HostnameInfo {
|
||||
bind.addresses.possible.insert(HostnameInfo {
|
||||
gateway: gateway.clone(),
|
||||
public,
|
||||
hostname: IpHostname::Domain {
|
||||
@@ -536,11 +546,10 @@ impl NetServiceData {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ip_info) = &info.ip_info {
|
||||
let public = info.public();
|
||||
if let Some(wan_ip) = ip_info.wan_ip {
|
||||
bind_hostname_info.push(HostnameInfo {
|
||||
bind.addresses.possible.insert(HostnameInfo {
|
||||
gateway: gateway.clone(),
|
||||
public: true,
|
||||
hostname: IpHostname::Ipv4 {
|
||||
@@ -554,7 +563,7 @@ impl NetServiceData {
|
||||
match ipnet {
|
||||
IpNet::V4(net) => {
|
||||
if !public {
|
||||
bind_hostname_info.push(HostnameInfo {
|
||||
bind.addresses.possible.insert(HostnameInfo {
|
||||
gateway: gateway.clone(),
|
||||
public,
|
||||
hostname: IpHostname::Ipv4 {
|
||||
@@ -566,7 +575,7 @@ impl NetServiceData {
|
||||
}
|
||||
}
|
||||
IpNet::V6(net) => {
|
||||
bind_hostname_info.push(HostnameInfo {
|
||||
bind.addresses.possible.insert(HostnameInfo {
|
||||
gateway: gateway.clone(),
|
||||
public: public && !ipv6_is_local(net.addr()),
|
||||
hostname: IpHostname::Ipv6 {
|
||||
@@ -581,8 +590,6 @@ impl NetServiceData {
|
||||
}
|
||||
}
|
||||
}
|
||||
hostname_info.insert(*port, bind_hostname_info);
|
||||
}
|
||||
}
|
||||
|
||||
let all = binds
|
||||
@@ -673,11 +680,28 @@ impl NetServiceData {
|
||||
}
|
||||
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 res.revision.is_some() {
|
||||
if let Some(cbs) = ctrl.callbacks.get_host_info(&(pkg_id.clone(), id)) {
|
||||
cbs.call(vector![]).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use ts_rs::TS;
|
||||
|
||||
use crate::{GatewayId, HostId, ServiceInterfaceId};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GatewayInfo {
|
||||
@@ -29,7 +29,7 @@ pub struct GatewayInfo {
|
||||
pub public: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename_all_fields = "camelCase")]
|
||||
|
||||
@@ -175,8 +175,12 @@ pub async fn remove_tunnel(
|
||||
let host = host?;
|
||||
host.as_bindings_mut().mutate(|b| {
|
||||
Ok(b.values_mut().for_each(|v| {
|
||||
v.net.private_disabled.remove(&id);
|
||||
v.net.public_enabled.remove(&id);
|
||||
v.addresses
|
||||
.private_disabled
|
||||
.retain(|h| h.gateway.id != id);
|
||||
v.addresses
|
||||
.public_enabled
|
||||
.retain(|h| h.gateway.id != id);
|
||||
}))
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -60,10 +60,10 @@ pub async fn get_ssl_certificate(
|
||||
.into_iter()
|
||||
.chain(m.as_private_domains().de()?)
|
||||
.chain(
|
||||
m.as_hostname_info()
|
||||
m.as_bindings()
|
||||
.de()?
|
||||
.values()
|
||||
.flatten()
|
||||
.flat_map(|b| b.addresses.possible.iter().cloned())
|
||||
.map(|h| h.to_san_hostname()),
|
||||
)
|
||||
.collect::<Vec<InternedString>>())
|
||||
@@ -184,10 +184,10 @@ pub async fn get_ssl_key(
|
||||
.into_iter()
|
||||
.chain(m.as_private_domains().de()?)
|
||||
.chain(
|
||||
m.as_hostname_info()
|
||||
m.as_bindings()
|
||||
.de()?
|
||||
.values()
|
||||
.flatten()
|
||||
.flat_map(|b| b.addresses.possible.iter().cloned())
|
||||
.map(|h| h.to_san_hostname()),
|
||||
)
|
||||
.collect::<Vec<InternedString>>())
|
||||
|
||||
@@ -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
|
||||
if let Some(key_store) = db
|
||||
.get_mut("private")
|
||||
@@ -89,6 +66,29 @@ impl VersionT for Version {
|
||||
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)
|
||||
}
|
||||
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||
@@ -96,20 +96,35 @@ impl VersionT for Version {
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_hostname_info(host: Option<&mut Value>) {
|
||||
if let Some(hostname_info) = host
|
||||
.and_then(|h| h.get_mut("hostnameInfo"))
|
||||
.and_then(|h| h.as_object_mut())
|
||||
{
|
||||
for (_, infos) in hostname_info.iter_mut() {
|
||||
if let Some(arr) = infos.as_array_mut() {
|
||||
// Remove onion entries
|
||||
arr.retain(|info| info.get("kind").and_then(|k| k.as_str()) != Some("onion"));
|
||||
// Strip "kind" field from remaining entries (HostnameInfo flattened from enum to struct)
|
||||
for info in arr.iter_mut() {
|
||||
if let Some(obj) = info.as_object_mut() {
|
||||
obj.remove("kind");
|
||||
fn migrate_host(host: Option<&mut Value>) {
|
||||
let Some(host) = host.and_then(|h| h.as_object_mut()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Remove hostnameInfo from host
|
||||
host.remove("hostnameInfo");
|
||||
|
||||
// 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()) {
|
||||
for (_, binding) in bindings.iter_mut() {
|
||||
if let Some(binding_obj) = binding.as_object_mut() {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
// 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 { DerivedAddressInfo } from './DerivedAddressInfo'
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// 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
|
||||
gateway: GatewayId
|
||||
address: string
|
||||
enabled: boolean | null
|
||||
}
|
||||
4
sdk/base/lib/osBindings/Bindings.ts
Normal file
4
sdk/base/lib/osBindings/Bindings.ts
Normal 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 }
|
||||
17
sdk/base/lib/osBindings/DerivedAddressInfo.ts
Normal file
17
sdk/base/lib/osBindings/DerivedAddressInfo.ts
Normal 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>
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
// 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 { HostnameInfo } from './HostnameInfo'
|
||||
import type { Bindings } from './Bindings'
|
||||
import type { PublicDomainConfig } from './PublicDomainConfig'
|
||||
|
||||
export type Host = {
|
||||
bindings: { [key: number]: BindInfo }
|
||||
bindings: Bindings
|
||||
publicDomains: { [key: string]: PublicDomainConfig }
|
||||
privateDomains: Array<string>
|
||||
/**
|
||||
* COMPUTED: NetService::update
|
||||
*/
|
||||
hostnameInfo: { [key: number]: Array<HostnameInfo> }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// 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 = {
|
||||
privateDisabled: Array<GatewayId>
|
||||
publicEnabled: Array<GatewayId>
|
||||
assignedPort: number | null
|
||||
assignedSslPort: number | null
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ export { BackupTargetFS } from './BackupTargetFS'
|
||||
export { Base64 } from './Base64'
|
||||
export { BindId } from './BindId'
|
||||
export { BindInfo } from './BindInfo'
|
||||
export { BindingGatewaySetEnabledParams } from './BindingGatewaySetEnabledParams'
|
||||
export { BindingSetAddressEnabledParams } from './BindingSetAddressEnabledParams'
|
||||
export { Bindings } from './Bindings'
|
||||
export { BindOptions } from './BindOptions'
|
||||
export { BindParams } from './BindParams'
|
||||
export { Blake3Commitment } from './Blake3Commitment'
|
||||
@@ -64,6 +65,7 @@ export { Dependencies } from './Dependencies'
|
||||
export { DependencyMetadata } from './DependencyMetadata'
|
||||
export { DependencyRequirement } from './DependencyRequirement'
|
||||
export { DepInfo } from './DepInfo'
|
||||
export { DerivedAddressInfo } from './DerivedAddressInfo'
|
||||
export { Description } from './Description'
|
||||
export { DesiredStatus } from './DesiredStatus'
|
||||
export { DestroySubcontainerFsParams } from './DestroySubcontainerFsParams'
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { PackageId, ServiceInterfaceId, ServiceInterfaceType } from '../types'
|
||||
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 { DropGenerator, DropPromise } from './Drop'
|
||||
import { IpAddress, IPV6_LINK_LOCAL } from './ip'
|
||||
@@ -220,6 +226,14 @@ function filterRec(
|
||||
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 = (
|
||||
host: Host,
|
||||
addressInfo: AddressInfo,
|
||||
@@ -229,7 +243,8 @@ export const filledAddress = (
|
||||
const u = toUrls(h)
|
||||
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>(
|
||||
hostnames: HostnameInfo[],
|
||||
|
||||
@@ -83,32 +83,8 @@ export class InterfaceGatewaysComponent {
|
||||
|
||||
readonly gateways = input.required<InterfaceGateway[] | undefined>()
|
||||
|
||||
async onToggle(gateway: InterfaceGateway) {
|
||||
const addressInfo = this.interface.value()!.addressInfo
|
||||
const pkgId = this.interface.packageId()
|
||||
|
||||
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()
|
||||
}
|
||||
async onToggle(_gateway: InterfaceGateway) {
|
||||
// TODO: Replace with per-address toggle UI (Section 6 frontend overhaul).
|
||||
// Gateway-level toggle replaced by set-address-enabled RPC.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,10 +252,16 @@ export class InterfaceService {
|
||||
serviceInterface: T.ServiceInterface,
|
||||
host: T.Host,
|
||||
): T.HostnameInfo[] {
|
||||
let hostnameInfo =
|
||||
host.hostnameInfo[serviceInterface.addressInfo.internalPort]
|
||||
return (
|
||||
hostnameInfo?.filter(
|
||||
const binding =
|
||||
host.bindings[serviceInterface.addressInfo.internalPort]
|
||||
if (!binding) return []
|
||||
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 =>
|
||||
this.config.accessType === 'localhost' ||
|
||||
!(
|
||||
@@ -263,7 +269,6 @@ export class InterfaceService {
|
||||
utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) ||
|
||||
h.gateway.id === 'lo'
|
||||
),
|
||||
) || []
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -135,8 +135,8 @@ export default class ServiceInterfaceRoute {
|
||||
gateways.map(g => ({
|
||||
enabled:
|
||||
(g.public
|
||||
? binding?.net.publicEnabled.includes(g.id)
|
||||
: !binding?.net.privateDisabled.includes(g.id)) ?? false,
|
||||
? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id)
|
||||
: !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false,
|
||||
...g,
|
||||
})) || [],
|
||||
publicDomains: getPublicDomains(host.publicDomains, gateways),
|
||||
|
||||
@@ -96,8 +96,8 @@ export default class StartOsUiComponent {
|
||||
gateways: gateways.map(g => ({
|
||||
enabled:
|
||||
(g.public
|
||||
? binding?.net.publicEnabled.includes(g.id)
|
||||
: !binding?.net.privateDisabled.includes(g.id)) ?? false,
|
||||
? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id)
|
||||
: !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false,
|
||||
...g,
|
||||
})),
|
||||
publicDomains: getPublicDomains(network.host.publicDomains, gateways),
|
||||
|
||||
@@ -2126,22 +2126,12 @@ export namespace Mock {
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
privateDisabled: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 443,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
publicEnabled: [],
|
||||
possible: [
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -2152,7 +2142,6 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -2163,7 +2152,6 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -2174,7 +2162,6 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -2185,7 +2172,6 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -2197,7 +2183,6 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -2210,6 +2195,15 @@ export namespace Mock {
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 443,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
},
|
||||
bcdefgh: {
|
||||
bindings: {
|
||||
@@ -2218,8 +2212,11 @@ export namespace Mock {
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
privateDisabled: [],
|
||||
publicEnabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -2230,9 +2227,6 @@ export namespace Mock {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
bindings: {
|
||||
@@ -2241,8 +2235,11 @@ export namespace Mock {
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
privateDisabled: [],
|
||||
publicEnabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -2253,9 +2250,6 @@ export namespace Mock {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
storeExposedDependents: [],
|
||||
|
||||
@@ -281,13 +281,13 @@ export namespace RR {
|
||||
}
|
||||
export type RemoveAcmeRes = null
|
||||
|
||||
export type ServerBindingToggleGatewayReq = {
|
||||
// server.host.binding.set-gateway-enabled
|
||||
gateway: T.GatewayId
|
||||
export type ServerBindingSetAddressEnabledReq = {
|
||||
// server.host.binding.set-address-enabled
|
||||
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 = {
|
||||
// server.host.address.domain.public.add
|
||||
@@ -315,16 +315,16 @@ export namespace RR {
|
||||
}
|
||||
export type OsUiRemovePrivateDomainRes = null
|
||||
|
||||
export type PkgBindingToggleGatewayReq = Omit<
|
||||
ServerBindingToggleGatewayReq,
|
||||
export type PkgBindingSetAddressEnabledReq = Omit<
|
||||
ServerBindingSetAddressEnabledReq,
|
||||
'internalPort'
|
||||
> & {
|
||||
// package.host.binding.set-gateway-enabled
|
||||
// package.host.binding.set-address-enabled
|
||||
internalPort: number
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
export type PkgBindingToggleGatewayRes = null
|
||||
export type PkgBindingSetAddressEnabledRes = null
|
||||
|
||||
export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & {
|
||||
// package.host.address.domain.public.add
|
||||
|
||||
@@ -336,9 +336,9 @@ export abstract class ApiService {
|
||||
|
||||
abstract removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes>
|
||||
|
||||
abstract serverBindingToggleGateway(
|
||||
params: RR.ServerBindingToggleGatewayReq,
|
||||
): Promise<RR.ServerBindingToggleGatewayRes>
|
||||
abstract serverBindingSetAddressEnabled(
|
||||
params: RR.ServerBindingSetAddressEnabledReq,
|
||||
): Promise<RR.ServerBindingSetAddressEnabledRes>
|
||||
|
||||
abstract osUiAddPublicDomain(
|
||||
params: RR.OsUiAddPublicDomainReq,
|
||||
@@ -356,9 +356,9 @@ export abstract class ApiService {
|
||||
params: RR.OsUiRemovePrivateDomainReq,
|
||||
): Promise<RR.OsUiRemovePrivateDomainRes>
|
||||
|
||||
abstract pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes>
|
||||
abstract pkgBindingSetAddressEnabled(
|
||||
params: RR.PkgBindingSetAddressEnabledReq,
|
||||
): Promise<RR.PkgBindingSetAddressEnabledRes>
|
||||
|
||||
abstract pkgAddPublicDomain(
|
||||
params: RR.PkgAddPublicDomainReq,
|
||||
|
||||
@@ -607,11 +607,11 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async serverBindingToggleGateway(
|
||||
params: RR.ServerBindingToggleGatewayReq,
|
||||
): Promise<RR.ServerBindingToggleGatewayRes> {
|
||||
async serverBindingSetAddressEnabled(
|
||||
params: RR.ServerBindingSetAddressEnabledReq,
|
||||
): Promise<RR.ServerBindingSetAddressEnabledRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.binding.set-gateway-enabled',
|
||||
method: 'server.host.binding.set-address-enabled',
|
||||
params,
|
||||
})
|
||||
}
|
||||
@@ -652,11 +652,11 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes> {
|
||||
async pkgBindingSetAddressEnabled(
|
||||
params: RR.PkgBindingSetAddressEnabledReq,
|
||||
): Promise<RR.PkgBindingSetAddressEnabledRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.binding.set-gateway-enabled',
|
||||
method: 'package.host.binding.set-address-enabled',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1348,20 +1348,12 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async serverBindingToggleGateway(
|
||||
params: RR.ServerBindingToggleGatewayReq,
|
||||
): Promise<RR.ServerBindingToggleGatewayRes> {
|
||||
async serverBindingSetAddressEnabled(
|
||||
params: RR.ServerBindingSetAddressEnabledReq,
|
||||
): Promise<RR.ServerBindingSetAddressEnabledRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/serverInfo/network/host/bindings/${params.internalPort}/net/publicEnabled`,
|
||||
value: params.enabled ? [params.gateway] : [],
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
// Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1380,10 +1372,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: true,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1412,7 +1403,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
@@ -1433,10 +1424,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1466,7 +1456,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
@@ -1474,20 +1464,12 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes> {
|
||||
async pkgBindingSetAddressEnabled(
|
||||
params: RR.PkgBindingSetAddressEnabledReq,
|
||||
): Promise<RR.PkgBindingSetAddressEnabledRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/privateDisabled`,
|
||||
value: params.enabled ? [] : [params.gateway],
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
// Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1506,10 +1488,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
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: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: true,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1538,7 +1519,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
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)
|
||||
@@ -1559,10 +1540,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
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: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1592,7 +1572,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -38,8 +38,74 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: null,
|
||||
assignedSslPort: 443,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
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: {
|
||||
preferredExternalPort: 80,
|
||||
@@ -54,78 +120,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
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: {
|
||||
eth0: {
|
||||
@@ -503,22 +497,12 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
privateDisabled: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 443,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
publicEnabled: [],
|
||||
possible: [
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -529,7 +513,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -540,7 +523,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -551,7 +533,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -562,7 +543,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -574,7 +554,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
@@ -587,6 +566,15 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 443,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
},
|
||||
bcdefgh: {
|
||||
bindings: {
|
||||
@@ -595,8 +583,11 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
privateDisabled: [],
|
||||
publicEnabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -607,9 +598,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
bindings: {
|
||||
@@ -618,8 +606,11 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
privateDisabled: [],
|
||||
publicEnabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -630,9 +621,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
storeExposedDependents: [],
|
||||
|
||||
Reference in New Issue
Block a user