hypha: implement basic networking support

This commit is contained in:
Alex Zenla 2024-02-05 12:45:45 +00:00
parent 70dc2f943f
commit 21c6a27097
No known key found for this signature in database
GPG Key ID: 067B238899B51269
14 changed files with 233 additions and 18 deletions

View File

@ -43,6 +43,10 @@ url = "2.5.0"
cli-tables = "0.2.1" cli-tables = "0.2.1"
rand = "0.8.5" rand = "0.8.5"
arrayvec = "0.7.4" arrayvec = "0.7.4"
rtnetlink = "0.14.1"
futures = "0.3.30"
ipnetwork = "0.20.0"
smoltcp = "0.11.0"
[workspace.dependencies.uuid] [workspace.dependencies.uuid]
version = "1.6.1" version = "1.6.1"
@ -55,3 +59,7 @@ default-features = false
[workspace.dependencies.clap] [workspace.dependencies.clap]
version = "4.4.18" version = "4.4.18"
features = ["derive"] features = ["derive"]
[workspace.dependencies.tokio]
version = "1.35.1"
features = ["macros", "rt", "rt-multi-thread"]

View File

@ -26,6 +26,11 @@ sys-mount = { workspace = true }
oci-spec = { workspace = true } oci-spec = { workspace = true }
backhand = { workspace = true } backhand = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
rtnetlink = { workspace = true }
tokio = { workspace = true }
futures = { workspace = true }
ipnetwork = { workspace = true }
smoltcp = { workspace = true }
[dependencies.nix] [dependencies.nix]
workspace = true workspace = true
@ -53,3 +58,7 @@ path = "bin/controller.rs"
[[bin]] [[bin]]
name = "hyphactr" name = "hyphactr"
path = "bin/container.rs" path = "bin/container.rs"
[[bin]]
name = "hyphanet"
path = "bin/network.rs"

View File

@ -3,7 +3,8 @@ use env_logger::Env;
use hypha::container::init::ContainerInit; use hypha::container::init::ContainerInit;
use std::env; use std::env;
fn main() -> Result<()> { #[tokio::main]
async fn main() -> Result<()> {
env::set_var("RUST_BACKTRACE", "1"); env::set_var("RUST_BACKTRACE", "1");
env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init(); 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" { 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(); let mut container = ContainerInit::new();
container.init()?; container.init().await?;
Ok(()) Ok(())
} }

View File

@ -110,12 +110,13 @@ fn main() -> Result<()> {
Commands::List { .. } => { Commands::List { .. } => {
let containers = controller.list()?; let containers = controller.list()?;
let mut table = cli_tables::Table::new(); let mut table = cli_tables::Table::new();
let header = vec!["domain", "uuid", "image"]; let header = vec!["domain", "uuid", "ipv4", "image"];
table.push_row(&header)?; table.push_row(&header)?;
for container in containers { for container in containers {
let row = vec![ let row = vec![
container.domid.to_string(), container.domid.to_string(),
container.uuid.to_string(), container.uuid.to_string(),
container.ipv4,
container.image, container.image,
]; ];
table.push_row_string(&row)?; table.push_row_string(&row)?;

20
hypha/bin/network.rs Normal file
View File

@ -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(())
}

View File

@ -1,5 +1,7 @@
use crate::shared::LaunchInfo; use crate::shared::{LaunchInfo, LaunchNetwork};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use futures::stream::TryStreamExt;
use ipnetwork::IpNetwork;
use log::{trace, warn}; use log::{trace, warn};
use nix::libc::{c_int, dup2, wait}; use nix::libc::{c_int, dup2, wait};
use nix::unistd::{execve, fork, ForkResult, Pid}; use nix::unistd::{execve, fork, ForkResult, Pid};
@ -53,7 +55,7 @@ impl ContainerInit {
ContainerInit {} ContainerInit {}
} }
pub fn init(&mut self) -> Result<()> { pub async fn init(&mut self) -> Result<()> {
self.early_init()?; self.early_init()?;
trace!("opening console descriptor"); trace!("opening console descriptor");
@ -72,6 +74,13 @@ impl ContainerInit {
self.mount_new_root()?; self.mount_new_root()?;
self.nuke_initrd()?; self.nuke_initrd()?;
self.bind_new_root()?; 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() { if let Some(cfg) = config.config() {
self.run(cfg, &launch)?; self.run(cfg, &launch)?;
} else { } else {
@ -271,6 +280,38 @@ impl ContainerInit {
Ok(()) 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<()> { fn run(&mut self, config: &Config, launch: &LaunchInfo) -> Result<()> {
let mut cmd = match config.cmd() { let mut cmd = match config.cmd() {
None => vec![], None => vec![],

View File

@ -5,11 +5,13 @@ use crate::ctl::cfgblk::ConfigBlock;
use crate::image::cache::ImageCache; use crate::image::cache::ImageCache;
use crate::image::name::ImageName; use crate::image::name::ImageName;
use crate::image::{ImageCompiler, ImageInfo}; use crate::image::{ImageCompiler, ImageInfo};
use crate::shared::LaunchInfo; use crate::shared::{LaunchInfo, LaunchNetwork};
use advmac::MacAddr6; use advmac::MacAddr6;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ipnetwork::Ipv4Network;
use loopdev::LoopControl; use loopdev::LoopControl;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::Ipv4Addr;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::exit; use std::process::exit;
use std::str::FromStr; use std::str::FromStr;
@ -36,6 +38,7 @@ pub struct ContainerInfo {
pub domid: u32, pub domid: u32,
pub image: String, pub image: String,
pub loops: Vec<ContainerLoopInfo>, pub loops: Vec<ContainerLoopInfo>,
pub ipv4: String,
} }
impl Controller { impl Controller {
@ -77,7 +80,16 @@ impl Controller {
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
let name = format!("hypha-{uuid}"); let name = format!("hypha-{uuid}");
let image_info = self.compile(image)?; 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)?; let cfgblk = ConfigBlock::new(&uuid, &image_info, config_bundle_path)?;
cfgblk.build(&launch_config)?; cfgblk.build(&launch_config)?;
@ -102,7 +114,10 @@ impl Controller {
let cmdline_options = [if debug { "debug" } else { "quiet" }, "elevator=noop"]; let cmdline_options = [if debug { "debug" } else { "quiet" }, "elevator=noop"];
let cmdline = cmdline_options.join(" "); let cmdline = cmdline_options.join(" ");
let mac = MacAddr6::random().to_string().replace('-', ":"); let mac = MacAddr6::random()
.to_string()
.replace('-', ":")
.to_lowercase();
let config = DomainConfig { let config = DomainConfig {
backend_domid: 0, backend_domid: 0,
name: &name, name: &name,
@ -126,8 +141,8 @@ impl Controller {
vifs: vec![DomainNetworkInterface { vifs: vec![DomainNetworkInterface {
mac: &mac, mac: &mac,
mtu: 1500, mtu: 1500,
bridge: "xenbr0", bridge: None,
script: "/etc/xen/scripts/vif-bridge", script: None,
}], }],
filesystems: vec![], filesystems: vec![],
extra_keys: vec![ extra_keys: vec![
@ -144,6 +159,7 @@ impl Controller {
), ),
), ),
("hypha/image".to_string(), image.to_string()), ("hypha/image".to_string(), image.to_string()),
("hypha/ipv4".to_string(), ipv4.to_string()),
], ],
}; };
match self.client.create(&config) { match self.client.create(&config) {
@ -267,12 +283,18 @@ impl Controller {
.store .store
.read_string_optional(&format!("{}/hypha/loops", &dom_path))? .read_string_optional(&format!("{}/hypha/loops", &dom_path))?
.unwrap_or("".to_string()); .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); let loops = Controller::parse_loop_set(&loops);
containers.push(ContainerInfo { containers.push(ContainerInfo {
uuid, uuid,
domid, domid,
image, image,
loops, loops,
ipv4,
}); });
} }
Ok(containers) Ok(containers)
@ -308,4 +330,37 @@ impl Controller {
}) })
.collect::<Vec<ContainerLoopInfo>>() .collect::<Vec<ContainerLoopInfo>>()
} }
fn allocate_ipv4(&mut self) -> Result<Ipv4Addr> {
let network = Ipv4Network::new(Ipv4Addr::new(192, 168, 42, 0), 24)?;
let mut used: Vec<Ipv4Addr> = 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<Ipv4Addr> = 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())
}
} }

View File

@ -2,4 +2,5 @@ pub mod autoloop;
pub mod container; pub mod container;
pub mod ctl; pub mod ctl;
pub mod image; pub mod image;
pub mod network;
pub mod shared; pub mod shared;

47
hypha/src/network/mod.rs Normal file
View File

@ -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<IpCidr>,
}
impl HyphaNetwork {
pub fn new(iface: &str, cidrs: &[&str]) -> Result<HyphaNetwork> {
let device = RawSocket::new(iface, smoltcp::phy::Medium::Ethernet)?;
let mut addresses: Vec<IpCidr> = 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))?;
}
}
}

View File

@ -1,7 +1,14 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct LaunchNetwork {
pub link: String,
pub ipv4: String,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct LaunchInfo { pub struct LaunchInfo {
pub network: Option<LaunchNetwork>,
pub env: Option<Vec<String>>, pub env: Option<Vec<String>>,
pub run: Option<Vec<String>>, pub run: Option<Vec<String>>,
} }

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
TARGET="x86_64-unknown-linux-gnu" TARGET="x86_64-unknown-linux-musl"
export RUSTFLAGS="-Ctarget-feature=+crt-static" export RUSTFLAGS="-Ctarget-feature=+crt-static"
cd "$(dirname "${0}")/.." cd "$(dirname "${0}")/.."

View File

@ -52,8 +52,8 @@ pub struct DomainFilesystem<'a> {
pub struct DomainNetworkInterface<'a> { pub struct DomainNetworkInterface<'a> {
pub mac: &'a str, pub mac: &'a str,
pub mtu: u32, pub mtu: u32,
pub bridge: &'a str, pub bridge: Option<&'a str>,
pub script: &'a str, pub script: Option<&'a str>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -524,24 +524,38 @@ impl XenClient {
vif: &DomainNetworkInterface, vif: &DomainNetworkInterface,
) -> Result<()> { ) -> Result<()> {
let id = 20 + index as u64; 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()), ("frontend-id", domid.to_string()),
("online", "1".to_string()), ("online", "1".to_string()),
("state", "1".to_string()), ("state", "1".to_string()),
("mac", vif.mac.to_string()), ("mac", vif.mac.to_string()),
("mtu", vif.mtu.to_string()), ("mtu", vif.mtu.to_string()),
("type", "vif".to_string()), ("type", "vif".to_string()),
("bridge", vif.bridge.to_string()),
("handle", id.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![ let frontend_items: Vec<(&str, String)> = vec![
("backend-id", backend_domid.to_string()), ("backend-id", backend_domid.to_string()),
("state", "1".to_string()), ("state", "1".to_string()),
("mac", vif.mac.to_string()), ("mac", vif.mac.to_string()),
("trusted", "1".to_string()), ("trusted", "1".to_string()),
("mtu", vif.mtu.to_string()),
]; ];
self.device_add( self.device_add(

View File

@ -2,5 +2,5 @@
set -e set -e
cd "$(dirname "${0}")/.." cd "$(dirname "${0}")/.."
cargo fmt --all
cargo clippy --target x86_64-unknown-linux-gnu --fix --allow-dirty --allow-staged cargo clippy --target x86_64-unknown-linux-gnu --fix --allow-dirty --allow-staged
cargo fmt --all

11
scripts/hyphanet-debug.sh Executable file
View File

@ -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 "${@}"