feat: personalized config generation with --listen addr + own fingerprint
Some checks failed
Mirror to GitHub / mirror (push) Failing after 39s
Build Release Binaries / build-amd64 (push) Failing after 3m16s

When --config points to a non-existent file, the relay now generates
a personalized example config that includes:
- listen_addr matching the --listen flag (not hardcoded 0.0.0.0:4433)
- Pre-filled [[peers]] section with this relay's detected IP, port,
  and TLS fingerprint — ready to copy/paste into other relay configs

This makes setting up federation much easier: start each relay, it
generates its config with its own peering info commented out, you
just uncomment and copy between configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-08 09:38:28 +04:00
parent 7064f484af
commit 406461d460
2 changed files with 72 additions and 19 deletions

View File

@@ -123,8 +123,15 @@ pub fn load_config(path: &str) -> Result<RelayConfig, anyhow::Error> {
Ok(config) Ok(config)
} }
/// Load config from path, or create an example config file if it doesn't exist. /// Info about this relay instance, used to generate personalized example configs.
pub fn load_or_create_config(path: &str) -> Result<RelayConfig, anyhow::Error> { pub struct RelayInfo {
pub listen_addr: String,
pub tls_fingerprint: String,
pub public_ip: Option<String>,
}
/// Load config from path, or create a personalized example config if it doesn't exist.
pub fn load_or_create_config(path: &str, info: Option<&RelayInfo>) -> Result<RelayConfig, anyhow::Error> {
let p = std::path::Path::new(path); let p = std::path::Path::new(path);
if p.exists() { if p.exists() {
return load_config(path); return load_config(path);
@@ -133,20 +140,38 @@ pub fn load_or_create_config(path: &str) -> Result<RelayConfig, anyhow::Error> {
if let Some(parent) = p.parent() { if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
// Write example config // Generate personalized example config
let example = EXAMPLE_CONFIG; let example = generate_example_config(info);
std::fs::write(p, example)?; std::fs::write(p, &example)?;
eprintln!("Created example config at {path} — edit it and restart."); eprintln!("Created example config at {path} — edit it and restart.");
let config: RelayConfig = toml::from_str(example)?; let config: RelayConfig = toml::from_str(&example)?;
Ok(config) Ok(config)
} }
/// Example TOML configuration written when --config points to a non-existent file. /// Generate an example TOML config, personalized with this relay's info if available.
pub const EXAMPLE_CONFIG: &str = r#"# WarzonePhone Relay Configuration fn generate_example_config(info: Option<&RelayInfo>) -> String {
let listen = info.map(|i| i.listen_addr.as_str()).unwrap_or("0.0.0.0:4433");
let peer_example = if let Some(i) = info {
let ip = i.public_ip.as_deref().unwrap_or("this-relay-ip");
format!(
r#"# Other relays can peer with this relay using:
# [[peers]]
# url = "{ip}:{port}"
# fingerprint = "{fp}"
# label = "This Relay""#,
port = listen.rsplit(':').next().unwrap_or("4433"),
fp = i.tls_fingerprint,
)
} else {
"# To peer with another relay, add its url + fingerprint:".to_string()
};
format!(
r#"# WarzonePhone Relay Configuration
# See docs/ADMINISTRATION.md for full reference. # See docs/ADMINISTRATION.md for full reference.
# Listen address for client connections # Listen address for client connections
listen_addr = "0.0.0.0:4433" listen_addr = "{listen}"
# Maximum concurrent sessions # Maximum concurrent sessions
# max_sessions = 100 # max_sessions = 100
@@ -157,9 +182,11 @@ listen_addr = "0.0.0.0:4433"
# featherChat auth endpoint (uncomment to enable) # featherChat auth endpoint (uncomment to enable)
# auth_url = "https://chat.example.com/v1/auth/validate" # auth_url = "https://chat.example.com/v1/auth/validate"
{peer_example}
# Federation: peer relays we connect to (outbound) # Federation: peer relays we connect to (outbound)
# [[peers]] # [[peers]]
# url = "relay-b.example.com:4433" # url = "other-relay.example.com:4433"
# fingerprint = "aa:bb:cc:dd:..." # fingerprint = "aa:bb:cc:dd:..."
# label = "Relay B" # label = "Relay B"
@@ -174,4 +201,6 @@ listen_addr = "0.0.0.0:4433"
# Debug: log packet headers for a room ("*" for all) # Debug: log packet headers for a room ("*" for all)
# debug_tap = "*" # debug_tap = "*"
"#; "#
)
}

View File

@@ -27,6 +27,8 @@ use wzp_relay::session_mgr::SessionManager;
struct CliResult { struct CliResult {
config: RelayConfig, config: RelayConfig,
identity_path: Option<String>, identity_path: Option<String>,
config_file: Option<String>,
config_needs_create: bool,
} }
fn parse_args() -> CliResult { fn parse_args() -> CliResult {
@@ -45,12 +47,20 @@ fn parse_args() -> CliResult {
i += 1; i += 1;
} }
// Track if we need to create the config after identity is known
let config_needs_create = config_file.as_ref().map(|p| !std::path::Path::new(p).exists()).unwrap_or(false);
let mut config = if let Some(ref path) = config_file { let mut config = if let Some(ref path) = config_file {
wzp_relay::config::load_or_create_config(path) if config_needs_create {
// Will be re-created with personalized info after identity is loaded
RelayConfig::default()
} else {
wzp_relay::config::load_config(path)
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
eprintln!("failed to load config from {path}: {e}"); eprintln!("failed to load config from {path}: {e}");
std::process::exit(1); std::process::exit(1);
}) })
}
} else { } else {
RelayConfig::default() RelayConfig::default()
}; };
@@ -164,7 +174,7 @@ fn parse_args() -> CliResult {
} }
i += 1; i += 1;
} }
CliResult { config, identity_path } CliResult { config, identity_path, config_file, config_needs_create }
} }
struct RelayStats { struct RelayStats {
@@ -249,7 +259,7 @@ fn detect_public_ip() -> Option<String> {
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let CliResult { config, identity_path } = parse_args(); let CliResult { mut config, identity_path, config_file, config_needs_create } = parse_args();
tracing_subscriber::fmt().init(); tracing_subscriber::fmt().init();
rustls::crypto::ring::default_provider() rustls::crypto::ring::default_provider()
.install_default() .install_default()
@@ -315,9 +325,23 @@ async fn main() -> anyhow::Result<()> {
let tls_fp = wzp_transport::tls_fingerprint(&cert_der); let tls_fp = wzp_transport::tls_fingerprint(&cert_der);
info!(tls_fingerprint = %tls_fp, "TLS certificate (deterministic from relay identity)"); info!(tls_fingerprint = %tls_fp, "TLS certificate (deterministic from relay identity)");
// Create personalized config file if it was missing
let public_ip = detect_public_ip();
if config_needs_create {
if let Some(ref path) = config_file {
let info = wzp_relay::config::RelayInfo {
listen_addr: config.listen_addr.to_string(),
tls_fingerprint: tls_fp.clone(),
public_ip: public_ip.clone(),
};
if let Err(e) = wzp_relay::config::load_or_create_config(path, Some(&info)) {
warn!("failed to create config: {e}");
}
}
}
// Print federation hint with our public IP + listen port + TLS fingerprint // Print federation hint with our public IP + listen port + TLS fingerprint
let listen_port = config.listen_addr.port(); let listen_port = config.listen_addr.port();
let public_ip = detect_public_ip();
if let Some(ip) = &public_ip { if let Some(ip) = &public_ip {
info!("federation: to peer with this relay, add to relay.toml:"); info!("federation: to peer with this relay, add to relay.toml:");
info!(" [[peers]]"); info!(" [[peers]]");