feat: configurable relay/discovery posture, default to no DNS beacon
The endpoint previously used presets::N0, which both relays through n0's servers and publishes a signed EndpointId->addresses record to n0's public DNS every time you go online. Since PeerSpeak already exchanges full peer addresses via the join ticket and gossip, that DNS presence beacon is redundant here. Adds a NetworkMode config option (persisted, switchable from Settings): - RelayNoDiscovery (new default): presets::Minimal + RelayMode::Default + the in-memory address lookup. Keeps n0 relay for NAT traversal but drops the DNS publish/resolve, so n0 only ever sees relayed-call metadata, never a standing online beacon. - N0Full: previous behavior (relay + DNS) for maximum reliability. - DirectOnly: RelayMode::Disabled, fully serverless. The mode is applied when the endpoint is built on room join. Existing config.json files load unchanged via #[serde(default)]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+29
-1
@@ -1,7 +1,7 @@
|
||||
use crate::core::{CoreController, messages::{CoreCommand, UiEvent}};
|
||||
use crate::network::PeerState;
|
||||
use crate::audio::pw_cli::{AudioDevice, enumerate_audio_devices};
|
||||
use crate::config::AppConfig;
|
||||
use crate::config::{AppConfig, NetworkMode};
|
||||
|
||||
use iced::widget::{
|
||||
container, column, row, text, button, text_input, scrollable, slider, checkbox, pick_list, Column,
|
||||
@@ -40,6 +40,7 @@ pub enum AppMessage {
|
||||
InputDeviceSelected(AudioDevice),
|
||||
OutputDeviceSelected(AudioDevice),
|
||||
NoiseGateChanged(f32),
|
||||
NetworkModeSelected(NetworkMode),
|
||||
EventOccurred(Event),
|
||||
NavigateToSettings,
|
||||
NavigateBack,
|
||||
@@ -91,6 +92,7 @@ impl Default for AppState {
|
||||
|
||||
let config = AppConfig::load();
|
||||
let _ = controller.send(CoreCommand::SetNoiseGateThreshold(config.noise_gate_threshold));
|
||||
let _ = controller.send(CoreCommand::SetNetworkMode(config.network_mode));
|
||||
let all_devices = enumerate_audio_devices();
|
||||
let input_devices: Vec<_> = all_devices.iter().filter(|d| d.is_input).cloned().collect();
|
||||
let output_devices: Vec<_> = all_devices.iter().filter(|d| !d.is_input).cloned().collect();
|
||||
@@ -258,6 +260,12 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
state.config.save();
|
||||
let _ = state.controller.send(CoreCommand::SetNoiseGateThreshold(val));
|
||||
}
|
||||
AppMessage::NetworkModeSelected(mode) => {
|
||||
state.config.network_mode = mode;
|
||||
state.config.save();
|
||||
// Applied on the next join, since the endpoint is rebuilt then.
|
||||
let _ = state.controller.send(CoreCommand::SetNetworkMode(mode));
|
||||
}
|
||||
AppMessage::EventOccurred(Event::Keyboard(keyboard::Event::KeyPressed { key, .. })) => {
|
||||
if state.is_setting_hotkey {
|
||||
state.ptt_hotkey = key.clone();
|
||||
@@ -288,6 +296,15 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// One-line explanation of a network posture for the settings picker.
|
||||
fn network_mode_hint(mode: NetworkMode) -> &'static str {
|
||||
match mode {
|
||||
NetworkMode::RelayNoDiscovery => "n0 relay for NAT traversal; no presence published to n0 DNS.",
|
||||
NetworkMode::N0Full => "n0 relay + DNS discovery. Most reliable, most metadata shared.",
|
||||
NetworkMode::DirectOnly => "Fully serverless. May fail behind strict/CGNAT networks.",
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_space() -> iced::widget::Space {
|
||||
iced::widget::Space::new().width(iced::Length::Fill)
|
||||
}
|
||||
@@ -396,6 +413,17 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
text(format!("Mic Sensitivity (Noise Gate): {:.1}%", state.config.noise_gate_threshold * 100.0)).size(14).color(color_subtext),
|
||||
slider(0.0..=0.1, state.config.noise_gate_threshold, AppMessage::NoiseGateChanged).step(0.001)
|
||||
].spacing(10).width(iced::Length::Fixed(320.0)),
|
||||
vertical_space(20.0),
|
||||
column![
|
||||
text("Network Privacy").size(14).color(color_subtext),
|
||||
pick_list(
|
||||
&NetworkMode::ALL[..],
|
||||
Some(state.config.network_mode),
|
||||
AppMessage::NetworkModeSelected,
|
||||
).width(iced::Length::Fixed(320.0)),
|
||||
text(network_mode_hint(state.config.network_mode)).size(11).color(color_subtext),
|
||||
text("Takes effect on your next room join.").size(11).color(color_surface),
|
||||
].spacing(6).width(iced::Length::Fixed(320.0)),
|
||||
vertical_space(30.0),
|
||||
button(
|
||||
text("Back")
|
||||
|
||||
@@ -2,11 +2,47 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Relay/discovery posture, trading connectivity against how much the n0
|
||||
/// infrastructure learns about you. See the network module for details.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum NetworkMode {
|
||||
/// n0 relay for NAT traversal, but no DNS presence beacon. Peer addresses
|
||||
/// come from the join ticket and gossip, so n0 only sees relayed-call
|
||||
/// metadata, never a standing "I'm online" record. Default.
|
||||
#[default]
|
||||
RelayNoDiscovery,
|
||||
/// Full n0 defaults: relay plus DNS publish/resolve (most convenient,
|
||||
/// most phone-home).
|
||||
N0Full,
|
||||
/// No relay, no discovery: direct hole-punching only. Fully serverless,
|
||||
/// but fails behind symmetric/CGNAT NATs with no fallback.
|
||||
DirectOnly,
|
||||
}
|
||||
|
||||
impl NetworkMode {
|
||||
/// All variants, for presentation in a picker.
|
||||
pub const ALL: [NetworkMode; 3] =
|
||||
[NetworkMode::RelayNoDiscovery, NetworkMode::N0Full, NetworkMode::DirectOnly];
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NetworkMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let label = match self {
|
||||
NetworkMode::RelayNoDiscovery => "Relay, no presence beacon",
|
||||
NetworkMode::N0Full => "n0 defaults (relay + DNS)",
|
||||
NetworkMode::DirectOnly => "Direct only (no relay)",
|
||||
};
|
||||
f.write_str(label)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub input_device: String,
|
||||
pub output_device: String,
|
||||
pub noise_gate_threshold: f32,
|
||||
#[serde(default)]
|
||||
pub network_mode: NetworkMode,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
@@ -15,6 +51,7 @@ impl Default for AppConfig {
|
||||
input_device: "".to_string(),
|
||||
output_device: "".to_string(),
|
||||
noise_gate_threshold: 0.01,
|
||||
network_mode: NetworkMode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::config::NetworkMode;
|
||||
use crate::network::PeerState;
|
||||
use iroh::EndpointId;
|
||||
|
||||
@@ -11,6 +12,9 @@ pub enum CoreCommand {
|
||||
SetPttActive(bool),
|
||||
SetPeerVolume(EndpointId, f32),
|
||||
SetNoiseGateThreshold(f32),
|
||||
/// Set the relay/discovery posture. Takes effect on the next room join,
|
||||
/// since the endpoint is (re)built then.
|
||||
SetNetworkMode(NetworkMode),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
+44
-12
@@ -11,7 +11,8 @@ use crate::network::{
|
||||
};
|
||||
use crate::core::messages::{CoreCommand, UiEvent};
|
||||
|
||||
use iroh::{Endpoint, EndpointId, endpoint::presets, protocol::Router};
|
||||
use crate::config::NetworkMode;
|
||||
use iroh::{Endpoint, EndpointId, RelayMode, endpoint::presets, protocol::Router};
|
||||
use iroh_gossip::net::Gossip;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use std::collections::HashMap;
|
||||
@@ -100,6 +101,7 @@ async fn run_core_loop(
|
||||
let noise_gate_threshold = Arc::new(std::sync::atomic::AtomicU32::new(0.01f32.to_bits()));
|
||||
let peer_volumes = Arc::new(Mutex::new(HashMap::<EndpointId, f32>::new()));
|
||||
let mut current_name = "Anonymous".to_string();
|
||||
let mut network_mode = NetworkMode::default();
|
||||
|
||||
let mut active_session: Option<ActiveSession> = None;
|
||||
|
||||
@@ -114,17 +116,43 @@ async fn run_core_loop(
|
||||
session.shutdown(audio_backend.clone()).await;
|
||||
}
|
||||
|
||||
let endpoint = match Endpoint::builder(presets::N0)
|
||||
.secret_key(secret_key.clone())
|
||||
.address_lookup(memory_lookup.clone())
|
||||
.bind()
|
||||
.await {
|
||||
Ok(ep) => ep,
|
||||
Err(e) => {
|
||||
let _ = ui_tx.send(UiEvent::Error(format!("Failed to bind endpoint: {}", e))).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// Build the endpoint per the configured relay/discovery posture.
|
||||
// All postures keep the in-memory address lookup (fed by tickets
|
||||
// and gossip); they differ in whether n0's relay and DNS presence
|
||||
// beacon are used. `Minimal` sets only the mandatory crypto
|
||||
// provider and deliberately omits the n0 DNS publish/resolve.
|
||||
let bind_result = match network_mode {
|
||||
NetworkMode::N0Full => {
|
||||
Endpoint::builder(presets::N0)
|
||||
.secret_key(secret_key.clone())
|
||||
.address_lookup(memory_lookup.clone())
|
||||
.bind()
|
||||
.await
|
||||
}
|
||||
NetworkMode::RelayNoDiscovery => {
|
||||
Endpoint::builder(presets::Minimal)
|
||||
.secret_key(secret_key.clone())
|
||||
.relay_mode(RelayMode::Default)
|
||||
.address_lookup(memory_lookup.clone())
|
||||
.bind()
|
||||
.await
|
||||
}
|
||||
NetworkMode::DirectOnly => {
|
||||
Endpoint::builder(presets::Minimal)
|
||||
.secret_key(secret_key.clone())
|
||||
.relay_mode(RelayMode::Disabled)
|
||||
.address_lookup(memory_lookup.clone())
|
||||
.bind()
|
||||
.await
|
||||
}
|
||||
};
|
||||
let endpoint = match bind_result {
|
||||
Ok(ep) => ep,
|
||||
Err(e) => {
|
||||
let _ = ui_tx.send(UiEvent::Error(format!("Failed to bind endpoint: {}", e))).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
endpoint.online().await;
|
||||
|
||||
// Determine target ticket
|
||||
@@ -443,6 +471,10 @@ async fn run_core_loop(
|
||||
CoreCommand::SetNoiseGateThreshold(threshold) => {
|
||||
noise_gate_threshold.store(threshold.to_bits(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
CoreCommand::SetNetworkMode(mode) => {
|
||||
network_mode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user