571 lines
16 KiB
Rust
571 lines
16 KiB
Rust
#![feature(yggdrasil_os)]
|
|
|
|
use std::os::{
|
|
fd::AsRawFd,
|
|
yggdrasil::io::{poll::PollChannel, net::raw_socket::RawSocket, timer::TimerFd},
|
|
};
|
|
use std::{io, mem::size_of, process::ExitCode, time::Duration};
|
|
|
|
use bytemuck::{Pod, Zeroable};
|
|
use netutils::{netconfig::NetConfig, proto::parse_udp_protocol, Error};
|
|
use yggdrasil_abi::net::protocols::{
|
|
EtherType, EthernetFrame, InetChecksum, IpProtocol, Ipv4Frame, UdpFrame,
|
|
};
|
|
use yggdrasil_abi::net::types::NetValueImpl;
|
|
use yggdrasil_abi::net::{IpAddr, Ipv4Addr, MacAddress, SubnetAddr, SubnetV4Addr};
|
|
|
|
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
|
|
#[repr(C, packed)]
|
|
struct DhcpMessageHeader {
|
|
op: u8,
|
|
htype: u8,
|
|
hlen: u8,
|
|
hops: u8,
|
|
xid: u32,
|
|
secs: u16,
|
|
flags: u16,
|
|
ciaddr: u32,
|
|
yiaddr: u32,
|
|
siaddr: u32,
|
|
giaddr: u32,
|
|
chaddr: [u8; 16],
|
|
sname: [u8; 64],
|
|
file: [u8; 128],
|
|
cookie: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum DhcpMessageType {
|
|
Offer,
|
|
Discover,
|
|
Request,
|
|
Acknowledge,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum DhcpOption {
|
|
MessageType(DhcpMessageType),
|
|
SubnetMask(u32),
|
|
Router(Ipv4Addr),
|
|
ServerId(Ipv4Addr),
|
|
// ParameterRequestList(Vec<u8>),
|
|
RequestedAddress(Ipv4Addr),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum ParsedOption {
|
|
Option(usize, DhcpOption),
|
|
Unknown(usize),
|
|
Pad,
|
|
End,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct DhcpMessage {
|
|
header: DhcpMessageHeader,
|
|
options: Vec<DhcpOption>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct DhcpOffer {
|
|
router_address: Ipv4Addr,
|
|
server_address: Ipv4Addr,
|
|
your_address: Ipv4Addr,
|
|
subnet_mask: u32,
|
|
}
|
|
|
|
fn pad_to_align(buffer: &mut Vec<u8>) {
|
|
let offset = buffer.len() % 4;
|
|
for _ in offset..4 {
|
|
buffer.push(0);
|
|
}
|
|
}
|
|
|
|
impl From<DhcpMessageType> for u8 {
|
|
fn from(value: DhcpMessageType) -> u8 {
|
|
match value {
|
|
DhcpMessageType::Discover => 0x01,
|
|
DhcpMessageType::Offer => 0x02,
|
|
DhcpMessageType::Request => 0x03,
|
|
DhcpMessageType::Acknowledge => 0x05,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<u8> for DhcpMessageType {
|
|
type Error = ();
|
|
|
|
fn try_from(value: u8) -> Result<DhcpMessageType, Self::Error> {
|
|
match value {
|
|
0x01 => Ok(DhcpMessageType::Discover),
|
|
0x02 => Ok(DhcpMessageType::Offer),
|
|
0x03 => Ok(DhcpMessageType::Request),
|
|
0x05 => Ok(DhcpMessageType::Acknowledge),
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DhcpOption {
|
|
fn write(&self, buffer: &mut Vec<u8>) {
|
|
match self {
|
|
Self::MessageType(ty) => {
|
|
buffer.extend_from_slice(&[53, 0x01, (*ty).into()]);
|
|
}
|
|
// Self::ParameterRequestList(list) => todo!(),
|
|
Self::RequestedAddress(addr) => {
|
|
buffer.extend_from_slice(&[50, 0x04]);
|
|
buffer.extend_from_slice(&u32::from(*addr).to_be_bytes());
|
|
}
|
|
Self::ServerId(addr) => {
|
|
buffer.extend_from_slice(&[54, 0x04]);
|
|
buffer.extend_from_slice(&u32::from(*addr).to_be_bytes());
|
|
}
|
|
_ => todo!(),
|
|
}
|
|
}
|
|
|
|
fn parse(data: &[u8]) -> Option<ParsedOption> {
|
|
if data.is_empty() {
|
|
return None;
|
|
}
|
|
if data[0] == 0 {
|
|
return Some(ParsedOption::Pad);
|
|
}
|
|
if data[0] == 0xFF {
|
|
return Some(ParsedOption::End);
|
|
}
|
|
|
|
if data.len() < 2 {
|
|
return None;
|
|
}
|
|
|
|
let opcode = data[0];
|
|
let length = data[1] as usize;
|
|
|
|
if data.len() < 2 + length {
|
|
return None;
|
|
}
|
|
|
|
Some(ParsedOption::Option(
|
|
2 + length,
|
|
match opcode {
|
|
// DHCP message type
|
|
1 if length == 4 => {
|
|
let value = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
|
|
DhcpOption::SubnetMask(value)
|
|
}
|
|
3 if length == 4 => {
|
|
let value = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
|
|
DhcpOption::Router(value.into())
|
|
}
|
|
53 if length == 1 => {
|
|
let ty = DhcpMessageType::try_from(data[2]).ok()?;
|
|
DhcpOption::MessageType(ty)
|
|
}
|
|
54 if length == 4 => {
|
|
let value = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
|
|
DhcpOption::ServerId(value.into())
|
|
}
|
|
_ => return Some(ParsedOption::Unknown(2 + length)),
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
impl DhcpMessage {
|
|
fn to_vec(&self) -> Vec<u8> {
|
|
let mut result = vec![];
|
|
|
|
result.extend_from_slice(bytemuck::bytes_of(&self.header));
|
|
|
|
for option in self.options.iter() {
|
|
option.write(&mut result);
|
|
pad_to_align(&mut result);
|
|
}
|
|
|
|
// End option
|
|
result.push(0xFF);
|
|
pad_to_align(&mut result);
|
|
|
|
result
|
|
}
|
|
|
|
fn parse(data: &[u8]) -> Option<Self> {
|
|
if data.len() < size_of::<DhcpMessageHeader>() {
|
|
return None;
|
|
}
|
|
let header: &DhcpMessageHeader =
|
|
bytemuck::from_bytes(&data[..size_of::<DhcpMessageHeader>()]);
|
|
|
|
let mut options = vec![];
|
|
|
|
let mut offset = size_of::<DhcpMessageHeader>();
|
|
loop {
|
|
let data = &data[offset..];
|
|
let option = DhcpOption::parse(data)?;
|
|
|
|
match option {
|
|
ParsedOption::Option(len, value) => {
|
|
options.push(value);
|
|
offset += len;
|
|
}
|
|
ParsedOption::Unknown(len) => {
|
|
offset += len;
|
|
}
|
|
ParsedOption::Pad => {
|
|
offset += 1;
|
|
}
|
|
ParsedOption::End => break,
|
|
}
|
|
}
|
|
|
|
Some(Self {
|
|
header: *header,
|
|
options,
|
|
})
|
|
}
|
|
|
|
fn message_type(&self) -> Option<DhcpMessageType> {
|
|
self.options.iter().find_map(|option| match option {
|
|
DhcpOption::MessageType(ty) => Some(*ty),
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
fn as_dhcp_acknowledge(&self, requested_address: Ipv4Addr) -> Option<()> {
|
|
let ty = self.message_type()?;
|
|
|
|
if ty != DhcpMessageType::Acknowledge {
|
|
return None;
|
|
}
|
|
let yiaddr = u32::from_be(self.header.yiaddr);
|
|
if yiaddr == 0 {
|
|
return None;
|
|
}
|
|
if Ipv4Addr::from(yiaddr) != requested_address {
|
|
return None;
|
|
}
|
|
|
|
Some(())
|
|
}
|
|
|
|
fn as_dhcp_offer(&self) -> Option<DhcpOffer> {
|
|
let ty = self.message_type()?;
|
|
if ty != DhcpMessageType::Offer {
|
|
return None;
|
|
}
|
|
let yiaddr = u32::from_be(self.header.yiaddr);
|
|
if yiaddr == 0 {
|
|
return None;
|
|
}
|
|
let mut router = None;
|
|
let mut server = None;
|
|
let mut subnet_mask = None;
|
|
|
|
for option in self.options.iter() {
|
|
match option {
|
|
DhcpOption::Router(address) if router.is_none() => {
|
|
router.replace(*address);
|
|
}
|
|
DhcpOption::ServerId(address) if server.is_none() => {
|
|
server.replace(*address);
|
|
}
|
|
DhcpOption::SubnetMask(mask) if subnet_mask.is_none() => {
|
|
subnet_mask.replace(*mask);
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
let router_address = router?;
|
|
let server_address = server?;
|
|
let subnet_mask = subnet_mask?;
|
|
|
|
Some(DhcpOffer {
|
|
server_address,
|
|
router_address,
|
|
your_address: Ipv4Addr::from(yiaddr),
|
|
subnet_mask,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn send_split_packet(
|
|
socket: &RawSocket,
|
|
l2_frame: &[u8],
|
|
l3_frame: &[u8],
|
|
l4_frame: &[u8],
|
|
l4_data: &[u8],
|
|
) -> Result<(), io::Error> {
|
|
let mut buffer =
|
|
Vec::with_capacity(l2_frame.len() + l3_frame.len() + l4_frame.len() + l4_data.len());
|
|
buffer.extend_from_slice(l2_frame);
|
|
buffer.extend_from_slice(l3_frame);
|
|
buffer.extend_from_slice(l4_frame);
|
|
buffer.extend_from_slice(l4_data);
|
|
|
|
socket.send(&buffer)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn send_udp_broadcast(
|
|
socket: &RawSocket,
|
|
source_mac: MacAddress,
|
|
source_port: u16,
|
|
destination_port: u16,
|
|
data: &[u8],
|
|
) -> Result<(), io::Error> {
|
|
let ip_size = size_of::<Ipv4Frame>() + size_of::<UdpFrame>() + data.len();
|
|
let udp_size = size_of::<UdpFrame>() + data.len();
|
|
|
|
let l2_frame = EthernetFrame {
|
|
source_mac,
|
|
destination_mac: MacAddress::BROADCAST,
|
|
ethertype: EtherType::IPV4.to_network_order(),
|
|
};
|
|
let mut l3_frame = Ipv4Frame {
|
|
source_address: u32::from(Ipv4Addr::UNSPECIFIED).to_network_order(),
|
|
destination_address: u32::from(Ipv4Addr::BROADCAST).to_network_order(),
|
|
protocol: IpProtocol::UDP,
|
|
version_length: 0x45,
|
|
total_length: u16::try_from(ip_size).unwrap().to_network_order(),
|
|
dscp_flags: 0,
|
|
flags_frag: 0x4000u16.to_network_order(),
|
|
id: 0u16.to_network_order(),
|
|
header_checksum: 0u16.to_network_order(),
|
|
ttl: 64,
|
|
};
|
|
let l4_frame = UdpFrame {
|
|
source_port: source_port.to_network_order(),
|
|
destination_port: destination_port.to_network_order(),
|
|
length: u16::try_from(udp_size).unwrap().to_network_order(),
|
|
checksum: 0u16.to_network_order(),
|
|
};
|
|
|
|
let mut ip_checksum = InetChecksum::new();
|
|
ip_checksum.add_bytes(bytemuck::bytes_of(&l3_frame), true);
|
|
let ip_checksum = ip_checksum.finish();
|
|
|
|
l3_frame.header_checksum = ip_checksum.to_network_order();
|
|
|
|
send_split_packet(
|
|
socket,
|
|
bytemuck::bytes_of(&l2_frame),
|
|
bytemuck::bytes_of(&l3_frame),
|
|
bytemuck::bytes_of(&l4_frame),
|
|
data,
|
|
)
|
|
}
|
|
|
|
fn wait_for_dhcp_offer(
|
|
poll: &mut PollChannel,
|
|
timer: &mut TimerFd,
|
|
socket: &RawSocket,
|
|
) -> Result<DhcpOffer, Error> {
|
|
let mut packet = [0; 4096];
|
|
|
|
let socket_fd = socket.as_raw_fd();
|
|
let timer_fd = timer.as_raw_fd();
|
|
|
|
timer.start(Duration::from_secs(1))?;
|
|
|
|
loop {
|
|
let (fd, result) = poll.wait(None, true)?.unwrap();
|
|
result?;
|
|
|
|
match fd {
|
|
fd if fd == socket_fd => {
|
|
let len = socket.recv(&mut packet)?;
|
|
|
|
let Some((source, destination, data)) = parse_udp_protocol(&packet[..len]) else {
|
|
continue;
|
|
};
|
|
if source.port() != 67 || destination.port() != 68 {
|
|
continue;
|
|
}
|
|
let Some(msg) = DhcpMessage::parse(data) else {
|
|
continue;
|
|
};
|
|
let Some(offer) = msg.as_dhcp_offer() else {
|
|
continue;
|
|
};
|
|
|
|
return Ok(offer);
|
|
}
|
|
fd if fd == timer_fd => {
|
|
return Err(Error::TimedOut);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn wait_for_dhcp_acknowledge(
|
|
poll: &mut PollChannel,
|
|
timer: &mut TimerFd,
|
|
socket: &RawSocket,
|
|
requested_address: Ipv4Addr,
|
|
) -> Result<(), Error> {
|
|
let mut packet = [0; 4096];
|
|
|
|
let socket_fd = socket.as_raw_fd();
|
|
let timer_fd = timer.as_raw_fd();
|
|
|
|
timer.start(Duration::from_secs(1))?;
|
|
|
|
loop {
|
|
let (fd, result) = poll.wait(None, true)?.unwrap();
|
|
result?;
|
|
|
|
match fd {
|
|
fd if fd == socket_fd => {
|
|
let len = socket.recv(&mut packet)?;
|
|
|
|
let Some((source, destination, data)) = parse_udp_protocol(&packet[..len]) else {
|
|
continue;
|
|
};
|
|
if source.port() != 67 || destination.port() != 68 {
|
|
continue;
|
|
}
|
|
let Some(msg) = DhcpMessage::parse(data) else {
|
|
continue;
|
|
};
|
|
|
|
if msg.as_dhcp_acknowledge(requested_address).is_some() {
|
|
return Ok(());
|
|
}
|
|
}
|
|
fd if fd == timer_fd => {
|
|
return Err(Error::TimedOut);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn attempt_request(
|
|
poll: &mut PollChannel,
|
|
timer: &mut TimerFd,
|
|
socket: &RawSocket,
|
|
iface_mac_bytes: [u8; 6],
|
|
) -> Result<DhcpOffer, Error> {
|
|
let iface_mac = MacAddress::from(iface_mac_bytes);
|
|
|
|
let transaction_id = rand::random::<u32>();
|
|
let mut message = DhcpMessage {
|
|
header: DhcpMessageHeader {
|
|
op: 1,
|
|
htype: 1,
|
|
hlen: 6,
|
|
xid: transaction_id.to_be(),
|
|
chaddr: [0; 16],
|
|
cookie: 0x63825363u32.to_be(),
|
|
..DhcpMessageHeader::zeroed()
|
|
},
|
|
options: vec![DhcpOption::MessageType(DhcpMessageType::Discover)],
|
|
};
|
|
message.header.chaddr[..6].copy_from_slice(&iface_mac_bytes);
|
|
let data = message.to_vec();
|
|
|
|
send_udp_broadcast(socket, iface_mac, 68, 67, &data)?;
|
|
|
|
let offer = wait_for_dhcp_offer(poll, timer, socket)?;
|
|
|
|
println!(
|
|
"Got DHCP offer: address={} router={} server={} mask={}",
|
|
offer.your_address,
|
|
offer.router_address,
|
|
offer.server_address,
|
|
Ipv4Addr::from(offer.subnet_mask)
|
|
);
|
|
|
|
// Send DHCP request
|
|
let mut message = DhcpMessage {
|
|
header: DhcpMessageHeader {
|
|
op: 1,
|
|
htype: 1,
|
|
hlen: 6,
|
|
xid: transaction_id.to_be(),
|
|
chaddr: [0; 16],
|
|
siaddr: u32::from(offer.server_address).to_be(),
|
|
cookie: 0x63825363u32.to_be(),
|
|
..DhcpMessageHeader::zeroed()
|
|
},
|
|
options: vec![
|
|
DhcpOption::MessageType(DhcpMessageType::Request),
|
|
DhcpOption::RequestedAddress(offer.your_address),
|
|
DhcpOption::ServerId(offer.server_address),
|
|
],
|
|
};
|
|
message.header.chaddr[..6].copy_from_slice(&iface_mac_bytes);
|
|
let data = message.to_vec();
|
|
|
|
send_udp_broadcast(socket, iface_mac, 68, 67, &data)?;
|
|
|
|
wait_for_dhcp_acknowledge(poll, timer, socket, offer.your_address)?;
|
|
|
|
println!("Got DHCP acknowledge for {}", offer.your_address);
|
|
|
|
Ok(offer)
|
|
}
|
|
|
|
fn request_address(interface: &str) -> Result<DhcpOffer, Error> {
|
|
let mut poll = PollChannel::new()?;
|
|
let mut timer = TimerFd::new(false, false)?;
|
|
let socket = RawSocket::bind(interface)?;
|
|
let iface_mac_bytes = socket.hardware_address()?;
|
|
|
|
poll.add(socket.as_raw_fd())?;
|
|
poll.add(timer.as_raw_fd())?;
|
|
|
|
for _ in 0..5 {
|
|
match attempt_request(&mut poll, &mut timer, &socket, iface_mac_bytes) {
|
|
Ok(offer) => {
|
|
return Ok(offer);
|
|
}
|
|
Err(error) => {
|
|
eprintln!("Error: {}, trying again", error);
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(Error::TimedOut)
|
|
}
|
|
|
|
fn configure_address(interface: &str, offer: DhcpOffer) -> Result<(), Error> {
|
|
let mut netconf = NetConfig::open()?;
|
|
netconf.set_interface_address(interface, IpAddr::V4(offer.your_address))?;
|
|
netconf.add_route(
|
|
interface,
|
|
SubnetAddr::V4(SubnetV4Addr::UNSPECIFIED),
|
|
Some(IpAddr::V4(offer.router_address)),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn main() -> ExitCode {
|
|
let args: Vec<_> = std::env::args().collect();
|
|
|
|
if args.len() != 2 {
|
|
eprintln!("Usage: {} INTERFACE", args[0]);
|
|
return ExitCode::FAILURE;
|
|
}
|
|
|
|
let offer = match request_address(&args[1]) {
|
|
Ok(offer) => offer,
|
|
Err(error) => {
|
|
eprintln!("Could not obtain an address from DHCP: {}", error);
|
|
return ExitCode::FAILURE;
|
|
}
|
|
};
|
|
match configure_address(&args[1], offer) {
|
|
Ok(_) => ExitCode::SUCCESS,
|
|
Err(error) => {
|
|
eprintln!("Could not assign offered address to {}: {}", args[1], error);
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|