fix: OTG 网卡桥接改由 NetworkManager 生成桥接配置 UUID;docker 补全 NetworkManager 依赖

This commit is contained in:
mofeng-git
2026-07-24 11:01:54 +08:00
parent 8d5366444b
commit 4cd622ff07
3 changed files with 92 additions and 30 deletions

View File

@@ -19,6 +19,10 @@ RUN sed -i 's/ main$/ main contrib non-free/' /etc/apt/sources.list && \
ca-certificates \ ca-certificates \
libudev1 \ libudev1 \
libasound2 \ libasound2 \
# OTG Ethernet bridge control (nmcli talks to the host NetworkManager over D-Bus)
network-manager \
iproute2 \
iputils-ping \
# v4l2 is handled by kernel, minimal userspace needed # v4l2 is handled by kernel, minimal userspace needed
libv4l-0 \ libv4l-0 \
&& \ && \

View File

@@ -19,6 +19,10 @@ RUN sed -i 's/ main$/ main contrib non-free/' /etc/apt/sources.list && \
ca-certificates \ ca-certificates \
libudev1 \ libudev1 \
libasound2 \ libasound2 \
# OTG Ethernet bridge control (nmcli talks to the host NetworkManager over D-Bus)
network-manager \
iproute2 \
iputils-ping \
# v4l2 is handled by kernel, minimal userspace needed # v4l2 is handled by kernel, minimal userspace needed
libv4l-0 \ libv4l-0 \
&& \ && \

View File

@@ -67,8 +67,14 @@ struct BridgeJournal {
existing_bridge: bool, existing_bridge: bool,
original_connection_uuid: Option<String>, original_connection_uuid: Option<String>,
bridge_profile_uuid: Option<String>, bridge_profile_uuid: Option<String>,
#[serde(default)]
bridge_profile_name: Option<String>,
uplink_profile_uuid: Option<String>, uplink_profile_uuid: Option<String>,
#[serde(default)]
uplink_profile_name: Option<String>,
usb_profile_uuid: String, usb_profile_uuid: String,
#[serde(default)]
usb_profile_name: Option<String>,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -87,11 +93,11 @@ impl TransactionProfiles {
let suffix = &transaction[..12]; let suffix = &transaction[..12];
Self { Self {
bridge_name: format!("{PROFILE_PREFIX}-bridge-{suffix}"), bridge_name: format!("{PROFILE_PREFIX}-bridge-{suffix}"),
bridge_uuid: Uuid::new_v4().to_string(), bridge_uuid: String::new(),
uplink_name: format!("{PROFILE_PREFIX}-uplink-{suffix}"), uplink_name: format!("{PROFILE_PREFIX}-uplink-{suffix}"),
uplink_uuid: Uuid::new_v4().to_string(), uplink_uuid: String::new(),
usb_name: format!("{PROFILE_PREFIX}-usb-{suffix}"), usb_name: format!("{PROFILE_PREFIX}-usb-{suffix}"),
usb_uuid: Uuid::new_v4().to_string(), usb_uuid: String::new(),
} }
} }
} }
@@ -151,15 +157,18 @@ impl NetworkBridgeRuntime {
.trim() .trim()
.to_string(); .to_string();
let profiles = TransactionProfiles::new(); let mut profiles = TransactionProfiles::new();
let journal = BridgeJournal { let mut journal = BridgeJournal {
version: JOURNAL_VERSION, version: JOURNAL_VERSION,
uplink: uplink.to_string(), uplink: uplink.to_string(),
existing_bridge: false, existing_bridge: false,
original_connection_uuid: Some(original_connection_uuid.clone()), original_connection_uuid: Some(original_connection_uuid.clone()),
bridge_profile_uuid: Some(profiles.bridge_uuid.clone()), bridge_profile_uuid: None,
uplink_profile_uuid: Some(profiles.uplink_uuid.clone()), bridge_profile_name: Some(profiles.bridge_name.clone()),
usb_profile_uuid: profiles.usb_uuid.clone(), uplink_profile_uuid: None,
uplink_profile_name: Some(profiles.uplink_name.clone()),
usb_profile_uuid: String::new(),
usb_profile_name: Some(profiles.usb_name.clone()),
}; };
write_journal(&journal)?; write_journal(&journal)?;
@@ -174,9 +183,10 @@ impl NetworkBridgeRuntime {
BRIDGE_IF, BRIDGE_IF,
"con-name", "con-name",
&profiles.bridge_name, &profiles.bridge_name,
"connection.uuid",
&profiles.bridge_uuid,
])?; ])?;
profiles.bridge_uuid = connection_value(&profiles.bridge_name, "connection.uuid")?;
journal.bridge_profile_uuid = Some(profiles.bridge_uuid.clone());
write_journal(&journal)?;
run_nmcli(&[ run_nmcli(&[
"connection", "connection",
"modify", "modify",
@@ -220,8 +230,6 @@ impl NetworkBridgeRuntime {
uplink, uplink,
"con-name", "con-name",
&profiles.uplink_name, &profiles.uplink_name,
"connection.uuid",
&profiles.uplink_uuid,
"master", "master",
BRIDGE_IF, BRIDGE_IF,
"slave-type", "slave-type",
@@ -229,6 +237,9 @@ impl NetworkBridgeRuntime {
"connection.autoconnect", "connection.autoconnect",
"no", "no",
])?; ])?;
profiles.uplink_uuid = connection_value(&profiles.uplink_name, "connection.uuid")?;
journal.uplink_profile_uuid = Some(profiles.uplink_uuid.clone());
write_journal(&journal)?;
run_nmcli(&[ run_nmcli(&[
"connection", "connection",
"add", "add",
@@ -238,8 +249,6 @@ impl NetworkBridgeRuntime {
usb_interface, usb_interface,
"con-name", "con-name",
&profiles.usb_name, &profiles.usb_name,
"connection.uuid",
&profiles.usb_uuid,
"master", "master",
BRIDGE_IF, BRIDGE_IF,
"slave-type", "slave-type",
@@ -247,6 +256,9 @@ impl NetworkBridgeRuntime {
"connection.autoconnect", "connection.autoconnect",
"no", "no",
])?; ])?;
profiles.usb_uuid = connection_value(&profiles.usb_name, "connection.uuid")?;
journal.usb_profile_uuid = profiles.usb_uuid.clone();
write_journal(&journal)?;
Ok(()) Ok(())
})(); })();
if let Err(error) = prepare_result { if let Err(error) = prepare_result {
@@ -449,17 +461,27 @@ fn select_bridge_candidate<'a>(
fn restore_from_journal(journal: &BridgeJournal) -> Result<()> { fn restore_from_journal(journal: &BridgeJournal) -> Result<()> {
let mut errors = Vec::new(); let mut errors = Vec::new();
for (kind, profile_uuid) in [ for (kind, profile_uuid, profile_name) in [
("USB", Some(journal.usb_profile_uuid.as_str())), (
("uplink", journal.uplink_profile_uuid.as_deref()), "USB",
("bridge", journal.bridge_profile_uuid.as_deref()), (!journal.usb_profile_uuid.is_empty()).then_some(journal.usb_profile_uuid.as_str()),
journal.usb_profile_name.as_deref(),
),
(
"uplink",
journal.uplink_profile_uuid.as_deref(),
journal.uplink_profile_name.as_deref(),
),
(
"bridge",
journal.bridge_profile_uuid.as_deref(),
journal.bridge_profile_name.as_deref(),
),
] { ] {
let Some(profile_uuid) = profile_uuid else { if let Err(error) = delete_owned_connection(profile_uuid, profile_name) {
continue; let profile = profile_uuid.or(profile_name).unwrap_or("unknown");
};
if let Err(error) = delete_connection(profile_uuid) {
errors.push(format!( errors.push(format!(
"failed to remove owned {kind} profile {profile_uuid}: {error}" "failed to remove owned {kind} profile {profile}: {error}"
)); ));
} }
} }
@@ -627,12 +649,23 @@ fn connection_uuids() -> Result<Vec<String>> {
.collect()) .collect())
} }
fn delete_connection(profile_uuid: &str) -> Result<()> { fn delete_owned_connection(profile_uuid: Option<&str>, profile_name: Option<&str>) -> Result<()> {
if !connection_uuids()?.iter().any(|uuid| uuid == profile_uuid) { if let Some(profile_uuid) = profile_uuid {
if connection_uuids()?.iter().any(|uuid| uuid == profile_uuid) {
return run_nmcli(&["connection", "delete", "uuid", profile_uuid]).map(|_| ());
}
}
let Some(profile_name) = profile_name else {
return Ok(());
};
let output = run_nmcli(&["-t", "--escape", "no", "-f", "NAME", "connection", "show"])?;
if !String::from_utf8_lossy(&output.stdout)
.lines()
.any(|name| name == profile_name)
{
return Ok(()); return Ok(());
} }
run_nmcli(&["connection", "delete", "uuid", profile_uuid])?; run_nmcli(&["connection", "delete", "id", profile_name]).map(|_| ())
Ok(())
} }
fn copy_connection_properties(source: &str, target: &str, properties: &[&str]) -> Result<()> { fn copy_connection_properties(source: &str, target: &str, properties: &[&str]) -> Result<()> {
@@ -834,8 +867,11 @@ mod tests {
existing_bridge: false, existing_bridge: false,
original_connection_uuid: Some("original-uuid".to_string()), original_connection_uuid: Some("original-uuid".to_string()),
bridge_profile_uuid: Some("bridge-uuid".to_string()), bridge_profile_uuid: Some("bridge-uuid".to_string()),
bridge_profile_name: Some("bridge-name".to_string()),
uplink_profile_uuid: Some("uplink-uuid".to_string()), uplink_profile_uuid: Some("uplink-uuid".to_string()),
uplink_profile_name: Some("uplink-name".to_string()),
usb_profile_uuid: "usb-uuid".to_string(), usb_profile_uuid: "usb-uuid".to_string(),
usb_profile_name: Some("usb-name".to_string()),
}; };
let value = serde_json::to_string(&journal).unwrap(); let value = serde_json::to_string(&journal).unwrap();
let decoded: BridgeJournal = serde_json::from_str(&value).unwrap(); let decoded: BridgeJournal = serde_json::from_str(&value).unwrap();
@@ -844,13 +880,31 @@ mod tests {
} }
#[test] #[test]
fn transaction_profiles_use_unique_names_and_uuids() { fn bridge_journal_accepts_legacy_entries_without_profile_names() {
let value = r#"{
"version": 2,
"uplink": "eth0",
"existing_bridge": false,
"original_connection_uuid": "original-uuid",
"bridge_profile_uuid": "bridge-uuid",
"uplink_profile_uuid": "uplink-uuid",
"usb_profile_uuid": "usb-uuid"
}"#;
let decoded: BridgeJournal = serde_json::from_str(value).unwrap();
assert_eq!(decoded.bridge_profile_name, None);
assert_eq!(decoded.uplink_profile_name, None);
assert_eq!(decoded.usb_profile_name, None);
}
#[test]
fn transaction_profiles_use_unique_names() {
let first = TransactionProfiles::new(); let first = TransactionProfiles::new();
let second = TransactionProfiles::new(); let second = TransactionProfiles::new();
assert_ne!(first.bridge_name, second.bridge_name); assert_ne!(first.bridge_name, second.bridge_name);
assert_ne!(first.bridge_uuid, second.bridge_uuid);
assert!(first.usb_name.starts_with(PROFILE_PREFIX)); assert!(first.usb_name.starts_with(PROFILE_PREFIX));
assert!(Uuid::parse_str(&first.usb_uuid).is_ok()); assert!(first.bridge_uuid.is_empty());
assert!(first.uplink_uuid.is_empty());
assert!(first.usb_uuid.is_empty());
} }
#[test] #[test]