Files
peerspeak/src/config.rs
T
molluskandClaude Opus 4.8 c8666d588b 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>
2026-05-29 16:10:38 -04:00

88 lines
2.7 KiB
Rust

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 {
fn default() -> Self {
Self {
input_device: "".to_string(),
output_device: "".to_string(),
noise_gate_threshold: 0.01,
network_mode: NetworkMode::default(),
}
}
}
impl AppConfig {
fn config_path() -> Option<PathBuf> {
dirs::config_dir().map(|mut p| {
p.push("peerspeak");
p.push("config.json");
p
})
}
pub fn load() -> Self {
if let Some(path) = Self::config_path()
&& let Ok(contents) = fs::read_to_string(&path)
&& let Ok(config) = serde_json::from_str(&contents) {
return config;
}
Self::default()
}
pub fn save(&self) {
if let Some(path) = Self::config_path() {
if let Some(dir) = path.parent() {
let _ = fs::create_dir_all(dir);
}
if let Ok(json) = serde_json::to_string_pretty(self) {
let _ = fs::write(path, json);
}
}
}
}