mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
Feature/start tunnel (#3037)
* fix live-build resolv.conf * improved debuggability * wip: start-tunnel * fixes for trixie and tor * non-free-firmware on trixie * wip * web server WIP * wip: tls refactor * FE patchdb, mocks, and most endpoints * fix editing records and patch mocks * refactor complete * finish api * build and formatter update * minor change toi viewing addresses and fix build * fixes * more providers * endpoint for getting config * fix tests * api fixes * wip: separate port forward controller into parts * simplify iptables rules * bump sdk * misc fixes * predict next subnet and ip, use wan ips, and form validation * refactor: break big components apart and address todos (#3043) * refactor: break big components apart and address todos * starttunnel readme, fix pf mocks, fix adding tor domain in startos --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * better tui * tui tweaks * fix: address comments * better regex for subnet * fixes * better validation * handle rpc errors * build fixes * fix: address comments (#3044) * fix: address comments * fix unread notification mocks * fix row click for notification --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * fix raspi build * fix build * fix build * fix build * fix build * try to fix build * fix tests * fix tests * fix rsync tests * delete useless effectful test --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
@@ -1,49 +1,61 @@
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
|
||||
use clap::Parser;
|
||||
use imbl_value::InternedString;
|
||||
use ipnet::Ipv4Net;
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::tunnel::context::TunnelContext;
|
||||
use crate::tunnel::wg::WgSubnetConfig;
|
||||
use crate::tunnel::wg::{WgConfig, WgSubnetClients, WgSubnetConfig};
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
|
||||
pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand("web", super::web::web_api::<C>())
|
||||
.subcommand(
|
||||
"db",
|
||||
super::db::db_api::<C>()
|
||||
.with_about("Commands to interact with the db i.e. dump and apply"),
|
||||
)
|
||||
.subcommand(
|
||||
"auth",
|
||||
super::auth::auth_api::<C>().with_about("Add or remove authorized clients"),
|
||||
)
|
||||
.subcommand(
|
||||
"subnet",
|
||||
subnet_api::<C>().with_about("Add, remove, or modify subnets"),
|
||||
)
|
||||
// .subcommand(
|
||||
// "port-forward",
|
||||
// ParentHandler::<C>::new()
|
||||
// .subcommand(
|
||||
// "add",
|
||||
// from_fn_async(add_forward)
|
||||
// .with_metadata("sync_db", Value::Bool(true))
|
||||
// .no_display()
|
||||
// .with_about("Add a new port forward")
|
||||
// .with_call_remote::<CliContext>(),
|
||||
// )
|
||||
// .subcommand(
|
||||
// "remove",
|
||||
// from_fn_async(remove_forward)
|
||||
// .with_metadata("sync_db", Value::Bool(true))
|
||||
// .no_display()
|
||||
// .with_about("Remove a port forward")
|
||||
// .with_call_remote::<CliContext>(),
|
||||
// ),
|
||||
// )
|
||||
.subcommand(
|
||||
"device",
|
||||
device_api::<C>().with_about("Add, remove, or list devices in subnets"),
|
||||
)
|
||||
.subcommand(
|
||||
"port-forward",
|
||||
ParentHandler::<C>::new()
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_forward)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Add a new port forward")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_forward)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove a port forward")
|
||||
.with_call_remote::<CliContext>(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubnetParams {
|
||||
subnet: Ipv4Net,
|
||||
}
|
||||
@@ -68,44 +80,89 @@ pub fn subnet_api<C: Context>() -> ParentHandler<C, SubnetParams> {
|
||||
.with_about("Remove a subnet")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
// .subcommand(
|
||||
// "set-default-forward-target",
|
||||
// from_fn_async(set_default_forward_target)
|
||||
// .with_metadata("sync_db", Value::Bool(true))
|
||||
// .no_display()
|
||||
// .with_about("Set the default target for port forwarding")
|
||||
// .with_call_remote::<CliContext>(),
|
||||
// )
|
||||
// .subcommand(
|
||||
// "add-device",
|
||||
// from_fn_async(add_device)
|
||||
// .with_metadata("sync_db", Value::Bool(true))
|
||||
// .no_display()
|
||||
// .with_about("Add a device to a subnet")
|
||||
// .with_call_remote::<CliContext>(),
|
||||
// )
|
||||
// .subcommand(
|
||||
// "remove-device",
|
||||
// from_fn_async(remove_device)
|
||||
// .with_metadata("sync_db", Value::Bool(true))
|
||||
// .no_display()
|
||||
// .with_about("Remove a device from a subnet")
|
||||
// .with_call_remote::<CliContext>(),
|
||||
// )
|
||||
}
|
||||
|
||||
pub fn device_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_device)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Add a device to a subnet")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_device)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove a device from a subnet")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_devices)
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
|
||||
use prettytable::*;
|
||||
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, res);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc => "NAME", "IP", "PUBLIC KEY"]);
|
||||
for (ip, config) in res.clients.0 {
|
||||
table.add_row(row![config.name, ip, config.key.verifying_key()]);
|
||||
}
|
||||
|
||||
table.print_tty(false)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.with_about("List devices in a subnet")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"show-config",
|
||||
from_fn_async(show_config)
|
||||
.with_about("Show the WireGuard configuration for a device")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddSubnetParams {
|
||||
name: InternedString,
|
||||
}
|
||||
|
||||
pub async fn add_subnet(
|
||||
ctx: TunnelContext,
|
||||
_: Empty,
|
||||
SubnetParams { subnet }: SubnetParams,
|
||||
AddSubnetParams { name }: AddSubnetParams,
|
||||
SubnetParams { mut subnet }: SubnetParams,
|
||||
) -> Result<(), Error> {
|
||||
if subnet.prefix_len() > 24 {
|
||||
return Err(Error::new(
|
||||
eyre!("invalid subnet"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
let addr = subnet
|
||||
.hosts()
|
||||
.next()
|
||||
.ok_or_else(|| Error::new(eyre!("invalid subnet"), ErrorKind::InvalidRequest))?;
|
||||
subnet = Ipv4Net::new_assert(addr, subnet.prefix_len());
|
||||
let server = ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let map = db.as_wg_mut().as_subnets_mut();
|
||||
if !map.contains_key(&subnet)? {
|
||||
map.insert(&subnet, &WgSubnetConfig::new())?;
|
||||
}
|
||||
map.upsert(&subnet, || {
|
||||
Ok(WgSubnetConfig::new(InternedString::default()))
|
||||
})?
|
||||
.as_name_mut()
|
||||
.ser(&name)?;
|
||||
db.as_wg().de()
|
||||
})
|
||||
.await
|
||||
@@ -128,3 +185,221 @@ pub async fn remove_subnet(
|
||||
.result?;
|
||||
server.sync().await
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddDeviceParams {
|
||||
subnet: Ipv4Net,
|
||||
name: InternedString,
|
||||
ip: Option<Ipv4Addr>,
|
||||
}
|
||||
|
||||
pub async fn add_device(
|
||||
ctx: TunnelContext,
|
||||
AddDeviceParams { subnet, name, ip }: AddDeviceParams,
|
||||
) -> Result<(), Error> {
|
||||
let server = ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_wg_mut()
|
||||
.as_subnets_mut()
|
||||
.as_idx_mut(&subnet)
|
||||
.or_not_found(&subnet)?
|
||||
.as_clients_mut()
|
||||
.mutate(|WgSubnetClients(clients)| {
|
||||
let ip = if let Some(ip) = ip {
|
||||
ip
|
||||
} else {
|
||||
subnet
|
||||
.hosts()
|
||||
.find(|ip| !clients.contains_key(ip) && *ip != subnet.addr())
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("no available ips in subnet"),
|
||||
ErrorKind::InvalidRequest,
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
if ip.octets()[3] == 0 || ip.octets()[3] == 255 {
|
||||
return Err(Error::new(eyre!("invalid ip"), ErrorKind::InvalidRequest));
|
||||
}
|
||||
if ip == subnet.addr() {
|
||||
return Err(Error::new(eyre!("invalid ip"), ErrorKind::InvalidRequest));
|
||||
}
|
||||
if !subnet.contains(&ip) {
|
||||
return Err(Error::new(
|
||||
eyre!("ip not in subnet"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
let client = clients
|
||||
.entry(ip)
|
||||
.or_insert_with(|| WgConfig::generate(name.clone()));
|
||||
client.name = name;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
db.as_wg().de()
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
server.sync().await
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveDeviceParams {
|
||||
subnet: Ipv4Net,
|
||||
ip: Ipv4Addr,
|
||||
}
|
||||
|
||||
pub async fn remove_device(
|
||||
ctx: TunnelContext,
|
||||
RemoveDeviceParams { subnet, ip }: RemoveDeviceParams,
|
||||
) -> Result<(), Error> {
|
||||
let server = ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_wg_mut()
|
||||
.as_subnets_mut()
|
||||
.as_idx_mut(&subnet)
|
||||
.or_not_found(&subnet)?
|
||||
.as_clients_mut()
|
||||
.remove(&ip)?
|
||||
.or_not_found(&ip)?;
|
||||
db.as_wg().de()
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
server.sync().await
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListDevicesParams {
|
||||
subnet: Ipv4Net,
|
||||
}
|
||||
|
||||
pub async fn list_devices(
|
||||
ctx: TunnelContext,
|
||||
ListDevicesParams { subnet }: ListDevicesParams,
|
||||
) -> Result<WgSubnetConfig, Error> {
|
||||
ctx.db
|
||||
.peek()
|
||||
.await
|
||||
.as_wg()
|
||||
.as_subnets()
|
||||
.as_idx(&subnet)
|
||||
.or_not_found(&subnet)?
|
||||
.de()
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ShowConfigParams {
|
||||
subnet: Ipv4Net,
|
||||
ip: Ipv4Addr,
|
||||
wan_addr: Option<IpAddr>,
|
||||
#[serde(rename = "__ConnectInfo_local_addr")]
|
||||
#[arg(skip)]
|
||||
local_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
pub async fn show_config(
|
||||
ctx: TunnelContext,
|
||||
ShowConfigParams {
|
||||
subnet,
|
||||
ip,
|
||||
wan_addr,
|
||||
local_addr,
|
||||
}: ShowConfigParams,
|
||||
) -> Result<String, Error> {
|
||||
let peek = ctx.db.peek().await;
|
||||
let wg = peek.as_wg();
|
||||
let client = wg
|
||||
.as_subnets()
|
||||
.as_idx(&subnet)
|
||||
.or_not_found(&subnet)?
|
||||
.as_clients()
|
||||
.as_idx(&ip)
|
||||
.or_not_found(&ip)?
|
||||
.de()?;
|
||||
let wan_addr = if let Some(wan_addr) = wan_addr.or(local_addr.map(|a| a.ip())).filter(|ip| {
|
||||
!ip.is_loopback()
|
||||
&& !match ip {
|
||||
IpAddr::V4(ipv4) => ipv4.is_private() || ipv4.is_link_local(),
|
||||
IpAddr::V6(ipv6) => ipv6.is_unique_local() || ipv6.is_unicast_link_local(),
|
||||
}
|
||||
}) {
|
||||
wan_addr
|
||||
} else if let Some(webserver) = peek.as_webserver().as_listen().de()? {
|
||||
webserver.ip()
|
||||
} else {
|
||||
ctx.net_iface
|
||||
.peek(|i| {
|
||||
i.iter().find_map(|(_, info)| {
|
||||
info.ip_info
|
||||
.as_ref()
|
||||
.filter(|_| info.public())
|
||||
.iter()
|
||||
.find_map(|info| info.subnets.iter().next())
|
||||
.copied()
|
||||
})
|
||||
})
|
||||
.or_not_found("a public IP address")?
|
||||
.addr()
|
||||
};
|
||||
Ok(client
|
||||
.client_config(
|
||||
ip,
|
||||
wg.as_key().de()?.verifying_key(),
|
||||
(wan_addr, wg.as_port().de()?).into(),
|
||||
)
|
||||
.to_string())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddPortForwardParams {
|
||||
source: SocketAddrV4,
|
||||
target: SocketAddrV4,
|
||||
}
|
||||
|
||||
pub async fn add_forward(
|
||||
ctx: TunnelContext,
|
||||
AddPortForwardParams { source, target }: AddPortForwardParams,
|
||||
) -> Result<(), Error> {
|
||||
let rc = ctx.forward.add_forward(source, target).await?;
|
||||
ctx.active_forwards.mutate(|m| {
|
||||
m.insert(source, rc);
|
||||
});
|
||||
|
||||
ctx.db
|
||||
.mutate(|db| db.as_port_forwards_mut().insert(&source, &target))
|
||||
.await
|
||||
.result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemovePortForwardParams {
|
||||
source: SocketAddrV4,
|
||||
}
|
||||
|
||||
pub async fn remove_forward(
|
||||
ctx: TunnelContext,
|
||||
RemovePortForwardParams { source, .. }: RemovePortForwardParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| db.as_port_forwards_mut().remove(&source))
|
||||
.await
|
||||
.result?;
|
||||
if let Some(rc) = ctx.active_forwards.mutate(|m| m.remove(&source)) {
|
||||
drop(rc);
|
||||
ctx.forward.gc().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user