feat: added beacon installer generation, download

This commit is contained in:
Andrew Rioux
2025-02-02 02:37:53 -05:00
parent b416f35b63
commit 0576c4fd3b
22 changed files with 554 additions and 87 deletions

View File

@@ -112,6 +112,7 @@ pub fn provide_beacon_resources() {
#[component]
pub fn BeaconView() -> impl IntoView {
#[cfg(feature = "hydrate")]
Effect::new(move || {
let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
if user.get().is_none() {

View File

@@ -101,7 +101,6 @@ pub async fn rename_category(id: i64, name: String) -> Result<(), ServerFnError>
pub fn CategoriesView() -> impl IntoView {
let BeaconResources { add_category, categories, .. } = expect_context();
view! {
<div class="categories">
<h2>"Categories"</h2>

View File

@@ -71,6 +71,17 @@ pub async fn get_listeners() -> Result<Vec<PubListener>, ServerFnError> {
.collect())
}
#[cfg(feature = "ssr")]
pub fn generate_cert_from_keypair(kp: &rcgen::KeyPair, names: Vec<String>) -> Result<rcgen::Certificate, rcgen::Error> {
use rcgen::CertificateParams;
let mut params = CertificateParams::new(names)?;
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
params.self_signed(&kp)
}
#[server]
pub async fn add_listener(public_ip: String, port: i16, domain_name: String) -> Result<(), ServerFnError> {
let user = user::get_auth_session().await?;
@@ -84,15 +95,19 @@ pub async fn add_listener(public_ip: String, port: i16, domain_name: String) ->
}
let subject_alt_names = vec![public_ip.to_string(), domain_name.clone()];
let CertifiedKey { cert, key_pair } = tokio::task::spawn_blocking(|| {
generate_simple_self_signed(subject_alt_names)
let (key_pair, cert) = tokio::task::spawn_blocking(|| {
rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
.and_then(|keypair|
generate_cert_from_keypair(&keypair, subject_alt_names).map(|cert| (keypair, cert)))
}).await??;
let db = expect_context::<SqlitePool>();
let public_ip = public_ip.to_string();
let cert = cert.pem().to_string();
let key_pair = key_pair.serialize_pem().to_string();
let cert = cert.der().to_vec();
let key_pair = key_pair.serialize_der();
sqlx::query!(
"INSERT INTO beacon_listener (port, public_ip, domain_name, certificate, privkey) VALUES (?, ?, ?, ?, ?)",
@@ -116,23 +131,32 @@ pub async fn remove_listener(listener_id: i64) -> Result<(), ServerFnError> {
return Err(ServerFnError::<NoCustomError>::ServerError("You are not signed in!".to_owned()));
}
let pool = expect_context::<SqlitePool>();
let blm = expect_context::<BeaconListenerMap>();
{
let blm = expect_context::<BeaconListenerMap>();
let Ok(mut blm_handle) = blm.write() else {
return Err(ServerFnError::<NoCustomError>::ServerError("Failed to get write handle for beacon listener map".to_owned()));
};
let Ok(mut blm_handle) = blm.write() else {
return Err(ServerFnError::<NoCustomError>::ServerError("Failed to get write handle for beacon listener map".to_owned()));
};
if let Some(mut bl) = blm_handle.get_mut(&listener_id) {
bl.abort();
} else {
return Err(ServerFnError::<NoCustomError>::ServerError("Failed to get write handle for beacon listener map".to_owned()));
if let Some(bl) = blm_handle.get_mut(&listener_id) {
bl.abort();
} else {
return Err(ServerFnError::<NoCustomError>::ServerError("Failed to get write handle for beacon listener map".to_owned()));
}
blm_handle.remove(&listener_id);
}
blm_handle.remove(&listener_id);
drop(blm_handle);
let pool = expect_context::<SqlitePool>();
unimplemented!()
sqlx::query!(
"DELETE FROM beacon_listener WHERE listener_id = ?",
listener_id
)
.execute(&pool)
.await?;
Ok(())
}
#[server]

View File

@@ -103,6 +103,34 @@ pub async fn add_template(
let db = expect_context::<SqlitePool>();
let listener = sqlx::query!(
"SELECT certificate, privkey FROM beacon_listener WHERE listener_id = ?",
listener_id
)
.fetch_one(&db)
.await?;
use rcgen::{Certificate, CertificateParams, KeyPair};
let keypair = KeyPair::from_der_and_sign_algo(
match &rustls_pki_types::PrivateKeyDer::try_from(&*listener.privkey) {
Ok(pk) => pk,
Err(e) => {
srverr!("Could not parse private key: {e}");
}
},
&rcgen::PKCS_ECDSA_P256_SHA256
)?;
let ca_params = CertificateParams::from_ca_cert_der(&(*listener.certificate).into())?;
let ca_cert = ca_params.self_signed(&keypair)?;
let client_key = KeyPair::generate()?;
let client_params = CertificateParams::default();
let client_cert = client_params.signed_by(&client_key, &ca_cert, &keypair)?;
let client_key_der = client_key.serialize_der();
let client_cert_der = client_cert.der().to_vec();
match &*source_mode {
"host" => {
let source_mac = Some(source_mac).filter(|mac| mac != "00:00:00:00:00:00");
@@ -110,16 +138,18 @@ pub async fn add_template(
sqlx::query!(
r"INSERT INTO beacon_template
(template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, default_category)
(template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, default_category, client_key, client_cert)
VALUES
(?, ?, ?, ?, ?, ?, 'host', ?)",
(?, ?, ?, ?, ?, ?, 'host', ?, ?, ?)",
template_name,
operating_system,
config_id,
listener_id,
source_ip,
source_mac,
default_category
default_category,
client_key_der,
client_cert_der
)
.execute(&db)
.await?;
@@ -132,9 +162,9 @@ pub async fn add_template(
sqlx::query!(
r"INSERT INTO beacon_template
(template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, source_netmask, source_gateway, default_category)
(template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, source_netmask, source_gateway, default_category, client_key, client_cert)
VALUES
(?, ?, ?, ?, ?, ?, 'host', ?, ?, ?)",
(?, ?, ?, ?, ?, ?, 'host', ?, ?, ?, ?, ?)",
template_name,
operating_system,
config_id,
@@ -143,7 +173,9 @@ pub async fn add_template(
source_mac,
source_netmask,
source_gateway,
default_category
default_category,
client_key_der,
client_cert_der
)
.execute(&db)
.await?;
@@ -376,9 +408,13 @@ pub fn DisplayTemplates(
")"
</h4>
<div>
<button>
<a
class="button"
download=""
href=format!("/installer/{}", template.template_id)
>
"Download installer"
</button>
</a>
<button
on:click={
let template_id = template.template_id;
@@ -386,6 +422,7 @@ pub fn DisplayTemplates(
remove_template.dispatch(RemoveTemplate { template_id });
}
}
class="warning"
>
"Delete template"
</button>

View File

@@ -10,6 +10,7 @@ pub enum Error {
Pbkdf2(pbkdf2::password_hash::errors::Error),
#[cfg(feature = "ssr")]
Io(std::io::Error),
AddrParse(std::net::AddrParseError),
}
impl std::fmt::Display for Error {
@@ -37,6 +38,9 @@ impl std::fmt::Display for Error {
Error::Io(err) => {
write!(f, "io error: {err:?}")
}
Error::AddrParse(err) => {
write!(f, "ip address parse error: {err:?}")
}
}
}
}
@@ -50,11 +54,23 @@ impl std::error::Error for Error {
Error::TokioJoin(err) => Some(err),
#[cfg(feature = "ssr")]
Error::Io(err) => Some(err),
Error::AddrParse(err) => Some(err),
_ => None,
}
}
}
#[cfg(feature = "ssr")]
impl axum::response::IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
format!("{:?}", self),
)
.into_response()
}
}
impl std::str::FromStr for Error {
type Err = Self;
@@ -90,3 +106,9 @@ impl From<std::io::Error> for Error {
Self::Io(err)
}
}
impl From<std::net::AddrParseError> for Error {
fn from(err: std::net::AddrParseError) -> Self {
Self::AddrParse(err)
}
}

View File

@@ -1,19 +1,3 @@
#[cfg(feature = "hydrate")]
pub(crate) mod beacon_binaries {
#[allow(dead_code)]
pub const LINUX_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_LINUX"));
#[allow(dead_code)]
pub const FREEBSD_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_FREEBSD"));
#[allow(dead_code)]
pub const WINDOWS_BEACON: &'static [u8] = include_bytes!(std::env!("SPARSE_BEACON_WINDOWS"));
#[allow(dead_code)]
pub const LINUX_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_LINUX"));
#[allow(dead_code)]
pub const FREEBSD_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_FREEBSD"));
#[allow(dead_code)]
pub const WINDOWS_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_WINDOWS"));
}
#[cfg(feature = "ssr")]
mod cli;
@@ -29,6 +13,7 @@ pub mod db;
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() -> anyhow::Result<std::process::ExitCode> {
use std::{path::PathBuf, process::ExitCode, str::FromStr};
use structopt::StructOpt;
@@ -38,7 +23,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("{}=debug,sparse_handler=debug,tower_http=trace", env!("CARGO_CRATE_NAME")).into()),
.unwrap_or_else(|_| format!("{}=debug,sparse_handler=debug", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer())
.init();

View File

@@ -1,13 +1,167 @@
use std::{net::SocketAddrV4, process::ExitCode};
use sqlx::sqlite::SqlitePool;
use axum::Router;
use axum::{extract::{FromRef, Path, State}, response::IntoResponse, Router, routing::get};
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use tokio::signal;
use sparse_server::app::*;
#[cfg(not(debug_assertions))]
pub(crate) mod beacon_binaries {
pub const LINUX_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_LINUX"));
pub const FREEBSD_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_FREEBSD"));
pub const WINDOWS_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_WINDOWS"));
}
#[cfg(debug_assertions)]
pub async fn get_installer(btype: &str) -> Result<Vec<u8>, crate::error::Error> {
let path = match btype {
"linux" => "target/x86_64-unknown-linux-musl/debug/sparse-unix-installer",
"freebsd" => "target/x86_64-unknown-freebsd/debug/sparse-unix-installer",
"windows" => "target/x86_64-pc-windows-gnu/debug/sparse-unix-installer",
other => return Err(crate::error::Error::Generic(format!("unknown beacon type: {other}"))),
};
Ok(tokio::fs::read(path).await?)
}
#[cfg(not(debug_assertions))]
pub async fn get_installer(btype: &str) -> Result<Vec<u8>, crate::error::Error> {
match btype {
"linux" => Ok(beacon_binaries::LINUX_INSTALLER.to_owned()),
"windows" => Ok(beacon_binaries::WINDOWS_INSTALLER.to_owned()),
"freebsd" => Ok(beacon_binaries::FREEBSD_INSTALLER.to_owned()),
other => Err(crate::error::Error::Generic(format!("unknown beacon type: {other}")))
}
}
#[derive(FromRef, Clone, Debug)]
pub struct AppState {
db: SqlitePool,
leptos_options: leptos::config::LeptosOptions
}
#[axum::debug_handler]
pub async fn download_beacon_installer(
Path(template_id): Path<i64>,
State(db): State<AppState>
) -> Result<impl IntoResponse, crate::error::Error> {
use rand::{rngs::OsRng, TryRngCore};
use sparse_actions::payload_types::Parameters_t;
let mut parameters_buffer = vec![0u8; std::mem::size_of::<Parameters_t>()];
//let _ = OsRng.try_fill_bytes(&mut parameters_buffer);
let parameters: &mut Parameters_t = unsafe { std::mem::transmute(parameters_buffer.as_mut_ptr()) };
let template = sqlx::query!(
r"SELECT operating_system, source_ip, source_mac, source_mode, source_netmask,
source_gateway, port, public_ip, domain_name, certificate, client_cert, client_key
FROM beacon_template JOIN beacon_listener"
)
.fetch_one(&db.db)
.await?;
let dest_ip = template.public_ip.parse::<std::net::Ipv4Addr>()?;
let src_ip = template.source_ip.parse::<std::net::Ipv4Addr>()?;
let dest_octets = dest_ip.octets();
parameters.destination_ip.a = dest_octets[0];
parameters.destination_ip.b = dest_octets[1];
parameters.destination_ip.c = dest_octets[2];
parameters.destination_ip.d = dest_octets[3];
let src_mac: [u8; 6] = template
.source_mac
.unwrap_or("00:00:00:00:00:00".to_string())
.split(":")
.map(|by| u8::from_str_radix(by, 16))
.collect::<Result<Vec<u8>, _>>()
.map_err(|_| crate::error::Error::Generic("Could not parse source MAC address".to_string()))
.and_then(
|bytes| bytes.try_into().map_err(|_| crate::error::Error::Generic("Could not parse source MAC address".to_string()))
)?;
let src_octets = src_ip.octets();
match (template.source_mode.as_deref(), template.source_netmask, template.source_gateway) {
(Some("custom"), Some(nm), Some(ip)) => unsafe {
let gateway = ip.parse::<std::net::Ipv4Addr>()?;
let gw_octets = gateway.octets();
parameters.source_ip.custom_networking.mode = 0;
parameters.source_ip.custom_networking.source_mac.copy_from_slice(&src_mac[..]);
parameters.source_ip.custom_networking.netmask = nm as u16;
parameters.source_ip.custom_networking.source_ip.a = src_octets[0];
parameters.source_ip.custom_networking.source_ip.b = src_octets[1];
parameters.source_ip.custom_networking.source_ip.c = src_octets[2];
parameters.source_ip.custom_networking.source_ip.d = src_octets[3];
parameters.source_ip.custom_networking.gateway.a = gw_octets[0];
parameters.source_ip.custom_networking.gateway.b = gw_octets[1];
parameters.source_ip.custom_networking.gateway.c = gw_octets[2];
parameters.source_ip.custom_networking.gateway.d = gw_octets[3];
}
(Some("host"), _, _) => unsafe {
parameters.source_ip.use_host_networking.mode = 1;
parameters.source_ip.use_host_networking.source_mac.copy_from_slice(&src_mac[..]);
parameters.source_ip.use_host_networking.source_ip.a = src_octets[0];
parameters.source_ip.use_host_networking.source_ip.b = src_octets[1];
parameters.source_ip.use_host_networking.source_ip.c = src_octets[2];
parameters.source_ip.use_host_networking.source_ip.d = src_octets[3];
}
_ => {
return Err(crate::error::Error::Generic("Could not parse host networking configuration".to_string()));
}
}
parameters.destination_port = template.port as u16;
parameters.template_id = template_id as u16;
let pubkey_cert = template.certificate;
parameters.pubkey_cert[..pubkey_cert.len()].copy_from_slice(&pubkey_cert[..]);
parameters.pubkey_cert_size = pubkey_cert.len() as u16;
let client_key = template.client_key;
parameters.client_key[..client_key.len()].copy_from_slice(&client_key[..]);
parameters.client_key_length = client_key.len() as u16;
let client_cert = template.client_cert;
parameters.client_cert[..client_cert.len()].copy_from_slice(&client_cert[..]);
parameters.client_cert_length = client_cert.len() as u16;
let domain_name = template.domain_name.as_bytes();
parameters.domain_name[..domain_name.len()].copy_from_slice(&domain_name[..]);
parameters.domain_name_length = domain_name.len() as u16;
let installer_bytes = get_installer(&template.operating_system).await?;
use axum::http::header;
Ok((
[
(
header::CONTENT_TYPE,
"application/octet-stream".to_string()
),
(
header::CONTENT_DISPOSITION,
format!(
r#"attachement; filename="sparse-installer{}""#,
if template.operating_system == "windows" {
".exe"
} else {
""
}
)
)
],
[
&installer_bytes[..],
&parameters_buffer[..]
].concat()
))
}
pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyhow::Result<ExitCode> {
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options;
@@ -22,9 +176,15 @@ pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyh
sparse_handler::start_all_listeners(beacon_listeners.clone(), db.clone()).await?;
let state = AppState {
leptos_options: leptos_options.clone(),
db: db.clone()
};
let app = Router::new()
.route("/installer/:template_id", get(download_beacon_installer))
.leptos_routes_with_context(
&leptos_options,
&state,
routes,
move || {
provide_context(beacon_listeners.clone());
@@ -36,7 +196,7 @@ pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyh
}
)
.fallback(leptos_axum::file_and_error_handler::<leptos::config::LeptosOptions, _>(shell))
.with_state(leptos_options)
.with_state(state)
.layer(
tower::ServiceBuilder::new()
.layer(tower_http::trace::TraceLayer::new_for_http())