mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 04:01:58 +00:00
304 lines
16 KiB
Markdown
304 lines
16 KiB
Markdown
# AI Agent TODOs
|
|
|
|
Pending tasks for AI agents. Remove items when completed.
|
|
|
|
## Unreviewed CLAUDE.md Sections
|
|
|
|
- [ ] Architecture - Web (`/web`) - @MattDHill
|
|
|
|
## Features
|
|
|
|
- [ ] Support preferred external ports besides 443 - @dr-bonez
|
|
|
|
**Problem**: Currently, port 443 is the only preferred external port that is actually honored. When a
|
|
service requests `preferred_external_port: 8443` (or any non-443 value) for SSL, the system ignores
|
|
the preference and assigns a dynamic-range port (49152-65535). The `preferred_external_port` is only
|
|
used as a label for Tor mappings and as a trigger for the port-443 special case in `update()`.
|
|
|
|
**Goal**: Honor `preferred_external_port` for both SSL and non-SSL binds when the requested port is
|
|
available, with proper conflict resolution and fallback to dynamic-range allocation.
|
|
|
|
### Design
|
|
|
|
**Key distinction**: There are two separate concepts for SSL port usage:
|
|
|
|
1. **Port ownership** (`assigned_ssl_port`) — A port exclusively owned by a binding, allocated from
|
|
`AvailablePorts`. Used for server hostnames (`.local`, mDNS, etc.) and iptables forwards.
|
|
2. **Domain SSL port** — The port used for domain-based vhost entries. A binding does NOT need to own
|
|
a port to have a domain vhost on it. The VHostController already supports multiple hostnames on the
|
|
same port via SNI. Any binding can create a domain vhost entry on any SSL port that the
|
|
VHostController has a listener for, regardless of who "owns" that port.
|
|
|
|
For example: the OS owns port 443 as its `assigned_ssl_port`. A service with
|
|
`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`)
|
|
|
|
Expand `AvailablePorts` to support trying a preferred port before falling back to the dynamic range:
|
|
|
|
- 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.
|
|
|
|
Modify `BindInfo::new()` and `BindInfo::update()` to attempt the preferred port first:
|
|
|
|
```
|
|
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
|
|
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>,
|
|
}
|
|
```
|
|
|
|
`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.
|
|
|
|
**How disabling works per address type**:
|
|
|
|
The enforcement mechanism varies by address type because different addresses are reached through
|
|
different network paths:
|
|
|
|
- **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.
|
|
|
|
**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`)
|
|
|
|
**Current problem**: The `if ssl.preferred_external_port == 443` branch (line 341 of
|
|
`net_controller.rs`) creates a bespoke dual-vhost setup: port 5443 for private-only access and port
|
|
443 for public (or public+private). This exists because both public and private traffic arrive on the
|
|
same port 443 listener, and the current `InterfaceFilter`/`PublicFilter` model distinguishes
|
|
public/private by which *network interface* the connection arrived on — which doesn't work when both
|
|
traffic types share a listener.
|
|
|
|
**Solution**: Determine public vs private based on **source IP** at the vhost level. Traffic arriving
|
|
from the gateway IP should be treated as public (the gateway may MASQUERADE/NAT internet traffic, so
|
|
anything from the gateway is potentially public). Traffic from LAN IPs is private.
|
|
|
|
This applies to **all** vhost targets, not just port 443:
|
|
|
|
- **Add a `public` field to `ProxyTarget`** (or an enum: `Public`, `Private`, `Both`) indicating
|
|
what traffic this target accepts, derived from the binding's user-controlled `public` field.
|
|
- **Modify `VHostTarget::filter()`** (`vhost.rs:342`): Instead of (or in addition to) checking the
|
|
network interface via `GatewayInfo`, check the source IP of the TCP connection against known gateway
|
|
IPs. If the source IP matches a gateway or IP outside the subnet, the connection is public;
|
|
otherwise it's private. Use this to gate against the target's `public` field.
|
|
- **Eliminate the 5443 port entirely**: A single vhost entry on port 443 (or any shared SSL port) can
|
|
serve both public and private traffic, with per-target source-IP gating determining which backend
|
|
handles which connections.
|
|
|
|
#### 4. Port Forward Mapping in Patch-DB
|
|
|
|
When a binding is marked `public = true`, StartOS must record the required port forwards in patch-db
|
|
so the frontend can display them to the user. The user then configures these on their router manually.
|
|
|
|
For each public binding, store:
|
|
- The external port the router should forward (the actual vhost port used for domains, or the
|
|
`assigned_port` / `assigned_ssl_port` for non-domain access)
|
|
- The protocol (TCP/UDP)
|
|
- The StartOS LAN IP as the forward target
|
|
- Which service/binding this forward is for (for display purposes)
|
|
|
|
This mapping should be in the public database model so the frontend can read and display it.
|
|
|
|
#### 5. Simplify `update()` Domain Vhost Logic (`net_controller.rs`)
|
|
|
|
With source-IP gating in the vhost controller:
|
|
|
|
- **Remove the `== 443` special case** and the 5443 secondary vhost.
|
|
- For **server hostnames** (`.local`, mDNS, embassy, startos, localhost): use `assigned_ssl_port`
|
|
(the port the binding owns).
|
|
- For **domain-based vhost entries**: attempt to use `preferred_external_port` as the vhost port.
|
|
This succeeds if the port is either unused or already has an SSL listener (SNI handles sharing).
|
|
It fails only if the port is already in use by a non-SSL binding, or is a restricted port. On
|
|
failure, fall back to `assigned_ssl_port`.
|
|
- The binding's `public` field determines the `ProxyTarget`'s public/private gating.
|
|
- Hostname info must exactly match the actual vhost port used: for server hostnames, report
|
|
`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. Frontend: Interfaces Page Overhaul (View/Manage Split)
|
|
|
|
The current interfaces page is a single page showing gateways (with toggle), addresses, public
|
|
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, 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).
|
|
|
|
No gateway-level toggles. The old `gateways.component.ts` toggle UI is removed.
|
|
|
|
**Note**: Exact UI element placement (where toggles, buttons, info badges go) is sensitive.
|
|
Prompt the user for specific placement decisions during implementation.
|
|
|
|
##### Manage Page
|
|
|
|
Simple CRUD interface for configuring which addresses exist. Two 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`
|
|
|
|
##### 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 compute enabled addresses from `DerivedAddressInfo` |
|
|
| `web/projects/ui/src/app/routes/portal/components/interfaces/addresses/` | Refactor for View page with overflow menu (enable/disable) 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
|
|
address: HostnameInfo
|
|
}
|
|
```
|
|
|
|
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`/`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/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) |
|
|
| `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed |
|
|
| `core/src/db/model/public.rs` | Public DB model — port forward mapping |
|
|
|
|
- [ ] Auto-configure port forwards via UPnP/NAT-PMP/PCP - @dr-bonez
|
|
|
|
**Blocked by**: "Support preferred external ports besides 443" (must be implemented and tested
|
|
end-to-end first).
|
|
|
|
**Goal**: When a binding is marked public, automatically configure port forwards on the user's router
|
|
using UPnP, NAT-PMP, or PCP, instead of requiring manual router configuration. Fall back to
|
|
displaying manual instructions (the port forward mapping from patch-db) when auto-configuration is
|
|
unavailable or fails.
|