aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format104
-rw-r--r--.gitignore5
-rw-r--r--LICENSE21
-rw-r--r--Makefile20
-rw-r--r--README.md141
-rw-r--r--doc/dhcp.txt134
-rw-r--r--lib/dhcp/dhcp.cc36
-rw-r--r--lib/dhcp/dhcp.h116
-rw-r--r--lib/dhcp/lease_db.h111
-rw-r--r--lib/dhcp/types.h14
-rw-r--r--lib/dhcp/utils.h31
-rw-r--r--platformio.ini20
-rw-r--r--src/main.cc349
-rw-r--r--test/native/dhcp.cc65
-rw-r--r--test/native/hash.cc71
-rw-r--r--test/native/lease_db.cc85
16 files changed, 1323 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..cc75bce
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,104 @@
+# dotfiles -- clang-format
+# author: johannst
+# doc : https://clang.llvm.org/docs/ClangFormatStyleOptions.html
+
+Language: Cpp
+Standard: Auto
+
+AccessModifierOffset: -2
+AlignAfterOpenBracket: Align
+AlignConsecutiveMacros: true
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignEscapedNewlines: Left
+AlignOperands: true
+AlignTrailingComments: true
+AllowAllArgumentsOnNextLine: true
+AllowAllConstructorInitializersOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: Inline
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: true
+AlwaysBreakTemplateDeclarations: Yes
+BinPackArguments: true
+BinPackParameters: true
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: false
+ AfterControlStatement: false
+ AfterEnum: false
+ AfterFunction: false
+ AfterNamespace: false
+ AfterStruct: false
+ AfterUnion: false
+ AfterExternBlock: false
+ BeforeCatch: false
+ BeforeElse: false
+ IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyRecord: true
+ SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializers: AfterColon
+BreakConstructorInitializersBeforeComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeInheritanceComma: false
+BreakStringLiterals: true
+ColumnLimit: 140
+CompactNamespaces: true
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 2
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+DisableFormat: false
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+IncludeBlocks: Preserve
+# Could use `IncludeCategories` to define rules for includes
+IndentCaseLabels: true
+IndentPPDirectives: AfterHash
+IndentWidth: 4
+IndentWrappedFunctionNames: false
+KeepEmptyLinesAtTheStartOfBlocks: false
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 2
+NamespaceIndentation: All
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 1
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyReturnTypeOnItsOwnLine: 200
+PointerAlignment: Left
+ReflowComments: true
+SortIncludes: true
+SortUsingDeclarations: true
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: false
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: ControlStatements
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 2
+SpacesInAngles: false
+SpacesInCStyleCastParentheses: false
+SpacesInContainerLiterals: true
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+TabWidth: 4
+UseTab: Never
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..daa6f43
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.cache
+.ccls
+blob
+compile_commands.json
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7c153e5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Johannes Stoelp
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..203276a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+# Copyright (c) 2022 Johannes Stoelp
+
+build:
+ pio run
+
+run:
+ pio run -e nodemcuv2 -t upload && pio device monitor -b 115200
+
+check:
+ pio test
+
+clean:
+ pio run -t clean
+
+help:
+ @echo "Targets:"
+ @echo " build - Build project."
+ @echo " run - Build & flash project and attach serial monitor."
+ @echo " check - Run tests."
+ @echo " clean - Clean project."
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5b1bb29
--- /dev/null
+++ b/README.md
@@ -0,0 +1,141 @@
+# pio-nodemcuv2-dhcp-server
+
+This repository implements a minimal [dhcp server][dhcp] on the [nodemcu v2]
+board using the [arduino] framework and the [PlatformIO][pio] build
+environment.
+
+
+## DHCP protocol
+
+All details of the dhcp protocol required for this implementation can be found
+in the following two RFCs
+- [rfc2131]: Dynamic Host Configuration Protocol
+- [rfc2132]: DHCP Options and BOOTP Vendor Extensions
+
+## Run
+
+Install PlatformIO core following the [installation guide][pio-install].
+
+```shell
+# Build the project.
+pio run
+
+# Run the test (only added host native tests).
+pio test
+
+# ... Attach nodemcu v2 board via USB.
+
+# Build and flash the embedded software.
+pio run -e nodemcuv2 -t upload
+
+# Connect to the serial monitor.
+pio device monitor -b 115200
+```
+
+> Check the [Makefile](Makefile) for reference.
+
+## Configuration
+
+The wifi client and dhcp server are currently configured with the following
+global variables in [main.cc](src/main.cc).
+
+```cpp
+/// -- 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 */
+```
+
+## Why all this?
+
+My ultimate goal was to setup a **guest wifi** to isolate my home network while
+offering wifi to my guests.
+For that I planned to use of the baked in guest wifi feature of the router I
+got from my ISP.
+
+In my home network I am running [pi-hole] for blocking ads and trackers, which
+works by blocking those services on the DNS level. For this to function, all
+the devices in my home network need to use the pi-hole node as DNS server.
+This is typically done by letting the dhcp server distribute the DNS server to
+use (so it does not need to be configured on each device).
+Because the built-in dhcp server in the ISP router doesn't allow to configure
+the DNS server I disabled that one and run the optional dhcp server which comes
+with pi-hole.
+
+With that setup as starting point, one problem arises when using the guest wifi
+on the ISP router. Guest devices connected to the guest wifi don't see a dhcp
+server and hence don't get an IP address assigned. In turn, the setup is
+useless as I don't want guests to statically configure an IP address :^)
+
+Therefore the plan was to write a minimal dhcp server running on a [nodemcu v2]
+which would then linger in the guest wifi and distribute IP addresses to my
+guests.
+
+> Why a nodemcu v2? Because it has an wireless antenna and I had one laying
+> around collecting dust.
+
+So all in all my home setup would look something like the following.
+
+```
+ Home +-----------+ Guest +---------------+
+ Wifi (((-| ISP modem |-))) Wifi (((-| nodemcuv2 |
+ | & router | | (dhcp server) |
+ +-----+-----+ +---------------+
+ |
+ +-------+-------+
+ | pihole |
+ | (dhcp server) |
+ +---------------+
+```
+
+## End of the story ...
+
+... I read the RFCs, developed & tested the dhcp server in my home network and
+then learned that the guests are isolated and I couldn't disable that isolation
+in the ISP router.
+
+At the end I couldn't deploy the guest wifi as planned but I had fun
+implementing the dhcp server and I guess learned something :^)
+
+For now I switched over to offering guest wifi by running an access point on
+the raspberry pi spanning a new sub-net and then doing NAT and iptables
+isolation to protect my home network. It's not ideal but it's sufficient for
+now.
+
+```
+ Home +-----------+
+ Wifi (((-| ISP modem |
+ | & router |
+ +-----+-----+
+ |
+ +-------+-------+----------+ Guest
+ | pihole | NAT |-))) Wifi
+ | (dhcp server) | iptables |
+ +---------------+----------+
+```
+
+## License
+
+This project is licensed under the [MIT](LICENSE) license.
+
+[dhcp]: https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol
+[rfc2131]: https://datatracker.ietf.org/doc/html/rfc2131
+[rfc2132]: https://datatracker.ietf.org/doc/html/rfc2132
+[pio]: https://platformio.org
+[pio-install]: https://docs.platformio.org/en/latest//core/installation.html
+[arduino]: https://docs.platformio.org/en/latest/frameworks/arduino.html
+[pi-hole]: https://pi-hole.net
+[nodemcu v2]: https://www.az-delivery.de/en/products/nodemcu
diff --git a/doc/dhcp.txt b/doc/dhcp.txt
new file mode 100644
index 0000000..686137b
--- /dev/null
+++ b/doc/dhcp.txt
@@ -0,0 +1,134 @@
+dhcp protocol: https://datatracker.ietf.org/doc/html/rfc2131
+dhcp options : https://datatracker.ietf.org/doc/html/rfc2132
+
+dhcp message:
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | op (1) | htype (1) | hlen (1) | hops (1) |
+ +---------------+---------------+---------------+---------------+
+ | xid (4) |
+ +-------------------------------+-------------------------------+
+ | secs (2) | flags (2) |
+ +-------------------------------+-------------------------------+
+ | ciaddr (4) |
+ +---------------------------------------------------------------+
+ | yiaddr (4) |
+ +---------------------------------------------------------------+
+ | siaddr (4) |
+ +---------------------------------------------------------------+
+ | giaddr (4) |
+ +---------------------------------------------------------------+
+ | |
+ | chaddr (16) |
+ | |
+ | |
+ +---------------------------------------------------------------+
+ | |
+ | sname (64) |
+ +---------------------------------------------------------------+
+ | |
+ | file (128) |
+ +---------------------------------------------------------------+
+ | |
+ | options (variable) |
+ +---------------------------------------------------------------+
+
+dhcp message flags field:
+
+ 1 1 1 1 1 1
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ |B| MBZ |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+ B: BROADCAST flag
+ MBZ: MUST BE ZERO (reserved for future use)
+
+dhcp message field description:
+
+ FIELD OCTETS DESCRIPTION
+ ----- ------ -----------
+ op 1 Message op code / message type.
+ 1 = BOOTREQUEST, 2 = BOOTREPLY
+ htype 1 Hardware address type, see ARP section in "Assigned
+ Numbers" RFC; e.g., '1' = 10mb ethernet.
+ hlen 1 Hardware address length (e.g. '6' for 10mb
+ ethernet).
+ hops 1 Client sets to zero, optionally used by relay agents
+ when booting via a relay agent.
+ xid 4 Transaction ID, a random number chosen by the
+ client, used by the client and server to associate
+ messages and responses between a client and a
+ server.
+ secs 2 Filled in by client, seconds elapsed since client
+ began address acquisition or renewal process.
+ flags 2 Flags ([0]: broadcast flag).
+ ciaddr 4 Client IP address; only filled in if client is in
+ BOUND, RENEW or REBINDING state and can respond
+ to ARP requests.
+ yiaddr 4 'your' (client) IP address.
+ siaddr 4 IP address of next server to use in bootstrap;
+ returned in DHCPOFFER, DHCPACK by server.
+ giaddr 4 Relay agent IP address, used in booting via a
+ relay agent.
+ chaddr 16 Client hardware address.
+ sname 64 Optional server host name, null terminated string.
+ file 128 Boot file name, null terminated string; "generic"
+ name or null in DHCPDISCOVER, fully qualified
+ directory-path name in DHCPOFFER.
+ options var Optional parameters field. See the options
+ documents for a list of defined options.
+
+- A DHCP server always returns its own address in the 'server identifier'
+ option.
+
+- Client must accept DHCP of at least 576 octets (312 octects of option)
+
+- Time values (eg lease) are relative times in seconds and should be treated
+ relative wo each nodes clock.
+
+- Services:
+ - Parameter repository (2.1), used for persistent storage of network
+ parameters.
+ - Dynamic nw address allocation (2.2)
+
+- Protocol
+ - op == BOOTREQUEST client -> server message
+ - op == BOOTREPLY server -> client message
+ - options[0:4] -> magic cookie {0x63, 0x82, 0x53, 0x63}
+ - option (53 - DHCP Message Type) required in all dhcp messages
+
+ 1. Client broadcasts DHCPDISCOVER (53).
+ 2. Server respond with DHCPOFFER (53).
+ - yiaddr -> allocated address
+ - additional options
+ 3. Client broadcasts DHCPREQUEST (53).
+ - must include 'server identifier' option
+ - 'requested ip address' option must be set to 'yiaddr' offerened in 2.
+ 4. Server sends DHCPACK (53) to client.
+ - fill 'yiaddr' with allocated address
+ - add configuration parameters
+ 5. Client may send DHCPDECLINE (53) if it detects addr already in use.
+ 6. Client may send DHCPRELEASE (53).
+
+- Constructing dhcp message
+ - variable options must end with 'end (255)' option
+ - client -> server udp port 67
+ - server -> client udp port 68
+ - @server (msg received by client)
+ - giaddr !=0 -> using BOOTP relay -> send resp to @giaddr:67
+ - giaddr == 0 && ciaddr != 0 -> send resp to @ciaddr:68
+ - giaddr == 0 && ciaddr == 0 && flags.b == 1 -> send resp to broadcast 0xffff_ffff
+ - giaddr == 0 && ciaddr == 0 && flags.b == 0 -> send resp to client hw addr + yiaddr
+
+- Options:
+ - 'maximum DHCP message size'
+ - Client may negotiate lager dhcp messages.
+ - 'server identifier'
+ - A DHCP server always returns its own address in the option.
+ - 'client identifier'
+ - Client may pass explicit client identifier to server.
+ - 'DHCP message type' (REQUIRED)
+ - type of dhcp message
diff --git a/lib/dhcp/dhcp.cc b/lib/dhcp/dhcp.cc
new file mode 100644
index 0000000..1e5f785
--- /dev/null
+++ b/lib/dhcp/dhcp.cc
@@ -0,0 +1,36 @@
+// Copyright (c) 2022 Johannes Stoelp
+
+#include "dhcp.h"
+#include "utils.h"
+
+std::optional<struct option_view> get_option(const u8* opt, usize len, dhcp_option search_tag) {
+ const u8* end = opt + len;
+
+ while (opt < end) {
+ // Get tag of current option.
+ dhcp_option tag = from_raw<dhcp_option>(*opt++);
+
+ if (tag == dhcp_option::END) {
+ break;
+ }
+
+ // Extract length of current option.
+ usize len = *opt++;
+
+ if (tag == search_tag) {
+ if (static_cast<usize>(end - opt) < len) {
+ // If length is malformed.
+ return std::nullopt;
+ } else {
+ // Option found.
+ return {{opt, len}};
+ }
+ }
+
+ // Advance option iterator to beginning of next option.
+ opt = opt + len;
+ }
+
+ // Option not found.
+ return std::nullopt;
+}
diff --git a/lib/dhcp/dhcp.h b/lib/dhcp/dhcp.h
new file mode 100644
index 0000000..ce904f0
--- /dev/null
+++ b/lib/dhcp/dhcp.h
@@ -0,0 +1,116 @@
+// Copyright (c) 2022 Johannes Stoelp
+//
+// dhcp protocol: https://datatracker.ietf.org/doc/html/rfc2131
+// dhcp options : https://datatracker.ietf.org/doc/html/rfc2132
+
+#ifndef DHCP_H
+#define DHCP_H
+
+#include "types.h"
+
+#include <optional>
+
+// -- Global constants.
+
+constexpr u16 DHCP_SERVER_PORT = 67;
+constexpr u16 DHCP_CLIENT_PORT = 68;
+
+constexpr usize DHCP_MESSAGE_LEN = 576;
+constexpr usize DHCP_MESSAGE_MIN_LEN = 243;
+
+constexpr u32 DHCP_OPTION_COOKIE = 0x63538263;
+
+enum class dhcp_operation : u8 {
+ BOOTREQUEST = 1,
+ BOOTREPLY,
+};
+
+enum class dhcp_option : u8 {
+ PAD = 0,
+ END = 255,
+
+ // Vendor Extensions.
+ SUBNET_MASK = 1,
+ ROUTER = 3,
+ DNS = 6,
+
+ // IP Layer Parameters per Interface.
+ BROADCAST_ADDR = 28,
+
+ // DHCP Extensions.
+ REQUESTED_IP = 50,
+ IP_ADDRESS_LEASE_TIME,
+ OPTION_OVERLOAD,
+ DHCP_MESSAGE_TYPE,
+ SERVER_IDENTIFIER,
+ PARAMETER_REQUEST_LIST,
+ MESSAGE,
+ MAX_DHCP_MESSAGE_SIZE,
+ RENEWAL_TIME_T1,
+ REBINDING_TIME_T2,
+ CLASS_ID,
+ CLIENT_ID,
+};
+
+// -- DHCP message.
+
+enum class dhcp_message_type : u8 {
+ DHCP_DISCOVER = 1,
+ DHCP_OFFER,
+ DHCP_REQUEST,
+ DHCP_DECLINE,
+ DHCP_ACK,
+ DHCP_NAK,
+ DHCP_RELEAE,
+};
+
+struct dhcp_message {
+ dhcp_operation op;
+ u8 htype;
+ u8 hlen;
+ u8 hops;
+ u32 xid;
+ u16 secs;
+ u16 flags;
+ u32 ciaddr;
+ u32 yiaddr;
+ u32 siaddr;
+ u32 giaddr;
+ u8 chaddr[16];
+ u8 sname[64];
+ u8 file[128];
+ u32 cookie;
+ u8 options[312 - 4 /* cookie len */];
+} __attribute__((packed));
+
+static_assert(DHCP_MESSAGE_MIN_LEN == offsetof(dhcp_message, options) + 3 /* DHCP_MESSAGE_TYPE size */,
+ "Declare min size as dhcp_message with the DHCP_MESSAGE_TYPE option.");
+
+// -- DHCP Utilities.
+
+struct option_view {
+ const u8* data;
+ usize len;
+};
+
+// Search for the option with tag 'search_tag' in the options 'opt' of length 'len'.
+std::optional<struct option_view> get_option(const u8* opt, usize len, dhcp_option search_tag);
+
+template<typename T>
+constexpr T get_opt_val(const u8* opt_data) {
+ T ret = 0;
+ for (size_t i = 0; i < sizeof(T); ++i) {
+ ret = (ret << 8) | *opt_data++;
+ }
+ return ret;
+}
+
+template<typename T>
+u8* put_opt_val(u8* opt_data, T val) {
+ for (size_t i = sizeof(T); i; --i) {
+ *opt_data++ = (val >> ((i - 1) * 8)) & 0xff;
+ }
+ return opt_data;
+}
+
+#endif
diff --git a/lib/dhcp/lease_db.h b/lib/dhcp/lease_db.h
new file mode 100644
index 0000000..86b959f
--- /dev/null
+++ b/lib/dhcp/lease_db.h
@@ -0,0 +1,111 @@
+// Copyright (c) 2022 Johannes Stoelp
+
+#ifndef LEASE_DB_H
+#define LEASE_DB_H
+
+#include "types.h"
+
+#include <array>
+#include <optional>
+
+struct lease {
+ u32 client_hash;
+ usize lease_end;
+};
+
+// Lease database, for managing client leases, which includes
+// - allocation of new leases
+// - lookup of existing leases
+// - update of existing leases
+// - flushing of expired leases
+//
+// The database supports 'LEASE' number of clients.
+//
+// The lease APIs return an idx which represents the allocated client lease and
+// should be interpreted as offset to the start address of the dhcp address
+// range.
+//
+// For example, the dhcp server starts to assing addresses starting with
+// 10.0.0.100, and the lease database returns idx=4, this would represent the
+// allocated client address 10.0.0.104.
+template<usize LEASES>
+class lease_db {
+ public:
+ constexpr lease_db() = default;
+
+ lease_db(const lease_db&) = delete;
+ lease_db& operator=(const lease_db&) = delete;
+
+ // Try to allocate a new client lease for the client represented by
+ // 'client_hash'.
+ //
+ // If a lease could be allocated return the corresponding idx.
+ //
+ // If a lease for the client already exist or all leases are allocated,
+ // return nullopt.
+ //
+ // 'lease_end' sets the expiration time of the lease (should be absolute time).
+ std::optional<usize> new_lease(u32 client_hash, usize lease_end) {
+ if (get_lease(client_hash)) {
+ return std::nullopt;
+ }
+
+ for (usize l = 0; client_hash != 0 && l < LEASES; ++l) {
+ if (leases[l].client_hash == 0) {
+ leases[l].client_hash = client_hash;
+ leases[l].lease_end = lease_end;
+ return l;
+ }
+ }
+ return std::nullopt;
+ }
+
+ // Try to get the lease for the client if it exists.
+ std::optional<usize> get_lease(u32 client_hash) const {
+ for (usize l = 0; client_hash != 0 && l < LEASES; ++l) {
+ if (leases[l].client_hash == client_hash) {
+ return l;
+ }
+ }
+ return std::nullopt;
+ }
+
+ // Update expiration time for client if the client has an allocated lease.
+ // Similar to 'new_lease' the 'lease_end' should be an absolute time value.
+ bool update_lease(u32 client_hash, usize lease_end) {
+ for (usize l = 0; client_hash != 0 && l < LEASES; ++l) {
+ if (leases[l].client_hash == client_hash) {
+ leases[l].lease_end = lease_end;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Check for expired leases and free them accordingly.
+ // 'curr_time' should be the current time as absolute time value.
+ void flush_expired(usize curr_time) {
+ for (lease& l : leases) {
+ if (l.lease_end <= curr_time) {
+ l.client_hash = 0;
+ l.lease_end = 0;
+ }
+ }
+ }
+
+ // Get the number of active leases.
+ usize active_leases() const {
+ usize cnt = 0;
+ for (const lease& l : leases) {
+ if (l.client_hash != 0) {
+ ++cnt;
+ }
+ }
+ return cnt;
+ }
+
+ private:
+ std::array<lease, LEASES> leases = {0, 0};
+};
+
+#endif
diff --git a/lib/dhcp/types.h b/lib/dhcp/types.h
new file mode 100644
index 0000000..049de0f
--- /dev/null
+++ b/lib/dhcp/types.h
@@ -0,0 +1,14 @@
+// Copyright (c) 2022 Johannes Stoelp
+
+#ifndef TYPES_H
+#define TYPES_H
+
+#include <cstddef>
+#include <cstdint>
+
+using u8 = uint8_t;
+using u16 = uint16_t;
+using u32 = uint32_t;
+using usize = size_t;
+
+#endif
diff --git a/lib/dhcp/utils.h b/lib/dhcp/utils.h
new file mode 100644
index 0000000..9016007
--- /dev/null
+++ b/lib/dhcp/utils.h
@@ -0,0 +1,31 @@
+// Copyright (c) 2022 Johannes Stoelp
+
+#ifndef UTILS_H
+#define UTILS_H
+
+#include <type_traits>
+
+// Convert from an underlying enum type into an enum variant.
+template<typename E>
+constexpr E from_raw(std::underlying_type_t<E> u) {
+ static_assert(std::is_enum_v<E>);
+ return static_cast<E>(u);
+}
+
+// Convert from an enum variant into an underlying enum type.
+template<typename E>
+constexpr std::underlying_type_t<E> into_raw(E e) {
+ static_assert(std::is_enum_v<E>);
+ return static_cast<std::underlying_type_t<E>>(e);
+}
+
+// Simple cyclic rotation hash function.
+constexpr u32 hash(const u8* data, usize len) {
+ u32 hash = 0xa5a55a5a /* seed */;
+ for (usize i = 0; i < len; ++i) {
+ hash += ((hash << 25) | (hash >> 7) /* rrot(7) */) ^ data[i];
+ }
+ return hash;
+}
+
+#endif
diff --git a/platformio.ini b/platformio.ini
new file mode 100644
index 0000000..4fa5c0f
--- /dev/null
+++ b/platformio.ini
@@ -0,0 +1,20 @@
+; Copyright (c) 2022 Johannes Stoelp
+
+; Build embedded target.
+[env:nodemcuv2]
+platform = espressif8266
+board = nodemcuv2
+framework = arduino
+build_flags = -Wextra
+; Ignore tests in test/native for this target.
+test_ignore = native
+
+; Build host native target.
+[env:native]
+platform = native
+targets = test
+lib_deps = google/googletest@^1.10.0
+build_flags = -lpthread -lgtest_main
+; Turn off compat mode.
+; https://community.platformio.org/t/googletest-problem-with-compilation-process/12048/12
+lib_compat_mode = off
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();
+}
diff --git a/test/native/dhcp.cc b/test/native/dhcp.cc
new file mode 100644
index 0000000..983c6c8
--- /dev/null
+++ b/test/native/dhcp.cc
@@ -0,0 +1,65 @@
+// Copyright (c) 2022 Johannes Stoelp
+
+#include <dhcp.h>
+#include <utils.h>
+
+#include <gtest/gtest.h>
+
+TEST(option, get_available_opt) {
+ u8 opts[] = {
+ into_raw(dhcp_option::PAD), into_raw(dhcp_option::PAD), into_raw(dhcp_option::DHCP_MESSAGE_TYPE), 5 /* len */, 0, 1, 2, 3, 4,
+ into_raw(dhcp_option::END),
+ };
+
+ auto ov = get_option(opts, sizeof(opts), dhcp_option::DHCP_MESSAGE_TYPE);
+
+ ASSERT_EQ(true, ov.has_value());
+ ASSERT_EQ(0, ov.value().data[0]);
+ ASSERT_EQ(1, ov.value().data[1]);
+ ASSERT_EQ(2, ov.value().data[2]);
+ ASSERT_EQ(3, ov.value().data[3]);
+ ASSERT_EQ(4, ov.value().data[4]);
+ ASSERT_EQ(5, ov.value().len);
+}
+
+TEST(option, maleformed_len) {
+ u8 opts[] = {
+ into_raw(dhcp_option::PAD), into_raw(dhcp_option::PAD), into_raw(dhcp_option::DHCP_MESSAGE_TYPE), 5 /* len */, 0,
+ into_raw(dhcp_option::END),
+ };
+
+ {
+ auto ov = get_option(opts, sizeof(opts), dhcp_option::DHCP_MESSAGE_TYPE);
+
+ ASSERT_EQ(false, ov.has_value());
+ }
+ {
+ auto ov = get_option(opts, sizeof(opts), dhcp_option::CLASS_ID);
+
+ ASSERT_EQ(false, ov.has_value());
+ }
+}
+
+TEST(option, maleformed_missing_end) {
+ u8 opts[] = {
+ into_raw(dhcp_option::PAD),
+ into_raw(dhcp_option::PAD),
+ };
+
+ auto ov = get_option(opts, sizeof(opts), dhcp_option::DHCP_MESSAGE_TYPE);
+
+ ASSERT_EQ(false, ov.has_value());
+}
+
+TEST(option, put_opt_val) {
+ u8 options[8] = {0};
+ u32 val = 0xdeadbeef;
+
+ u8* nextp = put_opt_val(options, val);
+
+ ASSERT_EQ(options + 4, nextp);
+ ASSERT_EQ(0xde, options[0]);
+ ASSERT_EQ(0xad, options[1]);
+ ASSERT_EQ(0xbe, options[2]);
+ ASSERT_EQ(0xef, options[3]);
+} \ No newline at end of file
diff --git a/test/native/hash.cc b/test/native/hash.cc
new file mode 100644
index 0000000..8aa0a35
--- /dev/null
+++ b/test/native/hash.cc
@@ -0,0 +1,71 @@
+// Copyright (c) 2022 Johannes Stoelp
+
+#include <types.h>
+#include <utils.h>
+
+#include <filesystem>
+#include <fstream>
+#include <gtest/gtest.h>
+#include <iostream>
+#include <vector>
+
+using ID = std::array<u8, 6>;
+namespace fs = std::filesystem;
+
+std::vector<ID> read_blob() {
+ if (!fs::exists("blob")) {
+ std::system("dd if=/dev/urandom of=blob count=16 bs=1M");
+ }
+
+ std::vector<ID> ids;
+ ids.reserve(fs::file_size("blob") / sizeof(ID));
+
+ auto ifs = std::ifstream("blob");
+ assert(ifs.is_open());
+ ID id;
+ while (ifs.read((char*)id.data(), sizeof(id))) {
+ ids.push_back(id);
+ }
+
+ return ids;
+}
+
+TEST(hash, uniform_distribuation) {
+ constexpr usize BUCKETS = 64;
+ constexpr float BUCKET_SIZE = static_cast<float>(100) / BUCKETS; // Bucket size in percent.
+ constexpr float BUCKET_ERR = BUCKET_SIZE * 0.05 /* 5% */; // Allowed distribution error.
+
+ const auto ids = read_blob();
+
+ usize cnt[BUCKETS] = {0};
+ for (const auto& id : ids) {
+ u32 h = hash(id.data(), id.size());
+ cnt[h % BUCKETS] += 1;
+ }
+
+ for (usize b = 0; b < BUCKETS; ++b) {
+ const float dist = static_cast<float>(cnt[b]) / ids.size() * 100;
+ ASSERT_GT(dist, BUCKET_SIZE - BUCKET_ERR);
+ ASSERT_LT(dist, BUCKET_SIZE + BUCKET_ERR);
+ // printf("bucket %2ld: %5.2f (%ld)\n", b, dist, cnt[b]);
+ }
+}
+
+TEST(hash, DISABLED_collisions) {
+ const auto ids = read_blob();
+
+ std::unordered_map<u32, usize> hits;
+ for (const auto& id : ids) {
+ u32 h = hash(id.data(), id.size());
+ hits[h] = hits[h] + 1;
+ }
+
+ usize collisions = 0;
+ for (const auto& hit : hits) {
+ if (hit.second > 1) {
+ ++collisions;
+ }
+ }
+
+ printf("Hashed %ld values got %ld collisions\n", ids.size(), collisions);
+}
diff --git a/test/native/lease_db.cc b/test/native/lease_db.cc
new file mode 100644
index 0000000..8f87912
--- /dev/null
+++ b/test/native/lease_db.cc
@@ -0,0 +1,85 @@
+// Copyright (c) 2022 Johannes Stoelp
+
+#include <lease_db.h>
+
+#include <gtest/gtest.h>
+
+TEST(lease_db, null_client_hash) {
+ lease_db<2> db;
+
+ ASSERT_EQ(std::nullopt, db.new_lease(0, 0));
+ ASSERT_EQ(std::nullopt, db.get_lease(0));
+ ASSERT_EQ(0, db.active_leases());
+}
+
+TEST(lease_db, get_new_lease) {
+ lease_db<2> db;
+
+ ASSERT_EQ(std::optional(0), db.new_lease(10, 0));
+ ASSERT_EQ(std::optional(1), db.new_lease(20, 0));
+ ASSERT_EQ(std::nullopt, db.new_lease(30, 0)); // exhausted
+
+ ASSERT_EQ(std::optional(0), db.get_lease(10));
+ ASSERT_EQ(std::optional(1), db.get_lease(20));
+ ASSERT_EQ(std::nullopt, db.get_lease(30)); // exhausted
+
+ ASSERT_EQ(2, db.active_leases());
+}
+
+TEST(lease_db, get_new_lease_twice) {
+ lease_db<2> db;
+
+ ASSERT_EQ(std::optional(0), db.new_lease(10, 0));
+ ASSERT_EQ(std::nullopt, db.new_lease(10, 0));
+
+ ASSERT_EQ(1, db.active_leases());
+}
+
+TEST(lease_db, flush_expired) {
+ lease_db<2> db;
+
+ ASSERT_EQ(std::optional(0), db.new_lease(10, 100 /* lease end */));
+ ASSERT_EQ(std::optional(1), db.new_lease(20, 200 /* lease end */));
+
+ db.flush_expired(50 /* current time */);
+ ASSERT_EQ(2, db.active_leases());
+ ASSERT_EQ(std::optional(0), db.get_lease(10));
+ ASSERT_EQ(std::optional(1), db.get_lease(20));
+
+ db.flush_expired(150 /* current time */);
+ ASSERT_EQ(1, db.active_leases());
+ ASSERT_EQ(std::nullopt, db.get_lease(10));
+ ASSERT_EQ(std::optional(1), db.get_lease(20));
+
+ db.flush_expired(250 /* current time */);
+ ASSERT_EQ(0, db.active_leases());
+ ASSERT_EQ(std::nullopt, db.get_lease(10));
+ ASSERT_EQ(std::nullopt, db.get_lease(20));
+}
+
+TEST(lease_db, update_lease) {
+ lease_db<2> db;
+
+ ASSERT_EQ(std::optional(0), db.new_lease(10, 100 /* lease end */));
+ ASSERT_EQ(std::optional(1), db.new_lease(20, 200 /* lease end */));
+
+ ASSERT_EQ(2, db.active_leases());
+
+ db.flush_expired(150 /* current time */);
+ ASSERT_EQ(1, db.active_leases());
+ ASSERT_EQ(std::nullopt, db.get_lease(10));
+ ASSERT_EQ(std::optional(1), db.get_lease(20));
+
+ ASSERT_EQ(false, db.update_lease(10, 300 /* lease end */));
+ ASSERT_EQ(true, db.update_lease(20, 300 /* lease end */));
+
+ db.flush_expired(250 /* current time */);
+ ASSERT_EQ(1, db.active_leases());
+ ASSERT_EQ(std::nullopt, db.get_lease(10));
+ ASSERT_EQ(std::optional(1), db.get_lease(20));
+
+ db.flush_expired(350 /* current time */);
+ ASSERT_EQ(0, db.active_leases());
+ ASSERT_EQ(std::nullopt, db.get_lease(10));
+ ASSERT_EQ(std::nullopt, db.get_lease(20));
+}