From 68fbc34bceb5c839fb25afe174ae5b8717b3fc7f Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Mon, 20 Sep 2021 11:13:04 -0600 Subject: [PATCH] Feature/nginx management (#483) * Implements nginx controller and initialization * adds nginx controller add to netmod add * adds nginx remove to netmod remove * fix code review issues --- appmgr/src/context/rpc.rs | 1 + appmgr/src/net/interface.rs | 4 +- appmgr/src/net/mod.rs | 75 +++++++++--- appmgr/src/net/nginx.conf.template | 4 +- appmgr/src/net/nginx.rs | 187 +++++++++++++++++++++++++++++ 5 files changed, 248 insertions(+), 23 deletions(-) create mode 100644 appmgr/src/net/nginx.rs diff --git a/appmgr/src/context/rpc.rs b/appmgr/src/context/rpc.rs index 97256f681..ed6f38fea 100644 --- a/appmgr/src/context/rpc.rs +++ b/appmgr/src/context/rpc.rs @@ -150,6 +150,7 @@ impl RpcContext { crate::net::tor::os_key(&mut secret_store.acquire().await?).await?, base.tor_control .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))), + secret_store.clone(), ) .await?; let managers = ManagerMap::default(); diff --git a/appmgr/src/net/interface.rs b/appmgr/src/net/interface.rs index 5c2d8f3dc..970cb11cc 100644 --- a/appmgr/src/net/interface.rs +++ b/appmgr/src/net/interface.rs @@ -3,7 +3,7 @@ use std::path::Path; use anyhow::anyhow; use futures::TryStreamExt; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use itertools::Either; use serde::{Deserialize, Deserializer, Serialize}; use sqlx::{Executor, Sqlite}; @@ -150,7 +150,7 @@ pub struct Interface { pub tor_config: Option, pub lan_config: Option>, pub ui: bool, - pub protocols: Vec, + pub protocols: IndexSet, } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] diff --git a/appmgr/src/net/mod.rs b/appmgr/src/net/mod.rs index a059f72fe..b4b8ddbbb 100644 --- a/appmgr/src/net/mod.rs +++ b/appmgr/src/net/mod.rs @@ -1,19 +1,24 @@ use std::net::{Ipv4Addr, SocketAddr}; +use std::path::PathBuf; use rpc_toolkit::command; -use torut::onion::TorSecretKeyV3; +use sqlx::SqlitePool; +use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use self::interface::{Interface, InterfaceId}; #[cfg(feature = "avahi")] use self::mdns::MdnsController; +use self::nginx::NginxController; 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; @@ -27,18 +32,20 @@ pub struct NetController { pub tor: TorController, #[cfg(feature = "avahi")] pub mdns: MdnsController, - // nginx: NginxController, // TODO + nginx: NginxController, } impl NetController { pub async fn init( embassyd_addr: SocketAddr, embassyd_tor_key: TorSecretKeyV3, tor_control: SocketAddr, + db: SqlitePool, ) -> Result { 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"), db).await?, }) } @@ -59,19 +66,42 @@ impl NetController { Some(cfg) => Some((i.0, cfg, i.2)), }) .collect::>(); - let (tor_res, _) = tokio::join!(self.tor.add(pkg_id, ip, interfaces_tor), { - #[cfg(feature = "avahi")] - let mdns_fut = self.mdns.add( - pkg_id, - interfaces + 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() - .map(|(interface_id, _, key)| (interface_id, key)), - ); - #[cfg(not(feature = "avahi"))] - let mdns_fut = futures::future::ready(()); - mdns_fut - },); + .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(pkg_id.clone(), ip, interfaces) + } + ); tor_res?; + nginx_res?; + Ok(()) } @@ -80,14 +110,19 @@ impl NetController { pkg_id: &PackageId, interfaces: I, ) -> Result<(), Error> { - let (tor_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 - }); + 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(()) } } diff --git a/appmgr/src/net/nginx.conf.template b/appmgr/src/net/nginx.conf.template index 4f244becb..df6180abb 100644 --- a/appmgr/src/net/nginx.conf.template +++ b/appmgr/src/net/nginx.conf.template @@ -1,6 +1,8 @@ server {{ - listen {port}; + listen {listen_args}; server_name {hostname}.local; + {ssl_certificate_line} + {ssl_certificate_key_line} location / {{ proxy_pass http://{app_ip}:{internal_port}/; proxy_set_header Host $host; diff --git a/appmgr/src/net/nginx.rs b/appmgr/src/net/nginx.rs new file mode 100644 index 000000000..edbb4b37d --- /dev/null +++ b/appmgr/src/net/nginx.rs @@ -0,0 +1,187 @@ +use std::collections::HashMap; +use std::net::Ipv4Addr; +use std::path::PathBuf; + +use indexmap::{IndexMap, IndexSet}; +use sqlx::SqlitePool; +use tokio::sync::Mutex; + +use super::interface::{InterfaceId, LanPortConfig}; +use super::ssl::SslManager; +use crate::s9pk::manifest::PackageId; +use crate::util::{Invoke, Port}; +use crate::{Error, ErrorKind}; + +pub struct NginxController(Mutex); +impl NginxController { + pub async fn init(nginx_root: PathBuf, db: SqlitePool) -> Result { + Ok(NginxController(Mutex::new( + NginxControllerInner::init(nginx_root, db).await?, + ))) + } + pub async fn add>( + &self, + package: PackageId, + ipv4: Ipv4Addr, + interfaces: I, + ) -> Result<(), Error> { + self.0.lock().await.add(package, ipv4, interfaces).await + } + pub async fn remove(&self, package: &PackageId) -> Result<(), Error> { + self.0.lock().await.remove(package).await + } +} + +pub struct NginxControllerInner { + nginx_root: PathBuf, + interfaces: HashMap, + ssl_manager: SslManager, +} +impl NginxControllerInner { + async fn init(nginx_root: PathBuf, db: SqlitePool) -> Result { + Ok(NginxControllerInner { + nginx_root, + interfaces: HashMap::new(), + ssl_manager: SslManager::init(db).await?, + }) + } + async fn add>( + &mut self, + 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::>(); + + 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 { + let ssl_path_key = self + .nginx_root + .join(format!("ssl/{}_{}.key.pem", package, id)); + let ssl_path_cert = self + .nginx_root + .join(format!("ssl/{}_{}.cert.pem", package, id)); + let (key, chain) = self.ssl_manager.certificate_for(&meta.dns_base).await?; + // write nginx ssl certs + futures::try_join!( + tokio::fs::write(&ssl_path_key, key.private_key_to_pem_pkcs8()?), + tokio::fs::write( + &ssl_path_cert, + chain + .into_iter() + .flat_map(|c| c.to_pem().unwrap()) + .collect::>() + ), + )?; + + ( + 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 = self + .nginx_root + .join(format!("sites-available/{}_{}.conf", package, id)); + tokio::fs::write( + &nginx_conf_path, + format!( + include_str!("nginx.conf.template"), + listen_args = 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?; + let sites_enabled_link_path = self + .nginx_root + .join(format!("sites-enabled/{}_{}.conf", package, id)); + 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?; + } + } + match self.interfaces.get_mut(&package) { + None => { + let info = PackageNetInfo { + ip: ipv4, + interfaces: interface_map, + }; + self.interfaces.insert(package, info); + } + Some(p) => { + p.interfaces.extend(interface_map); + } + }; + + self.hup().await?; + Ok(()) + } + async fn remove(&mut self, package: &PackageId) -> Result<(), Error> { + let removed = self.interfaces.remove(package); + if let Some(net_info) = removed { + for (id, _meta) in net_info.interfaces { + // TODO remove ssl certificates and nginx configs + let _ = futures::try_join!( + tokio::fs::remove_file( + self.nginx_root + .join(format!("ssl/{}_{}.key.pem", package, id)) + ), + tokio::fs::remove_file( + self.nginx_root + .join(format!("ssl/{}_{}.cert.pem", package, id)) + ), + tokio::fs::remove_file( + self.nginx_root + .join(format!("sites-enabled/{}_{}.conf", package, id)) + ), + tokio::fs::remove_file( + self.nginx_root + .join(format!("sites-available/{}_{}.conf", package, id)) + ), + )?; + } + } + self.hup().await?; + Ok(()) + } + async fn hup(&self) -> Result<(), Error> { + let _ = tokio::process::Command::new("systemctl") + .arg("reload") + .arg("nginx") + .invoke(ErrorKind::Nginx) + .await?; + Ok(()) + } +} +struct PackageNetInfo { + ip: Ipv4Addr, + interfaces: HashMap, +} +pub struct InterfaceMetadata { + pub dns_base: String, + pub lan_config: IndexMap, + pub protocols: IndexSet, +}