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:
parent
fa7c0693cb
commit
26168e95b8
9 changed files with 67 additions and 270 deletions
22
CLAUDE.md
22
CLAUDE.md
|
|
@ -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; };
|
||||
# }
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 &
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue