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 let mut suggested_address = match db::get_ip_from_client_id(leases, &client_id).await {
80 Ok(address) => {
81 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 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 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_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#[derive(Debug)]
201enum DhcpResponse {
202 Ack(Message),
203 Nak(Message),
204}
205
206fn 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 nak.set_yiaddr(Ipv4Addr::new(0, 0, 0, 0));
218 nak.set_siaddr(Ipv4Addr::new(0, 0, 0, 0));
219
220 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 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 let selecting = have_server_id && have_requested_ip && ciaddr_is_zero;
248
249 let init_reboot = !have_server_id && have_requested_ip && ciaddr_is_zero;
251
252 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 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 if is_init_reboot {
297 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 info!("[ACK] Creating new lease for {:?} on INIT-REBOOT request", ip_to_validate);
308 insert_lease(leases, *ip_to_validate, &chaddr).await?;
309 None } 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 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 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 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 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 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 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}