diff --git a/core/src/tunnel/api.rs b/core/src/tunnel/api.rs index 85d226edd..523dc3900 100644 --- a/core/src/tunnel/api.rs +++ b/core/src/tunnel/api.rs @@ -60,6 +60,14 @@ pub fn tunnel_api() -> ParentHandler { .no_display() .with_about("about.update-port-forward-label") .with_call_remote::(), + ) + .subcommand( + "set-enabled", + from_fn_async(set_forward_enabled) + .with_metadata("sync_db", Value::Bool(true)) + .no_display() + .with_about("about.enable-or-disable-port-forward") + .with_call_remote::(), ), ) .subcommand( @@ -497,7 +505,7 @@ pub async fn add_forward( m.insert(source, rc); }); - let entry = PortForwardEntry { target, label }; + let entry = PortForwardEntry { target, label, enabled: true }; ctx.db .mutate(|db| { @@ -568,3 +576,64 @@ pub async fn update_forward_label( .await .result } + +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +pub struct SetPortForwardEnabledParams { + source: SocketAddrV4, + enabled: bool, +} + +pub async fn set_forward_enabled( + ctx: TunnelContext, + SetPortForwardEnabledParams { source, enabled }: SetPortForwardEnabledParams, +) -> Result<(), Error> { + let target = ctx + .db + .mutate(|db| { + db.as_port_forwards_mut().mutate(|pf| { + let entry = pf.0.get_mut(&source).ok_or_else(|| { + Error::new( + eyre!("Port forward from {source} not found"), + ErrorKind::NotFound, + ) + })?; + entry.enabled = enabled; + Ok(entry.target) + }) + }) + .await + .result?; + + if enabled { + let prefix = ctx + .net_iface + .peek(|i| { + i.iter() + .find_map(|(_, i)| { + i.ip_info.as_ref().and_then(|i| { + i.subnets + .iter() + .find(|s| s.contains(&IpAddr::from(*target.ip()))) + }) + }) + .cloned() + }) + .map(|s| s.prefix_len()) + .unwrap_or(32); + let rc = ctx + .forward + .add_forward(source, target, prefix, None) + .await?; + ctx.active_forwards.mutate(|m| { + m.insert(source, rc); + }); + } else { + if let Some(rc) = ctx.active_forwards.mutate(|m| m.remove(&source)) { + drop(rc); + ctx.forward.gc().await?; + } + } + + Ok(()) +} diff --git a/core/src/tunnel/context.rs b/core/src/tunnel/context.rs index da9f61297..769f62787 100644 --- a/core/src/tunnel/context.rs +++ b/core/src/tunnel/context.rs @@ -185,6 +185,9 @@ impl TunnelContext { let mut active_forwards = BTreeMap::new(); for (from, entry) in peek.as_port_forwards().de()?.0 { + if !entry.enabled { + continue; + } let to = entry.target; let prefix = net_iface .peek(|i| { diff --git a/core/src/tunnel/db.rs b/core/src/tunnel/db.rs index da13209b5..b18c01abf 100644 --- a/core/src/tunnel/db.rs +++ b/core/src/tunnel/db.rs @@ -76,6 +76,12 @@ pub struct PortForwardEntry { pub target: SocketAddrV4, #[serde(default)] pub label: String, + #[serde(default = "default_true")] + pub enabled: bool, +} + +fn default_true() -> bool { + true } #[derive(Clone, Debug, Default, Deserialize, Serialize, TS)] diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/index.ts b/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/index.ts index ca7718288..26e8dd52c 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/index.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/index.ts @@ -3,19 +3,22 @@ import { Component, computed, inject, + signal, Signal, } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' +import { FormsModule } from '@angular/forms' import { ErrorService, LoadingService } from '@start9labs/shared' import { utils } from '@start9labs/start-sdk' import { TuiButton, TuiDataList, TuiDropdown, + TuiLoader, TuiTextfield, } from '@taiga-ui/core' import { TuiDialogService } from '@taiga-ui/experimental' -import { TUI_CONFIRM } from '@taiga-ui/kit' +import { TUI_CONFIRM, TuiSwitch } from '@taiga-ui/kit' import { PatchDB } from 'patch-db-client' import { filter, map } from 'rxjs' import { PORT_FORWARDS_ADD } from 'src/app/routes/home/routes/port-forwards/add' @@ -30,6 +33,7 @@ import { MappedDevice, MappedForward } from './utils' + @@ -45,6 +49,22 @@ import { MappedDevice, MappedForward } from './utils' @for (forward of forwards(); track $index) { + @@ -88,7 +108,15 @@ import { MappedDevice, MappedForward } from './utils'
Label External IP External Port
+ + + + {{ forward.label || '—' }} {{ forward.externalip }} {{ forward.externalport }}
`, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiButton, TuiDropdown, TuiDataList, TuiTextfield], + imports: [ + FormsModule, + TuiButton, + TuiDropdown, + TuiDataList, + TuiLoader, + TuiSwitch, + TuiTextfield, + ], }) export default class PortForwards { private readonly dialogs = inject(TuiDialogService) @@ -136,10 +164,26 @@ export default class PortForwards { device: this.devices().find(d => d.ip === targetSplit[0])!, internalport: targetSplit[1]!, label: entry.label, + enabled: entry.enabled, } }), ) + protected readonly toggling = signal(null) + + protected async onToggle(forward: MappedForward, index: number) { + this.toggling.set(index) + const source = `${forward.externalip}:${forward.externalport}` + + try { + await this.api.setForwardEnabled({ source, enabled: !forward.enabled }) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + this.toggling.set(null) + } + } + protected onAdd(): void { this.dialogs .open(PORT_FORWARDS_ADD, { diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/utils.ts b/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/utils.ts index 973f3faa8..c9b55f25f 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/utils.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/utils.ts @@ -11,6 +11,7 @@ export interface MappedForward { readonly device: MappedDevice readonly internalport: string readonly label: string + readonly enabled: boolean } export interface PortForwardsData { diff --git a/web/projects/start-tunnel/src/app/services/api/api.service.ts b/web/projects/start-tunnel/src/app/services/api/api.service.ts index e91f5fa78..cb1b29e57 100644 --- a/web/projects/start-tunnel/src/app/services/api/api.service.ts +++ b/web/projects/start-tunnel/src/app/services/api/api.service.ts @@ -26,6 +26,7 @@ export abstract class ApiService { abstract addForward(params: AddForwardReq): Promise // port-forward.add abstract deleteForward(params: DeleteForwardReq): Promise // port-forward.remove abstract updateForwardLabel(params: UpdateForwardLabelReq): Promise // port-forward.update-label + abstract setForwardEnabled(params: SetForwardEnabledReq): Promise // port-forward.set-enabled // update abstract checkUpdate(): Promise // update.check abstract applyUpdate(): Promise // update.apply @@ -73,6 +74,11 @@ export type UpdateForwardLabelReq = { label: string } +export type SetForwardEnabledReq = { + source: string + enabled: boolean +} + export type TunnelUpdateResult = { status: string installed: string diff --git a/web/projects/start-tunnel/src/app/services/api/live-api.service.ts b/web/projects/start-tunnel/src/app/services/api/live-api.service.ts index cb3896d97..f0ed98b97 100644 --- a/web/projects/start-tunnel/src/app/services/api/live-api.service.ts +++ b/web/projects/start-tunnel/src/app/services/api/live-api.service.ts @@ -17,6 +17,7 @@ import { LoginReq, SubscribeRes, TunnelUpdateResult, + SetForwardEnabledReq, UpdateForwardLabelReq, UpsertDeviceReq, UpsertSubnetReq, @@ -109,6 +110,10 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'port-forward.update-label', params }) } + async setForwardEnabled(params: SetForwardEnabledReq): Promise { + return this.rpcRequest({ method: 'port-forward.set-enabled', params }) + } + // update async checkUpdate(): Promise { diff --git a/web/projects/start-tunnel/src/app/services/api/mock-api.service.ts b/web/projects/start-tunnel/src/app/services/api/mock-api.service.ts index 1dc50bdef..d24562b9d 100644 --- a/web/projects/start-tunnel/src/app/services/api/mock-api.service.ts +++ b/web/projects/start-tunnel/src/app/services/api/mock-api.service.ts @@ -10,6 +10,7 @@ import { LoginReq, SubscribeRes, TunnelUpdateResult, + SetForwardEnabledReq, UpdateForwardLabelReq, UpsertDeviceReq, UpsertSubnetReq, @@ -181,7 +182,11 @@ export class MockApiService extends ApiService { { op: PatchOp.ADD, path: `/portForwards/${params.source}`, - value: { target: params.target, label: params.label || '' }, + value: { + target: params.target, + label: params.label || '', + enabled: true, + }, }, ] this.mockRevision(patch) @@ -204,6 +209,21 @@ export class MockApiService extends ApiService { return null } + async setForwardEnabled(params: SetForwardEnabledReq): Promise { + await pauseFor(1000) + + const patch: ReplaceOperation[] = [ + { + op: PatchOp.REPLACE, + path: `/portForwards/${params.source}/enabled`, + value: params.enabled, + }, + ] + this.mockRevision(patch) + + return null + } + async deleteForward(params: DeleteForwardReq): Promise { await pauseFor(1000) diff --git a/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts b/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts index c4c046c93..8bb5e23e0 100644 --- a/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts +++ b/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts @@ -3,6 +3,7 @@ import { T } from '@start9labs/start-sdk' export type PortForwardEntry = { target: string label: string + enabled: boolean } export type TunnelData = { @@ -44,8 +45,12 @@ export const mockTunnelData: TunnelData = { }, }, portForwards: { - '69.1.1.42:443': { target: '10.59.0.2:443', label: 'HTTPS' }, - '69.1.1.42:3000': { target: '10.59.0.2:3000', label: 'Grafana' }, + '69.1.1.42:443': { target: '10.59.0.2:443', label: 'HTTPS', enabled: true }, + '69.1.1.42:3000': { + target: '10.59.0.2:3000', + label: 'Grafana', + enabled: true, + }, }, gateways: { eth0: {