diff --git a/Cargo.lock b/Cargo.lock index 16ac2ea29..31f1dfd49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "bzip2" version = "0.5.2" @@ -235,7 +241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -398,12 +404,54 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.12", +] + [[package]] name = "devices" version = "0.1.0" dependencies = [ "arch", "bitflags 1.3.2", + "bytes", "caps", "crossbeam-channel", "env_logger", @@ -415,12 +463,20 @@ dependencies = [ "libloading", "log", "lru", + "mio", "nix 0.24.3", "pipewire", + "pnet", "polly", "rand", + "rustix", "rutabaga_gfx", - "thiserror", + "smoltcp", + "socket2", + "tempfile", + "thiserror 1.0.69", + "tokio", + "tracing", "utils", "virtio-bindings", "vm-fdt", @@ -475,6 +531,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "flate2" version = "1.1.1" @@ -630,6 +702,15 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -641,6 +722,16 @@ dependencies = [ "foldhash", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -740,6 +831,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8972d5be69940353d5347a1344cb375d9b457d6809b428b05bb1ca2fb9ce007" +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "itertools" version = "0.12.1" @@ -838,9 +938,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" [[package]] name = "libkrun" @@ -931,6 +1031,22 @@ dependencies = [ "vm-memory", ] +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -946,6 +1062,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + [[package]] name = "memchr" version = "2.7.4" @@ -985,6 +1107,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + [[package]] name = "nix" version = "0.24.3" @@ -1033,6 +1167,12 @@ dependencies = [ "libc", ] +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + [[package]] name = "nom" version = "7.1.3" @@ -1127,6 +1267,29 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1153,7 +1316,7 @@ dependencies = [ "nix 0.27.1", "once_cell", "pipewire-sys", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1173,6 +1336,97 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "pnet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" +dependencies = [ + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_macros" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "pnet_macros_support" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pnet_transport" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", +] + [[package]] name = "polly" version = "0.0.1" @@ -1200,6 +1454,28 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -1269,12 +1545,6 @@ dependencies = [ "getrandom 0.2.15", ] -[[package]] -name = "rangemap" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" - [[package]] name = "rdrand" version = "0.8.3" @@ -1284,6 +1554,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -1292,7 +1571,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1362,6 +1641,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustversion" version = "1.0.20" @@ -1379,7 +1671,7 @@ dependencies = [ "nix 0.26.4", "pkg-config", "remain", - "thiserror", + "thiserror 1.0.69", "winapi", "zerocopy 0.6.6", ] @@ -1399,6 +1691,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.26" @@ -1522,6 +1820,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.9" @@ -1544,16 +1851,38 @@ dependencies = [ "vm-memory", ] +[[package]] +name = "smoltcp" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt 0.3.100", + "heapless", + "libc", + "log", + "managed", +] + [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1590,6 +1919,19 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1605,7 +1947,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -1619,6 +1970,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.44.2" @@ -1626,7 +1988,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1768,7 +2149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1720e7240cdc739f935456eb77f370d7e9b2a3909204da1e2b47bef1137a013" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.69", "winapi", ] @@ -1796,7 +2177,6 @@ dependencies = [ "nix 0.24.3", "polly", "procfs", - "rangemap", "rdrand", "serde", "serde_json", diff --git a/src/devices/Cargo.toml b/src/devices/Cargo.toml index f89a929c2..35408af06 100644 --- a/src/devices/Cargo.toml +++ b/src/devices/Cargo.toml @@ -18,10 +18,10 @@ virgl_resource_map2 = [] bitflags = "1.2.0" crossbeam-channel = ">=0.5.15" env_logger = "0.9.0" -libc = ">=0.2.39" +libc = ">=0.2.173" libloading = "0.8" log = "0.4.0" -nix = { version = "0.24.1", features = ["poll"] } +nix = { version = "0.24.1", features = ["poll", "event"] } pw = { package = "pipewire", version = "0.8.0", optional = true } rand = "0.8.5" thiserror = { version = "1.0", optional = true } @@ -29,11 +29,29 @@ virtio-bindings = "0.2.0" vm-memory = { version = ">=0.13", features = ["backend-mmap"] } zerocopy = { version = "0.6.3", optional = true } zerocopy-derive = { version = "0.6.3", optional = true } +bytes = "1" +mio = { version = "1.0.4", features = ["net", "os-ext", "os-poll"] } +socket2 = { version = "0.5.10", features = ["all"] } +pnet = "0.35.0" +tracing = { version = "0.1.41" } # , features = ["release_max_level_debug"] +rustix = { version = "1", features = ["fs"] } +smoltcp = { version = "0.12", features = [ + "std", + "log", + "medium-ip", + "proto-ipv4", + "proto-ipv6", + "socket-udp", + "socket-tcp", +] } arch = { path = "../arch" } utils = { path = "../utils" } polly = { path = "../polly" } -rutabaga_gfx = { path = "../rutabaga_gfx", features = ["virgl_renderer", "virgl_renderer_next"], optional = true } +rutabaga_gfx = { path = "../rutabaga_gfx", features = [ + "virgl_renderer", + "virgl_renderer_next", +], optional = true } imago = { version = "0.1.4", features = ["sync-wrappers", "vm-memory"] } [target.'cfg(target_os = "macos")'.dependencies] @@ -48,3 +66,7 @@ kvm-ioctls = ">=0.21" [target.'cfg(target_arch = "aarch64")'.dependencies] vm-fdt = ">= 0.2.0" + +[dev-dependencies] +tempfile = "3.0" +tokio = { version = "1.0", features = ["full"] } diff --git a/src/devices/src/legacy/hvfgicv3.rs b/src/devices/src/legacy/hvfgicv3.rs index c831bba15..a0553829f 100644 --- a/src/devices/src/legacy/hvfgicv3.rs +++ b/src/devices/src/legacy/hvfgicv3.rs @@ -75,14 +75,14 @@ impl HvfGicV3 { let mut dist_size: usize = 0; let ret = unsafe { (bindings.hv_gic_get_distributor_size)(&mut dist_size) }; if ret != HV_SUCCESS { - return Err(Error::VmCreate); + return Err(Error::VmCreate(ret)); } let dist_size = dist_size as u64; let mut redist_size: usize = 0; let ret = unsafe { (bindings.hv_gic_get_redistributor_size)(&mut redist_size) }; if ret != HV_SUCCESS { - return Err(Error::VmCreate); + return Err(Error::VmCreate(ret)); } let redists_size = redist_size as u64 * vcpu_count; @@ -92,7 +92,7 @@ impl HvfGicV3 { let gic_config = unsafe { (bindings.hv_gic_config_create)() }; let ret = unsafe { (bindings.hv_gic_config_set_distributor_base)(gic_config, dist_addr) }; if ret != HV_SUCCESS { - return Err(Error::VmCreate); + return Err(Error::VmCreate(ret)); } let ret = unsafe { @@ -102,12 +102,12 @@ impl HvfGicV3 { ) }; if ret != HV_SUCCESS { - return Err(Error::VmCreate); + return Err(Error::VmCreate(ret)); } let ret = unsafe { (bindings.hv_gic_create)(gic_config) }; if ret != HV_SUCCESS { - return Err(Error::VmCreate); + return Err(Error::VmCreate(ret)); } Ok(Self { diff --git a/src/devices/src/virtio/console/device.rs b/src/devices/src/virtio/console/device.rs index dadcf4dbe..e29860e5d 100644 --- a/src/devices/src/virtio/console/device.rs +++ b/src/devices/src/virtio/console/device.rs @@ -50,7 +50,9 @@ pub(crate) fn get_win_size() -> (u16, u16) { let ret = unsafe { tiocgwinsz(0, &mut ws) }; if let Err(err) = ret { - error!("Couldn't get terminal dimensions: {}", err); + if err != nix::errno::Errno::ENODEV { + error!("Couldn't get terminal dimensions: {}", err); + } (0, 0) } else { (ws.cols, ws.rows) diff --git a/src/devices/src/virtio/console/port_io.rs b/src/devices/src/virtio/console/port_io.rs index 06086b7a0..4e9caad7f 100644 --- a/src/devices/src/virtio/console/port_io.rs +++ b/src/devices/src/virtio/console/port_io.rs @@ -166,7 +166,8 @@ impl PortOutputLog { } fn force_flush(&mut self) { - log::log!(target: PortOutputLog::LOG_TARGET, Level::Error, "[missing newline]{}", String::from_utf8_lossy(&self.buf)); + println!("[missing newline]{}", String::from_utf8_lossy(&self.buf)); + // log::log!(target: PortOutputLog::LOG_TARGET, Level::Error, "[missing newline]{}", String::from_utf8_lossy(&self.buf)); self.buf.clear(); } } @@ -178,7 +179,8 @@ impl PortOutput for PortOutputLog { let mut start = 0; for (i, ch) in self.buf.iter().cloned().enumerate() { if ch == b'\n' { - log::log!(target: PortOutputLog::LOG_TARGET, Level::Error, "{}", String::from_utf8_lossy(&self.buf[start..i])); + println!("{}", String::from_utf8_lossy(&self.buf[start..i])); + // log::log!(target: PortOutputLog::LOG_TARGET, Level::Error, "{}", String::from_utf8_lossy(&self.buf[start..i])); start = i + 1; } } diff --git a/src/devices/src/virtio/fs/linux/passthrough.rs b/src/devices/src/virtio/fs/linux/passthrough.rs index dc87749b9..9685e38d9 100644 --- a/src/devices/src/virtio/fs/linux/passthrough.rs +++ b/src/devices/src/virtio/fs/linux/passthrough.rs @@ -33,8 +33,6 @@ const EMPTY_CSTR: &[u8] = b"\0"; const PROC_CSTR: &[u8] = b"/proc/self/fd\0"; const INIT_CSTR: &[u8] = b"init.krun\0"; -static INIT_BINARY: &[u8] = include_bytes!("../../../../../../init/init"); - type Inode = u64; type Handle = u64; @@ -147,26 +145,20 @@ fn stat(f: &File) -> io::Result { } fn statx(f: &File) -> io::Result<(libc::stat64, u64)> { - let mut stx = MaybeUninit::::zeroed(); - // Safe because this is a constant value and a valid C string. let pathname = unsafe { CStr::from_bytes_with_nul_unchecked(EMPTY_CSTR) }; // Safe because the kernel will only write data in `st` and we check the return // value. let res = unsafe { - libc::statx( - f.as_raw_fd(), - pathname.as_ptr(), - libc::AT_EMPTY_PATH | libc::AT_SYMLINK_NOFOLLOW, - libc::STATX_BASIC_STATS | libc::STATX_MNT_ID, - stx.as_mut_ptr(), + rustix::fs::statx( + f, + pathname, + rustix::fs::AtFlags::EMPTY_PATH | rustix::fs::AtFlags::SYMLINK_NOFOLLOW, + rustix::fs::StatxFlags::BASIC_STATS | rustix::fs::StatxFlags::MNT_ID, ) }; - if res >= 0 { - // Safe because the kernel guarantees that the struct is now fully initialized. - let stx = unsafe { stx.assume_init() }; - + if let Ok(stx) = res { // Unfortunately, we cannot use an initializer to create the stat64 object, // because it may contain padding and reserved fields (depending on the // architecture), and it does not implement the Default trait. @@ -940,25 +932,7 @@ impl FileSystem for PassthroughFs { fn lookup(&self, _ctx: Context, parent: Inode, name: &CStr) -> io::Result { debug!("do_lookup: {:?}", name); - let init_name = unsafe { CStr::from_bytes_with_nul_unchecked(INIT_CSTR) }; - - if self.init_inode != 0 && name == init_name { - let mut st: libc::stat64 = unsafe { mem::zeroed() }; - st.st_size = INIT_BINARY.len() as i64; - st.st_ino = self.init_inode; - st.st_mode = 0o100_755; - - Ok(Entry { - inode: self.init_inode, - generation: 0, - attr: st, - attr_flags: 0, - attr_timeout: self.cfg.attr_timeout, - entry_timeout: self.cfg.entry_timeout, - }) - } else { - self.do_lookup(parent, name) - } + self.do_lookup(parent, name) } fn forget(&self, _ctx: Context, inode: Inode, count: u64) { @@ -1174,17 +1148,6 @@ impl FileSystem for PassthroughFs { _flags: u32, ) -> io::Result { debug!("read: {:?}", inode); - if inode == self.init_inode { - let off: usize = offset - .try_into() - .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; - let len = if off + (size as usize) < INIT_BINARY.len() { - size as usize - } else { - INIT_BINARY.len() - off - }; - return w.write(&INIT_BINARY[off..(off + len)]); - } let data = self .handles @@ -2019,36 +1982,6 @@ impl FileSystem for PassthroughFs { debug!("setupmapping: ino {:?} addr={:x} len={}", inode, addr, len); - if inode == self.init_inode { - let ret = unsafe { - libc::mmap( - addr as *mut libc::c_void, - len as usize, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED, - -1, - 0, - ) - }; - if std::ptr::eq(ret, libc::MAP_FAILED) { - return Err(io::Error::last_os_error()); - } - - let to_copy = if len as usize > INIT_BINARY.len() { - INIT_BINARY.len() - } else { - len as usize - }; - unsafe { - libc::memcpy( - addr as *mut libc::c_void, - INIT_BINARY.as_ptr() as *const _, - to_copy, - ) - }; - return Ok(()); - } - let file = self.open_inode(inode, open_flags)?; let fd = file.as_raw_fd(); diff --git a/src/devices/src/virtio/fs/macos/passthrough.rs b/src/devices/src/virtio/fs/macos/passthrough.rs index 4b35bb130..24461cd8a 100644 --- a/src/devices/src/virtio/fs/macos/passthrough.rs +++ b/src/devices/src/virtio/fs/macos/passthrough.rs @@ -37,9 +37,6 @@ const XATTR_KEY: &[u8] = b"user.containers.override_stat\0"; const UID_MAX: u32 = u32::MAX - 1; -#[cfg(not(feature = "efi"))] -static INIT_BINARY: &[u8] = include_bytes!("../../../../../../init/init"); - type Inode = u64; type Handle = u64; @@ -974,27 +971,6 @@ impl FileSystem for PassthroughFs { fn lookup(&self, _ctx: Context, parent: Inode, name: &CStr) -> io::Result { debug!("lookup: {:?}", name); - let _init_name = unsafe { CStr::from_bytes_with_nul_unchecked(INIT_CSTR) }; - - #[cfg(not(feature = "efi"))] - if self.init_inode != 0 && name == _init_name { - let mut st: bindings::stat64 = unsafe { mem::zeroed() }; - st.st_size = INIT_BINARY.len() as i64; - st.st_ino = self.init_inode; - st.st_mode = 0o100_755; - - Ok(Entry { - inode: self.init_inode, - generation: 0, - attr: st, - attr_flags: 0, - attr_timeout: self.cfg.attr_timeout, - entry_timeout: self.cfg.entry_timeout, - }) - } else { - self.do_lookup(parent, name) - } - #[cfg(feature = "efi")] self.do_lookup(parent, name) } @@ -1236,18 +1212,6 @@ impl FileSystem for PassthroughFs { _flags: u32, ) -> io::Result { debug!("read: {:?}", inode); - #[cfg(not(feature = "efi"))] - if inode == self.init_inode { - let off: usize = offset - .try_into() - .map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?; - let len = if off + (size as usize) < INIT_BINARY.len() { - size as usize - } else { - INIT_BINARY.len() - off - }; - return w.write(&INIT_BINARY[off..(off + len)]); - } let data = self .handles diff --git a/src/devices/src/virtio/net/backend.rs b/src/devices/src/virtio/net/backend.rs index c3da32906..fb4f71f58 100644 --- a/src/devices/src/virtio/net/backend.rs +++ b/src/devices/src/virtio/net/backend.rs @@ -1,5 +1,7 @@ use std::os::fd::RawFd; +use utils::epoll::EpollEvent; + #[allow(dead_code)] #[derive(Debug)] pub enum ConnectError { @@ -37,4 +39,7 @@ pub trait NetBackend { fn has_unfinished_write(&self) -> bool; fn try_finish_write(&mut self, hdr_len: usize, buf: &[u8]) -> Result<(), WriteError>; fn raw_socket_fd(&self) -> RawFd; + fn handle_event(&self, _event: &EpollEvent) { + // noop by default + } } diff --git a/src/devices/src/virtio/net/device.rs b/src/devices/src/virtio/net/device.rs index e7a363a75..2fcae85f8 100644 --- a/src/devices/src/virtio/net/device.rs +++ b/src/devices/src/virtio/net/device.rs @@ -5,14 +5,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the THIRD-PARTY file. use crate::legacy::IrqChip; +use crate::virtio::net::proxy::ProxyNetWorker; use crate::virtio::net::{Error, Result}; use crate::virtio::net::{QUEUE_SIZES, RX_INDEX, TX_INDEX}; use crate::virtio::queue::Error as QueueError; -use crate::virtio::{ActivateResult, DeviceState, Queue, VirtioDevice, TYPE_NET}; +use crate::virtio::{ActivateError, ActivateResult, DeviceState, Queue, VirtioDevice, TYPE_NET}; use crate::Error as DeviceError; use super::backend::{ReadError, WriteError}; use super::worker::NetWorker; +use crossbeam_channel::Sender; use std::cmp; use std::io::Write; @@ -43,6 +45,7 @@ pub enum FrontendError { pub enum RxError { Backend(ReadError), DeviceError(DeviceError), + QueueError(QueueError), } #[derive(Debug)] @@ -50,6 +53,25 @@ pub enum TxError { Backend(WriteError), DeviceError(DeviceError), QueueError(QueueError), + GuestMemory(vm_memory::GuestMemoryError), +} + +impl From for TxError { + fn from(value: WriteError) -> Self { + Self::Backend(value) + } +} + +impl From for TxError { + fn from(value: DeviceError) -> Self { + Self::DeviceError(value) + } +} + +impl From for TxError { + fn from(value: QueueError) -> Self { + Self::QueueError(value) + } } #[derive(Copy, Clone, Debug, Default)] @@ -67,6 +89,7 @@ unsafe impl ByteValued for VirtioNetConfig {} pub enum VirtioNetBackend { Passt(RawFd), Gvproxy(PathBuf), + Proxy(Vec<(u16, String)>), } pub struct Net { @@ -95,10 +118,6 @@ impl Net { pub fn new(id: String, cfg_backend: VirtioNetBackend, mac: [u8; 6]) -> Result { let avail_features = (1 << VIRTIO_NET_F_GUEST_CSUM) | (1 << VIRTIO_NET_F_CSUM) - | (1 << VIRTIO_NET_F_GUEST_TSO4) - | (1 << VIRTIO_NET_F_HOST_TSO4) - | (1 << VIRTIO_NET_F_GUEST_UFO) - | (1 << VIRTIO_NET_F_HOST_UFO) | (1 << VIRTIO_NET_F_MAC) | (1 << VIRTIO_RING_F_EVENT_IDX) | (1 << VIRTIO_F_VERSION_1); @@ -222,17 +241,39 @@ impl VirtioDevice for Net { .iter() .map(|e| e.try_clone().unwrap()) .collect(); - let worker = NetWorker::new( - self.queues.clone(), - queue_evts, - self.interrupt_status.clone(), - self.interrupt_evt.try_clone().unwrap(), - self.intc.clone(), - self.irq_line, - mem.clone(), - self.cfg_backend.clone(), - ); - worker.run(); + + match &self.cfg_backend { + VirtioNetBackend::Proxy(listeners) => { + let proxy = ProxyNetWorker::new( + self.queues.clone(), + queue_evts, + self.interrupt_status.clone(), + self.interrupt_evt.try_clone().unwrap(), + self.intc.clone(), + self.irq_line, + mem.clone(), + listeners.clone(), + ) + .map_err(|e| { + log::error!("Failed to create unified proxy: {}", e); + ActivateError::EpollCtl(e) + })?; + proxy.run(); + } + _ => { + let worker = NetWorker::new( + self.queues.clone(), + queue_evts, + self.interrupt_status.clone(), + self.interrupt_evt.try_clone().unwrap(), + self.intc.clone(), + self.irq_line, + mem.clone(), + self.cfg_backend.clone(), + ); + worker.run(); + } + } self.device_state = DeviceState::Activated(mem); Ok(()) @@ -245,3 +286,12 @@ impl VirtioDevice for Net { } } } + +#[cfg(test)] +mod tests { + #[test] + fn test_net_module_works() { + // Simple test to verify virtio::net tests are running + assert_eq!(2 + 2, 4); + } +} diff --git a/src/devices/src/virtio/net/mod.rs b/src/devices/src/virtio/net/mod.rs index 7300a4a41..7a95cbd81 100644 --- a/src/devices/src/virtio/net/mod.rs +++ b/src/devices/src/virtio/net/mod.rs @@ -11,10 +11,12 @@ pub const RX_INDEX: usize = 0; // The index of the tx queue from Net device queues/queues_evts vector. pub const TX_INDEX: usize = 1; -mod backend; +pub mod backend; pub mod device; mod gvproxy; mod passt; +pub mod proxy; +// pub mod proxy_backend; mod worker; pub use self::device::Net; diff --git a/src/devices/src/virtio/net/proxy.rs b/src/devices/src/virtio/net/proxy.rs new file mode 100644 index 000000000..cff44c488 --- /dev/null +++ b/src/devices/src/virtio/net/proxy.rs @@ -0,0 +1,1437 @@ +use crate::legacy::IrqChip; +use crate::virtio::net::{MAX_BUFFER_SIZE, RX_INDEX, TX_INDEX}; +use crate::virtio::{Queue, VIRTIO_MMIO_INT_VRING}; +use crate::Error as DeviceError; +use bytes::Bytes; +use mio::event::{Event, Source}; +use mio::net::UnixListener; +use mio::unix::SourceFd; +use mio::{Events, Interest, Poll, Registry, Token}; +use pnet::packet::ethernet::EthernetPacket; +use pnet::packet::ip::IpNextHeaderProtocols; +use pnet::packet::ipv4::{Ipv4Packet, MutableIpv4Packet}; +use pnet::packet::tcp::{TcpFlags, TcpPacket}; +use pnet::packet::udp::{MutableUdpPacket, UdpPacket}; +use pnet::packet::{MutablePacket, Packet}; +use smoltcp::iface::{Config, Interface, PollResult, SocketHandle, SocketSet}; +use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; +use smoltcp::time::Instant as SmoltcpInstant; +use smoltcp::wire::{ + EthernetAddress, IpAddress, IpCidr, IpEndpoint, IpListenEndpoint, IpProtocol, IpVersion, + Ipv4Address, +}; +use socket2::{Domain, SockAddr, Socket}; +use std::cmp; +use std::collections::HashMap; +use std::collections::VecDeque; +use std::io::{self, Read, Write}; +use std::net::{IpAddr, SocketAddr}; +use std::os::fd::AsRawFd; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; +use tracing::{debug, error, info, trace, warn}; +use utils::eventfd::EventFd; +use virtio_bindings::virtio_net::virtio_net_hdr_v1; +use vm_memory::{Bytes as MemBytes, GuestMemoryMmap}; + +// --- Constants and Configuration --- +const VIRTQ_TX_TOKEN: Token = Token(0); +const VIRTQ_RX_TOKEN: Token = Token(1); +const HOST_SOCKET_START_TOKEN: usize = 2; + +const VM_MAC: EthernetAddress = EthernetAddress([0xde, 0xad, 0xbe, 0xef, 0x00, 0x00]); +const PROXY_MAC: EthernetAddress = EthernetAddress([0x02, 0x00, 0x00, 0x01, 0x02, 0x03]); +const VM_IP: Ipv4Address = Ipv4Address::new(192, 168, 100, 2); +const PROXY_IP: Ipv4Address = Ipv4Address::new(192, 168, 100, 1); + +/// Represents the virtio-net device as a `smoltcp` PHY device. +/// This acts as the bridge between the VM's virtio queues and the smoltcp stack. +struct VirtualDevice { + rx_buffer: VecDeque, + mem: GuestMemoryMmap, + queues: Vec, + rx_frame_buf: [u8; MAX_BUFFER_SIZE], + tx_frame_buf: [u8; MAX_BUFFER_SIZE], +} + +impl VirtualDevice { + pub fn receive_raw_from_guest(&mut self) -> Option { + if let Some(head) = self.queues[TX_INDEX].pop(&self.mem) { + let head_index = head.index; + let mut read_count = 0; + let mut next_desc = Some(head); + + while let Some(desc) = next_desc { + if !desc.is_write_only() { + // Calculate the length to read for this specific descriptor. + let len = cmp::min(self.rx_frame_buf.len() - read_count, desc.len as usize); + + // Read from guest memory directly into our scratchpad array. + if self + .mem + .read_slice( + &mut self.rx_frame_buf[read_count..read_count + len], + desc.addr, + ) + .is_ok() + { + read_count += len; + } + } + next_desc = desc.next_descriptor(); + } + + self.queues[TX_INDEX] + .add_used(&self.mem, head_index, 0) + .unwrap(); + + let header_len = std::mem::size_of::(); + if read_count > header_len { + let packet_payload = &self.rx_frame_buf[header_len..read_count]; + let packet = Bytes::copy_from_slice(packet_payload); + + trace!("{}", packet_dumper::log_vm_packet_in(&packet)); + return Some(packet); + } + } + None + } +} + +impl Device for VirtualDevice { + type RxToken<'a> + = RxToken + where + Self: 'a; + type TxToken<'a> + = TxToken<'a> + where + Self: 'a; + + /// Receives a packet from the virtio TX queue (i.e., from the guest). + fn receive( + &mut self, + _timestamp: smoltcp::time::Instant, + ) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + self.rx_buffer.pop_front().map(|buffer| { + let rx_token = RxToken { buffer }; + let tx_token = TxToken { + mem: &self.mem, + rx_queue: &mut self.queues[RX_INDEX], + buf: &mut self.tx_frame_buf, + }; + (rx_token, tx_token) + }) + } + + /// Transmits a packet to the virtio RX queue (i.e., to the guest). + fn transmit(&mut self, _timestamp: smoltcp::time::Instant) -> Option> { + // Check if there are any available descriptors in the RX queue. + // The guest puts empty buffers here for us to fill. + if !self.queues[RX_INDEX].is_empty(&self.mem) { + // If a buffer is available, return a TxToken. + // smoltcp will then call the token's `consume` method to fill the buffer. + Some(TxToken { + mem: &self.mem, + rx_queue: &mut self.queues[RX_INDEX], + buf: &mut self.tx_frame_buf, + }) + } else { + // If the guest has not provided any empty buffers, we can't transmit. + // Tell smoltcp the device is exhausted. + None + } + } + + fn capabilities(&self) -> DeviceCapabilities { + let mut caps = DeviceCapabilities::default(); + caps.max_transmission_unit = 1500; + caps.medium = Medium::Ethernet; + caps + } +} + +// A token that holds a received packet. +struct RxToken { + buffer: Bytes, +} + +impl<'a> phy::RxToken for RxToken { + fn consume(self, f: F) -> R + where + F: FnOnce(&[u8]) -> R, + { + f(&self.buffer) + } +} + +// A token that can transmit a packet. +struct TxToken<'a> { + mem: &'a GuestMemoryMmap, + rx_queue: &'a mut Queue, + buf: &'a mut [u8], +} + +impl<'a> phy::TxToken for TxToken<'a> { + fn consume(self, len: usize, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + const VIRTIO_HEADER_SIZE: usize = std::mem::size_of::(); + + // Let smoltcp write the packet *after* the space for the header + let result = f(&mut self.buf[VIRTIO_HEADER_SIZE..VIRTIO_HEADER_SIZE + len]); + + trace!( + "{}", + packet_dumper::log_vm_packet_out( + &self.buf[VIRTIO_HEADER_SIZE..VIRTIO_HEADER_SIZE + len] + ) + ); + + // The virtio-net header is all zeros, which is the default for virtio_net_hdr_v1. + // If you needed to set fields, you'd do it here on `&mut self.buf[..VIRTIO_HEADER_SIZE]`. + + // Now, `&self.buf[..VIRTIO_HEADER_SIZE + len]` is the full frame. No new allocation needed. + let frame = &self.buf[..VIRTIO_HEADER_SIZE + len]; + + trace!( + "sending frame with header: {:?}", + &self.buf[..VIRTIO_HEADER_SIZE] + ); + + // Write the frame to the guest's RX queue. + if let Some(head) = self.rx_queue.pop(self.mem) { + let head_index = head.index; + let mut written = 0; + let mut next_desc = Some(head); + + while let Some(desc) = next_desc { + if desc.is_write_only() { + let write_len = cmp::min(frame.len() - written, desc.len as usize); + if self + .mem + .write_slice(&frame[written..written + write_len], desc.addr) + .is_ok() + { + written += write_len; + } + } + next_desc = desc.next_descriptor(); + } + self.rx_queue + .add_used(self.mem, head_index, written as u32) + .unwrap(); + } + + result + } +} + +enum HostSocket { + Tcp(mio::net::TcpStream), + Udp(mio::net::UdpSocket), + Unix(mio::net::UnixStream), +} + +struct Conn { + socket: HostSocket, + handle: SocketHandle, + last_activity: Instant, +} + +/// The main proxy structure, now using smoltcp. +pub struct ProxyNetWorker { + // Virtio-related fields + queue_evts: Vec, + interrupt_status: Arc, + interrupt_evt: EventFd, + intc: Option, + irq_line: Option, + + // smoltcp-related fields + device: VirtualDevice, + iface: Interface, + sockets: SocketSet<'static>, + + // mio and networking fields + poll: Poll, + registry: Registry, + next_token: usize, + host_connections: HashMap, + nat_table: HashMap, // (External IP, External Port) -> Token + reverse_nat_table: HashMap, + unix_listeners: HashMap, + + raw_socket_handle: SocketHandle, + + next_ephemeral_port: u16, +} + +impl ProxyNetWorker { + pub fn new( + queues: Vec, + queue_evts: Vec, + interrupt_status: Arc, + interrupt_evt: EventFd, + intc: Option, + irq_line: Option, + mem: GuestMemoryMmap, + listeners: Vec<(u16, String)>, + ) -> io::Result { + let poll = Poll::new()?; + let registry = poll.registry().try_clone()?; + + // Create the virtual device for smoltcp + let mut virtual_device = VirtualDevice { + rx_buffer: VecDeque::new(), + mem, + queues, + rx_frame_buf: [0; MAX_BUFFER_SIZE], + tx_frame_buf: [0; MAX_BUFFER_SIZE], + }; + + let mut iface = Interface::new( + Config::new(smoltcp::wire::HardwareAddress::Ethernet(PROXY_MAC)), + &mut virtual_device, + smoltcp::time::Instant::now(), + ); + + iface.set_any_ip(true); + + iface.update_ip_addrs(|ip_addrs| { + ip_addrs + .push(IpCidr::new(IpAddress::from(PROXY_IP), 24)) + .expect("maximum number of IPs in TCP interface reached"); + }); + + iface + .routes_mut() + .add_default_ipv4_route(PROXY_IP) + .expect("could not add default ipv4 route"); + + let mut sockets = SocketSet::new(vec![]); + + // Create a raw socket for sending manually crafted IP packets. + // This allows smoltcp to handle the L2 framing. + let raw_rx_buffer = smoltcp::socket::raw::PacketBuffer::new( + vec![smoltcp::socket::raw::PacketMetadata::EMPTY; 1024], + vec![0; 1024 * 1500], + ); + let raw_tx_buffer = smoltcp::socket::raw::PacketBuffer::new( + vec![smoltcp::socket::raw::PacketMetadata::EMPTY; 1024], + vec![0; 1024 * 1500], + ); + let raw_socket_handle = sockets.add(smoltcp::socket::raw::Socket::new( + IpVersion::Ipv4, + IpProtocol::Udp, // You can make this more generic if needed + raw_rx_buffer, + raw_tx_buffer, + )); + + let mut next_token = HOST_SOCKET_START_TOKEN; + let mut unix_listeners = HashMap::new(); + + fn configure_socket(domain: Domain, sock_type: socket2::Type) -> io::Result { + let socket = Socket::new(domain, sock_type, None)?; + const BUF_SIZE: usize = 8 * 1024 * 1024; + if let Err(e) = socket.set_recv_buffer_size(BUF_SIZE) { + warn!(error = %e, "Failed to set receive buffer size."); + } + if let Err(e) = socket.set_send_buffer_size(BUF_SIZE) { + warn!(error = %e, "Failed to set send buffer size."); + } + socket.set_nonblocking(true)?; + Ok(socket) + } + + for (vm_port, path) in listeners { + if std::fs::exists(path.as_str())? { + std::fs::remove_file(path.as_str())?; + } + let listener_socket = configure_socket(Domain::UNIX, socket2::Type::STREAM)?; + listener_socket.bind(&SockAddr::unix(path.as_str())?)?; + listener_socket.listen(1024)?; + info!(socket_path = %path, %vm_port, "Listening for Unix socket ingress connections"); + + let mut listener = UnixListener::from_std(listener_socket.into()); + + let token = Token(next_token); + registry.register(&mut listener, token, Interest::READABLE)?; + next_token += 1; + + unix_listeners.insert(token, (listener, vm_port)); + } + + Ok(ProxyNetWorker { + queue_evts, + interrupt_status, + interrupt_evt, + intc, + irq_line, + device: virtual_device, + iface, + sockets: unsafe { std::mem::transmute(sockets) }, + poll, + registry, + next_token, + host_connections: HashMap::new(), + nat_table: HashMap::new(), + reverse_nat_table: HashMap::new(), + next_ephemeral_port: 49152, + unix_listeners, + raw_socket_handle, + }) + } + + pub fn run(mut self) { + thread::Builder::new() + .name("smoltcp-proxy".into()) + .spawn(move || self.work()) + .unwrap(); + } + + fn work(&mut self) { + let mut events = Events::with_capacity(1024); + + // Register virtio queue events with mio + self.poll + .registry() + .register( + &mut SourceFd(&self.queue_evts[TX_INDEX].as_raw_fd()), + VIRTQ_TX_TOKEN, + Interest::READABLE, + ) + .unwrap(); + self.poll + .registry() + .register( + &mut SourceFd(&self.queue_evts[RX_INDEX].as_raw_fd()), + VIRTQ_RX_TOKEN, + Interest::READABLE, + ) + .unwrap(); + + let mut last_changes_at = Instant::now(); + let start_time = Instant::now(); + + let mut last_cleanup = Instant::now(); + + loop { + // Poll for events from virtio queues and host sockets + let timeout = self + .iface + .poll_delay( + SmoltcpInstant::from_millis(start_time.elapsed().as_millis() as i64), + &self.sockets, + ) + .map(|d| std::time::Duration::from_millis(d.total_millis() as u64)); + + self.poll.poll(&mut events, timeout).unwrap(); + + // Process virtio queue events + for event in events.iter() { + match event.token() { + VIRTQ_TX_TOKEN => { + trace!("handling TX queue event"); + self.queue_evts[TX_INDEX].read().unwrap(); + self.device.queues[TX_INDEX] + .disable_notification(&self.device.mem) + .unwrap(); + } + VIRTQ_RX_TOKEN => { + trace!("handling RX queue event"); + self.queue_evts[RX_INDEX].read().unwrap(); + self.device.queues[RX_INDEX] + .disable_notification(&self.device.mem) + .unwrap(); + } + token => { + if self.unix_listeners.contains_key(&token) { + self.handle_unix_listener_event(token); + } else { + self.handle_host_socket_event(token, event); + } + } + } + } + + while let Some(data) = self.device.receive_raw_from_guest() { + // A TX buffer was just consumed. Signal the guest. + self.signal_used_queue(TX_INDEX).unwrap(); + + // Check if the packet was the start of a new session and was handled. + let packet_was_intercepted = self.intercept_new_session(&data); + + // ONLY if the packet was not intercepted (e.g., it's an ACK or data for an + // existing connection), do we queue it for smoltcp. + if !packet_was_intercepted { + self.device.rx_buffer.push_back(data); + } + } + + let timestamp = SmoltcpInstant::from_millis(start_time.elapsed().as_millis() as i64); + + match self + .iface + .poll(timestamp, &mut self.device, &mut self.sockets) + { + PollResult::None => { + let elapsed = last_changes_at.elapsed(); + if elapsed > Duration::from_secs(5) { + trace!("no changes since {elapsed:?}"); + for (handle, socket) in self.sockets.iter() { + match socket { + smoltcp::socket::Socket::Raw(socket) => { + trace!(%handle, ip_version = ?socket.ip_version(), ip_protocol = ?socket.ip_protocol(), "raw socket"); + } + smoltcp::socket::Socket::Icmp(socket) => { + trace!(%handle, "icmp socket"); + } + smoltcp::socket::Socket::Udp(socket) => { + trace!(%handle, endpoint = %socket.endpoint(), send_queue = socket.send_queue(), recv_queue = socket.recv_queue(), "udp socket"); + } + smoltcp::socket::Socket::Tcp(socket) => { + trace!(%handle, local_ep = ?socket.local_endpoint(), remote_ep = ?socket.remote_endpoint(), listen_ep = %socket.listen_endpoint(), state = %socket.state(), "tcp socket"); + } + smoltcp::socket::Socket::Dhcpv4(socket) => { + trace!(%handle, "dhcpv4 socket"); + } + smoltcp::socket::Socket::Dns(socket) => { + trace!(%handle, "dns socket"); + } + } + } + } + } + PollResult::SocketStateChanged => { + trace!("socket state changed!"); + last_changes_at = Instant::now(); + } + } + + // Signal the guest if packets were sent to the RX queue + if self.device.queues[RX_INDEX] + .needs_notification(&self.device.mem) + .unwrap() + { + trace!("signaling rx queue that it was used"); + self.signal_used_queue(RX_INDEX).unwrap(); + } + if self.device.queues[TX_INDEX] + .needs_notification(&self.device.mem) + .unwrap() + { + trace!("signaling tx queue that it was used"); + self.signal_used_queue(TX_INDEX).unwrap(); + } + + // Re-enable notifications + self.device.queues[RX_INDEX] + .enable_notification(&self.device.mem) + .unwrap(); + self.device.queues[TX_INDEX] + .enable_notification(&self.device.mem) + .unwrap(); + + // Check TCP sockets for data to send to the host + for ( + token, + Conn { + socket: stream, + handle, + .. + }, + ) in self.host_connections.iter_mut() + { + let socket = match stream { + HostSocket::Tcp(_) | HostSocket::Unix(_) => { + self.sockets.get::(*handle) + } + HostSocket::Udp(_udp_socket) => { + continue; + } + }; + + let interests = if socket.can_recv() && socket.can_send() { + Interest::READABLE | Interest::WRITABLE + } else if socket.can_recv() { + Interest::WRITABLE + } else if socket.can_send() { + Interest::READABLE + } else { + continue; + }; + + // Only re-register if we need any events + match stream { + HostSocket::Tcp(s) => { + self.registry.reregister(s, *token, interests).unwrap(); + } + HostSocket::Unix(s) => { + self.registry.reregister(s, *token, interests).unwrap(); + } + _ => {} + } + } + + const CLEANUP_INTERVAL: Duration = Duration::from_secs(5); + const UDP_TIMEOUT: Duration = Duration::from_secs(30); + + if last_cleanup.elapsed() > CLEANUP_INTERVAL { + trace!("Running periodic cleanup of stale UDP connections..."); + let now = Instant::now(); + let mut expired_tokens = Vec::new(); + + // Find expired UDP connections + for (token, conn) in self.host_connections.iter() { + if let HostSocket::Udp(_) = conn.socket { + if now.duration_since(conn.last_activity) > UDP_TIMEOUT { + expired_tokens.push((*token, conn.handle)); + } + } + } + + // Now, clean them up + for (token, handle) in expired_tokens { + trace!(?token, %handle, "Connection timed out. Removing."); + self.host_connections.remove(&token); + + // no smoltcp socket to remove for UDP + + if let Some((guest_ep, _)) = self.reverse_nat_table.remove(&token) { + self.nat_table.remove(&guest_ep); + } + } + + last_cleanup = Instant::now(); + } + } + } + + fn forward_stream( + &mut self, + token: Token, + event: &Event, + stream: &mut T, + handle: SocketHandle, + ) -> bool { + let socket = self.sockets.get_mut::(handle); + + let socket_state = socket.state(); + if socket_state == smoltcp::socket::tcp::State::Closed + || socket_state == smoltcp::socket::tcp::State::TimeWait + { + trace!( + ?token, + state = %socket_state, + "Connection is fully closed, removing." + ); + return false; // This connection is truly done. + } + + // If the socket is still handshaking, it can't send/recv data yet, but it's not dead. + // We should just return true to keep it alive and wait for the handshake to complete. + if !socket.is_active() || !socket.may_send() && !socket.may_recv() { + trace!( + ?token, + state = %socket_state, + active = socket.is_active(), + may_send = socket.may_send(), + can_send = socket.can_send(), + may_recv = socket.may_recv(), + can_recv = socket.can_recv(), + "Socket not ready for I/O, but still alive. Waiting." + ); + // Keep the connection alive, but don't try to do I/O. + return true; + } + + // --- 1. Read from Host, Write to Guest --- + if event.is_readable() { + trace!(?token, %socket_state, "socket is readable"); + let mut buffer = [0u8; 2048]; + loop { + // Loop to drain the readable data from the host socket. + if !socket.can_send() { + trace!(?token, %socket_state, "socket can't send"); + break; // Guest-side buffer is full. + } + + let send_capacity = socket.send_capacity() - socket.send_queue(); + let read_limit = std::cmp::min(send_capacity, buffer.len()); + + match stream.read(&mut buffer[..read_limit]) { + Ok(0) => { + // Host closed the connection. + trace!(?token, "Host stream EOF, closing smoltcp socket"); + socket.close(); + break; + } + Ok(n) => { + trace!(?token, bytes = n, "Read from host, wrote to smoltcp"); + if let Err(e) = socket.send_slice(&buffer[..n]) { + error!(?token, "could not send slice to smoltcp socket: {e}"); + socket.abort(); + } + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + trace!(?token, "would block, breaking stream write loop"); + break; // No more data to read for now. + } + Err(e) => { + error!(?token, error = %e, "Read error on host stream, aborting"); + socket.abort(); + break; + } + } + } + } + + // --- 2. Read from Guest, Write to Host --- + if event.is_writable() { + trace!(?token, %socket_state, "socket is writable"); + loop { + if !socket.can_recv() { + trace!(?token, %socket_state, "socket can't recv"); + break; + } + // Loop to drain the guest-side buffer. + let result = socket.recv(|data| { + match stream.write(data) { + Ok(n) => (n, (n == 0, false)), // Continue writing + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + (0, (true, false)) // Host buffer is full, break inner loop. + } + Err(e) => { + error!(?token, error = %e, "Write error on host stream, aborting"); + (data.len(), (true, true)) // Mark all data as "consumed" to abort. + } + } + }); + + match result { + Ok((should_break, should_abort)) => { + trace!( + ?token, + should_break, + should_abort, + "read a packet from socket" + ); + if should_abort { + socket.abort(); + } + // Broke due to WouldBlock or an error. + if should_break { + break; + } + } + Err(e) => { + error!(?token, "could not recv from smoltcp socket: {e}"); + socket.abort(); + break; + } + } + } + } + + // --- 3. Manage Mio Interest --- + // After all I/O, decide if we still need to be notified about writability. + if socket.can_recv() { + // We still have data to send to the host, so we need WRITABLE interest. + // This handles the case where a write was blocked by WouldBlock. + self.registry + .reregister(stream, token, Interest::READABLE | Interest::WRITABLE) + .unwrap_or_else(|e| { + error!(?token, error=%e, "Reregister R|W failed"); + socket.abort(); + }); + } else { + // The guest-side buffer is empty, we only need to know when the host sends us data. + self.registry + .reregister(stream, token, Interest::READABLE) + .unwrap_or_else(|e| { + error!(?token, error=%e, "Reregister R-only failed"); + socket.abort(); + }); + } + + // Return true to keep the connection + true + } + + fn handle_unix_listener_event(&mut self, token: Token) { + // Retrieve the listener and the target guest port. + if let Some((listener, guest_port)) = self.unix_listeners.remove(&token) { + loop { + let (mut stream, _addr) = match listener.accept() { + Ok(res) => res, + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + // No more pending connections to accept. + break; + } + Err(e) => { + error!(?token, error = %e, "Failed to accept unix socket connection"); + // FIXME: probably need to cleanup something + break; + } + }; + + trace!( + ?token, + port = guest_port, + "Accepted new unix socket connection" + ); + + // Create the smoltcp TCP socket that will connect TO the guest. + let rx_buffer = smoltcp::socket::tcp::SocketBuffer::new(vec![0; 65535]); + let tx_buffer = smoltcp::socket::tcp::SocketBuffer::new(vec![0; 65535]); + let mut smoltcp_socket = smoltcp::socket::tcp::Socket::new(rx_buffer, tx_buffer); + + // Set up the connection parameters. The remote endpoint is the guest. + let remote_endpoint = IpEndpoint::new(IpAddress::from(VM_IP), guest_port); + let ephemeral_port = self.get_ephemeral_port(); + + trace!(?token, "connecting to {remote_endpoint}"); + + // Tell the smoltcp socket to initiate a connection. + smoltcp_socket + .connect( + self.iface.context(), + remote_endpoint, + IpListenEndpoint { + port: ephemeral_port, + addr: Some(IpAddress::Ipv4(PROXY_IP)), + }, + ) + .unwrap(); + let smoltcp_handle = self.sockets.add(smoltcp_socket); + + // Register the new stream with mio for read/write events. + let new_token = Token(self.next_token); + self.next_token += 1; + self.registry + .register( + &mut stream, + new_token, + Interest::READABLE | Interest::WRITABLE, + ) + .unwrap(); + + // Add the new active connection to our tracking map. + self.host_connections.insert( + new_token, + Conn { + socket: HostSocket::Unix(stream), + handle: smoltcp_handle, + last_activity: Instant::now(), + }, + ); + + trace!(token = ?new_token, "assigned token to proxy (host unix) connection"); + } + self.unix_listeners.insert(token, (listener, guest_port)); + } + } + + /// Parses a raw packet from the guest. If it's a new TCP connection attempt, + /// it sets up the host-side connection and the smoltcp "twin" socket. + /// Returns true if the packet was handled, meaning it should not be given to smoltcp. + fn intercept_new_session(&mut self, data: &[u8]) -> bool { + if let Some(eth) = EthernetPacket::new(data) { + if let Some(ipv4) = Ipv4Packet::new(eth.payload()) { + match ipv4.get_next_level_protocol() { + // --- Keep your existing TCP logic --- + IpNextHeaderProtocols::Tcp => { + if let Some(tcp) = TcpPacket::new(ipv4.payload()) { + // We only care about the initial SYN packet to start a connection + if tcp.get_flags() == TcpFlags::SYN { + let guest_addr = IpAddress::from(ipv4.get_source()); + let dest_addr = IpAddress::from(ipv4.get_destination()); + let guest_port = tcp.get_source(); + let dest_port = tcp.get_destination(); + + let dest_socket_addr = + std::net::SocketAddr::new(dest_addr.into(), dest_port); + + trace!(from = %guest_addr, to = %dest_socket_addr, "New connection attempt from guest"); + + let real_dest = SocketAddr::new(dest_addr.into(), dest_port); + let stream = match dest_addr.into() { + IpAddr::V4(_) => { + Socket::new(Domain::IPV4, socket2::Type::STREAM, None) + } + IpAddr::V6(_) => { + Socket::new(Domain::IPV6, socket2::Type::STREAM, None) + } + }; + + let Ok(sock) = stream else { + error!(error = %stream.unwrap_err(), "Failed to create egress socket"); + return true; + }; + + sock.set_nonblocking(true).unwrap(); + + match sock.connect(&real_dest.into()) { + Ok(()) => (), + Err(e) if e.raw_os_error() == Some(libc::EINPROGRESS) => (), + Err(e) => { + error!(error = %e, "Failed to connect egress socket"); + return true; + } + } + + let mut stream = mio::net::TcpStream::from_std(sock.into()); + + // 2. Create the smoltcp "twin" socket to represent the guest's side + let rx_buffer = + smoltcp::socket::tcp::SocketBuffer::new(vec![0; 65535]); + let tx_buffer = + smoltcp::socket::tcp::SocketBuffer::new(vec![0; 65535]); + let mut smoltcp_socket = + smoltcp::socket::tcp::Socket::new(rx_buffer, tx_buffer); + + smoltcp_socket + .set_keep_alive(Some(smoltcp::time::Duration::from_secs(28))); + // FIXME: It should follow system's setting. 7200 is Linux's default. + smoltcp_socket + .set_timeout(Some(smoltcp::time::Duration::from_secs(7200))); + + smoltcp_socket + .listen(IpEndpoint::new(dest_addr, dest_port)) + .unwrap(); + + let smoltcp_handle = self.sockets.add(smoltcp_socket); + + // 3. Register the real socket with mio and map it to the twin + let token = Token(self.next_token); + self.next_token += 1; + self.registry + .register( + &mut stream, + token, + Interest::READABLE | Interest::WRITABLE, + ) + .unwrap(); + self.host_connections.insert( + token, + Conn { + socket: HostSocket::Tcp(stream), + handle: smoltcp_handle, + last_activity: Instant::now(), + }, + ); + } + } + } + + IpNextHeaderProtocols::Udp => { + let src = ipv4.get_source(); + let dst = ipv4.get_destination(); + if let Some(udp) = UdpPacket::new(ipv4.payload()) { + let guest_addr = IpAddress::from(src); + let guest_port = udp.get_source(); + let guest_endpoint: IpEndpoint = (guest_addr, guest_port).into(); + + // Check if this is part of an existing session. + if let Some(token) = self.nat_table.get(&guest_endpoint).copied() { + // This is an existing flow. Forward the packet directly. + if let Some(conn) = self.host_connections.get_mut(&token) { + if let HostSocket::Udp(udp_socket) = &conn.socket { + if let Some((_, real_dest_endpoint)) = + self.reverse_nat_table.get(&token) + { + let dest_addr = SocketAddr::new( + real_dest_endpoint.addr.into(), + real_dest_endpoint.port, + ); + trace!(?token, bytes = udp.payload().len(), %dest_addr, "Forwarding subsequent UDP packet from guest to host"); + if let Err(e) = + udp_socket.send_to(udp.payload(), dest_addr) + { + error!(?token, error = %e, "Failed to send subsequent UDP packet to host"); + } + conn.last_activity = Instant::now(); + } else { + warn!(?token, "Could not find reverse NAT entry for existing UDP session"); + } + } + } else { + warn!( + ?token, + "Could not find connection for existing UDP session" + ); + } + // We handled the packet. + return true; + } + + // This is the FIRST packet for a new UDP session. + // Create the host socket and NAT state. + self.handle_udp_datagram(src, dst, udp); + // We've handled this packet by sending it directly. + return true; + } + } + _ => {} + } + } + } + false + } + + /// Handles events on host-side TCP sockets. + fn handle_host_socket_event(&mut self, token: Token, event: &Event) { + trace!( + ?token, + readable = event.is_readable(), + writable = event.is_writable(), + "handling socket event" + ); + let mut keep_connection = true; + if let Some(Conn { + socket: mut stream, + handle, + mut last_activity, + }) = self.host_connections.remove(&token) + { + trace!(?token, %handle, "found connection for token"); + match &mut stream { + HostSocket::Tcp(stream) => { + trace!(?token, "fowarding tcp stream"); + if !self.forward_stream(token, event, stream, handle) { + keep_connection = false; + } + last_activity = Instant::now(); + } + HostSocket::Unix(stream) => { + trace!(?token, "fowarding unix stream"); + if !self.forward_stream(token, event, stream, handle) { + keep_connection = false; + } + last_activity = Instant::now(); + } + HostSocket::Udp(stream) => { + // The `handle` is for the shared smoltcp socket used for replies. + // The `stream` is the session-specific mio socket. + + if event.is_readable() { + if let Some((guest_endpoint, _)) = self.reverse_nat_table.get(&token) { + let mut buffer = [0u8; 2048]; + loop { + match stream.recv_from(&mut buffer) { + Ok((size, real_source)) => { + trace!(?token, bytes = size, %real_source, %guest_endpoint, "Received UDP reply from host for guest"); + last_activity = Instant::now(); // Update activity timer + + let payload = &buffer[..size]; + + let raw_socket = + self.sockets.get_mut::( + self.raw_socket_handle, + ); + + // Manually construct the IPv4 and UDP headers using pnet, but NOT the Ethernet header. + // The buffer for this needs to be large enough for an IP packet. + let mut ip_packet_buf = vec![0u8; 20 + 8 + payload.len()]; + + // Create IPv4 packet view. + let mut ipv4_packet = + MutableIpv4Packet::new(&mut ip_packet_buf).unwrap(); + ipv4_packet.set_version(4); + ipv4_packet.set_header_length(5); + ipv4_packet + .set_total_length((20 + 8 + payload.len()) as u16); + ipv4_packet.set_ttl(64); + ipv4_packet + .set_next_level_protocol(IpNextHeaderProtocols::Udp); + + // Spoof the source and destination IPs. + let src_ip: std::net::Ipv4Addr = + if let IpAddr::V4(addr) = real_source.ip() { + addr + } else { + unimplemented!("IPv6 not supported for UDP NAT yet") + }; + let dst_ip: std::net::Ipv4Addr = + if let IpAddress::Ipv4(addr) = guest_endpoint.addr { + addr + } else { + unimplemented!("IPv6 not supported for UDP NAT yet") + }; + + ipv4_packet.set_source(src_ip); + ipv4_packet.set_destination(dst_ip); + ipv4_packet.set_checksum(pnet::packet::ipv4::checksum( + &ipv4_packet.to_immutable(), + )); + + // Create UDP packet view. + let mut udp_packet = + MutableUdpPacket::new(ipv4_packet.payload_mut()) + .unwrap(); + udp_packet.set_source(real_source.port()); + udp_packet.set_destination(guest_endpoint.port); + udp_packet.set_length((8 + payload.len()) as u16); + udp_packet.set_payload(payload); + udp_packet.set_checksum(pnet::packet::udp::ipv4_checksum( + &udp_packet.to_immutable(), + &src_ip, + &dst_ip, + )); + + // Send the IP packet using the smoltcp raw socket. + // smoltcp will now wrap it in a proper Ethernet frame and send it. + if let Err(e) = raw_socket.send_slice(&ip_packet_buf) { + error!( + "Failed to send UDP reply via raw socket: {}", + e + ); + } + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + // No more data to read for now + break; + } + Err(e) => { + error!(?token, error = %e, "Error reading from host UDP socket"); + break; + } + } + } + } else { + warn!(?token, "could not find udp socket in reverse_nat_table! this shouldn't happen"); + } + } + } + } + + if keep_connection { + self.host_connections.insert( + token, + Conn { + socket: stream, + handle, + last_activity, + }, + ); + } else { + trace!( + ?token, + ?handle, + "Connection terminated. Removing smoltcp socket." + ); + // Close the OS socket + match stream { + HostSocket::Tcp(s) => _ = s.shutdown(std::net::Shutdown::Both), + HostSocket::Unix(s) => _ = s.shutdown(std::net::Shutdown::Both), + _ => {} + } + self.sockets.remove(handle); + // Also remove from NAT tables if applicable + if let Some((guest_ep, _)) = self.reverse_nat_table.remove(&token) { + self.nat_table.remove(&guest_ep); + } + } + } + } + + fn get_ephemeral_port(&mut self) -> u16 { + const EPHEMERAL_PORT_MIN: u16 = 49152; + + loop { + // Get the next port number from our counter. + let candidate_port = self.next_ephemeral_port; + + // Increment the counter for the next time, wrapping around if needed. + self.next_ephemeral_port = self.next_ephemeral_port.wrapping_add(1); + if self.next_ephemeral_port < EPHEMERAL_PORT_MIN { + self.next_ephemeral_port = EPHEMERAL_PORT_MIN; + } + + // Check if the candidate port is already in use by any existing socket. + let is_in_use = self.sockets.iter().any(|(_, socket)| { + let local_port = match socket { + smoltcp::socket::Socket::Tcp(s) => s.local_endpoint().map(|ep| ep.port), + smoltcp::socket::Socket::Udp(s) => Some(s.endpoint().port), + // Add other socket types here if you use them + _ => None, + }; + local_port == Some(candidate_port) + }); + + // If the port is not in use, we've found one. Return it. + if !is_in_use { + return candidate_port; + } + + // Otherwise, the loop continues and we'll try the next port. + } + } + + fn handle_udp_datagram( + &mut self, + guest_addr: std::net::Ipv4Addr, + dest_addr: std::net::Ipv4Addr, + udp_packet: UdpPacket, + ) { + let guest_addr = IpAddress::Ipv4(guest_addr); + let dest_addr = IpAddress::Ipv4(dest_addr); + let guest_port = udp_packet.get_source(); + let dest_port = udp_packet.get_destination(); + + let guest_endpoint = IpEndpoint::new(guest_addr, guest_port); + let dest_endpoint = IpEndpoint::new(dest_addr, dest_port); + + trace!( + "New UDP session from guest {}:{} to {}:{}", + guest_addr, + guest_port, + dest_addr, + dest_port + ); + + let is_ipv4 = dest_addr.version() == IpVersion::Ipv4; + let domain = if is_ipv4 { Domain::IPV4 } else { Domain::IPV6 }; + + // Create and configure the host-facing socket + let socket = Socket::new(domain, socket2::Type::DGRAM, None).unwrap(); + const BUF_SIZE: usize = 8 * 1024 * 1024; + if let Err(e) = socket.set_recv_buffer_size(BUF_SIZE) { + warn!(error = %e, "Failed to set UDP receive buffer size."); + } + if let Err(e) = socket.set_send_buffer_size(BUF_SIZE) { + warn!(error = %e, "Failed to set UDP send buffer size."); + } + socket.set_nonblocking(true).unwrap(); + + let bind_addr: SocketAddr = if is_ipv4 { "0.0.0.0:0" } else { "[::]:0" } + .parse() + .unwrap(); + socket.bind(&bind_addr.into()).unwrap(); + + let mut mio_socket = mio::net::UdpSocket::from_std(socket.into()); + + // Register with mio and update NAT tables + let token = Token(self.next_token); + self.next_token += 1; + + self.registry + .register(&mut mio_socket, token, Interest::READABLE) + .unwrap(); + + // The host_connections entry now represents a single UDP session. + // The handle is a dummy value since we are not using a smoltcp socket for UDP. + self.host_connections.insert( + token, + Conn { + socket: HostSocket::Udp(mio_socket), + handle: SocketHandle::default(), // Dummy handle + last_activity: Instant::now(), + }, + ); + + self.nat_table.insert(guest_endpoint, token); + self.reverse_nat_table + .insert(token, (guest_endpoint, dest_endpoint)); + + if let Some(conn) = self.host_connections.get(&token) { + if let HostSocket::Udp(s) = &conn.socket { + let real_dest = SocketAddr::new(dest_addr.into(), dest_port); + if let Err(e) = s.send_to(udp_packet.payload(), real_dest.into()) { + error!("Failed to send initial UDP datagram: {}", e); + } + } + } + } + + /// Signals the guest that there are used descriptors in a queue. + fn signal_used_queue(&mut self, queue_index: usize) -> Result<(), DeviceError> { + self.interrupt_status + .fetch_or(VIRTIO_MMIO_INT_VRING as usize, Ordering::SeqCst); + if let Some(intc) = &self.intc { + intc.lock() + .unwrap() + .set_irq(self.irq_line, Some(&self.interrupt_evt))?; + } + Ok(()) + } +} + +mod packet_dumper { + use super::*; + use pnet::packet::{ + arp::{ArpOperations, ArpPacket}, + ethernet::{EtherTypes, EthernetPacket}, + ip::IpNextHeaderProtocols, + ipv4::Ipv4Packet, + ipv6::Ipv6Packet, + tcp::{TcpFlags, TcpPacket}, + Packet, + }; + fn format_tcp_flags(flags: u8) -> String { + let mut s = String::new(); + if (flags & TcpFlags::SYN) != 0 { + s.push('S'); + } + if (flags & TcpFlags::ACK) != 0 { + s.push('.'); + } + if (flags & TcpFlags::FIN) != 0 { + s.push('F'); + } + if (flags & TcpFlags::RST) != 0 { + s.push('R'); + } + if (flags & TcpFlags::PSH) != 0 { + s.push('P'); + } + if (flags & TcpFlags::URG) != 0 { + s.push('U'); + } + s + } + pub fn log_vm_packet_in(data: &[u8]) -> PacketDumper { + PacketDumper { + data, + direction: "VM|IN", + } + } + pub fn log_vm_packet_out(data: &[u8]) -> PacketDumper { + PacketDumper { + data, + direction: "VM|OUT", + } + } + + pub struct PacketDumper<'a> { + data: &'a [u8], + direction: &'static str, + } + + impl<'a> std::fmt::Display for PacketDumper<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(eth) = EthernetPacket::new(self.data) { + match eth.get_ethertype() { + EtherTypes::Ipv4 => { + if let Some(ipv4) = Ipv4Packet::new(eth.payload()) { + let src = ipv4.get_source(); + let dst = ipv4.get_destination(); + match ipv4.get_next_level_protocol() { + IpNextHeaderProtocols::Tcp => { + if let Some(tcp) = TcpPacket::new(ipv4.payload()) { + write!(f, "[{}] IP {}.{} > {}.{}: Flags [{}], seq {}, ack {}, win {}, len {}", + self.direction, src, tcp.get_source(), dst, tcp.get_destination(), + format_tcp_flags(tcp.get_flags()), tcp.get_sequence(), + tcp.get_acknowledgement(), tcp.get_window(), tcp.payload().len()) + } else { + write!( + f, + "[{}] IP {} > {}: TCP (parse failed)", + self.direction, src, dst + ) + } + } + IpNextHeaderProtocols::Udp => { + if let Some(udp) = UdpPacket::new(ipv4.payload()) { + write!( + f, + "[{}] IP {}.{} > {}.{}: len {} ({} > {})", + self.direction, + src, + udp.get_source(), + dst, + udp.get_destination(), + udp.get_length(), + eth.get_source(), + eth.get_destination() + ) + } else { + write!( + f, + "[{}] IP {} > {}: UDP (parse failed)", + self.direction, src, dst + ) + } + } + _ => write!( + f, + "[{}] IPv4 {} > {}: proto {} ({} > {})", + self.direction, + src, + dst, + ipv4.get_next_level_protocol(), + eth.get_source(), + eth.get_destination(), + ), + } + } else { + write!(f, "[{}] IPv4 packet (parse failed)", self.direction) + } + } + EtherTypes::Ipv6 => { + if let Some(ipv6) = Ipv6Packet::new(eth.payload()) { + let src = ipv6.get_source(); + let dst = ipv6.get_destination(); + match ipv6.get_next_header() { + IpNextHeaderProtocols::Tcp => { + if let Some(tcp) = TcpPacket::new(ipv6.payload()) { + write!(f, "[{}] IP6 [{}]:{} > [{}]:{}: Flags [{}], seq {}, ack {}, win {}, len {}", + self.direction, src, tcp.get_source(), dst, tcp.get_destination(), + format_tcp_flags(tcp.get_flags()), tcp.get_sequence(), + tcp.get_acknowledgement(), tcp.get_window(), tcp.payload().len()) + } else { + write!( + f, + "[{}] IP6 {} > {}: TCP (parse failed)", + self.direction, src, dst + ) + } + } + _ => write!( + f, + "[{}] IPv6 {} > {}: proto {}", + self.direction, + src, + dst, + ipv6.get_next_header() + ), + } + } else { + write!(f, "[{}] IPv6 packet (parse failed)", self.direction) + } + } + EtherTypes::Arp => { + if let Some(arp) = ArpPacket::new(eth.payload()) { + write!( + f, + "[{}] ARP, {}, who has {}? Tell {}", + self.direction, + if arp.get_operation() == ArpOperations::Request { + "request" + } else { + "reply" + }, + arp.get_target_proto_addr(), + arp.get_sender_proto_addr() + ) + } else { + write!(f, "[{}] ARP packet (parse failed)", self.direction) + } + } + _ => write!( + f, + "[{}] Unknown L3 protocol: {}", + self.direction, + eth.get_ethertype() + ), + } + } else { + write!(f, "[{}] Ethernet packet (parse failed)", self.direction) + } + } + } +} diff --git a/src/devices/src/virtio/net/worker.rs b/src/devices/src/virtio/net/worker.rs index 015463dbc..48c894604 100644 --- a/src/devices/src/virtio/net/worker.rs +++ b/src/devices/src/virtio/net/worker.rs @@ -68,6 +68,7 @@ impl NetWorker { VirtioNetBackend::Gvproxy(path) => { Box::new(Gvproxy::new(path).unwrap()) as Box } + _ => unimplemented!(), }; Self { diff --git a/src/hvf/src/lib.rs b/src/hvf/src/lib.rs index 0a2755ae9..4d62d1b7c 100644 --- a/src/hvf/src/lib.rs +++ b/src/hvf/src/lib.rs @@ -113,7 +113,7 @@ pub enum Error { VcpuSetRegister, VcpuSetSystemRegister(u16, u64), VcpuSetVtimerMask, - VmCreate, + VmCreate(i32), } impl Display for Error { @@ -143,7 +143,7 @@ impl Display for Error { reg, val ), VcpuSetVtimerMask => write!(f, "Error setting HVF vCPU vtimer mask"), - VmCreate => write!(f, "Error creating HVF VM instance"), + VmCreate(code) => write!(f, "Error creating HVF VM instance, code: {code}"), } } } @@ -255,7 +255,7 @@ impl HvfVm { let ret = unsafe { hv_vm_create(config) }; if ret != HV_SUCCESS { - Err(Error::VmCreate) + Err(Error::VmCreate(ret)) } else { Ok(Self {}) } diff --git a/src/libkrun/Cargo.toml b/src/libkrun/Cargo.toml index 4691c9876..61f89dbe8 100644 --- a/src/libkrun/Cargo.toml +++ b/src/libkrun/Cargo.toml @@ -7,10 +7,10 @@ build = "build.rs" [features] tee = [] -amd-sev = [ "blk", "tee" ] -net = [] -blk = [] -efi = [ "blk", "net" ] +amd-sev = ["blk", "tee"] +net = ["devices/net", "vmm/net"] +blk = ["devices/blk", "vmm/blk"] +efi = ["blk", "net"] gpu = [] snd = [] virgl_resource_map2 = [] @@ -27,6 +27,7 @@ devices = { path = "../devices" } polly = { path = "../polly" } utils = { path = "../utils" } vmm = { path = "../vmm" } +vm-memory = { version = ">=0.13", features = ["backend-mmap"] } [target.'cfg(target_os = "macos")'.dependencies] hvf = { path = "../hvf" } @@ -38,4 +39,4 @@ vm-memory = ">=0.13" [lib] name = "krun" -crate-type = ["cdylib"] +# crate-type = ["cdylib"] diff --git a/src/libkrun/src/lib.rs b/src/libkrun/src/lib.rs index 00ab44f02..6b3c1a5b6 100644 --- a/src/libkrun/src/lib.rs +++ b/src/libkrun/src/lib.rs @@ -5,9 +5,9 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::convert::TryInto; use std::env; -use std::ffi::CStr; #[cfg(target_os = "linux")] use std::ffi::CString; +use std::ffi::{CStr, OsStr}; #[cfg(all(target_arch = "x86_64", not(feature = "tee")))] use std::fs::File; #[cfg(target_os = "linux")] @@ -16,17 +16,18 @@ use std::os::fd::RawFd; use std::path::PathBuf; use std::slice; use std::sync::atomic::{AtomicI32, Ordering}; -#[cfg(not(feature = "efi"))] -use std::sync::LazyLock; use std::sync::Mutex; +#[cfg(not(feature = "efi"))] +use std::sync::OnceLock; -use crossbeam_channel::unbounded; +use crossbeam_channel::{unbounded, Sender}; #[cfg(feature = "blk")] use devices::virtio::block::ImageType; #[cfg(feature = "net")] use devices::virtio::net::device::VirtioNetBackend; #[cfg(feature = "blk")] use devices::virtio::CacheType; +use devices::virtio::Queue; use env_logger::Env; #[cfg(not(feature = "efi"))] use libc::size_t; @@ -34,6 +35,7 @@ use libc::{c_char, c_int}; use once_cell::sync::Lazy; use polly::event_manager::EventManager; use utils::eventfd::EventFd; +use vm_memory::GuestMemoryMmap; use vmm::resources::VmResources; #[cfg(feature = "blk")] use vmm::vmm_config::block::BlockDeviceConfig; @@ -68,8 +70,7 @@ const KRUNFW_NAME: &str = "libkrunfw.4.dylib"; const INIT_PATH: &str = "/init.krun"; #[cfg(not(feature = "efi"))] -static KRUNFW: LazyLock> = - LazyLock::new(|| unsafe { libloading::Library::new(KRUNFW_NAME).ok() }); +static KRUNFW: OnceLock = OnceLock::new(); #[cfg(not(feature = "efi"))] pub struct KrunfwBindings { @@ -85,11 +86,27 @@ pub struct KrunfwBindings { #[cfg(not(feature = "efi"))] impl KrunfwBindings { - fn load_bindings() -> Result { - let krunfw = match KRUNFW.as_ref() { - Some(krunfw) => krunfw, - None => return Err(libloading::Error::DlOpenUnknown), + fn load_bindings>( + path: Option

, + ) -> Result { + if let Some(p) = path { + eprintln!("setting custom krunfw"); + KRUNFW + .set(unsafe { libloading::Library::new(p)? }) + .expect("could not set custom KRUNFW"); + } + let krunfw = if let Some(krunfw) = KRUNFW.get() { + krunfw + } else { + eprintln!("attempting to load default krunfw {KRUNFW_NAME}"); + let lib = unsafe { libloading::Library::new(KRUNFW_NAME)? }; + KRUNFW.set(lib).expect("could not set default KRUNFW"); + KRUNFW.get().unwrap() }; + // match KRUNFW.get_or_init(|| unsafe { libloading::Library::new(KRUNFW_NAME).ok() }) { + // Some(krunfw) => krunfw, + // None => return Err(libloading::Error::DlOpenUnknown), + // }; Ok(unsafe { KrunfwBindings { get_kernel: krunfw.get(b"krunfw_get_kernel")?, @@ -101,8 +118,8 @@ impl KrunfwBindings { }) } - pub fn new() -> Option { - Self::load_bindings().ok() + pub fn new>(path: Option

) -> Option { + Self::load_bindings(path).ok() } } @@ -115,6 +132,7 @@ enum NetworkConfig { Tsi(TsiConfig), VirtioNetPasst(RawFd), VirtioNetGvproxy(PathBuf), + VirtioNetProxy(Vec<(u16, String)>), } impl Default for NetworkConfig { @@ -152,6 +170,7 @@ struct ContextConfig { console_output: Option, vmm_uid: Option, vmm_gid: Option, + kernel_cmdline: Option, } impl ContextConfig { @@ -258,6 +277,7 @@ impl ContextConfig { } NetworkConfig::VirtioNetPasst(_) => Err(()), NetworkConfig::VirtioNetGvproxy(_) => Err(()), + NetworkConfig::VirtioNetProxy(_) => Err(()), } } @@ -315,8 +335,7 @@ pub extern "C" fn krun_set_log_level(level: u32) -> i32 { KRUN_SUCCESS } -#[no_mangle] -pub extern "C" fn krun_create_ctx() -> i32 { +pub fn krun_create_ctx>(krunfw: Option

) -> i32 { let ctx_cfg = { let shutdown_efd = if cfg!(feature = "efi") { Some(EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap()) @@ -326,7 +345,7 @@ pub extern "C" fn krun_create_ctx() -> i32 { ContextConfig { #[cfg(not(feature = "efi"))] - krunfw: KrunfwBindings::new(), + krunfw: KrunfwBindings::new(krunfw), shutdown_efd, ..Default::default() } @@ -631,13 +650,13 @@ pub unsafe extern "C" fn krun_set_passt_fd(ctx_id: u32, fd: c_int) -> i32 { return -libc::ENOTSUP; } - match CTX_MAP.lock().unwrap().entry(ctx_id) { - Entry::Occupied(mut ctx_cfg) => { - let cfg = ctx_cfg.get_mut(); - cfg.set_net_cfg(NetworkConfig::VirtioNetPasst(fd)); - } - Entry::Vacant(_) => return -libc::ENOENT, - } + // match CTX_MAP.lock().unwrap().entry(ctx_id) { + // Entry::Occupied(mut ctx_cfg) => { + // let cfg = ctx_cfg.get_mut(); + // cfg.set_net_cfg(NetworkConfig::VirtioNetPasst(fd)); + // } + // Entry::Vacant(_) => return -libc::ENOENT, + // } KRUN_SUCCESS } @@ -664,6 +683,22 @@ pub unsafe extern "C" fn krun_set_gvproxy_path(ctx_id: u32, c_path: *const c_cha KRUN_SUCCESS } +pub fn krun_set_direct_proxy(ctx_id: u32, listeners: &[(u16, &str)]) -> i32 { + match CTX_MAP.lock().unwrap().entry(ctx_id) { + Entry::Occupied(mut ctx_cfg) => { + let cfg = ctx_cfg.get_mut(); + cfg.set_net_cfg(NetworkConfig::VirtioNetProxy( + listeners + .iter() + .map(|(vm_port, path)| (*vm_port, (*path).to_owned())) + .collect(), + )); + } + Entry::Vacant(_) => return -libc::ENOENT, + } + KRUN_SUCCESS +} + #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn krun_set_net_mac(ctx_id: u32, c_mac: *const u8) -> i32 { @@ -1346,8 +1381,41 @@ pub extern "C" fn krun_setgid(ctx_id: u32, gid: libc::gid_t) -> i32 { KRUN_SUCCESS } +#[allow(clippy::missing_safety_doc)] #[no_mangle] -pub extern "C" fn krun_start_enter(ctx_id: u32) -> i32 { +pub unsafe extern "C" fn krun_set_kernel_cmdline(ctx_id: u32, c_cmdline: *const c_char) -> i32 { + let cmdline = match CStr::from_ptr(c_cmdline).to_str() { + Ok(cmdline) => cmdline, + Err(e) => { + error!("Error parsing cmdline: {:?}", e); + return -libc::EINVAL; + } + }; + + match CTX_MAP.lock().unwrap().entry(ctx_id) { + Entry::Occupied(mut ctx_cfg) => { + let cfg = ctx_cfg.get_mut(); + cfg.kernel_cmdline = Some(cmdline.to_owned()); + } + Entry::Vacant(_) => return -libc::ENOENT, + } + + KRUN_SUCCESS +} + +pub struct StartVmm { + pub handle: std::thread::JoinHandle>, + pub virtio_net: Option, +} + +pub struct VirtioNetDevice { + pub rx_queue: Queue, + pub tx_queue: Queue, + pub tx_eventfd: std::fs::File, // The File for notifying the guest + pub guest_memory: GuestMemoryMmap, // The shared memory region +} + +pub fn krun_start_enter(ctx_id: u32) -> i32 { #[cfg(target_os = "linux")] { let prname = match env::var("HOSTNAME") { @@ -1385,8 +1453,8 @@ pub extern "C" fn krun_start_enter(ctx_id: u32) -> i32 { #[cfg(feature = "blk")] for block_cfg in ctx_cfg.get_block_cfg() { - if ctx_cfg.vmr.add_block_device(block_cfg).is_err() { - error!("Error configuring virtio-blk for block"); + if let Err(e) = ctx_cfg.vmr.add_block_device(block_cfg) { + error!("Error configuring virtio-blk for block: {e}"); return -libc::EINVAL; } } @@ -1408,19 +1476,40 @@ pub extern "C" fn krun_start_enter(ctx_id: u32) -> i32 { return -libc::EINVAL; } - let boot_source = BootSourceConfig { - kernel_cmdline_prolog: Some(format!( - "{} init={} {} {} {} {}", - DEFAULT_KERNEL_CMDLINE, - INIT_PATH, - ctx_cfg.get_exec_path(), - ctx_cfg.get_workdir(), - ctx_cfg.get_rlimits(), - ctx_cfg.get_env(), - )), - kernel_cmdline_epilog: Some(format!(" -- {}", ctx_cfg.get_args())), + let boot_source = if let Some(kernel_cmdline) = &ctx_cfg.kernel_cmdline { + BootSourceConfig { + kernel_cmdline_prolog: Some(kernel_cmdline.clone()), + kernel_cmdline_epilog: Some(format!(" -- {}", ctx_cfg.get_args())), + } + } else { + BootSourceConfig { + kernel_cmdline_prolog: Some(format!( + "{} init={} {} {} {} {}", + DEFAULT_KERNEL_CMDLINE, + "/sbin/init", // INIT_PATH, + ctx_cfg.get_exec_path(), + ctx_cfg.get_workdir(), + ctx_cfg.get_rlimits(), + ctx_cfg.get_env(), + )), + kernel_cmdline_epilog: Some(format!(" -- {}", ctx_cfg.get_args())), + } }; + // eprintln!( + // "cmdline: {}{}", + // boot_source + // .kernel_cmdline_prolog + // .as_ref() + // .map(|s| s.as_str()) + // .unwrap_or_default(), + // boot_source + // .kernel_cmdline_epilog + // .as_ref() + // .map(|s| s.as_str()) + // .unwrap_or_default() + // ); + if ctx_cfg.vmr.set_boot_source(boot_source).is_err() { return -libc::EINVAL; } @@ -1438,6 +1527,8 @@ pub extern "C" fn krun_start_enter(ctx_id: u32) -> i32 { vsock_set = true; } + let mut wants_virtio_net = false; + match ctx_cfg.net_cfg { NetworkConfig::Tsi(tsi_cfg) => { vsock_config.host_port_map = tsi_cfg.port_map; @@ -1457,6 +1548,13 @@ pub extern "C" fn krun_start_enter(ctx_id: u32) -> i32 { create_virtio_net(&mut ctx_cfg, backend); } } + NetworkConfig::VirtioNetProxy(ref listeners) => { + #[cfg(feature = "net")] + { + let backend = VirtioNetBackend::Proxy(listeners.clone()); + create_virtio_net(&mut ctx_cfg, backend); + } + } } if vsock_set { diff --git a/src/vmm/Cargo.toml b/src/vmm/Cargo.toml index fa010a4ca..f3f8ce11d 100644 --- a/src/vmm/Cargo.toml +++ b/src/vmm/Cargo.toml @@ -21,7 +21,6 @@ libc = ">=0.2.39" linux-loader = { version = "0.13.0", features = ["bzimage", "elf", "pe"] } log = "0.4.0" vm-memory = { version = ">=0.13", features = ["backend-mmap"] } -rangemap = "1.5.1" arch = { path = "../arch" } devices = { path = "../devices" } diff --git a/src/vmm/src/linux/vstate.rs b/src/vmm/src/linux/vstate.rs index 90c887c1a..a346f9098 100644 --- a/src/vmm/src/linux/vstate.rs +++ b/src/vmm/src/linux/vstate.rs @@ -10,6 +10,7 @@ use libc::{c_int, c_void, siginfo_t}; use std::cell::Cell; use std::fmt::{Display, Formatter}; use std::io; +use std::ops::Range; use std::os::unix::io::RawFd; @@ -57,8 +58,6 @@ use vm_memory::{ GuestRegionMmap, }; -use rangemap::RangeMap; - #[cfg(feature = "amd-sev")] use sev::launch::snp; @@ -457,7 +456,7 @@ pub struct Vm { #[cfg(feature = "amd-sev")] pub tee_config: Tee, - pub guest_memfds: RangeMap, + pub guest_memfds: Vec<(Range, RawFd)>, } impl Vm { @@ -482,7 +481,7 @@ impl Vm { supported_cpuid, #[cfg(target_arch = "x86_64")] supported_msrs, - guest_memfds: RangeMap::new(), + guest_memfds: Vec::new(), }) } @@ -521,7 +520,7 @@ impl Vm { supported_msrs, tee, tee_config: tee_config.tee, - guest_memfds: RangeMap::new(), + guest_memfds: Vec::new(), }) } @@ -560,7 +559,12 @@ impl Vm { } pub fn guest_memfd_get(&self, gpa: u64) -> Option<(RawFd, u64)> { - self.guest_memfds.get(&gpa).copied() + for (range, rawfd) in self.guest_memfds.iter() { + if range.contains(&gpa) { + return Some((*rawfd, range.start)); + } + } + None } #[allow(unused_mut)] @@ -631,7 +635,7 @@ impl Vm { .set_memory_attributes(attr) .map_err(Error::SetMemoryAttributes)?; - self.guest_memfds.insert(start..end, (guest_memfd, start)); + self.guest_memfds.push((Range { start, end }, guest_memfd)); } self.next_mem_slot += 1; diff --git a/src/vmm/src/resources.rs b/src/vmm/src/resources.rs index c2650e3a5..079307982 100644 --- a/src/vmm/src/resources.rs +++ b/src/vmm/src/resources.rs @@ -337,6 +337,8 @@ mod tests { external_kernel: None, fs: Default::default(), vsock: Default::default(), + #[cfg(feature = "blk")] + block: Default::default(), #[cfg(feature = "net")] net_builder: Default::default(), gpu_virgl_flags: None,