mini_dhcp/
lib.rs

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    // The client's current address as recorded in the client's current
64    // binding
65    // The client's current address as recorded in the client's current
66    // binding, ELSE
67    //
68    // The client's previous address as recorded in the client's (now
69    // expired or released) binding, if that address is in the server's
70    // pool of available addresses and not already allocated, ELSE
71    let mut suggested_address = match leases.get_ip_from_client_id(&client_id).await {
72        Ok(address) => {
73            // Check if this lease has expired
74            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                        // Lease has expired, renew it
79                        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    // The address requested in the 'Requested IP Address' option, if that
117    // address is valid and not already allocated, ELSE
118    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    // A new address allocated from the server's pool of available addresses
138    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/// Response type for DHCP REQUEST handling
191#[derive(Debug)]
192enum DhcpResponse {
193    Ack(Message),
194    Nak(Message),
195}
196
197/// Build a DHCP NAK packet according to RFC 2131 Table 3
198fn 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    // RFC 2131 Table 3: yiaddr and siaddr must be 0 for NACK
209    nak.set_yiaddr(Ipv4Addr::new(0, 0, 0, 0));
210    nak.set_siaddr(Ipv4Addr::new(0, 0, 0, 0));
211
212    // Only include MessageType and ServerIdentifier options
213    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    // `ciaddr` is the client’s current IP address (used in RENEWING/REBINDING)
231    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        // SELECTING state
240        let selecting = have_server_id && have_requested_ip && ciaddr_is_zero;
241
242        // INIT-REBOOT
243        let init_reboot = !have_server_id && have_requested_ip && ciaddr_is_zero;
244
245        // RENEWING/REBINDING: ciaddr != 0, no 'requested IP address'
246        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    // 4) Validate that the IP is on the correct subnet (RFC says to NAK if it's on the wrong net).
275    //    Also check if you have a valid lease for this client in your DB, etc.
276
277    // First check if IP is in valid range
278    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            // No lease exists - check if IP is already assigned to someone else
289            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            // Lenient mode: accept the client's claimed IP if it's in range and available.
301            // This deviates from RFC 2131 which says to remain silent when we have no record,
302            // but provides better UX for single-server setups (e.g., after server restart).
303            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    // Validate the lease if it exists (skip if we just created it)
320    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        // Check if lease has expired
329        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        // Validate that the client_id matches the lease
342        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    // Update lease expiry time in database
377    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            // Send notification to event queue
490            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        // Receive a packet
520        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        // https://datatracker.ietf.org/doc/html/rfc2131#page-13
526        // The 'op' field of each DHCP message sent from a client to a server contains BOOTREQUEST.
527        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}