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 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 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_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#[derive(Debug)]
176enum DhcpResponse {
177 Ack(Message),
178 Nak(Message),
179}
180
181fn 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 nak.set_yiaddr(Ipv4Addr::new(0, 0, 0, 0));
193 nak.set_siaddr(Ipv4Addr::new(0, 0, 0, 0));
194
195 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 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 let selecting = have_server_id && have_requested_ip && ciaddr_is_zero;
223
224 let init_reboot = !have_server_id && have_requested_ip && ciaddr_is_zero;
226
227 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 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 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 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 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 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 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}