mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
Merge branch 'feat/preferred-port-design' of github.com:Start9Labs/start-os into feature/outbound-gateways
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -840,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(
|
||||||
@@ -860,7 +855,6 @@ impl NetworkInterfaceWatcher {
|
|||||||
watcher(ip_info, activated).await
|
watcher(ip_info, activated).await
|
||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
listeners: SyncMutex::new(BTreeMap::new()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,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 {
|
||||||
@@ -1239,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,
|
||||||
@@ -1479,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,
|
||||||
@@ -1511,213 +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(),
|
|
||||||
})),
|
|
||||||
gateway_type: None,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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};
|
|
||||||
@@ -195,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()
|
||||||
|
|||||||
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export type AccessType =
|
export type AccessType =
|
||||||
| 'tor'
|
|
||||||
| 'mdns'
|
| 'mdns'
|
||||||
| 'localhost'
|
| 'localhost'
|
||||||
| 'ipv4'
|
| 'ipv4'
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,10 +2250,6 @@ export namespace Mock {
|
|||||||
},
|
},
|
||||||
publicDomains: {},
|
publicDomains: {},
|
||||||
privateDomains: [],
|
privateDomains: [],
|
||||||
onions: [],
|
|
||||||
hostnameInfo: {
|
|
||||||
8333: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
storeExposedDependents: [],
|
storeExposedDependents: [],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -301,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
|
||||||
@@ -351,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>
|
||||||
@@ -352,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,
|
||||||
@@ -384,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(
|
||||||
@@ -633,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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -708,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(
|
||||||
@@ -1415,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1504,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',
|
||||||
@@ -1536,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)
|
||||||
@@ -1557,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',
|
||||||
@@ -1590,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)
|
||||||
@@ -1598,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1677,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',
|
||||||
@@ -1709,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)
|
||||||
@@ -1730,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',
|
||||||
@@ -1763,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,87 +120,6 @@ 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: {
|
||||||
@@ -529,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,
|
||||||
@@ -541,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: {
|
||||||
@@ -630,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,
|
||||||
@@ -642,10 +615,6 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
publicDomains: {},
|
publicDomains: {},
|
||||||
privateDomains: [],
|
privateDomains: [],
|
||||||
onions: [],
|
|
||||||
hostnameInfo: {
|
|
||||||
8332: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
cdefghi: {
|
cdefghi: {
|
||||||
bindings: {
|
bindings: {
|
||||||
@@ -654,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,
|
||||||
@@ -666,10 +638,6 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
publicDomains: {},
|
publicDomains: {},
|
||||||
privateDomains: [],
|
privateDomains: [],
|
||||||
onions: [],
|
|
||||||
hostnameInfo: {
|
|
||||||
8333: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
storeExposedDependents: [],
|
storeExposedDependents: [],
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user