fix(migrate): backfill outbound SOCKS5 credential fields

migrate_server_json backfilled the `api` section but not the newer
outbound.username / outbound.password (added with SOCKS5 upstream auth), so a
server config predating them reported "nothing to migrate" while the fields were
absent. Now backfilled to their "" (no-auth) default.

Deliberately does NOT inject the optional bind_ip (top level) or per-rule
send_from: absent means "use the default source", which is correct and concise,
and a placeholder would be either stripped (null) or, worse, parse as an invalid
source IP (""). Those stay hand-added when the operator wants multi-address
egress. Test covers both the backfill and the no-inject.
This commit is contained in:
ospab 2026-08-25 10:13:30 +03:00
parent c1b7172fc0
commit d170b6de73
1 changed files with 41 additions and 0 deletions

View File

@ -353,6 +353,22 @@ pub fn migrate_server_json(json: Value) -> (Value, MigrationReport) {
}
}
// Backfill the SOCKS5 credential fields on `outbound`, added after some
// server configs already existed. These are plain strings that default to
// "" (no-auth), so making them explicit is safe and concise. The optional
// `bind_ip` (top level) and per-rule `send_from` are deliberately NOT
// backfilled: absent means "use the default source", which is correct — and
// a placeholder would either be stripped (null) or, worse, parse as an
// invalid source IP ("").
if let Some(outbound) = obj.get_mut("outbound").and_then(|o| o.as_object_mut()) {
for key in ["username", "password"] {
if !outbound.contains_key(key) {
report.note(format!("Added outbound.{key} = \"\" (missing default)"));
outbound.insert(key.to_string(), json!(""));
}
}
}
(out, report)
}
@ -584,6 +600,31 @@ mod tests {
assert!(report.notes.iter().any(|n| n.contains("api.token")));
}
/// A server config whose `outbound` predates the SOCKS5 credential fields
/// must get them backfilled — this is exactly the "migrate said nothing to
/// migrate but the new fields were missing" gap. The optional bind_ip /
/// send_from must NOT be injected (absent = correct).
#[test]
fn server_migrate_backfills_outbound_credentials() {
let old = json!({
"listen": "0.0.0.0:50000",
"access_keys": ["k1"],
"api": { "enabled": true, "bind": "0.0.0.0:9090", "webpath": "", "username": "", "password_hash": "" },
"outbound": {
"enabled": false, "protocol": "socks5", "address": "127.0.0.1", "port": 40000,
"default_action": "proxy",
"rules": [{ "action": "proxy", "domain_suffix": [".onion"] }]
}
});
let (new, report) = migrate_server_json(old);
assert!(report.changed, "adding the missing credential fields is a change");
assert_eq!(new["outbound"]["username"], "");
assert_eq!(new["outbound"]["password"], "");
// Optional fields are left absent, not injected.
assert!(new.get("bind_ip").is_none());
assert!(new["outbound"]["rules"][0].get("send_from").is_none());
}
#[test]
fn detect_kind_falls_back_to_structural_sniffing_without_mode_tag() {
assert_eq!(detect_kind(&json!({"access_key": "x", "server": "y"})), Some(ConfigKind::Client));