Skip to content

feat(net): IPv6 GUA exposure tri-state (disabled/LAN/LAN+WAN)#3368

Merged
dr-bonez merged 6 commits into
masterfrom
feat/ipv6-gua-tristate
Jul 1, 2026
Merged

feat(net): IPv6 GUA exposure tri-state (disabled/LAN/LAN+WAN)#3368
dr-bonez merged 6 commits into
masterfrom
feat/ipv6-gua-tristate

Conversation

@helix-nine

@helix-nine helix-nine commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Stacks on #3366 (feat/port-range-interfaces) — base is that branch, not master.

Gives IPv6 GUA (global-unicast) addresses on a service interface a Disabled / LAN / LAN+WAN tri-state instead of the on/off toggle every other address uses. A GUA is a single globally-routable address, so "on" must distinguish LAN-only (firewalled to the subnet) from WAN-exposed. Per @dr-bonez:

  • LAN (default, behavior-preserving) — reachable on-link; traffic from outside the gateway subnet is rejected (already enforced by vhost::ProxyTarget::filter()).
  • LAN+WAN — also exposed to the WAN (vhost binds the GUA and accepts any source); the automatic gateway pinhole + non-SSL forwarding land in a follow-up (see scope note).
  • IPv4 keeps separate LAN/WAN rows; ULAs can't be exposed publicly, so only GUAs get the tri-state.

In this PR

  • ModelGuaAccess { Disabled, Lan, LanWan } (default Lan) + gua_access: BTreeMap<SocketAddrV6, GuaAccess> on DerivedAddressInfo; HostnameInfo::gua(), access_for/is_wan, GUA-aware enabled().
  • RPCset-gua-access (mirrors the removed set_range_gateway_access); CliFromJsonString<T> so the param is a structured HostnameInfo over the wire but a JSON-string CLI arg (also applied to the inherited set_address_enabled). i18n ×5.
  • Derivation — both the SSL/vhost and non-SSL paths classify WAN via is_wan().
  • UI — client-side GUA detection + a Disabled/LAN/LAN+WAN selector on the Interfaces page, wired to set-gua-access (+ mock). check:ui green.
  • Docs + CHANGELOG.

Verified: cargo check, model unit tests, ts-bindings (no drift), check:ui.

Follow-up — separate PR (per @dr-bonez, easier to review)

The v6 WAN backend forwarding, which shares the forward path and needs VM validation:

  • port_map Ipv4AddrIpAddr generalization (UPnP stays v4-gated) + PCP-v6 pinhole for LAN+WAN GUAs (the v6 gateway is already in ip_info.lan_ip).
  • Non-SSL v6 forwardinglxcbr0 IPv6, container v6 discovery/storage, an ip6 startos nft table (filter + LAN source-reject), a v6 forward-port, and net.ipv6.conf.all.forwarding.

The slot's pre-commit hook is broken (cd web/, a stale pre-reorg path), so commits used --no-verify; formatting verified via prettier + check:ui locally and CI.

Comment thread shared-libs/crates/start-core/src/net/host/binding.rs Outdated
Comment thread shared-libs/crates/start-core/src/net/host/binding.rs Outdated
helix-nine added a commit that referenced this pull request Jun 30, 2026
…ing param

Per @dr-bonez's review on #3368:

- `gua_access` keyed by `SocketAddrV6` (type-precise — GUAs are always v6);
  `HostnameInfo::gua()` returns `SocketAddrV6`.
- New `CliFromJsonString<T>`: a param that is structured `T` over the JSON-RPC
  wire (serde passthrough — no double-encoding) but a JSON-string arg on the CLI
  (clap `ValueParser` parses the JSON). Replaces the `address: String` +
  `serde_json::from_str` pattern in both `set_gua_access` and the inherited
  `set_address_enabled`. Pair with `#[ts(as = "HostnameInfo")]` so the binding
  shows the object.
- UI: `set-address-enabled` now sends the `HostnameInfo` object instead of a
  JSON string (api.types, both toggle callers, mock).
- ts-bindings regenerated.
@helix-nine helix-nine marked this pull request as ready for review June 30, 2026 18:34
Comment thread shared-libs/crates/start-core/src/net/service_interface.rs Outdated
Comment thread shared-libs/crates/start-core/src/net/service_interface.rs Outdated
dr-bonez
dr-bonez previously approved these changes Jun 30, 2026
helix-nine added a commit that referenced this pull request Jun 30, 2026
…ing param

Per @dr-bonez's review on #3368:

- `gua_access` keyed by `SocketAddrV6` (type-precise — GUAs are always v6);
  `HostnameInfo::gua()` returns `SocketAddrV6`.
- New `CliFromJsonString<T>`: a param that is structured `T` over the JSON-RPC
  wire (serde passthrough — no double-encoding) but a JSON-string arg on the CLI
  (clap `ValueParser` parses the JSON). Replaces the `address: String` +
  `serde_json::from_str` pattern in both `set_gua_access` and the inherited
  `set_address_enabled`. Pair with `#[ts(as = "HostnameInfo")]` so the binding
  shows the object.
- UI: `set-address-enabled` now sends the `HostnameInfo` object instead of a
  JSON string (api.types, both toggle callers, mock).
- ts-bindings regenerated.
@helix-nine helix-nine force-pushed the feat/ipv6-gua-tristate branch from e98448b to a4b1fc6 Compare June 30, 2026 22:08
helix-nine added a commit that referenced this pull request Jun 30, 2026
…ing param

Per @dr-bonez's review on #3368:

- `gua_access` keyed by `SocketAddrV6` (type-precise — GUAs are always v6);
  `HostnameInfo::gua()` returns `SocketAddrV6`.
- New `CliFromJsonString<T>`: a param that is structured `T` over the JSON-RPC
  wire (serde passthrough — no double-encoding) but a JSON-string arg on the CLI
  (clap `ValueParser` parses the JSON). Replaces the `address: String` +
  `serde_json::from_str` pattern in both `set_gua_access` and the inherited
  `set_address_enabled`. Pair with `#[ts(as = "HostnameInfo")]` so the binding
  shows the object.
- UI: `set-address-enabled` now sends the `HostnameInfo` object instead of a
  JSON string (api.types, both toggle callers, mock).
- ts-bindings regenerated.
@helix-nine helix-nine force-pushed the feat/ipv6-gua-tristate branch from a4b1fc6 to bcf56f3 Compare June 30, 2026 23:24
helix-nine added a commit that referenced this pull request Jul 1, 2026
…ing param

Per @dr-bonez's review on #3368:

- `gua_access` keyed by `SocketAddrV6` (type-precise — GUAs are always v6);
  `HostnameInfo::gua()` returns `SocketAddrV6`.
- New `CliFromJsonString<T>`: a param that is structured `T` over the JSON-RPC
  wire (serde passthrough — no double-encoding) but a JSON-string arg on the CLI
  (clap `ValueParser` parses the JSON). Replaces the `address: String` +
  `serde_json::from_str` pattern in both `set_gua_access` and the inherited
  `set_address_enabled`. Pair with `#[ts(as = "HostnameInfo")]` so the binding
  shows the object.
- UI: `set-address-enabled` now sends the `HostnameInfo` object instead of a
  JSON string (api.types, both toggle callers, mock).
- ts-bindings regenerated.
@helix-nine helix-nine force-pushed the feat/ipv6-gua-tristate branch from bcf56f3 to 96fb458 Compare July 1, 2026 00:17
helix-nine added a commit that referenced this pull request Jul 1, 2026
…ing param

Per @dr-bonez's review on #3368:

- `gua_access` keyed by `SocketAddrV6` (type-precise — GUAs are always v6);
  `HostnameInfo::gua()` returns `SocketAddrV6`.
- New `CliFromJsonString<T>`: a param that is structured `T` over the JSON-RPC
  wire (serde passthrough — no double-encoding) but a JSON-string arg on the CLI
  (clap `ValueParser` parses the JSON). Replaces the `address: String` +
  `serde_json::from_str` pattern in both `set_gua_access` and the inherited
  `set_address_enabled`. Pair with `#[ts(as = "HostnameInfo")]` so the binding
  shows the object.
- UI: `set-address-enabled` now sends the `HostnameInfo` object instead of a
  JSON string (api.types, both toggle callers, mock).
- ts-bindings regenerated.
@helix-nine helix-nine force-pushed the feat/ipv6-gua-tristate branch from 96fb458 to 1c5572e Compare July 1, 2026 00:33
helix-nine added a commit that referenced this pull request Jul 1, 2026
…ing param

Per @dr-bonez's review on #3368:

- `gua_access` keyed by `SocketAddrV6` (type-precise — GUAs are always v6);
  `HostnameInfo::gua()` returns `SocketAddrV6`.
- New `CliFromJsonString<T>`: a param that is structured `T` over the JSON-RPC
  wire (serde passthrough — no double-encoding) but a JSON-string arg on the CLI
  (clap `ValueParser` parses the JSON). Replaces the `address: String` +
  `serde_json::from_str` pattern in both `set_gua_access` and the inherited
  `set_address_enabled`. Pair with `#[ts(as = "HostnameInfo")]` so the binding
  shows the object.
- UI: `set-address-enabled` now sends the `HostnameInfo` object instead of a
  JSON string (api.types, both toggle callers, mock).
- ts-bindings regenerated.
@helix-nine helix-nine force-pushed the feat/ipv6-gua-tristate branch from 1c5572e to cf634fe Compare July 1, 2026 01:39
helix-nine added a commit that referenced this pull request Jul 1, 2026
…ing param

Per @dr-bonez's review on #3368:

- `gua_access` keyed by `SocketAddrV6` (type-precise — GUAs are always v6);
  `HostnameInfo::gua()` returns `SocketAddrV6`.
- New `CliFromJsonString<T>`: a param that is structured `T` over the JSON-RPC
  wire (serde passthrough — no double-encoding) but a JSON-string arg on the CLI
  (clap `ValueParser` parses the JSON). Replaces the `address: String` +
  `serde_json::from_str` pattern in both `set_gua_access` and the inherited
  `set_address_enabled`. Pair with `#[ts(as = "HostnameInfo")]` so the binding
  shows the object.
- UI: `set-address-enabled` now sends the `HostnameInfo` object instead of a
  JSON string (api.types, both toggle callers, mock).
- ts-bindings regenerated.
@helix-nine helix-nine force-pushed the feat/ipv6-gua-tristate branch from cf634fe to 5ab0cc5 Compare July 1, 2026 02:03
helix-nine added a commit that referenced this pull request Jul 1, 2026
…ing param

Per @dr-bonez's review on #3368:

- `gua_access` keyed by `SocketAddrV6` (type-precise — GUAs are always v6);
  `HostnameInfo::gua()` returns `SocketAddrV6`.
- New `CliFromJsonString<T>`: a param that is structured `T` over the JSON-RPC
  wire (serde passthrough — no double-encoding) but a JSON-string arg on the CLI
  (clap `ValueParser` parses the JSON). Replaces the `address: String` +
  `serde_json::from_str` pattern in both `set_gua_access` and the inherited
  `set_address_enabled`. Pair with `#[ts(as = "HostnameInfo")]` so the binding
  shows the object.
- UI: `set-address-enabled` now sends the `HostnameInfo` object instead of a
  JSON string (api.types, both toggle callers, mock).
- ts-bindings regenerated.
@helix-nine helix-nine force-pushed the feat/ipv6-gua-tristate branch from 5ab0cc5 to c729eed Compare July 1, 2026 16:31
Base automatically changed from feat/port-range-interfaces to master July 1, 2026 16:50
@dr-bonez dr-bonez dismissed their stale review July 1, 2026 16:50

The base branch was changed.

… + RPC + derivation

An IPv6 global-unicast address is a single globally-routable address, so a
plain on/off toggle can't express LAN-only (firewalled) vs WAN-exposed. Give
GUAs a tri-state instead, defaulting to LAN (behavior-preserving — GUAs are
LAN-only source-filtered today).

- GuaAccess { Disabled, Lan, LanWan } + gua_access map on DerivedAddressInfo
- HostnameInfo::gua() (global-unicast detection), access_for/is_wan, GUA-aware enabled()
- set-gua-access RPC (mirrors the removed set_range_gateway_access) + i18n x5
- net_controller derivation (SSL/vhost + non-SSL) classifies WAN via is_wan(),
  so a LAN+WAN GUA flows into the public set — vhost already binds the GUA and
  source-filters LAN per ProxyTarget::filter()
- ts-bindings regenerated; model unit tests

Follow-up commits (this PR): PCP-v6 pinhole, non-SSL v6 forwarding (lxcbr0 IPv6
+ ip6 nft table), UI tri-state control, docs/CHANGELOG.
…ing param

Per @dr-bonez's review on #3368:

- `gua_access` keyed by `SocketAddrV6` (type-precise — GUAs are always v6);
  `HostnameInfo::gua()` returns `SocketAddrV6`.
- New `CliFromJsonString<T>`: a param that is structured `T` over the JSON-RPC
  wire (serde passthrough — no double-encoding) but a JSON-string arg on the CLI
  (clap `ValueParser` parses the JSON). Replaces the `address: String` +
  `serde_json::from_str` pattern in both `set_gua_access` and the inherited
  `set_address_enabled`. Pair with `#[ts(as = "HostnameInfo")]` so the binding
  shows the object.
- UI: `set-address-enabled` now sends the `HostnameInfo` object instead of a
  JSON string (api.types, both toggle callers, mock).
- ts-bindings regenerated.
Render IPv6 GUA addresses on the service Interfaces page with a
Disabled / LAN / LAN+WAN selector instead of the on/off toggle every other
address uses, wired to the new set-gua-access RPC.

- interface.service.ts: client-side GUA detection (matches the backend's
  ipv6_is_local complement), `GatewayAddress.guaAccess`, GUA-aware `isEnabled`.
- item.component.ts: tri-state <select> for GUA rows (switch for the rest) +
  onSetGuaAccess handler.
- api: serverBindingSetGuaAccess / pkgBindingSetGuaAccess (types, abstract,
  live RPC, mock + mockSetGuaAccess).
- fixtures/mocks: guaAccess: {} on DerivedAddressInfo literals.

check:ui passes.
Interfaces page + CHANGELOG: the Disabled / LAN / LAN+WAN control for IPv6
global-unicast addresses (default LAN, LAN+WAN attempts a PCP pinhole).
Per @dr-bonez: parse the hostname straight to Ipv6Addr instead of IpAddr +
matching IpAddr::V6. The parse is itself the discriminant, so the redundant
Ipv6-metadata guard is dropped.
Per @dr-bonez: restore the `matches!(metadata, Ipv6 { .. })` guard; only the
IpAddr::V6 parse dance was the redundant part.
@dr-bonez

dr-bonez commented Jul 1, 2026

Copy link
Copy Markdown
Member

@helix-nine rebase this

@helix-nine helix-nine force-pushed the feat/ipv6-gua-tristate branch from c729eed to df10f6a Compare July 1, 2026 17:09
@dr-bonez dr-bonez merged commit fe52a19 into master Jul 1, 2026
16 checks passed
@dr-bonez dr-bonez deleted the feat/ipv6-gua-tristate branch July 1, 2026 17:22
@helix-nine

Copy link
Copy Markdown
Contributor Author

Already covered — I'd rebased this onto master (df10f6a79) before you merged it, and it squash-landed as fe52a19ee. #3373 is now rebased on top of the merged master too (bc1162907, 151 net:: green), so the stack is current.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants