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"
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"]

View File

@ -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"

View File

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

View File

@ -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)?;

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 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![],

View File

@ -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<ContainerLoopInfo>,
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::<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 ctl;
pub mod image;
pub mod network;
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};
#[derive(Serialize, Deserialize, Debug)]
pub struct LaunchNetwork {
pub link: String,
pub ipv4: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LaunchInfo {
pub network: Option<LaunchNetwork>,
pub env: Option<Vec<String>>,
pub run: Option<Vec<String>>,
}

View File

@ -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}")/.."

View File

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

View File

@ -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

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