mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
20
core/src/tunnel/migrations/m_00_port_forward_entry.rs
Normal file
20
core/src/tunnel/migrations/m_00_port_forward_entry.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
34
core/src/tunnel/migrations/mod.rs
Normal file
34
core/src/tunnel/migrations/mod.rs
Normal 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(())
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user