1use anyhow::Context;
2use dhcproto::v4::{
3 Decodable, Decoder, DhcpOption, Encodable, Encoder, Message, MessageType, Opcode, OptionCode,
4};
5use jiff::{ToSpan, Unit, Zoned};
6use rand::Rng;
7use serde::Serialize;
8use std::net::Ipv4Addr;
9use std::path::PathBuf;
10use tokio::{net::UdpSocket, sync::mpsc};
11use tracing::{error, info, warn};
12pub mod db;
13pub mod info;
14
15#[derive(Debug, Serialize)]
16pub struct Client {
17 pub ip: Ipv4Addr,
18 pub client_id: String,
19}
20
21fn check_ip_in_range(addr: Ipv4Addr) -> bool {
22 let [a, b, c, d] = addr.octets();
23 a == 192 && b == 168 && c == 1 && (100..200).contains(&d)
24}
25
26fn get_ip_in_range() -> Ipv4Addr {
27 let octet = rand::thread_rng().gen_range(100..200);
28 Ipv4Addr::new(192, 168, 1, octet)
29}
30
31async fn insert_lease(
32 store: &db::LeaseStore,
33 ip: Ipv4Addr,
34 client_id: &Vec<u8>,
35) -> anyhow::Result<()> {
36 let expire_at = Zoned::now()
37 .round(Unit::Second)?
38 .checked_add(1.hour())
39 .with_context(|| "Failed to calculate lease expiry time".to_string())?
40 .timestamp()
41 .as_second();
42
43 let lease = db::Lease {
44 ip,
45 client_id: client_id.clone(),
46 leased: true,
47 expires_at: expire_at,
48 network: 0,
49 probation: false,
50 };
51
52 store.insert_lease(lease).await?;
53 Ok(())
54}
55
56async fn build_dhcp_offer_packet(
57 leases: &db::LeaseStore,
58 discover_message: &Message,
59) -> anyhow::Result<Message> {
60 let xid = discover_message.xid();
61 let client_id = discover_message.chaddr().to_vec();
62
63 let mut suggested_address = match leases.get_ip_from_client_id(&client_id).await {
72 Ok(address) => {
73 match leases.get_lease_by_ip(&address).await {
75 Ok(lease) => {
76 let current_time = Zoned::now().round(Unit::Second)?.timestamp().as_second();
77 if lease.expires_at < current_time {
78 let new_expiry = Zoned::now()
80 .round(Unit::Second)?
81 .checked_add(1.hour())
82 .with_context(|| "Failed to calculate lease expiry time".to_string())?
83 .timestamp()
84 .as_second();
85 leases.update_lease_expiry(address, new_expiry).await?;
86 info!(
87 "[{xid:X}] [OFFER] Client {:?} has expired lease for {:?}, renewed",
88 client_id, address
89 );
90 } else {
91 info!(
92 "[{xid:X}] [OFFER] Client {:?} already has IP assigned: {:?}",
93 client_id, address
94 );
95 }
96 Some(address)
97 }
98 Err(_) => {
99 warn!(
100 "[{xid:X}] [OFFER] Could not fetch lease details for {:?}",
101 address
102 );
103 Some(address)
104 }
105 }
106 }
107 _ => {
108 warn!(
109 "[{xid:X}] [OFFER] Client {:?} has no IP assigned in the database",
110 client_id
111 );
112 None
113 }
114 };
115
116 if suggested_address.is_none() {
119 let requested_ip_address = discover_message.opts().get(OptionCode::RequestedIpAddress);
120 info!(
121 "[{xid:X}] [OFFER] Client requested IP address: {:?}",
122 requested_ip_address
123 );
124 match requested_ip_address {
125 Some(DhcpOption::RequestedIpAddress(ip)) => {
126 if !leases.is_ip_assigned(*ip).await? && check_ip_in_range(*ip) {
127 insert_lease(leases, *ip, &client_id).await?;
128 suggested_address = Some(*ip);
129 }
130 }
131 _ => {
132 warn!("[{xid:X}] [OFFER] No requested IP address")
133 }
134 };
135 }
136
137 if suggested_address.is_none() {
139 for _ in 0..10 {
140 let random_address = get_ip_in_range();
141 if !leases.is_ip_assigned(random_address).await? {
142 insert_lease(leases, random_address, &client_id).await?;
143 suggested_address = Some(random_address);
144 break;
145 }
146 }
147 }
148
149 let suggested_address = match suggested_address {
150 Some(address) => address,
151 None => return Err(anyhow::anyhow!("Could not assign IP address")),
152 };
153
154 info!(
155 "[{xid:X}] [OFFER] Creating offer with IP {}",
156 suggested_address
157 );
158
159 let mut offer = Message::default();
160
161 let reply_opcode = Opcode::BootReply;
162 offer.set_opcode(reply_opcode);
163 offer.set_xid(discover_message.xid());
164 offer.set_yiaddr(suggested_address);
165 offer.set_siaddr(Ipv4Addr::new(192, 168, 1, 69));
166 offer.set_flags(discover_message.flags());
167 offer.set_giaddr(discover_message.giaddr());
168 offer.set_chaddr(discover_message.chaddr());
169
170 offer
171 .opts_mut()
172 .insert(DhcpOption::MessageType(MessageType::Offer));
173 offer.opts_mut().insert(DhcpOption::AddressLeaseTime(3600));
174 offer
175 .opts_mut()
176 .insert(DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 69)));
177 offer
178 .opts_mut()
179 .insert(DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)));
180 offer
181 .opts_mut()
182 .insert(DhcpOption::BroadcastAddr(Ipv4Addr::new(255, 255, 255, 255)));
183 offer
184 .opts_mut()
185 .insert(DhcpOption::Router(vec![Ipv4Addr::new(192, 168, 1, 69)]));
186
187 Ok(offer)
188}
189
190#[derive(Debug)]
192enum DhcpResponse {
193 Ack(Message),
194 Nak(Message),
195}
196
197fn build_dhcp_nack_packet(request_message: &Message, reason: &str) -> Message {
199 let xid = request_message.xid();
200 info!("[{xid:X}] [NAK] Sending: {}", reason);
201
202 let mut nak = Message::default();
203 nak.set_opcode(Opcode::BootReply);
204 nak.set_xid(request_message.xid());
205 nak.set_flags(request_message.flags());
206 nak.set_chaddr(request_message.chaddr());
207
208 nak.set_yiaddr(Ipv4Addr::new(0, 0, 0, 0));
210 nak.set_siaddr(Ipv4Addr::new(0, 0, 0, 0));
211
212 nak.opts_mut()
214 .insert(DhcpOption::MessageType(MessageType::Nak));
215 nak.opts_mut()
216 .insert(DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 69)));
217
218 nak
219}
220
221async fn build_dhcp_ack_packet(
222 leases: &db::LeaseStore,
223 request_message: &Message,
224) -> anyhow::Result<DhcpResponse> {
225 let xid = request_message.xid();
226 let server_identifier_option = request_message.opts().get(OptionCode::ServerIdentifier);
227
228 let requested_ip_option = request_message.opts().get(OptionCode::RequestedIpAddress);
229
230 let ciaddr = request_message.ciaddr();
232 let chaddr = request_message.chaddr().to_owned();
233
234 let (is_selecting, is_init_reboot, is_renewing_rebinding) = {
235 let have_server_id = server_identifier_option.is_some();
236 let have_requested_ip = requested_ip_option.is_some();
237 let ciaddr_is_zero = ciaddr == Ipv4Addr::new(0, 0, 0, 0);
238
239 let selecting = have_server_id && have_requested_ip && ciaddr_is_zero;
241
242 let init_reboot = !have_server_id && have_requested_ip && ciaddr_is_zero;
244
245 let renewing_rebinding = ciaddr != Ipv4Addr::new(0, 0, 0, 0) && !have_requested_ip;
247
248 (selecting, init_reboot, renewing_rebinding)
249 };
250
251 let ip_to_validate = if is_selecting || is_init_reboot {
252 match requested_ip_option {
253 Some(DhcpOption::RequestedIpAddress(ip)) => {
254 info!("[{xid:X}] [ACK] Client requested IP address: {:?}", ip);
255 ip
256 }
257 _ => {
258 return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
259 request_message,
260 "Client didn't provide requested IP address",
261 )));
262 }
263 }
264 } else if is_renewing_rebinding {
265 info!("[{xid:X}] [ACK] Using ciaddr {:?}", ciaddr);
266 &ciaddr
267 } else {
268 return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
269 request_message,
270 "DHCPREQUEST does not match any known valid state",
271 )));
272 };
273
274 if !check_ip_in_range(*ip_to_validate) {
279 return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
280 request_message,
281 "Requested IP address is outside valid range",
282 )));
283 }
284
285 let lease = match leases.get_lease_by_ip(ip_to_validate).await {
286 Ok(lease) => Some(lease),
287 Err(db::LeaseError::NotFound) => {
288 if leases.is_ip_assigned(*ip_to_validate).await? {
290 warn!(
291 "[{xid:X}] [ACK] IP {:?} is assigned to another client",
292 ip_to_validate
293 );
294 return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
295 request_message,
296 "IP address is assigned to another client",
297 )));
298 }
299
300 info!(
304 "[{xid:X}] [ACK] No lease record found, creating lease for {:?}",
305 ip_to_validate
306 );
307 insert_lease(leases, *ip_to_validate, &chaddr).await?;
308 None
309 }
310 Err(e) => {
311 warn!("[{xid:X}] [ACK] Database error: {:?}", e);
312 return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
313 request_message,
314 "Database error",
315 )));
316 }
317 };
318
319 if let Some(lease) = &lease {
321 if !lease.leased {
322 return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
323 request_message,
324 "IP address is not currently leased",
325 )));
326 }
327
328 let current_time = Zoned::now().round(Unit::Second)?.timestamp().as_second();
330 if lease.expires_at < current_time {
331 warn!(
332 "[{xid:X}] [ACK] Lease has expired: expires_at={}, current={}",
333 lease.expires_at, current_time
334 );
335 return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
336 request_message,
337 "Lease has expired",
338 )));
339 }
340
341 if lease.client_id != chaddr {
343 warn!(
344 "[{xid:X}] [ACK] Client ID mismatch: lease has {:?}, request has {:?}",
345 lease.client_id, chaddr
346 );
347 return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
348 request_message,
349 "IP address is leased to a different client",
350 )));
351 }
352 }
353
354 let mut ack = Message::default();
355 ack.set_opcode(Opcode::BootReply);
356 ack.set_xid(request_message.xid());
357 ack.set_flags(request_message.flags());
358 ack.set_giaddr(request_message.giaddr());
359 ack.set_chaddr(&chaddr);
360
361 ack.set_yiaddr(*ip_to_validate);
362
363 ack.opts_mut()
364 .insert(DhcpOption::MessageType(MessageType::Ack));
365 ack.opts_mut()
366 .insert(DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 69)));
367
368 ack.opts_mut().insert(DhcpOption::AddressLeaseTime(3600));
369 ack.opts_mut()
370 .insert(DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)));
371 ack.opts_mut()
372 .insert(DhcpOption::BroadcastAddr(Ipv4Addr::new(255, 255, 255, 255)));
373 ack.opts_mut()
374 .insert(DhcpOption::Router(vec![Ipv4Addr::new(192, 168, 1, 69)]));
375
376 let new_expiry = Zoned::now()
378 .round(Unit::Second)?
379 .checked_add(1.hour())
380 .with_context(|| "Failed to calculate new lease expiry time".to_string())?
381 .timestamp()
382 .as_second();
383
384 leases
385 .update_lease_expiry(*ip_to_validate, new_expiry)
386 .await?;
387
388 Ok(DhcpResponse::Ack(ack))
389}
390
391#[derive(Clone)]
392pub struct MiniDHCPConfiguration {
393 interface: String,
394 event_queue: Vec<mpsc::Sender<String>>,
395 pub leases: db::LeaseStore,
396}
397
398pub struct MiniDHCPConfigurationBuilder {
399 interface: Option<String>,
400 event_queue: Vec<mpsc::Sender<String>>,
401}
402
403impl MiniDHCPConfigurationBuilder {
404 pub fn new() -> MiniDHCPConfigurationBuilder {
405 Self {
406 interface: None,
407 event_queue: Vec::new(),
408 }
409 }
410
411 pub fn set_listening_interface(mut self, interface: &str) -> Self {
412 self.interface = Some(interface.into());
413 self
414 }
415
416 pub fn set_event_queue(mut self, event_queue: mpsc::Sender<String>) -> Self {
417 self.event_queue.push(event_queue);
418 self
419 }
420
421 pub async fn build(self) -> anyhow::Result<MiniDHCPConfiguration> {
422 let interface = self
423 .interface
424 .ok_or_else(|| anyhow::anyhow!("Interface not set"))?;
425
426 let leases = db::LeaseStore::new(PathBuf::from("leases.csv")).await?;
427
428 let event_queue = self.event_queue;
429
430 Ok(MiniDHCPConfiguration {
431 interface,
432 leases,
433 event_queue,
434 })
435 }
436}
437
438async fn handle_discover(
439 config: &MiniDHCPConfiguration,
440 decoded_message: &Message,
441) -> anyhow::Result<Vec<u8>> {
442 let transaction_id = decoded_message.xid();
443 let client_address = decoded_message.chaddr();
444 info!("[{:X}] DISCOVER {:?}", transaction_id, client_address);
445 let offer = build_dhcp_offer_packet(&config.leases, decoded_message);
446
447 match offer.await {
448 Ok(offer) => {
449 let offered_ip = offer.yiaddr();
450 info!(
451 "[{:X}] [OFFER]: client {:?} ip {:?}",
452 transaction_id, client_address, offered_ip
453 );
454
455 let mut buf = Vec::new();
456 let mut e = Encoder::new(&mut buf);
457 offer.encode(&mut e)?;
458 Ok(buf)
459 }
460 Err(e) => {
461 anyhow::bail!("OFFER Error: {:?}", e)
462 }
463 }
464}
465
466async fn handle_request(
467 config: &MiniDHCPConfiguration,
468 decoded_message: &Message,
469) -> anyhow::Result<Vec<u8>> {
470 let options = decoded_message.opts();
471 let transaction_id = decoded_message.xid();
472 let client_address = decoded_message.chaddr();
473 let server_identifier = options.get(OptionCode::ServerIdentifier);
474 info!(
475 "[{:X}] REQUEST from {:?} to {:?}",
476 transaction_id, client_address, server_identifier
477 );
478
479 let response = build_dhcp_ack_packet(&config.leases, decoded_message).await?;
480
481 match response {
482 DhcpResponse::Ack(ack) => {
483 let offered_ip = ack.yiaddr();
484 info!(
485 "[{:X}] [ACK]: {:?} {:?}",
486 transaction_id, client_address, offered_ip
487 );
488
489 for sender in &config.event_queue {
491 let msg = format!("NEW_LEASE: {} -> {:?}", offered_ip, client_address);
492 let _ = sender.try_send(msg);
493 }
494
495 let mut buf = Vec::new();
496 let mut e = Encoder::new(&mut buf);
497 ack.encode(&mut e)?;
498 Ok(buf)
499 }
500 DhcpResponse::Nak(nak) => {
501 info!("[{:X}] [NAK]: {:?}", transaction_id, client_address);
502
503 let mut buf = Vec::new();
504 let mut e = Encoder::new(&mut buf);
505 nak.encode(&mut e)?;
506 Ok(buf)
507 }
508 }
509}
510
511pub async fn start(config: MiniDHCPConfiguration) -> anyhow::Result<()> {
512 let address = "0.0.0.0:67";
513 info!("Starting DHCP listener [{}] {}", config.interface, address);
514 let socket = UdpSocket::bind(address).await?;
515 socket.set_broadcast(true)?;
516 socket.bind_device(Some(config.interface.as_bytes()))?;
517
518 loop {
519 let mut read_buffer = vec![0u8; 1024];
521 let (_len, addr) = socket.recv_from(&mut read_buffer).await?;
522 info!("== Received packet from {:?} ==", addr);
523
524 let decoded_message = Message::decode(&mut Decoder::new(&read_buffer))?;
525 if decoded_message.opcode() != Opcode::BootRequest {
528 error!("[ERROR] opcode is not BootRequest, ignoring message");
529 continue;
530 }
531
532 let options = decoded_message.opts();
533
534 if options.has_msg_type(MessageType::Discover) {
535 let transaction_id = decoded_message.xid();
536 let response = handle_discover(&config, &decoded_message).await;
537 if let Ok(response) = response {
538 info!("[{:X}] [OFFER] Sending...", transaction_id);
539 if let Err(e) = socket.send_to(&response, "255.255.255.255:68").await {
540 error!(
541 "[{:X}] [OFFER] Failed to send in socket: {:?}",
542 transaction_id, e
543 );
544 }
545 } else {
546 error!("[ERROR] handling DISCOVER {:?}", response);
547 }
548 continue;
549 }
550
551 if options.has_msg_type(MessageType::Request) {
552 let transaction_id = decoded_message.xid();
553 let response = handle_request(&config, &decoded_message).await;
554 if let Ok(response) = response {
555 info!("[{:X}] [ACK/NAK] Sending...", transaction_id);
556 if let Err(e) = socket.send_to(&response, "255.255.255.255:68").await {
557 error!(
558 "[{:X}] [ACK/NAK] Failed to send in socket: {:?}",
559 transaction_id, e
560 );
561 }
562 } else {
563 error!("[ERROR] handling REQUEST {:?}", response);
564 }
565 continue;
566 }
567
568 if options.has_msg_type(MessageType::Decline) {
569 let transaction_id = decoded_message.xid();
570 let requested_ip = decoded_message.opts().get(OptionCode::RequestedIpAddress);
571
572 if let Some(DhcpOption::RequestedIpAddress(ip)) = requested_ip {
573 info!(
574 "[{:X}] [DECLINE] Client declined IP {:?} (address conflict detected)",
575 transaction_id, ip
576 );
577 if let Err(e) = config.leases.mark_ip_declined(*ip).await {
578 error!(
579 "[{:X}] [DECLINE] Failed to mark IP as declined: {:?}",
580 transaction_id, e
581 );
582 } else {
583 info!(
584 "[{:X}] [DECLINE] IP {:?} marked as unavailable",
585 transaction_id, ip
586 );
587 }
588 } else {
589 warn!(
590 "[{:X}] [DECLINE] No requested IP in DECLINE message",
591 transaction_id
592 );
593 }
594 continue;
595 }
596
597 if options.has_msg_type(MessageType::Release) {
598 let transaction_id = decoded_message.xid();
599 let client_id = decoded_message.chaddr().to_vec();
600 let ciaddr = decoded_message.ciaddr();
601
602 info!(
603 "[{:X}] [RELEASE] Client releasing IP {:?}",
604 transaction_id, ciaddr
605 );
606 if let Err(e) = config.leases.release_lease(ciaddr, &client_id).await {
607 error!(
608 "[{:X}] [RELEASE] Failed to release lease: {:?}",
609 transaction_id, e
610 );
611 } else {
612 info!(
613 "[{:X}] [RELEASE] Lease for {:?} released",
614 transaction_id, ciaddr
615 );
616 }
617 continue;
618 }
619 if options.has_msg_type(MessageType::Inform) {
620 let transaction_id = decoded_message.xid();
621 info!("[{:X}] [INFORM]", transaction_id);
622 continue;
623 }
624 }
625}