diff --git a/Cargo.lock b/Cargo.lock index 7ac8040..852cb3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,72 +4,74 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.6.11" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", + "once_cell_polyfill", "windows-sys", ] [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" @@ -79,9 +81,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block2" @@ -93,25 +95,22 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.0.83" +name = "cfg-if" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] -name = "cfg-if" -version = "1.0.0" +name = "cfg_aliases" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "clap" -version = "4.5.0" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -119,9 +118,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.0" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -131,27 +130,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "core-foundation" @@ -171,9 +170,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -181,41 +180,72 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "block2", "objc2", ] [[package]] name = "either" -version = "1.13.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "env_filter" -version = "0.1.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -223,9 +253,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", @@ -236,9 +266,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "httparse" @@ -246,6 +276,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.18" @@ -254,26 +290,39 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.10" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" dependencies = [ + "defmt", + "jiff-core", "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", ] [[package]] name = "jiff-static" -version = "0.2.10" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" dependencies = [ + "jiff-core", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -292,25 +341,26 @@ dependencies = [ "serde", "serde_json", "sysinfo", + "windows-sys", ] [[package]] name = "libc" -version = "0.2.153" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "log" -version = "0.4.27" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "mac_address" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4863ee94f19ed315bf3bc00299338d857d4b5bc856af375cc97d237382ad3856" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" dependencies = [ "nix", "winapi", @@ -318,46 +368,46 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" -version = "0.6.5" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "nix" -version = "0.23.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 1.3.2", - "cc", + "bitflags 2.13.2", "cfg-if", + "cfg_aliases", "libc", "memoffset", ] [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi", ] [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -368,7 +418,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "dispatch2", "objc2", ] @@ -385,7 +435,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "block2", "dispatch2", "libc", @@ -393,44 +443,50 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ "portable-atomic", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -438,9 +494,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -448,9 +504,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -460,9 +516,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -471,15 +527,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -487,29 +543,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -520,15 +576,26 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] name = "syn" -version = "2.0.101" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -549,17 +616,37 @@ dependencies = [ "windows", ] +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "winapi" @@ -613,7 +700,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -624,9 +711,15 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.1.2" @@ -638,11 +731,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] @@ -711,6 +804,6 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index abd8d9f..4088c1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,16 @@ sysinfo = "0.31.4" log = "0.4.0" env_logger = "0.11.8" regex = "1.11.1" +[target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.10.1" objc2-io-kit = "0.3.2" httparse = "1.10.1" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.61.2", features = [ + "Win32_Foundation", + "Win32_Networking_WinSock", + "Win32_System_Console", +] } diff --git a/README.md b/README.md index 155fa9f..2d883fe 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ `krunkit` is a tool to launch configurable virtual machines using the [libkrun](https://github.com/containers/libkrun) platform. > [!IMPORTANT] -> krunkit is only supported on hosts running macOS 14 or newer. +> The established krunkit path supports macOS 14 or newer. Experimental Windows support uses libkrun 2.0 and WHP. ## Installation @@ -39,6 +39,14 @@ make sudo make install ``` +On Windows, `LIBKRUN_LIB_DIR` must point to the directory containing `krun.dll.lib` when invoking Cargo. +Place the matching `krun.dll` beside `krunkit.exe` to run it. For local development, `build.ps1` builds a +libkrun 2.0 tree and copies its DLL beside the executable: + +```powershell +.\build.ps1 -LibkrunSource C:\src\libkrun +``` + To build with `libkrun` from *Homebrew* or *MacPorts* use the appropriate `PREFIX`: ``` diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..4c0f337 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,66 @@ +param ( + [ValidateSet("all", "debug", "install", "clean")] + [string]$Task = "all", + + [string]$Prefix = $env:PREFIX, + + [string]$LibkrunSource = $env:LIBKRUN_SOURCE_DIR +) + +$ErrorActionPreference = "Stop" +$BinName = "krunkit.exe" +$ReleasePath = "target\release\$BinName" +$DebugPath = "target\debug\$BinName" + +function Build-Krunkit([string]$Profile) { + if ([string]::IsNullOrWhiteSpace($LibkrunSource)) { + throw "Specify -LibkrunSource or set LIBKRUN_SOURCE_DIR" + } + + $ReleaseArg = @() + if ($Profile -eq "release") { + $ReleaseArg += "--release" + } + cargo build --manifest-path (Join-Path $LibkrunSource "Cargo.toml") -p libkrun --features "ffi blk net" @ReleaseArg + if ($LASTEXITCODE -ne 0) { throw "libkrun build failed" } + + $env:LIBKRUN_LIB_DIR = Join-Path $LibkrunSource "target\$Profile" + cargo build @ReleaseArg + if ($LASTEXITCODE -ne 0) { throw "krunkit build failed" } + + Copy-Item -Path (Join-Path $env:LIBKRUN_LIB_DIR "krun.dll") -Destination "target\$Profile\krun.dll" -Force +} + +switch ($Task) { + "all" { + Build-Krunkit "release" + } + + "debug" { + Build-Krunkit "debug" + } + + "install" { + if ([string]::IsNullOrWhiteSpace($Prefix)) { + throw "Specify -Prefix or set PREFIX" + } + + if (-not (Test-Path $ReleasePath)) { + Build-Krunkit "release" + } + + $TargetBinDir = Join-Path $Prefix "bin" + + if (-not (Test-Path $TargetBinDir)) { + New-Item -ItemType Directory -Path $TargetBinDir -Force | Out-Null + } + + Copy-Item -Path $ReleasePath -Destination $TargetBinDir -Force + Copy-Item -Path "target\release\krun.dll" -Destination $TargetBinDir -Force + Write-Host "Successfully installed to: $TargetBinDir" -ForegroundColor Green + } + + "clean" { + cargo clean + } +} diff --git a/build.rs b/build.rs index 73247a9..cc35b3d 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,4 @@ -// SDPX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 fn main() { #[cfg(target_os = "macos")] @@ -11,4 +11,13 @@ fn main() { println!("cargo:rerun-if-env-changed=PREFIX"); } + + #[cfg(target_os = "windows")] + { + let lib_dir = std::env::var("LIBKRUN_LIB_DIR") + .expect("LIBKRUN_LIB_DIR must point to the directory containing krun.dll.lib"); + println!("cargo:rustc-link-search={lib_dir}"); + + println!("cargo:rerun-if-env-changed=LIBKRUN_LIB_DIR"); + } } diff --git a/docs/usage.md b/docs/usage.md index 3ea2e2b..fb34e6e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,7 +1,8 @@ # krunkit Command Line -`krunkit` can launch configurable virtual machines using macOS's hypervisor framework and the `libkrun` virtual -machine monitor library. The `libkrun` virtual machine configuration can be specified from command line arguments. +`krunkit` can launch configurable virtual machines using macOS's hypervisor framework or Windows Hypervisor +Platform and the `libkrun` virtual machine monitor library. The `libkrun` virtual machine configuration can be +specified from command line arguments. Specifying a virtual machine's vCPU and RAM allocation is required. Adding devices is optional, yet most workloads will require a root disk to be useful. @@ -64,11 +65,9 @@ This configures a virtual machine to use two vCPUs and 2048 MiB of RAM: --cpus 2 --memory 2048 ``` -## Bootloader Configuration - ### EFI bootloader -`--bootloader efi` allows booting a disk image using EFI, which removes the need for providing external kernel/initrd/... jthe disk image bootloader will be started by the EFI firmware, which will in turn know which kernel it should be booting. +`--bootloader efi` allows booting a disk image using EFI, which removes the need for providing external kernel/initrd/... the disk image bootloader will be started by the EFI firmware, which will in turn know which kernel it should be booting. #### Arguments - `variable-store`: path to a file which EFI can use to store its variables @@ -271,3 +270,71 @@ The table below provides some data on how offloading effects the gvproxy and vmn | gvproxy | krunkit | false | 1.47 Gbits/s | 2.58 Gbits/s | | gvproxy | vfkit | false | 1.43 Gbits/s | 2.84 Gbits/s | +## Bootloader Configuration on Windows + +On Windows, krunkit boots `edk2/OVMF.fd` by default. Use `--firmware-path PATH` to select another UEFI image. + +The Windows path connects `ttyS0` to the host terminal. It supports raw and QCOW2 `virtio-blk` images, +path-based `virtio-net,type=unixstream` devices, and `virtio-vsock` Unix socket mappings. + +### Windows Limitations + +On Windows, only the following devices can be specified with `--device`: + +- `virtio-blk` +- `virtio-net,type=unixstream,path=...`; `unixgram`, `unixSocketPath`, and `fd` backends are unsupported. +- `virtio-vsock` +- `virtio-fs` (requires `permissionSemantics=complete`; `simplified` is not supported on Windows) + +`virtio-serial`, `virtio-gpu`, and `virtio-input` are unsupported. `virtio-rng` is added +automatically, but must not be specified with `--device`. + +The RESTful service (`--restful-uri`), `--timesync`, and nested virtualization (`--nested`) are not +supported on Windows. The default console is connected directly to the host terminal; use it instead of a +`virtio-serial` device for guest text output. + +```powershell +krunkit.exe --cpus 2 --memory 2G ` + --firmware-path C:\vm\OVMF.fd ` + --device virtio-blk,path=C:\vm\disk.raw,format=raw +``` + +### SSH on Windows + +Start gvproxy in a separate terminal. Its default virtual network assigns the guest `192.168.127.2`, and +`-ssh-port 2222` forwards host port 2222 to guest port 22: + +```powershell +gvproxy.exe -listen-qemu unix://C:/vm/network.sock -ssh-port 2222 +``` + +Add the matching network device when starting krunkit: + +```powershell +krunkit.exe --cpus 2 --memory 2G ` + --firmware-path C:\vm\OVMF.fd ` + --device virtio-blk,path=C:\vm\rootfs.raw,format=raw ` + --device virtio-net,type=unixstream,path=C:\vm\network.sock,mac=5a:94:ef:e4:0c:ee,offloading=true +``` + +The guest must run DHCP on the virtio network interface and have `sshd` listening on port 22. Connect with: + +```powershell +ssh.exe -p 2222 USER@127.0.0.1 +``` + +### vsock on Windows + +The default `listen` action handles guest-initiated connections: libkrun connects guest port 1024 to an existing +host Unix socket. + +```powershell +--device virtio-vsock,port=1024,socketURL=C:\vm\guest-to-host.sock,listen +``` + +Use `connect` for host-initiated connections. Libkrun listens at the host socket path and forwards accepted +connections to a service listening on guest CID 3, port 1024. + +```powershell +--device virtio-vsock,port=1024,socketURL=C:\vm\host-to-guest.sock,connect +``` diff --git a/src/context/mod.rs b/src/context/mod.rs new file mode 100644 index 0000000..8b6ea39 --- /dev/null +++ b/src/context/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(not(windows))] +mod unix; +#[cfg(windows)] +mod windows; + +#[cfg(not(windows))] +pub use unix::KrunContext; +#[cfg(windows)] +pub use windows::KrunContext; diff --git a/src/context.rs b/src/context/unix.rs similarity index 98% rename from src/context.rs rename to src/context/unix.rs index eed1a6e..5a43734 100644 --- a/src/context.rs +++ b/src/context/unix.rs @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -use super::*; - use crate::{ + cmdline::Args, status::{get_shutdown_eventfd, status_listener, RestfulUri}, virtio::KrunContextSet, }; @@ -16,7 +15,9 @@ use std::{ io, }; +#[cfg(target_os = "macos")] use crate::timesync::timesync_listener; +#[cfg(target_os = "macos")] use crate::virtio::{VsockAction, VsockConfig}; use anyhow::{anyhow, Context}; use env_logger::{Builder, Env, Target}; @@ -199,6 +200,7 @@ impl TryFrom for KrunContext { } } + #[cfg(target_os = "macos")] if let Some(timesync_port) = args.timesync { let vsock_config = VsockConfig { port: timesync_port, diff --git a/src/context/windows.rs b/src/context/windows.rs new file mode 100644 index 0000000..5abad27 --- /dev/null +++ b/src/context/windows.rs @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::{cmdline::Args, status::RestfulUri, virtio::windows::attach_devices}; +use anyhow::{anyhow, Context}; +use std::{ + ffi::{c_char, c_void, CStr}, + fs::OpenOptions, + io, + os::windows::io::AsRawHandle, + path::{Path, PathBuf}, + ptr, +}; + +type KrunObject = *mut c_void; +type KrunError = *mut c_void; +type KrunResult = u64; + +const KRUN_SUCCESS: KrunResult = 0; +const KRUN_LOG_STYLE_AUTO: u32 = 0; +const KRUN_LOG_OPTION_ENV: u32 = 0; +const KRUN_LOG_OPTION_NO_ENV: u32 = 1; + +#[repr(C)] +#[derive(Clone, Copy)] +struct KrunStr { + data: *const c_char, + len: usize, +} +impl KrunStr { + fn new(value: &str) -> Self { + Self { + data: value.as_ptr().cast(), + len: value.len(), + } + } +} + +#[link(name = "krun.dll")] +unsafe extern "C" { + fn krun_init_log( + target: u64, + level: u32, + style: u32, + options: u32, + err_out: *mut KrunError, + ) -> KrunResult; + fn krun_mmio_device_manager_new() -> KrunObject; + fn krun_mmio_device_manager_add(manager: KrunObject, device: KrunObject); + fn krun_console_device_builder() -> KrunObject; + fn krun_console_builder_add_default_console( + builder: KrunObject, + stdin: *mut c_void, + stdout: *mut c_void, + stderr: *mut c_void, + err_out: *mut KrunError, + ) -> KrunResult; + fn krun_console_builder_build(builder: KrunObject, err_out: *mut KrunError) -> KrunObject; + fn krun_payload_load_firmware( + path: KrunStr, + cmdline: KrunStr, + err_out: *mut KrunError, + ) -> KrunObject; + fn krun_vmm_builder_new() -> KrunObject; + fn krun_vmm_builder_vcpus( + builder: *mut KrunObject, + count: u8, + err_out: *mut KrunError, + ) -> KrunResult; + fn krun_vmm_builder_ram_mib( + builder: *mut KrunObject, + mib: u32, + err_out: *mut KrunError, + ) -> KrunResult; + fn krun_vmm_builder_payload(builder: *mut KrunObject, payload: KrunObject); + fn krun_vmm_builder_devices(builder: *mut KrunObject, devices: KrunObject); + fn krun_vmm_builder_acpi( + builder: *mut KrunObject, + enabled: bool, + err_out: *mut KrunError, + ) -> KrunResult; + fn krun_vmm_builder_build(builder: *mut KrunObject, err_out: *mut KrunError) -> KrunObject; + fn krun_vmm_run(vmm: KrunObject); + fn krun_balloon_device_new(err_out: *mut KrunError) -> KrunObject; + fn krun_rng_device_new(err_out: *mut KrunError) -> KrunObject; + fn krun_error_result(error: KrunError) -> KrunResult; + fn krun_error_destroy(error: KrunError); + fn krun_result_name_cstr(result: KrunResult) -> *const c_char; +} + +pub struct KrunContext { + vmm: KrunObject, + args: Args, +} + +impl TryFrom for KrunContext { + type Error = anyhow::Error; + fn try_from(args: Args) -> Result { + if args.cpus == 0 { + return Err(anyhow!("vcpus must be a minimum of 1 (0 is invalid)")); + } + if args.memory == 0 { + return Err(anyhow!("zero MiB RAM inputted (invalid)")); + } + if args + .restful_uri + .as_ref() + .is_some_and(|uri| *uri != RestfulUri::None) + { + return Err(anyhow!("the RESTful service is not supported on Windows")); + } + init_logging(&args)?; + let devices = unsafe { krun_mmio_device_manager_new() }; + if devices.is_null() { + return Err(anyhow!("unable to create libkrun device manager")); + } + + let console_builder = unsafe { krun_console_device_builder() }; + let mut error = ptr::null_mut(); + check_result( + unsafe { + krun_console_builder_add_default_console( + console_builder, + io::stdin().as_raw_handle().cast(), + io::stdout().as_raw_handle().cast(), + io::stderr().as_raw_handle().cast(), + &mut error, + ) + }, + error, + "unable to configure virtio console", + )?; + let mut error = ptr::null_mut(); + let console = unsafe { krun_console_builder_build(console_builder, &mut error) }; + check_object(console, error, "unable to build virtio console")?; + unsafe { krun_mmio_device_manager_add(devices, console) }; + add_device( + devices, + unsafe { krun_balloon_device_new(ptr::null_mut()) }, + "balloon", + )?; + add_device( + devices, + unsafe { krun_rng_device_new(ptr::null_mut()) }, + "RNG", + )?; + + unsafe { attach_devices(devices, &args.devices, check_object) }?; + let firmware = args + .firmware_path + .clone() + .or_else(get_firmware_path) + .ok_or_else(|| anyhow!("can't find a firmware to load"))?; + let mut error = ptr::null_mut(); + let payload = unsafe { + krun_payload_load_firmware(KrunStr::new(path_str(&firmware, "firmware")?), KrunStr::new("reboot=k panic=-1 panic_print=0 nomodule console=hvc0 rootfstype=virtiofs rw quiet no_timer_check"), &mut error) + }; + let payload = check_object(payload, error, "unable to load firmware")?; + let mut builder = unsafe { krun_vmm_builder_new() }; + let mut error = ptr::null_mut(); + check_result( + unsafe { krun_vmm_builder_vcpus(&mut builder, args.cpus, &mut error) }, + error, + "unable to configure vCPUs", + )?; + let mut error = ptr::null_mut(); + check_result( + unsafe { krun_vmm_builder_ram_mib(&mut builder, args.memory, &mut error) }, + error, + "unable to configure RAM", + )?; + unsafe { + krun_vmm_builder_payload(&mut builder, payload); + krun_vmm_builder_devices(&mut builder, devices) + }; + let mut error = ptr::null_mut(); + check_result( + unsafe { krun_vmm_builder_acpi(&mut builder, true, &mut error) }, + error, + "unable to enable ACPI", + )?; + let mut error = ptr::null_mut(); + let vmm = unsafe { krun_vmm_builder_build(&mut builder, &mut error) }; + Ok(Self { + vmm: check_object(vmm, error, "unable to build libkrun VMM")?, + args, + }) + } +} + +impl KrunContext { + pub fn run(&self) -> Result<(), anyhow::Error> { + if let Some(pidfile) = &self.args.pidfile { + std::fs::write(pidfile, std::process::id().to_string())?; + } + unsafe { krun_vmm_run(self.vmm) }; + Ok(()) + } +} + +fn add_device( + manager: KrunObject, + device: KrunObject, + description: &str, +) -> Result<(), anyhow::Error> { + if device.is_null() { + return Err(anyhow!("unable to create {description} device")); + } + unsafe { krun_mmio_device_manager_add(manager, device) }; + Ok(()) +} +fn init_logging(args: &Args) -> Result<(), anyhow::Error> { + let (level, options) = args + .krun_log_level + .map(|level| (level, KRUN_LOG_OPTION_NO_ENV)) + .unwrap_or((3, KRUN_LOG_OPTION_ENV)); + let log_file; + let handle = match &args.log_file { + Some(path) => { + log_file = OpenOptions::new().append(true).create(true).open(path)?; + log_file.as_raw_handle() as u64 + } + None => io::stderr().as_raw_handle() as u64, + }; + let mut error = ptr::null_mut(); + check_result( + unsafe { krun_init_log(handle, level, KRUN_LOG_STYLE_AUTO, options, &mut error) }, + error, + "unable to initialize libkrun logging", + ) +} +fn check_object( + object: KrunObject, + error: KrunError, + context: &str, +) -> Result { + check_error(error, context)?; + if object.is_null() { + Err(anyhow!("{context}")) + } else { + Ok(object) + } +} +fn check_result(result: KrunResult, error: KrunError, context: &str) -> Result<(), anyhow::Error> { + check_error(error, context)?; + if result == KRUN_SUCCESS { + Ok(()) + } else { + Err(anyhow!("{context}: libkrun result {result:#x}")) + } +} +fn check_error(error: KrunError, context: &str) -> Result<(), anyhow::Error> { + if error.is_null() { + return Ok(()); + } + let result = unsafe { krun_error_result(error) }; + let name = unsafe { + let name = krun_result_name_cstr(result); + (!name.is_null()).then(|| CStr::from_ptr(name).to_string_lossy().into_owned()) + } + .unwrap_or_else(|| format!("result {result:#x}")); + unsafe { krun_error_destroy(error) }; + Err(anyhow!("{context}: {name}")) +} +fn path_str<'a>(path: &'a Path, description: &str) -> Result<&'a str, anyhow::Error> { + path.to_str() + .with_context(|| format!("{description} path is not valid UTF-8: {}", path.display())) +} +fn get_firmware_path() -> Option { + let executable = std::env::current_exe().ok()?; + let directory = executable.parent()?; + [ + directory.join("OVMF.fd"), + directory.join("edk2/OVMF.fd"), + directory.parent()?.join("edk2/OVMF.fd"), + directory.parent()?.parent()?.join("edk2/OVMF.fd"), + ] + .into_iter() + .find(|path| path.is_file()) +} diff --git a/src/main.rs b/src/main.rs index 96e3170..3e8b3f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod cmdline; mod context; mod status; +#[cfg(target_os = "macos")] mod timesync; mod virtio; diff --git a/src/status/mod.rs b/src/status/mod.rs new file mode 100644 index 0000000..4ac16f0 --- /dev/null +++ b/src/status/mod.rs @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::{net::Ipv4Addr, str::FromStr}; + +use anyhow::{anyhow, Context}; + +#[cfg(unix)] +mod unix; + +#[cfg(unix)] +pub use unix::{get_shutdown_eventfd, status_listener}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum UriScheme { + Tcp, + Unix, + #[default] + None, +} + +impl FromStr for UriScheme { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "tcp" => Ok(Self::Tcp), + "unix" => Ok(Self::Unix), + "none" => Ok(Self::None), + _ => Err(anyhow!("invalid scheme")), + } + } +} + +/// Socket address in which the restful URI socket should listen on. Identical to Rust's +/// SocketAddrV4, but requires a modified FromStr implementation due to how the address is +/// presented on the command line. +#[derive(Clone, Debug, Default, PartialEq)] +pub enum RestfulUri { + Tcp(Ipv4Addr, u16), + Unix(String), + #[default] + None, +} + +impl FromStr for RestfulUri { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let expression = regex::Regex::new(r"^(?Pnone|tcp|unix)://(?P.*)").unwrap(); + let Some(cap) = expression.captures(s) else { + return Err(anyhow!("invalid scheme input")); + }; + let scheme = &cap["scheme"]; + let value = &cap["value"]; + match UriScheme::from_str(scheme)? { + UriScheme::Tcp => { + let (ip_addr, port) = parse_tcp_input(value)?; + Ok(Self::Tcp(ip_addr, port)) + } + UriScheme::Unix => { + if value.is_empty() { + return Err(anyhow!("empty unix socket path")); + } + Ok(Self::Unix(value.to_string())) + } + UriScheme::None => Ok(Self::None), + } + } +} + +fn parse_tcp_input(input: &str) -> Result<(Ipv4Addr, u16), anyhow::Error> { + let mut parts: Vec = input.split(':').map(|s| s.to_string()).collect(); + if parts.len() != 2 { + return Err(anyhow!("restful URI formatted incorrectly")); + } + + // Ipv4Address's FromStr does not understand that the "localhost" IP address translates to + // 127.0.0.1, this must be manually translated. + if &parts[0][..] == "localhost" { + parts[0] = String::from("127.0.0.1"); + } + + let ip_addr = + Ipv4Addr::from_str(&parts[0]).context("restful URI IP address formatted incorrectly")?; + let port = u16::from_str(&parts[1]).context("restful URI port number formatted incorrectly")?; + Ok((ip_addr, port)) +} + +#[allow(unused_imports)] +mod tests { + use super::*; + + #[test] + fn parse_valid_unix_scheme() { + assert_eq!( + RestfulUri::Unix("/tmp/path".to_string()), + RestfulUri::from_str("unix:///tmp/path").unwrap() + ); + } + + #[test] + fn parse_unix_scheme_missing_path() { + assert_eq!( + anyhow!("empty unix socket path").to_string(), + RestfulUri::from_str("unix://").err().unwrap().to_string() + ); + } + + #[test] + fn parse_unix_scheme_missing_slashes() { + assert_eq!( + anyhow!("invalid scheme input").to_string(), + RestfulUri::from_str("unix:").err().unwrap().to_string() + ); + } + + #[test] + fn parse_unix_scheme_misspelling() { + assert_eq!( + anyhow!("invalid scheme input").to_string(), + RestfulUri::from_str("uni://path") + .err() + .unwrap() + .to_string() + ); + } + + #[test] + fn parse_valid_tcp_scheme() { + assert_eq!( + RestfulUri::Tcp(Ipv4Addr::new(127, 0, 0, 1), 8080), + RestfulUri::from_str("tcp://localhost:8080").unwrap(), + ); + } + + #[test] + fn parse_tcp_scheme_missing_port() { + assert_eq!( + anyhow!("restful URI formatted incorrectly").to_string(), + RestfulUri::from_str("tcp://localhost") + .err() + .unwrap() + .to_string() + ); + } + + #[test] + fn parse_tcp_scheme_with_unix_path() { + assert_eq!( + anyhow!("restful URI formatted incorrectly").to_string(), + RestfulUri::from_str("tcp:///tmp/path") + .err() + .unwrap() + .to_string(), + ); + } + + #[test] + fn parse_valid_none_scheme() { + assert_eq!(RestfulUri::None, RestfulUri::from_str("none://").unwrap()); + } + + #[test] + fn parse_none_scheme_missing_postfix() { + assert_eq!( + anyhow!("invalid scheme input").to_string(), + RestfulUri::from_str("none").err().unwrap().to_string(), + ); + } + + #[test] + fn parse_random_string_scheme() { + assert_eq!( + anyhow!("invalid scheme input").to_string(), + RestfulUri::from_str("foobar").err().unwrap().to_string(), + ); + } +} \ No newline at end of file diff --git a/src/status.rs b/src/status/unix.rs similarity index 68% rename from src/status.rs rename to src/status/unix.rs index ab6a961..080644d 100644 --- a/src/status.rs +++ b/src/status/unix.rs @@ -3,17 +3,18 @@ use std::{ fs::File, io::{ErrorKind, Read, Write}, - net::{Ipv4Addr, TcpListener}, + net::TcpListener, os::{ fd::{FromRawFd, RawFd}, unix::net::UnixListener, }, - str::FromStr, }; -use anyhow::{anyhow, Context}; +use anyhow::anyhow; use serde::{Deserialize, Serialize}; +use super::RestfulUri; + #[link(name = "krun")] extern "C" { fn krun_get_shutdown_eventfd(ctx_id: u32) -> i32; @@ -21,84 +22,8 @@ extern "C" { const VM_STATE_PATH: &str = "/vm/state"; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum UriScheme { - Tcp, - Unix, - #[default] - None, -} - -impl FromStr for UriScheme { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "tcp" => Ok(Self::Tcp), - "unix" => Ok(Self::Unix), - "none" => Ok(Self::None), - _ => Err(anyhow!("invalid scheme")), - } - } -} - -/// Socket address in which the restful URI socket should listen on. Identical to Rust's -/// SocketAddrV4, but requires a modified FromStr implementation due to how the address is -/// presented on the command line. -#[derive(Clone, Debug, Default, PartialEq)] -pub enum RestfulUri { - Tcp(Ipv4Addr, u16), - Unix(String), - #[default] - None, -} - -impl FromStr for RestfulUri { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let expression = regex::Regex::new(r"^(?Pnone|tcp|unix)://(?P.*)").unwrap(); - let Some(cap) = expression.captures(s) else { - return Err(anyhow!("invalid scheme input")); - }; - let scheme = &cap["scheme"]; - let value = &cap["value"]; - match UriScheme::from_str(scheme)? { - UriScheme::Tcp => { - let (ip_addr, port) = parse_tcp_input(value)?; - Ok(Self::Tcp(ip_addr, port)) - } - UriScheme::Unix => { - if value.is_empty() { - return Err(anyhow!("empty unix socket path")); - } - Ok(Self::Unix(value.to_string())) - } - UriScheme::None => Ok(Self::None), - } - } -} - -fn parse_tcp_input(input: &str) -> Result<(Ipv4Addr, u16), anyhow::Error> { - let mut parts: Vec = input.split(':').map(|s| s.to_string()).collect(); - if parts.len() != 2 { - return Err(anyhow!("restful URI formatted incorrectly")); - } - - // Ipv4Address's FromStr does not understand that the "localhost" IP address translates to - // 127.0.0.1, this must be manually translated. - if &parts[0][..] == "localhost" { - parts[0] = String::from("127.0.0.1"); - } - - let ip_addr = - Ipv4Addr::from_str(&parts[0]).context("restful URI IP address formatted incorrectly")?; - let port = u16::from_str(&parts[1]).context("restful URI port number formatted incorrectly")?; - Ok((ip_addr, port)) -} - -/// Retrieve the shutdown event file descriptor initialized by libkrun. -pub unsafe fn get_shutdown_eventfd(ctx_id: u32) -> i32 { +/// Retrieve the shutdown event file descriptor / handle initialized by libkrun. +pub unsafe fn get_shutdown_eventfd(ctx_id: u32) -> RawFd { let fd = krun_get_shutdown_eventfd(ctx_id); if fd < 0 { panic!("unable to retrieve krun shutdown file descriptor"); @@ -232,7 +157,7 @@ fn handle_incoming_stream( let body = &buf[header_len..body_end]; let state_req: VmStateRequest = match serde_json::from_slice(body) { - Ok(r) => r, + Ok(request) => request, Err(_) => { write_http_response( stream, @@ -263,9 +188,7 @@ fn handle_incoming_stream( } } } - _ => { - write_http_error(stream, 405, "Method Not Allowed"); - } + _ => write_http_error(stream, 405, "Method Not Allowed"), } } @@ -311,20 +234,21 @@ pub fn status_listener( #[cfg(test)] mod tests { + use std::{io::Cursor, os::fd::AsRawFd}; + use super::*; - use std::io::Cursor; fn make_request(method: &str, path: &str, body: Option<&str>) -> Vec { - let mut req = format!("{method} {path} HTTP/1.1\r\nHost: localhost\r\n"); - if let Some(b) = body { - req.push_str(&format!("Content-Length: {}\r\n", b.len())); - req.push_str("Content-Type: application/json\r\n"); + let mut request = format!("{method} {path} HTTP/1.1\r\nHost: localhost\r\n"); + if let Some(body) = body { + request.push_str(&format!("Content-Length: {}\r\n", body.len())); + request.push_str("Content-Type: application/json\r\n"); } - req.push_str("\r\n"); - if let Some(b) = body { - req.push_str(b); + request.push_str("\r\n"); + if let Some(body) = body { + request.push_str(body); } - req.into_bytes() + request.into_bytes() } struct MockStream { @@ -332,17 +256,18 @@ mod tests { written: Vec, } - impl std::io::Read for MockStream { + impl Read for MockStream { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.read.read(buf) } } - impl std::io::Write for MockStream { + impl Write for MockStream { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.written.extend_from_slice(buf); Ok(buf.len()) } + fn flush(&mut self) -> std::io::Result<()> { Ok(()) } @@ -353,13 +278,10 @@ mod tests { read: Cursor::new(request.to_vec()), written: Vec::new(), }; - - let (sock_a, _sock_b) = std::os::unix::net::UnixStream::pair().unwrap(); - let mut shutdown_fd = - unsafe { File::from_raw_fd(std::os::fd::AsRawFd::as_raw_fd(&sock_a)) }; + let (socket, _peer) = std::os::unix::net::UnixStream::pair().unwrap(); + let mut shutdown_fd = unsafe { File::from_raw_fd(socket.as_raw_fd()) }; handle_incoming_stream(&mut stream, &mut shutdown_fd, stopping); - std::mem::forget(shutdown_fd); String::from_utf8(stream.written).unwrap() @@ -369,7 +291,7 @@ mod tests { response .split_whitespace() .nth(1) - .and_then(|s| s.parse().ok()) + .and_then(|status| status.parse().ok()) .unwrap() } @@ -493,90 +415,4 @@ mod tests { assert_eq!(json["canHardStop"], true); assert_eq!(json["canPause"], false); } - - #[test] - fn parse_valid_unix_scheme() { - assert_eq!( - RestfulUri::Unix("/tmp/path".to_string()), - RestfulUri::from_str("unix:///tmp/path").unwrap() - ); - } - - #[test] - fn parse_unix_scheme_missing_path() { - assert_eq!( - anyhow!("empty unix socket path").to_string(), - RestfulUri::from_str("unix://").err().unwrap().to_string() - ); - } - - #[test] - fn parse_unix_scheme_missing_slashes() { - assert_eq!( - anyhow!("invalid scheme input").to_string(), - RestfulUri::from_str("unix:").err().unwrap().to_string() - ); - } - - #[test] - fn parse_unix_scheme_misspelling() { - assert_eq!( - anyhow!("invalid scheme input").to_string(), - RestfulUri::from_str("uni://path") - .err() - .unwrap() - .to_string() - ); - } - - #[test] - fn parse_valid_tcp_scheme() { - assert_eq!( - RestfulUri::Tcp(Ipv4Addr::new(127, 0, 0, 1), 8080), - RestfulUri::from_str("tcp://localhost:8080").unwrap(), - ); - } - - #[test] - fn parse_tcp_scheme_missing_port() { - assert_eq!( - anyhow!("restful URI formatted incorrectly").to_string(), - RestfulUri::from_str("tcp://localhost") - .err() - .unwrap() - .to_string() - ); - } - - #[test] - fn parse_tcp_scheme_with_unix_path() { - assert_eq!( - anyhow!("restful URI formatted incorrectly").to_string(), - RestfulUri::from_str("tcp:///tmp/path") - .err() - .unwrap() - .to_string(), - ); - } - - #[test] - fn parse_valid_none_scheme() { - assert_eq!(RestfulUri::None, RestfulUri::from_str("none://").unwrap()); - } - - #[test] - fn parse_none_scheme_missing_postfix() { - assert_eq!( - anyhow!("invalid scheme input").to_string(), - RestfulUri::from_str("none").err().unwrap().to_string(), - ); - } - - #[test] - fn parse_random_string_scheme() { - assert_eq!( - anyhow!("invalid scheme input").to_string(), - RestfulUri::from_str("foobar").err().unwrap().to_string(), - ); - } } diff --git a/src/virtio.rs b/src/virtio/mod.rs similarity index 64% rename from src/virtio.rs rename to src/virtio/mod.rs index 1d3baf3..6aeee6f 100644 --- a/src/virtio.rs +++ b/src/virtio/mod.rs @@ -1,19 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::cmdline::{ - check_required_args, check_unknown_args, cstring_to_ptr, parse_args, parse_boolean, -}; - -use std::{ - ffi::{c_char, c_int, CString}, - os::fd::RawFd, - os::unix::ffi::OsStrExt, - path::{Path, PathBuf}, - str::FromStr, -}; +use crate::cmdline::{check_required_args, check_unknown_args, parse_args, parse_boolean}; + +use std::{path::PathBuf, str::FromStr}; use anyhow::{anyhow, Context, Result}; use mac_address::MacAddress; +#[cfg(unix)] +pub type PlatformSocket = std::os::fd::RawFd; +#[cfg(windows)] +pub type PlatformSocket = std::os::windows::io::RawSocket; /// Taken from https://github.com/containers/libkrun/blob/7116644749c7b1028a970c9e8bd2d0163745a225/include/libkrun.h#L269 const NET_FEATURE_CSUM: u32 = 1 << 0; @@ -26,7 +22,7 @@ const NET_FEATURE_HOST_TSO6: u32 = 1 << 12; const NET_FEATURE_HOST_UFO: u32 = 1 << 14; /// These are the features enabled by krun_set_passt_fd and krun_set_gvproxy_path. -const COMPAT_NET_FEATURES: u32 = NET_FEATURE_CSUM +pub(crate) const COMPAT_NET_FEATURES: u32 = NET_FEATURE_CSUM | NET_FEATURE_GUEST_CSUM | NET_FEATURE_GUEST_TSO4 | NET_FEATURE_GUEST_UFO @@ -35,49 +31,18 @@ const COMPAT_NET_FEATURES: u32 = NET_FEATURE_CSUM /// Send the VFKIT magic after establishing the connection, /// as required by gvproxy in vfkit mode. -const NET_FLAG_VFKIT: u32 = 1 << 0; +pub(crate) const NET_FLAG_VFKIT: u32 = 1 << 0; const SOCK_TYPE_UNIX_SOCKET_PATH: &str = "unixSocketPath"; const SOCK_TYPE_UNIXGRAM: &str = "unixgram"; const SOCK_TYPE_UNIXSTREAM: &str = "unixstream"; -#[link(name = "krun")] -extern "C" { - fn krun_add_disk2( - ctx_id: u32, - c_block_id: *const c_char, - c_disk_path: *const c_char, - disk_format: u32, - read_only: bool, - ) -> i32; - fn krun_add_vsock_port2(ctx_id: u32, port: u32, c_filepath: *const c_char, listen: bool) - -> i32; - fn krun_add_virtiofs4( - ctx_id: u32, - c_tag: *const c_char, - c_path: *const c_char, - shm_size: u64, - read_only: bool, - semantics: u32, - ) -> i32; - fn krun_set_console_output(ctx_id: u32, c_filepath: *const c_char) -> i32; - fn krun_add_net_unixgram( - ctx_id: u32, - c_path: *const c_char, - fd: c_int, - c_mac: *const u8, - features: u32, - flags: u32, - ) -> i32; - fn krun_add_net_unixstream( - ctx_id: u32, - c_path: *const c_char, - fd: c_int, - c_mac: *const u8, - features: u32, - flags: u32, - ) -> i32; -} +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub use unix::KrunContextSet; +#[cfg(windows)] +pub(crate) mod windows; #[repr(u32)] #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] @@ -118,12 +83,6 @@ impl FromStr for FsPermissions { } } -/// Each virito device configures itself with krun differently. This is used by each virtio device -/// to set their respective configurations with libkrun. -pub trait KrunContextSet { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error>; -} - /// virtio device configurations. #[derive(Clone, Debug, PartialEq)] pub enum VirtioDeviceConfig { @@ -169,23 +128,6 @@ impl FromStr for VirtioDeviceConfig { } } -/// Configure the device in the krun context based on which underlying device is contained. -impl KrunContextSet for VirtioDeviceConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - match self { - Self::Blk(blk) => blk.krun_ctx_set(id), - Self::Vsock(vsock) => vsock.krun_ctx_set(id), - Self::Net(net) => net.krun_ctx_set(id), - Self::Fs(fs) => fs.krun_ctx_set(id), - Self::Serial(serial) => serial.krun_ctx_set(id), - - // virtio-input, virtio-gpu, and virtio-rng devices are currently not configured in - // krun. - _ => Ok(()), - } - } -} - /// Configuration of a virtio-blk device. #[derive(Clone, Debug, Default, PartialEq)] pub struct BlkConfig { @@ -194,6 +136,8 @@ pub struct BlkConfig { /// Format of the disk image. pub format: DiskImageFormat, + + pub serial: Option, } impl FromStr for BlkConfig { @@ -212,40 +156,18 @@ impl FromStr for BlkConfig { blk_config.format = DiskImageFormat::from_str(f.as_str())?; } + // Parse custom serial / id if provided. + if let Some(s) = args.remove("serial") { + blk_config.serial = Some(s); + } else if let Some(s) = args.remove("id") { + blk_config.serial = Some(s); + } check_unknown_args(args, "virtio-blk")?; Ok(blk_config) } } -/// Set the virtio-blk device to be the krun VM's root disk. -impl KrunContextSet for BlkConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - let basename = match self.path.file_name() { - Some(osstr) => osstr.to_str().unwrap_or("disk"), - None => "disk", - }; - let block_id_cstr = CString::new(basename).context("can't convert basename to cstring")?; - let path_cstr = path_to_cstring(&self.path)?; - - if krun_add_disk2( - id, - block_id_cstr.as_ptr(), - path_cstr.as_ptr(), - self.format as u32, - false, - ) < 0 - { - return Err(anyhow!(format!( - "unable to set virtio-blk disk for {}", - self.path.display() - ))); - } - - Ok(()) - } -} - /// Configuration of a virtio-serial device. #[derive(Clone, Debug, PartialEq)] pub struct SerialConfig { @@ -270,21 +192,6 @@ impl FromStr for SerialConfig { } } -/// Set the krun console output to be written to the virtio-serial's log file. -impl KrunContextSet for SerialConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - let path_cstr = path_to_cstring(&self.log_file_path)?; - - if krun_set_console_output(id, path_cstr.as_ptr()) < 0 { - return Err(anyhow!( - "unable to set krun console output redirection to virtio-serial log file" - )); - } - - Ok(()) - } -} - /// Configuration of a virtio-vsock device. #[derive(Clone, Debug, Default, PartialEq)] pub struct VsockConfig { @@ -341,31 +248,6 @@ impl FromStr for VsockConfig { } } -/// Map the virtio-vsock's guest port and host path to enable the krun VM to communicate with the -/// socket on the host. -impl KrunContextSet for VsockConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - let path_cstr = path_to_cstring(&self.socket_url)?; - - // libkrun's `listen` parameter means "guest expects connections from host" which is true when VsockAction::Connect. - if krun_add_vsock_port2( - id, - self.port, - path_cstr.as_ptr(), - self.action == VsockAction::Connect, - ) < 0 - { - return Err(anyhow!(format!( - "unable to add vsock port {} for path {}", - self.port, - &self.socket_url.display() - ))); - } - - Ok(()) - } -} - /// virtio-vsock action. #[derive(Clone, Debug, Default, PartialEq)] pub enum VsockAction { @@ -393,7 +275,7 @@ impl FromStr for VsockAction { #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct SocketConfig { pub path: Option, - pub fd: Option, + pub fd: Option, pub offloading: bool, pub send_vfkit_magic: bool, } @@ -501,7 +383,7 @@ fn parse_socket_config( if let Some(fd) = args.remove("fd") { socket_config.fd = Some( - fd.parse::() + fd.parse::() .context("virtio-net unable to convert \"fd\" value {fd} to a file descriptor")?, ); } @@ -528,80 +410,6 @@ fn parse_socket_config( Ok((socket_type, socket_config)) } -/// Set the gvproxy's path and network MAC address. -impl KrunContextSet for NetConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - match &self.socket_type { - SocketType::UnixGram => { - let features = if self.socket_config.offloading { - COMPAT_NET_FEATURES - } else { - 0 - }; - - let path = match &self.socket_config.path { - Some(path) => path_to_cstring(path)?, - None => path_to_cstring(&PathBuf::new())?, - }; - - let flags = if self.socket_config.send_vfkit_magic { - NET_FLAG_VFKIT - } else { - 0 - }; - - if krun_add_net_unixgram( - id, - cstring_to_ptr(&path), - self.socket_config.fd.unwrap_or(-1), - self.mac_address.bytes().as_ptr(), - features, - flags, - ) < 0 - { - // TODO(jakecorrenti): if this fails, we should display all of the values the - // user provided to the virtio-net cmdline - return Err(anyhow!(format!( - "virtio-net unable to add device with unix datagram backend {:#?}", - self.socket_config - ))); - } - } - SocketType::UnixStream => { - let features = if self.socket_config.offloading { - COMPAT_NET_FEATURES - } else { - 0 - }; - - let path = match &self.socket_config.path { - Some(path) => path_to_cstring(path)?, - None => path_to_cstring(&PathBuf::new())?, - }; - - if krun_add_net_unixstream( - id, - cstring_to_ptr(&path), - self.socket_config.fd.unwrap_or(-1), - self.mac_address.bytes().as_ptr(), - features, - 0, - ) < 0 - { - // TODO(jakecorrenti): if this fails, we should display all of the values the - // user provided to the virtio-net cmdline - return Err(anyhow!(format!( - "virtio-net unable to add device with unix stream backend {:#?}", - self.socket_config - ))); - } - } - } - - Ok(()) - } -} - /// Configuration of a virtio-fs device. #[derive(Clone, Debug, Default, PartialEq)] pub struct FsConfig { @@ -640,32 +448,6 @@ impl FromStr for FsConfig { } } -/// Set the shared directory with its guest mount tag. -impl KrunContextSet for FsConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - let shared_dir_cstr = path_to_cstring(&self.shared_dir)?; - let mount_tag_cstr = path_to_cstring(&self.mount_tag)?; - - if krun_add_virtiofs4( - id, - mount_tag_cstr.as_ptr(), - shared_dir_cstr.as_ptr(), - 0, - false, - self.permission_semantics.clone() as u32, - ) < 0 - { - return Err(anyhow!(format!( - "unable to add virtiofs shared directory {} with mount tag {}", - &self.shared_dir.display(), - &self.mount_tag.display() - ))); - } - - Ok(()) - } -} - /// Configuration of a virtio-gpu device. #[derive(Clone, Debug, Default, PartialEq)] pub struct GpuConfig { @@ -733,13 +515,3 @@ impl FromStr for InputConfig { } } } - -/// Construct a NULL-terminated C string from a Rust Path object. -fn path_to_cstring(path: &Path) -> Result { - let cstring = CString::new(path.as_os_str().as_bytes()).context(format!( - "unable to convert path {} into NULL-terminated C string", - path.display() - ))?; - - Ok(cstring) -} diff --git a/src/virtio/unix.rs b/src/virtio/unix.rs new file mode 100644 index 0000000..4984dea --- /dev/null +++ b/src/virtio/unix.rs @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::cmdline::cstring_to_ptr; +use std::{ + ffi::{c_char, c_int, CString}, + os::unix::ffi::OsStrExt, + path::Path, +}; + +#[link(name = "krun")] +unsafe extern "C" { + fn krun_add_disk2( + ctx_id: u32, + block_id: *const c_char, + path: *const c_char, + format: u32, + read_only: bool, + ) -> i32; + fn krun_add_vsock_port2(ctx_id: u32, port: u32, path: *const c_char, listen: bool) -> i32; + fn krun_add_virtiofs4( + ctx_id: u32, + tag: *const c_char, + path: *const c_char, + shm_size: u64, + read_only: bool, + semantics: u32, + ) -> i32; + fn krun_set_console_output(ctx_id: u32, path: *const c_char) -> i32; + fn krun_add_net_unixgram( + ctx_id: u32, + path: *const c_char, + fd: c_int, + mac: *const u8, + features: u32, + flags: u32, + ) -> i32; + fn krun_add_net_unixstream( + ctx_id: u32, + path: *const c_char, + fd: c_int, + mac: *const u8, + features: u32, + flags: u32, + ) -> i32; +} + +/// Each virito device configures itself with krun differently. This is used by each virtio device +/// to set their respective configurations with libkrun. +pub trait KrunContextSet { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error>; +} + +/// Configure the device in the krun context based on which underlying device is contained +impl KrunContextSet for VirtioDeviceConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + match self { + Self::Blk(blk) => blk.krun_ctx_set(id), + Self::Vsock(vsock) => vsock.krun_ctx_set(id), + Self::Net(net) => net.krun_ctx_set(id), + Self::Fs(fs) => fs.krun_ctx_set(id), + Self::Serial(serial) => serial.krun_ctx_set(id), + + // virtio-input, virtio-gpu, and virtio-rng devices are currently not configured in + // krun. + _ => Ok(()), + } + } +} + +impl KrunContextSet for BlkConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + let basename = match self.path.file_name() { + Some(osstr) => osstr.to_str().unwrap_or("disk"), + None => "disk", + }; + let block_id_cstr = CString::new(basename).context("can't convert basename to cstring")?; + let path_cstr = path_to_cstring(&self.path)?; + + if krun_add_disk2( + id, + block_id_cstr.as_ptr(), + path_cstr.as_ptr(), + self.format as u32, + false, + ) < 0 + { + return Err(anyhow!(format!( + "unable to set virtio-blk disk for {}", + self.path.display() + ))); + } + + Ok(()) + } +} + +/// Set the krun console output to be written to the virtio-serial's log file. +impl KrunContextSet for SerialConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + let path = path_to_cstring(&self.log_file_path)?; + + if unsafe { krun_set_console_output(id, path.as_ptr()) } < 0 { + return Err(anyhow!( + "unable to set krun console output redirection to virtio-serial log file" + )); + } + + Ok(()) + } +} + +/// Map the virtio-vsock's guest port and host path to enable the krun VM to communicate with the +/// socket on the host. +impl KrunContextSet for VsockConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + let path_cstr = path_to_cstring(&self.socket_url)?; + + // libkrun's `listen` parameter means "guest expects connections from host" which is true when VsockAction::Connect. + if krun_add_vsock_port2( + id, + self.port, + path_cstr.as_ptr(), + self.action == VsockAction::Connect, + ) < 0 + { + return Err(anyhow!(format!( + "unable to add vsock port {} for path {}", + self.port, + self.socket_url.display() + ))); + } + + Ok(()) + } +} + +impl KrunContextSet for NetConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + match &self.socket_type { + SocketType::UnixGram => { + let features = if self.socket_config.offloading { + COMPAT_NET_FEATURES + } else { + 0 + }; + + let path = match &self.socket_config.path { + Some(path) => path_to_cstring(path)?, + None => path_to_cstring(&PathBuf::new())?, + }; + + let flags = if self.socket_config.send_vfkit_magic { + NET_FLAG_VFKIT + } else { + 0 + }; + + if krun_add_net_unixgram( + id, + cstring_to_ptr(&path), + self.socket_config.fd.unwrap_or(-1), + self.mac_address.bytes().as_ptr(), + features, + flags, + ) < 0 + { + // TODO(jakecorrenti): if this fails, we should display all of the values the + // user provided to the virtio-net cmdline + return Err(anyhow!(format!( + "virtio-net unable to add device with unix datagram backend {:#?}", + self.socket_config + ))); + } + } + SocketType::UnixStream => { + let features = if self.socket_config.offloading { + COMPAT_NET_FEATURES + } else { + 0 + }; + + let path = match &self.socket_config.path { + Some(path) => path_to_cstring(path)?, + None => path_to_cstring(&PathBuf::new())?, + }; + + if krun_add_net_unixstream( + id, + cstring_to_ptr(&path), + self.socket_config.fd.unwrap_or(-1), + self.mac_address.bytes().as_ptr(), + features, + 0, + ) < 0 + { + // TODO(jakecorrenti): if this fails, we should display all of the values the + // user provided to the virtio-net cmdline + return Err(anyhow!(format!( + "virtio-net unable to add device with unix stream backend {:#?}", + self.socket_config + ))); + } + } + } + + Ok(()) + } +} + +impl KrunContextSet for FsConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + let shared_dir_cstr = path_to_cstring(&self.shared_dir)?; + let mount_tag_cstr = path_to_cstring(&self.mount_tag)?; + + if krun_add_virtiofs4( + id, + mount_tag_cstr.as_ptr(), + shared_dir_cstr.as_ptr(), + 0, + false, + self.permission_semantics.clone() as u32, + ) < 0 + { + return Err(anyhow!(format!( + "unable to add virtiofs shared directory {} with mount tag {}", + self.shared_dir.display(), + self.mount_tag.display() + ))); + } + + Ok(()) + } +} + +/// Construct a NULL-terminated C string from a Rust Path object. +fn path_to_cstring(path: &Path) -> Result { + let cstring = CString::new(path.as_os_str().as_bytes()).context(format!( + "unable to convert path {} into NULL-terminated C string", + path.display() + ))?; + + Ok(cstring) +} diff --git a/src/virtio/windows.rs b/src/virtio/windows.rs new file mode 100644 index 0000000..d03b57b --- /dev/null +++ b/src/virtio/windows.rs @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use anyhow::{anyhow, Context, Result}; +use std::{ + ffi::{c_char, c_void}, + path::Path, + ptr, +}; + +type KrunObject = *mut c_void; +type KrunError = *mut c_void; + +#[repr(C)] +#[derive(Clone, Copy)] +struct KrunStr { + data: *const c_char, + len: usize, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct KrunBytes { + data: *const u8, + len: usize, +} + +impl KrunStr { + fn new(value: &str) -> Self { + Self { + data: value.as_ptr().cast(), + len: value.len(), + } + } +} + +#[link(name = "krun.dll")] +unsafe extern "C" { + fn krun_mmio_device_manager_add(manager: KrunObject, device: KrunObject); + fn krun_block_device_new( + id: KrunStr, + path: KrunStr, + format: u32, + err_out: *mut KrunError, + ) -> KrunObject; + fn krun_fs_device_new(tag: KrunStr, host_path: KrunStr, err_out: *mut KrunError) -> KrunObject; + fn krun_net_device_new_unixstream_path( + id: KrunStr, + path: KrunStr, + mac: KrunBytes, + features: u32, + flags: u32, + err_out: *mut KrunError, + ) -> KrunObject; + fn krun_vsock_device_new(cid: u64, tsi_features: u32, err_out: *mut KrunError) -> KrunObject; + fn krun_vsock_device_add_unix_port(vsock: KrunObject, port: u32, path: KrunStr, listen: bool); +} + +pub(crate) unsafe fn attach_devices( + manager: *mut c_void, + configs: &[VirtioDeviceConfig], + check_object: fn(KrunObject, KrunError, &str) -> Result, +) -> Result<()> { + for (index, config) in configs.iter().enumerate() { + let device = match config { + VirtioDeviceConfig::Blk(block) => { + let generated_id; + let id = match block.serial.as_deref() { + Some(id) => id, + None => { + generated_id = format!("disk{index}"); + &generated_id + } + }; + let mut error = ptr::null_mut(); + let device = unsafe { + krun_block_device_new( + KrunStr::new(id), + KrunStr::new(path_str(&block.path, "block device")?), + block.format as u32, + &mut error, + ) + }; + check_object(device, error, "unable to create block device")? + } + VirtioDeviceConfig::Fs(fs) => { + let mut error = ptr::null_mut(); + let device = unsafe { + krun_fs_device_new( + KrunStr::new(path_str(&fs.mount_tag, "virtio-fs mount tag")?), + KrunStr::new(path_str(&fs.shared_dir, "virtio-fs shared directory")?), + &mut error, + ) + }; + check_object(device, error, "unable to create virtio-fs device")? + } + VirtioDeviceConfig::Net(net) => { + if net.socket_type != SocketType::UnixStream || net.socket_config.fd.is_some() { + return Err(anyhow!( + "Windows supports only path-based type=unixstream networking" + )); + } + let path = + net.socket_config.path.as_deref().ok_or_else(|| { + anyhow!("virtio-net type=unixstream requires path=") + })?; + let id = format!("net{index}"); + let features = if net.socket_config.offloading { + COMPAT_NET_FEATURES + } else { + 0 + }; + let flags = if net.socket_config.send_vfkit_magic { + NET_FLAG_VFKIT + } else { + 0 + }; + let mut error = ptr::null_mut(); + let device = unsafe { + krun_net_device_new_unixstream_path( + KrunStr::new(&id), + KrunStr::new(path_str(path, "network socket")?), + KrunBytes { + data: net.mac_address.bytes().as_ptr(), + len: 6, + }, + features, + flags, + &mut error, + ) + }; + check_object(device, error, "unable to create network device")? + } + VirtioDeviceConfig::Vsock(config) => { + let mut error = ptr::null_mut(); + let device = unsafe { krun_vsock_device_new(3, 0, &mut error) }; + let device = check_object(device, error, "unable to create vsock device")?; + unsafe { + krun_vsock_device_add_unix_port( + device, + config.port, + KrunStr::new(path_str(&config.socket_url, "vsock socket")?), + config.action == VsockAction::Connect, + ) + }; + device + } + _ => return Err(anyhow!("unsupported device on Windows: {config:?}")), + }; + unsafe { krun_mmio_device_manager_add(manager, device) }; + } + Ok(()) +} + +fn path_str<'a>(path: &'a Path, description: &str) -> Result<&'a str> { + path.to_str() + .with_context(|| format!("{description} path is not valid UTF-8: {}", path.display())) +}