enable-disable forwards

This commit is contained in:
Matt Hill
2026-03-10 09:34:09 -06:00
parent 30f6492abc
commit c10fb66fa0
9 changed files with 165 additions and 6 deletions

View File

@@ -60,6 +60,14 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
.no_display() .no_display()
.with_about("about.update-port-forward-label") .with_about("about.update-port-forward-label")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
)
.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::<CliContext>(),
), ),
) )
.subcommand( .subcommand(
@@ -497,7 +505,7 @@ pub async fn add_forward(
m.insert(source, rc); m.insert(source, rc);
}); });
let entry = PortForwardEntry { target, label }; let entry = PortForwardEntry { target, label, enabled: true };
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
@@ -568,3 +576,64 @@ pub async fn update_forward_label(
.await .await
.result .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(())
}

View File

@@ -185,6 +185,9 @@ impl TunnelContext {
let mut active_forwards = BTreeMap::new(); let mut active_forwards = BTreeMap::new();
for (from, entry) in peek.as_port_forwards().de()?.0 { for (from, entry) in peek.as_port_forwards().de()?.0 {
if !entry.enabled {
continue;
}
let to = entry.target; let to = entry.target;
let prefix = net_iface let prefix = net_iface
.peek(|i| { .peek(|i| {

View File

@@ -76,6 +76,12 @@ pub struct PortForwardEntry {
pub target: SocketAddrV4, pub target: SocketAddrV4,
#[serde(default)] #[serde(default)]
pub label: String, pub label: String,
#[serde(default = "default_true")]
pub enabled: bool,
}
fn default_true() -> bool {
true
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]

View File

@@ -3,19 +3,22 @@ import {
Component, Component,
computed, computed,
inject, inject,
signal,
Signal, Signal,
} from '@angular/core' } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { FormsModule } from '@angular/forms'
import { ErrorService, LoadingService } from '@start9labs/shared' import { ErrorService, LoadingService } from '@start9labs/shared'
import { utils } from '@start9labs/start-sdk' import { utils } from '@start9labs/start-sdk'
import { import {
TuiButton, TuiButton,
TuiDataList, TuiDataList,
TuiDropdown, TuiDropdown,
TuiLoader,
TuiTextfield, TuiTextfield,
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { TuiDialogService } from '@taiga-ui/experimental' 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 { PatchDB } from 'patch-db-client'
import { filter, map } from 'rxjs' import { filter, map } from 'rxjs'
import { PORT_FORWARDS_ADD } from 'src/app/routes/home/routes/port-forwards/add' import { PORT_FORWARDS_ADD } from 'src/app/routes/home/routes/port-forwards/add'
@@ -30,6 +33,7 @@ import { MappedDevice, MappedForward } from './utils'
<table class="g-table"> <table class="g-table">
<thead> <thead>
<tr> <tr>
<th></th>
<th>Label</th> <th>Label</th>
<th>External IP</th> <th>External IP</th>
<th>External Port</th> <th>External Port</th>
@@ -45,6 +49,22 @@ import { MappedDevice, MappedForward } from './utils'
<tbody> <tbody>
@for (forward of forwards(); track $index) { @for (forward of forwards(); track $index) {
<tr> <tr>
<td>
<tui-loader
[showLoader]="toggling() === $index"
size="xs"
[overlay]="true"
>
<input
tuiSwitch
type="checkbox"
size="s"
[showIcons]="false"
[ngModel]="forward.enabled"
(ngModelChange)="onToggle(forward, $index)"
/>
</tui-loader>
</td>
<td>{{ forward.label || '—' }}</td> <td>{{ forward.label || '—' }}</td>
<td>{{ forward.externalip }}</td> <td>{{ forward.externalip }}</td>
<td>{{ forward.externalport }}</td> <td>{{ forward.externalport }}</td>
@@ -88,7 +108,15 @@ import { MappedDevice, MappedForward } from './utils'
</table> </table>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButton, TuiDropdown, TuiDataList, TuiTextfield], imports: [
FormsModule,
TuiButton,
TuiDropdown,
TuiDataList,
TuiLoader,
TuiSwitch,
TuiTextfield,
],
}) })
export default class PortForwards { export default class PortForwards {
private readonly dialogs = inject(TuiDialogService) private readonly dialogs = inject(TuiDialogService)
@@ -136,10 +164,26 @@ export default class PortForwards {
device: this.devices().find(d => d.ip === targetSplit[0])!, device: this.devices().find(d => d.ip === targetSplit[0])!,
internalport: targetSplit[1]!, internalport: targetSplit[1]!,
label: entry.label, label: entry.label,
enabled: entry.enabled,
} }
}), }),
) )
protected readonly toggling = signal<number | null>(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 { protected onAdd(): void {
this.dialogs this.dialogs
.open(PORT_FORWARDS_ADD, { .open(PORT_FORWARDS_ADD, {

View File

@@ -11,6 +11,7 @@ export interface MappedForward {
readonly device: MappedDevice readonly device: MappedDevice
readonly internalport: string readonly internalport: string
readonly label: string readonly label: string
readonly enabled: boolean
} }
export interface PortForwardsData { export interface PortForwardsData {

View File

@@ -26,6 +26,7 @@ export abstract class ApiService {
abstract addForward(params: AddForwardReq): Promise<null> // port-forward.add abstract addForward(params: AddForwardReq): Promise<null> // port-forward.add
abstract deleteForward(params: DeleteForwardReq): Promise<null> // port-forward.remove abstract deleteForward(params: DeleteForwardReq): Promise<null> // port-forward.remove
abstract updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> // port-forward.update-label abstract updateForwardLabel(params: UpdateForwardLabelReq): Promise<null> // port-forward.update-label
abstract setForwardEnabled(params: SetForwardEnabledReq): Promise<null> // port-forward.set-enabled
// update // update
abstract checkUpdate(): Promise<TunnelUpdateResult> // update.check abstract checkUpdate(): Promise<TunnelUpdateResult> // update.check
abstract applyUpdate(): Promise<TunnelUpdateResult> // update.apply abstract applyUpdate(): Promise<TunnelUpdateResult> // update.apply
@@ -73,6 +74,11 @@ export type UpdateForwardLabelReq = {
label: string label: string
} }
export type SetForwardEnabledReq = {
source: string
enabled: boolean
}
export type TunnelUpdateResult = { export type TunnelUpdateResult = {
status: string status: string
installed: string installed: string

View File

@@ -17,6 +17,7 @@ import {
LoginReq, LoginReq,
SubscribeRes, SubscribeRes,
TunnelUpdateResult, TunnelUpdateResult,
SetForwardEnabledReq,
UpdateForwardLabelReq, UpdateForwardLabelReq,
UpsertDeviceReq, UpsertDeviceReq,
UpsertSubnetReq, UpsertSubnetReq,
@@ -109,6 +110,10 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'port-forward.update-label', params }) return this.rpcRequest({ method: 'port-forward.update-label', params })
} }
async setForwardEnabled(params: SetForwardEnabledReq): Promise<null> {
return this.rpcRequest({ method: 'port-forward.set-enabled', params })
}
// update // update
async checkUpdate(): Promise<TunnelUpdateResult> { async checkUpdate(): Promise<TunnelUpdateResult> {

View File

@@ -10,6 +10,7 @@ import {
LoginReq, LoginReq,
SubscribeRes, SubscribeRes,
TunnelUpdateResult, TunnelUpdateResult,
SetForwardEnabledReq,
UpdateForwardLabelReq, UpdateForwardLabelReq,
UpsertDeviceReq, UpsertDeviceReq,
UpsertSubnetReq, UpsertSubnetReq,
@@ -181,7 +182,11 @@ export class MockApiService extends ApiService {
{ {
op: PatchOp.ADD, op: PatchOp.ADD,
path: `/portForwards/${params.source}`, path: `/portForwards/${params.source}`,
value: { target: params.target, label: params.label || '' }, value: {
target: params.target,
label: params.label || '',
enabled: true,
},
}, },
] ]
this.mockRevision(patch) this.mockRevision(patch)
@@ -204,6 +209,21 @@ export class MockApiService extends ApiService {
return null return null
} }
async setForwardEnabled(params: SetForwardEnabledReq): Promise<null> {
await pauseFor(1000)
const patch: ReplaceOperation<boolean>[] = [
{
op: PatchOp.REPLACE,
path: `/portForwards/${params.source}/enabled`,
value: params.enabled,
},
]
this.mockRevision(patch)
return null
}
async deleteForward(params: DeleteForwardReq): Promise<null> { async deleteForward(params: DeleteForwardReq): Promise<null> {
await pauseFor(1000) await pauseFor(1000)

View File

@@ -3,6 +3,7 @@ import { T } from '@start9labs/start-sdk'
export type PortForwardEntry = { export type PortForwardEntry = {
target: string target: string
label: string label: string
enabled: boolean
} }
export type TunnelData = { export type TunnelData = {
@@ -44,8 +45,12 @@ export const mockTunnelData: TunnelData = {
}, },
}, },
portForwards: { portForwards: {
'69.1.1.42:443': { target: '10.59.0.2:443', label: 'HTTPS' }, '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' }, '69.1.1.42:3000': {
target: '10.59.0.2:3000',
label: 'Grafana',
enabled: true,
},
}, },
gateways: { gateways: {
eth0: { eth0: {