diff options
author | Johannes Stoelp <johannes.stoelp@gmail.com> | 2022-01-14 23:51:05 +0100 |
---|---|---|
committer | Johannes Stoelp <johannes.stoelp@gmail.com> | 2022-01-14 23:51:05 +0100 |
commit | f9928a1a08c57fe853888119a996c3acc98ee09d (patch) | |
tree | c9770b76ffcc281da141f3aa2c595600372c0fca /src | |
download | pio-nodemcuv2-dhcp-server-f9928a1a08c57fe853888119a996c3acc98ee09d.tar.gz pio-nodemcuv2-dhcp-server-f9928a1a08c57fe853888119a996c3acc98ee09d.zip |
Able to offer IP address + DNS/Gateway ...
Worked with devices at my hand.
Diffstat (limited to 'src')
-rw-r--r-- | src/main.cc | 349 |
1 files changed, 349 insertions, 0 deletions
diff --git a/src/main.cc b/src/main.cc new file mode 100644 index 0000000..e294a1d --- /dev/null +++ b/src/main.cc @@ -0,0 +1,349 @@ +// 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(); +} |