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