//! Changing a network port after it is made: the name other machines see, its UDP port, who may //! join, and its automatic port (FR-015, R-078). use crate::state::{Change, Daemon, DaemonError}; use midi_harbor_core::config; use midi_harbor_core::endpoint::{ Endpoint, EndpointKind, EndpointName, InvitationPolicy, NetworkSession, }; use midi_harbor_core::failure::FailureReason; use midi_harbor_core::ids::EndpointId; use std::sync::Arc; use tracing::{info, warn}; /// What to change about a network port. A field left `None` is left as it is. #[derive(Clone, Debug, Default)] pub struct NetworkPortChange { /// The name other machines see it by, its Bonjour name. pub local_name: Option, /// The UDP port it listens on; zero lets the system choose. pub control_port: Option, /// How it treats invitations. pub policy: Option, /// Whether other applications see it as a MIDI port of its name. pub automatic_port: Option, } /// Refuses a UDP port a network port cannot listen on. Zero lets the system choose. /// /// The data port is the next one up, and binding moves an odd control port up one because some /// implementations refuse an odd data port, so only an even port is taken as asked. pub(crate) fn check_control_port(port: u16) -> Result<(), DaemonError> { if !port.is_multiple_of(2) { return Err(FailureReason::ConfigInvalid { detail: format!( "UDP port {port} is odd, and RTP-MIDI needs an even one with the data port above \ it; choose {} or {}", port.saturating_sub(1), port.saturating_add(1) ), } .into()); } Ok(()) } impl Daemon { /// Changes a network port's settings and applies them to it while it runs. /// /// A new name reaches the machines that connect from then on, and those already connected /// stay connected. A new UDP port means listening again, so the network port restarts: a /// machine it connected to is connected to again, and a machine that connected to it has to /// be told the new port. pub async fn update_network_port( self: &Arc, id: EndpointId, change: NetworkPortChange, ) -> Result { // Validate the change. let local_name = change .local_name .as_deref() .map(EndpointName::new) .transpose()?; if let Some(port) = change.control_port { check_control_port(port)?; // A running port moved to a pair that is taken is refused before it is touched. One // another network port has is left to the check below, which names it. let running = self.sessions.read().await.get(&id).map(Arc::clone); let configured = self .read(|config, _| { config.endpoints.iter().any(|other| { other.id != id && matches!(&other.kind, EndpointKind::NetworkSession(session) if session.control_port == port) }) }) .await; if let Some(session) = running && port != 0 && port != session.control_port() && !configured && !crate::net::SessionSockets::pair_free(port).await { return Err(FailureReason::ConfigInvalid { detail: format!("UDP port {port} or the one above it is in use"), } .into()); } } // Record it. let (before, updated) = { let mut inner = self.inner.write().await; for other in inner.config.endpoints.iter().filter(|e| e.id != id) { let EndpointKind::NetworkSession(session) = &other.kind else { continue; }; // Two network ports advertising one name look like one machine to every peer. if local_name.as_ref() == Some(&session.local_name) { return Err(FailureReason::NameConflict { name: session.local_name.as_str().to_owned(), } .into()); } if let Some(port) = change.control_port && port != 0 && port == session.control_port { return Err(FailureReason::ConfigInvalid { detail: format!("UDP port {port} is already used by '{}'", other.name), } .into()); } } let Some(endpoint) = inner.config.endpoints.iter_mut().find(|e| e.id == id) else { return Err(DaemonError::NotFound(id.to_string())); }; let EndpointKind::NetworkSession(held) = &mut endpoint.kind else { return Err(FailureReason::ConfigInvalid { detail: format!("{} is not a network port", endpoint.name), } .into()); }; let before = held.clone(); if let Some(name) = local_name { held.local_name = name; } if let Some(port) = change.control_port { held.control_port = port; } if let Some(policy) = change.policy { held.invitation_policy = policy; } if let Some(on) = change.automatic_port { held.automatic_port = on; } let updated = endpoint.clone(); config::save(&self.paths, &inner.config)?; (before, updated) }; let EndpointKind::NetworkSession(after) = &updated.kind else { return Ok(updated); }; // Apply it to the running session. let running = self.sessions.read().await.get(&id).map(Arc::clone); if let Some(session) = running { if after.control_port != before.control_port { info!(endpoint = %updated.name, port = after.control_port, "network port moving to a new UDP port"); self.stop_session(id, before.local_name.as_str()).await; // Binding moves to a nearby pair when the one asked for is taken, which keeps a // network port up after a restart but here would put it somewhere the user did // not choose. let started = self.start_session(&updated).await; let listening = self .session_status(id) .await .map(|status| status.control_port); let refused = match started { Err(error) => Some(error), Ok(()) if after.control_port != 0 && listening != Some(after.control_port) => { Some( FailureReason::ConfigInvalid { detail: format!( "UDP port {} or the one above it is in use", after.control_port ), } .into(), ) } Ok(()) => None, }; if let Some(error) = refused { // Put it back as it was, so a port that could not be bound leaves the network // port neither down, nor somewhere else, nor half changed. warn!(endpoint = %updated.name, error = %error, "could not listen on the new UDP port; keeping the old settings"); self.stop_session(id, after.local_name.as_str()).await; self.restore(id, before).await; return Err(error); } } else if after.local_name != before.local_name { let _ = session.rename(after.local_name.as_str().to_owned()).await; if let Some(discovery) = &self.discovery { discovery.withdraw(before.local_name.as_str()); let announce = self .read(|config, _| config.preferences.advertise_sessions) .await; if announce && let Err(error) = discovery.advertise(after.local_name.as_str(), session.control_port()) { warn!(endpoint = %updated.name, error = %error, "could not advertise the new name"); } } } } if change.policy.is_some() { self.push_invitation_policy().await; } if change.automatic_port.is_some() { self.settle_automatic_port(&updated, false).await; } let _ = self.changes.send(Change::EndpointChanged(id)); Ok(updated) } /// Puts a network port's settings back as they were, and starts it with them. async fn restore(self: &Arc, id: EndpointId, settings: NetworkSession) { let restored = { let mut inner = self.inner.write().await; let Some(endpoint) = inner.config.endpoints.iter_mut().find(|e| e.id == id) else { return; }; if let EndpointKind::NetworkSession(held) = &mut endpoint.kind { *held = settings; } let restored = endpoint.clone(); if let Err(error) = config::save(&self.paths, &inner.config) { warn!(error = %error, "could not persist a network port's restored settings"); } restored }; if let Err(error) = self.start_session(&restored).await { warn!(endpoint = %restored.name, error = %error, "could not listen on the old UDP port either"); } } /// Deletes a network port, reporting the routes it leaves broken. /// /// Everything it was carrying is stopped first: the notes it played here, and those it sent /// its machines, which have no way to stop them once it has gone. Its routes stay, broken, /// and mend if a network port of its name returns. pub async fn delete_network_port( self: &Arc, id: EndpointId, ) -> Result, DaemonError> { let local_name = match self.read(|config, _| config.endpoint(id).cloned()).await { Some(Endpoint { kind: EndpointKind::NetworkSession(session), .. }) => session.local_name, Some(endpoint) => { return Err(FailureReason::ConfigInvalid { detail: format!("{} is not a network port", endpoint.name), } .into()); } None => return Err(DaemonError::NotFound(id.to_string())), }; // Stop it while it can still be heard. self.silence_routes_from(id).await; self.stop_session(id, local_name.as_str()).await; // An invitation waiting on it could only be answered for a network port that is gone. self.invitations .write() .await .retain(|_, invitation| invitation.session != id); // Forget it. let (name, orphaned) = { let mut inner = self.inner.write().await; let Some(index) = inner.config.endpoints.iter().position(|e| e.id == id) else { return Err(DaemonError::NotFound(id.to_string())); }; let endpoint = inner.config.endpoints.remove(index); let orphaned: Vec = inner .config .routes .iter() .filter(|route| route.touches(&endpoint)) .map(|route| format!("{} -> {}", route.from, route.to)) .collect(); config::save(&self.paths, &inner.config)?; (endpoint.name.to_string(), orphaned) }; let _ = self.changes.send(Change::EndpointRemoved(id)); info!(endpoint = %name, "network port deleted"); Ok(orphaned) } /// Switches a network port's automatic port on or off, and keeps the choice. pub async fn set_automatic_port( self: &Arc, id: EndpointId, on: bool, ) -> Result { self.update_network_port( id, NetworkPortChange { automatic_port: Some(on), ..NetworkPortChange::default() }, ) .await } /// Changes how a network port treats invitations. pub async fn set_invitation_policy( self: &Arc, id: EndpointId, policy: InvitationPolicy, ) -> Result { self.update_network_port( id, NetworkPortChange { policy: Some(policy), ..NetworkPortChange::default() }, ) .await } }