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