mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +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?,
|
crate::net::tor::os_key(&mut secret_store.acquire().await?).await?,
|
||||||
base.tor_control
|
base.tor_control
|
||||||
.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))),
|
.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))),
|
||||||
|
secret_store.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let managers = ManagerMap::default();
|
let managers = ManagerMap::default();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use indexmap::IndexMap;
|
use indexmap::{IndexMap, IndexSet};
|
||||||
use itertools::Either;
|
use itertools::Either;
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use sqlx::{Executor, Sqlite};
|
use sqlx::{Executor, Sqlite};
|
||||||
@@ -150,7 +150,7 @@ pub struct Interface {
|
|||||||
pub tor_config: Option<TorConfig>,
|
pub tor_config: Option<TorConfig>,
|
||||||
pub lan_config: Option<IndexMap<Port, LanPortConfig>>,
|
pub lan_config: Option<IndexMap<Port, LanPortConfig>>,
|
||||||
pub ui: bool,
|
pub ui: bool,
|
||||||
pub protocols: Vec<String>,
|
pub protocols: IndexSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
use std::net::{Ipv4Addr, SocketAddr};
|
use std::net::{Ipv4Addr, SocketAddr};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use rpc_toolkit::command;
|
use rpc_toolkit::command;
|
||||||
use torut::onion::TorSecretKeyV3;
|
use sqlx::SqlitePool;
|
||||||
|
use torut::onion::{OnionAddressV3, TorSecretKeyV3};
|
||||||
|
|
||||||
use self::interface::{Interface, InterfaceId};
|
use self::interface::{Interface, InterfaceId};
|
||||||
#[cfg(feature = "avahi")]
|
#[cfg(feature = "avahi")]
|
||||||
use self::mdns::MdnsController;
|
use self::mdns::MdnsController;
|
||||||
|
use self::nginx::NginxController;
|
||||||
use self::tor::TorController;
|
use self::tor::TorController;
|
||||||
use crate::net::interface::TorConfig;
|
use crate::net::interface::TorConfig;
|
||||||
|
use crate::net::nginx::InterfaceMetadata;
|
||||||
use crate::s9pk::manifest::PackageId;
|
use crate::s9pk::manifest::PackageId;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
pub mod interface;
|
pub mod interface;
|
||||||
#[cfg(feature = "avahi")]
|
#[cfg(feature = "avahi")]
|
||||||
pub mod mdns;
|
pub mod mdns;
|
||||||
|
pub mod nginx;
|
||||||
pub mod ssl;
|
pub mod ssl;
|
||||||
pub mod tor;
|
pub mod tor;
|
||||||
pub mod wifi;
|
pub mod wifi;
|
||||||
@@ -27,18 +32,20 @@ pub struct NetController {
|
|||||||
pub tor: TorController,
|
pub tor: TorController,
|
||||||
#[cfg(feature = "avahi")]
|
#[cfg(feature = "avahi")]
|
||||||
pub mdns: MdnsController,
|
pub mdns: MdnsController,
|
||||||
// nginx: NginxController, // TODO
|
nginx: NginxController,
|
||||||
}
|
}
|
||||||
impl NetController {
|
impl NetController {
|
||||||
pub async fn init(
|
pub async fn init(
|
||||||
embassyd_addr: SocketAddr,
|
embassyd_addr: SocketAddr,
|
||||||
embassyd_tor_key: TorSecretKeyV3,
|
embassyd_tor_key: TorSecretKeyV3,
|
||||||
tor_control: SocketAddr,
|
tor_control: SocketAddr,
|
||||||
|
db: SqlitePool,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
tor: TorController::init(embassyd_addr, embassyd_tor_key, tor_control).await?,
|
tor: TorController::init(embassyd_addr, embassyd_tor_key, tor_control).await?,
|
||||||
#[cfg(feature = "avahi")]
|
#[cfg(feature = "avahi")]
|
||||||
mdns: MdnsController::init(),
|
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)),
|
Some(cfg) => Some((i.0, cfg, i.2)),
|
||||||
})
|
})
|
||||||
.collect::<Vec<(InterfaceId, TorConfig, TorSecretKeyV3)>>();
|
.collect::<Vec<(InterfaceId, TorConfig, TorSecretKeyV3)>>();
|
||||||
let (tor_res, _) = tokio::join!(self.tor.add(pkg_id, ip, interfaces_tor), {
|
let (tor_res, _, nginx_res) = tokio::join!(
|
||||||
#[cfg(feature = "avahi")]
|
self.tor.add(pkg_id, ip, interfaces_tor),
|
||||||
let mdns_fut = self.mdns.add(
|
{
|
||||||
pkg_id,
|
#[cfg(feature = "avahi")]
|
||||||
interfaces
|
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()
|
.into_iter()
|
||||||
.map(|(interface_id, _, key)| (interface_id, key)),
|
.filter_map(|(id, interface, tor_key)| match &interface.lan_config {
|
||||||
);
|
None => None,
|
||||||
#[cfg(not(feature = "avahi"))]
|
Some(cfg) => Some((
|
||||||
let mdns_fut = futures::future::ready(());
|
id,
|
||||||
mdns_fut
|
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?;
|
tor_res?;
|
||||||
|
nginx_res?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,14 +110,19 @@ impl NetController {
|
|||||||
pkg_id: &PackageId,
|
pkg_id: &PackageId,
|
||||||
interfaces: I,
|
interfaces: I,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let (tor_res, _) = tokio::join!(self.tor.remove(pkg_id, interfaces.clone()), {
|
let (tor_res, _, nginx_res) = tokio::join!(
|
||||||
#[cfg(feature = "avahi")]
|
self.tor.remove(pkg_id, interfaces.clone()),
|
||||||
let mdns_fut = self.mdns.remove(pkg_id, interfaces);
|
{
|
||||||
#[cfg(not(feature = "avahi"))]
|
#[cfg(feature = "avahi")]
|
||||||
let mdns_fut = futures::future::ready(());
|
let mdns_fut = self.mdns.remove(pkg_id, interfaces);
|
||||||
mdns_fut
|
#[cfg(not(feature = "avahi"))]
|
||||||
});
|
let mdns_fut = futures::future::ready(());
|
||||||
|
mdns_fut
|
||||||
|
},
|
||||||
|
self.nginx.remove(pkg_id)
|
||||||
|
);
|
||||||
tor_res?;
|
tor_res?;
|
||||||
|
nginx_res?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
server {{
|
server {{
|
||||||
listen {port};
|
listen {listen_args};
|
||||||
server_name {hostname}.local;
|
server_name {hostname}.local;
|
||||||
|
{ssl_certificate_line}
|
||||||
|
{ssl_certificate_key_line}
|
||||||
location / {{
|
location / {{
|
||||||
proxy_pass http://{app_ip}:{internal_port}/;
|
proxy_pass http://{app_ip}:{internal_port}/;
|
||||||
proxy_set_header Host $host;
|
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