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 sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
9use std::net::Ipv4Addr;
10use std::str::FromStr;
11use tokio::net::UdpSocket;
12use tracing::{error, info, warn};
13mod db;
14pub mod info;
15
16#[allow(dead_code)]
17#[derive(Debug)]
18struct Lease {
19    ip: i64,
20    client_id: Vec<u8>,
21    leased: bool,
22    expires_at: i64,
23    network: i64,
24    probation: bool,
25}
26
27#[derive(Debug, Serialize)]
28pub struct Client {
29    pub ip: Ipv4Addr,
30    pub client_id: String,
31}
32
33fn check_ip_in_range(addr: Ipv4Addr) -> bool {
34    let [a, b, c, d] = addr.octets();
35    a == 192 && b == 168 && c == 1 && (100..200).contains(&d)
36}
37
38fn get_ip_in_range() -> Ipv4Addr {
39    let octet = rand::thread_rng().gen_range(100..200);
40    Ipv4Addr::new(192, 168, 1, octet)
41}
42
43async fn insert_lease(pool: &SqlitePool, ip: Ipv4Addr, client_id: &Vec<u8>) -> anyhow::Result<()> {
44    let ip = u32::from(ip);
45
46    let expire_at = Zoned::now()
47        .round(Unit::Second)?
48        .checked_add(1.hour())
49        .with_context(|| "Failed to calculate lease expiry time".to_string())?
50        .timestamp()
51        .as_second();
52
53    sqlx::query_file!(
54        "./db/queries/insert-new-lease.sql",
55        ip,
56        client_id,
57        expire_at,
58        0
59    )
60    .execute(pool)
61    .await?;
62    Ok(())
63}
64
65async fn build_dhcp_offer_packet(
66    leases: &SqlitePool,
67    discover_message: &Message,
68) -> anyhow::Result<Message> {
69    let client_id = discover_message.chaddr().to_vec();
70
71    // The client's current address as recorded in the client's current
72    // binding
73    // The client's current address as recorded in the client's current
74    // binding, ELSE
75    //
76    // The client's previous address as recorded in the client's (now
77    // expired or released) binding, if that address is in the server's
78    // pool of available addresses and not already allocated, ELSE
79    let mut suggested_address = match db::get_ip_from_client_id(leases, &client_id).await {
80        Ok(address) => {
81            // Check if this lease has expired
82            match db::get_lease_by_ip(leases, &address).await {
83                Ok(lease) => {
84                    let current_time = Zoned::now().round(Unit::Second)?.timestamp().as_second();
85                    if lease.expires_at < current_time {
86                        // Lease has expired, renew it
87                        let new_expiry = Zoned::now()
88                            .round(Unit::Second)?
89                            .checked_add(1.hour())
90                            .with_context(|| "Failed to calculate lease expiry time".to_string())?
91                            .timestamp()
92                            .as_second();
93                        db::update_lease_expiry(leases, address, new_expiry).await?;
94                        info!(
95                            "[OFFER] Client {:?} has expired lease for {:?}, renewed",
96                            client_id, address
97                        );
98                    } else {
99                        info!(
100                            "[OFFER] Client {:?} already has IP assigned: {:?}",
101                            client_id, address
102                        );
103                    }
104                    Some(address)
105                }
106                Err(_) => {
107                    warn!("[OFFER] Could not fetch lease details for {:?}", address);
108                    Some(address)
109                }
110            }
111        }
112        _ => {
113            warn!(
114                "[OFFER] Client {:?} has no IP assigned in the database",
115                client_id
116            );
117            None
118        }
119    };
120
121    // The address requested in the 'Requested IP Address' option, if that
122    // address is valid and not already allocated, ELSE
123    if suggested_address.is_none() {
124        let requested_ip_address = discover_message.opts().get(OptionCode::RequestedIpAddress);
125        info!(
126            "[OFFER] Client requested IP address: {:?}",
127            requested_ip_address
128        );
129        match requested_ip_address {
130            Some(DhcpOption::RequestedIpAddress(ip)) => {
131                if !db::is_ip_assigned(leases, *ip).await? && check_ip_in_range(*ip) {
132                    suggested_address = Some(*ip);
133                }
134            }
135            _ => {
136                warn!("[OFFER] No requested IP address")
137            }
138        };
139    }
140
141    if suggested_address.is_none() {
142        let mut max_tries: u8 = 10;
143        loop {
144            let random_address = get_ip_in_range();
145
146            if !db::is_ip_assigned(leases, random_address).await? {
147                suggested_address = Some(random_address);
148                // insert the lease into the database
149                insert_lease(leases, random_address, &client_id).await?;
150                break;
151            }
152
153            if max_tries == 0 {
154                return Err(anyhow::anyhow!("Could not assign IP address"));
155            } else {
156                max_tries = max_tries.saturating_sub(1);
157            }
158        }
159    }
160
161    let suggested_address = match suggested_address {
162        Some(address) => address,
163        None => return Err(anyhow::anyhow!("Could not assign IP address")),
164    };
165
166    info!("[OFFER] creating offer with IP {}", suggested_address);
167
168    let mut offer = Message::default();
169
170    let reply_opcode = Opcode::BootReply;
171    offer.set_opcode(reply_opcode);
172    offer.set_xid(discover_message.xid());
173    offer.set_yiaddr(suggested_address);
174    offer.set_siaddr(Ipv4Addr::new(192, 168, 1, 69));
175    offer.set_flags(discover_message.flags());
176    offer.set_giaddr(discover_message.giaddr());
177    offer.set_chaddr(discover_message.chaddr());
178
179    offer
180        .opts_mut()
181        .insert(DhcpOption::MessageType(MessageType::Offer));
182    offer.opts_mut().insert(DhcpOption::AddressLeaseTime(3600));
183    offer
184        .opts_mut()
185        .insert(DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 69)));
186    offer
187        .opts_mut()
188        .insert(DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)));
189    offer
190        .opts_mut()
191        .insert(DhcpOption::BroadcastAddr(Ipv4Addr::new(255, 255, 255, 255)));
192    offer
193        .opts_mut()
194        .insert(DhcpOption::Router(vec![Ipv4Addr::new(192, 168, 1, 69)]));
195
196    Ok(offer)
197}
198
199/// Response type for DHCP REQUEST handling
200#[derive(Debug)]
201enum DhcpResponse {
202    Ack(Message),
203    Nak(Message),
204}
205
206/// Build a DHCP NAK packet according to RFC 2131 Table 3
207fn build_dhcp_nack_packet(request_message: &Message, reason: &str) -> Message {
208    info!("[NAK] Sending NACK: {}", reason);
209
210    let mut nak = Message::default();
211    nak.set_opcode(Opcode::BootReply);
212    nak.set_xid(request_message.xid());
213    nak.set_flags(request_message.flags());
214    nak.set_chaddr(request_message.chaddr());
215
216    // RFC 2131 Table 3: yiaddr and siaddr must be 0 for NACK
217    nak.set_yiaddr(Ipv4Addr::new(0, 0, 0, 0));
218    nak.set_siaddr(Ipv4Addr::new(0, 0, 0, 0));
219
220    // Only include MessageType and ServerIdentifier options
221    nak.opts_mut()
222        .insert(DhcpOption::MessageType(MessageType::Nak));
223    nak.opts_mut()
224        .insert(DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 69)));
225
226    nak
227}
228
229async fn build_dhcp_ack_packet(
230    leases: &SqlitePool,
231    request_message: &Message,
232) -> anyhow::Result<DhcpResponse> {
233    let server_identifier_option = request_message.opts().get(OptionCode::ServerIdentifier);
234
235    let requested_ip_option = request_message.opts().get(OptionCode::RequestedIpAddress);
236
237    // `ciaddr` is the client’s current IP address (used in RENEWING/REBINDING)
238    let ciaddr = request_message.ciaddr();
239    let chaddr = request_message.chaddr().to_owned();
240
241    let (is_selecting, is_init_reboot, is_renewing_rebinding) = {
242        let have_server_id = server_identifier_option.is_some();
243        let have_requested_ip = requested_ip_option.is_some();
244        let ciaddr_is_zero = ciaddr == Ipv4Addr::new(0, 0, 0, 0);
245
246        // SELECTING state
247        let selecting = have_server_id && have_requested_ip && ciaddr_is_zero;
248
249        // INIT-REBOOT
250        let init_reboot = !have_server_id && have_requested_ip && ciaddr_is_zero;
251
252        // RENEWING/REBINDING: ciaddr != 0, no 'requested IP address'
253        let renewing_rebinding = ciaddr != Ipv4Addr::new(0, 0, 0, 0) && !have_requested_ip;
254
255        (selecting, init_reboot, renewing_rebinding)
256    };
257
258    let ip_to_validate = if is_selecting || is_init_reboot {
259        match requested_ip_option {
260            Some(DhcpOption::RequestedIpAddress(ip)) => {
261                info!("[ACK] Client requested IP address: {:?}", ip);
262                ip
263            }
264            _ => {
265                return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
266                    request_message,
267                    "Client didn't provide requested IP address",
268                )));
269            }
270        }
271    } else if is_renewing_rebinding {
272        info!("[ACK] using ciaddr {:?}", ciaddr);
273        &ciaddr
274    } else {
275        return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
276            request_message,
277            "DHCPREQUEST does not match any known valid state",
278        )));
279    };
280
281    // 4) Validate that the IP is on the correct subnet (RFC says to NAK if it's on the wrong net).
282    //    Also check if you have a valid lease for this client in your DB, etc.
283
284    // First check if IP is in valid range
285    if !check_ip_in_range(*ip_to_validate) {
286        return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
287            request_message,
288            "Requested IP address is outside valid range",
289        )));
290    }
291
292    let lease = match db::get_lease_by_ip(leases, ip_to_validate).await {
293        Ok(lease) => Some(lease),
294        Err(sqlx::Error::RowNotFound) => {
295            // No lease exists - for INIT-REBOOT state, create one if IP is available
296            if is_init_reboot {
297                // Check if IP is already assigned to someone else
298                if db::is_ip_assigned(leases, *ip_to_validate).await? {
299                    warn!("[ACK] IP {:?} is assigned to another client", ip_to_validate);
300                    return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
301                        request_message,
302                        "IP address is assigned to another client",
303                    )));
304                }
305
306                // IP is available, create a lease for this client
307                info!("[ACK] Creating new lease for {:?} on INIT-REBOOT request", ip_to_validate);
308                insert_lease(leases, *ip_to_validate, &chaddr).await?;
309                None // Will create ACK below
310            } else {
311                warn!("[ACK] NO RECORD FOUND ON DB for non-INIT-REBOOT state");
312                return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
313                    request_message,
314                    "No lease record found in database",
315                )));
316            }
317        }
318        Err(e) => {
319            warn!("[ACK] Database error: {:?}", e);
320            return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
321                request_message,
322                "Database error",
323            )));
324        }
325    };
326
327    // Validate the lease if it exists (skip if we just created it)
328    if let Some(lease) = &lease {
329        if !lease.leased {
330            return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
331                request_message,
332                "IP address is not currently leased",
333            )));
334        }
335
336        // Check if lease has expired
337        let current_time = Zoned::now().round(Unit::Second)?.timestamp().as_second();
338        if lease.expires_at < current_time {
339            warn!("[ACK] Lease has expired: expires_at={}, current={}", lease.expires_at, current_time);
340            return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
341                request_message,
342                "Lease has expired",
343            )));
344        }
345
346        // Validate that the client_id matches the lease
347        if lease.client_id != chaddr {
348            warn!("[ACK] Client ID mismatch: lease has {:?}, request has {:?}", lease.client_id, chaddr);
349            return Ok(DhcpResponse::Nak(build_dhcp_nack_packet(
350                request_message,
351                "IP address is leased to a different client",
352            )));
353        }
354    }
355
356    let mut ack = Message::default();
357    ack.set_opcode(Opcode::BootReply);
358    ack.set_xid(request_message.xid());
359    ack.set_flags(request_message.flags());
360    ack.set_giaddr(request_message.giaddr());
361    ack.set_chaddr(&chaddr);
362
363    ack.set_yiaddr(*ip_to_validate);
364
365    ack.opts_mut()
366        .insert(DhcpOption::MessageType(MessageType::Ack));
367    ack.opts_mut()
368        .insert(DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 69)));
369
370    ack.opts_mut().insert(DhcpOption::AddressLeaseTime(3600));
371    ack.opts_mut()
372        .insert(DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)));
373    ack.opts_mut()
374        .insert(DhcpOption::BroadcastAddr(Ipv4Addr::new(255, 255, 255, 255)));
375    ack.opts_mut()
376        .insert(DhcpOption::Router(vec![Ipv4Addr::new(192, 168, 1, 69)]));
377
378    // Update lease expiry time in database
379    let new_expiry = Zoned::now()
380        .round(Unit::Second)?
381        .checked_add(1.hour())
382        .with_context(|| "Failed to calculate new lease expiry time".to_string())?
383        .timestamp()
384        .as_second();
385
386    db::update_lease_expiry(leases, *ip_to_validate, new_expiry).await?;
387
388    Ok(DhcpResponse::Ack(ack))
389}
390
391#[derive(Clone)]
392pub struct MiniDHCPConfiguration {
393    interface: String,
394    leases: SqlitePool,
395}
396
397impl MiniDHCPConfiguration {
398    pub async fn new(interface: String) -> anyhow::Result<Self> {
399        let conn = SqliteConnectOptions::from_str("sqlite://dhcp.db")?.create_if_missing(true);
400
401        let leases = SqlitePool::connect_with(conn).await?;
402
403        sqlx::migrate!("./db/migrations").run(&leases).await?;
404
405        Ok(Self { leases, interface })
406    }
407}
408
409async fn handle_discover(
410    config: &MiniDHCPConfiguration,
411    decoded_message: &Message,
412) -> anyhow::Result<Vec<u8>> {
413    let transaction_id = decoded_message.xid();
414    let client_address = decoded_message.chaddr();
415    info!("[{:X}] DISCOVER {:?}", transaction_id, client_address);
416    let offer = build_dhcp_offer_packet(&config.leases, decoded_message);
417
418    match offer.await {
419        Ok(offer) => {
420            let offered_ip = offer.yiaddr();
421            info!(
422                "[{:X}] [OFFER]: client {:?} ip {:?}",
423                transaction_id, client_address, offered_ip
424            );
425
426            let mut buf = Vec::new();
427            let mut e = Encoder::new(&mut buf);
428            offer.encode(&mut e)?;
429            Ok(buf)
430        }
431        Err(e) => {
432            anyhow::bail!("OFFER Error: {:?}", e)
433        }
434    }
435}
436
437async fn handle_request(
438    config: &MiniDHCPConfiguration,
439    decoded_message: &Message,
440) -> anyhow::Result<Vec<u8>> {
441    let options = decoded_message.opts();
442    let transaction_id = decoded_message.xid();
443    let client_address = decoded_message.chaddr();
444    let server_identifier = options.get(OptionCode::ServerIdentifier);
445    info!(
446        "[{:X}] REQUEST from {:?} to {:?}",
447        transaction_id, client_address, server_identifier
448    );
449
450    let response = build_dhcp_ack_packet(&config.leases, decoded_message).await?;
451
452    match response {
453        DhcpResponse::Ack(ack) => {
454            let offered_ip = ack.yiaddr();
455            info!(
456                "[{:X}] [ACK]: {:?} {:?}",
457                transaction_id, client_address, offered_ip
458            );
459
460            let mut buf = Vec::new();
461            let mut e = Encoder::new(&mut buf);
462            ack.encode(&mut e)?;
463            Ok(buf)
464        }
465        DhcpResponse::Nak(nak) => {
466            info!("[{:X}] [NAK]: {:?}", transaction_id, client_address);
467
468            let mut buf = Vec::new();
469            let mut e = Encoder::new(&mut buf);
470            nak.encode(&mut e)?;
471            Ok(buf)
472        }
473    }
474}
475
476pub async fn start(config: MiniDHCPConfiguration) -> anyhow::Result<()> {
477    let address = "0.0.0.0:67";
478    info!("Starting DHCP listener [{}] {}", config.interface, address);
479    let socket = UdpSocket::bind(address).await?;
480    socket.set_broadcast(true)?;
481    socket.bind_device(Some(config.interface.as_bytes()))?;
482
483    loop {
484        // Receive a packet
485        let mut read_buffer = vec![0u8; 1024];
486        let (_len, addr) = socket.recv_from(&mut read_buffer).await?;
487        info!("== Received packet from {:?} ==", addr);
488
489        let decoded_message = Message::decode(&mut Decoder::new(&read_buffer))?;
490        // https://datatracker.ietf.org/doc/html/rfc2131#page-13
491        // The 'op' field of each DHCP message sent from a client to a server contains BOOTREQUEST.
492        if decoded_message.opcode() != Opcode::BootRequest {
493            error!("[ERROR] opcode is not BootRequest, ignoring message");
494            continue;
495        }
496
497        let options = decoded_message.opts();
498
499        if options.has_msg_type(MessageType::Discover) {
500            let transaction_id = decoded_message.xid();
501            let response = handle_discover(&config, &decoded_message).await;
502            if let Ok(response) = response {
503                info!("[{:X}] [OFFER] Sending...", transaction_id);
504                if let Err(e) = socket
505                    .send_to(&response, "255.255.255.255:68")
506                    .await
507                {
508                    error!("[{:X}] [OFFER] Failed to send in socket: {:?}", transaction_id, e);
509                }
510            } else {
511                error!("[ERROR] handling DISCOVER {:?}", response);
512            }
513            continue;
514        }
515
516        if options.has_msg_type(MessageType::Request) {
517            let transaction_id = decoded_message.xid();
518            let response = handle_request(&config, &decoded_message).await;
519            if let Ok(response) = response {
520                info!("[{:X}] [ACK/NAK] Sending...", transaction_id);
521                if let Err(e) = socket
522                    .send_to(&response, "255.255.255.255:68")
523                    .await
524                {
525                    error!("[{:X}] [ACK/NAK] Failed to send in socket: {:?}", transaction_id, e);
526                }
527            } else {
528                error!("[ERROR] handling REQUEST {:?}", response);
529            }
530            continue;
531        }
532
533        if options.has_msg_type(MessageType::Decline) {
534            let transaction_id = decoded_message.xid();
535            let requested_ip = decoded_message.opts().get(OptionCode::RequestedIpAddress);
536
537            if let Some(DhcpOption::RequestedIpAddress(ip)) = requested_ip {
538                info!("[{:X}] [DECLINE] Client declined IP {:?} (address conflict detected)", transaction_id, ip);
539                if let Err(e) = db::mark_ip_declined(&config.leases, *ip).await {
540                    error!("[{:X}] [DECLINE] Failed to mark IP as declined: {:?}", transaction_id, e);
541                } else {
542                    info!("[{:X}] [DECLINE] IP {:?} marked as unavailable", transaction_id, ip);
543                }
544            } else {
545                warn!("[{:X}] [DECLINE] No requested IP in DECLINE message", transaction_id);
546            }
547            continue;
548        }
549
550        if options.has_msg_type(MessageType::Release) {
551            let transaction_id = decoded_message.xid();
552            let client_id = decoded_message.chaddr().to_vec();
553            let ciaddr = decoded_message.ciaddr();
554
555            info!("[{:X}] [RELEASE] Client releasing IP {:?}", transaction_id, ciaddr);
556            if let Err(e) = db::release_lease(&config.leases, ciaddr, &client_id).await {
557                error!("[{:X}] [RELEASE] Failed to release lease: {:?}", transaction_id, e);
558            } else {
559                info!("[{:X}] [RELEASE] Lease for {:?} released", transaction_id, ciaddr);
560            }
561            continue;
562        }
563        if options.has_msg_type(MessageType::Inform) {
564            let transaction_id = decoded_message.xid();
565            info!("[{:X}] [INFORM]", transaction_id);
566            continue;
567        }
568    }
569}