refactor: remove VmRole, unify config files to client.ip/client.mac

The router/client role distinction in vm-switch is no longer meaningful
with L3 IP-based forwarding. All VMs now use client.ip/client.mac/
client.sock uniformly; the only behavioral difference is the
receive_broadcast flag file.

- Remove VmRole enum and all role-based logic from Rust code
- Rename NixOS option vmNetwork.router to vmNetwork.receiveBroadcast
- Remove "exactly one router per network" assertion
- Update bufferbloat-test, documentation, and all tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-02-13 19:06:26 +00:00
parent fa7c0693cb
commit 26168e95b8
9 changed files with 67 additions and 270 deletions

View file

@ -109,15 +109,15 @@ The console relay service (`vmsilo-<name>-console-relay.service`) bridges crosvm
- Shuts down after `autoShutdown.after` seconds of inactivity
**VM-to-VM Networking**: VMs can communicate via `network.interfaces` with `type = "vm-switch"`:
- Each network has one router VM and multiple client VMs
- Uses vhost-user-net backed by vm-switch daemon
- VMs are connected via vhost-user-net backed by vm-switch daemon
- Config files written to `/run/vm-switch/<network>/<vm>/`:
- `<role>.ip` - VM's IPv4 address (e.g., `10.0.0.1`)
- `<role>.mac` - VM's MAC address
- `receive_broadcast` - flag file (present for router VMs)
- `client.ip` - VM's IPv4 address (e.g., `10.0.0.1`)
- `client.mac` - VM's MAC address
- `client.sock` - vhost-user socket path
- `receive_broadcast` - optional flag file (present for VMs that should receive broadcast traffic)
- `peers/<name>` - optional ACL directory for allowed peers
- vm-switch monitors config files (requires both `.ip` and `.mac`) and creates sockets
- crosvm connects via `--vhost-user type=net,socket=...,pci-device=0:<ifIndex>.0`
- vm-switch monitors config files (requires both `client.ip` and `client.mac`) and creates sockets
- crosvm connects via `--vhost-user type=net,socket=<client.sock>,pci-device=0:<ifIndex>.0`
### rootfs-nixos Package
@ -171,8 +171,8 @@ nix build .#vm-switch
# In another terminal, create test config files
mkdir -p /tmp/test-switch/router
echo "10.0.0.254" > /tmp/test-switch/router/router.ip
echo "52:00:00:00:00:01" > /tmp/test-switch/router/router.mac
echo "10.0.0.254" > /tmp/test-switch/router/client.ip
echo "52:00:00:00:00:01" > /tmp/test-switch/router/client.mac
touch /tmp/test-switch/router/receive_broadcast
mkdir -p /tmp/test-switch/client1
@ -191,7 +191,7 @@ echo "52:00:00:00:00:02" > /tmp/test-switch/client1/client.mac
- `src/main.rs` - Entry point, sandbox/seccomp setup, async event loop, SIGCHLD handling
- `src/manager.rs` - BackendManager: fork children, buffer exchange, crash cleanup
- `src/args.rs` - CLI argument parsing (clap)
- `src/config.rs` - VM configuration types (VmRole, VmConfig with ip, mac, receive_broadcast, allowed_peers)
- `src/config.rs` - VM configuration types (VmConfig with ip, mac, receive_broadcast, allowed_peers)
- `src/watcher.rs` - Config directory file watcher (inotify + debouncer)
- `src/mac.rs` - MAC address type and parsing
- `src/control.rs` - Main-child IPC over Unix seqpacket sockets + SCM_RIGHTS
@ -331,7 +331,7 @@ Two filter tiers (child is a strict subset of main):
# Or for VM-to-VM networking:
# {
# type = "vm-switch";
# vmNetwork = { name = "internal"; router = true; };
# vmNetwork = { name = "internal"; receiveBroadcast = true; };
# }
];
};

View file

@ -36,8 +36,8 @@ log "Temp dir: $TMPDIR"
# Create vm-switch config files (L3 format: ip + mac + receive_broadcast)
mkdir -p "$TMPDIR/server" "$TMPDIR/client"
echo "52:00:00:00:00:01" > "$TMPDIR/server/router.mac"
echo "10.0.0.1" > "$TMPDIR/server/router.ip"
echo "52:00:00:00:00:01" > "$TMPDIR/server/client.mac"
echo "10.0.0.1" > "$TMPDIR/server/client.ip"
touch "$TMPDIR/server/receive_broadcast"
echo "52:00:00:00:00:02" > "$TMPDIR/client/client.mac"
echo "10.0.0.2" > "$TMPDIR/client/client.ip"
@ -57,13 +57,13 @@ SWITCH_PID=$!
# Wait for sockets to appear
log "Waiting for vhost-user sockets..."
for i in {1..30}; do
if [[ -S "$TMPDIR/server/router.sock" && -S "$TMPDIR/client/client.sock" ]]; then
if [[ -S "$TMPDIR/server/client.sock" && -S "$TMPDIR/client/client.sock" ]]; then
break
fi
sleep 0.5
done
if [[ ! -S "$TMPDIR/server/router.sock" ]]; then
if [[ ! -S "$TMPDIR/server/client.sock" ]]; then
echo "ERROR: server socket not created" >&2
cat "$TMPDIR/switch.log" >&2
exit 1
@ -75,7 +75,7 @@ log "Starting server VM..."
-m 512 \
--block "path=@rootfs@/nixos.qcow2,ro=true" \
--serial type=file,path="$TMPDIR/server-serial.txt",hardware=virtio-console,console \
--vhost-user "type=net,socket=$TMPDIR/server/router.sock,pci-address=00:16.0" \
--vhost-user "type=net,socket=$TMPDIR/server/client.sock,pci-address=00:16.0" \
-p "console=hvc0 init=@systemToplevel@/init benchrole=server peerip=10.0.0.2 ip=10.0.0.1:::255.255.255.0::enp0s22:none" \
@rootfs@/bzImage \
--initrd @rootfs@/initrd &

View file

@ -160,22 +160,6 @@ let
) vm.network.interfaces
) cfg.nixosVms;
# Count routers on a specific network
routerCount =
netName:
lib.length (
lib.filter (
vm:
lib.any (
iface:
iface.type == "vm-switch"
&& iface.vmNetwork != null
&& iface.vmNetwork.name == netName
&& iface.vmNetwork.router
) vm.network.interfaces
) cfg.nixosVms
);
# Find the interface index for a VM on a specific network
getVmNetworkIfaceIdx =
vm: netName:
@ -360,8 +344,7 @@ let
"--net tap-name=${tapName},mac=${mac},pci-address=${pciAddr}"
else
let
role = if iface.vmNetwork.router then "router" else "client";
socket = "/run/vm-switch/${iface.vmNetwork.name}/${vm.name}/${role}.sock";
socket = "/run/vm-switch/${iface.vmNetwork.name}/${vm.name}/client.sock";
in
"--vhost-user type=net,socket=${socket},pci-address=${pciAddr}"
) vm.network.interfaces
@ -1042,11 +1025,6 @@ in
message = "programs.vmsilo.user '${cfg.user}' must have an explicit uid set in users.users";
}
]
# VM network assertions: exactly one router per network
++ map (netName: {
assertion = routerCount netName == 1;
message = "VM network '${netName}' must have exactly one router. Found ${toString (routerCount netName)}.";
}) allVmNetworkNames
# Network interface assertions
++ lib.concatMap (
vm:
@ -1164,29 +1142,24 @@ in
let
idx = getVmNetworkIfaceIdx vm netName;
iface = builtins.elemAt vm.network.interfaces idx;
vmType = if iface.vmNetwork.router then "router" else "client";
wrongType = if vmType == "router" then "client" else "router";
mac = getEffectiveIfaceMac vm idx iface;
# Extract first IP address (strip prefix length)
ip = builtins.head (builtins.split "/" (builtins.head iface.addresses));
in
''
# VM: ${vm.name} (${vmType})
# VM: ${vm.name}
mkdir -p /run/vm-switch/${netName}/${vm.name}
rm -f /run/vm-switch/${netName}/${vm.name}/${wrongType}.ip
rm -f /run/vm-switch/${netName}/${vm.name}/${wrongType}.mac
rm -f /run/vm-switch/${netName}/${vm.name}/${wrongType}.sock
echo '${ip}' > /run/vm-switch/${netName}/${vm.name}/${vmType}.ip.tmp
mv /run/vm-switch/${netName}/${vm.name}/${vmType}.ip.tmp \
/run/vm-switch/${netName}/${vm.name}/${vmType}.ip
echo '${ip}' > /run/vm-switch/${netName}/${vm.name}/client.ip.tmp
mv /run/vm-switch/${netName}/${vm.name}/client.ip.tmp \
/run/vm-switch/${netName}/${vm.name}/client.ip
echo '${mac}' > /run/vm-switch/${netName}/${vm.name}/${vmType}.mac.tmp
mv /run/vm-switch/${netName}/${vm.name}/${vmType}.mac.tmp \
/run/vm-switch/${netName}/${vm.name}/${vmType}.mac
echo '${mac}' > /run/vm-switch/${netName}/${vm.name}/client.mac.tmp
mv /run/vm-switch/${netName}/${vm.name}/client.mac.tmp \
/run/vm-switch/${netName}/${vm.name}/client.mac
${
if iface.vmNetwork.router then
if iface.vmNetwork.receiveBroadcast then
"touch /run/vm-switch/${netName}/${vm.name}/receive_broadcast"
else
"rm -f /run/vm-switch/${netName}/${vm.name}/receive_broadcast"

View file

@ -37,10 +37,10 @@ let
description = "Network name for vm-switch.";
example = "internal";
};
router = lib.mkOption {
receiveBroadcast = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether this VM is the router for this network.";
description = "Whether this VM receives broadcast traffic on this network.";
};
};
}

View file

@ -8,53 +8,11 @@ use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};
use thiserror::Error;
/// Role of a VM in the network topology.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VmRole {
/// Router VM - can communicate with all clients.
Router,
/// Client VM - can only communicate with router.
Client,
}
impl VmRole {
/// Detect role from a .mac filename.
///
/// Returns `Some(VmRole::Router)` for "router.mac",
/// `Some(VmRole::Client)` for "client.mac",
/// `None` for other filenames.
pub fn from_filename(filename: &str) -> Option<Self> {
match filename {
"router.mac" => Some(VmRole::Router),
"client.mac" => Some(VmRole::Client),
_ => None,
}
}
/// Returns the expected socket filename for this role.
pub fn socket_filename(&self) -> &'static str {
match self {
VmRole::Router => "router.sock",
VmRole::Client => "client.sock",
}
}
/// Returns the expected MAC filename for this role.
pub fn mac_filename(&self) -> &'static str {
match self {
VmRole::Router => "router.mac",
VmRole::Client => "client.mac",
}
}
}
/// Configuration for a VM's network connection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VmConfig {
/// Name of the VM (directory name).
pub name: String,
/// Role in the network topology.
pub role: VmRole,
/// IPv4 address for this connection.
pub ip: Ipv4Addr,
/// MAC address for this connection.
@ -67,10 +25,9 @@ pub struct VmConfig {
impl VmConfig {
/// Create a new VM configuration.
pub fn new(name: impl Into<String>, role: VmRole, ip: Ipv4Addr, mac: Mac) -> Self {
pub fn new(name: impl Into<String>, ip: Ipv4Addr, mac: Mac) -> Self {
Self {
name: name.into(),
role,
ip,
mac,
receive_broadcast: false,
@ -79,19 +36,6 @@ impl VmConfig {
}
}
/// Errors that can occur when reading a .mac file.
#[derive(Debug, Error)]
pub enum MacFileError {
#[error("failed to read file: {0}")]
Io(#[from] io::Error),
#[error("invalid MAC address: {0}")]
InvalidMac(#[from] MacParseError),
#[error("unrecognized filename: {0}")]
UnrecognizedFilename(String),
}
/// Errors that can occur when reading VM config.
#[derive(Debug, Error)]
pub enum VmConfigError {
@ -103,9 +47,6 @@ pub enum VmConfigError {
#[error("invalid IP address: {0}")]
InvalidIp(#[from] std::net::AddrParseError),
#[error("unrecognized role files")]
UnrecognizedRole,
}
/// Events emitted when VM configuration changes.
@ -137,7 +78,7 @@ pub fn parse_mac_content(content: &str) -> Result<Mac, MacParseError> {
/// Read VM configuration from a directory.
///
/// Looks for `client.ip` + `client.mac` or `router.ip` + `router.mac`.
/// Looks for `client.ip` + `client.mac`.
/// Optionally reads `receive_broadcast` flag and `peers/` directory.
pub fn read_vm_config(dir: &Path) -> Result<VmConfig, VmConfigError> {
let name = dir
@ -146,21 +87,12 @@ pub fn read_vm_config(dir: &Path) -> Result<VmConfig, VmConfigError> {
.unwrap_or("unknown")
.to_string();
// Determine role by checking which files exist
let (role, ip_file, mac_file) = if dir.join("router.ip").exists() {
(VmRole::Router, "router.ip", "router.mac")
} else if dir.join("client.ip").exists() {
(VmRole::Client, "client.ip", "client.mac")
} else {
return Err(VmConfigError::UnrecognizedRole);
};
// Read IP
let ip_content = fs::read_to_string(dir.join(ip_file))?;
let ip_content = fs::read_to_string(dir.join("client.ip"))?;
let ip: Ipv4Addr = ip_content.trim().parse()?;
// Read MAC
let mac_content = fs::read_to_string(dir.join(mac_file))?;
let mac_content = fs::read_to_string(dir.join("client.mac"))?;
let mac = parse_mac_content(&mac_content)?;
// Read optional receive_broadcast flag
@ -180,7 +112,6 @@ pub fn read_vm_config(dir: &Path) -> Result<VmConfig, VmConfigError> {
Ok(VmConfig {
name,
role,
ip,
mac,
receive_broadcast,
@ -228,62 +159,23 @@ mod tests {
fn create_vm_config(
dir: &TempDir,
vm_name: &str,
role: &str,
ip: &str,
mac: &str,
) -> PathBuf {
let vm_dir = dir.path().join(vm_name);
fs::create_dir_all(&vm_dir).unwrap();
fs::write(vm_dir.join(format!("{}.ip", role)), format!("{}\n", ip)).unwrap();
fs::write(vm_dir.join(format!("{}.mac", role)), format!("{}\n", mac)).unwrap();
fs::write(vm_dir.join("client.ip"), format!("{}\n", ip)).unwrap();
fs::write(vm_dir.join("client.mac"), format!("{}\n", mac)).unwrap();
vm_dir
}
#[test]
fn role_from_filename_router() {
assert_eq!(VmRole::from_filename("router.mac"), Some(VmRole::Router));
}
#[test]
fn role_from_filename_client() {
assert_eq!(VmRole::from_filename("client.mac"), Some(VmRole::Client));
}
#[test]
fn role_from_filename_unknown() {
assert_eq!(VmRole::from_filename("other.mac"), None);
assert_eq!(VmRole::from_filename("router.txt"), None);
assert_eq!(VmRole::from_filename(""), None);
}
#[test]
fn role_socket_filename_router() {
assert_eq!(VmRole::Router.socket_filename(), "router.sock");
}
#[test]
fn role_socket_filename_client() {
assert_eq!(VmRole::Client.socket_filename(), "client.sock");
}
#[test]
fn role_mac_filename_router() {
assert_eq!(VmRole::Router.mac_filename(), "router.mac");
}
#[test]
fn role_mac_filename_client() {
assert_eq!(VmRole::Client.mac_filename(), "client.mac");
}
#[test]
fn vm_config_new() {
let mac = Mac::parse("aa:bb:cc:dd:ee:ff").unwrap();
let ip = Ipv4Addr::new(10, 0, 0, 1);
let config = VmConfig::new("banking", VmRole::Client, ip, mac);
let config = VmConfig::new("banking", ip, mac);
assert_eq!(config.name, "banking");
assert_eq!(config.role, VmRole::Client);
assert_eq!(config.ip, ip);
assert_eq!(config.mac, mac);
assert!(!config.receive_broadcast);
@ -318,7 +210,7 @@ mod tests {
fn config_event_vm_added() {
let mac = Mac::parse("aa:bb:cc:dd:ee:ff").unwrap();
let ip = Ipv4Addr::new(10, 0, 0, 1);
let config = VmConfig::new("banking", VmRole::Client, ip, mac);
let config = VmConfig::new("banking", ip, mac);
let event = ConfigEvent::VmAdded {
path: PathBuf::from("/run/vm-switch/banking"),
config: config.clone(),
@ -350,12 +242,11 @@ mod tests {
#[test]
fn read_vm_config_with_ip_and_mac() {
let dir = TempDir::new().unwrap();
let vm_dir = create_vm_config(&dir, "banking", "client", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
let vm_dir = create_vm_config(&dir, "banking", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
let config = read_vm_config(&vm_dir).unwrap();
assert_eq!(config.name, "banking");
assert_eq!(config.role, VmRole::Client);
assert_eq!(config.ip, Ipv4Addr::new(10, 0, 0, 1));
assert_eq!(config.mac, Mac::parse("aa:bb:cc:dd:ee:ff").unwrap());
}
@ -363,19 +254,20 @@ mod tests {
#[test]
fn read_vm_config_router() {
let dir = TempDir::new().unwrap();
let vm_dir = create_vm_config(&dir, "router", "router", "10.0.0.254", "11:22:33:44:55:66");
let vm_dir = create_vm_config(&dir, "gateway", "10.0.0.254", "11:22:33:44:55:66");
fs::write(vm_dir.join("receive_broadcast"), "").unwrap();
let config = read_vm_config(&vm_dir).unwrap();
assert_eq!(config.name, "router");
assert_eq!(config.role, VmRole::Router);
assert_eq!(config.name, "gateway");
assert_eq!(config.ip, Ipv4Addr::new(10, 0, 0, 254));
assert!(config.receive_broadcast);
}
#[test]
fn read_vm_config_with_receive_broadcast() {
let dir = TempDir::new().unwrap();
let vm_dir = create_vm_config(&dir, "router", "router", "10.0.0.254", "11:22:33:44:55:66");
let vm_dir = create_vm_config(&dir, "router", "10.0.0.254", "11:22:33:44:55:66");
fs::write(vm_dir.join("receive_broadcast"), "").unwrap();
let config = read_vm_config(&vm_dir).unwrap();
@ -386,7 +278,7 @@ mod tests {
#[test]
fn read_vm_config_with_peers_directory() {
let dir = TempDir::new().unwrap();
let vm_dir = create_vm_config(&dir, "client1", "client", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
let vm_dir = create_vm_config(&dir, "client1", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
let peers_dir = vm_dir.join("peers");
fs::create_dir_all(&peers_dir).unwrap();
fs::write(peers_dir.join("router"), "").unwrap();
@ -421,8 +313,8 @@ mod tests {
#[test]
fn scan_config_dir_finds_vm_configs() {
let dir = TempDir::new().unwrap();
create_vm_config(&dir, "banking", "client", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
create_vm_config(&dir, "gateway", "router", "10.0.0.254", "11:22:33:44:55:66");
create_vm_config(&dir, "banking", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
create_vm_config(&dir, "gateway", "10.0.0.254", "11:22:33:44:55:66");
let results = scan_config_dir(dir.path());
@ -433,20 +325,17 @@ mod tests {
assert!(banking.is_some());
let (path, config) = banking.unwrap();
assert!(path.ends_with("banking"));
assert_eq!(config.role, VmRole::Client);
assert_eq!(config.ip, Ipv4Addr::new(10, 0, 0, 1));
// Find gateway router
// Find gateway
let gateway = results.iter().find(|(_, c)| c.name == "gateway");
assert!(gateway.is_some());
let (_, config) = gateway.unwrap();
assert_eq!(config.role, VmRole::Router);
}
#[test]
fn scan_config_dir_skips_invalid_configs() {
let dir = TempDir::new().unwrap();
create_vm_config(&dir, "valid", "client", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
create_vm_config(&dir, "valid", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
// Invalid - missing IP
let invalid_dir = dir.path().join("invalid");
fs::create_dir_all(&invalid_dir).unwrap();

View file

@ -42,7 +42,7 @@ pub mod watcher;
// Re-export commonly used types
pub use args::{init_logging, Args, LogLevel};
pub use config::{ConfigEvent, VmConfig, VmRole};
pub use config::{ConfigEvent, VmConfig};
pub use control::{ChildToMain, ControlChannel, ControlError, MainToChild, MAX_FDS, MAX_MESSAGE_SIZE};
pub use fq_codel::FqCodelConfig;
pub use metrics::{ChildMetrics, FlowIdentity, FlowMetrics, PeerMetrics, Stats, SwitchMetrics};

View file

@ -13,7 +13,7 @@ use tokio::io::unix::AsyncFd;
use tokio::sync::mpsc;
use tracing::{debug, info, trace, warn};
use crate::config::{ConfigEvent, VmConfig, VmRole};
use crate::config::{ConfigEvent, VmConfig};
use crate::control::{ChildToMain, ControlChannel, ControlError, MainToChild};
use crate::fq_codel::FqCodelConfig;
use crate::metrics::SwitchMetrics;
@ -58,8 +58,6 @@ struct ChildState {
pid: Pid,
/// Control channel for communication with child.
control: ControlChannel,
/// VM's role (router or client).
role: VmRole,
/// Child's IPv4 address.
ip: Ipv4Addr,
/// Child's MAC address.
@ -88,7 +86,6 @@ impl ChildState {
fn new(
pid: Pid,
control: ControlChannel,
role: VmRole,
ip: Ipv4Addr,
mac: [u8; 6],
receive_broadcast: bool,
@ -98,7 +95,6 @@ impl ChildState {
Self {
pid,
control,
role,
ip,
mac,
receive_broadcast,
@ -220,13 +216,6 @@ impl BackendManager {
}
}
/// Find the name of the current router child, if any.
fn find_router(&self) -> Option<String> {
self.children.iter()
.find(|(_, state)| state.role == VmRole::Router)
.map(|(name, _)| name.clone())
}
/// Check if a child process is running for a VM.
pub fn is_daemon_running(&self, vm_name: &str) -> bool {
self.children.contains_key(vm_name)
@ -305,7 +294,6 @@ impl BackendManager {
let restart_info = (
crate::mac::Mac::from_bytes(child.mac),
child.ip,
child.role,
child.receive_broadcast,
child.allowed_peers.clone(),
child.socket_path.clone(),
@ -341,9 +329,9 @@ impl BackendManager {
}
// Restart children
for (vm_name, (mac, ip, role, receive_broadcast, allowed_peers, socket_path)) in to_restart {
for (vm_name, (mac, ip, receive_broadcast, allowed_peers, socket_path)) in to_restart {
info!(vm = %vm_name, "Restarting child process");
match self.fork_child(&vm_name, mac, ip, role, receive_broadcast, allowed_peers, &socket_path) {
match self.fork_child(&vm_name, mac, ip, receive_broadcast, allowed_peers, &socket_path) {
Ok(child_state) => {
self.children.insert(vm_name, child_state);
}
@ -694,7 +682,6 @@ impl BackendManager {
vm_name: &str,
mac: crate::mac::Mac,
ip: Ipv4Addr,
role: VmRole,
receive_broadcast: bool,
allowed_peers: Option<HashSet<String>>,
socket_path: &Path,
@ -765,7 +752,6 @@ impl BackendManager {
Ok(ChildState::new(
child,
main_end,
role,
ip,
mac.bytes(),
receive_broadcast,
@ -804,24 +790,10 @@ impl BackendManager {
self.remove_vm(&name);
}
// Handle router replacement
if config.role == VmRole::Router {
if let Some(old_router) = self.find_router() {
warn!(
old_router = %old_router,
new_router = %config.name,
"Replacing existing router"
);
self.remove_vm(&old_router);
}
}
let socket_name = config.role.socket_filename();
let socket_path = vm_dir.join(socket_name);
let socket_path = vm_dir.join("client.sock");
info!(
vm = %config.name,
role = ?config.role,
ip = %config.ip,
mac = %config.mac,
socket = ?socket_path,
@ -831,10 +803,9 @@ impl BackendManager {
let vm_name = config.name.clone();
let mac = config.mac;
let ip = config.ip;
let role = config.role;
let receive_broadcast = config.receive_broadcast;
let allowed_peers = config.allowed_peers.clone();
match self.fork_child(&vm_name, mac, ip, role, receive_broadcast, allowed_peers, &socket_path) {
match self.fork_child(&vm_name, mac, ip, receive_broadcast, allowed_peers, &socket_path) {
Ok(child_state) => {
self.children.insert(vm_name, child_state);
}
@ -906,7 +877,6 @@ mod tests {
let mut child = ChildState::new(
Pid::from_raw(1234),
main_end,
VmRole::Client,
Ipv4Addr::new(10, 0, 0, 1),
[1, 2, 3, 4, 5, 6],
false,
@ -931,7 +901,6 @@ mod tests {
let mut child = ChildState::new(
Pid::from_raw(1234),
main_end,
VmRole::Client,
Ipv4Addr::new(10, 0, 0, 1),
[1, 2, 3, 4, 5, 6],
false,
@ -952,36 +921,6 @@ mod tests {
assert!(!child.pending_buffers.contains_key("router"));
}
#[test]
fn child_state_stores_role() {
let (main_end, _child_end) = ControlChannel::pair().unwrap();
let client = ChildState::new(
Pid::from_raw(1),
main_end,
VmRole::Client,
Ipv4Addr::new(10, 0, 0, 1),
[1, 2, 3, 4, 5, 6],
false,
None,
std::path::PathBuf::from("/tmp/client.sock"),
);
assert_eq!(client.role, VmRole::Client);
let (main_end2, _child_end2) = ControlChannel::pair().unwrap();
let router = ChildState::new(
Pid::from_raw(2),
main_end2,
VmRole::Router,
Ipv4Addr::new(10, 0, 0, 254),
[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff],
true,
None,
std::path::PathBuf::from("/tmp/router.sock"),
);
assert_eq!(router.role, VmRole::Router);
}
#[test]
fn peers_can_communicate_both_allow_all() {
// Both have no ACL restrictions

View file

@ -230,7 +230,6 @@ impl Drop for ConfigWatcher {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::VmRole;
use std::fs;
use std::net::Ipv4Addr;
use std::path::PathBuf;
@ -240,22 +239,21 @@ mod tests {
fn create_vm_config(
dir: &Path,
vm_name: &str,
role: &str,
ip: &str,
mac: &str,
) -> PathBuf {
let vm_dir = dir.join(vm_name);
fs::create_dir_all(&vm_dir).unwrap();
fs::write(vm_dir.join(format!("{}.ip", role)), format!("{}\n", ip)).unwrap();
fs::write(vm_dir.join(format!("{}.mac", role)), format!("{}\n", mac)).unwrap();
fs::write(vm_dir.join("client.ip"), format!("{}\n", ip)).unwrap();
fs::write(vm_dir.join("client.mac"), format!("{}\n", mac)).unwrap();
vm_dir
}
#[tokio::test]
async fn watcher_emits_initial_scan_events() {
let dir = TempDir::new().unwrap();
create_vm_config(dir.path(), "banking", "client", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
create_vm_config(dir.path(), "gateway", "router", "10.0.0.254", "11:22:33:44:55:66");
create_vm_config(dir.path(), "banking", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
create_vm_config(dir.path(), "gateway", "10.0.0.254", "11:22:33:44:55:66");
let watcher = ConfigWatcher::new(dir.path(), 16).unwrap();
let mut rx = watcher.into_receiver();
@ -294,7 +292,7 @@ mod tests {
let mut rx = watcher.subscribe();
// Create VM config with both IP and MAC files
create_vm_config(dir.path(), "banking", "client", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
create_vm_config(dir.path(), "banking", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
// Wait for the event (allow extra time for debounce delay)
let event = tokio::time::timeout(Duration::from_secs(3), rx.recv())
@ -305,7 +303,6 @@ mod tests {
match event {
ConfigEvent::VmAdded { config, .. } => {
assert_eq!(config.name, "banking");
assert_eq!(config.role, VmRole::Client);
assert_eq!(config.ip, Ipv4Addr::new(10, 0, 0, 1));
}
_ => panic!("Expected VmAdded event"),
@ -320,7 +317,7 @@ mod tests {
let dir = TempDir::new().unwrap();
// Create a full VM config first
let vm_dir = create_vm_config(dir.path(), "banking", "client", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
let vm_dir = create_vm_config(dir.path(), "banking", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
let ip_path = vm_dir.join("client.ip");
// Create watcher (will emit VmAdded for existing file)
@ -360,7 +357,7 @@ mod tests {
let mut rx2 = watcher.subscribe();
// Create a full VM config
create_vm_config(dir.path(), "test", "client", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
create_vm_config(dir.path(), "test", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
// Both receivers should get the event (allow extra time for debounce delay)
let event1 = tokio::time::timeout(Duration::from_secs(3), rx1.recv())
@ -383,7 +380,7 @@ mod tests {
let dir = TempDir::new().unwrap();
// Create initial VM config
let vm_dir = create_vm_config(dir.path(), "banking", "client", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
let vm_dir = create_vm_config(dir.path(), "banking", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
let ip_path = vm_dir.join("client.ip");
// Create watcher (will emit VmAdded for existing file)
@ -415,7 +412,6 @@ mod tests {
match event {
ConfigEvent::VmAdded { config, .. } => {
assert_eq!(config.name, "banking");
assert_eq!(config.role, VmRole::Client);
assert_eq!(config.ip, Ipv4Addr::new(10, 0, 0, 2));
}
_ => panic!("Expected VmAdded event after modify, got {:?}", event),
@ -429,7 +425,7 @@ mod tests {
let dir = TempDir::new().unwrap();
// Create a VM config in a subdirectory
let vm_dir = create_vm_config(dir.path(), "banking", "client", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
let vm_dir = create_vm_config(dir.path(), "banking", "10.0.0.1", "aa:bb:cc:dd:ee:ff");
// Create watcher (will emit VmAdded for existing file)
let mut watcher = ConfigWatcher::new(dir.path(), 16).unwrap();

View file

@ -199,12 +199,12 @@ fn new_protocol_buffer_exchange() {
// Step 4: Main sends PutBuffer to B with A's ingress buffer
// B will use this as egress to A
// broadcast=true because A is a client and B is router
// broadcast=true because B has receive_broadcast set
let put_buffer_msg = MainToChild::PutBuffer {
peer_name: name_a.clone(),
peer_ip: ip_a,
peer_mac: mac_a,
broadcast: false, // A is client, not router
broadcast: false, // A does not have receive_broadcast
};
main_b.send_with_fds_typed(&put_buffer_msg, &[
fds_from_a[0].as_raw_fd(),
@ -295,24 +295,24 @@ fn bidirectional_new_protocol_exchange() {
let (_, fds_from_b): (ChildToMain, _) = main_b.recv_with_fds_typed().expect("main recv B BufferReady");
// Cross-send: A's ingress becomes B's egress to A, and vice versa
// Send A's buffer to B (B is router, so broadcast=true when sending TO router)
// Send A's buffer to B (B has receive_broadcast, so broadcast=true)
main_b.send_with_fds_typed(&MainToChild::PutBuffer {
peer_name: name_a.clone(),
peer_ip: ip_a,
peer_mac: mac_a,
broadcast: false, // A is not router
broadcast: false, // A does not have receive_broadcast
}, &[
fds_from_a[0].as_raw_fd(),
fds_from_a[1].as_raw_fd(),
fds_from_a[2].as_raw_fd(),
]).expect("PutBuffer A to B");
// Send B's buffer to A (A is client, broadcast=true because B is router)
// Send B's buffer to A (broadcast=true because B has receive_broadcast)
main_a.send_with_fds_typed(&MainToChild::PutBuffer {
peer_name: name_b.clone(),
peer_ip: ip_b,
peer_mac: mac_b,
broadcast: true, // B is router
broadcast: true, // B has receive_broadcast
}, &[
fds_from_b[0].as_raw_fd(),
fds_from_b[1].as_raw_fd(),