Compare commits

...
Author SHA1 Message Date
Nikolay Edigaryev 431a2384a4 WIP: support peer MAC addresses in --allow/--block
To enable cross-VM communication.
2026-05-19 07:35:00 +01: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](https://github.com/bluecatengineering/dhcproto/compare/b4ea30defc01e7ae66e7075d6a0533d9bb9503dc...eece41c9a13b0e4912fb9a8f08401ab01b4123d4)

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
10 changed files with 1279 additions and 243 deletions
Generated
+782 -199
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -16,7 +16,7 @@ smoltcp = "0"
libc = "0"
polling = "3"
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
vmnet = "0.5.0"
vmnet = "0.5.1"
clap = { version = "4", features = ["derive"] }
mac_address = "1"
privdrop = "0"
@@ -34,6 +34,9 @@ oslog = "0.2.0"
log = "0.4.29"
serial_test = "3"
coarsetime = "0.1.37"
pnet_datalink = "0.35.0"
pcap = "2.4.0"
ipnetwork = "0.20"
[profile.release]
debug = true
+210
View File
@@ -0,0 +1,210 @@
use crate::dhcp_snooper::Lease;
use anyhow::{Context, Result, anyhow};
use dhcproto::Decodable;
use dhcproto::v4::{DhcpOption, MessageType, Opcode, OptionCode};
use smoltcp::wire::{
EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Address, Ipv4Packet, UdpPacket,
};
use std::collections::HashMap;
use std::os::unix::io::RawFd;
use std::time::{Duration, Instant};
const TABLE_PRINT_INTERVAL: Duration = Duration::from_secs(5);
unsafe extern "C" {
fn pcap_get_selectable_fd(pcap: *mut libc::c_void) -> libc::c_int;
}
#[derive(Default)]
pub struct DhcpSnooperGlobal {
pcap_capture: Option<pcap::Capture<pcap::Active>>,
pcap_fd: Option<RawFd>,
leases_by_mac: HashMap<[u8; 6], Lease>,
uncertainty_duration: Duration,
last_table_print: Option<Instant>,
}
impl DhcpSnooperGlobal {
pub fn new(gateway_interface_name: &str, uncertainty_duration: Duration) -> Result<Self> {
// Start capturing packets on bridge interface in promiscuous mode.
let mut pcap_capture = pcap::Capture::from_device(gateway_interface_name)?
.promisc(true)
.timeout(1)
.immediate_mode(true)
.open()?;
// Capture packets using a filter to avoid wasting CPU cycles in user-space.
pcap_capture
.filter("udp and (port 67 or port 68)", true)
.context("failed to install DHCP pcap filter")?;
// Prepare packet capture for event-driven consumption.
let pcap_capture = pcap_capture.setnonblock()?;
let pcap_fd = unsafe { pcap_get_selectable_fd(pcap_capture.as_ptr().cast()) };
if pcap_fd == -1 {
return Err(anyhow!("failed to call pcap_get_selectable_fd(3)"));
}
Ok(DhcpSnooperGlobal {
pcap_capture: Some(pcap_capture),
pcap_fd: Some(pcap_fd),
uncertainty_duration,
..Default::default()
})
}
pub fn disabled(uncertainty_duration: Duration) -> Self {
DhcpSnooperGlobal {
uncertainty_duration,
..Default::default()
}
}
pub fn pcap_raw_fd(&self) -> Option<RawFd> {
self.pcap_fd
}
pub fn read_pcap(&mut self, mut handle_packet: impl FnMut(&mut Self, &[u8])) -> Result<()> {
let Some(mut pcap_capture) = self.pcap_capture.take() else {
return Ok(());
};
let result = loop {
match pcap_capture.next_packet() {
Ok(packet) => handle_packet(self, packet.data),
Err(pcap::Error::TimeoutExpired) => break Ok(()),
Err(err) => break Err(err.into()),
}
};
self.pcap_capture = Some(pcap_capture);
result
}
pub fn register_ethernet_packet(&mut self, packet: &[u8]) {
Self::register_ethernet_packet_with(
&mut self.leases_by_mac,
self.uncertainty_duration,
packet,
);
}
fn register_ethernet_packet_with(
leases_by_mac: &mut HashMap<[u8; 6], Lease>,
uncertainty_duration: Duration,
packet: &[u8],
) {
let Some(dhcp_packet) = Self::dhcp_payload(packet) else {
return;
};
let mut decoder = dhcproto::v4::Decoder::new(dhcp_packet);
let message = match dhcproto::v4::Message::decode(&mut decoder) {
Ok(message) => message,
Err(_) => return,
};
println!("{message}");
let Some(mac) = Self::message_mac(&message) else {
return;
};
match message.opts().msg_type() {
Some(MessageType::Ack) => {
let lease_time = match message.opts().get(OptionCode::AddressLeaseTime) {
Some(DhcpOption::AddressLeaseTime(lease_time)) => *lease_time,
_ => 600,
};
let mut lease_duration = Duration::from_secs(lease_time as u64);
lease_duration = lease_duration.saturating_sub(uncertainty_duration);
leases_by_mac.insert(
mac,
Lease::new(message.yiaddr(), lease_duration, Default::default()),
);
println!(
"DHCP global lease learned: {} -> {}",
Self::format_mac(&mac),
message.yiaddr()
);
}
Some(MessageType::Nak) => {
leases_by_mac.remove(&mac);
println!("DHCP global lease removed: {}", Self::format_mac(&mac));
}
_ => {}
}
}
pub fn print_table_periodically(&mut self) {
let now = Instant::now();
if self
.last_table_print
.is_some_and(|last_print| now.duration_since(last_print) < TABLE_PRINT_INTERVAL)
{
return;
}
self.last_table_print = Some(now);
self.print_table();
}
pub fn valid_ip_for_mac(&self, mac: &[u8; 6], ip: Ipv4Address) -> bool {
self.leases_by_mac
.get(mac)
.is_some_and(|lease| lease.valid_ip_source(ip))
}
fn print_table(&self) {
if self.leases_by_mac.is_empty() {
println!("DHCP global leases: <empty>");
return;
}
println!("DHCP global leases:");
for (mac, lease) in &self.leases_by_mac {
let state = if lease.valid() { "valid" } else { "expired" };
println!(
" {} -> {} ({state})",
Self::format_mac(mac),
lease.address()
);
}
}
fn dhcp_payload(packet: &[u8]) -> Option<&[u8]> {
let frame = EthernetFrame::new_checked(packet).ok()?;
if frame.ethertype() != EthernetProtocol::Ipv4 {
return None;
}
let ipv4_packet = Ipv4Packet::new_checked(frame.payload()).ok()?;
if ipv4_packet.next_header() != IpProtocol::Udp {
return None;
}
let udp_packet = UdpPacket::new_checked(ipv4_packet.payload()).ok()?;
if !matches!(udp_packet.src_port(), 67 | 68) && !matches!(udp_packet.dst_port(), 67 | 68) {
return None;
}
Some(udp_packet.payload())
}
fn message_mac(message: &dhcproto::v4::Message) -> Option<[u8; 6]> {
if message.opcode() != Opcode::BootReply {
return None;
}
message.chaddr().try_into().ok()
}
fn format_mac(mac: &[u8; 6]) -> String {
mac.iter()
.map(|octet| format!("{octet:02x}"))
.collect::<Vec<_>>()
.join(":")
}
}
+75 -4
View File
@@ -1,6 +1,9 @@
use anyhow::{Context, Result, anyhow};
use clap::ValueEnum;
use ipnetwork::{IpNetwork, Ipv4Network};
use log::info;
use pnet_datalink::MacAddr;
use smoltcp::wire::EthernetAddress;
use std::net::Ipv4Addr;
use std::os::unix::io::{AsRawFd, RawFd};
use std::os::unix::net::UnixDatagram;
@@ -27,7 +30,9 @@ pub struct Host {
interface: vmnet::Interface,
new_packets_rx: UnixDatagram,
callback_can_continue_tx: SyncSender<()>,
pub gateway_interface_name: String,
pub gateway_ip: smoltcp::wire::Ipv4Address,
pub gateway_mac: EthernetAddress,
pub max_packet_size: u64,
pub read_max_packets: u64,
finalized: bool,
@@ -48,17 +53,38 @@ impl Host {
)
.context("failed to initialize vmnet interface")?;
// Retrieve first IP (gateway) used for this interface
let Some(Parameter::StartAddress(gateway_ip)) =
// Retrieve the first IP (gateway) used for this interface
let Some(Parameter::StartAddress(start_address)) =
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)
let start_address = Ipv4Addr::from_str(&start_address)
.context("failed to parse vmnet's interface start address")?;
// Retrieve the last IP used for this interface
let Some(Parameter::EndAddress(end_address)) =
interface.parameters().get(ParameterKind::EndAddress)
else {
return Err(anyhow!("failed to retrieve vmnet's interface end address"));
};
let end_address = Ipv4Addr::from_str(&end_address)
.context("failed to parse vmnet's interface end address")?;
// Determine the prefix used for this interface
let prefix = Self::ipv4_range_prefix(start_address, end_address);
let Some((gateway_name, gateway_mac)) = Self::interface_ip_to_mac(start_address, prefix)
else {
return Err(anyhow!(
"failed to resolve vmnet's interface from start address {}",
start_address
));
};
let gateway_mac = EthernetAddress(gateway_mac.octets());
// Retrieve max packet size for this interface
let Some(Parameter::MaxPacketSize(max_packet_size)) =
interface.parameters().get(ParameterKind::MaxPacketSize)
@@ -104,12 +130,32 @@ impl Host {
interface,
new_packets_rx,
callback_can_continue_tx,
gateway_ip,
gateway_interface_name: gateway_name,
gateway_ip: start_address,
gateway_mac,
max_packet_size,
read_max_packets,
finalized: false,
})
}
fn interface_ip_to_mac(ip: Ipv4Addr, prefix: u8) -> Option<(String, MacAddr)> {
let ip_network = IpNetwork::V4(Ipv4Network::new(ip, prefix).unwrap());
for iface in pnet_datalink::interfaces() {
if iface.ips.contains(&ip_network)
&& let Some(mac) = iface.mac
{
return Some((iface.name, mac));
}
}
None
}
fn ipv4_range_prefix(start_address: Ipv4Addr, end_address: Ipv4Addr) -> u8 {
(u32::from(start_address) ^ u32::from(end_address)).leading_zeros() as u8
}
}
impl Host {
@@ -198,3 +244,28 @@ impl AsRawFd for Host {
self.new_packets_rx.as_raw_fd()
}
}
#[cfg(test)]
mod tests {
use super::Host;
use std::net::Ipv4Addr;
#[test]
fn ipv4_range_prefix_for_vmnet_class_c_range() {
assert_eq!(
Host::ipv4_range_prefix(
Ipv4Addr::new(192, 168, 64, 1),
Ipv4Addr::new(192, 168, 64, 254),
),
24
);
}
#[test]
fn ipv4_range_prefix_for_single_address() {
assert_eq!(
Host::ipv4_range_prefix(Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 1)),
32
);
}
}
+1
View File
@@ -1,4 +1,5 @@
mod dhcp_snooper;
mod dhcp_snooper_global;
mod host;
pub use host::NetType;
mod poller;
+33 -2
View File
@@ -12,6 +12,7 @@ pub struct Poller<'poller> {
timeout: Duration,
vm_fd: BorrowedFd<'poller>,
host_fd: BorrowedFd<'poller>,
pcap_fd: Option<BorrowedFd<'poller>>,
}
#[derive(IntoPrimitive)]
@@ -19,13 +20,22 @@ pub struct Poller<'poller> {
enum EventKey {
VM,
Host,
Pcap,
Interrupt,
}
pub struct Readiness {
pub vm_readable: bool,
pub host_readable: bool,
pub pcap_readable: bool,
pub interrupt: bool,
}
impl Poller<'_> {
pub fn new<'poller>(
vm_fd: RawFd,
host_fd: RawFd,
pcap_fd: Option<RawFd>,
timeout: Duration,
) -> Result<Poller<'poller>> {
let poller = polling::Poller::new()?;
@@ -36,6 +46,7 @@ impl Poller<'_> {
timeout,
vm_fd: unsafe { BorrowedFd::borrow_raw(vm_fd) },
host_fd: unsafe { BorrowedFd::borrow_raw(host_fd) },
pcap_fd: pcap_fd.map(|fd| unsafe { BorrowedFd::borrow_raw(fd) }),
})
}
@@ -51,6 +62,13 @@ impl Poller<'_> {
self.host_interest(),
PollMode::Edge,
)?;
if let Some(pcap_fd) = self.pcap_fd {
self.poller.add_with_mode(
pcap_fd.as_raw_fd(),
self.pcap_interest(),
PollMode::Edge,
)?;
}
}
let interrupt_signal = polling::os::kqueue::Signal(libc::SIGINT);
@@ -64,7 +82,7 @@ impl Poller<'_> {
self.events.clear();
}
pub fn wait(&mut self) -> Result<(bool, bool, bool)> {
pub fn wait(&mut self) -> Result<Readiness> {
self.poller.wait(&mut self.events, Some(self.timeout))?;
let vm_readable = self
@@ -75,12 +93,21 @@ impl Poller<'_> {
.events
.iter()
.any(|ev| ev.key == Into::<usize>::into(EventKey::Host));
let pcap_readable = self
.events
.iter()
.any(|ev| ev.key == Into::<usize>::into(EventKey::Pcap));
let interrupt = self
.events
.iter()
.any(|ev| ev.key == Into::<usize>::into(EventKey::Interrupt));
Ok((vm_readable, host_readable, interrupt))
Ok(Readiness {
vm_readable,
host_readable,
pcap_readable,
interrupt,
})
}
fn vm_interest(&self) -> polling::Event {
@@ -90,4 +117,8 @@ impl Poller<'_> {
fn host_interest(&self) -> polling::Event {
polling::Event::readable(EventKey::Host.into())
}
fn pcap_interest(&self) -> polling::Event {
polling::Event::readable(EventKey::Pcap.into())
}
}
+25
View File
@@ -37,6 +37,29 @@ impl Proxy<'_> {
}
fn allowed_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
// let mac = frame
// .src_addr()
// .as_bytes()
// .iter()
// .map(|b| format!("{:02x}", b))
// .collect::<Vec<_>>()
// .join(":");
//
// println!("{}", mac);
let from_gateway = frame.src_addr() == self.host.gateway_mac;
let peer_action = self.rules_mac.get(frame.src_addr().as_bytes());
let from_allowed_peer = peer_action == Some(&crate::proxy::Action::Allow);
if !from_gateway && !from_allowed_peer {
println!("dropping packet from {}", frame.src_addr());
return None;
}
if from_allowed_peer {
println!("allowing packet from peer {}", frame.src_addr());
}
match frame.ethertype() {
EthernetProtocol::Arp => Some(()),
EthernetProtocol::Ipv4 => Some(()),
@@ -45,6 +68,8 @@ impl Proxy<'_> {
}
fn snoop(&mut self, frame: &EthernetFrame<&[u8]>) {
// TODO: resolve IPs for the MAC addresses from --allow/--block too
if frame.ethertype() != EthernetProtocol::Ipv4 {
return;
}
+122 -25
View File
@@ -5,6 +5,7 @@ mod udp_packet_helper;
mod vm;
use crate::dhcp_snooper::DhcpSnooper;
use crate::dhcp_snooper_global::DhcpSnooperGlobal;
use crate::host::Host;
use crate::host::NetType;
use crate::poller::Poller;
@@ -14,10 +15,12 @@ pub use exposed_port::ExposedPort;
use ipnet::Ipv4Net;
use mac_address::MacAddress;
use port_forwarder::PortForwarder;
use prefix_trie::{Prefix, PrefixMap, PrefixSet};
use prefix_trie::{Prefix, PrefixMap};
use smoltcp::wire::EthernetFrame;
use std::collections::HashMap;
use std::io::ErrorKind;
use std::os::unix::io::{AsRawFd, RawFd};
use std::str::FromStr;
use std::time::Duration;
use vmnet::Batch;
@@ -27,11 +30,36 @@ pub struct Proxy<'proxy> {
poller: Poller<'proxy>,
vm_mac_address: smoltcp::wire::EthernetAddress,
dhcp_snooper: DhcpSnooper,
dhcp_snooper_global: DhcpSnooperGlobal,
rules: PrefixMap<Ipv4Net, Action>,
rules_mac: HashMap<[u8; 6], Action>,
enobufs_encountered: bool,
port_forwarder: PortForwarder,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Target {
Prefix(Ipv4Net),
MacAddress(MacAddress),
Host,
}
impl FromStr for Target {
type Err = ipnet::AddrParseError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if s == "@host" {
return Ok(Target::Host);
}
if let Ok(mac_address) = MacAddress::from_str(s) {
return Ok(Target::MacAddress(mac_address));
}
Ipv4Net::from_str(s).map(Target::Prefix)
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Action {
Block,
@@ -43,27 +71,65 @@ impl Proxy<'_> {
vm_fd: RawFd,
vm_mac_address: MacAddress,
vm_net_type: NetType,
allow: PrefixSet<Ipv4Net>,
block: PrefixSet<Ipv4Net>,
allow: Vec<Target>,
block: Vec<Target>,
exposed_ports: Vec<ExposedPort>,
) -> Result<Proxy<'proxy>> {
let allowing_all_ipv4 = allow.contains(&Target::Prefix(Ipv4Net::zero()));
let using_mac_filtering = allow
.iter()
.chain(block.iter())
.any(|target| matches!(target, Target::MacAddress(_)));
let enable_isolation = !allowing_all_ipv4 && !using_mac_filtering;
let vm = VM::new(vm_fd)?;
let host = Host::new(vm_net_type, !allow.contains(&Ipv4Net::zero()))?;
let host = Host::new(vm_net_type, enable_isolation)?;
let poller_timeout = Duration::from_millis(100);
let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd(), poller_timeout)?;
let dhcp_snooper_global = if using_mac_filtering {
DhcpSnooperGlobal::new(&host.gateway_interface_name, poller_timeout)?
} else {
DhcpSnooperGlobal::disabled(poller_timeout)
};
let poller = Poller::new(
vm.as_raw_fd(),
host.as_raw_fd(),
dhcp_snooper_global.pcap_raw_fd(),
poller_timeout,
)?;
// Craft packet filter rules
//
// SECURITY: blocking rules must always take precedence
// over allowing rules when prefixes are identical.
let mut rules = PrefixMap::new();
let mut rules_mac = HashMap::new();
for allow_net in allow {
rules.insert(allow_net, Action::Allow);
for allow_target in allow {
let allow_prefix = match allow_target {
Target::Prefix(prefix) => prefix,
Target::Host => host.gateway_ip.into(),
Target::MacAddress(mac_address) => {
rules_mac.insert(mac_address.bytes(), Action::Allow);
continue;
}
};
rules.insert(allow_prefix, Action::Allow);
}
for block_net in block {
rules.insert(block_net, Action::Block);
for block_target in block {
let block_prefix = match block_target {
Target::Prefix(prefix) => prefix,
Target::Host => host.gateway_ip.into(),
Target::MacAddress(mac_address) => {
rules_mac.insert(mac_address.bytes(), Action::Block);
continue;
}
};
rules.insert(block_prefix, Action::Block);
}
Ok(Proxy {
@@ -72,7 +138,9 @@ impl Proxy<'_> {
poller,
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
dhcp_snooper: DhcpSnooper::new(poller_timeout),
dhcp_snooper_global,
rules,
rules_mac,
enobufs_encountered: false,
port_forwarder: PortForwarder::new(exposed_ports),
})
@@ -92,26 +160,36 @@ impl Proxy<'_> {
self.poller.arm()?;
loop {
let (vm_readable, host_readable, interrupt) = self.poller.wait()?;
let readiness = self.poller.wait()?;
// Update coarse time for the DHCP snooper
coarsetime::Instant::update();
if vm_readable {
if readiness.vm_readable {
self.read_from_vm(buf.as_mut_slice())?;
}
if host_readable {
if readiness.host_readable {
self.read_from_host(&mut batch, &mut bufs)?;
}
if readiness.pcap_readable {
self.dhcp_snooper_global
.read_pcap(|snooper, packet| snooper.register_ethernet_packet(packet))?;
}
self.dhcp_snooper_global.print_table_periodically();
// Graceful termination
if interrupt {
if readiness.interrupt {
return Ok(());
}
// Timeout
if !vm_readable && !host_readable && !interrupt {
if !readiness.vm_readable
&& !readiness.host_readable
&& !readiness.pcap_readable
&& !readiness.interrupt
{
self.port_forwarder
.tick(&mut self.host, self.dhcp_snooper.lease());
}
@@ -175,7 +253,7 @@ mod tests {
use ipnet::Ipv4Net;
use mac_address::MacAddress;
use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair};
use prefix_trie::{PrefixMap, PrefixSet};
use prefix_trie::PrefixMap;
use serial_test::serial;
use smoltcp::wire::{Ipv4Address, Ipv4Packet};
use std::collections::HashSet;
@@ -219,6 +297,27 @@ mod tests {
assert!(allowed_from_vm_ipv4(&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 proxy = create_proxy(vm_ip, vec!["@host"], vec!["0.0.0.0/0"]);
assert_eq!(
proxy.rules,
PrefixMap::from_iter(vec![
(proxy.host.gateway_ip.into(), Action::Allow),
(Ipv4Net::from_str("0.0.0.0/0").unwrap(), Action::Block),
])
);
// Access to global IPs should be disallowed because of --block=0.0.0.0/0
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "8.8.8.8").is_none());
// Despite the above, access to host IP address should be possible because of --allow=@host
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, &proxy.host.gateway_ip.to_string()).is_some());
}
fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
let (vm_fd, _) = socketpair(
AddressFamily::Unix,
@@ -233,16 +332,14 @@ mod tests {
vm_fd.as_raw_fd(),
MacAddress::from_str("02:00:00:00:00:01").unwrap(),
NetType::Nat,
PrefixSet::from_iter(
allow
.into_iter()
.map(|cidr| Ipv4Net::from_str(cidr).unwrap()),
),
PrefixSet::from_iter(
block
.into_iter()
.map(|cidr| Ipv4Net::from_str(cidr).unwrap()),
),
allow
.into_iter()
.map(|cidr| cidr.parse().unwrap())
.collect(),
block
.into_iter()
.map(|cidr| cidr.parse().unwrap())
.collect(),
Vec::default(),
)
.unwrap();
+12
View File
@@ -65,6 +65,18 @@ impl Proxy<'_> {
{
let dst_addr = ipv4_pkt.dst_addr();
if self.rules_mac.iter().any(|(mac, action)| {
*action == Action::Block && self.dhcp_snooper_global.valid_ip_for_mac(mac, dst_addr)
}) {
return None;
}
if self.rules_mac.iter().any(|(mac, action)| {
*action == Action::Allow && self.dhcp_snooper_global.valid_ip_for_mac(mac, dst_addr)
}) {
return Some(());
}
// Filter traffic based on user-specified rules first
if !self.rules.is_empty() {
let dst_net = Ipv4Net::from(dst_addr);
+15 -12
View File
@@ -1,14 +1,13 @@
use anyhow::{Context, anyhow};
use clap::Parser;
use ipnet::Ipv4Net;
use log::LevelFilter;
use nix::sys::signal::{SigHandler, Signal, signal};
use oslog::OsLogger;
use prefix_trie::PrefixSet;
use privdrop::PrivDrop;
use softnet::NetType;
use softnet::proxy::ExposedPort;
use softnet::proxy::Proxy;
use softnet::proxy::Target;
use std::borrow::Cow;
use std::env;
use std::os::raw::c_int;
@@ -53,29 +52,33 @@ struct Args {
#[clap(
long,
help = "Comma-separated list of CIDRs to allow the traffic to \
(e.g. --allow=192.168.0.0/24 may be used to allow a LAN access for a VM). \
(e.g. --allow=192.168.0.0/24 may be used to allow a LAN access for a VM), \
plus supported @-aliases. Currently the only supported @-alias is @host, \
which matches the vmnet bridge gateway IP. \
When used with --block, the longest prefix match always wins. \
In case an identical prefix is both --allow'ed and --block'ed, \
blocking will take precedence. --allow=0.0.0.0/0 is a special case, \
it additionally disables bridge isolation (even when --block=0.0.0.0/0 is specified).",
value_name = "comma-separated CIDRs",
value_name = "comma-separated CIDRs, MAC addresses or @-aliases",
use_value_delimiter = true,
action = clap::ArgAction::Set
)]
allow: Vec<Ipv4Net>,
allow: Vec<Target>,
#[clap(
long,
help = "Comma-separated list of CIDRs to block the traffic to \
(e.g. --block=0.0.0.0/0 may be used to establish a default deny policy \
that is further relaxed with --allow). When used with --allow, \
the longest prefix match always wins. In case the same prefix is both \
--allow'ed and --block'ed, blocking takes precedence.",
value_name = "comma-separated CIDRs",
that is further relaxed with --allow), plus supported @-aliases. \
Currently the only supported @-alias is @host, which matches the vmnet bridge gateway IP. \
When used with --allow, the longest prefix match always wins. \
In case an identical prefix is both --allow'ed and --block'ed, \
blocking will take precedence.",
value_name = "comma-separated CIDRs, MAC addresses or @-aliases",
use_value_delimiter = true,
action = clap::ArgAction::Set
)]
block: Vec<Ipv4Net>,
block: Vec<Target>,
#[clap(
long,
@@ -196,8 +199,8 @@ fn try_main() -> anyhow::Result<()> {
args.vm_fd as RawFd,
args.vm_mac_address,
args.vm_net_type,
PrefixSet::from_iter(args.allow),
PrefixSet::from_iter(args.block),
args.allow,
args.block,
args.expose,
)
.context("failed to initialize proxy")?;