// Copyright (c) 2022 Johannes Stoelp #include <dhcp.h> #include <lease_db.h> #include <utils.h> #include <ESP8266WiFi.h> #include <WiFiUdp.h> /// -- WIFI access configuration. static constexpr char STATION_SSID[] = "<SSID>"; static constexpr char STATION_WPA2[] = "<WPA2PW>"; /// -- WIFI client config. static const IPAddress LOCAL_IP(10, 0, 0, 2); static const IPAddress GATEWAY(10, 0, 0, 1); static const IPAddress BROADCAST(10, 0, 0, 255); static const IPAddress SUBNET(255, 255, 255, 0); static const IPAddress DNS1(192, 168, 2, 1); /// -- DHCP lease config. static const IPAddress LEASE_START(10, 0, 0, 10); static constexpr u32 LEASE_TIME_SECS = 8 * 60 * 60; /* 8h */ /// -- DHCP message buffer. alignas(dhcp_message) static u8 MSG_BUFFER[DHCP_MESSAGE_LEN]; static_assert(sizeof(dhcp_message) <= sizeof(MSG_BUFFER), "UDP buffer must be big enough to hold dhcp_message!"); /// -- Lease DB. static lease_db<16> LEASE_DB; /// -- UDP io handler. static WiFiUDP UDP; /// -- Template specialization. template<> inline IPAddress get_opt_val(const u8* opt_data) { return IPAddress(opt_data[0], opt_data[1], opt_data[2], opt_data[3]); } template<> inline u8* put_opt_val(u8* opt_data, IPAddress addr) { *opt_data++ = addr[0]; *opt_data++ = addr[1]; *opt_data++ = addr[2]; *opt_data++ = addr[3]; return opt_data; } #define LOG_UART(uart, fmt, ...) \ do { \ if (uart) { \ uart.printf(fmt "\r", ##__VA_ARGS__); \ } \ } while (0) #define LOG(fmt, ...) LOG_UART(Serial, fmt, ##__VA_ARGS__) static void setup_station_wifi() { // Configure wifi in station mode. WiFi.mode(WIFI_STA); // Configure static IP. WiFi.config(LOCAL_IP, GATEWAY, SUBNET, DNS1); // Connect to SSID. WiFi.begin(STATION_SSID, STATION_WPA2); // Wait until wifi is connected. Serial.printf("Connecting to SSID = %s\n\r", STATION_SSID); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(500); } Serial.printf("\n\rConnected to %s\n\r", STATION_SSID); Serial.print(" local_ip : "); Serial.println(WiFi.localIP()); Serial.print(" gateway : "); Serial.println(WiFi.gatewayIP()); Serial.print(" subnet : "); Serial.println(WiFi.subnetMask()); Serial.print(" dns1 : "); Serial.println(WiFi.dnsIP()); Serial.print(" broadcast: "); Serial.println(WiFi.broadcastIP()); } void setup() { // Initialize serial port for logging. Serial.begin(115200); // Connect as client to wifi using global configuration. setup_station_wifi(); // Start listening for udp messages. UDP.begin(DHCP_SERVER_PORT); pinMode(LED_BUILTIN, OUTPUT); } static void handle_dhcp_message(dhcp_message& msg, usize len); void loop() { const usize npbytes = UDP.parsePacket(); // Only handle UDP packets with valid size wrt dhcp messages. if (npbytes >= DHCP_MESSAGE_MIN_LEN && npbytes < sizeof(dhcp_message)) { const usize nrbytes = UDP.read(MSG_BUFFER, npbytes); // Type pun buffer to to dhcp_message (cpp). // Should be optimized out mainly due to alignment specification of buffer. dhcp_message msg; std::memcpy(&msg, MSG_BUFFER, sizeof(msg)); handle_dhcp_message(msg, nrbytes); } else { if (npbytes > 0) { LOG("Ignored UDP message of size %d bytes\n", npbytes); } delay(500); } } // Try to unwrap an optional, return of optional doesn't hold a value. #define TRY(expr) \ ({ \ auto optional = expr; \ if (!optional) \ return; \ optional.value(); \ }) // Get seconds since boot (absolute time value). // We use this to maintain lease expiration times. usize now_secs() { return millis() / 1000; } static void handle_dhcp_message(dhcp_message& msg, usize len) { // Sanity check dhcp message. if (msg.op != dhcp_operation::BOOTREQUEST || msg.cookie != DHCP_OPTION_COOKIE) { return; } // Length of the filled in client options. const usize opt_len = len - (msg.options - (const u8*)&msg); // Each dhcp message must contain the dhcp message type (state in the protocol). const auto msg_type = ({ auto opt = TRY(get_option(msg.options, opt_len, dhcp_option::DHCP_MESSAGE_TYPE)); from_raw<dhcp_message_type>(opt.data[0]); }); // Remove expired leases. LEASE_DB.flush_expired(now_secs()); // Compute client hash, using the CLIENT_ID option if available else use // the hardware address. u32 client_hash; if (const auto client_id = get_option(msg.options, opt_len, dhcp_option::CLIENT_ID)) { client_hash = hash(client_id->data, client_id->len); } else { client_hash = hash(msg.chaddr, msg.hlen); } // Extract the dhcp options requested by the client (using 16 was sufficient in my case). dhcp_option requested_param[16]; usize requested_param_len = 0; if (const auto opt = get_option(msg.options, opt_len, dhcp_option::PARAMETER_REQUEST_LIST)) { requested_param_len = opt->len > sizeof(requested_param) ? sizeof(requested_param) : opt->len; for (usize i = 0; i < requested_param_len; ++i) { requested_param[i] = from_raw<dhcp_option>(opt->data[i]); } } usize lease_id; dhcp_message_type resp_msg; switch (msg_type) { case dhcp_message_type::DHCP_DISCOVER: { LOG("Received DHCP_DISCOVER client_hash=%x\n", client_hash); if (const auto lease = LEASE_DB.get_lease(client_hash)) { // We already have a lease for this client. lease_id = lease.value(); } else { // Allocate a new lease for this client and reserve for a short // amount of time. lease_id = TRY(LEASE_DB.new_lease(client_hash, now_secs() + 15 /* secs */)); } // DHCP message type answer. resp_msg = dhcp_message_type::DHCP_OFFER; } break; case dhcp_message_type::DHCP_REQUEST: { LOG("Received DHCP_REQUEST client_hash=%x\n", client_hash); // Get server identifier specified by client. const auto server_id = ({ auto op = TRY(get_option(msg.options, opt_len, dhcp_option::SERVER_IDENTIFIER)); get_opt_val<IPAddress>(op.data); }); // Check if dhcp message was ment for us. if (server_id != LOCAL_IP) { return; } // Client is now requesting the offered lease, at that stage the // lease should have been allocated. lease_id = TRY(LEASE_DB.get_lease(client_hash)); // Update the lease db with the proper lease expiration time // (absolute time). LEASE_DB.update_lease(client_hash, now_secs() + LEASE_TIME_SECS /* secs */); // DHCP message type answer. resp_msg = dhcp_message_type::DHCP_ACK; } break; default: { LOG("Received unexpected DHCP MESSAGE TYPE %d\n", into_raw(msg_type)); return; } } // Craft response package. // Compute client id based on start address of dhcp range and lease idx. auto client_addr = LEASE_START; client_addr[3] = client_addr[3] + lease_id; // From rfc2131 Table 3: // // Field DHCPOFFER DHCPACK // ----- --------- ------- // 'op' BOOTREPLY BOOTREPLY // 'htype' keep keep // 'hlen' keep keep // 'hops' 0 0 // 'xid' keep keep // 'secs' 0 0 // 'ciaddr' 0 0 // 'yiaddr' IP address offered to client // 'siaddr' IP address of next bootstrap server // 'flags' keep keep // 'giaddr' keep keep // 'chaddr' keep keep // 'sname' Server host name or options // 'file' Client boot file name or options msg.op = dhcp_operation::BOOTREPLY; msg.hops = 0; msg.secs = 0; msg.ciaddr = 0; msg.yiaddr = client_addr.v4(); msg.siaddr = LOCAL_IP.v4(); // From rfc2131 Table 3: // // Option DHCPOFFER DHCPACK // ------ --------- ------- // IP address lease time MUST MUST (DHCPREQUEST) // DHCP message type DHCPOFFER DHCPACK // Server identifier MUST MUST u8* optp = msg.options; // DHCP message type. *optp++ = into_raw(dhcp_option::DHCP_MESSAGE_TYPE); *optp++ = 1 /* len */; *optp++ = into_raw(resp_msg); // Server identifier. *optp++ = into_raw(dhcp_option::SERVER_IDENTIFIER); *optp++ = 4 /* len */; optp = put_opt_val(optp, LOCAL_IP); // Lease time. *optp++ = into_raw(dhcp_option::IP_ADDRESS_LEASE_TIME); *optp++ = 4 /* len */; optp = put_opt_val(optp, LEASE_TIME_SECS); // Renewal time. *optp++ = into_raw(dhcp_option::RENEWAL_TIME_T1); *optp++ = 4 /* len */; optp = put_opt_val(optp, LEASE_TIME_SECS / 2); // Rebind time. *optp++ = into_raw(dhcp_option::REBINDING_TIME_T2); *optp++ = 4 /* len */; optp = put_opt_val(optp, LEASE_TIME_SECS * 8 / 12); // Add options requested by client that we support. for (usize i = 0; i < requested_param_len; ++i) { auto opt = requested_param[i]; switch (opt) { case dhcp_option::SUBNET_MASK: { // Subnet mask. *optp++ = into_raw(opt); *optp++ = 4 /* len */; optp = put_opt_val(optp, SUBNET); } break; case dhcp_option::ROUTER: { // Router address. *optp++ = into_raw(opt); *optp++ = 4 /* len */; optp = put_opt_val(optp, GATEWAY); } break; case dhcp_option::DNS: { // DNS address. *optp++ = into_raw(opt); *optp++ = 4 /* len */; optp = put_opt_val(optp, DNS1); } break; case dhcp_option::BROADCAST_ADDR: { // Broadcast address. *optp++ = into_raw(opt); *optp++ = 4 /* len */; optp = put_opt_val(optp, BROADCAST); } break; default: break; } } // End option end marker. *optp++ = into_raw(dhcp_option::END); // Send out dhcp message. UDP.beginPacket(BROADCAST, DHCP_CLIENT_PORT); UDP.write((const u8*)&msg, optp - (u8*)&msg); UDP.endPacket(); }