Skip to content

Commit 4a6d3de

Browse files
committed
Add wifi init process
1 parent 65fd810 commit 4a6d3de

8 files changed

Lines changed: 1732 additions & 76 deletions

File tree

access-controller/Cargo.lock

Lines changed: 418 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

access-controller/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ embedded-storage = { version = "0.3", optional = true }
3333
embassy-executor = { version = "0.9", optional = true }
3434
embassy-time = { version = "0.5", optional = true }
3535
embassy-sync = { version = "0.7", optional = true }
36-
embassy-net = { version = "0.8", features = ["dhcpv4", "tcp", "medium-ethernet", "proto-ipv4", "dns"], optional = true }
36+
embassy-net = { version = "0.8", features = ["dhcpv4", "tcp", "udp", "medium-ethernet", "proto-ipv4", "dns"], optional = true }
3737
embassy-futures = { version = "0.1", optional = true }
3838

39-
smoltcp = { version = "0.12", default-features = false, features = ["medium-ethernet", "proto-ipv4", "proto-dhcpv4", "socket-tcp", "socket-dhcpv4"], optional = true }
39+
smoltcp = { version = "0.12", default-features = false, features = ["medium-ethernet", "proto-ipv4", "proto-dhcpv4", "socket-tcp", "socket-udp", "socket-dhcpv4"], optional = true }
4040
embedded-io-async = { version = "0.7", optional = true }
4141
static_cell = { version = "2", optional = true }
4242

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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

Comments
 (0)