diff --git a/Cargo.lock b/Cargo.lock index 061ac6b6..c203b7f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1001,7 +1001,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -1203,7 +1203,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "ldk-node" version = "0.8.0+git" -source = "git+https://github.com/lightningdevkit/ldk-node?rev=c754e2fe85c70741b5e370334cd16856c615265e#c754e2fe85c70741b5e370334cd16856c615265e" +source = "git+https://github.com/lightningdevkit/ldk-node?rev=21eea8c881790db7a90bcad4f7f45f341c72222b#21eea8c881790db7a90bcad4f7f45f341c72222b" dependencies = [ "async-trait", "base64 0.22.1", @@ -1224,6 +1224,7 @@ dependencies = [ "lightning", "lightning-background-processor", "lightning-block-sync", + "lightning-dns-resolver", "lightning-invoice", "lightning-liquidity", "lightning-macros", @@ -1381,6 +1382,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "lightning-dns-resolver" +version = "0.3.0+git" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=38a62c32454d3eac22578144c479dbf9a6d9bff6#38a62c32454d3eac22578144c479dbf9a6d9bff6" +dependencies = [ + "dnssec-prover", + "lightning", + "lightning-types", + "tokio", +] + [[package]] name = "lightning-invoice" version = "0.35.0+git" @@ -1733,7 +1745,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.34", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror", "tokio", "tracing", @@ -1770,7 +1782,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] diff --git a/contrib/ldk-server-config.toml b/contrib/ldk-server-config.toml index 486baa8a..ce569cad 100644 --- a/contrib/ldk-server-config.toml +++ b/contrib/ldk-server-config.toml @@ -103,3 +103,16 @@ poll_metrics_interval = 60 # The polling interval for metrics in seconds. [tor] # Only connections to OnionV3 peers will be made via this proxy; other connections (IPv4 peers, Electrum server) will not be routed over Tor. #proxy_address = "127.0.0.1:9050" # Tor daemon SOCKS proxy address. + +# Human-Readable Names (BIP 353) resolution +[hrn] +# Resolution method: "dns" (resolve locally via a DNS server) or "blip32" (ask other +# nodes to resolve for us via bLIP-32). Defaults to "dns". +#mode = "dns" +# DNS server used when `mode = "dns"`. Defaults to 8.8.8.8:53 (Google Public DNS). The +# port defaults to 53 if omitted (e.g., "1.1.1.1" is treated as "1.1.1.1:53"). +#dns_server_address = "8.8.8.8:53" +# When set to true (and `mode = "dns"`), also offer HRN resolution to the rest of the +# network over Onion Messages. Requires the node to be announceable so resolution +# requests can be routed to us. Defaults to false. +#enable_resolution_service = false diff --git a/docs/configuration.md b/docs/configuration.md index 40f2927d..45f3020f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -123,6 +123,23 @@ are not proxied. This does not set up inbound connections, to make your node rea hidden service, you need to configure Tor separately. See the [Tor guide](tor.md) for the full setup. +### `[hrn]` + +Configures how the node resolves [BIP 353](https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki) +Human-Readable Names (e.g., `â‚¿alice@example.com`) to Lightning payment destinations. + +Two resolution methods are supported via the `mode` field: + +- **`"dns"`** (default) - Resolve names locally using a DNS server. The server is set via + `dns_server_address` (default: `8.8.8.8:53`, Google Public DNS). The port defaults to + `53` if omitted. When `enable_resolution_service = true`, the node additionally offers + HRN resolution to the rest of the network over Onion Messages. This requires the node + to be announceable so resolution requests can be routed to it, and is therefore + disabled by default. +- **`"blip32"`** - Ask other nodes to resolve names on our behalf via + [bLIP-32](https://github.com/lightning/blips/blob/master/blip-0032.md). `dns_server_address` + and `enable_resolution_service` only apply in `"dns"` mode and are rejected here. + ## Storage Layout ``` diff --git a/ldk-server/Cargo.toml b/ldk-server/Cargo.toml index bb1c10cd..58c1e1c2 100644 --- a/ldk-server/Cargo.toml +++ b/ldk-server/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "c754e2fe85c70741b5e370334cd16856c615265e" } +ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "21eea8c881790db7a90bcad4f7f45f341c72222b" } serde = { version = "1.0.203", default-features = false, features = ["derive"] } hyper = { version = "1", default-features = false, features = ["server", "http2"] } http-body-util = { version = "0.1", default-features = false } diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 30807541..3551a367 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -141,6 +141,7 @@ fn main() { ldk_node_config.listening_addresses = config_file.listening_addrs; ldk_node_config.announcement_addresses = config_file.announcement_addrs; ldk_node_config.network = config_file.network; + ldk_node_config.hrn_config = config_file.hrn_config; let mut builder = Builder::from_config(ldk_node_config); builder.set_log_facade_logger(); diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index 22e3b61b..24e31c81 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -15,6 +15,7 @@ use std::{fs, io}; use clap::Parser; use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::Network; +use ldk_node::config::{HRNResolverConfig, HumanReadableNamesConfig}; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::routing::gossip::NodeAlias; use ldk_node::liquidity::LSPS2ServiceConfig; @@ -61,6 +62,7 @@ pub struct Config { pub metrics_username: Option, pub metrics_password: Option, pub tor_config: Option, + pub hrn_config: HumanReadableNamesConfig, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -114,6 +116,7 @@ struct ConfigBuilder { metrics_username: Option, metrics_password: Option, tor_proxy_address: Option, + hrn: Option, } impl ConfigBuilder { @@ -180,6 +183,10 @@ impl ConfigBuilder { if let Some(tor) = toml.tor { self.tor_proxy_address = Some(tor.proxy_address) } + + if let Some(hrn) = toml.hrn { + self.hrn = Some(hrn); + } } fn merge_args(&mut self, args: &ArgsConfig) { @@ -402,6 +409,11 @@ impl ConfigBuilder { }) .transpose()?; + let hrn_config = match self.hrn { + Some(hrn) => HumanReadableNamesConfig::try_from(hrn)?, + None => HumanReadableNamesConfig::default(), + }; + Ok(Config { network, listening_addrs, @@ -422,6 +434,7 @@ impl ConfigBuilder { metrics_username, metrics_password, tor_config: tor_proxy_address.map(|proxy_address| TorConfig { proxy_address }), + hrn_config, }) } } @@ -439,6 +452,7 @@ pub struct TomlConfig { tls: Option, metrics: Option, tor: Option, + hrn: Option, } #[derive(Deserialize, Serialize)] @@ -505,6 +519,93 @@ struct TomlTorConfig { proxy_address: String, } +#[derive(Deserialize, Serialize)] +struct HrnTomlConfig { + mode: Option, + dns_server_address: Option, + enable_resolution_service: Option, +} + +impl TryFrom for HumanReadableNamesConfig { + type Error = io::Error; + + fn try_from(value: HrnTomlConfig) -> Result { + let HrnTomlConfig { mode, dns_server_address, enable_resolution_service } = value; + + let resolution_config = match mode.as_deref() { + None | Some("dns") => { + // Start from LDK Node's DNS defaults so we don't have to hardcode them, but fall + // back to explicit values if the upstream default ever stops being `Dns`. + let (mut dns_server_address_val, mut enable_hrn_resolution_service) = + if let HRNResolverConfig::Dns { + dns_server_address, + enable_hrn_resolution_service, + } = HumanReadableNamesConfig::default().resolution_config + { + (dns_server_address, enable_hrn_resolution_service) + } else { + ( + SocketAddress::from_str("8.8.8.8:53") + .expect("`8.8.8.8:53` is a valid socket address"), + false, + ) + }; + + if let Some(addr) = dns_server_address.as_deref() { + dns_server_address_val = parse_dns_server_address(addr)?; + } + if let Some(enable) = enable_resolution_service { + enable_hrn_resolution_service = enable; + } + + HRNResolverConfig::Dns { + dns_server_address: dns_server_address_val, + enable_hrn_resolution_service, + } + }, + Some("blip32") => { + if dns_server_address.is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "`hrn.dns_server_address` only applies when `hrn.mode = \"dns\"`" + .to_string(), + )); + } + if enable_resolution_service.is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "`hrn.enable_resolution_service` only applies when `hrn.mode = \"dns\"`" + .to_string(), + )); + } + HRNResolverConfig::Blip32 + }, + Some(other) => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid HRN mode '{}' configured; expected 'dns' or 'blip32'", other), + )) + }, + }; + + Ok(HumanReadableNamesConfig { resolution_config }) + } +} + +/// Parses a DNS server address, falling back to port 53 if the user omitted the port. +fn parse_dns_server_address(addr: &str) -> io::Result { + if let Ok(sa) = SocketAddress::from_str(addr) { + return Ok(sa); + } + let with_default_port = format!("{}:53", addr); + SocketAddress::from_str(&with_default_port).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid HRN DNS server address configured: {}", e), + ) + }) +} + #[derive(Deserialize, Serialize)] struct LiquidityConfig { lsps2_client: Option, @@ -936,6 +1037,7 @@ mod tests { tor_config: Some(TorConfig { proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(), }), + hrn_config: HumanReadableNamesConfig::default(), }; assert_eq!(config.listening_addrs, expected.listening_addrs); @@ -1241,6 +1343,7 @@ mod tests { metrics_username: None, metrics_password: None, tor_config: None, + hrn_config: HumanReadableNamesConfig::default(), }; assert_eq!(config.listening_addrs, expected.listening_addrs); @@ -1350,6 +1453,7 @@ mod tests { tor_config: Some(TorConfig { proxy_address: SocketAddress::from_str("127.0.0.1:9050").unwrap(), }), + hrn_config: HumanReadableNamesConfig::default(), }; assert_eq!(config.listening_addrs, expected.listening_addrs); @@ -1501,4 +1605,113 @@ mod tests { let err = result.unwrap_err(); assert_eq!(err.kind(), io::ErrorKind::InvalidInput); } + + #[test] + fn test_hrn_config() { + let storage_path = std::env::temp_dir(); + let config_file_name = "test_hrn_config.toml"; + + let base_config = r#" + [node] + network = "regtest" + + [bitcoind] + rpc_address = "127.0.0.1:8332" + rpc_user = "bitcoind-testuser" + rpc_password = "bitcoind-testpassword" + + [liquidity.lsps2_service] + advertise_service = false + channel_opening_fee_ppm = 1000 + channel_over_provisioning_ppm = 500000 + min_channel_opening_fee_msat = 10000000 + min_channel_lifetime = 4320 + max_client_to_self_delay = 1440 + min_payment_size_msat = 10000000 + max_payment_size_msat = 25000000000 + client_trusts_lsp = true + disable_client_reserve = false + "#; + + let mut args_config = empty_args_config(); + args_config.config_file = + Some(storage_path.join(config_file_name).to_string_lossy().to_string()); + + // Default: no `[hrn]` section -> DNS against 8.8.8.8:53, resolution service disabled. + fs::write(storage_path.join(config_file_name), base_config).unwrap(); + let config = load_config(&args_config).unwrap(); + match config.hrn_config.resolution_config { + HRNResolverConfig::Dns { dns_server_address, enable_hrn_resolution_service } => { + assert_eq!(dns_server_address, SocketAddress::from_str("8.8.8.8:53").unwrap()); + assert!(!enable_hrn_resolution_service); + }, + other => panic!("unexpected default HRN resolver config: {:?}", other), + } + + // Custom DNS server address with resolution service enabled. + let toml_config = format!( + "{}\n[hrn]\ndns_server_address = \"1.1.1.1:53\"\nenable_resolution_service = true\n", + base_config + ); + fs::write(storage_path.join(config_file_name), &toml_config).unwrap(); + let config = load_config(&args_config).unwrap(); + match config.hrn_config.resolution_config { + HRNResolverConfig::Dns { dns_server_address, enable_hrn_resolution_service } => { + assert_eq!(dns_server_address, SocketAddress::from_str("1.1.1.1:53").unwrap()); + assert!(enable_hrn_resolution_service); + }, + other => panic!("unexpected HRN resolver config: {:?}", other), + } + + // Blip32 mode. + let toml_config = format!("{}\n[hrn]\nmode = \"blip32\"\n", base_config); + fs::write(storage_path.join(config_file_name), &toml_config).unwrap(); + let config = load_config(&args_config).unwrap(); + assert!(matches!(config.hrn_config.resolution_config, HRNResolverConfig::Blip32)); + + // Invalid mode is rejected. + let toml_config = format!("{}\n[hrn]\nmode = \"bogus\"\n", base_config); + fs::write(storage_path.join(config_file_name), &toml_config).unwrap(); + let err = load_config(&args_config).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + + // Invalid DNS server address is rejected (contains chars disallowed in hostnames, so + // neither the as-is parse nor the `:53` fallback can accept it). + let toml_config = + format!("{}\n[hrn]\ndns_server_address = \"invalid@address\"\n", base_config); + fs::write(storage_path.join(config_file_name), &toml_config).unwrap(); + let err = load_config(&args_config).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + + // DNS server address without an explicit port defaults to port 53. + let toml_config = format!("{}\n[hrn]\ndns_server_address = \"1.1.1.1\"\n", base_config); + fs::write(storage_path.join(config_file_name), &toml_config).unwrap(); + let config = load_config(&args_config).unwrap(); + match config.hrn_config.resolution_config { + HRNResolverConfig::Dns { dns_server_address, .. } => { + assert_eq!(dns_server_address, SocketAddress::from_str("1.1.1.1:53").unwrap()); + }, + other => panic!("unexpected HRN resolver config: {:?}", other), + } + + // `blip32` mode combined with DNS-only settings is rejected so users aren't confused + // by settings that would silently have no effect. + let toml_config = format!( + "{}\n[hrn]\nmode = \"blip32\"\ndns_server_address = \"1.1.1.1:53\"\n", + base_config + ); + fs::write(storage_path.join(config_file_name), &toml_config).unwrap(); + let err = load_config(&args_config).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + assert!(err.to_string().contains("dns_server_address")); + + let toml_config = format!( + "{}\n[hrn]\nmode = \"blip32\"\nenable_resolution_service = true\n", + base_config + ); + fs::write(storage_path.join(config_file_name), &toml_config).unwrap(); + let err = load_config(&args_config).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + assert!(err.to_string().contains("enable_resolution_service")); + } }