diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76efa3446f..6ad19d88e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/src/env/ip_configuration.rs b/src/env/ip_configuration.rs new file mode 100644 index 0000000000..ce57434d47 --- /dev/null +++ b/src/env/ip_configuration.rs @@ -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, + }, +} + +#[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 { + 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: +/// +#[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, + #[cfg(feature = "dns")] + pub dns1: Option, + // ntp0 is omitted +} + +impl TryFrom<&str> for IpConfiguration { + type Error = IpConfigurationParseError; + + fn try_from(value: &str) -> Result { + 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) + } +} diff --git a/src/env/mod.rs b/src/env/mod.rs index 7f09c778cb..2e52370438 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -1,11 +1,12 @@ //! 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; @@ -13,6 +14,8 @@ 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 = OnceCell::new(); @@ -27,6 +30,8 @@ struct Cli { image_path: Option, #[cfg(not(target_arch = "riscv64"))] freq: Option, + #[cfg(feature = "net")] + default_interface_config: IpConfiguration, env_vars: HashMap, args: Vec, #[allow(dead_code)] @@ -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() { @@ -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); @@ -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)] @@ -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() } diff --git a/src/executor/device.rs b/src/executor/device.rs index 42d1886e48..098979d443 100644 --- a/src/executor/device.rs +++ b/src/executor/device.rs @@ -1,5 +1,6 @@ use alloc::boxed::Box; -use core::str::FromStr; +#[cfg(feature = "dns")] +use alloc::vec::Vec; use smoltcp::iface::{Config, Interface, SocketSet}; #[cfg(feature = "net-trace")] @@ -11,7 +12,7 @@ use smoltcp::phy::{PcapMode, PcapWriter}; use smoltcp::socket::dhcpv4; #[cfg(feature = "dns")] use smoltcp::socket::dns; -use smoltcp::wire::{EthernetAddress, HardwareAddress, IpCidr, Ipv4Address, Ipv4Cidr}; +use smoltcp::wire::{EthernetAddress, HardwareAddress}; use super::network::{NetworkInterface, NetworkState}; use crate::arch::kernel::systemtime; @@ -25,6 +26,7 @@ use crate::drivers::Driver; ))] use crate::drivers::net::NetworkDevice; use crate::drivers::net::NetworkDriver; +use crate::env::{IpAddressConfiguration, default_interface_configuration}; cfg_select! { any( @@ -101,47 +103,44 @@ impl<'a> NetworkInterface<'a> { #[cfg_attr(all(not(feature = "dhcpv4"), not(feature = "dns")), expect(unused_mut))] let mut sockets = SocketSet::new(vec![]); + #[cfg(feature = "dhcpv4")] + let mut dhcp_handle = None; #[cfg(feature = "dns")] let mut dns_handle = None; - #[cfg(feature = "dhcpv4")] - let dhcp_handle = { - if let Some(hermit_ip) = hermit_var!("HERMIT_IP") { - warn!("HERMIT_IP was set to {hermit_ip}, but Hermit was built with DHCPv4."); - warn!( - "HERMIT_IP will be overwritten if a DHCP configuration is acquired. If the provided configuration was not meant to be a fallback, disable the DHCP feature." - ); - } - sockets.add(dhcpv4::Socket::new()) - }; - - if !cfg!(feature = "dhcpv4") || hermit_var!("HERMIT_IP").is_some() { - let myip = Ipv4Address::from_str(hermit_var_or!("HERMIT_IP", "10.0.5.3")).unwrap(); - let mygw = Ipv4Address::from_str(hermit_var_or!("HERMIT_GATEWAY", "10.0.5.1")).unwrap(); - let mymask = - Ipv4Address::from_str(hermit_var_or!("HERMIT_MASK", "255.255.255.0")).unwrap(); + let if_config = default_interface_configuration(); - let ip_addr = IpCidr::from(Ipv4Cidr::from_netmask(myip, mymask).unwrap()); - info!("IP address: {ip_addr}"); - info!("Gateway: {mygw}"); - - iface.update_ip_addrs(|ip_addrs| { - ip_addrs.push(ip_addr).unwrap(); - }); - iface.routes_mut().add_default_ipv4_route(mygw).unwrap(); + match if_config.ip_and_gateway { + IpAddressConfiguration::None => {} + #[cfg(feature = "dhcpv4")] + IpAddressConfiguration::Dhcp => { + dhcp_handle = Some(sockets.add(dhcpv4::Socket::new())); + } + IpAddressConfiguration::Static { + ip_and_netmask, + gateway, + } => { + info!("IP address: {ip_and_netmask}"); + + iface.update_ip_addrs(|ip_addrs| { + ip_addrs.push(ip_and_netmask).unwrap(); + }); + + if let Some(gateway) = gateway { + info!("Gateway: {gateway}"); + iface.routes_mut().add_default_ipv4_route(gateway).unwrap(); + } - #[cfg(feature = "dns")] - { - // Quad9 DNS server - let mydns1 = - Ipv4Address::from_str(hermit_var_or!("HERMIT_DNS1", "9.9.9.9")).unwrap(); - // Cloudflare DNS server - let mydns2 = - Ipv4Address::from_str(hermit_var_or!("HERMIT_DNS2", "1.1.1.1")).unwrap(); - let servers = &[mydns1.into(), mydns2.into()]; - let dns_socket = dns::Socket::new(servers, vec![]); - dns_handle = Some(sockets.add(dns_socket)); - }; + #[cfg(feature = "dns")] + { + let servers = &[if_config.dns0, if_config.dns1] + .into_iter() + .flat_map(|i| i.map(Into::into)) + .collect::>(); + let dns_socket = dns::Socket::new(servers, vec![]); + dns_handle = Some(sockets.add(dns_socket)); + }; + } } NetworkState::Initialized(Box::new(Self { diff --git a/src/executor/network.rs b/src/executor/network.rs index a233f427a2..885f63e5b8 100644 --- a/src/executor/network.rs +++ b/src/executor/network.rs @@ -98,7 +98,7 @@ pub(crate) struct NetworkInterface<'a> { pub(super) sockets: SocketSet<'a>, pub(super) device: MaybeTracerDevice, #[cfg(feature = "dhcpv4")] - pub(super) dhcp_handle: SocketHandle, + pub(super) dhcp_handle: Option, #[cfg(feature = "dns")] pub(super) dns_handle: Option, } @@ -149,7 +149,11 @@ async fn dhcpv4_run() { }; let nic = guard.as_nic_mut().unwrap(); - let dhcp_handle = nic.dhcp_handle; + let Some(dhcp_handle) = nic.dhcp_handle else { + // DHCP enabled at compile time but not configure at runtime + return Poll::Ready(()); + }; + let socket = nic.sockets.get_mut::>(dhcp_handle); socket.register_waker(cx.waker()); diff --git a/xtask/src/ci/qemu.rs b/xtask/src/ci/qemu.rs index b656d79883..8c5b460430 100644 --- a/xtask/src/ci/qemu.rs +++ b/xtask/src/ci/qemu.rs @@ -17,6 +17,10 @@ use crate::arch::Arch; use crate::ci; const DEFAULT_GUEST_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 5, 3)); +const DEFAULT_GUEST_PREFIX_LEN: u8 = 24; +const DEFAULT_GUEST_GATEWAY: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 5, 1)); +const DEFAULT_GUEST_DNS0: IpAddr = IpAddr::V4(Ipv4Addr::new(9, 9, 9, 9)); +const DEFAULT_GUEST_DNS1: IpAddr = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); /// Run image on QEMU. #[derive(Args)] @@ -548,7 +552,9 @@ impl Qemu { args.extend(["-freq".to_owned(), frequency.to_string()]); } if self.tap { - args.extend(["-ip".to_owned(), DEFAULT_GUEST_IP.to_string()]); + args.push(format!( + "ip={DEFAULT_GUEST_IP}/{DEFAULT_GUEST_PREFIX_LEN}:{DEFAULT_GUEST_GATEWAY}::::{DEFAULT_GUEST_DNS0}:{DEFAULT_GUEST_DNS1}" + )); } args } @@ -574,11 +580,7 @@ impl Qemu { fn guest_ip(&self) -> IpAddr { if self.tap { - if let Ok(ip) = env::var("HERMIT_IP") { - ip.parse().unwrap() - } else { - DEFAULT_GUEST_IP - } + DEFAULT_GUEST_IP } else { Ipv4Addr::LOCALHOST.into() }