Skip to content

Commit c276646

Browse files
authored
Merge pull request #216 from benthecarman/deny-bad-config
Reject unknown config options
2 parents d1c9b68 + d71a479 commit c276646

3 files changed

Lines changed: 126 additions & 11 deletions

File tree

ldk-server-cli/src/main.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,20 @@ async fn main() {
563563
}
564564

565565
let config_path = cli.config.map(PathBuf::from).or_else(get_default_config_path);
566-
let config = config_path.as_ref().and_then(|p| load_config(p).ok());
566+
let config = match config_path.as_ref() {
567+
None => None,
568+
Some(path) => {
569+
if path.is_file() {
570+
let cfg = load_config(path).unwrap_or_else(|e| {
571+
eprintln!("Failed to load config file '{}': {}", path.display(), e);
572+
std::process::exit(1);
573+
});
574+
Some(cfg)
575+
} else {
576+
None
577+
}
578+
},
579+
};
567580

568581
let api_key = resolve_api_key(cli.api_key, config.as_ref()).unwrap_or_else(|| {
569582
eprintln!("API key not provided. Use --api-key or ensure the api_key file exists at {DEFAULT_DIR}/[network]/api_key");

ldk-server-client/src/config.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ pub fn cert_path_for_storage_dir(storage_dir: &str) -> PathBuf {
7070

7171
/// Top-level structure of the `ldk-server` configuration TOML file.
7272
#[derive(Debug, Deserialize)]
73+
#[serde(deny_unknown_fields)]
7374
pub struct Config {
7475
/// Node-level configuration.
7576
pub node: NodeConfig,
@@ -81,13 +82,15 @@ pub struct Config {
8182

8283
/// `[tls]` section of the configuration file.
8384
#[derive(Debug, Deserialize, Serialize)]
85+
#[serde(deny_unknown_fields)]
8486
pub struct TlsConfig {
8587
/// Path to the server's TLS certificate in PEM format.
8688
pub cert_path: Option<String>,
8789
}
8890

8991
/// `[node]` section of the configuration file.
9092
#[derive(Debug, Deserialize)]
93+
#[serde(deny_unknown_fields)]
9194
pub struct NodeConfig {
9295
/// Address of the `ldk-server` gRPC service.
9396
#[serde(default = "default_grpc_service_address")]
@@ -97,13 +100,15 @@ pub struct NodeConfig {
97100

98101
/// `[storage]` section of the configuration file.
99102
#[derive(Debug, Deserialize)]
103+
#[serde(deny_unknown_fields)]
100104
pub struct StorageConfig {
101105
/// On-disk storage configuration.
102106
pub disk: Option<DiskConfig>,
103107
}
104108

105109
/// `[storage.disk]` section of the configuration file.
106110
#[derive(Debug, Deserialize)]
111+
#[serde(deny_unknown_fields)]
107112
pub struct DiskConfig {
108113
/// Directory used by the server to store its persistent data.
109114
pub dir_path: Option<String>,
@@ -202,6 +207,31 @@ mod tests {
202207
assert_eq!(config.node.grpc_service_address, DEFAULT_GRPC_SERVICE_ADDRESS);
203208
}
204209

210+
#[test]
211+
fn config_rejects_unknown_fields() {
212+
let top_level_err = toml::from_str::<Config>(
213+
r#"
214+
[node]
215+
network = "regtest"
216+
217+
[unknown]
218+
option = true
219+
"#,
220+
)
221+
.unwrap_err();
222+
assert!(top_level_err.to_string().contains("unknown field `unknown`"));
223+
224+
let node_err = toml::from_str::<Config>(
225+
r#"
226+
[node]
227+
network = "regtest"
228+
unknown = true
229+
"#,
230+
)
231+
.unwrap_err();
232+
assert!(node_err.to_string().contains("unknown field `unknown`"));
233+
}
234+
205235
#[test]
206236
fn resolve_base_url_uses_cli_arg_first() {
207237
let config: Config = toml::from_str(

ldk-server/src/util/config.rs

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ impl ConfigBuilder {
460460

461461
/// Configuration loaded from a TOML file.
462462
#[derive(Deserialize, Serialize)]
463+
#[serde(deny_unknown_fields)]
463464
pub struct TomlConfig {
464465
node: Option<NodeConfig>,
465466
storage: Option<StorageConfig>,
@@ -475,6 +476,7 @@ pub struct TomlConfig {
475476
}
476477

477478
#[derive(Deserialize, Serialize)]
479+
#[serde(deny_unknown_fields)]
478480
struct NodeConfig {
479481
network: Option<Network>,
480482
listening_addresses: Option<Vec<String>>,
@@ -487,46 +489,54 @@ struct NodeConfig {
487489
}
488490

489491
#[derive(Deserialize, Serialize)]
492+
#[serde(deny_unknown_fields)]
490493
struct StorageConfig {
491494
disk: Option<DiskConfig>,
492495
}
493496

494497
#[derive(Deserialize, Serialize)]
498+
#[serde(deny_unknown_fields)]
495499
struct DiskConfig {
496500
dir_path: Option<String>,
497501
}
498502

499503
#[derive(Deserialize, Serialize)]
504+
#[serde(deny_unknown_fields)]
500505
struct BitcoindConfig {
501506
rpc_address: Option<String>,
502507
rpc_user: Option<String>,
503508
rpc_password: Option<String>,
504509
}
505510

506511
#[derive(Deserialize, Serialize)]
512+
#[serde(deny_unknown_fields)]
507513
struct ElectrumConfig {
508514
server_url: String,
509515
}
510516

511517
#[derive(Deserialize, Serialize)]
518+
#[serde(deny_unknown_fields)]
512519
struct EsploraConfig {
513520
server_url: String,
514521
}
515522

516523
#[derive(Deserialize, Serialize)]
524+
#[serde(deny_unknown_fields)]
517525
struct LogConfig {
518526
level: Option<String>,
519527
file: Option<String>,
520528
}
521529

522530
#[derive(Deserialize, Serialize)]
531+
#[serde(deny_unknown_fields)]
523532
struct TomlTlsConfig {
524533
cert_path: Option<String>,
525534
key_path: Option<String>,
526535
hosts: Option<Vec<String>>,
527536
}
528537

529538
#[derive(Deserialize, Serialize)]
539+
#[serde(deny_unknown_fields)]
530540
struct MetricsTomlConfig {
531541
enabled: Option<bool>,
532542
poll_metrics_interval: Option<u64>,
@@ -535,11 +545,13 @@ struct MetricsTomlConfig {
535545
}
536546

537547
#[derive(Deserialize, Serialize)]
548+
#[serde(deny_unknown_fields)]
538549
struct TomlTorConfig {
539550
proxy_address: String,
540551
}
541552

542553
#[derive(Deserialize, Serialize)]
554+
#[serde(deny_unknown_fields)]
543555
struct HrnTomlConfig {
544556
mode: Option<String>,
545557
dns_server_address: Option<String>,
@@ -645,19 +657,22 @@ fn parse_async_payments_role(role: &str) -> io::Result<AsyncPaymentsRole> {
645657
}
646658

647659
#[derive(Deserialize, Serialize)]
660+
#[serde(deny_unknown_fields)]
648661
struct LiquidityConfig {
649662
lsps2_client: Option<LSPSClientTomlConfig>,
650663
lsps2_service: Option<LSPS2ServiceTomlConfig>,
651664
}
652665

653666
#[derive(Deserialize, Serialize, Debug)]
667+
#[serde(deny_unknown_fields)]
654668
struct LSPSClientTomlConfig {
655669
node_pubkey: String,
656670
address: String,
657671
token: Option<String>,
658672
}
659673

660674
#[derive(Deserialize, Serialize, Debug)]
675+
#[serde(deny_unknown_fields)]
661676
struct LSPS2ServiceTomlConfig {
662677
advertise_service: bool,
663678
channel_opening_fee_ppm: u32,
@@ -1343,6 +1358,73 @@ mod tests {
13431358
validate_missing!("network =", missing_field_msg("network"));
13441359
}
13451360

1361+
#[test]
1362+
fn test_config_unknown_fields_in_file() {
1363+
let storage_path = std::env::temp_dir();
1364+
let config_file_name = "test_config_unknown_fields_in_file.toml";
1365+
1366+
let mut args_config = empty_args_config();
1367+
args_config.config_file =
1368+
Some(storage_path.join(config_file_name).to_string_lossy().to_string());
1369+
1370+
fs::write(
1371+
storage_path.join(config_file_name),
1372+
format!("{}\n[unknown]\noption = true\n", DEFAULT_CONFIG),
1373+
)
1374+
.unwrap();
1375+
let err = load_config(&args_config).unwrap_err();
1376+
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1377+
assert!(err.to_string().contains("unknown field `unknown`"));
1378+
1379+
fs::write(
1380+
storage_path.join(config_file_name),
1381+
DEFAULT_CONFIG
1382+
.replace("network = \"regtest\"", "network = \"regtest\"\nunknown = true"),
1383+
)
1384+
.unwrap();
1385+
let err = load_config(&args_config).unwrap_err();
1386+
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1387+
assert!(err.to_string().contains("unknown field `unknown`"));
1388+
}
1389+
1390+
#[test]
1391+
#[cfg(not(feature = "experimental-lsps2-support"))]
1392+
fn test_config_allows_unused_lsps2_service_config_without_feature() {
1393+
let storage_path = std::env::temp_dir();
1394+
let config_file_name =
1395+
"test_config_allows_unused_lsps2_service_config_without_feature.toml";
1396+
1397+
let mut args_config = empty_args_config();
1398+
args_config.config_file =
1399+
Some(storage_path.join(config_file_name).to_string_lossy().to_string());
1400+
1401+
let toml_config = r#"
1402+
[node]
1403+
network = "regtest"
1404+
1405+
[bitcoind]
1406+
rpc_address = "127.0.0.1:8332"
1407+
rpc_user = "bitcoind-testuser"
1408+
rpc_password = "bitcoind-testpassword"
1409+
1410+
[liquidity.lsps2_service]
1411+
advertise_service = false
1412+
channel_opening_fee_ppm = 1000
1413+
channel_over_provisioning_ppm = 500000
1414+
min_channel_opening_fee_msat = 10000000
1415+
min_channel_lifetime = 4320
1416+
max_client_to_self_delay = 1440
1417+
min_payment_size_msat = 10000000
1418+
max_payment_size_msat = 25000000000
1419+
client_trusts_lsp = true
1420+
disable_client_reserve = false
1421+
"#;
1422+
1423+
fs::write(storage_path.join(config_file_name), toml_config).unwrap();
1424+
let config = load_config(&args_config).unwrap();
1425+
assert!(config.lsps2_service_config.is_none());
1426+
}
1427+
13461428
fn remove_config_line(config: &str, key: &str) -> String {
13471429
config
13481430
.lines()
@@ -1573,7 +1655,6 @@ mod tests {
15731655
let toml_config = r#"
15741656
[node]
15751657
network = "regtest"
1576-
rest_service_address = "127.0.0.1:3002"
15771658
15781659
[bitcoind]
15791660
rpc_address = "127.0.0.1:8332"
@@ -1585,10 +1666,6 @@ mod tests {
15851666
username = "admin"
15861667
password = "password123"
15871668
1588-
[rabbitmq]
1589-
connection_string = "rabbitmq_connection_string"
1590-
exchange_name = "rabbitmq_exchange_name"
1591-
15921669
[liquidity.lsps2_service]
15931670
advertise_service = false
15941671
channel_opening_fee_ppm = 1000 # 0.1% fee
@@ -1621,7 +1698,6 @@ mod tests {
16211698
let toml_config = r#"
16221699
[node]
16231700
network = "regtest"
1624-
rest_service_address = "127.0.0.1:3002"
16251701
16261702
[bitcoind]
16271703
rpc_address = "127.0.0.1:8332"
@@ -1632,10 +1708,6 @@ mod tests {
16321708
enabled = true
16331709
username = "admin"
16341710
1635-
[rabbitmq]
1636-
connection_string = "rabbitmq_connection_string"
1637-
exchange_name = "rabbitmq_exchange_name"
1638-
16391711
[liquidity.lsps2_service]
16401712
advertise_service = false
16411713
channel_opening_fee_ppm = 1000 # 0.1% fee

0 commit comments

Comments
 (0)