diff --git a/Cargo.toml b/Cargo.toml index abc99a7..46df2db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,10 @@ url = "2.5.0" cli-tables = "0.2.1" rand = "0.8.5" arrayvec = "0.7.4" +rtnetlink = "0.14.1" +futures = "0.3.30" +ipnetwork = "0.20.0" +smoltcp = "0.11.0" [workspace.dependencies.uuid] version = "1.6.1" @@ -55,3 +59,7 @@ default-features = false [workspace.dependencies.clap] version = "4.4.18" features = ["derive"] + +[workspace.dependencies.tokio] +version = "1.35.1" +features = ["macros", "rt", "rt-multi-thread"] diff --git a/hypha/Cargo.toml b/hypha/Cargo.toml index 70b51d4..acba865 100644 --- a/hypha/Cargo.toml +++ b/hypha/Cargo.toml @@ -26,6 +26,11 @@ sys-mount = { workspace = true } oci-spec = { workspace = true } backhand = { workspace = true } uuid = { workspace = true } +rtnetlink = { workspace = true } +tokio = { workspace = true } +futures = { workspace = true } +ipnetwork = { workspace = true } +smoltcp = { workspace = true } [dependencies.nix] workspace = true @@ -53,3 +58,7 @@ path = "bin/controller.rs" [[bin]] name = "hyphactr" path = "bin/container.rs" + +[[bin]] +name = "hyphanet" +path = "bin/network.rs" diff --git a/hypha/bin/container.rs b/hypha/bin/container.rs index 7f41c0c..a56de53 100644 --- a/hypha/bin/container.rs +++ b/hypha/bin/container.rs @@ -3,7 +3,8 @@ use env_logger::Env; use hypha::container::init::ContainerInit; use std::env; -fn main() -> Result<()> { +#[tokio::main] +async fn main() -> Result<()> { env::set_var("RUST_BACKTRACE", "1"); env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init(); if env::var("HYPHA_UNSAFE_ALWAYS_ALLOW_INIT").unwrap_or("0".to_string()) != "1" { @@ -18,6 +19,6 @@ fn main() -> Result<()> { } } let mut container = ContainerInit::new(); - container.init()?; + container.init().await?; Ok(()) } diff --git a/hypha/bin/controller.rs b/hypha/bin/controller.rs index 9b91e2d..b8e1268 100644 --- a/hypha/bin/controller.rs +++ b/hypha/bin/controller.rs @@ -110,12 +110,13 @@ fn main() -> Result<()> { Commands::List { .. } => { let containers = controller.list()?; let mut table = cli_tables::Table::new(); - let header = vec!["domain", "uuid", "image"]; + let header = vec!["domain", "uuid", "ipv4", "image"]; table.push_row(&header)?; for container in containers { let row = vec![ container.domid.to_string(), container.uuid.to_string(), + container.ipv4, container.image, ]; table.push_row_string(&row)?; diff --git a/hypha/bin/network.rs b/hypha/bin/network.rs new file mode 100644 index 0000000..b70f61e --- /dev/null +++ b/hypha/bin/network.rs @@ -0,0 +1,20 @@ +use anyhow::Result; +use clap::Parser; +use env_logger::Env; +use hypha::network::HyphaNetwork; + +#[derive(Parser, Debug)] +struct NetworkArgs { + #[arg(short, long)] + interface: String, + #[arg(short, long, default_value = "192.168.42.1/24")] + network: String, +} + +fn main() -> Result<()> { + env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init(); + let args = NetworkArgs::parse(); + let mut network = HyphaNetwork::new(&args.interface, &[&args.network])?; + network.run()?; + Ok(()) +} diff --git a/hypha/src/container/init.rs b/hypha/src/container/init.rs index e78e3b9..0568533 100644 --- a/hypha/src/container/init.rs +++ b/hypha/src/container/init.rs @@ -1,5 +1,7 @@ -use crate::shared::LaunchInfo; +use crate::shared::{LaunchInfo, LaunchNetwork}; use anyhow::{anyhow, Result}; +use futures::stream::TryStreamExt; +use ipnetwork::IpNetwork; use log::{trace, warn}; use nix::libc::{c_int, dup2, wait}; use nix::unistd::{execve, fork, ForkResult, Pid}; @@ -53,7 +55,7 @@ impl ContainerInit { ContainerInit {} } - pub fn init(&mut self) -> Result<()> { + pub async fn init(&mut self) -> Result<()> { self.early_init()?; trace!("opening console descriptor"); @@ -72,6 +74,13 @@ impl ContainerInit { self.mount_new_root()?; self.nuke_initrd()?; self.bind_new_root()?; + + if let Some(network) = &launch.network { + if let Err(error) = self.network_setup(network).await { + warn!("failed to initialize network: {}", error); + } + } + if let Some(cfg) = config.config() { self.run(cfg, &launch)?; } else { @@ -271,6 +280,38 @@ impl ContainerInit { Ok(()) } + async fn network_setup(&mut self, network: &LaunchNetwork) -> Result<()> { + trace!( + "setting up network with link {} and ipv4 {}", + network.link, + network.ipv4 + ); + + let (connection, handle, _) = rtnetlink::new_connection()?; + tokio::spawn(connection); + + let ip: IpNetwork = network.ipv4.parse()?; + + let mut links = handle + .link() + .get() + .match_name(network.link.clone()) + .execute(); + if let Some(link) = links.try_next().await? { + handle + .address() + .add(link.header.index, ip.ip(), ip.prefix()) + .execute() + .await?; + + handle.link().set(link.header.index).up().execute().await?; + } else { + warn!("unable to find link named {}", network.link); + } + + Ok(()) + } + fn run(&mut self, config: &Config, launch: &LaunchInfo) -> Result<()> { let mut cmd = match config.cmd() { None => vec![], diff --git a/hypha/src/ctl/mod.rs b/hypha/src/ctl/mod.rs index c819b0a..7d26719 100644 --- a/hypha/src/ctl/mod.rs +++ b/hypha/src/ctl/mod.rs @@ -5,11 +5,13 @@ use crate::ctl::cfgblk::ConfigBlock; use crate::image::cache::ImageCache; use crate::image::name::ImageName; use crate::image::{ImageCompiler, ImageInfo}; -use crate::shared::LaunchInfo; +use crate::shared::{LaunchInfo, LaunchNetwork}; use advmac::MacAddr6; use anyhow::{anyhow, Result}; +use ipnetwork::Ipv4Network; use loopdev::LoopControl; use std::io::{Read, Write}; +use std::net::Ipv4Addr; use std::path::PathBuf; use std::process::exit; use std::str::FromStr; @@ -36,6 +38,7 @@ pub struct ContainerInfo { pub domid: u32, pub image: String, pub loops: Vec, + pub ipv4: String, } impl Controller { @@ -77,7 +80,16 @@ impl Controller { let uuid = Uuid::new_v4(); let name = format!("hypha-{uuid}"); let image_info = self.compile(image)?; - let launch_config = LaunchInfo { env, run }; + + let ipv4 = self.allocate_ipv4()?; + let launch_config = LaunchInfo { + network: Some(LaunchNetwork { + link: "eth0".to_string(), + ipv4: format!("{}/24", ipv4), + }), + env, + run, + }; let cfgblk = ConfigBlock::new(&uuid, &image_info, config_bundle_path)?; cfgblk.build(&launch_config)?; @@ -102,7 +114,10 @@ impl Controller { let cmdline_options = [if debug { "debug" } else { "quiet" }, "elevator=noop"]; let cmdline = cmdline_options.join(" "); - let mac = MacAddr6::random().to_string().replace('-', ":"); + let mac = MacAddr6::random() + .to_string() + .replace('-', ":") + .to_lowercase(); let config = DomainConfig { backend_domid: 0, name: &name, @@ -126,8 +141,8 @@ impl Controller { vifs: vec![DomainNetworkInterface { mac: &mac, mtu: 1500, - bridge: "xenbr0", - script: "/etc/xen/scripts/vif-bridge", + bridge: None, + script: None, }], filesystems: vec![], extra_keys: vec![ @@ -144,6 +159,7 @@ impl Controller { ), ), ("hypha/image".to_string(), image.to_string()), + ("hypha/ipv4".to_string(), ipv4.to_string()), ], }; match self.client.create(&config) { @@ -267,12 +283,18 @@ impl Controller { .store .read_string_optional(&format!("{}/hypha/loops", &dom_path))? .unwrap_or("".to_string()); + let ipv4 = self + .client + .store + .read_string_optional(&format!("{}/hypha/ipv4", &dom_path))? + .unwrap_or("unknown".to_string()); let loops = Controller::parse_loop_set(&loops); containers.push(ContainerInfo { uuid, domid, image, loops, + ipv4, }); } Ok(containers) @@ -308,4 +330,37 @@ impl Controller { }) .collect::>() } + + fn allocate_ipv4(&mut self) -> Result { + let network = Ipv4Network::new(Ipv4Addr::new(192, 168, 42, 0), 24)?; + let mut used: Vec = vec![ + Ipv4Addr::new(192, 168, 42, 0), + Ipv4Addr::new(192, 168, 42, 1), + Ipv4Addr::new(192, 168, 42, 255), + ]; + for domid_candidate in self.client.store.list_any("/local/domain")? { + let dom_path = format!("/local/domain/{}", domid_candidate); + let ip_path = format!("{}/hypha/ipv4", dom_path); + let existing_ip = self.client.store.read_string_optional(&ip_path)?; + if let Some(existing_ip) = existing_ip { + used.push(Ipv4Addr::from_str(&existing_ip)?); + } + } + + let mut found: Option = None; + for ip in network.iter() { + if !used.contains(&ip) { + found = Some(ip); + break; + } + } + + if found.is_none() { + return Err(anyhow!( + "unable to find ipv4 to allocate to container, ipv4 addresses are exhausted" + )); + } + + Ok(found.unwrap()) + } } diff --git a/hypha/src/lib.rs b/hypha/src/lib.rs index 5d1be80..a2878fb 100644 --- a/hypha/src/lib.rs +++ b/hypha/src/lib.rs @@ -2,4 +2,5 @@ pub mod autoloop; pub mod container; pub mod ctl; pub mod image; +pub mod network; pub mod shared; diff --git a/hypha/src/network/mod.rs b/hypha/src/network/mod.rs new file mode 100644 index 0000000..fae2020 --- /dev/null +++ b/hypha/src/network/mod.rs @@ -0,0 +1,47 @@ +use std::os::fd::AsRawFd; +use std::str::FromStr; + +use advmac::MacAddr6; +use anyhow::{anyhow, Result}; +use smoltcp::iface::{Config, Interface, SocketSet}; +use smoltcp::phy::{self, RawSocket}; +use smoltcp::time::Instant; +use smoltcp::wire::{EthernetAddress, HardwareAddress, IpCidr}; + +pub struct HyphaNetwork { + pub device: RawSocket, + pub addresses: Vec, +} + +impl HyphaNetwork { + pub fn new(iface: &str, cidrs: &[&str]) -> Result { + let device = RawSocket::new(iface, smoltcp::phy::Medium::Ethernet)?; + let mut addresses: Vec = Vec::new(); + for cidr in cidrs { + let address = + IpCidr::from_str(cidr).map_err(|_| anyhow!("failed to parse cidr: {}", *cidr))?; + addresses.push(address); + } + Ok(HyphaNetwork { device, addresses }) + } + + pub fn run(&mut self) -> Result<()> { + let mac = MacAddr6::random(); + let mac = HardwareAddress::Ethernet(EthernetAddress(mac.to_array())); + let config = Config::new(mac); + let mut iface = Interface::new(config, &mut self.device, Instant::now()); + iface.update_ip_addrs(|addrs| { + addrs + .extend_from_slice(&self.addresses) + .expect("failed to set ip addresses"); + }); + + let mut sockets = SocketSet::new(vec![]); + let fd = self.device.as_raw_fd(); + loop { + let timestamp = Instant::now(); + iface.poll(timestamp, &mut self.device, &mut sockets); + phy::wait(fd, iface.poll_delay(timestamp, &sockets))?; + } + } +} diff --git a/hypha/src/shared/mod.rs b/hypha/src/shared/mod.rs index 44ac6b5..1281840 100644 --- a/hypha/src/shared/mod.rs +++ b/hypha/src/shared/mod.rs @@ -1,7 +1,14 @@ use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize, Debug)] +pub struct LaunchNetwork { + pub link: String, + pub ipv4: String, +} + #[derive(Serialize, Deserialize, Debug)] pub struct LaunchInfo { + pub network: Option, pub env: Option>, pub run: Option>, } diff --git a/initrd/build.sh b/initrd/build.sh index cb01d6e..7e8b553 100755 --- a/initrd/build.sh +++ b/initrd/build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -TARGET="x86_64-unknown-linux-gnu" +TARGET="x86_64-unknown-linux-musl" export RUSTFLAGS="-Ctarget-feature=+crt-static" cd "$(dirname "${0}")/.." diff --git a/libs/xen/xenclient/src/lib.rs b/libs/xen/xenclient/src/lib.rs index d15483a..a12343e 100644 --- a/libs/xen/xenclient/src/lib.rs +++ b/libs/xen/xenclient/src/lib.rs @@ -52,8 +52,8 @@ pub struct DomainFilesystem<'a> { pub struct DomainNetworkInterface<'a> { pub mac: &'a str, pub mtu: u32, - pub bridge: &'a str, - pub script: &'a str, + pub bridge: Option<&'a str>, + pub script: Option<&'a str>, } #[derive(Debug)] @@ -524,24 +524,38 @@ impl XenClient { vif: &DomainNetworkInterface, ) -> Result<()> { let id = 20 + index as u64; - let backend_items: Vec<(&str, String)> = vec![ + let mut backend_items: Vec<(&str, String)> = vec![ ("frontend-id", domid.to_string()), ("online", "1".to_string()), ("state", "1".to_string()), ("mac", vif.mac.to_string()), ("mtu", vif.mtu.to_string()), ("type", "vif".to_string()), - ("bridge", vif.bridge.to_string()), ("handle", id.to_string()), - ("script", vif.script.to_string()), - ("hotplug-status", "".to_string()), ]; + if vif.bridge.is_some() { + backend_items.extend_from_slice(&[("bridge", vif.bridge.unwrap().to_string())]); + } + + if vif.script.is_some() { + backend_items.extend_from_slice(&[ + ("script", vif.script.unwrap().to_string()), + ("hotplug-status", "".to_string()), + ]); + } else { + backend_items.extend_from_slice(&[ + ("script", "".to_string()), + ("hotplug-status", "connected".to_string()), + ]); + } + let frontend_items: Vec<(&str, String)> = vec![ ("backend-id", backend_domid.to_string()), ("state", "1".to_string()), ("mac", vif.mac.to_string()), ("trusted", "1".to_string()), + ("mtu", vif.mtu.to_string()), ]; self.device_add( diff --git a/scripts/fix.sh b/scripts/fix.sh index 0e56133..e8a73d2 100755 --- a/scripts/fix.sh +++ b/scripts/fix.sh @@ -2,5 +2,5 @@ set -e cd "$(dirname "${0}")/.." -cargo fmt --all cargo clippy --target x86_64-unknown-linux-gnu --fix --allow-dirty --allow-staged +cargo fmt --all diff --git a/scripts/hyphanet-debug.sh b/scripts/hyphanet-debug.sh new file mode 100755 index 0000000..2f5e56c --- /dev/null +++ b/scripts/hyphanet-debug.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +if [ -z "${RUST_LOG}" ] +then + RUST_LOG="INFO" +fi + +cd "$(dirname "${0}")/.." +cargo build --target x86_64-unknown-linux-gnu --bin hyphanet +exec sudo RUST_LOG="${RUST_LOG}" target/x86_64-unknown-linux-gnu/debug/hyphanet "${@}"