Compare commits

...

148 Commits
0.2.0 ... main

Author SHA1 Message Date
Darry 18f5a3338e
Require BOOTP chaddr match for host→VM DHCP responses (#192)
* Require BOOTP chaddr match for host→VM DHCP responses

Mirror #191 request-path identity checks: admit/forward DHCP
BootReplies only when chaddr matches the VM MAC, so foreign
client replies are not written into the guest fd.

Co-authored-by: Cursor <cursoragent@cursor.com>

* $ cargo fmt

---------

Co-authored-by: genforAI <genforAI@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Nikolay Edigaryev <edi@openai.com>
2026-08-14 13:41:41 +01:00
Yibo Zhuang 0c4327dd71
Exit Softnet when its VM socket disconnects (#193)
Check the VM socket on poll timeouts because macOS kqueue does not
report Unix datagram peer disconnects. This prevents orphaned
Softnet processes from blocking VM cleanup.
2026-08-14 13:36:32 +01:00
edi-oai 28bb29df4a
Validate DHCP client identity and source address (#191)
* Validate DHCP client identity and source address

* Use CFBoolean instead of CFNumber for dhcp_ignore_client_identifier

* Serialize bootpd preference updates
2026-08-10 22:10:20 -04:00
dependabot[bot] d079057ecf
Bump the all-updates group with 4 updates (#190)
Bumps the all-updates group with 4 updates: [clap](https://github.com/clap-rs/clap), [sentry](https://github.com/getsentry/sentry-rust), [sentry-anyhow](https://github.com/getsentry/sentry-rust) and [ipnet](https://github.com/krisprice/ipnet).


Updates `clap` from 4.6.4 to 4.6.5
- [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.4...clap_complete-v4.6.5)

Updates `sentry` from 0.49.0 to 0.49.1
- [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.0...0.49.1)

Updates `sentry-anyhow` from 0.49.0 to 0.49.1
- [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.0...0.49.1)

Updates `ipnet` from 2.12.0 to 2.12.1
- [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)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.49.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.49.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: ipnet
  dependency-version: 2.12.1
  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-08-11 00:47:15 +01:00
dependabot[bot] 237aec1df5
Bump the all-updates group across 1 directory with 5 updates (#189)
* Bump the all-updates group across 1 directory with 5 updates

Bumps the all-updates group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [libc](https://github.com/rust-lang/libc) | `0.2.188` | `0.2.189` |
| [clap](https://github.com/clap-rs/clap) | `4.6.3` | `4.6.4` |
| [sentry](https://github.com/getsentry/sentry-rust) | `0.48.5` | `0.49.0` |
| [sentry-anyhow](https://github.com/getsentry/sentry-rust) | `0.48.5` | `0.49.0` |
| [serial_test](https://github.com/palfrey/serial_test) | `3.5.0` | `4.0.1` |



Updates `libc` from 0.2.188 to 0.2.189
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.189/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.188...0.2.189)

Updates `clap` from 4.6.3 to 4.6.4
- [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.3...clap_complete-v4.6.4)

Updates `sentry` from 0.48.5 to 0.49.0
- [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.48.5...0.49.0)

Updates `sentry-anyhow` from 0.48.5 to 0.49.0
- [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.48.5...0.49.0)

Updates `serial_test` from 3.5.0 to 4.0.1
- [Release notes](https://github.com/palfrey/serial_test/releases)
- [Commits](https://github.com/palfrey/serial_test/compare/v3.5.0...v4.0.1)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: libc
  dependency-version: 0.2.189
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: serial_test
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

* Work around Sentry 0.49 marking ClientOptions as #[non_exhaustive]

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikolay Edigaryev <edi@openai.com>
2026-08-04 14:50:20 +01:00
edi-oai cdc2a508aa
Introduce stateful "in TARGET" and "out TARGET" rules (#188) 2026-08-03 11:33:22 -04:00
edi-oai 54b419fd18
dhcp_snooper: accept DHCP replies destined to broadcast addresses (#186) 2026-07-30 18:27:12 +01:00
dependabot[bot] c709387d71
Bump the all-updates group across 1 directory with 11 updates (#183)
* Bump the all-updates group across 1 directory with 11 updates

Bumps the all-updates group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [libc](https://github.com/rust-lang/libc) | `0.2.186` | `0.2.188` |
| [dhcproto](https://github.com/bluecatengineering/dhcproto) | ``eece41c`` | ``60719e5`` |
| [clap](https://github.com/clap-rs/clap) | `4.6.1` | `4.6.3` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.102` | `1.0.104` |
| [sentry](https://github.com/getsentry/sentry-rust) | `0.48.1` | `0.48.5` |
| [sentry-anyhow](https://github.com/getsentry/sentry-rust) | `0.48.1` | `0.48.5` |
| [nix](https://github.com/nix-rust/nix) | `0.31.2` | `0.31.3` |
| [prefix-trie](https://github.com/tiborschneider/prefix-trie) | `0.8.3` | `0.9.2` |
| [log](https://github.com/rust-lang/log) | `0.4.29` | `0.4.33` |
| [serial_test](https://github.com/palfrey/serial_test) | `3.4.0` | `3.5.0` |
| [serde](https://github.com/serde-rs/serde) | `1.0.228` | `1.0.229` |



Updates `libc` from 0.2.186 to 0.2.188
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.188/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.186...0.2.188)

Updates `dhcproto` from `eece41c` to `60719e5`
- [Release notes](https://github.com/bluecatengineering/dhcproto/releases)
- [Commits](eece41c9a1...60719e5df1)

Updates `clap` from 4.6.1 to 4.6.3
- [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.1...clap_complete-v4.6.3)

Updates `anyhow` from 1.0.102 to 1.0.104
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.102...1.0.104)

Updates `sentry` from 0.48.1 to 0.48.5
- [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.48.1...0.48.5)

Updates `sentry-anyhow` from 0.48.1 to 0.48.5
- [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.48.1...0.48.5)

Updates `nix` from 0.31.2 to 0.31.3
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.31.2...v0.31.3)

Updates `prefix-trie` from 0.8.3 to 0.9.2
- [Release notes](https://github.com/tiborschneider/prefix-trie/releases)
- [Commits](https://github.com/tiborschneider/prefix-trie/commits)

Updates `log` from 0.4.29 to 0.4.33
- [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.29...0.4.33)

Updates `serial_test` from 3.4.0 to 3.5.0
- [Release notes](https://github.com/palfrey/serial_test/releases)
- [Commits](https://github.com/palfrey/serial_test/compare/v3.4.0...v3.5.0)

Updates `serde` from 1.0.228 to 1.0.229
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.188
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: dhcproto
  dependency-version: 60719e5df11359b12bf74e743b3c7e0831351c2d
  dependency-type: direct:production
  dependency-group: all-updates
- dependency-name: clap
  dependency-version: 4.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: anyhow
  dependency-version: 1.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.48.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.48.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: nix
  dependency-version: 0.31.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: prefix-trie
  dependency-version: 0.9.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: log
  dependency-version: 0.4.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: serial_test
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: serde
  dependency-version: 1.0.229
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

* prefix_trie: Ipv4Net::zero() → Ipv4Net::default()

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikolay Edigaryev <edi@openai.com>
2026-07-21 21:44:44 +01:00
Fedor Kororkov 08200b7a09
Add dynamic Softnet policy control (#181)
* Add dynamic Softnet policy control

* Simplify Softnet policy RPC methods

* Use jsonrpsee types for Softnet policy RPC

* Reject superseded Softnet policy revisions

* Apply Softnet policy updates only after enqueue

* Flush final Softnet response after input EOF

* Signal EOF when disabling Softnet control

* Normalize Softnet default-route isolation

* Keep policy rules on Proxy (#182)

* Simplify Softnet policy protocol

* Bound pipelined Softnet policy responses

* Keep Softnet port forwarding active on control wakes

---------

Co-authored-by: edi-oai <edi@openai.com>
2026-07-21 21:30:06 +01:00
Minh Vu 5d6b4b9d0c
Validate VM file descriptors before ownership transfer (#177)
* Validate VM file descriptors before ownership transfer

* Handle unnamed macOS Unix sockets
2026-07-16 10:47:53 -04:00
Minh Vu 2d351db2df
Validate VM ARP packets before reading addresses (#175) 2026-07-08 09:45:25 +01:00
Minh Vu 4461df5b7b
Tighten DHCP port matching (#174) 2026-07-08 00:17:45 +01:00
Fedor Kororkov f4a444fb7d
Use GitHub macOS 26 runners (#176) 2026-07-07 12:59:26 -04:00
Greg Hurrell 80f6813049
fix(homebrew): wrap macOS version dependency in on_macos block (#169)
GoReleaser's Homebrew template always emits a bare `depends_on :macos`
for macOS-only formulae. Combining that with the `depends_on :macos =>
:sequoia` line injected via custom_block triggers a Homebrew deprecation
warning on `brew upgrade`:

  Warning: Calling `depends_on :macos` with `depends_on macos:` is deprecated! Use `depends_on :macos` with `depends_on macos:` inside an `on_macos` block instead.
  Please report this issue to the cirruslabs/homebrew-cli tap (not Homebrew/* repositories), or even better, submit a PR to fix it:
    /opt/homebrew/Library/Taps/cirruslabs/homebrew-cli/softnet.rb:18

Declaring the version constraint inside an `on_macos` block is the form
Homebrew recommends and silences the warning without changing behavior
(still macOS-only, Sequoia or newer).
2026-07-07 12:40:37 -04:00
Fedor Kororkov 14a3ee2ebc
Migrate CI from Cirrus CI to GitHub Actions (#167)
* Migrate CI to GitHub Actions

* Publish Softnet releases to homebrew-tools

* Isolate Softnet release secrets
2026-07-07 12:39:20 -04:00
Fedor Kororkov 943bd1eb85
Relicense under FSL-1.1-ALv2 (#166)
* Relicense under FSL-1.1-ALv2

* Use project lifetime in copyright notice
2026-06-05 15:37:14 -07:00
Nikolay Edigaryev df84a30016
--allow: support "@host" syntax (#160)
* --allow: support "@host" syntax

To make communication with the guest VM possible
when using --block=0.0.0.0/0.

* Use "@-alias" wording instead of "@host"
2026-05-15 16:06:03 +02:00
dependabot[bot] bbff9996ab
Bump rustls-webpki from 0.103.9 to 0.103.13 (#154)
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.9 to 0.103.13.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.103.9...v/0.103.13)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 22:24:05 +02:00
dependabot[bot] dd55a20de0
Bump openssl from 0.10.72 to 0.10.79 (#158)
Bumps [openssl](https://github.com/rust-openssl/rust-openssl) from 0.10.72 to 0.10.79.
- [Release notes](https://github.com/rust-openssl/rust-openssl/releases)
- [Commits](https://github.com/rust-openssl/rust-openssl/compare/openssl-v0.10.72...openssl-v0.10.79)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.79
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 22:23:56 +02:00
dependabot[bot] 012a93834a
Bump the all-updates group across 1 directory with 7 updates (#159)
Bumps the all-updates group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [smoltcp](https://github.com/smoltcp-rs/smoltcp) | `0.13.0` | `0.13.1` |
| [libc](https://github.com/rust-lang/libc) | `0.2.183` | `0.2.186` |
| [dhcproto](https://github.com/bluecatengineering/dhcproto) | ``b4ea30d`` | ``eece41c`` |
| [clap](https://github.com/clap-rs/clap) | `4.6.0` | `4.6.1` |
| [sentry](https://github.com/getsentry/sentry-rust) | `0.47.0` | `0.48.1` |
| [sentry-anyhow](https://github.com/getsentry/sentry-rust) | `0.47.0` | `0.48.1` |
| [prefix-trie](https://github.com/tiborschneider/prefix-trie) | `0.8.2` | `0.8.3` |



Updates `smoltcp` from 0.13.0 to 0.13.1
- [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.0...v0.13.1)

Updates `libc` from 0.2.183 to 0.2.186
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.186/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.183...0.2.186)

Updates `dhcproto` from `b4ea30d` to `eece41c`
- [Release notes](https://github.com/bluecatengineering/dhcproto/releases)
- [Commits](b4ea30defc...eece41c9a1)

Updates `clap` from 4.6.0 to 4.6.1
- [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.0...clap_complete-v4.6.1)

Updates `sentry` from 0.47.0 to 0.48.1
- [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.47.0...0.48.1)

Updates `sentry-anyhow` from 0.47.0 to 0.48.1
- [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.47.0...0.48.1)

Updates `prefix-trie` from 0.8.2 to 0.8.3
- [Release notes](https://github.com/tiborschneider/prefix-trie/releases)
- [Commits](https://github.com/tiborschneider/prefix-trie/compare/v0.8.2...v0.8.3)

---
updated-dependencies:
- dependency-name: smoltcp
  dependency-version: 0.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: libc
  dependency-version: 0.2.186
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: dhcproto
  dependency-version: eece41c9a13b0e4912fb9a8f08401ab01b4123d4
  dependency-type: direct:production
  dependency-group: all-updates
- dependency-name: clap
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.48.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.48.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: prefix-trie
  dependency-version: 0.8.3
  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-05-12 22:23:19 +02:00
dependabot[bot] d806af1f9d
Bump the all-updates group with 2 updates (#149)
Bumps the all-updates group with 2 updates: [smoltcp](https://github.com/smoltcp-rs/smoltcp) and [prefix-trie](https://github.com/tiborschneider/prefix-trie).


Updates `smoltcp` from 0.12.0 to 0.13.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.12.0...v0.13.0)

Updates `prefix-trie` from 0.8.1 to 0.8.2
- [Release notes](https://github.com/tiborschneider/prefix-trie/releases)
- [Commits](https://github.com/tiborschneider/prefix-trie/compare/v0.8.1...v0.8.2)

---
updated-dependencies:
- dependency-name: smoltcp
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: prefix-trie
  dependency-version: 0.8.2
  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-03-23 16:54:31 +01:00
dependabot[bot] d992da7023
Bump the all-updates group with 4 updates (#147)
Bumps the all-updates group with 4 updates: [clap](https://github.com/clap-rs/clap), [num_enum](https://github.com/illicitonion/num_enum), [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `clap` from 4.5.60 to 4.6.0
- [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.5.60...clap_complete-v4.6.0)

Updates `num_enum` from 0.7.5 to 0.7.6
- [Commits](https://github.com/illicitonion/num_enum/compare/0.7.5...0.7.6)

Updates `sentry` from 0.46.2 to 0.47.0
- [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.46.2...0.47.0)

Updates `sentry-anyhow` from 0.46.2 to 0.47.0
- [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.46.2...0.47.0)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: num_enum
  dependency-version: 0.7.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.47.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-03-16 12:40:06 +01:00
dependabot[bot] 2e74102ee4
Bump the all-updates group with 2 updates (#146)
Bumps the all-updates group with 2 updates: [libc](https://github.com/rust-lang/libc) and [ipnet](https://github.com/krisprice/ipnet).


Updates `libc` from 0.2.182 to 0.2.183
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.183/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.182...0.2.183)

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

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.183
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: ipnet
  dependency-version: 2.12.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-03-09 10:58:27 +01:00
dependabot[bot] 35ae0608db
Bump nix from 0.31.1 to 0.31.2 in the all-updates group (#145)
Bumps the all-updates group with 1 update: [nix](https://github.com/nix-rust/nix).


Updates `nix` from 0.31.1 to 0.31.2
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.31.1...v0.31.2)

---
updated-dependencies:
- dependency-name: nix
  dependency-version: 0.31.2
  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-03-02 11:01:12 +01:00
dependabot[bot] 54f40f914b
Bump the all-updates group with 3 updates (#144)
Bumps the all-updates group with 3 updates: [clap](https://github.com/clap-rs/clap), [anyhow](https://github.com/dtolnay/anyhow) and [serial_test](https://github.com/palfrey/serial_test).


Updates `clap` from 4.5.58 to 4.5.60
- [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.5.58...clap_complete-v4.5.60)

Updates `anyhow` from 1.0.101 to 1.0.102
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.101...1.0.102)

Updates `serial_test` from 3.3.1 to 3.4.0
- [Release notes](https://github.com/palfrey/serial_test/releases)
- [Commits](https://github.com/palfrey/serial_test/compare/v3.3.1...v3.4.0)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.60
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: anyhow
  dependency-version: 1.0.102
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: serial_test
  dependency-version: 3.4.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-02-23 02:12:09 +01:00
dependabot[bot] 088011e714
Bump clap from 4.5.57 to 4.5.58 in the all-updates group (#143)
Bumps the all-updates group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.57 to 4.5.58
- [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.5.57...clap_complete-v4.5.58)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.58
  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-02-17 14:14:33 +01:00
dependabot[bot] 3cd8732f1c
Bump the all-updates group with 4 updates (#142)
Bumps the all-updates group with 4 updates: [clap](https://github.com/clap-rs/clap), [anyhow](https://github.com/dtolnay/anyhow), [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `clap` from 4.5.54 to 4.5.57
- [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.5.54...clap_complete-v4.5.57)

Updates `anyhow` from 1.0.100 to 1.0.101
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.100...1.0.101)

Updates `sentry` from 0.46.1 to 0.46.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.46.1...0.46.2)

Updates `sentry-anyhow` from 0.46.1 to 0.46.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.46.1...0.46.2)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.57
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: anyhow
  dependency-version: 1.0.101
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.46.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.46.2
  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-02-11 13:56:41 +01:00
dependabot[bot] ca15d50f8d
Bump time from 0.3.36 to 0.3.44 (#141)
Bumps [time](https://github.com/time-rs/time) from 0.3.36 to 0.3.44.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.36...v0.3.44)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.44
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-05 22:18:49 +01:00
dependabot[bot] 928dc3c333
Bump nix from 0.30.1 to 0.31.1 in the all-updates group (#139)
Bumps the all-updates group with 1 update: [nix](https://github.com/nix-rust/nix).


Updates `nix` from 0.30.1 to 0.31.1
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.30.1...v0.31.1)

---
updated-dependencies:
- dependency-name: nix
  dependency-version: 0.31.1
  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-01-27 22:39:35 +01:00
dependabot[bot] 62d9326d26
Bump vmnet from 0.5.0 to 0.5.1 in the all-updates group (#138)
Bumps the all-updates group with 1 update: [vmnet](https://github.com/edigaryev/vmnet).


Updates `vmnet` from 0.5.0 to 0.5.1
- [Release notes](https://github.com/edigaryev/vmnet/releases)
- [Commits](https://github.com/edigaryev/vmnet/compare/0.5.0...0.5.1)

---
updated-dependencies:
- dependency-name: vmnet
  dependency-version: 0.5.1
  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-01-19 14:29:58 +01:00
Nikolay Edigaryev 173f7832b3
DHCP snooper: use coarse time to avoid clock_gettime() overhead (#137)
* DHCP snooper: use coarse time to avoid clock_gettime() overhead

* Introduce uncertainty duration and subtract it from total lease time

* Update coarse time after we've performed the waiting

* Use coarsetime::Updater to update time even when deep inside event loop

* No need for manual coarsetime::Instant::update() anymore

* Ensure that coarsetime::Updater is stopped on Proxy::shutdown()

* Revert "Ensure that coarsetime::Updater is stopped on Proxy::shutdown()"

This reverts commit de255f4240.

* Revert "Use coarsetime::Updater to update time even when deep inside event loop"

This reverts commit 6a95272dcf.

* Revert "No need for manual coarsetime::Instant::update() anymore"

This reverts commit 9ffa829add.

* Update coarse time for the DHCP snooper after reading packet(s)
2026-01-14 21:16:19 +01:00
dependabot[bot] be2c706b00
Bump the all-updates group with 3 updates (#136)
Bumps the all-updates group with 3 updates: [libc](https://github.com/rust-lang/libc), [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `libc` from 0.2.179 to 0.2.180
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.180/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.179...0.2.180)

Updates `sentry` from 0.46.0 to 0.46.1
- [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.46.0...0.46.1)

Updates `sentry-anyhow` from 0.46.0 to 0.46.1
- [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.46.0...0.46.1)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.180
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.46.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.46.1
  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-01-12 11:03:21 +01:00
dependabot[bot] b1f5678f18
Bump the all-updates group across 1 directory with 4 updates (#135)
Bumps the all-updates group with 4 updates in the / directory: [libc](https://github.com/rust-lang/libc), [clap](https://github.com/clap-rs/clap), [uzers](https://github.com/rustadopt/uzers-rs) and [serial_test](https://github.com/palfrey/serial_test).


Updates `libc` from 0.2.178 to 0.2.179
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.178...0.2.179)

Updates `clap` from 4.5.53 to 4.5.54
- [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.5.53...clap_complete-v4.5.54)

Updates `uzers` from 0.12.1 to 0.12.2
- [Release notes](https://github.com/rustadopt/uzers-rs/releases)
- [Changelog](https://github.com/rustadopt/uzers-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustadopt/uzers-rs/compare/v0.12.1...v0.12.2)

Updates `serial_test` from 3.2.0 to 3.3.1
- [Release notes](https://github.com/palfrey/serial_test/releases)
- [Commits](https://github.com/palfrey/serial_test/compare/v3.2.0...v3.3.1)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.179
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: clap
  dependency-version: 4.5.54
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: uzers
  dependency-version: 0.12.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: serial_test
  dependency-version: 3.3.1
  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-01-05 12:53:19 +01:00
dependabot[bot] c1ddb2afc6
Bump the all-updates group with 3 updates (#132)
Bumps the all-updates group with 3 updates: [libc](https://github.com/rust-lang/libc), [system-configuration](https://github.com/mullvad/system-configuration-rs) and [log](https://github.com/rust-lang/log).


Updates `libc` from 0.2.177 to 0.2.178
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.178/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.177...0.2.178)

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

Updates `log` from 0.4.28 to 0.4.29
- [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.28...0.4.29)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.178
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: system-configuration
  dependency-version: 0.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: log
  dependency-version: 0.4.29
  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>
2025-12-08 11:55:22 +01:00
dependabot[bot] 401dea6612
Bump the all-updates group with 2 updates (#131)
Bumps the all-updates group with 2 updates: [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `sentry` from 0.45.0 to 0.46.0
- [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.45.0...0.46.0)

Updates `sentry-anyhow` from 0.45.0 to 0.46.0
- [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.45.0...0.46.0)

---
updated-dependencies:
- dependency-name: sentry
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.46.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>
2025-12-01 13:37:30 +04:00
dependabot[bot] 1dcb0755df
Bump clap from 4.5.51 to 4.5.53 in the all-updates group (#130)
Bumps the all-updates group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.51 to 4.5.53
- [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.5.51...clap_complete-v4.5.53)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.53
  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>
2025-11-24 14:53:18 +04:00
dependabot[bot] 1706062004
Bump clap from 4.5.50 to 4.5.51 in the all-updates group (#129)
Bumps the all-updates group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.50 to 4.5.51
- [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.5.50...clap_complete-v4.5.51)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.51
  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>
2025-11-03 14:17:54 +04:00
Nikolay Edigaryev dfc04a49e4 .goreleaser.yml: require macOS Sequoia (see #128) 2025-10-28 13:49:09 +01:00
Nikolay Edigaryev 7dc5992b55
Perform batched reads from host to improve efficiency (#128)
* Perform batched reads from host to improve efficiency

* CI: don't test on macOS Sonoma
2025-10-28 16:48:05 +04:00
dependabot[bot] 58d4b32258
Bump the all-updates group with 3 updates (#127)
Bumps the all-updates group with 3 updates: [dhcproto](https://github.com/bluecatengineering/dhcproto), [clap](https://github.com/clap-rs/clap) and [serial_test](https://github.com/palfrey/serial_test).


Updates `dhcproto` from `96a6845` to `b4ea30d`
- [Release notes](https://github.com/bluecatengineering/dhcproto/releases)
- [Commits](96a6845f71...b4ea30defc)

Updates `clap` from 4.5.49 to 4.5.50
- [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.5.49...clap_complete-v4.5.50)

Updates `serial_test` from 0.10.0 to 3.2.0
- [Release notes](https://github.com/palfrey/serial_test/releases)
- [Commits](https://github.com/palfrey/serial_test/compare/v0.10.0...v3.2.0)

---
updated-dependencies:
- dependency-name: dhcproto
  dependency-version: b4ea30defc01e7ae66e7075d6a0533d9bb9503dc
  dependency-type: direct:production
  dependency-group: all-updates
- dependency-name: clap
  dependency-version: 4.5.50
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: serial_test
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 16:10:36 +04:00
Nikolay Edigaryev 431ae9bbc9
Introduce --block in addition to --allow (#126) 2025-10-21 17:14:28 +04:00
dependabot[bot] 98988e5c73
Bump the all-updates group with 2 updates (#125)
Bumps the all-updates group with 2 updates: [clap](https://github.com/clap-rs/clap) and [num_enum](https://github.com/illicitonion/num_enum).


Updates `clap` from 4.5.48 to 4.5.49
- [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.5.48...clap_complete-v4.5.49)

Updates `num_enum` from 0.7.4 to 0.7.5
- [Commits](https://github.com/illicitonion/num_enum/compare/0.7.4...0.7.5)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.49
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: num_enum
  dependency-version: 0.7.5
  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>
2025-10-20 11:11:33 +04:00
dependabot[bot] b122b49b3c
Bump the all-updates group with 3 updates (#123)
Bumps the all-updates group with 3 updates: [libc](https://github.com/rust-lang/libc), [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `libc` from 0.2.176 to 0.2.177
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.177/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.176...0.2.177)

Updates `sentry` from 0.43.0 to 0.45.0
- [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.43.0...0.45.0)

Updates `sentry-anyhow` from 0.43.0 to 0.45.0
- [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.43.0...0.45.0)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.177
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.45.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>
2025-10-13 13:22:56 +04:00
dependabot[bot] 2650e78eb0
Bump the all-updates group with 3 updates (#122)
Bumps the all-updates group with 3 updates: [libc](https://github.com/rust-lang/libc), [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `libc` from 0.2.175 to 0.2.176
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.176/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.175...0.2.176)

Updates `sentry` from 0.42.0 to 0.43.0
- [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.42.0...0.43.0)

Updates `sentry-anyhow` from 0.42.0 to 0.43.0
- [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.42.0...0.43.0)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.176
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.43.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>
2025-10-07 00:45:39 +04:00
dependabot[bot] 0c59910018
Bump the all-updates group with 2 updates (#121)
Bumps the all-updates group with 2 updates: [clap](https://github.com/clap-rs/clap) and [anyhow](https://github.com/dtolnay/anyhow).


Updates `clap` from 4.5.47 to 4.5.48
- [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.5.47...clap_complete-v4.5.48)

Updates `anyhow` from 1.0.99 to 1.0.100
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.99...1.0.100)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: anyhow
  dependency-version: 1.0.100
  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>
2025-09-22 12:52:22 +04:00
dependabot[bot] 7e622716f7
Bump polling from 3.10.0 to 3.11.0 in the all-updates group (#120)
Bumps the all-updates group with 1 update: [polling](https://github.com/smol-rs/polling).


Updates `polling` from 3.10.0 to 3.11.0
- [Release notes](https://github.com/smol-rs/polling/releases)
- [Changelog](https://github.com/smol-rs/polling/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/polling/compare/v3.10.0...v3.11.0)

---
updated-dependencies:
- dependency-name: polling
  dependency-version: 3.11.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>
2025-09-15 12:43:22 +04:00
dependabot[bot] 80baf28ead
Bump the all-updates group with 2 updates (#119)
Bumps the all-updates group with 2 updates: [clap](https://github.com/clap-rs/clap) and [log](https://github.com/rust-lang/log).


Updates `clap` from 4.5.46 to 4.5.47
- [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.5.46...clap_complete-v4.5.47)

Updates `log` from 0.4.27 to 0.4.28
- [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.27...0.4.28)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.47
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: log
  dependency-version: 0.4.28
  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>
2025-09-10 20:40:51 +04:00
dependabot[bot] 7c75e2294e
Bump the all-updates group with 2 updates (#118)
Bumps the all-updates group with 2 updates: [dhcproto](https://github.com/bluecatengineering/dhcproto) and [clap](https://github.com/clap-rs/clap).


Updates `dhcproto` from `073e258` to `96a6845`
- [Release notes](https://github.com/bluecatengineering/dhcproto/releases)
- [Commits](073e2588b5...96a6845f71)

Updates `clap` from 4.5.45 to 4.5.46
- [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.5.45...clap_complete-v4.5.46)

---
updated-dependencies:
- dependency-name: dhcproto
  dependency-version: 96a6845f7154a54e9c18d3595807c62e7994d5fe
  dependency-type: direct:production
  dependency-group: all-updates
- dependency-name: clap
  dependency-version: 4.5.46
  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>
2025-09-01 14:36:41 +04:00
dependabot[bot] 78e373d992
Bump tracing-subscriber from 0.3.18 to 0.3.19 in the cargo group (#117)
Bumps the cargo group with 1 update: [tracing-subscriber](https://github.com/tokio-rs/tracing).


Updates `tracing-subscriber` from 0.3.18 to 0.3.19
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.18...tracing-subscriber-0.3.19)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-version: 0.3.19
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-01 14:34:30 +04:00
dependabot[bot] e42ef5150a
Bump the all-updates group with 3 updates (#116)
Bumps the all-updates group with 3 updates: [libc](https://github.com/rust-lang/libc), [clap](https://github.com/clap-rs/clap) and [anyhow](https://github.com/dtolnay/anyhow).


Updates `libc` from 0.2.174 to 0.2.175
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.175/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.174...0.2.175)

Updates `clap` from 4.5.43 to 4.5.45
- [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.5.43...clap_complete-v4.5.45)

Updates `anyhow` from 1.0.98 to 1.0.99
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.98...1.0.99)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.175
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: clap
  dependency-version: 4.5.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: anyhow
  dependency-version: 1.0.99
  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>
2025-08-18 12:03:08 +04:00
dependabot[bot] 580d84f0ca
Bump clap from 4.5.42 to 4.5.43 in the all-updates group (#115)
Bumps the all-updates group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.42 to 4.5.43
- [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.5.42...clap_complete-v4.5.43)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.43
  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>
2025-08-11 12:58:29 +04:00
dependabot[bot] a7da95bd5b
Bump the all-updates group with 4 updates (#114)
Bumps the all-updates group with 4 updates: [polling](https://github.com/smol-rs/polling), [clap](https://github.com/clap-rs/clap), [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `polling` from 3.9.0 to 3.10.0
- [Release notes](https://github.com/smol-rs/polling/releases)
- [Changelog](https://github.com/smol-rs/polling/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/polling/compare/v3.9.0...v3.10.0)

Updates `clap` from 4.5.41 to 4.5.42
- [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.5.41...clap_complete-v4.5.42)

Updates `sentry` from 0.41.0 to 0.42.0
- [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.41.0...0.42.0)

Updates `sentry-anyhow` from 0.41.0 to 0.42.0
- [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.41.0...0.42.0)

---
updated-dependencies:
- dependency-name: polling
  dependency-version: 3.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: clap
  dependency-version: 4.5.42
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.42.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>
2025-08-04 12:25:33 +04:00
dependabot[bot] 1c52f32688
Bump polling from 3.8.0 to 3.9.0 in the all-updates group (#113)
Bumps the all-updates group with 1 update: [polling](https://github.com/smol-rs/polling).


Updates `polling` from 3.8.0 to 3.9.0
- [Release notes](https://github.com/smol-rs/polling/releases)
- [Changelog](https://github.com/smol-rs/polling/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/polling/compare/v3.8.0...v3.9.0)

---
updated-dependencies:
- dependency-name: polling
  dependency-version: 3.9.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>
2025-07-21 12:33:26 +04:00
dependabot[bot] a8d5ff65d6
Bump clap from 4.5.40 to 4.5.41 in the all-updates group (#112)
Bumps the all-updates group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.40 to 4.5.41
- [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.5.40...clap_complete-v4.5.41)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.41
  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>
2025-07-14 13:03:45 +04:00
dependabot[bot] b1459e9188
Bump the all-updates group with 2 updates (#111)
Bumps the all-updates group with 2 updates: [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `sentry` from 0.40.0 to 0.41.0
- [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.40.0...0.41.0)

Updates `sentry-anyhow` from 0.40.0 to 0.41.0
- [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.40.0...0.41.0)

---
updated-dependencies:
- dependency-name: sentry
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.41.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>
2025-06-30 09:20:41 -04:00
dependabot[bot] 80745a6720
Bump the all-updates group with 5 updates (#110)
Bumps the all-updates group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [libc](https://github.com/rust-lang/libc) | `0.2.173` | `0.2.174` |
| [num_enum](https://github.com/illicitonion/num_enum) | `0.7.3` | `0.7.4` |
| [sentry](https://github.com/getsentry/sentry-rust) | `0.39.0` | `0.40.0` |
| [sentry-anyhow](https://github.com/getsentry/sentry-rust) | `0.39.0` | `0.40.0` |
| [prefix-trie](https://github.com/tiborschneider/prefix-trie) | `0.7.0` | `0.8.1` |


Updates `libc` from 0.2.173 to 0.2.174
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.174/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.173...0.2.174)

Updates `num_enum` from 0.7.3 to 0.7.4
- [Commits](https://github.com/illicitonion/num_enum/compare/0.7.3...0.7.4)

Updates `sentry` from 0.39.0 to 0.40.0
- [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.39.0...0.40.0)

Updates `sentry-anyhow` from 0.39.0 to 0.40.0
- [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.39.0...0.40.0)

Updates `prefix-trie` from 0.7.0 to 0.8.1
- [Commits](https://github.com/tiborschneider/prefix-trie/commits)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.174
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: num_enum
  dependency-version: 0.7.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: prefix-trie
  dependency-version: 0.8.1
  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>
2025-06-23 15:20:07 +04:00
dependabot[bot] 0117fcbdb2
Bump the all-updates group with 4 updates (#109)
* Bump the all-updates group with 4 updates

Bumps the all-updates group with 4 updates: [libc](https://github.com/rust-lang/libc), [clap](https://github.com/clap-rs/clap), [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `libc` from 0.2.172 to 0.2.173
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.173/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.172...0.2.173)

Updates `clap` from 4.5.39 to 4.5.40
- [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.5.39...clap_complete-v4.5.40)

Updates `sentry` from 0.38.1 to 0.39.0
- [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.38.1...0.39.0)

Updates `sentry-anyhow` from 0.38.1 to 0.39.0
- [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.38.1...0.39.0)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.173
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: clap
  dependency-version: 4.5.40
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

* $ brew install --cask goreleaser/tap/goreleaser-pro

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2025-06-16 15:40:25 +04:00
dependabot[bot] 31a1296e64
Bump clap from 4.5.38 to 4.5.39 in the all-updates group (#108)
Bumps the all-updates group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.38 to 4.5.39
- [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.5.38...clap_complete-v4.5.39)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.39
  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>
2025-06-02 12:57:27 +04:00
dependabot[bot] d4f66bfb5c
Bump the all-updates group with 3 updates (#107)
Bumps the all-updates group with 3 updates: [polling](https://github.com/smol-rs/polling), [dhcproto](https://github.com/bluecatengineering/dhcproto) and [privdrop](https://github.com/jedisct1/rust-privdrop).


Updates `polling` from 3.7.4 to 3.8.0
- [Release notes](https://github.com/smol-rs/polling/releases)
- [Changelog](https://github.com/smol-rs/polling/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/polling/compare/v3.7.4...v3.8.0)

Updates `dhcproto` from `120da6f` to `073e258`
- [Release notes](https://github.com/bluecatengineering/dhcproto/releases)
- [Commits](120da6fcd8...073e2588b5)

Updates `privdrop` from 0.5.5 to 0.5.6
- [Commits](https://github.com/jedisct1/rust-privdrop/compare/0.5.5...0.5.6)

---
updated-dependencies:
- dependency-name: polling
  dependency-version: 3.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: dhcproto
  dependency-version: 073e2588b50326654f68b6141b378a1e5d60781c
  dependency-type: direct:production
  dependency-group: all-updates
- dependency-name: privdrop
  dependency-version: 0.5.6
  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>
2025-05-26 16:26:58 +04:00
dependabot[bot] b54679d6e4
Bump the all-updates group with 2 updates (#106)
Bumps the all-updates group with 2 updates: [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `sentry` from 0.38.0 to 0.38.1
- [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.38.0...0.38.1)

Updates `sentry-anyhow` from 0.38.0 to 0.38.1
- [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.38.0...0.38.1)

---
updated-dependencies:
- dependency-name: sentry
  dependency-version: 0.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.38.1
  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>
2025-05-19 13:09:28 +04:00
dependabot[bot] 560e909b28
Bump the all-updates group with 3 updates (#105)
Bumps the all-updates group with 3 updates: [clap](https://github.com/clap-rs/clap), [sentry](https://github.com/getsentry/sentry-rust) and [sentry-anyhow](https://github.com/getsentry/sentry-rust).


Updates `clap` from 4.5.37 to 4.5.38
- [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.5.37...clap_complete-v4.5.38)

Updates `sentry` from 0.37.0 to 0.38.0
- [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.37.0...0.38.0)

Updates `sentry-anyhow` from 0.37.0 to 0.38.0
- [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.37.0...0.38.0)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.38
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.38.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>
2025-05-12 13:46:57 +04:00
Andrew Malchuk e6c9f98162
Build universal binary instead of architecture dependent (#104) 2025-05-07 23:41:12 +04:00
dependabot[bot] ddc699e076
Bump nix from 0.29.0 to 0.30.1 in the all-updates group (#103)
Bumps the all-updates group with 1 update: [nix](https://github.com/nix-rust/nix).


Updates `nix` from 0.29.0 to 0.30.1
- [Changelog](https://github.com/nix-rust/nix/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nix-rust/nix/compare/v0.29.0...v0.30.1)

---
updated-dependencies:
- dependency-name: nix
  dependency-version: 0.30.1
  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>
2025-05-05 18:38:48 +04:00
Nikolay Edigaryev 4fa4fb1bda
Avoid using one-shot triggered events (#102) 2025-04-21 18:16:09 +04:00
dependabot[bot] 11213d0f46
Bump the all-updates group with 4 updates (#101)
Bumps the all-updates group with 4 updates: [libc](https://github.com/rust-lang/libc), [clap](https://github.com/clap-rs/clap), [privdrop](https://github.com/jedisct1/rust-privdrop) and [anyhow](https://github.com/dtolnay/anyhow).


Updates `libc` from 0.2.171 to 0.2.172
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.172/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.171...0.2.172)

Updates `clap` from 4.5.36 to 4.5.37
- [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.5.36...clap_complete-v4.5.37)

Updates `privdrop` from 0.5.4 to 0.5.5
- [Commits](https://github.com/jedisct1/rust-privdrop/compare/0.5.4...0.5.5)

Updates `anyhow` from 1.0.97 to 1.0.98
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.97...1.0.98)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.172
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: clap
  dependency-version: 4.5.37
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: privdrop
  dependency-version: 0.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: anyhow
  dependency-version: 1.0.98
  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>
2025-04-21 12:41:49 +04:00
dependabot[bot] ba114b3c86
Bump clap from 4.5.35 to 4.5.36 in the all-updates group (#100)
Bumps the all-updates group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.5.35 to 4.5.36
- [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.5.35...clap_complete-v4.5.36)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.36
  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>
2025-04-14 13:38:24 +04:00
dependabot[bot] e37f52971d
Bump tokio from 1.40.0 to 1.44.2 in the cargo group (#99)
Bumps the cargo group with 1 update: [tokio](https://github.com/tokio-rs/tokio).


Updates `tokio` from 1.40.0 to 1.44.2
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.40.0...tokio-1.44.2)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.44.2
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 11:17:44 +04:00
dependabot[bot] 7ba7849a80
Bump the all-updates group with 4 updates (#98)
Bumps the all-updates group with 4 updates: [clap](https://github.com/clap-rs/clap), [sentry](https://github.com/getsentry/sentry-rust), [sentry-anyhow](https://github.com/getsentry/sentry-rust) and [prefix-trie](https://github.com/tiborschneider/prefix-trie).


Updates `clap` from 4.5.34 to 4.5.35
- [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.5.34...clap_complete-v4.5.35)

Updates `sentry` from 0.36.0 to 0.37.0
- [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.36.0...0.37.0)

Updates `sentry-anyhow` from 0.36.0 to 0.37.0
- [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.36.0...0.37.0)

Updates `prefix-trie` from 0.6.0 to 0.7.0
- [Commits](https://github.com/tiborschneider/prefix-trie/commits)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.35
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: sentry
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: sentry-anyhow
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: prefix-trie
  dependency-version: 0.7.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>
2025-04-07 13:01:29 +04:00
dependabot[bot] 6d0702f884
Bump openssl from 0.10.70 to 0.10.72 in the cargo group (#97)
Bumps the cargo group with 1 update: [openssl](https://github.com/sfackler/rust-openssl).


Updates `openssl` from 0.10.70 to 0.10.72
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.70...openssl-v0.10.72)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.72
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-05 00:53:14 +04:00
Nikolay Edigaryev 54e02845ac
Add CODEOWNERS and group all Dependabot updates in a single PR (#96) 2025-03-31 19:15:04 +04:00
dependabot[bot] 50044e9cef
Bump clap from 4.5.32 to 4.5.34 (#95)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.32 to 4.5.34.
- [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.5.32...clap_complete-v4.5.34)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 15:50:34 +04:00
dependabot[bot] 36a7e994c0
Bump log from 0.4.26 to 0.4.27 (#94)
Bumps [log](https://github.com/rust-lang/log) from 0.4.26 to 0.4.27.
- [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.26...0.4.27)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 15:50:28 +04:00
dependabot[bot] 4e70021b2b
Bump libc from 0.2.170 to 0.2.171 (#93)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.170 to 0.2.171.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.171/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.170...0.2.171)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 14:03:37 +04:00
dependabot[bot] 37f3c2f8ea
Bump clap from 4.5.31 to 4.5.32 (#92)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.31 to 4.5.32.
- [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/v4.5.31...clap_complete-v4.5.32)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 14:02:35 +04:00
dependabot[bot] c41a37a40f
Bump anyhow from 1.0.96 to 1.0.97 (#91)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.96 to 1.0.97.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.96...1.0.97)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 18:00:53 +04:00
Peter A. 570f02e8d5
PartialEq for ExposedPort (#90) 2025-03-05 15:13:07 +04:00
Nikolay Edigaryev 179ef6c282 CI(rustup): install toolchain in addition to target 2025-03-03 19:06:58 +01:00
dependabot[bot] 8692eb9de6
Bump clap from 4.5.30 to 4.5.31 (#89)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.30 to 4.5.31.
- [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.5.30...v4.5.31)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 18:37:42 +04:00
dependabot[bot] 86cc71e711
Bump clap from 4.5.29 to 4.5.30 (#87)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.29 to 4.5.30.
- [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.5.29...clap_complete-v4.5.30)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 22:38:31 +04:00
dependabot[bot] 4e648fade1
Bump log from 0.4.25 to 0.4.26 (#88)
Bumps [log](https://github.com/rust-lang/log) from 0.4.25 to 0.4.26.
- [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.25...0.4.26)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 22:37:54 +04:00
dependabot[bot] 47faa8f577
Bump libc from 0.2.169 to 0.2.170 (#86)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.169 to 0.2.170.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.170/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.169...0.2.170)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 22:37:22 +04:00
dependabot[bot] 224ae136f6
Bump anyhow from 1.0.95 to 1.0.96 (#85)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.95 to 1.0.96.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.95...1.0.96)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 22:37:01 +04:00
dependabot[bot] 6253053799
Bump clap from 4.5.28 to 4.5.29 (#83)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.28 to 4.5.29.
- [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.5.28...clap_complete-v4.5.29)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 14:54:54 +04:00
dependabot[bot] 3b8ddff4d3
Bump mac_address from 1.1.7 to 1.1.8 (#84)
Bumps [mac_address](https://github.com/rep-nop/mac_address) from 1.1.7 to 1.1.8.
- [Commits](https://github.com/rep-nop/mac_address/commits)

---
updated-dependencies:
- dependency-name: mac_address
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 14:54:29 +04:00
dependabot[bot] 0eac5c49a9
Bump hickory-proto from 0.24.2 to 0.24.3 in the cargo group (#82)
Bumps the cargo group with 1 update: [hickory-proto](https://github.com/hickory-dns/hickory-dns).


Updates `hickory-proto` from 0.24.2 to 0.24.3
- [Release notes](https://github.com/hickory-dns/hickory-dns/releases)
- [Changelog](https://github.com/hickory-dns/hickory-dns/blob/v0.24.3/CHANGELOG.md)
- [Commits](https://github.com/hickory-dns/hickory-dns/compare/v0.24.2...v0.24.3)

---
updated-dependencies:
- dependency-name: hickory-proto
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-12 02:52:38 +04:00
dependabot[bot] 2cfe616b1e
Bump clap from 4.5.27 to 4.5.28 (#81)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.27 to 4.5.28.
- [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.5.27...clap_complete-v4.5.28)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 16:15:07 +04:00
dependabot[bot] 06c1b809b6
Bump openssl from 0.10.66 to 0.10.70 in the cargo group (#80)
Bumps the cargo group with 1 update: [openssl](https://github.com/sfackler/rust-openssl).


Updates `openssl` from 0.10.66 to 0.10.70
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.66...openssl-v0.10.70)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-04 01:54:04 +04:00
dependabot[bot] 4ef98589c4
Bump prefix-trie from 0.5.1 to 0.6.0 (#78)
Bumps [prefix-trie](https://github.com/tiborschneider/prefix-trie) from 0.5.1 to 0.6.0.
- [Commits](https://github.com/tiborschneider/prefix-trie/commits)

---
updated-dependencies:
- dependency-name: prefix-trie
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-28 00:21:22 +04:00
dependabot[bot] 82cdc24556
Bump ipnet from 2.10.1 to 2.11.0 (#77)
Bumps [ipnet](https://github.com/krisprice/ipnet) from 2.10.1 to 2.11.0.
- [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/2.11.0)

---
updated-dependencies:
- dependency-name: ipnet
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-28 00:19:56 +04:00
dependabot[bot] 267466d572
Bump clap from 4.5.26 to 4.5.27 (#79)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.26 to 4.5.27.
- [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.5.26...clap_complete-v4.5.27)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-28 00:19:38 +04:00
dependabot[bot] c05bd23f4a
Bump log from 0.4.22 to 0.4.25 (#76)
Bumps [log](https://github.com/rust-lang/log) from 0.4.22 to 0.4.25.
- [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.22...0.4.25)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 11:48:26 +04:00
dependabot[bot] 3b83823a09
Bump sentry from 0.35.0 to 0.36.0 (#74)
Bumps [sentry](https://github.com/getsentry/sentry-rust) from 0.35.0 to 0.36.0.
- [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.35.0...0.36.0)

---
updated-dependencies:
- dependency-name: sentry
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 12:29:19 +04:00
dependabot[bot] cf24be0992
Bump sentry-anyhow from 0.35.0 to 0.36.0 (#73)
Bumps [sentry-anyhow](https://github.com/getsentry/sentry-rust) from 0.35.0 to 0.36.0.
- [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.35.0...0.36.0)

---
updated-dependencies:
- dependency-name: sentry-anyhow
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 12:19:49 +04:00
dependabot[bot] 14fe582a4d
Bump clap from 4.5.23 to 4.5.26 (#72)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.23 to 4.5.26.
- [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.5.23...clap_complete-v4.5.26)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 12:18:30 +04:00
Nikolay Edigaryev 539cff564d
README.md: clarify how Softnet restricts networking (#71) 2025-01-10 19:31:08 +04:00
Nikolay Edigaryev 8519fa2f86
Support TCP port exposure via "--expose" command-line argument (#70) 2025-01-09 16:50:39 +04:00
Nikolay Edigaryev f3acef87a7
Upgrade "dhcproto" and "url" crates to avoid depending on "idna" < 1.0.0 (#69) 2025-01-06 18:53:37 +04:00
dependabot[bot] a35e5ae92a
Bump anyhow from 1.0.94 to 1.0.95 (#67)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.94 to 1.0.95.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.94...1.0.95)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-23 12:39:11 +04:00
dependabot[bot] 1f5aeb29f3
Bump libc from 0.2.168 to 0.2.169 (#68)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.168 to 0.2.169.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.169/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.168...0.2.169)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-23 12:38:42 +04:00
dependabot[bot] 9a62801cd6
Bump libc from 0.2.167 to 0.2.168 (#66)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.167 to 0.2.168.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.168/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.167...0.2.168)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 16:15:12 +04:00
dependabot[bot] 694f2e138a
Bump clap from 4.5.21 to 4.5.23 (#64)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.21 to 4.5.23.
- [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.5.21...clap_complete-v4.5.23)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 13:32:45 +04:00
dependabot[bot] c2ff5761cb
Bump anyhow from 1.0.93 to 1.0.94 (#65)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.93 to 1.0.94.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.93...1.0.94)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 12:52:46 +04:00
dependabot[bot] 762868a8eb
Bump smoltcp from 0.11.0 to 0.12.0 (#62)
* Bump smoltcp from 0.11.0 to 0.12.0

Bumps [smoltcp](https://github.com/smoltcp-rs/smoltcp) from 0.11.0 to 0.12.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.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: smoltcp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* smoltcp moved to core::net types for IP addresses

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2024-12-05 00:15:21 +04:00
dependabot[bot] 95a3358f59
Bump hashbrown from 0.15.0 to 0.15.2 in the cargo group (#63)
Bumps the cargo group with 1 update: [hashbrown](https://github.com/rust-lang/hashbrown).


Updates `hashbrown` from 0.15.0 to 0.15.2
- [Changelog](https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/hashbrown/commits)

---
updated-dependencies:
- dependency-name: hashbrown
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 00:15:12 +04:00
dependabot[bot] ebc7cf8973
Bump sentry from 0.34.0 to 0.35.0 (#60)
Bumps [sentry](https://github.com/getsentry/sentry-rust) from 0.34.0 to 0.35.0.
- [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.34.0...0.35.0)

---
updated-dependencies:
- dependency-name: sentry
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 12:11:21 +04:00
dependabot[bot] 86f082ac77
Bump sentry-anyhow from 0.34.0 to 0.35.0 (#59)
Bumps [sentry-anyhow](https://github.com/getsentry/sentry-rust) from 0.34.0 to 0.35.0.
- [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.34.0...0.35.0)

---
updated-dependencies:
- dependency-name: sentry-anyhow
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 12:02:04 +04:00
dependabot[bot] b3df49889a
Bump libc from 0.2.164 to 0.2.167 (#61)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.164 to 0.2.167.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.167/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.164...0.2.167)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 12:01:37 +04:00
dependabot[bot] 82be9577b3
Bump clap from 4.5.20 to 4.5.21 (#57)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.20 to 4.5.21.
- [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.5.20...clap_complete-v4.5.21)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 13:32:24 +04:00
dependabot[bot] 04c6019437
Bump libc from 0.2.162 to 0.2.164 (#58)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.162 to 0.2.164.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.162...0.2.164)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 13:32:14 +04:00
dependabot[bot] 7374ceb239
Bump polling from 3.7.3 to 3.7.4 (#54)
Bumps [polling](https://github.com/smol-rs/polling) from 3.7.3 to 3.7.4.
- [Release notes](https://github.com/smol-rs/polling/releases)
- [Changelog](https://github.com/smol-rs/polling/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/polling/compare/v3.7.3...v3.7.4)

---
updated-dependencies:
- dependency-name: polling
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 10:38:49 +04:00
dependabot[bot] cc7cc0e740
Bump anyhow from 1.0.92 to 1.0.93 (#55)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.92 to 1.0.93.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.92...1.0.93)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 10:38:38 +04:00
dependabot[bot] e53beeeb79
Bump libc from 0.2.161 to 0.2.162 (#56)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.161 to 0.2.162.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.162/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.161...0.2.162)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 10:37:50 +04:00
dependabot[bot] 05cba5d771
Bump anyhow from 1.0.91 to 1.0.92 (#53)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.91 to 1.0.92.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.91...1.0.92)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 18:50:40 +04:00
dependabot[bot] cf97e3878d
Bump anyhow from 1.0.90 to 1.0.91 (#52)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.90 to 1.0.91.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.90...1.0.91)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 12:13:17 +04:00
dependabot[bot] 7f5293dd5e
Bump libc from 0.2.159 to 0.2.161 (#50)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.159 to 0.2.161.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.161/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.159...0.2.161)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 11:53:12 +04:00
dependabot[bot] 9a2e59b844
Bump anyhow from 1.0.89 to 1.0.90 (#51)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.89 to 1.0.90.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.89...1.0.90)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 11:42:26 +04:00
dependabot[bot] 603c8b4889
Bump clap from 4.5.19 to 4.5.20 (#49) 2024-10-14 10:48:35 -04:00
Nikolay Edigaryev 24641d5325
$ cargo update (#48) 2024-10-03 23:54:02 +04:00
Nikolay Edigaryev ed64c139cf
dependabot.yml: enable version updates for Cargo (#42) 2024-10-03 23:45:45 +04:00
Nikolay Edigaryev 8359992a08
$ cargo update (#40)
Also convert the "rust-toolchain" to "rust-toolchain.toml".
2024-07-23 14:33:31 +04:00
Nikolay Edigaryev 147c051b0e
Disable isolation when `--allow=0.0.0.0/0` is specified (#39)
* Disable isolation when --allow=0.0.0.0/0 is specified

* Upgrade & upgrade the dependencies
2024-07-02 15:36:07 +04:00
Nikolay Edigaryev 6456ed7228
README.md: proper nested bullet point syntax 2024-06-13 14:57:44 +04:00
Nikolay Edigaryev 56808c591f
README.md: remove extraneous space 2024-06-13 14:57:02 +04:00
Nikolay Edigaryev eba21ed33e
Link to Wikipedia's "ARP spoofing" article
And include some examples of the tools.
2024-06-13 14:56:01 +04:00
Nikolay Edigaryev 867679446e
Ignore socketpair(2) errors when sending (#36) 2024-03-25 12:37:53 -04:00
Nikolay Edigaryev 4a13c5922b
Prevent multiple --allow flags (#35)
* Prevent multiple --allow flags

* --allow: better value name instead of just <ALLOW>
2024-03-11 23:02:59 +04:00
Nikolay Edigaryev a92f4e0c99
Introduce --allow command-line argument to allow traffic to CIDRs (#34) 2024-03-11 22:07:39 +04:00
Sergei Parshev 0a92c290be
Added a way to enable host-only networking through tart using --net-host (#32)
* Added a way to enable host-only networking through tart using SOFTNET_NET_TYPE=host

* Removed env variable and moved to Enum instead of str

* Fixed defaults & restricted publicity of host

* Fixed usage of NetType
2024-03-01 11:25:19 -05:00
Fedor Korotkov f5a1b1cdbd
Goreleaser Fix (#30)
* Goreleaser Bug

Theoretically this config should work but it doesn't

* Specify `goamd64`
2024-01-24 19:09:59 +04:00
Fedor Korotkov 817dbb6e32
Fixed `x86_64` Homebrew update (#29)
Seems we need to build separate archives too and pass it to `brew`.
2024-01-24 07:30:36 -05:00
Fedor Korotkov 5f3b371e93
Build for `x86_64` (#28) 2024-01-24 15:23:04 +04:00
Nikolay Edigaryev cd5f1d2f4f
Fix unaligned read by switching from unmaintained users crate (#26)
* Fix unaligned read by switching from unmaintained users crate

* Bump proc-macro2 to to fix "unknown feature `proc_macro_span_shrink`"
2023-09-12 06:59:02 -04:00
Fedor Korotkov a775a92772
Add link to the blog post (#25) 2023-05-05 03:19:40 +04:00
Nikolay Edigaryev f38d65f98f
README.md: Tart now uses --net-softnet 2023-03-28 08:21:19 +04:00
dependabot[bot] 4ba480ff4f
Bump openssl from 0.10.45 to 0.10.48 (#24)
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.45 to 0.10.48.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.45...openssl-v0.10.48)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-25 16:20:34 +04:00
Nikolay Edigaryev d7699e95a9
Support graceful termination via SIGINT (#23)
* Support graceful termination via SIGINT

* $ cargo fmt

* Explain why we need to ignore the SIGINT
2023-03-16 18:38:03 +04:00
dependabot[bot] 535e03c97f
Bump tokio from 1.23.0 to 1.25.0 (#21)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.23.0 to 1.25.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.23.0...tokio-1.25.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-05 19:46:48 +04:00
Nikolay Edigaryev d635751948
Capture ENOBUFS message into Sentry only once (#19) 2022-12-24 11:36:42 +04:00
Fedor Korotkov e71b32a8dd
Populate Sentry Release (#17)
* Populate Sentry Release

* Reverted new line

* Update src/main.rs

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2022-12-14 17:17:34 -05:00
Nikolay Edigaryev 59cd9098e0
Ignore ENOBUFS when writing to VM's socket (#18)
* Ignore ENOBUFS when writing to VM's socket

* Hint the into() target type to the compiler to fix the build error

* Fix Clippy warnings
2022-12-15 00:15:19 +04:00
Nikolay Edigaryev 4ab3cd7e5c
Switch back to nightly toolchain (#16) 2022-12-14 23:18:24 +04:00
Nikolay Edigaryev cd78047d79
Upload symbols and sources to Sentry (#15)
* Upload symbols and sources to Sentry

* .cirrus.yml: add SENTRY_AUTH_TOKEN

* .cirrus.yml: install Sentry CLI
2022-12-14 19:34:26 +04:00
Fedor Korotkov a1108b1b7f
Full backtrace (#14)
To include line numbers
2022-12-14 18:59:24 +04:00
Nikolay Edigaryev 11910d8540
Sentry integration (#13)
* Sentry integration

* Introduce a more generic CIRRUS_SENTRY_TAGS

* Revert switching to nightly toolchain
2022-12-12 21:33:45 +04:00
Nikolay Edigaryev 22c92688e5
Don't panic in Drop (#12) 2022-11-19 20:53:30 +04:00
Nikolay Edigaryev e2403f0ea9
Only perform DHCP snooping for frames destined to the VM (#10) 2022-11-10 20:31:18 +04:00
Nikolay Edigaryev 717e6b0f89
Increase the default bootpd(8) lease time from 1 to 10 minutes (#8) 2022-10-21 19:09:18 +04:00
Nikolay Edigaryev 71465f8bff
README.md: clarify installation instructions a bit 2022-10-13 18:30:30 +04:00
32 changed files with 7849 additions and 1169 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[target.aarch64-apple-darwin]
runner = 'sudo -E'

View File

@ -1,27 +0,0 @@
env:
PATH: "$PATH:$HOME/.cargo/bin"
task:
name: Build
macos_instance:
image: ghcr.io/cirruslabs/macos-monterey-xcode:14
install_rust_script:
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
build_script:
- cargo build
task:
name: Release
only_if: $CIRRUS_TAG != ''
macos_instance:
image: ghcr.io/cirruslabs/macos-monterey-xcode:14
env:
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
install_rust_script:
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
install_goreleaser_script:
- brew install go goreleaser/tap/goreleaser-pro
build_script:
- cargo build --release
release_script: goreleaser

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
* @edigaryev @fkorotkov

10
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
groups:
all-updates:
patterns:
- "*"

43
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: CI
on:
merge_group:
pull_request:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: macos-26
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install lint components
run: rustup component add clippy rustfmt
- name: Check formatting
run: cargo fmt --check
- name: Run Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
test:
name: Test on macOS 26
runs-on: macos-26
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Run tests
run: cargo test

84
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,84 @@
name: Release
on:
push:
tags:
- "*"
workflow_dispatch:
permissions:
contents: read
jobs:
release:
name: Release
if: github.event_name == 'push' && github.ref_type == 'tag'
runs-on: macos-26
environment: publish
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install release targets
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
- name: Create release app token for this repo
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
permission-contents: write
- name: Create release app token for homebrew-tools
id: tap-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
owner: openai
repositories: homebrew-tools
permission-contents: write
permission-pull-requests: write
- name: Release
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser-pro
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ steps.tap-token.outputs.token }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
snapshot:
name: Release (Dry Run)
if: github.event_name == 'workflow_dispatch'
runs-on: macos-26
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install release targets
run: rustup target add aarch64-apple-darwin x86_64-apple-darwin
- name: Release dry run
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser-pro
version: "~> v2"
args: release --skip=publish --snapshot --clean
- name: Upload dry-run artifacts
uses: actions/upload-artifact@v6
with:
name: softnet-snapshot
path: dist/**

2
.gitignore vendored
View File

@ -1 +1,3 @@
/.idea
/dist
/target

View File

@ -1,32 +1,40 @@
---
version: 2
project_name: softnet
builds:
- builder: prebuilt
goos:
- darwin
goarch:
- arm64
prebuilt:
path: target/release/softnet
- builder: rust
command: build
targets:
- aarch64-apple-darwin
- x86_64-apple-darwin
universal_binaries:
- replace: true
archives:
- id: binary
format: binary
name_template: "{{ .ProjectName }}"
- id: regular
name_template: "{{ .ProjectName }}"
- name_template: "{{ .ProjectName }}"
formats:
- tar.gz
release:
prerelease: auto
brews:
- name: softnet
ids:
- regular
tap:
owner: cirruslabs
name: homebrew-cli
- name: "{{ .ProjectName }}"
repository:
owner: openai
name: homebrew-tools
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
branch: "softnet-{{ .Version }}"
pull_request:
enabled: true
directory: Formula
caveats: See the Github repository for more information
homepage: https://github.com/cirruslabs/softnet
homepage: https://github.com/openai/softnet
description: Software networking with isolation for Tart
skip_upload: auto
custom_block: |
on_macos do
depends_on :macos => :sequoia
end

3425
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,22 +2,41 @@
name = "softnet"
version = "0.1.0"
publish = false
edition = "2021"
edition = "2024"
[lib]
path = "lib/mod.rs"
[profile.release-with-debug]
inherits = "release"
debug = true
[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"
smoltcp = "0"
libc = "0"
polling = "3"
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
vmnet = "0.5.1"
clap = { version = "4", features = ["derive"] }
mac_address = "1"
privdrop = "0"
anyhow = { version = "1", features = ["backtrace"] }
ip_network = "0"
uzers = "0"
system-configuration = "0"
num_enum = "0"
sentry = { version = "0", features = ["debug-images"] }
sentry-anyhow = { version = "0", features = ["backtrace"] }
nix = { version = "0", features = ["signal", "socket"] }
prefix-trie = "0"
ipnet = "2"
oslog = "0.2.0"
log = "0.4.29"
serial_test = "4"
coarsetime = "0.1.37"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
jsonrpsee-types = "0.26"
[profile.release]
debug = true

766
LICENSE
View File

@ -1,661 +1,105 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
# Functional Source License, Version 1.1, ALv2 Future License
## Abbreviation
FSL-1.1-ALv2
## Notice
Copyright 2022-2026 OpenAI
## Terms and Conditions
### Licensor ("We")
The party offering the Software under these Terms and Conditions.
### The Software
The "Software" is each version of the software that we make available under
these Terms and Conditions, as indicated by our inclusion of these Terms and
Conditions with the Software.
### License Grant
Subject to your compliance with this License Grant and the Patents,
Redistribution and Trademark clauses below, we hereby grant you the right to
use, copy, modify, create derivative works, publicly perform, publicly display
and redistribute the Software for any Permitted Purpose identified below.
### Permitted Purpose
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
means making the Software available to others in a commercial product or
service that:
1. substitutes for the Software;
2. substitutes for any other product or service we offer using the Software
that exists as of the date we make the Software available; or
3. offers the same or substantially similar functionality as the Software.
Permitted Purposes specifically include using the Software:
1. for your internal use and access;
2. for non-commercial education;
3. for non-commercial research; and
4. in connection with professional services that you provide to a licensee
using the Software in accordance with these Terms and Conditions.
### Patents
To the extent your use for a Permitted Purpose would necessarily infringe our
patents, the license grant above includes a license under our patents. If you
make a claim against any party that the Software infringes or contributes to
the infringement of any patent, then your patent license to the Software ends
immediately.
### Redistribution
The Terms and Conditions apply to all copies, modifications and derivatives of
the Software.
If you redistribute any copies, modifications or derivatives of the Software,
you must include a copy of or a link to these Terms and Conditions and not
remove any copyright notices provided in or with the Software.
### Disclaimer
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
### Trademarks
Except for displaying the License Details and identifying us as the origin of
the Software, you have no right under these Terms and Conditions to use our
trademarks, trade names, service marks or product names.
## Grant of Future License
We hereby irrevocably grant you an additional license to use the Software under
the Apache License, Version 2.0 that is effective on the second anniversary of
the date we make the Software available. On or after that date, you may use the
Software under the Apache License, Version 2.0, in which case the following
will apply:
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.

View File

@ -2,14 +2,26 @@
Softnet is a software networking for [Tart](https://github.com/cirruslabs/tart) which provides better network isolation and alleviates DHCP shortage on production systems.
It is essentially a userspace packet filter which restricts the VM networking and prevents a class of security issues, such as ARP spoofing. By default, the VM will only be able to:
* send traffic from its own MAC-address
* send traffic from the IP-address assigned to it by the DHCP
* send traffic to globally routable IPv4 addresses
* send traffic to gateway IP of the vmnet bridge (this would normally be \"bridge100\" interface)
* receive any incoming traffic
In addition, Softnet tunes macOS built-in DHCP server to decrease its lease time from the default 86,400 seconds (one day) to 600 seconds (10 minutes). This is especially important when you use Tart to clone and run a lot of ephemeral VMs over a period of one day.
Please check out [this blog post](https://cirrus-ci.org/blog/2022/07/07/isolating-network-between-tarts-macos-virtual-machines/) for backstory.
## 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
* [`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 by using tools that enable conducting the [ARP spoofing attacks](https://en.wikipedia.org/wiki/ARP_spoofing) (e.g. [arpspoof](https://www.monkey.org/~dugsong/dsniff/), [arpoison](http://www.arpoison.net/) and so on)
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
* 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:
@ -18,16 +30,60 @@ And assumes that:
...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.
### Stateful flow authorization
Stateful `in`/`out` rules use a bounded authorization cache that records the
direction and exact transport tuple of policy-approved flows, allowing matching
return traffic without treating it as a new flow and thus requiring a separate
policy entry. It is not a complete TCP connection tracker: endpoint transport
stacks remain responsible for validating sequence numbers, receive windows,
resets, and application-level traffic.
This cache deliberately favors security, bounded resource use, and a simple
implementation over availability. Only policy-authorized initiator traffic
renews an entry; return traffic does not. Softnet does not maintain fairness
quotas, eviction heuristics, or complete TCP lifecycle state.
A packet admitted by a stateful rule is denied when Softnet cannot represent its
flow, including when the cache is full. High flow churn, long idle connections,
or ambiguous retransmissions may therefore interrupt networking and require the
affected VM to reconnect.
For TCP, a bare TCP SYN on an existing tuple is deliberately returned to policy
because Softnet cannot distinguish a retransmission from tuple reuse without
tracking TCP sequence state. If authorized, it may replace the tuple's previous
cache lifetime; this can reduce availability but cannot grant traffic that
policy did not permit.
For ICMP, stateful flow authorization supports only echo requests and replies.
## Installing
For proper functioning Softnet binary requires two things:
For proper functioning, Softnet binary requires two things:
* a [SUID-bit](https://en.wikipedia.org/wiki/Setuid#SUID) is set on the binary or [passwordless sudo](https://serverfault.com/questions/160581/how-to-setup-passwordless-sudo-on-linux) is enabled, which effectively gives the binary `root` privileges
* this is needed in order to create [`vmnet.framework`](https://developer.apple.com/documentation/vmnet) interface and perform DHCP-related tweaks
* 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
* these privileges are needed to create [`vmnet.framework`](https://developer.apple.com/documentation/vmnet) interface and perform DHCP-related system tweaks
* the privileges will be dropped automatically to that of the calling user (or those represented by the `--user` and `--group` command-line arguments) once all of the initialization is completed
* the binary is available in `PATH`
* the binary 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`.
Softnet is started and managed automatically by Tart if `--net-softnet` flag is provided when calling `tart run`.
### Dynamic network policy
Softnet can update the running VM's IPv4 policy without restarting the VM. Pass a connected Unix stream socket as `--control-fd` to enable a newline-delimited [JSON-RPC 2.0](https://www.jsonrpc.org/specification) control channel. The socket is duplex and must be separate from `--vm-fd`, which carries VM packets.
The supported methods are `softnet.policy.get` and `softnet.policy.set`. A complete policy update looks like this (each request and response occupies one line):
```json
{"jsonrpc":"2.0","id":"42","method":"softnet.policy.set","params":{"allow":["@host","10.0.0.0/8"],"block":["0.0.0.0/0"]}}
{"jsonrpc":"2.0","id":"42","result":{"allow":["10.0.0.0/8","@host"],"block":["0.0.0.0/0"],"ruleCount":3}}
```
Every request must include a non-null string (at most 256 bytes) or non-negative integer `id`; notifications are rejected so policy changes always have an acknowledgment. Policy updates are atomic: all rules are parsed and a new prefix map is built before the active policy changes. Longest-prefix matching and block precedence for identical rules are preserved. Rules are normalized and deduplicated. A policy update may contain at most 4096 combined allow/block rules, and a request frame may not exceed 1 MiB.
When the normalized allow or block policy changes, Softnet clears the flow table so the new policy applies to established flows immediately. This may interrupt active connections. Repeating the same normalized policy is a no-op and preserves the flow table.
Use `block=["0.0.0.0/0"]` with specific allow rules for a default-deny egress policy. Closing the control socket leaves the last accepted policy active.

View File

@ -1,15 +1,25 @@
use dhcproto::v4::{DhcpOption, MessageType, OptionCode};
use dhcproto::Decodable;
use dhcproto::v4::{DhcpOption, HType, Message, MessageType, Opcode, OptionCode};
use smoltcp::wire::Ipv4Address;
use std::collections::HashSet;
use std::time::{Duration, Instant};
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);
@ -18,6 +28,14 @@ impl DhcpSnooper {
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) {
@ -26,17 +44,18 @@ impl DhcpSnooper {
};
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())),
),
Some(DhcpOption::DomainNameServer(dns_ips)) => {
HashSet::from_iter(dns_ips.iter().cloned())
}
_ => HashSet::new(),
};
self.vm_lease = Some(Lease::new(
message.yiaddr().into(),
Duration::from_secs(*lease_time as u64),
dns_ips,
))
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;
@ -45,10 +64,20 @@ impl DhcpSnooper {
};
}
#[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);
@ -58,22 +87,96 @@ impl DhcpSnooper {
}
}
#[derive(Debug)]
pub struct Lease {
address: Ipv4Address,
valid_until: Instant,
valid_until: coarsetime::Instant,
dns_ips: HashSet<Ipv4Address>,
}
impl Lease {
fn new(address: Ipv4Address, lease_time: Duration, dns_ips: HashSet<Ipv4Address>) -> Lease {
pub fn new(address: Ipv4Address, lease_time: Duration, dns_ips: HashSet<Ipv4Address>) -> Lease {
Lease {
address,
valid_until: Instant::now() + lease_time,
valid_until: coarsetime::Instant::recent() + lease_time.into(),
dns_ips,
}
}
pub fn valid_ip_source(&self, address: Ipv4Address) -> bool {
self.address == address && Instant::now() < self.valid_until
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());
}
}

View File

@ -1,12 +1,27 @@
use crate::{Error, Result};
use anyhow::{Context, Result, anyhow};
use clap::ValueEnum;
use log::info;
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 std::sync::mpsc::{SyncSender, sync_channel};
use vmnet::mode::Mode;
use vmnet::parameters::{Parameter, ParameterKind};
use vmnet::{Events, Options};
use vmnet::port_forwarding::{AddressFamily, Protocol};
use vmnet::{Batch, Events, Options};
#[derive(ValueEnum, Clone, Debug)]
pub enum NetType {
/// Shared network
///
/// Uses NAT-translation to give guests access to the global network
Nat,
/// Host network
///
/// Guests will be able to talk only to the host without access to global network
Host,
}
pub struct Host {
interface: vmnet::Interface,
@ -14,47 +29,66 @@ pub struct Host {
callback_can_continue_tx: SyncSender<()>,
pub gateway_ip: smoltcp::wire::Ipv4Address,
pub max_packet_size: u64,
pub read_max_packets: u64,
finalized: bool,
}
impl Host {
pub fn new() -> Result<Host> {
// Initialize a vmnet.framework NAT interface with isolation enabled
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(
Mode::Shared(Default::default()),
match vm_net_type {
NetType::Nat => Mode::Shared(Default::default()),
NetType::Host => Mode::Host(Default::default()),
},
Options {
enable_isolation: Some(true),
enable_isolation: Some(enable_isolation),
..Default::default()
},
)
.map_err(|err| Error::VmnetFailed { source: err })?;
.context("failed to initialize vmnet interface")?;
// 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 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).map_err(|_| Error::VmnetUnexpected)?;
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 max_packet_size = match interface.parameters().get(ParameterKind::MaxPacketSize) {
Some(Parameter::MaxPacketSize(max_packet_size)) => max_packet_size,
_ => return Err(Error::VmnetUnexpected),
let Some(Parameter::MaxPacketSize(max_packet_size)) =
interface.parameters().get(ParameterKind::MaxPacketSize)
else {
return Err(anyhow!(
"failed to retrieve vmnet's interface max packet size"
));
};
// Retrieve read max packets for this interface
let Some(Parameter::ReadMaxPackets(read_max_packets)) =
interface.parameters().get(ParameterKind::ReadMaxPackets)
else {
return Err(anyhow!(
"failed to retrieve vmnet's interface read max packets"
));
};
// 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 (new_packets_tx, new_packets_rx) = UnixDatagram::pair()?;
new_packets_rx.set_nonblocking(true)?;
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();
// and ignore the error as this merely a signalling channel to wake up
// the poller
new_packets_tx.send(&[0; 1]).ok();
// Wait for the permission to continue to avoid
// wasting CPU cycles or in case of termination,
@ -64,28 +98,60 @@ impl Host {
// [1]: https://en.wikipedia.org/wiki/Blocks_(C_language_extension)
callback_can_continue_rx.recv().unwrap();
})
.map_err(|err| Error::VmnetFailed { source: err })?;
.context("failed to set vmnet interface's event callback")?;
Ok(Host {
interface,
new_packets_rx,
callback_can_continue_tx,
gateway_ip: gateway_ip.into(),
gateway_ip,
max_packet_size,
read_max_packets,
finalized: false,
})
}
}
impl Host {
pub fn read(&mut self, buf: &mut [u8]) -> vmnet::Result<usize> {
pub fn port_forwarding_add_rule(
&mut self,
external_port: u16,
internal_addr: Ipv4Addr,
internal_port: u16,
) -> Result<()> {
let details = format!(
"external_port={external_port}, internal_addr={internal_addr}, internal_port={internal_port}"
);
self.interface
.port_forwarding_rule_add(
AddressFamily::Ipv4,
Protocol::Tcp,
external_port,
internal_addr.into(),
internal_port,
)
.map(|_| info!("added port forwarding rule {details}"))
.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
// 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);
let result = self.interface.read_batch(batch, bufs);
if let Err(vmnet::Error::VmnetReadNothing) = result {
// We've emptied everything, unlock the callback
@ -104,14 +170,14 @@ impl Host {
// 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 })?;
.context("failed to clear vmnet interface's event callback")?;
// Now let the callback finish
self.callback_can_continue_tx.send(()).unwrap();
let _ = self.callback_can_continue_tx.send(());
self.interface
.finalize()
.map_err(|err| Error::VmnetFailed { source: err })?;
.context("failed to finalize vmnet's interface")?;
self.finalized = true;

View File

@ -1,30 +1,6 @@
mod dhcp_snooper;
mod host;
pub use host::NetType;
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>;

View File

@ -1,13 +1,18 @@
use crate::{Error, Result};
use anyhow::Result;
use num_enum::IntoPrimitive;
use polling::PollMode;
use polling::os::kqueue::PollerKqueueExt;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::os::unix::io::RawFd;
use std::time::Duration;
pub struct Poller {
pub struct Poller<'poller> {
poller: polling::Poller,
events: Vec<polling::Event>,
vm_fd: RawFd,
host_fd: RawFd,
events: polling::Events,
timeout: Duration,
vm_fd: BorrowedFd<'poller>,
host_fd: BorrowedFd<'poller>,
control_fd: Option<BorrowedFd<'poller>>,
}
#[derive(IntoPrimitive)]
@ -15,55 +20,86 @@ pub struct Poller {
enum EventKey {
VM,
Host,
Control,
Interrupt,
}
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() })?;
impl Poller<'_> {
pub fn new<'poller>(
vm_fd: RawFd,
host_fd: RawFd,
control_fd: Option<RawFd>,
timeout: Duration,
) -> Result<Poller<'poller>> {
let poller = polling::Poller::new()?;
Ok(Poller {
poller,
events: Vec::new(),
vm_fd,
host_fd,
events: polling::Events::new(),
timeout,
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) }),
})
}
pub fn arm(&self) -> Result<()> {
self.poller
.add(self.vm_fd as RawFd, self.vm_interest())
.map_err(|err| Error::PollFailed { source: err })?;
unsafe {
self.poller.add_with_mode(
self.vm_fd.as_raw_fd(),
self.vm_interest(),
PollMode::Edge,
)?;
if let Some(control_fd) = self.control_fd {
self.poller.add_with_mode(
control_fd.as_raw_fd(),
polling::Event::all(EventKey::Control.into()),
PollMode::Edge,
)?;
}
self.poller.add_with_mode(
self.host_fd.as_raw_fd(),
self.host_interest(),
PollMode::Edge,
)?;
}
let interrupt_signal = polling::os::kqueue::Signal(libc::SIGINT);
self.poller
.add(self.host_fd as RawFd, self.host_interest())
.map_err(|err| Error::PollFailed { source: err })?;
.add_filter(interrupt_signal, EventKey::Interrupt.into(), PollMode::Edge)?;
Ok(())
}
pub fn rearm(&mut self) -> Result<()> {
pub fn rearm(&mut self) {
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 })?;
pub fn wait(&mut self) -> Result<(bool, bool, bool)> {
self.poller.wait(&mut self.events, Some(self.timeout))?;
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());
let vm_readable = self
.events
.iter()
.any(|ev| ev.key == Into::<usize>::into(EventKey::VM));
let host_readable = self
.events
.iter()
.any(|ev| ev.key == Into::<usize>::into(EventKey::Host));
let interrupt = self
.events
.iter()
.any(|ev| ev.key == Into::<usize>::into(EventKey::Interrupt));
Ok((vm_readable, host_readable, interrupt))
}
Ok((vm_readable, host_readable))
pub fn remove_control(&mut self) -> Result<()> {
if let Some(control_fd) = self.control_fd.take() {
self.poller.delete(control_fd)?;
}
Ok(())
}
fn vm_interest(&self) -> polling::Event {

1153
lib/proxy/control.rs Normal file

File diff suppressed because it is too large Load Diff

47
lib/proxy/exposed_port.rs Normal file
View File

@ -0,0 +1,47 @@
use anyhow::{Context, Error, anyhow};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct ExposedPort {
pub external_port: u16,
pub internal_port: u16,
}
impl FromStr for ExposedPort {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let splits: Vec<&str> = s.split(':').collect();
match splits.len() {
2 => Ok(ExposedPort {
external_port: splits[0]
.parse()
.context(format!("invalid external port {:?}", splits[0]))?,
internal_port: splits[1]
.parse()
.context(format!("invalid internal port {:?}", splits[1]))?,
}),
_ => Err(anyhow!(
"invalid exposed port specification {:?}, the format should be EXTERNAL:INTERNAL",
s
)),
}
}
}
#[cfg(test)]
mod tests {
use crate::proxy::exposed_port::ExposedPort;
#[test]
fn exposed_port() {
assert_eq!(
ExposedPort {
external_port: 2222,
internal_port: 22
},
"2222:22".parse().unwrap()
);
}
}

173
lib/proxy/flows/icmp.rs Normal file
View File

@ -0,0 +1,173 @@
use super::{FlowDirection, FlowKey, FlowMatch, FlowTable};
use coarsetime::{Duration, Instant};
use smoltcp::wire::{Icmpv4Message, Icmpv4Packet, Ipv4Address, Ipv4Packet};
const ECHO_TIMEOUT: Duration = Duration::from_secs(30);
impl FlowTable {
pub(super) fn inspect_icmp(
&mut self,
ipv4_pkt: &Ipv4Packet<&[u8]>,
direction: FlowDirection,
now: Instant,
) -> FlowMatch {
let Ok(icmp) = Icmpv4Packet::new_checked(ipv4_pkt.payload()) else {
return FlowMatch::Denied;
};
if !icmp.verify_checksum() {
return FlowMatch::Denied;
}
match (icmp.msg_type(), icmp.msg_code()) {
(Icmpv4Message::EchoRequest, 0) => {
self.inspect_echo(ipv4_pkt, direction, icmp.echo_ident(), true, now)
}
(Icmpv4Message::EchoReply, 0) => {
self.inspect_echo(ipv4_pkt, direction, icmp.echo_ident(), false, now)
}
// Other ICMP messages follow normal source policy: treating errors that quote
// tracked tuples as RELATED requires validation beyond this exact-tuple table
//
// Potential degradation of PMTU discovery and traceroute is an accepted tradeoff.
_ => FlowMatch::Untracked,
}
}
fn inspect_echo(
&mut self,
ipv4_pkt: &Ipv4Packet<&[u8]>,
direction: FlowDirection,
ident: u16,
is_request: bool,
now: Instant,
) -> FlowMatch {
// We need to preserve the request direction so opposite-direction echo flows remain distinct
let initiating_direction = if is_request {
direction
} else {
direction.opposite()
};
let key = FlowKey::icmp_echo(
ipv4_pkt.src_addr(),
ipv4_pkt.dst_addr(),
direction,
initiating_direction,
ident,
);
if let Some(matched) = self.match_existing_flow(key, direction, now, ECHO_TIMEOUT) {
return matched;
}
if is_request {
FlowMatch::candidate(key, initiating_direction, now, ECHO_TIMEOUT)
} else {
// Only replies matching an admitted request are tracked
FlowMatch::Untracked
}
}
}
impl FlowKey {
fn icmp_echo(
src_addr: Ipv4Address,
dst_addr: Ipv4Address,
direction: FlowDirection,
initiating_direction: FlowDirection,
ident: u16,
) -> Self {
let (host_addr, vm_addr) = direction.host_vm_pair(src_addr, dst_addr);
Self::IcmpEcho {
host_addr,
vm_addr,
ident,
initiating_direction,
}
}
}
#[cfg(test)]
mod tests {
use super::super::test_support::{HOST, VM, inspect_from_host, inspect_from_vm, ipv4_packet};
use super::super::{FlowMatch, FlowTable};
use smoltcp::wire::{Icmpv4Message, Icmpv4Packet, IpProtocol, Ipv4Address};
#[test]
fn echo_request_and_reply_are_tracked_in_both_directions() {
let mut tracker = FlowTable::new();
let vm_request = echo_packet(VM, HOST, Icmpv4Message::EchoRequest, 7, 1);
let FlowMatch::Candidate(pending) = inspect_from_vm(&mut tracker, &vm_request) else {
panic!("expected a new flow");
};
assert!(tracker.commit(pending));
let host_reply = echo_packet(HOST, VM, Icmpv4Message::EchoReply, 7, 1);
assert!(matches!(
inspect_from_host(&mut tracker, &host_reply),
FlowMatch::Allowed
));
let wrong_ident = echo_packet(HOST, VM, Icmpv4Message::EchoReply, 8, 1);
assert!(matches!(
inspect_from_host(&mut tracker, &wrong_ident),
FlowMatch::Untracked
));
// The opposite direction is a distinct flow even with the same identifier.
let host_request = echo_packet(HOST, VM, Icmpv4Message::EchoRequest, 7, 1);
let FlowMatch::Candidate(pending) = inspect_from_host(&mut tracker, &host_request) else {
panic!("expected a new flow");
};
assert!(tracker.commit(pending));
let vm_reply = echo_packet(VM, HOST, Icmpv4Message::EchoReply, 7, 1);
assert!(matches!(
inspect_from_vm(&mut tracker, &vm_reply),
FlowMatch::Allowed
));
}
#[test]
fn invalid_echo_is_denied_and_unsolicited_echo_is_untracked() {
let mut tracker = FlowTable::new();
let unsolicited = echo_packet(HOST, VM, Icmpv4Message::EchoReply, 7, 1);
assert!(matches!(
inspect_from_host(&mut tracker, &unsolicited),
FlowMatch::Untracked
));
let mut wrong_code = echo_packet(HOST, VM, Icmpv4Message::EchoRequest, 7, 1);
wrong_code[21] = 1;
Icmpv4Packet::new_unchecked(&mut wrong_code[20..]).fill_checksum();
assert!(matches!(
inspect_from_host(&mut tracker, &wrong_code),
FlowMatch::Untracked
));
let mut bad_checksum = echo_packet(HOST, VM, Icmpv4Message::EchoRequest, 7, 1);
bad_checksum[27] ^= 1;
assert!(matches!(
inspect_from_host(&mut tracker, &bad_checksum),
FlowMatch::Denied
));
}
fn echo_packet(
src_addr: Ipv4Address,
dst_addr: Ipv4Address,
message: Icmpv4Message,
ident: u16,
sequence: u16,
) -> Vec<u8> {
let mut bytes = ipv4_packet(src_addr, dst_addr, IpProtocol::Icmp, 8);
let mut icmp = Icmpv4Packet::new_unchecked(&mut bytes[20..]);
icmp.set_msg_type(message);
icmp.set_msg_code(0);
icmp.set_echo_ident(ident);
icmp.set_echo_seq_no(sequence);
icmp.fill_checksum();
bytes
}
}

394
lib/proxy/flows/mod.rs Normal file
View File

@ -0,0 +1,394 @@
mod icmp;
mod tcp;
mod udp;
use coarsetime::{Duration, Instant};
use smoltcp::wire::{IpProtocol, Ipv4Address, Ipv4Packet};
use std::collections::HashMap;
const MAX_FLOWS: usize = 32_768;
const SWEEP_INTERVAL: Duration = Duration::from_secs(1);
/// A bounded cache of exact-tuple permissions for return traffic.
///
/// The table establishes only the direction in which a flow was authorized.
/// This is deliberately not a TCP state machine. Endpoint transport stacks remain
/// responsible for TCP handshakes, teardown, sequence numbers, and receive windows,
/// as well as ICMP echo sequences.
///
/// Capacity is intentionally enforced with one fail-closed limit per VM.
/// Exhaustion may deny further networking for that VM; this availability
/// tradeoff is accepted to avoid fairness quotas, admission scans, and eviction.
#[derive(Debug)]
pub(crate) struct FlowTable {
flows: HashMap<FlowKey, Flow>,
next_sweep: Instant,
}
/// A proposed flow-table entry awaiting policy authorization.
pub(crate) struct PendingFlow {
key: FlowKey,
flow: Flow,
}
/// Metadata stored for an admitted flow.
#[derive(Clone, Copy, Debug)]
struct Flow {
initiating_direction: FlowDirection,
expires_at: Instant,
}
/// The result of classifying a packet against the flow table.
pub(crate) enum FlowMatch {
/// The packet is exact-tuple return traffic for an admitted flow.
Allowed,
/// Policy must authorize this initiator-side packet before committing it.
Candidate(PendingFlow),
/// The packet is malformed or violates a transport invariant that we deliberately enforce.
Denied,
/// The flow table has no stateful interpretation for this packet.
Untracked,
}
impl FlowMatch {
fn candidate(
key: FlowKey,
initiating_direction: FlowDirection,
now: Instant,
timeout: Duration,
) -> Self {
Self::Candidate(PendingFlow {
key,
flow: Flow {
initiating_direction,
expires_at: now + timeout,
},
})
}
}
/// The canonical identity used to index a flow-table entry.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum FlowKey {
IcmpEcho {
host_addr: Ipv4Address,
vm_addr: Ipv4Address,
ident: u16,
initiating_direction: FlowDirection,
},
Tcp {
host_addr: Ipv4Address,
host_port: u16,
vm_addr: Ipv4Address,
vm_port: u16,
},
Udp {
host_addr: Ipv4Address,
host_port: u16,
vm_addr: Ipv4Address,
vm_port: u16,
},
}
/// A packet direction across the hostVM boundary.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(super) enum FlowDirection {
FromHost,
FromVm,
}
impl FlowTable {
pub(crate) fn new() -> Self {
Self {
flows: HashMap::new(),
next_sweep: Instant::recent() + SWEEP_INTERVAL,
}
}
pub(super) fn inspect(
&mut self,
ipv4_pkt: &Ipv4Packet<&[u8]>,
direction: FlowDirection,
) -> FlowMatch {
self.inspect_at(ipv4_pkt, direction, Instant::recent())
}
fn inspect_at(
&mut self,
ipv4_pkt: &Ipv4Packet<&[u8]>,
direction: FlowDirection,
now: Instant,
) -> FlowMatch {
// Perform lazy garbage collection of expired flow entries
self.sweep_if_due(now);
// Later fragments do not contain enough transport information to bind
// them to an exact flow, so packet policy must decide
if ipv4_pkt.more_frags() || ipv4_pkt.frag_offset() != 0 {
return FlowMatch::Untracked;
}
match ipv4_pkt.next_header() {
IpProtocol::Icmp => self.inspect_icmp(ipv4_pkt, direction, now),
IpProtocol::Tcp => self.inspect_tcp(ipv4_pkt, direction, now),
IpProtocol::Udp => self.inspect_udp(ipv4_pkt, direction, now),
_ => FlowMatch::Untracked,
}
}
pub(crate) fn commit(&mut self, pending: PendingFlow) -> bool {
let PendingFlow { key, flow } = pending;
// Reject new entries when full while allowing existing entries to be updated
if !self.flows.contains_key(&key) && self.flows.len() >= MAX_FLOWS {
return false;
}
self.flows.insert(key, flow);
true
}
pub(crate) fn clear(&mut self) {
self.flows.clear();
}
fn sweep_if_due(&mut self, now: Instant) {
if now < self.next_sweep {
return;
}
self.flows.retain(|_, flow| now < flow.expires_at);
self.next_sweep = now + SWEEP_INTERVAL;
}
fn match_existing_flow(
&self,
key: FlowKey,
direction: FlowDirection,
now: Instant,
timeout: Duration,
) -> Option<FlowMatch> {
let existing = self.flows.get(&key).copied()?;
// Treat expired entries as missing even before the next sweep
if now >= existing.expires_at {
return None;
}
if direction == existing.initiating_direction {
Some(FlowMatch::candidate(
key,
existing.initiating_direction,
now,
timeout,
))
} else {
Some(FlowMatch::Allowed)
}
}
}
impl FlowDirection {
fn host_vm_pair<T>(self, src: T, dst: T) -> (T, T) {
match self {
Self::FromHost => (src, dst),
Self::FromVm => (dst, src),
}
}
fn opposite(self) -> Self {
match self {
Self::FromHost => Self::FromVm,
Self::FromVm => Self::FromHost,
}
}
}
#[cfg(test)]
mod tests {
use super::test_support::{HOST, VM, inspect_from_host, ipv4_packet, udp_packet};
use super::{
Duration, FlowDirection, FlowMatch, FlowTable, Instant, MAX_FLOWS, SWEEP_INTERVAL,
};
use smoltcp::wire::{IpProtocol, Ipv4Packet};
#[test]
fn all_ipv4_fragments_are_untracked() {
for (more_fragments, offset) in [(true, 0), (false, 8)] {
let mut bytes = udp_packet(HOST, 50_000, VM, 53);
let mut packet = Ipv4Packet::new_unchecked(bytes.as_mut_slice());
packet.set_frag_offset(offset);
packet.set_more_frags(more_fragments);
let mut tracker = FlowTable::new();
assert!(matches!(
inspect_from_host(&mut tracker, &bytes),
FlowMatch::Untracked
));
}
}
#[test]
fn expired_tuple_is_not_matched_before_the_next_sweep() {
let mut tracker = FlowTable::new();
let datagram = udp_packet(HOST, 50_000, VM, 53);
let FlowMatch::Candidate(pending) = inspect_from_host(&mut tracker, &datagram) else {
panic!("expected a candidate");
};
assert!(tracker.commit(pending));
let now = Instant::recent();
let flow = tracker.flows.values_mut().next().unwrap();
flow.expires_at = now - Duration::from_secs(1);
tracker.next_sweep = now + SWEEP_INTERVAL;
let reply = udp_packet(VM, 53, HOST, 50_000);
assert!(matches!(
tracker.inspect_at(
&Ipv4Packet::new_checked(reply.as_slice()).unwrap(),
FlowDirection::FromVm,
now,
),
FlowMatch::Candidate(_)
));
}
#[test]
fn global_limit_rejects_new_tuple_but_allows_replacement() {
let mut tracker = FlowTable::new();
for port in 10_000..10_000 + MAX_FLOWS as u16 {
let datagram = udp_packet(HOST, port, VM, 53);
let FlowMatch::Candidate(pending) = inspect_from_host(&mut tracker, &datagram) else {
panic!("expected a candidate");
};
assert!(tracker.commit(pending));
}
let over_limit = udp_packet(HOST, 50_000, VM, 53);
let FlowMatch::Candidate(pending) = inspect_from_host(&mut tracker, &over_limit) else {
panic!("expected a candidate");
};
assert!(!tracker.commit(pending));
let replacement = udp_packet(HOST, 10_000, VM, 53);
let FlowMatch::Candidate(pending) = inspect_from_host(&mut tracker, &replacement) else {
panic!("expected a candidate");
};
assert!(tracker.commit(pending));
}
#[test]
fn unsupported_protocol_is_untracked() {
let bytes = ipv4_packet(HOST, VM, IpProtocol::Unknown(253), 0);
let mut tracker = FlowTable::new();
assert!(matches!(
inspect_from_host(&mut tracker, &bytes),
FlowMatch::Untracked
));
}
}
#[cfg(test)]
mod test_support {
use super::{FlowDirection, FlowMatch, FlowTable};
use smoltcp::wire::{IpProtocol, Ipv4Address, Ipv4Packet, TcpPacket, UdpPacket};
pub(super) const HOST: Ipv4Address = Ipv4Address::new(192, 168, 64, 1);
pub(super) const VM: Ipv4Address = Ipv4Address::new(192, 168, 64, 2);
pub(super) fn inspect_from_host(tracker: &mut FlowTable, bytes: &[u8]) -> FlowMatch {
let packet = Ipv4Packet::new_checked(bytes).unwrap();
tracker.inspect(&packet, FlowDirection::FromHost)
}
pub(super) fn inspect_from_vm(tracker: &mut FlowTable, bytes: &[u8]) -> FlowMatch {
let packet = Ipv4Packet::new_checked(bytes).unwrap();
tracker.inspect(&packet, FlowDirection::FromVm)
}
#[derive(Clone, Copy)]
pub(super) struct TcpFlags {
syn: bool,
ack: bool,
rst: bool,
fin: bool,
}
impl TcpFlags {
pub(super) const SYN: Self = Self {
syn: true,
ack: false,
rst: false,
fin: false,
};
pub(super) const SYN_ACK: Self = Self {
syn: true,
ack: true,
rst: false,
fin: false,
};
pub(super) const ACK: Self = Self {
syn: false,
ack: true,
rst: false,
fin: false,
};
}
pub(super) fn tcp_packet(
src_addr: Ipv4Address,
src_port: u16,
dst_addr: Ipv4Address,
dst_port: u16,
flags: TcpFlags,
) -> Vec<u8> {
let mut bytes = ipv4_packet(src_addr, dst_addr, IpProtocol::Tcp, 20);
let mut tcp = TcpPacket::new_unchecked(&mut bytes[20..]);
tcp.set_src_port(src_port);
tcp.set_dst_port(dst_port);
tcp.set_header_len(20);
tcp.set_syn(flags.syn);
tcp.set_ack(flags.ack);
tcp.set_rst(flags.rst);
tcp.set_fin(flags.fin);
tcp.set_window_len(u16::MAX);
bytes
}
pub(super) fn udp_packet(
src_addr: Ipv4Address,
src_port: u16,
dst_addr: Ipv4Address,
dst_port: u16,
) -> Vec<u8> {
let mut bytes = ipv4_packet(src_addr, dst_addr, IpProtocol::Udp, 8);
let mut udp = UdpPacket::new_unchecked(&mut bytes[20..]);
udp.set_src_port(src_port);
udp.set_dst_port(dst_port);
udp.set_len(8);
bytes
}
pub(super) fn ipv4_packet(
src_addr: Ipv4Address,
dst_addr: Ipv4Address,
protocol: IpProtocol,
payload_len: usize,
) -> Vec<u8> {
let mut bytes = vec![0; 20 + payload_len];
let total_len = bytes.len() as u16;
let mut ipv4 = Ipv4Packet::new_unchecked(bytes.as_mut_slice());
ipv4.set_version(4);
ipv4.set_header_len(20);
ipv4.set_total_len(total_len);
ipv4.set_next_header(protocol);
ipv4.set_src_addr(src_addr);
ipv4.set_dst_addr(dst_addr);
bytes
}
}

156
lib/proxy/flows/tcp.rs Normal file
View File

@ -0,0 +1,156 @@
use super::{FlowDirection, FlowKey, FlowMatch, FlowTable};
use coarsetime::{Duration, Instant};
use smoltcp::wire::{Ipv4Address, Ipv4Packet, TcpPacket};
const SYN_TIMEOUT: Duration = Duration::from_secs(60);
const TCP_TIMEOUT: Duration = Duration::from_secs(5 * 60);
impl FlowTable {
pub(super) fn inspect_tcp(
&mut self,
ipv4_pkt: &Ipv4Packet<&[u8]>,
direction: FlowDirection,
now: Instant,
) -> FlowMatch {
let Ok(tcp) = TcpPacket::new_checked(ipv4_pkt.payload()) else {
return FlowMatch::Denied;
};
if tcp.src_port() == 0 || tcp.dst_port() == 0 {
return FlowMatch::Denied;
}
let key = FlowKey::tcp(
direction,
(ipv4_pkt.src_addr(), tcp.src_port()),
(ipv4_pkt.dst_addr(), tcp.dst_port()),
);
// A bare SYN may be either a retransmission or a new connection reusing
// the tuple in either direction. Always return it to policy, and do not
// replace an existing permission until the candidate is committed.
let is_initial_syn = tcp.syn() && !tcp.ack() && !tcp.fin() && !tcp.rst();
if is_initial_syn {
return FlowMatch::candidate(key, direction, now, SYN_TIMEOUT);
}
self.match_existing_flow(key, direction, now, TCP_TIMEOUT)
.unwrap_or(FlowMatch::Untracked)
}
}
impl FlowKey {
fn tcp(direction: FlowDirection, src: (Ipv4Address, u16), dst: (Ipv4Address, u16)) -> Self {
let ((host_addr, host_port), (vm_addr, vm_port)) = direction.host_vm_pair(src, dst);
Self::Tcp {
host_addr,
host_port,
vm_addr,
vm_port,
}
}
}
#[cfg(test)]
mod tests {
use super::super::test_support::{
HOST, TcpFlags, VM, inspect_from_host, inspect_from_vm, tcp_packet,
};
use super::super::{FlowMatch, FlowTable};
fn admit_host_syn(tracker: &mut FlowTable) {
let syn = tcp_packet(HOST, 49_152, VM, 22, TcpFlags::SYN);
let FlowMatch::Candidate(pending) = inspect_from_host(tracker, &syn) else {
panic!("expected a candidate");
};
assert!(tracker.commit(pending));
}
#[test]
fn admitted_syn_creates_an_exact_reverse_permission() {
let mut tracker = FlowTable::new();
admit_host_syn(&mut tracker);
let original_expiry = tracker.flows.values().next().unwrap().expires_at;
let reply = tcp_packet(VM, 22, HOST, 49_152, TcpFlags::ACK);
assert!(matches!(
inspect_from_vm(&mut tracker, &reply),
FlowMatch::Allowed
));
assert_eq!(
tracker.flows.values().next().unwrap().expires_at,
original_expiry,
"return traffic must not refresh a tuple"
);
let initiator_data = tcp_packet(HOST, 49_152, VM, 22, TcpFlags::ACK);
let FlowMatch::Candidate(pending) = inspect_from_host(&mut tracker, &initiator_data) else {
panic!("expected an initiator candidate");
};
assert_eq!(
tracker.flows.values().next().unwrap().expires_at,
original_expiry,
"inspection must not refresh a tuple before policy accepts it"
);
assert!(tracker.commit(pending));
assert!(tracker.flows.values().next().unwrap().expires_at > original_expiry);
}
#[test]
fn new_tuple_requires_a_clean_syn() {
let mut tracker = FlowTable::new();
for flags in [TcpFlags::ACK, TcpFlags::SYN_ACK] {
let packet = tcp_packet(HOST, 49_152, VM, 22, flags);
assert!(matches!(
inspect_from_host(&mut tracker, &packet),
FlowMatch::Untracked
));
}
let syn = tcp_packet(HOST, 49_152, VM, 22, TcpFlags::SYN);
assert!(matches!(
inspect_from_host(&mut tracker, &syn),
FlowMatch::Candidate(_)
));
}
#[test]
fn reply_requires_the_exact_tuple() {
let mut tracker = FlowTable::new();
admit_host_syn(&mut tracker);
let wrong_port = tcp_packet(VM, 22, HOST, 49_153, TcpFlags::ACK);
assert!(matches!(
inspect_from_vm(&mut tracker, &wrong_port),
FlowMatch::Untracked
));
}
#[test]
fn reverse_bare_syn_is_a_new_policy_candidate() {
let mut tracker = FlowTable::new();
admit_host_syn(&mut tracker);
let reverse_syn = tcp_packet(VM, 22, HOST, 49_152, TcpFlags::SYN);
let FlowMatch::Candidate(pending) = inspect_from_vm(&mut tracker, &reverse_syn) else {
panic!("reverse SYN must return to policy");
};
// Inspection is provisional: a policy rejection leaves the admitted
// flow untouched.
let old_flow_reply = tcp_packet(VM, 22, HOST, 49_152, TcpFlags::ACK);
assert!(matches!(
inspect_from_vm(&mut tracker, &old_flow_reply),
FlowMatch::Allowed
));
assert!(tracker.commit(pending));
let reverse_reply = tcp_packet(HOST, 49_152, VM, 22, TcpFlags::SYN_ACK);
assert!(matches!(
inspect_from_host(&mut tracker, &reverse_reply),
FlowMatch::Allowed
));
}
}

92
lib/proxy/flows/udp.rs Normal file
View File

@ -0,0 +1,92 @@
use super::{FlowDirection, FlowKey, FlowMatch, FlowTable};
use coarsetime::{Duration, Instant};
use smoltcp::wire::{Ipv4Address, Ipv4Packet, UdpPacket};
const UDP_TIMEOUT: Duration = Duration::from_secs(30);
impl FlowTable {
pub(super) fn inspect_udp(
&mut self,
ipv4_pkt: &Ipv4Packet<&[u8]>,
direction: FlowDirection,
now: Instant,
) -> FlowMatch {
let Ok(udp) = UdpPacket::new_checked(ipv4_pkt.payload()) else {
return FlowMatch::Denied;
};
if udp.dst_port() == 0 {
return FlowMatch::Denied;
}
// Deliberate policy, not merely a packet-format validation:
// RFC 8085, §5.1 says UDP senders SHOULD NOT use source port zero.
//
// We enforce this recommendation to retain source-port entropy
// and protection against off-path packet injection.
if udp.src_port() == 0 {
return FlowMatch::Denied;
}
let key = FlowKey::udp(
direction,
(ipv4_pkt.src_addr(), udp.src_port()),
(ipv4_pkt.dst_addr(), udp.dst_port()),
);
if let Some(matched) = self.match_existing_flow(key, direction, now, UDP_TIMEOUT) {
return matched;
}
FlowMatch::candidate(key, direction, now, UDP_TIMEOUT)
}
}
impl FlowKey {
fn udp(direction: FlowDirection, src: (Ipv4Address, u16), dst: (Ipv4Address, u16)) -> Self {
let ((host_addr, host_port), (vm_addr, vm_port)) = direction.host_vm_pair(src, dst);
Self::Udp {
host_addr,
host_port,
vm_addr,
vm_port,
}
}
}
#[cfg(test)]
mod tests {
use super::super::test_support::{HOST, VM, inspect_from_host, inspect_from_vm, udp_packet};
use super::super::{FlowMatch, FlowTable};
#[test]
fn rejects_zero_source_port_per_rfc_8085() {
let mut tracker = FlowTable::new();
let datagram = udp_packet(HOST, 0, VM, 5353);
assert!(matches!(
inspect_from_host(&mut tracker, &datagram),
FlowMatch::Denied
));
}
#[test]
fn udp_reply_requires_an_exact_admitted_request() {
let mut tracker = FlowTable::new();
let request = udp_packet(HOST, 50_000, VM, 5353);
let reply = udp_packet(VM, 5353, HOST, 50_000);
assert!(matches!(
inspect_from_vm(&mut tracker, &reply),
FlowMatch::Candidate(_)
));
let FlowMatch::Candidate(pending) = inspect_from_host(&mut tracker, &request) else {
panic!("expected a candidate");
};
assert!(tracker.commit(pending));
assert!(matches!(
inspect_from_vm(&mut tracker, &reply),
FlowMatch::Allowed
));
}
}

View File

@ -1,9 +1,14 @@
use crate::dhcp_snooper::message_matches_bootp_client;
use crate::proxy::flows::{FlowDirection, FlowMatch};
use crate::proxy::udp_packet_helper::UdpPacketHelper;
use crate::proxy::Proxy;
use crate::{Error, Result};
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
use crate::proxy::{Direction, PolicyDecision, Proxy};
use anyhow::{Context, Result};
use dhcproto::Decodable;
use dhcproto::v4::Opcode;
use smoltcp::phy::ChecksumCapabilities;
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, Ipv4Repr, UdpPacket};
impl Proxy {
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
@ -12,22 +17,98 @@ impl Proxy {
// Snoop bootpd(8) replies from the host to
// figure out the IP assigned to the VM
self.snoop(frame);
if frame.dst_addr() == self.vm_mac_address || frame.dst_addr().is_broadcast() {
self.snoop(frame);
}
self.vm
.write(frame.as_ref())
.map(|_| ())
.map_err(|err| Error::VMIOFailed { source: err })
match self.vm.write(frame.as_ref()) {
Ok(_) => Ok(()),
Err(err) => {
if let Some(libc::ENOBUFS) = err.raw_os_error() {
if !self.enobufs_encountered {
sentry::capture_message(
"No buffer space available in VM's socket",
sentry::Level::Warning,
);
self.enobufs_encountered = true;
}
return Ok(());
}
Err(err).context("failed to write to the VM")
}
}
}
fn allowed_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
match frame.ethertype() {
EthernetProtocol::Arp => Some(()),
EthernetProtocol::Ipv4 => Some(()),
EthernetProtocol::Ipv4 => {
let ipv4_pkt = Ipv4Packet::new_unchecked(frame.payload());
Ipv4Repr::parse(&ipv4_pkt, &ChecksumCapabilities::ignored()).ok()?;
self.allowed_from_host_ipv4(&ipv4_pkt)
}
_ => None,
}
}
pub(super) fn allowed_from_host_ipv4(&mut self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> Option<()> {
// 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()))
{
match self
.flows
.as_mut()?
.inspect(ipv4_pkt, FlowDirection::FromHost)
{
FlowMatch::Allowed => return Some(()),
FlowMatch::Denied => return None,
FlowMatch::Candidate(pending) => Some(pending),
FlowMatch::Untracked => None,
}
} else {
None
};
// The flow is either pending or untracked, evaluate it against inbound policy
match self
.rules
.policy_decision(ipv4_pkt.src_addr(), Direction::In)
{
// Return traffic was handled above; enforce explicit inbound blocks here
Some(PolicyDecision::Block) => None,
// Stateless policy is outbound-only; fail closed if this invariant is violated
Some(PolicyDecision::AllowStateless) => None,
// Untracked packets cannot satisfy stateful policy
Some(PolicyDecision::AllowStateful) => self.admit_with_tracking(pending?),
// No inbound rule matched, so allow by default. Track the flow when needed
// so its reply is not treated as a new outbound flow
None => {
self.admit_with_tracking_if_stateful(pending, ipv4_pkt.src_addr(), Direction::Out)
}
}
}
fn snoop(&mut self, frame: &EthernetFrame<&[u8]>) {
if frame.ethertype() != EthernetProtocol::Ipv4 {
return;
@ -38,11 +119,7 @@ impl Proxy {
_ => return,
};
if ipv4_pkt.src_addr() != self.host.gateway_ip {
return;
}
if ipv4_pkt.protocol() != smoltcp::wire::IpProtocol::Udp {
if !self.is_allowed_dhcp_response(&ipv4_pkt) {
return;
}
@ -51,10 +128,91 @@ impl Proxy {
Err(_) => return,
};
if !udp_pkt.is_dhcp_response() {
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;
}
self.dhcp_snooper.register_dhcp_reply(udp_pkt.payload());
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
}
}

View File

@ -1,96 +1,443 @@
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::host::Host;
use crate::host::NetType;
use crate::poller::Poller;
use crate::vm::VM;
use crate::Error;
use crate::Result;
use anyhow::Result;
use control::{Control, normalize_rules};
pub use exposed_port::ExposedPort;
use flows::{FlowTable, PendingFlow};
use ipnet::Ipv4Net;
use mac_address::MacAddress;
use smoltcp::wire::EthernetFrame;
use port_forwarder::PortForwarder;
pub use rule::{Direction, Rule, Target};
pub(crate) use rules::{PolicyDecision, Rules};
use smoltcp::wire::{EthernetFrame, Ipv4Address};
use std::io::ErrorKind;
use std::os::unix::io::{AsRawFd, RawFd};
use std::time::Duration;
use vmnet::Batch;
pub struct Proxy {
pub struct Proxy<'proxy> {
vm: VM,
host: Host,
poller: Poller,
poller: Poller<'proxy>,
vm_mac_address: smoltcp::wire::EthernetAddress,
dhcp_snooper: DhcpSnooper,
rules: Rules,
control: Option<Control>,
flows: Option<FlowTable>,
enobufs_encountered: bool,
port_forwarder: PortForwarder,
}
impl Proxy {
pub fn new(vm_fd: RawFd, vm_mac_address: MacAddress) -> Result<Proxy> {
impl Proxy<'_> {
pub fn new<'proxy>(
vm_fd: RawFd,
vm_mac_address: MacAddress,
vm_net_type: NetType,
allow: Vec<Rule>,
block: Vec<Rule>,
exposed_ports: Vec<ExposedPort>,
control_fd: Option<RawFd>,
) -> Result<Proxy<'proxy>> {
let allow = normalize_rules(allow);
let block = normalize_rules(block);
let vm = VM::new(vm_fd)?;
let host = Host::new()?;
let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd())?;
let host = Host::new(
vm_net_type,
!allow.contains(&Rule::Stateless(Target::Prefix(Ipv4Net::default()))),
)?;
let poller_timeout = Duration::from_millis(100);
let control = control_fd
.map(|control_fd| {
Control::new(control_fd, host.gateway_ip, allow.clone(), block.clone())
})
.transpose()?;
let poller = Poller::new(
vm.as_raw_fd(),
host.as_raw_fd(),
control.as_ref().map(AsRawFd::as_raw_fd),
poller_timeout,
)?;
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
let flows = rules.has_stateful().then(FlowTable::new);
Ok(Proxy {
vm,
host,
poller,
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
dhcp_snooper: Default::default(),
dhcp_snooper: DhcpSnooper::new(poller_timeout, vm_mac_address.bytes()),
rules,
control,
flows,
enobufs_encountered: false,
port_forwarder: PortForwarder::new(exposed_ports),
})
}
pub fn run(&mut self) -> Result<()> {
// Create a single buffer from reading from the VM
let mut buf: Vec<u8> = vec![0; self.host.max_packet_size as usize];
// Create multiple buffers and a batch for reading from the host
let mut bufs = vec![
vec![0u8; self.host.max_packet_size as usize];
self.host.read_max_packets as usize
];
let mut batch = Batch::preallocate(bufs.len());
self.poller.arm()?;
loop {
let (vm_readable, host_readable) = self.poller.wait()?;
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
coarsetime::Instant::update();
// Service control on every wake (including timeouts) so a bounded read or a pending
// response continues making progress even when no new edge is generated.
self.service_control();
if vm_readable {
self.read_from_vm(buf.as_mut_slice())?;
}
if host_readable {
self.read_from_host(buf.as_mut_slice())?;
self.read_from_host(&mut batch, &mut bufs)?;
}
self.poller.rearm()?;
// Graceful termination
if interrupt {
return Ok(());
}
// Timeout
if !vm_readable && !host_readable && !interrupt {
self.port_forwarder
.tick(&mut self.host, self.dhcp_snooper.lease());
}
self.poller.rearm();
}
}
fn read_from_vm(&mut self, buf: &mut [u8]) -> Result<()> {
let mut packets_read = 0;
loop {
match self.vm.read(buf) {
Ok(n) => {
// Update coarse time for DHCP snooping and flows
coarsetime::Instant::update();
if let Ok(frame) = EthernetFrame::new_checked(&buf[..n]) {
self.process_frame_from_vm(frame)?;
}
packets_read += 1;
if packets_read == 128 {
self.service_control();
packets_read = 0;
}
}
Err(err) => {
if err.kind() == ErrorKind::WouldBlock {
return Ok(());
}
return Err(Error::VMIOFailed { source: err });
return Err(err.into());
}
}
}
}
fn read_from_host(&mut self, buf: &mut [u8]) -> Result<()> {
fn read_from_host(&mut self, batch: &mut Batch, bufs: &mut [Vec<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)?;
match self.host.read(batch, bufs) {
Ok(pktcnt) => {
// Update coarse time for DHCP snooping and flows
coarsetime::Instant::update();
for buf in batch.packet_sized_bufs(bufs).take(pktcnt) {
if let Ok(pkt) = EthernetFrame::new_checked(buf) {
self.process_frame_from_host(&pkt)?;
}
}
self.service_control();
}
Err(err) => {
if let vmnet::Error::VmnetReadNothing = err {
return Ok(());
}
return Err(Error::HostIOFailed { source: err });
return Err(err.into());
}
}
}
}
fn service_control(&mut self) {
let Some(control) = self.control.as_mut() else {
return;
};
let keep_open = match control.service(&mut self.rules) {
Ok(keep_open) => keep_open,
Err(err) => {
log::warn!("disabling Softnet control socket: {err:#}");
false
}
};
// Invalidate tracked flows whenever the policy changes
if control.policy_changed() {
self.flows = self.rules.has_stateful().then(FlowTable::new);
}
if keep_open {
return;
}
if let Err(err) = self.poller.remove_control() {
log::warn!("failed to remove Softnet control socket from the poller: {err:#}");
}
if let Some(control) = self.control.take()
&& let Err(err) = control.shutdown()
{
log::warn!("failed to shut down Softnet control socket: {err:#}");
}
}
/// Commits the pending flow, rejecting the packet if the table cannot store it.
fn admit_with_tracking(&mut self, pending: PendingFlow) -> Option<()> {
self.flows.as_mut()?.commit(pending).then_some(())
}
/// Commits a pending flow for trackable packets; untracked packets proceed without one.
fn admit_with_tracking_if_trackable(&mut self, pending: Option<PendingFlow>) -> Option<()> {
match pending {
Some(pending) => self.admit_with_tracking(pending),
None => Some(()),
}
}
/// Commits a pending flow when the return-direction rule is stateful.
fn admit_with_tracking_if_stateful(
&mut self,
pending: Option<PendingFlow>,
peer_addr: Ipv4Address,
return_direction: Direction,
) -> Option<()> {
if self.rules.is_stateful(peer_addr, return_direction) {
self.admit_with_tracking_if_trackable(pending)
} else {
Some(())
}
}
}
#[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"]);
assert_eq!(proxy.rules.len(), 1);
assert!(allowed_from_vm_ipv4(&mut proxy, vm_ip, "66.66.66.66").is_none());
}
#[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"]);
assert_eq!(proxy.rules.len(), 2);
assert!(allowed_from_vm_ipv4(&mut proxy, vm_ip, "33.33.33.32").is_none());
assert!(allowed_from_vm_ipv4(&mut proxy, vm_ip, "33.33.33.33").is_some());
assert!(allowed_from_vm_ipv4(&mut proxy, vm_ip, "33.33.33.34").is_none());
}
#[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"]);
assert_eq!(proxy.rules.len(), 2);
// Access to global IPs should be disallowed because of --block=0.0.0.0/0
assert!(allowed_from_vm_ipv4(&mut proxy, vm_ip, "8.8.8.8").is_none());
// Despite the above, access to host IP address should be possible because of --allow=@host
let gateway_ip = proxy.host.gateway_ip.to_string();
assert!(allowed_from_vm_ipv4(&mut proxy, vm_ip, &gateway_ip).is_some());
}
#[test]
#[serial]
fn test_bare_default_block_applies_in_both_directions_in_stateful_mode() {
let vm_ip = Ipv4Address::new(192, 168, 0, 2);
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();
assert!(proxy.allowed_from_host_ipv4(&fallback_request).is_none());
let fallback_reply = udp_packet(vm_ip, 1_234, fallback_peer, 40_000);
let fallback_reply = Ipv4Packet::new_checked(fallback_reply.as_slice()).unwrap();
assert!(proxy.allowed_from_vm_ipv4(fallback_reply).is_none());
let allowed_request = udp_packet(explicitly_allowed_peer, 40_000, vm_ip, 1_234);
let allowed_request = Ipv4Packet::new_checked(allowed_request.as_slice()).unwrap();
assert!(proxy.allowed_from_host_ipv4(&allowed_request).is_some());
let allowed_reply = udp_packet(vm_ip, 1_234, explicitly_allowed_peer, 40_000);
let allowed_reply = Ipv4Packet::new_checked(allowed_reply.as_slice()).unwrap();
assert!(proxy.allowed_from_vm_ipv4(allowed_reply).is_some());
}
#[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 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();
assert!(proxy.allowed_from_host_ipv4(&request).is_some());
let reply = udp_packet(vm_ip, 1_234, peer, 40_000);
let reply = Ipv4Packet::new_checked(reply.as_slice()).unwrap();
assert!(proxy.allowed_from_vm_ipv4(reply).is_some());
}
#[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 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();
assert!(proxy.allowed_from_vm_ipv4(request).is_some());
let reply = udp_packet(peer, 40_000, vm_ip, 1_234);
let reply = Ipv4Packet::new_checked(reply.as_slice()).unwrap();
assert!(proxy.allowed_from_host_ipv4(&reply).is_some());
}
fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
let (vm_fd, _) = socketpair(
AddressFamily::Unix,
SockType::Datagram,
None,
SockFlag::empty(),
)
.unwrap();
let vm_fd = Box::leak(Box::new(vm_fd));
let mut proxy = Proxy::new(
vm_fd.as_raw_fd(),
MacAddress::from_str("02:00:00:00:00:01").unwrap(),
NetType::Nat,
allow
.into_iter()
.map(|value| value.parse().unwrap())
.collect(),
block
.into_iter()
.map(|value| value.parse().unwrap())
.collect(),
Vec::default(),
None,
)
.unwrap();
proxy.dhcp_snooper.set_lease(Some(Lease::new(
vm_ip,
Duration::from_secs(600),
HashSet::new(),
)));
proxy
}
fn allowed_from_vm_ipv4(proxy: &mut Proxy, src: Ipv4Address, dst: &str) -> Option<()> {
let mut buf = vec![0; 1500];
let mut ipv4_pkt_mut = Ipv4Packet::new_unchecked(&mut buf[..]);
ipv4_pkt_mut.set_src_addr(src);
ipv4_pkt_mut.set_dst_addr(Ipv4Address::from_str(dst).unwrap());
let ipv4_pkt = Ipv4Packet::new_unchecked(buf.as_slice());
proxy.allowed_from_vm_ipv4(ipv4_pkt)
}
fn udp_packet(
src_addr: Ipv4Address,
src_port: u16,
dst_addr: Ipv4Address,
dst_port: u16,
) -> Vec<u8> {
let mut bytes = vec![0; 28];
let mut ipv4 = Ipv4Packet::new_unchecked(bytes.as_mut_slice());
ipv4.set_version(4);
ipv4.set_header_len(20);
ipv4.set_total_len(28);
ipv4.set_next_header(IpProtocol::Udp);
ipv4.set_src_addr(src_addr);
ipv4.set_dst_addr(dst_addr);
let mut udp = UdpPacket::new_unchecked(ipv4.payload_mut());
udp.set_src_port(src_port);
udp.set_dst_port(dst_port);
udp.set_len(8);
bytes
}
}

View File

@ -0,0 +1,99 @@
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(())
}
}

158
lib/proxy/rule.rs Normal file
View File

@ -0,0 +1,158 @@
use ipnet::Ipv4Net;
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rule {
Stateless(Target),
Stateful {
direction: Direction,
target: Target,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Target {
Prefix(Ipv4Net),
Host,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
In,
Out,
}
impl Rule {
pub(super) fn normalized(self) -> Self {
match self {
Rule::Stateless(target) => Rule::Stateless(target.normalized()),
Rule::Stateful { direction, target } => Rule::Stateful {
direction,
target: target.normalized(),
},
}
}
}
impl FromStr for Rule {
type Err = ipnet::AddrParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let (direction, target) = if let Some(target) = input.strip_prefix("in ") {
(Direction::In, target)
} else if let Some(target) = input.strip_prefix("out ") {
(Direction::Out, target)
} else {
return input.parse().map(Rule::Stateless);
};
let target = target.trim_start_matches(' ').parse()?;
Ok(Rule::Stateful { direction, target })
}
}
impl Display for Rule {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Rule::Stateless(target) => Display::fmt(target, formatter),
Rule::Stateful { direction, target } => match direction {
Direction::In => write!(formatter, "in {target}"),
Direction::Out => write!(formatter, "out {target}"),
},
}
}
}
impl Target {
fn normalized(self) -> Self {
match self {
Target::Prefix(prefix) => Target::Prefix(prefix.trunc()),
Target::Host => Target::Host,
}
}
}
impl FromStr for Target {
type Err = ipnet::AddrParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
if input == "@host" {
Ok(Target::Host)
} else {
input.parse().map(Target::Prefix)
}
}
}
impl Display for Target {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Target::Prefix(prefix) => Display::fmt(prefix, formatter),
Target::Host => formatter.write_str("@host"),
}
}
}
#[cfg(test)]
mod tests {
use super::{Direction, Rule, Target};
use ipnet::Ipv4Net;
use std::str::FromStr;
#[test]
fn parses_stateless_target() {
assert_eq!(
"@host".parse::<Rule>().unwrap(),
Rule::Stateless(Target::Host)
);
}
#[test]
fn parses_stateful_directions() {
let private_network = Target::Prefix(Ipv4Net::from_str("10.0.0.0/8").unwrap());
assert_eq!(
"in @host".parse::<Rule>().unwrap(),
Rule::Stateful {
direction: Direction::In,
target: Target::Host,
}
);
assert_eq!(
"out 10.0.0.0/8".parse::<Rule>().unwrap(),
Rule::Stateful {
direction: Direction::Out,
target: private_network,
}
);
}
#[test]
fn displays_normalized_rules() {
for (input, expected) in [("in 10.1.2.3/8", "in 10.0.0.0/8"), ("@host", "@host")] {
assert_eq!(
input.parse::<Rule>().unwrap().normalized().to_string(),
expected
);
}
}
#[test]
fn rejects_invalid_rules() {
for input in [
"",
"from @host",
"in",
"out",
"in from @host",
"out to @host",
"infrom @host",
" in @host",
"in @host ",
"in\t@host",
] {
assert!(input.parse::<Rule>().is_err(), "{input:?} should fail");
}
}
}

279
lib/proxy/rules.rs Normal file
View File

@ -0,0 +1,279 @@
use super::{Direction, Rule, Target};
use ipnet::Ipv4Net;
use prefix_trie::PrefixMap;
use smoltcp::wire::Ipv4Address;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Action {
Block,
Allow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PolicyDecision {
Block,
AllowStateless,
AllowStateful,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
enum Mode {
#[default]
Legacy,
Stateful,
}
#[derive(Default)]
pub(crate) struct Rules {
mode: Mode,
inbound: PrefixMap<Ipv4Net, Action>,
outbound: PrefixMap<Ipv4Net, Action>,
}
impl Rules {
pub(crate) fn new(host_address: Ipv4Address, allow: &[Rule], block: &[Rule]) -> Self {
// Preserve legacy behavior for bare-only policies. Once a directional rule
// is present, compile the whole policy using directional semantics.
let mode = if allow
.iter()
.chain(block)
.any(|rule| matches!(rule, Rule::Stateful { .. }))
{
Mode::Stateful
} else {
Mode::Legacy
};
let mut rules = Self {
mode,
..Self::default()
};
for &rule in allow {
rules.insert(rule, Action::Allow, host_address);
}
// SECURITY: blocking rules must always take precedence
// over allowing rules when the rules are identical.
for &rule in block {
rules.insert(rule, Action::Block, host_address);
}
rules
}
pub(crate) fn policy_decision(
&self,
address: Ipv4Address,
direction: Direction,
) -> Option<PolicyDecision> {
match (self.select(address, direction)?, self.mode) {
(Action::Block, _) => Some(PolicyDecision::Block),
(Action::Allow, Mode::Legacy) => Some(PolicyDecision::AllowStateless),
(Action::Allow, Mode::Stateful) => Some(PolicyDecision::AllowStateful),
}
}
pub(crate) fn is_stateful(&self, address: Ipv4Address, direction: Direction) -> bool {
self.mode == Mode::Stateful && self.select(address, direction).is_some()
}
pub(crate) fn len(&self) -> usize {
self.inbound.len() + self.outbound.len()
}
pub(crate) fn has_stateful(&self) -> bool {
self.mode == Mode::Stateful
}
fn select(&self, address: Ipv4Address, direction: Direction) -> Option<Action> {
let entries = match direction {
Direction::In => &self.inbound,
Direction::Out => &self.outbound,
};
entries
.get_lpm(&Ipv4Net::from(address))
.map(|(_, action)| *action)
}
fn insert(&mut self, rule: Rule, action: Action, host_address: Ipv4Address) {
match rule {
Rule::Stateless(target) => {
// Bare rules apply in both directions in stateful mode
if self.mode == Mode::Stateful {
self.insert_direction(Direction::In, target, action, host_address);
}
self.insert_direction(Direction::Out, target, action, host_address);
}
Rule::Stateful { direction, target } => {
self.insert_direction(direction, target, action, host_address);
}
}
}
fn insert_direction(
&mut self,
direction: Direction,
target: Target,
action: Action,
host_address: Ipv4Address,
) {
let prefix = match target {
Target::Prefix(prefix) => prefix,
Target::Host => host_address.into(),
};
let entries = match direction {
Direction::In => &mut self.inbound,
Direction::Out => &mut self.outbound,
};
entries.insert(prefix, action);
}
}
#[cfg(test)]
mod tests {
use super::{Action, Mode, PolicyDecision, Rules};
use crate::proxy::Direction;
use smoltcp::wire::Ipv4Address;
const HOST: Ipv4Address = Ipv4Address::new(192, 168, 64, 1);
fn stateful_rules() -> Rules {
Rules {
mode: Mode::Stateful,
..Rules::default()
}
}
#[test]
fn test_policy_precedence() {
let target = Ipv4Address::new(10, 0, 0, 1);
let mut rules = stateful_rules();
rules.insert("0.0.0.0/0".parse().unwrap(), Action::Block, HOST);
rules.insert("in 10.0.0.0/8".parse().unwrap(), Action::Allow, HOST);
assert_eq!(
rules.policy_decision(target, Direction::In),
Some(PolicyDecision::AllowStateful)
);
assert_eq!(
rules.policy_decision(target, Direction::Out),
Some(PolicyDecision::Block)
);
rules.insert("10.0.0.1/32".parse().unwrap(), Action::Allow, HOST);
assert_eq!(
rules.policy_decision(target, Direction::Out),
Some(PolicyDecision::AllowStateful)
);
}
#[test]
fn test_directional_rules_at_same_prefix_are_independent() {
let mut rules = stateful_rules();
for (target, action) in [
("in @host", Action::Allow),
("out @host", Action::Allow),
("in @host", Action::Block),
] {
rules.insert(target.parse().unwrap(), action, HOST);
}
assert_eq!(
rules.policy_decision(HOST, Direction::In),
Some(PolicyDecision::Block)
);
assert_eq!(
rules.policy_decision(HOST, Direction::Out),
Some(PolicyDecision::AllowStateful)
);
assert_eq!(rules.len(), 2);
}
#[test]
fn test_stateless_rules_are_outbound_only() {
let target = Ipv4Address::new(10, 1, 2, 3);
let mut rules = Rules::default();
rules.insert("10.0.0.0/8".parse().unwrap(), Action::Block, HOST);
assert!(rules.policy_decision(target, Direction::In).is_none());
assert_eq!(
rules.policy_decision(target, Direction::Out),
Some(PolicyDecision::Block)
);
}
#[test]
fn test_inbound_selection_uses_more_specific_bare_rule() {
let target = Ipv4Address::new(10, 1, 2, 3);
let mut rules = stateful_rules();
rules.insert("10.1.0.0/16".parse().unwrap(), Action::Allow, HOST);
rules.insert("in 10.0.0.0/8".parse().unwrap(), Action::Block, HOST);
assert_eq!(
rules.policy_decision(target, Direction::In),
Some(PolicyDecision::AllowStateful)
);
}
#[test]
fn test_block_wins_over_allow_at_same_outbound_prefix() {
let target = Ipv4Address::new(10, 1, 2, 3);
for (allow, block) in [
("10.0.0.0/8", "out 10.0.0.0/8"),
("out 10.0.0.0/8", "10.0.0.0/8"),
] {
let mut rules = stateful_rules();
rules.insert(allow.parse().unwrap(), Action::Allow, HOST);
rules.insert(block.parse().unwrap(), Action::Block, HOST);
assert_eq!(
rules.policy_decision(target, Direction::Out),
Some(PolicyDecision::Block)
);
}
}
#[test]
fn test_directional_rule_makes_bare_rules_stateful() {
let allow = "out @host".parse().unwrap();
let block = "0.0.0.0/0".parse().unwrap();
let rules = Rules::new(HOST, &[allow], &[block]);
assert_eq!(rules.len(), 3);
assert!(rules.has_stateful());
assert_eq!(
rules.policy_decision(HOST, Direction::In),
Some(PolicyDecision::Block)
);
assert_eq!(
rules.policy_decision(HOST, Direction::Out),
Some(PolicyDecision::AllowStateful)
);
}
#[test]
fn test_return_tracking_applies_to_all_rules_in_stateful_mode() {
let stateful_target = Ipv4Address::new(10, 1, 2, 3);
let stateless_target = Ipv4Address::new(192, 0, 2, 1);
let mut rules = stateful_rules();
rules.insert("0.0.0.0/0".parse().unwrap(), Action::Block, HOST);
rules.insert("out 10.0.0.0/8".parse().unwrap(), Action::Block, HOST);
assert!(rules.is_stateful(stateful_target, Direction::Out));
assert!(rules.is_stateful(stateless_target, Direction::Out));
assert!(rules.is_stateful(stateful_target, Direction::In));
rules.insert("10.0.0.0/8".parse().unwrap(), Action::Allow, HOST);
assert!(rules.is_stateful(stateful_target, Direction::Out));
}
}

View File

@ -17,10 +17,50 @@ impl UdpPacketHelper for UdpPacket<&[u8]> {
}
fn is_dhcp_request(&self) -> bool {
self.src_port() == Self::BOOTPC_PORT || self.dst_port() == Self::BOOTPS_PORT
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
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
}
}

View File

@ -1,12 +1,20 @@
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::Proxy;
use crate::{Error, Result};
use crate::proxy::{Direction, PolicyDecision, Proxy};
use anyhow::Context;
use anyhow::Result;
use dhcproto::Decodable;
use dhcproto::v4::Opcode;
use smoltcp::phy::ChecksumCapabilities;
use smoltcp::wire::{
ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
ArpOperation, ArpPacket, ArpRepr, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Address,
Ipv4Packet, Ipv4Repr, UdpPacket,
};
use std::net::Ipv4Addr;
impl Proxy {
const IPV4_HEADER_LEN_WITHOUT_OPTIONS: u8 = 20;
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
@ -16,10 +24,10 @@ impl Proxy {
self.host
.write(frame.as_ref())
.map(|_| ())
.map_err(|err| Error::HostIOFailed { source: err })
.context("failed to write to the host")
}
fn allowed_from_vm(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
fn allowed_from_vm(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
if frame.src_addr() != self.vm_mac_address {
return None;
}
@ -30,7 +38,14 @@ impl Proxy {
self.allowed_from_vm_arp(arp_pkt)
}
EthernetProtocol::Ipv4 => {
let ipv4_pkt = Ipv4Packet::new_checked(frame.payload()).ok()?;
let ipv4_pkt = Ipv4Packet::new_unchecked(frame.payload());
Ipv4Repr::parse(&ipv4_pkt, &ChecksumCapabilities::ignored()).ok()?;
// Reject IPv4 options because source routing could bypass destination-based policy
if ipv4_pkt.header_len() != IPV4_HEADER_LEN_WITHOUT_OPTIONS {
return None;
}
self.allowed_from_vm_ipv4(ipv4_pkt)
}
_ => None,
@ -38,56 +53,363 @@ impl Proxy {
}
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
vm_arp_allowed(arp_pkt, self.vm_mac_address, self.dhcp_snooper.lease())
}
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 {
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(());
}
}
// Allow communication with host
if ipv4_pkt.dst_addr() == self.host.gateway_ip {
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())
// Consult the flow table before evaluating outbound policy
// so established flows are not treated as new traffic
let pending = match self
.flows
.as_mut()
.map(|flows| flows.inspect(&ipv4_pkt, FlowDirection::FromVm))
.unwrap_or(FlowMatch::Untracked)
{
return Some(());
FlowMatch::Allowed => return Some(()),
FlowMatch::Denied => return None,
FlowMatch::Candidate(pending) => Some(pending),
FlowMatch::Untracked => None,
};
// The flow is either pending or untracked, evaluate it against outbound policy
let dst_addr = ipv4_pkt.dst_addr();
match self.rules.policy_decision(dst_addr, Direction::Out) {
// Return traffic was handled above; enforce explicit outbound blocks here
Some(PolicyDecision::Block) => return None,
// Track statelessly allowed traffic only when needed so its reply is not
// treated as a new inbound flow
Some(PolicyDecision::AllowStateless) => {
return self.admit_with_tracking_if_stateful(pending, dst_addr, Direction::In);
}
// Untracked packets cannot satisfy stateful policy
Some(PolicyDecision::AllowStateful) => return self.admit_with_tracking(pending?),
// No outbound rule matched; apply the built-in fallbacks below
None => {}
}
// Allow DHCP communication with the bootpd(8) on host via broadcast address
if udp_pkt.is_dhcp_request() && ipv4_pkt.dst_addr().is_broadcast() {
return Some(());
// When no user-specified rules matched, simply allow all global traffic
if ip_network::IpNetwork::from(dst_addr).is_global() {
return self.admit_with_tracking_if_trackable(pending);
}
// Additionally, allow communication with the host,
// otherwise things like SSH to a VM won't work
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;
}
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>,
) -> Option<()> {
let (operation, source_hardware_addr, source_protocol_addr) =
match ArpRepr::parse(&arp_pkt).ok()? {
ArpRepr::EthernetIpv4 {
operation,
source_hardware_addr,
source_protocol_addr,
..
} => (operation, source_hardware_addr, source_protocol_addr),
_ => return None,
};
if !matches!(operation, ArpOperation::Request | ArpOperation::Reply) {
return None;
}
if source_hardware_addr != vm_mac_address {
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
}
#[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,
};
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() {
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());
}
#[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 arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &lease).is_some());
}
#[test]
fn test_allowed_from_vm_arp_rejects_unknown_operation() {
let vm_mac_address = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
let buf = arp_packet(
vm_mac_address.0,
[0, 0, 0, 0],
ArpOperation::Unknown(3),
6,
4,
);
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_none());
}
#[test]
fn test_allowed_from_vm_arp_rejects_non_ethernet_hardware_type() {
let vm_mac_address = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
let mut buf = arp_packet(vm_mac_address.0, [0, 0, 0, 0], ArpOperation::Request, 6, 4);
let mut arp_pkt = ArpPacket::new_unchecked(buf.as_mut_slice());
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());
}
#[test]
fn test_allowed_from_vm_arp_rejects_non_ipv4_protocol_type() {
let vm_mac_address = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
let mut buf = arp_packet(vm_mac_address.0, [0, 0, 0, 0], ArpOperation::Request, 6, 4);
let mut arp_pkt = ArpPacket::new_unchecked(buf.as_mut_slice());
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());
}
#[test]
fn test_allowed_from_vm_arp_rejects_non_ipv4_protocol_length() {
let vm_mac_address = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
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());
}
fn arp_packet(
source_hardware_addr: [u8; 6],
source_protocol_addr: impl AsRef<[u8]>,
operation: ArpOperation,
hardware_len: u8,
protocol_len: u8,
) -> Vec<u8> {
let source_protocol_addr = source_protocol_addr.as_ref();
let payload_len = 8 + 2 * (hardware_len as usize + protocol_len as usize);
let mut buf = vec![0; payload_len];
let mut arp_pkt = ArpPacket::new_unchecked(buf.as_mut_slice());
arp_pkt.set_hardware_type(ArpHardware::Ethernet);
arp_pkt.set_protocol_type(EthernetProtocol::Ipv4);
arp_pkt.set_hardware_len(hardware_len);
arp_pkt.set_protocol_len(protocol_len);
arp_pkt.set_operation(operation);
arp_pkt.set_source_hardware_addr(&source_hardware_addr[..hardware_len as usize]);
arp_pkt.set_source_protocol_addr(source_protocol_addr);
arp_pkt.set_target_hardware_addr(&[0; 6][..hardware_len as usize]);
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
}
}

235
lib/vm.rs
View File

@ -1,5 +1,7 @@
use crate::{Error, Result};
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use anyhow::{Context, Result, bail};
use std::io;
use std::mem::{size_of, zeroed};
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
use std::os::unix::net::UnixDatagram;
pub struct VM {
@ -8,9 +10,11 @@ pub struct VM {
impl VM {
pub fn new(vm_fd: RawFd) -> Result<VM> {
let vm_fd = duplicate_vm_fd(vm_fd)?;
// SAFETY: duplicate_vm_fd only returns a valid descriptor that it owns.
let sock = unsafe { UnixDatagram::from_raw_fd(vm_fd) };
sock.set_nonblocking(true)
.map_err(|err| Error::InitFailed { source: err.into() })?;
sock.set_nonblocking(true)?;
Ok(VM { sock })
}
@ -22,6 +26,92 @@ impl VM {
pub fn read(&self, buf: &mut [u8]) -> std::io::Result<usize> {
self.sock.recv(buf)
}
pub fn is_connected(&self) -> io::Result<bool> {
match self.sock.peer_addr() {
Ok(_) => Ok(true),
Err(error) if error.kind() == io::ErrorKind::NotConnected => Ok(false),
Err(error) => Err(error),
}
}
}
fn duplicate_vm_fd(vm_fd: RawFd) -> Result<RawFd> {
if vm_fd < 0 {
bail!("invalid VM file descriptor {vm_fd}: value must be non-negative");
}
// SAFETY: fcntl duplicates the descriptor without transferring ownership of vm_fd.
let duplicated_fd = unsafe { libc::fcntl(vm_fd, libc::F_DUPFD_CLOEXEC, 0) };
if duplicated_fd == -1 {
return Err(io::Error::last_os_error())
.with_context(|| format!("failed to duplicate VM file descriptor {vm_fd}"));
}
if let Err(error) = validate_vm_fd(duplicated_fd) {
// SAFETY: duplicated_fd is an open descriptor owned by this function.
unsafe { libc::close(duplicated_fd) };
return Err(error);
}
Ok(duplicated_fd)
}
fn validate_vm_fd(vm_fd: RawFd) -> Result<()> {
// SAFETY: fcntl only reads descriptor state and does not take ownership.
if unsafe { libc::fcntl(vm_fd, libc::F_GETFD) } == -1 {
return Err(io::Error::last_os_error())
.with_context(|| format!("failed to inspect VM file descriptor {vm_fd}"));
}
let mut socket_type = 0;
let mut socket_type_len = size_of::<libc::c_int>() as libc::socklen_t;
// SAFETY: socket_type and socket_type_len are valid writable buffers of the sizes given.
if unsafe {
libc::getsockopt(
vm_fd,
libc::SOL_SOCKET,
libc::SO_TYPE,
(&mut socket_type as *mut libc::c_int).cast(),
&mut socket_type_len,
)
} == -1
{
return Err(io::Error::last_os_error())
.with_context(|| format!("VM file descriptor {vm_fd} is not a socket"));
}
if socket_type != libc::SOCK_DGRAM {
bail!("VM file descriptor {vm_fd} is not a Unix datagram socket");
}
let mut address: libc::sockaddr_storage = unsafe { zeroed() };
let mut address_len = size_of::<libc::sockaddr_storage>() as libc::socklen_t;
// SAFETY: address and address_len are valid writable buffers of the sizes given.
if unsafe {
libc::getsockname(
vm_fd,
(&mut address as *mut libc::sockaddr_storage).cast(),
&mut address_len,
)
} == -1
{
return Err(io::Error::last_os_error()).with_context(|| {
format!("failed to inspect the address family of VM file descriptor {vm_fd}")
});
}
// macOS returns a zero-length address for unnamed UNIX-domain sockets,
// including socketpair descriptors. Other socket families return their
// address family when getsockname succeeds.
let is_unix_socket = address_len == 0 || address.ss_family as libc::c_int == libc::AF_UNIX;
if !is_unix_socket {
bail!("VM file descriptor {vm_fd} is not a Unix socket");
}
Ok(())
}
impl AsRawFd for VM {
@ -29,3 +119,140 @@ impl AsRawFd for VM {
self.sock.as_raw_fd()
}
}
#[cfg(test)]
mod tests {
use super::VM;
use polling::{Event, Events, PollMode, Poller};
use std::fs::File;
use std::net::UdpSocket;
use std::os::fd::AsRawFd;
use std::os::unix::net::{UnixDatagram, UnixStream};
use std::time::Duration;
#[test]
fn test_new_rejects_negative_fd() {
let error = VM::new(-1).err().unwrap();
assert_eq!(
error.to_string(),
"invalid VM file descriptor -1: value must be non-negative"
);
}
#[test]
fn test_new_rejects_non_socket_fd_without_taking_ownership() {
let file = File::open("/dev/null").unwrap();
let error = VM::new(file.as_raw_fd()).err().unwrap();
assert!(error.to_string().contains("is not a socket"));
assert!(file.metadata().is_ok());
}
#[test]
fn test_new_rejects_closed_fd() {
let (socket, _peer) = UnixDatagram::pair().unwrap();
let vm_fd = socket.as_raw_fd();
drop(socket);
let error = VM::new(vm_fd).err().unwrap();
assert!(
error
.to_string()
.contains("failed to duplicate VM file descriptor")
);
}
#[test]
fn test_new_rejects_non_datagram_socket() {
let (stream, _peer) = UnixStream::pair().unwrap();
let error = VM::new(stream.as_raw_fd()).err().unwrap();
assert!(error.to_string().contains("not a Unix datagram socket"));
}
#[test]
fn test_new_rejects_internet_datagram_socket() {
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
let error = VM::new(socket.as_raw_fd()).err().unwrap();
assert!(error.to_string().contains("not a Unix socket"));
}
#[test]
fn test_new_does_not_close_original_fd_when_vm_is_dropped() {
let (socket, _peer) = UnixDatagram::pair().unwrap();
let vm = VM::new(socket.as_raw_fd()).unwrap();
drop(vm);
let socket_fd_is_open = unsafe { libc::fcntl(socket.as_raw_fd(), libc::F_GETFD) != -1 };
if socket_fd_is_open {
drop(socket);
} else {
// Avoid double-closing the descriptor if this test catches an unsafe implementation.
std::mem::forget(socket);
}
assert!(socket_fd_is_open);
}
#[test]
fn test_connected_socket_has_peer() {
let (socket, _peer) = UnixDatagram::pair().unwrap();
let vm = VM::new(socket.as_raw_fd()).unwrap();
assert!(vm.is_connected().unwrap());
}
#[test]
fn test_disconnected_peer_is_detected_without_kqueue_event() {
let (socket, peer) = UnixDatagram::pair().unwrap();
let vm = VM::new(socket.as_raw_fd()).unwrap();
let poller = Poller::new().unwrap();
let mut events = Events::new();
unsafe {
poller
.add_with_mode(vm.as_raw_fd(), Event::readable(0), PollMode::Edge)
.unwrap();
}
drop(peer);
poller
.wait(&mut events, Some(Duration::from_millis(20)))
.unwrap();
assert!(events.is_empty());
assert!(!vm.is_connected().unwrap());
}
#[test]
fn test_disconnected_peer_is_detected_when_another_socket_wakes_kqueue() {
let (socket, peer) = UnixDatagram::pair().unwrap();
let (host, host_peer) = UnixDatagram::pair().unwrap();
let vm = VM::new(socket.as_raw_fd()).unwrap();
let poller = Poller::new().unwrap();
let mut events = Events::new();
unsafe {
poller
.add_with_mode(vm.as_raw_fd(), Event::readable(0), PollMode::Edge)
.unwrap();
poller
.add_with_mode(host.as_raw_fd(), Event::readable(1), PollMode::Edge)
.unwrap();
}
drop(peer);
host_peer.send(&[1]).unwrap();
poller
.wait(&mut events, Some(Duration::from_millis(20)))
.unwrap();
assert!(events.iter().any(|event| event.key == 1));
assert!(!events.iter().any(|event| event.key == 0));
assert!(!vm.is_connected().unwrap());
}
}

2
rust-toolchain.toml Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@ -1,33 +1,56 @@
use anyhow::{Context, anyhow};
use clap::Parser;
use log::LevelFilter;
use nix::sys::signal::{SigHandler, Signal, signal};
use oslog::OsLogger;
use privdrop::PrivDrop;
use softnet::NetType;
use softnet::proxy::ExposedPort;
use softnet::proxy::Proxy;
use softnet::proxy::Rule;
use std::env;
use std::os::raw::c_int;
use std::os::unix::io::RawFd;
use std::os::unix::process::CommandExt;
use std::process::Command;
use std::process::{Command, ExitCode};
use system_configuration::core_foundation::base::TCFType;
use system_configuration::core_foundation::boolean::CFBoolean;
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, get_effective_uid};
use system_configuration::sys::preferences::{
SCPreferencesApplyChanges, SCPreferencesCommitChanges, SCPreferencesLock,
SCPreferencesSetValue, SCPreferencesUnlock,
};
use uzers::{get_current_groupname, get_current_username, get_effective_uid};
#[derive(Parser, Debug)]
struct Args {
#[clap(
long,
value_parser = parse_vm_fd,
help = "FD number to use for communicating with the VM's networking stack"
)]
vm_fd: c_int,
#[clap(
long,
value_parser = parse_vm_fd,
help = "connected Unix stream FD for newline-delimited JSON-RPC policy control"
)]
control_fd: Option<c_int>,
#[clap(long, help = "MAC address to enforce for the VM")]
vm_mac_address: mac_address::MacAddress,
#[clap(long, value_enum, help = "type of network to use for the VM", default_value_t=NetType::Nat)]
vm_net_type: NetType,
#[clap(
long,
help = "set bootpd(8) lease time to this value (in seconds) before starting the VM",
default_value_t = 60
default_value_t = 600
)]
bootpd_lease_time: u32,
@ -37,24 +60,122 @@ struct Args {
#[clap(long, help = "group name to drop privileges to")]
group: Option<String>,
#[clap(long, hide=true)]
#[clap(
long,
help = "Comma-separated rules for allowing traffic, in the following forms:\n\n\
* TARGET: traffic sent from the VM to TARGET; reverse traffic is not filtered by this rule\n\
* in TARGET: flows initiated from TARGET to the VM\n\
* out TARGET: flows initiated from the VM to TARGET\n\n\
Targets are:\n\n\
* IPv4 CIDRs\n\
* @host, which matches the vmnet bridge gateway IP\n\n\
Directional rules make bare TARGET rules stateful in both directions.\n\n\
When used with --block, the longest prefix match wins. If an identical rule is both \
allowed and blocked, blocking takes precedence.\n\n\
--allow=0.0.0.0/0 additionally disables bridge isolation, even when \
--block=0.0.0.0/0 is specified.\n\n\
Examples:\n\n\
* --allow=192.168.0.0/24 allow stateless traffic with this LAN\n\
* --allow=\"in @host\" — allow stateful flows initiated from @host\n\
* --allow=\"out 192.168.0.0/24\" — allow stateful flows initiated toward this LAN\n\
* --allow=\"in @host,out 192.168.0.0/24\" — multiple rules may be comma-separated",
value_name = "comma-separated rules",
use_value_delimiter = true,
action = clap::ArgAction::Set
)]
allow: Vec<Rule>,
#[clap(
long,
help = "Comma-separated rules for blocking traffic, in the following forms:\n\n\
* TARGET: traffic sent from the VM to TARGET; reverse traffic is not filtered by this rule\n\
* in TARGET: flows initiated from TARGET to the VM\n\
* out TARGET: flows initiated from the VM to TARGET\n\n\
Targets are:\n\n\
* IPv4 CIDRs\n\
* @host, which matches the vmnet bridge gateway IP\n\n\
Directional rules make bare TARGET rules stateful in both directions.\n\n\
When used with --allow, the longest prefix match wins. If an identical rule is both \
allowed and blocked, blocking takes precedence.\n\n\
Examples:\n\n\
* --block=0.0.0.0/0 establish a stateless default-deny egress policy\n\
* --block=\"out @host\" — block stateful flows initiated toward @host\n\
* --block=\"out 66.66.66.0/24\" — block stateful flows initiated toward this CIDR\n\
* --block=\"out @host,out 66.66.66.0/24\" — multiple rules may be comma-separated",
value_name = "comma-separated rules",
use_value_delimiter = true,
action = clap::ArgAction::Set
)]
block: Vec<Rule>,
#[clap(
long,
help = "comma-separated list of TCP ports to expose (e.g. --expose 2222:22,8080:80)",
value_name = "comma-separated port specifications",
use_value_delimiter = true,
action = clap::ArgAction::Set
)]
expose: Vec<ExposedPort>,
#[clap(long, hide = true)]
sudo_escalation_probing: bool,
#[clap(long, hide=true)]
#[clap(long, hide = true)]
sudo_escalation_done: bool,
}
fn main() {
if let Err(err) = try_main() {
match err.source() {
Some(source) => eprintln!("{}: {}", err, source),
None => eprintln!("{}", err),
fn main() -> ExitCode {
// Enable backtraces by default
if env::var("RUST_BACKTRACE").is_err() {
unsafe {
env::set_var("RUST_BACKTRACE", "full");
}
}
// Initialize Sentry
let _sentry = sentry::init(
sentry::ClientOptions::default()
.maybe_release(option_env!("CIRRUS_TAG").map(|tag| format!("softnet@{tag}"))),
);
// Enrich future events with Cirrus CI-specific tags
if let Ok(tags) = env::var("CIRRUS_SENTRY_TAGS") {
sentry::configure_scope(|scope| {
for (key, value) in tags.split(',').filter_map(|tag| tag.split_once('=')) {
scope.set_tag(key, value);
}
});
}
match try_main() {
Ok(_) => ExitCode::SUCCESS,
Err(err) => {
// Print the error into stderr
let causes: Vec<String> = err.chain().map(|x| x.to_string()).collect();
eprintln!("{}", causes.join(": "));
// Capture the error into Sentry
sentry_anyhow::capture_anyhow(&err);
ExitCode::FAILURE
}
std::process::exit(1);
}
}
fn try_main() -> Result<(), Box<dyn std::error::Error>> {
fn try_main() -> anyhow::Result<()> {
// Initialize logger
OsLogger::new("org.cirruslabs.softnet")
.level_filter(LevelFilter::Info)
.init()?;
// The default signal(3)[1] action for SIGINT is to interrupt program,
// but we want to handle SIGINT ourselves, so we ignore it. The kqueue(2)'s[2]
// EVFILT_SIGNAL will receive it anyways, because it has lower precedence.
//
// [1]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/signal.3.html
// [2]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html
unsafe { signal(Signal::SIGINT, SigHandler::SigIgn) }?;
let args: Args = Args::parse();
// No need to run anything, just return
@ -66,11 +187,11 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
// Retrieve real (not effective) user and group names
let current_user_name = get_current_username()
.ok_or("failed to resolve real user name")?
.ok_or(anyhow!("failed to resolve real user name"))?
.to_string_lossy()
.to_string();
let current_group_name = get_current_groupname()
.ok_or("failed to resolve real group name")?
.ok_or(anyhow!("failed to resolve real group name"))?
.to_string_lossy()
.to_string();
@ -81,7 +202,8 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
let args = std::env::args().skip(1);
let _ = Command::new("sudo")
.arg("-n")
.arg("--non-interactive")
.arg("--preserve-env=SENTRY_DSN,CIRRUS_SENTRY_TAGS")
.arg(&exe)
.args(args)
.arg("--sudo-escalation-done")
@ -92,14 +214,25 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
.exec();
}
return Err("root privileges are required to run and passwordless sudo was not available".into());
return Err(anyhow!(
"root privileges are required to run and passwordless sudo was not available"
));
}
// Set bootpd(8) min/max lease time while still having the root privileges
set_bootpd_lease_time(args.bootpd_lease_time);
// Configure bootpd(8) while still having the root privileges
configure_bootpd(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)?;
let mut proxy = Proxy::new(
args.vm_fd as RawFd,
args.vm_mac_address,
args.vm_net_type,
args.allow,
args.block,
args.expose,
args.control_fd.map(|fd| fd as RawFd),
)
.context("failed to initialize proxy")?;
// Drop effective privileges to the user
// and group which have had invoked us
@ -107,10 +240,22 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
.user(args.user.unwrap_or(current_user_name))
.group(args.group.unwrap_or(current_group_name))
.apply()
.map_err(|err| format!("failed to drop privileges: {}", err))?;
.context("failed to drop privileges")?;
// Run proxy
proxy.run().map_err(|err| err.into())
proxy.run()
}
fn parse_vm_fd(value: &str) -> Result<c_int, String> {
let vm_fd = value
.parse::<c_int>()
.map_err(|err| format!("invalid file descriptor: {err}"))?;
if vm_fd < 0 {
return Err("file descriptor must be non-negative".to_string());
}
Ok(vm_fd)
}
fn sudo_escalation_works() -> bool {
@ -127,24 +272,96 @@ fn sudo_escalation_works() -> bool {
.unwrap_or(false)
}
fn set_bootpd_lease_time(lease_time: u32) {
fn configure_bootpd(lease_time: u32) -> anyhow::Result<()> {
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),
)]);
let bootpd_dict = CFDictionary::from_CFType_pairs(&[
(
CFString::new("DHCPLeaseTimeSecs"),
CFNumber::from(lease_time as i32).as_CFType(),
),
(
CFString::new("dhcp_ignore_client_identifier"),
CFBoolean::true_value().as_CFType(),
),
]);
unsafe {
SCPreferencesSetValue(
prefs.as_concrete_TypeRef(),
CFString::new("bootpd").as_concrete_TypeRef(),
bootpd_dict.as_concrete_TypeRef().cast(),
let prefs = prefs.as_concrete_TypeRef();
anyhow::ensure!(
SCPreferencesLock(prefs, 1) != 0,
"failed to lock bootpd preferences"
);
SCPreferencesCommitChanges(prefs.as_concrete_TypeRef());
let result = (|| -> anyhow::Result<()> {
anyhow::ensure!(
SCPreferencesSetValue(
prefs,
CFString::new("bootpd").as_concrete_TypeRef(),
bootpd_dict.as_concrete_TypeRef().cast(),
) != 0,
"failed to set bootpd preferences"
);
anyhow::ensure!(
SCPreferencesCommitChanges(prefs) != 0,
"failed to commit bootpd preferences"
);
anyhow::ensure!(
SCPreferencesApplyChanges(prefs) != 0,
"failed to apply bootpd preferences"
);
Ok(())
})();
let unlocked = SCPreferencesUnlock(prefs) != 0;
result?;
anyhow::ensure!(unlocked, "failed to unlock bootpd preferences");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::Args;
use clap::Parser;
#[test]
fn test_cli_rejects_negative_vm_fd_before_startup() {
let error = Args::try_parse_from([
"softnet",
"--vm-fd=-1",
"--vm-mac-address=02:00:00:00:00:01",
])
.unwrap_err();
assert!(
error
.to_string()
.contains("file descriptor must be non-negative")
);
}
#[test]
fn test_cli_rejects_negative_control_fd_before_startup() {
let error = Args::try_parse_from([
"softnet",
"--vm-fd=0",
"--control-fd=-1",
"--vm-mac-address=02:00:00:00:00:01",
])
.unwrap_err();
assert!(
error
.to_string()
.contains("file descriptor must be non-negative")
);
}
}