ftth-dhcp 0.1.2

DHCP implementations for `ftth` suite of Rust FTTH CPE software.
Documentation

use std::io::ErrorKind;
use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6};
use std::time::Duration;

use dhcproto::v6::{DhcpOption, DhcpOptions, IAPrefix, OptionCode, Status, StatusCode, UnknownOption, IAPD};
use dhcproto::{Decodable, Decoder, Encodable, Encoder};
use socket2::{Socket, Domain, Type};

pub use dhcproto::v6::MessageType;

#[derive(Debug)]
pub struct Dhcp6Client {
    socket: std::net::UdpSocket,
    local_if_mac: [u8; 6],
    local_ll_addr: Ipv6Addr,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PdPrefix {
    pub prefix: Ipv6Addr,
    pub prefix_len: u8,
    pub preferred_lifetime: u32,
    pub valid_lifetime: u32,
    pub t1: u32,
    pub t2: u32,
}

#[derive(Debug, Clone)]
pub struct Dhcp6Response {
    pub client_id: Vec<u8>,
    pub server_id: Vec<u8>,
    pub pd: Option<PdPrefix>,
    pub nameserver_addrs: Vec<Ipv6Addr>,
    pub domain_search_list: Vec<String>,
    pub sip_server_addrs: Vec<Ipv6Addr>,
    pub sntp_server_addrs: Vec<Ipv6Addr>,
}

pub fn ipv6_ll_to_mac(ll_addr: Ipv6Addr) -> [u8; 6] {
    if !ll_addr.is_unicast_link_local() {
        return [0; 6];
    }

    let addr: [u8; 16] = ll_addr.octets();
    let mut part1: [u8; 3] = addr[8..11].try_into().unwrap();
    part1[0] ^= 2;
    let part2: [u8; 3] = addr[13..16].try_into().unwrap();
    let mut mac: [u8; 6] = [0; 6];
    mac[0] = part1[0];
    mac[1] = part1[1];
    mac[2] = part1[2];
    mac[3] = part2[0];
    mac[4] = part2[1];
    mac[5] = part2[2];
    mac
}

impl Dhcp6Client {
    pub const CLIENT_PORT: u16 = 546;
    pub const SERVER_PORT: u16 = 547;
    pub const VENDOR_CODE_NTT: u32 = 210;

    pub fn new(local_ll_address: Ipv6Addr, local_if_mac: [u8; 6], if_name: &str) -> std::io::Result<Self> {
        if !local_ll_address.is_unicast_link_local() {
            return Err(std::io::Error::new(ErrorKind::InvalidInput, "Invalid IPv6 link-local address"));
        }

        let socket = Socket::new(Domain::IPV6, Type::DGRAM, None)?;
        socket.bind_device(Some(if_name.as_bytes()))?;
        socket.bind(&(SocketAddr::V6(SocketAddrV6::new(local_ll_address, Self::CLIENT_PORT, 0, 0)).into()))?;
        socket.set_nonblocking(false)?;
        let socket: std::net::UdpSocket = socket.into();
        socket.set_read_timeout(Some(Duration::from_secs(15)))?;
        socket.set_write_timeout(Some(Duration::from_secs(15)))?;
        Ok(Self {
            socket,
            local_if_mac,
            local_ll_addr: local_ll_address,
        })
    }

    fn encode_send(&self, msg: dhcproto::v6::Message) -> std::io::Result<()> {
        let mut buf = Vec::with_capacity(1500);
        let mut e = Encoder::new(&mut buf);
        msg.encode(&mut e).map_err(|_| std::io::Error::new(ErrorKind::InvalidData, "DHCPv6 encoding failed"))?;
        let buf = e.buffer_filled();
        let sentlen = self.socket.send_to(buf, ("ff02::1:2", Self::SERVER_PORT))?;
        if sentlen < buf.len() {
            log::error!("Packet ({} Bytes) not sent in whole", buf.len());
        } else {
            log::debug!("Packet ({} Bytes) sent", buf.len());
        }
        Ok(())
    }

    pub fn local_ll(&self) -> std::io::Result<Ipv6Addr> {
        let addr = self.local_ll_addr;
        assert!(addr.is_unicast_link_local(), "Local address is not link local");
        Ok(addr)
    }

    pub fn local_duid(&self) -> std::io::Result<Vec<u8>> {
        let mac = self.local_if_mac;
        let mut duid: Vec<u8> = vec![0x00, 0x03, 0x00, 0x01];
        duid.extend_from_slice(&mac);
        Ok(duid)
    }

    pub fn solicit_pd(&self, elapsed: Duration, ia_id: u32) -> std::io::Result<()> {
        let duid = self.local_duid()?;
        let mut msg = dhcproto::v6::Message::new(dhcproto::v6::MessageType::Solicit);
        msg.opts_mut().insert(DhcpOption::ClientId(duid));
        msg.opts_mut().insert(DhcpOption::ElapsedTime(elapsed.as_millis().try_into().unwrap_or(0)));
        msg.opts_mut().insert(DhcpOption::IAPD(IAPD {
            id: ia_id,
            t1: 0,
            t2: 0,
            opts: DhcpOptions::new(),
        }));
        self.encode_send(msg)?;
        Ok(())
    }

    pub fn request_pd(&self, elapsed: Duration, ia_id: u32, server_id: Vec<u8>, pd: PdPrefix) -> std::io::Result<()> {
        let duid = self.local_duid()?;
        let mut msg = dhcproto::v6::Message::new(dhcproto::v6::MessageType::Request);
        msg.opts_mut().insert(DhcpOption::ClientId(duid));
        msg.opts_mut().insert(DhcpOption::ServerId(server_id));
        let mut oro = dhcproto::v6::ORO {
            opts: Vec::new(),
        };
        oro.opts.push(OptionCode::IAPD);
        oro.opts.push(OptionCode::SipServerA);
        oro.opts.push(OptionCode::SntpServers);
        oro.opts.push(OptionCode::DomainNameServers);
        oro.opts.push(OptionCode::DomainSearchList);
        msg.opts_mut().insert(DhcpOption::ORO(oro));
        msg.opts_mut().insert(DhcpOption::ElapsedTime(elapsed.as_millis().try_into().unwrap_or(0)));

        let mut data1 = Vec::new();
        data1.extend_from_slice(&u32::to_be_bytes(Self::VENDOR_CODE_NTT));
        data1.extend_from_slice(&u16::to_be_bytes(6));
        data1.extend_from_slice(&self.local_if_mac);
        msg.opts_mut().insert(DhcpOption::Unknown(UnknownOption::new(
            OptionCode::VendorClass,
            data1,
        )));
        // msg.opts_mut().insert(DhcpOption::VendorClass(VendorClass {
        //     num: Self::VENDOR_CODE_NTT,
        //     data: vec![self.local_if_mac.to_vec()], // MAC address
        // }));

        let mut pd_options = DhcpOptions::new();
        let mut prefix_options = DhcpOptions::new();
        prefix_options.insert(DhcpOption::StatusCode(StatusCode {
            status: dhcproto::v6::Status::Success,
            msg: "".to_string(),
        }));
        pd_options.insert(DhcpOption::IAPrefix(IAPrefix {
            preferred_lifetime: pd.preferred_lifetime,
            valid_lifetime: pd.valid_lifetime,
            prefix_ip: pd.prefix,
            prefix_len: pd.prefix_len,
            opts: prefix_options,
        }));
        msg.opts_mut().insert(DhcpOption::IAPD(IAPD {
            id: ia_id,
            t1: 0,
            t2: 0,
            opts: pd_options,
        }));
        log::debug!("REQUEST: {:?}", &msg);
        self.encode_send(msg)?;
        Ok(())
    }

    fn recv_msg(&self) -> std::io::Result<dhcproto::v6::Message> {
        let mut buf = [0u8; 1500];
        let (nlen, _remote_addr) = self.socket.recv_from(&mut buf)?;
        let slice = &buf[..nlen];
        let msg = dhcproto::v6::Message::decode(&mut Decoder::new(slice))
        .map_err(|_| std::io::Error::new(ErrorKind::InvalidData, "DHCPv6 decoding failed"))?;
        Ok(msg)
    }

    pub fn recv(&self, expected_msg_type: MessageType) -> std::io::Result<Dhcp6Response> {
        let msg = self.recv_msg()?;
        let msg_type = msg.msg_type();
        if msg_type != expected_msg_type {
            return Err(std::io::Error::new(ErrorKind::InvalidData, "Unexpected message type"));
        }

        let mut domain_search_list = Vec::new();
        let mut nameserver_addrs = Vec::new();
        let mut sntp_server_addrs = Vec::new();
        let mut sip_server_addrs = Vec::new();
        let mut client_id = None;
        let mut server_id = None;
        let mut t1: u32 = 0;
        let mut t2: u32 = 0;
        let mut valid_lifetime: u32 = 0;
        let mut preferred_lifetime: u32 = 0;
        let mut prefix = None;
        let mut prefix_len = None;
        for opt in msg.opts().iter() {
            let opt = opt.to_owned();
            match opt {
                DhcpOption::ClientId(id) => {
                    let duid = self.local_duid()?;
                    if id != duid {
                        return Err(std::io::Error::new(ErrorKind::InvalidData, "client DUID mismatch"));
                    }
                    client_id = Some(id);
                },

                DhcpOption::ServerId(id) => {
                    server_id = Some(id);
                },

                DhcpOption::StatusCode(code) => {
                    match code.status {
                        Status::Success => {},
                        _ => {
                            return Err(std::io::Error::new(ErrorKind::ConnectionAborted, "DHCP error"));
                        },
                    }
                },

                DhcpOption::DomainNameServers(srv) => {
                    nameserver_addrs.extend_from_slice(&srv);
                },

                DhcpOption::DomainSearchList(l) => {
                    for name in l {
                        domain_search_list.push(name.to_ascii());
                    }
                },

                DhcpOption::IAPD(pd) => {
                    t1 = pd.t1;
                    t2 = pd.t2;

                    for opt in pd.opts.iter() {
                        let opt = opt.to_owned();
                        match opt {
                            DhcpOption::IAPrefix(pd_prefix) => {
                                valid_lifetime = pd_prefix.valid_lifetime;
                                preferred_lifetime = pd_prefix.preferred_lifetime;
                                prefix = Some(pd_prefix.prefix_ip);
                                prefix_len = Some(pd_prefix.prefix_len);
                            },
                            _ => {},
                        }
                    }
                },

                DhcpOption::Unknown(opt) => {
                    let code = opt.code();
                    let (_, data) = opt.into_parts();

                    match code {
                        OptionCode::SipServerA => {
                            for i in 0usize.. {
                                let start = i * 16;
                                let end = start + 16;
                                if end > data.len() {
                                    break;
                                }
                                let addr: [u8; 16] = data[start..end].try_into().unwrap();
                                let addr: Ipv6Addr = addr.into();
                                sip_server_addrs.push(addr);
                            }
                        },
                        OptionCode::SntpServers => {
                            for i in 0usize.. {
                                let start = i * 16;
                                let end = start + 16;
                                if end > data.len() {
                                    break;
                                }
                                let addr: [u8; 16] = data[start..end].try_into().unwrap();
                                let addr: Ipv6Addr = addr.into();
                                sntp_server_addrs.push(addr);
                            }
                        }
                        _ => {},
                    }
                },

                _ => {},
            }
        }

        let pd;
        if prefix.is_none() || prefix_len.is_none() {
            pd = None;
        } else {
            pd = Some(PdPrefix {
                prefix: prefix.unwrap(),
                prefix_len: prefix_len.unwrap(),
                preferred_lifetime,
                valid_lifetime,
                t1,
                t2,
            });
        }

        let res = Dhcp6Response {
            client_id: client_id.unwrap(),
            server_id: server_id.unwrap(),
            pd,
            nameserver_addrs,
            domain_search_list,
            sip_server_addrs,
            sntp_server_addrs,
        };
        Ok(res)
    }

}