feat: tunnel TS exports, port forward labels, and db migrations

- Add TS derive and type annotations to all tunnel API param structs
- Export tunnel bindings to a tunnel/ subdirectory with index generation
- Change port forward label from String to Option<String>
- Add TunnelDatabase::init() with default subnet creation
- Add tunnel migration framework with m_00_port_forward_entry migration
  to convert legacy string-only port forwards to the new entry format
This commit is contained in:
Aiden McClelland
2026-03-12 13:38:01 -06:00
parent fd54e9ca91
commit c485edfa12
32 changed files with 288 additions and 39 deletions

View File

@@ -283,6 +283,10 @@ core/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE)
rm -rf core/bindings
./core/build/build-ts.sh
ls core/bindings/*.ts | sed 's/core\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/bindings/index.ts
if [ -d core/bindings/tunnel ]; then \
ls core/bindings/tunnel/*.ts | sed 's/core\/bindings\/tunnel\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' > core/bindings/tunnel/index.ts; \
echo 'export * as Tunnel from "./tunnel";' >> core/bindings/index.ts; \
fi
npm --prefix sdk/base exec -- prettier --config=./sdk/base/package.json -w './core/bindings/**/*.ts'
touch core/bindings/index.ts

View File

@@ -5,6 +5,7 @@ use imbl_value::InternedString;
use ipnet::Ipv4Net;
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::context::CliContext;
use crate::db::model::public::NetworkInterfaceType;
@@ -90,9 +91,10 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
)
}
#[derive(Deserialize, Serialize, Parser)]
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct SubnetParams {
#[ts(type = "string")]
subnet: Ipv4Net,
}
@@ -168,7 +170,7 @@ pub fn device_api<C: Context>() -> ParentHandler<C> {
)
}
#[derive(Deserialize, Serialize, Parser)]
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct AddSubnetParams {
name: InternedString,
@@ -293,11 +295,13 @@ pub async fn remove_subnet(
Ok(())
}
#[derive(Deserialize, Serialize, Parser)]
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct AddDeviceParams {
#[ts(type = "string")]
subnet: Ipv4Net,
name: InternedString,
#[ts(type = "string | null")]
ip: Option<Ipv4Addr>,
}
@@ -354,10 +358,12 @@ pub async fn add_device(
server.sync().await
}
#[derive(Deserialize, Serialize, Parser)]
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct RemoveDeviceParams {
#[ts(type = "string")]
subnet: Ipv4Net,
#[ts(type = "string")]
ip: Ipv4Addr,
}
@@ -383,9 +389,10 @@ pub async fn remove_device(
ctx.gc_forwards(&keep).await
}
#[derive(Deserialize, Serialize, Parser)]
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct ListDevicesParams {
#[ts(type = "string")]
subnet: Ipv4Net,
}
@@ -403,14 +410,18 @@ pub async fn list_devices(
.de()
}
#[derive(Deserialize, Serialize, Parser)]
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct ShowConfigParams {
#[ts(type = "string")]
subnet: Ipv4Net,
#[ts(type = "string")]
ip: Ipv4Addr,
#[ts(type = "string | null")]
wan_addr: Option<IpAddr>,
#[serde(rename = "__ConnectInfo_local_addr")]
#[arg(skip)]
#[ts(skip)]
local_addr: Option<SocketAddr>,
}
@@ -465,13 +476,15 @@ pub async fn show_config(
.to_string())
}
#[derive(Deserialize, Serialize, Parser)]
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct AddPortForwardParams {
#[ts(type = "string")]
source: SocketAddrV4,
#[ts(type = "string")]
target: SocketAddrV4,
#[arg(long)]
label: String,
label: Option<String>,
}
pub async fn add_forward(
@@ -505,7 +518,11 @@ pub async fn add_forward(
m.insert(source, rc);
});
let entry = PortForwardEntry { target, label, enabled: true };
let entry = PortForwardEntry {
target,
label,
enabled: true,
};
ctx.db
.mutate(|db| {
@@ -528,9 +545,10 @@ pub async fn add_forward(
Ok(())
}
#[derive(Deserialize, Serialize, Parser)]
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct RemovePortForwardParams {
#[ts(type = "string")]
source: SocketAddrV4,
}
@@ -549,11 +567,12 @@ pub async fn remove_forward(
Ok(())
}
#[derive(Deserialize, Serialize, Parser)]
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct UpdatePortForwardLabelParams {
#[ts(type = "string")]
source: SocketAddrV4,
label: String,
label: Option<String>,
}
pub async fn update_forward_label(
@@ -569,7 +588,7 @@ pub async fn update_forward_label(
ErrorKind::NotFound,
)
})?;
entry.label = label.clone();
entry.label = label;
Ok(())
})
})
@@ -577,9 +596,10 @@ pub async fn update_forward_label(
.result
}
#[derive(Deserialize, Serialize, Parser)]
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
pub struct SetPortForwardEnabledParams {
#[ts(type = "string")]
source: SocketAddrV4,
enabled: bool,
}

View File

@@ -10,8 +10,8 @@ use http::HeaderMap;
use imbl::OrdMap;
use imbl_value::InternedString;
use include_dir::Dir;
use ipnet::Ipv4Net;
use patch_db::PatchDb;
use patch_db::json_ptr::ROOT;
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{CallRemote, Context, Empty, ParentHandler};
use serde::{Deserialize, Serialize};
@@ -34,7 +34,8 @@ use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations};
use crate::tunnel::TUNNEL_DEFAULT_LISTEN;
use crate::tunnel::api::tunnel_api;
use crate::tunnel::db::TunnelDatabase;
use crate::tunnel::wg::{WIREGUARD_INTERFACE_NAME, WgSubnetConfig};
use crate::tunnel::migrations::run_migrations;
use crate::tunnel::wg::WIREGUARD_INTERFACE_NAME;
use crate::util::collections::OrdMapIterMut;
use crate::util::io::read_file_to_string;
use crate::util::sync::{SyncMutex, Watch};
@@ -98,21 +99,11 @@ impl TunnelContext {
tokio::fs::create_dir_all(&datadir).await?;
}
let db_path = datadir.join("tunnel.db");
let db = TypedPatchDb::<TunnelDatabase>::load_or_init(
PatchDb::open(&db_path).await?,
|| async {
let mut db = TunnelDatabase::default();
db.wg.subnets.0.insert(
Ipv4Net::new_assert([10, 59, rand::random(), 1].into(), 24),
WgSubnetConfig {
name: "Default Subnet".into(),
..Default::default()
},
);
Ok(db)
},
)
.await?;
let db = TypedPatchDb::<TunnelDatabase>::load_unchecked(PatchDb::open(&db_path).await?);
if db.dump(&ROOT).await.value.is_null() {
db.put(&ROOT, &TunnelDatabase::init()).await?;
}
db.mutate(|db| run_migrations(db)).await.result?;
let listen = config.tunnel_listen.unwrap_or(TUNNEL_DEFAULT_LISTEN);
let ip_info = crate::net::utils::load_ip_info().await?;
let net_iface = db

View File

@@ -7,6 +7,7 @@ use axum::extract::ws;
use clap::Parser;
use imbl::{HashMap, OrdMap};
use imbl_value::InternedString;
use ipnet::Ipv4Net;
use itertools::Itertools;
use patch_db::Dump;
use patch_db::json_ptr::{JsonPointer, ROOT};
@@ -25,25 +26,49 @@ use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::sign::AnyVerifyingKey;
use crate::tunnel::auth::SignerInfo;
use crate::tunnel::context::TunnelContext;
use crate::tunnel::migrations;
use crate::tunnel::web::WebserverInfo;
use crate::tunnel::wg::WgServer;
use crate::tunnel::wg::{WgServer, WgSubnetConfig};
use crate::util::serde::{HandlerExtSerde, apply_expr};
#[derive(Default, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
pub struct TunnelDatabase {
#[serde(default)]
#[ts(skip)]
pub migrations: BTreeSet<InternedString>,
pub webserver: WebserverInfo,
pub sessions: Sessions,
pub password: Option<String>,
#[ts(as = "std::collections::HashMap::<AnyVerifyingKey, SignerInfo>")]
pub auth_pubkeys: HashMap<AnyVerifyingKey, SignerInfo>,
#[ts(as = "std::collections::BTreeMap::<AnyVerifyingKey, SignerInfo>")]
#[ts(as = "std::collections::BTreeMap::<GatewayId, NetworkInterfaceInfo>")]
pub gateways: OrdMap<GatewayId, NetworkInterfaceInfo>,
pub wg: WgServer,
pub port_forwards: PortForwards,
}
impl TunnelDatabase {
pub fn init() -> Self {
let mut db = Self {
migrations: migrations::MIGRATIONS
.iter()
.map(|m| m.name().into())
.collect(),
..Default::default()
};
db.wg.subnets.0.insert(
Ipv4Net::new_assert([10, 59, rand::random(), 1].into(), 24),
WgSubnetConfig {
name: "Default Subnet".into(),
..Default::default()
},
);
db
}
}
impl Model<TunnelDatabase> {
pub fn gc_forwards(&mut self) -> Result<BTreeSet<SocketAddrV4>, Error> {
let mut keep_sources = BTreeSet::new();
@@ -67,15 +92,30 @@ impl Model<TunnelDatabase> {
#[test]
fn export_bindings_tunnel_db() {
use crate::tunnel::api::*;
use crate::tunnel::auth::{AddKeyParams, RemoveKeyParams, SetPasswordParams};
TunnelDatabase::export_all_to("bindings/tunnel").unwrap();
SubnetParams::export_all_to("bindings/tunnel").unwrap();
AddSubnetParams::export_all_to("bindings/tunnel").unwrap();
AddDeviceParams::export_all_to("bindings/tunnel").unwrap();
RemoveDeviceParams::export_all_to("bindings/tunnel").unwrap();
ListDevicesParams::export_all_to("bindings/tunnel").unwrap();
ShowConfigParams::export_all_to("bindings/tunnel").unwrap();
AddPortForwardParams::export_all_to("bindings/tunnel").unwrap();
RemovePortForwardParams::export_all_to("bindings/tunnel").unwrap();
UpdatePortForwardLabelParams::export_all_to("bindings/tunnel").unwrap();
SetPortForwardEnabledParams::export_all_to("bindings/tunnel").unwrap();
AddKeyParams::export_all_to("bindings/tunnel").unwrap();
RemoveKeyParams::export_all_to("bindings/tunnel").unwrap();
SetPasswordParams::export_all_to("bindings/tunnel").unwrap();
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct PortForwardEntry {
pub target: SocketAddrV4,
#[serde(default)]
pub label: String,
pub label: Option<String>,
#[serde(default = "default_true")]
pub enabled: bool,
}

View File

@@ -0,0 +1,20 @@
use imbl_value::json;
use super::TunnelMigration;
use crate::prelude::*;
pub struct PortForwardEntry;
impl TunnelMigration for PortForwardEntry {
fn action(&self, db: &mut Value) -> Result<(), Error> {
for (_, value) in db["portForwards"].as_object_mut().unwrap().iter_mut() {
if value.is_string() {
*value = json!({
"target": value.clone(),
"label": null,
"enabled": true,
});
}
}
Ok(())
}
}

View File

@@ -0,0 +1,34 @@
use patch_db::ModelExt;
use crate::prelude::*;
use crate::tunnel::db::TunnelDatabase;
mod m_00_port_forward_entry;
pub trait TunnelMigration {
fn name(&self) -> &'static str {
let val = std::any::type_name_of_val(self);
val.rsplit_once("::").map_or(val, |v| v.1)
}
fn action(&self, db: &mut Value) -> Result<(), Error>;
}
pub const MIGRATIONS: &[&dyn TunnelMigration] = &[
&m_00_port_forward_entry::PortForwardEntry,
];
#[instrument(skip_all)]
pub fn run_migrations(db: &mut Model<TunnelDatabase>) -> Result<(), Error> {
let mut migrations = db.as_migrations().de().unwrap_or_default();
for migration in MIGRATIONS {
let name = migration.name();
if !migrations.contains(name) {
migration.action(ModelExt::as_value_mut(db))?;
migrations.insert(name.into());
}
}
let mut db_deser = db.de()?;
db_deser.migrations = migrations;
db.ser(&db_deser)?;
Ok(())
}

View File

@@ -9,6 +9,7 @@ pub mod api;
pub mod auth;
pub mod context;
pub mod db;
pub(crate) mod migrations;
pub mod update;
pub mod web;
pub mod wg;

View File

@@ -6,5 +6,5 @@ export type AddPackageSignerParams = {
id: PackageId
signer: Guid
versions: string | null
merge?: boolean
merge: boolean
}

View File

@@ -26,7 +26,7 @@ export type ServerInfo = {
zram: boolean
governor: Governor | null
smtp: SmtpValue | null
ifconfigUrl: string
echoipUrls: string[]
ram: number
devices: Array<LshwDevice>
kiosk: boolean | null

View File

@@ -306,3 +306,4 @@ export { WifiInfo } from './WifiInfo'
export { WifiListInfo } from './WifiListInfo'
export { WifiListOut } from './WifiListOut'
export { WifiSsidParams } from './WifiSsidParams'
export * as Tunnel from './tunnel'

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AddDeviceParams = {
subnet: string
name: string
ip: string | null
}

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnyVerifyingKey } from './AnyVerifyingKey'
export type AddKeyParams = { name: string; key: AnyVerifyingKey }

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AddPortForwardParams = {
source: string
target: string
label: string | null
}

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AddSubnetParams = { name: string }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type GatewayId = string

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type GatewayType = 'inbound-outbound' | 'outbound-only'

View File

@@ -0,0 +1,13 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { NetworkInterfaceType } from './NetworkInterfaceType'
export type IpInfo = {
name: string
scopeId: number
deviceType: NetworkInterfaceType | null
subnets: string[]
lanIp: string[]
wanIp: string | null
ntpServers: string[]
dnsServers: string[]
}

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ListDevicesParams = { subnet: string }

View File

@@ -0,0 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayType } from './GatewayType'
import type { IpInfo } from './IpInfo'
export type NetworkInterfaceInfo = {
name: string | null
secure: boolean | null
ipInfo: IpInfo | null
type: GatewayType | null
}

View File

@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NetworkInterfaceType =
| 'ethernet'
| 'wireless'
| 'bridge'
| 'wireguard'
| 'loopback'

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PortForwardEntry = {
target: string
label: string | null
enabled: boolean
}

View File

@@ -1,3 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PortForwardEntry } from './PortForwardEntry'
export type PortForwards = { [key: string]: string }
export type PortForwards = { [key: string]: PortForwardEntry }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RemoveDeviceParams = { subnet: string; ip: string }

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnyVerifyingKey } from './AnyVerifyingKey'
export type RemoveKeyParams = { key: AnyVerifyingKey }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RemovePortForwardParams = { source: string }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SetPasswordParams = { password: string }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SetPortForwardEnabledParams = { source: string; enabled: boolean }

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ShowConfigParams = {
subnet: string
ip: string
wanAddr: string | null
}

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SubnetParams = { subnet: string }

View File

@@ -1,5 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnyVerifyingKey } from './AnyVerifyingKey'
import type { GatewayId } from './GatewayId'
import type { NetworkInterfaceInfo } from './NetworkInterfaceInfo'
import type { PortForwards } from './PortForwards'
import type { Sessions } from './Sessions'
import type { SignerInfo } from './SignerInfo'
@@ -11,7 +13,7 @@ export type TunnelDatabase = {
sessions: Sessions
password: string | null
authPubkeys: { [key: AnyVerifyingKey]: SignerInfo }
gateways: { [key: AnyVerifyingKey]: SignerInfo }
gateways: { [key: GatewayId]: NetworkInterfaceInfo }
wg: WgServer
portForwards: PortForwards
}

View File

@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UpdatePortForwardLabelParams = {
source: string
label: string | null
}

View File

@@ -0,0 +1,35 @@
export { AddDeviceParams } from './AddDeviceParams'
export { AddKeyParams } from './AddKeyParams'
export { AddPortForwardParams } from './AddPortForwardParams'
export { AddSubnetParams } from './AddSubnetParams'
export { AnyVerifyingKey } from './AnyVerifyingKey'
export { Base64 } from './Base64'
export { GatewayId } from './GatewayId'
export { GatewayType } from './GatewayType'
export { IpInfo } from './IpInfo'
export { ListDevicesParams } from './ListDevicesParams'
export { NetworkInterfaceInfo } from './NetworkInterfaceInfo'
export { NetworkInterfaceType } from './NetworkInterfaceType'
export { Pem } from './Pem'
export { PortForwardEntry } from './PortForwardEntry'
export { PortForwards } from './PortForwards'
export { RemoveDeviceParams } from './RemoveDeviceParams'
export { RemoveKeyParams } from './RemoveKeyParams'
export { RemovePortForwardParams } from './RemovePortForwardParams'
export { Sessions } from './Sessions'
export { Session } from './Session'
export { SetPasswordParams } from './SetPasswordParams'
export { SetPortForwardEnabledParams } from './SetPortForwardEnabledParams'
export { ShowConfigParams } from './ShowConfigParams'
export { SignerInfo } from './SignerInfo'
export { SubnetParams } from './SubnetParams'
export { TunnelCertData } from './TunnelCertData'
export { TunnelDatabase } from './TunnelDatabase'
export { TunnelUpdateResult } from './TunnelUpdateResult'
export { UpdatePortForwardLabelParams } from './UpdatePortForwardLabelParams'
export { WebserverInfo } from './WebserverInfo'
export { WgConfig } from './WgConfig'
export { WgServer } from './WgServer'
export { WgSubnetClients } from './WgSubnetClients'
export { WgSubnetConfig } from './WgSubnetConfig'
export { WgSubnetMap } from './WgSubnetMap'