From 4e8b22bedb63bcf697cbad642598ee05ff3159a3 Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Thu, 20 Aug 2026 01:06:50 +0000 Subject: [PATCH 01/11] Shuffle vsock folder structure --- Cargo.lock | 16 +--------- Cargo.toml | 4 +-- rs/ic_os/guest_upgrade/server/BUILD.bazel | 2 +- rs/ic_os/guest_upgrade/server/Cargo.toml | 2 +- rs/ic_os/guest_upgrade/tests/BUILD.bazel | 2 +- rs/ic_os/guest_upgrade/tests/Cargo.toml | 2 +- rs/ic_os/release/BUILD.bazel | 4 +-- rs/ic_os/vsock/{vsock_lib => }/BUILD.bazel | 29 ++++++++++++++++++- rs/ic_os/vsock/{vsock_lib => }/Cargo.toml | 3 +- rs/ic_os/vsock/guest/BUILD.bazel | 17 ----------- rs/ic_os/vsock/guest/Cargo.toml | 10 ------- rs/ic_os/vsock/host/BUILD.bazel | 16 ---------- rs/ic_os/vsock/host/Cargo.toml | 9 ------ .../{guest/src/main.rs => src/bin/guest.rs} | 0 .../{host/src/main.rs => src/bin/host.rs} | 0 .../vsock/{vsock_lib => }/src/guest/client.rs | 0 .../vsock/{vsock_lib => }/src/guest/mod.rs | 0 .../vsock/{vsock_lib => }/src/host/agent.rs | 0 .../src/host/command_utilities.rs | 0 .../vsock/{vsock_lib => }/src/host/hsm.rs | 0 .../vsock/{vsock_lib => }/src/host/mod.rs | 0 .../vsock/{vsock_lib => }/src/host/server.rs | 0 rs/ic_os/vsock/{vsock_lib => }/src/lib.rs | 0 .../vsock/{vsock_lib => }/src/protocol/mod.rs | 0 .../src/protocol/structures.rs | 0 .../{vsock_lib => }/src/protocol/utils.rs | 0 26 files changed, 38 insertions(+), 78 deletions(-) rename rs/ic_os/vsock/{vsock_lib => }/BUILD.bazel (64%) rename rs/ic_os/vsock/{vsock_lib => }/Cargo.toml (86%) delete mode 100644 rs/ic_os/vsock/guest/BUILD.bazel delete mode 100644 rs/ic_os/vsock/guest/Cargo.toml delete mode 100644 rs/ic_os/vsock/host/BUILD.bazel delete mode 100644 rs/ic_os/vsock/host/Cargo.toml rename rs/ic_os/vsock/{guest/src/main.rs => src/bin/guest.rs} (100%) rename rs/ic_os/vsock/{host/src/main.rs => src/bin/host.rs} (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/guest/client.rs (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/guest/mod.rs (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/host/agent.rs (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/host/command_utilities.rs (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/host/hsm.rs (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/host/mod.rs (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/host/server.rs (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/lib.rs (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/protocol/mod.rs (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/protocol/structures.rs (100%) rename rs/ic_os/vsock/{vsock_lib => }/src/protocol/utils.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 9162869b2275..1e0c189fdca0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24538,26 +24538,12 @@ dependencies = [ "nix 0.27.1", ] -[[package]] -name = "vsock_guest" -version = "1.0.0" -dependencies = [ - "clap", - "vsock_lib", -] - -[[package]] -name = "vsock_host" -version = "1.0.0" -dependencies = [ - "vsock_lib", -] - [[package]] name = "vsock_lib" version = "1.0.0" dependencies = [ "anyhow", + "clap", "ic-http-utils", "mockall", "procfs", diff --git a/Cargo.toml b/Cargo.toml index b3eedeffb384..81eb2b796cda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -199,9 +199,7 @@ members = [ "rs/ic_os/sev/guest/testing", "rs/ic_os/sev/host", "rs/ic_os/utils", - "rs/ic_os/vsock/guest", - "rs/ic_os/vsock/host", - "rs/ic_os/vsock/vsock_lib", + "rs/ic_os/vsock", "rs/ingress_manager", "rs/interfaces", "rs/interfaces/adapter_client", diff --git a/rs/ic_os/guest_upgrade/server/BUILD.bazel b/rs/ic_os/guest_upgrade/server/BUILD.bazel index 6aa7beaaf080..006d1ce71779 100644 --- a/rs/ic_os/guest_upgrade/server/BUILD.bazel +++ b/rs/ic_os/guest_upgrade/server/BUILD.bazel @@ -16,7 +16,7 @@ rust_library( "//rs/ic_os/os_tools/guest_disk:guest_disk_lib", "//rs/ic_os/sev/attestation", "//rs/ic_os/sev/guest", - "//rs/ic_os/vsock/vsock_lib", + "//rs/ic_os/vsock:vsock_lib", "//rs/interfaces", "//rs/interfaces/registry", "//rs/protobuf", diff --git a/rs/ic_os/guest_upgrade/server/Cargo.toml b/rs/ic_os/guest_upgrade/server/Cargo.toml index 334eda028bea..65a52997db92 100644 --- a/rs/ic_os/guest_upgrade/server/Cargo.toml +++ b/rs/ic_os/guest_upgrade/server/Cargo.toml @@ -20,7 +20,7 @@ thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } tonic = { workspace = true, features = ["default"] } -vsock_lib = { path = "../../vsock/vsock_lib" } +vsock_lib = { path = "../../vsock" } x509-parser = { workspace = true } hyper = { workspace = true } hyper-util = { workspace = true } diff --git a/rs/ic_os/guest_upgrade/tests/BUILD.bazel b/rs/ic_os/guest_upgrade/tests/BUILD.bazel index 5488fb5f81ec..8736a22e3ef1 100644 --- a/rs/ic_os/guest_upgrade/tests/BUILD.bazel +++ b/rs/ic_os/guest_upgrade/tests/BUILD.bazel @@ -14,7 +14,7 @@ rust_test( "//rs/ic_os/sev/attestation", "//rs/ic_os/sev/guest", "//rs/ic_os/sev/guest/testing", - "//rs/ic_os/vsock/vsock_lib", + "//rs/ic_os/vsock:vsock_lib", "//rs/interfaces/registry", "//rs/protobuf", "//rs/registry/fake", diff --git a/rs/ic_os/guest_upgrade/tests/Cargo.toml b/rs/ic_os/guest_upgrade/tests/Cargo.toml index 00e60c839073..b60fe6394006 100644 --- a/rs/ic_os/guest_upgrade/tests/Cargo.toml +++ b/rs/ic_os/guest_upgrade/tests/Cargo.toml @@ -25,4 +25,4 @@ sev_guest = { path = "../../sev/guest" } sev_guest_testing = { path = "../../sev/guest/testing" } tempfile = { workspace = true } tokio = { workspace = true } -vsock_lib = { path = "../../vsock/vsock_lib" } +vsock_lib = { path = "../../vsock" } diff --git a/rs/ic_os/release/BUILD.bazel b/rs/ic_os/release/BUILD.bazel index df4d36dd5294..1e79d91b0535 100644 --- a/rs/ic_os/release/BUILD.bazel +++ b/rs/ic_os/release/BUILD.bazel @@ -13,8 +13,8 @@ OBJECTS = { "setupos_tool": "//rs/ic_os/os_tools/setupos_tool:setupos_tool", "config_tool": "//rs/ic_os/config/tool:config_tool", "config_tool_dev": "//rs/ic_os/config/tool:config_tool_dev", - "vsock_guest": "//rs/ic_os/vsock/guest:vsock_guest", - "vsock_host": "//rs/ic_os/vsock/host:vsock_host", + "vsock_guest": "//rs/ic_os/vsock:vsock_guest", + "vsock_host": "//rs/ic_os/vsock:vsock_host", "metrics-proxy": "//rs/monitoring/metrics_proxy:metrics-proxy", "nss_icos": "//rs/ic_os/networking/nss_icos", "custom_metrics": "//rs/ic_os/metrics/custom_metrics:custom_metrics_bin", diff --git a/rs/ic_os/vsock/vsock_lib/BUILD.bazel b/rs/ic_os/vsock/BUILD.bazel similarity index 64% rename from rs/ic_os/vsock/vsock_lib/BUILD.bazel rename to rs/ic_os/vsock/BUILD.bazel index ef6952747377..e45ab0cdb381 100644 --- a/rs/ic_os/vsock/vsock_lib/BUILD.bazel +++ b/rs/ic_os/vsock/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") package(default_visibility = ["//rs:ic-os-pkg"]) @@ -44,3 +44,30 @@ rust_test( "@crate_index//:vsock", ], ) + +rust_binary( + name = "vsock_guest", + srcs = ["src/bin/guest.rs"], + crate_name = "vsock_guest", + target_compatible_with = [ + "@platforms//os:linux", + ], + deps = [ + # Keep sorted. + ":vsock_lib", + "@crate_index//:clap", + ], +) + +rust_binary( + name = "vsock_host", + srcs = ["src/bin/host.rs"], + crate_name = "vsock_host", + target_compatible_with = [ + "@platforms//os:linux", + ], + deps = [ + # Keep sorted. + ":vsock_lib", + ], +) diff --git a/rs/ic_os/vsock/vsock_lib/Cargo.toml b/rs/ic_os/vsock/Cargo.toml similarity index 86% rename from rs/ic_os/vsock/vsock_lib/Cargo.toml rename to rs/ic_os/vsock/Cargo.toml index 805730a5df68..63dc5f2434f0 100644 --- a/rs/ic_os/vsock/vsock_lib/Cargo.toml +++ b/rs/ic_os/vsock/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true [dependencies] anyhow = { workspace = true } +clap = { workspace = true } # only needed by bin, can't express in direct Cargo.toml mockall = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -21,5 +22,5 @@ rusb = { version = "0.9", features = ["vendored"] } sha2 = { workspace = true } tempfile = { workspace = true } vsock = "0.4" -ic-http-utils = { path = "../../../http_utils" } +ic-http-utils = { path = "../../http_utils" } tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } diff --git a/rs/ic_os/vsock/guest/BUILD.bazel b/rs/ic_os/vsock/guest/BUILD.bazel deleted file mode 100644 index 3f8c993075ad..000000000000 --- a/rs/ic_os/vsock/guest/BUILD.bazel +++ /dev/null @@ -1,17 +0,0 @@ -load("@rules_rust//rust:defs.bzl", "rust_binary") - -package(default_visibility = ["//rs:ic-os-pkg"]) - -rust_binary( - name = "vsock_guest", - srcs = ["src/main.rs"], - crate_name = "vsock_guest", - target_compatible_with = [ - "@platforms//os:linux", - ], - deps = [ - # Keep sorted. - "//rs/ic_os/vsock/vsock_lib", - "@crate_index//:clap", - ], -) diff --git a/rs/ic_os/vsock/guest/Cargo.toml b/rs/ic_os/vsock/guest/Cargo.toml deleted file mode 100644 index 423c5317437e..000000000000 --- a/rs/ic_os/vsock/guest/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "vsock_guest" -version = "1.0.0" -edition.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[target.'cfg(target_os = "linux")'.dependencies] -clap = { workspace = true } -vsock_lib = { path = "../vsock_lib" } diff --git a/rs/ic_os/vsock/host/BUILD.bazel b/rs/ic_os/vsock/host/BUILD.bazel deleted file mode 100644 index 2597fa5ed2d9..000000000000 --- a/rs/ic_os/vsock/host/BUILD.bazel +++ /dev/null @@ -1,16 +0,0 @@ -load("@rules_rust//rust:defs.bzl", "rust_binary") - -package(default_visibility = ["//rs:ic-os-pkg"]) - -rust_binary( - name = "vsock_host", - srcs = ["src/main.rs"], - crate_name = "vsock_host", - target_compatible_with = [ - "@platforms//os:linux", - ], - deps = [ - # Keep sorted. - "//rs/ic_os/vsock/vsock_lib", - ], -) diff --git a/rs/ic_os/vsock/host/Cargo.toml b/rs/ic_os/vsock/host/Cargo.toml deleted file mode 100644 index 114818d5246b..000000000000 --- a/rs/ic_os/vsock/host/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "vsock_host" -version = "1.0.0" -edition.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[target.'cfg(target_os = "linux")'.dependencies] -vsock_lib = { path = "../vsock_lib" } \ No newline at end of file diff --git a/rs/ic_os/vsock/guest/src/main.rs b/rs/ic_os/vsock/src/bin/guest.rs similarity index 100% rename from rs/ic_os/vsock/guest/src/main.rs rename to rs/ic_os/vsock/src/bin/guest.rs diff --git a/rs/ic_os/vsock/host/src/main.rs b/rs/ic_os/vsock/src/bin/host.rs similarity index 100% rename from rs/ic_os/vsock/host/src/main.rs rename to rs/ic_os/vsock/src/bin/host.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/guest/client.rs b/rs/ic_os/vsock/src/guest/client.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/guest/client.rs rename to rs/ic_os/vsock/src/guest/client.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/guest/mod.rs b/rs/ic_os/vsock/src/guest/mod.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/guest/mod.rs rename to rs/ic_os/vsock/src/guest/mod.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/host/agent.rs b/rs/ic_os/vsock/src/host/agent.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/host/agent.rs rename to rs/ic_os/vsock/src/host/agent.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/host/command_utilities.rs b/rs/ic_os/vsock/src/host/command_utilities.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/host/command_utilities.rs rename to rs/ic_os/vsock/src/host/command_utilities.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/host/hsm.rs b/rs/ic_os/vsock/src/host/hsm.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/host/hsm.rs rename to rs/ic_os/vsock/src/host/hsm.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/host/mod.rs b/rs/ic_os/vsock/src/host/mod.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/host/mod.rs rename to rs/ic_os/vsock/src/host/mod.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/host/server.rs b/rs/ic_os/vsock/src/host/server.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/host/server.rs rename to rs/ic_os/vsock/src/host/server.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/lib.rs b/rs/ic_os/vsock/src/lib.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/lib.rs rename to rs/ic_os/vsock/src/lib.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/protocol/mod.rs b/rs/ic_os/vsock/src/protocol/mod.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/protocol/mod.rs rename to rs/ic_os/vsock/src/protocol/mod.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/protocol/structures.rs b/rs/ic_os/vsock/src/protocol/structures.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/protocol/structures.rs rename to rs/ic_os/vsock/src/protocol/structures.rs diff --git a/rs/ic_os/vsock/vsock_lib/src/protocol/utils.rs b/rs/ic_os/vsock/src/protocol/utils.rs similarity index 100% rename from rs/ic_os/vsock/vsock_lib/src/protocol/utils.rs rename to rs/ic_os/vsock/src/protocol/utils.rs From f51bad67668230104057382a0d0fd8f9513ef87f Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Thu, 20 Aug 2026 01:07:21 +0000 Subject: [PATCH 02/11] Extract tests --- rs/ic_os/vsock/src/protocol/mod.rs | 3 + rs/ic_os/vsock/src/protocol/tests.rs | 197 ++++++++++++++++++++++++++ rs/ic_os/vsock/src/protocol/utils.rs | 202 --------------------------- 3 files changed, 200 insertions(+), 202 deletions(-) create mode 100644 rs/ic_os/vsock/src/protocol/tests.rs diff --git a/rs/ic_os/vsock/src/protocol/mod.rs b/rs/ic_os/vsock/src/protocol/mod.rs index 195359284b84..c6d244c60bef 100644 --- a/rs/ic_os/vsock/src/protocol/mod.rs +++ b/rs/ic_os/vsock/src/protocol/mod.rs @@ -3,3 +3,6 @@ mod utils; pub use structures::*; pub use utils::*; + +#[cfg(test)] +mod tests; diff --git a/rs/ic_os/vsock/src/protocol/tests.rs b/rs/ic_os/vsock/src/protocol/tests.rs new file mode 100644 index 000000000000..4f8bb58d3a25 --- /dev/null +++ b/rs/ic_os/vsock/src/protocol/tests.rs @@ -0,0 +1,197 @@ +use crate::protocol::structures::*; +use crate::protocol::utils::*; + +#[test] +fn test_request_serialization() { + assert_eq!( + serde_json::json!({ + "sender_cid": 1_u32, + "message": "attach-hsm" + }), + serde_json::to_value(&Request { + guest_cid: 1, + command: Command::AttachHSM + }) + .unwrap() + ); + assert_eq!( + serde_json::json!({ + "sender_cid": 1_u32, + "message": "detach-hsm" + }), + serde_json::to_value(&Request { + guest_cid: 1, + command: Command::DetachHSM + }) + .unwrap() + ); + assert_eq!( + serde_json::json!({ + "sender_cid": 1_u32, + "message": { + "notify": { + "count": 1_i32, + "message": "Hello World", + } + } + }), + serde_json::to_value(Request { + guest_cid: 1, + command: Command::Notify(NotifyData { + count: 1, + message: "Hello World".to_string(), + }) + }) + .unwrap() + ); + assert_eq!( + serde_json::json!({ + "sender_cid": 1_u32, + "message": { + "upgrade": { + "url": "https://example.com", + "target-hash": "0x1111222233334444" + } + } + }), + serde_json::to_value(Request { + guest_cid: 1, + command: Command::Upgrade(UpgradeData { + url: "https://example.com".to_string(), + target_hash: "0x1111222233334444".to_string() + }) + }) + .unwrap() + ); +} + +#[test] +fn test_response_serialization() { + let response: Response = Ok(Payload::NoPayload); + assert_eq!( + serde_json::json!({ + "Ok": "NoPayload", + }), + serde_json::to_value(response).unwrap() + ); + + let vsock_version: HostOSVsockVersion = HostOSVsockVersion { + major: 1, + minor: 0, + patch: 0, + }; + let response: Response = Ok(Payload::HostOSVsockVersion(vsock_version)); + + let expected_json = serde_json::json!({ + "Ok": { + "HostOSVsockVersion": { + "major": 1, + "minor": 0, + "patch": 0 + } + } + }); + assert_eq!(expected_json, serde_json::to_value(response).unwrap()); + + let response: Response = Ok(Payload::HostOSVersion("VERSION".to_string())); + let expected_json = serde_json::json!({ + "Ok": { + "HostOSVersion": "VERSION", + } + }); + assert_eq!(expected_json, serde_json::to_value(response).unwrap()); +} + +#[test] +fn test_parse_request() { + // Test AttachHSM command + let json_str = r#"{"sender_cid": 123, "message": "attach-hsm"}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.command, Command::AttachHSM); + + // Test DetachHSM command + let json_str = r#"{"sender_cid": 123, "message": "detach-hsm"}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.command, Command::DetachHSM); + + // Test Upgrade command + let json_str = r#"{"sender_cid": 123, "message": {"upgrade": {"url": "http://example.com/upgrade", "target-hash": "abcd1234hash"}}}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.guest_cid, 123); + match request.command { + Command::Upgrade(data) => { + assert_eq!(data.url, "http://example.com/upgrade"); + assert_eq!(data.target_hash, "abcd1234hash"); + } + _ => panic!("Expected Upgrade command"), + } + + // Test Notify command + let json_str = r#"{"sender_cid": 123, "message": {"notify": {"message": "System update required", "count": 2}}}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.guest_cid, 123); + match request.command { + Command::Notify(data) => { + assert_eq!(data.count, 2); + assert_eq!(data.message, "System update required"); + } + _ => panic!("Expected Notify command"), + } + + // Test GetVsockProtocol command + let json_str = r#"{"sender_cid": 123, "message": "GetVsockProtocol"}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.guest_cid, 123); + assert_eq!(request.command, Command::GetVsockProtocol); + + // Test GetHostOSVersion command + let json_str = r#"{"sender_cid": 123, "message": "GetHostOSVersion"}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.guest_cid, 123); + assert_eq!(request.command, Command::GetHostOSVersion); + + // Test malformed command + let json_str = r#"{"sender_cid": 123, "message": "attach-hsm"#; // Missing closing brace + let request = parse_request(json_str); + assert!(request.is_err()); +} + +#[test] +fn test_parse_response() { + assert_eq!( + Ok(Payload::NoPayload), + parse_response("{\"Ok\":\"NoPayload\"}") + ); + assert_eq!( + Ok(Payload::HostOSVersion("123".to_string())), + parse_response("{\"Ok\":{\"HostOSVersion\":\"123\"}}") + ); + assert_eq!( + Ok(Payload::HostOSVsockVersion(HostOSVsockVersion { + major: 1, + minor: 0, + patch: 0, + })), + parse_response("{\"Ok\":{\"HostOSVsockVersion\":{\"major\":1,\"minor\":0,\"patch\":0}}}") + ); + assert_eq!( + Err("Unable to parse host response: Error response".to_string()), + parse_response("Error response") + ); + + let json_str = r#"{"Ok":"NoPayload"#; // Missing closing brace + let response = parse_response(json_str); + assert!(response.is_err()); +} diff --git a/rs/ic_os/vsock/src/protocol/utils.rs b/rs/ic_os/vsock/src/protocol/utils.rs index 089546322239..f174defe4df7 100644 --- a/rs/ic_os/vsock/src/protocol/utils.rs +++ b/rs/ic_os/vsock/src/protocol/utils.rs @@ -13,205 +13,3 @@ pub fn parse_request(json_str: &str) -> Result { serde_json::from_str::(json_str) .map_err(|error| format!("Unable to parse guest request: {json_str}: {error}")) } - -#[cfg(test)] -pub mod tests { - use super::*; - - #[test] - fn test_request_serialization() { - assert_eq!( - serde_json::json!({ - "sender_cid": 1_u32, - "message": "attach-hsm" - }), - serde_json::to_value(&Request { - guest_cid: 1, - command: Command::AttachHSM - }) - .unwrap() - ); - assert_eq!( - serde_json::json!({ - "sender_cid": 1_u32, - "message": "detach-hsm" - }), - serde_json::to_value(&Request { - guest_cid: 1, - command: Command::DetachHSM - }) - .unwrap() - ); - assert_eq!( - serde_json::json!({ - "sender_cid": 1_u32, - "message": { - "notify": { - "count": 1_i32, - "message": "Hello World", - } - } - }), - serde_json::to_value(Request { - guest_cid: 1, - command: Command::Notify(NotifyData { - count: 1, - message: "Hello World".to_string(), - }) - }) - .unwrap() - ); - assert_eq!( - serde_json::json!({ - "sender_cid": 1_u32, - "message": { - "upgrade": { - "url": "https://example.com", - "target-hash": "0x1111222233334444" - } - } - }), - serde_json::to_value(Request { - guest_cid: 1, - command: Command::Upgrade(UpgradeData { - url: "https://example.com".to_string(), - target_hash: "0x1111222233334444".to_string() - }) - }) - .unwrap() - ); - } - - #[test] - fn test_response_serialization() { - let response: Response = Ok(Payload::NoPayload); - assert_eq!( - serde_json::json!({ - "Ok": "NoPayload", - }), - serde_json::to_value(response).unwrap() - ); - - let vsock_version: HostOSVsockVersion = HostOSVsockVersion { - major: 1, - minor: 0, - patch: 0, - }; - let response: Response = Ok(Payload::HostOSVsockVersion(vsock_version)); - - let expected_json = serde_json::json!({ - "Ok": { - "HostOSVsockVersion": { - "major": 1, - "minor": 0, - "patch": 0 - } - } - }); - assert_eq!(expected_json, serde_json::to_value(response).unwrap()); - - let response: Response = Ok(Payload::HostOSVersion("VERSION".to_string())); - let expected_json = serde_json::json!({ - "Ok": { - "HostOSVersion": "VERSION", - } - }); - assert_eq!(expected_json, serde_json::to_value(response).unwrap()); - } - - #[test] - fn test_parse_request() { - // Test AttachHSM command - let json_str = r#"{"sender_cid": 123, "message": "attach-hsm"}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.command, Command::AttachHSM); - - // Test DetachHSM command - let json_str = r#"{"sender_cid": 123, "message": "detach-hsm"}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.command, Command::DetachHSM); - - // Test Upgrade command - let json_str = r#"{"sender_cid": 123, "message": {"upgrade": {"url": "http://example.com/upgrade", "target-hash": "abcd1234hash"}}}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.guest_cid, 123); - match request.command { - Command::Upgrade(data) => { - assert_eq!(data.url, "http://example.com/upgrade"); - assert_eq!(data.target_hash, "abcd1234hash"); - } - _ => panic!("Expected Upgrade command"), - } - - // Test Notify command - let json_str = r#"{"sender_cid": 123, "message": {"notify": {"message": "System update required", "count": 2}}}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.guest_cid, 123); - match request.command { - Command::Notify(data) => { - assert_eq!(data.count, 2); - assert_eq!(data.message, "System update required"); - } - _ => panic!("Expected Notify command"), - } - - // Test GetVsockProtocol command - let json_str = r#"{"sender_cid": 123, "message": "GetVsockProtocol"}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.guest_cid, 123); - assert_eq!(request.command, Command::GetVsockProtocol); - - // Test GetHostOSVersion command - let json_str = r#"{"sender_cid": 123, "message": "GetHostOSVersion"}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.guest_cid, 123); - assert_eq!(request.command, Command::GetHostOSVersion); - - // Test malformed command - let json_str = r#"{"sender_cid": 123, "message": "attach-hsm"#; // Missing closing brace - let request = parse_request(json_str); - assert!(request.is_err()); - } - - #[test] - fn test_parse_response() { - assert_eq!( - Ok(Payload::NoPayload), - parse_response("{\"Ok\":\"NoPayload\"}") - ); - assert_eq!( - Ok(Payload::HostOSVersion("123".to_string())), - parse_response("{\"Ok\":{\"HostOSVersion\":\"123\"}}") - ); - assert_eq!( - Ok(Payload::HostOSVsockVersion(HostOSVsockVersion { - major: 1, - minor: 0, - patch: 0, - })), - parse_response( - "{\"Ok\":{\"HostOSVsockVersion\":{\"major\":1,\"minor\":0,\"patch\":0}}}" - ) - ); - assert_eq!( - Err("Unable to parse host response: Error response".to_string()), - parse_response("Error response") - ); - - let json_str = r#"{"Ok":"NoPayload"#; // Missing closing brace - let response = parse_response(json_str); - assert!(response.is_err()); - } -} From b2eea89c8963646dc30ca20f52dd3e1c6be655ec Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Thu, 20 Aug 2026 01:07:39 +0000 Subject: [PATCH 03/11] Inline client --- rs/ic_os/vsock/src/client.rs | 86 ++++++++++++++++++++++++++++++ rs/ic_os/vsock/src/guest/client.rs | 42 --------------- rs/ic_os/vsock/src/guest/mod.rs | 47 ---------------- rs/ic_os/vsock/src/lib.rs | 6 +-- 4 files changed, 89 insertions(+), 92 deletions(-) create mode 100644 rs/ic_os/vsock/src/client.rs delete mode 100644 rs/ic_os/vsock/src/guest/client.rs delete mode 100644 rs/ic_os/vsock/src/guest/mod.rs diff --git a/rs/ic_os/vsock/src/client.rs b/rs/ic_os/vsock/src/client.rs new file mode 100644 index 000000000000..ff9aa615cd68 --- /dev/null +++ b/rs/ic_os/vsock/src/client.rs @@ -0,0 +1,86 @@ +use crate::protocol::{Command, Response}; +use mockall::automock; + +#[automock] +pub trait VSockClient { + fn send_command(&self, command: Command) -> Response; +} + +#[cfg(target_os = "linux")] +pub use linux::*; + +#[cfg(target_os = "linux")] +mod linux { + use super::*; + use crate::protocol::{Request, Response, parse_response}; + use std::io::{Read, Write}; + use vsock::{VMADDR_CID_HOST, VsockStream}; + + pub struct LinuxVSockClient { + port: u32, + } + + impl LinuxVSockClient { + pub const DEFAULT_PORT: u32 = 19090; + + pub fn with_port(port: u32) -> Self { + Self { port } + } + } + + impl Default for LinuxVSockClient { + fn default() -> Self { + Self::with_port(Self::DEFAULT_PORT) + } + } + + impl VSockClient for LinuxVSockClient { + fn send_command(&self, command: Command) -> Response { + let port = self.port; + let guest_cid = vsock::get_local_cid().map_err(|e| e.to_string())?; + + let request = Request { guest_cid, command }; + + send_request_to_host_and_parse_response(&request, &port) + } + } + + pub fn send_request_to_host_and_parse_response(request: &Request, port: &u32) -> Response { + let response_str = send_request_to_host(request, port)?; + + parse_response(response_str.as_str()) + } + + pub fn send_request_to_host(request: &Request, port: &u32) -> Result { + let mut stream = create_stream(port).map_err(|e| e.to_string())?; + + let json_request = serde_json::to_string(request).map_err(|e| e.to_string())?; + stream + .write_all(json_request.as_bytes()) + .map_err(|e| e.to_string())?; + + let read_result = read_response_from_host(&mut stream); + + read_result.map_err(|e| e.to_string()) + } + + fn read_response_from_host(stream: &mut VsockStream) -> anyhow::Result { + // 64 KiB - generous for current responses (typically <1 KiB) while + // preventing unbounded allocation from a misbehaving host. + const MAX_RESPONSE_SIZE: u64 = 64 * 1024; + let mut response = String::new(); + stream + .take(MAX_RESPONSE_SIZE) + .read_to_string(&mut response)?; + Ok(response) + } + + fn create_stream(port: &u32) -> Result { + let stream = VsockStream::connect_with_cid_port(VMADDR_CID_HOST, *port)?; + // Set a long timeout, so HostOS has enough time to upgrade. + stream.set_write_timeout(Some(std::time::Duration::from_secs(60 * 5)))?; + stream.set_read_timeout(Some(std::time::Duration::from_secs(60 * 5)))?; + + Ok(stream) + } +} diff --git a/rs/ic_os/vsock/src/guest/client.rs b/rs/ic_os/vsock/src/guest/client.rs deleted file mode 100644 index 6b30104a1a20..000000000000 --- a/rs/ic_os/vsock/src/guest/client.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::protocol::{Request, Response, parse_response}; -use std::io::{Read, Write}; -use vsock::{VMADDR_CID_HOST, VsockStream}; - -pub fn send_request_to_host_and_parse_response(request: &Request, port: &u32) -> Response { - let response_str = send_request_to_host(request, port)?; - - parse_response(response_str.as_str()) -} - -pub fn send_request_to_host(request: &Request, port: &u32) -> Result { - let mut stream = create_stream(port).map_err(|e| e.to_string())?; - - let json_request = serde_json::to_string(request).map_err(|e| e.to_string())?; - stream - .write_all(json_request.as_bytes()) - .map_err(|e| e.to_string())?; - - let read_result = read_response_from_host(&mut stream); - - read_result.map_err(|e| e.to_string()) -} - -fn read_response_from_host(stream: &mut VsockStream) -> anyhow::Result { - // 64 KiB - generous for current responses (typically <1 KiB) while - // preventing unbounded allocation from a misbehaving host. - const MAX_RESPONSE_SIZE: u64 = 64 * 1024; - let mut response = String::new(); - stream - .take(MAX_RESPONSE_SIZE) - .read_to_string(&mut response)?; - Ok(response) -} - -fn create_stream(port: &u32) -> Result { - let stream = VsockStream::connect_with_cid_port(VMADDR_CID_HOST, *port)?; - // Set a long timeout, so HostOS has enough time to upgrade. - stream.set_write_timeout(Some(std::time::Duration::from_secs(60 * 5)))?; - stream.set_read_timeout(Some(std::time::Duration::from_secs(60 * 5)))?; - - Ok(stream) -} diff --git a/rs/ic_os/vsock/src/guest/mod.rs b/rs/ic_os/vsock/src/guest/mod.rs deleted file mode 100644 index f1b160091078..000000000000 --- a/rs/ic_os/vsock/src/guest/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -#[cfg(target_os = "linux")] -mod client; - -use crate::protocol::{Command, Request, Response}; -use mockall::automock; - -#[cfg(target_os = "linux")] -pub use linux::*; - -#[automock] -pub trait VSockClient { - fn send_command(&self, command: Command) -> Response; -} - -#[cfg(target_os = "linux")] -mod linux { - use super::*; - - pub struct LinuxVSockClient { - port: u32, - } - - impl LinuxVSockClient { - pub const DEFAULT_PORT: u32 = 19090; - - pub fn with_port(port: u32) -> Self { - Self { port } - } - } - - impl Default for LinuxVSockClient { - fn default() -> Self { - Self::with_port(Self::DEFAULT_PORT) - } - } - - impl VSockClient for LinuxVSockClient { - fn send_command(&self, command: Command) -> Response { - let port = self.port; - let guest_cid = vsock::get_local_cid().map_err(|e| e.to_string())?; - - let request = Request { guest_cid, command }; - - client::send_request_to_host_and_parse_response(&request, &port) - } - } -} diff --git a/rs/ic_os/vsock/src/lib.rs b/rs/ic_os/vsock/src/lib.rs index f9ec55270f14..fb4ca55cd808 100644 --- a/rs/ic_os/vsock/src/lib.rs +++ b/rs/ic_os/vsock/src/lib.rs @@ -1,7 +1,7 @@ -mod guest; +mod client; #[cfg(target_os = "linux")] -pub use guest::LinuxVSockClient; -pub use guest::{MockVSockClient, VSockClient}; +pub use client::LinuxVSockClient; +pub use client::{MockVSockClient, VSockClient}; #[cfg(target_os = "linux")] mod host; From 8718ff623e7ada8a217dd6430ecfcee58ed7bc24 Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Thu, 20 Aug 2026 01:07:50 +0000 Subject: [PATCH 04/11] Inline server --- rs/ic_os/vsock/src/host/mod.rs | 4 - rs/ic_os/vsock/src/host/server.rs | 105 ------------- rs/ic_os/vsock/src/lib.rs | 4 +- .../src/{host => server}/command_utilities.rs | 0 rs/ic_os/vsock/src/{host => server}/hsm.rs | 2 +- .../src/{host/agent.rs => server/mod.rs} | 146 +++++++++++++++--- 6 files changed, 131 insertions(+), 130 deletions(-) delete mode 100644 rs/ic_os/vsock/src/host/mod.rs delete mode 100644 rs/ic_os/vsock/src/host/server.rs rename rs/ic_os/vsock/src/{host => server}/command_utilities.rs (100%) rename rs/ic_os/vsock/src/{host => server}/hsm.rs (98%) rename rs/ic_os/vsock/src/{host/agent.rs => server/mod.rs} (57%) diff --git a/rs/ic_os/vsock/src/host/mod.rs b/rs/ic_os/vsock/src/host/mod.rs deleted file mode 100644 index 7ed0508ad481..000000000000 --- a/rs/ic_os/vsock/src/host/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod agent; -mod command_utilities; -mod hsm; -pub(crate) mod server; diff --git a/rs/ic_os/vsock/src/host/server.rs b/rs/ic_os/vsock/src/host/server.rs deleted file mode 100644 index f9331cef1e6a..000000000000 --- a/rs/ic_os/vsock/src/host/server.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::host::agent::dispatch; -use crate::protocol::{Request, Response, parse_request}; -use std::io::{Error, ErrorKind, Read, Result, Write}; -use vsock::{VMADDR_CID_ANY, VsockAddr, VsockListener, VsockStream}; - -// The first CID available for guests to use. This is used later to enforce -// that only the first guest is able to connect over VSOCK, for now. -const VIR_VSOCK_GUEST_CID_MIN: u32 = 3; -const DEFAULT_PORT: u32 = 19090; - -/// Runs the vsock server and awaits incoming vsock connections. -pub fn run_server() -> Result<()> { - let vsock_listener: VsockListener = create_vsock_listener()?; - - println!("Listening for vsock connection.\n"); - - for stream in vsock_listener.incoming() { - let mut stream: VsockStream = stream?; - stream.set_write_timeout(Some(std::time::Duration::from_secs(5)))?; - stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?; - - std::thread::spawn(move || -> Result<()> { process_connection(&mut stream) }); - } - - Ok(()) -} - -fn create_vsock_listener() -> Result { - // Only listen for the first GuestOS VM. Only type4.* nodes will have more - // than one VM that uses VSOCK. We treat the first GuestOS as the leader in - // charge of HostOS. - let addr = VsockAddr::new(VMADDR_CID_ANY, DEFAULT_PORT); - VsockListener::bind(&addr) -} - -fn process_connection(stream: &mut VsockStream) -> Result<()> { - let request = match get_request(stream) { - Ok(request) => request, - Err(err) => { - send_response(stream, &Err(err.to_string()))?; - return Err(err); - } - }; - println!("Received vsock request: {request}"); - - if request.guest_cid != VIR_VSOCK_GUEST_CID_MIN { - let err = Error::new( - ErrorKind::ConnectionRefused, - "A type4 host only accepts VSOCK connections from the first VM.", - ); - send_response(stream, &Err(err.to_string()))?; - return Err(err); - }; - - if let Err(err) = verify_sender_cid(stream, request.guest_cid) { - send_response(stream, &Err(err.to_string()))?; - return Err(err); - }; - - let response: Response = dispatch(&request.command); - - send_response(stream, &response) -} - -fn get_request(stream: &mut VsockStream) -> Result { - let mut buffer = [0; 4096]; - let bytes_read = stream.read(&mut buffer)?; - let json_request: String = match std::str::from_utf8(&buffer[..bytes_read]) { - Ok(json_str_request) => json_str_request.to_string(), - Err(error) => { - println!("Error converting bytes to string: {error}"); - return Err(Error::new(ErrorKind::InvalidData, error)); - } - }; - - parse_request(json_request.as_str()).map_err(|e| Error::new(ErrorKind::InvalidInput, e)) -} - -// As a sanity check, we request that the sender adds its own CID to the message, and that CID must match the CID in the stream peer address. -// NOTE: The kernel vhost driver also enforces this. Any packet with a forged source is dropped. -fn verify_sender_cid(stream: &mut VsockStream, guest_cid: u32) -> Result<()> { - let peer_address = match stream.peer_addr() { - Ok(peer_address) => peer_address, - Err(err) => { - let error = format!("Error: could not verify the sender_cid. {err}"); - return Err(Error::new(ErrorKind::InvalidData, error)); - } - }; - - if peer_address.cid() == guest_cid { - Ok(()) - } else { - Err(Error::new( - ErrorKind::InvalidData, - "The actual sender CID did not match the sender CID in the request object", - )) - } -} - -fn send_response(stream: &mut VsockStream, response: &Response) -> Result<()> { - let json_response = serde_json::to_string(&response)?; - stream.write_all(json_response.as_bytes())?; - - Ok(()) -} diff --git a/rs/ic_os/vsock/src/lib.rs b/rs/ic_os/vsock/src/lib.rs index fb4ca55cd808..885ebeec178f 100644 --- a/rs/ic_os/vsock/src/lib.rs +++ b/rs/ic_os/vsock/src/lib.rs @@ -4,8 +4,8 @@ pub use client::LinuxVSockClient; pub use client::{MockVSockClient, VSockClient}; #[cfg(target_os = "linux")] -mod host; +mod server; #[cfg(target_os = "linux")] -pub use host::server::run_server; +pub use server::run_server; pub mod protocol; diff --git a/rs/ic_os/vsock/src/host/command_utilities.rs b/rs/ic_os/vsock/src/server/command_utilities.rs similarity index 100% rename from rs/ic_os/vsock/src/host/command_utilities.rs rename to rs/ic_os/vsock/src/server/command_utilities.rs diff --git a/rs/ic_os/vsock/src/host/hsm.rs b/rs/ic_os/vsock/src/server/hsm.rs similarity index 98% rename from rs/ic_os/vsock/src/host/hsm.rs rename to rs/ic_os/vsock/src/server/hsm.rs index a7ddf179c0b6..3a587c0b0513 100644 --- a/rs/ic_os/vsock/src/host/hsm.rs +++ b/rs/ic_os/vsock/src/server/hsm.rs @@ -1,4 +1,4 @@ -use crate::host::command_utilities::handle_command_output; +use super::command_utilities::handle_command_output; use crate::protocol::Response; use rusb::{Context, Device, UsbContext}; use std::io::{Error, Write}; diff --git a/rs/ic_os/vsock/src/host/agent.rs b/rs/ic_os/vsock/src/server/mod.rs similarity index 57% rename from rs/ic_os/vsock/src/host/agent.rs rename to rs/ic_os/vsock/src/server/mod.rs index fffc72451bd5..22581a27c11e 100644 --- a/rs/ic_os/vsock/src/host/agent.rs +++ b/rs/ic_os/vsock/src/server/mod.rs @@ -1,12 +1,135 @@ -use crate::host::command_utilities::handle_command_output; -use crate::host::hsm::{attach_hsm, detach_hsm}; -use crate::protocol::{Command, HostOSVsockVersion, NotifyData, Payload, Response, UpgradeData}; -use ic_http_utils::file_downloader::FileDownloader; use std::fs::OpenOptions; -use std::io::Write; +use std::io::{self, Read, Write}; use std::path::Path; use std::time::Duration; + +mod command_utilities; +mod hsm; + +use crate::protocol::{ + Command, HostOSVsockVersion, NotifyData, Payload, Request, Response, UpgradeData, parse_request, +}; +use command_utilities::handle_command_output; +use hsm::{attach_hsm, detach_hsm}; + +use ic_http_utils::file_downloader::FileDownloader; + use tokio::runtime::Runtime; +use vsock::{VMADDR_CID_ANY, VsockAddr, VsockListener, VsockStream}; + +// The first CID available for guests to use. This is used later to enforce +// that only the first guest is able to connect over VSOCK, for now. +const VIR_VSOCK_GUEST_CID_MIN: u32 = 3; +const DEFAULT_PORT: u32 = 19090; + +// get_hostos_version +const HOSTOS_VERSION_FILE_PATH: &str = "/opt/ic/share/version.txt"; + +// upgrade +const UPGRADE_FILE_PATH: &str = "/tmp/upgrade"; +const INSTALL_UPGRADE_FILE_PATH: &str = "/opt/ic/bin/install-upgrade.sh"; + +const VSOCK_VERSION: HostOSVsockVersion = HostOSVsockVersion { + major: 1, + minor: 0, + patch: 0, +}; + +/// Runs the vsock server and awaits incoming vsock connections. +pub fn run_server() -> io::Result<()> { + let vsock_listener: VsockListener = create_vsock_listener()?; + + println!("Listening for vsock connection.\n"); + + for stream in vsock_listener.incoming() { + let mut stream: VsockStream = stream?; + stream.set_write_timeout(Some(std::time::Duration::from_secs(5)))?; + stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?; + + std::thread::spawn(move || -> io::Result<()> { process_connection(&mut stream) }); + } + + Ok(()) +} + +fn create_vsock_listener() -> io::Result { + // Only listen for the first GuestOS VM. Only type4.* nodes will have more + // than one VM that uses VSOCK. We treat the first GuestOS as the leader in + // charge of HostOS. + let addr = VsockAddr::new(VMADDR_CID_ANY, DEFAULT_PORT); + VsockListener::bind(&addr) +} + +fn process_connection(stream: &mut VsockStream) -> io::Result<()> { + let request = match get_request(stream) { + Ok(request) => request, + Err(err) => { + send_response(stream, &Err(err.to_string()))?; + return Err(err); + } + }; + println!("Received vsock request: {request}"); + + if request.guest_cid != VIR_VSOCK_GUEST_CID_MIN { + let err = io::Error::new( + io::ErrorKind::ConnectionRefused, + "A type4 host only accepts VSOCK connections from the first VM.", + ); + send_response(stream, &Err(err.to_string()))?; + return Err(err); + }; + + if let Err(err) = verify_sender_cid(stream, request.guest_cid) { + send_response(stream, &Err(err.to_string()))?; + return Err(err); + }; + + let response: Response = dispatch(&request.command); + + send_response(stream, &response) +} + +fn get_request(stream: &mut VsockStream) -> io::Result { + let mut buffer = [0; 4096]; + let bytes_read = stream.read(&mut buffer)?; + let json_request: String = match std::str::from_utf8(&buffer[..bytes_read]) { + Ok(json_str_request) => json_str_request.to_string(), + Err(error) => { + println!("Error converting bytes to string: {error}"); + return Err(io::Error::new(io::ErrorKind::InvalidData, error)); + } + }; + + parse_request(json_request.as_str()).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e)) +} + +// As a sanity check, we request that the sender adds its own CID to the message, and that CID must match the CID in the stream peer address. +// NOTE: The kernel vhost driver also enforces this. Any packet with a forged source is dropped. +fn verify_sender_cid(stream: &mut VsockStream, guest_cid: u32) -> io::Result<()> { + let peer_address = match stream.peer_addr() { + Ok(peer_address) => peer_address, + Err(err) => { + let error = format!("Error: could not verify the sender_cid. {err}"); + return Err(io::Error::new(io::ErrorKind::InvalidData, error)); + } + }; + + if peer_address.cid() == guest_cid { + Ok(()) + } else { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "The actual sender CID did not match the sender CID in the request object", + )) + } +} + +fn send_response(stream: &mut VsockStream, response: &Response) -> io::Result<()> { + let json_response = serde_json::to_string(&response)?; + stream.write_all(json_response.as_bytes())?; + + Ok(()) +} pub fn dispatch(command: &Command) -> Response { use Command::*; @@ -24,19 +147,6 @@ pub fn dispatch(command: &Command) -> Response { } } -// get_hostos_version -const HOSTOS_VERSION_FILE_PATH: &str = "/opt/ic/share/version.txt"; - -// upgrade -const UPGRADE_FILE_PATH: &str = "/tmp/upgrade"; -const INSTALL_UPGRADE_FILE_PATH: &str = "/opt/ic/bin/install-upgrade.sh"; - -const VSOCK_VERSION: HostOSVsockVersion = HostOSVsockVersion { - major: 1, - minor: 0, - patch: 0, -}; - fn get_hostos_version() -> Response { let version = std::fs::read_to_string(HOSTOS_VERSION_FILE_PATH) .map_err(|_| "Could not read hostOS version".to_string())?; From 32accdc5c3ff8dc083bfcbae89a0ff808275cfd2 Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Thu, 20 Aug 2026 01:09:10 +0000 Subject: [PATCH 05/11] Inline structures --- rs/ic_os/vsock/src/protocol/mod.rs | 112 +++++++++++++++++++++- rs/ic_os/vsock/src/protocol/structures.rs | 104 -------------------- rs/ic_os/vsock/src/protocol/tests.rs | 4 +- rs/ic_os/vsock/src/protocol/utils.rs | 2 +- 4 files changed, 111 insertions(+), 111 deletions(-) delete mode 100644 rs/ic_os/vsock/src/protocol/structures.rs diff --git a/rs/ic_os/vsock/src/protocol/mod.rs b/rs/ic_os/vsock/src/protocol/mod.rs index c6d244c60bef..4d27903d187d 100644 --- a/rs/ic_os/vsock/src/protocol/mod.rs +++ b/rs/ic_os/vsock/src/protocol/mod.rs @@ -1,8 +1,112 @@ -mod structures; +use std::fmt; + +#[cfg(test)] +mod tests; mod utils; -pub use structures::*; pub use utils::*; -#[cfg(test)] -mod tests; +use serde::{Deserialize, Serialize}; + +pub type Response = Result; + +#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] +pub enum Payload { + HostOSVsockVersion(HostOSVsockVersion), + HostOSVersion(String), + NoPayload, +} + +impl fmt::Display for Payload { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Payload::HostOSVsockVersion(version) => write!(f, "HostOSVsockVersion({version})"), + Payload::HostOSVersion(version) => write!(f, "HostOSVersion({version})"), + Payload::NoPayload => write!(f, "NoPayload"), + } + } +} + +#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] +pub struct Request { + #[serde(rename = "sender_cid")] + pub guest_cid: u32, + #[serde(rename = "message")] + pub command: Command, +} + +impl fmt::Display for Request { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Request {{ sender_cid: {}, command: {} }}", + self.guest_cid, self.command + ) + } +} + +/// All commands that can be sent to the Host server +#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] +pub enum Command { + #[serde(rename = "attach-hsm")] + AttachHSM, + #[serde(rename = "detach-hsm")] + DetachHSM, + #[serde(rename = "upgrade")] + Upgrade(UpgradeData), + #[serde(rename = "notify")] + Notify(NotifyData), + GetVsockProtocol, + GetHostOSVersion, + /// Start the Upgrade Guest VM. If it's already running, the VM will be stopped and restarted. + #[serde(rename = "start-upgrade-guest-vm")] + StartUpgradeGuestVM, +} + +impl fmt::Display for Command { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Command::AttachHSM => write!(f, "Command: Attach HSM"), + Command::DetachHSM => write!(f, "Command: Detach HSM"), + Command::Upgrade(upgrade_data) => write!( + f, + "Command: Upgrade\nURL: {}\nHASH: {}", + upgrade_data.url, upgrade_data.target_hash + ), + Command::Notify(notify_data) => write!( + f, + "Command: Notify\nMessage: {}\nCount: {}", + notify_data.message, notify_data.count + ), + Command::GetVsockProtocol => write!(f, "Command: Get Vsock Protocol"), + Command::GetHostOSVersion => write!(f, "Command: Get HostOS Version"), + Command::StartUpgradeGuestVM => write!(f, "Command: Start Upgrade Guest VM"), + } + } +} + +#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] +pub struct HostOSVsockVersion { + pub major: u32, + pub minor: u32, + pub patch: u32, +} + +impl fmt::Display for HostOSVsockVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] +pub struct UpgradeData { + pub url: String, + #[serde(rename = "target-hash")] + pub target_hash: String, +} + +#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] +pub struct NotifyData { + pub count: u32, + pub message: String, +} diff --git a/rs/ic_os/vsock/src/protocol/structures.rs b/rs/ic_os/vsock/src/protocol/structures.rs deleted file mode 100644 index 56a2290c382e..000000000000 --- a/rs/ic_os/vsock/src/protocol/structures.rs +++ /dev/null @@ -1,104 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fmt; -pub type Response = Result; - -#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] -pub enum Payload { - HostOSVsockVersion(HostOSVsockVersion), - HostOSVersion(String), - NoPayload, -} - -impl fmt::Display for Payload { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Payload::HostOSVsockVersion(version) => write!(f, "HostOSVsockVersion({version})"), - Payload::HostOSVersion(version) => write!(f, "HostOSVersion({version})"), - Payload::NoPayload => write!(f, "NoPayload"), - } - } -} - -#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] -pub struct Request { - #[serde(rename = "sender_cid")] - pub guest_cid: u32, - #[serde(rename = "message")] - pub command: Command, -} - -impl fmt::Display for Request { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "Request {{ sender_cid: {}, command: {} }}", - self.guest_cid, self.command - ) - } -} - -/// All commands that can be sent to the Host server -#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] -pub enum Command { - #[serde(rename = "attach-hsm")] - AttachHSM, - #[serde(rename = "detach-hsm")] - DetachHSM, - #[serde(rename = "upgrade")] - Upgrade(UpgradeData), - #[serde(rename = "notify")] - Notify(NotifyData), - GetVsockProtocol, - GetHostOSVersion, - /// Start the Upgrade Guest VM. If it's already running, the VM will be stopped and restarted. - #[serde(rename = "start-upgrade-guest-vm")] - StartUpgradeGuestVM, -} - -impl fmt::Display for Command { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Command::AttachHSM => write!(f, "Command: Attach HSM"), - Command::DetachHSM => write!(f, "Command: Detach HSM"), - Command::Upgrade(upgrade_data) => write!( - f, - "Command: Upgrade\nURL: {}\nHASH: {}", - upgrade_data.url, upgrade_data.target_hash - ), - Command::Notify(notify_data) => write!( - f, - "Command: Notify\nMessage: {}\nCount: {}", - notify_data.message, notify_data.count - ), - Command::GetVsockProtocol => write!(f, "Command: Get Vsock Protocol"), - Command::GetHostOSVersion => write!(f, "Command: Get HostOS Version"), - Command::StartUpgradeGuestVM => write!(f, "Command: Start Upgrade Guest VM"), - } - } -} - -#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] -pub struct HostOSVsockVersion { - pub major: u32, - pub minor: u32, - pub patch: u32, -} - -impl fmt::Display for HostOSVsockVersion { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}.{}.{}", self.major, self.minor, self.patch) - } -} - -#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] -pub struct UpgradeData { - pub url: String, - #[serde(rename = "target-hash")] - pub target_hash: String, -} - -#[derive(Eq, PartialEq, Debug, Deserialize, Serialize)] -pub struct NotifyData { - pub count: u32, - pub message: String, -} diff --git a/rs/ic_os/vsock/src/protocol/tests.rs b/rs/ic_os/vsock/src/protocol/tests.rs index 4f8bb58d3a25..39fdde2c00dd 100644 --- a/rs/ic_os/vsock/src/protocol/tests.rs +++ b/rs/ic_os/vsock/src/protocol/tests.rs @@ -1,5 +1,5 @@ -use crate::protocol::structures::*; -use crate::protocol::utils::*; +use super::utils::*; +use super::*; #[test] fn test_request_serialization() { diff --git a/rs/ic_os/vsock/src/protocol/utils.rs b/rs/ic_os/vsock/src/protocol/utils.rs index f174defe4df7..17a728710583 100644 --- a/rs/ic_os/vsock/src/protocol/utils.rs +++ b/rs/ic_os/vsock/src/protocol/utils.rs @@ -1,4 +1,4 @@ -use crate::protocol::structures::*; +use super::{Request, Response}; /// Parse a response in a json string to a `Response` struct. pub fn parse_response(json_str: &str) -> Response { From b6295823d4c2ab2e84aab443a86a176524c47973 Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Thu, 20 Aug 2026 01:09:19 +0000 Subject: [PATCH 06/11] Break out upgrade logic --- rs/ic_os/vsock/src/server/mod.rs | 86 +--------------------------- rs/ic_os/vsock/src/server/upgrade.rs | 84 +++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 83 deletions(-) create mode 100644 rs/ic_os/vsock/src/server/upgrade.rs diff --git a/rs/ic_os/vsock/src/server/mod.rs b/rs/ic_os/vsock/src/server/mod.rs index 22581a27c11e..931a7d715d3f 100644 --- a/rs/ic_os/vsock/src/server/mod.rs +++ b/rs/ic_os/vsock/src/server/mod.rs @@ -1,18 +1,15 @@ use std::fs::OpenOptions; use std::io::{self, Read, Write}; -use std::path::Path; -use std::time::Duration; mod command_utilities; mod hsm; +mod upgrade; use crate::protocol::{ - Command, HostOSVsockVersion, NotifyData, Payload, Request, Response, UpgradeData, parse_request, + Command, HostOSVsockVersion, NotifyData, Payload, Request, Response, parse_request, }; -use command_utilities::handle_command_output; use hsm::{attach_hsm, detach_hsm}; - -use ic_http_utils::file_downloader::FileDownloader; +use upgrade::{start_upgrade_guest_vm, upgrade_hostos}; use tokio::runtime::Runtime; use vsock::{VMADDR_CID_ANY, VsockAddr, VsockListener, VsockStream}; @@ -25,10 +22,6 @@ const DEFAULT_PORT: u32 = 19090; // get_hostos_version const HOSTOS_VERSION_FILE_PATH: &str = "/opt/ic/share/version.txt"; -// upgrade -const UPGRADE_FILE_PATH: &str = "/tmp/upgrade"; -const INSTALL_UPGRADE_FILE_PATH: &str = "/opt/ic/bin/install-upgrade.sh"; - const VSOCK_VERSION: HostOSVsockVersion = HostOSVsockVersion { major: 1, minor: 0, @@ -210,76 +203,3 @@ fn notify(notify_data: &NotifyData) -> Response { Ok(Payload::NoPayload) } - -async fn create_hostos_upgrade_file( - upgrade_url: &str, - file_path: &str, - target_hash: &str, -) -> Result<(), String> { - println!("Starting download from: {}", upgrade_url); - let file_downloader = FileDownloader::new_with_timeout(None, Duration::from_secs(120)); - - file_downloader - .download_file( - upgrade_url, - Path::new(file_path), - Some(target_hash.to_string()), - ) - .await - .map_err(|e| e.to_string()) -} - -fn run_upgrade() -> Response { - let command_output = std::process::Command::new(INSTALL_UPGRADE_FILE_PATH) - .arg(UPGRADE_FILE_PATH) - .output(); - - handle_command_output(command_output)?; - - // Schedule a reboot for +1 minute - let command_output = std::process::Command::new("shutdown") - .arg("--reboot") - .output(); - - handle_command_output(command_output) -} - -async fn upgrade_hostos(upgrade_data: &UpgradeData) -> Response { - println!("Trying to fetch hostOS upgrade file from request: {upgrade_data:?}"); - - create_hostos_upgrade_file( - &upgrade_data.url, - UPGRADE_FILE_PATH, - &upgrade_data.target_hash, - ) - .await?; - - println!("Download completed, starting upgrade installation..."); - run_upgrade() -} - -fn start_upgrade_guest_vm() -> Response { - const GUESTOS_UPGRADER_SERVICE: &str = "upgrade-guestos.service"; - - match std::process::Command::new("systemctl") - .arg("restart") - .arg(GUESTOS_UPGRADER_SERVICE) - .output() - { - Ok(output) if output.status.success() => return Ok(Payload::NoPayload), - Ok(_) => {} // systemctl failed, fallthrough to error handling below - Err(err) => return Err(format!("Could not start {GUESTOS_UPGRADER_SERVICE}: {err}")), - }; - - // systemctl failed, get status - let status = std::process::Command::new("journalctl") - .arg("status") - .arg(GUESTOS_UPGRADER_SERVICE) - .output() - .map(|output| String::from_utf8_lossy(&output.stdout).into_owned()) - .unwrap_or_else(|_| format!("[Could not get {GUESTOS_UPGRADER_SERVICE} status]")); - - Err(format!( - "Could not start {GUESTOS_UPGRADER_SERVICE}, status: {status}" - )) -} diff --git a/rs/ic_os/vsock/src/server/upgrade.rs b/rs/ic_os/vsock/src/server/upgrade.rs new file mode 100644 index 000000000000..71930617a44c --- /dev/null +++ b/rs/ic_os/vsock/src/server/upgrade.rs @@ -0,0 +1,84 @@ +use std::path::Path; +use std::time::Duration; + +use super::command_utilities::handle_command_output; +use crate::protocol::{Payload, Response, UpgradeData}; + +use ic_http_utils::file_downloader::FileDownloader; + +// upgrade +const UPGRADE_FILE_PATH: &str = "/tmp/upgrade"; +const INSTALL_UPGRADE_FILE_PATH: &str = "/opt/ic/bin/install-upgrade.sh"; + +async fn create_hostos_upgrade_file( + upgrade_url: &str, + file_path: &str, + target_hash: &str, +) -> Result<(), String> { + println!("Starting download from: {}", upgrade_url); + let file_downloader = FileDownloader::new_with_timeout(None, Duration::from_secs(120)); + + file_downloader + .download_file( + upgrade_url, + Path::new(file_path), + Some(target_hash.to_string()), + ) + .await + .map_err(|e| e.to_string()) +} + +fn run_upgrade() -> Response { + let command_output = std::process::Command::new(INSTALL_UPGRADE_FILE_PATH) + .arg(UPGRADE_FILE_PATH) + .output(); + + handle_command_output(command_output)?; + + // Schedule a reboot for +1 minute + let command_output = std::process::Command::new("shutdown") + .arg("--reboot") + .output(); + + handle_command_output(command_output) +} + +pub(crate) async fn upgrade_hostos(upgrade_data: &UpgradeData) -> Response { + println!("Trying to fetch hostOS upgrade file from request: {upgrade_data:?}"); + + create_hostos_upgrade_file( + &upgrade_data.url, + UPGRADE_FILE_PATH, + &upgrade_data.target_hash, + ) + .await?; + + println!("Download completed, starting upgrade installation..."); + run_upgrade() +} + +pub(crate) fn start_upgrade_guest_vm() -> Response { + const GUESTOS_UPGRADER_SERVICE: &str = "upgrade-guestos.service"; + + match std::process::Command::new("systemctl") + .arg("restart") + .arg(GUESTOS_UPGRADER_SERVICE) + .output() + { + Ok(output) if output.status.success() => return Ok(Payload::NoPayload), + Ok(_) => {} // systemctl failed, fallthrough to error handling below + Err(err) => return Err(format!("Could not start {GUESTOS_UPGRADER_SERVICE}: {err}")), + }; + + // systemctl failed, get status + let status = std::process::Command::new("journalctl") + .arg("status") + .arg(GUESTOS_UPGRADER_SERVICE) + .output() + .map(|output| String::from_utf8_lossy(&output.stdout).into_owned()) + .unwrap_or_else(|_| format!("[Could not get {GUESTOS_UPGRADER_SERVICE} status]")); + + Err(format!( + "Could not start {GUESTOS_UPGRADER_SERVICE}, status: {status}" + )) +} From 4886d7d08d957c0ca5eb491b5ef12e46ba0433c6 Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Thu, 20 Aug 2026 01:09:24 +0000 Subject: [PATCH 07/11] Inline and break out more server code --- .../vsock/src/server/command_utilities.rs | 2 + rs/ic_os/vsock/src/server/misc.rs | 66 ++++++++ rs/ic_os/vsock/src/server/mod.rs | 157 ++++-------------- rs/ic_os/vsock/src/server/upgrade.rs | 44 ++--- 4 files changed, 116 insertions(+), 153 deletions(-) create mode 100644 rs/ic_os/vsock/src/server/misc.rs diff --git a/rs/ic_os/vsock/src/server/command_utilities.rs b/rs/ic_os/vsock/src/server/command_utilities.rs index cba761d9e986..95d131d2713c 100644 --- a/rs/ic_os/vsock/src/server/command_utilities.rs +++ b/rs/ic_os/vsock/src/server/command_utilities.rs @@ -7,6 +7,7 @@ pub fn handle_command_output(command_output: Result) -> Response .map_err(|err| { let error_string = format!("Unable to read command output: {err}"); println!("Error: {error_string}"); + error_string }) .and_then(|output| { @@ -26,6 +27,7 @@ fn handle_output_string( .map_err(|err| { let error_string = format!("Unable to read command {label}: {err}"); println!("Error: {error_string}"); + error_string }) .and_then(|output| { diff --git a/rs/ic_os/vsock/src/server/misc.rs b/rs/ic_os/vsock/src/server/misc.rs new file mode 100644 index 000000000000..db342a92fafd --- /dev/null +++ b/rs/ic_os/vsock/src/server/misc.rs @@ -0,0 +1,66 @@ +use std::fs::OpenOptions; +use std::io::Write; + +use super::VSOCK_VERSION; +use crate::protocol::{NotifyData, Payload, Response}; + +const HOSTOS_VERSION_FILE_PATH: &str = "/opt/ic/share/version.txt"; + +pub(crate) fn get_hostos_version() -> Response { + let version = std::fs::read_to_string(HOSTOS_VERSION_FILE_PATH) + .map_err(|_| "Could not read hostOS version".to_string())?; + let version = version.trim().to_string(); + + Ok(Payload::HostOSVersion(version)) +} + +// HostOSVsockVersion command used for backwards compatibility +pub(crate) fn get_hostos_vsock_version() -> Response { + Ok(Payload::HostOSVsockVersion(VSOCK_VERSION)) +} + +pub(crate) fn notify(notify_data: &NotifyData) -> Response { + // Skip logging if manual recovery TUI is running to avoid interfering with the display + if procfs::process::all_processes().is_ok_and(|processes| { + processes.flatten().any(|process| { + process.cmdline().is_ok_and(|args| { + args.iter().any(|v| v.contains("hostos_tool")) + && args.iter().any(|v| v.contains("manual-recovery")) + }) + }) + }) { + return Ok(Payload::NoPayload); + } + + let message_output_count = std::cmp::min(notify_data.count, 10); + let message = notify_data.message.clone(); + + for device_path in &["/dev/tty1", "/dev/ttyS0"] { + let mut terminal_device_file = + OpenOptions::new() + .write(true) + .open(device_path) + .map_err(|err| { + println!( + "Error opening terminal device file {}: {}", + device_path, err + ); + err.to_string() + })?; + + let message_clone = message.clone(); + let write_lambda = move || -> Result<(), String> { + for _ in 0..message_output_count { + match terminal_device_file.write_all(format!("\n{message_clone}\n").as_bytes()) { + Ok(_) => std::thread::sleep(std::time::Duration::from_secs(2)), + Err(err) => return Err(err.to_string()), + } + } + Ok(()) + }; + + std::thread::spawn(write_lambda); + } + + Ok(Payload::NoPayload) +} diff --git a/rs/ic_os/vsock/src/server/mod.rs b/rs/ic_os/vsock/src/server/mod.rs index 931a7d715d3f..7d9d5bca62a9 100644 --- a/rs/ic_os/vsock/src/server/mod.rs +++ b/rs/ic_os/vsock/src/server/mod.rs @@ -1,17 +1,15 @@ -use std::fs::OpenOptions; use std::io::{self, Read, Write}; mod command_utilities; mod hsm; +mod misc; mod upgrade; -use crate::protocol::{ - Command, HostOSVsockVersion, NotifyData, Payload, Request, Response, parse_request, -}; +use crate::protocol::{Command, HostOSVsockVersion, Response, parse_request}; use hsm::{attach_hsm, detach_hsm}; +use misc::{get_hostos_version, get_hostos_vsock_version, notify}; use upgrade::{start_upgrade_guest_vm, upgrade_hostos}; -use tokio::runtime::Runtime; use vsock::{VMADDR_CID_ANY, VsockAddr, VsockListener, VsockStream}; // The first CID available for guests to use. This is used later to enforce @@ -19,9 +17,6 @@ use vsock::{VMADDR_CID_ANY, VsockAddr, VsockListener, VsockStream}; const VIR_VSOCK_GUEST_CID_MIN: u32 = 3; const DEFAULT_PORT: u32 = 19090; -// get_hostos_version -const HOSTOS_VERSION_FILE_PATH: &str = "/opt/ic/share/version.txt"; - const VSOCK_VERSION: HostOSVsockVersion = HostOSVsockVersion { major: 1, minor: 0, @@ -30,7 +25,11 @@ const VSOCK_VERSION: HostOSVsockVersion = HostOSVsockVersion { /// Runs the vsock server and awaits incoming vsock connections. pub fn run_server() -> io::Result<()> { - let vsock_listener: VsockListener = create_vsock_listener()?; + // Only listen for the first GuestOS VM. Only type4.* nodes will have more + // than one VM that uses VSOCK. We treat the first GuestOS as the leader in + // charge of HostOS. + let addr = VsockAddr::new(VMADDR_CID_ANY, DEFAULT_PORT); + let vsock_listener = VsockListener::bind(&addr)?; println!("Listening for vsock connection.\n"); @@ -45,19 +44,24 @@ pub fn run_server() -> io::Result<()> { Ok(()) } -fn create_vsock_listener() -> io::Result { - // Only listen for the first GuestOS VM. Only type4.* nodes will have more - // than one VM that uses VSOCK. We treat the first GuestOS as the leader in - // charge of HostOS. - let addr = VsockAddr::new(VMADDR_CID_ANY, DEFAULT_PORT); - VsockListener::bind(&addr) -} - fn process_connection(stream: &mut VsockStream) -> io::Result<()> { - let request = match get_request(stream) { + let mut buffer = [0; 4096]; + let bytes_read = stream.read(&mut buffer)?; + let json_request: String = match std::str::from_utf8(&buffer[..bytes_read]) { + Ok(json_str_request) => json_str_request.to_string(), + Err(error) => { + println!("Error converting bytes to string: {error}"); + return Err(io::Error::new(io::ErrorKind::InvalidData, error)); + } + }; + + let request = match parse_request(json_request.as_str()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e)) + { Ok(request) => request, Err(err) => { - send_response(stream, &Err(err.to_string()))?; + stream + .write_all(serde_json::to_string::(&Err(err.to_string()))?.as_bytes())?; return Err(err); } }; @@ -68,32 +72,26 @@ fn process_connection(stream: &mut VsockStream) -> io::Result<()> { io::ErrorKind::ConnectionRefused, "A type4 host only accepts VSOCK connections from the first VM.", ); - send_response(stream, &Err(err.to_string()))?; + stream.write_all(serde_json::to_string::(&Err(err.to_string()))?.as_bytes())?; return Err(err); }; if let Err(err) = verify_sender_cid(stream, request.guest_cid) { - send_response(stream, &Err(err.to_string()))?; + stream.write_all(serde_json::to_string::(&Err(err.to_string()))?.as_bytes())?; return Err(err); }; - let response: Response = dispatch(&request.command); - - send_response(stream, &response) -} - -fn get_request(stream: &mut VsockStream) -> io::Result { - let mut buffer = [0; 4096]; - let bytes_read = stream.read(&mut buffer)?; - let json_request: String = match std::str::from_utf8(&buffer[..bytes_read]) { - Ok(json_str_request) => json_str_request.to_string(), - Err(error) => { - println!("Error converting bytes to string: {error}"); - return Err(io::Error::new(io::ErrorKind::InvalidData, error)); - } + let response: Response = match &request.command { + Command::AttachHSM => attach_hsm(), + Command::DetachHSM => detach_hsm(), + Command::Upgrade(upgrade_data) => upgrade_hostos(upgrade_data), + Command::Notify(notify_data) => notify(notify_data), + Command::GetVsockProtocol => get_hostos_vsock_version(), + Command::GetHostOSVersion => get_hostos_version(), + Command::StartUpgradeGuestVM => start_upgrade_guest_vm(), }; - parse_request(json_request.as_str()).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e)) + stream.write_all(serde_json::to_string::(&response)?.as_bytes()) } // As a sanity check, we request that the sender adds its own CID to the message, and that CID must match the CID in the stream peer address. @@ -116,90 +114,3 @@ fn verify_sender_cid(stream: &mut VsockStream, guest_cid: u32) -> io::Result<()> )) } } - -fn send_response(stream: &mut VsockStream, response: &Response) -> io::Result<()> { - let json_response = serde_json::to_string(&response)?; - stream.write_all(json_response.as_bytes())?; - - Ok(()) -} - -pub fn dispatch(command: &Command) -> Response { - use Command::*; - match command { - AttachHSM => attach_hsm(), - DetachHSM => detach_hsm(), - Upgrade(upgrade_data) => { - let rt = Runtime::new().map_err(|e| e.to_string())?; - rt.block_on(upgrade_hostos(upgrade_data)) - } - Notify(notify_data) => notify(notify_data), - GetVsockProtocol => get_hostos_vsock_version(), - GetHostOSVersion => get_hostos_version(), - StartUpgradeGuestVM => start_upgrade_guest_vm(), - } -} - -fn get_hostos_version() -> Response { - let version = std::fs::read_to_string(HOSTOS_VERSION_FILE_PATH) - .map_err(|_| "Could not read hostOS version".to_string())?; - let version = version.trim().to_string(); - - Ok(Payload::HostOSVersion(version)) -} - -// HostOSVsockVersion command used for backwards compatibility -fn get_hostos_vsock_version() -> Response { - Ok(Payload::HostOSVsockVersion(VSOCK_VERSION)) -} - -fn is_manual_recovery_running() -> bool { - match procfs::process::all_processes() { - Ok(processes) => processes.into_iter().filter_map(Result::ok).any(|process| { - process.cmdline().is_ok_and(|args| { - let cmd = args.join(" "); - cmd.contains("hostos_tool") && cmd.contains("manual-recovery") - }) - }), - Err(_) => false, - } -} - -fn notify(notify_data: &NotifyData) -> Response { - // Skip logging if manual recovery TUI is running to avoid interfering with the display - if is_manual_recovery_running() { - return Ok(Payload::NoPayload); - } - - let message_output_count = std::cmp::min(notify_data.count, 10); - let message = notify_data.message.clone(); - - for device_path in &["/dev/tty1", "/dev/ttyS0"] { - let mut terminal_device_file = - OpenOptions::new() - .write(true) - .open(device_path) - .map_err(|err| { - println!( - "Error opening terminal device file {}: {}", - device_path, err - ); - err.to_string() - })?; - - let message_clone = message.clone(); - let write_lambda = move || -> Result<(), String> { - for _ in 0..message_output_count { - match terminal_device_file.write_all(format!("\n{message_clone}\n").as_bytes()) { - Ok(_) => std::thread::sleep(std::time::Duration::from_secs(2)), - Err(err) => return Err(err.to_string()), - } - } - Ok(()) - }; - - std::thread::spawn(write_lambda); - } - - Ok(Payload::NoPayload) -} diff --git a/rs/ic_os/vsock/src/server/upgrade.rs b/rs/ic_os/vsock/src/server/upgrade.rs index 71930617a44c..c94e1f4d110b 100644 --- a/rs/ic_os/vsock/src/server/upgrade.rs +++ b/rs/ic_os/vsock/src/server/upgrade.rs @@ -6,29 +6,27 @@ use crate::protocol::{Payload, Response, UpgradeData}; use ic_http_utils::file_downloader::FileDownloader; -// upgrade +use tokio::runtime::Runtime; + const UPGRADE_FILE_PATH: &str = "/tmp/upgrade"; const INSTALL_UPGRADE_FILE_PATH: &str = "/opt/ic/bin/install-upgrade.sh"; -async fn create_hostos_upgrade_file( - upgrade_url: &str, - file_path: &str, - target_hash: &str, -) -> Result<(), String> { - println!("Starting download from: {}", upgrade_url); +pub(crate) fn upgrade_hostos(upgrade_data: &UpgradeData) -> Response { + println!("Trying to fetch hostOS upgrade file from request: {upgrade_data:?}"); + + println!("Starting download from: {}", upgrade_data.url); let file_downloader = FileDownloader::new_with_timeout(None, Duration::from_secs(120)); - file_downloader - .download_file( - upgrade_url, - Path::new(file_path), - Some(target_hash.to_string()), - ) - .await + Runtime::new().map_err(|e| e.to_string()).and_then(|rt| { + rt.block_on(file_downloader.download_file( + &upgrade_data.url, + Path::new(UPGRADE_FILE_PATH), + Some(upgrade_data.target_hash.to_string()), + )) .map_err(|e| e.to_string()) -} + })?; -fn run_upgrade() -> Response { + println!("Download completed, starting upgrade installation..."); let command_output = std::process::Command::new(INSTALL_UPGRADE_FILE_PATH) .arg(UPGRADE_FILE_PATH) .output(); @@ -43,20 +41,6 @@ fn run_upgrade() -> Response { handle_command_output(command_output) } -pub(crate) async fn upgrade_hostos(upgrade_data: &UpgradeData) -> Response { - println!("Trying to fetch hostOS upgrade file from request: {upgrade_data:?}"); - - create_hostos_upgrade_file( - &upgrade_data.url, - UPGRADE_FILE_PATH, - &upgrade_data.target_hash, - ) - .await?; - - println!("Download completed, starting upgrade installation..."); - run_upgrade() -} - pub(crate) fn start_upgrade_guest_vm() -> Response { const GUESTOS_UPGRADER_SERVICE: &str = "upgrade-guestos.service"; From 160d9e22ee2c7ab7fdebfc2f2b906381563470b8 Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Thu, 20 Aug 2026 01:09:38 +0000 Subject: [PATCH 08/11] Inline client code --- rs/ic_os/vsock/src/client.rs | 66 ++++++++++++++---------------------- 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/rs/ic_os/vsock/src/client.rs b/rs/ic_os/vsock/src/client.rs index ff9aa615cd68..d37be75ad368 100644 --- a/rs/ic_os/vsock/src/client.rs +++ b/rs/ic_os/vsock/src/client.rs @@ -36,51 +36,35 @@ mod linux { impl VSockClient for LinuxVSockClient { fn send_command(&self, command: Command) -> Response { - let port = self.port; let guest_cid = vsock::get_local_cid().map_err(|e| e.to_string())?; let request = Request { guest_cid, command }; - send_request_to_host_and_parse_response(&request, &port) + let mut stream = VsockStream::connect_with_cid_port(VMADDR_CID_HOST, self.port) + .map_err(|e| e.to_string())?; + // Set a long timeout, so HostOS has enough time to upgrade. + stream + .set_write_timeout(Some(std::time::Duration::from_secs(60 * 5))) + .map_err(|e| e.to_string())?; + stream + .set_read_timeout(Some(std::time::Duration::from_secs(60 * 5))) + .map_err(|e| e.to_string())?; + + let json_request = serde_json::to_string(&request).map_err(|e| e.to_string())?; + stream + .write_all(json_request.as_bytes()) + .map_err(|e| e.to_string())?; + + // 64 KiB - generous for current responses (typically <1 KiB) while + // preventing unbounded allocation from a misbehaving host. + const MAX_RESPONSE_SIZE: u64 = 64 * 1024; + let mut response_str = String::new(); + stream + .take(MAX_RESPONSE_SIZE) + .read_to_string(&mut response_str) + .map_err(|e| e.to_string())?; + + parse_response(response_str.as_str()) } } - - pub fn send_request_to_host_and_parse_response(request: &Request, port: &u32) -> Response { - let response_str = send_request_to_host(request, port)?; - - parse_response(response_str.as_str()) - } - - pub fn send_request_to_host(request: &Request, port: &u32) -> Result { - let mut stream = create_stream(port).map_err(|e| e.to_string())?; - - let json_request = serde_json::to_string(request).map_err(|e| e.to_string())?; - stream - .write_all(json_request.as_bytes()) - .map_err(|e| e.to_string())?; - - let read_result = read_response_from_host(&mut stream); - - read_result.map_err(|e| e.to_string()) - } - - fn read_response_from_host(stream: &mut VsockStream) -> anyhow::Result { - // 64 KiB - generous for current responses (typically <1 KiB) while - // preventing unbounded allocation from a misbehaving host. - const MAX_RESPONSE_SIZE: u64 = 64 * 1024; - let mut response = String::new(); - stream - .take(MAX_RESPONSE_SIZE) - .read_to_string(&mut response)?; - Ok(response) - } - - fn create_stream(port: &u32) -> Result { - let stream = VsockStream::connect_with_cid_port(VMADDR_CID_HOST, *port)?; - // Set a long timeout, so HostOS has enough time to upgrade. - stream.set_write_timeout(Some(std::time::Duration::from_secs(60 * 5)))?; - stream.set_read_timeout(Some(std::time::Duration::from_secs(60 * 5)))?; - - Ok(stream) - } } From c7bea36af52108dc688e80c8dbce1a907bc6736d Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Thu, 20 Aug 2026 01:10:07 +0000 Subject: [PATCH 09/11] Inline guest code --- rs/ic_os/vsock/src/bin/guest.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/rs/ic_os/vsock/src/bin/guest.rs b/rs/ic_os/vsock/src/bin/guest.rs index c8b991d75c82..441f7b3deced 100644 --- a/rs/ic_os/vsock/src/bin/guest.rs +++ b/rs/ic_os/vsock/src/bin/guest.rs @@ -15,7 +15,11 @@ fn main() -> Result<(), String> { // Echo notify messages to the local GuestOS console so they are visible // in cloud environments where the host console is not accessible. if let Command::Notify(NotifyData { ref message, .. }) = command { - write_to_guest_console(message); + for path in ["/dev/tty1", "/dev/ttyS0"] { + if let Ok(mut tty) = OpenOptions::new().write(true).open(path) { + let _ = writeln!(tty, "\n{message}"); + } + } } let payload = LinuxVSockClient::with_port(port).send_command(command)?; @@ -30,14 +34,6 @@ fn main() -> Result<(), String> { Ok(()) } -fn write_to_guest_console(message: &str) { - for path in ["/dev/tty1", "/dev/ttyS0"] { - if let Ok(mut tty) = OpenOptions::new().write(true).open(path) { - let _ = writeln!(tty, "\n{message}"); - } - } -} - #[derive(Debug, Parser)] #[clap( version = "1.0.0", From 608fa64426f5531ef5617138e6d4daebd8e8fce1 Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Thu, 20 Aug 2026 01:10:15 +0000 Subject: [PATCH 10/11] Re-home tests --- rs/ic_os/vsock/src/client.rs | 46 +++- .../src/{protocol/mod.rs => protocol.rs} | 112 +++++++++- rs/ic_os/vsock/src/protocol/tests.rs | 197 ------------------ rs/ic_os/vsock/src/protocol/utils.rs | 15 -- rs/ic_os/vsock/src/server/mod.rs | 78 ++++++- 5 files changed, 228 insertions(+), 220 deletions(-) rename rs/ic_os/vsock/src/{protocol/mod.rs => protocol.rs} (51%) delete mode 100644 rs/ic_os/vsock/src/protocol/tests.rs delete mode 100644 rs/ic_os/vsock/src/protocol/utils.rs diff --git a/rs/ic_os/vsock/src/client.rs b/rs/ic_os/vsock/src/client.rs index d37be75ad368..662cce74b7bc 100644 --- a/rs/ic_os/vsock/src/client.rs +++ b/rs/ic_os/vsock/src/client.rs @@ -12,7 +12,7 @@ pub use linux::*; #[cfg(target_os = "linux")] mod linux { use super::*; - use crate::protocol::{Request, Response, parse_response}; + use crate::protocol::{Request, Response}; use std::io::{Read, Write}; use vsock::{VMADDR_CID_HOST, VsockStream}; @@ -68,3 +68,47 @@ mod linux { } } } + +/// Parse a response in a json string to a `Response` struct. +pub fn parse_response(json_str: &str) -> Response { + if let Ok(response) = serde_json::from_str::(json_str) { + return response; + } + Err("Unable to parse host response: ".to_string() + json_str) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::{HostOSVsockVersion, Payload}; + + #[test] + fn test_parse_response() { + assert_eq!( + Ok(Payload::NoPayload), + parse_response("{\"Ok\":\"NoPayload\"}") + ); + assert_eq!( + Ok(Payload::HostOSVersion("123".to_string())), + parse_response("{\"Ok\":{\"HostOSVersion\":\"123\"}}") + ); + assert_eq!( + Ok(Payload::HostOSVsockVersion(HostOSVsockVersion { + major: 1, + minor: 0, + patch: 0, + })), + parse_response( + "{\"Ok\":{\"HostOSVsockVersion\":{\"major\":1,\"minor\":0,\"patch\":0}}}" + ) + ); + assert_eq!( + Err("Unable to parse host response: Error response".to_string()), + parse_response("Error response") + ); + + let json_str = r#"{"Ok":"NoPayload"#; // Missing closing brace + let response = parse_response(json_str); + assert!(response.is_err()); + } +} diff --git a/rs/ic_os/vsock/src/protocol/mod.rs b/rs/ic_os/vsock/src/protocol.rs similarity index 51% rename from rs/ic_os/vsock/src/protocol/mod.rs rename to rs/ic_os/vsock/src/protocol.rs index 4d27903d187d..d3c68d640b9e 100644 --- a/rs/ic_os/vsock/src/protocol/mod.rs +++ b/rs/ic_os/vsock/src/protocol.rs @@ -1,11 +1,5 @@ use std::fmt; -#[cfg(test)] -mod tests; -mod utils; - -pub use utils::*; - use serde::{Deserialize, Serialize}; pub type Response = Result; @@ -110,3 +104,109 @@ pub struct NotifyData { pub count: u32, pub message: String, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_request_serialization() { + assert_eq!( + serde_json::json!({ + "sender_cid": 1_u32, + "message": "attach-hsm" + }), + serde_json::to_value(&Request { + guest_cid: 1, + command: Command::AttachHSM + }) + .unwrap() + ); + assert_eq!( + serde_json::json!({ + "sender_cid": 1_u32, + "message": "detach-hsm" + }), + serde_json::to_value(&Request { + guest_cid: 1, + command: Command::DetachHSM + }) + .unwrap() + ); + assert_eq!( + serde_json::json!({ + "sender_cid": 1_u32, + "message": { + "notify": { + "count": 1_i32, + "message": "Hello World", + } + } + }), + serde_json::to_value(Request { + guest_cid: 1, + command: Command::Notify(NotifyData { + count: 1, + message: "Hello World".to_string(), + }) + }) + .unwrap() + ); + assert_eq!( + serde_json::json!({ + "sender_cid": 1_u32, + "message": { + "upgrade": { + "url": "https://example.com", + "target-hash": "0x1111222233334444" + } + } + }), + serde_json::to_value(Request { + guest_cid: 1, + command: Command::Upgrade(UpgradeData { + url: "https://example.com".to_string(), + target_hash: "0x1111222233334444".to_string() + }) + }) + .unwrap() + ); + } + + #[test] + fn test_response_serialization() { + let response: Response = Ok(Payload::NoPayload); + assert_eq!( + serde_json::json!({ + "Ok": "NoPayload", + }), + serde_json::to_value(response).unwrap() + ); + + let vsock_version: HostOSVsockVersion = HostOSVsockVersion { + major: 1, + minor: 0, + patch: 0, + }; + let response: Response = Ok(Payload::HostOSVsockVersion(vsock_version)); + + let expected_json = serde_json::json!({ + "Ok": { + "HostOSVsockVersion": { + "major": 1, + "minor": 0, + "patch": 0 + } + } + }); + assert_eq!(expected_json, serde_json::to_value(response).unwrap()); + + let response: Response = Ok(Payload::HostOSVersion("VERSION".to_string())); + let expected_json = serde_json::json!({ + "Ok": { + "HostOSVersion": "VERSION", + } + }); + assert_eq!(expected_json, serde_json::to_value(response).unwrap()); + } +} diff --git a/rs/ic_os/vsock/src/protocol/tests.rs b/rs/ic_os/vsock/src/protocol/tests.rs deleted file mode 100644 index 39fdde2c00dd..000000000000 --- a/rs/ic_os/vsock/src/protocol/tests.rs +++ /dev/null @@ -1,197 +0,0 @@ -use super::utils::*; -use super::*; - -#[test] -fn test_request_serialization() { - assert_eq!( - serde_json::json!({ - "sender_cid": 1_u32, - "message": "attach-hsm" - }), - serde_json::to_value(&Request { - guest_cid: 1, - command: Command::AttachHSM - }) - .unwrap() - ); - assert_eq!( - serde_json::json!({ - "sender_cid": 1_u32, - "message": "detach-hsm" - }), - serde_json::to_value(&Request { - guest_cid: 1, - command: Command::DetachHSM - }) - .unwrap() - ); - assert_eq!( - serde_json::json!({ - "sender_cid": 1_u32, - "message": { - "notify": { - "count": 1_i32, - "message": "Hello World", - } - } - }), - serde_json::to_value(Request { - guest_cid: 1, - command: Command::Notify(NotifyData { - count: 1, - message: "Hello World".to_string(), - }) - }) - .unwrap() - ); - assert_eq!( - serde_json::json!({ - "sender_cid": 1_u32, - "message": { - "upgrade": { - "url": "https://example.com", - "target-hash": "0x1111222233334444" - } - } - }), - serde_json::to_value(Request { - guest_cid: 1, - command: Command::Upgrade(UpgradeData { - url: "https://example.com".to_string(), - target_hash: "0x1111222233334444".to_string() - }) - }) - .unwrap() - ); -} - -#[test] -fn test_response_serialization() { - let response: Response = Ok(Payload::NoPayload); - assert_eq!( - serde_json::json!({ - "Ok": "NoPayload", - }), - serde_json::to_value(response).unwrap() - ); - - let vsock_version: HostOSVsockVersion = HostOSVsockVersion { - major: 1, - minor: 0, - patch: 0, - }; - let response: Response = Ok(Payload::HostOSVsockVersion(vsock_version)); - - let expected_json = serde_json::json!({ - "Ok": { - "HostOSVsockVersion": { - "major": 1, - "minor": 0, - "patch": 0 - } - } - }); - assert_eq!(expected_json, serde_json::to_value(response).unwrap()); - - let response: Response = Ok(Payload::HostOSVersion("VERSION".to_string())); - let expected_json = serde_json::json!({ - "Ok": { - "HostOSVersion": "VERSION", - } - }); - assert_eq!(expected_json, serde_json::to_value(response).unwrap()); -} - -#[test] -fn test_parse_request() { - // Test AttachHSM command - let json_str = r#"{"sender_cid": 123, "message": "attach-hsm"}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.command, Command::AttachHSM); - - // Test DetachHSM command - let json_str = r#"{"sender_cid": 123, "message": "detach-hsm"}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.command, Command::DetachHSM); - - // Test Upgrade command - let json_str = r#"{"sender_cid": 123, "message": {"upgrade": {"url": "http://example.com/upgrade", "target-hash": "abcd1234hash"}}}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.guest_cid, 123); - match request.command { - Command::Upgrade(data) => { - assert_eq!(data.url, "http://example.com/upgrade"); - assert_eq!(data.target_hash, "abcd1234hash"); - } - _ => panic!("Expected Upgrade command"), - } - - // Test Notify command - let json_str = r#"{"sender_cid": 123, "message": {"notify": {"message": "System update required", "count": 2}}}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.guest_cid, 123); - match request.command { - Command::Notify(data) => { - assert_eq!(data.count, 2); - assert_eq!(data.message, "System update required"); - } - _ => panic!("Expected Notify command"), - } - - // Test GetVsockProtocol command - let json_str = r#"{"sender_cid": 123, "message": "GetVsockProtocol"}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.guest_cid, 123); - assert_eq!(request.command, Command::GetVsockProtocol); - - // Test GetHostOSVersion command - let json_str = r#"{"sender_cid": 123, "message": "GetHostOSVersion"}"#; - let request = parse_request(json_str); - assert!(request.is_ok()); - let request = request.unwrap(); - assert_eq!(request.guest_cid, 123); - assert_eq!(request.command, Command::GetHostOSVersion); - - // Test malformed command - let json_str = r#"{"sender_cid": 123, "message": "attach-hsm"#; // Missing closing brace - let request = parse_request(json_str); - assert!(request.is_err()); -} - -#[test] -fn test_parse_response() { - assert_eq!( - Ok(Payload::NoPayload), - parse_response("{\"Ok\":\"NoPayload\"}") - ); - assert_eq!( - Ok(Payload::HostOSVersion("123".to_string())), - parse_response("{\"Ok\":{\"HostOSVersion\":\"123\"}}") - ); - assert_eq!( - Ok(Payload::HostOSVsockVersion(HostOSVsockVersion { - major: 1, - minor: 0, - patch: 0, - })), - parse_response("{\"Ok\":{\"HostOSVsockVersion\":{\"major\":1,\"minor\":0,\"patch\":0}}}") - ); - assert_eq!( - Err("Unable to parse host response: Error response".to_string()), - parse_response("Error response") - ); - - let json_str = r#"{"Ok":"NoPayload"#; // Missing closing brace - let response = parse_response(json_str); - assert!(response.is_err()); -} diff --git a/rs/ic_os/vsock/src/protocol/utils.rs b/rs/ic_os/vsock/src/protocol/utils.rs deleted file mode 100644 index 17a728710583..000000000000 --- a/rs/ic_os/vsock/src/protocol/utils.rs +++ /dev/null @@ -1,15 +0,0 @@ -use super::{Request, Response}; - -/// Parse a response in a json string to a `Response` struct. -pub fn parse_response(json_str: &str) -> Response { - if let Ok(response) = serde_json::from_str::(json_str) { - return response; - } - Err("Unable to parse host response: ".to_string() + json_str) -} - -/// Parse a request in a json string to `Request` struct. -pub fn parse_request(json_str: &str) -> Result { - serde_json::from_str::(json_str) - .map_err(|error| format!("Unable to parse guest request: {json_str}: {error}")) -} diff --git a/rs/ic_os/vsock/src/server/mod.rs b/rs/ic_os/vsock/src/server/mod.rs index 7d9d5bca62a9..e88f35083655 100644 --- a/rs/ic_os/vsock/src/server/mod.rs +++ b/rs/ic_os/vsock/src/server/mod.rs @@ -5,7 +5,7 @@ mod hsm; mod misc; mod upgrade; -use crate::protocol::{Command, HostOSVsockVersion, Response, parse_request}; +use crate::protocol::{Command, HostOSVsockVersion, Request, Response}; use hsm::{attach_hsm, detach_hsm}; use misc::{get_hostos_version, get_hostos_vsock_version, notify}; use upgrade::{start_upgrade_guest_vm, upgrade_hostos}; @@ -114,3 +114,79 @@ fn verify_sender_cid(stream: &mut VsockStream, guest_cid: u32) -> io::Result<()> )) } } + +pub fn parse_request(json_str: &str) -> Result { + serde_json::from_str::(json_str) + .map_err(|error| format!("Unable to parse guest request: {json_str}: {error}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_request() { + // Test AttachHSM command + let json_str = r#"{"sender_cid": 123, "message": "attach-hsm"}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.command, Command::AttachHSM); + + // Test DetachHSM command + let json_str = r#"{"sender_cid": 123, "message": "detach-hsm"}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.command, Command::DetachHSM); + + // Test Upgrade command + let json_str = r#"{"sender_cid": 123, "message": {"upgrade": {"url": "http://example.com/upgrade", "target-hash": "abcd1234hash"}}}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.guest_cid, 123); + match request.command { + Command::Upgrade(data) => { + assert_eq!(data.url, "http://example.com/upgrade"); + assert_eq!(data.target_hash, "abcd1234hash"); + } + _ => panic!("Expected Upgrade command"), + } + + // Test Notify command + let json_str = r#"{"sender_cid": 123, "message": {"notify": {"message": "System update required", "count": 2}}}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.guest_cid, 123); + match request.command { + Command::Notify(data) => { + assert_eq!(data.count, 2); + assert_eq!(data.message, "System update required"); + } + _ => panic!("Expected Notify command"), + } + + // Test GetVsockProtocol command + let json_str = r#"{"sender_cid": 123, "message": "GetVsockProtocol"}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.guest_cid, 123); + assert_eq!(request.command, Command::GetVsockProtocol); + + // Test GetHostOSVersion command + let json_str = r#"{"sender_cid": 123, "message": "GetHostOSVersion"}"#; + let request = parse_request(json_str); + assert!(request.is_ok()); + let request = request.unwrap(); + assert_eq!(request.guest_cid, 123); + assert_eq!(request.command, Command::GetHostOSVersion); + + // Test malformed command + let json_str = r#"{"sender_cid": 123, "message": "attach-hsm"#; // Missing closing brace + let request = parse_request(json_str); + assert!(request.is_err()); + } +} From f2bdd93e6a2c2fe5668e280724209170d084b393 Mon Sep 17 00:00:00 2001 From: Eero Kelly Date: Fri, 21 Aug 2026 17:16:27 +0000 Subject: [PATCH 11/11] Trigger CI