[Fix] Connectivity

This commit is contained in:
Alex Emmet 2026-08-29 12:48:46 +02:00
commit 82a5d9469b
No known key found for this signature in database
11 changed files with 131 additions and 148 deletions

1
Cargo.lock generated
View file

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

View file

@ -9,6 +9,7 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
"client",
"crypto",
"files",
"raw",
] }
mtp-transport = { git = "https://git.methanium.net/Methanium/mtp.git" }
@ -33,12 +34,3 @@ livekit-protocol = "=0.7.10"
thiserror = "2.0.19"
trust-dns-resolver = "0.23.2"
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
`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:
@ -19,11 +19,3 @@ Rho connection budgets can be configured with:
semaphore and to the MTP WebServer admission semaphore. This means the same
budget limits transport handshakes and authenticated Rho sessions instead of
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::files::{FileError, load_keyring, save_keyring, save_public_key_bundle};
use zeroize::Zeroizing;
use mtp::files::{FileError, load_keyring_raw, save_keyring_raw, save_public_key_bundle};
pub const KEYRING_PATH: &str = "./omikron.mk";
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(
passphrase: &[u8],
keyring_path: impl AsRef<Path>,
public_key_path: impl AsRef<Path>,
) -> Result<Keyring, String> {
let keyring_path = keyring_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),
Err(FileError::Io(error)) if error.kind() == ErrorKind::NotFound => {
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}")
})?;
save_public_key_bundle(&keyring.public_key_bundle(), public_key_path).map_err(
@ -41,10 +27,7 @@ pub fn load_or_create_keyring(
)
},
)?;
eprintln!(
"Generated new protected keyring at {}",
keyring_path.display()
);
eprintln!("Generated new keyring at {}", keyring_path.display());
Ok(keyring)
}
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)]
mod tests {
use super::*;
use std::{
fs,
env, fs,
time::{SystemTime, UNIX_EPOCH},
};
@ -111,11 +76,15 @@ mod tests {
#[test]
fn missing_identity_is_created_and_can_be_loaded_again() {
let paths = TestPaths::new();
let passphrase = b"test identity secret";
let first = load_or_create_keyring(passphrase, &paths.keyring, &paths.public_key).unwrap();
let first = load_or_create_keyring(&paths.keyring, &paths.public_key).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!(
second.public_key_bundle().try_as_bytes().unwrap(),
first_public
@ -127,9 +96,7 @@ mod tests {
let paths = TestPaths::new();
fs::write(&paths.keyring, b"not a keyring").unwrap();
let error =
load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key)
.unwrap_err();
let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err();
assert!(error.contains("unable to load existing Omikron identity"));
}
@ -138,9 +105,7 @@ mod tests {
let paths = TestPaths::new();
fs::create_dir(&paths.keyring).unwrap();
let error =
load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key)
.unwrap_err();
let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err();
assert!(error.contains("unable to load existing Omikron identity"));
}
@ -149,33 +114,7 @@ mod tests {
let paths = TestPaths::new();
fs::create_dir(&paths.public_key).unwrap();
let error =
load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key)
.unwrap_err();
let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err();
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,
calls::{call_manager::CallManager, call_util::LiveKitService},
config::Config,
identity::{
KEYRING_PATH, PUBLIC_KEY_PATH, identity_secret_from_environment, load_or_create_keyring,
},
identity::{KEYRING_PATH, PUBLIC_KEY_PATH, load_or_create_keyring},
omega::omega_connection::{OmegaConnection, start_task_cleanup_loop},
rho::rho_manager::RhoManager,
rho::server::start,
@ -50,18 +48,7 @@ async fn main() {
}
};
let identity_secret = match identity_secret_from_environment() {
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,
) {
let keyring = match load_or_create_keyring(KEYRING_PATH, PUBLIC_KEY_PATH) {
Ok(keyring) => keyring,
Err(error) => {
eprintln!("Unable to load Omikron keyring: {error}");

View file

@ -17,6 +17,13 @@ use tokio::sync::RwLock;
use trust_dns_resolver::TokioAsyncResolver;
use uuid::Uuid;
fn authenticated_peer_control_request(
cv: &CommunicationValue,
user_id: u64,
) -> CommunicationValue {
cv.clone().with_sender(user_id)
}
pub struct ClientConnection {
pub state: Arc<AppState>,
pub user_id: u64,
@ -210,7 +217,10 @@ impl ClientConnection {
.await;
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,
Err(_) => {
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>
impl Clone for ClientConnection {
fn clone(&self) -> Self {

View file

@ -302,6 +302,13 @@ impl IotaConnection {
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;
if let Some((_, task)) = self.waiting_tasks.remove(&msg_id) {
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) {
self.add_call_state(cv).await
} else {

View file

@ -95,6 +95,10 @@ pub fn message_security_class(frame: &CommunicationValue) -> MessageSecurityClas
} else if frame.is_type(CommunicationType::GetChatSecret)
|| frame.is_type(CommunicationType::MessageGet)
|| 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
} else {
@ -296,7 +300,8 @@ mod tests {
use mtp::codec::{CommunicationType, CommunicationValue, DataValue};
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 {
@ -418,4 +423,37 @@ mod tests {
};
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>>,
}
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 {
/// Create a new RhoConnection
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
pub async fn message_to_client(&self, cv: CommunicationValue) {
let connections = self.get_client_connections().await;
let Some(receiver_id) = cv.receiver() else {
if cv.receiver().is_none() {
log_err!(
0,
crate::util::logger::PrintType::General,
"Discarded message without an MTP receiver"
);
return;
};
let session_id = cv.get_data(DataType::SessionId).as_number();
}
for connection in connections.iter() {
if connection.user_id != receiver_id {
if !message_targets_client(&cv, connection.user_id, connection.session_id) {
continue;
}
if let Some(session_id) = session_id {
if connection.session_id as i128 != session_id {
continue;
}
}
connection.clone().send_message(&cv).await;
}
}
@ -357,3 +361,20 @@ impl RhoConnection {
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());
}
}