Initial revision of Softnet, a software networking for Tart

This commit is contained in:
Nikolay Edigaryev 2022-06-10 16:51:29 +03:00
commit 226a7e6c86
14 changed files with 1462 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

688
Cargo.lock generated Normal file
View File

@ -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"

23
Cargo.toml Normal file
View File

@ -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"

33
README.md Normal file
View File

@ -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`.

79
lib/dhcp_snooper.rs Normal file
View File

@ -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<Lease>,
}
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<Lease> {
&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<Ipv4Address>,
}
impl Lease {
fn new(address: Ipv4Address, lease_time: Duration, dns_ips: HashSet<Ipv4Address>) -> 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
}
}

134
lib/host.rs Normal file
View File

@ -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<Host> {
// 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<usize> {
// 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<usize> {
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()
}
}

30
lib/mod.rs Normal file
View File

@ -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<dyn std::error::Error> },
#[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<T> = std::result::Result<T, Error>;

76
lib/poller.rs Normal file
View File

@ -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<polling::Event>,
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<Poller> {
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())
}
}

60
lib/proxy/host.rs Normal file
View File

@ -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());
}
}

96
lib/proxy/mod.rs Normal file
View File

@ -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<Proxy> {
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<u8> = 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 });
}
}
}
}
}

View File

@ -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
}
}

91
lib/proxy/vm.rs Normal file
View File

@ -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
}
}

31
lib/vm.rs Normal file
View File

@ -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<VM> {
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<usize> {
self.sock.send(pkt)
}
pub fn read(&self, buf: &mut [u8]) -> std::io::Result<usize> {
self.sock.recv(buf)
}
}
impl AsRawFd for VM {
fn as_raw_fd(&self) -> RawFd {
self.sock.as_raw_fd()
}
}

94
src/main.rs Normal file
View File

@ -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<dyn std::error::Error>> {
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());
}
}