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
33async fn insert_lease(pool: &SqlitePool, ip: Ipv4Addr, client_id: &Vec<u8>) -> anyhow::Result<()> {
34    let ip = u32::from(ip);
35
36    let expire_at = Zoned::now()
37        .round(Unit::Second)?
38        .checked_add(1.hour())
39        .with_context(|| "Fuck".to_string())?
40        .timestamp()
41        .as_second();
42
43    sqlx::query_file!(
44        "./db/queries/insert-new-lease.sql",
45        ip,
46        client_id,
47        expire_at,
48        0
49    )
50    .execute(pool)
51    .await?;
52    Ok(())
53}
54
55async fn build_dhcp_offer_packet(
56    leases: &SqlitePool,
57    discover_message: &Message,
58) -> anyhow::Result<Message> {
59    let client_id = discover_message.chaddr().to_vec();
60
61    // The client's current address as recorded in the client's current
62    // binding
63    // The client's current address as recorded in the client's current
64    // binding, ELSE
65    //
66    // The client's previous address as recorded in the client's (now
67    // expired or released) binding, if that address is in the server's
68    // pool of available addresses and not already allocated, ELSE
69    let mut suggested_address = match db::get_ip_from_client_id(leases, &client_id).await {
70        Ok(address) => {
71            info!(
72                "[OFFER] Client {:?} already has IP assigned: {:?}",
73                client_id, address
74            );
75            Some(address)
76        }
77        _ => {
78            warn!(
79                "[OFFER] Client {:?} has no IP assigned in the database",
80                client_id
81            );
82            None
83        }
84    };
85
86    // The address requested in the 'Requested IP Address' option, if that
87    // address is valid and not already allocated, ELSE
88    if suggested_address.is_none() {
89        let requested_ip_address = discover_message.opts().get(OptionCode::RequestedIpAddress);
90        info!(
91            "[OFFER] Client requested IP address: {:?}",
92            requested_ip_address
93        );
94        match requested_ip_address {
95            Some(DhcpOption::RequestedIpAddress(ip)) => {
96                if !db::is_ip_assigned(leases, *ip).await? {
97                    suggested_address = Some(*ip);
98                }
99            }
100            _ => {
101                warn!("[OFFER] No requested IP address")
102            }
103        };
104    }
105
106    if suggested_address.is_none() {
107        let mut max_tries: u8 = 10;
108        loop {
109            let random_address = Ipv4Addr::new(192, 168, 1, rand::thread_rng().gen_range(100..200));
110
111            if !db::is_ip_assigned(leases, random_address).await? {
112                suggested_address = Some(random_address);
113                // insert the lease into the database
114                insert_lease(leases, random_address, &client_id).await?;
115                break;
116            }
117
118            if max_tries == 0 {
119                return Err(anyhow::anyhow!("Could not assign IP address"));
120            } else {
121                max_tries = max_tries.saturating_sub(1);
122            }
123        }
124    }
125
126    let suggested_address = match suggested_address {
127        Some(address) => address,
128        None => return Err(anyhow::anyhow!("Could not assign IP address")),
129    };
130
131    info!("[OFFER] creating offer with IP {}", suggested_address);
132
133    let mut offer = Message::default();
134
135    let reply_opcode = Opcode::BootReply;
136    offer.set_opcode(reply_opcode);
137    offer.set_xid(discover_message.xid());
138    offer.set_yiaddr(suggested_address);
139    offer.set_siaddr(Ipv4Addr::new(192, 168, 1, 69));
140    offer.set_flags(discover_message.flags());
141    offer.set_giaddr(discover_message.giaddr());
142    offer.set_chaddr(discover_message.chaddr());
143
144    offer
145        .opts_mut()
146        .insert(DhcpOption::MessageType(MessageType::Offer));
147    offer.opts_mut().insert(DhcpOption::AddressLeaseTime(3600));
148    offer
149        .opts_mut()
150        .insert(DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 69)));
151    offer
152        .opts_mut()
153        .insert(DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)));
154    offer
155        .opts_mut()
156        .insert(DhcpOption::BroadcastAddr(Ipv4Addr::new(255, 255, 255, 255)));
157    offer
158        .opts_mut()
159        .insert(DhcpOption::Router(vec![Ipv4Addr::new(192, 168, 1, 69)]));
160
161    Ok(offer)
162}
163
164async fn build_dhcp_ack_packet(
165    leases: &SqlitePool,
166    request_message: &Message,
167) -> anyhow::Result<Message> {
168    let server_identifier_option = request_message.opts().get(OptionCode::ServerIdentifier);
169
170    let requested_ip_option = request_message.opts().get(OptionCode::RequestedIpAddress);
171
172    // `ciaddr` is the client’s current IP address (used in RENEWING/REBINDING)
173    let ciaddr = request_message.ciaddr();
174    let chaddr = request_message.chaddr().to_owned();
175
176    let (is_selecting, is_init_reboot, is_renewing_rebinding) = {
177        let have_server_id = server_identifier_option.is_some();
178        let have_requested_ip = requested_ip_option.is_some();
179        let ciaddr_is_zero = ciaddr == Ipv4Addr::new(0, 0, 0, 0);
180
181        // SELECTING state
182        let selecting = have_server_id && have_requested_ip && ciaddr_is_zero;
183
184        // INIT-REBOOT
185        let init_reboot = !have_server_id && have_requested_ip && ciaddr_is_zero;
186
187        // RENEWING/REBINDING: ciaddr != 0, no 'requested IP address'
188        let renewing_rebinding = ciaddr != Ipv4Addr::new(0, 0, 0, 0) && !have_requested_ip;
189
190        (selecting, init_reboot, renewing_rebinding)
191    };
192
193    let ip_to_validate = if is_selecting || is_init_reboot {
194        match requested_ip_option {
195            Some(DhcpOption::RequestedIpAddress(ip)) => {
196                info!("[ACK] Client requested IP address: {:?}", ip);
197                ip
198            }
199            _ => {
200                anyhow::bail!("[ACK] Client didnt requested IP address")
201            }
202        }
203    } else if is_renewing_rebinding {
204        info!("[ACK] using ciaddr {:?}", ciaddr);
205        &ciaddr
206    } else {
207        anyhow::bail!("[ACK] DHCPREQUEST does not match any known valid state.");
208    };
209
210    // 4) Validate that the IP is on the correct subnet (RFC says to NAK if it’s on the wrong net).
211    //    Also check if you have a valid lease for this client in your DB, etc.
212    let lease = match db::get_lease_by_ip(leases, ip_to_validate).await {
213        Ok(lease) => lease,
214        Err(e) => {
215            anyhow::bail!("[ACK] NO RECORD FOUND ON DB {:?}", e);
216        }
217    };
218
219    if !lease.leased {
220        anyhow::bail!("[ACK] IP address is not leased");
221    }
222
223    let mut ack = Message::default();
224    ack.set_opcode(Opcode::BootReply);
225    ack.set_xid(request_message.xid());
226    ack.set_flags(request_message.flags());
227    ack.set_giaddr(request_message.giaddr());
228    ack.set_chaddr(&chaddr);
229
230    ack.set_yiaddr(*ip_to_validate);
231
232    ack.opts_mut()
233        .insert(DhcpOption::MessageType(MessageType::Ack));
234    ack.opts_mut()
235        .insert(DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 69)));
236
237    ack.opts_mut().insert(DhcpOption::AddressLeaseTime(3600));
238    ack.opts_mut()
239        .insert(DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)));
240    ack.opts_mut()
241        .insert(DhcpOption::BroadcastAddr(Ipv4Addr::new(255, 255, 255, 255)));
242    ack.opts_mut()
243        .insert(DhcpOption::Router(vec![Ipv4Addr::new(192, 168, 1, 69)]));
244
245    Ok(ack)
246}
247
248#[derive(Clone)]
249pub struct MiniDHCPConfiguration {
250    interface: String,
251    leases: SqlitePool,
252}
253
254impl MiniDHCPConfiguration {
255    pub async fn new(interface: String) -> anyhow::Result<Self> {
256        let conn = SqliteConnectOptions::from_str("sqlite://dhcp.db")?.create_if_missing(true);
257
258        let leases = SqlitePool::connect_with(conn).await?;
259
260        sqlx::migrate!("./db/migrations").run(&leases).await?;
261
262        Ok(Self { leases, interface })
263    }
264}
265
266async fn handle_discover(
267    config: &MiniDHCPConfiguration,
268    decoded_message: &Message,
269) -> anyhow::Result<Vec<u8>> {
270    let transaction_id = decoded_message.xid();
271    let client_address = decoded_message.chaddr();
272    info!("[{:X}] DISCOVER {:?}", transaction_id, client_address);
273    let offer = build_dhcp_offer_packet(&config.leases, decoded_message);
274
275    match offer.await {
276        Ok(offer) => {
277            let offered_ip = offer.yiaddr();
278            info!(
279                "[{:X}] [OFFER]: client {:?} ip {:?}",
280                transaction_id, client_address, offered_ip
281            );
282
283            let mut buf = Vec::new();
284            let mut e = Encoder::new(&mut buf);
285            offer.encode(&mut e)?;
286            Ok(buf)
287        }
288        Err(e) => {
289            anyhow::bail!("OFFER Error: {:?}", e)
290        }
291    }
292}
293
294async fn handle_request(
295    config: &MiniDHCPConfiguration,
296    decoded_message: &Message,
297) -> anyhow::Result<Vec<u8>> {
298    let options = decoded_message.opts();
299    let transaction_id = decoded_message.xid();
300    let client_address = decoded_message.chaddr();
301    let server_identifier = options.get(OptionCode::ServerIdentifier);
302    info!(
303        "[{:X}] REQUEST from {:?} to {:?}",
304        transaction_id, client_address, server_identifier
305    );
306
307    let ack = build_dhcp_ack_packet(&config.leases, decoded_message);
308
309    match ack.await {
310        Ok(ack) => {
311            let mut buf = Vec::new();
312            let mut e = Encoder::new(&mut buf);
313            ack.encode(&mut e)?;
314            let offered_ip = ack.yiaddr();
315            info!(
316                "[{:X}] [ACK]: {:?} {:?}",
317                transaction_id, client_address, offered_ip
318            );
319            Ok(buf)
320        }
321        Err(e) => {
322            anyhow::bail!("ACK Error: {:?}", e)
323        }
324    }
325}
326
327pub async fn start(config: MiniDHCPConfiguration) -> anyhow::Result<()> {
328    let address = "0.0.0.0:67";
329    info!("Starting DHCP listener [{}] {}", config.interface, address);
330    let socket = UdpSocket::bind(address).await?;
331    socket.set_broadcast(true)?;
332    socket.bind_device(Some(config.interface.as_bytes()))?;
333
334    let mut read_buffer = vec![0u8; 1024];
335
336    loop {
337        // Receive a packet
338        let (_len, addr) = socket.recv_from(&mut read_buffer).await?;
339        info!("== Received packet from {:?} ==", addr);
340
341        let decoded_message = Message::decode(&mut Decoder::new(&read_buffer))?;
342        // https://datatracker.ietf.org/doc/html/rfc2131#page-13
343        // The 'op' field of each DHCP message sent from a client to a server contains BOOTREQUEST.
344        if decoded_message.opcode() != Opcode::BootRequest {
345            error!("[ERROR] opcode is not BootRequest, ignoring message");
346            continue;
347        }
348
349        let options = decoded_message.opts();
350
351        if options.has_msg_type(MessageType::Discover) {
352            let transaction_id = decoded_message.xid();
353            let response = handle_discover(&config, &decoded_message).await;
354            if let Ok(response) = response {
355                info!("[{:X}] [OFFER] Sending...", transaction_id);
356                socket
357                    .send_to(&response, "255.255.255.255:68")
358                    .await
359                    .expect("[OFFER] Failed to send in socket");
360            } else {
361                error!("[ERROR] handling DISCOVER {:?}", response);
362            }
363            continue;
364        }
365
366        if options.has_msg_type(MessageType::Request) {
367            let transaction_id = decoded_message.xid();
368            let response = handle_request(&config, &decoded_message).await;
369            if let Ok(response) = response {
370                info!("[{:X}] [ACK] Sending...", transaction_id);
371                socket
372                    .send_to(&response, "255.255.255.255:68")
373                    .await
374                    .expect("[ACK] Failed to send in socket");
375            } else {
376                error!("[ERROR] handling REQUEST {:?}", response);
377            }
378            continue;
379        }
380
381        if options.has_msg_type(MessageType::Decline) {
382            let transaction_id = decoded_message.xid();
383            info!("[{:X}] [DECLINE]", transaction_id);
384            continue;
385        }
386
387        if options.has_msg_type(MessageType::Release) {
388            let transaction_id = decoded_message.xid();
389            info!("[{:X}] [RELEASE]", transaction_id);
390            continue;
391        }
392        if options.has_msg_type(MessageType::Inform) {
393            let transaction_id = decoded_message.xid();
394            info!("[{:X}] [INFORM]", transaction_id);
395            continue;
396        }
397    }
398}