mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
rename appmgr
This commit is contained in:
committed by
Aiden McClelland
parent
9cf379f9ee
commit
edde478382
10
backend/src/net/cert-local.csr.conf.template
Normal file
10
backend/src/net/cert-local.csr.conf.template
Normal file
@@ -0,0 +1,10 @@
|
||||
[req]
|
||||
default_bits = 4096
|
||||
default_md = sha256
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
CN = {hostname}.local
|
||||
O = Start9 Labs
|
||||
OU = Embassy
|
||||
167
backend/src/net/interface.rs
Normal file
167
backend/src/net/interface.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::TryStreamExt;
|
||||
use indexmap::IndexSet;
|
||||
use itertools::Either;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use sqlx::{Executor, Sqlite};
|
||||
use torut::onion::TorSecretKeyV3;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::db::model::{InterfaceAddressMap, InterfaceAddresses};
|
||||
use crate::id::Id;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::serde::Port;
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Interfaces(pub BTreeMap<InterfaceId, Interface>); // TODO
|
||||
impl Interfaces {
|
||||
#[instrument(skip(secrets))]
|
||||
pub async fn install<Ex>(
|
||||
&self,
|
||||
secrets: &mut Ex,
|
||||
package_id: &PackageId,
|
||||
) -> Result<InterfaceAddressMap, Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
let mut interface_addresses = InterfaceAddressMap(BTreeMap::new());
|
||||
for (id, iface) in &self.0 {
|
||||
let mut addrs = InterfaceAddresses {
|
||||
tor_address: None,
|
||||
lan_address: None,
|
||||
};
|
||||
if iface.tor_config.is_some() || iface.lan_config.is_some() {
|
||||
let key = TorSecretKeyV3::generate();
|
||||
let key_vec = key.as_bytes().to_vec();
|
||||
sqlx::query!(
|
||||
"INSERT OR IGNORE INTO tor (package, interface, key) VALUES (?, ?, ?)",
|
||||
**package_id,
|
||||
**id,
|
||||
key_vec,
|
||||
)
|
||||
.execute(&mut *secrets)
|
||||
.await?;
|
||||
let key_row = sqlx::query!(
|
||||
"SELECT key FROM tor WHERE package = ? AND interface = ?",
|
||||
**package_id,
|
||||
**id,
|
||||
)
|
||||
.fetch_one(&mut *secrets)
|
||||
.await?;
|
||||
let mut key = [0_u8; 64];
|
||||
key.clone_from_slice(&key_row.key);
|
||||
let key = TorSecretKeyV3::from(key);
|
||||
let onion = key.public().get_onion_address();
|
||||
if iface.tor_config.is_some() {
|
||||
addrs.tor_address = Some(onion.to_string());
|
||||
}
|
||||
if iface.lan_config.is_some() {
|
||||
addrs.lan_address =
|
||||
Some(format!("{}.local", onion.get_address_without_dot_onion()));
|
||||
}
|
||||
}
|
||||
interface_addresses.0.insert(id.clone(), addrs);
|
||||
}
|
||||
Ok(interface_addresses)
|
||||
}
|
||||
|
||||
#[instrument(skip(secrets))]
|
||||
pub async fn tor_keys<Ex>(
|
||||
&self,
|
||||
secrets: &mut Ex,
|
||||
package_id: &PackageId,
|
||||
) -> Result<BTreeMap<InterfaceId, TorSecretKeyV3>, Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
Ok(sqlx::query!(
|
||||
"SELECT interface, key FROM tor WHERE package = ?",
|
||||
**package_id
|
||||
)
|
||||
.fetch_many(secrets)
|
||||
.map_err(Error::from)
|
||||
.try_filter_map(|qr| async move {
|
||||
Ok(if let Either::Right(r) = qr {
|
||||
let mut buf = [0; 64];
|
||||
buf.clone_from_slice(r.key.get(0..64).ok_or_else(|| {
|
||||
Error::new(eyre!("Invalid Tor Key Length"), crate::ErrorKind::Database)
|
||||
})?);
|
||||
Some((InterfaceId::from(Id::try_from(r.interface)?), buf.into()))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
})
|
||||
.try_collect()
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
pub struct InterfaceId<S: AsRef<str> = String>(Id<S>);
|
||||
impl<S: AsRef<str>> From<Id<S>> for InterfaceId<S> {
|
||||
fn from(id: Id<S>) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> std::fmt::Display for InterfaceId<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> std::ops::Deref for InterfaceId<S> {
|
||||
type Target = S;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<str> for InterfaceId<S> {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de, S> Deserialize<'de> for InterfaceId<S>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
Id<S>: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(InterfaceId(Deserialize::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
impl<S: AsRef<str>> AsRef<Path> for InterfaceId<S> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Interface {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub tor_config: Option<TorConfig>,
|
||||
pub lan_config: Option<BTreeMap<Port, LanPortConfig>>,
|
||||
pub ui: bool,
|
||||
pub protocols: IndexSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct TorConfig {
|
||||
pub port_mapping: BTreeMap<Port, Port>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LanPortConfig {
|
||||
pub ssl: bool,
|
||||
pub mapping: u16,
|
||||
}
|
||||
241
backend/src/net/mdns.rs
Normal file
241
backend/src/net/mdns.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::IpAddr;
|
||||
|
||||
use avahi_sys::{
|
||||
self, avahi_client_errno, avahi_entry_group_add_service, avahi_entry_group_commit,
|
||||
avahi_entry_group_free, avahi_entry_group_reset, avahi_free, avahi_strerror, AvahiClient,
|
||||
AvahiEntryGroup,
|
||||
};
|
||||
use color_eyre::eyre::eyre;
|
||||
use libc::c_void;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::Mutex;
|
||||
use torut::onion::TorSecretKeyV3;
|
||||
|
||||
use super::interface::InterfaceId;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
pub async fn resolve_mdns(hostname: &str) -> Result<IpAddr, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("avahi-resolve-host-name")
|
||||
.arg("-4")
|
||||
.arg(hostname)
|
||||
.invoke(crate::ErrorKind::Network)
|
||||
.await?,
|
||||
)?
|
||||
.split_once("\t")
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("Failed to resolve hostname: {}", hostname),
|
||||
crate::ErrorKind::Network,
|
||||
)
|
||||
})?
|
||||
.1
|
||||
.trim()
|
||||
.parse()?)
|
||||
}
|
||||
|
||||
pub struct MdnsController(Mutex<MdnsControllerInner>);
|
||||
impl MdnsController {
|
||||
pub fn init() -> Self {
|
||||
MdnsController(Mutex::new(MdnsControllerInner::init()))
|
||||
}
|
||||
pub async fn add<'a, I: IntoIterator<Item = (InterfaceId, TorSecretKeyV3)>>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
interfaces: I,
|
||||
) {
|
||||
self.0.lock().await.add(pkg_id, interfaces)
|
||||
}
|
||||
pub async fn remove<I: IntoIterator<Item = InterfaceId>>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
interfaces: I,
|
||||
) {
|
||||
self.0.lock().await.remove(pkg_id, interfaces)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MdnsControllerInner {
|
||||
hostname: Vec<u8>,
|
||||
hostname_raw: *const libc::c_char,
|
||||
entry_group: *mut AvahiEntryGroup,
|
||||
services: BTreeMap<(PackageId, InterfaceId), TorSecretKeyV3>,
|
||||
client_error: std::pin::Pin<Box<i32>>,
|
||||
}
|
||||
unsafe impl Send for MdnsControllerInner {}
|
||||
unsafe impl Sync for MdnsControllerInner {}
|
||||
|
||||
impl MdnsControllerInner {
|
||||
fn load_services(&mut self) {
|
||||
unsafe {
|
||||
tracing::debug!("Loading services for mDNS");
|
||||
let mut res;
|
||||
let http_tcp_cstr = std::ffi::CString::new("_http._tcp")
|
||||
.expect("Could not cast _http._tcp to c string");
|
||||
res = avahi_entry_group_add_service(
|
||||
self.entry_group,
|
||||
avahi_sys::AVAHI_IF_UNSPEC,
|
||||
avahi_sys::AVAHI_PROTO_UNSPEC,
|
||||
avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST,
|
||||
self.hostname_raw,
|
||||
http_tcp_cstr.as_ptr(),
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
443,
|
||||
// below is a secret final argument that the type signature of this function does not tell you that it
|
||||
// needs. This is because the C lib function takes a variable number of final arguments indicating the
|
||||
// desired TXT records to add to this service entry. The way it decides when to stop taking arguments
|
||||
// from the stack and dereferencing them is when it finds a null pointer...because fuck you, that's why.
|
||||
// The consequence of this is that forgetting this last argument will cause segfaults or other undefined
|
||||
// behavior. Welcome back to the stone age motherfucker.
|
||||
std::ptr::null::<libc::c_char>(),
|
||||
);
|
||||
if res < avahi_sys::AVAHI_OK {
|
||||
let e_str = avahi_strerror(res);
|
||||
tracing::error!(
|
||||
"Could not add service to Avahi entry group: {:?}",
|
||||
std::ffi::CStr::from_ptr(e_str)
|
||||
);
|
||||
avahi_free(e_str as *mut c_void);
|
||||
panic!("Failed to load Avahi services");
|
||||
}
|
||||
tracing::info!(
|
||||
"Published {:?}",
|
||||
std::ffi::CStr::from_ptr(self.hostname_raw)
|
||||
);
|
||||
for key in self.services.values() {
|
||||
let lan_address = key
|
||||
.public()
|
||||
.get_onion_address()
|
||||
.get_address_without_dot_onion()
|
||||
+ ".local";
|
||||
tracing::debug!("Adding mdns CNAME entry for {}", &lan_address);
|
||||
let lan_address_ptr = std::ffi::CString::new(lan_address)
|
||||
.expect("Could not cast lan address to c string");
|
||||
res = avahi_sys::avahi_entry_group_add_record(
|
||||
self.entry_group,
|
||||
avahi_sys::AVAHI_IF_UNSPEC,
|
||||
avahi_sys::AVAHI_PROTO_UNSPEC,
|
||||
avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST
|
||||
| avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_ALLOW_MULTIPLE,
|
||||
lan_address_ptr.as_ptr(),
|
||||
avahi_sys::AVAHI_DNS_CLASS_IN as u16,
|
||||
avahi_sys::AVAHI_DNS_TYPE_CNAME as u16,
|
||||
avahi_sys::AVAHI_DEFAULT_TTL,
|
||||
self.hostname.as_ptr().cast(),
|
||||
self.hostname.len(),
|
||||
);
|
||||
if res < avahi_sys::AVAHI_OK {
|
||||
let e_str = avahi_strerror(res);
|
||||
tracing::error!(
|
||||
"Could not add CNAME record to Avahi entry group: {:?}",
|
||||
std::ffi::CStr::from_ptr(e_str)
|
||||
);
|
||||
avahi_free(e_str as *mut c_void);
|
||||
panic!("Failed to load Avahi services");
|
||||
}
|
||||
tracing::info!("Published {:?}", lan_address_ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn init() -> Self {
|
||||
unsafe {
|
||||
tracing::debug!("Initializing mDNS controller");
|
||||
let simple_poll = avahi_sys::avahi_simple_poll_new();
|
||||
let poll = avahi_sys::avahi_simple_poll_get(simple_poll);
|
||||
let mut box_err = Box::pin(0 as i32);
|
||||
let err_c: *mut i32 = box_err.as_mut().get_mut();
|
||||
let avahi_client = avahi_sys::avahi_client_new(
|
||||
poll,
|
||||
avahi_sys::AvahiClientFlags::AVAHI_CLIENT_NO_FAIL,
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
err_c,
|
||||
);
|
||||
if avahi_client == std::ptr::null_mut::<AvahiClient>() {
|
||||
let e_str = avahi_strerror(*box_err);
|
||||
tracing::error!(
|
||||
"Could not create avahi client: {:?}",
|
||||
std::ffi::CStr::from_ptr(e_str)
|
||||
);
|
||||
avahi_free(e_str as *mut c_void);
|
||||
panic!("Failed to create Avahi Client");
|
||||
}
|
||||
let group =
|
||||
avahi_sys::avahi_entry_group_new(avahi_client, Some(noop), std::ptr::null_mut());
|
||||
if group == std::ptr::null_mut() {
|
||||
let e_str = avahi_strerror(avahi_client_errno(avahi_client));
|
||||
tracing::error!(
|
||||
"Could not create avahi entry group: {:?}",
|
||||
std::ffi::CStr::from_ptr(e_str)
|
||||
);
|
||||
avahi_free(e_str as *mut c_void);
|
||||
panic!("Failed to create Avahi Entry Group");
|
||||
}
|
||||
let mut hostname_buf = vec![0];
|
||||
let hostname_raw = avahi_sys::avahi_client_get_host_name_fqdn(avahi_client);
|
||||
hostname_buf
|
||||
.extend_from_slice(std::ffi::CStr::from_ptr(hostname_raw).to_bytes_with_nul());
|
||||
let buflen = hostname_buf.len();
|
||||
debug_assert!(hostname_buf.ends_with(b".local\0"));
|
||||
debug_assert!(!hostname_buf[..(buflen - 7)].contains(&b'.'));
|
||||
// assume fixed length prefix on hostname due to local address
|
||||
hostname_buf[0] = (buflen - 8) as u8; // set the prefix length to len - 8 (leading byte, .local, nul) for the main address
|
||||
hostname_buf[buflen - 7] = 5; // set the prefix length to 5 for "local"
|
||||
|
||||
let mut res = MdnsControllerInner {
|
||||
hostname: hostname_buf,
|
||||
hostname_raw,
|
||||
entry_group: group,
|
||||
services: BTreeMap::new(),
|
||||
client_error: box_err,
|
||||
};
|
||||
res.load_services();
|
||||
avahi_entry_group_commit(res.entry_group);
|
||||
res
|
||||
}
|
||||
}
|
||||
fn sync(&mut self) {
|
||||
unsafe {
|
||||
avahi_entry_group_reset(self.entry_group);
|
||||
self.load_services();
|
||||
avahi_entry_group_commit(self.entry_group);
|
||||
}
|
||||
}
|
||||
fn add<'a, I: IntoIterator<Item = (InterfaceId, TorSecretKeyV3)>>(
|
||||
&mut self,
|
||||
pkg_id: &PackageId,
|
||||
interfaces: I,
|
||||
) {
|
||||
self.services.extend(
|
||||
interfaces
|
||||
.into_iter()
|
||||
.map(|(interface_id, key)| ((pkg_id.clone(), interface_id), key)),
|
||||
);
|
||||
self.sync();
|
||||
}
|
||||
fn remove<I: IntoIterator<Item = InterfaceId>>(&mut self, pkg_id: &PackageId, interfaces: I) {
|
||||
for interface_id in interfaces {
|
||||
self.services.remove(&(pkg_id.clone(), interface_id));
|
||||
}
|
||||
self.sync();
|
||||
}
|
||||
}
|
||||
impl Drop for MdnsControllerInner {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
avahi_free(self.hostname_raw as *mut c_void);
|
||||
avahi_entry_group_free(self.entry_group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn noop(
|
||||
_group: *mut avahi_sys::AvahiEntryGroup,
|
||||
_state: avahi_sys::AvahiEntryGroupState,
|
||||
_userdata: *mut core::ffi::c_void,
|
||||
) {
|
||||
}
|
||||
184
backend/src/net/mod.rs
Normal file
184
backend/src/net/mod.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::x509::X509;
|
||||
use rpc_toolkit::command;
|
||||
use sqlx::SqlitePool;
|
||||
use torut::onion::{OnionAddressV3, TorSecretKeyV3};
|
||||
use tracing::instrument;
|
||||
|
||||
use self::interface::{Interface, InterfaceId};
|
||||
#[cfg(feature = "avahi")]
|
||||
use self::mdns::MdnsController;
|
||||
use self::nginx::NginxController;
|
||||
use self::ssl::SslManager;
|
||||
use self::tor::TorController;
|
||||
use crate::net::interface::TorConfig;
|
||||
use crate::net::nginx::InterfaceMetadata;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::Error;
|
||||
|
||||
pub mod interface;
|
||||
#[cfg(feature = "avahi")]
|
||||
pub mod mdns;
|
||||
pub mod nginx;
|
||||
pub mod ssl;
|
||||
pub mod tor;
|
||||
pub mod wifi;
|
||||
|
||||
const PACKAGE_CERT_PATH: &str = "/var/lib/embassy/ssl";
|
||||
|
||||
#[command(subcommands(tor::tor))]
|
||||
pub fn net() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Indicates that the net controller has created the
|
||||
/// SSL keys
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GeneratedCertificateMountPoint(());
|
||||
|
||||
pub struct NetController {
|
||||
pub tor: TorController,
|
||||
#[cfg(feature = "avahi")]
|
||||
pub mdns: MdnsController,
|
||||
pub nginx: NginxController,
|
||||
pub ssl: SslManager,
|
||||
}
|
||||
impl NetController {
|
||||
#[instrument(skip(db))]
|
||||
pub async fn init(
|
||||
embassyd_addr: SocketAddr,
|
||||
embassyd_tor_key: TorSecretKeyV3,
|
||||
tor_control: SocketAddr,
|
||||
db: SqlitePool,
|
||||
import_root_ca: Option<(PKey<Private>, X509)>,
|
||||
) -> Result<Self, Error> {
|
||||
let ssl = match import_root_ca {
|
||||
None => SslManager::init(db).await,
|
||||
Some(a) => SslManager::import_root_ca(db, a.0, a.1).await,
|
||||
}?;
|
||||
Ok(Self {
|
||||
tor: TorController::init(embassyd_addr, embassyd_tor_key, tor_control).await?,
|
||||
#[cfg(feature = "avahi")]
|
||||
mdns: MdnsController::init(),
|
||||
nginx: NginxController::init(PathBuf::from("/etc/nginx"), &ssl).await?,
|
||||
ssl,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ssl_directory_for(&self, pkg_id: &PackageId) -> PathBuf {
|
||||
PathBuf::from(format!("{}/{}", PACKAGE_CERT_PATH, pkg_id))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, interfaces, _generated_certificate))]
|
||||
pub async fn add<'a, I>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
ip: Ipv4Addr,
|
||||
interfaces: I,
|
||||
_generated_certificate: GeneratedCertificateMountPoint,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
I: IntoIterator<Item = (InterfaceId, &'a Interface, TorSecretKeyV3)> + Clone,
|
||||
for<'b> &'b I: IntoIterator<Item = &'b (InterfaceId, &'a Interface, TorSecretKeyV3)>,
|
||||
{
|
||||
let interfaces_tor = interfaces
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|i| match i.1.tor_config.clone() {
|
||||
None => None,
|
||||
Some(cfg) => Some((i.0, cfg, i.2)),
|
||||
})
|
||||
.collect::<Vec<(InterfaceId, TorConfig, TorSecretKeyV3)>>();
|
||||
let (tor_res, _, nginx_res) = tokio::join!(
|
||||
self.tor.add(pkg_id, ip, interfaces_tor),
|
||||
{
|
||||
#[cfg(feature = "avahi")]
|
||||
let mdns_fut = self.mdns.add(
|
||||
pkg_id,
|
||||
interfaces
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(interface_id, _, key)| (interface_id, key)),
|
||||
);
|
||||
#[cfg(not(feature = "avahi"))]
|
||||
let mdns_fut = futures::future::ready(());
|
||||
mdns_fut
|
||||
},
|
||||
{
|
||||
let interfaces = interfaces
|
||||
.into_iter()
|
||||
.filter_map(|(id, interface, tor_key)| match &interface.lan_config {
|
||||
None => None,
|
||||
Some(cfg) => Some((
|
||||
id,
|
||||
InterfaceMetadata {
|
||||
dns_base: OnionAddressV3::from(&tor_key.public())
|
||||
.get_address_without_dot_onion(),
|
||||
lan_config: cfg.clone(),
|
||||
protocols: interface.protocols.clone(),
|
||||
},
|
||||
)),
|
||||
});
|
||||
self.nginx.add(&self.ssl, pkg_id.clone(), ip, interfaces)
|
||||
}
|
||||
);
|
||||
tor_res?;
|
||||
nginx_res?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, interfaces))]
|
||||
pub async fn remove<I: IntoIterator<Item = InterfaceId> + Clone>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
interfaces: I,
|
||||
) -> Result<(), Error> {
|
||||
let (tor_res, _, nginx_res) = tokio::join!(
|
||||
self.tor.remove(pkg_id, interfaces.clone()),
|
||||
{
|
||||
#[cfg(feature = "avahi")]
|
||||
let mdns_fut = self.mdns.remove(pkg_id, interfaces);
|
||||
#[cfg(not(feature = "avahi"))]
|
||||
let mdns_fut = futures::future::ready(());
|
||||
mdns_fut
|
||||
},
|
||||
self.nginx.remove(pkg_id)
|
||||
);
|
||||
tor_res?;
|
||||
nginx_res?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn generate_certificate_mountpoint<'a, I>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
interfaces: &I,
|
||||
) -> Result<GeneratedCertificateMountPoint, Error>
|
||||
where
|
||||
I: IntoIterator<Item = (InterfaceId, &'a Interface, TorSecretKeyV3)> + Clone,
|
||||
for<'b> &'b I: IntoIterator<Item = &'b (InterfaceId, &'a Interface, TorSecretKeyV3)>,
|
||||
{
|
||||
tracing::info!("Generating SSL Certificate mountpoints for {}", pkg_id);
|
||||
let package_path = PathBuf::from(PACKAGE_CERT_PATH).join(pkg_id);
|
||||
tokio::fs::create_dir_all(&package_path).await?;
|
||||
for (id, _, key) in interfaces {
|
||||
let dns_base = OnionAddressV3::from(&key.public()).get_address_without_dot_onion();
|
||||
let ssl_path_key = package_path.join(format!("{}.key.pem", id));
|
||||
let ssl_path_cert = package_path.join(format!("{}.cert.pem", id));
|
||||
let (key, chain) = self.ssl.certificate_for(&dns_base, pkg_id).await?;
|
||||
tokio::try_join!(
|
||||
crate::net::ssl::export_key(&key, &ssl_path_key),
|
||||
crate::net::ssl::export_cert(&chain, &ssl_path_cert)
|
||||
)?;
|
||||
}
|
||||
Ok(GeneratedCertificateMountPoint(()))
|
||||
}
|
||||
|
||||
pub async fn export_root_ca(&self) -> Result<(PKey<Private>, X509), Error> {
|
||||
self.ssl.export_root_ca().await
|
||||
}
|
||||
}
|
||||
15
backend/src/net/nginx.conf.template
Normal file
15
backend/src/net/nginx.conf.template
Normal file
@@ -0,0 +1,15 @@
|
||||
server {{
|
||||
listen {listen_args};
|
||||
listen [::]:{listen_args_ipv6};
|
||||
server_name .{hostname}.local;
|
||||
{ssl_certificate_line}
|
||||
{ssl_certificate_key_line}
|
||||
location / {{
|
||||
proxy_pass http://{app_ip}:{internal_port}/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
client_max_body_size 0;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
}}
|
||||
}}
|
||||
237
backend/src/net/nginx.rs
Normal file
237
backend/src/net/nginx.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use futures::FutureExt;
|
||||
use indexmap::IndexSet;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::interface::{InterfaceId, LanPortConfig};
|
||||
use super::ssl::SslManager;
|
||||
use crate::hostname::get_hostname;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::serde::Port;
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
|
||||
pub struct NginxController {
|
||||
pub nginx_root: PathBuf,
|
||||
inner: Mutex<NginxControllerInner>,
|
||||
}
|
||||
impl NginxController {
|
||||
pub async fn init(nginx_root: PathBuf, ssl_manager: &SslManager) -> Result<Self, Error> {
|
||||
Ok(NginxController {
|
||||
inner: Mutex::new(NginxControllerInner::init(&nginx_root, ssl_manager).await?),
|
||||
nginx_root,
|
||||
})
|
||||
}
|
||||
pub async fn add<I: IntoIterator<Item = (InterfaceId, InterfaceMetadata)>>(
|
||||
&self,
|
||||
ssl_manager: &SslManager,
|
||||
package: PackageId,
|
||||
ipv4: Ipv4Addr,
|
||||
interfaces: I,
|
||||
) -> Result<(), Error> {
|
||||
self.inner
|
||||
.lock()
|
||||
.await
|
||||
.add(&self.nginx_root, ssl_manager, package, ipv4, interfaces)
|
||||
.await
|
||||
}
|
||||
pub async fn remove(&self, package: &PackageId) -> Result<(), Error> {
|
||||
self.inner
|
||||
.lock()
|
||||
.await
|
||||
.remove(&self.nginx_root, package)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NginxControllerInner {
|
||||
interfaces: BTreeMap<PackageId, PackageNetInfo>,
|
||||
}
|
||||
impl NginxControllerInner {
|
||||
#[instrument]
|
||||
async fn init(nginx_root: &Path, ssl_manager: &SslManager) -> Result<Self, Error> {
|
||||
let inner = NginxControllerInner {
|
||||
interfaces: BTreeMap::new(),
|
||||
};
|
||||
// write main ssl key/cert to fs location
|
||||
let (key, cert) = ssl_manager
|
||||
.certificate_for(&get_hostname().await?, &"embassy".parse().unwrap())
|
||||
.await?;
|
||||
let ssl_path_key = nginx_root.join(format!("ssl/embassy_main.key.pem"));
|
||||
let ssl_path_cert = nginx_root.join(format!("ssl/embassy_main.cert.pem"));
|
||||
tokio::try_join!(
|
||||
crate::net::ssl::export_key(&key, &ssl_path_key),
|
||||
crate::net::ssl::export_cert(&cert, &ssl_path_cert),
|
||||
)?;
|
||||
Ok(inner)
|
||||
}
|
||||
#[instrument(skip(self, interfaces))]
|
||||
async fn add<I: IntoIterator<Item = (InterfaceId, InterfaceMetadata)>>(
|
||||
&mut self,
|
||||
nginx_root: &Path,
|
||||
ssl_manager: &SslManager,
|
||||
package: PackageId,
|
||||
ipv4: Ipv4Addr,
|
||||
interfaces: I,
|
||||
) -> Result<(), Error> {
|
||||
let interface_map = interfaces
|
||||
.into_iter()
|
||||
.filter(|(_, meta)| {
|
||||
// don't add nginx stuff for anything we can't connect to over some flavor of http
|
||||
(meta.protocols.contains("http") || meta.protocols.contains("https"))
|
||||
// also don't add nginx unless it has at least one exposed port
|
||||
&& meta.lan_config.len() > 0
|
||||
})
|
||||
.collect::<BTreeMap<InterfaceId, InterfaceMetadata>>();
|
||||
|
||||
for (id, meta) in interface_map.iter() {
|
||||
for (port, lan_port_config) in meta.lan_config.iter() {
|
||||
// get ssl certificate chain
|
||||
let (listen_args, ssl_certificate_line, ssl_certificate_key_line) =
|
||||
if lan_port_config.ssl {
|
||||
// these have already been written by the net controller
|
||||
let package_path = nginx_root.join(format!("ssl/{}", package));
|
||||
if tokio::fs::metadata(&package_path).await.is_err() {
|
||||
tokio::fs::create_dir_all(&package_path)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(ErrorKind::Filesystem, package_path.display().to_string())
|
||||
})?;
|
||||
}
|
||||
let ssl_path_key = package_path.join(format!("{}.key.pem", id));
|
||||
let ssl_path_cert = package_path.join(format!("{}.cert.pem", id));
|
||||
let (key, chain) = ssl_manager
|
||||
.certificate_for(&meta.dns_base, &package)
|
||||
.await?;
|
||||
tokio::try_join!(
|
||||
crate::net::ssl::export_key(&key, &ssl_path_key),
|
||||
crate::net::ssl::export_cert(&chain, &ssl_path_cert)
|
||||
)?;
|
||||
(
|
||||
format!("{} ssl", lan_port_config.mapping),
|
||||
format!("ssl_certificate {};", ssl_path_cert.to_str().unwrap()),
|
||||
format!("ssl_certificate_key {};", ssl_path_key.to_str().unwrap()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("{}", lan_port_config.mapping),
|
||||
String::from(""),
|
||||
String::from(""),
|
||||
)
|
||||
};
|
||||
// write nginx configs
|
||||
let nginx_conf_path = nginx_root.join(format!(
|
||||
"sites-available/{}_{}_{}.conf",
|
||||
package, id, port.0
|
||||
));
|
||||
tokio::fs::write(
|
||||
&nginx_conf_path,
|
||||
format!(
|
||||
include_str!("nginx.conf.template"),
|
||||
listen_args = listen_args,
|
||||
listen_args_ipv6 = listen_args,
|
||||
hostname = meta.dns_base,
|
||||
ssl_certificate_line = ssl_certificate_line,
|
||||
ssl_certificate_key_line = ssl_certificate_key_line,
|
||||
app_ip = ipv4,
|
||||
internal_port = port.0,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, nginx_conf_path.display().to_string()))?;
|
||||
let sites_enabled_link_path =
|
||||
nginx_root.join(format!("sites-enabled/{}_{}_{}.conf", package, id, port.0));
|
||||
if tokio::fs::metadata(&sites_enabled_link_path).await.is_ok() {
|
||||
tokio::fs::remove_file(&sites_enabled_link_path).await?;
|
||||
}
|
||||
tokio::fs::symlink(&nginx_conf_path, &sites_enabled_link_path)
|
||||
.await
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, nginx_conf_path.display().to_string()))?;
|
||||
}
|
||||
}
|
||||
match self.interfaces.get_mut(&package) {
|
||||
None => {
|
||||
let info = PackageNetInfo {
|
||||
interfaces: interface_map,
|
||||
};
|
||||
self.interfaces.insert(package, info);
|
||||
}
|
||||
Some(p) => {
|
||||
p.interfaces.extend(interface_map);
|
||||
}
|
||||
};
|
||||
|
||||
self.hup().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn remove(&mut self, nginx_root: &Path, package: &PackageId) -> Result<(), Error> {
|
||||
let removed = self.interfaces.remove(package);
|
||||
if let Some(net_info) = removed {
|
||||
for (id, meta) in net_info.interfaces {
|
||||
for (port, _lan_port_config) in meta.lan_config.iter() {
|
||||
// remove ssl certificates and nginx configs
|
||||
let package_path = nginx_root.join(format!("ssl/{}", package));
|
||||
let enabled_path = nginx_root
|
||||
.join(format!("sites-enabled/{}_{}_{}.conf", package, id, port.0));
|
||||
let available_path = nginx_root.join(format!(
|
||||
"sites-available/{}_{}_{}.conf",
|
||||
package, id, port.0
|
||||
));
|
||||
let _ = tokio::try_join!(
|
||||
async {
|
||||
if tokio::fs::metadata(&package_path).await.is_ok() {
|
||||
tokio::fs::remove_dir_all(&package_path)
|
||||
.map(|res| {
|
||||
res.with_ctx(|_| {
|
||||
(
|
||||
ErrorKind::Filesystem,
|
||||
package_path.display().to_string(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
tokio::fs::remove_file(&enabled_path).map(|res| res.with_ctx(|_| (
|
||||
ErrorKind::Filesystem,
|
||||
enabled_path.display().to_string()
|
||||
))),
|
||||
tokio::fs::remove_file(&available_path).map(|res| res.with_ctx(|_| (
|
||||
ErrorKind::Filesystem,
|
||||
available_path.display().to_string()
|
||||
))),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.hup().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn hup(&self) -> Result<(), Error> {
|
||||
let _ = tokio::process::Command::new("systemctl")
|
||||
.arg("reload")
|
||||
.arg("nginx")
|
||||
.invoke(ErrorKind::Nginx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
struct PackageNetInfo {
|
||||
interfaces: BTreeMap<InterfaceId, InterfaceMetadata>,
|
||||
}
|
||||
pub struct InterfaceMetadata {
|
||||
pub dns_base: String,
|
||||
pub lan_config: BTreeMap<Port, LanPortConfig>,
|
||||
pub protocols: IndexSet<String>,
|
||||
}
|
||||
544
backend/src/net/ssl.rs
Normal file
544
backend/src/net/ssl.rs
Normal file
@@ -0,0 +1,544 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::FutureExt;
|
||||
use openssl::asn1::{Asn1Integer, Asn1Time};
|
||||
use openssl::bn::{BigNum, MsbOption};
|
||||
use openssl::ec::{EcGroup, EcKey};
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::nid::Nid;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509};
|
||||
use openssl::*;
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
|
||||
static CERTIFICATE_VERSION: i32 = 2; // X509 version 3 is actually encoded as '2' in the cert because fuck you.
|
||||
pub const ROOT_CA_STATIC_PATH: &str = "/var/lib/embassy/ssl/root-ca.crt";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SslManager {
|
||||
store: SslStore,
|
||||
root_cert: X509,
|
||||
int_key: PKey<Private>,
|
||||
int_cert: X509,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SslStore {
|
||||
secret_store: SqlitePool,
|
||||
}
|
||||
impl SslStore {
|
||||
fn new(db: SqlitePool) -> Result<Self, Error> {
|
||||
Ok(SslStore { secret_store: db })
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
async fn save_root_certificate(&self, key: &PKey<Private>, cert: &X509) -> Result<(), Error> {
|
||||
let key_str = String::from_utf8(key.private_key_to_pem_pkcs8()?)?;
|
||||
let cert_str = String::from_utf8(cert.to_pem()?)?;
|
||||
let _n = sqlx::query!("INSERT INTO certificates (id, priv_key_pem, certificate_pem, lookup_string, created_at, updated_at) VALUES (0, ?, ?, NULL, datetime('now'), datetime('now'))", key_str, cert_str).execute(&self.secret_store).await?;
|
||||
Ok(())
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
async fn load_root_certificate(&self) -> Result<Option<(PKey<Private>, X509)>, Error> {
|
||||
let m_row =
|
||||
sqlx::query!("SELECT priv_key_pem, certificate_pem FROM certificates WHERE id = 0;")
|
||||
.fetch_optional(&self.secret_store)
|
||||
.await?;
|
||||
match m_row {
|
||||
None => Ok(None),
|
||||
Some(row) => {
|
||||
let priv_key = PKey::private_key_from_pem(&row.priv_key_pem.into_bytes())?;
|
||||
let certificate = X509::from_pem(&row.certificate_pem.into_bytes())?;
|
||||
Ok(Some((priv_key, certificate)))
|
||||
}
|
||||
}
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
async fn save_intermediate_certificate(
|
||||
&self,
|
||||
key: &PKey<Private>,
|
||||
cert: &X509,
|
||||
) -> Result<(), Error> {
|
||||
let key_str = String::from_utf8(key.private_key_to_pem_pkcs8()?)?;
|
||||
let cert_str = String::from_utf8(cert.to_pem()?)?;
|
||||
let _n = sqlx::query!("INSERT INTO certificates (id, priv_key_pem, certificate_pem, lookup_string, created_at, updated_at) VALUES (1, ?, ?, NULL, datetime('now'), datetime('now'))", key_str, cert_str).execute(&self.secret_store).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn load_intermediate_certificate(&self) -> Result<Option<(PKey<Private>, X509)>, Error> {
|
||||
let m_row =
|
||||
sqlx::query!("SELECT priv_key_pem, certificate_pem FROM certificates WHERE id = 1;")
|
||||
.fetch_optional(&self.secret_store)
|
||||
.await?;
|
||||
match m_row {
|
||||
None => Ok(None),
|
||||
Some(row) => {
|
||||
let priv_key = PKey::private_key_from_pem(&row.priv_key_pem.into_bytes())?;
|
||||
let certificate = X509::from_pem(&row.certificate_pem.into_bytes())?;
|
||||
Ok(Some((priv_key, certificate)))
|
||||
}
|
||||
}
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
async fn import_root_certificate(
|
||||
&self,
|
||||
root_key: &PKey<Private>,
|
||||
root_cert: &X509,
|
||||
) -> Result<(), Error> {
|
||||
// remove records for both root and intermediate CA
|
||||
sqlx::query!("DELETE FROM certificates WHERE id = 0 OR id = 1;")
|
||||
.execute(&self.secret_store)
|
||||
.await?;
|
||||
self.save_root_certificate(root_key, root_cert).await?;
|
||||
Ok(())
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
async fn save_certificate(
|
||||
&self,
|
||||
key: &PKey<Private>,
|
||||
cert: &X509,
|
||||
lookup_string: &str,
|
||||
) -> Result<(), Error> {
|
||||
let key_str = String::from_utf8(key.private_key_to_pem_pkcs8()?)?;
|
||||
let cert_str = String::from_utf8(cert.to_pem()?)?;
|
||||
let _n = sqlx::query!("INSERT INTO certificates (priv_key_pem, certificate_pem, lookup_string, created_at, updated_at) VALUES (?, ?, ?, datetime('now'), datetime('now'))", key_str, cert_str, lookup_string).execute(&self.secret_store).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn load_certificate(
|
||||
&self,
|
||||
lookup_string: &str,
|
||||
) -> Result<Option<(PKey<Private>, X509)>, Error> {
|
||||
let m_row = sqlx::query!(
|
||||
"SELECT priv_key_pem, certificate_pem FROM certificates WHERE lookup_string = ?",
|
||||
lookup_string
|
||||
)
|
||||
.fetch_optional(&self.secret_store)
|
||||
.await?;
|
||||
match m_row {
|
||||
None => Ok(None),
|
||||
Some(row) => {
|
||||
let priv_key = PKey::private_key_from_pem(&row.priv_key_pem.into_bytes())?;
|
||||
let certificate = X509::from_pem(&row.certificate_pem.into_bytes())?;
|
||||
Ok(Some((priv_key, certificate)))
|
||||
}
|
||||
}
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
async fn update_certificate(
|
||||
&self,
|
||||
key: &PKey<Private>,
|
||||
cert: &X509,
|
||||
lookup_string: &str,
|
||||
) -> Result<(), Error> {
|
||||
let key_str = String::from_utf8(key.private_key_to_pem_pkcs8()?)?;
|
||||
let cert_str = String::from_utf8(cert.to_pem()?)?;
|
||||
let n = sqlx::query!("UPDATE certificates SET priv_key_pem = ?, certificate_pem = ?, updated_at = datetime('now') WHERE lookup_string = ?", key_str, cert_str, lookup_string).execute(&self.secret_store).await?;
|
||||
if n.rows_affected() == 0 {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"Attempted to update non-existent certificate: {}",
|
||||
lookup_string
|
||||
),
|
||||
ErrorKind::OpenSsl,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const EC_CURVE_NAME: nid::Nid = nid::Nid::X9_62_PRIME256V1;
|
||||
lazy_static::lazy_static! {
|
||||
static ref EC_GROUP: EcGroup = EcGroup::from_curve_name(EC_CURVE_NAME).unwrap();
|
||||
static ref SSL_MUTEX: Mutex<()> = Mutex::new(()); // TODO: make thread safe
|
||||
}
|
||||
|
||||
impl SslManager {
|
||||
#[instrument(skip(db))]
|
||||
pub async fn init(db: SqlitePool) -> Result<Self, Error> {
|
||||
let store = SslStore::new(db)?;
|
||||
let (root_key, root_cert) = match store.load_root_certificate().await? {
|
||||
None => {
|
||||
let root_key = generate_key()?;
|
||||
let root_cert = make_root_cert(&root_key)?;
|
||||
store.save_root_certificate(&root_key, &root_cert).await?;
|
||||
Ok::<_, Error>((root_key, root_cert))
|
||||
}
|
||||
Some((key, cert)) => Ok((key, cert)),
|
||||
}?;
|
||||
// generate static file for download, this will get blown up on embassy restart so it's good to write it on
|
||||
// every ssl manager init
|
||||
tokio::fs::create_dir_all(
|
||||
Path::new(ROOT_CA_STATIC_PATH)
|
||||
.parent()
|
||||
.unwrap_or(Path::new("/")),
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::write(ROOT_CA_STATIC_PATH, root_cert.to_pem()?).await?;
|
||||
let (int_key, int_cert) = match store.load_intermediate_certificate().await? {
|
||||
None => {
|
||||
let int_key = generate_key()?;
|
||||
let int_cert = make_int_cert((&root_key, &root_cert), &int_key)?;
|
||||
store
|
||||
.save_intermediate_certificate(&int_key, &int_cert)
|
||||
.await?;
|
||||
Ok::<_, Error>((int_key, int_cert))
|
||||
}
|
||||
Some((key, cert)) => Ok((key, cert)),
|
||||
}?;
|
||||
Ok(SslManager {
|
||||
store,
|
||||
root_cert,
|
||||
int_key,
|
||||
int_cert,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: currently the burden of proof is on the caller to ensure that all of the arguments to this function are
|
||||
// consistent. The following properties are assumed and not verified:
|
||||
// 1. `root_cert` is self-signed and contains the public key that matches the private key `root_key`
|
||||
// 2. certificate is not past its expiration date
|
||||
// Warning: If this function ever fails, you must either call it again or regenerate your certificates from scratch
|
||||
// since it is possible for it to fail after successfully saving the root certificate but before successfully saving
|
||||
// the intermediate certificate
|
||||
#[instrument(skip(db))]
|
||||
pub async fn import_root_ca(
|
||||
db: SqlitePool,
|
||||
root_key: PKey<Private>,
|
||||
root_cert: X509,
|
||||
) -> Result<Self, Error> {
|
||||
let store = SslStore::new(db)?;
|
||||
store.import_root_certificate(&root_key, &root_cert).await?;
|
||||
let int_key = generate_key()?;
|
||||
let int_cert = make_int_cert((&root_key, &root_cert), &int_key)?;
|
||||
store
|
||||
.save_intermediate_certificate(&int_key, &int_cert)
|
||||
.await?;
|
||||
Ok(SslManager {
|
||||
store,
|
||||
root_cert,
|
||||
int_key,
|
||||
int_cert,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn export_root_ca(&self) -> Result<(PKey<Private>, X509), Error> {
|
||||
match self.store.load_root_certificate().await? {
|
||||
None => Err(Error::new(
|
||||
eyre!("Failed to export root certificate: root certificate has not been generated"),
|
||||
ErrorKind::OpenSsl,
|
||||
)),
|
||||
Some(a) => Ok(a),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn certificate_for(
|
||||
&self,
|
||||
dns_base: &str,
|
||||
package_id: &PackageId,
|
||||
) -> Result<(PKey<Private>, Vec<X509>), Error> {
|
||||
let (key, cert) = match self.store.load_certificate(dns_base).await? {
|
||||
None => {
|
||||
let key = generate_key()?;
|
||||
let cert = make_leaf_cert(
|
||||
(&self.int_key, &self.int_cert),
|
||||
(&key, dns_base, package_id),
|
||||
)?;
|
||||
self.store.save_certificate(&key, &cert, dns_base).await?;
|
||||
Ok::<_, Error>((key, cert))
|
||||
}
|
||||
Some((key, cert)) => {
|
||||
let window_end = Asn1Time::days_from_now(30)?;
|
||||
let expiration = cert.not_after();
|
||||
if expiration.compare(&window_end)? == Ordering::Less {
|
||||
let key = generate_key()?;
|
||||
let cert = make_leaf_cert(
|
||||
(&self.int_key, &self.int_cert),
|
||||
(&key, dns_base, package_id),
|
||||
)?;
|
||||
self.store.update_certificate(&key, &cert, dns_base).await?;
|
||||
Ok((key, cert))
|
||||
} else {
|
||||
Ok((key, cert))
|
||||
}
|
||||
}
|
||||
}?;
|
||||
Ok((
|
||||
key,
|
||||
vec![cert, self.int_cert.clone(), self.root_cert.clone()],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn export_key(key: &PKey<Private>, target: &Path) -> Result<(), Error> {
|
||||
tokio::fs::write(target, key.private_key_to_pem_pkcs8()?)
|
||||
.map(|res| res.with_ctx(|_| (ErrorKind::Filesystem, target.display().to_string())))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn export_cert(chain: &Vec<X509>, target: &Path) -> Result<(), Error> {
|
||||
tokio::fs::write(
|
||||
target,
|
||||
chain
|
||||
.into_iter()
|
||||
.flat_map(|c| c.to_pem().unwrap())
|
||||
.collect::<Vec<u8>>(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
#[instrument]
|
||||
fn rand_serial() -> Result<Asn1Integer, Error> {
|
||||
let mut bn = BigNum::new()?;
|
||||
bn.rand(64, MsbOption::MAYBE_ZERO, false)?;
|
||||
let asn1 = Asn1Integer::from_bn(&bn)?;
|
||||
Ok(asn1)
|
||||
}
|
||||
#[instrument]
|
||||
fn generate_key() -> Result<PKey<Private>, Error> {
|
||||
let new_key = EcKey::generate(EC_GROUP.as_ref())?;
|
||||
let key = PKey::from_ec_key(new_key)?;
|
||||
Ok(key)
|
||||
}
|
||||
#[instrument]
|
||||
fn make_root_cert(root_key: &PKey<Private>) -> Result<X509, Error> {
|
||||
let mut builder = X509Builder::new()?;
|
||||
builder.set_version(CERTIFICATE_VERSION)?;
|
||||
|
||||
let embargo = Asn1Time::days_from_now(0)?;
|
||||
builder.set_not_before(&embargo)?;
|
||||
|
||||
let expiration = Asn1Time::days_from_now(3650)?;
|
||||
builder.set_not_after(&expiration)?;
|
||||
|
||||
builder.set_serial_number(&*rand_serial()?)?;
|
||||
|
||||
let mut subject_name_builder = X509NameBuilder::new()?;
|
||||
subject_name_builder.append_entry_by_text("CN", "Embassy Local Root CA")?;
|
||||
subject_name_builder.append_entry_by_text("O", "Start9")?;
|
||||
subject_name_builder.append_entry_by_text("OU", "Embassy")?;
|
||||
let subject_name = subject_name_builder.build();
|
||||
builder.set_subject_name(&subject_name)?;
|
||||
|
||||
builder.set_issuer_name(&subject_name)?;
|
||||
|
||||
builder.set_pubkey(&root_key)?;
|
||||
|
||||
// Extensions
|
||||
let cfg = conf::Conf::new(conf::ConfMethod::default())?;
|
||||
let ctx = builder.x509v3_context(None, Some(&cfg));
|
||||
// subjectKeyIdentifier = hash
|
||||
let subject_key_identifier =
|
||||
X509Extension::new_nid(Some(&cfg), Some(&ctx), Nid::SUBJECT_KEY_IDENTIFIER, "hash")?;
|
||||
// basicConstraints = critical, CA:true, pathlen:0
|
||||
let basic_constraints = X509Extension::new_nid(
|
||||
Some(&cfg),
|
||||
Some(&ctx),
|
||||
Nid::BASIC_CONSTRAINTS,
|
||||
"critical,CA:true",
|
||||
)?;
|
||||
// keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
||||
let key_usage = X509Extension::new_nid(
|
||||
Some(&cfg),
|
||||
Some(&ctx),
|
||||
Nid::KEY_USAGE,
|
||||
"critical,digitalSignature,cRLSign,keyCertSign",
|
||||
)?;
|
||||
builder.append_extension(subject_key_identifier)?;
|
||||
builder.append_extension(basic_constraints)?;
|
||||
builder.append_extension(key_usage)?;
|
||||
builder.sign(&root_key, MessageDigest::sha256())?;
|
||||
let cert = builder.build();
|
||||
Ok(cert)
|
||||
}
|
||||
#[instrument]
|
||||
fn make_int_cert(
|
||||
signer: (&PKey<Private>, &X509),
|
||||
applicant: &PKey<Private>,
|
||||
) -> Result<X509, Error> {
|
||||
let mut builder = X509Builder::new()?;
|
||||
builder.set_version(CERTIFICATE_VERSION)?;
|
||||
|
||||
let embargo = Asn1Time::days_from_now(0)?;
|
||||
builder.set_not_before(&embargo)?;
|
||||
|
||||
let expiration = Asn1Time::days_from_now(3650)?;
|
||||
builder.set_not_after(&expiration)?;
|
||||
|
||||
builder.set_serial_number(&*rand_serial()?)?;
|
||||
|
||||
let mut subject_name_builder = X509NameBuilder::new()?;
|
||||
subject_name_builder.append_entry_by_text("CN", "Embassy Local Intermediate CA")?;
|
||||
subject_name_builder.append_entry_by_text("O", "Start9")?;
|
||||
subject_name_builder.append_entry_by_text("OU", "Embassy")?;
|
||||
let subject_name = subject_name_builder.build();
|
||||
builder.set_subject_name(&subject_name)?;
|
||||
|
||||
builder.set_issuer_name(signer.1.subject_name())?;
|
||||
|
||||
builder.set_pubkey(&applicant)?;
|
||||
|
||||
let cfg = conf::Conf::new(conf::ConfMethod::default())?;
|
||||
let ctx = builder.x509v3_context(Some(&signer.1), Some(&cfg));
|
||||
// subjectKeyIdentifier = hash
|
||||
let subject_key_identifier =
|
||||
X509Extension::new_nid(Some(&cfg), Some(&ctx), Nid::SUBJECT_KEY_IDENTIFIER, "hash")?;
|
||||
// authorityKeyIdentifier = keyid:always,issuer
|
||||
let authority_key_identifier = X509Extension::new_nid(
|
||||
Some(&cfg),
|
||||
Some(&ctx),
|
||||
Nid::AUTHORITY_KEY_IDENTIFIER,
|
||||
"keyid:always,issuer",
|
||||
)?;
|
||||
// basicConstraints = critical, CA:true, pathlen:0
|
||||
let basic_constraints = X509Extension::new_nid(
|
||||
Some(&cfg),
|
||||
Some(&ctx),
|
||||
Nid::BASIC_CONSTRAINTS,
|
||||
"critical,CA:true,pathlen:0",
|
||||
)?;
|
||||
// keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
||||
let key_usage = X509Extension::new_nid(
|
||||
Some(&cfg),
|
||||
Some(&ctx),
|
||||
Nid::KEY_USAGE,
|
||||
"critical,digitalSignature,cRLSign,keyCertSign",
|
||||
)?;
|
||||
builder.append_extension(subject_key_identifier)?;
|
||||
builder.append_extension(authority_key_identifier)?;
|
||||
builder.append_extension(basic_constraints)?;
|
||||
builder.append_extension(key_usage)?;
|
||||
builder.sign(&signer.0, MessageDigest::sha256())?;
|
||||
let cert = builder.build();
|
||||
Ok(cert)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn make_leaf_cert(
|
||||
signer: (&PKey<Private>, &X509),
|
||||
applicant: (&PKey<Private>, &str, &PackageId),
|
||||
) -> Result<X509, Error> {
|
||||
let mut builder = X509Builder::new()?;
|
||||
builder.set_version(CERTIFICATE_VERSION)?;
|
||||
|
||||
let embargo = Asn1Time::days_from_now(0)?;
|
||||
builder.set_not_before(&embargo)?;
|
||||
|
||||
// Google Apple and Mozilla reject certificate horizons longer than 397 days
|
||||
// https://techbeacon.com/security/google-apple-mozilla-enforce-1-year-max-security-certifications
|
||||
let expiration = Asn1Time::days_from_now(397)?;
|
||||
builder.set_not_after(&expiration)?;
|
||||
|
||||
builder.set_serial_number(&*rand_serial()?)?;
|
||||
|
||||
let mut subject_name_builder = X509NameBuilder::new()?;
|
||||
subject_name_builder.append_entry_by_text("CN", &format!("{}.local", &applicant.1))?;
|
||||
subject_name_builder.append_entry_by_text("O", "Start9")?;
|
||||
subject_name_builder.append_entry_by_text("OU", "Embassy")?;
|
||||
let subject_name = subject_name_builder.build();
|
||||
builder.set_subject_name(&subject_name)?;
|
||||
|
||||
builder.set_issuer_name(signer.1.subject_name())?;
|
||||
|
||||
builder.set_pubkey(&applicant.0)?;
|
||||
|
||||
// Extensions
|
||||
let cfg = conf::Conf::new(conf::ConfMethod::default())?;
|
||||
let ctx = builder.x509v3_context(Some(&signer.1), Some(&cfg));
|
||||
// subjectKeyIdentifier = hash
|
||||
let subject_key_identifier =
|
||||
X509Extension::new_nid(Some(&cfg), Some(&ctx), Nid::SUBJECT_KEY_IDENTIFIER, "hash")?;
|
||||
// authorityKeyIdentifier = keyid:always,issuer
|
||||
let authority_key_identifier = X509Extension::new_nid(
|
||||
Some(&cfg),
|
||||
Some(&ctx),
|
||||
Nid::AUTHORITY_KEY_IDENTIFIER,
|
||||
"keyid,issuer:always",
|
||||
)?;
|
||||
let basic_constraints =
|
||||
X509Extension::new_nid(Some(&cfg), Some(&ctx), Nid::BASIC_CONSTRAINTS, "CA:FALSE")?;
|
||||
let key_usage = X509Extension::new_nid(
|
||||
Some(&cfg),
|
||||
Some(&ctx),
|
||||
Nid::KEY_USAGE,
|
||||
"critical,digitalSignature,keyEncipherment",
|
||||
)?;
|
||||
|
||||
let subject_alt_name = X509Extension::new_nid(
|
||||
Some(&cfg),
|
||||
Some(&ctx),
|
||||
Nid::SUBJECT_ALT_NAME,
|
||||
&format!(
|
||||
"DNS:{}.local,DNS:*.{}.local,DNS:{}.onion,DNS:*.{}.onion,DNS:{}.embassy,DNS:*.{}.embassy",
|
||||
&applicant.1, &applicant.1, &applicant.1, &applicant.1, &applicant.2, &applicant.2,
|
||||
),
|
||||
)?;
|
||||
builder.append_extension(subject_key_identifier)?;
|
||||
builder.append_extension(authority_key_identifier)?;
|
||||
builder.append_extension(subject_alt_name)?;
|
||||
builder.append_extension(basic_constraints)?;
|
||||
builder.append_extension(key_usage)?;
|
||||
|
||||
builder.sign(&signer.0, MessageDigest::sha256())?;
|
||||
|
||||
let cert = builder.build();
|
||||
Ok(cert)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ca_details_persist() -> Result<(), Error> {
|
||||
let pool = sqlx::Pool::<sqlx::Sqlite>::connect("sqlite::memory:").await?;
|
||||
sqlx::query_file!("migrations/20210629193146_Init.sql")
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
let mgr = SslManager::init(pool.clone()).await?;
|
||||
let root_cert0 = mgr.root_cert;
|
||||
let int_key0 = mgr.int_key;
|
||||
let int_cert0 = mgr.int_cert;
|
||||
let mgr = SslManager::init(pool).await?;
|
||||
let root_cert1 = mgr.root_cert;
|
||||
let int_key1 = mgr.int_key;
|
||||
let int_cert1 = mgr.int_cert;
|
||||
|
||||
assert_eq!(root_cert0.to_pem()?, root_cert1.to_pem()?);
|
||||
assert_eq!(
|
||||
int_key0.private_key_to_pem_pkcs8()?,
|
||||
int_key1.private_key_to_pem_pkcs8()?
|
||||
);
|
||||
assert_eq!(int_cert0.to_pem()?, int_cert1.to_pem()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn certificate_details_persist() -> Result<(), Error> {
|
||||
let pool = sqlx::Pool::<sqlx::Sqlite>::connect("sqlite::memory:").await?;
|
||||
sqlx::query_file!("migrations/20210629193146_Init.sql")
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
let mgr = SslManager::init(pool.clone()).await?;
|
||||
let package_id = "bitcoind".parse().unwrap();
|
||||
let (key0, cert_chain0) = mgr.certificate_for("start9", &package_id).await?;
|
||||
let (key1, cert_chain1) = mgr.certificate_for("start9", &package_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
key0.private_key_to_pem_pkcs8()?,
|
||||
key1.private_key_to_pem_pkcs8()?
|
||||
);
|
||||
assert_eq!(
|
||||
cert_chain0
|
||||
.iter()
|
||||
.map(|cert| cert.to_pem().unwrap())
|
||||
.collect::<Vec<Vec<u8>>>(),
|
||||
cert_chain1
|
||||
.iter()
|
||||
.map(|cert| cert.to_pem().unwrap())
|
||||
.collect::<Vec<Vec<u8>>>()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
449
backend/src/net/tor.rs
Normal file
449
backend/src/net/tor.rs
Normal file
@@ -0,0 +1,449 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt;
|
||||
use reqwest::Client;
|
||||
use rpc_toolkit::command;
|
||||
use serde_json::json;
|
||||
use sqlx::{Executor, Sqlite};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Mutex;
|
||||
use torut::control::{AsyncEvent, AuthenticatedConn, ConnError};
|
||||
use torut::onion::{OnionAddressV3, TorSecretKeyV3};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::interface::{InterfaceId, TorConfig};
|
||||
use crate::context::RpcContext;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::serde::{display_serializable, IoFormat};
|
||||
use crate::{Error, ErrorKind, ResultExt as _};
|
||||
|
||||
#[test]
|
||||
fn random_key() {
|
||||
println!("x'{}'", hex::encode(TorSecretKeyV3::generate().as_bytes()));
|
||||
}
|
||||
|
||||
#[command(subcommands(list_services))]
|
||||
pub fn tor() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_services(services: Vec<OnionAddressV3>, matches: &ArgMatches<'_>) {
|
||||
use prettytable::*;
|
||||
|
||||
if matches.is_present("format") {
|
||||
return display_serializable(services, matches);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
for service in services {
|
||||
let row = row![&service.to_string()];
|
||||
table.add_row(row);
|
||||
}
|
||||
table.print_tty(false);
|
||||
}
|
||||
|
||||
#[command(rename = "list-services", display(display_services))]
|
||||
pub async fn list_services(
|
||||
#[context] ctx: RpcContext,
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<Vec<OnionAddressV3>, Error> {
|
||||
ctx.net_controller.tor.list_services().await
|
||||
}
|
||||
|
||||
#[instrument(skip(secrets))]
|
||||
pub async fn os_key<Ex>(secrets: &mut Ex) -> Result<TorSecretKeyV3, Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
let key = sqlx::query!("SELECT tor_key FROM account")
|
||||
.fetch_one(secrets)
|
||||
.await?
|
||||
.tor_key;
|
||||
|
||||
let mut buf = [0; 64];
|
||||
buf.clone_from_slice(
|
||||
key.get(0..64).ok_or_else(|| {
|
||||
Error::new(eyre!("Invalid Tor Key Length"), crate::ErrorKind::Database)
|
||||
})?,
|
||||
);
|
||||
Ok(buf.into())
|
||||
}
|
||||
|
||||
fn event_handler(_event: AsyncEvent<'static>) -> BoxFuture<'static, Result<(), ConnError>> {
|
||||
async move { Ok(()) }.boxed()
|
||||
}
|
||||
|
||||
pub struct TorController(Mutex<TorControllerInner>);
|
||||
impl TorController {
|
||||
pub async fn init(
|
||||
embassyd_addr: SocketAddr,
|
||||
embassyd_tor_key: TorSecretKeyV3,
|
||||
tor_control: SocketAddr,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(TorController(Mutex::new(
|
||||
TorControllerInner::init(embassyd_addr, embassyd_tor_key, tor_control).await?,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn add<I: IntoIterator<Item = (InterfaceId, TorConfig, TorSecretKeyV3)> + Clone>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
ip: Ipv4Addr,
|
||||
interfaces: I,
|
||||
) -> Result<(), Error> {
|
||||
self.0.lock().await.add(pkg_id, ip, interfaces).await
|
||||
}
|
||||
|
||||
pub async fn remove<I: IntoIterator<Item = InterfaceId> + Clone>(
|
||||
&self,
|
||||
pkg_id: &PackageId,
|
||||
interfaces: I,
|
||||
) -> Result<(), Error> {
|
||||
self.0.lock().await.remove(pkg_id, interfaces).await
|
||||
}
|
||||
|
||||
pub async fn replace(&self) -> Result<bool, Error> {
|
||||
self.0.lock().await.replace().await
|
||||
}
|
||||
|
||||
pub async fn embassyd_tor_key(&self) -> TorSecretKeyV3 {
|
||||
self.0.lock().await.embassyd_tor_key.clone()
|
||||
}
|
||||
|
||||
pub async fn embassyd_onion(&self) -> OnionAddressV3 {
|
||||
self.0.lock().await.embassyd_onion()
|
||||
}
|
||||
|
||||
pub async fn list_services(&self) -> Result<Vec<OnionAddressV3>, Error> {
|
||||
self.0.lock().await.list_services().await
|
||||
}
|
||||
}
|
||||
|
||||
type AuthenticatedConnection = AuthenticatedConn<
|
||||
TcpStream,
|
||||
fn(AsyncEvent<'static>) -> BoxFuture<'static, Result<(), ConnError>>,
|
||||
>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct HiddenServiceConfig {
|
||||
ip: Ipv4Addr,
|
||||
cfg: TorConfig,
|
||||
}
|
||||
|
||||
pub struct TorControllerInner {
|
||||
embassyd_addr: SocketAddr,
|
||||
embassyd_tor_key: TorSecretKeyV3,
|
||||
control_addr: SocketAddr,
|
||||
connection: Option<AuthenticatedConnection>,
|
||||
services: BTreeMap<(PackageId, InterfaceId), (TorSecretKeyV3, TorConfig, Ipv4Addr)>,
|
||||
}
|
||||
impl TorControllerInner {
|
||||
#[instrument(skip(self, interfaces))]
|
||||
async fn add<'a, I: IntoIterator<Item = (InterfaceId, TorConfig, TorSecretKeyV3)>>(
|
||||
&mut self,
|
||||
pkg_id: &PackageId,
|
||||
ip: Ipv4Addr,
|
||||
interfaces: I,
|
||||
) -> Result<(), Error> {
|
||||
for (interface_id, tor_cfg, key) in interfaces {
|
||||
let id = (pkg_id.clone(), interface_id);
|
||||
match self.services.get(&id) {
|
||||
Some(k) if k.0 != key => {
|
||||
self.remove(pkg_id, std::iter::once(id.1.clone())).await?;
|
||||
}
|
||||
Some(_) => continue,
|
||||
None => (),
|
||||
}
|
||||
self.connection
|
||||
.as_mut()
|
||||
.ok_or_else(|| {
|
||||
Error::new(eyre!("Missing Tor Control Connection"), ErrorKind::Unknown)
|
||||
})?
|
||||
.add_onion_v3(
|
||||
&key,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
&mut tor_cfg
|
||||
.port_mapping
|
||||
.iter()
|
||||
.map(|(external, internal)| {
|
||||
(external.0, SocketAddr::from((ip, internal.0)))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.iter(),
|
||||
)
|
||||
.await?;
|
||||
self.services.insert(id, (key, tor_cfg, ip));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, interfaces))]
|
||||
async fn remove<I: IntoIterator<Item = InterfaceId>>(
|
||||
&mut self,
|
||||
pkg_id: &PackageId,
|
||||
interfaces: I,
|
||||
) -> Result<(), Error> {
|
||||
for interface_id in interfaces {
|
||||
if let Some((key, _cfg, _ip)) = self.services.remove(&(pkg_id.clone(), interface_id)) {
|
||||
self.connection
|
||||
.as_mut()
|
||||
.ok_or_else(|| {
|
||||
Error::new(eyre!("Missing Tor Control Connection"), ErrorKind::Tor)
|
||||
})?
|
||||
.del_onion(
|
||||
&key.public()
|
||||
.get_onion_address()
|
||||
.get_address_without_dot_onion(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn init(
|
||||
embassyd_addr: SocketAddr,
|
||||
embassyd_tor_key: TorSecretKeyV3,
|
||||
tor_control: SocketAddr,
|
||||
) -> Result<Self, Error> {
|
||||
let mut conn = torut::control::UnauthenticatedConn::new(
|
||||
TcpStream::connect(tor_control).await?, // TODO
|
||||
);
|
||||
let auth = conn
|
||||
.load_protocol_info()
|
||||
.await?
|
||||
.make_auth_data()?
|
||||
.ok_or_else(|| eyre!("Cookie Auth Not Available"))
|
||||
.with_kind(crate::ErrorKind::Tor)?;
|
||||
conn.authenticate(&auth).await?;
|
||||
let mut connection: AuthenticatedConnection = conn.into_authenticated().await;
|
||||
connection.set_async_event_handler(Some(event_handler));
|
||||
|
||||
let mut controller = TorControllerInner {
|
||||
embassyd_addr,
|
||||
embassyd_tor_key,
|
||||
control_addr: tor_control,
|
||||
connection: Some(connection),
|
||||
services: BTreeMap::new(),
|
||||
};
|
||||
controller.add_embassyd_onion().await?;
|
||||
Ok(controller)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn add_embassyd_onion(&mut self) -> Result<(), Error> {
|
||||
tracing::info!(
|
||||
"Registering Main Tor Service: {}",
|
||||
self.embassyd_tor_key.public().get_onion_address()
|
||||
);
|
||||
self.connection
|
||||
.as_mut()
|
||||
.ok_or_else(|| Error::new(eyre!("Missing Tor Control Connection"), ErrorKind::Tor))?
|
||||
.add_onion_v3(
|
||||
&self.embassyd_tor_key,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
&mut std::iter::once(&(self.embassyd_addr.port(), self.embassyd_addr)),
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
"Registered Main Tor Service: {}",
|
||||
self.embassyd_tor_key.public().get_onion_address()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn replace(&mut self) -> Result<bool, Error> {
|
||||
let connection = self.connection.take();
|
||||
let uptime = if let Some(mut c) = connection {
|
||||
// this should be unreachable because the only time when this should be none is for the duration of tor's
|
||||
// restart lower down in this method, which is held behind a Mutex
|
||||
let uptime = c.get_info("uptime").await?.parse::<u64>()?;
|
||||
// we never want to restart the tor daemon if it hasn't been up for at least a half hour
|
||||
if uptime < 1800 {
|
||||
self.connection = Some(c); // put it back
|
||||
return Ok(false);
|
||||
}
|
||||
// when connection closes below, tor daemon is restarted
|
||||
c.take_ownership().await?;
|
||||
// this should close the connection
|
||||
drop(c);
|
||||
Some(uptime)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// attempt to reconnect to the control socket, not clear how long this should take
|
||||
let mut new_connection: AuthenticatedConnection;
|
||||
loop {
|
||||
match TcpStream::connect(self.control_addr).await {
|
||||
Ok(stream) => {
|
||||
let mut new_conn = torut::control::UnauthenticatedConn::new(stream);
|
||||
let auth = new_conn
|
||||
.load_protocol_info()
|
||||
.await?
|
||||
.make_auth_data()?
|
||||
.ok_or_else(|| eyre!("Cookie Auth Not Available"))
|
||||
.with_kind(crate::ErrorKind::Tor)?;
|
||||
new_conn.authenticate(&auth).await?;
|
||||
new_connection = new_conn.into_authenticated().await;
|
||||
let uptime_new = new_connection.get_info("uptime").await?.parse::<u64>()?;
|
||||
// if the new uptime exceeds the one we got at the beginning, it's the same tor daemon, do not proceed
|
||||
match uptime {
|
||||
Some(uptime) if uptime_new > uptime => (),
|
||||
_ => {
|
||||
new_connection.set_async_event_handler(Some(event_handler));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::info!("Failed to reconnect to tor control socket: {}", e);
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
// replace the connection object here on the new copy of the tor daemon
|
||||
self.connection.replace(new_connection);
|
||||
|
||||
// swap empty map for owned old service map
|
||||
let old_services = std::mem::replace(&mut self.services, BTreeMap::new());
|
||||
|
||||
// re add all of the services on the new control socket
|
||||
for ((package_id, interface_id), (tor_key, tor_cfg, ipv4)) in old_services {
|
||||
self.add(
|
||||
&package_id,
|
||||
ipv4,
|
||||
std::iter::once((interface_id, tor_cfg, tor_key)),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// add embassyd hidden service again
|
||||
self.add_embassyd_onion().await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn embassyd_onion(&self) -> OnionAddressV3 {
|
||||
self.embassyd_tor_key.public().get_onion_address()
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn list_services(&mut self) -> Result<Vec<OnionAddressV3>, Error> {
|
||||
self.connection
|
||||
.as_mut()
|
||||
.ok_or_else(|| Error::new(eyre!("Missing Tor Control Connection"), ErrorKind::Tor))?
|
||||
.get_info("onions/current")
|
||||
.await?
|
||||
.lines()
|
||||
.map(|l| l.trim().parse().with_kind(ErrorKind::Tor))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn tor_health_check(client: &Client, tor_controller: &TorController) {
|
||||
tracing::debug!("Attempting to self-check tor address");
|
||||
let onion = tor_controller.embassyd_onion().await;
|
||||
let result = client
|
||||
.post(format!("http://{}/rpc/v1", onion))
|
||||
.body(
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "echo",
|
||||
"params": { "message": "Follow the orange rabbit" },
|
||||
})
|
||||
.to_string()
|
||||
.into_bytes(),
|
||||
)
|
||||
.send()
|
||||
.await;
|
||||
match result {
|
||||
// if success, do nothing
|
||||
Ok(_) => {
|
||||
tracing::debug!(
|
||||
"Successfully verified main tor address liveness at {}",
|
||||
onion
|
||||
)
|
||||
}
|
||||
// if failure, disconnect tor control port, and restart tor controller
|
||||
Err(e) => {
|
||||
tracing::error!("Unable to reach self over tor: {}", e);
|
||||
loop {
|
||||
match tor_controller.replace().await {
|
||||
Ok(restarted) => {
|
||||
if restarted {
|
||||
tracing::error!("Tor has been recently restarted, refusing to restart");
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Unable to restart tor: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test() {
|
||||
let mut conn = torut::control::UnauthenticatedConn::new(
|
||||
TcpStream::connect(SocketAddr::from(([127, 0, 0, 1], 9051)))
|
||||
.await
|
||||
.unwrap(), // TODO
|
||||
);
|
||||
let auth = conn
|
||||
.load_protocol_info()
|
||||
.await
|
||||
.unwrap()
|
||||
.make_auth_data()
|
||||
.unwrap()
|
||||
.ok_or_else(|| eyre!("Cookie Auth Not Available"))
|
||||
.with_kind(crate::ErrorKind::Tor)
|
||||
.unwrap();
|
||||
conn.authenticate(&auth).await.unwrap();
|
||||
let mut connection: AuthenticatedConn<
|
||||
TcpStream,
|
||||
fn(AsyncEvent<'static>) -> BoxFuture<'static, Result<(), ConnError>>,
|
||||
> = conn.into_authenticated().await;
|
||||
let tor_key = torut::onion::TorSecretKeyV3::generate();
|
||||
dbg!(connection.get_conf("SocksPort").await.unwrap());
|
||||
connection
|
||||
.add_onion_v3(
|
||||
&tor_key,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
&mut [(443_u16, SocketAddr::from(([127, 0, 0, 1], 8443)))].iter(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
connection
|
||||
.add_onion_v3(
|
||||
&tor_key,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
&mut [(8443_u16, SocketAddr::from(([127, 0, 0, 1], 8443)))].iter(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
757
backend/src/net/wifi.rs
Normal file
757
backend/src/net/wifi.rs
Normal file
@@ -0,0 +1,757 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use isocountry::CountryCode;
|
||||
use lazy_static::lazy_static;
|
||||
use patch_db::DbHandle;
|
||||
use regex::Regex;
|
||||
use rpc_toolkit::command;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::util::serde::{display_serializable, IoFormat};
|
||||
use crate::util::{display_none, Invoke};
|
||||
use crate::{Error, ErrorKind};
|
||||
|
||||
#[command(subcommands(add, connect, delete, get, country, available))]
|
||||
pub async fn wifi() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(subcommands(get_available))]
|
||||
pub async fn available() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(subcommands(set_country))]
|
||||
pub async fn country() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
#[instrument(skip(ctx, password))]
|
||||
pub async fn add(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] ssid: String,
|
||||
#[arg] password: String,
|
||||
#[arg] priority: isize,
|
||||
#[arg] connect: bool,
|
||||
) -> Result<(), Error> {
|
||||
if !ssid.is_ascii() {
|
||||
return Err(Error::new(
|
||||
color_eyre::eyre::eyre!("SSID may not have special characters"),
|
||||
ErrorKind::Wifi,
|
||||
));
|
||||
}
|
||||
if !password.is_ascii() {
|
||||
return Err(Error::new(
|
||||
color_eyre::eyre::eyre!("WiFi Password may not have special characters"),
|
||||
ErrorKind::Wifi,
|
||||
));
|
||||
}
|
||||
async fn add_procedure(
|
||||
db: impl DbHandle,
|
||||
wifi_manager: Arc<RwLock<WpaCli>>,
|
||||
ssid: &Ssid,
|
||||
password: &Psk,
|
||||
priority: isize,
|
||||
) -> Result<(), Error> {
|
||||
tracing::info!("Adding new WiFi network: '{}'", ssid.0);
|
||||
let mut wpa_supplicant = wifi_manager.write().await;
|
||||
wpa_supplicant
|
||||
.add_network(db, ssid, password, priority)
|
||||
.await?;
|
||||
drop(wpa_supplicant);
|
||||
Ok(())
|
||||
}
|
||||
if let Err(err) = add_procedure(
|
||||
&mut ctx.db.handle(),
|
||||
ctx.wifi_manager.clone(),
|
||||
&Ssid(ssid.clone()),
|
||||
&Psk(password.clone()),
|
||||
priority,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to add new WiFi network '{}': {}", ssid, err);
|
||||
return Err(Error::new(
|
||||
color_eyre::eyre::eyre!("Failed adding {}", ssid),
|
||||
ErrorKind::Wifi,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn connect(#[context] ctx: RpcContext, #[arg] ssid: String) -> Result<(), Error> {
|
||||
if !ssid.is_ascii() {
|
||||
return Err(Error::new(
|
||||
color_eyre::eyre::eyre!("SSID may not have special characters"),
|
||||
ErrorKind::Wifi,
|
||||
));
|
||||
}
|
||||
async fn connect_procedure(
|
||||
mut db: impl DbHandle,
|
||||
wifi_manager: Arc<RwLock<WpaCli>>,
|
||||
ssid: &Ssid,
|
||||
) -> Result<(), Error> {
|
||||
let wpa_supplicant = wifi_manager.read().await;
|
||||
let current = wpa_supplicant.get_current_network().await?;
|
||||
drop(wpa_supplicant);
|
||||
let mut wpa_supplicant = wifi_manager.write().await;
|
||||
let connected = wpa_supplicant.select_network(&mut db, &ssid).await?;
|
||||
if connected {
|
||||
tracing::info!("Successfully connected to WiFi: '{}'", ssid.0);
|
||||
} else {
|
||||
tracing::info!("Failed to connect to WiFi: '{}'", ssid.0);
|
||||
match current {
|
||||
None => {
|
||||
tracing::info!("No WiFi to revert to!");
|
||||
}
|
||||
Some(current) => {
|
||||
wpa_supplicant.select_network(&mut db, ¤t).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if let Err(err) = connect_procedure(
|
||||
&mut ctx.db.handle(),
|
||||
ctx.wifi_manager.clone(),
|
||||
&Ssid(ssid.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to connect to WiFi network '{}': {}", &ssid, err);
|
||||
return Err(Error::new(
|
||||
color_eyre::eyre::eyre!("Can't connect to {}", ssid),
|
||||
ErrorKind::Wifi,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn delete(#[context] ctx: RpcContext, #[arg] ssid: String) -> Result<(), Error> {
|
||||
if !ssid.is_ascii() {
|
||||
return Err(Error::new(
|
||||
color_eyre::eyre::eyre!("SSID may not have special characters"),
|
||||
ErrorKind::Wifi,
|
||||
));
|
||||
}
|
||||
let wpa_supplicant = ctx.wifi_manager.read().await;
|
||||
let current = wpa_supplicant.get_current_network().await?;
|
||||
drop(wpa_supplicant);
|
||||
let mut wpa_supplicant = ctx.wifi_manager.write().await;
|
||||
let ssid = Ssid(ssid);
|
||||
let is_current_being_removed = matches!(current, Some(current) if current == ssid);
|
||||
let is_current_removed_and_no_hardwire =
|
||||
is_current_being_removed && !interface_connected("eth0").await?;
|
||||
if is_current_removed_and_no_hardwire {
|
||||
return Err(Error::new(color_eyre::eyre::eyre!("Forbidden: Deleting this Network would make your Embassy Unreachable. Either connect to ethernet or connect to a different WiFi network to remedy this."), ErrorKind::Wifi));
|
||||
}
|
||||
|
||||
wpa_supplicant
|
||||
.remove_network(&mut ctx.db.handle(), &ssid)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WiFiInfo {
|
||||
ssids: HashMap<Ssid, SignalStrength>,
|
||||
connected: Option<Ssid>,
|
||||
country: CountryCode,
|
||||
ethernet: bool,
|
||||
available_wifi: Vec<WifiListOut>,
|
||||
}
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WifiListInfo {
|
||||
strength: SignalStrength,
|
||||
security: Vec<String>,
|
||||
}
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct WifiListOut {
|
||||
ssid: Ssid,
|
||||
strength: SignalStrength,
|
||||
security: Vec<String>,
|
||||
}
|
||||
pub type WifiList = HashMap<Ssid, WifiListInfo>;
|
||||
fn display_wifi_info(info: WiFiInfo, matches: &ArgMatches<'_>) {
|
||||
use prettytable::*;
|
||||
|
||||
if matches.is_present("format") {
|
||||
return display_serializable(info, matches);
|
||||
}
|
||||
|
||||
let mut table_global = Table::new();
|
||||
table_global.add_row(row![bc =>
|
||||
"CONNECTED",
|
||||
"SIGNAL_STRENGTH",
|
||||
"COUNTRY",
|
||||
"ETHERNET",
|
||||
]);
|
||||
table_global.add_row(row![
|
||||
&info
|
||||
.connected
|
||||
.as_ref()
|
||||
.map_or("[N/A]".to_owned(), |c| format!("{}", c.0)),
|
||||
&info
|
||||
.connected
|
||||
.as_ref()
|
||||
.and_then(|x| info.ssids.get(x))
|
||||
.map_or("[N/A]".to_owned(), |ss| format!("{}", ss.0)),
|
||||
&format!("{}", info.country.alpha2()),
|
||||
&format!("{}", info.ethernet)
|
||||
]);
|
||||
table_global.print_tty(false);
|
||||
|
||||
let mut table_ssids = Table::new();
|
||||
table_ssids.add_row(row![bc => "SSID", "STRENGTH"]);
|
||||
for (ssid, signal_strength) in &info.ssids {
|
||||
let mut row = row![&ssid.0, format!("{}", signal_strength.0)];
|
||||
row.iter_mut()
|
||||
.map(|c| {
|
||||
c.style(Attr::ForegroundColor(match &signal_strength.0 {
|
||||
x if x >= &90 => color::GREEN,
|
||||
x if x == &50 => color::MAGENTA,
|
||||
x if x == &0 => color::RED,
|
||||
_ => color::YELLOW,
|
||||
}))
|
||||
})
|
||||
.for_each(drop);
|
||||
table_ssids.add_row(row);
|
||||
}
|
||||
table_ssids.print_tty(false);
|
||||
|
||||
let mut table_global = Table::new();
|
||||
table_global.add_row(row![bc =>
|
||||
"SSID",
|
||||
"STRENGTH",
|
||||
"SECURITY",
|
||||
]);
|
||||
for table_info in info.available_wifi {
|
||||
table_global.add_row(row![
|
||||
&table_info.ssid.0,
|
||||
&format!("{}", table_info.strength.0),
|
||||
&format!("{}", table_info.security.join(" "))
|
||||
]);
|
||||
}
|
||||
|
||||
table_global.print_tty(false);
|
||||
}
|
||||
|
||||
fn display_wifi_list(info: Vec<WifiListOut>, matches: &ArgMatches<'_>) {
|
||||
use prettytable::*;
|
||||
|
||||
if matches.is_present("format") {
|
||||
return display_serializable(info, matches);
|
||||
}
|
||||
|
||||
let mut table_global = Table::new();
|
||||
table_global.add_row(row![bc =>
|
||||
"SSID",
|
||||
"STRENGTH",
|
||||
"SECURITY",
|
||||
]);
|
||||
for table_info in info {
|
||||
table_global.add_row(row![
|
||||
&table_info.ssid.0,
|
||||
&format!("{}", table_info.strength.0),
|
||||
&format!("{}", table_info.security.join(" "))
|
||||
]);
|
||||
}
|
||||
|
||||
table_global.print_tty(false);
|
||||
}
|
||||
|
||||
#[command(display(display_wifi_info))]
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn get(
|
||||
#[context] ctx: RpcContext,
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<WiFiInfo, Error> {
|
||||
let wpa_supplicant = ctx.wifi_manager.read().await;
|
||||
let (list_networks, current_res, country_res, ethernet_res, signal_strengths) = tokio::join!(
|
||||
wpa_supplicant.list_networks_low(),
|
||||
wpa_supplicant.get_current_network(),
|
||||
wpa_supplicant.get_country_low(),
|
||||
interface_connected("eth0"), // TODO: pull from config
|
||||
wpa_supplicant.list_wifi_low()
|
||||
);
|
||||
let signal_strengths = signal_strengths?;
|
||||
let list_networks = list_networks?;
|
||||
let available_wifi = {
|
||||
let mut wifi_list: Vec<WifiListOut> = signal_strengths
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|(ssid, _)| !list_networks.contains_key(ssid))
|
||||
.map(|(ssid, info)| WifiListOut {
|
||||
ssid,
|
||||
strength: info.strength,
|
||||
security: info.security,
|
||||
})
|
||||
.collect();
|
||||
wifi_list.sort_by_key(|x| x.strength);
|
||||
wifi_list.reverse();
|
||||
wifi_list
|
||||
};
|
||||
let ssids: HashMap<Ssid, SignalStrength> = list_networks
|
||||
.into_keys()
|
||||
.map(|x| {
|
||||
let signal_strength = signal_strengths
|
||||
.get(&x)
|
||||
.map(|x| x.strength)
|
||||
.unwrap_or_default();
|
||||
(x, signal_strength)
|
||||
})
|
||||
.collect();
|
||||
let current = current_res?;
|
||||
Ok(WiFiInfo {
|
||||
ssids,
|
||||
connected: current.map(|x| x),
|
||||
country: country_res?,
|
||||
ethernet: ethernet_res?,
|
||||
available_wifi,
|
||||
})
|
||||
}
|
||||
|
||||
#[command(rename = "get", display(display_wifi_list))]
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn get_available(
|
||||
#[context] ctx: RpcContext,
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<Vec<WifiListOut>, Error> {
|
||||
let wpa_supplicant = ctx.wifi_manager.read().await;
|
||||
let (wifi_list, network_list) = tokio::join!(
|
||||
wpa_supplicant.list_wifi_low(),
|
||||
wpa_supplicant.list_networks_low()
|
||||
);
|
||||
let network_list = network_list?;
|
||||
let mut wifi_list: Vec<WifiListOut> = wifi_list?
|
||||
.into_iter()
|
||||
.filter(|(ssid, _)| !network_list.contains_key(ssid))
|
||||
.map(|(ssid, info)| WifiListOut {
|
||||
ssid,
|
||||
strength: info.strength,
|
||||
security: info.security,
|
||||
})
|
||||
.collect();
|
||||
wifi_list.sort_by_key(|x| x.strength);
|
||||
wifi_list.reverse();
|
||||
Ok(wifi_list)
|
||||
}
|
||||
|
||||
#[command(rename = "set", display(display_none))]
|
||||
pub async fn set_country(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg(parse(country_code_parse))] country: CountryCode,
|
||||
) -> Result<(), Error> {
|
||||
if !interface_connected("eth0").await? {
|
||||
return Err(Error::new(
|
||||
color_eyre::eyre::eyre!("Won't change country without hardwire connection"),
|
||||
crate::ErrorKind::Wifi,
|
||||
));
|
||||
}
|
||||
let mut wpa_supplicant = ctx.wifi_manager.write().await;
|
||||
wpa_supplicant.set_country_low(country.alpha2()).await?;
|
||||
for (_ssid, network_id) in wpa_supplicant.list_networks_low().await? {
|
||||
wpa_supplicant.remove_network_low(network_id).await?;
|
||||
}
|
||||
wpa_supplicant.remove_all_connections().await?;
|
||||
|
||||
wpa_supplicant.save_config(&mut ctx.db.handle()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WpaCli {
|
||||
interface: String,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct NetworkId(String);
|
||||
|
||||
/// Ssid are the names of the wifis, usually human readable.
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub struct Ssid(String);
|
||||
|
||||
/// So a signal strength is a number between 0-100, I want the null option to be 0 since there is no signal
|
||||
#[derive(
|
||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub struct SignalStrength(u8);
|
||||
|
||||
impl SignalStrength {
|
||||
fn new(size: Option<u8>) -> Self {
|
||||
let size = match size {
|
||||
None => return Self(0),
|
||||
Some(x) => x,
|
||||
};
|
||||
if size >= 100 {
|
||||
return Self(100);
|
||||
}
|
||||
Self(size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SignalStrength {
|
||||
fn default() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Psk(String);
|
||||
impl WpaCli {
|
||||
pub fn init(interface: String) -> Self {
|
||||
WpaCli { interface }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, psk))]
|
||||
pub async fn set_add_network_low(&mut self, ssid: &Ssid, psk: &Psk) -> Result<(), Error> {
|
||||
let _ = Command::new("nmcli")
|
||||
.arg("-a")
|
||||
.arg("-w")
|
||||
.arg("30")
|
||||
.arg("d")
|
||||
.arg("wifi")
|
||||
.arg("con")
|
||||
.arg(&ssid.0)
|
||||
.arg("password")
|
||||
.arg(&psk.0)
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
#[instrument(skip(self, psk))]
|
||||
pub async fn add_network_low(&mut self, ssid: &Ssid, psk: &Psk) -> Result<(), Error> {
|
||||
let _ = Command::new("nmcli")
|
||||
.arg("con")
|
||||
.arg("add")
|
||||
.arg("con-name")
|
||||
.arg(&ssid.0)
|
||||
.arg("ifname")
|
||||
.arg(&self.interface)
|
||||
.arg("type")
|
||||
.arg("wifi")
|
||||
.arg("ssid")
|
||||
.arg(&ssid.0)
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
let _ = Command::new("nmcli")
|
||||
.arg("con")
|
||||
.arg("modify")
|
||||
.arg(&ssid.0)
|
||||
.arg("wifi-sec.key-mgmt")
|
||||
.arg("wpa-psk")
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
let _ = Command::new("nmcli")
|
||||
.arg("con")
|
||||
.arg("modify")
|
||||
.arg(&ssid.0)
|
||||
.arg("wifi-sec.psk")
|
||||
.arg(&psk.0)
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn set_country_low(&mut self, country_code: &str) -> Result<(), Error> {
|
||||
let _ = Command::new("iw")
|
||||
.arg("reg")
|
||||
.arg("set")
|
||||
.arg(country_code)
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub async fn get_country_low(&self) -> Result<CountryCode, Error> {
|
||||
let r = Command::new("iw")
|
||||
.arg("reg")
|
||||
.arg("get")
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
let r = String::from_utf8(r)?;
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new("country (\\w+):").unwrap();
|
||||
}
|
||||
let first_country = r
|
||||
.lines()
|
||||
.filter(|s| s.contains("country"))
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
color_eyre::eyre::eyre!("Could not find a country config lines"),
|
||||
ErrorKind::Wifi,
|
||||
)
|
||||
})?;
|
||||
let country = &RE.captures(first_country).ok_or_else(|| {
|
||||
Error::new(
|
||||
color_eyre::eyre::eyre!("Could not find a country config with regex"),
|
||||
ErrorKind::Wifi,
|
||||
)
|
||||
})?[1];
|
||||
Ok(CountryCode::for_alpha2(country).or(Err(Error::new(
|
||||
color_eyre::eyre::eyre!("Invalid Country Code: {}", country),
|
||||
ErrorKind::Wifi,
|
||||
)))?)
|
||||
}
|
||||
pub async fn remove_network_low(&mut self, id: NetworkId) -> Result<(), Error> {
|
||||
let _ = Command::new("nmcli")
|
||||
.arg("c")
|
||||
.arg("del")
|
||||
.arg(&id.0)
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
#[instrument]
|
||||
pub async fn list_networks_low(&self) -> Result<BTreeMap<Ssid, NetworkId>, Error> {
|
||||
let r = Command::new("nmcli")
|
||||
.arg("-t")
|
||||
.arg("c")
|
||||
.arg("show")
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
Ok(String::from_utf8(r)?
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let mut cs = l.split(":");
|
||||
let name = Ssid(cs.next()?.to_owned());
|
||||
let uuid = NetworkId(cs.next()?.to_owned());
|
||||
let _connection_type = cs.next()?;
|
||||
let _device = cs.next()?;
|
||||
Some((name, uuid))
|
||||
})
|
||||
.collect::<BTreeMap<Ssid, NetworkId>>())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn list_wifi_low(&self) -> Result<WifiList, Error> {
|
||||
let r = Command::new("nmcli")
|
||||
.arg("-g")
|
||||
.arg("SSID,SIGNAL,security")
|
||||
.arg("d")
|
||||
.arg("wifi")
|
||||
.arg("list")
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
Ok(String::from_utf8(r)?
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let mut values = l.split(":");
|
||||
let ssid = Ssid(values.next()?.to_owned());
|
||||
let signal = SignalStrength::new(std::str::FromStr::from_str(values.next()?).ok());
|
||||
let security: Vec<String> =
|
||||
values.next()?.split(" ").map(|x| x.to_owned()).collect();
|
||||
Some((
|
||||
ssid,
|
||||
WifiListInfo {
|
||||
strength: signal,
|
||||
security,
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect::<WifiList>())
|
||||
}
|
||||
pub async fn select_network_low(&mut self, id: &NetworkId) -> Result<(), Error> {
|
||||
let _ = Command::new("nmcli")
|
||||
.arg("c")
|
||||
.arg("up")
|
||||
.arg(&id.0)
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn remove_all_connections(&mut self) -> Result<(), Error> {
|
||||
let location_connections = Path::new("/etc/NetworkManager/system-connections");
|
||||
let mut connections = tokio::fs::read_dir(&location_connections).await?;
|
||||
while let Some(connection) = connections.next_entry().await? {
|
||||
let path = connection.path();
|
||||
if path.is_file() {
|
||||
let _ = tokio::fs::remove_file(&path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub async fn save_config(&mut self, mut db: impl DbHandle) -> Result<(), Error> {
|
||||
crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.last_wifi_region()
|
||||
.put(&mut db, &Some(self.get_country_low().await?))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn check_network(&self, ssid: &Ssid) -> Result<Option<NetworkId>, Error> {
|
||||
Ok(self.list_networks_low().await?.remove(ssid))
|
||||
}
|
||||
#[instrument(skip(db))]
|
||||
pub async fn select_network(&mut self, db: impl DbHandle, ssid: &Ssid) -> Result<bool, Error> {
|
||||
let m_id = self.check_network(ssid).await?;
|
||||
match m_id {
|
||||
None => Err(Error::new(
|
||||
color_eyre::eyre::eyre!("SSID Not Found"),
|
||||
ErrorKind::Wifi,
|
||||
)),
|
||||
Some(x) => {
|
||||
self.select_network_low(&x).await?;
|
||||
self.save_config(db).await?;
|
||||
let connect = async {
|
||||
let mut current;
|
||||
loop {
|
||||
current = self.get_current_network().await;
|
||||
match ¤t {
|
||||
Ok(Some(ssid)) => {
|
||||
tracing::debug!("Connected to: {}", ssid.0);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
tracing::debug!("Retrying...");
|
||||
}
|
||||
current
|
||||
};
|
||||
let res = match tokio::time::timeout(Duration::from_secs(20), connect).await {
|
||||
Err(_) => None,
|
||||
Ok(net) => net?,
|
||||
};
|
||||
tracing::debug!("{:?}", res);
|
||||
Ok(match res {
|
||||
None => false,
|
||||
Some(net) => &net == ssid,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
#[instrument]
|
||||
pub async fn get_current_network(&self) -> Result<Option<Ssid>, Error> {
|
||||
let r = Command::new("iwgetid")
|
||||
.arg(&self.interface)
|
||||
.arg("--raw")
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
let output = String::from_utf8(r)?;
|
||||
let network = output.trim();
|
||||
tracing::debug!("Current Network: \"{}\"", network);
|
||||
if network.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(Ssid(network.to_owned())))
|
||||
}
|
||||
}
|
||||
#[instrument(skip(db))]
|
||||
pub async fn remove_network(&mut self, db: impl DbHandle, ssid: &Ssid) -> Result<bool, Error> {
|
||||
match self.check_network(ssid).await? {
|
||||
None => Ok(false),
|
||||
Some(x) => {
|
||||
self.remove_network_low(x).await?;
|
||||
self.save_config(db).await?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
#[instrument(skip(psk, db))]
|
||||
pub async fn set_add_network(
|
||||
&mut self,
|
||||
db: impl DbHandle,
|
||||
ssid: &Ssid,
|
||||
psk: &Psk,
|
||||
priority: isize,
|
||||
) -> Result<(), Error> {
|
||||
self.set_add_network_low(&ssid, &psk).await?;
|
||||
self.save_config(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
#[instrument(skip(psk, db))]
|
||||
pub async fn add_network(
|
||||
&mut self,
|
||||
db: impl DbHandle,
|
||||
ssid: &Ssid,
|
||||
psk: &Psk,
|
||||
priority: isize,
|
||||
) -> Result<(), Error> {
|
||||
self.add_network_low(&ssid, &psk).await?;
|
||||
self.save_config(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn interface_connected(interface: &str) -> Result<bool, Error> {
|
||||
let out = Command::new("ifconfig")
|
||||
.arg(interface)
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
let v = std::str::from_utf8(&out)?
|
||||
.lines()
|
||||
.filter(|s| s.contains("inet"))
|
||||
.next();
|
||||
Ok(!v.is_none())
|
||||
}
|
||||
|
||||
pub fn country_code_parse(code: &str, _matches: &ArgMatches<'_>) -> Result<CountryCode, Error> {
|
||||
CountryCode::for_alpha2(code).or(Err(Error::new(
|
||||
color_eyre::eyre::eyre!("Invalid Country Code: {}", code),
|
||||
ErrorKind::Wifi,
|
||||
)))
|
||||
}
|
||||
|
||||
#[instrument(skip(main_datadir))]
|
||||
pub async fn synchronize_wpa_supplicant_conf<P: AsRef<Path>>(
|
||||
main_datadir: P,
|
||||
last_country_code: &Option<CountryCode>,
|
||||
) -> Result<(), Error> {
|
||||
let persistent = main_datadir.as_ref().join("system-connections");
|
||||
tracing::debug!("persistent: {:?}", persistent);
|
||||
let supplicant = Path::new("/etc/wpa_supplicant.conf");
|
||||
|
||||
if tokio::fs::metadata(&persistent).await.is_err() {
|
||||
tokio::fs::create_dir_all(&persistent).await?;
|
||||
}
|
||||
crate::disk::mount::util::bind(&persistent, "/etc/NetworkManager/system-connections", false)
|
||||
.await?;
|
||||
if tokio::fs::metadata(&supplicant).await.is_err() {
|
||||
tokio::fs::write(&supplicant, include_str!("wpa_supplicant.conf.base")).await?;
|
||||
}
|
||||
|
||||
Command::new("systemctl")
|
||||
.arg("restart")
|
||||
.arg("NetworkManager")
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
Command::new("ifconfig")
|
||||
.arg("wlan0")
|
||||
.arg("up")
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
Command::new("dhclient").invoke(ErrorKind::Wifi).await?;
|
||||
if let Some(last_country_code) = last_country_code {
|
||||
tracing::info!("Setting the region");
|
||||
let _ = Command::new("iw")
|
||||
.arg("reg")
|
||||
.arg("set")
|
||||
.arg(last_country_code.alpha2())
|
||||
.invoke(ErrorKind::Wifi)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
3
backend/src/net/wpa_supplicant.conf.base
Normal file
3
backend/src/net/wpa_supplicant.conf.base
Normal file
@@ -0,0 +1,3 @@
|
||||
ctrl_interface=DIR=/run/wpa_supplicant GROUP=netdev
|
||||
update_config=1
|
||||
country=US
|
||||
Reference in New Issue
Block a user