Feature/cli clearnet (#2789)

* add support for ACME cert acquisition

* add support for modifying hosts for a package

* misc fixes

* more fixes

* use different port for lan clearnet than wan clearnet

* fix chroot-and-upgrade always growing

* bail on failure

* wip

* fix alpn auth

* bump async-acme

* fix cli

* add barebones documentation

* add domain to hostname info
This commit is contained in:
Aiden McClelland
2024-11-21 10:55:59 -07:00
committed by GitHub
parent ed8a7ee8a5
commit fefa88fc2a
23 changed files with 1589 additions and 218 deletions

View File

@@ -50,6 +50,10 @@ test = []
[dependencies]
aes = { version = "0.7.5", features = ["ctr"] }
async-acme = { version = "0.5.0", git = "https://github.com/dr-bonez/async-acme.git", features = [
"use_rustls",
"use_tokio",
] }
async-compression = { version = "0.4.4", features = [
"gzip",
"brotli",

View File

@@ -55,6 +55,7 @@ impl Public {
.parse()
.unwrap(),
ip_info: BTreeMap::new(),
acme: None,
status_info: ServerStatus {
backup_progress: None,
updated: false,
@@ -130,6 +131,7 @@ pub struct ServerInfo {
#[ts(type = "string")]
pub tor_address: Url,
pub ip_info: BTreeMap<String, IpInfo>,
pub acme: Option<AcmeSettings>,
#[serde(default)]
pub status_info: ServerStatus,
pub wifi: WifiInfo,
@@ -174,6 +176,20 @@ impl IpInfo {
}
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct AcmeSettings {
#[ts(type = "string")]
pub provider: Url,
/// email addresses for letsencrypt
pub contact: Vec<String>,
#[ts(type = "string[]")]
/// domains to get letsencrypt certs for
pub domains: BTreeSet<InternedString>,
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[model = "Model<Self>"]
#[ts(export)]

View File

@@ -355,7 +355,7 @@ pub fn package<C: Context>() -> ParentHandler<C> {
from_fn_async(control::start)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Start a package container")
.with_about("Start a service")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -363,7 +363,7 @@ pub fn package<C: Context>() -> ParentHandler<C> {
from_fn_async(control::stop)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Stop a package container")
.with_about("Stop a service")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -371,7 +371,7 @@ pub fn package<C: Context>() -> ParentHandler<C> {
from_fn_async(control::restart)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Restart a package container")
.with_about("Restart a service")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -409,9 +409,14 @@ pub fn package<C: Context>() -> ParentHandler<C> {
"attach",
from_fn_async(service::attach)
.with_metadata("get_session", Value::Bool(true))
.with_about("Execute commands within a service container")
.no_cli(),
)
.subcommand("attach", from_fn_async(service::cli_attach).no_display())
.subcommand(
"host",
net::host::host::<C>().with_about("Manage network hosts for a package"),
)
}
pub fn diagnostic_api() -> ParentHandler<DiagnosticContext> {

View File

@@ -0,0 +1,324 @@
use std::collections::{BTreeMap, BTreeSet};
use std::str::FromStr;
use clap::builder::ValueParserFactory;
use clap::Parser;
use imbl_value::InternedString;
use itertools::Itertools;
use models::{ErrorData, FromStrParser};
use openssl::pkey::{PKey, Private};
use openssl::x509::X509;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::context::{CliContext, RpcContext};
use crate::db::model::public::AcmeSettings;
use crate::db::model::Database;
use crate::prelude::*;
use crate::util::serde::{Pem, Pkcs8Doc};
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]
#[model = "Model<Self>"]
pub struct AcmeCertStore {
pub accounts: BTreeMap<JsonKey<Vec<String>>, Pem<Pkcs8Doc>>,
pub certs: BTreeMap<Url, BTreeMap<JsonKey<BTreeSet<InternedString>>, AcmeCert>>,
}
impl AcmeCertStore {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AcmeCert {
pub key: Pem<PKey<Private>>,
pub fullchain: Vec<Pem<X509>>,
}
pub struct AcmeCertCache<'a>(pub &'a TypedPatchDb<Database>);
#[async_trait::async_trait]
impl<'a> async_acme::cache::AcmeCache for AcmeCertCache<'a> {
type Error = ErrorData;
async fn read_account(&self, contacts: &[&str]) -> Result<Option<Vec<u8>>, Self::Error> {
let contacts = JsonKey::new(contacts.into_iter().map(|s| (*s).to_owned()).collect_vec());
let Some(account) = self
.0
.peek()
.await
.into_private()
.into_key_store()
.into_acme()
.into_accounts()
.into_idx(&contacts)
else {
return Ok(None);
};
Ok(Some(account.de()?.0.document.into_vec()))
}
async fn write_account(&self, contacts: &[&str], contents: &[u8]) -> Result<(), Self::Error> {
let contacts = JsonKey::new(contacts.into_iter().map(|s| (*s).to_owned()).collect_vec());
let key = Pkcs8Doc {
tag: "EC PRIVATE KEY".into(),
document: pkcs8::Document::try_from(contents).with_kind(ErrorKind::Pem)?,
};
self.0
.mutate(|db| {
db.as_private_mut()
.as_key_store_mut()
.as_acme_mut()
.as_accounts_mut()
.insert(&contacts, &Pem::new(key))
})
.await?;
Ok(())
}
async fn read_certificate(
&self,
domains: &[String],
directory_url: &str,
) -> Result<Option<(String, String)>, Self::Error> {
let domains = JsonKey::new(domains.into_iter().map(InternedString::intern).collect());
let directory_url = directory_url
.parse::<Url>()
.with_kind(ErrorKind::ParseUrl)?;
let Some(cert) = self
.0
.peek()
.await
.into_private()
.into_key_store()
.into_acme()
.into_certs()
.into_idx(&directory_url)
.and_then(|a| a.into_idx(&domains))
else {
return Ok(None);
};
let cert = cert.de()?;
Ok(Some((
String::from_utf8(
cert.key
.0
.private_key_to_pem_pkcs8()
.with_kind(ErrorKind::OpenSsl)?,
)
.with_kind(ErrorKind::Utf8)?,
cert.fullchain
.into_iter()
.map(|cert| {
String::from_utf8(cert.0.to_pem().with_kind(ErrorKind::OpenSsl)?)
.with_kind(ErrorKind::Utf8)
})
.collect::<Result<Vec<_>, _>>()?
.join("\n"),
)))
}
async fn write_certificate(
&self,
domains: &[String],
directory_url: &str,
key_pem: &str,
certificate_pem: &str,
) -> Result<(), Self::Error> {
tracing::info!("Saving new certificate for {domains:?}");
let domains = JsonKey::new(domains.into_iter().map(InternedString::intern).collect());
let directory_url = directory_url
.parse::<Url>()
.with_kind(ErrorKind::ParseUrl)?;
let cert = AcmeCert {
key: Pem(PKey::<Private>::private_key_from_pem(key_pem.as_bytes())
.with_kind(ErrorKind::OpenSsl)?),
fullchain: X509::stack_from_pem(certificate_pem.as_bytes())
.with_kind(ErrorKind::OpenSsl)?
.into_iter()
.map(Pem)
.collect(),
};
self.0
.mutate(|db| {
db.as_private_mut()
.as_key_store_mut()
.as_acme_mut()
.as_certs_mut()
.upsert(&directory_url, || Ok(BTreeMap::new()))?
.insert(&domains, &cert)
})
.await?;
Ok(())
}
}
pub fn acme<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"init",
from_fn_async(init)
.no_display()
.with_about("Setup ACME certificate acquisition")
.with_call_remote::<CliContext>(),
)
.subcommand(
"domain",
domain::<C>()
.with_about("Add, remove, or view domains for which to acquire ACME certificates"),
)
}
#[derive(Clone, Deserialize, Serialize)]
pub struct AcmeProvider(pub Url);
impl FromStr for AcmeProvider {
type Err = <Url as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"letsencrypt" => async_acme::acme::LETS_ENCRYPT_PRODUCTION_DIRECTORY.parse(),
"letsencrypt-staging" => async_acme::acme::LETS_ENCRYPT_STAGING_DIRECTORY.parse(),
s => s.parse(),
}
.map(Self)
}
}
impl ValueParserFactory for AcmeProvider {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {
Self::Parser::new()
}
}
#[derive(Deserialize, Serialize, Parser)]
pub struct InitAcmeParams {
#[arg(long)]
pub provider: AcmeProvider,
#[arg(long)]
pub contact: Vec<String>,
}
pub async fn init(
ctx: RpcContext,
InitAcmeParams {
provider: AcmeProvider(provider),
contact,
}: InitAcmeParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_public_mut()
.as_server_info_mut()
.as_acme_mut()
.map_mutate(|acme| {
Ok(Some(AcmeSettings {
provider,
contact,
domains: acme.map(|acme| acme.domains).unwrap_or_default(),
}))
})
})
.await?;
Ok(())
}
pub fn domain<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"add",
from_fn_async(add_domain)
.no_display()
.with_about("Add a domain for which to acquire ACME certificates")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove_domain)
.no_display()
.with_about("Remove a domain for which to acquire ACME certificates")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_domains)
.with_custom_display_fn(|_, res| {
for domain in res {
println!("{domain}")
}
Ok(())
})
.with_about("List domains for which to acquire ACME certificates")
.with_call_remote::<CliContext>(),
)
}
#[derive(Deserialize, Serialize, Parser)]
pub struct DomainParams {
pub domain: InternedString,
}
pub async fn add_domain(
ctx: RpcContext,
DomainParams { domain }: DomainParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_public_mut()
.as_server_info_mut()
.as_acme_mut()
.transpose_mut()
.ok_or_else(|| {
Error::new(
eyre!("Please call `start-cli net acme init` before adding a domain"),
ErrorKind::InvalidRequest,
)
})?
.as_domains_mut()
.mutate(|domains| {
domains.insert(domain);
Ok(())
})
})
.await?;
Ok(())
}
pub async fn remove_domain(
ctx: RpcContext,
DomainParams { domain }: DomainParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
if let Some(acme) = db
.as_public_mut()
.as_server_info_mut()
.as_acme_mut()
.transpose_mut()
{
acme.as_domains_mut().mutate(|domains| {
domains.remove(&domain);
Ok(())
})
} else {
Ok(())
}
})
.await?;
Ok(())
}
pub async fn list_domains(ctx: RpcContext) -> Result<BTreeSet<InternedString>, Error> {
if let Some(acme) = ctx
.db
.peek()
.await
.into_public()
.into_server_info()
.into_acme()
.transpose()
{
acme.into_domains().de()
} else {
Ok(BTreeSet::new())
}
}

View File

@@ -1,7 +1,9 @@
use std::fmt;
use std::str::FromStr;
use clap::builder::ValueParserFactory;
use imbl_value::InternedString;
use models::FromStrParser;
use serde::{Deserialize, Serialize};
use torut::onion::OnionAddressV3;
use ts_rs::TS;
@@ -46,3 +48,10 @@ impl fmt::Display for HostAddress {
}
}
}
impl ValueParserFactory for HostAddress {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {
Self::Parser::new()
}
}

View File

@@ -1,10 +1,13 @@
use std::collections::{BTreeMap, BTreeSet};
use clap::Parser;
use imbl_value::InternedString;
use models::{HostId, PackageId};
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::context::{CliContext, RpcContext};
use crate::db::model::DatabaseModel;
use crate::net::forward::AvailablePorts;
use crate::net::host::address::HostAddress;
@@ -134,3 +137,163 @@ impl Model<Host> {
})
}
}
#[derive(Deserialize, Serialize, Parser)]
pub struct HostParams {
package: PackageId,
}
pub fn host<C: Context>() -> ParentHandler<C, HostParams> {
ParentHandler::<C, HostParams>::new()
.subcommand(
"list",
from_fn_async(list_hosts)
.with_inherited(|HostParams { package }, _| package)
.with_custom_display_fn(|_, ids| {
for id in ids {
println!("{id}")
}
Ok(())
})
.with_about("List host IDs available for this service"),
)
.subcommand(
"address",
address::<C>().with_inherited(|HostParams { package }, _| package),
)
}
pub async fn list_hosts(
ctx: RpcContext,
_: Empty,
package: PackageId,
) -> Result<Vec<HostId>, Error> {
ctx.db
.peek()
.await
.into_public()
.into_package_data()
.into_idx(&package)
.or_not_found(&package)?
.into_hosts()
.keys()
}
#[derive(Deserialize, Serialize, Parser)]
pub struct AddressApiParams {
host: HostId,
}
pub fn address<C: Context>() -> ParentHandler<C, AddressApiParams, PackageId> {
ParentHandler::<C, AddressApiParams, PackageId>::new()
.subcommand(
"add",
from_fn_async(add_address)
.with_inherited(|AddressApiParams { host }, package| (package, host))
.no_display()
.with_about("Add an address to this host")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove_address)
.with_inherited(|AddressApiParams { host }, package| (package, host))
.no_display()
.with_about("Remove an address from this host")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_addresses)
.with_inherited(|AddressApiParams { host }, package| (package, host))
.with_custom_display_fn(|_, res| {
for address in res {
println!("{address}")
}
Ok(())
})
.with_about("List addresses for this host")
.with_call_remote::<CliContext>(),
)
}
#[derive(Deserialize, Serialize, Parser)]
pub struct AddressParams {
pub address: HostAddress,
}
pub async fn add_address(
ctx: RpcContext,
AddressParams { address }: AddressParams,
(package, host): (PackageId, HostId),
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
if let HostAddress::Onion { address } = address {
db.as_private()
.as_key_store()
.as_onion()
.get_key(&address)?;
}
db.as_public_mut()
.as_package_data_mut()
.as_idx_mut(&package)
.or_not_found(&package)?
.as_hosts_mut()
.as_idx_mut(&host)
.or_not_found(&host)?
.as_addresses_mut()
.mutate(|a| Ok(a.insert(address)))
})
.await?;
let service = ctx.services.get(&package).await;
let service_ref = service.as_ref().or_not_found(&package)?;
service_ref.update_host(host).await?;
Ok(())
}
pub async fn remove_address(
ctx: RpcContext,
AddressParams { address }: AddressParams,
(package, host): (PackageId, HostId),
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_public_mut()
.as_package_data_mut()
.as_idx_mut(&package)
.or_not_found(&package)?
.as_hosts_mut()
.as_idx_mut(&host)
.or_not_found(&host)?
.as_addresses_mut()
.mutate(|a| Ok(a.remove(&address)))
})
.await?;
let service = ctx.services.get(&package).await;
let service_ref = service.as_ref().or_not_found(&package)?;
service_ref.update_host(host).await?;
Ok(())
}
pub async fn list_addresses(
ctx: RpcContext,
_: Empty,
(package, host): (PackageId, HostId),
) -> Result<BTreeSet<HostAddress>, Error> {
ctx.db
.peek()
.await
.into_public()
.into_package_data()
.into_idx(&package)
.or_not_found(&package)?
.into_hosts()
.into_idx(&host)
.or_not_found(&host)?
.into_addresses()
.de()
}

View File

@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use crate::account::AccountInfo;
use crate::net::acme::AcmeCertStore;
use crate::net::ssl::CertStore;
use crate::net::tor::OnionStore;
use crate::prelude::*;
@@ -10,13 +11,15 @@ use crate::prelude::*;
pub struct KeyStore {
pub onion: OnionStore,
pub local_certs: CertStore,
// pub letsencrypt_certs: BTreeMap<BTreeSet<InternedString>, CertData>
#[serde(default)]
pub acme: AcmeCertStore,
}
impl KeyStore {
pub fn new(account: &AccountInfo) -> Result<Self, Error> {
let mut res = Self {
onion: OnionStore::new(),
local_certs: CertStore::new(account)?,
acme: AcmeCertStore::new(),
};
res.onion.insert(account.tor_key.clone());
Ok(res)

View File

@@ -1,5 +1,6 @@
use rpc_toolkit::{Context, HandlerExt, ParentHandler};
pub mod acme;
pub mod dhcp;
pub mod dns;
pub mod forward;
@@ -28,4 +29,8 @@ pub fn net<C: Context>() -> ParentHandler<C> {
"dhcp",
dhcp::dhcp::<C>().with_about("Command to update IP assigned from dhcp"),
)
.subcommand(
"acme",
acme::acme::<C>().with_about("Setup automatic clearnet certificate acquisition"),
)
}

View File

@@ -261,7 +261,7 @@ impl NetService {
errors.into_result()
}
async fn update(&mut self, id: HostId, host: Host) -> Result<(), Error> {
pub async fn update(&mut self, id: HostId, host: Host) -> Result<(), Error> {
let ctrl = self.net_controller()?;
let mut hostname_info = BTreeMap::new();
let binds = self.binds.entry(id.clone()).or_default();
@@ -330,16 +330,29 @@ impl NetService {
}
HostAddress::Domain { address } => {
if hostnames.insert(address.clone()) {
let address = Some(address.clone());
rcs.push(
ctrl.vhost
.add(
Some(address.clone()),
address.clone(),
external,
target,
connect_ssl.clone(),
)
.await?,
);
if ssl.preferred_external_port == 443 {
rcs.push(
ctrl.vhost
.add(
address.clone(),
5443,
target,
connect_ssl.clone(),
)
.await?,
);
}
}
}
}
@@ -363,11 +376,32 @@ impl NetService {
network_interface_id: interface.clone(),
public: false,
hostname: IpHostname::Local {
value: format!("{hostname}.local"),
value: InternedString::from_display(&{
let hostname = &hostname;
lazy_format!("{hostname}.local")
}),
port: new_lan_bind.0.assigned_port,
ssl_port: new_lan_bind.0.assigned_ssl_port,
},
});
for address in host.addresses() {
if let HostAddress::Domain { address } = address {
if let Some(ssl) = &new_lan_bind.1 {
if ssl.preferred_external_port == 443 {
bind_hostname_info.push(HostnameInfo::Ip {
network_interface_id: interface.clone(),
public: false,
hostname: IpHostname::Domain {
domain: address.clone(),
subdomain: None,
port: None,
ssl_port: Some(443),
},
});
}
}
}
}
if let Some(ipv4) = ip_info.ipv4 {
bind_hostname_info.push(HostnameInfo::Ip {
network_interface_id: interface.clone(),
@@ -515,6 +549,7 @@ impl NetService {
ctrl.tor.gc(Some(addr.clone()), None).await?;
}
}
self.net_controller()?
.db
.mutate(|db| {

View File

@@ -47,13 +47,16 @@ pub enum IpHostname {
ssl_port: Option<u16>,
},
Local {
value: String,
#[ts(type = "string")]
value: InternedString,
port: Option<u16>,
ssl_port: Option<u16>,
},
Domain {
domain: String,
subdomain: Option<String>,
#[ts(type = "string")]
domain: InternedString,
#[ts(type = "string | null")]
subdomain: Option<InternedString>,
port: Option<u16>,
ssl_port: Option<u16>,
},

View File

@@ -26,7 +26,7 @@ use ts_rs::TS;
use crate::context::{CliContext, RpcContext};
use crate::logs::{journalctl, LogSource, LogsParams};
use crate::prelude::*;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
use crate::util::serde::{display_serializable, Base64, HandlerExtSerde, WithIoFormat};
use crate::util::Invoke;
pub const SYSTEMD_UNIT: &str = "tor@default";
@@ -59,7 +59,9 @@ impl Model<OnionStore> {
self.insert(&key.public().get_onion_address(), &key)
}
pub fn get_key(&self, address: &OnionAddressV3) -> Result<TorSecretKeyV3, Error> {
self.as_idx(address).or_not_found(address)?.de()
self.as_idx(address)
.or_not_found(lazy_format!("private key for {address}"))?
.de()
}
}
@@ -108,7 +110,85 @@ pub fn tor<C: Context>() -> ParentHandler<C> {
.with_about("Reset Tor daemon")
.with_call_remote::<CliContext>(),
)
.subcommand(
"key",
key::<C>().with_about("Manage the onion service key store"),
)
}
pub fn key<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"generate",
from_fn_async(generate_key)
.with_about("Generate an onion service key and add it to the key store")
.with_call_remote::<CliContext>(),
)
.subcommand(
"add",
from_fn_async(add_key)
.with_about("Add an onion service key to the key store")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(list_keys)
.with_custom_display_fn(|_, res| {
for addr in res {
println!("{addr}");
}
Ok(())
})
.with_about("List onion services with keys in the key store")
.with_call_remote::<CliContext>(),
)
}
pub async fn generate_key(ctx: RpcContext) -> Result<OnionAddressV3, Error> {
ctx.db
.mutate(|db| {
Ok(db
.as_private_mut()
.as_key_store_mut()
.as_onion_mut()
.new_key()?
.public()
.get_onion_address())
})
.await
}
#[derive(Deserialize, Serialize, Parser)]
pub struct AddKeyParams {
pub key: Base64<[u8; 64]>,
}
pub async fn add_key(
ctx: RpcContext,
AddKeyParams { key }: AddKeyParams,
) -> Result<OnionAddressV3, Error> {
let key = TorSecretKeyV3::from(key.0);
ctx.db
.mutate(|db| {
db.as_private_mut()
.as_key_store_mut()
.as_onion_mut()
.insert_key(&key)
})
.await?;
Ok(key.public().get_onion_address())
}
pub async fn list_keys(ctx: RpcContext) -> Result<Vec<OnionAddressV3>, Error> {
ctx.db
.peek()
.await
.into_private()
.into_key_store()
.into_onion()
.keys()
}
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]

View File

@@ -4,6 +4,7 @@ use std::str::FromStr;
use std::sync::{Arc, Weak};
use std::time::Duration;
use async_acme::acme::ACME_TLS_ALPN_NAME;
use axum::body::Body;
use axum::extract::Request;
use axum::response::Response;
@@ -15,31 +16,47 @@ use models::ResultExt;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{Mutex, RwLock};
use tokio::sync::{watch, Mutex, RwLock};
use tokio_rustls::rustls::crypto::CryptoProvider;
use tokio_rustls::rustls::pki_types::{
CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName,
};
use tokio_rustls::rustls::server::Acceptor;
use tokio_rustls::rustls::server::{Acceptor, ResolvesServerCert};
use tokio_rustls::rustls::sign::CertifiedKey;
use tokio_rustls::rustls::{RootCertStore, ServerConfig};
use tokio_rustls::{LazyConfigAcceptor, TlsConnector};
use tokio_stream::wrappers::WatchStream;
use tokio_stream::StreamExt;
use tracing::instrument;
use ts_rs::TS;
use crate::db::model::Database;
use crate::net::acme::AcmeCertCache;
use crate::net::static_server::server_error;
use crate::prelude::*;
use crate::util::io::BackTrackingIO;
use crate::util::sync::SyncMutex;
use crate::util::serde::MaybeUtf8String;
#[derive(Debug)]
struct SingleCertResolver(Arc<CertifiedKey>);
impl ResolvesServerCert for SingleCertResolver {
fn resolve(&self, _: tokio_rustls::rustls::server::ClientHello) -> Option<Arc<CertifiedKey>> {
Some(self.0.clone())
}
}
// not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353
pub struct VHostController {
crypto_provider: Arc<CryptoProvider>,
db: TypedPatchDb<Database>,
servers: Mutex<BTreeMap<u16, VHostServer>>,
}
impl VHostController {
pub fn new(db: TypedPatchDb<Database>) -> Self {
Self {
crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()),
db,
servers: Mutex::new(BTreeMap::new()),
}
@@ -56,7 +73,8 @@ impl VHostController {
let server = if let Some(server) = writable.remove(&external) {
server
} else {
VHostServer::new(external, self.db.clone()).await?
tracing::info!("Listening on {external}");
VHostServer::new(external, self.db.clone(), self.crypto_provider.clone()).await?
};
let rc = server
.add(
@@ -108,7 +126,11 @@ struct VHostServer {
}
impl VHostServer {
#[instrument(skip_all)]
async fn new(port: u16, db: TypedPatchDb<Database>) -> Result<Self, Error> {
async fn new(port: u16, db: TypedPatchDb<Database>, crypto_provider: Arc<CryptoProvider>) -> Result<Self, Error> {
let acme_tls_alpn_cache = Arc::new(SyncMutex::new(BTreeMap::<
InternedString,
watch::Receiver<Option<Arc<CertifiedKey>>>,
>::new()));
// check if port allowed
let listener = TcpListener::bind(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), port))
.await
@@ -133,9 +155,11 @@ impl VHostServer {
let mut stream = BackTrackingIO::new(stream);
let mapping = mapping.clone();
let db = db.clone();
let acme_tls_alpn_cache = acme_tls_alpn_cache.clone();
let crypto_provider = crypto_provider.clone();
tokio::spawn(async move {
if let Err(e) = async {
let mid = match LazyConfigAcceptor::new(
let mid: tokio_rustls::StartHandshake<&mut BackTrackingIO<TcpStream>> = match LazyConfigAcceptor::new(
Acceptor::default(),
&mut stream,
)
@@ -206,38 +230,102 @@ impl VHostServer {
.map(|(target, _)| target.clone())
};
if let Some(target) = target {
let mut tcp_stream =
TcpStream::connect(target.addr).await?;
let hostnames = target_name
.into_iter()
.chain(
db.peek()
.await
.into_public()
.into_server_info()
.into_ip_info()
.into_entries()?
.into_iter()
.flat_map(|(_, ips)| [
ips.as_ipv4().de().map(|ip| ip.map(IpAddr::V4)),
ips.as_ipv6().de().map(|ip| ip.map(IpAddr::V6))
])
.filter_map(|a| a.transpose())
.map(|a| a.map(|ip| InternedString::from_display(&ip)))
.collect::<Result<Vec<_>, _>>()?,
)
.collect();
let key = db
.mutate(|v| {
v.as_private_mut()
.as_key_store_mut()
.as_local_certs_mut()
.cert_for(&hostnames)
})
.await?;
let cfg = ServerConfig::builder()
.with_no_client_auth();
let mut cfg =
let peek = db.peek().await;
let root = peek.as_private().as_key_store().as_local_certs().as_root_cert().de()?;
let mut cfg = match async {
if let Some(acme_settings) = peek.as_public().as_server_info().as_acme().de()? {
if let Some(domain) = target_name.as_ref().filter(|target_name| acme_settings.domains.contains(*target_name)) {
if mid
.client_hello()
.alpn()
.into_iter()
.flatten()
.any(|alpn| alpn == ACME_TLS_ALPN_NAME)
{
let cert = WatchStream::new(
acme_tls_alpn_cache.peek(|c| c.get(&**domain).cloned())
.ok_or_else(|| {
Error::new(
eyre!("No challenge recv available for {domain}"),
ErrorKind::OpenSsl
)
})?,
);
tracing::info!("Waiting for verification cert for {domain}");
let cert = cert
.filter(|c| c.is_some())
.next()
.await
.flatten()
.ok_or_else(|| {
Error::new(eyre!("No challenge available for {domain}"), ErrorKind::OpenSsl)
})?;
tracing::info!("Verification cert received for {domain}");
let mut cfg = ServerConfig::builder_with_provider(crypto_provider.clone())
.with_safe_default_protocol_versions()
.with_kind(crate::ErrorKind::OpenSsl)?
.with_no_client_auth()
.with_cert_resolver(Arc::new(SingleCertResolver(cert)));
cfg.alpn_protocols = vec![ACME_TLS_ALPN_NAME.to_vec()];
return Ok(Err(cfg));
} else {
let domains = [domain.to_string()];
let (send, recv) = watch::channel(None);
acme_tls_alpn_cache.mutate(|c| c.insert(domain.clone(), recv));
let cert =
async_acme::rustls_helper::order(
|_, cert| {
send.send_replace(Some(Arc::new(cert)));
Ok(())
},
acme_settings.provider.as_str(),
&domains,
Some(&AcmeCertCache(&db)),
&acme_settings.contact,
)
.await
.with_kind(ErrorKind::OpenSsl)?;
return Ok(Ok(
ServerConfig::builder_with_provider(crypto_provider.clone())
.with_safe_default_protocol_versions()
.with_kind(crate::ErrorKind::OpenSsl)?
.with_no_client_auth()
.with_cert_resolver(Arc::new(SingleCertResolver(Arc::new(cert))))
));
}
}
}
let hostnames = target_name
.into_iter()
.chain(
peek
.as_public()
.as_server_info()
.as_ip_info()
.as_entries()?
.into_iter()
.flat_map(|(_, ips)| [
ips.as_ipv4().de().map(|ip| ip.map(IpAddr::V4)),
ips.as_ipv6().de().map(|ip| ip.map(IpAddr::V6))
])
.filter_map(|a| a.transpose())
.map(|a| a.map(|ip| InternedString::from_display(&ip)))
.collect::<Result<Vec<_>, _>>()?,
)
.collect();
let key = db
.mutate(|v| {
v.as_private_mut()
.as_key_store_mut()
.as_local_certs_mut()
.cert_for(&hostnames)
})
.await?;
let cfg = ServerConfig::builder_with_provider(crypto_provider.clone())
.with_safe_default_protocol_versions()
.with_kind(crate::ErrorKind::OpenSsl)?
.with_no_client_auth();
if mid.client_hello().signature_schemes().contains(
&tokio_rustls::rustls::SignatureScheme::ED25519,
) {
@@ -275,16 +363,34 @@ impl VHostServer {
)),
)
}
.with_kind(crate::ErrorKind::OpenSsl)?;
.with_kind(crate::ErrorKind::OpenSsl)
.map(Ok)
}.await? {
Ok(a) => a,
Err(cfg) => {
tracing::info!("performing ACME auth challenge");
let mut accept = mid.into_stream(Arc::new(cfg));
let io = accept.get_mut().unwrap();
let buffered = io.stop_buffering();
io.write_all(&buffered).await?;
accept.await?;
tracing::info!("ACME auth challenge completed");
return Ok(());
}
};
let mut tcp_stream =
TcpStream::connect(target.addr).await?;
match target.connect_ssl {
Ok(()) => {
let mut client_cfg =
tokio_rustls::rustls::ClientConfig::builder()
tokio_rustls::rustls::ClientConfig::builder_with_provider(crypto_provider)
.with_safe_default_protocol_versions()
.with_kind(crate::ErrorKind::OpenSsl)?
.with_root_certificates({
let mut store = RootCertStore::empty();
store.add(
CertificateDer::from(
key.root.to_der()?,
root.to_der()?,
),
).with_kind(crate::ErrorKind::OpenSsl)?;
store

View File

@@ -16,7 +16,7 @@ use futures::stream::FusedStream;
use futures::{SinkExt, StreamExt, TryStreamExt};
use imbl_value::{json, InternedString};
use itertools::Itertools;
use models::{ActionId, ImageId, PackageId, ProcedureName};
use models::{ActionId, HostId, ImageId, PackageId, ProcedureName};
use nix::sys::signal::Signal;
use persistent_container::{PersistentContainer, Subcontainer};
use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor};
@@ -603,6 +603,30 @@ impl Service {
memory_usage: MiB::from_MiB(used),
})
}
pub async fn update_host(&self, host_id: HostId) -> Result<(), Error> {
let host = self
.seed
.ctx
.db
.peek()
.await
.as_public()
.as_package_data()
.as_idx(&self.seed.id)
.or_not_found(&self.seed.id)?
.as_hosts()
.as_idx(&host_id)
.or_not_found(&host_id)?
.de()?;
self.seed
.persistent_container
.net_service
.lock()
.await
.update(host_id, host)
.await
}
}
#[derive(Debug, Clone)]

View File

@@ -1029,6 +1029,12 @@ impl<T: TryFrom<Vec<u8>>> FromStr for Base64<T> {
})
}
}
impl<T: TryFrom<Vec<u8>>> ValueParserFactory for Base64<T> {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {
Self::Parser::new()
}
}
impl<'de, T: TryFrom<Vec<u8>>> Deserialize<'de> for Base64<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -1215,6 +1221,30 @@ impl PemEncoding for ed25519_dalek::SigningKey {
}
}
#[derive(Clone, Debug)]
pub struct Pkcs8Doc {
pub tag: String,
pub document: pkcs8::Document,
}
impl PemEncoding for Pkcs8Doc {
fn from_pem<E: serde::de::Error>(pem: &str) -> Result<Self, E> {
let (tag, document) = pkcs8::Document::from_pem(pem).map_err(E::custom)?;
Ok(Pkcs8Doc {
tag: tag.into(),
document,
})
}
fn to_pem<E: serde::ser::Error>(&self) -> Result<String, E> {
der::pem::encode_string(
&self.tag,
pkcs8::LineEnding::default(),
self.document.as_bytes(),
)
.map_err(E::custom)
}
}
pub mod pem {
use serde::{Deserialize, Deserializer, Serializer};