mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 12:33: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": {
|
||||
"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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
|
||||
## 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
|
||||
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
|
||||
if ! iptables -C $kind -j "${NAME}_${kind}" 2> /dev/null; then
|
||||
@@ -36,8 +36,22 @@ if [ "$UNDO" = 1 ]; then
|
||||
fi
|
||||
|
||||
# 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"
|
||||
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||
# When src_subnet is set, only forward traffic from that subnet (private forwards)
|
||||
# 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)
|
||||
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
|
||||
@@ -52,4 +66,4 @@ iptables -t nat -A ${NAME}_POSTROUTING -d "$dip" -p udp --dport "$dport" -j MASQ
|
||||
iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT
|
||||
iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT
|
||||
|
||||
exit $err
|
||||
exit $err
|
||||
|
||||
@@ -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:
|
||||
|
||||
| Procedure | Description |
|
||||
|-----------|-------------|
|
||||
| `/backup/create` | Create a backup |
|
||||
| Procedure | Description |
|
||||
| -------------------------- | ---------------------------- |
|
||||
| `/backup/create` | Create a backup |
|
||||
| `/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") {
|
||||
const hostInfo = await effects.getHostInfo({
|
||||
hostId: volumeMount["interface-id"],
|
||||
})
|
||||
const hostnames = [
|
||||
`${packageId}.embassy`,
|
||||
...new Set(
|
||||
Object.values(
|
||||
(
|
||||
await effects.getHostInfo({
|
||||
hostId: volumeMount["interface-id"],
|
||||
})
|
||||
)?.hostnameInfo || {},
|
||||
)
|
||||
.flatMap((h) => h)
|
||||
.flatMap((h) => (h.kind === "onion" ? [h.hostname.value] : [])),
|
||||
Object.values(hostInfo?.bindings || {})
|
||||
.flatMap((b) => b.addresses.possible)
|
||||
.map((h) => h.hostname.value),
|
||||
).values(),
|
||||
]
|
||||
const certChain = await effects.getSslCertificate({
|
||||
|
||||
@@ -1244,12 +1244,8 @@ async function updateConfig(
|
||||
? ""
|
||||
: catchFn(
|
||||
() =>
|
||||
(specValue.target === "lan-address"
|
||||
? filled.addressInfo!.filter({ kind: "mdns" }) ||
|
||||
filled.addressInfo!.onion
|
||||
: filled.addressInfo!.onion ||
|
||||
filled.addressInfo!.filter({ kind: "mdns" })
|
||||
).hostnames[0].hostname.value,
|
||||
filled.addressInfo!.filter({ kind: "mdns" })!.hostnames[0]
|
||||
.hostname.value,
|
||||
) || ""
|
||||
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"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/Start9Labs/start-os"
|
||||
version = "0.4.0-alpha.19" # VERSION_BUMP
|
||||
version = "0.4.0-alpha.20" # VERSION_BUMP
|
||||
|
||||
[lib]
|
||||
name = "startos"
|
||||
@@ -42,17 +42,6 @@ name = "tunnelbox"
|
||||
path = "src/main/tunnelbox.rs"
|
||||
|
||||
[features]
|
||||
arti = [
|
||||
"arti-client",
|
||||
"safelog",
|
||||
"tor-cell",
|
||||
"tor-hscrypto",
|
||||
"tor-hsservice",
|
||||
"tor-keymgr",
|
||||
"tor-llcrypto",
|
||||
"tor-proto",
|
||||
"tor-rtcompat",
|
||||
]
|
||||
beta = []
|
||||
console = ["console-subscriber", "tokio/tracing"]
|
||||
default = []
|
||||
@@ -62,16 +51,6 @@ unstable = ["backtrace-on-stack-overflow"]
|
||||
|
||||
[dependencies]
|
||||
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 = [
|
||||
"use_rustls",
|
||||
"use_tokio",
|
||||
@@ -100,7 +79,6 @@ console-subscriber = { version = "0.5.0", optional = true }
|
||||
const_format = "0.2.34"
|
||||
cookie = "0.18.0"
|
||||
cookie_store = "0.22.0"
|
||||
curve25519-dalek = "4.1.3"
|
||||
der = { version = "0.7.9", features = ["derive", "pem"] }
|
||||
digest = "0.10.7"
|
||||
divrem = "1.0.0"
|
||||
@@ -216,7 +194,6 @@ rpassword = "7.2.0"
|
||||
rust-argon2 = "3.0.0"
|
||||
rust-i18n = "3.1.5"
|
||||
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"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
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-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
|
||||
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"
|
||||
tracing = "0.1.39"
|
||||
tracing-error = "0.2.0"
|
||||
|
||||
@@ -8,7 +8,6 @@ use openssl::x509::X509;
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::hostname::{Hostname, generate_hostname, generate_id};
|
||||
use crate::net::ssl::{gen_nistp256, make_root_cert};
|
||||
use crate::net::tor::TorSecretKey;
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::Pem;
|
||||
|
||||
@@ -26,7 +25,6 @@ pub struct AccountInfo {
|
||||
pub server_id: String,
|
||||
pub hostname: Hostname,
|
||||
pub password: String,
|
||||
pub tor_keys: Vec<TorSecretKey>,
|
||||
pub root_ca_key: PKey<Private>,
|
||||
pub root_ca_cert: X509,
|
||||
pub ssh_key: ssh_key::PrivateKey,
|
||||
@@ -36,7 +34,6 @@ impl AccountInfo {
|
||||
pub fn new(password: &str, start_time: SystemTime) -> Result<Self, Error> {
|
||||
let server_id = generate_id();
|
||||
let hostname = generate_hostname();
|
||||
let tor_key = vec![TorSecretKey::generate()];
|
||||
let root_ca_key = gen_nistp256()?;
|
||||
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(
|
||||
@@ -48,7 +45,6 @@ impl AccountInfo {
|
||||
server_id,
|
||||
hostname,
|
||||
password: hash_password(password)?,
|
||||
tor_keys: tor_key,
|
||||
root_ca_key,
|
||||
root_ca_cert,
|
||||
ssh_key,
|
||||
@@ -61,17 +57,6 @@ impl AccountInfo {
|
||||
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
|
||||
let password = db.as_private().as_password().de()?;
|
||||
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 root_ca_key = cert_store.as_root_key().de()?.0;
|
||||
let root_ca_cert = cert_store.as_root_cert().de()?.0;
|
||||
@@ -82,7 +67,6 @@ impl AccountInfo {
|
||||
server_id,
|
||||
hostname,
|
||||
password,
|
||||
tor_keys,
|
||||
root_ca_key,
|
||||
root_ca_cert,
|
||||
ssh_key,
|
||||
@@ -97,17 +81,6 @@ impl AccountInfo {
|
||||
server_info
|
||||
.as_pubkey_mut()
|
||||
.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)?;
|
||||
db.as_private_mut().as_password_mut().ser(&self.password)?;
|
||||
db.as_private_mut()
|
||||
@@ -117,9 +90,6 @@ impl AccountInfo {
|
||||
.as_developer_key_mut()
|
||||
.ser(Pem::new_ref(&self.developer_key))?;
|
||||
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();
|
||||
if cert_store.as_root_cert().de()?.0 != self.root_ca_cert {
|
||||
cert_store
|
||||
@@ -148,11 +118,5 @@ impl AccountInfo {
|
||||
self.hostname.no_dot_host_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::hostname::{Hostname, generate_hostname, generate_id};
|
||||
use crate::net::tor::TorSecretKey;
|
||||
use crate::prelude::*;
|
||||
use crate::util::crypto::ed25519_expand_key;
|
||||
use crate::util::serde::{Base32, Base64, Pem};
|
||||
|
||||
pub struct OsBackup {
|
||||
@@ -85,10 +83,6 @@ impl OsBackupV0 {
|
||||
&mut ssh_key::rand_core::OsRng::default(),
|
||||
ssh_key::Algorithm::Ed25519,
|
||||
)?,
|
||||
tor_keys: TorSecretKey::from_bytes(self.tor_key.0)
|
||||
.ok()
|
||||
.into_iter()
|
||||
.collect(),
|
||||
developer_key: ed25519_dalek::SigningKey::generate(
|
||||
&mut ssh_key::rand_core::OsRng::default(),
|
||||
),
|
||||
@@ -119,10 +113,6 @@ impl OsBackupV1 {
|
||||
root_ca_key: self.root_ca_key.0,
|
||||
root_ca_cert: self.root_ca_cert.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),
|
||||
},
|
||||
ui: self.ui,
|
||||
@@ -140,7 +130,6 @@ struct OsBackupV2 {
|
||||
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
|
||||
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
|
||||
ui: Value, // JSON Value
|
||||
}
|
||||
@@ -154,7 +143,6 @@ impl OsBackupV2 {
|
||||
root_ca_key: self.root_ca_key.0,
|
||||
root_ca_cert: self.root_ca_cert.0,
|
||||
ssh_key: self.ssh_key.0,
|
||||
tor_keys: self.tor_keys,
|
||||
developer_key: self.compat_s9pk_key.0,
|
||||
},
|
||||
ui: self.ui,
|
||||
@@ -167,7 +155,6 @@ impl OsBackupV2 {
|
||||
root_ca_key: Pem(backup.account.root_ca_key.clone()),
|
||||
root_ca_cert: Pem(backup.account.root_ca_cert.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()),
|
||||
ui: backup.ui.clone(),
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::disk::fsck::RepairStrategy;
|
||||
use crate::disk::main::DEFAULT_PASSWORD;
|
||||
use crate::firmware::{check_for_firmware_update, update_firmware};
|
||||
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::prelude::*;
|
||||
use crate::progress::FullProgressTracker;
|
||||
@@ -19,7 +19,7 @@ use crate::{DATA_DIR, PLATFORM};
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn setup_or_init(
|
||||
server: &mut WebServer<UpgradableListener>,
|
||||
server: &mut WebServer<WildcardListener>,
|
||||
config: &ServerConfig,
|
||||
) -> Result<Result<(RpcContext, FullProgressTracker), Shutdown>, Error> {
|
||||
if let Some(firmware) = check_for_firmware_update()
|
||||
@@ -204,7 +204,7 @@ async fn setup_or_init(
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn main(
|
||||
server: &mut WebServer<UpgradableListener>,
|
||||
server: &mut WebServer<WildcardListener>,
|
||||
config: &ServerConfig,
|
||||
) -> Result<Result<(RpcContext, FullProgressTracker), Shutdown>, Error> {
|
||||
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::rpc::InitRpcContextPhases;
|
||||
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::web_server::{Acceptor, WebServer};
|
||||
use crate::prelude::*;
|
||||
@@ -23,7 +23,7 @@ use crate::util::logger::LOGGER;
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn inner_main(
|
||||
server: &mut WebServer<UpgradableListener>,
|
||||
server: &mut WebServer<WildcardListener>,
|
||||
config: &ServerConfig,
|
||||
) -> Result<Option<Shutdown>, Error> {
|
||||
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"));
|
||||
let res = rt.block_on(async {
|
||||
let mut server = WebServer::new(
|
||||
Acceptor::bind_upgradable(SelfContainedNetworkInterfaceListener::bind(BindTcp, 80)),
|
||||
Acceptor::new(WildcardListener::new(80)?),
|
||||
refresher(),
|
||||
);
|
||||
match inner_main(&mut server, &config).await {
|
||||
|
||||
@@ -13,7 +13,7 @@ use visit_rs::Visit;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::context::config::ClientConfig;
|
||||
use crate::net::gateway::{Bind, BindTcp};
|
||||
use tokio::net::TcpListener;
|
||||
use crate::net::tls::TlsListener;
|
||||
use crate::net::web_server::{Accept, Acceptor, MetadataVisitor, WebServer};
|
||||
use crate::prelude::*;
|
||||
@@ -57,7 +57,12 @@ async fn inner_main(config: &TunnelConfig) -> Result<(), Error> {
|
||||
if !a.contains_key(&key) {
|
||||
match (|| {
|
||||
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 {
|
||||
db: https_db.clone(),
|
||||
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::install::PKG_ARCHIVE_DIR;
|
||||
use crate::lxc::LxcManager;
|
||||
use crate::net::gateway::UpgradableListener;
|
||||
use crate::net::gateway::WildcardListener;
|
||||
use crate::net::net_controller::{NetController, NetService};
|
||||
use crate::net::socks::DEFAULT_SOCKS_LISTEN;
|
||||
use crate::net::utils::{find_eth_iface, find_wifi_iface};
|
||||
@@ -132,7 +132,7 @@ pub struct RpcContext(Arc<RpcContextSeed>);
|
||||
impl RpcContext {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn init(
|
||||
webserver: &WebServerAcceptorSetter<UpgradableListener>,
|
||||
webserver: &WebServerAcceptorSetter<WildcardListener>,
|
||||
config: &ServerConfig,
|
||||
disk_guid: InternedString,
|
||||
init_result: Option<InitResult>,
|
||||
@@ -167,7 +167,7 @@ impl RpcContext {
|
||||
} else {
|
||||
let net_ctrl =
|
||||
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?;
|
||||
(net_ctrl, os_net_service)
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::context::RpcContext;
|
||||
use crate::context::config::ServerConfig;
|
||||
use crate::disk::mount::guard::{MountGuard, TmpMountGuard};
|
||||
use crate::hostname::Hostname;
|
||||
use crate::net::gateway::UpgradableListener;
|
||||
use crate::net::gateway::WildcardListener;
|
||||
use crate::net::web_server::{WebServer, WebServerAcceptorSetter};
|
||||
use crate::prelude::*;
|
||||
use crate::progress::FullProgressTracker;
|
||||
@@ -51,7 +51,7 @@ pub struct SetupResult {
|
||||
}
|
||||
|
||||
pub struct SetupContextSeed {
|
||||
pub webserver: WebServerAcceptorSetter<UpgradableListener>,
|
||||
pub webserver: WebServerAcceptorSetter<WildcardListener>,
|
||||
pub config: SyncMutex<ServerConfig>,
|
||||
pub disable_encryption: bool,
|
||||
pub progress: FullProgressTracker,
|
||||
@@ -70,7 +70,7 @@ pub struct SetupContext(Arc<SetupContextSeed>);
|
||||
impl SetupContext {
|
||||
#[instrument(skip_all)]
|
||||
pub fn init(
|
||||
webserver: &WebServer<UpgradableListener>,
|
||||
webserver: &WebServer<WildcardListener>,
|
||||
config: ServerConfig,
|
||||
) -> Result<Self, Error> {
|
||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::db::model::Database;
|
||||
use crate::db::model::package::AllPackageData;
|
||||
use crate::net::acme::AcmeProvider;
|
||||
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::vhost::AlpnInfo;
|
||||
use crate::prelude::*;
|
||||
@@ -63,36 +63,35 @@ impl Public {
|
||||
post_init_migration_todos: BTreeMap::new(),
|
||||
network: NetworkInfo {
|
||||
host: Host {
|
||||
bindings: [(
|
||||
80,
|
||||
BindInfo {
|
||||
enabled: false,
|
||||
options: BindOptions {
|
||||
preferred_external_port: 80,
|
||||
add_ssl: Some(AddSslOptions {
|
||||
preferred_external_port: 443,
|
||||
add_x_forwarded_headers: false,
|
||||
alpn: Some(AlpnInfo::Specified(vec![
|
||||
MaybeUtf8String("h2".into()),
|
||||
MaybeUtf8String("http/1.1".into()),
|
||||
])),
|
||||
}),
|
||||
secure: None,
|
||||
bindings: Bindings(
|
||||
[(
|
||||
80,
|
||||
BindInfo {
|
||||
enabled: false,
|
||||
options: BindOptions {
|
||||
preferred_external_port: 80,
|
||||
add_ssl: Some(AddSslOptions {
|
||||
preferred_external_port: 443,
|
||||
add_x_forwarded_headers: false,
|
||||
alpn: Some(AlpnInfo::Specified(vec![
|
||||
MaybeUtf8String("h2".into()),
|
||||
MaybeUtf8String("http/1.1".into()),
|
||||
])),
|
||||
}),
|
||||
secure: None,
|
||||
},
|
||||
net: NetInfo {
|
||||
assigned_port: None,
|
||||
assigned_ssl_port: Some(443),
|
||||
},
|
||||
addresses: DerivedAddressInfo::default(),
|
||||
},
|
||||
net: NetInfo {
|
||||
assigned_port: None,
|
||||
assigned_ssl_port: Some(443),
|
||||
private_disabled: OrdSet::new(),
|
||||
public_enabled: OrdSet::new(),
|
||||
},
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
onions: account.tor_keys.iter().map(|k| k.onion_address()).collect(),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
public_domains: BTreeMap::new(),
|
||||
private_domains: BTreeSet::new(),
|
||||
hostname_info: BTreeMap::new(),
|
||||
},
|
||||
wifi: WifiInfo {
|
||||
enabled: true,
|
||||
|
||||
@@ -42,11 +42,11 @@ pub enum ErrorKind {
|
||||
ParseUrl = 19,
|
||||
DiskNotAvailable = 20,
|
||||
BlockDevice = 21,
|
||||
InvalidOnionAddress = 22,
|
||||
// InvalidOnionAddress = 22,
|
||||
Pack = 23,
|
||||
ValidateS9pk = 24,
|
||||
DiskCorrupted = 25, // Remove
|
||||
Tor = 26,
|
||||
// Tor = 26,
|
||||
ConfigGen = 27,
|
||||
ParseNumber = 28,
|
||||
Database = 29,
|
||||
@@ -126,11 +126,11 @@ impl ErrorKind {
|
||||
ParseUrl => t!("error.parse-url"),
|
||||
DiskNotAvailable => t!("error.disk-not-available"),
|
||||
BlockDevice => t!("error.block-device"),
|
||||
InvalidOnionAddress => t!("error.invalid-onion-address"),
|
||||
// InvalidOnionAddress => t!("error.invalid-onion-address"),
|
||||
Pack => t!("error.pack"),
|
||||
ValidateS9pk => t!("error.validate-s9pk"),
|
||||
DiskCorrupted => t!("error.disk-corrupted"), // Remove
|
||||
Tor => t!("error.tor"),
|
||||
// Tor => t!("error.tor"),
|
||||
ConfigGen => t!("error.config-gen"),
|
||||
ParseNumber => t!("error.parse-number"),
|
||||
Database => t!("error.database"),
|
||||
@@ -370,17 +370,6 @@ impl From<reqwest::Error> for Error {
|
||||
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 {
|
||||
fn from(e: zbus::Error) -> Self {
|
||||
Error::new(e, ErrorKind::DBus)
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::db::model::public::ServerStatus;
|
||||
use crate::developer::OS_DEVELOPER_KEY_PATH;
|
||||
use crate::hostname::Hostname;
|
||||
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::socks::DEFAULT_SOCKS_LISTEN;
|
||||
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)]
|
||||
pub async fn init(
|
||||
webserver: &WebServerAcceptorSetter<UpgradableListener>,
|
||||
webserver: &WebServerAcceptorSetter<WildcardListener>,
|
||||
cfg: &ServerConfig,
|
||||
InitPhases {
|
||||
preinit,
|
||||
@@ -218,7 +218,7 @@ pub async fn init(
|
||||
)
|
||||
.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?;
|
||||
start_net.complete();
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use id_pool::IdPool;
|
||||
use iddqd::{IdOrdItem, IdOrdMap};
|
||||
use rand::Rng;
|
||||
use imbl::OrdMap;
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -15,7 +15,6 @@ use tokio::sync::mpsc;
|
||||
use crate::GatewayId;
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::public::NetworkInterfaceInfo;
|
||||
use crate::net::gateway::{DynInterfaceFilter, InterfaceFilter};
|
||||
use crate::prelude::*;
|
||||
use crate::util::Invoke;
|
||||
use crate::util::future::NonDetachingJoinHandle;
|
||||
@@ -23,25 +22,76 @@ use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
use crate::util::sync::Watch;
|
||||
|
||||
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)]
|
||||
pub struct AvailablePorts(IdPool);
|
||||
pub struct AvailablePorts(BTreeMap<u16, bool>);
|
||||
impl AvailablePorts {
|
||||
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> {
|
||||
self.0.request_id().ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("net.forward.no-dynamic-ports-available")),
|
||||
ErrorKind::Network,
|
||||
)
|
||||
})
|
||||
pub fn alloc(&mut self, ssl: bool) -> Result<u16, Error> {
|
||||
let mut rng = rand::rng();
|
||||
for _ in 0..1000 {
|
||||
let port = rng.random_range(EPHEMERAL_PORT_START..u16::MAX);
|
||||
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>) {
|
||||
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();
|
||||
table.add_row(row![bc => "FROM", "TO", "FILTER"]);
|
||||
table.add_row(row![bc => "FROM", "TO", "REQS"]);
|
||||
|
||||
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)?;
|
||||
@@ -79,6 +129,7 @@ struct ForwardMapping {
|
||||
source: SocketAddrV4,
|
||||
target: SocketAddrV4,
|
||||
target_prefix: u8,
|
||||
src_filter: Option<SourceFilter>,
|
||||
rc: Weak<()>,
|
||||
}
|
||||
|
||||
@@ -93,9 +144,10 @@ impl PortForwardState {
|
||||
source: SocketAddrV4,
|
||||
target: SocketAddrV4,
|
||||
target_prefix: u8,
|
||||
src_filter: Option<SourceFilter>,
|
||||
) -> Result<Arc<()>, Error> {
|
||||
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() {
|
||||
return Ok(existing_rc);
|
||||
} else {
|
||||
@@ -104,21 +156,28 @@ impl PortForwardState {
|
||||
return Ok(rc);
|
||||
}
|
||||
} 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) {
|
||||
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(());
|
||||
forward(source, target, target_prefix).await?;
|
||||
forward(source, target, target_prefix, src_filter.as_ref()).await?;
|
||||
self.mappings.insert(
|
||||
source,
|
||||
ForwardMapping {
|
||||
source,
|
||||
target,
|
||||
target_prefix,
|
||||
src_filter,
|
||||
rc: Arc::downgrade(&rc),
|
||||
},
|
||||
);
|
||||
@@ -136,7 +195,13 @@ impl PortForwardState {
|
||||
|
||||
for source in to_remove {
|
||||
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(())
|
||||
@@ -157,9 +222,14 @@ impl Drop for PortForwardState {
|
||||
let mappings = std::mem::take(&mut self.mappings);
|
||||
tokio::spawn(async move {
|
||||
for (_, mapping) in mappings {
|
||||
unforward(mapping.source, mapping.target, mapping.target_prefix)
|
||||
.await
|
||||
.log_err();
|
||||
unforward(
|
||||
mapping.source,
|
||||
mapping.target,
|
||||
mapping.target_prefix,
|
||||
mapping.src_filter.as_ref(),
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -171,6 +241,7 @@ enum PortForwardCommand {
|
||||
source: SocketAddrV4,
|
||||
target: SocketAddrV4,
|
||||
target_prefix: u8,
|
||||
src_filter: Option<SourceFilter>,
|
||||
respond: oneshot::Sender<Result<Arc<()>, Error>>,
|
||||
},
|
||||
Gc {
|
||||
@@ -257,9 +328,12 @@ impl PortForwardController {
|
||||
source,
|
||||
target,
|
||||
target_prefix,
|
||||
src_filter,
|
||||
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();
|
||||
}
|
||||
PortForwardCommand::Gc { respond } => {
|
||||
@@ -284,6 +358,7 @@ impl PortForwardController {
|
||||
source: SocketAddrV4,
|
||||
target: SocketAddrV4,
|
||||
target_prefix: u8,
|
||||
src_filter: Option<SourceFilter>,
|
||||
) -> Result<Arc<()>, Error> {
|
||||
let (send, recv) = oneshot::channel();
|
||||
self.req
|
||||
@@ -291,6 +366,7 @@ impl PortForwardController {
|
||||
source,
|
||||
target,
|
||||
target_prefix,
|
||||
src_filter,
|
||||
respond: send,
|
||||
})
|
||||
.map_err(err_has_exited)?;
|
||||
@@ -321,14 +397,14 @@ struct InterfaceForwardRequest {
|
||||
external: u16,
|
||||
target: SocketAddrV4,
|
||||
target_prefix: u8,
|
||||
filter: DynInterfaceFilter,
|
||||
reqs: ForwardRequirements,
|
||||
rc: Arc<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct InterfaceForwardEntry {
|
||||
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
|
||||
forwards: BTreeMap<SocketAddrV4, Arc<()>>,
|
||||
}
|
||||
@@ -346,7 +422,7 @@ impl InterfaceForwardEntry {
|
||||
fn new(external: u16) -> Self {
|
||||
Self {
|
||||
external,
|
||||
filter: BTreeMap::new(),
|
||||
targets: BTreeMap::new(),
|
||||
forwards: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
@@ -358,28 +434,50 @@ impl InterfaceForwardEntry {
|
||||
) -> Result<(), Error> {
|
||||
let mut keep = BTreeSet::<SocketAddrV4>::new();
|
||||
|
||||
for (iface, info) in ip_info.iter() {
|
||||
if let Some((target, target_prefix)) = self
|
||||
.filter
|
||||
.iter()
|
||||
.filter(|(_, (_, _, rc))| rc.strong_count() > 0)
|
||||
.find(|(filter, _)| filter.filter(iface, info))
|
||||
.map(|(_, (target, target_prefix, _))| (*target, *target_prefix))
|
||||
{
|
||||
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
|
||||
for (gw_id, info) in ip_info.iter() {
|
||||
if let Some(ip_info) = &info.ip_info {
|
||||
for subnet in ip_info.subnets.iter() {
|
||||
if let IpAddr::V4(ip) = subnet.addr() {
|
||||
let addr = SocketAddrV4::new(ip, self.external);
|
||||
if keep.contains(&addr) {
|
||||
continue;
|
||||
}
|
||||
}) {
|
||||
keep.insert(addr);
|
||||
if !self.forwards.contains_key(&addr) {
|
||||
let rc = port_forward
|
||||
.add_forward(addr, target, target_prefix)
|
||||
|
||||
for (reqs, (target, target_prefix, rc)) in self.targets.iter() {
|
||||
if rc.strong_count() == 0 {
|
||||
continue;
|
||||
}
|
||||
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?;
|
||||
self.forwards.insert(addr, rc);
|
||||
self.forwards.insert(addr, fwd_rc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,7 +496,7 @@ impl InterfaceForwardEntry {
|
||||
external,
|
||||
target,
|
||||
target_prefix,
|
||||
filter,
|
||||
reqs,
|
||||
mut rc,
|
||||
}: InterfaceForwardRequest,
|
||||
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
||||
@@ -412,8 +510,8 @@ impl InterfaceForwardEntry {
|
||||
}
|
||||
|
||||
let entry = self
|
||||
.filter
|
||||
.entry(filter)
|
||||
.targets
|
||||
.entry(reqs)
|
||||
.or_insert_with(|| (target, target_prefix, Arc::downgrade(&rc)));
|
||||
if entry.0 != target {
|
||||
entry.0 = target;
|
||||
@@ -436,7 +534,7 @@ impl InterfaceForwardEntry {
|
||||
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
||||
port_forward: &PortForwardController,
|
||||
) -> 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
|
||||
}
|
||||
@@ -495,7 +593,7 @@ pub struct ForwardTable(pub BTreeMap<u16, ForwardTarget>);
|
||||
pub struct ForwardTarget {
|
||||
pub target: SocketAddrV4,
|
||||
pub target_prefix: u8,
|
||||
pub filter: String,
|
||||
pub reqs: String,
|
||||
}
|
||||
|
||||
impl From<&InterfaceForwardState> for ForwardTable {
|
||||
@@ -506,16 +604,16 @@ impl From<&InterfaceForwardState> for ForwardTable {
|
||||
.iter()
|
||||
.flat_map(|entry| {
|
||||
entry
|
||||
.filter
|
||||
.targets
|
||||
.iter()
|
||||
.filter(|(_, (_, _, rc))| rc.strong_count() > 0)
|
||||
.map(|(filter, (target, target_prefix, _))| {
|
||||
.map(|(reqs, (target, target_prefix, _))| {
|
||||
(
|
||||
entry.external,
|
||||
ForwardTarget {
|
||||
target: *target,
|
||||
target_prefix: *target_prefix,
|
||||
filter: format!("{:#?}", filter),
|
||||
reqs: format!("{reqs}"),
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -534,16 +632,6 @@ enum InterfaceForwardCommand {
|
||||
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 {
|
||||
req: mpsc::UnboundedSender<InterfaceForwardCommand>,
|
||||
_thread: NonDetachingJoinHandle<()>,
|
||||
@@ -593,7 +681,7 @@ impl InterfacePortForwardController {
|
||||
pub async fn add(
|
||||
&self,
|
||||
external: u16,
|
||||
filter: DynInterfaceFilter,
|
||||
reqs: ForwardRequirements,
|
||||
target: SocketAddrV4,
|
||||
target_prefix: u8,
|
||||
) -> Result<Arc<()>, Error> {
|
||||
@@ -605,7 +693,7 @@ impl InterfacePortForwardController {
|
||||
external,
|
||||
target,
|
||||
target_prefix,
|
||||
filter,
|
||||
reqs,
|
||||
rc,
|
||||
},
|
||||
send,
|
||||
@@ -637,15 +725,21 @@ async fn forward(
|
||||
source: SocketAddrV4,
|
||||
target: SocketAddrV4,
|
||||
target_prefix: u8,
|
||||
src_filter: Option<&SourceFilter>,
|
||||
) -> Result<(), Error> {
|
||||
Command::new("/usr/lib/startos/scripts/forward-port")
|
||||
.env("sip", source.ip().to_string())
|
||||
let mut cmd = Command::new("/usr/lib/startos/scripts/forward-port");
|
||||
cmd.env("sip", source.ip().to_string())
|
||||
.env("dip", target.ip().to_string())
|
||||
.env("dprefix", target_prefix.to_string())
|
||||
.env("sport", source.port().to_string())
|
||||
.env("dport", target.port().to_string())
|
||||
.invoke(ErrorKind::Network)
|
||||
.await?;
|
||||
.env("dport", target.port().to_string());
|
||||
if let Some(filter) = src_filter {
|
||||
cmd.env("src_subnet", &filter.subnet);
|
||||
if !filter.excluded.is_empty() {
|
||||
cmd.env("excluded_src", &filter.excluded);
|
||||
}
|
||||
}
|
||||
cmd.invoke(ErrorKind::Network).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -653,15 +747,21 @@ async fn unforward(
|
||||
source: SocketAddrV4,
|
||||
target: SocketAddrV4,
|
||||
target_prefix: u8,
|
||||
src_filter: Option<&SourceFilter>,
|
||||
) -> Result<(), Error> {
|
||||
Command::new("/usr/lib/startos/scripts/forward-port")
|
||||
.env("UNDO", "1")
|
||||
let mut cmd = Command::new("/usr/lib/startos/scripts/forward-port");
|
||||
cmd.env("UNDO", "1")
|
||||
.env("sip", source.ip().to_string())
|
||||
.env("dip", target.ip().to_string())
|
||||
.env("dprefix", target_prefix.to_string())
|
||||
.env("sport", source.port().to_string())
|
||||
.env("dport", target.port().to_string())
|
||||
.invoke(ErrorKind::Network)
|
||||
.await?;
|
||||
.env("dport", target.port().to_string());
|
||||
if let Some(filter) = src_filter {
|
||||
cmd.env("src_subnet", &filter.subnet);
|
||||
if !filter.excluded.is_empty() {
|
||||
cmd.env("excluded_src", &filter.excluded);
|
||||
}
|
||||
}
|
||||
cmd.invoke(ErrorKind::Network).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use std::any::Any;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV6};
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::task::{Poll, ready};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use futures::future::Either;
|
||||
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
|
||||
use imbl::{OrdMap, OrdSet};
|
||||
use imbl_value::InternedString;
|
||||
@@ -36,15 +33,14 @@ use crate::db::model::Database;
|
||||
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
|
||||
use crate::net::forward::START9_BRIDGE_IFACE;
|
||||
use crate::net::gateway::device::DeviceProxy;
|
||||
use crate::net::utils::ipv6_is_link_local;
|
||||
use crate::net::web_server::{Accept, AcceptStream, Acceptor, MetadataVisitor};
|
||||
use crate::net::web_server::{Accept, AcceptStream, MetadataVisitor, TcpMetadata};
|
||||
use crate::prelude::*;
|
||||
use crate::util::Invoke;
|
||||
use crate::util::collections::OrdMapIterMut;
|
||||
use crate::util::future::{NonDetachingJoinHandle, Until};
|
||||
use crate::util::io::open_file;
|
||||
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> {
|
||||
ParentHandler::new()
|
||||
@@ -840,7 +836,6 @@ pub struct NetworkInterfaceWatcher {
|
||||
activated: Watch<BTreeMap<GatewayId, bool>>,
|
||||
ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
||||
_watcher: NonDetachingJoinHandle<()>,
|
||||
listeners: SyncMutex<BTreeMap<u16, Weak<()>>>,
|
||||
}
|
||||
impl NetworkInterfaceWatcher {
|
||||
pub fn new(
|
||||
@@ -860,7 +855,6 @@ impl NetworkInterfaceWatcher {
|
||||
watcher(ip_info, activated).await
|
||||
})
|
||||
.into(),
|
||||
listeners: SyncMutex::new(BTreeMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -887,51 +881,6 @@ impl NetworkInterfaceWatcher {
|
||||
pub fn ip_info(&self) -> OrdMap<GatewayId, NetworkInterfaceInfo> {
|
||||
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 {
|
||||
@@ -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(
|
||||
ip_info: &OrdMap<GatewayId, NetworkInterfaceInfo>,
|
||||
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)]
|
||||
pub struct GatewayInfo {
|
||||
pub id: GatewayId,
|
||||
@@ -1511,213 +1209,88 @@ impl<V: MetadataVisitor> Visit<V> for GatewayInfo {
|
||||
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> {
|
||||
pub ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
|
||||
listeners: ListenerMap<B>,
|
||||
_arc: Arc<()>,
|
||||
}
|
||||
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,
|
||||
/// Metadata for connections accepted by WildcardListener or VHostBindListener.
|
||||
#[derive(Clone, Debug, VisitFields)]
|
||||
pub struct NetworkInterfaceListenerAcceptMetadata {
|
||||
pub inner: TcpMetadata,
|
||||
pub info: GatewayInfo,
|
||||
}
|
||||
impl<B: Bind> fmt::Debug for NetworkInterfaceListenerAcceptMetadata<B> {
|
||||
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,
|
||||
{
|
||||
impl<V: MetadataVisitor> Visit<V> for NetworkInterfaceListenerAcceptMetadata {
|
||||
fn visit(&self, visitor: &mut V) -> V::Result {
|
||||
self.visit_fields(visitor).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Bind> Accept for NetworkInterfaceListener<B> {
|
||||
type Metadata = NetworkInterfaceListenerAcceptMetadata<B>;
|
||||
/// A simple TCP listener on 0.0.0.0:port that looks up GatewayInfo from the
|
||||
/// 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(
|
||||
&mut self,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
|
||||
NetworkInterfaceListener::poll_accept(self, cx, &true).map(|res| {
|
||||
res.map(|(info, inner, stream)| {
|
||||
(
|
||||
NetworkInterfaceListenerAcceptMetadata { inner, info },
|
||||
stream,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SelfContainedNetworkInterfaceListener<B: Bind = BindTcp> {
|
||||
_watch_thread: NonDetachingJoinHandle<()>,
|
||||
listener: NetworkInterfaceListener<B>,
|
||||
}
|
||||
impl<B: Bind> SelfContainedNetworkInterfaceListener<B> {
|
||||
pub fn bind(bind: B, port: u16) -> Self {
|
||||
let ip_info = Watch::new(OrdMap::new());
|
||||
let _watch_thread =
|
||||
tokio::spawn(watcher(ip_info.clone(), Watch::new(BTreeMap::new()))).into();
|
||||
Self {
|
||||
_watch_thread,
|
||||
listener: NetworkInterfaceListener::new(ip_info, bind, port),
|
||||
if let Poll::Ready((stream, peer_addr)) = TcpListener::poll_accept(&self.listener, cx)? {
|
||||
if let Err(e) = socket2::SockRef::from(&stream).set_keepalive(true) {
|
||||
tracing::error!("Failed to set tcp keepalive: {e}");
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
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(),
|
||||
info: info.clone(),
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| GatewayInfo {
|
||||
id: InternedString::from_static("").into(),
|
||||
info: NetworkInterfaceInfo::default(),
|
||||
});
|
||||
return Poll::Ready(Ok((
|
||||
NetworkInterfaceListenerAcceptMetadata {
|
||||
inner: TcpMetadata {
|
||||
local_addr,
|
||||
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::net::acme::AcmeProvider;
|
||||
use crate::net::host::{HostApiKind, all_hosts};
|
||||
use crate::net::tor::OnionAddress;
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(rename_all_fields = "camelCase")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum HostAddress {
|
||||
Onion {
|
||||
address: OnionAddress,
|
||||
},
|
||||
Domain {
|
||||
address: InternedString,
|
||||
public: Option<PublicDomainConfig>,
|
||||
private: bool,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HostAddress {
|
||||
pub address: InternedString,
|
||||
pub public: Option<PublicDomainConfig>,
|
||||
pub private: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||
@@ -38,18 +30,7 @@ pub struct PublicDomainConfig {
|
||||
}
|
||||
|
||||
fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
|
||||
let mut onions = BTreeSet::<OnionAddress>::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| {
|
||||
if domains.contains(&domain) {
|
||||
return Err(Error::new(
|
||||
@@ -68,9 +49,6 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
|
||||
not_in_use.push(host);
|
||||
continue;
|
||||
}
|
||||
for onion in host.as_onions().de()? {
|
||||
check_onion(&mut onions, onion)?;
|
||||
}
|
||||
let public = host.as_public_domains().keys()?;
|
||||
for domain in &public {
|
||||
check_domain(&mut domains, domain.clone())?;
|
||||
@@ -82,16 +60,11 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
for host in not_in_use {
|
||||
host.as_onions_mut()
|
||||
.mutate(|o| Ok(o.retain(|o| !onions.contains(o))))?;
|
||||
host.as_public_domains_mut()
|
||||
.mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?;
|
||||
host.as_private_domains_mut()
|
||||
.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()?;
|
||||
for domain in &public {
|
||||
check_domain(&mut domains, domain.clone())?;
|
||||
@@ -159,29 +132,6 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
|
||||
)
|
||||
.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(
|
||||
"list",
|
||||
from_fn_async(list_addresses::<Kind>)
|
||||
@@ -197,32 +147,18 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]);
|
||||
for address in &res {
|
||||
match address {
|
||||
HostAddress::Onion { address } => {
|
||||
table.add_row(row![address, true, "N/A"]);
|
||||
}
|
||||
HostAddress::Domain {
|
||||
address,
|
||||
public: Some(PublicDomainConfig { gateway, acme }),
|
||||
private,
|
||||
} => {
|
||||
table.add_row(row![
|
||||
address,
|
||||
&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"]);
|
||||
}
|
||||
for entry in &res {
|
||||
if let Some(PublicDomainConfig { gateway, acme }) = &entry.public {
|
||||
table.add_row(row![
|
||||
entry.address,
|
||||
&format!(
|
||||
"{} ({gateway})",
|
||||
if entry.private { "YES" } else { "ONLY" }
|
||||
),
|
||||
acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE")
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![entry.address, &format!("NO"), "N/A"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,55 +287,6 @@ pub async fn remove_private_domain<Kind: HostApiKind>(
|
||||
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>(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
|
||||
@@ -3,21 +3,20 @@ use std::str::FromStr;
|
||||
|
||||
use clap::Parser;
|
||||
use clap::builder::ValueParserFactory;
|
||||
use imbl::OrdSet;
|
||||
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::public::NetworkInterfaceInfo;
|
||||
use crate::db::prelude::Map;
|
||||
use crate::net::forward::AvailablePorts;
|
||||
use crate::net::gateway::InterfaceFilter;
|
||||
use crate::net::host::HostApiKind;
|
||||
use crate::net::service_interface::HostnameInfo;
|
||||
use crate::net::vhost::AlpnInfo;
|
||||
use crate::prelude::*;
|
||||
use crate::util::FromStrParser;
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
use crate::{GatewayId, HostId};
|
||||
use crate::HostId;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)]
|
||||
#[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")]
|
||||
#[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 enabled: bool,
|
||||
pub options: BindOptions,
|
||||
pub net: NetInfo,
|
||||
pub addresses: DerivedAddressInfo,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
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_ssl_port: Option<u16>,
|
||||
}
|
||||
@@ -71,25 +127,28 @@ impl BindInfo {
|
||||
pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result<Self, Error> {
|
||||
let mut assigned_port = None;
|
||||
let mut assigned_ssl_port = None;
|
||||
if options.add_ssl.is_some() {
|
||||
assigned_ssl_port = Some(available_ports.alloc()?);
|
||||
if let Some(ssl) = &options.add_ssl {
|
||||
assigned_ssl_port = available_ports
|
||||
.try_alloc(ssl.preferred_external_port, true)
|
||||
.or_else(|| Some(available_ports.alloc(true).ok()?));
|
||||
}
|
||||
if options
|
||||
.secure
|
||||
.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 {
|
||||
enabled: true,
|
||||
options,
|
||||
net: NetInfo {
|
||||
private_disabled: OrdSet::new(),
|
||||
public_enabled: OrdSet::new(),
|
||||
assigned_port,
|
||||
assigned_ssl_port,
|
||||
},
|
||||
addresses: DerivedAddressInfo::default(),
|
||||
})
|
||||
}
|
||||
pub fn update(
|
||||
@@ -97,7 +156,11 @@ impl BindInfo {
|
||||
available_ports: &mut AvailablePorts,
|
||||
options: BindOptions,
|
||||
) -> Result<Self, Error> {
|
||||
let Self { net: mut lan, .. } = self;
|
||||
let Self {
|
||||
net: mut lan,
|
||||
addresses,
|
||||
..
|
||||
} = self;
|
||||
if options
|
||||
.secure
|
||||
.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() {
|
||||
Some(port)
|
||||
} else if let Some(port) =
|
||||
available_ports.try_alloc(options.preferred_external_port, false)
|
||||
{
|
||||
Some(port)
|
||||
} else {
|
||||
Some(available_ports.alloc()?)
|
||||
Some(available_ports.alloc(false)?)
|
||||
};
|
||||
} else {
|
||||
if let Some(port) = lan.assigned_port.take() {
|
||||
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() {
|
||||
Some(port)
|
||||
} else if let Some(port) = available_ports.try_alloc(ssl.preferred_external_port, true)
|
||||
{
|
||||
Some(port)
|
||||
} else {
|
||||
Some(available_ports.alloc()?)
|
||||
Some(available_ports.alloc(true)?)
|
||||
};
|
||||
} else {
|
||||
if let Some(port) = lan.assigned_ssl_port.take() {
|
||||
@@ -128,22 +198,17 @@ impl BindInfo {
|
||||
enabled: true,
|
||||
options,
|
||||
net: lan,
|
||||
addresses: DerivedAddressInfo {
|
||||
private_disabled: addresses.private_disabled,
|
||||
public_enabled: addresses.public_enabled,
|
||||
possible: BTreeSet::new(),
|
||||
},
|
||||
})
|
||||
}
|
||||
pub fn disable(&mut self) {
|
||||
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)]
|
||||
#[ts(export)]
|
||||
@@ -188,7 +253,7 @@ pub fn binding<C: Context, Kind: HostApiKind>()
|
||||
|
||||
let mut table = Table::new();
|
||||
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![
|
||||
internal,
|
||||
info.enabled,
|
||||
@@ -213,12 +278,12 @@ pub fn binding<C: Context, Kind: HostApiKind>()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"set-gateway-enabled",
|
||||
from_fn_async(set_gateway_enabled::<Kind>)
|
||||
"set-address-enabled",
|
||||
from_fn_async(set_address_enabled::<Kind>)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_inherited(Kind::inheritance)
|
||||
.no_display()
|
||||
.with_about("about.set-gateway-enabled-for-binding")
|
||||
.with_about("about.set-address-enabled-for-binding")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
@@ -227,7 +292,7 @@ pub async fn list_bindings<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<BTreeMap<u16, BindInfo>, Error> {
|
||||
) -> Result<Bindings, Error> {
|
||||
Kind::host_for(&inheritance, &mut ctx.db.peek().await)?
|
||||
.as_bindings()
|
||||
.de()
|
||||
@@ -236,50 +301,44 @@ pub async fn list_bindings<Kind: HostApiKind>(
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct BindingGatewaySetEnabledParams {
|
||||
pub struct BindingSetAddressEnabledParams {
|
||||
#[arg(help = "help.arg.internal-port")]
|
||||
internal_port: u16,
|
||||
#[arg(help = "help.arg.gateway-id")]
|
||||
gateway: GatewayId,
|
||||
#[arg(long, help = "help.arg.address")]
|
||||
address: String,
|
||||
#[arg(long, help = "help.arg.binding-enabled")]
|
||||
enabled: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn set_gateway_enabled<Kind: HostApiKind>(
|
||||
pub async fn set_address_enabled<Kind: HostApiKind>(
|
||||
ctx: RpcContext,
|
||||
BindingGatewaySetEnabledParams {
|
||||
BindingSetAddressEnabledParams {
|
||||
internal_port,
|
||||
gateway,
|
||||
address,
|
||||
enabled,
|
||||
}: BindingGatewaySetEnabledParams,
|
||||
}: BindingSetAddressEnabledParams,
|
||||
inheritance: Kind::Inheritance,
|
||||
) -> Result<(), Error> {
|
||||
let enabled = enabled.unwrap_or(true);
|
||||
let gateway_public = ctx
|
||||
.net_controller
|
||||
.net_iface
|
||||
.watcher
|
||||
.ip_info()
|
||||
.get(&gateway)
|
||||
.or_not_found(&gateway)?
|
||||
.public();
|
||||
let address: HostnameInfo =
|
||||
serde_json::from_str(&address).with_kind(ErrorKind::Deserialization)?;
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
Kind::host_for(&inheritance, db)?
|
||||
.as_bindings_mut()
|
||||
.mutate(|b| {
|
||||
let net = &mut b.get_mut(&internal_port).or_not_found(internal_port)?.net;
|
||||
if gateway_public {
|
||||
let bind = b.get_mut(&internal_port).or_not_found(internal_port)?;
|
||||
if address.public {
|
||||
if enabled {
|
||||
net.public_enabled.insert(gateway);
|
||||
bind.addresses.public_enabled.insert(address.clone());
|
||||
} else {
|
||||
net.public_enabled.remove(&gateway);
|
||||
bind.addresses.public_enabled.remove(&address);
|
||||
}
|
||||
} else {
|
||||
if enabled {
|
||||
net.private_disabled.remove(&gateway);
|
||||
bind.addresses.private_disabled.remove(&address);
|
||||
} else {
|
||||
net.private_disabled.insert(gateway);
|
||||
bind.addresses.private_disabled.insert(address.clone());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -13,9 +13,7 @@ use crate::context::RpcContext;
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::net::forward::AvailablePorts;
|
||||
use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api};
|
||||
use crate::net::host::binding::{BindInfo, BindOptions, binding};
|
||||
use crate::net::service_interface::HostnameInfo;
|
||||
use crate::net::tor::OnionAddress;
|
||||
use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding};
|
||||
use crate::prelude::*;
|
||||
use crate::{HostId, PackageId};
|
||||
|
||||
@@ -27,13 +25,9 @@ pub mod binding;
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct Host {
|
||||
pub bindings: BTreeMap<u16, BindInfo>,
|
||||
#[ts(type = "string[]")]
|
||||
pub onions: BTreeSet<OnionAddress>,
|
||||
pub bindings: Bindings,
|
||||
pub public_domains: BTreeMap<InternedString, PublicDomainConfig>,
|
||||
pub private_domains: BTreeSet<InternedString>,
|
||||
/// COMPUTED: NetService::update
|
||||
pub hostname_info: BTreeMap<u16, Vec<HostnameInfo>>, // internal port -> Hostnames
|
||||
}
|
||||
|
||||
impl AsRef<Host> for Host {
|
||||
@@ -46,24 +40,18 @@ impl Host {
|
||||
Self::default()
|
||||
}
|
||||
pub fn addresses<'a>(&'a self) -> impl Iterator<Item = HostAddress> + 'a {
|
||||
self.onions
|
||||
self.public_domains
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|address| HostAddress::Onion { address })
|
||||
.chain(
|
||||
self.public_domains
|
||||
.iter()
|
||||
.map(|(address, config)| HostAddress::Domain {
|
||||
address: address.clone(),
|
||||
public: Some(config.clone()),
|
||||
private: self.private_domains.contains(address),
|
||||
}),
|
||||
)
|
||||
.map(|(address, config)| HostAddress {
|
||||
address: address.clone(),
|
||||
public: Some(config.clone()),
|
||||
private: self.private_domains.contains(address),
|
||||
})
|
||||
.chain(
|
||||
self.private_domains
|
||||
.iter()
|
||||
.filter(|a| !self.public_domains.contains_key(*a))
|
||||
.map(|address| HostAddress::Domain {
|
||||
.map(|address| HostAddress {
|
||||
address: address.clone(),
|
||||
public: None,
|
||||
private: true,
|
||||
@@ -112,22 +100,7 @@ pub fn host_for<'a>(
|
||||
.as_hosts_mut(),
|
||||
)
|
||||
}
|
||||
let tor_key = if host_info(db, package_id)?.as_idx(host_id).is_none() {
|
||||
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)
|
||||
})
|
||||
host_info(db, package_id)?.upsert(host_id, || Ok(Host::new()))
|
||||
}
|
||||
|
||||
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::net::acme::AcmeCertStore;
|
||||
use crate::net::ssl::CertStore;
|
||||
use crate::net::tor::OnionStore;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[model = "Model<Self>"]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KeyStore {
|
||||
pub onion: OnionStore,
|
||||
pub local_certs: CertStore,
|
||||
#[serde(default)]
|
||||
pub acme: AcmeCertStore,
|
||||
}
|
||||
impl KeyStore {
|
||||
pub fn new(account: &AccountInfo) -> Result<Self, Error> {
|
||||
let mut res = Self {
|
||||
onion: OnionStore::new(),
|
||||
Ok(Self {
|
||||
local_certs: CertStore::new(account)?,
|
||||
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 static_server;
|
||||
pub mod tls;
|
||||
pub mod tor;
|
||||
pub mod tunnel;
|
||||
pub mod utils;
|
||||
pub mod vhost;
|
||||
@@ -23,7 +22,6 @@ pub mod wifi;
|
||||
|
||||
pub fn net_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand("tor", tor::tor_api::<C>().with_about("about.tor-commands"))
|
||||
.subcommand(
|
||||
"acme",
|
||||
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 color_eyre::eyre::eyre;
|
||||
use imbl::{OrdMap, vector};
|
||||
use imbl::vector;
|
||||
use imbl_value::InternedString;
|
||||
use ipnet::IpNet;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -16,17 +16,15 @@ use crate::db::model::public::NetworkInterfaceType;
|
||||
use crate::error::ErrorCollection;
|
||||
use crate::hostname::Hostname;
|
||||
use crate::net::dns::DnsController;
|
||||
use crate::net::forward::{InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule};
|
||||
use crate::net::gateway::{
|
||||
AndFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, NetworkInterfaceController, OrFilter,
|
||||
PublicFilter, SecureFilter, TypeFilter,
|
||||
use crate::net::forward::{
|
||||
ForwardRequirements, InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule,
|
||||
};
|
||||
use crate::net::gateway::NetworkInterfaceController;
|
||||
use crate::net::host::address::HostAddress;
|
||||
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
|
||||
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::tor::{OnionAddress, TorController, TorSecretKey};
|
||||
use crate::net::utils::ipv6_is_local;
|
||||
use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController};
|
||||
use crate::prelude::*;
|
||||
@@ -36,7 +34,6 @@ use crate::{GatewayId, HOST_IP, HostId, OptionExt, PackageId};
|
||||
|
||||
pub struct NetController {
|
||||
pub(crate) db: TypedPatchDb<Database>,
|
||||
pub(super) tor: TorController,
|
||||
pub(super) vhost: VHostController,
|
||||
pub(super) tls_client_config: Arc<TlsClientConfig>,
|
||||
pub(crate) net_iface: Arc<NetworkInterfaceController>,
|
||||
@@ -54,8 +51,7 @@ impl NetController {
|
||||
socks_listen: SocketAddr,
|
||||
) -> Result<Self, Error> {
|
||||
let net_iface = Arc::new(NetworkInterfaceController::new(db.clone()));
|
||||
let tor = TorController::new()?;
|
||||
let socks = SocksController::new(socks_listen, tor.clone())?;
|
||||
let socks = SocksController::new(socks_listen)?;
|
||||
let crypto_provider = Arc::new(tokio_rustls::rustls::crypto::ring::default_provider());
|
||||
let tls_client_config = Arc::new(crate::net::tls::client_config(
|
||||
crypto_provider.clone(),
|
||||
@@ -87,7 +83,6 @@ impl NetController {
|
||||
.await?;
|
||||
Ok(Self {
|
||||
db: db.clone(),
|
||||
tor,
|
||||
vhost: VHostController::new(db.clone(), net_iface.clone(), crypto_provider),
|
||||
tls_client_config,
|
||||
dns: DnsController::init(db, &net_iface.watcher).await?,
|
||||
@@ -165,10 +160,9 @@ impl NetController {
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct HostBinds {
|
||||
forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter, Arc<()>)>,
|
||||
forwards: BTreeMap<u16, (SocketAddrV4, ForwardRequirements, Arc<()>)>,
|
||||
vhosts: BTreeMap<(Option<InternedString>, u16), (ProxyTarget, Arc<()>)>,
|
||||
private_dns: BTreeMap<InternedString, Arc<()>>,
|
||||
tor: BTreeMap<OnionAddress, (OrdMap<u16, SocketAddr>, Vec<Arc<()>>)>,
|
||||
}
|
||||
|
||||
pub struct NetServiceData {
|
||||
@@ -207,7 +201,7 @@ impl NetServiceData {
|
||||
.as_entries_mut()?
|
||||
{
|
||||
host.as_bindings_mut().mutate(|b| {
|
||||
for (internal_port, info) in b {
|
||||
for (internal_port, info) in b.iter_mut() {
|
||||
if !except.contains(&BindId {
|
||||
id: host_id.clone(),
|
||||
internal_port: *internal_port,
|
||||
@@ -238,7 +232,7 @@ impl NetServiceData {
|
||||
.as_network_mut()
|
||||
.as_host_mut();
|
||||
host.as_bindings_mut().mutate(|b| {
|
||||
for (internal_port, info) in b {
|
||||
for (internal_port, info) in b.iter_mut() {
|
||||
if !except.contains(&BindId {
|
||||
id: HostId::default(),
|
||||
internal_port: *internal_port,
|
||||
@@ -256,425 +250,295 @@ impl NetServiceData {
|
||||
}
|
||||
}
|
||||
|
||||
async fn update(&mut self, ctrl: &NetController, id: HostId, host: Host) -> Result<(), Error> {
|
||||
let mut forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter)> = BTreeMap::new();
|
||||
async fn update(
|
||||
&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 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 peek = ctrl.db.peek().await;
|
||||
|
||||
// LAN
|
||||
let server_info = peek.as_public().as_server_info();
|
||||
let net_ifaces = ctrl.net_iface.watcher.ip_info();
|
||||
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 {
|
||||
continue;
|
||||
}
|
||||
if bind.net.assigned_port.is_some() || bind.net.assigned_ssl_port.is_some() {
|
||||
let mut hostnames = BTreeSet::new();
|
||||
if let Some(ssl) = &bind.options.add_ssl {
|
||||
let external = bind
|
||||
.net
|
||||
.assigned_ssl_port
|
||||
.or_not_found("assigned ssl port")?;
|
||||
let addr = (self.ip, *port).into();
|
||||
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)
|
||||
}
|
||||
};
|
||||
for hostname in ctrl.server_hostnames.iter().cloned() {
|
||||
vhosts.insert(
|
||||
(hostname, external),
|
||||
ProxyTarget {
|
||||
filter: bind.net.clone().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()),
|
||||
},
|
||||
);
|
||||
}
|
||||
for address in host.addresses() {
|
||||
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))
|
||||
})
|
||||
if bind.net.assigned_port.is_none() && bind.net.assigned_ssl_port.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
bind.addresses.possible.clear();
|
||||
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(|(_, info)| info.ip_info.is_some())
|
||||
{
|
||||
let gateway = GatewayInfo {
|
||||
id: gateway_id.clone(),
|
||||
name: info
|
||||
.name
|
||||
.clone()
|
||||
.or_else(|| info.ip_info.as_ref().map(|i| i.name.clone()))
|
||||
.unwrap_or_else(|| gateway_id.clone().into()),
|
||||
public: info.public(),
|
||||
};
|
||||
let port = bind.net.assigned_port.filter(|_| {
|
||||
bind.options.secure.map_or(false, |s| {
|
||||
!(s.ssl && bind.options.add_ssl.is_some()) || info.secure()
|
||||
})
|
||||
});
|
||||
// .local addresses (private only, non-public, non-wireguard gateways)
|
||||
if !info.public()
|
||||
&& info.ip_info.as_ref().map_or(false, |i| {
|
||||
i.device_type != Some(NetworkInterfaceType::Wireguard)
|
||||
})
|
||||
.filter(|(id, info)| bind.net.filter(id, info))
|
||||
{
|
||||
let gateway = GatewayInfo {
|
||||
id: gateway_id.clone(),
|
||||
name: info
|
||||
.name
|
||||
.clone()
|
||||
.or_else(|| info.ip_info.as_ref().map(|i| i.name.clone()))
|
||||
.unwrap_or_else(|| gateway_id.clone().into()),
|
||||
public: info.public(),
|
||||
};
|
||||
let port = bind.net.assigned_port.filter(|_| {
|
||||
bind.options.secure.map_or(false, |s| {
|
||||
!(s.ssl && bind.options.add_ssl.is_some()) || info.secure()
|
||||
})
|
||||
bind.addresses.possible.insert(HostnameInfo {
|
||||
gateway: gateway.clone(),
|
||||
public: false,
|
||||
hostname: IpHostname::Local {
|
||||
value: InternedString::from_display(&{
|
||||
let hostname = &hostname;
|
||||
lazy_format!("{hostname}.local")
|
||||
}),
|
||||
port,
|
||||
ssl_port: bind.net.assigned_ssl_port,
|
||||
},
|
||||
});
|
||||
if !info.public()
|
||||
&& info.ip_info.as_ref().map_or(false, |i| {
|
||||
i.device_type != Some(NetworkInterfaceType::Wireguard)
|
||||
})
|
||||
{
|
||||
bind_hostname_info.push(HostnameInfo::Ip {
|
||||
}
|
||||
// Domain addresses
|
||||
for HostAddress {
|
||||
address,
|
||||
public,
|
||||
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(),
|
||||
public: false,
|
||||
hostname: IpHostname::Local {
|
||||
value: InternedString::from_display(&{
|
||||
let hostname = &hostname;
|
||||
lazy_format!("{hostname}.local")
|
||||
}),
|
||||
public,
|
||||
hostname: IpHostname::Domain {
|
||||
value: address.clone(),
|
||||
port: domain_port,
|
||||
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,
|
||||
ssl_port: bind.net.assigned_ssl_port,
|
||||
},
|
||||
});
|
||||
}
|
||||
for address in host.addresses() {
|
||||
if let HostAddress::Domain {
|
||||
address,
|
||||
public,
|
||||
private,
|
||||
} = 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 {
|
||||
for ipnet in &ip_info.subnets {
|
||||
match ipnet {
|
||||
IpNet::V4(net) => {
|
||||
if !public {
|
||||
bind.addresses.possible.insert(HostnameInfo {
|
||||
gateway: gateway.clone(),
|
||||
public,
|
||||
hostname: IpHostname::Domain {
|
||||
value: address.clone(),
|
||||
port: None,
|
||||
ssl_port: Some(443),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
bind_hostname_info.push(HostnameInfo::Ip {
|
||||
gateway: gateway.clone(),
|
||||
public,
|
||||
hostname: IpHostname::Domain {
|
||||
value: address.clone(),
|
||||
hostname: IpHostname::Ipv4 {
|
||||
value: net.addr(),
|
||||
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(),
|
||||
public: true,
|
||||
hostname: IpHostname::Ipv4 {
|
||||
value: wan_ip,
|
||||
port,
|
||||
ssl_port: bind.net.assigned_ssl_port,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 2: Build controller entries from enabled addresses ──
|
||||
for (port, bind) in host.bindings.iter() {
|
||||
if !bind.enabled {
|
||||
continue;
|
||||
}
|
||||
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 {
|
||||
bind_hostname_info.push(HostnameInfo::Ip {
|
||||
gateway: gateway.clone(),
|
||||
public,
|
||||
hostname: IpHostname::Ipv4 {
|
||||
value: net.addr(),
|
||||
port,
|
||||
ssl_port: bind.net.assigned_ssl_port,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Domain vhosts: group by (domain, ssl_port), merge public/private sets
|
||||
for addr_info in &enabled_addresses {
|
||||
if let IpHostname::Domain {
|
||||
value: domain,
|
||||
ssl_port: Some(domain_ssl_port),
|
||||
..
|
||||
} = &addr_info.hostname
|
||||
{
|
||||
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: Option<u16>,
|
||||
ssl: Option<u16>,
|
||||
}
|
||||
let mut tor_hostname_ports = BTreeMap::<u16, TorHostnamePorts>::new();
|
||||
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)
|
||||
// Non-SSL forwards
|
||||
if bind
|
||||
.options
|
||||
.secure
|
||||
.map_or(true, |s| !(s.ssl && bind.options.add_ssl.is_some()))
|
||||
{
|
||||
tor_binds.insert(
|
||||
ssl.preferred_external_port,
|
||||
SocketAddr::from(([127, 0, 0, 1], ssl_internal)),
|
||||
);
|
||||
tor_hostname_ports.insert(
|
||||
*internal,
|
||||
TorHostnamePorts {
|
||||
non_ssl: Some(info.options.preferred_external_port)
|
||||
.filter(|p| *p != ssl.preferred_external_port),
|
||||
ssl: Some(ssl.preferred_external_port),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
tor_hostname_ports.insert(
|
||||
*internal,
|
||||
TorHostnamePorts {
|
||||
non_ssl: Some(info.options.preferred_external_port),
|
||||
ssl: None,
|
||||
},
|
||||
let external = bind.net.assigned_port.or_not_found("assigned lan port")?;
|
||||
let fwd_public: BTreeSet<GatewayId> = enabled_addresses
|
||||
.iter()
|
||||
.filter(|a| a.public)
|
||||
.map(|a| a.gateway.id.clone())
|
||||
.collect();
|
||||
let fwd_private: BTreeSet<IpAddr> = enabled_addresses
|
||||
.iter()
|
||||
.filter(|a| !a.public)
|
||||
.filter_map(|a| {
|
||||
net_ifaces
|
||||
.get(&a.gateway.id)
|
||||
.and_then(|i| i.ip_info.as_ref())
|
||||
})
|
||||
.flat_map(|ip| ip.subnets.iter().map(|s| s.addr()))
|
||||
.collect();
|
||||
forwards.insert(
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 3: Reconcile ──
|
||||
let all = binds
|
||||
.forwards
|
||||
.keys()
|
||||
@@ -683,8 +547,8 @@ impl NetServiceData {
|
||||
.collect::<BTreeSet<_>>();
|
||||
for external in all {
|
||||
let mut prev = binds.forwards.remove(&external);
|
||||
if let Some((internal, filter)) = forwards.remove(&external) {
|
||||
prev = prev.filter(|(i, f, _)| i == &internal && *f == filter);
|
||||
if let Some((internal, reqs)) = forwards.remove(&external) {
|
||||
prev = prev.filter(|(i, r, _)| i == &internal && *r == reqs);
|
||||
binds.forwards.insert(
|
||||
external,
|
||||
if let Some(prev) = prev {
|
||||
@@ -692,11 +556,11 @@ impl NetServiceData {
|
||||
} else {
|
||||
(
|
||||
internal,
|
||||
filter.clone(),
|
||||
reqs.clone(),
|
||||
ctrl.forward
|
||||
.add(
|
||||
external,
|
||||
filter,
|
||||
reqs,
|
||||
internal,
|
||||
net_ifaces
|
||||
.iter()
|
||||
@@ -763,40 +627,18 @@ impl NetServiceData {
|
||||
}
|
||||
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
|
||||
.db
|
||||
.mutate(|db| {
|
||||
host_for(db, self.id.as_ref(), &id)?
|
||||
.as_hostname_info_mut()
|
||||
.ser(&hostname_info)
|
||||
let bindings = host_for(db, self.id.as_ref(), &id)?.as_bindings_mut();
|
||||
for (port, bind) in host.bindings.0 {
|
||||
if let Some(b) = bindings.as_idx_mut(&port) {
|
||||
b.as_addresses_mut()
|
||||
.as_possible_mut()
|
||||
.ser(&bind.addresses.possible)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
res.result?;
|
||||
|
||||
@@ -6,31 +6,21 @@ use ts_rs::TS;
|
||||
|
||||
use crate::{GatewayId, HostId, ServiceInterfaceId};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename_all_fields = "camelCase")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum HostnameInfo {
|
||||
Ip {
|
||||
gateway: GatewayInfo,
|
||||
public: bool,
|
||||
hostname: IpHostname,
|
||||
},
|
||||
Onion {
|
||||
hostname: OnionHostname,
|
||||
},
|
||||
pub struct HostnameInfo {
|
||||
pub gateway: GatewayInfo,
|
||||
pub public: bool,
|
||||
pub hostname: IpHostname,
|
||||
}
|
||||
impl HostnameInfo {
|
||||
pub fn to_san_hostname(&self) -> InternedString {
|
||||
match self {
|
||||
Self::Ip { hostname, .. } => hostname.to_san_hostname(),
|
||||
Self::Onion { hostname } => hostname.to_san_hostname(),
|
||||
}
|
||||
self.hostname.to_san_hostname()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GatewayInfo {
|
||||
@@ -39,22 +29,7 @@ pub struct GatewayInfo {
|
||||
pub public: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, 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)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename_all_fields = "camelCase")]
|
||||
|
||||
@@ -8,7 +8,6 @@ use socks5_impl::server::{AuthAdaptor, ClientConnection, Server};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
use crate::HOST_IP;
|
||||
use crate::net::tor::TorController;
|
||||
use crate::prelude::*;
|
||||
use crate::util::actor::background::BackgroundJobQueue;
|
||||
use crate::util::future::NonDetachingJoinHandle;
|
||||
@@ -22,7 +21,7 @@ pub struct SocksController {
|
||||
_thread: NonDetachingJoinHandle<()>,
|
||||
}
|
||||
impl SocksController {
|
||||
pub fn new(listen: SocketAddr, tor: TorController) -> Result<Self, Error> {
|
||||
pub fn new(listen: SocketAddr) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
_thread: tokio::spawn(async move {
|
||||
let auth: AuthAdaptor<()> = Arc::new(NoAuth);
|
||||
@@ -45,7 +44,6 @@ impl SocksController {
|
||||
loop {
|
||||
match server.accept().await {
|
||||
Ok((stream, _)) => {
|
||||
let tor = tor.clone();
|
||||
bg.add_job(async move {
|
||||
if let Err(e) = async {
|
||||
match stream
|
||||
@@ -57,40 +55,6 @@ impl SocksController {
|
||||
.await
|
||||
.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) => {
|
||||
if let Ok(mut target) = match addr {
|
||||
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?;
|
||||
host.as_bindings_mut().mutate(|b| {
|
||||
Ok(b.values_mut().for_each(|v| {
|
||||
v.net.private_disabled.remove(&id);
|
||||
v.net.public_enabled.remove(&id);
|
||||
v.addresses
|
||||
.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::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::net::{IpAddr, SocketAddr, SocketAddrV6};
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::task::{Poll, ready};
|
||||
use std::time::Duration;
|
||||
|
||||
use async_acme::acme::ACME_TLS_ALPN_NAME;
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::FutureExt;
|
||||
use futures::future::BoxFuture;
|
||||
use imbl::OrdMap;
|
||||
use imbl_value::{InOMap, InternedString};
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio_rustls::TlsConnector;
|
||||
use tokio_rustls::rustls::crypto::CryptoProvider;
|
||||
use tokio_rustls::rustls::pki_types::ServerName;
|
||||
@@ -23,28 +23,28 @@ use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
use visit_rs::Visit;
|
||||
|
||||
use crate::ResultExt;
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
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::net::acme::{
|
||||
AcmeCertStore, AcmeProvider, AcmeTlsAlpnCache, AcmeTlsHandler, GetAcmeProvider,
|
||||
};
|
||||
use crate::net::gateway::{
|
||||
AnyFilter, BindTcp, DynInterfaceFilter, GatewayInfo, InterfaceFilter,
|
||||
NetworkInterfaceController, NetworkInterfaceListener,
|
||||
GatewayInfo, NetworkInterfaceController, NetworkInterfaceListenerAcceptMetadata,
|
||||
};
|
||||
use crate::net::ssl::{CertStore, RootCaTlsHandler};
|
||||
use crate::net::tls::{
|
||||
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::prelude::*;
|
||||
use crate::util::collections::EqSet;
|
||||
use crate::util::future::{NonDetachingJoinHandle, WeakFuture};
|
||||
use crate::util::serde::{HandlerExtSerde, MaybeUtf8String, display_serializable};
|
||||
use crate::util::sync::{SyncMutex, Watch};
|
||||
use crate::{GatewayId, ResultExt};
|
||||
|
||||
pub fn vhost_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new().subcommand(
|
||||
@@ -93,7 +93,7 @@ pub struct VHostController {
|
||||
interfaces: Arc<NetworkInterfaceController>,
|
||||
crypto_provider: Arc<CryptoProvider>,
|
||||
acme_cache: AcmeTlsAlpnCache,
|
||||
servers: SyncMutex<BTreeMap<u16, VHostServer<NetworkInterfaceListener>>>,
|
||||
servers: SyncMutex<BTreeMap<u16, VHostServer<VHostBindListener>>>,
|
||||
}
|
||||
impl VHostController {
|
||||
pub fn new(
|
||||
@@ -114,14 +114,22 @@ impl VHostController {
|
||||
&self,
|
||||
hostname: Option<InternedString>,
|
||||
external: u16,
|
||||
target: DynVHostTarget<NetworkInterfaceListener>,
|
||||
target: DynVHostTarget<VHostBindListener>,
|
||||
) -> Result<Arc<()>, Error> {
|
||||
self.servers.mutate(|writable| {
|
||||
let server = if let Some(server) = writable.remove(&external) {
|
||||
server
|
||||
} 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(
|
||||
self.interfaces.watcher.bind(BindTcp, external)?,
|
||||
listener,
|
||||
bind_reqs,
|
||||
self.db.clone(),
|
||||
self.crypto_provider.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 {
|
||||
type PreprocessRes: Send + 'static;
|
||||
#[allow(unused_variables)]
|
||||
@@ -182,6 +327,10 @@ pub trait VHostTarget<A: Accept>: std::fmt::Debug + Eq {
|
||||
fn acme(&self) -> Option<&AcmeProvider> {
|
||||
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>(
|
||||
&'a self,
|
||||
prev: ServerConfig,
|
||||
@@ -200,6 +349,7 @@ pub trait VHostTarget<A: Accept>: std::fmt::Debug + Eq {
|
||||
pub trait DynVHostTargetT<A: Accept>: std::fmt::Debug + Any {
|
||||
fn filter(&self, metadata: &<A as Accept>::Metadata) -> bool;
|
||||
fn acme(&self) -> Option<&AcmeProvider>;
|
||||
fn bind_requirements(&self) -> (BTreeSet<GatewayId>, BTreeSet<IpAddr>);
|
||||
fn preprocess<'a>(
|
||||
&'a self,
|
||||
prev: ServerConfig,
|
||||
@@ -224,6 +374,9 @@ impl<A: Accept, T: VHostTarget<A> + 'static> DynVHostTargetT<A> for T {
|
||||
fn acme(&self) -> Option<&AcmeProvider> {
|
||||
VHostTarget::acme(self)
|
||||
}
|
||||
fn bind_requirements(&self) -> (BTreeSet<GatewayId>, BTreeSet<IpAddr>) {
|
||||
VHostTarget::bind_requirements(self)
|
||||
}
|
||||
fn preprocess<'a>(
|
||||
&'a self,
|
||||
prev: ServerConfig,
|
||||
@@ -301,7 +454,8 @@ impl<A: Accept + 'static> Preprocessed<A> {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProxyTarget {
|
||||
pub filter: DynInterfaceFilter,
|
||||
pub public: BTreeSet<GatewayId>,
|
||||
pub private: BTreeSet<IpAddr>,
|
||||
pub acme: Option<AcmeProvider>,
|
||||
pub addr: SocketAddr,
|
||||
pub add_x_forwarded_headers: bool,
|
||||
@@ -309,7 +463,8 @@ pub struct ProxyTarget {
|
||||
}
|
||||
impl PartialEq for ProxyTarget {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.filter == other.filter
|
||||
self.public == other.public
|
||||
&& self.private == other.private
|
||||
&& self.acme == other.acme
|
||||
&& self.addr == other.addr
|
||||
&& self.connect_ssl.as_ref().map(Arc::as_ptr)
|
||||
@@ -320,7 +475,8 @@ impl Eq for ProxyTarget {}
|
||||
impl fmt::Debug for ProxyTarget {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ProxyTarget")
|
||||
.field("filter", &self.filter)
|
||||
.field("public", &self.public)
|
||||
.field("private", &self.private)
|
||||
.field("acme", &self.acme)
|
||||
.field("addr", &self.addr)
|
||||
.field("add_x_forwarded_headers", &self.add_x_forwarded_headers)
|
||||
@@ -340,16 +496,37 @@ where
|
||||
{
|
||||
type PreprocessRes = AcceptStream;
|
||||
fn filter(&self, metadata: &<A as Accept>::Metadata) -> bool {
|
||||
let info = extract::<GatewayInfo, _>(metadata);
|
||||
if info.is_none() {
|
||||
tracing::warn!("No GatewayInfo on metadata");
|
||||
let gw = extract::<GatewayInfo, _>(metadata);
|
||||
let tcp = extract::<TcpMetadata, _>(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> {
|
||||
self.acme.as_ref()
|
||||
}
|
||||
fn bind_requirements(&self) -> (BTreeSet<GatewayId>, BTreeSet<IpAddr>) {
|
||||
(self.public.clone(), self.private.clone())
|
||||
}
|
||||
async fn preprocess<'a>(
|
||||
&'a self,
|
||||
mut prev: ServerConfig,
|
||||
@@ -634,28 +811,15 @@ where
|
||||
|
||||
struct VHostServer<A: Accept + 'static> {
|
||||
mapping: Watch<Mapping<A>>,
|
||||
bind_reqs: Watch<VHostBindRequirements>,
|
||||
_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> {
|
||||
#[instrument(skip_all)]
|
||||
fn new<M: HasModel>(
|
||||
listener: A,
|
||||
bind_reqs: Watch<VHostBindRequirements>,
|
||||
db: TypedPatchDb<M>,
|
||||
crypto_provider: Arc<CryptoProvider>,
|
||||
acme_cache: AcmeTlsAlpnCache,
|
||||
@@ -679,6 +843,7 @@ impl<A: Accept> VHostServer<A> {
|
||||
let mapping = Watch::new(BTreeMap::new());
|
||||
Self {
|
||||
mapping: mapping.clone(),
|
||||
bind_reqs,
|
||||
_thread: tokio::spawn(async move {
|
||||
let mut listener = VHostListener(TlsListener::new(
|
||||
listener,
|
||||
@@ -729,6 +894,9 @@ impl<A: Accept> VHostServer<A> {
|
||||
targets.insert(target, Arc::downgrade(&rc));
|
||||
writable.insert(hostname, targets);
|
||||
res = Ok(rc);
|
||||
if changed {
|
||||
self.update_bind_reqs(writable);
|
||||
}
|
||||
changed
|
||||
});
|
||||
if self.mapping.watcher_count() > 1 {
|
||||
@@ -752,9 +920,23 @@ impl<A: Accept> VHostServer<A> {
|
||||
if !targets.is_empty() {
|
||||
writable.insert(hostname, targets);
|
||||
}
|
||||
if pre != post {
|
||||
self.update_bind_reqs(writable);
|
||||
}
|
||||
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 {
|
||||
self.mapping.peek(|m| m.is_empty())
|
||||
}
|
||||
|
||||
@@ -366,28 +366,6 @@ where
|
||||
pub struct WebServerAcceptorSetter<A: Accept> {
|
||||
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> {
|
||||
type Target = Watch<A>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
||||
@@ -55,20 +55,18 @@ pub async fn get_ssl_certificate(
|
||||
.map(|(_, m)| m.as_hosts().as_entries())
|
||||
.flatten_ok()
|
||||
.map_ok(|(_, m)| {
|
||||
Ok(m.as_onions()
|
||||
.de()?
|
||||
.iter()
|
||||
.map(InternedString::from_display)
|
||||
.chain(m.as_public_domains().keys()?)
|
||||
Ok(m.as_public_domains()
|
||||
.keys()?
|
||||
.into_iter()
|
||||
.chain(m.as_private_domains().de()?)
|
||||
.chain(
|
||||
m.as_hostname_info()
|
||||
m.as_bindings()
|
||||
.de()?
|
||||
.values()
|
||||
.flatten()
|
||||
.flat_map(|b| b.addresses.possible.iter().cloned())
|
||||
.map(|h| h.to_san_hostname()),
|
||||
)
|
||||
.collect::<Vec<_>>())
|
||||
.collect::<Vec<InternedString>>())
|
||||
})
|
||||
.map(|a| a.and_then(|a| a))
|
||||
.flatten_ok()
|
||||
@@ -181,20 +179,18 @@ pub async fn get_ssl_key(
|
||||
.map(|m| m.as_hosts().as_entries())
|
||||
.flatten_ok()
|
||||
.map_ok(|(_, m)| {
|
||||
Ok(m.as_onions()
|
||||
.de()?
|
||||
.iter()
|
||||
.map(InternedString::from_display)
|
||||
.chain(m.as_public_domains().keys()?)
|
||||
Ok(m.as_public_domains()
|
||||
.keys()?
|
||||
.into_iter()
|
||||
.chain(m.as_private_domains().de()?)
|
||||
.chain(
|
||||
m.as_hostname_info()
|
||||
m.as_bindings()
|
||||
.de()?
|
||||
.values()
|
||||
.flatten()
|
||||
.flat_map(|b| b.addresses.possible.iter().cloned())
|
||||
.map(|h| h.to_san_hostname()),
|
||||
)
|
||||
.collect::<Vec<_>>())
|
||||
.collect::<Vec<InternedString>>())
|
||||
})
|
||||
.map(|a| a.and_then(|a| a))
|
||||
.flatten_ok()
|
||||
|
||||
@@ -459,7 +459,7 @@ pub async fn add_forward(
|
||||
})
|
||||
.map(|s| s.prefix_len())
|
||||
.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| {
|
||||
m.insert(source, rc);
|
||||
});
|
||||
|
||||
@@ -199,7 +199,7 @@ impl TunnelContext {
|
||||
})
|
||||
.map(|s| s.prefix_len())
|
||||
.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 {
|
||||
|
||||
@@ -59,8 +59,9 @@ mod v0_4_0_alpha_16;
|
||||
mod v0_4_0_alpha_17;
|
||||
mod v0_4_0_alpha_18;
|
||||
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 {
|
||||
#[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_17(Wrapper<v0_4_0_alpha_17::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),
|
||||
}
|
||||
|
||||
@@ -243,7 +245,8 @@ impl Version {
|
||||
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_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) => {
|
||||
return Err(Error::new(
|
||||
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_17(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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -23,17 +23,14 @@ use crate::disk::mount::filesystem::cifs::Cifs;
|
||||
use crate::disk::mount::util::unmount;
|
||||
use crate::hostname::Hostname;
|
||||
use crate::net::forward::AvailablePorts;
|
||||
use crate::net::host::Host;
|
||||
use crate::net::keys::KeyStore;
|
||||
use crate::net::tor::{OnionAddress, TorSecretKey};
|
||||
use crate::notifications::Notifications;
|
||||
use crate::prelude::*;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::ssh::{SshKeys, SshPubKey};
|
||||
use crate::util::Invoke;
|
||||
use crate::util::crypto::ed25519_expand_key;
|
||||
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! {
|
||||
static ref V0_3_6_alpha_0: exver::Version = exver::Version::new(
|
||||
@@ -146,12 +143,7 @@ pub struct Version;
|
||||
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_3_5_2::Version;
|
||||
type PreUpRes = (
|
||||
AccountInfo,
|
||||
SshKeys,
|
||||
CifsTargets,
|
||||
BTreeMap<PackageId, BTreeMap<HostId, TorSecretKey>>,
|
||||
);
|
||||
type PreUpRes = (AccountInfo, SshKeys, CifsTargets);
|
||||
fn semver(self) -> exver::Version {
|
||||
V0_3_6_alpha_0.clone()
|
||||
}
|
||||
@@ -166,20 +158,18 @@ impl VersionT for Version {
|
||||
|
||||
let cifs = previous_cifs(&pg).await?;
|
||||
|
||||
let tor_keys = previous_tor_keys(&pg).await?;
|
||||
|
||||
Command::new("systemctl")
|
||||
.arg("stop")
|
||||
.arg("postgresql@*.service")
|
||||
.invoke(crate::ErrorKind::Database)
|
||||
.await?;
|
||||
|
||||
Ok((account, ssh_keys, cifs, tor_keys))
|
||||
Ok((account, ssh_keys, cifs))
|
||||
}
|
||||
fn up(
|
||||
self,
|
||||
db: &mut Value,
|
||||
(account, ssh_keys, cifs, tor_keys): Self::PreUpRes,
|
||||
(account, ssh_keys, cifs): Self::PreUpRes,
|
||||
) -> Result<Value, Error> {
|
||||
let prev_package_data = db["package-data"].clone();
|
||||
|
||||
@@ -242,11 +232,7 @@ impl VersionT for Version {
|
||||
"ui": db["ui"],
|
||||
});
|
||||
|
||||
let mut 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 keystore = KeyStore::new(&account)?;
|
||||
|
||||
let private = {
|
||||
let mut value = json!({});
|
||||
@@ -350,20 +336,6 @@ impl VersionT for Version {
|
||||
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 {
|
||||
let package_s9pk = tokio::fs::File::open(path).await?;
|
||||
let file = MultiCursorFile::open(&package_s9pk).await?;
|
||||
@@ -381,11 +353,8 @@ impl VersionT for Version {
|
||||
.await?
|
||||
.await?;
|
||||
|
||||
let to_sync = ctx
|
||||
.db
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let mut to_sync = BTreeSet::new();
|
||||
|
||||
let package = db
|
||||
.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
@@ -396,29 +365,11 @@ impl VersionT for Version {
|
||||
.as_tasks_mut()
|
||||
.remove(&ReplayId::from("needs-config"))?;
|
||||
}
|
||||
for (id, onion) in onions {
|
||||
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)
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.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>(())
|
||||
}
|
||||
.await
|
||||
@@ -481,33 +432,6 @@ async fn previous_account_info(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<Accoun
|
||||
password: account_query
|
||||
.try_get("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
|
||||
.try_get("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)
|
||||
}
|
||||
|
||||
#[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 crate::GatewayId;
|
||||
use crate::net::host::address::PublicDomainConfig;
|
||||
use crate::net::tor::OnionAddress;
|
||||
use crate::prelude::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -22,7 +21,7 @@ lazy_static::lazy_static! {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "kind")]
|
||||
enum HostAddress {
|
||||
Onion { address: OnionAddress },
|
||||
Onion { address: String },
|
||||
Domain { address: InternedString },
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use exver::{PreReleaseSegment, VersionRange};
|
||||
use imbl_value::InternedString;
|
||||
|
||||
use super::v0_3_5::V0_3_0_COMPAT;
|
||||
use super::{VersionT, v0_4_0_alpha_11};
|
||||
use crate::net::tor::TorSecretKey;
|
||||
use crate::prelude::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -33,48 +29,6 @@ impl VersionT for Version {
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
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() {
|
||||
db["private"]["keyStore"]["localCerts"] =
|
||||
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.
|
||||
import type { BindOptions } from './BindOptions'
|
||||
import type { DerivedAddressInfo } from './DerivedAddressInfo'
|
||||
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.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type BindingGatewaySetEnabledParams = {
|
||||
export type BindingSetAddressEnabledParams = {
|
||||
internalPort: number
|
||||
gateway: GatewayId
|
||||
address: string
|
||||
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.
|
||||
import type { BindInfo } from './BindInfo'
|
||||
|
||||
export type OnionHostname = {
|
||||
value: string
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}
|
||||
export type Bindings = { [key: number]: BindInfo }
|
||||
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.
|
||||
|
||||
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.
|
||||
import type { BindInfo } from './BindInfo'
|
||||
import type { HostnameInfo } from './HostnameInfo'
|
||||
import type { Bindings } from './Bindings'
|
||||
import type { PublicDomainConfig } from './PublicDomainConfig'
|
||||
|
||||
export type Host = {
|
||||
bindings: { [key: number]: BindInfo }
|
||||
onions: string[]
|
||||
bindings: Bindings
|
||||
publicDomains: { [key: string]: PublicDomainConfig }
|
||||
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.
|
||||
import type { GatewayInfo } from './GatewayInfo'
|
||||
import type { IpHostname } from './IpHostname'
|
||||
import type { OnionHostname } from './OnionHostname'
|
||||
|
||||
export type HostnameInfo =
|
||||
| { kind: 'ip'; gateway: GatewayInfo; public: boolean; hostname: IpHostname }
|
||||
| { kind: 'onion'; hostname: OnionHostname }
|
||||
export type HostnameInfo = {
|
||||
gateway: GatewayInfo
|
||||
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.
|
||||
import type { GatewayId } from './GatewayId'
|
||||
|
||||
export type NetInfo = {
|
||||
privateDisabled: Array<GatewayId>
|
||||
publicEnabled: Array<GatewayId>
|
||||
assignedPort: number | null
|
||||
assignedSslPort: number | null
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ export { BackupTargetFS } from './BackupTargetFS'
|
||||
export { Base64 } from './Base64'
|
||||
export { BindId } from './BindId'
|
||||
export { BindInfo } from './BindInfo'
|
||||
export { BindingGatewaySetEnabledParams } from './BindingGatewaySetEnabledParams'
|
||||
export { BindingSetAddressEnabledParams } from './BindingSetAddressEnabledParams'
|
||||
export { Bindings } from './Bindings'
|
||||
export { BindOptions } from './BindOptions'
|
||||
export { BindParams } from './BindParams'
|
||||
export { Blake3Commitment } from './Blake3Commitment'
|
||||
@@ -64,6 +65,7 @@ export { Dependencies } from './Dependencies'
|
||||
export { DependencyMetadata } from './DependencyMetadata'
|
||||
export { DependencyRequirement } from './DependencyRequirement'
|
||||
export { DepInfo } from './DepInfo'
|
||||
export { DerivedAddressInfo } from './DerivedAddressInfo'
|
||||
export { Description } from './Description'
|
||||
export { DesiredStatus } from './DesiredStatus'
|
||||
export { DestroySubcontainerFsParams } from './DestroySubcontainerFsParams'
|
||||
@@ -149,7 +151,6 @@ export { NetInfo } from './NetInfo'
|
||||
export { NetworkInfo } from './NetworkInfo'
|
||||
export { NetworkInterfaceInfo } from './NetworkInterfaceInfo'
|
||||
export { NetworkInterfaceType } from './NetworkInterfaceType'
|
||||
export { OnionHostname } from './OnionHostname'
|
||||
export { OsIndex } from './OsIndex'
|
||||
export { OsVersionInfoMap } from './OsVersionInfoMap'
|
||||
export { OsVersionInfo } from './OsVersionInfo'
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { PackageId, ServiceInterfaceId, ServiceInterfaceType } from '../types'
|
||||
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 { DropGenerator, DropPromise } from './Drop'
|
||||
import { IpAddress, IPV6_LINK_LOCAL } from './ip'
|
||||
@@ -20,7 +26,6 @@ export const getHostname = (url: string): Hostname | null => {
|
||||
}
|
||||
|
||||
type FilterKinds =
|
||||
| 'onion'
|
||||
| 'mdns'
|
||||
| 'domain'
|
||||
| 'ip'
|
||||
@@ -42,27 +47,25 @@ type VisibilityFilter<V extends 'public' | 'private'> = V extends 'public'
|
||||
| (HostnameInfo & { public: false })
|
||||
| VisibilityFilter<Exclude<V, 'private'>>
|
||||
: never
|
||||
type KindFilter<K extends FilterKinds> = K extends 'onion'
|
||||
? (HostnameInfo & { kind: 'onion' }) | KindFilter<Exclude<K, 'onion'>>
|
||||
: K extends 'mdns'
|
||||
type KindFilter<K extends FilterKinds> = K extends 'mdns'
|
||||
?
|
||||
| (HostnameInfo & { hostname: { kind: 'local' } })
|
||||
| KindFilter<Exclude<K, 'mdns'>>
|
||||
: K extends 'domain'
|
||||
?
|
||||
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'local' } })
|
||||
| KindFilter<Exclude<K, 'mdns'>>
|
||||
: K extends 'domain'
|
||||
| (HostnameInfo & { hostname: { kind: 'domain' } })
|
||||
| KindFilter<Exclude<K, 'domain'>>
|
||||
: K extends 'ipv4'
|
||||
?
|
||||
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'domain' } })
|
||||
| KindFilter<Exclude<K, 'domain'>>
|
||||
: K extends 'ipv4'
|
||||
| (HostnameInfo & { hostname: { kind: 'ipv4' } })
|
||||
| KindFilter<Exclude<K, 'ipv4'>>
|
||||
: K extends 'ipv6'
|
||||
?
|
||||
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv4' } })
|
||||
| KindFilter<Exclude<K, 'ipv4'>>
|
||||
: K extends 'ipv6'
|
||||
?
|
||||
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv6' } })
|
||||
| KindFilter<Exclude<K, 'ipv6'>>
|
||||
: K extends 'ip'
|
||||
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
|
||||
: never
|
||||
| (HostnameInfo & { hostname: { kind: 'ipv6' } })
|
||||
| KindFilter<Exclude<K, 'ipv6'>>
|
||||
: K extends 'ip'
|
||||
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
|
||||
: never
|
||||
|
||||
type FilterReturnTy<F extends Filter> = F extends {
|
||||
visibility: infer V extends 'public' | 'private'
|
||||
@@ -90,10 +93,6 @@ const nonLocalFilter = {
|
||||
const publicFilter = {
|
||||
visibility: 'public',
|
||||
} as const
|
||||
const onionFilter = {
|
||||
kind: 'onion',
|
||||
} as const
|
||||
|
||||
type Formats = 'hostname-info' | 'urlstring' | 'url'
|
||||
type FormatReturnTy<
|
||||
F extends Filter,
|
||||
@@ -124,7 +123,6 @@ export type Filled<F extends Filter = {}> = {
|
||||
|
||||
nonLocal: Filled<typeof nonLocalFilter & Filter>
|
||||
public: Filled<typeof publicFilter & Filter>
|
||||
onion: Filled<typeof onionFilter & Filter>
|
||||
}
|
||||
export type FilledAddressInfo = AddressInfo & Filled
|
||||
export type ServiceInterfaceFilled = {
|
||||
@@ -162,18 +160,14 @@ export const addressHostToUrl = (
|
||||
scheme in knownProtocols &&
|
||||
port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort
|
||||
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
|
||||
} 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}://` : ''}${
|
||||
username ? `${username}@` : ''
|
||||
@@ -201,13 +195,9 @@ function filterRec(
|
||||
hostnames = hostnames.filter((h) => invert !== pred(h))
|
||||
}
|
||||
if (filter.visibility === 'public')
|
||||
hostnames = hostnames.filter(
|
||||
(h) => invert !== (h.kind === 'onion' || h.public),
|
||||
)
|
||||
hostnames = hostnames.filter((h) => invert !== h.public)
|
||||
if (filter.visibility === 'private')
|
||||
hostnames = hostnames.filter(
|
||||
(h) => invert !== (h.kind !== 'onion' && !h.public),
|
||||
)
|
||||
hostnames = hostnames.filter((h) => invert !== !h.public)
|
||||
if (filter.kind) {
|
||||
const kind = new Set(
|
||||
Array.isArray(filter.kind) ? filter.kind : [filter.kind],
|
||||
@@ -219,19 +209,13 @@ function filterRec(
|
||||
hostnames = hostnames.filter(
|
||||
(h) =>
|
||||
invert !==
|
||||
((kind.has('onion') && h.kind === 'onion') ||
|
||||
(kind.has('mdns') &&
|
||||
h.kind === 'ip' &&
|
||||
h.hostname.kind === 'local') ||
|
||||
(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('mdns') && h.hostname.kind === 'local') ||
|
||||
(kind.has('domain') && h.hostname.kind === 'domain') ||
|
||||
(kind.has('ipv4') && h.hostname.kind === 'ipv4') ||
|
||||
(kind.has('ipv6') && h.hostname.kind === 'ipv6') ||
|
||||
(kind.has('localhost') &&
|
||||
['localhost', '127.0.0.1', '::1'].includes(h.hostname.value)) ||
|
||||
(kind.has('link-local') &&
|
||||
h.kind === 'ip' &&
|
||||
h.hostname.kind === 'ipv6' &&
|
||||
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname.value)))),
|
||||
)
|
||||
@@ -242,6 +226,14 @@ function filterRec(
|
||||
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 = (
|
||||
host: Host,
|
||||
addressInfo: AddressInfo,
|
||||
@@ -251,7 +243,8 @@ export const filledAddress = (
|
||||
const u = toUrls(h)
|
||||
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>(
|
||||
hostnames: HostnameInfo[],
|
||||
@@ -266,11 +259,6 @@ export const filledAddress = (
|
||||
filterRec(hostnames, publicFilter, false),
|
||||
),
|
||||
)
|
||||
const getOnion = once(() =>
|
||||
filledAddressFromHostnames<typeof onionFilter & F>(
|
||||
filterRec(hostnames, onionFilter, false),
|
||||
),
|
||||
)
|
||||
return {
|
||||
...addressInfo,
|
||||
hostnames,
|
||||
@@ -294,9 +282,6 @@ export const filledAddress = (
|
||||
get public(): Filled<typeof publicFilter & F> {
|
||||
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',
|
||||
}
|
||||
|
||||
export const torHostname: Pattern = {
|
||||
regex: regexes.torHostname.matches(),
|
||||
description: 'Must be a valid Tor (".onion") hostname',
|
||||
}
|
||||
|
||||
export const url: Pattern = {
|
||||
regex: regexes.url.matches(),
|
||||
description: 'Must be a valid URL',
|
||||
@@ -36,11 +31,6 @@ export const localUrl: Pattern = {
|
||||
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 = {
|
||||
regex: regexes.ascii.matches(),
|
||||
description:
|
||||
|
||||
@@ -39,10 +39,6 @@ export const localHostname = new ComposableRegex(
|
||||
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/,
|
||||
)
|
||||
|
||||
export const torHostname = new ComposableRegex(
|
||||
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion/,
|
||||
)
|
||||
|
||||
// https://ihateregex.io/expr/url/
|
||||
export const url = new ComposableRegex(
|
||||
/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()!@:%_\+.~#?&\/\/=]*)/,
|
||||
)
|
||||
|
||||
export const torUrl = new ComposableRegex(
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
||||
)
|
||||
|
||||
// https://ihateregex.io/expr/ascii/
|
||||
export const ascii = new ComposableRegex(/[ -~]*/)
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ import {
|
||||
import { getOwnServiceInterfaces } from '../../base/lib/util/getServiceInterfaces'
|
||||
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
|
||||
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
|
||||
export function camelCase(value: string) {
|
||||
return value
|
||||
.replace(/([\(\)\[\]])/g, "")
|
||||
.replace(/([\(\)\[\]])/g, '')
|
||||
.replace(/^([A-Z])|[\s-_](\w)/g, function (match, p1, p2, offset) {
|
||||
if (p2) return p2.toUpperCase()
|
||||
return p1.toLowerCase()
|
||||
@@ -23,12 +23,12 @@ export async function oldSpecToBuilder(
|
||||
}
|
||||
|
||||
function isString(x: unknown): x is string {
|
||||
return typeof x === "string"
|
||||
return typeof x === 'string'
|
||||
}
|
||||
|
||||
export default async function makeFileContentFromOld(
|
||||
inputData: Promise<any> | any,
|
||||
{ StartSdk = "start-sdk", nested = true } = {},
|
||||
{ StartSdk = 'start-sdk', nested = true } = {},
|
||||
) {
|
||||
const outputLines: string[] = []
|
||||
outputLines.push(`
|
||||
@@ -37,16 +37,16 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
`)
|
||||
const data = await inputData
|
||||
|
||||
const namedConsts = new Set(["InputSpec", "Value", "List"])
|
||||
const inputSpecName = newConst("inputSpecSpec", convertInputSpec(data))
|
||||
const namedConsts = new Set(['InputSpec', 'Value', 'List'])
|
||||
const inputSpecName = newConst('inputSpecSpec', convertInputSpec(data))
|
||||
outputLines.push(`export type InputSpecSpec = typeof ${inputSpecName}._TYPE;`)
|
||||
|
||||
return outputLines.join("\n")
|
||||
return outputLines.join('\n')
|
||||
|
||||
function newConst(key: string, data: string, type?: string) {
|
||||
const variableName = getNextConstName(camelCase(key))
|
||||
outputLines.push(
|
||||
`export const ${variableName}${!type ? "" : `: ${type}`} = ${data};`,
|
||||
`export const ${variableName}${!type ? '' : `: ${type}`} = ${data};`,
|
||||
)
|
||||
return variableName
|
||||
}
|
||||
@@ -55,7 +55,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
return newConst(key, data)
|
||||
}
|
||||
function convertInputSpecInner(data: any) {
|
||||
let answer = "{"
|
||||
let answer = '{'
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const variableName = maybeNewConst(key, convertValueSpec(value))
|
||||
|
||||
@@ -69,7 +69,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
}
|
||||
function convertValueSpec(value: any): string {
|
||||
switch (value.type) {
|
||||
case "string": {
|
||||
case 'string': {
|
||||
if (value.textarea) {
|
||||
return `${rangeToTodoComment(
|
||||
value?.range,
|
||||
@@ -99,12 +99,12 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
warning: value.warning || null,
|
||||
masked: value.masked || false,
|
||||
placeholder: value.placeholder || null,
|
||||
inputmode: "text",
|
||||
inputmode: 'text',
|
||||
patterns: value.pattern
|
||||
? [
|
||||
{
|
||||
regex: value.pattern,
|
||||
description: value["pattern-description"],
|
||||
description: value['pattern-description'],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
@@ -115,7 +115,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
2,
|
||||
)})`
|
||||
}
|
||||
case "number": {
|
||||
case 'number': {
|
||||
return `${rangeToTodoComment(
|
||||
value?.range,
|
||||
)}Value.number(${JSON.stringify(
|
||||
@@ -136,7 +136,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
2,
|
||||
)})`
|
||||
}
|
||||
case "boolean": {
|
||||
case 'boolean': {
|
||||
return `Value.toggle(${JSON.stringify(
|
||||
{
|
||||
name: value.name || null,
|
||||
@@ -148,15 +148,15 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
2,
|
||||
)})`
|
||||
}
|
||||
case "enum": {
|
||||
case 'enum': {
|
||||
const allValueNames = new Set([
|
||||
...(value?.["values"] || []),
|
||||
...Object.keys(value?.["value-names"] || {}),
|
||||
...(value?.['values'] || []),
|
||||
...Object.keys(value?.['value-names'] || {}),
|
||||
])
|
||||
const values = Object.fromEntries(
|
||||
Array.from(allValueNames)
|
||||
.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(
|
||||
{
|
||||
@@ -170,9 +170,9 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
2,
|
||||
)} as const)`
|
||||
}
|
||||
case "object": {
|
||||
case 'object': {
|
||||
const specName = maybeNewConst(
|
||||
value.name + "_spec",
|
||||
value.name + '_spec',
|
||||
convertInputSpec(value.spec),
|
||||
)
|
||||
return `Value.object({
|
||||
@@ -180,10 +180,10 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
description: ${JSON.stringify(value.description || null)},
|
||||
}, ${specName})`
|
||||
}
|
||||
case "union": {
|
||||
case 'union': {
|
||||
const variants = maybeNewConst(
|
||||
value.name + "_variants",
|
||||
convertVariants(value.variants, value.tag["variant-names"] || {}),
|
||||
value.name + '_variants',
|
||||
convertVariants(value.variants, value.tag['variant-names'] || {}),
|
||||
)
|
||||
|
||||
return `Value.union({
|
||||
@@ -194,18 +194,18 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
variants: ${variants},
|
||||
})`
|
||||
}
|
||||
case "list": {
|
||||
if (value.subtype === "enum") {
|
||||
case 'list': {
|
||||
if (value.subtype === 'enum') {
|
||||
const allValueNames = new Set([
|
||||
...(value?.spec?.["values"] || []),
|
||||
...Object.keys(value?.spec?.["value-names"] || {}),
|
||||
...(value?.spec?.['values'] || []),
|
||||
...Object.keys(value?.spec?.['value-names'] || {}),
|
||||
])
|
||||
const values = Object.fromEntries(
|
||||
Array.from(allValueNames)
|
||||
.filter(isString)
|
||||
.map((key: string) => [
|
||||
key,
|
||||
value?.spec?.["value-names"]?.[key] ?? key,
|
||||
value?.spec?.['value-names']?.[key] ?? key,
|
||||
]),
|
||||
)
|
||||
return `Value.multiselect(${JSON.stringify(
|
||||
@@ -222,10 +222,10 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
2,
|
||||
)})`
|
||||
}
|
||||
const list = maybeNewConst(value.name + "_list", convertList(value))
|
||||
const list = maybeNewConst(value.name + '_list', convertList(value))
|
||||
return `Value.list(${list})`
|
||||
}
|
||||
case "pointer": {
|
||||
case 'pointer': {
|
||||
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) {
|
||||
switch (value.subtype) {
|
||||
case "string": {
|
||||
case 'string': {
|
||||
return `${rangeToTodoComment(value?.range)}List.text(${JSON.stringify(
|
||||
{
|
||||
name: value.name || null,
|
||||
@@ -253,7 +253,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
? [
|
||||
{
|
||||
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,
|
||||
// })})`
|
||||
// }
|
||||
case "enum": {
|
||||
return "/* error!! list.enum */"
|
||||
case 'enum': {
|
||||
return '/* error!! list.enum */'
|
||||
}
|
||||
case "object": {
|
||||
case 'object': {
|
||||
const specName = maybeNewConst(
|
||||
value.name + "_spec",
|
||||
value.name + '_spec',
|
||||
convertInputSpec(value.spec.spec),
|
||||
)
|
||||
return `${rangeToTodoComment(value?.range)}List.obj({
|
||||
@@ -297,20 +297,20 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
description: ${JSON.stringify(value.description || null)},
|
||||
}, {
|
||||
spec: ${specName},
|
||||
displayAs: ${JSON.stringify(value?.spec?.["display-as"] || null)},
|
||||
uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)},
|
||||
displayAs: ${JSON.stringify(value?.spec?.['display-as'] || null)},
|
||||
uniqueBy: ${JSON.stringify(value?.spec?.['unique-by'] || null)},
|
||||
})`
|
||||
}
|
||||
case "union": {
|
||||
case 'union': {
|
||||
const variants = maybeNewConst(
|
||||
value.name + "_variants",
|
||||
value.name + '_variants',
|
||||
convertVariants(
|
||||
value.spec.variants,
|
||||
value.spec["variant-names"] || {},
|
||||
value.spec['variant-names'] || {},
|
||||
),
|
||||
)
|
||||
const unionValueName = maybeNewConst(
|
||||
value.name + "_union",
|
||||
value.name + '_union',
|
||||
`${rangeToTodoComment(value?.range)}
|
||||
Value.union({
|
||||
name: ${JSON.stringify(value?.spec?.tag?.name || null)},
|
||||
@@ -324,7 +324,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
`,
|
||||
)
|
||||
const listInputSpec = maybeNewConst(
|
||||
value.name + "_list_inputSpec",
|
||||
value.name + '_list_inputSpec',
|
||||
`
|
||||
InputSpec.of({
|
||||
"union": ${unionValueName}
|
||||
@@ -340,8 +340,8 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
warning: ${JSON.stringify(value.warning || null)},
|
||||
}, {
|
||||
spec: ${listInputSpec},
|
||||
displayAs: ${JSON.stringify(value?.spec?.["display-as"] || null)},
|
||||
uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)},
|
||||
displayAs: ${JSON.stringify(value?.spec?.['display-as'] || null)},
|
||||
uniqueBy: ${JSON.stringify(value?.spec?.['unique-by'] || null)},
|
||||
})`
|
||||
}
|
||||
}
|
||||
@@ -352,7 +352,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
variants: Record<string, unknown>,
|
||||
variantNames: Record<string, string>,
|
||||
): string {
|
||||
let answer = "Variants.of({"
|
||||
let answer = 'Variants.of({'
|
||||
for (const [key, value] of Object.entries(variants)) {
|
||||
const variantSpec = maybeNewConst(key, convertInputSpec(value))
|
||||
answer += `"${key}": {name: "${
|
||||
@@ -373,7 +373,7 @@ const {InputSpec, List, Value, Variants} = sdk
|
||||
}
|
||||
|
||||
function rangeToTodoComment(range: string | undefined) {
|
||||
if (!range) return ""
|
||||
if (!range) return ''
|
||||
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",
|
||||
"version": "0.4.0-alpha.19",
|
||||
"version": "0.4.0-alpha.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "startos-ui",
|
||||
"version": "0.4.0-alpha.19",
|
||||
"version": "0.4.0-alpha.20",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^20.3.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.4.0-alpha.19",
|
||||
"version": "0.4.0-alpha.20",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export type AccessType =
|
||||
| 'tor'
|
||||
| 'mdns'
|
||||
| 'localhost'
|
||||
| 'ipv4'
|
||||
|
||||
@@ -83,32 +83,8 @@ export class InterfaceGatewaysComponent {
|
||||
|
||||
readonly gateways = input.required<InterfaceGateway[] | undefined>()
|
||||
|
||||
async onToggle(gateway: InterfaceGateway) {
|
||||
const addressInfo = this.interface.value()!.addressInfo
|
||||
const pkgId = this.interface.packageId()
|
||||
|
||||
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()
|
||||
}
|
||||
async onToggle(_gateway: InterfaceGateway) {
|
||||
// TODO: Replace with per-address toggle UI (Section 6 frontend overhaul).
|
||||
// Gateway-level toggle replaced by set-address-enabled RPC.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { MappedServiceInterface } from './interface.service'
|
||||
import { InterfaceGatewaysComponent } from './gateways.component'
|
||||
import { InterfaceTorDomainsComponent } from './tor-domains.component'
|
||||
import { PublicDomainsComponent } from './public-domains/pd.component'
|
||||
import { InterfacePrivateDomainsComponent } from './private-domains.component'
|
||||
import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
@@ -16,7 +15,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
[publicDomains]="value()?.publicDomains"
|
||||
[addSsl]="value()?.addSsl || false"
|
||||
></section>
|
||||
<section [torDomains]="value()?.torDomains"></section>
|
||||
<section [privateDomains]="value()?.privateDomains"></section>
|
||||
</div>
|
||||
<hr [style.width.rem]="10" />
|
||||
@@ -52,7 +50,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
providers: [tuiButtonOptionsProvider({ size: 'xs' })],
|
||||
imports: [
|
||||
InterfaceGatewaysComponent,
|
||||
InterfaceTorDomainsComponent,
|
||||
PublicDomainsComponent,
|
||||
InterfacePrivateDomainsComponent,
|
||||
InterfaceAddressesComponent,
|
||||
|
||||
@@ -27,17 +27,9 @@ function cmpWithRankedPredicates<T extends AddressWithInfo>(
|
||||
return 0
|
||||
}
|
||||
|
||||
type TorAddress = AddressWithInfo & { info: { kind: 'onion' } }
|
||||
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 } }
|
||||
type LanAddress = AddressWithInfo & { info: { public: false } }
|
||||
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 {
|
||||
return cmpWithRankedPredicates(a, b, [
|
||||
@@ -53,15 +45,12 @@ function cmpLan(host: T.Host, a: LanAddress, b: LanAddress): -1 | 0 | 1 {
|
||||
|
||||
type VpnAddress = AddressWithInfo & {
|
||||
info: {
|
||||
kind: 'ip'
|
||||
public: false
|
||||
hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
|
||||
}
|
||||
}
|
||||
function filterVpn(a: AddressWithInfo): a is VpnAddress {
|
||||
return (
|
||||
a.info.kind === 'ip' && !a.info.public && a.info.hostname.kind !== 'local'
|
||||
)
|
||||
return !a.info.public && a.info.hostname.kind !== 'local'
|
||||
}
|
||||
function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 {
|
||||
return cmpWithRankedPredicates(a, b, [
|
||||
@@ -76,13 +65,12 @@ function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 {
|
||||
|
||||
type ClearnetAddress = AddressWithInfo & {
|
||||
info: {
|
||||
kind: 'ip'
|
||||
public: true
|
||||
hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
|
||||
}
|
||||
}
|
||||
function filterClearnet(a: AddressWithInfo): a is ClearnetAddress {
|
||||
return a.info.kind === 'ip' && a.info.public
|
||||
return a.info.public
|
||||
}
|
||||
function cmpClearnet(
|
||||
host: T.Host,
|
||||
@@ -142,10 +130,7 @@ export class InterfaceService {
|
||||
h,
|
||||
)
|
||||
const info = h
|
||||
const gateway =
|
||||
h.kind === 'ip'
|
||||
? gateways.find(g => h.gateway.id === g.id)
|
||||
: undefined
|
||||
const gateway = gateways.find(g => h.gateway.id === g.id)
|
||||
const res = []
|
||||
if (url) {
|
||||
res.push({
|
||||
@@ -171,7 +156,6 @@ export class InterfaceService {
|
||||
},
|
||||
)
|
||||
|
||||
const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor)
|
||||
const lanAddrs = allAddressesWithInfo
|
||||
.filter(filterLan)
|
||||
.sort((a, b) => cmpLan(host, a, b))
|
||||
@@ -188,7 +172,6 @@ export class InterfaceService {
|
||||
clearnetAddrs[0],
|
||||
lanAddrs[0],
|
||||
vpnAddrs[0],
|
||||
torAddrs[0],
|
||||
]
|
||||
.filter(a => !!a)
|
||||
.reduce((acc, x) => {
|
||||
@@ -214,9 +197,8 @@ export class InterfaceService {
|
||||
kind: 'domain',
|
||||
visibility: 'public',
|
||||
})
|
||||
const tor = addresses.filter({ kind: 'onion' })
|
||||
const wanIp = addresses.filter({ kind: 'ipv4', visibility: 'public' })
|
||||
const bestPublic = [publicDomains, tor, wanIp].flatMap(h =>
|
||||
const bestPublic = [publicDomains, wanIp].flatMap(h =>
|
||||
h.format('urlstring'),
|
||||
)[0]
|
||||
const privateDomains = addresses.filter({
|
||||
@@ -254,9 +236,6 @@ export class InterfaceService {
|
||||
.format('urlstring')[0]
|
||||
onLan = true
|
||||
break
|
||||
case 'tor':
|
||||
matching = tor.format('urlstring')[0]
|
||||
break
|
||||
case 'mdns':
|
||||
matching = mdns.format('urlstring')[0]
|
||||
onLan = true
|
||||
@@ -273,19 +252,23 @@ export class InterfaceService {
|
||||
serviceInterface: T.ServiceInterface,
|
||||
host: T.Host,
|
||||
): T.HostnameInfo[] {
|
||||
let hostnameInfo =
|
||||
host.hostnameInfo[serviceInterface.addressInfo.internalPort]
|
||||
return (
|
||||
hostnameInfo?.filter(
|
||||
h =>
|
||||
this.config.accessType === 'localhost' ||
|
||||
!(
|
||||
h.kind === 'ip' &&
|
||||
((h.hostname.kind === 'ipv6' &&
|
||||
utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) ||
|
||||
h.gateway.id === 'lo')
|
||||
),
|
||||
) || []
|
||||
const binding =
|
||||
host.bindings[serviceInterface.addressInfo.internalPort]
|
||||
if (!binding) return []
|
||||
const addr = binding.addresses
|
||||
const enabled = addr.possible.filter(h =>
|
||||
h.public
|
||||
? addr.publicEnabled.some(e => utils.deepEqual(e, h))
|
||||
: !addr.privateDisabled.some(d => utils.deepEqual(d, h)),
|
||||
)
|
||||
return enabled.filter(
|
||||
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",
|
||||
)
|
||||
|
||||
// ** 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
|
||||
gatewayName = info.gateway.name
|
||||
|
||||
@@ -479,7 +438,6 @@ export class InterfaceService {
|
||||
|
||||
export type MappedServiceInterface = T.ServiceInterface & {
|
||||
gateways: InterfaceGateway[]
|
||||
torDomains: string[]
|
||||
publicDomains: PublicDomain[]
|
||||
privateDomains: string[]
|
||||
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',
|
||||
loadComponent: () => import('./routes/os.component'),
|
||||
},
|
||||
{
|
||||
path: 'tor',
|
||||
loadComponent: () => import('./routes/tor.component'),
|
||||
},
|
||||
]
|
||||
|
||||
export default ROUTES
|
||||
|
||||
@@ -79,12 +79,6 @@ export default class SystemLogsComponent {
|
||||
subtitle: 'Raw, unfiltered operating system logs',
|
||||
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',
|
||||
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 => ({
|
||||
enabled:
|
||||
(g.public
|
||||
? binding?.net.publicEnabled.includes(g.id)
|
||||
: !binding?.net.privateDisabled.includes(g.id)) ?? false,
|
||||
? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id)
|
||||
: !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false,
|
||||
...g,
|
||||
})) || [],
|
||||
torDomains: host.onions,
|
||||
publicDomains: getPublicDomains(host.publicDomains, gateways),
|
||||
privateDomains: host.privateDomains,
|
||||
addSsl: !!binding?.options.addSsl,
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
TuiFiles,
|
||||
tuiInputFilesOptionsProvider,
|
||||
} from '@taiga-ui/kit'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
|
||||
import { SideloadPackageComponent } from './package.component'
|
||||
@@ -55,11 +54,6 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
|
||||
<div>
|
||||
<tui-avatar appearance="secondary" src="@tui.upload" />
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
@@ -92,8 +86,6 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
|
||||
],
|
||||
})
|
||||
export default class SideloadComponent {
|
||||
readonly isTor = inject(ConfigService).accessType === 'tor'
|
||||
|
||||
file: File | null = null
|
||||
readonly package = signal<MarketplacePkgSideload | 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 { filter } from 'rxjs'
|
||||
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 { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { SnekDirective } from './snek.directive'
|
||||
import { UPDATE } from './update.component'
|
||||
import { SystemWipeComponent } from './wipe.component'
|
||||
import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||
|
||||
@Component({
|
||||
@@ -66,7 +64,7 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||
</ng-container>
|
||||
@if (server(); as server) {
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.zap" />
|
||||
<tui-icon icon="@tui.zap" (click)="count = count + 1" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ 'Software Update' | i18n }}</strong>
|
||||
<span tuiSubtitle [style.flex-wrap]="'wrap'">
|
||||
@@ -178,18 +176,6 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||
</button>
|
||||
}
|
||||
</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) {
|
||||
<div tuiCell tuiAppearance="outline-grayscale" tuiAnimated>
|
||||
<tui-icon icon="@tui.briefcase-medical" />
|
||||
@@ -283,12 +269,10 @@ export default class SystemGeneralComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly isTor = inject(ConfigService).accessType === 'tor'
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
private readonly injector = inject(INJECTOR)
|
||||
|
||||
wipe = false
|
||||
count = 0
|
||||
|
||||
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() {
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
@@ -532,19 +498,6 @@ export default class SystemGeneralComponent {
|
||||
.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() {
|
||||
this.dialogs
|
||||
.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 => ({
|
||||
enabled:
|
||||
(g.public
|
||||
? binding?.net.publicEnabled.includes(g.id)
|
||||
: !binding?.net.privateDisabled.includes(g.id)) ?? false,
|
||||
? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id)
|
||||
: !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false,
|
||||
...g,
|
||||
})),
|
||||
torDomains: network.host.onions,
|
||||
publicDomains: getPublicDomains(network.host.publicDomains, gateways),
|
||||
privateDomains: network.host.privateDomains,
|
||||
addSsl: true,
|
||||
|
||||
@@ -2126,8 +2126,74 @@ export namespace Mock {
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
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: {
|
||||
addSsl: null,
|
||||
@@ -2138,87 +2204,6 @@ export namespace Mock {
|
||||
},
|
||||
publicDomains: {},
|
||||
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: {
|
||||
bindings: {
|
||||
@@ -2227,8 +2212,11 @@ export namespace Mock {
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
privateDisabled: [],
|
||||
publicEnabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -2239,10 +2227,6 @@ export namespace Mock {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: [],
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
bindings: {
|
||||
@@ -2251,8 +2235,11 @@ export namespace Mock {
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
privateDisabled: [],
|
||||
publicEnabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -2263,10 +2250,6 @@ export namespace Mock {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: [],
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
storeExposedDependents: [],
|
||||
|
||||
@@ -79,14 +79,14 @@ export namespace RR {
|
||||
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 FollowServerLogsReq = {
|
||||
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
|
||||
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 = {
|
||||
startCursor: string
|
||||
guid: string
|
||||
@@ -120,12 +120,6 @@ export namespace RR {
|
||||
} // net.dns.query
|
||||
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 SetKeyboardRes = null
|
||||
|
||||
@@ -301,29 +295,13 @@ export namespace RR {
|
||||
}
|
||||
export type RemoveAcmeRes = null
|
||||
|
||||
export type AddTorKeyReq = {
|
||||
// net.tor.key.add
|
||||
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
|
||||
export type ServerBindingSetAddressEnabledReq = {
|
||||
// server.host.binding.set-address-enabled
|
||||
internalPort: 80
|
||||
enabled: boolean
|
||||
address: string // JSON-serialized HostnameInfo
|
||||
enabled: boolean | null // null = reset to default
|
||||
}
|
||||
export type ServerBindingToggleGatewayRes = 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 ServerBindingSetAddressEnabledRes = null
|
||||
|
||||
export type OsUiAddPublicDomainReq = {
|
||||
// server.host.address.domain.public.add
|
||||
@@ -351,23 +329,16 @@ export namespace RR {
|
||||
}
|
||||
export type OsUiRemovePrivateDomainRes = null
|
||||
|
||||
export type PkgBindingToggleGatewayReq = Omit<
|
||||
ServerBindingToggleGatewayReq,
|
||||
export type PkgBindingSetAddressEnabledReq = Omit<
|
||||
ServerBindingSetAddressEnabledReq,
|
||||
'internalPort'
|
||||
> & {
|
||||
// package.host.binding.set-gateway-enabled
|
||||
// package.host.binding.set-address-enabled
|
||||
internalPort: number
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
export type PkgBindingToggleGatewayRes = 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 PkgBindingSetAddressEnabledRes = null
|
||||
|
||||
export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & {
|
||||
// package.host.address.domain.public.add
|
||||
|
||||
@@ -81,8 +81,6 @@ export abstract class ApiService {
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes>
|
||||
|
||||
abstract getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes>
|
||||
|
||||
abstract getKernelLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes>
|
||||
@@ -91,10 +89,6 @@ export abstract class ApiService {
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
|
||||
abstract followTorLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
|
||||
abstract followKernelLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
@@ -125,8 +119,6 @@ export abstract class ApiService {
|
||||
|
||||
abstract queryDns(params: RR.QueryDnsReq): Promise<RR.QueryDnsRes>
|
||||
|
||||
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
|
||||
|
||||
// smtp
|
||||
|
||||
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 addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes>
|
||||
|
||||
abstract generateTorKey(
|
||||
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 serverBindingSetAddressEnabled(
|
||||
params: RR.ServerBindingSetAddressEnabledReq,
|
||||
): Promise<RR.ServerBindingSetAddressEnabledRes>
|
||||
|
||||
abstract osUiAddPublicDomain(
|
||||
params: RR.OsUiAddPublicDomainReq,
|
||||
@@ -384,15 +364,9 @@ export abstract class ApiService {
|
||||
params: RR.OsUiRemovePrivateDomainReq,
|
||||
): Promise<RR.OsUiRemovePrivateDomainRes>
|
||||
|
||||
abstract pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes>
|
||||
|
||||
abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes>
|
||||
|
||||
abstract pkgRemoveOnion(
|
||||
params: RR.PkgRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes>
|
||||
abstract pkgBindingSetAddressEnabled(
|
||||
params: RR.PkgBindingSetAddressEnabledReq,
|
||||
): Promise<RR.PkgBindingSetAddressEnabledRes>
|
||||
|
||||
abstract pkgAddPublicDomain(
|
||||
params: RR.PkgAddPublicDomainReq,
|
||||
|
||||
@@ -195,10 +195,6 @@ export class LiveApiService extends ApiService {
|
||||
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(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
@@ -211,12 +207,6 @@ export class LiveApiService extends ApiService {
|
||||
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(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): 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
|
||||
|
||||
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({
|
||||
method: 'net.tor.key.add',
|
||||
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',
|
||||
method: 'server.host.binding.set-address-enabled',
|
||||
params,
|
||||
})
|
||||
}
|
||||
@@ -708,27 +664,11 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes> {
|
||||
async pkgBindingSetAddressEnabled(
|
||||
params: RR.PkgBindingSetAddressEnabledReq,
|
||||
): Promise<RR.PkgBindingSetAddressEnabledRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.binding.set-gateway-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',
|
||||
method: 'package.host.binding.set-address-enabled',
|
||||
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(
|
||||
params: RR.GetServerLogsReq,
|
||||
): 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(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
@@ -504,11 +483,6 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async checkOSUpdate(
|
||||
@@ -1415,77 +1389,12 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return 'vanityabcdefghijklmnop.onion'
|
||||
}
|
||||
|
||||
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return 'abcdefghijklmnopqrstuv.onion'
|
||||
}
|
||||
|
||||
async serverBindingToggleGateway(
|
||||
params: RR.ServerBindingToggleGatewayReq,
|
||||
): Promise<RR.ServerBindingToggleGatewayRes> {
|
||||
async serverBindingSetAddressEnabled(
|
||||
params: RR.ServerBindingSetAddressEnabledReq,
|
||||
): Promise<RR.ServerBindingSetAddressEnabledRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
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)
|
||||
|
||||
// Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1504,10 +1413,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: true,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1536,7 +1444,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
@@ -1557,10 +1465,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1590,7 +1497,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
@@ -1598,67 +1505,12 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes> {
|
||||
async pkgBindingSetAddressEnabled(
|
||||
params: RR.PkgBindingSetAddressEnabledReq,
|
||||
): Promise<RR.PkgBindingSetAddressEnabledRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
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)
|
||||
|
||||
// Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1677,10 +1529,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
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: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: true,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1709,7 +1560,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
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)
|
||||
@@ -1730,10 +1581,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
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: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1763,7 +1613,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -38,8 +38,74 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: null,
|
||||
assignedSslPort: 443,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
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: {
|
||||
preferredExternalPort: 80,
|
||||
@@ -54,87 +120,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
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: {
|
||||
eth0: {
|
||||
@@ -529,8 +514,74 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
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: {
|
||||
addSsl: null,
|
||||
@@ -541,87 +592,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
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: {
|
||||
bindings: {
|
||||
@@ -630,8 +600,11 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
privateDisabled: [],
|
||||
publicEnabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -642,10 +615,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: [],
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
bindings: {
|
||||
@@ -654,8 +623,11 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
},
|
||||
addresses: {
|
||||
privateDisabled: [],
|
||||
publicEnabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -666,10 +638,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: [],
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
storeExposedDependents: [],
|
||||
|
||||
@@ -32,7 +32,6 @@ export class ConfigService {
|
||||
private getAccessType = utils.once(() => {
|
||||
if (useMocks) return mocks.maskAs
|
||||
if (this.hostname === 'localhost') return 'localhost'
|
||||
if (this.hostname.endsWith('.onion')) return 'tor'
|
||||
if (this.hostname.endsWith('.local')) return 'mdns'
|
||||
let ip = null
|
||||
try {
|
||||
@@ -51,7 +50,7 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
isLanHttp(): boolean {
|
||||
return !this.isHttps() && !['localhost', 'tor'].includes(this.accessType)
|
||||
return !this.isHttps() && this.accessType !== 'localhost'
|
||||
}
|
||||
|
||||
isHttps(): boolean {
|
||||
|
||||
Reference in New Issue
Block a user