Compare commits

..
Author SHA1 Message Date
f2fb75a099 chore(deps): update rust crate livekit-api to 0.6.0
Some checks failed
renovate/artifacts Artifact file update failure
renovate/stability-days Updates have met minimum release age requirement
2026-08-29 14:01:56 +03:00
Alex Emmet
82a5d9469b
[Fix] Connectivity 2026-08-29 12:48:46 +02:00
11 changed files with 131 additions and 148 deletions

1
Cargo.lock generated
View file

@ -2030,7 +2030,6 @@ dependencies = [
"tokio", "tokio",
"trust-dns-resolver", "trust-dns-resolver",
"uuid", "uuid",
"zeroize",
] ]
[[package]] [[package]]

View file

@ -9,6 +9,7 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
"client", "client",
"crypto", "crypto",
"files", "files",
"raw",
] } ] }
mtp-transport = { git = "https://git.methanium.net/Methanium/mtp.git" } mtp-transport = { git = "https://git.methanium.net/Methanium/mtp.git" }
@ -33,12 +34,3 @@ livekit-protocol = "=0.7.10"
thiserror = "2.0.19" thiserror = "2.0.19"
trust-dns-resolver = "0.23.2" trust-dns-resolver = "0.23.2"
serde_json = "1.0.151" serde_json = "1.0.151"
zeroize = "1.9"
[features]
raw-migration = ["mtp/raw"]
[[bin]]
name = "migrate_raw_keyring"
path = "src/bin/migrate_raw_keyring.rs"
required-features = ["raw-migration"]

View file

@ -7,7 +7,7 @@ The Omikron is only used when the Iota is in Centralized and Hybrid mode, or whe
## Configuration ## Configuration
`OMIKRON_IDENTITY_SECRET` is required. Provision it through the deployment's secret environment before starting Omikron. Omikron uses it to load `omikron.mk` as a protected keyring and fails startup if the secret or existing identity cannot be loaded. Omikron stores its keyring unencrypted in `omikron.mk` and its public key bundle in `omikron.mpkb`. If the keyring does not exist, Omikron generates it on startup.
Rho connection budgets can be configured with: Rho connection budgets can be configured with:
@ -19,11 +19,3 @@ Rho connection budgets can be configured with:
semaphore and to the MTP WebServer admission semaphore. This means the same semaphore and to the MTP WebServer admission semaphore. This means the same
budget limits transport handshakes and authenticated Rho sessions instead of budget limits transport handshakes and authenticated Rho sessions instead of
only limiting connections after authentication. only limiting connections after authentication.
To migrate an existing raw `omikron.mk`, provision `OMIKRON_IDENTITY_SECRET` and run:
```sh
cargo run --features raw-migration --bin migrate_raw_keyring
```
The normal Omikron binary does not enable raw keyring support.

@ -1 +1 @@
Subproject commit f3c5037b0a099ae5389486eec77471bd5addab4a Subproject commit f4e45aa3a3ad0e3c3a257f66857b904a1af7901c

View file

@ -1,23 +0,0 @@
#[path = "../identity.rs"]
mod identity;
use identity::{
KEYRING_PATH, PUBLIC_KEY_PATH, identity_secret_from_environment, migrate_raw_keyring,
};
fn main() {
let secret = match identity_secret_from_environment() {
Ok(secret) => secret,
Err(error) => {
eprintln!("Unable to load Omikron identity secret: {error}");
std::process::exit(1);
}
};
if let Err(error) = migrate_raw_keyring(secret.as_slice(), KEYRING_PATH, PUBLIC_KEY_PATH) {
eprintln!("Unable to migrate Omikron identity: {error}");
std::process::exit(1);
}
println!("Migrated {KEYRING_PATH} to protected keyring storage");
}

View file

@ -1,37 +1,23 @@
use std::{env, io::ErrorKind, path::Path}; use std::{io::ErrorKind, path::Path};
use mtp::crypto::Keyring; use mtp::crypto::Keyring;
use mtp::files::{FileError, load_keyring, save_keyring, save_public_key_bundle}; use mtp::files::{FileError, load_keyring_raw, save_keyring_raw, save_public_key_bundle};
use zeroize::Zeroizing;
pub const KEYRING_PATH: &str = "./omikron.mk"; pub const KEYRING_PATH: &str = "./omikron.mk";
pub const PUBLIC_KEY_PATH: &str = "./omikron.mpkb"; pub const PUBLIC_KEY_PATH: &str = "./omikron.mpkb";
const IDENTITY_SECRET_ENV: &str = "OMIKRON_IDENTITY_SECRET";
pub fn identity_secret_from_environment() -> Result<Zeroizing<Vec<u8>>, String> {
let secret = env::var(IDENTITY_SECRET_ENV).map_err(|_| {
format!("{IDENTITY_SECRET_ENV} must be set before Omikron networking starts")
})?;
if secret.trim().is_empty() {
return Err(format!("{IDENTITY_SECRET_ENV} must not be empty"));
}
Ok(Zeroizing::new(secret.into_bytes()))
}
#[allow(dead_code)]
pub fn load_or_create_keyring( pub fn load_or_create_keyring(
passphrase: &[u8],
keyring_path: impl AsRef<Path>, keyring_path: impl AsRef<Path>,
public_key_path: impl AsRef<Path>, public_key_path: impl AsRef<Path>,
) -> Result<Keyring, String> { ) -> Result<Keyring, String> {
let keyring_path = keyring_path.as_ref(); let keyring_path = keyring_path.as_ref();
let public_key_path = public_key_path.as_ref(); let public_key_path = public_key_path.as_ref();
match load_keyring(keyring_path, passphrase) { match load_keyring_raw(keyring_path) {
Ok(keyring) => Ok(keyring), Ok(keyring) => Ok(keyring),
Err(FileError::Io(error)) if error.kind() == ErrorKind::NotFound => { Err(FileError::Io(error)) if error.kind() == ErrorKind::NotFound => {
let keyring = Keyring::generate(); let keyring = Keyring::generate();
save_keyring(&keyring, keyring_path, passphrase).map_err(|error| { save_keyring_raw(&keyring, keyring_path).map_err(|error| {
format!("unable to persist Omikron keyring at {keyring_path:?}: {error}") format!("unable to persist Omikron keyring at {keyring_path:?}: {error}")
})?; })?;
save_public_key_bundle(&keyring.public_key_bundle(), public_key_path).map_err( save_public_key_bundle(&keyring.public_key_bundle(), public_key_path).map_err(
@ -41,10 +27,7 @@ pub fn load_or_create_keyring(
) )
}, },
)?; )?;
eprintln!( eprintln!("Generated new keyring at {}", keyring_path.display());
"Generated new protected keyring at {}",
keyring_path.display()
);
Ok(keyring) Ok(keyring)
} }
Err(error) => Err(format!( Err(error) => Err(format!(
@ -54,29 +37,11 @@ pub fn load_or_create_keyring(
} }
} }
#[cfg(feature = "raw-migration")]
#[allow(dead_code)]
pub fn migrate_raw_keyring(
passphrase: &[u8],
keyring_path: impl AsRef<Path>,
public_key_path: impl AsRef<Path>,
) -> Result<Keyring, String> {
use mtp::files::load_keyring_raw;
let keyring = load_keyring_raw(keyring_path.as_ref())
.map_err(|error| format!("raw keyring migration failed: {error}"))?;
save_keyring(&keyring, keyring_path.as_ref(), passphrase)
.map_err(|error| format!("protected keyring write failed: {error}"))?;
save_public_key_bundle(&keyring.public_key_bundle(), public_key_path.as_ref())
.map_err(|error| format!("public key bundle write failed: {error}"))?;
Ok(keyring)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::{ use std::{
fs, env, fs,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
@ -111,11 +76,15 @@ mod tests {
#[test] #[test]
fn missing_identity_is_created_and_can_be_loaded_again() { fn missing_identity_is_created_and_can_be_loaded_again() {
let paths = TestPaths::new(); let paths = TestPaths::new();
let passphrase = b"test identity secret"; let first = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap();
let first = load_or_create_keyring(passphrase, &paths.keyring, &paths.public_key).unwrap();
let first_public = first.public_key_bundle().try_as_bytes().unwrap(); let first_public = first.public_key_bundle().try_as_bytes().unwrap();
let second = load_or_create_keyring(passphrase, &paths.keyring, &paths.public_key).unwrap(); let stored = load_keyring_raw(&paths.keyring).unwrap();
let second = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap();
assert_eq!(
stored.public_key_bundle().try_as_bytes().unwrap(),
first_public
);
assert_eq!( assert_eq!(
second.public_key_bundle().try_as_bytes().unwrap(), second.public_key_bundle().try_as_bytes().unwrap(),
first_public first_public
@ -127,9 +96,7 @@ mod tests {
let paths = TestPaths::new(); let paths = TestPaths::new();
fs::write(&paths.keyring, b"not a keyring").unwrap(); fs::write(&paths.keyring, b"not a keyring").unwrap();
let error = let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err();
load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key)
.unwrap_err();
assert!(error.contains("unable to load existing Omikron identity")); assert!(error.contains("unable to load existing Omikron identity"));
} }
@ -138,9 +105,7 @@ mod tests {
let paths = TestPaths::new(); let paths = TestPaths::new();
fs::create_dir(&paths.keyring).unwrap(); fs::create_dir(&paths.keyring).unwrap();
let error = let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err();
load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key)
.unwrap_err();
assert!(error.contains("unable to load existing Omikron identity")); assert!(error.contains("unable to load existing Omikron identity"));
} }
@ -149,33 +114,7 @@ mod tests {
let paths = TestPaths::new(); let paths = TestPaths::new();
fs::create_dir(&paths.public_key).unwrap(); fs::create_dir(&paths.public_key).unwrap();
let error = let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err();
load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key)
.unwrap_err();
assert!(error.contains("unable to persist Omikron public key bundle")); assert!(error.contains("unable to persist Omikron public key bundle"));
} }
#[cfg(feature = "raw-migration")]
#[test]
fn raw_identity_migration_rewrites_protected_storage() {
use mtp::files::{load_keyring, save_keyring_raw};
let paths = TestPaths::new();
let original = Keyring::generate();
save_keyring_raw(&original, &paths.keyring).unwrap();
let migrated =
migrate_raw_keyring(b"test identity secret", &paths.keyring, &paths.public_key)
.unwrap();
let loaded = load_keyring(&paths.keyring, b"test identity secret").unwrap();
assert_eq!(
migrated.public_key_bundle().try_as_bytes().unwrap(),
original.public_key_bundle().try_as_bytes().unwrap()
);
assert_eq!(
loaded.public_key_bundle().try_as_bytes().unwrap(),
original.public_key_bundle().try_as_bytes().unwrap()
);
}
} }

View file

@ -24,9 +24,7 @@ use crate::{
app_state::AppState, app_state::AppState,
calls::{call_manager::CallManager, call_util::LiveKitService}, calls::{call_manager::CallManager, call_util::LiveKitService},
config::Config, config::Config,
identity::{ identity::{KEYRING_PATH, PUBLIC_KEY_PATH, load_or_create_keyring},
KEYRING_PATH, PUBLIC_KEY_PATH, identity_secret_from_environment, load_or_create_keyring,
},
omega::omega_connection::{OmegaConnection, start_task_cleanup_loop}, omega::omega_connection::{OmegaConnection, start_task_cleanup_loop},
rho::rho_manager::RhoManager, rho::rho_manager::RhoManager,
rho::server::start, rho::server::start,
@ -50,18 +48,7 @@ async fn main() {
} }
}; };
let identity_secret = match identity_secret_from_environment() { let keyring = match load_or_create_keyring(KEYRING_PATH, PUBLIC_KEY_PATH) {
Ok(secret) => secret,
Err(error) => {
eprintln!("Unable to load Omikron identity secret: {error}");
return;
}
};
let keyring = match load_or_create_keyring(
&identity_secret,
KEYRING_PATH,
PUBLIC_KEY_PATH,
) {
Ok(keyring) => keyring, Ok(keyring) => keyring,
Err(error) => { Err(error) => {
eprintln!("Unable to load Omikron keyring: {error}"); eprintln!("Unable to load Omikron keyring: {error}");

View file

@ -17,6 +17,13 @@ use tokio::sync::RwLock;
use trust_dns_resolver::TokioAsyncResolver; use trust_dns_resolver::TokioAsyncResolver;
use uuid::Uuid; use uuid::Uuid;
fn authenticated_peer_control_request(
cv: &CommunicationValue,
user_id: u64,
) -> CommunicationValue {
cv.clone().with_sender(user_id)
}
pub struct ClientConnection { pub struct ClientConnection {
pub state: Arc<AppState>, pub state: Arc<AppState>,
pub user_id: u64, pub user_id: u64,
@ -210,7 +217,10 @@ impl ClientConnection {
.await; .await;
return; return;
}; };
match rho.await_peer_control(&cv.with_sender(self.user_id)).await { match rho
.await_peer_control(&authenticated_peer_control_request(&cv, self.user_id))
.await
{
Ok(response) => self.send_message(&response).await, Ok(response) => self.send_message(&response).await,
Err(_) => { Err(_) => {
self.send_error_response(message_id, CommunicationType::ErrorInternal) self.send_error_response(message_id, CommunicationType::ErrorInternal)
@ -968,6 +978,29 @@ impl ClientConnection {
} }
} }
#[cfg(test)]
mod tests {
use super::authenticated_peer_control_request;
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
#[test]
fn peer_control_requests_replace_a_client_supplied_sender() {
let request = CommunicationValue::new(CommunicationType::SyncedSettingSet)
.with_sender(99)
.add_typed_default(DataType::UserId, DataValue::SignedNumber(99));
let authenticated = authenticated_peer_control_request(&request, 7);
assert_eq!(authenticated.sender(), Some(7));
assert_eq!(
authenticated
.get_data(DataType::UserId)
.and_then(|value| value.as_number()),
Some(99)
);
}
}
// Implement Clone to make it easier to work with Arc<ClientConnection> // Implement Clone to make it easier to work with Arc<ClientConnection>
impl Clone for ClientConnection { impl Clone for ClientConnection {
fn clone(&self) -> Self { fn clone(&self) -> Self {

View file

@ -302,6 +302,13 @@ impl IotaConnection {
return; return;
} }
log_cv_in!(PrintType::Iota, cv);
if cv.is_type(CommunicationType::SyncedSettingChanged) {
self.forward_to_client(cv).await;
return;
}
let msg_id = message_id; let msg_id = message_id;
if let Some((_, task)) = self.waiting_tasks.remove(&msg_id) { if let Some((_, task)) = self.waiting_tasks.remove(&msg_id) {
if (task)(self.clone(), cv.clone()) { if (task)(self.clone(), cv.clone()) {
@ -309,8 +316,6 @@ impl IotaConnection {
} }
} }
log_cv_in!(PrintType::Iota, cv);
let cv = if cv.is_type(CommunicationType::ClientStateSync) { let cv = if cv.is_type(CommunicationType::ClientStateSync) {
self.add_call_state(cv).await self.add_call_state(cv).await
} else { } else {

View file

@ -95,6 +95,10 @@ pub fn message_security_class(frame: &CommunicationValue) -> MessageSecurityClas
} else if frame.is_type(CommunicationType::GetChatSecret) } else if frame.is_type(CommunicationType::GetChatSecret)
|| frame.is_type(CommunicationType::MessageGet) || frame.is_type(CommunicationType::MessageGet)
|| frame.is_type(CommunicationType::MessagesGet) || frame.is_type(CommunicationType::MessagesGet)
|| frame.is_type(CommunicationType::SyncedSettingSet)
|| frame.is_type(CommunicationType::SyncedSettingGet)
|| frame.is_type(CommunicationType::SyncedSettingDelete)
|| frame.is_type(CommunicationType::SyncedSettingsList)
{ {
MessageSecurityClass::AuthenticatedPeerControl MessageSecurityClass::AuthenticatedPeerControl
} else { } else {
@ -296,7 +300,8 @@ mod tests {
use mtp::codec::{CommunicationType, CommunicationValue, DataValue}; use mtp::codec::{CommunicationType, CommunicationValue, DataValue};
use super::{ use super::{
RelayRouteError, RelaySource, RouteTarget, prepare_frame, validate_source_next_hop, MessageSecurityClass, RelayRouteError, RelaySource, RouteTarget, message_security_class,
prepare_frame, validate_source_next_hop,
}; };
fn wire(target: RouteTarget) -> u64 { fn wire(target: RouteTarget) -> u64 {
@ -418,4 +423,37 @@ mod tests {
}; };
assert_eq!(routed.payload(), response.payload()); assert_eq!(routed.payload(), response.payload());
} }
#[test]
fn synchronized_setting_requests_use_authenticated_peer_control() {
for setting_type in [
CommunicationType::SyncedSettingSet,
CommunicationType::SyncedSettingGet,
CommunicationType::SyncedSettingDelete,
CommunicationType::SyncedSettingsList,
] {
let frame = CommunicationValue::new(setting_type);
assert_eq!(
message_security_class(&frame),
MessageSecurityClass::AuthenticatedPeerControl
);
}
}
#[test]
fn synchronized_setting_requests_are_not_relay_only() {
for setting_type in [
CommunicationType::SyncedSettingSet,
CommunicationType::SyncedSettingGet,
CommunicationType::SyncedSettingDelete,
CommunicationType::SyncedSettingsList,
CommunicationType::SyncedSettingChanged,
] {
let frame = CommunicationValue::new(setting_type);
assert_ne!(
message_security_class(&frame),
MessageSecurityClass::RelayOnly
);
}
}
} }

View file

@ -18,6 +18,16 @@ pub struct RhoConnection {
app_connections: DashMap<(u64, String, Uuid), Arc<AppConnection>>, app_connections: DashMap<(u64, String, Uuid), Arc<AppConnection>>,
} }
fn message_targets_client(cv: &CommunicationValue, user_id: u64, session_id: u64) -> bool {
if cv.receiver() != Some(user_id) {
return false;
}
let Some(target_session_id) = cv.get_data(DataType::SessionId).as_number() else {
return true;
};
target_session_id == i128::from(session_id)
}
impl RhoConnection { impl RhoConnection {
/// Create a new RhoConnection /// Create a new RhoConnection
pub async fn new(iota_connection: Arc<IotaConnection>, user_ids: Vec<i64>) -> Self { pub async fn new(iota_connection: Arc<IotaConnection>, user_ids: Vec<i64>) -> Self {
@ -254,25 +264,19 @@ impl RhoConnection {
/// Send message from Iota to specific client /// Send message from Iota to specific client
pub async fn message_to_client(&self, cv: CommunicationValue) { pub async fn message_to_client(&self, cv: CommunicationValue) {
let connections = self.get_client_connections().await; let connections = self.get_client_connections().await;
let Some(receiver_id) = cv.receiver() else { if cv.receiver().is_none() {
log_err!( log_err!(
0, 0,
crate::util::logger::PrintType::General, crate::util::logger::PrintType::General,
"Discarded message without an MTP receiver" "Discarded message without an MTP receiver"
); );
return; return;
}; }
let session_id = cv.get_data(DataType::SessionId).as_number();
for connection in connections.iter() { for connection in connections.iter() {
if connection.user_id != receiver_id { if !message_targets_client(&cv, connection.user_id, connection.session_id) {
continue; continue;
} }
if let Some(session_id) = session_id {
if connection.session_id as i128 != session_id {
continue;
}
}
connection.clone().send_message(&cv).await; connection.clone().send_message(&cv).await;
} }
} }
@ -357,3 +361,20 @@ impl RhoConnection {
self.client_connections.len() self.client_connections.len()
} }
} }
#[cfg(test)]
mod tests {
use super::message_targets_client;
use mtp::codec::{CommunicationType, CommunicationValue, DataType};
#[test]
fn sessionless_setting_change_targets_all_devices_for_one_user() {
let event =
CommunicationValue::new(CommunicationType::SyncedSettingChanged).with_receiver(7);
assert!(message_targets_client(&event, 7, 1));
assert!(message_targets_client(&event, 7, 2));
assert!(!message_targets_client(&event, 8, 1));
assert!(event.get_data(DataType::SessionId).is_none());
}
}