commit 226a7e6c862fa4a3b262e676c8ab63715b11e62e Author: Nikolay Edigaryev Date: Fri Jun 10 16:51:29 2022 +0300 Initial revision of Softnet, a software networking for Tart diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..75f84bb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,688 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "darling" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dhcproto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5e3e1b355a83bd58b250b7611ce150f36f943d9a5bfa67c95647cd13483169e" +dependencies = [ + "rand", + "thiserror", +] + +[[package]] +name = "enum-iterator" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a7cbade83f72b1330fa58dc6203176f374e61eb3682055b6d3df05f56723f85" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a375a452d20c4dbbaa397e84cdfaa2712b4b3d22bf49426e9c8725d10fef005" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hexdump" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40283dadb02f3af778878be1d717b17b4e4ab92e1d935ab03a730b0542905f2" +dependencies = [ + "arrayvec", + "itertools", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + +[[package]] +name = "itertools" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a9b56eb56058f43dc66e58f40a214b2ccbc9f3df51861b63d51dec7b65bc3f" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mac_address" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d1bc1084549d60725ccc53a2bfa07f67fe4689fda07b05a36531f2988104a" +dependencies = [ + "nix 0.23.1", + "winapi", +] + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "num_enum" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "os_str_bytes" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" + +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "privdrop" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5b1f7e40f628a2f8f90e40d3f313be83066cc61997fdcb96cade6abf7cee93" +dependencies = [ + "libc", + "nix 0.24.1", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "serde" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" + +[[package]] +name = "smoltcp" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72165c4af59f5f19c7fb774b88b95660591b612380305b5f4503157341a9f7ee" +dependencies = [ + "bitflags", + "byteorder", + "libc", + "log", + "managed", +] + +[[package]] +name = "softnet" +version = "0.1.0" +dependencies = [ + "clap", + "dhcproto", + "ip_network", + "libc", + "mac_address", + "num_enum", + "polling", + "privdrop", + "smoltcp", + "system-configuration", + "thiserror", + "users", + "vmnet", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-configuration" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75182f12f490e953596550b65ee31bda7c8e043d9386174b353bda50838c3fd" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "uuid" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238" +dependencies = [ + "getrandom", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vmnet" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3466ad0e4409268556ce303dc9d86c8dc500a180650eef600676a7b515cbe5" +dependencies = [ + "bitflags", + "block", + "enum-iterator", + "hexdump", + "lazy_static", + "libc", + "num_enum", + "thiserror", + "uuid", + "vmnet-derive", +] + +[[package]] +name = "vmnet-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8d8b9df9abd3c39593e6d79cf2a28771c6d90f4fe6c524491f6906a509de2b" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a9e7f4f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "softnet" +version = "0.1.0" +publish = false +edition = "2021" + +[lib] +path = "lib/mod.rs" + +[dependencies] +smoltcp = "0.8.1" +libc = "0.2.126" +polling = "2.2.0" +dhcproto = "0.7.0" +vmnet = "0.1.1" +clap = { version = "3.1.18", features = ["derive"] } +mac_address = "1.1.3" +privdrop = "0.5.2" +thiserror = "1.0.31" +ip_network = "0.4.1" +users = "0.11.0" +system-configuration = "0.5.0" +num_enum = "0.5.7" diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a30de4 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Softnet + +Softnet is a software networking for [Tart](https://github.com/cirruslabs/tart) which provides better network isolation and alleviates DHCP shortage on production systems. + +## Working model + +Softnet solves two problems: + +1. VM network isolation + * [`VZNATNetworkDeviceAttachment`](https://developer.apple.com/documentation/virtualization/vznatnetworkdeviceattachment) (the default networking in Tart) enables [vmnet's bridge isolation](https://developer.apple.com/documentation/vmnet/vmnet_enable_isolation_key) by default and prevents cross-VM traffic, however it's still possible for any VM to spoof the host's ARP-table and capture other VMs traffic, for example +2. DHCP exhaustion + * macOS built-in DHCP-server allocates a `/24` subnet with 86400 seconds lease time by default, which only allows for ~253 VMs a day (or 1 VM every ~6 minutes) to be spawned without causing a denial-of-service, which is pretty limiting for CI services like Cirrus CI + +And assumes that: + +1. Tart gives it's VMs unique MAC-addresses +2. macOS built-in DHCP-server won't re-use the IP-addresses from it's pool until their lease expire + +...otherwise it's possible for two VMs to receive an identical IP-address from the macOS built-in DHCP-server (even in the presence of Softnet's packet filtering) and thus bypass the protections offered by Softnet. + +## Installing + +For proper functioning Softnet binary requires two things: + +* a [SUID-bit](https://en.wikipedia.org/wiki/Setuid#SUID) set which effectively gives it `root` privileges + * this is needed in order to create [`vmnet.framework`](https://developer.apple.com/documentation/vmnet) interface and perform DHCP-related tweaks + * the privileges will be dropped once all of the initialization is completed +* to be available in `PATH` + * so that the Tart will be able to find it + +## Running + +Softnet is started and managed automatically by Tart if `--with-softnet` flag is present when calling `tart run`. diff --git a/lib/dhcp_snooper.rs b/lib/dhcp_snooper.rs new file mode 100644 index 0000000..07c0e70 --- /dev/null +++ b/lib/dhcp_snooper.rs @@ -0,0 +1,79 @@ +use dhcproto::v4::{DhcpOption, MessageType, OptionCode}; +use dhcproto::Decodable; +use smoltcp::wire::Ipv4Address; +use std::collections::HashSet; +use std::time::{Duration, Instant}; + +#[derive(Default)] +pub struct DhcpSnooper { + vm_lease: Option, +} + +impl DhcpSnooper { + pub fn register_dhcp_reply(&mut self, dhcp_packet: &[u8]) { + let mut decoder = dhcproto::v4::Decoder::new(dhcp_packet); + + let message = match dhcproto::v4::Message::decode(&mut decoder) { + Ok(message) => message, + Err(_) => return, + }; + + match message.opts().msg_type() { + Some(MessageType::Ack) => { + let lease_time = match message.opts().get(OptionCode::AddressLeaseTime) { + Some(DhcpOption::AddressLeaseTime(lease_time)) => lease_time, + _ => return, + }; + + let dns_ips = match message.opts().get(OptionCode::DomainNameServer) { + Some(DhcpOption::DomainNameServer(dns_ips)) => HashSet::from_iter( + dns_ips.iter().map(|dns_ip| Ipv4Address(dns_ip.octets())), + ), + _ => HashSet::new(), + }; + + self.vm_lease = Some(Lease::new( + message.yiaddr().into(), + Duration::from_secs(*lease_time as u64), + dns_ips, + )) + } + Some(MessageType::Nak) => { + self.vm_lease = None; + } + _ => {} + }; + } + + pub fn lease(&self) -> &Option { + &self.vm_lease + } + + pub fn valid_dns_target(&self, addr: &Ipv4Address) -> bool { + if let Some(lease) = &self.vm_lease { + return lease.dns_ips.contains(addr); + } + + false + } +} + +pub struct Lease { + address: Ipv4Address, + valid_until: Instant, + dns_ips: HashSet, +} + +impl Lease { + fn new(address: Ipv4Address, lease_time: Duration, dns_ips: HashSet) -> Lease { + Lease { + address, + valid_until: Instant::now() + lease_time, + dns_ips, + } + } + + pub fn valid_ip_source(&self, address: Ipv4Address) -> bool { + self.address == address && Instant::now() < self.valid_until + } +} diff --git a/lib/host.rs b/lib/host.rs new file mode 100644 index 0000000..4b24d33 --- /dev/null +++ b/lib/host.rs @@ -0,0 +1,134 @@ +use crate::{Error, Result}; +use std::net::Ipv4Addr; +use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::unix::net::UnixDatagram; +use std::str::FromStr; +use std::sync::mpsc::{sync_channel, SyncSender}; +use vmnet::mode::Mode; +use vmnet::parameters::{Parameter, ParameterKind}; +use vmnet::{Events, Options}; + +pub struct Host { + interface: vmnet::Interface, + new_packets_rx: UnixDatagram, + callback_can_continue_tx: SyncSender<()>, + pub gateway_ip: smoltcp::wire::Ipv4Address, + pub max_packet_size: u64, + finalized: bool, +} + +impl Host { + pub fn new() -> Result { + // Initialize a vmnet.framework NAT interface with isolation enabled + let mut interface = vmnet::Interface::new( + Mode::Shared(Default::default()), + Options { + enable_isolation: Some(true), + ..Default::default() + }, + ) + .map_err(|err| Error::VmnetFailed { source: err })?; + + // Retrieve first IP (gateway) used for this interface + let gateway_ip = match interface.parameters().get(ParameterKind::StartAddress) { + Some(Parameter::StartAddress(gateway_ip)) => gateway_ip, + _ => return Err(Error::VmnetUnexpected), + }; + let gateway_ip = Ipv4Addr::from_str(&gateway_ip).map_err(|_| Error::VmnetUnexpected)?; + + // Retrieve max packet size for this interface + let max_packet_size = match interface.parameters().get(ParameterKind::MaxPacketSize) { + Some(Parameter::MaxPacketSize(max_packet_size)) => max_packet_size, + _ => return Err(Error::VmnetUnexpected), + }; + + // Set up a socketpair() to emulate polling of the vmnet interface + let (new_packets_tx, new_packets_rx) = + UnixDatagram::pair().map_err(|err| Error::InitFailed { source: err.into() })?; + new_packets_rx + .set_nonblocking(true) + .map_err(|err| Error::InitFailed { source: err.into() })?; + + let (callback_can_continue_tx, callback_can_continue_rx) = sync_channel(0); + + interface + .set_event_callback(Events::PACKETS_AVAILABLE, move |_mask, _params| { + // Send a dummy datagram to make the other end of socketpair() readable + new_packets_tx.send(&[0; 1]).unwrap(); + + // Wait for the permission to continue to avoid + // wasting CPU cycles or in case of termination, + // to unblock this Block[1] and allow + // vmnet.framework to terminate + // + // [1]: https://en.wikipedia.org/wiki/Blocks_(C_language_extension) + callback_can_continue_rx.recv().unwrap(); + }) + .map_err(|err| Error::VmnetFailed { source: err })?; + + Ok(Host { + interface, + new_packets_rx, + callback_can_continue_tx, + gateway_ip: gateway_ip.into(), + max_packet_size, + finalized: false, + }) + } +} + +impl Host { + pub fn read(&mut self, buf: &mut [u8]) -> vmnet::Result { + // Dequeue dummy datagram from the socket (if any) + // to free up buffer space and reduce false-positives + // when polling + let mut buf_to_be_discarded: [u8; 1] = [0; 1]; + let _ = self.new_packets_rx.recv(&mut buf_to_be_discarded); + + let result = self.interface.read(buf); + + if let Err(vmnet::Error::VmnetReadNothing) = result { + // We've emptied everything, unlock the callback + // so that it will be able to pick up new events + let _ = self.callback_can_continue_tx.send(()); + } + + result + } + + pub fn write(&mut self, buf: &[u8]) -> vmnet::Result { + self.interface.write(buf) + } + + pub fn finalize(&mut self) -> Result<()> { + // First make sure our callback won't be scheduled again after it finishes + self.interface + .clear_event_callback() + .map_err(|err| Error::VmnetFailed { source: err })?; + + // Now let the callback finish + self.callback_can_continue_tx.send(()).unwrap(); + + self.interface + .finalize() + .map_err(|err| Error::VmnetFailed { source: err })?; + + self.finalized = true; + + Ok(()) + } +} + +impl Drop for Host { + fn drop(&mut self) { + if !self.finalized { + let _ = self.finalize(); + } + } +} + +impl AsRawFd for Host { + fn as_raw_fd(&self) -> RawFd { + self.new_packets_rx.as_raw_fd() + } +} diff --git a/lib/mod.rs b/lib/mod.rs new file mode 100644 index 0000000..63171ae --- /dev/null +++ b/lib/mod.rs @@ -0,0 +1,30 @@ +mod dhcp_snooper; +mod host; +mod poller; +pub mod proxy; +mod vm; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("initialization failed")] + InitFailed { source: Box }, + + #[error("failed to poll")] + PollFailed { source: std::io::Error }, + + #[error("vmnet failed")] + VmnetFailed { source: vmnet::Error }, + + #[error("vmnet returned unexpected data")] + VmnetUnexpected, + + #[error("failed to do I/O on VM socket")] + VMIOFailed { source: std::io::Error }, + + #[error("failed to do I/O on host socket")] + HostIOFailed { source: vmnet::Error }, +} + +pub type Result = std::result::Result; diff --git a/lib/poller.rs b/lib/poller.rs new file mode 100644 index 0000000..60ad917 --- /dev/null +++ b/lib/poller.rs @@ -0,0 +1,76 @@ +use crate::{Error, Result}; +use num_enum::IntoPrimitive; +use std::os::unix::io::RawFd; +use std::time::Duration; + +pub struct Poller { + poller: polling::Poller, + events: Vec, + vm_fd: RawFd, + host_fd: RawFd, +} + +#[derive(IntoPrimitive)] +#[repr(usize)] +enum EventKey { + VM, + Host, +} + +impl Poller { + pub fn new(vm_fd: RawFd, host_fd: RawFd) -> Result { + let poller = + polling::Poller::new().map_err(|err| Error::InitFailed { source: err.into() })?; + + Ok(Poller { + poller, + events: Vec::new(), + vm_fd, + host_fd, + }) + } + + pub fn arm(&self) -> Result<()> { + self.poller + .add(self.vm_fd as RawFd, self.vm_interest()) + .map_err(|err| Error::PollFailed { source: err })?; + + self.poller + .add(self.host_fd as RawFd, self.host_interest()) + .map_err(|err| Error::PollFailed { source: err })?; + + Ok(()) + } + + pub fn rearm(&mut self) -> Result<()> { + self.events.clear(); + + self.poller + .modify(self.vm_fd as RawFd, self.vm_interest()) + .map_err(|err| Error::PollFailed { source: err })?; + self.poller + .modify(self.host_fd as RawFd, self.host_interest()) + .map_err(|err| Error::PollFailed { source: err })?; + + Ok(()) + } + + pub fn wait(&mut self) -> Result<(bool, bool)> { + self.poller + .wait(&mut self.events, Some(Duration::from_millis(100))) + .map_err(|err| Error::PollFailed { source: err })?; + + let vm_readable = self.events.iter().any(|ev| ev.key == EventKey::VM.into()); + let host_readable = self.events.iter().any(|ev| ev.key == EventKey::Host.into()); + + Ok((vm_readable, host_readable)) + } + + fn vm_interest(&self) -> polling::Event { + polling::Event::readable(EventKey::VM.into()) + } + + fn host_interest(&self) -> polling::Event { + polling::Event::readable(EventKey::Host.into()) + } +} diff --git a/lib/proxy/host.rs b/lib/proxy/host.rs new file mode 100644 index 0000000..40681fb --- /dev/null +++ b/lib/proxy/host.rs @@ -0,0 +1,60 @@ +use crate::proxy::udp_packet_helper::UdpPacketHelper; +use crate::proxy::Proxy; +use crate::{Error, Result}; +use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket}; + +impl Proxy { + pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> { + if self.allowed_from_host(frame).is_none() { + // Block packet by not forwarding it to the VM + return Ok(()); + } + + // Snoop bootpd(8) replies from the host to + // figure out the IP assigned to the VM + self.snoop(frame); + + self.vm + .write(frame.as_ref()) + .map(|_| ()) + .map_err(|err| Error::VMIOFailed { source: err }) + } + + fn allowed_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> { + match frame.ethertype() { + EthernetProtocol::Arp => Some(()), + EthernetProtocol::Ipv4 => Some(()), + _ => None, + } + } + + fn snoop(&mut self, frame: &EthernetFrame<&[u8]>) { + if frame.ethertype() != EthernetProtocol::Ipv4 { + return; + } + + let ipv4_pkt = match Ipv4Packet::new_checked(frame.payload()) { + Ok(ipv4_pkt) => ipv4_pkt, + _ => return, + }; + + if ipv4_pkt.src_addr() != self.host.gateway_ip { + return; + } + + if ipv4_pkt.protocol() != smoltcp::wire::IpProtocol::Udp { + return; + } + + let udp_pkt = match UdpPacket::new_checked(ipv4_pkt.payload()) { + Ok(udp_pkt) => udp_pkt, + Err(_) => return, + }; + + if !udp_pkt.is_dhcp_response() { + return; + } + + self.dhcp_snooper.register_dhcp_reply(udp_pkt.payload()); + } +} diff --git a/lib/proxy/mod.rs b/lib/proxy/mod.rs new file mode 100644 index 0000000..2f14ac7 --- /dev/null +++ b/lib/proxy/mod.rs @@ -0,0 +1,96 @@ +mod host; +mod udp_packet_helper; +mod vm; + +use crate::dhcp_snooper::DhcpSnooper; +use crate::host::Host; +use crate::poller::Poller; +use crate::vm::VM; +use crate::Error; +use crate::Result; +use mac_address::MacAddress; +use smoltcp::wire::EthernetFrame; +use std::io::ErrorKind; +use std::os::unix::io::{AsRawFd, RawFd}; + +pub struct Proxy { + vm: VM, + host: Host, + poller: Poller, + vm_mac_address: smoltcp::wire::EthernetAddress, + dhcp_snooper: DhcpSnooper, +} + +impl Proxy { + pub fn new(vm_fd: RawFd, vm_mac_address: MacAddress) -> Result { + let vm = VM::new(vm_fd)?; + let host = Host::new()?; + let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd())?; + + Ok(Proxy { + vm, + host, + poller, + vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()), + dhcp_snooper: Default::default(), + }) + } + + pub fn run(&mut self) -> Result<()> { + let mut buf: Vec = vec![0; self.host.max_packet_size as usize]; + + self.poller.arm()?; + + loop { + let (vm_readable, host_readable) = self.poller.wait()?; + + if vm_readable { + self.read_from_vm(buf.as_mut_slice())?; + } + + if host_readable { + self.read_from_host(buf.as_mut_slice())?; + } + + self.poller.rearm()?; + } + } + + fn read_from_vm(&mut self, buf: &mut [u8]) -> Result<()> { + loop { + match self.vm.read(buf) { + Ok(n) => { + if let Ok(frame) = EthernetFrame::new_checked(&buf[..n]) { + self.process_frame_from_vm(frame)?; + } + } + Err(err) => { + if err.kind() == ErrorKind::WouldBlock { + return Ok(()); + } + + return Err(Error::VMIOFailed { source: err }); + } + } + } + } + + fn read_from_host(&mut self, buf: &mut [u8]) -> Result<()> { + loop { + match self.host.read(buf) { + Ok(n) => { + if let Ok(pkt) = EthernetFrame::new_checked(&buf[..n]) { + self.process_frame_from_host(&pkt)?; + } + } + Err(err) => { + if let vmnet::Error::VmnetReadNothing = err { + return Ok(()); + } + + return Err(Error::HostIOFailed { source: err }); + } + } + } + } +} diff --git a/lib/proxy/udp_packet_helper.rs b/lib/proxy/udp_packet_helper.rs new file mode 100644 index 0000000..3afa59d --- /dev/null +++ b/lib/proxy/udp_packet_helper.rs @@ -0,0 +1,26 @@ +use smoltcp::wire::UdpPacket; + +pub(crate) trait UdpPacketHelper { + const DNS_PORT: u16 = 53; + const BOOTPS_PORT: u16 = 67; + const BOOTPC_PORT: u16 = 68; + + fn is_dns_request(&self) -> bool; + + fn is_dhcp_request(&self) -> bool; + fn is_dhcp_response(&self) -> bool; +} + +impl UdpPacketHelper for UdpPacket<&[u8]> { + fn is_dns_request(&self) -> bool { + self.dst_port() == Self::DNS_PORT + } + + fn is_dhcp_request(&self) -> bool { + self.src_port() == Self::BOOTPC_PORT || self.dst_port() == Self::BOOTPS_PORT + } + + fn is_dhcp_response(&self) -> bool { + self.src_port() == Self::BOOTPS_PORT || self.dst_port() == Self::BOOTPC_PORT + } +} diff --git a/lib/proxy/vm.rs b/lib/proxy/vm.rs new file mode 100644 index 0000000..0c5fe89 --- /dev/null +++ b/lib/proxy/vm.rs @@ -0,0 +1,91 @@ +use crate::proxy::udp_packet_helper::UdpPacketHelper; +use crate::proxy::Proxy; +use crate::{Error, Result}; +use smoltcp::wire::{ + ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket, +}; +use std::net::Ipv4Addr; + +impl Proxy { + pub(crate) fn process_frame_from_vm(&mut self, frame: EthernetFrame<&[u8]>) -> Result<()> { + if self.allowed_from_vm(&frame).is_none() { + // Block packet by not forwarding it to the host + return Ok(()); + } + + self.host + .write(frame.as_ref()) + .map(|_| ()) + .map_err(|err| Error::HostIOFailed { source: err }) + } + + fn allowed_from_vm(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> { + if frame.src_addr() != self.vm_mac_address { + return None; + } + + match frame.ethertype() { + EthernetProtocol::Arp => { + let arp_pkt = ArpPacket::new_checked(frame.payload()).ok()?; + self.allowed_from_vm_arp(arp_pkt) + } + EthernetProtocol::Ipv4 => { + let ipv4_pkt = Ipv4Packet::new_checked(frame.payload()).ok()?; + self.allowed_from_vm_ipv4(ipv4_pkt) + } + _ => None, + } + } + + fn allowed_from_vm_arp(&self, arp_pkt: ArpPacket<&[u8]>) -> Option<()> { + if arp_pkt.source_hardware_addr() != self.vm_mac_address.0 { + return None; + } + + let source_protocol_addr: [u8; 4] = arp_pkt.source_protocol_addr().try_into().unwrap(); + let source_protocol_addr = Ipv4Addr::from(source_protocol_addr); + + if let Some(lease) = self.dhcp_snooper.lease() { + if lease.valid_ip_source(source_protocol_addr.into()) { + return Some(()); + } + } else if source_protocol_addr.is_unspecified() { + return Some(()); + } + + None + } + + fn allowed_from_vm_ipv4(&self, ipv4_pkt: Ipv4Packet<&[u8]>) -> Option<()> { + // Once we've learned the VM's IP from the DHCP snooping, + // allow all global traffic for that VM's IP + if let Some(lease) = &self.dhcp_snooper.lease() { + let dst_is_global = + ip_network::IpNetwork::from(Ipv4Addr::from(ipv4_pkt.dst_addr().0)).is_global(); + + if lease.valid_ip_source(ipv4_pkt.src_addr()) && dst_is_global { + return Some(()); + } + } + + if ipv4_pkt.protocol() == IpProtocol::Udp { + let udp_pkt = UdpPacket::new_checked(ipv4_pkt.payload()).ok()?; + + // Allow DNS communication with the DNS-servers provided by DHCP + if udp_pkt.is_dns_request() && self.dhcp_snooper.valid_dns_target(&ipv4_pkt.dst_addr()) + { + return Some(()); + } + + // Allow DHCP communication with the bootpd(8) on host + let allowed_dhcp_target = + ipv4_pkt.dst_addr().is_broadcast() || ipv4_pkt.dst_addr() == self.host.gateway_ip; + + if udp_pkt.is_dhcp_request() && allowed_dhcp_target { + return Some(()); + } + } + + None + } +} diff --git a/lib/vm.rs b/lib/vm.rs new file mode 100644 index 0000000..9806afa --- /dev/null +++ b/lib/vm.rs @@ -0,0 +1,31 @@ +use crate::{Error, Result}; +use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; +use std::os::unix::net::UnixDatagram; + +pub struct VM { + sock: UnixDatagram, +} + +impl VM { + pub fn new(vm_fd: RawFd) -> Result { + let sock = unsafe { UnixDatagram::from_raw_fd(vm_fd) }; + sock.set_nonblocking(true) + .map_err(|err| Error::InitFailed { source: err.into() })?; + + Ok(VM { sock }) + } + + pub fn write(&self, pkt: &[u8]) -> std::io::Result { + self.sock.send(pkt) + } + + pub fn read(&self, buf: &mut [u8]) -> std::io::Result { + self.sock.recv(buf) + } +} + +impl AsRawFd for VM { + fn as_raw_fd(&self) -> RawFd { + self.sock.as_raw_fd() + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7b63dfb --- /dev/null +++ b/src/main.rs @@ -0,0 +1,94 @@ +use clap::Parser; +use libc::geteuid; +use privdrop::PrivDrop; +use softnet::proxy::Proxy; +use std::os::raw::c_int; +use std::os::unix::io::RawFd; +use system_configuration::core_foundation::base::TCFType; +use system_configuration::core_foundation::dictionary::CFDictionary; +use system_configuration::core_foundation::number::CFNumber; +use system_configuration::core_foundation::string::CFString; +use system_configuration::preferences::SCPreferences; +use system_configuration::sys::preferences::{SCPreferencesCommitChanges, SCPreferencesSetValue}; +use users::{get_current_groupname, get_current_username}; + +#[derive(Parser, Debug)] +struct Args { + #[clap( + long, + help = "FD number to use for communicating with the VM's networking stack" + )] + vm_fd: c_int, + + #[clap(long, help = "MAC address to enforce for the VM")] + vm_mac_address: mac_address::MacAddress, + + #[clap( + long, + help = "set bootpd(8) lease time to this value (in seconds) before starting the VM", + default_value_t = 60 + )] + bootpd_lease_time: u32, +} + +fn main() { + if let Err(err) = try_main() { + match err.source() { + Some(source) => eprintln!("{}: {}", err, source), + None => eprintln!("{}", err), + } + std::process::exit(1); + } +} + +fn try_main() -> Result<(), Box> { + let args: Args = Args::parse(); + + // Ensure we are running as root + if unsafe { geteuid() } != 0 { + return Err("root privileges are required to run".into()); + } + + // Set bootpd(8) min/max lease time while still having the root privileges + set_bootpd_lease_time(args.bootpd_lease_time); + + // Initialize the proxy while still having the root privileges + let mut proxy = Proxy::new(args.vm_fd as RawFd, args.vm_mac_address)?; + + // Retrieve real (not effective) user and group names + let user = get_current_username().ok_or("failed to resolve real user name")?; + let group = get_current_groupname().ok_or("failed to resolve real group name")?; + + // Drop effective privileges to the user + // and group which have had invoked us + PrivDrop::default() + .user(user) + .group(group) + .apply() + .map_err(|err| format!("failed to drop privileges: {}", err))?; + + // Run proxy + proxy.run().map_err(|err| err.into()) +} + +fn set_bootpd_lease_time(lease_time: u32) { + let prefs = SCPreferences::group( + &CFString::new("softnet"), + &CFString::new("com.apple.InternetSharing.default.plist"), + ); + + let bootpd_dict = CFDictionary::from_CFType_pairs(&[( + CFString::new("DHCPLeaseTimeSecs"), + CFNumber::from(lease_time as i32), + )]); + + unsafe { + SCPreferencesSetValue( + prefs.as_concrete_TypeRef(), + CFString::new("bootpd").as_concrete_TypeRef(), + bootpd_dict.as_concrete_TypeRef().cast(), + ); + + SCPreferencesCommitChanges(prefs.as_concrete_TypeRef()); + } +}