From c0b2cbe1c8256852adf20f3c4feccf69c66ba7b5 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 6 Feb 2026 09:30:35 -0700 Subject: [PATCH] docs: update preferred external port design in TODO --- agents/TODO.md | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/agents/TODO.md b/agents/TODO.md index 70124aa74..2ee9bbe2b 100644 --- a/agents/TODO.md +++ b/agents/TODO.md @@ -6,4 +6,111 @@ Pending tasks for AI agents. Remove items when completed. - [ ] 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`: 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. Eliminate the Port 5443 Hack: Source-IP-Based Public/Private Gating (`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. + - **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. + + #### 3. 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`. + - Each domain vhost entry declares whether it's public, private, or both — the vhost controller uses + source IP to enforce this. + - 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`. + + #### 4. No SDK or Frontend Changes Needed + + - SDK: `preferredExternalPort` is already exposed in `BindOptions` and `AddSslOptions`. + - Frontend: Already reads `assigned_port`/`assigned_ssl_port` from `NetInfo`. + + ### 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/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private | + | `core/src/net/gateway.rs` | `PublicFilter`, `InterfaceFilter` — may need refactoring | + | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed |