diff --git a/example.yaml b/example.yaml index 0702ba5..471eba7 100644 --- a/example.yaml +++ b/example.yaml @@ -244,7 +244,9 @@ v6: value: - 2001:db8::1 - 2001:db8::2 - +#TODO: +# 1. ipv6 static addr; +# 2. server preference(https://datatracker.ietf.org/doc/html/rfc8415#section-21.8) # Example Client Classifier # # We define in the dora config a client_classes section, where each class has a predicate whose syntax diff --git a/libs/config/src/lib.rs b/libs/config/src/lib.rs index dde8b1b..63191dc 100644 --- a/libs/config/src/lib.rs +++ b/libs/config/src/lib.rs @@ -184,7 +184,7 @@ pub fn rebind(t: Duration) -> Duration { } pub fn generate_random_bytes(len: usize) -> Vec { - let mut ident = Vec::with_capacity(len); + let mut ident = vec![0;len]; rand::thread_rng().fill_bytes(&mut ident); ident } diff --git a/plugins/leases/src/lib.rs b/plugins/leases/src/lib.rs index 5b50038..988fe08 100644 --- a/plugins/leases/src/lib.rs +++ b/plugins/leases/src/lib.rs @@ -23,6 +23,8 @@ use dora_core::{ anyhow::anyhow, chrono::{DateTime, SecondsFormat, Utc}, dhcproto::v4::{DhcpOption, Message, MessageType, OptionCode}, + dhcproto::v6, + dhcproto::v6::OptionCode as v6OptionCode, metrics, prelude::*, tracing::warn, @@ -39,6 +41,7 @@ use ip_manager::{IpError, IpManager, IpState, Storage}; #[derive(Register)] #[register(msg(Message))] +#[register(msg(v6::Message))] #[register(plugin(StaticAddr))] pub struct Leases where @@ -163,6 +166,107 @@ where } } +#[async_trait] +impl Plugin for Leases +where + S: Storage + Send + Sync + 'static, +{ + #[instrument(level = "debug", skip_all)] + async fn handle(&self, ctx: &mut MsgContext) -> Result { + let req = ctx.msg(); + let meta = ctx.meta(); + let client_id = self + .cfg + .v6() + .get_opts(meta.ifindex) + .context("can not get dhcp options")? + .get(v6OptionCode::ClientId) + .context("no client id")?; + //TODO unfinish + let rapid_commit = ctx.msg().opts().get(v6::OptionCode::RapidCommit).is_some() + && self.cfg.v4().rapid_commit(); + let network = self + .cfg + .v6() + .get_network(meta.ifindex) + .context("no network")?; + match req.msg_type() { + v6::MessageType::Solicit => { + //self.solicit() + todo!() + } + v6::MessageType::Request => todo!(), + v6::MessageType::Confirm => todo!(), + v6::MessageType::Renew => todo!(), + v6::MessageType::Rebind => todo!(), + v6::MessageType::Reply => todo!(), + v6::MessageType::Release => todo!(), + v6::MessageType::Decline => todo!(), + v6::MessageType::InformationRequest => todo!(), + v6::MessageType::RelayForw => todo!(), + _ => { + debug!("unsupported message type"); + return Ok(Action::NoResponse); + } + } + } +} + +//v6 related +impl Leases +where + S: Storage, +{ + async fn solicit( + &self, + ctx: &mut MsgContext, + server_id: &[u8], + client_id: &[u8], + network: &Network, + rapid_commit: bool, + ) -> Result { + if rapid_commit { + todo!() + } else { + ctx.resp_msg_mut() + .unwrap() + .opts_mut() + .insert(v6::DhcpOption::ServerId(server_id.to_vec())); + ctx.resp_msg_mut() + .unwrap() + .opts_mut() + .insert(v6::DhcpOption::ClientId(client_id.to_vec())); + //TODO: Preference option + //TODO: Reconfigure Accept option + // fill informations that requested in ORO + if let Some(opts) = self.cfg.v6().get_opts(ctx.meta().ifindex) { + ctx.populate_opts(opts); + } + //TODO: IA related and leases + // IANA + if let Some(ia_na) = ctx.msg().opts().get(v6::OptionCode::IANA) { + let iana = match ia_na { + v6::DhcpOption::IANA(iana) => iana, + _ => unreachable!(), + }; + let iaid = iana.id; + let t1 = iana.t1; + let t2 = iana.t2; + //TODO: generate v6 lease and addr + let iana = v6::IANA { + id: iaid, + t1: t1, + t2: t2, + opts: todo!(), + }; + let iana = v6::DhcpOption::IANA(iana); + ctx.resp_msg_mut().unwrap().opts_mut().insert(iana); + } + } + Ok(Action::Continue) + } +} + impl Leases where S: Storage, diff --git a/plugins/message-type/src/lib.rs b/plugins/message-type/src/lib.rs index bc9d960..e3f91f3 100644 --- a/plugins/message-type/src/lib.rs +++ b/plugins/message-type/src/lib.rs @@ -338,6 +338,29 @@ pub mod util { use dora_core::server::msg::SerialMsg; use unix_udp_sock::RecvMeta; + pub fn blank_ctx_v6( + recv_addr: SocketAddr, + ifindex: u32, + msg_type: v6::MessageType, + ) -> Result> { + let msg = dhcproto::v6::Message::new(msg_type); + let buf = msg.to_vec().unwrap(); + let meta = RecvMeta { + addr: recv_addr, + len: buf.len(), + ifindex, + // recv addr copied here + dst_ip: Some(recv_addr.ip()), + ..RecvMeta::default() + }; + let ctx: MsgContext = MsgContext::new( + SerialMsg::new(buf.into(), recv_addr), + meta, + Arc::new(State::new(10)), + )?; + Ok(ctx) + } + /// for testing pub fn blank_ctx( recv_addr: SocketAddr, @@ -429,10 +452,13 @@ impl Plugin for MsgType { // create initial response with reply type let mut resp = v6::Message::new_with_id(Reply, req.xid()); + let rapid_commit = ctx.msg().opts().get(v6::OptionCode::RapidCommit).is_some() + && self.cfg.v4().rapid_commit(); let server_id = self.cfg.v6().server_id(); // TODO RelayForw type // TODO: make sure we handle client ids as specified - https://www.rfc-editor.org/rfc/rfc8415#section-16.1 let req_sid = req.opts().get(v6::OptionCode::ServerId); + let req_cid = req.opts().get(v6::OptionCode::ClientId); // if the request includes a server id, it must match our server id if matches!(req_sid, Some(v6::DhcpOption::ServerId(id)) if *id != server_id) { debug!(?server_id, "server identifier in msg doesn't match"); @@ -443,12 +469,70 @@ impl Plugin for MsgType { .insert(v6::DhcpOption::ServerId(server_id.to_vec())); match msg_type { - // discard if it has these types but NO server id - // https://www.rfc-editor.org/rfc/rfc8415#section-16.6 - Request | Renew | Decline | Release if req_sid.is_none() => { - return Ok(Action::NoResponse); + Solicit => { + //https://datatracker.ietf.org/doc/html/rfc8415#section-16.2 + if req_sid.is_some() || req_cid.is_none() { + return Ok(Action::NoResponse); + } + if rapid_commit { + resp.set_msg_type(v6::MessageType::Reply); + } else { + resp.set_msg_type(v6::MessageType::Advertise); + } + //TODO: discard if req not fulfill administrative policy + } + Request => { + // https://datatracker.ietf.org/doc/html/rfc8415#section-16.4 + if req_sid.is_none() || req_cid.is_none() { + return Ok(Action::NoResponse); + } + resp.set_msg_type(v6::MessageType::Reply); + } + Confirm => { + // https://datatracker.ietf.org/doc/html/rfc8415#section-16.5 + if req_sid.is_some() || req_cid.is_none() { + return Ok(Action::NoResponse); + } + resp.set_msg_type(v6::MessageType::Reply); + } + Renew => { + // https://datatracker.ietf.org/doc/html/rfc8415#section-16.6 + if req_sid.is_none() || req_cid.is_none() { + return Ok(Action::NoResponse); + } + resp.set_msg_type(v6::MessageType::Reply); + } + Rebind => { + // https://datatracker.ietf.org/doc/html/rfc8415#section-16.7 + if req_sid.is_some() || req_cid.is_none() { + return Ok(Action::NoResponse); + } + resp.set_msg_type(v6::MessageType::Reply); + } + Decline => { + // https://datatracker.ietf.org/doc/html/rfc8415#section-16.8 + if req_sid.is_none() || req_cid.is_none() { + return Ok(Action::NoResponse); + } + resp.set_msg_type(v6::MessageType::Reply); + } + Release => { + // https://datatracker.ietf.org/doc/html/rfc8415#section-16.9 + if req_sid.is_none() || req_cid.is_none() { + return Ok(Action::NoResponse); + } + resp.set_msg_type(v6::MessageType::Reply); } InformationRequest => { + // discard if req has IA option + // https://datatracker.ietf.org/doc/html/rfc8415#section-16.12 + if req.opts().get(v6::OptionCode::IANA).is_some() + || req.opts().get(v6::OptionCode::IATA).is_some() + || req.opts().get(v6::OptionCode::IAPD).is_some() + { + return Ok(Action::NoResponse); + } + resp.set_msg_type(v6::MessageType::Reply); if let Some(opts) = self.cfg.v6().get_opts(meta.ifindex) { ctx.set_resp_msg(resp); ctx.populate_opts(opts); @@ -460,6 +544,7 @@ impl Plugin for MsgType { "couldn't match any options with INFORMATION-REQUEST message" ); } + //RelayForw => {} _ => { debug!("currently unsupported message type"); return Ok(Action::NoResponse); @@ -477,14 +562,19 @@ pub struct MatchedClasses(pub Vec); #[cfg(test)] mod tests { + use config::{generate_random_bytes, v6::is_unicast_link_local}; use util::get_server_id_override; - use dora_core::dhcproto::v4::{self, relay}; + use dora_core::dhcproto::{ + v4::{self, relay}, + v6::{duid::Duid, ORO}, + }; use tracing_test::traced_test; use super::*; static SAMPLE_YAML: &str = include_str!("../../../libs/config/sample/config.yaml"); + static V6_EXAMPLE_YAML: &str = include_str!("../../../libs/config/sample/config_v6.yaml"); #[tokio::test] #[traced_test] @@ -629,4 +719,91 @@ mod tests { assert_eq!(res, Action::NoResponse); Ok(()) } + + /// for testing + fn find_interface_with_unicast_link_local(cfg: &DhcpConfig) -> Option { + let interfaces = cfg.v6().interfaces(); + //find index of first interface that has a unicast address + interfaces.iter().find_map(|int| { + int.ips.iter().find_map(|ip| match ip { + IpNetwork::V6(ip) => { + if is_unicast_link_local(&ip.ip()) { + Some(int.index) + } else { + None + } + } + _ => None, + }) + }) + } + + // create a uuid type duid + fn generate_duid() -> Result { + let bytes = generate_random_bytes(16); + println!("!!!{:?}",bytes); + let duid = Duid::uuid(&bytes); + Ok(duid) + } + ///test that we respond to an information request + #[tokio::test] + #[traced_test] + async fn test_information_request() -> Result<()> { + let cfg = DhcpConfig::parse_str(V6_EXAMPLE_YAML).unwrap(); + //find index of first interface that has a unicast address + let ifindex = find_interface_with_unicast_link_local(&cfg) + .context("no interface with unicast link local address")?; + let plugin = MsgType::new(Arc::new(cfg.clone()))?; + let mut ctx = util::blank_ctx_v6( + "[2001:db8::1]:546".parse()?, + ifindex as u32, + v6::MessageType::InformationRequest, + )?; + //according to https://datatracker.ietf.org/doc/html/rfc8415#section-18.2.6, Information-request Messages might not include Client Identifier option, so here we ignore it. + //add elapsed time option + ctx.msg_mut() + .opts_mut() + .insert(v6::DhcpOption::ElapsedTime(60)); + let oro = ORO { + opts: vec![ + v6::OptionCode::InfMaxRt, + v6::OptionCode::InformationRefreshTime, + v6::OptionCode::DomainNameServers, + ], + }; + //add option request + ctx.msg_mut().opts_mut().insert(v6::DhcpOption::ORO(oro)); + let res = plugin.handle(&mut ctx).await?; + let resp = ctx.resp_msg().unwrap(); + println!("{:?}", resp); + assert_eq!(res, Action::Respond); + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn test_solicit() -> Result<()> { + let cfg = DhcpConfig::parse_str(V6_EXAMPLE_YAML).unwrap(); + //find index of first interface that has a unicast address + let ifindex = find_interface_with_unicast_link_local(&cfg) + .context("no interface with unicast link local address")?; + let plugin = MsgType::new(Arc::new(cfg.clone()))?; + + let mut ctx = util::blank_ctx_v6( + "[2001:db8::1]:546".parse()?, + ifindex as u32, + v6::MessageType::Solicit, + )?; + let client_id = generate_duid().unwrap().as_ref().to_vec(); + ctx.msg_mut() + .opts_mut() + .insert(v6::DhcpOption::ClientId(client_id)); + //ctx.msg_mut().opts_mut().insert(v6::DhcpOption::ClientId()); + let res = plugin.handle(&mut ctx).await?; + println!("{:?}", res); + //let resp = ctx.resp_msg().unwrap(); + //println!("{:?}", resp); + //assert_eq!(res, Action::Continue); + Ok(()) + } }