feat: dhcp client implementation

This commit is contained in:
s3lph 2024-12-27 18:56:50 +01:00
commit 1ea86e8c2d
Signed by: s3lph
GPG key ID: 0AA29A52FB33CFB5
5 changed files with 554 additions and 0 deletions

355
SoftDhcp.cpp Normal file
View file

@ -0,0 +1,355 @@
#include "SoftDhcp.h"
#define PORT_BOOTPS 67
#define PORT_BOOTPC 68
const uint32_t DHCP_MAGIC = htonl(0x63825363);
const IPAddress BROADCAST(0xffffffffUL);
SoftDhcp::SoftDhcp() {
this->leaseHandler = nullptr;
this->offerHandler = nullptr;
this->errorHandler = nullptr;
}
void SoftDhcp::initializePacket() {
memset((uint8_t *) &this->packet, 0, sizeof(DhcpPacket));
WiFi.macAddress((uint8_t *) this->packet.chaddr);
this->packet.magic = DHCP_MAGIC;
this->clearOptions();
}
void SoftDhcp::clearOptions() {
this->packet.options[0] = DhcpOption::END;
this->optptr = this->packet.options;
}
bool SoftDhcp::addOption(uint8_t type, uint8_t len, uint8_t *value) {
if (this->optptr + len + 3 >= this->packet.options + 308) { // 3 = type+len+end
return false;
}
*(this->optptr++) = type;
*(this->optptr++) = len;
memcpy(this->optptr, value, len);
this->optptr += len;
*this->optptr = DhcpOption::END;
return true;
}
bool SoftDhcp::addOption(uint8_t type, uint8_t value) {
return this->addOption(type, 1, &value);
}
uint8_t *SoftDhcp::getOption(uint8_t type) {
uint8_t *optptr = this->packet.options;
while (optptr < this->packet.options + 305 && *optptr != DhcpOption::END && *optptr != DhcpOption::PAD) {
if (*optptr == type) {
return optptr;
} else {
uint8_t optlen = *(optptr+1);
optptr += optlen + 2;
}
}
return nullptr;
}
ssize_t SoftDhcp::getOption(uint8_t type, void *buf, size_t len, ssize_t item) {
uint8_t *optptr = this->packet.options;
while (optptr < this->packet.options + 305 && *optptr != DhcpOption::END && *optptr != DhcpOption::PAD) {
uint8_t opt = *(optptr++);
uint8_t optlen = *(optptr++);
if (opt != type) {
optptr += optlen;
continue;
}
if (item < 0) {
if (optlen > len) {
return -2;
} else {
memcpy(buf, optptr, optlen);
return optlen;
}
} else {
if (optlen < len * (item + 1) || optlen % len != 0) {
return -2;
} else {
memcpy(buf, optptr + len * item, len);
return len;
}
}
}
return -1;
}
ssize_t SoftDhcp::getOption(uint8_t type, void *buf, size_t len) {
return this->getOption(type, buf, len, -1);
}
void SoftDhcp::request() {
uint8_t macbuf[7];
if (this->last_xid == 0) {
// Send DHCP DISCOVER
this->initializePacket();
this->request_sent = true;
while ((this->last_xid = random(0xffffffffUL)) == 0);
this->packet.op = 1;
this->packet.htype = 1;
this->packet.hlen = 6;
this->packet.xid = this->last_xid;
this->addOption(DhcpOption::MSG_TYPE, DhcpMessageType::DHCPDISCOVER);
macbuf[0] = 1; // 1 = ethernet
WiFi.macAddress(macbuf+1);
this->addOption(DhcpOption::CLIENT_ID, 7, macbuf);
this->addOption(DhcpOption::PARAMETER_LIST, this->prl_count, this->prl);
this->udp.beginPacket(this->serverIP, PORT_BOOTPS);
this->udp.write((uint8_t *) &this->packet, sizeof(DhcpPacket));
this->udp.endPacket();
} else {
this->getOption(DhcpOption::SERVER_ID, &this->sid.addr, 4);
if (this->packet.yiaddr.addr) {
this->yiaddr.addr = this->packet.yiaddr.addr;
}
// Send DHCP REQUEST
this->initializePacket();
this->request_sent = true;
this->packet.op = 1;
this->packet.htype = 1;
this->packet.hlen = 6;
this->packet.xid = this->last_xid;
this->addOption(DhcpOption::MSG_TYPE, DhcpMessageType::DHCPREQUEST);
macbuf[0] = 1; // 1 = ethernet
WiFi.macAddress(macbuf+1);
this->addOption(DhcpOption::CLIENT_ID, 7, macbuf);
this->addOption(DhcpOption::PARAMETER_LIST, this->prl_count, this->prl);
this->addOption(DhcpOption::SERVER_ID, 4, this->sid.octets);
this->addOption(DhcpOption::ADDRESS_REQUEST, 4, this->yiaddr.octets);
this->udp.beginPacket(this->serverIP, PORT_BOOTPS);
this->udp.write((uint8_t *) &this->packet, sizeof(DhcpPacket));
this->udp.endPacket();
}
}
int32_t SoftDhcp::getTimeoutOption(uint8_t option, int32_t def) {
uint32_t time;
if (this->getOption(option, (uint8_t *) &time, 4) < 0) {
time = def;
} else {
time = ntohl(time);
}
if (time > INT32_MAX) {
time = INT32_MAX;
}
return (int32_t) time;
}
void SoftDhcp::respond() {
if (this->packet.magic != DHCP_MAGIC) {
// not a DHCP packet
return;
}
if (this->packet.xid != this->last_xid) {
// not my DHCP packet
return;
}
uint8_t type;
if (this->getOption(DhcpOption::MSG_TYPE, &type, 1) != 1) {
// Malformed DHCP packet
return;
}
switch (type) {
case DhcpMessageType::DHCPOFFER:
if (this->offerHandler) {
this->offerHandler();
}
this->request();
return;
case DhcpMessageType::DHCPACK:
this->request_sent = false;
this->last_update = millis();
this->clientIP = this->packet.yiaddr.addr;
if (packet.giaddr.addr) {
this->serverIP = this->packet.giaddr.addr;
} else {
this->serverIP = this->packet.siaddr.addr;
}
uint32_t addr;
this->getOption(DhcpOption::SUBNET_MASK, &addr, 4);
this->netmask = addr;
this->getOption(DhcpOption::ROUTER, &addr, 4, 0);
this->routerIP = addr;
this->getOption(DhcpOption::DOMAIN_SERVER, &addr, 4, 0);
this->dnsIP1 = addr;
this->getOption(DhcpOption::DOMAIN_SERVER, &addr, 4, 1);
this->dnsIP2 = addr;
WiFi.config(this->clientIP, this->routerIP, this->netmask, this->dnsIP1, this->dnsIP2);
// Set countdowns
this->renew_countdown = this->getTimeoutOption(DhcpOption::RENEWAL_TIME, 1800);
this->rebind_countdown = this->getTimeoutOption(DhcpOption::REBINDING_TIME, 3600);
this->expire_countdown = this->getTimeoutOption(DhcpOption::ADDRESS_TIME, INT32_MAX);
if (this->leaseHandler) {
this->leaseHandler();
}
return;
default:
this->begin();
if (this->errorHandler) {
this->errorHandler();
}
return;
}
}
void SoftDhcp::requestOption(uint8_t option) {
if (!memchr(this->prl, option, this->prl_count)) {
this->prl[this->prl_count++] = option;
}
}
void SoftDhcp::begin() {
this->request_sent = false;
this->last_update = millis();
this->last_xid = 0;
this->prl_count = 0;
this->requestOption(DhcpOption::SUBNET_MASK);
this->requestOption(DhcpOption::ROUTER);
this->requestOption(DhcpOption::DOMAIN_SERVER);
this->serverIP = BROADCAST;
this->clientIP = BROADCAST;
this->udp.begin(PORT_BOOTPC);
// Disable builtin dhcpc
WiFi.config(BROADCAST, BROADCAST, BROADCAST, 0, 0);
}
void SoftDhcp::update() {
if (this->request_sent) {
size_t len = this->udp.parsePacket();
if (len < 240) {
// not a DHCP packet
return;
}
// TODO check length
this->udp.read((uint8_t *) &this->packet, sizeof(DhcpPacket));
this->respond();
} else {
// Count down renew countdown if more than 1s has passed
uint32_t now = millis();
uint32_t td = (now - this->last_update) / 1000UL;
if (td) {
this->last_update = now;
this->renew_countdown -= td;
this->rebind_countdown -= td;
this->expire_countdown -= td;
}
if (this->rebind_countdown <= 0) {
this->serverIP = BROADCAST;
this->rebind_countdown = INT_MAX;
}
if (this->expire_countdown <= 0) {
this->begin();
if (this->expireHandler) {
this->expireHandler();
}
}
if (this->renew_countdown <= 0) {
this->request();
this->renew_countdown = 60;
}
}
}
IPAddress SoftDhcp::localIP() {
return this->clientIP;
}
IPAddress SoftDhcp::subnetMask() {
return this->netmask;
}
IPAddress SoftDhcp::gatewayIP() {
return this->routerIP;
}
IPAddress SoftDhcp::dnsIP(uint8_t dns_no) {
if (dns_no == 0) {
return this->dnsIP1;
} else if (dns_no == 1) {
return this->dnsIP2;
} else {
return IPAddress(0);
}
}
void SoftDhcp::onLease(void (*handler)()) {
this->leaseHandler = handler;
}
void SoftDhcp::onOffer(void (*handler)()) {
this->offerHandler = handler;
}
void SoftDhcp::onError(void (*handler)()) {
this->errorHandler = handler;
}
void SoftDhcp::onExpire(void (*handler)()) {
this->expireHandler = handler;
}
void SoftDhcp::dump() {
uint8_t type;
this->getOption(DhcpOption::MSG_TYPE, &type, 1);
switch (type) {
case DhcpMessageType::DHCPDISCOVER:
Serial.println("DHCPDISCOVER:");
break;
case DhcpMessageType::DHCPOFFER:
Serial.println("DHCPOFFER:");
break;
case DhcpMessageType::DHCPREQUEST:
Serial.println("DHCPREQUEST:");
break;
case DhcpMessageType::DHCPACK:
Serial.println("DHCPACK:");
break;
case DhcpMessageType::DHCPNAK:
Serial.println("DHCPNAK:");
break;
default:
Serial.println("OTHER:");
break;
}
Serial.printf(" op: %d\r\n", this->packet.op);
Serial.printf(" htype: %d\r\n", this->packet.htype);
Serial.printf(" hlen: %d\r\n", this->packet.hlen);
Serial.printf(" hops: %d\r\n", this->packet.hops);
Serial.printf(" xid: %08x\r\n", this->packet.xid);
Serial.printf(" secs: %d\r\n", this->packet.secs);
Serial.printf(" flags: %d\r\n", this->packet.flags);
Serial.printf(" ciaddr: %d.%d.%d.%d\r\n", this->packet.ciaddr.octets[0], this->packet.ciaddr.octets[1], this->packet.ciaddr.octets[2], this->packet.ciaddr.octets[3]);
Serial.printf(" yiaddr: %d.%d.%d.%d\r\n", this->packet.yiaddr.octets[0], this->packet.yiaddr.octets[1], this->packet.yiaddr.octets[2], this->packet.yiaddr.octets[3]);
Serial.printf(" siaddr: %d.%d.%d.%d\r\n", this->packet.siaddr.octets[0], this->packet.siaddr.octets[1], this->packet.siaddr.octets[2], this->packet.siaddr.octets[3]);
Serial.printf(" giaddr: %d.%d.%d.%d\r\n", this->packet.giaddr.octets[0], this->packet.giaddr.octets[1], this->packet.giaddr.octets[2], this->packet.giaddr.octets[3]);
Serial.printf(" chaddr: %s\r\n", this->packet.chaddr);
Serial.printf(" sname: %s\r\n", this->packet.sname);
Serial.printf(" file: %s\r\n", this->packet.file);
Serial.printf(" magic: %08x\r\n", this->packet.magic);
Serial.println(" options:");
uint8_t *optptr = this->packet.options;
while (optptr < this->packet.options + 305 && *optptr != DhcpOption::END && *optptr != DhcpOption::PAD) {
uint8_t opttype = *(optptr++);
uint8_t optlen = *(optptr++);
Serial.printf(" option %02x length %02x:\r\n ", opttype, optlen);
for (uint8_t i = 0; i < optlen; ++i) {
Serial.printf(" %02x", optptr[i]);
}
Serial.println();
optptr += optlen;
}
}

101
SoftDhcp.h Normal file
View file

@ -0,0 +1,101 @@
#ifndef _SOFT_DHCP_H_
#define _SOFT_DHCP_H_
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include "SoftDhcpConsts.h"
typedef union {
uint8_t octets[4];
uint32_t addr;
} ipaddr;
typedef struct __attribute__ ((packed)) DhcpPacket {
uint8_t op;
uint8_t htype;
uint8_t hlen;
uint8_t hops;
uint32_t xid;
uint16_t secs;
uint16_t flags;
ipaddr ciaddr;
ipaddr yiaddr;
ipaddr siaddr;
ipaddr giaddr;
uint8_t chaddr[16];
uint8_t sname[64];
uint8_t file[128];
uint32_t magic;
uint8_t options[308];
} DhcpPacket;
class SoftDhcp {
private:
DhcpPacket packet;
uint8_t *optptr;
uint8_t prl[64];
uint8_t prl_count;
IPAddress clientIP;
IPAddress serverIP;
IPAddress routerIP;
IPAddress netmask;
IPAddress dnsIP1;
IPAddress dnsIP2;
WiFiUDP udp;
bool request_sent;
uint32_t last_xid;
uint32_t last_update;
ipaddr sid;
ipaddr yiaddr;
int32_t renew_countdown;
int32_t rebind_countdown;
int32_t expire_countdown;
void (*leaseHandler)();
void (*offerHandler)();
void (*errorHandler)();
void (*expireHandler)();
void initializePacket();
void request();
void respond();
void clearOptions();
bool addOption(uint8_t type, uint8_t len, uint8_t *value);
bool addOption(uint8_t type, uint8_t value);
uint8_t *getOption(uint8_t type);
int32_t getTimeoutOption(uint8_t option, int32_t def);
void dump();
public:
SoftDhcp();
void onLease(void (*handler)());
void onOffer(void (*handler)());
void onError(void (*handler)());
void onExpire(void (*handler)());
void begin();
void update();
IPAddress localIP();
IPAddress subnetMask();
IPAddress gatewayIP();
IPAddress dnsIP(uint8_t dns_no = 0);
ssize_t getOption(uint8_t type, void *buf, size_t len);
ssize_t getOption(uint8_t type, void *buf, size_t len, ssize_t item);
void requestOption(uint8_t type);
bool addRequestOption(uint8_t type, uint8_t len, uint8_t *value);
};
#endif // _SOFT_DHCP_H_

31
SoftDhcpConsts.h Normal file
View file

@ -0,0 +1,31 @@
#ifndef _SOFT_DHCP_CONSTS_H_
#define _SOFT_DHCP_CONSTS_H_
enum DhcpMessageType: uint8_t {
DHCPDISCOVER = 1,
DHCPOFFER = 2,
DHCPREQUEST = 3,
DHCPDECLINE = 4,
DHCPACK = 5,
DHCPNAK = 6,
DHCPRELEASE = 7,
DHCPINFORM = 8
};
enum DhcpOption: uint8_t {
PAD = 0,
SUBNET_MASK = 1,
ROUTER = 3,
DOMAIN_SERVER = 6,
ADDRESS_REQUEST = 50,
ADDRESS_TIME = 51,
MSG_TYPE = 53,
SERVER_ID = 54,
PARAMETER_LIST = 55,
RENEWAL_TIME = 58,
REBINDING_TIME = 59,
CLIENT_ID = 61,
END = 255
};
#endif // _SOFT_DHCP_CONSTS_H_

View file

@ -0,0 +1,57 @@
#include <Arduino.h>
#include <SoftDhcp.h>
#define DHCP_DOMAIN_NAME 15
#define DHCP_TIME_SERVERS 4
SoftDhcp dhcp;
void dhcpLease() {
// Requested options can be retrieved like this:
char domainName[64];
// getOption returns the number of bytes retrieved, or a negative
// number if the option is missing or too long for the buffer
ssize_t dnlen = dhcp.getOption(DHCP_DOMAIN_NAME, domainName, 63);
if (dnlen > 0) {
// The domain name in the DNS option is not zero-terminated. You
// need to add your own terminator for strings.
domainName[dnlen] = 0;
Serial.printf("Domain name: %s\r\n", domainName);
}
uint32_t addr;
IPAddress ipa;
// Multi-valued options can be retrieved like this
for (uint8_t i = 0, ssize_t len; (len = dhcp.getOption(DHCP_TIME_SERVERS, &addr, 4, i)) > 0; ++i) {
ipa = addr;
Serial.printf("Time server: %s\r\n", ipa.toString());
}
// Start your network services (e.g. HTTP server/client) here!
}
void dhcpExpire() {
// This function is called when the DHCP lease has expired and the IP
// address has been removed from the interface.
// Stop your network services here!
}
void setup() {
Serial.begin(115200);
// SoftDhcp is event driven; you must register your own callback
// functions for these events (onOffer, onLease, onError, onExpire)
dhcp.onLease(dhcpLease); // Called on DHCPACK
dhcp.onExpire(dhcpExpire); // Called when the lease expires
// SoftDhcp.begin must be called before WiFi.begin so that the builtin
// DHCP client can be disabled.
dhcp.begin();
// You can request additional DHCP options like this:
dhcp.requestOption(DHCP_DOMAIN_NAME);
dhcp.requestOption(DHCP_TIME_SERVERS);
WiFi.begin("ssid", "psk");
}
void loop() {
dhcp.update();
}

10
library.properties Normal file
View file

@ -0,0 +1,10 @@
name=SoftDhcp
version=0.0.1
author=s3lph
maintainer=s3lph <s3lph@kabelsalat.ch>
sentence=Software DHCP client implementation with support for additional options.
paragraph=Software DHCP client implementation with support for additional options.
category=Network
url=https://git.kabelsalat.ch/s3lph/Arduino-Library-SoftDhcp
architectures=*
includes=SoftDhcp.h