|
| 1 | +//! Minimal DHCPv4 server for AP onboarding mode. |
| 2 | +//! |
| 3 | +//! Hands out a single IPv4 lease from a fixed pool to whichever client |
| 4 | +//! probes us first. Just enough RFC-2131 to keep iOS/Android/Windows |
| 5 | +//! happy when they associate with the onboarding SSID: |
| 6 | +//! |
| 7 | +//! - DISCOVER → OFFER (ip = pool[round_robin]) |
| 8 | +//! - REQUEST → ACK (or NAK if requested address is wrong) |
| 9 | +//! - DECLINE / RELEASE / INFORM are logged and ignored. |
| 10 | +//! |
| 11 | +//! We intentionally do not track leases beyond the latest client; the |
| 12 | +//! AP is online for a few minutes during initial setup and the next |
| 13 | +//! client just gets the next slot in the pool. |
| 14 | +
|
| 15 | +use embassy_net::udp::{PacketMetadata, UdpSocket}; |
| 16 | +use embassy_net::{IpEndpoint, IpListenEndpoint, Stack}; |
| 17 | + |
| 18 | +/// AP-side static IP, also the DHCP server identifier + gateway + DNS. |
| 19 | +pub const AP_IP: [u8; 4] = [192, 168, 4, 1]; |
| 20 | +const NETMASK: [u8; 4] = [255, 255, 255, 0]; |
| 21 | +const LEASE_SECS: u32 = 3600; |
| 22 | + |
| 23 | +const SERVER_PORT: u16 = 67; |
| 24 | +const CLIENT_PORT: u16 = 68; |
| 25 | + |
| 26 | +/// Pool of addresses we will hand out (round-robin). |
| 27 | +const POOL: [[u8; 4]; 8] = [ |
| 28 | + [192, 168, 4, 100], |
| 29 | + [192, 168, 4, 101], |
| 30 | + [192, 168, 4, 102], |
| 31 | + [192, 168, 4, 103], |
| 32 | + [192, 168, 4, 104], |
| 33 | + [192, 168, 4, 105], |
| 34 | + [192, 168, 4, 106], |
| 35 | + [192, 168, 4, 107], |
| 36 | +]; |
| 37 | + |
| 38 | +// DHCP magic cookie. |
| 39 | +const MAGIC: [u8; 4] = [0x63, 0x82, 0x53, 0x63]; |
| 40 | + |
| 41 | +// DHCP message types (option 53 value). |
| 42 | +const DHCP_DISCOVER: u8 = 1; |
| 43 | +const DHCP_OFFER: u8 = 2; |
| 44 | +const DHCP_REQUEST: u8 = 3; |
| 45 | +const DHCP_DECLINE: u8 = 4; |
| 46 | +const DHCP_ACK: u8 = 5; |
| 47 | +const DHCP_NAK: u8 = 6; |
| 48 | + |
| 49 | +#[embassy_executor::task] |
| 50 | +pub async fn dhcp_server_task(stack: &'static Stack<'static>) { |
| 51 | + // Wait for the stack to come up with our static IP. |
| 52 | + loop { |
| 53 | + if stack.config_v4().is_some() { |
| 54 | + break; |
| 55 | + } |
| 56 | + embassy_time::Timer::after(embassy_time::Duration::from_millis(200)).await; |
| 57 | + } |
| 58 | + log::info!("dhcp: server starting on {:?}", AP_IP); |
| 59 | + |
| 60 | + let mut rx_meta = [PacketMetadata::EMPTY; 4]; |
| 61 | + let mut tx_meta = [PacketMetadata::EMPTY; 4]; |
| 62 | + let mut rx_buf = [0u8; 1024]; |
| 63 | + let mut tx_buf = [0u8; 1024]; |
| 64 | + |
| 65 | + let mut socket = UdpSocket::new( |
| 66 | + *stack, |
| 67 | + &mut rx_meta, |
| 68 | + &mut rx_buf, |
| 69 | + &mut tx_meta, |
| 70 | + &mut tx_buf, |
| 71 | + ); |
| 72 | + |
| 73 | + if let Err(e) = socket.bind(IpListenEndpoint { |
| 74 | + addr: None, |
| 75 | + port: SERVER_PORT, |
| 76 | + }) { |
| 77 | + log::error!("dhcp: bind failed: {:?}", e); |
| 78 | + return; |
| 79 | + } |
| 80 | + |
| 81 | + let mut next_idx: usize = 0; |
| 82 | + let mut buf = [0u8; 1024]; |
| 83 | + |
| 84 | + loop { |
| 85 | + let (n, _meta) = match socket.recv_from(&mut buf).await { |
| 86 | + Ok(v) => v, |
| 87 | + Err(e) => { |
| 88 | + log::warn!("dhcp: recv error: {:?}", e); |
| 89 | + continue; |
| 90 | + } |
| 91 | + }; |
| 92 | + if n < 240 { |
| 93 | + continue; |
| 94 | + } |
| 95 | + let pkt = &buf[..n]; |
| 96 | + |
| 97 | + // RFC 2131 BOOTP layout: op(1)=1 for request. |
| 98 | + if pkt[0] != 1 { |
| 99 | + continue; |
| 100 | + } |
| 101 | + let xid = [pkt[4], pkt[5], pkt[6], pkt[7]]; |
| 102 | + let chaddr: [u8; 16] = pkt[28..44].try_into().unwrap(); |
| 103 | + |
| 104 | + // Validate magic cookie. |
| 105 | + if pkt[236..240] != MAGIC { |
| 106 | + continue; |
| 107 | + } |
| 108 | + |
| 109 | + // Parse options to find the message type and the requested IP. |
| 110 | + let opts = &pkt[240..]; |
| 111 | + let mut msg_type: u8 = 0; |
| 112 | + let mut requested_ip: Option<[u8; 4]> = None; |
| 113 | + let mut server_id: Option<[u8; 4]> = None; |
| 114 | + let mut i = 0; |
| 115 | + while i < opts.len() { |
| 116 | + let code = opts[i]; |
| 117 | + if code == 0xFF { |
| 118 | + break; |
| 119 | + } |
| 120 | + if code == 0 { |
| 121 | + i += 1; |
| 122 | + continue; |
| 123 | + } |
| 124 | + if i + 1 >= opts.len() { |
| 125 | + break; |
| 126 | + } |
| 127 | + let len = opts[i + 1] as usize; |
| 128 | + let val_start = i + 2; |
| 129 | + let val_end = val_start + len; |
| 130 | + if val_end > opts.len() { |
| 131 | + break; |
| 132 | + } |
| 133 | + let val = &opts[val_start..val_end]; |
| 134 | + match code { |
| 135 | + 53 if len == 1 => msg_type = val[0], |
| 136 | + 50 if len == 4 => requested_ip = Some([val[0], val[1], val[2], val[3]]), |
| 137 | + 54 if len == 4 => server_id = Some([val[0], val[1], val[2], val[3]]), |
| 138 | + _ => {} |
| 139 | + } |
| 140 | + i = val_end; |
| 141 | + } |
| 142 | + |
| 143 | + match msg_type { |
| 144 | + DHCP_DISCOVER => { |
| 145 | + let offer = POOL[next_idx % POOL.len()]; |
| 146 | + next_idx = next_idx.wrapping_add(1); |
| 147 | + log::info!("dhcp: DISCOVER -> OFFER {:?}", offer); |
| 148 | + let reply = build_reply(xid, chaddr, offer, DHCP_OFFER); |
| 149 | + send_broadcast(&mut socket, &reply).await; |
| 150 | + } |
| 151 | + DHCP_REQUEST => { |
| 152 | + let want = requested_ip.unwrap_or_else(|| { |
| 153 | + // If no Requested-IP option, fall back to ciaddr. |
| 154 | + [pkt[12], pkt[13], pkt[14], pkt[15]] |
| 155 | + }); |
| 156 | + let in_pool = POOL.iter().any(|p| *p == want); |
| 157 | + let bad_server = matches!(server_id, Some(sid) if sid != AP_IP); |
| 158 | + if !in_pool || bad_server { |
| 159 | + log::info!("dhcp: REQUEST {:?} -> NAK", want); |
| 160 | + let reply = build_reply(xid, chaddr, [0, 0, 0, 0], DHCP_NAK); |
| 161 | + send_broadcast(&mut socket, &reply).await; |
| 162 | + } else { |
| 163 | + log::info!("dhcp: REQUEST {:?} -> ACK", want); |
| 164 | + let reply = build_reply(xid, chaddr, want, DHCP_ACK); |
| 165 | + send_broadcast(&mut socket, &reply).await; |
| 166 | + } |
| 167 | + } |
| 168 | + DHCP_DECLINE => { |
| 169 | + log::warn!("dhcp: DECLINE from client"); |
| 170 | + } |
| 171 | + other => { |
| 172 | + log::debug!("dhcp: ignoring message type {}", other); |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | +} |
| 177 | + |
| 178 | +async fn send_broadcast(socket: &mut UdpSocket<'_>, pkt: &[u8]) { |
| 179 | + let dst = IpEndpoint::new( |
| 180 | + smoltcp::wire::IpAddress::Ipv4(smoltcp::wire::Ipv4Address::new(255, 255, 255, 255)), |
| 181 | + CLIENT_PORT, |
| 182 | + ); |
| 183 | + if let Err(e) = socket.send_to(pkt, dst).await { |
| 184 | + log::warn!("dhcp: send error: {:?}", e); |
| 185 | + } |
| 186 | +} |
| 187 | + |
| 188 | +/// Build a minimal BOOTP/DHCP reply (300 bytes, no padding beyond the |
| 189 | +/// option block). |
| 190 | +fn build_reply(xid: [u8; 4], chaddr: [u8; 16], yiaddr: [u8; 4], msg_type: u8) -> [u8; 300] { |
| 191 | + let mut p = [0u8; 300]; |
| 192 | + |
| 193 | + p[0] = 2; // BOOTREPLY |
| 194 | + p[1] = 1; // ethernet |
| 195 | + p[2] = 6; // hw addr len |
| 196 | + p[3] = 0; // hops |
| 197 | + p[4..8].copy_from_slice(&xid); |
| 198 | + // secs(8..10), flags(10..12) = 0 |
| 199 | + // ciaddr(12..16) = 0 |
| 200 | + p[16..20].copy_from_slice(&yiaddr); // yiaddr |
| 201 | + // siaddr(20..24) = AP itself (helpful for some clients) |
| 202 | + p[20..24].copy_from_slice(&AP_IP); |
| 203 | + // giaddr(24..28) = 0 |
| 204 | + p[28..44].copy_from_slice(&chaddr); |
| 205 | + // sname(44..108), file(108..236) = 0 |
| 206 | + p[236..240].copy_from_slice(&MAGIC); |
| 207 | + |
| 208 | + // Options |
| 209 | + let mut i = 240; |
| 210 | + // 53: message type |
| 211 | + p[i] = 53; |
| 212 | + p[i + 1] = 1; |
| 213 | + p[i + 2] = msg_type; |
| 214 | + i += 3; |
| 215 | + // 54: server identifier |
| 216 | + p[i] = 54; |
| 217 | + p[i + 1] = 4; |
| 218 | + p[i + 2..i + 6].copy_from_slice(&AP_IP); |
| 219 | + i += 6; |
| 220 | + if msg_type == DHCP_OFFER || msg_type == DHCP_ACK { |
| 221 | + // 51: lease time |
| 222 | + p[i] = 51; |
| 223 | + p[i + 1] = 4; |
| 224 | + p[i + 2..i + 6].copy_from_slice(&LEASE_SECS.to_be_bytes()); |
| 225 | + i += 6; |
| 226 | + // 1: subnet mask |
| 227 | + p[i] = 1; |
| 228 | + p[i + 1] = 4; |
| 229 | + p[i + 2..i + 6].copy_from_slice(&NETMASK); |
| 230 | + i += 6; |
| 231 | + // 3: router |
| 232 | + p[i] = 3; |
| 233 | + p[i + 1] = 4; |
| 234 | + p[i + 2..i + 6].copy_from_slice(&AP_IP); |
| 235 | + i += 6; |
| 236 | + // 6: DNS server (us) |
| 237 | + p[i] = 6; |
| 238 | + p[i + 1] = 4; |
| 239 | + p[i + 2..i + 6].copy_from_slice(&AP_IP); |
| 240 | + i += 6; |
| 241 | + } |
| 242 | + // 255: end |
| 243 | + p[i] = 0xFF; |
| 244 | + |
| 245 | + p |
| 246 | +} |
0 commit comments