mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 18:31:52 +00:00
Compare commits
12 Commits
bugfix/alp
...
feature/ou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea130079ab | ||
|
|
2a54625f43 | ||
|
|
4e638fb58e | ||
|
|
73274ef6e0 | ||
|
|
e1915bf497 | ||
|
|
8204074bdf | ||
|
|
2ee403e7de | ||
|
|
cc5f316514 | ||
|
|
1974dfd66f | ||
|
|
2e03a95e47 | ||
|
|
8f809dab21 | ||
|
|
c0b2cbe1c8 |
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"attribution": {
|
"attribution": {
|
||||||
"commit": ""
|
"commit": "",
|
||||||
|
"pr": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
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
|
## Architecture
|
||||||
|
|
||||||
### Core (`/core`)
|
### Core (`/core`)
|
||||||
|
|||||||
219
agents/TODO.md
219
agents/TODO.md
@@ -6,4 +6,223 @@ Pending tasks for AI agents. Remove items when completed.
|
|||||||
|
|
||||||
- [ ] Architecture - Web (`/web`) - @MattDHill
|
- [ ] 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 ✅ DONE
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
#### 2. Per-Address Enable/Disable ✅ DONE
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
**`DerivedAddressInfo` struct** (on `BindInfo`):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct DerivedAddressInfo {
|
||||||
|
pub private_disabled: BTreeSet<HostnameInfo>,
|
||||||
|
pub public_enabled: BTreeSet<HostnameInfo>,
|
||||||
|
pub possible: BTreeSet<HostnameInfo>, // COMPUTED by update()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
**RPC endpoint**: `set-gateway-enabled` replaced with `set-address-enabled` (on both
|
||||||
|
`server.host.binding` and `package.host.binding`).
|
||||||
|
|
||||||
|
**How disabling works per address type** (enforcement deferred to Section 3):
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
#### 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 `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).
|
||||||
|
|
||||||
|
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, `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` 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 |
|
||||||
|
|
||||||
|
- [ ] 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.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ if [ -z "$sip" ] || [ -z "$dip" ] || [ -z "$dprefix" ] || [ -z "$sport" ] || [ -
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
NAME="F$(echo "$sip:$sport -> $dip/$dprefix:$dport" | sha256sum | head -c 15)"
|
NAME="F$(echo "$sip:$sport -> $dip/$dprefix:$dport ${src_subnet:-any} ${excluded_src:-none}" | sha256sum | head -c 15)"
|
||||||
|
|
||||||
for kind in INPUT FORWARD ACCEPT; do
|
for kind in INPUT FORWARD ACCEPT; do
|
||||||
if ! iptables -C $kind -j "${NAME}_${kind}" 2> /dev/null; then
|
if ! iptables -C $kind -j "${NAME}_${kind}" 2> /dev/null; then
|
||||||
@@ -36,8 +36,22 @@ if [ "$UNDO" = 1 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# DNAT: rewrite destination for incoming packets (external traffic)
|
# DNAT: rewrite destination for incoming packets (external traffic)
|
||||||
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
# When src_subnet is set, only forward traffic from that subnet (private forwards)
|
||||||
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
# excluded_src: comma-separated gateway/router IPs to reject (they may masquerade internet traffic)
|
||||||
|
if [ -n "$src_subnet" ]; then
|
||||||
|
if [ -n "$excluded_src" ]; then
|
||||||
|
IFS=',' read -ra EXCLUDED <<< "$excluded_src"
|
||||||
|
for excl in "${EXCLUDED[@]}"; do
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -s "$excl" -d "$sip" -p tcp --dport "$sport" -j RETURN
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -s "$excl" -d "$sip" -p udp --dport "$sport" -j RETURN
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -s "$src_subnet" -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -s "$src_subnet" -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
else
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
fi
|
||||||
|
|
||||||
# DNAT: rewrite destination for locally-originated packets (hairpin from host itself)
|
# DNAT: rewrite destination for locally-originated packets (hairpin from host itself)
|
||||||
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ Evaluate a script in the runtime context. Used for debugging.
|
|||||||
|
|
||||||
The `execute` and `sandbox` methods route to procedures based on the `procedure` path:
|
The `execute` and `sandbox` methods route to procedures based on the `procedure` path:
|
||||||
|
|
||||||
| Procedure | Description |
|
| Procedure | Description |
|
||||||
|-----------|-------------|
|
| -------------------------- | ---------------------------- |
|
||||||
| `/backup/create` | Create a backup |
|
| `/backup/create` | Create a backup |
|
||||||
| `/actions/{name}/getInput` | Get input spec for an action |
|
| `/actions/{name}/getInput` | Get input spec for an action |
|
||||||
| `/actions/{name}/run` | Run an action with input |
|
| `/actions/{name}/run` | Run an action with input |
|
||||||
|
|||||||
@@ -82,18 +82,15 @@ export class DockerProcedureContainer extends Drop {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
} else if (volumeMount.type === "certificate") {
|
} else if (volumeMount.type === "certificate") {
|
||||||
|
const hostInfo = await effects.getHostInfo({
|
||||||
|
hostId: volumeMount["interface-id"],
|
||||||
|
})
|
||||||
const hostnames = [
|
const hostnames = [
|
||||||
`${packageId}.embassy`,
|
`${packageId}.embassy`,
|
||||||
...new Set(
|
...new Set(
|
||||||
Object.values(
|
Object.values(hostInfo?.bindings || {})
|
||||||
(
|
.flatMap((b) => b.addresses.possible)
|
||||||
await effects.getHostInfo({
|
.map((h) => h.hostname.value),
|
||||||
hostId: volumeMount["interface-id"],
|
|
||||||
})
|
|
||||||
)?.hostnameInfo || {},
|
|
||||||
)
|
|
||||||
.flatMap((h) => h)
|
|
||||||
.flatMap((h) => (h.kind === "onion" ? [h.hostname.value] : [])),
|
|
||||||
).values(),
|
).values(),
|
||||||
]
|
]
|
||||||
const certChain = await effects.getSslCertificate({
|
const certChain = await effects.getSslCertificate({
|
||||||
|
|||||||
@@ -1244,12 +1244,8 @@ async function updateConfig(
|
|||||||
? ""
|
? ""
|
||||||
: catchFn(
|
: catchFn(
|
||||||
() =>
|
() =>
|
||||||
(specValue.target === "lan-address"
|
filled.addressInfo!.filter({ kind: "mdns" })!.hostnames[0]
|
||||||
? filled.addressInfo!.filter({ kind: "mdns" }) ||
|
.hostname.value,
|
||||||
filled.addressInfo!.onion
|
|
||||||
: filled.addressInfo!.onion ||
|
|
||||||
filled.addressInfo!.filter({ kind: "mdns" })
|
|
||||||
).hostnames[0].hostname.value,
|
|
||||||
) || ""
|
) || ""
|
||||||
mutConfigValue[key] = url
|
mutConfigValue[key] = url
|
||||||
}
|
}
|
||||||
|
|||||||
2724
core/Cargo.lock
generated
2724
core/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ license = "MIT"
|
|||||||
name = "start-os"
|
name = "start-os"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/Start9Labs/start-os"
|
repository = "https://github.com/Start9Labs/start-os"
|
||||||
version = "0.4.0-alpha.19" # VERSION_BUMP
|
version = "0.4.0-alpha.20" # VERSION_BUMP
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "startos"
|
name = "startos"
|
||||||
@@ -42,17 +42,6 @@ name = "tunnelbox"
|
|||||||
path = "src/main/tunnelbox.rs"
|
path = "src/main/tunnelbox.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
arti = [
|
|
||||||
"arti-client",
|
|
||||||
"safelog",
|
|
||||||
"tor-cell",
|
|
||||||
"tor-hscrypto",
|
|
||||||
"tor-hsservice",
|
|
||||||
"tor-keymgr",
|
|
||||||
"tor-llcrypto",
|
|
||||||
"tor-proto",
|
|
||||||
"tor-rtcompat",
|
|
||||||
]
|
|
||||||
beta = []
|
beta = []
|
||||||
console = ["console-subscriber", "tokio/tracing"]
|
console = ["console-subscriber", "tokio/tracing"]
|
||||||
default = []
|
default = []
|
||||||
@@ -62,16 +51,6 @@ unstable = ["backtrace-on-stack-overflow"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes = { version = "0.7.5", features = ["ctr"] }
|
aes = { version = "0.7.5", features = ["ctr"] }
|
||||||
arti-client = { version = "0.33", features = [
|
|
||||||
"compression",
|
|
||||||
"ephemeral-keystore",
|
|
||||||
"experimental-api",
|
|
||||||
"onion-service-client",
|
|
||||||
"onion-service-service",
|
|
||||||
"rustls",
|
|
||||||
"static",
|
|
||||||
"tokio",
|
|
||||||
], default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
|
|
||||||
async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [
|
async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [
|
||||||
"use_rustls",
|
"use_rustls",
|
||||||
"use_tokio",
|
"use_tokio",
|
||||||
@@ -100,7 +79,6 @@ console-subscriber = { version = "0.5.0", optional = true }
|
|||||||
const_format = "0.2.34"
|
const_format = "0.2.34"
|
||||||
cookie = "0.18.0"
|
cookie = "0.18.0"
|
||||||
cookie_store = "0.22.0"
|
cookie_store = "0.22.0"
|
||||||
curve25519-dalek = "4.1.3"
|
|
||||||
der = { version = "0.7.9", features = ["derive", "pem"] }
|
der = { version = "0.7.9", features = ["derive", "pem"] }
|
||||||
digest = "0.10.7"
|
digest = "0.10.7"
|
||||||
divrem = "1.0.0"
|
divrem = "1.0.0"
|
||||||
@@ -216,7 +194,6 @@ rpassword = "7.2.0"
|
|||||||
rust-argon2 = "3.0.0"
|
rust-argon2 = "3.0.0"
|
||||||
rust-i18n = "3.1.5"
|
rust-i18n = "3.1.5"
|
||||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" }
|
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" }
|
||||||
safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
|
|
||||||
semver = { version = "1.0.20", features = ["serde"] }
|
semver = { version = "1.0.20", features = ["serde"] }
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
serde_cbor = { package = "ciborium", version = "0.2.1" }
|
serde_cbor = { package = "ciborium", version = "0.2.1" }
|
||||||
@@ -244,23 +221,6 @@ tokio-stream = { version = "0.1.14", features = ["io-util", "net", "sync"] }
|
|||||||
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
|
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
|
||||||
tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
|
tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
|
||||||
tokio-util = { version = "0.7.9", features = ["io"] }
|
tokio-util = { version = "0.7.9", features = ["io"] }
|
||||||
tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
|
|
||||||
tor-hscrypto = { version = "0.33", features = [
|
|
||||||
"full",
|
|
||||||
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
|
|
||||||
tor-hsservice = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
|
|
||||||
tor-keymgr = { version = "0.33", features = [
|
|
||||||
"ephemeral-keystore",
|
|
||||||
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
|
|
||||||
tor-llcrypto = { version = "0.33", features = [
|
|
||||||
"full",
|
|
||||||
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
|
|
||||||
tor-proto = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
|
|
||||||
tor-rtcompat = { version = "0.33", features = [
|
|
||||||
"rustls",
|
|
||||||
"tokio",
|
|
||||||
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
|
|
||||||
torut = "0.2.1"
|
|
||||||
tower-service = "0.3.3"
|
tower-service = "0.3.3"
|
||||||
tracing = "0.1.39"
|
tracing = "0.1.39"
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use openssl::x509::X509;
|
|||||||
use crate::db::model::DatabaseModel;
|
use crate::db::model::DatabaseModel;
|
||||||
use crate::hostname::{Hostname, generate_hostname, generate_id};
|
use crate::hostname::{Hostname, generate_hostname, generate_id};
|
||||||
use crate::net::ssl::{gen_nistp256, make_root_cert};
|
use crate::net::ssl::{gen_nistp256, make_root_cert};
|
||||||
use crate::net::tor::TorSecretKey;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::serde::Pem;
|
use crate::util::serde::Pem;
|
||||||
|
|
||||||
@@ -26,7 +25,6 @@ pub struct AccountInfo {
|
|||||||
pub server_id: String,
|
pub server_id: String,
|
||||||
pub hostname: Hostname,
|
pub hostname: Hostname,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub tor_keys: Vec<TorSecretKey>,
|
|
||||||
pub root_ca_key: PKey<Private>,
|
pub root_ca_key: PKey<Private>,
|
||||||
pub root_ca_cert: X509,
|
pub root_ca_cert: X509,
|
||||||
pub ssh_key: ssh_key::PrivateKey,
|
pub ssh_key: ssh_key::PrivateKey,
|
||||||
@@ -36,7 +34,6 @@ impl AccountInfo {
|
|||||||
pub fn new(password: &str, start_time: SystemTime) -> Result<Self, Error> {
|
pub fn new(password: &str, start_time: SystemTime) -> Result<Self, Error> {
|
||||||
let server_id = generate_id();
|
let server_id = generate_id();
|
||||||
let hostname = generate_hostname();
|
let hostname = generate_hostname();
|
||||||
let tor_key = vec![TorSecretKey::generate()];
|
|
||||||
let root_ca_key = gen_nistp256()?;
|
let root_ca_key = gen_nistp256()?;
|
||||||
let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?;
|
let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?;
|
||||||
let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random(
|
let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random(
|
||||||
@@ -48,7 +45,6 @@ impl AccountInfo {
|
|||||||
server_id,
|
server_id,
|
||||||
hostname,
|
hostname,
|
||||||
password: hash_password(password)?,
|
password: hash_password(password)?,
|
||||||
tor_keys: tor_key,
|
|
||||||
root_ca_key,
|
root_ca_key,
|
||||||
root_ca_cert,
|
root_ca_cert,
|
||||||
ssh_key,
|
ssh_key,
|
||||||
@@ -61,17 +57,6 @@ impl AccountInfo {
|
|||||||
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
|
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
|
||||||
let password = db.as_private().as_password().de()?;
|
let password = db.as_private().as_password().de()?;
|
||||||
let key_store = db.as_private().as_key_store();
|
let key_store = db.as_private().as_key_store();
|
||||||
let tor_addrs = db
|
|
||||||
.as_public()
|
|
||||||
.as_server_info()
|
|
||||||
.as_network()
|
|
||||||
.as_host()
|
|
||||||
.as_onions()
|
|
||||||
.de()?;
|
|
||||||
let tor_keys = tor_addrs
|
|
||||||
.into_iter()
|
|
||||||
.map(|tor_addr| key_store.as_onion().get_key(&tor_addr))
|
|
||||||
.collect::<Result<_, _>>()?;
|
|
||||||
let cert_store = key_store.as_local_certs();
|
let cert_store = key_store.as_local_certs();
|
||||||
let root_ca_key = cert_store.as_root_key().de()?.0;
|
let root_ca_key = cert_store.as_root_key().de()?.0;
|
||||||
let root_ca_cert = cert_store.as_root_cert().de()?.0;
|
let root_ca_cert = cert_store.as_root_cert().de()?.0;
|
||||||
@@ -82,7 +67,6 @@ impl AccountInfo {
|
|||||||
server_id,
|
server_id,
|
||||||
hostname,
|
hostname,
|
||||||
password,
|
password,
|
||||||
tor_keys,
|
|
||||||
root_ca_key,
|
root_ca_key,
|
||||||
root_ca_cert,
|
root_ca_cert,
|
||||||
ssh_key,
|
ssh_key,
|
||||||
@@ -97,17 +81,6 @@ impl AccountInfo {
|
|||||||
server_info
|
server_info
|
||||||
.as_pubkey_mut()
|
.as_pubkey_mut()
|
||||||
.ser(&self.ssh_key.public_key().to_openssh()?)?;
|
.ser(&self.ssh_key.public_key().to_openssh()?)?;
|
||||||
server_info
|
|
||||||
.as_network_mut()
|
|
||||||
.as_host_mut()
|
|
||||||
.as_onions_mut()
|
|
||||||
.ser(
|
|
||||||
&self
|
|
||||||
.tor_keys
|
|
||||||
.iter()
|
|
||||||
.map(|tor_key| tor_key.onion_address())
|
|
||||||
.collect(),
|
|
||||||
)?;
|
|
||||||
server_info.as_password_hash_mut().ser(&self.password)?;
|
server_info.as_password_hash_mut().ser(&self.password)?;
|
||||||
db.as_private_mut().as_password_mut().ser(&self.password)?;
|
db.as_private_mut().as_password_mut().ser(&self.password)?;
|
||||||
db.as_private_mut()
|
db.as_private_mut()
|
||||||
@@ -117,9 +90,6 @@ impl AccountInfo {
|
|||||||
.as_developer_key_mut()
|
.as_developer_key_mut()
|
||||||
.ser(Pem::new_ref(&self.developer_key))?;
|
.ser(Pem::new_ref(&self.developer_key))?;
|
||||||
let key_store = db.as_private_mut().as_key_store_mut();
|
let key_store = db.as_private_mut().as_key_store_mut();
|
||||||
for tor_key in &self.tor_keys {
|
|
||||||
key_store.as_onion_mut().insert_key(tor_key)?;
|
|
||||||
}
|
|
||||||
let cert_store = key_store.as_local_certs_mut();
|
let cert_store = key_store.as_local_certs_mut();
|
||||||
if cert_store.as_root_cert().de()?.0 != self.root_ca_cert {
|
if cert_store.as_root_cert().de()?.0 != self.root_ca_cert {
|
||||||
cert_store
|
cert_store
|
||||||
@@ -148,11 +118,5 @@ impl AccountInfo {
|
|||||||
self.hostname.no_dot_host_name(),
|
self.hostname.no_dot_host_name(),
|
||||||
self.hostname.local_domain_name(),
|
self.hostname.local_domain_name(),
|
||||||
]
|
]
|
||||||
.into_iter()
|
|
||||||
.chain(
|
|
||||||
self.tor_keys
|
|
||||||
.iter()
|
|
||||||
.map(|k| InternedString::from_display(&k.onion_address())),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ use ssh_key::private::Ed25519Keypair;
|
|||||||
|
|
||||||
use crate::account::AccountInfo;
|
use crate::account::AccountInfo;
|
||||||
use crate::hostname::{Hostname, generate_hostname, generate_id};
|
use crate::hostname::{Hostname, generate_hostname, generate_id};
|
||||||
use crate::net::tor::TorSecretKey;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::crypto::ed25519_expand_key;
|
|
||||||
use crate::util::serde::{Base32, Base64, Pem};
|
use crate::util::serde::{Base32, Base64, Pem};
|
||||||
|
|
||||||
pub struct OsBackup {
|
pub struct OsBackup {
|
||||||
@@ -85,10 +83,6 @@ impl OsBackupV0 {
|
|||||||
&mut ssh_key::rand_core::OsRng::default(),
|
&mut ssh_key::rand_core::OsRng::default(),
|
||||||
ssh_key::Algorithm::Ed25519,
|
ssh_key::Algorithm::Ed25519,
|
||||||
)?,
|
)?,
|
||||||
tor_keys: TorSecretKey::from_bytes(self.tor_key.0)
|
|
||||||
.ok()
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
developer_key: ed25519_dalek::SigningKey::generate(
|
developer_key: ed25519_dalek::SigningKey::generate(
|
||||||
&mut ssh_key::rand_core::OsRng::default(),
|
&mut ssh_key::rand_core::OsRng::default(),
|
||||||
),
|
),
|
||||||
@@ -119,10 +113,6 @@ impl OsBackupV1 {
|
|||||||
root_ca_key: self.root_ca_key.0,
|
root_ca_key: self.root_ca_key.0,
|
||||||
root_ca_cert: self.root_ca_cert.0,
|
root_ca_cert: self.root_ca_cert.0,
|
||||||
ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.0)),
|
ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.0)),
|
||||||
tor_keys: TorSecretKey::from_bytes(ed25519_expand_key(&self.net_key.0))
|
|
||||||
.ok()
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
developer_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key),
|
developer_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key),
|
||||||
},
|
},
|
||||||
ui: self.ui,
|
ui: self.ui,
|
||||||
@@ -140,7 +130,6 @@ struct OsBackupV2 {
|
|||||||
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
|
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
|
||||||
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
|
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
|
||||||
ssh_key: Pem<ssh_key::PrivateKey>, // PEM Encoded OpenSSH Key
|
ssh_key: Pem<ssh_key::PrivateKey>, // PEM Encoded OpenSSH Key
|
||||||
tor_keys: Vec<TorSecretKey>, // Base64 Encoded Ed25519 Expanded Secret Key
|
|
||||||
compat_s9pk_key: Pem<ed25519_dalek::SigningKey>, // PEM Encoded ED25519 Key
|
compat_s9pk_key: Pem<ed25519_dalek::SigningKey>, // PEM Encoded ED25519 Key
|
||||||
ui: Value, // JSON Value
|
ui: Value, // JSON Value
|
||||||
}
|
}
|
||||||
@@ -154,7 +143,6 @@ impl OsBackupV2 {
|
|||||||
root_ca_key: self.root_ca_key.0,
|
root_ca_key: self.root_ca_key.0,
|
||||||
root_ca_cert: self.root_ca_cert.0,
|
root_ca_cert: self.root_ca_cert.0,
|
||||||
ssh_key: self.ssh_key.0,
|
ssh_key: self.ssh_key.0,
|
||||||
tor_keys: self.tor_keys,
|
|
||||||
developer_key: self.compat_s9pk_key.0,
|
developer_key: self.compat_s9pk_key.0,
|
||||||
},
|
},
|
||||||
ui: self.ui,
|
ui: self.ui,
|
||||||
@@ -167,7 +155,6 @@ impl OsBackupV2 {
|
|||||||
root_ca_key: Pem(backup.account.root_ca_key.clone()),
|
root_ca_key: Pem(backup.account.root_ca_key.clone()),
|
||||||
root_ca_cert: Pem(backup.account.root_ca_cert.clone()),
|
root_ca_cert: Pem(backup.account.root_ca_cert.clone()),
|
||||||
ssh_key: Pem(backup.account.ssh_key.clone()),
|
ssh_key: Pem(backup.account.ssh_key.clone()),
|
||||||
tor_keys: backup.account.tor_keys.clone(),
|
|
||||||
compat_s9pk_key: Pem(backup.account.developer_key.clone()),
|
compat_s9pk_key: Pem(backup.account.developer_key.clone()),
|
||||||
ui: backup.ui.clone(),
|
ui: backup.ui.clone(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::disk::fsck::RepairStrategy;
|
|||||||
use crate::disk::main::DEFAULT_PASSWORD;
|
use crate::disk::main::DEFAULT_PASSWORD;
|
||||||
use crate::firmware::{check_for_firmware_update, update_firmware};
|
use crate::firmware::{check_for_firmware_update, update_firmware};
|
||||||
use crate::init::{InitPhases, STANDBY_MODE_PATH};
|
use crate::init::{InitPhases, STANDBY_MODE_PATH};
|
||||||
use crate::net::gateway::UpgradableListener;
|
use crate::net::gateway::WildcardListener;
|
||||||
use crate::net::web_server::WebServer;
|
use crate::net::web_server::WebServer;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::progress::FullProgressTracker;
|
use crate::progress::FullProgressTracker;
|
||||||
@@ -19,7 +19,7 @@ use crate::{DATA_DIR, PLATFORM};
|
|||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn setup_or_init(
|
async fn setup_or_init(
|
||||||
server: &mut WebServer<UpgradableListener>,
|
server: &mut WebServer<WildcardListener>,
|
||||||
config: &ServerConfig,
|
config: &ServerConfig,
|
||||||
) -> Result<Result<(RpcContext, FullProgressTracker), Shutdown>, Error> {
|
) -> Result<Result<(RpcContext, FullProgressTracker), Shutdown>, Error> {
|
||||||
if let Some(firmware) = check_for_firmware_update()
|
if let Some(firmware) = check_for_firmware_update()
|
||||||
@@ -204,7 +204,7 @@ async fn setup_or_init(
|
|||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn main(
|
pub async fn main(
|
||||||
server: &mut WebServer<UpgradableListener>,
|
server: &mut WebServer<WildcardListener>,
|
||||||
config: &ServerConfig,
|
config: &ServerConfig,
|
||||||
) -> Result<Result<(RpcContext, FullProgressTracker), Shutdown>, Error> {
|
) -> Result<Result<(RpcContext, FullProgressTracker), Shutdown>, Error> {
|
||||||
if &*PLATFORM == "raspberrypi" && tokio::fs::metadata(STANDBY_MODE_PATH).await.is_ok() {
|
if &*PLATFORM == "raspberrypi" && tokio::fs::metadata(STANDBY_MODE_PATH).await.is_ok() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use tracing::instrument;
|
|||||||
use crate::context::config::ServerConfig;
|
use crate::context::config::ServerConfig;
|
||||||
use crate::context::rpc::InitRpcContextPhases;
|
use crate::context::rpc::InitRpcContextPhases;
|
||||||
use crate::context::{DiagnosticContext, InitContext, RpcContext};
|
use crate::context::{DiagnosticContext, InitContext, RpcContext};
|
||||||
use crate::net::gateway::{BindTcp, SelfContainedNetworkInterfaceListener, UpgradableListener};
|
use crate::net::gateway::WildcardListener;
|
||||||
use crate::net::static_server::refresher;
|
use crate::net::static_server::refresher;
|
||||||
use crate::net::web_server::{Acceptor, WebServer};
|
use crate::net::web_server::{Acceptor, WebServer};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -23,7 +23,7 @@ use crate::util::logger::LOGGER;
|
|||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn inner_main(
|
async fn inner_main(
|
||||||
server: &mut WebServer<UpgradableListener>,
|
server: &mut WebServer<WildcardListener>,
|
||||||
config: &ServerConfig,
|
config: &ServerConfig,
|
||||||
) -> Result<Option<Shutdown>, Error> {
|
) -> Result<Option<Shutdown>, Error> {
|
||||||
let rpc_ctx = if !tokio::fs::metadata("/run/startos/initialized")
|
let rpc_ctx = if !tokio::fs::metadata("/run/startos/initialized")
|
||||||
@@ -148,7 +148,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
|
|||||||
.expect(&t!("bins.startd.failed-to-initialize-runtime"));
|
.expect(&t!("bins.startd.failed-to-initialize-runtime"));
|
||||||
let res = rt.block_on(async {
|
let res = rt.block_on(async {
|
||||||
let mut server = WebServer::new(
|
let mut server = WebServer::new(
|
||||||
Acceptor::bind_upgradable(SelfContainedNetworkInterfaceListener::bind(BindTcp, 80)),
|
Acceptor::new(WildcardListener::new(80)?),
|
||||||
refresher(),
|
refresher(),
|
||||||
);
|
);
|
||||||
match inner_main(&mut server, &config).await {
|
match inner_main(&mut server, &config).await {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use visit_rs::Visit;
|
|||||||
|
|
||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use crate::context::config::ClientConfig;
|
use crate::context::config::ClientConfig;
|
||||||
use crate::net::gateway::{Bind, BindTcp};
|
use tokio::net::TcpListener;
|
||||||
use crate::net::tls::TlsListener;
|
use crate::net::tls::TlsListener;
|
||||||
use crate::net::web_server::{Accept, Acceptor, MetadataVisitor, WebServer};
|
use crate::net::web_server::{Accept, Acceptor, MetadataVisitor, WebServer};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -57,7 +57,12 @@ async fn inner_main(config: &TunnelConfig) -> Result<(), Error> {
|
|||||||
if !a.contains_key(&key) {
|
if !a.contains_key(&key) {
|
||||||
match (|| {
|
match (|| {
|
||||||
Ok::<_, Error>(TlsListener::new(
|
Ok::<_, Error>(TlsListener::new(
|
||||||
BindTcp.bind(addr)?,
|
TcpListener::from_std(
|
||||||
|
mio::net::TcpListener::bind(addr)
|
||||||
|
.with_kind(ErrorKind::Network)?
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.with_kind(ErrorKind::Network)?,
|
||||||
TunnelCertHandler {
|
TunnelCertHandler {
|
||||||
db: https_db.clone(),
|
db: https_db.clone(),
|
||||||
crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()),
|
crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ use crate::disk::mount::guard::MountGuard;
|
|||||||
use crate::init::{InitResult, check_time_is_synchronized};
|
use crate::init::{InitResult, check_time_is_synchronized};
|
||||||
use crate::install::PKG_ARCHIVE_DIR;
|
use crate::install::PKG_ARCHIVE_DIR;
|
||||||
use crate::lxc::LxcManager;
|
use crate::lxc::LxcManager;
|
||||||
use crate::net::gateway::UpgradableListener;
|
use crate::net::gateway::WildcardListener;
|
||||||
use crate::net::net_controller::{NetController, NetService};
|
use crate::net::net_controller::{NetController, NetService};
|
||||||
use crate::net::socks::DEFAULT_SOCKS_LISTEN;
|
use crate::net::socks::DEFAULT_SOCKS_LISTEN;
|
||||||
use crate::net::utils::{find_eth_iface, find_wifi_iface};
|
use crate::net::utils::{find_eth_iface, find_wifi_iface};
|
||||||
@@ -132,7 +132,7 @@ pub struct RpcContext(Arc<RpcContextSeed>);
|
|||||||
impl RpcContext {
|
impl RpcContext {
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn init(
|
pub async fn init(
|
||||||
webserver: &WebServerAcceptorSetter<UpgradableListener>,
|
webserver: &WebServerAcceptorSetter<WildcardListener>,
|
||||||
config: &ServerConfig,
|
config: &ServerConfig,
|
||||||
disk_guid: InternedString,
|
disk_guid: InternedString,
|
||||||
init_result: Option<InitResult>,
|
init_result: Option<InitResult>,
|
||||||
@@ -167,7 +167,7 @@ impl RpcContext {
|
|||||||
} else {
|
} else {
|
||||||
let net_ctrl =
|
let net_ctrl =
|
||||||
Arc::new(NetController::init(db.clone(), &account.hostname, socks_proxy).await?);
|
Arc::new(NetController::init(db.clone(), &account.hostname, socks_proxy).await?);
|
||||||
webserver.try_upgrade(|a| net_ctrl.net_iface.watcher.upgrade_listener(a))?;
|
webserver.send_modify(|wl| wl.set_ip_info(net_ctrl.net_iface.watcher.subscribe()));
|
||||||
let os_net_service = net_ctrl.os_bindings().await?;
|
let os_net_service = net_ctrl.os_bindings().await?;
|
||||||
(net_ctrl, os_net_service)
|
(net_ctrl, os_net_service)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use crate::context::RpcContext;
|
|||||||
use crate::context::config::ServerConfig;
|
use crate::context::config::ServerConfig;
|
||||||
use crate::disk::mount::guard::{MountGuard, TmpMountGuard};
|
use crate::disk::mount::guard::{MountGuard, TmpMountGuard};
|
||||||
use crate::hostname::Hostname;
|
use crate::hostname::Hostname;
|
||||||
use crate::net::gateway::UpgradableListener;
|
use crate::net::gateway::WildcardListener;
|
||||||
use crate::net::web_server::{WebServer, WebServerAcceptorSetter};
|
use crate::net::web_server::{WebServer, WebServerAcceptorSetter};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::progress::FullProgressTracker;
|
use crate::progress::FullProgressTracker;
|
||||||
@@ -51,7 +51,7 @@ pub struct SetupResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct SetupContextSeed {
|
pub struct SetupContextSeed {
|
||||||
pub webserver: WebServerAcceptorSetter<UpgradableListener>,
|
pub webserver: WebServerAcceptorSetter<WildcardListener>,
|
||||||
pub config: SyncMutex<ServerConfig>,
|
pub config: SyncMutex<ServerConfig>,
|
||||||
pub disable_encryption: bool,
|
pub disable_encryption: bool,
|
||||||
pub progress: FullProgressTracker,
|
pub progress: FullProgressTracker,
|
||||||
@@ -70,7 +70,7 @@ pub struct SetupContext(Arc<SetupContextSeed>);
|
|||||||
impl SetupContext {
|
impl SetupContext {
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub fn init(
|
pub fn init(
|
||||||
webserver: &WebServer<UpgradableListener>,
|
webserver: &WebServer<WildcardListener>,
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::s9pk::manifest::{LocaleString, Manifest};
|
|||||||
use crate::status::StatusInfo;
|
use crate::status::StatusInfo;
|
||||||
use crate::util::DataUrl;
|
use crate::util::DataUrl;
|
||||||
use crate::util::serde::{Pem, is_partial_of};
|
use crate::util::serde::{Pem, is_partial_of};
|
||||||
use crate::{ActionId, HealthCheckId, HostId, PackageId, ReplayId, ServiceInterfaceId};
|
use crate::{ActionId, GatewayId, HealthCheckId, HostId, PackageId, ReplayId, ServiceInterfaceId};
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize, TS)]
|
#[derive(Debug, Default, Deserialize, Serialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
@@ -381,6 +381,9 @@ pub struct PackageDataEntry {
|
|||||||
pub hosts: Hosts,
|
pub hosts: Hosts,
|
||||||
#[ts(type = "string[]")]
|
#[ts(type = "string[]")]
|
||||||
pub store_exposed_dependents: Vec<JsonPointer>,
|
pub store_exposed_dependents: Vec<JsonPointer>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[ts(type = "string | null")]
|
||||||
|
pub outbound_gateway: Option<GatewayId>,
|
||||||
}
|
}
|
||||||
impl AsRef<PackageDataEntry> for PackageDataEntry {
|
impl AsRef<PackageDataEntry> for PackageDataEntry {
|
||||||
fn as_ref(&self) -> &PackageDataEntry {
|
fn as_ref(&self) -> &PackageDataEntry {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use crate::db::model::Database;
|
|||||||
use crate::db::model::package::AllPackageData;
|
use crate::db::model::package::AllPackageData;
|
||||||
use crate::net::acme::AcmeProvider;
|
use crate::net::acme::AcmeProvider;
|
||||||
use crate::net::host::Host;
|
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::utils::ipv6_is_local;
|
||||||
use crate::net::vhost::AlpnInfo;
|
use crate::net::vhost::AlpnInfo;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -63,36 +63,35 @@ impl Public {
|
|||||||
post_init_migration_todos: BTreeMap::new(),
|
post_init_migration_todos: BTreeMap::new(),
|
||||||
network: NetworkInfo {
|
network: NetworkInfo {
|
||||||
host: Host {
|
host: Host {
|
||||||
bindings: [(
|
bindings: Bindings(
|
||||||
80,
|
[(
|
||||||
BindInfo {
|
80,
|
||||||
enabled: false,
|
BindInfo {
|
||||||
options: BindOptions {
|
enabled: false,
|
||||||
preferred_external_port: 80,
|
options: BindOptions {
|
||||||
add_ssl: Some(AddSslOptions {
|
preferred_external_port: 80,
|
||||||
preferred_external_port: 443,
|
add_ssl: Some(AddSslOptions {
|
||||||
add_x_forwarded_headers: false,
|
preferred_external_port: 443,
|
||||||
alpn: Some(AlpnInfo::Specified(vec![
|
add_x_forwarded_headers: false,
|
||||||
MaybeUtf8String("h2".into()),
|
alpn: Some(AlpnInfo::Specified(vec![
|
||||||
MaybeUtf8String("http/1.1".into()),
|
MaybeUtf8String("h2".into()),
|
||||||
])),
|
MaybeUtf8String("http/1.1".into()),
|
||||||
}),
|
])),
|
||||||
secure: None,
|
}),
|
||||||
|
secure: None,
|
||||||
|
},
|
||||||
|
net: NetInfo {
|
||||||
|
assigned_port: None,
|
||||||
|
assigned_ssl_port: Some(443),
|
||||||
|
},
|
||||||
|
addresses: DerivedAddressInfo::default(),
|
||||||
},
|
},
|
||||||
net: NetInfo {
|
)]
|
||||||
assigned_port: None,
|
.into_iter()
|
||||||
assigned_ssl_port: Some(443),
|
.collect(),
|
||||||
private_disabled: OrdSet::new(),
|
),
|
||||||
public_enabled: OrdSet::new(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
onions: account.tor_keys.iter().map(|k| k.onion_address()).collect(),
|
|
||||||
public_domains: BTreeMap::new(),
|
public_domains: BTreeMap::new(),
|
||||||
private_domains: BTreeSet::new(),
|
private_domains: BTreeSet::new(),
|
||||||
hostname_info: BTreeMap::new(),
|
|
||||||
},
|
},
|
||||||
wifi: WifiInfo {
|
wifi: WifiInfo {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -117,6 +116,7 @@ impl Public {
|
|||||||
acme
|
acme
|
||||||
},
|
},
|
||||||
dns: Default::default(),
|
dns: Default::default(),
|
||||||
|
default_outbound: None,
|
||||||
},
|
},
|
||||||
status_info: ServerStatus {
|
status_info: ServerStatus {
|
||||||
backup_progress: None,
|
backup_progress: None,
|
||||||
@@ -220,6 +220,9 @@ pub struct NetworkInfo {
|
|||||||
pub acme: BTreeMap<AcmeProvider, AcmeSettings>,
|
pub acme: BTreeMap<AcmeProvider, AcmeSettings>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dns: DnsSettings,
|
pub dns: DnsSettings,
|
||||||
|
#[serde(default)]
|
||||||
|
#[ts(type = "string | null")]
|
||||||
|
pub default_outbound: Option<GatewayId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||||
@@ -239,39 +242,42 @@ pub struct DnsSettings {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct NetworkInterfaceInfo {
|
pub struct NetworkInterfaceInfo {
|
||||||
pub name: Option<InternedString>,
|
pub name: Option<InternedString>,
|
||||||
|
#[ts(skip)]
|
||||||
pub public: Option<bool>,
|
pub public: Option<bool>,
|
||||||
pub secure: Option<bool>,
|
pub secure: Option<bool>,
|
||||||
pub ip_info: Option<Arc<IpInfo>>,
|
pub ip_info: Option<Arc<IpInfo>>,
|
||||||
|
#[serde(default, rename = "type")]
|
||||||
|
pub gateway_type: Option<GatewayType>,
|
||||||
}
|
}
|
||||||
impl NetworkInterfaceInfo {
|
impl NetworkInterfaceInfo {
|
||||||
pub fn public(&self) -> bool {
|
pub fn public(&self) -> bool {
|
||||||
self.public.unwrap_or_else(|| {
|
self.public.unwrap_or_else(|| {
|
||||||
!self.ip_info.as_ref().map_or(true, |ip_info| {
|
!self.ip_info.as_ref().map_or(true, |ip_info| {
|
||||||
let ip4s = ip_info
|
let ip4s = ip_info
|
||||||
.subnets
|
.subnets
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|ipnet| {
|
.filter_map(|ipnet| {
|
||||||
if let IpAddr::V4(ip4) = ipnet.addr() {
|
if let IpAddr::V4(ip4) = ipnet.addr() {
|
||||||
Some(ip4)
|
Some(ip4)
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
if !ip4s.is_empty() {
|
|
||||||
return ip4s
|
|
||||||
.iter()
|
|
||||||
.all(|ip4| ip4.is_loopback() || ip4.is_private() || ip4.is_link_local());
|
|
||||||
}
|
|
||||||
ip_info.subnets.iter().all(|ipnet| {
|
|
||||||
if let IpAddr::V6(ip6) = ipnet.addr() {
|
|
||||||
ipv6_is_local(ip6)
|
|
||||||
} else {
|
} else {
|
||||||
true
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
if !ip4s.is_empty() {
|
||||||
|
return ip4s
|
||||||
|
.iter()
|
||||||
|
.all(|ip4| ip4.is_loopback() || ip4.is_private() || ip4.is_link_local());
|
||||||
|
}
|
||||||
|
ip_info.subnets.iter().all(|ipnet| {
|
||||||
|
if let IpAddr::V6(ip6) = ipnet.addr() {
|
||||||
|
ipv6_is_local(ip6)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn secure(&self) -> bool {
|
pub fn secure(&self) -> bool {
|
||||||
@@ -310,6 +316,15 @@ pub enum NetworkInterfaceType {
|
|||||||
Loopback,
|
Loopback,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, clap::ValueEnum)]
|
||||||
|
#[ts(export)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum GatewayType {
|
||||||
|
#[default]
|
||||||
|
InboundOutbound,
|
||||||
|
OutboundOnly,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[model = "Model<Self>"]
|
#[model = "Model<Self>"]
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ pub enum ErrorKind {
|
|||||||
ParseUrl = 19,
|
ParseUrl = 19,
|
||||||
DiskNotAvailable = 20,
|
DiskNotAvailable = 20,
|
||||||
BlockDevice = 21,
|
BlockDevice = 21,
|
||||||
InvalidOnionAddress = 22,
|
// InvalidOnionAddress = 22,
|
||||||
Pack = 23,
|
Pack = 23,
|
||||||
ValidateS9pk = 24,
|
ValidateS9pk = 24,
|
||||||
DiskCorrupted = 25, // Remove
|
DiskCorrupted = 25, // Remove
|
||||||
Tor = 26,
|
// Tor = 26,
|
||||||
ConfigGen = 27,
|
ConfigGen = 27,
|
||||||
ParseNumber = 28,
|
ParseNumber = 28,
|
||||||
Database = 29,
|
Database = 29,
|
||||||
@@ -126,11 +126,11 @@ impl ErrorKind {
|
|||||||
ParseUrl => t!("error.parse-url"),
|
ParseUrl => t!("error.parse-url"),
|
||||||
DiskNotAvailable => t!("error.disk-not-available"),
|
DiskNotAvailable => t!("error.disk-not-available"),
|
||||||
BlockDevice => t!("error.block-device"),
|
BlockDevice => t!("error.block-device"),
|
||||||
InvalidOnionAddress => t!("error.invalid-onion-address"),
|
// InvalidOnionAddress => t!("error.invalid-onion-address"),
|
||||||
Pack => t!("error.pack"),
|
Pack => t!("error.pack"),
|
||||||
ValidateS9pk => t!("error.validate-s9pk"),
|
ValidateS9pk => t!("error.validate-s9pk"),
|
||||||
DiskCorrupted => t!("error.disk-corrupted"), // Remove
|
DiskCorrupted => t!("error.disk-corrupted"), // Remove
|
||||||
Tor => t!("error.tor"),
|
// Tor => t!("error.tor"),
|
||||||
ConfigGen => t!("error.config-gen"),
|
ConfigGen => t!("error.config-gen"),
|
||||||
ParseNumber => t!("error.parse-number"),
|
ParseNumber => t!("error.parse-number"),
|
||||||
Database => t!("error.database"),
|
Database => t!("error.database"),
|
||||||
@@ -370,17 +370,6 @@ impl From<reqwest::Error> for Error {
|
|||||||
Error::new(e, kind)
|
Error::new(e, kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "arti")]
|
|
||||||
impl From<arti_client::Error> for Error {
|
|
||||||
fn from(e: arti_client::Error) -> Self {
|
|
||||||
Error::new(e, ErrorKind::Tor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<torut::control::ConnError> for Error {
|
|
||||||
fn from(e: torut::control::ConnError) -> Self {
|
|
||||||
Error::new(e, ErrorKind::Tor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<zbus::Error> for Error {
|
impl From<zbus::Error> for Error {
|
||||||
fn from(e: zbus::Error) -> Self {
|
fn from(e: zbus::Error) -> Self {
|
||||||
Error::new(e, ErrorKind::DBus)
|
Error::new(e, ErrorKind::DBus)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use crate::db::model::public::ServerStatus;
|
|||||||
use crate::developer::OS_DEVELOPER_KEY_PATH;
|
use crate::developer::OS_DEVELOPER_KEY_PATH;
|
||||||
use crate::hostname::Hostname;
|
use crate::hostname::Hostname;
|
||||||
use crate::middleware::auth::local::LocalAuthContext;
|
use crate::middleware::auth::local::LocalAuthContext;
|
||||||
use crate::net::gateway::UpgradableListener;
|
use crate::net::gateway::WildcardListener;
|
||||||
use crate::net::net_controller::{NetController, NetService};
|
use crate::net::net_controller::{NetController, NetService};
|
||||||
use crate::net::socks::DEFAULT_SOCKS_LISTEN;
|
use crate::net::socks::DEFAULT_SOCKS_LISTEN;
|
||||||
use crate::net::utils::find_wifi_iface;
|
use crate::net::utils::find_wifi_iface;
|
||||||
@@ -144,7 +144,7 @@ pub async fn run_script<P: AsRef<Path>>(path: P, mut progress: PhaseProgressTrac
|
|||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn init(
|
pub async fn init(
|
||||||
webserver: &WebServerAcceptorSetter<UpgradableListener>,
|
webserver: &WebServerAcceptorSetter<WildcardListener>,
|
||||||
cfg: &ServerConfig,
|
cfg: &ServerConfig,
|
||||||
InitPhases {
|
InitPhases {
|
||||||
preinit,
|
preinit,
|
||||||
@@ -218,7 +218,7 @@ pub async fn init(
|
|||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
webserver.try_upgrade(|a| net_ctrl.net_iface.watcher.upgrade_listener(a))?;
|
webserver.send_modify(|wl| wl.set_ip_info(net_ctrl.net_iface.watcher.subscribe()));
|
||||||
let os_net_service = net_ctrl.os_bindings().await?;
|
let os_net_service = net_ctrl.os_bindings().await?;
|
||||||
start_net.complete();
|
start_net.complete();
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use std::sync::{Arc, Weak};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use id_pool::IdPool;
|
|
||||||
use iddqd::{IdOrdItem, IdOrdMap};
|
use iddqd::{IdOrdItem, IdOrdMap};
|
||||||
|
use rand::Rng;
|
||||||
use imbl::OrdMap;
|
use imbl::OrdMap;
|
||||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -15,7 +15,6 @@ use tokio::sync::mpsc;
|
|||||||
use crate::GatewayId;
|
use crate::GatewayId;
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
use crate::db::model::public::NetworkInterfaceInfo;
|
use crate::db::model::public::NetworkInterfaceInfo;
|
||||||
use crate::net::gateway::{DynInterfaceFilter, InterfaceFilter};
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
use crate::util::future::NonDetachingJoinHandle;
|
use crate::util::future::NonDetachingJoinHandle;
|
||||||
@@ -23,25 +22,76 @@ use crate::util::serde::{HandlerExtSerde, display_serializable};
|
|||||||
use crate::util::sync::Watch;
|
use crate::util::sync::Watch;
|
||||||
|
|
||||||
pub const START9_BRIDGE_IFACE: &str = "lxcbr0";
|
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, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct ForwardRequirements {
|
||||||
|
pub public_gateways: BTreeSet<GatewayId>,
|
||||||
|
pub private_ips: BTreeSet<IpAddr>,
|
||||||
|
pub secure: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ForwardRequirements {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"ForwardRequirements {{ public: {:?}, private: {:?}, secure: {} }}",
|
||||||
|
self.public_gateways, self.private_ips, self.secure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Source-IP filter for private forwards: restricts traffic to a subnet
|
||||||
|
/// while excluding gateway/router IPs that may masquerade internet traffic.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct SourceFilter {
|
||||||
|
/// Network CIDR to allow (e.g. "192.168.1.0/24")
|
||||||
|
subnet: String,
|
||||||
|
/// Comma-separated gateway IPs to exclude (they may masquerade internet traffic)
|
||||||
|
excluded: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct AvailablePorts(IdPool);
|
pub struct AvailablePorts(BTreeMap<u16, bool>);
|
||||||
impl AvailablePorts {
|
impl AvailablePorts {
|
||||||
pub fn new() -> Self {
|
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> {
|
pub fn alloc(&mut self, ssl: bool) -> Result<u16, Error> {
|
||||||
self.0.request_id().ok_or_else(|| {
|
let mut rng = rand::rng();
|
||||||
Error::new(
|
for _ in 0..1000 {
|
||||||
eyre!("{}", t!("net.forward.no-dynamic-ports-available")),
|
let port = rng.random_range(EPHEMERAL_PORT_START..u16::MAX);
|
||||||
ErrorKind::Network,
|
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>) {
|
pub fn free(&mut self, ports: impl IntoIterator<Item = u16>) {
|
||||||
for port in ports {
|
for port in ports {
|
||||||
self.0.return_id(port).unwrap_or_default();
|
self.0.remove(&port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,10 +111,10 @@ pub fn forward_api<C: Context>() -> ParentHandler<C> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
table.add_row(row![bc => "FROM", "TO", "FILTER"]);
|
table.add_row(row![bc => "FROM", "TO", "REQS"]);
|
||||||
|
|
||||||
for (external, target) in res.0 {
|
for (external, target) in res.0 {
|
||||||
table.add_row(row![external, target.target, target.filter]);
|
table.add_row(row![external, target.target, target.reqs]);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.print_tty(false)?;
|
table.print_tty(false)?;
|
||||||
@@ -79,6 +129,7 @@ struct ForwardMapping {
|
|||||||
source: SocketAddrV4,
|
source: SocketAddrV4,
|
||||||
target: SocketAddrV4,
|
target: SocketAddrV4,
|
||||||
target_prefix: u8,
|
target_prefix: u8,
|
||||||
|
src_filter: Option<SourceFilter>,
|
||||||
rc: Weak<()>,
|
rc: Weak<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +144,10 @@ impl PortForwardState {
|
|||||||
source: SocketAddrV4,
|
source: SocketAddrV4,
|
||||||
target: SocketAddrV4,
|
target: SocketAddrV4,
|
||||||
target_prefix: u8,
|
target_prefix: u8,
|
||||||
|
src_filter: Option<SourceFilter>,
|
||||||
) -> Result<Arc<()>, Error> {
|
) -> Result<Arc<()>, Error> {
|
||||||
if let Some(existing) = self.mappings.get_mut(&source) {
|
if let Some(existing) = self.mappings.get_mut(&source) {
|
||||||
if existing.target == target {
|
if existing.target == target && existing.src_filter == src_filter {
|
||||||
if let Some(existing_rc) = existing.rc.upgrade() {
|
if let Some(existing_rc) = existing.rc.upgrade() {
|
||||||
return Ok(existing_rc);
|
return Ok(existing_rc);
|
||||||
} else {
|
} else {
|
||||||
@@ -104,21 +156,28 @@ impl PortForwardState {
|
|||||||
return Ok(rc);
|
return Ok(rc);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Different target, need to remove old and add new
|
// Different target or src_filter, need to remove old and add new
|
||||||
if let Some(mapping) = self.mappings.remove(&source) {
|
if let Some(mapping) = self.mappings.remove(&source) {
|
||||||
unforward(mapping.source, mapping.target, mapping.target_prefix).await?;
|
unforward(
|
||||||
|
mapping.source,
|
||||||
|
mapping.target,
|
||||||
|
mapping.target_prefix,
|
||||||
|
mapping.src_filter.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let rc = Arc::new(());
|
let rc = Arc::new(());
|
||||||
forward(source, target, target_prefix).await?;
|
forward(source, target, target_prefix, src_filter.as_ref()).await?;
|
||||||
self.mappings.insert(
|
self.mappings.insert(
|
||||||
source,
|
source,
|
||||||
ForwardMapping {
|
ForwardMapping {
|
||||||
source,
|
source,
|
||||||
target,
|
target,
|
||||||
target_prefix,
|
target_prefix,
|
||||||
|
src_filter,
|
||||||
rc: Arc::downgrade(&rc),
|
rc: Arc::downgrade(&rc),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -136,7 +195,13 @@ impl PortForwardState {
|
|||||||
|
|
||||||
for source in to_remove {
|
for source in to_remove {
|
||||||
if let Some(mapping) = self.mappings.remove(&source) {
|
if let Some(mapping) = self.mappings.remove(&source) {
|
||||||
unforward(mapping.source, mapping.target, mapping.target_prefix).await?;
|
unforward(
|
||||||
|
mapping.source,
|
||||||
|
mapping.target,
|
||||||
|
mapping.target_prefix,
|
||||||
|
mapping.src_filter.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -157,9 +222,14 @@ impl Drop for PortForwardState {
|
|||||||
let mappings = std::mem::take(&mut self.mappings);
|
let mappings = std::mem::take(&mut self.mappings);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
for (_, mapping) in mappings {
|
for (_, mapping) in mappings {
|
||||||
unforward(mapping.source, mapping.target, mapping.target_prefix)
|
unforward(
|
||||||
.await
|
mapping.source,
|
||||||
.log_err();
|
mapping.target,
|
||||||
|
mapping.target_prefix,
|
||||||
|
mapping.src_filter.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -171,6 +241,7 @@ enum PortForwardCommand {
|
|||||||
source: SocketAddrV4,
|
source: SocketAddrV4,
|
||||||
target: SocketAddrV4,
|
target: SocketAddrV4,
|
||||||
target_prefix: u8,
|
target_prefix: u8,
|
||||||
|
src_filter: Option<SourceFilter>,
|
||||||
respond: oneshot::Sender<Result<Arc<()>, Error>>,
|
respond: oneshot::Sender<Result<Arc<()>, Error>>,
|
||||||
},
|
},
|
||||||
Gc {
|
Gc {
|
||||||
@@ -257,9 +328,12 @@ impl PortForwardController {
|
|||||||
source,
|
source,
|
||||||
target,
|
target,
|
||||||
target_prefix,
|
target_prefix,
|
||||||
|
src_filter,
|
||||||
respond,
|
respond,
|
||||||
} => {
|
} => {
|
||||||
let result = state.add_forward(source, target, target_prefix).await;
|
let result = state
|
||||||
|
.add_forward(source, target, target_prefix, src_filter)
|
||||||
|
.await;
|
||||||
respond.send(result).ok();
|
respond.send(result).ok();
|
||||||
}
|
}
|
||||||
PortForwardCommand::Gc { respond } => {
|
PortForwardCommand::Gc { respond } => {
|
||||||
@@ -284,6 +358,7 @@ impl PortForwardController {
|
|||||||
source: SocketAddrV4,
|
source: SocketAddrV4,
|
||||||
target: SocketAddrV4,
|
target: SocketAddrV4,
|
||||||
target_prefix: u8,
|
target_prefix: u8,
|
||||||
|
src_filter: Option<SourceFilter>,
|
||||||
) -> Result<Arc<()>, Error> {
|
) -> Result<Arc<()>, Error> {
|
||||||
let (send, recv) = oneshot::channel();
|
let (send, recv) = oneshot::channel();
|
||||||
self.req
|
self.req
|
||||||
@@ -291,6 +366,7 @@ impl PortForwardController {
|
|||||||
source,
|
source,
|
||||||
target,
|
target,
|
||||||
target_prefix,
|
target_prefix,
|
||||||
|
src_filter,
|
||||||
respond: send,
|
respond: send,
|
||||||
})
|
})
|
||||||
.map_err(err_has_exited)?;
|
.map_err(err_has_exited)?;
|
||||||
@@ -321,14 +397,14 @@ struct InterfaceForwardRequest {
|
|||||||
external: u16,
|
external: u16,
|
||||||
target: SocketAddrV4,
|
target: SocketAddrV4,
|
||||||
target_prefix: u8,
|
target_prefix: u8,
|
||||||
filter: DynInterfaceFilter,
|
reqs: ForwardRequirements,
|
||||||
rc: Arc<()>,
|
rc: Arc<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct InterfaceForwardEntry {
|
struct InterfaceForwardEntry {
|
||||||
external: u16,
|
external: u16,
|
||||||
filter: BTreeMap<DynInterfaceFilter, (SocketAddrV4, u8, Weak<()>)>,
|
targets: BTreeMap<ForwardRequirements, (SocketAddrV4, u8, Weak<()>)>,
|
||||||
// Maps source SocketAddr -> strong reference for the forward created in PortForwardController
|
// Maps source SocketAddr -> strong reference for the forward created in PortForwardController
|
||||||
forwards: BTreeMap<SocketAddrV4, Arc<()>>,
|
forwards: BTreeMap<SocketAddrV4, Arc<()>>,
|
||||||
}
|
}
|
||||||
@@ -346,7 +422,7 @@ impl InterfaceForwardEntry {
|
|||||||
fn new(external: u16) -> Self {
|
fn new(external: u16) -> Self {
|
||||||
Self {
|
Self {
|
||||||
external,
|
external,
|
||||||
filter: BTreeMap::new(),
|
targets: BTreeMap::new(),
|
||||||
forwards: BTreeMap::new(),
|
forwards: BTreeMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,28 +434,50 @@ impl InterfaceForwardEntry {
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut keep = BTreeSet::<SocketAddrV4>::new();
|
let mut keep = BTreeSet::<SocketAddrV4>::new();
|
||||||
|
|
||||||
for (iface, info) in ip_info.iter() {
|
for (gw_id, info) in ip_info.iter() {
|
||||||
if let Some((target, target_prefix)) = self
|
if let Some(ip_info) = &info.ip_info {
|
||||||
.filter
|
for subnet in ip_info.subnets.iter() {
|
||||||
.iter()
|
if let IpAddr::V4(ip) = subnet.addr() {
|
||||||
.filter(|(_, (_, _, rc))| rc.strong_count() > 0)
|
let addr = SocketAddrV4::new(ip, self.external);
|
||||||
.find(|(filter, _)| filter.filter(iface, info))
|
if keep.contains(&addr) {
|
||||||
.map(|(_, (target, target_prefix, _))| (*target, *target_prefix))
|
continue;
|
||||||
{
|
|
||||||
if let Some(ip_info) = &info.ip_info {
|
|
||||||
for addr in ip_info.subnets.iter().filter_map(|net| {
|
|
||||||
if let IpAddr::V4(ip) = net.addr() {
|
|
||||||
Some(SocketAddrV4::new(ip, self.external))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
keep.insert(addr);
|
for (reqs, (target, target_prefix, rc)) in self.targets.iter() {
|
||||||
if !self.forwards.contains_key(&addr) {
|
if rc.strong_count() == 0 {
|
||||||
let rc = port_forward
|
continue;
|
||||||
.add_forward(addr, target, target_prefix)
|
}
|
||||||
|
if !reqs.secure && !info.secure() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let src_filter =
|
||||||
|
if reqs.public_gateways.contains(gw_id) {
|
||||||
|
None
|
||||||
|
} else if reqs.private_ips.contains(&IpAddr::V4(ip)) {
|
||||||
|
let excluded = ip_info
|
||||||
|
.lan_ip
|
||||||
|
.iter()
|
||||||
|
.filter_map(|ip| match ip {
|
||||||
|
IpAddr::V4(v4) => Some(v4.to_string()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
|
Some(SourceFilter {
|
||||||
|
subnet: subnet.trunc().to_string(),
|
||||||
|
excluded,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
keep.insert(addr);
|
||||||
|
let fwd_rc = port_forward
|
||||||
|
.add_forward(addr, *target, *target_prefix, src_filter)
|
||||||
.await?;
|
.await?;
|
||||||
self.forwards.insert(addr, rc);
|
self.forwards.insert(addr, fwd_rc);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,7 +496,7 @@ impl InterfaceForwardEntry {
|
|||||||
external,
|
external,
|
||||||
target,
|
target,
|
||||||
target_prefix,
|
target_prefix,
|
||||||
filter,
|
reqs,
|
||||||
mut rc,
|
mut rc,
|
||||||
}: InterfaceForwardRequest,
|
}: InterfaceForwardRequest,
|
||||||
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
||||||
@@ -412,8 +510,8 @@ impl InterfaceForwardEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let entry = self
|
let entry = self
|
||||||
.filter
|
.targets
|
||||||
.entry(filter)
|
.entry(reqs)
|
||||||
.or_insert_with(|| (target, target_prefix, Arc::downgrade(&rc)));
|
.or_insert_with(|| (target, target_prefix, Arc::downgrade(&rc)));
|
||||||
if entry.0 != target {
|
if entry.0 != target {
|
||||||
entry.0 = target;
|
entry.0 = target;
|
||||||
@@ -436,7 +534,7 @@ impl InterfaceForwardEntry {
|
|||||||
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
||||||
port_forward: &PortForwardController,
|
port_forward: &PortForwardController,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.filter.retain(|_, (_, _, rc)| rc.strong_count() > 0);
|
self.targets.retain(|_, (_, _, rc)| rc.strong_count() > 0);
|
||||||
|
|
||||||
self.update(ip_info, port_forward).await
|
self.update(ip_info, port_forward).await
|
||||||
}
|
}
|
||||||
@@ -495,7 +593,7 @@ pub struct ForwardTable(pub BTreeMap<u16, ForwardTarget>);
|
|||||||
pub struct ForwardTarget {
|
pub struct ForwardTarget {
|
||||||
pub target: SocketAddrV4,
|
pub target: SocketAddrV4,
|
||||||
pub target_prefix: u8,
|
pub target_prefix: u8,
|
||||||
pub filter: String,
|
pub reqs: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&InterfaceForwardState> for ForwardTable {
|
impl From<&InterfaceForwardState> for ForwardTable {
|
||||||
@@ -506,16 +604,16 @@ impl From<&InterfaceForwardState> for ForwardTable {
|
|||||||
.iter()
|
.iter()
|
||||||
.flat_map(|entry| {
|
.flat_map(|entry| {
|
||||||
entry
|
entry
|
||||||
.filter
|
.targets
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, (_, _, rc))| rc.strong_count() > 0)
|
.filter(|(_, (_, _, rc))| rc.strong_count() > 0)
|
||||||
.map(|(filter, (target, target_prefix, _))| {
|
.map(|(reqs, (target, target_prefix, _))| {
|
||||||
(
|
(
|
||||||
entry.external,
|
entry.external,
|
||||||
ForwardTarget {
|
ForwardTarget {
|
||||||
target: *target,
|
target: *target,
|
||||||
target_prefix: *target_prefix,
|
target_prefix: *target_prefix,
|
||||||
filter: format!("{:#?}", filter),
|
reqs: format!("{reqs}"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -534,16 +632,6 @@ enum InterfaceForwardCommand {
|
|||||||
DumpTable(oneshot::Sender<ForwardTable>),
|
DumpTable(oneshot::Sender<ForwardTable>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
use crate::net::gateway::SecureFilter;
|
|
||||||
|
|
||||||
assert_ne!(
|
|
||||||
false.into_dyn(),
|
|
||||||
SecureFilter { secure: false }.into_dyn().into_dyn()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct InterfacePortForwardController {
|
pub struct InterfacePortForwardController {
|
||||||
req: mpsc::UnboundedSender<InterfaceForwardCommand>,
|
req: mpsc::UnboundedSender<InterfaceForwardCommand>,
|
||||||
_thread: NonDetachingJoinHandle<()>,
|
_thread: NonDetachingJoinHandle<()>,
|
||||||
@@ -593,7 +681,7 @@ impl InterfacePortForwardController {
|
|||||||
pub async fn add(
|
pub async fn add(
|
||||||
&self,
|
&self,
|
||||||
external: u16,
|
external: u16,
|
||||||
filter: DynInterfaceFilter,
|
reqs: ForwardRequirements,
|
||||||
target: SocketAddrV4,
|
target: SocketAddrV4,
|
||||||
target_prefix: u8,
|
target_prefix: u8,
|
||||||
) -> Result<Arc<()>, Error> {
|
) -> Result<Arc<()>, Error> {
|
||||||
@@ -605,7 +693,7 @@ impl InterfacePortForwardController {
|
|||||||
external,
|
external,
|
||||||
target,
|
target,
|
||||||
target_prefix,
|
target_prefix,
|
||||||
filter,
|
reqs,
|
||||||
rc,
|
rc,
|
||||||
},
|
},
|
||||||
send,
|
send,
|
||||||
@@ -637,15 +725,21 @@ async fn forward(
|
|||||||
source: SocketAddrV4,
|
source: SocketAddrV4,
|
||||||
target: SocketAddrV4,
|
target: SocketAddrV4,
|
||||||
target_prefix: u8,
|
target_prefix: u8,
|
||||||
|
src_filter: Option<&SourceFilter>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
Command::new("/usr/lib/startos/scripts/forward-port")
|
let mut cmd = Command::new("/usr/lib/startos/scripts/forward-port");
|
||||||
.env("sip", source.ip().to_string())
|
cmd.env("sip", source.ip().to_string())
|
||||||
.env("dip", target.ip().to_string())
|
.env("dip", target.ip().to_string())
|
||||||
.env("dprefix", target_prefix.to_string())
|
.env("dprefix", target_prefix.to_string())
|
||||||
.env("sport", source.port().to_string())
|
.env("sport", source.port().to_string())
|
||||||
.env("dport", target.port().to_string())
|
.env("dport", target.port().to_string());
|
||||||
.invoke(ErrorKind::Network)
|
if let Some(filter) = src_filter {
|
||||||
.await?;
|
cmd.env("src_subnet", &filter.subnet);
|
||||||
|
if !filter.excluded.is_empty() {
|
||||||
|
cmd.env("excluded_src", &filter.excluded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd.invoke(ErrorKind::Network).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,15 +747,21 @@ async fn unforward(
|
|||||||
source: SocketAddrV4,
|
source: SocketAddrV4,
|
||||||
target: SocketAddrV4,
|
target: SocketAddrV4,
|
||||||
target_prefix: u8,
|
target_prefix: u8,
|
||||||
|
src_filter: Option<&SourceFilter>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
Command::new("/usr/lib/startos/scripts/forward-port")
|
let mut cmd = Command::new("/usr/lib/startos/scripts/forward-port");
|
||||||
.env("UNDO", "1")
|
cmd.env("UNDO", "1")
|
||||||
.env("sip", source.ip().to_string())
|
.env("sip", source.ip().to_string())
|
||||||
.env("dip", target.ip().to_string())
|
.env("dip", target.ip().to_string())
|
||||||
.env("dprefix", target_prefix.to_string())
|
.env("dprefix", target_prefix.to_string())
|
||||||
.env("sport", source.port().to_string())
|
.env("sport", source.port().to_string())
|
||||||
.env("dport", target.port().to_string())
|
.env("dport", target.port().to_string());
|
||||||
.invoke(ErrorKind::Network)
|
if let Some(filter) = src_filter {
|
||||||
.await?;
|
cmd.env("src_subnet", &filter.subnet);
|
||||||
|
if !filter.excluded.is_empty() {
|
||||||
|
cmd.env("excluded_src", &filter.excluded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd.invoke(ErrorKind::Network).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
use std::any::Any;
|
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
use std::fmt;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV6};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::Arc;
|
||||||
use std::task::{Poll, ready};
|
use std::task::Poll;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::future::Either;
|
|
||||||
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
|
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
|
||||||
use imbl::{OrdMap, OrdSet};
|
use imbl::{OrdMap, OrdSet};
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
@@ -36,15 +33,14 @@ use crate::db::model::Database;
|
|||||||
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
|
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
|
||||||
use crate::net::forward::START9_BRIDGE_IFACE;
|
use crate::net::forward::START9_BRIDGE_IFACE;
|
||||||
use crate::net::gateway::device::DeviceProxy;
|
use crate::net::gateway::device::DeviceProxy;
|
||||||
use crate::net::utils::ipv6_is_link_local;
|
use crate::net::web_server::{Accept, AcceptStream, MetadataVisitor, TcpMetadata};
|
||||||
use crate::net::web_server::{Accept, AcceptStream, Acceptor, MetadataVisitor};
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
use crate::util::collections::OrdMapIterMut;
|
use crate::util::collections::OrdMapIterMut;
|
||||||
use crate::util::future::{NonDetachingJoinHandle, Until};
|
use crate::util::future::{NonDetachingJoinHandle, Until};
|
||||||
use crate::util::io::open_file;
|
use crate::util::io::open_file;
|
||||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||||
use crate::util::sync::{SyncMutex, Watch};
|
use crate::util::sync::Watch;
|
||||||
|
|
||||||
pub fn gateway_api<C: Context>() -> ParentHandler<C> {
|
pub fn gateway_api<C: Context>() -> ParentHandler<C> {
|
||||||
ParentHandler::new()
|
ParentHandler::new()
|
||||||
@@ -758,13 +754,14 @@ async fn watch_ip(
|
|||||||
|
|
||||||
write_to.send_if_modified(
|
write_to.send_if_modified(
|
||||||
|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
|
|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
|
||||||
let (name, public, secure, prev_wan_ip) = m
|
let (name, public, secure, gateway_type, prev_wan_ip) = m
|
||||||
.get(&iface)
|
.get(&iface)
|
||||||
.map_or((None, None, None, None), |i| {
|
.map_or((None, None, None, None, None), |i| {
|
||||||
(
|
(
|
||||||
i.name.clone(),
|
i.name.clone(),
|
||||||
i.public,
|
i.public,
|
||||||
i.secure,
|
i.secure,
|
||||||
|
i.gateway_type,
|
||||||
i.ip_info
|
i.ip_info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|i| i.wan_ip),
|
.and_then(|i| i.wan_ip),
|
||||||
@@ -779,6 +776,7 @@ async fn watch_ip(
|
|||||||
public,
|
public,
|
||||||
secure,
|
secure,
|
||||||
ip_info: Some(ip_info.clone()),
|
ip_info: Some(ip_info.clone()),
|
||||||
|
gateway_type,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.filter(|old| &old.ip_info == &Some(ip_info))
|
.filter(|old| &old.ip_info == &Some(ip_info))
|
||||||
@@ -838,7 +836,6 @@ pub struct NetworkInterfaceWatcher {
|
|||||||
activated: Watch<BTreeMap<GatewayId, bool>>,
|
activated: Watch<BTreeMap<GatewayId, bool>>,
|
||||||
ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
||||||
_watcher: NonDetachingJoinHandle<()>,
|
_watcher: NonDetachingJoinHandle<()>,
|
||||||
listeners: SyncMutex<BTreeMap<u16, Weak<()>>>,
|
|
||||||
}
|
}
|
||||||
impl NetworkInterfaceWatcher {
|
impl NetworkInterfaceWatcher {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@@ -858,7 +855,6 @@ impl NetworkInterfaceWatcher {
|
|||||||
watcher(ip_info, activated).await
|
watcher(ip_info, activated).await
|
||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
listeners: SyncMutex::new(BTreeMap::new()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,51 +881,6 @@ impl NetworkInterfaceWatcher {
|
|||||||
pub fn ip_info(&self) -> OrdMap<GatewayId, NetworkInterfaceInfo> {
|
pub fn ip_info(&self) -> OrdMap<GatewayId, NetworkInterfaceInfo> {
|
||||||
self.ip_info.read()
|
self.ip_info.read()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind<B: Bind>(&self, bind: B, port: u16) -> Result<NetworkInterfaceListener<B>, Error> {
|
|
||||||
let arc = Arc::new(());
|
|
||||||
self.listeners.mutate(|l| {
|
|
||||||
if l.get(&port).filter(|w| w.strong_count() > 0).is_some() {
|
|
||||||
return Err(Error::new(
|
|
||||||
std::io::Error::from_raw_os_error(libc::EADDRINUSE),
|
|
||||||
ErrorKind::Network,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
l.insert(port, Arc::downgrade(&arc));
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
let ip_info = self.ip_info.clone_unseen();
|
|
||||||
Ok(NetworkInterfaceListener {
|
|
||||||
_arc: arc,
|
|
||||||
ip_info,
|
|
||||||
listeners: ListenerMap::new(bind, port),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn upgrade_listener<B: Bind>(
|
|
||||||
&self,
|
|
||||||
SelfContainedNetworkInterfaceListener {
|
|
||||||
mut listener,
|
|
||||||
..
|
|
||||||
}: SelfContainedNetworkInterfaceListener<B>,
|
|
||||||
) -> Result<NetworkInterfaceListener<B>, Error> {
|
|
||||||
let port = listener.listeners.port;
|
|
||||||
let arc = &listener._arc;
|
|
||||||
self.listeners.mutate(|l| {
|
|
||||||
if l.get(&port).filter(|w| w.strong_count() > 0).is_some() {
|
|
||||||
return Err(Error::new(
|
|
||||||
std::io::Error::from_raw_os_error(libc::EADDRINUSE),
|
|
||||||
ErrorKind::Network,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
l.insert(port, Arc::downgrade(arc));
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
let ip_info = self.ip_info.clone_unseen();
|
|
||||||
ip_info.mark_changed();
|
|
||||||
listener.change_ip_info_source(ip_info);
|
|
||||||
Ok(listener)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NetworkInterfaceController {
|
pub struct NetworkInterfaceController {
|
||||||
@@ -1237,235 +1188,6 @@ impl NetworkInterfaceController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait InterfaceFilter: Any + Clone + std::fmt::Debug + Eq + Ord + Send + Sync {
|
|
||||||
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool;
|
|
||||||
fn eq(&self, other: &dyn Any) -> bool {
|
|
||||||
Some(self) == other.downcast_ref::<Self>()
|
|
||||||
}
|
|
||||||
fn cmp(&self, other: &dyn Any) -> std::cmp::Ordering {
|
|
||||||
match (self as &dyn Any).type_id().cmp(&other.type_id()) {
|
|
||||||
std::cmp::Ordering::Equal => {
|
|
||||||
std::cmp::Ord::cmp(self, other.downcast_ref::<Self>().unwrap())
|
|
||||||
}
|
|
||||||
ord => ord,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn into_dyn(self) -> DynInterfaceFilter {
|
|
||||||
DynInterfaceFilter::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InterfaceFilter for bool {
|
|
||||||
fn filter(&self, _: &GatewayId, _: &NetworkInterfaceInfo) -> bool {
|
|
||||||
*self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct TypeFilter(pub NetworkInterfaceType);
|
|
||||||
impl InterfaceFilter for TypeFilter {
|
|
||||||
fn filter(&self, _: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
|
|
||||||
info.ip_info.as_ref().and_then(|i| i.device_type) == Some(self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct IdFilter(pub GatewayId);
|
|
||||||
impl InterfaceFilter for IdFilter {
|
|
||||||
fn filter(&self, id: &GatewayId, _: &NetworkInterfaceInfo) -> bool {
|
|
||||||
id == &self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct PublicFilter {
|
|
||||||
pub public: bool,
|
|
||||||
}
|
|
||||||
impl InterfaceFilter for PublicFilter {
|
|
||||||
fn filter(&self, _: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
|
|
||||||
self.public == info.public()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct SecureFilter {
|
|
||||||
pub secure: bool,
|
|
||||||
}
|
|
||||||
impl InterfaceFilter for SecureFilter {
|
|
||||||
fn filter(&self, _: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
|
|
||||||
self.secure || info.secure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct AndFilter<A, B>(pub A, pub B);
|
|
||||||
impl<A: InterfaceFilter, B: InterfaceFilter> InterfaceFilter for AndFilter<A, B> {
|
|
||||||
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
|
|
||||||
self.0.filter(id, info) && self.1.filter(id, info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct OrFilter<A, B>(pub A, pub B);
|
|
||||||
impl<A: InterfaceFilter, B: InterfaceFilter> InterfaceFilter for OrFilter<A, B> {
|
|
||||||
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
|
|
||||||
self.0.filter(id, info) || self.1.filter(id, info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct AnyFilter(pub BTreeSet<DynInterfaceFilter>);
|
|
||||||
impl InterfaceFilter for AnyFilter {
|
|
||||||
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
|
|
||||||
self.0.iter().any(|f| InterfaceFilter::filter(f, id, info))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct AllFilter(pub BTreeSet<DynInterfaceFilter>);
|
|
||||||
impl InterfaceFilter for AllFilter {
|
|
||||||
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
|
|
||||||
self.0.iter().all(|f| InterfaceFilter::filter(f, id, info))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DynInterfaceFilterT: std::fmt::Debug + Any + Send + Sync {
|
|
||||||
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool;
|
|
||||||
fn eq(&self, other: &dyn Any) -> bool;
|
|
||||||
fn cmp(&self, other: &dyn Any) -> std::cmp::Ordering;
|
|
||||||
fn as_any(&self) -> &dyn Any;
|
|
||||||
}
|
|
||||||
impl<T: InterfaceFilter> DynInterfaceFilterT for T {
|
|
||||||
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
|
|
||||||
InterfaceFilter::filter(self, id, info)
|
|
||||||
}
|
|
||||||
fn eq(&self, other: &dyn Any) -> bool {
|
|
||||||
InterfaceFilter::eq(self, other)
|
|
||||||
}
|
|
||||||
fn cmp(&self, other: &dyn Any) -> std::cmp::Ordering {
|
|
||||||
InterfaceFilter::cmp(self, other)
|
|
||||||
}
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
InterfaceFilter::as_any(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_interface_filter_eq() {
|
|
||||||
let dyn_t = true.into_dyn();
|
|
||||||
assert!(DynInterfaceFilterT::eq(
|
|
||||||
&dyn_t,
|
|
||||||
DynInterfaceFilterT::as_any(&true),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct DynInterfaceFilter(Arc<dyn DynInterfaceFilterT>);
|
|
||||||
impl InterfaceFilter for DynInterfaceFilter {
|
|
||||||
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
|
|
||||||
self.0.filter(id, info)
|
|
||||||
}
|
|
||||||
fn eq(&self, other: &dyn Any) -> bool {
|
|
||||||
self.0.eq(other)
|
|
||||||
}
|
|
||||||
fn cmp(&self, other: &dyn Any) -> std::cmp::Ordering {
|
|
||||||
self.0.cmp(other)
|
|
||||||
}
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
self.0.as_any()
|
|
||||||
}
|
|
||||||
fn into_dyn(self) -> DynInterfaceFilter {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl DynInterfaceFilter {
|
|
||||||
fn new<T: InterfaceFilter>(value: T) -> Self {
|
|
||||||
Self(Arc::new(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialEq for DynInterfaceFilter {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
DynInterfaceFilterT::eq(&*self.0, DynInterfaceFilterT::as_any(&*other.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Eq for DynInterfaceFilter {}
|
|
||||||
impl PartialOrd for DynInterfaceFilter {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
Some(self.0.cmp(other.0.as_any()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Ord for DynInterfaceFilter {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
self.0.cmp(other.0.as_any())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ListenerMap<B: Bind> {
|
|
||||||
prev_filter: DynInterfaceFilter,
|
|
||||||
bind: B,
|
|
||||||
port: u16,
|
|
||||||
listeners: BTreeMap<SocketAddr, B::Accept>,
|
|
||||||
}
|
|
||||||
impl<B: Bind> ListenerMap<B> {
|
|
||||||
fn new(bind: B, port: u16) -> Self {
|
|
||||||
Self {
|
|
||||||
prev_filter: false.into_dyn(),
|
|
||||||
bind,
|
|
||||||
port,
|
|
||||||
listeners: BTreeMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
fn update(
|
|
||||||
&mut self,
|
|
||||||
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
|
||||||
filter: &impl InterfaceFilter,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mut keep = BTreeSet::<SocketAddr>::new();
|
|
||||||
for (_, info) in ip_info
|
|
||||||
.iter()
|
|
||||||
.filter(|(id, info)| filter.filter(*id, *info))
|
|
||||||
{
|
|
||||||
if let Some(ip_info) = &info.ip_info {
|
|
||||||
for ipnet in &ip_info.subnets {
|
|
||||||
let addr = match ipnet.addr() {
|
|
||||||
IpAddr::V6(ip6) => SocketAddrV6::new(
|
|
||||||
ip6,
|
|
||||||
self.port,
|
|
||||||
0,
|
|
||||||
if ipv6_is_link_local(ip6) {
|
|
||||||
ip_info.scope_id
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
ip => SocketAddr::new(ip, self.port),
|
|
||||||
};
|
|
||||||
keep.insert(addr);
|
|
||||||
if !self.listeners.contains_key(&addr) {
|
|
||||||
self.listeners.insert(addr, self.bind.bind(addr)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.listeners.retain(|key, _| keep.contains(key));
|
|
||||||
self.prev_filter = filter.clone().into_dyn();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn poll_accept(
|
|
||||||
&mut self,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
) -> Poll<Result<(SocketAddr, <B::Accept as Accept>::Metadata, AcceptStream), Error>> {
|
|
||||||
let (metadata, stream) = ready!(self.listeners.poll_accept(cx)?);
|
|
||||||
Poll::Ready(Ok((metadata.key, metadata.inner, stream)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lookup_info_by_addr(
|
pub fn lookup_info_by_addr(
|
||||||
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
@@ -1477,28 +1199,6 @@ pub fn lookup_info_by_addr(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Bind {
|
|
||||||
type Accept: Accept;
|
|
||||||
fn bind(&mut self, addr: SocketAddr) -> Result<Self::Accept, Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
|
||||||
pub struct BindTcp;
|
|
||||||
impl Bind for BindTcp {
|
|
||||||
type Accept = TcpListener;
|
|
||||||
fn bind(&mut self, addr: SocketAddr) -> Result<Self::Accept, Error> {
|
|
||||||
TcpListener::from_std(
|
|
||||||
mio::net::TcpListener::bind(addr)
|
|
||||||
.with_kind(ErrorKind::Network)?
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.with_kind(ErrorKind::Network)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait FromGatewayInfo {
|
|
||||||
fn from_gateway_info(id: &GatewayId, info: &NetworkInterfaceInfo) -> Self;
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct GatewayInfo {
|
pub struct GatewayInfo {
|
||||||
pub id: GatewayId,
|
pub id: GatewayId,
|
||||||
@@ -1509,212 +1209,88 @@ impl<V: MetadataVisitor> Visit<V> for GatewayInfo {
|
|||||||
visitor.visit(self)
|
visitor.visit(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl FromGatewayInfo for GatewayInfo {
|
|
||||||
fn from_gateway_info(id: &GatewayId, info: &NetworkInterfaceInfo) -> Self {
|
|
||||||
Self {
|
|
||||||
id: id.clone(),
|
|
||||||
info: info.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct NetworkInterfaceListener<B: Bind = BindTcp> {
|
/// Metadata for connections accepted by WildcardListener or VHostBindListener.
|
||||||
pub ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
#[derive(Clone, Debug, VisitFields)]
|
||||||
listeners: ListenerMap<B>,
|
pub struct NetworkInterfaceListenerAcceptMetadata {
|
||||||
_arc: Arc<()>,
|
pub inner: TcpMetadata,
|
||||||
}
|
|
||||||
impl<B: Bind> NetworkInterfaceListener<B> {
|
|
||||||
pub(super) fn new(
|
|
||||||
mut ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
|
||||||
bind: B,
|
|
||||||
port: u16,
|
|
||||||
) -> Self {
|
|
||||||
ip_info.mark_unseen();
|
|
||||||
Self {
|
|
||||||
ip_info,
|
|
||||||
listeners: ListenerMap::new(bind, port),
|
|
||||||
_arc: Arc::new(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn port(&self) -> u16 {
|
|
||||||
self.listeners.port
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "unstable", inline(never))]
|
|
||||||
pub fn poll_accept<M: FromGatewayInfo>(
|
|
||||||
&mut self,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
filter: &impl InterfaceFilter,
|
|
||||||
) -> Poll<Result<(M, <B::Accept as Accept>::Metadata, AcceptStream), Error>> {
|
|
||||||
while self.ip_info.poll_changed(cx).is_ready()
|
|
||||||
|| !DynInterfaceFilterT::eq(&self.listeners.prev_filter, filter.as_any())
|
|
||||||
{
|
|
||||||
self.ip_info
|
|
||||||
.peek_and_mark_seen(|ip_info| self.listeners.update(ip_info, filter))?;
|
|
||||||
}
|
|
||||||
let (addr, inner, stream) = ready!(self.listeners.poll_accept(cx)?);
|
|
||||||
Poll::Ready(Ok((
|
|
||||||
self.ip_info
|
|
||||||
.peek(|ip_info| {
|
|
||||||
lookup_info_by_addr(ip_info, addr)
|
|
||||||
.map(|(id, info)| M::from_gateway_info(id, info))
|
|
||||||
})
|
|
||||||
.or_not_found(lazy_format!("gateway for {addr}"))?,
|
|
||||||
inner,
|
|
||||||
stream,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn change_ip_info_source(
|
|
||||||
&mut self,
|
|
||||||
mut ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
|
||||||
) {
|
|
||||||
ip_info.mark_unseen();
|
|
||||||
self.ip_info = ip_info;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn accept<M: FromGatewayInfo>(
|
|
||||||
&mut self,
|
|
||||||
filter: &impl InterfaceFilter,
|
|
||||||
) -> Result<(M, <B::Accept as Accept>::Metadata, AcceptStream), Error> {
|
|
||||||
futures::future::poll_fn(|cx| self.poll_accept(cx, filter)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_filter(&self) -> impl FnOnce(SocketAddr, &DynInterfaceFilter) -> bool + 'static {
|
|
||||||
let ip_info = self.ip_info.clone();
|
|
||||||
move |addr, filter| {
|
|
||||||
ip_info.peek(|i| {
|
|
||||||
lookup_info_by_addr(i, addr).map_or(false, |(id, info)| {
|
|
||||||
InterfaceFilter::filter(filter, id, info)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(VisitFields)]
|
|
||||||
pub struct NetworkInterfaceListenerAcceptMetadata<B: Bind> {
|
|
||||||
pub inner: <B::Accept as Accept>::Metadata,
|
|
||||||
pub info: GatewayInfo,
|
pub info: GatewayInfo,
|
||||||
}
|
}
|
||||||
impl<B: Bind> fmt::Debug for NetworkInterfaceListenerAcceptMetadata<B> {
|
impl<V: MetadataVisitor> Visit<V> for NetworkInterfaceListenerAcceptMetadata {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("NetworkInterfaceListenerAcceptMetadata")
|
|
||||||
.field("inner", &self.inner)
|
|
||||||
.field("info", &self.info)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<B: Bind> Clone for NetworkInterfaceListenerAcceptMetadata<B>
|
|
||||||
where
|
|
||||||
<B::Accept as Accept>::Metadata: Clone,
|
|
||||||
{
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: self.inner.clone(),
|
|
||||||
info: self.info.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<B, V> Visit<V> for NetworkInterfaceListenerAcceptMetadata<B>
|
|
||||||
where
|
|
||||||
B: Bind,
|
|
||||||
<B::Accept as Accept>::Metadata: Visit<V> + Clone + Send + Sync + 'static,
|
|
||||||
V: MetadataVisitor,
|
|
||||||
{
|
|
||||||
fn visit(&self, visitor: &mut V) -> V::Result {
|
fn visit(&self, visitor: &mut V) -> V::Result {
|
||||||
self.visit_fields(visitor).collect()
|
self.visit_fields(visitor).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: Bind> Accept for NetworkInterfaceListener<B> {
|
/// A simple TCP listener on 0.0.0.0:port that looks up GatewayInfo from the
|
||||||
type Metadata = NetworkInterfaceListenerAcceptMetadata<B>;
|
/// connection's local address on each accepted connection.
|
||||||
|
pub struct WildcardListener {
|
||||||
|
listener: TcpListener,
|
||||||
|
ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
||||||
|
/// Handle to the self-contained watcher task started in `new()`.
|
||||||
|
/// Dropped (and thus aborted) when `set_ip_info` replaces the ip_info source.
|
||||||
|
_watcher: Option<NonDetachingJoinHandle<()>>,
|
||||||
|
}
|
||||||
|
impl WildcardListener {
|
||||||
|
pub fn new(port: u16) -> Result<Self, Error> {
|
||||||
|
let listener = TcpListener::from_std(
|
||||||
|
mio::net::TcpListener::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port))
|
||||||
|
.with_kind(ErrorKind::Network)?
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.with_kind(ErrorKind::Network)?;
|
||||||
|
let ip_info = Watch::new(OrdMap::new());
|
||||||
|
let watcher_handle =
|
||||||
|
tokio::spawn(watcher(ip_info.clone(), Watch::new(BTreeMap::new()))).into();
|
||||||
|
Ok(Self {
|
||||||
|
listener,
|
||||||
|
ip_info,
|
||||||
|
_watcher: Some(watcher_handle),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the ip_info source with the one from the NetworkInterfaceController.
|
||||||
|
/// Aborts the self-contained watcher task.
|
||||||
|
pub fn set_ip_info(&mut self, ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>) {
|
||||||
|
self.ip_info = ip_info;
|
||||||
|
self._watcher = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Accept for WildcardListener {
|
||||||
|
type Metadata = NetworkInterfaceListenerAcceptMetadata;
|
||||||
fn poll_accept(
|
fn poll_accept(
|
||||||
&mut self,
|
&mut self,
|
||||||
cx: &mut std::task::Context<'_>,
|
cx: &mut std::task::Context<'_>,
|
||||||
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
|
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
|
||||||
NetworkInterfaceListener::poll_accept(self, cx, &true).map(|res| {
|
if let Poll::Ready((stream, peer_addr)) = TcpListener::poll_accept(&self.listener, cx)? {
|
||||||
res.map(|(info, inner, stream)| {
|
if let Err(e) = socket2::SockRef::from(&stream).set_keepalive(true) {
|
||||||
(
|
tracing::error!("Failed to set tcp keepalive: {e}");
|
||||||
NetworkInterfaceListenerAcceptMetadata { inner, info },
|
tracing::debug!("{e:?}");
|
||||||
stream,
|
}
|
||||||
)
|
let local_addr = stream.local_addr()?;
|
||||||
})
|
let info = self
|
||||||
})
|
.ip_info
|
||||||
}
|
.peek(|ip_info| {
|
||||||
}
|
lookup_info_by_addr(ip_info, local_addr).map(|(id, info)| GatewayInfo {
|
||||||
|
id: id.clone(),
|
||||||
pub struct SelfContainedNetworkInterfaceListener<B: Bind = BindTcp> {
|
info: info.clone(),
|
||||||
_watch_thread: NonDetachingJoinHandle<()>,
|
})
|
||||||
listener: NetworkInterfaceListener<B>,
|
})
|
||||||
}
|
.unwrap_or_else(|| GatewayInfo {
|
||||||
impl<B: Bind> SelfContainedNetworkInterfaceListener<B> {
|
id: InternedString::from_static("").into(),
|
||||||
pub fn bind(bind: B, port: u16) -> Self {
|
info: NetworkInterfaceInfo::default(),
|
||||||
let ip_info = Watch::new(OrdMap::new());
|
});
|
||||||
let _watch_thread =
|
return Poll::Ready(Ok((
|
||||||
tokio::spawn(watcher(ip_info.clone(), Watch::new(BTreeMap::new()))).into();
|
NetworkInterfaceListenerAcceptMetadata {
|
||||||
Self {
|
inner: TcpMetadata {
|
||||||
_watch_thread,
|
local_addr,
|
||||||
listener: NetworkInterfaceListener::new(ip_info, bind, port),
|
peer_addr,
|
||||||
|
},
|
||||||
|
info,
|
||||||
|
},
|
||||||
|
Box::pin(stream),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
Poll::Pending
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<B: Bind> Accept for SelfContainedNetworkInterfaceListener<B> {
|
|
||||||
type Metadata = <NetworkInterfaceListener<B> as Accept>::Metadata;
|
|
||||||
fn poll_accept(
|
|
||||||
&mut self,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
) -> std::task::Poll<Result<(Self::Metadata, AcceptStream), Error>> {
|
|
||||||
Accept::poll_accept(&mut self.listener, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type UpgradableListener<B = BindTcp> =
|
|
||||||
Option<Either<SelfContainedNetworkInterfaceListener<B>, NetworkInterfaceListener<B>>>;
|
|
||||||
|
|
||||||
impl<B> Acceptor<UpgradableListener<B>>
|
|
||||||
where
|
|
||||||
B: Bind + Send + Sync + 'static,
|
|
||||||
B::Accept: Send + Sync,
|
|
||||||
{
|
|
||||||
pub fn bind_upgradable(listener: SelfContainedNetworkInterfaceListener<B>) -> Self {
|
|
||||||
Self::new(Some(Either::Left(listener)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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(),
|
|
||||||
&wg1,
|
|
||||||
&NetworkInterfaceInfo {
|
|
||||||
name: None,
|
|
||||||
public: None,
|
|
||||||
secure: None,
|
|
||||||
ip_info: Some(Arc::new(IpInfo {
|
|
||||||
name: "".into(),
|
|
||||||
scope_id: 3,
|
|
||||||
device_type: Some(NetworkInterfaceType::Wireguard),
|
|
||||||
subnets: ["10.59.0.2/24".parse::<IpNet>().unwrap()]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
lan_ip: Default::default(),
|
|
||||||
wan_ip: None,
|
|
||||||
ntp_servers: Default::default(),
|
|
||||||
dns_servers: Default::default(),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,23 +12,15 @@ use crate::context::{CliContext, RpcContext};
|
|||||||
use crate::db::model::DatabaseModel;
|
use crate::db::model::DatabaseModel;
|
||||||
use crate::net::acme::AcmeProvider;
|
use crate::net::acme::AcmeProvider;
|
||||||
use crate::net::host::{HostApiKind, all_hosts};
|
use crate::net::host::{HostApiKind, all_hosts};
|
||||||
use crate::net::tor::OnionAddress;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[serde(rename_all_fields = "camelCase")]
|
pub struct HostAddress {
|
||||||
#[serde(tag = "kind")]
|
pub address: InternedString,
|
||||||
pub enum HostAddress {
|
pub public: Option<PublicDomainConfig>,
|
||||||
Onion {
|
pub private: bool,
|
||||||
address: OnionAddress,
|
|
||||||
},
|
|
||||||
Domain {
|
|
||||||
address: InternedString,
|
|
||||||
public: Option<PublicDomainConfig>,
|
|
||||||
private: bool,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||||
@@ -38,18 +30,7 @@ pub struct PublicDomainConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
|
fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
|
||||||
let mut onions = BTreeSet::<OnionAddress>::new();
|
|
||||||
let mut domains = BTreeSet::<InternedString>::new();
|
let mut domains = BTreeSet::<InternedString>::new();
|
||||||
let check_onion = |onions: &mut BTreeSet<OnionAddress>, onion: OnionAddress| {
|
|
||||||
if onions.contains(&onion) {
|
|
||||||
return Err(Error::new(
|
|
||||||
eyre!("onion address {onion} is already in use"),
|
|
||||||
ErrorKind::InvalidRequest,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
onions.insert(onion);
|
|
||||||
Ok(())
|
|
||||||
};
|
|
||||||
let check_domain = |domains: &mut BTreeSet<InternedString>, domain: InternedString| {
|
let check_domain = |domains: &mut BTreeSet<InternedString>, domain: InternedString| {
|
||||||
if domains.contains(&domain) {
|
if domains.contains(&domain) {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
@@ -68,9 +49,6 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
|
|||||||
not_in_use.push(host);
|
not_in_use.push(host);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for onion in host.as_onions().de()? {
|
|
||||||
check_onion(&mut onions, onion)?;
|
|
||||||
}
|
|
||||||
let public = host.as_public_domains().keys()?;
|
let public = host.as_public_domains().keys()?;
|
||||||
for domain in &public {
|
for domain in &public {
|
||||||
check_domain(&mut domains, domain.clone())?;
|
check_domain(&mut domains, domain.clone())?;
|
||||||
@@ -82,16 +60,11 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for host in not_in_use {
|
for host in not_in_use {
|
||||||
host.as_onions_mut()
|
|
||||||
.mutate(|o| Ok(o.retain(|o| !onions.contains(o))))?;
|
|
||||||
host.as_public_domains_mut()
|
host.as_public_domains_mut()
|
||||||
.mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?;
|
.mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?;
|
||||||
host.as_private_domains_mut()
|
host.as_private_domains_mut()
|
||||||
.mutate(|d| Ok(d.retain(|d| !domains.contains(d))))?;
|
.mutate(|d| Ok(d.retain(|d| !domains.contains(d))))?;
|
||||||
|
|
||||||
for onion in host.as_onions().de()? {
|
|
||||||
check_onion(&mut onions, onion)?;
|
|
||||||
}
|
|
||||||
let public = host.as_public_domains().keys()?;
|
let public = host.as_public_domains().keys()?;
|
||||||
for domain in &public {
|
for domain in &public {
|
||||||
check_domain(&mut domains, domain.clone())?;
|
check_domain(&mut domains, domain.clone())?;
|
||||||
@@ -159,29 +132,6 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
|
|||||||
)
|
)
|
||||||
.with_inherited(Kind::inheritance),
|
.with_inherited(Kind::inheritance),
|
||||||
)
|
)
|
||||||
.subcommand(
|
|
||||||
"onion",
|
|
||||||
ParentHandler::<C, Empty, Kind::Inheritance>::new()
|
|
||||||
.subcommand(
|
|
||||||
"add",
|
|
||||||
from_fn_async(add_onion::<Kind>)
|
|
||||||
.with_metadata("sync_db", Value::Bool(true))
|
|
||||||
.with_inherited(|_, a| a)
|
|
||||||
.no_display()
|
|
||||||
.with_about("about.add-address-to-host")
|
|
||||||
.with_call_remote::<CliContext>(),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
"remove",
|
|
||||||
from_fn_async(remove_onion::<Kind>)
|
|
||||||
.with_metadata("sync_db", Value::Bool(true))
|
|
||||||
.with_inherited(|_, a| a)
|
|
||||||
.no_display()
|
|
||||||
.with_about("about.remove-address-from-host")
|
|
||||||
.with_call_remote::<CliContext>(),
|
|
||||||
)
|
|
||||||
.with_inherited(Kind::inheritance),
|
|
||||||
)
|
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"list",
|
"list",
|
||||||
from_fn_async(list_addresses::<Kind>)
|
from_fn_async(list_addresses::<Kind>)
|
||||||
@@ -197,32 +147,18 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
|
|||||||
|
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]);
|
table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]);
|
||||||
for address in &res {
|
for entry in &res {
|
||||||
match address {
|
if let Some(PublicDomainConfig { gateway, acme }) = &entry.public {
|
||||||
HostAddress::Onion { address } => {
|
table.add_row(row![
|
||||||
table.add_row(row![address, true, "N/A"]);
|
entry.address,
|
||||||
}
|
&format!(
|
||||||
HostAddress::Domain {
|
"{} ({gateway})",
|
||||||
address,
|
if entry.private { "YES" } else { "ONLY" }
|
||||||
public: Some(PublicDomainConfig { gateway, acme }),
|
),
|
||||||
private,
|
acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE")
|
||||||
} => {
|
]);
|
||||||
table.add_row(row![
|
} else {
|
||||||
address,
|
table.add_row(row![entry.address, &format!("NO"), "N/A"]);
|
||||||
&format!(
|
|
||||||
"{} ({gateway})",
|
|
||||||
if *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"]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,55 +287,6 @@ pub async fn remove_private_domain<Kind: HostApiKind>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser)]
|
|
||||||
pub struct OnionParams {
|
|
||||||
#[arg(help = "help.arg.onion-address")]
|
|
||||||
pub onion: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_onion<Kind: HostApiKind>(
|
|
||||||
ctx: RpcContext,
|
|
||||||
OnionParams { onion }: OnionParams,
|
|
||||||
inheritance: Kind::Inheritance,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let onion = onion.parse::<OnionAddress>()?;
|
|
||||||
ctx.db
|
|
||||||
.mutate(|db| {
|
|
||||||
db.as_private().as_key_store().as_onion().get_key(&onion)?;
|
|
||||||
|
|
||||||
Kind::host_for(&inheritance, db)?
|
|
||||||
.as_onions_mut()
|
|
||||||
.mutate(|a| Ok(a.insert(onion)))?;
|
|
||||||
handle_duplicates(db)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.result?;
|
|
||||||
|
|
||||||
Kind::sync_host(&ctx, inheritance).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove_onion<Kind: HostApiKind>(
|
|
||||||
ctx: RpcContext,
|
|
||||||
OnionParams { onion }: OnionParams,
|
|
||||||
inheritance: Kind::Inheritance,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let onion = onion.parse::<OnionAddress>()?;
|
|
||||||
ctx.db
|
|
||||||
.mutate(|db| {
|
|
||||||
Kind::host_for(&inheritance, db)?
|
|
||||||
.as_onions_mut()
|
|
||||||
.mutate(|a| Ok(a.remove(&onion)))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.result?;
|
|
||||||
|
|
||||||
Kind::sync_host(&ctx, inheritance).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_addresses<Kind: HostApiKind>(
|
pub async fn list_addresses<Kind: HostApiKind>(
|
||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
_: Empty,
|
_: Empty,
|
||||||
|
|||||||
@@ -3,21 +3,20 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use clap::builder::ValueParserFactory;
|
use clap::builder::ValueParserFactory;
|
||||||
use imbl::OrdSet;
|
|
||||||
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
use crate::db::model::public::NetworkInterfaceInfo;
|
use crate::db::prelude::Map;
|
||||||
use crate::net::forward::AvailablePorts;
|
use crate::net::forward::AvailablePorts;
|
||||||
use crate::net::gateway::InterfaceFilter;
|
|
||||||
use crate::net::host::HostApiKind;
|
use crate::net::host::HostApiKind;
|
||||||
|
use crate::net::service_interface::HostnameInfo;
|
||||||
use crate::net::vhost::AlpnInfo;
|
use crate::net::vhost::AlpnInfo;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::FromStrParser;
|
use crate::util::FromStrParser;
|
||||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||||
use crate::{GatewayId, HostId};
|
use crate::HostId;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
@@ -45,25 +44,82 @@ impl FromStr for BindId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
#[derive(Debug, Default, Clone, Deserialize, Serialize, TS, HasModel)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
|
#[model = "Model<Self>"]
|
||||||
|
pub struct DerivedAddressInfo {
|
||||||
|
/// User-controlled: private addresses the user has disabled
|
||||||
|
pub private_disabled: BTreeSet<HostnameInfo>,
|
||||||
|
/// User-controlled: public 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(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 struct BindInfo {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub options: BindOptions,
|
pub options: BindOptions,
|
||||||
pub net: NetInfo,
|
pub net: NetInfo,
|
||||||
|
pub addresses: DerivedAddressInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct NetInfo {
|
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_port: Option<u16>,
|
||||||
pub assigned_ssl_port: Option<u16>,
|
pub assigned_ssl_port: Option<u16>,
|
||||||
}
|
}
|
||||||
@@ -71,25 +127,28 @@ impl BindInfo {
|
|||||||
pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result<Self, Error> {
|
pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result<Self, Error> {
|
||||||
let mut assigned_port = None;
|
let mut assigned_port = None;
|
||||||
let mut assigned_ssl_port = None;
|
let mut assigned_ssl_port = None;
|
||||||
if options.add_ssl.is_some() {
|
if let Some(ssl) = &options.add_ssl {
|
||||||
assigned_ssl_port = Some(available_ports.alloc()?);
|
assigned_ssl_port = available_ports
|
||||||
|
.try_alloc(ssl.preferred_external_port, true)
|
||||||
|
.or_else(|| Some(available_ports.alloc(true).ok()?));
|
||||||
}
|
}
|
||||||
if options
|
if options
|
||||||
.secure
|
.secure
|
||||||
.map_or(true, |s| !(s.ssl && options.add_ssl.is_some()))
|
.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 {
|
Ok(Self {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
options,
|
options,
|
||||||
net: NetInfo {
|
net: NetInfo {
|
||||||
private_disabled: OrdSet::new(),
|
|
||||||
public_enabled: OrdSet::new(),
|
|
||||||
assigned_port,
|
assigned_port,
|
||||||
assigned_ssl_port,
|
assigned_ssl_port,
|
||||||
},
|
},
|
||||||
|
addresses: DerivedAddressInfo::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn update(
|
pub fn update(
|
||||||
@@ -97,7 +156,11 @@ impl BindInfo {
|
|||||||
available_ports: &mut AvailablePorts,
|
available_ports: &mut AvailablePorts,
|
||||||
options: BindOptions,
|
options: BindOptions,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let Self { net: mut lan, .. } = self;
|
let Self {
|
||||||
|
net: mut lan,
|
||||||
|
addresses,
|
||||||
|
..
|
||||||
|
} = self;
|
||||||
if options
|
if options
|
||||||
.secure
|
.secure
|
||||||
.map_or(true, |s| !(s.ssl && options.add_ssl.is_some()))
|
.map_or(true, |s| !(s.ssl && options.add_ssl.is_some()))
|
||||||
@@ -105,19 +168,26 @@ impl BindInfo {
|
|||||||
{
|
{
|
||||||
lan.assigned_port = if let Some(port) = lan.assigned_port.take() {
|
lan.assigned_port = if let Some(port) = lan.assigned_port.take() {
|
||||||
Some(port)
|
Some(port)
|
||||||
|
} else if let Some(port) =
|
||||||
|
available_ports.try_alloc(options.preferred_external_port, false)
|
||||||
|
{
|
||||||
|
Some(port)
|
||||||
} else {
|
} else {
|
||||||
Some(available_ports.alloc()?)
|
Some(available_ports.alloc(false)?)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
if let Some(port) = lan.assigned_port.take() {
|
if let Some(port) = lan.assigned_port.take() {
|
||||||
available_ports.free([port]);
|
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() {
|
lan.assigned_ssl_port = if let Some(port) = lan.assigned_ssl_port.take() {
|
||||||
Some(port)
|
Some(port)
|
||||||
|
} else if let Some(port) = available_ports.try_alloc(ssl.preferred_external_port, true)
|
||||||
|
{
|
||||||
|
Some(port)
|
||||||
} else {
|
} else {
|
||||||
Some(available_ports.alloc()?)
|
Some(available_ports.alloc(true)?)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
if let Some(port) = lan.assigned_ssl_port.take() {
|
if let Some(port) = lan.assigned_ssl_port.take() {
|
||||||
@@ -128,22 +198,17 @@ impl BindInfo {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
options,
|
options,
|
||||||
net: lan,
|
net: lan,
|
||||||
|
addresses: DerivedAddressInfo {
|
||||||
|
private_disabled: addresses.private_disabled,
|
||||||
|
public_enabled: addresses.public_enabled,
|
||||||
|
possible: BTreeSet::new(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn disable(&mut self) {
|
pub fn disable(&mut self) {
|
||||||
self.enabled = false;
|
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)]
|
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
@@ -188,7 +253,7 @@ pub fn binding<C: Context, Kind: HostApiKind>()
|
|||||||
|
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
table.add_row(row![bc => "INTERNAL PORT", "ENABLED", "EXTERNAL PORT", "EXTERNAL SSL PORT"]);
|
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![
|
table.add_row(row![
|
||||||
internal,
|
internal,
|
||||||
info.enabled,
|
info.enabled,
|
||||||
@@ -213,12 +278,12 @@ pub fn binding<C: Context, Kind: HostApiKind>()
|
|||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"set-gateway-enabled",
|
"set-address-enabled",
|
||||||
from_fn_async(set_gateway_enabled::<Kind>)
|
from_fn_async(set_address_enabled::<Kind>)
|
||||||
.with_metadata("sync_db", Value::Bool(true))
|
.with_metadata("sync_db", Value::Bool(true))
|
||||||
.with_inherited(Kind::inheritance)
|
.with_inherited(Kind::inheritance)
|
||||||
.no_display()
|
.no_display()
|
||||||
.with_about("about.set-gateway-enabled-for-binding")
|
.with_about("about.set-address-enabled-for-binding")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -227,7 +292,7 @@ pub async fn list_bindings<Kind: HostApiKind>(
|
|||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
_: Empty,
|
_: Empty,
|
||||||
inheritance: Kind::Inheritance,
|
inheritance: Kind::Inheritance,
|
||||||
) -> Result<BTreeMap<u16, BindInfo>, Error> {
|
) -> Result<Bindings, Error> {
|
||||||
Kind::host_for(&inheritance, &mut ctx.db.peek().await)?
|
Kind::host_for(&inheritance, &mut ctx.db.peek().await)?
|
||||||
.as_bindings()
|
.as_bindings()
|
||||||
.de()
|
.de()
|
||||||
@@ -236,50 +301,44 @@ pub async fn list_bindings<Kind: HostApiKind>(
|
|||||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct BindingGatewaySetEnabledParams {
|
pub struct BindingSetAddressEnabledParams {
|
||||||
#[arg(help = "help.arg.internal-port")]
|
#[arg(help = "help.arg.internal-port")]
|
||||||
internal_port: u16,
|
internal_port: u16,
|
||||||
#[arg(help = "help.arg.gateway-id")]
|
#[arg(long, help = "help.arg.address")]
|
||||||
gateway: GatewayId,
|
address: String,
|
||||||
#[arg(long, help = "help.arg.binding-enabled")]
|
#[arg(long, help = "help.arg.binding-enabled")]
|
||||||
enabled: Option<bool>,
|
enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_gateway_enabled<Kind: HostApiKind>(
|
pub async fn set_address_enabled<Kind: HostApiKind>(
|
||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
BindingGatewaySetEnabledParams {
|
BindingSetAddressEnabledParams {
|
||||||
internal_port,
|
internal_port,
|
||||||
gateway,
|
address,
|
||||||
enabled,
|
enabled,
|
||||||
}: BindingGatewaySetEnabledParams,
|
}: BindingSetAddressEnabledParams,
|
||||||
inheritance: Kind::Inheritance,
|
inheritance: Kind::Inheritance,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let enabled = enabled.unwrap_or(true);
|
let enabled = enabled.unwrap_or(true);
|
||||||
let gateway_public = ctx
|
let address: HostnameInfo =
|
||||||
.net_controller
|
serde_json::from_str(&address).with_kind(ErrorKind::Deserialization)?;
|
||||||
.net_iface
|
|
||||||
.watcher
|
|
||||||
.ip_info()
|
|
||||||
.get(&gateway)
|
|
||||||
.or_not_found(&gateway)?
|
|
||||||
.public();
|
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
Kind::host_for(&inheritance, db)?
|
Kind::host_for(&inheritance, db)?
|
||||||
.as_bindings_mut()
|
.as_bindings_mut()
|
||||||
.mutate(|b| {
|
.mutate(|b| {
|
||||||
let net = &mut b.get_mut(&internal_port).or_not_found(internal_port)?.net;
|
let bind = b.get_mut(&internal_port).or_not_found(internal_port)?;
|
||||||
if gateway_public {
|
if address.public {
|
||||||
if enabled {
|
if enabled {
|
||||||
net.public_enabled.insert(gateway);
|
bind.addresses.public_enabled.insert(address.clone());
|
||||||
} else {
|
} else {
|
||||||
net.public_enabled.remove(&gateway);
|
bind.addresses.public_enabled.remove(&address);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if enabled {
|
if enabled {
|
||||||
net.private_disabled.remove(&gateway);
|
bind.addresses.private_disabled.remove(&address);
|
||||||
} else {
|
} else {
|
||||||
net.private_disabled.insert(gateway);
|
bind.addresses.private_disabled.insert(address.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ use crate::context::RpcContext;
|
|||||||
use crate::db::model::DatabaseModel;
|
use crate::db::model::DatabaseModel;
|
||||||
use crate::net::forward::AvailablePorts;
|
use crate::net::forward::AvailablePorts;
|
||||||
use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api};
|
use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api};
|
||||||
use crate::net::host::binding::{BindInfo, BindOptions, binding};
|
use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding};
|
||||||
use crate::net::service_interface::HostnameInfo;
|
|
||||||
use crate::net::tor::OnionAddress;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{HostId, PackageId};
|
use crate::{HostId, PackageId};
|
||||||
|
|
||||||
@@ -27,13 +25,9 @@ pub mod binding;
|
|||||||
#[model = "Model<Self>"]
|
#[model = "Model<Self>"]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct Host {
|
pub struct Host {
|
||||||
pub bindings: BTreeMap<u16, BindInfo>,
|
pub bindings: Bindings,
|
||||||
#[ts(type = "string[]")]
|
|
||||||
pub onions: BTreeSet<OnionAddress>,
|
|
||||||
pub public_domains: BTreeMap<InternedString, PublicDomainConfig>,
|
pub public_domains: BTreeMap<InternedString, PublicDomainConfig>,
|
||||||
pub private_domains: BTreeSet<InternedString>,
|
pub private_domains: BTreeSet<InternedString>,
|
||||||
/// COMPUTED: NetService::update
|
|
||||||
pub hostname_info: BTreeMap<u16, Vec<HostnameInfo>>, // internal port -> Hostnames
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<Host> for Host {
|
impl AsRef<Host> for Host {
|
||||||
@@ -46,24 +40,18 @@ impl Host {
|
|||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
pub fn addresses<'a>(&'a self) -> impl Iterator<Item = HostAddress> + 'a {
|
pub fn addresses<'a>(&'a self) -> impl Iterator<Item = HostAddress> + 'a {
|
||||||
self.onions
|
self.public_domains
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.map(|(address, config)| HostAddress {
|
||||||
.map(|address| HostAddress::Onion { address })
|
address: address.clone(),
|
||||||
.chain(
|
public: Some(config.clone()),
|
||||||
self.public_domains
|
private: self.private_domains.contains(address),
|
||||||
.iter()
|
})
|
||||||
.map(|(address, config)| HostAddress::Domain {
|
|
||||||
address: address.clone(),
|
|
||||||
public: Some(config.clone()),
|
|
||||||
private: self.private_domains.contains(address),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.chain(
|
.chain(
|
||||||
self.private_domains
|
self.private_domains
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|a| !self.public_domains.contains_key(*a))
|
.filter(|a| !self.public_domains.contains_key(*a))
|
||||||
.map(|address| HostAddress::Domain {
|
.map(|address| HostAddress {
|
||||||
address: address.clone(),
|
address: address.clone(),
|
||||||
public: None,
|
public: None,
|
||||||
private: true,
|
private: true,
|
||||||
@@ -112,22 +100,7 @@ pub fn host_for<'a>(
|
|||||||
.as_hosts_mut(),
|
.as_hosts_mut(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
let tor_key = if host_info(db, package_id)?.as_idx(host_id).is_none() {
|
host_info(db, package_id)?.upsert(host_id, || Ok(Host::new()))
|
||||||
Some(
|
|
||||||
db.as_private_mut()
|
|
||||||
.as_key_store_mut()
|
|
||||||
.as_onion_mut()
|
|
||||||
.new_key()?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
host_info(db, package_id)?.upsert(host_id, || {
|
|
||||||
let mut h = Host::new();
|
|
||||||
h.onions
|
|
||||||
.insert(tor_key.or_not_found("generated tor key")?.onion_address());
|
|
||||||
Ok(h)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_hosts(db: &mut DatabaseModel) -> impl Iterator<Item = Result<&mut Model<Host>, Error>> {
|
pub fn all_hosts(db: &mut DatabaseModel) -> impl Iterator<Item = Result<&mut Model<Host>, Error>> {
|
||||||
|
|||||||
@@ -3,28 +3,21 @@ use serde::{Deserialize, Serialize};
|
|||||||
use crate::account::AccountInfo;
|
use crate::account::AccountInfo;
|
||||||
use crate::net::acme::AcmeCertStore;
|
use crate::net::acme::AcmeCertStore;
|
||||||
use crate::net::ssl::CertStore;
|
use crate::net::ssl::CertStore;
|
||||||
use crate::net::tor::OnionStore;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||||
#[model = "Model<Self>"]
|
#[model = "Model<Self>"]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct KeyStore {
|
pub struct KeyStore {
|
||||||
pub onion: OnionStore,
|
|
||||||
pub local_certs: CertStore,
|
pub local_certs: CertStore,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub acme: AcmeCertStore,
|
pub acme: AcmeCertStore,
|
||||||
}
|
}
|
||||||
impl KeyStore {
|
impl KeyStore {
|
||||||
pub fn new(account: &AccountInfo) -> Result<Self, Error> {
|
pub fn new(account: &AccountInfo) -> Result<Self, Error> {
|
||||||
let mut res = Self {
|
Ok(Self {
|
||||||
onion: OnionStore::new(),
|
|
||||||
local_certs: CertStore::new(account)?,
|
local_certs: CertStore::new(account)?,
|
||||||
acme: AcmeCertStore::new(),
|
acme: AcmeCertStore::new(),
|
||||||
};
|
})
|
||||||
for tor_key in account.tor_keys.iter().cloned() {
|
|
||||||
res.onion.insert(tor_key);
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ pub mod socks;
|
|||||||
pub mod ssl;
|
pub mod ssl;
|
||||||
pub mod static_server;
|
pub mod static_server;
|
||||||
pub mod tls;
|
pub mod tls;
|
||||||
pub mod tor;
|
|
||||||
pub mod tunnel;
|
pub mod tunnel;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod vhost;
|
pub mod vhost;
|
||||||
@@ -23,7 +22,6 @@ pub mod wifi;
|
|||||||
|
|
||||||
pub fn net_api<C: Context>() -> ParentHandler<C> {
|
pub fn net_api<C: Context>() -> ParentHandler<C> {
|
||||||
ParentHandler::new()
|
ParentHandler::new()
|
||||||
.subcommand("tor", tor::tor_api::<C>().with_about("about.tor-commands"))
|
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"acme",
|
"acme",
|
||||||
acme::acme_api::<C>().with_about("about.setup-acme-certificate"),
|
acme::acme_api::<C>().with_about("about.setup-acme-certificate"),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
|||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use imbl::{OrdMap, vector};
|
use imbl::vector;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use ipnet::IpNet;
|
use ipnet::IpNet;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -16,17 +16,15 @@ use crate::db::model::public::NetworkInterfaceType;
|
|||||||
use crate::error::ErrorCollection;
|
use crate::error::ErrorCollection;
|
||||||
use crate::hostname::Hostname;
|
use crate::hostname::Hostname;
|
||||||
use crate::net::dns::DnsController;
|
use crate::net::dns::DnsController;
|
||||||
use crate::net::forward::{InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule};
|
use crate::net::forward::{
|
||||||
use crate::net::gateway::{
|
ForwardRequirements, InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule,
|
||||||
AndFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, NetworkInterfaceController, OrFilter,
|
|
||||||
PublicFilter, SecureFilter, TypeFilter,
|
|
||||||
};
|
};
|
||||||
|
use crate::net::gateway::NetworkInterfaceController;
|
||||||
use crate::net::host::address::HostAddress;
|
use crate::net::host::address::HostAddress;
|
||||||
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
|
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
|
||||||
use crate::net::host::{Host, Hosts, host_for};
|
use crate::net::host::{Host, Hosts, host_for};
|
||||||
use crate::net::service_interface::{GatewayInfo, HostnameInfo, IpHostname, OnionHostname};
|
use crate::net::service_interface::{GatewayInfo, HostnameInfo, IpHostname};
|
||||||
use crate::net::socks::SocksController;
|
use crate::net::socks::SocksController;
|
||||||
use crate::net::tor::{OnionAddress, TorController, TorSecretKey};
|
|
||||||
use crate::net::utils::ipv6_is_local;
|
use crate::net::utils::ipv6_is_local;
|
||||||
use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController};
|
use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -36,7 +34,6 @@ use crate::{GatewayId, HOST_IP, HostId, OptionExt, PackageId};
|
|||||||
|
|
||||||
pub struct NetController {
|
pub struct NetController {
|
||||||
pub(crate) db: TypedPatchDb<Database>,
|
pub(crate) db: TypedPatchDb<Database>,
|
||||||
pub(super) tor: TorController,
|
|
||||||
pub(super) vhost: VHostController,
|
pub(super) vhost: VHostController,
|
||||||
pub(super) tls_client_config: Arc<TlsClientConfig>,
|
pub(super) tls_client_config: Arc<TlsClientConfig>,
|
||||||
pub(crate) net_iface: Arc<NetworkInterfaceController>,
|
pub(crate) net_iface: Arc<NetworkInterfaceController>,
|
||||||
@@ -54,8 +51,7 @@ impl NetController {
|
|||||||
socks_listen: SocketAddr,
|
socks_listen: SocketAddr,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let net_iface = Arc::new(NetworkInterfaceController::new(db.clone()));
|
let net_iface = Arc::new(NetworkInterfaceController::new(db.clone()));
|
||||||
let tor = TorController::new()?;
|
let socks = SocksController::new(socks_listen)?;
|
||||||
let socks = SocksController::new(socks_listen, tor.clone())?;
|
|
||||||
let crypto_provider = Arc::new(tokio_rustls::rustls::crypto::ring::default_provider());
|
let crypto_provider = Arc::new(tokio_rustls::rustls::crypto::ring::default_provider());
|
||||||
let tls_client_config = Arc::new(crate::net::tls::client_config(
|
let tls_client_config = Arc::new(crate::net::tls::client_config(
|
||||||
crypto_provider.clone(),
|
crypto_provider.clone(),
|
||||||
@@ -87,7 +83,6 @@ impl NetController {
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
tor,
|
|
||||||
vhost: VHostController::new(db.clone(), net_iface.clone(), crypto_provider),
|
vhost: VHostController::new(db.clone(), net_iface.clone(), crypto_provider),
|
||||||
tls_client_config,
|
tls_client_config,
|
||||||
dns: DnsController::init(db, &net_iface.watcher).await?,
|
dns: DnsController::init(db, &net_iface.watcher).await?,
|
||||||
@@ -165,10 +160,9 @@ impl NetController {
|
|||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
struct HostBinds {
|
struct HostBinds {
|
||||||
forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter, Arc<()>)>,
|
forwards: BTreeMap<u16, (SocketAddrV4, ForwardRequirements, Arc<()>)>,
|
||||||
vhosts: BTreeMap<(Option<InternedString>, u16), (ProxyTarget, Arc<()>)>,
|
vhosts: BTreeMap<(Option<InternedString>, u16), (ProxyTarget, Arc<()>)>,
|
||||||
private_dns: BTreeMap<InternedString, Arc<()>>,
|
private_dns: BTreeMap<InternedString, Arc<()>>,
|
||||||
tor: BTreeMap<OnionAddress, (OrdMap<u16, SocketAddr>, Vec<Arc<()>>)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NetServiceData {
|
pub struct NetServiceData {
|
||||||
@@ -207,7 +201,7 @@ impl NetServiceData {
|
|||||||
.as_entries_mut()?
|
.as_entries_mut()?
|
||||||
{
|
{
|
||||||
host.as_bindings_mut().mutate(|b| {
|
host.as_bindings_mut().mutate(|b| {
|
||||||
for (internal_port, info) in b {
|
for (internal_port, info) in b.iter_mut() {
|
||||||
if !except.contains(&BindId {
|
if !except.contains(&BindId {
|
||||||
id: host_id.clone(),
|
id: host_id.clone(),
|
||||||
internal_port: *internal_port,
|
internal_port: *internal_port,
|
||||||
@@ -238,7 +232,7 @@ impl NetServiceData {
|
|||||||
.as_network_mut()
|
.as_network_mut()
|
||||||
.as_host_mut();
|
.as_host_mut();
|
||||||
host.as_bindings_mut().mutate(|b| {
|
host.as_bindings_mut().mutate(|b| {
|
||||||
for (internal_port, info) in b {
|
for (internal_port, info) in b.iter_mut() {
|
||||||
if !except.contains(&BindId {
|
if !except.contains(&BindId {
|
||||||
id: HostId::default(),
|
id: HostId::default(),
|
||||||
internal_port: *internal_port,
|
internal_port: *internal_port,
|
||||||
@@ -256,425 +250,295 @@ impl NetServiceData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(&mut self, ctrl: &NetController, id: HostId, host: Host) -> Result<(), Error> {
|
async fn update(
|
||||||
let mut forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter)> = BTreeMap::new();
|
&mut self,
|
||||||
|
ctrl: &NetController,
|
||||||
|
id: HostId,
|
||||||
|
mut host: Host,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut forwards: BTreeMap<u16, (SocketAddrV4, ForwardRequirements)> = BTreeMap::new();
|
||||||
let mut vhosts: BTreeMap<(Option<InternedString>, u16), ProxyTarget> = BTreeMap::new();
|
let mut vhosts: BTreeMap<(Option<InternedString>, u16), ProxyTarget> = BTreeMap::new();
|
||||||
let mut private_dns: BTreeSet<InternedString> = BTreeSet::new();
|
let mut private_dns: BTreeSet<InternedString> = BTreeSet::new();
|
||||||
let mut tor: BTreeMap<OnionAddress, (TorSecretKey, OrdMap<u16, SocketAddr>)> =
|
|
||||||
BTreeMap::new();
|
|
||||||
let mut hostname_info: BTreeMap<u16, Vec<HostnameInfo>> = BTreeMap::new();
|
|
||||||
let binds = self.binds.entry(id.clone()).or_default();
|
let binds = self.binds.entry(id.clone()).or_default();
|
||||||
|
|
||||||
let peek = ctrl.db.peek().await;
|
let peek = ctrl.db.peek().await;
|
||||||
|
|
||||||
// LAN
|
|
||||||
let server_info = peek.as_public().as_server_info();
|
let server_info = peek.as_public().as_server_info();
|
||||||
let net_ifaces = ctrl.net_iface.watcher.ip_info();
|
let net_ifaces = ctrl.net_iface.watcher.ip_info();
|
||||||
let hostname = server_info.as_hostname().de()?;
|
let hostname = server_info.as_hostname().de()?;
|
||||||
for (port, bind) in &host.bindings {
|
let host_addresses: Vec<_> = host.addresses().collect();
|
||||||
|
|
||||||
|
// Collect private DNS entries (domains without public config)
|
||||||
|
for HostAddress {
|
||||||
|
address, public, ..
|
||||||
|
} in &host_addresses
|
||||||
|
{
|
||||||
|
if public.is_none() {
|
||||||
|
private_dns.insert(address.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Phase 1: Compute possible addresses ──
|
||||||
|
for (_port, bind) in host.bindings.iter_mut() {
|
||||||
if !bind.enabled {
|
if !bind.enabled {
|
||||||
continue;
|
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() {
|
||||||
let mut hostnames = BTreeSet::new();
|
continue;
|
||||||
if let Some(ssl) = &bind.options.add_ssl {
|
}
|
||||||
let external = bind
|
|
||||||
.net
|
bind.addresses.possible.clear();
|
||||||
.assigned_ssl_port
|
for (gateway_id, info) in net_ifaces
|
||||||
.or_not_found("assigned ssl port")?;
|
.iter()
|
||||||
let addr = (self.ip, *port).into();
|
.filter(|(_, info)| {
|
||||||
let connect_ssl = if let Some(alpn) = ssl.alpn.clone() {
|
info.ip_info.as_ref().map_or(false, |i| {
|
||||||
Err(alpn)
|
!matches!(i.device_type, Some(NetworkInterfaceType::Bridge))
|
||||||
} else {
|
})
|
||||||
if bind.options.secure.as_ref().map_or(false, |s| s.ssl) {
|
})
|
||||||
Ok(())
|
.filter(|(_, info)| info.ip_info.is_some())
|
||||||
} else {
|
{
|
||||||
Err(AlpnInfo::Reflect)
|
let gateway = GatewayInfo {
|
||||||
}
|
id: gateway_id.clone(),
|
||||||
};
|
name: info
|
||||||
for hostname in ctrl.server_hostnames.iter().cloned() {
|
.name
|
||||||
vhosts.insert(
|
.clone()
|
||||||
(hostname, external),
|
.or_else(|| info.ip_info.as_ref().map(|i| i.name.clone()))
|
||||||
ProxyTarget {
|
.unwrap_or_else(|| gateway_id.clone().into()),
|
||||||
filter: bind.net.clone().into_dyn(),
|
public: info.public(),
|
||||||
acme: None,
|
};
|
||||||
addr,
|
let port = bind.net.assigned_port.filter(|_| {
|
||||||
add_x_forwarded_headers: ssl.add_x_forwarded_headers,
|
bind.options.secure.map_or(false, |s| {
|
||||||
connect_ssl: connect_ssl
|
!(s.ssl && bind.options.add_ssl.is_some()) || info.secure()
|
||||||
.clone()
|
})
|
||||||
.map(|_| ctrl.tls_client_config.clone()),
|
});
|
||||||
},
|
// .local addresses (private only, non-public, non-wireguard gateways)
|
||||||
);
|
if !info.public()
|
||||||
}
|
&& info.ip_info.as_ref().map_or(false, |i| {
|
||||||
for address in host.addresses() {
|
i.device_type != Some(NetworkInterfaceType::Wireguard)
|
||||||
match address {
|
|
||||||
HostAddress::Onion { address } => {
|
|
||||||
let hostname = InternedString::from_display(&address);
|
|
||||||
if hostnames.insert(hostname.clone()) {
|
|
||||||
vhosts.insert(
|
|
||||||
(Some(hostname), external),
|
|
||||||
ProxyTarget {
|
|
||||||
filter: OrFilter(
|
|
||||||
TypeFilter(NetworkInterfaceType::Loopback),
|
|
||||||
IdFilter(GatewayId::from(InternedString::from(
|
|
||||||
START9_BRIDGE_IFACE,
|
|
||||||
))),
|
|
||||||
)
|
|
||||||
.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: wrap onion ssl stream directly in tor ctrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HostAddress::Domain {
|
|
||||||
address,
|
|
||||||
public,
|
|
||||||
private,
|
|
||||||
} => {
|
|
||||||
if hostnames.insert(address.clone()) {
|
|
||||||
let address = Some(address.clone());
|
|
||||||
if ssl.preferred_external_port == 443 {
|
|
||||||
if let Some(public) = &public {
|
|
||||||
vhosts.insert(
|
|
||||||
(address.clone(), 5443),
|
|
||||||
ProxyTarget {
|
|
||||||
filter: AndFilter(
|
|
||||||
bind.net.clone(),
|
|
||||||
AndFilter(
|
|
||||||
IdFilter(public.gateway.clone()),
|
|
||||||
PublicFilter { public: false },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.into_dyn(),
|
|
||||||
acme: public.acme.clone(),
|
|
||||||
addr,
|
|
||||||
add_x_forwarded_headers: ssl
|
|
||||||
.add_x_forwarded_headers,
|
|
||||||
connect_ssl: connect_ssl
|
|
||||||
.clone()
|
|
||||||
.map(|_| ctrl.tls_client_config.clone()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
vhosts.insert(
|
|
||||||
(address.clone(), 443),
|
|
||||||
ProxyTarget {
|
|
||||||
filter: AndFilter(
|
|
||||||
bind.net.clone(),
|
|
||||||
if private {
|
|
||||||
OrFilter(
|
|
||||||
IdFilter(public.gateway.clone()),
|
|
||||||
PublicFilter { public: false },
|
|
||||||
)
|
|
||||||
.into_dyn()
|
|
||||||
} else {
|
|
||||||
AndFilter(
|
|
||||||
IdFilter(public.gateway.clone()),
|
|
||||||
PublicFilter { public: true },
|
|
||||||
)
|
|
||||||
.into_dyn()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.into_dyn(),
|
|
||||||
acme: public.acme.clone(),
|
|
||||||
addr,
|
|
||||||
add_x_forwarded_headers: ssl
|
|
||||||
.add_x_forwarded_headers,
|
|
||||||
connect_ssl: connect_ssl
|
|
||||||
.clone()
|
|
||||||
.map(|_| ctrl.tls_client_config.clone()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
vhosts.insert(
|
|
||||||
(address.clone(), 443),
|
|
||||||
ProxyTarget {
|
|
||||||
filter: AndFilter(
|
|
||||||
bind.net.clone(),
|
|
||||||
PublicFilter { public: false },
|
|
||||||
)
|
|
||||||
.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()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let Some(public) = public {
|
|
||||||
vhosts.insert(
|
|
||||||
(address.clone(), external),
|
|
||||||
ProxyTarget {
|
|
||||||
filter: AndFilter(
|
|
||||||
bind.net.clone(),
|
|
||||||
if private {
|
|
||||||
OrFilter(
|
|
||||||
IdFilter(public.gateway.clone()),
|
|
||||||
PublicFilter { public: false },
|
|
||||||
)
|
|
||||||
.into_dyn()
|
|
||||||
} else {
|
|
||||||
IdFilter(public.gateway.clone())
|
|
||||||
.into_dyn()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.into_dyn(),
|
|
||||||
acme: public.acme.clone(),
|
|
||||||
addr,
|
|
||||||
add_x_forwarded_headers: ssl
|
|
||||||
.add_x_forwarded_headers,
|
|
||||||
connect_ssl: connect_ssl
|
|
||||||
.clone()
|
|
||||||
.map(|_| ctrl.tls_client_config.clone()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
vhosts.insert(
|
|
||||||
(address.clone(), external),
|
|
||||||
ProxyTarget {
|
|
||||||
filter: AndFilter(
|
|
||||||
bind.net.clone(),
|
|
||||||
PublicFilter { public: false },
|
|
||||||
)
|
|
||||||
.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()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if bind
|
|
||||||
.options
|
|
||||||
.secure
|
|
||||||
.map_or(true, |s| !(s.ssl && bind.options.add_ssl.is_some()))
|
|
||||||
{
|
|
||||||
let external = bind.net.assigned_port.or_not_found("assigned lan port")?;
|
|
||||||
forwards.insert(
|
|
||||||
external,
|
|
||||||
(
|
|
||||||
SocketAddrV4::new(self.ip, *port),
|
|
||||||
AndFilter(
|
|
||||||
SecureFilter {
|
|
||||||
secure: bind.options.secure.is_some(),
|
|
||||||
},
|
|
||||||
bind.net.clone(),
|
|
||||||
)
|
|
||||||
.into_dyn(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let mut bind_hostname_info: Vec<HostnameInfo> =
|
|
||||||
hostname_info.remove(port).unwrap_or_default();
|
|
||||||
for (gateway_id, info) in net_ifaces
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, info)| {
|
|
||||||
info.ip_info.as_ref().map_or(false, |i| {
|
|
||||||
!matches!(i.device_type, Some(NetworkInterfaceType::Bridge))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.filter(|(id, info)| bind.net.filter(id, info))
|
|
||||||
{
|
{
|
||||||
let gateway = GatewayInfo {
|
bind.addresses.possible.insert(HostnameInfo {
|
||||||
id: gateway_id.clone(),
|
gateway: gateway.clone(),
|
||||||
name: info
|
public: false,
|
||||||
.name
|
hostname: IpHostname::Local {
|
||||||
.clone()
|
value: InternedString::from_display(&{
|
||||||
.or_else(|| info.ip_info.as_ref().map(|i| i.name.clone()))
|
let hostname = &hostname;
|
||||||
.unwrap_or_else(|| gateway_id.clone().into()),
|
lazy_format!("{hostname}.local")
|
||||||
public: info.public(),
|
}),
|
||||||
};
|
port,
|
||||||
let port = bind.net.assigned_port.filter(|_| {
|
ssl_port: bind.net.assigned_ssl_port,
|
||||||
bind.options.secure.map_or(false, |s| {
|
},
|
||||||
!(s.ssl && bind.options.add_ssl.is_some()) || info.secure()
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
if !info.public()
|
}
|
||||||
&& info.ip_info.as_ref().map_or(false, |i| {
|
// Domain addresses
|
||||||
i.device_type != Some(NetworkInterfaceType::Wireguard)
|
for HostAddress {
|
||||||
})
|
address,
|
||||||
{
|
public,
|
||||||
bind_hostname_info.push(HostnameInfo::Ip {
|
private,
|
||||||
|
} in host_addresses.iter().cloned()
|
||||||
|
{
|
||||||
|
let private = private && !info.public();
|
||||||
|
let public =
|
||||||
|
public.as_ref().map_or(false, |p| &p.gateway == gateway_id);
|
||||||
|
if public || private {
|
||||||
|
let (domain_port, domain_ssl_port) = if bind
|
||||||
|
.options
|
||||||
|
.add_ssl
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |ssl| ssl.preferred_external_port == 443)
|
||||||
|
{
|
||||||
|
(None, Some(443))
|
||||||
|
} else {
|
||||||
|
(port, bind.net.assigned_ssl_port)
|
||||||
|
};
|
||||||
|
bind.addresses.possible.insert(HostnameInfo {
|
||||||
gateway: gateway.clone(),
|
gateway: gateway.clone(),
|
||||||
public: false,
|
public,
|
||||||
hostname: IpHostname::Local {
|
hostname: IpHostname::Domain {
|
||||||
value: InternedString::from_display(&{
|
value: address.clone(),
|
||||||
let hostname = &hostname;
|
port: domain_port,
|
||||||
lazy_format!("{hostname}.local")
|
ssl_port: domain_ssl_port,
|
||||||
}),
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// IP addresses
|
||||||
|
if let Some(ip_info) = &info.ip_info {
|
||||||
|
let public = info.public();
|
||||||
|
if let Some(wan_ip) = ip_info.wan_ip {
|
||||||
|
bind.addresses.possible.insert(HostnameInfo {
|
||||||
|
gateway: gateway.clone(),
|
||||||
|
public: true,
|
||||||
|
hostname: IpHostname::Ipv4 {
|
||||||
|
value: wan_ip,
|
||||||
port,
|
port,
|
||||||
ssl_port: bind.net.assigned_ssl_port,
|
ssl_port: bind.net.assigned_ssl_port,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for address in host.addresses() {
|
for ipnet in &ip_info.subnets {
|
||||||
if let HostAddress::Domain {
|
match ipnet {
|
||||||
address,
|
IpNet::V4(net) => {
|
||||||
public,
|
if !public {
|
||||||
private,
|
bind.addresses.possible.insert(HostnameInfo {
|
||||||
} = address
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
if public || private {
|
|
||||||
if bind
|
|
||||||
.options
|
|
||||||
.add_ssl
|
|
||||||
.as_ref()
|
|
||||||
.map_or(false, |ssl| ssl.preferred_external_port == 443)
|
|
||||||
{
|
|
||||||
bind_hostname_info.push(HostnameInfo::Ip {
|
|
||||||
gateway: gateway.clone(),
|
gateway: gateway.clone(),
|
||||||
public,
|
public,
|
||||||
hostname: IpHostname::Domain {
|
hostname: IpHostname::Ipv4 {
|
||||||
value: address.clone(),
|
value: net.addr(),
|
||||||
port: None,
|
|
||||||
ssl_port: Some(443),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
bind_hostname_info.push(HostnameInfo::Ip {
|
|
||||||
gateway: gateway.clone(),
|
|
||||||
public,
|
|
||||||
hostname: IpHostname::Domain {
|
|
||||||
value: address.clone(),
|
|
||||||
port,
|
port,
|
||||||
ssl_port: bind.net.assigned_ssl_port,
|
ssl_port: bind.net.assigned_ssl_port,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
IpNet::V6(net) => {
|
||||||
|
bind.addresses.possible.insert(HostnameInfo {
|
||||||
|
gateway: gateway.clone(),
|
||||||
|
public: public && !ipv6_is_local(net.addr()),
|
||||||
|
hostname: IpHostname::Ipv6 {
|
||||||
|
value: net.addr(),
|
||||||
|
scope_id: ip_info.scope_id,
|
||||||
|
port,
|
||||||
|
ssl_port: bind.net.assigned_ssl_port,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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::Ip {
|
|
||||||
gateway: gateway.clone(),
|
// ── Phase 2: Build controller entries from enabled addresses ──
|
||||||
public: true,
|
for (port, bind) in host.bindings.iter() {
|
||||||
hostname: IpHostname::Ipv4 {
|
if !bind.enabled {
|
||||||
value: wan_ip,
|
continue;
|
||||||
port,
|
}
|
||||||
ssl_port: bind.net.assigned_ssl_port,
|
if bind.net.assigned_port.is_none() && bind.net.assigned_ssl_port.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let enabled_addresses = bind.addresses.enabled();
|
||||||
|
let addr: SocketAddr = (self.ip, *port).into();
|
||||||
|
|
||||||
|
// SSL vhosts
|
||||||
|
if let Some(ssl) = &bind.options.add_ssl {
|
||||||
|
let connect_ssl = if let Some(alpn) = ssl.alpn.clone() {
|
||||||
|
Err(alpn)
|
||||||
|
} else if bind.options.secure.as_ref().map_or(false, |s| s.ssl) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(AlpnInfo::Reflect)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(assigned_ssl_port) = bind.net.assigned_ssl_port {
|
||||||
|
// Collect private IPs from enabled private addresses' gateways
|
||||||
|
let server_private_ips: BTreeSet<IpAddr> = enabled_addresses
|
||||||
|
.iter()
|
||||||
|
.filter(|a| !a.public)
|
||||||
|
.filter_map(|a| {
|
||||||
|
net_ifaces
|
||||||
|
.get(&a.gateway.id)
|
||||||
|
.and_then(|info| info.ip_info.as_ref())
|
||||||
|
})
|
||||||
|
.flat_map(|ip_info| ip_info.subnets.iter().map(|s| s.addr()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Server hostname vhosts (on assigned_ssl_port) — private only
|
||||||
|
if !server_private_ips.is_empty() {
|
||||||
|
for hostname in ctrl.server_hostnames.iter().cloned() {
|
||||||
|
vhosts.insert(
|
||||||
|
(hostname, assigned_ssl_port),
|
||||||
|
ProxyTarget {
|
||||||
|
public: BTreeSet::new(),
|
||||||
|
private: server_private_ips.clone(),
|
||||||
|
acme: None,
|
||||||
|
addr,
|
||||||
|
add_x_forwarded_headers: ssl.add_x_forwarded_headers,
|
||||||
|
connect_ssl: connect_ssl
|
||||||
|
.clone()
|
||||||
|
.map(|_| ctrl.tls_client_config.clone()),
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
for ipnet in &ip_info.subnets {
|
}
|
||||||
match ipnet {
|
}
|
||||||
IpNet::V4(net) => {
|
|
||||||
if !public {
|
// Domain vhosts: group by (domain, ssl_port), merge public/private sets
|
||||||
bind_hostname_info.push(HostnameInfo::Ip {
|
for addr_info in &enabled_addresses {
|
||||||
gateway: gateway.clone(),
|
if let IpHostname::Domain {
|
||||||
public,
|
value: domain,
|
||||||
hostname: IpHostname::Ipv4 {
|
ssl_port: Some(domain_ssl_port),
|
||||||
value: net.addr(),
|
..
|
||||||
port,
|
} = &addr_info.hostname
|
||||||
ssl_port: bind.net.assigned_ssl_port,
|
{
|
||||||
},
|
let key = (Some(domain.clone()), *domain_ssl_port);
|
||||||
});
|
let target = vhosts.entry(key).or_insert_with(|| ProxyTarget {
|
||||||
|
public: BTreeSet::new(),
|
||||||
|
private: BTreeSet::new(),
|
||||||
|
acme: host_addresses
|
||||||
|
.iter()
|
||||||
|
.find(|a| &a.address == domain)
|
||||||
|
.and_then(|a| a.public.as_ref())
|
||||||
|
.and_then(|p| p.acme.clone()),
|
||||||
|
addr,
|
||||||
|
add_x_forwarded_headers: ssl.add_x_forwarded_headers,
|
||||||
|
connect_ssl: connect_ssl
|
||||||
|
.clone()
|
||||||
|
.map(|_| ctrl.tls_client_config.clone()),
|
||||||
|
});
|
||||||
|
if addr_info.public {
|
||||||
|
target.public.insert(addr_info.gateway.id.clone());
|
||||||
|
} else {
|
||||||
|
// Add interface IPs for this gateway to private set
|
||||||
|
if let Some(info) = net_ifaces.get(&addr_info.gateway.id) {
|
||||||
|
if let Some(ip_info) = &info.ip_info {
|
||||||
|
for subnet in &ip_info.subnets {
|
||||||
|
target.private.insert(subnet.addr());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IpNet::V6(net) => {
|
|
||||||
bind_hostname_info.push(HostnameInfo::Ip {
|
|
||||||
gateway: gateway.clone(),
|
|
||||||
public: public && !ipv6_is_local(net.addr()),
|
|
||||||
hostname: IpHostname::Ipv6 {
|
|
||||||
value: net.addr(),
|
|
||||||
scope_id: ip_info.scope_id,
|
|
||||||
port,
|
|
||||||
ssl_port: bind.net.assigned_ssl_port,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hostname_info.insert(*port, bind_hostname_info);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
struct TorHostnamePorts {
|
// Non-SSL forwards
|
||||||
non_ssl: Option<u16>,
|
if bind
|
||||||
ssl: Option<u16>,
|
.options
|
||||||
}
|
.secure
|
||||||
let mut tor_hostname_ports = BTreeMap::<u16, TorHostnamePorts>::new();
|
.map_or(true, |s| !(s.ssl && bind.options.add_ssl.is_some()))
|
||||||
let mut tor_binds = OrdMap::<u16, SocketAddr>::new();
|
|
||||||
for (internal, info) in &host.bindings {
|
|
||||||
if !info.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tor_binds.insert(
|
|
||||||
info.options.preferred_external_port,
|
|
||||||
SocketAddr::from((self.ip, *internal)),
|
|
||||||
);
|
|
||||||
if let (Some(ssl), Some(ssl_internal)) =
|
|
||||||
(&info.options.add_ssl, info.net.assigned_ssl_port)
|
|
||||||
{
|
{
|
||||||
tor_binds.insert(
|
let external = bind.net.assigned_port.or_not_found("assigned lan port")?;
|
||||||
ssl.preferred_external_port,
|
let fwd_public: BTreeSet<GatewayId> = enabled_addresses
|
||||||
SocketAddr::from(([127, 0, 0, 1], ssl_internal)),
|
.iter()
|
||||||
);
|
.filter(|a| a.public)
|
||||||
tor_hostname_ports.insert(
|
.map(|a| a.gateway.id.clone())
|
||||||
*internal,
|
.collect();
|
||||||
TorHostnamePorts {
|
let fwd_private: BTreeSet<IpAddr> = enabled_addresses
|
||||||
non_ssl: Some(info.options.preferred_external_port)
|
.iter()
|
||||||
.filter(|p| *p != ssl.preferred_external_port),
|
.filter(|a| !a.public)
|
||||||
ssl: Some(ssl.preferred_external_port),
|
.filter_map(|a| {
|
||||||
},
|
net_ifaces
|
||||||
);
|
.get(&a.gateway.id)
|
||||||
} else {
|
.and_then(|i| i.ip_info.as_ref())
|
||||||
tor_hostname_ports.insert(
|
})
|
||||||
*internal,
|
.flat_map(|ip| ip.subnets.iter().map(|s| s.addr()))
|
||||||
TorHostnamePorts {
|
.collect();
|
||||||
non_ssl: Some(info.options.preferred_external_port),
|
forwards.insert(
|
||||||
ssl: None,
|
external,
|
||||||
},
|
(
|
||||||
|
SocketAddrV4::new(self.ip, *port),
|
||||||
|
ForwardRequirements {
|
||||||
|
public_gateways: fwd_public,
|
||||||
|
private_ips: fwd_private,
|
||||||
|
secure: bind.options.secure.is_some(),
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for tor_addr in host.onions.iter() {
|
// ── Phase 3: Reconcile ──
|
||||||
let key = peek
|
|
||||||
.as_private()
|
|
||||||
.as_key_store()
|
|
||||||
.as_onion()
|
|
||||||
.get_key(tor_addr)?;
|
|
||||||
tor.insert(key.onion_address(), (key, tor_binds.clone()));
|
|
||||||
for (internal, ports) in &tor_hostname_ports {
|
|
||||||
let mut bind_hostname_info = hostname_info.remove(internal).unwrap_or_default();
|
|
||||||
bind_hostname_info.push(HostnameInfo::Onion {
|
|
||||||
hostname: OnionHostname {
|
|
||||||
value: InternedString::from_display(tor_addr),
|
|
||||||
port: ports.non_ssl,
|
|
||||||
ssl_port: ports.ssl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
hostname_info.insert(*internal, bind_hostname_info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let all = binds
|
let all = binds
|
||||||
.forwards
|
.forwards
|
||||||
.keys()
|
.keys()
|
||||||
@@ -683,8 +547,8 @@ impl NetServiceData {
|
|||||||
.collect::<BTreeSet<_>>();
|
.collect::<BTreeSet<_>>();
|
||||||
for external in all {
|
for external in all {
|
||||||
let mut prev = binds.forwards.remove(&external);
|
let mut prev = binds.forwards.remove(&external);
|
||||||
if let Some((internal, filter)) = forwards.remove(&external) {
|
if let Some((internal, reqs)) = forwards.remove(&external) {
|
||||||
prev = prev.filter(|(i, f, _)| i == &internal && *f == filter);
|
prev = prev.filter(|(i, r, _)| i == &internal && *r == reqs);
|
||||||
binds.forwards.insert(
|
binds.forwards.insert(
|
||||||
external,
|
external,
|
||||||
if let Some(prev) = prev {
|
if let Some(prev) = prev {
|
||||||
@@ -692,11 +556,11 @@ impl NetServiceData {
|
|||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
internal,
|
internal,
|
||||||
filter.clone(),
|
reqs.clone(),
|
||||||
ctrl.forward
|
ctrl.forward
|
||||||
.add(
|
.add(
|
||||||
external,
|
external,
|
||||||
filter,
|
reqs,
|
||||||
internal,
|
internal,
|
||||||
net_ifaces
|
net_ifaces
|
||||||
.iter()
|
.iter()
|
||||||
@@ -763,40 +627,18 @@ impl NetServiceData {
|
|||||||
}
|
}
|
||||||
ctrl.dns.gc_private_domains(&rm)?;
|
ctrl.dns.gc_private_domains(&rm)?;
|
||||||
|
|
||||||
let all = binds
|
|
||||||
.tor
|
|
||||||
.keys()
|
|
||||||
.chain(tor.keys())
|
|
||||||
.cloned()
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
for onion in all {
|
|
||||||
let mut prev = binds.tor.remove(&onion);
|
|
||||||
if let Some((key, tor_binds)) = tor.remove(&onion).filter(|(_, b)| !b.is_empty()) {
|
|
||||||
prev = prev.filter(|(b, _)| b == &tor_binds);
|
|
||||||
binds.tor.insert(
|
|
||||||
onion,
|
|
||||||
if let Some(prev) = prev {
|
|
||||||
prev
|
|
||||||
} else {
|
|
||||||
let service = ctrl.tor.service(key)?;
|
|
||||||
let rcs = service.proxy_all(tor_binds.iter().map(|(k, v)| (*k, *v)));
|
|
||||||
(tor_binds, rcs)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if let Some((_, rc)) = prev {
|
|
||||||
drop(rc);
|
|
||||||
ctrl.tor.gc(Some(onion)).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = ctrl
|
let res = ctrl
|
||||||
.db
|
.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
host_for(db, self.id.as_ref(), &id)?
|
let bindings = host_for(db, self.id.as_ref(), &id)?.as_bindings_mut();
|
||||||
.as_hostname_info_mut()
|
for (port, bind) in host.bindings.0 {
|
||||||
.ser(&hostname_info)
|
if let Some(b) = bindings.as_idx_mut(&port) {
|
||||||
|
b.as_addresses_mut()
|
||||||
|
.as_possible_mut()
|
||||||
|
.ser(&bind.addresses.possible)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
res.result?;
|
res.result?;
|
||||||
|
|||||||
@@ -6,31 +6,21 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
use crate::{GatewayId, HostId, ServiceInterfaceId};
|
use crate::{GatewayId, HostId, ServiceInterfaceId};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[serde(rename_all_fields = "camelCase")]
|
pub struct HostnameInfo {
|
||||||
#[serde(tag = "kind")]
|
pub gateway: GatewayInfo,
|
||||||
pub enum HostnameInfo {
|
pub public: bool,
|
||||||
Ip {
|
pub hostname: IpHostname,
|
||||||
gateway: GatewayInfo,
|
|
||||||
public: bool,
|
|
||||||
hostname: IpHostname,
|
|
||||||
},
|
|
||||||
Onion {
|
|
||||||
hostname: OnionHostname,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
impl HostnameInfo {
|
impl HostnameInfo {
|
||||||
pub fn to_san_hostname(&self) -> InternedString {
|
pub fn to_san_hostname(&self) -> InternedString {
|
||||||
match self {
|
self.hostname.to_san_hostname()
|
||||||
Self::Ip { hostname, .. } => hostname.to_san_hostname(),
|
|
||||||
Self::Onion { hostname } => hostname.to_san_hostname(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct GatewayInfo {
|
pub struct GatewayInfo {
|
||||||
@@ -39,22 +29,7 @@ pub struct GatewayInfo {
|
|||||||
pub public: bool,
|
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")]
|
|
||||||
pub struct OnionHostname {
|
|
||||||
#[ts(type = "string")]
|
|
||||||
pub value: InternedString,
|
|
||||||
pub port: Option<u16>,
|
|
||||||
pub ssl_port: Option<u16>,
|
|
||||||
}
|
|
||||||
impl OnionHostname {
|
|
||||||
pub fn to_san_hostname(&self) -> InternedString {
|
|
||||||
self.value.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[serde(rename_all_fields = "camelCase")]
|
#[serde(rename_all_fields = "camelCase")]
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use socks5_impl::server::{AuthAdaptor, ClientConnection, Server};
|
|||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
|
||||||
use crate::HOST_IP;
|
use crate::HOST_IP;
|
||||||
use crate::net::tor::TorController;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::actor::background::BackgroundJobQueue;
|
use crate::util::actor::background::BackgroundJobQueue;
|
||||||
use crate::util::future::NonDetachingJoinHandle;
|
use crate::util::future::NonDetachingJoinHandle;
|
||||||
@@ -22,7 +21,7 @@ pub struct SocksController {
|
|||||||
_thread: NonDetachingJoinHandle<()>,
|
_thread: NonDetachingJoinHandle<()>,
|
||||||
}
|
}
|
||||||
impl SocksController {
|
impl SocksController {
|
||||||
pub fn new(listen: SocketAddr, tor: TorController) -> Result<Self, Error> {
|
pub fn new(listen: SocketAddr) -> Result<Self, Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
_thread: tokio::spawn(async move {
|
_thread: tokio::spawn(async move {
|
||||||
let auth: AuthAdaptor<()> = Arc::new(NoAuth);
|
let auth: AuthAdaptor<()> = Arc::new(NoAuth);
|
||||||
@@ -45,7 +44,6 @@ impl SocksController {
|
|||||||
loop {
|
loop {
|
||||||
match server.accept().await {
|
match server.accept().await {
|
||||||
Ok((stream, _)) => {
|
Ok((stream, _)) => {
|
||||||
let tor = tor.clone();
|
|
||||||
bg.add_job(async move {
|
bg.add_job(async move {
|
||||||
if let Err(e) = async {
|
if let Err(e) = async {
|
||||||
match stream
|
match stream
|
||||||
@@ -57,40 +55,6 @@ impl SocksController {
|
|||||||
.await
|
.await
|
||||||
.with_kind(ErrorKind::Network)?
|
.with_kind(ErrorKind::Network)?
|
||||||
{
|
{
|
||||||
ClientConnection::Connect(
|
|
||||||
reply,
|
|
||||||
Address::DomainAddress(domain, port),
|
|
||||||
) if domain.ends_with(".onion") => {
|
|
||||||
if let Ok(mut target) = tor
|
|
||||||
.connect_onion(&domain.parse()?, port)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let mut sock = reply
|
|
||||||
.reply(
|
|
||||||
Reply::Succeeded,
|
|
||||||
Address::unspecified(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Network)?;
|
|
||||||
tokio::io::copy_bidirectional(
|
|
||||||
&mut sock,
|
|
||||||
&mut target,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Network)?;
|
|
||||||
} else {
|
|
||||||
let mut sock = reply
|
|
||||||
.reply(
|
|
||||||
Reply::HostUnreachable,
|
|
||||||
Address::unspecified(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Network)?;
|
|
||||||
sock.shutdown()
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Network)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ClientConnection::Connect(reply, addr) => {
|
ClientConnection::Connect(reply, addr) => {
|
||||||
if let Ok(mut target) = match addr {
|
if let Ok(mut target) = match addr {
|
||||||
Address::DomainAddress(domain, port) => {
|
Address::DomainAddress(domain, port) => {
|
||||||
|
|||||||
@@ -1,964 +0,0 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::{Arc, Weak};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use arti_client::config::onion_service::OnionServiceConfigBuilder;
|
|
||||||
use arti_client::{TorClient, TorClientConfig};
|
|
||||||
use base64::Engine;
|
|
||||||
use clap::Parser;
|
|
||||||
use color_eyre::eyre::eyre;
|
|
||||||
use futures::{FutureExt, StreamExt};
|
|
||||||
use imbl_value::InternedString;
|
|
||||||
use itertools::Itertools;
|
|
||||||
use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio::sync::Notify;
|
|
||||||
use tor_cell::relaycell::msg::Connected;
|
|
||||||
use tor_hscrypto::pk::{HsId, HsIdKeypair};
|
|
||||||
use tor_hsservice::status::State as ArtiOnionServiceState;
|
|
||||||
use tor_hsservice::{HsNickname, RunningOnionService};
|
|
||||||
use tor_keymgr::config::ArtiKeystoreKind;
|
|
||||||
use tor_proto::client::stream::IncomingStreamRequest;
|
|
||||||
use tor_rtcompat::tokio::TokioRustlsRuntime;
|
|
||||||
use ts_rs::TS;
|
|
||||||
|
|
||||||
use crate::context::{CliContext, RpcContext};
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::util::actor::background::BackgroundJobQueue;
|
|
||||||
use crate::util::future::{NonDetachingJoinHandle, Until};
|
|
||||||
use crate::util::io::ReadWriter;
|
|
||||||
use crate::util::serde::{
|
|
||||||
BASE64, Base64, HandlerExtSerde, WithIoFormat, deserialize_from_str, display_serializable,
|
|
||||||
serialize_display,
|
|
||||||
};
|
|
||||||
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
|
|
||||||
|
|
||||||
const BOOTSTRAP_PROGRESS_TIMEOUT: Duration = Duration::from_secs(300);
|
|
||||||
const HS_BOOTSTRAP_TIMEOUT: Duration = Duration::from_secs(300);
|
|
||||||
const RETRY_COOLDOWN: Duration = Duration::from_secs(15);
|
|
||||||
const HEALTH_CHECK_FAILURE_ALLOWANCE: usize = 5;
|
|
||||||
const HEALTH_CHECK_COOLDOWN: Duration = Duration::from_secs(120);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct OnionAddress(pub HsId);
|
|
||||||
impl std::fmt::Display for OnionAddress {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
safelog::DisplayRedacted::fmt_unredacted(&self.0, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for OnionAddress {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(Self(
|
|
||||||
if s.ends_with(".onion") {
|
|
||||||
Cow::Borrowed(s)
|
|
||||||
} else {
|
|
||||||
Cow::Owned(format!("{s}.onion"))
|
|
||||||
}
|
|
||||||
.parse::<HsId>()
|
|
||||||
.with_kind(ErrorKind::Tor)?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Serialize for OnionAddress {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
serialize_display(self, serializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'de> Deserialize<'de> for OnionAddress {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
deserialize_from_str(deserializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialEq for OnionAddress {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.0.as_ref() == other.0.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Eq for OnionAddress {}
|
|
||||||
impl PartialOrd for OnionAddress {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
self.0.as_ref().partial_cmp(other.0.as_ref())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Ord for OnionAddress {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
self.0.as_ref().cmp(other.0.as_ref())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TorSecretKey(pub HsIdKeypair);
|
|
||||||
impl TorSecretKey {
|
|
||||||
pub fn onion_address(&self) -> OnionAddress {
|
|
||||||
OnionAddress(HsId::from(self.0.as_ref().public().to_bytes()))
|
|
||||||
}
|
|
||||||
pub fn from_bytes(bytes: [u8; 64]) -> Result<Self, Error> {
|
|
||||||
Ok(Self(
|
|
||||||
tor_llcrypto::pk::ed25519::ExpandedKeypair::from_secret_key_bytes(bytes)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
Error::new(
|
|
||||||
eyre!("{}", t!("net.tor.invalid-ed25519-key")),
|
|
||||||
ErrorKind::Tor,
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
pub fn generate() -> Self {
|
|
||||||
Self(
|
|
||||||
tor_llcrypto::pk::ed25519::ExpandedKeypair::from(
|
|
||||||
&tor_llcrypto::pk::ed25519::Keypair::generate(&mut rand::rng()),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Clone for TorSecretKey {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self(HsIdKeypair::from(
|
|
||||||
tor_llcrypto::pk::ed25519::ExpandedKeypair::from_secret_key_bytes(
|
|
||||||
self.0.as_ref().to_secret_key_bytes(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl std::fmt::Display for TorSecretKey {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
BASE64.encode(self.0.as_ref().to_secret_key_bytes())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for TorSecretKey {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Self::from_bytes(Base64::<[u8; 64]>::from_str(s)?.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Serialize for TorSecretKey {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
serialize_display(self, serializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'de> Deserialize<'de> for TorSecretKey {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
deserialize_from_str(deserializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Serialize)]
|
|
||||||
pub struct OnionStore(BTreeMap<OnionAddress, TorSecretKey>);
|
|
||||||
impl Map for OnionStore {
|
|
||||||
type Key = OnionAddress;
|
|
||||||
type Value = TorSecretKey;
|
|
||||||
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
|
||||||
Self::key_string(key)
|
|
||||||
}
|
|
||||||
fn key_string(key: &Self::Key) -> Result<imbl_value::InternedString, Error> {
|
|
||||||
Ok(InternedString::from_display(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl OnionStore {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
pub fn insert(&mut self, key: TorSecretKey) {
|
|
||||||
self.0.insert(key.onion_address(), key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Model<OnionStore> {
|
|
||||||
pub fn new_key(&mut self) -> Result<TorSecretKey, Error> {
|
|
||||||
let key = TorSecretKey::generate();
|
|
||||||
self.insert(&key.onion_address(), &key)?;
|
|
||||||
Ok(key)
|
|
||||||
}
|
|
||||||
pub fn insert_key(&mut self, key: &TorSecretKey) -> Result<(), Error> {
|
|
||||||
self.insert(&key.onion_address(), &key)
|
|
||||||
}
|
|
||||||
pub fn get_key(&self, address: &OnionAddress) -> Result<TorSecretKey, Error> {
|
|
||||||
self.as_idx(address)
|
|
||||||
.or_not_found(lazy_format!("private key for {address}"))?
|
|
||||||
.de()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl std::fmt::Debug for OnionStore {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
struct OnionStoreMap<'a>(&'a BTreeMap<OnionAddress, TorSecretKey>);
|
|
||||||
impl<'a> std::fmt::Debug for OnionStoreMap<'a> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct KeyFor(#[allow(unused)] OnionAddress);
|
|
||||||
let mut map = f.debug_map();
|
|
||||||
for (k, v) in self.0 {
|
|
||||||
map.key(k);
|
|
||||||
map.value(&KeyFor(v.onion_address()));
|
|
||||||
}
|
|
||||||
map.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.debug_tuple("OnionStore")
|
|
||||||
.field(&OnionStoreMap(&self.0))
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tor_api<C: Context>() -> ParentHandler<C> {
|
|
||||||
ParentHandler::new()
|
|
||||||
.subcommand(
|
|
||||||
"list-services",
|
|
||||||
from_fn_async(list_services)
|
|
||||||
.with_display_serializable()
|
|
||||||
.with_custom_display_fn(|handle, result| display_services(handle.params, result))
|
|
||||||
.with_about("about.display-tor-v3-onion-addresses")
|
|
||||||
.with_call_remote::<CliContext>(),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
"reset",
|
|
||||||
from_fn_async(reset)
|
|
||||||
.no_display()
|
|
||||||
.with_about("about.reset-tor-daemon")
|
|
||||||
.with_call_remote::<CliContext>(),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
"key",
|
|
||||||
key::<C>().with_about("about.manage-onion-service-key-store"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn key<C: Context>() -> ParentHandler<C> {
|
|
||||||
ParentHandler::new()
|
|
||||||
.subcommand(
|
|
||||||
"generate",
|
|
||||||
from_fn_async(generate_key)
|
|
||||||
.with_about("about.generate-onion-service-key-add-to-store")
|
|
||||||
.with_call_remote::<CliContext>(),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
"add",
|
|
||||||
from_fn_async(add_key)
|
|
||||||
.with_about("about.add-onion-service-key-to-store")
|
|
||||||
.with_call_remote::<CliContext>(),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
"list",
|
|
||||||
from_fn_async(list_keys)
|
|
||||||
.with_custom_display_fn(|_, res| {
|
|
||||||
for addr in res {
|
|
||||||
println!("{addr}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.with_about("about.list-onion-services-with-keys-in-store")
|
|
||||||
.with_call_remote::<CliContext>(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn generate_key(ctx: RpcContext) -> Result<OnionAddress, Error> {
|
|
||||||
ctx.db
|
|
||||||
.mutate(|db| {
|
|
||||||
Ok(db
|
|
||||||
.as_private_mut()
|
|
||||||
.as_key_store_mut()
|
|
||||||
.as_onion_mut()
|
|
||||||
.new_key()?
|
|
||||||
.onion_address())
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.result
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser)]
|
|
||||||
pub struct AddKeyParams {
|
|
||||||
#[arg(help = "help.arg.onion-secret-key")]
|
|
||||||
pub key: Base64<[u8; 64]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_key(
|
|
||||||
ctx: RpcContext,
|
|
||||||
AddKeyParams { key }: AddKeyParams,
|
|
||||||
) -> Result<OnionAddress, Error> {
|
|
||||||
let key = TorSecretKey::from_bytes(key.0)?;
|
|
||||||
ctx.db
|
|
||||||
.mutate(|db| {
|
|
||||||
db.as_private_mut()
|
|
||||||
.as_key_store_mut()
|
|
||||||
.as_onion_mut()
|
|
||||||
.insert_key(&key)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.result?;
|
|
||||||
Ok(key.onion_address())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<OnionAddress>, Error> {
|
|
||||||
ctx.db
|
|
||||||
.peek()
|
|
||||||
.await
|
|
||||||
.into_private()
|
|
||||||
.into_key_store()
|
|
||||||
.into_onion()
|
|
||||||
.keys()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[command(rename_all = "kebab-case")]
|
|
||||||
pub struct ResetParams {
|
|
||||||
#[arg(
|
|
||||||
name = "wipe-state",
|
|
||||||
short = 'w',
|
|
||||||
long = "wipe-state",
|
|
||||||
help = "help.arg.wipe-tor-state"
|
|
||||||
)]
|
|
||||||
wipe_state: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reset(ctx: RpcContext, ResetParams { wipe_state }: ResetParams) -> Result<(), Error> {
|
|
||||||
ctx.net_controller.tor.reset(wipe_state).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_services(
|
|
||||||
params: WithIoFormat<Empty>,
|
|
||||||
services: BTreeMap<OnionAddress, OnionServiceInfo>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
use prettytable::*;
|
|
||||||
|
|
||||||
if let Some(format) = params.format {
|
|
||||||
return display_serializable(format, services);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut table = Table::new();
|
|
||||||
table.add_row(row![bc => "ADDRESS", "STATE", "BINDINGS"]);
|
|
||||||
for (service, info) in services {
|
|
||||||
let row = row![
|
|
||||||
&service.to_string(),
|
|
||||||
&format!("{:?}", info.state),
|
|
||||||
&info
|
|
||||||
.bindings
|
|
||||||
.into_iter()
|
|
||||||
.map(|(port, addr)| lazy_format!("{port} -> {addr}"))
|
|
||||||
.join("; ")
|
|
||||||
];
|
|
||||||
table.add_row(row);
|
|
||||||
}
|
|
||||||
table.print_tty(false)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub enum OnionServiceState {
|
|
||||||
Shutdown,
|
|
||||||
Bootstrapping,
|
|
||||||
DegradedReachable,
|
|
||||||
DegradedUnreachable,
|
|
||||||
Running,
|
|
||||||
Recovering,
|
|
||||||
Broken,
|
|
||||||
}
|
|
||||||
impl From<ArtiOnionServiceState> for OnionServiceState {
|
|
||||||
fn from(value: ArtiOnionServiceState) -> Self {
|
|
||||||
match value {
|
|
||||||
ArtiOnionServiceState::Shutdown => Self::Shutdown,
|
|
||||||
ArtiOnionServiceState::Bootstrapping => Self::Bootstrapping,
|
|
||||||
ArtiOnionServiceState::DegradedReachable => Self::DegradedReachable,
|
|
||||||
ArtiOnionServiceState::DegradedUnreachable => Self::DegradedUnreachable,
|
|
||||||
ArtiOnionServiceState::Running => Self::Running,
|
|
||||||
ArtiOnionServiceState::Recovering => Self::Recovering,
|
|
||||||
ArtiOnionServiceState::Broken => Self::Broken,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct OnionServiceInfo {
|
|
||||||
pub state: OnionServiceState,
|
|
||||||
pub bindings: BTreeMap<u16, SocketAddr>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_services(
|
|
||||||
ctx: RpcContext,
|
|
||||||
_: Empty,
|
|
||||||
) -> Result<BTreeMap<OnionAddress, OnionServiceInfo>, Error> {
|
|
||||||
ctx.net_controller.tor.list_services().await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TorController(Arc<TorControllerInner>);
|
|
||||||
struct TorControllerInner {
|
|
||||||
client: Watch<(usize, TorClient<TokioRustlsRuntime>)>,
|
|
||||||
_bootstrapper: NonDetachingJoinHandle<()>,
|
|
||||||
services: SyncMutex<BTreeMap<OnionAddress, OnionService>>,
|
|
||||||
reset: Arc<Notify>,
|
|
||||||
}
|
|
||||||
impl TorController {
|
|
||||||
pub fn new() -> Result<Self, Error> {
|
|
||||||
let mut config = TorClientConfig::builder();
|
|
||||||
config
|
|
||||||
.storage()
|
|
||||||
.keystore()
|
|
||||||
.primary()
|
|
||||||
.kind(ArtiKeystoreKind::Ephemeral.into());
|
|
||||||
let client = Watch::new((
|
|
||||||
0,
|
|
||||||
TorClient::with_runtime(TokioRustlsRuntime::current()?)
|
|
||||||
.config(config.build().with_kind(ErrorKind::Tor)?)
|
|
||||||
.local_resource_timeout(Duration::from_secs(0))
|
|
||||||
.create_unbootstrapped()?,
|
|
||||||
));
|
|
||||||
let reset = Arc::new(Notify::new());
|
|
||||||
let bootstrapper_reset = reset.clone();
|
|
||||||
let bootstrapper_client = client.clone();
|
|
||||||
let bootstrapper = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let (epoch, client): (usize, _) = bootstrapper_client.read();
|
|
||||||
if let Err(e) = Until::new()
|
|
||||||
.with_async_fn(|| bootstrapper_reset.notified().map(Ok))
|
|
||||||
.run(async {
|
|
||||||
let mut events = client.bootstrap_events();
|
|
||||||
let bootstrap_fut =
|
|
||||||
client.bootstrap().map(|res| res.with_kind(ErrorKind::Tor));
|
|
||||||
let failure_fut = async {
|
|
||||||
let mut prev_frac = 0_f32;
|
|
||||||
let mut prev_inst = Instant::now();
|
|
||||||
while let Some(event) =
|
|
||||||
tokio::time::timeout(BOOTSTRAP_PROGRESS_TIMEOUT, events.next())
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Tor)?
|
|
||||||
{
|
|
||||||
if event.ready_for_traffic() {
|
|
||||||
return Ok::<_, Error>(());
|
|
||||||
}
|
|
||||||
let frac = event.as_frac();
|
|
||||||
if frac == prev_frac {
|
|
||||||
if prev_inst.elapsed() > BOOTSTRAP_PROGRESS_TIMEOUT {
|
|
||||||
return Err(Error::new(
|
|
||||||
eyre!(
|
|
||||||
"{}",
|
|
||||||
t!(
|
|
||||||
"net.tor.bootstrap-no-progress",
|
|
||||||
duration = crate::util::serde::Duration::from(
|
|
||||||
BOOTSTRAP_PROGRESS_TIMEOUT
|
|
||||||
)
|
|
||||||
.to_string()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
ErrorKind::Tor,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
prev_frac = frac;
|
|
||||||
prev_inst = Instant::now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
futures::future::pending().await
|
|
||||||
};
|
|
||||||
if let Err::<(), Error>(e) = tokio::select! {
|
|
||||||
res = bootstrap_fut => res,
|
|
||||||
res = failure_fut => res,
|
|
||||||
} {
|
|
||||||
tracing::error!(
|
|
||||||
"{}",
|
|
||||||
t!("net.tor.bootstrap-error", error = e.to_string())
|
|
||||||
);
|
|
||||||
tracing::debug!("{e:?}");
|
|
||||||
} else {
|
|
||||||
bootstrapper_client.send_modify(|_| ());
|
|
||||||
|
|
||||||
for _ in 0..HEALTH_CHECK_FAILURE_ALLOWANCE {
|
|
||||||
if let Err::<(), Error>(e) = async {
|
|
||||||
loop {
|
|
||||||
let (bg, mut runner) = BackgroundJobQueue::new();
|
|
||||||
runner
|
|
||||||
.run_while(async {
|
|
||||||
const PING_BUF_LEN: usize = 8;
|
|
||||||
let key = TorSecretKey::generate();
|
|
||||||
let onion = key.onion_address();
|
|
||||||
let (hs, stream) = client
|
|
||||||
.launch_onion_service_with_hsid(
|
|
||||||
OnionServiceConfigBuilder::default()
|
|
||||||
.nickname(
|
|
||||||
onion
|
|
||||||
.to_string()
|
|
||||||
.trim_end_matches(".onion")
|
|
||||||
.parse::<HsNickname>()
|
|
||||||
.with_kind(ErrorKind::Tor)?,
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
.with_kind(ErrorKind::Tor)?,
|
|
||||||
key.clone().0,
|
|
||||||
)
|
|
||||||
.with_kind(ErrorKind::Tor)?;
|
|
||||||
bg.add_job(async move {
|
|
||||||
if let Err(e) = async {
|
|
||||||
let mut stream =
|
|
||||||
tor_hsservice::handle_rend_requests(
|
|
||||||
stream,
|
|
||||||
);
|
|
||||||
while let Some(req) = stream.next().await {
|
|
||||||
let mut stream = req
|
|
||||||
.accept(Connected::new_empty())
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Tor)?;
|
|
||||||
let mut buf = [0; PING_BUF_LEN];
|
|
||||||
stream.read_exact(&mut buf).await?;
|
|
||||||
stream.write_all(&buf).await?;
|
|
||||||
stream.flush().await?;
|
|
||||||
stream.shutdown().await?;
|
|
||||||
}
|
|
||||||
Ok::<_, Error>(())
|
|
||||||
}
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!(
|
|
||||||
"{}",
|
|
||||||
t!(
|
|
||||||
"net.tor.health-error",
|
|
||||||
error = e.to_string()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
tracing::debug!("{e:?}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::time::timeout(HS_BOOTSTRAP_TIMEOUT, async {
|
|
||||||
let mut status = hs.status_events();
|
|
||||||
while let Some(status) = status.next().await {
|
|
||||||
if status.state().is_fully_reachable() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(Error::new(
|
|
||||||
eyre!(
|
|
||||||
"{}",
|
|
||||||
t!("net.tor.status-stream-ended")
|
|
||||||
),
|
|
||||||
ErrorKind::Tor,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Tor)??;
|
|
||||||
|
|
||||||
let mut stream = client
|
|
||||||
.connect((onion.to_string(), 8080))
|
|
||||||
.await?;
|
|
||||||
let mut ping_buf = [0; PING_BUF_LEN];
|
|
||||||
rand::fill(&mut ping_buf);
|
|
||||||
stream.write_all(&ping_buf).await?;
|
|
||||||
stream.flush().await?;
|
|
||||||
let mut ping_res = [0; PING_BUF_LEN];
|
|
||||||
stream.read_exact(&mut ping_res).await?;
|
|
||||||
ensure_code!(
|
|
||||||
ping_buf == ping_res,
|
|
||||||
ErrorKind::Tor,
|
|
||||||
"ping buffer mismatch"
|
|
||||||
);
|
|
||||||
stream.shutdown().await?;
|
|
||||||
|
|
||||||
Ok::<_, Error>(())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
tokio::time::sleep(HEALTH_CHECK_COOLDOWN).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!(
|
|
||||||
"{}",
|
|
||||||
t!("net.tor.client-health-error", error = e.to_string())
|
|
||||||
);
|
|
||||||
tracing::debug!("{e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tracing::error!(
|
|
||||||
"{}",
|
|
||||||
t!(
|
|
||||||
"net.tor.health-check-failed-recycling",
|
|
||||||
count = HEALTH_CHECK_FAILURE_ALLOWANCE
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!(
|
|
||||||
"{}",
|
|
||||||
t!("net.tor.bootstrapper-error", error = e.to_string())
|
|
||||||
);
|
|
||||||
tracing::debug!("{e:?}");
|
|
||||||
}
|
|
||||||
if let Err::<(), Error>(e) = async {
|
|
||||||
tokio::time::sleep(RETRY_COOLDOWN).await;
|
|
||||||
bootstrapper_client.send((
|
|
||||||
epoch.wrapping_add(1),
|
|
||||||
TorClient::with_runtime(TokioRustlsRuntime::current()?)
|
|
||||||
.config(config.build().with_kind(ErrorKind::Tor)?)
|
|
||||||
.local_resource_timeout(Duration::from_secs(0))
|
|
||||||
.create_unbootstrapped_async()
|
|
||||||
.await?,
|
|
||||||
));
|
|
||||||
tracing::debug!("TorClient recycled");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!(
|
|
||||||
"{}",
|
|
||||||
t!("net.tor.client-creation-error", error = e.to_string())
|
|
||||||
);
|
|
||||||
tracing::debug!("{e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into();
|
|
||||||
Ok(Self(Arc::new(TorControllerInner {
|
|
||||||
client,
|
|
||||||
_bootstrapper: bootstrapper,
|
|
||||||
services: SyncMutex::new(BTreeMap::new()),
|
|
||||||
reset,
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn service(&self, key: TorSecretKey) -> Result<OnionService, Error> {
|
|
||||||
self.0.services.mutate(|s| {
|
|
||||||
use std::collections::btree_map::Entry;
|
|
||||||
let addr = key.onion_address();
|
|
||||||
match s.entry(addr) {
|
|
||||||
Entry::Occupied(e) => Ok(e.get().clone()),
|
|
||||||
Entry::Vacant(e) => Ok(e
|
|
||||||
.insert(OnionService::launch(self.0.client.clone(), key)?)
|
|
||||||
.clone()),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn gc(&self, addr: Option<OnionAddress>) -> Result<(), Error> {
|
|
||||||
if let Some(addr) = addr {
|
|
||||||
if let Some(s) = self.0.services.mutate(|s| {
|
|
||||||
let rm = if let Some(s) = s.get(&addr) {
|
|
||||||
!s.gc()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
if rm { s.remove(&addr) } else { None }
|
|
||||||
}) {
|
|
||||||
s.shutdown().await
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for s in self.0.services.mutate(|s| {
|
|
||||||
let mut rm = Vec::new();
|
|
||||||
s.retain(|_, s| {
|
|
||||||
if s.gc() {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
rm.push(s.clone());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
rm
|
|
||||||
}) {
|
|
||||||
s.shutdown().await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reset(&self, wipe_state: bool) -> Result<(), Error> {
|
|
||||||
self.0.reset.notify_waiters();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_services(&self) -> Result<BTreeMap<OnionAddress, OnionServiceInfo>, Error> {
|
|
||||||
Ok(self
|
|
||||||
.0
|
|
||||||
.services
|
|
||||||
.peek(|s| s.iter().map(|(a, s)| (a.clone(), s.info())).collect()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect_onion(
|
|
||||||
&self,
|
|
||||||
addr: &OnionAddress,
|
|
||||||
port: u16,
|
|
||||||
) -> Result<Box<dyn ReadWriter + Unpin + Send + Sync + 'static>, Error> {
|
|
||||||
if let Some(target) = self.0.services.peek(|s| {
|
|
||||||
s.get(addr).and_then(|s| {
|
|
||||||
s.0.bindings.peek(|b| {
|
|
||||||
b.get(&port).and_then(|b| {
|
|
||||||
b.iter()
|
|
||||||
.find(|(_, rc)| rc.strong_count() > 0)
|
|
||||||
.map(|(a, _)| *a)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}) {
|
|
||||||
let tcp_stream = TcpStream::connect(target)
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Network)?;
|
|
||||||
if let Err(e) = socket2::SockRef::from(&tcp_stream).set_keepalive(true) {
|
|
||||||
tracing::error!(
|
|
||||||
"{}",
|
|
||||||
t!("net.tor.failed-to-set-tcp-keepalive", error = e.to_string())
|
|
||||||
);
|
|
||||||
tracing::debug!("{e:?}");
|
|
||||||
}
|
|
||||||
Ok(Box::new(tcp_stream))
|
|
||||||
} else {
|
|
||||||
let mut client = self.0.client.clone();
|
|
||||||
client
|
|
||||||
.wait_for(|(_, c)| c.bootstrap_status().ready_for_traffic())
|
|
||||||
.await;
|
|
||||||
let stream = client
|
|
||||||
.read()
|
|
||||||
.1
|
|
||||||
.connect((addr.to_string(), port))
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Tor)?;
|
|
||||||
Ok(Box::new(stream))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct OnionService(Arc<OnionServiceData>);
|
|
||||||
struct OnionServiceData {
|
|
||||||
service: Arc<SyncMutex<Option<Arc<RunningOnionService>>>>,
|
|
||||||
bindings: Arc<SyncRwLock<BTreeMap<u16, BTreeMap<SocketAddr, Weak<()>>>>>,
|
|
||||||
_thread: NonDetachingJoinHandle<()>,
|
|
||||||
}
|
|
||||||
impl OnionService {
|
|
||||||
fn launch(
|
|
||||||
mut client: Watch<(usize, TorClient<TokioRustlsRuntime>)>,
|
|
||||||
key: TorSecretKey,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let service = Arc::new(SyncMutex::new(None));
|
|
||||||
let bindings = Arc::new(SyncRwLock::new(BTreeMap::<
|
|
||||||
u16,
|
|
||||||
BTreeMap<SocketAddr, Weak<()>>,
|
|
||||||
>::new()));
|
|
||||||
Ok(Self(Arc::new(OnionServiceData {
|
|
||||||
service: service.clone(),
|
|
||||||
bindings: bindings.clone(),
|
|
||||||
_thread: tokio::spawn(async move {
|
|
||||||
let (bg, mut runner) = BackgroundJobQueue::new();
|
|
||||||
runner
|
|
||||||
.run_while(async {
|
|
||||||
loop {
|
|
||||||
if let Err(e) = async {
|
|
||||||
client.wait_for(|(_,c)| c.bootstrap_status().ready_for_traffic()).await;
|
|
||||||
let epoch = client.peek(|(e, c)| {
|
|
||||||
ensure_code!(c.bootstrap_status().ready_for_traffic(), ErrorKind::Tor, "TorClient recycled");
|
|
||||||
Ok::<_, Error>(*e)
|
|
||||||
})?;
|
|
||||||
let addr = key.onion_address();
|
|
||||||
let (new_service, stream) = client.peek(|(_, c)| {
|
|
||||||
c.launch_onion_service_with_hsid(
|
|
||||||
OnionServiceConfigBuilder::default()
|
|
||||||
.nickname(
|
|
||||||
addr
|
|
||||||
.to_string()
|
|
||||||
.trim_end_matches(".onion")
|
|
||||||
.parse::<HsNickname>()
|
|
||||||
.with_kind(ErrorKind::Tor)?,
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
.with_kind(ErrorKind::Tor)?,
|
|
||||||
key.clone().0,
|
|
||||||
)
|
|
||||||
.with_kind(ErrorKind::Tor)
|
|
||||||
})?;
|
|
||||||
let mut status_stream = new_service.status_events();
|
|
||||||
let mut status = new_service.status();
|
|
||||||
if status.state().is_fully_reachable() {
|
|
||||||
tracing::debug!("{addr} is fully reachable");
|
|
||||||
} else {
|
|
||||||
tracing::debug!("{addr} is not fully reachable");
|
|
||||||
}
|
|
||||||
bg.add_job(async move {
|
|
||||||
while let Some(new_status) = status_stream.next().await {
|
|
||||||
if status.state().is_fully_reachable() && !new_status.state().is_fully_reachable() {
|
|
||||||
tracing::debug!("{addr} is no longer fully reachable");
|
|
||||||
} else if !status.state().is_fully_reachable() && new_status.state().is_fully_reachable() {
|
|
||||||
tracing::debug!("{addr} is now fully reachable");
|
|
||||||
}
|
|
||||||
status = new_status;
|
|
||||||
// TODO: health daemon?
|
|
||||||
}
|
|
||||||
});
|
|
||||||
service.replace(Some(new_service));
|
|
||||||
let mut stream = tor_hsservice::handle_rend_requests(stream);
|
|
||||||
while let Some(req) = tokio::select! {
|
|
||||||
req = stream.next() => req,
|
|
||||||
_ = client.wait_for(|(e, _)| *e != epoch) => None
|
|
||||||
} {
|
|
||||||
bg.add_job({
|
|
||||||
let bg = bg.clone();
|
|
||||||
let bindings = bindings.clone();
|
|
||||||
async move {
|
|
||||||
if let Err(e) = async {
|
|
||||||
let IncomingStreamRequest::Begin(begin) =
|
|
||||||
req.request()
|
|
||||||
else {
|
|
||||||
return req
|
|
||||||
.reject(tor_cell::relaycell::msg::End::new_with_reason(
|
|
||||||
tor_cell::relaycell::msg::EndReason::DONE,
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Tor);
|
|
||||||
};
|
|
||||||
let Some(target) = bindings.peek(|b| {
|
|
||||||
b.get(&begin.port()).and_then(|a| {
|
|
||||||
a.iter()
|
|
||||||
.find(|(_, rc)| rc.strong_count() > 0)
|
|
||||||
.map(|(addr, _)| *addr)
|
|
||||||
})
|
|
||||||
}) else {
|
|
||||||
return req
|
|
||||||
.reject(tor_cell::relaycell::msg::End::new_with_reason(
|
|
||||||
tor_cell::relaycell::msg::EndReason::DONE,
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Tor);
|
|
||||||
};
|
|
||||||
bg.add_job(async move {
|
|
||||||
if let Err(e) = async {
|
|
||||||
let mut outgoing =
|
|
||||||
TcpStream::connect(target)
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Network)?;
|
|
||||||
if let Err(e) = socket2::SockRef::from(&outgoing).set_keepalive(true) {
|
|
||||||
tracing::error!("{}", t!("net.tor.failed-to-set-tcp-keepalive", error = e.to_string()));
|
|
||||||
tracing::debug!("{e:?}");
|
|
||||||
}
|
|
||||||
let mut incoming = req
|
|
||||||
.accept(Connected::new_empty())
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Tor)?;
|
|
||||||
if let Err(e) =
|
|
||||||
tokio::io::copy_bidirectional(
|
|
||||||
&mut outgoing,
|
|
||||||
&mut incoming,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::trace!("Tor Stream Error: {e}");
|
|
||||||
tracing::trace!("{e:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<_, Error>(())
|
|
||||||
}
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::trace!("Tor Stream Error: {e}");
|
|
||||||
tracing::trace!("{e:?}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok::<_, Error>(())
|
|
||||||
}
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::trace!("Tor Request Error: {e}");
|
|
||||||
tracing::trace!("{e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok::<_, Error>(())
|
|
||||||
}
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!("{}", t!("net.tor.client-error", error = e.to_string()));
|
|
||||||
tracing::debug!("{e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
.into(),
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn proxy_all<Rcs: FromIterator<Arc<()>>>(
|
|
||||||
&self,
|
|
||||||
bindings: impl IntoIterator<Item = (u16, SocketAddr)>,
|
|
||||||
) -> Result<Rcs, Error> {
|
|
||||||
Ok(self.0.bindings.mutate(|b| {
|
|
||||||
bindings
|
|
||||||
.into_iter()
|
|
||||||
.map(|(port, target)| {
|
|
||||||
let entry = b.entry(port).or_default().entry(target).or_default();
|
|
||||||
if let Some(rc) = entry.upgrade() {
|
|
||||||
rc
|
|
||||||
} else {
|
|
||||||
let rc = Arc::new(());
|
|
||||||
*entry = Arc::downgrade(&rc);
|
|
||||||
rc
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gc(&self) -> bool {
|
|
||||||
self.0.bindings.mutate(|b| {
|
|
||||||
b.retain(|_, targets| {
|
|
||||||
targets.retain(|_, rc| rc.strong_count() > 0);
|
|
||||||
!targets.is_empty()
|
|
||||||
});
|
|
||||||
!b.is_empty()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn shutdown(self) -> Result<(), Error> {
|
|
||||||
self.0.service.replace(None);
|
|
||||||
self.0._thread.abort();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn state(&self) -> OnionServiceState {
|
|
||||||
self.0
|
|
||||||
.service
|
|
||||||
.peek(|s| s.as_ref().map(|s| s.status().state().into()))
|
|
||||||
.unwrap_or(OnionServiceState::Bootstrapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn info(&self) -> OnionServiceInfo {
|
|
||||||
OnionServiceInfo {
|
|
||||||
state: self.state(),
|
|
||||||
bindings: self.0.bindings.peek(|b| {
|
|
||||||
b.iter()
|
|
||||||
.filter_map(|(port, b)| {
|
|
||||||
b.iter()
|
|
||||||
.find(|(_, rc)| rc.strong_count() > 0)
|
|
||||||
.map(|(addr, _)| (*port, *addr))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
|||||||
#[cfg(feature = "arti")]
|
|
||||||
mod arti;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "arti"))]
|
|
||||||
mod ctor;
|
|
||||||
|
|
||||||
#[cfg(feature = "arti")]
|
|
||||||
pub use arti::{OnionAddress, OnionStore, TorController, TorSecretKey, tor_api};
|
|
||||||
#[cfg(not(feature = "arti"))]
|
|
||||||
pub use ctor::{OnionAddress, OnionStore, TorController, TorSecretKey, tor_api};
|
|
||||||
@@ -8,7 +8,7 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
use crate::GatewayId;
|
use crate::GatewayId;
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType};
|
use crate::db::model::public::{GatewayType, NetworkInterfaceInfo, NetworkInterfaceType};
|
||||||
use crate::net::host::all_hosts;
|
use crate::net::host::all_hosts;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
@@ -32,14 +32,19 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct AddTunnelParams {
|
pub struct AddTunnelParams {
|
||||||
#[arg(help = "help.arg.tunnel-name")]
|
#[arg(help = "help.arg.tunnel-name")]
|
||||||
name: InternedString,
|
name: InternedString,
|
||||||
#[arg(help = "help.arg.wireguard-config")]
|
#[arg(help = "help.arg.wireguard-config")]
|
||||||
config: String,
|
config: String,
|
||||||
#[arg(help = "help.arg.is-public")]
|
#[arg(help = "help.arg.gateway-type")]
|
||||||
public: bool,
|
#[serde(default, rename = "type")]
|
||||||
|
gateway_type: Option<GatewayType>,
|
||||||
|
#[arg(help = "help.arg.set-as-default-outbound")]
|
||||||
|
#[serde(default)]
|
||||||
|
set_as_default_outbound: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sanitize_config(config: &str) -> String {
|
fn sanitize_config(config: &str) -> String {
|
||||||
@@ -64,7 +69,8 @@ pub async fn add_tunnel(
|
|||||||
AddTunnelParams {
|
AddTunnelParams {
|
||||||
name,
|
name,
|
||||||
config,
|
config,
|
||||||
public,
|
gateway_type,
|
||||||
|
set_as_default_outbound,
|
||||||
}: AddTunnelParams,
|
}: AddTunnelParams,
|
||||||
) -> Result<GatewayId, Error> {
|
) -> Result<GatewayId, Error> {
|
||||||
let ifaces = ctx.net_controller.net_iface.watcher.subscribe();
|
let ifaces = ctx.net_controller.net_iface.watcher.subscribe();
|
||||||
@@ -76,9 +82,10 @@ pub async fn add_tunnel(
|
|||||||
iface.clone(),
|
iface.clone(),
|
||||||
NetworkInterfaceInfo {
|
NetworkInterfaceInfo {
|
||||||
name: Some(name),
|
name: Some(name),
|
||||||
public: Some(public),
|
public: None,
|
||||||
secure: None,
|
secure: None,
|
||||||
ip_info: None,
|
ip_info: None,
|
||||||
|
gateway_type,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -120,6 +127,19 @@ pub async fn add_tunnel(
|
|||||||
|
|
||||||
sub.recv().await;
|
sub.recv().await;
|
||||||
|
|
||||||
|
if set_as_default_outbound {
|
||||||
|
ctx.db
|
||||||
|
.mutate(|db| {
|
||||||
|
db.as_public_mut()
|
||||||
|
.as_server_info_mut()
|
||||||
|
.as_network_mut()
|
||||||
|
.as_default_outbound_mut()
|
||||||
|
.ser(&Some(iface.clone()))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.result?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(iface)
|
Ok(iface)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,8 +195,12 @@ pub async fn remove_tunnel(
|
|||||||
let host = host?;
|
let host = host?;
|
||||||
host.as_bindings_mut().mutate(|b| {
|
host.as_bindings_mut().mutate(|b| {
|
||||||
Ok(b.values_mut().for_each(|v| {
|
Ok(b.values_mut().for_each(|v| {
|
||||||
v.net.private_disabled.remove(&id);
|
v.addresses
|
||||||
v.net.public_enabled.remove(&id);
|
.private_disabled
|
||||||
|
.retain(|h| h.gateway.id != id);
|
||||||
|
v.addresses
|
||||||
|
.public_enabled
|
||||||
|
.retain(|h| h.gateway.id != id);
|
||||||
}))
|
}))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr, SocketAddrV6};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use std::task::{Poll, ready};
|
use std::task::{Poll, ready};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use async_acme::acme::ACME_TLS_ALPN_NAME;
|
use async_acme::acme::ACME_TLS_ALPN_NAME;
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
|
use imbl::OrdMap;
|
||||||
use imbl_value::{InOMap, InternedString};
|
use imbl_value::{InOMap, InternedString};
|
||||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn};
|
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio_rustls::TlsConnector;
|
use tokio_rustls::TlsConnector;
|
||||||
use tokio_rustls::rustls::crypto::CryptoProvider;
|
use tokio_rustls::rustls::crypto::CryptoProvider;
|
||||||
use tokio_rustls::rustls::pki_types::ServerName;
|
use tokio_rustls::rustls::pki_types::ServerName;
|
||||||
@@ -23,28 +23,28 @@ use tracing::instrument;
|
|||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use visit_rs::Visit;
|
use visit_rs::Visit;
|
||||||
|
|
||||||
use crate::ResultExt;
|
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
use crate::db::model::Database;
|
use crate::db::model::Database;
|
||||||
use crate::db::model::public::AcmeSettings;
|
use crate::db::model::public::{AcmeSettings, NetworkInterfaceInfo};
|
||||||
use crate::db::{DbAccessByKey, DbAccessMut};
|
use crate::db::{DbAccessByKey, DbAccessMut};
|
||||||
use crate::net::acme::{
|
use crate::net::acme::{
|
||||||
AcmeCertStore, AcmeProvider, AcmeTlsAlpnCache, AcmeTlsHandler, GetAcmeProvider,
|
AcmeCertStore, AcmeProvider, AcmeTlsAlpnCache, AcmeTlsHandler, GetAcmeProvider,
|
||||||
};
|
};
|
||||||
use crate::net::gateway::{
|
use crate::net::gateway::{
|
||||||
AnyFilter, BindTcp, DynInterfaceFilter, GatewayInfo, InterfaceFilter,
|
GatewayInfo, NetworkInterfaceController, NetworkInterfaceListenerAcceptMetadata,
|
||||||
NetworkInterfaceController, NetworkInterfaceListener,
|
|
||||||
};
|
};
|
||||||
use crate::net::ssl::{CertStore, RootCaTlsHandler};
|
use crate::net::ssl::{CertStore, RootCaTlsHandler};
|
||||||
use crate::net::tls::{
|
use crate::net::tls::{
|
||||||
ChainedHandler, TlsHandlerWrapper, TlsListener, TlsMetadata, WrapTlsHandler,
|
ChainedHandler, TlsHandlerWrapper, TlsListener, TlsMetadata, WrapTlsHandler,
|
||||||
};
|
};
|
||||||
|
use crate::net::utils::ipv6_is_link_local;
|
||||||
use crate::net::web_server::{Accept, AcceptStream, ExtractVisitor, TcpMetadata, extract};
|
use crate::net::web_server::{Accept, AcceptStream, ExtractVisitor, TcpMetadata, extract};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::collections::EqSet;
|
use crate::util::collections::EqSet;
|
||||||
use crate::util::future::{NonDetachingJoinHandle, WeakFuture};
|
use crate::util::future::{NonDetachingJoinHandle, WeakFuture};
|
||||||
use crate::util::serde::{HandlerExtSerde, MaybeUtf8String, display_serializable};
|
use crate::util::serde::{HandlerExtSerde, MaybeUtf8String, display_serializable};
|
||||||
use crate::util::sync::{SyncMutex, Watch};
|
use crate::util::sync::{SyncMutex, Watch};
|
||||||
|
use crate::{GatewayId, ResultExt};
|
||||||
|
|
||||||
pub fn vhost_api<C: Context>() -> ParentHandler<C> {
|
pub fn vhost_api<C: Context>() -> ParentHandler<C> {
|
||||||
ParentHandler::new().subcommand(
|
ParentHandler::new().subcommand(
|
||||||
@@ -93,7 +93,7 @@ pub struct VHostController {
|
|||||||
interfaces: Arc<NetworkInterfaceController>,
|
interfaces: Arc<NetworkInterfaceController>,
|
||||||
crypto_provider: Arc<CryptoProvider>,
|
crypto_provider: Arc<CryptoProvider>,
|
||||||
acme_cache: AcmeTlsAlpnCache,
|
acme_cache: AcmeTlsAlpnCache,
|
||||||
servers: SyncMutex<BTreeMap<u16, VHostServer<NetworkInterfaceListener>>>,
|
servers: SyncMutex<BTreeMap<u16, VHostServer<VHostBindListener>>>,
|
||||||
}
|
}
|
||||||
impl VHostController {
|
impl VHostController {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@@ -114,14 +114,22 @@ impl VHostController {
|
|||||||
&self,
|
&self,
|
||||||
hostname: Option<InternedString>,
|
hostname: Option<InternedString>,
|
||||||
external: u16,
|
external: u16,
|
||||||
target: DynVHostTarget<NetworkInterfaceListener>,
|
target: DynVHostTarget<VHostBindListener>,
|
||||||
) -> Result<Arc<()>, Error> {
|
) -> Result<Arc<()>, Error> {
|
||||||
self.servers.mutate(|writable| {
|
self.servers.mutate(|writable| {
|
||||||
let server = if let Some(server) = writable.remove(&external) {
|
let server = if let Some(server) = writable.remove(&external) {
|
||||||
server
|
server
|
||||||
} else {
|
} else {
|
||||||
|
let bind_reqs = Watch::new(VHostBindRequirements::default());
|
||||||
|
let listener = VHostBindListener {
|
||||||
|
ip_info: self.interfaces.watcher.subscribe(),
|
||||||
|
port: external,
|
||||||
|
bind_reqs: bind_reqs.clone_unseen(),
|
||||||
|
listeners: BTreeMap::new(),
|
||||||
|
};
|
||||||
VHostServer::new(
|
VHostServer::new(
|
||||||
self.interfaces.watcher.bind(BindTcp, external)?,
|
listener,
|
||||||
|
bind_reqs,
|
||||||
self.db.clone(),
|
self.db.clone(),
|
||||||
self.crypto_provider.clone(),
|
self.crypto_provider.clone(),
|
||||||
self.acme_cache.clone(),
|
self.acme_cache.clone(),
|
||||||
@@ -173,6 +181,143 @@ impl VHostController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Union of all ProxyTargets' bind requirements for a VHostServer.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct VHostBindRequirements {
|
||||||
|
pub public_gateways: BTreeSet<GatewayId>,
|
||||||
|
pub private_ips: BTreeSet<IpAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_bind_reqs<A: Accept + 'static>(mapping: &Mapping<A>) -> VHostBindRequirements {
|
||||||
|
let mut reqs = VHostBindRequirements::default();
|
||||||
|
for (_, targets) in mapping {
|
||||||
|
for (target, rc) in targets {
|
||||||
|
if rc.strong_count() > 0 {
|
||||||
|
let (pub_gw, priv_ip) = target.0.bind_requirements();
|
||||||
|
reqs.public_gateways.extend(pub_gw);
|
||||||
|
reqs.private_ips.extend(priv_ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reqs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listener that manages its own TCP listeners with IP-level precision.
|
||||||
|
/// Binds ALL IPs of public gateways and ONLY matching private IPs.
|
||||||
|
pub struct VHostBindListener {
|
||||||
|
ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
||||||
|
port: u16,
|
||||||
|
bind_reqs: Watch<VHostBindRequirements>,
|
||||||
|
listeners: BTreeMap<SocketAddr, (TcpListener, GatewayInfo)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_vhost_listeners(
|
||||||
|
listeners: &mut BTreeMap<SocketAddr, (TcpListener, GatewayInfo)>,
|
||||||
|
port: u16,
|
||||||
|
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
||||||
|
reqs: &VHostBindRequirements,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut keep = BTreeSet::<SocketAddr>::new();
|
||||||
|
for (gw_id, info) in ip_info {
|
||||||
|
if let Some(ip_info) = &info.ip_info {
|
||||||
|
for ipnet in &ip_info.subnets {
|
||||||
|
let ip = ipnet.addr();
|
||||||
|
let should_bind =
|
||||||
|
reqs.public_gateways.contains(gw_id) || reqs.private_ips.contains(&ip);
|
||||||
|
if should_bind {
|
||||||
|
let addr = match ip {
|
||||||
|
IpAddr::V6(ip6) => SocketAddrV6::new(
|
||||||
|
ip6,
|
||||||
|
port,
|
||||||
|
0,
|
||||||
|
if ipv6_is_link_local(ip6) {
|
||||||
|
ip_info.scope_id
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
ip => SocketAddr::new(ip, port),
|
||||||
|
};
|
||||||
|
keep.insert(addr);
|
||||||
|
if let Some((_, existing_info)) = listeners.get_mut(&addr) {
|
||||||
|
*existing_info = GatewayInfo {
|
||||||
|
id: gw_id.clone(),
|
||||||
|
info: info.clone(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let tcp = TcpListener::from_std(
|
||||||
|
mio::net::TcpListener::bind(addr)
|
||||||
|
.with_kind(ErrorKind::Network)?
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.with_kind(ErrorKind::Network)?;
|
||||||
|
listeners.insert(
|
||||||
|
addr,
|
||||||
|
(
|
||||||
|
tcp,
|
||||||
|
GatewayInfo {
|
||||||
|
id: gw_id.clone(),
|
||||||
|
info: info.clone(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listeners.retain(|key, _| keep.contains(key));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Accept for VHostBindListener {
|
||||||
|
type Metadata = NetworkInterfaceListenerAcceptMetadata;
|
||||||
|
fn poll_accept(
|
||||||
|
&mut self,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
|
||||||
|
// Update listeners when ip_info or bind_reqs change
|
||||||
|
while self.ip_info.poll_changed(cx).is_ready()
|
||||||
|
|| self.bind_reqs.poll_changed(cx).is_ready()
|
||||||
|
{
|
||||||
|
let reqs = self.bind_reqs.read_and_mark_seen();
|
||||||
|
let listeners = &mut self.listeners;
|
||||||
|
let port = self.port;
|
||||||
|
self.ip_info.peek_and_mark_seen(|ip_info| {
|
||||||
|
update_vhost_listeners(listeners, port, ip_info, &reqs)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll each listener for incoming connections
|
||||||
|
for (&addr, (listener, gw_info)) in &self.listeners {
|
||||||
|
match listener.poll_accept(cx) {
|
||||||
|
Poll::Ready(Ok((stream, peer_addr))) => {
|
||||||
|
if let Err(e) = socket2::SockRef::from(&stream).set_keepalive(true) {
|
||||||
|
tracing::error!("Failed to set tcp keepalive: {e}");
|
||||||
|
tracing::debug!("{e:?}");
|
||||||
|
}
|
||||||
|
return Poll::Ready(Ok((
|
||||||
|
NetworkInterfaceListenerAcceptMetadata {
|
||||||
|
inner: TcpMetadata {
|
||||||
|
local_addr: addr,
|
||||||
|
peer_addr,
|
||||||
|
},
|
||||||
|
info: gw_info.clone(),
|
||||||
|
},
|
||||||
|
Box::pin(stream),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Poll::Ready(Err(e)) => {
|
||||||
|
tracing::trace!("VHostBindListener accept error on {addr}: {e}");
|
||||||
|
}
|
||||||
|
Poll::Pending => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait VHostTarget<A: Accept>: std::fmt::Debug + Eq {
|
pub trait VHostTarget<A: Accept>: std::fmt::Debug + Eq {
|
||||||
type PreprocessRes: Send + 'static;
|
type PreprocessRes: Send + 'static;
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
@@ -182,6 +327,10 @@ pub trait VHostTarget<A: Accept>: std::fmt::Debug + Eq {
|
|||||||
fn acme(&self) -> Option<&AcmeProvider> {
|
fn acme(&self) -> Option<&AcmeProvider> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
/// Returns (public_gateways, private_ips) this target needs the listener to bind on.
|
||||||
|
fn bind_requirements(&self) -> (BTreeSet<GatewayId>, BTreeSet<IpAddr>) {
|
||||||
|
(BTreeSet::new(), BTreeSet::new())
|
||||||
|
}
|
||||||
fn preprocess<'a>(
|
fn preprocess<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
prev: ServerConfig,
|
prev: ServerConfig,
|
||||||
@@ -200,6 +349,7 @@ pub trait VHostTarget<A: Accept>: std::fmt::Debug + Eq {
|
|||||||
pub trait DynVHostTargetT<A: Accept>: std::fmt::Debug + Any {
|
pub trait DynVHostTargetT<A: Accept>: std::fmt::Debug + Any {
|
||||||
fn filter(&self, metadata: &<A as Accept>::Metadata) -> bool;
|
fn filter(&self, metadata: &<A as Accept>::Metadata) -> bool;
|
||||||
fn acme(&self) -> Option<&AcmeProvider>;
|
fn acme(&self) -> Option<&AcmeProvider>;
|
||||||
|
fn bind_requirements(&self) -> (BTreeSet<GatewayId>, BTreeSet<IpAddr>);
|
||||||
fn preprocess<'a>(
|
fn preprocess<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
prev: ServerConfig,
|
prev: ServerConfig,
|
||||||
@@ -224,6 +374,9 @@ impl<A: Accept, T: VHostTarget<A> + 'static> DynVHostTargetT<A> for T {
|
|||||||
fn acme(&self) -> Option<&AcmeProvider> {
|
fn acme(&self) -> Option<&AcmeProvider> {
|
||||||
VHostTarget::acme(self)
|
VHostTarget::acme(self)
|
||||||
}
|
}
|
||||||
|
fn bind_requirements(&self) -> (BTreeSet<GatewayId>, BTreeSet<IpAddr>) {
|
||||||
|
VHostTarget::bind_requirements(self)
|
||||||
|
}
|
||||||
fn preprocess<'a>(
|
fn preprocess<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
prev: ServerConfig,
|
prev: ServerConfig,
|
||||||
@@ -301,7 +454,8 @@ impl<A: Accept + 'static> Preprocessed<A> {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ProxyTarget {
|
pub struct ProxyTarget {
|
||||||
pub filter: DynInterfaceFilter,
|
pub public: BTreeSet<GatewayId>,
|
||||||
|
pub private: BTreeSet<IpAddr>,
|
||||||
pub acme: Option<AcmeProvider>,
|
pub acme: Option<AcmeProvider>,
|
||||||
pub addr: SocketAddr,
|
pub addr: SocketAddr,
|
||||||
pub add_x_forwarded_headers: bool,
|
pub add_x_forwarded_headers: bool,
|
||||||
@@ -309,7 +463,8 @@ pub struct ProxyTarget {
|
|||||||
}
|
}
|
||||||
impl PartialEq for ProxyTarget {
|
impl PartialEq for ProxyTarget {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.filter == other.filter
|
self.public == other.public
|
||||||
|
&& self.private == other.private
|
||||||
&& self.acme == other.acme
|
&& self.acme == other.acme
|
||||||
&& self.addr == other.addr
|
&& self.addr == other.addr
|
||||||
&& self.connect_ssl.as_ref().map(Arc::as_ptr)
|
&& self.connect_ssl.as_ref().map(Arc::as_ptr)
|
||||||
@@ -320,7 +475,8 @@ impl Eq for ProxyTarget {}
|
|||||||
impl fmt::Debug for ProxyTarget {
|
impl fmt::Debug for ProxyTarget {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("ProxyTarget")
|
f.debug_struct("ProxyTarget")
|
||||||
.field("filter", &self.filter)
|
.field("public", &self.public)
|
||||||
|
.field("private", &self.private)
|
||||||
.field("acme", &self.acme)
|
.field("acme", &self.acme)
|
||||||
.field("addr", &self.addr)
|
.field("addr", &self.addr)
|
||||||
.field("add_x_forwarded_headers", &self.add_x_forwarded_headers)
|
.field("add_x_forwarded_headers", &self.add_x_forwarded_headers)
|
||||||
@@ -340,16 +496,37 @@ where
|
|||||||
{
|
{
|
||||||
type PreprocessRes = AcceptStream;
|
type PreprocessRes = AcceptStream;
|
||||||
fn filter(&self, metadata: &<A as Accept>::Metadata) -> bool {
|
fn filter(&self, metadata: &<A as Accept>::Metadata) -> bool {
|
||||||
let info = extract::<GatewayInfo, _>(metadata);
|
let gw = extract::<GatewayInfo, _>(metadata);
|
||||||
if info.is_none() {
|
let tcp = extract::<TcpMetadata, _>(metadata);
|
||||||
tracing::warn!("No GatewayInfo on metadata");
|
let (Some(gw), Some(tcp)) = (gw, tcp) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(ip_info) = &gw.info.ip_info else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let src = tcp.peer_addr.ip();
|
||||||
|
// Public if: source is a gateway/router IP (NAT'd internet),
|
||||||
|
// or source is outside all known subnets (direct internet)
|
||||||
|
let is_public = ip_info.lan_ip.contains(&src)
|
||||||
|
|| !ip_info.subnets.iter().any(|s| s.contains(&src));
|
||||||
|
|
||||||
|
if is_public {
|
||||||
|
self.public.contains(&gw.id)
|
||||||
|
} else {
|
||||||
|
// Private: accept if connection arrived on an interface with a matching IP
|
||||||
|
ip_info
|
||||||
|
.subnets
|
||||||
|
.iter()
|
||||||
|
.any(|s| self.private.contains(&s.addr()))
|
||||||
}
|
}
|
||||||
info.as_ref()
|
|
||||||
.map_or(true, |i| self.filter.filter(&i.id, &i.info))
|
|
||||||
}
|
}
|
||||||
fn acme(&self) -> Option<&AcmeProvider> {
|
fn acme(&self) -> Option<&AcmeProvider> {
|
||||||
self.acme.as_ref()
|
self.acme.as_ref()
|
||||||
}
|
}
|
||||||
|
fn bind_requirements(&self) -> (BTreeSet<GatewayId>, BTreeSet<IpAddr>) {
|
||||||
|
(self.public.clone(), self.private.clone())
|
||||||
|
}
|
||||||
async fn preprocess<'a>(
|
async fn preprocess<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
mut prev: ServerConfig,
|
mut prev: ServerConfig,
|
||||||
@@ -634,28 +811,15 @@ where
|
|||||||
|
|
||||||
struct VHostServer<A: Accept + 'static> {
|
struct VHostServer<A: Accept + 'static> {
|
||||||
mapping: Watch<Mapping<A>>,
|
mapping: Watch<Mapping<A>>,
|
||||||
|
bind_reqs: Watch<VHostBindRequirements>,
|
||||||
_thread: NonDetachingJoinHandle<()>,
|
_thread: NonDetachingJoinHandle<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a BTreeMap<Option<InternedString>, BTreeMap<ProxyTarget, Weak<()>>>> for AnyFilter {
|
|
||||||
fn from(value: &'a BTreeMap<Option<InternedString>, BTreeMap<ProxyTarget, Weak<()>>>) -> Self {
|
|
||||||
Self(
|
|
||||||
value
|
|
||||||
.iter()
|
|
||||||
.flat_map(|(_, v)| {
|
|
||||||
v.iter()
|
|
||||||
.filter(|(_, r)| r.strong_count() > 0)
|
|
||||||
.map(|(t, _)| t.filter.clone())
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A: Accept> VHostServer<A> {
|
impl<A: Accept> VHostServer<A> {
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
fn new<M: HasModel>(
|
fn new<M: HasModel>(
|
||||||
listener: A,
|
listener: A,
|
||||||
|
bind_reqs: Watch<VHostBindRequirements>,
|
||||||
db: TypedPatchDb<M>,
|
db: TypedPatchDb<M>,
|
||||||
crypto_provider: Arc<CryptoProvider>,
|
crypto_provider: Arc<CryptoProvider>,
|
||||||
acme_cache: AcmeTlsAlpnCache,
|
acme_cache: AcmeTlsAlpnCache,
|
||||||
@@ -679,6 +843,7 @@ impl<A: Accept> VHostServer<A> {
|
|||||||
let mapping = Watch::new(BTreeMap::new());
|
let mapping = Watch::new(BTreeMap::new());
|
||||||
Self {
|
Self {
|
||||||
mapping: mapping.clone(),
|
mapping: mapping.clone(),
|
||||||
|
bind_reqs,
|
||||||
_thread: tokio::spawn(async move {
|
_thread: tokio::spawn(async move {
|
||||||
let mut listener = VHostListener(TlsListener::new(
|
let mut listener = VHostListener(TlsListener::new(
|
||||||
listener,
|
listener,
|
||||||
@@ -729,6 +894,9 @@ impl<A: Accept> VHostServer<A> {
|
|||||||
targets.insert(target, Arc::downgrade(&rc));
|
targets.insert(target, Arc::downgrade(&rc));
|
||||||
writable.insert(hostname, targets);
|
writable.insert(hostname, targets);
|
||||||
res = Ok(rc);
|
res = Ok(rc);
|
||||||
|
if changed {
|
||||||
|
self.update_bind_reqs(writable);
|
||||||
|
}
|
||||||
changed
|
changed
|
||||||
});
|
});
|
||||||
if self.mapping.watcher_count() > 1 {
|
if self.mapping.watcher_count() > 1 {
|
||||||
@@ -752,9 +920,23 @@ impl<A: Accept> VHostServer<A> {
|
|||||||
if !targets.is_empty() {
|
if !targets.is_empty() {
|
||||||
writable.insert(hostname, targets);
|
writable.insert(hostname, targets);
|
||||||
}
|
}
|
||||||
|
if pre != post {
|
||||||
|
self.update_bind_reqs(writable);
|
||||||
|
}
|
||||||
pre == post
|
pre == post
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
fn update_bind_reqs(&self, mapping: &Mapping<A>) {
|
||||||
|
let new_reqs = compute_bind_reqs(mapping);
|
||||||
|
self.bind_reqs.send_if_modified(|reqs| {
|
||||||
|
if *reqs != new_reqs {
|
||||||
|
*reqs = new_reqs;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
fn is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
self.mapping.peek(|m| m.is_empty())
|
self.mapping.peek(|m| m.is_empty())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,28 +366,6 @@ where
|
|||||||
pub struct WebServerAcceptorSetter<A: Accept> {
|
pub struct WebServerAcceptorSetter<A: Accept> {
|
||||||
acceptor: Watch<A>,
|
acceptor: Watch<A>,
|
||||||
}
|
}
|
||||||
impl<A, B> WebServerAcceptorSetter<Option<Either<A, B>>>
|
|
||||||
where
|
|
||||||
A: Accept,
|
|
||||||
B: Accept<Metadata = A::Metadata>,
|
|
||||||
{
|
|
||||||
pub fn try_upgrade<F: FnOnce(A) -> Result<B, Error>>(&self, f: F) -> Result<(), Error> {
|
|
||||||
let mut res = Ok(());
|
|
||||||
self.acceptor.send_modify(|a| {
|
|
||||||
*a = match a.take() {
|
|
||||||
Some(Either::Left(a)) => match f(a) {
|
|
||||||
Ok(b) => Some(Either::Right(b)),
|
|
||||||
Err(e) => {
|
|
||||||
res = Err(e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
x => x,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<A: Accept> Deref for WebServerAcceptorSetter<A> {
|
impl<A: Accept> Deref for WebServerAcceptorSetter<A> {
|
||||||
type Target = Watch<A>;
|
type Target = Watch<A>;
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
|||||||
@@ -55,20 +55,18 @@ pub async fn get_ssl_certificate(
|
|||||||
.map(|(_, m)| m.as_hosts().as_entries())
|
.map(|(_, m)| m.as_hosts().as_entries())
|
||||||
.flatten_ok()
|
.flatten_ok()
|
||||||
.map_ok(|(_, m)| {
|
.map_ok(|(_, m)| {
|
||||||
Ok(m.as_onions()
|
Ok(m.as_public_domains()
|
||||||
.de()?
|
.keys()?
|
||||||
.iter()
|
.into_iter()
|
||||||
.map(InternedString::from_display)
|
|
||||||
.chain(m.as_public_domains().keys()?)
|
|
||||||
.chain(m.as_private_domains().de()?)
|
.chain(m.as_private_domains().de()?)
|
||||||
.chain(
|
.chain(
|
||||||
m.as_hostname_info()
|
m.as_bindings()
|
||||||
.de()?
|
.de()?
|
||||||
.values()
|
.values()
|
||||||
.flatten()
|
.flat_map(|b| b.addresses.possible.iter().cloned())
|
||||||
.map(|h| h.to_san_hostname()),
|
.map(|h| h.to_san_hostname()),
|
||||||
)
|
)
|
||||||
.collect::<Vec<_>>())
|
.collect::<Vec<InternedString>>())
|
||||||
})
|
})
|
||||||
.map(|a| a.and_then(|a| a))
|
.map(|a| a.and_then(|a| a))
|
||||||
.flatten_ok()
|
.flatten_ok()
|
||||||
@@ -181,20 +179,18 @@ pub async fn get_ssl_key(
|
|||||||
.map(|m| m.as_hosts().as_entries())
|
.map(|m| m.as_hosts().as_entries())
|
||||||
.flatten_ok()
|
.flatten_ok()
|
||||||
.map_ok(|(_, m)| {
|
.map_ok(|(_, m)| {
|
||||||
Ok(m.as_onions()
|
Ok(m.as_public_domains()
|
||||||
.de()?
|
.keys()?
|
||||||
.iter()
|
.into_iter()
|
||||||
.map(InternedString::from_display)
|
|
||||||
.chain(m.as_public_domains().keys()?)
|
|
||||||
.chain(m.as_private_domains().de()?)
|
.chain(m.as_private_domains().de()?)
|
||||||
.chain(
|
.chain(
|
||||||
m.as_hostname_info()
|
m.as_bindings()
|
||||||
.de()?
|
.de()?
|
||||||
.values()
|
.values()
|
||||||
.flatten()
|
.flat_map(|b| b.addresses.possible.iter().cloned())
|
||||||
.map(|h| h.to_san_hostname()),
|
.map(|h| h.to_san_hostname()),
|
||||||
)
|
)
|
||||||
.collect::<Vec<_>>())
|
.collect::<Vec<InternedString>>())
|
||||||
})
|
})
|
||||||
.map(|a| a.and_then(|a| a))
|
.map(|a| a.and_then(|a| a))
|
||||||
.flatten_ok()
|
.flatten_ok()
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ impl ServiceMap {
|
|||||||
service_interfaces: Default::default(),
|
service_interfaces: Default::default(),
|
||||||
hosts: Default::default(),
|
hosts: Default::default(),
|
||||||
store_exposed_dependents: Default::default(),
|
store_exposed_dependents: Default::default(),
|
||||||
|
outbound_gateway: None,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ pub async fn add_forward(
|
|||||||
})
|
})
|
||||||
.map(|s| s.prefix_len())
|
.map(|s| s.prefix_len())
|
||||||
.unwrap_or(32);
|
.unwrap_or(32);
|
||||||
let rc = ctx.forward.add_forward(source, target, prefix).await?;
|
let rc = ctx.forward.add_forward(source, target, prefix, None).await?;
|
||||||
ctx.active_forwards.mutate(|m| {
|
ctx.active_forwards.mutate(|m| {
|
||||||
m.insert(source, rc);
|
m.insert(source, rc);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ impl TunnelContext {
|
|||||||
})
|
})
|
||||||
.map(|s| s.prefix_len())
|
.map(|s| s.prefix_len())
|
||||||
.unwrap_or(32);
|
.unwrap_or(32);
|
||||||
active_forwards.insert(from, forward.add_forward(from, to, prefix).await?);
|
active_forwards.insert(from, forward.add_forward(from, to, prefix, None).await?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self(Arc::new(TunnelContextSeed {
|
Ok(Self(Arc::new(TunnelContextSeed {
|
||||||
|
|||||||
@@ -59,8 +59,9 @@ mod v0_4_0_alpha_16;
|
|||||||
mod v0_4_0_alpha_17;
|
mod v0_4_0_alpha_17;
|
||||||
mod v0_4_0_alpha_18;
|
mod v0_4_0_alpha_18;
|
||||||
mod v0_4_0_alpha_19;
|
mod v0_4_0_alpha_19;
|
||||||
|
mod v0_4_0_alpha_20;
|
||||||
|
|
||||||
pub type Current = v0_4_0_alpha_19::Version; // VERSION_BUMP
|
pub type Current = v0_4_0_alpha_20::Version; // VERSION_BUMP
|
||||||
|
|
||||||
impl Current {
|
impl Current {
|
||||||
#[instrument(skip(self, db))]
|
#[instrument(skip(self, db))]
|
||||||
@@ -181,7 +182,8 @@ enum Version {
|
|||||||
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>),
|
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>),
|
||||||
V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>),
|
V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>),
|
||||||
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>),
|
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>),
|
||||||
V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>), // VERSION_BUMP
|
V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>),
|
||||||
|
V0_4_0_alpha_20(Wrapper<v0_4_0_alpha_20::Version>), // VERSION_BUMP
|
||||||
Other(exver::Version),
|
Other(exver::Version),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +245,8 @@ impl Version {
|
|||||||
Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)),
|
||||||
|
Self::V0_4_0_alpha_20(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
||||||
Self::Other(v) => {
|
Self::Other(v) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("unknown version {v}"),
|
eyre!("unknown version {v}"),
|
||||||
@@ -297,7 +300,8 @@ impl Version {
|
|||||||
Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(),
|
||||||
|
Version::V0_4_0_alpha_20(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
||||||
Version::Other(x) => x.clone(),
|
Version::Other(x) => x.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ A server is not a toy. It is a critical component of the computing paradigm, and
|
|||||||
|
|
||||||
Start9 is paving new ground with StartOS, trying to create what most developers and IT professionals thought impossible; namely, an OS and user experience that affords a normal person the same independent control over their data and communications as an experienced Linux sysadmin.
|
Start9 is paving new ground with StartOS, trying to create what most developers and IT professionals thought impossible; namely, an OS and user experience that affords a normal person the same independent control over their data and communications as an experienced Linux sysadmin.
|
||||||
|
|
||||||
The difficulty of our endeavor requires making mistakes; and our integrity and dedication to excellence require that we correct them. This means a willingness to discard bad ideas and broken parts, and if absolutely necessary, to tear it all down and start over. That is exactly what we did with StartOS v0.2.0 in 2020. It is what we did with StartOS v0.3.0 in 2022. And we are doing it now with StartOS v0.4.0 in 2025.
|
The difficulty of our endeavor requires making mistakes; and our integrity and dedication to excellence require that we correct them. This means a willingness to discard bad ideas and broken parts, and if absolutely necessary, to tear it all down and start over. That is exactly what we did with StartOS v0.2.0 in 2020. It is what we did with StartOS v0.3.0 in 2022. And we are doing it now with StartOS v0.4.0 in 2026.
|
||||||
|
|
||||||
v0.4.0 is a complete rewrite of StartOS, almost nothing survived. After nearly six years of building StartOS, we believe that we have finally arrived at the correct architecture and foundation that will allow us to deliver on the promise of sovereign computing.
|
v0.4.0 is a complete rewrite of StartOS, almost nothing survived. After nearly six years of building StartOS, we believe that we have finally arrived at the correct architecture and foundation that will allow us to deliver on the promise of sovereign computing.
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
### Improved User interface
|
### New User interface
|
||||||
|
|
||||||
We re-wrote the StartOS UI to be more performant, more intuitive, and better looking on both mobile and desktop. Enjoy.
|
We re-wrote the StartOS UI to be more performant, more intuitive, and better looking on both mobile and desktop. Enjoy.
|
||||||
|
|
||||||
@@ -28,6 +28,10 @@ StartOS v0.4.0 supports multiple languages and also makes it easy to add more la
|
|||||||
|
|
||||||
Neither Docker nor Podman offer the reliability and flexibility needed for StartOS. Instead, v0.4.0 uses a nested container paradigm based on LXC for the outer container and Linux namespaces for sub containers. This architecture naturally supports multi container setups.
|
Neither Docker nor Podman offer the reliability and flexibility needed for StartOS. Instead, v0.4.0 uses a nested container paradigm based on LXC for the outer container and Linux namespaces for sub containers. This architecture naturally supports multi container setups.
|
||||||
|
|
||||||
|
### Hardware Acceleration
|
||||||
|
|
||||||
|
Services can take advantage of (and require) the presence of certain hardware modules, such as Nvidia GPUs, for transcoding or inference purposes. For example, StartOS and Ollama can run natively on The Nvidia DGX Spark and take full advantage of the hardware/firmware stack to perform local inference against open source models.
|
||||||
|
|
||||||
### New S9PK archive format
|
### New S9PK archive format
|
||||||
|
|
||||||
The S9PK archive format has been overhauled to allow for signature verification of partial downloads, and allow direct mounting of container images without unpacking the s9pk.
|
The S9PK archive format has been overhauled to allow for signature verification of partial downloads, and allow direct mounting of container images without unpacking the s9pk.
|
||||||
@@ -80,13 +84,13 @@ The new start-fs fuse module unifies file system expectations for various platfo
|
|||||||
|
|
||||||
StartOS now uses Extended Versioning (Exver), which consists of three parts: (1) a Semver-compliant upstream version, (2) a Semver-compliant wrapper version, and (3) an optional "flavor" prefix. Flavors can be thought of as alternative implementations of services, where a user would only want one or the other installed, and data can feasibly be migrating between the two. Another common characteristic of flavors is that they satisfy the same API requirement of dependents, though this is not strictly necessary. A valid Exver looks something like this: `#knots:29.0:1.0-beta.1`. This would translate to "the first beta release of StartOS wrapper version 1.0 of Bitcoin Knots version 29.0".
|
StartOS now uses Extended Versioning (Exver), which consists of three parts: (1) a Semver-compliant upstream version, (2) a Semver-compliant wrapper version, and (3) an optional "flavor" prefix. Flavors can be thought of as alternative implementations of services, where a user would only want one or the other installed, and data can feasibly be migrating between the two. Another common characteristic of flavors is that they satisfy the same API requirement of dependents, though this is not strictly necessary. A valid Exver looks something like this: `#knots:29.0:1.0-beta.1`. This would translate to "the first beta release of StartOS wrapper version 1.0 of Bitcoin Knots version 29.0".
|
||||||
|
|
||||||
### ACME
|
### Let's Encrypt
|
||||||
|
|
||||||
StartOS now supports using ACME protocol to automatically obtain SSL/TLS certificates from widely trusted certificate authorities, such as Let's Encrypt, for your public domains. This means people visiting your public websites and APIs will not need to download and trust your server's Root CA.
|
StartOS now supports Let's Encrypt to automatically obtain SSL/TLS certificates for public domains. This means people visiting your public websites and APIs will not need to download and trust your server's Root CA.
|
||||||
|
|
||||||
### Gateways
|
### Gateways
|
||||||
|
|
||||||
Gateways connect your server to the Internet. They process outbound traffic, and under certain conditions, they also permit inbound traffic. For example, your router is a gateway. It is now possible add gateways to StartOS, such as StartTunnel, in order to more granularly control how your installed services are exposed to the Internet.
|
Gateways connect your server to the Internet, facilitating inbound and outbound traffic. Your router is a gateway. It is now possible to add Wireguard VPN gateways to your server to control how devices outside the LAN connect to your server and how your server connects out to the Internet.
|
||||||
|
|
||||||
### Static DNS Servers
|
### Static DNS Servers
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::BTreeMap;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -23,17 +23,14 @@ use crate::disk::mount::filesystem::cifs::Cifs;
|
|||||||
use crate::disk::mount::util::unmount;
|
use crate::disk::mount::util::unmount;
|
||||||
use crate::hostname::Hostname;
|
use crate::hostname::Hostname;
|
||||||
use crate::net::forward::AvailablePorts;
|
use crate::net::forward::AvailablePorts;
|
||||||
use crate::net::host::Host;
|
|
||||||
use crate::net::keys::KeyStore;
|
use crate::net::keys::KeyStore;
|
||||||
use crate::net::tor::{OnionAddress, TorSecretKey};
|
|
||||||
use crate::notifications::Notifications;
|
use crate::notifications::Notifications;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||||
use crate::ssh::{SshKeys, SshPubKey};
|
use crate::ssh::{SshKeys, SshPubKey};
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
use crate::util::crypto::ed25519_expand_key;
|
|
||||||
use crate::util::serde::Pem;
|
use crate::util::serde::Pem;
|
||||||
use crate::{DATA_DIR, HostId, Id, PACKAGE_DATA, PackageId, ReplayId};
|
use crate::{DATA_DIR, PACKAGE_DATA, PackageId, ReplayId};
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref V0_3_6_alpha_0: exver::Version = exver::Version::new(
|
static ref V0_3_6_alpha_0: exver::Version = exver::Version::new(
|
||||||
@@ -146,12 +143,7 @@ pub struct Version;
|
|||||||
|
|
||||||
impl VersionT for Version {
|
impl VersionT for Version {
|
||||||
type Previous = v0_3_5_2::Version;
|
type Previous = v0_3_5_2::Version;
|
||||||
type PreUpRes = (
|
type PreUpRes = (AccountInfo, SshKeys, CifsTargets);
|
||||||
AccountInfo,
|
|
||||||
SshKeys,
|
|
||||||
CifsTargets,
|
|
||||||
BTreeMap<PackageId, BTreeMap<HostId, TorSecretKey>>,
|
|
||||||
);
|
|
||||||
fn semver(self) -> exver::Version {
|
fn semver(self) -> exver::Version {
|
||||||
V0_3_6_alpha_0.clone()
|
V0_3_6_alpha_0.clone()
|
||||||
}
|
}
|
||||||
@@ -166,20 +158,18 @@ impl VersionT for Version {
|
|||||||
|
|
||||||
let cifs = previous_cifs(&pg).await?;
|
let cifs = previous_cifs(&pg).await?;
|
||||||
|
|
||||||
let tor_keys = previous_tor_keys(&pg).await?;
|
|
||||||
|
|
||||||
Command::new("systemctl")
|
Command::new("systemctl")
|
||||||
.arg("stop")
|
.arg("stop")
|
||||||
.arg("postgresql@*.service")
|
.arg("postgresql@*.service")
|
||||||
.invoke(crate::ErrorKind::Database)
|
.invoke(crate::ErrorKind::Database)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok((account, ssh_keys, cifs, tor_keys))
|
Ok((account, ssh_keys, cifs))
|
||||||
}
|
}
|
||||||
fn up(
|
fn up(
|
||||||
self,
|
self,
|
||||||
db: &mut Value,
|
db: &mut Value,
|
||||||
(account, ssh_keys, cifs, tor_keys): Self::PreUpRes,
|
(account, ssh_keys, cifs): Self::PreUpRes,
|
||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
let prev_package_data = db["package-data"].clone();
|
let prev_package_data = db["package-data"].clone();
|
||||||
|
|
||||||
@@ -242,11 +232,7 @@ impl VersionT for Version {
|
|||||||
"ui": db["ui"],
|
"ui": db["ui"],
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut keystore = KeyStore::new(&account)?;
|
let keystore = KeyStore::new(&account)?;
|
||||||
for key in tor_keys.values().flat_map(|v| v.values()) {
|
|
||||||
assert!(key.is_valid());
|
|
||||||
keystore.onion.insert(key.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let private = {
|
let private = {
|
||||||
let mut value = json!({});
|
let mut value = json!({});
|
||||||
@@ -350,20 +336,6 @@ impl VersionT for Version {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
let onions = input[&*id]["installed"]["interface-addresses"]
|
|
||||||
.as_object()
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.filter_map(|(id, addrs)| {
|
|
||||||
addrs["tor-address"].as_str().map(|addr| {
|
|
||||||
Ok((
|
|
||||||
HostId::from(Id::try_from(id.clone())?),
|
|
||||||
addr.parse::<OnionAddress>()?,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Result<BTreeMap<_, _>, Error>>()?;
|
|
||||||
|
|
||||||
if let Err(e) = async {
|
if let Err(e) = async {
|
||||||
let package_s9pk = tokio::fs::File::open(path).await?;
|
let package_s9pk = tokio::fs::File::open(path).await?;
|
||||||
let file = MultiCursorFile::open(&package_s9pk).await?;
|
let file = MultiCursorFile::open(&package_s9pk).await?;
|
||||||
@@ -381,11 +353,8 @@ impl VersionT for Version {
|
|||||||
.await?
|
.await?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let to_sync = ctx
|
ctx.db
|
||||||
.db
|
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let mut to_sync = BTreeSet::new();
|
|
||||||
|
|
||||||
let package = db
|
let package = db
|
||||||
.as_public_mut()
|
.as_public_mut()
|
||||||
.as_package_data_mut()
|
.as_package_data_mut()
|
||||||
@@ -396,29 +365,11 @@ impl VersionT for Version {
|
|||||||
.as_tasks_mut()
|
.as_tasks_mut()
|
||||||
.remove(&ReplayId::from("needs-config"))?;
|
.remove(&ReplayId::from("needs-config"))?;
|
||||||
}
|
}
|
||||||
for (id, onion) in onions {
|
Ok(())
|
||||||
package
|
|
||||||
.as_hosts_mut()
|
|
||||||
.upsert(&id, || Ok(Host::new()))?
|
|
||||||
.as_onions_mut()
|
|
||||||
.mutate(|o| {
|
|
||||||
o.clear();
|
|
||||||
o.insert(onion);
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
to_sync.insert(id);
|
|
||||||
}
|
|
||||||
Ok(to_sync)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|
||||||
if let Some(service) = &*ctx.services.get(&id).await {
|
|
||||||
for host_id in to_sync {
|
|
||||||
service.sync_host(host_id.clone()).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<_, Error>(())
|
Ok::<_, Error>(())
|
||||||
}
|
}
|
||||||
.await
|
.await
|
||||||
@@ -481,33 +432,6 @@ async fn previous_account_info(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<Accoun
|
|||||||
password: account_query
|
password: account_query
|
||||||
.try_get("password")
|
.try_get("password")
|
||||||
.with_ctx(|_| (ErrorKind::Database, "password"))?,
|
.with_ctx(|_| (ErrorKind::Database, "password"))?,
|
||||||
tor_keys: vec![TorSecretKey::from_bytes(
|
|
||||||
if let Some(bytes) = account_query
|
|
||||||
.try_get::<Option<Vec<u8>>, _>("tor_key")
|
|
||||||
.with_ctx(|_| (ErrorKind::Database, "tor_key"))?
|
|
||||||
{
|
|
||||||
<[u8; 64]>::try_from(bytes).map_err(|e| {
|
|
||||||
Error::new(
|
|
||||||
eyre!("expected vec of len 64, got len {}", e.len()),
|
|
||||||
ErrorKind::ParseDbField,
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
} else {
|
|
||||||
ed25519_expand_key(
|
|
||||||
&<[u8; 32]>::try_from(
|
|
||||||
account_query
|
|
||||||
.try_get::<Vec<u8>, _>("network_key")
|
|
||||||
.with_kind(ErrorKind::Database)?,
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
Error::new(
|
|
||||||
eyre!("expected vec of len 32, got len {}", e.len()),
|
|
||||||
ErrorKind::ParseDbField,
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)?],
|
|
||||||
server_id: account_query
|
server_id: account_query
|
||||||
.try_get("server_id")
|
.try_get("server_id")
|
||||||
.with_ctx(|_| (ErrorKind::Database, "server_id"))?,
|
.with_ctx(|_| (ErrorKind::Database, "server_id"))?,
|
||||||
@@ -579,68 +503,3 @@ async fn previous_ssh_keys(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<SshKeys, E
|
|||||||
Ok(ssh_keys)
|
Ok(ssh_keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
async fn previous_tor_keys(
|
|
||||||
pg: &sqlx::Pool<sqlx::Postgres>,
|
|
||||||
) -> Result<BTreeMap<PackageId, BTreeMap<HostId, TorSecretKey>>, Error> {
|
|
||||||
let mut res = BTreeMap::<PackageId, BTreeMap<HostId, TorSecretKey>>::new();
|
|
||||||
let net_key_query = sqlx::query(r#"SELECT * FROM network_keys"#)
|
|
||||||
.fetch_all(pg)
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
|
|
||||||
for row in net_key_query {
|
|
||||||
let package_id: PackageId = row
|
|
||||||
.try_get::<String, _>("package")
|
|
||||||
.with_ctx(|_| (ErrorKind::Database, "network_keys::package"))?
|
|
||||||
.parse()?;
|
|
||||||
let interface_id: HostId = row
|
|
||||||
.try_get::<String, _>("interface")
|
|
||||||
.with_ctx(|_| (ErrorKind::Database, "network_keys::interface"))?
|
|
||||||
.parse()?;
|
|
||||||
let key = TorSecretKey::from_bytes(ed25519_expand_key(
|
|
||||||
&<[u8; 32]>::try_from(
|
|
||||||
row.try_get::<Vec<u8>, _>("key")
|
|
||||||
.with_ctx(|_| (ErrorKind::Database, "network_keys::key"))?,
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
Error::new(
|
|
||||||
eyre!("expected vec of len 32, got len {}", e.len()),
|
|
||||||
ErrorKind::ParseDbField,
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
))?;
|
|
||||||
res.entry(package_id).or_default().insert(interface_id, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tor_key_query = sqlx::query(r#"SELECT * FROM tor"#)
|
|
||||||
.fetch_all(pg)
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Database)?;
|
|
||||||
|
|
||||||
for row in tor_key_query {
|
|
||||||
let package_id: PackageId = row
|
|
||||||
.try_get::<String, _>("package")
|
|
||||||
.with_ctx(|_| (ErrorKind::Database, "tor::package"))?
|
|
||||||
.parse()?;
|
|
||||||
let interface_id: HostId = row
|
|
||||||
.try_get::<String, _>("interface")
|
|
||||||
.with_ctx(|_| (ErrorKind::Database, "tor::interface"))?
|
|
||||||
.parse()?;
|
|
||||||
let key = TorSecretKey::from_bytes(
|
|
||||||
<[u8; 64]>::try_from(
|
|
||||||
row.try_get::<Vec<u8>, _>("key")
|
|
||||||
.with_ctx(|_| (ErrorKind::Database, "tor::key"))?,
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
Error::new(
|
|
||||||
eyre!("expected vec of len 64, got len {}", e.len()),
|
|
||||||
ErrorKind::ParseDbField,
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
res.entry(package_id).or_default().insert(interface_id, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use super::v0_3_5::V0_3_0_COMPAT;
|
|||||||
use super::{VersionT, v0_3_6_alpha_9};
|
use super::{VersionT, v0_3_6_alpha_9};
|
||||||
use crate::GatewayId;
|
use crate::GatewayId;
|
||||||
use crate::net::host::address::PublicDomainConfig;
|
use crate::net::host::address::PublicDomainConfig;
|
||||||
use crate::net::tor::OnionAddress;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
@@ -22,7 +21,7 @@ lazy_static::lazy_static! {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
enum HostAddress {
|
enum HostAddress {
|
||||||
Onion { address: OnionAddress },
|
Onion { address: String },
|
||||||
Domain { address: InternedString },
|
Domain { address: InternedString },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
use exver::{PreReleaseSegment, VersionRange};
|
use exver::{PreReleaseSegment, VersionRange};
|
||||||
use imbl_value::InternedString;
|
|
||||||
|
|
||||||
use super::v0_3_5::V0_3_0_COMPAT;
|
use super::v0_3_5::V0_3_0_COMPAT;
|
||||||
use super::{VersionT, v0_4_0_alpha_11};
|
use super::{VersionT, v0_4_0_alpha_11};
|
||||||
use crate::net::tor::TorSecretKey;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
@@ -33,48 +29,6 @@ impl VersionT for Version {
|
|||||||
}
|
}
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||||
let mut err = None;
|
|
||||||
let onion_store = db["private"]["keyStore"]["onion"]
|
|
||||||
.as_object_mut()
|
|
||||||
.or_not_found("private.keyStore.onion")?;
|
|
||||||
onion_store.retain(|o, v| match from_value::<TorSecretKey>(v.clone()) {
|
|
||||||
Ok(k) => k.is_valid() && &InternedString::from_display(&k.onion_address()) == o,
|
|
||||||
Err(e) => {
|
|
||||||
err = Some(e);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if let Some(e) = err {
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
let allowed_addresses = onion_store.keys().cloned().collect::<BTreeSet<_>>();
|
|
||||||
let fix_host = |host: &mut Value| {
|
|
||||||
Ok::<_, Error>(
|
|
||||||
host["onions"]
|
|
||||||
.as_array_mut()
|
|
||||||
.or_not_found("host.onions")?
|
|
||||||
.retain(|addr| {
|
|
||||||
addr.as_str()
|
|
||||||
.map(|s| allowed_addresses.contains(s))
|
|
||||||
.unwrap_or(false)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
for (_, pde) in db["public"]["packageData"]
|
|
||||||
.as_object_mut()
|
|
||||||
.or_not_found("public.packageData")?
|
|
||||||
.iter_mut()
|
|
||||||
{
|
|
||||||
for (_, host) in pde["hosts"]
|
|
||||||
.as_object_mut()
|
|
||||||
.or_not_found("public.packageData[].hosts")?
|
|
||||||
.iter_mut()
|
|
||||||
{
|
|
||||||
fix_host(host)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fix_host(&mut db["public"]["serverInfo"]["network"]["host"])?;
|
|
||||||
|
|
||||||
if db["private"]["keyStore"]["localCerts"].is_null() {
|
if db["private"]["keyStore"]["localCerts"].is_null() {
|
||||||
db["private"]["keyStore"]["localCerts"] =
|
db["private"]["keyStore"]["localCerts"] =
|
||||||
db["private"]["keyStore"]["local_certs"].clone();
|
db["private"]["keyStore"]["local_certs"].clone();
|
||||||
|
|||||||
192
core/src/version/v0_4_0_alpha_20.rs
Normal file
192
core/src/version/v0_4_0_alpha_20.rs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
use exver::{PreReleaseSegment, VersionRange};
|
||||||
|
|
||||||
|
use super::v0_3_5::V0_3_0_COMPAT;
|
||||||
|
use super::{VersionT, v0_4_0_alpha_19};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref V0_4_0_alpha_20: exver::Version = exver::Version::new(
|
||||||
|
[0, 4, 0],
|
||||||
|
[PreReleaseSegment::String("alpha".into()), 20.into()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct Version;
|
||||||
|
|
||||||
|
impl VersionT for Version {
|
||||||
|
type Previous = v0_4_0_alpha_19::Version;
|
||||||
|
type PreUpRes = ();
|
||||||
|
|
||||||
|
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn semver(self) -> exver::Version {
|
||||||
|
V0_4_0_alpha_20.clone()
|
||||||
|
}
|
||||||
|
fn compat(self) -> &'static VersionRange {
|
||||||
|
&V0_3_0_COMPAT
|
||||||
|
}
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||||
|
// Remove onions and tor-related fields from server host
|
||||||
|
if let Some(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"))
|
||||||
|
.and_then(|h| h.as_object_mut())
|
||||||
|
{
|
||||||
|
host.remove("onions");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove onions from 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() {
|
||||||
|
if let Some(host_obj) = host.as_object_mut() {
|
||||||
|
host_obj.remove("onions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove onion store from private keyStore
|
||||||
|
if let Some(key_store) = db
|
||||||
|
.get_mut("private")
|
||||||
|
.and_then(|p| p.get_mut("keyStore"))
|
||||||
|
.and_then(|k| k.as_object_mut())
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate availablePorts from IdPool format to BTreeMap<u16, bool>
|
||||||
|
// Rebuild from actual assigned ports in all bindings
|
||||||
|
migrate_available_ports(db);
|
||||||
|
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_ports_from_host(host: Option<&Value>, ports: &mut Value) {
|
||||||
|
let Some(bindings) = host
|
||||||
|
.and_then(|h| h.get("bindings"))
|
||||||
|
.and_then(|b| b.as_object())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for (_, binding) in bindings.iter() {
|
||||||
|
if let Some(net) = binding.get("net") {
|
||||||
|
if let Some(port) = net.get("assignedPort").and_then(|p| p.as_u64()) {
|
||||||
|
if let Some(obj) = ports.as_object_mut() {
|
||||||
|
obj.insert(port.to_string().into(), Value::from(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(port) = net.get("assignedSslPort").and_then(|p| p.as_u64()) {
|
||||||
|
if let Some(obj) = ports.as_object_mut() {
|
||||||
|
obj.insert(port.to_string().into(), Value::from(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrate_available_ports(db: &mut Value) {
|
||||||
|
let mut new_ports: Value = serde_json::json!({}).into();
|
||||||
|
|
||||||
|
// Collect from server host
|
||||||
|
let server_host = db
|
||||||
|
.get("public")
|
||||||
|
.and_then(|p| p.get("serverInfo"))
|
||||||
|
.and_then(|s| s.get("network"))
|
||||||
|
.and_then(|n| n.get("host"))
|
||||||
|
.cloned();
|
||||||
|
collect_ports_from_host(server_host.as_ref(), &mut new_ports);
|
||||||
|
|
||||||
|
// Collect from all package hosts
|
||||||
|
if let Some(packages) = db
|
||||||
|
.get("public")
|
||||||
|
.and_then(|p| p.get("packageData"))
|
||||||
|
.and_then(|p| p.as_object())
|
||||||
|
{
|
||||||
|
for (_, package) in packages.iter() {
|
||||||
|
if let Some(hosts) = package.get("hosts").and_then(|h| h.as_object()) {
|
||||||
|
for (_, host) in hosts.iter() {
|
||||||
|
collect_ports_from_host(Some(host), &mut new_ports);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace private.availablePorts
|
||||||
|
if let Some(private) = db.get_mut("private").and_then(|p| p.as_object_mut()) {
|
||||||
|
private.insert("availablePorts".into(), new_ports);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,11 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// 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 { BindOptions } from './BindOptions'
|
||||||
|
import type { DerivedAddressInfo } from './DerivedAddressInfo'
|
||||||
import type { NetInfo } from './NetInfo'
|
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.
|
// 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
|
internalPort: number
|
||||||
gateway: GatewayId
|
address: string
|
||||||
enabled: boolean | null
|
enabled: boolean | null
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// 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 OnionHostname = {
|
export type Bindings = { [key: number]: BindInfo }
|
||||||
value: string
|
|
||||||
port: number | null
|
|
||||||
sslPort: number | null
|
|
||||||
}
|
|
||||||
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,3 +1,3 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type ErrorData = { details: string; debug: string }
|
export type ErrorData = { details: string; debug: string; info: unknown }
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// 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 { Bindings } from './Bindings'
|
||||||
import type { HostnameInfo } from './HostnameInfo'
|
|
||||||
import type { PublicDomainConfig } from './PublicDomainConfig'
|
import type { PublicDomainConfig } from './PublicDomainConfig'
|
||||||
|
|
||||||
export type Host = {
|
export type Host = {
|
||||||
bindings: { [key: number]: BindInfo }
|
bindings: Bindings
|
||||||
onions: string[]
|
|
||||||
publicDomains: { [key: string]: PublicDomainConfig }
|
publicDomains: { [key: string]: PublicDomainConfig }
|
||||||
privateDomains: Array<string>
|
privateDomains: Array<string>
|
||||||
/**
|
|
||||||
* COMPUTED: NetService::update
|
|
||||||
*/
|
|
||||||
hostnameInfo: { [key: number]: Array<HostnameInfo> }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { GatewayInfo } from './GatewayInfo'
|
import type { GatewayInfo } from './GatewayInfo'
|
||||||
import type { IpHostname } from './IpHostname'
|
import type { IpHostname } from './IpHostname'
|
||||||
import type { OnionHostname } from './OnionHostname'
|
|
||||||
|
|
||||||
export type HostnameInfo =
|
export type HostnameInfo = {
|
||||||
| { kind: 'ip'; gateway: GatewayInfo; public: boolean; hostname: IpHostname }
|
gateway: GatewayInfo
|
||||||
| { kind: 'onion'; hostname: OnionHostname }
|
public: boolean
|
||||||
|
hostname: IpHostname
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// 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 = {
|
export type NetInfo = {
|
||||||
privateDisabled: Array<GatewayId>
|
|
||||||
publicEnabled: Array<GatewayId>
|
|
||||||
assignedPort: number | null
|
assignedPort: number | null
|
||||||
assignedSslPort: number | null
|
assignedSslPort: number | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export { BackupTargetFS } from './BackupTargetFS'
|
|||||||
export { Base64 } from './Base64'
|
export { Base64 } from './Base64'
|
||||||
export { BindId } from './BindId'
|
export { BindId } from './BindId'
|
||||||
export { BindInfo } from './BindInfo'
|
export { BindInfo } from './BindInfo'
|
||||||
export { BindingGatewaySetEnabledParams } from './BindingGatewaySetEnabledParams'
|
export { BindingSetAddressEnabledParams } from './BindingSetAddressEnabledParams'
|
||||||
|
export { Bindings } from './Bindings'
|
||||||
export { BindOptions } from './BindOptions'
|
export { BindOptions } from './BindOptions'
|
||||||
export { BindParams } from './BindParams'
|
export { BindParams } from './BindParams'
|
||||||
export { Blake3Commitment } from './Blake3Commitment'
|
export { Blake3Commitment } from './Blake3Commitment'
|
||||||
@@ -64,6 +65,7 @@ export { Dependencies } from './Dependencies'
|
|||||||
export { DependencyMetadata } from './DependencyMetadata'
|
export { DependencyMetadata } from './DependencyMetadata'
|
||||||
export { DependencyRequirement } from './DependencyRequirement'
|
export { DependencyRequirement } from './DependencyRequirement'
|
||||||
export { DepInfo } from './DepInfo'
|
export { DepInfo } from './DepInfo'
|
||||||
|
export { DerivedAddressInfo } from './DerivedAddressInfo'
|
||||||
export { Description } from './Description'
|
export { Description } from './Description'
|
||||||
export { DesiredStatus } from './DesiredStatus'
|
export { DesiredStatus } from './DesiredStatus'
|
||||||
export { DestroySubcontainerFsParams } from './DestroySubcontainerFsParams'
|
export { DestroySubcontainerFsParams } from './DestroySubcontainerFsParams'
|
||||||
@@ -149,7 +151,6 @@ export { NetInfo } from './NetInfo'
|
|||||||
export { NetworkInfo } from './NetworkInfo'
|
export { NetworkInfo } from './NetworkInfo'
|
||||||
export { NetworkInterfaceInfo } from './NetworkInterfaceInfo'
|
export { NetworkInterfaceInfo } from './NetworkInterfaceInfo'
|
||||||
export { NetworkInterfaceType } from './NetworkInterfaceType'
|
export { NetworkInterfaceType } from './NetworkInterfaceType'
|
||||||
export { OnionHostname } from './OnionHostname'
|
|
||||||
export { OsIndex } from './OsIndex'
|
export { OsIndex } from './OsIndex'
|
||||||
export { OsVersionInfoMap } from './OsVersionInfoMap'
|
export { OsVersionInfoMap } from './OsVersionInfoMap'
|
||||||
export { OsVersionInfo } from './OsVersionInfo'
|
export { OsVersionInfo } from './OsVersionInfo'
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { PackageId, ServiceInterfaceId, ServiceInterfaceType } from '../types'
|
import { PackageId, ServiceInterfaceId, ServiceInterfaceType } from '../types'
|
||||||
import { knownProtocols } from '../interfaces/Host'
|
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 { Effects } from '../Effects'
|
||||||
import { DropGenerator, DropPromise } from './Drop'
|
import { DropGenerator, DropPromise } from './Drop'
|
||||||
import { IpAddress, IPV6_LINK_LOCAL } from './ip'
|
import { IpAddress, IPV6_LINK_LOCAL } from './ip'
|
||||||
@@ -20,7 +26,6 @@ export const getHostname = (url: string): Hostname | null => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FilterKinds =
|
type FilterKinds =
|
||||||
| 'onion'
|
|
||||||
| 'mdns'
|
| 'mdns'
|
||||||
| 'domain'
|
| 'domain'
|
||||||
| 'ip'
|
| 'ip'
|
||||||
@@ -42,27 +47,25 @@ type VisibilityFilter<V extends 'public' | 'private'> = V extends 'public'
|
|||||||
| (HostnameInfo & { public: false })
|
| (HostnameInfo & { public: false })
|
||||||
| VisibilityFilter<Exclude<V, 'private'>>
|
| VisibilityFilter<Exclude<V, 'private'>>
|
||||||
: never
|
: never
|
||||||
type KindFilter<K extends FilterKinds> = K extends 'onion'
|
type KindFilter<K extends FilterKinds> = K extends 'mdns'
|
||||||
? (HostnameInfo & { kind: 'onion' }) | KindFilter<Exclude<K, 'onion'>>
|
?
|
||||||
: K extends 'mdns'
|
| (HostnameInfo & { hostname: { kind: 'local' } })
|
||||||
|
| KindFilter<Exclude<K, 'mdns'>>
|
||||||
|
: K extends 'domain'
|
||||||
?
|
?
|
||||||
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'local' } })
|
| (HostnameInfo & { hostname: { kind: 'domain' } })
|
||||||
| KindFilter<Exclude<K, 'mdns'>>
|
| KindFilter<Exclude<K, 'domain'>>
|
||||||
: K extends 'domain'
|
: K extends 'ipv4'
|
||||||
?
|
?
|
||||||
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'domain' } })
|
| (HostnameInfo & { hostname: { kind: 'ipv4' } })
|
||||||
| KindFilter<Exclude<K, 'domain'>>
|
| KindFilter<Exclude<K, 'ipv4'>>
|
||||||
: K extends 'ipv4'
|
: K extends 'ipv6'
|
||||||
?
|
?
|
||||||
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv4' } })
|
| (HostnameInfo & { hostname: { kind: 'ipv6' } })
|
||||||
| KindFilter<Exclude<K, 'ipv4'>>
|
| KindFilter<Exclude<K, 'ipv6'>>
|
||||||
: K extends 'ipv6'
|
: K extends 'ip'
|
||||||
?
|
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
|
||||||
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv6' } })
|
: never
|
||||||
| KindFilter<Exclude<K, 'ipv6'>>
|
|
||||||
: K extends 'ip'
|
|
||||||
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
|
|
||||||
: never
|
|
||||||
|
|
||||||
type FilterReturnTy<F extends Filter> = F extends {
|
type FilterReturnTy<F extends Filter> = F extends {
|
||||||
visibility: infer V extends 'public' | 'private'
|
visibility: infer V extends 'public' | 'private'
|
||||||
@@ -90,10 +93,6 @@ const nonLocalFilter = {
|
|||||||
const publicFilter = {
|
const publicFilter = {
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
} as const
|
} as const
|
||||||
const onionFilter = {
|
|
||||||
kind: 'onion',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
type Formats = 'hostname-info' | 'urlstring' | 'url'
|
type Formats = 'hostname-info' | 'urlstring' | 'url'
|
||||||
type FormatReturnTy<
|
type FormatReturnTy<
|
||||||
F extends Filter,
|
F extends Filter,
|
||||||
@@ -124,7 +123,6 @@ export type Filled<F extends Filter = {}> = {
|
|||||||
|
|
||||||
nonLocal: Filled<typeof nonLocalFilter & Filter>
|
nonLocal: Filled<typeof nonLocalFilter & Filter>
|
||||||
public: Filled<typeof publicFilter & Filter>
|
public: Filled<typeof publicFilter & Filter>
|
||||||
onion: Filled<typeof onionFilter & Filter>
|
|
||||||
}
|
}
|
||||||
export type FilledAddressInfo = AddressInfo & Filled
|
export type FilledAddressInfo = AddressInfo & Filled
|
||||||
export type ServiceInterfaceFilled = {
|
export type ServiceInterfaceFilled = {
|
||||||
@@ -162,18 +160,14 @@ export const addressHostToUrl = (
|
|||||||
scheme in knownProtocols &&
|
scheme in knownProtocols &&
|
||||||
port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort
|
port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort
|
||||||
let hostname
|
let hostname
|
||||||
if (host.kind === 'onion') {
|
if (host.hostname.kind === 'domain') {
|
||||||
|
hostname = host.hostname.value
|
||||||
|
} else if (host.hostname.kind === 'ipv6') {
|
||||||
|
hostname = IPV6_LINK_LOCAL.contains(host.hostname.value)
|
||||||
|
? `[${host.hostname.value}%${host.hostname.scopeId}]`
|
||||||
|
: `[${host.hostname.value}]`
|
||||||
|
} else {
|
||||||
hostname = host.hostname.value
|
hostname = host.hostname.value
|
||||||
} else if (host.kind === 'ip') {
|
|
||||||
if (host.hostname.kind === 'domain') {
|
|
||||||
hostname = host.hostname.value
|
|
||||||
} else if (host.hostname.kind === 'ipv6') {
|
|
||||||
hostname = IPV6_LINK_LOCAL.contains(host.hostname.value)
|
|
||||||
? `[${host.hostname.value}%${host.hostname.scopeId}]`
|
|
||||||
: `[${host.hostname.value}]`
|
|
||||||
} else {
|
|
||||||
hostname = host.hostname.value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return `${scheme ? `${scheme}://` : ''}${
|
return `${scheme ? `${scheme}://` : ''}${
|
||||||
username ? `${username}@` : ''
|
username ? `${username}@` : ''
|
||||||
@@ -201,13 +195,9 @@ function filterRec(
|
|||||||
hostnames = hostnames.filter((h) => invert !== pred(h))
|
hostnames = hostnames.filter((h) => invert !== pred(h))
|
||||||
}
|
}
|
||||||
if (filter.visibility === 'public')
|
if (filter.visibility === 'public')
|
||||||
hostnames = hostnames.filter(
|
hostnames = hostnames.filter((h) => invert !== h.public)
|
||||||
(h) => invert !== (h.kind === 'onion' || h.public),
|
|
||||||
)
|
|
||||||
if (filter.visibility === 'private')
|
if (filter.visibility === 'private')
|
||||||
hostnames = hostnames.filter(
|
hostnames = hostnames.filter((h) => invert !== !h.public)
|
||||||
(h) => invert !== (h.kind !== 'onion' && !h.public),
|
|
||||||
)
|
|
||||||
if (filter.kind) {
|
if (filter.kind) {
|
||||||
const kind = new Set(
|
const kind = new Set(
|
||||||
Array.isArray(filter.kind) ? filter.kind : [filter.kind],
|
Array.isArray(filter.kind) ? filter.kind : [filter.kind],
|
||||||
@@ -219,19 +209,13 @@ function filterRec(
|
|||||||
hostnames = hostnames.filter(
|
hostnames = hostnames.filter(
|
||||||
(h) =>
|
(h) =>
|
||||||
invert !==
|
invert !==
|
||||||
((kind.has('onion') && h.kind === 'onion') ||
|
((kind.has('mdns') && h.hostname.kind === 'local') ||
|
||||||
(kind.has('mdns') &&
|
(kind.has('domain') && h.hostname.kind === 'domain') ||
|
||||||
h.kind === 'ip' &&
|
(kind.has('ipv4') && h.hostname.kind === 'ipv4') ||
|
||||||
h.hostname.kind === 'local') ||
|
(kind.has('ipv6') && h.hostname.kind === 'ipv6') ||
|
||||||
(kind.has('domain') &&
|
|
||||||
h.kind === 'ip' &&
|
|
||||||
h.hostname.kind === 'domain') ||
|
|
||||||
(kind.has('ipv4') && h.kind === 'ip' && h.hostname.kind === 'ipv4') ||
|
|
||||||
(kind.has('ipv6') && h.kind === 'ip' && h.hostname.kind === 'ipv6') ||
|
|
||||||
(kind.has('localhost') &&
|
(kind.has('localhost') &&
|
||||||
['localhost', '127.0.0.1', '::1'].includes(h.hostname.value)) ||
|
['localhost', '127.0.0.1', '::1'].includes(h.hostname.value)) ||
|
||||||
(kind.has('link-local') &&
|
(kind.has('link-local') &&
|
||||||
h.kind === 'ip' &&
|
|
||||||
h.hostname.kind === 'ipv6' &&
|
h.hostname.kind === 'ipv6' &&
|
||||||
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname.value)))),
|
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname.value)))),
|
||||||
)
|
)
|
||||||
@@ -242,6 +226,14 @@ function filterRec(
|
|||||||
return hostnames
|
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 = (
|
export const filledAddress = (
|
||||||
host: Host,
|
host: Host,
|
||||||
addressInfo: AddressInfo,
|
addressInfo: AddressInfo,
|
||||||
@@ -251,7 +243,8 @@ export const filledAddress = (
|
|||||||
const u = toUrls(h)
|
const u = toUrls(h)
|
||||||
return [u.url, u.sslUrl].filter((u) => u !== null)
|
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>(
|
function filledAddressFromHostnames<F extends Filter>(
|
||||||
hostnames: HostnameInfo[],
|
hostnames: HostnameInfo[],
|
||||||
@@ -266,11 +259,6 @@ export const filledAddress = (
|
|||||||
filterRec(hostnames, publicFilter, false),
|
filterRec(hostnames, publicFilter, false),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const getOnion = once(() =>
|
|
||||||
filledAddressFromHostnames<typeof onionFilter & F>(
|
|
||||||
filterRec(hostnames, onionFilter, false),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
...addressInfo,
|
...addressInfo,
|
||||||
hostnames,
|
hostnames,
|
||||||
@@ -294,9 +282,6 @@ export const filledAddress = (
|
|||||||
get public(): Filled<typeof publicFilter & F> {
|
get public(): Filled<typeof publicFilter & F> {
|
||||||
return getPublic()
|
return getPublic()
|
||||||
},
|
},
|
||||||
get onion(): Filled<typeof onionFilter & F> {
|
|
||||||
return getOnion()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,6 @@ export const localHostname: Pattern = {
|
|||||||
description: 'Must be a valid ".local" hostname',
|
description: 'Must be a valid ".local" hostname',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const torHostname: Pattern = {
|
|
||||||
regex: regexes.torHostname.matches(),
|
|
||||||
description: 'Must be a valid Tor (".onion") hostname',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const url: Pattern = {
|
export const url: Pattern = {
|
||||||
regex: regexes.url.matches(),
|
regex: regexes.url.matches(),
|
||||||
description: 'Must be a valid URL',
|
description: 'Must be a valid URL',
|
||||||
@@ -36,11 +31,6 @@ export const localUrl: Pattern = {
|
|||||||
description: 'Must be a valid ".local" URL',
|
description: 'Must be a valid ".local" URL',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const torUrl: Pattern = {
|
|
||||||
regex: regexes.torUrl.matches(),
|
|
||||||
description: 'Must be a valid Tor (".onion") URL',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ascii: Pattern = {
|
export const ascii: Pattern = {
|
||||||
regex: regexes.ascii.matches(),
|
regex: regexes.ascii.matches(),
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ export const localHostname = new ComposableRegex(
|
|||||||
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/,
|
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/,
|
||||||
)
|
)
|
||||||
|
|
||||||
export const torHostname = new ComposableRegex(
|
|
||||||
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion/,
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://ihateregex.io/expr/url/
|
// https://ihateregex.io/expr/url/
|
||||||
export const url = new ComposableRegex(
|
export const url = new ComposableRegex(
|
||||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
||||||
@@ -52,10 +48,6 @@ export const localUrl = new ComposableRegex(
|
|||||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
||||||
)
|
)
|
||||||
|
|
||||||
export const torUrl = new ComposableRegex(
|
|
||||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://ihateregex.io/expr/ascii/
|
// https://ihateregex.io/expr/ascii/
|
||||||
export const ascii = new ComposableRegex(/[ -~]*/)
|
export const ascii = new ComposableRegex(/[ -~]*/)
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ import {
|
|||||||
import { getOwnServiceInterfaces } from '../../base/lib/util/getServiceInterfaces'
|
import { getOwnServiceInterfaces } from '../../base/lib/util/getServiceInterfaces'
|
||||||
import { Volumes, createVolumes } from './util/Volume'
|
import { Volumes, createVolumes } from './util/Volume'
|
||||||
|
|
||||||
export const OSVersion = testTypeVersion('0.4.0-alpha.19')
|
export const OSVersion = testTypeVersion('0.4.0-alpha.20')
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
type AnyNeverCond<T extends any[], Then, Else> =
|
type AnyNeverCond<T extends any[], Then, Else> =
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as fs from "fs"
|
import * as fs from 'fs'
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/2970525/converting-any-string-into-camel-case
|
// https://stackoverflow.com/questions/2970525/converting-any-string-into-camel-case
|
||||||
export function camelCase(value: string) {
|
export function camelCase(value: string) {
|
||||||
return value
|
return value
|
||||||
.replace(/([\(\)\[\]])/g, "")
|
.replace(/([\(\)\[\]])/g, '')
|
||||||
.replace(/^([A-Z])|[\s-_](\w)/g, function (match, p1, p2, offset) {
|
.replace(/^([A-Z])|[\s-_](\w)/g, function (match, p1, p2, offset) {
|
||||||
if (p2) return p2.toUpperCase()
|
if (p2) return p2.toUpperCase()
|
||||||
return p1.toLowerCase()
|
return p1.toLowerCase()
|
||||||
@@ -23,12 +23,12 @@ export async function oldSpecToBuilder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isString(x: unknown): x is string {
|
function isString(x: unknown): x is string {
|
||||||
return typeof x === "string"
|
return typeof x === 'string'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function makeFileContentFromOld(
|
export default async function makeFileContentFromOld(
|
||||||
inputData: Promise<any> | any,
|
inputData: Promise<any> | any,
|
||||||
{ StartSdk = "start-sdk", nested = true } = {},
|
{ StartSdk = 'start-sdk', nested = true } = {},
|
||||||
) {
|
) {
|
||||||
const outputLines: string[] = []
|
const outputLines: string[] = []
|
||||||
outputLines.push(`
|
outputLines.push(`
|
||||||
@@ -37,16 +37,16 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
`)
|
`)
|
||||||
const data = await inputData
|
const data = await inputData
|
||||||
|
|
||||||
const namedConsts = new Set(["InputSpec", "Value", "List"])
|
const namedConsts = new Set(['InputSpec', 'Value', 'List'])
|
||||||
const inputSpecName = newConst("inputSpecSpec", convertInputSpec(data))
|
const inputSpecName = newConst('inputSpecSpec', convertInputSpec(data))
|
||||||
outputLines.push(`export type InputSpecSpec = typeof ${inputSpecName}._TYPE;`)
|
outputLines.push(`export type InputSpecSpec = typeof ${inputSpecName}._TYPE;`)
|
||||||
|
|
||||||
return outputLines.join("\n")
|
return outputLines.join('\n')
|
||||||
|
|
||||||
function newConst(key: string, data: string, type?: string) {
|
function newConst(key: string, data: string, type?: string) {
|
||||||
const variableName = getNextConstName(camelCase(key))
|
const variableName = getNextConstName(camelCase(key))
|
||||||
outputLines.push(
|
outputLines.push(
|
||||||
`export const ${variableName}${!type ? "" : `: ${type}`} = ${data};`,
|
`export const ${variableName}${!type ? '' : `: ${type}`} = ${data};`,
|
||||||
)
|
)
|
||||||
return variableName
|
return variableName
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
return newConst(key, data)
|
return newConst(key, data)
|
||||||
}
|
}
|
||||||
function convertInputSpecInner(data: any) {
|
function convertInputSpecInner(data: any) {
|
||||||
let answer = "{"
|
let answer = '{'
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
const variableName = maybeNewConst(key, convertValueSpec(value))
|
const variableName = maybeNewConst(key, convertValueSpec(value))
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
}
|
}
|
||||||
function convertValueSpec(value: any): string {
|
function convertValueSpec(value: any): string {
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case "string": {
|
case 'string': {
|
||||||
if (value.textarea) {
|
if (value.textarea) {
|
||||||
return `${rangeToTodoComment(
|
return `${rangeToTodoComment(
|
||||||
value?.range,
|
value?.range,
|
||||||
@@ -99,12 +99,12 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
warning: value.warning || null,
|
warning: value.warning || null,
|
||||||
masked: value.masked || false,
|
masked: value.masked || false,
|
||||||
placeholder: value.placeholder || null,
|
placeholder: value.placeholder || null,
|
||||||
inputmode: "text",
|
inputmode: 'text',
|
||||||
patterns: value.pattern
|
patterns: value.pattern
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
regex: value.pattern,
|
regex: value.pattern,
|
||||||
description: value["pattern-description"],
|
description: value['pattern-description'],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
@@ -115,7 +115,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
2,
|
2,
|
||||||
)})`
|
)})`
|
||||||
}
|
}
|
||||||
case "number": {
|
case 'number': {
|
||||||
return `${rangeToTodoComment(
|
return `${rangeToTodoComment(
|
||||||
value?.range,
|
value?.range,
|
||||||
)}Value.number(${JSON.stringify(
|
)}Value.number(${JSON.stringify(
|
||||||
@@ -136,7 +136,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
2,
|
2,
|
||||||
)})`
|
)})`
|
||||||
}
|
}
|
||||||
case "boolean": {
|
case 'boolean': {
|
||||||
return `Value.toggle(${JSON.stringify(
|
return `Value.toggle(${JSON.stringify(
|
||||||
{
|
{
|
||||||
name: value.name || null,
|
name: value.name || null,
|
||||||
@@ -148,15 +148,15 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
2,
|
2,
|
||||||
)})`
|
)})`
|
||||||
}
|
}
|
||||||
case "enum": {
|
case 'enum': {
|
||||||
const allValueNames = new Set([
|
const allValueNames = new Set([
|
||||||
...(value?.["values"] || []),
|
...(value?.['values'] || []),
|
||||||
...Object.keys(value?.["value-names"] || {}),
|
...Object.keys(value?.['value-names'] || {}),
|
||||||
])
|
])
|
||||||
const values = Object.fromEntries(
|
const values = Object.fromEntries(
|
||||||
Array.from(allValueNames)
|
Array.from(allValueNames)
|
||||||
.filter(isString)
|
.filter(isString)
|
||||||
.map((key) => [key, value?.spec?.["value-names"]?.[key] || key]),
|
.map((key) => [key, value?.spec?.['value-names']?.[key] || key]),
|
||||||
)
|
)
|
||||||
return `Value.select(${JSON.stringify(
|
return `Value.select(${JSON.stringify(
|
||||||
{
|
{
|
||||||
@@ -170,9 +170,9 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
2,
|
2,
|
||||||
)} as const)`
|
)} as const)`
|
||||||
}
|
}
|
||||||
case "object": {
|
case 'object': {
|
||||||
const specName = maybeNewConst(
|
const specName = maybeNewConst(
|
||||||
value.name + "_spec",
|
value.name + '_spec',
|
||||||
convertInputSpec(value.spec),
|
convertInputSpec(value.spec),
|
||||||
)
|
)
|
||||||
return `Value.object({
|
return `Value.object({
|
||||||
@@ -180,10 +180,10 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
description: ${JSON.stringify(value.description || null)},
|
description: ${JSON.stringify(value.description || null)},
|
||||||
}, ${specName})`
|
}, ${specName})`
|
||||||
}
|
}
|
||||||
case "union": {
|
case 'union': {
|
||||||
const variants = maybeNewConst(
|
const variants = maybeNewConst(
|
||||||
value.name + "_variants",
|
value.name + '_variants',
|
||||||
convertVariants(value.variants, value.tag["variant-names"] || {}),
|
convertVariants(value.variants, value.tag['variant-names'] || {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return `Value.union({
|
return `Value.union({
|
||||||
@@ -194,18 +194,18 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
variants: ${variants},
|
variants: ${variants},
|
||||||
})`
|
})`
|
||||||
}
|
}
|
||||||
case "list": {
|
case 'list': {
|
||||||
if (value.subtype === "enum") {
|
if (value.subtype === 'enum') {
|
||||||
const allValueNames = new Set([
|
const allValueNames = new Set([
|
||||||
...(value?.spec?.["values"] || []),
|
...(value?.spec?.['values'] || []),
|
||||||
...Object.keys(value?.spec?.["value-names"] || {}),
|
...Object.keys(value?.spec?.['value-names'] || {}),
|
||||||
])
|
])
|
||||||
const values = Object.fromEntries(
|
const values = Object.fromEntries(
|
||||||
Array.from(allValueNames)
|
Array.from(allValueNames)
|
||||||
.filter(isString)
|
.filter(isString)
|
||||||
.map((key: string) => [
|
.map((key: string) => [
|
||||||
key,
|
key,
|
||||||
value?.spec?.["value-names"]?.[key] ?? key,
|
value?.spec?.['value-names']?.[key] ?? key,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
return `Value.multiselect(${JSON.stringify(
|
return `Value.multiselect(${JSON.stringify(
|
||||||
@@ -222,10 +222,10 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
2,
|
2,
|
||||||
)})`
|
)})`
|
||||||
}
|
}
|
||||||
const list = maybeNewConst(value.name + "_list", convertList(value))
|
const list = maybeNewConst(value.name + '_list', convertList(value))
|
||||||
return `Value.list(${list})`
|
return `Value.list(${list})`
|
||||||
}
|
}
|
||||||
case "pointer": {
|
case 'pointer': {
|
||||||
return `/* TODO deal with point removed point "${value.name}" */null as any`
|
return `/* TODO deal with point removed point "${value.name}" */null as any`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,7 +234,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
|
|
||||||
function convertList(value: any) {
|
function convertList(value: any) {
|
||||||
switch (value.subtype) {
|
switch (value.subtype) {
|
||||||
case "string": {
|
case 'string': {
|
||||||
return `${rangeToTodoComment(value?.range)}List.text(${JSON.stringify(
|
return `${rangeToTodoComment(value?.range)}List.text(${JSON.stringify(
|
||||||
{
|
{
|
||||||
name: value.name || null,
|
name: value.name || null,
|
||||||
@@ -253,7 +253,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
regex: value.spec.pattern,
|
regex: value.spec.pattern,
|
||||||
description: value?.spec?.["pattern-description"],
|
description: value?.spec?.['pattern-description'],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
@@ -281,12 +281,12 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
// placeholder: value?.spec?.placeholder || null,
|
// placeholder: value?.spec?.placeholder || null,
|
||||||
// })})`
|
// })})`
|
||||||
// }
|
// }
|
||||||
case "enum": {
|
case 'enum': {
|
||||||
return "/* error!! list.enum */"
|
return '/* error!! list.enum */'
|
||||||
}
|
}
|
||||||
case "object": {
|
case 'object': {
|
||||||
const specName = maybeNewConst(
|
const specName = maybeNewConst(
|
||||||
value.name + "_spec",
|
value.name + '_spec',
|
||||||
convertInputSpec(value.spec.spec),
|
convertInputSpec(value.spec.spec),
|
||||||
)
|
)
|
||||||
return `${rangeToTodoComment(value?.range)}List.obj({
|
return `${rangeToTodoComment(value?.range)}List.obj({
|
||||||
@@ -297,20 +297,20 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
description: ${JSON.stringify(value.description || null)},
|
description: ${JSON.stringify(value.description || null)},
|
||||||
}, {
|
}, {
|
||||||
spec: ${specName},
|
spec: ${specName},
|
||||||
displayAs: ${JSON.stringify(value?.spec?.["display-as"] || null)},
|
displayAs: ${JSON.stringify(value?.spec?.['display-as'] || null)},
|
||||||
uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)},
|
uniqueBy: ${JSON.stringify(value?.spec?.['unique-by'] || null)},
|
||||||
})`
|
})`
|
||||||
}
|
}
|
||||||
case "union": {
|
case 'union': {
|
||||||
const variants = maybeNewConst(
|
const variants = maybeNewConst(
|
||||||
value.name + "_variants",
|
value.name + '_variants',
|
||||||
convertVariants(
|
convertVariants(
|
||||||
value.spec.variants,
|
value.spec.variants,
|
||||||
value.spec["variant-names"] || {},
|
value.spec['variant-names'] || {},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const unionValueName = maybeNewConst(
|
const unionValueName = maybeNewConst(
|
||||||
value.name + "_union",
|
value.name + '_union',
|
||||||
`${rangeToTodoComment(value?.range)}
|
`${rangeToTodoComment(value?.range)}
|
||||||
Value.union({
|
Value.union({
|
||||||
name: ${JSON.stringify(value?.spec?.tag?.name || null)},
|
name: ${JSON.stringify(value?.spec?.tag?.name || null)},
|
||||||
@@ -324,7 +324,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
const listInputSpec = maybeNewConst(
|
const listInputSpec = maybeNewConst(
|
||||||
value.name + "_list_inputSpec",
|
value.name + '_list_inputSpec',
|
||||||
`
|
`
|
||||||
InputSpec.of({
|
InputSpec.of({
|
||||||
"union": ${unionValueName}
|
"union": ${unionValueName}
|
||||||
@@ -340,8 +340,8 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
warning: ${JSON.stringify(value.warning || null)},
|
warning: ${JSON.stringify(value.warning || null)},
|
||||||
}, {
|
}, {
|
||||||
spec: ${listInputSpec},
|
spec: ${listInputSpec},
|
||||||
displayAs: ${JSON.stringify(value?.spec?.["display-as"] || null)},
|
displayAs: ${JSON.stringify(value?.spec?.['display-as'] || null)},
|
||||||
uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)},
|
uniqueBy: ${JSON.stringify(value?.spec?.['unique-by'] || null)},
|
||||||
})`
|
})`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,7 +352,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
variants: Record<string, unknown>,
|
variants: Record<string, unknown>,
|
||||||
variantNames: Record<string, string>,
|
variantNames: Record<string, string>,
|
||||||
): string {
|
): string {
|
||||||
let answer = "Variants.of({"
|
let answer = 'Variants.of({'
|
||||||
for (const [key, value] of Object.entries(variants)) {
|
for (const [key, value] of Object.entries(variants)) {
|
||||||
const variantSpec = maybeNewConst(key, convertInputSpec(value))
|
const variantSpec = maybeNewConst(key, convertInputSpec(value))
|
||||||
answer += `"${key}": {name: "${
|
answer += `"${key}": {name: "${
|
||||||
@@ -373,7 +373,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
|||||||
}
|
}
|
||||||
|
|
||||||
function rangeToTodoComment(range: string | undefined) {
|
function rangeToTodoComment(range: string | undefined) {
|
||||||
if (!range) return ""
|
if (!range) return ''
|
||||||
return `/* TODO: Convert range for this value (${range})*/`
|
return `/* TODO: Convert range for this value (${range})*/`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.19",
|
"version": "0.4.0-alpha.20",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.19",
|
"version": "0.4.0-alpha.20",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^20.3.0",
|
"@angular/animations": "^20.3.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.19",
|
"version": "0.4.0-alpha.20",
|
||||||
"author": "Start9 Labs, Inc",
|
"author": "Start9 Labs, Inc",
|
||||||
"homepage": "https://start9.com/",
|
"homepage": "https://start9.com/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -679,4 +679,16 @@ export default {
|
|||||||
714: 'Installation abgeschlossen!',
|
714: 'Installation abgeschlossen!',
|
||||||
715: 'StartOS wurde erfolgreich installiert.',
|
715: 'StartOS wurde erfolgreich installiert.',
|
||||||
716: 'Weiter zur Einrichtung',
|
716: 'Weiter zur Einrichtung',
|
||||||
|
717: '',
|
||||||
|
718: '',
|
||||||
|
719: '',
|
||||||
|
720: '',
|
||||||
|
721: '',
|
||||||
|
722: '',
|
||||||
|
723: '',
|
||||||
|
724: '',
|
||||||
|
725: '',
|
||||||
|
726: '',
|
||||||
|
727: '',
|
||||||
|
728: '',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -679,4 +679,16 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Installation Complete!': 714,
|
'Installation Complete!': 714,
|
||||||
'StartOS has been installed successfully.': 715,
|
'StartOS has been installed successfully.': 715,
|
||||||
'Continue to Setup': 716,
|
'Continue to Setup': 716,
|
||||||
|
'Set Outbound Gateway': 717,
|
||||||
|
'Current': 718,
|
||||||
|
'System default)': 719,
|
||||||
|
'Outbound Gateway': 720,
|
||||||
|
'Select the gateway for outbound traffic': 721,
|
||||||
|
'The type of gateway': 722,
|
||||||
|
'Outbound Only': 723,
|
||||||
|
'Set as default outbound': 724,
|
||||||
|
'Route all outbound traffic through this gateway': 725,
|
||||||
|
'Wireguard Config File': 726,
|
||||||
|
'Inbound/Outbound': 727,
|
||||||
|
'StartTunnel (Inbound/Outbound)': 728,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -679,4 +679,16 @@ export default {
|
|||||||
714: '¡Instalación completada!',
|
714: '¡Instalación completada!',
|
||||||
715: 'StartOS se ha instalado correctamente.',
|
715: 'StartOS se ha instalado correctamente.',
|
||||||
716: 'Continuar con la configuración',
|
716: 'Continuar con la configuración',
|
||||||
|
717: '',
|
||||||
|
718: '',
|
||||||
|
719: '',
|
||||||
|
720: '',
|
||||||
|
721: '',
|
||||||
|
722: '',
|
||||||
|
723: '',
|
||||||
|
724: '',
|
||||||
|
725: '',
|
||||||
|
726: '',
|
||||||
|
727: '',
|
||||||
|
728: '',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -679,4 +679,16 @@ export default {
|
|||||||
714: 'Installation terminée !',
|
714: 'Installation terminée !',
|
||||||
715: 'StartOS a été installé avec succès.',
|
715: 'StartOS a été installé avec succès.',
|
||||||
716: 'Continuer vers la configuration',
|
716: 'Continuer vers la configuration',
|
||||||
|
717: '',
|
||||||
|
718: '',
|
||||||
|
719: '',
|
||||||
|
720: '',
|
||||||
|
721: '',
|
||||||
|
722: '',
|
||||||
|
723: '',
|
||||||
|
724: '',
|
||||||
|
725: '',
|
||||||
|
726: '',
|
||||||
|
727: '',
|
||||||
|
728: '',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -679,4 +679,16 @@ export default {
|
|||||||
714: 'Instalacja zakończona!',
|
714: 'Instalacja zakończona!',
|
||||||
715: 'StartOS został pomyślnie zainstalowany.',
|
715: 'StartOS został pomyślnie zainstalowany.',
|
||||||
716: 'Przejdź do konfiguracji',
|
716: 'Przejdź do konfiguracji',
|
||||||
|
717: '',
|
||||||
|
718: '',
|
||||||
|
719: '',
|
||||||
|
720: '',
|
||||||
|
721: '',
|
||||||
|
722: '',
|
||||||
|
723: '',
|
||||||
|
724: '',
|
||||||
|
725: '',
|
||||||
|
726: '',
|
||||||
|
727: '',
|
||||||
|
728: '',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export type AccessType =
|
export type AccessType =
|
||||||
| 'tor'
|
|
||||||
| 'mdns'
|
| 'mdns'
|
||||||
| 'localhost'
|
| 'localhost'
|
||||||
| 'ipv4'
|
| 'ipv4'
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export const mockTunnelData: TunnelData = {
|
|||||||
gateways: {
|
gateways: {
|
||||||
eth0: {
|
eth0: {
|
||||||
name: null,
|
name: null,
|
||||||
public: null,
|
|
||||||
secure: null,
|
secure: null,
|
||||||
|
type: null,
|
||||||
ipInfo: {
|
ipInfo: {
|
||||||
name: 'Wired Connection 1',
|
name: 'Wired Connection 1',
|
||||||
scopeId: 1,
|
scopeId: 1,
|
||||||
|
|||||||
@@ -83,32 +83,8 @@ export class InterfaceGatewaysComponent {
|
|||||||
|
|
||||||
readonly gateways = input.required<InterfaceGateway[] | undefined>()
|
readonly gateways = input.required<InterfaceGateway[] | undefined>()
|
||||||
|
|
||||||
async onToggle(gateway: InterfaceGateway) {
|
async onToggle(_gateway: InterfaceGateway) {
|
||||||
const addressInfo = this.interface.value()!.addressInfo
|
// TODO: Replace with per-address toggle UI (Section 6 frontend overhaul).
|
||||||
const pkgId = this.interface.packageId()
|
// Gateway-level toggle replaced by set-address-enabled RPC.
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
|||||||
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||||
import { MappedServiceInterface } from './interface.service'
|
import { MappedServiceInterface } from './interface.service'
|
||||||
import { InterfaceGatewaysComponent } from './gateways.component'
|
import { InterfaceGatewaysComponent } from './gateways.component'
|
||||||
import { InterfaceTorDomainsComponent } from './tor-domains.component'
|
|
||||||
import { PublicDomainsComponent } from './public-domains/pd.component'
|
import { PublicDomainsComponent } from './public-domains/pd.component'
|
||||||
import { InterfacePrivateDomainsComponent } from './private-domains.component'
|
import { InterfacePrivateDomainsComponent } from './private-domains.component'
|
||||||
import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||||
@@ -16,7 +15,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
|||||||
[publicDomains]="value()?.publicDomains"
|
[publicDomains]="value()?.publicDomains"
|
||||||
[addSsl]="value()?.addSsl || false"
|
[addSsl]="value()?.addSsl || false"
|
||||||
></section>
|
></section>
|
||||||
<section [torDomains]="value()?.torDomains"></section>
|
|
||||||
<section [privateDomains]="value()?.privateDomains"></section>
|
<section [privateDomains]="value()?.privateDomains"></section>
|
||||||
</div>
|
</div>
|
||||||
<hr [style.width.rem]="10" />
|
<hr [style.width.rem]="10" />
|
||||||
@@ -52,7 +50,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
|||||||
providers: [tuiButtonOptionsProvider({ size: 'xs' })],
|
providers: [tuiButtonOptionsProvider({ size: 'xs' })],
|
||||||
imports: [
|
imports: [
|
||||||
InterfaceGatewaysComponent,
|
InterfaceGatewaysComponent,
|
||||||
InterfaceTorDomainsComponent,
|
|
||||||
PublicDomainsComponent,
|
PublicDomainsComponent,
|
||||||
InterfacePrivateDomainsComponent,
|
InterfacePrivateDomainsComponent,
|
||||||
InterfaceAddressesComponent,
|
InterfaceAddressesComponent,
|
||||||
|
|||||||
@@ -27,17 +27,9 @@ function cmpWithRankedPredicates<T extends AddressWithInfo>(
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
type TorAddress = AddressWithInfo & { info: { kind: 'onion' } }
|
type LanAddress = AddressWithInfo & { info: { public: false } }
|
||||||
function filterTor(a: AddressWithInfo): a is TorAddress {
|
|
||||||
return a.info.kind === 'onion'
|
|
||||||
}
|
|
||||||
function cmpTor(a: TorAddress, b: TorAddress): -1 | 0 | 1 {
|
|
||||||
return cmpWithRankedPredicates(a, b, [x => !x.showSsl])
|
|
||||||
}
|
|
||||||
|
|
||||||
type LanAddress = AddressWithInfo & { info: { kind: 'ip'; public: false } }
|
|
||||||
function filterLan(a: AddressWithInfo): a is LanAddress {
|
function filterLan(a: AddressWithInfo): a is LanAddress {
|
||||||
return a.info.kind === 'ip' && !a.info.public
|
return !a.info.public
|
||||||
}
|
}
|
||||||
function cmpLan(host: T.Host, a: LanAddress, b: LanAddress): -1 | 0 | 1 {
|
function cmpLan(host: T.Host, a: LanAddress, b: LanAddress): -1 | 0 | 1 {
|
||||||
return cmpWithRankedPredicates(a, b, [
|
return cmpWithRankedPredicates(a, b, [
|
||||||
@@ -53,15 +45,12 @@ function cmpLan(host: T.Host, a: LanAddress, b: LanAddress): -1 | 0 | 1 {
|
|||||||
|
|
||||||
type VpnAddress = AddressWithInfo & {
|
type VpnAddress = AddressWithInfo & {
|
||||||
info: {
|
info: {
|
||||||
kind: 'ip'
|
|
||||||
public: false
|
public: false
|
||||||
hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
|
hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function filterVpn(a: AddressWithInfo): a is VpnAddress {
|
function filterVpn(a: AddressWithInfo): a is VpnAddress {
|
||||||
return (
|
return !a.info.public && a.info.hostname.kind !== 'local'
|
||||||
a.info.kind === 'ip' && !a.info.public && a.info.hostname.kind !== 'local'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 {
|
function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 {
|
||||||
return cmpWithRankedPredicates(a, b, [
|
return cmpWithRankedPredicates(a, b, [
|
||||||
@@ -76,13 +65,12 @@ function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 {
|
|||||||
|
|
||||||
type ClearnetAddress = AddressWithInfo & {
|
type ClearnetAddress = AddressWithInfo & {
|
||||||
info: {
|
info: {
|
||||||
kind: 'ip'
|
|
||||||
public: true
|
public: true
|
||||||
hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
|
hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function filterClearnet(a: AddressWithInfo): a is ClearnetAddress {
|
function filterClearnet(a: AddressWithInfo): a is ClearnetAddress {
|
||||||
return a.info.kind === 'ip' && a.info.public
|
return a.info.public
|
||||||
}
|
}
|
||||||
function cmpClearnet(
|
function cmpClearnet(
|
||||||
host: T.Host,
|
host: T.Host,
|
||||||
@@ -142,10 +130,7 @@ export class InterfaceService {
|
|||||||
h,
|
h,
|
||||||
)
|
)
|
||||||
const info = h
|
const info = h
|
||||||
const gateway =
|
const gateway = gateways.find(g => h.gateway.id === g.id)
|
||||||
h.kind === 'ip'
|
|
||||||
? gateways.find(g => h.gateway.id === g.id)
|
|
||||||
: undefined
|
|
||||||
const res = []
|
const res = []
|
||||||
if (url) {
|
if (url) {
|
||||||
res.push({
|
res.push({
|
||||||
@@ -171,7 +156,6 @@ export class InterfaceService {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor)
|
|
||||||
const lanAddrs = allAddressesWithInfo
|
const lanAddrs = allAddressesWithInfo
|
||||||
.filter(filterLan)
|
.filter(filterLan)
|
||||||
.sort((a, b) => cmpLan(host, a, b))
|
.sort((a, b) => cmpLan(host, a, b))
|
||||||
@@ -188,7 +172,6 @@ export class InterfaceService {
|
|||||||
clearnetAddrs[0],
|
clearnetAddrs[0],
|
||||||
lanAddrs[0],
|
lanAddrs[0],
|
||||||
vpnAddrs[0],
|
vpnAddrs[0],
|
||||||
torAddrs[0],
|
|
||||||
]
|
]
|
||||||
.filter(a => !!a)
|
.filter(a => !!a)
|
||||||
.reduce((acc, x) => {
|
.reduce((acc, x) => {
|
||||||
@@ -214,9 +197,8 @@ export class InterfaceService {
|
|||||||
kind: 'domain',
|
kind: 'domain',
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
})
|
})
|
||||||
const tor = addresses.filter({ kind: 'onion' })
|
|
||||||
const wanIp = addresses.filter({ kind: 'ipv4', visibility: 'public' })
|
const wanIp = addresses.filter({ kind: 'ipv4', visibility: 'public' })
|
||||||
const bestPublic = [publicDomains, tor, wanIp].flatMap(h =>
|
const bestPublic = [publicDomains, wanIp].flatMap(h =>
|
||||||
h.format('urlstring'),
|
h.format('urlstring'),
|
||||||
)[0]
|
)[0]
|
||||||
const privateDomains = addresses.filter({
|
const privateDomains = addresses.filter({
|
||||||
@@ -254,9 +236,6 @@ export class InterfaceService {
|
|||||||
.format('urlstring')[0]
|
.format('urlstring')[0]
|
||||||
onLan = true
|
onLan = true
|
||||||
break
|
break
|
||||||
case 'tor':
|
|
||||||
matching = tor.format('urlstring')[0]
|
|
||||||
break
|
|
||||||
case 'mdns':
|
case 'mdns':
|
||||||
matching = mdns.format('urlstring')[0]
|
matching = mdns.format('urlstring')[0]
|
||||||
onLan = true
|
onLan = true
|
||||||
@@ -273,19 +252,23 @@ export class InterfaceService {
|
|||||||
serviceInterface: T.ServiceInterface,
|
serviceInterface: T.ServiceInterface,
|
||||||
host: T.Host,
|
host: T.Host,
|
||||||
): T.HostnameInfo[] {
|
): T.HostnameInfo[] {
|
||||||
let hostnameInfo =
|
const binding =
|
||||||
host.hostnameInfo[serviceInterface.addressInfo.internalPort]
|
host.bindings[serviceInterface.addressInfo.internalPort]
|
||||||
return (
|
if (!binding) return []
|
||||||
hostnameInfo?.filter(
|
const addr = binding.addresses
|
||||||
h =>
|
const enabled = addr.possible.filter(h =>
|
||||||
this.config.accessType === 'localhost' ||
|
h.public
|
||||||
!(
|
? addr.publicEnabled.some(e => utils.deepEqual(e, h))
|
||||||
h.kind === 'ip' &&
|
: !addr.privateDisabled.some(d => utils.deepEqual(d, h)),
|
||||||
((h.hostname.kind === 'ipv6' &&
|
)
|
||||||
utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) ||
|
return enabled.filter(
|
||||||
h.gateway.id === 'lo')
|
h =>
|
||||||
),
|
this.config.accessType === 'localhost' ||
|
||||||
) || []
|
!(
|
||||||
|
(h.hostname.kind === 'ipv6' &&
|
||||||
|
utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) ||
|
||||||
|
h.gateway.id === 'lo'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,31 +285,7 @@ export class InterfaceService {
|
|||||||
"Requires trusting your server's Root CA",
|
"Requires trusting your server's Root CA",
|
||||||
)
|
)
|
||||||
|
|
||||||
// ** Tor **
|
{
|
||||||
if (info.kind === 'onion') {
|
|
||||||
access = null
|
|
||||||
gatewayName = null
|
|
||||||
type = 'Tor'
|
|
||||||
bullets = [
|
|
||||||
this.i18n.transform('Connections can be slow or unreliable at times'),
|
|
||||||
this.i18n.transform(
|
|
||||||
'Public if you share the address publicly, otherwise private',
|
|
||||||
),
|
|
||||||
this.i18n.transform('Requires using a Tor-enabled device or browser'),
|
|
||||||
]
|
|
||||||
// Tor (SSL)
|
|
||||||
if (showSsl) {
|
|
||||||
bullets = [rootCaRequired, ...bullets]
|
|
||||||
// Tor (NON-SSL)
|
|
||||||
} else {
|
|
||||||
bullets.unshift(
|
|
||||||
this.i18n.transform(
|
|
||||||
'Ideal for anonymous, censorship-resistant hosting and remote access',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// ** Not Tor **
|
|
||||||
} else {
|
|
||||||
const port = info.hostname.sslPort || info.hostname.port
|
const port = info.hostname.sslPort || info.hostname.port
|
||||||
gatewayName = info.gateway.name
|
gatewayName = info.gateway.name
|
||||||
|
|
||||||
@@ -479,7 +438,6 @@ export class InterfaceService {
|
|||||||
|
|
||||||
export type MappedServiceInterface = T.ServiceInterface & {
|
export type MappedServiceInterface = T.ServiceInterface & {
|
||||||
gateways: InterfaceGateway[]
|
gateways: InterfaceGateway[]
|
||||||
torDomains: string[]
|
|
||||||
publicDomains: PublicDomain[]
|
publicDomains: PublicDomain[]
|
||||||
privateDomains: string[]
|
privateDomains: string[]
|
||||||
addresses: {
|
addresses: {
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
input,
|
|
||||||
} from '@angular/core'
|
|
||||||
import {
|
|
||||||
DialogService,
|
|
||||||
DocsLinkDirective,
|
|
||||||
ErrorService,
|
|
||||||
i18nPipe,
|
|
||||||
LoadingService,
|
|
||||||
} from '@start9labs/shared'
|
|
||||||
import { ISB, utils } from '@start9labs/start-sdk'
|
|
||||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
|
||||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
|
||||||
import { filter } from 'rxjs'
|
|
||||||
import {
|
|
||||||
FormComponent,
|
|
||||||
FormContext,
|
|
||||||
} from 'src/app/routes/portal/components/form.component'
|
|
||||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
|
||||||
|
|
||||||
import { InterfaceComponent } from './interface.component'
|
|
||||||
|
|
||||||
type OnionForm = {
|
|
||||||
key: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'section[torDomains]',
|
|
||||||
template: `
|
|
||||||
<header>
|
|
||||||
{{ 'Tor Domains' | i18n }}
|
|
||||||
<a
|
|
||||||
tuiIconButton
|
|
||||||
docsLink
|
|
||||||
path="/user-manual/connecting-remotely/tor.html"
|
|
||||||
appearance="icon"
|
|
||||||
iconStart="@tui.external-link"
|
|
||||||
>
|
|
||||||
{{ 'Documentation' | i18n }}
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
tuiButton
|
|
||||||
iconStart="@tui.plus"
|
|
||||||
[style.margin-inline-start]="'auto'"
|
|
||||||
(click)="add()"
|
|
||||||
[disabled]="!torDomains()"
|
|
||||||
>
|
|
||||||
{{ 'Add' | i18n }}
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
@for (domain of torDomains(); track domain) {
|
|
||||||
<div tuiCell="s">
|
|
||||||
<span tuiTitle>{{ domain }}</span>
|
|
||||||
<button
|
|
||||||
tuiIconButton
|
|
||||||
iconStart="@tui.trash"
|
|
||||||
appearance="action-destructive"
|
|
||||||
(click)="remove(domain)"
|
|
||||||
>
|
|
||||||
{{ 'Delete' | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
} @empty {
|
|
||||||
@if (torDomains()) {
|
|
||||||
<app-placeholder icon="@tui.target">
|
|
||||||
{{ 'No Tor domains' | i18n }}
|
|
||||||
</app-placeholder>
|
|
||||||
} @else {
|
|
||||||
@for (_ of [0, 1]; track $index) {
|
|
||||||
<label tuiCell="s">
|
|
||||||
<span tuiTitle [tuiSkeleton]="true">{{ 'Loading' | i18n }}</span>
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
styles: `
|
|
||||||
:host {
|
|
||||||
grid-column: span 6;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
host: { class: 'g-card' },
|
|
||||||
imports: [
|
|
||||||
TuiCell,
|
|
||||||
TuiTitle,
|
|
||||||
TuiButton,
|
|
||||||
PlaceholderComponent,
|
|
||||||
i18nPipe,
|
|
||||||
DocsLinkDirective,
|
|
||||||
TuiSkeleton,
|
|
||||||
],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
})
|
|
||||||
export class InterfaceTorDomainsComponent {
|
|
||||||
private readonly dialog = inject(DialogService)
|
|
||||||
private readonly formDialog = inject(FormDialogService)
|
|
||||||
private readonly loader = inject(LoadingService)
|
|
||||||
private readonly errorService = inject(ErrorService)
|
|
||||||
private readonly api = inject(ApiService)
|
|
||||||
private readonly interface = inject(InterfaceComponent)
|
|
||||||
private readonly i18n = inject(i18nPipe)
|
|
||||||
|
|
||||||
readonly torDomains = input.required<readonly string[] | undefined>()
|
|
||||||
|
|
||||||
async remove(onion: string) {
|
|
||||||
this.dialog
|
|
||||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(async () => {
|
|
||||||
const loader = this.loader.open('Removing').subscribe()
|
|
||||||
const params = { onion }
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.interface.packageId()) {
|
|
||||||
await this.api.pkgRemoveOnion({
|
|
||||||
...params,
|
|
||||||
package: this.interface.packageId(),
|
|
||||||
host: this.interface.value()?.addressInfo.hostId || '',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverRemoveOnion(params)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async add() {
|
|
||||||
this.formDialog.open<FormContext<OnionForm>>(FormComponent, {
|
|
||||||
label: 'New Tor domain',
|
|
||||||
data: {
|
|
||||||
spec: await configBuilderToSpec(
|
|
||||||
ISB.InputSpec.of({
|
|
||||||
key: ISB.Value.text({
|
|
||||||
name: this.i18n.transform('Private Key (optional)')!,
|
|
||||||
description: this.i18n.transform(
|
|
||||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) domain. If not provided, a random key will be generated.',
|
|
||||||
),
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
patterns: [utils.Patterns.base64],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: this.i18n.transform('Save')!,
|
|
||||||
handler: async value => this.save(value.key),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async save(key?: string): Promise<boolean> {
|
|
||||||
const loader = this.loader.open('Saving').subscribe()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const onion = key
|
|
||||||
? await this.api.addTorKey({ key })
|
|
||||||
: await this.api.generateTorKey({})
|
|
||||||
|
|
||||||
if (this.interface.packageId()) {
|
|
||||||
await this.api.pkgAddOnion({
|
|
||||||
onion,
|
|
||||||
package: this.interface.packageId(),
|
|
||||||
host: this.interface.value()?.addressInfo.hostId || '',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverAddOnion({ onion })
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,10 +13,6 @@ export const ROUTES: Routes = [
|
|||||||
path: 'os',
|
path: 'os',
|
||||||
loadComponent: () => import('./routes/os.component'),
|
loadComponent: () => import('./routes/os.component'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'tor',
|
|
||||||
loadComponent: () => import('./routes/tor.component'),
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export default ROUTES
|
export default ROUTES
|
||||||
|
|||||||
@@ -79,12 +79,6 @@ export default class SystemLogsComponent {
|
|||||||
subtitle: 'Raw, unfiltered operating system logs',
|
subtitle: 'Raw, unfiltered operating system logs',
|
||||||
icon: '@tui.square-dashed-bottom-code',
|
icon: '@tui.square-dashed-bottom-code',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
link: 'tor',
|
|
||||||
title: 'Tor Logs',
|
|
||||||
subtitle: 'Diagnostics for the Tor daemon on this server',
|
|
||||||
icon: '@tui.target',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
link: 'kernel',
|
link: 'kernel',
|
||||||
title: 'Kernel Logs',
|
title: 'Kernel Logs',
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|
||||||
import { i18nPipe } from '@start9labs/shared'
|
|
||||||
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
|
||||||
import { LogsHeaderComponent } from 'src/app/routes/portal/routes/logs/components/header.component'
|
|
||||||
import { RR } from 'src/app/services/api/api.types'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
template: `
|
|
||||||
<logs-header [title]="'Tor Logs' | i18n">
|
|
||||||
{{ 'Diagnostics for the Tor daemon on this server' | i18n }}
|
|
||||||
</logs-header>
|
|
||||||
<logs context="tor" [followLogs]="follow" [fetchLogs]="fetch" />
|
|
||||||
`,
|
|
||||||
styles: `
|
|
||||||
:host {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [LogsComponent, LogsHeaderComponent, i18nPipe],
|
|
||||||
host: { class: 'g-page' },
|
|
||||||
})
|
|
||||||
export default class SystemTorComponent {
|
|
||||||
private readonly api = inject(ApiService)
|
|
||||||
|
|
||||||
protected readonly follow = (params: RR.FollowServerLogsReq) =>
|
|
||||||
this.api.followTorLogs(params)
|
|
||||||
|
|
||||||
protected readonly fetch = (params: RR.GetServerLogsReq) =>
|
|
||||||
this.api.getTorLogs(params)
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ import { TuiBadge } from '@taiga-ui/kit'
|
|||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
:host {
|
||||||
clip-path: inset(0 round 0.75rem);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -6,12 +6,18 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
import { getPkgId, i18nPipe } from '@start9labs/shared'
|
import {
|
||||||
import { T } from '@start9labs/start-sdk'
|
ErrorService,
|
||||||
|
getPkgId,
|
||||||
|
i18nPipe,
|
||||||
|
LoadingService,
|
||||||
|
} from '@start9labs/shared'
|
||||||
|
import { ISB, T } from '@start9labs/start-sdk'
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { map } from 'rxjs'
|
import { firstValueFrom, map } from 'rxjs'
|
||||||
import { ActionService } from 'src/app/services/action.service'
|
import { ActionService } from 'src/app/services/action.service'
|
||||||
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||||
import { getManifest } from 'src/app/utils/get-package-data'
|
import { getManifest } from 'src/app/utils/get-package-data'
|
||||||
@@ -20,6 +26,9 @@ import {
|
|||||||
PrimaryStatus,
|
PrimaryStatus,
|
||||||
renderPkgStatus,
|
renderPkgStatus,
|
||||||
} from 'src/app/services/pkg-status-rendering.service'
|
} from 'src/app/services/pkg-status-rendering.service'
|
||||||
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
|
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||||
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
|
|
||||||
const INACTIVE: PrimaryStatus[] = [
|
const INACTIVE: PrimaryStatus[] = [
|
||||||
'installing',
|
'installing',
|
||||||
@@ -65,6 +74,12 @@ const ALLOWED_STATUSES: Record<T.AllowedStatuses, Set<string>> = {
|
|||||||
|
|
||||||
<section class="g-card">
|
<section class="g-card">
|
||||||
<header>StartOS</header>
|
<header>StartOS</header>
|
||||||
|
<button
|
||||||
|
tuiCell
|
||||||
|
[action]="outboundGatewayAction()"
|
||||||
|
[inactive]="inactive"
|
||||||
|
(click)="openOutboundGatewayModal()"
|
||||||
|
></button>
|
||||||
<button
|
<button
|
||||||
tuiCell
|
tuiCell
|
||||||
[action]="rebuild"
|
[action]="rebuild"
|
||||||
@@ -95,66 +110,78 @@ const ALLOWED_STATUSES: Record<T.AllowedStatuses, Set<string>> = {
|
|||||||
export default class ServiceActionsRoute {
|
export default class ServiceActionsRoute {
|
||||||
private readonly actions = inject(ActionService)
|
private readonly actions = inject(ActionService)
|
||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
|
private readonly formDialog = inject(FormDialogService)
|
||||||
|
private readonly api = inject(ApiService)
|
||||||
|
private readonly loader = inject(LoadingService)
|
||||||
|
private readonly errorService = inject(ErrorService)
|
||||||
|
|
||||||
ungrouped: 'General' | 'Other' = 'General'
|
ungrouped: 'General' | 'Other' = 'General'
|
||||||
|
|
||||||
readonly service = inject(StandardActionsService)
|
readonly service = inject(StandardActionsService)
|
||||||
readonly package = toSignal(
|
readonly package = toSignal(
|
||||||
inject<PatchDB<DataModel>>(PatchDB)
|
this.patch.watch$('packageData', getPkgId()).pipe(
|
||||||
.watch$('packageData', getPkgId())
|
map(pkg => {
|
||||||
.pipe(
|
const specialGroup = Object.values(pkg.actions).some(a => !!a.group)
|
||||||
map(pkg => {
|
? 'Other'
|
||||||
const specialGroup = Object.values(pkg.actions).some(a => !!a.group)
|
: 'General'
|
||||||
? 'Other'
|
const status = renderPkgStatus(pkg).primary
|
||||||
: 'General'
|
return {
|
||||||
const status = renderPkgStatus(pkg).primary
|
status,
|
||||||
return {
|
icon: pkg.icon,
|
||||||
status,
|
manifest: getManifest(pkg),
|
||||||
icon: pkg.icon,
|
outboundGateway: pkg.outboundGateway,
|
||||||
manifest: getManifest(pkg),
|
actions: Object.entries(pkg.actions)
|
||||||
actions: Object.entries(pkg.actions)
|
.filter(([_, action]) => action.visibility !== 'hidden')
|
||||||
.filter(([_, action]) => action.visibility !== 'hidden')
|
.map(([id, action]) => ({
|
||||||
.map(([id, action]) => ({
|
...action,
|
||||||
...action,
|
id,
|
||||||
id,
|
group: action.group || specialGroup,
|
||||||
group: action.group || specialGroup,
|
visibility: ALLOWED_STATUSES[action.allowedStatuses].has(status)
|
||||||
visibility: ALLOWED_STATUSES[action.allowedStatuses].has(
|
? action.visibility
|
||||||
status,
|
: ({
|
||||||
)
|
disabled: `${this.i18n.transform('Action can only be executed when service is')} ${this.i18n.transform(action.allowedStatuses === 'only-running' ? 'Running' : 'Stopped')?.toLowerCase()}`,
|
||||||
? action.visibility
|
} as T.ActionVisibility),
|
||||||
: ({
|
}))
|
||||||
disabled: `${this.i18n.transform('Action can only be executed when service is')} ${this.i18n.transform(action.allowedStatuses === 'only-running' ? 'Running' : 'Stopped')?.toLowerCase()}`,
|
.sort((a, b) => {
|
||||||
} as T.ActionVisibility),
|
if (a.group === specialGroup && b.group !== specialGroup) return 1
|
||||||
}))
|
if (b.group === specialGroup && a.group !== specialGroup)
|
||||||
.sort((a, b) => {
|
return -1
|
||||||
if (a.group === specialGroup && b.group !== specialGroup)
|
|
||||||
return 1
|
|
||||||
if (b.group === specialGroup && a.group !== specialGroup)
|
|
||||||
return -1
|
|
||||||
|
|
||||||
const groupCompare = a.group.localeCompare(b.group) // sort groups lexicographically
|
const groupCompare = a.group.localeCompare(b.group) // sort groups lexicographically
|
||||||
if (groupCompare !== 0) return groupCompare
|
if (groupCompare !== 0) return groupCompare
|
||||||
|
|
||||||
return a.id.localeCompare(b.id) // sort actions within groups lexicographically
|
return a.id.localeCompare(b.id) // sort actions within groups lexicographically
|
||||||
})
|
})
|
||||||
.reduce<
|
.reduce<
|
||||||
Record<
|
Record<
|
||||||
string,
|
string,
|
||||||
Array<T.ActionMetadata & { id: string; group: string }>
|
Array<T.ActionMetadata & { id: string; group: string }>
|
||||||
>
|
>
|
||||||
>((acc, action) => {
|
>((acc, action) => {
|
||||||
const key = action.group
|
const key = action.group
|
||||||
if (!acc[key]) {
|
if (!acc[key]) {
|
||||||
acc[key] = []
|
acc[key] = []
|
||||||
}
|
}
|
||||||
acc[key].push(action)
|
acc[key].push(action)
|
||||||
return acc
|
return acc
|
||||||
}, {}),
|
}, {}),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readonly outboundGatewayAction = computed(() => {
|
||||||
|
const pkg = this.package()
|
||||||
|
const gateway = pkg?.outboundGateway
|
||||||
|
return {
|
||||||
|
name: this.i18n.transform('Set Outbound Gateway')!,
|
||||||
|
description: gateway
|
||||||
|
? `${this.i18n.transform('Current')}: ${gateway}`
|
||||||
|
: `${this.i18n.transform('Current')}: ${this.i18n.transform('System')}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
readonly rebuild = {
|
readonly rebuild = {
|
||||||
name: this.i18n.transform('Rebuild Service')!,
|
name: this.i18n.transform('Rebuild Service')!,
|
||||||
description: this.i18n.transform(
|
description: this.i18n.transform(
|
||||||
@@ -181,6 +208,71 @@ export default class ServiceActionsRoute {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openOutboundGatewayModal() {
|
||||||
|
const pkg = this.package()
|
||||||
|
if (!pkg) return
|
||||||
|
|
||||||
|
const gateways = await firstValueFrom(
|
||||||
|
this.patch.watch$('serverInfo', 'network', 'gateways'),
|
||||||
|
)
|
||||||
|
|
||||||
|
const SYSTEM_KEY = 'system'
|
||||||
|
|
||||||
|
const options: Record<string, string> = {
|
||||||
|
[SYSTEM_KEY]: this.i18n.transform('System default')!,
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(gateways)
|
||||||
|
.filter(
|
||||||
|
([_, g]) =>
|
||||||
|
!!g.ipInfo &&
|
||||||
|
g.ipInfo.deviceType !== 'bridge' &&
|
||||||
|
g.ipInfo.deviceType !== 'loopback',
|
||||||
|
)
|
||||||
|
.forEach(([id, g]) => {
|
||||||
|
options[id] = g.name ?? g.ipInfo?.name ?? id
|
||||||
|
})
|
||||||
|
|
||||||
|
const spec = ISB.InputSpec.of({
|
||||||
|
gateway: ISB.Value.select({
|
||||||
|
name: this.i18n.transform('Outbound Gateway'),
|
||||||
|
description: this.i18n.transform(
|
||||||
|
'Select the gateway for outbound traffic',
|
||||||
|
),
|
||||||
|
default: pkg.outboundGateway ?? SYSTEM_KEY,
|
||||||
|
values: options,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
this.formDialog.open(FormComponent, {
|
||||||
|
label: 'Set Outbound Gateway',
|
||||||
|
data: {
|
||||||
|
spec: await configBuilderToSpec(spec),
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: this.i18n.transform('Save'),
|
||||||
|
handler: async (input: typeof spec._TYPE) => {
|
||||||
|
const loader = this.loader.open('Saving').subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.setServiceOutbound({
|
||||||
|
packageId: pkg.manifest.id,
|
||||||
|
gateway: input.gateway === SYSTEM_KEY ? null : input.gateway,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly isInactive = computed(
|
protected readonly isInactive = computed(
|
||||||
(pkg = this.package()) => !pkg || INACTIVE.includes(pkg.status),
|
(pkg = this.package()) => !pkg || INACTIVE.includes(pkg.status),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -135,11 +135,10 @@ export default class ServiceInterfaceRoute {
|
|||||||
gateways.map(g => ({
|
gateways.map(g => ({
|
||||||
enabled:
|
enabled:
|
||||||
(g.public
|
(g.public
|
||||||
? binding?.net.publicEnabled.includes(g.id)
|
? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id)
|
||||||
: !binding?.net.privateDisabled.includes(g.id)) ?? false,
|
: !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false,
|
||||||
...g,
|
...g,
|
||||||
})) || [],
|
})) || [],
|
||||||
torDomains: host.onions,
|
|
||||||
publicDomains: getPublicDomains(host.publicDomains, gateways),
|
publicDomains: getPublicDomains(host.publicDomains, gateways),
|
||||||
privateDomains: host.privateDomains,
|
privateDomains: host.privateDomains,
|
||||||
addSsl: !!binding?.options.addSsl,
|
addSsl: !!binding?.options.addSsl,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
TuiFiles,
|
TuiFiles,
|
||||||
tuiInputFilesOptionsProvider,
|
tuiInputFilesOptionsProvider,
|
||||||
} from '@taiga-ui/kit'
|
} from '@taiga-ui/kit'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
|
|
||||||
import { SideloadPackageComponent } from './package.component'
|
import { SideloadPackageComponent } from './package.component'
|
||||||
@@ -55,11 +54,6 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
|
|||||||
<div>
|
<div>
|
||||||
<tui-avatar appearance="secondary" src="@tui.upload" />
|
<tui-avatar appearance="secondary" src="@tui.upload" />
|
||||||
<p>{{ 'Upload .s9pk package file' | i18n }}</p>
|
<p>{{ 'Upload .s9pk package file' | i18n }}</p>
|
||||||
@if (isTor) {
|
|
||||||
<p class="g-warning">
|
|
||||||
{{ 'Warning: package upload will be slow over Tor.' | i18n }}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
<button tuiButton>{{ 'Select' | i18n }}</button>
|
<button tuiButton>{{ 'Select' | i18n }}</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -92,8 +86,6 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class SideloadComponent {
|
export default class SideloadComponent {
|
||||||
readonly isTor = inject(ConfigService).accessType === 'tor'
|
|
||||||
|
|
||||||
file: File | null = null
|
file: File | null = null
|
||||||
readonly package = signal<MarketplacePkgSideload | null>(null)
|
readonly package = signal<MarketplacePkgSideload | null>(null)
|
||||||
readonly error = signal<i18nKey | null>(null)
|
readonly error = signal<i18nKey | null>(null)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { GatewaysTableComponent } from './table.component'
|
|||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { ISB } from '@start9labs/start-sdk'
|
import { ISB } from '@start9labs/start-sdk'
|
||||||
|
import { RR } from 'src/app/services/api/api.types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -51,11 +52,6 @@ import { ISB } from '@start9labs/start-sdk'
|
|||||||
<gateways-table />
|
<gateways-table />
|
||||||
</section>
|
</section>
|
||||||
`,
|
`,
|
||||||
styles: `
|
|
||||||
:host {
|
|
||||||
max-width: 64rem;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -85,8 +81,19 @@ export default class GatewaysComponent {
|
|||||||
default: null,
|
default: null,
|
||||||
placeholder: 'StartTunnel 1',
|
placeholder: 'StartTunnel 1',
|
||||||
}),
|
}),
|
||||||
|
type: ISB.Value.select({
|
||||||
|
name: this.i18n.transform('Type'),
|
||||||
|
description: this.i18n.transform('The type of gateway'),
|
||||||
|
default: 'inbound-outbound',
|
||||||
|
values: {
|
||||||
|
'inbound-outbound': this.i18n.transform(
|
||||||
|
'StartTunnel (Inbound/Outbound)',
|
||||||
|
),
|
||||||
|
'outbound-only': this.i18n.transform('Outbound Only'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
config: ISB.Value.union({
|
config: ISB.Value.union({
|
||||||
name: this.i18n.transform('StartTunnel Config File'),
|
name: this.i18n.transform('Wireguard Config File'),
|
||||||
default: 'paste',
|
default: 'paste',
|
||||||
variants: ISB.Variants.of({
|
variants: ISB.Variants.of({
|
||||||
paste: {
|
paste: {
|
||||||
@@ -113,10 +120,17 @@ export default class GatewaysComponent {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
setAsDefaultOutbound: ISB.Value.toggle({
|
||||||
|
name: this.i18n.transform('Set as default outbound'),
|
||||||
|
description: this.i18n.transform(
|
||||||
|
'Route all outbound traffic through this gateway',
|
||||||
|
),
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.formDialog.open(FormComponent, {
|
this.formDialog.open(FormComponent, {
|
||||||
label: 'Add StartTunnel Gateway',
|
label: 'Add Wireguard Gateway',
|
||||||
data: {
|
data: {
|
||||||
spec: await configBuilderToSpec(spec),
|
spec: await configBuilderToSpec(spec),
|
||||||
buttons: [
|
buttons: [
|
||||||
@@ -132,7 +146,8 @@ export default class GatewaysComponent {
|
|||||||
input.config.selection === 'paste'
|
input.config.selection === 'paste'
|
||||||
? input.config.value.file
|
? input.config.value.file
|
||||||
: await (input.config.value.file as any as File).text(),
|
: await (input.config.value.file as any as File).text(),
|
||||||
public: false,
|
type: input.type as RR.GatewayType,
|
||||||
|
setAsDefaultOutbound: input.setAsDefaultOutbound,
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
TuiButton,
|
TuiButton,
|
||||||
TuiDataList,
|
TuiDataList,
|
||||||
TuiDropdown,
|
TuiDropdown,
|
||||||
|
TuiIcon,
|
||||||
TuiOptGroup,
|
TuiOptGroup,
|
||||||
TuiTextfield,
|
TuiTextfield,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
@@ -24,32 +25,55 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
import { GatewayPlus } from 'src/app/services/gateway.service'
|
import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||||
|
import { TuiBadge } from '@taiga-ui/kit'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tr[gateway]',
|
selector: 'tr[gateway]',
|
||||||
template: `
|
template: `
|
||||||
@if (gateway(); as gateway) {
|
@if (gateway(); as gateway) {
|
||||||
<td class="name">
|
<td>
|
||||||
{{ gateway.name }}
|
{{ gateway.name }}
|
||||||
</td>
|
@if (gateway.isDefaultOutbound) {
|
||||||
<td class="type">
|
<span tuiBadge tuiStatus appearance="positive">Default outbound</span>
|
||||||
@if (gateway.ipInfo.deviceType; as type) {
|
}
|
||||||
{{ type }} ({{
|
</td>
|
||||||
gateway.public ? ('public' | i18n) : ('private' | i18n)
|
<td>
|
||||||
}})
|
@switch (gateway.ipInfo.deviceType) {
|
||||||
} @else {
|
@case ('ethernet') {
|
||||||
-
|
<tui-icon icon="@tui.cable" />
|
||||||
|
{{ 'Ethernet' | i18n }}
|
||||||
|
}
|
||||||
|
@case ('wireless') {
|
||||||
|
<tui-icon icon="@tui.wifi" />
|
||||||
|
{{ 'WiFi' | i18n }}
|
||||||
|
}
|
||||||
|
@case ('wireguard') {
|
||||||
|
<tui-icon icon="@tui.shield" />
|
||||||
|
{{ 'WireGuard' | i18n }}
|
||||||
|
}
|
||||||
|
@default {
|
||||||
|
{{ gateway.ipInfo.deviceType }}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (gateway.type === 'outbound-only') {
|
||||||
|
<tui-icon icon="@tui.arrow-up-right" />
|
||||||
|
{{ 'Outbound Only' | i18n }}
|
||||||
|
} @else {
|
||||||
|
<tui-icon icon="@tui.arrow-left-right" />
|
||||||
|
{{ 'Inbound/Outbound' | i18n }}
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="lan">{{ gateway.lanIpv4.join(', ') }}</td>
|
|
||||||
<td
|
<td
|
||||||
class="wan"
|
class="wan"
|
||||||
[style.color]="
|
[style.color]="
|
||||||
gateway.ipInfo.wanIp ? 'var(--tui-text-warning)' : undefined
|
gateway.ipInfo.wanIp ? undefined : 'var(--tui-text-warning)'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ gateway.ipInfo.wanIp || ('Error' | i18n) }}
|
{{ gateway.ipInfo.wanIp || ('Error' | i18n) }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="lan">{{ gateway.lanIpv4.join(', ') || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
tuiIconButton
|
tuiIconButton
|
||||||
@@ -67,6 +91,18 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
|||||||
{{ 'Rename' | i18n }}
|
{{ 'Rename' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</tui-opt-group>
|
</tui-opt-group>
|
||||||
|
@if (!gateway.isDefaultOutbound) {
|
||||||
|
<tui-opt-group>
|
||||||
|
<button
|
||||||
|
tuiOption
|
||||||
|
new
|
||||||
|
iconStart="@tui.arrow-up-right"
|
||||||
|
(click)="setDefaultOutbound()"
|
||||||
|
>
|
||||||
|
{{ 'Set as Default Outbound' | i18n }}
|
||||||
|
</button>
|
||||||
|
</tui-opt-group>
|
||||||
|
}
|
||||||
@if (gateway.ipInfo.deviceType === 'wireguard') {
|
@if (gateway.ipInfo.deviceType === 'wireguard') {
|
||||||
<tui-opt-group>
|
<tui-opt-group>
|
||||||
<button
|
<button
|
||||||
@@ -87,19 +123,11 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
|||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
td:last-child {
|
td:last-child {
|
||||||
grid-area: 1 / 3 / 5;
|
grid-area: 1 / 3 / 7;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
|
||||||
width: 14rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type {
|
|
||||||
width: 14rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
:host-context(tui-root._mobile) {
|
||||||
grid-template-columns: min-content 1fr min-content;
|
grid-template-columns: min-content 1fr min-content;
|
||||||
|
|
||||||
@@ -107,11 +135,15 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
|||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type {
|
.connection {
|
||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
order: -1;
|
order: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
.lan,
|
.lan,
|
||||||
.wan {
|
.wan {
|
||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
@@ -132,9 +164,11 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
|||||||
TuiButton,
|
TuiButton,
|
||||||
TuiDropdown,
|
TuiDropdown,
|
||||||
TuiDataList,
|
TuiDataList,
|
||||||
|
TuiIcon,
|
||||||
TuiOptGroup,
|
TuiOptGroup,
|
||||||
TuiTextfield,
|
TuiTextfield,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
|
TuiBadge,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class GatewaysItemComponent {
|
export class GatewaysItemComponent {
|
||||||
@@ -166,6 +200,18 @@ export class GatewaysItemComponent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setDefaultOutbound() {
|
||||||
|
const loader = this.loader.open().subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.setDefaultOutbound({ gateway: this.gateway().id })
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async rename() {
|
async rename() {
|
||||||
const { id, name } = this.gateway()
|
const { id, name } = this.gateway()
|
||||||
const renameSpec = ISB.InputSpec.of({
|
const renameSpec = ISB.InputSpec.of({
|
||||||
|
|||||||
@@ -8,12 +8,21 @@ import { GatewayService } from 'src/app/services/gateway.service'
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'gateways-table',
|
selector: 'gateways-table',
|
||||||
template: `
|
template: `
|
||||||
<table [appTable]="['Name', 'Type', $any('LAN IP'), $any('WAN IP'), null]">
|
<table
|
||||||
|
[appTable]="[
|
||||||
|
'Name',
|
||||||
|
'Connection',
|
||||||
|
'Type',
|
||||||
|
$any('WAN IP'),
|
||||||
|
$any('LAN IP'),
|
||||||
|
null,
|
||||||
|
]"
|
||||||
|
>
|
||||||
@for (gateway of gatewayService.gateways(); track $index) {
|
@for (gateway of gatewayService.gateways(); track $index) {
|
||||||
<tr [gateway]="gateway"></tr>
|
<tr [gateway]="gateway"></tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5">
|
<td colspan="7">
|
||||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -47,13 +47,11 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
|||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { filter } from 'rxjs'
|
import { filter } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import { OSService } from 'src/app/services/os.service'
|
import { OSService } from 'src/app/services/os.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { SnekDirective } from './snek.directive'
|
import { SnekDirective } from './snek.directive'
|
||||||
import { UPDATE } from './update.component'
|
import { UPDATE } from './update.component'
|
||||||
import { SystemWipeComponent } from './wipe.component'
|
|
||||||
import { KeyboardSelectComponent } from './keyboard-select.component'
|
import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -66,7 +64,7 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
@if (server(); as server) {
|
@if (server(); as server) {
|
||||||
<div tuiCell tuiAppearance="outline-grayscale">
|
<div tuiCell tuiAppearance="outline-grayscale">
|
||||||
<tui-icon icon="@tui.zap" />
|
<tui-icon icon="@tui.zap" (click)="count = count + 1" />
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
<strong>{{ 'Software Update' | i18n }}</strong>
|
<strong>{{ 'Software Update' | i18n }}</strong>
|
||||||
<span tuiSubtitle [style.flex-wrap]="'wrap'">
|
<span tuiSubtitle [style.flex-wrap]="'wrap'">
|
||||||
@@ -178,18 +176,6 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div tuiCell tuiAppearance="outline-grayscale">
|
|
||||||
<tui-icon icon="@tui.rotate-cw" (click)="count = count + 1" />
|
|
||||||
<span tuiTitle>
|
|
||||||
<strong>{{ 'Restart Tor' | i18n }}</strong>
|
|
||||||
<span tuiSubtitle>
|
|
||||||
{{ 'Restart the Tor daemon on your server' | i18n }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<button tuiButton appearance="glass" (click)="onTorRestart()">
|
|
||||||
{{ 'Restart' | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@if (count > 4) {
|
@if (count > 4) {
|
||||||
<div tuiCell tuiAppearance="outline-grayscale" tuiAnimated>
|
<div tuiCell tuiAppearance="outline-grayscale" tuiAnimated>
|
||||||
<tui-icon icon="@tui.briefcase-medical" />
|
<tui-icon icon="@tui.briefcase-medical" />
|
||||||
@@ -283,12 +269,10 @@ export default class SystemGeneralComponent {
|
|||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly isTor = inject(ConfigService).accessType === 'tor'
|
|
||||||
private readonly dialog = inject(DialogService)
|
private readonly dialog = inject(DialogService)
|
||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
private readonly injector = inject(INJECTOR)
|
private readonly injector = inject(INJECTOR)
|
||||||
|
|
||||||
wipe = false
|
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
readonly server = toSignal(this.patch.watch$('serverInfo'))
|
readonly server = toSignal(this.patch.watch$('serverInfo'))
|
||||||
@@ -392,24 +376,6 @@ export default class SystemGeneralComponent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onTorRestart() {
|
|
||||||
this.wipe = false
|
|
||||||
this.dialog
|
|
||||||
.openConfirm({
|
|
||||||
label: this.isTor ? 'Warning' : 'Confirm',
|
|
||||||
data: {
|
|
||||||
content: new PolymorpheusComponent(
|
|
||||||
SystemWipeComponent,
|
|
||||||
this.injector,
|
|
||||||
),
|
|
||||||
yes: 'Restart',
|
|
||||||
no: 'Cancel',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(() => this.resetTor(this.wipe))
|
|
||||||
}
|
|
||||||
|
|
||||||
async onRepair() {
|
async onRepair() {
|
||||||
this.dialog
|
this.dialog
|
||||||
.openConfirm({
|
.openConfirm({
|
||||||
@@ -532,19 +498,6 @@ export default class SystemGeneralComponent {
|
|||||||
.subscribe(() => this.restart())
|
.subscribe(() => this.restart())
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resetTor(wipeState: boolean) {
|
|
||||||
const loader = this.loader.open().subscribe()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.api.resetTor({ wipeState, reason: 'User triggered' })
|
|
||||||
this.dialog.openAlert('Tor restart in progress').subscribe()
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.dialogs
|
this.dialogs
|
||||||
.open(UPDATE, {
|
.open(UPDATE, {
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|
||||||
import { FormsModule } from '@angular/forms'
|
|
||||||
import { TuiLabel } from '@taiga-ui/core'
|
|
||||||
import { TuiCheckbox } from '@taiga-ui/kit'
|
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import SystemGeneralComponent from './general.component'
|
|
||||||
import { i18nPipe } from '@start9labs/shared'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
template: `
|
|
||||||
@if (isTor) {
|
|
||||||
<p>
|
|
||||||
{{
|
|
||||||
'You are currently connected over Tor. If you restart the Tor daemon, you will lose connectivity until it comes back online.'
|
|
||||||
| i18n
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
<p>
|
|
||||||
{{
|
|
||||||
'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.'
|
|
||||||
| i18n
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<label tuiLabel>
|
|
||||||
<input type="checkbox" tuiCheckbox [(ngModel)]="component.wipe" />
|
|
||||||
{{ 'Wipe state' | i18n }}
|
|
||||||
</label>
|
|
||||||
`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [TuiLabel, FormsModule, TuiCheckbox, i18nPipe],
|
|
||||||
})
|
|
||||||
export class SystemWipeComponent {
|
|
||||||
readonly isTor = inject(ConfigService).accessType === 'tor'
|
|
||||||
readonly component = inject(SystemGeneralComponent)
|
|
||||||
}
|
|
||||||
@@ -96,11 +96,10 @@ export default class StartOsUiComponent {
|
|||||||
gateways: gateways.map(g => ({
|
gateways: gateways.map(g => ({
|
||||||
enabled:
|
enabled:
|
||||||
(g.public
|
(g.public
|
||||||
? binding?.net.publicEnabled.includes(g.id)
|
? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id)
|
||||||
: !binding?.net.privateDisabled.includes(g.id)) ?? false,
|
: !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false,
|
||||||
...g,
|
...g,
|
||||||
})),
|
})),
|
||||||
torDomains: network.host.onions,
|
|
||||||
publicDomains: getPublicDomains(network.host.publicDomains, gateways),
|
publicDomains: getPublicDomains(network.host.publicDomains, gateways),
|
||||||
privateDomains: network.host.privateDomains,
|
privateDomains: network.host.privateDomains,
|
||||||
addSsl: true,
|
addSsl: true,
|
||||||
|
|||||||
@@ -2126,8 +2126,74 @@ export namespace Mock {
|
|||||||
net: {
|
net: {
|
||||||
assignedPort: 80,
|
assignedPort: 80,
|
||||||
assignedSslPort: 443,
|
assignedSslPort: 443,
|
||||||
publicEnabled: [],
|
},
|
||||||
|
addresses: {
|
||||||
privateDisabled: [],
|
privateDisabled: [],
|
||||||
|
publicEnabled: [],
|
||||||
|
possible: [
|
||||||
|
{
|
||||||
|
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||||
|
public: false,
|
||||||
|
hostname: {
|
||||||
|
kind: 'local',
|
||||||
|
value: 'adjective-noun.local',
|
||||||
|
port: null,
|
||||||
|
sslPort: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||||
|
public: false,
|
||||||
|
hostname: {
|
||||||
|
kind: 'local',
|
||||||
|
value: 'adjective-noun.local',
|
||||||
|
port: null,
|
||||||
|
sslPort: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||||
|
public: false,
|
||||||
|
hostname: {
|
||||||
|
kind: 'ipv4',
|
||||||
|
value: '192.168.10.11',
|
||||||
|
port: null,
|
||||||
|
sslPort: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||||
|
public: false,
|
||||||
|
hostname: {
|
||||||
|
kind: 'ipv4',
|
||||||
|
value: '10.0.0.2',
|
||||||
|
port: null,
|
||||||
|
sslPort: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
addSsl: null,
|
addSsl: null,
|
||||||
@@ -2138,87 +2204,6 @@ export namespace Mock {
|
|||||||
},
|
},
|
||||||
publicDomains: {},
|
publicDomains: {},
|
||||||
privateDomains: [],
|
privateDomains: [],
|
||||||
onions: [],
|
|
||||||
hostnameInfo: {
|
|
||||||
80: [
|
|
||||||
{
|
|
||||||
kind: 'ip',
|
|
||||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
|
||||||
public: false,
|
|
||||||
hostname: {
|
|
||||||
kind: 'local',
|
|
||||||
value: 'adjective-noun.local',
|
|
||||||
port: null,
|
|
||||||
sslPort: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'ip',
|
|
||||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
|
||||||
public: false,
|
|
||||||
hostname: {
|
|
||||||
kind: 'local',
|
|
||||||
value: 'adjective-noun.local',
|
|
||||||
port: null,
|
|
||||||
sslPort: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'ip',
|
|
||||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
|
||||||
public: false,
|
|
||||||
hostname: {
|
|
||||||
kind: 'ipv4',
|
|
||||||
value: '192.168.10.11',
|
|
||||||
port: null,
|
|
||||||
sslPort: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'ip',
|
|
||||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
|
||||||
public: false,
|
|
||||||
hostname: {
|
|
||||||
kind: 'ipv4',
|
|
||||||
value: '10.0.0.2',
|
|
||||||
port: null,
|
|
||||||
sslPort: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'onion',
|
|
||||||
hostname: {
|
|
||||||
value: 'bitcoin-p2p.onion',
|
|
||||||
port: 80,
|
|
||||||
sslPort: 443,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
bcdefgh: {
|
bcdefgh: {
|
||||||
bindings: {
|
bindings: {
|
||||||
@@ -2227,8 +2212,11 @@ export namespace Mock {
|
|||||||
net: {
|
net: {
|
||||||
assignedPort: 8332,
|
assignedPort: 8332,
|
||||||
assignedSslPort: null,
|
assignedSslPort: null,
|
||||||
publicEnabled: [],
|
},
|
||||||
|
addresses: {
|
||||||
privateDisabled: [],
|
privateDisabled: [],
|
||||||
|
publicEnabled: [],
|
||||||
|
possible: [],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
addSsl: null,
|
addSsl: null,
|
||||||
@@ -2239,10 +2227,6 @@ export namespace Mock {
|
|||||||
},
|
},
|
||||||
publicDomains: {},
|
publicDomains: {},
|
||||||
privateDomains: [],
|
privateDomains: [],
|
||||||
onions: [],
|
|
||||||
hostnameInfo: {
|
|
||||||
8332: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
cdefghi: {
|
cdefghi: {
|
||||||
bindings: {
|
bindings: {
|
||||||
@@ -2251,8 +2235,11 @@ export namespace Mock {
|
|||||||
net: {
|
net: {
|
||||||
assignedPort: 8333,
|
assignedPort: 8333,
|
||||||
assignedSslPort: null,
|
assignedSslPort: null,
|
||||||
publicEnabled: [],
|
},
|
||||||
|
addresses: {
|
||||||
privateDisabled: [],
|
privateDisabled: [],
|
||||||
|
publicEnabled: [],
|
||||||
|
possible: [],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
addSsl: null,
|
addSsl: null,
|
||||||
@@ -2263,13 +2250,10 @@ export namespace Mock {
|
|||||||
},
|
},
|
||||||
publicDomains: {},
|
publicDomains: {},
|
||||||
privateDomains: [],
|
privateDomains: [],
|
||||||
onions: [],
|
|
||||||
hostnameInfo: {
|
|
||||||
8333: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
storeExposedDependents: [],
|
storeExposedDependents: [],
|
||||||
|
outboundGateway: null,
|
||||||
registry: 'https://registry.start9.com/',
|
registry: 'https://registry.start9.com/',
|
||||||
developerKey: 'developer-key',
|
developerKey: 'developer-key',
|
||||||
tasks: {
|
tasks: {
|
||||||
@@ -2338,6 +2322,7 @@ export namespace Mock {
|
|||||||
},
|
},
|
||||||
hosts: {},
|
hosts: {},
|
||||||
storeExposedDependents: [],
|
storeExposedDependents: [],
|
||||||
|
outboundGateway: null,
|
||||||
registry: 'https://registry.start9.com/',
|
registry: 'https://registry.start9.com/',
|
||||||
developerKey: 'developer-key',
|
developerKey: 'developer-key',
|
||||||
tasks: {},
|
tasks: {},
|
||||||
@@ -2444,6 +2429,7 @@ export namespace Mock {
|
|||||||
},
|
},
|
||||||
hosts: {},
|
hosts: {},
|
||||||
storeExposedDependents: [],
|
storeExposedDependents: [],
|
||||||
|
outboundGateway: null,
|
||||||
registry: 'https://registry.start9.com/',
|
registry: 'https://registry.start9.com/',
|
||||||
developerKey: 'developer-key',
|
developerKey: 'developer-key',
|
||||||
tasks: {
|
tasks: {
|
||||||
|
|||||||
@@ -79,14 +79,14 @@ export namespace RR {
|
|||||||
uptime: number // seconds
|
uptime: number // seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs & net.tor.logs
|
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs
|
||||||
export type GetServerLogsRes = FetchLogsRes
|
export type GetServerLogsRes = FetchLogsRes
|
||||||
|
|
||||||
export type FollowServerLogsReq = {
|
export type FollowServerLogsReq = {
|
||||||
limit?: number // (optional) default is 50. Ignored if cursor provided
|
limit?: number // (optional) default is 50. Ignored if cursor provided
|
||||||
boot?: number | string | null // (optional) number is offset (0: current, -1 prev, +1 first), string is a specific boot id, null is all. Default is undefined
|
boot?: number | string | null // (optional) number is offset (0: current, -1 prev, +1 first), string is a specific boot id, null is all. Default is undefined
|
||||||
cursor?: string // the last known log. Websocket will return all logs since this log
|
cursor?: string // the last known log. Websocket will return all logs since this log
|
||||||
} // server.logs.follow & server.kernel-logs.follow & net.tor.follow-logs
|
} // server.logs.follow & server.kernel-logs.follow
|
||||||
export type FollowServerLogsRes = {
|
export type FollowServerLogsRes = {
|
||||||
startCursor: string
|
startCursor: string
|
||||||
guid: string
|
guid: string
|
||||||
@@ -120,12 +120,6 @@ export namespace RR {
|
|||||||
} // net.dns.query
|
} // net.dns.query
|
||||||
export type QueryDnsRes = string | null
|
export type QueryDnsRes = string | null
|
||||||
|
|
||||||
export type ResetTorReq = {
|
|
||||||
wipeState: boolean
|
|
||||||
reason: string
|
|
||||||
} // net.tor.reset
|
|
||||||
export type ResetTorRes = null
|
|
||||||
|
|
||||||
export type SetKeyboardReq = FullKeyboard // server.set-keyboard
|
export type SetKeyboardReq = FullKeyboard // server.set-keyboard
|
||||||
export type SetKeyboardRes = null
|
export type SetKeyboardRes = null
|
||||||
|
|
||||||
@@ -258,10 +252,13 @@ export namespace RR {
|
|||||||
|
|
||||||
// network
|
// network
|
||||||
|
|
||||||
|
export type GatewayType = 'inbound-outbound' | 'outbound-only'
|
||||||
|
|
||||||
export type AddTunnelReq = {
|
export type AddTunnelReq = {
|
||||||
name: string
|
name: string
|
||||||
config: string // file contents
|
config: string // file contents
|
||||||
public: boolean
|
type: GatewayType
|
||||||
|
setAsDefaultOutbound?: boolean
|
||||||
} // net.tunnel.add
|
} // net.tunnel.add
|
||||||
export type AddTunnelRes = {
|
export type AddTunnelRes = {
|
||||||
id: string
|
id: string
|
||||||
@@ -276,6 +273,17 @@ export namespace RR {
|
|||||||
export type RemoveTunnelReq = { id: string } // net.tunnel.remove
|
export type RemoveTunnelReq = { id: string } // net.tunnel.remove
|
||||||
export type RemoveTunnelRes = null
|
export type RemoveTunnelRes = null
|
||||||
|
|
||||||
|
// Set default outbound gateway
|
||||||
|
export type SetDefaultOutboundReq = { gateway: string | null } // net.gateway.set-default-outbound
|
||||||
|
export type SetDefaultOutboundRes = null
|
||||||
|
|
||||||
|
// Set service outbound gateway
|
||||||
|
export type SetServiceOutboundReq = {
|
||||||
|
packageId: string
|
||||||
|
gateway: string | null
|
||||||
|
} // package.set-outbound-gateway
|
||||||
|
export type SetServiceOutboundRes = null
|
||||||
|
|
||||||
export type InitAcmeReq = {
|
export type InitAcmeReq = {
|
||||||
provider: string
|
provider: string
|
||||||
contact: string[]
|
contact: string[]
|
||||||
@@ -287,29 +295,13 @@ export namespace RR {
|
|||||||
}
|
}
|
||||||
export type RemoveAcmeRes = null
|
export type RemoveAcmeRes = null
|
||||||
|
|
||||||
export type AddTorKeyReq = {
|
export type ServerBindingSetAddressEnabledReq = {
|
||||||
// net.tor.key.add
|
// server.host.binding.set-address-enabled
|
||||||
key: string
|
|
||||||
}
|
|
||||||
export type GenerateTorKeyReq = {} // net.tor.key.generate
|
|
||||||
export type AddTorKeyRes = string // onion address *with* .onion suffix
|
|
||||||
|
|
||||||
export type ServerBindingToggleGatewayReq = {
|
|
||||||
// server.host.binding.set-gateway-enabled
|
|
||||||
gateway: T.GatewayId
|
|
||||||
internalPort: 80
|
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 ServerAddOnionReq = {
|
|
||||||
// server.host.address.onion.add
|
|
||||||
onion: string // address *with* .onion suffix
|
|
||||||
}
|
|
||||||
export type AddOnionRes = null
|
|
||||||
|
|
||||||
export type ServerRemoveOnionReq = ServerAddOnionReq // server.host.address.onion.remove
|
|
||||||
export type RemoveOnionRes = null
|
|
||||||
|
|
||||||
export type OsUiAddPublicDomainReq = {
|
export type OsUiAddPublicDomainReq = {
|
||||||
// server.host.address.domain.public.add
|
// server.host.address.domain.public.add
|
||||||
@@ -337,23 +329,16 @@ export namespace RR {
|
|||||||
}
|
}
|
||||||
export type OsUiRemovePrivateDomainRes = null
|
export type OsUiRemovePrivateDomainRes = null
|
||||||
|
|
||||||
export type PkgBindingToggleGatewayReq = Omit<
|
export type PkgBindingSetAddressEnabledReq = Omit<
|
||||||
ServerBindingToggleGatewayReq,
|
ServerBindingSetAddressEnabledReq,
|
||||||
'internalPort'
|
'internalPort'
|
||||||
> & {
|
> & {
|
||||||
// package.host.binding.set-gateway-enabled
|
// package.host.binding.set-address-enabled
|
||||||
internalPort: number
|
internalPort: number
|
||||||
package: T.PackageId // string
|
package: T.PackageId // string
|
||||||
host: T.HostId // string
|
host: T.HostId // string
|
||||||
}
|
}
|
||||||
export type PkgBindingToggleGatewayRes = null
|
export type PkgBindingSetAddressEnabledRes = null
|
||||||
|
|
||||||
export type PkgAddOnionReq = ServerAddOnionReq & {
|
|
||||||
// package.host.address.onion.add
|
|
||||||
package: T.PackageId // string
|
|
||||||
host: T.HostId // string
|
|
||||||
}
|
|
||||||
export type PkgRemoveOnionReq = PkgAddOnionReq // package.host.address.onion.remove
|
|
||||||
|
|
||||||
export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & {
|
export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & {
|
||||||
// package.host.address.domain.public.add
|
// package.host.address.domain.public.add
|
||||||
|
|||||||
@@ -81,8 +81,6 @@ export abstract class ApiService {
|
|||||||
params: RR.GetServerLogsReq,
|
params: RR.GetServerLogsReq,
|
||||||
): Promise<RR.GetServerLogsRes>
|
): Promise<RR.GetServerLogsRes>
|
||||||
|
|
||||||
abstract getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes>
|
|
||||||
|
|
||||||
abstract getKernelLogs(
|
abstract getKernelLogs(
|
||||||
params: RR.GetServerLogsReq,
|
params: RR.GetServerLogsReq,
|
||||||
): Promise<RR.GetServerLogsRes>
|
): Promise<RR.GetServerLogsRes>
|
||||||
@@ -91,10 +89,6 @@ export abstract class ApiService {
|
|||||||
params: RR.FollowServerLogsReq,
|
params: RR.FollowServerLogsReq,
|
||||||
): Promise<RR.FollowServerLogsRes>
|
): Promise<RR.FollowServerLogsRes>
|
||||||
|
|
||||||
abstract followTorLogs(
|
|
||||||
params: RR.FollowServerLogsReq,
|
|
||||||
): Promise<RR.FollowServerLogsRes>
|
|
||||||
|
|
||||||
abstract followKernelLogs(
|
abstract followKernelLogs(
|
||||||
params: RR.FollowServerLogsReq,
|
params: RR.FollowServerLogsReq,
|
||||||
): Promise<RR.FollowServerLogsRes>
|
): Promise<RR.FollowServerLogsRes>
|
||||||
@@ -125,8 +119,6 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
abstract queryDns(params: RR.QueryDnsReq): Promise<RR.QueryDnsRes>
|
abstract queryDns(params: RR.QueryDnsReq): Promise<RR.QueryDnsRes>
|
||||||
|
|
||||||
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
|
|
||||||
|
|
||||||
// smtp
|
// smtp
|
||||||
|
|
||||||
abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes>
|
abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes>
|
||||||
@@ -183,6 +175,14 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
abstract removeTunnel(params: RR.RemoveTunnelReq): Promise<RR.RemoveTunnelRes>
|
abstract removeTunnel(params: RR.RemoveTunnelReq): Promise<RR.RemoveTunnelRes>
|
||||||
|
|
||||||
|
abstract setDefaultOutbound(
|
||||||
|
params: RR.SetDefaultOutboundReq,
|
||||||
|
): Promise<RR.SetDefaultOutboundRes>
|
||||||
|
|
||||||
|
abstract setServiceOutbound(
|
||||||
|
params: RR.SetServiceOutboundReq,
|
||||||
|
): Promise<RR.SetServiceOutboundRes>
|
||||||
|
|
||||||
// ** domains **
|
// ** domains **
|
||||||
|
|
||||||
// wifi
|
// wifi
|
||||||
@@ -344,21 +344,9 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
abstract removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes>
|
abstract removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes>
|
||||||
|
|
||||||
abstract addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes>
|
abstract serverBindingSetAddressEnabled(
|
||||||
|
params: RR.ServerBindingSetAddressEnabledReq,
|
||||||
abstract generateTorKey(
|
): Promise<RR.ServerBindingSetAddressEnabledRes>
|
||||||
params: RR.GenerateTorKeyReq,
|
|
||||||
): Promise<RR.AddTorKeyRes>
|
|
||||||
|
|
||||||
abstract serverBindingToggleGateway(
|
|
||||||
params: RR.ServerBindingToggleGatewayReq,
|
|
||||||
): Promise<RR.ServerBindingToggleGatewayRes>
|
|
||||||
|
|
||||||
abstract serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes>
|
|
||||||
|
|
||||||
abstract serverRemoveOnion(
|
|
||||||
params: RR.ServerRemoveOnionReq,
|
|
||||||
): Promise<RR.RemoveOnionRes>
|
|
||||||
|
|
||||||
abstract osUiAddPublicDomain(
|
abstract osUiAddPublicDomain(
|
||||||
params: RR.OsUiAddPublicDomainReq,
|
params: RR.OsUiAddPublicDomainReq,
|
||||||
@@ -376,15 +364,9 @@ export abstract class ApiService {
|
|||||||
params: RR.OsUiRemovePrivateDomainReq,
|
params: RR.OsUiRemovePrivateDomainReq,
|
||||||
): Promise<RR.OsUiRemovePrivateDomainRes>
|
): Promise<RR.OsUiRemovePrivateDomainRes>
|
||||||
|
|
||||||
abstract pkgBindingToggleGateway(
|
abstract pkgBindingSetAddressEnabled(
|
||||||
params: RR.PkgBindingToggleGatewayReq,
|
params: RR.PkgBindingSetAddressEnabledReq,
|
||||||
): Promise<RR.PkgBindingToggleGatewayRes>
|
): Promise<RR.PkgBindingSetAddressEnabledRes>
|
||||||
|
|
||||||
abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes>
|
|
||||||
|
|
||||||
abstract pkgRemoveOnion(
|
|
||||||
params: RR.PkgRemoveOnionReq,
|
|
||||||
): Promise<RR.RemoveOnionRes>
|
|
||||||
|
|
||||||
abstract pkgAddPublicDomain(
|
abstract pkgAddPublicDomain(
|
||||||
params: RR.PkgAddPublicDomainReq,
|
params: RR.PkgAddPublicDomainReq,
|
||||||
|
|||||||
@@ -195,10 +195,6 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.rpcRequest({ method: 'server.logs', params })
|
return this.rpcRequest({ method: 'server.logs', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
|
||||||
return this.rpcRequest({ method: 'net.tor.logs', params })
|
|
||||||
}
|
|
||||||
|
|
||||||
async getKernelLogs(
|
async getKernelLogs(
|
||||||
params: RR.GetServerLogsReq,
|
params: RR.GetServerLogsReq,
|
||||||
): Promise<RR.GetServerLogsRes> {
|
): Promise<RR.GetServerLogsRes> {
|
||||||
@@ -211,12 +207,6 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.rpcRequest({ method: 'server.logs.follow', params })
|
return this.rpcRequest({ method: 'server.logs.follow', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async followTorLogs(
|
|
||||||
params: RR.FollowServerLogsReq,
|
|
||||||
): Promise<RR.FollowServerLogsRes> {
|
|
||||||
return this.rpcRequest({ method: 'net.tor.logs.follow', params })
|
|
||||||
}
|
|
||||||
|
|
||||||
async followKernelLogs(
|
async followKernelLogs(
|
||||||
params: RR.FollowServerLogsReq,
|
params: RR.FollowServerLogsReq,
|
||||||
): Promise<RR.FollowServerLogsRes> {
|
): Promise<RR.FollowServerLogsRes> {
|
||||||
@@ -278,10 +268,6 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
|
|
||||||
return this.rpcRequest({ method: 'net.tor.reset', params })
|
|
||||||
}
|
|
||||||
|
|
||||||
// marketplace URLs
|
// marketplace URLs
|
||||||
|
|
||||||
async checkOSUpdate(
|
async checkOSUpdate(
|
||||||
@@ -369,6 +355,18 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.rpcRequest({ method: 'net.tunnel.remove', params })
|
return this.rpcRequest({ method: 'net.tunnel.remove', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setDefaultOutbound(
|
||||||
|
params: RR.SetDefaultOutboundReq,
|
||||||
|
): Promise<RR.SetDefaultOutboundRes> {
|
||||||
|
return this.rpcRequest({ method: 'net.gateway.set-default-outbound', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
async setServiceOutbound(
|
||||||
|
params: RR.SetServiceOutboundReq,
|
||||||
|
): Promise<RR.SetServiceOutboundRes> {
|
||||||
|
return this.rpcRequest({ method: 'package.set-outbound-gateway', params })
|
||||||
|
}
|
||||||
|
|
||||||
// wifi
|
// wifi
|
||||||
|
|
||||||
async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
|
async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
|
||||||
@@ -621,41 +619,11 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
|
async serverBindingSetAddressEnabled(
|
||||||
|
params: RR.ServerBindingSetAddressEnabledReq,
|
||||||
|
): Promise<RR.ServerBindingSetAddressEnabledRes> {
|
||||||
return this.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'net.tor.key.add',
|
method: 'server.host.binding.set-address-enabled',
|
||||||
params,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
|
|
||||||
return this.rpcRequest({
|
|
||||||
method: 'net.tor.key.generate',
|
|
||||||
params,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async serverBindingToggleGateway(
|
|
||||||
params: RR.ServerBindingToggleGatewayReq,
|
|
||||||
): Promise<RR.ServerBindingToggleGatewayRes> {
|
|
||||||
return this.rpcRequest({
|
|
||||||
method: 'server.host.binding.set-gateway-enabled',
|
|
||||||
params,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes> {
|
|
||||||
return this.rpcRequest({
|
|
||||||
method: 'server.host.address.onion.add',
|
|
||||||
params,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async serverRemoveOnion(
|
|
||||||
params: RR.ServerRemoveOnionReq,
|
|
||||||
): Promise<RR.RemoveOnionRes> {
|
|
||||||
return this.rpcRequest({
|
|
||||||
method: 'server.host.address.onion.remove',
|
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -696,27 +664,11 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async pkgBindingToggleGateway(
|
async pkgBindingSetAddressEnabled(
|
||||||
params: RR.PkgBindingToggleGatewayReq,
|
params: RR.PkgBindingSetAddressEnabledReq,
|
||||||
): Promise<RR.PkgBindingToggleGatewayRes> {
|
): Promise<RR.PkgBindingSetAddressEnabledRes> {
|
||||||
return this.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'package.host.binding.set-gateway-enabled',
|
method: 'package.host.binding.set-address-enabled',
|
||||||
params,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes> {
|
|
||||||
return this.rpcRequest({
|
|
||||||
method: 'package.host.address.onion.add',
|
|
||||||
params,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async pkgRemoveOnion(
|
|
||||||
params: RR.PkgRemoveOnionReq,
|
|
||||||
): Promise<RR.RemoveOnionRes> {
|
|
||||||
return this.rpcRequest({
|
|
||||||
method: 'package.host.address.onion.remove',
|
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,17 +281,6 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
|
||||||
await pauseFor(2000)
|
|
||||||
const entries = this.randomLogs(params.limit)
|
|
||||||
|
|
||||||
return {
|
|
||||||
entries,
|
|
||||||
startCursor: 'start-cursor',
|
|
||||||
endCursor: 'end-cursor',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getKernelLogs(
|
async getKernelLogs(
|
||||||
params: RR.GetServerLogsReq,
|
params: RR.GetServerLogsReq,
|
||||||
): Promise<RR.GetServerLogsRes> {
|
): Promise<RR.GetServerLogsRes> {
|
||||||
@@ -315,16 +304,6 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async followTorLogs(
|
|
||||||
params: RR.FollowServerLogsReq,
|
|
||||||
): Promise<RR.FollowServerLogsRes> {
|
|
||||||
await pauseFor(2000)
|
|
||||||
return {
|
|
||||||
startCursor: 'start-cursor',
|
|
||||||
guid: 'logs-guid',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async followKernelLogs(
|
async followKernelLogs(
|
||||||
params: RR.FollowServerLogsReq,
|
params: RR.FollowServerLogsReq,
|
||||||
): Promise<RR.FollowServerLogsRes> {
|
): Promise<RR.FollowServerLogsRes> {
|
||||||
@@ -504,11 +483,6 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
|
|
||||||
await pauseFor(2000)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// marketplace URLs
|
// marketplace URLs
|
||||||
|
|
||||||
async checkOSUpdate(
|
async checkOSUpdate(
|
||||||
@@ -592,13 +566,12 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
const id = `wg${this.proxyId++}`
|
const id = `wg${this.proxyId++}`
|
||||||
|
|
||||||
const patch: AddOperation<T.NetworkInterfaceInfo>[] = [
|
const patch: AddOperation<any>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.ADD,
|
op: PatchOp.ADD,
|
||||||
path: `/serverInfo/network/gateways/${id}`,
|
path: `/serverInfo/network/gateways/${id}`,
|
||||||
value: {
|
value: {
|
||||||
name: params.name,
|
name: params.name,
|
||||||
public: params.public,
|
|
||||||
secure: false,
|
secure: false,
|
||||||
ipInfo: {
|
ipInfo: {
|
||||||
name: id,
|
name: id,
|
||||||
@@ -610,9 +583,19 @@ export class MockApiService extends ApiService {
|
|||||||
lanIp: ['192.168.1.10'],
|
lanIp: ['192.168.1.10'],
|
||||||
dnsServers: [],
|
dnsServers: [],
|
||||||
},
|
},
|
||||||
|
type: params.type,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (params.setAsDefaultOutbound) {
|
||||||
|
;(patch as any[]).push({
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: '/serverInfo/network/defaultOutbound',
|
||||||
|
value: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.mockRevision(patch)
|
this.mockRevision(patch)
|
||||||
|
|
||||||
return { id }
|
return { id }
|
||||||
@@ -646,6 +629,38 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setDefaultOutbound(
|
||||||
|
params: RR.SetDefaultOutboundReq,
|
||||||
|
): Promise<RR.SetDefaultOutboundRes> {
|
||||||
|
await pauseFor(2000)
|
||||||
|
const patch = [
|
||||||
|
{
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: '/serverInfo/network/defaultOutbound',
|
||||||
|
value: params.gateway,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
this.mockRevision(patch)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async setServiceOutbound(
|
||||||
|
params: RR.SetServiceOutboundReq,
|
||||||
|
): Promise<RR.SetServiceOutboundRes> {
|
||||||
|
await pauseFor(2000)
|
||||||
|
const patch = [
|
||||||
|
{
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: `/packageData/${params.packageId}/outboundGateway`,
|
||||||
|
value: params.gateway,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
this.mockRevision(patch)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// wifi
|
// wifi
|
||||||
|
|
||||||
async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
|
async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
|
||||||
@@ -1374,77 +1389,12 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
|
async serverBindingSetAddressEnabled(
|
||||||
await pauseFor(2000)
|
params: RR.ServerBindingSetAddressEnabledReq,
|
||||||
return 'vanityabcdefghijklmnop.onion'
|
): Promise<RR.ServerBindingSetAddressEnabledRes> {
|
||||||
}
|
|
||||||
|
|
||||||
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
|
|
||||||
await pauseFor(2000)
|
|
||||||
return 'abcdefghijklmnopqrstuv.onion'
|
|
||||||
}
|
|
||||||
|
|
||||||
async serverBindingToggleGateway(
|
|
||||||
params: RR.ServerBindingToggleGatewayReq,
|
|
||||||
): Promise<RR.ServerBindingToggleGatewayRes> {
|
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
const patch = [
|
// Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
|
||||||
{
|
|
||||||
op: PatchOp.REPLACE,
|
|
||||||
path: `/serverInfo/network/host/bindings/${params.internalPort}/net/publicEnabled`,
|
|
||||||
value: params.enabled ? [params.gateway] : [],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch)
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes> {
|
|
||||||
await pauseFor(2000)
|
|
||||||
|
|
||||||
const patch: Operation<any>[] = [
|
|
||||||
{
|
|
||||||
op: PatchOp.ADD,
|
|
||||||
path: `/serverInfo/host/onions/0`,
|
|
||||||
value: params.onion,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: PatchOp.ADD,
|
|
||||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
|
||||||
value: {
|
|
||||||
kind: 'onion',
|
|
||||||
hostname: {
|
|
||||||
port: 80,
|
|
||||||
sslPort: 443,
|
|
||||||
value: params.onion,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch)
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async serverRemoveOnion(
|
|
||||||
params: RR.ServerRemoveOnionReq,
|
|
||||||
): Promise<RR.RemoveOnionRes> {
|
|
||||||
await pauseFor(2000)
|
|
||||||
|
|
||||||
const patch: RemoveOperation[] = [
|
|
||||||
{
|
|
||||||
op: PatchOp.REMOVE,
|
|
||||||
path: `/serverInfo/host/onions/0`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: PatchOp.REMOVE,
|
|
||||||
path: `/serverInfo/host/hostnameInfo/80/-1`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch)
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1463,10 +1413,9 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.ADD,
|
op: PatchOp.ADD,
|
||||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||||
value: {
|
value: {
|
||||||
kind: 'ip',
|
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||||
gatewayId: 'eth0',
|
|
||||||
public: true,
|
public: true,
|
||||||
hostname: {
|
hostname: {
|
||||||
kind: 'domain',
|
kind: 'domain',
|
||||||
@@ -1495,7 +1444,7 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.REMOVE,
|
op: PatchOp.REMOVE,
|
||||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(patch)
|
this.mockRevision(patch)
|
||||||
@@ -1516,10 +1465,9 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.ADD,
|
op: PatchOp.ADD,
|
||||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||||
value: {
|
value: {
|
||||||
kind: 'ip',
|
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||||
gatewayId: 'eth0',
|
|
||||||
public: false,
|
public: false,
|
||||||
hostname: {
|
hostname: {
|
||||||
kind: 'domain',
|
kind: 'domain',
|
||||||
@@ -1549,7 +1497,7 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.REMOVE,
|
op: PatchOp.REMOVE,
|
||||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(patch)
|
this.mockRevision(patch)
|
||||||
@@ -1557,67 +1505,12 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async pkgBindingToggleGateway(
|
async pkgBindingSetAddressEnabled(
|
||||||
params: RR.PkgBindingToggleGatewayReq,
|
params: RR.PkgBindingSetAddressEnabledReq,
|
||||||
): Promise<RR.PkgBindingToggleGatewayRes> {
|
): Promise<RR.PkgBindingSetAddressEnabledRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
const patch = [
|
// Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
|
||||||
{
|
|
||||||
op: PatchOp.REPLACE,
|
|
||||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/privateDisabled`,
|
|
||||||
value: params.enabled ? [] : [params.gateway],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch)
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes> {
|
|
||||||
await pauseFor(2000)
|
|
||||||
|
|
||||||
const patch: Operation<any>[] = [
|
|
||||||
{
|
|
||||||
op: PatchOp.ADD,
|
|
||||||
path: `/packageData/${params.package}/hosts/${params.host}/onions/0`,
|
|
||||||
value: params.onion,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: PatchOp.ADD,
|
|
||||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
|
||||||
value: {
|
|
||||||
kind: 'onion',
|
|
||||||
hostname: {
|
|
||||||
port: 80,
|
|
||||||
sslPort: 443,
|
|
||||||
value: params.onion,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch)
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async pkgRemoveOnion(
|
|
||||||
params: RR.PkgRemoveOnionReq,
|
|
||||||
): Promise<RR.RemoveOnionRes> {
|
|
||||||
await pauseFor(2000)
|
|
||||||
|
|
||||||
const patch: RemoveOperation[] = [
|
|
||||||
{
|
|
||||||
op: PatchOp.REMOVE,
|
|
||||||
path: `/packageData/${params.package}/hosts/${params.host}/onions/0`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: PatchOp.REMOVE,
|
|
||||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch)
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1636,10 +1529,9 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.ADD,
|
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: {
|
value: {
|
||||||
kind: 'ip',
|
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||||
gatewayId: 'eth0',
|
|
||||||
public: true,
|
public: true,
|
||||||
hostname: {
|
hostname: {
|
||||||
kind: 'domain',
|
kind: 'domain',
|
||||||
@@ -1668,7 +1560,7 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.REMOVE,
|
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)
|
this.mockRevision(patch)
|
||||||
@@ -1689,10 +1581,9 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.ADD,
|
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: {
|
value: {
|
||||||
kind: 'ip',
|
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||||
gatewayId: 'eth0',
|
|
||||||
public: false,
|
public: false,
|
||||||
hostname: {
|
hostname: {
|
||||||
kind: 'domain',
|
kind: 'domain',
|
||||||
@@ -1722,7 +1613,7 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.REMOVE,
|
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)
|
this.mockRevision(patch)
|
||||||
|
|||||||
@@ -38,8 +38,74 @@ export const mockPatchData: DataModel = {
|
|||||||
net: {
|
net: {
|
||||||
assignedPort: null,
|
assignedPort: null,
|
||||||
assignedSslPort: 443,
|
assignedSslPort: 443,
|
||||||
publicEnabled: [],
|
},
|
||||||
|
addresses: {
|
||||||
privateDisabled: [],
|
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: {
|
options: {
|
||||||
preferredExternalPort: 80,
|
preferredExternalPort: 80,
|
||||||
@@ -54,93 +120,12 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
publicDomains: {},
|
publicDomains: {},
|
||||||
privateDomains: [],
|
privateDomains: [],
|
||||||
onions: ['myveryownspecialtoraddress'],
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'onion',
|
|
||||||
hostname: {
|
|
||||||
value: 'myveryownspecialtoraddress.onion',
|
|
||||||
port: 80,
|
|
||||||
sslPort: 443,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
gateways: {
|
gateways: {
|
||||||
eth0: {
|
eth0: {
|
||||||
name: null,
|
name: null,
|
||||||
public: null,
|
|
||||||
secure: null,
|
secure: null,
|
||||||
|
type: null,
|
||||||
ipInfo: {
|
ipInfo: {
|
||||||
name: 'Wired Connection 1',
|
name: 'Wired Connection 1',
|
||||||
scopeId: 1,
|
scopeId: 1,
|
||||||
@@ -154,8 +139,8 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
wlan0: {
|
wlan0: {
|
||||||
name: null,
|
name: null,
|
||||||
public: null,
|
|
||||||
secure: null,
|
secure: null,
|
||||||
|
type: null,
|
||||||
ipInfo: {
|
ipInfo: {
|
||||||
name: 'Wireless Connection 1',
|
name: 'Wireless Connection 1',
|
||||||
scopeId: 2,
|
scopeId: 2,
|
||||||
@@ -172,8 +157,8 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
wireguard1: {
|
wireguard1: {
|
||||||
name: 'StartTunnel',
|
name: 'StartTunnel',
|
||||||
public: null,
|
|
||||||
secure: null,
|
secure: null,
|
||||||
|
type: 'inbound-outbound',
|
||||||
ipInfo: {
|
ipInfo: {
|
||||||
name: 'wireguard1',
|
name: 'wireguard1',
|
||||||
scopeId: 2,
|
scopeId: 2,
|
||||||
@@ -188,7 +173,23 @@ export const mockPatchData: DataModel = {
|
|||||||
dnsServers: ['1.1.1.1'],
|
dnsServers: ['1.1.1.1'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
wireguard2: {
|
||||||
|
name: 'Mullvad VPN',
|
||||||
|
secure: null,
|
||||||
|
type: 'outbound-only',
|
||||||
|
ipInfo: {
|
||||||
|
name: 'wireguard2',
|
||||||
|
scopeId: 4,
|
||||||
|
deviceType: 'wireguard',
|
||||||
|
subnets: [],
|
||||||
|
wanIp: '198.51.100.77',
|
||||||
|
ntpServers: [],
|
||||||
|
lanIp: [],
|
||||||
|
dnsServers: ['10.64.0.1'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
defaultOutbound: 'eth0',
|
||||||
dns: {
|
dns: {
|
||||||
dhcpServers: ['1.1.1.1', '8.8.8.8'],
|
dhcpServers: ['1.1.1.1', '8.8.8.8'],
|
||||||
staticServers: null,
|
staticServers: null,
|
||||||
@@ -335,6 +336,7 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
hosts: {},
|
hosts: {},
|
||||||
storeExposedDependents: [],
|
storeExposedDependents: [],
|
||||||
|
outboundGateway: null,
|
||||||
registry: 'https://registry.start9.com/',
|
registry: 'https://registry.start9.com/',
|
||||||
developerKey: 'developer-key',
|
developerKey: 'developer-key',
|
||||||
tasks: {
|
tasks: {
|
||||||
@@ -512,8 +514,74 @@ export const mockPatchData: DataModel = {
|
|||||||
net: {
|
net: {
|
||||||
assignedPort: 80,
|
assignedPort: 80,
|
||||||
assignedSslPort: 443,
|
assignedSslPort: 443,
|
||||||
publicEnabled: [],
|
},
|
||||||
|
addresses: {
|
||||||
privateDisabled: [],
|
privateDisabled: [],
|
||||||
|
publicEnabled: [],
|
||||||
|
possible: [
|
||||||
|
{
|
||||||
|
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||||
|
public: false,
|
||||||
|
hostname: {
|
||||||
|
kind: 'local',
|
||||||
|
value: 'adjective-noun.local',
|
||||||
|
port: null,
|
||||||
|
sslPort: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||||
|
public: false,
|
||||||
|
hostname: {
|
||||||
|
kind: 'local',
|
||||||
|
value: 'adjective-noun.local',
|
||||||
|
port: null,
|
||||||
|
sslPort: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||||
|
public: false,
|
||||||
|
hostname: {
|
||||||
|
kind: 'ipv4',
|
||||||
|
value: '10.0.0.1',
|
||||||
|
port: null,
|
||||||
|
sslPort: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||||
|
public: false,
|
||||||
|
hostname: {
|
||||||
|
kind: 'ipv4',
|
||||||
|
value: '10.0.0.2',
|
||||||
|
port: null,
|
||||||
|
sslPort: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
addSsl: null,
|
addSsl: null,
|
||||||
@@ -524,87 +592,6 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
publicDomains: {},
|
publicDomains: {},
|
||||||
privateDomains: [],
|
privateDomains: [],
|
||||||
onions: [],
|
|
||||||
hostnameInfo: {
|
|
||||||
80: [
|
|
||||||
{
|
|
||||||
kind: 'ip',
|
|
||||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
|
||||||
public: false,
|
|
||||||
hostname: {
|
|
||||||
kind: 'local',
|
|
||||||
value: 'adjective-noun.local',
|
|
||||||
port: null,
|
|
||||||
sslPort: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'ip',
|
|
||||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
|
||||||
public: false,
|
|
||||||
hostname: {
|
|
||||||
kind: 'local',
|
|
||||||
value: 'adjective-noun.local',
|
|
||||||
port: null,
|
|
||||||
sslPort: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'ip',
|
|
||||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
|
||||||
public: false,
|
|
||||||
hostname: {
|
|
||||||
kind: 'ipv4',
|
|
||||||
value: '10.0.0.1',
|
|
||||||
port: null,
|
|
||||||
sslPort: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'ip',
|
|
||||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
|
||||||
public: false,
|
|
||||||
hostname: {
|
|
||||||
kind: 'ipv4',
|
|
||||||
value: '10.0.0.2',
|
|
||||||
port: null,
|
|
||||||
sslPort: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: 1234,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: 'onion',
|
|
||||||
hostname: {
|
|
||||||
value: 'bitcoin-p2p.onion',
|
|
||||||
port: 80,
|
|
||||||
sslPort: 443,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
bcdefgh: {
|
bcdefgh: {
|
||||||
bindings: {
|
bindings: {
|
||||||
@@ -613,8 +600,11 @@ export const mockPatchData: DataModel = {
|
|||||||
net: {
|
net: {
|
||||||
assignedPort: 8332,
|
assignedPort: 8332,
|
||||||
assignedSslPort: null,
|
assignedSslPort: null,
|
||||||
publicEnabled: [],
|
},
|
||||||
|
addresses: {
|
||||||
privateDisabled: [],
|
privateDisabled: [],
|
||||||
|
publicEnabled: [],
|
||||||
|
possible: [],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
addSsl: null,
|
addSsl: null,
|
||||||
@@ -625,10 +615,6 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
publicDomains: {},
|
publicDomains: {},
|
||||||
privateDomains: [],
|
privateDomains: [],
|
||||||
onions: [],
|
|
||||||
hostnameInfo: {
|
|
||||||
8332: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
cdefghi: {
|
cdefghi: {
|
||||||
bindings: {
|
bindings: {
|
||||||
@@ -637,8 +623,11 @@ export const mockPatchData: DataModel = {
|
|||||||
net: {
|
net: {
|
||||||
assignedPort: 8333,
|
assignedPort: 8333,
|
||||||
assignedSslPort: null,
|
assignedSslPort: null,
|
||||||
publicEnabled: [],
|
},
|
||||||
|
addresses: {
|
||||||
privateDisabled: [],
|
privateDisabled: [],
|
||||||
|
publicEnabled: [],
|
||||||
|
possible: [],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
addSsl: null,
|
addSsl: null,
|
||||||
@@ -649,13 +638,10 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
publicDomains: {},
|
publicDomains: {},
|
||||||
privateDomains: [],
|
privateDomains: [],
|
||||||
onions: [],
|
|
||||||
hostnameInfo: {
|
|
||||||
8333: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
storeExposedDependents: [],
|
storeExposedDependents: [],
|
||||||
|
outboundGateway: null,
|
||||||
registry: 'https://registry.start9.com/',
|
registry: 'https://registry.start9.com/',
|
||||||
developerKey: 'developer-key',
|
developerKey: 'developer-key',
|
||||||
tasks: {
|
tasks: {
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export class ConfigService {
|
|||||||
private getAccessType = utils.once(() => {
|
private getAccessType = utils.once(() => {
|
||||||
if (useMocks) return mocks.maskAs
|
if (useMocks) return mocks.maskAs
|
||||||
if (this.hostname === 'localhost') return 'localhost'
|
if (this.hostname === 'localhost') return 'localhost'
|
||||||
if (this.hostname.endsWith('.onion')) return 'tor'
|
|
||||||
if (this.hostname.endsWith('.local')) return 'mdns'
|
if (this.hostname.endsWith('.local')) return 'mdns'
|
||||||
let ip = null
|
let ip = null
|
||||||
try {
|
try {
|
||||||
@@ -51,7 +50,7 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isLanHttp(): boolean {
|
isLanHttp(): boolean {
|
||||||
return !this.isHttps() && !['localhost', 'tor'].includes(this.accessType)
|
return !this.isHttps() && this.accessType !== 'localhost'
|
||||||
}
|
}
|
||||||
|
|
||||||
isHttps(): boolean {
|
isHttps(): boolean {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { inject, Injectable } from '@angular/core'
|
import { inject, Injectable } from '@angular/core'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { T, utils } from '@start9labs/start-sdk'
|
import { T, utils } from '@start9labs/start-sdk'
|
||||||
import { map } from 'rxjs/operators'
|
import { map } from 'rxjs'
|
||||||
import { DataModel } from './patch-db/data-model'
|
import { DataModel } from './patch-db/data-model'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
|
|
||||||
@@ -12,39 +12,47 @@ export type GatewayPlus = T.NetworkInterfaceInfo & {
|
|||||||
subnets: utils.IpNet[]
|
subnets: utils.IpNet[]
|
||||||
lanIpv4: string[]
|
lanIpv4: string[]
|
||||||
wanIp?: utils.IpAddress
|
wanIp?: utils.IpAddress
|
||||||
public: boolean
|
isDefaultOutbound: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GatewayService {
|
export class GatewayService {
|
||||||
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
|
|
||||||
|
private readonly network$ = this.patch.watch$('serverInfo', 'network')
|
||||||
|
|
||||||
|
readonly defaultOutbound = toSignal(
|
||||||
|
this.network$.pipe(map(n => n.defaultOutbound)),
|
||||||
|
)
|
||||||
|
|
||||||
readonly gateways = toSignal(
|
readonly gateways = toSignal(
|
||||||
inject<PatchDB<DataModel>>(PatchDB)
|
this.network$.pipe(
|
||||||
.watch$('serverInfo', 'network', 'gateways')
|
map(network => {
|
||||||
.pipe(
|
const gateways = network.gateways
|
||||||
map(gateways =>
|
const defaultOutbound = network.defaultOutbound
|
||||||
Object.entries(gateways)
|
return Object.entries(gateways)
|
||||||
.filter(([_, val]) => !!val?.ipInfo)
|
.filter(([_, val]) => !!val?.ipInfo)
|
||||||
.filter(
|
.filter(
|
||||||
([_, val]) =>
|
([_, val]) =>
|
||||||
val?.ipInfo?.deviceType !== 'bridge' &&
|
val?.ipInfo?.deviceType !== 'bridge' &&
|
||||||
val?.ipInfo?.deviceType !== 'loopback',
|
val?.ipInfo?.deviceType !== 'loopback',
|
||||||
)
|
)
|
||||||
.map(([id, val]) => {
|
.map(([id, val]) => {
|
||||||
const subnets =
|
const subnets =
|
||||||
val.ipInfo?.subnets.map(s => utils.IpNet.parse(s)) ?? []
|
val.ipInfo?.subnets.map(s => utils.IpNet.parse(s)) ?? []
|
||||||
const name = val.name ?? val.ipInfo!.name
|
const name = val.name ?? val.ipInfo!.name
|
||||||
return {
|
return {
|
||||||
...val,
|
...val,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
subnets,
|
subnets,
|
||||||
lanIpv4: subnets.filter(s => s.isIpv4()).map(s => s.address),
|
lanIpv4: subnets.filter(s => s.isIpv4()).map(s => s.address),
|
||||||
public: val.public ?? subnets.some(s => s.isPublic()),
|
wanIp:
|
||||||
wanIp:
|
val.ipInfo?.wanIp && utils.IpAddress.parse(val.ipInfo?.wanIp),
|
||||||
val.ipInfo?.wanIp && utils.IpAddress.parse(val.ipInfo?.wanIp),
|
isDefaultOutbound: id === defaultOutbound,
|
||||||
} as GatewayPlus
|
} as GatewayPlus
|
||||||
}),
|
})
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user