Files
dhcpv6stateless/src/main.rs

174 lines
5.3 KiB
Rust

use anyhow::{
Context,
Result,
};
use clap::Parser;
use dhcproto::{
Decodable,
Decoder,
Encodable,
Encoder,
v6::{
DhcpOption,
Message,
MessageType,
OptionCode,
ORO,
},
};
use ipnetwork;
use log::{
debug,
log_enabled,
info,
Level,
};
use pnet;
use serde::Serialize;
use std::net::SocketAddrV6;
use tokio::net::UdpSocket;
const ALL_DHCP_RELAY_AGENTS_AND_SERVERS: &str = "ff02::1:2";
const DHCP_CLIENT_PORT: u16 = 546;
const DHCP_RELAY_AGENT_AND_SERVER_PORT: u16 = 547;
/// Requests some simple parameters using DHCPv6 from specified network
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
/// Network interface name used for DHCPv6
interface: String,
/// Format output as json
#[arg(long)]
json: bool,
}
#[derive(Serialize)]
struct MessageMap {
#[serde(skip_serializing_if = "Option::is_none")]
server_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
domain_name_servers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
domain_search_list: Option<Vec<String>>,
}
impl MessageMap {
fn new() -> Self {
Self {
server_id: None,
domain_name_servers: None,
domain_search_list: None,
}
}
}
fn message_to_map(msg: &Message) -> MessageMap {
let mut map = MessageMap::new();
if let Some(DhcpOption::ServerId(server_id)) = &msg.opts().get(OptionCode::ServerId) {
map.server_id = Some(hex::encode(&server_id));
}
if let Some(DhcpOption::DomainNameServers(domain_name_servers)) = &msg.opts().get(OptionCode::DomainNameServers) {
map.domain_name_servers = Some(domain_name_servers.iter().map(|a| a.to_string()).collect());
}
if let Some(DhcpOption::DomainSearchList(domain_search_list)) = &msg.opts().get(OptionCode::DomainSearchList) {
map.domain_search_list = Some(domain_search_list.iter().map(|n| n.to_string()).collect());
}
return map;
}
#[tokio::main]
async fn main() -> Result<()>{
env_logger::init();
let cli = Cli::parse();
let all_interfaces = pnet::datalink::interfaces();
let selected_interface = all_interfaces
.iter()
.find(|i| i.name == cli.interface)
.with_context(|| format!("No interface found with name: {}", cli.interface))?;
let ipv6_addresses = selected_interface.ips
.iter()
.filter_map(|a| match a {
ipnetwork::IpNetwork::V6(a) => Some(a),
_ => None,
});
let mut ipv6_link_local_addresses = ipv6_addresses
.filter(|a| a.is_subnet_of("fe80::/10".parse().unwrap()));
// Just take the first address found on the interface
let selected_address = ipv6_link_local_addresses.next()
.context("No IPv6 link local address assigned to this interface")?;
let socket_addr = SocketAddrV6::new(selected_address.ip(), DHCP_CLIENT_PORT, 0, selected_interface.index.clone());
let sock = UdpSocket::bind(socket_addr).await
.context("Unable to bind UDP socket")?;
info!("Listening on {}", sock.local_addr()
.context("Failed to fetch address socket bound to")?);
let remote_addr = SocketAddrV6::new(ALL_DHCP_RELAY_AGENTS_AND_SERVERS.parse().unwrap(), DHCP_RELAY_AGENT_AND_SERVER_PORT, 0, selected_interface.index.clone());
let mut msg = Message::new(MessageType::InformationRequest);
let msg_opts = msg.opts_mut();
//msg_opts.insert(DhcpOption::ClientId(Vec::from(duid)));
msg_opts.insert(DhcpOption::ElapsedTime(0));
msg_opts.insert(DhcpOption::ORO(ORO { opts: vec![
OptionCode::InfMaxRt,
OptionCode::InformationRefreshTime,
OptionCode::DomainNameServers,
OptionCode::DomainSearchList,
],})); // request options
let mut msg_buf = Vec::new();
let mut msg_encoder = Encoder::new(&mut msg_buf);
msg.encode(&mut msg_encoder)
.context("Unable to serialize DHCP options for message to send")?; // Serializes msg to msg_buf
sock.send_to(&msg_buf, remote_addr).await
.context("Failed to send DHCPv6 INFORMATION-REQUEST message")?;
let response_msg = loop {
let mut recv_buf = [0; 1024];
let (recv_len, recv_addr) = sock.recv_from(&mut recv_buf).await
.context("Failed to receive DHCPv6 response")?;
if log_enabled!(Level::Debug) {
debug!("{:?} bytes received from {:?}", recv_len, recv_addr);
debug!("Received packet: {}", hex::encode(&recv_buf[..recv_len]));
}
let recv_msg = Message::decode(&mut Decoder::new(&recv_buf))
.context("Unable to parse DHCPv6 response message")?;
if log_enabled!(Level::Debug) {
debug!("Packet contains transaction id: 0x{}", hex::encode(&msg.xid()));
}
if recv_msg.xid() == msg.xid() {
info!("Found reponse with matching transaction id");
break recv_msg;
}
};
let message_map = message_to_map(&response_msg);
if cli.json {
let message_map_json = serde_json::to_string(&message_map)
.context("Unable to format message to json")?;
println!("{}", message_map_json);
}
else {
let message_map_yaml = serde_yaml::to_string(&message_map)
.context("Unable to format message to yaml")?;
println!("{}", message_map_yaml);
}
Ok(())
}