Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,4 @@ jobs:
if: matrix.arch != 'riscv64'
- run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package mioudp --features hermit/udp,hermit/dhcpv4,hermit/rtl8139 qemu ${{ matrix.qemu_flags }} --devices rtl8139
if: matrix.arch != 'riscv64'
- run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package loopback qemu ${{ matrix.qemu_flags }}
env:
HERMIT_IP: 127.0.0.1
- run: cargo xtask ci rs --arch ${{ matrix.arch }} --profile ${{ matrix.profile }} ${{ matrix.rs_flags }} --package loopback qemu ${{ matrix.qemu_flags }} -- -- ip=127.0.0.1/8
179 changes: 179 additions & 0 deletions src/env/ip_configuration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use core::net::Ipv4Addr;
use core::{fmt, str};

use smoltcp::wire::{IpCidr, Ipv4Cidr};

#[derive(Clone, Copy, Debug, Default)]
pub enum IpAddressConfiguration {
#[cfg_attr(not(feature = "dhcpv4"), default)]
None,
#[cfg(feature = "dhcpv4")]
#[default]
Dhcp,
Static {
ip_and_netmask: IpCidr,
gateway: Option<Ipv4Addr>,
},
}

#[derive(Debug)]
pub enum IpConfigurationParseError {
#[cfg(feature = "dns")]
InvalidDns,
InvalidGateway,
InvalidIp,
InvalidPrefixLen,
MissingIpOrMethod,
MissingPrefixLen,
#[cfg(not(feature = "dhcpv4"))]
DhcpNotEnabled,
}

impl fmt::Display for IpConfigurationParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "dns")]
Self::InvalidDns => f.write_str("invalid DNS IP address"),
Self::InvalidGateway => f.write_str("invalid gateway IP address"),
Self::InvalidIp => f.write_str("invalid IP address"),
Self::InvalidPrefixLen => f.write_str("invalid prefix length"),
Self::MissingIpOrMethod => f.write_str("IP configuration is missing a method"),
Self::MissingPrefixLen => {
f.write_str("static IP configuration is missing a prefix length")
}
#[cfg(not(feature = "dhcpv4"))]
Self::DhcpNotEnabled => f.write_str("DHCP cannot be selected, disable via feature flags"),
}
}
}

impl IpAddressConfiguration {
fn parse_from_parts(
parts: &mut str::Split<'_, char>,
) -> Result<Self, IpConfigurationParseError> {
let ip_or_type = parts
.next()
.ok_or(IpConfigurationParseError::MissingIpOrMethod)?;

match ip_or_type {
"none" | "off" => Ok(Self::None),
"dhcp" => {
#[cfg(feature = "dhcpv4")]
{
Ok(Self::Dhcp)
}
#[cfg(not(feature = "dhcpv4"))]
{
Err(IpConfigurationParseError::DhcpNotEnabled)
}
}
// Anything else must be an IP with a prefix length
ip_and_prefix => {
let mut ip_and_prefix_parts = ip_and_prefix.split('/');
// We only support IPv4 for now
let ip = ip_and_prefix_parts
.next()
// split always has at least one item
.unwrap()
.parse()
.map_err(|_| IpConfigurationParseError::InvalidIp)?;
let prefix_len = ip_and_prefix_parts
.next()
.ok_or(IpConfigurationParseError::MissingPrefixLen)?
.parse()
.map_err(|_| IpConfigurationParseError::InvalidPrefixLen)?;

// The gateway is optional since you can technically not specify one
let gateway = parts.next().map_or(Ok(None), |ip_str| {
ip_str
.parse()
.map_or(Err(IpConfigurationParseError::InvalidGateway), |ip| {
Ok(Some(ip))
})
})?;

Ok(Self::Static {
ip_and_netmask: IpCidr::from(Ipv4Cidr::new(ip, prefix_len)),
gateway,
})
}
}
}
}

/// IP configuration as passed via ip= to the kernel commandline.
///
/// Fields are specified separated by colons (:), for example:
/// - ip=none or ip=off to skip configuring the default interface
/// - ip=dhcp to use DHCP for configuring the default interface
/// - ip=10.0.5.3/24:10.0.5.1 to configure a static IP and gateway on the default interface
///
/// This is heavily inspired by the Linux kernel's parameter of the same name:
/// <https://docs.kernel.org/admin-guide/nfs/nfsroot.html#kernel-command-line>
#[derive(Clone, Copy, Debug, Default)]
pub struct IpConfiguration {
pub ip_and_gateway: IpAddressConfiguration,
// hostname is omitted
// device is omitted
// autoconf is omitted
#[cfg(feature = "dns")]
pub dns0: Option<Ipv4Addr>,
#[cfg(feature = "dns")]
pub dns1: Option<Ipv4Addr>,
// ntp0 is omitted
}

impl TryFrom<&str> for IpConfiguration {
type Error = IpConfigurationParseError;

fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut ret = Self::default();
let mut parts = value.split(':');

// The IP configuration is mandatory
ret.ip_and_gateway = IpAddressConfiguration::parse_from_parts(&mut parts)?;

// Everything else is optional
let Some(_hostname) = parts.next() else {
return Ok(ret);
};

let Some(_device) = parts.next() else {
return Ok(ret);
};

let Some(_autoconf) = parts.next() else {
return Ok(ret);
};

let Some(dns0_ip_str) = parts.next() else {
return Ok(ret);
};
#[cfg(feature = "dns")]
if !dns0_ip_str.is_empty() {
ret.dns0 = Some(dns0_ip_str.parse().map_err(|_| Self::Error::InvalidDns)?);
}
#[cfg(not(feature = "dns"))]
if !dns0_ip_str.is_empty() {
warn!("DNS 0 IP specified without enabling the dns feature, ignoring");
}

let Some(dns1_ip_str) = parts.next() else {
return Ok(ret);
};
#[cfg(feature = "dns")]
if !dns1_ip_str.is_empty() {
ret.dns1 = Some(dns1_ip_str.parse().map_err(|_| Self::Error::InvalidDns)?);
}
#[cfg(not(feature = "dns"))]
if !dns1_ip_str.is_empty() {
warn!("DNS 1 IP specified without enabling the dns feature, ignoring");
}

let Some(_ntp0_ip) = parts.next() else {
return Ok(ret);
};

Ok(ret)
}
}
52 changes: 39 additions & 13 deletions src/env/mod.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
//! Inspection and manipulation of the kernel's environment.

#[cfg(feature = "net")]
mod ip_configuration;
mod start_info;

use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec::Vec;
use core::str;

use ahash::RandomState;
use hashbrown::HashMap;
use hashbrown::hash_map::Iter;
use hermit_sync::OnceCell;
use shlex::Shlex;

#[cfg(feature = "net")]
pub use self::ip_configuration::*;
pub use self::start_info::*;

static CLI: OnceCell<Cli> = OnceCell::new();
Expand All @@ -27,6 +30,8 @@ struct Cli {
image_path: Option<String>,
#[cfg(not(target_arch = "riscv64"))]
freq: Option<u16>,
#[cfg(feature = "net")]
default_interface_config: IpConfiguration,
env_vars: HashMap<String, String, RandomState>,
args: Vec<String>,
#[allow(dead_code)]
Expand All @@ -52,6 +57,9 @@ impl Default for Cli {
})
};

#[cfg(feature = "net")]
let mut default_interface_config = None;

let mut args = Vec::new();
let mut mmio = Vec::new();
while let Some(word) = words.next() {
Expand All @@ -61,24 +69,34 @@ impl Default for Cli {
continue;
}

#[cfg_attr(not(feature = "net"), expect(unused_variables))]
if let Some(ip_config_str) = word.as_str().strip_prefix("ip=") {
#[cfg(feature = "net")]
match IpConfiguration::try_from(ip_config_str) {
Ok(config) => {
// This is the IP configuration for the default interface
// Once we support multiple interfaces, we need to support parsing multiple configurations
if default_interface_config.is_some() {
warn!("Duplicate ip= parameter passed, this is currently unsupported!");
}

default_interface_config = Some(config);
}
Err(e) => panic!("Could not parse configuration for default interface: {e}"),
}

#[cfg(not(feature = "net"))]
warn!("ip= parameter passed with networking support disabled, ignoring");

continue;
}

match word.as_str() {
#[cfg(not(target_arch = "riscv64"))]
"-freq" => {
let s = expect_arg(words.next(), word.as_str());
freq = Some(s.parse().unwrap());
}
"-ip" => {
let ip = expect_arg(words.next(), word.as_str());
env_vars.insert(String::from("HERMIT_IP"), ip);
}
"-mask" => {
let mask = expect_arg(words.next(), word.as_str());
env_vars.insert(String::from("HERMIT_MASK"), mask);
}
"-gateway" => {
let gateway = expect_arg(words.next(), word.as_str());
env_vars.insert(String::from("HERMIT_GATEWAY"), gateway);
}
"-mount" => {
let gateway = expect_arg(words.next(), word.as_str());
env_vars.insert(String::from("UHYVE_MOUNT"), gateway);
Expand Down Expand Up @@ -107,6 +125,8 @@ impl Default for Cli {
image_path,
#[cfg(not(target_arch = "riscv64"))]
freq,
#[cfg(feature = "net")]
default_interface_config: default_interface_config.unwrap_or_default(),
env_vars,
args,
#[allow(dead_code)]
Expand All @@ -126,6 +146,12 @@ pub fn var(key: &str) -> Option<&String> {
CLI.get().unwrap().env_vars.get(key)
}

/// Returns the default interface IP configuration specified via ip=.
#[cfg(feature = "net")]
pub fn default_interface_configuration() -> IpConfiguration {
CLI.get().unwrap().default_interface_config
}

pub fn vars() -> Iter<'static, String, String> {
CLI.get().unwrap().env_vars.iter()
}
Expand Down
Loading
Loading