fix(openwrt): harden TollGate PR integration

This commit is contained in:
archipelago
2026-09-08 21:06:36 -04:00
parent f9af30b08a
commit e661f237f1
3 changed files with 135 additions and 9 deletions
+4
View File
@@ -270,6 +270,10 @@ impl RpcHandler {
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string);
if let Some(address) = payout_address.as_deref() {
tollgate::config::validate_payout_address(address)
.context("invalid TollGate payout address")?;
}
let config = TollGateConfig {
ssid: "archipelago".to_string(),
+100 -6
View File
@@ -136,11 +136,44 @@ pub fn apply_payout_identity(router: &Router, payout_address: Option<&str>) -> R
let Some(address) = payout_address else {
return Ok(());
};
validate_payout_address(address)?;
let existing = router.run_ok("cat /etc/tollgate/identities.json 2>/dev/null || echo '{}'")?;
let mut doc: serde_json::Value =
serde_json::from_str(existing.trim()).unwrap_or_else(|_| serde_json::json!({}));
let mut doc = parse_identities(&existing)?;
merge_payout_identity(&mut doc, address)?;
let json_str = serde_json::to_string_pretty(&doc).context("serialize identities.json")?;
router
.upload_file("/etc/tollgate/identities.json", json_str.as_bytes())
.context("upload /etc/tollgate/identities.json")?;
Ok(())
}
fn parse_identities(existing: &str) -> Result<serde_json::Value> {
serde_json::from_str(existing.trim()).context(
"parse existing /etc/tollgate/identities.json; refusing to overwrite malformed identity data",
)
}
/// Reject malformed values before provisioning changes anything on the
/// router. A payout typo otherwise remains dormant until the threshold is
/// reached, when the operator discovers that settlement cannot resolve.
pub fn validate_payout_address(address: &str) -> Result<()> {
let (name, domain) = address
.split_once('@')
.context("Lightning address must look like name@example.com")?;
if name.is_empty()
|| domain.is_empty()
|| domain.contains('@')
|| address.chars().any(char::is_whitespace)
{
anyhow::bail!("Lightning address must look like name@example.com");
}
Ok(())
}
fn merge_payout_identity(doc: &mut serde_json::Value, address: &str) -> Result<()> {
let identities = doc
.as_object_mut()
.context("identities.json root is not a JSON object")?
@@ -166,9 +199,70 @@ pub fn apply_payout_identity(router: &Router, payout_address: Option<&str>) -> R
}
}
let json_str = serde_json::to_string_pretty(&doc).context("serialize identities.json")?;
router
.upload_file("/etc/tollgate/identities.json", json_str.as_bytes())
.context("upload /etc/tollgate/identities.json")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{merge_payout_identity, parse_identities, validate_payout_address};
#[test]
fn payout_merge_changes_only_owner_address() {
let mut doc = serde_json::json!({
"config_version": "v0.0.1",
"owned_identities": [{ "name": "merchant", "privatekey": "keep-secret" }],
"public_identities": [
{ "name": "owner", "pubkey": "not currently used", "lightning_address": "old@example.com" },
{ "name": "upstream", "lightning_address": "keep@example.com" }
]
});
let before_owned = doc["owned_identities"].clone();
let before_other = doc["public_identities"][1].clone();
merge_payout_identity(&mut doc, "operator@example.com").unwrap();
assert_eq!(doc["owned_identities"], before_owned);
assert_eq!(doc["public_identities"][1], before_other);
assert_eq!(
doc["public_identities"][0]["lightning_address"],
"operator@example.com"
);
}
#[test]
fn payout_merge_can_create_missing_owner() {
let mut doc = serde_json::json!({ "public_identities": [] });
merge_payout_identity(&mut doc, "operator@example.com").unwrap();
assert_eq!(doc["public_identities"][0]["name"], "owner");
assert_eq!(
doc["public_identities"][0]["lightning_address"],
"operator@example.com"
);
}
#[test]
fn payout_address_validation_rejects_typographical_failures() {
assert!(validate_payout_address("operator@example.com").is_ok());
for invalid in [
"",
"operator",
"@example.com",
"operator@",
"a@b@c",
"a b@example.com",
] {
assert!(
validate_payout_address(invalid).is_err(),
"accepted {invalid:?}"
);
}
}
#[test]
fn malformed_identity_data_is_never_replaced() {
let err = parse_identities("{ truncated").unwrap_err();
assert!(err
.to_string()
.contains("refusing to overwrite malformed identity data"));
}
}
+31 -3
View File
@@ -149,10 +149,18 @@ pub fn regate_upstream_default_aps(router: &Router) -> Result<()> {
for section in sections.lines().map(str::trim).filter(|s| !s.is_empty()) {
let network_key = format!("{}.network", section);
let current = router.uci_get(&network_key).unwrap_or_default();
if current == "lan" {
let ssid = router
.uci_get(&format!("{}.ssid", section))
.unwrap_or_default();
// A failed/changed upstream first-boot script can leave a stock
// default_radioN section in place. Moving that interface merely
// because it is on LAN can seize the router's existing management AP.
// Only the public APs the TollGate installer demonstrably rebranded
// belong on the paid network.
if should_regate_upstream_ap(&current, &ssid) {
info!(
"[{}] Re-gating upstream default AP {} (was network=lan) onto the tollgate network",
router.host, section
"[{}] Re-gating upstream default AP {} ({}) onto the tollgate network",
router.host, section, ssid
);
router.uci_set(&network_key, "tollgate")?;
}
@@ -161,6 +169,10 @@ pub fn regate_upstream_default_aps(router: &Router) -> Result<()> {
Ok(())
}
fn should_regate_upstream_ap(network: &str, ssid: &str) -> bool {
network.trim() == "lan" && ssid.trim().starts_with("TollGate-")
}
/// Return the first available wireless radio device name (e.g. "radio0").
fn detect_radio(router: &Router) -> Result<String> {
let out =
@@ -169,3 +181,19 @@ fn detect_radio(router: &Router) -> Result<String> {
let radio = out.trim().split('.').nth(1).unwrap_or("radio0").to_string();
Ok(radio)
}
#[cfg(test)]
mod tests {
use super::should_regate_upstream_ap;
#[test]
fn regates_only_confirmed_upstream_tollgate_aps() {
assert!(should_regate_upstream_ap("lan", "TollGate-3458"));
assert!(should_regate_upstream_ap(" lan\n", " TollGate-A1B2 "));
assert!(!should_regate_upstream_ap("lan", "OpenWrt"));
assert!(!should_regate_upstream_ap("lan", "Archipelago Admin"));
assert!(!should_regate_upstream_ap("tollgate", "TollGate-3458"));
assert!(!should_regate_upstream_ap("lan", "tollgate-3458"));
}
}