aboutsummaryrefslogtreecommitdiff
// 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();
}