Compare commits

...
7 Commits
Author SHA1 Message Date
dependabot[bot] b869e33897 Bump the all-updates group across 1 directory with 7 updates (#208)
Bumps the all-updates group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [smoltcp](https://github.com/smoltcp-rs/smoltcp) | `0.13.1` | `0.14.0` |
| [dhcproto](https://github.com/bluecatengineering/dhcproto) | ``60719e5`` | ``fef7634`` |
| [clap](https://github.com/clap-rs/clap) | `4.6.6` | `4.6.7` |
| [sentry](https://github.com/getsentry/sentry-rust) | `0.49.1` | `0.49.2` |
| [sentry-anyhow](https://github.com/getsentry/sentry-rust) | `0.49.1` | `0.49.2` |
| [ipnet](https://github.com/krisprice/ipnet) | `2.12.1` | `2.12.2` |
| [log](https://github.com/rust-lang/log) | `0.4.33` | `0.4.34` |



Updates `smoltcp` from 0.13.1 to 0.14.0
- [Release notes](https://github.com/smoltcp-rs/smoltcp/releases)
- [Changelog](https://github.com/smoltcp-rs/smoltcp/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smoltcp-rs/smoltcp/compare/v0.13.1...v0.14.0)

Updates `dhcproto` from `60719e5` to `fef7634`
- [Release notes](https://github.com/bluecatengineering/dhcproto/releases)
- [Commits](https://github.com/bluecatengineering/dhcproto/compare/60719e5df11359b12bf74e743b3c7e0831351c2d...fef76341d22e6fec31bd5b507ee69571b63bc843)

Updates `clap` from 4.6.6 to 4.6.7
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/main/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.6...clap_complete-v4.6.7)

Updates `sentry` from 0.49.1 to 0.49.2
- [Release notes](https://github.com/getsentry/sentry-rust/releases)
- [Changelog](https://github.com/getsentry/sentry-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-rust/compare/0.49.1...0.49.2)

Updates `sentry-anyhow` from 0.49.1 to 0.49.2
- [Release notes](https://github.com/getsentry/sentry-rust/releases)
- [Changelog](https://github.com/getsentry/sentry-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-rust/compare/0.49.1...0.49.2)

Updates `ipnet` from 2.12.1 to 2.12.2
- [Release notes](https://github.com/krisprice/ipnet/releases)
- [Changelog](https://github.com/krisprice/ipnet/blob/master/RELEASES.md)
- [Commits](https://github.com/krisprice/ipnet/commits)

Updates `log` from 0.4.33 to 0.4.34
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.33...0.4.34)

---
updated-dependencies:
- dependency-name: smoltcp
  dependency-version: 0.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: dhcproto
  dependency-version: fef76341d22e6fec31bd5b507ee69571b63bc843
  dependency-type: direct:production
  dependency-group: all-updates
- dependency-name: clap
  dependency-version: 4.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.49.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.49.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: ipnet
  dependency-version: 2.12.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: log
  dependency-version: 0.4.34
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 22:51:16 +01:00
edi-oai e6ee4dba02 Serve DHCP directly from Softnet and use predictable vmnet reservations (#209)
* Serve DHCP directly from Softnet and use predictable vmnet reservations

* $ cargo fmt
2026-09-28 22:50:26 +01:00
Yibo Zhuang abc1fb2cd3 Pin GitHub Actions to full commit SHAs (#206) 2026-09-17 07:19:43 -07:00
edi-oai 3656e8b55e Service port forwarding and is_connected() periodically during traffic (#198) 2026-08-18 22:18:34 +01:00
edi-oai e5fd48cf03 Reject foreign DHCP replies before policy fallback (#196) 2026-08-17 22:58:54 +01:00
dependabot[bot] d805100161 Bump the all-updates group with 2 updates (#195)
Bumps the all-updates group with 2 updates: [clap](https://github.com/clap-rs/clap) and [system-configuration](https://github.com/mullvad/system-configuration-rs).


Updates `clap` from 4.6.5 to 4.6.6
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.5...clap_complete-v4.6.6)

Updates `system-configuration` from 0.7.0 to 0.8.0
- [Changelog](https://github.com/mullvad/system-configuration-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mullvad/system-configuration-rs/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: system-configuration
  dependency-version: 0.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:00:12 +01:00
edi-oai 0528ec2002 Support --version (#194) 2026-08-17 13:59:48 +01:00
20 changed files with 850 additions and 973 deletions
+3
View File
@@ -1,2 +1,5 @@
[env]
MACOSX_DEPLOYMENT_TARGET = "26.0"
[target.aarch64-apple-darwin]
runner = 'sudo -E'
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: macos-26
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
@@ -34,7 +34,7 @@ jobs:
runs-on: macos-26
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
+5 -5
View File
@@ -17,7 +17,7 @@ jobs:
environment: publish
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 0
persist-credentials: false
@@ -45,7 +45,7 @@ jobs:
permission-contents: write
permission-pull-requests: write
- name: Release
uses: goreleaser/goreleaser-action@v7
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
distribution: goreleaser-pro
version: "~> v2"
@@ -61,7 +61,7 @@ jobs:
runs-on: macos-26
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 0
persist-credentials: false
@@ -72,13 +72,13 @@ jobs:
- name: Install release targets
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
- name: Release dry run
uses: goreleaser/goreleaser-action@v7
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
distribution: goreleaser-pro
version: "~> v2"
args: release --skip=publish --snapshot --clean
- name: Upload dry-run artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: softnet-snapshot
path: dist/**
+3 -1
View File
@@ -5,6 +5,8 @@ project_name: softnet
builds:
- builder: rust
command: build
env:
- SOFTNET_VERSION={{ .Version }}-{{ .ShortCommit }}
targets:
- aarch64-apple-darwin
- x86_64-apple-darwin
@@ -36,5 +38,5 @@ brews:
skip_upload: auto
custom_block: |
on_macos do
depends_on :macos => :sequoia
depends_on :macos => :tahoe
end
Generated
+157 -196
View File
@@ -8,7 +8,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"bytes",
"futures-core",
"futures-sink",
@@ -30,7 +30,7 @@ dependencies = [
"actix-service",
"actix-utils",
"base64",
"bitflags 2.9.4",
"bitflags 2.13.2",
"bytes",
"bytestring",
"derive_more",
@@ -287,15 +287,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.4"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "block-buffer"
@@ -306,6 +300,15 @@ dependencies = [
"hybrid-array",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@@ -368,9 +371,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.6.5"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf"
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
dependencies = [
"clap_builder",
"clap_derive",
@@ -378,26 +381,26 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.6.5"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078"
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.1",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.4"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 3.0.2",
"syn 3.0.6",
]
[[package]]
@@ -458,6 +461,16 @@ dependencies = [
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -496,9 +509,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.14.4"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
dependencies = [
"darling_core",
"darling_macro",
@@ -506,27 +519,26 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.14.4"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 1.0.109",
"strsim",
"syn 3.0.6",
]
[[package]]
name = "darling_macro"
version = "0.14.4"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
dependencies = [
"darling_core",
"quote",
"syn 1.0.109",
"syn 3.0.6",
]
[[package]]
@@ -560,9 +572,9 @@ dependencies = [
[[package]]
name = "defmt"
version = "0.3.8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99dd22262668b887121d4672af5a64b238f026099f1a2a1b322066c9ecfe9e0"
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
dependencies = [
"bitflags 1.3.2",
"defmt-macros",
@@ -570,12 +582,11 @@ dependencies = [
[[package]]
name = "defmt-macros"
version = "0.3.9"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9f309eff1f79b3ebdf252954d90ae440599c26c2c553fe87a2d17195f2dcb"
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
dependencies = [
"defmt-parser",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -583,11 +594,11 @@ dependencies = [
[[package]]
name = "defmt-parser"
version = "0.3.4"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff4a5fefe330e8d7f31b16a318f9ce81000d8e35e69b93eae154d16d2278f70f"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror 1.0.64",
"thiserror 2.0.19",
]
[[package]]
@@ -633,7 +644,7 @@ dependencies = [
[[package]]
name = "dhcproto"
version = "0.16.0"
source = "git+https://github.com/bluecatengineering/dhcproto.git?branch=master#60719e5df11359b12bf74e743b3c7e0831351c2d"
source = "git+https://github.com/bluecatengineering/dhcproto.git?branch=master#fef76341d22e6fec31bd5b507ee69571b63bc843"
dependencies = [
"dhcproto-macros",
"hickory-proto",
@@ -645,7 +656,7 @@ dependencies = [
[[package]]
name = "dhcproto-macros"
version = "0.2.0"
source = "git+https://github.com/bluecatengineering/dhcproto.git?branch=master#60719e5df11359b12bf74e743b3c7e0831351c2d"
source = "git+https://github.com/bluecatengineering/dhcproto.git?branch=master#fef76341d22e6fec31bd5b507ee69571b63bc843"
dependencies = [
"proc-macro2",
"quote",
@@ -691,18 +702,18 @@ dependencies = [
[[package]]
name = "enum-iterator"
version = "1.5.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fd242f399be1da0a5354aa462d57b4ab2b4ee0683cc552f7c007d2d12d36e94"
checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016"
dependencies = [
"enum-iterator-derive",
]
[[package]]
name = "enum-iterator-derive"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842"
dependencies = [
"proc-macro2",
"quote",
@@ -1293,9 +1304,9 @@ checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1"
[[package]]
name = "ipnet"
version = "2.12.1"
version = "2.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
[[package]]
name = "iri-string"
@@ -1461,9 +1472,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.33"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "mac_address"
@@ -1541,13 +1552,25 @@ dependencies = [
"tempfile",
]
[[package]]
name = "network-interface"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6"
dependencies = [
"cc",
"libc",
"thiserror 2.0.19",
"winapi",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
@@ -1560,7 +1583,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
@@ -1572,7 +1595,7 @@ version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
@@ -1594,49 +1617,49 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_enum"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
dependencies = [
"num_enum_derive 0.5.11",
]
[[package]]
name = "num_enum"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
dependencies = [
"num_enum_derive 0.7.6",
"num_enum_derive",
"rustversion",
]
[[package]]
name = "num_enum_derive"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "num_enum_derive"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
dependencies = [
"proc-macro-crate 3.2.0",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "object"
version = "0.36.4"
@@ -1668,7 +1691,7 @@ version = "0.10.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"cfg-if",
"foreign-types",
"libc",
@@ -1850,47 +1873,13 @@ dependencies = [
"nix 0.30.1",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit 0.19.15",
]
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
dependencies = [
"toml_edit 0.22.22",
]
[[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 1.0.109",
"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",
"toml_edit",
]
[[package]]
@@ -1975,7 +1964,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
]
[[package]]
@@ -2071,7 +2060,7 @@ version = "0.38.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"errno",
"libc",
"linux-raw-sys 0.4.14",
@@ -2084,7 +2073,7 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"errno",
"libc",
"linux-raw-sys 0.9.4",
@@ -2151,8 +2140,8 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.9.4",
"core-foundation",
"bitflags 2.13.2",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -2176,9 +2165,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "sentry"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63207365db50cb817402f0ec8097d6dad3d644ef3fffd6ba30c75b3077e514da"
checksum = "76a88b65feb368d9dde5d531a2b59b25016e36fd6e4978432371a3ee77746ca2"
dependencies = [
"cfg_aliases",
"httpdate",
@@ -2198,9 +2187,9 @@ dependencies = [
[[package]]
name = "sentry-actix"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0eb1ed18478fb48db007aeaa672a3de176055357feb768eee0c3471802d0ce"
checksum = "6b59b6298f8b1c621e7e711050f7ae9620b972215eb2822db2bfc4b061ed0cd2"
dependencies = [
"actix-http",
"actix-web",
@@ -2211,9 +2200,9 @@ dependencies = [
[[package]]
name = "sentry-anyhow"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6265521f1b724f709bc07747ecb01c5289b974cb70b348026df01780280fc136"
checksum = "34e4c4b2e5bf7bb63f8223eb4f6d24f64e2c18b4d01fdeb3d561a951e057425b"
dependencies = [
"anyhow",
"sentry-backtrace",
@@ -2222,9 +2211,9 @@ dependencies = [
[[package]]
name = "sentry-backtrace"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bcc2497c2327998146207b7600599ef7592233920f4d4e1d41ddeeba0e4210"
checksum = "2c51511d67ed670266a93afeaa26a08019b04ad1420cb9191da30f2338d22c48"
dependencies = [
"backtrace",
"regex",
@@ -2233,9 +2222,9 @@ dependencies = [
[[package]]
name = "sentry-contexts"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cb04cba225b38f59d08f9e3c29ab7259d9cc94fbb2c46d613a51cb0a4a7ee05"
checksum = "4b757ac1f335856e8d7f5062a1fd9bc07a05a7eb3cc48247db79bf2618a490bd"
dependencies = [
"hostname",
"libc",
@@ -2247,9 +2236,9 @@ dependencies = [
[[package]]
name = "sentry-core"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48759bc392fb5e3b36b4efcb485f51074c0ba8479711cd5c8cf79e86f9f75d41"
checksum = "d889520a375e5b93efb0a66def1630d21d168b0251eb55b286b03fa940ccfc84"
dependencies = [
"rand 0.9.4",
"sentry-types",
@@ -2260,9 +2249,9 @@ dependencies = [
[[package]]
name = "sentry-debug-images"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5bd325059b70b21ca6e42b0f2409821272dddc1cf37923de37269af55389b5"
checksum = "656487fd5f7b7c0105b2e82171982aac64489e0cb679436038febf070f2f76d6"
dependencies = [
"findshlibs",
"sentry-core",
@@ -2270,20 +2259,20 @@ dependencies = [
[[package]]
name = "sentry-log"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9553a2f0f75bc8550137ebc753e8d2161af63ff780ca59983935f8df8f01655"
checksum = "ba6c1bee99be05938885266fe5c95e0e4a2f46722cae205519f26e4ebdddd733"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"log",
"sentry-core",
]
[[package]]
name = "sentry-panic"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a685d22f672b1562ebeff3f2affceb5960595648cc936dae73da3e1d6a55fbd1"
checksum = "5e0fe456a7380e9df875a1ef4df1e468d35b19b9051599845fab9d8f0c71c4ae"
dependencies = [
"sentry-backtrace",
"sentry-core",
@@ -2291,11 +2280,11 @@ dependencies = [
[[package]]
name = "sentry-tracing"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aa7d8e0db4cccddbac01ddabd5344ad03b7c2f954b3ff850cecb1d9cf5d4758"
checksum = "77796d14eedbef22c2fe78e9c69fb09f14c7ad607e624d48dce92eedc17a136e"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"sentry-backtrace",
"sentry-core",
"tracing-core",
@@ -2304,9 +2293,9 @@ dependencies = [
[[package]]
name = "sentry-types"
version = "0.49.1"
version = "0.49.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fab146d15a30ab4897a95fd15d6a038b8d7fbf2661c5f8b13738ce8df7a095b7"
checksum = "fc71f5ca55942d9b2901af95d5df5c5d65c164134c22a83682cac3d0b6c7ef2d"
dependencies = [
"debugid",
"hex",
@@ -2346,7 +2335,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
"syn 3.0.6",
]
[[package]]
@@ -2396,7 +2385,7 @@ checksum = "a22144e767da4ddd8416dbf383700542ffd8a5dc493dfecedfe1fe3ad03c98ae"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
"syn 3.0.6",
]
[[package]]
@@ -2458,9 +2447,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smoltcp"
version = "0.13.1"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f73d40463bba65efc9adc6370b56df76d563cc46e2482bba58351b4afb7535e"
checksum = "b6f8b28ad56c6e35524a37dd492af5d1a47e31e1a4d175cd12f89c075f01980f"
dependencies = [
"bitflags 1.3.2",
"byteorder",
@@ -2506,12 +2495,14 @@ dependencies = [
"libc",
"log",
"mac_address",
"network-interface",
"nix 0.31.3",
"num_enum 0.7.6",
"num_enum",
"oslog",
"polling",
"prefix-trie",
"privdrop",
"rand 0.10.1",
"sentry",
"sentry-anyhow",
"serde",
@@ -2529,12 +2520,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
@@ -2548,7 +2533,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
@@ -2565,9 +2549,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.2"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
@@ -2596,12 +2580,12 @@ dependencies = [
[[package]]
name = "system-configuration"
version = "0.7.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
checksum = "501336eb7ba9e417300a6a0fa985721065467aa83a6dcf0422a8e43e4c0328fa"
dependencies = [
"bitflags 2.9.4",
"core-foundation",
"bitflags 2.13.2",
"core-foundation 0.10.1",
"system-configuration-sys",
]
@@ -2665,7 +2649,7 @@ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.2",
"syn 3.0.6",
]
[[package]]
@@ -2770,17 +2754,6 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"toml_datetime",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
@@ -2789,7 +2762,7 @@ checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"toml_datetime",
"winnow 0.6.20",
"winnow",
]
[[package]]
@@ -2813,7 +2786,7 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"bytes",
"futures-util",
"http 1.1.0",
@@ -3010,40 +2983,37 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vmnet"
version = "0.5.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03f0531c358eba83803f9a46c165b8d1e9747758bf45c41df1499c1017d43f8c"
checksum = "9b7a1fba175b6ff0e54e30ead88d7ae7a4c6939bb81a2228e8af7e0897d38061"
dependencies = [
"bitflags 1.3.2",
"block",
"bitflags 2.13.2",
"block2",
"enum-iterator",
"hexdump",
"lazy_static",
"libc",
"num_enum 0.5.11",
"thiserror 1.0.64",
"num_enum",
"objc2",
"objc2-core-foundation",
"serial_test",
"thiserror 2.0.19",
"uuid",
"vmnet-derive",
]
[[package]]
name = "vmnet-derive"
version = "0.5.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8cbf3c6928db44bb3757ac38ed65d25460205be9f6e50f61840c1d350c0e9c"
checksum = "805fcba7fb9fa3dbf5ff2680916f9797321eed33f91cd618f93a98f84b3144bd"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 3.0.6",
]
[[package]]
@@ -3192,7 +3162,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
"hashbrown 0.15.2",
"indexmap",
"semver",
@@ -3447,15 +3417,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.6.20"
@@ -3497,7 +3458,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.4",
"bitflags 2.13.2",
]
[[package]]
@@ -3538,7 +3499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.9.4",
"bitflags 2.13.2",
"indexmap",
"log",
"serde",
+3 -1
View File
@@ -16,7 +16,7 @@ smoltcp = "0"
libc = "0"
polling = "3"
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
vmnet = "0.5.1"
vmnet = "0.7.0"
clap = { version = "4", features = ["derive"] }
mac_address = "1"
privdrop = "0"
@@ -28,8 +28,10 @@ num_enum = "0"
sentry = { version = "0", features = ["debug-images"] }
sentry-anyhow = { version = "0", features = ["backtrace"] }
nix = { version = "0", features = ["signal", "socket"] }
network-interface = "2"
prefix-trie = "0"
ipnet = "2"
rand = "0.10"
oslog = "0.2.0"
log = "0.4.29"
serial_test = "4"
+3 -6
View File
@@ -23,12 +23,7 @@ Softnet solves two problems:
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.
Each VM gets a separate [`vmnet` network](https://developer.apple.com/documentation/vmnet/vmnet_network_create(_:_:)) with a private `/30` subnet selected to avoid overlap with existing host interface subnets. Softnet reserves an IP-address for the VM's MAC-address, delivers it through its own DHCP-server, and uses the reserved address for packet filtering.
### Stateful flow authorization
@@ -59,6 +54,8 @@ For ICMP, stateful flow authorization supports only echo requests and replies.
## Installing
Softnet requires macOS 26 or newer; building also requires the macOS 26 SDK or newer.
For proper functioning, Softnet binary requires two things:
* a [SUID-bit](https://en.wikipedia.org/wiki/Setuid#SUID) to be set on the binary or a [passwordless sudo](https://serverfault.com/questions/160581/how-to-setup-passwordless-sudo-on-linux) to be configured, which effectively gives the binary `root` privileges
+389
View File
@@ -0,0 +1,389 @@
use anyhow::Result;
use dhcproto::v4::{
CLIENT_PORT, DhcpOption, Flags, HType, Message, MessageType, Opcode, OptionCode, SERVER_PORT,
};
use dhcproto::{Decodable, Encodable};
use smoltcp::phy::ChecksumCapabilities;
use smoltcp::wire::{
ETHERNET_HEADER_LEN, EthernetAddress, EthernetFrame, EthernetProtocol, IpAddress, IpProtocol,
Ipv4Address, Ipv4Packet, Ipv4Repr, UDP_HEADER_LEN, UdpPacket, UdpRepr,
};
pub(crate) const DHCP_SERVER_MAC: EthernetAddress = EthernetAddress([0x02, 0, 0, 0, 0, 1]);
pub(crate) struct DhcpServer {
vm_mac: EthernetAddress,
vm_ip: Ipv4Address,
subnet_mask: Ipv4Address,
gateway_ip: Ipv4Address,
}
impl DhcpServer {
/// Creates a DHCP server for the VM's reserved IPv4 configuration.
pub fn new(
vm_mac: EthernetAddress,
vm_ip: Ipv4Address,
subnet_mask: Ipv4Address,
gateway_ip: Ipv4Address,
) -> Self {
Self {
vm_mac,
vm_ip,
subnet_mask,
gateway_ip,
}
}
/// Returns a reply for a request sent to the DHCP server port.
pub fn receive_vm(
&self,
ip: &Ipv4Packet<&[u8]>,
udp: &UdpPacket<&[u8]>,
) -> Result<Option<Vec<u8>>> {
// Accept only intact DHCP client datagrams with valid checksums
if udp.src_port() != CLIENT_PORT
|| !ip.verify_checksum()
|| ip.more_frags()
|| ip.frag_offset() != 0
|| UdpRepr::parse(
udp,
&IpAddress::Ipv4(ip.src_addr()),
&IpAddress::Ipv4(ip.dst_addr()),
&ChecksumCapabilities::default(),
)
.is_err()
{
return Ok(None);
}
// Only accept broadcasts or requests addressed to our gateway
if !(ip.dst_addr().is_broadcast() || ip.dst_addr() == self.gateway_ip) {
return Ok(None);
}
// Decode the DHCP request
let Ok(request) = Message::from_bytes(udp.payload()) else {
return Ok(None);
};
// Allow REQUESTs from an obsolete address so the VM can receive a NAK
if request.opts().msg_type() != Some(MessageType::Request)
&& !ip.src_addr().is_unspecified()
&& self.vm_ip != ip.src_addr()
{
return Ok(None);
}
// Build a reply if the request is supported
if let Some(reply) = self.receive(&request) {
// Broadcast NAKs and requested broadcasts; otherwise unicast to the VM
let (destination_mac, destination_ip) =
if reply.opts().msg_type() == Some(MessageType::Nak) || request.flags().broadcast()
{
(EthernetAddress::BROADCAST, Ipv4Address::BROADCAST)
} else if !request.ciaddr().is_unspecified() {
(self.vm_mac, request.ciaddr())
} else {
(self.vm_mac, reply.yiaddr())
};
// Encode the reply for delivery to the VM
let packet = encode_packet(
&reply,
DHCP_SERVER_MAC,
destination_mac,
self.gateway_ip,
destination_ip,
SERVER_PORT,
CLIENT_PORT,
)?;
return Ok(Some(packet));
}
Ok(None)
}
/// Handles a DHCP request from the attached VM.
fn receive(&self, request: &Message) -> Option<Message> {
// Only accept direct Ethernet requests from the attached VM
if request.opcode() != Opcode::BootRequest
|| request.htype() != HType::Eth
|| request.hlen() != self.vm_mac.0.len() as u8
|| request.chaddr() != self.vm_mac.0
|| !request.giaddr().is_unspecified()
{
return None;
}
// Ignore requests that select another DHCP server
let server_id = match request.opts().get(OptionCode::ServerIdentifier) {
Some(DhcpOption::ServerIdentifier(address)) => Some(*address),
_ => None,
};
if server_id.is_some_and(|address| address != self.gateway_ip) {
return None;
}
// Read the requested address, if supplied
let requested_ip = match request.opts().get(OptionCode::RequestedIpAddress) {
Some(DhcpOption::RequestedIpAddress(address)) => Some(*address),
_ => None,
};
// Offer the reserved address or validate a request for it
match request.opts().msg_type()? {
MessageType::Discover if request.ciaddr().is_unspecified() && server_id.is_none() => {
Some(self.address_reply(request, MessageType::Offer))
}
MessageType::Request => {
// Use option 50 for SELECTING/INIT-REBOOT and ciaddr for RENEWING/REBINDING
let address = match (server_id, requested_ip, request.ciaddr().is_unspecified()) {
(_, Some(address), true) => address,
(None, None, false) => request.ciaddr(),
_ => return None,
};
// Reject requests for any address other than the VM's reservation
if address != self.vm_ip {
return Some(self.reply(request, MessageType::Nak));
}
// Acknowledge the reserved address
Some(self.address_reply(request, MessageType::Ack))
}
_ => None,
}
}
/// Builds an OFFER or ACK with the VM's reserved address and network configuration.
fn address_reply(&self, request: &Message, message_type: MessageType) -> Message {
// Include the reserved address and network configuration with an infinite lease
let mut reply = self.reply(request, message_type);
reply.set_yiaddr(self.vm_ip);
let options = reply.opts_mut();
options.insert(DhcpOption::SubnetMask(self.subnet_mask));
options.insert(DhcpOption::Router(vec![self.gateway_ip]));
options.insert(DhcpOption::DomainNameServer(vec![self.gateway_ip]));
options.insert(DhcpOption::AddressLeaseTime(u32::MAX));
reply
}
/// Builds a DHCP reply with the request's transaction and client identifiers.
fn reply(&self, request: &Message, message_type: MessageType) -> Message {
// Create a reply for the same client and transaction
let nak = message_type == MessageType::Nak;
let mut reply = Message::default();
reply
.set_xid(request.xid())
.set_chaddr(request.chaddr())
.set_opcode(Opcode::BootReply);
// Broadcast NAKs; otherwise retain the client's address and flags
if nak {
reply.set_flags(Flags::default().set_broadcast());
} else {
reply
.set_ciaddr(request.ciaddr())
.set_flags(request.flags());
}
// Identify the reply type and this DHCP server
let options = reply.opts_mut();
options.insert(DhcpOption::MessageType(message_type));
options.insert(DhcpOption::ServerIdentifier(self.gateway_ip));
// RFC 6842 requires echoing the client's identifier in every reply
if let Some(identifier) = request.opts().get(OptionCode::ClientIdentifier) {
options.insert(identifier.clone());
}
reply
}
}
/// Encodes a DHCP message in an Ethernet frame with IPv4 and UDP headers.
fn encode_packet(
message: &Message,
source_mac: EthernetAddress,
destination_mac: EthernetAddress,
source_ip: Ipv4Address,
destination_ip: Ipv4Address,
source_port: u16,
destination_port: u16,
) -> Result<Vec<u8>> {
// Pad the encoded message to the minimum BOOTP size, including short NAKs
let mut payload = message.to_vec()?;
payload.resize(payload.len().max(dhcproto::v4::MIN_PACKET_SIZE), 0);
// Allocate space for Ethernet, IPv4, UDP, and the DHCP payload
let ip_repr = Ipv4Repr {
src_addr: source_ip,
dst_addr: destination_ip,
next_header: IpProtocol::Udp,
payload_len: UDP_HEADER_LEN + payload.len(),
hop_limit: 64,
};
let mut bytes = vec![0; ETHERNET_HEADER_LEN + ip_repr.buffer_len() + ip_repr.payload_len];
// Write the Ethernet header
let mut ethernet = EthernetFrame::new_unchecked(bytes.as_mut_slice());
ethernet.set_src_addr(source_mac);
ethernet.set_dst_addr(destination_mac);
ethernet.set_ethertype(EthernetProtocol::Ipv4);
// Write the IPv4 header and checksum
let mut ip = Ipv4Packet::new_unchecked(ethernet.payload_mut());
ip_repr.emit(&mut ip, &ChecksumCapabilities::default());
// Write the UDP header, DHCP payload, and checksum
let mut udp = UdpPacket::new_unchecked(ip.payload_mut());
UdpRepr {
src_port: source_port,
dst_port: destination_port,
}
.emit(
&mut udp,
&IpAddress::Ipv4(source_ip),
&IpAddress::Ipv4(destination_ip),
payload.len(),
|buffer| buffer.copy_from_slice(&payload),
&ChecksumCapabilities::default(),
);
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::{DHCP_SERVER_MAC, DhcpServer, encode_packet};
use dhcproto::Decodable;
use dhcproto::v4::{
CLIENT_PORT, DhcpOption, Message, MessageType, Opcode, OptionCode, SERVER_PORT,
};
use smoltcp::phy::ChecksumCapabilities;
use smoltcp::wire::{
EthernetAddress, EthernetFrame, Ipv4Address, Ipv4Packet, Ipv4Repr, UdpPacket, UdpRepr,
};
const VM: EthernetAddress = EthernetAddress([2, 0, 0, 0, 0, 2]);
const ADDRESS: Ipv4Address = Ipv4Address::new(192, 168, 1, 2);
const GATEWAY: Ipv4Address = Ipv4Address::new(192, 168, 1, 1);
const MASK: Ipv4Address = Ipv4Address::new(255, 255, 255, 252);
const CLIENT_ID: &[u8] = &[1, 2, 0, 0, 0, 0, 2];
#[test]
fn assigns_reserved_address() {
// Create a server and a client request
let dhcp = DhcpServer::new(VM, ADDRESS, MASK, GATEWAY);
let mut message = request(MessageType::Discover);
// Discover the reserved address
let (offer, destination) = exchange(&dhcp, &message, Ipv4Address::BROADCAST);
assert_eq!(offer.opts().msg_type(), Some(MessageType::Offer));
assert_eq!(offer.yiaddr(), ADDRESS);
assert_eq!(destination, ADDRESS);
// Request the offered address
message
.opts_mut()
.insert(DhcpOption::MessageType(MessageType::Request));
message
.opts_mut()
.insert(DhcpOption::RequestedIpAddress(offer.yiaddr()));
message
.opts_mut()
.insert(DhcpOption::ServerIdentifier(GATEWAY));
let (ack, destination) = exchange(&dhcp, &message, Ipv4Address::BROADCAST);
// Check the address and configuration in the acknowledgement
assert_eq!(ack.opcode(), Opcode::BootReply);
assert_eq!(ack.opts().msg_type(), Some(MessageType::Ack));
assert_eq!(ack.xid(), message.xid());
assert_eq!(ack.chaddr(), VM.0);
assert_eq!(ack.yiaddr(), ADDRESS);
assert_eq!(destination, ADDRESS);
for option in [
DhcpOption::ServerIdentifier(GATEWAY),
DhcpOption::SubnetMask(MASK),
DhcpOption::Router(vec![GATEWAY]),
DhcpOption::DomainNameServer(vec![GATEWAY]),
DhcpOption::AddressLeaseTime(u32::MAX),
DhcpOption::ClientIdentifier(CLIENT_ID.to_vec()),
] {
assert_eq!(ack.opts().get(OptionCode::from(&option)), Some(&option));
}
}
#[test]
fn renews_reserved_address_and_rejects_stale_address() {
// Renew the reserved address
let dhcp = DhcpServer::new(VM, ADDRESS, MASK, GATEWAY);
let mut message = request(MessageType::Request);
message.set_ciaddr(ADDRESS);
let (ack, destination) = exchange(&dhcp, &message, GATEWAY);
assert_eq!(ack.opts().msg_type(), Some(MessageType::Ack));
assert_eq!(ack.yiaddr(), ADDRESS);
assert_eq!(destination, ADDRESS);
// Broadcast a NAK when the client renews an old address
message.set_ciaddr(Ipv4Address::new(192, 168, 2, 2));
let (nak, destination) = exchange(&dhcp, &message, GATEWAY);
assert_eq!(nak.opts().msg_type(), Some(MessageType::Nak));
assert!(nak.yiaddr().is_unspecified());
assert_eq!(destination, Ipv4Address::BROADCAST);
}
fn request(kind: MessageType) -> Message {
let mut message = Message::default();
message.set_xid(42).set_chaddr(&VM.0);
message.opts_mut().insert(DhcpOption::MessageType(kind));
message
.opts_mut()
.insert(DhcpOption::ClientIdentifier(CLIENT_ID.to_vec()));
message
}
fn exchange(
dhcp: &DhcpServer,
request: &Message,
destination: Ipv4Address,
) -> (Message, Ipv4Address) {
// Encode the client request
let frame = encode_packet(
request,
VM,
EthernetAddress::BROADCAST,
request.ciaddr(),
destination,
CLIENT_PORT,
SERVER_PORT,
)
.unwrap();
// Pass the packet to the DHCP server
let ethernet = EthernetFrame::new_checked(frame.as_slice()).unwrap();
let ip = Ipv4Packet::new_checked(ethernet.payload()).unwrap();
let udp = UdpPacket::new_unchecked(ip.payload());
let frame = dhcp
.receive_vm(&ip, &udp)
.unwrap()
.expect("expected a DHCP reply");
// Decode and validate the reply
let ethernet = EthernetFrame::new_checked(frame.as_slice()).unwrap();
let ip = Ipv4Packet::new_unchecked(ethernet.payload());
Ipv4Repr::parse(&ip, &ChecksumCapabilities::default()).unwrap();
let udp = UdpPacket::new_unchecked(ip.payload());
UdpRepr::parse(
&udp,
&ip.src_addr().into(),
&ip.dst_addr().into(),
&ChecksumCapabilities::default(),
)
.unwrap();
assert_eq!(ethernet.src_addr(), DHCP_SERVER_MAC);
assert_eq!(ip.src_addr(), GATEWAY);
(Message::from_bytes(udp.payload()).unwrap(), ip.dst_addr())
}
}
-182
View File
@@ -1,182 +0,0 @@
use dhcproto::Decodable;
use dhcproto::v4::{DhcpOption, HType, Message, MessageType, Opcode, OptionCode};
use smoltcp::wire::Ipv4Address;
use std::collections::HashSet;
use std::time::Duration;
#[derive(Default)]
pub struct DhcpSnooper {
vm_mac_address: [u8; 6],
vm_lease: Option<Lease>,
uncertainty_duration: Duration,
}
impl DhcpSnooper {
pub fn new(uncertainty_duration: Duration, vm_mac_address: [u8; 6]) -> Self {
DhcpSnooper {
vm_mac_address,
uncertainty_duration,
..Default::default()
}
}
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,
};
// Decoded DHCP replies may be broadcast[1], so additionally validate the BOOTP client
// hardware address to avoid acting on another VM's lease transition
//
// [1]: https://datatracker.ietf.org/doc/html/rfc2131#section-4.1
if !message_matches_bootp_client(&message, Opcode::BootReply, self.vm_mac_address) {
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().cloned())
}
_ => HashSet::new(),
};
let mut lease_duration = Duration::from_secs(*lease_time as u64);
// Adjust for uncertainty caused by using a coarse clock
lease_duration = lease_duration.saturating_sub(self.uncertainty_duration);
self.vm_lease = Some(Lease::new(message.yiaddr(), lease_duration, dns_ips))
}
Some(MessageType::Nak) => {
self.vm_lease = None;
}
_ => {}
};
}
#[cfg(test)]
pub(crate) fn set_lease(&mut self, vm_lease: Option<Lease>) {
self.vm_lease = vm_lease
}
pub fn lease(&self) -> &Option<Lease> {
&self.vm_lease
}
pub(crate) fn address_and_dns_ips(&self) -> Option<(Ipv4Address, HashSet<Ipv4Address>)> {
let lease = self.vm_lease.as_ref().filter(|lease| lease.valid())?;
Some((lease.address(), lease.dns_ips.clone()))
}
pub fn valid_dns_target(&self, addr: &Ipv4Address) -> bool {
if let Some(lease) = &self.vm_lease {
return lease.dns_ips.contains(addr);
}
false
}
}
#[derive(Debug)]
pub struct Lease {
address: Ipv4Address,
valid_until: coarsetime::Instant,
dns_ips: HashSet<Ipv4Address>,
}
impl Lease {
pub fn new(address: Ipv4Address, lease_time: Duration, dns_ips: HashSet<Ipv4Address>) -> Lease {
Lease {
address,
valid_until: coarsetime::Instant::recent() + lease_time.into(),
dns_ips,
}
}
pub fn address(&self) -> Ipv4Address {
self.address
}
pub fn valid(&self) -> bool {
coarsetime::Instant::recent() < self.valid_until
}
pub fn is_valid_for(&self, address: Ipv4Address) -> bool {
self.address == address && self.valid()
}
}
pub(crate) fn message_matches_bootp_client(
message: &Message,
opcode: Opcode,
mac: [u8; 6],
) -> bool {
message.opcode() == opcode
&& message.htype() == HType::Eth
&& message.hlen() == mac.len() as u8
&& message.chaddr() == mac
}
#[cfg(test)]
mod tests {
use super::{DhcpSnooper, Lease};
use dhcproto::v4::{DhcpOption, Message, MessageType, Opcode};
use dhcproto::{Encodable, Encoder};
use smoltcp::wire::Ipv4Address;
use std::collections::HashSet;
use std::time::Duration;
const VM_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
const OTHER_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02];
const OLD_ADDRESS: Ipv4Address = Ipv4Address::new(192, 168, 64, 2);
#[test]
fn processes_replies_only_for_matching_client() {
// Start with an active lease
let mut snooper = DhcpSnooper::new(Duration::ZERO, VM_MAC);
snooper.set_lease(Some(Lease::new(
OLD_ADDRESS,
Duration::from_secs(600),
HashSet::new(),
)));
// Ignore a NAK for another client
let mut message = Message::new(
Ipv4Address::UNSPECIFIED,
Ipv4Address::UNSPECIFIED,
Ipv4Address::UNSPECIFIED,
Ipv4Address::UNSPECIFIED,
&OTHER_MAC,
);
message.set_opcode(Opcode::BootReply);
message
.opts_mut()
.insert(DhcpOption::MessageType(MessageType::Nak));
let mut encoded = Vec::new();
message.encode(&mut Encoder::new(&mut encoded)).unwrap();
snooper.register_dhcp_reply(&encoded);
assert_eq!(snooper.lease().as_ref().unwrap().address(), OLD_ADDRESS);
// Process a NAK for the matching client
message.set_chaddr(&VM_MAC);
encoded.clear();
message.encode(&mut Encoder::new(&mut encoded)).unwrap();
snooper.register_dhcp_reply(&encoded);
assert!(snooper.lease().is_none());
}
}
+44 -30
View File
@@ -1,15 +1,16 @@
use crate::subnet_finder;
use anyhow::{Context, Result, anyhow};
use clap::ValueEnum;
use log::info;
use smoltcp::wire::EthernetAddress;
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::{SyncSender, sync_channel};
use vmnet::mode::Mode;
use vmnet::network::Mode;
use vmnet::parameters::{Parameter, ParameterKind};
use vmnet::port_forwarding::{AddressFamily, Protocol};
use vmnet::{Batch, Events, Options};
use vmnet::{Batch, Events, Network, NetworkConfiguration, Options};
#[derive(ValueEnum, Clone, Debug)]
pub enum NetType {
@@ -27,6 +28,8 @@ pub struct Host {
interface: vmnet::Interface,
new_packets_rx: UnixDatagram,
callback_can_continue_tx: SyncSender<()>,
pub vm_ip: Ipv4Addr,
pub subnet_mask: Ipv4Addr,
pub gateway_ip: smoltcp::wire::Ipv4Address,
pub max_packet_size: u64,
pub read_max_packets: u64,
@@ -34,31 +37,49 @@ pub struct Host {
}
impl Host {
pub fn new(vm_net_type: NetType, enable_isolation: bool) -> Result<Host> {
// Initialize a vmnet.framework NAT or Host interface with isolation enabled
let mut interface = vmnet::Interface::new(
match vm_net_type {
NetType::Nat => Mode::Shared(Default::default()),
NetType::Host => Mode::Host(Default::default()),
},
pub fn new(
vm_net_type: NetType,
vm_mac: EthernetAddress,
enable_isolation: bool,
) -> Result<Host> {
// Find an unused /30 subnet for the gateway and VM
let (gateway_ip, vm_ip, subnet) = subnet_finder::find_available_subnet(30)?;
// Create vmnet's network configuration
let mut configuration = NetworkConfiguration::new(match vm_net_type {
NetType::Nat => Mode::Shared,
NetType::Host => Mode::Host,
})
.context("failed to create vmnet network configuration")?;
// Configure the gateway address and subnet mask
let subnet_mask = subnet.netmask();
configuration
.set_ipv4_subnet(gateway_ip, subnet_mask)
.context("failed to configure IPv4 subnet")?;
// Reserve the VM's address for its MAC
configuration
.add_dhcp_reservation(vm_mac.0, vm_ip)
.context("failed to add DHCP address reservation")?;
// Create vmnet's network
let network = Network::new(&configuration)
.with_context(|| format!("failed to create vmnet network {subnet}"))?;
// Instantiate vmnet's interface
//
// The interface retains the network reservation until it is stopped
let mut interface = vmnet::Interface::with_network(
&network,
Options {
allocate_mac_address: Some(false),
enable_isolation: Some(enable_isolation),
..Default::default()
},
)
.context("failed to initialize vmnet interface")?;
// Retrieve first IP (gateway) used for this interface
let Some(Parameter::StartAddress(gateway_ip)) =
interface.parameters().get(ParameterKind::StartAddress)
else {
return Err(anyhow!(
"failed to retrieve vmnet's interface start address"
));
};
let gateway_ip = Ipv4Addr::from_str(&gateway_ip)
.context("failed to parse vmnet's interface start address")?;
// Retrieve max packet size for this interface
let Some(Parameter::MaxPacketSize(max_packet_size)) =
interface.parameters().get(ParameterKind::MaxPacketSize)
@@ -104,6 +125,8 @@ impl Host {
interface,
new_packets_rx,
callback_can_continue_tx,
vm_ip,
subnet_mask,
gateway_ip,
max_packet_size,
read_max_packets,
@@ -135,15 +158,6 @@ impl Host {
.map_err(|err| anyhow!("failed to add port forwarding rule {details}: {err}"))
}
pub fn port_forwarding_remove_rule(&mut self, external_port: u16) -> Result<()> {
let details = format!("external_port={external_port}");
self.interface
.port_forwarding_rule_remove(AddressFamily::Ipv4, Protocol::Tcp, external_port)
.map(|_| info!("removed port forwarding rule {details}"))
.map_err(|err| anyhow!("failed to remove port forwarding rule {details}: {err}"))
}
pub fn read(&mut self, batch: &mut Batch, bufs: &mut [Vec<u8>]) -> vmnet::Result<usize> {
// Dequeue dummy datagram from the socket (if any)
// to free up buffer space and reduce false-positives
+2 -1
View File
@@ -1,6 +1,7 @@
mod dhcp_snooper;
mod dhcp_server;
mod host;
pub use host::NetType;
mod poller;
pub mod proxy;
mod subnet_finder;
mod vm;
+12
View File
@@ -10,6 +10,7 @@ pub struct Poller<'poller> {
poller: polling::Poller,
events: polling::Events,
timeout: Duration,
last_periodic_tick: coarsetime::Instant,
vm_fd: BorrowedFd<'poller>,
host_fd: BorrowedFd<'poller>,
control_fd: Option<BorrowedFd<'poller>>,
@@ -37,6 +38,7 @@ impl Poller<'_> {
poller,
events: polling::Events::new(),
timeout,
last_periodic_tick: coarsetime::Instant::recent(),
vm_fd: unsafe { BorrowedFd::borrow_raw(vm_fd) },
host_fd: unsafe { BorrowedFd::borrow_raw(host_fd) },
control_fd: control_fd.map(|fd| unsafe { BorrowedFd::borrow_raw(fd) }),
@@ -94,6 +96,16 @@ impl Poller<'_> {
Ok((vm_readable, host_readable, interrupt))
}
pub fn periodic_tick_due(&mut self) -> bool {
let due = self.last_periodic_tick.elapsed_since_recent() >= self.timeout.into();
if due {
self.last_periodic_tick = coarsetime::Instant::recent();
}
due
}
pub fn remove_control(&mut self) -> Result<()> {
if let Some(control_fd) = self.control_fd.take() {
self.poller.delete(control_fd)?;
-4
View File
@@ -152,10 +152,6 @@ impl FlowTable {
true
}
pub(crate) fn clear(&mut self) {
self.flows.clear();
}
fn sweep_if_due(&mut self, now: Instant) {
if now < self.next_sweep {
return;
+18 -129
View File
@@ -1,12 +1,9 @@
use crate::dhcp_snooper::message_matches_bootp_client;
use crate::proxy::flows::{FlowDirection, FlowMatch};
use crate::proxy::udp_packet_helper::UdpPacketHelper;
use crate::proxy::{Direction, PolicyDecision, Proxy};
use anyhow::{Context, Result};
use dhcproto::Decodable;
use dhcproto::v4::Opcode;
use dhcproto::v4::{CLIENT_PORT, SERVER_PORT};
use smoltcp::phy::ChecksumCapabilities;
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, Ipv4Repr, UdpPacket};
use smoltcp::wire::{EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, Ipv4Repr, UdpPacket};
impl Proxy<'_> {
pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> {
@@ -15,13 +12,11 @@ impl Proxy<'_> {
return Ok(());
}
// Snoop bootpd(8) replies from the host to
// figure out the IP assigned to the VM
if frame.dst_addr() == self.vm_mac_address || frame.dst_addr().is_broadcast() {
self.snoop(frame);
}
self.write_to_vm(frame.as_ref())
}
match self.vm.write(frame.as_ref()) {
pub(super) fn write_to_vm(&mut self, packet: &[u8]) -> Result<()> {
match self.vm.write(packet) {
Ok(_) => Ok(()),
Err(err) => {
if let Some(libc::ENOBUFS) = err.raw_os_error() {
@@ -55,24 +50,25 @@ impl Proxy<'_> {
}
pub(super) fn allowed_from_host_ipv4(&mut self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> Option<()> {
// Drop external DHCP replies, including malformed and fragmented replies
if ipv4_pkt.next_header() == IpProtocol::Udp
&& ipv4_pkt.frag_offset() == 0
&& ipv4_pkt.payload().len() >= 4
{
let udp = UdpPacket::new_unchecked(ipv4_pkt.payload());
if udp.src_port() == SERVER_PORT && udp.dst_port() == CLIENT_PORT {
return None;
}
}
// Backwards compatibility with Softnet consumers that only use stateless rules
if self.flows.is_none() {
return Some(());
}
// DHCP is required to maintain the VM's lease and must bypass user-specified rules
if self.is_allowed_dhcp_response(ipv4_pkt) {
return Some(());
}
// Consult the flow table before evaluating inbound policy
// so established flows are not treated as new traffic
let pending = if self
.dhcp_snooper
.lease()
.as_ref()
.is_some_and(|lease| lease.is_valid_for(ipv4_pkt.dst_addr()))
{
let pending = if self.host.vm_ip == ipv4_pkt.dst_addr() {
match self
.flows
.as_mut()?
@@ -108,111 +104,4 @@ impl Proxy<'_> {
}
}
}
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 !self.is_allowed_dhcp_response(&ipv4_pkt) {
return;
}
let udp_pkt = match UdpPacket::new_checked(ipv4_pkt.payload()) {
Ok(udp_pkt) => udp_pkt,
Err(_) => return,
};
let address_and_dns_ips_saved = self.dhcp_snooper.address_and_dns_ips();
self.dhcp_snooper.register_dhcp_reply(udp_pkt.payload());
if address_and_dns_ips_saved != self.dhcp_snooper.address_and_dns_ips()
&& let Some(flows) = &mut self.flows
{
flows.clear();
}
}
fn is_allowed_dhcp_response(&self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> bool {
if ipv4_pkt.src_addr() != self.host.gateway_ip
|| ipv4_pkt.next_header() != smoltcp::wire::IpProtocol::Udp
{
return false;
}
let Ok(udp_pkt) = UdpPacket::new_checked(ipv4_pkt.payload()) else {
return false;
};
// Require the standard DHCP server and client ports
if !udp_pkt.is_dhcp_response() {
return false;
}
// Require the BOOTP client hardware address to match this VM
// (symmetric with is_allowed_dhcp_request / #191 on the VM→host path)
let mut decoder = dhcproto::v4::Decoder::new(udp_pkt.payload());
let Ok(message) = dhcproto::v4::Message::decode(&mut decoder) else {
return false;
};
message_matches_bootp_client(&message, Opcode::BootReply, self.vm_mac_address.0)
}
}
#[cfg(test)]
mod tests {
use crate::dhcp_snooper::message_matches_bootp_client;
use dhcproto::Decodable;
use dhcproto::v4::{DhcpOption, Message, MessageType, Opcode};
use dhcproto::{Encodable, Encoder};
use smoltcp::wire::Ipv4Address;
const VM_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
const OTHER_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02];
#[test]
fn dhcp_boot_reply_chaddr_must_match_vm() {
let own = encode_boot_reply(VM_MAC);
let foreign = encode_boot_reply(OTHER_MAC);
let mut dec = dhcproto::v4::Decoder::new(&own);
let own_msg = Message::decode(&mut dec).unwrap();
let mut dec = dhcproto::v4::Decoder::new(&foreign);
let foreign_msg = Message::decode(&mut dec).unwrap();
assert!(message_matches_bootp_client(
&own_msg,
Opcode::BootReply,
VM_MAC
));
assert!(!message_matches_bootp_client(
&foreign_msg,
Opcode::BootReply,
VM_MAC
));
}
fn encode_boot_reply(chaddr: [u8; 6]) -> Vec<u8> {
let mut message = Message::new(
Ipv4Address::UNSPECIFIED,
Ipv4Address::new(192, 168, 64, 2),
Ipv4Address::UNSPECIFIED,
Ipv4Address::UNSPECIFIED,
&chaddr,
);
message.set_opcode(Opcode::BootReply);
message
.opts_mut()
.insert(DhcpOption::MessageType(MessageType::Ack));
message.opts_mut().insert(DhcpOption::AddressLeaseTime(600));
let mut encoded = Vec::new();
message.encode(&mut Encoder::new(&mut encoded)).unwrap();
encoded
}
}
+55 -52
View File
@@ -2,27 +2,24 @@ mod control;
mod exposed_port;
mod flows;
mod host;
mod port_forwarder;
mod rule;
mod rules;
mod udp_packet_helper;
mod vm;
use crate::dhcp_snooper::DhcpSnooper;
use crate::dhcp_server::{DHCP_SERVER_MAC, DhcpServer};
use crate::host::Host;
use crate::host::NetType;
use crate::poller::Poller;
use crate::vm::VM;
use anyhow::Result;
use anyhow::{Context, Result, bail};
use control::{Control, normalize_rules};
pub use exposed_port::ExposedPort;
use flows::{FlowTable, PendingFlow};
use ipnet::Ipv4Net;
use mac_address::MacAddress;
use port_forwarder::PortForwarder;
pub use rule::{Direction, Rule, Target};
pub(crate) use rules::{PolicyDecision, Rules};
use smoltcp::wire::{EthernetFrame, Ipv4Address};
use smoltcp::wire::{EthernetAddress, EthernetFrame, Ipv4Address};
use std::io::ErrorKind;
use std::os::unix::io::{AsRawFd, RawFd};
use std::time::Duration;
@@ -33,12 +30,11 @@ pub struct Proxy<'proxy> {
host: Host,
poller: Poller<'proxy>,
vm_mac_address: smoltcp::wire::EthernetAddress,
dhcp_snooper: DhcpSnooper,
dhcp_server: DhcpServer,
rules: Rules,
control: Option<Control>,
flows: Option<FlowTable>,
enobufs_encountered: bool,
port_forwarder: PortForwarder,
}
impl Proxy<'_> {
@@ -51,12 +47,19 @@ impl Proxy<'_> {
exposed_ports: Vec<ExposedPort>,
control_fd: Option<RawFd>,
) -> Result<Proxy<'proxy>> {
// Ensure that VM's MAC address won't conflict with our DHCP server's MAC address
let vm_mac_address = EthernetAddress(vm_mac_address.bytes());
if vm_mac_address == DHCP_SERVER_MAC {
bail!("VM MAC address {vm_mac_address} is reserved for the DHCP server");
}
let allow = normalize_rules(allow);
let block = normalize_rules(block);
let vm = VM::new(vm_fd)?;
let host = Host::new(
let mut host = Host::new(
vm_net_type,
vm_mac_address,
!allow.contains(&Rule::Stateless(Target::Prefix(Ipv4Net::default()))),
)?;
let poller_timeout = Duration::from_millis(100);
@@ -75,20 +78,34 @@ impl Proxy<'_> {
let rules = Rules::new(host.gateway_ip, &allow, &block);
// Any stateful rule enables flow inspection for the whole VM, including
// traffic admitted through implicit global, gateway, and DNS fallbacks
// traffic admitted through implicit global and gateway fallbacks
let flows = rules.has_stateful().then(FlowTable::new);
// vmnet owns the gateway (including ARP and DNS); the VM address is
// reserved for this MAC for the lifetime of the attached interface.
let dhcp_server = DhcpServer::new(
vm_mac_address,
host.vm_ip,
host.subnet_mask,
host.gateway_ip,
);
// Install port forwardings for the reserved VM's IP address
for port in exposed_ports {
host.port_forwarding_add_rule(port.external_port, host.vm_ip, port.internal_port)
.context("failed to configure port forwarding")?;
}
Ok(Proxy {
vm,
host,
poller,
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
dhcp_snooper: DhcpSnooper::new(poller_timeout, vm_mac_address.bytes()),
vm_mac_address,
dhcp_server,
rules,
control,
flows,
enobufs_encountered: false,
port_forwarder: PortForwarder::new(exposed_ports),
})
}
@@ -108,12 +125,7 @@ impl Proxy<'_> {
loop {
let (vm_readable, host_readable, interrupt) = self.poller.wait()?;
// kqueue does not report peer disconnects for Unix datagram sockets.
if !self.vm.is_connected()? {
return Ok(());
}
// Update coarse time for DHCP snooping and flows
// Update coarse time for flows
coarsetime::Instant::update();
// Service control on every wake (including timeouts) so a bounded read or a pending
@@ -133,10 +145,12 @@ impl Proxy<'_> {
return Ok(());
}
// Timeout
if !vm_readable && !host_readable && !interrupt {
self.port_forwarder
.tick(&mut self.host, self.dhcp_snooper.lease());
// Periodic maintenance
if self.poller.periodic_tick_due() {
// kqueue(2) does not report peer disconnects for Unix datagram sockets
if !self.vm.is_connected()? {
return Ok(());
}
}
self.poller.rearm();
@@ -149,7 +163,7 @@ impl Proxy<'_> {
loop {
match self.vm.read(buf) {
Ok(n) => {
// Update coarse time for DHCP snooping and flows
// Update coarse time for flows
coarsetime::Instant::update();
if let Ok(frame) = EthernetFrame::new_checked(&buf[..n]) {
@@ -177,7 +191,7 @@ impl Proxy<'_> {
loop {
match self.host.read(batch, bufs) {
Ok(pktcnt) => {
// Update coarse time for DHCP snooping and flows
// Update coarse time for flows
coarsetime::Instant::update();
for buf in batch.packet_sized_bufs(bufs).take(pktcnt) {
@@ -263,22 +277,19 @@ impl Proxy<'_> {
#[cfg(test)]
mod tests {
use crate::NetType;
use crate::dhcp_snooper::Lease;
use crate::proxy::Proxy;
use mac_address::MacAddress;
use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair};
use serial_test::serial;
use smoltcp::wire::{IpProtocol, Ipv4Address, Ipv4Packet, UdpPacket};
use std::collections::HashSet;
use std::os::fd::AsRawFd;
use std::str::FromStr;
use std::time::Duration;
#[test]
#[serial]
fn test_blocking_takes_precedence() {
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
let mut proxy = create_proxy(vm_ip, vec!["66.66.0.0/16"], vec!["66.66.0.0/16"]);
let mut proxy = create_proxy(vec!["66.66.0.0/16"], vec!["66.66.0.0/16"]);
let vm_ip = proxy.host.vm_ip;
assert_eq!(proxy.rules.len(), 1);
@@ -288,8 +299,8 @@ mod tests {
#[test]
#[serial]
fn test_longest_prefix_match_wins() {
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
let mut proxy = create_proxy(vm_ip, vec!["33.33.33.33/32"], vec!["33.33.33.0/24"]);
let mut proxy = create_proxy(vec!["33.33.33.33/32"], vec!["33.33.33.0/24"]);
let vm_ip = proxy.host.vm_ip;
assert_eq!(proxy.rules.len(), 2);
@@ -301,8 +312,8 @@ mod tests {
#[test]
#[serial]
fn test_allow_host() {
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
let mut proxy = create_proxy(vm_ip, vec!["@host"], vec!["0.0.0.0/0"]);
let mut proxy = create_proxy(vec!["@host"], vec!["0.0.0.0/0"]);
let vm_ip = proxy.host.vm_ip;
assert_eq!(proxy.rules.len(), 2);
@@ -317,10 +328,10 @@ mod tests {
#[test]
#[serial]
fn test_bare_default_block_applies_in_both_directions_in_stateful_mode() {
let vm_ip = Ipv4Address::new(192, 168, 0, 2);
let mut proxy = create_proxy(vec!["in 192.0.2.0/24"], vec!["0.0.0.0/0"]);
let vm_ip = proxy.host.vm_ip;
let fallback_peer = Ipv4Address::new(203, 0, 113, 1);
let explicitly_allowed_peer = Ipv4Address::new(192, 0, 2, 1);
let mut proxy = create_proxy(vm_ip, vec!["in 192.0.2.0/24"], vec!["0.0.0.0/0"]);
let fallback_request = udp_packet(fallback_peer, 40_000, vm_ip, 1_234);
let fallback_request = Ipv4Packet::new_checked(fallback_request.as_slice()).unwrap();
@@ -342,9 +353,9 @@ mod tests {
#[test]
#[serial]
fn test_directional_egress_block_does_not_block_reply_to_unmatched_inbound_flow() {
let vm_ip = Ipv4Address::new(192, 168, 0, 2);
let mut proxy = create_proxy(vec![], vec!["out 203.0.113.0/24"]);
let vm_ip = proxy.host.vm_ip;
let peer = Ipv4Address::new(203, 0, 113, 1);
let mut proxy = create_proxy(vm_ip, vec![], vec!["out 203.0.113.0/24"]);
let request = udp_packet(peer, 40_000, vm_ip, 1_234);
let request = Ipv4Packet::new_checked(request.as_slice()).unwrap();
@@ -358,9 +369,9 @@ mod tests {
#[test]
#[serial]
fn test_directional_ingress_block_does_not_block_reply_to_bare_outbound_allow() {
let vm_ip = Ipv4Address::new(192, 168, 0, 2);
let mut proxy = create_proxy(vec!["203.0.113.0/24"], vec!["in 203.0.113.0/24"]);
let vm_ip = proxy.host.vm_ip;
let peer = Ipv4Address::new(203, 0, 113, 1);
let mut proxy = create_proxy(vm_ip, vec!["203.0.113.0/24"], vec!["in 203.0.113.0/24"]);
let request = udp_packet(vm_ip, 1_234, peer, 40_000);
let request = Ipv4Packet::new_checked(request.as_slice()).unwrap();
@@ -371,7 +382,7 @@ mod tests {
assert!(proxy.allowed_from_host_ipv4(&reply).is_some());
}
fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
fn create_proxy<'test>(allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
let (vm_fd, _) = socketpair(
AddressFamily::Unix,
SockType::Datagram,
@@ -381,9 +392,9 @@ mod tests {
.unwrap();
let vm_fd = Box::leak(Box::new(vm_fd));
let mut proxy = Proxy::new(
Proxy::new(
vm_fd.as_raw_fd(),
MacAddress::from_str("02:00:00:00:00:01").unwrap(),
MacAddress::from_str("02:00:00:00:00:02").unwrap(),
NetType::Nat,
allow
.into_iter()
@@ -396,15 +407,7 @@ mod tests {
Vec::default(),
None,
)
.unwrap();
proxy.dhcp_snooper.set_lease(Some(Lease::new(
vm_ip,
Duration::from_secs(600),
HashSet::new(),
)));
proxy
.unwrap()
}
fn allowed_from_vm_ipv4(proxy: &mut Proxy, src: Ipv4Address, dst: &str) -> Option<()> {
-99
View File
@@ -1,99 +0,0 @@
use crate::dhcp_snooper::Lease;
use crate::host::Host;
use crate::proxy::exposed_port::ExposedPort;
use anyhow::Result;
use log::error;
use std::net::Ipv4Addr;
#[derive(Default)]
pub struct PortForwarder {
port_forwardings: Vec<PortForwarding>,
failed: bool,
}
#[derive(Debug, Clone, Copy, Default)]
struct PortForwarding {
exposed_port: ExposedPort,
forwarding_to_addr: Option<Ipv4Addr>,
}
impl PortForwarder {
pub fn new(exposed_ports: Vec<ExposedPort>) -> PortForwarder {
let port_forwardings = exposed_ports
.into_iter()
.map(|exposed_port| PortForwarding {
exposed_port,
..Default::default()
})
.collect();
PortForwarder {
port_forwardings,
..Default::default()
}
}
pub fn tick(&mut self, host: &mut Host, lease: &Option<Lease>) {
if self.failed {
return;
}
if let Err(err) = self.tick_inner(host, lease) {
error!("port-forwarding failed: {}", err);
self.failed = true;
}
}
fn tick_inner(&mut self, host: &mut Host, lease: &Option<Lease>) -> Result<()> {
if let Some(lease) = lease {
// Lease exists, but is not valid, remove all port forwardings
if !lease.valid() {
self.remove_all_port_forwardings(host)?;
return Ok(());
}
// Lease exists and is valid, install/re-install port forwardings
for port_forwarding in &mut self.port_forwardings {
if let Some(installed_addr) = port_forwarding.forwarding_to_addr {
// Port forwarding already installed, perhaps it's outdated?
if installed_addr == lease.address() {
// Nope, the port forwarding is up to date
continue;
}
// Remove port forwarding since the lease address had changed
host.port_forwarding_remove_rule(port_forwarding.exposed_port.external_port)?;
port_forwarding.forwarding_to_addr = None;
}
// Install new port forwarding
host.port_forwarding_add_rule(
port_forwarding.exposed_port.external_port,
lease.address(),
port_forwarding.exposed_port.internal_port,
)?;
port_forwarding.forwarding_to_addr = Some(lease.address());
}
} else {
// Lease does not exist, remove all port forwardings
self.remove_all_port_forwardings(host)?;
}
Ok(())
}
fn remove_all_port_forwardings(&mut self, host: &mut Host) -> Result<()> {
for port_forwarding in &mut self.port_forwardings {
if port_forwarding.forwarding_to_addr.is_none() {
continue;
}
host.port_forwarding_remove_rule(port_forwarding.exposed_port.external_port)?;
port_forwarding.forwarding_to_addr = None;
}
Ok(())
}
}
-66
View File
@@ -1,66 +0,0 @@
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
}
}
#[cfg(test)]
mod tests {
use super::UdpPacketHelper;
use smoltcp::wire::UdpPacket;
#[test]
fn test_is_dhcp_request_requires_both_standard_ports() {
assert!(is_dhcp_request(68, 67));
assert!(!is_dhcp_request(68, 9999));
assert!(!is_dhcp_request(9999, 67));
}
#[test]
fn test_is_dhcp_response_requires_both_standard_ports() {
assert!(is_dhcp_response(67, 68));
assert!(!is_dhcp_response(67, 9999));
assert!(!is_dhcp_response(9999, 68));
}
fn is_dhcp_request(src_port: u16, dst_port: u16) -> bool {
let buffer = udp_packet_buffer(src_port, dst_port);
let udp_pkt = UdpPacket::new_unchecked(&buffer[..]);
udp_pkt.is_dhcp_request()
}
fn is_dhcp_response(src_port: u16, dst_port: u16) -> bool {
let buffer = udp_packet_buffer(src_port, dst_port);
let udp_pkt = UdpPacket::new_unchecked(&buffer[..]);
udp_pkt.is_dhcp_response()
}
fn udp_packet_buffer(src_port: u16, dst_port: u16) -> [u8; 8] {
let mut buffer = [0; 8];
let mut udp_pkt = UdpPacket::new_unchecked(&mut buffer[..]);
udp_pkt.set_src_port(src_port);
udp_pkt.set_dst_port(dst_port);
buffer
}
}
+62 -199
View File
@@ -1,11 +1,8 @@
use crate::dhcp_snooper::{Lease, message_matches_bootp_client};
use crate::proxy::flows::{FlowDirection, FlowMatch};
use crate::proxy::udp_packet_helper::UdpPacketHelper;
use crate::proxy::{Direction, PolicyDecision, Proxy};
use anyhow::Context;
use anyhow::Result;
use dhcproto::Decodable;
use dhcproto::v4::Opcode;
use dhcproto::v4::SERVER_PORT;
use smoltcp::phy::ChecksumCapabilities;
use smoltcp::wire::{
ArpOperation, ArpPacket, ArpRepr, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Address,
@@ -53,25 +50,17 @@ impl Proxy<'_> {
}
fn allowed_from_vm_arp(&self, arp_pkt: ArpPacket<&[u8]>) -> Option<()> {
vm_arp_allowed(arp_pkt, self.vm_mac_address, self.dhcp_snooper.lease())
vm_arp_allowed(arp_pkt, self.vm_mac_address, self.host.vm_ip)
}
pub(crate) fn allowed_from_vm_ipv4(&mut self, ipv4_pkt: Ipv4Packet<&[u8]>) -> Option<()> {
// Is this packet coming from VM's IP address that we've learned from DHCP snooping?
if let Some(lease) = &self.dhcp_snooper.lease()
&& lease.is_valid_for(ipv4_pkt.src_addr())
{
// Unicast DHCP renewal is required to maintain the VM's lease
// and must bypass user-specified rules
if is_allowed_dhcp_request(
&ipv4_pkt,
Some(self.host.gateway_ip),
self.vm_mac_address,
self.dhcp_snooper.lease(),
) {
return Some(());
}
// Consume DHCP before enforcing the reserved source address
if self.consume_dhcp(&ipv4_pkt) {
return None;
}
// Is this packet coming from the VM's reserved IP address?
if self.host.vm_ip == ipv4_pkt.src_addr() {
// Consult the flow table before evaluating outbound policy
// so established flows are not treated as new traffic
let pending = match self
@@ -116,83 +105,50 @@ impl Proxy<'_> {
if dst_addr == self.host.gateway_ip {
return self.admit_with_tracking_if_trackable(pending);
}
// Additionally, allow DNS requests to DNS-servers
// provided to a VM by the host's DHCP server
if ipv4_pkt.next_header() == IpProtocol::Udp {
let udp_pkt = UdpPacket::new_checked(ipv4_pkt.payload()).ok()?;
if udp_pkt.is_dns_request() && self.dhcp_snooper.valid_dns_target(&dst_addr) {
return self.admit_with_tracking_if_trackable(pending);
}
}
}
// Allow outgoing DHCP requests to the bootpd(8) broadcast address,
// otherwise DHCP snooper will never be populated
if is_allowed_dhcp_request(
&ipv4_pkt,
None,
self.vm_mac_address,
self.dhcp_snooper.lease(),
) {
return Some(());
}
None
}
}
fn is_allowed_dhcp_request(
ipv4_pkt: &Ipv4Packet<&[u8]>,
unicast_target: Option<Ipv4Address>,
vm_mac_address: smoltcp::wire::EthernetAddress,
lease: &Option<Lease>,
) -> bool {
// Require the source address to be either:
// * covered by the VM's current lease
// * unspecified on the broadcast DHCP path
let src_addr = ipv4_pkt.src_addr();
let src_has_valid_lease = lease
.as_ref()
.is_some_and(|lease| lease.is_valid_for(src_addr));
if !src_has_valid_lease && !(unicast_target.is_none() && src_addr.is_unspecified()) {
return false;
fn consume_dhcp(&mut self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> bool {
// Only inspect UDP packets that contain the source and destination ports
if ipv4_pkt.next_header() != IpProtocol::Udp
|| ipv4_pkt.frag_offset() != 0
|| ipv4_pkt.payload().len() < 4
{
return false;
}
// Only consume packets addressed to the DHCP server port
let udp = UdpPacket::new_unchecked(ipv4_pkt.payload());
if udp.dst_port() != SERVER_PORT {
return false;
}
// Pass the request to the DHCP server and send any reply
let result = match self.dhcp_server.receive_vm(ipv4_pkt, &udp) {
Ok(Some(reply)) => self.write_to_vm(&reply),
Ok(None) => Ok(()),
Err(err) => Err(err),
};
// Report failures to generate or send the reply
if let Err(err) = result {
sentry::capture_message(
&format!("Failed to reply to DHCP request: {err:#}"),
sentry::Level::Warning,
);
}
// Consume the packet even if the request was rejected or the reply failed
true
}
let dst_addr = ipv4_pkt.dst_addr();
// Keep the common path cheap and inspect UDP only for a permitted DHCP target
if !dst_addr.is_broadcast() && unicast_target != Some(dst_addr) {
return false;
}
if ipv4_pkt.next_header() != IpProtocol::Udp {
return false;
}
let Ok(udp_pkt) = UdpPacket::new_checked(ipv4_pkt.payload()) else {
return false;
};
// Require the standard DHCP client and server ports
if !udp_pkt.is_dhcp_request() {
return false;
}
// Require the BOOTP client hardware address to match this VM
let mut decoder = dhcproto::v4::Decoder::new(udp_pkt.payload());
let Ok(message) = dhcproto::v4::Message::decode(&mut decoder) else {
return false;
};
message_matches_bootp_client(&message, Opcode::BootRequest, vm_mac_address.0)
}
fn vm_arp_allowed(
arp_pkt: ArpPacket<&[u8]>,
vm_mac_address: smoltcp::wire::EthernetAddress,
lease: &Option<Lease>,
address: Ipv4Address,
) -> Option<()> {
let (operation, source_hardware_addr, source_protocol_addr) =
match ArpRepr::parse(&arp_pkt).ok()? {
@@ -213,84 +169,34 @@ fn vm_arp_allowed(
return None;
}
if let Some(lease) = lease {
if lease.is_valid_for(source_protocol_addr) {
return Some(());
}
} else if source_protocol_addr.is_unspecified() {
return Some(());
}
None
(source_protocol_addr == address || source_protocol_addr.is_unspecified()).then_some(())
}
#[cfg(test)]
mod tests {
use crate::dhcp_snooper::Lease;
use dhcproto::v4::{DhcpOption, Message, MessageType};
use dhcproto::{Encodable, Encoder};
use smoltcp::wire::{
ArpHardware, ArpOperation, ArpPacket, EthernetAddress, EthernetProtocol, IpProtocol,
Ipv4Address, Ipv4Packet, UdpPacket,
ArpHardware, ArpOperation, ArpPacket, EthernetAddress, EthernetProtocol, Ipv4Address,
};
use std::collections::HashSet;
use std::time::Duration;
const VM_MAC: EthernetAddress = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
#[test]
fn test_allowed_dhcp_request_policy() {
let gateway = Ipv4Address::new(192, 168, 64, 1);
let lease_ip = Ipv4Address::new(192, 168, 64, 2);
let other = Ipv4Address::new(192, 168, 64, 3);
let no_lease = None;
let lease = Some(Lease::new(
lease_ip,
Duration::from_secs(600),
HashSet::new(),
));
let initial = |src, chaddr| {
allowed_dhcp_request(src, Ipv4Address::BROADCAST, None, chaddr, &no_lease)
};
let renewal = |src, dst| allowed_dhcp_request(src, dst, Some(gateway), VM_MAC.0, &lease);
let other_mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02];
assert!(initial(Ipv4Address::UNSPECIFIED, VM_MAC.0));
assert!(renewal(lease_ip, gateway));
assert!(!renewal(other, gateway));
assert!(!renewal(Ipv4Address::UNSPECIFIED, gateway));
assert!(!renewal(lease_ip, other));
assert!(!initial(Ipv4Address::UNSPECIFIED, other_mac));
}
#[test]
fn test_allowed_from_vm_arp_allows_unspecified_request_without_lease() {
fn test_allowed_from_vm_arp_allows_unspecified_request() {
let vm_mac_address = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
let buf = arp_packet(vm_mac_address.0, [0, 0, 0, 0], ArpOperation::Request, 6, 4);
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_some());
assert!(
super::vm_arp_allowed(arp_pkt, vm_mac_address, Ipv4Address::new(1, 2, 3, 4)).is_some()
);
}
#[test]
fn test_allowed_from_vm_arp_allows_reply_for_leased_ip() {
let vm_mac_address = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
let lease_ip = Ipv4Address::new(192, 168, 0, 2);
let lease = Some(Lease::new(
lease_ip,
Duration::from_secs(600),
HashSet::new(),
));
let buf = arp_packet(
vm_mac_address.0,
lease_ip.octets(),
ArpOperation::Reply,
6,
4,
);
let vm_ip = Ipv4Address::new(192, 168, 0, 2);
let buf = arp_packet(vm_mac_address.0, vm_ip.octets(), ArpOperation::Reply, 6, 4);
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &lease).is_some());
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, vm_ip).is_some());
}
#[test]
@@ -305,7 +211,9 @@ mod tests {
);
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_none());
assert!(
super::vm_arp_allowed(arp_pkt, vm_mac_address, Ipv4Address::new(1, 2, 3, 4)).is_none()
);
}
#[test]
@@ -316,7 +224,9 @@ mod tests {
arp_pkt.set_hardware_type(ArpHardware::Unknown(2));
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_none());
assert!(
super::vm_arp_allowed(arp_pkt, vm_mac_address, Ipv4Address::new(1, 2, 3, 4)).is_none()
);
}
#[test]
@@ -327,7 +237,9 @@ mod tests {
arp_pkt.set_protocol_type(EthernetProtocol::Ipv6);
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_none());
assert!(
super::vm_arp_allowed(arp_pkt, vm_mac_address, Ipv4Address::new(1, 2, 3, 4)).is_none()
);
}
#[test]
@@ -336,7 +248,9 @@ mod tests {
let buf = arp_packet(vm_mac_address.0, [0, 0, 0], ArpOperation::Request, 6, 3);
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_none());
assert!(
super::vm_arp_allowed(arp_pkt, vm_mac_address, Ipv4Address::new(1, 2, 3, 4)).is_none()
);
}
fn arp_packet(
@@ -361,55 +275,4 @@ mod tests {
arp_pkt.set_target_protocol_addr(&vec![0; protocol_len as usize]);
buf
}
fn allowed_dhcp_request(
src_addr: Ipv4Address,
dst_addr: Ipv4Address,
unicast_target: Option<Ipv4Address>,
chaddr: [u8; 6],
lease: &Option<Lease>,
) -> bool {
let mut buf = dhcp_request(chaddr);
let mut ipv4_pkt = Ipv4Packet::new_unchecked(buf.as_mut_slice());
ipv4_pkt.set_src_addr(src_addr);
ipv4_pkt.set_dst_addr(dst_addr);
let ipv4_pkt = Ipv4Packet::new_checked(buf.as_slice()).unwrap();
super::is_allowed_dhcp_request(&ipv4_pkt, unicast_target, VM_MAC, lease)
}
fn dhcp_request(chaddr: [u8; 6]) -> Vec<u8> {
let mut message = Message::new(
Ipv4Address::UNSPECIFIED,
Ipv4Address::UNSPECIFIED,
Ipv4Address::UNSPECIFIED,
Ipv4Address::UNSPECIFIED,
&chaddr,
);
message
.opts_mut()
.insert(DhcpOption::MessageType(MessageType::Discover));
let mut dhcp_payload = Vec::new();
message
.encode(&mut Encoder::new(&mut dhcp_payload))
.unwrap();
let total_len = 20 + 8 + dhcp_payload.len();
let mut buf = vec![0; total_len];
let mut ipv4_pkt = Ipv4Packet::new_unchecked(buf.as_mut_slice());
ipv4_pkt.set_version(4);
ipv4_pkt.set_header_len(20);
ipv4_pkt.set_total_len(total_len as u16);
ipv4_pkt.set_next_header(IpProtocol::Udp);
ipv4_pkt.set_src_addr(Ipv4Address::UNSPECIFIED);
ipv4_pkt.set_dst_addr(Ipv4Address::BROADCAST);
let mut udp_pkt = UdpPacket::new_unchecked(ipv4_pkt.payload_mut());
udp_pkt.set_src_port(68);
udp_pkt.set_dst_port(67);
udp_pkt.set_len((8 + dhcp_payload.len()) as u16);
udp_pkt.payload_mut().copy_from_slice(&dhcp_payload);
buf
}
}
+86
View File
@@ -0,0 +1,86 @@
use anyhow::{Context, Result};
use ipnet::{Ipv4AddrRange, Ipv4Net};
use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig};
use rand::seq::{IndexedRandom, IteratorRandom};
use std::net::Ipv4Addr;
const MAX_ATTEMPTS: usize = 128;
/// Find an available private subnet and its first two usable host addresses
pub(crate) fn find_available_subnet(prefix_len: u8) -> Result<(Ipv4Addr, Ipv4Addr, Ipv4Net)> {
// Add private address space (as defined in RFC 1918[1])
//
// [1]: https://datatracker.ietf.org/doc/html/rfc1918#section-3
let private_subnets: Vec<Ipv4Net> = vec![
"10.0.0.0/8".parse()?,
"172.16.0.0/12".parse()?,
"192.168.0.0/16".parse()?,
];
// Figure out which address space is already utilized on the host
let mut used_subnets = Vec::new();
for interface in NetworkInterface::show()? {
for addr in interface.addr {
// We only support IPv4 for now
if let Addr::V4(addr) = addr {
let mask = addr.netmask.context("IPv4 interface has no netmask")?;
used_subnets.push(Ipv4Net::with_netmask(addr.ip, mask)?);
}
}
}
// Try up to MAX_ATTEMPTS random subnets until we're able to
// get an available subnet of the desired length
for _ in 0..MAX_ATTEMPTS {
// Give each private range an equal chance, even if a larger one is occupied
let private_subnet = private_subnets.choose(&mut rand::rng()).unwrap();
// Skip private ranges too small to contain the requested subnet
if prefix_len < private_subnet.prefix_len() {
continue;
}
// Pick a subnet of the desired length within this private range
let address = Ipv4AddrRange::new(private_subnet.network(), private_subnet.broadcast())
.choose(&mut rand::rng())
.unwrap();
let candidate = Ipv4Net::new(address, prefix_len)?.trunc();
// Only use the subnet if it doesn't overlap address space
// that is already utilized on the host
let overlaps = used_subnets
.iter()
.any(|used_subnet| candidate.contains(used_subnet) || used_subnet.contains(&candidate));
if overlaps {
continue;
}
// Take the first two hosts from the subnet
let mut hosts = candidate.hosts();
if let (Some(first_host), Some(second_host)) = (hosts.next(), hosts.next()) {
return Ok((first_host, second_host, candidate));
}
}
anyhow::bail!(
"failed to find an unused private IPv4 /{prefix_len} subnet after {MAX_ATTEMPTS} attempts"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[serial_test::serial]
fn finds_two_usable_addresses() {
// Find a subnet on the current host
let (gateway_ip, vm_ip, subnet) = find_available_subnet(30).unwrap();
// Check the subnet and its two usable addresses
assert_eq!(subnet.prefix_len(), 30);
assert!(subnet.network().is_private());
assert_eq!(subnet.hosts().collect::<Vec<_>>(), [gateway_ip, vm_ip]);
}
}
+6
View File
@@ -25,7 +25,13 @@ use system_configuration::sys::preferences::{
};
use uzers::{get_current_groupname, get_current_username, get_effective_uid};
const VERSION: &str = match option_env!("SOFTNET_VERSION") {
Some(version) => version,
None => "unknown-unknown",
};
#[derive(Parser, Debug)]
#[command(version = VERSION)]
struct Args {
#[clap(
long,