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
This commit is contained in:
Keagan McClelland
2021-09-20 11:13:04 -06:00
committed by Aiden McClelland
parent 8b15a5f443
commit 68fbc34bce
5 changed files with 248 additions and 23 deletions

View File

@@ -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();

View File

@@ -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<TorConfig>,
pub lan_config: Option<IndexMap<Port, LanPortConfig>>,
pub ui: bool,
pub protocols: Vec<String>,
pub protocols: IndexSet<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]

View File

@@ -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<Self, Error> {
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::<Vec<(InterfaceId, TorConfig, TorSecretKeyV3)>>();
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(())
}
}

View File

@@ -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;

187
appmgr/src/net/nginx.rs Normal file
View File

@@ -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<NginxControllerInner>);
impl NginxController {
pub async fn init(nginx_root: PathBuf, db: SqlitePool) -> Result<Self, Error> {
Ok(NginxController(Mutex::new(
NginxControllerInner::init(nginx_root, db).await?,
)))
}
pub async fn add<I: IntoIterator<Item = (InterfaceId, InterfaceMetadata)>>(
&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<PackageId, PackageNetInfo>,
ssl_manager: SslManager,
}
impl NginxControllerInner {
async fn init(nginx_root: PathBuf, db: SqlitePool) -> Result<Self, Error> {
Ok(NginxControllerInner {
nginx_root,
interfaces: HashMap::new(),
ssl_manager: SslManager::init(db).await?,
})
}
async fn add<I: IntoIterator<Item = (InterfaceId, InterfaceMetadata)>>(
&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::<HashMap<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 {
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::<Vec<u8>>()
),
)?;
(
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<InterfaceId, InterfaceMetadata>,
}
pub struct InterfaceMetadata {
pub dns_base: String,
pub lan_config: IndexMap<Port, LanPortConfig>,
pub protocols: IndexSet<String>,
}