mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Appmgr/feature/lan (#209)
* WIP: Lan with blocking * stuff * appmgr: non-ui lan services * dbus linker errors * appmgr: allocate on stack * dns resolves finally * cleanup * appmgr: generate ssl if missing * appmgr: remove -p for purge Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
committed by
Aiden McClelland
parent
882cfde5f3
commit
c83baec363
1030
appmgr/Cargo.lock
generated
1030
appmgr/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,18 +18,21 @@ portable = []
|
||||
production = []
|
||||
|
||||
[dependencies]
|
||||
argonautica = "0.2.0"
|
||||
async-trait = "0.1.42"
|
||||
avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", branch = "feature/dynamic-linking", features = ["dynamic"] }
|
||||
base32 = "0.4.0"
|
||||
clap = "2.33"
|
||||
ctrlc = "3.1.7"
|
||||
ed25519-dalek = "1.0.1"
|
||||
emver = { version = "0.1.0", features = ["serde"] }
|
||||
failure = "0.1.8"
|
||||
file-lock = "1.1"
|
||||
futures = "0.3.8"
|
||||
git-version = "0.3.4"
|
||||
http = "0.2.3"
|
||||
itertools = "0.9.0"
|
||||
lazy_static = "1.4"
|
||||
libc = "0.2.86"
|
||||
linear-map = { version = "1.2", features = ["serde_impl"] }
|
||||
log = "0.4.11"
|
||||
nix = "0.19.1"
|
||||
@@ -41,6 +44,8 @@ rand = "0.7.3"
|
||||
regex = "1.4.2"
|
||||
reqwest = { version = "0.10.9", features = ["stream", "json"] }
|
||||
rpassword = "5.0.0"
|
||||
rust-argon2 = "0.8.3"
|
||||
scopeguard = "1.1" # because avahi-sys fucks your shit up
|
||||
serde = { version = "1.0.118", features = ["derive", "rc"] }
|
||||
serde_cbor = "0.11.1"
|
||||
serde_json = "1.0.59"
|
||||
|
||||
@@ -6,4 +6,4 @@ shopt -s expand_aliases
|
||||
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
|
||||
|
||||
cd ..
|
||||
rust-arm-builder sh -c "(cd appmgr && cargo build --release)"
|
||||
rust-arm-builder sh -c "(cd appmgr && cargo build)"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::path::Path;
|
||||
|
||||
use argonautica::{Hasher, Verifier};
|
||||
use argon2::Config;
|
||||
use emver::Version;
|
||||
use futures::try_join;
|
||||
use futures::TryStreamExt;
|
||||
use rand::Rng;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::util::from_yaml_async_reader;
|
||||
@@ -46,10 +47,7 @@ pub async fn create_backup<P: AsRef<Path>>(
|
||||
let mut hash = String::new();
|
||||
f.read_to_string(&mut hash).await?;
|
||||
crate::ensure_code!(
|
||||
Verifier::new()
|
||||
.with_password(password)
|
||||
.with_hash(hash)
|
||||
.verify()
|
||||
argon2::verify_encoded(&hash, password.as_bytes())
|
||||
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
|
||||
crate::error::INVALID_BACKUP_PASSWORD,
|
||||
"Invalid Backup Decryption Password"
|
||||
@@ -58,10 +56,8 @@ pub async fn create_backup<P: AsRef<Path>>(
|
||||
{
|
||||
// save password
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
let mut hasher = Hasher::default();
|
||||
hasher.opt_out_of_secret_key(true);
|
||||
let hash = hasher.with_password(password).hash().no_code()?;
|
||||
let salt = rand::thread_rng().gen::<[u8; 32]>();
|
||||
let hash = argon2::hash_encoded(password.as_bytes(), &salt, &Config::default()).unwrap(); // this is safe because apparently the API was poorly designed
|
||||
let mut f = tokio::fs::File::create(pw_path).await?;
|
||||
f.write_all(hash.as_bytes()).await?;
|
||||
f.flush().await?;
|
||||
@@ -160,10 +156,7 @@ pub async fn restore_backup<P: AsRef<Path>>(
|
||||
let mut hash = String::new();
|
||||
f.read_to_string(&mut hash).await?;
|
||||
crate::ensure_code!(
|
||||
Verifier::new()
|
||||
.with_password(password)
|
||||
.with_hash(hash)
|
||||
.verify()
|
||||
argon2::verify_encoded(&hash, password.as_bytes())
|
||||
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
|
||||
crate::error::INVALID_BACKUP_PASSWORD,
|
||||
"Invalid Backup Decryption Password"
|
||||
|
||||
10
appmgr/src/cert-local.csr.conf.template
Normal file
10
appmgr/src/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
|
||||
@@ -1864,6 +1864,9 @@ mod test {
|
||||
hidden_service_version: crate::tor::HiddenServiceVersion::V3,
|
||||
dependencies: deps,
|
||||
extra: LinearMap::new(),
|
||||
install_alert: None,
|
||||
restore_alert: None,
|
||||
uninstall_alert: None,
|
||||
})
|
||||
.unwrap();
|
||||
let config = spec
|
||||
|
||||
79
appmgr/src/lan.rs
Normal file
79
appmgr/src/lan.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use crate::Error;
|
||||
use avahi_sys;
|
||||
use futures::future::pending;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct AppId {
|
||||
pub un_app_id: String,
|
||||
}
|
||||
|
||||
pub async fn enable_lan(app_id: &AppId) -> Result<(), Error> {
|
||||
let tor_address = crate::apps::info(&app_id.un_app_id).await?.tor_address;
|
||||
let lan_address = tor_address
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
failure::format_err!("Service {} does not have Tor Address", app_id.un_app_id)
|
||||
})?
|
||||
.strip_suffix(".onion")
|
||||
.ok_or_else(|| failure::format_err!("Invalid Tor Address: {:?}", tor_address))?
|
||||
.to_owned()
|
||||
+ ".local";
|
||||
let lan_address_ptr =
|
||||
std::ffi::CString::new(lan_address).expect("Could not cast lan address to c string");
|
||||
unsafe {
|
||||
let simple_poll = avahi_sys::avahi_simple_poll_new();
|
||||
let poll = avahi_sys::avahi_simple_poll_get(simple_poll);
|
||||
let mut stack_err = 0;
|
||||
let err_c: *mut i32 = &mut stack_err;
|
||||
let avahi_client = avahi_sys::avahi_client_new(
|
||||
poll,
|
||||
avahi_sys::AvahiClientFlags::AVAHI_CLIENT_NO_FAIL,
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
err_c,
|
||||
);
|
||||
let group =
|
||||
avahi_sys::avahi_entry_group_new(avahi_client, Some(noop), std::ptr::null_mut());
|
||||
let hostname_raw = avahi_sys::avahi_client_get_host_name_fqdn(avahi_client);
|
||||
let hostname_bytes = dbg!(std::ffi::CStr::from_ptr(hostname_raw)).to_bytes_with_nul();
|
||||
const HOSTNAME_LEN: usize = 1 + 15 + 1 + 5; // leading byte, main address, dot, "local"
|
||||
debug_assert_eq!(hostname_bytes.len(), HOSTNAME_LEN);
|
||||
let mut hostname_buf = [0; HOSTNAME_LEN + 1];
|
||||
hostname_buf[1..].copy_from_slice(hostname_bytes);
|
||||
// assume fixed length prefix on hostname due to local address
|
||||
hostname_buf[0] = 15; // set the prefix length to 15 for the main address
|
||||
hostname_buf[16] = 5; // set the prefix length to 5 for "local"
|
||||
dbg!(hostname_buf);
|
||||
let _ = avahi_sys::avahi_entry_group_add_record(
|
||||
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,
|
||||
hostname_buf.as_ptr().cast(),
|
||||
hostname_buf.len(),
|
||||
);
|
||||
avahi_sys::avahi_entry_group_commit(group);
|
||||
println!("{:?}", lan_address_ptr);
|
||||
ctrlc::set_handler(move || {
|
||||
// please the borrow checker with the below semantics
|
||||
// avahi_sys::avahi_entry_group_free(group);
|
||||
// avahi_sys::avahi_client_free(avahi_client);
|
||||
// drop(Box::from_raw(err_c));
|
||||
std::process::exit(0);
|
||||
})
|
||||
.expect("Error setting signal handler");
|
||||
}
|
||||
pending().await
|
||||
}
|
||||
|
||||
unsafe extern "C" fn noop(
|
||||
_group: *mut avahi_sys::AvahiEntryGroup,
|
||||
_state: avahi_sys::AvahiEntryGroupState,
|
||||
_userdata: *mut core::ffi::c_void,
|
||||
) {
|
||||
}
|
||||
@@ -31,6 +31,7 @@ pub mod error;
|
||||
pub mod index;
|
||||
pub mod inspect;
|
||||
pub mod install;
|
||||
pub mod lan;
|
||||
pub mod logs;
|
||||
pub mod manifest;
|
||||
pub mod pack;
|
||||
|
||||
@@ -399,7 +399,6 @@ async fn inner_main() -> Result<(), Error> {
|
||||
.about("Removes an installed app")
|
||||
.arg(
|
||||
Arg::with_name("purge")
|
||||
.short("p")
|
||||
.long("purge")
|
||||
.help("Deletes all application data"),
|
||||
)
|
||||
@@ -449,6 +448,19 @@ async fn inner_main() -> Result<(), Error> {
|
||||
)
|
||||
.subcommand(SubCommand::with_name("reload").about("Reloads the tor configuration")),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("lan")
|
||||
.about("Configures LAN services")
|
||||
.subcommand(
|
||||
SubCommand::with_name("enable")
|
||||
.about("Publishes the LAN address for the service over avahi")
|
||||
.arg(
|
||||
Arg::with_name("ID")
|
||||
.help("ID of the application to publish the LAN address for")
|
||||
.required(true),
|
||||
),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("info")
|
||||
.about("Prints information about an installed app")
|
||||
@@ -1189,6 +1201,19 @@ async fn inner_main() -> Result<(), Error> {
|
||||
}
|
||||
},
|
||||
#[cfg(not(feature = "portable"))]
|
||||
("lan", Some(sub_m)) => match sub_m.subcommand() {
|
||||
("enable", Some(sub_sub_m)) => {
|
||||
crate::lan::enable_lan(&crate::lan::AppId {
|
||||
un_app_id: sub_sub_m.value_of("ID").unwrap().to_owned(),
|
||||
})
|
||||
.await?
|
||||
}
|
||||
_ => {
|
||||
println!("{}", sub_m.usage());
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
#[cfg(not(feature = "portable"))]
|
||||
("info", Some(sub_m)) => {
|
||||
let name = sub_m.value_of("ID").unwrap();
|
||||
let info = crate::apps::info_full(
|
||||
|
||||
15
appmgr/src/nginx-standard.conf.template
Normal file
15
appmgr/src/nginx-standard.conf.template
Normal file
@@ -0,0 +1,15 @@
|
||||
server {{
|
||||
listen 443 ssl;
|
||||
server_name {hostname}.local;
|
||||
ssl_certificate /root/appmgr/apps/{app_id}/cert-local.crt.pem;
|
||||
ssl_certificate_key /root/appmgr/apps/{app_id}/cert-local.key.pem;
|
||||
location / {{
|
||||
proxy_pass http://{app_ip}:{internal_port}/;
|
||||
proxy_set_header Host $host;
|
||||
}}
|
||||
}}
|
||||
server {{
|
||||
listen 80;
|
||||
server_name {hostname}.local;
|
||||
return 301 https://$host$request_uri;
|
||||
}}
|
||||
@@ -1,15 +1,8 @@
|
||||
server {{
|
||||
listen 443 ssl;
|
||||
server_name {app_id}.{hostname}.local;
|
||||
ssl_certificate /etc/nginx/ssl/{hostname}-local.crt.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/{hostname}-local.key.pem;
|
||||
listen {port};
|
||||
server_name {hostname}.local;
|
||||
location / {{
|
||||
proxy_pass http://{app_ip}:80/;
|
||||
proxy_pass http://{app_ip}:{internal_port}/;
|
||||
proxy_set_header Host $host;
|
||||
}}
|
||||
}}
|
||||
server {{
|
||||
listen 80;
|
||||
server_name {hostname}.local;
|
||||
return 301 https://$host$request_uri;
|
||||
}}
|
||||
@@ -8,13 +8,47 @@ use failure::ResultExt as _;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::util::{PersistencePath, YamlUpdateHandle};
|
||||
use crate::util::{Invoke, PersistencePath, YamlUpdateHandle};
|
||||
use crate::{Error, ResultExt as _};
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LanOptions {
|
||||
Standard,
|
||||
Custom { port: u16 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize)]
|
||||
pub struct PortMapping {
|
||||
pub internal: u16,
|
||||
pub tor: u16,
|
||||
pub lan: Option<LanOptions>, // only for http interfaces
|
||||
}
|
||||
impl<'de> serde::de::Deserialize<'de> for PortMapping {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct PortMappingIF {
|
||||
pub internal: u16,
|
||||
pub tor: u16,
|
||||
#[serde(default)]
|
||||
pub lan: Option<Option<LanOptions>>,
|
||||
}
|
||||
let input_format: PortMappingIF = serde::de::Deserialize::deserialize(deserializer)?;
|
||||
Ok(PortMapping {
|
||||
internal: input_format.internal,
|
||||
tor: input_format.tor,
|
||||
lan: if let Some(lan) = input_format.lan {
|
||||
lan
|
||||
} else if input_format.tor == 80 {
|
||||
Some(LanOptions::Standard)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc";
|
||||
@@ -184,28 +218,118 @@ pub async fn write_services(hidden_services: &ServicesMap) -> Result<(), Error>
|
||||
}
|
||||
|
||||
pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Error> {
|
||||
let hostname = tokio::fs::read_to_string(ETC_HOSTNAME).await?;
|
||||
let mut f = tokio::fs::File::create(ETC_NGINX_SERVICES_CONF).await?;
|
||||
for (name, service) in &hidden_services.map {
|
||||
if service
|
||||
.ports
|
||||
.iter()
|
||||
.filter(|p| p.internal == 80)
|
||||
.next()
|
||||
.is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
f.write_all(
|
||||
format!(
|
||||
include_str!("nginx.conf.template"),
|
||||
hostname = hostname.trim(),
|
||||
app_id = name,
|
||||
app_ip = service.ip,
|
||||
)
|
||||
.as_bytes(),
|
||||
for (app_id, service) in &hidden_services.map {
|
||||
let hostname = tokio::fs::read_to_string(
|
||||
Path::new(HIDDEN_SERVICE_DIR_ROOT)
|
||||
.join(format!("app-{}", app_id))
|
||||
.join("hostname"),
|
||||
)
|
||||
.await?;
|
||||
let hostname_str = hostname
|
||||
.trim()
|
||||
.strip_suffix(".onion")
|
||||
.ok_or_else(|| failure::format_err!("invalid tor hostname"))
|
||||
.no_code()?;
|
||||
for mapping in &service.ports {
|
||||
match &mapping.lan {
|
||||
Some(LanOptions::Standard) => {
|
||||
let base_path = PersistencePath::from_ref("apps").join(&app_id);
|
||||
let key_path = base_path.join("cert-local.key.pem").path();
|
||||
if tokio::fs::metadata(&key_path).await.is_err() {
|
||||
tokio::process::Command::new("openssl")
|
||||
.arg("ecparam")
|
||||
.arg("-genkey")
|
||||
.arg("-name")
|
||||
.arg("prime256v1")
|
||||
.arg("-noout")
|
||||
.arg("-out")
|
||||
.arg(&key_path)
|
||||
.invoke("OpenSSL GenKey")
|
||||
.await?;
|
||||
}
|
||||
let conf_path = base_path.join("cert-local.csr.conf").path();
|
||||
if tokio::fs::metadata(&conf_path).await.is_err() {
|
||||
tokio::fs::write(
|
||||
&conf_path,
|
||||
format!(
|
||||
include_str!("cert-local.csr.conf.template"),
|
||||
hostname = hostname_str
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let req_path = base_path.join("cert-local.csr").path();
|
||||
if tokio::fs::metadata(&req_path).await.is_err() {
|
||||
tokio::process::Command::new("openssl")
|
||||
.arg("req")
|
||||
.arg("-config")
|
||||
.arg(&conf_path)
|
||||
.arg("-key")
|
||||
.arg(&key_path)
|
||||
.arg("-new")
|
||||
.arg("-addext")
|
||||
.arg(format!(
|
||||
"subjectAltName=DNS:{hostname}.local",
|
||||
hostname = hostname_str
|
||||
))
|
||||
.arg("-out")
|
||||
.arg(&req_path)
|
||||
.invoke("OpenSSL Req")
|
||||
.await?;
|
||||
}
|
||||
let cert_path = base_path.join("cert-local.crt.pem").path();
|
||||
if tokio::fs::metadata(&cert_path).await.is_err() {
|
||||
tokio::process::Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-batch")
|
||||
.arg("-config")
|
||||
.arg("/root/agent/ca/intermediate/openssl.conf")
|
||||
.arg("-rand_serial")
|
||||
.arg("-keyfile")
|
||||
.arg("/root/agent/ca/intermediate/private/embassy-int-ca.key.pem")
|
||||
.arg("-cert")
|
||||
.arg("/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem")
|
||||
.arg("-extensions")
|
||||
.arg("server_cert")
|
||||
.arg("-days")
|
||||
.arg("365")
|
||||
.arg("-notext")
|
||||
.arg("-in")
|
||||
.arg(&req_path)
|
||||
.arg("-out")
|
||||
.arg(&cert_path)
|
||||
.invoke("OpenSSL GenKey")
|
||||
.await?;
|
||||
}
|
||||
f.write_all(
|
||||
format!(
|
||||
include_str!("nginx-standard.conf.template"),
|
||||
hostname = hostname_str,
|
||||
app_ip = service.ip,
|
||||
internal_port = mapping.internal,
|
||||
app_id = app_id,
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Some(LanOptions::Custom { port }) => {
|
||||
f.write_all(
|
||||
format!(
|
||||
include_str!("nginx.conf.template"),
|
||||
hostname = hostname_str,
|
||||
app_ip = service.ip,
|
||||
port = port,
|
||||
internal_port = mapping.internal,
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "embassy-ui",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.9",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -7903,9 +7903,9 @@
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-1.2.5.tgz",
|
||||
"integrity": "sha512-2AlqgYnVPOc9WDyWu7S5DJaEZsfk6dNh/neatQ3IHUW4QLutM/VPSH9lG7bif+XjFWc9K9XR3QvR+fXuECmfdA=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-2.0.0.tgz",
|
||||
"integrity": "sha512-NqRSh2+LlN2NInpqTQnS614Y/3NkVMFFU6sJlRFEpxJ/LHuK/qJECH7/fXZjk4VZstPW/Pevjil/VtSONsLc7Q=="
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.5",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"json-pointer": "^0.6.1",
|
||||
"jsonpointerx": "^1.0.30",
|
||||
"jsontokens": "^3.0.0",
|
||||
"marked": "^1.2.0",
|
||||
"marked": "^2.0.0",
|
||||
"rxjs": "^6.6.3",
|
||||
"uuid": "^8.3.1",
|
||||
"zone.js": "^0.11.2"
|
||||
|
||||
Reference in New Issue
Block a user