proxy -> tunnel, implement backend apis

This commit is contained in:
Aiden McClelland
2025-07-23 15:44:57 -06:00
parent 21adce5c5d
commit 84f554269f
14 changed files with 337 additions and 64 deletions

View File

@@ -12,6 +12,7 @@ pub mod service_interface;
pub mod ssl;
pub mod static_server;
pub mod tor;
pub mod tunnel;
pub mod utils;
pub mod vhost;
pub mod web_server;
@@ -32,6 +33,10 @@ pub fn net<C: Context>() -> ParentHandler<C> {
network_interface::network_interface_api::<C>()
.with_about("View and edit network interface configurations"),
)
.subcommand(
"tunnel",
tunnel::tunnel_api::<C>().with_about("Manage tunnels"),
)
.subcommand(
"vhost",
vhost::vhost_api::<C>().with_about("Manage ssl virtual host proxy"),

View File

@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6};
use std::sync::{Arc, Weak};
use std::task::Poll;
@@ -28,6 +28,7 @@ use crate::context::{CliContext, RpcContext};
use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType};
use crate::db::model::Database;
use crate::net::forward::START9_BRIDGE_IFACE;
use crate::net::network_interface::device::DeviceProxy;
use crate::net::utils::{ipv6_is_link_local, ipv6_is_local};
use crate::net::web_server::Accept;
use crate::prelude::*;
@@ -86,15 +87,15 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
.with_call_remote::<CliContext>(),
)
.subcommand(
"set-inbound",
from_fn_async(set_inbound)
"set-public",
from_fn_async(set_public)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Indicate whether this interface has inbound access from the WAN")
.with_call_remote::<CliContext>(),
).subcommand(
"unset-inbound",
from_fn_async(unset_inbound)
from_fn_async(unset_public)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Allow this interface to infer whether it has inbound access from the WAN based on its IPv4 address")
@@ -105,7 +106,13 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
.no_display()
.with_about("Forget a disconnected interface")
.with_call_remote::<CliContext>()
)
).subcommand("set-name",
from_fn_async(set_name)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Rename an interface")
.with_call_remote::<CliContext>()
)
}
async fn list_interfaces(
@@ -116,19 +123,19 @@ async fn list_interfaces(
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
struct NetworkInterfaceSetInboundParams {
struct NetworkInterfaceSetPublicParams {
#[ts(type = "string")]
interface: InternedString,
inbound: Option<bool>,
public: Option<bool>,
}
async fn set_inbound(
async fn set_public(
ctx: RpcContext,
NetworkInterfaceSetInboundParams { interface, inbound }: NetworkInterfaceSetInboundParams,
NetworkInterfaceSetPublicParams { interface, public }: NetworkInterfaceSetPublicParams,
) -> Result<(), Error> {
ctx.net_controller
.net_iface
.set_inbound(&interface, Some(inbound.unwrap_or(true)))
.set_public(&interface, Some(public.unwrap_or(true)))
.await
}
@@ -139,13 +146,13 @@ struct UnsetInboundParams {
interface: InternedString,
}
async fn unset_inbound(
async fn unset_public(
ctx: RpcContext,
UnsetInboundParams { interface }: UnsetInboundParams,
) -> Result<(), Error> {
ctx.net_controller
.net_iface
.set_inbound(&interface, None)
.set_public(&interface, None)
.await
}
@@ -163,12 +170,32 @@ async fn forget_iface(
ctx.net_controller.net_iface.forget(&interface).await
}
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
struct RenameInterfaceParams {
#[ts(type = "string")]
interface: InternedString,
name: String,
}
async fn set_name(
ctx: RpcContext,
RenameInterfaceParams { interface, name }: RenameInterfaceParams,
) -> Result<(), Error> {
ctx.net_controller
.net_iface
.set_name(&interface, &name)
.await
}
#[proxy(
interface = "org.freedesktop.NetworkManager",
default_service = "org.freedesktop.NetworkManager",
default_path = "/org/freedesktop/NetworkManager"
)]
trait NetworkManager {
fn get_device_by_ip_iface(&self, iface: &str) -> Result<OwnedObjectPath, Error>;
#[zbus(property)]
fn all_devices(&self) -> Result<Vec<OwnedObjectPath>, Error>;
@@ -193,6 +220,9 @@ mod active_connection {
default_service = "org.freedesktop.NetworkManager"
)]
pub trait ActiveConnection {
#[zbus(property)]
fn connection(&self) -> Result<OwnedObjectPath, Error>;
#[zbus(property)]
fn id(&self) -> Result<String, Error>;
@@ -210,6 +240,19 @@ mod active_connection {
}
}
#[proxy(
interface = "org.freedesktop.NetworkManager.Settings.Connection",
default_service = "org.freedesktop.NetworkManager"
)]
trait ConnectionSettings {
fn update2(
&self,
settings: HashMap<String, HashMap<String, ZValue<'_>>>,
flags: u32,
args: HashMap<String, ZValue<'_>>,
) -> Result<(), Error>;
}
#[proxy(
interface = "org.freedesktop.NetworkManager.IP4Config",
default_service = "org.freedesktop.NetworkManager"
@@ -276,6 +319,8 @@ mod device {
default_service = "org.freedesktop.NetworkManager"
)]
pub trait Device {
fn delete(&self) -> Result<(), Error>;
#[zbus(property)]
fn ip_interface(&self) -> Result<String, Error>;
@@ -836,7 +881,7 @@ impl NetworkInterfaceController {
Ok(listener)
}
pub async fn set_inbound(
pub async fn set_public(
&self,
interface: &InternedString,
public: Option<bool>,
@@ -904,6 +949,96 @@ impl NetworkInterfaceController {
}
Ok(())
}
pub async fn delete_iface(&self, interface: &InternedString) -> Result<(), Error> {
let Some(has_ip_info) = self
.ip_info
.peek(|ifaces| ifaces.get(interface).map(|i| i.ip_info.is_some()))
else {
return Ok(());
};
if has_ip_info {
let mut ip_info = self.ip_info.clone_unseen();
let connection = Connection::system().await?;
let netman_proxy = NetworkManagerProxy::new(&connection).await?;
let device = Some(netman_proxy.get_device_by_ip_iface(&**interface).await?)
.filter(|o| &**o != "/")
.or_not_found(lazy_format!("{interface} in NetworkManager"))?;
let device_proxy = DeviceProxy::new(&connection, device).await?;
device_proxy.delete().await?;
ip_info
.wait_for(|ifaces| ifaces.get(interface).map_or(true, |i| i.ip_info.is_none()))
.await;
}
self.forget(interface).await?;
Ok(())
}
pub async fn set_name(&self, interface: &InternedString, name: &str) -> Result<(), Error> {
let (dump, mut sub) = self
.db
.dump_and_sub(
"/public/serverInfo/network/networkInterfaces"
.parse::<JsonPointer<_, _>>()
.with_kind(ErrorKind::Database)?
.join_end(&**interface)
.join_end("ipInfo")
.join_end("name"),
)
.await;
let change = dump.value.as_str().or_not_found(interface)? != name;
if !change {
return Ok(());
}
let connection = Connection::system().await?;
let netman_proxy = NetworkManagerProxy::new(&connection).await?;
let device = Some(netman_proxy.get_device_by_ip_iface(&**interface).await?)
.filter(|o| &**o != "/")
.or_not_found(lazy_format!("{interface} in NetworkManager"))?;
let device_proxy = DeviceProxy::new(&connection, device).await?;
let dac = Some(device_proxy.active_connection().await?)
.filter(|o| &**o != "/")
.or_not_found(lazy_format!("ActiveConnection for {interface}"))?;
let dac_proxy = active_connection::ActiveConnectionProxy::new(&connection, dac).await?;
let settings = Some(dac_proxy.connection().await?)
.filter(|o| &**o != "/")
.or_not_found(lazy_format!("ConnectionSettings for {interface}"))?;
let settings_proxy = ConnectionSettingsProxy::new(&connection, settings).await?;
settings_proxy.update2(
[(
"connection".into(),
[("id".into(), zbus::zvariant::Value::Str(name.into()))]
.into_iter()
.collect(),
)]
.into_iter()
.collect(),
0x1,
HashMap::new(),
);
sub.recv().await;
Ok(())
}
}
struct ListenerMap {

View File

@@ -0,0 +1,125 @@
use clap::Parser;
use imbl_value::InternedString;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use ts_rs::TS;
use crate::context::{CliContext, RpcContext};
use crate::db::model::public::NetworkInterfaceType;
use crate::prelude::*;
use crate::util::io::{write_file_atomic, TmpDir};
use crate::util::Invoke;
pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"add",
from_fn_async(add_tunnel)
.with_about("Add a new tunnel")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove_tunnel)
.no_display()
.with_about("Remove a tunnel")
.with_call_remote::<CliContext>(),
)
}
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct AddTunnelParams {
#[ts(type = "string")]
name: InternedString,
config: String,
public: bool,
}
pub async fn add_tunnel(
ctx: RpcContext,
AddTunnelParams {
name,
config,
public,
}: AddTunnelParams,
) -> Result<InternedString, Error> {
let existing = ctx
.db
.peek()
.await
.into_public()
.into_server_info()
.into_network()
.into_network_interfaces()
.keys()?;
let mut iface = InternedString::intern("wg0");
for id in 1.. {
if !existing.contains(&iface) {
break;
}
iface = InternedString::from_display(&lazy_format!("wg{id}"));
}
let tmpdir = TmpDir::new().await?;
let conf = tmpdir.join(&*iface).with_extension("conf");
write_file_atomic(&conf, &config).await?;
let mut ifaces = ctx.net_controller.net_iface.subscribe();
Command::new("nmcli")
.arg("connection")
.arg("import")
.arg("type")
.arg("wireguard")
.arg("file")
.arg(&conf)
.invoke(ErrorKind::Network)
.await?;
tmpdir.delete().await?;
ifaces.wait_for(|ifaces| ifaces.contains_key(&iface)).await;
ctx.net_controller
.net_iface
.set_public(&iface, Some(public))
.await?;
ctx.net_controller.net_iface.set_name(&iface, &name).await?;
Ok(iface)
}
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct RemoveTunnelParams {
#[ts(type = "string")]
id: InternedString,
}
pub async fn remove_tunnel(
ctx: RpcContext,
RemoveTunnelParams { id }: RemoveTunnelParams,
) -> Result<(), Error> {
let Some(existing) = ctx
.db
.peek()
.await
.into_public()
.into_server_info()
.into_network()
.into_network_interfaces()
.into_idx(&id)
.and_then(|e| e.into_ip_info().transpose())
else {
return Ok(());
};
if existing.as_device_type().de()? != Some(NetworkInterfaceType::Wireguard) {
return Err(Error::new(
eyre!("network interface {id} is not a proxy"),
ErrorKind::InvalidRequest,
));
}
ctx.net_controller.net_iface.delete_iface(&id).await?;
Ok(())
}

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 AddTunnelParams = { name: string; config: string; public: boolean }

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NetworkInterfaceSetInboundParams = {
export type NetworkInterfaceSetPublicParams = {
interface: string
inbound: boolean | null
public: boolean | 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 RemoveTunnelParams = { id: 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 RenameInterfaceParams = { interface: string; name: string }

View File

@@ -17,6 +17,7 @@ export { AddPackageParams } from "./AddPackageParams"
export { AddPackageToCategoryParams } from "./AddPackageToCategoryParams"
export { AddressInfo } from "./AddressInfo"
export { AddSslOptions } from "./AddSslOptions"
export { AddTunnelParams } from "./AddTunnelParams"
export { AddVersionParams } from "./AddVersionParams"
export { Alerts } from "./Alerts"
export { Algorithm } from "./Algorithm"
@@ -138,7 +139,7 @@ export { NamedProgress } from "./NamedProgress"
export { NetInfo } from "./NetInfo"
export { NetworkInfo } from "./NetworkInfo"
export { NetworkInterfaceInfo } from "./NetworkInterfaceInfo"
export { NetworkInterfaceSetInboundParams } from "./NetworkInterfaceSetInboundParams"
export { NetworkInterfaceSetPublicParams } from "./NetworkInterfaceSetPublicParams"
export { NetworkInterfaceType } from "./NetworkInterfaceType"
export { OnionHostname } from "./OnionHostname"
export { OsIndex } from "./OsIndex"
@@ -167,7 +168,9 @@ export { RemoveAssetParams } from "./RemoveAssetParams"
export { RemoveCategoryParams } from "./RemoveCategoryParams"
export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryParams"
export { RemovePackageParams } from "./RemovePackageParams"
export { RemoveTunnelParams } from "./RemoveTunnelParams"
export { RemoveVersionParams } from "./RemoveVersionParams"
export { RenameInterfaceParams } from "./RenameInterfaceParams"
export { ReplayId } from "./ReplayId"
export { RequestCommitment } from "./RequestCommitment"
export { RunActionParams } from "./RunActionParams"

View File

@@ -165,8 +165,8 @@ export default class ProxiesComponent {
const loader = this.loader.open('Saving').subscribe()
try {
await this.api.addProxy({
label: input.label,
await this.api.addTunnel({
name: input.label,
config: input.config.value.file as string, // @TODO alex this is the file represented as a string
public: input.type === 'public',
})

View File

@@ -71,7 +71,7 @@ export class ProxiesTableComponent<T extends WireguardProxy> {
const loader = this.loader.open('Deleting').subscribe()
try {
await this.api.removeProxy({ id })
await this.api.removeTunnel({ id })
} catch (e: any) {
this.errorService.handleError(e)
} finally {
@@ -108,7 +108,7 @@ export class ProxiesTableComponent<T extends WireguardProxy> {
const loader = this.loader.open('Saving').subscribe()
try {
await this.api.updateProxy({ id, label })
await this.api.updateTunnel({ id, name: label })
return true
} catch (e: any) {
this.errorService.handleError(e)

View File

@@ -234,30 +234,25 @@ export namespace RR {
}
export type CreateBackupRes = null
// proxy
// tunnel
export type AddProxyReq = {
label: string
config: string // hash of file
export type AddTunnelReq = {
name: string
config: string // file contents
public: boolean
} // net.proxy.add
export type AddProxyRes = {
} // net.tunnel.add
export type AddTunnelRes = {
id: string
}
export type UpdateProxyReq = {
export type UpdateTunnelReq = {
id: string
label: string
} // net.netwok-interface.set-label
export type UpdateProxyRes = null
name: string
} // net.netwok-interface.set-name
export type UpdateTunnelRes = null
export type RemoveProxyReq = { id: string } // net.proxy.remove
export type RemoveProxyRes = null
// export type SetOutboundProxyReq = {
// id: string | null
// } // net.proxy.set-outbound
// export type SetOutboundProxyRes = null
export type RemoveTunnelReq = { id: string } // net.tunnel.remove
export type RemoveTunnelRes = null
export type InitAcmeReq = {
provider: 'letsencrypt' | 'letsencrypt-staging' | string

View File

@@ -176,17 +176,17 @@ export abstract class ApiService {
// ** proxies **
abstract addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes>
abstract addTunnel(params: RR.AddTunnelReq): Promise<RR.AddTunnelRes>
abstract updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes>
abstract updateTunnel(params: RR.UpdateTunnelReq): Promise<RR.UpdateTunnelRes>
abstract removeProxy(params: RR.RemoveProxyReq): Promise<RR.RemoveProxyRes>
abstract removeTunnel(params: RR.RemoveTunnelReq): Promise<RR.RemoveTunnelRes>
// @TODO 041
// abstract setOutboundProxy(
// params: RR.SetOutboundProxyReq,
// ): Promise<RR.SetOutboundProxyRes>
// params: RR.SetOutboundTunnelReq,
// ): Promise<RR.SetOutboundTunnelRes>
// ** domains **
@@ -364,8 +364,8 @@ export abstract class ApiService {
// ** service outbound proxy **
// abstract setServiceOutboundProxy(
// params: RR.SetServiceOutboundProxyReq,
// ): Promise<RR.SetServiceOutboundProxyRes>
// params: RR.SetServiceOutboundTunnelReq,
// ): Promise<RR.SetServiceOutboundTunnelRes>
abstract initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes>

View File

@@ -346,21 +346,21 @@ export class LiveApiService extends ApiService {
// proxies
async addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes> {
return this.rpcRequest({ method: 'net.proxy.add', params })
async addTunnel(params: RR.AddTunnelReq): Promise<RR.AddTunnelRes> {
return this.rpcRequest({ method: 'net.tunnel.add', params })
}
async updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes> {
return this.rpcRequest({ method: 'net.netwok-interface.set-label', params })
async updateTunnel(params: RR.UpdateTunnelReq): Promise<RR.UpdateTunnelRes> {
return this.rpcRequest({ method: 'net.netwok-interface.set-name', params })
}
async removeProxy(params: RR.RemoveProxyReq): Promise<RR.RemoveProxyRes> {
return this.rpcRequest({ method: 'net.proxy.remove', params })
async removeTunnel(params: RR.RemoveTunnelReq): Promise<RR.RemoveTunnelRes> {
return this.rpcRequest({ method: 'net.tunnel.remove', params })
}
// async setOutboundProxy(
// params: RR.SetOutboundProxyReq,
// ): Promise<RR.SetOutboundProxyRes> {
// params: RR.SetOutboundTunnelReq,
// ): Promise<RR.SetOutboundTunnelRes> {
// return this.rpcRequest({ method: 'server.proxy.set-outbound', params })
// }
@@ -627,8 +627,8 @@ export class LiveApiService extends ApiService {
}
// async setServiceOutboundProxy(
// params: RR.SetServiceOutboundProxyReq,
// ): Promise<RR.SetServiceOutboundProxyRes> {
// params: RR.SetServiceOutboundTunnelReq,
// ): Promise<RR.SetServiceOutboundTunnelRes> {
// return this.rpcRequest({ method: 'package.proxy.set-outbound', params })
// }

View File

@@ -544,10 +544,11 @@ export class MockApiService extends ApiService {
// proxies
async addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes> {
private proxyId = 0
async addTunnel(params: RR.AddTunnelReq): Promise<RR.AddTunnelRes> {
await pauseFor(2000)
const id = `wga-${params.label}`
const id = `wg${this.proxyId++}`
const patch: AddOperation<T.NetworkInterfaceInfo>[] = [
{
@@ -556,7 +557,7 @@ export class MockApiService extends ApiService {
value: {
public: params.public,
ipInfo: {
name: params.label,
name: params.name,
scopeId: 3,
deviceType: 'wireguard',
subnets: [],
@@ -571,14 +572,14 @@ export class MockApiService extends ApiService {
return { id }
}
async updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes> {
async updateTunnel(params: RR.UpdateTunnelReq): Promise<RR.UpdateTunnelRes> {
await pauseFor(2000)
const patch: ReplaceOperation<string>[] = [
{
op: PatchOp.REPLACE,
path: `/serverInfo/network/networkInterfaces/${params.id}/label`,
value: params.label,
value: params.name,
},
]
this.mockRevision(patch)
@@ -586,7 +587,7 @@ export class MockApiService extends ApiService {
return null
}
async removeProxy(params: RR.RemoveProxyReq): Promise<RR.RemoveProxyRes> {
async removeTunnel(params: RR.RemoveTunnelReq): Promise<RR.RemoveTunnelRes> {
await pauseFor(2000)
const patch: RemoveOperation[] = [
{
@@ -600,8 +601,8 @@ export class MockApiService extends ApiService {
}
// async setOutboundProxy(
// params: RR.SetOutboundProxyReq,
// ): Promise<RR.SetOutboundProxyRes> {
// params: RR.SetOutboundTunnelReq,
// ): Promise<RR.SetOutboundTunnelRes> {
// await pauseFor(2000)
// const patch: ReplaceOperation<string | null>[] = [
@@ -1372,8 +1373,8 @@ export class MockApiService extends ApiService {
}
// async setServiceOutboundProxy(
// params: RR.SetServiceOutboundProxyReq,
// ): Promise<RR.SetServiceOutboundProxyRes> {
// params: RR.SetServiceOutboundTunnelReq,
// ): Promise<RR.SetServiceOutboundTunnelRes> {
// await pauseFor(2000)
// const patch = [
// {