- A network port kept a machine it had connected to after the connection was removed, listed it as on the network whenever its host advertised any session, and could not connect to it again: Connect resolved identifiers only among advertised sessions and failed with "no peer named". A machine remembered only for a connection is now dropped once no network port uses it, an untrusted machine is marked present only by a session at its exact address, and Connect resolves remembered machines by identifier or name. - A network port invited the one address it had connected to until it answered, so a session that came back on another port was never reached. The session name advertised at a machine's address is now stored as advertised_as in the configuration, and while the link is down the port connects where that session is advertised and stores the new address. A machine carrying MIDI is never moved. - Each daemon now holds an Ed25519 key in identity.key beside the configuration and publishes mhkey and mhport in every session's TXT record. Two packets on the control port, a challenge and a signed proof, let a network port prove which port of which daemon it is. A machine stored with a proved key and port_id is followed under any name and to another host, with its trust, and a port deleted and made again is not followed. The challenge is sent only to a session that advertises a key, so no other RTP-MIDI implementation receives it. - A machine with no key is followed by name to a new port on its host, and to another host only when it is not trusted, since an advertisement proves nothing and trust is held by host. - An invitation over an IPv6 link-local address was never answered, because the sender's address was kept without its scope. Apple's Network MIDI invites that way and reported that the port did not respond. The address is now kept whole for the control and data ports. - Adds ed25519-dalek and hex. The packet fuzz target reads the new packets.
317 lines
13 KiB
Rust
317 lines
13 KiB
Rust
//! 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<String>,
|
|
/// The UDP port it listens on; zero lets the system choose.
|
|
pub control_port: Option<u16>,
|
|
/// How it treats invitations.
|
|
pub policy: Option<InvitationPolicy>,
|
|
/// Whether other applications see it as a MIDI port of its name.
|
|
pub automatic_port: Option<bool>,
|
|
}
|
|
|
|
/// 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<Self>,
|
|
id: EndpointId,
|
|
change: NetworkPortChange,
|
|
) -> Result<Endpoint, DaemonError> {
|
|
// 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(),
|
|
id,
|
|
)
|
|
{
|
|
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<Self>, 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<Self>,
|
|
id: EndpointId,
|
|
) -> Result<Vec<String>, 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<String> = 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<Self>,
|
|
id: EndpointId,
|
|
on: bool,
|
|
) -> Result<Endpoint, DaemonError> {
|
|
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<Self>,
|
|
id: EndpointId,
|
|
policy: InvitationPolicy,
|
|
) -> Result<Endpoint, DaemonError> {
|
|
self.update_network_port(
|
|
id,
|
|
NetworkPortChange {
|
|
policy: Some(policy),
|
|
..NetworkPortChange::default()
|
|
},
|
|
)
|
|
.await
|
|
}
|
|
}
|