aboutsummaryrefslogtreecommitdiff
path: root/src/main.cc
diff options
context:
space:
mode:
authorJohannes Stoelp <johannes.stoelp@gmail.com>2022-01-14 23:51:05 +0100
committerJohannes Stoelp <johannes.stoelp@gmail.com>2022-01-14 23:51:05 +0100
commitf9928a1a08c57fe853888119a996c3acc98ee09d (patch)
treec9770b76ffcc281da141f3aa2c595600372c0fca /src/main.cc
downloadpio-nodemcuv2-dhcp-server-main.tar.gz
pio-nodemcuv2-dhcp-server-main.zip
Initial version of nodemcuv2 dhcp serverHEADmain
Able to offer IP address + DNS/Gateway ... Worked with devices at my hand.
Diffstat (limited to 'src/main.cc')
-rw-r--r--src/main.cc349
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();
+}