From 2e03a95e4716bcec1e4f01a587ee8476f28494bd Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 9 Feb 2026 13:10:57 -0700 Subject: [PATCH] docs: overhaul interfaces page design with view/manage split and per-address controls --- agents/TODO.md | 175 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 144 insertions(+), 31 deletions(-) diff --git a/agents/TODO.md b/agents/TODO.md index 9d39f2d7a..956343afb 100644 --- a/agents/TODO.md +++ b/agents/TODO.md @@ -57,44 +57,73 @@ Pending tasks for AI agents. Remove items when completed. 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. User-Controlled Public/Private on Bindings + #### 2. Per-Address Enable/Disable (replaces gateway overrides) - There are three distinct concepts for public/private in the system: + **Current model being removed**: `NetInfo` has `private_disabled: OrdSet` and + `public_enabled: OrdSet` — 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. - 1. **Gateway public/private** (descriptive): A property of the gateway/network interface — whether - its ports are publicly reachable from the internet without port forwards. This is - `NetworkInterfaceInfo::public()`. - 2. **Binding public/private** (user intent): Whether the user wants this binding to be reachable from - the internet. This is a **new** user-controlled field on the binding. - 3. **`public_enabled` / `private_disabled`** (per-gateway overrides): Override the default behavior - where private gateways are enabled and public gateways are disabled. These sets are per-gateway - exceptions to that default. + **New model**: Per-address enable/disable. Each computed address in `hostname_info` gets an `enabled` + field. Users toggle individual addresses on the View page (see Section 6). - Add a `public` field to `BindInfo` (or `NetInfo`) representing user intent: + **How disabling works per address type**: - - **When `public = false`** (default): Block WAN traffic to this binding using source-IP gating - (see Section 3). The binding is only accessible on the LAN. - - **When `public = true`**: Allow WAN traffic. Additionally, maintain a port forward mapping in - patch-db (see Section 4) so the user knows what to configure on their router. + The enforcement mechanism varies by address type because different addresses are reached through + different network paths: - **New RPC endpoints** (`binding.rs`): + - **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. + - **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. + - **Onion addresses**: Disabled by **not creating the Tor hidden service mapping**. The Tor + daemon won't advertise the onion:port, so no traffic arrives. - Following the existing `HostApiKind` pattern, add a new subcommand to the `binding` parent handler: + **Backend changes**: - - **`set-public`** — Set whether a binding should be publicly accessible. + - **Remove from `NetInfo`**: Delete the `private_disabled` and `public_enabled` fields entirely. + - **Add to `NetInfo`**: A `disabled` set containing identifiers for addresses the user has explicitly + disabled. The identifier must be stable across network changes — e.g., `(gateway_id, hostname_kind)` + for IP/Local addresses, `(gateway_id, domain_value)` for domain addresses, or `onion_value` for + onion addresses. Exact format TBD at implementation time. + - **Default behavior preserved**: Private-gateway addresses default to enabled, public-gateway + addresses default to disabled. An address is enabled if it's not in the `disabled` set AND either + (a) the gateway is private, or (b) the user has explicitly enabled it. + - **Add `enabled` field to `HostnameInfo`**: The computed `hostname_info` output should include + whether each address is enabled, derived from the `disabled` set during `NetServiceData::update()`. + - **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 in `update()`. + + **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 BindingSetPublicParams { + interface BindingSetAddressEnabledParams { internalPort: number - public: boolean + addressId: AddressId // identifies a specific HostnameInfo entry + enabled: boolean } ``` - Mutates `BindInfo` to set the `public` field, syncs the host. Uses `sync_db` metadata. + Mutates `NetInfo.disabled` and syncs the host. Uses `sync_db` metadata. This yields two RPC methods: - - `server.host.binding.set-public` - - `package.host.binding.set-public` + - `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`) @@ -151,22 +180,106 @@ Pending tasks for AI agents. Remove items when completed. `ssl_port: assigned_ssl_port`. For domains, report `ssl_port: preferred_external_port` if it was successfully used for the domain vhost, otherwise report `ssl_port: assigned_ssl_port`. - #### 6. SDK and Frontend Changes + #### 6. Frontend: Interfaces Page Overhaul (View/Manage Split) - - **SDK**: `preferredExternalPort` is already exposed. No additional SDK changes needed. - - **Frontend**: Needs to display the port forward mapping from patch-db, showing the user what - router configuration is required for their public bindings. Also needs UI for toggling the - `public` field on a binding. Bindings are always private by default. + The current interfaces page is a single page showing gateways (with toggle), addresses, public + domains, Tor domains, and private domains. It gets split into two pages: **View** and **Manage**. + + **SDK**: `preferredExternalPort` is already exposed. No additional SDK changes needed. + + ##### View Page + + Displays all computed addresses for the interface (from `hostname_info`) as a flat list. For each + address: + + - **Enable/disable toggle**: Calls `set-address-enabled` RPC endpoint to toggle the address. Reflects + the `enabled` field from `HostnameInfo`. + - **Address details**: URL, type (IPv4, IPv6, .local, domain, onion), access level (public/private), + gateway name. + - **Port forward info**: For public addresses, display the required port forward mapping (external + port, protocol, LAN IP target) so the user knows what to configure on their router. + - **Test button**: Per-address reachability test (see Section 7). The backend returns structured + failure data (which check failed, relevant context like expected IP, actual IP, port, etc.). + The frontend constructs user-facing fix messaging from the structured result. + + No gateway-level toggles. The old `gateways.component.ts` toggle UI is removed. + + ##### Manage Page + + Simple CRUD interface for configuring which addresses exist. Three sections: + + - **Public domains**: Add/remove. Uses existing RPC endpoints: + - `{server,package}.host.address.domain.public.add` + - `{server,package}.host.address.domain.public.remove` + - **Private domains**: Add/remove. Uses existing RPC endpoints: + - `{server,package}.host.address.domain.private.add` + - `{server,package}.host.address.domain.private.remove` + - **Onion addresses**: Add/remove. Uses existing RPC endpoints: + - `{server,package}.host.address.onion.add` + - `{server,package}.host.address.onion.remove` + + ##### Key Frontend Files to Modify + + | File | Change | + |------|--------| + | `web/projects/ui/src/app/routes/portal/components/interfaces/` | Overhaul: split into view/manage | + | `web/projects/ui/src/app/routes/portal/components/interfaces/gateways.component.ts` | Remove (replaced by per-address toggles on View page) | + | `web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts` | Update `MappedServiceInterface` to use `enabled` field from `HostnameInfo` | + | `web/projects/ui/src/app/routes/portal/components/interfaces/addresses/` | Refactor for View page with enable/disable toggles and test buttons | + | `web/projects/ui/src/app/routes/portal/routes/services/services.routes.ts` | Add routes for view/manage sub-pages | + | `web/projects/ui/src/app/routes/portal/routes/system/system.routes.ts` | Add routes for view/manage sub-pages | + + #### 7. Reachability Test Endpoint + + New RPC endpoint that tests whether an address is actually reachable, with diagnostic info on + failure. + + **RPC endpoint** (`binding.rs` or new file): + + - **`test-address`** — Test reachability of a specific address. + + ```ts + interface BindingTestAddressParams { + internalPort: number + addressId: AddressId + } + ``` + + The backend simply performs the raw checks and returns the results. The **frontend** owns all + interpretation — it already knows the address type, expected IP, expected port, etc. from the + `HostnameInfo` data, so it can compare against the backend results and construct fix messaging. + + ```ts + interface TestAddressResult { + dns: string[] | null // resolved IPs, null if not a domain address or lookup failed + portOpen: boolean | null // TCP connect result, null if not applicable + } + ``` + + This yields two RPC methods: + - `server.host.binding.test-address` + - `package.host.binding.test-address` + + The frontend already has the full `HostnameInfo` context (expected IP, domain, port, gateway, + public/private). It compares the backend's raw results against the expected state and constructs + localized fix instructions. For example: + - `dns` returned but doesn't contain the expected WAN IP → "Update DNS A record for {domain} + to {wanIp}" + - `dns` is `null` for a domain address → "DNS lookup failed for {domain}" + - `portOpen` is `false` → "Configure port forward on your router: external {port} TCP → + {lanIp}:{port}" ### Key Files | File | Role | |------|------| | `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation | - | `core/src/net/host/binding.rs` | `BindInfo::new()` / `update()` — port assignment at bind time | - | `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — vhost/forward/DNS reconciliation, 5443 hack removal | + | `core/src/net/host/binding.rs` | `BindInfo`/`NetInfo` — remove gateway overrides, add per-address disable set, 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/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private | - | `core/src/net/gateway.rs` | `PublicFilter`, `InterfaceFilter` — may need refactoring | + | `core/src/net/gateway.rs` | `InterfaceFilter` — remove `NetInfo` impl, simplify | + | `core/src/net/service_interface.rs` | `HostnameInfo` — add `enabled` field | + | `core/src/net/host/address.rs` | Existing domain/onion CRUD endpoints (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 |