mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +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:
323
core/startos/src/tunnel/auth.rs
Normal file
323
core/startos/src/tunnel/auth.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
use clap::Parser;
|
||||
use imbl::HashMap;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use patch_db::HasModel;
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::auth::{Sessions, check_password};
|
||||
use crate::context::CliContext;
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::middleware::signature::SignatureAuthContext;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::OpenAuthedContinuations;
|
||||
use crate::sign::AnyVerifyingKey;
|
||||
use crate::tunnel::context::TunnelContext;
|
||||
use crate::tunnel::db::TunnelDatabase;
|
||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||
use crate::util::sync::SyncMutex;
|
||||
|
||||
impl SignatureAuthContext for TunnelContext {
|
||||
type Database = TunnelDatabase;
|
||||
type AdditionalMetadata = ();
|
||||
type CheckPubkeyRes = ();
|
||||
fn db(&self) -> &TypedPatchDb<Self::Database> {
|
||||
&self.db
|
||||
}
|
||||
async fn sig_context(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
|
||||
let peek = self.db().peek().await;
|
||||
peek.as_webserver()
|
||||
.as_listen()
|
||||
.de()
|
||||
.map(|a| a.as_ref().map(InternedString::from_display))
|
||||
.transpose()
|
||||
.into_iter()
|
||||
.chain(
|
||||
std::iter::once_with(move || {
|
||||
peek.as_webserver()
|
||||
.as_certificate()
|
||||
.de()
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|cert_data| cert_data.cert.0.first().cloned())
|
||||
.and_then(|cert| cert.subject_alt_names())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|san| {
|
||||
san.dnsname().map(InternedString::from).or_else(|| {
|
||||
san.ipaddress().and_then(|ip_bytes| {
|
||||
let ip: std::net::IpAddr = match ip_bytes.len() {
|
||||
4 => std::net::IpAddr::V4(std::net::Ipv4Addr::from(
|
||||
<[u8; 4]>::try_from(ip_bytes).ok()?,
|
||||
)),
|
||||
16 => std::net::IpAddr::V6(std::net::Ipv6Addr::from(
|
||||
<[u8; 16]>::try_from(ip_bytes).ok()?,
|
||||
)),
|
||||
_ => return None,
|
||||
};
|
||||
Some(InternedString::from_display(&ip))
|
||||
})
|
||||
})
|
||||
})
|
||||
.map(Ok)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
fn check_pubkey(
|
||||
db: &Model<Self::Database>,
|
||||
pubkey: Option<&crate::sign::AnyVerifyingKey>,
|
||||
_: Self::AdditionalMetadata,
|
||||
) -> Result<Self::CheckPubkeyRes, Error> {
|
||||
if let Some(pubkey) = pubkey {
|
||||
if db.as_auth_pubkeys().de()?.contains_key(pubkey) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::new(
|
||||
eyre!("Key is not authorized"),
|
||||
ErrorKind::IncorrectPassword,
|
||||
))
|
||||
}
|
||||
async fn post_auth_hook(
|
||||
&self,
|
||||
_: Self::CheckPubkeyRes,
|
||||
_: &rpc_toolkit::RpcRequest,
|
||||
) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl AuthContext for TunnelContext {
|
||||
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/start-tunnel/rpc.authcookie";
|
||||
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:root";
|
||||
fn access_sessions(db: &mut Model<Self::Database>) -> &mut Model<crate::auth::Sessions> {
|
||||
db.as_sessions_mut()
|
||||
}
|
||||
fn ephemeral_sessions(&self) -> &SyncMutex<Sessions> {
|
||||
&self.ephemeral_sessions
|
||||
}
|
||||
fn open_authed_continuations(&self) -> &OpenAuthedContinuations<Option<InternedString>> {
|
||||
&self.open_authed_continuations
|
||||
}
|
||||
fn check_password(db: &Model<Self::Database>, password: &str) -> Result<(), Error> {
|
||||
check_password(&db.as_password().de()?.unwrap_or_default(), password)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct SignerInfo {
|
||||
pub name: InternedString,
|
||||
}
|
||||
|
||||
pub fn auth_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"login",
|
||||
from_fn_async(crate::auth::login_impl::<TunnelContext>)
|
||||
.with_metadata("login", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"logout",
|
||||
from_fn_async(crate::auth::logout::<TunnelContext>)
|
||||
.with_metadata("get_session", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Log out of current auth session")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand("set-password", from_fn_async(set_password_rpc).no_cli())
|
||||
.subcommand(
|
||||
"set-password",
|
||||
from_fn_async(set_password_cli)
|
||||
.with_about("Set user interface password")
|
||||
.no_display(),
|
||||
)
|
||||
.subcommand(
|
||||
"reset-password",
|
||||
from_fn_async(reset_password)
|
||||
.with_about("Reset user interface password")
|
||||
.no_display(),
|
||||
)
|
||||
.subcommand(
|
||||
"key",
|
||||
ParentHandler::<C>::new()
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_key)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Add a new authorized key")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_key)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove an authorized key")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_keys)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.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", "KEY"]);
|
||||
for (key, info) in res {
|
||||
table.add_row(row![info.name, key]);
|
||||
}
|
||||
|
||||
table.print_tty(false)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.with_about("List authorized keys")
|
||||
.with_call_remote::<CliContext>(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddKeyParams {
|
||||
pub name: InternedString,
|
||||
pub key: AnyVerifyingKey,
|
||||
}
|
||||
|
||||
pub async fn add_key(
|
||||
ctx: TunnelContext,
|
||||
AddKeyParams { name, key }: AddKeyParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_auth_pubkeys_mut().mutate(|auth_pubkeys| {
|
||||
auth_pubkeys.insert(key, SignerInfo { name });
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveKeyParams {
|
||||
pub key: AnyVerifyingKey,
|
||||
}
|
||||
|
||||
pub async fn remove_key(
|
||||
ctx: TunnelContext,
|
||||
RemoveKeyParams { key }: RemoveKeyParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_auth_pubkeys_mut()
|
||||
.mutate(|auth_pubkeys| Ok(auth_pubkeys.remove(&key)))
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_keys(ctx: TunnelContext) -> Result<HashMap<AnyVerifyingKey, SignerInfo>, Error> {
|
||||
ctx.db.peek().await.into_auth_pubkeys().de()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SetPasswordParams {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub async fn set_password_rpc(
|
||||
ctx: TunnelContext,
|
||||
SetPasswordParams { password }: SetPasswordParams,
|
||||
) -> Result<(), Error> {
|
||||
let pwhash = argon2::hash_encoded(
|
||||
password.as_bytes(),
|
||||
&rand::random::<[u8; 16]>(),
|
||||
&argon2::Config::rfc9106_low_mem(),
|
||||
)
|
||||
.with_kind(ErrorKind::PasswordHashGeneration)?;
|
||||
ctx.db
|
||||
.mutate(|db| db.as_password_mut().ser(&Some(pwhash)))
|
||||
.await
|
||||
.result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_password_cli(
|
||||
HandlerArgs {
|
||||
context,
|
||||
parent_method,
|
||||
method,
|
||||
..
|
||||
}: HandlerArgs<CliContext>,
|
||||
) -> Result<(), Error> {
|
||||
let password = rpassword::prompt_password("New Password: ")?;
|
||||
let confirm = rpassword::prompt_password("Confirm Password: ")?;
|
||||
|
||||
if password != confirm {
|
||||
return Err(Error::new(
|
||||
eyre!("Passwords do not match"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
|
||||
context
|
||||
.call_remote::<TunnelContext>(
|
||||
&parent_method.iter().chain(method.iter()).join("."),
|
||||
to_value(&SetPasswordParams { password })?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Password set successfully");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset_password(
|
||||
HandlerArgs {
|
||||
context,
|
||||
parent_method,
|
||||
method,
|
||||
..
|
||||
}: HandlerArgs<CliContext>,
|
||||
) -> Result<(), Error> {
|
||||
println!("Generating a random password...");
|
||||
let params = SetPasswordParams {
|
||||
password: base32::encode(
|
||||
base32::Alphabet::Rfc4648Lower { padding: false },
|
||||
&rand::random::<[u8; 16]>(),
|
||||
),
|
||||
};
|
||||
|
||||
context
|
||||
.call_remote::<TunnelContext>(
|
||||
&parent_method.iter().chain(method.iter()).join("."),
|
||||
to_value(¶ms)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Your new password is:");
|
||||
println!("{}", params.password);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user