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:
Keagan McClelland
2021-02-24 12:59:05 -07:00
committed by Aiden McClelland
parent 882cfde5f3
commit c83baec363
14 changed files with 728 additions and 652 deletions

1030
appmgr/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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)"

View File

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

View 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

View File

@@ -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
View 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,
) {
}

View File

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

View File

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

View 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;
}}

View File

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

View File

@@ -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
View File

@@ -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",

View File

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