mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
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:
committed by
Aiden McClelland
parent
8b15a5f443
commit
68fbc34bce
@@ -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();
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
187
appmgr/src/net/nginx.rs
Normal 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>,
|
||||
}
|
||||
Reference in New Issue
Block a user